网站服务器防护,国家住房城乡建设部网站,网站运营策划是什么,wordpress 列表圆点目 录 前言模块化开发Stage1 - 文件划分方式Stage2 - 命名空间方式Stage3 - IIFE#xff08;立即调用函数表达式#xff09;Stage 4 - IIFE 依赖参数模块化的标准规范 使用Webpack实现模块化打包安装WebpackWebpack基本配置Webpack构建流程Webpack热更新Webpack打包优化 前言… 目 录 前言模块化开发Stage1 - 文件划分方式Stage2 - 命名空间方式Stage3 - IIFE立即调用函数表达式Stage 4 - IIFE 依赖参数模块化的标准规范 使用Webpack实现模块化打包安装WebpackWebpack基本配置Webpack构建流程Webpack热更新Webpack打包优化 前言 车同轨书同文行同伦。 ——《礼记·中庸》 模块化开发
Webpack是一个JavaScript应用的静态模块化打包工具它最早的出发点就是去实践前端方向的模块化开发解决如何在前端项目中更高效地管理和维护项目中的每一个资源问题。在Webpack的理念中前端项目中的任何资源都可以作为一个模块任何模块都可以经过Loader机制的处理最终再被打包到一起。
既如此说到webpack就不得不cue一下模块化了。 模块化 随着前端应用的日益复杂化我们的项目已经逐渐膨胀到了不得不花大量时间去管理的程度。而模块化就是一种最主流的项目组织方式它通过把复杂的代码按照功能划分为不同的模块单独维护从而提高开发效率降低维护成本。 关于模块化的发展其实是有几个代表阶段
Stage1 - 文件划分方式
早期是基于文件划分的方式实现模块化开发这也是web最原始的模块系统。 具体做法将每个功能及其相关状态数据各自单独放到不同的JS文件中约定每个文件是一个独立的模块。如果要使用某个模块就将这个模块引入到页面中一个script标签对应一个模块然后直接调用模块中的成员变量/函数。 |——module-a.js
|——module-b.js
|——index.html// module-a.js
function foo() {console.log(moduleA#foo);
}// module-b.js
let name aDiao;
const data moduleB#foo;// index.htmlbodyscript srcmoudle-a.js/scriptscript srcmoudle-b.js/scriptscriptfoo();console.log(name);console.log(data);name aDiao#Ya;console.log(, name);data index#html;console.log(, data);/script/body从上面的demo中可以看到这样写会出现一些问题
模块直接在全局工作大量模块成员污染全局作用域没有私有空间所有模块内的成员都可以在模块外部被访问或者修改一旦模块增多容易产生命名冲突无法管理模块与模块之间的依赖关系在维护的过程中也很难分辨每个成员所属的模块。
Stage2 - 命名空间方式
后来我们约定每个模块只暴露一个全局对象所有模块成员都挂载到这个全局对象中。 具体做法是在第一阶段的基础上通过将每个模块“包裹”为一个全局对象的形式实现这种方式就好像是为模块内的成员添加了“命名空间”所以我们又称之为命名空间方式。 |——module-a.js
|——module-b.js
|——index.html// module-a.js
window.moduleA {method1: function () {console.log(moduleA#method1);},
};// module-b.js
window.moduleB {data: ImModuleB,method1: function () {console.log(moduleB#method1);},
};// index.htmlbodyscript srcmoudle-a.js/scriptscript srcmoudle-b.js/scriptscriptmoduleA.method1();moduleB.method1();moduleA.data ImModuleA;console.log(moduleA.data);console.log(moduleB.data);/script/body这种方式只是解决了命名冲突的问题但其他问题仍然存在。
Stage3 - IIFE立即调用函数表达式
立即调用函数表达式IIFE是一个在定义时就会立即执行的 JavaScript 函数。
它是一种设计模式也被称为自执行匿名函数主要包含两部分
第一部分是一个具有词法作用域的匿名函数并且用圆括号运算符 () 运算符闭合起来。这样不但阻止了外界访问自执行匿名函数中的变量而且不会污染全局作用域。第二部分创建了一个立即执行函数表达式 ()通过它JavaScript 引擎将立即执行该函数。
使用IIFE给模块提供私有空间避免污染全局命名空间。 具体做法是将每个模块成员都放在一个立即执行函数所形成的私有作用域中对于需要暴露给外部的成员通过挂到全局对象上的方式实现。 |——module-a.js
|——module-b.js
|——index.html// module-a.js
(function () {var name module-a;function method1() {console.log(name #method1);}window.moduleA {method1: method1,};
})();
// module-b.js
(function () {var name module-b;function method1() {console.log(name #method1);}window.moduleB {method1: method1,};
})();
// index.htmlbodyscript srcmoudle-a.js/scriptscript srcmoudle-b.js/scriptscriptmoduleA.method1();moduleB.method1();console.log(name);/script/body这种方式将成员私有化私有成员只能在模块成员内通过闭包的形式访问解决了全局作用域污染和命名冲突的问题。
Stage 4 - IIFE 依赖参数
在IIFE的基础上还可以利用IIFE参数作为依赖声明使用让每一个模块之间的依赖关系变得更加明显。
// module-a.js
(function ($) {var name module-a;function method1() {console.log(name #method1);$(.box).animate({ width: 200px });}window.moduleA {method1: method1,};
})(jQuery);// index.htmlstyle.box {width: 100px;height: 100px;background-color: pink;}/stylebodydiv classbox/divscript srchttps://unpkg.com/jquery/scriptscript srcmoudle-a.js/scriptscript srcmoudle-b.js/scriptscriptmoduleA.method1();moduleB.method1();/script/body模块化的标准规范
以上4个阶段是早期的开发者在没有工具和规范的情况下对模块化的实现方式虽然解决了很多在前端领域实现模块化的问题但仍然存在一些没有解决的问题。
其中比较明显的问题
模块化的加载。 上面都是通过script标签的方式直接在页面中引入这些模块时间久了维护起来会十分麻烦。比如某个代码需要用到某个模块如果html中忘记引入这个模块或者代码中移除了某个模块但是html中忘记删除该模块的引用等都会引起很多问题和麻烦。模块化的规范。 上面几种方式不同的开发者在实现的过程中都会出现一些细微的差别为了统一不同开发者、不同项目之间的差异需要指定一个行业标准来规范模块化的实现方式。 由此结合上述的问题现在的需求就是 一个统一的模块化标准规范一个可以自动加载模块的基础库 说到模块化规范在历史的长河中出现了前端五大模块化规范我知道滴有五种~ CommonJS规范最初提出来是在浏览器以外的地方使用并且当时命名为ServerJS后来为了体现它的广泛性更名为CommonJS也可以简称为CJS。 该规范约定一个文件就是一个模块每个模块都有单独的作用域通过 exports 或者 module.exports 导出需要暴露的内容然后通过 require 方法同步加载所依赖的模块。CommonJS模块的加载是同步的需要等模块加载完毕后后面的逻辑才会执行这个在服务器不会有什么问题因为服务器加载的是本地JS文件速度会很快。但如果在浏览器端加载需要先从服务端下载下来然后再加载运行会造成浏览器线程阻塞。 AMD规范即异步模块定义规范主要是为浏览器环境设计的推崇依赖前置也就是提前执行预执行在模块使用之前就已经执行完毕。 在 AMD 规范中约定每个模块通过 define() 函数定义这个函数默认可以接收两个参数第一个参数是一个数组用来声明这个模块的依赖项第二个参数是一个函数参数与前面的依赖项一一对应每一项分别对应依赖项模块的导出成员这个函数的作用就是为当前模块提供一个私有空间。如果在当前模块中需要向外部导出成员可以通过 return 的方式实现然后通过 require 语句加载模块。实现 AMD 规范的库主要是 require.js 和 curl.js。原生的 JavaScript 环境并不支持异步加载的方式require.js 提供了一种机制来异步加载模块并且可以在加载完成后执行回调函数。 CMD规范应用于浏览器的一种模块化规范也是通过异步加载模块的要解决的问题与 AMD 一样只不过是对依赖模块的执行时机不同 推崇就近依赖、延迟执行目前也很少使用了。 在CMD规范中通过全局函数 define 定义模块这个函数接受一个 factory 参数可以是一个函数也可以是一个对象或字符串当 factory 是函数时接收三个参数function(require, exports, module) require 函数用来获取其他模块提供的接口require(模块标识ID)exports 对象用来向外提供模块接口module 对象存储了与当前模块相关联的属性和方法。CMD从语法上分析结合了AMD模块定义的特点同时又沿用了CommonJs 模块导入和导出的特点 UMD规范UMD是AMD和CommonJS的糅合。 UMD的实现先判断是否支持Node.js模块exports是否存在存在则使用Node.js模块模式再判断是否支持AMDdefine是否存在存在则使用AMD方式加载模块前两个都不存在则将模块公开到全局window或global。 ESModule规范ESM是ECMAScript 2015 (ES6)中才定义的模块系统存在环境兼容问题。它的设计思想是尽量的静态化使得编译时就能确定模块的依赖关系以及输入和输出的变量。 在ESM规范 中使用 import 引用模块使用 export 导出模块。默认情况下Node.js 是不支持 import 语法的通过 babel 将 ES6 模块 编译为 ES5 的 CommonJS。因此 Babel 实际上是将 import/export 翻译成 Node.js 支持的 require/exports 。ESM的解析过程可以划分为三个阶段1构建根据地址查找JS文件并且下载将其解析为模块记录。2实例化对模块记录进行实例化并且分配内存空间解析模块的导入和导出语句把模块指向对应的内存地址。 3运行运行代码计算值并将值填充到内存地址中。
模块化可以帮助我们更好地解决复杂应用开发过程中的代码组织问题但是也会产生新的问题
模块化的方式划分出来的模块文件过多而前端应用又运行在浏览器中每一个文件都需要单独从服务器请求回来。零散的模块文件必然会导致浏览器的频繁发送网络请求影响应用的工作效率。【将散落的模块打包到一起】随着应用日益复杂在前端应用开发过程中不仅仅只有 JavaScript 代码需要模块化HTML 和 CSS 这些资源文件也会面临需要被模块化的问题。而且从宏观角度来看这些文件也都应该看作前端应用中的一个模块只不过这些模块的种类和用途跟 JavaScript 不同。【支持不同种类的前端资源模块】
针对这些问题我们可使用前端模块打包工具来解决。
使用Webpack实现模块化打包 Webpack 作为一个模块打包工具可以解决模块化代码打包的问题将零散的JavaScript代码打包到一个JS文件中。对于有环境兼容问题的代码Webpack 可以在打包过程中通过 Loader 机制对其实现编译转换然后再进行打包。对于不同类型的前端模块类型Webpack 支持在 JavaScript 中以模块化的方式载入任意类型的资源文件例如我们可以通过 Webpack 实现在 JavaScript 中加载 CSS 文件被加载的 CSS 文件将会通过 style 标签的方式工作。Webpack 还具备代码拆分的能力它能够将应用中所有的模块按照我们的需要分块打包。这样一来就不用担心全部代码打包到一起产生单个文件过大导致加载慢的问题。我们可以把应用初次加载所必需的模块打包到一起其他的模块再单独打包等到应用工作过程中实际需要用到某个模块再异步加载该模块实现增量加载或者叫作渐进式加载非常适合现代化的大型 Web 应用。 安装Webpack
1、初始化项目
npm init --yes2、安装Webpack如果是webpack4.0以上版本需要安装Webpack-cli
npm i webpack webpack-cli --save-dev3、查看Webpack版本信息
npx webpack --versionWebpack基本配置
在项目根目录添加Webpack的配置文件 webpack.config.js
module.exports {};1. 配置入口文件
入口文件就是应用程序的起点webpack在解析代码的时候会先找到入口文件从入口文件开始递归解析入口文件中所有的依赖项构建依赖图。
// 单入口
module.exports {entry: ./src/main.js,
};
// 多入口当需要创建多个 bundle 时可以配置多个入口点。
module.exports {entry: {pageOne: ./src/pageOne/index.js,pageTwo: ./src/pageTwo/index.js,pageThree: ./src/pageThree/index.js,},
};2. 配置输出
通过配置output属性告诉webpack在哪里输出它所创建的bundle以及如何命名这些文件。
const path require(path);module.exports {entry: ./src/main.js,output: {path: path.resolve(__dirname, dist),filename: bundle.js,},
};3. 配置loader
loader用来转换某些类型的模块负责完成项目中各种各样资源模块的加载。因为webpack默认只能打包处理JS类型的文件无法处理其它非JS类型的文件。如果想要处理非JS类型的文件需要手动安装一些合适的第三方loader加载器。比如将样式表CSS、图片、JSON 或 TypeScript 文件转换为 JavaScript 模块。
const path require(path);module.exports {entry: ./src/main.js,output: {path: path.resolve(__dirname, dist),filename: bundle.js,},module: {rules: [{ test: /\.css$/, use: css-loader },{ test: /\.ts$/, use: ts-loader },],},
};4. 配置plugin
plugin可以用来执行范围更广的任务增强webpack在项目自动化构建方面的能力。比如
打包之前自动清除上次打包的dist文件自动生成应用所需要的html文件自动压缩webpack打包完成后输出的文件自动发布打包结果到服务器实现自动部署…
const path require(path);module.exports {entry: ./src/main.js,output: {path: path.resolve(__dirname, dist),filename: bundle.js,},module: {rules: [{ test: /\.css$/, use: css-loader },{ test: /\.ts$/, use: ts-loader },],},plugins: [new HtmlWebpackPlugin({ template: ./src/index.html })],
};5. 配置工作模式
通过设置 mode 参数来选择不同的工作模式production【启动内置优化插件自动优化打包结果打包速度偏慢】、development【自动优化打包速度添加一些调试过程中的辅助插件】、none【运行最原始的打包不做任何额外处理】可以启用 webpack 内置在相应环境下的优化。其默认值为 production。
const path require(path);module.exports {mode: development,entry: ./src/main.js,output: {path: path.resolve(__dirname, dist),filename: bundle.js,},module: {rules: [{ test: /\.css$/, use: css-loader },{ test: /\.ts$/, use: ts-loader },],},plugins: [new HtmlWebpackPlugin({ template: ./src/index.html })],
};Webpack构建流程 webpack打包的大致过程 根据配置找到指定的入口文件从入口文件开始根据代码中出现的 import/require 解析这个文件所依赖的资源模块然后再分别解析每个资源模块的依赖构建依赖关系树然后递归遍历这个依赖树找到每个节点对应的资源文件把不同类型的模块交给对应的Loader 处理处理完成后打包到一起 (bundle.js) 。 注意 对于依赖模块中无法通过JS代码表示的资源模块如图片、字体文件等一般Loader会把它们单独作为资源文件拷贝到输出目录中然后将这个资源文件所对应的访问路径作为这个模块的导出成员暴露给外部。 浅看一下 webpack5.92.1 和 webpack-cli5.1.4 的源码 ~
运行webpack命令时把通过命令行传入的参数转换为webpack的配置选项对象根据命令行参数加载指定的配置文件载入webpack核心模块传入配置选项创建Compiler编译器对象。【webpack-cli5.1.4】
// webpack-cli/bin/cli.js
...
// 初始化并执行runCLI
const runCLI require(../lib/bootstrap);
...
runCLI(process.argv);// webpack-cli/lib/bootstrap.js
use strict;
Object.defineProperty(exports, __esModule, { value: true });
// eslint-disable-next-line typescript-eslint/no-var-requires
const WebpackCLI require(./webpack-cli);
// // 创建Webpack CLI的一个新实例并使用给定的参数运行CLI
const runCLI async (args) {const cli new WebpackCLI();try {await cli.run(args);}catch (error) {cli.logger.error(error);process.exit(2);}
};
module.exports runCLI;// webpack-cli/lib/webpack-cli.js
...// 运行 CLI解析命令行参数并执行相应的命令。async run(args, parseOptions) {...// 如果命令是构建或监听命令则执行相应的Webpack配置和构建流程。if (isBuildCommandUsed || isWatchCommandUsed) {await this.makeCommand(isBuildCommandUsed ? buildCommandOptions : watchCommandOptions, async () {// 加载Webpack配置。this.webpack await this.loadWebpack();// 获取内置选项。return this.getBuiltInOptions();}, async (entries, options) {// 如果有入口文件则将其合并到Webpack配置的入口选项中。if (entries.length 0) {options.entry [...entries, ...(options.entry || [])];}// 运行Webpack构建/监听流程。await this.runWebpack(options, isWatchCommandUsed);});}...}...// 异步执行 Webpack 打包过程的核心方法async runWebpack(options, isWatchCommand) {// 初始化Webpack编译器实例。let compiler;...// 创建Webpack编译器实例。compiler await this.createCompiler(options, callback);...}...// 配置和初始化Webpack编译器async createCompiler(options, callback) {...// 加载Webpack配置let config await this.loadConfig(options);// 构建Webpack配置config await this.buildConfig(config, options);let compiler;try {// 根据传入的配置选项数据初始化Webpack编译器如果提供了回调函数则在编译完成后调用compiler this.webpack(config.options,callback? (error, stats) {if (error this.isValidationError(error)) {this.logger.error(error.message);process.exit(2);}callback(error, stats);}: callback);// ts-expect-error error type assertion} catch (error) {// 处理初始化Webpack编译器时的错误...}return compiler;}
...从Webpack模块的入口文件出发首先会检查options参数是否符合webpack配置要求然后判断options的类型创建单个或多个编译器。
// webpack/lib/webpack.js
...
/*** 根据提供的配置选项创建并运行 Webpack 编译器可以接受单个配置对象或多个配置对象的数组。* 如果提供了回调函数则会根据配置运行编译器并在编译完成后执行回调。* 如果配置中设置了监听模式watch则会进入监听模式。*/
const webpack ((options, callback) {// 用于创建编译器实例并配置监听模式。const create () {// // 检查配置选项是否符合 Webpack 配置 schemaif (!asArray(options).every(webpackOptionsSchemaCheck)) {// 如果配置不通过 schema 检查则报告错误getValidateSchema()(webpackOptionsSchema, options);// 使用 util.deprecate 标记已弃用的功能并提供错误消息util.deprecate(() {},webpack bug: Pre-compiled schema reports error while real schema is happy. This has performance drawbacks.,DEP_WEBPACK_PRE_COMPILED_SCHEMA_INVALID)();}let compiler;let watch false;let watchOptions;// 根据 options 是否为数组创建单个或多个编译器if (Array.isArray(options)) {/*** 创建一个处理多个Webpack配置的编译器实例* createMultiCompiler()内部还是通过遍历options创建单个编译器实例合并成数组做处理。*/compiler createMultiCompiler(options,options);watch options.some(options options.watch);watchOptions options.map(options options.watchOptions || {});} else {// 创建一个处理单个Webpack配置的编译器实例const webpackOptions options;compiler createCompiler(webpackOptions);watch webpackOptions.watch;watchOptions webpackOptions.watchOptions || {};}return { compiler, watch, watchOptions };};if (callback) {...}}
);module.exports webpack;在创建单个Compiler对象的时候webpack会注册配置中的插件。
// webpack/lib/webpack.js
...
/*** 创建单个Webpack编译器实例。* param {number} [compilerIndex] index of compiler* returns {Compiler} a compiler*/
const createCompiler (rawOptions, compilerIndex) {...const compiler new Compiler(options.context,options);// 初始化Node环境插件设置编译器的运行环境。new NodeEnvironmentPlugin({infrastructureLogging: options.infrastructureLogging}).apply(compiler);// 遍历并应用配置中的插件列表。if (Array.isArray(options.plugins)) {for (const plugin of options.plugins) {if (typeof plugin function) {plugin.call(compiler, compiler);} else if (plugin) {plugin.apply(compiler);}}}...// 触发编译器的environment和afterEnvironment钩子在编译开始前进行环境相关的初始化。compiler.hooks.environment.call();compiler.hooks.afterEnvironment.call();// 这里创建内置插件new WebpackOptionsApply().process(options, compiler);compiler.hooks.initialize.call();return compiler;
};
...创建完Compiler对象之后会判断配置选项中是否开启监视模式。
如果是监视模式就调用Compiler对象的watch方法用监视模式启动构建。如果不是监视模式就调用Compiler对象的run方法开始构建整个应用。
// webpack/lib/webpack.js
...
/*** 根据提供的配置选项创建并运行 Webpack 编译器可以接受单个配置对象或多个配置对象的数组。* 如果提供了回调函数则会根据配置运行编译器并在编译完成后执行回调。* 如果配置中设置了监听模式watch则会进入监听模式。*/
const webpack ((options, callback) {// 用于创建编译器实例并配置监听模式。const create () {... };if (callback) {try {// 创建Webpack编译器和监听配置const { compiler, watch, watchOptions } create();// 如果设置了监听模式则直接开始监听if (watch) {compiler.watch(watchOptions, callback);} else {// 在非监听模式下执行编译开始构建整个应用在编译完成后关闭编译器compiler.run((err, stats) {compiler.close(err2 {callback(err || err2,stats);});});}// 返回webpack编译器实例return compiler;} catch (err) {// 如果在处理过程中出现错误就延迟调用回调函数并传递错误process.nextTick(() callback(err));// 返回null处理过程中出现错误return null;}} else {// 创建webpack编译器实例但不执行编译或者监听const { compiler, watch } create();if (watch) {util.deprecate(() { },A callback argument needs to be provided to the webpack(options, callback) function when the watch option is set. There is no way to handle the watch option without a callback.,DEP_WEBPACK_WATCH_WITHOUT_CALLBACK)();}return compiler;}}
);module.exports webpack;Compiler内部先触发beforeRun和run两个钩子然后调用 this.compile(onCompiled); 开始编译整个项目。
// webpack/lib/Compiler.js
...
class Compiler {constructor(context, options ({})) {...}...// 执行编译过程run(callback) {if (this.running) {return callback(new ConcurrentCompilationError());}const finalCallback (err, stats) {...}const startTime Date.now();this.running true;const onCompiled (err, _compilation) {...}// 执行编译的函数。const run () {// 调用beforeRun钩子。this.hooks.beforeRun.callAsync(this, err {if (err) return finalCallback(err);// 调用run钩子。this.hooks.run.callAsync(this, err {if (err) return finalCallback(err);// 读取记录。this.readRecords(err {if (err) return finalCallback(err);// 开始编译。this.compile(onCompiled);});});});};// 如果当前处于空闲状态则先结束缓存的空闲状态然后开始运行。if (this.idle) {this.cache.endIdle(err {if (err) return finalCallback(err);this.idle false;run();});} else {// 如果不处于空闲状态直接开始运行。run();}}...
}
module.exports Compiler;调用 this.compile(onCompiled); 方法内部主要是创建一个Compilation对象包含本次构建中全部的资源和信息。
// webpack/lib/Compiler.js
...
/*** 编译器编译方法负责执行编译的各个阶段包括预编译、编译、制作、完成制作、完成编译等步骤。*/
compile(callback) {// 初始化编译参数const params this.newCompilationParams();// 在编译前执行自定义钩子异步调用。this.hooks.beforeCompile.callAsync(params, err {// 如果钩子执行出错直接返回错误回调。if (err) return callback(err);// 执行编译阶段的钩子。this.hooks.compile.call(params);// 创建新的编译实例里面包含这次构建中全部的资源信息const compilation this.newCompilation(params);// 获取编译器的日志记录器用于记录特定于编译器的日志。const logger compilation.getLogger(webpack.Compiler);// 记录make阶段的开始时间。logger.time(make hook);// 执行make阶段的钩子异步调用。this.hooks.make.callAsync(compilation, err {// 记录make阶段的结束时间。logger.timeEnd(make hook);// 如果钩子执行出错直接返回错误回调。if (err) return callback(err);// 记录完成make阶段的开始时间。logger.time(finish make hook);// 执行完成make阶段的钩子异步调用。this.hooks.finishMake.callAsync(compilation, err {...});});
}
...创建完Compilation之后触发make钩子【事件触发机制】根据入口文件配置找到入口模块开始递归遍历所有的依赖形成依赖关系树。 make钩子是在编译过程中生成新的模块、依赖关系、chunk等。这个阶段的代码执行是通过事件触发机制让外部监听这个make事件的地方开始执行的。如果要知道哪些地方会开始执行就需要找到哪个地方注册了make事件。
// webpack/lib/Compiler.js
...
// 调用 make 钩子在编译过程中生成新的模块和 chunk
this.hooks.make.callAsync(compilation, (err) {logger.timeEnd(make hook); // 结束 make hook 计时器if (err) return callback(err); // 如果出现错误调用回调函数并传递错误logger.time(finish make hook); // 开始 finish make hook 计时器// 调用 finishMake 钩子在 make 钩子完成后执行的逻辑this.hooks.finishMake.callAsync(compilation, (err) {logger.timeEnd(finish make hook); // 结束 finish make hook 计时器if (err) return callback(err); // 如果出现错误调用回调函数并传递错误// 使用 process.nextTick 来确保在事件循环的下一轮执行process.nextTick(() {logger.time(finish compilation); // 开始 finish compilation 计时器// 调用 compilation.finish完成编译过程准备生成最终的输出compilation.finish((err) {logger.timeEnd(finish compilation); // 结束 finish compilation 计时器if (err) return callback(err); // 如果出现错误调用回调函数并传递错误logger.time(seal compilation); // 开始 seal compilation 计时器// 调用 compilation.seal封闭编译结果使其不可更改compilation.seal((err) {logger.timeEnd(seal compilation); // 结束 seal compilation 计时器if (err) return callback(err); // 如果出现错误调用回调函数并传递错误logger.time(afterCompile hook); // 开始 afterCompile hook 计时器// 调用 afterCompile 钩子用于执行编译完成后的逻辑this.hooks.afterCompile.callAsync(compilation, (err) {logger.timeEnd(afterCompile hook); // 结束 afterCompile hook 计时器if (err) return callback(err); // 如果出现错误调用回调函数并传递错误// 如果整个过程成功完成调用回调函数并传递编译结果return callback(null, compilation);});});});});});
});
...webpack官方通过自己的Tapable库实现事件注册可以在VSCode全局搜索 make.tap 来找到事件的注册位置。
// VSCode可能无法搜索node_modules里面的内容可以在setting.json里添加代码
{search.exclude: {**/node_modules:false},search.useIgnoreFiles:false
}然后就搜索到了7个插件中都注册了make事件这些插件都是前面创建Compiler对象的时候创建的【去看createCompiler代码】。 根据内置插件找到入口文件的处理插件 EntryPlugin 内部调用了 compilation.addEntry() 传入上下文、入口依赖和选项等参数开始解析入口文件。
// webpack/lib/EntryPlugin.js
class EntryPlugin {
...
/*** 此方法主要是在webpack编译器上应用EntryPlugin插件。通过监听编译和make阶段的钩子来设置入口依赖项和添加入口点到编译。*/
apply(compiler) {// 在编译阶段注册一个钩子用于设置入口依赖的工厂。compiler.hooks.compilation.tap(EntryPlugin,(compilation, { normalModuleFactory }) {// 将EntryDependency依赖的工厂设置为normalModuleFactory。compilation.dependencyFactories.set(EntryDependency,normalModuleFactory);});const { entry, options, context } this;// 创建一个入口依赖项。const dep EntryPlugin.createDependency(entry, options);// 在make阶段注册一个钩子用于向编译添加入口点。compiler.hooks.make.tapAsync(EntryPlugin, (compilation, callback) {// 添加入口点到编译传入上下文、入口依赖和选项。compilation.addEntry(context, dep, options, err {// 回调函数处理错误或完成添加。callback(err);});});
}
...
}找到 addEntry() 方法可以发现里面通过addModuleTree() 将入口模块添加到模块依赖列表中。
// webpack/lib/Compilation.js
class Compilation {...// 向webpack配置中的特定上下文添加一个新的入口项。addEntry(context, entry, optionsOrName, callback) {...// 调用内部方法添加入口项到指定的dependencies中。this._addEntryItem(context, entry, dependencies, options, callback);}...// 添加入口项。_addEntryItem(context, entry, target, options, callback) {...// // 添加模块树并处理结果。this.addModuleTree({context,dependency: entry,contextInfo: entryData.options.layer? { issuerLayer: entryData.options.layer }: undefined},(err, module) {...});}...// 根据传入的依赖信息创建一个新的模块添加模块树。addModuleTree({ context, dependency, contextInfo }, callback) {// // 验证 dependency 是否为有效对象if (typeof dependency ! object || dependency null || !dependency.constructor) {return callback(new WebpackError(Parameter dependency must be a Dependency));}// 从 dependency 中获取构造函数const Dep dependency.constructor;// 尝试从工厂中获取对应的模块创建函数const moduleFactory this.dependencyFactories.get(Dep);// 如果没有找到对应的工厂返回错误if (!moduleFactory) {return callback(new WebpackError(No dependency factory available for this dependency type: ${dependency.constructor.name}));}// 处理模块的创建过程this.handleModuleCreation({factory: moduleFactory,dependencies: [dependency],originModule: null,contextInfo,context},(err, result) {...});}
}在 handleModuleCreation 里调用 _handleModuleBuildAndDependencies 其内部通过Compiler对象的buildModule来进行模块构建。
// webpack/lib/Compilation.js
...
// 模块构建处理模块的解析、工厂调用、依赖注入和模块图的更新。
handleModuleCreation({factory,dependencies,originModule,contextInfo,context,recursive true,connectOrigin recursive,checkCycle !recursive},callback
) {// 获取模块图实例const moduleGraph this.moduleGraph;// 根据当前是否启用 profiling 创建对应的模块profileconst currentProfile this.profile ? new ModuleProfile() : undefined;// 实例化模块的过程包括解析依赖和执行工厂函数this.factorizeModule({currentProfile,factory,dependencies,factoryResult: true,originModule,contextInfo,context},(err, factoryResult) {// 处理工厂结果中的依赖信息const applyFactoryResultDependencies () {...};...// 获取工厂函数返回的模块实例const newModule factoryResult.module;...// 添加模块到模块图this.addModule(newModule, (err, _module) {if (err) {applyFactoryResultDependencies();if (!err.module) {err.module _module;}this.errors.push(err);return callback(err);}// 处理模块的不安全缓存逻辑const module _module;if (this._unsafeCache factoryResult.cacheable ! false module.restoreFromUnsafeCache this._unsafeCachePredicate(module)) {...}...// 继续处理模块的构建和依赖this._handleModuleBuildAndDependencies(originModule,module,recursive,checkCycle,callback);});});
}
...
// 处理模块构建及其依赖
_handleModuleBuildAndDependencies(originModule, // 原始模块触发构建的起点module, // 当前要处理的模块recursive, // 是否递归处理依赖checkCycle, // 是否检查循环依赖callback // 构建完成后的回调函数
) {// 检查在另一个构建过程中是否触发了构建以避免循环依赖let creatingModuleDuringBuildSet undefined;...// 构建模块buildModule方法中执行具体的Loader处理特殊资源的加载。this.buildModule(module, err {if (creatingModuleDuringBuildSet ! undefined) {// 如果在构建过程中添加了模块构建完成后移除creatingModuleDuringBuildSet.delete(module);}if (err) {// 如果构建过程中出现错误将错误与模块关联并添加到错误列表if (!err.module) {err.module module;}this.errors.push(err);return callback(err); // 返回错误}if (!recursive) {// 如果不需要递归处理依赖直接处理当前模块的依赖this.processModuleDependenciesNonRecursive(module);callback(null, module); // 回调函数无错误返回模块return;}// 为了避免循环依赖导致的死锁检查是否已经在处理依赖队列中if (this.processDependenciesQueue.isProcessing(module)) {return callback(null, module); // 如果已经在处理中直接返回模块}// 递归处理模块的依赖this.processModuleDependencies(module, err {if (err) {return callback(err); // 如果处理依赖时出现错误返回错误}callback(null, module); // 否则回调函数无错误返回模块});});
}然后再回到EntryPlugin类的apply方法里有一段代码是将 EntryDependency 类与 normalModuleFactory 关联起来。这意味着当遇到 EntryDependency 类型的依赖时将使用 normalModuleFactory 来创建对应的模块。
compiler.hooks.compilation.tap(EntryPlugin,(compilation, { normalModuleFactory }) {compilation.dependencyFactories.set(EntryDependency,normalModuleFactory);
});normalModuleFactory 主要是负责创建处理 JavaScript 模块的 NormalModule 实例。在 Webpack 的构建过程中normalModuleFactory 用来生成模块对象这些对象随后会经过一系列的处理步骤包括解析、编译、优化等。 在 normalModuleFactory 里通过createParser() 创建一个新的解析器实例, getParser() 获取一个特定类型的模块的解析器来解析模块构成抽象语法树AST。
// webpack/lib/NormalModuleFactory.js
...
getParser(type, parserOptions EMPTY_PARSER_OPTIONS) {let cache this.parserCache.get(type);if (cache undefined) {cache new WeakMap();this.parserCache.set(type, cache);}let parser cache.get(parserOptions);if (parser undefined) {parser this.createParser(type, parserOptions);cache.set(parserOptions, parser);}return parser;
}/*** param {string} type type* param {ParserOptions} parserOptions parser options* returns {Parser} parser*/
createParser(type, parserOptions {}) {parserOptions mergeGlobalOptions(this._globalParserOptions,type,parserOptions);const parser this.hooks.createParser.for(type).call(parserOptions);if (!parser) {throw new Error(No parser registered for ${type});}this.hooks.parser.for(type).call(parser, parserOptions);return parser;
}
...根据语法树分析模块是否还有对应的依赖模块如果有的话就会继续循环构建每个依赖直到所有的依赖解完成构建阶段结束。 最后会合并生成需要输出的bundle.js到dist目录。
Webpack热更新
webpack中的模块热替换就是说我们在程序运行的时候修改了某个模块内容如果没有使用模块热替换就需要刷新整个应用程序来实现更新并且刷新后页面中的状态信息都会丢失如果使用模块热替换就可以只用把变更的模块替换到应用程序里不用完全刷新整个应用。
在webpack中主要是通过开启 HotModuleReplacementPlugin 这个插件来开启模块热更新。
在HMR运行的时候通过执行 webpack-dev-server 命令开启两个服务器 express server 和socket server。express server 主要负责提供静态资源的服务打包后的资源直接被浏览器请求和解析socket server 是一个websocket长连接主要是监听对应模块发生变化后生成两个补丁文件并且推送给浏览器端。
当某个文件或者模块发生变化webpack通过监听这个文件或者模块对应的唯一hash值的变化来判断文件是否需要重新编译打包重新编译之后会再生成文件/模块对应的hash值作为下次热更新的标识。之后服务端会通过socket server向浏览器端推送变更消息消息内容主要是两个补丁文件和新的hash值。浏览器拿到两个新的文件后通过HMR runtime机制加载这两个文件并且针对修改的模块进行更新。
Webpack打包优化
1. 提高打包速度
1优化Loader让webpack拥有了加载和解析非js文件的能力在使用loader时可以通过配置include、exclude、test属性来匹配文件通过include、exclude规定哪些匹配应用loader优化Loader的文件搜索范围
module.exports {module:{rules:[{// js文件才使用babeltest:/\.js$/,loader:babel-loader,// 只在src文件夹下查找include:[resolve(src)],// 不会去查找的路径exclude:/node_modules/}]}
}2HappyPack因为webpack在打包的过程中是单线程的在执行过程中可能会遇到很多需要耗时间编译的任务HappyPack可以将Loader的同步执行转换为并行的。
module:{loaders:[{test:/\.js$/,include:[resolve(src)],exclude:/node_modules/,// id后面的内容对应下面loader:happypack/loader?idhappybabel}]
},
plugins:[new HappyPack({id:happybabel,loaders:[babel-loader?cacheDirectory],// 开启4个线程threads:4})
]3DllPluginDllPlugin可以将特定的类库提前打包然后引入。这种方式可以减少导包类库的次数只有当类库更新版本才会需要重新打包。
// 打包一个Dll库
module.exports {entry:{// 想统一打包的类库vendor:[react]},output:{path:path.join(__dirname,dist),filename:[name].dll.js,library:[name]-[hash]},plugins:[new webpack.DllPlugin({//name必须和output.library一致name:[name]-[hash],path:path.resolve(__dirname,./dll/[name].mainfest.json)})]
}// 引入Dll库
module.exports {...plugins:[new webpack.DllReferencePlugin({context:__dirname,// manifest就是之前打包出来的json文件manifest:require(./dist/vendor-manifest.json)})]
}4Code Splitting代码分割把项目中的资源模块按照我们设计的规则打包到不同的bundle中。实现方式有两种多入口打包动态导入。
// 多入口打包-webpack.config.js
module.exports {entry: {index: ./src/index.js,main: ./src/main.js},output: {filename: [name].bundle.js},optimization:{splitChunks:{chunks:all}}
};
// 动态导入-在入口文件里写
const update () {const hash window.location.hash || #postsconst mainElement document.querySelector(.main)mainElement.innerHTML switch (hash) {case #posts:import(./components/posts).then(posts {mainElement.appendChild(posts())})breakcase #about:import(./components/about).then(about {mainElement.appendChild(about())})breakdefault:}
}
window.addEventListener(hashchange, update)
update()2. 减少webpack打包体积
1使用插件压缩代码CSS代码压缩 CssMinimizerPlugin、HTML代码压缩 HtmlWebpackPlugin、文件大小压缩 ComepressionPlugin 、图片压缩…
const { HtmlWebpackPlugin } require(html-webpack-plugin);
const CompressionPlugin require(compression-webpack-plugin);
const CssMinimizerPlugin require(css-minimizer-webpack-plugin);module.exports {module: {rules: [{test: /\.(png|jpg|gif)$/,use: [{loader: file-loader,options: {name: [name]_[hash].[ext],outputPath: images/,},},{loader: image-webpack-loader,options: {mozjpeg: {progressive: true,quality: 65,},optipng: {enabled: false,},pngquant: {quality: 65-90,speed: 4,},gifsicle: {interlaced: false,},webp: {quality: 75,},},},],},],},plugins: [new HtmlWebpackPlugin({// HtmlWebpackPlugin 的配置项minify: {minifyCSS: false,collapseWhitespace: false,removeComments: true,},}),new CompressionPlugin({test: /\.(css|js)$/,threshold: 500,minRatio: 0.7,algorithm: gzip,}),],optimization: {minimize: true,minimizer: [new CssMinimizerPlugin({parallel: true,}),],},
};2Scope Hoisting能分析出模块之间的依赖关系尽可能的把打包出来的模块合并到一个函数中去。
module.exports {optimization:{concatenateModules:true, // 尽可能将所有模块合并到一起输出到一个函数中}
}3Tree Shaking可以“摇掉”项目中没有被引用的代码。webpack的Tree-shaking特性在生产环境下会自动开启。在其他环境下通过如下配置
module.exports {optimization:{usedExports:true, // 打包结果中指导处外部用到的成员minimize:true, // 压缩打包结果}
}4sideEffects通过配置标识我们的代码是否有副作用模块执行的时候除了导出成员是否还做了其他的事情从而决定是否要完整移除没有用到的模块。在生产环境下会自动开启。
module.exports {optimization:{sideEffects:true // 判断模块是否有副作用是否需要被打包。}
}以上就是我学习Webpack的知识笔记如有误请指正 学习链接 JavaScript模块化七日谈 一文吃透 Webpack核心原理 前端模块化开发那点历史 「前端工程四部曲」模块化的前世今生