模块打包工具产生的原因
ESM
存在兼容问题- 模块文件过多,网络请求频繁
- 所有前端资源都需要模块化,不仅仅是
js
文件 - 需要有一个工具能自动完成下列工作
- 编译最新特性,使浏览器能够兼容最新特性
- 能够将模块文件打包到一起
- 能够支持不同种类的资源类型,如图片,
html
,js
,css
,以及其他的各种文件
常用的模块打包工具
Webpack
Rollup
Parcel
Webpack的使用
-
基本使用
- 安装
webpack
及webpack-cli
- 命令行运行
yarn webpack
即可打包,webpack
默认以/src/index.js
为入口,并将打包后的js
文件放在dist/main.js
目录中
- 安装
-
webpack
配置文件: 在项目根目录下新建webpack.config.js
,该文件是运行在node
中的js
,遵守CommonJs
规范。该文件需要导出一个对象,用来描述webpack
行为。对象中的基本配置属性:-
entry
: 项目的入口文件配置选项,可以是一个字符串,数组,对象- 字符串: 表示单个入口,如果是相对路径,
./
不能省略 - 数组: 表示将多个文件打包到一个文件
- 对象:多文件入口,
key
为chunk
名,值是入口配置
- 字符串: 表示单个入口,如果是相对路径,
-
output
:打包之后的输出配置,接收一个对象,对象下列基本配置属性filename
: 输出文件的文件名path
: 输出文件的目录,是个绝对路径,可以通过node path
模块来指定绝对路径
const path = require('path') module.exports = { mode: 'none', entry: './src/index.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'output'), publicPath: 'output/' }, }
-
-
工作模式,包含
3
种工作模式:production
、development
、none
,不同的模式有不同的内置优化- 指定模式的方式:
-
在命令行中指定:通过添加
--mode production/development/none
命令行参数的形式添加
-
在配置对象中添加
mode
属性const path = require('path') module.exports = { mode: 'none', entry: './src/index.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'output'), publicPath: 'output/' }, }
-
- 工作模式的具体介绍
-
development
:会将DefinePlugin
中process.env.NODE_ENV
的值设置为development
(设置NODE_ENV
时不会设置mode
). 为模块和chunk
启用有效的名。
-
production
(默认):会将DefinePlugin
中process.env.NODE_ENV
的值设置为production
。为模块和chunk
启用确定性的混淆名称,FlagDependencyUsagePlugin
,FlagIncludedChunksPlugin
,ModuleConcatenationPlugin
,NoEmitOnErrorsPlugin
和TerserPlugin
。
-
none
:不使用任何默认优化选项
-
- 指定模式的方式:
-
webpack
资源模块加载-
webpack
中默认只会处理js
文件,其他资源模块一般都需要经过loader
处理,然后在js
中引入。loader
是webpack
的一个核心功能。 这样设计的目的:- 根据代码加载资源,按需加载。
JavaScript
驱动前端应用 - 确保上线的文件不会缺失,都是必要的
- 根据代码加载资源,按需加载。
-
css
资源的加载:需要先经过css-loader
处理,将css
代码转换为一个js
模块,然后再经过style-loader
将css
样式通过style
标签的方式引入。- 先安装
css-loader
和style-loader
- 在
webpack.config.js
中配置module->rules
。rules
接收一个对象数组-
对象中的test为文件匹配的正则
-
use: 表示使用的
loader
,可以是一个字符串和数组,如果是一个数组,文件则会从后向前依次经过多个loader
转换。const path = require('path') module.exports = { mode: 'none', entry: './src/index.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'output'), publicPath: 'output/' }, module: { rules: [ { test: /.css$/, use: [ 'style-loader', 'css-loader' ] } ] } }
-
- 先安装
-
文件资源的加载:
file-loader
,file-loader
可以将文件资源转化为js
模块,通过import
导入之后可以拿到文件资源对应的路径。// webpack.config.js const path = require('path') module.exports = { mode: 'none', entry: './src/index.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'output'), publicPath: 'output/' }, module: { rules: [ { test: /.css$/, use: [ 'style-loader', 'css-loader' ] } ] } } // 使用 import bg from './loginBg.jpg' const img = new Image() img.src = bg
此外还可以使用
url-loader
来转换文件。会将文件转换为DataURL
的形式。但是只适合体积较小的文件, 体积较大的文件打包之后会导致打包之后的文件过大。因此需要限制不超过指定大小的文件使用url-loader
处理(通过url-loader
配置选项中的limit
来配置),对于超出限制大小的文件默认会使用file-loader
进行处理,所以需要默认安装file-loader
。url-loader
转换后的文件不会有实体文件,而是一串DataURL
字符串。
DataURL
格式:
DataURL
字符串示例
url-loader
使用配置示例const path = require('path') module.exports = { mode: 'none', entry: './src/index.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'output'), publicPath: 'output/' }, module: { rules: [ { test: /.(png|jpg)$/, use: { loader: 'url-loader', options: { limit: 10 * 1024 } }, } ] } }
-
转换
ES6
代码到ES5
。通过babel-loader
来实现js代码的编译转换webpack
只负责打包,代码的编译通过loader
来实现-
安装
babel-loader
@babel/core
@babel/preset-env
-
在
webpack.config.js
中添加配置const path = require('path') module.exports = { mode: 'none', entry: './src/index.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'output'), publicPath: 'output/' }, module: { rules: [ { test: /.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } } ] } }
-
重新运行打包,可以查看打包后的对应文件的
js
代码已经转换为es5
。
-
-
常见加载器分类,即
loader
分类- 文件类:主要用于加载文件资源,并返回相应的内容。如
url-loader
,file-loader
等 - 语法转换类:用于将资源转换为指定语法的代码,如
babel-loader
、ts-loader
等 - 样式类:用于加载转换各种样式资源,如
css-loader
,style-loader
,less-loader
,sass-loader
等 - 语法检查和测试类:用于检查语法或测试,如
eslint-loader
- 文件类:主要用于加载文件资源,并返回相应的内容。如
-
-
webpack
加载资源的方式js
加载 ,支持ESM
、CommonJs
、AMD
等模块化加载方式loader
加载的非js
文件,如css
中的@import
和url
函数 ,html-loader
img
、video
等标签 的src
属性。
-
webpack
核心工作原理- 根据配置文件确定入口文件
- 通过解析入口文件中的
import/require
等解析文件的依赖文件, 逐步深入查找并构建表示项目文件的依赖关系的依赖树 - 对依赖树进行递归,找到对应的文件,并根据配置文件中的
module
下的rules
规则对文件进行加载 - 将加载后的文件合并输出到
bundle.js
中
-
编写
loader
-
loader
是导出为一个函数的node
模块,该函数在loader
转换资源的时候调用-
接收上一个
loader
产生的结果或者资源文件(resource file)
作为参数 -
函数的 this 上下文由 webpack 填充
-
函数可以返回一个代表
JavaScript
源码的String / Buffer
,注意为防止源码中的特殊字符出现识别错误,需要将源码字符串进行转义const marked = require('marked') module.exports = source => { const html = marked(source) return `export default ${JSON.stringify(html)}` }
-
也可以直接返回一个字符串,交给下一个
loader
处理,如下图直接返回html
字符串,然后使用html-loader
进行处理const marked = require('marked') module.exports = source => { const html = marked(source) return html }
-
-
loader
特点:loader
实际上是一个输入到输出的转换loader
支持链式调用,一个文件资源可以依次经过多个loader
处理
-
-
webpack
插件:增强webpack
自动化能力-
用途:处理资源加载之外的自动化工作
- 打包之前清除目录
- 拷贝静态文件到输出目录
- 压缩输出代码
-
常见插件的使用
-
自动清除输出目录
clean-webpack-plugin
,cleanStaleWebpackAssets
自动构建时是否清除未使用的资源。const { CleanWebpackPlugin } = require('clean-webpack-plugin') module.exports = { mode: 'none', entry: './src/index.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'output'), // publicPath: 'output/' }, plugins: [ new CleanWebpackPlugin({ cleanStaleWebpackAssets: false }) ] }
-
自动生成
HTML
插件html-webpack-plugin
-
基本使用
const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { mode: 'none', entry: './src/index.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'output'), // publicPath: 'output/' }, plugins: [ new CleanWebpackPlugin({ cleanStaleWebpackAssets: false }), new HtmlWebpackPlugin(), ] }
-
修改标题,添加源数据标签
new HtmlWebpackPlugin({ title: 'webpack-loader', meta: { viewport: 'widt=device-width' }, }),
-
指定模板,在使用插件是指定配置属性
template
,在html
模板文件中可以使用ejs
模板语法,通过htmlWebpackPlugin.options
可以访问到插件的配置属性。new HtmlWebpackPlugin({ title: 'webpack-loader', meta: { viewport: 'widt=device-width' }, template: './index.html' }),
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <h1> <%= htmlWebpackPlugin.options.title %> </h1> </body> </html>
-
创建多个
HTML
文件,只需要在plugins
中添加多个实例。通过filename
配置来指定生成的文件名称。plugins: [ new HtmlWebpackPlugin({ title: 'webpack-loader', meta: { viewport: 'widt=device-width' }, template: './index.html' }), new HtmlWebpackPlugin({ filename: 'mutiple.html' }), ]
-
-
复制文件到输出目录
copy-webpack-plugin
,patterns
是要复制的文件目录列表。new CopyWebpackPlguin([ 'public' ]),
-
-
插件的编写
-
webpack
通过在每个环节挂载钩子函数的方式来实现插件的扩展
-
webpack
插件是一个函数或类,函数或类的prototype
中必须包含一个apply
方法 -
然后根据插件的需求,确定插件所在事件钩子,并在该钩子中通过
tap
绑定一个回调函数到事件钩子中,在构建到该钩子所处阶段时会调用该回调函数。 -
然后再函数中进行相应的处理
compiler
: 代表了完整的webpack
环境配置,包括options
、loader
、plugin
、hooks
等compilation
:代表当前的构建的上下文环境,包括当前的模块资源,编译生成资源、变化的文件、以及被跟踪依赖的状态信息。通过compilation.assets
对象访问到所有编译后的内容。
class MyPlugin { // 在 prototype 上定义一个接收 compiler 对象的 apply 方法 apply(compiler) { // 指定要附加到的钩子事件 compiler.hooks.emit.tap( 'MyPlugin', (compilation) => { const assets = compilation.assets; for(const f in assets) { if (f.endsWith('.js')) { const newContent = assets[f].source().replace(/\/\**\*\//g, '') assets[f] = { source: () => newContent, size: () => newContent.length } } } } ); } }
-
-
-
开发体验增强
-
文件改变之后自动打包:以 监视模式 运行
webpack
打包,监听文件变化之后会自动运行webpack
打包。命令行添加--watch
参数将开启监视模式。 -
webpack-dev-serve
:webpack
本地开发功能插件,打包项目,并启动本地http server
,监听文件变化,自动重新打包,并刷新浏览器。-
安装
webpack-dev-server
之后, 直接运行yarn webpack-dev-sever
,会自动以监视模式打包,并启动本地http server
,文件修改自动打包,并刷新浏览器。 -
指定静态资源目录:通过
webpack.config.js
中配置devServer
下的contentBase
,会从指定的目录下获取静态资源module.exports = { mode: 'none', entry: './src/index.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'output'), // publicPath: 'output/' }, devServer: { contentBase: 'public', } }
-
配置代理
通过webpack.config.js
中配置devServer
下的proxy
来配置module.exports = { mode: 'none', entry: './src/index.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'output'), // publicPath: 'output/' }, devServer: { contentBase: 'public', proxy: { '/api': { // 以 /api 开头的 localhost:8080 请求将被 替换为 https://api.guthub.com 即localhost:8080/api/user -> https://api.guthub.com/api/user target: 'https://api.github.com', // 重写替换后的请求路径,将 './api' 部分替换为 ‘’ pathRewrite: { '/api': '' }, // 不能使用 localhost:8080 作为请求 GitHub 的主机名 changeOrigin: true, } } } }
-
-
源代码映射
(Source Map)
-
Source Map
: 开发阶段帮助直接定位到源码。关键文件为一个.map
文件,文件保存了压缩代码文件到源文件的映射关系。文件为一个JSON
格式的文件,包括以下属性:-
version
:文件版本 -
sources
: 源文件名,可能是多个文件合并产生的,所以是一个数组 -
names
:源码中定义的变量名
-
mappings
:记录转换之后以及转换之前代码的映射关系,是一个base64-vlq
编码的字符串
-
如何在压缩代码中引用
source Map
文件:在压缩代码最后一行添加注释引入。引入之后将会自动请求源代码,所以map
文件所在目录下需要有源码文件,否则将无法查看源码
-
-
webpack
开启Source Map
: 通过在webpack.config.js
配置中添加devtool
属性,并指定一个表示Source Map
模式的值,下列是所有模式的比较。
从模式命名解读各模式的区别:打包时不生成source map
文件,打包速度快,但是错误定位时只能定位到文件。-
eval
,使用eval
函数执行打包后的代码,只能定位文件
-
cheap
,简易版,只能定位行,不能定位列 -
source-map
,生成source-map
文件 -
module
,不经过loader
转换 -
inline
,以dataURL
的形式将source-map
代码嵌入到生成的文件中 -
hidden
,浏览器中隐藏source-map
文件,但是会生成source Map文件 -
nosources
,不在浏览器中显示source-map
文件,会提示行列信息
-
-
-
自动刷新问题:自动刷新时会自动刷新浏览器,导致页面状态丢失,解决办法就是开启
HMR
-
开启
HMR
有两种方式:-
启动
webpack-dev-server
时添加命令行参数--hot
-
配置
webpack.config.js
,在devServer
中添加hot:true
,并在plugins
中添加new webpack.HotModuleReplacementPlugin()
module.exports = { mode: 'none', entry: './src/index.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'output'), // publicPath: 'output/' }, devServer: { hot: true, contentBase: 'public', proxy: { '/api': { // 以 /api 开头的 localhost:8080 请求将被 替换为 https://api.guthub.com 即localhost:8080/api/user -> https://api.guthub.com/api/user target: 'https://api.github.com', // 重写替换后的请求路径,将 './api' 部分替换为 ‘’ pathRewrite: { '/api': '' }, // 不能使用 localhost:8080 作为请求 GitHub 的主机名 changeOrigin: true, } } } plugins: [ new HtmlWebpackPlugin({ title: 'webpack-loader', meta: { viewport: 'widt=device-width' }, template: './index.html' }), new webpack.HotModuleReplacementPlugin(), ] }
-
-
HMR
的问题-
需要手动处理当模块更新之后如何将更新之后的代码替换到页面中
-
样式文件在开启
HMR
之后,文件更新能直接替换到页面中,而不改变页面状态,是因为style-loader
中自动处理了样式的热更新
开启source Map
后能在开发者工具中的sources
下的webpack->src
中找到对应处理的js
-
样式文件能够统一处理热更新是因为只需要将修改的样式替换即可,而
js
代码则是由于代码逻辑各不相同,无法统一替换执行 -
某些框架也提供了脚手架也集成了
HMR
方案,所以可以直接使用。因为框架的js
代码相对而言更有规律,或是导出一个函数或是导出一个对象,所以能够实现统一的逻辑来处理文件的热更新。 -
手动处理
js
文件的热更新,通过使用处理HMR
的API
-
在引用模块的文件中,通过
module.hot.accept(url, func)
注册模块热更新处理 -
module.hot.accept()
接收两个参数,第一个参数为模块的路径,第二个参数为模块热更新的逻辑。// heading.js import './heading.css' export default () => { const ele = document.createElement('div') ele.contentEditable = true ele.className = 'head' return ele } // index.js import createHello from './heading' import test from './test.md' import bg from './images/loginBg.jpg' const helloEle = createHello() document.body.appendChild(helloEle) document.body.style.background = `url(${bg})` let lastEel = helloEle // 根据代码逻辑,heading 文件是导出一个生成元素的函数,文件更新时只要重新生成元素替换掉原来的元素即可, module.hot.accept('./heading', () => { // 根据操作,先生成一个新的元素,并记录 const newEle = createHello() // 移除原来的元素 // document.body.removeChild(lastEel) // 将原来元素的状态保存到新的元素中 newEle.innerHTML = lastEel.innerHTML // // 替换原来的元素 document.body.replaceChild(newEle, lastEel) lastEel = newEle }) ```
-
-
处理图片的热更新
图片的热更新只需要将引用图片的位置重新替换图片路径即可。import createHello from './heading' import test from './test.md' import bg from './images/loginBg.jpg' const helloEle = createHello() document.body.appendChild(helloEle) document.body.style.background = `url(${bg})` // 图片热更新处理 module.hot.accept('./images/loginBg.jpg', () => { document.body.style.background = `url(${bg})` })
-
HMR
注意事项-
module.hot
是HMR
的插件提供的,若是没有开启HMR
时,module.hot
不存在,热更新的处理逻辑将报错,所以需要将热更新的处理逻辑包裹在module.hot
是否存在的判断中if(module.hot) { let lastEel = helloEle // 根据代码逻辑,heading 文件是导出一个生成元素的函数,文件更新时只要重新生成元素替换掉原来的元素即可, module.hot.accept('./heading', () => { // 根据操作,先生成一个新的元素,并记录 const newEle = createHello() // 移除原来的元素 // document.body.removeChild(lastEel) // 将原来元素的状态保存到新的元素中 newEle2.innerHTML = lastEel.innerHTML // // 替换原来的元素 document.body.replaceChild(newEle, lastEel) lastEel = newEle }) // 图片热更新处理 module.hot.accept('./images/loginBg.jpg', () => { document.body.style.background = `url(${bg})` }) }
-
打包时需要关闭
HMR
,去除HMR
相关的插件,这样在运行webpack
命令打包时,热更新的代码会被包裹在一个if(false){}
的逻辑判断中,压缩代码时也会将这种无用的逻辑判断代码去除。
-
-
-
-
-
webapck
不同环境下的配置-
根据环境导出不同的配置:
webapck.config.js
还支持导出一个函数,函数接收两个参数,第一个参数为--env
命令行参数的值,第二个参数为运行命令行的所有参数对象。module.exports = (env, argv) => { console.log(env, argv); }
module.exports = (env, argv) => { const config = { mode: 'development', entry: './src/index.js', output: { filename: 'bundle.js', path: path.join(__dirname, 'output'), // publicPath: 'output/' }, devtool: 'cheap-module-source-map', module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] }, { test: /\.jpg/, use: 'file-loader' } ] }, plugins: [ new HtmlWebpackPlugin({ title: 'webpack-loader', meta: { viewport: 'widt=device-width' }, template: './index.html' }), ] } if (env === 'production') { // 生产环境 config.mode = env config.devtool = false config.plugins = [ ...config.plugins, new CleanWebpackPlugin(), new CopyWebpackPlguin(['public']) ] } else { config.devServer = { hot: true, // hotOnly: true, contentBase: 'public', proxy: { '/api': { // 以 /api 开头的 localhost:8080 请求将被 替换为 https://api.guthub.com 即localhost:8080/api/user -> https://api.guthub.com/api/user target: 'https://api.github.com', // 重写替换后的请求路径,将 './api' 部分替换为 ‘’ pathRewrite: { '/api': '' }, // 不能使用 localhost:8080 作为请求 GitHub 的主机名 changeOrigin: true, } } } config.plugins = [ ...config.plugins, new webpack.HotModuleReplacementPlugin(), ] } return config }
-
不同环境对应不同的配置文件
-
首先,新建
webpack.common.js
、webpack.dev.js
、webpack.prod.js
分别存在公共配置、开发环境配置以及生产环境配置 -
分别将配置写入到三个配置文件中
-
引入
webpack-merge
,然后分别在webpack.dev.js
和webpack.prod.js
中使用webpack-merge
提供的merge
方法合并webpack.common.js
中的配置。// webpack.common.js const path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { mode: 'development', entry: './src/index.js', output: { filename: 'bundle.js', path: path.join(__dirname, '../output'), // publicPath: 'output/' }, devtool: 'cheap-module-source-map', module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] }, { test: /\.jpg/, use: 'file-loader' } ] }, plugins: [ new HtmlWebpackPlugin({ title: 'webpack-loader', meta: { viewport: 'widt=device-width' }, template: './index.html' }), ] } //webpack.dev.js const { merge } = require('webpack-merge') const common = require('./webpack.common') const webpack = require('webpack') module.exports = merge(common, { devServer: { hot: true, // hotOnly: true, contentBase: 'public', proxy: { '/api': { // 以 /api 开头的 localhost:8080 请求将被 替换为 https://api.guthub.com 即localhost:8080/api/user -> https://api.guthub.com/api/user target: 'https://api.github.com', // 重写替换后的请求路径,将 './api' 部分替换为 ‘’ pathRewrite: { '/api': '' }, // 不能使用 localhost:8080 作为请求 GitHub 的主机名 changeOrigin: true, } } }, plugins: [ new webpack.HotModuleReplacementPlugin(), ] }) //webpack.prod.js const { merge } = require('webpack-merge') const common = require('./webpack.common') const { CleanWebpackPlugin } = require('clean-webpack-plugin') const CopyWebpackPlguin = require('copy-webpack-plugin') module.exports = merge(common, { mode: 'production', devtool: false, plugins: [ new CleanWebpackPlugin(), new CopyWebpackPlguin(['public']) ] })
-
运行命令行时需要通过
--config
命令行参数来指定配置文件
-
-
-
Webpack DefinePlugin
:为代码注入全局成员plugins: [ new webpack.DefinePlugin({ TEST: 'test' }) ]
-
production
模式下,会默认启动此插件并注入一个process.env.NODE_ENV=production
的成员 -
传入插件的对象中的键值对中的值是一个
js
代码片段的字符串
在js
中使用注入的全局变量
打包之后的代码,直接将值输出到代码中
修改全局变量的值如下
在
js
中使用
打包之后的代码如下
-
如果想要传入一个字符串,可以通过
JSON.stringify()
将字符串转换为一个js
代码片段
-
-
Tree Shaking
-
Tree Shaking
production
模式下,会默认启用Tree Shaking
,移除未引用代码(dead-code)
-
在非
production
模式下启用Tree Shaking
module.exports = { entry: './src/index.js', mode: 'none', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, optimization: { // 配置 webpack 内置优化功能 usedExports: true, // 只导出外部使用的成员(标记未使用代码) concatenateModules: true, // 将所有模块合并到一个函数中, Scope Hoisting ,作用域提升 minimize: true, // 开启代码压缩(移除未使用代码) } }
-
如果使用了
babel-loader
,Tree Shaking
将失效-
Tree Shaking
工作的前提条件是代码使用ESM
规范 -
babel-loader
早期版本是默认将代码转换成CommonJS
规范。最新版本是支持ESM
的,如果想要指定编译之后的模块化规范,可以通过@babel/preset-env
的配置选项的modules
来指定。const path = require('path') module.exports = { entry: './src/index.js', mode: 'none', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist') }, optimization: { // 配置 webpack 内置优化功能 usedExports: true, // 只导出外部使用的成员(标记未使用代码) concatenateModules: true, // 将所有模块合并到一个函数中, Scope Hoisting ,作用域提升 minimize: true, // 开启代码压缩(移除未使用代码) }, module: { rules: [ { test: /\.js$/, use: [ { loader: 'babel-loader', options: { presets: [ ['@babel/preset-env', { modules: 'commonjs'}] ] } } ] } ] } }
按照上述配置打包之后的代码如下包含了未引用代码,说明没有启用
Tree Shaking
去掉@babel/preset-env
的module
配置之后打包生成的代码如下:移除了未引用代码,启用了Tree Shaking
-
-
-
副作用
SideEffects
- 副作用是模块除了导出成员之外所做的事情,比如在原型中添加方法或属性等,
css
文件都属于副作用模块。 SideEffects
功能的作用是移除项目中没有被用到且没有副作用的代码。- 开启
SideEffects
- 首先需要在
webpack.config.js
中配置optimization
中的SideEffect
为true
,表示开启SideEffect
功能。 - 然后在
package.json
中配置sideEffects
,可以配置为一个boolean
或文件路径一个字符串数组。-
boolean
:表示项目中的所有代码都有/没有副作用, -
数组:表示项目中指定的文件具有副作用,打包时不会移除这些文件
"sideEffects": false, "sideEffects": [ "*.css" ]
-
- 首先需要在
- 副作用是模块除了导出成员之外所做的事情,比如在原型中添加方法或属性等,
-
代码分割
Code Splitting
实现代码分割有两种方式:-
多入口打包:适用于多页面应用,一个页面对应一个大包入口,公共部分则单独提取
-
需要将
entry
配置为一个对象,指定多个入口,同时利用HtmlWebpackPlugin
生成多个html
文件const path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { entry: { index: './src/index.js', album: './src/album.js' }, mode: 'none', output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') }, module: { rules: [ { test: /\.css$/, use: ['style-loader', 'css-loader'] } ] }, plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', template: './src/index.html' }), new HtmlWebpackPlugin({ filename: 'album.html', template: './src/album.html' }) ] }
-
HtmlWebpackPlugin
默认会将打包生成的所有bundle.js
引入到生成的html
文件中,这就意味着除了引入需要的bundle.js
,还会引入其他入口生成的bundle.js
-
通过在
HtmlWebpackPlugin
插件中指定chunks
,来指定生成的html
需要引入哪些bundle.js
。plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', template: './src/index.html', chunks: ['index'] }), new HtmlWebpackPlugin({ filename: 'album.html', template: './src/album.html', chunks: ['album'] }) ]
-
提取公共模块:提取多个入口的公共部分到一个单独的
bundle.js
中。
提取公共模块功能的开启只需要在optimization
属性中配置splitChunks
,然后在splitChunks
中通过chunks
指定提取哪些公共模块到一个bundle.js
中。optimization: { splitChunks: { chunks: 'all', // all 表示将所有的公共模块提取到单独的bundle.js中,‘async’:按需加载的模块 } },
但是最新的
css-loader
需要将手动启用css
模块化,才能成功提取公共模块。module: { rules: [ { test: /\.css$/, use: [ 'style-loader', { loader: 'css-loader', options: { modules: true } } ] } ] },
-
-
模块的动态导入
- 模块的动态导入只需要将原来代码中使用
import
关键字导入的模块改成使用import()
函数导入,无需进行配置,webpack
会自动处理代码分割。 webpack
默认以chunkId
作为chunk
名,如果想设置chunk
名,可以import()
方法参数使用行内注释webpackChunkName
来指定import(/* webpackChunkName: 'components' */'./posts/posts').then(({ default: posts }) => { mainElement.appendChild(posts()) })
不指定chunk名时的打包结果
- 模块的动态导入只需要将原来代码中使用
-
css
文件提取,将css
代码提取到一个css
文件中,适合于css
代码体积较大的场景.使用MiniCssExtractPlugin
插件。
在plugins
中添加插件实例,将module-->rules
中的style-loader
修改为MiniCssExtractPlugin.loader
,因为css代码不再是通过<style>
标签注入,而是通过<link>
标签引入。const { CleanWebpackPlugin } = require('clean-webpack-plugin') const HtmlWebpackPlugin = require('html-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') module.exports = { mode: 'none', entry: { main: './src/index.js' }, output: { filename: '[name].bundle.js' }, module: { rules: [ { test: /\.css$/, use: [ // 'style-loader', MiniCssExtractPlugin.loader, 'css-loader' ] } ] }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ title: 'Dynamic import', template: './src/index.html', filename: 'index.html' }), new MiniCssExtractPlugin(), ] }
生产模式下提取的
css
文件不会自动压缩,需要使用OptimizeCssAssetsWebpackPlugin
插件来帮助压缩css
文件。
-
-
文件名
Hash
-
客户端会缓存服务器的文件,为尽可能的多的利用客户端的缓存文件,减少文件的请求次数,同时又能及时更新有变化的文件,我们需要在文件变化时改变文件名,让客户端重新请求变化之后的文件,同时对于没有发生改变的文件,不改变文件名,从而让客户端利用缓存中的文件。
-
webpack
支持三种形式的文件名Hash
hash
:项目级别的Hash
,只要文件有修改,重新打包之后会重新生成Hash
chunkhash
:chunk
级别的Hash
,文件修改,只会改变对应chunk
的Hash
contenthash
:内容级别的Hash
,文件修改,只会改变内容对应文件的Hash
-
指定
Hash
长度。通过在hash
后面跟上:num
来指定生成Hash
长度。output: { filename: '[name]-[contenthash:6].bundle.js' },
-
Rollup的使用
-
Rollup
是一款ESM
打包器,充分利用ESM
特性的高效打包器 -
Rollup
的简单使用
安装之后,使用yarn rollup entryPath
命令行来运行rollup
打包,然后可以通过--file
来指定输出文件,--format
指定输出的代码格式。rollup
默认会启动Tree Shaking
优化打包之后的结果,移除未引用代码。
-
Rollup
配置文件- 配置文件
rollup.config.js
是一个node
模块,但是rollup
会对配置文件进行处理,因此可以直接使用ESM
。rollup.config.js
需要默认导出一个对象。export default { input: './src/index.js', // 入口文件 output: { // 输出文件配置 file: 'dist/main.js', // 输出文件位置 format: 'iife', // 输出文件格式 } }
Rollup
默认不会使用配置文件,需要通过--config [file]
指定配置文件,如果不指定file
将默认使用根目录下的rollup.config.js
文件
- 配置文件
-
Rollup
使用插件Rollup
自身功能只是ESM
模块的合并打包,其他功能需要通过插件去扩展,插件是Rollup
唯一的扩展途径。- 使用插件,以
rollup-plugin-json
(让我们可以导入json
文件)为例。安装插件之后,直接在配置文件中引入,并在plugins
中配置import json from 'rollup-plugin-json' export default { input: './src/index.js', // 入口文件 output: { // 输出文件配置 file: 'dist/main.js', // 输出文件位置 format: 'iife', // 输出文件格式 }, plugins: [ json() ] }
Rollup
默认只能按照文件路径的方式加载本地模块,对于node_modules
中的模块并不能和webpack
一样直接通过名称导入。为了能和webpack
一样直接通过模块名导入第三方模块,需要使用rollup-plugin-node-resolve
插件,使用方法同上。注意rollup
默认只能处理ESM
模块,所以如果不添加特殊处理,第三方模块也必须是ESM
模块。- 加载
CommonJs
模块,需要添加rollup-plugin-commonjs
插件,使用方法同上。
-
Rollup
代码拆分- 使用
import()
函数动态导入,Rollup
会自动拆分代码。但是输出代码的格式不能时iife
,因为自执行函数不支持代码拆分,所有代码包裹在一个函数中,无法进行代码拆分。浏览器环境我们只能将代码格式指定为amd
。同时代码拆分会生成多个文件,所以输出文件不能通过file
指定,file只能指定单个文件。需要通过dir
指定一个输出目录import json from 'rollup-plugin-json' export default { input: './src/index.js', // 入口文件 output: { // 输出文件配置 dir: 'dist', format: 'amd' }, plugins: [ json() ] }
- 使用
-
多入口代码
多入口打包只需要将input
修改为一个数组或对象即可。rollup
在多入口打包时会自动使用代码拆分,所以format
不能是iife
,需要指定为amd
,但是amd
格式的js
文件不能直接引入到页面,需要通过实现AMD
标准的库(如require
)加载。加载时使用<script>
标签引入require.js
,并通过data-main
指定requirejs
加载模块的入口路径。import json from 'rollup-plugin-json' export default { input: ['./src/index.js', './src/message.js'], // 入口文件 output: { // 输出文件配置 dir: 'dist', format: 'amd' }, plugins: [ json() ] }
-
优缺点:
- 优点:
- 输出结果更加偏平
- 自动移除未引用代码
- 打包结果直接可读
- 缺点:
- 加载非
ESM
的第三方模块比较复杂 - 模块最终都被打包到一个函数中,无法实现
HMR
- 浏览器环境中,代码拆分依赖
AMD
库,需要依赖AMD
库加载amd
格式的js
- 加载非
- 优点:
Parcel的使用
Parcel
是一款零配置的前端应用打包工具。
-
parcel
的使用-
安装
parcel-bundler
插件, 运行命令yarn parcel entryFile
即可。
-
parcel
推荐使用.html
文件为入口文件,因为浏览器也是使用.html
文件作为入口。通过script
标签引入入口的js
,然后根据模块导入来构建依赖。<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>parcel study</title> </head> <body> <script src="./main.js"></script> </body> </html>
-
运行
parcel
命令之后会自动构建打包,并启动一个本地http server
,文件修改时会自动重新构建代码并刷新浏览器。 -
parcel
也支持手动处理热更新import foo from './foo' // import $ from 'jquery' import './main.css' import logo from './logo.png' foo.bar() import('jquery').then($ => { $(document.body).append('<h1>Hello Parcel</h1>') $(document.body).append(`<img src=${logo} alt="" />`) }) if(module.hot) { module.hot.accept(() => { console.log('hmr'); }) }
-
-
parcel
的特点- 完全零配置
- 自动处理各种类型的文件资源的加载
- 引入第三方模块时会自动安装依赖,无需手动安装
- 相同体量,打包速度比
webpack
更快,因为内部使用多线程进行打包。虽然webpack
可以使用happypack
插件来加速打包。