Vue-cli3项目全面配置


2019/2/4 frontend vue

配置环境变量

通过在 package.json 里的 scripts 配置项中添加 --mode xxx 来选择不同环境。

NODE_ENV 和 BASE_URL 是两个特殊变量,在项目中始终可用。只有以 VUE_APP 开头的变量才会被 webpack.DefinePlugin 静态嵌入到客户端包中,代码中可以通过 process.env.VUE_APP_BASE_API 访问。

在项目根目录中新建 .env, .env.production, .env.analyz 等文件:

  • .env

    serve 默认的开发环境配置:

NODE_ENV = 'development'
BASE_URL = './'
VUE_APP_PUBLIC_PATH = './'
VUE_APP_API = 'https://test.staven630.com/api'
1
2
3
4
  • .env.production

    build 默认的生产环境配置:

NODE_ENV = 'production'
BASE_URL = 'https://prod.staven630.com/'
VUE_APP_PUBLIC_PATH = 'https://prod.oss.com/staven-blog'
VUE_APP_API = 'https://prod.staven630.com/api'

ACCESS_KEY_ID = 'xxxxxxxxxxxxx'
ACCESS_KEY_SECRET = 'xxxxxxxxxxxxx'
REGION = 'oss-cn-hangzhou'
BUCKET = 'staven-prod'
PREFIX = 'staven-blog'
1
2
3
4
5
6
7
8
9
10
  • .env.analyz

    自定义 build 环境配置:

NODE_ENV = 'production'
BASE_URL = 'https://prod.staven630.com/'
VUE_APP_PUBLIC_PATH = 'https://prod.oss.com/staven-blog'
VUE_APP_API = 'https://prod.staven630.com/api'

ACCESS_KEY_ID = 'xxxxxxxxxxxxx'
ACCESS_KEY_SECRET = 'xxxxxxxxxxxxx'
REGION = 'oss-cn-hangzhou'
BUCKET = 'staven-prod'
PREFIX = 'staven-blog'

IS_ANALYZE = true
1
2
3
4
5
6
7
8
9
10
11
12

修改 package.json

"scripts": {
  "serve": "vue-cli-service serve",
  "build": "vue-cli-service build",
  "analyz": "vue-cli-service build --mode analyze",
  "lint": "vue-cli-service lint"
}
1
2
3
4
5
6

使用环境变量:

<template>
  <div class="home">
    <!-- template中使用环境变量 -->
    {{ api }}
  </div>
</template>

<script>
  export default {
    name: 'home',
    data() {
      return {
        api: process.env.VUE_APP_API
      }
    },
    mounted() {
      // js代码中使用环境变量
      console.log('BASE_URL', process.env.BASE_URL)
      console.log('VUE_APP_API', process.env.VUE_APP_API)
    }
  }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

配置 Proxy 代理

假设 mock 接口为 https://www.easy-mock.com/mock/5bc75b55dc36971c160cad1b/sheets/1

