webpack是一种前端资源构建工具,一个静态模块打包器。在webpack看来,前端的所有资源文件都会作为模块处理。它将根据模块的依赖关系进行静态分析,打包生成对应的静态资源。
webpack与gulp最大的区别就是在于打包过程上有所不同
Grunt、Gulp 这类构建工具的打包过程是通过遍历源文件–>匹配规则–>打包,整个过程是基于文件流的打包方式且做不到按需加载。
webpack 是从入口文件开始,把相关模块引入通过加载模块–>解析模块–>打包,整个过程是基于模块化的打包方式且支持按需加载,同时还可以在执行过程中针对性的去做一些优化操作。
1) 打包的主要流程如下:
- 需要读到入口文件里面的内容。
- 分析入口文件,递归的去读取模块所依赖的文件内容,生成AST语法树。
- 根据AST语法树,生成浏览器能够运行的代码
source map 是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。
map文件只要不打开开发者工具,浏览器是不会加载的。
HMR(Hot Module Replacement)是webpack一个重要的特性,当代码文件修改并保存之后,webapck通过watch监听到文件发生变化,会对代码文件重新打包生成两个模块补丁文件manifest(js)和一个(或多个)updated chunk(js),将结果存储在内存文件系统中,通过websocket通信机制将重新打包的模块发送到浏览器端,浏览器动态的获取新的模块补丁替换旧的模块,浏览器不需要刷新页面就可以实现应用的更新。
在发现源码发生变化时,自动重新构建出新的输出文件。
1)Webpack开启监听模式,有两种方式:
启动 webpack 命令时,带上 --watch 参数
在配置 webpack.config.js 中设置 watch:true
缺点:每次需要手动刷新浏览器
2)原理:轮询判断文件的最后编辑时间是否变化,如果某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等 aggregateTimeout 后再执行。
module.export = {
// 默认false,也就是不开启
watch: true,
// 只有开启监听模式时,watchOptions才有意义
watchOptions: {
// 默认为空,不监听的文件或者文件夹,支持正则匹配
ignored: /node_modules/,
// 监听到变化发生后会等300ms再去执行,默认300ms
aggregateTimeout:300,
// 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
poll:1000
}
}
文件指纹是打包后输出的文件名的后缀。
Hash:和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改
Chunkhash:和 Webpack 打包的 chunk 有关,不同的 entry 会生出不同的 chunkhash
Contenthash:根据文件内容来定义 hash,文件内容不变,则 contenthash 不变
设置 output 的 filename,用 chunkhash。
设置 MiniCssExtractPlugin 的 filename,使用 contenthash。
设置file-loader的name,使用hash。
module.exports = {
entry: {
app: './scr/app.js',
search: './src/search.js'
},
output: {
filename: '[name][chunkhash:8].js',
path:__dirname + '/dist'
},
plugins:[
new MiniCssExtractPlugin({
filename: `[name][contenthash:8].css`
})
],
module:{
rules:[{
test:/\.(png|svg|jpg|gif)$/,
use:[{
loader:'file-loader',
options:{
name:'img/[name][hash:8].[ext]'
}
}]
}]
}
}
webpack-paralle-uglify-plugin(不再维护)
uglifyjs-webpack-plugin 开启 parallel 参数 (不支持ES6)
terser-webpack-plugin 开启 parallel 参数(支持ES6)
使用 DllPlugin 进行对第三方库分包提前打包,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,通过 json 文件告诉webpack这些库提前打包好了,避免反复编译浪费时间。
HashedModuleIdsPlugin 可以解决模块数字id问题
babel-loader 开启缓存
terser-webpack-plugin 开启缓存
使用 cache-loader 或者 hard-source-webpack-plugin
- exclude(不需要被解析的模块)/include(需要被解析的模块)
- resolve.modules 告诉 webpack 解析模块时搜索的目录,指明第三方模块的绝对路径
- resolve.mainFields 限定模块入口文件名,只采用 main 字段作为入口文件描述字段 (减少搜索步骤,需要考虑到所有运行时依赖的第三方模块的入口文件描述字段)
- resolve.alias 当从 npm 包中导入模块时(例如,import * as React from ‘react’),此选项将决定在 package.json 中使用哪个字段导入模块。根据 webpack 配置中指定的 target 不同,默认值也会有所不同
- resolve.extensions 尽可能减少后缀尝试的可能性
- noParse 对完全不需要解析的库进行忽略 (不去解析但仍会打包到 bundle 中,注意被忽略掉的文件里不应该包含 import、require、define 等模块化语句)
IgnorePlugin (完全排除模块)
通过 Polyfill Service识别 User Agent,下发不同的 Polyfill,做到按需加载,社区维护。(部分国内奇葩浏览器UA可能无法识别,但可以降级返回所需全部polyfill)
构建后的代码会存在大量闭包,造成体积增大,运行代码时创建的函数作用域变多,内存开销变大。Scope hoisting 把引入的 js 文件“提升到”它的引入者顶部,其实现原理为:分析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。因此只有那些被引用了一次的模块才能被合并。
必须是ES6的语法,因为有很多第三方库仍采用 CommonJS 语法和 Scope Hoisting 要分析模块之间的依赖关系,需要配置 mainFields 对第三方模块优先采用 jsnext:main 中指向的ES6模块化语法
- 使用 html-webpack-externals-plugin,将基础包通过 CDN 引入,不打入 bundle 中
- 使用 SplitChunksPlugin 进行(公共脚本、基础包、页面公共文件)分离(Webpack4内置) ,替代了 CommonsChunkPlugin 插件
基础包分离
- purgecss-webpack-plugin 和 mini-css-extract-plugin配合使用(建议)
打包过程中检测工程中没有引用过的模块并进行标记,在资源压缩时将它们从最终的bundle中去掉(只能对ES6 Modlue生效) 开发中尽可能使用ES6 Module的模块,提高tree shaking效率- 禁用 babel-loader 的模块依赖解析,否则 Webpack 接收到的就都是转换过的 CommonJS 形式的模块,无法进行 tree-shaking
- 使用 PurifyCSS(不在维护) 或者 uncss 去除无用 CSS 代码
Loader像一个"翻译官"把读到的源文件内容转义成新的文件内容,并且每个Loader通过链式操作,将源文件一步步翻译成想要的样子。
编写Loader时要遵循单一原则,每个Loader只做一种"转义"工作。 每个Loader的拿到的是源文件内容(source),可以通过返回值的方式将处理后的内容输出,也可以调用**this.callback()**方法,将内容返回给webpack。 还可以通过 this.async()生成一个callback函数,再用这个callback将处理后的内容输出出去
const loaderUtils = require('loader-utils');
const fs = require('fs');
const path = require('path');
module.exports = function(source) {
const { name } = loaderUtils.getOptions(this);
const url = loaderUtils.interpolateName(this, "[name].[ext]", {
source,
});
console.log(url);
this.emitFile(path.join(__dirname, url), source);
const json = JSON.stringify(source)
.replace('foo', '')
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
return `export default ${json}`;
}
Plugin的编写核心是在plugin类中一定要实现apply(compiler)这个方法,绑定compiler对象中的hooks,进行监听,webpack会在特定的时间点广播出特定的事件,插件在监听感兴趣的事件后会执行特定的逻辑,改变webpack的运行结果。
module.exports = class ZipPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
compiler.hooks.emit.tapAsync('ZipPlugin', (compilation, callback) => {
const folder = zip.folder(this.options.filename);
for (let filename in compilation.assets) {
const source = compilation.assets[filename].source();
folder.file(filename, source);
}
zip.generateAsync({
type: 'nodebuffer'
}).then((content) => {
const outputPath = path.join(
compilation.options.output.path,
this.options.filename + '.zip'
);
const outputRelativePath = path.relative(
compilation.options.output.path,
outputPath
);
compilation.assets[outputRelativePath] = new RawSource(content);
callback();
});
});
}
}
大多数JavaScript Parser遵循 estree 规范,Babel 最初基于 acorn 项目(轻量级现代 JavaScript 解析器),Babel 是对浏览器识别不了的代码进行转换兼容的库,Babel大概分为三大部分: