Deno的基础教程

331 阅读9分钟

我最近有机会玩弄了Deno。我所说的 "玩弄 "是指把它剖析成小块,看看香肠是怎么做出来的。所以,我的观点不是从一个想用它来创建和运行应用程序的用户角度出发,而是从一个对JavaScript运行时、无服务器和Rust有巨大兴趣的用户角度出发。

让我说,我学到了很多东西因为我把学到的东西都写下来了,所以我想和你分享我的学习成果。免责声明:可能有一些东西是完全错误的。这主要是我在浏览Deno资源库和自己使用Deno crates时写下的。如果我做错了什么,请告诉我。

另外。事情可能会发生变化!你在这里看到的或多或少都是时间上的一个快照。

一个现代的JavaScript运行时间#

Deno将自己推销为一个现代的JavaScript和TypeScript的运行时。就像Node.js或浏览器一样,它的主要任务是执行JavaScript。你可以编写TypeScript,并将Deno指向你的TypeScript文件,但它们会通过SWC进行预编译

就像Node或Chrome一样,Deno建立在谷歌的V8引擎上。Deno团队在为V8创建美妙的Rust绑定方面做了出色的工作,使得安装和使用V8变得非常简单。适用于各种架构的预编译V8图像让你可以简单地在你的Cargo.toml 文件中添加一行。

由于Deno也建立在V8的基础上,所以Deno和Node.js之间有很多相似之处。Joyee Chung在去年的NodeConf远程会议上做了一个关于V8内部的精彩演讲。在这次演讲中,她解释了Node.js的启动方式。我使用这个我从Joyee的演讲中重新制作的图形,因为Node.js和Deno的过程非常相似。但Joyee比我更有权威。

The Node.js Bootstrap process

  1. Node.js的过程开始了。这就启动了V8平台。V8平台是依赖于平台的绑定,所以你可以在所有不同的操作系统上运行V8。根据我的经验,初始化进程实际上是可能占用相当多时间的部分。
  2. 之后,Node会创建一个新的V8隔离区。V8隔离区是V8运行时的一个独立副本,包括堆管理器、垃圾收集器等。这在一个单线程上运行。这两个步骤都发生在本地土地上。
  3. 现在我们进入JavaScript的土地。我们初始化一个新的V8上下文。一个V8上下文包括全局对象和JavaScript内置。这些东西构成了语言,而不是具体的运行时间。到此为止,浏览器、Node.js和Deno几乎都是一样的。
  4. 在Node.js中,运行时独立的状态,如Node.jsprimordials被初始化。这意味着所有的JavaScript内置插件都被克隆并冻结,以用于运行时独立状态。因此,如果用户使用Object原型或类似的脾气,这将不会影响Node.js的功能
  5. 我们启动事件循环(Deno中的Tokio,Node中的libuv)并启动V8检查器
  6. 最后,Node初始化运行时的依赖状态。这是与你所使用的运行时有关的一切。这意味着Node.js中的processrequire 等,Deno中的fetch ,各地的console
  7. 加载主脚本并启动老的循环!

让我们看一下代码。

Rusty V8#

Rusty V8包含,嗯,Rust与V8的绑定。其中一个好处是,你不需要每次都编译V8,而是可以使用一个准备好的镜像,因为Rusty V8的build.rs文件有一些好处。一个文件,在你安装/构建crate(一个包)与你的应用一起的时候就会执行。

来自Deno团队的每个crate都包括很多非常简洁易读的例子,这些例子摆脱了你在运行像Deno这样的东西时所需要的所有额外内容。例如,hello_world.rs 显示了V8的一些最基本的使用方法。

// Rust!
use rusty_v8 as v8;

fn main() {
  // Initialize V8.
  let platform = v8::new_default_platform(0, false).make_shared();
  v8::V8::initialize_platform(platform);
  v8::V8::initialize();

  {
    // Create a new Isolate and make it the current one.
    let isolate = &mut v8::Isolate::new(v8::CreateParams::default());

    // Create a stack-allocated handle scope.
    let handle_scope = &mut v8::HandleScope::new(isolate);

    // Create a new context.
    let context = v8::Context::new(handle_scope);

    // Enter the context for compiling and running the hello world script.
    let scope = &mut v8::ContextScope::new(handle_scope, context);

    // Create a string containing the JavaScript source code.
    let code = v8::String::new(scope, "'Hello' + ' World!'").unwrap();

    // Compile the source code.
    let script = v8::Script::compile(scope, code, None).unwrap();
    // Run the script to get the result.
    let result = script.run(scope).unwrap();

    // Convert the result to a string and print it.
    let result = result.to_string(scope).unwrap();
    println!("{}", result.to_rust_string_lossy(scope));

    // ...
  }

  unsafe {
    v8::V8::dispose();
  }
  v8::V8::shutdown_platform();
}

