Webpack 基础原理

本文介绍了Webpack的基础原理,包括Symbol.toStringTag、Object.create(proto)和getter的概念。深入讲解了Webpack的文档渲染和loader的工作机制,特别是loader的执行顺序,如post、inline、normal和pre。还探讨了如何获取loader的绝对路径以及loader-runner在处理资源文件中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

0. 准备知识

Symbol.toStringTag

Symbol.toStringTag 是一个内置 symbol,它通常作为对象的属性键使用,对应的属性值应该为字符串类型,这个字符串用来表示该对象的自定义类型标签,通常只有内置的 Object.prototype.toString()方法会去读取这个标签并把它包含在自己的返回值里。

说白了,就是希望自己创建的对象能有自己的标签类型[Object Xxxx]

例如,有许多JS对象类型没有toStringTag属性,但还是能通过toString识别特定返回的方法。

Object.prototype.toString.call('foo');     // "[object String]"
Object.prototype.toString.call([1, 2]);    // "[object Array]"
Object.prototype.toString.call(3);         // "[object Number]"
Object.prototype.toString.call(true);      // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null);      // "[object Null]"

我们也想这样创建一个对象,用toString返回其类型标签,但是依然是[Object Object],咱没这待遇… 不过通过 Symbol.toStringTag给了我们vip特权。

let obj = {};
Object.defineProperty(obj,Symbol.toStringTag,{value: 'MyTagOY'});
console.log( Object.prototype.toString.call( obj ) ) // [object MyTagOY]
class MyTag {
    get [Symbol.toStringTag](){
        return 'MyTagOY'
    }
}
console.log( Object.prototype.toString.call( new MyTag() ) ) // [object MyTagOY]

Object.create(proto)

方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
参数 proto:新创建对象的原型对象

Object.create(null) 创建一个非常纯洁的对象,可以自己定义自己的hasOwnPropertytoString等方法。使用for...in遍历对象时,不需要在对原型属性进行检查了。

Object.create()的原理:

if( typeof Object.create != 'function' ){
	Object.create = function(proto){
		function F(){}; // 构造函数
		F.prototype = proto;
		return new F();
	}
}

getter

defineProperty 方法会直接在一个对象上定义一个新的属性,或者修改一个对象的现有属性,并返回这个对象。

复习一下 Object.defineProperty(obj, prop, descriptor)
obj:要在其上定义属性的对象。prop:要定义或修改的属性的名称。descriptor:属性描述符。

对于描述符:
configurable:属性是否可配置。默认值为false。

let obj = {};
Object.defineProperty(obj,'name',{
	value: 1
})

// 1. 试图去删除name属性时,属性是不可删除的。
console.log( delete obj.name ) ; console.log( Reflect.deleteProperty(o,'name') );
// 2. 试图再次去修改value, 会报错。
Object.defineProperty(obj,'name',{
	value: 2
})
// TypeError: Cannot redefine property: key
// 3. 配置writable为true后,可以修改值,注意writable只可以单向从true变为false,相反会报错。
Object.defineProperty(obj,'name',{
	value: 1,
	writable: true
});
Object.defineProperty(obj,'name',{
	value: 2
	writable: false
})
console.log(obj.name) // 2
// 4. 我们从value + writable这种数据描述符,转换到get + set这种存取描述符,也会报错。
Object.defineProperty(o, 'name', {
    value: 1,
	writable: true
})
console.log(o.key) // 1
Object.defineProperty(o, 'name', {
    get(){
		return '2'
	}
})
//5. 从一个存取描述符,转换到另一个存取描述符,也是报错的。
Object.defineProperty(o, 'name', {
    get () {
    	return 1;
    }
})
console.log(o.key)  // 1
Object.defineProperty(o, 'name', {
    get () {
    	return 2;
    }
})

enumerable:是否可枚举,默认false。
value:对应的值,默认undefined。
writable::value 是否可以被修改。默认false。
getter:当访问该属性时,会调用此函数。默认返回undefined。
setter:接受一个函数,当属性值被修改时,会调用此函数,并赋予新值。默认undefined。

注意valuewritable getset 键 不可同时出现。(数据描述符与存取描述符不可混用。)
扩展:proxy 可以代替 Object.defineProperty 去做事件监听(Vue)。

1. 原理

1.1 文档渲染

webpack打包后的格式:(function(){...})({'./src/index.js': (function(){...})})

(function(modules){
	// 1. 异步加载第一步 解析入口文件
	var installedModules = {};
	var installedChunks = {main: 0}; // 默认只有一个代码块,并且是已加载状态0
	
	function __webpack_require__(moduleId){
		 if(installedModules[moduleId]){
            return installedModules[moduleId]
        }
        var module = installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {}
        };
        // 执行模块函数
        modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);
        module.l = true;
        return module.exports;
	}
	// 2. 创建 __webpack_require__.e  函数 => 返回promise, 读取已有代码块或存新状态;创建JSONP请求。
	__webpack_require__.e = function(chunkId){ // title
		var installChunkData = installedChunks[chunkId]; // 获取老的代码块,刚开始只有main,是没有的title的。
		let promise = new Promise(function(resolve,reject){
            installChunkData = installedChunks[chunkId] = [resolve,reject]
        }) // 如果调用了resolve方法,则此promise会变为成功态。
        // 由于没有title代码块,所以 installChunkData => undefined -> [resolve,reject] -> [resolve,reject,promise]
        installChunkData[2] = promise;
        // 请求服务服务器(JSONP)
        let script = document.createElement('script');
        script.src = chunkId + '.bundle.js' // title.bundle.js
        document.head.appendChild(script);
        // 这里就会拿到 title.bundle.js 中的代码块
        return promise
	}
	// 3. jsonArray拿到window['webpackJsonp'] 一开始是没有的,没有 赋值为 [];重新jsonArray的push方法为webpackJsonpCallBack,记得保存老的push方法,并设置this指向。
	function webpackJsonpCallBack(data){ // 读取异步导入文件,合并到modules上;installedChunks 赋予该模块状态。
		let chunkIds = data[0]; // ['script']
        let moreModules = data[1]; // {'./src/title.js': (function(){...})}
        let resolves = []; // 保存值
        // 给installedChunks赋值title, 并标示状态。
        for( let i = 0 ; i < chunkIds.length ; i++ ){
            let chunkId = chunkIds[i];
            resolves.push(installedChunks[chunkId][0]) //这时还是installedChunks[chunkId]还是[resolve,reject,promise] (保存)
            installedChunks[chunkId] = 0 // 已经加载成功。(改变)
        }
        // 将moreModules(对象)与 modules进行合并 => {'./src/index.js':... ; './src/title.js':...}
        // 合并到modules上的原因是 可以用__webpack_require__执行对应的模块函数了
        for (const moduleId in moreModules) {
            modules[moduleId] = moreModules[moduleId]
        };
        
        while( resolves.length ){
            resolves.shift()() // 取数第一个,并执行。代表着promise的成功,会走下一个then
        };
        
        if(parentJsonFunction){
            // 虽然把数组的push重写了,但是老的push方法也得保留在parentJsonFunction。确保data放到数组里,必须绑定this。
            parentJsonFunction(data)
        }
	};
	var jsonArray =( window['webpackJsonp'] = window['webpackJsonp'] || [] );
    var oldJsonpFunction = jsonArray.push.bind(jsonArray);
    // 重新jsonArray的push方法,重新复制为webpackJsonpCallBack;在 title.bundle.js 中的push实际上是webpackJsonpCallBack方法。
    jsonArray.push = webpackJsonpCallBack;
    var parentJsonFunction = oldJsonpFunction; // 保留 老数组的push方法。
	
	// 4. __webpack_require__.t
	__webpack_require__.t = function(value,mode){ //mode 为什么要用二进制判断,十分方便。 7 对应二进制 111
		value = __webpack_require__(value); // 返回字符串'title'
		// 兼容 es6Module,保证 __webpack_require__.t函数返回的值中 必须带有'default',不然还得判断是否为es6模块,是es6模块要用'default'取值,不是则直接获取。
        // 补充:commomjs => module.exports = 'title' => 'title'
        //       ex6Module => export default 'title'  => {default: 'title'}  两种模式取值是不同的。
        let ns = Object.create(null);
        Object.defineProperty(ns,'__esModule',{value: true}); // __esModule表示这是es6模块语法
        Object.defineProperty(ns,'default',{value});
        return ns; // {__esModule: true,default: 'title'}
	}
	
	return __webpack_require__('./src/index.js');
})
({
	'./src/index.js': (function(){	// 入口文件
		function (module, exports, __webpack_require__) {
                let button = document.createElement('button');
                button.innerHTML = '点击';
                button.addEventListener('click', () => {
                    __webpack_require__
                    .e("title") // e函数:加载‘title’代码块。
                    .then(__webpack_require__.t.bind(null, "./src/title.js", 7)) // 作用是保证 result肯定是一个对象 并且有'title'属性。
                    .then(result => { // {__esModule: true,default: 'title'}
                        console.log(result.default) // title
                    })
                })
                document.body.appendChild(button)
            }
	})
})
// title.config.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["title"], {
    "./src/title.js":
    (function (module, exports) {
    module.exports = 'title'
})
}]);

