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

2,560 阅读7分钟

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:

"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 中增加一个包分析的构建脚本

"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下。

NODE_ENV = 'production'
IS_ANALYZ = 'analyz'

在package.json中统一管理配置

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

elsint配置

"eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "eslint:recommended"
    ],
    "rules": {},
    "parserOptions": {
      "parser": "babel-eslint"
    }
  }

postcss处理

"postcss": {
  "plugins": {
    "autoprefixer": {}
  }
}

浏览器兼容性

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

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

注册全局组件

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

// 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

// 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的目录,通过配合后端服务的静态资源路径使用:

// vue.config.js 
module.exports = {
  assetsDir: 'static'
}

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

lintOnSave

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

// vue.config.js 
module.exports = {
  lintOnSave: false
}

productionSourceMap

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

// vue.config.js 
module.exports = {
  lintOnSave: true
}

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

configureWebpack

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

// vue.config.js
module.exports = {
  configureWebpack: {
    plugins: [
      new MyAwesomeWebpackPlugin()
    ]
  }
}

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

css.loaderOptions

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

// vue.config.js
module.exports = {
  css: {
    loaderOptions: {
      // 给 sass-loader 传递选项
      sass: {
        // @/ 是 src/ 的别名
        data: `@import "~@/variables.scss";`
      }
    }
  }
}

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

chainWebpack

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

// 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 转发到对应后端服务上:

// 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是否支持并行构建,以提高构建速度。

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

构建流程优化

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

生产模式下启用 Gzip 压缩

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

// 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 插件实现的:

// 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文件中,下面是我的栗子:

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 , 大概长这个样子:

"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 中完成了配置:

/* 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 选项:

// 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:

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

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

'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工具进化的同时需要一点时间代价去消化,但是其带来的增益是无可估量,当然笔者这份指引仅仅也是冰山一角,如果有错误,恳请斧正。