前端小白的網(wǎng)站搭建

作為一個主攻后端研究的碼狗來說,前端在我心中一直是那么的神秘而又高大上,什么vue框架啦,什么javasript,css,html啦,雖然都有所了解但是其實(shí)一直不知道從哪入手,也就一直沒有了解。然而最近我在寫一個c++網(wǎng)絡(luò)庫(hfh1999/feipu: a network library ,inspired by muduo),在寫到http模塊時,突然發(fā)覺自己對后端和前端的的交互并不熟悉,于是就決定自己來寫一個簡單的網(wǎng)站,從而了解前后端的交互過程。下面讓我們開始吧:)
這個小項(xiàng)目的示例可以在hfh1999/fei_blogs: 寫一個簡單的博客網(wǎng)站中找到。

后端語言及框架的選擇

關(guān)于后端語言的話,由于我沒學(xué)過java,又鑒于C++后端薄弱的生態(tài),于是我選擇了我最鐘愛的rust-lang。那么還需要再選擇一個好用的后端框架。經(jīng)過我“嚴(yán)密”地調(diào)查后,最終選定使用axum - crates.io: Rust Package Registry作為后端框架。axum是基于tokio異步io框架的web框架,所以其io性能可以得到充分地保證,而其使用也比較簡便,文檔也較為詳細(xì),像我這種新手只研究了半天就明白了其基本的使用。

前端框架的選擇

由于我沒有精力將前端的所有知識都學(xué)一遍,于是只好借助于前端框架。當(dāng)然了前端框架也是有許多選擇的,常用的有vue,react等等。但是,先等一下,像我這樣懶的人怎么會花那么大的精力去學(xué)習(xí)vue和react呢:)?經(jīng)過搜索,很快發(fā)現(xiàn)了我的目標(biāo):amis,這個前端框架利用低代碼的思想,只需要在網(wǎng)頁中利用json配置就可以很好地呈現(xiàn)出不同的效果,更完美的是,它還有個可視化編輯器!

可視化編輯器

前端編寫

在可視化編輯器中設(shè)計好后就可以提取出相應(yīng)的json代碼,把json代碼粘貼進(jìn)框架里的html頁面即可。amis的一些部件會在一些條件發(fā)出request,比如按鈕會在按下時發(fā)出請求,而增刪改查部件則會在初始化時發(fā)出請求,在框架里都用api字段表示,可以在api字段配置request的類型及發(fā)出的request的格式。像我下面的主頁中的增刪改查部件就會在初始化時發(fā)出get請求,且其url的參數(shù)為page和perpage以用于翻頁。總而言之前端編寫只需要拖拖拽拽,改改畫畫,效果,動畫,請求接收,發(fā)送,這個框架都已經(jīng)幫我們處理好了。
下面是寫(拖)出來的主界面,從上往下分別是標(biāo)題欄,文本區(qū)域,兩個按鈕,和增刪改查組件:


寫出來的主界面

點(diǎn)擊綠色按鈕時會跳轉(zhuǎn)到留言頁面,下面是寫出來的留言頁面:


image.png

主頁面有兩個部分需要注意:
綠色按鈕,會觸發(fā)網(wǎng)頁重定向,向服務(wù)器請求(GET)新的頁面;
增刪改查組件(就是哪個表格狀的東西),這個組件在初始化時也會向服務(wù)器發(fā)送請求(GET)。

留言見面只有一個部分需要注意:
在點(diǎn)擊提交按鈕的時候會向服務(wù)器發(fā)送請求(POST)來提交表單。

后端編寫

rust是一個復(fù)雜的語言,后端技術(shù)更是一個復(fù)雜的技術(shù)棧,但是兩者一組合,沒想到世界居然突然就變得美好了,當(dāng)然了這一切都要多謝那些開源世界的大佬們,他們幫我們處理好了所有的復(fù)雜細(xì)節(jié),于是我們就只需要考慮業(yè)務(wù)代碼了。
我們在這個小項(xiàng)目里用到的后端框架是axum框架,來一小段代碼來展示以下axum的使用:

