React服务端渲染(ssr)之Next.js框架

标签: react 服务 渲染 | 发表时间:2020-12-22 10:13 | 作者:rexhang_web
出处:https://juejin.im/frontend

NextjsReact生态中非常受欢迎的SSR(server side render——服务端渲染)框架,只需要几个步骤就可以搭建一个支持SSR的工程(_Nextjs_的快速搭建见 Next.js入门)。 本文的案例代码来自于 前端标准模板项目

服务端组织数据

Nextjs

提供了便捷强大的服务端渲染功能—— getInitialProps(),通过这个方法可以简单为服务端和前端同时处理异步请求数据:

   const load = async () =>{
    return new Promise((res, rej)=>{
        res('Success')
    })
}
class Simple extends React.Component{
    static async getInitialProps({req, query}) {
        const data = await load();
        return {data}
    }
    render() {
        return(    

{this.props.data}

) } } 复制代码

Next的强大之一体现在就这么几行代码就解决了SSR中最麻烦的前后端异步数据组装功能。再复杂的异步数据组装过程都可以放置到代码中的Promise对象中。

页面与内页

在继续述说本文内容之前还需要强化两个概念—— 内页页面

通过浏览器输入一个地址获取到的内容称之为 页面

而在单页面应用中也会有通过导航栏或菜单控制的内容切换效果,我们将这些切换的内容称之为 内页。单页面应用中一般会先打开一个页面,然后通过Dom的增删改模拟页面切换的效果。

Nextjs中SSR渲染的局限性

getInitialProps()方法虽然强大好用,但是现在还存在一个问题—— 只能在“内页”中使用

