最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Webpack源码浅析

    正文概述 掘金(李彦辉Jacky)   2021-04-02   520

    前言

    上一篇文章中我们实现了一个简易打包器,简单了解了 webpack 是的工作原理,为了验证我们的猜想对不对,那我们就去源码找找看吧

    How 怎么读源码

    读源码之前,首页要准备好源码(废话)

    有两种获得源码的方式

    第一种git clone , 这种方法可以得到各个版本的 webpack 代码,当 webpack 更新的时候也可以通过 git pull 拉取最新代码,不过可能是网络原因,我是使用 git clone 一直无法拉取下来,退而求其次我选择了第二种方法

    第二种是通过 releases 下载,那就需要选择对应的版本下载,这里我选择的是 5.10.0 版本,下载对应的压缩文件即可

    带着问题读源码

    webpack 代码量极大,里面分支众多,错综复杂,如果想一行一行的把代码都读一遍,显然是不可行的,也是低效的,那么我们的目标应该是只看核心代码,了解 webpack 打包的整体流程

    为了了解 webpack 的整体运行流程这一个大目标,我们需要把它分解成一个个小目标或问题,通过不断地拆解问题,找到答案的过程中,了解整个脉络

    那么第一问题是: 编译的起点?

    源码解析

    如何运行 webpack

    在问题开始之前,我们先了解一个基本知识,启动 webpack 有两种方法

    第一种方法 使用 webpack-cli 从命令行启动

    npx webpack-cli
    

    这是最快最便捷的方法,默认的打包入口是 src/index.js

    第二种方法 通过 require('webpack') 引入包的方式执行

    两种方式都会调用 webpack 来打包代码,只是操作方式不同

    问题一:编译的起点

    一切从 compiler = webpack(options, callback); 开始

    webpack 函数源码(lib/webpack.js)

    const webpack = ((options,callback) => {
        const create = () => {
            ...
            let compiler;
            if (Array.isArray(options)) {
                ...
            } else {
                /** @type {Compiler} */
                compiler = createCompiler(options);
                ...
    	}
    	return { compiler, watch, watchOptions }
        };
        if (callback) {
            try {
                const { compiler, watch, watchOptions } = create();
                    if (watch) {
                        compiler.watch(watchOptions, callback);
    		} else {
                        compiler.run((err, stats) => {
    			compiler.close(err2 => {
                                callback(err || err2, stats);
    			});
                        });
    		}
    		return compiler;
                } catch (err) {...}
    	} else {...}
    });
    

    这段代码中可以看到 compiler.run 是核心代码,代表者编译的开始,而 createCompiler 里面主要是实例化 compiler 和做一些初始化,比如读取配置,激活 webpack 的内置插件...

    webpackcompiler 是核心对象之一,通过 new Compiler 实例化,compiler 记录了完整的 webpack 环境信息,webpack 从启动到结束,compiler 只会被生成一次,你可以在 compiler 对象上读取到 webpack config 信息,outputPath

    comilper.run() 编译真正开始了

    Tapable

    在阅读 webpack 源码的时候,经常会看到 xxx.hooks.xxx,这实际上是一个事件管理库 Tapable, 想要继续阅读源码就必须快速了解一下 Tapable 的基本用法

    // 定义一个事件/钩子
    this.hooks.eventname = new SyncHook(['arg1', 'arg2'])
    // 监听一个事件/钩子
    this.hooks.eventName.tab('监听理由',fn)
    // 触发一个事件/钩子
    this.hooks.eventName.call('arg1', arg2)
    

    问题二:webpack 的流程是怎样的

    上文说到 compiler.run() 是编译的开始,那我们跟着这个方法,进去看吧

    找到 Compiler 类的定义文件 lib/Compiler.js (源码)

    run(callback) {
        const onCompiled = (err, compilation) => {...};
        const run = () => {
            this.hooks.beforeRun.callAsync(this, err => {
                ...
                this.hooks.run.callAsync(this, err => {
                    ...
                    this.readRecords(err => {
                        ...
                        this.compile(onCompiled);
                   });
                });
              });
        };
    
        if (this.idle) {
            ...
    	run();
        });
        } else {
            run();
        }
    }
    

    compiler.run 的代码中,我们可以看到在编译的过程中,会在相应的阶段触发钩子,如 hooks.beforeRun hooks.run,这种方式就很像我们在用 vue 或者 react 中的生命周期钩子,第三方插件可以定义对于不同的事件的处理函数,在相应的编译时间段触发,来实现插件的效果

    在阅读的过程中,我们应该把重要的钩子和方法记录下来,方便阅读

    比如上面的代码就可以记成

    new Compiler // 重要的方法
    -env         // -hooks 代表钩子
    -init
    compiler.run   
    -beforeRun    
    -run
    compiler.readRecords
    compiler.compiler
    ...
    

    通过这种方法,我们可以快速了解 webpack 的流程

    接着上面的代码,我们要开始看 compiler.compiler

    webpack 源码 lib/Compiler.js

    compile(callback) {
        const params = this.newCompilationParams();
        this.hooks.beforeCompile.callAsync(params, err => {
            this.hooks.compile.call(params);
            const compilation = this.newCompilation(params);
            this.hooks.make.callAsync(compilation, err => {
                this.hooks.finishMake.callAsync(compilation, err => {
                    process.nextTick(() => {
                        compilation.finish(err => {
                                compilation.seal(err => {
    				this.hooks.afterCompile.callAsync(compilation, err => {
                                        return callback(null, compilation);
                                    });
                                });
                            });
                        });
                    });
                });
            });
    }
    

    因为源码细节比较多,所以上面简略了一些错误处理和log信息

    继续记录编译过程

    new Compiler // 重要的方法
    -env         // -hooks 代表钩子
    -init
    compiler.run   
    -beforeRun   
    -run
    compiler.readRecords
    compiler.compiler
    -beforeCompile
    -compile
    newCompilation
    -make
    -finishMake
    nextTick
    compilation.finish
    compilation.seal
    -afterCompile
    

    最终我们基本可以知道 webpack 的编译可以分为 env > init > run > compile > compilation > make > finishMake > afterCompile > seal > emit 这几个阶段

    另外从代码中我们看到了一个重量级的对象 compilation, 看名字就可以知道,这个对象在编译过程中一定是起着重要作用的。compilation 代表一次单一的编译作业,compilation 记录了当前编译作业的模块资源和编译生成的文件、以及依赖信息等。

    问题三:读取 index.js 并分析依赖是在哪个阶段

    按照我们刚刚分析的几个阶段,我们首先可以排除 env init(猜的),肯定是在 compiler > afterCompile 之间

    make > finishMake 这几个阶段的可能性是最大的

    为什么呢?了解过 C 语言就会知道,make是编译时必须用到的工具

    那么我们就在这两个阶段中寻找

    直接贴上源码

    Webpack源码浅析

    上面的代码除了 logger 和 错误处理之后,好像什么都没做,怎么就从 make 变成 finishMake 了呢?

    Webpack源码浅析

    问题四:make-finishMake 之间做了什么

    还记得之前提到的 Tabable 库吗?也就是 webpack 的事件分发系统,那么我们可以从 make 这个事件绑定的处理函数开始下手

    直接全局搜索 make.tapAsync 可以发现都是插件在定义处理函数

    Webpack源码浅析

    其中 EntryPlupin 特别显眼,编译既然要分析依赖,分析之前就要有入口,那么 entryPlupin 显然是必须看的

    查看 make.tapAsync("EntryPlugin") 代码可以发现,里面实际上调用了 compilation.addEntry, 经过多次的查找 compilation.addEntry -> compilation._addEntryItem -> compilation.addModuleChain -> compilation.handleModuleCreation

    我们研究一下 handleModuleCreation

    handleModuleCreation({ factory, dependencies, originModule, context, recursive = true },callback) {
        ...
        this.factorizeModule({ currentProfile, factory, dependencies, originModule, context },(err, newModule) => {
            ...
            this.addModule(newModule, (err, module) => {
                ...
                this.processModuleDependencies(module, err => {
                    ...
                    callback(null, module);
                });
            });
        }
        );
    }
    

    可以看到这里有很多重要步骤,我们从第一开始分析,也就是 this.factorizeModule(), 点击这个函数,我们最终会看到 this.factorizeQueue.add(options, callback)

    那么 factorizeQueue 到底是什么呢? 可以看看他的定义

    Webpack源码浅析

    这时我们应该查看它的处理器 processor,在 this._factorizeModule 中,我们最终可以看到是 factory.create()

    问题五:factory.create 是什么东西

    Webpack源码浅析

    在这里我们需要摸着参数一个个根据调用关系往上找,最终在 addModuleChain 中我们找到了线索,factory 是从 this.dependencyFactories.get(Dep) 得到的

    Webpack源码浅析

    然后线索就中断了 this.dependencyFactories.get(Dep) 是什么呢? 既然有 get 那就应该有 set,经过再次全局搜索(全靠运气),终于在 EntryPlugin 中找到了

    Webpack源码浅析

    factory 就是 normalModuleFactory

    那么 factory.create 就是 normalModuleFactory.create

    由于 normalModuleFactory 太长了,以下简称 nmf

    阶段性小结

    目前为止我们了解到

    • webpack 使用 hooks 把主要的阶段固定下来
    • webpack 执行过程中,插件自己选择阶段做事
    • 入口是由入口插件 EntryPlugin.js 搞定的

    理清思路后,继续探究...

    问题六:nmf.create 做了什么

    源码 nmf.create

    create(data, callback) {
        ...
        this.hooks.beforeResolve.callAsync(resolveData, (err, result) => {
            ...
            this.hooks.factorize.callAsync(resolveData, (err, module) => {
                ...
                callback(null, factoryResult);
            });
        });
    }
    

    通过这里我们可以看到实际上触发了两个 hooks ,boforeResolvefactorize , 通过查看对应的事件处理函数,我们发现 factory.tap 里面由重要代码

    它触发了 resolveresolve 主要的作用就是收集 loaders

    然后它触发了 createModule 得到了 createdMolude

    也就是说,factory.create 的作用是收集 loaders 和得到了 module 对象

    问题七:addModule 做了什么

    既然我们已经清楚了 factory.create 做了什么,那我们再回到 compilation.factorizeModle()(忘记了可以回看问题四的代码图) , 发现后面的操作是 addModulebuildModule

    那么 addModule 做了什么呢?

    addModule 就是把一个 module 加入到 compilation.modules 里面

    回看问题四里面的代码,我们可以看到这个阶段的 addModule 实际上是把,factory.create() 得到的 newModule 加入到 copilation.modules 里面

    问题八:buildModule 做了什么

    一看这名字,就知道是很重要的一步

    buildModule 实际上是调用了 normalModule.buildnormalModule.build 则调用了自身的 doBuild 方法

    const { runLoaders } = require("loader-runner");
    doBuild(options, compilation, resolver, fs, callback){
        // runLoaders从包'loader-runner'引入的方法
        runLoaders({
            resource: this.resource,  // 这里的resource可能是js文件,可能是css文件,可能是img文件
            loaders: this.loaders,
        }, (err, result) => {
            const source = result[0];
            const sourceMap = result.length >= 1 ? result[1] : null;
            const extraInfo = result.length >= 2 ? result[2] : null;
            // ...
        })
    }
    

    doBuild 函数里面可以看到调用了 runLoaders 方法,作用是通过 loader 将一些非 js 的文件,比如 css,html..., 转成 js(webpack 只能识别 js),runLoaders 之后得到 result 继续下一步处理

    接下来就是把 js 转成 ast 代码了

    result = this.parser.parse(source);

    其中 this.parse.parse 实际上是一个第三方库 acorn 提供的 parse 方法

    parse(code, options){
        // 调用第三方插件`acorn`解析JS模块
        let ast = acorn.parse(code)
        ...
        if (this.hooks.program.call(ast, comments) === undefined) {
            this.detectStrictMode(ast.body)
            this.prewalkStatements(ast.body)
            this.blockPrewalkStatements(ast.body) // 分析依赖
            this.walkStatements(ast.body)
        }
    }
    

    问题九:webpack 如何知道 index.js 依赖了哪些文件的

    上一个问题,我们已经分析到了 webpack 会使用 runLoaders 把代码都转成 js,然后通过 acorn 库,将 js 转换成 ast 代码

    那么 webpack 如何分析依赖呢?

    按照上篇文章的原理分析,应该会 traverse 这个 ast,寻找 import 语句

    在源码中可以发现 this.blockPrewalkStatements(ast.body)ImportDeclaration 进行了检查,一旦发现 import 'xxx', 就会触发 import 钩子,对应的监听函数会处理依赖,将依赖加入到 module.dependencies 的数组中

    Webpack源码浅析

    问题十:怎么把 module 合并成一个文件

    经过上面的分析,我们大概知道 webpack 分为 env > compile > make > seal > emit 这几个阶段

    我们可以猜出合并文件会在 seal > emit 阶段之间

    在这个阶段调用了 compilation.seal, 该函数会创建 chunks、对每个 chunk 进行 codeGeneration, 然后为每个 chunk 创建 asset

    seal 之后就是 emit 阶段,也就是发射,很明显就行文件写出去,最终得到 dist/main.js 和其他 chunk 文件

    总结

    本文篇幅比较长,非常感谢你阅读到最后,最后简单总结一下 webpack 的基本流程:

    1. env 和 init 阶段,调用 webpack 函数接收 config 配置信息,并 apply 所有 webpack 内置插件
    2. 调用 compiler.run 进入编译阶段
    3. make 阶段,从 entry 为入口,通过 loaders 对模块的源代码进行转换成 JS 模块,然后使用 acorn parse 成 ast(抽象语法数),再遍历语法树分析和收集依赖
    4. seal 阶段,webpackmodule 转为 chunk,每个 chunk 可以是一个模块或多个模块
    5. emit 阶段,为每个 chunk 创建文件,并写入硬盘

    码字不易,如果这篇文章对你有用请点个赞吧

    如果有写得不好的地方,欢迎大佬们指导


    起源地下载网 » Webpack源码浅析

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元