欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解React+Koa實(shí)現(xiàn)服務(wù)端渲染(SSR)

 更新時(shí)間:2018年05月23日 10:41:16   作者:jasonboy7  
這篇文章主要介紹了詳解React+Koa實(shí)現(xiàn)服務(wù)端渲染(SSR),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧

React是目前前端社區(qū)最流行的UI庫(kù)之一,它的基于組件化的開(kāi)發(fā)方式極大地提升了前端開(kāi)發(fā)體驗(yàn),React通過(guò)拆分一個(gè)大的應(yīng)用至一個(gè)個(gè)小的組件,來(lái)使得我們的代碼更加的可被重用,以及獲得更好的可維護(hù)性,等等還有其他很多的優(yōu)點(diǎn)...

通過(guò)React, 我們通常會(huì)開(kāi)發(fā)一個(gè)單頁(yè)應(yīng)用(SPA),單頁(yè)應(yīng)用在瀏覽器端會(huì)比傳統(tǒng)的網(wǎng)頁(yè)有更好的用戶體驗(yàn),瀏覽器一般會(huì)拿到一個(gè)body為空的html,然后加載script指定的js, 當(dāng)所有js加載完畢后,開(kāi)始執(zhí)行js, 最后再渲染到dom中, 在這個(gè)過(guò)程中,一般用戶只能等待,什么都做不了,如果用戶在一個(gè)高速的網(wǎng)絡(luò)中,高配置的設(shè)備中,以上先要加載所有的js然后再執(zhí)行的過(guò)程可能不是什么大問(wèn)題,但是有很多情況是我們的網(wǎng)速一般,設(shè)備也可能不是最好的,在這種情況下的單頁(yè)應(yīng)用可能對(duì)用戶來(lái)說(shuō)是個(gè)很差的用戶體驗(yàn),用戶可能還沒(méi)體驗(yàn)到瀏覽器端SPA的好處時(shí),就已經(jīng)離開(kāi)網(wǎng)站了,這樣的話你的網(wǎng)站做的再好也不會(huì)有太多的瀏覽量。

但是我們總不能回到以前的一個(gè)頁(yè)面一個(gè)頁(yè)面的傳統(tǒng)開(kāi)發(fā)吧,現(xiàn)代化的UI庫(kù)都提供了服務(wù)端渲染(SSR)的功能,使得我們開(kāi)發(fā)的SPA應(yīng)用也能完美的運(yùn)行在服務(wù)端,大大加快了首屏渲染的時(shí)間,這樣的話用戶既能更快的看到網(wǎng)頁(yè)的內(nèi)容,與此同時(shí),瀏覽器同時(shí)加載需要的js,加載完后把所有的dom事件,及各種交互添加到頁(yè)面中,最后還是以一個(gè)SPA的形式運(yùn)行,這樣的話我們既提升了首屏渲染的時(shí)間,又能獲得SPA的客戶端用戶體驗(yàn),對(duì)于SEO也是個(gè)必須的功能。

OK,我們大致了解了SSR的必要性,下面我們就可以在一個(gè)React App中來(lái)實(shí)現(xiàn)服務(wù)端渲染的功能,BTW, 既然我們已經(jīng)處在一個(gè)到處是async/await的環(huán)境中,這里的服務(wù)端我們使用koa2來(lái)實(shí)現(xiàn)我們的服務(wù)端渲染。

初始化一個(gè)普通的單頁(yè)應(yīng)用SPA

首先我們先不管服務(wù)端渲染的東西,我們先創(chuàng)建一個(gè)基于React和React-Router的SPA,等我們把一個(gè)完整的SPA創(chuàng)建好后,再加入SSR的功能來(lái)最大化提升app的性能。

首先進(jìn)入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'))

上面我們?yōu)槁酚? 和 /hello創(chuàng)建了2個(gè)只是渲染一些文字到頁(yè)面的組件。但當(dāng)我們的項(xiàng)目變得越來(lái)越大,組件越來(lái)越多,最終我們打包出來(lái)的js可能會(huì)變得很大,甚至變得不可控,所以呢我們第一步需要優(yōu)化的是代碼拆分(code-splitting),幸運(yùn)的是通過(guò)webpack dynamic import react-loadable,我們可以很容易做到這一點(diǎn)。

用React-Loadable來(lái)時(shí)間代碼拆分

使用之前,先安裝 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組件寫到單獨(dú)的文件中
 //然后使用webpack的 dynamic import
 loader: () => import('./Hello'), 
})

//然后在你的路由中使用loadable包裝過(guò)的組件:
<Route exact path="/hello" component={AsyncHello} />

