针对JavaScript开发人员的Rust简介

共 8772字,需浏览 18分钟

 ·

2021-02-16 16:53

Rust是2010年起源于Mozilla Research的一种编程语言。如今,所有大公司都在使用它。

亚马逊和微软都认可它是其系统中C / C ++的最佳替代品,但是Rust并不止于此。像Figma和Discord这样的公司现在也通过在客户端应用中使用Rust来引领潮流。

本篇Rust教程旨在简要介绍Rust,如何在浏览器中使用它,以及何时应该考虑使用它。我将从比较Rust和JavaScript开始,然后引导你完成Rust在浏览器中运行的步骤。最后,我将介绍一个使用Rust和JavaScript的COVID simulator web应用程序的快速性能评估。

简而言之

Rust在概念上与JavaScript非常不同。但也有相似之处需要指出,让我们来看看问题的两面。

相似之处

这两种语言都有一个现代化的包管理系统。JavaScript有npm,Rust有Cargo。Rust有 Cargo.toml 来代替 package.json 进行依赖管理。要创建一个新的项目,使用 cargo init,要运行它,使用 cargo run。不太陌生吧?

Rust中有许多很酷的功能,你已经从JavaScript中知道了,只是语法略有不同。利用这个常见的JavaScript模式,对数组中的每个元素都应用一个闭包:

let staff = [
{name: "George", money: 0},
{name: "Lea", money: 500000},
];
let salary = 1000;
staff.forEach( (employee) => { employee.money += salary; } );

在Rust中,我们可以这样写:

let salary = 1000;
staff.iter_mut().for_each(
|employee| { employee.money += salary; }
);

诚然,习惯这种语法需要时间,用管子( | )代替括号。但在克服了最初的尴尬之后,我发现它比另一组括号读起来更清晰。

再举一个例子,这是JavaScript中的对象解构:

let point = { x: 5, y: 10 };
let {x,y} = point;

同样在Rust中:

let point = Point { x: 5, y: 10 };
let Point { x, y } = point;

主要的区别是,在Rust中我们必须指定类型(Point),更普遍的是,Rust需要在编译时知道所有类型。但与大多数其他编译语言不同的是,编译器尽可能自己推断类型。

为了进一步解释这个问题,下面是在C++和许多其他语言中有效的代码。每个变量都需要明确的类型声明。

int a = 5;
float b = 0.5;
float c = 1.5 * a;

在JavaScript以及Rust中,这段代码是有效的:

let a = 5;
let b = 0.5;
let c = 1.5 * a;

共享功能不胜枚举:

  • Rust具有 async + await 语法。
  • 数组可以像让 let array = [1,2,3] 一样简单地创建。
  • 代码按模块组织,有明确的导入和导出。
  • 字符串是用Unicode编码的,处理特殊字符没有问题。

我可以继续列举下去,但我想我的观点现在已经很清楚了。Rust有一系列丰富的功能,这些功能在现代JavaScript中也有使用。

不同点

Rust是一种编译语言,这意味着没有运行时可以执行Rust代码。一个应用程序只能在编译器(rustc)完成它的魔法之后运行。这种方法的好处通常是更好的性能。

幸运的是,Cargo为我们解决了调用编译器的问题。而有了webpack,我们还可以将 Cargo 隐藏在 npm run build 后面。有了这个指南,只要为项目设置好Rust,就可以保留Web开发者的正常工作流程。

Rust 是一种强类型语言,这意味着在编译时所有类型必须匹配。例如,你不能调用一个参数类型错误或参数数量错误的函数。编译器会在你运行时遇到这个错误之前为你捕捉到它。显而易见的比较是TypeScript,如果你喜欢TypeScript,那么你很可能会喜欢Rust。

但别担心:如果你不喜欢TypeScript,Rust可能还是适合你。Rust 是近几年从头开始构建的,它考虑到了过去几十年来人类在编程语言设计方面所学到的一切。其结果是一种令人耳目一新的简洁语言。

