用 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 之间的交互更加简单。

相关文章
|
1月前
|
前端开发 JavaScript
使用JavaScript实现复杂功能:构建一个自定义的拖拽功能
使用JavaScript实现复杂功能:构建一个自定义的拖拽功能
|
14天前
|
JavaScript 前端开发 持续交付
【专栏】Vue.js和Node.js如何结合构建现代Web应用
【4月更文挑战第27天】本文探讨了Vue.js和Node.js如何结合构建现代Web应用。Vue.js作为轻量级前端框架,以其简洁易懂、组件化开发、双向数据绑定和虚拟DOM等特点受到青睐;而Node.js是高性能后端平台,具备事件驱动、非阻塞I/O、丰富生态系统和跨平台优势。两者结合实现前后端分离,高效通信,并支持热更新、持续集成、跨平台和多端适配,为开发高性能、易维护的Web应用提供强有力的支持。
|
11天前
|
JavaScript 前端开发 API
如何利用JavaScript和Electron构建具有丰富功能的桌面应用
【4月更文挑战第30天】如何利用JavaScript和Electron构建具有丰富功能的桌面应用
5 0
|
11天前
|
缓存 监控 JavaScript
Node.js中构建RESTful API的最佳实践
【4月更文挑战第30天】本文介绍了在Node.js中构建RESTful API的最佳实践:选择合适的框架(如Express、Koa)、设计清晰的API接口(遵循HTTP动词和资源路径)、实现认证授权(JWT、OAuth 2.0)、错误处理、限流缓存、编写文档和测试,以及监控性能优化。这些实践有助于创建健壮、可维护和易用的API。
|
11天前
|
消息中间件 监控 JavaScript
Node.js中的微服务架构:构建与实践
【4月更文挑战第30天】本文探讨了在Node.js中构建微服务的实践,包括定义服务边界、选择框架(如Express、Koa或NestJS)、设计RESTful API、实现服务间通信(HTTP、gRPC、消息队列)、错误处理、服务发现与负载均衡,以及监控和日志记录。微服务架构能提升应用的可伸缩性、灵活性和可维护性。
|
19天前
|
资源调度 JavaScript 搜索推荐
服务端渲染:使用Nuxt.js构建Vue SSR应用
【4月更文挑战第22天】本文介绍了如何使用Nuxt.js创建Vue SSR应用。首先确保安装Node.js和npm/yarn,然后全局安装Create Nuxt App,创建项目并选择配置。Nuxt.js提供清晰的项目结构,如`pages/`存放Vue页面。编写简单SSR页面后,启动开发服务器预览。完成开发,使用Nuxt.js命令部署到静态文件托管服务,实现首屏加载优化和SEO提升。Nuxt.js简化了SSR开发,助力高效构建高性能Vue应用。
|
23天前
|
JavaScript 前端开发 开发者
在JavaScript中,可以利用以下几种输出语句来查看运行结果
【4月更文挑战第18天】在JavaScript中,可以利用以下几种输出语句来查看运行结果
24 1
|
24天前
|
JavaScript 前端开发
用js使鼠标放轮播图上使其停止,移开鼠标轮播图继续运行
用js使鼠标放轮播图上使其停止,移开鼠标轮播图继续运行
9 0
|
1月前
|
JavaScript 前端开发 API
Vue.js:构建高效且灵活的Web应用的利器
Vue.js:构建高效且灵活的Web应用的利器
|
1月前
|
前端开发 JavaScript 数据可视化
使用JavaScript实现复杂功能:动态数据可视化的构建
使用JavaScript实现复杂功能:动态数据可视化的构建