阅读 1396
Webpack5,了解从0到1搭建一个项目的细节

Webpack5,了解从0到1搭建一个项目的细节

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

前言

本篇文章不只是搭建webpack5的项目,还有个更重要的目的是,为了在搭建过程中了解每个插件,loader的作用,为什么要使用这些东西,这些东西带来了什么。

我为本项目写了一个cli工具:fight-react-cli,想直接看最终结果的同学,安装一下这个cli工具,初始化一个项目就能查看全部的配置。

准备工作

首先我们创建一个项目webpack-demo,然后初始化npm,然后在本地安装webpackwebpack-cli

mkdir webpack-demo
cd webpack-demo
npm init -y
npm install webpack webpack-cli --save-dev
复制代码

安装的webpack包则是webpack的核心功能包,webpack-cli则是webpack的命令行工具,可以在终端中使用webpack命令启动项目和打包项目。

然后我们在项目的根目录下创建一个文件夹webpack,在这个文件夹中创建三个文件用以区分环境:

webpack.common.js // 公用配置

webpack.dev.js // 开发时的配置

webpack.prod.js // 打包构建时的配置
复制代码

1637049449030.jpg

然后在根目录创建src文件夹,在src文件夹下面创建index.js:

// src/index.js

const el = document.getElementById('root');
el.innerHTML = 'hello webpack5';
复制代码

基本配置

我们在webpack文件夹下的webpack.common.js中来写基本的配置:

// webpack/webpack.common.js

const path = require('path');

module.exports = (webpackEnv) => {
    const isEnvDevelopment = webpackEnv === 'development';
    const isEnvProduction = webpackEnv === 'production';

    return {
      mode: webpackEnv, 
      entry: './src/index.js',
      output: {
        filename: 'main.js',
        path: path.resolve(__dirname, 'dist'),
      },
      module: {
          rules: []
      },
      plugins: [],
    };
};
复制代码

这里我们导出了一个函数,函数中返回了webpack的配置信息。当这个函数被调用时,会传入当前运行的环境标识webpackEnv,它的值是development或者production,并将webpackEnv赋值给了mode,用于根据不同模式开启相应的内置优化,还有个作用则是根据不同环境自定义开启不同的配置,在后续配置中会用到。

在配置信息中是webpack的5大基本模块:

  1. mode:模式,通过选择:development,production,none这三个参数来告诉webpack使用相应模式的内置优化。
  2. entry:设置入口文件。
  3. output:告诉wenpack打包出的文件存放在哪里
  4. module.rules:loader(加载器),webpack本身只支持处理js,json文件,要想能够处理其它类型的文件,如:css,jsx,ts,vue等,则需要相应的loader将这些文件转换成有效的模块。
  5. plugins:插件,loader用于处理不支持的类型的文件,而plugin则可以用于执行范围更广的任务,如:压缩代码(new TerserWebpackPlugin()),资源管理(new HtmlWebPackPlugin()),注入环境变量(new webpack.DefinePlugin({...}))等。

配置webpack-dev-server

基本配置完成了,我们现在想要让代码运行起来,并且当代码修改后可以自动刷新页面。

首先先安装webpack-dev-server

npm install --save-dev webpack-dev-server
复制代码

安装完成后,我们进入webpack.dev.js中来添加开发时的配置:

const webpackCommonConfig = require('./webpack.common.js')('development');

module.exports = {
  devServer: {
    host: 'localhost', // 指定host,,改为0.0.0.0可以被外部访问
    port: 8081, // 指定端口号
    open: true, // 服务启动后自动打开默认浏览器
    historyApiFallback: true, // 当找不到页面时,会返回index.html
    hot: true, // 启用模块热替换HMR,在修改模块时不会重新加载整个页面,只会更新改变的内容
    compress: true, // 启动GZip压缩
    https: false, // 是否启用https协议
    proxy: { // 启用请求代理,可以解决前端跨域请求的问题
      '/api': 'www.baidu.com',
    },
  },
  ...webpackCommonConfig,
};
复制代码

在这里我们首先引入了webpack.common.js,上面我们介绍了这个文件导出一个函数,接收环境标识作为参数,这里我们传入的是development,然后将返回的配置对象webpackCommonConfig,与开发时的配置进行了合并。

配置html-webpack-plugin

html-webpack-plugin的作用是生成一个html文件,并且会将webpack构建好的文件自动引用。

npm install --save-dev html-webpack-plugin
复制代码

安装完成后,在webpack.common.js中添加该插件:

// webpack/webpack.common.js

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

