Open libin1991 opened 5 years ago
如果在简历上写“XX电商系统”的实现,其实第一直觉是这个人一定是从培训班出来的。而我们“项目管理”课程正好就是做一个小型电商网站。开发时长一个月左右,包含买家端、卖家端、管理员端,虽然业务逻辑比较常见,但是这次开发仍有收获,最重要的一点收获就是 对Vuex有了真正的实践和认识。 所以,本文大部分介绍Vuex在该项目上的实践以及所踩的坑,另外的部分则是项目中一些其他要点的总结。
前面的几次项目开发,拿到后端接口和需求之后,会按照以下的方式去实现功能:
Promise
then
第1、2、3步基本上是没太大问题的,问题就出在第4步,其结果是导致一个Vue文件越来越大,到后面一个复杂的页面vue文件将会包含如下内容:
data
控制是否展示元素的
渲染用数据
用于"上锁"的变量
created
mounted
请求
回调处理
template
props
最后的结果就是vue文件变得臃肿。
文件越长,可读难度会提高,可维护性会降低。
谁也不喜欢看一个包含了逻辑处理、界面处理、数据处理的冗杂代码文件。
所以,为了解决上述问题,引入 Vuex 是非常必要的。
Vuex
其实,之前也一直在使用Vuex,不过用的目的仅仅是为了“保存一点数据能够全局使用”而已,根本没有深入使用Action,Getters,Mutations等。 本文关于Vuex不会过多介绍其原理以及好处,主要关注实践以及使用心得,Vuex介绍或入门请移步官方文档。
“保存一点数据能够全局使用”
Action
Getters
Mutations
笔者所做的,是将关于数据请求和更新,基本都放到了Vuex里,让Vuex成为了整个项目的“数据集散中心”,而vue组件里,仅仅是进行事件调度(dispatch)和绑定来自Vuex的数据。
“数据集散中心”
vue
事件调度(dispatch)
绑定来自Vuex的数据
我们以商品详情页面举例,其包含如下的功能:
如图所示:
如果我们不使用Vuex,代码的结构可能如下:
<template> <template> <script> export default { data() { detail: { name: '', shopId: '', id: '', pic: '', description: '', price: '', updateTime: '', categoryId: '', stock: 0, createTime: '', attributeList: {}, collectId: 0, }, // 一大堆商品的字段数据 【MARK】 }, created(){ this.$http.get('/product/:id') .then() // 以及一端更新其他数据的请求 【MARK】 }, methods(){ clickCart(){ this.$http.get('xxx') .then('xxx') this.$emit('xxx'); // 与上层组件通信更新购物车的红点 } // 一堆用户事件的触发方法,每个方法内都是一堆http请求和回调处理 【MARK】 }, computed:{ // 一些基于商品数据判断状态的数据 【MARK】 } } </script> <style> <!-- 忽略相关样式代码--> <style> 复制代码
如果按照上述的方式来写,虽然可以保证关于这个页面的逻辑处理都保留在这个vue文件内,但是却显得十分臃肿,如果后续要加更多的功能抑或是修改,其需要读的代码可能更多。
所以,以上代码示例中的被【MARK】标记的部分,将是Vuex可以优化的。 最后的代码如下图所示:
【MARK】
代码的可读性提高了不少:
【触发事件】-【回调】
关于数据处理的代码,全部封装到了Vuex上了:
我们在Vuex层,不仅请求了数据,同时对返回的数据做了判断以及不同的逻辑处理,同时设置Getters使的页面能够基于Getters提供的数据做各类二元判断。
二元判断
Vuex层带来了如下的好处:
Actions
加入购物车
添加商品进购物车的同时更新商品数据并更新购物车数据
HTTP
数据仓库
$emit
直观体验来说,彻底使用Vuex后,编码思路清晰不少,因为写完整体的一个功能需要2~3天,而Vuex对于“间断性编程”非常有帮助,能够迅速捡起上一次开发的进度。
“间断性编程”
使用Vuex之后就是按照如下的步骤进行开发了:
(第1、2、3步不变,如前文) 4.构建Vuex的Store模型,定义所需的方法。 5.编写组件并绑定在Vuex上的相关数据和方法。 6.编写触发dispatch后的回调。
dispatch
使用Vuex存在一定的规范,在官网上已经陈列了诸如表单处理,测试,项目结构的规范,而下面的一些原则仅仅是笔者的一些经验之谈,不一定正确,有所帮助便是最好的。
表单处理
测试
项目结构
最初我的设计是在Action请求数据之后同时也负责所有的跳转逻辑和弹窗逻辑,不过,这样做又让视图层和数据层耦合了,使得我抽象出来的Action的复用性变得极差,同时,在vue文件看到关于视图层的处理才算是比较正常的编写逻辑。
跳转逻辑
弹窗逻辑
视图层
数据层
mutation
action
正如上一条原则中提到的要提高action的复用性,那么就要保证需要复用的action只做一个功能,如更新A数据和更新B数据不要直接融合成一个action,而是分别定义两个action,并通过组合的方式来实现一个更复杂的功能,有点类似精简指令集的思想。
更新A数据
更新B数据
如:
actions:{ updateA(){}, // A 可能在其他地方被单独使用 updateB(){}, // B 可能在其他地方被单独使用 updateAandB({dispatch}){ dispatch("updateA"); dispatch("updateB"); }, // 同时触发 A B 的复杂action } 复制代码
为了让dispatch函数拥有Promise风格的处理回调的能力,可以让action的返回值作为一个Promise对象,如:
Promise风格
// store.js actions:{ updateA(){ return Promise.resolve() } } 复制代码
在组件内就可以使用.then:
.then
... created(){ this.$store.dispatch('updateA') .then(() => { ... }) } ... 复制代码
而Vuex一般建议在actions进行异步操作,所以为了让代码更加优雅,可以用AA如下编写:
actions
AA
// store.js actions:{ async updateA(){ const result = await ajax('/**/**'); if(result){ return Promise.resolve() } } } 复制代码
所以,在实践中的代码如图:
一般使用vuex,会根据实体或者页面来拆分state形成module,一个module里包含其所需的所有actions,states,mutation等。如电商里,一般有商品,订单,用户这几个实体可以拆成module,拆分出来之后如图:
actions,states,mutation
商品
订单
用户
拆分的好处是更加独立直观了,坏处是如果不使用命名空间,可能造成actions,mutations冲突,正好Vuex也提供namespace的配置,参考官方文档即可。
mutations
namespace
虽然总结到了原则里,不过这次并没有使用namespace,算是这次比较大的不足了,解决的办法是”手动namespace“,如图:
将actions的方法名前缀加上该实体名用于区分不同的方法,实在是比较拙劣的手段了。
情况发生在获取到异步数据之后的代码,一般如下这段代码会大量重复:
if (result.code){ return Promise.resolve(data); } return Promise.reject(msg); 复制代码
虽然不算多,但是这逻辑仍然有可抽象出来的可能性,下次打算在RootState里添加一个action,用于处理这样的后端返回数据,期望结果如下:
RootState
actions: { parseResult({}, result){ if (result.code){ return Promise.resolve(data); } return Promise.reject(msg); }, async getA({dispatch}){ const result = await http.get('xx/xx'); return dispatch('parseResult', result); // One line , it's clean } } 复制代码
3.0 基本在配置上做到极简化了。很久没开发Vue的项目了,记得上一个Vue项目里,仍然有一大堆xxx.congfig.js,而现在,基本集成到了CLI内部了,通过一个vue.config.js可满足大部分开发需求。本项目配置如下图:
极简化
Vue
xxx.congfig.js
vue.config.js
比以前的配置更加直观了,更多请参考:cli.vuejs.org/zh/,至于插件机制,本次开发未用上,只是在安装第三方包的时候可以用vue add了,有GUI可以查看安装了哪些插件。
vue add
Vue.use 和 Vue.install
plugins
本项目是一个多页面应用,总共有3个Vue实例,所以希望能够按需注入想要的第三方依赖如axios,qrcode,lazyload等一些工具库或第三方组件。于是都将其汇总到plugins目录下了,如下图:
axios
qrcode
lazyload
这样封装的好处是,我可以对每个Vue实例按需导入第三方依赖,一行import就可以,如下图:
一个大型的web app 打包压缩后都不超过500kb,所以就以此作为Benchmark来优化该项目打包后的大小,按需引入第三方依赖(特别是Element和lodash)减少了不少项目体积。不过最主要的还是得学会用webpack analyse tool来分析。
Benchmark
按需引入
Element
lodash
webpack analyse tool
总之,本次项目主要是刷新了对Vuex的认知,其能够让我们编写的vue组件可读性和可维护性更高。同时,也能够让数据更新这块的代码复用性更高。通过Vuex来构建一个中心化的数据集散中心还能让开发的思路更顺畅,值得学习。
中心化的数据集散中心
参考:
Vuex 是什么?
如果在简历上写“XX电商系统”的实现,其实第一直觉是这个人一定是从培训班出来的。而我们“项目管理”课程正好就是做一个小型电商网站。开发时长一个月左右,包含买家端、卖家端、管理员端,虽然业务逻辑比较常见,但是这次开发仍有收获,最重要的一点收获就是 对Vuex有了真正的实践和认识。 所以,本文大部分介绍Vuex在该项目上的实践以及所踩的坑,另外的部分则是项目中一些其他要点的总结。
过去那臃肿的Vue组件
前面的几次项目开发,拿到后端接口和需求之后,会按照以下的方式去实现功能:
Promise
的then
方法里进行数据更新或者做更多的业务处理。好像没什么问题哈?
第1、2、3步基本上是没太大问题的,问题就出在第4步,其结果是导致一个Vue文件越来越大,到后面一个复杂的页面vue文件将会包含如下内容:
data
里大量的数据字段,有控制是否展示元素的
,渲染用数据
,用于"上锁"的变量
等。created
或mounted
里包含了大量的请求
和回调处理
。template
里使用的组件的属性内,绑定了大量的props
。最后的结果就是vue文件变得臃肿。
文件越长,可读难度会提高,可维护性会降低。
谁也不喜欢看一个包含了逻辑处理、界面处理、数据处理的冗杂代码文件。
所以,为了解决上述问题,引入
Vuex
是非常必要的。干净、解耦合、职责分明
笔者所做的,是将关于数据请求和更新,基本都放到了
Vuex
里,让Vuex
成为了整个项目的“数据集散中心”
,而vue
组件里,仅仅是进行事件调度(dispatch)
和绑定来自Vuex的数据
。简单的例子
我们以商品详情页面举例,其包含如下的功能:
如图所示:
如果我们不使用Vuex,代码的结构可能如下:
<template> <template> <script> export default { data() { detail: { name: '', shopId: '', id: '', pic: '', description: '', price: '', updateTime: '', categoryId: '', stock: 0, createTime: '', attributeList: {}, collectId: 0, }, // 一大堆商品的字段数据 【MARK】 }, created(){ this.$http.get('/product/:id') .then() // 以及一端更新其他数据的请求 【MARK】 }, methods(){ clickCart(){ this.$http.get('xxx') .then('xxx') this.$emit('xxx'); // 与上层组件通信更新购物车的红点 } // 一堆用户事件的触发方法,每个方法内都是一堆http请求和回调处理 【MARK】 }, computed:{ // 一些基于商品数据判断状态的数据 【MARK】 } } </script> <style> <!-- 忽略相关样式代码--> <style> 复制代码
如果按照上述的方式来写,虽然可以保证关于这个页面的逻辑处理都保留在这个vue文件内,但是却显得十分臃肿,如果后续要加更多的功能抑或是修改,其需要读的代码可能更多。
所以,以上代码示例中的被
【MARK】
标记的部分,将是Vuex
可以优化的。 最后的代码如下图所示:代码的可读性提高了不少:
data
里不含任何业务数据字段,仅保留控制页面的字段【触发事件】-【回调】
这样简单的逻辑。关于数据处理的代码,全部封装到了
Vuex
上了:我们在Vuex层,不仅请求了数据,同时对返回的数据做了判断以及不同的逻辑处理,同时设置
Getters
使的页面能够基于Getters
提供的数据做各类二元判断
。Vuex层带来了如下的好处:
Actions
,就这个例子,我只要写了一次加入购物车
的函数,在各个页面都可以使用了。添加商品进购物车的同时更新商品数据并更新购物车数据
,这样需要写三次HTTP
请求的业务逻辑,此时只需要将三个Actions进行组合即可,而这几个Actions还可以在其他地方单独使用。数据仓库
带来的好处就是减少了数据层层传递的操作,按照以前的编码习惯,我会大量使用$emit
和props
来实现父子或兄弟组件通信,而现在,只需要在组件中触发相应的事件绑定相应的数据即可。全新的业务编写体验
直观体验来说,彻底使用
Vuex
后,编码思路清晰不少,因为写完整体的一个功能需要2~3天,而Vuex对于“间断性编程”
非常有帮助,能够迅速捡起上一次开发的进度。使用Vuex之后就是按照如下的步骤进行开发了:
(第1、2、3步不变,如前文) 4.构建Vuex的Store模型,定义所需的方法。 5.编写组件并绑定在Vuex上的相关数据和方法。 6.编写触发
dispatch
后的回调。笔者认为使用Vuex几个重要的原则
使用
Vuex
存在一定的规范,在官网上已经陈列了诸如表单处理
,测试
,项目结构
的规范,而下面的一些原则仅仅是笔者的一些经验之谈,不一定正确,有所帮助便是最好的。【不处理页面跳转】:页面跳转在
dispatch
的回调里进行处理。最初我的设计是在
Action
请求数据之后同时也负责所有的跳转逻辑
和弹窗逻辑
,不过,这样做又让视图层
和数据层
耦合了,使得我抽象出来的Action
的复用性变得极差,同时,在vue
文件看到关于视图层的处理才算是比较正常的编写逻辑。【单一职责原则】:
mutation
和action
尽量保持只做一件事原则。正如上一条原则中提到的要提高
action
的复用性,那么就要保证需要复用的action
只做一个功能,如更新A数据
和更新B数据
不要直接融合成一个action
,而是分别定义两个action
,并通过组合的方式来实现一个更复杂的功能,有点类似精简指令集的思想。如:
actions:{ updateA(){}, // A 可能在其他地方被单独使用 updateB(){}, // B 可能在其他地方被单独使用 updateAandB({dispatch}){ dispatch("updateA"); dispatch("updateB"); }, // 同时触发 A B 的复杂action } 复制代码
【使用Promise和AA】(Async & Await)
为了让
dispatch
函数拥有Promise风格
的处理回调的能力,可以让action
的返回值作为一个Promise
对象,如:// store.js actions:{ updateA(){ return Promise.resolve() } } 复制代码
在组件内就可以使用
.then
:... created(){ this.$store.dispatch('updateA') .then(() => { ... }) } ... 复制代码
而
Vuex
一般建议在actions
进行异步操作,所以为了让代码更加优雅,可以用AA
如下编写:// store.js actions:{ async updateA(){ const result = await ajax('/**/**'); if(result){ return Promise.resolve() } } } 复制代码
所以,在实践中的代码如图:
【使用namespace】,为了你自己好。
一般使用vuex,会根据实体或者页面来拆分state形成module,一个module里包含其所需的所有
actions,states,mutation
等。如电商里,一般有商品
,订单
,用户
这几个实体可以拆成module,拆分出来之后如图:拆分的好处是更加独立直观了,坏处是如果不使用命名空间,可能造成
actions
,mutations
冲突,正好Vuex
也提供namespace
的配置,参考官方文档即可。这次使用的不足
未使用namespace
虽然总结到了原则里,不过这次并没有使用
namespace
,算是这次比较大的不足了,解决的办法是”手动namespace“,如图:将
actions
的方法名前缀加上该实体名用于区分不同的方法,实在是比较拙劣的手段了。部分代码仍然有重复书写的情况
情况发生在获取到异步数据之后的代码,一般如下这段代码会大量重复:
if (result.code){ return Promise.resolve(data); } return Promise.reject(msg); 复制代码
虽然不算多,但是这逻辑仍然有可抽象出来的可能性,下次打算在
RootState
里添加一个action
,用于处理这样的后端返回数据,期望结果如下:actions: { parseResult({}, result){ if (result.code){ return Promise.resolve(data); } return Promise.reject(msg); }, async getA({dispatch}){ const result = await http.get('xx/xx'); return dispatch('parseResult', result); // One line , it's clean } } 复制代码
其他收获
拥抱 Vue CLI 3
3.0 基本在配置上做到
极简化
了。很久没开发Vue
的项目了,记得上一个Vue
项目里,仍然有一大堆xxx.congfig.js
,而现在,基本集成到了CLI内部了,通过一个vue.config.js
可满足大部分开发需求。本项目配置如下图:比以前的配置更加直观了,更多请参考:cli.vuejs.org/zh/,至于插件机制,本次开发未用上,只是在安装第三方包的时候可以用
vue add
了,有GUI可以查看安装了哪些插件。将
Vue.use 和 Vue.install
使用的地方汇总到plugins
目录下本项目是一个多页面应用,总共有3个Vue实例,所以希望能够按需注入想要的第三方依赖如
axios
,qrcode
,lazyload
等一些工具库或第三方组件。于是都将其汇总到plugins
目录下了,如下图:这样封装的好处是,我可以对每个Vue实例按需导入第三方依赖,一行import就可以,如下图:
开始考虑优化了
一个大型的web app 打包压缩后都不超过500kb,所以就以此作为
Benchmark
来优化该项目打包后的大小,按需引入
第三方依赖(特别是Element
和lodash
)减少了不少项目体积。不过最主要的还是得学会用webpack analyse tool
来分析。结论
总之,本次项目主要是刷新了对
Vuex
的认知,其能够让我们编写的vue
组件可读性和可维护性更高。同时,也能够让数据更新这块的代码复用性更高。通过Vuex
来构建一个中心化的数据集散中心
还能让开发的思路更顺畅,值得学习。参考:
Vuex 是什么?