从零实现一个迷你 Webpack

大家好,我是若川。我持续组织了近一年的源码共读活动,感兴趣的可以 点此扫码加我微信 lxchuan12 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列。另外:目前建有江西|湖南|湖北籍前端群,可加我微信进群。

本文为来自 字节跳动-国际化电商-S项目 的文章,已授权 ELab 发布。

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;
1d1729f34fd6aecf20213c0b119ae3e7.png

可以看到打包产物是一个立即执行函数,函数初始先定义了多个 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'); // 引用 webpackconst compiler = webpack(options); // 传入配置生成 compiler 对象compiler.run((err, stats) => {  // 执行编译, 传入回调});

因此,首先需要实现一个 webpack 方法,同时该方法支持传入 webpack 配置,返回 compiler 实例,webpack 官方支持了以 cli 的形式运行 webpack 命令和指定参数、配置文件,这一部分暂时简单实现,我们暴露出一个方法,方法接收用户的配置。

// mini-webpack/core/index.jsfunction 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 处理,返回处理后的代码

  1. 开始编译 loader 处理完的代码

  1. 若代码中依赖了其他文件,则对 require 函数替换为 webpack 自行实现的 __webpack__require__, 保存该文件的处理结果,同时让其他文件回到第 1 步进行处理,不断循环。

  1. 编译结束后,每个文件都有其对应的处理结果,将这些文件的编译结果从初始的入口文件开始组织到一起。

入口文件 loader 处理

读取入口文件,将入口文件交给 匹配的 loader 处理

// mini-webpack compiler.jsconst fs = require('fs');
class Compiler {constructor(options) {this.options = options || {};// 保存编译过程编译的 modulethis.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,});// 深度优先 遍历语法Treetraverse(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;}

Chunk

在上面的步骤中,已经处理了入口文件、依赖文件,但目前它们还是分散开来,在 webpack 中,是支持多个入口,每个入口是一个 chunk,这个 chunk 将包含入口文件及其依赖的 module

// mini-webpack compiler.jsconst fs = require('fs');
class Compiler {constructor(options) {this.options = options || {};// 保存编译过程编译的 modulethis.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.jsconst 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 `(() => {// webpackBootstrapvar __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 cachevar 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 modulereturn 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;

试试在浏览器下跑一下生成的代码

65f3711d4508c46519fa686f0a1b62ac.png

符合预期,至此便完成了一个极简的 webpack,针对单入口文件进行打包。当然真正的 webpack 远非如此简单,这里仅仅只是实现其一个打包思路。

❤️ 谢谢支持

以上便是本次分享的全部内容,希望对你有所帮助^_^

喜欢的话别忘了 分享、点赞、收藏 三连哦~。

欢迎关注公众号 ELab团队 收货大厂一手好文章~

  • 字节跳动校/社招内推码: WWCM1TA

