webpack系列-入门

webpack是什么

静态资源模块打包工具

当 webpack 处理应用程序时,它会在内部构建一个依赖图(dependency graph),此依赖图对应映射到项目所需的每个模块,并生成一个或多个包(bundle)。

webpack打包是前端工程化必不可少的环节,可以基于webpack 解决前端一系列问题,逐渐形成一套前端工程化解决方案。

webpack解决了什么

webpack 是从入口文件开始, 经过模块依赖加载、 分析和打包三个流程完成项目的构建。 在加载、 分析和打包的三个过程中, 可以针对性的做一些解决方案 。

  • 模块化打包, 一切皆模块, JS 是模块, CSS 等也是模块;
  • 语法糖转换: 比如 ES6 转 ES5、 TypeScript;
  • 预处理器编译: 比如 Less、 Sass 等;
  • 项目优化: 比如压缩、 CDN、代码分割;
  • 解决方案封装: 通过强大的 Loader 和插件机制, 可以完成解决方案的封装, 比如 PWA;
  • 流程对接: 比如测试流程、 语法检测等

基础概念

  • 工程化:将工程方法系统化地应用到软件开发中。

    工程指的是以系统、严谨、可量化的方法开发、运营、维护软件。

    工程化的理解可以是类似于盖房子,需要经过一系列的图纸设计、购买原材料、挖地基、搭建房子,一步一步来建造房子。

    前端工程可以定义为,将工程方法系统化地应用到前端开发中,以系统、严谨、可量化的方法开发、运营、维护前端应用程序。

    参考链接:「前端工程化」该怎么理解?

  • 模块化:模块化是指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,有多种属性,分别反映其内部特性。

  • Bundle:分发代码是指在构建过程中,经过最小化和优化后产生的输出结果,最终将在浏览器中加载。最终打包完成的文件,一般与 chunk 是一一对应的关系,bundle 就是对chunk 进行压缩打包等处理后的结果。

  • Chunk:每一个被打包后的文件都是一个chunk

  • Loader: 编译时对模块进行预处理,进行转化解析编译等处理。处理非JavaScript模块,例如将less解析为css。

  • Plugin:在打包过程中的某个具体时刻进行处理。直接作用于webpack,扩展webpack能力,改变输出结果,例如打包优化和压缩。

创建项目

初始化项目目录npm init,会自动生成package.json文件,就可以使用npm scripts定义命令,对项目进行管理。

package.json

{
  "name": "webpacktemplate",
  "version": "1.0.0",
  "description": "My template + pug",
  "main": "index.js",
  "private": true,
  "scripts": {
    "dev": "webpack-dev-server --open --config ./build/webpack.dev.conf.js",
    "build": "webpack --config ./build/webpack.build.conf.js"
  },
  "browserslist": [
    "> 1%",
    "last 3 version"
  ],
  "keywords": [
    "webpack4"
  ],
  "license": "MIT",
  "devDependencies": {},
  "dependencies": {}
}

安装

mkdir webpack-demo && cd webpack-demo
npm init -y
npm install webpack webpack-cli --save-dev

webpack-cli会默认安装webpack

webpack-cli使得可以在命令行中运行webpack命令

如果npm scripts中没有指明webpack配置文件,则webpack-cli会默认加载webpack.config.js文件

配置

创建一个配置文件webpack.config.js

const path = require('path');

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
};

// 或
module.exports = (env) => {
    // 处理 webpack-cli 传参
    return {
        entry: './src/index.js',
        output: {
            filename: 'bundle.js',
            path: path.resolve(__dirname, 'dist')
        }
    }
};

webpack 最出色的功能之一就是,除了 JavaScript,还可以通过 loader 引入任何其他类型的文件

webpack运行在node环境下,遵循node的CommonJS模块化规则使用require,最后导出的是对象或函数返回的对象

Entry与Output

context 上下文:基础目录,绝对路径,用于从配置中解析入口点 和 加载器

const path = require('path');

module.exports = {
  //...
  context: path.resolve(__dirname, 'app'),
};

默认使用当前目录process.cwd()

单个文件输入输出

module.exports = {
  entry: './src/index.js', // 写法一
  // entry: { // 写法二
  //  main: './src/index.js', 
  // },
  ...
  output: {
    filename: 'bundle.js', // 入口文件命名规则 默认命名文件为main.js
    chunkFilename: '[name].chunk.js', // 文件内部异步加载的文件命名规则
    path: path.resolve(__dirname, 'dist')
  }
};

