同时兼容两种渲染模式

我们的应用的一大特色是能够同时兼容/启动, ssr/csr 两种渲染模式,在本地开发时,你可以同时启动两种渲染模式来观察区别。在生产环境时,你可以通过config配置,来随时切换两种渲染模式

详细做法

下面来介绍我们的详细做法,我们的一大特色是全面拥抱jsx来作为前端组件以及页面模版,抛弃index.html文件

使用jsx来当作通用模版

我们没有采用html-webpack-plugin这个插件来作为csr的页面模版,这个经典插件是根据传入的 index.html 来自动注入打包的静态资源。 但此方式缺点太多,一个是传统的模版引擎的语法实在是不人性化,比起jsx这种带语法糖的手写 AST的方法已经及其的落后,对前端工程师极度不友好,还得去专门学该模版引擎的语法造成心智负担。且灵活性太低,不能应对多变的业务需求。 所以我们移除 web/index.html 文件 其功能由 web/layout/index.js 来代替

csr模式下自己diy模版的生成内容

借助React官方Api我们可以将一个React组件编译为html字符串

本地开发

以下代码皆封装在yk-cli 当中,让用户无感知 本地开发我们通过 webpack-dev-server 来创建一个服务,此时需要在访问根路由时返回正确的dom解构。 我们首先将layout组件编译为string

// yk-cli/renderLayout.js
const renderLayout = async () => {
  let Layout
  // 我们调用webpack的api来用应用目录下的webpack配置来编译layout组件,使其可以在Node环境中运行
  try {
    // serverless 场景我们从事先构建好的应用目录下的dist文件夹中读取layout
    Layout = isServerless ? require('../../../dist/Layout.server').default : require('../dist/Layout.server').default
  } catch (error) {
    // 非serverless场景首次读取失败我们先调用webpack api构建一遍在ykcli的目录下再读取
    const webpackWithPromise = require('./util')
    await webpackWithPromise(serverConfig)
    Layout = require('../dist/Layout.server').default
  }

  // 此时props.children 为 undefined 我们只需要传染一个空的layout骨架即可
  const props = {
    layoutData: {
      app: {
        config: config
      }
    }
  }

  const str = reactToString(Layout, props)
  return str
}

然后启动服务,将string返回

// ykcli/clientRender.js
const dev = () => {
    const compiler = webpack(clientConfig)
    const server = new WebpackDevServer(compiler, {
        disableHostCheck: true,
        publicPath: '/',
        hotOnly: true,
        host: 'localhost',
        contentBase: cwd + '/dist',
        hot: true,
        port: 8000,
        clientLogLevel: 'error',
        headers: {
            'access-control-allow-origin': '*'
        },
        before(app) {
            app.get('/', async (req, res) => {
                res.write(string)
                res.end()
            })
        },
        after(app) {
            app.get(/^\//, async (req, res) => {
                res.write(string)
                res.end()
            })
        }
    })
    server.listen(8000, 'localhost')
}

此时我们只需要返回一个空的html结构且包含 <div id="app"></div> 并且插入 css/js 资源即可 此时的最终渲染形式如下

const commonNode = props => (
  // 为了同时兼容ssr/csr请保留此判断,如果你的layout没有内容请使用 props.children ? <div>{ props.children }</div> : ''
  // 作为承载csr应用页面模版时,我们只需要返回一个空的节点
  props.children ? <div className='normal'><h1 className='title'><Link to='/'>Egg + React + SSR</Link><div className='author'>by ykfe</div></h1>{props.children}</div>
    : ''
)

const Layout = (props) => {
  if (__isBrowser__) {
    // 客户端hydrate时,只需要hydrate <div id='app'>里面的内容
    return commonNode(props)
  } else {
    const { serverData } = props.layoutData
    const { injectCss, injectScript } = props.layoutData.app.config
    return (
      <html lang='en'>
        <head>
          <meta charSet='utf-8' />
          <meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no' />
          <meta name='theme-color' content='#000000' />
          <title>React App</title>
          {
            injectCss && injectCss.map(item => <link rel='stylesheet' href={item} key={item} />)
          }
        </head>
        <body>
          <div id='app'>{ commonNode(props) }</div>
          {
            serverData && <script dangerouslySetInnerHTML={{
              __html: `window.__USE_SSR__=true; window.__INITIAL_DATA__ =${serialize(serverData)}`
            }} />
          }
          <div dangerouslySetInnerHTML={{
            __html: injectScript && injectScript.join('')
          }} />
        </body>
      </html>
    )
  }
}

生产环境

生产环境我们直接将 string 写入 dist/index.html 文件,使得兼容 csr

// ykcli/clientRender.js

const build = async () => {
    const stats = await webpackWithPromise(clientConfig)
    console.log(stats.toString({
        assets: true,
        colors: true,
        hash: true,
        timings: true,
        version: true
    }))
    fs.writeFileSync(cwd + '/dist/index.html', string)
}

ssr模式

ssr模式下我们可以直接渲染包含子组件的layout组件即可以获取到完整的页面结构

// ykfe-utils/renderToStream.js

const serverRes = await global.serverStream(ctx)
const stream = global.renderToNodeStream(serverRes)
return stream

我们直接将 entry/serverRender 方法的返回值传入 renderToNodeStream 即可

ssr模式下切换为csr

为了应对大流量或者ssr应用执行错误,需要紧急切换到csr渲染模式下,我们照样可以通过 config.type 来控制。 实现方式如下

// ykfe-utils/renderToStream.js

if (config.type !== 'ssr') {
    const renderLayout = require('yk-cli/bin/renderLayout').default
    const str = await renderLayout()
    return str
}

在非ssr渲染模式下,服务端直接返回一个只包含空的 <div id="app"></app> 的html文档

总结

2.0.0版本的好处在于,原来的页面模版拼接逻辑都是写在 renderToStream 方法内部的,有如下缺点

  • 过于黑盒,里面的逻辑略显复杂,使用者不知道自己的页面究竟是怎么渲染出来的
  • 灵活性差,拼接的内容皆来自于锚点与config中的 key-value 的互相对应,一旦想要新增一个config配置,renderToStream 也得随之添加对应的锚点

而我们新的版本将这块逻辑迁移到 layout 组件中进行使用者可以灵活决定页面的元素。并且此时让 renderToStream 中的逻辑变得十分简洁。保证每一个第三方模块中的方法做的事情都十分简单