-
Notifications
You must be signed in to change notification settings - Fork 212
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c533549
commit 5193413
Showing
88 changed files
with
936 additions
and
773 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ | ||
} | ||
} | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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` 中的逻辑变得十分简洁。保证每一个第三方模块中的方法做的事情都十分简单 |
Oops, something went wrong.