module.exports = {
  devServer: {
    proxy: {
      '/api': {
        // 目标代理接口地址
        target: 'https://www.easy-mock.com/mock/5bc75b55dc36971c160cad1b/sheets',
        secure: false,
        // 开启代理,在本地创建一个虚拟服务端
        changeOrigin: true,
        // 是否启用websockets
        ws: true,
        pathRewrite: {
          '^/api': '/'
        }
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

配置图片压缩

npm i -D image-webpack-loader
1
module.exports = {
  chainWebpack: config => {
    config.module
      .rule('images')
      .use('image-webpack-loader')
      .loader('image-webpack-loader')
      .options({
        mozjpeg: { progressive: true, quality: 65 },
        optipng: { enabled: false },
        pngquant: { quality: '65-90', speed: 4 },
        gifsicle: { interlaced: false },
        webp: { quality: 75 }
      })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

配置雪碧图

默认 src/assets/icons 中存放需要生成雪碧图的 PNG 格式图片。首次运行 npm run serve/build 会生成雪碧图,并在根目录生成 icons.json 文件。再次运行命令时,会对比 icons 目录下图片文件与 icons.json 的匹配关系,确定是否需要再次执行 webpack-spritesmith 插件。

npm i -D webpack-spritesmith
1
const SpritesmithPlugin = require('webpack-spritesmith')
const path = require('path')
const fs = require('fs')

let has_sprite = true

try {
  let result = fs.readFileSync(path.resolve(__dirname, './icons.json'), 'utf8')
  result = JSON.parse(result)
  const files = fs.readdirSync(path.resolve(__dirname, './src/assets/icons'))
  if (files && files.length) {
    has_sprite = files.some(item => {
      let filename = item.toLocaleLowerCase().replace(/_/g, '-')
      return !result[filename]
    })
      ? true
      : false
  } else {
    has_sprite = false
  }
} catch (error) {}

// 雪碧图样式处理模板
const SpritesmithTemplate = function(data) {
  // pc
  let icons = {}
  let tpl = `.ico { 
  display: inline-block; 
  background-image: url(${data.sprites[0].image}); 
  background-size: ${data.spritesheet.width}px ${data.spritesheet.height}px; 
}`

  data.sprites.forEach(sprite => {
    const name = '' + sprite.name.toLocaleLowerCase().replace(/_/g, '-')
    icons[`${name}.png`] = true
    tpl = `${tpl} 
.ico-${name}{
  width: ${sprite.width}px; 
  height: ${sprite.height}px; 
  background-position: ${sprite.offset_x}px ${sprite.offset_y}px;
}
`
  })

  fs.writeFile(path.resolve(__dirname, './icons.json'), JSON.stringify(icons, null, 2), (err, data) => {})

  return tpl
}

module.exports = {
  configureWebpack: config => {
    const plugins = []
    if (has_sprite) {
      plugins.push(
        new SpritesmithPlugin({
          src: {
            cwd: path.resolve(__dirname, './src/assets/icons/'), // 图标根路径
            glob: '**/*.png' // 匹配任意 png 图标
          },
          target: {
            image: path.resolve(__dirname, './src/assets/images/sprites.png'), // 生成雪碧图目标路径与名称
            // 设置生成CSS背景及其定位的文件或方式
            css: [
              [
                path.resolve(__dirname, './src/assets/scss/sprites.scss'),
                {
                  format: 'function_based_template'
                }
              ]
            ]
          },
          customTemplates: {
            function_based_template: SpritesmithTemplate
          },
          apiOptions: {
            cssImageRef: '../images/sprites.png' // css文件中引用雪碧图的相对位置路径配置
          },
          spritesmithOptions: {
            padding: 2
          }
        })
      )
    }
    config.plugins = [...config.plugins, ...plugins]
  }
}
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86

配置 CDN 资源

防止将某些 import 的包(package)打包到 bundle 中,而是在运行时 runtime 再去从外部获取这些扩展依赖:

module.exports = {
  configureWebpack: config => {
    config.externals = {
      vue: 'Vue',
      vuex: 'Vuex',
      axios: 'axios',
      'element-ui': 'ELEMENT',
      'vue-router': 'VueRouter'
    }
  },
  chainWebpack: config => {
    const cdn = {
      css: ['//unpkg.com/[email protected]/lib/theme-chalk/index.css'],
      js: [
        '//unpkg.com/[email protected]/dist/vue.min.js',
        '//unpkg.com/[email protected]/dist/vuex.min.js',
        '//unpkg.com/[email protected]/dist/axios.min.js',
        '//unpkg.com/[email protected]/lib/index.js',
        '//unpkg.com/[email protected]/dist/vue-router.min.js'
      ]
    }

    // html中添加cdn
    config.plugin('html').tap(args => {
      args[0].cdn = cdn
      return args
    })
  }
}
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

在 html 中添加:

<!-- 使用CDN的CSS文件 -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="external nofollow" rel="preload" as="style" />
<% } %>
<!-- 使用CDN的CSS文件 -->
<% for (var css of htmlWebpackPlugin.options.cdn.css) { %>
<link href="<%=css%>" rel="stylesheet" as="style" />
<% } %>

<!-- 使用CDN的JS文件 -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<link href="<%= htmlWebpackPlugin.options.cdn.js[i] %>" rel="external nofollow" rel="preload" as="script" />
<% } %>
<!-- 使用CDN的JS文件 -->
<% for (var js of htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%=js%>"></script>
<% } %>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

配置模块打包

const isPro = process.env.NODE_ENV === 'production'

module.exports = {
  chainWebpack: config => {
    if (isPro) {
      config.optimization = {
        splitChunks: {
          cacheGroups: {
            libs: {
              name: 'chunk-libs',
              test: /[\\/]node_modules[\\/]/,
              priority: 10,
              chunks: 'initial'
            },
            elementUI: {
              name: 'chunk-elementUI',
              priority: 20,
              test: /[\\/]node_modules[\\/]element-ui[\\/]/,
              chunks: 'all'
            }
          }
        }
      }
    }
  }
}
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

配置页面预渲染

npm i -D prerender-spa-plugin
1
const PrerenderSpaPlugin = require('prerender-spa-plugin')
const path = require('path')
const resolve = dir => path.join(__dirname, dir)
const isPro = process.env.NODE_ENV === 'production'

module.exports = {
  configureWebpack: config => {
    const plugins = []
    if (isPro) {
      plugins.push(
        new PrerenderSpaPlugin({
          staticDir: resolve('dist'),
          routes: ['/'],
          postProcess(ctx) {
            ctx.route = ctx.originalRoute
            ctx.html = ctx.html.split(/>[\s]+</gim).join('><')
            if (ctx.route.endsWith('.html')) {
              ctx.outputPath = path.join(__dirname, 'dist', ctx.route)
            }
            return ctx
          },
          minify: {
            collapseBooleanAttributes: true,
            collapseWhitespace: true,
            decodeEntities: true,
            keepClosingSlash: true,
            sortAttributes: true
          },
          renderer: new PrerenderSpaPlugin.PuppeteerRenderer({
            // 需要注入一个值,这样就可以检测页面当前是否是预渲染的
            inject: {},
            headless: false,
            // 视图组件是在API请求获取所有必要数据后呈现的,因此我们在dom中存在“data view”属性后创建页面快照
            renderAfterDocumentEvent: 'render-event'
          })
        })
      )
    }
    config.plugins = [...config.plugins, ...plugins]
  }
}
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

mounted() 中添加 document.dispatchEvent(new Event('render-event'))

new Vue({
  router,
  store,
  render: h => h(App),
  mounted() {
    document.dispatchEvent(new Event('render-event'))
  }
}).$mount('#app')
1
2
3
4
5
6
7
8

修复热更新失效

module.exports = {
  chainWebpack: config => {
    // 修复HMR
    config.resolve.symlinks(true)
  }
}
1
2
3
4
5
6

修复路由懒加载

module.exports = {
  chainWebpack: config => {
    config.plugin('html').tap(args => {
      args[0].chunksSortMode = 'none'
      return args
    })
  }
}
1
2
3
4
5
6
7
8

添加别名 alias

const path = require('path')
const resolve = dir => path.join(__dirname, dir)
const isPro = process.env.NODE_ENV === 'production'

module.exports = {
  chainWebpack: config => {
    // 添加别名
    config.resolve.alias
      .set('vue$', 'vue/dist/vue.esm.js')
      .set('@', resolve('src'))
      .set('@assets', resolve('src/assets'))
      .set('@components', resolve('src/components'))
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

添加 IE 兼容

npm i --save @babel/polyfill
1

main.js 中添加:

import '@babel/polyfill'
1

配置 babel.config.js

const plugins = []

module.exports = {
  presets: [['@vue/app', { useBuiltIns: 'entry' }]],
  plugins: plugins
}
1
2
3
4
5
6

添加打包分析

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

module.exports = {
  chainWebpack: config => {
    // 打包分析
    if (process.env.IS_ANALYZ) {
      config.plugin('webpack-report').use(BundleAnalyzerPlugin, [
        {
          analyzerMode: 'static'
        }
      ])
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

添加 .env.analyz 文件:

NODE_ENV = 'production'
IS_ANALYZE = true
1
2

package.jsonscripts 中添加:

"analyz": "vue-cli-service build --mode analyze"
1

添加全局 Sass

可以通过在 main.jsVue.prototype.$src = process.env.VUE_APP_PUBLIC_PATH; 挂载环境变量中的配置信息,然后在 js 中使用 $src 访问。

css 中可以使用注入 sass 变量访问环境变量中的配置信息:

module.exports = {
  css: {
    modules: false,
    extract: isPro,
    sourceMap: false,
    loaderOptions: {
      sass: {
        // 向全局sass样式传入共享的全局变量
        data: `@import "~assets/scss/variables.scss";
          $src: "${process.env.VUE_APP_PUBLIC_PATH}";`
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

在 scss 中引用:

.home {
  background: url($src+'/images/500.png');
}
1
2
3

添加全局 Stylus

npm i -D style-resources-loader
1
const path = require('path')
const resolve = dir => path.resolve(__dirname, dir)
const addStylusResource = rule => {
  rule
    .use('style-resouce')
    .loader('style-resources-loader')
    .options({
      patterns: [resolve('src/assets/stylus/variable.styl')]
    })
}
module.exports = {
  chainWebpack: config => {
    const types = ['vue-modules', 'vue', 'normal-modules', 'normal']
    types.forEach(type => addStylusResource(config.module.rule('stylus').oneOf(type)))
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

添加文件上传

开启文件上传 ali oss,需要将 publicPath 改成 ali oss 资源 url 前缀,也就是修改 VUE_APP_PUBLIC_PATH:

npm i -D webpack-oss
1
const AliOssPlugin = require('webpack-oss')

module.exports = {
  configureWebpack: config => {
    if (isPro) {
      const plugins = []
      // 上传文件到oss
      if (
        process.env.ACCESS_KEY_ID ||
        process.env.ACCESS_KEY_SECRET ||
        process.env.REGION ||
        process.env.BUCKET ||
        process.env.PREFIX
      ) {
        plugins.push(
          new AliOssPlugin({
            accessKeyId: process.env.ACCESS_KEY_ID,
            accessKeySecret: process.env.ACCESS_KEY_SECRET,
            region: process.env.REGION,
            bucket: process.env.BUCKET,
            prefix: process.env.PREFIX,
            exclude: /.*\.html$/,
            deleteAll: false
          })
        )
      }
      config.plugins = [...config.plugins, ...plugins]
    }
  }
}
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

添加 Gzip 压缩

npm i --D compression-webpack-plugin
1
const isPro = process.env.NODE_ENV === 'production'
const productionGzipExtensions = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i
const CompressionWebpackPlugin = require('compression-webpack-plugin')

module.exports = {
  configureWebpack: config => {
    const plugins = []
    if (isPro) {
      plugins.push(
        new CompressionWebpackPlugin({
          filename: '[path].gz[query]',
          algorithm: 'gzip',
          test: productionGzipExtensions,
          threshold: 10240,
          minRatio: 0.8
        })
      )
    }
    config.plugins = [...config.plugins, ...plugins]
  },
  chainWebpack: config => {
    if (isPro) {
      config
        .plugin('compression')
        .use(CompressionWebpackPlugin, {
          asset: '[path].gz[query]',
          algorithm: 'gzip',
          test: productionGzipExtensions,
          threshold: 10240,
          minRatio: 0.8,
          cache: true
        })
        .tap(args => {})
    }
  }
}
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

如果服务端使用的是 nginx 服务器的话,需要配置开启 gzip 才能支持:

gzip on;
gzip_static on;
gzip_min_length 1024;
gzip_buffers 4 16k;
gzip_comp_level 2;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php application/vnd.ms-fontobject font/ttf font/opentype font/x-woff image/svg+xml;
gzip_vary off;
gzip_disable "MSIE [1-6]\.";
1
2
3
4
5
6
7
8

除了 gzip 压缩,还有体验更好的 Zopfli 压缩

npm i --save-dev @gfx/zopfli brotli-webpack-plugin
1
const CompressionWebpackPlugin = require('compression-webpack-plugin')
const zopfli = require('@gfx/zopfli')
const BrotliPlugin = require('brotli-webpack-plugin')

const isPro = process.env.NODE_ENV === 'production'
const productionGzipExtensions = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i

module.exports = {
  configureWebpack: config => {
    const plugins = []
    if (isPro) {
      plugins.push(
        new CompressionWebpackPlugin({
          algorithm(input, compressionOptions, callback) {
            return zopfli.gzip(input, compressionOptions, callback)
          },
          compressionOptions: {
            numiterations: 15
          },
          minRatio: 0.99,
          test: productionGzipExtensions
        })
      )
      plugins.push(
        new BrotliPlugin({
          test: productionGzipExtensions,
          minRatio: 0.99
        })
      )
    }
    config.plugins = [...config.plugins, ...plugins]
  }
}
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

移除无效 CSS

方案一:@fullhuman/postcss-purgecss

npm i -D postcss-import @fullhuman/postcss-purgecss
1
const autoprefixer = require('autoprefixer')
const postcssImport = require('postcss-import')
const purgecss = require('@fullhuman/postcss-purgecss')
const isPro = process.env.NODE_ENV === 'production'
let plugins = []
if (isPro) {
  plugins.push(postcssImport)
  plugins.push(
    purgecss({
      content: [
        './layouts/**/*.vue',
        './components/**/*.vue',
        './pages/**/*.vue'
      ],
      extractors: [
        {
          extractor: class Extractor {
            static extract(content) {
              const validSection = content.replace(
                /<style([\s\S]*?)<\/style>+/gim,
                ''
              )
              return validSection..match(/[A-Za-z0-9-_/:]*[A-Za-z0-9-_/]+/g) || []
            }
          },
          extensions: ['html', 'vue']
        }
      ],
      whitelist: ['html', 'body'],
      whitelistPatterns: [/el-.*/, /-(leave|enter|appear)(|-(to|from|active))$/, /^(?!cursor-move).+-move$/, /^router-link(|-exact)-active$/],
      whitelistPatternsChildren: [/^token/, /^pre/, /^code/]
    })
  )
}
module.exports = {
  plugins: [...plugins, autoprefixer]
}
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

方案二:purgecss-webpack-plugin

npm i -D glob-all purgecss-webpack-plugin
1
const path = require("path");
const glob = require("glob-all");
const PurgecssPlugin = require("purgecss-webpack-plugin");
const resolve = dir => path.join(__dirname, dir);
const isPro = ["production", "prod"].includes(process.env.NODE_ENV);

module.exports = {
  configureWebpack: config => {
    const plugins = [];
    if (isPro) {
      plugins.push(
        new PurgecssPlugin({
          paths: glob.sync([resolve("./**/*.vue")]),
          extractors: [
            {
              extractor: class Extractor {
                static extract(content) {
                  const validSection = content.replace(
                    /<style([\s\S]*?)<\/style>+/gim,
                    ""
                  );
                  return validSection..match(/[A-Za-z0-9-_/:]*[A-Za-z0-9-_/]+/g) || []
                }
              },
              extensions: ["html", "vue"]
            }
          ],
          whitelist: ["html", "body"],
          whitelistPatterns: [/el-.*/, /-(leave|enter|appear)(|-(to|from|active))$/, /^(?!cursor-move).+-move$/, /^router-link(|-exact)-active$/],
          whitelistPatternsChildren: [/^token/, /^pre/, /^code/]
        })
      );
    }
    config.plugins = [...config.plugins, ...plugins];
  }
};
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

移除 console.log

方法一:使用 babel-plugin-transform-remove-console

npm i --D babel-plugin-transform-remove-console
1

babel.config.js 中配置:

const isPro = process.env.NODE_ENV === 'production'

const plugins = []
if (isPro) {
  plugins.push('transform-remove-console')
}

module.exports = {
  presets: ['@vue/app', { useBuiltIns: 'entry' }],
  plugins
}
1
2
3
4
5
6
7
8
9
10
11

方法二:

const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
  configureWebpack: config => {
    if (isPro) {
      const plugins = []
      plugins.push(
        new UglifyJsPlugin({
          uglifyOptions: {
            warnings: false,
            compress: {
              drop_console: true,
              drop_debugger: true,
              pure_funcs: ['console.log']
            }
          },
          sourceMap: false,
          parallel: true
        })
      )
      config.plugins = [...config.plugins, ...plugins]
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

如果使用 uglifyjs-webpack-plugin 会报错,可能存在 node_modules 中有些依赖需要 babel 转译。

vue-clitranspileDependencies 配置默认为 [], babel-loader 会忽略所有 node_modules 中的文件。如果你想要通过 Babel 显式转译一个依赖,可以在这个选项中列出来。配置需要转译的第三方库。

完整配置代码

const path = require('path')
const glob = require('glob-all')
const AliOssPlugin = require('webpack-oss')
const PurgecssPlugin = require('purgecss-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const PrerenderSpaPlugin = require('prerender-spa-plugin')
const CompressionWebpackPlugin = require('compression-webpack-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin

const resolve = dir => path.join(__dirname, dir)
const isPro = process.env.NODE_ENV === 'production'
const productionGzipExtensions = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i

const addStylusResource = rule => {
  rule
    .use('style-resouce')
    .loader('style-resources-loader')
    .options({
      patterns: [resolve('src/assets/stylus/variable.styl')]
    })
}

module.exports = {
  publicPath: process.env.VUE_APP_PUBLIC_PATH,
  outputDir: process.env.outputDir || 'dist',
  assetsDir: 'static',
  configureWebpack: config => {
    const plugins = []

    if (isPro) {
      // 移除无效CSS
      plugins.push(
        new PurgecssPlugin({
          paths: glob.sync([resolve('./**/*.vue')]),
          extractors: [
            {
              extractor: class Extractor {
                static extract(content) {
                  const validSection = content.replace(/<style([\s\S]*?)<\/style>+/gim, '')
                  return validSection.match(/[A-Za-z0-9-_:/]+/g) || []
                }
              },
              extensions: ['html', 'vue']
            }
          ],
          whitelist: ['html', 'body'],
          whitelistPatterns: [/el-.*/],
          whitelistPatternsChildren: [/^token/, /^pre/, /^code/]
        })
      )
      // 移除打印
      plugins.push(
        new UglifyJsPlugin({
          uglifyOptions: {
            compress: {
              warnings: false,
              drop_console: true,
              drop_debugger: false,
              pure_funcs: ['console.log']
            }
          },
          sourceMap: false,
          parallel: true
        })
      )
      // 文件压缩
      plugins.push(
        new CompressionWebpackPlugin({
          filename: '[path].gz[query]',
          algorithm: 'gzip',
          test: productionGzipExtensions,
          threshold: 10240,
          minRatio: 0.8
        })
      )
      // 页面预加载
      plugins.push(
        new PrerenderSpaPlugin({
          staticDir: resolve('dist'),
          routes: ['/'],
          postProcess(ctx) {
            ctx.route = ctx.originalRoute
            ctx.html = ctx.html.split(/>[\s]+</gim).join('><')
            if (ctx.route.endsWith('.html')) {
              ctx.outputPath = path.join(__dirname, 'dist', ctx.route)
            }
            return ctx
          },
          minify: {
            collapseBooleanAttributes: true,
            collapseWhitespace: true,
            decodeEntities: true,
            keepClosingSlash: true,
            sortAttributes: true
          },
          renderer: new PrerenderSpaPlugin.PuppeteerRenderer({
            // 需要注入一个值,这样就可以检测页面当前是否是预渲染的
            inject: {},
            headless: false,
            // 视图组件是在API请求获取所有必要数据后呈现的,因此我们在dom中存在“data view”属性后创建页面快照
            renderAfterDocumentEvent: 'render-event'
          })
        })
      )
      // 文件上传
      plugins.push(
        new AliOssPlugin({
          accessKeyId: process.env.ACCESS_KEY_ID,
          accessKeySecret: process.env.ACCESS_KEY_SECRET,
          region: process.env.REGION,
          bucket: process.env.BUCKET,
          prefix: process.env.PREFIX,
          exclude: /.*\.html$/,
          deleteAll: false
        })
      )
    }
    config.externals = {
      vue: 'Vue',
      'element-ui': 'ELEMENT',
      'vue-router': 'VueRouter',
      vuex: 'Vuex',
      axios: 'axios'
    }
    config.plugins = [...config.plugins, ...plugins]
  },
  chainWebpack: config => {
    // CDN资源
    const cdn = {
      css: ['//unpkg.com/[email protected]/lib/theme-chalk/index.css'],
      js: [
        '//unpkg.com/[email protected]/dist/vue.min.js',
        '//unpkg.com/[email protected]/dist/vue-router.min.js',
        '//unpkg.com/[email protected]/dist/vuex.min.js',
        '//unpkg.com/[email protected]/dist/axios.min.js',
        '//unpkg.com/[email protected]/lib/index.js'
      ]
    }
    // 修复HMR
    config.resolve.symlinks(true)
    // 修复路由懒加载
    config.plugin('html').tap(args => {
      args[0].chunksSortMode = 'none'
      // html中添加cdn
      // args[0].cdn = cdn;
      return args
    })
    // 添加别名
    config.resolve.alias
      .set('vue$', 'vue/dist/vue.esm.js')
      .set('@', resolve('src'))
      .set('@assets', resolve('src/assets'))
      .set('@components', resolve('src/components'))
    // 图片压缩
    config.module
      .rule("images")
      .use("image-webpack-loader")
      .loader("image-webpack-loader")
      .options({
        mozjpeg: { progressive: true, quality: 65 },
        optipng: { enabled: false },
        pngquant: { quality: "65-90", speed: 4 },
        gifsicle: { interlaced: false },
        webp: { quality: 75 }
      })

    const types = ["vue-modules", "vue", "normal-modules", "normal"];
    types.forEach(type =>
      addStylusResource(config.module.rule("stylus").oneOf(type))
    )
    // 打包分析
    if (process.env.IS_ANALYZ) {
      config.plugin('webpack-report').use(BundleAnalyzerPlugin, [
        {
          analyzerMode: 'static'
        }
      ])
    }
    if (isPro) {
      // 打包模块
      config.optimization.splitChunks({
        cacheGroups: {
          libs: {
            name: 'chunk-libs',
            chunks: 'initial',
            priority: 10,
            test: /[\\/]node_modules[\\/]/
          },
          elementUI: {
            name: 'chunk-elementUI',
            chunks: 'all'
            priority: 20,
            test: /[\\/]node_modules[\\/]element-ui[\\/]/
          }
        }
      })
    }
    return config
  },
  css: {
    modules: false,
    extract: isPro,
    sourceMap: false,
    loaderOptions: {
      sass: {
        data: `@import "~assets/scss/variables.scss";
        $src: "${process.env.VUE_APP_PUBLIC_PATH}";`
      }
    }
  },
  transpileDependencies: [],
  lintOnSave: false,
  runtimeCompiler: true,
  productionSourceMap: !isPro,
  parallel: require('os').cpus().length > 1,
  pwa: {},
  devServer: {
    overlay: {
      warnings: false,
      errors: true
    },
    open: false,
    host: "localhost",
    port: "8080",
    https: false,
    hotOnly: false,
    proxy: {
      '/api': {
        target: 'https://www.easy-mock.com/mock/5bc75b55dc36971c160cad1b/sheets',
        secure: false,
        changeOrigin: true,
        ws: true,
        pathRewrite: {
          '^/api': '/'
        }
      }
    }
  }
}
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
上次更新: 10/24/2019, 12:39:39 AM