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

您所在的位置:网站首页 0a1d63485f9 Webpack5,了解从0到1搭建一个项目的细节

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

2023-09-20 15:38| 来源: 网络整理| 查看: 265

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

前言

本篇文章不只是搭建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 // 打包构建时的配置

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大基本模块:

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> Webpack5

然后在插件中引入模板:

// 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对象:

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-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 之前,通常使用:

raw-loader 将文件导入为字符串 url-loader 将文件作为 data URI 内联到 bundle 中 file-loader 将文件发送到输出目录

资源模块类型(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$/, 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插件:

@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-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) 关于webpack其它文章

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

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

来吧!一起肝一个CLI工具



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3