亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 開發 > JS > 正文

react koa rematch 如何打造一套服務端渲染架子

2024-05-06 16:52:49
字體:
來源:轉載
供稿:網友

前言

本次講述的內容主要是 react 與 koa 搭建的一套 ssr 框架,是在別人造的輪子上再添加了一些自己的想法和完善一下自己的功能。

本次用到的技術為: react | rematch | react-router | koa

react服務端渲染優勢

SPA(single page application)單頁應用雖然在交互體驗上比傳統多頁更友好,但它也有一個天生的缺陷,就是對搜索引擎不友好,不利于爬蟲爬取數據(雖然聽說chrome能夠異步抓取spa頁面數據了);

SSR與傳統 SPA(Single-Page Application - 單頁應用程序)相比,服務器端渲染(SSR)的優勢主要在于:更好的 SEO 和首屏加載效果。

在 SPA 初始化的時候內容是一個空的 div,必須等待 js 下載完才開始渲染頁面,但 SSR 就可以做到直接渲染html結構,極大地優化了首屏加載時間,但上帝是公平的,這種做法也增加了我們極大的開發成本,所以大家必須綜合首屏時間對應用程序的重要程度來進行開發,或許還好更好地代替品(骨架屏)。

react服務端渲染流程

組件渲染

首先肯定是根組件的render,而這一部分和SPA有一些小不同。

使用 ReactDOM.render() 來混合服務端渲染的容器已經被棄用,并且會在React 17 中刪除。使用hydrate() 來代替。

hydrate與 render 相同,但用于混合容器,該容器的HTML內容是由 ReactDOMServer 渲染的。 React 將嘗試將事件監聽器附加到現有的標記。

hydrate 描述的是 ReactDOM 復用 ReactDOMServer 服務端渲染的內容時盡可能保留結構,并補充事件綁定等 Client 特有內容的過程。

import React from 'react';import ReactDOM from 'react-dom';ReactDOM.hydrate(<App />, document.getElementById('app'));

在服務端中,我們可以通過 renderToString 來獲取渲染的內容來替換 html 模版中的東西。

const jsx =   <StaticRouter location={url} context={routerContext}>    <AppRoutes context={defaultContext} initialData={data} />  </StaticRouter>  const html = ReactDOMServer.renderToString(jsx);let ret = `  <!DOCTYPE html>    <html lang="en">    <head>     <meta charset="UTF-8">     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no">    </head>    <body>     <div id="app">${html}</div>    </body>  </html>`;return ret;

服務端返回替換后的 html 就完成了本次組件服務端渲染。

路由同步渲染

在項目中避免不了使用路由,而在SSR中,我們必須做到路由同步渲染。

首先我們可以把路由拆分成一個組件,服務端入口和客戶端都可以分別引用。

function AppRoutes({ context, initialData }: any) { return (  <Switch>   {    routes.map((d: any) => (     <Route<InitRoute>      key={d.path}      exact={d.exact}      path={d.path}      init={d.init || ''}      component={d.component}     />    ))   }   <Route path='/' component={Home} />  </Switch> );}

(routes.js)

export const routes = [ {  path: '/Home',  component: Home,  init: Home.init,  exact: true, }, {  path: '/Hello',  component: Hello,  init: Hello.init,  exact: true, }];

這樣我們的路由基本定義完了,然后客戶端引用還是老規矩,和SPA沒什么區別

import { BrowserRouter as Router } from 'react-router-dom';import AppRoutes from './AppRoutes';class App extends Component<any, Readonly<State>> {... render() {  return (  <Router>   <AppRoutes/>  </Router>  ); }}

在服務端中,需要使用將BrowserRouter 替換為 StaticRouter 區別在于,BrowserRouter 會通過HTML5 提供的 history API來保持頁面與URL的同步,而StaticRouter 則不會改變URL,當一個 匹配時,它將把 context 對象傳遞給呈現為 staticContext 的組件。

const jsx =   <StaticRouter location={url}>    <AppRoutes />  </StaticRouter>  const html = ReactDOMServer.renderToString(jsx);

至此,路由的同步已經完成了。

redux同構

在寫這個之前必須先了解什么是注水和脫水,所謂脫水,就是服務器在構建 HTML 之前處理一些預請求,并且把數據注入html中返回給瀏覽器。而注水就是瀏覽器把這些數據當初始數據來初始化組件,以完成服務端與瀏覽器端數據的統一。

組件配置

在組件內部定義一個靜態方法

