用React框架和Express模块进行服务器端渲染

这周末我启动了一个编外项目,这个项目里要做的是服务器端的渲染。我在网上找的教程也好,建议也好都太深了,像Redux框架或React路由导航(React Router)这些特殊、时髦的东西根本不需要,我们可爱的React好像没什么单纯的教程。

程序的生成步骤我就当大家已经准备好了。没有的话,下面给你一个链接,这个网页上包含了一个webpack配置文件,有了以后可以直接运行 npm run build这个命令。

https://github.com/Roilan/react-server-boilerplate

一开始,我们先要建立文件夹结构。文件夹结构看起来会是这样的:

/
 /dist -- 放生成文件
  /assets -- 放从生成步骤中打包过来的素材文件
   index.css
   bundle.js
  server.js -- 这是打包后的服务器文件

 /src -- 放源文件
  /app -- 放React组件(Component)
   index.js -- React根组件(root component)
   browser.js -- React根组件,用来包裹在`react-dom/render`里
  index.js -- express服务器文件
  template.js -- 基本HTML模板文件

dist文件夹里的文件不用看,这些是从生成步骤中产生的。创立好这些文件后,只要安装以下模块:

npm install --save react react-dom express

我先创建React的根组件,还有浏览器如何渲染。在 app/index.js文件里,就写一个hello world组件。

// app/index.js

import React, { Component } from 'react';

export default class App extends Component {
  render() {
    return (
      <div>
        <h1>hello world</h1>
      </div>
    );
  }
}

将这个组件导入到 app/browser.js文件中,并把它渲染到DOM树里.

// app/browser.js
import React from 'react';
import { render } from 'react-dom';
import App from './index';

render(<App />, document.getElementById('root'));

大家可能会想“为什么把这两个文件分开?写在一起不好吗?”一会儿我就会说到这点,肯定是有道理的,相信我。

我们现在来看 src/template.js模板文件,在里面创建一个初始的HTML页面,服务器会把这个页面传送下来。 template.js模板文件只有一个函数,返回值是一个HTML字符串,然后我们的组件就可以渲染到这里面去,和 app/browser.js做的事差不多,只不过是由服务器完成的。模板会像这个样子:

// src/template.js
export default ({ body, title }) => {
  return `
    <!DOCTYPE html>
    <html>
      <head>
        <title>${title}</title>
        <link rel="stylesheet" href="/assets/index.css" />
      </head>

      <body>
        <div id="root">${body}</div>
      </body>

      `<script src="/assets/bundle.js">`</script>
    </html>
  `;
};

接下来要做的就是把 body内容和 title内容传进来,插到这个HTML字符串里去。 body的内容就是之前的React组件, title的内容 就是当前所在页面的标题。大家还可以看到两个额外的素材文件 index.cssbundle.jsindex.css是编译过的CSS样式文件, bundle.js是客户端用的React打包文件,从服务器发送时会一起发过来。当服务器完成渲染时,客户端的React会接收这个打包文件。

src/server.js服务器文件,这里是最终奇迹发生的地方,它会把React组件发送到客户端去。先导入所有的库、组件和模板。

// src/server.js
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './app';
import template from './template';

我们看到里面有一些新的内容,从 react-dom/server模块中导入了 renderToString函数。客户端调用 ReactDOM.render函数时, renderToString函数会将React组件渲染到HTML中去并保留。我们不想造成不必要的客户端渲染,而丧失了服务器端渲染的益处,所以这一点很好。剩下要做的就是告诉express模块,客户访问初始路线时,要把我们的组件传送下来。

const server = express();

server.use('/assets', express.static('assets'));

server.get('/', (req, res) => {
  const appString = renderToString(<App />);

  res.send(template({
    body: appString,
    title: 'Hello World from the server'
  }));
});

server.listen(8080);

大家可以看到三个主要部分,先设定好 assets路线要用 assets文件夹,这样,把里面的CSS文件和JS打包文件包括进来就很容易了。这里,大家会看到 renderToString函数如何实际运用,唯一传进去的参数就是React的根组件,这就是为什么我们之前要把这个组件分开写在两个文件里,我们只关心怎么把这个根组件渲染到服务器上的某个字符串里去。最后,把 body内容和 title内容传进模板文件里去,最终生成的字符串发到客户端去。


如果我们想从服务器发送一些属性到客户端怎么办?比如,要检测一下是不是移动设备,如果是,就渲染一个不同的视图。 让我们修改一下请求,加入一个 isMobile属性,更新一下根组件。

// src/server.js
server.get('/', (req, res) => {
  const isMobile = true; // 假设是移动设备
  const appString = renderToString(<App isMobile={isMobile} />);

  res.send(template({
    body: appString,
    title: 'Hello World from the server'
  }));
});
// app/index.js
export default class App extends Component {
  render() {
    const { isMobile } = this.props;

    return (
      <div>
        <h1>hello world {isMobile ? 'mobile' : 'desktop'}</h1>
      </div>
    );
  }
}

啊,不对!这是什么意思?

应该显示的是 hello world mobile,而现在这个结果不是我们想要的。要说的话,React是很智能的,它会保证客服两端的东西都能配对。这个错误信息很清楚,不是什么我们看不见的魔术,它问的是为什么有一个新的标记元素插进来。看到这个错误信息,我们明白了,客户端预计收到的标记元素和实际的不符。这个信息指出了一点,那就是要看看初始状态。

那到底发生了什么?当服务器上生成响应时,客户端不知道 isMobile这个属性应该是收到的一部分,也不知道要把这个属性的值设为真。我们需要给它一个初始状态,能让客户端先取得这个属性,然后客服两端就匹配了。

只要做一些小调整就可以了。一开始,先打开 server.js文件,给模板传入某个初始状态。

// src/server.js
server.get('/', (req, res) => {
  const isMobile = true;
  const initialState = { isMobile };
  const appString = renderToString(<App {...initialState} />);

  res.send(template({
    body: appString,
    title: 'Hello World from the server',
    initialState: JSON.stringify(initialState)
  }));
});

开始的部分,我们创建了一个初始状态( initialState)对象,将这个对象散布到根组件中去,再往下传到模板里去。在模板中,我们要把这个变化传到客户端去,看起来像这样:

// src/template.js
export default ({ body, title, initialState }) => {
  return '
    <!DOCTYPE html>
    <html>
      <head>
        `<script>window.__APP_INITIAL_STATE__ = ${initialState}</script>`
        <title>${title}</title>
        <link rel="stylesheet" href="/assets/index.css" />
      </head>

      <body>
        <div id="root">${body}</div>
      </body>

      `<script src="/assets/bundle.js">`</script>
    </html>
  ';
};

注意在窗口(window)中设置的这个初始状态对象。最后要改的是将这个初始状态对象散布到 browser.js文件里,加到根组件里去,使客服两端初始状态一致。

// app/browser.js
import React from 'react';
import { render } from 'react-dom';
import App from './index';

render(<App {...window.__APP_INITIAL_STATE__} />, document.getElementById('root'));

运行这个程序并记录下初始状态,我们会得到第一次想得到的结果。

成功!!!


往期精选文章

使用虚拟dom和JavaScript构建完全响应式的UI框架

扩展 Vue 组件

使用Three.js制作酷炫无比的无穷隧道特效

一个治愈JavaScript疲劳的学习计划

全栈工程师技能大全

WEB前端性能优化常见方法

一小时内搭建一个全栈Web应用框架

干货:CSS 专业技巧

四步实现React页面过渡动画效果

让你分分钟理解 JavaScript 闭包



小手一抖,资料全有。长按二维码关注京程一灯,阅读更多技术文章和业界动态。

原文发布于微信公众号 - 京程一灯(jingchengyideng)

原文发表时间:2017-11-09

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券