生产环境优化

前端工程化模块化开发Webpack

# webpack开发体验解决却带来了生产环境的新问题

在我们提升开发体验的时候,我们的webpack打包也越来越臃肿,因为webpack为了实现那些特性,会在打包结果中添加额外的代码实现各自的功能(例如:source map和HMR)。

这些代码对于生产环境来讲是冗余的。生产环境注重运行效率,开发环境中注重开发效率,解决这些问题webpack4提出了mode(模式)的用法。

webpack也建议我们为不同的工作环境创建不同的配置,便于让我们的打包结果适用于不同的环境

# 生产环境值得优化的地方 —— 不同环境下的配置

# M1:配置文件根据环境不同导出不同配置

  1. webpack.config.js中编写
/**
 * webpack配置支持导出一个函数,里面return的对象就是我们的配置对象
 * @param {string} env cli传递的环境名参数
 * @param {*} argv 运行中传递的所有参数
 */
module.exports = (env, argv) => {
  // 开发模式下的选项配置
  const config = {
    entry: './src/main.js',
    output: {
      filename: 'bundle.js',
      path: path.join(__dirname, 'output')
    },
    devtool: 'eval',
    devServer: {
      ...
    },
    mode: 'none',
    module: {
      ...
    },
    plugins: [
      ...
    ]
  }

  // 判断是生产环境
  if(env === 'production') {
    // 模式修改为生产环境
    config.mode = 'production'
    // 禁用source map
    config.devtool = false
    // 上线打包的时候用的插件
    config.plugins = {
      ...config.plugins,
      new CleanWebpackPlugin(),
      new CopyWebpackPlugin(['public'])
    }
  }

  return config
}
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
  1. 在命令行中运行npm run buildwebpack)不加任何参数可以看到是正常打包
  2. 在package.json中添加
"scripts": {
    "prod": "webpack --env production"
  }
1
2
3

命令行中运行npm run prodwebpack --env production)可以返回生产模式下的配置

# M2:一个环境对应一个配置文件

上面的M1方法只适用于中小型项目,由于项目的复杂,我们的配置也会跟着复杂。对于大型项目还是推荐不同环境对应不同配置文件。

一般项目中会有三个配置文件,两个是用于不同环境的,还有一个是公共配置。

  1. 创建webpack.common.js用于放公共配置
const path = require('path')

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'output')
  },
  module: {
    rules: [{
      test: /\.js/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    },{
      test: /\.css$/,
      use: [
        'style-loader',
        'css-loader' 
      ]
    },{
      test:/\.png$/,
      use: {
        loader:'url-loader',
        options: {
          limit: 10 * 1024
        }
      }
    },{
      test: /\.html$/,
      use: {
        loader: 'html-loader',
        options: {
          attributes: {
            list: [
              {
                tag: 'img',
                attribute: 'src',
                type: 'src'
              },
              {
                tag: 'a',
                attribute: 'href',
                type: 'src'
              }
            ]
          }
        }
      }
    },{
      test: /\.md$/,
      use: [
        'html-loader',
        './markdown-loader'
      ]
    }]
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'webpack plugin sample',
      hello: 'hello~',
      meta: {
        viewport: 'width=device-width'
      },
      template: './src/index.ejs'
    }),
    new HtmlWebpackPlugin({
      filename: 'about.html',
      title: 'About'
    })
  ]
}
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
  1. 创建webpack.dev.js用于开发环境配置
// 先导入公共的配置模块
const common = require('./webpack.common')
const webpack = require('webpack')
const { merge } = require('webpack-merge')

module.exports = merge(common, {
  mode: 'development',
  devtool: 'cheap-eval-module-source-map',
  devServer: {
    contentBase: './public',
    proxy: {
      '/api': {
        target:'https://api.github.com',
        pathRewrite: {
          '^/api': ''
        },
        changeOrigin: true
      }
    },
    hotonly: true
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin()
  ]
})
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
  1. 创建webpack.prod.js用于生产环境配置
  2. 不在webpack.prod.js中使用Object.assign进行合并的原因是因为plugins里面的东西不需要完全覆盖,所以安装社区中官方提供的npm i webpack-merge --save-dev到依赖中,并且使用
// 先导入公共的配置模块
const common = require('./webpack.common')
// 使用webpack-merge中的merge函数
const { merge } = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = merge(common, {
  mode: 'production',
  devtool: false,
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin({
      patterns: [{
        from: 'public'
      }]
    })
  ]
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  1. 在命令行终端执行webpack --config webpack.prod.js来指定我们运行的配置文件

# 其他优化功能

webpack4的production模式下面内部开启了很多优化功能,对于使用者而言这种开箱即用的方式是很方便的,但是对于学习者而言会忽略掉很多东西,以致于出现问题之后无从下手。

如果要深入了解webpack的使用,需要单独了解每个配置背后的作用,这里先学习几个主要的优化配置,了解一下webpack是如何优化打包结果的。

# DefinePlugin

definePlugin是webpack内置插件,是为代码注入全局成员的,production模式下这个插件会默认启用起来,并且在代码中注入了process.env.NODE_ENV的常量,很多第三方模块都是通过这个常量判断当前的运行环境,从而去决定是否打印日志等操作。

  1. webpack.config.js中写
const webpack = require('webpack')

plugins: [
    // 接收一个对象,每一个键值都会注入到代码中,里面可以自定义常数变量,还可以根据环境自己定义
    new webpack.DefinePlugin({
        // 值不是字符串,而是JS代码片段,所以应该在内容里面自己加双引号说明这是一个字符串
        API_BASE_URL: '"https://api.example.com"',
    })
]
1
2
3
4
5
6
7
8
9

小技巧

如果我们注入的是一个值的话,我们可以用JSON.stringify转换成一个表示值的代码片段,这样就不容易错了

API_BASE_URL: JSON.stringify('https://api.example.com')

  1. 在main.js中使用
console.log(API_BASE_URL)
1
  1. 在命令行中打包npm run build可以看到bundle.js中解析出了对应的代码。

# Tree-shaking

字面意思就是摇树,摇掉代码中未引用代码(dead-code)。webpack可以自动检测出未引用的代码然后移除掉。其并不是某个功能选项,而是一组功能搭配使用后的优化效果,这种功能会在production模式下自动开启

举个例子:

代码中写下列代码然后生产环境打包,在bundle.js中会找不到显示的未引用部分的代码。

// index.js
import { Button } from './components'

document.body.appendChild(Button())


// components.js
export const Button = () => {
  return document.createElement('button')
  // 未引用部分
  console.log('dead-code')
}
// 未引用部分
export const Link = () => {
  return document.createElement('a')
}
// 未引用部分
export const Heading = level => {
  return document.createElement('h' + level)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 在其他模式下如何开启Tree-shaking?

因为目前官方对tree-shaking的介绍有些混乱,所以我们通过在其他模式下一步步开启Tree-shaking,了解其工作过程和其他的优化功能。

  • usedExports 负责标记【枯树叶】
  • minimize 负责【摇掉】它们
  1. 下载模板 webapck-tree-shaking-temp (opens new window),命令行中执行npm run build打包可以在bundle.js中找到模块有对link函数等进行模块导出

image

  1. 在webpack.config.js中添加
// webpack.config.js
module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  // 集中配置webpack内部优化功能
  optimization: {
    // 只导出外部使用了的成员
    usedExports: true
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

设置之后命令行执行npm run build发现bundle.js中不再对模块进行导出,但是还是有定义的link等函数

image

  1. 再对webpack.config.js进行添加
module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  optimization: {
    usedExports: true,
    // 进行压缩,未被引用的部分会被弃用
    minimize: true
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

命令行执行npm run build发现bundle.js中的代码已经找不到了未引用的link等函数,完美~

image

# Tree Shaking & Babel

因为早期webpack发展很快,所以我们找到的文档可能并不适用于当前版本,很多文档说:

# 如果我们使用babel-loader,就会导致tree-shaking失效?!

对于这现象的解释: Tree Shaking前提是使用ES Modules去组织我们的代码,即我们交给webpack打包的代码必须使用ESM。 webpack在打包前先将文件交给不同的loader进行处理,最后将处理后的结果打包到一起,我们在处理ES6新特性的时候会用babel-loader,babel-loader在处理的使用会把ES Modules转化成CommonJS,最后会不会失效这取决于我们有没有使用转换ES Modules的插件(@babel/preset-env就使用了)

# 验证
  1. 安装npm i babel-loader @babel/core @babel/preset-env --save-dev,在webpack.config.js中引用
module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  module: {
    rules: [{
      test: /\.js/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    }]
  },
  optimization: {
    usedExports: true
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

发现tree-shaking并没有失效

image

# 原因分析

因为在最近版本的babel-loader中已经自动关闭了ES Modules转换的插件。

在babel中可以看到其支持ESModules

image

在preset-env中自动禁用了的转换

image

所以webpack在打包的时候得到的还是ESModules的代码。

# 强制开启插件
module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  module: {
    rules: [{
      test: /\.js/,
      use: {
        loader: 'babel-loader',
        options: {
          // 这里是数组套数组,注意!
          presets: [
            // 第二个参数是一个对象,默认是auto,根据环境判断是否支持ES Modules
            // 指定modules是commonjs,强制只用ESM的转换插件
            // 如果不确定的话,那么将modules强制设定为false,就绝对不会开启ES Modules转换的插件
            ['@babel/preset-env', { modules: 'commonjs' }]
          ]
        }
      }
    }]
  },
  optimization: {
    usedExports: true
  }
}
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

这时候打包看确实tree-shaking失效了。

image

# concatenateModules

concatenateModules —— 合并模块函数,这个特性又被成为Scope Hoisting(作用域提升),是webpack3中添加的一个特性。

普通的打包结果是将我们的模块最终放在一个单独的函数中,这样的话如果我们的模块很多,就意味着我们在输出结果中有很多的模块函数。使用concatenateModules可以将所有模块合并到一个函数中,既提升了运行效率,又减少了代码的体积。

image

module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  optimization: {
    usedExports: true,
    // 开启合并模块函数特性
    concatenateModules: true
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

这样所有的模块都合并到一个函数中。

image

# sideEffects

webpack4中新增了一个新特性,允许我们通过配置的方式标识我们的代码是否有副作用,从而为tree-shaking提供更大的压缩空间

副作用:模块执行时除了导出成员之外所做的事情。

ps: sideEffects一般用于npm包开发标记是否有副作用。因为官网中把sideEffects和tree-shaking混到了一起,很多人误认为他们是因果关系,其实他们没有什么关系。

# 遇到的问题

下载模板文件 webpack-sideEffects-temp (opens new window) ,src/components的index.js文件把所有的模块都引入了,那些模块除了引用并没有别的操作,但是我们在src/index.js中只想引用Button,却把其他文件也引用进来了,命令行npm run build打包文件中可以看到

image

我们需要的是我们虽然有引入模块但是并没有别的操作,我们就可以把这些模块去掉

# 解决此类问题

  1. 设置weboack.config.js开启功能
module.exports = {
  mode: 'none',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js'
  },
  // 集中配置webpack内部优化功能
  optimization: {
    // 此特性在production模式下会自动开启
    // 会先检查当前代码所属的package.json中有没有sideEffects标识,以此来判断这个模块是否有副作用。如果没有副作用,那些没用的模块就不会打包
    sideEffects: true,
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. 在package.json中配置标识
{
  // 标识当前项目的代码,都没有副作用,一旦没有引用的模块没有副作用,就会被移除掉
  "sideEffects": false
}
1
2
3
4
  1. 重新命令行打包npm run build可以看到没有引用的代码不会打包进去

image

# 如果文件有副作用怎么办?

使用sideEffcts的前提是确保代码真的没有副作用,否则在打包的时候就会误删掉有副作用的代码。

模板中有一个extend.js的代码,这段代码就是副作用代码。

// 为 Number 的原型添加一个扩展方法
Number.prototype.pad = function (size) {
  // 将数字转为字符串 => '8'
  let result = this + ''
  // 在数字前补指定个数的 0 => '008'
  while (result.length < size) {
    result = '0' + result
  }
  return result
}

1
2
3
4
5
6
7
8
9
10
11

在index.js中直接引用

// 副作用模块
import './extend'

console.log((8).pad(3))
1
2
3
4

这个时候我们还运行没有副作用的话,就不会被打包进去。同样如果引用css模块,也是副作用代码,也面临同样的问题,

// 样式文件属于副作用模块
import './global.css'
1
2

image

解决途径:

  1. 关闭掉没有副作用的标识
  2. 标识一下当前项目的哪些文件标识为有副作用的文件,webpack就不会忽略了
// package.json
"sideEffects": [
    // 值可以是一个数组,元素是对应的路径
    "./src/extend.js",
    // 也可以使用通配符的方式
    "*.css"
  ]
1
2
3
4
5
6
7

命令行运行一下看到bundle.js文件中对有副作用的代码并没有删除

image

webpack优化内置属性的介绍,这些特性都是为了弥补javascript早期在设计的遗留问题,随着webpack技术的发展,javascript越来越好。

# Code Splitting

Code Splitting是代码分包,代码分割。为了解决应用很复杂,模块很多,导致生成bundle体积过大的问题。

# 原因描述

通过webpack将所有的代码最终都被打包到一起,会造成bundle体积过大,事实上并不是每个模块在启动时都是必要的,因为这些模块被打包到一起,我们只用其中一个,就要把所有的模块全部加载进去。应用一般运行在浏览器端,这就意味着会浪费掉很多流量和带宽。

这个是否和之前的模块合并有冲突?

并不会有冲突,模块打包是必要的,HTTP1.1版本,本身有很多的缺陷,我们并不能同时对同一个域名下发起很多次的并行请求。而且每一次请求都会有一定的延迟,还会有额外的请求头和响应头,会造成很多流量带宽的浪费。

但是物极必反,我们需要在模块变大之后灵活处理合并和分包的问题。

# 最佳实践

将模块根据不同的规则打包到多个bundle中,按需加载。可以大大提高响应速度和运行效率。

# M1:多入口打包(Multi Entry)

多个打包入口同时打包,会输出多个打包结果。一般适用于传统的多页应用程序,最常见的就是一个页面对应一个打包入口,对于公共部分单独提取。

  1. 下载模板 webpack-multiple-entry-temp (opens new window), 修改webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: 'none',
  // 这里定义的是一个对象而不是数组,如果是数组就是多个文件打包到一起,最终还是一个入口
  // 对象的键就是打包的入口,键就是入口名称,值就是文件路径
  entry: {
    index: './src/index.js',
    album: './src/album.js'
  },
  output: {
    // 两个入口有两个文件名,这里用[name]占位符的方式动态输出文件名,最终会被替换成入口名称
    filename: '[name].bundle.js'
  },
  module: {
    ...
  },
  plugins: [
    ...
  ]
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  1. 命令行输入npm run build可以看到dist目录下确实生成了不同的文件。但是他们的html中还是两个文件都引入了
<script type="text/javascript" src="index.bundle.js"></script><script type="text/javascript" src="album.bundle.js"></script></body>
1
  1. HtmlWebpackPlugin生成插件,这个插件默认是产生一个自动输入所有打包结果的html,继续修改webpack.config.js
plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/index.html',
      filename: 'index.html',
      // 添加chunks属性,确定引入的bundle文件
      chunks: ['index']
    }),
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/album.html',
      filename: 'album.html',
      // 添加chunks属性,确定引入的bundle文件
      chunks: ['album']
    })
  ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  1. 再运行命令行打包的结果就一个html引入一个bundle.js文件了。
  2. 不同入口中肯定会有公共模块,所以在不同的打包结果中会有相同的模块出现(例如:项目中都运用了vue 、jquery之类的模块),所以还是需要对公共模块进行提取(Split Chunks)。
// webpack.config.js
optimization: {
    splitChunks: {
      // 把所有的公共模块都提取到bundle中
      chunks: 'all'
    }
},
1
2
3
4
5
6
7
  1. 命令行运行npm run build可以看到多出了一个打包文件

image

# M2:采用ES Modules的动态导入(Dynamic Imports)

通过动态导入,实现按需加载。webpack会把动态导入的模块自动单独输出到一个bundle中。

相比于多入口,这个更加的灵活,我们可以根据代码的逻辑控制是否需要或者何时加载模块。更加适用于单页应用程序。

  1. 下载模板webpack-dynamic-import-temp (opens new window),里面有album模块和posts模块,现在是启动首页就加载了两个模块,很多人如果不进行锚点切换的时候其实可以不用去加载别的模块,这里进行优化
// src/index.js

// 将两个模块的导入注释掉,我们一开始不导入这两个模块
// import posts from './posts/posts'
// import album from './album/album'

const render = () => {
  const hash = window.location.hash || '#posts'

  const mainElement = document.querySelector('.main')

  mainElement.innerHTML = ''
    
  if (hash === '#posts') {
    // 这里使用动态导入,参数是参入的路径,返回一个promise对象,then是导入成功之后执行的函数
    // then的返回参数是module,这里posts是默认导出所以将default赋值成posts别名
    import('./posts/posts').then(({ default: posts }) => {
      mainElement.appendChild(posts())
    })
  } else if (hash === '#album') {
    // 这里同上
    import('./album/album').then(({ default: album }) => {
      mainElement.appendChild(album())
    })
  }
}

render()

window.addEventListener('hashchange', render)

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
  1. 命令行打包npm run build可以看到dist/中多出来三个bundle,其中有两个是两个模块,还有一个是两个中公共的部分提取的bundle

image

如果使用的是vue单页应用,那么通过路由映射组件动态导入的方式就可以实现按需加载

# 锦上添花 —— 魔法注释

默认用动态导入的文件,文件只是一个序号,一般没啥。如果想要命名的话,可以使用webpack特有的魔法注释(Magic Comments)实现:

  1. 在import里面添加行内注释,格式为
if (hash === '#posts') {
    import(/* webpackChunkName: 'posts' */'./posts/posts').then(({ default: posts }) => {
      mainElement.appendChild(posts())
    })
  } else if (hash === '#album') {
    import(/* webpackChunkName: 'album' */'./album/album').then(({ default: album }) => {
      mainElement.appendChild(album())
    })
  }
1
2
3
4
5
6
7
8
9
  1. 运行命令行文件npm run build,可以看到打包的文件变成了我们要的名字

image

  1. 如果webpackChunkName是相同的,那么他们就会被打包到一起。
// src/index.js

if (hash === '#posts') {
    // mainElement.appendChild(posts())
    import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => {
      mainElement.appendChild(posts())
    })
  } else if (hash === '#album') {
    // mainElement.appendChild(album())
    import(/* webpackChunkName: 'components' */'./album/album').then(({ default: album }) => {
      mainElement.appendChild(album())
    })
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. 命令行运行npm run build可以看到生成的打包文件

image

这样我们就可以灵活的控制我们要打包的文件了。

# MiniCssExtractPlugin

可以将css代码从打包文件中提取的插件,通过这个插件我们可以做到css模块的按需加载

  1. 安装插件 npm i mini-css-extract-plugin --save-dev
  2. webpack.config.js中导入插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
...

module: {
    rules: [
      {
        test: /\.css$/,
        use: [
            // style-loader的作用将样式通过style标签注入html中,因为加入插件后css会单独放一个文件,所以不需要style标签,这里的style-loader就不需要了
            // 'style-loader', 
            // 这里会使用link的方式导入css文件,所以使用MiniCssExtractPlugin插件自带的loader处理
            MiniCssExtractPlugin.loader,
            'css-loader'
        ]
      }
    ]
  },
plugins: [
    ...
    // 在plugins中引用,就可以单独放在一个文件中,不需要style标签,而是通过link的方式去引入
    new MiniCssExtractPlugin()
  ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  1. 在命令行运行npm run build可以看到dist文件中有css文件单独形成。

注意:

这个功能适用于css文件比较大而言

  • css超过150kb之后可以单独加载
  • css小于150kb,不单独加载放在代码中减少一次请求会更好

# OptimizeCssAssetsWebpackPlugin

压缩输出的css文件

单独的css文件提取之后,即使运行生产模式代码都没有被压缩,是因为webpack内置只对js文件进行压缩处理,其他文件就需要额外的工具进行处理

  1. 安装npm i optimize-css-assets-webpack-plugin --save-dev
  2. webpack.config.js中进行配置
// 引入插件
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
  ...
  plugins: [
    ...
    // 在plugins中初始化
    new OptimizeCssAssetsWebpackPlugin()
  ]
}
1
2
3
4
5
6
7
8
9
10
11
  1. 在命令行运行npm run build可以看到生成的css文件已经经过了压缩。

# 将其配置到minimizer中

官方推荐我们把这个插件配置到minimize中而不是plugins,原因是因为配置到plugins,文件一直会被压缩,如果配置到optimization的minimizer中,可以通过开启关闭minimizer对文件是否要压缩进行控制

  1. webpack.config.js中修改
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
  ...
  // 在这个里面添加配置,自定义压缩器插件,webpack内置的压缩器就会被覆盖
  optimization: {
    minimizer: [
      new OptimizeCssAssetsWebpackPlugin()
    ]
  },
  ...
  plugins: [
    ...
    // 把这个注释掉
    // new OptimizeCssAssetsWebpackPlugin()
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  1. 在命令行中运行npm run build可以看到其不会进行压缩
  2. 配置在生产环境中运行npm run prod,下面是package.json中的配置,可以看到css代码自动进行了压缩
"scripts": {
    "build": "webpack",
    "prod": "webpack --mode production"
}
1
2
3
4

# 解决原生js压缩被覆盖的情况 —— terser-webpack-plugin

  1. 但是这样会把webpack内置的压缩器覆盖掉,我们要进行手动配置,安装npm i terser-webpack-plugin --save-dev(目前安装5.0.3会报错,所以跟着例子安装2.2.1可以正常工作)
  2. 在webpack.config.js中配置
const TerserWebpackPlugin = require("terser-webpack-plugin")

module.exports = {
  ...
  optimization: {
    minimizer: [
      // 替代原生压缩插件初始化 
      new TerserWebpackPlugin(),
      new OptimizeCssAssetsWebpackPlugin()
    ]
  },
  plugins: [
    ...
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  1. 命令行运行npm run prod可以看到js和css一起压缩了。

# Hash

一般部署前端资源文件的时候都会启动服务器的静态资源缓存,这样对于用户的浏览器而言,就可以缓存到应用的静态资源,后续不需要再请求服务器。整体的响应速度会提升,如果在缓存策略中,缓存时长设置的过短,效果并不是很明显。如果缓存时间过长,一旦应用发生更新,又没有办法及时更新到客户端。为了解决这个问题:

在生产模式下,为输出的文件名添加Hash,一旦名称发生改变,全新的文件名就是全新的请求,不用担心不刷新的问题,那么缓存时间就可以设置的很长。

webpack中filename属性都支持通过占位符的方式支持Hash,不过他们支持三种Hash,效果各不相同。

# M1:hash

module.exports = {
  ...
  output: {
    filename: '[name]-[hash].bundle.js'
  },
  plugins: [
    ...
    new MiniCssExtractPlugin({
      filename: "[name]-[hash].bundle.css"
    })
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12

项目级别的hash,所有文件生成一个hash值

image

如果项目中有任何的改动,生成的所有文件都会生成一个新的hash

image

# M2:chunkhash(推荐)

module.exports = {
  ...
  output: {
    filename: '[name]-[chunkhash].bundle.js'
  },
  plugins: [
    ...
    new MiniCssExtractPlugin({
      filename: "[name]-[chunkhash].bundle.css"
    })
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12

组件级别的hash,相同组件的hash是一样的

image

单独修改js文件,只有js文件的hash修改

image

修改posts.js文件,其整个组包括main的hash都会改变,其他的文件不会改变。

image

# contenthash

module.exports = {
  ...
  output: {
    filename: '[name]-[contenthash].bundle.js'
  },
  plugins: [
    ...
    new MiniCssExtractPlugin({
      filename: "[name]-[contenthash].bundle.css"
    })
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12

文件级别的hash,根据文件的内容生成的hash值,只要是不用的文件,就有不同的hash值

image

修改posts文件,posts文件和其被引用文件都会更新hash

image

这种方式是解决缓存问题最好的方式

# 修改hash长度

22位的hash长度过长,可以自己指定,冒号后面跟长度即可。

module.exports = {
  ...
  output: {
    filename: '[name]-[contenthash:8].bundle.js'
  },
  plugins: [
    ...
    new MiniCssExtractPlugin({
      filename: "[name]-[contenthash:8].bundle.css"
    })
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
更新时间: 2021-12-18 20:56