我最近有机会玩弄了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比我更有权威。
- Node.js的过程开始了。这就启动了V8平台。V8平台是依赖于平台的绑定,所以你可以在所有不同的操作系统上运行V8。根据我的经验,初始化进程实际上是可能占用相当多时间的部分。
- 之后,Node会创建一个新的V8隔离区。V8隔离区是V8运行时的一个独立副本,包括堆管理器、垃圾收集器等。这在一个单线程上运行。这两个步骤都发生在本地土地上。
- 现在我们进入JavaScript的土地。我们初始化一个新的V8上下文。一个V8上下文包括全局对象和JavaScript内置。这些东西构成了语言,而不是具体的运行时间。到此为止,浏览器、Node.js和Deno几乎都是一样的。
- 在Node.js中,运行时独立的状态,如Node.jsprimordials被初始化。这意味着所有的JavaScript内置插件都被克隆并冻结,以用于运行时独立状态。因此,如果用户使用Object原型或类似的脾气,这将不会影响Node.js的功能
- 我们启动事件循环(Deno中的Tokio,Node中的libuv)并启动V8检查器
- 最后,Node初始化运行时的依赖状态。这是与你所使用的运行时有关的一切。这意味着Node.js中的
process、require等,Deno中的fetch,各地的console。 - 加载主脚本并启动老的循环!
让我们看一下代码。
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。有几句话要说。
- 每个平台可以有一个以上的隔离器。想想看一个浏览器。启动浏览器就是初始化平台。打开一个新的标签,就会创建一个新的隔离器+上下文。
- 如果你认为无服务器平台,Cloudflare工人或Deno部署的工作方式非常类似。他们的工作者在一个V8平台中运行,但每次调用时,你都可以启动一个新的隔离区。有了所有的安全保证。
- 隔离区有一个全局对象和一个上下文,但它缺乏任何你在使用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之间进行通信。例如,这是打印东西到stdout 或stderr 的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 ,然后才可以在其他扩展中使用它。同样地,如果没有URLs ,fetch 就不能发生。
每个扩展都会加载一个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的真正力量。让人们能够做一些他们以前不会做的事情。