关于 React服务器端渲染(SSR)

来源:互联网 发布:统计局70城房价数据 编辑:程序博客网 时间:2024/05/01 11:40

最近经常听到其他主攻 Vue项目的前端小组提到 SSR这个名词,一直都不明白是什么东西,只以为是他们内部做的什么工具之类的东西,虽然有些好奇,但也没怎么在意,直到后来在某技术论坛闲逛的时候忽然也看到这个名词了,点进去后通篇看完才知道 ssr是个什么东西。


SSR的概念

Server Slide Rendering,缩写为 ssr,即服务器端渲染,因为是后端出身,所以其实早就明白是怎么回事,只是没这个具体名词的概念罢了,这个词被频繁提起也是拜近年来前端飞速发展所赐,主要针对 SPA应用,目的大概有以下几个:

  1. 解决单页面应用的 SEO
    单页应用页面大部分主要的 HTML并不是服务器返回,服务器只是返回一大串的 脚本,页面上看到的大部分内容都是由脚本生成,对于一般网站影响不大,但是对于一些依赖搜索引擎带来流量的网站来说则是致命的,搜索引擎无法抓取页面相关内容,也就是用户搜不到此网站的相关信息,自然也就无流量可言。
  2. 解决渲染白屏
    因为页面 HTML由服务器端返回的脚本生成,一般来说这种脚本的体积都不会太小,客户端下载需要时间,浏览器解析以生成页面元素也需要时间,这必然会导致页面的显示速度比传统服务器端渲染得要慢,很容易出现首页白屏的情况,甚至如果浏览器禁用了 JS,那么将直接导致页面连基本的元素都看不到。

VueReact是用来做单页应用最普遍的框架,因为我对 react更熟悉,所以这里我只说下 React SSR


客户端部分

新建 React页面

首先,要有一个用于展示的 React页面,例如:

// ./client/components/About/index.jsximport 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.jsexport 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.jsimport 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.jsimport Koa from 'koa'const app = new Koa()export default app

配置服务端路由:

// ./server/routes/index.jsimport 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.jsimport 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的注入等,不仅需要考虑浏览器端,还要考虑 服务端,一个都不能少。

// 转码器 babelrequire('babel-polyfill')// react 的转码 hookrequire('babel-register')({    presets: ['es2015', 'react', 'stage-0'],    plugins: ['add-module-exports']})// css 的转码 hookrequire('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

Next.js 是一个用于在服务端渲染 React 应用程序的简单框架,2016 年 10 月 25 日由 zeit.co 背后的团队发布。

React服务端渲染 SSR应用框架,支持可选的服务端与客户端渲染功能,简单易用,安装这个框架会搭建一个基于React、WebpackBabel的构建过程,也就是说脚手架已经预设了配置,开发人员不必在搭建WebpackBabel配置上。

此项目算是众多 react server slide rendering方案中最受欢迎的一个,如果现在考虑 react服务端渲染,此方案算是最佳选择。

另外,不仅是 reactvue同样有类似对标的库:Nuxt.js,作用和功能几乎和 Next.js一致,有意思的是,Nuxt.js的发布时间仅在 Next.js宣告发布后的几个小时内,算是同一天发布,如今在各自领域内的影响力不分伯仲。