不少互联网公司都在使用vue技术栈,或称为vue全家桶。
使用过vue的程序员一般这样评价它,“vue.js兼具angular.js和react.js的优点”。Vue.js 是一个JavaScript MVVM(Model-View-ViewModel)库,用于渐近式构建用户界面。它以数据驱动和组件化思想构建,采用自底向上增量开发的设计思想。相比Angular.js,Vue.js API更加简洁;相比 React + Redux 复杂的架构,Vue.js 上手更加容易。
目录
一、vue全家桶包括什么
vue-router路由
vuex
vue-resource
构建工具vue-cli
调度工具Devtools
关于UI组件库
二、vue工程目录结构
编辑器
三、vue使用简介
数据代理
vue实例生命周期图解
四、vue的运行原理
双向绑定图解
模板是如何解析的
五、发布前优化
UI组件按需加载
路由懒加载
使用异步组件(动态组件)
图片压缩与合并
使用CDN加速vue类库
压缩代码
v-for和v-if不要同时使用
使用Object.freeze冻结大数据
使用Keep-alive标签优化组件创建
使用Set
在scope中少用元素选择器
关于template的优化
-------------------------------------------------
一、vue全家桶包括什么
vue-router路由
网站:http://router.vuejs.org。使用npm工具来安装vue-router
npm install vue-router
通过import导入Vue模块、vue-router模块及其它组件。
import Vue from’vue’
importRouter from’vue-router’
在使用路由前,必须要通过 Vue.use() 明确地安装路由功能。
Vue.use(Router)
通过const router= new VueRouter()定义路由,并传入对应的配置,包括路径path和组件components等。
在使用newVue来创建和挂载vue根实例的时候,记得要通过 router配置参数注入路由。使用router-link:
有两种模式:
hash 模式
history 模式
vuex
在vue开发实战中,多个组件共享数据时,单向数据流的简洁性很容易被破坏。为解决多个视图使用同一数据及多个视图驱动同一数据更新的问题,vuex应运而生。
当网站足够大时,一个状态树下,根的部分字段繁多,解决这个问题就要模块化 vuex,官网提供了模块化方案,允许我们在初始化 vuex 的时候配置 modules。每一个 module 里面又分别包含 state 、action 等,看似是多个状态树,其实还是基于 rootState 的子树。细分后整个 state 结构就清晰了,管理起来也方便许多。
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 的四个核心概念是:
The state tree:Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态。至此它便作为一个唯一数据源(SSOT)而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。
Getters:用来从 store 获取 Vue 组件数据。
Mutators:事件处理器用来驱动状态的变化。
Actions:可以给组件使用的函数,以此用来驱动事件处理器 mutations。(注:此许或许称之为EventHandler更为恰当。)
Vuex和简单的全局对象是不同的。当Vuex从store中读取状态值的时候,若状态发生了变化,那么相应的组件也会更新。并且改变store中状态的唯一途径就是提交commit mutations。只要发生了状态的变化,一定伴随着mutation的提交。 例如:
通过 store.state 来获取状态对象,以及通过 store.commit 方法触发状态变更:
由于 vuex 的灵活性,带来了编码不统一的情况,完整的闭环是 store.dispatch('action') -> action -> commit -> mutation -> getter -> computed,实际上中间的环节有的可以省略,因为 API 文档提供了以下几个方法 mapState、mapGetters、mapActions、mapMutations,然后在组件里可以直接调取任何一步,还是项目小想怎么调用都可以,项目大的时候,就要考虑 vuex 使用的统一性,有人建议是不论多简单的流程都跑完整个闭环,形成代码的统一,方便后期管理,在组件里只允许出现 dispatch 和 mapGetters,其余的流程都在名为 store 的 vuex 文件夹里进行。
注:mapGetters 工具函数会将 store 中的 getter 映射到局部计算属性中。它的功能和 mapState 非常类似。
vue-resource
网站:https://github.com/pagekit/vue-resource
使用npm来安装Vue-resource:
$ npm install vue-resource
在安装并引入vue-resource后,可以基于全局的Vue对象使用http,也可以基于某个Vue实例使用http。
在发送请求后,使用then方法来处理响应结果,then方法有两个参数,第一个参数是响应成功时的回调函数,第二个参数是响应失败时的回调函数。
vue-resource的请求API是按照REST风格设计的,它提供了7种请求API:
· get(url,[options])
· head(url,[options])
· delete(url,[options])
· jsonp(url,[options])
· post(url,[body], [options])
· put(url, [body],[options])
· patch(url,[body], [options])
构建工具vue-cli
vue-cli是vue标准的开发工具。网站:https://cli.vuejs.org/
安装
npm install -g @vue/cli
最新版本为3.4.0。
创建项目
vue create my-project
以上是命令行创建。也可以通过 vue ui 命令以图形化界面创建和管理项目:
vue ui
运行
npm run serve
调度工具Devtools
vue在调试方面,可以选择安装chrome插件vue Devtools。打开vue项目,在调试vue应用的时候,chrome开发者工具中会看一个vue的一栏,点击之后就可以看见当前页面vue对象的一些信息。
在Devtools工具中,可以选择组件,查看对应组件内的数据信息。也可以选择Vuex选项,查看该项目内Vuex的状态变量信息。
关于UI组件库
可以自己写,为提高开发效率也可以复用第三方组件库。element(https://github.com/ElemeFE/element)是一个最好支持vue2.0的UI组件库。
二、vue工程目录结构
这是一个简单的vue项目的大概结构:
components/文件夹:用来存放Vue 组件。个人建议,把每一个组件中使用到的image图片放置到对应的组件子文件目录下,便于统一的管理
Node_modules/:npm安装的该项目的依赖库
vuex/文件夹:存放的是和 Vuex store 相关的东西(state对象,actions,mutations)
router/文件夹:存放的是跟vue-router相关的路由配置项
build/文件:是 webpack 的打包编译配置文件
static/文件夹:存放一些静态的、较少变动的image或者css文件
config/文件夹:存放的是一些配置项,比如服务器访问的端口配置等
dist/该文件夹:一开始是不存在,在我们的项目经过 build 之后才会产出
App.vue根组件,所有的子组件都将在这里被引用
index.html整个项目的入口文件,将会引用我们的根组件 App.vue
main.js入口文件的 js 逻辑,在webpack 打包之后将被注入到 index.html 中
编辑器
VSCode with Vetur
三、vue使用简介
数据代理
每个 Vue.js 应用都是通过构造函数 Vue 创建一个 Vue 的根实例 启动的。每个 Vue 实例都会代理其 data 对象里所有的属性:
var data = { a: 1 }
var vm = new Vue({
data: data
})
vm.a === data.a // -> true
设置新值也会同步影响:
vm.a = 2
data.a // -> 2
// ... 反之亦然
data.a = 3
vm.a // -> 3
实现数据代理的伪代码如下:
var self = this; // this为vue实例, 即vm
Object.keys(this.data).forEach(function(key) {
Object.defineProperty(this, key, { // this.title, 即vm.title
enumerable: false,
configurable: true,
get: function getter () {
return self.data[key]; //触发对应data[key]的getter
},
set: function setter (newVal) {
self.data[key] = newVal; //触发对应data[key]的setter
}
});
}
Vue 实例暴露了一些有用的实例属性与方法。这些属性与方法都有前缀 $,以便与代理的 data 属性区分。例如:
vm.$data === data // -> true
vm.$el === document.getElementById('example') // -> true
vue实例生命周期图解
四、vue的运行原理
Vue采用简洁的模板语法,以声明的方式将数据渲染进 DOM。vue代码是没有办法直接被浏览器解析的,必须经过“编译”,变为浏览器可以识别为html、js与css代码。这种声明式开发方式把方便留给了程序员,转换工作交给了自动化工具。
注:el是element的缩写,指Vue实例挂载的元素节点。
双向绑定图解
一般说的双向绑定,指:
数据变动 --> 视图更新
视图更新 --> 数据变动
视图更新 --> 数据变动,这个方向的绑定比较简单。主要通过事件监听来改变数据,比如input控件可以监听input事件,一旦事件触发,调用JS改变data。
模型层(model)只是普通 JavaScript 对象,修改它,DOM本是不能更新的。当程序员把一个普通 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。在每个setter中,可以做许多事件,使表面看起来数据变了,视图就更新了。并且这种数据更新,和原来一样,只是 vm.a=123 这样的简单更新。
如上所求,每个vue组件实例都有相应的 watcher 实例对象,它会在vue组件渲染的过程中把需要用到的属性(getter)记录为依赖。之后,当依赖项的 setter 被(其它JS代码)调用时,setter 会通知 watcher 重新计算,从而致使它关联的组件得以更新。
此处实现的是一个观察者模式。
通过object.defineProperty遍历设置this.data里面所有属性,在每个属性的setter里面去通知对应的回调函数,这里的回调函数包括dom视图重新渲染的函数、使用$watch添加的回调函数等,这样我们就通过object.defineProperty劫持了数据,当我们对数据重新赋值时,如this.title = 'hello vue',就会触发setter函数,从而触发dom视图重新渲染的函数,实现数据变动,对应视图更新。
那么,如何在setter里面触发所有绑定该数据的回调函数呢?
既然绑定该数据的回调函数不止一个,我们就把所有的回调函数放在一个数组里面,一旦触发该数据的setter,就遍历数组触发里面所有的回调函数,我们把这些回调函数称为订阅者。数组最好就定义在setter函数的最近的上级作用域中,如下面实例代码所示。
Object.keys(this.data).forEach(function(key) {
var subs = []; // 在这里放置添加所有订阅者的数组
Object.defineProperty(this.data, key, { // this.data.title
enumerable: false,
configurable: true,
get: function getter () {
console.log('访问数据啦啦啦')
return this.data[key]; //返回对应数据的值
},
set: function setter (newVal) {
if (newVal === this.data[key]) {
return; // 如果数据没有变动,函数结束,不执行下面的代码
}
this.data[key] = newVal; //数据重新赋值
subs.forEach(function () {
// 通知subs里面的所有的订阅者
})
}
});
}
那么,怎么把绑定数据的所有回调函数放到一个数组里面呢?这是通过gettter内部的代码完成的。
我们知道只要访问数据就会触发对应数据的getter,那我们可以先设置一个全局变量target,如果我们要在data里面title属性添加一个订阅者(changeTitle函数),我们可以先设置target = changeTitle,把changeTitle函数缓存在target中,然后访问this.title去触发title的getter,在getter里面把target这个全局变量的值添加到subs数组里面,添加完成后再把全局变量target设置为null,以便添加其他订阅者。
伪代码如下:
target = changeTitle
...
Object.keys(this.data).forEach(function(key) {
var subs = []; // 在这里放置添加所有订阅者的数组
Object.defineProperty(this.data, key, { // this.data.title
enumerable: false,
configurable: true,
get: function getter () {
console.log('访问数据啦啦啦')
if (target) {
subs.push(target);
}
return this.data[key]; //返回对应数据的值
},
set: function setter (newVal) {
if (newVal === this.data[key]) {
return; // 如果数据没有变动,函数结束,不执行下面的代码
}
this.data[key] = newVal; //数据重新赋值
subs.forEach(function () {
// 通知subs里面的所有的订阅者
})
}
});
}
上面代码中提到的changeTitle,即是上面最近一张图解中的watcher。vue通过getter收集watcher集合。因为vue充许在运行时添加代码,所以该收集行为不能仅限制于模板“编译”之前。(注:vue中是不存在严格的编译的,js是解析执行型语言,像C、Go等语言将源码编译为目标平台的二进制文件,才是真的编译。)
模板是如何解析的
假如说有下面这一段代码,我们怎么把它解析成对应的html呢?
{{title}}
注:该示例实现的效果是,在input输入框内输入任何内容,下方h1文本同步更新。
先简单介绍视图更新函数的用途,比如解析指令v-model="title",v-on:click="changeTitle",还有把{{title}}替换为对应的数据等。
回到上面那个问题,如何解析模板?我们只要去遍历所有dom节点包括其子节点:
如果节点属性含有v-model,视图更新函数就为把input的value设置为title的值
如果节点为文本节点,视图更新函数就为先用正则表达式取出大括号里面的值'title',再设置文本节点的值为data['title']
如果节点属性含有v-on:xxxx,视图更新函数就为先用正则获取事件类型为click,然后获取该属性的值为changeTitle,则事件的回调函数为this.methods['changeTitle'],接着用addEventListener监听节点click事件。
五、发布前优化
使用vue-cli部署生产包时,发现资源包很大,打包后的vendor.js达到了1M+。
UI组件按需加载
如果使用了第三方组件/UI库,如element-ui, mint-ui,echarts等,如果全部引入,项目体积非常大,这时可以按需引入组件。
安装 babel-plugin-component
npm install babel-plugin-component -D
然后,将.babelrc 修改为:
{
"presets": [["es2015", { "modules": false }]],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
然后引入部分组件,这样一来,就不需要引入样式了,插件会帮我们处理。
// main.js
import Vue from 'vue'
import { Dialog, Loading } from 'element-ui'
Vue.use(Dialog)
Vue.use(Loading.directive)
Vue.prototype.$loading = Loading.service
// 然后正常使用组件
注:Babel是一个广泛使用的转码器,可以将ES6代码转为ES5代码。让不支持ES6的宿主环境,支持使用一套源码开发。
mint-ui是element-ui的移动端组件,所以它的使用和引入几乎和element-ui一样。
路由懒加载
vue-router官方推荐syntax-dynamic-import插件,不过它要求同时安装@bable/core^7.0.0,如果你安装了babel-core6,可能有版本冲突的,解决方法如下:
npm install babel-plugin-syntax-dynamic-import --save-dev(^6.18.0)
当打包构建应用时,Javascript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。结合 Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。
// router.js
const login = () => import('@/components/login')
const router = new VueRouter({
routes: [
{ path: '/login', component: login }
]
})
还有一种魔法注释用法,不推荐使用。
使用异步组件(动态组件)
app bundle 文件过大,可以尝试通过组件懒加载优化。
动态组件主页面加载是不会加载,等到触发条件时才加载该组件,并且加载一次后就有缓存。如果组件在页面加载时不需要,只在调用时用到,这时可以使用异步组件的写法。仅仅是引入和组件注册写法不同:
// template
// script
components: {
test: () => import('./test') // 将组件异步引入,告诉webpack,将该部分代码分割打包
},
methods:{
clickTest () {
this.showTest = !this.showTest
}
}
图片压缩与合并
无损压缩图片:https://tinypng.com/。可以将图片制成雪碧精灵图。
使用CDN加速vue类库
一般项目里用到的第三方js库主要有:vue、vue-router、vuex、vue-resource、axio、qiniu等。这些依赖库的js文件被一起打包到vender那个js文件里面,导致vender这个文件很大,那首屏加载速度肯定会被拖慢。
类库文件使用cdn加速
修改 build/webpack.base.conf.js
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: './src/main.js'
},
externals:{
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex':'Vuex',
'vue-resource': 'VueResource'
}
排除已经手动收入的js文件
利用webpack的externals。具体做法就是在 build/webpack.base.conf.js文件的module里面与rules同层加入externals。具体做法,修改src/main.js src/router/index.js 注释掉import引入的vue,vue-resource等:
// import Vue from 'vue'
// import VueResource from 'vue-resource'
// Vue.use(VueResource)
上面已经引用过。
压缩代码
vue-cli已经使用UglifyJsPlugin 插件来压缩代码,可以设置成如下配置:
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
drop_console: true,
pure_funcs: ['console.log']
},
sourceMap: false
})
其中sourceMap: false是禁用除错功能。如果设为true,在部署包中会生成.map结尾的js文件。它用于在代码混淆压缩的情况下仍可进行调试。这个功能虽好,但会大大增加整体资源包的体积,所以将其禁用。
v-for和v-if不要同时使用
在vue中v-for和v-if不要放在同一个元素上使用。由于 v-for 和 v-if 放在同一个元素上使用会带来一些性能上的影响,在计算属性上过滤之后再进行遍历。反例:
使用Object.freeze冻结大数据
对于前端纯大数据展示(纯大数据指:拿到数据就是直接用于展示的,不需要做修改其中字段等处理的,而且数据量比较大)的情况下,使用Object.freeze方法来包裹变量,那边vue内部不会使用defineproperty去监听数据内部的变化,只有本身变化时才会触发,在大量数据的情况下,vue内部不在去监听数据的变化会提高性能。使用demo如下:
使用Keep-alive标签优化组件创建
vue提供了keep-alive标签来存储缓存,对于一些视频控件object或图表类的使用,我们经常会使用v-if指令,而v-if是会创建和销毁的,如果频繁操作在ie下的内存会持续上升,而keep-alive可以有效的缓存,抑制内存的持续上升。
见:https://cn.vuejs.org/v2/api/#keep-alive
使用Set
Es6集合Set()可优化遍历速度,set集合是可用于查找该集合内是否存在某个元素。但如果使用了Bable自动转化,该优化无效。
在scope中少用元素选择器
scope中元素选择器尽量少用。在 scoped 样式中,类选择器比元素选择器更好,因为大量使用元素选择器是很慢的。
为了给样式设置作用域,Vue 会为元素添加一个独一无二的特性,例如 data-v-f3f3eg9。然后修改选择器,使得在匹配选择器的元素中,只有带这个特性才会真正生效 (比如 button[data-v-f3f3eg9])。问题在于大量的元素和特性组合的选择器 (比如 button[data-v-f3f3eg9]) 会比类和特性组合的选择器 慢,所以应该尽可能选用类选择器。
关于template的优化
v-show,v-if 用哪个?在我来看要分两个维度去思考问题,第一个维度是权限问题,只要涉及到权限相关的展示无疑要用 v-if,第二个维度在没有权限限制下根据用户点击的频次选择,频繁切换的使用 v-show,不频繁切换的使用 v-if,这里要说的优化点在于减少页面中 dom 总数,我比较倾向于使用 v-if,因为减少了 dom 数量,加快首屏渲染,至于性能方面我感觉肉眼看不出来切换的渲染过程,也不会影响用户的体验。
不要在模板里面写过多的表达式与判断 v-if="isShow && isAdmin && (a || b)",这种表达式虽说可以识别,但是不是长久之计,当看着不舒服时,适当的写到 methods 和 computed 里面封装成一个方法,这样的好处是方便我们在多处判断相同的表达式,其他权限相同的元素再判断展示的时候调用同一个方法即可。
循环调用子组件时添加 key,key 可以唯一标识一个循环个体,可以使用例如 item.id 作为 key,假如数组数据是这样的 ['a' , 'b', 'c', 'a'],使用 :key="item" 显然没有意义,更好的办法就是在循环的时候 (item, index) in arr,然后 :key="index"来确保 key 的唯一性。
2019年2月14日
--------------------------------------------------------
参考资料:
本文分享自微信公众号 - 程序员LIYI(CoderLIYI)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。