Skip to content

Commit fd17f67

Browse files
authored
v2.1.0 (#82)
* feat: 更新yk-cli * docs: update readme * chore: enhance development experience (#79) * chore: enhance development experience * version: update yk-cli 2.3.1 * fix: fix browserslist * docs/deploy (#80) * docs: 增加 Node.js 部署文档 * docs: 完善faq.md * fix: 修复ssr-with-loadable * feat: 新增配置文件config.ssr 分离前后端配置 close #81
1 parent 56d1d0e commit fd17f67

Some content is hidden

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

42 files changed

+555
-240
lines changed

README.md

+56-6
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111
最小而美的服务端渲染应用骨架,特点
1212

13-
- 小:实现方式简洁,生产环境构建出来的bundle为同等复杂度的next.js项目的0.4倍,文件数量相比于next.js减少非常多
14-
- 全:支持HMR,同时支持本地开发以及生产环境CSR/SSR两种渲染模式无缝切换,支持定制特定组件的渲染模式
13+
- 小:实现方式简洁,生产环境构建出来的bundle为同等复杂度的next.js项目的0.4倍,生成文件数量相比于next.js减少非常多
14+
- 全:支持HMR,同时支持本地开发以及生产环境CSR/SSR两种渲染模式无缝切换,支持定制组件的渲染模式
1515
- 美:基于[React](https://reactjs.org/)[Eggjs](https://eggjs.org/)框架,拥有强大的插件生态,配置非黑盒,且一切关键位置皆可通过config.default.js来配置
1616

1717
## 快速入门
@@ -29,7 +29,7 @@ $ open http://localhost:7001
2929

3030
## 功能/特性
3131

32-
这个项目骨架的特色是写法简单,功能强大,相关特性、原理也会在本节一一说明。
32+
`这个项目骨架的特色是写法简单,功能强大,一切都是组件,支持 SSR/CSR 两种渲染模式无缝切换`
3333

3434
### 写法
3535

@@ -61,7 +61,57 @@ export default Page
6161
getInitialProps入参对象的属性如下:
6262

6363
- ctx: Node应用请求的上下文(仅在SSR阶段可以获取)
64-
- Router Props: 路由信息,包括pathname以及Router params等信息,详细信息参考react-router文档
64+
- Router Props: 包含路由对象属性,包括pathname以及Router params history 等对象,详细信息参考react-router文档
65+
66+
### 一切皆组件
67+
68+
我们的页面基础模版 html,meta 等标签皆使用JSX来生成,避免你去使用繁琐的模版引擎语法
69+
70+
``` js
71+
const commonNode = props => (
72+
// 为了同时兼容ssr/csr请保留此判断,如果你的layout没有内容请使用 props.children ? <div>{ props.children }</div> : ''
73+
props.children
74+
? <div className='normal'><h1 className='title'><Link to='/'>Egg + React + SSR</Link><div className='author'>by ykfe</div></h1>{props.children}</div>
75+
: ''
76+
)
77+
78+
const Layout = (props) => {
79+
if (__isBrowser__) {
80+
return commonNode(props)
81+
} else {
82+
const { serverData } = props.layoutData
83+
const { injectCss, injectScript } = props.layoutData.app.config
84+
return (
85+
<html lang='en'>
86+
<head>
87+
<meta charSet='utf-8' />
88+
<meta name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no' />
89+
<meta name='theme-color' content='#000000' />
90+
<title>React App</title>
91+
{
92+
injectCss && injectCss.map(item => <link rel='stylesheet' href={item} key={item} />)
93+
}
94+
</head>
95+
<body>
96+
<div id='app'>{ commonNode(props) }</div>
97+
{
98+
serverData && <script dangerouslySetInnerHTML={{
99+
__html: `window.__USE_SSR__=true; window.__INITIAL_DATA__ =${serialize(serverData)}`
100+
}} />
101+
}
102+
<div dangerouslySetInnerHTML={{
103+
__html: injectScript && injectScript.join('')
104+
}} />
105+
</body>
106+
</html>
107+
)
108+
}
109+
}
110+
```
111+
112+
### 渲染模式无缝切换
113+
114+
在本地开发时,你可以同时启动ssr/csr两种渲染模式查看区别,在生产环境时,你可以通过设置config中的type属性来切换不同的渲染模式,在流量较大时可以降级为csr应用
65115

66116
## 执行环境
67117

@@ -96,10 +146,10 @@ getInitialProps入参对象的属性如下:
96146
为了足够灵活使用,这里我们将一些关键项提供可配置的选项,可根据实际需要来配置,如无特殊必要,使用默认配置即可。由于项目是基于Egg的,所以配置信息统一放在config.default.js。
97147

98148
```js
149+
// config/config.ssr
99150
const resolvePath = (path) => require('path').resolve(process.cwd(), path)
100151

101152
module.exports = {
102-
keys: 'eggssr',
103153
type: 'ssr', // 指定运行类型可设置为csr切换为客户端渲染,此时服务端不会做获取数据生成字符串的操作以及不会使用hydrate API
104154
static: {
105155
// 设置Node应用的静态资源目录,为了生产环境读取静态资源文件
@@ -131,7 +181,7 @@ module.exports = {
131181
`<script src='/static/js/vendor.chunk.js'></script>`,
132182
`<script src='/static/js/Page.chunk.js'></script>`
133183
], // 客户端需要加载的静态资源文件表
134-
serverJs: resolvePath(`dist/Page.server.js`) // 打包后的server端的bundle文件路径
184+
serverJs: resolvePath(`dist/Page.server.js`) || function // 打包后的server端的bundle文件路径或者直接传入require后的function
135185
}
136186
```
137187

docs/guide/faq.md

+34
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,37 @@ export default OnlyCsr(Page)
8787

8888
形如`http://localhost:8000/user/:id`这种路由,在没有后端路由支持的情况下,服务端并不存在与之对应的资源,刷新后相当于去服务器访问该资源自然会404。
8989
解决方式请查看: [HTML5 History 模式](https://router.vuejs.org/zh/guide/essentials/history-mode.html)
90+
91+
<span style="color:red">已针对该情况做本地开发时的优化</span> by this [PR](https://github.com/ykfe/egg-react-ssr/pull/79)
92+
93+
## 如何切换渲染模式
94+
95+
分别介绍在本地开发和生产环境如何切换渲染模式
96+
97+
### 本地开发
98+
99+
由于本地开发时修改config,egg进程会自动重启,故只需要修改`config.type=csr`即可
100+
101+
### 生产环境
102+
103+
生产环境,config修改并不能自动重启进程,我们建议采用以下做法。访问应用时先通过配置平台获取到最新的config配置覆盖默认配置,可采用http接口的形式或者metaq这种工具来做发布订阅
104+
105+
```js
106+
async index () {
107+
const { ctx } = this
108+
try {
109+
// Page为webpack打包的chunkName,项目默认的entry为Page
110+
ctx.type = 'text/html'
111+
ctx.status = 200
112+
let config = ctx.app.config
113+
if (ctx.app.config.env !== 'local') {
114+
const extraConfig = await http.get('xxx') // 通过接口拿到实时的config,覆盖默认配置
115+
config = Object.assign(config, extraConfig)
116+
}
117+
const stream = await renderToStream(ctx, config)
118+
ctx.body = stream
119+
} catch (error) {
120+
ctx.logger.error(`Page Controller renderToStream Error ${error}`)
121+
}
122+
}
123+
```

docs/guide/getInitialProps.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import ReactDOM from 'react-dom'
88
import { BrowserRouter, StaticRouter, Route } from 'react-router-dom'
99
import defaultLayout from '@/layout'
1010
import { getWrappedComponent, getComponent } from 'ykfe-utils'
11-
import { routes as Routes } from '../config/config.default'
11+
import { routes as Routes } from '../config/config.ssr'
1212

1313
const serverRender = async (ctx) => {
1414
// 服务端渲染 根据ctx.path获取请求的具体组件,调用getInitialProps并渲染
15-
const ActiveComponent = getComponent(Routes, ctx.path)()
15+
const ActiveComponent = getComponent(Routes, ctx.path)
1616
const serverData = ActiveComponent.getInitialProps ? await ActiveComponent.getInitialProps(ctx) : {}
1717
const Layout = ActiveComponent.Layout || defaultLayout
1818
ctx.serverData = serverData

docs/guide/hydrate.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import ReactDOM from 'react-dom'
1212
import { BrowserRouter, StaticRouter, Route } from 'react-router-dom'
1313
import defaultLayout from '@/layout'
1414
import { getWrappedComponent, getComponent } from 'ykfe-utils'
15-
import { routes as Routes } from '../config/config.default'
15+
import { routes as Routes } from '../config/config.ssr'
1616

1717
const clientRender = async () => {
1818
// 客户端渲染|水合

docs/guide/publish.md

+199-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,200 @@
1-
# 部署
1+
# Node.js 应用部署
22

3-
在这里我们会介绍使用docker来部署应用
4-
可以先参考egg官方的[部署文档](https://eggjs.org/zh-cn/core/deployment.html),稍后会详细介绍如何使用docker来部署应用
5-
待更新...
3+
如果是使用 egg 框架开发的应用,强烈推荐使用 `egg-scripts` 进行部署,使用 egg 提供的一系列解决方案,包括但不限于:
4+
5+
- 灵活的启动参数
6+
- [Node.js 性能平台](https://www.aliyun.com/product/nodejs)
7+
- [egg-alinode](https://github.com/eggjs/egg-alinode)
8+
9+
egg 提供了从部署、进程守护、监控、问题排查等一系列的解决方案。详情见[egg 部署文档](https://eggjs.org/zh-cn/core/deployment.html)
10+
11+
## pm2 部署
12+
13+
pm2 是一个带有负载均衡功能的应用的进程管理器。当你要把你的独立代码利用全部的服务器上的所有 CPU 核数,并保证进程永远都是存活状态和 0 秒的重载,那么 PM2 是很完美的选择。详细可参考[官方的部署文档示例](http://pm2.keymetrics.io/docs/usage/deployment/)[github项目地址](https://github.com/Unitech/pm2)
14+
15+
pm2 有以下的几个非常给力的能力:
16+
17+
- 内建负载均衡(使用Node cluster 集群模块)
18+
- 后台运行
19+
- 0 秒停机重载,我理解大概意思是维护升级的时候不需要停机
20+
- 具有 Ubuntu 和 CentOS 的启动脚本
21+
- 停止不稳定的进程(避免无限循环)
22+
- 控制台检测
23+
- 提供 HTTP API
24+
- 远程控制和实时的接口API (Nodejs 模块, 允许和PM2进程管理器交互)
25+
26+
### pm2部署简单应用
27+
28+
- 安装pm2
29+
30+
```
31+
$ npm install -g pm2
32+
```
33+
34+
- 使用 pm2 部署简单的项目
35+
36+
```
37+
$ pm2 start app.js --name "egg-react-cli" -i 0 --watch
38+
39+
pm2 start: 使用pm2启动 app.js
40+
-i 0: 使用最大进程数启动
41+
–name: 指定一个你喜欢的名字
42+
–watch: 开启监视模式,如果代码有变动pm2自动重启(一般用于本地开发 )
43+
```
44+
45+
- 查看pm2部署
46+
47+
```
48+
$ pm2 ls
49+
50+
┌───────────────┬────┬─────────┬──────┬───────┬────────┬─────────┬────────┬─────┬───────────┬──────┬──────────┐
51+
│ App name │ id │ version │ mode │ pid │ status │ restart │ uptime │ cpu │ mem │ user │ watching │
52+
├───────────────┼────┼─────────┼──────┼───────┼────────┼─────────┼────────┼─────┼───────────┼──────┼──────────┤
53+
│ egg-react-ssr │ 0 │ 1.0.53 │ fork │ 58835 │ online │ 0 │ 0s │ 0% │ 17.4 MB │ xxx │ disabled │
54+
└───────────────┴────┴─────────┴──────┴───────┴────────┴─────────┴────────┴─────┴───────────┴──────┴──────────┘
55+
```
56+
57+
### pm2自动部署远程服务器
58+
59+
不太清楚读者是使用什么方式部署 Node.js 应用的,之前我们的做法是使用 git 托管项目,然后在服务器中安装 git 将项目克隆到服务器中,然后一台一台机器的使用 pm2 命令启动项目,如果项目有任何的修改,就会需要跑到几个服务器中 pull 代码,然后pm2 reload 项目,蛋疼的要死。
60+
现在就使用pm2的远程部署方式,解决这个蛋疼的问题!
61+
62+
#### 准备工作
63+
64+
- git ssh
65+
66+
在服务器上生成git ssh公钥(本地机器和服务器操作一样),并添加到 git 上。这样服务器中clone项目也不需要输入密码。
67+
68+
```
69+
$ git config --global user.name "yourname"
70+
$ git config --global user.email "[email protected]"
71+
$ ssh-keygen -t rsa -C "[email protected]"
72+
```
73+
74+
连续三次回车,这样生成的ssh公钥添加到 github 或其他的 git 托管平台上。
75+
76+
- 机器免密登录
77+
78+
查看生成的ssh公钥:
79+
80+
```
81+
$ ls ~/.ssh/
82+
authorized_keys id_rsa id_rsa.pub known_hosts
83+
```
84+
85+
理论上已经生成 ssh 公钥,在用户主目录下的 .ssh 目录中生成的 id_rsa.pub 就是生成的公钥。authorized_keys 文件是通过授权的 ssh 公钥,在使用 ssh 协议进行远程访问的时候,如果该机器的 ssh 公钥在这个文件中,那么能直接进行访问。将本地机器和线上服务器建立ssh信任,实现免密码登陆。
86+
87+
将ssh公钥拷贝到服务器:
88+
89+
```
90+
$ scp ~/.ssh/id_rsa.pub username@ip:用户主目录/.ssh/authorized_keys
91+
```
92+
93+
### pm2 配置文件 ecosystem.json
94+
95+
```
96+
{
97+
/**
98+
* Deployment section
99+
* http://pm2.keymetrics.io/docs/usage/deployment/
100+
*/
101+
"deploy" : {
102+
"yourprojectname" : {
103+
// 你登陆到远程主机的用户名
104+
"user" : "node",
105+
// 服务器的ip地址 支持数组
106+
"host" : ["ip"],
107+
// 部署的分支
108+
"ref" : "origin/master",
109+
// github 或 oschina 中托管的地址
110+
"repo" : "your-project-repo",
111+
// 部署到服务器的目录
112+
"path" : "/your/deploy/folder/",
113+
// 部署时的命令
114+
"post-deploy" : "npm install ; pm2 start bin/www --name 'your-app-name' --watch",
115+
// 环境变量
116+
"env" : {
117+
"NODE_ENV": "dev"
118+
}
119+
}
120+
}
121+
}
122+
```
123+
124+
### 执行部署
125+
126+
```
127+
$ pm2 deploy ecosystem.json yourprojectname setup
128+
```
129+
130+
上面命令是将项目从 github 中克隆到指定 path 中,需要注意一下的是,pm2 将目录结构分为 :
131+
132+
```
133+
|current | shared |source |
134+
```
135+
136+
克隆好之后执行安装和启动
137+
138+
```
139+
$ pm2 deploy ecosystem.json yourprojectname
140+
```
141+
142+
以上的操作会将你的项目从远程仓库中克隆到服务器指定目录,然后执行配置文件中的执行命令。实现从本地执行一行命令部署多台服务器的操作。此功能可以大大减少部署和运维成本。
143+
144+
## 使用nginx来做负载均衡和端口代理
145+
146+
nginx 作为负载和代理服务可以实现在服务器上的静态资源托管、多应用 route 级别转发等基本需求。
147+
148+
- install
149+
150+
```
151+
$ sudo yum install nginx
152+
```
153+
154+
### nginx 托管静态资源
155+
156+
```
157+
server {
158+
listen 80;
159+
server_name yourServerName;
160+
root your/folder;
161+
index index.html;
162+
# Load configuration files for the default server block.
163+
include /etc/nginx/default.d/*.conf;
164+
location / {}
165+
error_page 404 /404.html;
166+
location = /40x.html {
167+
}
168+
error_page 500 502 503 504 /50x.html;
169+
location = /50x.html {
170+
}
171+
}
172+
```
173+
174+
### nginx 开机自启
175+
176+
```
177+
$ systemctl enable nginx
178+
$ systemctl restart nginx
179+
```
180+
181+
### 修改用户
182+
183+
nginx 文件首行默认用户为 nginx,需要修改为当前用户名。
184+
185+
### 本地代理某端口的服务
186+
187+
```
188+
location / {
189+
proxy_pass http://127.0.0.1:7001;
190+
proxy_hide_header 'x-frame-options';
191+
#root html;
192+
#index index.html index.htm;
193+
}
194+
```
195+
196+
### 启动
197+
198+
```
199+
$ sudo nginx -c /usr/local/etc/nginx/nginx.conf
200+
```

docs/guide/ssr-csr.md

+6
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ const dev = () => {
6666
res.write(string)
6767
res.end()
6868
})
69+
},
70+
after(app) {
71+
app.get(/^\//, async (req, res) => {
72+
res.write(string)
73+
res.end()
74+
})
6975
}
7076
})
7177
server.listen(8000, 'localhost')

example/ssr-with-antd/app/controller/page.js

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11

22
const Controller = require('egg').Controller
33
const { renderToStream } = require('ykfe-utils')
4+
const ssrConfig = require('../../config/config.ssr')
45

56
class PageController extends Controller {
67
async index () {
@@ -9,6 +10,7 @@ class PageController extends Controller {
910
// Page为webpack打包的chunkName,项目默认的entry为Page
1011
ctx.type = 'text/html'
1112
ctx.status = 200
13+
Object.assign(ctx.app.config, ssrConfig)
1214
const stream = await renderToStream(ctx, ctx.app.config)
1315
ctx.body = stream
1416
} catch (error) {

0 commit comments

Comments
 (0)