多个文件输入输出,不能多个文件输入一个文件输出

module.exports = {
  entry: {
    app: './src/index.js', 
    main: './src/main.js'
  },
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

HtmlWebpackPlugin会自动将两个打包后的js文件添加到index.html文件中。

webpack 默认入口文件是 src/index.js,默认输出目录是 dist/main.js

特殊输出内容需要进行更复杂的 output 配置。

与输出内容相关配置有target、externals、devtool

解析resolve

概念:https://webpack.docschina.org/concepts/module-resolution/

设置如何找到依赖模块和模块如何被解析。resolver 是一个帮助寻找模块绝对路径的库。 一个模块可以作为另一个模块的依赖模块,然后被后者引用,如下:

import foo from 'path/to/module';
// 或者
require('path/to/module');

所依赖的模块可以是来自应用程序的代码或第三方库。 resolver 帮助 webpack 从每个 require/import 语句中,找到需要引入到 bundle 中的模块代码。 当打包模块时,webpack 使用 enhanced-resolve 来解析文件路径(绝对路径、相对路径、模块路径)。

配置别名 resolve.alias

创建 importrequire 的别名,来确保模块引入变得更简单。例如,一些位于 src/ 文件夹下的常用模块:

const path = require('path');

module.exports = {
  //...
  resolve: {
    alias: {
      Utilities: path.resolve(__dirname, 'src/utilities/'),
      Templates: path.resolve(__dirname, 'src/templates/'),
    },
  },
};
  1. alias 的名字可以使用 @ ! ~ 等这些特殊字符, 实际使用中 alias 都使用一种, 或者不同类型使用一种, 这样可以跟正常的模块引入区分开, 增加辨识度;

  2. 使用 @ 注意不要跟 npm 包的scope冲突!

  3. 这时在 vscode 中会导致我们检测不到 utils 中的内容, 不能帮我们快速编写代码, 可以通过在项目根目录创建 jsconfig.json 来帮助我们定位:

    //jsconfig.json
    {
        "compilerOptions": {
            "baseUrl": "./src",
                "paths": {
                    "@lib/": ["src/lib"]
                }
        }
    }
resolve.mainFields

有一些我们用到的模块会针对不同宿主环境提供几份代码, 例如提供 ES5 和 ES6 的两份代码, 或者提供浏览器环境和 nodejs 环境两份代码, 这时候在 package.json 文件里会做如下配置:

{
    "jsnext:main": "es/index.js", //采用ES6语法的代码入口文件
    "main": "lib/index.js", //采用ES5语法的代码入口文件, node
    "browser": "lib/web.js" //这个是专门给浏览器用的版本
}

在 Webpack 中, 会根据 resolve.mainFields 的设置去决定使用哪个版本的模块代码, 在不同的 target 下对应的 resolve.mainFields 默认值不同, 默认 target=web 对应的默认值为:

module.exports = {
    resolve: {
        mainFields: ['browser', 'module', 'main']
    }
};

所以在 target=web 打包时, 会寻找 browser 版本的模块代码。

其他配置
  • resolve.mainFiles : 解析目录时候的默认文件名, 默认是 index , 即查找目录下面的 index + resolve.extensions 文件;
  • resolve.modules : 查找模块依赖时, 默认是 node_modules ;
  • resolve.symlinks : 是否解析符合链接(软连接, symlink) ;
  • resolve.plugins : 添加解析插件, 数组格式;
  • resolve.cachePredicate : 是否缓存, 支持 boolean 和 function, function 传入一个带有 path 和 require 的对象, 必须返回 boolean 值。

模块module

概念:https://webpack.docschina.org/concepts/modules/

模块化编程中,开发者将程序分解为功能离散的 chunk,并称之为 模块

在 webpack 解析模块时,不同的模块可以根据配置来进行特殊处理。

常用配置:

module.noParse

可以让 webpack 忽略对部分没有采用模块化的文件的递归解析和处理,能够提高构建性能。

module.exports = {
    module: {
        // 使用正则表达式
        noParse: /jquery|lodash/
        // 使用函数, 从 Webpack 3.0.0 开始支持
        noParse: (content) => {
            // content 代表一个模块的文件路径
            // 返回 true or false
            return /jquery|lodash/.test(content);
        }
	}
}

这里一定要确定被排除出去的模块代码中不能包含 import 、 require 、 define 等内容, 以保证 webpack 的打包包含了所有的模块, 不然会导致打包出来的 js 因为缺少模块而报错

module.rules

在处理模块时,将符合规则条件的模块,提交给对应的处理器来处理,通常来配置loader。

Loader

文件预处理需要安装配置相应的 loader 进行编译的时候处理。

loader 使得 webpack 具有处理多种文件格式的能力,将多种文件格式转化处理成 webpack 能够进行打包的模块。

loader配置执行顺序,从下到上,从右到左。

加载CSS、图片、字体、数据
npm install --save-dev style-loader css-loader # css
npm install --save-dev file-loader # 图片/字体
npm install --save-dev csv-loader xml-loader # 数据

webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
      rules: [
          // css预处理,css-loader将css处理后,style-loader将css代码插入到html文件中
          // less-loader/sass-loader将相应的less/sass代码翻译成css代码
          // postcss-loader自动加载浏览器厂商前缀,配合postcss.config.js使用,安装autoprefixer插件
          // options:{ importLoaders: 2 } 表示文件内引用的子文件也要走最开始的两个loader,否则容易忽略最开始的loader处理
          {
              test: /\.css$/,
              use: [
                  'style-loader',
                  {
                      loader: 'css-loader',
                      modules: true // css模块化,避免样式冲突,使用方式如styleModule.className
                  }
              ]
          },
          // 图片预处理
          // image-webpack-loader可以进行图片压缩
          {
              test: /\.(png|svg|jpg|gif)$/,
              use: [
                  'file-loader' // 将项目下的文件复制到打包目录下
              ]
          },
          // 图片预处理可以使用url-loader,url-loader包含file-loader功能且能够将小图片进行转换成base64
          {
              test: /\.(jpg|png|gif)$/,
              use: {
                  loader: 'url-loader',
                  options: {
                      name: '[name]_[hash].[ext]',
                      outputPath: 'images/',
                      limit: 204800
                  }
              }
          },
          // 字体预处理
          {
              test: /\.(woff|woff2|eot|ttf|otf)$/,
              use: [
                  'file-loader'
              ]
          },
          // 数据预处理
          {
              test: /\.(csv|tsv)$/,
              use: [
                  'csv-loader'
              ]
          },
          {
              test: /\.xml$/,
              use: [
                  'xml-loader'
              ]
          }
      ]
  }
};