module.exports = (webpackEnv) => {
    const isEnvDevelopment = webpackEnv === 'development';
    const isEnvProduction = webpackEnv === 'production';

    return {
      mode: webpackEnv, 
      entry: './src/index.js',
      output: {
        filename: 'main.js',
        path: path.resolve(__dirname, 'dist'),
      },
      module: {
          rules: []
      },
      plugins: [
          new HtmlWebpackPlugin(),
      ],
    };
};
复制代码

html-webpack-plugin还可以添加一个模板文件,让html-webpack-plugin根据模板文件生成html文件。

我们在根目录下创建一个public文件夹,在文件夹下创建一个index.ejs:

// public/index.ejs

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="Web site created using create-react-app" />
    <title>Webpack5</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

复制代码

然后在插件中引入模板:

// webpack/webpack.common.js

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

module.exports = (webpackEnv) => {
   ...

    return {
      ...
      plugins: [
          new HtmlWebpackPlugin({
              template: path.resolve(__dirname, '../public/index.ejs')
          }),
      ],
    };
};
复制代码

注意::这里我引用.html后缀的模板,html-webpack-plugin始终无法正常的生成html,然后改为了.ejs后就正常了。

在package.json中配置启动,打包命令

然后我们在package.js中来配置启动和打包的命令:

{
  "name": "fcc-template-typescript",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "build": "webpack --config ./webpack/webpack.prod.js",
    "start": "webpack server --config ./webpack/webpack.dev.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  ...
}
复制代码

我们在scripts中添加了build和start命令,build用于打包发布,start用以开发时启动项目。

然后我们命令行中进入到项目的更目录下,运行:npm start 或者 yarn start命令来启动项目。

加载CSS

我们知道webpack本身只支持处理js和json类型的文件,如果我们想处理其它类型的文件,则需要使用相应的loader

提前列出需要使用到的loader:

  1. style-loader
  2. css-loader
  3. postcss-loader

安装:

npm install --save-dev style-loader css-loader postcss-loader postcss postcss-preset-env
复制代码

对于css文件,则需要添加:css-loader

webpack.common.js

// webpack/webpack.common.js
...

module.exports = (webpackEnv) => {
   ...

    return {
        ...
        module: {
            rules: [
                {
                    test: /.css$/i,
                    use: ["css-loader"],
                },
            ],
        },
    };
};
复制代码

index.js

// src/index.js

import './index.css';
复制代码

index.css

#root {
  color: red;
}
复制代码

此时我们运行发现文字并没有添加颜色,这是为什么?

因为css-loader只负责解析css文件,解析完成后会返回一个包含了css样式的js对象:

1637054164915.jpg

我们需要css样式生效,则需要将css样式插入到dom中,那么又需要安装自动插入样式的loader:style-loader

webpack.common.js

// webpack/webpack.common.js
...

module.exports = (webpackEnv) => {
   ...

    return {
        ...
        module: {
            rules: [
                {
                    test: /.css$/i,
                    use: ["style-loader", "css-loader"],
                },
            ],
        },
    };
};
复制代码

这里需要注意,loader的执行顺序是倒序执行(从右向左或者说从下向上),我们需要先使用css-loader解析css生成js对象后,将css对象交给style-loaderstyle-loader会创建style标签,将css样式抽取出来放在style标签中,然后插入到head中。

在不同浏览器上css的支持是不一样的,所以我们需要使用postcss-loader来做css的兼容: webpack.common.js

module: {
    rules: [
        {
            test: /.css$/i,
            use: [
                "style-loader", 
                "css-loader",
                 {
                    // css兼容性处理
                    loader: 'postcss-loader',
                    options: {
                      postcssOptions: {
                        plugins: [
                          [
                            'postcss-preset-env',
                            {
                              autoprefixer: {
                                flexbox: 'no-2009',
                              },
                              stage: 3,
                            },
                          ],
                        ],
                      },
                    }
                },
            ],
        },
    ],
},
复制代码

在postcss中使用了postcss-preset-env插件来自动添加前缀。

加载image图像

在webpack5之前我们使用url-loader来加载图片,在webpack5中我们使用内置的Asset Modules来加载图像资源。

在 webpack 5 之前,通常使用:

资源模块类型(asset module type),通过添加 4 种新的模块类型,来替换所有这些 loader:

  • asset/resource 发送一个单独的文件并导出 URL。之前通过使用 file-loader 实现。
  • asset/inline 导出一个资源的 data URI。之前通过使用 url-loader 实现。
  • asset/source 导出资源的源代码。之前通过使用 raw-loader 实现。
  • asset 在导出一个 data URI 和发送一个单独的文件之间自动选择。之前通过使用 url-loader,并且配置资源体积限制实现。

