likes
comments
collection
share

用 rust 从零开发一套 web 框架:day2

作者站长头像
站长
· 阅读数 38

修炼内丹

前面有了基础的框架,现在进行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实例增加获取内部数据的接口,可以获取queryparamheaderform等请求参数。

///获取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 格式使用了 serdeserde_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
评论
请登录