class Home extends React.Component {... public static init(store:any) {  return store.dispatch.Home.incrementAsync(5); } componentDidMount() {  const { incrementAsync }:any = this.props;  incrementAsync(5); } render() { ... }}const mapStateToProps = (state:any) => { return {  count: state.Home.count };};const mapDispatchToProps = (dispatch:any) => ({ incrementAsync: dispatch.Home.incrementAsync});export default connect( mapStateToProps, mapDispatchToProps)(Home);

由于我這邊使用的是rematch,所以我們的方法都寫在model中。

const Home: ModelConfig= { state: {  count: 1 },  reducers: {  increment(state, payload) {   return {    count: payload   };  } }, effects: {  async incrementAsync(payload, rootState) {   await new Promise((resolve) => setTimeout(resolve, 1000));   this.increment(payload);  } }};export default Home;

然后通過根 store 中進行 init。

import { init } from '@rematch/core';import models from './models';const store = init({ models: {...models}});export default store;

然后可以綁定在我們 redux 的 Provider 中。

<Provider store = {store}>  <Router>   <AppRoutes    context={context}    initialData={this.initialData}   />  </Router></Provider>

路由方面我們需要把組件的 init 方法綁定在路由上方便服務端請求數據時使用。

<Switch>   {    routes.map((d: any) => (     <Route<InitRoute>      key={d.path}      exact={d.exact}      path={d.path}      init={d.init || ''}      component={d.component}     />    ))   }   <Route path='/' component={Home} />  </Switch>

以上就是客戶端需要進行的操作了,因為 SSR 中我們服務端也需要進行數據的操作,所以為了解耦,我們就新建另一個 ServiceStore 來提供服務端使用。

在服務端構建 Html 前,我們必須先執行完當前組件的 init 方法。

import { matchRoutes } from 'react-router-config';// 用matchRoutes方法獲取匹配到的路由對應的組件數組const matchedRoutes = matchRoutes(routes, url);const promises = [];for (const item of matchedRoutes) { if (item.route.init) {  const promise = new Promise((resolve, reject) => {   item.route.init(serverStore).then(resolve).catch(resolve);  });  promises.push(promise); }}return Promise.all(promises);

注意我們新建一個 Promise 的數組來放置 init 方法,因為一個頁面可能是由多個組件組成的,我們必須等待所有的 init 執行完畢后才執行相應的 html 構建。

現在可以得到的數據掛在 window 下,等待客戶端的讀取了。

let ret = `   <!DOCTYPE html>    <html lang="en">    <head>     <meta charset="UTF-8">     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no">    </head>    <body>     <div id="app">${html}</div>     <script type="text/javascript">window.__INITIAL_STORE__ = ${JSON.stringify(      extra.initialStore || {}     )}</script>    </body>   </html>  `;

然后在我們的客戶端中讀取剛剛的 initialStore 數據

....const defaultStore = window.__INITIAL_STORE__ || {};const store = init({ models, redux: {  initialState: defaultStore }});export default store;

至此,redux的同構基本完成了,因為邊幅的限定,我就沒有貼太多代碼,大家可以到文章底部的點擊我的倉庫看看具體代碼哈,然后我再說說幾個 redux 同構中比較坑的地方。

1.使用不了 @loadable/component 異步組件加載,因為不能獲取組件內部方法。 解決的辦法就是在預請求我們不放在組件中,直接拆分出來寫在一個文件中統一管理,但我嫌這樣不好管理就放棄了異步加載組件了。

2.在客戶端渲染的時候如果數據一閃而過,那就是初始化數據并沒有成功,當時這里卡了我好久喔。

css樣式直出

首先,服務端渲染的時候,解析 css 文件,不能使用 style-loader 了,要使用 isomorphic-style-loader 。使用 style-loader 的時候會有一閃而過的現象,是因為瀏覽器是需要加載完 css 才能把樣式加上。為了解決這樣的問題,我們可以通過isomorphic-style-loader 在組件加載的時候把 css 放置在全局的 context 里面,然后在服務端渲染時候提取出來,插入到返回的HTML中的 style 標簽。

組件的改造

import withStyles from 'isomorphic-style-loader/withStyles';@withStyles(style)class Home extends React.Component {... render() {  const {count}:any = this.props;  return (  ...  ); }}const mapStateToProps = (state:any) => { return {  count: state.Home.count };};const mapDispatchToProps = (dispatch:any) => ({ incrementAsync: dispatch.Home.incrementAsync});export default connect( mapStateToProps, mapDispatchToProps)(Home);

