⚛️React是目前前端社区最流行的UI库之一,它的基于组件化的开发方式极大地提升了前端开发体验,React通过拆分一个大的应用至一个个小的组件,来使得我们的代码更加的可被重用,以及获得更好的可维护性,等等还有其他很多的优点...

Part II 版本 传送门

通过React, 我们通常会开发一个单页应用(SPA),单页应用在浏览器端会比传统的网页有更好的用户体验,浏览器一般会拿到一个body为空的html,然后加载script指定的js, 当所有js加载完毕后,开始执行js, 最后再渲染到dom中, 在这个过程中,一般用户只能等待,什么都做不了,如果用户在一个高速的网络中,高配置的设备中,以上先要加载所有的js然后再执行的过程可能不是什么大问题,但是有很多情况是我们的网速一般,设备也可能不是最好的,在这种情况下的单页应用可能对用户来说是个很差的用户体验,用户可能还没体验到浏览器端SPA的好处时,就已经离开网站了,这样的话你的网站做的再好也不会有太多的浏览量。

但是我们总不能回到以前的一个页面一个页面的传统开发吧,现代化的UI库都提供了服务端渲染(SSR)的功能,使得我们开发的SPA应用也能完美的运行在服务端,大大加快了首屏渲染的时间,这样的话用户既能更快的看到网页的内容,与此同时,浏览器同时加载需要的js,加载完后把所有的dom事件,及各种交互添加到页面中,最后还是以一个SPA的形式运行,这样的话我们既提升了首屏渲染的时间,又能获得SPA的客户端用户体验,对于SEO也是个必须的功能?。

OK,我们大致了解了SSR的必要性,下面我们就可以在一个React App中来实现服务端渲染的功能,BTW, 既然我们已经处在一个到处是async/await的环境中,这里的服务端我们使用koa2来实现我们的服务端渲染。

初始化一个普通的单页应用SPA

首先我们先不管服务端渲染的东西,我们先创建一个基于React和React-Router的SPA,等我们把一个完整的SPA创建好后,再加入SSR的功能来最大化提升app的性能。

首先进入app入口 App.js:

import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route } from 'react-router-dom';const Home = () => <div>Home</div>;
const Hello = () => <div>Hello</div>;const App = () => {return (<Router><Route exact path="/" component={Home} /><Route exact path="/hello" component={Hello} /></Router>)
}ReactDOM.render(<App/>, document.getElementById('app'))
复制代码

上面我们为路由//hello创建了2个只是渲染一些文字到页面的组件。但当我们的项目变得越来越大,组件越来越多,最终我们打包出来的js可能会变得很大,甚至变得不可控,所以呢我们第一步需要优化的是代码拆分(code-splitting),幸运的是通过webpack dynamic import 和 react-loadable,我们可以很容易做到这一点。

用React-Loadable来时间代码拆分

使用之前,先安装 react-loadable:

npm install react-loadable
# or
yarn add react-loadable
复制代码

然后在你的 javascript中:

//...
import Loadable from 'react-loadable';
//...const AsyncHello = Loadable({loading: <div>loading...</div>,//把你的Hello组件写到单独的文件中//然后使用webpack的 dynamic importloader: () => import('./Hello'),
})//然后在你的路由中使用loadable包装过的组件:
<Route exact path="/hello" component={AsyncHello} />
复制代码

很简单吧,我们只需要import react-loadable, 然后传一些option进去就行了,其中的loading选项是当动态加载Hello组件所需的js时,渲染loading组件,给用户一种加载中的感觉,体验也会比什么都不加好。

好了,现在如果我们访问首页的话,只有Home组件依赖的js才会被加载,然后点击某个链接进入hello页面的话,会先渲染loading组件,并同时异步加载hello组件依赖的js,加载完后,替换掉loading来渲染hello组件。通过基于路由拆分代码到不同的代码块,我们的SPA已经有了很大的优化,cheers?。更叼的是react-loadable同样支持SSR,所以你可以在任意地方使用react-loadable,不管是运行在前端还是服务端,要让react-loadable在服务端正常运行的话我们需要做一些额外的配置,本文后面会讲到,先不急?。‍

到这里我们已经创建好一个基本的React SPA,加上代码拆分,我们的app已经有了不错的性能,但是我们还可以更加极致的优化app的性能,下面我们通过增加SSR的功能来进一步提升加载速度,顺便解决一下SPA中的SEO问题?。

加入服务端渲染(SSR)功能

首先我们先搭建一个最简单的koa web服务器:

npm install koa koa-router
复制代码

然后在koa的入口文件app.js中:

