網易智慧企業Node.js實踐(1) | Node應用架構設計和React同構

導讀:css

近期網易智慧企業在Node.js(如下簡稱 Node) 的接入上已輸出階段性成果,特推出此係列文章,但願能與你們分享部分接入過程的方案,從而提供幫助。系列主要包括如下內容。html

1. Node 應用架構設計前端

2. React 同構json

3. 健康檢查和平滑發佈後端

4. 前端代碼上CDN、代碼發現瀏覽器

5. 應用監控前端框架

6. 灰度環境服務器

本文做爲系列文章的第一篇主要介紹網易智慧企業Node 從0到1的接入過程,主要涉及 Node 的應用架構和同構渲染,也就是一、2這兩部分。後續會分享關於 Node 工程實踐相關內容(三、四、五、6)。微信

關於Node
圖片1.pngcookie

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塊需求:

一、互客平臺。

二、互客運營系統(內部使用)。

三、互客官網。

前兩部分對交互要求比較高,有一些需求決定技術上須要優先使用單頁應用的形式。官網又是對SEO 有需求的,因此須要有同構渲染的能力(前端使用 React 框架);另外鑑於目前的技術架構對開發效率的提高已經造成瓶頸,因此考慮使用新的技術方案,來徹底解放先後端的生產力,最終考慮使用 Node 來實現先後端的徹底分離,完全解決以前前端要寫 Java 模版文件和先後端對頁面數據理解不一致尷尬局面。

決定使用Node 後,首先要解決的問題是如何和 Java 端配合,也就是新的先後端分工,鑑於這是咱們第一個對外服務的Node 項目,做爲初次的嘗試,咱們考慮使用漸進式開發模式,先從接進來開始作,因此咱們初始給 Node 分配的任務比較簡單,包括:

一、頁面渲染。

二、用戶登陸校驗。

三、頁面初始必要數據填充。

四、功能型接口實現。

另外還有一個目標是經過這個項目,逐步完善智慧企業的Node 工程工具體系,最終造成智慧企業本身的 Node 生態。

設計和實現

肯定了如何和Java 端的配合後,另外一個問題是選擇 Node 框架,通過調研,咱們選擇了 Egg.js 做爲 Node 框架方案,選它的緣由是由於它應該是目前國內使用最爲普遍,生態最爲完善的 Node 企業級框架。任務分工和框架都定下來以後咱們應用的總體架構也就出來了,以下圖:
圖片2.png

【架構圖】

簡單介紹下一個完整的用戶請求的訪問路徑。首先用戶請求到網關,網關根據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 的同構能力。

關於同構
圖片3.png

一套代碼既能夠在服務端運行又能夠在客戶端運行,在服務器端執行一次,用於實現服務器端渲染,在客戶端再執行一次,用於接管頁面交互,這就是同構應用。簡而言之, 就是服務端直出和客戶端渲染的組合, 可以充分結合二者的優點,並有效避免二者的不足。

同構不只僅能解決前面說的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 應用更穩定的提供服務、以及更快更方便的排查問題。

相關文章
相關標籤/搜索