Kun

Kun

IT学徒、技术民工、斜杠青年,机器人爱好者、摄影爱好 PS、PR、LR、达芬奇潜在学习者


共 212 篇文章


  前端AST与Rust在前端工程化中的应用,本文转自知乎https://zhuanlan.zhihu.com/p/436090437

AST相关

前端同学们对于编译原理都存在着复杂的看法,大部分人都觉得自己写业务也用不到这么高深的理论知识,况且编译原理晦涩难懂,并不能提升自己在前端领域内的专业知识。我不觉得这种想法有什么错,况且我之前也是这么认为的。而在前端领域内,和编译原理强相关的框架与工具类库主要有这么几种:

1.以 Babel 为代表,主要做 ECMAScript 的语法支持,比如 ?.?? 对应的 babel-plugin-optional-chainingbabel-plugin-nullish-coalescing-operator,这一类工具还有 ESBuild 、swc 等。类似的,还有 Scss、Less 这一类最终编译到 CSS 的“超集”。这一类工具的特点是转换前的代码与转换产物实际上是同一层级的,它们的目标是得到标准环境能够运行的产物。

2.以 Vue、Svelte 还有刚诞生不久的 Astro 为代表,主要做其他自定义文件到 JavaScript(或其他产物) 的编译转化,如 .vue .svelte .astro 这一类特殊的语法。这一类工具的特点是,转换后的代码可能会有多种产物,如 Vue 的 SFC 最终会构建出 HTML、CSS、JavaScript

3.典型的 DSL 实现,其没有编译产物,而是由独一的编译引擎消费, 如 GraphQL (.graphql)、Prisma (.prisma) 这一类工具库(还有更熟悉一些的,如 HTML、SQL、Lex、XML 等),其不需要被编译为 JavaScript,如 .graphql 文件直接由 GraphQL 各个语言自己实现的 Engine 来消费。

4.语言层面的转换,TypeScript、Flow、CoffeeScript 等,以及使用者不再一定是狭义上前端开发者的语言,如张宏波老师的 ReScript(原 BuckleScript)、Dart 等。

FB 的 jscodeshift,相对于 Babel 的 Visitor API,jscodeshift 提供了命令式 + 链式调用的 API,更符合前端同学的认知模式(因为就像 Lodash、RxJS 这样)

GoGoCode

GoGoCode 是一个基于 AST 的 JavaScript/Typescript/HTML 代码转换工具,但相较于同类,它提供了更符合直觉的 API

  • 一套类 Jquery 的 API 用来查找和处理 AST
  • 一套和正则表达式接近的语法用来匹配和替换代码

javascript代码

const a = 1;
const b = 2;

gogoCode转换

const $ = require('gogocode');
const script = $(source);
// 按照你的意图,用 $_$ 当通配符能匹配任意位置的 AST 节点
const aAssignment = script.find('const a = $_$');
// 获得我们匹配的 AST 节点的 value
const aValue = aAssignment.match?.[0]?.[0]?.value;
// 就像替换字符串一样去替换代码
// 但可以忽略空格、缩进或者换行的影响
script.replace('const b = $_$', `const b = ${aValue}`);
// 把 ast 节点输出成字符串
const outCode = script.generate();

转换后代码

const a = 1;
const b = 1;

相关项目

项目 描述
gogocode-plugin-vue 通过这个 gogocode 插件可以把 vue2 语法的项目转换成 vue3 的
gogocode-cli gogocode 的命令行工具
gogocode-playground 可以在浏览器里尝试 gogocode 转换
gogocode-vscode 在 vscode 中通过此插件用 gogocode 重构你的代码

jscodeshift

安装

npm install -g jscodeshift

使用

const {run: jscodeshift} = require('jscodeshift/src/Runner')
const path = require('node:path');

const transformPath = path.resolve('transform.js')
const paths = ['foo.js', 'bar']
const options = {
  dry: true,
  print: true,
  verbose: 1,
  // ...
}

const res = await jscodeshift(transformPath, paths, options)
console.log(res)
/*
{
  stats: {},
  timeElapsed: '0.001',
  error: 0,
  ok: 0,
  nochange: 0,
  skip: 0
}
*/

ts-morph

import { Project } from "ts-morph";

const s = new Project().createSourceFile("./func.ts", "");

