這篇文章是以一個實習生的視角對前端網站架構的一點分析和理解css
最開始接觸前端的時候,是從簡單的html、css、js開始的,當時盛行的WEB理念是結構樣式行爲相分離,即html、css、js分離,獨立開發,互相之間經過link和script來互相調用。html
最開始我所接觸到的小項目,都是直接將html、css、js等靜態資源直接部署到服務器上,而後根據請求路由響應不一樣的html文件便可。 前端
即便學習了webpack以後,我依然認爲webpack的做用只是壓縮js和css文件,提升服務器響應速度,優化用戶體驗,而部署到服務器上的依然是壓縮後的min.css和min.js文件。node
後來進入A公司實習以後,確實也是這種開發模式,當時咱們開發h5頁面,都是直接書寫html、css、js文件,而後部署到服務器上,直接訪問html便可。react
後來進入B公司工做以後,才慢慢接觸到真正的前端工程是什麼樣子的。webpack
部分傳統大型PC網站的業務前端部分都是採用的MVC架構方式,也就是每次立項以後,先後端約定好接口,分別開發,開發完畢以後,前端將開發好的頁面交給後端(通常是Java或者PHP),而後由後端響應客戶端請求,返回具體的html頁面。es6
這種開發模式的缺點在於費時費力,溝通成本和聯調正本很是高,前端有一點小的改動都須要先後端一塊兒聯調改動上線,大大增長了總工做量。web
所以,現代大型PC網站通常都採用了先後端分離的架構方式,前端和後端的業務功能各自收斂,能夠分別開發上線,互不影響,能夠極大提升工做效率。數據庫
先後端分離通常分爲兩種:express
無中間層 沒有web中間層的先後端分離屬於比較簡單的類型,咱們將一個統一的html/pug模板和其餘的css、js等靜態資源放置到cdn上,每次訪問頁面的時候,直接將模板返回給用戶,而後裏面全部的dom節點和其餘數據都是由js來執行生成的。
然而這種沒有中間層的先後端分離的又有不少劣勢:
有中間層的先後端分離是通常大型項目採用的先後端分離方式。
自從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爬蟲通常分爲兩種:
而對於非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
public
最後還有一些其餘的文件夾能夠自由發揮,好比template存放模板,scripts存放平時寫的腳本等。
一個線上項目要擁有兩套模式——生產模式和開發模式。
生產模式即咱們線上運行環境。
開發模式即咱們平時本地開發環境。
若是有須要的話甚至能夠配置更多的環境。
這兩種環境的要求不同,所以咱們會有兩套配置文件,將不一樣的配置文件傳入node和webpack中,就能夠根據配置的不一樣啓動不一樣環境了。
自動化測試在一個成熟的大型網站中必不可少。
雖然目前由於前端領域的快速增加,業務層的自動化測試也由於業務的快速迭代而變得不穩定,可是一些基礎的測試仍是頗有必要作的。
平時開發的時候要作好類庫單元測試的自動化以及UI組件的單元測試的自動化。
這些測試文件最好存放在單獨的test目錄下,或者在每個基礎UI組件目錄下加上component.test.js文件,這樣啓動測試的時候會自動找到.test文件進行測試。
每次項目上線以前都要進行一次集成測試,測試路由是否正常,webpack打包和node模塊安裝是否正常,模擬用戶登陸訪問等操做是否正常。
偶爾咱們還須要作壓力測試和容災測試等。
對於初學者來講,測試是一個很重要的概念和習慣,平時要多寫一寫單元測試。