对于html
的加载,以React为
例,我们习惯的做法是加载js文件中的React
代码,去生成页面渲染,同时,js也完成页面交互事件的绑定,这样的一个过程就是CSR(客户端渲染),如下:
但如果这个js文件比较大的话,加载起来就会比较慢,到达页面渲染的时间就会比较长,导致首屏白屏。这时候,SSR(服务端渲染)就出来了:由服务端直接生成html内容返回给浏览器渲染首屏内容,如下:
但是服务端渲染的页面交互能力有限,如果要实现复杂交互,还是要通过引入js文件来辅助实现,我们把页面的展示内容和交互写在一起,让代码执行两次,这种方式就叫同构。
CSR和SSR的区别在于,最终的html代码是从客户端添加的还是从服务端,而服务端要添加html,就得从服务端引入之前写好的组件文件。然而之前的组件都是按照ES6规范来码的,node端并不完全支持,怎么办呢?
react-dom
为我们提供了将jsx
语法渲染成html
代码的函数,我们可以将我们要渲染的页面直接引入,通过renderToString
来转出 router.js
...
import Intro from './Intro';
import {
renderToString } from 'react-dom/server';
...
router.get('/', function (req, res) {
res.render('body', {
htmlStr: renderToString( )
})
})
但这里就会涉及到在node
运行ES6
语法的问题,可以在server
运行入口文件,引入babel
server.js
'use strict';
import 'babel-polyfill';
import 'babel-register';
...
同时在配置里面,.babelrc
加上es2015
的presets
.babelrc
{
"presets": [ 'es2015' ]
}
我的做法是在渲染页面入口,新增一个server
端打包文件 intro.ssr.js
import React from 'react';
import {
renderToString } from 'react-dom/server';
import Intro from './Intro';
// 导出渲染函数,以给采用 Node.js 编写的 HTTP 服务器代码调用
export function render() {
// 把根组件渲染成 HTML 字符串
return renderToString( )
}
同时新起一个webpack
配置文件,来打包server
端所需要的文件
webpack.server.js
const path = require('path');
const nodeExternals = require('webpack-node-externals');
module.exports = require('./webpack.base.server')({
mode: 'none',
entry: {
intro: [
path.join(process.cwd(), 'client/modules/site/pages/Index/index.ssr.js')
]
},
target: 'node',
externals: [nodeExternals()],
output: {
filename: '[name].ssr.js',
path: path.join(process.cwd(), './server/ssr'),
libraryTarget: 'commonjs2'
},
plugins: [],
devtool: 'source-map',
module: {
rules: [
{
test: /.js$/,
use: ['babel-loader'],
exclude: path.resolve(__dirname, 'node_modules')
},
{
// 忽略掉 CSS 文件
test: /.(less|css|sass)$/,
use: ['ignore-loader'],
}
]
},
})
server
所需要的代码跟客户端的不同:
commonjs
规范的代码 webpack-node-externals
,来去除第三方库的代码 需要将样式文件去除,因为服务端无法识别对css、image资源后缀的模块引用,所以我们交给client
打包处理(下面会讲到client
打包怎么处理样式文件) node
路由来引入好,弄完这一步,我们可以在路由里面引入我们打包好的代码啦
router.js
...
var {
render } = require('../../../ssr/intro.ssr');
...
router.get('/', function (req, res) {
var indexStr = render();
res.render('body', {
htmlStr: indexStr
});
})
跟上面直接引入方式对比,这样会有几个好处
node
端处理es6
的语法问题 client
的代码和server
互不影响燃鹅,当你打包好,启动server的时候,可能会遇到这个问题
这个是因为,渲染内容里面,调用了window/document,而在DOM
都没有的服务端肯定没有这些的啦
window.copyright
...
render() {
return (
希沃教学软件,尽在希沃易+官网 e.seewo.com
Copyright © {
window.copyright} GuangZhou Shirui. All Rights Reserved. 粤ICP备12092924号-1
);
}
在node
端,我们可以通过global.copyright
去替换 而对于document
里面的原生属性及方法,只能借助第三方库的力量
var {
JSDOM } = require('jsdom');
// 制造个document避免未定义
var dom = new JSDOM('');
global.document = dom.window.document;
注意:如果使用global.window = window
也能解决此问题,但对于一些判断user-agent的库会出问题,例如isMobile
,看了一下其源码,其中有一段用到了window去判断是否处于浏览器端
...
// instantiate直接返回IsMobileClass的实例,没有参数的获取
var instantiate = function() {
var IM = new IsMobileClass();
IM.Class = IsMobileClass;
return IM;
};
if (typeof module !== 'undefined' && module.exports && typeof window === 'undefined') {
//node
module.exports = IsMobileClass;
} else if (typeof module !== 'undefined' && module.exports && typeof window !== 'undefined') {
//browserify
module.exports = instantiate();
}
...
如果是在浏览器端,就直接拿navigator
里面的userAgent
而不拿传入的参数,所以在node
端会无法判断。
上面说到要在client
端处理样式文件,之前我们打包前端代码,习惯将js
和样式文件合并到一个bunble
里面,但ssr
不处理样式的打包,所以我们有必要在client
打包的时候,将服务端需要的样式独立出来
...
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractLESS = new ExtractTextPlugin('[name].css');
module.exports = {
...
plugins: [ extractLESS ],
rules: [
...
{
test: /.less$/,
include: path.join(process.cwd(), 'client/modules/site/styles/site.less'),
use: ExtractTextPlugin.extract({
use: ['css-loader', 'less-loader'],
fallback: 'style-loader'
})
}
]
}
通过node路由来传递调用模板变量,按需加载
...
res.render('body', {
chunks: ['analytics', 'site'],
cssChunks: ['site'],
htmlStr: indexStr
});
在我们的模板页加入css文件
希沃信鸽
<% if (typeof cssChunks !== 'undefined') { %>
<% cssChunks.forEach(function(name){ %>
<%- getChunkCss(name) %>
<% }) %>
<% } %>
getChunkCss
是中间件配置的一个方法
...
res.locals.getChunkCss = function(chunkName){
var chunkCss = _.get(assetData, ['chunks', chunkName, 'css'], []);
var cssArr = [];
chunkCss.map(function(item){
cssArr.push('')
})
return cssArr.join('');
}
...
上面的实现结果基本把静态页面渲染出来了,但是对于一些js操作,如事件绑定,dom操作等,在服务端渲染的html文本无法执行,所以这些js逻辑必须是在浏览器端才能执行,这里我们将目标页面的代码,在浏览器进行二次渲染
hydrate(
,
document.getElementById('root')
);
其中hydrate
是react-dom
针对服务端渲染提供的方法,有别于render
。hydrate
的用处是,当ReactDOM
复用 ReactDOMServer
服务端渲染的内容时尽可能保留结构,并补充事件绑定等 Client
特有内容的过程。
如果是静态页面做ssr
,那么到这里其实已经实现了,如果是需要数据交互的,还得考虑数据请求的方式 :
node
端进行处理,这样的好处是快,在node端
直接请求后台数据有其服务器地理优势 client
请求数据接口的方式跟server
端不同,所以client
请求数据接口的组件可只在client
端代码引入,无需打包在server
所需文件中。