网易智慧企业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 应用更稳定的提供服务、以及更快更方便的排查问题。

相关文章
相关标签/搜索