前言 从Webpack开始的前端工程化探索
模块化 随着前端应用的日益复杂,程序员需要更高效的代码组织形式,以便提高可维护性并提升开发效率。
模块化将复杂的代码按功能的不同,分为不同的模块,单独维护,提高开发效率。
模块:
将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起。
块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信。
演变 1、文件划分 将不同的功能及其状态数据存放在单独的JS文件中。约定一个文件就是一个模块,以单独的script标签引入至HTML。
虽然实现了功能的划分,但缺点也十分明显:
污染全局作用域
命名冲突
无法管理模块的依赖关系
2、命名空间 在文件划分的基础上,约定每个模块只暴露一个全局的对象,对象包裹着模块的方法和状态。
虽然避免了命名冲突,但仍然没有私有空间,
3、IIFE 通过立即执行函数实现私有空间,使用window暴露模块的成员,通过参数声明依赖。
1 2 3 4 5 6 7 8 9 10 11 (($ )=> { 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 模块,都是运行时的
1 2 3 4 <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仍然有兼容性问题,早期的浏览器,特别是国产和手机上的浏览器
使用 caniuse-cmd 检查兼容性 1 2 npm install -g caniuse-cmd caniuse 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全局成员的值:
index.mjs 1 2 3 4 5 6 7 8 9 10 11 import { fileURLToPath } from 'url' import { dirname } from 'path' const __filename = fileURLToPath (import .meta .url )const __dirname = dirname (__filename)console .log (__filename)console .log (__dirname)
前端打包工具 ESM仍然存在一些问题:
存在兼容性问题
模块文件过多,网络请求频繁
不仅是JS,前端所有的资源,包括CSS、HTML都需要模块化
前端需要更好的工具和规范,让开发者继续享受模块化带来的便利,而不需要担心对生产环境 产生的影响。
需求推动技术的进步 ,打包工具顺势出现了。打包工具解决的是前端整体的模块化,不只是局限于JS的模块化。
如今前端项目的代码组织,已经走上了编辑代码 和最终运行文件 完全两样的形式,一系列工具链 和自动化的思想也融入进了打包工具中,打包也逐渐从一个技术问题,转变为了生态和管理问题。打包工具现在也可称为构建工具 ,支撑着前端工程化。
常见的打包工具:Webpack 、Vite 、Rollup 、esbuild
Webpack Webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具。
学习webpack大体上就是学习webpack.config.js的配置、各种loaders和plugins的使用,所以,多看文档,广泛了解,取所需使用文档 、配置 、指南 、loaders 、plugins
快速上手 安装 1 2 npm init -y npm install webpack webpack-cli --save-dev
package.json 1 2 3 4 5 6 { "private" : true , "scripts" : { "build" : "webpack" } , }
创建index.html和src目录,并写两个模块化文件:
index.html 1 2 3 4 5 6 7 8 9 10 11 <!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 >
src/index.js 1 2 import { createTitle } from "./module.js" ;document .body .append (createTitle ('Hello World' ));
src/module.js 1 2 3 4 5 6 7 8 export const createTitle = (title ) => { const element = document .createElement ('h2' ) element.textContent = title element.addEventListener ('click' , () => { alert (title) }) return element }
直接打开 index.html 可以看到页面上的 Hello World
接着使用webpack进行打包
1 2 3 npx webpack # 或 npm run build
webpack自动创建了 dist 目录,存放了打包好的 main.js
dist/main.js 1 (()=> {"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
1 2 3 <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
插件默认创建的webpack.config.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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 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 : { rules : [{ test : /.jsx?$/ , include : [ path.resolve (__dirname, 'src' ) ], exclude : [ path.resolve (__dirname, 'node_modules' ) ], loader : 'babel-loader' , options : { presets : [ ["@babel/env" , { "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写法
插件还会自动安装这些插件:
1 2 3 4 5 6 7 8 "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、none
loader
: 对javascript等文件进行预处理的,可以通过loader来构建包含javascript在内的任何静态资源
plugin
: 插件是用来解决loader解决不了的问题,它可以在webpack构建过程中任何一个节点来调用
createapp.dev 是一个创建自定义 webpack 配置的在线工具
使用不同的配置文件:
package.json 1 2 3 "scripts" : { "build" : "webpack --config prod.config.js" }
入口和上下文 入口对象是用于 webpack 查找开始构建 bundle 的地方。上下文是入口文件所处的目录的绝对路径的字符串。文档
基础目录context context
基础目录,绝对路径 ,解析入口点(entry point)和加载器(loader)
1 context : path.resolve (__dirname, 'src' ),
配置了context后,entry路径相对于context
1 2 3 4 entry : './src/index.js' ,context : path.resolve (__dirname, 'src' ),entry : './index.js' ,
入口entry entry指示webpack使用一个或多个模块,来作为构建应用的入口,webpack会找出哪些模块和是入口起点的直接或者间接的依赖,并将其打包到一起。默认值 ./src/index.js
value类型: string、array、object
入口分为单入口 (单页应用SPA)和多入口 (多页面应用)
1、单入口 单入口主要使用string、array为值,应用于单页应用SPA
1 2 3 entry : './index.js' ,entry : ['./index.js' , './main.js' ],
这种写法默认的chunkname是main,是object的简略写法
object完整写法 1 2 3 4 5 6 entry : { main : './index.js' , }, entry : { main : ['./index.js' , './main.js' ], },
2、多入口 多入口即有多个html,分别需要不同的打包好的js
1 2 3 4 5 6 7 8 9 10 11 { entry : { main : './src/main.js' , bundle : './src/index.js' }, output : { filename : '[name].js' } }
可以看到dist目录下打包好了 main.js 和 index.js 两个文件
3、更多配置 打包入口不仅仅是写一个入口文件地址就可以,它还有额外的配置:
dependOn
: 指当前入口文件所依赖的模块,这些模块必须在入口文件被加载前加载
filename
: 指定要输出的文件名称(优先级高于output中的filename和path)
import
: 启动时要加载的模块(入口文件地址)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 output : { filename : '[name].js' , } output : { filename : '[id].bundle.js' , }, output : { filename : '[name]-[contenthash].js' , },
2、path 和 clean
1 2 3 4 5 output : { path : path.resolve (__dirname, 'dist' ), filename : '[name]-[contenthash].js' , clean : true , }
工作模式mode mode
用于设置webpack的工作模式,告知 webpack 使用相应模式的内置优化,文档
webpack.config.js 1 2 3 module .exports = { mode : 'development' , };
三种工作模式:
production 默认,生产模式,启用自动压缩代码、去除业务无关代码等
development 开发模式
none 不使用任何默认优化选项
打包结果分析 将 mode 设为 none,查看输出文件
整个打包好的模块是一个立即执行函数
折叠后
展开一层
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 (() => { "use strict" ; var __webpack_modules__ = ([ ]); var __webpack_module_cache__ = {}; function __webpack_require__ (moduleId ) { } (() => { })(); (() => { })(); (() => { })(); var __webpack_exports__ = {};(() => { })(); (() => { })(); })() ;
先看最末尾的两个IIFE,两个入口文件 index.js 和 main.js,分别被打包为了立即执行函数,以此实现私有作用域
1 2 3 4 5 6 7 8 9 10 11 12 (() => { var __webpack_exports__ = {};__webpack_require__.r (__webpack_exports__); var _module_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__ (1 ); document .body .append ((0 ,_module_js__WEBPACK_IMPORTED_MODULE_0__.createTitle )('Hello World' ));})(); (() => { })();
再看其它代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 var __webpack_modules__ = ([]);var __webpack_module_cache__ = {};function __webpack_require__ (moduleId ) {} (() => { })(); (() => { })(); (() => { })(); 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
案例:
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 module : { rules : [{ test : /.jsx?$/ , include : [ path.resolve (__dirname, 'src' ) ], exclude : [ path.resolve (__dirname, 'node_modules' ) ], loader : 'babel-loader' , options : { presets : [ ["@babel/env" , { "targets" : { "browsers" : "last 2 chrome versions" } }] ] } }] },
Module的文档:module
加载css 查看官方文档指南-管理资源-加载CSS
安装所需的loader
1 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中)
1 2 3 4 5 6 7 8 9 10 11 12 module .exports = { module : { rules : [ { test : /\.css$/i , use : ['style-loader' , 'css-loader' ], }, ], }, }
src目录下创建index.css
src/index.css 1 2 3 body { background-color : #ccc ; }
打包入口中使用该css
src/index.js
现在样式已经生效
webpack推荐我们使用import根据JS代码的需要动态 导入资源,就像刚刚import css一样,这样的代码与资源的关系,更符合模块化的依赖思想
加载其它资源 webpack5使用资源模块Asset Modules 来加载图片、字体等资源,webpack4则使用file-loader 和url-loader 等
file-loader 安装file-loader :
在代码所需的地方导入图片并使用
src/index.js 1 2 3 4 import avatar from './avatar.png' const img = new Image ();img.src = avatar; document .body .append (img);
配置规则
1 2 3 4 5 6 7 8 9 module : { rules : [ { test : /\.png$/ , use : 'file-loader' , } ], },
打包后,在dist目录下生成了871132b331c17257fcba75273b57f9fe.png,这是将文件的hash值作为了打包后的文件名,当然,这也是可以自定义的。
1 2 3 4 5 6 7 8 9 10 { test : /\.png$/ , use : { loader : 'file-loader' , options : { name : '[path][name].[ext]' , } }, }
这样就能保留图片的原始相对路径和名称。
url-loader file-loader拷贝文件到输出目录,而url-loader 通过durl 的形式表示文件
durl即Data URLs,可以通过url直接去表示文件的内容,不会产生任何请求
1 2 3 4 data :text/html;charset=UTF -8 ,<h1 > html content</h1 > data :image/png;base64,iDAHAidhbaIADHA...AHiDAd
最佳实践: 配置小文件使用url-loader,大文件则使用file-loader
1 2 3 4 5 6 7 8 9 10 11 { test : /\.(png|ico)$/ , use : { loader : "url-loader" , options : { name : "[path][name]_[hash:6].[ext]" , limit : 50 * 1024 , }, }, }
1 2 3 4 5 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,并且配置资源体积限制实现。
案例:
asset/resource 1 2 3 4 5 6 7 8 9 { test : /\.(png|jpg|svg|gif|ico)$/ , type : "asset/resource" , generator : { filename : "img/[name]_[hash:6][ext]" , }, },
asset/inline 1 2 3 4 { test : /\.(png|jpg|svg|gif|ico)$/ , type : "asset/inline" , },
最佳实践:type设为asset,添加一个parser属性,并且制定dataUrl的条件,添加maxSize属性;
1 2 3 4 5 6 7 8 9 10 11 12 { test : /\.(png|jpg|svg|gif|ico)$/ , type : "asset" , generator : { filename : "img/[name]_[hash:6][ext]" , }, parser : { dataUrlCondition : { maxSize : 50 * 1024 , }, }, },
babel-loader webpack由于打包需要,会去处理import和export,但对于其它ES6新特性,则不会去做兼容处理
如果需要将代码中的ES6进行转换,则需要babel-loader
1 2 npm install -D babel-loader @babel/core @babel/preset-env
注意: babel只是转换JS代码的一个平台,还需要用其它的插件,如@babel/preset-env,通过该平台来转换ES6特性
进行配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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失效,不推荐开启)
1 2 3 4 5 presets : [ ["@babel/preset-env" , { modules : "commonjs" , }] ]
这样就完成了简单的ES6转换,更完善的使用core-js@3兼容,后面再说吧。
资源加载方式 除了在js文件中使用import加载资源,webpack还会自动处理其它加载资源的方式,如css文件中的url()、@import
例如:当css-loader在处理css文件时遇到url()时,会找到符合的规则对所需的资源进行处理,如使用asset/resource对图片资源处理
1 2 3 4 5 6 7 8 9 10 11 body { background-image : url (avatar.png ); background-size : auto; } @import './main.css' ;body { background-color : #ccc ; }
开发loader 尝试开发一个markdown-loader,深入了解loader的工作过程
文档: 编写loader
功能: 将模块中所需的markdown资源转为html内容导入
在根目录新建markdown-loader.js
,一个最简单的loader是一个函数,接收传入的资源内容,若该loader是最后一个执行的,返回结果必须是JS代码
1 2 3 4 module .exports = source => { console .log (source) return 'console.log(source)' }
使用该loader
1 2 3 4 { test : /\.md$/i , use : path.resolve (__dirname, 'markdown-loader.js' ), }
在模块中导入markdown,webpack只会处理模块所依赖的资源
src/main.js 1 2 import md from './01.md' console .log (md)
打包时控制台输出了markdown的内容。查看打包结果,loader返回的js也在其中,被一个IIFE包裹。
下面继续完成功能:
安装解析markdown内容的模块,使用marked
修改 markdown-loader.js
1 2 3 4 5 6 7 8 const marked = require ('marked' );module .exports = source => { console .log (source) const html = marked.parse (source) console .log (html) return 'console.log(source)' }
输出如下,现在loader已经能解析markdown文件了
1 2 3 4 # 简介 这是一个**markdown** <h1>简介</h1> <p>这是一个<strong>markdown</strong></p>
完善loader,将html暴露给模块使用,会作为模块中import markdown文件的default值
1 2 3 4 5 6 7 const marked = require ('marked' );module .exports = source => { const html = marked.parse (source) return `export default ${JSON .stringify(html)} ` }
现在,markdown-loader就完成了,模块导入的就是解析好的html内容
src/main.js 1 2 3 4 import md from './01.md' console .log (md)
当然,markdown-loader也可以直接返回解析好的html内容,再交给loader管道中下一个loader进行处理,webpack只要求最后一个loader返回的需要是JS代码
处理html就需要安装html-loader,npm i html-loader -D
修改代码:
1 2 3 4 5 6 7 { test : /\.md$/i , use : [ 'html-loader' , path.resolve (__dirname, 'markdown-loader.js' ), ] }
1 2 3 4 5 6 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是一个数组,保存插件的实例。
1 2 3 4 5 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
1 2 3 4 5 6 7 8 9 10 const HtmlWebpackPlugin = require ('html-webpack-plugin' )plugins : [ new HtmlWebpackPlugin ({ template : path.resolve (__dirname, './index.html' ), inject : 'body' , filename : 'index.html' , title : 'webpack测试' , minify : true , }), ]
修改根目录下的index.html,使其作为一个模板
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 > <%= htmlWebpackPlugin.options.title %></title > </head > <body > </body > </html >
查看打包后的index.html
1 2 3 4 5 6 7 8 9 10 11 <!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
copy-webpack-plugin
安装:npm i copy-webpack-plugin -D
1 2 3 4 5 6 7 8 9 const CopyPlugin = require ("copy-webpack-plugin" );plugins : [ new CopyPlugin ({ patterns : [ { from : path.resolve (__dirname, 'public' ), to : "" }, ], }), ]
开发plugin 相较于loader只作用于模块加载,plugin的作用范围更广。plugin通过常见的钩子机制 实现,就像Vue生命周期提供的钩子一样。
webpack提供了很多打包过程中的钩子 ,plugin向这些钩子上挂载 任务,并获取上下文 ,来实现对资源的操作等功能。
钩子相关文档:compiler-hooks
webpack要求plugin必须是一个函数,或一个包含apply方法 的对象,通常是定义一个包含apply方法的类
新建myPlugin.js
1 2 3 4 5 export default class { apply (compiler ) {} }
现在开发一个用于清除webpack生成的/******/
注释,以方便阅读打包后的JS代码
明确了功能,考虑需要用到哪些钩子,显然,清除注释要在输出文件前执行,对要输出的内容进行处理。emit 钩子符合需求,这个钩子在输出 asset 到 output 目录之前执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export default class { apply (compiler ) { compiler.hooks .emit .tap ('MyPlugin' , compilation => { for (const name in compilation.assets ) { console .log (name); } }) } }
使用插件
1 2 3 4 const MyPlugin = require ('./myPlugin.js' );plugins : [ new MyPlugin (), ]
输出:
1 2 3 4 5 main_28990539038fea465479.js img/avatar_871132b331c17257fcba.png img/icon_36fa45932bf38a34e9af.ico favicon.ico index.html
插件已经能读取到打包后的文件名,接下来通过正则替换来处理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 module .exports = class { #isJSFile (filename ) { return /\.js$/i .test (filename); } apply (compiler ) { compiler.hooks .emit .tap ('MyPlugin' , compilation => { for (const name in compilation.assets ) { if (this .#isJSFile (name)){ let content = compilation.assets [name].source (); content = content.replace (/\/\*{3,}\//g , '' ); compilation.assets [name] = { source : () => content, size : () => content.length } } } }) } }
现在,打包的JS文件内容已经去除了/******/
但控制台有警告信息:
1 2 3 4 5 (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 来对资源进行再处理
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 module .exports = class { #isJSFile (filename ) { return /\.js$/i .test (filename); } apply (compiler ) { compiler.hooks .thisCompilation .tap ('MyPlugin' , compilation => { compilation.hooks .processAssets .tap ( { name : 'MyPlugin' , stage : compilation.PROCESS_ASSETS_STAGE_OPTIMIZE , }, (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配置:
1 2 3 module .exports = { watch : true , }
DevServer webpack dev server 提供了HTTP Server,集成了自动编译和自动刷新浏览器的功能。
该插件会将将打包结果暂时存放于内存,而不输出于硬盘,以提高性能。
安装:npm i webpack-dev-server -D
添加配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 module .exports = { devServer : { host : 'localhost' , port : 8080 , compress : true , static : { directory : path.join (__dirname, 'public' ), }, } }
添加命令脚本
1 2 3 4 5 { "scripts" : { "serve" : "webpack serve" } }
使用命令运行npm run serve
代理API服务 前后端同源部署时,本地开发在请求api时可能有cors问题,可以使用开发服务器代理api请求,服务器间通信就不存在cors了
DevServer就支持proxy
配置api代理,文档
1 2 3 4 5 6 7 8 9 10 11 12 13 devServer : { proxy : { '/api' : { target : 'https://api.github.com' , pathRewrite : { '^/api' : '' }, changeOrigin : true , }, }, }
Source Map 前端工程化后,源代码和运行代码几乎完全不同,调试和报错都是基于运行代码,调试源代码就成了问题
Source Map用于映射源代码和运行代码之间的关系
一个Source Map的组成:
1 2 3 4 5 6 { "version" : 3 , "sources" : [ "main.js" ] , "names" : [ "global" , ] , "mappings" : ";/****/" }
通过一行特定格式的注释引入Source Map
如果Source Map不起作用,需在浏览器控制台-设置-偏好设置中启用JavaScript源代映射
使用Devtool 在webpack中配置Source Map:
1 2 3 module .exports = { devtool : 'source-map' , }
相关文章:一文搞懂SourceMap以及webpack devtool
Source Map工作模式:
1 [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
inline-
将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 直接更改样式。
使用HMR 1 2 3 4 5 devServer : { hot : true , }
从webpack-dev-server v4开始,HMR已默认启用。会自动应用HotModuleReplacementPlugin插件
注意: HMR并不是开箱即用,还需要使用HMR-API 手动处理模块的热替换逻辑,否则还会自动刷新,部分loader和插件如style-loader已经处理好了css的热更新逻辑,在Vue等框架下开发,框架本身也处理好了HMR
使用HMR-API 手动处理JS模块热替换:
通常在入口模块统一做处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 if (module .hot ) { module .hot .accept ('./library.js' , function ( ) { }); } if (import .meta .webpackHot ) { import .meta .webpackHot .accept ('./library.js' , function ( ) { }); } module .hot .accept ( dependencies, callback errorHandler );
案例:
src/index.js 1 2 3 4 5 6 7 8 9 10 11 12 13 import { appendMarkdown } from "./module.js" ;import md from './01.md' let mde = appendMarkdown (md);if (module .hot ) { module .hot .accept ('./01.md' , () => { document .body .removeChild (mde); mde = appendMarkdown (md); }); }
热重载的需要根据自己的业务逻辑去实现,没有通用的方法,这也是webpack没有提供JS模块HMR的原因。
打包后,HMR相关代码会被自动去除
不同环境的配置 不同的环境需要不同的webpack配置,主要是区分生产和开发环境,文档
区分环境有两种方式
配置函数中判断env,返回不同的配置信息
创建多个配置文件对应不同的环境(推荐)
判断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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 module .exports = (env, argv ) => { console .log (env); const config = { mode : 'none' , devtool : 'eval-source-map' , 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 : [ { 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 , }, }, }, { 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' , filename : 'index.html' , title : 'webpack测试' , minify : true , }), ], devServer : { host : 'localhost' , port : 8080 , hot : true , compress : true , static : { directory : path.join (__dirname, 'public' ), }, proxy : { '/api' : { target : 'https://api.github.com' , pathRewrite : { '^/api' : '' }, changeOrigin : true , }, }, } } if (env.production ) { config.mode = 'production' ; config.devtool = false ; config.plugins = [ ...config.plugins , new CopyPlugin ({ patterns : [ { from : path.resolve (__dirname, 'public' ), to : "" }, ], }), ]; } else if (env.development ) { config.mode = 'development' ; } return config }
1 2 npx webpack --env production npx webpack --env development
多配置文件 若项目较大配置复杂,就不适合用判断env的方式,写多个配置文件更清晰明了,文档
通常有三个配置文件:
webpack.common.js
通用配置文件,写一些项目的通用基础配置
webpack.dev.js
开发配置文件
webpack.prod.js
生产配置文件
安装webpack-merge 合并配置对象:npm i webpack-merge -D
webpack.common.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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 const path = require ('path' );const HtmlWebpackPlugin = require ('html-webpack-plugin' );module .exports = { mode : 'none' , devtool : 'source-map' , context : path.resolve (__dirname, 'src' ), entry : { main : ['./index.js' , './main.js' ], }, output : { path : path.resolve (__dirname, 'dist' ), filename : '[name]_[contenthash].js' , clean : true , }, performance : { hints : 'warning' , maxEntrypointSize : 1024 * 1024 * 10 , maxAssetSize : 1024 * 1024 , assetFilter : function (assetFilename ) { return /\.js$/ .test (assetFilename); } }, module : { rules : [ { 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 , }, }, }, { 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' , filename : 'index.html' , title : 'webpack测试' , minify : true , }), ], devServer : { host : 'localhost' , port : 8080 , hot : true , compress : true , static : { directory : path.join (__dirname, 'public' ), }, proxy : { '/api' : { target : 'https://api.github.com' , pathRewrite : { '^/api' : '' }, changeOrigin : true , }, }, } }
webpack.dev.js 1 2 3 4 5 6 7 const common = require ("./webpack.common" );const { merge } = require ("webpack-merge" );module .exports = merge (common, { mode : 'development' , devtool : 'eval-source-map' , });
webpack.prod.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 : [ { from : path.resolve (__dirname, 'public' ), to : "" }, ], }), ], });
通过 —config 标志使用不同的配置文件
1 2 3 4 5 6 "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接收一个对象,对象中的值若为字符串,将被作为代码片段使用,
使用API_BASE_URL区分生产和开发环境API接口 1 2 3 4 5 6 7 8 9 10 const webpack = require ('webpack' );plugins : [ new webpack.DefinePlugin ({ 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功能自动开启,也可通过配置开启
1 2 3 4 5 6 module .exports = { optimization : { usedExports : true , }, }
测试代码:
src/utils.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 __webpack_require__.d (__webpack_exports__, { logName : () => ( logName) }); var info = { name : 'chuckle' , age : '20' }; function getName ( ) { return info.name ; } function getAge ( ) { return info.age ; } function logName ( ) { var _console; (_console = console ).log .apply (_console, _toConsumableArray (oo_oo ("3634127370_12_2_12_24_4" , info.name ))); } function logAge ( ) { var _console2; (_console2 = console ).log .apply (_console2, _toConsumableArray (oo_oo ("3634127370_15_2_15_23_4" , info.age ))); }
压缩代码去除未引用 optimization.minimize
压缩bundle并去除未引用代码,mode: production
默认开启
1 2 3 4 5 6 module .exports = { optimization : { minimize : true , }, };
打包后,未使用过的代码已经去除
1 e.d (_,{logName :()=> d});var t={name :"chuckle" ,age :"20" };
副作用 将文件标记为side-effect-free (无副作用)安全地删除未用到的export,目的是为了给Tree Shaking更大的优化空间
副作用:模块执行时,除了导出成员之外所作的事情
optimization.sideEffects 告知webpack去辨识package.json中的副作用标记或规则,以跳过那些当导出不被使用且被标记不包含副作用的模块。
1 2 3 optimization : { sideEffects : true , },
1 2 3 "sideEffects" : false , "sideEffects" : [ "*.css" ] ,
常见副作用代码:
src/pad.js 1 2 3 4 5 6 7 8 9 10 11 Number .prototype .pad = function (size ) { let result = String (this ); while (result.length < size) { result += '0' ; } return result; } import './pad'
若没有标记副作用,打包会排除该代码片段
1 2 3 4 5 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
标记副作用
1 "sideEffects" : [ "*.css" , "./src/pad.js" ] ,
模块分包 webpack会将所有小颗粒度的模块,从入口模块开始打包到一个JS模块,若项目较大,bundle也会很大,一些模块可以分包出来,减小bundle的体积,文档
模块分包办法:
多入口打包
动态导入
多入口打包 多入口打包通常用于多页面应用,但也可以一个页面应用多个bundle,实现分包
同事可以使用dependOn
指定依赖的公共模块,并在html中引入公共模块
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 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' , filename : 'index.html' , title : 'webpack测试' , 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
1 2 3 4 optimization : { runtimeChunk : 'single' , },
自动提取 当多个模块引入了同一个模块,可以使用splitChunks 将其自动提取为独立的chunk
1 2 3 4 5 splitChunks : { chunks : 'all' , minSize : 20 * 1024 , minSizeReduction : 50 * 1024 , },
minSizeReduction
:设置需要分包的bundle最小大小,这意味着如果分割成一个 chunk 并没有减少主 chunk(bundle)的给定字节数,它将不会被分割,即使它满足 splitChunks.minSize
这样就不用使用dependOn
指定依赖的公共模块了
动态导入 动态导入 实现按需加载,需要某个模块再加载该模块,所有动态导入的模块都会被自动分包
使用ESM的import()
实现动态导入
下面是一个hash路由的小demo
1 2 3 4 5 6 7 <body > <header > <a href ="#Home" > 首页</a > <a href ="#List" > 列表</a > </header > <div id ="main" > </div > </body >
src/blog.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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)
src/home/index.js 1 2 3 4 import { renderMarkdown } from "../module" ;import './index.css' import md from './index.md' export default () => renderMarkdown (md, "home" );
src/list/index.js 1 2 3 4 import { renderMarkdown } from "../module" ;import './index.css' import md from './index.md' export default () => renderMarkdown (md, 'list' );
若不使用动态导入,不同路由页引入的css都同时影响样式,导致样式冲突,下面使用import()
改造
src/blog.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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)
魔法注释 在动态导入过程中可以加入魔法注释,控制分包命名、合并、开启预加载
1 2 3 4 5 6 7 8 import ('./home' )import ('./list' )import ('./home' )import ('./list' )import ('./list' );
css处理进阶 css这东西吧,还得琢磨琢磨
css-loader模块化 开启options.modules
,css-loader会将样式中的类名进行转换,根据模块路径和类名生成转换为一个唯一的hash值。文档
作用: CSS的规则都是全局的,任何一个组件的样式规则,都对整个页面有效。产生局部作用域的唯一方法,就是使用一个独一无二的class的名字,不会与其他选择器重名
1 2 3 4 5 6 7 8 9 rules : [ { test : /\.css$/i , loader : "css-loader" , options : { modules : true , }, }, ],
通过导出对象访问类名来应用样式
1 2 3 .list { background-color : #2f59b4 }
1 2 3 4 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
配置 1 2 3 4 5 6 7 8 9 10 11 12 13 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
配置 1 2 3 4 5 6 7 8 9 10 11 const CssMinimizerPlugin = require ("css-minimizer-webpack-plugin" );optimization : { minimize : true , minimizer : [ `...` , new CssMinimizerPlugin (), ], },
文件hash 开启静态资源的客户端缓存后,为了能及时更新资源,资源文件就需要带上hash,文档
绝大多数插件都支持使用filename
配置输出的文件名
三种hash:
1 2 3 4 5 6 filename : '[name]_[hash].js' ,filename : '[name]_[chunkhash].js' ,filename : '[name]_[contenthash].js' ,
指定hash长度
1 filename : '[name]_[contenthash:8].js' ,
控制缓存最佳实践:8位contenthash
总结 package.json 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 { "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
配置webpack.config.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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 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' , 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 , minimizer : [ `...` , new CssMinimizerPlugin (), ], }, performance : { hints : 'warning' , maxEntrypointSize : 1024 * 1024 * 10 , maxAssetSize : 1024 * 1024 , assetFilter : function (assetFilename ) { return /\.ts$/ .test (assetFilename); } }, module : { rules : [ { test : /\.css$/i , use : [ { loader : MiniCssExtractPlugin .loader }, { loader : 'css-loader' , }, ], }, { test : /\.(png|jpg|svg|gif|ico)$/ , type : "asset" , generator : { filename : "img/[name]_[contenthash][ext]" , }, parser : { dataUrlCondition : { maxSize : 50 * 1024 , }, }, }, { test : /\.js$/i , include : [ path.resolve (__dirname, 'src' ) ], exclude : [ path.resolve (__dirname, 'node_modules' ) ], loader : 'babel-loader' , options : { presets : ["@babel/preset-env" ] } }, { 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' , filename : 'index.html' , title : 'webpack测试' , 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 , hot : true , compress : true , static : { directory : path.join (__dirname, 'public' ), }, proxy : { '/api' : { target : 'https://api.github.com' , pathRewrite : { '^/api' : '' }, changeOrigin : true , }, }, } }
tsconfig.json 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 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 { "compilerOptions" : { "incremental" : false , "diagnostics" : true , "target" : "esnext" , "module" : "esnext" , "lib" : [ "esnext" , "dom" , "dom.iterable" , ] , "allowJs" : true , "checkJs" : true , "outDir" : "./dist" , "rootDir" : "./src" , "declaration" : true , "declarationDir" : "./dist/typings" , "sourceMap" : false , "declarationMap" : false , "types" : [ ] , "removeComments" : true , "noEmit" : false , "noEmitOnError" : true , "noEmitHelpers" : true , "importHelpers" : true , "downlevelIteration" : true , "strict" : true , "alwaysStrict" : true , "noImplicitAny" : true , "strictNullChecks" : true , "strictFunctionTypes" : true , "strictPropertyInitialization" : true , "strictBindCallApply" : true , "noImplicitThis" : true , "noUnusedLocals" : true , "noUnusedParameters" : true , "noFallthroughCasesInSwitch" : true , "noImplicitReturns" : true , "esModuleInterop" : true , "allowUmdGlobalAccess" : true , "moduleResolution" : "node" , "baseUrl" : "./" , "paths" : { "@/*" : [ "src/*" ] } , "rootDirs" : [ "src" ] , "listEmittedFiles" : true , "listFiles" : true , "experimentalDecorators" : true , "emitDecoratorMetadata" : true , "resolveJsonModule" : true , "allowImportingTsExtensions" : true , } , "include" : [ "src/**/*" , ] , }