一、Source Map

运行代码与源代码之间完全不同,如果需要调试应用,错误信息无法定位,调试和报错都是基于运行代码,SourceMap 就是解决这种问题的最好办法。
Source Map解决了源代码与运行代码不一致所产生的问题.
Webpack 支持sourceMap 12种不同的方式,每种方式的效率和效果各不相同。效果最好的速度最慢,速度最快的效果最差
eval函数可以运行字符串当中的js代码:eval(console.log(123))
当DevTool的值为eval,打包后的报错信息只有源代码文件名称,没有行列信息

每个关键词的特点组合:

  • eval 是否使用eval执行代码模块
  • cheap Source map是否包含行信息
  • module 是否能够得到Loader处理之前的源代码
  • inline SourceMap 不是物理文件,而是以URL形式嵌入到代码中
  • hidden 看不到SourceMap文件,但确实是生成了该文件
  • nosources 没有源代码,但是有行列信息。为了在生产模式下保护源代码不被暴露

开发模式推荐使用:eval-cheap-module-source-map,因为:

  • 代码每行不会太长,没有列也没问题
  • 代码经过Loader转换后的差异较大
  • 首次打包速度慢无所谓,重新打包相对较快

生产模式推荐使用:none,原因

  • Source Map会暴露源代码
  • 调试是开发阶段的事情
  • 对代码实在没有信心可以使用nosources-source-map
    1. mode: 'none',
    2. entry: './src/main.js',
    3. output: {
    4. filename: 'bundle.js',
    5. path: path.join(__dirname, 'dist')
    6. },
    7. devtool: 'eval-cheap-module-source-map',

    image.png

    二、webpack HMR

    HMR(Hot Module Replacement) 模块热替换,应用运行过程中,实时替换某个模块,应用运行状态不受影响。
    webpack-dev-server自动刷新导致的页面状态丢失。我们希望在页面不刷新的前提下,模块也可以即使更新。热替换只将修改的模块实时替换至应用中。

HMR是webpack中最强大的功能之一,极大程度的提高了开发者的工作效率。
HMR已经集成在了webpack-dev-server中,运行webpack-dev-server --hot,也可以通过配置文件开启

  1. const webpack = require('webpack')
  2. module.exports = {
  3. devServer: {
  4. hot: true
  5. // hotOnly: true // HMR替换代码中无论是否有问题,浏览器不会自动刷新
  6. },
  7. plugins: [
  8. new webpack.HotModuleReplacementPlugin()
  9. ]
  10. }

Webpack中的HMR并不是对所有文件开箱即用,样式文件支持热更新,脚本文件需要手动处理模块热替换逻辑。而通过脚手架创建的项目内部都集成了HMR方案。没有通用的js热替换代码,每个依赖都是不同的,大部分框架中都已经内置了HMR替换方案

  1. // ================================================================
  2. // HMR 手动处理模块热更新
  3. // 不用担心这些代码在生产环境冗余的问题,因为通过 webpack 打包后,
  4. // 这些代码全部会被移除,这些只是开发阶段用到
  5. if (module.hot) {
  6. let hotEditor = editor
  7. module.hot.accept('./editor.js', () => {
  8. // 当 editor.js 更新,自动执行此函数
  9. // 临时记录编辑器内容
  10. const value = hotEditor.innerHTML
  11. // 移除更新前的元素
  12. document.body.removeChild(hotEditor)
  13. // 创建新的编辑器
  14. // 此时 createEditor 已经是更新过后的函数了
  15. hotEditor = createEditor()
  16. // 还原编辑器内容
  17. hotEditor.innerHTML = value
  18. // 追加到页面
  19. document.body.appendChild(hotEditor)
  20. })
  21. module.hot.accept('./better.png', () => {
  22. // 当 better.png 更新后执行
  23. // 重写设置 src 会触发图片元素重新加载,从而局部更新图片
  24. img.src = background
  25. })
  26. // style-loader 内部自动处理更新样式,所以不需要手动处理样式模块
  27. }

HMR注意事项:

  • 处理HMR的代码报错会导致自动刷新,不易发现
  • 没启动HMR的情况下,HMR API报错
  • 代码中多了很多与业务无关的代码

三、webpack生产环境优化