s.addFunction({
  isExported: true,
  name: "factorial",
  returnType: "number",
  parameters: [
    {
      name: "n",
      isReadonly: true,
      type: "number",
    },
  ],
  statements: (writer) => {
    writer.write(`
if (n <=1) {
  return 1;
}

return n * factorial(n - 1);
    `);
  },
}).addStatements([]);

s.saveSync();

console.log(s.getText());

ts-morpher

tern

Tern采用了C/S架构。采用这种架构是由它的目标所决定的,因为Tern是为了给各种代码编辑器提供增强服务,Tern只是提供服务就好了,它制定协议,让代码编辑器根据协议与服务进行通信即可。

Tern提供的主要特征如下:

  • 变量和属性的自动完成
  • 函数参数提示
  • 查询一个表达式的类型
  • 查找定义
  • 重构

javascript语法分析器

安装

npm install -g tern

安装完成后,有个tern命令可以使用,执行该命令就会启动Tern Server

tern命令的实现者没有提供-h或者--help参数来让我们看它的使用方法

tern [option]...

option有下面这些

option 说明
--host HOST 指定监听的主机,默认是127.0.0.1
--port NUMBER 指定监听的端口,默认是62098
--verbose 打印通信协议的具体内容,默认是不打印的
--persistent 默认的,Tern Server超过5分钟没有任何活动,就会自动关闭,如果不想让他自动关闭,使用该参数即可
--ignore-stdin 默认的,Tern Server的标准输入一旦被关闭了,就会自动关闭,如果不想让他自动关闭,使用该参数即可

.tern_project文件是JSON格式。

.tern_project文件Tern Server的配置文件。

.tern_project文件在Tern Server启动的时候进行查找,如果找到,就读取它。

Tern Server查找.tern_project文件的过程如下:

1、在被编辑的.js文件所在的目录中,看是否存在.tern_project文件;如果存在,就读取;如果不存在,则实行第二步;

2、在被编辑的.js文件的父目录中,看是否存在.tern_project文件;如果存在,就读取;如果不存在,则实行第三步;

3、在被编辑的.js文件的父目录的父目录中,看是否存在.tern_project文件;如果存在,就读取;如果不存在,则继续从它的父目录往上找...

4、如果能找到就使用找到的配置,如果没有找到,就使用默认的配置, 默认的配置只支持JavaScript的基本语法,不支持其他的第三方库和Node.js语法。

~/.tern_config文件与.tern_project文件的格式完全一样。

~/.tern_config文件就是在查找.tern_project文件过程中,因为没有找到,而由用户指定的默认配置。

swc

swc 是基于 Rust 开发的一系列编译、打包、压缩等工具,并且被广泛应用于更多更上层的 JS 基建,大大推动了 Rust 在 JS 基建的影响力,所以要第一个介绍。

swc 提供了一系列原子能力,涵盖构建与运行时:

@swc/cli

@swc/cli 可以同时构建 js 与 ts 文件:

const a = 1

npm i -D @swc/cli
npx swc ./main.ts

# output:
# Successfully compiled 1 file with swc.
# var a = 1;

具体功能与 babel 类似,都可以让浏览器支持先进语法或者 ts,只是 @swc/cli 比 babel 快了至少 20 倍。可以通过 .swcrc 文件做自定义配置

@swc/core

你可以利用 @swc/core 制作更上层的构建工具,所以它是 @swc/cli 的开发者调用版本。基本 API 来自官网开发者文档:

const swc = require("@swc/core");

swc
  .transform("source code", {
    // Some options cannot be specified in .swcrc
    filename: "input.js",
    sourceMaps: true,
    // Input files are treated as module by default.
    isModule: false,

    // All options below can be configured via .swcrc
    jsc: {
      parser: {
        syntax: "ecmascript",
      },
      transform: {},
    },
  })
  .then((output) => {
    output.code; // transformed code
    output.map; // source map (in string)
  });

其实就是把 cli 调用改成了 node 调用。

@swc/jest

@swc/jest 提供了 Rust 版本的 jest 实现,让 jest 跑得更快。使用方式也很简单,

npm i @swc/jest

然后在 jest.config.js 配置文件中,将 ts 文件 compile 指向 @swc/jest 即可

module.exports = {
  transform: {
    "^.+\\.(t|j)sx?$": ["@swc/jest"],
  },
};