在使用 d3 等工具来实现某些数据可视化时,预加载数据会非常有用。我们可以不用再发送 ajax 请求,然后于运行时解析数据,而是在构建过程中将其提前载入并打包到模块中,以便浏览器加载模块后,可以立即从模块中解析数据。

Plugin

webpack 有丰富的插件接口,许多webpack不具有的功能可以使用插件进行实现,这样使得webpack具有更大的灵活性。

安装

npm install --save-dev html-webpack-plugin # HtmlWebpackPlugin
npm install --save-dev clean-webpack-plugin # clean-webpack-plugin

HtmlWebpackPlugin简化了HTML文件的创建,以便为你的webpack包提供服务。会在打包结束后,自动生成一个html文件,并把打包生成的js自动引入到这个html文件中。

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  entry: './src/index.js', 
  plugins: [
    new HtmlWebpackPlugin({ // 对每个html进行处理
        template: 'index.js', // 模板文件
        title: 'xxx' // 设置页面title
    }),
    new CleanWebpackPlugin(), // 清空输出文件夹
  ],
  output: {
    filename: 'bundle.js', 
    path: path.resolve(__dirname, 'dist')
  }
};

DevTool

选择一种 source map 格式来增强调试过程。不同的值会明显影响到构建(build)和重新构建(rebuild)的速度。

sourceMap关系映射,设置后在相对应模式中可以查找到报错位置。

module.export = {
    devtool: 'none' | 'source-map' | ...
}

建议:development环境打包使用cheap-module-eval-source-map模式,production环境打包如果出错需要查看问题所在,使用cheap-module-source-map模式。

自动编译

代码发生变化后自动编译代码,仅在开发阶段。

  • --watch监听代码变化后进行重新编译。webpack --watch无法进行ajax请求。
  • 构建node服务器进行编译发布。
  • webpack-dev-server提供了一个简单的web服务器,并且能够实现重新加载。监听文件变化后重新打包后打开浏览器并刷新。将打包的内容暂存在内存中,提升打包效率。