webpack.common.js

 module: {
   rules: [
    {
      test: /\.(png|svg|jpg|jpeg|gif)$/,
      type: 'asset', 
      generator: {
        filename: 'image/[name].[contenthash:8][ext][query]'
      }
    },
   ]
  },
复制代码

添加generator属性自定义文件名与文件存放位置。

也可以在output中定义assetModuleFilename设置默认存放位置与文件名格式:

output: {
  assetModuleFilename: 'asset/[name].[contenthash:8][ext][query]', 
}
复制代码

加载fonts字体或者其他资源

webpack.common.js

 module: {
   rules: [
    {
      exclude: /\.(js|mjs|ejs|jsx|ts|tsx|css|scss|sass|png|svg|jpg|jpeg|gif)$/i,
      type: 'asset/resource', 
    },
   ]
  },
复制代码

我们通过排除其他资源的后缀名来加载其他资源。

兼容js:将es6语法转换为es5

需要使用到的loader:

  1. babel-loader

安装:

npm install --save-dev babel-loader @babel/core @babel/preset-env
复制代码

需要用到的babel插件:

  1. @babel/plugin-transform-runtime
  2. @babel/runtime

安装:

npm install --save-dev @babel/plugin-transform-runtime
复制代码
npm install --save @babel/runtime
复制代码

webpack.common.js

 module: {
   rules: [
    {
        test: /\.js$/,
        include: path.resolve(__dirname, './src'),
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                [
                    "@babel/preset-env",
                    {
                        "useBuiltIns": "usage",
                        "corejs": 3,
                    }
                ]
              ],
            }
          },
        ],
      },
   ]
  },
复制代码

这里我们会使用babel的插件:@babel/preset-env,它是转译插件的集合。

比如说我们使用了箭头函数,浏览器是不识别的需要转译成普通函数,那么我们就需要添加babel插件:@babel/plugin-transform-arrow-functions来处理箭头函数,如果我们使用了很多es6的api,都需要手动添加插件,这样会非常麻烦,babel为了简便开发者的使用,将所有需要转换的es6特性的插件都集合到了@babel/preset-env中。

在使用@babel/preset-env我们需要配置corejs属性,什么是corejs?

babel只支持最新语法的转换,比如:extends,但是它没办法支持最新的Api,比如:Map,Set,Promise等,需要在不兼容的环境中也支持最新的Api那么则需要通过Polyfill的方式在目标环境中添加缺失的Api,这时我们就需要引入core-js来实现polyfill。

useBuiltIns则是告诉babel怎么引入polyfill。

当选择entry时,babel不会引入polyfill,需要我们手动全量引入:

import "core-js"; 

var a = new Promise();
复制代码

当选择usage时,babel会根据当前的代码自动引入需要的polyfill:

import "core-js/modules/es.promise"; 

var a = new Promise();
复制代码

但是我们发现这样使用polyfill,会污染全局对象,如下:

"foobar".includes("foo");

使用polyfill后,会在String的原型对象上添加includes方法:

String.prototype.includes = function() {}
复制代码

如果我们使用了其它插件也在原型对象上添加了同名方法的,那就会导致出现问题。

这时我们则可以使用@babel/plugin-transform-runtime插件,通过引入模块的方式来实现polyfill:

 module: {
   rules: [
    {
        test: /\.js$/,
        use: [
          {
            loader: 'babel-loader',
            options: {
              presets: [
                "@babel/preset-env",
              ],
              plugins: [
                [
                  '@babel/plugin-transform-runtime',
                  {
                    "helpers": true, 
                    "corejs": 3,
                    "regenerator": true,
                  }
                ]
              ],
            }
          },
        ],
      },
   ]
  },
复制代码

我们来看下效果:

"foobar".includes("foo");
复制代码

转译后:

var _babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_1___default = __webpack_require__.n(_babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_1__);


_babel_runtime_corejs3_core_js_stable_instance_includes__WEBPACK_IMPORTED_MODULE_1___default()(_context = "foobar").call(_context, "foo");
复制代码

可以看到转译后includes的实现是通过调用了runtime—corejs3中的includes方法。

通过上面我们知道了@babel/plugin-transform-runtime的作用,我们再来看看它常用的配置属性。

helpers,我们将helpers先设置为false来看看编译后的效果。

class Test {}
复制代码

转译后:

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Test = function Test() {
  _classCallCheck(this, Test);
};
复制代码

我们看到在转译后,在顶部添加了一个_classCallCheck工具函数,如果打包后有多个文件,每个文件中都是用了class,那么在顶部都会生成同样的_classCallCheck工具函数,这会使我们最后打包出来的文件体积变大。

我们将helpers设置为true,再来看转译后的效果:

var _babel_runtime_corejs3_helpers_classCallCheck__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);

