Webpack 已经成为了现在前端工程化中最重要的一环,通过Webpack
与Node
的配合,前端领域完成了不可思议的进步。通过预编译,将软件编程中先进的思想和理念能够真正运用于生产,让前端开发领域告别原始的蛮荒阶段。深入理解Webpack
,可以让你在编程思维及技术领域上产生质的成长,极大拓展技术边界。这也是在面试中必不可少的一个内容。
JavaScript 的 模块打包工具 (module bundler)。通过分析模块之间的依赖,最终将所有模块打包成一份或者多份代码包 (bundler),供 HTML 直接引用。实质上,Webpack 仅仅提供了 打包功能 和一套 文件处理机制,然后通过生态中的各种 Loader 和 Plugin 对代码进行预编译和打包。因此 Webpack 具有高度的可拓展性,能更好的发挥社区生态的力量。
(function(modules) {// 模拟 require 函数,从内存中加载模块;function __webpack_require__(moduleId) {// 缓存模块if (installedModules[moduleId]) {return installedModules[moduleId].exports;}var module = (installedModules[moduleId] = {i: moduleId,l: false,exports: {},});// 执行代码;modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);// Flag: 标记是否加载完成;module.l = true;return module.exports;}// ...// 开始执行加载入口文件;return __webpack_require__((__webpack_require__.s = "./src/index.js"));})({"./src/index.js": function(module, __webpack_exports__, __webpack_require__) {// 使用 eval 执行编译后的代码;// 继续递归引用模块内部依赖;// 实际情况并不是使用模板字符串,这里是为了代码的可读性;eval(`__webpack_require__.r(__webpack_exports__);//var _test__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("test", ./src/test.js");`);},"./src/test.js": function(module, __webpack_exports__, __webpack_require__) {// ...},});
由于 Webpack 是基于 Node,因此 Webpack 其实是只能识别 js 模块,比如 css / html / 图片等类型的文件并无法加载,因此就需要一个对 不同格式文件转换器。其实 Loader 做的事,也并不难理解: 对 Webpack 传入的字符串进行按需修改。例如一个最简单的 Loader:
// html-loader/index.jsmodule.exports = function(htmlSource) {// 返回处理后的代码字符串// 删除 html 文件中的所有注释return htmlSource.replace(/<!--[\w\W]*?-->/g, "");};
当然,实际的 Loader 不会这么简单,通常是需要将代码进行分析,构建 AST (抽象语法树), 遍历进行定向的修改后,再重新生成新的代码字符串。如我们常用的 Babel-loader 会执行以下步骤:
<style>
标签的形式插入到 html 中;@import
和url()
,引用 css 文件与对应的资源;插件系统是 Webpack 成功的一个关键性因素。在编译的整个生命周期中,Webpack 会触发许多事件钩子,Plugin 可以监听这些事件,根据需求在相应的时间点对打包内容进行定向的修改。
class Plugin {// 注册插件时,会调用 apply 方法// apply 方法接收 compiler 对象// 通过 compiler 上提供的 Api,可以对事件进行监听,执行相应的操作apply(compiler) {// compilation 是监听每次编译循环// 每次文件变化,都会生成新的 compilation 对象并触发该事件compiler.plugin("compilation", function(compilation) {});}}
// webpack.config.jsmodule.export = {plugins: [new Plugin(options)],};
Webpack
就像工厂中的一条产品流水线。原材料经过 Loader
与 Plugin
的一道道处理,最后输出结果。
Loader
;Plugin
可以插入到整个生产过程中的每个步骤中;Webpack
事件流编程范式的核心是基础类 Tapable,是一种 观察者模式 的实现事件的订阅与广播:
const { SyncHook } = require("tapable");const hook = new SyncHook(["arg"]);// 订阅hook.tap("event", (arg) => {// 'event-hook'console.log(arg);});// 广播hook.call("event-hook");
Webpack 中两个最重要的类 Compiler 与 Compilation 便是继承于 Tapable,也拥有这样的事件流机制。
Compiler: 可以简单的理解为 Webpack 实例,它包含了当前 Webpack 中的所有配置信息,如 options, loaders, plugins 等信息,全局唯一,只在启动时完成初始化创建,随着生命周期逐一传递;
Compilation: 可以称为 编译实例。当监听到文件发生改变时,Webpack 会创建一个新的 Comilation 对象,开始一次新的编译。它包含了当前的输入资源,输出资源,变化的文件等,同时通过它提供的 api,可以监听每次编译过程中触发的事件钩子;
区别:
无用代码消除,是许多编程语言都具有的优化手段,这个过程称为 DCE (dead code elimination),即 删除不可能执行的代码;
var fn = function() {return 1;// 下面代码便属于 不可能执行的代码;// 通过 UglifyJs (Webpack4+ 已内置) 便会进行 DCE;var a = 1;return a;};
摇树优化 (Tree-shaking),这是一种形象比喻。我们把打包后的代码比喻成一棵树,这里其实表示的就是,通过工具 "摇" 我们打包后的 js 代码,将没有使用到的无用代码 "摇" 下来 (删除)。即 消除那些被 引用了但未被使用 的模块代码。
code-spliting: 代码分割 技术,将代码分割成多份进行 懒加载 或 异步加载**,避免打包成一份后导致体积过大,影响页面的首屏加载;
scope hoisting: 作用域提升**,将分散的模块划分到同一个作用域中,避免了代码的重复引入,有效减少打包后的代码体积和运行时的内存损耗;
升级至 最新 版本的 webpack,能有效提升编译性能;
使用 dev-server / 模块热替换 (HMR) 提升开发体验;
缩小编译范围:
modules: 指定模块路径,减少递归搜索;
mainFields: 指定入口文件描述字段,减少搜索;
noParse: 避免对非模块化文件的加载;
includes/exclude: 指定搜索范围/排除不必要的搜索范围;
alias: 缓存目录,避免重复寻址;
babel-loader
:
node_moudles
,避免编译第三方库中已经被编译过的代码;cacheDirectory
,可以缓存编译结果,避免多次重复编译;多进程并发:
第三方库模块缓存:
使用分析:
profile:true
,对各个编译阶段耗时进行监控,寻找耗时最多的地方;source-map
:
cheap-module-eval-source-map
;hidden-source-map
;mapBtn.click(function() {require.ensure([], function() {var baidumap = require('./baidumap.js') //baidumap.js放在我们当前目录下})})