webpack核心功能实现
2021 M02 14
前言
站在面试角度,webpack核心功能实现无非是plugin,loader。 更深入一点,自然是同步加载和异步加载。 如果说到加分项,那优化配置,提高打包速度也都算,但这部分不再本系列中。
本篇为源码系列核心实现第六篇,对应下图webpack部分。
同步加载
概述
不论是同步加载还是异步加载,webpack打包结果都是一个IIFE。 该IIFE接收一个类型为对象的modules参数, 该参数是一个以模块路径为key,函数为value的对象,其中函数体是文件内容。 对于每个require语法,都会被替换为webpack_require,每个文件路径都会补全为src下相关路径。 module(打包前),chunk(打包过程中),bundle(打包后) 是同一套代码在webpack打包不同时机的不同称呼。
实现
直接丢代码了,关键位置已经加了注释,代码不多,应该比较好理解。
(function (modules) {
//模块缓存
var installedModules = {}
//自己实现的__webpack_require__ 取代原生的require
function __webpack_require__(moduleId) {
//命中缓存 直接返回
if (installedModules[moduleId]) {
return installedModules[moduleId].exports
}
//未命中缓存 添加
var module = installedModules[moduleId] = {
id: moduleId,//模块id
load: false,//默认模块没有被加载,
exports: {},//导出的内容
}
//加载对应模块
//这里的this指向为module.exports貌似是一种规范
modules[moduleId].call(
module.exports, module,
module.exports, __webpack_require__
);
module.load = true//已经加载
return module.exports
}
//加载入口模块
return __webpack_require__('./src/index.js')
})(
{
'./src/index.js': (function (module, exports, __webpack_require__) {
const title = __webpack_require__('./src/title.js')
console.log(title)
}),
'./src/title.js': (function (module, exports,) {
module.exports = 'hello'
})
})
注意:同步加载用了闭包做缓存,在面试时候这是一个很好的闭包使用场景举例。
异步加载
概述
首先要明确一点,异步加载包含同步加载。 最先被加载的入口文件index.js就是同步加载,异步加载指的是其他模块。 整个异步加载过程其实有三个重要环节:同步加载,e函数,t函数。
同步加载好理解,e函数和t函数是什么东西呢?
e函数内部会通过创建script并指定src去加载其他模块,最后会把加载的依赖结果处理成promise返回。 t函数内部会进行esmodule和commonjs的兼容处理。
打包前vs打包后
//打包前
let btn = document.createElement('button')
btn.innerText = '按我'
document.body.appendChild(btn)
btn.addEventListener('click', () => {
import(/* webpackChunkName:'title' */'./title').then(res => {
console.log(res.default)
})
})
//打包后
(function (modules) {
//...
}, {
"./src/index.js":
(function (module, exports, __webpack_require__) {
let btn = document.createElement('button');
btn.innerText = '按我';
document.body.appendChild(btn);
btn.addEventListener('click', () => {
__webpack_require__.e(/*! import() | title */ "title")
.then(__webpack_require__.t.bind(null,
/*! ./title */ "./src/title.js", 7))
.then(res => {
console.log(res.default);
});
});
})
})
e函数之所以能够.then,是因为已经成了promise。 根据promise的链式调用原理,e函数return的结果会交给t函数继续处理。 同理,t函数的处理结果也会再次向后传递。 而此时的结果必然是兼容处理后的,带有default属性的对象。
异步加载的实现
// The module cache
var installedModules = {};
// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
var installedChunks = {
"main": 0
};
function __webpack_require__(moduleId) {
// Check if module is in cache
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(
module.exports,
module, module.exports,
__webpack_require__
);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
__webpack_require__.e = function (chunkId) {
var promises = []
var installedChunkData = installedChunks[chunkId]
//同样的资源加载后不会加载第二次
if (installedChunkData !== 0) {
var promise = new Promise(function (resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject]
})
promises.push(installedChunkData[2] = promise);
var script = document.createElement('script')
script.src = chunkId + '.bundle.js'
document.head.appendChild(script)
}
return Promise.all(promises)
}
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
//核心是push方法
jsonpArray.push = webpackJsonpCallback;
// 异步加载的模块结构如下
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["say"], {
"./src/say.js":
(function (module, exports) {
module.exports = 'say import ';
})
}]);
//该模块被加载就会执行push方法,也就是执行webpackJsonpCallback
// webpackJsonpCallback 接收一个data参数
// data形如 [[chunkId1,chunkId2],{}]
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
var moduleId, chunkId, i resolves = [];
for ( var = 0; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
//存储resolves回调数组
resolves.push(installedChunks[chunkId][0]);//[resolve,reject,promise]
}
//将所有加载后的chunkId标识为已加载
installedChunks[chunkId] = 0;
}
//将异步加载的模块与原模块合并,后续会在t函数内部执行一次webpack_require加载
for (moduleId in moreModules) {
modules[moduleId] = moreModules[moduleId];
}
//取出resolves回调数组并逐步执行
while (resolves.length) {
resolves.shift()();
}
//t函数会加载异步合并的代码并进行esmodule和commonjs语法的兼容处理
//经过t函数处理后的结果必然是一个带有default属性的对象
//promsie中,返回的内容会在下一个then中获取
// 默认进入时mode是7
// create a fake namespace object
// mode & 1: value is a module id, require it
// mode & 2: merge all properties of value into the ns
// mode & 4: return value when already ns object
// mode & 8|1: behave like require
__webpack_require__.t = function (value, mode) {
if (mode & 1) value = __webpack_require__(value);
if (mode & 8) return value;
if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if (mode & 2 && typeof value != 'string'){
for (var key in value){
__webpack_require__.d(ns, key,
function (key) { return value[key]; }.bind(null, key)
);
}
}
return ns;
};
};
关键部分已经给出注释,但可能细节上还需要动手多测试,验证。
loader
loader概述
webpack本身只能处理js文件,loader就是扩展webpack打包能力用的, 告诉它非js格式文件该怎么处理。比如css相关的style-loader,css-loader。
loader其实和node中的pipe很像,如同流水线,一个loader之后可以接另一个loader。 但最终,必须是一个js形式收尾。
实现一个简易loader
loader的本质是一个函数,该函数通过source参数接收输入,通过返回值输出。 下面以一个markdown-loader为例,举例说明一个loader是如何实现的。
const marked = require('marked')
module.exports = source => {
const html = marked(source)
return `module.exports = ${JSON.stringify(html)}`
}
这就完事了?是的,就是如此简单。 测试的话,这个loader可以在webpack的配置文件中以本地路径形式加载。
plugin
plugin概述
Webpack 的插件本质就是各种钩子,这就像组件生命周期一样,不同阶段存在不同钩子函数。 webpack赋予了这些钩子函数特定的能力,直接拿过来用就行了。
实现一个简易plugin
plugin可以写成一个类的形式,然后身上有个apply方法,这个是webpack约定好的。 下面写一个简单的小插件,用于打印构建过程涉及的文件名。
class GetFileListPlugin {
apply(compiler) {
compiler.hooks.emit.tap('GetFileListPlugin ', compilation => {
// compilation => 可以理解为此次打包的上下文
for (const name in compilation.assets) {
console.log(name)
const contents = compilation.assets[name].source()
compilation.assets[name] = {
//用于返回新内容
source: () => contents,
//返回内容大小
size: () => noComments.length
}
}
})
}
}
module.exports = GetFileListPlugin
源码压缩包
再会
情如风雪无常,
却是一动既殇。
感谢你这么好看还来阅读我的文章,
我是冷月心,下期再见。