Skip to content
🌈A boilerplate easy to start vue-cli3 project.
JavaScript Vue HTML
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
public
src
tests/unit
.env
.env.analyz
.env.production
.gitignore
README.md
babel.config.js
dll.config.js
package-lock.json
package.json
vue.config.js

README.md

easy-cli3-boilerplate

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

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

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

You can’t perform that action at this time.