Vue实战:一口气理解和配置你的vue-cli3项目

Vue实战:一口气理解和配置你的vue-cli3项目

Vue实战系列教程二,本篇是针对线上项目从cli2升级到cli3的一个过程,记录了升级(爬坑)过程,其中包括了cli3的变化vue.config.js的关键配置,以及 项目结构组织构建流程优化 等内容,对 cli3 项目搭建中应知应会的知识点做了一个实践总结。

本文代码仓库,已整理到 『https://github.com/WeideMo/easy-cli3-boilerplate』

实战系列之前有 Vue.js渡劫系列, 读者如果有兴趣也可以先进行通用了解,附上传送门:

『Vue实战系列一:简单几步,优化你的开发体验与效率』

『Vuejs渡劫系列一:日常开发中必须掌握的细节(keng)』

『Vuejs渡劫系列二:最全的vue-cli项目下的配置简析』

『Vuejs渡劫系列三:构建一个包含路由控制、状态管理和权限校验的vue-cli项目』

基础

安装

安装这部分其实 CLI 3 官网已经叙述的比较清楚,原本是想直接跳过这part,但是考虑到连贯性,还是对安装部分简单的总结以下几点:

  • npm install -g @vue/cli 进行全局安装
  • 可通过 vue create [projectname] or vue ui进行可视化创建项目

cli-service 和 cli-plugin

cli-service 是整个CLI的开发环境依赖,和cli-plugin则是插件,可以理解为容器插件 的关系,通过查看 package.jsondevDependencies:

1
2
3
4
5
6
7
"devDependencies": {
"@vue/cli-plugin-babel": "^3.6.0",
"@vue/cli-plugin-eslint": "^3.6.0",
"@vue/cli-plugin-pwa": "^3.6.0",
"@vue/cli-plugin-unit-mocha": "^3.6.0",
"@vue/cli-service": "^3.6.0"
},

我们不难发现了,规律性的插件命名,和显眼的cli-service,通过基于 cli-service的底层环境,结合各种实用的 cli-plugin-x插件,是 CLI 3的核心组成部分。

CLI 3 的变化

组织结构

新的 CLI 毕竟是大版本的迭代,因此在文件组织结构上也带来了一定的变化,简单的归纳为 约定大于配置,这个思想也很符合行业的主流,以下是一些主要的组织结构变动:

webpack build 配置移除

CLI 3 中,我们会发现根目录下的 /build目录被移除了,原有通过 webpack.[env].conf.js 文件去配置不同环境的构建更改为 configureWebpack对象的配置,笔者认为文件的方式相对更为直观,而通过对象配置,更为简洁,有点springBoot中通过 config bean的味道。

环境变量和模式

CLI 2中,环境变量统一放在 /config中管理,而在 CLI 3中,则需要在根目录下新建对应的环境文件,如下图所示:

Events

通过文件名去标识不同的模式,在执行特定模式构建时,环境文件将会被加载,如在 package.json 中增加一个包分析的构建脚本

1
2
3
"scripts": {
"analyz": "vue-cli-service build --mode analyz"
},

则在执行 npm run analyz 时,会自动加载根目录下的 .env.analyz中的环境变量。

资源文件目录

相对 CLI 2, 新版的资源目录从原来的 static=> public,原来的模板文件 index.html,迁移至 /public/index.html

更简洁的根目录

CLI 2中,很多插件的配置会放置在根目录,如 .babelrc.postcssrc.js 等配置,现在都可以在统一配置在 package.json中了。

项目结构组织

在对 CLI 3进行配置之前,我们有必要对项目结构进行调整,其中包括新建一些文件配置,以及增加一些工具等,使得我们的脚手架能更优。

新建环境文件

在根目录下,新建 .env.env.production.env-anaylyz 等环境文件,并在对应的文件中配置好对应的环境变量,如 NODE_ENV 等,以下是 .env.analyz中的环境变量,通过判断 IS_ANALYZ的值,可以判断当前构建模式是否在 analyz mode下。

1
2
NODE_ENV = 'production'
IS_ANALYZ = 'analyz'

在package.json中统一管理配置

前文也提到过,针对一些插件的配置,我们可以使用 package.json 对其统一管理,以下是针对 eslintpostcssbabel浏览器兼容列表 等配置

elsint配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
}

postcss处理

1
2
3
4
5
"postcss": {
"plugins": {
"autoprefixer": {}
}
}

浏览器兼容性

1
2
3
4
5
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]

CLI 3在构建过程中会通过扫描 package.json 的关键字去结合处理,达到通过将配置集中化处理的目的。

注册全局组件

