webpack读物
webpack打包原理
根据webpack-cli返回的complier对象构建整个流程,执行complier构建钩子:1,通过option参数确认入口2、构建依赖树并通过loader去处理对应的module3、将结果进行合并输出到对应的dist目录中。过程中需执行的plugin会根据传入的complier对象执行的钩子执行操作,并将结果返回。
webpack启动流程
webpack热更新原理
- 使用webpack-dev-server启动服务
- 已经启动了服务,并建立了socket连接,监听本地文件变化和浏览器进行双向通信
- devServer会在entry中添加两个文件,用于监听内容的变化及建立连接
- 浏览器接收到热更新的通知
- 比较hash值确认是否需要更新,需要就更新注入在bundle中的代码
- 如果开启了模块热更新将使用会删除旧的模块,添加新的module
webpack plugin
在开发 Plugin 时最常用的两个对象就是 Compiler 和 Compilation,它们是 Plugin 和 Webpack 之间的桥梁。 Compiler 和 Compilation 的含义如下:
- Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
- Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。 Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。
Webpack 插件的工作原理是基于 Webpack 的事件机制。Webpack 在构建过程中会触发一系列的事件,插件可以通过监听这些事件,并在特定的时机执行自定义的逻辑。 Webpack 插件通常是一个 JavaScript 类或函数,它需要实现一个
apply方法。在apply方法中,插件可以通过 Webpack 提供的钩子函数来注册自己的逻辑。
class MyPlugin {
apply(compiler) {
// 注册一个事件监听器,在构建完成后执行自定义逻辑
compiler.hooks.done.tap('MyPlugin', (stats) => {
console.log('Build is done!');
console.log(stats);
});
}
}
module.exports = MyPlugin;
实现一个日期时间处理的webpack plugin
class TimeFormatPlugin {
constructor(options) {
this.options = options; // 将传入的配置选项保存到实例的 options 属性中
}
apply(compiler) {
compiler.hooks.emit.tapAsync('TimeFormatPlugin', (compilation, callback) => {
const { format } = this.options; // 从配置选项中获取时间格式化字符串
const timestamp = new Date().toLocaleString(format); // 获取当前时间的格式化字符串
compilation.assets['timestamp.txt'] = { // 将生成的文件添加到 compilation.assets 对象中
source: () => timestamp, // 文件内容为当前时间的格式化字符串
size: () => timestamp.length, // 文件大小为字符串的长度
};
callback(); // 告诉 Webpack 插件已经完成操作
});
}
}
Tapable
webpack loader
使用loader-utils将参数source转成浏览器识别的字符串
同步loader和异步loader
同步 loader 是指处理模块的过程是同步的,即 loader 会立即返回处理结果。同步 loader 的编写比较简单,只需要编写一个函数,接收源代码作为参数,返回转换后的代码即可。同步 loader 的执行顺序是按照配置中 loader 数组的顺序依次执行的。 异步 loader 是指处理模块的过程是异步的,即 loader 会返回一个 Promise 对象,异步地处理源代码,并在处理完成后通过 resolve 方法返回转换后的代码。异步 loader 的编写比较复杂,需要使用异步编程的技巧,例如 Promise、async/await 等等。异步 loader 的执行顺序是按照配置中 loader 数组的倒序依次执行的。babel-loader,ts-loader,eslint-loader都是异步loader
module.exports = function(source) {
// 将 CSS 文件内容转换为 JavaScript 可以理解的模块格式
const cssModule = JSON.stringify(source);
// 返回 JavaScript 代码,导出 CSS 模块
return `module.exports = ${cssModule};`;
};
Pitching Loader
Pitching Loader 是 webpack 中的一个特殊类型的 loader,它可以在其他 loader 之前执行,并且可以中断整个 loader 链的执行。类似style-loader就是一个pitching-loader
module: {
rules: [
{
test: /\.js$/,
use: [
'pitching-loader',
'normal-loader',
],
enforce: 'pre',
},
],
},
常用的webpack loader和plugin
esbuild-loader
babel-loader
Babel 是一个广泛使用的 JavaScript 编译器,它可以将较新版本的 JavaScript 代码转换为向后兼容的版本,以便在旧版浏览器或其他环境中运行。Babel 的实现原理是基于 AST(抽象语法树)的转换。Babel 的转换过程就是将源代码转换为 AST,然后对 AST 进行遍历和修改,最后将修改后的 AST 转换回代码。
esbuild-loader和babel-loader打包产物的差异
使用esbuild-loader打包出的内容不包含webpackJSONP加载文件的方法
webpackJSONP是什么
webpackJSONP是 Webpack 在打包时生成的一个全局函数,用于实现模块的异步加载和代码分割。
在 Webpack 中,当你使用 import() 或 require.ensure() 等语法来异步加载模块时,Webpack 会将这些模块打包成一个个独立的 chunk,并将它们存储在一个叫做 chunkFilename 的文件中。当页面需要加载这些异步模块时,Webpack 会通过 jsonp 的方式动态地将这些模块加载到页面中。
webpack presets
Presets 的作用是简化 webpack 的配置过程,减少配置选项的数量和复杂度。通过使用 Presets,可以快速启用和配置 webpack 的各种功能,例如代码压缩、文件处理、代码分离、热更新等等。 webpack 提供了一些内置的 Presets,例如
@babel/preset-env、@babel/preset-react、@babel/preset-typescript等等。这些 Presets 包含了一系列的插件和配置选项,可以用于编译和打包不同类型的代码。 除了内置的 Presets,还可以使用第三方的 Presets,例如webpack-merge、webpack-box等等。这些 Presets 可以根据项目的需求进行定制和扩展,提供更加灵活和个性化的配置选项。
bundle.js app.js chunk.js
bundle.js:bundle.js是整个应用程序的主要 bundle,它包含了所有应用程序代码和依赖的第三方库代码。在 React 应用中,bundle.js包含了 React 库、ReactDOM 库以及应用程序的所有组件和模块代码。app.js:app.js是应用程序的入口文件,它包含了应用程序的启动逻辑和路由配置等。在 React 应用中,app.js通常包含了应用程序的根组件以及路由配置等。chunk.js:chunk.js是按需加载的代码块,它包含了应用程序中某些功能模块的代码。在 React 应用中,chunk.js通常是通过 React Router 或其他路由库动态加载的组件和模块代码。
split chunk
splitChunks是 Webpack 中用于配置代码分割的选项,它可以将应用程序的代码拆分成多个小的块,以便实现按需加载和提高应用程序性能。splitChunks选项可以配置以下值:
chunks:指定哪些块需要被分割。可选值有all、async和initial。默认值为async。minSize:指定块的最小大小,只有大于这个值的块才会被分割。默认值为30000。maxSize:指定块的最大大小,只有小于这个值的块才会被分割。默认值为0,表示没有大小限制。minChunks:指定一个模块被引用的最小次数,只有被引用次数大于等于这个值的模块才会被分割。默认值为1。maxAsyncRequests:指定按需加载时并行请求的最大数量。默认值为5。maxInitialRequests:指定入口点并行请求的最大数量。默认值为3。automaticNameDelimiter:指定自动生成的名称中用于分隔符的字符串。默认值为~。name:指定拆分出来的块的名称。默认情况下,Webpack 会根据拆分出来的块的内容和位置自动生成名称。cacheGroups:用于配置缓存组,可以将符合条件的模块打包到同一个块中。每个缓存组可以配置test、priority、reuseExistingChunk、enforce、name等选项。
常见的webpack优化策略
scope hositing
Scope Hoisting 的原理是通过静态分析模块之间的依赖关系,将模块之间的依赖关系图转换为一个简单的函数调用关系图。这样,Webpack 就可以将所有模块中的变量声明提升到模块作用域的顶部,从而避免了在每个模块中创建闭包的开销。
- 代码拆分(Code Splitting):通过拆分代码块,将应用程序的代码分割成更小的块,实现按需加载和减少初始加载时间。可以使用 Webpack 的
splitChunks配置选项来进行代码拆分。- 懒加载(Lazy Loading):将不常用的模块或页面延迟加载,只在需要时才加载。可以使用动态
import()或require.ensure()来实现懒加载。- Tree Shaking:通过静态分析代码,删除未使用的代码,减小打包后的文件大小。可以使用 Webpack 的
mode配置选项设置为production,以及使用工具如UglifyJS或Terser来进行代码压缩和消除未使用的代码。- 模块热替换(Hot Module Replacement):在开发环境中,使用模块热替换功能,实现在运行时更新模块,而不需要刷新整个页面。可以使用 Webpack 的
HotModuleReplacementPlugin插件来启用模块热替换。- 缓存优化:使用文件名哈希或内容哈希作为文件名的一部分,实现文件内容变化时,文件名也会变化,从而利用浏览器缓存机制,减少文件的重复加载。可以使用 Webpack 的
[hash]或[contenthash]来生成哈希文件名。- 并行构建:使用多线程或并行构建工具,如
thread-loader或happypack,将任务分发给多个子进程并行执行,加快构建速度。- 外部引入:将一些不常变化的库或依赖通过外部引入的方式加载,如使用
externals配置选项或 CDN 引入,减少打包体积。- 优化图片:对图片进行压缩、转换为 Base64 编码或使用懒加载等方式,减小图片的体积和加载时间。
- 优化字体:对字体进行压缩、子集化或使用外部引入等方式,减小字体的体积和加载时间。
- 缩小搜索范围:通过配置
resolve的alias或extensions,缩小模块搜索的范围,加快模块的查找速度。
externals: {
// 将不需要打包的库或依赖通过外部引入的方式加载
// 如 jQuery、React、Vue 等
jquery: 'jQuery',
react: 'React',
'react-dom': 'ReactDOM',
vue: 'Vue',
},
module: {
rules: [
// ...其他规则
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
name: 'img/[name].[hash:8].[ext]',
esModule: false,
},
},
],
},
],
},
plugins: [
new HtmlWebpackPlugin({
// 配置 HtmlWebpackPlugin 的选项
// ...
externals: {
// 将不需要打包的库或依赖通过外部引入的方式加载
// 如 jQuery、React、Vue 等
jquery: 'jQuery',
react: 'React',
'react-dom': 'ReactDOM',
vue: 'Vue',
},
}),
new CleanWebpackPlugin(),
new webpack.HotModuleReplacementPlugin(),
//开启scope hositing
new webpack.optimize.ModuleConcatenationPlugin(),
],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
extensions: ['.js', '.json', '.vue'],
},
optimization: {
minimizer: [
//
new CssMinimizerPlugin(),
// 压缩线上js文件
new ESBuildMinifyPlugin({
target: 'es2015'
})
],
splitChunks: {
chunks: 'async',
minSize: 100 * 1024,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 8,
maxInitialRequests: 8,
enforceSizeThreshold: 50000,
cacheGroups: {
...config.cacheGroups,
//
vendors: {
test: /[\\/]node_modules[\\/]/,
name(module, chunks, cacheGroupKey) {
if (config.splitNodeModules) {
// const packageName = module.identifier().match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1]
// return `${cacheGroupKey}-${packageName}`
const chunkFilename = chunks.map((item) => item.name).slice(0, 2).join('~')
return `${cacheGroupKey}-${chunkFilename}`
}
return 'vendors'
},
priority: -10,
reuseExistingChunk: true,
},
common: {
name: 'common',
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
}
}
}
}
};