内容简介:Hey Guys, 之前写过一篇Why use [Part II]?: Go to playreact-loadable已经好久没维护了,而且跟最新的webpack4+,还有babel7+都不兼容,还会有Deprecation Warning,如果你使用
Hey Guys, 之前写过一篇 React + Koa 服务端渲染SSR 的文章,都是大半年前的事了 ,最近回顾了一下,发现有些之前主流的懒加载组件的库已经过时了,然后关于SSR似乎之前的文章没有涉及到React-v16的功能,特别是v16新加的stream API,只是在上一篇文章的末尾提了一下,所以在这篇Part 2的版本中会添加这些新功能:beer:
Why use [Part II]?: Go to play The Last of Us
and wait for The Last of Us Part II
:children_crossing:
:tada:主要内容:
-
:scissors:替换
react-loadable,使用 loadable-components -
:chart_with_downwards_trend:使用
loadable-components来实现浏览器端和服务端的异步组件功能 - :potable_water:使用 react stream API 实现服务端渲染
- :floppy_disk:为服务端渲染的内容(html)添加缓存机制, 适用于同步和stream API
:scissors: 替换 react-loadable
react-loadable已经好久没维护了,而且跟最新的webpack4+,还有babel7+都不兼容,还会有Deprecation Warning,如果你使用 koa-web-kit
v2.8及之前的版本的话,webpack build的时候会出现warning,而且可能还有一些潜在未知的坑在里面,所以我们第一件要做的事就是把它替换成别的库,而且要跟最新的 React.lazy|React Suspense
这类API完美兼容, loadable-components
是个官方推荐的库, 如果我们既想在客户端懒加载组件,又想实现SSR的话( React.lazy
暂不支持SSR).
首先我们安装需要的库:
# For `dependencies`: npm i @loadable/component @loadable/server # For `devDependencies`: npm i -D @loadable/babel-plugin @loadable/webpack-plugin 复制代码
然后你可以在对应的webpack配置文件及babel配置文件里把 react-loadable/webpack
和 react-loadable/babel
移除掉,替换成 @loadable/webpack-plugin
和 @loadable/babel-plugin
。
然后下一步我们需要对我们的懒加载的组件做一些修改。
:chart_with_downwards_trend:使用 loadable-components 来实现浏览器端和服务端的异步组件功能
在一个需要懒加载 React 组件的地方:
// import Loadable from 'react-loadable';
import loadable from '@loadable/component';
const Loading = <h3>Loading...</h3>;
const HelloAsyncLoadable = loadable(
() => import('components/Hello'),
{ fallback: Loading, }
);
//简单使用
export default MyComponent() {
return (
<div>
<HelloAsyncLoadable />
</div>
)
}
//配合 react-router 使用
export default MyComponent() {
return (
<Router>
<Route path="/hello" render={props => <HelloAsyncLoadable {...props}/>}/>
</Router>
)
}
复制代码
其实跟之前react-loadable的使用方式差不多,传一个callback进去,返回动态import,也可以选择性的传入loading时需要显示的组件。
然后我们需要在入口文件中 hydrate
服务端渲染出来的内容,在 src/index.js
:
import React from 'react';
import ReactDOM from 'react-dom';
import { loadableReady } from '@loadable/component';
import App from './App';
loadableReady(() => {
ReactDOM.hydrate(
<App />,
document.getElementById('app')
);
});
复制代码
OK, 上面这个基本就是客户端需要做的修改,下一步我们需要对服务端的代码做修改,来使得loadable-components能完美的运行在SSR的环境中。
在之前使用react-loadable的时候,我们需要在服务端调用 Loadable.preloadAll()
来预先加载所有异步的组件,因为在服务端没必要实时异步加载组件,初始化的时候就可以全部加载进来,但是在使用loadable-components的时候已经不需要了,所以直接删掉这个方法的调用。然后在我们的服务端的webpack入口文件中:
import path from 'path';
import { StaticRouter } from 'react-router-dom';
import ReactDOMServer from 'react-dom/server';
import { ChunkExtractor } from '@loadable/server';
import AppRoutes from 'src/AppRoutes';
//...可能还一下其他的库
function render(url, initialData = {}) {
const extractor = new ChunkExtractor({ statsFile: path.resolve('../dist/loadable-stats.json') });
const jsx = extractor.collectChunks(
<StaticRouter location={url}>
<AppRoutes initialData={data} />
</StaticRouter>
);
const html = ReactDOMServer.renderToString(jsx);
const renderedScriptTags = extractor.getScriptTags();
const renderedLinkTags = extractor.getLinkTags();
const renderedStyleTags = extractor.getStyleTags();
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>React App</title>
${renderedLinkTags}
${renderedStyleTags}
</head>
<body>
<div id="app">${html}</div>
<script type="text/javascript">window.__INITIAL_DATA__ = ${JSON.stringify(
initialData
)}</script>
${renderedScriptTags}
</body>
</html>
`;
}
复制代码
其实就是 renderToString
附近那块做一些修改,根据新的库换了一些写法,对于同步渲染基本上就OK了:grinning:。
:potable_water: 服务端渲染使用 React Stream API
React v16+中,React团队添加了一个Stream API renderToNodeStream
来提升渲染大型React App的性能,由于JS的单线程特点,频繁同步的调用 renderToString
会柱塞event loop,使得其他的http请求/任务会等待很长时间,很影响性能,所以接下来我们使用流API来提升渲染的性能。
以一个koa route作为例子:
router.get('/index', async ctx => {
//防止koa自动处理response, 我们要直接把react stream pipe到ctx.res
ctx.respond = false;
//见下面render方法
const {htmlStream, extractor} = render(ctx.url);
const before = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
${extractor.getStyleTags()}
</head>
<body><div id="app">`;
//先往res里html 头部信息,包括div容器的一半
ctx.res.write(before);
//把react放回的stream pipe进res, 并且传入`end:false`关闭流的自动关闭,因为我们还有下面一半的html没有写进去
htmlStream.pipe(
ctx.res,
{ end: false }
);
//监听react stream的结束,然后把后面剩下的html写进html document
htmlStream.on('end', () => {
const after = `</div>
<script type="text/javascript">window.__INITIAL_DATA__ = ${JSON.stringify(
extra.initialData || {}
)}</script>
${extractor.getScriptTags()}
</body>
</html>`;
ctx.res.write(after);
//全部写完后,结束掉http response
ctx.res.end();
});
});
function render(url){
//...
//替换renderToString 为 renderToNodeStream,返回一个ReadableStream,其他都差不多
const htmlStream = ReactDOMServer.renderToNodeStream(jsx);
return {
htmlStream,
extractor,
}
//...
}
复制代码
上面的代码加了注释说明每一行的功能,主要分为3个部分,我们先向response写入head相关的html, 然后把react返回的readableStream pipe到response, 监听react stream的结束,然后写入剩下一般的html, 然后手动调用 res.end()
结束repsonse stream,因为我们上面关闭了response stream 的自动关闭,所以这里要手动end掉,不然浏览器会一直处于pending状态。
使用Stream API OK后,我们还有一个在生产环境中常见的问题:对于每一个进来的请求,特别是一些静态页面,我们其实没必要都重新渲染一次App, 这样的话对于同步渲染和stream渲染都会或多或少产生影响,特别是当App很大的时候,所以为了解决这样的问题,我们需要在这中间加一层缓存,我们可以存到内存,文件,或者数据库,取决于你项目的实际情况。
:floppy_disk:为服务端渲染添加缓存机制, 适用于同步和stream API
如果我们使用 renderToString
的话其实很简单,只需要拿到html后根据key(url或者其他的)存到某个地方就行了,但是对于Stream 渲染的话可能会有些tricky。因为我们把react的stream直接pipe到response了,这里我们使用了2种stream类型, ReadableStream
(ReactDom.renderToNodeStream)和 WritableStream
(ctx.res),但其实node里还有其他的stream类型,其中的TransformStream类型就可以帮我们解决上面stream的问题,我们可以在把react的readableStream pipe到TransformStream,然后这个TransformStream再pipe到res, 在transform的过程中(其实这里我们没有修改任何数据,只是为了拿到所有的html),我们就可以拿到所有react渲染出来的内容了,然后在transform结束时把所有拿到的chunk组合起来就是完整的html, 再像同步渲染的方式一样缓存起来就搞定了
OK,不扯淡了, 直接上代码:
const { Transform } = require('stream');
//这里简单用Map作为缓存的地方
const cache = new Map();
//临时的数组用来把react stream每次拿到的数据块存起来
const bufferedChunks = [];
//创建一个transform Stream来获取所有的chunk
const cacheStream = new Transform({
//每次从react stream拿到数据后,会调用此方法,存到bufferedChunks里面,然后原封不动的扔给res
transform(data, enc, cb) {
bufferedChunks.push(data);
cb(null, data);
},
//等全部结束后会调用flush
flush(cb) {
//把bufferedChunks组合起来,转成html字符串,set到cache中
cache.set(key, Buffer.concat(bufferedChunks).toString() );
cb();
},
});
复制代码
可以把上面的代码封装成一个方法,以便每次请求进来方便调用,然后我们在使用的时候:
//假设上面的代码已经封装到createCacheStream方法里了,key可以为当前的url,或者其他的
const cacheStream = createCacheStream(key);
//cacheStream现在会pipe到res
cacheStream.pipe(
res,
{ end: false }
);
//这里只显示部分html
const before = ` <!DOCTYPE html> <html lang="en"> <head>...`;
//现在是往cacheStream里直接写html
cacheStream.write(before);
// res.write(before);
//react stream pipe到cacheStream
htmlStream.pipe(
cacheStream,
{ end: false }
);
//同上监听react渲染结束
htmlStream.on('end', () => {
const after = `</div>
<script type="text/javascript">window.__INITIAL_DATA__ = ${JSON.stringify( {} )}</script>
${extractor.getScriptTags()}
</body>
</html>`;
cacheStream.write(after);
console.log('streaming rest html content done!');
//结束http response
res.end();
//结束cacheStream
cacheStream.end();
});
复制代码
上面我们把htmlStream 通过管道扔给cacheStream,来让cacheStream可以获取react渲染出来的html,并且缓存起来,然后下次同一个url请求过来时,我们可以通过key检查一下(如: cache.has(key)
)当前url是否已经有渲染过的html了,有的话直接扔给浏览器而不需要再重新渲染一遍。
好了,上面就是这次SSR更新的主要内容了。
:sparkling_heart: 想尝试完整demo的话可以关顾一下 koa-web-kit , 然后体验SSR给你带来的效果吧 :grinning:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 实现一个react系列二:渲染组件
- Flask 结合 Highcharts 实现动态渲染图表
- React服务端渲染实现(基于Dva)
- 记一次Vue动态渲染路由的实现
- Bake工作流下,如何实现卡通化渲染?
- 如何通过Geometry Shader来实现草海渲染
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
人类2.0
皮埃罗∙斯加鲁菲(Piero Scaruffi) / 闫景立、牛金霞 / 中信出版集团股份有限公司 / 2017-2-1 / CNY 68.00
《人类2.0:在硅谷探索科技未来》从在众多新技术中选择了他认为最有潜力塑造科技乃至人类未来的新技术进行详述,其中涉及大数据、物联网、人工智能、纳米科技、虚拟现实、生物技术、社交媒体、区块链、太空探索和3D打印。皮埃罗用一名硅谷工程师的严谨和一名历史文化学者的哲学视角,不仅在书中勾勒出这些新技术的未来演变方向和面貌,还对它们对社会和人性的影响进行了深入思考。 为了补充和佐证其观点,《人类2.0......一起来看看 《人类2.0》 这本书的介绍吧!