var Test = function Test() {
  (0,_babel_runtime_corejs3_helpers_classCallCheck__WEBPACK_IMPORTED_MODULE_0__["default"])(this, Test);
};
复制代码

我们看到_classCallCheck函数通过模块的方式被引入,这样就使babel通用的工具函数能够被复用,从而减小文件打包后的体积。

corejs:指定依赖corejs的版本进行polyfill。

regenerator:在我们使用generate时,会在全局环境上注入generate的实现函数,这样会造成全局污染,将regenerator设置true,通过模块引入的方式来调用generate,避免全局污染:

function* test() {
  yield 1;
}
复制代码

regenerator设置为false时:

function test() {
  return regeneratorRuntime.wrap(function test$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return 1;

        case 2:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}
复制代码

可以看到regeneratorRuntime这个对象是直接使用的,并没有引入,那么它肯定就是存在于全局环境上。

regenerator设置为true时:

function test() {
  return _babel_runtime_corejs3_regenerator__WEBPACK_IMPORTED_MODULE_0___default().wrap(function test$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          _context.next = 2;
          return 1;

        case 2:
        case "end":
          return _context.stop();
      }
    }
  }, _marked);
}
复制代码

可以看到,这次使用的generate函数是从runtime-corejs3中导出引用的。

注意:还需要在package.json中配置目标浏览器,告诉babel我们要为哪些浏览器进行polyfill

// package.json

{
  "name": "webpack5",
  "version": "1.0.0",
  ...
  "browserslist": {
   // 开发时配置,针对较少的浏览器,使polyfill的代码更少,编译更快
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ],
    // 生产的配置,需要考虑所有支持的浏览器,支持的浏览器越多,polyfill的代码也就越多
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ]
  }
}

复制代码

进阶配置

完成了webpack的基本配置后,我们再来配置一些更高级的。

加载css modules

什么是css modules?

我是这么理解的:每个css文件都有自己的作用域,css文件中的属性在该作用域下都是唯一的

在我们有多个组件时,每个组件都有相对应的css文件,其中的属性名称难免会有重名的,我们直接使用的话,后者则会覆盖前者,只会有一个样式生效。我们通过css modules对属性名通过hash值或者路径字符串的形式进行重命名,保证每个属性名都是唯一的,只会作用在本身的组件上,而不会影响到其它组件。

直接在css-loader中添加modules属性:

{
    test: /\.module\.css$/,
    use: [
        ...
        {
            loader: 'css-loader',
            options: {
                modules: {
                  localIdentName: '[hash:base64:8]',
                }
            }
        }
    ],
},
复制代码

加载sass

sass是一款强化css的辅助工具,它在css基础上增加了变量,嵌套,混合,导入等功能,能够使我们更好的管理样式文件,更高效的开发项目。

安装:

npm install sass-loader sass --save-dev
复制代码

在webpack中需要添加sass-loader,来对sass文件进行处理,将sass文件转化为css文件。

{
    test: /\.(scss|sass)$/,
    use: [
        ...
        'sass-loader'
    ],
},
复制代码

配置React

我们在写react代码的时候,使用了jsx语法,但是浏览器并不认识jsx语法,我们需要先对jsx语法进行转换为浏览器认识的语法React.createElement(...)

需要的babel插件:

  1. @babel/preset-react

安装:

npm install --save-dev @babel/preset-react
复制代码

使用:

{
  loader: 'babel-loader',
  options: {
    presets: [
      "@babel/preset-env",
      [
        "@babel/preset-react",
        {
          runtime: 'automatic',
        }
      ],
    ],
  }
},
复制代码

在以前旧版本中,我们在使用jsx语法时,必须要引入:

import react from 'react';
复制代码

在最新的版本中,我们将runtime设置为automatic,就可以省略这一步,babel会自动为我们导入jsx的转换函数。

配置Typescript

浏览器是不支持ts的语法的,我们需要先将ts文件进行编译,转换为js后浏览器才能够识别。

安装:

npm install --save-dev @babel/preset-typescript
复制代码

使用:

{
  loader: 'babel-loader',
  options: {
    presets: [
      "@babel/preset-env",
      [
        "@babel/preset-react",
        {
          runtime: 'automatic',
        }
      ],
      "@babel/preset-typescript",
    ],
  }
},
复制代码

还需要在项目根目录下添加tsconfig.json:

{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
  },
  "include": [
    "src",
  ]
}
复制代码

具体配置可以查看ts官网。

配置ESLint

多人开发时,我们希望每个人写的代码风格都是统一的,那么则需要ESLint来帮助我们实现。

安装:

yarn add -D eslint eslint-webpack-plugin
yarn add -D @typescript-eslint/eslint-plugin @typescript-eslint/parser
yarn add -D eslint-config-airbnb eslint-config-airbnb-typescript
yarn add -D eslint-plugin-import eslint-plugin-jsx-a11y
yarn add -D eslint-config-prettier eslint-plugin-react eslint-plugin-react-hooks
复制代码

这里我们选用的是 eslint-config-airbnb 配置,它对 JSX、Hooks、TypeScript 及 A11y 无障碍化都有良好的支持,可能也是目前最流行、最严格的 ESLint 校验之一。

接下来,创建 ESLint 配置文件 .eslintrc.js:

module.exports = {
  root: true,
  parser: '@typescript-eslint/parser',
  extends: [
    'airbnb',
    'airbnb-typescript',
    'airbnb/hooks',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/recommended-requiring-type-checking',
    'plugin:react/jsx-runtime',
  ],
  parserOptions: {
    project: './tsconfig.json',
  },
  ignorePatterns: [".*", "webpack", "public", "node_modules", "dist"], // 忽略指定文件夹或文件
  rules: {
    // 在这里添加需要覆盖的规则
    "react/function-component-definition": 0,
    "quotes": ["error", "single"],
    "jsx-quotes": ["error", "prefer-single"]
  }
};
复制代码

到这里eslin配置完成,但是需要我们每次都运行命令去检查和修复代码的问题,这样比较麻烦,所以我们使用webpack的插件:eslint-webpack-plugin来自动查找和修复代码中的问题:

{
    plugins: [
        new ESLintPlugin({
          extensions: ['.tsx', '.ts', '.js', '.jsx'],
          fix: true, // 自动修复错误代码
        }),
    ]
}
复制代码

在命令行中提示ts的错误

在打包的过程中我们发现,代码中提示ts的错误依然能打包成功,这不是我们期望的结果。我们期望的是当ts在代码中显示错误,那么打包时也应该报错。

安装:

npm instal --save-dev fork-ts-checker-webpack-plugin
复制代码

使用:

const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

plugins: [
    new ForkTsCheckerWebpackPlugin({
        typescript: {
             configFile: path.resolve(__dirname, '../tsconfig.json')
        }
    });
]
复制代码

配置别名和扩展名

在引入文件的时候会这样写:

import demo from 'demo.js';
复制代码

我们可以在webpack中配置扩展名,之后再引入文件则可以省略文件的后缀名:

resolve: {
    extensions: ['.tsx', '.ts', '.jsx', '.js', '.json'],
}
复制代码

当我们在引入一些文件时,它的路径比较长,写起来非常麻烦,而且不易阅读, 比如:

import demo from './src/xxx/xx/components/demo';
复制代码

我们则可以在webpack中配置别名,来达到缩短文件路径的目的:

resolve: {
    alias: {
        'components': path.resolve(__dirname, '../src/xxx/xx/components/'),
    },
}
复制代码

之后我们就可以这样引入文件:

import demo from 'components/demo';
复制代码

优化

我们优化可以分为两个方面,一个是开发是的优化,一个是打包时的优化。

开发时的优化

sourcemap

在开发时,我们需要对代码进行调式和错误定位,希望能够准确的定位到源码的位置上,那么我们则需要配置sourcemap

webpack.common.js

devtool: 'cheap-module-source-map',
复制代码

配置好后,当代码报错,浏览器中就会显示报错的代码的准确信息。

配置缓存

当我们启动项目时,每次都会重新构建所有的文件,但是有的文件是长期不变的,比如说在node_modules中的文件,并不需要每次都重新构建。

那么我们就讲这些长期不变的文件进行缓存:

webpack.common.js

cache: {
  type: "filesystem", // 使用文件缓存
},
复制代码

在下一次启动的时候,webpack首先会去读取缓存中的文件,以此来提高构建的速度。

babel的缓存:babel的缓存特性也是和webpack是一样的,在构建时,首先回去读取缓存,以此提高构建的速度:

{
  loader: 'babel-loader',
  options: {
    cacheDirectory: true,
  }
}
复制代码

缓存的文件默认会在node_modules下的.cache文件夹下。

开启HRM模块热替换

什么是HRM?简单说就是当有模块被修改了,那么则会立即刷新这个模块,但是其他的模块不会刷新。

a.js -> b.js
复制代码

a文件中引用了b文件,在没有开启HRM的情况下,我们修改了b文件,那么整个页面都会刷新。

在开启了HRM后,修改了b文件,b文件会马上刷新,但是a文件是不会刷新的。

使用:

webapck.common.js

devServer: {
    hot: true
}
复制代码

在需要热更新的文件中添加以下代码:

if(module && module.hot) {
  module.hot.accept() // 接受自更新
}
复制代码