npm install --save-dev webpack-dev-server

webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.js',
  devtool: 'inline-source-map', // 帮助编译时输出错误信息
  devServer: {
    contentBase: './dist', // 告诉webpack-dev-server查找文件的位置
    compress: true,
  	port : 9000
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

npm配置中添加脚本

"script": {
    ...
    "start": "webpack-dev-server --open"
}

–open参数表示编译后自动打开浏览器

模块热替换(HMR)

允许在运行时更新各种模块,而无需进行完全刷新。

模块热替换运行步骤:

  1. 应用程序要求 HMR runtime 检查更新。
  2. HMR runtime 异步地下载更新,然后通知应用程序。
  3. 应用程序要求 HMR runtime 应用更新。
  4. HMR runtime 同步地应用更新。

webpack.config.js

const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    app: './src/index.js', 
  },
  devtool: 'inline-source-map',
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    compress: true,
    port : 9000,
    hot: true, // 启用HotModuleReplacementPlugin功能
    hotOnly: true // 启用热更新模块,以防HMR失效时刷新页面
  },
  plugins: [
    new webpack.NamedModulesPlugin(),
    new webpack.HotModuleReplacementPlugin() // 热更新
  ],
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

package.json

"script": {
    "hot": "webpack-dev-server"
}

在模块中控制热更新,使用module.hotHMR API

if (module.hot) {
    module.hot.accept('./library.js', function() {
        // 对更新过的library模块做特殊处理
    })
}

部分loader内置了上面的功能所以不用特意去写响应热更新的代码块(手动热更新),如css-loader、vue-loader

构建目标targets

概念:https://webpack.docschina.org/concepts/targets/

告知 webpack 为 目标(target)指定一个环境。默认值为”browserslist”,如果没有找到 browserslist 的配置,则默认为”web”。

外部扩展externals

externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。相反,所创建的 bundle 依赖于那些存在于用户环境(consumer’s environment)中的依赖。此功能通常对 library 开发人员来说是最有用的,然而也会有各种各样的应用程序用到它。

一般减少依赖包打包体积会使用到该项。

ES6+转ES5(polyfill)

使用babel进行转换。需要安装babel-loader@babel/core(核心库,使babel能识别js代码内容转换成ast抽象语法树,再将语法树编译成目的语法)

npm install --save-dev babel-loader @babel/core
module.exports = {
    ...
    rules: [
      {
        test: /\.m?js$/, // 如果检测到js文件则使用babel-loader处理
        exclude: /node_modules/, // 除外
        use: {
          loader: "babel-loader", // webpack与babel做了打通 但是并没有处理js文件
          options: {
            presets: [
              [
                // 将ES6+代码转换为ES5, 如果使用预设环境配置需要安装@babel/preset-env和配置
                "@babel/preset-env",
              	{
                    targets: {
                      chrome: "67" // 目标最低版本
                    },
                	useBuiltIns: "entry" // usage -- 仅转译使用过的代码 按需引入polyfill
              	}
              ]
            ]
          }
        }
      }
    ]
    ...
}

@babel/preset-env预设环境,在babel处理的时候就根据目标的环境能支持的语法和功能进行代码转换,如IE>9

浏览器或electron项目需要创建.browserslist 文件进行配置或在package.json中配置preset属性

具体配置内容可查看@babel/preset-env 文档

使用@babel/polyfill补充低版本浏览器不支持的语法。进行安装npm install --save @babel/polyfill。要配合@babel/preset-env和 useBuiltIns option配置使用,使用时在全局入口文件顶部导入import "@babel/polyfill"; 但如果使用usage模式可以不用手动引入。

如果库进行兼容编译则需要引入@babel/plugin-transform-runtime,需要polyfill则要设置core-js@3避免全局污染

{
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "absoluteRuntime": false,
        "corejs": 3,
        "helpers": true,
        "regenerator": true,
        "version": "7.0.0-beta.0"
      }
    ]
  ]
}

babel相关配置可以直接抽取到.babelrc配置文件中

执行

webpack-cli

命令行配置,CLI 中传入的任何参数会在配置文件中映射为对应的参数。在命令行中可以设置webpack配置中的属性值。

常用配置命令:

--config: 指定一个 Webpack 配置文件的路径;

--mode: 指定打包环境的 mode, 取值为 development 和 production , 分别对应着开发环境和生产环境;

