前端 webpack Webpack 基础入门 4rozeN 2025-03-02 2025-09-21 前言
webpack 相关知识较为琐碎,学习起来需要静心。
本文基于 Webpack v5 文档 和一些视频资料,记录一些基础配置、概念,完成 webpack 入门。
概念
本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具 。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图 (dependency graph) ,然后将你项目中所需的每一个模块组合成一个或多个 bundles ,它们均为静态资源,用于展示你的内容。
功能
webpack 本体功能是有限的:
开发模式:仅能编译 JS 中的 ES module
语法
生产模式:能编译 JS 中的 ES module
语法,还能压缩 JS 代码
创建简单工程
在一个空的目录下,执行命令:
创建以下结构的项目:
1 2 3 4 5 6 7 8 9 10 11 ├─📁 dist ├─📁 src │ ├─📁 css │ ├─📁 font │ ├─📁 img │ ├─📄 asset.d.ts │ └─📄 index.ts ├─📄 index.html ├─📄 package-lock.json ├─📄 package.json └─📄 webpack.config.js
index.html
简单导入 index.ts
:
1 2 3 4 5 6 7 8 9 10 11 12 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <title > webpack-ts</title > </head > <body > <div class ="container" > </div > <script src ="./src/index.ts" > </script > </body > </html >
安装 webpack 和 webpack-cli:
1 npm i webpack webpack-cli -D
webpack 是核心本体,而 webpack-cli 则是命令行工具,用来和人类进行交互的;-D
表示安装到开发环境 development
(在 package.json
的 devDependencies
项可以找到)。
CLI(Command-Line Interface)
和 GUI(Graphical User Interface)
是两种不同的交互,前者为命令行式,后者则是可视化界面。
对于什么时候安装到开发环境,什么时候安装到生产环境,区分原则就是:运行时是否需要该依赖
对于 ts,我们还需要安装 ts:
1 npm install -D typescript
核心概念
entry(入口)
这将指定 webpack 从哪个文件开始分析依赖,进行打包
output(输出)
这将指定 webpack 打包完的文件输出到哪里去
loader(加载器)
webpack 本体只能处理 js、json,其他资源需要借助 loader 才能解析
plugins(插件)
扩展 webpack 的功能
mode(模式)
主要分为两种模式:development
和 production
基础配置
入口起点 (entry point) 指示 webpack 应该使用哪个模块,来作为构建其内部 依赖图 (dependency graph) 的开始。这就是说,你需要给 webpack 指定一个分析入口,webpack 就会从这个入口文件中分析得到一张依赖关系图,webpack 本身不会运行你的代码 ,此阶段只专注于分析依赖。
入口起点,其默认值为:./src/index.js
,一般通过 webpack 的配置文件进行配置,可以指定一个或多个不同的入口。对于我们目前的项目结构,入口文件就是./src/index.ts
在项目根目录下新建文件:webpack.config.js
1 2 3 4 5 const path = require ('path' ); module .exports = { entry : './src/index.ts' , }
这样就做好了入口文件的标识。
Webpack 的配置文件运行在 Node.js 环境 下的,所以默认采用 CommonJS(CJS) 模块语法(require
/ module.exports
),而不是 ECMAScript Modules(ESM) 语法(import
/ export
)。
具体应该选哪种语法,其实取决于项目结构以及所依赖的生态环境:
1、CJS(默认 & 最常见)
历史原因,目前绝大多数 Webpack 插件和 Loader 的文档与示例仍然使用 CJS。
兼容性最好,几乎所有 Node.js 版本都支持。
2、ESM(现代写法)
在 Node.js 12+ 支持 ES Modules 后,可以把 Webpack 配置文件写成 ESM。
使用时需要在 package.json
里加 "type": "module"
,或者把文件命名为 webpack.config.mjs
。
更适合想要在前后端统一用 import/export
的项目。
如果是学习 / 小项目,用 CJS 更省心。
如果整个项目已经全局采用 ESM (比如 Vue 3 + Vite 生态),可以考虑 Webpack 配置也写 ESM。
output 属性告诉 webpack 在哪里输出它所创建的 bundle ,以及如何命名这些文件。主要输出文件的默认值是 ./dist/main.js
,其他生成文件默认放置在 ./dist
文件夹中。
你可以通过在配置中指定一个 output
字段,来配置这些处理过程:
webpack.config.js
1 2 3 4 5 6 7 8 9 10 const path = require ('path' ); module .exports = { entry : './src/index.ts' , output : { filename : 'main.js' , path : path.resolve (__dirname, 'dist' ), clean : true }, }
再就是加载器和插件了,当前我们的项目比较简单干净,所以目前并不需要,可以留空先看看 webpack 运行效果。
你还可以根据 mode 字段给 webpack 指定运行模式,有 development 和 production 两种,默认是 production。所以,最后一个简单的 webpack 配置文件如下:
webpack.config.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const path = require ('path' ); module .exports = { entry : './src/index.ts' , output : { filename : 'main.js' , path : path.resolve (__dirname, 'dist' ), clean : true }, module : { rules : [] }, plugins : [], mode : "development" }
开发模式与生产模式概念
Webpack 通过 mode
选项来区分运行环境,常见的有 development (开发模式) 和 production (生产模式)。
开发模式(mode: "development"
)
优化点:速度优先,帮助开发调试。
特点:
不压缩、不混淆代码。构建更快。
自动启用 eval-cheap-module-source-map
,方便定位源码。
保留有用的警告和错误信息。
通常配合 webpack-dev-server
实现热更新。
生产模式(mode: "production"
)
优化点:体积优先,帮助上线运行更快。
特点:
自动压缩(TerserPlugin)并混淆 JS。
Tree Shaking(删除未使用代码)。
自动分离 runtime,优化缓存。
scope hoisting(作用域提升),减少函数包装。
输出的 bundle 尽可能小、加载更快。
本文前半段的相关配置基本关注于开发模式。
在这个模式下,我们先前提到 webpack 本身并不能处理除了 js 和 json 以外的文件,所以我们需要配置一系列东西帮助 webpack 完成工作。
处理 TS
现在基本项目都需要用到 TS,那么优先处理 TS 是必要的,否则无法输出正确的 js。
写一段简单的 hello, world
式的 ts 代码如下:src/index.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type User = { id : number ; name : string ; }; const greet = (user : User ): string => `Hello, ${user.name} !` ;const me : User = { id : 1 , name : "Webpack + TS" };console .log (greet (me));const el = document .createElement ("div" );el.textContent = greet (me); el.className = "greeting" ; document .body .appendChild (el);
因为我们之前已经在 index.html
中引入了 index.ts
,所以我们可以直接运行 webpack
看看没有经过任何配置的 webpack
将输出什么样的 js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 npx webpack npx webpack asset main.js 1.51 KiB [emitted] (name: main) ./src/index.ts 424 bytes [built] [code generated] [1 error] ERROR in ./src/index.ts 2:5 Module parse failed: Unexpected token (2:5) You may need an appropriate loader to handle this file type , currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders | // 定义一个类型 > type User = { | id : number; | name: string; webpack 5.101.3 compiled with 1 error in 56 ms
可以看到,webpack 无法处理该文件,并贴心提示你:You may need an appropriate loader to handle this file type => 你或许需要一个合适的loader来处理该文件类型
ts-loader
于是我们来安装 ts-loader 帮 webpack 处理 ts 文件:
1 npm install ts-loader --save-dev
有的同学可能会问,我们之前不是用 npm 已经安装了 ts 吗?那直接配置 tsconfig.json
不就好了?
是的,但这不是说两者只需要一个,而是他们完全可以搭配干活,完成更精细的控制。
对于 ts 编译器,它只会做 语法检查 + 转换成 JS ,不会做打包(不处理 import
的依赖、不打包 CSS / 图片);而 Webpack 是个打包工具,他本身不懂 TypeScript。有了 ts-loader
后,不用单独跑 tsc
,Webpack 在打包时就会自动把 TS 转成 JS。
这就是说,整个工作流程如下:
当我们安装了 ts-loader 之后,并且配置了 tsconfig.json 文件,再执行 npx webpack
时:
ts-loader → 发现要处理 .ts
文件
它调用 TypeScript 编译器 → 读取 tsconfig.json
按配置(比如转成 ES5)输出 JS → 再交给 Webpack 打包
于是,我们编写 webpack.config.js 的 module 如下:
1 2 3 4 5 6 7 8 9 10 module : { rules : [ { test : /\.tsx?$/ , use : 'ts-loader' , exclude : /node_modules/ } ] },
并且在根目录下创建 tsconfig.json
:
1 2 3 4 5 6 7 8 9 10 { "compilerOptions" : { "target" : "es5" , "module" : "ESNext" , "esModuleInterop" : true , "forceConsistentCasingInFileNames" : true , "lib" : [ "DOM" , "ES2022" ] } , "include" : [ "src/**/*" ] , }
我们来逐项说明:
target :指定编译之后的 js 语法版本,取决于项目需要兼容多旧的浏览器,比如 es5
最终代码能在老旧的 IE11 上运行,现在一般是 es2016+
,支持 async/await 等常用特性。
module :告诉编译器生成的 JS 模块化方案,这里是最新的 ES Modules 语法(import/export
)。
详细解释
常见取值
现代 ES 模块 ESNext
始终使用 TS 支持的最新 ES Module 语法。 输出 import
/ export
。 场景:现代打包工具(Webpack、Vite、Rollup)、Node.js 14+(支持 ES Modules)。 ES6
/ ES2015
与 "ESNext"
类似,但只保证符合 ES2015 版本。 场景:需要固定在 ES6 标准,不跟随 TS 的更新。 CommonJS 系列 CommonJS
输出 require()
/ module.exports
。 场景:Node.js 里最常见的模块系统;老旧工具链。 Webpack 默认配置文件就是 CJS。 AMD
输出 AMD 风格模块,依赖 require.js
。 场景:老项目,前端按需加载。 UMD
通用模块定义,兼容 CommonJS + AMD + 全局变量。 场景:要发布一个库给不同环境使用时。 System
输出 SystemJS 模块。 场景:用 SystemJS 动态加载模块的老项目。 Node.js 特定 "Node16"
/ "NodeNext"
TypeScript 4.7+ 引入的新选项。 模拟 Node.js 的真实模块解析方式(支持 package.json
里的 "type": "module"
)。 场景:写 Node.js 服务端项目,特别是用 ESM 时推荐
esModuleInterop :允许用 import
语法导入 CommonJS 模块。
比如 import express from "express";
(否则你要写 import * as express from "express";
)。
forceConsistentCasingInFileNames :在导入路径时,强制区分大小写,防止在大小写不敏感的文件系统(如 Windows)上编译通过,但在 Linux 上运行失败。
lib :指定编译时要包含的库声明文件,影响类型检查和代码补全。
"DOM"
→ 提供 document
、window
等类型。
"ES2022"
→ 提供最新 ECMAScript 2022 的内置对象类型(比如 Object.hasOwn
)
这里的 lib 中我写 ES2022
主要是为了给后面的高版本语法转换预留的。
现在写好了配置文件也配置好了 ts-loader,可以运行试试了:
不出意外,肯定是编译成功的。但是我们也无法通过实际效果亲眼看到,这是因为我们的 index.html
是引入的 src
的 index.ts
,我们需要给它改成 dist
下的 main.js
:
1 2 3 4 <div class="container"></div> - <script src="./src/index.ts"></script> + <script src="./dist/main.js"></script> </body>
此时页面就能正常看到效果了:
js 兼容性问题
但是,这就完了吗?其实,上述操作编译过后的 js 文件是有可能存在一定的兼容性问题的。
对于老旧浏览器,例如 IE11:TypeScript 只会降级 TS 语法(比如 interface
→ 删除,class
→ 函数),但不会 polyfill 新的 JS 特性。比如:Promise
、Array.includes
还需要 Babel + core-js 来处理。
Babel
Babel 是一个 JavaScript 编译器。对于 webpack 来说,babel 是它的一个 loader。
核心功能
语法转换
let/const
→ var
箭头函数 → 普通函数
class
→ 构造函数 + 原型
Polyfill(垫片)
新 API(Promise
、Array.includes
、Object.assign
等)自动引入 core-js
来兼容老环境。
插件和预设
插件(plugins):处理具体语法,比如 @babel/plugin-transform-arrow-functions
。
预设(presets):插件集合,比如 @babel/preset-env
、@babel/preset-react
、@babel/preset-typescript
。
常见应用场景
前端 :保证新语法在旧浏览器可运行。
React/Vue 项目 :转换 JSX / TS。
Node 项目 :用最新语法但运行在老版本 Node。
库开发 :发布前打包成多种模块格式(ESM / CJS)。
应用 Babel
我们首先来重写 一下 src/index.ts
方便展示转换效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 type Task = { id : number ; name : string ; done : boolean ; }; function fetchTasks ( ): Promise <Task []> { return new Promise ((resolve ) => { setTimeout (() => { resolve ([ { id : 1 , name : "Learn TypeScript" , done : true }, { id : 2 , name : "Play with Webpack" , done : false }, ]); }, 500 ); }); } const tags : string [] = ["ts" , "webpack" , "babel" ];console .log ("是否包含 babel? ->" , tags.includes ("babel" ));fetchTasks ().then ((tasks ) => { const list = document .createElement ("ul" ); for (const t of tasks) { const item = document .createElement ("li" ); item.textContent = `${t.name} - ${t.done ? "yes" : "no" } ` ; list.appendChild (item); } document .querySelector (".container" ).appendChild (list); });
接着我们安装 Babel :
1 npm install -D babel-loader @babel/core @babel/preset-env
安装完成之后,在 webpack.config.js
中进行配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // loader配置 module: { rules: [ { test: /\.tsx?$/, // 匹配文件后缀名为.ts或.tsx的文件 - use: 'ts-loader', // 使用ts-loader进行编译,当use中有多个loader时,webpack会从右到左依次执行 + use: [ + { + loader: "babel-loader", + }, + { + loader: "ts-loader", // 使用ts-loader进行编译 + options: { + transpileOnly: true // 跳过类型检查,提升编译速度 + } + } + ], // 使用ts-loader进行编译,当use数组中有多个loader时,webpack会从右到左依次执行 exclude: /node_modules/ // 排除node_modules目录,提升编译速度 } ] },
因为 babel 处理的是 js,所以它需要运行在 ts-loader 之后 ,等待 ts 被转换为 js。
这里的 babel 需要我们提供一个配置文件(也可以直接写在 webpack.config.js 中):babel.config.js
:
1 2 3 4 5 6 7 8 9 10 11 12 module .exports = { presets : [ [ "@babel/preset-env" , { targets : "> 0.25%, not dead, ie 11" , useBuiltIns : "usage" , corejs : 3 , }, ], ], };
这里的 presets 如果存在多个预设,处理顺序也是从右到左。解释如下:
1、@babel/preset-env
Babel 的核心预设,用来 根据目标环境决定要转换哪些语法和 API 。
它包含了大量插件,例如:
箭头函数转换插件
模板字符串转换插件
ES6+ 新方法 polyfill 插件
2、targets
指定代码需要兼容哪些环境 / 浏览器。
> 0.25%
: 使用率大于 0.25% 的浏览器
not dead
:不包含停止更新的浏览器
ie 11
:强制兼容 IE11
3、useBuiltIns: "usage"
告诉 Babel 按需注入 polyfill ,而不是全量引入。例如 const arr = [1,2].includes(2)
,Babel 会检测到你用到了 Array.prototype.includes
,自动引入对应 polyfill,而不是把所有 ES6/ES2015 API 都打包进去。
4、corejs: 3
指定 polyfill 的版本
Babel 会用 core-js@3
提供缺失的 API
为什么 ts-loader 需要跳过类型检查
这里为什么 ts-loader 需要跳过类型检查(transpileOnly: true
)呢?
这是因为编译过程中 包含类型检查 ,构建速度较慢(因为 Webpack 编译时要等类型检查完成)。如果项目较小,是可以的;如果项目是中大型项目,完全可以新开独立进程交给 fork-ts-checker-webpack-plugin 完成。
fork-ts-checker-webpack-plugin
ts-loader(transpileOnly: true)
+ fork-ts-checker-webpack-plugin
+ babel-loader
的工作流程:
ts-loader:仅转译 TS 到 JS(跳过类型检查)
babel-loader:做语法转换(比如转成 ES5,polyfill 等)
fork-ts-checker-webpack-plugin:在单独的进程里做 类型检查
需要注意的是,fork-ts-checker-webpack-plugin 对 Node 、Webpack 、TS 三个东西的版本都有要求(更多要求详见其 README 文档):
1 This plugin requires Node.js >=14.0.0+, Webpack ^5.11.0, TypeScript ^3.6.0
目前我们的项目中,可以在 package.json
中找到 webpack 和 TS:
1 2 3 4 5 6 7 8 9 "devDependencies" : { "@babel/core" : "^7.28.4" , "@babel/preset-env" : "^7.28.3" , "babel-loader" : "^10.0.0" , "ts-loader" : "^9.5.4" , "typescript" : "^5.9.2" , "webpack" : "^5.101.3" , "webpack-cli" : "^6.0.1" }
它们都是高于要求的,所以可以使用 fork-ts-checker-webpack-plugin:
1 npm install --save-dev fork-ts-checker-webpack-plugin
配置 webpack.config.js
如下:
1 2 3 4 5 + // 引入插件 + const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); - plugins: [], + plugins: [new ForkTsCheckerWebpackPlugin()],
此时再进行编译:
这将报错。这是因为我们没有安装 core.js
无法进行 polyfill。这也是 webpack 文档的一个小问题了,有时候不会指明全部需要的依赖项。
执行安装命令,安装 core.js 到生产环境 :
为什么是生产依赖呢?前面说过,安装到生产还是开发环境,主要看它需不需要被运行。这里我们为了追求编译打包后的代码体积小,所以采用的是按需使用 polyfill(useBuiltIns: "usage"
而不是 useBuiltIns: "entry"
),Babel 会在编译过后的代码文件中依然引用 core.js
如果是全量引入,那就要在入口文件中手动引入:
1 2 3 import "core-js/stable" ;import "regenerator-runtime/runtime" ;
全量引入情况下 Babel 不再插入单独模块,而是依赖入口文件的全量导入,也就可以不在生产环境使用 core.js 了。
此时再次执行编译命令 npx webpack
就可以到 dist/index.js
查找 index.ts
是否在转换后使用 polyfill 了:
1 2 3 4 5 6 7 8 9 "./src/index.ts" : ((__unused_webpack_module, __webpack_exports__, __webpack_require__ ) => { eval ("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var core_js_modules_es_array_concat_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.array.concat.js */ \"./node_modules/core-js/modules/es.array.concat.js\");\n/* harmony import */ var core_js_modules_es_array_concat_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_concat_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.array.includes.js */ \"./node_modules/core-js/modules/es.array.includes.js\");\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var core_js_modules_es_function_name_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! core-js/modules/es.function.name.js */ \"./node_modules/core-js/modules/es.function.name.js\");\n/* harmony import */ var core_js_modules_es_function_name_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_function_name_js__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var core_js_modules_es_object_to_string_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! core-js/modules/es.object.to-string.js */ \"./node_modules/core-js/modules/es.object.to-string.js\");\n/* harmony import */ var core_js_modules_es_object_to_string_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_object_to_string_js__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! core-js/modules/es.promise.js */ \"./node_modules/core-js/modules/es.promise.js\");\n/* harmony import */ var core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_4__);\n\n\n\n\n\n// 模拟异步请求\nfunction fetchTasks() {\n return new Promise(function (resolve) {\n setTimeout(function () {\n resolve([{\n id: 1,\n name: \"Learn TypeScript\",\n done: true\n }, {\n id: 2,\n name: \"Play with Webpack\",\n done: false\n }]);\n }, 500);\n });\n}\n// 使用 ES6+ 的 includes\nvar tags = [\"ts\", \"webpack\", \"babel\"];\nconsole.log(\"是否包含 babel? ->\", tags.includes(\"babel\"));\n// DOM 操作\nfetchTasks().then(function (tasks) {\n var list = document.createElement(\"ul\");\n for (var _i = 0, tasks_1 = tasks; _i < tasks_1.length; _i++) {\n var t = tasks_1[_i];\n var item = document.createElement(\"li\");\n item.textContent = \"\".concat(t.name, \" - \").concat(t.done ? \"yes\" : \"no\");\n list.appendChild(item);\n }\n document.body.appendChild(list);\n});\n\n//# sourceURL=webpack://webpack-code/./src/index.ts?\n}" ); })
能看到 core.js 相关的导入语句:
1 2 3 4 5 var core_js_modules_es_array_concat_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__ ( "./node_modules/core-js/modules/es.array.concat.js" ); var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__ ( "./node_modules/core-js/modules/es.array.includes.js" ); var core_js_modules_es_function_name_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__ ( "./node_modules/core-js/modules/es.function.name.js" ); var core_js_modules_es_object_to_string_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__ ( "./node_modules/core-js/modules/es.object.to-string.js" ); var core_js_modules_es_promise_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__ ( "./node_modules/core-js/modules/es.promise.js" );
可以说明 polyfill 已经发生了。
处理 CSS
前面提到,webpack 本身不能处理除了 JS 和 json 以外的资源,所以 css 也要借助 loader 来处理,即 css-loader :
1 npm install --save-dev css-loader style-loader
注意,文档中可能没有提到安装 style-loader
,自己在安装时需要一起安装。
接着我们在 src/css
中创建 index.css
:
1 2 3 .container { background-color : rebeccapurple; }
然后在入口文件 index.ts
中进行引用:
1 import "./css/index.css"
最后,在 webpack 配置文件中配置 css-loader:
1 2 3 4 5 6 7 8 9 10 module .exports = { module : { rules : [ { test : /\.css$/i , use : ['style-loader' , 'css-loader' ], }, ], }, };
此时就可以编译试试了。
处理 sass
对于 sass、less 和 stylus,如果你会了其中一种,那么其他的大差不差,本文在此介绍 sass 的处理方法。
安装 sass-loader :
1 npm i sass sass-loader --save-dev
配置 webpack:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 module .exports = { module : { rules : [ { test : /\.s[ac]ss$/i , use : [ 'style-loader' , 'css-loader' , 'sass-loader' , ], }, ], }, };
那么,我们在 css 目录下删除 index.css(别忘记移除入口文件中对它的引入),然后创建一个 index.sass
和 index.scss
文件来检验:
index.sass
1 2 3 .container ul li:first-child background: blue color: #ff8a6a
index.scss
1 2 3 .container ul li :not (:first-child) { background : lightpink; }
当然,这两个文件也需要到入口文件 index.ts
中进行引入。之后执行编译命令之后应该就能看到效果了。
处理图片资源
在 webpack4 时,处理图片资源通过的是 file-loader
(将资源原封不动地输出)和 url-loader
(将图片转换为 Base64 格式,可指定大小阈值)。现在 webpack5 已经将这两个 loader 功能内置了,我们只需要简单配置一下即可。
要处理图片,首先要有图片资源,我将下面这张《浪客剑心:追忆篇》的海报和《风骚律师》的海报放入到 src/img
中(如果你没有图片资源,可以下载下面这两张图片测试):
按照文档进行配置 webpack(可以通过搜索 asset 找到,文档详见资源模块 ):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 module .exports = { module : { rules : [ { test : /\.(png|jpe?g|gif|svg|webp)$/i , type : 'asset' , parser : { dataUrlCondition : { maxSize : 40 * 1024 } } } ] } }
配置好了,但是我们还没指定图片放在哪,于是在 index.ts 中给页面增加两个 div 用来放图:
1 2 3 4 5 6 const img1 = document .createElement ("div" )img1.classList .add ("img1" ) const img2 = document .createElement ("div" )img2.classList .add ("img2" ) document .querySelector (".container" ).appendChild (img1)document .querySelector (".container" ).appendChild (img2)
接着在 index.sass
和 index.scss
中写样式:
index.sass
1 2 3 4 5 6 7 8 9 10 .container display: flex flex-direction: column .img1 height: 320px background-size: contain !important background: url("../img/追忆.webp") no-repeat ul li:first-child background: blue color: #ff8a6a
index.scss
1 2 3 4 5 6 7 8 9 10 .container { .img2 { height : 320px ; background-size : contain !important ; background : url ("../img/风骚律师.webp" ) no-repeat; } ul li :not (:first-child) { background : lightpink; } }
执行编译命令之后,可以看到追忆.webp
被输出为原数据格式,而风骚律师.webp
则被输出为 Base64 格式。
分类输出资源
观察打包目录 dist:
可以看到输出结果是没有分类的,我们最好对各类资源进行分类,这样方便管理。修改 webpack 配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 module.exports = { output: { - filename: 'main.js', // 输出文件名 + filename: 'static/js/main.js', // 输出文件名 path: path.resolve(__dirname, 'dist'), // __dirname是node.js的全局变量,代表当前执行脚本所在的目录 clean: true // 每次打包前清空dist目录 }, // loader配置 module: { rules: [ { test: /\.css$/, // 匹配文件后缀名为.css的文件 use: ['style-loader', 'css-loader'], + generator: { + filename: 'static/css/[name].[contenthash:8].css' // 输出文件名 + } }, { test: /\.s[ac]ss$/i, // 匹配文件后缀名为.scss或.sass的文件 use: ['style-loader', 'css-loader','sass-loader'], + generator: { + filename: 'static/css/[name].[contenthash:8].css' // 输出文件名 + } }, { test: /\.(png|jpe?g|gif|svg|webp)$/i, // 匹配文件后缀名为.png、.jpg、.jpeg、.gif、.svg的文件 type: 'asset', // 打包成静态资源 parser: { dataUrlCondition: { maxSize: 40 * 1024 // 图片大小小于40kb的会被转为base64格式 } }, + generator: { + filename: 'static/img/[name].[contenthash:8][ext]' // 输出文件名 + } } ] }, }
[name].[contenthash:8][ext]
中:
[name]:使用文件原名
[contenthash:8]:使用文件指纹,取生成的 hash 值的前 8 位
[ext]:使用原扩展名
contenthash 和 hash
需要说明的是,contenthash 和 hash 是两个不同的占位符:
[hash]
整个 项目构建的 hash 值 (一次构建一个值) 缺点:只要有一个文件改动,所有文件的 [hash]
都会变 (缓存不友好 ) [chunkhash]
根据 chunk 内容 生成的 hash 不同 entry 的 chunk 值不同 优点:只改动某个入口文件时,其他 chunk 的文件名不变(更适合持久缓存) [contenthash]
根据 文件内容 生成 hash 最精细化的方式:只有内容变化时,才会生成新的 hash 适合生产环境:比如 CSS 抽离文件(MiniCssExtractPlugin
)常用 [contenthash]
处理字体图标资源
要处理字体图标资源,首先得要有这些资源。推荐从阿里巴巴矢量图标库中下载这些资源:https://www.iconfont.cn/
随便点开一个图标库,将一些图标加入到项目(需要登录):
如果此前没有项目就需要创建项目,如果已经有项目直接选择一个项目加入到项目即可。加入之后将会跳转到项目详情页,我们将这些图标资源下载到本地(仅作演示,以后图标资源也可能是通过服务器得到的):
下载到本地的图标资源是一个压缩包的形式,我们将其剪切到 src/font
目录下,并将其解压:
解压后的目录如下:
这之中的 demo_index.html
也可以指导你如何使用图标资源:
这三种方式主要是兼容性的区别:
Unicode 在 IE6 以上可以使用
Font class 在 IE8 以上可以用
Symbol 在 IE9 以上可以用
我们一般使用 Font class 的方式进行使用:
依照指导文档,我们将 iconfont.css
剪切到 src/css
目录中。
需要注意的是,iconfont.css 中引入的路径需要进行修改:
我们将字体文件从 font 的深层目录移动出来,剪切到 src/font
目录下:
然后将 iconfont.css
中的路径修改如下:
别忘了,我们还得导入才能被 webpack 打包。在 index.ts
中引入 iconfont.css
:
此时就可以执行打包命令看看效果了:
不过默认打包的结构肯定不是我们期望的,我们可以像配置图片资源一样配置它们(webpack.config.js):
1 2 3 4 5 6 7 { test : /\.(ttf|woff2?)$/i , type : 'asset/resource' , generator : { filename : 'static/media/[contenthash:8][ext]' } }
asset/resource
表示将资源原封不动的输出,这里为了输出字体文件名短点,所以 filename 不保留原名了。
为了在页面中能显示这三个图标,我们还需要按照 demo_index.html
的指导加入 span 标签(index.html):
1 2 3 4 5 <span class ="iconfont icon-a-028-medicalapp" > </span > <span class ="iconfont icon-a-004-bones" > </span > <span class ="iconfont icon-webpack" > </span > <script src ="./dist/static/js/main.js" > </script >
icon 名和图标名一致:
这样执行打包命令就可以在 index.html
看到效果了。
处理其他资源
其实上述资源已经是我们经常会使用到的资源了,处理其他资源,如视频资源等,我们也只需要原封不动将其输出就可以了,所以就扩大一下 test
的匹配范围就行:
1 2 3 4 5 6 7 { test : /\.(ttf|woff2?|mp4|avi|mkv)$/i , type : 'asset/resource' , generator : { filename : 'static/media/[contenthash:8][ext][query]' } }
处理 Html 资源
安装 html-webpack-plugin
1 npm i html-webpack-plugin -D
接着在 webpack 配置文件中进行引入并配置:
1 2 3 4 5 6 7 8 9 10 const HtmlWebpackPlugin = require ('html-webpack-plugin' ); module .exports = { entry : 'index.js' , output : { path : path.resolve (__dirname, './dist' ), filename : 'index_bundle.js' , }, plugins : [new HtmlWebpackPlugin ()], };
并且在 index.html
中取消手动引入编译后的 main.js
:
1 2 3 4 5 6 <div class ="container" > </div > <span class ="iconfont icon-a-028-medicalapp" > </span > <span class ="iconfont icon-a-004-bones" > </span > <span class ="iconfont icon-webpack" > </span >
此时执行打包命令,你会发现打包输出的 dist/index.html
中:
所以我们还需要给这个 html 插件配置 template
字段:
1 2 3 4 5 6 7 8 plugins : [ new HtmlWebpackPlugin ({ template : path.resolve (__dirname, './index.html' ) }), new ForkTsCheckerWebpackPlugin (), ],
template 参数指定生成的 html 以哪个文件为参考生成一致的元素结构
搭建开发服务器
每次写完代码都需要手动执行打包命令才能同步代码,太麻烦了,我们安装 webpack-dev-server 来自动化执行:
1 npm install --save-dev webpack-dev-server
然后在 webpack 的配置文件中进行配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 plugins : [ new HtmlWebpackPlugin ({ template : path.resolve (__dirname, './index.html' ) }), new ForkTsCheckerWebpackPlugin (), ], devServer : { host : 'localhost' , port : 3000 , static : "./dist" , open : true , },
此时,打包命令需要执行的是:
需要注意的是,如果使用 serve 开发服务器,那么实际是不会输出文件的,编译打包结果存在内存之中。
ESLint
ESLint ,可组装的 JavaScript 和 JSX 检查工具。主要被用来检测 js 和 jsx 语法,可以配置各种功能。
安装 ESLint
在 webpack4 中,ESLint 是一个 loader;而在 webpack5 中,ESLint 是一个插件。
执行命令安装 EslintWebpackPlugin 和 ESLint 到开发环境:
1 npm install eslint-webpack-plugin eslint --save-dev
在 webpack 配置文件中进行引入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const ESLintPlugin = require ('eslint-webpack-plugin' );module .exports = { plugins : [ new ESLintPlugin ({ context : path.resolve (__dirname, './src' ), extensions : ['ts' , 'tsx' , 'js' , 'jsx' ], eslintPath : require .resolve ('eslint/use-at-your-own-risk' ), }), ], };
ESLint 配置
和 webpack、babel 一样,eslint 也是依照配置文件工作。eslint 配置文件有很多种写法:
.eslintrc.*
:.eslintrc
、.eslintrc.js
、.eslintrc.json
eslint.config.js
写在 package.json
中:eslintConfig
配置项(适合简单项目)
ESLint 会自动查找读取它们,配置文件只需要存在一个 。
其中,eslint.config.js
写法是 ESLint 9 推出的新标准 ,叫做 Flat Config (平面配置),逐步替代 .eslintrc.*
。它必须是一个 JavaScript/ESM 文件 。
如果你的项目用的工具链都比较新(比如 webpack5、Vite、TS 最新版),建议直接用 eslint.config.js
;
如果你需要兼容老工具,暂时用 .eslintrc.js
更省心。
我们这里使用 eslint.config.js
写法。需要注意的是,我们的项目环境默认在 Node 环境,而 Node 环境下,.js
文件默认是 CommonJS (CJS) 模块,我们需要将 ESLint 配置文件后缀改为 mjs
,Node 就会自动按 ESM 方式解析。
你也可以在 package.json
中声明 type: "module"
。
但是,一旦加了 "type": "module"
,项目里的所有 .js
文件都变成 ESM ,会影响 Webpack 等其他配置。
所以,除非项目的相关生态能统一所有配置文件全为一种语法(要么全是 CJS 要么全是 ESM),否则还是只改个别文件的后缀让 Node 自己识别为好。
于是,我们在根目录下创建 eslint.config.mjs
。由于 ESLint 的配置项十分的多,所以我们一般不会自己手写全部规则,而是使用官方推荐的规则,然后在官方的规则下面写一些自定义的规则覆盖冲突规则即可。例如:
@eslint/js
:官方维护的 JavaScript 推荐规则集 ,代替了过去 .eslintrc
里常用的 "eslint:recommended"
。
typescript-eslint
:官方的 TypeScript 插件 + 解析器(集成了 @typescript-eslint/parser
和 @typescript-eslint/eslint-plugin
),Flat Config 推荐用法。
globals
:提供常见的全局变量集合(比如 window
, document
, process
等),让 ESLint 知道这些名字是合法的,不会误报 no-undef
。
上面三个基本够用了,执行安装命令:
1 npm install --save-dev @eslint/js typescript-eslint globals
接着我们配置完整的 config.eslint.mjs
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import globals from "globals" ;import pluginJs from "@eslint/js" ;import tseslint from "typescript-eslint" ;export default [ pluginJs.configs .recommended , ...tseslint.configs .recommended , { files : ["src/**/*.{js,mjs,cjs,ts,tsx}" ], languageOptions : { globals : globals.browser , }, }, { files : ["config/**/*.js" ], languageOptions : { globals : globals.node , }, rules : { "@typescript-eslint/no-require-imports" : "off" , }, }, { ignores : [ "**/*.d.ts" , "**/node_modules/**" , "src/font" , "**/dist/**" , "**/*.config.*" , ], }, ];
如果是旧的 ESLint 配置,应当是:.eslintrc.js
+.eslintignore
其中 eslintrc
中的 rc
指的是 run commands
或者 runtime configuration
,前者是历史渊源,后者是通俗解释
生产模式
在生产模式下,我们主要关注:代码运行速度、代码打包速度。
在根目录下创建 config
目录,将根目录下的 webpack.config.js
移动到 config
目录下,重命名为 webpack.dev.js
,并复制一份重命名为 webpack.prod.js
。
dev 环境
我们对 webapck.dev.js
进行修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 const path = require('path'); // 引入node.js的path模块 const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const ESLintPlugin = require('eslint-webpack-plugin'); module.exports = { entry: './src/index.ts', // 入口文件,webpack从入口文件开始解析依赖关系 output: { filename: 'static/js/main.js', // 输出文件名 - path: path.resolve(__dirname, 'dist'), // __dirname是node.js的全局变量,代表当前执行脚本所在的目录 + path: undefined, // 开发模式无输出 - clean: true // 每次打包前清空dist目录 }, // loader配置 module: { rules: [ // ...... ] // 插件配置 plugins: [ new ESLintPlugin({ // 指定要约束的文件位置 - context: path.resolve(__dirname, './src'), + context: path.resolve(__dirname, '../src') extensions: ['ts', 'tsx', 'js', 'jsx'], // 指定要检查的文件后缀名 // 插件没完全兼容eslint >= 8的版本带来的 Flat Config,需要手动指定路径 eslintPath: require.resolve('eslint/use-at-your-own-risk'), }), new HtmlWebpackPlugin({ // template参数指定生成的html以哪个文件为参考生成结构 - template: path.resolve(__dirname, './index.html') + template: path.resolve(__dirname, '../index.html') }), new ForkTsCheckerWebpackPlugin(), ], devServer: { host: 'localhost', // 主机名 port: 3000, // 端口号 static: "./dist", // 静态文件目录 open: true, // 自动打开浏览器 }, + devtool: "inline-source-map", // 启用源码地图 // mode表示webpack的运行模式,有development和production两种,默认是production mode: "development" }
在开发模式下,编译打包发生在内存中,没有输出结果不会写入到磁盘变成文件,所以 output 的 path 我写成 undefined(实际开发模式会忽略此项配置);因为我们改动了 webpack 配置文件所在的路径,需要给使用绝对路径的配置项纠正路径,所以插件部分的 path.resolve
的路径都往上了一级;入口文件为什么不改呢?首先入口文件并非绝对路径,其次因为 webpack 是运行在 Node 环境中的,Node 的运行环境又是相对于执行命令时的路径(也就是根目录),所以入口文件不需要改动也能找到正确的文件。
eslintPath: require.resolve("eslint/use-at-your-own-risk")
它直接走 Node 的模块查找机制,最终解析到 node_modules/eslint
中的绝对路径,和配置文件在哪个目录没关系。
prod 环境
生产环境也需要修改配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 const path = require('path'); // 引入node.js的path模块 const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const ESLintPlugin = require('eslint-webpack-plugin'); module.exports = { entry: './src/index.ts', // 入口文件,webpack从入口文件开始解析依赖关系 output: { filename: 'static/js/main.js', // 输出文件名 - path: path.resolve(__dirname, 'dist'), // __dirname是node.js的全局变量,代表当前执行脚本所在的目录 + path: path.resolve(__dirname, '../dist'), // __dirname是node.js的全局变量,代表当前执行脚本所在的目录 clean: true // 每次打包前清空dist目录 }, // loader配置 module: { rules: [ // ...... ] }, // 插件配置 plugins: [ new ESLintPlugin({ // 指定要约束的文件位置 - context: path.resolve(__dirname, './src'), + context: path.resolve(__dirname, '../src'), extensions: ['ts', 'tsx', 'js', 'jsx'], // 指定要检查的文件后缀名 // 插件没完全兼容eslint >= 8的版本带来的 Flat Config,需要手动指定路径 eslintPath: require.resolve('eslint/use-at-your-own-risk'), }), new HtmlWebpackPlugin({ // template参数指定生成的html以哪个文件为参考生成结构 - template: path.resolve(__dirname, './index.html') + template: path.resolve(__dirname, '../index.html') }), new ForkTsCheckerWebpackPlugin(), ], - devServer: { - host: 'localhost', // 主机名 - port: 3000, // 端口号 - static: "./dist", // 静态文件目录 - open: true, // 自动打开浏览器 - }, // mode表示webpack的运行模式,有development和production两种,默认是production - mode: "development" + mode: "production" }
改写命令脚本
前面我们配置了 ESLint,现在又分离了开发和生产环境,此时执行命令急需改造。修改 package.json
,将原来的 scripts 项:
1 2 3 "scripts" : { "test" : "echo \"Error: no test specified\" && exit 1" } ,
改成下面:
1 2 3 4 5 6 "scripts" : { "lint" : "eslint \"src/**/*.{ts,tsx,js}\"" , "dev" : "npm run lint && webpack serve --config ./config/webpack.dev.js" , "start" : "npm run dev" , "build" : "webpack --config ./config/webpack.prod.js" } ,
scripts 配置的是一个对象,所以其中的命令顺序没有关系。配置了上述命令脚本,以后我们使用开发模式进行查看,就只需要运行简单的命令:
这将先触发 ESLint 的检查然后再启动开发服务器,一旦 ESLint 检测到语法问题触发报错,打包将停止。
生产模式同理,执行简单命令:
即可打包生成静态结果。
CSS 提取优化
目前我们打包 css 有个问题:css 被打包到 js 文件中,css 样式只能等待到 js 被执行完毕才能被应用,而在 js 执行的过程中,浏览器可能已经渲染出了原始的 DOM 结构 ,页面表现为裸 HTML 结构,等待到 JS 执行完毕,style 标签 终于被插入完毕,样式被应用,浏览器又重新渲染,这在用户看来就是 "闪屏" 现象。
针对这个现象,我们要做的就是把 css 单独提取出来变成 <link>
标签,让 CSS 可以和 JS 并行下载,并且在 首次渲染之前 就能应用样式,避免闪屏。
为什么单独抽取 css 为 link 标签可以避免闪屏?
要知道为什么,需要你对浏览器渲染原理有一定了解。可以看看我之前写的《浏览器渲染原理 》
简单来说,当浏览器解析 HTML 遇到 <link>
,会下载 CSS,启动 CSS 的预解析线程,使得 CSSOM 在较早阶段可用。
浏览器会等待关键 CSSOM 就绪再做首次完全渲染 (即使主解析线程继续解析 HTML,渲染会等 CSSOM)。
因此当 CSS 是通过 <link>
提前可用时,首次绘制就包含正确样式,从而避免闪屏出现(代价是阻塞渲染的时间,但通常能改善一点视觉稳定性)。
如果你想验证,可以按照下面的思路进行:
在本地启动 dev server 并使用 style-loader
,在 Chrome DevTools (一般浏览器叫开发者工具)的 Network 设置为 Slow 3G 并禁用 cache,刷新页面:你会看到先是裸 HTML / 文本再有样式。 在 Performance 面板录制加载过程,查看首次绘制(FP)和后面的 repaint/reflow,能明显看到样式注入引起的重排。 临时把 JS 放到头部或把 CSS 直接注入 <style>
,观察差异。
安装 MiniCssExtractPlugin 插件:
1 npm install --save-dev mini-css-extract-plugin
安装之后我们就不再需要 style-loader
了,顾名思义,它就是用来动态插入生成 style 标签的,我们将其改为 MiniCssExtractPlugin(webpack.prod.js
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const MiniCssExtractPlugin = require ("mini-css-extract-plugin" );module .exports = { plugins : [new MiniCssExtractPlugin ()], module : { rules : [ { test : /\.css$/ , use : [ MiniCssExtractPlugin .loader , 'css-loader' ], generator : { filename : 'static/css/[name].[contenthash:8].css' } }, { test : /\.s[ac]ss$/i , use : [MiniCssExtractPlugin .loader , 'css-loader' , 'sass-loader' ], generator : { filename : 'static/css/[name].[contenthash:8].css' } }, ], }, };
注意改动的是生产环境的 webpack 配置文件,别搞错了。
这之后可以使用打包命令看看效果了:
可以看到默认是打包到了 dist
目录下,我们自然希望它在 static
目录下,所以继续配置插件:
1 2 3 new MiniCssExtractPlugin ({ filename : "static/css/[name].[contenthash:8].css" }),
这样就好了。
CSS 兼容性
css 兼容性是老生常谈的问题了,我们一般使用 postcss 来解决。postcss 在 webpack 中是一个 loader:postcss-loader
安装命令:
1 npm install --save-dev postcss-loader postcss postcss-preset-env
接着修改 webpack 的开发配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 { test : /\.css$/ , use : [ MiniCssExtractPlugin .loader , 'css-loader' , { loader : 'postcss-loader' , options : { postcssOptions : { plugins : [ "postcss-preset-env" , ] } } }], generator : { filename : 'static/css/[name].[contenthash:8].css' } }, { test : /\.s[ac]ss$/i , use : [MiniCssExtractPlugin .loader , 'css-loader' , { loader : 'postcss-loader' , options : { postcssOptions : { plugins : [ "postcss-preset-env" , ] } } }, 'sass-loader' ], generator : { filename : 'static/css/[name].[contenthash:8].css' } }
请注意,use 中 loader 的执行顺序是从右到左。所以对于普通的 css 应当是将 postcss 配置写在最右,让其第一个被执行,先处理兼容性问题,然后让 css-loader 处理 css 内容,最后让 MiniCssExtractPlugin 提取 css 文件为 link 标签;对于 sass、less、stylus 等,应当是将 postcss 配置写在预编译之后,让 posttcss 处理预编译得到的 css 代码而非 sass、lesss 和 stylus 代码。
除了预设,我们还可以让 postcss 知道兼容性要做到什么程度。在 package.json
中增加配置:
1 2 3 "browserslist" : [ "ie >= 8" ]
实际上兼容性不需要做到如此好,此处仅作演示。兼容的浏览器版本越久远,就越需要更多的兼容性代码,文件体积也会一定程度的增加。
一般我们会配置为(注释仅供参考,实际不需要):
1 2 3 4 5 6 7 8 "browserslist" : [ "last 2 versions" , "> 1%" , "not dead" ]
因为我们此前的 css 中使用到了 display: flex
,而 flex 布局是存在一定兼容性问题的,所以我们可以直接执行生产打包看看效果:
display: flex;
变成了 -ms-flexbox
,这就说明 postcss 正确配置了。
优化 css 的 loader 结构
目前我们的 webpack 配置中,关于 css 的相关 loader 是存在一定重复代码的(因为我没有处理每一种 css 所以较少,如果你们写了 css、sass、less、stylus,那就很多重复代码了),我们可以将它们抽离为函数,精简代码结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 const path = require ('path' ); const ForkTsCheckerWebpackPlugin = require ('fork-ts-checker-webpack-plugin' );const HtmlWebpackPlugin = require ('html-webpack-plugin' );const ESLintPlugin = require ('eslint-webpack-plugin' );const MiniCssExtractPlugin = require ("mini-css-extract-plugin" );const getStyleLoaders = (...loaders ) => { return [ MiniCssExtractPlugin .loader , 'css-loader' , { loader : 'postcss-loader' , options : { postcssOptions : { plugins : [ "postcss-preset-env" , ] } } }, ...loaders ].filter (Boolean ); } module .exports = { module : { rules : [ { test : /\.css$/ , use : getStyleLoaders (), generator : { filename : 'static/css/[name].[contenthash:8].css' } }, { test : /\.s[ac]ss$/i , use : getStyleLoaders ('sass-loader' ), generator : { filename : 'static/css/[name].[contenthash:8].css' } }, ] }
这里函数的参数是剩余参数,所以传入多个参数时,需要理解 use 的处理顺序(从右到左)传入合适的参数。
例如:
1 2 3 4 5 6 7 const getStyleLoaders = (...loaders ) => [ 'css-loader' , ...loaders ]; console .log (getStyleLoaders ('postcss-loader' , 'sass-loader' ));
此时 use 也能正确处理。
CSS 压缩
CSS 压缩需要使用到插件:CssMinimizerWebpackPlugin
安装命令:
1 npm install css-minimizer-webpack-plugin --save-dev
引入插件(webpack.prod.js
):
1 2 3 4 5 6 7 8 9 10 const CssMinimizerPlugin = require ('css-minimizer-webpack-plugin' );module .exports = { optimization : { minimizer : [ new CssMinimizerPlugin (), ] }, }
执行生产打包命令之后如果看到 dist/static/css/
目录下的 css 文件如果是一行,那就说明压缩生效了。
html 和 js 压缩
目前,html 打包后会自动压缩,但是 js 并不会(说实话这很奇怪,按理来说是都会被压缩的)。翻看了官方文档:
Webpack v5 comes with the latest terser-webpack-plugin
out of the box. If you are using Webpack v5 or above and wish to customize the options, you will still need to install terser-webpack-plugin
.
译:Webpack v5 自带最新的 terser-webpack-plugin
。如果你使用的是 Webpack v5 或更高版本,并且希望自定义选项,你仍然需要安装 terser-webpack-plugin
。
说实话,我没看懂这左右脑互搏的说明,自带!= 安装过了?
安装 terser-webpack-plugin
:
1 npm install terser-webpack-plugin --save-dev
引入并应用(webpack.prod.js
):
1 2 3 4 5 6 7 8 const TerserPlugin = require ('terser-webpack-plugin' );module .exports = { optimization : { minimize : true , minimizer : [new TerserPlugin ()], }, };
这样执行生产打包就能看到效果了。
我后来想了想,文档的意思应该是:如果你覆盖了默认的 minimizer(比如加了 CssMinimizerPlugin
),就必须自己安装并手动添加 TerserPlugin,否则 JS 不会压缩。
至此,webpack 基础篇完结撒花。
最终代码地址:webpack5-tutorial-basics