Skip to content

Commit

Permalink
feat: 使用jsx代替模版
Browse files Browse the repository at this point in the history
  • Loading branch information
zhangyuang committed Aug 11, 2019
1 parent c533549 commit 5193413
Show file tree
Hide file tree
Showing 88 changed files with 936 additions and 773 deletions.
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ getInitialProps入参对象的属性如下:
- [x] 配套结合[antd](https://github.com/ykfe/egg-react-ssr/tree/master/example/ssr-with-antd)的example的实现
- [x] 配套结合[react-loadable](https://github.com/ykfe/egg-react-ssr/tree/master/example/ssr-with-loadable)做路由分割的example的实现
- [x] 配套结合[dva](https://github.com/ykfe/egg-react-ssr/tree/master/example/ssr-with-dva)做数据管理的example的实现
- [x] 抛弃传统模版引擎,拥抱 React 组件,使用JSX来作为模版
- [ ] 配套[TypeScript](https://github.com/ykfe/egg-react-ssr-typescript)版本的实现
- [ ] 配套serverless版本的实现

Expand Down Expand Up @@ -117,19 +118,15 @@ module.exports = {
}
],
template: resolvePath('web/index.html'), // 使用的模版文件路径
head: [
'<meta description=xxx />',
'<title>title</title>'
], // 自定义头部内容,通常在动态设置meta信息的时候用到
injectCss: (chunkName) => ([
`<link rel='stylesheet' href='/static/css/${chunkName}.chunk.css' />`
]), // 客户端需要加载的静态css文件资源
injectScript: (chunkName) => ([
`<script src='/static/js/runtime~${chunkName}.js'></script>`,
`<script src='/static/js/vendor.chunk.js'></script>`,
`<script src='/static/js/${chunkName}.chunk.js'></script>`
]), // 客户端需要加载的静态js文件资源
serverJs: (chunkName) => resolvePath(`dist/${chunkName}.server.js`) // 服务端需要使用的打包后的serverRender方法js文件的路径
injectCss: [
`/static/css/Page.chunk.css`
], // 客户端需要加载的静态样式表
injectScript: [
`<script src='/static/js/runtime~Page.js'></script>`,
`<script src='/static/js/vendor.chunk.js'></script>`,
`<script src='/static/js/Page.chunk.js'></script>`
], // 客户端需要加载的静态资源文件表
serverJs: resolvePath(`dist/Page.server.js`) // 打包后的server端的bundle文件路径
}
```

Expand Down Expand Up @@ -168,7 +165,6 @@ module.exports = {
├── assets
│   └── common.less
├── entry.js // webpack打包入口文件,分环境导出不同配置
├── index.html // 页面骨架模版
├── layout
│   ├── index.js // 页面布局
│   └── index.less
Expand Down Expand Up @@ -209,6 +205,10 @@ $ npm run build // 打包服务端以及客户端资源文件
$ npm run analyze // 可视化分析客户端打包的资源详情
```

## Changelog

每一个版本的详细改动请查看 [release notes](https://github.com/ykfe/egg-react-ssr/releases)

## 与其他方案的对比

-[easy-team](https://github.com/ykfe/egg-react-ssr/wiki/与easy-team实现方案的对比)方案的对比
Expand Down
2 changes: 2 additions & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ module.exports = {
'getInitialProps',
'hydrate',
'stream',
'ssr-csr',
'hmr',
'optimize',
'dev',
'publish',
'ts',
'serverless',
Expand Down
57 changes: 57 additions & 0 deletions docs/guide/dev.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 本地开发

本章节将会讲述在本地开发模式时,我们需要专注于什么特性以及具体做了什么工作,来让我们的应用启动

## 本地开发/部署时我们需要什么

在本地开发环境以及生产环境部署时我们需要的环境是不一样的

### 本地开发环境

* hmr,本地开发时,我们需要 `hmr` 功能来实现热替换
* sourceMap,本地开发时,我们需要 `sourceMap` 功能来帮我们定位错误源代码

### 生产环境

* 稳定的前端静态资源代码,我们不需要hmr等功能,只需要minify之后的前端静态资源代码
* 进程的稳定性,保证进程崩溃时可以自动重启

我们在[部署章节](./publish.md)会详细介绍这些内容。

## npm start 到底干了什么

查看package.json

``` js
"start": "rimraf dist && concurrently \"npm run ssr\" \" npm run csr \"",
"ssr": "concurrently \"egg-bin dev\" \"cross-env NODE_ENV=development webpack --watch --config ./build/webpack.config.server.js\"",
"csr": "cross-env NODE_ENV=development ykcli dev",
```

可以看到,在执行 `npm start` 时,我们执行了 `npm run ssr` 以及 `npm run csr` 两个script,这里我们分别来介绍两个script分别干了什么

### npm run ssr

`npm run ssr` 时,我们使用开发环境的 `egg-bin` 模块,来启动我们的 `egg` 应用,同时使用 `webpack` 去编译服务端的 `js bundle` ,并开启 `watch` 模式,使得源码改变时,会自动重新build

1. 使用 egg-bin 启动egg应用
2. 使用webpack watch模式来将服务端bundle编译到本地磁盘,即 `dist/Page.server.js` 文件

### npm run csr

`npm run csr` 时,我们使用 `ykcli dev` ,其中内置了 `webpack-dev-server` , 我们做的事情其实只是用 `webpack-dev-server` 来编译前端静态资源文件,并托管到一个本地服务中使其具有 `hmr` 功能

### 代理前端静态资源

我们使用 `npm run csr` 启动的 `webpack-dev-server` 的服务监听的是 `8000` 端口,但我们的 `egg` 应用启动的是 `7001` 端口,为了让我们不需要手动给静态资源加上 `<script src="http://localhost:8000/static/js/Page.chunk.js"></script>` 这样的写法,我们使用了 `egg-proxy` , 来将指定路径的请求转发到 `8000` 端口

``` js
// config.local.js
module.exports = {
proxy: {
host: 'http://127.0.0.1:8000', // 本地开发的时候代理前端打包出来的资源地址
match: /(\/static)|(\/sockjs-node)|(\/__webpack_dev_server__)|hot-update/
}
}
```

2 changes: 1 addition & 1 deletion docs/guide/getInitialProps.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const serverRender = async (ctx) => {
const Layout = ActiveComponent.Layout || defaultLayout
ctx.serverData = serverData
return <StaticRouter location={ctx.req.url} context={serverData}>
<Layout>
<Layout layoutData={ctx}>
<ActiveComponent {...serverData} />
</Layout>
</StaticRouter>
Expand Down
178 changes: 178 additions & 0 deletions docs/guide/ssr-csr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# 同时兼容两种渲染模式

我们的应用的一大特色是能够同时兼容/启动, 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](https://github.com/ykfe/egg-react-ssr/tree/feat/useJsxToTpl/packages/yk-cli) 当中,让用户无感知
本地开发我们通过 `webpack-dev-server` 来创建一个服务,此时需要在访问根路由时返回正确的dom解构。
我们首先将layout组件编译为string

``` js
// yk-cli/renderLayout.js
const Layout = require(cwd + '/web/layout').default

const reactToString = (Component, props) => {
return renderToString(React.createElement(Component, props))
}

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

const string = reactToString(Layout, props)

module.exports = string
```

然后启动服务,将string返回

``` js
// 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()
})
}
})
server.listen(8000, 'localhost')
}
```

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

``` js
const commonNode = props => (
// 为了同时兼容ssr/csr请保留此判断,如果你的layout没有内容请使用 props.children ? { props.children } : ''
// 作为承载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`

``` js
// 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组件即可以获取到完整的页面结构

``` js
// 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` 来控制。
实现方式如下

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

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

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

## 总结

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

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

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

0 comments on commit 5193413

Please sign in to comment.