156

前言

vue-element-admin2017.04.17提交第一个 commit 以来,维护至今已经有两年多的时间了了,发布了四十多个版本,收获了三万多的 stars,远远的超出了自己的预期。距离上次手摸手系列教程也已经过去了很久,主要因为:作为一个个人开源项目,维持它已经很难了,所以真的没啥时间写详细的教程了,光是维护项目 文档 就让我很头疼了。也有不少人建议我出付费教学视频,但我个人还是更愿意把这个时间投入到维护开源项目之中吧。

本篇教程主要是趁着vue-element-admin发布了 v4.0 新版本,首先来简单说一下4.0版本做了哪些改动和优化。后半部分则会分享一些新的思考和一些小技巧吧。之前几篇手摸手文章都差不多两年前的了,但随着技术的不断发展迭代,很多之前的不能解决的问题也是都是有了新的解决方案的,同时也会出现一些新的问题和挑战。

4.0 做了什么

首先大概说一下4.0版本做了些什么,通过 pull request 可以看出这是一次比较大的升级,有大概 170 多次的 commits,200 多个文件的改动。其中最大的改变是接轨 vue 社区,直接通过 vue-cli来进行构建,省去了很多额外繁琐的配置(下文会介绍),并修改了之前 mock 数据的方案,本地改用 mock-server 来解决之前mockjs带来的各种问题。同时增加了 jest 单元测试,使用了async/await,增加了可视化配置权限,增加了自定义布局等等,优化了原先addRoutes的权限方案,支持不刷新页面更新路由等等功能。具体的可看 github release。接下来我们着重来分析一下这几个功能。

vue-cli@3

本身配置方面没有啥特别好说的,官方文档已经写得很详细了。这次更新基本上就是基于 webpack-chain 把之前的 webpack 配置迁移了一遍,因为vue-cli帮你做了很多默认配置,所有可以省去一些代码。当然这种out-of-the-box的工具利弊也很明显,它能快速上手,大部分简单场景无需任何额外配置基本就能用了。但对于复杂度高的或者自定义性强的项目来说,配置复杂度可能没有减少太多。它要求你不仅要对 webpack 或者相关工程化的东西很很熟悉,你还要对vue-cli做的一些默认配置和参数也有有一定了解,时不时要去看一下源码它到底干了啥,有的时候它的一些 plugin 出现了问题还不太好解决。而且说实话 webpack-chain 的书写也是有些门槛的,大部分情况下我也很难保证自己的配置写对的,还好官方提供了inspec功能,能让配置简单了不少。当你想知道自己的 vue-config.js 里的配置到底对不对的时候,你可以在命令行里执行vue inspect > output.js,它会将你最终生成的config展现在output.js之中,不过它默认显示的是开发环境的配置。如果你想查看其它环境的配置可以通过vue inspect --mode production > output.js。在写构建配置的时候这个功能很有帮助,同时也能帮助你了解vue-cli在构建时到底帮你做了些什么。

其它还有些需要注意的如:环境变量 必须以VUE_APP_开头啊,怎么设置polyfill啊,怎么配置各种各样的loader啊,就不展开了,文档或者社区都有很多文章了。具体配置可以参考 vue.config.js

这里还有一个黑科技,看过我之前文章的小伙伴应该还有印象,我一般在开发环境是不使用路由懒加载的,因为这样会导致热更新速度变慢,具体的可以看之前的 文章,在vue-cli@3中可以更简单的实现,你只要在.env.development环境变量配置文件中设置VUE_CLI_BABEL_TRANSPILE_MODULES:true就可以了。它的实现逻辑和原理与之前还是一样的,还是基于 plugins babel-plugin-dynamic-import-node 来实现的。之所以在vue-cli中只需要设置一个变量就可以了,是借用了vue-cli它的默认配置,它帮你代码都写好了。通过阅读 源码 可知,vue-cli会通过VUE_CLI_BABEL_TRANSPILE_MODULES这个环境变量来区分是否使用babel-plugin-dynamic-import-node,所以我们只要开其它就可以。虽然它的初衷是为了单元测试的,但正好满足了我们的需求。

