Vue.js使用vue-router构建单页应用

Posted by

你一定是闲得蛋疼才重构的吧

2018/07/25 · 基础技术 ·
重构

原文出处: 奇舞团 –
hxl   

随着“发布”进度条走到100%,重构的代码终于上线了。我露出了老母亲般的围笑……

最近看了一篇文章,叫《史上最烂的开发项目长啥样:苦撑12年,600多万行代码》,讲的是法国的一个软件项目,因为各种奇葩的原因,导致代码质量惨不忍睹,项目多年无法交付,最终还有公司领导入狱。里面有一些细节让人哭笑不得:一个右键响应事件需要花45分钟;读取700MB的数据,需要花7天时间。足见这个软件的性能有多糟心。

如果让笔者来接手这“坨”代码,内心早就飘过无数个敏感词。其实,笔者自己也维护着一套陈酿了将近7年的代码,随着后辈的添油加醋……哦不,添砖加瓦,功能逻辑日益复杂,代码也变得臃肿,维护起来步履维艰,性能也不尽如人意。终于有一天,我听见了内心的魔鬼在呼唤:“重构吧~~”

重构是一件磨人的事情,轻易使不得。好在兄弟们齐心协力,各方资源也配合到位。我们小步迭代了大半年,最后一鼓作气,终于完成了。今天跟大家分享一下这次重构的经验和收益。

  • Vue2简单入门
  • Vue.js再入门
  • Vue.js使用vue-router构建单页应用
  • Vue.js状态管理工具Vuex快速上手

挑战

此次重构的对象是一个大型单页应用。它实现了云端文件管理功能,共有10个路由页面,涉及文件上传、音视频播放、图片预览、套餐购买等几十个功能。前端使用QWrap、jQuery、RequireJS搭建,HTML使用PHP模板引擎Smarty编写。

我们选择了Vue.js、vue-router、vuex来改造代码,用webpack完成模块打包的工作。仿佛一下子从原始社会迈向了新世纪,是不是很完美?

图片 1

(图片来自网络)

由于项目比较庞大,为了快速迭代,重构的过渡期允许新旧代码并存,开发完一部分就测试上线一部分,直到最终完全替代旧代码。

然鹅,我们很快就意识到一个问题:重构部分跟新增需求无法保证一致。比如重构到一半,线上功能变了……产品不会等重构完再往前发展。难不成要在新老代码中并行迭代相同的需求?

别慌,一定能想出更高效的解决办法。稍微分析一下,发现我们要处理三种情况:

1. 产品需要新增一个功能。比如一个活动弹窗或路由页面。

解决方法:新功能用vue组件实现,然后手动加载到页面。比如:

JavaScript

const wrap = document.createElement(‘div’)
document.body.appendChild(wrap) new Vue({ el: wrap, template: ‘<App
/>’, components: { App } })

1
2
3
4
5
6
7
const wrap = document.createElement(‘div’)
document.body.appendChild(wrap)
new Vue({
  el: wrap,
  template: ‘<App />’,
  components: { App }
})

如果这个组件必须跟老代码交互,就将组件暴露给全局变量,然后由老代码调用全局变量的方法。比如:

JavaScript

// someApp.js window.someApp = new Vue({ … methods: { funcA() { // do
somthing } } })

1
2
3
4
5
6
7
8
9
// someApp.js
window.someApp = new Vue({
  …
  methods: {
    funcA() {
      // do somthing
    }
  }
})

JavaScript

// 老代码.js … window.someApp.funcA()

1
2
3
// 老代码.js
window.someApp.funcA()

注意:全局变量名需要人工协调,避免命名冲突。PS:这是过渡期的妥协,不是最终状态

新增一个路由页面时更棘手。聪明的读者一定会想到让新增的路由页面独立于已有的单页应用,单独分配一个URL,这样代码会更干净。

假如新增的路由页面需要实现十几个功能,而这些功能已经存在于旧代码中呢?权衡了需求的紧急性和对代码整洁度的追求,我们再次妥协(PS:这也是过渡期,不是最终状态)。大家不要轻易模仿,如果条件允许,还是新起一个页面吧,心情会舒畅很多哦。

2. 产品需要修改老代码里的独立组件。

解决方法:如果这个组件不是特别复杂,我们会以“夹带私货”的方式重构上线,这样还能顺便让测试童鞋帮忙验一下重构后有没有bug。具体实现参考第一种情况。

3. 产品需要修改整站的公共部分。

我们的网站包含好几个页面,此次重构的单页应用只是其中之一。它们共用了顶部导航栏。在这些页面模板中通过Smarty的include语法加载:

JavaScript

{%include file=”topPanel.inc”%}

1
{%include file="topPanel.inc"%}

产品在一次界面改版中提出要给导航栏加上一些功能的快捷入口,比如导入文件,购买套餐等。而这些功能在单页应用中已经用vue实现了。所以还得将导航栏实现为vue组件。

为了更快渲染导航栏,需要保留它原有的标签,而不是在JS里以组件的形式渲染。所以需要用到特殊手段:

  • 在topPanel.inc里写上自定义标签,对应到vue组件,比如下面代码里的“。当JS未加载时,会立即渲染导航栏的常规标签以及自定义标签。

<div id=”topPanelMountee”> <div id=”topPanel”>
<div>一些页面直出的内容</div> … <import-button>
<button class=”btn-import”> 导入 </button>
</import-button> … </div> </div>

1
2
3
4
5
6
7
8
9
10
11
12
<div id="topPanelMountee">
  <div id="topPanel">
      <div>一些页面直出的内容</div>
      …
      <import-button>
        <button class="btn-import">
          导入
        </button>
      </import-button>
      …
  </div>
</div>
  • 导航栏组件:topPanel.js,它包含了ImportButton等子组件(对应上面的<import-button>)。等JS加载后,ImportButton组件就会挂载到<import-button>上并为这个按钮绑定行为。另外,注意下面代码中的template并不是<App />,而是一个ID选择器,这样topPanel组件就会以#topPanelMountee里的内容作为模板挂载到#topPanelMountee元素中,是不是很机智~

JavaScript

// topPanel.js new Vue({ el: ‘#topPanelMountee’, template:
‘#topPanelMountee’, components: { … ImportButton } })

1
2
3
4
5
6
7
8
9
// topPanel.js
new Vue({
  el: ‘#topPanelMountee’,
  template: ‘#topPanelMountee’,
  components: {
    …
    ImportButton
  }
})

彻底重构后,我们还做了进一步的性能优化。

传统的网页应用

进一步优化

单页应用

1. HTML瘦身

在采用组件化开发之前,HTML中预置了许多标签元素,比如:

JavaScript

<button data-cn=”del” class=”del”>删除</button> <button
data-cn=”rename” class=”rename”>重命名</button> …

1
2
3
<button data-cn="del" class="del">删除</button>
<button data-cn="rename" class="rename">重命名</button>

当状态改变时,通过JS操作DOM来控制预置标签的内容或显示隐藏状态。这种做法不仅让HTML很臃肿,JS跟DOM的紧耦合也让人头大。改成组件化开发后,将这些元素统统删掉。

之前还使用了很多全局变量存放服务端输出的数据。比如:

<script> var SYS_CONF = { userName: {%$userInfo.name%} … }
</script>

1
2
3
4
5
6
<script>
    var SYS_CONF = {
        userName: {%$userInfo.name%}
        …
    }
</script>

随着时间的推移,这些全局变量越来越多,管理起来很费劲。还有一些已经废弃的变量,对HTML的体积做出了“贡献”。所以重构时只保留了必需的变量。更多数据则在运行时加载。

另外,在没有模板字面量的年代,HTML里大量使用了script标签存放运行时所需的模板元素。比如:

<script type=”text/template” id=”sharePanel”> <div
class=”share”> … </div> </script>

1
2
3
4
5
<script type="text/template" id="sharePanel">
    <div class="share">
        …
    </div>
</script>

虽然上线时会把这些标签内的字符串提取成JS变量,以减小HTML的体积,但在开发时,这些script标签会增加代码阅读的难度,因为要不停地切换HTML和JS目录查找。所以重构后删掉了大量的<script>标签,使用vue的<template>以及ES6的模板字面量来管理模板字符串。

单页应用的优缺点

每种技术都有其利弊,单页应用也是如此。

  • 无刷新体验,这个应该是最显著的有点,由于路由分发直接在浏览器端完成,页面是不刷新,对用户的响应非常及时,因此提升了用户体验;
  • 完全的前端组件化,前端开发不再以页面为单位,更多地采用组件化的思想,代码结构和组织方式更加规范化,便于修改和调整;
  • API
    共享
    ,如果你的服务是多端的(浏览器端、Android、iOS、微信等),单页应用的模式便于你在多个端共用
    API,可以显著减少服务端的工作量。容易变化的 UI
    部分都已经前置到了多端,只受到业务数据模型影响的
    API,更容易稳定下来,便于提供鲁棒的服务;
  • 组件共享,在某些对性能体验要求不高的场景,或者产品处于快速试错阶段,借助于一些技术(Hybrid、React
    Native),可以在多端共享组件,便于产品的快速迭代,节约资源。