--json: 输mode出 Webpack 打包的结果, 可以使用 webpack –json > stats.json 方式将打包结果输出到指定的文件;

--progress: 显示 Webpack 打包进度;

--watch, -w: watch 模式打包, 监控文件变化之后重新开始打包;

--color, --colors / –no-color, --no-colors: 控制台输出的内容是否开启颜色;

--hot: 开启 Hot Module Replacement模式, 后面会详细介绍;

--profile: 会详细的输出每个环节的用时( 时间) , 方便排查打包速度瓶颈。

常用命令:

  • 打包过程输出进度和颜色:webpack --progress --colors
  • 出错时打印错误详情:webpack --display-error-details
  • 开启监听模式:webpack --watch
  • 环境打包:-d-p 分别代表开发环境和生产环境的打包

通过配置文件执行

npx webpack --config webpack.config.js

--config 选项只是向你表明,可以传递任何名称的配置文件。

默认配置文件名webpack.config.js

设置环境/mode变量

参考链接:Environment 选项

production模式默认压缩,development模式默认不进行压缩

# 方式一 env到文件中解析的结果是字符串(一个env参数) 或 数组(多个env参数) 如['NODE_ENV=local', 'production']
npx webpack --env NODE_ENV=local --env production --progress

# 方式二 env到文件中解析的结果是对象
npx webpack --env.NODE_ENV=local --env.production --progress

结果:webpack.config.js

const path = require('path');

module.exports = env => {
  // Use env.<YOUR VARIABLE> here:
  console.log('NODE_ENV: ', env.NODE_ENV); // 'local'
  console.log('Production: ', env.production); // true

  return {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
  };
};

只设置编译mode

npx webpack --mode=development
# 或 npx webpack --mode development
# mode = 'production': 'none' | 'development' | 'production'
# 默认mode = 'production'

webpack.config.jsprocess.env.NODE_ENV会获取到mode

或者配置package.json中的script执行命令设置mode

{
    ...
    "script": {
        "dev": "webpack-dev-server --mode=development --env.server=dev",
    	"build-test": "webpack --mode=production --env.server=test",
    	"build-pro": "webpack --mode=production --env.server=pro"
    }
    ...
}

mode获取的方式和env取值的方式不同,mode两种方式都会返回mode对象。

mode对象中的mode值为设置的值,所以设置mode参数webpack会将设置的参数值合并到预设的mode对象中,如果设置值不是提供的mode参数,则会报错。

可以将不同模式下的配置抽取出来放在不同的文件中,使用webpack-merge进行合并。

如果想设置 node 环境值(process.env),则 window 下需要使用 cross-env 包进行设置,而 Linux 和 Mac 下可以不用进行特殊处理。参考链接:https://stackoverflow.com/questions/25112510/how-to-set-environment-variables-from-within-package-json

优化(optimization)

module.exports = {
    optimization: {
        usedExports: true // production模式下自动配置了,可以省略
    }
}

package.json

{
    "sideEffects": false
}

sideEffects配置tree shaking模式进行打包。

导入某些文件未进行显式使用,但实际有用如import "@babel/polyfill",则需要在sideEffects中配置。

{
    "sideEffects": [
        "@babel/polyfill",
        "*.css" // 防止css模块化文件被tree shaking 
    ]
}

development模式下即使使用了tree shaking,但是不会去除掉。

参考链接:使用Webpack4优化Web性能

代码分割(Code Splitting)

代码分割与webpack无关。

手动进行代码分割,创建子文件。

使用webpack自带的插件 split-chunk-plugin 进行代码分割。

module.exports = {
  //...默认配置
  optimization: {
    splitChunks: {
      chunks: 'async', // 'all' | 'initial'
      minSize: 20000, // 生成chunk的最小体积
      minRemainingSize: 0,
      maxSize: 0,
      minChunks: 1, // 拆分前必须共享模块的最小chunks数,被多个chunk文件引入的次数
      maxAsyncRequests: 30, // 按需加载时的最大并行请求数
      maxInitialRequests: 30, // 入口点的最大并行请求数
      enforceSizeThreshold: 50000,
      cacheGroups: { 
        defaultVendors: {
          test: /[\\/]node_modules[\\/]/, // 如果是node_modules引入的则打包出来
          priority: -10, // 优先级
          reuseExistingChunk: true,
        },
        default: { // 默认分割打包
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true, // 如果模块之前被打包过,则直接使用之前打包的
        },
      },
    },
  },
};