const Koa = require('koa');
const Router = require('koa-router');const app = new Koa();
const router = new Router();
router.get('*', async (ctx) => {ctx.body = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>React SSR</title></head><body><div id="app"></div><script type="text/javascript" src="/bundle.js"></script></body></html>`;
});app.use(router.routes());
app.listen(3000, '0.0.0.0');
复制代码

上面*路由代表任意的url进来我们都默认渲染这个html,包括html中打包出来的js,你也可以用一些服务端模板引擎(如:nunjucks)来直接渲染html文件,在webpack打包时通过html-webpack-plugin来自动插入打包出来的js/css资源路径。

OK, 我们的简易koa server好了,接下来我们开始编写React SSR的入口文件AppSSR.js,这里我们需要使用StaticRouter来代替之前的BrowserRouter,因为在服务端,路由是静态的,用BrowserRouter的话是不起作用的,后面还会做一些配置来使得react-loadable运行在服务端。

提示: 你可以把整个node端的代码用ES6/JSX风格编写,而不是部分commonjs,部分JSX, 但这样的话你需要用webpack把整个服务端的代码编译成commonjs风格,才能使得它运行在node环境中,这里的话我们把React SSR的代码单独抽出去,然后在普通的node代码里去require它。因为可能在一个现有的项目中,之前都是commonjs的风格,把以前的node代码一次性转成ES6的话成本有点大,但是可以后期一步步的再迁移过去

OK, 现在在你的 AppSSR.js中:

import React from 'react';
//使用静态 static router
import { StaticRouter } from 'react-router-dom';
import ReactDOMServer from 'react-dom/server';
import Loadable from 'react-loadable';
//下面这个是需要让react-loadable在服务端可运行需要的,下面会讲到
import { getBundles } from 'react-loadable/webpack';
import stats from '../build/react-loadable.json';//这里吧react-router的路由设置抽出去,使得在浏览器跟服务端可以共用
//下面也会讲到...
import AppRoutes from 'src/AppRoutes';//这里我们创建一个简单的class,暴露一些方法出去,然后在koa路由里去调用来实现服务端渲染
class SSR {//koa 路由里会调用这个方法render(url, data) {let modules = [];const context = {};const html = ReactDOMServer.renderToString(<Loadable.Capture report={moduleName => modules.push(moduleName)}><StaticRouter location={url} context={context}><AppRoutes initialData={data} /></StaticRouter></Loadable.Capture>);//获取服务端已经渲染好的组件数组let bundles = getBundles(stats, modules);return {html,scripts: this.generateBundleScripts(bundles),};}//把SSR过的组件都转成script标签扔到html里generateBundleScripts(bundles) {return bundles.filter(bundle => bundle.file.endsWith('.js')).map(bundle => {return `<script type="text/javascript" src="${bundle.file}"></script>\n`;});}static preloadAll() {return Loadable.preloadAll();}
}export default SSR;
复制代码

当编译这个文件的时候,在webpack配置里使用target: "node"externals,并且在你的打包前端app的webpack配置中,需要加入react-loadable的插件,app的打包需要在ssr打包之前运行,不然拿不到react-loadable需要的各组件信息,先来看app的打包:

//webpack.config.dev.js, app bundle
const ReactLoadablePlugin = require('react-loadable/webpack').ReactLoadablePlugin;module.exports = {//...plugins: [//...new ReactLoadablePlugin({ filename: './build/react-loadable.json', }),]
}
复制代码

.babelrc中加入loadable plugin:

{"plugins": ["syntax-dynamic-import","react-loadable/babel",["import-inspector", {"serverSideRequirePath": true}]]
}
复制代码

上面的配置会让react-loadable知道哪些组件最终在服务端被渲染了,然后直接插入到html script标签中,并在前端初始化时把SSR过的组件考虑在内,避免重复加载,下面是SSR的打包:

//webpack.ssr.js
const nodeExternals = require('webpack-node-externals');module.exports = {//...target: 'node',output: {path: 'build/node',filename: 'ssr.js',libraryExport: 'default',libraryTarget: 'commonjs2',},//避免把node_modules里的库都打包进去,此ssr js会直接运行在node端,//所以不需要打包进最终的文件中,运行时会自动从node_modules里加载externals: [nodeExternals()],//...
}
复制代码

然后在koa app.js, require它,并且调用SSR的方法:

//...koa app.js
//build出来的ssr.js
const SSR = require('./build/node/ssr');
//preload all components on server side, 服务端没有动态加载各个组件,提前先加载好
SSR.preloadAll();//实例化一个SSR对象
const s = new SSR();router.get('*', async (ctx) => {//根据路由,渲染不同的页面组件const rendered = s.render(ctx.url);const html = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"></head><body><div id="app">${rendered.html}</div><script type="text/javascript" src="/runtime.js"></script>${rendered.scripts.join()}<script type="text/javascript" src="/app.js"></script></body></html>`;ctx.body = html;
});
//...
复制代码

