# 同时兼容两种渲染模式

我们的应用的一大特色是能够同时兼容/启动， 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 字符串

### CSR模式渲染

本地开发我们通过 `webpack-dev-server` 来创建一个服务
我们首先将 layout 组件编译为 string

``` js
 if (config.type !== 'ssr' || csr) {
    const stream = await renderLayout(ctx, config)
    const doctypeStream = new ReadableString('<!DOCTYPE html>')
    // @ts-ignore
    return mergeStream(doctypeStream, stream)
  }
```

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

``` 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>
    )
  }
}
```

## SSR模式

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

``` js
// ykfe-utils/renderToStream.js

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

```

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

### SSR模式下切换为CSR

为了应对大流量或者ssr应用执行错误，需要紧急切换到csr渲染模式下，我们照样可以通过 `config.type` 来控制。
在非ssr渲染模式下，服务端直接返回一个只包含空的 `<div id="app"></app>` 的html文档

## 总结

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

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

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