很簡(jiǎn)單吧,我們只需要import react-loadable, 然后傳一些option進(jìn)去就行了,其中的loading選項(xiàng)是當(dāng)動(dòng)態(tài)加載Hello組件所需的js時(shí),渲染loading組件,給用戶一種加載中的感覺(jué),體驗(yàn)也會(huì)比什么都不加好。

好了,現(xiàn)在如果我們?cè)L問(wèn)首頁(yè)的話,只有Home組件依賴的js才會(huì)被加載,然后點(diǎn)擊某個(gè)鏈接進(jìn)入hello頁(yè)面的話,會(huì)先渲染loading組件,并同時(shí)異步加載hello組件依賴的js,加載完后,替換掉loading來(lái)渲染hello組件。通過(guò)基于路由拆分代碼到不同的代碼塊,我們的SPA已經(jīng)有了很大的優(yōu)化,cheers🍻。更叼的是react-loadable同樣支持SSR,所以你可以在任意地方使用react-loadable,不管是運(yùn)行在前端還是服務(wù)端,要讓react-loadable在服務(wù)端正常運(yùn)行的話我們需要做一些額外的配置,本文后面會(huì)講到,先不急🏃。‍

到這里我們已經(jīng)創(chuàng)建好一個(gè)基本的React SPA,加上代碼拆分,我們的app已經(jīng)有了不錯(cuò)的性能,但是我們還可以更加極致的優(yōu)化app的性能,下面我們通過(guò)增加SSR的功能來(lái)進(jìn)一步提升加載速度,順便解決一下SPA中的SEO問(wèn)題🎉。

加入服務(wù)端渲染(SSR)功能

首先我們先搭建一個(gè)最簡(jiǎn)單的koa web服務(wù)器:

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進(jìn)來(lái)我們都默認(rèn)渲染這個(gè)html,包括html中打包出來(lái)的js,你也可以用一些服務(wù)端模板引擎(如:nunjucks)來(lái)直接渲染html文件,在webpack打包時(shí)通過(guò)html-webpack-plugin來(lái)自動(dòng)插入打包出來(lái)的js/css資源路徑。

OK, 我們的簡(jiǎn)易koa server好了,接下來(lái)我們開(kāi)始編寫React SSR的入口文件AppSSR.js,這里我們需要使用StaticRouter來(lái)代替之前的BrowserRouter,因?yàn)樵诜?wù)端,路由是靜態(tài)的,用BrowserRouter的話是不起作用的,后面還會(huì)做一些配置來(lái)使得react-loadable運(yùn)行在服務(wù)端。

提示: 你可以把整個(gè)node端的代碼用ES6/JSX風(fēng)格編寫,而不是部分commonjs,部分JSX, 但這樣的話你需要用webpack把整個(gè)服務(wù)端的代碼編譯成commonjs風(fēng)格,才能使得它運(yùn)行在node環(huán)境中,這里的話我們把React SSR的代碼單獨(dú)抽出去,然后在普通的node代碼里去require它。因?yàn)榭赡茉谝粋€(gè)現(xiàn)有的項(xiàng)目中,之前都是commonjs的風(fēng)格,把以前的node代碼一次性轉(zhuǎn)成ES6的話成本有點(diǎn)大,但是可以后期一步步的再遷移過(guò)去

OK, 現(xiàn)在在你的 AppSRR.js中:

import React from 'react';
//使用靜態(tài) static router
import { StaticRouter } from 'react-router-dom';
import ReactDOMServer from 'react-dom/server';
import Loadable from 'react-loadable';
//下面這個(gè)是需要讓react-loadable在服務(wù)端可運(yùn)行需要的,下面會(huì)講到
import { getBundles } from 'react-loadable/webpack';
import stats from '../build/react-loadable.json';

//這里吧react-router的路由設(shè)置抽出去,使得在瀏覽器跟服務(wù)端可以共用
//下面也會(huì)講到...
import AppRoutes from 'src/AppRoutes';

//這里我們創(chuàng)建一個(gè)簡(jiǎn)單的class,暴露一些方法出去,然后在koa路由里去調(diào)用來(lái)實(shí)現(xiàn)服務(wù)端渲染
class SSR {
 //koa 路由里會(huì)調(diào)用這個(gè)方法
 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>
  );
  //獲取服務(wù)端已經(jīng)渲染好的組件數(shù)組
  let bundles = getBundles(stats, modules);
  return {
   html,
   scripts: this.generateBundleScripts(bundles),
  };
 }
 //把SSR過(guò)的組件都轉(zhuǎn)成script標(biāo)簽扔到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;

