前言
从Webpack开始的前端工程化探索
模块化
随着前端应用的日益复杂,程序员需要更高效的代码组织形式,以便提高可维护性并提升开发效率。
模块化将复杂的代码按功能的不同,分为不同的模块,单独维护,提高开发效率。
模块:
- 将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起。
- 块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信。
演变
1、文件划分 将不同的功能及其状态数据存放在单独的JS文件中。约定一个文件就是一个模块,以单独的script标签引入至HTML。
虽然实现了功能的划分,但缺点也十分明显:
- 污染全局作用域
- 命名冲突
- 无法管理模块的依赖关系
2、命名空间 在文件划分的基础上,约定每个模块只暴露一个全局的对象,对象包裹着模块的方法和状态。
虽然避免了命名冲突,但仍然没有私有空间,
3、IIFE 通过立即执行函数实现私有空间,使用window暴露模块的成员,通过参数声明依赖。
(($)=>{ const name = 123; const fun = () => { $('body'); console.log(name); // 闭包 } window.modelA = { name }})(JQuery) // 通过参数声明依赖以上是在没有工具和相关规范的早期,以约定的形式,实践模块化思想的方式。
仍然存在问题:
- 依赖管理混乱
- 不同开发者、不同项目,模块化的实现有差异
- 模块的导入不受代码控制
现代化
- **模块化规范:**对模块代码书写格式和交互规则的详细描述
- **模块加载器:**使用代码的方式,自动控制模块的导入,管理模块的依赖。
在ES6模块化出现之前,为了解决模块化的需求,出现了众多的模块化机制,CommonJS(NodeJS内置)、AMD(require.js)、CMD(Sea.js)
ES6模块化出现后的最佳实践:
- 浏览器环境:ES Module
- Node环境:CommonJS
ES Module
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量,而 CommonJS 和 AMD 模块,都是运行时的
<script type='module'> import { name } from './modules/index.js'; export name;</script>特性:
- 自动采用严格模式,忽略'use strict'
- 每个ESM模块都是单独的私有作用域
- ESM通过CORS请求外部JS模块
- ESM的script标签会延迟执行脚本(相当于defer)
详见ES6查缺补漏 #Module模块化、MDN-JavaScript模块
兼容性问题
ES6仍然有兼容性问题,早期的浏览器,特别是国产和手机上的浏览器
npm install -g caniuse-cmdcaniuse import可以引入 Polyfill 兼容
使用 webpack、vite 后,有更多的兼容插件可以安装使用,如 @vitejs/plugin-legacy
script添加nomodule属性,仅在不支持ESM的浏览器上执行该脚本
ESM in Node
文档: Node.js 如何处理 ES6 模块-阮一峰 Node.js文档-ECMAScript模块
ESM在Node.js的v8.5.0中作为实验性功能被引入,v12.17.0为所有Node.js应用程序提供了ESM支持
使用 .mjs、.cjs 文件后缀区分 ESM 和 CommonJS 模块
原生Node环境中的ESM与CommonJS:
- ES Module中可以导入CommonJS模块
- CommonJS中不能导入ES Module模块
- CommonJS始终只会导出一个默认成员
ESM得到CommonJS全局成员的值:
// console.log(__dirname)// ReferenceError: __dirname is not defined in ES module scopeimport { fileURLToPath } from 'url'import { dirname } from 'path'const __filename = fileURLToPath(import.meta.url)const __dirname = dirname(__filename)console.log(__filename)console.log(__dirname)// c:\***\esm-in-node\index.mjs// c:\***\esm-in-node前端打包工具
ESM仍然存在一些问题:
- 存在兼容性问题
- 模块文件过多,网络请求频繁
- 不仅是JS,前端所有的资源,包括CSS、HTML都需要模块化
前端需要更好的工具和规范,让开发者继续享受模块化带来的便利,而不需要担心对生产环境产生的影响。
需求推动技术的进步,打包工具顺势出现了。打包工具解决的是前端整体的模块化,不只是局限于JS的模块化。
如今前端项目的代码组织,已经走上了编辑代码和最终运行文件完全两样的形式,一系列工具链和自动化的思想也融入进了打包工具中,打包也逐渐从一个技术问题,转变为了生态和管理问题。打包工具现在也可称为构建工具,支撑着前端工程化。
常见的打包工具:Webpack、Vite、Rollup、esbuild
Webpack
Webpack是一个用于现代 JavaScript 应用程序的静态模块打包工具。
学习webpack大体上就是学习webpack.config.js的配置、各种loaders和plugins的使用,所以,多看文档,广泛了解,取所需使用文档、配置、指南、loaders、plugins
快速上手
npm init -ynpm install webpack webpack-cli --save-dev{ "private": true, // 防止意外发布 "scripts": { "build": "webpack" // 打包命令 },}创建index.html和src目录,并写两个模块化文件:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title></head><body> <script type="module" src="src/index.js"></script></body></html>import { createTitle } from "./module.js";document.body.append(createTitle('Hello World'));export const createTitle = (title) => { const element = document.createElement('h2') element.textContent = title element.addEventListener('click', () => { alert(title) }) return element}直接打开 index.html 可以看到页面上的 Hello World
接着使用webpack进行打包
npx webpack# 或npm run buildwebpack自动创建了 dist 目录,存放了打包好的 main.js
(()=>{"use strict";document.body.append((e=>{const t=document.createElement("h2");return t.textContent=e,t.addEventListener("click",(()=>{alert(e)})),t})("Hello World"))})();改为引入main.js
<!-- <script type="module" src="src/index.js"></script> --><!-- 打包好的main.js已经是ES5语法,可以去掉type="module" --><script src="dist/main.js"></script>打开 index.html 仍然可以看到 Hello World
Webpack4以后,支持这样0配置的方式,快速打包项目,src是默认打包入口,dist是输出,index.js -> main.js
配置
根目录的 webpack.config.js 是webpack默认的配置文件
可以手动创建配置文件,或在 VSCode 中下载webpack插件,通过 Webpack Create 命令,初始化 webpack
const path = require('path');module.exports = { // 设置模式为开发模式 mode: 'development', // 应用程序的入口点 entry: path.join(__dirname, 'src', 'index'), // 启用监视模式,以在文件更改时进行自动重新编译 watch: true, // 输出配置 output: { // 存储输出文件的目录 path: path.join(__dirname, 'dist'), // 打包文件的公共路径(由浏览器用于加载资源) publicPath: '/dist/', // 主要包的输出文件名 filename: "bundle.js", // 动态加载的代码的输出文件名(代码分割) chunkFilename: '[name].js' }, // 用于处理不同文件类型的模块配置 module: { // 使用Babel处理JavaScript和JSX文件的规则 rules: [{ // 用于匹配以.jsx或.js结尾的文件 test: /.jsx?$/, // 只处理src目录下的文件 include: [ path.resolve(__dirname, 'src') ], // 排除node_modules目录下的文件 exclude: [ path.resolve(__dirname, 'node_modules') ], // 使用babel-loader进行转译 loader: 'babel-loader', options: { // 使用指定的Babel预设 presets: [ ["@babel/env", { // 指定目标浏览器版本为最近的两个Chrome版本 "targets": { "browsers": "last 2 chrome versions" } }] ] } }] }, // 配置模块解析的文件扩展名 resolve: { extensions: ['.json', '.js', '.jsx'] }, // 生成源映射以方便调试 devtool: 'source-map', // 配置开发服务器 devServer: { // 提供内容的基本目录 contentBase: path.join(__dirname, '/dist/'), // 启用内联模式,自动注入脚本以处理实时更新 inline: true, // 服务器主机 host: 'localhost', // 服务器端口 port: 8080, }};JS配置文件运行在node环境中,所以需要使用CommonJS写法
插件还会自动安装这些插件:
"devDependencies": { "webpack": "^5.74.0", "webpack-cli": "^4.10.0", "@babel/core": "^7.18.13", "@babel/preset-env": "^7.18.10", "babel-loader": "^8.2.5", "webpack-dev-server": "^4.10.0"}基本概念
entry: webpack打包的入口起点,从这里开始根据各个文件之间的依赖来对文件进行打包。单页面应用只有一个入口起点,多页面应用则存在多个入口起点output: 打包文件输出定义的地方,定义打包后输出文件的名字以及输出路径等mode: 模式,webpack打包的模式,分为三种 development、production、noneloader: 对javascript等文件进行预处理的,可以通过loader来构建包含javascript在内的任何静态资源plugin: 插件是用来解决loader解决不了的问题,它可以在webpack构建过程中任何一个节点来调用
createapp.dev 是一个创建自定义 webpack 配置的在线工具
使用不同的配置文件:
"scripts": { "build": "webpack --config prod.config.js"}入口和上下文
入口对象是用于 webpack 查找开始构建 bundle 的地方。上下文是入口文件所处的目录的绝对路径的字符串。文档
基础目录context
context 基础目录,绝对路径,解析入口点(entry point)和加载器(loader)
context: path.resolve(__dirname, 'src'),配置了context后,entry路径相对于context
entry: './src/index.js',// 配置context后context: path.resolve(__dirname, 'src'),entry: './index.js',入口entry
entry指示webpack使用一个或多个模块,来作为构建应用的入口,webpack会找出哪些模块和是入口起点的直接或者间接的依赖,并将其打包到一起。默认值 ./src/index.js
**value类型:**string、array、object
入口分为单入口(单页应用SPA)和多入口(多页面应用)
1、单入口 单入口主要使用string、array为值,应用于单页应用SPA
entry: './index.js',// 将这两个文件以及其中所依赖的代码打包到同一个文件中entry: ['./index.js', './main.js'],这种写法默认的chunkname是main,是object的简略写法
entry: { main: './index.js',},entry: { main: ['./index.js', './main.js'],},2、多入口 多入口即有多个html,分别需要不同的打包好的js
{ entry: { main: './src/main.js', bundle: './src/index.js' }, // 这里的[name]相当于是个占位符,值就是上面入口的key值,单入口时可以写死,不过不建议 output: { // 多入口需要对应多出口,通常用[name]占位,不然打包会报错 filename: '[name].js' }}可以看到dist目录下打包好了 main.js 和 index.js 两个文件
3、更多配置 打包入口不仅仅是写一个入口文件地址就可以,它还有额外的配置:
dependOn: 指当前入口文件所依赖的模块,这些模块必须在入口文件被加载前加载filename: 指定要输出的文件名称(优先级高于output中的filename和path)import: 启动时要加载的模块(入口文件地址)
{ entry: { index: './index.js', main: './main.js', catalog: { import: './catalog.js', filename: 'pages/log.js', dependOn: 'main', }, }, output: { filename: '[name].js', }}出口output
output配置的作用是告知webpack如何向硬盘写入打包好的文件。
**注意:**entry可以存在多个入口,但output只有一个出口配置
配置项:
- filename 打包文件名,默认main.js
- path 打包文件输出路径,默认dist
- clean 输出包前清空输出目录,默认false
1、filename
// [name]对应entry的keyoutput: { filename: '[name].js',}// 使用内部chunk id占位output: { filename: '[id].bundle.js', // main-0.js},// 使用由生成的内容产生的 hash,通常和[name]组合使用output: { filename: '[name]-[contenthash].js', // main-8bc05850732530fe321c.js},2、path 和 clean
output: { path: path.resolve(__dirname, 'dist'), filename: '[name]-[contenthash].js', clean: true,}工作模式mode
mode 用于设置webpack的工作模式,告知 webpack 使用相应模式的内置优化,文档
module.exports = { mode: 'development',};三种工作模式:
- production 默认,生产模式,启用自动压缩代码、去除业务无关代码等
- development 开发模式
- none 不使用任何默认优化选项
打包结果分析
将 mode 设为 none,查看输出文件
整个打包好的模块是一个立即执行函数
/******/ (() => { // webpackBootstrap/******/ })();展开一层
/******/ (() => { // webpackBootstrap/******/ "use strict";/******/ var __webpack_modules__ = ([/******/ ]);/************************************************************************//******/ // The module cache/******/ var __webpack_module_cache__ = {};/******//******/ // The require function/******/ function __webpack_require__(moduleId) {/******/ }/******//************************************************************************//******/ /* webpack/runtime/define property getters *//******/ (() => {/******/ })();/******//******/ /* webpack/runtime/hasOwnProperty shorthand *//******/ (() => {/******/ })();/******//******/ /* webpack/runtime/make namespace object *//******/ (() => {/******/ })();/******//************************************************************************/var __webpack_exports__ = {};// This entry need to be wrapped in an IIFE because it need to be isolated against other entry modules.(() => {})();
// This entry need to be wrapped in an IIFE because it need to be isolated against other entry modules.(() => {})();
/******/ })();先看最末尾的两个IIFE,两个入口文件 index.js 和 main.js,分别被打包为了立即执行函数,以此实现私有作用域
// This entry need to be wrapped in an IIFE because it need to be isolated against other entry modules.(() => {var __webpack_exports__ = {};__webpack_require__.r(__webpack_exports__);/* harmony import */ var _module_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
document.body.append((0,_module_js__WEBPACK_IMPORTED_MODULE_0__.createTitle)('Hello World'));})();
// This entry need to be wrapped in an IIFE because it need to be isolated against other entry modules.(() => {})();再看其它代码
// 这个数组包含了Webpack构建过程中生成的模块信息。// 数组的每个元素对应一个模块,具体模块的内容在数组索引位置的函数中定义var __webpack_modules__ = ([/******/]);// 缓存已经加载的模块,以避免重复加载var __webpack_module_cache__ = {};// 模块加载函数,用于加载模块并执行其中的代码。// 它接受一个模块ID作为参数,检查是否已经缓存了该模块,如果没有,则执行模块代码,并将模块的导出结果缓存起来function __webpack_require__(moduleId) {/******/}// 三个立即执行函数,往__webpack_require__上挂载了一些数据和工具函数/******/ /* webpack/runtime/define property getters *//******/ (() => {/******/ })();/******//******/ /* webpack/runtime/hasOwnProperty shorthand *//******/ (() => {/******/ })();/******//******/ /* webpack/runtime/make namespace object *//******/ (() => {/******/ })();// 用于存储模块的导出内容var __webpack_exports__ = {};loader
loader是webpack实现前端模块化的核心,用于将指定格式的资源文件按一定格式进行转换输出
例如,可以使用 loader 告诉 webpack 加载 CSS 文件,或者将 TypeScript 转为 JavaScript。
官方loaders
特点:
- 单一职责:一个Loader只做一件事情,正因为职责越单一,所以Loaders的组合性强,可配置性好
- loader支持链式调用,上一个loader的处理结果可以传给下一个loader接着处理,上一个Loader的参数options可以传递给下一个loader,直到最后一个loader,返回Webpack所期望的JavaScript
webpack内部的default loader只能处理JavaScript,想要处理如css、ts等其它类型文件,就要安装对应的loader
loader可以分为三类:
- 编译转换型:如css-loader
- 文件操作型:如file-loader
- 代码检查型:如eslint-loader
案例:
// 用于处理不同文件类型的模块配置module: { // 使用Babel处理JavaScript和JSX文件的规则 rules: [{ // 用于匹配以.jsx或.js结尾的文件 test: /.jsx?$/, // 只处理src目录下的文件 include: [ path.resolve(__dirname, 'src') ], // 排除node_modules目录下的文件 exclude: [ path.resolve(__dirname, 'node_modules') ], // 使用babel-loader进行转译 loader: 'babel-loader', options: { // 使用指定的Babel预设 presets: [ ["@babel/env", { // 指定目标浏览器版本为最近的两个Chrome版本 "targets": { "browsers": "last 2 chrome versions" } }] ] } }]},Module的文档:module
加载css
查看官方文档指南-管理资源-加载CSS
安装所需的loader
npm install --save-dev style-loader css-loader添加配置,css-loader将css文件打包为js模块,style-loader把 CSS 插入到 DOM 中(css-loader将css push到一个数组中,style-loader将数组中的css通过style标签追加到html-head中)
module.exports = { module: { rules: [ { // 正则匹配loader要处理的资源 test: /\.css$/i, // 逆序执行,从右往左 use: ['style-loader', 'css-loader'], }, ], },}src目录下创建index.css
body{ background-color: #ccc;}打包入口中使用该css
import './index.css'现在样式已经生效
webpack推荐我们使用import根据JS代码的需要动态导入资源,就像刚刚import css一样,这样的代码与资源的关系,更符合模块化的依赖思想
加载其它资源
webpack5使用资源模块Asset Modules来加载图片、字体等资源,webpack4则使用file-loader和url-loader等
file-loader
安装file-loader:
npm i file-loader -D在代码所需的地方导入图片并使用
import avatar from './avatar.png' // 导入打包后资源的路径const img = new Image();img.src = avatar;document.body.append(img);配置规则
module: { rules: [ /******/ { test: /\.png$/, use: 'file-loader', } ],},打包后,在dist目录下生成了871132b331c17257fcba75273b57f9fe.png,这是将文件的hash值作为了打包后的文件名,当然,这也是可以自定义的。
{ test: /\.png$/, use: { loader: 'file-loader', options: { // 默认[hash].[ext] name: '[path][name].[ext]', } },}这样就能保留图片的原始相对路径和名称。
url-loader
file-loader拷贝文件到输出目录,而url-loader通过durl的形式表示文件
npm i url-loader -Ddurl即Data URLs,可以通过url直接去表示文件的内容,不会产生任何请求
// 一个html类型的文件内容,编码是utf-8data:text/html;charset=UTF-8,<h1>html content</h1>// 如果是图片这种无法直接通过文本表示的文件,则可以将文件内容进行base64编码data:image/png;base64,iDAHAidhbaIADHA...AHiDAd**最佳实践:**配置小文件使用url-loader,大文件则使用file-loader
{ test: /\.(png|ico)$/, use: { loader: "url-loader", options: { name: "[path][name]_[hash:6].[ext]", limit: 50 * 1024, //小于50kb的进行编码 // 超过这个大小,url-loader会自动调用file-loader }, },}import { createImage } from "./module.js";import avatar from './avatar.png'import icon from './icon.ico'document.body.append(createImage(avatar));document.body.append(createImage(icon));Asset Modules
webpack5使用资源模块Asset Modules来加载图片、字体等资源。
在webpack5之前,通常使用:raw-loader将文件导入为字符串,url-loader将文件作为durl内联到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,并且配置资源体积限制实现。
案例:
{ test: /\.(png|jpg|svg|gif|ico)$/, // type选择类型 type: "asset/resource", // 通过generator属性进行配置 generator: { filename: "img/[name]_[hash:6][ext]", },},{ test: /\.(png|jpg|svg|gif|ico)$/, type: "asset/inline",},最佳实践:type设为asset,添加一个parser属性,并且制定dataUrl的条件,添加maxSize属性;
{ test: /\.(png|jpg|svg|gif|ico)$/, type: "asset", generator: { filename: "img/[name]_[hash:6][ext]", }, parser: { dataUrlCondition: { maxSize: 50 * 1024,//小于50kb进行编码,转为base64 }, },},babel-loader
webpack由于打包需要,会去处理import和export,但对于其它ES6新特性,则不会去做兼容处理
如果需要将代码中的ES6进行转换,则需要babel-loader
// 安装babel-loader及其依赖的核心库和特性转换集合npm install -D babel-loader @babel/core @babel/preset-env**注意:**babel只是转换JS代码的一个平台,还需要用其它的插件,如@babel/preset-env,通过该平台来转换ES6特性
进行配置:
module: { rules: [ { test: /\.js$/i, include: [ path.resolve(__dirname, 'src') ], exclude: [ path.resolve(__dirname, 'node_modules') ], loader: 'babel-loader', options: { presets: ["@babel/preset-env"] } }, ]}开启ESM转CommonJS(会导致Tree Shaking失效,不推荐开启)
presets: [ ["@babel/preset-env", { modules: "commonjs", // 开启ESM转CommonJS,默认:"auto" }]]这样就完成了简单的ES6转换,更完善的使用core-js@3兼容,后面再说吧。
资源加载方式
除了在js文件中使用import加载资源,webpack还会自动处理其它加载资源的方式,如css文件中的url()、@import
例如:当css-loader在处理css文件时遇到url()时,会找到符合的规则对所需的资源进行处理,如使用asset/resource对图片资源处理
body{ background-image: url(avatar.png); background-size: auto;}
/* index.css */@import './main.css';body{ background-color: #ccc;}开发loader
尝试开发一个markdown-loader,深入了解loader的工作过程
文档:编写loader
**功能:**将模块中所需的markdown资源转为html内容导入
在根目录新建markdown-loader.js,一个最简单的loader是一个函数,接收传入的资源内容,若该loader是最后一个执行的,返回结果必须是JS代码
module.exports = source => { console.log(source) return 'console.log(source)'}使用该loader
{ test: /\.md$/i, use: path.resolve(__dirname, 'markdown-loader.js'),}在模块中导入markdown,webpack只会处理模块所依赖的资源
import md from './01.md'console.log(md)打包时控制台输出了markdown的内容。查看打包结果,loader返回的js也在其中,被一个IIFE包裹。
下面继续完成功能:
安装解析markdown内容的模块,使用marked
npm i marked -D修改 markdown-loader.js
const marked = require('marked');
module.exports = source => { console.log(source) const html = marked.parse(source) console.log(html) return 'console.log(source)'}输出如下,现在loader已经能解析markdown文件了
# 简介这是一个**markdown**<h1>简介</h1><p>这是一个<strong>markdown</strong></p>完善loader,将html暴露给模块使用,会作为模块中import markdown文件的default值
const marked = require('marked');
module.exports = source => { const html = marked.parse(source) // html中存在一些字符,使用JSON.stringify进行转译 return `export default ${JSON.stringify(html)}`}现在,markdown-loader就完成了,模块导入的就是解析好的html内容
import md from './01.md'console.log(md)// <h1>简介</h1>// <p>这是一个<strong>markdown</strong></p>当然,markdown-loader也可以直接返回解析好的html内容,再交给loader管道中下一个loader进行处理,webpack只要求最后一个loader返回的需要是JS代码
处理html就需要安装html-loader,npm i html-loader -D
修改代码:
{ test: /\.md$/i, use: [ 'html-loader', path.resolve(__dirname, 'markdown-loader.js'), ]}const marked = require('marked');
module.exports = source => { const html = marked.parse(source) return html}实现的功能也是一样的
plugin
loader用于处理资源的加载,而插件plugin用于实现各种自动化操作,如压缩代码、替换内容、处理资源
官方plugins
打包分析插件
webpack-bundle-analyzer是一个打包分析插件,使用交互式可缩放树形地图可视化,并输出文件的大小。可以方便开发人员检查打包后的文件拆分、分析文件大小。
每次打包时,会自动打开浏览器,访问127.0.0.1:8888查看项目结构
安装:npm i webpack-bundle-analyzer -D
webpack中,插件都需要导入后使用,且通常插件导出的都是一个class,需要new实例。配置项plugins是一个数组,保存插件的实例。
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');/*****/plugins: [ new BundleAnalyzerPlugin(),],自动生成HTML
手动在根目录创建index.html,并配置打包好的JS等资源的路径,这样硬编码过于麻烦且易出错
可以使用html-webpack-plugin简化HTML文件的创建,自动引入打包好的JS模块,这对于那些文件名中包含哈希值,并且哈希值会随着每次编译而改变的 webpack 包特别有用。
安装:npm i html-webpack-plugin --D
const HtmlWebpackPlugin = require('html-webpack-plugin')plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, './index.html'), // 自定义模板 inject: 'body', // 插入到body filename: 'index.html', // 输出文件名,默认index.html title: 'webpack测试', // 自定义title,通过<%= htmlWebpackPlugin.options.title %>在html中使用 minify: true, // 压缩 }),]修改根目录下的index.html,使其作为一个模板
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><%= htmlWebpackPlugin.options.title %></title></head><body> <!-- 去掉js文件的引入,插件会自动引入 --> <!-- <script src="dist/main.js"></script> --></body></html>查看打包后的index.html
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>webpack测试</title></head><body> <script defer src="main_0cc0be.js"></script></body></html>html-webpack-plugin还有其它的配置项,查看官方仓库文档:配置项
若是多页面、多个html文件,则创建多个插件实例加入到plugins数组中
拷贝文件
通常项目中还有一些无需打包的静态文件存放于public目录,这些资源同样需要输出到dist
安装:npm i copy-webpack-plugin -D
const CopyPlugin = require("copy-webpack-plugin");plugins: [ new CopyPlugin({ patterns: [ // 复制public文件夹内的文件到{output}中 { from: path.resolve(__dirname, 'public'), to: "" }, ], }),]开发plugin
相较于loader只作用于模块加载,plugin的作用范围更广。plugin通过常见的钩子机制实现,就像Vue生命周期提供的钩子一样。
webpack提供了很多打包过程中的钩子,plugin向这些钩子上挂载任务,并获取上下文,来实现对资源的操作等功能。
钩子相关文档:compiler-hooks
webpack要求plugin必须是一个函数,或一个包含apply方法的对象,通常是定义一个包含apply方法的类
新建myPlugin.js
export default class { // apply接收一个compiler对象参数 // 这个对象包含了打包过程中所有信息以及用于注册钩子函数 apply(compiler) {}}现在开发一个用于清除webpack生成的/******/注释,以方便阅读打包后的JS代码
明确了功能,考虑需要用到哪些钩子,显然,清除注释要在输出文件前执行,对要输出的内容进行处理。emit钩子符合需求,这个钩子在输出 asset 到 output 目录之前执行。
export default class { // apply接收一个compiler对象参数 // 这个对象包含了打包过程中所有信息以及用于注册钩子函数 apply(compiler) { // 通过hooks属性访问钩子,tap方法注册钩子函数 // tap方法第一个参数为plugin名,第二个参数是接收了compilation对象的钩子函数 compiler.hooks.emit.tap('MyPlugin', compilation => { // compilation是此次打包过程中的上下文,存放了打包过程的信息和结果 // compilation.assets获取即将输出的资源文件信息 for (const name in compilation.assets) { console.log(name); // 输出文件名 } }) }}使用插件
const MyPlugin = require('./myPlugin.js');plugins: [ new MyPlugin(),]输出:
main_28990539038fea465479.jsimg/avatar_871132b331c17257fcba.pngimg/icon_36fa45932bf38a34e9af.icofavicon.icoindex.html插件已经能读取到打包后的文件名,接下来通过正则替换来处理JS文件
module.exports = class { #isJSFile(filename) { // 使用正则表达式检查文件名是否以 .js 结尾 return /\.js$/i.test(filename); }
// apply接收一个compiler对象参数 // 这个对象包含了打包过程中所有信息以及用于注册钩子函数 apply(compiler) { // 通过hooks属性访问钩子,tap方法注册钩子函数 // tap方法第一个参数为plugin名,第二个参数是接收了compilation对象的钩子函数 compiler.hooks.emit.tap('MyPlugin', compilation => { // compilation是此次打包过程中的上下文,存放了打包过程的信息和结果 // compilation.assets获取即将输出的资源文件信息 for (const name in compilation.assets) { // console.log(name); // 使用source方法获取文件内容 if(this.#isJSFile(name)){ let content = compilation.assets[name].source(); // 使用正则去除webpack生成的/******/ content = content.replace(/\/\*{3,}\//g, ''); // 覆盖文件信息 compilation.assets[name] = { // 覆盖内容 source: () => content, // webpack要求指定大小 size: () => content.length } } } }) }}现在,打包的JS文件内容已经去除了/******/
但控制台有警告信息:
(node:76292) [DEP_WEBPACK_COMPILATION_ASSETS] DeprecationWarning: Compilation.assets will be frozen in future, all modifications are deprecated.BREAKING CHANGE: No more changes should happen to Compilation.assets after sealing the Compilation. Do changes to assets earlier, e. g. in Compilation.hooks.processAssets. Make sure to select an appropriate stage from Compilation.PROCESS_ASSETS_STAGE_*.(Use `node --trace-deprecation ...` to show where the warning was created)这是因为Webpack5将在未来版本冻结compilation.assets,需在compiler.hooks.thisCompilation钩子中使用 Compilation 中的 processAssets hook 来对资源进行再处理
module.exports = class { #isJSFile(filename) { // 使用正则表达式检查文件名是否以 .js 结尾 return /\.js$/i.test(filename); }
// apply接收一个compiler对象参数 // 这个对象包含了打包过程中所有信息以及用于注册钩子函数 apply(compiler) { // 使用thisCompilation钩子,在 compilation 对象创建时执行一些自定义逻辑 compiler.hooks.thisCompilation.tap('MyPlugin', compilation => { // processAssets钩子用于在 webpack 编译完成后,但在最终资源输出之前,处理资源文件的阶段执行插件代码 compilation.hooks.processAssets.tap( { name: 'MyPlugin', // https://webpack.docschina.org/api/compilation-hooks/#list-of-asset-processing-stages stage: compilation.PROCESS_ASSETS_STAGE_OPTIMIZE, // 以通用的方式优化已有asset }, (assets) => { for (const name in assets) { if (this.#isJSFile(name)) { // 使用正则表达式去除块注释 const content = assets[name].source().replace(/\/\*{3,}\//g, ''); // 覆盖文件信息 assets[name] = { source: () => content, size: () => content.length, }; } } } ); }) }}优化开发过程
项目打包过程已经自动化了,但开发过程仍然在手动操作
编写代码->命令打包->运行应用->刷新浏览器,这个繁琐的过程也需要自动化,以提高开发效率
提出下面的需求:
- 以 HTTP Server 运行,而不是打开文件浏览
- 自动编译 + 自动刷新
- 提供 Source Map 支持,方便调试
watch工作模式
处于watch工作模式时,webpack会监听文件变化,自动重新打包
添加watch配置:
module.exports = { watch: true,}DevServer
webpack dev server提供了HTTP Server,集成了自动编译和自动刷新浏览器的功能。
该插件会将将打包结果暂时存放于内存,而不输出于硬盘,以提高性能。
安装:npm i webpack-dev-server -D
module.exports = { // 配置开发服务器 devServer: { // 服务器主机 host: 'localhost', // 服务器端口 port: 8080, // 启用Gzip compress: true, // CopyPlugin通常只在项目上线时去使用,开发时通常将静态资源目录配置给devServer,以提高性能 // 使用static配置从目录提供静态文件的选项,默认public static: { // 告诉服务器从哪里提供内容 directory: path.join(__dirname, 'public'), }, }}添加命令脚本
{ "scripts": { "serve": "webpack serve" }}使用命令运行npm run serve
代理API服务
前后端同源部署时,本地开发在请求api时可能有cors问题,可以使用开发服务器代理api请求,服务器间通信就不存在cors了
DevServer就支持proxy配置api代理,文档
devServer: { proxy: { // 代理api路径 '/api': { // localhost:8080/api/user -> api.github.com/api/user target: 'https://api.github.com', // 请求路径重写 /api/user -> /user pathRewrite: { '^/api': '' }, // 将 host 请求头修改为 target 的 URL changeOrigin: true, }, },}Source Map
前端工程化后,源代码和运行代码几乎完全不同,调试和报错都是基于运行代码,调试源代码就成了问题
Source Map用于映射源代码和运行代码之间的关系
一个Source Map的组成:
{ "version": 3, // 当前Map使用的Source Map标准版本 "sources": ["main.js"], // 记录源文件的名称,可以是多个文件 "names": ["global",/****/], // 源代码使用的成员名称,如变量的原名 "mappings": ";/****/" // 核心,记录源码和运行代码一些字符的映射关系}通过一行特定格式的注释引入Source Map
//# sourceMappingURL=main.map如果Source Map不起作用,需在浏览器控制台-设置-偏好设置中启用JavaScript源代映射
使用Devtool在webpack中配置Source Map:
module.exports = { devtool: 'source-map', // 值为Source Map工作模式}相关文章:一文搞懂SourceMap以及webpack devtool
Source Map工作模式:
[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-mapinline-将SourceMap内联到原始文件中,而不是创建一个单独的文件。hidden-仍然会生成.map文件,但是打包后的代码中没有sourceMappingURL,即浏览器不会加载.map文件,控制台中看不到源代码。Map生成后只供服务端分析使用,前端将出错的行列传给服务端。eval-通过eval包裹每个模块打包后代码以及对应生成的SourceMap(不实际生成),因为eval中为字符串形式,进行字符串处理会提升rebuild的速度。nosources-不包含 sourcesContent 内容,调试时只能看到文件信息和行信息,无法看到源码。cheap-[module-]只定位到源码所在的行,不定位至具体的列,构建速度有所提升。如果只用 cheap ,显示的是 loader 编译之后的源代码,加上 module 后会显示编译之前的源代码。
如何选择devtool:
- **production:**none,source-map,hidden-source-map,nosources-source-map
- **development:**eval,eval-source-map,eval-cheap-source-map,eval-cheap-module-source-map
开发环境下,需要频繁的修改代码,更多地考虑的开发效率和调试效率,所以更多关注 performance 中 rebuild 的性能。生产环境下,不必过多关注打包性能,主要考虑 quality 代码的保护性、出错的定位速度已经安全性
热替换HMR
监视模块变动后重新打包、自动刷新会导致页面的一些状态丢失(输入的文本内容),如果能让页面不刷新,模块也能更新,这样的开发体验会好很多
模块热替换HMR(Hot Module Replacement)可以实现无刷更新模块,「webpack 核心特性」模块热替换(HMR)
HMR作用:
- 保留在完全重新加载页面期间丢失的应用程序状态。
- 只更新变更内容,以节省宝贵的开发时间。
- 在源代码中 CSS/JS 产生修改时,会立刻在浏览器中进行更新,这几乎相当于在浏览器 devtools 直接更改样式。
devServer: { hot: true, // 开启HMR // 在构建失败时不刷新页面作为回退 // hot: 'only',}从webpack-dev-server v4开始,HMR已默认启用。会自动应用HotModuleReplacementPlugin插件
**注意:**HMR并不是开箱即用,还需要使用HMR-API手动处理模块的热替换逻辑,否则还会自动刷新,部分loader和插件如style-loader已经处理好了css的热更新逻辑,在Vue等框架下开发,框架本身也处理好了HMR
使用HMR-API手动处理JS模块热替换:
if (module.hot) { module.hot.accept('./library.js', function() { // 对更新过的 library 模块做些事情... });}// orif (import.meta.webpackHot) { import.meta.webpackHot.accept('./library.js', function () { // Do something with the updated library module… });}
// accept方法module.hot.accept( dependencies, // 可以是一个字符串或字符串数组 callback // 用于在模块更新后触发的函数 errorHandler // (err, {moduleId, dependencyId}) => {});案例:
import { appendMarkdown } from "./module.js";import md from './01.md'let mde = appendMarkdown(md);
if (module.hot) { // 处理01.md的更新 module.hot.accept('./01.md', () => { // 热重载,先移除原来的 document.body.removeChild(mde); // 再创建新的 mde = appendMarkdown(md); });}热重载的需要根据自己的业务逻辑去实现,没有通用的方法,这也是webpack没有提供JS模块HMR的原因。
打包后,HMR相关代码会被自动去除
不同环境的配置
不同的环境需要不同的webpack配置,主要是区分生产和开发环境,文档
区分环境有两种方式
- 配置函数中判断env,返回不同的配置信息
- 创建多个配置文件对应不同的环境(推荐)
判断env
/** * * @param {string} env 环境名参数 * @param {array} argv cli传递的所有参数 * @returns {object} webpack配置 */module.exports = (env, argv) => { console.log(env); // 默认的通用配置 const config = { mode: 'none', // 生成源映射以方便调试 devtool: 'eval-source-map', // watch: true, context: path.resolve(__dirname, 'src'), entry: { main: ['./index.js', './main.js'], }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name]_[contenthash].js', clean: true, }, module: { rules: [ { // 正则匹配loader要处理的资源 test: /\.css$/i, // 逆序执行,从右往左 use: ['style-loader', 'css-loader'], }, { test: /\.(png|jpg|svg|gif|ico)$/, type: "asset", generator: { filename: "img/[name]_[contenthash][ext]", }, parser: { dataUrlCondition: { maxSize: 50 * 1024,//小于50kb进行编码,转为base64 }, }, }, { test: /\.js$/i, include: [ path.resolve(__dirname, 'src') ], exclude: [ path.resolve(__dirname, 'node_modules') ], loader: 'babel-loader', options: { presets: ["@babel/preset-env"] } }, { test: /\.md$/i, use: path.resolve(__dirname, 'markdown-loader.js'), } ], }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, './index.html'), // 自定义模板 inject: 'body', // 插入到body filename: 'index.html', // 输出文件名,默认index.html title: 'webpack测试', // 自定义title,通过<%= htmlWebpackPlugin.options.title %>在html中使用 minify: true, // 压缩 }), ], // 配置开发服务器 devServer: { // 服务器主机 host: 'localhost', // 服务器端口 port: 8080, // 使用HMR hot: true, // 启用Gzip compress: true, // CopyPlugin通常只在项目上线时去使用,开发时通常将静态资源目录配置给devServer,以提高性能 // 使用static配置从目录提供静态文件的选项,默认public static: { // 告诉服务器从哪里提供内容 directory: path.join(__dirname, 'public'), }, proxy: { // 代理api路径 '/api': { // localhost:8080/api/user -> api.github.com/api/user target: 'https://api.github.com', // 请求路径重写 /api/user -> /user pathRewrite: { '^/api': '' }, // 将 host 请求头修改为 target 的 URL changeOrigin: true, }, }, } } // 判断环境,修改配置 if (env.production) { // 生产环境 config.mode = 'production'; config.devtool = false; config.plugins = [ ...config.plugins, new CopyPlugin({ patterns: [ // 复制public文件夹内的文件到{output}中 { from: path.resolve(__dirname, 'public'), to: "" }, ], }), ]; } else if (env.development) { // 开发环境 config.mode = 'development'; }
return config}npx webpack --env productionnpx webpack --env development多配置文件
若项目较大配置复杂,就不适合用判断env的方式,写多个配置文件更清晰明了,文档
通常有三个配置文件:
webpack.common.js通用配置文件,写一些项目的通用基础配置webpack.dev.js开发配置文件webpack.prod.js生产配置文件
安装webpack-merge合并配置对象:npm i webpack-merge -D
const path = require('path');const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = { mode: 'none', // 生成源映射以方便调试 devtool: 'source-map', // watch: true, context: path.resolve(__dirname, 'src'), entry: { main: ['./index.js', './main.js'], }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name]_[contenthash].js', clean: true, }, //警告 webpack 的性能提示 performance: { hints: 'warning', //入口起点的最大体积 maxEntrypointSize: 1024 * 1024 * 10, //生成文件的最大体积 maxAssetSize: 1024 * 1024, //只给出 js 文件的性能提示 assetFilter: function (assetFilename) { return /\.js$/.test(assetFilename); } }, module: { rules: [ { // 正则匹配loader要处理的资源 test: /\.css$/i, // 逆序执行,从右往左 use: ['style-loader', 'css-loader'], }, { test: /\.(png|jpg|svg|gif|ico)$/, type: "asset", generator: { filename: "img/[name]_[contenthash][ext]", }, parser: { dataUrlCondition: { maxSize: 50 * 1024,//小于50kb进行编码,转为base64 }, }, }, { test: /\.js$/i, include: [ path.resolve(__dirname, 'src') ], exclude: [ path.resolve(__dirname, 'node_modules') ], loader: 'babel-loader', options: { presets: ["@babel/preset-env"] } }, { test: /\.md$/i, use: path.resolve(__dirname, 'markdown-loader.js'), } ], }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, './index.html'), // 自定义模板 inject: 'body', // 插入到body filename: 'index.html', // 输出文件名,默认index.html title: 'webpack测试', // 自定义title,通过<%= htmlWebpackPlugin.options.title %>在html中使用 minify: true, // 压缩 }), ], // 配置开发服务器 devServer: { // 服务器主机 host: 'localhost', // 服务器端口 port: 8080, // 使用HMR hot: true, // 启用Gzip compress: true, // CopyPlugin通常只在项目上线时去使用,开发时通常将静态资源目录配置给devServer,以提高性能 // 使用static配置从目录提供静态文件的选项,默认public static: { // 告诉服务器从哪里提供内容 directory: path.join(__dirname, 'public'), }, proxy: { // 代理api路径 '/api': { // localhost:8080/api/user -> api.github.com/api/user target: 'https://api.github.com', // 请求路径重写 /api/user -> /user pathRewrite: { '^/api': '' }, // 将 host 请求头修改为 target 的 URL changeOrigin: true, }, }, }}const common = require("./webpack.common");const { merge } = require("webpack-merge");
module.exports = merge(common, { mode: 'development', devtool: 'eval-source-map',});const common = require("./webpack.common");const path = require('path');const CopyPlugin = require("copy-webpack-plugin");const { merge } = require("webpack-merge");
module.exports = merge(common, { mode: 'production', devtool: false, plugins: [ new CopyPlugin({ patterns: [ // 复制public文件夹内的文件到{output}中 { from: path.resolve(__dirname, 'public'), to: "" }, ], }), ],});通过 --config 标志使用不同的配置文件
"scripts": { "build": "webpack --config webpack.prod.js", "build-dev": "webpack --config webpack.dev.js", "prod": "webpack serve --config webpack.prod.js", "dev": "webpack serve --config webpack.dev.js"},内置插件
webpack本身内置了很多插件对bundle进行优化,且一些插件在mode: production时会自动开启,进行一些通用的优化操作,优化(Optimization)
DefinePlugin
DefinePlugin用来注入全局成员,在编译时将代码中的变量替换为其他值或表达式
在mode: production时,DefinePlugin默认启用,并注入了process.env.NODE_ENV,许多第三方的模块使用这个常量来判断当前环境
DefinePlugin接收一个对象,对象中的值若为字符串,将被作为代码片段使用,
const webpack = require('webpack');
plugins: [ new webpack.DefinePlugin({ // 使用JSON.stringify作为表示字符串的代码片段 API_BASE_URL: JSON.stringify('http://api.github.com'), }),],
console.log(API_BASE_URL)Tree Shaking
Tree Shaking用于移除JS上下文中的未引用代码(dead-code),基于ESM
在mode: production时Tree Shaking功能自动开启,也可通过配置开启
module.exports = { optimization: { // 只导出使用了的成员 usedExports: true, },}测试代码:
const info = { name: 'chuckle', age: '20',}export function getName(){ return info.name}export function getAge(){ return info.age}export function logName(){ console.log(info.name);}export function logAge(){ console.log(info.age);}打包结果,仍然存在未使用的代码片段,这是因为usedExports只是标记了未引用代码,而optimization.minimize才是用于压缩bundle,并去除未引用代码,两者搭配才实现了Tree Shaking
/* harmony export */ __webpack_require__.d(__webpack_exports__, {/* harmony export */ logName: () => (/* binding */ logName)/* harmony export */ });/* unused harmony exports getName, getAge, logAge */var info = { name: 'chuckle', age: '20'};function getName() { return info.name;}function getAge() { return info.age;}function logName() { var _console; /* eslint-disable */(_console = console).log.apply(_console, _toConsumableArray(oo_oo("3634127370_12_2_12_24_4", info.name)));}function logAge() { var _console2; /* eslint-disable */(_console2 = console).log.apply(_console2, _toConsumableArray(oo_oo("3634127370_15_2_15_23_4", info.age)));}压缩代码去除未引用
optimization.minimize压缩bundle并去除未引用代码,mode: production默认开启
module.exports = { //... optimization: { minimize: true, },};打包后,未使用过的代码已经去除
e.d(_,{logName:()=>d});var t={name:"chuckle",age:"20"};副作用
将文件标记为side-effect-free(无副作用)安全地删除未用到的export,目的是为了给Tree Shaking更大的优化空间
副作用:模块执行时,除了导出成员之外所作的事情
optimization.sideEffects告知webpack去辨识package.json中的副作用标记或规则,以跳过那些当导出不被使用且被标记不包含副作用的模块。
optimization: { sideEffects: true, // 开启},"sideEffects": false, // 项目所有模块都无副作用// or"sideEffects": ["*.css"], // 使用css-loader且在css文件中使用import时很有必要常见副作用代码:
// 数字前补全0Number.prototype.pad = function (size) { let result = String(this); while (result.length < size) { result += '0'; } return result;}
// 导入使用import './pad'若没有标记副作用,打包会排除该代码片段
Uncaught TypeError: 8.pad is not a function at ./main.js (main.js:17:72) at __webpack_require__ (bootstrap:24:1) at startup:7:1 at startup:7:1标记副作用
"sideEffects": ["*.css", "./src/pad.js"],模块分包
webpack会将所有小颗粒度的模块,从入口模块开始打包到一个JS模块,若项目较大,bundle也会很大,一些模块可以分包出来,减小bundle的体积,文档
模块分包办法:
- 多入口打包
- 动态导入
多入口打包
多入口打包通常用于多页面应用,但也可以一个页面应用多个bundle,实现分包
同事可以使用dependOn指定依赖的公共模块,并在html中引入公共模块
entry: { main: { import: ['./index.js', './main.js'], dependOn: 'shared', }, about: { import: ['./about.js'], dependOn: 'shared', }, shared: './module.js',},output: { path: path.resolve(__dirname, 'dist'), filename: '[name]_[contenthash].js', clean: true,},plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, './index.html'), // 自定义模板 inject: 'body', // 插入到body filename: 'index.html', // 输出文件名,默认index.html title: 'webpack测试', // 自定义title,通过<%= htmlWebpackPlugin.options.title %>在html中使用 minify: true, // 压缩 chunks: ['main', 'shared'], // 公共模块也要引入 }), new HtmlWebpackPlugin({ template: path.resolve(__dirname, './about.html'), inject: 'body', filename: 'about.html', title: '关于页', minify: true, // 压缩 chunks: ['about', 'shared'], }),],如果想要在一个 HTML 页面上使用多个入口,还需设置 runtimeChunk
optimization: { // 用于指定运行时(runtime)代码的拆分策略 runtimeChunk: 'single',},自动提取
当多个模块引入了同一个模块,可以使用splitChunks将其自动提取为独立的chunk
splitChunks: { chunks: 'all', minSize: 20 * 1024, // 设置最小分包大小,默认20000 minSizeReduction: 50 * 1024, // 需要分包的bundle最小大小},minSizeReduction:设置需要分包的bundle最小大小,这意味着如果分割成一个 chunk 并没有减少主 chunk(bundle)的给定字节数,它将不会被分割,即使它满足 splitChunks.minSize
这样就不用使用dependOn指定依赖的公共模块了
动态导入
动态导入实现按需加载,需要某个模块再加载该模块,所有动态导入的模块都会被自动分包
使用ESM的import()实现动态导入
下面是一个hash路由的小demo
<body> <header> <a href="#Home">首页</a> <a href="#List">列表</a> </header> <div id="main"></div></body>import home from './home';import list from './list';
const render = ()=>{ const hash = window.location.hash || "#Home"; const mainEle = document.querySelector('#main'); mainEle.innerHTML = ""; if(hash === "#List"){ mainEle.appendChild(list()); }else if(hash === "#Home"){ mainEle.appendChild(home()); }}render();
window.addEventListener("hashchange", render)import { renderMarkdown } from "../module";import './index.css'import md from './index.md'export default () => renderMarkdown(md, "home");import { renderMarkdown } from "../module";import './index.css'import md from './index.md'export default () => renderMarkdown(md, 'list');若不使用动态导入,不同路由页引入的css都同时影响样式,导致样式冲突,下面使用import()改造
const render = () => { const hash = window.location.hash || "#Home"; const mainEle = document.querySelector('#main'); mainEle.innerHTML = ""; if (hash === "#List") { import('./list').then(({ default: list }) => { mainEle.appendChild(list()); }) } else if (hash === "#Home") { import('./home').then(({ default: home }) => { mainEle.appendChild(home()); }) }}render();
window.addEventListener("hashchange", render)魔法注释
在动态导入过程中可以加入魔法注释,控制分包命名、合并、开启预加载
// 使用chunkName设置分包命名import(/* webpackChunkName: "home" */'./home')import(/* webpackChunkName: "list" */'./list')// 相同chunkName会被打包到一起import(/* webpackChunkName: "components" */'./home')import(/* webpackChunkName: "components" */'./list')// 开启预加载import(/* webpackPrefetch: true */'./list');css处理进阶
css这东西吧,还得琢磨琢磨
css-loader模块化
开启options.modules,css-loader会将样式中的类名进行转换,根据模块路径和类名生成转换为一个唯一的hash值。文档
**作用:**CSS的规则都是全局的,任何一个组件的样式规则,都对整个页面有效。产生局部作用域的唯一方法,就是使用一个独一无二的class的名字,不会与其他选择器重名
rules: [ { test: /\.css$/i, loader: "css-loader", options: { modules: true, }, },],通过导出对象访问类名来应用样式
.list{ background-color: #2f59b4}import { renderMarkdown } from "../module";import css from './index.css'import md from './index.md'export default () => renderMarkdown(md, css.list);提取css
之前css通过style-loader直接应用到style标签内,而css则保存在js模块中,若css体积较大,还是提取css为一个单独的文件好
MiniCssExtractPlugin将 CSS 提取到单独的文件中,为每个包含 CSS 的 JS 文件创建一个 CSS 文件,并且支持 CSS 和 SourceMaps 的按需加载
安装:npm i mini-css-extract-plugin -D
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = { plugins: [new MiniCssExtractPlugin()], module: { rules: [ { test: /\.css$/i, use: [MiniCssExtractPlugin.loader, "css-loader"], }, ], },};压缩css
webpack本身只能压缩JS模块,需要压缩CSS等其它类型文件需要安装对应的插件
使用CssMinimizerWebpackPlugin优化和压缩独立的CSS模块
安装:npm i css-minimizer-webpack-plugin -D
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
optimization: { minimize: true, // 压缩类的插件应配置在minimizer,受minimize控制 minimizer: [ // 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 `terser-webpack-plugin`) `...`, new CssMinimizerPlugin(), ],},文件hash
开启静态资源的客户端缓存后,为了能及时更新资源,资源文件就需要带上hash,文档
绝大多数插件都支持使用filename配置输出的文件名
三种hash:
// [hash]项目级hash,项目中一个模块有变化,该hash就变化filename: '[name]_[hash].js',// [chunkhash]chunk级hash,同一路的打包相同hashfilename: '[name]_[chunkhash].js',// [contenthash]文件级hash,根据输出文件内容的hashfilename: '[name]_[contenthash].js',指定hash长度
filename: '[name]_[contenthash:8].js',控制缓存最佳实践:8位contenthash
总结
{ "name": "webpack01", "version": "1.0.0", "description": "", "private": true, "scripts": { "build": "webpack --config webpack.prod.js", "build-dev": "webpack --config webpack.dev.js", "prod": "webpack serve --config webpack.prod.js", "dev": "webpack serve --config webpack.dev.js" }, "sideEffects": [ "*.css", "./src/pad.js" ], "keywords": [], "author": "", "license": "ISC", "devDependencies": { "@babel/core": "^7.23.3", "@babel/preset-env": "^7.23.3", "babel-loader": "^8.3.0", "copy-webpack-plugin": "^11.0.0", "css-loader": "^6.8.1", "css-minimizer-webpack-plugin": "^5.0.1", "file-loader": "^6.2.0", "html-loader": "^4.2.0", "html-webpack-plugin": "^5.5.3", "marked": "^9.1.6", "mini-css-extract-plugin": "^2.7.6", "style-loader": "^3.3.3", "url-loader": "^4.1.1", "webpack": "^5.74.0", "webpack-bundle-analyzer": "^4.9.1", "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.15.1", "webpack-merge": "^5.10.0" }}配置TS环境
安装TS相关依赖:
- 编译TS
npm install ts-loader -D - TS环境
npm install typescript -D
const path = require('path');const HtmlWebpackPlugin = require('html-webpack-plugin');const webpack = require('webpack');const MiniCssExtractPlugin = require("mini-css-extract-plugin");const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = { mode: 'none', // 生成源映射以方便调试 devtool: 'source-map', // watch: true, context: path.resolve(__dirname, 'src'), entry: { main: { import: ['./index.ts'], } }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name]_[contenthash].js', clean: true, }, optimization: { usedExports: true, // minimize: true, // 用于指定运行时(runtime)代码的拆分策略 // runtimeChunk: 'single', // splitChunks: { // chunks: 'all', // minSize: 10 * 1024, // 设置最小分包大小 // // minSizeReduction: 50 * 1024, // 需要分包的bundle最小大小 // }, minimizer: [ // 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 `terser-webpack-plugin`) `...`, new CssMinimizerPlugin(), ], }, //警告 webpack 的性能提示 performance: { hints: 'warning', //入口起点的最大体积 maxEntrypointSize: 1024 * 1024 * 10, //生成文件的最大体积 maxAssetSize: 1024 * 1024, //只给出 js 文件的性能提示 assetFilter: function (assetFilename) { return /\.ts$/.test(assetFilename); } }, module: { rules: [ { // 正则匹配loader要处理的资源 test: /\.css$/i, // 逆序执行,从右往左 use: [ // { // loader: 'style-loader', // }, { loader: MiniCssExtractPlugin.loader }, { loader: 'css-loader', // options: { // modules: true // css-loader会将样式中的类名进行转换,根据模块路径和类名生成转换为一个唯一的hash值。 // }, }, ], }, { test: /\.(png|jpg|svg|gif|ico)$/, type: "asset", generator: { filename: "img/[name]_[contenthash][ext]", }, parser: { dataUrlCondition: { maxSize: 50 * 1024,//小于50kb进行编码,转为base64 }, }, }, { test: /\.js$/i, include: [ path.resolve(__dirname, 'src') ], exclude: [ path.resolve(__dirname, 'node_modules') ], loader: 'babel-loader', options: { presets: ["@babel/preset-env"] // presets: [ // ["@babel/preset-env", { // modules: "commonjs", // 开启ESM转CommonJS // }] // ] } }, { test: /\.ts$/i, loader: "ts-loader", include: [ path.resolve(__dirname, 'src') ], exclude: [ path.resolve(__dirname, 'node_modules') ], } ], }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, './index.html'), // 自定义模板 inject: 'body', // 插入到body filename: 'index.html', // 输出文件名,默认index.html title: 'webpack测试', // 自定义title,通过<%= htmlWebpackPlugin.options.title %>在html中使用 minify: true, // 压缩 chunks: ['main'], }), new webpack.DefinePlugin({ API_BASE_URL: JSON.stringify('http://api.github.com'), }), new MiniCssExtractPlugin(), ], resolve: { extensions: ['.ts', '.js'], alias: { '@': path.resolve(__dirname, './src') } }, // 配置开发服务器 devServer: { // 服务器主机 host: 'localhost', // 服务器端口 port: 8080, // 使用HMR hot: true, // 启用Gzip compress: true, // CopyPlugin通常只在项目上线时去使用,开发时通常将静态资源目录配置给devServer,以提高性能 // 使用static配置从目录提供静态文件的选项,默认public static: { // 告诉服务器从哪里提供内容 directory: path.join(__dirname, 'public'), }, proxy: { // 代理api路径 '/api': { // localhost:8080/api/user -> api.github.com/api/user target: 'https://api.github.com', // 请求路径重写 /api/user -> /user pathRewrite: { '^/api': '' }, // 将 host 请求头修改为 target 的 URL changeOrigin: true, }, }, }}{ "compilerOptions": { "incremental": false, // TS编译器在第一次编译之后会生成一个存储编译信息的文件,第二次编译会在第一次的基础上进行增量编译,可以提高编译的速度 // "tsBuildInfoFile": "./buildFile", // 增量编译文件的存储位置 "diagnostics": true, // 打印诊断信息 "target": "esnext", /* 指定 ECMAScript 目标版本:'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ "module": "esnext", /* 输出的代码使用什么方式进行模块化: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ "lib": [ /* 指定引用的标准库 */ "esnext", "dom", "dom.iterable", ], // TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置,如es8的数组新特性需要引入"ES2019.Array", "allowJs": true, // 允许编译器编译JS,JSX文件 "checkJs": true, // 允许在JS文件中报错,通常与allowJS一起使用 "outDir": "./dist", // 指定输出目录 "rootDir": "./src", // 指定输出文件目录(用于输出),用于控制输出目录结构 "declaration": true, // 生成声明文件,开启后会自动生成声明文件 "declarationDir": "./dist/typings", // 指定生成声明文件存放目录 // "emitDeclarationOnly": true, // 只生成声明文件,而不会生成js文件 "sourceMap": false, // 生成目标文件的sourceMap文件 // "inlineSourceMap": true, // 生成目标文件的inline SourceMap,inline SourceMap会包含在生成的js文件中 "declarationMap": false, // 为声明文件生成sourceMap // "typeRoots": [], // 声明文件目录,默认时node_modules/@types "types": [], // 加载的声明文件包 "removeComments": true, // 删除注释 "noEmit": false, // 不输出文件,即编译后不会生成任何js文件 "noEmitOnError": true, // 发送错误时不输出任何文件 "noEmitHelpers": true, // 不生成helper函数,减小体积,需要额外安装,常配合importHelpers一起使用 "importHelpers": true, // 通过tslib引入helper函数,文件必须是模块 "downlevelIteration": true, // 降级遍历器实现,如果目标源是es3/5,那么遍历器会有降级的实现 "strict": true, // 开启所有严格的类型检查 "alwaysStrict": true, // 在代码中注入'use strict' "noImplicitAny": true, // 不允许隐式的any类型 "strictNullChecks": true, // 不允许把null、undefined赋值给其他类型的变量 "strictFunctionTypes": true, // 不允许函数参数双向协变 "strictPropertyInitialization": true, // 类的实例属性必须初始化 "strictBindCallApply": true, // 严格的bind/call/apply检查 "noImplicitThis": true, // 不允许this有隐式的any类型 "noUnusedLocals": true, // 检查只声明、未使用的局部变量(只提示不报错) "noUnusedParameters": true, // 检查未使用的函数参数(只提示不报错) "noFallthroughCasesInSwitch": true, // 防止switch语句贯穿(即如果没有break语句后面不会执行) "noImplicitReturns": true, //每个分支都会有返回值 "esModuleInterop": true, // 允许export=导出,由import from 导入 "allowUmdGlobalAccess": true, // 允许在模块中全局变量的方式访问umd模块 "moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入 "baseUrl": "./", // 解析非相对模块的基地址,默认是当前目录 "paths": { // 路径映射,相对于baseUrl // 如使用jq时不想使用默认版本,而需要手动指定版本,可进行如下配置 // "jquery": [ // "node_modules/jquery/dist/jquery.min.js" // ], "@/*": [ "src/*" ] }, "rootDirs": [ "src" ], // 将多个目录放在一个虚拟目录下,用于运行时,即编译后引入文件的位置可能发生变化,这也设置可以虚拟src和out在同一个目录下,不用再去改变路径也不会报错 "listEmittedFiles": true, // 打印输出文件 "listFiles": true, // 打印编译的文件(包括引用的声明文件) "experimentalDecorators": true, "emitDecoratorMetadata": true, "resolveJsonModule": true, "allowImportingTsExtensions": true, }, // 指定一个匹配列表(属于自动指定该路径下的所有ts相关文件) "include": [ "src/**/*", ], // 指定一个排除列表(include的反向操作) // "exclude": [ // "demo.ts" // ], // 指定哪些文件使用该配置(属于手动一个个指定文件) // "files": [ // "demo.ts" // ]}
评论