Skip to content

VueJS项目实践总结

说起来,从最初用客户端vue.js,到后面用vue-cli使用的Vue1 + webpack模版,再到后面使用Vue2+webpack,用这个框架做SPA应用也有一年半的时间了。做一些总结吧,想到哪里写哪里(针对Vue2 + webpack)。

一、页面按需加载

这主要是为了加快首屏加载速度。这样做的好处是第一屏所需加载的文件大小变小了,代价是如果用户会走完整个SPA的话,实际的总代码下载量是变多了的。按需加载页面主要就是修改/src/router/index.js文件,示例代码如下:

default
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)

const Home = resolve => require(['@/views/Home.vue'], resolve)
const MyRoot = resolve => require(['@/views/my/Root.vue'], resolve)
const MyIndex = resolve => require(['@/views/my/Index.vue'], resolve)
const MyLogs = resolve => require(['@/views/my/Logs.vue'], resolve)
const MyBrokerages = resolve => require(['@/views/my/Brokerages.vue'], resolve)

export default new Router({
routes: [
{
path: '/home',
name: 'Home',
component: Home,
meta: { needLogin: false }
},
{
path: '/my',
name: 'MyRoot',
component: MyRoot,
meta: { needLogin: true },
children: [
{
path: '',
redirect: 'index'
},
{
path: 'index',
name: 'MyIndex',
component: MyIndex,
meta: { needLogin: true }
},
{
path: 'logs',
name: 'MyLogs',
component: MyLogs,
meta: { needLogin: true }
},
{
path: 'brokerages',
name: 'MyBrokerages',
component: MyBrokerages,
meta: { needLogin: true }
}
]
},
// must be placed at the bottom
{ path: '\*', redirect: '/home' }
]
})