总的来说,vue-cli对于大部分用户来说还是省去了一些繁琐的配置的。如果你使用本项目的话,基本也不需要做其它过多的额外配置的。

redirect 刷新页面

在不刷新页面的情况下,更新页面。这个 issue 两年前就提出来了,之前的文章里面也提供了一个 解决方案。在这里分享一下,我目前使用的新方案。

// 先注册一个名为 `redirect` 的路由
<script>
export default {
  beforeCreate() {
    const { params, query } = this.$route
    const { path } = params
    this.$router.replace({ path: '/' + path, query })
  },
  render: function(h) {
    return h() // avoid warning message
  }
}
</script>
// 手动重定向页面到 '/redirect' 页面
const { fullPath } = this.$route
this.$router.replace({
  path: '/redirect' + fullPath
})

当遇到你需要刷新页面的情况,你就手动重定向页面到redirect页面,它会将页面重新redirect重定向回来,由于页面的 key 发生了变化,从而间接实现了刷新页面组件的效果。

addRoutes && removeRoutes

看过我之前文章的人肯定知道,我目前 vue 项目的权限控制都是通过 addRoutes来实现的。简单说就是:用户登录之后会返回一个权限凭证Token,用户在根据这个Token去问服务端询问自己的权限,辟如服务端返回权限是['editor'],前端再根据这个权限动态生成他能访问的路由,再通过addRoutes进行动态的路由挂载。具体的代码可见 permission.js

但这个方案一直是有一个弊端的。那就是动态添加的路由,并不能动态的删除。这就是导致一个问题,当用户权限发生变化的时候,或者说用户登出的时候,我们只能通过刷新页面的方式,才能清空我们之前注册的路由。之前老版本的 vue-element-admin就一直采用的是这种方式。虽然能用,但作为一个 spa,刷新页面其实是一种很糟糕的用户体验。但是官方也迟迟没有出相关的 remove api,相关 issue

后来发现了一种 hack 的方法,能很好的动态清除注册的路由。先看代码:

它的原理其实很简单,所有的 vue-router 注册的路由信息都是存放在matcher之中的,所以当我们想清空路由的时候,我们只要新建一个空的Router实例,将它的matcher重新赋值给我们之前定义的路由就可以了。巧妙的实现了动态路由的清除。
现在我们只需要调用resetRouter,就能得到一个空的路有实例,之后你就可以重新addRoutes你想要的路由了。完整的代码实例 router.jsresetRouter

Mock 数据

如果你在实际开发中,最理想的前后端交互方式当然是后端先帮我们 mock 数据,然后前端开发。但现实很骨感,总会因为种种原因,前端需要自己来 mock 假数据。尤其是我的几个开源项目,都是纯前端项目,根本没有后端服务。
在之前的文章中也介绍过,vue-element-adminvue-admin-template 使用的是 MockJSeasy-mock 这两个库。但实际用下来两者都有一些问题。

  • MockJs

    它的原理是: 拦截了所有的请求并代理到本地,然后进行数据模拟,所以你会发现 network 中没有发出任何的请求。但它的最大的问题是就是它的实现机制。它会重写浏览器的XMLHttpRequest对象,从而才能拦截所有请求,代理到本地。大部分情况下用起来还是蛮方便的,但就因为它重写了XMLHttpRequest对象,所以比如progress方法,或者一些底层依赖XMLHttpRequest的库都会和它发生不兼容,可以看一下我项目的 issues,就知道多少人被坑了。

    它还有一个问题:因为是它是本地模拟数据,实际上不会走任何网络请求。所以本地调试起来很蛋疼,只能通过console.log来调试。就拿vue-element-admin来说,想搞清楚 getInfo()接口返回了什么数据,只能通过看源码或者手动 Debug 才能知道。

  • Easy-Mock

    这个项目刚出的时候用的人比较少,还真的挺好用的。天然支持跨域,还是支持MockJs的所有语法,我在之前也推荐过。但因为用的人多了,它的免费服务会经常的挂,可以说天天挂。。。但毕竟人家这是免费的服务,也不能苛求什么,官方的建议是自己搭建服务。如果你的公司整体搭建一个这样的 mock 服务的话也是一个不错的选择。但大部分人可能还是没有这个技术条件的。