文件进行分割时,从上到下开始匹配,如果到cacheGroups之后都能满足条件,则会进行优先级高的分割方式进行分割。否则不会进行代码分割。

动态导入。使用import异步加载模块。无需做任何配置,会自动进行代码分割。

function getComponent() {
    return import('lodash').then(({ default: _ }) => {
        var element = document.createElement('div')
        element.innerHTML = _.join(['a', 'b'], '-')
        return element
    })
}

getComponent().then(element => {
    document.body.appendChlid(element)
})

可以使用魔法注释的方法去命名代码分割后的文件名,否则默认索引。

import(/* webpackChunkName: “lodash” */ ‘lodash’)

懒加载(Lazy Loading)

通过import异步加载,实现懒加载,在触发时才进行加载,提升页面初加载速度。

function getComponent(){
  return import(/* webpackChunkName: "lodash" */ 'lodash').then(({ default: _ }) => {
    var element = document.createElementNS('div');
    element.innerHTML = _.join(['a', 'b'], '-')
    return element
  })
}

document.addEventListener('click', () => {
  getComponent().then(element => {
    document.body.appendChild(element)
  })
})

// 使用async await进行改写异步
async function getComponent() {
    const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash')
    const element = document.createElement('div')
    element.innerHTML = _.join('a', 'b')
    return element
}

路由实现懒加载。

webpack没有懒加载概念,只是能识别import语法进行代码分割。

异步打包代码在首屏渲染时加载的数据更少更快,所以webpack推荐异步打包代码(chunks: 'async')。

打包分析

分析输出结果来检查模块在何处结束。

  • 官方分析工具webpack --profile --json > stats.json,将分析结果放到stats.json文件中然后在分析网站上进行分析。
  • webpack-chart: webpack stats 可交互饼图。
  • webpack-visualizer: 可视化并分析你的 bundle,检查哪些模块占用空间,哪些可能是重复使用的。
  • webpack-bundle-analyzer:一个 plugin 和 CLI 工具,它将 bundle 内容展示为一个便捷的、交互式、可缩放的树状图形式。(常用)
  • webpack bundle optimize helper:这个工具会分析你的 bundle,并提供可操作的改进措施,以减少 bundle 的大小。
  • bundle-stats:生成一个 bundle 报告(bundle 大小、资源、模块),并比较不同构建之间的结果。

预获取/预加载模块 (Preloading / Prefetching)

提升代码利用率,但有浏览器版本兼容问题。

在声明 import 时,使用下面这些内置指令,可以让 webpack 输出 “resource hint(资源提示)”,来告知浏览器:

  • prefetch(预获取):将来某些导航下可能需要的资源
  • preload(预加载):当前导航下可能需要资源

prefetch示例:在组件中有个需要点击后按需加载的组件

// 在组件中声明
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');

这会生成 <link rel="prefetch" href="login-modal-chunk.js"> 并追加到页面头部,指示着浏览器在闲置时间预取 login-modal-chunk.js 文件。

只要父 chunk 完成加载,webpack 就会添加 prefetch hint(预取提示)。

prefetchpreload区别

  • preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
  • preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
  • preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
  • 浏览器支持程度不同。

preload示例:图表组件 ChartComponent 组件需要依赖一个体积巨大的 ChartingLibrary 库。它会在渲染时显示一个 LoadingIndicator(加载进度条) 组件,然后立即按需导入 ChartingLibrary

// ChartingLibrary组件
import(/* webpackPreload: true */ 'ChartingLibrary');

CSS代码分割

使用MiniCssExtractPlugin 插件将CSS提取到单独的文件中,为每个包含CSS的JS文件创建一个CSS文件,并且支持CSS和SourceMaps的按需加载。

与 extract-text-webpack-plugin 相比:

  • 异步加载
  • 没有重复的编译(性能)
  • 更容易使用
  • 特别针对 CSS 开发

安装 mini-css-extract-plugin

npm install --save-dev mini-css-extract-plugin

js文件中引入css文件:

import './style.css';

注意tree shaking的配置,会使css无法打包出来

webpack.config.js配置:

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  plugins: [new MiniCssExtractPlugin(
  	{
        fileName: '[name].css',
        chunkFilename: '[name].chunk.css'
    }
  )],
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
    ],
  },
};

