這篇文章是以一個實習生的視角對前端網站架構的一點分析和理解
序
最開始接觸前端的時候,是從簡單的html、css、js開始的,當時盛行的WEB理念是結構樣式行為相分離,即html、css、js分離,獨立開發,互相之間通過link和script來互相呼叫。
最開始我所接觸到的小專案,都是直接將html、css、js等靜態資源直接部署到伺服器上,然後根據請求路由響應不同的html檔案即可。
即使學習了webpack之後,我依然認為webpack的作用只是壓縮js和css檔案,提高伺服器響應速度,優化使用者體驗,而部署到伺服器上的依然是壓縮後的min.css和min.js檔案。
後來進入A公司實習之後,確實也是這種開發模式,當時我們開發h5頁面,都是直接書寫html、css、js檔案,然後部署到伺服器上,直接訪問html即可。
後來進入B公司工作之後,才慢慢接觸到真正的前端工程是什麼樣子的。
前後端分離
部分傳統大型PC網站的業務前端部分都是採用的MVC架構方式,也就是每次立項之後,前後端約定好介面,分別開發,開發完畢之後,前端將開發好的頁面交給後端(一般是Java或者PHP),然後由後端響應客戶端請求,返回具體的html頁面。
這種開發模式的缺點在於費時費力,溝通成本和聯調正本非常高,前端有一點小的改動都需要前後端一起聯調改動上線,大大增加了總工作量。
因此,現代大型PC網站一般都採用了前後端分離的架構方式,前端和後端的業務功能各自收斂,可以分別開發上線,互不影響,可以極大提高工作效率。
前後端分離一般分為兩種:
- 沒有中間層的前後端分離;
- 有中間層的前後端分離。 這裡以目前最火的三大框架之一的react為主進行介紹。
無中間層 沒有web中間層的前後端分離屬於比較簡單的型別,我們將一個統一的html/pug模板和其他的css、js等靜態資源放置到cdn上,每次訪問頁面的時候,直接將模板返回給使用者,然後裡面所有的dom節點和其他資料都是由js來執行生成的。
然而這種沒有中間層的前後端分離的又有很多劣勢:
- 首屏渲染時間過長;
- 對seo不友好。
- 有中間層
有中間層的前後端分離是一般大型專案採用的前後端分離方式。
自從2009年node橫空出世之後,前端也逐漸承擔了一些後端的業務,但是node由於自身健壯性的限制,又不適合作為大型專案的後端伺服器,所以node熱過一陣之後,逐漸成為了連線傳統前端和後端的中間層,我們也稱這種前端+node的架構為“大前端”。
返回模板 在node層中,我們可以做的事情就有很多了,其中最基礎的就是返回不同的前端模板。
使用過幾款前端模板,其中給我感覺最好的就是pug模板了(以前叫做jade)。
pug中的語法都是js語法,對前端工程師十分友好,而且pug功能很強大,可以作為html-middleware,被node完美支援,這裡建議學習使用 Get Started。
資料拼接
其次,當網站發展地越來越大,資料量越來越多,對服務層進行分割的時候,會產生很多的服務模組,或者我們的資料分散在不同的資料庫伺服器上的時候,或者我們的前端頁面中要嵌入第三方廣告的一些api的時候,node就可以幫我們完成資料拼接的工作。
因為伺服器訪問介面的速度要比瀏覽器快很多個數量級,因此在node中訪問多個介面並且拼接起來是非常高效的,拼接後的資料我們就可以直接傳入模板中,供js使用了。
但是通常意義上來說,訪問基礎服務或者從資料伺服器訪問資料放在後端來做比較合適,但凡事總有例外,在萬不得已的情況下,我們可以在node中間層中進行資料拼接。
這種模式一般不提倡使用,因為可維護性太差,而且安全性也很低,一般情況下都是後端有一個專門的資料模組去訪問資料庫和服務,然後將資料拼接起來,我們只需要在node中呼叫後端的一個API,就可以拿到我們想要的資料了。
監控服務
node層可以捕捉一些異常請求或者事件,上報到我們的第三方監控平臺,如Sentry等,同時node還可以承擔一部分的資料統計的工作,將一些使用者應為打到第三方資料統計平臺,供pm和資料分析師檢視。
node還可以承擔對整個例項進行監控的職責,當出現異常導致cpu使用率或者記憶體使用率超過閾值之後就可以及時觸發報警機制。
但是同樣,加上node層就意味這網站又多了一層後端需要監控和oncall,工作的複雜度會上升很多。
伺服器端渲染
SSR(server-side-render)可以說是非常重要的功能之一了,它可以幫助我們解決之前提到的首屏渲染時間過長和對seo支援較低的問題。
現代seo爬蟲一般分為兩種:
- 支援解析js的爬蟲,這類數量較少,以Google為代表;
- 不支援解析js的爬蟲,大部分都是這類,基本上都是非Google的搜尋引擎的爬蟲了。 對於Google的爬蟲來說,是否使用SSR在seo方面無關緊要,因為最終都可以爬取到正常的頁面。
而對於非Google的搜尋引擎來說,我們就需要利用SSR,先將具體的dom節點渲染出來,供爬蟲爬取。
而且這樣同時還有一個優點:使用者在網頁loading的過程就中可以看到頁面內容了,而不是一個空白頁面。如果不使用SSR的話,在網頁loading資源的過程中,一直呈現給使用者一片空白,這就有可能造成使用者的流失。
這張圖是我在50kb/s的網速下,訪問b站第一秒鐘看到的內容,若是b站不使用SSR技術的話,可能等到使用者能夠看到首屏內容之後,時間都過去了五六秒。
這裡是一個簡單的使用SSR的Node層程式碼:
// 程式碼中使用了es6語法,不懂的可以先學習一下阮一峰老師的《ES6入門》
// 這個地方node如果沒有使用babel的話,import會報錯,可以直接使用require方法替換
import { renderToString } from 'react-dom/server';
import DemoContainer from 'containers/demo';
// 以koa2框架為例
module.exports = (ctx) => {
const props = {...};
// 這裡的html就存放著我們元件render完之後的dom節點。
const html = renderToString(<Demo >);
// 這裡以返回pug模板為例,第二個引數是要傳入pug模板中的資料
ctx.render('demo.pug', {
__props: JSON.stringify(props),
html
});
};
複製程式碼
這裡是pug的程式碼片段:
// pug程式碼片段
body
#root
!{ html }
script.
window.__props = '!{ __props }'
複製程式碼
使用SSR的時候要切記保證前端和伺服器端的元件props
保持一致,因此這裡我的習慣是在node層將props
直接傳入window
物件上,然後前端的元件直接從window
物件獲取props
即可。
SSR的時候,React元件只會執行componentWillMount和render
兩個生命週期用來生成dom結構,其他的生命週期以及方法掛載都是在前端完成的。
node層的功能不止以上這些,這裡就不過多展開介紹了。
雖然SSR的有點很多,但是還是有自身的弊端的。使用SSR就意味著你用自己伺服器代替了一部分原本屬於使用者客戶端的功能,因此會造成伺服器效能降低,成本增高的可能,相對於小團隊或者資金不算充裕的團隊,要謹慎選擇是否使用SSR。
前後端同構
說起SSR,就不得不提一下前後端同構問題。 同構的意思為前端和node用執行同一套程式碼,首屏使用服務端渲染,將渲染好的html直接交給瀏覽器去渲染,客戶端負責載入js,執行元件剩餘生命週期,並掛載自定義事件等。 一套好的前後端同構程式碼可以大幅減少我們維護程式碼的工作量,並且有十分高效的執行效率,如何優雅地書寫前後端同構的程式碼也是一項技術活,需要我們提前規劃好一套前端架構。
例如:
import React, { Component } from "react";
import { Provider } from "react-redux";
import ReactDOM from "react-dom";
import Loadable from "react-loadable";
import { BrowserRouter, StaticRouter } from "react-router-dom";
// server side render
const SSR = App =>
class SSR extends Component<{
store: any;
url: string;
}> {
render() {
const context = {};
return (
<Provider store={this.props.store} context={context}>
<StaticRouter location={this.props.url}>
<App />
</StaticRouter>
</Provider>
);
}
};
// client side render
const CLIENT = configureState => Component => {
const initStates = window.__INIT_STATES__;
const store = configureState(initStates);
Loadable.preloadReady().then(() => {
ReactDOM.hydrate(
<Provider store={store}>
<BrowserRouter>
<Component />
</BrowserRouter>
</Provider>,
document.getElementById("root")
);
});
};
export default function entry(configureState) {
return IS_NODE ? SSR : CLIENT(configureState);
}
複製程式碼
並且在同構方面,阿里有一套降級策略。當伺服器壓力正常時,由伺服器進行SSR,提高使用者體驗,當使用者訪問量激增,如雙十一時,伺服器會自動進行降級處理,node不進行SSR,全部轉換成客戶端渲染,減輕伺服器的壓力。
選擇框架
瞭解了前後端分離之後,我們就要對node層進行框架選擇了。
目前比較主流的框架有三款:Express、koa1.0、koa2.0。
對於初學者來說,建議直接使用koa2.0進行中間層的學習和開發。
express的缺點在於:
-
太重,有很多模組我們可能都不會用到; 回撥地獄,即使使用Promise也只能緩解。 koa1.0的缺點在於:
-
必須配合
co
庫和generator
來使用,配置繁瑣。 而自從node升級到7.6版本以上,增加了async/await
語法糖之後,我們就可以不需要任何三方庫,直接在原生node中使用koa2的語法。
koa2是express的升級版框架,裡面很多模組是直接從express中遷移過來的,但是又將以前不經常用到的模組刪除,只有開發者在需要使用的時候採取引入那麼模組。
並且koa2使用async/await
語法糖之後,程式碼看似變成了同步執行,非常適合前端工程師的邏輯思維。
這裡是express、promise、koa2的樣例程式碼:
// express版本
module.exports = (req, res) => {
const data1 = request.get('/api/demo1', (err, res) => {
const data2 = request.get('/api/demo2', (err, res) => {
const data3 = request.get('/api/demo3', (err, res) => {
res.send(data1 + data2 + data3);
})
})
})
}
// promise版本
module.exports = (req, res) => {
new Promise((resolve, reject) => {
request.get('/api/demo1', (err, res) => {
resolve(res);
}).then(res => {
request.get('/api/demo2', (err, res2) => res + res2 );
}).then(res2 => {
request.get('/api/demo3', (err, res3) => res2 + res3)
}).then((data) => {
res.send(data);
});
})
}
複製程式碼
看起來雖然整齊了一些,但是依然十分繁瑣。
// koa1和koa2在寫法上基本相同,區別在於koa1在使用之前要對co庫和generator進行繁瑣的配置。
// 每一個await的時候最好加上try-catch,防止因為一個非同步請求失敗而導致node程式崩潰,這裡簡化了寫法。
module.exports = async (ctx) => {
const data1 = await request.get('/api/demo1');
const data2 = await request.get('/api/demo2');
const data3 = await request.get('api/demo3');
ctx.body = {
data: data1 + data2 + data3
};
}
複製程式碼
koa2用起來非常的舒服,很適合前端工程師的思維邏輯。
雖然koa2的程式碼看起來像同步執行,但其實在編譯之後只是變成了promise函式,await後面所有的程式碼都放到了promise的回撥中執行了。
開發結構
選擇好了框架之後,剩下的就只有開發了,一般的node層都遵循一下的目錄結構:
node
- lib // 存放第三方外掛
- util // 存放自己編寫的工具函式
- middleware // 存放中介軟體
- routes // 存放路由
- controller // 存放路由處理函式
- app.js // node層入口檔案 基本的node層架構這裡就介紹差不多了,剩下的前端部分也一般是大家熟悉的東西。 前端目錄結構:
public
- static
- src
- js
- components
- containers
- routes
- stores
- actions
- reducers
- pages
- css/scss/less
- static 這裡按照正常的React開發邏輯去走即可。
最後還有一些其他的資料夾可以自由發揮,比如template存放模板,scripts存放平時寫的指令碼等。
配置
一個線上專案要擁有兩套模式——生產模式和開發模式。
生產模式即我們線上執行環境。
開發模式即我們平時本地開發環境。
如果有需要的話甚至可以配置更多的環境。
這兩種環境的要求不一樣,因此我們會有兩套配置檔案,將不同的配置檔案傳入node和webpack中,就可以根據配置的不同啟動不同環境了。
自動化測試
自動化測試在一個成熟的大型網站中必不可少。
雖然目前因為前端領域的快速增長,業務層的自動化測試也因為業務的快速迭代而變得不穩定,但是一些基礎的測試還是很有必要做的。
平時開發的時候要做好類庫單元測試的自動化以及UI元件的單元測試的自動化。
這些測試檔案最好存放在單獨的test目錄下,或者在每一個基礎UI元件目錄下加上component.test.js檔案,這樣啟動測試的時候會自動找到.test檔案進行測試。
每次專案上線之前都要進行一次整合測試,測試路由是否正常,webpack打包和node模組安裝是否正常,模擬使用者登入訪問等操作是否正常。
偶爾我們還需要做壓力測試和容災測試等。
對於初學者來說,測試是一個很重要的概念和習慣,平時要多寫一寫單元測試。