react koa rematch 如何打造一套服务端渲染架子

前言

本次讲述的内容主要是 react 与 koa 搭建的一套 ssr 框架,是在别人造的轮子上再添加了一些自己的想法和完善一下自己的功能。

本次用到的技术为: react | rematch | react-router | koa

react服务端渲染优势

SPA(single page application)单页应用虽然在交互体验上比传统多页更友好,但它也有一个天生的缺陷,就是对搜索引擎不友好,不利于爬虫爬取数据(虽然听说chrome能够异步抓取spa页面数据了);

SSR与传统 SPA(Single-Page Application - 单页应用程序)相比,服务器端渲染(SSR)的优势主要在于:更好的 SEO 和首屏加载效果。

在 SPA 初始化的时候内容是一个空的 div,必须等待 js 下载完才开始渲染页面,但 SSR 就可以做到直接渲染html结构,极大地优化了首屏加载时间,但上帝是公平的,这种做法也增加了我们极大的开发成本,所以大家必须综合首屏时间对应用程序的重要程度来进行开发,或许还好更好地代替品(骨架屏)。

react服务端渲染流程

组件渲染

首先肯定是根组件的render,而这一部分和SPA有一些小不同。

使用 ReactDOM.render() 来混合服务端渲染的容器已经被弃用,并且会在React 17 中删除。使用hydrate() 来代替。

hydrate与 render 相同,但用于混合容器,该容器的HTML内容是由 ReactDOMServer 渲染的。 React 将尝试将事件监听器附加到现有的标记。

hydrate 描述的是 ReactDOM 复用 ReactDOMServer 服务端渲染的内容时尽可能保留结构,并补充事件绑定等 Client 特有内容的过程。

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.hydrate(<App />, document.getElementById('app'));

在服务端中,我们可以通过 renderToString 来获取渲染的内容来替换 html 模版中的东西。

const jsx =
  <StaticRouter location={url} context={routerContext}>
    <AppRoutes context={defaultContext} initialData={data} />
  </StaticRouter>

const html = ReactDOMServer.renderToString(jsx);

let ret = `
  <!DOCTYPE html>
    <html lang="en">
    <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no">
    </head>
    <body>
     <div id="app">${html}</div>
    </body>
  </html>
`;

return ret;

服务端返回替换后的 html 就完成了本次组件服务端渲染。

路由同步渲染

在项目中避免不了使用路由,而在SSR中,我们必须做到路由同步渲染。

首先我们可以把路由拆分成一个组件,服务端入口和客户端都可以分别引用。

function AppRoutes({ context, initialData }: any) {
 return (
  <Switch>
   {
    routes.map((d: any) => (
     <Route<InitRoute>
      key={d.path}
      exact={d.exact}
      path={d.path}
      init={d.init || ''}
      component={d.component}
     />
    ))
   }
   <Route path='/' component={Home} />
  </Switch>
 );
}

(routes.js)

export const routes = [
 {
  path: '/Home',
  component: Home,
  init: Home.init,
  exact: true,
 },
 {
  path: '/Hello',
  component: Hello,
  init: Hello.init,
  exact: true,
 }
];

这样我们的路由基本定义完了,然后客户端引用还是老规矩,和SPA没什么区别

import { BrowserRouter as Router } from 'react-router-dom';
import AppRoutes from './AppRoutes';
class App extends Component<any, Readonly<State>> {
...
 render() {
  return (
  <Router>
   <AppRoutes/>
  </Router>
  );
 }
}

在服务端中,需要使用将BrowserRouter 替换为 StaticRouter 区别在于,BrowserRouter 会通过HTML5 提供的 history API来保持页面与URL的同步,而StaticRouter 则不会改变URL,当一个 匹配时,它将把 context 对象传递给呈现为 staticContext 的组件。

const jsx =
  <StaticRouter location={url}>
    <AppRoutes />
  </StaticRouter>

const html = ReactDOMServer.renderToString(jsx);

至此,路由的同步已经完成了。

redux同构

