总览
Juri 是一个 Web 服务器框架。
安装
cargo:
cargo add juri
cargo add async-std
或者
在 Cargo.toml 文件里添加 juri = "0.4.0-alpha.2"
使用
最小 Demo:
use juri::{Request, Response, Router, handler};
use std::net::SocketAddr;
#[handler]
fn handle_index(_request: &Request) -> juri::Result<Response> {
Ok(Response::html("Hello Juri"))
}
#[juri::main]
async fn main() {
let mut router = Router::new();
router.at("/").get(api::views::handle_index);
let addr = SocketAddr::from(([127, 0, 0, 1], 7878));
juri::Server::bind(addr).server(router).await.unwrap();
}
新建路由器
use juri::Router;
let router = Router::new();
添加处理函数
At 方式
只支持 get
和 post
两种请求方式
use juri::Router;
let mut router = Router::new();
router.at("/").get(handle_fn);
/// 路径一样时,可以链式调用
router.at("/").get(handle_fn).post(handle_fn);
函数定义
use juri::{handler, Request, Response};
/// 支持同步和异步。异步时在 `fn` 前添加 `async`
#[handler]
fn handle_fn(request: &Request) -> juri::Result<Response> {
/// body
}
Route 方式
只支持 get
和 post
两种请求方式,另外支持 ws
use juri::Router;
let mut router = Router::new();
/// 由函数提供,请求方法和路径
router.route(handle_fn);
函数定义
use juri::{get, post, Request, Response, WSRequest, WSResponse};
/// 支持同步和异步。异步时在 `fn` 前添加 `async`
#[get("/")]
fn handle_fn(request: &Request) -> juri::Result<Response> {
/// body
}
/// 使用 ws 时,只需在 `get` 宏中加 ws 参数即可,其他同 get 请求格式
#[get("/", ws)]
fn handle_fn_ws(request: &juri::Request) -> juri::Result<WSResponse> {
/// body
}
/// 支持同步和异步。异步时在 `fn` 前添加 `async`
#[post("/")]
fn handle_fn_post(request: &Request) -> juri::Result<Response> {
/// body
}
路由器分组
#![allow(unused)] fn main() { use juri::Router; let mut router = Router::new(); router.at("/").get(handle_fn); let mut child_router = Router::new(); child_router.at("/one").get(child_handle_fn); /// 此时,`/` 和 `/one` 都可以处理 router.router(child_router); }
子路由添加根路径
#![allow(unused)] fn main() { use juri::Router; let mut router = Router::new(); router.at("/").get(handle_fn); let mut child_router = Router::new(); child_router.root("/hi"); child_router.at("/one").get(child_handle_fn); /// 此时处理 `/` 和 `/hi/one` /// 也就是路径是 `/hi/one`,才执行 `child_handle_fn` 函数 router.router(child_router); }
路由的匹配语法
普通参数
/// 匹配 /one
router.at("/one");
/// 匹配 /one/two
router.at("/one/two");
参数
通过加 :
来把一个路径节点,变成匹配参数。
/// 匹配 /one/two
/// 通过 `request.param("chapters")` 来获取 two
router.at("/one/:chapters");
/// 匹配 /one/two/three
router.get("/one/:chapters/three", handle_index);
可重复的参数
参数路径节点后面加 +
时可匹配 1 个或多个
/// 匹配 /one/two,/one/two/three 时。
/// /one/two 时 `request.param("chapters") == two
/// /one/two/three 时 `request.param("chapters") == two/three
router.get("/one/:chapters+", handle_index);
请求(Request)
Header
#![allow(unused)] fn main() { /// 获取 header pub fn header(&self, key: &str) -> Option<String>; /// 获取多个 key 相同的值 pub fn header_multi_value(&self, key: &str) -> Option<HeaderValues>; }
#![allow(unused)] fn main() { /// 获取 cookie pub fn cookie(&self, key: &str) -> Option<String>; }
#![allow(unused)] fn main() { /// 获取路径参数 pub fn param(&self, key: &str) -> Option<String>; /// 获取查询参数 pub fn query(&self, key: &str) -> Option<String>; }
FormData
FormData
#![allow(unused)] fn main() { /// 获取单个文件,返回 Option 类型 request.file("file"); /// 获取多个 name 值一样的文件 ,返回 Vec 类型 request.files("file"); }
#![allow(unused)] fn main() { let file = request.file("file").unwrap(); /// 打开缓存文件 file.open(); /// 获取缓存文件大小 file.file_size(); /// 获取文件类型 file.file_type(); }
复制缓存文件到指定路径
#![allow(unused)] fn main() { let file = request.file("file").unwrap(); let file_name = file.file_name.clone().unwrap(); let path = format!("/home/upload/{}", file_name); let path = Path::new(&path); file.copy(path).unwrap(); }
WebSocket 是啥
WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。具体可以参考 维基百科
JavaScript 的 WebSocket 使用可以参考 MDN 文档
使用
#![allow(unused)] fn main() { use juri::{get, Request, Response, web_socket::{Message, RequestExt, WSResponse}}; #[get("/ws", ws)] pub fn handle_ws(&request: Request) -> juri::Result<WSResponse> { /// 升级为 ws, 成功时返回 WSResponse let mut ws = request.upgrader().unwrap(); // 传入 ws 处理逻辑 ws.on(|mut stream| async move { loop { let message = stream.read().await.unwrap(); match message { Message::Text(text) => { stream.send(WSMessage::Text("hi".to_string())).await.unwrap(); }, Message::Binary(_) => todo!(), Message::Ping(_) => todo!(), Message::Pong(_) => todo!(), Message::Close => { return; } } } }); /// 返回 WSResponse Ok(ws) /// 也可以自定义返回 // Ok(WSResponse::new(Response::html(""))) } }
插件
创建插件需要实现以下 JuriPlugin trait
#![allow(unused)] fn main() { pub trait JuriPlugin: Send + Sync + 'static { /// 请求拦截器 fn request(&self, request: &mut Request) -> Option<Response>; /// 响应拦截器 fn response(&self, request: &Request, response: &mut Response); } }
例:
#![allow(unused)] fn main() { use juri::plugin::JuriPlugin; struct MyPlugin { } impl JuriPlugin for MyPlugin { fn request(&self, request: &mut Request) -> Option<Response> { /// 可修改请求内容 /// 返回是 `None` 时,继续执行 /// or /// 返回是 `Response` 时,拦截接下来的操作 /// 例:插件有 `[1, 2, 3]` /// 插件 2 返回 `Response` 时,执行顺序为 request:1 - 2,response: 2 - 1 /// 都为 `None` 时,执行顺序为 request:1 - 2 - 3,匹配路由,response: 3 - 2 - 1 } fn response(&self, request: &Request, response: &mut Response) { /// 可修改请求内容和响应内容 } } }
使用
#![allow(unused)] fn main() { let my_plugin = MyPlugin {}; /// 生成 `MyPlugin` 实例传入 `plugin` 函数 juri::Server::bind(addr).plugin(my_plugin); }
内置插件
静态文件插件
加载静态文件插件
使用签名如下
#![allow(unused)] fn main() { impl StaticFilePlugin { /// HashMap 的 key 代表匹配 URL 的前缀,value 代表匹配 的文件路径(为多个文件路径) pub fn new(config: HashMap<&str, Vec<PathBuf>>>) -> Self; } }
例:
key 为 /static
, value 为伪代码 ["/home", "/www/site"]
,URL 为 /static/js/index.js
时
会先查找 /home/js/index.js
,然后在查找 /www/site/js/index.js
,最后匹配失败
使用
#![allow(unused)] fn main() { use juri::plugin::StaticFilePlugin; use std::{env, collections::HashMap}; let current_dir = env::current_dir().unwrap(); let static_file_plugin = StaticFilePlugin::new(HashMap::from([( "/static", vec![current_dir.join("web-socket").join("static")], )])); juri::Server::bind(addr).plugin(static_file_plugin); }