前言
本篇文章不只是搭建webpack5的项目,还有个更重要的目的是,为了在搭建过程中了解每个插件,loader的作用,为什么要使用这些东西,这些东西带来了什么。
我为本项目写了一个cli工具:fight-react-cli,想直接看最终结果的同学,安装一下这个cli工具,初始化一个项目就能查看全部的配置。
准备工作
首先我们创建一个项目webpack-demo ,然后初始化npm ,然后在本地安装webpack 和webpack-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 // 打包构建时的配置
复制代码
然后在根目录创建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大基本模块:
mode :模式,通过选择:development,production,none 这三个参数来告诉webpack使用相应模式的内置优化。entry :设置入口文件。output :告诉wenpack打包出的文件存放在哪里module.rules :loader(加载器),webpack本身只支持处理js,json文件,要想能够处理其它类型的文件,如:css,jsx,ts,vue等,则需要相应的loader将这些文件转换成有效的模块。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:
style-loader css-loader 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对象:
我们需要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-loader ,style-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:
babel-loader
安装:
npm install --save-dev babel-loader @babel/core @babel/preset-env
复制代码
需要用到的babel插件:
@babel/plugin-transform-runtime @babel/runtime
安装:
npm install --save-dev @babel/plugin-transform-runtime
复制代码
npm install --save @babel/runtime
复制代码
webpack.common.js
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'babel-loader',
include: path.resolve(__dirname, './src'),
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插件:
@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来说,已经不使用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-load 的plugins 中添加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中已经内置了该插件,当mode 为production 时会自动启用。
如果我们想自定义的话:
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文件,包含了我们需要单独打包的模块:react ,react-dom ,并且还需生成一个包含被打包模块信息的json文件。
- dll
- vendor.dll.js
- dll.manifest.json
复制代码
然后我们还需要干两件事:
- 在上线打包时告诉webpack,不要将我们dll的模块进行打包
- 在打包成功后的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,可以理解为css 的tree 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} => {
...
});
复制代码
我们来看下两种加载方式的区别:
- prefetch:会在浏览器空闲时提前加载文件
- 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值,在开发环境会影响构建效率。
优化工具
这里简单介绍一下:
progress-bar-webpack-plugin :打包时显示进度,但是会影响打包速度,需要斟酌使用,如果打包时间过长可以使用,否则不需要使用。speed-measure-webpack-plugin :查看loader和plugin的耗时,可以对耗时较长的loader和plugin针对性优化。webpack-bundle-analyzer :可以查看打包后各个文件的占比,来针对性的优化。
注:以上都是webpack的plugin
总结
使用的loader
处理css的loader:
style-loader :插入css样式到dom中css-loader :解析css文件生成js对象postcss-loader : 处理css兼容问题
postcss-preset-env :postcss预置插件,处理css兼容性,自动添加浏览器内核前缀@fullhuman/postcss-purgecss : 清除未使用的css样式
sass-loader :处理sass文件less-loader :处理less文件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
url-loader :处理图片,设置options.limite属性,小于该属性值的图片转为base64(内联),大于则将图片发送至输出目录。(内置file-loader)file-loader :处理文件,将文件发送至输出目录raw-loader :将文件导出为字符串
webpack5中使用内置的Asset Module :
asset/resource 替代file-loader asset/inline 替代url-loader asset/source 替代raw-loader asset 通过配置parser.dataUrlCondition.maxSize ,来自动选择使用asset/resource (大于MaxSize)和asset/inline (小于maxSize)。
使用的webpack plugin
html-webpack-plugin : 自动生成html文件,并且自动引用webpack构建好的文件。mini-css-extract-plugin : 分离css单独打包成一个文件。clean-webapck-plugin : 自动清除输出文件目录,被output.clean 替代copy-webpack-plugin : 复制文件到另外一个文件夹compression-webpack-plugin : 压缩文件webpack.DefinePlugin :注入全局变量webpack.DllPlugin :生成打包一个含有打包模块信息的json文件webpack.DllReferancePlugin :使用Dllplugin生成的json文件使webpack在打包时排除json文件中包含的模块。add-asset-html-webpack-plugin :自定义添加bundle文件到html中,本文中是将dll生成的js文件,使用该插件添加到了html中。eslint-webpack-plugin :代码格式校验fork-ts-checker-webpack-plugin :在命令行中显示ts错误@pmmmwh/react-refresh-webpack-plugin :react组件热更新(HRM)
|