webpack编译打包过程浅析

如何进行webpack调试,可以参考这里

//webpack.config.js
const path = require("path");
module.exports = {
    entry:"./src/index.js",
    output:{
        filename:"bundle.js",
        path:path.resolve(__dirname,"dist")
    }
}

1、node_modules/webpack/bin/webpack.js

来看看node_modules/webpack/bin/webpack.js的核心代码。

const path = require("path");

//"d:\workspace\webpack\demo\node_modules\webpack-cli\package.json"
const pkgPath = require.resolve("webpack-cli/package.json");

const pkg = require(pkgPath);

//"d:\workspace\webpack\demo\node_modules\webpack-cli\bin\cli.js"
require(path.resolve(path.dirname(pkgPath),pkg.bin["webpack-cli"]));

require.resolvepath.resolve功能一样,解析后都会得到一个绝对路径。
加载node_modules/webpack-cli/package.json,从中获取bin属性值,进而加载node_modules/webpack-cli/bin/cli.js
在这里插入图片描述

2、node_modules/webpack-cli/bin/cli.js

继续探探node_modules/webpack-cli/bin/cli.js主要做了些什么。

let options;
try{
 	options = require("./utils/convert-argv")(argv);
}catch(err){
}
function processOptions(options){
    const webpack = require("webpack");
    try {
        compiler = webpack(options);
    } catch (err) {
    }
    function compilerCallback(err, stats) {
    }
    compiler.run((err, stats) => {
        compilerCallback(err, stats);
    });
}
processOptions(options);

在上面这部分代码中,大家很容易发现这样一种写法:在一个函数A中定义另一个函数B,并在A中调用B。

function A(){
    function B(){
    }
    B();
}
A();

这也是大家常说的闭包,用来延长作用域链的。读webpack源码的过程中,会发现很多模块在其实现过程中都是这样做的。

options = require("./utils/convert-argv")(argv)

require("./utils/convert-argv"),require了convert-argv这么一个模块,后面紧跟着(argv),直接传参调用了。可见,convert-argv这个模块对外提供的接口是一个函数。
一句话说清楚这个函数的功能:到webpack配置文件>加载配置文件,获取配置参数>处理各项配置参数,最后返回了options
下面代码是这个函数的一个大概实现,其中省略了"找"的过程,require(path.resolve(process.cwd(),configPath))即加载配置文件,processConfiguredOptions(options)即处理各项配置参数。

//convert-argv.js
const path = require("path");
module.exports = function(){
    let configFiles = [];
    let configFileLoaded = false;
    const options = [];
    const configFileName = "webpack.config.js";
    //D:\workspace\webpack\demo\webpack.config.js
    const resolvedPath = path.resolve(configFileName);
    configFiles.push({
        path:resolvedPath
    });
    const requireConfig = function requireConfig(configPath){
        let options = (function  WEBPACK_OPTIONS(){
            return require(path.resolve(process.cwd(),configPath));
        })();
        return options;
    }
    if(configFiles.length>0){
        configFiles.forEach(function(file){
            options.push(requireConfig(file.path));
        })
    }
    configFileLoaded = true;

    if(!configFileLoaded){
        return processConfiguredOptions(options);
    }else if(options.length==1){
        return processConfiguredOptions(options[0]);
    }else {
        return processConfiguredOptions(options);
    }
    
    function processConfiguredOptions(options){
        if(Array.isArray(options)){
            options.forEach(processOptions);
        }else {
            processOptions(options);
        }
        if(!options.context){
            options.context = process.cwd();
        }
        return options;
    }
    function processOptions(options){

    }
}
const webpack = require(“webpack”)

加载的模块是node_modules\webpack\lib\webpack.js,该模块提供的对外接口是函数webpack,接受两个参数:optionscallback

compiler = webpack(options)

webpack(options),调用webpack函数并传入参数options时,便进入了node_modules\webpack\lib\webpack.js模块。

3、node_modules\webpack\lib\webpack.js

这个模块中最重要的就是webpack函数了,输入options,输出compiler对象。
webpack函数首先会检查options语法,有错则抛出;语法OK则往下执行。
然后判断options是一个数组(其中的每个元素都是一个普通对象),还是一个普通对象。如果是数组,就会递归webpack(options)
本例中,options是一个普通对象,所以这里就分析普通对象这种情况。