缺点:

  • 首次加载大量资源,要在一个页面上为用户提供产品的所有功能,在这个页面加载的时候,首先要加载大量的静态资源,这个加载时间相对比较长;
  • 较高的前端开发门槛,MVC
    前置,对前端工程师的要求提高了,不再是『切切图,画画页面这么简单』;同时工作量也会增加数倍,开发这类应用前端工程师的数量往往多于后端;
  • 不利于 SEO,单页页面,数据在前端渲染,就意味着没有
    SEO,或者需要使用变通的方案。

2. 渐进渲染

首屏想要更快渲染,还要确保文档加载的CSS和JS尽量少,因为它们会阻塞文档加载。所以我们尽可能延迟加载非关键组件。比如:

  • 延迟非默认路由

单页应用有很多路由组件。所以除了默认跳转的路由组件,将非默认路由组件打包成单独的chunk。使用import()的方式动态加载。只有命中该路由时,才加载组件。比如:

JavaScript

const AsyncComp = () => import(/* webpackChunkName: “AsyncCompName”
*/ ‘AsyncComp.vue’) const routes = [{ path: ‘/some/path’, meta: {
type: ‘sharelink’, isValid: true, listKey: ‘sharelink’ }, component:
AsyncComp }] …

1
2
3
4
5
6
7
8
9
10
11
const AsyncComp = () => import(/* webpackChunkName: "AsyncCompName" */ ‘AsyncComp.vue’)
const routes = [{
  path: ‘/some/path’,
  meta: {
    type: ‘sharelink’,
    isValid: true,
    listKey: ‘sharelink’
  },
  component: AsyncComp
}]
  • 延迟不重要的展示型组件

这些组件其实可以延迟到主要内容渲染完毕再加载。将这些组件单独打包为一个chunk。比如:

JavaScript

import(/* webpackChunkName: “lazy_load” */ ‘a.js’) import(/*
webpackChunkName: “lazy_load” */ ‘b.js’)

1
2
import(/* webpackChunkName: "lazy_load" */ ‘a.js’)
import(/* webpackChunkName: "lazy_load" */ ‘b.js’)
  • 延迟低频的功能

如果某些功能属于低频操作,或者不是所有用户都需要。则可以选择延迟到需要的时候再加载。比如:

JavaScript

async handler () { await const {someFunc} = import(‘someFuncModule’)
someFunc() }

1
2
3
4
async handler () {
  await const {someFunc} = import(‘someFuncModule’)
  someFunc()
}

使用vue-router构建单页应用

vue-router.js是Vue.js官方的路由插件用于构建单页面应用。
vue的单页应用是基于路由和组件的。传统的页面应用,是用一些超链接来实现页面切换和跳转的。在vue-router单面应用中,则是路径之间的切换,也就是组件的切换。

先来看一下官方提供的最简单的例子:示例
HTML

<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>

<div id="app">
    <h1>Hello App!</h1>
    <p>
        <!-- 使用 router-link 组件来导航. -->
        <!-- 通过传入 `to` 属性指定链接. -->
        <!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
        <router-link to="/foo">Go to Foo</router-link>
        <router-link to="/bar">Go to Bar</router-link>
    </p>
    <!-- 路由出口 -->
    <!-- 路由匹配到的组件将渲染在这里 -->
    <router-view></router-view>
</div>
  • router-link标签:跳转的链接,to=””是必须的属性,双引号中的内容是我们接下来在JS文件中定义的路由path。
  • router-view标签:展示我们匹配到的组件的区域。

JavaScript

// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter)

// 1. 定义(路由)组件。
// 也可以从其他文件 import 进来
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
const routes = [
    { path: '/foo', component: Foo },
    { path: '/bar', component: Bar }
]

// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
    routes // (缩写)相当于 routes: routes
})

// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({
    router
}).$mount('#app')

// 现在,应用已经启动了!

JavaScript文件主要做的事情是:

  • 定义路由列表,即routes。
  • 创建router实例及router配置,即router。
  • 创建和挂载根实例。

以上只是教我们用最简单的方法使用vue-router。但实际开发过程中,首先我们的vue组件显然不会只有一个template模板这么简单,会用到vue的单文件组件;
其次我们通常会希望<router-view>的范围是整个页面,而不是像现在这样一直有几个碍眼的导航存在于页面上,这就需要先定义好默认状态下<router-view>显示的内容。

既然是单页应用(SPA),那么整个项目有以下三个文件是必要的:

  • 一个html文件:index.html
  • 一个webpack打包时的入口js文件:main.js
  • 一个根vue组件,作为其他组件的挂载点:app.vue