要在css loader的最后一步使用MiniCssExtractPlugin.loader进行处理css

将多个CSS文件打包到一个CSS文件中

module.exports = {
    optimization: {
        cacheeGroups: {
            styles: {
                name: 'styles',
                test: /\.css$/,
                chunks: 'all',
                enforce: true
            }
        }
    }
}

使用CssMinimizerWebpackPlugin 进行CSS代码压缩和cssnano优化

安装css-minimizer-webpack-plugin

npm install css-minimizer-webpack-plugin --save-dev

webpack.config.js配置:

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  module: {
    loaders: [
      {
        test: /.s?css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
      },
    ],
  },
  optimization: {
    minimize: true,
    minimizer: [
      // 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 `terser-webpack-plugin`),将下一行取消注释
      // `...`,
      new CssMinimizerPlugin(),
    ],
  },
};

预置依赖(Shimming)

改变webpack默认打包的一些行为,或补充webpack未实现的效果

使用ProvidePlugin 插件自动导入模块。

const webpack = require('webpack')

module.exports = {
    plugins: [
        new webpack.ProvidePlugin({
            $: 'jquery',
  			jQuery: 'jquery',
            _join: ['lodash', 'join'] // 类似import { join as _join } from 'lodash'
        })
    ]
}

如果在某个模块中使用了’$’字符串则会在模块中自动引入jquery模块并取名’$’

将每个js文件的this默认为当前js模块自身,修改this指向window,,则需要使用imports-loader进行修改。

安装imports-loadernpm install imports-loader --save-dev

webpack.config.js

module.exports = {
    module: {
        rules: [{
            test: /\.js$/,
            exclude: /node_modules/,
            user: [
                {
                    loader: 'babel-loader' // 对ES6语法做解析
                },
                {
                    loader: 'imports-loader?this=>window'
                }
            ]
        }]
    }
}

WebpackDevServer

安装npm install webpack-dev-server --save-dev

module.exports = {
    devServe: { // 在开发环境下才有用
        constentBase: './dist',
        open: true,
        port: 8080,
        hot: true,
        hotOnly: true,
        proxy: { // 实现请求转发
            '/api': {
                target: 'https://xxxx.cn',
                pathRewrite: {
                    'header.json': 'demo.json' // 如果请求header.json数据其实是加载demo.json的数据
                }
            }
        }
    }
}

单页面应用配置historyApiFallback 响应html页面

// 只在开发环境下使用,如果需要线上转发同样有效需要配置nginx或tomcat
module.exports = {
  //...
  devServer: {
    historyApiFallback: true, // 当使用h5 history api时,所有404请求都会响应index.html的内容
  },
};

// 也可以设置对象
module.exports = {
  //...
  devServer: {
    historyApiFallback: {
      rewrites: [ // 特殊情况转向其他页面
        { from: /^\/$/, to: '/views/landing.html' },
        { from: /^\/subpage/, to: '/views/subpage.html' },
        { from: /./, to: '/views/404.html' },
      ],
    },
  },
};

更多的参考配置内容 connect-history-api-fallback

ESLint配置

在项目目录下安装ESLintnpm install eslint --save-dev

初始化ESLint配置文件:npx eslint --init,使用npx避免全局ESLint配置污染问题。

使用ESLint手动检测代码是否规范:npx eslint src

有的时候不识别react或最新的ES新特性可以设置指定解析器 进行安装和配置。

.eslintrc.js进行配置

module.exports = {
  parser: "@babel/eslint-parser",
};

webpack中配置eslint

会降低打包的速度,可以只在开发环境下使用,配合git仓库的钩子进行判断提交代码是否符合规则。

(已过时)安装eslint-loadernpm install eslint-loader --save-dev

webpack.config.js中配置

module.export = {
    ...
    devServer: {
        overlay: true // 直接将错误信息显示在浏览器上
        ...
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: ['babel-loader', 'eslint-loader']
            }
        ]
    }
}

更新使用EslintWebpackPlugin 解决了eslint-loader 的一些问题。

需要先进行安装eslint-webpack-pluginnpm install eslint-webpack-plugin --save-dev

// webpack.config.js 中使用 EslintWebpackPlugin
const ESLintPlugin = require('eslint-webpack-plugin');

module.exports = {
  // ...
  plugins: [new ESLintPlugin(options)],
  // ...
};