新方案

所以我一直在寻求一个更好的解决方案,我也去体验了其它很多 mock api 服务,如 mockapiMocky 等等。总之体验都不能满足我的需求。

v4.0版本之后,在本地会启动一个mock-server来模拟数据,线上环境还是继续使用mockjs来进行模拟(因为本项目是一个纯前端项目,你也可以自己搭建一个线上 server 来提供数据)。不管是本地还是线上所以的数据模拟都是基于mockjs生成的,所以只要写一套 mock 数据,就可以在多环境中使用。

该方案的好处是,在保留 mockjs的优势的同时,解决之前的痛点。由于我们的 mock 是完全基于webpack-dev-serve来实现的,所以在你启动前端服务的同时,mock-server就会自动启动,这里还通过 chokidar 来观察 mock 文件夹内容的变化。在发生变化时会清除之前注册的mock-api接口,重新动态挂载新的接口,从而支持热更新。有兴趣的可以自己看一下代码 mock-server.js。由于是一个真正的server,所以你可以通过控制台中的network,清楚的知道接口返回的数据结构。并且同时解决了之前mockjs会重写 XMLHttpRequest对象,导致很多第三方库失效的问题。

在本地开发环境中基于webpack-dev-serveafter这个middleware中间件,在这里自动读取你的 mock文件,模拟出 REST API,它最大的好处是,完全不需要什么额外的工作,完全基于webpack-dev-serve就能实现。如果你还是想单独启动一个serve也是可以的,完全可以引入一个express或者其它插件来启动一个 mock-serve。

我们模拟数据有了,现在要做的事情就是,将我们的接口代理到我们的 mock 服务上就好了,这里我们使用webpack-dev-serve自带的 proxy进行接口代理。

proxy: {
      // xxx-api/login => mock/login
      [process.env.VUE_APP_BASE_API]: {
        target: `http://localhost:${port}/mock`,
        changeOrigin: true,
        pathRewrite: {
          ['^' + process.env.VUE_APP_BASE_API]: ''
        }
      }
    }

snippets 自动生成代码片段

平时日常工作中,做最多的就是写业务模块和组件。当每次新开一个view或者component的时候都需要手动创建一个新.vue文件,然后再创建<template><script><style>这些标签,还是有些麻烦的。

所以在新版本中,基于plop,提供了几个基础模板,方便创建新的view或者component
执行如下命令:

npm run new

plop

如上面 gif 所示,现在只要轻松的点几次回车就可以轻松生成我要的基础代码片段。这里只是一个 demo,你完全可以按照自己需求定制模板。老版本的vue-cli实现逻辑和它类似。

如果你觉得配置太复杂,我推荐你可以安装如 Vue 2 Snippets VS Code插件。 这种代码片段在平时工作中还是能提升不少开发效率的。

async/await or promise

本次更新中,我也将部分代码用了async/await的方式替代了原有的 promise方式,主要是 @/src/permission.js。有兴趣的大家自己可以通过 git-history 自己对比下,可以发现代码阅读性高了不少。 不过本项目中也并没有把所有promiseasync/await替代。我来简单说一下我的看法。