很多时候,我们需要注册一些通用的全局组件,减少重复import的工作量,此时我们可以在 /src/components/common下新建组件,然后新建一个 registerGlobalComponents 扫描文件,去自动帮我们扫描注册 Global Components

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// registerGlobalComponents.js
/**
*{全局扫描注册组件}
*/
import Vue from 'vue'
// 自动加载 common 目录下的 .js 结尾的文件
const componentsContext = require.context('@/components/common', true, /\.js$/)
componentsContext.keys().forEach(component => {
const componentConfig = componentsContext(component)
/**
* 兼容 import export 和 require module.export 两种规范
*/
const ctrl = componentConfig.default || componentConfig
Vue.component(ctrl.name, ctrl)
})

后续如果我们需要新增全局组件,只需在/src/components/common下新建组件,及通过组件入口 index.js 引用即可,详情可以参考demo项目。

状态与路由

鉴于状态和路由的配置,边幅较大,不在此文展开,后续实战系列会说到,请期待。

vue.config.js 配置

讲了这么久,终于到了主角 vue.config.js 的配置,作为 CLI 3的统一配置文件,刚更新的同学可能会一脸懵逼,因为对比原有的文件直观性,CLI 3的配置可以说变得更抽象,如果是初学者更是晦涩难懂,这里不会对所有API 一个个说,只是针对一些实用性强的配置进行举例使用:

publicPath

v3.3之前,又叫 baseUrl,可以理解为项目的一个contextPath,默认为/, 如果你配置了为 yourProjectPath,那么在构建后,所有引用的路径都会带上 yourProjectPath

1
2
3
4
5
6
7
// vue.config.js
const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV);
// yourProject Context-path
const productionPath = '/yourProjectPath/'
module.exports = {
publicPath: IS_PROD ? productionPath : '/'
}

如我们在后端服务中,如spring-boot的配置中,增加了一个名为 yourProjectPath 的项目路径,就可以如上设置,去区分开发环境和生产环境。

assetsDir

构建后的资源输出路径,默认是直接生成在 /dist下,但是如果为了方便资源管理,我们一般会新建一个 static的目录,通过配合后端服务的静态资源路径使用:

1
2
3
4
// vue.config.js
module.exports = {
assetsDir: 'static'
}

在配置完成后,执行项目构建 npm run build 之后,所有的打包资源路径都有附带路径前缀 /yourProjectPath/static/xxx.js/yourProjectPath/static/xxx.css

lintOnSave

在安装了 @vue/cli-plugin-eslint 之后才会生效,默认是安装插件后会自动开启,插件会根据你配置的eslint校验规则去检查,如果团队开发不能统一规范的,这意味着你的代码面临编译不通过的风险,此时可以通过配置:

1
2
3
4
// vue.config.js
module.exports = {
lintOnSave: false
}

productionSourceMap

通过配置 productionSourceMap 来告诉打包编译时,是否需要在生产环境中生成对应的sourceMap:

1
2
3
4
// vue.config.js
module.exports = {
lintOnSave: true
}

如果项目需要在生产环境排查和调试,建议开始,否则可以关闭提高构建速度。

configureWebpack

通过定义一个配置对象,改对象会通过 webpack-merge 插件合并到最终的webpack配置,官方写的很清楚了,大概是这样的一个样子:

1
2
3
4
5
6
7
8
// vue.config.js
module.exports = {
configureWebpack: {
plugins: [
new MyAwesomeWebpackPlugin()
]
}
}

这里的 MyAwesomeWebpackPlugin 就是你构建中需要使用的插件,当然要通过import才能使用,这里写的比较简单,后文会通过实例继续说明。

css.loaderOptions

该选项,提供了一个针对CSS预处理的统一配置入口,包括了SassLess等的,如我们在应用中有一个包含了网站的颜色、大小的变量scss文件 variables.scss,通过以下配置可以传入共享的变量:

1
2
3
4
5
6
7
8
9
10
11
12
// vue.config.js
module.exports = {
css: {
loaderOptions: {
// 给 sass-loader 传递选项
sass: {
// @/ 是 src/ 的别名
data: `@import "~@/variables.scss";`
}
}
}
}

不难理解,其实就是在构建过程中,切面式的注入了全局样式,以减少一些高频文件的引入重复工作。

chainWebpack

