浅尝webpack
栏目: JavaScript · 发布时间: 5年前
内容简介:webpack 自出现时,一直备受青睐。作为强大的打包工具,它只是出现在项目初始或优化的阶段。如果没有参与项目的构建,接触的机会几乎为零。即使是参与了,但也会因为项目的周期短,从网上东拼西凑草草了事。纵观网上的 webpack 教程,要么是蜻蜓点水,科普了一些常规配置项;要么是过于深入原理,于实际操作无益。最近一段时间,我把 webpack 的官方文档来来回回地看了几遍,结果发现,真香。中文版的官方文档,通俗易懂,很感谢翻译组的辛勤奉献。看完之后,虽然达不到炉火纯青的地步,但也不会捉襟见肘,疲于应付。对于这
吐槽一下
webpack 自出现时,一直备受青睐。作为强大的打包工具,它只是出现在项目初始或优化的阶段。如果没有参与项目的构建,接触的机会几乎为零。即使是参与了,但也会因为项目的周期短,从网上东拼西凑草草了事。
纵观网上的 webpack 教程,要么是蜻蜓点水,科普了一些常规配置项;要么是过于深入原理,于实际操作无益。最近一段时间,我把 webpack 的官方文档来来回回地看了几遍,结果发现,真香。中文版的官方文档,通俗易懂,很感谢翻译组的辛勤奉献。看完之后,虽然达不到炉火纯青的地步,但也不会捉襟见肘,疲于应付。
对于这种 工具 类的博文,依然沿袭 用Type驯化JavaScript 的风格,串联各个概念。至于细节,就是官方文档的事了。
本文基于 webpack v4.31.0 版本。
Tapable
Tapable 是一个小型的库,允许你对一个 javascript 模块添加和应用插件。它可以被继承或混入到其他模块中。类似于 NodeJS 的 EventEmitter 类,专注于自定义事件的触发和处理。除此之外,Tapable 还允许你通过回调函数的参数,访问事件的“触发者(emittee)”或“提供者(producer)”。
tapable 是 webpack 的核心,webpack 中的很多对象(compile, compilation等)都扩展自tapable,包括 webpack 也是 tapable 的实例。扩展自 tapable 的对象内部会有很多钩子,它们贯穿了 webpack 构建的整个过程。我们可以利用这些钩子,在其被触发时,做一些我们想做的事情。
抛开 webpack 不谈,先看看 tapable 的简单使用。
// Main.js const { SyncHook } = require("tapable"); class Main { constructor(options) { this.hooks = { init: new SyncHook(['init']) }; this.plugins = options.plugins; this.init(); } init() { this.beforeInit(); if (Array.isArray(this.plugins)) { this.plugins.forEach(plugin => { plugin.apply(this); }) } this.hooks.init.call('初始化中。。。'); this.afterInit(); } beforeInit() { console.log('初始化前。。。'); } afterInit() { console.log('初始化后。。。'); } } module.exports = Main; // MyPlugin.js class MyPlugin { apply(main) { main.hooks.init.tap('MyPlugin', param => { console.log('init 钩子,做些啥;', param); }); } }; module.exports = MyPlugin; // index.js const Main = require('./Main'); const MyPlugin = require('./MyPlugin'); let myPlugin = new MyPlugin(); new Main({ plugins: [myPlugin] }); // 初始化前。。。 // init 钩子,做些啥; 初始化中。。。 // 初始化后。。。
理解起来很简单,就是在 init
处触发钩子, this.hooks.init.call(params)
类似于我们熟悉的 EventEmitter.emit('init', params)
。 main.hooks.init.tap
类似于 EventEmitter.on('init', callback)
,在 init
钩子上绑定一些我们想做的事情。在后面将要说的 webpack 自定义插件,就是在 webpack 中的某个钩子处,插入自定义的事。
理清概念
-
依赖图
在单页面应用中,只要有一个入口文件,就可以把散落在项目下的各个文件整合到一起。何谓依赖,当前文件需要什么,什么就是当前文件的依赖。依赖引入的形式有如下:
-
ES2015
import
语句 -
CommonJS
require()
语句 -
AMD
define
和require
语句 -
样式(
url(...)
)或 HTML 文件(<img src=...>
)中的图片链接
-
ES2015
-
入口(entry)
入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图(dependency graph)的开始。 -
输出(output)
output 属性告诉 webpack 在哪里输出它所创建的 bundle,以及如何命名这些文件。 -
模块(module)
决定了如何处理项目中的不同类型的模块。比如设置 loader,处理各种模块。设置 noParse,忽略无需 webpack 解析的模块。 -
解析(resolve)
设置模块如何被解析。引用依赖时,需要知道依赖间的路径关系,应遵循何种解析规则。比如给路径设置别名(alias),解析模块的搜索目录(modules),解析 loader 包路径(resolveLoader)等。 -
外部扩展(externals)
防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖。比如说,项目中引用了 jQuery 的CDN资源,在使用import $ from 'jquery';
时,webpack 会把 jQuery 打包进 bundle,其实这是没有必要的,此时需要配置externals: {jquery: 'jQuery'}
,将其剔除 bundle。 -
插件(plugins)
用于以各种方式自定义 webpack 构建过程。可以利用 webpack 中的钩子,做些优化或者搞些小动作。 -
开发设置(devServer)
顾名思义,就是开发时用到的选项。比如,开发服务根路径(contentBase),模块热替换(hot,需配合HotModuleReplacementPlugin
使用),代理(proxy)等。 -
模式(mode)
提供 mode 配置选项,告知 webpack 使用相应环境的内置优化。具体可见 模式(mode) -
优化(optimization)
从 webpack 4 开始,会根据你选择的 mode 来执行不同的优化,不过所有的优化还是可以手动配置和重写。比如,CommonsChunkPlugin
被optimization.splitChunks
取代。
webpack 差不多就是这几个配置项,搞清楚这几个概念,上手还是比较容易的。
代码分离
现在的前端项目越来越复杂,如果最终导出为一个 bundle,会极大地影响加载速度。切割 bundle,控制资源加载优先级,按需加载或并行加载,合理应用就会大大缩短加载时间。官方文档提供了三种常见的代码分离方法:
-
入口起点
配置多个入口文件,然后将最终生成的过个 bundle 出入到 HTML 中。
// webpack.config.js entry: { index: './src/index.js', vendor: './src/vendor.js' } output: { filename: '[name].bundle.js', }, plugins: [ new HtmlWebpackPlugin({ chunks: ['vendor', 'index'] }) ]
不过如果这两个文件中存在相同的模块,这就意味着相同的模块被加载了两次。此时,我们就需要提取出重复的模块。
-
防止重复
在 webpack 老的版本中,CommonsChunkPlugin
常用来提取公共的模块。新版本中SplitChunksPlugin
取而代之,可以通过optimization.splitChunks
设置,多见于多页面应用。 -
动态导入
就是在需要时再去加载模块,而不是一股脑的全部加载。webpack 还提供了预取和预加载的方式。非入口 chunk,我们可以通过 chunkFilename 为其命名。常见的如,vue 路由动态导入。
// webpack.config.js output: { chunkFilename: '[name].bundle.js', } // index.js import(/* webpackChunkName: "someJs" */ 'someJs'); import(/* webpackPrefetch: true */ 'someJs'); import(/* webpackPreload: true */ 'someJs');
缓存
基于浏览器的缓存策略,我们知道如果本地缓存命中,则无需再次请求资源。对于改动不频繁或基本不会再做改动的模块,可以剥离出来。
// webpack.config.js output: { filename: '[name].[contenthash].js', }
按照我们的想法,只要模块的内容没有变化,对应的名字也就不会发生变化,这样缓存就会起作用了。事实上并非如此,webpack 打包后的文件,并非只有用户自己的代码,还包括管理用户代码的代码,如 runtime 和 manifest。
模块依赖间的整合并不是简单的代码拼接,其中包括模块的加载和解析逻辑。注入的 runtime 和 manifest 在每次构建后都会发生变化。这就导致了即使用户代码没有变化,某些 hash 还是发生了改变。通过 optimization.runtimeChunk
提取 runtime 代码。通过 optimization.splitChunks
剥离第三方库。比如, react,react-dom。
module.exports = { //... optimization: { splitChunks: { cacheGroups: { vendor: { test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/, name: 'vendor', chunks: 'all', } } } } };
最后使用 HashedModuleIdsPlugin
来消除因模块 ID 变动带来的影响。
loader
loader 用于对模块的源代码进行转换。loader 是导出为一个函数的 node 模块。该函数在 loader 转换资源的时候调用。给定的函数将调用 loader API,并通过 this 上下文访问。
// loader API; this.callback( err: Error | null, content: string | Buffer, sourceMap?: SourceMap, meta?: any ); // sync loader module.exports = function(content, map, meta){ this.callback(null, syncOperation(content, map, meta)); return; } // async loader module.exports = function(content, map, meta){ let callback = this.async(); asyncOperation(content, (error, result) => { if(error) callback(error); callback(null, result, map, meta); return; }) }
多个 loader 串行时,在从右向左执行 loader 之前,会向从左到右调用 loader 上的 pitch 方法。如果在 pitch 中返回了结果,则会跳过后续 loader。
|- a-loader `pitch` |- b-loader `pitch` |- c-loader `pitch` |- requested module is picked up as a dependency |- c-loader normal execution |- b-loader normal execution |- a-loader normal execution <!-- pitch 中返回结果 --> |- a-loader `pitch` |- b-loader `pitch` returns a module |- a-loader normal execution
plugins
webpack 的自定义插件和本文开头 Tapable 中的差不多。webpack 插件是一个具有 apply 方法的 JavaScript 对象。apply 方法会被 webpack compiler 调用,并且 compiler 对象可在整个编译生命周期访问。钩子有同步的,也有异步的,这需要根据 webpack 提供的 API 文档。
// 官方例子 class FileListPlugin { apply(compiler) { // emit 是异步 hook,使用 tapAsync 触及它,还可以使用 tapPromise/tap(同步) compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => { // 在生成文件中,创建一个头部字符串: var filelist = 'In this build:\n\n'; // 遍历所有编译过的资源文件, // 对于每个文件名称,都添加一行内容。 for (var filename in compilation.assets) { filelist += '- ' + filename + '\n'; } // 将这个列表作为一个新的文件资源,插入到 webpack 构建中: compilation.assets['filelist.md'] = { source: function() { return filelist; }, size: function() { return filelist.length; } }; callback(); }); } } module.exports = FileListPlugin;
-
ProvidePlugin
自动加载模块,无需处处引用。有点类似
expose-loader
。// webpack.config.js new webpack.ProvidePlugin({ $: 'jquery', }) // some.js $('#item');
-
DllPlugin
将基础模块打包进动态链接库,当依赖的模块存在于动态链接库中时,无需再次打包,而是直接从动态链接库中获取。DLLPlugin 负责打包出动态链接库,DllReferencePlugin 负责从主要配置文件中引入 DllPlugin 插件打包好的动态链接库文件。
// webpack-dll-config.js // 先执行该配置文件 output: { path: path.join(__dirname, "dist"), filename: "MyDll.[name].js", library: "[name]_[hash]" }, plugins: [ new webpack.DllPlugin({ path: path.join(__dirname, "dist", "[name]-manifest.json"), name: "[name]_[hash]" }) ] // webpack-config.js // 后执行该配置文件 plugins: [ new webpack.DllReferencePlugin({ manifest: require("../dll/dist/alpha-manifest.json") }), ]
-
HappyPack
启动子进程处理任务,充分利用资源。不过进程间的通讯比较耗资源,要酌情处理。
const HappyPack = require('happypack'); // loader { test: /\.js$/, use: ['happypack/loader?id=babel'], exclude: path.resolve(__dirname, 'node_modules'), }, // plugins new HappyPack({ id: 'babel', loaders: ['babel-loader?cacheDirectory'], }),
-
webpack-bundle-analyzer
webpack 打包后的分析工具。
webpack 告一段落,浅尝辄止。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
What Technology Wants
Kevin Kelly / Penguin Group (USA) Incorporated / 2010-10-14 / USD 27.95
A refreshing view of technology as a living force in the world. This provocative book introduces a brand-new view of technology. It suggests that technology as a whole is not a jumble of wires and ......一起来看看 《What Technology Wants》 这本书的介绍吧!