这几行做了所有与V8有关的事情:初始化平台,创建一个隔离区,创建一个上下文,并加载一些基本的JavaScript。有几句话要说。

  1. 每个平台可以有一个以上的隔离器。想想看一个浏览器。启动浏览器就是初始化平台。打开一个新的标签,就会创建一个新的隔离器+上下文。
  2. 如果你认为无服务器平台,Cloudflare工人或Deno部署的工作方式非常类似。他们的工作者在一个V8平台中运行,但每次调用时,你都可以启动一个新的隔离区。有了所有的安全保证。
  3. 隔离区有一个全局对象和一个上下文,但它缺乏任何你在使用Node.js、Deno、浏览器时熟悉的东西。在这个例子中,我们只是创建了一个新的JavaScript字符串,我们试图从V8中获取。没有办法console.log 。没有办法调用任何不属于该语言的API。

启动Deno核心#

如果我们看一下实际的JsRuntime ,我们会发现Deno本身使用的V8绑定有点不同(简略)。

// Rust!
pub fn new(mut options: RuntimeOptions) -> Self {
    // Initialize the V8 platform once
    let v8_platform = options.v8_platform.take();

    static DENO_INIT: Once = Once::new();
    DENO_INIT.call_once(move || v8_init(v8_platform));

    let global_context;

    // Init the Isolate + Context

    let (mut isolate, maybe_snapshot_creator) = if options.will_snapshot {
        // init code for an isolate that will snapshot
        // snip!
        (isolate, Some(creator))
    } else {
        // the other branch. Create a new isolate that 
        // might load a snapshot

        // snip!

        let isolate = v8::Isolate::new(params);
        let mut isolate = JsRuntime::setup_isolate(isolate);
        {
            let scope = &mut v8::HandleScope::new(&mut isolate);
            let context = if snapshot_loaded {
                v8::Context::new(scope)
            } else {
                // If no snapshot is provided, we 
                // initialize the context with empty
                // main source code and source maps.
                bindings::initialize_context(scope)
            };
            global_context = v8::Global::new(scope, context);
        }
      (isolate, None)
    };

    // Attach a new insepector
    let inspector =
      JsRuntimeInspector::new(&mut isolate, global_context.clone());


    // snip! See later
    // ...
}

到目前为止,还不错。对于Deno提供的所有可能性来说,有点额外的工作。然后一些有趣的事情发生了。例如:附加一个模块加载器。

// Rust!
// Attach a module loader
let loader = options
    .module_loader
    .unwrap_or_else(|| Rc::new(NoopModuleLoader));

解决模块的方式与Node不同,是通过一个额外的模块加载器处理的。

拷贝原始数据并启动核心操作#

再往下,Deno会初始化内置扩展。

// Rust!
// Add builtins extension
options
    .extensions
    .insert(0, crate::ops_builtin::init_builtins());

内置扩展是像克隆原始对象一样的东西。

// JavaScript
// Create copies of intrinsic objects
[  "AggregateError",  "Array",  "ArrayBuffer",  "BigInt",  "BigInt64Array",  "BigUint64Array",  "Boolean",  "DataView",  "Date",  "Error",  "EvalError",  "FinalizationRegistry",  "Float32Array",  "Float64Array",  "Function",  "Int16Array",  "Int32Array",  "Int8Array",  "Map",  "Number",  "Object",  "RangeError",  "ReferenceError",  "RegExp",  "Set",  "String",  "Symbol",  "SyntaxError",  "TypeError",  "URIError",  "Uint16Array",  "Uint32Array",  "Uint8Array",  "Uint8ClampedArray",  "WeakMap",  "WeakRef",  "WeakSet",].forEach((name) => {
  const original = globalThis[name];
  primordials[name] = original;
  copyPropsRenamed(original, primordials, name);
  copyPrototype(original.prototype, primordials, `${name}Prototype`);
});

这不仅复制了原始对象,而且还使Object.freeze 等函数成为可用的ObjectFreeze ,这将在下面进一步使用。

// JavaScript
ObjectFreeze(primordials);

// Provide bootstrap namespace
globalThis.__bootstrap = { primordials };

其他事情包括设置核心和错误行为。核心增加了一些功能,允许使用所谓的 "OPS "在V8和Rust之间进行通信。例如,这是打印东西到stdoutstderr 的JavaScript方面。

// JavaScript
function print(str, isErr = false) {
  opSync("op_print", str, isErr);
}

随着opSync ,解析到一个早先已经被激活的opcall

// Rust
// core/bidings.rs
set_func(scope, core_val, "opcall", opcall);

print 的Rust方面看起来是这样的。

// Rust
/// Builtin utility to print to stdout/stderr
pub fn op_print(
    _state: &mut OpState,
    msg: String,
    is_err: bool,
) -> Result<(), AnyError> {
    if is_err {
        stderr().write_all(msg.as_bytes())?;
        stderr().flush().unwrap();
    } else {
        stdout().write_all(msg.as_bytes())?;
        stdout().flush().unwrap();
    }
    Ok(())
}

