- 原文地址:A Simple Web App in Rust, Part 2b
- 原文作者:Joel's Journal
- 译文出自:掘金翻译计划
- 本文永久链接:github.com/xitu/gold-m…
- 译者:LeopPro
使用 Rust 开发一个简单的 Web 应用,第 2b 部分
目录
1 系列文章
在这个系列文章中,我记录下了,我在尝试使用 Rust 开发一个简单的 Web 应用过程中获得的经验。
到目前为止,我们有:
上一篇文章很恶心。这次我们会探索 Rust 的时间、日期格式,重点是用一个合适的格式记录时间。
2 使用 Chrono
在 crates.io 中搜索“日期”将得到一个名为 chrono 的包。它热度很高,更新频繁,所以这看起来是一个好的候选方案。 从 README 文件来看,它有着很棒的的日期、时间输出功能。
第一件事情是在 Cargo.toml
中添加 Chrono 依赖,但在此之前,我们先把旧的 main.rs
移出,腾出空间用于实验:
$ ls
Cargo.lock Cargo.toml log.txt src target
$ cd src/
$ ls
main.rs web_main.rs
$ git mv main.rs main_file_writing.rs
$ touch main.rs
$ git add main.rs
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: main.rs
copied: main.rs -> main_file_writing.rs
Untracked files:
(use "git add <file>..." to include in what will be committed)
../log.txt
$ git commit -m 'move file writing out of the way for working with dates'
[master 4cd2b0e] move file writing out of the way for working with dates
2 files changed, 16 deletions(-)
rewrite src/main.rs (100%)
copy src/{main.rs => main_file_writing.rs} (100%)
在 Cargo.toml
中添加 Chrono 依赖:
[package]
name = "simple-log"
version = "0.1.0"
authors = ["Joel McCracken <mccracken.joel@gmail.com>"]
[dependencies]
chrono = "0.2"
[dependencies.nickel]
git = "https://github.com/nickel-org/nickel.rs.git"
自述文件接着说:
And put this in your crate root:
extern crate chrono;
我不知道这是什么意思,但我要尝试把它放到 main.rs
顶部,因为它看起来像是 Rust 代码:
extern crate chrono;
fn main() { }
编译:
$ cargo run
Updating registry `https://github.com/rust-lang/crates.io-index`
Downloading num v0.1.25
Downloading rand v0.3.8
Downloading chrono v0.2.14
Compiling rand v0.3.8
Compiling num v0.1.25
Compiling chrono v0.2.14
Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
Running `/Users/joel/Projects/simple-log/target/debug/simple-log`
好了,它似乎下载了 Chrono,并且编译成功了、结束了。我想下一步就是尝试使用它。根据自述文件第一个例子,我想这样:
extern crate chrono;
use chrono::*;
fn main() {
let local: DateTime<Local> = Local::now();
println!('{}', local);
}
=>
$ cargo run
Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
main.rs:6:14: 6:16 error: unterminated character constant: '{
main.rs:6 println!('{}', local);
^~
Could not compile `simple-log`.
To learn more, run the command again with --verbose.
……?我愣了几秒后,我意识到它是告诉我,我应该使用双引号,而不是单引号。这是有道理的,单引号被用于生命周期规范。
从单引号切换到双引号之后:
$ cargo run
Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
Running `/Users/joel/Projects/simple-log/target/debug/simple-log`
2015-06-05 16:54:47.483088 -04:00
……哇偶,这真简单。看起来 println!
可以调用某种接口以打印各种不同的东西。
这很讽刺。我很轻松就构建一个简单的“Hello World”级 Web 应用并且打印了一个格式良好的时间,但我在写入文件上花费了很多时间。我不知道这意味着什么。尽管 Rust 语言很难用(对我来说),但是我相信 Rust 社区已经做了许多努力使系统包工作良好。
3 将日期时间写入文件
我认为,下一步我们应该将这个字符串写入文件。为此,我想看看上一篇文章的结尾:
$ cat main_file_writing.rs
use std::io::prelude::*;
use std::fs::File;
use std::io;
fn log_something(filename: &'static str, string: &'static [u8; 12]) -> io::Result<()> {
let mut f = try!(File::create(filename));
try!(f.write_all(string));
Ok(())
}
fn main() {
match log_something("log.txt", b"ITS ALIVE!!!") {
Ok(..) => println!("File created!"),
Err(..) => println!("Error: could not create file.")
}
}
我只是将上面那个例子和这个合并到一起:
extern crate chrono;
use std::io::prelude::*;
use std::fs::File;
use std::io;
use chrono::*;
fn log_something(filename: &'static str, string: &'static [u8; 12]) -> io::Result<()> {
let mut f = try!(File::create(filename));
try!(f.write_all(string));
Ok(())
}
fn main() {
let local: DateTime<Local> = Local::now();
println!('{}', local);
match log_something("log.txt", b"ITS ALIVE!!!") {
Ok(..) => println!("File created!"),
Err(..) => println!("Error: could not create file.")
}
}
编译:
$ ls
Cargo.lock Cargo.toml log.txt src target
$ pwd
/Users/joel/Projects/simple-log
$ ls
Cargo.lock Cargo.toml log.txt src target
$ rm log.txt
$ cargo run
Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
Running `target/debug/simple-log`
2015-06-05 17:08:57.814176 -04:00
File created!
$ cat log.txt
ITS ALIVE!!!$
它工作了!和语言作斗争真有意思,很顺利地把两个东西放在一起。
4 构建时间记录器
我们离写一个真正的、完整的、最终系统越来越近。我突然想起,我可以为这个代码写一些测试,但是不急,一会再说。
以下是这个函数应该做的事情:
- 给定一个文件名,
- 如果它不存在则创建它,然后打开这个文件。
- 创建一个时间日期字符串,
- 将这个字符串写入文件,然后关闭这个文件。
4.1 对 u8
的误解
我的第一次尝试:
extern crate chrono;
use std::io::prelude::*;
use std::fs::File;
use std::io;
use chrono::*;
fn log_time(filename: &'static str) -> io::Result<()> {
let local: DateTime<Local> = Local::now();
let time_str = local.format("%Y").to_string();
let mut f = try!(File::create(filename));
try!(f.write_all(time_str));
Ok(())
}
fn main() {
match log_time("log.txt") {
Ok(..) => println!("File created!"),
Err(..) => println!("Error: could not create file.")
}
}
=>
$ cargo run
Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
src/main.rs:13:22: 13:30 error: mismatched types:
expected `&[u8]`,
found `collections::string::String`
(expected &-ptr,
found struct `collections::string::String`) [E0308]
src/main.rs:13 try!(f.write_all(time_str));
^~~~~~~~
<std macros>:1:1: 6:48 note: in expansion of try!
src/main.rs:13:5: 13:33 note: expansion site
error: aborting due to previous error
Could not compile `simple-log`.
To learn more, run the command again with --verbose.
我知道 Rust 中有很多字符串类型1,看起来这里我需要另一种类型。我不知道怎么下手,所以我只能搜索一番。
我记得在 Rust 文档的某一部分中特别提到了字符串。查一查,它说,可以使用 &
符号实现从 String
到 &str
的转换。我感觉这不是我们需要的,因为它应该是 [u8]
与 &str
2 之间的类型冲突,让我们试试:
extern crate chrono;
use std::io::prelude::*;
use std::fs::File;
use std::io;
use chrono::*;
fn log_time(filename: &'static str) -> io::Result<()> {
let local: DateTime<Local> = Local::now();
let time_str = local.format("%Y").to_string();
let mut f = try!(File::create(filename));
try!(f.write_all(&time_str));
Ok(())
}
fn main() {
match log_time("log.txt") {
Ok(..) => println!("File created!"),
Err(..) => println!("Error: could not create file.")
}
}
=>
$ cargo run
Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
src/main.rs:13:22: 13:31 error: mismatched types:
expected `&[u8]`,
found `&collections::string::String`
(expected slice,
found struct `collections::string::String`) [E0308]
src/main.rs:13 try!(f.write_all(&time_str));
^~~~~~~~~
<std macros>:1:1: 6:48 note: in expansion of try!
src/main.rs:13:5: 13:34 note: expansion site
error: aborting due to previous error
Could not compile `simple-log`.
To learn more, run the command again with --verbose.
好吧,显然,添加 &
符号只能从 String
转换到 &String
。这似乎与 Rust 文档中所说的直相矛盾,但也可能是我不知道发生了什么事情。
……而且我刚刚读了字符串的章节的末尾。据我所知,这里没有任何东西。
我离开了一段时间去忙别的事情(家长里短,你懂),当我走的时候,我恍然大悟。在此之前,我一直以为 u8
是 UTF-8
的缩写,但是现在我仔细想想,它肯定是“无符号 8 位整数”的意思。而且我记得我看见过 as_bytes
方法,所以,我们试一下:
extern crate chrono;
use std::io::prelude::*;
use std::fs::File;
use std::io;
use chrono::*;
fn log_time(filename: &'static str) -> io::Result<()> {
let local: DateTime<Local> = Local::now();
let bytes = local.format("%Y").to_string().as_bytes();
let mut f = try!(File::create(filename));
try!(f.write_all(bytes));
Ok(())
}
fn main() {
match log_time("log.txt") {
Ok(..) => println!("File created!"),
Err(..) => println!("Error: could not create file.")
}
}
=>
$ cargo run
Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
main.rs:10:17: 10:47 error: borrowed value does not live long enough
main.rs:10 let bytes = local.format("%Y").to_string().as_bytes();
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
main.rs:10:59: 14:2 note: reference must be valid for the block suffix following statement 1 at 10:
58...
main.rs:10 let bytes = local.format("%Y").to_string().as_bytes();
main.rs:11 let mut f = try!(File::create(filename));
main.rs:12 try!(f.write_all(bytes));
main.rs:13 Ok(())
main.rs:14 }
main.rs:10:5: 10:59 note: ...but borrowed value is only valid for the statement at 10:4
main.rs:10 let bytes = local.format("%Y").to_string().as_bytes();
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
main.rs:10:5: 10:59 help: consider using a `let` binding to increase its lifetime
main.rs:10 let bytes = local.format("%Y").to_string().as_bytes();
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
error: aborting due to previous error
Could not compile `simple-log`.
To learn more, run the command again with --verbose.
好吧,我希望事情有所进展。这个错误是否意味着我修正了一些东西,而还有一些其他的错误掩盖了这个问题?我是不是遇到了一个全新的问题?
奇怪的是错误信息集中体现在同一行上。我并不是很明白,但我觉得它是想告诉我,我需要添加一个赋值语句在方法中。我们试一下:
fn log_time(filename: &'static str) -> io::Result<()> {
let local: DateTime<Local> = Local::now();
let formatted = local.format("%Y").to_string();
let bytes = formatted.as_bytes();
let mut f = try!(File::create(filename));
try!(f.write_all(bytes));
Ok(())
}
=>
$ cargo run
Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
Running `target/debug/simple-log`
File created!
$ cat log.txt
2015$
太棒了!我们想要的都在这了。在我继续之前,我想吐槽一下,我有一点失望。没有我的提示,Rust 也应该可以通过上下文推断正确的行为。
测试脚本:
$ ls
Cargo.lock Cargo.toml log.txt src target
$ rm log.txt
$ cargo run
Running `target/debug/simple-log`
File created!
$ cat log.txt
2015$ cargo run
Running `target/debug/simple-log`
File created!
$ cat log.txt
2015$
4.2 查缺补漏
一些问题:
- 没有另起一行,这忍不了。
- 格式需要一些处理。
- 新的日期会覆盖旧的。
Let's verify #3 by fixing the format. If the time changes between runs, then we will know that's what is happening.
DateTime
中的 format
方法使用标准 strftime 格式公约。理想情况下,我希望时间看起来像是这样的:
Sat, Jun 6 2015 05:32:00 PM
Sun, Jun 7 2015 08:35:00 AM
……等等。这可读性应该是足够的,供我使用。查阅文档后,我想出了这个:
extern crate chrono;
use std::io::prelude::*;
use std::fs::File;
use std::io;
use chrono::*;
fn log_time(filename: &'static str) -> io::Result<()> {
let local: DateTime<Local> = Local::now();
let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string();
let bytes = formatted.as_bytes();
let mut f = try!(File::create(filename));
try!(f.write_all(bytes));
Ok(())
}
fn main() {
match log_time("log.txt") {
Ok(..) => println!("File created!"),
Err(..) => println!("Error: could not create file.")
}
}
测试:
$ rm log.txt
$ cargo run
Running `target/debug/simple-log`
File created!
$ cat log.txt
Sun, Jun 07 2015 06:37:21 PM
$ sleep 5; cargo run
Running `target/debug/simple-log`
File created!
$ cat log.txt
Sun, Jun 07 2015 06:37:41 PM
显然,程序覆盖我想要的日志项。我记得 File::create
的文档中指出了这里发生的事。所以,为了正确处理文件我需要再次查阅文档。
我进行了一些搜索,基本上找到答案都是无关紧要的。随后,我找到了 std::path::Path 的文档,其中有一个 exists
模式。
此时,我的程序中的类型转换变得越来越难以管理。我感到紧张,所以继续之前,我要提交一次。
我想把对时间实体字符串的处理逻辑从 log_time
函数中抽取出来,因为时间的创建与格式化显然与文件操作代码不同。所以,我做了如下尝试:
extern crate chrono;
use std::io::prelude::*;
use std::fs::File;
use std::io;
use chrono::*;
fn log_time_entry() -> String {
let local: DateTime<Local> = Local::now();
let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string();
formatted
}
fn log_time(filename: &'static str) -> io::Result<()> {
let bytes = log_time_entry().as_bytes();
let mut f = try!(File::create(filename));
try!(f.write_all(bytes));
Ok(())
}
fn main() {
match log_time("log.txt") {
Ok(..) => println!("File created!"),
Err(..) => println!("Error: could not create file.")
}
}
=>
$ cargo run
Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
src/main.rs:16:17: 16:33 error: borrowed value does not live long enough
src/main.rs:16 let bytes = log_time_entry().as_bytes();
^~~~~~~~~~~~~~~~
src/main.rs:16:45: 20:2 note: reference must be valid for the block suffix following statement 0 at
16:44...
src/main.rs:16 let bytes = log_time_entry().as_bytes();
src/main.rs:17 let mut f = try!(File::create(filename));
src/main.rs:18 try!(f.write_all(bytes));
src/main.rs:19 Ok(())
src/main.rs:20 }
src/main.rs:16:5: 16:45 note: ...but borrowed value is only valid for the statement at 16:4
src/main.rs:16 let bytes = log_time_entry().as_bytes();
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/main.rs:16:5: 16:45 help: consider using a `let` binding to increase its lifetime
src/main.rs:16 let bytes = log_time_entry().as_bytes();
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
error: aborting due to previous error
Could not compile `simple-log`.
To learn more, run the command again with --verbose.
好吧,这看起来就像我以前遇到的问题。是不是假借或持有要求函数拥有明确的资源引用?这似乎有一点奇怪。我再次尝试修复它:
extern crate chrono;
use std::io::prelude::*;
use std::fs::File;
use std::io;
use chrono::*;
fn formatted_time_entry() -> String {
let local: DateTime<Local> = Local::now();
let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string();
formatted
}
fn log_time(filename: &'static str) -> io::Result<()> {
let entry = formatted_time_entry();
let bytes = entry.as_bytes();
let mut f = try!(File::create(filename));
try!(f.write_all(bytes));
Ok(())
}
fn main() {
match log_time("log.txt") {
Ok(..) => println!("File created!"),
Err(..) => println!("Error: could not create file.")
}
}
=>
$ cargo run
Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
Running `target/debug/simple-log`
File created!
所以,看起来添加一个明确的引用解决了问题。不管怎样,这个规则还蛮简单。
下面,我要将文件操作代码抽取至它自己的函数:
extern crate chrono;
use std::io::prelude::*;
use std::fs::File;
use std::io;
use chrono::*;
fn formatted_time_entry() -> String {
let local: DateTime<Local> = Local::now();
let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string();
formatted
}
fn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {
let mut f = try!(File::create(filename));
try!(f.write_all(bytes));
Ok(())
}
fn log_time(filename: &'static str) -> io::Result<()> {
let entry = formatted_time_entry();
let bytes = entry.as_bytes();
try!(record_entry_in_log(filename, &bytes));
Ok(())
}
fn main() {
match log_time("log.txt") {
Ok(..) => println!("File created!"),
Err(..) => println!("Error: could not create file.")
}
}
它正常工作。我犯了一些一开始的错误,但它们很快被纠正了。这里已经是修改后的代码。
查阅文档中的 std::fs::File,我注意到文档对 std::fs::OpenOptions 的介绍,这正是我一直在寻找的。这肯定比使用 std::path
更好。
我的第一次尝试:
extern crate chrono;
use std::io::prelude::*;
use std::fs::{File,OpenOptions};
use std::io;
use chrono::{DateTime,Local};
fn formatted_time_entry() -> String {
let local: DateTime<Local> = Local::now();
let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string();
formatted
}
fn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {
let mut file = try!(OpenOptions::new().
append(true).
create(true).
open(filename));
try!(file.write_all(bytes));
Ok(())
}
fn log_time(filename: &'static str) -> io::Result<()> {
let entry = formatted_time_entry();
let bytes = entry.as_bytes();
try!(record_entry_in_log(filename, &bytes));
Ok(())
}
fn main() {
match log_time("log.txt") {
Ok(..) => println!("File created!"),
Err(..) => println!("Error: could not create file.")
}
}
=>
$ cargo run
Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
src/main.rs:4:15: 4:19 warning: unused import, #[warn(unused_imports)] on by default
src/main.rs:4 use std::fs::{File,OpenOptions};
^~~~
Running `target/debug/simple-log`
Error: could not create file.
有趣。其实它成功创建文件了。哦,我注意错误提示是我硬编码到 main
的信息。我认为这样它将工作:
extern crate chrono;
use std::io::prelude::*;
use std::fs::{File,OpenOptions};
use std::io;
use chrono::{DateTime,Local};
fn formatted_time_entry() -> String {
let local: DateTime<Local> = Local::now();
let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string();
formatted
}
fn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {
let mut file = try!(OpenOptions::new().
append(true).
create(true).
open(filename));
try!(file.write_all(bytes));
Ok(())
}
fn log_time(filename: &'static str) -> io::Result<()> {
let entry = formatted_time_entry();
let bytes = entry.as_bytes();
try!(record_entry_in_log(filename, &bytes));
Ok(())
}
fn main() {
match log_time("log.txt") {
Ok(..) => println!("File created!"),
Err(e) => println!("Error: {}", e)
}
}
=>
$ cargo run
Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
src/main.rs:4:15: 4:19 warning: unused import, #[warn(unused_imports)] on by default
src/main.rs:4 use std::fs::{File,OpenOptions};
^~~~
Running `target/debug/simple-log`
Error: Bad file descriptor (os error 9)
奇怪。搜索“非法的文件描述”错误信息似乎表明,被使用的文件描述已经被关闭了。如果我注释掉 file.write_all
调用,将会发生什么呢?
$ rm log.txt
$ cargo run
Compiling simple-log v0.1.0 (file:///Users/joel/Projects/simple-log)
src/main.rs:3:5: 3:25 warning: unused import, #[warn(unused_imports)] on by default
src/main.rs:3 use std::io::prelude::*;
^~~~~~~~~~~~~~~~~~~~
src/main.rs:4:15: 4:19 warning: unused import, #[warn(unused_imports)] on by default
src/main.rs:4 use std::fs::{File,OpenOptions};
^~~~
src/main.rs:15:40: 15:45 warning: unused variable: `bytes`, #[warn(unused_variables)] on by default
src/main.rs:15 fn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {
^~~~~
src/main.rs:16:9: 16:17 warning: unused variable: `file`, #[warn(unused_variables)] on by default
src/main.rs:16 let mut file = try!(OpenOptions::new().
^~~~~~~~
src/main.rs:16:9: 16:17 warning: variable does not need to be mutable, #[warn(unused_mut)] on by de
fault
src/main.rs:16 let mut file = try!(OpenOptions::new().
^~~~~~~~
Running `target/debug/simple-log`
File created!
$ ls
Cargo.lock Cargo.toml log.txt src target
不出所料,有一堆未使用的警告信息,但是无他,文件的确被创建了。
这似乎有点傻,但我尝试向函数调用链中添加 .write(true)
后,它工作了。语义上 .append(true)
就意味着 .write(true)
,但我想规定上不是这样的。
搞定了这个,它工作了!最终版本:
extern crate chrono;
use std::io::prelude::*;
use std::fs::{File,OpenOptions};
use std::io;
use chrono::{DateTime,Local};
fn formatted_time_entry() -> String {
let local: DateTime<Local> = Local::now();
let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string();
formatted
}
fn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {
let mut file = try!(OpenOptions::new().
append(true).
write(true).
create(true).
open(filename));
try!(file.write_all(bytes));
Ok(())
}
fn log_time(filename: &'static str) -> io::Result<()> {
let entry = formatted_time_entry();
let bytes = entry.as_bytes();
try!(record_entry_in_log(filename, &bytes));
Ok(())
}
fn main() {
match log_time("log.txt") {
Ok(..) => println!("File created!"),
Err(e) => println!("Error: {}", e)
}
}
=>
$ ls
Cargo.lock Cargo.toml src target
$ cargo run
Running `target/debug/simple-log`
File created!
$ cargo run
Running `target/debug/simple-log`
File created!
$ cat log.txt
Sun, Jun 07 2015 10:40:01 PM
Sun, Jun 07 2015 10:40:05 PM
5 结论 & 后续步骤
Rust 对我来说越来越容易了。我现在有一些有效的、单功能的代码可以使用,我对下一部分程序的开发感到相当有信心。
当我首次规划这个系列的时候,我计划下一个任务是整合日志代码和 nickel.rs
代码,但是现在,我认为这是非常简单的。我猜测,下一个有挑战的部分将是处理选项解析。
—
系列文章:使用 Rust 开发一个简单的 Web 应用
脚注:
1 有很多种类的字符串是非常合理的事情。字符串是一个复杂的实体,很难得到正确的表达。不幸的是,乍一看字符串非常简单,这种事情似乎没必要复杂。
2 我也不知道我在说什么。这些就是现在所能企及的。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。