Appearance
概述
webpack通过打包commonjs(现在能识别esm)格式的js文件,可以打包出浏览器能过识别的commonjs语法的代码,本质上是模拟commonjs的加载过程。对于非commonjs的资源,需要通过loader变换成commonjs风格。
打包后的文件分析
打包成单文件
精简了部分代码。
js
// a.js
exports.a = 1;
// index.js
const a = require('./a.js').a;
exports.b = a;
(function(context) {
const self = context;
const wrapJsContent = (content) => {
return (_webpack_require_, module, exports, modules) => eval(content)
}
const modules = {
'a.js': wrapJsContent(`exports.a = 1`),
'main.js': wrapJsContent(`
const a = _webpack_require_('./a.js').a;
exports.b = a;
`),
}
const _webpack_require_ = (moduleId) => {
if(_webpack_require_.cache[moduleId]) {
return _webpack_require_.cache[moduleId].exports
};
const module = {
exports: {},
cache: false
}
_webpack_require_.cache[moduleId] = module;
modules[moduleId](_webpack_require_, module, module.exports);
return module.exports;
}
_webpack_require_.cache = {}
_webpack_require.g = 'xx'
_webpack_require.p = 'xxx' // 定义其他的工具函数方法等
_webpack_require_('main.js')
})(this)打包成多个文件
通过import() 语法可以实现打包多个文件,每个文件就是一个chunk。通过output.chunkFilename修改。在浏览器中,异步的加载chunk通过jsonp的方式执行。
js
// a.js
exports.a = 1;
// index.js
const a = import('./a.js').then(module => {console.log(module.a)});
exports.b = a;
// a.chunk.js
self[xxx].push([['a.js'], {
'a.js': wrapJsContent(`exports.a = 1`)
}])
// main.js
const modules = {
'main.js': wrapJsContent(`
const a = _webpack_require_.e('./a.js').then(() => _webpack_require_('.a.js')).then(module => {console.log(module.a)});
exports.b = a;
`),
}
_webpack_require_.e = (id) => {
return _webpack_require_.l(id)
}
_webpack_require_.l = (id) => {
const src = genUrl(id);
const result = [null, null, null]
const result[2] = new Promise((resolve, reject) => [resolve, reject] = resolve);
const s = document.createElement('script');
s.src = src;
appendScript(s);
s.onload = () => {
removeScript(s)
result[0]();
};
}
self[xxx].push = (chunks) => {
modules = {
...modules,
...chunks
}
}打包细节
webpack不是单纯的对js文件封装,需要匹配出文件中的require、import等引用。所以如果js文件有错(使用了高阶语法或者宏形式),打包不能正常运行。对于这种情况可以采用loader的形式先编译成js能够识别的内容。
rollup打包同理,需要解决上面源码中的语法错误。
打包库
通过设置output.libraryTarget, 将打包结果添加到不同的目标上 详情
- libraryTarget = 'var',将结果添加到library 设置的值上 ]
- libraryTarget = 'umd', 将函数结果使用umd格式包装起来, commonjs绑定到exports[library]或者module.exports = result
对比rollup
rollup是esm文件格式的打包,rollup是简单的import,不会模拟commonjs的加载逻辑,比较轻量级。
external 和 target
target默认等于'web',在import()时采用jsonp的方式加载代码,而且不能引用内置模块,比如require('path');
可以设置 target: 'node',这样就能正确的require内置模块。 依然是采用的webpack_require的方式加载,只是在modules有点特别的写法。webpack通过这种方式可以灵活的设置加载模式,比如后面的externals。
js
"path": ((module) => {
module.exports = require("path");
})对于node_module时的模块,依旧当做外部模块,还是会打包到output中,对于这种,可以采用externals的方式。
js
const lodash = require('lodash')
{
externals: {
lodash: "commonjs lodash", // 会将 require('lodash') 变成 module.exports = require('lodash');
lodash: "lodash", // 会将 require('lodash') 变成 module.exports = lodash;
lodash: ['commonjs lodash', 'a'], // 会将 require('lodash') 变成 module.exports = require('lodash').a;
lodash: ['require111('a')', 'a'], // 会将 require('lodash') 变成 module.exports = require111('lodash').a;
}
}这样不管是在web还是node中,均可以实现。这个相当于一个宏,可以实现一些骚操作,比如不是绑定的全局变量。
webpack中的path.resolve和path.join
path.join是从左往右依次执行,上一次的结果成为下一次的相对路径或者绝对路径的开头,是一个简单的拼接。不一定能得出相对路径。 path.resolve是完成一个绝对路径,每一次碰到绝对路径重新计算路径,最后能获得一个绝对路径。
js
const path = require('path');
const path1 = path.join('/a', 'b') // /a/b
const path2 = path.join(__dirname, '/a', 'b') // D://xx/xx/a/b
const path2 = path.resolve(__dirname, '/a', 'b') // D:/a/b 第二个开始会独立计算,
const path2 = path.join('a', 'b') // D://xx/xx/a/bmodule,chunk和bundle的理解
moudle很好理解,一个资源就是就是一个module
chunk包括入口chunk和子chunk,webpack内部执行时将moudule转换成chunk bundle是多个chunk的合并,最终打包的产物,默认情况下每个入门文件和代码分离(import)的部分会生成bundle,其余的可以通过插件来设置。
如何降低入口文件的大小
- externals
- 通过splitChunk调整
TODO: 调整bundle文件大小
配置文件
可以采用函数、promise和数组的形式配置,数组的形式会独立的执行多个打包程序。promise的场景可以用在统一管理配置服务的地方。
- context可以指定entry和loader的相对路径,本身必须是相对路径。
entry
- entry 支持多入口,webpack会独立的从多个入口打包,如果遇到多个入口共享的chunk,只会打包一次。
- 单个入口支持数组的形式,相当于新建一个文件文件里面是依次rerquire 数组,同时数组最后一个require的内容作为导出
js
{
entry: {
main: ['./a', '.b']
}
}
// 相当于新建一个文件temp.js
require('./a');
module.exports = require('./b');
{
entry: {
main: './temp.js'
}
}- 最完整的配置方式是通过对象,output.filename中的name是entry的key(默认是main),可以通过filename来修改。
js
{
entry: {
'main': {
import: ['./index.js'],
filename: 'test'
}
}
}- 可以通过dependOn来指明依赖,实现公共chunk的提取。
js
{
entry: {
'lodash11': 'lodash'
'main': {
import: ['./index.js'],
filename: 'test',
dependOn: ['lodash11']
}
}
}最终生成bundle中 main实际的代码是 self['xxx'].push(['main.js', modules]),相当于import()分离的代码。lodash包含了主要代码。实际上不推荐这样做,lodash没办法复用。
output
- path必须是绝对路径,使用path.resolve或者path.join(__dirname, xxx)
- filename 用于配置入口chunk打包后的bundle, chunkFilename是其他chunk打包的bundle
contenthash、fullhash和chunkhash
TODO:
chunkFormat 和 chunkLoading
chunkFormat用于制定chunk的格式,'array-push' | 'commonjs' | 'module'
chunkLoading 表示入口main如何加载chunk module_require.f.xxx 'jsonp' | 'import-scripts' | 'require' | 'async-node' | 'import'
默认情况下和target有关,可以用于跨项目共享chunk上面,比如chunkFormat只要和chunkloading能匹配,项目中就可以引用其他项目的chunk
目前支持在每个entry中配置output.library
js
{
entry: {
main: {
import: '.index.js',
library: {
name: 'xxx',
type: 'umd',
}
}
}
}asset和publicPath以及base问题。
- 通过copy-webpack-plugin实现静态资源到dist目录的拷贝,所有的静态资源必须使用相对路劲。
- publicPath用于chunk和部分其他资源的路劲前缀,请总是以/结尾
- base 定义了页面中所有资源相对路劲,如果未设置默认是当前文档(路径的最后一个/前的目录)。同时也支持相对路径,相对时当前文档
支持动态publicPath,_webpack_public__path 代码中设置即可。这些webpack内置变量都是宏,会在编译阶段替换。
部署子目录时需要涉及上面的问题,一般按照下面流程调整。
- 修改publicPath调整webpack资源,最终的请求地址是简单拼接,所以一定/结尾,调整成./
- 静态资源全部修改成相对路径,使用copy-webpack-plugin 拷贝到dist目录下
- 修改index.html 中的base,调整成子目录绝对路劲(history模式下) ,如果是hash模式不需要调整,只需要在浏览器路径后补齐/
针对开发环境asset异常的问题按照下面解决 TODO:
module.noParse对于externals的补充
对于不需要打包的外部模块可以采用noParse的方式,webpack会忽略对当前文件的引用。这个行为发生在externals之前,用在node环境比较多
module.rules
配置webpack对资源的处理,通过use一个一个loader,返回一个webpack能够识别的js文件。
resolve 别名和编辑器快速引用
resolve.alias配置别名,需要配置绝对路径,使用别名相当于path.resolve(@xxx, 'somepath'); 搭配上jsconfig.json 或则 tsconfig.json 中的compilerOptions
json
{
"compilerOptions": {
"module": "commonjs esnext", // 使用import还是require实现自动补全
"baseUrl": "", // 指定后面的路径的基路径
"paths": {
"@": ["./src"]
}
}
}TODO: vscode中自动补全时,通过配置xx总是补全绝对路劲
webpack如何查找module
- resolve.extensions: array 省略后缀
- resolve.mainFiles: 'index' 针对目录默认加载的文件
- resolve.modules: ["node_modules"] 对于无路径的引用,到node_modules下查找目录。
- 查找目录后根据resolve.mainFields: array 配置,查询package.json对应字段中的入口文件。默认情况下, target: web是[browser, module, main]。target:node [module, main],如果没有配置默认当做目录处理。 可以查看lodash的包
package.json 中的main browser和module
main 对应 commonjs。 module对应 esm。 browser 对应 umd 规范。
优化篇
webpack的重点
webpack 优化选项配置
optimization.chunkIds
webpack生成的chunkId,在output.chunkFilename 替换ID选项, 开发环境默认是named,生产环境默认 deterministic,如果都不是,设置为natural natural: 从0开始顺序编号 named: 方便调试的id,一般是文件名 deterministic: 在多次构建不会变化的短数字id,方便生产环境缓存 size: 让初始化下载包更小的数字id,比如某个包按natural编码是11,为了在降低初始包大小,把这个包改成2(两位数变以为数),刚好降低 total-size: 让总包变得更小的数字id
optimization.moduleIds
控制台__webpack_require.m 中module的名字。参数同chunkIds。
js
__webpack_require.m = {
'./src/main.ts':eval(xxx), // 把'./src/main.ts' 替换成更短的数字,减少包大小
'./src/index.ts':eval(xxx),
}optimization.mangleExports
允许控制导出处理 size: 最小下载 deterministic: 最小缓存
optimization.concatenateModules
让webpack查找那些包是可以安全的合并到某一个模块,生成会默认开启
optimiaztion.emitOnErrors
默认是false,表示如果编译报错会将错误的资源发送出来到生成的代码中。场景是用来在页面上提示编译报错的信息,正常情况下,引用main.js会渲染界面,开启这个选项后,编译报错后,引入main.js 执行后控制台就会报错(真是个小垃圾功能)
optimization.flagIncludedChunks
告诉webpack不要加载子chunk
optimization.mergeDuplicateChunks
是否需要合并相同的引用,比如A引用了B,C引用B,正常情况会打包两次B,开启了只会打包一次。生产环境会默认开启。
optimization.minimizer 和 minimize
webpack5默认内置了minimizer,生产默认开启 也能通过自定义TerserPlugin来实现。 [new TerserPlugin(), '...'] '...'表示默认的压缩组件
optimization.runtimeChunk
true: 为每个入口开启单独一个runtime文件 false: 在入口文件中直接嵌入 { name: (entry: string) => string } 为每个入口开启一个runtime,并自定义 独立出runtime后,入口文件的加载方式会和chunkload相同
optimization.splitchunk
前面的优化项都是对chunk的生成做优化,chunk生成好后,通过splitchunk来再次对chunk进行处理 默认的splitchunk配置如下
js
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async', // 只处理异步导入的chunk 选择落入cacheGroupdechunk那些chunk会被处理
minSize: 20000, // 单chunk最小20000b
minRemainingSize: 0, // 拆分后最小chunk大小
minChunks: 1, // 如果一个模块被引用了1次就会被拆封,即使是同步应用。
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};拆分包是多条件判断,多个条件同时满足才会拆分。比如需要同时满足minSize和minChunks
cacheGroup表示先将chunk划分到cache组中,每个异步包也是一个cache,按照cacheGroup来处理再按照cache组规则优先级进行分包。
除了splitChunks会做分包外,异步导入的模块webpack总是会对其进行分包。
汇总一下分包的逻辑,首先是webpack的分包,webpack会区分为入口chunk、异步chunk还有入口依赖(dependOn依赖的包)作为分包。 分包完后,进入splitChunk分包,上面分入的包会按照cacheGroup进入缓存组,在缓存组中按照配置的规则再次进行分包。一个chunk只会落入一个cacheGroup,通过优先级来判断落入那个。
DDL优化
这是一种落后的配置,通过DDL又称动态运行库,webpack5中不需要DDL技术(splitChunks太强大),不过也可以实现。通过dependOn声明DDL库
js
export default {
entry: {
main: {
deependOn: 'ddl',
import: './src/index.ts'
},
ddl: ['lodash', 'vue']
}
}会把ddl打包到单独ddl.js包含了ddl代码和webpack runtime。再通过优化配置将runtime剔除掉。会有单独的ddl.js 这个ddl即可每次打包不再更新。
指定部分模块单独成chunk
利用splitChunks的分包特性很容易做到,webpack默认只会有3种包,dependOn,init和异步导入的chunk,还有runtime分离包。
ts
export default {
optimization: {
splitChunks: {
lodash: { // 将lodash从包里拆分出
chunks: 'all',
minSize: 0, // 分包最小大小
test: /[/\\]node_modules[/\\]lodash[/\\]?/
},
}
}
}优化检查点
- reslove 优化,提高查找效率速度
- extensions
- alias
- modules
- mainFields
- mainFiles
- resloveLoader
- noParse 跳过部分模块中的解析(模块本身还是会解析)
- 资源cdn化,在cdn过程提前查询dns域名。
- external分离第三方资源,通过htmlWepbackPlugin引入
- 动态导入,分离模块。
- bebael-loader使用缓存
- 配置webpack缓存
- oneof loader提高loader的查找性能
optimization 常用字段
js
{
optimization: {
chunkIds: 'named deterministic' // 调整的是chunkFilename中[name]字段 named 使用文件名 deterministic 使用有缓存的数字编码,比如刚开始是0.js 1.js 2.js 如果删除了1.js ,下次打包会生成0.js 2.js,不会生成1.js。利于长期缓存
moduleIds: 'natural size total-size', //生成bundle中modules moduleId。 同上 natural 从自然数开始 size尽可能让初始包小 total-size 尽可能让总包小。
flagIncludedChunks: boolean, // prod 默认开启,表示是否不重复加载子chunk,如果a,b 都同时引用了c c会打包两次
mergeDuplicateChunks: true, // 是否合并相同的chunk,在多个入口同时引用了相同的文件就会被合并。
minimize: true, // 是否开启内置压缩,生产默认开启,开发默认关闭
minimizer: xxx // 自定义压缩工具
}
}minimizer TerserPlugin 插件使用
TODO:
dev server
js
{
devServer: {
client: {
overlay: true, { errors: true, warnings: false }, // 是否需要在浏览器全屏展示错误警告信息
progress: true, // 是否展示编译的进度条信息
reconnect: true number // 是否自动重连
compress: true, // 启用gzip压缩
},
historyApiFallback: true // 启用history api
headers: { // 添加自定义响应头,可用于直接鉴权
"X-xx-x": 'something'
},
host: '0.0.0.0' // 配置本地监听的host,一般配置local-ip
port: 8088 // 本地监听端口号
hot: true // 是否启动热刷新,开启后会默认打开插件
open: ['/xxx'] true // 自动打开新页面,详情参考open库
open: { // open 包
target: ['first.html', 'http://localhost:8080/second.html'],
app: {
name: 'google-chrome',
arguments: ['--incognito', '--new-window'],
},
}
proxy: { // http-proxy-middleware
'/api': {
target: 'xcxx:xxxx',
pathRewrite: {
'^/api': ''
},
secure: false, // 关闭https的安全校验,用于后端的https自签名情况
bypass: () => boolean // 判断是否转发
}
},
// 开发环境配置静态目录
static: {
directory: ['./src/assets'],
publicPath: './xxx'
},
devMiddleware: { // webpack-dev-middleware 用于配置devServer在哪儿查找资源
}
}
}devServer中的publicPath 和 output中的publicPath的区别
devServer中会先将代码打包到内存中,和打包到dist一样,会按照output.publicPath组织chunk和资源的加载。
output.publicPath 仅用于chunk和资源加载的,是一个简单的拼接,不会影响入口chunk。 比如设置了output.publicPath: './xx',入口chunk的地址还是localhost:3000/main.js,但是子chunk的加载地址会变成./xx/chunk.js
devServer 中的publicPath是用于生成子目录服务器,会影响所有资源的加载。包括入口chunk。 默认情况下这个值和output中的一致,所有output中不要设置相对路劲。 output.publicPath: ./a devMiddleware.publicPath: /b 入口chunk的地址是host:3000/b/main.js 加载的资源地址变成了 host:3000/b/a/chunk.js,显而易见的main.js 和chunk.js 不在同济目录下。 这个时候有种方法,是把子chunk打包到目录下。
在CICD中如何利用缓存加快打包进度
TODO:
监听文件变动watch 和 watchOption
在开发服务器中默认开启,build默认是关闭。 watchOption控制监听的方式、重新构建间隔和目录。会监听已经解析的资源变更。如果新增了一个文件,只需要引用一次就可以实现监听
模块联邦
模块联邦提供了加载其他模块的能力,可以用于跨项目共享chunk。
- 本地模块指的是入口文件构建的模块,远程模块是MF定义出的。相当于共享配置的两个入口文件。
- 每个构建都是一个容器,容器可以加载远程模块。
- 远程模块需要使用异步的方式加载。
- 远程模块也可以作为容器加载其他远程模块。
- 远程模块中,因为publicPath是简单的拼接,如果设置了publicPath会出现加载宿主域的问题。需要将publicPath设置为auto或则动态设置。
- 远程模块至少会生成runtime、remote和module 3种chunk文件
js
const MF = webpack.container.ModuleFederationPlugin;
module.exports = {
plugins: [
// 打包逻辑 runtime -> remote.js -> ./componentA
new MF({
name: 'remote', // 远程模块名称
filename: 'remote.js', // 生成的远程模块文件,用于宿主容器加载
runtime: 'remote.runtime.js', // 默认情况下runtime文件生成在filename 定义的文件中,可以单独抽离出来。
exposes: [{
'./componentA': './src/componentA.vue' // 需要使用./前缀,host 通过 import('remote/componentA')
'.': 'src/componentA/index.js' // import('remote')
}]
})
]
}动态设置publicPath
因为runtime.js 需要加载remote.js和module.js 所以需要提前设置publicPath。MF在打包时如果container chunk和入口chunk同名,会合并。所以需要将name设置为一个入口name相同。
js
module.exports = {
entry: {
main: './index.js',
remote: './public-path.js' // 在入口文件中配置动态的js。
},
plugins: [
new MF({
name: 'remote', // 远程模块名称
filename: 'remote.js', // 生成的远程模块文件,用于宿主容器加载
runtime: 'remote.runtime.js', // 默认情况下runtime文件生成在filename 定义的文件中,可以单独抽离出来。
exposes: [{
'./componentA': './src/componentA.vue'
'.': 'src/componentA/index.js'
}]
})
]
}动态加载远程模块
正常情况下远程模块是在webpack配置中制定的,可以手动实现远程模块的加载。
js
const loadModule = async (url, containerName, module) => {
await loadScript(url); // 加载remoteEntry js 脚本。
const container = window[containerName]; // 默认情况下remoteEntry 按照var 导出。 通过window containerName 可以获取到container。
const factory = await container.get(module) // get方法是一个异步的方法,获取的是modules里的value。
return factory() // 执行一次相当于执行了module文件。
}上面代码不够完整,缺少共享模块的加载以及重复引用下module不是单例问题。
js
const remoteModuleCache = new set();
let loadModule = async (url, containerName, module) => {
if(hasCache(container, module)) {
return getCache()
}
await loadScript(url); // 加载remoteEntry js 脚本
await __webpack_init_sharing__('default') // 全局初始化共享模块
const container = window[containerName]; // 默认情况下remoteEntry library 按照var 导出。 通过window containerName 可以获取到container。
container.init(__webpack_share_scoped__.default)
const factory = await container.get(module) // get方法是一个异步的方法,获取的是modules里的value。
return factory() // 执行一次相当于执行了module文件。
}远程模块加载流程
- webpack_modules 声明了remoteEntry入口文件。
- 通过__webpack_require__.e 调用 __webpack__require.f.remote 中间件加载入口js文件。
- 入口js文件加载好后,在全局暴露remote变量
- host中加载远程模块的module会固定编译成 一个chunkid,
- host 会调用remote去获取这个chunkId对应的js文件,
- 加载完成后回调在remote中,然后host执行这个remote中的回调函数得到module。
远程模块踩坑
- 远程模块的异步chunk加载方式务必不要和host应用雷同,会出现覆盖host modules问题。大坑,使用 output.chunkLoadingGlobal、output.uniquename修改
- 目前自动加载远程模块的方式仅默认方式能生效,即把远程模块绑定到全局变量上。其他的方式均需要自行加载远程模块
webpack宏变量
webpack_xxx_xx 格式的都是webpack的宏,会在编译阶段替换成内置的命令,可以嚣张点直接写内置的命令或者使用DefinePlugin 插件来定义。
loader
loader用于将webpack不能直接识别的内容转换,一般来说需要返回一个具有导出对象的文件。可以用处理图片、代码宏等操作。
style-loader、css-loader、postcss-loader、sass-loader和less-loader
样式处理按照如下loader顺序执行
- vue-loader 根据style中lang将style的内容单独使用对应的loader处理。
- sass-loader和less-loader调用sass或者less编译成css。
- postcss-loader对编译后的css进行处理,添加autofixprefix、修复部分错误等
- css-loader解决css文件中的@import和url()
- style-loader实现一个js,将css代码添加到style标签中。
重点关注的地方:各种样式、资源引用的处理细节。
vue-loader不会对资源进行任何处理,@import '../style/a.scss'。相对的是vue文件本身,简单理解vue-loader会将vue文件拆分成两个文件,一个js文件,一个scss文件。
sass-loader扩展了@import功能(同js require一致的查找规则),添加~前缀可以从node_module中查找(可以不添加,默认相对路径,相对路径无法查询自动使用node_modules)。在js中被直接引用的scss文件视为入口文件,scss中@import文件的资源相对路径都参考这个(见后文例子)。 除此之外sass-loader不会处理代码中的url()。sass处理@import时,不会处理.css后缀、http开头、@import url() 格式。针对这个问题,可以使用resolve-url-loader 前置处理。
less-loader 大致同上。less也实现了webpackImport功能。less还扩展了一些,比如只引用不输出等。具体参考文档。不过less-loader 处理url,不需要使用resolve-url-loader
css-loader中@import始终应该被处理,对于url() 按照webpackImport方式处理,不过别名使用的是~,node_modules使用的~moudle,配置上options.url,可以动态的判断是否需要处理url资源
scss
// src/style/test/test.scss
@import 'a'; // ./a.scss => node_modules/a.scss
@import '@src/a'; // /src/a.scss
.test {
background: url('xx.jpeg'); // src/style/test/xx.jpeg
background: url('~@src/assets/img/xx.jpeg'); // src/assets/img/xx.jpegjpeg
background: url('./public/xx.jpeg'); // ./public/xx.jpeg // 这个不会经过webpack处理,其余的都会
background: url('/src/xx.jpeg'); // /src/xx.jpeg'
}图片、字体等资源的处理。
webpack5中使用module.rules.type取代了file-loader, url-loader 和 raw-loader。
asset/resource代替 raw-loaderasset/inline代替 url-loaderasset/source代替 file-loaderasset代替了url-loader 并自动选择data-url还是url
默认情况下总是会使用asset/source,并且以8KB为分界线,通过module.rules.parser.dataUrlCondition调整。 在output.assetModuleFilename定义输出的文件名称。
默认情况下webapck总是会对资源asset处理, 如果需要自己配置loader,需要加上type: 'javascript/auto',否则资源会被打包两次。
loader的加载方式和加载顺序
当use: ['style-loader', 'css-loader']时,我们直到loader总是从后往前执行,css-loader 传递给style-loader的总是js代码,style-loader如何处理js代码将css提取出。
有如下两种require('xxx.css') 和 require('style-loader!css-loader!xxx.css')。前面是根据在rules中的规则进行解析,后者是直接在require时指明loader的处理方式,再和和rules指定的,以!作为间隔符。
loader分为下面四种加载,通过enforce可以制定loader应用时机。
- pre 前置加载
- normal 默认值,正常require(使用当前流程)
- post 后置加载
- inline 上文的行内形式
加载流程 pre -> inline -> normal -> post
js
module.exports = {
module: {
rules: [
{
test: /\.less/,
use: [
{
loader: 'pre-loader',
enforce: 'pre'
},
{
loader: ['post-loader'],
enforce: 'post'
},
{
loader: ['normal-loaderA', 'normal-loaderB'],
// enforce: 'normal'
}
]
}
]
}
}
require('inline-loader!xxx.less') // use: ['pre-loader', 'inline-loader', 'normal-loaderA', 'normal-loaderB', 'post-loader']在inline前面添加不同修饰符可以定义不同的行为
- ! 禁用normal loader
- !! 禁用所有loader pre normal post
- -! 禁用pre normal
js
require('!inline-loader!xxx.less') // use: ['pre-loader', 'inline-loader', 'post-loader']
require('-!inline-loader!xxx.less') // use: ['inline-loader', 'normal-loaderA', 'normal-loaderB', 'post-loader']在确定好顺序后开始加载loader loader区分为pitch阶段和normal,pitch阶段相当于时间捕获,从前到后执行,具有break功能。pitch中能够获取后续loader链。pitch 如果有返回会直接跳到前一个loader的normal阶段。
js
require('inline-loader!xxx.less') 'pre-loader', 'inline-loader', 'normal-loaderA', 'normal-loaderB', 'post-loader' => 'post-loader', 'normal-loaderB', 'normal-loaderA', 'inline-loader', 'pre-loader',
require('-!inline-loader!xxx.less') 'inline-loader' => 'inline-loader'了解加载顺序后,分析一下style-loader 和 css-loader的执行流程。 less-loader对less文件处理后会返回css内容,非js模块 css-loader处理css中的url和import 返回js模块(为什么),js导出一个对象,执行toString可以得到原始的css文件。 style-loader 只接受css内容 ,不支持js模块包裹的css。
style-loader 通过pitch的方式在运行时获取了css-loader 返回的moudle,通过隐式调用toString拿到原始的css文本添加到style中。
js
// css-loader
module.exports = {
toString() {
return `
.test {
color: red
}
`
}
}正常情况下css-loader 返回给下一级是包含module.exports语句的文本,需要动态执行才能获取到原始的文本,可以实现一个css-loader和style-loader的适配器
js
// css-style-loader.js
module.exports = (content) => {
const evalFn = () => {
const module = { exports: {}};
eval(content);
return module
}
const cssText = evalFn(content).toString();
const style = document.createElement('style')
style.innerText = cssText;
document.body.appendChild(style)
}publicPath那些事儿
publicPath是webpack中最重要的属性,大部分不熟悉的同学都会碰到各种奇怪的404。有二个进阶的使用办法,第一个是设置为auto。编译的后代码如下
js
/******/ (() => {
/******/ var scriptUrl;
/******/ if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + "";// __webpack_require__.g 就是window全局变量
/******/ var document = __webpack_require__.g.document; // window.document
/******/ if (!scriptUrl && document) {
/******/ if (document.currentScript)
/******/ scriptUrl = document.currentScript.src // currentScript会指向当前这个代码所在的js文件
/******/ if (!scriptUrl) { // 获取第一个js文件所在的标签地址
/******/ var scripts = document.getElementsByTagName("script");
/******/ if(scripts.length) scriptUrl = scripts[scripts.length - 1].src
/******/ }
/******/ }
/******/ // When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration
/******/ // or pass an empty string ("") and set the __webpack_public_path__ variable from your code to use your own logic.
/******/ if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser");
/******/ scriptUrl = scriptUrl.replace(/#.*$/, "").replace(/\?.*$/, "").replace(/\/[^\/]+$/, "/"); // 获取地址的相对路径
/******/ __webpack_require__.p = scriptUrl;
/******/ })();第二个是动态设置通过设置__webpack_public_path__ = 'xxxxx'运行时设置,需要注意的是,webpack_public_path__是宏命令,在编译时会转换成__webpack_require.p,所以务必需要在入口module中定义。而且如果是esmodule,还必须在在单独的文件中定义,使用import 导入。
ts
__webpack_public_path__ = 'xxxxx' // 这种方式不行,因为import总会提到最上级执行,import xx from 'xxx' 就在 __webpack_public_path__ = 'xxxxx' 前面
import 'public-path.ts' // 这种方式才行
import xxx from 'xxx'代码压缩
webpack默认带了开箱即用的terserWebpackPlugin配置,如果需要自行添加压缩,需要重新安装依赖 terser-webpack-plugin。并在optimization.minimizer 新增。同时还需要保留原来的配置['...', new terserWebpackPlugin()]。terserWebpackPlugin压缩文件时只针对source-map 生效,其他的都无法生成source-map。
除了对js压缩外,webpack通过CssMinimizerPlugin 对css进行压缩
contenthash、fullhash和chunkHash
contentHash: 针对产物的内容(原始内容)进行hash fullHash: 整个项目内容hash chunkHash: 针对chunk的内容进行hash hash有一个边界的效应,异步chunk变化时主chunk也会变,原因是主chunk需要引用异步chunk,异步chunk名字变化,主应用也必须要变化,就没法满足contentHash,这点在入口chunk中最为明显。
webpack-runtime 和 manifest
打包后代码大量包含webpack内置的加载方法,这部分代码理论不会有太大变化,通过optimization.runtimeChunk分离出来。但是每次带hash打包会,wepback runtime文件中总是会包含对异步模块的映射(manifest)就是那堆__webpack_require__.u ,导致runtime总是会变化。通过html-webpack-inline-source-plugin插件直接将代码内联到html中,随着html更新。为什么不能将代码webpack.u固定到其他位置。后来找到了原因,整个runtime文件也就才2Kb,没必要再过度拆分了。社区也有方案(chunk-manifest-webpack-plugin),这个方案也会没用(如果是runtime引用这个文件,那么runtime的hash势必会变(参考hash的边界效应))没得意义。
异步模块的骚操作
在vue中路由模块中组件一般是通过异步组件导入的。一个异步组件就会生成一个chunk,开发者可以制定chunk,将多个异步组件合并
js
export default [
{
name: 'home',
path: '/',
component: Home
},
{
name: 'foo',
path: '/foo',
component: () => import(/* webpackChunkName: "foo" */'./src/page/foo')
},
{ // 如果boo文件比较小,不想拆封成两个文件,可以这样做
name: 'boo',
path: '/boo',
component: () => import(/* webpackChunkName: "foo" */'./src/page/boo')
}
]loader开发
plugin开发
杂项提效
scss中如何在组件使用scss变量
- 全局变量定义为引用文件,防止引入时重复编译
- scss-loader中对每一个sass入口文件添加对全局变量的引用。
sass中全局变量、extend、mixin都应该使用引用并导出。
css
$colors: ( blue: #007dc6, blue-hover: #3da1e0 ); @mixin colorSet($colorName) { color: map-get($colors, $colorName); &:hover { color: map-get($colors, $colorName#{-hover}); } } a { @include colorSet(blue); }产出如下:a { color:#007dc6 } a:hover { color:#3da1e0 }动态创建:@function addColorSet($colorName, $colorValue, $colorHoverValue: null) { $colorHoverValue: if($colorHoverValue == null, darken( $colorValue, 10% ), $colorHoverValue); $colors: map-merge($colors, ( $colorName: $colorValue, $colorName#{-hover}: $colorHoverValue )); @return $colors; } @each $color in blue, red { @if not map-has-key($colors, $color) { $colors: addColorSet($color, $color); } a { &.#{$color} { @include colorSet($color); } } }产出如下:a.blue { color: #007dc6; } a.blue:hover { color: #3da1e0; } a.red { color: red; } a.red:hover { color: #cc0000; }contextModule
如果request含有表达式,webpack会自动生成一个context module 打包所有符合条件的文件。同时开发者也能自定义context module。通过require.context 获取一个context module返回一个函数,通过函数的keys 获取所有的request的
js
require('./template' + name + '.js') // 生成一个contextModule = { directory: './template' ,regexp: /\.js&/ }
import('./template' + name + '.js') // 异步打包的方式, 通过内联注释的方式可以自定义context module 详解见webpack import() 文档
const m = require.context('./template', false, /\.js&/, 'sync') // 通过第四个参数可以定义的打包的方式,同步异步还是promise,是否分包等特性
m.keys.forEach(m)自行实现一下require.context 异步的场景