Nextjs_规定了所有放置到 ./pages中的文件(通常是*.js_文件,也可以引入

.ts*文件)都视为一个内页,这些文件中被导出的React组件可以直接输入地址上访问。例如现在有[

./pages/about.js

]( github.com/palmg/websi…

Nextjs

后在浏览输入 http://localhost:3000/about就可以看到这个组件,而[

./pages/async/simple.js

]( github.com/palmg/websi…

但是在其他路径(比如 ./component)的组件是无法使用 getInitialProps()方法的。乍一看这样似乎没多大问题,但是某些应用又需要这些组件不能放置到 ./pages中暴露到_url_中,又需要异步加载数据。看下面的例子。

按需加载菜单的例子

应用菜单

如上图。在企业级应用中(例如OA系统)通常不太需要实现SSR,这个时候可以根据角色权限在组件的 componentDidMount()方法中异步加载菜单,但是在某些时候(例如一个可配置菜单的内容网站,或者对企业级应用进行服务端缓存)也会有菜单异步加载并且实现SSR的需要,这个时候需要在_Nextjs_框架的基础上扩展。

看到这里可能你会想可以把菜单的组装像下面放到每个内页的 getInitialProps()方法中去:

   const Comp = props =>(    
{props.pageData}
); Comp.getInitialProps = async ({req})=>{ //load Menu Promise const menus = await getMenus(); //load Page Data Promise const pageData = await getPageData(); return {menus, pageData} } 复制代码

这样做在实现上没问题,但是在架构设计上是颇为糟糕的。以下三个原因:

  1. 对于React有各种各样的描述,比如单向数据流、组件化等等。但是他的核心思想其实是 分而治之。在Jquery“统治”的年代可以使用_selector_(比如 $('#id'))轻易获取到页面上的任何元素。一个项目如果没有很好的规范化管理(长久的人工规范化管理是需要投入不少成本的),久而久之会发现各个板块之间耦合性越来越强、坑越来越多(代码腐烂)。而React的单向数据流让组件与组件之间没有直接的沟通方式,规范化从技术层面就被强化,进而才会产生了_Redux_、_Flux_这一类按照“分-总-分”的模式(实际上就是一个消息总线模式)去控制模块间沟通的。所以将业务逻辑相关性并不强的页面和菜单放置在一个地方处理并不合理。
  2. 绝大多数项目都不是一个人开发的,一个架构设计者要考虑到未来参与项目的开发者水平参差不齐。如果让框架级的结构直接暴露到业务开发者的面前,保不准某个负责业务开发的小伙伴忽略或修改了什么代码导致框架级的坑出现。
  3. 按照上面的代码,实际上要求每个内页都保留 const menus = await getMenus();这一类的代码(每个内页都复制粘贴)。在架构上这叫“样板式代码”,架构设计者应当尽量将这些代码通过“分层”的方式放到一个地方去处理。

所以有理由为_Nextjs_的 ./pages之外的组件实现ssr数据异步加载。

组件ssr异步数据实现

为了实现本文的需求——让所有组件实现类似于 getInitialProps()的方法,我们先要理清_Nextjs_前后端渲染的过程。

渲染过程

_Nextjs_为使用者提供了 ./pages/_app.js ./pages/_document.js在内页处理之前执行某些任务,后者用于构建整个HTML的结构。并且 ./pages/_document.js只会在服务端执行。本文将开发者自行实现的内页称为_page,现在对于_Nextjs_就有三个类型的构建——_

document

、_

app_和_component

,每个构建都可以包含 static getInitialProps()constructor()render()方法,他们的执行过程如下。

服务端执行过程

  1. _document getInitialProps()
  2. _app getInitialProps()
  3. _page getInitialProps()
  4. _app constructor()
  5. _app render()
  6. _page constructor()
  7. _page render()
  8. _document constructor()
  9. _document render()

以上的过程分解如下:

  1. 组装异步数据(1~3):服务端会先开始执行 _document.getInitialProps()这个静态方法,方法中会执行 _app.getInitialProps()再遍历所有的 _page.getInitialProps()执行到这里所有的异步数据完成组装。

  2. 渲染React组件(4~7):有了数据之后开始渲染页面,会使用 ReactDOMServer执行产生一个HTML格式的字符串。

  3. 构建静态HTML(8~9):有了 ReactDOMServer产生的字符串剩下的工作就是将其组装为一个标准的HTML文档返回给客户端。

客户端执行过程

初始化页面时(首次打开页面):

  1. _app constructor()
  2. _app render()
  3. _page constructor()
  4. _page render()

客户端在首次打开页面时(或刷新页面)服务端已经提供了完整的HTML文档可以立即显示。此时React的组件依然执行一次虚拟Dom渲染,所以所有的组件都会执行。然后_Nextjs_利用类似于_React_服务端渲染的_checksum_的机制防止虚拟Dom对真实Dom进行渲染,关于_React_服务端渲染的_checksum_机制可以到 React 前后端同构防止重复渲染一文了解。

内页跳转时(通过 next/link跳转):

  1. _app getInitialProps()
  2. _page getInitialProps()
  3. _app render()
  4. _page constructor()
  5. _page render()

客户端跳转到一个新的内页和服务端渲染就没有什么关系了。__app和_page_的 getInitialProps()先组装数据,然后通过 props将组装好的数据传递给组件去渲染。需要注意的是_app的构造方法在内页跳转的时候并不会执行,因为它只在整个页面渲染的时候实例化一次。

实现

在了解_Nextjs_解执行过程之后实现需求就很简单了——先通过_document或_app的 getInitialProps()方法完成数据组装,然后将数据传递给对应的组件即可。当然按照分而治之的思想不能直接在框架去完成业务的事,需要为组件提供一个注册接口然后由_document或_app使用注册的方法去构建业务数据。

数据加载方法注册

首先需要为我们组件提供一个注册异步加载数据的接口,组件可以利用这个接口注册异步加载数据的方法让框架统一去 getInitialProps()执行。 ./util/serverInitProps.js提供了这个功能:

   const FooDict = {};
//注册方法
export const registerAsyncFoo = (key, foo, params = {}) => {
    FooDict[key] = {foo, params};
};

//获取方法
export const executeAsyncFoo = async () => {
    const valueDict = {};
    const keys = Object.keys(FooDict);
    for (let key of keys) {
        const dict = FooDict[key];
        valueDict[key] = await dict.foo(dict.params);
    }
    return valueDict;
};
复制代码

然后我们在 menu组件中注册异步获取数据的方法:

   registerAsyncFoo('menus', getMenus);
复制代码

getMenus模拟异步获取数据的过程:

   import {Menus} from "../../../../data/menuData";
export const getMenus = () => {
    //可以将这个promise修改为一个net方法实现异步动态装菜菜单
    return new Promise((resolve, reject) => {
        resolve(Menus)
    })
};
复制代码

注册完成后再 _app中执行异步加载:

   import {executeAsyncFoo} from "../util/serverInitProps";
class ExpressApp extends App {
    static async getInitialProps({Component, router, ctx}) {
        info('Execute _App getInitialProps()!', 'executeReport');
        /**
         * app的getInitialProps会在服务端被调用一次,在前端每次切换页面时被调用。
         */
        let pageProps = {}, appProps = {};
        if (Component.getInitialProps) {
            pageProps = await Component.getInitialProps(ctx);
        }
        if (ctx && !ctx.req) {//客户端执行
            appProps = window.__NEXT_DATA__.props.appProps;
        } else {//服务端执行
            appProps = await executeAsyncFoo();
        }
        return {pageProps, appProps}
    }
    //other function
}
复制代码

在服务端获取到数据之后会返回给 _ducoment,_Nextjs_会将这些数据写到HTML的 window.__NEXT_DATA__对象上而后在客户端可以从这个对象获取到已经在服务端加载的数据。 最后用React的Context特性传递数据,有需要用到这些数据的组件可以从 ApplicationContext中获取这些数据:

   //_app
import ApplicationContext from '../components/app/applicationContext'
class ExpressApp extends App {
     //other function
     render() {
        info('Execute _App render()!', 'executeReport');
        const {Component, pageProps, appProps} = this.props;
        return (
            
                
                    
                
            
        )
    }
    //other function
}


//menu
import ApplicationContext from '../applicationContext'
const Menu = props => {
    return (
        
            {appProps => {
                const {menus} = appProps;
                return menus.map(menu => (
                    
                            
                    
                ))
            }}
        
    );
};
复制代码
   

./util/serverInitProps.js可以在任何组件中使用, _app会逐一执行方法获取数据按照kev-value的方式设置到 ApplicationContext中,而任意组件要做的仅仅是从 ApplicationContext拿到目标数据。

当然传递数据的方式不仅仅局限于React的Context特性,换成Redux或全局管理数据的方法都是可行的。

原文转载自  @随风溜达的向日葵

相关 [react 服务 渲染] 推荐:

React服务端渲染(ssr)之Next.js框架

- - 掘金前端
Nextjs是 React生态中非常受欢迎的SSR(server side render——服务端渲染)框架,只需要几个步骤就可以搭建一个支持SSR的工程(_Nextjs_的快速搭建见 Next.js入门). 本文的案例代码来自于 前端标准模板项目. 提供了便捷强大的服务端渲染功能—— getInitialProps(),通过这个方法可以简单为服务端和前端同时处理异步请求数据:.

谈谈 React Native

- - 唐巧的技术博客
几天前,Facebook 在 React.js Conf 2015 大会上推出了 React Native( 视频链接). 我发了一条微博( 地址),结果引来了 100 多次转发. 为什么 React Native 会引来如此多的关注呢. 我在这里谈谈我对 React Native 的理解. 一个新框架的出现总是为了解决现有的一些问题,那么对于现在的移动开发者来说,到底有哪些问题 React Native 能涉及呢.

Webpack 和 React 小书

- - SegmentFault 最新的文章
Webpack 和 React 小书. 这本小书的目的是引导你进入 React 和 Webpack 的世界. 他们两个都是非常有用的技术,如果同时使用他们,前端开发会更加有趣. 这本小书会提供所有相关的技能. 如果你只是对 React 感兴趣,那可以跳过 Webpack 相关的内容,反之亦然. 如果想学习更多的相关知识可以移步 SurviveJS - Webpack and React.

React入门实例学习

- - JavaScript - Web前端 - ITeye博客
        React可以在浏览器运行,也可以在服务器运行,但是在这为了尽量保持简单,且React语法是一致的,服务器的用法和浏览器差别不大,在这只涉及浏览器. 一. HTML模板.         使用React的网页源码,结构大致如下:.         1.最后一个script标签的type属性为text/jsx.

轻松入门React和Webpack

- - SegmentFault 最新的文章
小广告:更多内容可以看 我的博客和 读书笔记. 最近在学习React.js,之前都是直接用最原生的方式去写React代码,发现组织起来特别麻烦,之前听人说用Webpack组织React组件得心应手,就花了点时间学习了一下,收获颇丰. 一个组件,有自己的结构,有自己的逻辑,有自己的样式,会依赖一些资源,会依赖某些其他组件.

React 入门实例教程

- - 阮一峰的网络日志
现在最热门的前端框架,毫无疑问是 React. 上周,基于 React 的 React Native 发布,结果一天之内,就获得了 5000 颗星,受瞩目程度可见一斑. React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设 Instagram 的网站.

React Native 原理与实践

- - 掘金 前端
React Native 介绍. 什么是 React Native. React Native 是一个由 Facebook 于 2015 年 9 月发布的一款开源的 JavaScript 框架,它可以让开发者使用 JavaScript 和 React 来开发跨平台的移动应用. 它既保留了 React 的开发效率,又同时拥有 Native 应用的良好体验,加上 Virtual DOM 跨平台的优势,实现了真正意义上的:.

Google开发新服务 重写网页代码提高渲染速度

- Haides - cnBeta.COM
北京时间7月28日晚间消息,谷歌将于今天推出一种名为Page Speed Service(页面速度服务)的新服务这种服务可帮助提升网页速度. 当用户将其网站的DNS入口指向谷歌时,Page Speed Service将可从用户网站的服务器获取内容、重写网页并通过谷歌自身的全球服务器来为用户网站提供服务.

“天河一号”为国内动漫影视制作提供渲染服务

- David - cnBeta.COM
记者23日从天津滨海新区获悉,借用“云计算”技术,超级计算机“天河一号”的公共服务辐射到动漫、影视领域,成为当今世界上规模最大、渲染速度最快的渲染平台之一. 该平台实现了渲染应用与超级计算机系统的有机结合,可根据用户需求提供大型渲染业务,大大缩短了影视后期的制作时间,提升了整体后期制作水平.

React Native通信机制详解

- - bang's blog
React Native是facebook刚开源的框架,可以用javascript直接开发原生APP,先不说这个框架后续是否能得到大众认可,单从源码来说,这个框架源码里有非常多的设计思想和实现方式值得学习,本篇先来看看它最基础的JavaScript-ObjectC通信机制(以下简称JS/OC). 普通的JS-OC通信实际上很简单,OC向JS传信息有现成的接口,像webview提供的-stringByEvaluatingJavaScriptFromString方法可以直接在当前context上执行一段JS脚本,并且可以获取执行后的返回值,这个返回值就相当于JS向OC传递信息.