相信很多人都有这个疑问,为什么要阅读源码,仅仅只是一个打包工具,会用不就行了,一些配置项在官网,或者谷歌查一查不就好了吗,诚然在大部分的时候是这样的,但这样在深入时也会遇到以下几种问题。

  1. webpack 配置繁琐,具有 100 多个内置插件,200 多个钩子函数,在保持灵活配置的同时,也把问题抛给了开发者。如不同的配置项会不会对同一个功能产生影响,引用 Plugin 的先后顺序会不会影响打包结果?这些问题,不看源码是无法真正清晰的。

  2. plugin 也就是插件,是 webpack 的支柱功能。开发者可以自己使用钩子函数写出插件,来丰富 webpack 的生态,也可以在自己或公司的项目中引用自己开发的插件,来去解决实际的工程问题,不去探究源码,无法理解 webpack 插件的运行,也无法写出高质量的插件。

  3. 从前端整体来看,现代前端的生态与打包工具高度相关,webpack 作为其中的佼佼者,了解源码,也就是在了解前端的生态圈。

Tapable浅析

首先我们要先明白什么是 Tapable,这个小型库是 webpack 的一个核心工具。在 webpack 的编译过程中,本质上通过 Tapable 实现了在编译过程中的一种发布订阅者模式的插件机制。它提供了一系列事件的发布订阅 API ,通过 Tapable 可以注册事件,从而在不同时机去触发注册的事件进行执行。

下面将会有一个模拟 webpack 注册插件的例子来尝试帮助理解。

compiler.js

const { SyncHook, AsyncParallelHook }  = require('tapable');

class Compiler {
constructor(options) {
this.hooks = {
testSyncHook: new SyncHook(['name', 'age']),
testAsyncHook: new AsyncParallelHook(['name', 'age'])
} let plugins = options.plugins; plugins.forEach(plugin => {
plugin.apply(this);
});
} run() {
this.testSyncHook('ggg', 25);
this.testAsyncHook('hhh', 24);
} testSyncHook(name, age) {
this.hooks.testSyncHook.call(name, age);
} testAsyncHook(name, age) {
this.hooks.testAsyncHook.callAsync(name, age);
}
} module.exports = Compiler;

index.js

const Compiler = require('./complier');
const MockWebpackPlugin = require('./mock-webpack-plugin'); const complier = new Compiler({
plugins: [
new MockWebpackPlugin(),
]
}); complier.run();

mock-webpack-plugin.js

class MockWebpackPlugin {

  apply(compiler) {

    compiler.hooks.testSyncHook.tap('MockWebpackPlugin', (name, age) => {
console.log('同步事件', name, age);
}) compiler.hooks.testAsyncHook.tapAsync('MockWebpackPlugin', (name, age) => {
setTimeout(() => {
console.log('异步事件', name, age)
}, 3000)
})
}
} module.exports = MockWebpackPlugin;

我相信有些小伙伴看到上述代码,就已经明白了大概的逻辑,我们只需要抓住发布订阅这两个词,在代码中呈现的就是 tap 和 call,如果是异步钩子,使用 tapAsync, tapPromise 注册(发布),就要用 callAsync, promise(注意这里的 promise 是 Tapable 钩子实例方法,不要跟 Promise API 搞混) 触发(订阅)。

发布

    compiler.hooks.testSyncHook.tap('MockWebpackPlugin', (name, age) => {
console.log('同步事件', name, age);
}) compiler.hooks.testAsyncHook.tapAsync('MockWebpackPlugin', (name, age) => {
setTimeout(() => {
console.log('异步事件', name, age)
}, 3000)
})

这里可以看到使用 tab 和 tabAsync 进行注册,在什么时机注册的呢,在 Compiler 类的初始化时期,也就是在通过 new 命令生成对象实例的时候,下面的代码已经在 constructor 中被调用并执行了,当然这个时候并没有像函数一样被调用,打印出来姓名和年龄,这时我们只需要先知道,它们已经被注册了。

订阅

  run() {
this.testSyncHook('ggg', 25);
this.testAsyncHook('hhh', 24);
} testSyncHook(name, age) {
this.hooks.testSyncHook.call(name, age);
} testAsyncHook(name, age) {
this.hooks.testAsyncHook.callAsync(name, age);
}

通过 compiler.run() 命令将会执行下面两个函数,使用 call 和 callAsync 订阅。这个时候就会执行 console.log 来打印姓名和年龄了,所以说此时我们就能明白 webpack 中 compiler 和 compilation 中的钩子函数是以触发的时期进行区分,归根结底,是注册的钩子在 webpack 不同的编译时期被触发。

注意事项

这里要注意在初始化 Tapable Hook 的同时,要加上参数,传入参数的数量需要与实例化时传递给钩子类构造函数的数组长度保持一致。

    this.hooks = {
testSyncHook: new SyncHook(['name', 'age']),
testAsyncHook: new AsyncParallelHook(['name', 'age'])
}

