最近经常听到其他主攻 Vue
项目的前端小组提到 SSR
这个名词,一直都不明白是什么东西,只以为是他们内部做的什么工具之类的东西,虽然有些好奇,但也没怎么在意,直到后来在某技术论坛闲逛的时候忽然也看到这个名词了,点进去后通篇看完才知道 ssr
是个什么东西。
SSR的概念
Server Slide Rendering
,缩写为 ssr
,即服务器端渲染,因为是后端出身,所以其实早就明白是怎么回事,只是没这个具体名词的概念罢了,这个词被频繁提起也是拜近年来前端飞速发展所赐,主要针对 SPA
应用,目的大概有以下几个:
- 解决单页面应用的
SEO
单页应用页面大部分主要的HTML
并不是服务器返回,服务器只是返回一大串的 脚本,页面上看到的大部分内容都是由脚本生成,对于一般网站影响不大,但是对于一些依赖搜索引擎带来流量的网站来说则是致命的,搜索引擎无法抓取页面相关内容,也就是用户搜不到此网站的相关信息,自然也就无流量可言。
- 解决渲染白屏
因为页面HTML
由服务器端返回的脚本生成,一般来说这种脚本的体积都不会太小,客户端下载需要时间,浏览器解析以生成页面元素也需要时间,这必然会导致页面的显示速度比传统服务器端渲染得要慢,很容易出现首页白屏的情况,甚至如果浏览器禁用了JS
,那么将直接导致页面连基本的元素都看不到。
Vue
和 React
是用来做单页应用最普遍的框架,因为我对 react
更熟悉,所以这里我只说下 React SSR
。
客户端部分
新建 React页面
首先,要有一个用于展示的 React
页面,例如:
// ./client/components/About/index.jsx
import React, { Component } from 'react'
import styles from './style.scss'
import './style'
export default class About extends Component {
constructor() {
super()
this.state = {
txtInfo: ''
}
}
componentWillMount() {
this.state.txtInfo = 'zhangsan'
}
componentDidMount() {
this.getInfo()
}
render() {
return (
<section className={styles['about-wrapper']}>
<p className="title">About Page</p>
<p className="txt-info">{this.state.txtInfo}</p>
</section>
)
}
getInfo() {
fetch('/api/user/getInfo', {
credentials: 'include',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'text/plain; application/json; charset=utf8'
}
}).then(res=>{
return res.json()
}).then(data=> {
this.setState({
txtInfo: data.name
})
})
}
}
这里的路由使用 react-router
,简便考虑,只有一个路由,创建路由:
// ./client/routes.js
export default const routes = {
childRoutes: [{
path: '/',
component: require('./components/app.jsx'),
indexRoute: {
getComponent(nextState, callback) {
require.ensure([], require => {
callback(null, require('./components/About/index.jsx'))
}, 'about')
}
}
}
其中 ./components/app.jsx
的结构很简单,主要只有一个 render
方法:
render() {
const {children, ...props} = this.props
return (
<div>
{React.Children.map(children, child =>
React.cloneElement(child, {...props})
)}
</div>
)
}
最主要的是入口组件 index.js
:
// ./client/index.js
import React from 'react'
import { render } from 'react-dom'
import { Router, match, browserHistory } from 'react-router'
import routes from './routes'
match({ history: browserHistory, routes }, (error, redirectLocation, renderProps) => {
render(
<Router {...renderProps}/>,
document.getElementById('root')
)
})
其中,有个 match
方法,这是 react-router
提供的方法,作用在于在渲染之前根据URL匹配路由组件,更多详情可看 这里
客户端就这些了,下面开始服务端。
服务端
服务端使用 koa
框架,安装配置什么的我就不多说了,不知道的可见 这里
// ./server/app.js
import Koa from 'koa'
const app = new Koa()
export default app
配置服务端路由:
// ./server/routes/index.js
import Router from 'koa-router'
const router = new Router({prefix: '/api/user'})
router.get('/getInfo', async(ctx, next)=> {
ctx.body = {
name: 'xiaoming',
age: 18
}
})
export default router
配置了一个 /api/user/getInfo
的路由
但是,服务端只有服务端路由还不行,因为是服务器端渲染,所以还必须在服务端配置客户端路由,因为只有知道客户端的路由,服务器才知道该向客户端传送什么页面的 HTML
字符串,如下:
// ./server/clientRoutes.js
import React from 'react'
import { renderToString } from 'react-dom/server'
import { match, RouterContext } from 'react-router'
import routes from '../../client/routes'
async function clientRoute(ctx, next) {
let _renderProps
match({ routes, location: ctx.url }, (error, redirectLocation, renderProps) => {
_renderProps = renderProps
})
if (_renderProps) {
await ctx.render('index', {
root: renderToString(
<RouterContext {..._renderProps}/>
),
info: { name: '小明' }
})
} else {
await next()
}
}
export default clientRoute
除了 match
之外,这次又多了个 RouterContext
,此 API
也是属于 react-router
,它的作用是以同步的方式渲染路由组件,不然你想啊,一个 react
组件,有很多 API
钩子函数,若是在这些钩子函数还没执行完之前,服务器就把 HTML
渲染到客户端了,那肯定会缺斤少两啊,所以这个时候此 API
就派上了用场。
下面就只剩下启动服务渲染页面了。
这个稍微复杂一点,因为需要考虑到很多问题,比如jsx
语法的转码、css
样式文件的打包,HTML
的注入等,不仅需要考虑浏览器端,还要考虑 服务端,一个都不能少。
// 转码器 babel
require('babel-polyfill')
// react 的转码 hook
require('babel-register')({
presets: ['es2015', 'react', 'stage-0'],
plugins: ['add-module-exports']
})
// css 的转码 hook
require('css-modules-require-hook')({
extensions: ['.scss'],
preprocessCss: (data, filename) =>
require('node-sass').renderSync({
data,
file: filename
}).css,
camelCase: true,
generateScopedName: '[name]__[local]__[hash:base64:8]'
})
const webpack = require('webpack'),
app = require('./app'),
convert = require('koa-convert'),
fs = require('fs'),
path = require('path'),
devMiddleware = require('koa-webpack-dev-middleware'),
views = require('koa-views'),
router = require('./routes'),
clientRoute = require('./clientRoute'),
config = require('../webpack.dev.config'),
port = process.env.port || 3000
compiler = webpack(config)
app.use(views(path.resolve(__dirname, '../views/dev'), { map: { html: 'ejs' } }))
app.use(clientRoute)
app.use(router.routes())
app.use(router.allowedMethods())
console.log(`Listening on port ${port}`)
app.use(convert(devMiddleware(compiler, {
noInfo: true,
publicPath: config.output.publicPath
})))
app.listen(port)
基本上就是这样了,打开页面,是这个效果:
同时如果使用 Restlet Client
这样的工具请求页面,也能够看到返回的 HTML
字符串与浏览器实际渲染后的页面HTML
完全一致,这说明服务器端渲染的目的已经达到了。
有关项目
上面代码还有一些我都没贴出来,我也不想贴项目地址,因为没必要了。
如果是在一年前,关于 react
服务器端渲染还没什么标准,社区各路大神各显神通的话,那么现在再提起 react server slide rendering
的话,就没必要再自己吃力不讨好的瞎折腾了,你应该想起这个脚手架:Next.js
Next.js
是一个用于在服务端渲染 React
应用程序的简单框架,2016 年 10 月 25 日由 zeit.co 背后的团队发布。
React
服务端渲染SSR
应用框架,支持可选的服务端与客户端渲染功能,简单易用,安装这个框架会搭建一个基于React、Webpack
和Babel
的构建过程,也就是说脚手架已经预设了配置,开发人员不必在搭建Webpack
或Babel
配置上。
此项目算是众多 react server slide rendering
方案中最受欢迎的一个,如果现在考虑 react
服务端渲染,此方案算是最佳选择。
另外,不仅是 react
, vue
同样有类似对标的库:Nuxt.js,作用和功能几乎和 Next.js
一致,有意思的是,Nuxt.js
的发布时间仅在 Next.js
宣告发布后的几个小时内,算是同一天发布,如今在各自领域内的影响力不分伯仲。