以上是个简单的实现React SSR到koa web server, 为了使react-loadable知道哪些组件在服务端渲染了,rendered里面的scripts数组里面包含了SSR过的组件组成的各个script标签,里面调用了SSR#generateBundleScripts()方法,在插入时需要确保这些script标签在runtime.js之后((通过 CommonsChunkPlugin 来抽出来)),并且在app bundle之前(也就是初始化的时候应该已经知道之前的哪些组件已经渲染过了)。更多react-loadable服务端支持,参考这里.

上面我们还把react-router的路由都单独抽出去了,使得它可以运行在浏览器跟服务端,以下是AppRoutes组件:

//AppRoutes.js
import Loadable from 'react-loadable';
//...const AsyncHello = Loadable({loading: <div>loading...</div>,loader: () => import('./Hello'),
})function AppRoutes(props) {<Switch><Route exact path="/hello" component={AsyncHello} /><Route path="/" component={Home} /></Switch>
}export default AppRoutes//然后在 App.js 入口中
import AppRoutes from './AppRoutes';
// ...
export default () => {return (<Router><AppRoutes/></Router>)
}
复制代码

服务端渲染的初始状态

目前为止,我们已经创建了一个React SPA,并且能在浏览器端跟服务端共同运行?,社区称之为universal app 或者 isomophic app。但是我们现在的app还有一个遗留问题,一般来说我们app的数据或者状态都需要通过远端的api来异步获取,拿到数据后我们才能开始渲染组件,服务端SSR也是一样,我们要动态的获取初始数据,然后才能扔给React去做SSR,然后在浏览器端我们还要初始化就能同步获取这些SSR时的初始化数据,避免浏览器端初始化时又重新获取了一遍。

下面我们简单从github获取一些项目的信息作为页面初始化的数据, 在koa的app.js中:

//...
const fetch = require('isomorphic-fetch');router.get('*', async (ctx) => {//fetch branch info from githubconst api = 'https://api.github.com/repos/jasonboy/wechat-jssdk/branches';const data = await fetch(api).then(res => res.json());//传入初始化数据const rendered = s.render(ctx.url, data);const html = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"></head><body><div id="app">${rendered.html}</div><script type="text/javascript">window.__INITIAL_DATA__ = ${JSON.stringify(data)}</script><script type="text/javascript" src="/runtime.js"></script>${rendered.scripts.join()}<script type="text/javascript" src="/app.js"></script></body></html>`;ctx.body = html;
});
复制代码

然后在你的Hello组件中,你需要checkwindow里面(或者在App入口中统一判断,然后通过props传到子组件中)是否存在window.__INITIAL_DATA__,有的话直接用来当做初始数据,没有的话我们在componentDidMount生命周期函数中再去来数据:

export default class Hello extends React.Component {constructor(props) {super(props);this.state = {//这里直接判断window,如果是父组件传入的话,通过props判断github: window.__INITIAL_DATA__ || [],};}componentDidMount() {//判断没有数据的话,再去请求数据//请求数据的方法也可以抽出去,以让浏览器及服务端能统一调用,避免重复写if (this.state.github.length <= 0) {fetch('https://api.github.com/repos/jasonboy/wechat-jssdk/branches').then(res => res.json()).then(data => {this.setState({ github: data });});}}render() {return (<div><ul>{this.state.github.map(b => {return <li key={b.name}>{b.name}</li>;})}</ul></div>);}
}
复制代码

好了,现在如果页面被服务端渲染过的话,浏览器会拿到所有渲染过的html, 包括初始化数据,然后通过这些SSR的内容配合加载的js,再组成一个完整的SPA,就像一个普通的SPA一样,但是我们得到了更好的性能,更好的SEO?。

?React-v16 更新

在React的最新版v16中,SSR的API做了很多的优化,并且提供了新的基于流的API来更好的提升性能,通过streaming api, 服务端可以边渲染边把前面渲染好的html发到浏览器,浏览器端也可以提前开始渲染页面而不是等服务端所有组件都渲染完成后才能开始浏览器端的初始化,提升了性能也降低了服务端资源的消耗。还有一个在浏览器端需要注意的是需要使用ReactDOM.hydrate()来代替之前的ReactDOM.render(),更多的更新参考medium文章whats-new-with-server-side-rendering-in-react-16.

?要查看完整的demo, 参考 koa-web-kit, koa-web-kit是一个现代化的基于React/Koa的全栈开发框架,包括React SSR支持,可以直接用来测试服务端渲染的功能?

结论

好了,以上就是React-SSR + Koa的简单实践,通过SSR,我们既提升了性能,又很好的满足了SEO的要求,Best of the Both Worlds?。

PPT in Browser

English Version

React + Koa 实现服务端渲染(SSR)相关推荐

  1. 服务端渲染(SSR) 通用技术解决方案

