Vue设计思想
- 数据驱动应用
- MVVM模式的践行者
MVVM框架的三要素:
- 响应式 —— vue如何监听数据变化?
- 模板引擎 —— vue的模板如何编写和解析?
- 渲染 —— vue如何将模板转换为html?
模板语法是如何实现的?
在底层的实现上,Vue将模板编译成虚拟DOM渲染函数。结合响应式系统,Vue能智能计算出最少需要重新渲染多少组件,并把DOM操作次数减到最少。
计算属性VS监听器
- 优先使用computed
语境上的差异
- watch —— 一个值变化了我要做一些事情,适合一个值影响多个值的情形
- computed —— 一个值由其他值得来,这些值变了我也要变,适合多个值影响一个值的情形
- 计算属性具有缓存性,计算所得的值如果没有变化不会重复执行
- 监听器选项提供了更通用的方法,适合执行异步操作或较大开销操作
生命周期
- 使用场景分析
{
beforeCreate () {}, // 执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务
created () {}, // 组件初始化完毕,各种数据可以使用,常用于异步数据获取
beforeMount () {}, // 未执行渲染、更新,dom未创建
mounted () {}, // 初始化结束,dom已创建,可用于获取访问数据和dom元素
beforeUpdate () {}, // 更新前,可用于获取更新前各种状态
updated () {}, // 更新后,所有状态已是最新
beforeDestroy () {}, // 销毁前,用于一些定时器或订阅取消
destroyed () {}, // 组件已销毁,作用同上
}
Vue 组件化的理解
组件化是Vue的精髓,Vue应用就是由一个个组件构成的
定义:组件是可以复用的Vue实例,准确讲是VueComponent的实例,继承自Vue、
优点:组件化可以增加代码的复用性、可维护性和可测试性
使用场景:
- 通用组件:实现最基本的功能,具有通用性、复用性,如按钮组件、输入框组件、布局组件等
- 业务组件:完成具体业务,具有一定的复用性,如登录组件、轮播图组件等
- 页面组件:组织应用各个部分独立内容,需要时在不同页面组件间切换,如列表页、详情页组件
如何使用组件:
- 定义:Vue.component(), components选项,sfc
- 分类:有状态组件,functional(不维护数据),abstract(不涉及视图)
- 通信:props,$emit()/$on(), provide/inject,$children/$parent/$root/$attrs/$listeners,vue bus
- 内容分发:< slot>,< template>,v-slot
- 使用及优化:is,keep-alive, 异步组件
组件的本质:产生虚拟DOM
- 过程:组件配置->VueComponent实例->render()->Virtual DOM->DOM
数据相关API
Vue.set(vm.$set)
- 作用:向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新
- 用法:Vue.set(target, property/index, value)
Vue.delete(vm.$delete)
- 作用:删除对象的属性,如果对象是响应式的,确保能触发更新视图
- 用法:Vue.delete(target, property/index)
事件相关API
vm.$on
- 作用:监听当前实例上的自定义事件,事件可以由vm.$emit触发,回调函数会接收所有传入事件触发函数的额外参数
vm.$on('test', function(msg) {
console.log(msg);
})
vm.$emit
- 作用:触发当前实例上的事件,附加参数都会传给监听器回调
vm.$emit('test', 'hello');
典型应用:事件总线
- 原理:通过在Vue原型上添加一个Vue实例作为事件总线,实现组件间相互通信,而且不受组件间关系的影响
vm.$once
- 作用:监听一个自定义事件,但是只触发一次,一旦触发后,监听器就会被移除
vm.$once('test', function(msg) {
console.log(msg);
})
vm.$off
- 作用:移除自定义事件监听器
vm.$off(); // 没有提供参数时,移除所有的事件监听器
vm.$off('test'); // 如果只提供了事件,则移除该事件所有的监听器
vm.$off('test', callback); // 如果同时提供了事件与回调,则只移除这个回调的监听器
组件或元素引用
ref 和 vm.$refs
ref被用来给元素或子组件注册引用信息,引用信息将会注册在父组件的$refs对象上
- 如果在普通元素上使用,引用指向的就是DOM元素
- 如果用在子组件上,引用就指向组件
注意:
- ref是作为渲染结果被创建的,在初始渲染时不能访问它们
- $refs不是响应式的,不要试图使用它在模板中做数据绑定
- 当v-for用于元素或组件时,引用信息将是包含DOM节点或组件实例的数组
过渡&动画
Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果,包括:
- 在 CSS 过渡和动画中自动应用 class
- 可以配合使用第三方的CSS库,如Animate.css
- 在过渡钩子函数中使用JS直接操作DOM
- 可以配合使用第三方JS库,如Velocity.js
CSS 过渡动画
- 过渡类名
- v-enter:定义进入过渡的开始,在元素被插入之前生效,在元素被插入之后的下一帧失效
.fade-enter { opacity: 0; }
- v-enter-active: 定义进入过渡生效时的状态。在元素被插入之前生效,在过渡/动画完成之后移除
.fade-enter-active { transition: opacity 0.5s; }
- v-enter-to:定义进入过渡的结束状态。在元素被插入之后的下一帧生效(与此同时v-enter被移除),在过渡/动画完成之后移除
.fade-enter-to { opacity: 1; }
- v-leave:定义离开过渡的开始状态,在离开过渡被触发时立刻生效,下一帧被移除
.fade-leave { opacity: 1; }
- v-leave-active:定义离开过渡生效时的状态,在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。该类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
.fade-leave-active { transition: opacity 0.5s; }
- v-leave-to:定义离开过渡的结束状态。在离开过渡被触发之后下一帧生效(与此同时v-else被删除),在过渡/动画完成之后移除
.fade-leave-to { opacity: 0; }
使用CSS动画库
通过自定义过渡类名可以有效结合Animate.css这类动画库制作动画效果<transition enter-active-class="animated bounceIn" leave-active-class="animated bounceOut"></transition>
JS动画
可以在< transition>属性中声明JS钩子,使用JS实现动画<transition v-on:before-enter="beforeEnter" // 动画开始前,设置初始状态 v-on:enter="enter" // 执行动画 v-on:after-enter="afterEnter" // 动画结束,清理工作 v-on:enter-cancelled="enterCancelled" // 取消动画 v-on:before-leave="beforeLeave" v-on:leave="leave" v-on:after-leave="afterLeave" v-on:leave-cancelled="leaveCancelled"></transition>
列表过渡
利用transition-group可以对v-for渲染的每个元素应用过度<transition-group name="fade"> <div v-for="c in courses" :key="c.name">{{ c.name }} - ¥{{c.price}} <button @click="addToCart(c)">加购</button> </div> </transition-group>
可复用性
过滤器
- 用于一些常见的文本格式化
- 可以用在两个地方:双花括号插值和v-bind表达式
<!-- 双花括号 -->
{{ message | capitalize }}
<!-- 在 v-bind 中 -->
<div v-bind:id="rawId | formatId"></div>
{{ c.price | currency('RMB) }}
filter: {
currency(value, symbol = '¥') {
return symbol + value;
}
}
自定义指令
- 需要对普通DOM元素进行底层操作,会用到自定义指令
Vue.directive('focus', {
inserted(el) {
el.focus();
}
})
<input v-focus>
指令定义对象钩子函数
- bind:只调用一次,指令第一次绑定到元素时调用,可在此进行一次性的初始化操作
- inserted:被绑定元素插入到父节点时调用(仅保证父节点存在,但不一定已经被插入文档)
- update:所在组件的VNode更新时调用,但可能发生在其子VNode更新之前
- componentUpdate:指令所在组件的VNode及其子VNode全部更新后调用
- unbind:只调用一次,指令与元素解绑时调用
在按钮权限控制中的应用
const role = 'user'; Vue.directive('permission', { inserted(el, binding) { if (role !== binding.value) { el.parentElement.removeChild(el); } } })
<div class="toolbar" v-permission="'admin'"></div>
渲染函数
- 在需要JS的完全编程的能力时使用,渲染函数比模板更接近编译器
render: function(createElement) {
// createElement返回的结果是VNode
return createElement(
tag, // 标签名
data, // 数据
children, // 子节点数组
)
}
示例
Vue.component('heading', {
props: {
level: {
type: String,
required: true,
},
title: {
type: String,
default: '',
}
},
render(h) {
return h(
'h' + this.level, // tagname
{ attrs: { title: this.title } } // 参数
this.$slots.default, // 子节点数组
)
}
})
// <heading :level="1" :title="title">{{ title }}</heading>
虚拟DOM
- Vue通过建立一个虚拟DOM来追踪自己要如何改变真实DOM
createElement参数
// @return {VNode} createElement( // {String | Object | Function} tagname 'div', // 必填,标签名或组件名,也可以是返回前两个的函数 // {Object} // 一个与模板中属性对应的数据对象,可选 { // }, // {String | Array} // 子级虚拟节点(VNodes),由createElement()构建而成, // 也可以使用字符串来生成“文本虚拟节点”,可选 [ 'some text', createElement('h1', 'another text'), createElement(MyComponent, { props: { someProp: 'foobar', } }), ] );
函数式组件
- 组件没有管理任何状态,也没有监听任何传递给他的状态,也没有生命周期,可以将组件标记为functional,即意味着它无状态(没有响应式数据),也没有实例(没有this上下文)
Vue.component('heading', {
functional: true,
props: ['level','title','icon'],
render(h, context) {
let children = [];
// 属性获取
const { icon, title, level } = context.props;
if (icon) {
children.push(h(
'svg',
{ class: 'icon' },
[h('use', { attrs: { 'xlink:href': '#icon-' + icon } })],
));
// 子元素获取
children = children.concat(context.children);
}
vnode = h(
'h' + level,
{ attrs: { title }},
children,
)
return vnode;
}
})
混入
- 提供一种非常灵活的方式来分发Vue组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被"混合"进入该组件本身的选项。
// 定义一个混入对象
var myMixin = {
created: function() {
this.hello();
},
methods: {
hello: function() {
console.log("hello mixin");
}
}
};
// 定义一个使用混入对象的组件
Vue.component('comp', {
mixins: [myMixin]
});
插件
一般用来给Vue添加全局功能
- 添加全局方法或属性 如vue-custom-element
- 添加全局资源:指令/过滤器/过渡等 如vue-touch
- 通过全局混入来添加一些组件选项 如vue-router
- 添加Vue实例方法,通过把它添加到Vue.prototype上实现
- 一个库,提供自己的API,同时提供上面提到的一个或多个功能。如 vue-router
- 插件声明
MyPlugin.install = function(Vue, options) {
// 1. 添加全局方法或属性
Vue.myGlobalMethod = function() {}
// 2. 添加全局资源
Vue.directive('my-directive', {})
// 3. 注入组件选项
Vue.mixin({
created: function() {
}
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function(methodOptions) {}
}
- 组件改为插件方式
const MyPlugin = {
install(Vue, options) {
Vue.component('heading', {/*...*/});
}
}
if (typeof window !== 'undefined' && window.Vue) {
window.Vue.use(MyPlugin)
}
工程化
Vue CLI
快速原型开发
安装全局扩展npm i -g @vue/cli-service-global
启动一个服务并运行原型
vue serve Hello.vue
创建项目
vue create my-vue-app
插件
vue add router
开发
- 处理资源路径
在JS、CSS或vue文件中使用相对路径(必须以.开头)引用一个静态资源时,该资源将被webpack处理 转换规则
如果URL是一个绝对路径,会被保留
<img alt="vue logo" src="/assets/logo.png"> <img alt="vue logo" src="http://img.xx.com/logo.png">
如果URL以.开头会作为一个相对模块请求被解析并给予文件系统相对路径
<img alt="vue logo" src="./assets/logo.png">
如果URL以~开头会作为一个模块被请求被解析,即可以引用Node模块中的资源
<img alt="vue logo" src="~some-npm-pkg/foo.png">
- 如果URL以@开头会作为一个模块请求被解析,VueCLI默认会设置一个指向src的别名@
import Hello from '@/components/Hello.vue';
什么时候使用public文件夹
通过webpack的处理- 脚本和样式表会被压缩且打包在一起,从而避免额外的网络请求
- 文件丢失会直接在编译时报错,而不是到了用户端才报错
- 最终生成的文件名包含了内容哈希,因此不必担心浏览器缓存旧版本
什么时候直接用public文件夹
- 需要在构建输出中指定一个固定的文件名字
- 有上千图片,需要动态引用他们的路径
- 有些库可能与webpack不兼容,除了将其用一个独立的< script>标签引入没有别的选择
使用public文件夹的注意事项
- 如果应用不是部署在服务器的根目录,需要为URL配置publicPath前缀
// vue.config.js module.exports = { publicPath: process.env.NODE_ENV === 'production' ? '/cart/' : '/' }
- 在public/index.htmnl等通过html-webpack-plugin用作模板的HTML文件中,需要设置链接前缀
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
- 在模板中使用先向组件传入BASE_URL
data() { return { publicPath: process.env.BASE_URL, } } // 使用 // <img :src="`${publicPath}my-img.jpg"`">
- 处理资源路径
CSS相关
使用预处理器
# sass npm i -D sass-loader node-sass # Less npm i -D less-loader less # Stylus npm i -D stylus-loader stylus
自动化导入样式
自动化导入样式文件(用于颜色、变量、mixin等),可以使用style-resources-loadernpm i -D style-resources-loader
配置
// vue.config.js const path = required('path') function addStyleResource(rule) { rule.use('style-resource') .loader('style-resources-loader') .options({ patterns: [ path.resolve(__dirname, './src/styles/import.scss'), ] }) } module.exports = { chainWebpack: config => { const types = ['vue-modules', 'vue', 'normal-modules', 'normal'] types.forEach(type => { addStyleResource(config.module.rule('scss').oneOf(type)) }) } }
Scoped CSS
当< style>标签有scoped属性时,它的CSS只作用于当前组件中的元素<style scoped> .red { color: red; } </style>
原理,使用PostCSS实现
<template> <div class="red" data-v-f3f3eg9>hello</div> </template> <style> .red[data-v-f3f3eg9] { color: red; } </style>
混用本地和全局
<style> /* 全局样式 */ </style> <style scoped>/* 本地样式 */</style>
深度作用选择器:使用>>>操作符可以使scoped样式中的一个选择器能够作用的更深
<style scoped> #app >>> a { color: red; } </style>
Sass 等预处理器无法正确解析>>>,这种情况下可以使用/deep/或::deep操作符
<style scoped lang="scss"> #app { /deep/ a { color: red; } ::v-deep a { color: red; } } </style>
CSS Module
CSSModules 是用于模块化和组合CSS的系统。vue-loader提供了与CSSModules的集成,可以作为模拟scoped CSS的替代方案<style module lang="scss"> .red { color: red; } .bold { font-weight: bold; } </style>
模板中通过$style.xxx访问
<a :class="$style.red">awesome-vue</a> <a :class="{[$style.red]: isRed}">awesome-vue</a> <a :class="[$style.red, $style.bold]">awesome-vue</a>
在JS中访问
<script> export default { created() { console.log(this.$style.red); // red_1vyoJ-uz 一个基于文件名和类名生成的标识符 } } </script>
数据访问相关
数据模拟
使用开发服务器配置before选项,可以编写接口,提供模拟数据// vue.config.js devServer: { before(app) { // app是一个express app.get('/api/courses', (req, res) => { res.json([ { name: 'web', price: 10 }, { name: 'api', price: 19 } ]) }) } }
使用
// api/courses.js import axios from 'axios'; export function getCourses() { return axios.get('/api/courses').then((res) => res.data); }
请求代理
设置开发服务器代理选项代理可以有效避免调用接口时出现的跨域问题// vue.config.js devServer: { proxy: 'http://localhost:3000' }
SSR
服务端渲染:将vue实例渲染为HTML字符串直接返回,在前端激活为交互程序
优点
- seo
- 首屏内容到达时间
缺点
- 开发逻辑复杂
- 开发条件限制,如一些生命周期不能使用,一些第三方库会不能用
- 服务器负载大
已存在spa转SSR
- 需要SEO的页面是少数可以考虑预渲染
- 利用puppeteer虚拟运行后在返回(即在服务端模拟浏览器渲染出结果再返回给用户端)
- 重构
- nuxt.js 重写
- 基础实现
使用渲染器将vue实例变成html字符串并返回