接下来
我们就创建两个自定义组件:index.vue和hello.vue。我们希望的结果是他们之间互相跳转。
我们利用官方提供的脚手架vue-cli工具生成简单的一个基于webpack打包的vue项目
准备工作:

npm install webpack -g
npm install vue-cli -g
//打开要创建的项目路径目录,创建项目
vue init webpack-simple <项目名>
cd <项目名>
//安装依赖
npm install
//安装vue-router 
npm install vue-router --save
npm run dev

生成的vue项目如下图:

3. 优化图片

虽然代码做了很多优化,但是动辄几十到几百KB的图片瞬间碾压了辛苦重构带来的提升。所以图片的优化也是至关重要滴~

1. PNG改成SVG

由于项目曾经支持IE6-8,大量使用了PNG,JPEG等格式的图片。随着历史的车轮滚滚向前,IE6-8的用户占比已经大大降低,我们在去年放弃了对IE8-的支持。这样一来就能采取更优的解决方案啦~

我们的页面上有各种大小的图标和不同种类的占位图。原先使用位图并不能很好的适配retina显示器。现在改成SVG,只需要一套图片即可。相比PNG,SVG有以下优点:

  1. 压缩后体积小
  2. 无限缩放,不失真
  3. retina显示器上清晰

2. 进一步“压榨”SVG

虽然换成SVG,但是还远远不够,使用webpack的loader可以有效地压缩SVG体积。

  • 用svgo-loader去除无用属性

SVG本身既是文本也是图片。设计师提供的SVG大多有冗余的内容,比如一些无用的defstitle等,删除后并不会降低图片质量,还能减小图片体积。

我们使用svgo-loader对SVG做了一些优化,比如去掉无用属性,去掉空格换行等。这里就不细数它能提供的优化项目。大家可以对照svgo-loader的选项配置。

  • 用svg-sprite-loader合并多个SVG

另外,SVG有多种用法,比如:img,background,inline,inline
<use>。如果某些图反复出现并且对页面渲染很关键,可以使用svg-sprite-loader将多个图合并成一个大的SVG,避免逐个发起图片请求。然后使用内联或者JS加载的方式将这个SVG引入页面,然后在需要的地方使用SVG的<use>标签引用该图标。合并后的大SVG如下图:

图片 2

使用时:

<svg> <use xlink:href=”#icon-add”></use> </svg>

1
2
3
<svg>
  <use xlink:href="#icon-add"></use>
</svg>

即可在使用的位置展现该图标。

以上是一些优化手段,下面给大家分享一下重构后的收益。

一、使用路由

  1. 首先在目录下创建components文件夹,然后再创建index.vuehello.vue文件

//index.vue
<template>
    <div>
        <h2>Index</h2>
        <hr>
        <p>{{sContent}}</p>
    </div>
</template>
<script>
    export default{
        data(){
            return {
                sContent:"This is index components"
            }
        }
    }
</script>

//hello.vue
<template>
    <div>
        <h2>Hello Vue.js</h2>
        <hr/>
        <p>{{sContent}}</p>
    </div>
</template>
<script>
    export default{
        data(){
            return {
                sContent:"This is hello components"
            }
        }
    }
</script>
  1. 修改main.js文件

//引入并安装vue-router插件
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
//引入index.vue和hello.vue组件
import App from './App.vue';
import index from './components/index.vue';
import hello from './components/hello.vue';
//定义路由
const routes = [
    {path:'/',component:App},
    { path: '/index', component: index },
    { path: '/hello', component: hello }
]
//创建 router 实例,然后传 routes 配置
const router=new VueRouter({
  routes
});
//创建和挂载根实例。通过 router 配置参数注入路由,从而让整个应用都有路由功能
new Vue({
  el:"#app",
  router
});
  1. 修改App.vue

<template>
  <div>
    ![](./assets/logo.png)
    <h1>{{msg}}</h1>
    <ul>
      <router-link to='/index' tag='li'><a href="/index">Index</a></router-link>
      <router-link to='/hello' tag='li'><a href="/hello">Hello</a></router-link>
    </ul>
  </div>
</template>
  1. 修改index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>vue-webpack-simple</title>
  </head>
  <body>
    <div id="app">
        <router-view></router-view>
    </div>
    <script src="/dist/build.js"></script>
  </body>
</html>

这样就会把渲染出来的页面挂载到这个id为app的div里了。

修改后运行的效果如下:

相关文章

Leave a Reply

电子邮件地址不会被公开。 必填项已用*标注