Skip to content
This repository was archived by the owner on May 8, 2025. It is now read-only.

Commit 5193413

Browse files
committed
feat: 使用jsx代替模版
1 parent c533549 commit 5193413

File tree

88 files changed

+936
-773
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+936
-773
lines changed

README.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ getInitialProps入参对象的属性如下:
7676
- [x] 配套结合[antd](https://github.com/ykfe/egg-react-ssr/tree/master/example/ssr-with-antd)的example的实现
7777
- [x] 配套结合[react-loadable](https://github.com/ykfe/egg-react-ssr/tree/master/example/ssr-with-loadable)做路由分割的example的实现
7878
- [x] 配套结合[dva](https://github.com/ykfe/egg-react-ssr/tree/master/example/ssr-with-dva)做数据管理的example的实现
79+
- [x] 抛弃传统模版引擎,拥抱 React 组件,使用JSX来作为模版
7980
- [ ] 配套[TypeScript](https://github.com/ykfe/egg-react-ssr-typescript)版本的实现
8081
- [ ] 配套serverless版本的实现
8182

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

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

208+
## Changelog
209+
210+
每一个版本的详细改动请查看 [release notes](https://github.com/ykfe/egg-react-ssr/releases)
211+
212212
## 与其他方案的对比
213213

214214
-[easy-team](https://github.com/ykfe/egg-react-ssr/wiki/与easy-team实现方案的对比)方案的对比

docs/.vuepress/config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ module.exports = {
4545
'getInitialProps',
4646
'hydrate',
4747
'stream',
48+
'ssr-csr',
4849
'hmr',
4950
'optimize',
51+
'dev',
5052
'publish',
5153
'ts',
5254
'serverless',

docs/guide/dev.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# 本地开发
2+
3+
本章节将会讲述在本地开发模式时,我们需要专注于什么特性以及具体做了什么工作,来让我们的应用启动
4+
5+
## 本地开发/部署时我们需要什么
6+
7+
在本地开发环境以及生产环境部署时我们需要的环境是不一样的
8+
9+
### 本地开发环境
10+
11+
* hmr,本地开发时,我们需要 `hmr` 功能来实现热替换
12+
* sourceMap,本地开发时,我们需要 `sourceMap` 功能来帮我们定位错误源代码
13+
14+
### 生产环境
15+
16+
* 稳定的前端静态资源代码,我们不需要hmr等功能,只需要minify之后的前端静态资源代码
17+
* 进程的稳定性,保证进程崩溃时可以自动重启
18+
19+
我们在[部署章节](./publish.md)会详细介绍这些内容。
20+
21+
## npm start 到底干了什么
22+
23+
查看package.json
24+
25+
``` js
26+
"start": "rimraf dist && concurrently \"npm run ssr\" \" npm run csr \"",
27+
"ssr": "concurrently \"egg-bin dev\" \"cross-env NODE_ENV=development webpack --watch --config ./build/webpack.config.server.js\"",
28+
"csr": "cross-env NODE_ENV=development ykcli dev",
29+
```
30+
31+
可以看到,在执行 `npm start` 时,我们执行了 `npm run ssr` 以及 `npm run csr` 两个script,这里我们分别来介绍两个script分别干了什么
32+
33+
### npm run ssr
34+
35+
`npm run ssr` 时,我们使用开发环境的 `egg-bin` 模块,来启动我们的 `egg` 应用,同时使用 `webpack` 去编译服务端的 `js bundle` ,并开启 `watch` 模式,使得源码改变时,会自动重新build
36+
37+
1. 使用 egg-bin 启动egg应用
38+
2. 使用webpack watch模式来将服务端bundle编译到本地磁盘,即 `dist/Page.server.js` 文件
39+
40+
### npm run csr
41+
42+
`npm run csr` 时,我们使用 `ykcli dev` ,其中内置了 `webpack-dev-server` , 我们做的事情其实只是用 `webpack-dev-server` 来编译前端静态资源文件,并托管到一个本地服务中使其具有 `hmr` 功能
43+
44+
### 代理前端静态资源
45+
46+
我们使用 `npm run csr` 启动的 `webpack-dev-server` 的服务监听的是 `8000` 端口,但我们的 `egg` 应用启动的是 `7001` 端口,为了让我们不需要手动给静态资源加上 `<script src="http://localhost:8000/static/js/Page.chunk.js"></script>` 这样的写法,我们使用了 `egg-proxy` , 来将指定路径的请求转发到 `8000` 端口
47+
48+
``` js
49+
// config.local.js
50+
module.exports = {
51+
proxy: {
52+
host: 'http://127.0.0.1:8000', // 本地开发的时候代理前端打包出来的资源地址
53+
match: /(\/static)|(\/sockjs-node)|(\/__webpack_dev_server__)|hot-update/
54+
}
55+
}
56+
```
57+

docs/guide/getInitialProps.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const serverRender = async (ctx) => {
1717
const Layout = ActiveComponent.Layout || defaultLayout
1818
ctx.serverData = serverData
1919
return <StaticRouter location={ctx.req.url} context={serverData}>
20-
<Layout>
20+
<Layout layoutData={ctx}>
2121
<ActiveComponent {...serverData} />
2222
</Layout>
2323
</StaticRouter>

docs/guide/ssr-csr.md

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# 同时兼容两种渲染模式
2+
3+
我们的应用的一大特色是能够同时兼容/启动, ssr/csr 两种渲染模式,在本地开发时,你可以同时启动两种渲染模式来观察区别。在生产环境时,你可以通过config配置,来随时切换两种渲染模式
4+
5+
## 详细做法
6+
7+
下面来介绍我们的详细做法,我们的一大特色是全面拥抱`jsx`来作为前端组件以及页面模版,抛弃`index.html`文件
8+
9+
## 使用jsx来当作通用模版
10+
11+
我们没有采用`html-webpack-plugin`这个插件来作为`csr`的页面模版,这个经典插件是根据传入的 `index.html` 来自动注入打包的静态资源。 但此方式缺点太多,一个是传统的模版引擎的语法实在是不人性化,比起`jsx`这种`带语法糖的手写 AST`的方法已经及其的落后,对前端工程师极度不友好,还得去专门学该模版引擎的语法造成心智负担。且灵活性太低,不能应对多变的业务需求。
12+
所以我们移除 `web/index.html` 文件 其功能由 `web/layout/index.js` 来代替
13+
14+
## csr模式下自己diy模版的生成内容
15+
16+
借助React官方api我们可以将一个React组件编译为html字符串
17+
18+
### 本地开发
19+
20+
以下代码皆封装在[yk-cli](https://github.com/ykfe/egg-react-ssr/tree/feat/useJsxToTpl/packages/yk-cli) 当中,让用户无感知
21+
本地开发我们通过 `webpack-dev-server` 来创建一个服务,此时需要在访问根路由时返回正确的dom解构。
22+
我们首先将layout组件编译为string
23+
24+
``` js
25+
// yk-cli/renderLayout.js
26+
const Layout = require(cwd + '/web/layout').default
27+
28+
const reactToString = (Component, props) => {
29+
return renderToString(React.createElement(Component, props))
30+
}
31+
32+
// 此时props.children的值为undefined,我们只需要渲染一个空的layout骨架即可
33+
const props = {
34+
layoutData: {
35+
app: {
36+
config: config
37+
}
38+
}
39+
}
40+
41+
const string = reactToString(Layout, props)
42+
43+
module.exports = string
44+
```
45+
46+
然后启动服务,将string返回
47+
48+
``` js
49+
// ykcli/clientRender.js
50+
const dev = () => {
51+
const compiler = webpack(clientConfig)
52+
const server = new WebpackDevServer(compiler, {
53+
disableHostCheck: true,
54+
publicPath: '/',
55+
hotOnly: true,
56+
host: 'localhost',
57+
contentBase: cwd + '/dist',
58+
hot: true,
59+
port: 8000,
60+
clientLogLevel: 'error',
61+
headers: {
62+
'access-control-allow-origin': '*'
63+
},
64+
before(app) {
65+
app.get('/', async (req, res) => {
66+
res.write(string)
67+
res.end()
68+
})
69+
}
70+
})
71+
server.listen(8000, 'localhost')
72+
}
73+
```
74+
75+
此时我们只需要返回一个空的html结构且包含 `<div id="app"></div>` 并且插入 `css/js` 资源即可
76+
此时的最终渲染形式如下
77+
78+
``` js
79+
const commonNode = props => (
80+
// 为了同时兼容ssr/csr请保留此判断,如果你的layout没有内容请使用 props.children ? { props.children } : ''
81+
// 作为承载csr应用页面模版时,我们只需要返回一个空的节点
82+
props.children ? <div className='normal'><h1 className='title'><Link to='/'>Egg + React + SSR</Link><div className='author'>by ykfe</div></h1>{props.children}</div>
83+
: ''
84+
)
85+
86+
const Layout = (props) => {
87+
if (__isBrowser__) {
88+
// 客户端hydrate时,只需要hydrate <div id='app'>里面的内容
89+
return commonNode(props)
90+
} else {
91+
const { serverData } = props.layoutData
92+
const { injectCss, injectScript } = props.layoutData.app.config
93+
return (
94+
<html lang='en'>
95+
<head>
96+
<meta charSet='utf-8' />
97+
<meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no' />
98+
<meta name='theme-color' content='#000000' />
99+
<title>React App</title>
100+
{
101+
injectCss && injectCss.map(item => <link rel='stylesheet' href={item} key={item} />)
102+
}
103+
</head>
104+
<body>
105+
<div id='app'>{ commonNode(props) }</div>
106+
{
107+
serverData && <script dangerouslySetInnerHTML={{
108+
__html: `window.__USE_SSR__=true; window.__INITIAL_DATA__ =${serialize(serverData)}`
109+
}} />
110+
}
111+
<div dangerouslySetInnerHTML={{
112+
__html: injectScript && injectScript.join('')
113+
}} />
114+
</body>
115+
</html>
116+
)
117+
}
118+
}
119+
```
120+
121+
### 生产环境
122+
123+
生产环境我们直接将 `string` 写入 `dist/index.html` 文件,使得兼容 `csr`
124+
125+
``` js
126+
// ykcli/clientRender.js
127+
128+
const build = async () => {
129+
const stats = await webpackWithPromise(clientConfig)
130+
console.log(stats.toString({
131+
assets: true,
132+
colors: true,
133+
hash: true,
134+
timings: true,
135+
version: true
136+
}))
137+
fs.writeFileSync(cwd + '/dist/index.html', string)
138+
}
139+
```
140+
141+
## ssr模式
142+
143+
ssr模式下我们可以直接渲染包含子组件的layout组件即可以获取到完整的页面结构
144+
145+
``` js
146+
// ykfe-utils/renderToStream.js
147+
148+
const serverRes = await global.serverStream(ctx)
149+
const stream = global.renderToNodeStream(serverRes)
150+
return stream
151+
```
152+
153+
我们直接将 `entry/serverRender` 方法的返回值传入 `renderToNodeStream` 即可
154+
155+
### ssr模式下切换为csr
156+
157+
为了应对大流量或者ssr应用执行错误,需要紧急切换到csr渲染模式下,我们照样可以通过 `config.type` 来控制。
158+
实现方式如下
159+
160+
``` js
161+
// ykfe-utils/renderToStream.js
162+
163+
if (config.type !== 'ssr') {
164+
const string = require('yk-cli/bin/renderLayout')
165+
return string
166+
}
167+
```
168+
169+
在非ssr渲染模式下,服务端直接返回一个只包含空的 `<div id="app"></app>` 的html文档
170+
171+
## 总结
172+
173+
2.0.0版本的好处在于,原来的页面模版拼接逻辑都是写在 `renderToStream` 方法内部的,有如下缺点
174+
175+
* 过于黑盒,里面的逻辑略显复杂,使用者不知道自己的页面究竟是怎么渲染出来的
176+
* 灵活性差,拼接的内容皆来自于锚点与config中的 `key-value` 的互相对应,一旦想要新增一个config配置,renderToStream 也得随之添加对应的锚点
177+
178+
而我们新的版本将这块逻辑迁移到 `layout` 组件中进行使用者可以灵活决定页面的元素。并且此时让 `renderToStream` 中的逻辑变得十分简洁。保证每一个第三方模块中的方法做的事情都十分简单

0 commit comments

Comments
 (0)