12

针对JavaScript开发人员的Rust简介

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzI0MDIwNTQ1Mg%3D%3D&%3Bmid=2676496678&%3Bidx=1&%3Bsn=3732b8529e41e3a879dcd7f95d443a0c
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
YBFV3qj.png!mobile

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 <[email protected]>",
    "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的另一个原因可能是语言偏好,但这是一个完全不同的讨论,我不会在这里讨论。

N3uQBzy.png!mobile

参考资料

[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/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK