webpack 是当前使用较多的一个打包工具,将众多代码组织到一起使得在浏览器下可以正常运行,下面以打包为目的,实现一个简易版 webpack,支持单入口文件的打包,不涉及插件、分包等。
举个,先来看看下面这个 demo,例子很简单,一个 index.js,里面引用了一个文件 a.js,a.js 内部引入了 b.js,通过 webpack 最简单的配置,将 index.js 文件作为入口进行打包。
来看看打包后的内容是怎样的
// index.js
require('./a.js');
console.log('entry load');
// a.js
require("./b.js");
const a = 1;
console.log("a load");
module.exports = a;
// b.js
console.log("b load");
const b = 1;
module.exports = b;
可以看到打包产物是一个立即执行函数,函数初始先定义了多个 module,每个 module 是实际代码中被 require 的文件内容,同时由于浏览器不支持 require 方法,webpack 内部自行实现了一个 __webpack__require__,并将代码中的 require 全部替换为该函数(从打包结果可看出)。
在 webpack__require 定义之后,便开始执行入口文件,同时可以看出,webpack 的打包过程便是通过入口文件,将直接依赖和间接依赖以 module 的形式组织到一起,并通过自行实现的 require 实现模块的同步加载。
了解了打包产物后,便可以开始实现简易版的 webpack ,最终打包产物与 webpack 保持一致。
根据 Node 接口 | webpack 中文文档[1] 可以知道,webpack node api 对外暴露出了 webpack 方法,通过调用 webpack 方法传入配置,返回 compiler 对象,compiler 对象包含 run 方法可执行编译,即
const webpack = require('webpack'); // 引用 webpack
const compiler = webpack(options); // 传入配置生成 compiler 对象
compiler.run((err, stats) => { // 执行编译, 传入回调
});
因此,首先需要实现一个 webpack 方法,同时该方法支持传入 webpack 配置,返回 compiler 实例,webpack 官方支持了以 cli 的形式运行 webpack 命令和指定参数、配置文件,这一部分暂时简单实现,我们暴露出一个方法,方法接收用户的配置。
// mini-webpack/core/index.js
function webpack() {
// 创建compiler对象
const compiler = new Compiler(options);
}
module.exports = webpack;
如上,实现了一个 webpack 方法,可传入一个 options 参数,包括用户指定的打包入口 entry、output 等。
webpack({
entry: './index.js',
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js",
},
module: {
rules: []
}
})
上面已经实现了 webpack 配置的传入,compiler 的创建,接下来还需要实现 Compiler 类,该类内部暴露一个 run 方法,用于执行编译。
首先需要明确编译过程需要做的事情。
1 . 读取入口文件,将入口文件交给匹配的 loader 处理,返回处理后的代码
2 . 开始编译 loader 处理完的代码
3 . 若代码中依赖了其他文件,则对 require 函数替换为 webpack 自行实现的 __webpack__require__, 保存该文件的处理结果,同时让其他文件回到第 1 步进行处理,不断循环。
4 . 编译结束后,每个文件都有其对应的处理结果,将这些文件的编译结果从初始的入口文件开始组织到一起。
读取入口文件,将入口文件交给 匹配的 loader 处理
// mini-webpack compiler.js
const fs = require('fs');
class Compiler {
constructor(options) {
this.options = options || {};
// 保存编译过程编译的 module
this.modules = new Set();
}
run(callback) {
const entryChunk = this.build(path.join(process.cwd(), this.options.entry));
}
build(modulePath) {
let originCode = fs.readFileSync(modulePath);
originCode = this.dealWidthLoader(modulePath, originCode.toString());
return this.dealDependencies(originCode, modulePath);
}
// 将源码交给匹配的 loader 处理
dealWidthLoader(modulePath, originCode) {
[...this.options.module.rules].reverse().forEach(item => {
if (item.test(modulePath)) {
const loaders = [...item.use].reverse();
loaders.forEach(loader => originCode = loader(originCode))
}
})
return originCode
}
}
module.exports = Compiler;
这里需要开始处理入口文件的依赖,将其 require 转换成 自定义的 __webpack_require__,同时将其依赖收集起来,后续需要不断递归处理其直接依赖和间接依赖,这里用到了 babel 进行处理。
// 调用 webpack 处理依赖的代码
dealDependencies(code, modulePath) {
const fullPath = path.relative(process.cwd(), modulePath);
// 创建模块对象
const module = {
id: fullPath,
dependencies: [] // 该模块所依赖模块绝对路径地址
};
// 处理 require 语句,同时记录依赖了哪些文件
const ast = parser.parse(code, {
sourceType: "module",
ast: true,
});
// 深度优先 遍历语法Tree
traverse(ast, {
CallExpression: (nodePath) => {
const node = nodePath.node;
if (node.callee.name === "require") {
// 获得依赖的路径
const requirePath = node.arguments[0].value;
const moduleDirName = path.dirname(modulePath);
const fullPath = path.relative(path.join(moduleDirName, requirePath), requirePath);
// 替换 require 语句为 webpack 自定义的 require 方法
node.callee = t.identifier("__webpack_require__");
// 将依赖的路径修改成以当前路行为基准
node.arguments = [t.stringLiteral(fullPath)];
const exitModule = [...this.modules].find(item => item.id === fullPath)
// 该文件可能已经被处理过,这里判断一下
if (!exitModule) {
// 记录下当前处理的文件所依赖的文件(后续需逐一处理)
module.dependencies.push(fullPath);
}
}
},
});
// 根据新的 ast 生成代码
const { code: compilerCode } = generator(ast);
// 保存处理后的代码
module._source = compilerCode;
// 返回当前模块对象
return module;
}
到这里为止便处理完了入口文件,但是在处理文件过程,还收集了入口文件依赖的其他文件未处理,因此,在 dealDependencies 尾部,加入以下代码
// 调用 webpack 处理依赖的代码
dealDependencies(code, modulePath) {
...
...
...
// 为当前模块挂载新的生成的代码
module._source = compilerCode;
// 递归处理其依赖
module.dependencies.forEach((dependency) => {
const depModule = this.build(dependency);
// 同时保存下编译过的依赖
this.modules.add(depModule);
});
...
...
...
// 返回当前模块对象
return module;
}
在上面的步骤中,已经处理了入口文件、依赖文件,但目前它们还是分散开来,在 webpack 中,是支持多个入口,每个入口是一个 chunk,这个 chunk 将包含入口文件及其依赖的 module
// mini-webpack compiler.js
const fs = require('fs');
class Compiler {
constructor(options) {
this.options = options || {};
// 保存编译过程编译的 module
this.modules = new Set();
}
run(callback) {
const entryModule = this.build(path.join(process.cwd(), this.options.entry));
const entryChunk = this.buildChunk("entry", entryModule);
}
build(modulePath) {
}
// 将源码交给匹配的 loader 处理
dealWidthLoader(modulePath, originCode) {
}
// 调用 webpack 处理依赖的代码
dealDependencies(code, modulePath) {
}
buildChunk(entryName, entryModule) {
return {
name: entryName,
// 入口文件编译结果
entryModule: entryModule,
// 所有直接依赖和间接依赖编译结果
modules: this.modules,
};
}
}
module.exports = Compiler;
至此我们已经将入口文件和其所依赖的所有文件编译完成,现在需要将编译后的代码生成对应的文件。
根据最上面利用官方 webpack 打包出来的产物,保留其基本结构,将构造的 chunk 内部的 entryModule 的 source 以及 modules 的 souce 替换进去,并根据初始配置的 output 生成对应文件。
// mini-webpack compiler.js
const fs = require('fs');
class Compiler {
constructor(options) {
this.options = options || {};
// 保存编译过程编译的 module,下面会讲解到
this.modules = new Set();
}
run(callback) {
const entryModule = this.build(path.join(process.cwd(), this.options.entry));
const entryChunk = this.buildChunk("entry", entryModule);
this.generateFile(entryChunk);
}
build(modulePath) {
}
// 将源码交给匹配的 loader 处理
dealWidthLoader(modulePath, originCode) {
}
// 调用 webpack 处理依赖的代码
dealDependencies(code, modulePath) {
}
buildChunk(entryName, entryModule) {
}
generateFile(entryChunk) {
// 获取打包后的代码
const code = this.getCode(entryChunk);
if (!fs.existsSync(this.options.output.path)) {
fs.mkdirSync(this.options.output.path);
}
// 写入文件
fs.writeFileSync(
path.join(
this.options.output.path,
this.options.output.filename.replace("[name]", entryChunk.name)
),
code
);
}
getCode(entryChunk) {
return `
(() => {
// webpackBootstrap
var __webpack_modules__ = {
${entryChunk.modules.map(module => `
"${module.id}": (module, __unused_webpack_exports, __webpack_require__) => {
${module._source}
}
`).join(',')}
};
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
// Execute the module function
__webpack_modules__[moduleId](
module,
module.exports,
__webpack_require__
);
// Return the exports of the module
return module.exports;
}
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
(() => {
${entryChunk.entryModule._source};
})();
})()
`;
}
}
module.exports = Compiler;
试试在浏览器下跑一下生成的代码
符合预期,至此便完成了一个极简的 webpack,针对单入口文件进行打包。当然真正的 webpack 远非如此简单,这里仅仅只是实现其一个打包思路。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/e0ibggrhNdr_ZAGvxxok2Q
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。