再开新坑,eac,小而美的打包器

104 阅读4分钟

halo 大家好,俺是 132,今天给大家带来的是俺课余时间开的一个新坑,暂定名称为 pak,是一个全新的 JavaScript 打包器

前情提要

今年已经回归学生生活,有一些时间用来研究和尝试,所以是时候开一个坑了

如果说 fre 是俺大学的代表作,那研究生到底写个啥好呢?其实也没啥能写的,最终决定写一个rust的打包器,但和 rolldown 之类的不同,俺追求的不是极致 tree shaking,而是小而美

之前研究了很久,怎样在 rust 的世界里,用 1000 行实现一个 JavaScript parser,最终被我找到了方法,听我一一道来

最简单的打包器算法

打包器算法有很多,但总体上分为两类,一类是将 esm 打包成 cjs,一类是打包成单闭包函数(通常配合 scope-hoist)

parcel 作者写了一篇文章,里面也提到了这两种模式,目前 parcel 是不喜欢第二种的,也开始认为 scope hoist 的意义很小

devongovett.me/blog/scope-…

为了简化整个流程,俺选择了后者,因为前者还需要模拟 cjs 的运行才行

但,和 rollup 不同的时候,不需要 magic-string,tree shaking 也不需要做,而是进行简单的拼接和重命名,最终 tree shaking 由 minifier 做

//input

import { hello } from './hello.js'

function world() {
  return 'World'
}

console.log(hello)
console.log(world())

//output

(function (global) {
const P$hello_js$hello = 'hello';

const P$index_js$world = () => console.log("world");

console.log(P$hello_js$hello);
console.log(P$index_js$world());
})(typeof global !== 'undefined' ? global : typeof window !== 'undefined' ? window : this);

这可以说是世界上最简单的打包算法了,基本上可以算作是 rollup 的终极简化版

最小的 JavaScript parser

过去我一直在寻找 1000 行的 JavaScript 思路,终于还是被我找到了

传统的 parser 是大体上分为,lexer(token)+parser(ast)的过程,比如 swc 和 oxc,都是传统编译器的代表,它们通过传统的拆分 token,然后生成一棵 estree 兼容的树

这好吗,这不好,这样的工作量也不低

很久以前,我发现了一种新的 parser 思路,也就是基于 combinator 的方法

github.com/yisar/pak/b…

如上,这是之前通过组合子解析 js 中 jsx 的一段代码,最终生成一个嵌套数组

通俗来讲,就是不生成 ast 树,而是生成一个“嵌套数组”

从结构层面来讲,其实很容易理解

const a = 0;

如果是传统的 estree,可能是这样的

{
      "type": "VariableDeclaration",
      "start": 0,
      "end": 11,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 6,
          "end": 11,
          "id": {
            "type": "Identifier",
            "start": 6,
            "end": 7,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 10,
            "end": 11,
            "value": 0,
            "raw": "0"
          }
        }
      ],
      "kind": "const"
    }

但是基于组合子的 parser 则会生成一个数组

[const, [a = 0]]

这样一来,所有的事情都会变简单,rust 的 vec 结构也有类型,其实约等于也是一棵 ast 树了

如此,就可以做到 1000 行以内的 JavaScript parser 了

  • 优点是代码量少,好维护,而且 vec 结构在 rust 中也比较容易遍历和修改
  • 缺点是不符合 estree 标准,而且没有位置信息(有时也很重要)

内置 fre-compiler

现在的前端框架,基本上都转编译了,solidjs,react-compiler 之类的

但我仍旧发现了一个兼容 react 语法的新的编译思路,比如

export default () => {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

会编译为


// 卸载逻辑(作为marker)
const unmount = (ctx) => {
  removeEventListener(ctx.el, "click", ctx.clickHandler);
};

export default () => {
  const [count, setCount] = useState(0);
  
  return (ctx) => {
    // DOM缓存与复用逻辑
    if (ctx.marker !== unmount) {
      // 首次渲染或标记不匹配:清理旧元素并创建新元素
      ctx.marker && ctx.marker(ctx);
      ctx.el = createElement("button");
      ctx.clickHandler = () => setCount(c => c + 1);
      addEventListener(ctx.el, "click", ctx.clickHandler);
      ctx.marker = unmount;
    }
    
    // 更新
    ctx.el.textContent = count;
    
    return ctx.el;
  };
};

这种编译思路本质上是通过 marker 的缓存和比对,实现 dom 的创建更新和销毁

  • 优点是可以完全兼容 react 和 fre 的语法,不需要修改语义
  • 缺点是,组件仍然需要重新渲染

未来这会作为 fre-compiler 的主要渲染思路,用来将 fre 项目去 vdom 化

同时也会内知道 pak 打包器中,这也是俺费尽心思写一个打包器的终极原因

总结

总而言之,就是继 fre 以来,俺终于又开了一个大坑,为了满足 fre-compiler 的夙愿,专门写了一个小而美的打包器

github.com/yisar/pak

当然,不得不吐槽一下 rust,俺个人其实很不适应写 rust 代码,基本上全程和编译器斗智斗勇

但是没办法,只能说 rust 真的很适合写编译器,趁着这个项目,研究更多的编译技巧和 rust 的编码技巧吧

最后放一个俺的微信号:shibubyayuri

大家欢迎加俺交流哈