所以从这里开始,我们已经与所有其他的JavaScript运行时有了一些区别。在我们建立上下文的时候,在这里我们设置了第一个绑定,在这里我们加载了核心扩展。

这就是主要的Deno核心。

定义平台的扩展#

从这里开始,工人定义其他扩展,使所有有趣的Deno功能得以实现。

// Rust
// Init extension ops
js_runtime.init_extension_ops().unwrap();
js_runtime.sync_ops_cache();
// Init async ops callback
js_runtime.init_recv_cb();

js_runtime

哪些功能被加载是由工作者定义的。例如,主要的Deno工作者会加载这个功能列表。

// Rust
let extensions: Vec<Extension> = vec![
    // Web APIs
    deno_webidl::init(),
    deno_console::init(),
    deno_url::init(),
    deno_web::init(options.blob_store.clone(), options.location.clone()),
    deno_fetch::init::<Permissions>(
        options.user_agent.clone(),
        options.root_cert_store.clone(),
        None,
        None,
        options.unsafely_ignore_certificate_errors.clone(),
        None,
    ),
    deno_websocket::init::<Permissions>(
        options.user_agent.clone(),
        options.root_cert_store.clone(),
        options.unsafely_ignore_certificate_errors.clone(),
    ),
    deno_webstorage::init(options.origin_storage_dir.clone()),
    deno_crypto::init(options.seed),
    deno_broadcast_channel::init(
        options.broadcast_channel.clone(),
        options.unstable,
    ),
    deno_webgpu::init(options.unstable),
    deno_timers::init::<Permissions>(),
    // ffi
    deno_ffi::init::<Permissions>(options.unstable),
    // Metrics
    metrics::init(),
    // Runtime ops
    ops::runtime::init(main_module.clone()),
    ops::worker_host::init(options.create_web_worker_cb.clone()),
    ops::fs_events::init(),
    ops::fs::init(),
    ops::io::init(),
    ops::io::init_stdio(),
    deno_tls::init(),
    deno_net::init::<Permissions>(
        options.root_cert_store.clone(),
        options.unstable,
        options.unsafely_ignore_certificate_errors.clone(),
    ),
    ops::os::init(),
    ops::permissions::init(),
    ops::process::init(),
    ops::signal::init(),
    ops::tty::init(),
    deno_http::init(),
    ops::http::init(),
    // Permissions ext (worker specific state)
    perm_ext,
];

你在这里可以看到很多来自网络的功能。Deno努力做到与网络平台绝对兼容,不想创建自己的API。你在这里看到的是使Deno拥有所有这些网络平台功能的扩展。

其中重要的一点是,扩展在矢量中的顺序很重要。Deno毕竟是在加载JavaScript,你需要有例如console ,然后才可以在其他扩展中使用它。同样地,如果没有URLsfetch 就不能发生。

每个扩展都会加载一个JavaScript部分--一个调用Deno操作的接口(包括同步和异步),以及一个用Rust编写的本地插件。最后一个会进行实际的HTTP调用,或者从文件系统中读取。它总是在Deno土地和本地土地之间来回穿梭。

在启动后,我们启动了tokio事件循环。但这是另一个故事,在另一个时间。

你能用这个做什么?#

这一切都发生在Deno的主运行时间中。但是你可以通过把合适的crate(每个扩展都可以在crates.io上单独使用)和编写你自己的扩展来轻松创建你自己的运行时。我认为这就是Deno的真正力量所在。这是一种在任何地方使用V8的简单方法,并根据你的需要形成它。

// Rust
// define a couple of worker options
let options = WorkerOptions {
   // ...
};

// load my main file, or a string ...
let js_path = Path::new("main.js");
let main_module = deno_core::resolve_path(&js_path.to_string_lossy())?;

// allow everything
let permissions = Permissions::allow_all();

// Initialize a runtime instance

// create a new deno worker!
let mut worker = MainWorker::from_options(
    main_module.clone(), 
    permissions, 
    &options
);

let mut buf = BufferRedirect::stdout().unwrap();
    
worker.bootstrap(&options);
worker.execute_module(&main_module).await?;

// and let's go!!
worker.run_event_loop(false).await?;

理论上,你可以用它来重新创建Node.js。不过,这并没有什么意义。除此之外,你可以提供一个JavaScript运行时,例如:console.log到你的云提供商的日志引擎。或者是一个具有非常少的功能集来重构响应,例如在边缘网络上。

你可以注入你自己的SDK,并访问你架构中需要认证的部分。想想有一个像Netlify或Cloudflare这样的边缘网络,你可以重写HTTP响应,你有一大堆额外的工具可以这样做。

你可以有一个运行无服务器有效载荷的V8,这些有效载荷是根据他们的使用情况定制的。最重要的是:Rust让你的工作变得切实可行。安装Deno的一部分就像在Cargo.toml 中添加一行一样容易。 这就是Rust的真正力量。让人们能够做一些他们以前不会做的事情。