这里指的可降级与服务器降级意思相近(主逻辑失败采用备用逻辑的过程),考虑到服务端渲染并发及高负载的问题,在主服务器没法提供正常服务的时候可降级为"低功耗模式",采用客户端渲染方式来减轻服务器负载。javascript
我看了一下next.js,他们的开发方式很新颖,尤为是路由配置,彻底不用本身配,按照他的规则来作就好了。但我这我的比较求稳,这种约定大于配置的方式对于我来讲总以为会有坑,没法本身掌控,找不到解决办法还得看源码才知道。css
仍是求稳的我,毕竟服务端渲染是须要消耗服务器性能的,尤为是高并发和内存溢出,可能会致使服务器响应变慢甚至挂掉,因此我但愿个人框架还能降级,这个降级与服务器降级相似,在服务器高负载的时候,切换另外一台机器,启动"低功耗模式"(也就是客户端渲染的单页应用)来继续运做,我查了next.js好像没有这个功能(若是有请在评论告知)。前端
实在找不到既能服务端渲染又能客户端渲染的框架,最后我就找了这个项目https://github.com/cereallarceny/cra-ssr 进行改造,改造仍是蛮成功的,过程遇到不少坑,在这里分享一些过程步骤。java
其实cra-ssr这个项目自己已经作好了服务端同构渲染,在改造以前最好先理解cra-ssr实现同构渲染的流程原理再看下面的步骤,下面是cra-ssr实现同构渲染的流程图 node
下图是降级改造后的流程图: react
上图里黄色区域的frontload是用在服务端渲染时预先处理的接口请求预取数据,而后放入准备好的global_state,global_state是一个store,做用是存放预取数据再connect给组件渲染,同时也能够做为服务端与客户端均可用的全局变量;git
蓝色节点是服务器环境,橙色节点是客户端环境,当服务器渲染响应给客户端以后,用户进行了路由跳转,这时前端无刷新跳转到新页面,再走黄色区域取到页面数据,交给client_render渲染出新页面github
同构框架因为是同一套代码,但环境不一样有些对象是不自带的,咱们要加以区分避免出现问题,这个项目有提供一个isServer变量用来判断是服务端环境仍是客户端环境,而咱们作降级了就还要加多一个环境判断是不是客户端渲染的模式来区分,后面就知道为何要作区分了。typescript
咱们须要作3个环境的区分express
1.服务端渲染环境;
2.服务端渲染完成后的客户端环境;
3.客户端渲染环境;
我是这么作环境区分的:
// 经过node环境独有的process和具体的环境变量区分isServer
export const isServer = !!process&&!!process.env&&(process.env.RENDER_ENV == 'server');
// 只要不是服务端渲染环境都属于isClient
export const isClient = !isServer;
// 区分降级后的客户端渲染isCSR,CSR是没有服务端渲染事后填充的数据__PRELOADED_STATE__的
export const isCSR = !isServer && !window.__PRELOADED_STATE__;
复制代码
cra-ssr是使用react-frontload来作首屏异步处理的,用法看这个文件src/app/routes/profile/index.js,在服务端经过预取数据交给状态管理redux的store,插入到window.__PRELOADED_STATE__
做为初始store,在客户端拿window.__PRELOADED_STATE__
做为初始store,connect组件获得数据填充,
而客户端渲染是没有预取数据window.__PRELOADED_STATE__
的,因此frontload要同时知足下面3个条件:
①frontload在服务端渲染完以后到客户端首屏不能重复执行发请求
②降级后客户端渲染模式能执行frontload发请求拉取数据并保证跟服务端一样的写法
③服务端渲染完以后到客户端通过用户点击路由跳转又要能像客户端同样请求拉取数据
为了保证一样的写法,服务端渲染与客户端渲染必须统一用状态管理的store,须要封装一个特定的global_state用来传首屏数据的store,再封装一下frontload,写了个Adapter:
import { frontloadConnect } from 'react-frontload';
import {isServer, isCSR} from 'xxx/xxx';
import {change_state, get_state} from 'xxx/global_state';
import {connect} from 'dva';
export const frontload = (frontload_fn) => {
return (Target) => {
// 为组件默认注入global_state
return connect(({global}) => ({global}))(frontloadConnect(async (props) => {
// get_state('client_load') => 在客户端触发路由跳转的时候为true
if (get_state('client_load') || isServer || isCSR) {
await frontload_fn(props)
}
})(Target))
}
}
复制代码
用这个frontload来代替原来的frontloadConnect函数,不只简化了写法,还保持了同一套代码知足上面三个条件。
在组件里能够这么写:
//(本项目引入了dva、Typescript、scss和react-css-modules)
import * as React from 'react';
import { isCSR } from 'xxx/xxx';
const styles = require('./index.scss');
const CSSModules = require('react-css-modules');
import { frontload } from 'xxx/adapter';
import { API } from 'xxx/http';
import {config} from 'src/utils/config';
@frontload(async (props) => {
const res = await API.get('/getNews');
// 经过更新store的global达到统一服务端与客户端的写法。
props.dispatch({type: 'global/set', payload: {data_list: res.data}})
})
@CSSModules(styles)
export default class News extends React.PureComponent<any,any>{
constructor(props){
super(props);
this.state = {
news_list: [],
}
}
componentDidMount() {
this.setState({
// 经过frontload预取数据填充了global数据,并自动connect当前组件
// 因此组件不须要再connect就能够访问global数据
news_list: this.props.global.data_list,
})
}
render() {
const news_list = this.props.global.data_list;
return (
news_list.map((item) => {
return <section> <p>标题:{item.title}</p> <p>摘要:{item.abstract}</p> </section>
})
)
}
}
复制代码
还要添加一个启动客户端渲染的服务器文件,好比csr_server.js
var path = require('path')
var express = require('express')
var compression = require('compression')
var app = express()
var data = require('./data_config.json');
app.use(compression());
// 构建后的资源文件夹
app.use(express.static(path.join(__dirname, '../dist/')));
port = 8080
app.listen(port, function () {
console.log('The app server is working at ' + port)
})
复制代码
这就是我改造服务端同构框架降级的一些步骤,具体仍是要本身好好理解才行,根据自身项目需求调整(好比个人项目还引入了dva、typescript之类的),会遇到不少坑的😂
这篇文章只是在前端的角度讲降级的,至于具体在服务器上怎么降级,就得从运维角度看怎样的规则适合降级,怎么降级这些问题了,就不在此文讲述了。