Vue 在今年有一种大杀特杀的感觉,有机会尝试了一下,然后记录一下使用过程的一些想法

为什么是 Vue

首先业务场景大概是这样:前后端分离的开发模式,移动端,15 个页面重构,需要与服务端同步数据和状态,需要用户身份验证,需要跑一个 Node 应用托管静态资源和提供日志接口,需要全站屏蔽 SEO,需要打通 CI 和发布平台。

需求评审完开始定前端的技术栈。首先很开心地抛弃已有的基于 FIS3 的整套构建链路,原因如下:

  1. 以往项目以营销类为主,生命周期短,已有的实践不适用于这种小型立项(考虑组件的管理,开发人员交接)
  2. FIS3 在 Vue 和 React 上的开发体验不够友好,可能耗费过多时间去解决构建工具的兼容问题
  3. 对 webpack 好感度上升

然后是 Vue.js 1.0.26 和 React 15.1.0 的取舍。考虑如下:

  1. 移动端场景,Vue 更加轻量、灵活(双向绑定,指令,插件)
  2. Vue 的官方最佳实践足够成熟
  3. vue-loader 以类似 Web Component 的形式组织代码,组件的资源容易管理,方便后续维护迭代
  4. 工作交接,Vue 更容易让人接受
  5. 已经试过了 React 想再试试 Vue

主要技术栈

主角是 Vue 全家桶

  • vue.js
  • vuex
  • vue-router
  • vue-resource

构建工具

  • webpack
  • Babel
  • vue-loader
  • px2rem-loader

服务端

  • Koa

测试框架

  • Co Mocha & Chai
  • Karma

其他

  • ESLint
  • Bower

构建项目

目录规范

基于 vue-cli 初始化 webpack 项目,已经能满足大部分需要了

$ vue init webpack demo

然后根据需要做一些调整。最终的目录结构如下

+ bower_components
+ build
+ ci
+ config
+ docs
+ node_modules
+ server
+ src
+ test
- bower.json
- index.ejs
- package.json
- README.json

差不多是这样的角色:

  • bower_components: 没有放到 src 里面,通过 webpack 配置 resolve 允许直接引用第三方模块
  • build: 构建的配置,同时包括 mock.js,一个 Express 中间件,用以在开发过程中模拟服务端数据接口,基于 Mockjs
  • ci: CI 的配置
  • config: 服务端的配置
  • server: Koa 应用,仅用于生产环境中托管静态资源和提供日志接口
  • index.ejs: 应用的入口页面,使用 Ejs 是考虑到页面自适应的代码希望内联到页面顶部,但 html-loader 在这方面的支持不够友好,于是通过 Ejs 注入文件引用和内联代码,最终构建输出 index.html

展开 src, 目录结构如下:

- src
- assets
+ css
+ images
- vue-auth.js
- vue-logger.js
...
- components
+ Modal
- List.vue
...
- constants
- ActionTypes.js
- API.js
...
- containers
- Home.vue
- Register
+ images
- package.json
- Protocol.vue
- Register.vue
...
- router
- router.js
- vuex
- actions.js
- getters.js
- mutations.js
- plugins.js
- store.js
- App.vue
- main.js

也记录下 src 目录结构的设计意图

  • assets: 多个页面通用的样式 mixin 和图片 icon, 还包括根据业务需要写的一些 Vue 插件和指令
  • components: 通用的 Vue 组件,如 Modal 和 List,部分可视为纯组件 (pure component)
  • constants: 各种常量,包括 ActionTypes 和服务端接口等
  • containers: 每个页面是一个 Vue 组件,同时也被视为一个容器,包括相关的业务组件和静态资源。初衷是每个页面的资源能够集中管理,方便维护
  • router: vue-router 的配置
  • vuex: vuex 的配置
  • App.vue: 应用的根容器
  • main.js: 入口文件,引用并初始化根容器和路由,并注入 Vue 插件和指令

其他打包和测试的配置大部分还是按照 vue-cli 提供的方案,很厉害的 vue-cli

组件管理

首先,自己把组件区分了四个类型:根组件、容器组件、业务组件、通用组件。

  • 根组件:应用只有一个根组件,vue-router 通过根组件创建路由实例,直接挂载在 DOM 上面,并接管了 store
  • 容器组件:相当于页面,生命周期由路由管理
  • 业务组件:与业务较多耦合,可复用性差,作为容器组件的子组件
  • 通用组件:与业务耦合小,可复用性强,状态和数据由父组件传入

组件以 vue-loader 推荐的形式开发,逻辑、样式、模板都体现在 .vue 中,一个 .vue 文件既是一个完整的组件。比较复杂的组件通过文件夹的形式组织资源,包括图片、业务组件等,并通过 package.json 配置指定组件 .vue 的引用

这样做的好处如下:

  1. 组件资源聚合,更容易管理和维护,考虑到长期迭代和工作交接应该是必要的
  2. 引用组件时不需要关心资源依赖的问题
  3. 区分业务组件和通用组件有利于更好把握组件的粒度和方法的抽象程度

vue-loader

状态管理

数据和状态通过 vuex 管理,大体的使用与官方介绍的实践相似。与 Redux 做比较的话,感觉有这样的异同点

  1. 两者都限制了数据单向流动,只允许抛出 action 触发状态变更
  2. vuex 中组件接触 state 的方式却更加简单,相比 Redux 少了层级传递的限制
  3. vuex 中 action 被直接分发到对应命名的 mutation,而避免了 reducer 中 switch 的结构。好处是更加简单和对 action 更少的约束,坏处是状态变更方法的组织变得松散,更少的约束使得 action 的结构不好把控

vuex 在使用上明显更加轻便和简单,很有 Vue.js 的风格

然后提一下与服务端同步数据的方式。这次的尝试中,对于应用层面的状态,直接由组件 dispatch 一个 fetch 的 action,之后被 plugins 拦截,再由 plugins 发起请求,请求完成后 dispatch 一个 update 的 action 通过 mutation 更新 state,最终触发视图的更新。虽然不好说这是一种最佳实践,但有利于将与服务端交互的逻辑从组件中剥离开并统一管理,组件只关心 state。简单代码如下:

// component.vue
import { fetchUser } from 'src/vuex/actions'
export default {
vuex: {
getters: {
user: state => state.user
},
actions: {
fetchUser
}
},
created() {
this.fetchUser()
}
}
// plugin.js
import store from './store'
const plugin = store => {
store.subscribe(({type, payload}) => {
switch (type) {
case ActionTypes.USER_FETCH:
fetchUserInfo()
break
}
})
}
function fetchUserInfo() {
Vue.http.get('url').then(ret => {
store.dispatch(ActionTypes.USER_UPDATE, ret)
})
}

其他要做的就是区分好组件本地状态和应用层级状态就行啦。

插件 & 指令

Vue 的插件和指令学习成本很低,但根据项目需要编写合适的插件或指令能够发挥很大的作用。前者能够很好地封装工具函数和方法,后者能够快速创建轻量实用的组件。简单示例如下

插件:统计日志

// vue-logger.js
export default function(Vue) {
const logger = {
logClick() {},
logVisit() {}
}
Vue.prototype.$logger = Vue.logger = logger
}
// component.vue
export default {
methods: {
share() {
this.$logger.logClick('share')
}
}
}

指令:获取手机验证码

// vue-code.js
export default function(Vue) {
Vue.directive('code') {
params: ['mobile'],
data: {
ticking: 60
},
bind() {
this.el.addEventListener('click', this.getCode.bind(this), false)
},
unbind() {
// unbind event
},
getCode() {
let {mobile} = this.params
Vue.http.get(`url?m=${mobile}`)
// count down tips...
}
}
}
<!-- component.vue -->
<template>
<div>
<input type="tel" name="mobile" v-model="mobile">
<button v-code :mobile="mobile">获取验证码</button>
</div>
</template>
<script>
export default {
data() {
return {
mobile: ''
}
}
}
</script>

测试

前端测试基于 Karma 展开,主要关注逻辑方面而不是 UI。这次测试基于整个应用进行,而不是独立挂载单个组件,存在不足的地方。spec 按照组件的粒度组织,并对关键业务相关的组件进行 100% 测试覆盖,最终的质量也比较理想。

然后由于存在比较多的异步操作,使用 Co Mocha 以支持 ES6 generator。

在组织测试用例的过程中,需要模拟服务端返回不同状态码以确保被正确处理。这里提供模拟数据的依然是本地的开发服务器,但是响应数据在测试用例中声明。基于 vue-resource 的 interceptor 的特性,拦截应用中发起的请求并响应测试数据。封装之后的使用方法如下:

import { interceptor, wait } from './utils'
describe('page Profile', function*() {
beforeEach(function*() {
interceptor.reset()
})
it('bad auth forbidden', function*() {
interceptor.mock('/user/detail', {
code: 502,
message: '非法访问拦截'
})
App.$el.querySelector('#button').click()
yield wait(500)
expect(App.$el.querySelector('.modal-body').textContent).to.contain('非法访问拦截')
})
})

打包上线

这次采取的打包方案是推送分支触发 Git hook 然后推送到 Jenkins 进行线上打包,再直接推送到发布平台。之后迭代会考虑抛弃 Jenkins 通过 Gitlab-ci-runner 实现。

其他

其他一些零碎的东西

  • slot 使得组件的复用灵活很多很多
  • html-minify 对尖括号 < 敏感,如果 template 中表达式使用了尖括号会造成打包压缩 html 的时候抛出异常,需要避免
  • Karma 使用的服务端中间件是 Connect 而不是 Express
  • Karma 手动配置自定义的 plugins 时需要指定其他所有的插件,或者配置 karma-* 可自动引用其他插件

总结

第一次尝试 Vue 还是挺舒服的,印象最深刻的居然是 vue-cli;Vue.js 本身十分出色,从 MVVM 框架发展到现在有能力提供一整套前端应用解决方案,“简单”也是框架本身强大的地方;官方全家桶的搭配使用比 React 舒服,踩的坑也比之前自己摸索 React 少很多很多,两者有很多共通的地方,更适合新手尝试;vue-loader 应该是一个亮点,自己感觉类似的组织方法优势在于资源聚合,然后会觉得 JSX 好看一点。

期待 Vue 2.0 和全家桶的升级