因为觉得上面那样引入组件的代码重复的地方很多,本着DRY(Don't Repeat Yourself,中文意思是“懒” -\_-)的原则,试过用下面这种方式引入组件:

default
function generateComponentFunction (path) {
  return resolve => require([`@/views${path}`], resolve)
}

const Home = generateComponentFunction('/Home.vue')
const MyRoot = generateComponentFunction('/my/Root.vue')
const MyIndex = generateComponentFunction('/my/Index.vue')
const MyLogs = generateComponentFunction('/my/Logs.vue')
const MyBrokerages = generateComponentFunction('/my/Brokerages.vue')

是不是感觉这样写的话,引入组件的时候能少敲很多代码呢?然而实践发现这样子npm run build后各个页面被打包成了一个js,貌似是把所有这些都当成一个页面了。

二、通过script标签引入第三方js库

这种方式或许并非比较好的实践,个人保留意见,只是告知一下其实还有这种操作方法。这样能减少本地打包后的vendor.js文件的大小,同时也利用了CDN,但是也增加了very first time的首屏加载时间,后面这些第三方js文件被缓存了,首屏加载时间是会快一点的。假设说,现在你的package.json文件中定义的依赖包有:

default
...
"dependencies": {
  "fastclick": "^1.0.6",
  "mockjs": "^1.0.1-beta3",
  "vue": "^2.2.1",
  "vue-router": "^2.2.0",
  "vuex": "^2.2.1"
},
...

然后,在这些依赖包(第三方js文件),其实都是可以通过script标签引入的。打开/index.html文件,示例如下(mockjs未处理,一样一样的):

default
<body ontouchstart="">
  <div id="app"></div>
  <!-- <script src="http://cdn.bootcss.com/vue/2.2.1/vue.js"></script> -->
  <script src="http://cdn.bootcss.com/vue/2.2.1/vue.min.js"></script>
  <script src="http://cdn.bootcss.com/vue-router/2.2.0/vue-router.min.js"></script>
  <script src="http://cdn.bootcss.com/vuex/2.2.1/vuex.min.js"></script>
  <script src="http://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
  <script src="http://cdn.bootcss.com/fastclick/1.0.6/fastclick.min.js"></script>
  <script src="http://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
  <!-- built files will be auto injected -->
</body>

当然,事情没有这么简单,还需要修改/build/webpack.base.conf.js文件的externals部分(不记得脚手架生成的初始文件里有没有externals部分,如果没有的话自己加-_-):

default
entry: {
  app: './src/main.js'
},
output: {
  path: config.build.assetsRoot,
  filename: '[name].js',
  publicPath: process.env.NODE_ENV === 'production'
    ? config.build.assetsPublicPath
    : config.dev.assetsPublicPath
},
externals: {
  'jquery': 'window.$',
  'vue': 'window.Vue',
  'vue-router': 'window.VueRouter',
  'vuex': 'window.Vuex',
  'fastclick': 'window.FastClick'
},
resolve: {
  extensions: ['.js', '.vue', '.json'],
  alias: {
    'vue$': 'vue/dist/vue.esm.js',
    '@': resolve('src'),
  }
},

然后vue、vue-router、vuex、jquery、fastclick这些还是平常怎么用就怎么用,比如:

default
// /src/main.js
import FastClick from 'fastclick'
FastClick.attach(window.document.body)

// /src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)

但是vuex的使用是个例外,需要注释掉Vue.use(Vuex)这句代码,因为通过使用script标签加载window.Vuex时,Vuex会被自动安装,无需手动安装:

default
// import Vue from 'vue'
import Vuex from 'vuex'

// 用于使用script标签加载window.Vuex,Vuex会被自动安装,无需手动安装
// Vue.use(Vuex)
三、定义一个合并对象属性的merge方法,方便操作vuex store中的数据

/src/scripts/store.js:

default
/**
 * Created by Yakima Teng on 2017/3/8.
 */
// import Vue from 'vue'
import Vuex from 'vuex'
import { merge } from './utils'
import { config } from './mode'
import mock from '../mock'

// 用于使用script标签加载window.Vuex,Vuex会被自动安装,无需手动安装
// Vue.use(Vuex)

const state = {
config,
mock,
states: {
isLoading: true,
isAlerting: false,
alertingText: '',
alertingOkCallback () {},
alertingCancelCallback () {},
isRoaring: false,
roaringText: '',
roaringCallback () {},
isWaiting: false,
presenting: ''
},
user: {
openid: '',
phone: '',
id: '',
logined: false,
// 登录页的验证码
verificationCode: '',
// 登录页,用户是否勾选了“同意车保赢用户服务协议”
agree: true,
// 获取验证码的按钮上的文本
smsBtnText: '获取验证码',
agentName: '',
agentType: '',
agentId: ''
},
...
}

const getters = {
config: state => state.config,
mock: state => state.mock,
states: state => state.states,
user: state => state.user,
...
}

const mutations = {
setStates (state, options) { merge(state.states, options) },
setUser (state, options) { merge(state.user, options) },
...
}

const actions = {
load: ({ commit }, bool) => commit('setStates', {
isLoading: bool
}),
roar: ({ commit }, { text, callback }) => commit('setStates', {
isRoaring: true,
roaringText: text,
roaringCallback: callback || null
}),
alert: ({ commit }, { text, callback }) => commit('setStates', {
isAlerting: true,
alertingText: text,
alertingOkCallback () {
callback && callback()
commit('setStates', { isAlerting: false })
},
alertingCancelCallback: null
}),
confirm: ({ commit }, { text, okCallback, cancelCallback }) => commit('setStates', {
isAlerting: true,
alertingText: text,
alertingOkCallback () {
okCallback && okCallback()
commit('setStates', { isAlerting: false })
},
alertingCancelCallback () {
cancelCallback && cancelCallback()
commit('setStates', { isAlerting: false })
}
}),
wait: ({ commit }, bool) => commit('setStates', {
isWaiting: bool
}),
present: ({ commit }, val) => commit('setStates', {
presenting: val
})
}

export default new Vuex.Store({
state,
getters,
actions,
mutations
})

merge方法的定义:

default
// typeOf, return 'array', 'object', 'function', 'null', 'undefined', 'string', 'number'
const typeOf = input => {
  return ({}).toString.call(input).slice(8, -1).toLowerCase()
}

// 合并对象属性(在原始对象上进行修改)
const merge = (obj, options) => {
  if (obj && options) {
    for (let p in options) {
      if (typeOf(obj[p]) === 'object' && typeOf(options[p]) === 'object') {
        merge(obj[p], options[p])
      } else {
        obj[p] = options[p]
      }
    }
  }
  return obj
}

注意这个merge方法是假设如果你赋的值有数组的话那些数组都是临时创建的数组对象,而不是从其他地方引用来的数组对象,比如这样:

default
merge({
  a: 1,
  b: []
}, {
  b: arrA.map(item => {
    return {
      id: item.id,
      value: item.content
    }
  })
})

如果你赋的数组值是引用的其他地方的数组对象,建议修改下merge方法,对每个数组元素(包括数组里的数组元素)都用a = [].concat(arrB)或者a = arrB.slice(0)这种方式赋一个全新的数组对象。但是就我自己的项目而言,这种情况几乎没有,多加这样的代码除了影响性能外没啥好处。

四、延迟加载不需要立即加载的js文件

主要是延迟加载一些日期插件之类的js文件,这样首屏加载速度能快一点。示例代码如下(写在/index.html文件中):

default
<script>
  function downloadJSAfterOnload (fileUrl) {
    var elem = window.document.createElement('script')
    elem.src = fileUrl
    window.document.body.appendChild(elem)
  }
  function downloadJsFiles () {
    downloadJSAfterOnload('./static/a.js')
    downloadJSAfterOnload('http://example.com/b.js')
  }
  if (window.addEventListener) {
    window.addEventListener('load', downloadJsFiles, false)
  } else if (window.attachEvent) {
    window.attachEvent('onload', downloadJsFiles)
  } else {
    // window.onload = downloadJsFiles
  }
</script>
五、移除打包后图片文件文件名中的hash

如果你跟我一样,项目中频繁修改的是css和js代码,图片很少有改动,要改也是增删,很少有更新图片的操作的话,建议移除打包后图片文件名中的hash,这样可以将图片文件更好的缓存起来。要修改的文件为/build/webpack.base.conf.js:

default
...
{
  test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
  loader: 'url-loader',
  query: {
    limit: 10000,
    // name: utils.assetsPath('img/[name].[hash:7].[ext]')
    name: utils.assetsPath('img/[name].[ext]')
  }
},
...

或者,也可以将[hash:7]改成[chunkhash],根据webpack官方文档https://webpack.js.org/guides/caching/,这个是根据单个文件的内容产生的hash,只要这个文件内容不变,chunkhash的值也不会变。而\[hash\]是根据当前编译环境来生成的,只要当前编译的文件中有一个文件发生了变化,这个\[hash\]产生的hash值就会变化。

六、图片压缩

可以在https://tinypng.com/这个网站上事先对你的图片文件进行压缩,效果感人。

七、确保CSS值不被改写

有段时间发现本地npm run dev的时候样式好好的,npm run build后在手机里样式就变了,后面发现是一个插件搞的鬼,修改/build/webpack.prod.conf.js文件:

default
...
new OptimizeCSSPlugin({
  cssProcessorOptions: {
    safe: true
  }
}),
...

重点就是这个cssProcessorOptions: { safe: true }的配置,有些版本的vue2+webpack模板里没有这行配置,会导致比如你的z-index值被优化(坑)到你没脾气。所以如果你现在的文件里有这样配置就最好了,没有的话需要手动添加一下。

八、使用minxin避免过大的重复代码,提高可维护性

Vue提供了minxin这种在组件内插入组件属性的方法,个人建议这货能少用就少用,但是有个场景则非常建议使用minxin:当某段代码重复出现在多个组件中,并且这个重复的代码块很大的时候,将其作为一个minxin常常能给后期的维护带来很大的方便。 比如说,有个post请求,传参字段有二三十个,后端回参也有几十个字段,整个一个请求下来要写的代码量都好几十行了,这个就比较适合作为一个minxin来用了。minxin怎么用就不说了,vue官网上都有的。

天上的神明与星辰,人间的艺术和真纯,我们所敬畏和热爱的,莫过于此。