专栏名称: 京东设计中心JDC
专业,创造力,激情,设计。京东用户体验设计部门,致力于创造更美好的电子商务购物体验。
目录
相关文章推荐
51好读  ›  专栏  ›  京东设计中心JDC

JDC丨京东设计中心 - 《京保养》基于Vue+Vuex的单页面应用实践

京东设计中心JDC  · 公众号  ·  · 2017-11-23 17:18

正文

请到「今天看啥」查看全文


接到《京保养》项目需求,了解到是移动端项目,运用于微信公众号及京东 APP 。通过与后端研发沟通,后端将提供所有的数据展示接口,这样最终商定使用前后端分离技术,而作为前端这边就非常适合选择基于 webpack + Vue 的单页面应用来实现。

前期组内也有基于单页面应用的项目总结,他们的总结的确让我在本项目中少走了很多弯路,但是不同的项目又遇到了不同的新问题,本文将会介绍我所遇到的新问题及解决方案。

感兴趣的同学可以通过以下两个入口先去体验下京保养应用,然后回来接着看文章:

  1. 微信公众号搜索“京东汽车用品” – 关注公众号 – 菜单栏“京保养”,见图1;

  2. 京东 APP – 我的 – 我的爱车 – 京保养,见图2。


图1

图2

如果你在 APP 中找不到“我的爱车”入口,你得先在京东 APP – 我的 – 设置 – 添加档案 – 我的爱车 – 绑定自己的爱车,然后才会有入口。

为什么要使用 Vuex

初拟技术选型,项目开始了,而开发过程中发现,项目中有不同的表单视图需要大量数据的共享。而仅使用单页面的路由来传参并不能满足需求,因为数据量过大,导致路由传参过于复杂。如此,项目中引进 Vuex 技术来实现数据共享。

拿项目中需要数据共享的地方举例 —— 绑定车辆模块。先来看下该模块的操作流程:

  1. 绑定车辆页面填写车牌号码;

  2. 填写车系,包含选择品牌、选择车系、选择年款;

  3. 回到了绑定车辆页面继续填写车辆绑定信息。

如果上字描述还不清楚,我也录了个小视频,点击查看交互流程:


Vuex 的使用

Vuex 的具体使用,有几个核心概念:

1.state —— 定义存储状态;

2.getter —— 对数据进行过滤;

3.mutation —— 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation;

4.action —— 类似于 mutation,不同在于可以包含任意异步操作;

5.modules —— 如果应用过大,便可以使用 modules 来分割管理,不至于 store 变得非常臃肿。

store 实例具体实现:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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 状态的方法示例:

1

2

3

computed : {

address () {

return store . state . address ;

}

}

组件通过 commit 修改 state 状态的示例:

1

this .$ store . commit ( 'setAddress' , params );

组件通过 dispatch 触发 action 调用示例:

1

this .$ store . dispatch ( 'GET_SHOPS_LIST' , params );


Vuex 的持久化存储方案

以上 Vuex 用起来的确非常方便,解决了多视图之间的数据共享问题。但是运用过程中又带来了一个新的问题是,Vuex 的状态存储并不能持久化。也就是说当你存储在 Vuex 中的 store 里的数据,只要一刷新页面,数据就丢失了。

那本人很快就想到了要用 sessionStorage 或者 localStorage 的方式来解决此问题。解决方式便是在每个 mutations 中做一次 storage 的存值,因为 mutations 是改变 state 的唯一途径,所以每一次改变都进行 storage 的存值也不会有问题。


1

2

3

4

5

6

mutations : {

setAddress : ( state , data ) => {

state . address = data ;

window . localStorage . setItem ( ' address ' , data );

},

//…

那其实以上手动存取 localStorage 的方式还可以做得更简便。那就是引入 vuex-persist 插件,它就是为 Vuex 持久化存储而生的一个插件。不需要你手动存取 storage ,而是直接将状态保存至 cookie 或者 localStorage 中。具体用法如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

1

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 的数据请求。调用示例:

1

2

3

4

jsonp ( '//d.jd.com/xxxx?fid=1&callback=getAreaListCallBack' );

window . getAreaListCallBack = function ( r ) {

console . log ( r );

};

必要时需要做代码的封装

项目不断开发,发现 store 里的 action 堆积的代码越来越多,重复代码也不少。列举其中一个如下,其余都是类似:


1

2

3

4

5

6

7

8

9

10

11

12

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 的数据请求代码基本一致,而且要重复写无数遍,因此我将项目中的此种重复代码提出来放入单独的文件。数据请求类型可配置,参数可配置,接口访问路径可配置,这样减少了很多冗余代码,同时在修改某个通用配置的时候,只需要修改一处即可。具体封装如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

1

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 库。


1

2

3

4

5

6

7

8

9

10

11

12

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

组件的具体调用方式,举例顶部条的调用:

在主视图组件中,需要引入需要使用的组件:


1

import Header from '../component/header.vue' ;

然后注册组件:

1

2

3

components : {

jHeader : Header

}

模板中使用:

1

< j - header : title = "title" > j - header >


顶部条所指的具体内容,如图5红色框中的区域。

图5

类似顶部条这样的组件基本在每个视图中都会用到,但是他们的布局和样式基本完全相同,除了标题文字可能有所区别,所以单独作为组件提取出来是非常有必要的。

项目有待改进的地方

未加入 Vue 的懒加载,本项目整个应用的 JS 文件大小大概为200多 KB ,暂且能接受,如果项目内容再多一些,可能会有更大的 JS 文件,导致初次进入应用等待时间较长,所以复杂的项目可以考虑加入懒加载方案,按需加载 JS 。

写在最后

以上是项目中提取出来的一些比较棘手的问题,项目开发完成后,还有一个感触最深的就是 Vue 的模式是数据驱动视图,而曾经的 jQuery 的模式却是以 DOM 元素为中心,先查找 DOM ,再给 DOM 绑定事件,通过 DOM 元素渲染数据,或者使用模板等。而 Vue 不再需要模板语言,本身就带有模板语言性质,开发过程中更多的关注数据怎么处理就行,对于自身而言,感觉 Vue 的开发更加效率和便捷了,不同的尝试总会让人有意外的收获,这使人感到很兴奋。

本文到这就结束了,如果以上有什么不合理的地方,欢迎大家指正!








请到「今天看啥」查看全文