當(dāng)編譯這個(gè)文件的時(shí)候,在webpack配置里使用target: "node" externals,并且在你的打包前端app的webpack配置中,需要加入react-loadable的插件,app的打包需要在ssr打包之前運(yùn)行,不然拿不到react-loadable需要的各組件信息,先來(lái)看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
   }]
  ]
}

上面的配置會(huì)讓react-loadable知道哪些組件最終在服務(wù)端被渲染了,然后直接插入到html script標(biāo)簽中,并在前端初始化時(shí)把SSR過(guò)的組件考慮在內(nèi),避免重復(fù)加載,下面是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里的庫(kù)都打包進(jìn)去,此ssr js會(huì)直接運(yùn)行在node端,
 //所以不需要打包進(jìn)最終的文件中,運(yùn)行時(shí)會(huì)自動(dòng)從node_modules里加載
 externals: [nodeExternals()],
 //...
}

然后在koa app.js, require它,并且調(diào)用SSR的方法:

//...koa app.js
//build出來(lái)的ssr.js
const SSR = require('./build/node/ssr');
//preload all components on server side, 服務(wù)端沒(méi)有動(dòng)態(tài)加載各個(gè)組件,提前先加載好
SSR.preloadAll();

//實(shí)例化一個(gè)SSR對(duì)象
const s = new SSR();

router.get('*', async (ctx) => {
 //根據(jù)路由,渲染不同的頁(yè)面組件
 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;
});
//...

以上是個(gè)簡(jiǎn)單的實(shí)現(xiàn)React SSR到koa web server, 為了使react-loadable知道哪些組件在服務(wù)端渲染了,rendered里面的scripts數(shù)組里面包含了SSR過(guò)的組件組成的各個(gè)script標(biāo)簽,里面調(diào)用了SSR#generateBundleScripts()方法,在插入時(shí)需要確保這些script標(biāo)簽在runtime.js之后((通過(guò) CommonsChunkPlugin 來(lái)抽出來(lái))),并且在app bundle之前(也就是初始化的時(shí)候應(yīng)該已經(jīng)知道之前的哪些組件已經(jīng)渲染過(guò)了)。更多react-loadable服務(wù)端支持,參考這里.

上面我們還把react-router的路由都單獨(dú)抽出去了,使得它可以運(yùn)行在瀏覽器跟服務(wù)端,以下是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>
 )
}

服務(wù)端渲染的初始狀態(tài)

目前為止,我們已經(jīng)創(chuàng)建了一個(gè)React SPA,并且能在瀏覽器端跟服務(wù)端共同運(yùn)行🍺,社區(qū)稱之為universal app 或者 isomophic app。但是我們現(xiàn)在的app還有一個(gè)遺留問(wèn)題,一般來(lái)說(shuō)我們app的數(shù)據(jù)或者狀態(tài)都需要通過(guò)遠(yuǎn)端的api來(lái)異步獲取,拿到數(shù)據(jù)后我們才能開(kāi)始渲染組件,服務(wù)端SSR也是一樣,我們要?jiǎng)討B(tài)的獲取初始數(shù)據(jù),然后才能扔給React去做SSR,然后在瀏覽器端我們還要初始化就能同步獲取這些SSR時(shí)的初始化數(shù)據(jù),避免瀏覽器端初始化時(shí)又重新獲取了一遍。

下面我們簡(jiǎn)單從github獲取一些項(xiàng)目的信息作為頁(yè)面初始化的數(shù)據(jù), 在koa的app.js中:

//...
const fetch = require('isomorphic-fetch');

router.get('*', async (ctx) => {
 //fetch branch info from github
 const api = 'https://api.github.com/repos/jasonboy/wechat-jssdk/branches';
 const data = await fetch(api).then(res => res.json());
 
 //傳入初始化數(shù)據(jù)
 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入口中統(tǒng)一判斷,然后通過(guò)props傳到子組件中)是否存在window.__INITIAL_DATA__,有的話直接用來(lái)當(dāng)做初始數(shù)據(jù),沒(méi)有的話我們?cè)?code>componentDidMount生命周期函數(shù)中再去來(lái)數(shù)據(jù):

export default class Hello extends React.Component {
 constructor(props) {
  super(props);

  this.state = {
   //這里直接判斷window,如果是父組件傳入的話,通過(guò)props判斷
   github: window.__INITIAL_DATA__ || [],
  };
 }
 
 componentDidMount() {
  //判斷沒(méi)有數(shù)據(jù)的話,再去請(qǐng)求數(shù)據(jù)
  //請(qǐng)求數(shù)據(jù)的方法也可以抽出去,以讓瀏覽器及服務(wù)端能統(tǒng)一調(diào)用,避免重復(fù)寫
  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>
  );
 }
}