在写这个之前必须先了解什么是注水和脱水,所谓脱水,就是服务器在构建 HTML 之前处理一些预请求,并且把数据注入html中返回给浏览器。而注水就是浏览器把这些数据当初始数据来初始化组件,以完成服务端与浏览器端数据的统一。

组件配置

在组件内部定义一个静态方法

class Home extends React.Component {
...
 public static init(store:any) {
  return store.dispatch.Home.incrementAsync(5);
 }
 componentDidMount() {
  const { incrementAsync }:any = this.props;
  incrementAsync(5);
 }
 render() {
 ...
 }
}

const mapStateToProps = (state:any) => {
 return {
  count: state.Home.count
 };
};

const mapDispatchToProps = (dispatch:any) => ({
 incrementAsync: dispatch.Home.incrementAsync
});
export default connect(
 mapStateToProps,
 mapDispatchToProps
)(Home);

由于我这边使用的是rematch,所以我们的方法都写在model中。

const Home: ModelConfig= {
 state: {
  count: 1
 },
 reducers: {
  increment(state, payload) {
   return {
    count: payload
   };
  }
 },
 effects: {
  async incrementAsync(payload, rootState) {
   await new Promise((resolve) => setTimeout(resolve, 1000));
   this.increment(payload);
  }
 }
};
export default Home;

然后通过根 store 中进行 init。

import { init } from '@rematch/core';
import models from './models';

const store = init({
 models: {...models}
});

export default store;

然后可以绑定在我们 redux 的 Provider 中。

<Provider store = {store}>
  <Router>
   <AppRoutes
    context={context}
    initialData={this.initialData}
   />
  </Router>
</Provider>

路由方面我们需要把组件的 init 方法绑定在路由上方便服务端请求数据时使用。

<Switch>
   {
    routes.map((d: any) => (
     <Route<InitRoute>
      key={d.path}
      exact={d.exact}
      path={d.path}
      init={d.init || ''}
      component={d.component}
     />
    ))
   }
   <Route path='/' component={Home} />
  </Switch>

以上就是客户端需要进行的操作了,因为 SSR 中我们服务端也需要进行数据的操作,所以为了解耦,我们就新建另一个 ServiceStore 来提供服务端使用。

在服务端构建 Html 前,我们必须先执行完当前组件的 init 方法。

import { matchRoutes } from 'react-router-config';
// 用matchRoutes方法获取匹配到的路由对应的组件数组
const matchedRoutes = matchRoutes(routes, url);
const promises = [];
for (const item of matchedRoutes) {
 if (item.route.init) {
  const promise = new Promise((resolve, reject) => {
   item.route.init(serverStore).then(resolve).catch(resolve);
  });
  promises.push(promise);
 }
}
return Promise.all(promises);

注意我们新建一个 Promise 的数组来放置 init 方法,因为一个页面可能是由多个组件组成的,我们必须等待所有的 init 执行完毕后才执行相应的 html 构建。

现在可以得到的数据挂在 window 下,等待客户端的读取了。

let ret = `
   <!DOCTYPE html>
    <html lang="en">
    <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no">
    </head>
    <body>
     <div id="app">${html}</div>
     <script type="text/javascript">window.__INITIAL_STORE__ = ${JSON.stringify(
      extra.initialStore || {}
     )}</script>
    </body>
   </html>
  `;

然后在我们的客户端中读取刚刚的 initialStore 数据

....
const defaultStore = window.__INITIAL_STORE__ || {};
const store = init({
 models,
 redux: {
  initialState: defaultStore
 }
});

export default store;

至此,redux的同构基本完成了,因为边幅的限定,我就没有贴太多代码,大家可以到文章底部的点击我的仓库看看具体代码哈,然后我再说说几个 redux 同构中比较坑的地方。

1.使用不了 @loadable/component 异步组件加载,因为不能获取组件内部方法。 解决的办法就是在预请求我们不放在组件中,直接拆分出来写在一个文件中统一管理,但我嫌这样不好管理就放弃了异步加载组件了。