1.2 loader

所有的loader的执行顺序都有两个阶段:pitching阶段normal阶段,类似于js中的事件冒泡、捕获阶段。
Pitching阶段: post,inline,normal,pre
Normal阶段:pre,normal,inline,post

pitch / normal 每个完整的loader都有一个pitch,首先是从左往右执行 pitch没有返回值时,继续执行右面的pitch,当一旦遇到 带返回值的pitch,将不再执行后面的loader,直接跳到第一个loader,将pitch的返回值传给source,执行,然后结束。

loader类型

loader 的来源有两个:行内配置文件
由于loader有执行顺序,有的loader必须第一个执行,有的必须在最后执行。
loader的叠加顺序 = post(后置) + inline(内联) + normal(正常)+ pre(前置)。
前缀的含义

  1. 行内loader:一般写在路径里。
let request = 'inline-loader1!inline-loader2!./style.css';
  1. 前置loader,设置enforce 为 pre
    当不写 enforce时,为 normal 正常。
let rules = [
	{test: /\.css$/,enforce: 'pre',use: ['pre-loader1','pre-loader2']}
	{test: /\.css$/,enforce: 'pre',use: ['pre-loader1','pre-loader2']}
	{test: /\.css$/,enforce: 'pre',use: ['pre-loader1','pre-loader2']}
]
  1. 正常loader
let rules = [
	{test: /\.css$/,use: ['normal-loader1','normal-loader2']}
	{test: /\.css$/,use: ['normal-loader1','normal-loader2']}
]
  1. 后置loader,设置enforce 为 post
let rules = [
	{test: /\.css$/,enforce: 'post',use: ['post-loader1','post-loader2']}
	{test: /\.css$/,enforce: 'post',use: ['post-loader1','post-loader2']}
]

得到一堆loader的绝对路径

let path = require('path');

let nodeModules= path.resolve(__dirname,"node_modules");

let request = "!!inline-loader1!inline-loader2!./style.css"; //行内
let rules = [
    {test: /\.css$/,enforce: 'pre',use: ["pre-loader1","pre-loader2"]}, // 前置
    {test: /\.css$/,use: ["normol-loader1","normol-loader2"]}, // 正常
    {test: /\.css$/,enforce: 'post',use: ["post-loader1","post-loader2"]} // 后置
];

//-------------------------
// 不要pre和普通loader,只剩下 inline + post
const noPreAutoLoaders = request.startsWith("-!")
// 不要普通loaders
const noAutoLoaders = noPreAutoLoaders || request.startsWith("!");
// 不要pre post 普通,只剩下inline
const noPerPostAutoLoaders = request.startsWith("!!");

//-----------------------
let inlineLoaders = request.replace(/^-?!+/,"")
                           .replace(/!!+/g,"!")
                           .split("!"); // [inline-loader1,inline-loader2,./style.css]
let resource = inlineLoaders.pop(); // ./style.css

//-----------------------
// 经过这个映射,是把一个loader模块名变成一个绝对路径数组。
let resolveLoader = loader => path.resolve(nodeModules,loader + '.js')