6 个 Async/Await 优于 Promise 的方面,这篇文章很多人应该都看过,里面大部分观点我都是同意的,大部分复杂场景下async/await的确是更优解。但相对的也不是所有的情况下都是async/await写起来让我更爽的。先说说我最不爽的地方是它的错误处理,try catch让这个代码结构看起来就很奇怪(当然也有很多人很喜欢这种错误处理形式。社区也是相对的解决方案类似go语言的风格,比如 await-to-js

[err, res] = await to(getInfo())
if(err) //do something

这个方案是不错,但还需要引入一个新的库,增加了学习成本,得不偿失。所以以我个人的习惯,当只有一个异步请求,且需要做错误处理的情况下,更倾向于使用 promise。比如

// promise
getInfo()
  .then(res => {
    //do somethings
  })
  .catch(err => {
    //do somethings
  })

// async/await
try {
  const res = await getInfo()
  //do somethings
} catch (error) {
  //do somethings
}

在有嵌套请求的情况下,肯定是 async/await 更直观的。

// promise
a(() => {
  b(() => {
    c()
  })
})

// async/await
await a()
await b()
await c()

当然代码写的好与不好还是取决于写代码的人的。比如一个常见的业务场景:有两个并发的异步请求,在都完成后do something。但很多人会错误的用串行的方式实现了。

//错误
await a()
await b()
//这样变成了 a().then(() => b() )
// a 好了才会执行 b
done()

//正确
await Promise.all([a(), b()])
done()

还有一个小细节async/await打包后的代码>)其实会比 promise 复杂很多, 当然这个是一个忽略不计得问题。

总结:我认为它们两个人并不是or的关系,在特定的业务场景下,选择相对而言代码可读性更好地解决方案。

以上所述纯个人偏爱,并非什么最佳实现。具体该怎么选择还是需要大家更具自己团队的风格或者自己的理解来判断。

命名规范

其实刚开始我写 vue 文件的时候也不注意,各种驼峰啊、大写开头 (PascalCase)还是横线连接 (kebab-case)混着来,谁叫 vue 都可以,在 风格指南 中也没有定论。不过基于本项目我还是整理了一套文件的命名规则。

Component

所有的Component文件都是以大写开头 (PascalCase),这也是官方所 推荐的

但除了 index.vue

例子:

  • @/src/components/BackToTop/index.vue
  • @/src/components/Charts/Line.vue
  • @/src/views/example/components/Button.vue

JS 文件

所有的.js文件都遵循横线连接 (kebab-case)。

例子:

  • @/src/utils/open-window.js
  • @/src/views/svg-icons/require-icons.js
  • @/src/components/MarkdownEditor/default-options.js

Views

views文件下,代表路由的.vue文件都使用横线连接 (kebab-case),代表路由的文件夹也是使用同样的规则。

例子:

  • @/src/views/svg-icons/index.vue
  • @/src/views/svg-icons/require-icons.js

使用横线连接 (kebab-case)来命名views主要是出于以下几个考虑。

  • 横线连接 (kebab-case) 也是官方推荐的命名规范之一 文档
  • views下的.vue文件代表的是一个路由,所以它需要和component进行区分(component 都是大写开头)
  • 页面的url 也都是横线连接的,比如https://www.xxx.admin/export-excel,所以路由对应的view应该要保持统一
  • 没有大小写敏感问题

CDN

你可以通过执行npm run preview -- --report来分析webpack打包之后的结果,观察各个静态资源的大小。你可以发现占用空间最多的是第三方依赖。如vueelement-uiECharts等。

你可以使用 CDN 外链的方式引入第这些三方库,这样能大大增加构建的速度(通过 CDN 引入的资源不会经 webpack 打包)。如果你的项目没有自己的CDN服务的话,使用一些第三方的CDN服务,如 jsdelivrunpkg 等是一个很好的选择,它提供过了免费的资源加速,同时提供了缓存优化,由于你的第三方资源是在html中通过script引入的,它的缓存更新策略都是你自己手动来控制的,省去了你需要优化缓存策略功夫。

很多文章说使用 CDN 引入的方式能大大减小代码的体积,这是不可能的。虽然打包完的 bundle小了,但那部分代码只是被你拆出去,用CDN的方式引入罢了。你想减小体积,最高效的方案是启用GZIP

我个人暂时不使用CDN引入第三方依赖的原因:

暂时构建速度还没有遇到什么瓶颈,所有没有必要单独剥离部分第三方依赖。使用CDN引入的方式等于一些第三方依赖的版本你是通过package.json来控制的,一些依赖则需要手动维护,增加了一些维护成本。目前基于 webpack 的optimization.splitChunks已经做了资源的缓存优化,静态资源的缓存已经做得很好了。并且目前所有的静态资源都会上传到自己的CDN服务,没有必要使用第三方的CDN服务。

当然所有的优化都是需要结合自己的具体业务来调整的! 之后可能会采用这种引入方式,或者使用webpack dll的方式进行优化。如果你觉得CDN引入对于的项目有益处,你可以遵循如下方法进行修改:

使用方式

先找到 vue.config.js, 添加 externalswebpack 不打包 vueelement

externals: {
  vue: 'Vue',
  'element-ui':'ELEMENT'
}

然后配置那些第三方资源的CDN,请注意先后顺序。

const cdn = {
  css: [
    // element-ui css
    'https://unpkg.com/element-ui/lib/theme-chalk/index.css'
  ],
  js: [
    // vue must at first!
    'https://unpkg.com/vue/dist/vue.js',
    // element-ui js
    'https://unpkg.com/element-ui/lib/index.js'
  ]
}

之后通过 html-webpack-plugin注入到 index.html之中:

config.plugin('html').tap(args => {
  args[0].cdn = cdn
  return args
})

找到 public/index.html。通过你配置的CND Config 依次注入 css 和 js。

<head>
  <!-- 引入样式 -->
  <% for(var css of htmlWebpackPlugin.options.cdn.css) { %>
    <link rel="stylesheet" href="<%=css%>">
  <% } %>
</head>

<!-- 引入JS -->
<% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
  <script src="<%=js%>"></script>
<% } %>

完整的 代码修改

最终你可以使用 npm run preview -- --report 查看效果 如图:

同理,其它第三方依赖都可以使用相同的方式处理(比如vuexvue-router等)。当然你也可以选择使用 DLLPlugin的方式来处理第三方依赖,从而来优化构建。

小技巧与建议

Watch immediate

这个已经算是一个比较常见的技巧了,这里就简单说一下。当 watch 一个变量的时候,初始化时并不会执行,如下面的例子,你需要在created的时候手动调用一次。

// bad
created() {
  this.fetchUserList();
},
watch: {
  searchText: 'fetchUserList',
}

你可以添加immediate属性,这样初始化的时候也会触发,然后上面的代码就能简化为:

// good
watch: {
  searchText: {
    handler: 'fetchUserList',
    immediate: true,
  }
}

ps: watch 还有一个容易被大家忽略的属性deep。当设置为true时,它会进行深度监听。简而言之就是你有一个 const obj={a:1,b:2},里面任意一个 key 的 value 发生变化的时候都会触发watch。应用场景:比如我有一个列表,它有一堆query筛选项,这时候你就能deep watch它,只有任何一个筛序项改变的时候,就自动请求新的数据。或者你可以deep watch一个 form 表单,当任何一个字段内容发生变化的时候,你就帮它做自动保存等等。

Attrs 和 Listeners

这两个属性是 vue 2.4 版本之后提供的,它简直是二次封装组件或者说写高阶组件的神器。在我们平时写业务的时候免不了需要对一些第三方组件进行二次封装。比如我们需要基于el-select分装一个带有业务特性的组件,根据输入的 name 搜索用户,并将一些业务逻辑分装在其中。但el-select这个第三方组件支持几十个配置参数,我们当然可以适当的挑选几个参数通过 props 来传递,但万一哪天别人用你的业务组件的时候觉得你的参数少了,那你只能改你封装的组件了,亦或是哪天第三方组件加入了新参数,你该怎么办?

其实我们的这个组件只是基于el-select做了一些业务的封装,比如添加了默认的placeholder,封装了远程 ajax 搜索请求等等,总的来说它就是一个中间人组件,只负责传递数据而已。

这时候我们就可以使用v-bind="$attrs":传递所有属性、v-on="$listeners"传递所有方法。如下图所示:

这样,我们没有在$props中声明的方法和属性,会通过$attrs$listeners直接传递下去。这两个属性在我们平时分装第三方组件的时候非常有用!

.sync

这个也是 vue 2.3 之后新加的一个语法糖。这也是平时在分装组件的时候很好用的一个语法糖,它的实现机制和v-model是一样的。

当你有需要在子组件修改父组件值的时候这个方法很好用。
线上例子

Computed 的 get 和 set

computed 大家肯定都用过,它除了可以缓存计算属性外,它在处理传入数据和目标数据格式不一致的时候也是很有用的。set、get 文档

上面说的可能还是是有点抽象,举一个简单的的例子:我们有一个 form 表单,from 里面有一个记录创建时间的字段create_at。我们知道前端的时间戳都是 13 位的,但很多后端默认时间戳是 10 位的,这就很蛋疼了。前端和后端的时间戳位数不一致。最常见的做法如下:

上面的代码主要做的是:在拿到数据的时候将后端 10 位时间戳转化为 13 位时间戳,之后再向服务端发送数据的时候再转化回 10 位时间戳传给后端。目前这种做法当然是可行的,但之后可能不仅只有创建接口,还有更新接口的时候,你还需要在update的接口里在做一遍同样数据转化的操作么?而且这只是一个最简单的例子,真实的 form 表单会复杂的多,需要处理的数据也更为的多。这时候代码就会变得很难维护。

这时候就可以使用 computed 的 set 和 get 方法了。

通过上面的代码可以看到,我们把需要做前后端兼容的数据,放在了 computed 中,从 getDatasubmit中隔离了数据处理的部分。

当然上面说的方案还不是最好的方案,你其实应该利用之前所说的v-bind="$attrs"v-on="$listeners"对时间选择器组件进行二次封装。例如这样<date-time v-model="postForm.create_at" /> 外部无需做任何数据处理,直接传入一个 10 位的时间戳,内部进行转化。当日期发生变化的时候,自动通过emit触发input使v-model发生变化,把所有脏活累活都放在组件内部完成,保持外部业务代码的相对干净。具体 v-model 语法糖原理可以见官方 文档

set 和 get 处理可以做上面说的进行一些数据处理之外,你也可以把它当做一个 watch的升级版。它可以监听数据的变化,当发生变化时,做一些额外的操作。最经典的用法就是v-model上绑定一个 vuex 值的时候,input 发生变化时,通过 commit更新存在 vuex 里面的值。

具体的解释你也可以见官方 文档

Object.freeze

这算是一个性能优化的小技巧吧。在我们遇到一些 big data的业务场景,它就很有用了。尤其是做管理后台的时候,经常会有一些超大数据量的 table,或者一个含有 n 多数据的图表,这种数据量很大的东西使用起来最明显的感受就是卡。但其实很多时候其实这些数据其实并不需要响应式变化,这时候你就可以使用 Object.freeze 方法了,它可以冻结一个对象(注意它不并是 vue 特有的 api)。

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter,它们让 Vue 能进行追踪依赖,在属性被访问和修改时通知变化。
使用了 Object.freeze 之后,不仅可以减少 observer 的开销,还能减少不少内存开销。相关 issue

使用方式:this.item = Object.freeze(Object.assign({}, this.item))

这里我提供了一个在线测速 demo,点我

通过测速可以发现正常情况下1000 x 10 rerender 都稳定在 1000ms-2000ms 之间,而开启了Object.freeze的情况下,rerender 都稳住在 100ms-200ms 之间。有接近 10 倍的差距。所以能确定不需要变化检测的情况下,big data 还是要优化一下的。

Functional

函数式组件 这个是文档里就写的内容,但在其实很少人会刻意的去使用。因为你不用它,代码也不会有任何问题,用了到可能会出现 bug。