    项目背景 服务端渲染(SSR) 通用技术解决方案的诞生来源于对 360搜索百科移动端项目的一次重构实践.而当时决定重构该项目的主要原因有以下几点: 1.  技术栈陈旧,熟悉.开发以及维护成本都较高 项 ...

  2. 服务端渲染SSR与客户端渲染

    文章目录 一.服务端渲染和客户端渲染的区别 服务端渲染(SSR -- server side render) 客户端渲染 二.使用服务端渲染(SSR)的利弊 优势 局限 三.实际应用 应用原则 举例 ...

  3. vue服务端渲染ssr

    vue服务端渲染ssr 一.SSR概念 传统web渲染技术 SPA SSR 二.webpack+vue2的实现方式 1.创建工程 2.安装依赖 3.编写一个简单的SSR 4.完整的ssr 5.代码链接 ...

  4. 服务端渲染SSR的理解

    服务端渲染SSR的理解 SSR服务端渲染Server Side Render就是当进行请求时,页面上的内容是通过服务端渲染生成的,浏览器直接显示服务端返回的HTML即可. 客户端渲染CSR 通常在构建 ...

  5. node+ejs实现服务端渲染SSR

    一.服务端渲染SSR 1.什么是客户端渲染和服务端渲染? 客户端渲染:客户端向服务端请求HTML页面,后端不返回完整的 HTML页面,浏览器解析HTML页面过程中如果遇到ajax数据请求,会向服务端请 ...

  6. 服务端渲染(SSR)和Nuxt.js

    服务端渲染(SSR) 客户端渲染和传统服务端渲染的问题 SPA应用有两个非常明显的问题: 首屏渲染慢 不利于 SEO 传统的服务端渲染又存在: 应用的前后端部分完全耦合在一起,在前后端协同开发方面会有 ...

  7. 如何在 React 18中 利用Suspense 实现 服务端渲染(SSR)

    概述 React 18 将包括对 其服务器端渲染 (SSR) 性能的架构做了改进.这些改进带来了实质性的效果,是几年来其团队工作的结晶.大多数的改进点都是在幕后进行的,但您需要了解一些选择加入机制,尤 ...

  8. ssr Android简书,react服务端渲染ssr

    Next.js 一个轻量级的 React 服务端渲染框架 1 概念 SPA single page application : 单页面应用程序 缺点:首屏加载慢,不利于SEO SSR Server-s ...

  9. 服务端渲染ssr(server side render)

    目录 1 - 什么是服务器端渲染? 1.1 新建server文件夹 1.2 生成一个node项目 1.3 安装express 1.4 服务端渲染小案例 1.5 运行查看效果 1.6 打开浏览器 1.7 ...

最新文章

  1. PCB设计要点-DDR3布局布线技巧及注意事项
  2. IIS支持http协议的:put、delete等方法
  3. python从入门到精通视频百度云资源_python从入门到精通视频(全60集)声音修复版...
  4. 祝各位节日快乐!20151111
  5. 汇编:ret以及retf指令
  6. esp8266单片机透传_ESP8266通过MQTT接入Home Assistant
  7. 高德地图上线车道级导航 华为Mate 40用户可尝鲜体验
  8. mysql nt.exe 占cpu_解决并分析mysqld-nt.exe大量占用CPU问题
  9. 阿里云 ECS迁移数据至腾讯云云服务器
  10. 均值、均方值、方差、均方差和协方差概念及其物理意义
  11. linux文件管理命令ppt,linux命令以及文件管理.ppt
  12. 字体文件检查工具Webfont Generator
  13. halcon—利用顶帽操作减轻图像灰度不均匀对二值化的影响
  14. 素数问题 java_JAVA素数问题
  15. 【在linux中搭建网络文件系统NFS的详细教程】
  16. STM32 CAN过滤器详解
  17. 全国三级考试1-20
  18. 会计方面专业术语的英文翻译
  19. 员工出错处罚通知_员工犯错处罚通告
  20. 【元胞自动机】元胞自动机短消息网络病毒传播仿真【含Matlab源码 1289期】

热门文章

  1. [转] Java中public,private,final,static等概念的解读
  2. 【Treap】bzoj1588-HNOI2002营业额统计
  3. IoC容器Autofac(1) -- 什么是IoC以及理解为什么要使用Ioc
  4. 如何将广告始终定位到网页右下角
  5. 中兴V880使用手记之五——刷入recovery
  6. 数据连接池的工作机制是什么?
  7. 【跃迁之路】【554天】程序员高效学习方法论探索系列(实验阶段311-2018.08.13)...
  8. 用Inno Setup来解决.NetFramework安装问题
  9. 【智能家居篇】wifi网络结构(上)
  10. MySQL案例-多源复制引起的内存泄漏