改选项也是版本改动变化里的一个大头,简单而言就是 CLI 3通过 webpack-chain的方式,去组织了部分webpack构建的配置,对于熟悉的人而言会变得更简洁,但是个人感觉这部分有点晦涩和抽象了,下面是我整合的一些配置,分别包括了 添加别名html取消压缩, 打包分析等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// vue.config.js
module.exports = {
chainWebpack: config => {
// 添加别名
config.resolve.alias
.set('@', resolve('src'))
.set('@assets', resolve('src/assets'))
.set('@components', resolve('src/components'))
.set('@router', resolve('src/router'))
.set('@views', resolve('src/views'));
// html页面禁止压缩
config
.plugin('html')
.tap(args=>{
args[0].minify = false;
return args;
});
// 打包分析
if (process.env.IS_ANALYZ) {
config.plugin('webpack-report')
.use(BundleAnalyzerPlugin, [{
analyzerMode: 'static',
}]);
}
}
}

其中,config.resolveconfig.plugin 其实就是对应webpack 下面的 resolveplugin 配置,其实不难懂,参考我的例子和对应的webpack插件和配置项,就可以照葫芦画瓢。

同时,如果我们不满意默写, 可以通过 clear() 方法清除旧规则后去复写新的规则。

devServer.proxy

该选项是针对开发环境的一个代理服务,通过URL的 requestMaping 转发到对应后端服务上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: '<url>',
ws: true,
changeOrigin: true
},
'/foo': {
target: '<other_url>'
}
}
}
}

由于项目里已经用了yapi,所以就没配置 proxy 项了,有兴趣的同学也可以了解下 yapi(传送门)

parallel

基于node 的 os module去判断操作系统里的CPU是否支持并行构建,以提高构建速度。

1
2
3
4
// vue.config.js
module.exports = {
parallel: require('os').cpus().length > 1
}

构建流程优化

基于 CLI 3的脚手架去构建项目,其实有使用 CLI 2的用户都知道,提升了已经不止一点点了,当然,作为一个爱折腾的玩家而言,我们还可以继续润色:

生产模式下启用 Gzip 压缩

通过在构建过程中使用 CompressionWebpackPlugin 插件,在前端构建时完成.gz文件输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// vue.config.js
module.exports = {
configureWebpack: config => {
// 生产模式启用gzip压缩
if (IS_PROD) {
const plugins = [];
plugins.push(
new CompressionWebpackPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: productionGzipExtensions,
threshold: 10240,
minRatio: 0.8
});
// 合并plugins
config.plugins = [
...config.plugins,
...plugins
];
}
}
}

通过在 configureWebpack 中新创建一个空数组,然后通过解构语法,合并到原始的 config.plugins 中,完成配置合并。

DLL 动态链接库,分离稳定三方库

这个其实不是什么新鲜技术,简单理解就是将一些相对稳定的 node module 从最终的输出文件中分离的技术,达到了文件 动静分离,优化构建及浏览器侧缓存的的应用,下面例子是基于 DllReferencePlugin 插件实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// vue.config.js
module.exports = {
configureWebpack: config => {
// 生产模式启用gzip压缩
if (IS_PROD) {
const plugins = [];
plugins.push(
new webpack.DllReferencePlugin({
context: process.cwd(),
manifest: require('./public/vendor/vendor-manifest.json')
}),
// 将 dll 注入到 生成的 html 模板中
new AddAssetHtmlPlugin({
// dll文件位置
filepath: path.resolve(__dirname, './public/vendor/*.js'),
// dll 引用路径
publicPath: `/${process.env.BASE_URL}/js/vendor`,
// dll最终输出的目录
outputPath: '/js/vendor'
});
// 合并plugins
config.plugins = [
...config.plugins,
...plugins
];
}
}
}

在使用中,我们需要在根目录下,新增1个 dll.config.js 的文件,里面主要是通过配置你常用的稳定模块,告诉webpack后续打包,单独输出到一个vendor文件中,下面是我的栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const path = require('path')
const webpack = require('webpack')
const CleanWebpackPlugin = require('clean-webpack-plugin')
// dll文件存放的目录
const dllPath = 'public/vendor'
module.exports = {
entry: {
// 需要提取的库文件
vendor: ['vue', 'vue-router', 'vuex', 'axios']
},
output: {
path: path.join(__dirname, dllPath),
filename: '[name].dll.js',
// vendor.dll.js中暴露出的全局变量名
// 保持与 webpack.DllPlugin 中名称一致
library: '[name]_[hash]'
},
plugins: [
// 清除之前的dll文件
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns:['*.*']
}, {
root: path.join(__dirname, dllPath)
}),
// 设置环境变量
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: 'production'
}
}),
// manifest.json 描述动态链接库包含了哪些内容
new webpack.DllPlugin({
path: path.join(__dirname, dllPath, '[name]-manifest.json'),
// 保持与 output.library 中名称一致
name: '[name]_[hash]',
context: process.cwd()
})
]
}

