[譯]使用 Rust 開發一個簡單的 Web 應用,第 1 部分

LeopPro發表於2019-03-04

使用 Rust 開發一個簡單的 Web 應用,第 1 部分

1 簡介 & 背景

站在一個經驗豐富但剛接觸本生態系統的開發者的角度,使用 Rust 開發一個小型的 Web 應用是什麼感覺呢?請繼續閱讀。

我第一次聽說 Rust 的時候就對它產生了興趣。一個支援巨集的系統級語言,並且在高階抽象方面有成長空間。真棒!

到目前為止,我只寫過關於 Rust 的部落格,做了一些很基礎的“Hello World”級程式。所以,我估計我的觀點會欠一些火候。

不久之前,我看見了關於學習 Racket 的這篇文章,我覺得特別好。我們需要更多的人分享他們作為技術初學者時獲得的經驗,尤其是那些已經有相當豐富的技術經驗的人[1]。我也非常喜歡它的“思維流”方法。我想,像這樣寫一個 Rust 教程,應該是一個非常好的嘗試。

好了,前言說完了,我們開始吧!

2 應用

我想構建的應用要實現我的一個簡單需求:用一種無腦的方式記錄我每天服藥時間。我想我點一下主螢幕上的連結,讓它記錄這次訪問,並且這將會儲存為一份我服藥時間的記錄。

Rust 似乎很適合這個應用。它速度快,執行一個簡單的伺服器消耗的資源特別少,所以它不會對我的 VPS 造成負擔。我還想用 Rust 做一些更實際的事。

最小可行性版本非常小巧,但如果我想新增更多功能,它也有增長空間。聽起來完美!

3 計劃

我不得不承認一件事:我弄丟了這個專案的早期版本,這將產生以下弊端:當我重現它的時候,我並不會有幾周前剛剛接觸它的時候那種陌生感。然而,我想我仍然記得當時讓我痛苦的地方,並且我會盡力重現這些難點。

我知道一個道理有必要在這裡講一下:對於一個獨立的個人程式來說,利用現有 API 要比試著獨立完成所有的工作容易得多。

為了達成目的,我制定瞭如下計劃:

  1. 構建一個簡單的 Web 伺服器,當我訪問他的時候它能在螢幕上顯示“Hello World”。
  2. 構建一個小型程式,每當他執行的時候,它會按照一定格式記錄當前時間。
  3. 將上面兩個整合到一個程式中。
  4. 將此應用程式部署到我的 Digital Ocean VPS 上。

4 編寫一個“Hello World” Web 應用

所以,我要建立一個新的 Git 倉庫 & 裝好 homebrew。我至少知道,我先要安裝 Rust。

4.1 安裝 Rust

$ brew update
...
$ brew install rust
==> Downloading https://homebrew.bintray.com/bottles/rust-1.0.0.yosemite.bottle.tar.gz
############################################################ 100.0%
==> Pouring rust-1.0.0.yosemite.bottle.tar.gz
==> Caveats
Bash completion has been installed to:
  /usr/local/etc/bash_completion.d

zsh completion has been installed to:
  /usr/local/share/zsh/site-functions
==> Summary
   /usr/local/Cellar/rust/1.0.0: 13947 files, 353M
複製程式碼

Ok,在開始之前,我們先寫一個常規的“Hello World”程式。

$ cat > hello_world.rs
fn main() {

        println!("hello world");
}
^D
$ rustc hello_world.rs
$ ./hello_world
hello world
$
複製程式碼

到目前為止一切順利。Rust 正常工作了,或者至少說,Rust 的編譯器在正常工作。

有位朋友建議我嘗試使用 nickle.rs,那是 Rust 的 一個 Web 應用框架。我覺得不錯。

截止到今天,它的第一個示例是:

#[macro_use] extern crate nickel;

use nickel::Nickel;

fn main() {
    let mut server = Nickel::new();

    server.utilize(router! {
        get "**" => |_req, _res| {
            "Hello world!"
        }
    });

    server.listen("127.0.0.1:6767");
}
複製程式碼

我第一次做這些的時候,我有一點小分心,去學了一點 Cargo。這次我注意到了這個入門指南,所以我打算跟著它走而不是什麼都靠自己誤打誤撞。

這裡有一個指令碼,我應該通過 curl 下載然後使用 root 許可權執行。但是“患有強迫症的”我打算先把指令碼下載下來檢查一下。

curl -LO https://static.rust-lang.org/rustup.sh

Ok,這事實上並不像我預想的那樣,這個指令碼完成了很多工作,大部分都是我現在不想自己去做的。而我很想知道,cargo 是不是用 rustc 來安裝的?

$ which cargo
/usr/local/bin/cargo
$ cargo -v
Rust 包管理器

用法:
    cargo <命令> [<引數>...]
    cargo [選項]

選項:
    -h, --help       顯示幫助資訊
    -V, --version    顯示版本資訊並退出
    --list           安裝命令列表
    -v, --verbose    使用詳細的輸出

