Rfe 010:工程师(粗)试图做一点没用的有用项目
没用还是有用
在学习一种程序设计语言的时候, 我们通常有两种张力很强、相互拉扯的焦虑, 如何用这门语言做点什么有用的东西, 一直在学用这门语言做没用的东西。 所以那个《XXX in Action》系列的书籍才会有卖得那么好…… 可是,那个书里依然都是一些有用的没用东西。 到底是有用的没用东西,还是没用的有用东西呢? 实际上,有用的东西多半没用,而没用的东西多半很有用。
有无相生,
难易相成。
还有一个就是有用的东西很难学,而没用的东西很容易学。 最出奇的事情,有些时候,难学的东西居然很没用,而容易学的东西却很有用。
所以,说一千道一万,我们还是先从没用的东西开始学吧。
没用的有用项目
Checklist
先来看一个例子,我想做一个非常有用的项目。 因为本人极其没有组织度, 做事情,做前面忘后面, 做后面忘前面。 后来我好好读过《Checklist Manifesto》这本书, 才知道原来有一个东西叫做 checklist, 专治我们这种毛病,一劳永逸……才怪。
于是,我就想做一个 checklist 的管理器, 来帮助我管理我的 checklist。 我觉得有这种毛病的肯定不是我一个人, 所以我想做一个大家都能够访问的 checklist 管理器, 来帮助大家管理 checklist。
想要说这个项目没用的人请不要说, 因为你们没有这个毛病,不能理解我们的痛苦。
需求分析
那就开始进行需求分析,需要的就是能够简单管理 checklist,能够随时随地访问 checklist,并且能够提供工具让我对照着checklist来完成一项工作。
核心的信息:checklist,checklist可以重复使用,比如我实际上已经编过一个checklist程序, 那是我在Windtunnel.cn写文章时所需要的步骤。

# 写hugo文章: 用github管理hugo项目
更新本地库,git pull
新建一个分支来工作, git check -b branch_name
撰写文章,包括代码和图形, hugo new posts/artical_name.md
中途多次提交 commit 并 push 到服务器
把文章的 draft 标志设置为 false
commit 所有工作
把分支切换到主分支
合并工作分支
push 代码库,使 GitHub Action 渲染新文章
大概就是这样的,我以前的问题是,git push之后才发现,文章的 draft 标志没有设置为 false,导致文章没有被渲染出来。
同样的信息,如果用比较流行的的格式来表示可能就是:
1{
2 "id": 2,
3 "title": "写hugo文章",
4 "description": "用github管理hugo项目",
5 "created_at": "2026-08-19T00:39:49.202487043Z",
6 "items": [
7 {
8 "id": 3,
9 "step": "更新本地库,git pull"
10 },
11 {
12 "id": 4,
13 "step": "新建一个分支来工作, git check -b branch_name"
14 },
15 {
16 "id": 5,
17 "step": "撰写文章,包括代码和图形, hugo new posts/artical_name.md"
18 },
19 {
20 "id": 6,
21 "step": "中途多次提交 commit 并 push 到服务器"
22 },
23 {
24 "id": 7,
25 "step": "把文章的 draft 标志设置为 false"
26 },
27 {
28 "id": 8,
29 "step": "commit 所有工作"
30 },
31 {
32 "id": 9,
33 "step": "把分支切换到主分支"
34 },
35 {
36 "id": 10,
37 "step": "合并工作分支"
38 },
39 {
40 "id": 11,
41 "step": "push 代码库,使 GitHub Action 渲染新文章"
42 }
43 ]
44}
核心的信息分析就是这个样子。
其次,需要确定信息如何收集、如何存储、如何访问、如何使用。 这个部分大概就是架构设计,可以用 Rust 来实现一个 RESTful API, 用 JavaScript 来实现一个网页。 当然应该也非常容易实现一个手机端,估计也就是稍微搞下、几百个Token的事情。
OpenAPI 文档:http://8.152.97.118:8091/api/docs, 随便实现一个单页面前端 Checklist Manager
实际上,有这个RESTful API,我们在这里就内嵌一个随机显示checklist。
从 API_BASE 拉取随机 checklist
随机显示一个 checklist,里面的每一项都是一个 step,用户可以点击 checkbox 来完成每一项。每一项的 checkbox 只能在前一项完成后才能解锁。
点击按钮后,会从 API_BASE/api/checklists/random 拉取随机清单,并在这里渲染 checkbox。
随机一个checklist的接口是 /api/checklists/random,返回值是一个 checklist 对象,里面的 items 是数组,每一项的字段是 step。
架构和代码
架构
在实现一个信息化系统的初期, 我们总是会优先采用比较简单的技术来实现一系列松耦合的必要组件, 然后再把这些必不可少的组件组合起来, 形成一个功能上完整的系统。 至于这个系统的性能, 后面如果这个系统还能拿到钱的话, 再逐一替换就行。
对于我们的 checklist 管理器来说,我们可以把它拆分为三个组件:
- 采用sqlite数据库来存储checklist数据
- 提供一个RESTful API来访问这些数据
- 提供一个网页(HTML/CSS/Javascript)来访问这些数据
大概上来说,就是这样几个层次,四个部分。

在实际的实现中,前两个部分被整合在一起,形成一个完整的服务端程序,提供RESTful API。 后两个部分甚至被放在一个文件中(非常不好的做法),形成一个完整的前端网页,提供用户界面。

代码构成
在实现时,按照3个工程来组织代码,代码都可以在Github上找到。
- checklist-core:提供checklist的核心数据结构和操作
- checklist-server:提供checklist的Restful API
- checklist-ui:提供checklist的网页用户界面
Checklist-core
首先是 checklist-core 的代码,主要是提供ChecklistItem和Checklist两个结构体,以及它们的序列化和反序列化方法。通过serde库,我们可以很方便地将它们序列化为JSON格式,或者从JSON格式反序列化回来。当然,我们为了方便,也提供了从文本格式(类似Markdown的格式)来创建Checklist的功能。
1/**
2 * A single checklist step persisted as a row in the `checklist_items` table.
3 */
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
5pub struct ChecklistItem {
6 #[serde(default)]
7 pub id: i64,
8 pub step: String,
9 #[serde(skip_serializing, skip_deserializing, default = "default_ip")]
10 pub created_by_ip: String,
11}
12
13impl ChecklistItem {
14 /// Creates a new checklist step with the caller-supplied creator IP for auditability.
15 pub fn new(step: impl Into<String>, created_by_ip: impl Into<String>) -> Self {
16 Self {
17 id: 0,
18 step: step.into(),
19 created_by_ip: created_by_ip.into(),
20 }
21 }
22}
23
24/**
25 * A complete checklist, including metadata and all step entries.
26 *
27 * A checklist can be created from API payloads, imported from text, or serialized for
28 * export and import flows.
29 */
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
31pub struct Checklist {
32 #[serde(default)]
33 pub id: i64,
34 pub title: String,
35 pub description: String,
36 pub created_at: DateTime<Utc>,
37 #[serde(default)]
38 pub items: Vec<ChecklistItem>,
39 #[serde(skip_serializing, skip_deserializing, default = "default_ip")]
40 pub created_by_ip: String,
41 #[serde(default, skip_serializing, skip_deserializing)]
42 pub source_id: Option<i64>,
43}
44
45impl Checklist {
46 /// Creates a new checklist from a title, description, and raw item list.
47 pub fn new(
48 title: impl Into<String>,
49 description: impl Into<String>,
50 items: Vec<String>,
51 created_by_ip: impl Into<String>,
52 ) -> Self {
53 let created_by_ip = created_by_ip.into();
54 Self {
55 id: 0,
56 title: title.into(),
57 description: description.into(),
58 created_at: Utc::now(),
59 items: items
60 .into_iter()
61 .map(|step| ChecklistItem::new(step, created_by_ip.clone()))
62 .collect(),
63 created_by_ip,
64 source_id: None,
65 }
66 }
67
68 /// Serializes the checklist into pretty-printed JSON for export or API use.
69 pub fn to_json(&self) -> Result<String> {
70 Ok(serde_json::to_string_pretty(self)?)
71 }
72
73 /// Parses a checklist from JSON produced by `to_json()` or another compatible payload.
74 pub fn from_json(input: &str) -> Result<Self> {
75 Ok(serde_json::from_str::<Checklist>(input)?)
76 }
77
78 pub fn to_packed_string(&self) -> Result<String> {
79 let json = self.to_json()?;
80 Ok(BASE64.encode(json.as_bytes()))
81 }
82
83 pub fn from_packed_string(input: &str) -> Result<Self> {
84 let decoded = BASE64.decode(input)?;
85 let json = String::from_utf8(decoded)?;
86 Self::from_json(&json)
87 }
88
89 pub fn from_text(input: &str) -> Result<Self> {
90 let mut lines = input.lines();
91 let header = lines
92 .next()
93 .ok_or_else(|| ChecklistError::InvalidInput("missing checklist header".to_string()))?
94 .trim();
95
96 if !header.starts_with('#') {
97 return Err(ChecklistError::InvalidInput(
98 "header must start with '# '".to_string(),
99 ));
100 }
101
102 let header = header.trim_start_matches('#').trim();
103 let (title, description) = match header.split_once(':') {
104 Some((title, description)) => (title.trim(), description.trim()),
105 None => (header.trim(), ""),
106 };
107
108 if title.is_empty() {
109 return Err(ChecklistError::InvalidInput(
110 "title must not be empty".to_string(),
111 ));
112 }
113
114 let items = lines
115 .filter_map(|line| {
116 let line = line.trim();
117 if line.is_empty() {
118 None
119 } else {
120 Some(line.to_string())
121 }
122 })
123 .collect::<Vec<_>>();
124
125 Ok(Self::new(title, description, items, "0.0.0.0"))
126 }
127
128 pub fn to_text(&self) -> String {
129 let description = if self.description.trim().is_empty() {
130 String::new()
131 } else {
132 format!(":{}", self.description)
133 };
134
135 let mut out = format!("# {}{}\n", self.title, description);
136 for item in &self.items {
137 out.push_str(&item.step);
138 out.push('\n');
139 }
140 out
141 }
142}
然后是一个非常Naive的Sqlite数据库访问层,提供了对checklist的增删改查操作。
1/**
2 * SQLite-backed persistence layer for checklists and their steps.
3 *
4 * All reads and writes are centralized here to keep the application logic testable and
5 * consistent across API routes.
6 */
7pub struct ChecklistStore {
8 conn: Connection,
9 database_path: String,
10}
11
12impl ChecklistStore {
13 /// Opens or creates the SQLite database and initializes the required schema.
14 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
15 let database_path = path.as_ref().to_string_lossy().to_string();
16 let conn = Connection::open(path)?;
17 let store = Self {
18 conn,
19 database_path,
20 };
21 store.initialize()?;
22 Ok(store)
23 }
24
25 pub fn database_path(&self) -> &str {
26 &self.database_path
27 }
28
29 fn initialize(&self) -> Result<()> {
30 self.conn.execute_batch(
31 "
32 CREATE TABLE IF NOT EXISTS checklists (
33 id INTEGER PRIMARY KEY AUTOINCREMENT,
34 title TEXT NOT NULL,
35 description TEXT NOT NULL,
36 created_at TEXT NOT NULL,
37 created_by_ip TEXT NOT NULL,
38 source_id INTEGER
39 );
40
41 CREATE TABLE IF NOT EXISTS checklist_items (
42 id INTEGER PRIMARY KEY AUTOINCREMENT,
43 checklist_id INTEGER NOT NULL,
44 step TEXT NOT NULL,
45 created_by_ip TEXT NOT NULL,
46 FOREIGN KEY(checklist_id) REFERENCES checklists(id)
47 );
48 ",
49 )?;
50 Ok(())
51 }
52
53 pub fn create_checklist(&self, mut checklist: Checklist) -> Result<Checklist> {
54 if checklist.title.trim().is_empty() {
55 return Err(ChecklistError::InvalidInput(
56 "title must not be empty".to_string(),
57 ));
58 }
59
60 let created_at = checklist.created_at.to_rfc3339();
61 let created_by_ip = checklist.created_by_ip.clone();
62
63 self.conn.execute(
64 "INSERT INTO checklists (title, description, created_at, created_by_ip, source_id) VALUES (?1, ?2, ?3, ?4, ?5)",
65 params![
66 checklist.title,
67 checklist.description,
68 created_at,
69 created_by_ip,
70 checklist.source_id,
71 ],
72 )?;
73
74 let checklist_id = self.conn.last_insert_rowid();
75 checklist.id = checklist_id;
76
77 for item in &mut checklist.items {
78 self.conn.execute(
79 "INSERT INTO checklist_items (checklist_id, step, created_by_ip) VALUES (?1, ?2, ?3)",
80 params![checklist_id, item.step, item.created_by_ip.clone()],
81 )?;
82 item.id = self.conn.last_insert_rowid();
83 }
84
85 Ok(checklist)
86 }
87
88 /// Returns the newest checklists ordered by creation time.
89 pub fn list_checklists(&self) -> Result<Vec<Checklist>> {
90 self.list_checklists_limited(None)
91 }
92
93 /// Returns a capped subset of the newest checklists. The cap is intentionally enforced
94 /// here so the API layer and the UI share a consistent upper bound.
95 pub fn list_checklists_limited(&self, limit: Option<usize>) -> Result<Vec<Checklist>> {
96 let limit = limit
97 .unwrap_or(DEFAULT_RESULT_LIMIT)
98 .min(DEFAULT_RESULT_LIMIT);
99 let mut stmt = self.conn.prepare(
100 "SELECT id, title, description, created_at, created_by_ip, source_id FROM checklists ORDER BY created_at DESC LIMIT ?1",
101 )?;
102
103 let rows = stmt.query_map([limit as i64], |row| {
104 Ok((
105 row.get::<_, i64>(0)?,
106 row.get::<_, String>(1)?,
107 row.get::<_, String>(2)?,
108 row.get::<_, String>(3)?,
109 row.get::<_, String>(4)?,
110 row.get::<_, Option<i64>>(5)?,
111 ))
112 })?;
113
114 let mut checklists = Vec::new();
115 for row in rows {
116 let (id, title, description, created_at, created_by_ip, source_id) = row?;
117 let created_at = DateTime::parse_from_rfc3339(&created_at)
118 .map_err(|err| ChecklistError::InvalidInput(err.to_string()))?
119 .with_timezone(&Utc);
120
121 let items = self.load_items_for_checklist(id)?;
122 checklists.push(Checklist {
123 id,
124 title,
125 description,
126 created_at,
127 items,
128 created_by_ip,
129 source_id,
130 });
131 }
132
133 Ok(checklists)
134 }
135
136 pub fn get_checklist(&self, id: i64) -> Result<Option<Checklist>> {
137 let mut stmt = self.conn.prepare(
138 "SELECT title, description, created_at, created_by_ip, source_id FROM checklists WHERE id = ?1",
139 )?;
140
141 let row = stmt
142 .query_row([id], |row| {
143 Ok((
144 row.get::<_, String>(0)?,
145 row.get::<_, String>(1)?,
146 row.get::<_, String>(2)?,
147 row.get::<_, String>(3)?,
148 row.get::<_, Option<i64>>(4)?,
149 ))
150 })
151 .optional()?;
152
153 let Some((title, description, created_at, created_by_ip, source_id)) = row else {
154 return Ok(None);
155 };
156
157 let created_at = DateTime::parse_from_rfc3339(&created_at)
158 .map_err(|err| ChecklistError::InvalidInput(err.to_string()))?
159 .with_timezone(&Utc);
160
161 let items = self.load_items_for_checklist(id)?;
162
163 Ok(Some(Checklist {
164 id,
165 title,
166 description,
167 created_at,
168 items,
169 created_by_ip,
170 source_id,
171 }))
172 }
173
174 pub fn create_new_version_from(
175 &self,
176 source_id: i64,
177 checklist: Checklist,
178 ) -> Result<Checklist> {
179 if self.get_checklist(source_id)?.is_none() {
180 return Err(ChecklistError::NotFound(source_id));
181 }
182
183 let mut version = checklist;
184 version.source_id = Some(source_id);
185 self.create_checklist(version)
186 }
187
188 /// Searches over checklist titles, descriptions, and item text.
189 pub fn search_by_term(&self, term: &str) -> Result<Vec<Checklist>> {
190 self.search_by_term_limited(term, None)
191 }
192
193 /// Searches with a safe server-side cap to keep result payloads predictable.
194 pub fn search_by_term_limited(
195 &self,
196 term: &str,
197 limit: Option<usize>,
198 ) -> Result<Vec<Checklist>> {
199 let limit = limit
200 .unwrap_or(DEFAULT_RESULT_LIMIT)
201 .min(DEFAULT_RESULT_LIMIT);
202 let needle = format!("%{}%", term.trim());
203 let mut stmt = self.conn.prepare(
204 "SELECT c.id, c.title, c.description, c.created_at, c.created_by_ip, c.source_id
205 FROM checklists c
206 LEFT JOIN checklist_items i ON i.checklist_id = c.id
207 WHERE c.title LIKE ?1 OR c.description LIKE ?1 OR i.step LIKE ?1
208 GROUP BY c.id
209 ORDER BY c.created_at DESC
210 LIMIT ?2",
211 )?;
212
213 let rows = stmt.query_map(params![&needle, limit as i64], |row| {
214 Ok((
215 row.get::<_, i64>(0)?,
216 row.get::<_, String>(1)?,
217 row.get::<_, String>(2)?,
218 row.get::<_, String>(3)?,
219 row.get::<_, String>(4)?,
220 row.get::<_, Option<i64>>(5)?,
221 ))
222 })?;
223
224 let mut results = Vec::new();
225 for row in rows {
226 let (id, title, description, created_at, created_by_ip, source_id) = row?;
227 let created_at = DateTime::parse_from_rfc3339(&created_at)
228 .map_err(|err| ChecklistError::InvalidInput(err.to_string()))?
229 .with_timezone(&Utc);
230 let items = self.load_items_for_checklist(id)?;
231 results.push(Checklist {
232 id,
233 title,
234 description,
235 created_at,
236 items,
237 created_by_ip,
238 source_id,
239 });
240 }
241
242 Ok(results)
243 }
244
245 pub fn random_checklist(&self) -> Result<Option<Checklist>> {
246 let row = self
247 .conn
248 .query_row(
249 "SELECT id FROM checklists ORDER BY RANDOM() LIMIT 1",
250 [],
251 |row| row.get::<_, i64>(0),
252 )
253 .optional()?;
254
255 match row {
256 Some(id) => self.get_checklist(id),
257 None => Ok(None),
258 }
259 }
260
261 pub fn delete_checklist(&self, id: i64) -> Result<()> {
262 self.conn
263 .execute("DELETE FROM checklist_items WHERE checklist_id = ?1", [id])?;
264 self.conn
265 .execute("DELETE FROM checklists WHERE id = ?1", [id])?;
266 Ok(())
267 }
268
269 fn load_items_for_checklist(&self, checklist_id: i64) -> Result<Vec<ChecklistItem>> {
270 let mut stmt = self.conn.prepare(
271 "SELECT id, step, created_by_ip FROM checklist_items WHERE checklist_id = ?1 ORDER BY id ASC",
272 )?;
273
274 let rows = stmt.query_map([checklist_id], |row| {
275 Ok((
276 row.get::<_, i64>(0)?,
277 row.get::<_, String>(1)?,
278 row.get::<_, String>(2)?,
279 ))
280 })?;
281
282 let mut items = Vec::new();
283 for row in rows {
284 let (id, step, created_by_ip) = row?;
285 items.push(ChecklistItem {
286 id,
287 step,
288 created_by_ip,
289 });
290 }
291
292 Ok(items)
293 }
294}
checklist-server
这个服务器的代码就过于简单,用axum来实现,主要的代码量都在描述RESTful API的OpenAPI文档上了。
1fn app(state: AppState) -> Router {
2 let cors = CorsLayer::new()
3 .allow_origin(Any)
4 .allow_methods([Method::GET, Method::POST, Method::DELETE, Method::OPTIONS])
5 .allow_headers(Any);
6
7 Router::new()
8 .route(
9 "/api/checklists",
10 get(list_checklists).post(create_checklist_handler),
11 )
12 .route("/api/checklists/search", get(search_checklists_handler))
13 .route("/api/checklists/random", get(random_checklist_handler))
14 .route(
15 "/api/checklists/{id}",
16 get(get_checklist_handler).delete(delete_checklist_handler),
17 )
18 .route(
19 "/api/checklists/{id}/export.json",
20 get(export_checklist_handler),
21 )
22 .route("/api/checklists/template", get(template_checklist_handler))
23 .route(
24 "/api/checklists/from-text",
25 post(create_checklist_from_text_handler),
26 )
27 .route("/api/checklists/import", post(import_checklist_handler))
28 .route("/api/checklists/pack", post(pack_checklist_handler))
29 .route("/api/checklists/unpack", post(unpack_checklist_handler))
30 .with_state(state)
31 .layer(cors)
32 .layer(TraceLayer::new_for_http())
33}
这个就是主要的API路由了,其他的就是一些辅助函数和错误处理。
主函数被标记为#[tokio::main],使用tokio运行时来运行异步的axum服务器。
1#[tokio::main]
2async fn main() {
3 let cli = Cli::parse();
4 let store = Arc::new(Mutex::new(
5 ChecklistStore::open(&cli.database).expect("failed to open checklist database"),
6 ));
7
8 let app = app(AppState {
9 store: store.clone(),
10 });
11 let addr: SocketAddr = cli.bind.parse().expect("invalid bind address");
12
13 let docs = openapi_document();
14 let docs_for_json = docs.clone();
15 let router = app
16 .route("/api/docs", get(|| async { Html(swagger_ui_page()) }))
17 .route(
18 "/api-docs/openapi.json",
19 get(move || async move { Json(docs_for_json) }),
20 );
21
22 println!("Checklist server listening on http://{}", addr);
23 let listener = tokio::net::TcpListener::bind(addr)
24 .await
25 .expect("failed to bind to socket");
26 axum::serve(listener, router).await.expect("server failed");
27}
checklist-ui
这个网页的代码就更简单了,主要是一个HTML文件,里面包含了一些CSS和JavaScript代码,用来渲染checklist的界面。当然我们同样用一个tokio的服务器来提供这个网页。
1#[derive(Clone)]
2struct AppState {
3 api_base: String,
4}
5
6fn ui_html(api_base: &str) -> String {
7 let safe_api_base = api_base.replace('\\', "\\\\").replace('"', "\\\"");
8 include_str!("../static/index.html")
9 .replace("__API_BASE__", &safe_api_base)
10}
11
12async fn index(State(state): State<AppState>) -> Html<String> {
13 Html(ui_html(&state.api_base))
14}
15
16#[tokio::main]
17async fn main() {
18 let cli = Cli::parse();
19 let state = AppState {
20 api_base: cli.api_base,
21 };
22
23 let app = Router::new()
24 .route("/", get(index))
25 .layer(TraceLayer::new_for_http())
26 .with_state(state);
27
28 let addr: SocketAddr = cli.bind.parse().expect("invalid bind address");
29 println!("Checklist UI listening on http://{}", addr);
30 let listener = tokio::net::TcpListener::bind(addr)
31 .await
32 .expect("failed to bind UI port");
33 axum::serve(listener, app).await.expect("UI server failed");
34}
我们的网页服务器跟checklist服务器是分开的,网页服务器只提供一个HTML文件,里面包含了JavaScript代码来访问checklist服务器的API。
运行与配置
这两个单可执行的文件就能够运行整个系统,在实际运行中,可以通过命令行参数来指定checklist服务器的API地址和网页服务器的绑定地址。
1#[derive(Parser, Debug)]
2#[command(author, version, about = "Checklist server")]
3struct Cli {
4 #[arg(long, default_value = "./checklists.db")]
5 database: PathBuf,
6 #[arg(long, default_value = "127.0.0.1:3000")]
7 bind: String,
8}
1#[derive(Parser, Debug)]
2#[command(author, version, about = "Checklist UI")]
3struct Cli {
4 #[arg(long, default_value = "http://127.0.0.1:3000")]
5 api_base: String,
6 #[arg(long, default_value = "0.0.0.0:8090")]
7 bind: String,
8}
从这里看,rust中的clap库是非常方便的,可以很容易地定义命令行参数和选项。
实际运行中,需要确保防火墙开放两个端口,一个是checklist服务器的端口,另一个是网页服务器的端口。
1checklist-server --database ./checklists.db --bind 0.0.0.0:8092
2checklist-ui --api-base http://xxxx.xxxx.xxxx.xxxx:8092 --bind 0.0.0.0:8090
实际上,前端和后端之间的联系是非常简单的,前端只需要知道后端的API地址,就可以通过HTTP请求来访问checklist的数据。具体的API接口和返回的数据格式,可以通过访问/api/docs来查看OpenAPI文档。
结论
Rust的生态系统已经非常成熟,能够支持我们快速地开发一个完整的Web应用程序。 什么性能、什么安全性、什么并发性,在信息系统的初期阶段都不是最重要的, 最重要的是能够快速地实现一个可用的系统,能够满足用户的需求。 说真的,那些说Rust不香的人,我也不知道他们在说什么。
文章标签
|-->rust |-->rfe |-->网络服务 |-->restful |-->web |-->api |-->http |-->json
- 本站总访问量:loading次
- 本站总访客数:loading人
- 可通过邮件联系作者:Email大福
- 也可以访问技术博客:大福是小强
- 也可以在知乎搞抽象:知乎-大福
- Comments, requests, and/or opinions go to: Github Repository