我们在生产环境中,更注重开发效率,而在生产环境中,更注重开发效率。
模式(mode) webpack建议我们为不同的环境创建不同的配置,两种方案:

  • 配置文件根据环境不同导出不同配置 ```javascript const path = require(‘path’) const webpack = require(‘webpack’) const {CleanWebpackPlugin} = require(‘clean-webpack-plugin’) const HtmlWebpackPlugin = require(‘html-webpack-plugin’) const CopyWebpackPlugin = require(‘copy-webpack-plugin’)

module.exports = (env, argv) => { const config = { mode: ‘none’, entry: ‘./src/main.js’, output: { filename: ‘bundle.js’, path: path.join(__dirname, ‘dist’), // publicPath: ‘dist/‘ }, module: { rules: [ { test: /.md$/, use: [‘html-loader’, ‘./markdown-loader.js’] } ] }, plugins: [ new CleanWebpackPlugin(), // 用于生成index.html new HtmlWebpackPlugin({ title: ‘Webpack Plugin Sample’, meta: { viewport: ‘width=device-width’ }, template: ‘./src/index.html’ }), // 用于生成about.html new HtmlWebpackPlugin({ filename: ‘about.html’ }), // 开发过程最好不要使用这个插件 // new CopyWebpackPlugin({ // patterns: [‘public’] // }), // new MyPlugin(), new webpack.HotModuleReplacementPlugin() ], devServer: { contentBase: ‘./public’, proxy: { ‘/api’: {// 以/api开头的地址都会被代理到接口当中 // http://localhost:8080/api/users -> https://api.github.com/api/users target: ‘https://api.github.com‘, // http://localhost:8080/api/users -> https://api.github.com/users pathRewrite: { ‘^/api’: ‘’ }, // 不能使用localhost:8080作为请求GitHub的主机名 changeOrigin: true, // 以实际代理的主机名去请求 } }, // hot: true hotOnly: true, // 如果热替换代码报错了,则不刷新 }, devtool: ‘eval-cheap-module-source-map’ }

if (env === ‘production’) { config.mode = ‘production’ config.devtool = false config.plugins = [ …config.plugins, new CleanWebpackPlugin(), new CopyWebpackPlugin({ patterns: [‘public’] }) ] } return config }

  1. - 一个环境对应一个配置文件
  2. yarn add webpack-merge --dev<br />Webpack.common.js
  3. ```javascript
  4. const HtmlWebpackPlugin = require('html-webpack-plugin')
  5. module.exports = {
  6. entry: './src/main.js',
  7. output: {
  8. filename: `bundle.js`
  9. },
  10. module: {
  11. rules: [
  12. {
  13. test: /\.js$/,
  14. use: {
  15. loader: 'babel-loader',
  16. options: {
  17. presets: ['@babel/preset-env']
  18. }
  19. }
  20. }
  21. ]
  22. },
  23. plugins: [
  24. new HtmlWebpackPlugin({
  25. filename: `index.html`
  26. })
  27. ]
  28. }

Webpack.dev.js

const webpack = require("webpack");
const { merge } = require("webpack-merge");
const common = require("./webpack.common");

module.exports = merge(common, {
  mode: "development",
  devtool: "eval-cheap-module-source-map",
  devServer: {
    hot: true,
    contentBase: "public",
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.DefinePlugin({
      BASE_URL: JSON.stringify("/"),
    }),
  ],
});

Webpack.prod.js

const common = require('./webpack.common')
const { merge } = require('webpack-merge')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = merge(common, {
  mode: 'production',
  plugins: [
    new CleanWebpackPlugin(),
    new CopyWebpackPlugin({
        patterns: ['public']
    })
  ]
})

Package.json

"scripts": {
  "build": "webpack --config webpack.prod.js"
},

四、webpack优化配置

  • DefinePlugin 为代码注入全局成员,这个内置插件默认就会启动,往每个代码中注入一个全局变量process.env.NODE_ENV

方法的构造函数,接收一个对象,此对象的每一个键值都会被注入到代码中

const webpack = require('webpack')

plugins: [
  new webpack.DefinePlugin({
    API_BASE_URL: JSON.stringify('http://api.example.com')
  })
]
  • Tree-Shaking 摇掉代码中未引用到的代码(dead-code),这个功能在生产模式下自动被开启。

Tree-Shaking并不是webpack中的某一个配置选项,而是一组功能搭配使用后的效果。
在非生产模式下开启:

optimization: {
    usedExports: true, // 模块只导出被使用的成员
    minimize: true // 压缩输出结果
  }
  • 合并模块函数 concatenateModules, 又被成为Scope Hoisting,作用域提升

    optimization: {
      usedExports: true,
      minimize: true,
      concatenateModules: true // 尽可能合并每一个模块到一个函数中
    }
    
  • 很多资料中说如果使用Babel-Loader,会导致Tree-Shaking失效,因为Tree-Shaking前提是ES Modules,由Webpack打包的代码必须使用ESM,为了转化ES中的新特性,会使用babel处理新特性,就有可能将ESM转化CommonJS,而我们使用的@babel/preset-env这个插件集合就会转化ESM为CommonJS,所以Tree-Shaking会不生效。

