用 rust 从零开发一套 web 框架:day2
修炼内丹
前面有了基础的框架,现在进行AppContext
上下文拓展。要如何设计AppContext
结构?为了把扩展性和复杂性留在了内部,而对外简化了接口。请求参数、响应体、路由的处理函数,以及将要实现的中间件等,参数都统一使用 AppContext
实例, AppContext
就像一次会话的百宝箱,可以找到任何东西。
//上下文参数
#[derive(Default, Debug)]
pub struct AppContext {
//请求头
headers: HashMap<String, String>,
//params
params: HashMap<String, String>,
//query
queries: HashMap<String, String>,
// form表单提交的数据
forms: HashMap<String, String>,
//响应内容
pub response: Response<Body>,
//路径
path: String,
//方法
method: Method,
}
同时,为AppContext
实例增加获取内部数据的接口,可以获取query
、param
、header
、form
等请求参数。
///获取AppContext内部参数
impl AppContext {
/// 获取query参数值
pub fn query<T>(&self, name: &str) -> Option<T>
where
T: FromStr,
<T as FromStr>::Err: Debug,
{
self.queries.get(name.into()).map(|v| v.as_str().parse().unwrap())
}
/// 获取param参数值
pub fn param<T>(&self, name: &str) -> Option<T>
where
T: FromStr,
<T as FromStr>::Err: Debug,
{
self.params.get(name.into()).map(|v| v.as_str().parse().unwrap())
}
/// 获取请求头中的参数值
pub fn header(&self, name: &str) -> Option<String> {
self.headers
.get(name.to_lowercase().as_str())
.map(|v| v.as_str().parse().unwrap())
}
/// 获取 form 表单提交值
pub fn form<T>(&self, name: &str) -> Option<T>
where
T: FromStr,
<T as FromStr>::Err: Debug,
{
self.forms.get(name.into()).map(|v| v.as_str().parse().unwrap())
}
}
为了快速对响应体进行处理,我们也要提供构造 String/JSON/HTML 响应的方法:
///处理response响应体
impl AppContext {
/// 设置响应头信息
pub fn set_header(&mut self, name: &str, value: &str) {
let headers = self.response.headers_mut();
headers.insert(
HeaderName::from_str(name).unwrap(),
HeaderValue::from_str(value).unwrap(),
);
}
///返回json格式数据
pub fn json<T>(&mut self, json: T)
where
T: Serialize,
{
let data = serde_json::to_string(&json).unwrap();
self.set_header(header::CONTENT_TYPE.as_str(), "application/json; charset=utf-8");
*self.response.body_mut() = Body::from(data.to_string());
}
///返回string
pub fn string(&mut self, code: Option<StatusCode>, data: &str) {
//默认状态码为200
if let Some(status) = code {
*self.response.status_mut() = status;
} else {
*self.response.status_mut() = StatusCode::OK;
};
self.set_header(header::CONTENT_TYPE.as_str(), "text/plain; charset=utf-8");
*self.response.body_mut() = Body::from(data.to_string());
}
///返回string
pub fn html<T>(&mut self, data: &str) {
self.set_header(header::CONTENT_TYPE.as_str(), "text/html; charset=utf-8");
*self.response.body_mut() = Body::from(data.to_string());
}
}
注意:这里处理 json 格式使用了 serde
和serde_json
这些json
库,记得添加该库并导入Deserialize
, Serialize
这些特征。
最后,在 handler
函数中初始化AppContext
并进行赋值。现在,我们还没做请求参数的解析,暂时把请求头、method 等内容进行赋值。
async fn handler(addr: SocketAddr, req: Request<Body>, router: Arc<Router>) -> Result<Response<Body>, Infallible> {
// 每次请求到来就生成一个context,
// 他包含着请求信息,传递给每个处理函数处理之后
// 把结果保存在Context中,最后返回给客户端
let mut context = AppContext::default();
context.method = req.method().clone().into();
for (k, v) in req.headers() {
context
.headers
.insert(k.to_string().to_lowercase(), v.to_str().unwrap().to_string());
}
let key = format!("{}+{}", req.uri().path(), req.method().as_str());
if let Some(handle) = router.routes.get(&key) {
println!("AppContext:{:#?}", context);
(handle)(&mut context);
Ok(context.response)
} else {
context.string(Some(StatusCode::NOT_FOUND), "404 not found");
Ok(context.response)
}
}
接下来,我们来思考一下:请求参数的解析要怎么写呢?
首先,梳理一下 url 请求参数的类型:
1、params
参数,即/hello/:id
这种动态路由,一条路由规则可以匹配某一类型而非某一条固定的路由
2、query
参数,即/hello?id=1&ce=2
这种带参数 url,路由后面有?
号前缀和&
号连接
鉴于我们的动态路由系统尚未开始搭建,第一种params
参数的解析暂时搁置一下(下一章再实现动态路由和参数解析)
那么,第二种的解析就相对简单很多了,直接看代码:
...
let mut context = AppContext::default();
if let Some(qs) = req.uri().query() {
let mut queries: HashMap<String, String> = HashMap::new();
for item in qs.split('&').collect::<Vec<&str>>().into_iter() {
let res = item.split('=').collect::<Vec<&str>>();
queries.insert(res[0].to_string(), res[1].to_string());
}
// println!("queries: {:#?}", queries);
context.queries = queries;
};
context.method = req.method().clone().into();
...
先从请求 url 中获取query
字符串,然后简单粗暴直接对字符串进行拆分重组,最终赋值到AppContext
结构体上(PS: 哎,就是这么简单粗暴。当然这里还有很大的优化空间和容错处理,有兴趣的童鞋可以自己写一个解析query
参数的办法)。
最后简单测试一下:
运行程序,打开浏览器 http://localhost:3000/index?id=1&ce=2
可以看到终端打印了完整的 AppContext
内容:
AppContext:AppContext {
headers: {
"connection": "keep-alive",
"sec-fetch-user": "?1",
"sec-fetch-site": "none",
"cookie": "Hm_lvt_d2feba2eac8bedae244304195f7b064f=1660401119",
"sec-fetch-mode": "navigate",
"sec-fetch-dest": "document",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36",
"upgrade-insecure-requests": "1",
"sec-ch-ua-platform": "\"Windows\"",
"accept-encoding": "gzip, deflate, br",
"sec-ch-ua": "\"Google Chrome\";v=\"107\", \"Chromium\";v=\"107\", \"Not=A?Brand\";v=\"24\"",
"sec-ch-ua-mobile": "?0",
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"accept-language": "zh-CN,zh;q=0.9",
"host": "localhost:3000",
"cache-control": "max-age=0",
},
params: {},
queries: {
"ce": "2",
"id": "1",
},
forms: {},
response: Response {
status: 200,
version: HTTP/1.1,
headers: {},
body: Body(
Empty,
),
},
path: "",
method: GET,
}
转载自:https://juejin.cn/post/7171262149409538056