swc-loader

swc-loader 是针对 webpack 的 loader 插件,代替 babel-loader

module: {
  rules: [
    {
      test: /\.m?js$/,
      exclude: /(node_modules)/,
      use: {
        // `.swcrc` can be used to configure swc
        loader: "swc-loader"
      }
    }
  ];
}

@swc/wasm-web

@swc/wasm-web 可以在浏览器运行时调用 wsm 版的 swc,以得到更好的性能。

import { useEffect, useState } from "react";
import initSwc, { transformSync } from "@swc/wasm-web";

export default function App() {
  const [initialized, setInitialized] = useState(false);

  useEffect(() => {
    async function importAndRunSwcOnMount() {
      await initSwc();
      setInitialized(true);
    }
    importAndRunSwcOnMount();
  }, []);

  function compile() {
    if (!initialized) {
      return;
    }
    const result = transformSync(`console.log('hello')`, {});
    console.log(result);
  }

  return (
    <div className="App">
      <button onClick={compile}>Compile</button>
    </div>
  );
}

swcpack

增强了多文件 bundle 成一个文件的功能,基本可以认为是 swc 版本的 webpack,当然性能也会比 swc-loader 方案有进一步提升。

截至目前,该功能还在测试阶段,只要安装了 @swc/cli 就可使用,通过创建 spack.config.js 后执行 npx spack 即可运行,和 webpack 的使用方式一样。

Deno

Deno 的 linter、code formatter、文档生成器采用 swc 构建,因此也算属于 Rust 阵营。

Deno 是一种新的 js/ts 运行时,所以我们总喜欢与 node 进行类比。quickjs 也一样,这三个都是一种对 js 语言的运行器,作为开发者,需求永远是更好的性能、兼容性与生态,三者几乎缺一不可,所以当下虽然不能完全代替 Nodejs,但作为高性能替代方案是很香的,可以基于他们做一些跨端跨平台的解析器,比如 kraken 就是基于 quickjs + flutter 实现的一种高性能 web 渲染引擎,是 web 浏览器的替代方案,作为一种跨端方案。

esbuild

esbuild 是较早被广泛使用的新一代 JS 基建,是 JS 打包与压缩工具。虽然采用 Go 编写,但性能与 Rust 不相上下,可以与 Rust 风潮放在一起看。

esbuild 目前有两个功能:编译和压缩,理论上分别可代替 babel 与 terser。

编译功能的基本用法:

require('esbuild').transformSync('let x: number = 1', {
  loader: 'ts',
})

// 'let x = 1;\n'

压缩功能的基本用法:

require('esbuild').transformSync('fn = obj => { return obj.x }', {
  minify: true,
})

// 'fn=n=>n.x;\n'

压缩功能比较稳定,适合用在生产环境,而编译功能要考虑兼容 webpack 的地方太多,在成熟稳定后才考虑能在生产环境使用,目前其实已经有不少新项目已经在生产环境使用 esbuild 的编译功能了。

编译功能与 @swc 类似,但因为 Rust 支持编译到 wsm,所以 @swc 提供了 web 运行时编译能力,而 esbuild 目前还没有看到这种特性。

Rome

Rome 是 Babel 作者做的基于 Nodejs 的前端基建全家桶,包含但不限于 Babel, ESLint, webpack, Prettier, Jest。目前 计划使用 Rust 重构,虽然还没有实现,但我们姑且可以把 Rome 当作 Rust 的一员。

rome 是个全家桶 API,所以你只需要 yarn add rome 就完成了所有环境准备工作。

  • rome bundle 打包项目。
  • rome compile 编译单个文件。
  • rome develop 调试项目。
  • rome parse 解析文件抽象语法树。
  • rome analyzeDependencies 分析依赖。

Rome 还将文件格式化与 Lint 合并为了 rome check 命令,并提供了友好 UI 终端提示

其实我并不太看好 Rome,因为它负担太重了,测试、编译、Lint、格式化、压缩、打包的琐碎事情太多,把每一块交给社区可能会做得更好,这不现在还在重构中,牵一发而动全身。

NAPI-RS

NAPI-RS 提供了高性能的 Rust 到 Node 的衔接层,可以将 Rust 代码编译后成为 Node 可调用文件。下面是官网的例子:

#[js_function(1)]
fn fibonacci(ctx: CallContext) -> Result<JsNumber> {
  let n = ctx.get::<JsNumber>(0)?.try_into()?;
  ctx.env.create_int64(fibonacci_native(n))
}

上面写了一个斐波那契数列函数,直接调用了 fibonacci_native 函数实现。为了让这个方法被 Node 调用,首先安装 CLI:npm i @napi-rs/cli

由于环境比较麻烦,因此需要利用这个脚手架初始化一个工作台,我们在里面写 Rust,然后再利用固定的脚本发布 npm 包。执行 napi new 创建一个项目,我们发现入口文件肯定是个 js,毕竟要被 node 引用,大概长这样(我创建了一个 myLib 包):

const { loadBinding } = require('@node-rs/helper')

/**
 * __dirname means load native addon from current dir
 * 'myLib' is the name of native addon
 * the second arguments was decided by `napi.name` field in `package.json`
 * the third arguments was decided by `name` field in `package.json`
 * `loadBinding` helper will load `myLib.[PLATFORM].node` from `__dirname` first
 * If failed to load addon, it will fallback to load from `myLib-[PLATFORM]`
 */
module.exports = loadBinding(__dirname, 'myLib', 'myLib')

所以 loadBinding 才是入口,同时项目文件夹下存在三个系统环境包,分别供不同系统环境调用:

  • @cool/core-darwin-x64 macOS x64 平台。
  • @cool/core-win32-x64 Windows x64 平台。
  • @cool/core-linux-arm64-gnu Linux aarch64 平台。

@node-rs/helper 这个包的作用是引导 node 执行预编译的二进制文件,loadBinding 函数会尝试加载当前平台识别的二进制包。

src/lib.rs 的代码改成上面斐波那契数列的代码后,执行 npm run build 编译。注意在编译前需要安装 rust 开发环境,只要一行脚本即可安装,具体看 rustup.rs。然后把当前项目整体当作 node 包发布即可。

发布后,就可以在 node 代码中引用啦:

import { fibonacci } from 'myLib'

function hello() {
  let result = fibonacci(10000)
  console.log(result)
  return result
}

NAPI-RS 作为 Rust 与 Node 的桥梁,很好的解决了 Rust 渐进式替换现有 JS 工具链的问题。

Rust + WebAssembly

Rust + WebAssembly 说明 Rust 具备编译到 wsm 的能力,虽然编译后代码性能会变得稍慢,但还是比 js 快很多,同时由于 wsm 的可移植性,让 Rust 也变得可移植了。

其实 Rust 支持编译到 WebAssembly 也不奇怪,因为本来 WebAssembly 的定位之一就是作为其他语言的目标编译产物,然后它本身支持跨平台,这样它就很好的完成了传播的使命。

WebAssembly 是一个基于栈的虚拟机 (stack machine),所以跨平台能力一流。

想要将 Rust 编译为 wsm,除了安装 Rust 开发环境外,还要安装 wasm-pack

安装后编译只需执行 wasm-pack build 即可。更多用法可以查看 API 文档

dprint

dprint 是用 rust 编写的 js/ts 格式化工具,并提供了 dprint-node 版本,可以直接作为 node 包,通过 npm 安装使用,从 源码 可以看到,使用 NAPI-RS 实现。

dprint-node 可以直接在 Node 中使用:

const dprint = require('dprint-node');
dprint.format(filePath, code, options);

Parcel

Parcel 严格来说算是上一代 JS 基建,它出现在 Webpack 之后,Rust 风潮之前。不过由于它已经采用 SWC 重写,所以姑且算是跟上了时髦。

总结

前端全家桶已经有了一整套 Rust 实现,只是对于存量项目的编译准确性需要大量验证,我们还需要时间等待这些库的成熟度。

但毫无疑问的是,Rust 语言对 JS 基建支持已经较为完备了,剩下的只是工具层逻辑覆盖率的问题,都可以随时间而解决。而用 Rust 语言重写后的逻辑带来的巨幅性能提升将为社区注入巨大活力,就像原文说的,前端社区可以为了巨大性能提升而引入 Rust 语言,即便这可能导致为社区贡献门槛的提高。

如果你觉得我的文章对你有帮助的话,希望可以推荐和交流一下。欢迎關注和 Star 本博客或者关注我的 Github