2.在客户端渲染的时候如果数据一闪而过,那就是初始化数据并没有成功,当时这里卡了我好久喔。

css样式直出

首先,服务端渲染的时候,解析 css 文件,不能使用 style-loader 了,要使用 isomorphic-style-loader 。使用 style-loader 的时候会有一闪而过的现象,是因为浏览器是需要加载完 css 才能把样式加上。为了解决这样的问题,我们可以通过isomorphic-style-loader 在组件加载的时候把 css 放置在全局的 context 里面,然后在服务端渲染时候提取出来,插入到返回的HTML中的 style 标签。

组件的改造

import withStyles from 'isomorphic-style-loader/withStyles';

@withStyles(style)
class Home extends React.Component {
...
 render() {
  const {count}:any = this.props;
  return (
  ...
  );
 }
}
const mapStateToProps = (state:any) => {
 return {
  count: state.Home.count
 };
};

const mapDispatchToProps = (dispatch:any) => ({
 incrementAsync: dispatch.Home.incrementAsync
});
export default connect(
 mapStateToProps,
 mapDispatchToProps
)(Home);

withStyle 是一个柯里化函数,返回的是一个新的组件,并不影响 connect 函数,当然你也可以像 connect 一样的写法。withStyle 主要是为了把 style 插入到全局的 context 里面。

根组件的修改

import StyleContext from 'isomorphic-style-loader/StyleContext';

const insertCss = (...styles:any) => {
 const removeCss = styles.map((style:any) => style._insertCss());
 return () => removeCss.forEach((dispose:any) => dispose());
};

ReactDOM.hydrate(
  <StyleContext.Provider value={{ insertCss }}>
    <AppError>
     <Component />
    </AppError>
  </StyleContext.Provider>,
  elRoot
);

这一部分主要是引入了 StyleContext 初始化根部的context,并且定义好一个 insertCss 方法,在组件 withStyle 中触发。

部分 isomorphic-style-loader 源码

...
function WithStyles(props, context) {
  var _this;
  _this = _React$PureComponent.call(this, props, context) || this;
  _this.removeCss = context.insertCss.apply(context, styles);
  return _this;
 }

 var _proto = WithStyles.prototype;

 _proto.componentWillUnmount = function componentWillUnmount() {
  if (this.removeCss) {
   setTimeout(this.removeCss, 0);
  }
 };

 _proto.render = function render() {
  return React.createElement(ComposedComponent, this.props);
 };
 ...

可以看到 context 中的 insert 方法就是根组件中的 定义好的 insert 方法,并且在 componentWillUnmount 这个销毁的生命周期中把之前 style 清除掉。而 insert 方法主要是为了给当前的 style 定义好id并且嵌入,这里就不展开说明了,有兴趣的可以看一下源码。

服务端中获取定义好的css

const css = new Set(); // CSS for all rendered React components

const insertCss = (...styles :any) => {
 return styles.forEach((style:any) => css.add(style._getCss()));
};

const extractor = new ChunkExtractor({ statsFile: this.statsFile });

const jsx = extractor.collectChunks(
 <StyleContext.Provider value={{ insertCss }}>
  <Provider store={serverStore}>
    <StaticRouter location={url} context={routerContext}>
     <AppRoutes context={defaultContext} initialData={data} />
    </StaticRouter>
  </Provider>
 </StyleContext.Provider>
);

const html = ReactDOMServer.renderToString(jsx);
const cssString = Array.from(css).join('');
...

其中 cssString 就是我们最后获取到的 css 内容,我们可以像 html 替换一样把 css 嵌入到 html 中。

let ret = `
   <!DOCTYPE html>
    <html lang="en">
    <head>
     ...
     <style>${extra.cssString}</style>
    </head>
    <body>
     <div id="app">${html}</div>
     ...
    </body>
   </html>
  `;

那这样就大功告成啦!!!!

我来说一下在做这个的时候遇到的坑

1.不能使用分离 css 的插件 mini-css-extract-plugin ,因为分离 css 和把 css 放置到 style 中会有冲突,引入github大神的一句话