Rust中的模式匹配是我最喜欢的一个特征,其他语言有 switchcase 来避免像这样的长链:

if ( x == 1) { 
// ...
} else if ( x == 2 ) {
// ...
}
else if ( x == 3 || x == 4 ) {
// ...
} // ...

Rust使用了如下更优雅的匹配项:

match x {
1 => { /* Do something if x == 1 */},
2 => { /* Do something if x == 2 */},
3 | 4 => { /* Do something if x == 3 || x == 4 */},
5...10 => { /* Do something if x >= 5 && x <= 10 */},
_ => { /* Catch all other cases */ }
}

我认为这是非常整洁的,我希望JavaScript开发人员也能欣赏这种语法扩展。

不幸的是,我们还得谈谈Rust的黑暗面。直言不讳地说,使用严格的类型系统有时会让人感觉非常繁琐。如果你认为C++或Java的类型系统很严格,那么请准备好迎接Rust的艰难之旅吧。

就我个人而言,我很喜欢Rust这部分。我依赖于严格的类型系统,因此可以关闭大脑的一部分——每当我发现自己在编写JavaScript时,大脑的一部分就会剧烈地兴奋起来。但是我知道对于初学者来说,总是和编译器作对是很烦人的。我们将在稍后的Rust教程中看到一些。

Hello Rust

现在,让我们用Rust在浏览器中运行一个 hello world ,我们首先要确保所有必要的工具都已安装。

工具

使用rustup安装Cargo + rustc。 Rustup是推荐的安装Rust的方法,它将安装最新的稳定版Rust的编译器(rustc)和包管理器(Cargo)。它将安装Rust最新稳定版本的编译器(rustc)和包管理器(Cargo)。它还可以管理beta版和每夜构建版,但对于本例来说,这不是必需的。

  • 在终端机上输入 cargo --version 来检查安装情况,你应该可以看到 cargo 1.48.0 (65cbdd2dc 2020-10-14) 这样的内容。
  • 还要检查Rustup:rustup --version 应该产生 rustup 1.23.0(00924c9ba 2020-11-27)

安装wasm-pack。 这是为了将编译器与npm集成。

  • 通过输入 wasm-pack --version 来检查安装,这应该为您提供 wasm-pack 0.9.1 之类的东西。

我们还需要Node和npm。我们有一篇完整的文章[1]解释了安装这两个的最佳方法。

编写Rust代码

现在一切都安装好了,让我们来创建项目。最终的代码也可以在这个GitHub仓库[2]中找到。我们从一个可以编译成npm包的Rust项目开始,之后会有导入该包的JavaScript代码。

要创建一个名为 hello-world 的Rust项目,请使用 cargo init --lib hello-world。这将创建一个新目录并生成Rust库所需的所有文件:

├──hello-world
├── Cargo.toml
├── src
├── lib.rs

Rust代码将放在 lib.rs 中,在此之前我们必须调整 Cargo.toml。它使用 TOML[3] 定义了依赖关系和其他包的信息。如果想在浏览器中看到hello world,请在 Cargo.toml 中的某个地方添加以下行数(例如,在文件的最后)。

[lib]
crate-type = ["cdylib"]

这告诉编译器在C兼容模式下创建一个库。显然我们在我们的例子中没有使用C。C-compatible只是意味着不是Rust专用的,这是我们使用JavaScript中的库所需要的。

我们还需要两个外部库,将它们作为单独的一行添加到依赖关系部分。

[dependencies]
wasm-bindgen = "0.2.68"
web-sys = {version = "0.3.45", features = ["console"]}

这些都是来自 crates.io[4] 的依赖项,它是 Cargo 使用的默认包仓库。

wasm-bindgen[5]是必要的,以创建一个我们以后可以从JavaScript中调用的入口点。(你可以在这里找到完整的文档。)值 ”0.2.68" 指定了版本。

web-sys[6]包含了所有Web API的Rust绑定,它将使我们能够访问浏览器控制台。请注意,我们必须明确地选择控制台功能,我们最终的二进制文件将只包含这样选择的Web API绑定。

接下来是 lib.rs 内部的实际代码。自动生成的单元测试可以删除。只需使用以下代码替换文件的内容:

use wasm_bindgen::prelude::*;
use web_sys::console;

#[wasm_bindgen]
pub fn hello_world() {
console::log_1("Hello world");
}

顶部的 use 语句是用于从其他模块导入项目。这与JavaScript中的 import 类似)。

pub fn hello_world(){...} 声明一个函数。pub 修饰符是“public”的缩写,作用类似于JavaScript中的 export。注释 #[wasm_bindgen] 特定于Rust编译为[WebAssembly (Wasm)](https://webassembly.org/ "wasm_bindgen] 特定于Rust编译为[WebAssembly (Wasm "wasm_bindgen] 特定于Rust编译为[WebAssembly (Wasm)")")。我们在这里需要它来确保编译器将包装函数公开给JavaScript。

在功能主体中,“Hello world”被打印到控制台上。Rust中的 console :: log_1() 是对 console.log() 的调用的包装。

你是否注意到函数调用中的 _1 后缀?这是因为JavaScript允许使用可变数量的参数,而Rust不允许。为了解决这个问题, wasm_bindgen 为每种参数数量生成一个函数。是的,这很快就会变得丑陋!但这有效。在web-sys文档[7]中提供了一个可以在Rust控制台中调用的完整函数列表。

现在我们应该已经一切就绪,试着用下面的命令编译它。这将下载所有的依赖项并编译项目,第一次可能会花一些时间。

cd hello-world
wasm-pack build

哈!Rust编译器对我们不满意。

