这周末我启动了一个编外项目,这个项目里要做的是服务器端的渲染。我在网上找的教程也好,建议也好都太深了,像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.css
和 bundle.js
, index.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 闭包 |
小手一抖,资料全有。长按二维码关注京程一灯,阅读更多技术文章和业界动态。