同时,在 package.json 里需要新增一个 npm script: dll , 大概长这个样子:

1
2
3
4
5
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"dll": "webpack -p --progress --config ./dll.config.js"
}

通过实行一次(以后在依赖库没有变化的时候不用执行)npm run dll操作,即会自动在 './public/vendor/vendor-manifest.json' 下生成一份manifest文件,可用插件使用。

后续每次执行编译操作的时候,你可以惊喜发现稳定的依赖库,被打到了 'dist/vendor/vendor.dll.js''dist/vendor/vendor.dll.js.gz' 中:

miniAjax

可能细心的同学会发现,我们还用了一个 AddAssetHtmlPlugin 插件,用来把单独生产的 vendor.dll.js 文件放到了我们 index.html 中,这样优雅而快捷的方式是不是十分迷人。

开启 PWA

PWA(Progressive Web App) 是一种增加 web app的离线功能的技术,这里介绍如何在 CLI 3中使用:

首先我们需要安装 "@vue/cli-plugin-pwa" 插件,安装后,默认已经在 src/registerServiceWorker 中完成了配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* eslint-disable no-console */
import { register } from 'register-service-worker'
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}dist/service-worker.js`, {
ready () {
console.log(
'App is being served from cache by a service worker.\n' +
'For more details, visit https://goo.gl/AFskqB'
)
},
registered (registration) {
console.log('Service worker has been registered.')
console.log(registration)
},
cached () {
console.log('Content has been cached for offline use.')
},
updatefound () {
console.log('New content is downloading.')
},
updated () {
console.log('New content is available; please refresh.')
},
offline () {
console.log('No internet connection found. App is running in offline mode.')
},
error (error) {
console.error('Error during service worker registration:', error)
}
})
}

注意的是,我这里更改了注册使用路径 register(${process.env.BASE_URL}dist/service-worker.js, 与此同时需要在 vue.config.js中配置下 pwa 选项:

1
2
3
4
5
6
// vue.config.js
module.exports = {
pwa: {
manifestPath:'dist/manifest.json'
}
}

Post Build 处理

由于很多时候单页面应用项目都作为 Java Web 应用的war包部分代码嵌入,所以这个时候,就必须要针对后端引擎进行修改,由于该处理启发与Jenkins的 构建后处理,所以笔者取名为 Post Build,起作用就是在 npm run build 完成后进行后处理,例如 替换jsp模板代码index.html增加额外样式对模板文件做特殊处理等:

第一步,先新增 post-build script:

1
2
3
"scripts": {
"postbuild": "node ./build/postbuild.js",
},

第二步,在根目录下创建 /build/postbuild.js 文件,如我们需要在spring-boot的 thymleaf 模板引擎下处理下(包括了替换manifest文件、注入session变量等):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
'use strict'
const fs = require('fs');
const path = require('path');
const chalk = require('chalk')
const __dirDist = path.resolve(__dirname, "../dist");
//读取并替换
fs.readFile( __dirDist +'/service-worker.js','utf8',(err, data)=>{
if(err) {
console.log('error:' + err);
return;
};
let output = data.replace('/projectname/precache-manifest', '/projectname/dist/precache-manifest');
fs.writeFile(__dirDist + '/service-worker.js', output, 'utf8', function (err) {
if (err) console.log(err);
return;
});
});
const replaceString = '<script th:inline="javascript" type="text/javascript">';
//注入后端传入的session变量
const targetStr = '<script th:inline="javascript" type="text/javascript">window.user = JSON.parse([[${session.user}]]);'
fs.readFile( __dirDist +'/index.html','utf-8',(err, data)=>{
if(err) {
console.log('error:' + err);
return;
};
let result = data.replace(replaceString, targetStr);
fs.writeFile(__dirDist + '/index.html', result, 'utf8', function (err) {
if (err) console.log(err);
return;
});
});
//利用 chalk 输出模板替换完成的日期时间并打印到控制台
const finishedTime = new Date();
console.log(chalk.yellow(`模板替换完成:${finishedTime.getFullYear()}-${finishedTime.getMonth()+1}-${finishedTime.getDate()} ${finishedTime.getHours()}:${finishedTime.getMinutes()}:${finishedTime.getSeconds()}`));

总结

回顾对比 CLI 2CLI 3 可以说是带来了更先进的操作与思想:包括可视化的创建、容器与插件以及 约定大于配置的集中化配置思想等,如果你是要问是否有必要升级到 CLI 3,我可以很负责任的说有必要的! VUE CLI工具进化的同时需要一点时间代价去消化,但是其带来的增益是无可估量,当然笔者这份指引仅仅也是冰山一角,如果有错误,恳请斧正。