详解基于React.js和Node.js的SSR实现方案


Posted in Javascript onMarch 21, 2019

基础概念

SSR:即服务端渲染(Server Side Render) 传统的服务端渲染可以使用Java,php 等开发语言来实现,随着 Node.js 和相关前端领域技术的不断进步,前端同学也可以基于此完成独立的服务端渲染。

过程:浏览器发送请求 -> 服务器运行 react代码生成页面 -> 服务器返回页面 -> 浏览器下载HTML文档 -> 页面准备就绪 即:当前页面的内容是服务器生成好给到浏览器的。

详解基于React.js和Node.js的SSR实现方案 

对应CSR:即客户端渲染(Client Side Render) 过程:浏览器发送请求 -> 服务器返回空白 HTML(HTML里包含一个root节点和js文件) -> 浏览器下载js文件 -> 浏览器运行react代码 -> 页面准备就绪 即:当前页面的内容是js渲染出来

详解基于React.js和Node.js的SSR实现方案

如何区分页面是否服务端渲染: 右键点击 -> 显示网页源代码,如果页面上的内容在HTML文档里,是服务端渲染,否则就是客户端渲染。

对比

  • CSR:首屏渲染时间长,react代码运行在浏览器,消耗的是浏览器的性能
  • SSR:首屏渲染时间短,react代码运行在服务器,消耗的是服务器的性能

为什么要用服务端渲染

首屏加载时间优化,由于SSR是直接返回生成好内容的HTML,而普通的CSR是先返回空白的HTML,再由浏览器动态加载JavaScript脚本并渲染好后页面才有内容;所以SSR首屏加载更快、减少白屏的时间、用户体验更好。

SEO (搜索引擎优化),搜索关键词的时候排名,对大多数搜索引擎,不识别JavaScript 内容,只识别 HTML 内容。 (注:原则上可以不用服务端渲染时最好不用,所以如果只有 SEO 要求,可以用预渲染等技术去替代)

构建一个服务端渲染的项目

(1) 使用 Node.js 作为服务端和客户端的中间层,承担 proxy代理,处理cookie等操作。

(2) hydrate 的使用:在有服务端渲染情况下,使用hydrate代替render,它的作用主要是将相关的事件注水进HTML页面中(即:让React组件的数据随着HTML文档一起传递给浏览器网页),这样可以保持服务端数据和浏览器端一致,避免闪屏,使第一次加载体验更高效流畅。

ReactDom.hydrate(<App />, document.getElementById('root'));

(3) 服务端代码webpack编译:通常会建一个webpack.server.js文件,除了常规的参数配置外,还需要设置target参数为'node'。

const serverConfig = {
 target: 'node',
 entry: './src/server/index.js',
 output: {
 filename: 'bundle.js',
 path: path.resolve(__dirname, '../dist')
 },
 externals: [nodeExternals()],
 module: {
 rules: [{
  test: /\.js?$/,
  loader: 'babel-loader',
  exclude: [
  path.join(__dirname, './node_modules')
  ]
 }
 ...
 ]
 }
 (此处省略样式打包,代码压缩,运行坏境配置等等...)
 ...
};

(4) 使用react-dom/server下的 renderToString方法在服务器上把各种复杂的组件和代码转化成 HTML 字符串返回到浏览器,并在初始请求时发送标记以加快页面加载速度,并允许搜索引擎抓取页面以实现SEO目的。

const render = (store, routes, req, context) => {
 const content = renderToString((
 <Provider store={store}>
  <StaticRouter location={req.path} context={context}>
  <div>
   {renderRoutes(routes)}
  </div>
  </StaticRouter>
 </Provider>
 ));
 return `
 <html>
  <head>
  <title>ssr</title>
  </head>
  <body>
  <div id='root'>${content}</div>
  <script src='/index.js'></script>
  </body>
 </html>
 `;
}
app.get('*', function (req, res) {
 ...
 const html = render(store, routes, req, context);
 res.send(html);
});

与renderToString类似功能的还有: i. renderToStaticMarkup:区别在于renderToStaticMarkup 渲染出的是不带data-reactid的纯HTML,在JavaScript加载完成后因为不认识之前服务端渲染的内容导致重新渲染(可能页面会闪一下)。

ii. renderToNodeStream:将React元素渲染为其初始HTML,返回一个输出HTML字符串的可读流。

iii. renderToStaticNodeStream:与renderToNodeStream此类似,除了这不会创建React在内部使用的额外DOM属性,例如data-reactroot。

(5) 使用redux 承担数据准备,状态维护的职责,通常搭配react-redux, redux-thunk(中间件:发异步请求用到action)使用。(本猿目前使用比较多是就是Redux和Mobx,这里以Redux为例)。 A. 创建store(服务器每次请求都要创建一次,客户端只创建一次):

const reducer = combineReducers({
 home: homeReducer,
 page1: page1Reducer,
 page2: page2Reducer
});

export const getStore = (req) => {
 return createStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios(req))));
}

export const getClientStore = () => {
 return createStore(reducer, window.STATE_FROM_SERVER, applyMiddleware(thunk.withExtraArgument(clientAxios)));
}

B. action: 负责把数据从应用传到store,是store数据的唯一来源

export const getData = () => {
 return (dispatch, getState, axiosInstance) => {
 return axiosInstance.get('interfaceUrl/xxx')
  .then((res) => {
  dispatch({
   type: 'HOME_LIST',
   list: res.list
  })
  });
 }
}

C. reducer:接收旧的state和action,返回新的state,响应actions并发送到store。

export default (state = { list: [] }, action) => {
 switch(action.type) {
 case 'HOME_LIST':
  return {
  ...state,
  list: action.list
  }
 default:
  return state;
 }
}
export default (state = { list: [] }, action) => {
 switch(action.type) {
 case 'HOME_LIST':
  return {
  ...state,
  list: action.list
  }
 default:
  return state;
 }
}

D. 使用react-redux的connect,Provider把组件和store连接起来

Provider 将之前创建的store作为prop传给Provider

const content = renderToString((
 <Provider store={store}>
 <StaticRouter location={req.path} context={context}>
  <div>
  {renderRoutes(routes)}
  </div>
 </StaticRouter>
 </Provider>
));

connect([mapStateToProps],[mapDispatchToProps],[mergeProps], [options])接收四个参数 常用的是前两个属性 mapStateToProps函数允许我们将store中的数据作为props绑定到组件上mapDispatchToProps将action作为props绑定到组件上

connect(mapStateToProps(),mapDispatchToProps())(MyComponent)

(6) 使用react-router承担路由职责 服务端路由不同于客户端,它是无状态的。React 提供了一个无状态的组件StaticRouter,向StaticRouter传递当前URL,调用ReactDOMServer.renderToString() 就能匹配到路由视图。

服务端

import { StaticRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config'
import routes from './router.js'

<StaticRouter location={req.path} context={{context}}>
{renderRoutes(routes)}
</StaticRouter>

浏览器端

import { BrowserRouter } from 'react-router-dom';
import { renderRoutes } from 'react-router-config'
import routes from './router.js'

<BrowserRouter>
 {renderRoutes(routes)}
</BrowserRouter>

当浏览器的地址栏发生变化的时候,前端会去匹配路由视图,同时由于req.path发生变化,服务端匹配到路由视图,这样保持了前后端路由视图的一致,在页面刷新时,仍然可以正常显示当前视图。如果只有浏览器端路由,而且是采用BrowserRouter,当页面地址发生变化后去刷新页面时,由于没有对应的HTML,会导致页面找不到,但是加了服务端路由后,刷新发生时服务端会返回一个完整的html给客户端,页面仍然正常显示。 推荐使用 react-router-config插件,然后如上代码在StaticRouter和BrowserRouter标签的子元素里加renderRoutes(routes):建一个router.js文件

const routes = [{ component: Root,
 routes: [
 { path: '/',
  exact: true,
  component: Home,
  loadData: Home.loadData
 },
 { path: '/child/:id',
  component: Child,
  loadData: Child.loadData
  routes: [
  path: '/child/:id/grand-child',
  component: GrandChild,
  loadData: GrandChild.loadData
  ]
 }
 ]
}];

在浏览器端请求一个地址的时候,server.js 里在实际渲染前可以通过matchRouters 这种方式确定要渲染的内容,调用loaderData函数进行action派发,返回promise->promiseAll->renderToString,最终生成HTML文档返回。

import { matchRoutes } from 'react-router-config'
 const loadBranchData = (location) => {
 const branch = matchRoutes(routes, location.pathname)

 const promises = branch.map(({ route, match }) => {
  return route.loadData
  ? route.loadData(match)
  : Promise.resolve(null)
 })

 return Promise.all(promises)
}

(7) 写组件注意代码同构(即:一套React代码在服务端执行一次,在客户端再执行一次) 由于服务器端绑定事件是无效的,所以服务器返回的只有页面样式(&注水的数据),同时返回JavaScript文件,在浏览器上下载并执行JavaScript时才能把事件绑上,而我们希望这个过程只需编写一次代码,这个时候就会用到同构,服务端渲染出样式,在客户端执行时绑上事件。

优点: 共用前端代码,节省开发时间 弊端: 由于服务器端和浏览器环境差异,会带来一些问题,如document等对象找不到,DOM计算报错,前端渲染和服务端渲染内容不一致等;前端可以做非常复杂的请求合并和延迟处理,但为了同构,所有这些请求都在预先拿到结果才会渲染。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
为什么要在引入的css或者js文件后面加参数的详细讲解
May 03 Javascript
JavaScript的strict模式与with关键字介绍
Feb 08 Javascript
js函数名与form表单元素同名冲突的问题
Mar 07 Javascript
什么是Node.js?Node.js详细介绍
Jun 01 Javascript
js实现简单div拖拽功能实例
May 12 Javascript
jquery转盘抽奖功能实现
Nov 13 Javascript
javascript检查某个元素在数组中的索引值
Mar 30 Javascript
ES6新特征数字、数组、字符串
Oct 01 Javascript
利用JavaScript实现拖拽改变元素大小
Dec 14 Javascript
vue小白入门教程
Apr 02 Javascript
解决在Vue中使用axios用form表单出现的问题
Oct 30 Javascript
vue Element左侧无限级菜单实现
Jun 10 Javascript
javascript中call()、apply()的区别
Mar 21 #Javascript
vue实现微信获取用户信息的方法
Mar 21 #Javascript
vue里如何主动销毁keep-alive缓存的组件
Mar 21 #Javascript
基于node简单实现RSA加解密的方法步骤
Mar 21 #Javascript
详解React项目如何修改打包地址(编译输出文件地址)
Mar 21 #Javascript
js实现unicode码字符串与utf8字节数据互转详解
Mar 21 #Javascript
详解JS取出两个数组中的不同或相同元素
Mar 20 #Javascript
You might like
php visitFile()遍历指定文件夹函数
2010/08/21 PHP
destoon供应信息title调用出公司名称的方法
2014/08/22 PHP
PHP代码优化技巧小结
2015/09/29 PHP
Thinkphp框架开发移动端接口(1)
2016/08/18 PHP
workerman结合laravel开发在线聊天应用的示例代码
2018/10/30 PHP
JS阻止事件冒泡行为和闭包的方法
2016/06/16 Javascript
值得分享的Bootstrap Table使用教程
2016/11/23 Javascript
简单实现js菜单栏切换效果
2017/03/04 Javascript
原生JS实现左右箭头选择日期实例代码
2017/03/14 Javascript
JS表格组件神器bootstrap table使用指南详解
2017/04/12 Javascript
详解vue2.0监听属性的使用心得及搭配计算属性的使用
2018/07/18 Javascript
Vue+Koa2 打包后进行线上部署的教程详解
2019/07/31 Javascript
Vue 动态路由的实现及 Springsecurity 按钮级别的权限控制
2019/09/05 Javascript
layui问题之自动滚动二级iframe页面到指定位置的方法
2019/09/18 Javascript
前端vue-cli项目中使用img图片和background背景图的几种方法
2019/11/13 Javascript
vue-cli3配置favicon.ico和title的流程
2020/10/27 Javascript
[03:04]2018年国际邀请赛典藏宝瓶&莱恩声望物品展示 片尾有彩蛋
2018/06/04 DOTA
[01:16]DOTA2小知识课堂 Ep.03 芒果树无伤肉山
2019/12/05 DOTA
[00:57]英雄,你的补给到了!
2020/11/13 DOTA
Python列表(list)常用操作方法小结
2015/02/02 Python
python使用wxpython开发简单记事本的方法
2015/05/20 Python
用Python设计一个经典小游戏
2017/05/15 Python
Python读取word文本操作详解
2018/01/22 Python
Flask框架学习笔记之消息提示与异常处理操作详解
2019/08/15 Python
python+opencv实现车牌定位功能(实例代码)
2019/12/24 Python
基于MSELoss()与CrossEntropyLoss()的区别详解
2020/01/02 Python
CSS3 Flexbox中flex-shrink属性的用法示例介绍
2013/12/30 HTML / CSS
CSS Grid布局教程之什么是网格布局
2014/12/30 HTML / CSS
canvas里面如何基于随机点绘制一个多边形的方法
2018/06/13 HTML / CSS
美国汽车性能部件和赛车零件网站:Vivid Racing
2018/03/27 全球购物
市场营销个人求职信范文
2014/02/02 职场文书
个人借款担保书
2014/04/02 职场文书
巴西世界杯32强口号
2014/06/05 职场文书
党员考试作弊检讨书1000字
2015/02/16 职场文书
网络研修心得体会
2016/01/08 职场文书
Pytorch实现图像识别之数字识别(附详细注释)
2021/05/11 Python