这里并非要严格的传入 ['name', 'age'],你也可以取其它的名字,如 ['fff', 'ggg],但是为了语义化,还是要进行规范,如下方代码,截取自源码中的 lib/Compiler.js 片段,它们在初始化中也是严格按照了这个规范。

    /** @type {AsyncSeriesHook<[Compiler]>} */
beforeRun: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<[Compiler]>} */
run: new AsyncSeriesHook(["compiler"]),
/** @type {AsyncSeriesHook<[Compilation]>} */
emit: new AsyncSeriesHook(["compilation"]),

更具体的可以查看这篇文章 走进 Tapable - 掘金 (juejin.cn)

如何调试

想调试 webpack 源码,一般有两种方式,一种是 clone 调试,一种是 npm 包调试,笔者这里选择通过 clone 调试,运行 webpack 也有两种方式,一是通过 webpack-cli 输入命令启动,另外一种如下,引入 webapck,使用 webpack.run() 启动。

准备工作

首先可以用 https 从 github 上克隆 webpack 源码。

    git clone https://github.com/webpack/webpack
npm install

之后可以在根目录创建一个名为 source 的文件夹,source 文件夹目录如下

-- webpack
-- source
-- src
-- foo.js
-- main.js
-- index.html
-- index.js
-- webpack.config.js

index.js

const webpack = require('../lib/index.js');
const config = require('./webpack.config.js'); const complier = webpack(config);
complier.run((err, stats) => {
if (err) {
console.error(err);
} else {
console.log(stats);
}
})

webpack.config.js

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = {
mode: 'development',
devtool: 'source-map',
entry: './src/main.js',
output: {
path: path.join(__dirname, './dist'),
},
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
exclude: /node_modules/,
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Test Webpack',
template: './index.html',
filename: 'template.html'
})
]
}

引用 html-webpack-plugin 和 babel-loader 主要是想更清晰看到在构建过程中 webpack 会如何处理引入的 plugin 和 loader。

main.js

import foo from './foo.js';
import { isEmpty } from 'lodash'; foo(); const obj = {};
console.log(isEmpty(obj));
console.log('main.js');

foo.js

export default function foo() {
console.log('foo');
}

文件创建好了,这里使用 Vscode 进行调试, 打开 JavaScript 调试终端。

源码阅读

按照下面命令,启动 webpack

    cd source
node index.js

这里为了更加清晰, 可以打上一个断点。如在 lib/webpack.js 中,将断点打在 158 行,查看是如何生成的 compiler 实例。

这里需要点击单步调试,这样才能进入 create 函数中,一步步调试可以看到,首先会对传入的 options 进行校验, 如果不符合规范,将会抛出错误,由于这里的 options 是一个对象,将会进入到 createCompiler 函数内。

在这个函数内将会创造 Compiler 实例,以及注册引入的插件和内置插件。

笔者将会一步步的讲解这个函数都做了什么事,如

applyWebpackOptionsBaseDefaults:给没设置的基本配置加上默认值。

new Compiler:生成 compiler 实例,初始化一些钩子和参数。

NodeEnvironmentPlugin:主要是对文件模块进行了封装和优化,感兴趣的读者可以打断点,详细去查看。

接下来要做的事情就是注册钩子,如上文中引入了 html-webpack-plugin, 这里将会调用 HtmlWebpackplugin 实例的 apply 函数,这样就能明白为什么以 class 类的方式,写插件,为什么里面一定要加上 apply。紧接着创建完 compiler 实例后,正如官网上描述的,关于 compiler.hooks.environment 的订阅时期,在编译器准备环境时调用,时机就在配置文件中初始化插件之后。我们就能知其然,也能知所以然了。

再往下,

new WebpackOptionsApply().process(options, compiler):注册了内部插件,如 DllPlugin, HotModuleReplacementPlugin 等。

小技巧分享

这里简单分享了笔者看源码的步骤,然后还有两个技巧分享。

一是由于 webpack 运用了大量回调函数,一步步打断点是很难看的清楚的,可直接在 Vscode 中全局搜索 compiler.hooks.xxx 和 compilation.hooks.xxx, 去看 tap 中回调函数的执行。

二是可在 Vscode 调试中的 watch 模块,添加上 compiler 和 compilation,这样也是更方便观察回调函数的执行。如

总结

webpack 中的细节很是繁多,里面有大量的异常处理,在看的时候要有重点的看,有选择的看,如果你要看 make 阶段所做的事情, 可以重点去看如何生成模块,模块分为几种,如何递归处理依赖,如何使用 loader 解析文件等。笔者认为看源码还有一个好处,那就是让你对这些知名开源库没有畏惧心理,它们也是用 js 一行行写的,里面会有一些代码片段,可能写的也没有那么优美,我们在阅读代码的同时,说不定也能成为代码贡献者,能够在简历上留下浓墨重彩的一笔。

作者:百宝门-前端组-闫磊刚

原文地址:https://blog.baibaomen.com/我们为什么要阅读webpack源码/

