Webpack 实践指南
概述
实践前端工程化的工具:webpack
loader 和 plugin
通俗点讲loader是转换,plugin是执行比转换更复杂的任务,比如合并压缩等
loader
对于loader,它是一个转换器,将A文件进行编译形成B文件,这里操作的是文件,比如将A.scss转换为A.css,单纯的文件转换过程,比如babel-loader和babel-core模块时为了把ES6的代码转成ES5
module.exports = function(source) {
// 当前的this指向可以取出很多的属性和方法
// 比如:this.addDependency
// 关闭该 Loader 的缓存功能:this.cacheable(false);
console.log(this)
// 如果返回undefined的话,就必须调用this.callback(),也就是去这个回调函数的值
return source;
};
plugin
class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tap(pluginName, (compilation) => {
console.log('The webpack build process is starting!!!');
});
}
}
// 在plugins中调用
plugins:[
new ConsoleLogOnBuildWebpackPlugin()
]
是用于在webpack打包编译过程里,在对应的事件节点里执行自定义操作,比如资源管理、bundle文件优化等操作,plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务在整个编译周期都起作用,比如html-webpack-plugin,是在打包完后将.html文件生成并追加那些资源文件
常用的hooks:
entry-option 初始化 option
run 开始编译
compile 真正开始的编译,在创建 compilation 对象之前
compilation 生成好了 compilation 对象
make 从 entry 开始递归分析依赖,准备对每个模块进行 build
after-compile 编译 build 过程结束
emit 在将内存中 assets 内容写到磁盘文件夹之前
after-emit 在将内存中 assets 内容写到磁盘文件夹之后
done 完成所有的编译过程
failed 编译失败的时候
与rollup的对比
rollup中没有loader的概念,但是也是可以通过插件中的hooks来完成代码转换rollup-transform
function annotatingPlugin() {
return {
name: 'annotating',
transform(code, id) {
// code 就是每个模块的代码
// id 就是每个模块的路径
return {meta: {annotating: {special: true}}}
}
}
}
至于其他的为什么webpack把loader和插件分开,我猜的是webpack想更好的解耦两者的功能。
常用node命令
path.relative
node
path.relative(from, to)方法根据当前工作目录返回from到to的相对路径。如果from和to各自解析到相同的路径(分别调用 path.resolve() 之后),则返回零长度的字符串。
如果将零长度的字符串传入from或to,则使用当前工作目录代替该零长度的字符串。
例如
(POSIX标准定义了操作系统应该为应用程序提供的接口标准,是IEEE为要在各种UNIX操作系统上运行的软件而定义的一系列API标准的总称)上:path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb');
// 返回: '../../impl/bbb'在windows上
path.relative('C:\\orandea\\test\\aaa', 'C:\\orandea\\impl\\bbb');
// 返回: '..\\..\\impl\\bbb'path.resolve
node
path.resolve([…paths]) 方法将路径或路径片段的序列解析为绝对路径。给定的路径序列从右到左进行处理,每个后续的path前置,直到构造出一个绝对路径。例如,给定的路径片段序列:/foo,/bar,baz,调用path.resolve(‘/foo’, ‘/bar’, ‘baz’)将返回/bar/baz。
如果在处理完所有给定的path片段之后还未生成绝对路径,则再加上当前工作目录。
生成的路径已规范化,并且除非将路径解析为根目录,否则将删除尾部斜杠。
零长度的path片段会被忽略。
如果没有传入 path 片段,则 path.resolve() 将返回当前工作目录的绝对路径。
path.resolve('/foo/bar', './baz');
// 返回: '/foo/bar/baz'
path.resolve('/foo/bar', '/tmp/file/');
// 返回: '/tmp/file'
path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif');
// 如果当前工作目录是 /home/myself/node,
// 则返回 '/home/myself/node/wwwroot/static_files/gif/image.gif'如果任何参数不是字符串,则抛出TypeError。
path.join
node
path.join([…paths])方法使用平台特定的分隔符作为定界符讲所有给定的path片段连接在一起,然后规范化生成的路径。零长度的path片段会被忽略。如果连接的路径字符串是零长度的字符串,则返回’.’,表示当前工作目录。
path.join('/foo', 'bar', 'baz/asdf', 'quux');
// 返回: /foo/bar/baz/asdf/quux'
path.join('/foo', 'bar', 'baz/asdf', 'quux', '..');
// 返回: '/foo/bar/baz/asdf',因为..是返回上一级,所以quux被删掉了。
path.join('foo', {}, 'bar');
// 抛出 'TypeError: Path must be a string. Received {}'
如果任何路径片段不是字符串,则抛出 TypeError。
入口
多入口
hash、chunkHash、contentHash的区别
hash
如果都使用hash的话,即每次修改任何一个文件,所有文件名的hash至都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效。
chunkHash
chunkHash根据不同的入口文件(entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响。
contentHash
contenthash是针对文件内容级别的,只有你自己模块的内容变了,那么hash值才改变,所以我们可以通过contenthash解决上诉问题。
Optimization
splitChunks
提示
缓存组的公共配置,需要满足splitChunks的所有条件后才能进去判断缓存组。
runtimeChunk
提示
解决了文件名变换,导致缓存失效的问题。
optimization: {
runtimeChunk: {
name: entrypoint => `runtimechunk~${entrypoint.name}`
}
}
当打包后生成a,b,c模块,每个模块都自己的hash值,其中a引用了c,b引用了c,当c发生改变时,c对应的hash变了,正常情况下a,b的hash也会变,这时需要runtimeChunk来作为一个文件中心(包含每个文件的hash值),a,b只要向runtimeChunk获取c的文件内容就行,这样就浏览器就不会重新请求a,b文件。
Simba前端webpack优化策略
优化对比
优化前(启动四个模块):
- 第一次构建 ~ 82000ms
- 热更新 ~ 21000ms
优化后(启动两个模块):
- 第一次构建 - 43000ms
- 热更新 ~ 3000ms
更改source map
在测试环境下source map改为:eval-source-map
source-map: 在一个单独的文件中产生一个完整且功能完全的文件。这个文件具有最好的source map,但是它会减慢打包速度。
eval-source-map:使用eval打包源文件模块,在同一个文件中生成干净的完整的source map。这个选项可以在不影响构建速度的前提下生成完整的sourcemap,但是对打包后输出的JS文件的执行具有性能和安全的隐患。在开发阶段这是一个非常好的选项。
动态引入模块
在router根文件引入当前开发所需的模块即可。由于import是在编译时执行的所以需要在文件中禁掉模块后再构建项目。
//import a from 'path'
//import b from 'path'
import c from 'path'
export defaut{
// a,
// b,
c
}
脚本思路
在项目中新建脚本,在package.json中传入参数来决定是否注释哪些模块。比如:
"npm run dev:c": "node ./myScript.js a b && webpack-server"
就表示注释掉a,b两模块。
脚本内容:获取原有的路由文件originIndex.js进行注释,然后写入到新文件index.js,项目构建时入口点是index.js
babel升级
- “babel-core”: “^6.22.1” => “^7.0.0”
- “babel-loader”: “^7.1.1” => “^8.0.0”
- “babel-plugin-transform-vue-jsx”: “^3.5.0” => “^4.0.1”
- “babel-eslint”: “^8.2.1” => “^9.0.0”
- 更新.babelrc文件以支持webpack4
Vue 升级
- “vue”: “^2.5.2” => “^2.6.11”
- “vue-template-compiler”: “^2.5.2” => “^2.6.11”
- “vue-loader”: “^13.3.0” => “^15.9.0”
- 新增”@vue/component-compiler-utils”: “^1.3.1”
- “vue-style-loader”: “^3.0.1” => “^4.1.2”
webpack升级
- “webpack”: “^3.6.0” => “^4.42.0”
- “webpack-dev-server”: “^2.9.1” => “^3.10.3”
- “webpack-dev-server”: “^2.9.1” => “^3.10.3”
- “webpack-merge”: “^4.1.0” => “^4.2.2”
插件升级与弃用
- “html-webpack-plugin”: “^2.30.1” => “^3.2.0”
- “extract-text-webpack-plugin”: “^3.0.0”不再支持webpack4,弃用,使用”mini-css-extract-plugin”替换
- “uglifyjs-webpack-plugin”: “^1.1.1”不在维护,弃用
loader升级
- “url-loader”: “^1.1.1” => “^3.0.0”
- “file-loader”: “^1.1.4 => “^1.1.11”
- “optimize-css-assets-webpack-plugin”: “^3.2.0” => “^4.0.0”
新增插件
- “happypack”: “^5.0.1”不支持”vue-loader”:“^15.9.0”,用来加速打包babel-loader
- “terser-webpack-plugin”: “^2.3.5”用来代替uglifyjs-webpack-plugin
- “mini-css-extract-plugin”: “^0.9.0”
- “hard-source-webpack-plugin”: “^0.13.1”没有使用,提升效果不是很明显
- “compression-webpack-plugin”: “^3.1.0”没有使用,服务端没有配置gzip
配置优化
- 新增optimization配置
optimization:{
splitChunks: {
cacheGroups: {
commons: {
minSize: 2 * 1024 * 1024,
test: /[\\/]node_modules[\\/]/,
// cacheGroupKey here is `commons` as the key of the cacheGroup
name(module, chunks, cacheGroupKey) {
const moduleFileName = module.identifier().split('/').reduceRight(item => item);
const allChunksNames = chunks.map((item) => item.name).join('~');
return `${cacheGroupKey}-${allChunksNames}-${moduleFileName}`;
},
chunks: 'initial'
},
}
},
minimize: true,
minimizer: [
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: config.build.productionSourceMap,
terserOptions: {
warnings: false,
// https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions
}
}),
],
runtimeChunk: {
name: entrypoint => `runtime~${entrypoint.name}`
}
}
new HappyPack({
id: 'babel',
loaders: [
{
loader: 'babel-loader',
cacheDirectory: true
},
],
threadPool: happyThreadPool,
debug: true
})