好了,現(xiàn)在如果頁(yè)面被服務(wù)端渲染過(guò)的話,瀏覽器會(huì)拿到所有渲染過(guò)的html, 包括初始化數(shù)據(jù),然后通過(guò)這些SSR的內(nèi)容配合加載的js,再組成一個(gè)完整的SPA,就像一個(gè)普通的SPA一樣,但是我們得到了更好的性能,更好的SEO😎。

React-v16 更新

在React的最新版v16中,SSR的API做了很多的優(yōu)化,并且提供了新的基于流的API來(lái)更好的提升性能,通過(guò)streaming api, 服務(wù)端可以邊渲染邊把前面渲染好的html發(fā)到瀏覽器,瀏覽器端也可以提前開(kāi)始渲染頁(yè)面而不是等服務(wù)端所有組件都渲染完成后才能開(kāi)始瀏覽器端的初始化,提升了性能也降低了服務(wù)端資源的消耗。還有一個(gè)在瀏覽器端需要注意的是需要使用ReactDOM.hydrate()來(lái)代替之前的ReactDOM.render(),更多的更新參考medium文章whats-new-with-server-side-rendering-in-react-16.

💖要查看完整的demo, 參考koa-web-kit, koa-web-kit是一個(gè)現(xiàn)代化的基于React/Koa的全棧開(kāi)發(fā)框架,包括React SSR支持,可以直接用來(lái)測(cè)試服務(wù)端渲染的功能😀

結(jié)論

好了,以上就是React-SSR + Koa的簡(jiǎn)單實(shí)踐,通過(guò)SSR,我們既提升了性能,又很好的滿足了SEO的要求,Best of the Both Worlds🍺。

English Version

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。

相關(guān)文章

  • React?Hooks的useState、useRef使用小結(jié)

    React?Hooks的useState、useRef使用小結(jié)

    React Hooks 是 React 16.8 版本引入的新特性,useState和useRef是兩個(gè)常用的Hooks,本文主要介紹了React?Hooks的useState、useRef使用,感興趣的可以了解一下
    2024-01-01
  • ForwardRef?useImperativeHandle方法demo

    ForwardRef?useImperativeHandle方法demo

    這篇文章主要為大家介紹了ForwardRef?useImperativeHandle方法demo,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-03-03
  • ReactNative點(diǎn)擊事件.bind(this)操作分析

    ReactNative點(diǎn)擊事件.bind(this)操作分析

    這篇文章主要為大家介紹了ReactNative點(diǎn)擊事件.bind(this)操作分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-11-11
  • React?Context詳解使用方法

    React?Context詳解使用方法

    Context提供了一個(gè)無(wú)需為每層組件手動(dòng)添加props,就能在組件樹(shù)間進(jìn)行數(shù)據(jù)傳遞的方法。在一個(gè)典型的?React?應(yīng)用中,數(shù)據(jù)是通過(guò)props屬性自上而下(由父及子)進(jìn)行傳遞的,但這種做法對(duì)于某些類型的屬性而言是極其繁瑣的
    2022-12-12
  • React渲染的優(yōu)化方案

    React渲染的優(yōu)化方案

    react的渲染機(jī)制是非常獨(dú)特的,有別于 Vue 框架的渲染次數(shù)的優(yōu)化計(jì)算,React 很久以來(lái)就有PureComponent、shouldUpdate,本文小編給大家介紹了React渲染的優(yōu)化方案,需要的朋友可以參考下
    2024-08-08
  • next-redux-wrapper使用細(xì)節(jié)及源碼分析

    next-redux-wrapper使用細(xì)節(jié)及源碼分析

    這篇文章主要為大家介紹了next-redux-wrapper使用細(xì)節(jié)及源碼分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-02-02
  • react獲取input輸入框的值的方法示例

    react獲取input輸入框的值的方法示例

    這篇文章主要介紹了react獲取input輸入框的值的方法示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2021-04-04
  • React onBlur回調(diào)中使用document.activeElement返回body完美解決方案

    React onBlur回調(diào)中使用document.activeElement返回body完美解決方案

    這篇文章主要介紹了React onBlur回調(diào)中使用document.activeElement返回body完美解決方案,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2023-04-04
  • 淺談python的函數(shù)知識(shí)

    淺談python的函數(shù)知識(shí)

    這篇文章主要為大家介紹了python的函數(shù),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助
    2021-11-11
  • 解決React報(bào)錯(cuò)Invalid hook call

    解決React報(bào)錯(cuò)Invalid hook call

    這篇文章主要為大家介紹了React報(bào)錯(cuò)Invalid hook call解決方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-12-12

最新評(píng)論