我们先看一个例子:点我测试性能 肉眼可见的性能差距。当然很多人会觉得我的项目中也没有这种变化量级,但我觉得这是一个程序员的自我修养问题吧。,比如能用v-show的地方就不要用v-if,善用keep-alivev-onceObject.freeze()处理 vue big data 问题等。虽然都是一些小细节,但对性能和体验都是有不少的提升的。更多的性能优化技巧请查看该文章 vue-9-perf-secrets

减少全局操作

这其实并不只是针对 vue 项目的一个建议,我们平时写代码的时候一定要尽量避免一些全局的操作。如果必须要用到的时候,一定要自己检查,会不会产生一些全局的污染或者副作用。

举几个简单例子:

  1. 我们现在虽然用 vue 写代码了,核心思想转变为用数据驱动 view,不用像jQuery时代那样,频繁的操作 DOM 节点。但还是免不了有些场景还是要操作 DOM 的。我们在组件内选择节点的时候一定要切记避免使用 document.querySelector()等一系列的全局选择器。你应该使用this.$el或者this.refs.xxx.$el的方式来选择 DOM。这样就能将你的操作局限在当前的组件内,能避免很多问题。
  2. 我们经常会不可避免的需要注册一些全局性的事件,比如监听页面窗口的变化window.addEventListener('resize', this.__resizeHandler),但再声明了之后一定要在 beforeDestroy或者destroyed生命周期注销它。window.removeEventListener('resize', this.__resizeHandler)避免造成不必要的消耗。
  3. 避免过多的全局状态,不是所有的状态都需要存在 vuex 中的,应该根据业务进行合理的进行取舍。如果不可避免有很多的值需要存在 vuex 中,建议使用动态注册的方式。相关文档。只是部分业务需要的状态处理,建议使用 Event Bus或者使用 简单的 store 模式
  4. css 也应该尽量避免写太多的全局性的样式。除了一些全局公用的样式外,所以针对业务的或者组件的样式都应该使用命名空间的方式或者直接使用 vue-loader 提供的 scoped写法,避免一些全局冲突。文档

Sass 和 Js 之间变量共享

这个需求可能有些人没有遇到过,举个实际例子来说明一下。


如上面要实现一个动态的换肤,就需要将用户选择的 theme 主题色传递给 css。但同时初始化的时候 css 又需要将一个默认主题色传递给 js。所以下面我们就分两块来讲解。

  • js 将变量传递给 sass
    这部分是相对简单就可以实现的,实现方案也很多。最简单的方法就是通过 在模板里面写 style 标签来实现,就是俗话所说的内联标签。

    <div :style="{'background-color':color}" ></div>

    或者使用 css var(),在线 demo,还有用 less 的话modifyVars,等等方案都能实现 js 与 css 的变量传递。

  • sass 将变量给 js

还是那前面那个换肤来举例子,我们页面初始化的时候,总需要一个默认主题色吧,假设我们在 var.scss中声明了一个 theme:blue,我们在 js 中该怎么获取这个变量呢?我们可以通过 css-modules :export来实现。更具体的解释-How to Share Variables Between Javascript and Sass

// var.scss
$theme: blue;

:export {
  theme: $theme;
}
// test.js
import variables from '@/styles/var.scss'
console.log(variables.theme) // blue

当 js 和 css 共享一个变量的时候这个方案还是很实用的。vue-element-admin 中的侧边栏的宽度,颜色等等变量都是通过这种方案来实现共享的。

其它换肤方案可以参考 聊一聊前端换肤

自动注册全局组件

我的业务场景大部分是中后台,虽然封装和使用了很多第三方组件,但还是免不了需要自己封装和使用很多业务组件。但每次用的时候还需要手动引入,真的是有些麻烦的。

我们其实可以基于 webpack 的require.context来实现自动加载组件并注册的全局的功能。相关原理在之前的文章中已经阐述过了。具体代码如下

我们可以创建一个GlobalComponents文件夹,将你想要注册到全局的组件都放在这个文件夹里,在index.js里面放上如上代码。之后只要在入口文件main.js中引入即可。

//main.js
import './components/Table/index' // 自动注册全局业务组件