const webpack = (options,callback) => {
    let compiler;
    options = new WebpackOptionsDefaulter().process(options);
    compiler = new Compiler(options.context);
    new NodeEnvironmentPlugin({
		infrastructureLogging: options.infrastructureLogging
    }).apply(compiler);
    compiler.options = options;
    if(options.plugins && Array.isArray(options.plugins)){
        for(const plugin of options.plugins){
            if(typeof plugin === 'function'){
                plugin.call(compiler,compiler);
            }else {
                plugin.apply(compiler);
            }
        }
    }
    compiler.options = new WebpackOptionsApply().process(options,compiler);
}
options = new WebpackOptionsDefaulter().process(options)

虽然我们在webpack.config.js中简简单单地配置了entryoutput这两个参数。
options = require("./utils/convert-argv")(argv)处理后也只多了context
但在new WebpackOptionsDefaulter().process(options)的这个过程,webpack默默地给我们添加了很多参数,真的很多。

const OptionsDefaulter = require("./OptionsDefaulter");
class WebpackOptionsDefaulter extends OptionsDefaulter {
    constructor(){
        super();
        this.set("entry", "./src");
		this.set("devtool", "make", options =>
			options.mode === "development" ? "eval" : false
        );
        this.set("context", process.cwd());
        this.set("module", "call", value => Object.assign({}, value));
        this.set("module.rules", []);
		this.set("output", "call", (value, options) => {
			if (typeof value === "string") {
				return {
					filename: value
				};
			} else if (typeof value !== "object") {
				return {};
			} else {
				return Object.assign({}, value);
			}
		});
    }
}

entrydevtoolcontextmodulemodule.rules 等,这里只是罗列了一小部分。
呃,等等,我们自己不是在webpack.config.js中配置了entry吗?怎么还有个默认的entry呀?
这就要说到webpack的参数合并策略了,这个策略其实就是WebpackOptionsDefaulter的父类OptionsDefaulterprocess方法,该方法会根据配置类型(undefined|make||call|append)来决定属性是使用用户自己配置的,还是使用默认值,还是调用方法经计算得到。

呃,那entry是啥配置类型,它最终采用的是我们自己配置的吗?
我们再回到WebpackOptionsDefaulter,这个类只有一个构造函数,而且这个构造函数里整齐划一地调用着this.set(),有的传入两个参数,有的传了三个参数。

    只传入两个参数时,
    第一个参数:属性名
    第二个参数:属性值

    结果是:
    this.default[属性名] = 属性值      //默认属性
    this.config[属性名] = undefined   //配置类型
    
    合并策略:
    配置类型为undefined:options自己有,则使用自有属性;没有,则使用默认属性;
                                     
                              
    传入三个参数时
    第一个参数:属性名
    第二个参数:配置类型,"make" | "call" | "append" 
    第三个参数:计算方式,是一个函数

    结果是:
    this.defaults[属性名] = 计算方式
    this.config[属性名] = 配置类型    "make" | "call" | "append" 

	合并策略:
    配置类型为make:options自己有,则使用自有属性;没有,则调用默认方法,根据其他属性计算得到该属性值;
    配置类型为call:不论options有没有,都会调用默认方法,重新计算该属性值。
    配置类型为append:不论options有没有,都会追加默认值。
const getProperty = (obj,path) => {
    let name = path.split(".");
    for(let i=0;i<name.length-1;i++){
        obj = obj[name[i]];
        if(typeof obj!== 'object' || !obj || Array.isArray(obj)) return;
    }
    return obj[name.pop()];
}

const setProperty = (obj,path,value) => {
    let name = path.split(".");
    for(let i=0;i<name.length-1;i++){
        if(typeof obj[name[i]] !== 'object' && obj[name[i]]!==undefined) return 
        if(Array.isArray(obj[name[i]])) return;
        if(!obj[name[i]]) obj[name[i]] = {};
        obj = obj[name[i]];
    }
    obj[name.pop()] = value;
}