提升webpack打包速度

  1. 升级版本(nodenpmyarnwebpack版本)

  2. 在尽可能少的模块上应用loader,合理使用excludeinclude语法去优化loader

  3. Plugin尽可能精简并确保可靠性,避免冗余,选择性能好的插件,最好使用官方推荐的插件。

  4. resolve参数合理配置

    // webpack.config.js
    module.expotys = {
        resolve: {
            extensions: ['.js', '.jsx'], // 配置模块如何解析 文件查找 导入的时候可以省略文件后缀
            mainFilees: ['index'], // 默认
            alias: { // 别名
                @: '../src'
            }
        }
    }
  5. 使用DllPlugin提高打包速度。

    1. 将第三方模块单独做一次打包,打包成单独的库,使用DllPlugin进行分析生成映射文件。

      // webpack.dll.js
      const path = require('path');
      const webpack = require('webpack');
      
      module.exports = {
          mode: 'production',
          entry: {
              vendors: ['lodash'],
              react: ['react', 'react-dom'],
              jquery: ['jquery']
          },
          output: {
              filename: '[name].dll.js',
              path: path.resolve(__dirname, '../dll'),
              library: '[name]' // 将[name]为全局变量暴露出去
          },
          plugins: [
              new webpack.DllPlugin({ // 对暴露的文件进行分析,生成映射文件
                  name: '[name]',
                  path: path.resolve(__dirname, '../dll/[name].manifest.json')
              })
          ]
      }
    2. 项目中使用add-asset-html-webpack-plugin 将打包后的库加载到项目中,使用DllReferencePlugin插件对映射进行解析。

      // webpack.commo.js
      module.exports = {
          plugins: [
              new AddAssetHtmlWebpackPlugin({ // 需要引入多少个文件就需要使用几次插件
                  filepath: '../dll/venders.dll.js'
              }),
              new webpack.DllReferencePlugin({
                  manifeat: '../dll/vendors.maifeat.json'
              })
              ...
          ]
      }
  6. 控制包文件大小。

  7. thread-loaderparallel-webpackhappypack多进程打包。

  8. 合理使用sourceMap

  9. 结合stats分析打包结果。

  10. 开发环境内存编译。

  11. 开发环境无用插件剔除。

  12. 使用CDN进行导入第三方库,配置externals属性进行全局使用。

还有很多插件或loader的配置可以提升性能,如cache配置

处理多页面打包HtmlWebpackPlugin

需要修改entry配置和html-webpack-plugin 的配置

多个输出文件需要多个html-webpack-plugin配置

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

module.exports = {
    entry: { // 入口
        a: 'src/a.js',
        b: 'src/b.js'
    },
    plugins: [
        new HtmlWebpackPlugin({
      		filename: 'a.html',
      		template: 'src/index.html',
            chunks: ['a.js']
        });
        new HtmlWebpackPlugin({
      		filename: 'b.html',
      		template: 'src/index.html',
        	chunks: ['b.js'] // 引入的js文件
        });
    ]
}
// 会输出多个文件,且相对应的引入入口文件。(生成多个文件,一对一)

如果多个入口文件,但只设置了一个HtmlWebpackPlugin则只会生成一个文件,未设置chunks,会都引入入口文件。(生成一个文件,多对一)

如果只设置一个入口文件,但设置多个HtmlWebpackPlugin则会生成多个文件,未设置chunks,且都引入入口文件。(生成多个文件,一对多)

HtmlWebpackPlugin插件可以设置js文件引入位置,默认引入位置为body底部,可以设置为head标签底部引入。

可以自定义设置js/css引入位置:在基础HtmlWebpackPlugin上使用html-webpack-injector (支持两个chunks分别引入位置)

HtmlWebpackInjector会有一个问题,分开的时候会把meta注入到中,解决方法可以直接将meta信息写在pug模板中,不使用HtmlWebpackPlugin进行配置动态设置

问题

浏览器缓存失效问题,[contenthash][hash]未修改代码hash不一致问题(仅修改业务代码,但是库代码也重新打包)。可能是webpack版本过旧,需要将mainfest(库与业务代码的关系)提取到runtime中,进行以下配置:

module.exports = {
    optimization: {
        runtimeChunk: {
            name: 'runtime'
        }
    }
}

更多阅读

实验项目地址:https://github.com/Datura35422/study/tree/master/webpack-demos

webpack-v4.x

使用 webpack 定制前端开发环境 - 看云

webpack - 实战总结