Skip to content

Commit

Permalink
v2.1.0 (#82)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
zhangyuang authored Sep 19, 2019
1 parent 56d1d0e commit fd17f67
Show file tree
Hide file tree
Showing 42 changed files with 555 additions and 240 deletions.
62 changes: 56 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

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

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

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

## 功能/特性

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

### 写法

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

- ctx: Node应用请求的上下文(仅在SSR阶段可以获取)
- Router Props: 路由信息,包括pathname以及Router params等信息,详细信息参考react-router文档
- Router Props: 包含路由对象属性,包括pathname以及Router params history 等对象,详细信息参考react-router文档

### 一切皆组件

我们的页面基础模版 html,meta 等标签皆使用JSX来生成,避免你去使用繁琐的模版引擎语法

``` js
const commonNode = props => (
// 为了同时兼容ssr/csr请保留此判断,如果你的layout没有内容请使用 props.children ? <div>{ props.children }</div> : ''
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__) {
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/csr两种渲染模式查看区别,在生产环境时,你可以通过设置config中的type属性来切换不同的渲染模式,在流量较大时可以降级为csr应用

## 执行环境

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

```js
// config/config.ssr
const resolvePath = (path) => require('path').resolve(process.cwd(), path)

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

Expand Down
34 changes: 34 additions & 0 deletions docs/guide/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,37 @@ export default OnlyCsr(Page)

形如`http://localhost:8000/user/:id`这种路由,在没有后端路由支持的情况下,服务端并不存在与之对应的资源,刷新后相当于去服务器访问该资源自然会404。
解决方式请查看: [HTML5 History 模式](https://router.vuejs.org/zh/guide/essentials/history-mode.html)

<span style="color:red">已针对该情况做本地开发时的优化</span> by this [PR](https://github.com/ykfe/egg-react-ssr/pull/79)

## 如何切换渲染模式

分别介绍在本地开发和生产环境如何切换渲染模式

### 本地开发

由于本地开发时修改config,egg进程会自动重启,故只需要修改`config.type=csr`即可

### 生产环境

生产环境,config修改并不能自动重启进程,我们建议采用以下做法。访问应用时先通过配置平台获取到最新的config配置覆盖默认配置,可采用http接口的形式或者metaq这种工具来做发布订阅

```js
async index () {
const { ctx } = this
try {
// Page为webpack打包的chunkName,项目默认的entry为Page
ctx.type = 'text/html'
ctx.status = 200
let config = ctx.app.config
if (ctx.app.config.env !== 'local') {
const extraConfig = await http.get('xxx') // 通过接口拿到实时的config,覆盖默认配置
config = Object.assign(config, extraConfig)
}
const stream = await renderToStream(ctx, config)
ctx.body = stream
} catch (error) {
ctx.logger.error(`Page Controller renderToStream Error ${error}`)
}
}
```
4 changes: 2 additions & 2 deletions docs/guide/getInitialProps.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import ReactDOM from 'react-dom'
import { BrowserRouter, StaticRouter, Route } from 'react-router-dom'
import defaultLayout from '@/layout'
import { getWrappedComponent, getComponent } from 'ykfe-utils'
import { routes as Routes } from '../config/config.default'
import { routes as Routes } from '../config/config.ssr'

const serverRender = async (ctx) => {
// 服务端渲染 根据ctx.path获取请求的具体组件,调用getInitialProps并渲染
const ActiveComponent = getComponent(Routes, ctx.path)()
const ActiveComponent = getComponent(Routes, ctx.path)
const serverData = ActiveComponent.getInitialProps ? await ActiveComponent.getInitialProps(ctx) : {}
const Layout = ActiveComponent.Layout || defaultLayout
ctx.serverData = serverData
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/hydrate.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import ReactDOM from 'react-dom'
import { BrowserRouter, StaticRouter, Route } from 'react-router-dom'
import defaultLayout from '@/layout'
import { getWrappedComponent, getComponent } from 'ykfe-utils'
import { routes as Routes } from '../config/config.default'
import { routes as Routes } from '../config/config.ssr'

const clientRender = async () => {
// 客户端渲染|水合
Expand Down
203 changes: 199 additions & 4 deletions docs/guide/publish.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,200 @@
# 部署
# Node.js 应用部署

在这里我们会介绍使用docker来部署应用
可以先参考egg官方的[部署文档](https://eggjs.org/zh-cn/core/deployment.html),稍后会详细介绍如何使用docker来部署应用
待更新...
如果是使用 egg 框架开发的应用,强烈推荐使用 `egg-scripts` 进行部署,使用 egg 提供的一系列解决方案,包括但不限于:

- 灵活的启动参数
- [Node.js 性能平台](https://www.aliyun.com/product/nodejs)
- [egg-alinode](https://github.com/eggjs/egg-alinode)

egg 提供了从部署、进程守护、监控、问题排查等一系列的解决方案。详情见[egg 部署文档](https://eggjs.org/zh-cn/core/deployment.html)

## pm2 部署

pm2 是一个带有负载均衡功能的应用的进程管理器。当你要把你的独立代码利用全部的服务器上的所有 CPU 核数,并保证进程永远都是存活状态和 0 秒的重载,那么 PM2 是很完美的选择。详细可参考[官方的部署文档示例](http://pm2.keymetrics.io/docs/usage/deployment/)[github项目地址](https://github.com/Unitech/pm2)

pm2 有以下的几个非常给力的能力:

- 内建负载均衡(使用Node cluster 集群模块)
- 后台运行
- 0 秒停机重载,我理解大概意思是维护升级的时候不需要停机
- 具有 Ubuntu 和 CentOS 的启动脚本
- 停止不稳定的进程(避免无限循环)
- 控制台检测
- 提供 HTTP API
- 远程控制和实时的接口API (Nodejs 模块, 允许和PM2进程管理器交互)

### pm2部署简单应用

- 安装pm2

```
$ npm install -g pm2
```

- 使用 pm2 部署简单的项目

```
$ pm2 start app.js --name "egg-react-cli" -i 0 --watch
pm2 start: 使用pm2启动 app.js
-i 0: 使用最大进程数启动
–name: 指定一个你喜欢的名字
–watch: 开启监视模式,如果代码有变动pm2自动重启(一般用于本地开发 )
```

- 查看pm2部署

```
$ pm2 ls
┌───────────────┬────┬─────────┬──────┬───────┬────────┬─────────┬────────┬─────┬───────────┬──────┬──────────┐
│ App name │ id │ version │ mode │ pid │ status │ restart │ uptime │ cpu │ mem │ user │ watching │
├───────────────┼────┼─────────┼──────┼───────┼────────┼─────────┼────────┼─────┼───────────┼──────┼──────────┤
│ egg-react-ssr │ 0 │ 1.0.53 │ fork │ 58835 │ online │ 0 │ 0s │ 0% │ 17.4 MB │ xxx │ disabled │
└───────────────┴────┴─────────┴──────┴───────┴────────┴─────────┴────────┴─────┴───────────┴──────┴──────────┘
```

### pm2自动部署远程服务器

不太清楚读者是使用什么方式部署 Node.js 应用的,之前我们的做法是使用 git 托管项目,然后在服务器中安装 git 将项目克隆到服务器中,然后一台一台机器的使用 pm2 命令启动项目,如果项目有任何的修改,就会需要跑到几个服务器中 pull 代码,然后pm2 reload 项目,蛋疼的要死。
现在就使用pm2的远程部署方式,解决这个蛋疼的问题!

#### 准备工作

- git ssh

在服务器上生成git ssh公钥(本地机器和服务器操作一样),并添加到 git 上。这样服务器中clone项目也不需要输入密码。

```
$ git config --global user.name "yourname"
$ git config --global user.email "[email protected]"
$ ssh-keygen -t rsa -C "[email protected]"
```

连续三次回车,这样生成的ssh公钥添加到 github 或其他的 git 托管平台上。

- 机器免密登录

查看生成的ssh公钥:

```
$ ls ~/.ssh/
authorized_keys id_rsa id_rsa.pub known_hosts
```

理论上已经生成 ssh 公钥,在用户主目录下的 .ssh 目录中生成的 id_rsa.pub 就是生成的公钥。authorized_keys 文件是通过授权的 ssh 公钥,在使用 ssh 协议进行远程访问的时候,如果该机器的 ssh 公钥在这个文件中,那么能直接进行访问。将本地机器和线上服务器建立ssh信任,实现免密码登陆。

将ssh公钥拷贝到服务器:

```
$ scp ~/.ssh/id_rsa.pub username@ip:用户主目录/.ssh/authorized_keys
```

### pm2 配置文件 ecosystem.json

```
{
/**
* Deployment section
* http://pm2.keymetrics.io/docs/usage/deployment/
*/
"deploy" : {
"yourprojectname" : {
// 你登陆到远程主机的用户名
"user" : "node",
// 服务器的ip地址 支持数组
"host" : ["ip"],
// 部署的分支
"ref" : "origin/master",
// github 或 oschina 中托管的地址
"repo" : "your-project-repo",
// 部署到服务器的目录
"path" : "/your/deploy/folder/",
// 部署时的命令
"post-deploy" : "npm install ; pm2 start bin/www --name 'your-app-name' --watch",
// 环境变量
"env" : {
"NODE_ENV": "dev"
}
}
}
}
```

### 执行部署

```
$ pm2 deploy ecosystem.json yourprojectname setup
```

上面命令是将项目从 github 中克隆到指定 path 中,需要注意一下的是,pm2 将目录结构分为 :

```
|current | shared |source |
```

克隆好之后执行安装和启动

```
$ pm2 deploy ecosystem.json yourprojectname
```

以上的操作会将你的项目从远程仓库中克隆到服务器指定目录,然后执行配置文件中的执行命令。实现从本地执行一行命令部署多台服务器的操作。此功能可以大大减少部署和运维成本。

## 使用nginx来做负载均衡和端口代理

nginx 作为负载和代理服务可以实现在服务器上的静态资源托管、多应用 route 级别转发等基本需求。

- install

```
$ sudo yum install nginx
```

### nginx 托管静态资源

```
server {
listen 80;
server_name yourServerName;
root your/folder;
index index.html;
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {}
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
```

### nginx 开机自启

```
$ systemctl enable nginx
$ systemctl restart nginx
```

### 修改用户

nginx 文件首行默认用户为 nginx,需要修改为当前用户名。

### 本地代理某端口的服务

```
location / {
proxy_pass http://127.0.0.1:7001;
proxy_hide_header 'x-frame-options';
#root html;
#index index.html index.htm;
}
```

### 启动

```
$ sudo nginx -c /usr/local/etc/nginx/nginx.conf
```
6 changes: 6 additions & 0 deletions docs/guide/ssr-csr.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ const dev = () => {
res.write(string)
res.end()
})
},
after(app) {
app.get(/^\//, async (req, res) => {
res.write(string)
res.end()
})
}
})
server.listen(8000, 'localhost')
Expand Down
2 changes: 2 additions & 0 deletions example/ssr-with-antd/app/controller/page.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

const Controller = require('egg').Controller
const { renderToStream } = require('ykfe-utils')
const ssrConfig = require('../../config/config.ssr')

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

0 comments on commit fd17f67

Please sign in to comment.