class OptionsDefaulter{
    constructor(){
        this.defaults = {};
        this.config = {}
    }
    process(options){
        options = Object.assign({},options);
        for(let name in this.defaults){
            switch(this.config[name]){
                case undefined:
                    if(getProperty(options,name) === undefined){
                        setProperty(options,name,this.defaults[name]);
                    }
                    break;
                case "call":
                    setProperty(options,name,this.defaults[name].call(this,getProperty(options,name),options));
                    break;
                case "make":
                    if(getProperty(options,name) === undefined){
                        setProperty(options,name,this.defaults[name].call(this,options));
                    }
                    break;
                case "append":
                    let oldValue = getProperty(options,name);
                    if(!Array.isArray(oldValue)){
                        oldValue = [];
                    }
                    oldValue.push(...this.defaults[name]);
                    setProperty(options,name,oldValue);
                    break;
                default:
                    throw new Error("Defaulter cannot process " + this.config[name]);
            }
        }
        return options;
    }
    set(name,config,def){
        if(def !== undefined){
            this.defaults[name] = def;
            this.config[name] = config;
        }else{
            this.defaults[name] = config;
            delete this.config[name];
        }
    }
}
compiler = new Compiler(options.context)

这里新建了一个Compiler实例compiler。截取了部分代码,来看看Compiler实例有啥。
嗯,Compiler继承自父类Tapable,定义了有很多属性和方法,其中的一个属性hooks很特别,特别长。不着急哈,一步一步来看。

//node_modules/webpack/lib/Compiler.js
class Compiler extends Tapable{
    constructor(context){
        super();
        this.hooks = {
            shouldEmit: new SyncBailHook(["compilation"]),
			done: new AsyncSeriesHook(["stats"]),
			additionalPass: new AsyncSeriesHook([]),
			beforeRun: new AsyncSeriesHook(["compiler"]),
			run: new AsyncSeriesHook(["compiler"]),
			emit: new AsyncSeriesHook(["compilation"]),
			assetEmitted: new AsyncSeriesHook(["file", "content"]),
			afterEmit: new AsyncSeriesHook(["compilation"]),

			thisCompilation: new SyncHook(["compilation", "params"]),
			compilation: new SyncHook(["compilation", "params"]),
			normalModuleFactory: new SyncHook(["normalModuleFactory"]),
			contextModuleFactory: new SyncHook(["contextModulefactory"]),

			beforeCompile: new AsyncSeriesHook(["params"]),
			compile: new SyncHook(["params"]),
			make: new AsyncParallelHook(["compilation"]),
			afterCompile: new AsyncSeriesHook(["compilation"]),

			watchRun: new AsyncSeriesHook(["compiler"]),
			failed: new SyncHook(["error"]),
			invalid: new SyncHook(["filename", "changeTime"]),
			watchClose: new SyncHook([]),
            infrastructureLog: new SyncBailHook(["origin", "type", "args"])
            //省略不写了
        }
		this._pluginCompat.tap("Compiler", options => {
			switch (options.name) {
				case "additional-pass":
				case "before-run":
				case "run":
				case "emit":
				case "after-emit":
				case "before-compile":
				case "make":
				case "after-compile":
				case "watch-run":
					options.async = true;
					break;
			}
        });
        //部分属性
		this.outputFileSystem = null;
		this.inputFileSystem = null;
		this.recordsInpuPath = null;
		this.recordsOutputPath = null;
		this.options = {};
		this.context = context;
		this.running = false;
		this.watchMode = false;
    }
    //部分方法
    run(callback) {}
    createCompilation() {}
    newCompilation(params) {}
    createNormalModuleFactory() {}
    newCompilationParams(){}
    compile(callback){}
}

首先,super(),即调用父类Tapable的构造函数,Tapable的实现如下,它的第一行
new SyncBailHook(['options'])和上面hooks的第一个new SyncBailHook(["compilation"])几乎一样,仅有的差别是传入的参数不同。

//Tapable
function Tapable(){
    this._pluginCompat = new SyncBailHook(['options']);
	this._pluginCompat.tap(
		{
			name: "Tapable camelCase",
			stage: 100
		},
		options => {
            //省略了
		}
	);
	this._pluginCompat.tap(
		{
			name: "Tapable this.hooks",
			stage: 200
		},
		options => {
            //省略了
		}
	);
}

啊哈,到这儿,就到重点了webpack的事件系统,它本质上是订阅者发布者模式,很有必要好好了解下,那就从SyncBailHook开始吧。
可以看到SyncBailHook继承自Hook
Hook实现了tapAsynctapPromisetap方法,而compile方法却是抽象的,必须由子类自己去实现。
作为子类,SyncBailHook实现了compile方法,而tapAsynctapPromisetap三个方法中,它抛弃了tapAsynctapPromise,实际上只继承了tap
顾名思义,SyncBailHookSync同步的意思,tapAsyncAsynctapPromise中的promise都是异步的意思,明显不搭嘛。
另外,在这里我们能较明显地感受到javascript这门语言的弱点。

//SyncBailHook 
class SyncBailHook extends Hook {
	tapAsync() {
		throw new Error("tapAsync is not supported on a SyncBailHook");
	}
	tapPromise() {
		throw new Error("tapPromise is not supported on a SyncBailHook");
	}
	compile(){
	}
}
//Hook
class Hook{
    constructor(args){
        if(!Array.isArray(args)) args = [];
        this._args = args;
        this.taps = [];
    }

    tapAsync(options, fn) {}
    tapPromise(options, fn){}  
    compile(options){
        throw new Error("Abstract: should be overriden");
    }

    //options:可以是一个字符串,但更期望是一个对象。传入回调函数fn的参数
    //fn:回调函数
	tap(options, fn) {
        if (typeof options === "string") options = { name: options };   
		if (typeof options !== "object" || options === null)
			throw new Error(
				"Invalid arguments to tap(options: Object, fn: function)"
            );
            
        options = Object.assign({ type: "sync", fn: fn }, options);
        
		if (typeof options.name !== "string" || options.name === "")
            throw new Error("Missing name for tap");

        //订阅事件,注册监听事件
		this._insert(options);
    }
  
    //stage小,往前排;stage大,往后排
	_insert(item) {
		let stage = 0;
        if (typeof item.stage === "number") stage = item.stage;
        
        let i = this.taps.length;
		while (i > 0) {
			i--;
			const x = this.taps[i];
			this.taps[i + 1] = x;
			const xStage = x.stage || 0;
			if (xStage > stage) {
				continue;
			}
			i++;
			break;
        }
        this.taps[i] = item;
	}
}
new NodeEnvironmentPlugin().apply(compiler)

NodeEnvironmentPlugin是webpack的内置插件,这里初始化了这个插件,调用apply方法并传入Compiler对象作为参数。

new NodeEnvironmentPlugin({
	infrastructureLogging: options.infrastructureLogging
}).apply(compiler);

插件NodeEnvironmentPlugin的定义大体是这样的:

class NodeEnvironmentPlugin{
    constructor(options){
    	this.options = options || {};
    }
    apply(compiler){
        compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin",compiler => {
        });
    }
}
module.exports = NodeEnvironmentPlugin;

主要作用是在compiler.hooks.beforeRun钩子上注册了一个监听事件,回调函数compiler => {}就是事件处理函数,至于这个事件什么时候会被触发,我们拭目以待吧。
这里,我们也可以趁机学习下如何开发自己的插件,可以参考NodeEnvironmentPlugin用来定义:

class MyPlugin{
    constructor(){
    }
    apply(compiler){
        compiler.hooks.xxx.yyy("MyPlugin",compiler => {
        })
    }
}
module.exports = MyPlugin;

其中,xxxCompiler 实例的hooks中的任一种,yyy是与xxx配套的taptapAsynctapPromise

if (options.plugins && Array.isArray(options.plugins)) {}

如果我们在webpack.config.js中配置了plugins(是个数组,其中元素要么是通过new xxxPlugin()创建的插件实例,要么是一个函数),webpack就会在这里调用插件并传入参数compiler对象。事实上,不论是内置插件,第三方插件还是自己开发的插件,它的基本原理都是通过webpack提供的hooks(钩子),注册一个监听事件和事件回调函数, 来希望webpack在对的时间里做对的事情,从而顺利完成编译打包。

if (options.plugins && Array.isArray(options.plugins)) {
	for (const plugin of options.plugins) {
		if (typeof plugin === "function") {
			plugin.call(compiler, compiler);
		} else {
			plugin.apply(compiler);
		}
	}
}
compiler.options = new WebpackOptionsApply().process(options, compiler)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值