用 Rust 构建你自己的 JavaScript 运行时(2)

简介: 这是用 Rust 构建自定义 JavaScript 运行时系列文章的第二篇,我们在前一篇文章的基础上在实现了 fetch API,读取一个文件路径作为命令行参数,支持 TypeScript 和 TS。

theme: devui-blue

原文链接: https://deno.com/blog/roll-your-own-javascript-runtime-pt2

原文作者:Bartek Iwańczuk

为了大家能够读的更爽,本文全部由本人手工翻译而成,没有任何机翻内容,如果大家有收获的话,还请多多点赞收藏支持~

本文是《用 Rust 构建自定义 JavaScript 运行时》系列文章的第二篇,第一篇看这里。(如果你还没看过第一篇,建议先读完了第一篇再过来哦)。
构建一个自定义的 JavaScript 运行时有很多用途,例如搭建一个用 Rust 作为后端的可交互 Web 应用,通过插件系统扩展你的平台,或者给“我的世界”游戏编写一个插件。

在这篇文章中,我们在前一篇文章的基础上做这三件事:

  • 实现 fetch
  • 读取一个文件路径作为命令行参数
  • 支持 TypeScript 和 TSX

*你也可以 查看视频教程 或 [阅读源代码]

准备工作

如果你有跟着第一篇文章一路敲下来,你的项目中应该包含了三个文件:

  • example.js:用于执行并测试我们的运行时的 JS 文件。
  • main.rs:创建了一个 JsRuntime 实例的异步函数,负责 JS 代码的执行
  • runtime.js:定义并提供了与 main.rs 中的 JsRuntime 进行交互的运行时接口。

接下来,让我们在 runjs 运行时中实现一个 HTTP 函数 fetch

实现 fetch

在我们的 runtime.js 文件中,添加一个新函数 fetch 在全局对象 runjs 中。

// runtime.js

((globalThis) => {
  // ...

  globalThis.runjs = {
    readFile: (path) => {
      return ops.op_read_file(path);
    },
    writeFile: (path, contents) => {
      return ops.op_write_file(path, contents);
    },
    removeFile: (path) => {
      return ops.op_remove_file(path);
    },
    // 添加 fetch
    fetch: (url) => {
      return ops.op_fetch(url);
    },
  };
})(globalThis);

现在,我们需要在 main.rs 中定义 op_fetch。它是一个异步函数,接受一个 String 参数,返回一个 String 或者一个 error。

在这个函数中,我们将使用 reqwest 包,它是一个方便而强大的 HTTP 库,我们只会用到它的 get 函数。

// main.rs

// …

#[op]
async fn op_read_file(path: String) -> Result<String, AnyError> {
  let contents = tokio::fs::read_to_string(path).await?;
  Ok(contents)
}

#[op]
async fn op_write_file(path: String, contents: String) -> Result<(), AnyError> {
  tokio::fs::write(path, contents).await?;
  Ok(())
}

// 添加 op_fetch 函数
#[op]
async fn op_fetch(url: String) -> Result<String, AnyError> {
  let body = reqwest::get(url).await?.text().await?;
  Ok(body)
}
​
// …

为了能够使用 reqwest,我们需要通过命令行把它添加到项目中:

$ cargo add reqwest

接下来,我们在 run_js 中注册 op_fetch 函数:

// main.rs

// …

