网易智慧企业Node.js实践(1) | Node应用架构设计和React同构
導(dǎo)讀:
近期網(wǎng)易智慧企業(yè)在 Node.js(以下簡稱 Node) 的接入上已輸出階段性成果,特推出此系列文章,希望能與大家分享部分接入過程的方案,從而提供幫助。系列主要包括以下內(nèi)容。
?
???????? 1. Node 應(yīng)用架構(gòu)設(shè)計(jì)
???????? 2. React 同構(gòu)
???????? 3. 健康檢查和平滑發(fā)布
???????? 4. 前端代碼上CDN、代碼發(fā)現(xiàn)
???????? 5. 應(yīng)用監(jiān)控
???????? 6. 灰度環(huán)境
?
本文作為系列文章的第一篇主要介紹網(wǎng)易智慧企業(yè) Node 從0到1的接入過程,主要涉及 Node 的應(yīng)用架構(gòu)和同構(gòu)渲染,也就是1、2這兩部分。后續(xù)會(huì)分享關(guān)于 Node 工程實(shí)踐相關(guān)內(nèi)容(3、4、5、6)。
?
關(guān)于 Node
Node 是一個(gè)基于 Chrome V8 引擎的 JavaScript 運(yùn)行時(shí)。它誕生于2009年,Node 第一次把JavaScript帶入到后端服務(wù)器開發(fā),另外還可以通過它編寫工具,比如代碼打包工具,但是它誕生的最初目的還是為了實(shí)現(xiàn)高性能 Web 服務(wù)器。它內(nèi)部實(shí)現(xiàn)的異步 IO、事件驅(qū)動(dòng)就是為高性能 Web 服務(wù)而生的。
?
經(jīng)過過去這么多年發(fā)展,Node 已經(jīng)形成了非常成熟的應(yīng)用模式,比如:BFF(Back-end For Front-end)——服務(wù)于前端的后端,把 Node 作為后端的一層,專門為前端提供數(shù)據(jù)裁剪和格式化、聚合編排等功能。另外還有最近非?;馃岬幕?Node 實(shí)現(xiàn)的 Serverless 服務(wù)。那么具體到我們智慧企業(yè)是怎么使用 Node 的呢?那就首先介紹下我們的需求背景。
?
需求背景
?
2019年底網(wǎng)易智慧企業(yè)正在打造一款 SCRM 產(chǎn)品—網(wǎng)易互客(https://huke.163.com),它最初主要有3塊需求:
?
???????? 1、互客平臺(tái)。
???????? 2、互客運(yùn)營系統(tǒng)(內(nèi)部使用)。
???????? 3、互客官網(wǎng)。
?
前兩部分對(duì)交互要求比較高,有一些需求決定技術(shù)上需要優(yōu)先使用單頁應(yīng)用的形式。官網(wǎng)又是對(duì) SEO 有需求的,所以需要有同構(gòu)渲染的能力(前端使用 React 框架);另外鑒于目前的技術(shù)架構(gòu)對(duì)開發(fā)效率的提升已經(jīng)形成瓶頸,所以考慮使用新的技術(shù)方案,來完全解放前后端的生產(chǎn)力,最終考慮使用 Node 來實(shí)現(xiàn)前后端的完全分離,徹底解決之前前端要寫 Java 模版文件和前后端對(duì)頁面數(shù)據(jù)理解不一致尷尬局面。
?
決定使用 Node 后,首先要解決的問題是如何和 Java 端配合,也就是新的前后端分工,鑒于這是我們第一個(gè)對(duì)外服務(wù)的 Node 項(xiàng)目,作為初次的嘗試,我們考慮使用漸進(jìn)式開發(fā)模式,先從接進(jìn)來開始做,所以我們初始給 Node 分配的任務(wù)比較簡單,包括:
?
???????? 1、頁面渲染。
???????? 2、用戶登錄校驗(yàn)。
???????? 3、頁面初始必要數(shù)據(jù)填充。
???????? 4、功能型接口實(shí)現(xiàn)。
?
另外還有一個(gè)目標(biāo)是通過這個(gè)項(xiàng)目,逐步完善智慧企業(yè)的 Node 工程工具體系,最終形成智慧企業(yè)自己的 Node 生態(tài)。
?
設(shè)計(jì)和實(shí)現(xiàn)
?
確定了如何和 Java 端的配合后,另一個(gè)問題是選擇 Node 框架,經(jīng)過調(diào)研,我們選擇了 Egg.js 作為 Node 框架方案,選它的原因是因?yàn)樗鼞?yīng)該是目前國內(nèi)使用最為廣泛,生態(tài)最為完善的 Node 企業(yè)級(jí)框架。任務(wù)分工和框架都定下來之后我們應(yīng)用的整體架構(gòu)也就出來了,如下圖:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?【架構(gòu)圖】
?
簡單介紹下一個(gè)完整的用戶請求的訪問路徑。首先用戶請求到網(wǎng)關(guān),網(wǎng)關(guān)根據(jù) URL 轉(zhuǎn)發(fā)規(guī)則轉(zhuǎn)發(fā)到 Node 或者 Java 應(yīng)用,從而完成一次頁面訪問或接口請求。這里面涉及到路由的設(shè)計(jì),頁面和接口的 URL 要能夠通過 path 區(qū)分。
?
拿我們的客戶列表頁面舉例,客戶列表的 URL 的 path 是 `/admin/customer/all`,我們的規(guī)則是 `/admin*` 對(duì)應(yīng)頁面請求,所以請求會(huì)被網(wǎng)關(guān)轉(zhuǎn)發(fā)到 Node 上,在 Node 中使用 HTTP 請求從 Java 端獲取頁面初始數(shù)據(jù),放入頁面模版,返回給用戶,完成頁面訪問請求。
?
另外一個(gè)比較重要的問題是用戶的登錄信息,我們使用了比較偏傳統(tǒng)的方案,用戶登錄功能在 Java 端實(shí)現(xiàn),當(dāng)用戶訪問頁面時(shí),Node 會(huì)檢查 cookie 里的登錄 token,并進(jìn)行校驗(yàn),如果 token 不存在或不正確,就給用戶 redirect 到登錄頁面,當(dāng)用戶填寫完信息點(diǎn)擊登錄按鈕時(shí),調(diào)用 Java 端的登錄接口進(jìn)行登錄,成功后 Java 端會(huì)給登錄請求的響應(yīng)帶上 cookie ,這樣前端、Node 端、Java 端的登錄信息就能串起來。
?
當(dāng)然這些只是 Node 作為頁面服務(wù)提供的能力,但是我們還需要 React 的同構(gòu)能力。
?
關(guān)于同構(gòu)
一套代碼既可以在服務(wù)端運(yùn)行又可以在客戶端運(yùn)行,在服務(wù)器端執(zhí)行一次,用于實(shí)現(xiàn)服務(wù)器端渲染,在客戶端再執(zhí)行一次,用于接管頁面交互,這就是同構(gòu)應(yīng)用。簡而言之, 就是服務(wù)端直出和客戶端渲染的組合, 能夠充分結(jié)合兩者的優(yōu)勢,并有效避免兩者的不足。
?
同構(gòu)不僅僅能解決前面說的 SEO 問題,它還能有效縮減頁面白屏?xí)r間,因?yàn)樗馨阎暗娜未械?HTTP 請求縮減為一次,而白屏?xí)r間對(duì)用戶的影響也是非常大的。
?
一般前端框架是需要對(duì) DOM 進(jìn)行操作的,在瀏覽器環(huán)境當(dāng)然沒有問題,而在Node 是沒有 DOM 這個(gè)概念的,那 React 是如何實(shí)現(xiàn)在 Node 端進(jìn)渲染的呢?這因?yàn)?React 中引入的虛擬 DOM,虛擬 DOM 是真實(shí) DOM 的一個(gè) JavaScript 對(duì)象映射,React 在做頁面操作時(shí),實(shí)際上不是直接操作 DOM,而是操作虛擬 DOM,也就是操作普通的 JavaScript 對(duì)象,這就使得 SSR 成為了可能。在 Node 端 React 把虛擬 DOM 輸出為字符串,而在瀏覽器端 React 把虛擬 DOM 映射為真實(shí) DOM,完成頁面渲染。
?
那么如何在 Node 端把 React 頁面渲染為字符串呢?React 框架提供了4個(gè)API針對(duì)不同的使用場景,分別是:
?
???????? *? renderToString()
???????? *? renderToStaticMarkup()
???????? *? renderToNodeStream()
???????? *? renderToStaticNodeStream()
?
結(jié)合需求我們選擇 `renderToString` 方法。
?
其實(shí)整個(gè)服務(wù)端渲染的邏輯非常簡單,把初始數(shù)據(jù)傳給 React 組件使用 `renderToString` 進(jìn)行渲染,得到一個(gè)字符串,把字符串放入頁面模版中的 React 掛載節(jié)點(diǎn)內(nèi)就行了。但是要實(shí)現(xiàn)一個(gè)能根據(jù)路由自動(dòng)渲染對(duì)應(yīng)的組件的 Egg.js 插件還是有一點(diǎn)復(fù)雜的,所以我們實(shí)現(xiàn)了 `pp-fishssr` 服務(wù)端渲染插件,以滿足根據(jù)路由渲染對(duì)應(yīng)頁面的需求。
主要介紹下我們的實(shí)現(xiàn)的不一樣的地方,首先是配置方式:
?
```json
fishssr: {
???????? routes: [
???????? {
???????? ? path: ‘/admin/*’,
???????? ? Component: () => (require(‘@/page/admin’).default),
???????? ? controller: ‘page.admin’
???????? },
???????? {
???????? ? path: ‘/user/*’,
???????? ? Component: () => (require(‘@/page/user’).default),
???????? ? controller: ‘user.h5Page’,
???????? },
???????? ],
???????? // 頁面模版文件路徑
???????? template: ‘screen/index.html’,
???????? // 服務(wù)端渲染打包后的js文件
???????? serverJs: resolvePath(‘dist/Page.server.js’),
}
```
?
介紹配置項(xiàng):
?
path:? `/admin/*`、`/user/*` 分別對(duì)應(yīng)了一個(gè)單頁應(yīng)用。
Component: 對(duì)應(yīng)了頁面的 React 組件,內(nèi)部會(huì)處理初始數(shù)據(jù),轉(zhuǎn)化為store 的 preloadedState 或 props,里面使用前端路由。
controller:? 對(duì)應(yīng)的是 Egg.js 中的 controller,用來獲取頁面初始數(shù)據(jù),然后使用`this.ctx.fishssr.renderPage(initData)`實(shí)現(xiàn)頁面渲染。
template: 頁面的模版文件,內(nèi)部 `stream` 就是 Node 渲染 React 頁面組件之后得到的字符串,文件的內(nèi)容大致如下:
?
```html
<!DOCTYPE html>
<html lang=‘zh-CN’>
<head>
? <title>網(wǎng)易互客</title>
? <link rel=‘stylesheet’ href=‘/css/Page.css’ />
</head>
?
<body>
? <div id=‘a(chǎn)pp’>
??? {{stream | safe}}
? </div>
???????? <script>
? window.__INITIAL_DATA__ = {{ initialData | safe}};
? </script>
? <script src=‘/js/runtime~Page.js’></script>
? <script src=‘/js/Page.js’></script>
</body>
</html>
```
?
serverJs :是頁面入口文件對(duì)應(yīng)的 Node 端打包版本,入口文件主要代碼如下:
?
```
const clientRender = async () => {
? ReactDOM.hydrate(
??? <>
????? {
?????? ?Routes.map(route => {
????????? const { path, Component } = route
????????? const isMatch = matchPath(window.location.pathname, route)
????????? if ( !isMatch ) {
??????????? return null
????????? }
????????? const ActiveComponent = Component()
????????? const WrappedComponent = GetInitialProps(ActiveComponent)
????????? return <WrappedComponent key={path} />
??????? })
????? }
??? </>, document.getElementById('app'))
}
?
const serverRender = async (params) => {
? const { initData, path, url } = params
? const ActiveComponent = getComponent(Routes, path)()
? return (
??? <StaticRouter location={url} context={initData}>
????? <ActiveComponent {... initData} />
??? </StaticRouter>
? )
}
?
export default __isBrowser__ ? clientRender() : serverRender
```
?
這段代碼會(huì)根據(jù)路由渲染對(duì)應(yīng)的頁面組件,同時(shí)根據(jù)不同打包環(huán)境輸出對(duì)應(yīng) Node 端和瀏覽器端的渲染代碼。
?
總結(jié)
?
Egg.js 作為一個(gè)完備的企業(yè)級(jí) Node 框架,在接入過程中可以說非常順滑,主要精力放在解決自身業(yè)務(wù)需求和后端配合即可。
?
目前使用這個(gè)方案的產(chǎn)品**網(wǎng)易互客**已經(jīng)上線,這個(gè)方案解決了文章開頭所說技術(shù)和業(yè)務(wù)需求的,同時(shí)它帶來的新的前后端配合模式也極大的提高了不僅僅是前端的開發(fā)效率,對(duì)后端來說也是非常友好的。同時(shí)前端也可拓寬自己邊界,能夠承接更多需求,比如我們運(yùn)營系統(tǒng)、功能性 API,比如微信 JS-SDK 認(rèn)證,之前只能放在后端,現(xiàn)在放在 Node 端,前端開發(fā)起來更加靈活,減少很大的溝通成本。但是目前作為對(duì)外服務(wù) Node 應(yīng)用只有這些還是不夠的,還是需要很多工程工具的支持。
?
后續(xù)我會(huì)介紹我們在 Node 工程上的一些實(shí)踐,讓 Node 應(yīng)用更穩(wěn)定的提供服務(wù)、以及更快更方便的排查問題。
總結(jié)
以上是生活随笔為你收集整理的网易智慧企业Node.js实践(1) | Node应用架构设计和React同构的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 漫谈企业级SaaS的多租户设计
- 下一篇: 网易智慧企业 Node.js 实践(2)