webpack 深入系列

利用 webpack 可以做哪些事情?

  1. 代码转换
  2. 文件优化
  3. 代码分割
  4. 模块合并
  5. 自动刷新
  6. 代码校验
  7. 自动发布 ...

常见配置

npm install webpack webpack-cli -D

扩展

  1. -D 等价于 --save-dev, 用于开发环境时的依赖
  2. -S 等价于 --save, 用于生产环境时的依赖
  • 指定配置文件
npx webpack --config 自定义名称
  • 开发服务器
npm install webpack-dev-server -D

devServer: { // 开发服务器的配置
   port: 3000, // 指定端口
   progress: true, // 开启进度条
   contentBase: './dist', // 指定静态服务的目录
   open: true, // 自动打开网页
   compress: true, // 启动gzip压缩
}
  • 模块

    loader 特点:希望单一,每个 loader 做对应的事情

    loader 的用法:字符串只用一个 loader, 多个 loader 需要[]

    loader 的顺序:从右向左执行, 从下到上执行

    loader 还可以写成对象的形式

  1. style-loader: 是把 css 插入到 head 标签中
  2. css-loader: 解析 @import 语法
  3. less-loader: 把.less 文件转换为.css 文件
  4. stylus-loader: 把.styl 文件转换为.styl 文件
  5. sass-loader: 把.scss 文件转换为.scss 文件
  6. postcss-loader: 一般配合 autoprefixer 一起使用,添加浏览器前缀
  • 插件
  1. html-webpack-plugin
npm install html-webpack-plugin -D
new HtmlWebpackPlugin({
  template: './src/index.html', // 以哪个html文件作为模板
  filename: 'tmc.html', // 指定模板的名称,
  minify: {
    removeAttributeQuotes: true, // 删除双引号
    collapseWhitespace: true, // 压缩为一行
  },
  hash: true, // hash 值
  title: '汤小梦', // 设置网站标题
  favicon: './src/public/favicon.ico', // 设置网站的 icon 图
  inject: true | 'head' | 'body' | false, // 设置为 true 或 body,所有的 javascript 资源将被放置到 body 元素的底部,'head' 或 false 将放置到 head 元素中
})

注意:动态设置 title 时,需在模板的<title>标签中添加 <%= htmlWebpackPlugin.options.title %>

  1. mini-css-extract-plugin
npm install mini-css-extract-plugin -D
new MiniCssExtractPlugin({
  filename: 'css/index.css', // 输出的文件名
})

// 使用
style-loader ==> MiniCssExtractPlugin.loader
  1. optimize-css-assets-webpack-plugin
npm install optimize-css-assets-webpack-plugin -D
optimization: { // 优化项
    minimizer: [
        new OptimizeCssAssetsWebpackPlugin(), // 压缩打包过后的CSS
    ]
},
  1. uglifyjs-webpack-plugin
npm install uglifyjs-webpack-plugin -D 【弃用】
npm install terser-webpack-plugin -D
optimization: { // 优化项
  minimizer: [
      new UglifyJsPlugin({ // 压缩打包过后的JS
          cache: true, // 启动缓存
          parallel: true, // 并发压缩
          sourceMap: true // 启动源码调试
      }),
      new TerserWebpackPlugin()
  ]
},

注意:uglify-js 只支持压缩 ES5,uglify-es 用来支持 es6+,之后放弃维护了。如果要使用 uglify-js 压缩 ES6+需要先使用 babel 转码成 ES5. 取代 ugligy-es 的是 uglify-es 的一个分支 terser; webpack 放弃了 uglifyjs-webpack-plugin 转而使用 terser-webpack-plugin;

高级配置

优化策略

AST 抽象语法树

Tapable

webpack 的流程

手写 loader

什么是 Loader

