1. 搭建工程
vue-cli: https://cli.vuejs.org/zh/
1-1. vue-cli vue-cli是一个脚手架工具,用于搭建vue工程
它内部使用了webpack,并预置了诸多插件(plugin)和加载器(loader),以达到开箱即用的效果
除了基本的插件和加载器外,vue-cli还预置了:
babel
webpack-dev-server
eslint
postcss
less-loader
1-2. SFC 单文件组件,Single File Component,即一个文件就包含了一个组件所需的全部代码
1 2 3 4 5 6 7 8 9 10 11 12 13 <template > </template > <script > export default {} </script > <style > </style >
1-3. 预编译 当vue-cli进行打包 时,会直接把组件中的模板转换为render函数,这叫做模板预编译
这样做的好处在于:
运行时就不再需要编译模板了,提高了运行效率
打包结果中不再需要vue的编译代码,减少了打包体积
2. computed(计算属性) 完整的计算属性书写:
1 2 3 4 5 6 7 8 9 10 computed : { propName : { get ( ){ }, set (val ){ } } }
只包含getter的计算属性简写:
1 2 3 4 5 computed : { propName ( ){ } }
使用方式:
1 2 <div > {{ propName }}</div >
计算属性和方法有什么区别?
1 2 3 4 5 6 7 8 9 10 计算属性本质上是包含getter和setter的方法 当获取计算属性时,实际上是在调用计算属性的getter方法。vue会收集计算属性的依赖,并缓存计算属性的返回结果。只有当依赖变化后才会重新进行计算。 方法没有缓存,每次调用方法都会导致重新执行。 计算属性的getter和setter参数固定,getter没有参数,setter只有一个参数。而方法的参数不限。 由于有以上的这些区别,因此计算属性通常是根据已有数据得到其他数据,并在得到数据的过程中不建议使用异步、当前时间、随机数等副作用操作。 实际上,他们最重要的区别是含义上的区别。计算属性含义上也是一个数据,可以读取也可以赋值;方法含义上是一个操作,用于处理一些事情。
3. props (声明组件的属性、传值)
声明形式:数组和对象
基础形式:数组
1 props : ['title' , 'likes' , 'isPublished' , 'commentIds' , 'author' ]
规定属性类型:
1 2 3 4 5 6 7 8 9 props : { title : String , likes : Number , isPublished : Boolean , commentIds : Array , author : Object , callback : Function , contactsPromise : Promise }
Prop 验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 props : { propA : Number , propB : [String , Number ], propC : { type : String , required : true }, propD : { type : Number , default : 100 }, propE : { type : Object , default : function ( ) { return { message : 'hello' } } }, propF : { validator : function (value ) { return ['success' , 'warning' , 'danger' ].includes (value) } } }
4. v-if 和 v-show
v-if 和 v-show 有什么区别?
1 2 3 4 5 6 7 v-if能够控制是否生成vnode,也就间接控制了是否生成对应的dom。当v-if为true时,会生成对应的vnode,并生成对应的dom元素;当其为false时,不会生成对应的vnode,自然不会生成任何的dom元素。 v-show始终会生成vnode,也就间接导致了始终生成dom。它只是控制dom的display属性,当v-show为true时,不做任何处理;当其为false时,生成的dom的display属性为none。 使用v-if可以有效的减少树的节点和渲染量,但也会导致树的不稳定;而使用v-show可以保持树的稳定,但不能减少树的节点和渲染量。 因此,在实际开发中,显示状态变化频繁的情况下应该使用v-show,以保持树的稳定;显示状态变化较少时应该使用v-if,以减少树的节点和渲染量。
5. 组件事件 (props、event)
抛出事件:子组件在某个时候发生了一件事,但自身无法处理,于是通过事件的方式通知父组件处理
事件参数:子组件抛出事件时,传递给父组件的数据
注册事件:父组件申明,当子组件发生某件事的时候,自身将做出一些处理
事件的调用的写法:
@Click=”handleEvent”
方法参数为默认事件参数
@Click=”handleEvent()”
参数自定义,如果不传则为空;
可以通过关键字 $event 表示默认事件参数
可以自定义参数数量
5-1. v-on 的高阶用法 可以直接将当前组件的父级数据传递给自己的子集(可以实现多个事件传递)
1 2 3 4 5 6 <Ele v-on="{submit: handleSubmit(){}}" ></Ele > <Ele v-on:submit ="handleSubmit" > </Ele > <Ele v-on ="$listeners" > </Ele >
6. 如何测试组件效果?
https://cli.vuejs.org/zh/guide/prototyping.html
概述:快速原型开发 你可以使用 vue serve 和 vue build 命令对单个 *.vue 文件进行快速原型开发,不过这需要先额外安装一个全局的扩展:
安装:
1 npm install -g @vue/cli-service-global
使用:
1 vue serve ./src/components/Pager/test.vue
进阶 :在package.json中添加
1 2 3 4 5 6 7 8 9 10 11 12 13 { ... "scripts" : { ... "test:Pager" : "vue serve ./src/components/Pager/test.vue" , } , "dependencies" : { ... } , "devDependencies" : { ... } }
7. 插槽 问题:在某些组件的模板中,有一部分区域需要父组件来指定
1 2 3 4 5 6 7 8 <div class ="message-container" > <div class ="content" > </div > <button > 确定</button > <button > 关闭</button > </div >
7-1. 插槽的简单用法 此时,就需要使用插槽来定制组件的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <div class ="message-container" > <div class ="content" > <slot > </slot > </div > <button > 确定</button > <button > 关闭</button > </div > <Message > <div class ="app-message" > <p > App Message</p > <a href ="" > detail</a > </div > </Message > <div class ="message-container" > <div class ="content" > <div class ="app-message" > <p > App Message</p > <a href ="" > detail</a > </div > </div > <button > 确定</button > <button > 关闭</button > </div >
7-2. 具名插槽 如果某个组件中需要父元素传递多个区域的内容,也就意味着需要提供多个插槽
为了避免冲突,就需要给不同的插槽赋予不同的名字
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <div class ="layout-container" > <header > <slot name ="header" > </slot > </header > <main > <slot > </slot > </main > <footer > <slot name ="footer" > </slot > </footer > </div > <BaseLayout > <template v-slot:header > <h1 > Here might be a page title</h1 > </template > <template v-slot:default > <p > A paragraph for the main content.</p > <p > And another one.</p > </template > <template v-slot:footer > <p > Here's some contact info</p > </template > </BaseLayout >
v-slot:xxx 可以简写为 #xxx
8. 路由
如何根据地址中的路径选择不同的组件?
把选择的组件放到哪个位置?
如何无刷新的切换组件?
8-1. 路由插件 8-1-1. 安装
8-1-2. 路由插件的使用 1 2 3 4 5 6 7 8 9 10 11 12 import Vue from 'vue' import VueRouter from 'vue-router' Vue .use (VueRouter ); const router = new VueRouter ({ }) new Vue ({ ..., router })
8-1-3. 路由的基本使用 1 2 3 4 5 6 7 8 9 const router = new VueRouter ({ routes : [ { path : '/foo' , component : Foo }, { path : '/bar' , component : Bar } ] })
1 2 3 4 5 6 7 8 9 10 11 <div class ="container" > <div > </div > <div > <RouterView /> </div > </div >
8-2. 路由模式 路由模式决定了:
路由从哪里获取访问路径
路由如何改变访问路径
vue-router提供了三种路由模式:
hash:默认值。路由从浏览器地址栏中的hash部分获取路径,改变路径也是改变的hash部分。该模式兼容性最好。
1 2 http://localhost:8081/#/blog --> /blog http://localhost:8081/about#/blog --> /blog
history:路由从浏览器地址栏的location.pathname中获取路径,改变路径使用的H5的history api。该模式可以让地址栏最友好,但是需要浏览器支持history api
1 2 3 http://localhost:8081/#/blog --> / http://localhost:8081/about#/blog --> /about http://localhost:8081/blog --> /blog
history api
1 2 3 4 history.pushState (null , null , '/blog' );
abstract:路由从内存中获取路径,改变路径也只是改动内存中的值。这种模式通常应用到非浏览器环境中。
1 2 3 内存: / --> / 内存: /about --> /about 内存: /blog --> /blog
8-3. 导航 vue-router提供了全局的组件RouterLink,它的渲染结果是一个a元素
1 2 3 4 5 6 7 8 <RouterLink to ="/blog" > 文章</RouterLink > <a href ="#/blog" > 文章</a > <a href ="/blog" > 文章</a >
8-4. 激活状态 默认情况下,vue-router会用 当前路径 匹配 导航路径 :
如果当前路径是以导航路径开头,则算作匹配,会为导航的a元素添加类名router-link-active
如果当前路径完全等于导航路径,则算作精确匹配,会为导航的a元素添加类名router-link-exact-active
例如,当前访问的路径是/blog,则:
导航路径
类名
/
router-link-active
/blog
router-link-active router-link-exact-active
/about
无
/message
无
可以为组件RouterLink添加bool属性exact,将匹配规则改为:必须要精确匹配才能添加匹配类名router-link-active
例如,当前访问的路径是/blog,则:
导航路径
exact
类名
/
true
无
/blog
false
router-link-active router-link-exact-active
/about
true
无
/message
true
无
例如,当前访问的路径是/blog/detail/123,则:
导航路径
exact
类名
/
true
无
/blog
false
router-link-active
/about
true
无
/message
true
无
另外,可以通过active-class属性更改匹配的类名,通过exact-active-class更改精确匹配的类名
8-5. 命名路由 使用命名路由可以解除系统与路径之间的耦合
1 2 3 4 5 6 7 8 9 const router = new VueRouter ({ routes : [ { name :"foo" , path : '/foo' , component : Foo }, { name :"bar" , path : '/bar' , component : Bar } ] })
1 2 <RouterLink :to ="{ name:'foo' }" > go to foo</RouterLink >
8-6. 动态路由 问题:我们希望下面的地址都能够匹配到Blog组件
/article,显示全部文章
/article/cate/1,显示分类id为1的文章
/article/cate/3,显示分类id为3的文章
…
第一种情况很简单,只需要将一个固定的地址匹配到Blog组件即可
1 2 3 4 5 { path : "/article" , name : "Blog" , component : Blog }
但后面的情况则不同:匹配到Blog组件的地址中,有一部分是动态变化的,则需要使用一种特殊的表达方式:
1 2 3 4 5 { path : "/article/cate/:categoryId" , name : "CategoryBlog" , component : Blog }
在地址中使用:xxx,来表达这一部分的内容是变化的,在vue-router中,将变化的这一部分称之为params,可以在vue组件中通过this.$route.params来获取
1 2 3 4 this .$route .params this .$route .params
8-7. 动态路由的导航 1 2 3 4 5 6 7 8 <router-link to ="/article/cate/3" > to article of category 3</router-link > <router-link :to ="{ name: 'CategoryBlog', params: { categoryId: 3 } }" > to article of category 3</router-link >
8-8. 编程式导航 除了使用<RouterLink>超链接导航外,vue-router还允许在代码中跳转页面
1 2 3 4 5 6 7 8 9 10 11 12 13 this .$router .push ("跳转地址" ); this .$router .push ({ name :"Blog" }) this .$router .push ({ name :"Blog" , params : {}, query :{} }) this .$router .go (-1 );
9. css module 什么是 CSS Module?
CSS Modules 不是官方规范或浏览器中的实现,而是构建步骤中的一个过程(在 Webpack 或 Browserify 的帮助下),它改变了类名和选择器的作用域(即有点像命名空间)。
目的:解决 CSS 中全局作用域的问题
使用:
需要将样式文件命名为xxx.module.ooo
xxx为文件名
ooo为样式文件后缀名,可以是css、less
示例:
1 2 3 4 5 6 7 8 .header { ... } .footer { ... }
1 2 3 4 5 6 7 import styles from "./global.module.css" ;export default function ( ) { let header = `<div class="${styles.header} "></div>` ; header.className = `${styles.footer} ` ; return header; }
实现原理:
产生局部作用域的唯一方法,就是使用一个独一无二的 class 的名字,不会与其他选择器重名。这就是 CSS Modules 的做法。
10. 得到组件渲染的Dom 1 2 3 4 5 6 7 8 9 10 function getComponentRootDom (comp, props ){ const vm = new Vue ({ render : h => h (comp, {props}) }) vm.$mount(); return vm.$el ; }
11. 扩展vue实例
1 2 3 4 5 6 7 8 import Vue from "vue" ;Vue .prototype .$sayHello = function ( ) { console .log ("Hello!!!!" ); };
12. ref 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <template > <div > <p ref ="para" > some paragraph</p > <ChildComp ref ="comp" /> <button @click ="handleClick" > 查看所有引用</button > </div > </template > <script > import ChildComp from "./ChildComp" export default { components :{ ChildComp }, methods :{ handleClick ( ){ console .log (this .$refs ); } } } </script >
通过ref可以直接操作dom元素,甚至可能直接改动子组件,这些都不符合vue的设计理念。 除非迫不得已,否则不要使用ref
13. 组件生命周期
13-1. 常见应用
注意: 不要死记硬背,要根据具体情况灵活处理
13-2. 加载远程数据 1 2 3 4 5 6 7 8 9 10 export default { data ( ){ return { news : [] } }, async created ( ){ this .news = await getNews (); } }
13-3. 直接操作DOM 1 2 3 4 5 6 7 8 9 10 11 12 export default { data ( ){ return { containerWidth :0 , containerHeight :0 } }, mounted ( ){ this .containerWidth = this .$refs .container .clientWidth ; this .containerHeight = this .$refs .container .containerHeight ; } }
13-4. 启动和清除计时器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export default { data ( ){ return { timer : null } }, created ( ){ this .timer = setInterval (()=> { ... }, 1000 ) }, destroyed ( ){ clearInterval (this .timer ); } }
14. 自定义指令 14-1. 定义指令 14-1-1. 全局定义 1 2 3 4 5 6 7 8 9 Vue .directive ('mydirec1' , { }) Vue .directive ('mydirec2' , { })
之后,所有的组件均可以使用mydirec1和mydirec2指令
1 2 3 4 5 6 7 8 9 <template > <div > <MyComp v-mydirec1 ="js表达式" /> <div v-mydirec2 ="js表达式" > ... </div > </div > </template >
14-1-2. 局部定义 局部定义是指在某个组件中定义指令,和局部注册组件类似。
定义的指令仅在该组件中有效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <template > <div > <MyComp v-mydirec1 ="js表达式" /> <div v-mydirec2 ="js表达式" > ... </div > </div > </template > <script > export default { directives : { mydirec1 : { }, mydirec2 : { } } } </script >
和局部注册组件一样,为了让指令更加通用,通常我们会把指令的配置提取到其他模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <template > <div > <MyComp v-mydirec1 ="js表达式" /> <div v-mydirec2 ="js表达式" > ... </div > <img v-mydirec1 ="js表达式" /> </div > </template > <script > import mydirec1 from "@/directives/mydirec1" ; import mydirec2 from "@/directives/mydirec2" ; export default { directives : { mydirec1, mydirec2 } } </script >
14-2. 指令配置对象 没有配置的指令,就像没有配置的组件一样,毫无意义
vue支持在指令中配置一些钩子函数 ,在适当的时机,vue会调用这些钩子函数并传入适当的参数,以便开发者完成自己想做的事情。
常用的钩子函数:
1 2 3 4 5 6 7 8 9 10 11 12 { bind ( ){ }, inserted ( ){ }, update ( ){ } }
查看更多的钩子函数
每个钩子函数在调用时,vue都会向其传递一些参数,其中最重要的是前两个参数
1 2 3 4 5 6 7 8 { bind (el, binding ){ } }
14-2-1. bingding 对象
binding:一个对象,包含以下 property:
name:指令名,不包括 v- 前缀。
value:指令的绑定值,例如:v-my-directive=”1 + 1” 中,绑定值为 2。
oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
expression:字符串形式的指令表达式。例如 v-my-directive=”1 + 1” 中,表达式为 “1 + 1”。
arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 “foo”。
modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
查看更多bingding对象的属性
14-3. 配置简化 比较多的时候,在配置自定义指令时,我们都会配置两个钩子函数
1 2 3 4 5 6 7 8 { bind (el, bingding ){ }, update (el, bingding ){ } }
这样,在元素绑定和更新时,都能运行到钩子函数
如果这两个钩子函数实现的功能相同,可以直接把指令配置简化为一个单独的函数:
1 2 3 function (el, bingding ){ }
15. 组件的混入 有的时候,许多组件有着类似的功能,这些功能代码分散在组件不同的配置中。
于是,我们可以把这些配置代码抽离出来,利用混入 融合到组件中。
具体的做法非常简单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const common = { data ( ){ return { a : 1 , b : 2 } }, created ( ){ console .log ("common created" ); }, computed :{ sum ( ){ return this .a + this .b ; } } } const comp1 = { mixins : [common] created ( ){ console .log ("comp1 created" , this .a , this .b , this .sum ); } }
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。 比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先 。
更多细节参见官网
16. 组件递归 组件是可以在它们自己的模板中调用自身的。不过它们只能通过 name 选项来做这件事:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <template > ... <RightList /> ... </template > <script > export default { name : "RightList" , props : { ... } }; </script >
17. watch 利用watch配置,可以直接观察某个数据的变化,变化时可以做一些处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 export default { watch : { $route(newVal, oldVal){ }, $route : { handler (newVal, oldVal ){}, deep : false , immediate : false } ["$route.params" ](newVal, oldVal){ }, ["$route.params" ]: { handler (newVal, oldVal ){}, deep : false , immediate : false } } }
18. 事件修饰符 针对dom节点的原生事件 ,vue支持多种修饰符以简化代码
详见:事件修饰符、按键修饰符、系统修饰符
19. $listeners $listeners是vue的一个实例属性,它用于获取父组件传过来的所有事件函数
1 2 <Child @event1 ="handleEvent1" @event2 ="handleEvent2" />
$emit和$listeners通信的异同
相同点:均可实现子组件向父组件传递消息
差异点:
$emit更加符合单向数据流,子组件仅发出通知,由父组件监听做出改变;而$listeners则是在子组件中直接使用了父组件的方法。
调试工具可以监听到子组件$emit的事件,但无法监听到$listeners中的方法调用。(想想为什么)
由于$listeners中可以获得传递过来的方法,因此调用方法可以得到其返回值。但$emit仅仅是向父组件发出通知,无法知晓父组件处理的结果 对于上述中的第三点,可以在$emit中传递回调函数来解决
父组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <template > <Child @click ="handleClick" /> </template > <script > import Child from "./Child" export default { components :{ Child }, methods :{ handleClick (data, callback ){ console .log (data); setTimeout (()=> { callback (1 ); }, 3000 ) } } } </script >
子组件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <template > <button @click ="handleClick" > click</button > </template > <script > export default { methods :{ handleClick ( ){ this .$emit("click" , 123 , (data )=> { console .log (data); }) } } } </script >
20. v-model v-model指令实质是一个语法糖,它是value属性和input事件的结合体
1 2 3 <input :value ="data" @input ="data=$event.target.value" /> <input v-model ="data" />
详见:表单输入绑定
21. 事件总线
事件总线功能:
提供监听某个事件的接口
提供取消监听的接口
触发事件的接口(可传递数据)
触发事件后会自动通知监听者
21-1. 自己实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 const listeners = {};export default { $on(eventName, handler) { if (!listeners[eventName]) { listeners[eventName] = new Set (); } listeners[eventName].add (handler); }, $off(eventName, handler) { if (!listeners[eventName]) { return ; } listeners[eventName].delete (handler); }, $emit(eventName, ...args) { if (!listeners[eventName]) { return ; } for (const handler of listeners[eventName]) { handler (...args); } }, };
21-2. 通过vue实现 1 2 3 4 import Vue from "vue" ;Vue .prototype .$bus = new Vue ({});
在main.js文件中导入即可全局使用,当然也可局部引用;
1 2 3 4 5 6 7 import Vue from "vue" ;import App from "./App.vue" ;import "./eventBus" ;new Vue ({ render : (h ) => h (App ), }).$mount("#app" );
22. Vuex 数据共享 Vuex官方文档
在vue中遇到共享数据 ,会带来下面的多个问题:
如何保证数据的唯一性?
如果数据不唯一,则会浪费大量的内存资源,降低运行效率
如果数据不唯一,就可能出现不统一的数据,难以维护
某个组件改动数据后,如何让其他用到该数据的组件知道数据变化了?
事件总线貌似可以解决该问题,但需要在组件中手动的维护监听,极其不方便,而且事件总线的目的在于「通知」,而不是「共享数据」
一种比较容易想到的方案,就是把所有的共享数据全部 提升到根组件,然后通过属性不断下发,当某个组件需要修改数据时,又不断向上抛出事件,直到根组件完成对数据的修改。
这种方案的缺陷也非常明显:
需要编写大量的代码层层下发数据,很多组件被迫拥有了自己根本不需要的数据
需要编写大量的代码层层上抛事件,很多组件被迫注册了自己根本处理不了的事件
基于上面的问题,我们可以简单的设置一个独立的数据仓库 。
要实现这一切,可以选择vuex
22-1. 安装vuex
22-2. 创建仓库 安装vuex后,可以通过下面的代码创建一个数据仓库,在大部分情况下,一个工程仅需创建一个数据仓库
1 2 3 4 5 6 7 8 9 10 11 12 import Vuex from "vuex" ;import Vue from "vue" ;Vue .use (Vuex ); const store = new Vuex .Store ({ state : { count : 0 } }) export default store;
仓库创建好后,你可以使用store.state来访问仓库中的数据 如果希望在vue中方便的使用仓库数据,需要将vuex作为插件安装
1 2 3 4 5 6 7 8 import Vue from "vue" ;import App from "./App.vue" ;import store from "./store.js" ;new Vue ({ store, render : h => h (App ) }).$mount("#app" );
之后,在vue组件中,可以通过实例的$store属性访问到仓库
Vuex会自动将配置的状态数据设置为响应式数据,当数据变化时,依赖该数据的组件会自动渲染。
22-3. 数据的变更 尽管可以利用数据响应式的特点直接变更数据,但这样的做法在大型项目中会遇到问题
如果有一天,你发现某个共享数据是错误的,而有一百多个组件都有可能变更过这块数据,你该如何知道是哪一步数据变更出现了问题?
为了能够更好的跟踪数据的变化,vuex强烈建议使用mutation来更改数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const store = new Vuex ({ state : { count : 0 }, mutations : { increase (state ){ state.count ++; }, decrease (state ){ state.count --; }, power (state, payload ){ state.count **= payload; } } })
当我们有了mutation后,就不应该直接去改动仓库的数据了 而是通过store.commit方法提交一个mutation,具体做法是
1 store.commit ("mutation的名字" , payload);
现在,我们可以通过vue devtools观测到数据的变化了
特别注意:
mutation中不得出现异步操作
在实际开发的规范中,甚至要求不得有副作用操作
副作用操作包括:
异步
更改或读取外部环境的信息,例如localStorage、location、DOM等
提交mutation是数据改变的唯一原因
22-4. 异步处理 如果在vuex中要进行异步操作,需要使用action
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const store = new Vuex ({ state : { count : 0 }, mutations : { increase (state ){ state.count ++; }, decrease (state ){ state.count --; }, power (state, payload ){ state.count **= payload; } }, actions : { asyncPower (ctx, payload ){ setTimeout (function ( ){ ctx.commit ("power" , payload) }, 1000 ) } } })
使用示例:
1 2 3 4 5 6 7 8 9 10 export default { methods : { handleDecrease ( ) { this .$store .commit ("decrease" ); }, handleAsyncIncrease ( ) { this .$store .dispatch ("asyncIncrease" ); }, }, };
22-5. 进阶用法(modules) 配置样例
index.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import Vuex from 'vuex' ;import Vue from 'vue' ;import loginUser from './loginUser' ;import counter from './counter' ;Vue .use (Vuex );const store = new Vuex .Store ({ modules : { loginUser, counter }, strict : true , }); export default store;
loginUser.js:
1 2 3 4 5 6 7 8 export default { state : { loading : false , user : null }, mutations : {}, actions : {} }
counter.js:
1 2 3 4 5 6 7 export default { state : { count :0 }, mutations : {}, actions : {} }
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { mapState } from "vuex" ;export default { data ( ) { return { loginId : "" , loginPwd : "" , }; }, computed : mapState ("loginUser" , ["loading" ]), methods : { handleSubmit ( ) { this .$store .dispatch ("loginUser/login" , { loginId : this .loginId , loginPwd : this .loginPwd , }); }, }, };
23. 打包结果分析 模式:
模式是 Vue CLI 项目中一个重要的概念。默认情况下,一个 Vue CLI 项目有三个模式:
development 模式用于 vue-cli-service serve 即 npm run serve
test 模式用于 vue-cli-service test:unit 即 npm run build
production 模式用于 vue-cli-service build 和 vue-cli-service test:e2e
23-1. 分析打包结果 由于vue-cli是利用webpack进行打包,我们仅需加入一个webpack插件webpack-bundle-analyzer即可分析打包结果
23-1-1. 安装 1 npm install webpack-bundle-analyzer -D
23-1-2. 配置 为了避免在开发环境中启动webpack-bundle-analyzer,我们仅需使用以下代码即可
1 2 3 4 5 6 7 8 9 10 11 const BundleAnalyzerPlugin = require ("webpack-bundle-analyzer" ) .BundleAnalyzerPlugin ; module .exports = { ... configureWebpack : { plugins : [new BundleAnalyzerPlugin ()] }, };
完场上述配置后,运行npm run build即可显示打包分析;
但是运行npm run server也会显示打包分析,这个开发时一般是不需要的;
23-1-3. 自定义动态配置 如果你需要基于环境有条件地配置行为,或者想要直接修改配置,那就换成一个函数 (该函数会在环境变量被设置之后懒执行)。该方法的第一个参数会收到已经解析好的配置。在函数内,你可以直接修改配置,或者返回一个将会被合并的对象:
1 2 3 4 5 6 7 8 9 10 module .exports = { configureWebpack : config => { if (process.env .NODE_ENV === 'production' ) { } else { } } }
当然也可以提取到其他文件导入使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const BundleAnalyzerPlugin = require ("webpack-bundle-analyzer" ) .BundleAnalyzerPlugin ; if (process.env .NODE_ENV === "production" ) { module .exports = { devtool : "none" , plugins : [new BundleAnalyzerPlugin ()], externals : { vue : "Vue" , vuex : "Vuex" , "vue-router" : "VueRouter" , axios : "axios" , }, }; } else { module .exports = {}; }
1 2 3 4 5 6 7 8 9 10 11 module .exports = { devServer : { proxy : { "/api" : { target : "http://test.my-site.com" , }, }, }, configureWebpack : require ("./webpack.config" ), };
23-2. 优化公共库打包体积 23-2-1. 使用CDN CDN全称为Content Delivery Network,称之为内容分发网络
它的基本原理是:架设多台服务器,这些服务器定期从源站拿取资源保存本地,到让不同地域的用户能够通过访问最近的服务器获得资源
我们可以把项目中的所有静态资源都放到CDN上(收费),也可以利用现成免费的CDN获取公共库的资源
首先,我们需要告诉webpack不要对公共库进行打包
1 2 3 4 5 6 7 8 9 10 module .exports = { configureWebpack : { externals : { vue : "Vue" , vuex : "Vuex" , "vue-router" : "VueRouter" , } }, };
然后,在页面中手动加入cdn链接,这里使用bootcn
1 2 3 4 5 6 7 8 9 10 <body > <div id ="app" > </div > <% if(NODE_ENV === "production") { %> <script src ="https://cdn.bootcdn.net/ajax/libs/vue/2.6.12/vue.min.js" > </script > <script src ="https://cdn.bootcdn.net/ajax/libs/vuex/3.5.1/vuex.min.js" > </script > <script src ="https://cdn.bootcdn.net/ajax/libs/vue-router/3.4.7/vue-router.min.js" > </script > <% } %> </body >
对于vuex和vue-router,使用这种传统的方式 引入的话会自动成为Vue的插件,因此需要去掉Vue.use(xxx)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import Vue from "vue" ;import Vuex from "vuex" ;if (!window .Vuex ){ Vue .use (Vuex ); } import VueRouter from "vue-router" ;import Vue from "vue" ;if (!window .VueRouter ){ Vue .use (VueRouter ); }
23-2-2. 启用现代模式 为了兼容各种浏览器,vue-cli在内部使用了@babel/present-env对代码进行降级,你可以通过.browserlistrc配置来设置需要兼容的目标浏览器
这是一种比较偷懒 的办法,因为对于那些使用现代浏览器的用户,它们也被迫 使用了降级之后的代码,而降低的代码中包含了大量的polyfill,从而提升了包的体积
因此,我们希望提供两种打包结果:
降级后的包(大),提供给旧浏览器用户使用
未降级的包(小),提供给现代浏览器用户使用
除了应用webpack进行多次打包外,还可以利用vue-cli给我们提供的命令:
1 vue-cli-service build --modern
这里运行打包后会在dist文件夹下生成两份打包文件:
app.xxx.js (为降级:体积小)
app-legacy.xxx.js (降级后、兼容性代码:体积大)
打包后html中已自动生成兼容引入方式无需在做修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <!DOCTYPE html > <head > ... <link href ="/css/about.343abd82.css" rel ="prefetch" > <link href ="/css/app.d7ea4d3b.css" rel ="preload" as ="style" > <link href ="/js/app.5bc72638.js" rel ="modulepreload" as ="script" > <link href ="/css/app.d7ea4d3b.css" rel ="stylesheet" > </head > <body > ... <script type ="module" src ="/js/app.5bc72638.js" > </script > <script src ="/js/app-legacy.abf7d130.js" nomodule > </script > </body >
link标签属性rel:
rel 属性规定当前文档与被链接文档之间的关系
rel="preload":
预加载,先下载下来,暂时不用,将来如果需要使用直接使用预下载的文件
as="style" 当做样式文件处理
可以让浏览器今早下载将来要用的文件
rel="modulepreload":
只有现代浏览器支持
预加载,先下载下来,暂时不用,将来如果需要使用直接使用预下载的文件
as="script" 把href文件当做ES Module处理
rel="prefetch":
表示预提取,告诉浏览器加载下一页面可能会用到的资源
浏览器会利用空闲状态进行下载并将资源存储到缓存中
preload 和 prefetch 的区别:
preload 是告诉浏览器页面必定需要的资源,浏览器一定会预先加载这些资源
prefetch 是告诉浏览器下一个页面可能需要的资源,浏览器不一定会加载这些资源
在VUE SSR生成的页面中,首页的资源均使用preload,而路由对应的资源,则使用prefetch
对于当前页面很有必要的资源使用 preload,对于可能在将来的页面中使用的资源使用 prefetch
23-3. 优化项目包体积 这里的项目包是指src目录中的打包结果
23-3-1. 页面分包 默认情况下,vue-cli会利用webpack将src目录中的所有代码打包成一个bundle
这样就导致访问一个页面时,需要加载所有页面的js代码
我们可以利用webpack对动态import的支持,从而达到把不同页面的代码打包到不同文件中
1 2 3 4 5 6 7 8 9 10 11 12 13 export default [ { name : "Home" , path : "/" , component : () => import ( "@/views/Home" ), }, { name : "About" , path : "/about" , component : () => import ("@/views/About" ), } ];
23-4. 优化首屏响应
首页白屏受很多因素的影响
vue页面需要通过js构建,因此在js下载到本地之前,页面上什么也没有
一个非常简单有效的办法,即在页面中先渲染一个小的加载中效果,等到js下载到本地并运行后,即会自动替换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <div id ="app" > <style > .loading-img { position : fixed; width : 200px ; height : 200px ; left : 50% ; top : 50% ; margin-left : -100px ; margin-top : -100px ; } </style > <img src ="loading.gif" /> </div >
24. 异步组件 在代码层面,vue组件本质上是一个配置对象
1 2 3 4 5 6 var comp = { props : xxx, data : xxx, computed : xxx, methods : xxx }
但有的时候,要得到某个组件配置对象需要一个异步的加载过程,比如:
需要使用ajax获得某个数据之后才能加载该组件
为了合理的分包,组件配置对象需要通过import(xxx)动态加载
如果一个组件需要通过异步的方式得到组件配置对象 ,该组件可以把它做成一个异步组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const AsyncComponent = ( ) => import ("./MyComp" )var App = { components : { AsyncComponent } }
异步组件的函数不仅可以返回一个Promise,还支持返回一个对象
详见:返回对象格式的异步组件
24-1. 应用 异步组件通常应用在路由懒加载中,以达到更好的分包
为了提高用户体验,可以在组件配置对象加载完成之前给用户显示一些提示信息
1 2 3 4 5 6 7 8 var routes = [ { path : "/" , component : async () => { console .log ("组件开始加载" ); const HomeComp = await import ("./Views/Home.vue" ); console .log ("组件加载完毕" ); return HomeComp ; } } ]
推荐使用NProgress 展现一个进度条
25. NProgress 25-1. 安装 1 $ npm install --save nprogress
25-2. 使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import "nprogress/nprogress.css" ;import { start, done, configure } from "nprogress" ;configure ({ trickleSpeed : 20 , showSpinner : false , }); function getPageComponent (pageCompResolver ) { return async () => { start (); const comp = await pageCompResolver (); done (); return comp; }; }; export default [ { name : "Home" , path : "/" , component : getPageComponent (() => import ( "@/views/Home" ) ), }, ... ]
__END__