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