但是在我们开发过程中不可能每个文件手动添加,而且在打包上线的时候是不需要热更新的代码的。

所以出现了一些自动添加热更新函数的插件:

  • React Hot Loader: 实时调整 react 组件。
  • Vue Loader: 此 loader 支持 vue 组件的 HMR,提供开箱即用体验。
  • Elm Hot webpack Loader: 支持 Elm 编程语言的 HMR。
  • Angular HMR: 没有必要使用 loader!直接修改 NgModule 主文件就够了,它可以完全控制 HMR API。
  • Svelte Loader: 此 loader 开箱即用地支持 Svelte 组件的热更新。

对于React来说,已经不使用React Hot Loader这个loader,而是使用react-refresh.

安装:

yarn add -D @pmmmwh/react-refresh-webpack-plugin react-refresh
复制代码

使用:

const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); // react热更新


{
    module: {
        rules: [
            {
              test: /\.(js|jsx|ts|tsx)$/,
              include: path.resolve(__dirname, '../src'),
              use: [
                  {
                  loader: 'babel-loader',
                  options: {
                    plugins: [
                      isEnvDevelopment && 'react-refresh/babel',
                    ].filter(Boolean),
                  }
                },
              ]
            }
        ]
    },
    plugins: [
        isEnvDevelopment && new ReactRefreshWebpackPlugin(),
    ]
}
复制代码

babel-loadplugins中添加react-refresh/babel,然后在webpack的plugins中添加@pmmmwh/react-refresh-webpack-plugin。有一点需要注意,热更新只在开发环境开启,如果在生产环境开启了,会将热更新的代码一起打包,但是它对于我们生产环境的代码来说没有任何作用。

对于css的热更新来说,在我们使用的style-loader的内部已经实现了HRM。

打包时的优化

抽离css

mini-css-extract-plugin插件会将js中的css提取到单独的css文件中,并且支持css和sourcemaps的按需加载。

安装:

npm install --save-dev mini-css-extract-plugin
复制代码

使用:

const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 将css从js中分离为单独的css文件


{
    module: {
        rules: [
          {
            test: /\.css$/,
            use: [
                isEnvProduction ? 
                MiniCssExtractPlugin.loader: 
                'style-loader',
                'css-loader'
            ],
          },
       ]
  },
  
  plugins: [
      new MiniCssExtractPlugin(),
  ]
  
}
复制代码

通过环境区别,在开发环境使用style-loader,在生产环境使用mini-css-extract-plugin

代码分离

在开发过程中,同一个文件难免会被多个文件引用,在打包后,这个被引用的文件会重复存在于引用了它的文件当中,我们需要将它打包成独立的文件来达到复用的目的。

使用splitChunks:

{
    optimization: {
        splitChunks: {
            chunks: 'all'
        }
    }
}
复制代码

最小化入口chunk的体积

通过配置optimization.runtimeChunk,将入口文件中运行时的代码提出来单独创建一个chunk,减小入口chunk的体积。

{
    optimization: {
        runtimeChunk: 'single'
    }
}
复制代码

压缩js

通常压缩js代码我们会使用terser-webpack-plugin,在webpack5中已经内置了该插件,当modeproduction时会自动启用。

如果我们想自定义的话:

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimizer: [
        new TerserPlugin({...});
    ],
  },
};
复制代码

压缩css

我们会使用css-minimizer-webpack-plugin插件。

它与optimize-css-assets-webpack-plugin 相比,在 source maps 和 assets 中使用查询字符串会更加准确,支持缓存和并发模式下运行。

安装:

npm install css-minimizer-webpack-plugin --save-dev
复制代码

使用:

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

module.exports = {
  optimization: {
    minimizer: [
        new CssMinimizerPlugin();
        '...'
    ],
  },
};
复制代码

这里注意一点,如果我们只想添加额外的插件与默认插件一起使用,需要添加'...',表示添加默认插件。

dll

使用DllPlugin将不会频繁更改的代码单独打包生成一个文件,可以提高打包时的构建速度。

使用: 首先我们新建一个webapck.dll.js文件,将会不频繁更改的包添加在入口:

const paths = require('./paths');
const webpack = require('webpack');

module.exports = {

  mode: 'production',

  entry: {
    vendor: [
      'react',
      'react-dom'
    ]
  },

  output: {
    path: paths.dllPath,
    filename: paths.dllFilename,
    library: '[name]_dll_library'
  },

  plugins: [
    // 使用DllPlugin插件编译上面配置的NPM包
    // 会生成一个json文件,里面是关于dll.js的一些配置信息
    new webpack.DllPlugin({
      path: paths.dllJsonPath,
      name: '[name]_dll_library'
    })
  ]

};
复制代码