但是在最新版babel-loader关闭了转换ESM的插件,所以使用babel-loader不会导致Tree-Shaking失效

  • sideEffects 副作用,指的是模块执行时除了导出成员之外所做的事情,sideEffects用于npm包标记是否有副作用,一般是开发一个npm模块时才会用到。

如果没有副作用,则没有用到的模块则不会被打包。在webpack.config.js中开启这个功能:

optimization: {
  usedExports: true,
  minimize: true,
  concatenateModules: true,
  sideEffects: true
}

在package.json里面增加一个属性sideEffects,值为false,表示没有副作用,没有用到的代码则不进行打包。确保你的代码真的没有副作用,否则在webpack打包时就会误删掉有副作用的代码,比如说在原型上添加方法,则是副作用代码;还有CSS代码也属于副作用代码。
"sideEffects": false 表示项目里的代码都没有副作用
数组中则表示,里面的文件是有副作用的

"sizeEffects": [
  "./src/extend.js",
  "*.css"
]

五、Code Splitting 代码分包/代码分割

webpack的一个弊端:所有的代码都会被打包到一起,如果应用复杂,bundle会非常大。而并不是每个模块在启动时都是必要的,所以需要分包、按需加载。
资源太大了会影响加载速度;太碎了会导致请求次数过多,因为在目前主流的HTTP1.1有很多缺陷,如同域并行请求限制、每次请求都会有一定的延迟,请求的Header浪费带宽流量。所以模块打包时有必要的。

目前的webpack分包方式有两种:

多入口打包

适用于多页应用程序,一个页面对应一个打包入口,公共部分单独抽取。

entry: {
    index: './src/index.js',
    album: './src/album.js'
},
output: {
    filename: '[name].bundle.js'
},
// 每个打包入口形成一个独立的chunk
plugins: [
    new HtmlWebpackPlugin({
      title: 'Multi Entry',
      template: './src/index.html',
      filename: 'index.html',
      chunks: ['index']
    }),
    new HtmlWebpackPlugin({
      title: 'Nulti Entry',
      template: './src/album.html',
      filename: 'album.html',
      chunks: ['album']
    })
  ],
// 不同的打包入口肯定会有公共模块,我们需要提取公共模块:
    optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }

动态导入

需要用到某个模块时,再加载这个模块,动态导入的模块会被自动分包。通过动态导入生成的文件只是一个序号,可以使用魔法注释指定分包产生bundle的名称。相同的chunk名会被打包到一起。
魔法注释:在调用模块的之前增加行内注释

const render = () => {
  const hash = window.location.hash || '#posts'
  const mainElement = document.querySelector('.main')
  mainElement.innerHTML = ''

  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())
    })
  }
}

render()

window.addEventListener('hashchange', render)

六、MiniCssExtractPlugin

提取CSS到单个文件
当css代码超过150kb左右才建议使用。

const MiniCssExtracPlugin = require('mini-css-extract-plugin')

module: {
  rules: [
    {
      test: /\.css$/,
      use: [
        // 'style-loader',
        MiniCssExtracPlugin.loader,
        'css-loader'
      ]
    }
  ]
},

七、OptimizeCssAssetsWebpackPlugin

压缩输出的CSS文件
webpack仅支持对js的压缩,其他文件的压缩需要使用插件。
yarn add optimize-css-asset-webpack-plugin —dev
yarn add terser-webpack-plugin —dev
可以使用 optimize-css-assets-webpack-plugin压缩CSS代码。放到minimizer中,在生产模式下就会自动压缩

const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const TerserWebpackPlugin = require('terser-webpack-plugin')

optimization: {
  minimizer: [
    new TerseWebpackPlugin(), // 指定了minimizer说明要自定义压缩器,所以要把JS的压缩器指明,否则无法压缩
    new OptimizeCssAssetWebpackPlugin()
  ]
}

八、输出文件名hash

生产模式下,文件名使用Hash

  • 项目级别的hash

    output: {
    filename: '[name]-[hash].bundle.js'
    }
    new MiniCssExtractPlugin({
    filename: '[name]-[hash].bundle.css'
    })
    
  • chunk级别的hash ```javascript output: { filename: ‘[name]-[chunkhash].bundle.js’ }

new MiniCssExtractPlugin({ filename: ‘[name]-[chunkhash].bundle.css’ })


- 文件级别的hash,:8是指定hash长度 (推荐
```javascript
output: {
  filename: '[name]-[contenthash:8].bundle.js'
}


new MiniCssExtractPlugin({
  filename: '[name]-[contenthash:8].bundle.css'
})