let preLoaders = [];
let postLoades = [];
let normalLoaders = [];
for( let i = 0 ; i < rules.length ; i++ ){
    let rule = rules[i];
    if( rule.test.test(resource) ){
        if( rule.enforce === 'pre' ){
            preLoaders.push(...rule.use);
        }else if(rule.enforce === 'post'){
            postLoades.push(...rule.use);
        }else{
            normalLoaders.push(...rule.use)
        }
    }
}

let loaders;
if(noPerPostAutoLoaders){
    loaders = [...inlineLoaders]
}else if(noPreAutoLoaders){
    loaders = [...postLoades,...inlineLoaders]
}else if(noAutoLoaders){
    loaders = [...postLoades,...inlineLoaders,...preLoaders]
}else {
    loaders = [...postLoades,...inlineLoaders,...normalLoaders,...preLoaders];
}

loaders = loaders.map(resolveLoader) // 将loader全部转为绝队路径
console.log(loaders)

loader-runner

读取 要加载的资源文件(src/index.js),传给loader,一个一个的去加工,最终返回加工后的结果。

let path = require('path');
const fs = require('fs');

function createLoaderObject(loader){
    let loaderObj = {data: {}}; // data后面有用
    loaderObj.request= loader; // loader的绝对路径
    loaderObj.normal = require(loader); // 拿到loader对应函数代码块
    loaderObj.pitch = loaderObj.normal.pitch;
    return loaderObj;
};
function runLoader(options,callback){
    let loaderContext = {} // 这个对象最终会成为loader函数的this
    let resource = options.resource // 要加载的资源路径
    let loaders = options.loaders // 要使用的loader模块 [loader1,loader2,loader3]
    loaders = loaders.map(createLoaderObject); // [{normal:...,pitch:...},{normal:...,pitch:...},...]
    loaderContext.loaderIndex = 0; // 当前的索引
    loaderContext.readResource = fs; //读取资源的方法是readerFile方法
    loaderContext.resource = resource;
    loaderContext.loaders = loaders;
    iteratePitchingLoader(loaderContext,callback);

    function processResource(loaderContext,callback){
        let buffer = loaderContext.readResource.readFileSync(loaderContext.resource,'utf-8');
        loaderContext.loaderIndex--;
        iterateNormalLoader(loaderContext,buffer,callback)
    };
    function iterateNormalLoader(loaderContext,args,callback){
        if( loaderContext.loaderIndex < 0 ){
            return callback(null,args);
        }
        let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
        let normalFn = currentLoaderObject.normal;
        let result = normalFn.call(loaderContext,args); // loader n 的返回值是loader n-1 的参数。
        loaderContext.loaderIndex--;
        iterateNormalLoader(loaderContext,result,callback);
    };
    function iteratePitchingLoader(loaderContext,callback){
        if(loaderContext.loaderIndex >= loaderContext.loaders.length){ // 当索引大于loader个数时
            return processResource(loaderContext,callback); // 处理index.js文件
        };
        let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; // 拿到第一个loader
        let pitchFn = currentLoaderObject.pitch;
        if(!pitchFn){
            loaderContext.loaderIndex++
            return iteratePitchingLoader(loaderContext,callback)
        }
        let result = pitchFn.apply(loaderContext);
        if(result){
            loaderContext.loaderIndex--
            iterateNormalLoader(loaderContext,result,callback)
        }else{
            loaderContext.loaderIndex++
            iteratePitchingLoader(loaderContext,callback)
        }
    }
}

let entry = './src/index.js';
let options = {
    resource: path.resolve(__dirname,entry),
    loaders: [ // 用这三个loader去加载entry文件,也就是上一个标题代码返回的结果。
        path.resolve(__dirname,'dist注释/loader/loaders/loader1.js'),
        path.resolve(__dirname,'dist注释/loader/loaders/loader2.js'),
        path.resolve(__dirname,'dist注释/loader/loaders/loader3.js')
    ]
}

runLoader(options,(err,result)=>{
    console.log('执行完毕')
    console.log(result) // loader2patch//1
})
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值