《京保养》基于Vue+Vuex的单页面应用实践
接到《京保养》项目需求,了解到是移动端项目,运用于微信公众号及京东 APP 。通过与后端研发沟通,后端将提供所有的数据展示接口,这样最终商定使用前后端分离技术,而作为前端这边就非常适合选择基于 webpack + Vue 的单页面应用来实现。
前期组内也有基于单页面应用的项目总结,他们的总结的确让我在本项目中少走了很多弯路,但是不同的项目又遇到了不同的新问题,本文将会介绍我所遇到的新问题及解决方案。
感兴趣的同学可以通过以下两个入口先去体验下京保养应用,然后回来接着看文章:
微信公众号搜索“京东汽车用品” – 关注公众号 – 菜单栏“京保养”,见图1; 京东 APP – 我的 – 我的爱车 – 京保养,见图2。图1
图2
如果你在 APP 中找不到“我的爱车”入口,你得先在京东 APP – 我的 – 设置 – 添加档案 – 我的爱车 – 绑定自己的爱车,然后才会有入口。
为什么要使用 Vuex
初拟技术选型,项目开始了,而开发过程中发现,项目中有不同的表单视图需要大量数据的共享。而仅使用单页面的路由来传参并不能满足需求,因为数据量过大,导致路由传参过于复杂。如此,项目中引进 Vuex 技术来实现数据共享。
拿项目中需要数据共享的地方举例 —— 绑定车辆模块。先来看下该模块的操作流程:
绑定车辆页面填写车牌号码; 填写车系,包含选择品牌、选择车系、选择年款; 回到了绑定车辆页面继续填写车辆绑定信息。如果上字描述还不清楚,我也录了个小视频,点击查看交互流程:
http://jdc.jd.com/wp-content/uploads/2017/11/2.mp4可以看到这几个步骤中已经有多个视图的跳转了,但最终目的是将绑定车辆的信息填写完整。每一次跳转都需要将已经操作过的页面交互数据(比如:车牌号、车系信息、电话号码等)记录下来,最后回填到绑定车辆页面。这些数据如果没有一个可以由多个视图都能取得的地方存储,绑定车辆的信息永远也填写不完整,此时 Vuex 就可以派上了用场。
Vuex 的使用
Vuex 的具体使用,有几个核心概念:
state —— 定义存储状态; getter —— 对数据进行过滤; mutation —— 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation; action —— 类似于 mutation,不同在于可以包含任意异步操作; modules —— 如果应用过大,便可以使用 modules 来分割管理,不至于 store 变得非常臃肿。store 实例具体实现:
import Vuex from 'vuex';//引入 Vue.use(Vuex);//使用 export default new Vuex.Store({ state: { formParams: {}, address: {} }, actions: { GET_SERIES_LIST: function({ commit }, params) { axios.get(url, { params: data, withCredentials: true }) .then(response => { commit('setBrandsList', { data }); }); }, }, mutations: { setAddress: (state, data) => { state.address = data; } }, getters: {}, modules: {} });
用法举例:
子组件中读取 state 状态的方法示例:
computed: { address () { return store.state.address; } }
组件通过 commit 修改 state 状态的示例:
this.$store.commit('setAddress',params);
组件通过 dispatch 触发 action 调用示例:
this.$store.dispatch('GET_SHOPS_LIST',params);
Vuex 的持久化存储方案
以上 Vuex 用起来的确非常方便,解决了多视图之间的数据共享问题。但是运用过程中又带来了一个新的问题是,Vuex 的状态存储并不能持久化。也就是说当你存储在 Vuex 中的 store 里的数据,只要一刷新页面,数据就丢失了。
那本人很快就想到了要用 sessionStorage 或者 localStorage 的方式来解决此问题。解决方式便是在每个 mutations 中做一次 storage 的存值,因为 mutations 是改变 state 的唯一途径,所以每一次改变都进行 storage 的存值也不会有问题。
mutations: { setAddress: (state, data) => { state. address = data; window.localStorage.setItem(' address ',data); }, //… }
那其实以上手动存取 localStorage 的方式还可以做得更简便。那就是引入 vuex-persist 插件,它就是为 Vuex 持久化存储而生的一个插件。不需要你手动存取 storage ,而是直接将状态保存至 cookie 或者 localStorage 中。具体用法如下:
import VuexPersistence from 'vuex-persist';//引入 const vuexLocal = new VuexPersistence({//配置 storage: window.sessionStorage }); export default new Vuex.Store({ state: { formParams: {}, address: {} }, actions: { }, mutations: { setAddress: (state, data) => { state.address = data; } }, plugins: [vuexLocal.plugin]//添加插件 });
通过以上设置,在图3中各个页面之间跳转,如果刷新某个视图,数据并不会丢失,依然存在,并且不需要在每个 mutations 中手动存取 storage 。
本地开发项目遇到的数据请求跨域问题
当项目在本地开发还没有部署到测试环境时,项目的数据请求一律都是跨域,虽然很多文章都有说怎么解决跨域问题,但是在本项目中又遇到了个新问题。因为后端设置了指定的跨域允许域名(如:local.jd.com),并不是任何域名都可以开启 Chrome 的 CORS 后就可以跨域访问了(本方法详见文章“《京东E维》基于 vue+webpack 的单页面实践”)。
我们通常在本地开发时项目的访问路径为http://localhost:8080,指定的域名可跨域访问,只需要在本地配置 host: 127.0.0.1 local.jd.com,这样便可以以http://localhost:8080 的方式来访问了。但是如此配置后其中的数据接口还是提示跨域无法访问,后来发现 webpack-dev-server 的配置中是默认查找 hostname ,添加配置 disableHostCheck: true 就可以改变它的默认查找行为,问题也就解决了。
数据请求方案及 JSONP 请求
下面说说数据请求的方案:曾经我们常用的是 vue-resource ,但是 Vue 官方已经不建议使用了。Vue 的作者 Evan You 原话是这样说的:
最近团队讨论了一下,Ajax 本身跟 Vue 并没有什么需要特别整合的地方,使用 fetch polyfill 或是 axios、superagent 等等都可以起到同等的效果,vue-resource 提供的价值和其维护成本相比并不划算,所以决定在不久以后取消对 vue-resource 的官方推荐。已有的用户可以继续使用,但以后不再把 vue-resource 作为官方的 ajax 方案。
这里可以去掉 vue-resource,文档也不必翻译了。
链接:https://github.com/vuefe/vuejs.org/issues/186
因此项目中选择了 axios 来支持数据请求,但是 axios 并不支持 JSONP 请求,项目中有几处必需用到 JSONP 的接口,所以又引进了 JSONP 模块来支持 JSONP 的数据请求。调用示例:
jsonp('//d.jd.com/xxxx?fid=1&callback=getAreaListCallBack'); window.getAreaListCallBack = function(r) { console.log(r); };
必要时需要做代码的封装
项目不断开发,发现 store 里的 action 堆积的代码越来越多,重复代码也不少。列举其中一个如下,其余都是类似:
actions: { //获取服务记录查询及记录列表 GET_SERVICE_LIST: function({ commit }, params) { axios.get(this.state.host + '/xxxxxx', { params: params, withCredentials: true }).then((response) => { commit('setServiceList', { list: response.data.data }); // params.success(response.data); }, (err) => { console.log(err) }); }, //… }
可以看到 axios 的数据请求代码基本一致,而且要重复写无数遍,因此我将项目中的此种重复代码提出来放入单独的文件。数据请求类型可配置,参数可配置,接口访问路径可配置,这样减少了很多冗余代码,同时在修改某个通用配置的时候,只需要修改一处即可。具体封装如下:
xhr: function(apiskey, params, changeState) { let apis = this.apis; let url = debug ? host + apis[apiskey].url : apis[apiskey].url; let data = params.params || params || {}; let type = apis[apiskey].type || 'get'; if (type == 'post') { axios.post(url, data, { withCredentials: true }) .then(response => { //… }, response => { //… }); } else { axios.get(url, { params: data, withCredentials: true }) .then(response => { //… }, response => { //… }); } }
Vue 组件中第三方文件的引入
项目中有个别视图(图3)需要显示地图,而地图的引入需要引入第三方的地图库,如腾讯或者其他,当进入应用的时候,实际并不需要第三方的文件直接就加载进来,只在需要它的时候才加载就行。所以在显示地图的视图中,我做了如下处理,这样可以仅在该视图中才加载第三方地图 JS 库。
loadMap() { let self = this; return new Promise(function(resolve, reject) { window.initTheMap = function() { resolve(self.initMap()); } var script = document.createElement('script') script.type = 'text/javascript' script.async = true script.src = '//map.qq.com/api/js?v=2.exp&callback=initTheMap&key=' + self.k script.onerror = reject document.head.appendChild(script); }); }
图3
组件的提取
项目中所有的视图都可以视为组件,我这里的所指的需要提取的组件,是指那些复用率高的模块和功能。例如:顶部条、弹框、吐司提示、无限加载等功能提取为单独的组件。组件的提取可以让自己的开发效率更高,同时也让项目方便维护。所以项目结构中有单独的文件夹专门存放复用率高的组件(图4)。
图4
组件的具体调用方式,举例顶部条的调用:
在主视图组件中,需要引入需要使用的组件:
import Header from '../component/header.vue';
然后注册组件:
components: { jHeader: Header }
模板中使用:
<j-header :title="title"></j-header>
顶部条所指的具体内容,如图5红色框中的区域。
图5
类似顶部条这样的组件基本在每个视图中都会用到,但是他们的布局和样式基本完全相同,除了标题文字可能有所区别,所以单独作为组件提取出来是非常有必要的。
项目有待改进的地方
未加入 Vue 的懒加载,本项目整个应用的 JS 文件大小大概为200多 KB ,暂且能接受,如果项目内容再多一些,可能会有更大的 JS 文件,导致初次进入应用等待时间较长,所以复杂的项目可以考虑加入懒加载方案,按需加载 JS 。
写在最后
以上是项目中提取出来的一些比较棘手的问题,项目开发完成后,还有一个感触最深的就是 Vue 的模式是数据驱动视图,而曾经的 jQuery 的模式却是以 DOM 元素为中心,先查找 DOM ,再给 DOM 绑定事件,通过 DOM 元素渲染数据,或者使用模板等。而 Vue 不再需要模板语言,本身就带有模板语言性质,开发过程中更多的关注数据怎么处理就行,对于自身而言,感觉 Vue 的开发更加效率和便捷了,不同的尝试总会让人有意外的收获,这使人感到很兴奋。
本文到这就结束了,如果以上有什么不合理的地方,欢迎大家指正!
文章来源:
Author:liaoyanli
link:https://jdc.jd.com/archives/5019