withStyle 是一個柯里化函數,返回的是一個新的組件,并不影響 connect 函數,當然你也可以像 connect 一樣的寫法。withStyle 主要是為了把 style 插入到全局的 context 里面。

根組件的修改

import StyleContext from 'isomorphic-style-loader/StyleContext';

const insertCss = (...styles:any) => { const removeCss = styles.map((style:any) => style._insertCss()); return () => removeCss.forEach((dispose:any) => dispose());};ReactDOM.hydrate(  <StyleContext.Provider value={{ insertCss }}>    <AppError>     <Component />    </AppError>  </StyleContext.Provider>,  elRoot);

這一部分主要是引入了 StyleContext 初始化根部的context,并且定義好一個 insertCss 方法,在組件 withStyle 中觸發。

部分 isomorphic-style-loader 源碼

...function WithStyles(props, context) {  var _this;  _this = _React$PureComponent.call(this, props, context) || this;  _this.removeCss = context.insertCss.apply(context, styles);  return _this; } var _proto = WithStyles.prototype; _proto.componentWillUnmount = function componentWillUnmount() {  if (this.removeCss) {   setTimeout(this.removeCss, 0);  } }; _proto.render = function render() {  return React.createElement(ComposedComponent, this.props); }; ...

可以看到 context 中的 insert 方法就是根組件中的 定義好的 insert 方法,并且在 componentWillUnmount 這個銷毀的生命周期中把之前 style 清除掉。而 insert 方法主要是為了給當前的 style 定義好id并且嵌入,這里就不展開說明了,有興趣的可以看一下源碼。

服務端中獲取定義好的css

const css = new Set(); // CSS for all rendered React componentsconst insertCss = (...styles :any) => { return styles.forEach((style:any) => css.add(style._getCss()));};const extractor = new ChunkExtractor({ statsFile: this.statsFile });const jsx = extractor.collectChunks( <StyleContext.Provider value={{ insertCss }}>  <Provider store={serverStore}>    <StaticRouter location={url} context={routerContext}>     <AppRoutes context={defaultContext} initialData={data} />    </StaticRouter>  </Provider> </StyleContext.Provider>);const html = ReactDOMServer.renderToString(jsx);const cssString = Array.from(css).join('');...

其中 cssString 就是我們最后獲取到的 css 內容,我們可以像 html 替換一樣把 css 嵌入到 html 中。

let ret = `   <!DOCTYPE html>    <html lang="en">    <head>     ...     <style>${extra.cssString}</style>    </head>    <body>     <div id="app">${html}</div>     ...    </body>   </html>  `;

那這樣就大功告成啦?。。?!

我來說一下在做這個的時候遇到的坑

1.不能使用分離 css 的插件 mini-css-extract-plugin ,因為分離 css 和把 css 放置到 style 中會有沖突,引入github大神的一句話

With isomorphic-style-loader the idea was to always include css into js files but render into dom only critical css and also make this solution universal (works the same on client and server side). If you want to extract css into separate files you probably need to find another way how to generate critical css rather than use isomorphic-style-loader.

2.很多文章說到在 service 端的打包中不需要打包 css,那是因為他們使用的是style-loader 的情況,我們如果使用 isomorphic-style-loader, 我們也需要把 css 打包一下,因為我們在服務端中畢竟要觸發 withStyle。

總結

因為代碼太多了,所以只是展示了整個 SSR 流程的思想,詳細代碼可以查看。還有希望大牛們指導一下我的錯誤,萬分感謝??!

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持VeVb武林網。