Webpack 只能处理 JavaScript 的模块,如果要处理其他类型的文件,需要使用 loader 进行准转换。它是指用来将一段代码转换成另一段代码的 webpack 加载器. loader 由两部分组成,一分部是 pitchLoader, 另一部分是 normalLoader. pitchLoader 和 normalLoader 执行顺序相反. pitchLoader 中有返回值就直接不走后面的了.

  • loader 的顺序

    从右向左,从下到上

  • loader 的使用方式

    1. 单个 loader 可以写成字符串也可以写成对象
    2. 多个 loader 写成数组
  • loader 的特点

    1. 第一个 loader 要返回一个 js 脚本. (默认只能接受 String Or Buffer)
    2. 每个 loader 只能做一件事,为了 loader 再更多场景下链式调用
    3. 么讴歌 loader 都是一个模块
    4. 每个 loader 都是无状态的, 确保 loader 在不同模块转换直接不保存状态
  • loader 的分类

    1. pre 在前面的
    2. post 在后面的
    3. normal 正常的

    行内 loader 的用法:

    // 符号的含义
    // -! 不会让文件 去通过 pre + normal loader来处理了
    // ! 没有normal
    // !! 什么都不要 只要inline-loader
    let str = require('inline-loader!./xx.js')
    

    顺序:pre -> normal -> inline -> post

  • 查找 loader 的几种方式

    1. 绝对路径
    use: path.resolve(__dirname, 'loader.js')
    
    1. 设置别名
    resolveLoader: {
      alias: {
        loader: path.resolve(__dirname, 'loader.js')
      }
    }
    
    use: loader
    
    1. 自动查找
    resolveLoader: {
      // 默认在node_modules下查找,找不到就自动到loaders目录下查找
      modules: ['node_modules', path.resolve(__dirname, 'loaders')]
    }
    
  • babel-loader 的实现

let babel = require('@babel/core')
let loaderUtils = require('loader-utils')

function loader(source) {
  // console.log(this.resourcePath) 绝对路径 this loader上下文
  let options = loaderUtils.getOptions(this)
  let cb = this.async()
  babel.transform(
    source,
    {
      ...options,
      sourceMaps: true,
      filename: this.resourcePath.split('/').pop(),
    },
    function(err, result) {
      cb(err, result.code, result.map) // 异步
    }
  )
}

module.exports = loader

手写 plugin

优化

打包的数据分析

npm install friendly-errors-webpack-plugin node-notifier -D
const path = require('path')
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const notifier = require('node-notifier')
const icon = path.join(__dirname, 'icon.jpg')

module.exports = {
  plugins: [
    new FriendlyErrorsWebpackPlugin({
      onErrors: (severity, errors) => {
        let error = errors[0]
        notifier.notify({
          title: 'webpack编译失败',
          message: error.name,
          subscribe: error.file || '',
          icon,
        })
      },
    }),
  ],
}
npm install speed-measure-webpack-plugin -D // 适用于 webpack4
npm install speed-measure-webpack5-plugin -D // 适用于 webpack5
const SpeedMeasureWebpack5Plugin = require('speed-measure-webpack5-plugin')
const smw = new SpeedMeasureWebpack5Plugin()

module.exports = smw.wrap({
  // ...
})
npm install webpack-bundle-analyzer -D
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')

module.exports = smw.wrap({
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'disabled', // 不启动展示打包报告的HTTP服务器
      generateStatsFile: true, // 要生成stats.json文件
    }),
  ],
})

编译时间的优化: 减少要处理的文件、缩小查找的范围

编译体积优化

如何设置环境变量

mode 的默认值

  1. webpack 的 mode 默认值为 production
  2. webpack serve 的 默认值为 development
  3. 可以在模块内通过 process.env.NODE_ENV 获取当前的环境变量,无法在 webpack 配置文件中获取此变量
// webpack.config.js
console.log('webpack  process.env.NODE_ENV', process.env.NODE_ENV) // node环境 默认值是undefined

// src/index.js
console.log('src/index.js process.env.NODE_ENV', process.env.NODE_ENV) // production

设置 mode 值的方式

  1. 命令行 mode 方式
// package.json
"build": "webpack --mode development"
  1. 命令行 env 方式
// package.json
"build": "webpack --env development"
module.exports = (env) => {
  console.log(env)
  return ({
    mode: env == 'development' ? 'development' : 'production'
  })
})
  1. DefinePlugin 方式
module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      ENV: JSON.stringify('development'),
    }),
  ],
}

注意:该全局变量定义在 编译 时的,在浏览器 运行 阶段就是值了访问不到了

  1. cross-env 方式
"build": "cross-env NODE_ENV=development webpack"
  1. 创建 .env 和 dotenv 方式
// .env
NODE_ENV = production

require('dotenv').config()

preload vs prefetch

  1. preload 是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源
  2. 而 prefetch 是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源 所以建议:对于当前页面很有必要的资源使用 preload,对于可能在将来的页面中使用的资源使用 prefetch