我们为什么要阅读webpack源码的更多相关文章

  1. 如何阅读Java源码 阅读java的真实体会

    刚才在论坛不经意间,看到有关源码阅读的帖子.回想自己前几年,阅读源码那种兴奋和成就感(1),不禁又有一种激动. 源码阅读,我觉得最核心有三点:技术基础+强烈的求知欲+耐心.   说到技术基础,我打个比 ...

  2. newsstand杂志阅读应用源码ipad版

    一款newsstand iPad杂志阅读应用源码(newsstand在线下载/动态显示等)可以支持在线下载/动态显示等  ,也是一款newsstand iPad杂志阅读应用源码.运行之后,会在iPad ...

  3. 如何阅读Java源码

    刚才在论坛不经意间,看到有关源码阅读的帖子.回想自己前几年,阅读源码那种兴奋和成就感(1),不禁又有一种激动.源码阅读,我觉得最核心有三点:技术基础+强烈的求知欲+耐心. 说到技术基础,我打个比方吧, ...

  4. 如何阅读mysql源码

    在微博上问mysql高手,如何阅读mysql 源码大致给了下面的一些建议: step 1,知道代码的组织结构(官方文档http://t.cn/z8LoLgh: Step2: 尝试大致了解一条sql涉及 ...

  5. .30-浅析webpack源码之doResolve事件流(1)

    这里所有的插件都对应着一个小功能,画个图整理下目前流程: 上节是从ParsePlugin中出来,对'./input.js'入口文件的路径做了处理,返回如下: ParsePlugin.prototype ...

  6. 如何阅读jdk源码?

    简介 这篇文章主要讲述jdk本身的源码该如何阅读,关于各种框架的源码阅读我们后面再一起探讨. 笔者认为阅读源码主要包括下面几个步骤. 设定目标 凡事皆有目的,阅读源码也是一样. 从大的方面来说,我们阅 ...

  7. 如何阅读Java源码?

    阅读本文大概需要 3.6 分钟. 阅读Java源码的前提条件: 1.技术基础 在阅读源码之前,我们要有一定程度的技术基础的支持. 假如你从来都没有学过Java,也没有其它编程语言的基础,上来就啃< ...

  8. .17-浅析webpack源码之compile流程-入口函数run

    本节流程如图: 现在正式进入打包流程,起步方法为run: Compiler.prototype.run = (callback) => { const startTime = Date.now( ...

  9. .34-浅析webpack源码之事件流make(3)

    新年好呀~过个年光打游戏,function都写不顺溜了. 上一节的代码到这里了: // NormalModuleFactory的resolver事件流 this.plugin("resolv ...

  10. .30-浅析webpack源码之doResolve事件流(2)

    这里所有的插件都对应着一个小功能,画个图整理下目前流程: 上节是从ParsePlugin中出来,对'./input.js'入口文件的路径做了处理,返回如下: ParsePlugin.prototype ...

随机推荐

  1. unity读取Excel表格保存到Sqlite数据库

    1.读取Excel表格和保存sqlite数据库所用到的dll文件   下载链接:dll文件 最后如下图所示 废话不多说了,直接上代码吧 因为太长就折叠了 using System.Collection ...

  2. 记一次mybatis中like 不执行sql的问题,单引号 与双引号的坑

    使用环境: springboot 2.0.4 mybatis-spring:1.3.2 错误也是比较蛋疼:  不报错,但是查询出来的结果不对. 原错误sql 示意:  select  * from  ...

  3. MyBatis Plus 设置ID的自增 /非自增时遇到的问题

    非自增时 自己设置ID 其他可参考------>主键策略的几种类型 https://blog.csdn.net/hxyascx/article/details/105401767

  4. 前端vue框架上手记录

    ---恢复内容开始--- Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统,提供: 通过 @vue/cli 搭建交互式的项目脚手架. 通过 @vue/cli + @vue/cli-se ...

  5. window.location.href 下载文件页面出现跳转 (重定向失效的问题)

    页面出现跳转的话,要去检查请求的接口是不是出现了问题. 解决方法:URL前面加 "/" ${process.env.VUE_APP_BASE_API}   /   ${url}

  6. Jmeter九、jmeter中的函数和beanshell

    beanshell  松散类型的脚本语言 可在里面自定义函数

  7. ucharts的区域图、折线图(有x轴的),修改x轴显示为隔一个显示

    1.原本的显示方式: 2.想要的效果: 3.这边我使用的是uchart的组件,在uni_modules > qiun-data-charts > js_sdk > u-charts, ...

  8. centeros忘记root登录密码

    转载自:https://www.cnblogs.com/dongml/p/10333819.html 很多时候我们都会忘记Linux root 用户的口令,下面就教大家如果忘记root口令怎么办 第1 ...

  9. 第14章 身份验证:使用Identity将用户添加到应用程序(ASP.NET Core in Action, 2nd Edition)

    本章包括 ASP.NET Core中web应用程序的身份验证工作原理 使用ASP.NET Core标识系统创建项目 向现有web应用添加用户功能 自定义默认ASP.NET Core标识UI 像ASPN ...

  10. SQL Server 错误:特殊符号“•”导致的sql查询问题

    问题描述: 对于一些标题或字符串,例如: 如果导入数据库,就会发现会自动变成?号了: 在进行SQL查询的时候,会出现一个同一条sql语句在mysql直接执行sql可以查询到,但是mssql进行查询的时 ...