注:相關教程知識閱讀請移步到JavaScript/Ajax教程頻道。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
亚洲人av在线影院| 国产专区欧美专区| 日韩精品在线免费| 国产成人精品在线播放| 成人做爰www免费看视频网站| 美女黄色丝袜一区| 日韩一二三在线视频播| 亚洲综合色av| 成人h猎奇视频网站| 日韩中文字幕精品视频| 国产成人中文字幕| 青青草原成人在线视频| 久久中文字幕一区| 97免费中文视频在线观看| 欧美资源在线观看| 成人妇女淫片aaaa视频| 性夜试看影院91社区| 亚洲精选在线观看| 久久久久久久一区二区三区| 日韩av一卡二卡| 欧美多人乱p欧美4p久久| 一级做a爰片久久毛片美女图片| 久久99久久久久久久噜噜| 亚洲人成在线免费观看| 欧美激情精品久久久久久黑人| 亚洲成人1234| 日本欧美爱爱爱| 国产v综合ⅴ日韩v欧美大片| 日韩不卡在线观看| 国产精品久久久久久婷婷天堂| 亚洲美女在线观看| 欧美美最猛性xxxxxx| 国产精品啪视频| 成人在线一区二区| 久久精品国产精品亚洲| 亚洲free嫩bbb| 日本免费在线精品| 日韩精品免费一线在线观看| 大荫蒂欧美视频另类xxxx| 欧美一级淫片丝袜脚交| 中文字幕亚洲色图| 日韩福利在线播放| 国产精品老牛影院在线观看| 欧美在线视频导航| 精品亚洲一区二区三区在线播放| 色综合久久精品亚洲国产| 日本亚洲欧洲色α| 欧美精品免费播放| xxxxxxxxx欧美| 中国日韩欧美久久久久久久久| 精品在线欧美视频| 亚洲欧美日韩中文视频| 欧亚精品中文字幕| 亚洲成人av中文字幕| 色妞色视频一区二区三区四区| 欧美性开放视频| 欧美激情2020午夜免费观看| 黑人精品xxx一区一二区| 欧美电影在线观看高清| 欧美日韩国产区| 久久精品成人欧美大片古装| 亚洲午夜精品久久久久久久久久久久| 欧美俄罗斯乱妇| 国产午夜精品麻豆| 欧美日韩国产成人| 在线播放亚洲激情| 精品亚洲男同gayvideo网站| 国产精品中文久久久久久久| 国产精品久久久久99| 久久精品最新地址| 亚洲一区制服诱惑| 亚洲第一区中文99精品| 日韩视频免费在线| 欧美午夜精品久久久久久人妖| 日韩欧美中文字幕在线观看| 国产综合视频在线观看| 欧美疯狂做受xxxx高潮| 亚洲精品国产精品国自产观看浪潮| 日韩大片在线观看视频| 日韩精品999| 国产精品久久综合av爱欲tv| 久久久免费高清电视剧观看| 久久精品99久久久久久久久| 精品久久久久久久久久| 亚洲人成人99网站| 97不卡在线视频| 日韩免费黄色av| 久久久亚洲天堂| 亚洲精品99久久久久中文字幕| 日韩国产欧美精品一区二区三区| 91国自产精品中文字幕亚洲| 欧美在线xxx| 不卡毛片在线看| 亚洲国产另类 国产精品国产免费| 亚洲欧美在线免费观看| 亚洲精品网站在线播放gif| 亚洲男人av电影| 一本色道久久综合亚洲精品小说| 在线播放国产精品| 久久久久久成人精品| 成人午夜两性视频| 亚洲热线99精品视频| 欧美一级成年大片在线观看| 成人性生交大片免费看视频直播| 中文字幕日韩精品在线观看| 日韩大胆人体377p| 国产91精品不卡视频| 欧美又大粗又爽又黄大片视频| 久久99久国产精品黄毛片入口| 中文在线资源观看视频网站免费不卡| 国产极品精品在线观看| 亚洲美女视频网站| 69av在线播放| 久久久国产影院| 国内精品一区二区三区四区| 麻豆一区二区在线观看| 精品国产一区久久久| 91天堂在线观看| 亚洲伊人一本大道中文字幕| 国产综合香蕉五月婷在线| 亚洲小视频在线观看| 茄子视频成人在线| 日韩亚洲精品电影| 亚洲福利在线看| 中文字幕久热精品视频在线| 国产精品一区二区三区在线播放| 国产日韩欧美中文| 国产精品视频yy9099| 国产精品嫩草视频| 日本一区二区在线免费播放| 亚洲成在人线av| 亚洲精品美女网站| 亚洲影院色无极综合| 精品久久久久久亚洲精品| 91久久久久久久久久| 国产不卡av在线| 自拍偷拍亚洲精品| 福利精品视频在线| 亚洲深夜福利视频| 色与欲影视天天看综合网| 欧美xxxx18性欧美| 亚洲成人黄色在线观看| 日韩国产激情在线| 精品国产一区二区三区在线观看| 亚洲xxxxx性| 91精品国产91| 日本a级片电影一区二区| 少妇av一区二区三区| 欧美日韩性视频在线| 久久99久久99精品免观看粉嫩| 亚洲精品suv精品一区二区| 国产精品成人久久久久| 国产精品高精视频免费| 亚洲成人免费网站| 日韩中文字幕视频在线| 久久久久久久影视| 韩国精品久久久999| 在线观看精品自拍私拍| 日本久久久a级免费| 亚洲精品白浆高清久久久久久| 国产成人aa精品一区在线播放| 日韩成人网免费视频| 久久久久久久久久婷婷| 成人a在线视频|