从零实现 webpack,但 Rust 版 - [2] 实现 MVP 版本

从零实现 webpack,但 Rust 版 - [2] 实现 MVP 版本

技术博客 admin 178 浏览

参考 mini-webpack,使用 Rust 从零实现一个简单的 webpack,深入理解 webpack 的同时,还锻炼了 Rust 的技能,简直赢麻了!

代码地址:github.com/ParadeTo/rs…

本文对应 Pull Request:github.com/ParadeTo/rs…

前文已经介绍如何使用 oxc 来解析并修改 JS 代码,解决了最核心的问题,那现在已经可以实现一个 MVP 版本了,本次 MVP 目标是实现对下面这个代码的打包,并能正常运行输出的结果:

js
代码解读
复制代码
// index.js const b = require('./const.js') console.log(b) // const.js module.exports = 'hello'

我们新建一个 Compiler 结构体,它包括如下属性和方法:

rs
代码解读
复制代码
pub struct Compiler { config: Config, entry_id: String, root: String, modules: HashMap<String, String>, assets: HashMap<String, String>, } impl Compiler { pub fn new(config: Config) -> Compiler { } fn parse( &self, module_path: PathBuf, parent_path: &Path, ) -> (String, Rc<RefCell<Vec<String>>>) { // Get module's code and dependencies } fn build_module(&mut self, module_path: PathBuf, is_entry: bool) { // Call build_module recursively the get modules (key is module_id, value is the code) } fn emit_file(&mut self) { // Output result } pub fn run(&mut self) { // The entry } }

其中,run 是入口,它会调用 build_modulebuild_module 首先会调用 parse 得到 JS 模块的代码和它的依赖进行返回(顺便还会对原来的代码进行转换),然后 build_module 中会递归调用自己继续对这些依赖进行处理,最终得到 modulesmodules 是一个 HashMap,其中 key 为模块 id,即模块相对于 root 的路径,value 则是转换后的模块代码。run 最后会调用 emit_file 输出结果。完整代码请见 Pull Request,下面挑几个重点讲一下。

Tranform

前文曾给出了一个 Transform 的例子,用于解释如何修改 require 的参数,例子中我们是把参数改成了固定的 full_path_of_b

rs
代码解读
复制代码
string_literal.value = Atom::from("full_path_of_b")

但是,实际开发的时候,这里的参数是跟当前 JS 模块的路径有关的,是动态的。我们可以用下面这个例子来说明(给 require 的参数增加一个动态的 prefix):

rs
代码解读
复制代码
struct MyTransform { prefix: String, } impl<'a> Traverse<'a> for MyTransform { fn enter_call_expression(&mut self, node: &mut CallExpression<'a>, ctx: &mut TraverseCtx<'a>) { if node.is_require_call() { let argument: &mut Argument<'a> = &mut node.arguments.deref_mut()[0]; match argument { Argument::StringLiteral(string_literal) => { let old_name = string_literal.value.as_str(); let new_name = format!("{}{}", self.prefix, old_name); // !!!!!! `new_name` does not live long enough string_literal.value = Atom::from(new_name.as_str()); } _ => {} } } } }

上面的代码编译不通过,会报错 new_name does not live long enough,因为 new_name 会在函数执行完后销毁,但是 Atom::from 声明了一个生命周期 'a。解决办法是这样:

rs
代码解读
复制代码
string_literal.value = ctx.ast.atom(new_name.as_str());

原因可以通过 ctx.ast.atom 的源码来解释:

rs
代码解读
复制代码
#[inline] pub fn atom(self, value: &str) -> Atom<'a> { Atom::from(String::from_str_in(value, self.allocator).into_bump_str()) }

可以看到,atom 方法没有声明生命周期,最终还是调用了 Atom::from,但是里面的值是通过 String::from_str_in 生成的,其中第二个参数 self.allocator 是带生命周期的:

rs
代码解读
复制代码
pub struct AstBuilder<'a> { pub allocator: &'a Allocator, }

Allocator 则是基于 bumpalo 实现的内存分配的工具,Allocator 这个东西好像在实现解析器一般都要用到,可以参考这个教程,对这块还不太了解,先略过了。

Emit File

输出最终的打包文件时,用到了模板引擎 sailfish,模版如下所示:

ejs
代码解读
复制代码
(function(modules) { var installedModules = {}; ... // Load entry module and return exports return __webpack_require__(__webpack_require__.s = "<%- entry_id %>"); }) ({ <% for (key, value) in modules { %> "<%- key %>": (function(module, exports, __webpack_require__) { eval(`<%- value %>`); }), <%}%> });

可以看到,只需要将 entry_idmodules 的内容输出到模版中即可。

运行 cargo run,得到输出结果:

js
代码解读
复制代码
// out/bundle.js (function(modules) { var installedModules = {}; ... // Load entry module and return exports return __webpack_require__(__webpack_require__.s = "./index.js"); }) ({ "./const.js": (function(module, exports, __webpack_require__) { eval(`module.exports = "hello"; `); }), "./index.js": (function(module, exports, __webpack_require__) { eval(`const b = __webpack_require__("./const.js"); console.log(b); `); }), });

使用 Node.js 运行,如果可以正常打印 hello,则表示 MVP 成功完成。

跪求 star 并关注公众号“前端游”。

源文:从零实现 webpack,但 Rust 版 - [2] 实现 MVP 版本

如有侵权请联系站点删除!

技术合作服务热线,欢迎来电咨询!