error[E0308]: mismatched types
--> src\lib.rs:6:20
|
6 | console::log_1("Hello world");
| ^^^^^^^^^^^^^ expected struct `JsValue`, found `str`
|
= note: expected reference `&JsValue`
found reference `&'static str

注意:如果您看到其他错误(error: linking with cc failed: exit code: 1)并且你使用的是Linux,则说明缺少交叉编译依赖性。sudo apt install gcc-multilib 应该可以解决此问题。

正如我前面提到的,编译器很严格。当它期望一个 JsValue 的引用作为一个函数的参数时,它不会接受一个静态字符串。为了满足编译器的要求,必须进行显式转换。

console::log_1(&"Hello world".into());

方法 [into()](https://doc.rust-lang.org/std/convert/trait.Into.html "into( "into()")") 将一个值转换为另一个值。Rust 编译器很聪明,它可以推迟哪些类型参与转换,因为函数签名只留下了一种可能性。在这种情况下,它将转换为 JsValue,这是一个由JavaScript管理的值的包装类型。然后,我们还得加上 &,通过引用而不是通过值来传递,否则编译器又会抱怨。

尝试再次运行 wasm-pack build,如果一切顺利,则最后一行应如下所示:

[INFO]: :-) Your wasm pkg is ready to publish at /home/username/intro-to-rust/hello-world/pkg.

如果你能走到这一步,你现在就可以手动编译Rust了。下一步,我们将把它与npm和webpack集成,后者将自动为我们完成这项工作。

JavaScript整合

在这个例子中,我决定将 package.json 放在 hello-world 目录内。我们也可以为Rust项目和JavaScript项目使用不同的目录,这是个口味问题。

以下是我的 package.json 文件。遵循的最简单方法是将其复制并运行 npm install,或者运行 npm init 并仅复制 dev 依赖项:

{
"name": "hello-world",
"version": "1.0.0",
"description": "Hello world app for Rust in the browser.",
"main": "index.js",
"scripts": {
"build": "webpack",
"serve": "webpack serve"
},
"author": "Jakob Meier ",
"license": "(MIT OR Apache-2.0)",
"devDependencies": {
"@wasm-tool/wasm-pack-plugin": "~1.3.1",
"@webpack-cli/serve": "^1.1.0",
"css-loader": "^5.0.1",
"style-loader": "^2.0.0",
"webpack": "~5.8.0",
"webpack-cli": "~4.2.0",
"webpack-dev-server": "~3.11.0"
}
}

如你所见,我们使用的是webpack 5。Wasm-pack也可以和旧版本的webpack一起使用,甚至可以不使用捆绑程序。但每个设置的工作方式都有些不同,我建议你在跟随这个Rust教程时使用完全相同的版本。

另一个重要的依赖项是 wasm-pack-plugin。这是一个Webpack插件,专门用于加载使用wasm-pack构建的Rust软件包。

继续,我们还需要创建 webpack.config.js 文件来配置webpack。它应该是这样的:

const path = require('path');
const webpack = require('webpack');
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");

module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.js',
},
plugins: [
new WasmPackPlugin({
crateDirectory: path.resolve(__dirname, ".")
}),
],
devServer: {
contentBase: "./src",
hot: true,
},
module: {
rules: [{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
}, ]
},
experiments: {
syncWebAssembly: true,
},
};

所有的路径都配置为Rust代码和JavaScript代码并排。index.js 将在 src 文件夹中,紧挨着 lib.rs。如果你喜欢不同的设置,可以随时调整这些。

你还会注意到,我们使用webpack experiments[8],这是webpack 5引入的新选项。请确保将 syncWebAssembly 设置为true。

最后,我们必须创建JavaScript入口点 src/index.js

import("../pkg").catch(e => console.error("Failed loading Wasm module:", e)).then(
rust =>
rust.hello_world()
);

我们必须异步加载Rust模块。调用 rust.hello_world() 会调用一个生成的封装函数,而这个函数又会调用 lib.rs 中定义的Rust函数 hello_world

现在,运行 npm run serve 应该可以编译所有内容并启动开发服务器。我们没有定义HTML文件,因此页面上没有任何显示。你可能还必须手动转到 http://localhost:8080/index,因为http://localhost:8080只是列出文件而不执行任何代码。

打开空白页后,打开开发人员控制台。Hello World应该有一个日志条目。

好吧,对于一个简单的hello world来说,这是相当多的工作。但现在一切都到位了,我们可以轻松地扩展Rust代码,而不用担心这些。保存对 lib.rs 的修改后,你应该会自动看到重新编译和浏览器中的实时更新,就像JavaScript一样。

何时使用Rust

Rust不是JavaScript的一般替代品。它只能通过Wasm在浏览器中运行,这在很大程度上限制了它的作用。即使你可以用Rust替换几乎所有的JavaScript代码,如果你真的想的话,那是一个坏主意,而且不是Wasm的目的。例如,Rust并不适合与你网站的UI进行交互。

我认为Rust + Wasm是一个额外的选项,可以用来更有效地运行CPU重的工作负载。以较大的下载量为代价,Wasm避免了JavaScript代码面临的解析和编译开销。这一点,再加上编译器的强力优化,可能会带来更好的性能。这通常是公司为特定项目选择Rust的原因。选择Rust的另一个原因可能是语言偏好,但这是一个完全不同的讨论,我不会在这里讨论。

参考资料

[1]

文章: https://www.sitepoint.com/quick-tip-multiple-versions-node-nvm/

[2]

GitHub仓库: https://github.com/sitepoint-editors/rust-wasm-hello-world

[3]

TOML: https://github.com/toml-lang/toml

[4]

crates.io: https://crates.io/

[5]

wasm-bindgen: https://crates.io/crates/wasm-bindgen

[6]

web-sys: https://crates.io/crates/web-sys

[7]

web-sys文档: https://rustwasm.github.io/wasm-bindgen/api/web_sys/console/index.html

[8]

webpack experiments: https://webpack.js.org/configuration/experiments/


浏览 49
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报