#[tokio::main]
async fn main() {
    tracing_subscriber::registry()
        .with(tracing_subscriber::EnvFilter::new(
            std::env::var("RUST_LOG")
                .unwrap_or_else(|_| "example_todos=debug,tower_http=debug".into()),
        ))
        .with(tracing_subscriber::fmt::layer())
        .init();


    // Compose the routes
    let app = Router::new()
        .route("/push_text", post(get_text))
        // Add middleware to all routes
        .layer(
            ServiceBuilder::new()
                .layer(HandleErrorLayer::new(|error: BoxError| async move {
                    if error.is::<tower::timeout::error::Elapsed>() {
                        Ok(StatusCode::REQUEST_TIMEOUT)
                    } else {
                        Err((
                            StatusCode::INTERNAL_SERVER_ERROR,
                            format!("Unhandled internal error: {}", error),
                        ))
                    }
                }))
                .timeout(Duration::from_secs(10))
                .layer(TraceLayer::new_for_http())
                .into_inner(),
        );

    let addr = SocketAddr::from(([0, 0, 0, 0], 80));
    tracing::debug!("listening on {}", addr);
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

大家看到這么多稀奇古怪的表達(dá),未免覺得無從下手,不過我們只需要看最核心的一個表達(dá)式:

.route("/push_text", post(get_text))

這就是一個最簡單的處理,意思是假若請求的url指向"push_text" 且請求類型為post時就利用get_text進(jìn)行處理。route方法就是axum的一個核心的概念,把請求想象成流水,route方法就是水管上一個個不同位置的洞,當(dāng)滿足相應(yīng)條件時就會從洞中流出,進(jìn)入到相應(yīng)的處理函數(shù),然后再返回給客戶。

我們再看下這個最簡單例子的處理函數(shù)get_text:

#[derive(Debug, Deserialize)]
struct UserText {
    usertext: Option<String>,
    useremail: Option<String>
}

async fn get_text(
    Json(input): Json<UserText> // 1.這里是extract操作,請求的數(shù)據(jù)被解析到extractor中
) -> impl IntoResponse {
    match input.usertext {
        None => println!("recieve empty text"),
        Some(push_str) => println!("the text is : {}",push_str)
    }
    match input.useremail {
        None => println!("recieve empty text"),
        Some(push_str) => println!("the email is : {}",push_str)
    }
// 2.下面的格式是amis框架所要求的恢復(fù)格式
    let return_text = Return{
        status:0,
        msg:String::from("save success"),
        data:Data { id: 1 }
    };
    (StatusCode::CREATED, Json(return_text)) // 3.這里構(gòu)造請求的回復(fù)
}

讓我們從上往下分析以下這個處理函數(shù):
http的數(shù)據(jù)在1處進(jìn)行解析,通過Json<T>這個extractor匹配數(shù)據(jù)中的json數(shù)據(jù),然后將數(shù)據(jù)反序列化到UserText中。關(guān)于數(shù)據(jù)的反序列化,還得多謝rust的類型系統(tǒng),讓我們可以進(jìn)行方便的序列化和反序列化。在1處,我們將客戶端發(fā)來的純文本json的數(shù)據(jù){"usertext":"sometext"}轉(zhuǎn)換為UserText類型,在處理函數(shù)中很簡單的進(jìn)行信息提取。大家看到Json<T> 這個類型會感到莫名其妙,這在axum中被稱為extractor,我們這里只需要理解為axum看到Json<T>類型就明白要將請求中的json數(shù)據(jù)解析到T類型中即可。當(dāng)然也有一些其他的extractor如Path<T>,是用來解析匹配url用的。更多的extractor還請查找文檔的axum::extract部分。
發(fā)送來的數(shù)據(jù)經(jīng)過解析后,我們需要處理,這里我們只是簡單地在服務(wù)端打印到標(biāo)準(zhǔn)輸出中。緊接著來到2處,這里需要稍微介紹下,為什么要定義Return這樣一個樣式的數(shù)據(jù)結(jié)構(gòu)呢?這并不是我們自作主張,主要是因?yàn)榍岸丝蚣躠mis需要服務(wù)器在收到請求后回復(fù)的格式形如下面的json結(jié)構(gòu):

{
  "status":0,
  "msg":"string",
  "data":{
    "id":1
  }
}

于是我們不得不定義Return為下面的類型以進(jìn)行序列化。(關(guān)于序列化和反序列化這部分,建議看看rust的序列化/非序列化生態(tài):serde - crates.io: Rust Package Registry)下面是Return的定義:

#[derive(Debug, Serialize, Clone)]
struct Data{
    id:i64,
}

#[derive(Debug, Serialize, Clone)]
struct Return {
    status: i64,
    msg: String,
    data: Data,
}

然后在3處,我們構(gòu)造一個http回復(fù),這也是由框架自動完成的,我們只需要返回一個實(shí)現(xiàn)axum::response::IntoResponsetrait的類型就可以了,我們這里返回一個元組:

(StatusCode::CREATED, Json(return_text)) 

元組類型已經(jīng)被自動實(shí)現(xiàn)了axum::response::IntoResponsetrait,這里的含義是返回的恢復(fù)的返回碼是201(CREATED),搭載的數(shù)據(jù)是json格式數(shù)據(jù),且數(shù)據(jù)由類型Return序列化而成。

那么這個后端框架的基本使用我們就學(xué)會了,于是我們編寫一些新的router和新的router處理函數(shù)來應(yīng)付不同的請求就可以了。當(dāng)然了,我們這樣遠(yuǎn)不能稱為網(wǎng)站,因?yàn)槲覀冎皇菍懥艘粋€個api等待來自客戶端的http消息而已。但是,我們完全可以離線實(shí)驗(yàn)一下是否能成功連接到后臺。我們從文件夾中打開編寫好的index.html文件,然后驚喜地發(fā)現(xiàn)果然在瀏覽器中顯示出了好看的界面,于是點(diǎn)擊按鈕向服務(wù)器發(fā)起請求==> 提示錯誤為什么呢?

跨域http請求

于是打開瀏覽器的開發(fā)者面板,重新發(fā)起請求,發(fā)現(xiàn)瀏覽器根本沒有按照預(yù)料發(fā)起get和post請求,竟然意外地發(fā)送了OPTIONS。這是為什么呢?經(jīng)過搜索,我終于明白了關(guān)鍵所在,我的網(wǎng)頁是在本地資源上,而服務(wù)器是設(shè)置在共有ip上的,這樣http請求就"跨域請求資源"了,自然要發(fā)出OPTIONS請求,而我的服務(wù)器代碼根本沒有處理OPTIONS的,自然出現(xiàn)錯誤了。想到這,我們的改進(jìn)方法就很好辦,我們需要實(shí)現(xiàn)網(wǎng)頁資源也從服務(wù)器上獲取,這樣網(wǎng)頁和服務(wù)器資源就在同一個域上了。

構(gòu)建網(wǎng)頁服務(wù)

網(wǎng)頁服務(wù)很簡單,基本原理就是瀏覽器通過get請求請求相應(yīng)文件的過程,本質(zhì)上就是提供一個文件的下載服務(wù)。主要過程只要從url中解析出需要的文件的路徑,根據(jù)路徑找到相應(yīng)文件然后將文件內(nèi)容放到數(shù)據(jù)段返回給瀏覽器就可以了。但是有一個關(guān)鍵的地方需要我們尤其注意,那就是我們需要在http回復(fù)的content-type字段填寫相應(yīng)的文件類型。這可就有些困難了,首先文件類型有許多種,我們很難自己處理,其次每次都在http恢復(fù)中設(shè)置content-type也很麻煩。
第一個想到的方案就是在crates.io中搜索有關(guān)content-type的庫,果然讓我們找到了,利用mime-guess來判斷文件類型,利用mime來確定相應(yīng)類型的字符串形式。這樣得到相應(yīng)的文件的類型后我們就在回復(fù)中填入相應(yīng)的字符串即可。不過,慢著,這是否有些不對勁?
再看看我們的第二個方案,我們可以利用幾行代碼就完成這樣一個服務(wù):

 .route("/",get_service(ServeFile::new("./web_src/index.html")).handle_error(|error:io::Error|async move{
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                format!("Unhandled internal error:{}",error),
            )
        }))
        .route("/suggestion",get_service(ServeFile::new("./web_src/suggestion.html")).handle_error(|error:io::Error|async move{
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                format!("Unhandled internal error:{}",error),
            )
        }))
        .route("/get_articles/*pages",get(get_article))
        .route("/:file/*tmp",get_service(ServeDir::new("./web_src")).handle_error(|error:io::Error|async move{
            (
                StatusCode::INTERNAL_SERVER_ERROR,
                format!("Unhandled internal error:{}",error),
            )
        }))

我們增加下面幾個router即可,利用rust強(qiáng)大的web生態(tài),為route增加幾個service,也就是在route中更改get為get_service即可,然后利用從tower_http庫中的services模塊中的ServeFile,和ServeDir服務(wù)就可以實(shí)現(xiàn)這一目標(biāo),至于.handle_error()為什么突然出現(xiàn),還請大家觀看axum的官方文檔。這兩個服務(wù)的主要流程和我上面的方案一樣,但是既然別人已經(jīng)寫好了而且很好用,為什么不用呢?

最終實(shí)驗(yàn)

我們在編譯運(yùn)行服務(wù)器,然后在瀏覽器中輸入127.0.0.1回車,果然一個"精美"的網(wǎng)站就出現(xiàn)了,而且向服務(wù)器請求數(shù)據(jù)和發(fā)送數(shù)據(jù)都完全正常!這就是一個簡單的網(wǎng)站的搭架,對于不會前端的同學(xué)來說是不是很簡單呢?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容