With isomorphic-style-loader the idea was to always include css into js files but render into dom only critical css and also make this solution universal (works the same on client and server side). If you want to extract css into separate files you probably need to find another way how to generate critical css rather than use isomorphic-style-loader.

2.很多文章说到在 service 端的打包中不需要打包 css,那是因为他们使用的是style-loader 的情况,我们如果使用 isomorphic-style-loader, 我们也需要把 css 打包一下,因为我们在服务端中毕竟要触发 withStyle。

总结

因为代码太多了,所以只是展示了整个 SSR 流程的思想,详细代码可以查看。还有希望大牛们指导一下我的错误,万分感谢!!

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 详解React+Koa实现服务端渲染(SSR)

    React是目前前端社区最流行的UI库之一,它的基于组件化的开发方式极大地提升了前端开发体验,React通过拆分一个大的应用至一个个小的组件,来使得我们的代码更加的可被重用,以及获得更好的可维护性,等等还有其他很多的优点... 通过React, 我们通常会开发一个单页应用(SPA),单页应用在浏览器端会比传统的网页有更好的用户体验,浏览器一般会拿到一个body为空的html,然后加载script指定的js, 当所有js加载完毕后,开始执行js, 最后再渲染到dom中, 在这个过程中,一般用户只能

  • React服务端渲染(总结)

    一.前言 为什么需要服务端渲染?什么情况下进行服务端渲染?笔者认为,当我们要求渲染时间尽量快.页面响应速度快时(优点),才会采用服务器渲染,并且应该"按需"对页面进行渲染 --"首次加载/首屏".即服务端渲染的优势在于:由中间层( node端 )为客户端请求初始数据.并由node渲染页面.那客户端渲染和服务端渲染有什么差别?服务端渲染究竟快在哪里呢? 二.原因与思路 客户端渲染路线:1. 请求一个html -> 2. 服务端返回一个html -> 3.

  • 详解react服务端渲染(同构)的方法

    学习react也有一段时间了,使用react后首页渲染的速度与seo一直不理想.打算研究一下react神奇服务端渲染. react服务端渲染只能使用nodejs做服务端语言实现前后端同构,在后台对react组件进行解析并生成html字符串后返回视图页面. 后台为什么可以解析react组件?因为Node.js是一个Javascript运行环境,nodejs与javascript语法基本是相同的,所以nodejs可以正常解析react组件. 一.准备动作 1.安装nodejs与安装express 安

  • 利用React Router4实现的服务端直出渲染(SSR)

    我们已经熟悉React 服务端渲染(SSR)的基本步骤,现在让我们更进一步利用 React RouterV4 实现客户端和服务端的同构.毕竟大多数的应用都需要用到web前端路由器,所以要让SSR能够正常的运行,了解路由器的设置是十分有必要的 基本步骤 路由器配置 前言已经简单的介绍了React SSR,首先我们需要添加ReactRouter4到我们的项目中 $ yarn add react-router-dom # or, using npm $ npm install react-router

  • 使用Node搭建reactSSR服务端渲染架构

    如题:本文所讲架构主要用到技术栈有: Node, Express, React, Mobx, webpack4, ES6, ES7, axios, ejs,  log4js, scss,echarts,ant desige SSR的概念 Server Slide Rendering,缩写为 ssr,即服务器端渲染,因为是后端出身,所以其实早就明白是怎么回事,只是没这个具体名词的概念罢了,这个词被频繁提起也是拜近年来前端飞速发展所赐,主要针对 SPA应用,目的大概有以下几个: 解决单页面应用的 S

  • 详解React 服务端渲染方案完美的解决方案

    最近在开发一个服务端渲染工具,通过一篇小文大致介绍下服务端渲染,和服务端渲染的方式方法.在此文后面有两中服务端渲染方式的构思,根据你对服务端渲染的利弊权衡,你会选择哪一种服务端渲染方式呢? 什么是服务器端渲染 使用 React 构建客户端应用程序,默认情况下,可以在浏览器中输出 React 组件,进行生成 DOM 和操作 DOM.React 也可以在服务端通过 Node.js 转换成 HTML,直接在浏览器端"呈现"处理好的 HTML 字符串,这个过程可以被认为 "同构&qu

  • 详解React服务端渲染从入门到精通

    前言 这篇文章是我自己在搭建个人网站的过程中,用到了服务端渲染,看了一些教程,踩了一些坑.想把这个过程分享出来. 我会尽力把每个步骤讲明白,将我理解的全部讲出来. 文中的示例代码来自于这个仓库,也是我正在搭建的个人网站,大家可以一起交流一下.示例代码因为简化,与仓库代码有些许出入 本文中用到的技术 React V16 | React-Router v4 | Redux | Redux-thunk | express React 服务端渲染 服务端渲染的基本套路就是用户请求过来的时候,在服务端生成

  • 详解React项目的服务端渲染改造(koa2+webpack3.11)

    因为对网页SEO的需要,要把之前的React项目改造为服务端渲染,经过一番调查和研究,查阅了大量互联网资料.成功踩坑. 选型思路:实现服务端渲染,想用React最新的版本,并且不对现有的写法做大的改动,如果一开始就打算服务端渲染,建议直接用NEXT框架来写 项目地址:https://github.com/wlx200510/react_koa_ssr 脚手架选型:webpack3.11.0 + react Router4 + Redux + koa2 + React16 + Node8.x 主要

  • 详解React 在服务端渲染的实现

    React是最受欢迎的客户端 JavaScript 框架,但你知道吗(可以试试),你可以使用 React 在服务器端进行渲染? 假设你已经在客户端使用 React 构建了一个事件列表 app.该应用程序使用了您最喜欢的服务器端工具构建的API.几周后,用户告诉您,他们的页面没有显示在 Google 上,发布到 Facebook 时也显示不出来. 这些问题似乎是可以解决的,对吧? 您会发现,要解决这个问题,需要在初始加载时从服务器渲染 React 页面,以便来自搜索引擎和社交媒体网站的爬虫工具可以

  • react koa rematch 如何打造一套服务端渲染架子

    前言 本次讲述的内容主要是 react 与 koa 搭建的一套 ssr 框架,是在别人造的轮子上再添加了一些自己的想法和完善一下自己的功能. 本次用到的技术为: react | rematch | react-router | koa react服务端渲染优势 SPA(single page application)单页应用虽然在交互体验上比传统多页更友好,但它也有一个天生的缺陷,就是对搜索引擎不友好,不利于爬虫爬取数据(虽然听说chrome能够异步抓取spa页面数据了): SSR与传统 SPA

  • React服务端渲染原理解析与实践

    关于服务端渲染也就是我们说的SSR大多数人都听过这个概念,很多同学或许在公司中已经做过服务端渲染的项目了,主流的单页面应用比如说Vue或者React开发的项目采用的一般都是客户端渲染的模式也就是我们说的CSR. 但是这种模式会带来明显的两个问题,第一个就是TTFP时间比较长,TTFP指的就是首屏展示时间,同时不具备SEO排名的条件,搜索引擎上排名不是很好.所以我们可以借助一些工具来进行改良我们的项目,将单页面应用编程服务器端渲染项目,这样就可以解决掉这些问题了. 目前主流的服务器端渲染框架也就是

  • React服务端渲染和同构的实现

    目录 背景 第一阶段 第二阶段 第三阶段 创建一个服务端渲染应用 同构流程总结 路由 路由同构 背景 第一阶段 很久以前, 一个网站的开发还是前端和服务端在一个项目来维护, 可能是用php+jquery.那时候的页面渲染是放在服务端的, 也就是用户访问一个页面a的时候, 会直接访问服务端路由, 由服务端来渲染页面然后返回给浏览器. 也就是说网页的所有内容都会一次性被写在html里, 一起送给浏览器.这时候你右键点击查看网页源代码, 可以看到所有的代码; 或者你去查看html请求, 查看"预览&q

随机推荐