  • 投递链接: https://job.toutiao.com/s/rj1fwQW

可凭内推码投递 字节跳动-国际化电商-S项目 团队 相关岗位哦~

参考资料

[1]

Node 接口 | webpack 中文文档: https://webpack.docschina.org/api/node/#webpack

- END -

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/274440.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ios 刷新遮罩遮罩_在Adobe XD中进行遮罩的3种方法

ios 刷新遮罩遮罩Are you new to Adobe XD? Or maybe you’re just stuck on how to create a simple mask? Here are 3 quick tips for how to mask your photos and designs in Adobe XD.您是Adobe XD的新手吗? 或者,也许您只是停留在如何创建简单的…

Vite 4.0 正式发布!

源码共读我新出了:第40期 | vite 是如何解析用户配置的 .env 的链接:https://www.yuque.com/ruochuan12/notice/p40也可以点击文末阅读原文查看,欢迎学习记笔记~12 月 9 日,Vite 4.0 正式发布。下面就来看看 Vite 4.0 有哪些更新吧…

图像标注技巧_保护互联网上图像的一个简单技巧

图像标注技巧补习 (TUTORIAL) Have you ever worried about sharing your images on the Internet? Anytime you upload something to the web you risk the chance of your work being used (without permission) by another.您是否曾经担心过要在Internet上共享图像&#xf…

【VueConf 2022】尤雨溪:Vue的进化历程

大家好,我是若川。我持续组织了近一年的源码共读活动,感兴趣的可以 点此扫码加我微信 lxchuan12 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试…

WCF netTcpBinding寄宿到IIS7

config配置文件不多说 <?xml version"1.0" encoding"utf-8" ?> <configuration><system.serviceModel><behaviors><serviceBehaviors><behavior name"myBehavior"><serviceMetadata/></behavior…

ar软件测试工具_如何为用户测试制作快速的AR原型

ar软件测试工具We had a project recently with an element of AR-based interaction, which it turned out was impossible to create as a prototype in either Invision or Framer (our current stack). This had a massive impact on our ability to test with users in a …

未来ui设计的发展趋势_2025年的未来UI趋势?

未来ui设计的发展趋势Humans are restless.人类是不安的。 Once we find something that works, we get used to it and we crave the next big thing. The next innovation. When will the future finally arrive? And when it does, how long will it take us to get used …

Monorepo 在网易的工程改造实践

大家好&#xff0c;我是若川。我持续组织了近一年的源码共读活动&#xff0c;感兴趣的可以 点此扫码加我微信 lxchuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试…

这一年,Vue.js 生态开源之旅带给我很大收获~

大家好&#xff0c;我是若川。我持续组织了近一年的源码共读活动&#xff0c;感兴趣的可以 点此扫码加我微信 lxchuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试…

CSSyphus:烦躁不安的烦恼设计指南。

I’m trapped at home with my website. Or maybe it’s trapped at home with me. While some are using the weird lump of time provided by lockdown to indulge in baking, dancing, painting, singing, I’m using it to play around with code.我 被自己的网站困在家里。…

你构建的代码为什么这么大?如何优化~

大家好&#xff0c;我是若川。我持续组织了近一年的源码共读活动&#xff0c;感兴趣的可以 点此扫码加我微信 lxchuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试…

用户体验需求层次_需求和用户体验

用户体验需求层次Shortly after the start of 2020 I led the development of a new website, and it went live in August. A week before the deployment, I paused development and took a step back in order to write about the project. Taking that pause, that step ba…

VMwareWorkstation设置U盘启动(或U盘使用)

最近在工作中&#xff0c;经常要和LINUX部署打交道&#xff0c;一般在生产环境部署之前需要在自己的机器上进行测试。比如使用U盘安装操作系统等。 在机器上安装了VMware Workstation9.0&#xff0c;运行多个测试虚拟机。理由所当然的要使用此做一些操作系统部署&#xff0c;…

类从未使用_如果您从未依赖在线销售,如何优化您的网站

类从未使用初学者指南 (A beginner’s guide) If you own a small business with a store front, you might have never had to rely on online sales. Maybe you’re a small clothing store or a coffee shop. You just made that website so people could find you online, …

狼书三卷终大成,狼叔亲传Node神功【留言送书】

大家好&#xff0c;我是若川。之前送过N次书&#xff0c;可以点此查看回馈粉丝&#xff0c;现在又和博文视点合作再次争取了几本书&#xff0c;具体送书规则看文末。众所周知&#xff0c;我在参加掘金人气作者打榜活动&#xff08;可点击跳转&#xff09;&#xff0c;需要大家投…

程序详细设计之代码编写规范_我在不编写任何代码的情况下建立了一个设计策划网站

程序详细设计之代码编写规范It’s been just over a month since MakeStuffUp.Info — my first solo project as an independent Creator; was released to the world. It was not a big project or complicated in any way, it’s not even unique, but I’m thrilled where …

偷偷告诉你们一个 git 神器 tig,一般人我不告诉TA~

大家好&#xff0c;我是若川。众所周知&#xff0c;我参加了掘金创作者人气作者投票活动&#xff0c;最后3天投票。今天可投28票&#xff0c;明天32票&#xff0c;后天36票&#xff08;结束&#xff09;。投票操作流程看这里&#xff1a;一个普通小前端&#xff0c;将如何再战掘…

DAO层使用泛型的两种方式

package sanitation.dao;import java.util.List;/** * * param <T>*/public interface GenericDAO <T>{/** * 通过ID获得实体对象 * * param id实体对象的标识符 * return 该主键值对应的实体对象*/ T findById(int id);/** * 将实体对象持…

将是惊心动魄的决战~

大家好&#xff0c;我是若川。一个和大家一起学源码的普通小前端。众所周知&#xff0c;我参加了掘金人气创作者评选活动&#xff08;投票&#xff09;&#xff0c;具体操作见此文&#xff1a;一个普通小前端&#xff0c;将如何再战掘金年度创作者人气榜单~。最后再简单拉拉票吧…

图书漂流系统的设计和研究_研究在设计系统中的作用

图书漂流系统的设计和研究Having spent the past 8 months of my academic career working co-ops and internships in marketing & communication roles, my roots actually stem from arts & design. Although I would best describe myself as an early 2000s child…