然后我们在package.json处添加打包命令:

{
  "name": "webpack5",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "scripts": {
    "dll": "webpack --config ./webpack/webpack.dll.js"
    ...
  },
  ...
}
复制代码

然后我们运行:npm run dll

最后会在项目根目录生成一个dll文件夹,其中会生成一个js文件,包含了我们需要单独打包的模块:reactreact-dom,并且还需生成一个包含被打包模块信息的json文件。

- dll
  - vendor.dll.js
  - dll.manifest.json
复制代码

然后我们还需要干两件事:

  1. 在上线打包时告诉webpack,不要将我们dll的模块进行打包
  2. 在打包成功后的js文件中是不会包含我们dll的模块,所以我们需要将dll出来的js文件引入。

webpack.common.js

我们使用DllReferencePlugin来排除dll的模块:

new webpack.DllReferencePlugin({
  manifest: paths.dllJsonPath
})
复制代码

我们需要将dll出来的json文件引入,json文件中包含了已经被打包的模块的信息,在webpack打包时就会排除这些模块。

然后我们使用add-asset-html-webpack-plugin来将dll出来的文件引入:

安装:

yarn add  -D add-asset-html-webpack-plugin
复制代码

使用:

const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');

new AddAssetHtmlWebpackPlugin({
  filepath: path.resolve(__dirname, '../dll/vendor.dll.js'),
  publicPath: ''
})
复制代码

这里需要注意一点,在我引入js文件后,进行了打包运行,但是发现在运行时找不到vendor.dll.js,查看了路径发现多了一层:auto,需要设置publicPath为空字符串解决。

tree shaking - 树摇

关于树摇,简单的理解的话就是再打包的时候将没有使用的js代码排除掉。

开启树摇:只需要将mode设置为production,tree shaking就会自动开启。

有些时候我们会开发一些插件,里面会有很多方法是提供给别人使用的,在插件内部是没有使用,在打包的时候就会被tree shaking掉。这时我们需要在package.js中声明一下sideEffects属性:

{
  "name": "webpack5",
  "version": "1.0.0",
  "description": "",
  "private": true,
  "sideEffects": ["*.js", "*.css"]
  ...
}
复制代码

通过sideEffects告诉webpack在我声明的文件中是有副作用的,不要进行tree shaking。

清除未使用的css

清除未使用的css,可以理解为csstree shaking,我们使用purgecss来实现。

因为我们经常会使用css模块,所以需要安装@fullhuman/postcss-purgecss

yarn add -D @fullhuman/postcss-purgecss
复制代码

使用:

 {
    loader: 'postcss-loader',
    options: {
      postcssOptions: {
        plugins: [
          ...
          isEnvProduction && 
          [
            '@fullhuman/postcss-purgecss', // 删除未使用的css
            {
              content: [ paths.appHtml, ...glob.sync(path.join(paths.appSrc, '/**/*.{tsx,ts,js,jsx}'), { nodir: true }) ],
            }
          ]
        ].filter(Boolean),
      },
    }
  },
复制代码

postcss-loader添加该插件就可以了。

多线程

我们可以在工作比较繁重,花费时间比较久的操作中,使用thread-loader开启多线程构建,能够提高构建速度。

安装:

npm install --save-dev thread-loader
复制代码

使用:

{
  test: /\.(js|jsx|ts|tsx)$/,
  include: paths.appSrc,
  use: [
    {
      loader: "thread-loader", 
      options: {
        workers: 2,
        workerParallelJobs: 2
      },
    },
  ]
}
复制代码

webpack 官网 提到 node-sass  中有个来自 Node.js 线程池的阻塞线程的 bug。 当使用  thread-loader  时,需要设置  workerParallelJobs: 2

打包前清空输出文件夹

在webpack5之前我们使用clean-webpack-plugin来清除输出文件夹,在webpack5自带了清除功能。

使用:

output: {
    clean: true,
}
复制代码

output中配置clean属性为true

懒加载

懒加载也可以叫做按需加载,本质是将文件中的不会立即使用到的模块进行分离,当在使用到的时候才会去加载该模块,这样的做法会大大的减小入口文件的体积,让加载速度更快。

使用方式则是用import动态导入的方式实现懒加载。

使用:

import('demo.js')
 .then({default: res} => {
     ...
 });
复制代码

当webpack打包时,就会将demo文件单独打包成一个文件,当被使用时才会去加载。

可以使用魔法注释去指定chunk的名称与文件的加载方式。

指定chunk名称:

import(/* webpackChunkName: "demo_chunk" */ 'demo.js')
 .then({default: res} => {
     ...
 });
复制代码

指定加载方式:

import(
   /* webpackPreload: "demo_chunk", webpackPrefetch: true */
   'demo.js'
)
 .then({default: res} => {
     ...
 });
复制代码

我们来看下两种加载方式的区别:

  1. prefetch:会在浏览器空闲时提前加载文件
  2. preload:会立即加载文件

使用CDN

打包完成后,可以将静态资源上传到CDN,通过CDN加速来提升资源的加载速度。

webpack.common.js

output: {
    publicPath: isEnvProduction ? 'https://CDNxxx.com' : '', 
},
复制代码

通过配置publicPath来设置cdn域名。

浏览器缓存

浏览器缓存,就是在第一次加载页面后,会加载相应的资源,在下一次进入页面时会从浏览器缓存中去读取资源,加载速度更快。

webpack能够根据文件的内容生成相应的hash值,当内容变化hash才会改变。

使用:

module.exports = {
  output: {
    filename: isEnvProduction
      ? "[name].[contenthash].bundle.js"
      : "[name].bundle.js",
  },
};
复制代码

只在生产环境开启hash值,在开发环境会影响构建效率。

优化工具

这里简单介绍一下:

  1. progress-bar-webpack-plugin:打包时显示进度,但是会影响打包速度,需要斟酌使用,如果打包时间过长可以使用,否则不需要使用。
  2. speed-measure-webpack-plugin:查看loader和plugin的耗时,可以对耗时较长的loader和plugin针对性优化。
  3. webpack-bundle-analyzer:可以查看打包后各个文件的占比,来针对性的优化。

注:以上都是webpack的plugin

总结

使用的loader

处理css的loader:

  1. style-loader:插入css样式到dom中
  2. css-loader:解析css文件生成js对象
  3. postcss-loader: 处理css兼容问题
    • postcss-preset-env:postcss预置插件,处理css兼容性,自动添加浏览器内核前缀
    • @fullhuman/postcss-purgecss: 清除未使用的css样式
  4. sass-loader:处理sass文件
  5. less-loader:处理less文件
  6. mini-css-extract-plugin: 使用该插件内部的loader,将css独立打包成一个文件,还需要添加该插件到plugins中。

处理js|ts的loader:

1.babel-loader: 处理js兼容,将es6转es5,对新Api进行polyfill。

presets - babel预置插件:

  • @babel/preset-env:bebal预置es6转es5的插件
  • @babel/preset-react:jsx语法转换
  • @/babel/preset-typescript: ts语法转js

plugins - babel插件

  • @babel/plugin-transform-runtime:将babel的工具函数以模块的方式引用以达到重复使用的目的。将polyfill的方法以模块的方式引用,来处理polyfill污染全局变量的问题。将genarate以模块的方式引用,来处理generate污染全局变量的问题。
  • react-refresh/babel:react组件热更新(HRM)

处理资源文件:图片,字体文件等的loader

  1. url-loader:处理图片,设置options.limite属性,小于该属性值的图片转为base64(内联),大于则将图片发送至输出目录。(内置file-loader)
  2. file-loader:处理文件,将文件发送至输出目录
  3. raw-loader:将文件导出为字符串

webpack5中使用内置的Asset Module

  1. asset/resource替代file-loader
  2. asset/inline替代url-loader
  3. asset/source替代raw-loader
  4. asset通过配置parser.dataUrlCondition.maxSize,来自动选择使用asset/resource(大于MaxSize)和asset/inline(小于maxSize)。

使用的webpack plugin

  1. html-webpack-plugin: 自动生成html文件,并且自动引用webpack构建好的文件。
  2. mini-css-extract-plugin: 分离css单独打包成一个文件。
  3. clean-webapck-plugin: 自动清除输出文件目录,被output.clean替代
  4. copy-webpack-plugin: 复制文件到另外一个文件夹
  5. compression-webpack-plugin: 压缩文件
  6. webpack.DefinePlugin:注入全局变量
  7. webpack.DllPlugin:生成打包一个含有打包模块信息的json文件
  8. webpack.DllReferancePlugin:使用Dllplugin生成的json文件使webpack在打包时排除json文件中包含的模块。
  9. add-asset-html-webpack-plugin:自定义添加bundle文件到html中,本文中是将dll生成的js文件,使用该插件添加到了html中。
  10. eslint-webpack-plugin:代码格式校验
  11. fork-ts-checker-webpack-plugin:在命令行中显示ts错误
  12. @pmmmwh/react-refresh-webpack-plugin:react组件热更新(HRM)

关于webpack其它文章

不了解Tapable?那你咋写的Webpack插件

面试官:你写过webpack插件吗?

来吧!一起肝一个CLI工具

文章分类
前端
文章标签