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

  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. Cimage类处理图像像素(数据)的3种方式(转)

    这里只讨论对图像像素的处理,cimage类的具体用法查相关资料#include <atlimage.h>   //VS2010以后不用加这个 --------CImage  m_Image ...

  2. HTTP相关知识学习

    五层网络模型 应用层 包括HTTP,FTP... HTTP支持客户/服务器模式 简单快速有getpost方法 灵活,可以传输任意类型 无连接,每次连接只处理一个请求,收到应答就关闭 无状态,对于事物处 ...

  3. Mac电脑设置环境变量

    转载自:https://jingyan.baidu.com/article/8065f87f47b29523312498e4.html 1.使用快捷键Command+R(或者Windows键+R),或 ...

  4. async await和promise的区别,和使用方法

    async和promise都是异步方法,区别是async生成的结果是promise对象,async是promise的终结版. await只能在async中使用,await是阻塞的意思,就是暂停,你一起 ...

  5. 突然连不上虚拟机,本地网络里没有VMnet8

    今天打开虚拟机,突然发现无法ping通网络了,但是能ping通虚拟机ip,打开我的window的网络适配器发现居然没有vmnet 8虚拟网卡了,防火墙什么的都设置好了,仍然不行,后来发现,在网络和共享 ...

  6. .netcore 跨域问题

    CORS(跨域资源共享)是一种W3C标准,允许服务器放宽同源策略.使用CORS,服务器可以在显式允许某些跨域请求时拒绝其他跨域请求.CORS是相比其他跨域技术(比如JSONP)更安全.更灵活. ASP ...

  7. springboot项目记录3用户注册界面

    九.注册-前端页面 1.在register页面中编写发送请求的方法,采用点击事件来完成.选中对应的按钮(JQuery下的)(( " 选 择 器 " ) ) , 选 中 某 一 个 ...

  8. ffmpeg安装教程

    1 下载所需要的软件 mkdir /usr/local/soft cd /usr/local/soft wget https://www.ffmpeg.org/releases/ffmpeg-snap ...

  9. nginx中proxy_set_header Host $host的作用

    nginx配置upstream负载均衡后请求400,配置proxy_set_header Host $host成功.请问这是什么原理? proxy_set_header Host $host有什么作用 ...

  10. HBuilder uniapp手机定位

    // 获取当前位置 getPosition: function() { this.GetLocation(function(res) { if (res) { this.longitude = res ...