这样我们可以在模板中直接使用这些全局组建了。不需要再繁琐的手动引入了。

<template>
  <div>
    <user-select/>
    <status-button/>
  </div>
</template>

当然你也不要为了省事,啥组件都往全局注册,这样会让你初始化页面的时候你的初始init bundle很大。你应该就注册那些你经常使用且体积不大的组件。那些体积大的组件,如编辑器或者图表组件还是按需加载比较合理。而且你最好声明这些全局组件的时候有一个统一的命名规范比如:globel-user-select这样的,指定一个团队规范,不然人家看到你这个全局组件会一脸懵逼,这个组件是哪来的。

Lint

这又是一个老生常谈的问题了
vue 的一些最佳实践什么的话,这里不讨论了,我觉得看官方的 风格指南 差不多就够了。比如避免避免 v-if 和 v-for 用在一起元素特性的顺序这些等等规则,几十条规则,说真的写了这么久 vue,我也只能记住一些常规的。什么属性的顺序啊,不太可能记住的。这种东西还是交给程序来自动优化才是更合理的选择。强烈推荐配置编辑器自动化处理。具体配置见 文档。同时建议结合 Git Hooks 配合在每次提交代码时对代码进行 lint 校验,确保所有提交到远程仓库的代码都符合团队的规范。它主要使用到的工具是huskylint-staged,详细文档见 Git Hooks

Hook

这个是一个文档里没有写的 api,但我觉得是一个很有用的 api。比如我们平时使用一些第三方组件,或者注册一些全局事件的时候,都需要在mounted中声明,在destroyed中销毁。但由于这个是写在两个生命周期内的,很容易忘记,而且大部分在创建阶段声明的内容都会有副作用,如果你在组件摧毁阶段忘记移除的话,会造成内存的泄漏,而且都不太容易发现。如下代码:

react 在新版本中也加入了useEffect,将以前的多个 life-cycles 合并、重组,使逻辑更加清晰,这里就不展开了。那 vue 是不是也可以这样做?我去了看了一下官方的 vue-hooks源码 发现了一个新的 api:$on('hook:xxx')。有了它,我们就能将之前的代码用更简单和清楚地方式实现了。

和 react 的useEffect有异曲同工之妙。

而且我们有了这个 api 之后,能干的事情还不止这个。有时候我们会用一些第三方组件,比如我们有一个编辑器组件(加载比较慢,会有白屏),所以我们在它渲染完成之前需要给它一个占位符,但可能这个组件并没有暴露给我们这个接口,当然我们需要修改这个组件,在它创建的时候手动 emit 一个事件出去,然后在组件上监听它,比如:

当然这也是可行的,但万一还要监听一个更新或者摧毁的生命周期呢?其实利用 hook可以很方便的实现这个效果。

当然在 vue 3.0 版本中可能会有新的写法,就不如下面的讨论: Dynamic Lifecycle Injection。有兴趣的可以自行去研究,这里就不展开了。当 3.0 正式发布之后再来讨论吧。

RoadMap

最后来说一下,之后需要做的事情吧:

  • 更好的多级页面缓存:目前页面的缓存基于keep-alive,但当三级路由嵌套的情况下,支持的并不好。之后探索一个更好的解决方案。
  • 单元测试:当项目大了之后,没有单元测试维护起来还是有些吃力的。
    之后会慢慢补上unit-test 的测试用例。 酌情加上一些e2e-test的例子。
  • 去国际化:其实大部分人是不需要国际化的,默认情况下移除国际化。单独开一个国际化分支(v4.1 已完成)。
  • 适配 webpack5:webpack5 还是解决了不少之前的痛点的,正式版发布之后会进行升级。
  • vue 3.0: 等官方发布之后会基于新版本进行重构(这个或许还有很久)
  • 适配 element-ui 3.0 之前官方发了 3.0 的打算(我也不知道会不会跳票)

总结

开源不易,且行且珍惜吧。

系列文章:


花裤衩
8.9k 声望5k 粉丝

show me the code