async fn run_js(file_path: &str) -> Result<(), AnyError> {
  let main_module = deno_core::resolve_path(file_path)?;
  let runjs_extension = Extension::builder("runjs")
    .ops(vec![
      op_read_file::decl(),
      op_write_file::decl(),
      op_remove_file::decl(),
      op_fetch::decl(),  // 添加这一行
    ])
    .build();
// …

我们更新 example.js 来测试我们新添加的 fetch 函数:

console.log("Hello", "runjs!");
content = await runjs.fetch(
  "https://deno.land/std@0.177.0/examples/welcome.ts",
);
console.log("Content from fetch", content);

通过下面的命令运行它:

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 2m 14s
     Running `target/debug/runjs`
[out]: "Hello" "runjs!"
[out]: "Content from fetch" "// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.\n\n/** Welcome to Deno!\n *\n * @module\n */\n\nconsole.log("Welcome to Deno!");\n"

成功了!我们用了不到 10 行代码就把自定义的 fetch 函数添加到了 runjs 运行时中。

读取命令行参数

到目前为止,我们都是把加载和运行的文件路径写死的。我们只需要运行 cargo run 就可以执行 example.js 中的内容。

让我们来更新 runjs ,使其能够读取命令行的参数,将其中第一个参数作为要运行的代码的文件路径。我们可以在 main.rsmain() 函数中进行更改:

// main.rs

// ...

fn main() {
  let args: Vec<String> = std::env::args().collect();

  if args.is_empty() {
      eprintln!("Usage: runjs <file>");
      std::process::exit(1);
  }
  
  let file_path = &args[1];

  let runtime = tokio::runtime::Builder::new_current_thread()
    .enable_all()
    .build()
    .unwrap();
   if let Err(error) = runtime.block_on(run_js(file_path)) {
     eprintln!("error: {error}");
   }
}

我们运行 cargo run example.js 试试看:

$ cargo run example.js
    Finished dev [unoptimized + debuginfo] target(s) in 6.99s
     Running `target/debug/runjs example.js`
[out]: "Hello" "runjs!"
[out]: "Content from fetch" "// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.\n\n/** Welcome to Deno!\n *\n * @module\n */\n\nconsole.log("Welcome to Deno!");\n"

命令成功运行了!现在我们可以把文件名作为命令行参数传递给我们的运行时。

支持 TypeScript

如果我们想支持 TypeScript 和 TSX,应该怎么做呢?

首先,我们要想办法把 TypeScript 编译成 JavaScript。

我们把 example.js 重命名为 example.ts ,添加一些简单的 TS 代码:

console.log("Hello", "runjs!");

interface Foo {
  bar: string;
  fizz: number;
}
let content: string;
content = await runjs.fetch(
  "https://deno.land/std@0.177.0/examples/welcome.ts"
);
console.log("Content from fetch", content);

接下来,我们需要更新 main.rs 中的模块加载器。目前我们使用的[模块加载器]是 deno_core::FsModuleLoader,它提供了从本地文件系统中加载模块的能力。然而,它只能加载 JS 文件。

// main.rs
// …

  let mut js_runtime = deno_core::JsRuntime::new(deno_core::RuntimeOptions {
    module_loader: Some(Rc::new(deno_core::FsModuleLoader)),
    extensions: vec![runjs_extension],
    ..Default::default()

// …

所以我们需要自己实现一个新的 TsModuleLoader,它可以根据文件扩展名来决定如何编译文件。这个新的模块加载器需要实现 deno_core::ModuleLoader 这个 trait,这要求我们实现 resolveload 函数。

resolve 函数非常简单,我们只需要调用 deno_core::resolve_import

// main.rs

struct TsModuleLoader;

impl deno_core::ModuleLoader for TsModuleLoader {
  fn resolve(
    &self,
    specifier: &str,
    referrer: &str,
    _kind: deno_core::ResolutionKind,
  ) -> Result<deno_core::ModuleSpecifier, deno_core::error::AnyError> {
    deno_core::resolve_import(specifier, referrer).map_err(|e| e.into())
  }
}

接下来,我们需要实现 load 函数。这比较复杂,因为将 TypeScript 编译为 JavaScript 并不简单 —— 你需要能够解析 TS 文件,创建 AST(抽象语法树),然后把 JavaScript 不能理解的类型相关代码去除,最后再把 AST 转化为 JS 代码。

我们不打算自己实现这些功能(这可能会花上数周的时间),而是直接使用 Deno 生态系统中现成的解决方案:deno_ast

让我们通过命令行把它添加到依赖中:

$ cargo add deno_ast

Cargo.toml 中,我们需要添加 deno_asttranspile 能力:

// …
[dependencies]
deno_ast = { version = "0.24.0", features = ["transpiling"] }
// …

接下来,我们添加四条 use 声明到 main.rs 的顶部,这些会在 load 函数中用到:

// main.rs

use deno_ast::MediaType;
use deno_ast::ParseParams;
use deno_ast::SourceTextInfo;
use deno_core::futures::FutureExt;

// …

现在我们可以来着手实现 load 函数了:

// main.rs

struct TsModuleLoader;

impl deno_core::ModuleLoader for TsModuleLoader {
  // fn resolve() ...

  fn load(
    &self,
    module_specifier: &deno_core::ModuleSpecifier,
    _maybe_referrer: Option<deno_core::ModuleSpecifier>,
    _is_dyn_import: bool,
  ) -> std::pin::Pin<Box<deno_core::ModuleSourceFuture>> {
    let module_specifier = module_specifier.clone();
    async move {
      let path = module_specifier.to_file_path().unwrap();

      // 根据文件扩展名解析 MediaType,据此判断是否需要对文件进行编译
      let media_type = MediaType::from(&path);
      let (module_type, should_transpile) = match MediaType::from(&path) {
        MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs => {
          (deno_core::ModuleType::JavaScript, false)
        }
        MediaType::Jsx => (deno_core::ModuleType::JavaScript, true),
        MediaType::TypeScript
        | MediaType::Mts
        | MediaType::Cts
        | MediaType::Dts
        | MediaType::Dmts
        | MediaType::Dcts
        | MediaType::Tsx => (deno_core::ModuleType::JavaScript, true),
        MediaType::Json => (deno_core::ModuleType::Json, false),
        _ => panic!("Unknown extension {:?}", path.extension()),
      };

      // 读取文件,在需要的情况下对文件进行转译
      let code = std::fs::read_to_string(&path)?;
      let code = if should_transpile {
        let parsed = deno_ast::parse_module(ParseParams {
          specifier: module_specifier.to_string(),
          text_info: SourceTextInfo::from_string(code),
          media_type,
          capture_tokens: false,
          scope_analysis: false,
          maybe_syntax: None,
        })?;
        parsed.transpile(&Default::default())?.text
      } else {
        code
      };

      // 加载模块并返回
      let module = deno_core::ModuleSource {
        code: code.into_bytes().into_boxed_slice(),
        module_type,
        module_url_specified: module_specifier.to_string(),
        module_url_found: module_specifier.to_string(),
      };
      Ok(module)
    }
    .boxed_local()
  }
}

我们来拆解一下这段代码。load 函数接受一个文件路径作为参数,返回一个 JavaScipt 模块。这个文件路径可以指向一个 JavaScript 文件或 TypeScript 文件,只要它能够被编译成一个 JavaScript 模块就行。

首先我们获取代码文件的真实路径,确定其 MediaType 以及是否需要进行转译。随后,我们把文件内容读取至一个字符串中,并在有必要的情况下对其进行转译。最后,这份代码文件会被转化为一个 module 并返回。

接下来,我们需要在创建 JsRUntime 的地方把原本的 FsModuleLoader 替换为新创建的 TsModuleLoader

// …

  let mut js_runtime = deno_core::JsRuntime::new(deno_core::RuntimeOptions {
//  module_loader: Some(Rc::new(deno_core::FsModuleLoader)),
    module_loader: Some(Rc::new(TsModuleLoader)),
    extensions: vec![runjs_extension],
    ..Default::default()

// …

这样我们就可以让 TypeScript 文件顺利编译了!

让我们运行 cargo run example.ts 来看看效果:

cargo run example.ts
    Finished dev [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/runjs example.ts`
[out]: "Hello" "runjs!"
[out]: "Content from fetch" "// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.\n\n/** Welcome to Deno!\n *\n * @module\n */\n\nconsole.log("Welcome to Deno!");\n"

通过 133 行 Rust 代码,我们就能够实现 runjs 运行时对 TypeScript,TSX 的支持,以及其他丰富的功能和 API。

下一步?

将 JavaScript 和 TypeScript 嵌入 Rust 是搭建高性能富交互应用的一种很好的方法。不管是用于作为扩展你的平台功能的插件系统,或是构建高性能的专用运行时,Deno 都能让 JavaScript、TypeScript 和 Rust 之间的交互更加简单。

相关文章
|
18天前
|
JavaScript 中间件 关系型数据库
构建高效的后端服务:Node.js 与 Express 的实践指南
在后端开发领域,Node.js 与 Express 的组合因其轻量级和高效性而广受欢迎。本文将深入探讨如何利用这一组合构建高性能的后端服务。我们将从 Node.js 的事件驱动和非阻塞 I/O 模型出发,解释其如何优化网络请求处理。接着,通过 Express 框架的简洁 API,展示如何快速搭建 RESTful API。文章还将涉及中间件的使用,以及如何结合 MySQL 数据库进行数据操作。最后,我们将讨论性能优化技巧,包括异步编程模式和缓存策略,以确保服务的稳定性和扩展性。
|
8天前
|
JSON JavaScript API
深入浅出Node.js:从零开始构建RESTful API
【10月更文挑战第39天】 在数字化时代的浪潮中,API(应用程序编程接口)已成为连接不同软件应用的桥梁。本文将带领读者从零基础出发,逐步深入Node.js的世界,最终实现一个功能完备的RESTful API。通过实践,我们将探索如何利用Node.js的异步特性和强大的生态系统来构建高效、可扩展的服务。准备好迎接代码和概念的碰撞,一起解锁后端开发的新篇章。
|
14天前
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
|
22天前
|
资源调度 前端开发 数据可视化
构建高效的数据可视化仪表板:D3.js与React的融合之道
【10月更文挑战第25天】在数据驱动的时代,将复杂的数据集转换为直观、互动式的可视化表示已成为一项至关重要的技能。本文深入探讨了如何结合D3.js的强大可视化功能和React框架的响应式特性来构建高效、动态的数据可视化仪表板。文章首先介绍了D3.js和React的基础知识,然后通过一个实际的项目案例,详细阐述了如何将两者结合使用,并提供了实用的代码示例。无论你是数据科学家、前端开发者还是可视化爱好者,这篇文章都将为你提供宝贵的洞见和实用技能。
44 5
|
20天前
|
Web App开发 JavaScript 前端开发
探索Deno:新一代JavaScript/TypeScript运行时环境
【10月更文挑战第25天】Deno 是一个新兴的 JavaScript/TypeScript 运行时环境,由 Node.js 创始人 Ryan Dahl 发起。本文介绍了 Deno 的核心特性,如安全性、现代化、性能和 TypeScript 支持,以及开发技巧和实用工具。Deno 通过解决 Node.js 的设计问题,提供了更好的开发体验,未来有望进一步集成 WebAssembly,拓展其生态系统。
|
11天前
|
JavaScript 前端开发 NoSQL
深入浅出:使用Node.js构建RESTful API
【10月更文挑战第35天】在数字时代的浪潮中,后端技术如同海洋中稳固的灯塔,为前端应用提供数据和逻辑支撑。本文旨在通过浅显易懂的方式,带领读者了解如何利用Node.js这一强大的后端平台,搭建一个高效、可靠的RESTful API。我们将从基础概念入手,逐步深入到代码实践,最终实现一个简单的API示例。这不仅是对技术的探索,也是对知识传递方式的一次创新尝试。让我们一起启航,探索Node.js的奥秘,解锁后端开发的无限可能。
|
14天前
|
Web App开发 JavaScript 前端开发
构建高效后端服务:Node.js与Express框架的实践
【10月更文挑战第33天】在数字化时代的浪潮中,后端服务的效率和可靠性成为企业竞争的关键。本文将深入探讨如何利用Node.js和Express框架构建高效且易于维护的后端服务。通过实践案例和代码示例,我们将揭示这一组合如何简化开发流程、优化性能,并提升用户体验。无论你是初学者还是有经验的开发者,这篇文章都将为你提供宝贵的见解和实用技巧。
|
15天前
|
Web App开发 JavaScript 中间件
构建高效后端服务:Node.js与Express框架的融合之道
【10月更文挑战第31天】在追求快速、灵活和高效的后端开发领域,Node.js与Express框架的结合如同咖啡遇见了奶油——完美融合。本文将带你探索这一组合如何让后端服务搭建变得既轻松又充满乐趣,同时确保你的应用能够以光速运行。
24 0
|
4月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的客户关系管理系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的客户关系管理系统附带文章源码部署视频讲解等
97 2
|
4月前
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的小区物流配送系统附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的小区物流配送系统附带文章源码部署视频讲解等
123 4
下一篇
无影云桌面