《京保养》基于Vue+Vuex的单页面应用实践

avatar
UX @京东
原文链接: jdc.jd.com

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

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

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

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

图1

图2

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

为什么要使用 Vuex

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

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

  1. 绑定车辆页面填写车牌号码;
  2. 填写车系,包含选择品牌、选择车系、选择年款;
  3. 回到了绑定车辆页面继续填写车辆绑定信息。

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

Video Player下载文件

可以看到这几个步骤中已经有多个视图的跳转了,但最终目的是将绑定车辆的信息填写完整。每一次跳转都需要将已经操作过的页面交互数据(比如:车牌号、车系信息、电话号码等)记录下来,最后回填到绑定车辆页面。这些数据如果没有一个可以由多个视图都能取得的地方存储,绑定车辆的信息永远也填写不完整,此时 Vuex 就可以派上了用场。

Vuex 的使用

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

  1. state —— 定义存储状态;
  2. getter —— 对数据进行过滤;
  3. mutation —— 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation;
  4. action —— 类似于 mutation,不同在于可以包含任意异步操作;
  5. 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,文档也不必翻译了。

链接:github.com/vuefe/vuejs…

因此项目中选择了 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 的开发更加效率和便捷了,不同的尝试总会让人有意外的收获,这使人感到很兴奋。

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