常見的 cargo 命令:
    build       編譯當前工程
    clean       刪除目標目錄
    doc         編譯此工程及其依賴項文件
    new         建立一個新的 cargo 工程
    run         編譯並執行 src/main.rs
    test        執行測試
    bench       執行基準測試
    update      更新 Cargo.lock 中的依賴項
    search      搜尋註冊過的 crates

執行 `cargo help <command>` 獲取指定命令的更多幫助資訊。
複製程式碼

Ok,我猜這看起來不錯吧?我現在就開始用它。

$ rm rustup.sh

4.2 設定工程

下一步是生成一個新的專案目錄,但是我已經有了一個專案目錄。不管怎樣,我還是要試一試。

$ cargo new . --bin
目標 `/Users/joel/Projects/simplelog/.` 已經存在
複製程式碼

嗯……它不工作。

$ cargo -h
在 <路徑> 處建立一個新的 Cargo 包。

用法:
    cargo new [選項] <路徑>
    cargo new -h | --help

選項:
    -h, --help          顯示幫助資訊
    --vcs <vcs>         為指定的版本管理系統(git 或 hg)
                        初始化一個新倉庫
                        或者不使用版本管理系統(none)
    --bin               建立可執行檔案工程而不是庫工程
    --name <name>       設定結果包名
    -v, --verbose       使用詳細的輸出
複製程式碼

上述程式碼第一行中 cargo -h 應為作者筆誤,實為 cargo new -h。(譯者注)

嗯,它似乎不會按照我的預想去工作,我需要重建這個倉庫。

$ cd ../
$ rm -rf simplelog/
$ cargo new simple-log --bin
$ cd simple-log/
複製程式碼

Ok,我們看看這裡有什麼?

$ tree
.
|____.git
| |____config
| |____description
| |____HEAD
| |____hooks
| | |____README.sample
| |____info
| | |____exclude
| |____objects
| | |____info
| | |____pack
| |____refs
| | |____heads
| | |____tags
|____.gitignore
|____Cargo.toml
|____src
| |____main.rs
複製程式碼

看,它建立了一個 Git 倉庫,Cargo.toml 檔案和在 src 目錄中的 main.rs 檔案,看起來不錯。

根據 Nickel 的入門指南,我向 Cargo.toml 檔案中加入 nickel.rs 依賴,現在它看起來像是這樣:

[package]
name = "simple-log"
version = "0.1.0"
authors = ["Joel McCracken <mccracken.joel@gmail.com>"]

[dependencies.nickel]

git = "https://github.com/nickel-org/nickel.rs.git"
複製程式碼

我覺得這很容易理解。然而我不確定 dependencies.nickel 實際的含義是什麼。dependencies 是一個帶有 nickel 鍵的雜湊值麼?但可以肯定的是,我們已經在工程中引進 Nickel 了,真棒!

4.3 執行“Hello World”例子

管他呢,我把那個例子複製到 main.rs 中:

#[macro_use] extern crate nickel;

use nickel::Nickel;

fn main() {
    let mut server = Nickel::new();

    server.utilize(router! {
        get "**" => |_req, _res| {
            "Hello world!"
        }
    });

    server.listen("127.0.0.1:6767");
}
複製程式碼

啥?macro_useextern 都是什麼東西?為什麼要用 use?這些疑問我會在下面一一解答。

這裡我有一些疑問,macro_use 似乎是一個巨集指令[2],但是我沒有看到任何巨集呼叫,所以我刪除了它。然而現在我注意到了 router!,因此我更加傾向於這是一個巨集指令。同時,我懷疑用指令一詞來指代是否恰當,因此別太肯定。

我們試一下:

cargo run
    Updating git repository `https://github.com/nickel-org/nickel.rs.git`
    Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading unsafe-any v0.4.1
 Downloading httparse v0.1.2
 Downloading traitobject v0.0.3
 Downloading lazy_static v0.1.10
 Downloading pkg-config v0.3.4
 Downloading num_cpus v0.2.5
 Downloading modifier v0.1.0
 Downloading groupable v0.2.0
 Downloading unicase v0.1.0
 Downloading gcc v0.3.5
 Downloading time v0.1.25
 Downloading log v0.3.1
 Downloading url v0.2.34
 Downloading plugin v0.2.6
 Downloading hyper v0.4.0
 Downloading matches v0.1.2
 Downloading mustache v0.6.1
 Downloading bitflags v0.1.1
 Downloading typeable v0.1.1
 Downloading openssl v0.6.2
 Downloading rustc-serialize v0.3.14
 Downloading typemap v0.3.2
 Downloading regex v0.1.30
 Downloading cookie v0.1.20
 Downloading mime v0.0.11
 Downloading libc v0.1.8
 Downloading openssl-sys v0.6.2
   Compiling modifier v0.1.0
   Compiling traitobject v0.0.3
   Compiling regex v0.1.30
   Compiling libc v0.1.8
   Compiling lazy_static v0.1.10
   Compiling matches v0.1.2
   Compiling httparse v0.1.2
   Compiling rustc-serialize v0.3.14
   Compiling groupable v0.2.0
   Compiling pkg-config v0.3.4
   Compiling gcc v0.3.5
   Compiling bitflags v0.1.1
   Compiling unicase v0.1.0
   Compiling typeable v0.1.1
   Compiling unsafe-any v0.4.1
   Compiling log v0.3.1
   Compiling num_cpus v0.2.5
   Compiling typemap v0.3.2
   Compiling mime v0.0.11
   Compiling plugin v0.2.6
   Compiling openssl-sys v0.6.2
   Compiling time v0.1.25
   Compiling openssl v0.6.2
   Compiling url v0.2.34
   Compiling mustache v0.6.1
   Compiling cookie v0.1.20
   Compiling hyper v0.4.0
   Compiling nickel v0.5.0 (https://github.com/nickel-org/nickel.rs.git#69546f58)
   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
     Running `target/debug/simple-log`
Listening on http://127.0.0.1:6767
Ctrl-C to shutdown server
^C
複製程式碼

哦吼!我的瀏覽器中 localhost:6767 的訪問成功啦!

4.4 最終挑戰

Ok,現在我想嘗試一件事情,然後今晚就收工:我可以將“Hello World”移動到它自己的函式中麼?畢竟我們現在是嬰兒學步的階段。

fn say_hello() {
    "Hello dear world!";
}

fn main() {
    let mut server = Nickel::new();

    server.utilize(router! {
        get "**" => |_req, _res| {
            say_hello();
        }
    });

    server.listen("127.0.0.1:6767");
}
複製程式碼

錯誤……當我這次執行的時候,我看到了“未找到”。我們這次把分號去掉,以防萬一:

fn say_hello() {
    "Hello dear world!"
}

fn main() {
    let mut server = Nickel::new();

    server.utilize(router! {
        get "**" => |_req, _res| {
            say_hello()
        }
    });

    server.listen("127.0.0.1:6767");
}
複製程式碼

好吧……現在編譯器報出了不同的錯誤資訊:

$ cargo run
   Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
src/main.rs:6:5: 6:24 錯誤:不匹配的型別:
    預期 `()`,
    找到 `&`static str`
   (預期 (),
    找到 &-ptr) [E0308]
src/main.rs:6     "Hello dear world!"
                  ^~~~~~~~~~~~~~~~~~~
錯誤:由於先前的錯誤而中止
不能編譯 `simple-log`。

想檢視更多資訊,請加上 --verbose 重新執行命令。
複製程式碼

根據報錯資訊,我猜測分號的有無是重要的。現在這產生了一個型別錯誤。哦,我有九成的把握肯定這裡的 () 指的是“unit”,這是 Rust 中的空、未定義、或者未規定。我知道這不完全對,但是我想這是講得通的。

假設 Rust 會做型別推斷。編譯器沒這麼做嗎?還是隻在函式邊界附近沒有做?嗯……

錯誤資訊告訴我,編譯器希望函式的返回值是“unit”,但是實際上返回值是一個靜態字串(這是啥?)。我已經看過函式返回值的語法了,我們看一看:

#[macro_use] extern crate nickel;

use nickel::Nickel;

fn say_hello() -> &`static str {
    "Hello dear world!"
}

fn main() {
    let mut server = Nickel::new();

    server.utilize(router! {
        get "**" => |_req, _res| {
            say_hello()
        }
    });

    server.listen("127.0.0.1:6767");
}
複製程式碼

在我看來 &`static str 型別非常的怪異。它會成功編譯麼?它會正常工作麼?

$ cargo run &
[1] 14997
Running `target/debug/simple-log`
Listening on http://127.0.0.1:6767
Ctrl-C to shutdown server
$ curl http://localhost:6767
Hello dear world!
$ fg
cargo run
^C
複製程式碼

耶,它工作了!這一次 Rust 沒有令人失望。我不知道是不是因為我對這些工具更熟悉了,還是我選擇去多看文件,但是我樂在其中。一門語言和一門語言之間的差別非常的驚奇。雖然我理解這些程式碼示例,但是我仍然不能高效的編輯它們。

在下一章中,我們將完成當前日期寫入檔案的過程。你可以在這裡閱讀它。

系列文章:使用 Rust 開發一個簡單的 Web 應用

腳註:

[1] 我並不是想說,初學者的經驗是沒有價值的 —— 遠非如此!我認為相比於經驗豐富者而言,初學者經常會帶來一些獨到的見解,他們可能會注意到生態系統中的某些東西是非標準的。

[2] 我通常說編譯期指令,但是這對於 Rust 這樣一個編譯語言來說沒太大意義。所以除了巨集指令以外,我不知道該如何表述它了。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章