日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

服务端渲染与 Universal React App

發(fā)布時間:2023/12/19 编程问答 46 豆豆
生活随笔 收集整理的這篇文章主要介紹了 服务端渲染与 Universal React App 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

隨著 Webpack 等前端構(gòu)建工具的普及,客戶端渲染因為其構(gòu)建方便,部署簡單等方面的優(yōu)勢,逐漸成為了現(xiàn)代網(wǎng)站的主流渲染模式。而在剛剛發(fā)布的 React v16.0 中,改進后更為優(yōu)秀的服務端渲染性能作為六大更新點之一,被 React 官方重點提及。為此筆者還專門做了一個小調(diào)查,分別詢問了二十位國內(nèi)外(國內(nèi)國外各十位)前端開發(fā)者,希望能夠了解一下服務端渲染在使用 React 公司中所占的比例。

出人意料的是,十位國內(nèi)的前端開發(fā)者中在生產(chǎn)環(huán)境使用服務端渲染的只有三位。而在國外的十位前端開發(fā)者中,使用服務端渲染的達到了驚人的八位。

這讓人不禁開始思考,同是 React 的深度使用者,為什么國內(nèi)外前端開發(fā)者在服務端渲染這個 React 核心功能的使用率上有著如此巨大的差別?在經(jīng)過又一番刨根問底地詢問后,真正的答案逐漸浮出水面,那就是可靠的 SEO(reliable SEO)。

相比較而言,國外公司對于 SEO 的重視程度要遠高于國內(nèi)公司,在這方面積累的經(jīng)驗也要遠多于國內(nèi)公司,前端頁面上需要服務端塞入的內(nèi)容也絕不僅僅是用戶所看到的那些而已。所以對于國外的前端開發(fā)者來說,除去公司內(nèi)部系統(tǒng)不談,所有的客戶端應用都需要做大量的 SEO 工作,服務端渲染也就順理成章地成為了一個必選項。這也從一個側(cè)面證明了國內(nèi)外互聯(lián)網(wǎng)環(huán)境的一個巨大差異,即雖然國際上也有諸如 Google,Facebook,Amazon 這樣的巨頭公司,但放眼整個互聯(lián)網(wǎng),這些巨頭公司所產(chǎn)生的黑洞效應并沒有國內(nèi) BAT 三家那樣如此得明顯,中小型公司依然有其生存的空間,搜索引擎所帶來的自然流量就足夠中小型公司可以活得很好。在這樣的前提下,SEO 的重要性自然也就不言而喻了。

除去 SEO,服務端渲染對于前端應用的首屏加載速度也有著質(zhì)的提升。特別是在 React v16.0 發(fā)布之后,新版 React 的服務端渲染性能相較于老版提升了三倍之多,這讓已經(jīng)在生產(chǎn)環(huán)境中使用服務端渲染的公司“免費”獲得了一次網(wǎng)站加載速度提升的機會,同時也吸引了許多還未在生產(chǎn)環(huán)境中使用服務端渲染的開發(fā)者。

客戶端渲染 vs. 服務端渲染 vs. 同構(gòu)

在深入服務端渲染的細節(jié)之前,讓我們先明確幾個概念的具體含義。

  • 客戶端渲染:頁面在 JavaScript,CSS 等資源文件加載完畢后開始渲染,路由為客戶端路由,也就是我們經(jīng)常談到的 SPA(Single Page Application)。
  • 服務端渲染:頁面由服務端直接返回給瀏覽器,路由為服務端路由,URL 的變更會刷新頁面,原理與 ASP,PHP 等傳統(tǒng)后端框架類似。
  • 同構(gòu):英文表述為 Isomorphic 或 Universal,即編寫的 JavaScript 代碼可同時運行在瀏覽器及 Node.js 兩套環(huán)境中,用服務端渲染來提升首屏的加載速度,首屏之后的路由由客戶端控制,即在用戶到達首屏后,整個應用仍是一個 SPA。

在明確了這三種渲染方案的具體含義后,我們可以發(fā)現(xiàn),不論是客戶端渲染還是服務端渲染,都有著其明顯的缺陷,而同構(gòu)顯然是結(jié)合了二者優(yōu)點之后的一種更好的解決方案。

但想在客戶端寫出一套完全符合同構(gòu)要求的 React 代碼并不是一件容易的事,與此同時還需要額外部署一套穩(wěn)定的服務端渲染服務,這二者相加起來的開發(fā)或遷移成本都足以擊潰許多想要嘗試服務端渲染的 React 開發(fā)者的信心。

那么今天就讓我們來一起總結(jié)下,符合同構(gòu)要求的 React 代碼都有哪些需要注意的地方,以及如何搭建起一個基礎的服務端渲染服務。

總體架構(gòu)

為了方便各位理解同構(gòu)的具體實現(xiàn)過程,筆者基于 react,react-router,redux 以及 webpack3 實現(xiàn)了一個簡單的腳手架項目,支持客戶端渲染和服務端渲染兩種開發(fā)方式,供各位參考。

  • 服務端預先獲取編譯好的客戶端代碼及其他資源。
  • 服務端接收到用戶的 HTTP 請求后,觸發(fā)服務端的路由分發(fā),將當前請求送至服務端渲染模塊處理。
  • 服務端渲染模塊根據(jù)當前請求的 URL 初始化 memory history 及 redux store。
  • 根據(jù)路由獲取渲染當前頁面所需要的異步請求(thunk)并獲取數(shù)據(jù)。
  • 調(diào)用 renderToString 方法渲染 HTML 內(nèi)容并將初始化完畢的 redux store 塞入 HTML 中,供客戶端渲染時使用。
  • 客戶端收到服務端返回的已渲染完畢的 HTML 內(nèi)容并開始同步加載客戶端 JavaScript,CSS,圖片等其他資源。
  • 之后的流程與客戶端渲染完全相同,客戶端初始化 redux store,路由找到當前頁面的組件,觸發(fā)組件的生命周期函數(shù),再次獲取數(shù)據(jù)。唯一不同的是 redux store 的初始狀態(tài)將由服務端在 HTML 中塞入的數(shù)據(jù)提供,以保證客戶端渲染時可以得到與服務端渲染相同的結(jié)果。受益于 Virtual DOM 的 diff 算法,這里并不會觸發(fā)一次冗余的客戶端渲染。
  • 在了解了同構(gòu)的大致思路后,接下來再讓我們對同構(gòu)中需要注意的點逐一進行分析,與各位一起探討同構(gòu)的最佳實踐。

    客戶端與服務端構(gòu)建過程不同

    因為運行環(huán)境與渲染目的的不同,共用一套代碼的客戶端與服務端在構(gòu)建方面有著許多的不同之處。

    入口(entry)不同

    客戶端的入口為 ReactDOM.render 所在的文件,即將根組件掛載在 DOM 節(jié)點上。而服務端因為沒有 DOM 的存在,只需要拿到需要渲染的 react 組件即可。為此我們需要在客戶端抽離出獨立的 createApp及 createStore 的方法。

    // createApp.jsimport React from 'react'; import { Provider } from 'react-redux'; import Router from './router';const createApp = (store, history) => (<Provider store={store}><Router history={history} /></Provider> );export default createApp;復制代碼// createStore.jsimport { createStore, combineReducers, applyMiddleware, compose } from 'redux'; import { routerReducer, routerMiddleware } from 'react-router-redux'; import reduxThunk from 'redux-thunk'; import reducers from './reducers'; import routes from './router/routes';function createAppStore(history, preloadedState = {}) {// enhancerslet composeEnhancers = compose;if (typeof window !== 'undefined') {// eslint-disable-next-line no-underscore-danglecomposeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;}// middlewaresconst routeMiddleware = routerMiddleware(history);const middlewares = [routeMiddleware,reduxThunk,];const store = createStore(combineReducers({...reducers,router: routerReducer,}),preloadedState,composeEnhancers(applyMiddleware(...middlewares)),);return {store,history,routes,}; }export default createAppStore;復制代碼

    并在 app 文件夾中將這兩個方法一起輸出出去:

    import createApp from './createApp'; import createStore from './createStore';export default {createApp,createStore, };復制代碼

    出口(output)不同

    為了最大程度地提升用戶體驗,在客戶端渲染時我們將根據(jù)路由對代碼進行拆分,但在服務端渲染時,確定某段代碼與當前路由之間的對應關(guān)系是一件非常繁瑣的事情,所以我們選擇將所有客戶端代碼打包成一個完整的 js 文件供服務端使用。

    理想的打包結(jié)果如下:

    ├── build │ └── v1.0.0 │ ├── assets │ │ ├── 0.0.257727f5.js │ │ ├── 0.0.257727f5.js.map │ │ ├── 1.1.c3d038b9.js │ │ ├── 1.1.c3d038b9.js.map │ │ ├── 2.2.b11f6092.js │ │ ├── 2.2.b11f6092.js.map │ │ ├── 3.3.04ff628a.js │ │ ├── 3.3.04ff628a.js.map │ │ ├── client.fe149af4.js │ │ ├── client.fe149af4.js.map │ │ ├── css │ │ │ ├── style.db658e13004910514f8f.css │ │ │ └── style.db658e13004910514f8f.css.map │ │ ├── images │ │ │ └── 5d5d9eef.svg │ │ ├── vendor.db658e13.js │ │ └── vendor.db658e13.js.map │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ └── server (服務端需要的資源將被打包至這里) │ ├── assets │ │ ├── server.4b6bcd12.js │ │ └── server.4b6bcd12.js.map │ └── manifest.json 復制代碼

    使用的插件(plugin)不同

    與客戶端不同,除去 JavaScript 之外,服務端并不需要任何其他的資源,如 HTML 及 CSS 等,所以在構(gòu)建服務端 JavaScript 時,諸如 HtmlWebpackPlugin 等客戶端所特有的插件就可以省去了,具體細節(jié)各位可以參考項目中的 webpack.config.js。

    數(shù)據(jù)獲取方式不同

    異步數(shù)據(jù)獲取一直都是服務端渲染做得不夠優(yōu)雅的一個地方,其主要問題在于無法直接復用客戶端的數(shù)據(jù)獲取方法。如在 redux 的前提下,服務端沒有辦法像客戶端那樣直接在組件的componentDidMount 中調(diào)用 action 去獲取數(shù)據(jù)。

    為了解決這一問題,我們針對每一個 view 為其抽象出了一個 thunk 文件,并將其綁定在客戶端的路由文件中。這樣我們就可以在服務端通過 react-router-config 提供的 matchRoutes 方法找到當前頁面的 thunk,并在 renderToString 之前 dispatch 這些異步方法,將數(shù)據(jù)更新至 redux store 中,以保證 renderToString 的渲染結(jié)果是包含異步數(shù)據(jù)的。

    // thunk.js import homeAction from '../home/action'; import action from './action';const thunk = store => ([store.dispatch(homeAction.getMessage()),store.dispatch(action.getUser()), ]);export default thunk;// createAsyncThunk.js import get from 'lodash/get'; import isArrayLikeObject from 'lodash/isArrayLikeObject';function promisify(value) {if (typeof value.then === 'function') {return value;}if (isArrayLikeObject(value)) {return Promise.all(value);}return value; }function createAsyncThunk(thunk) {return store => (thunk().then(component => get(component, 'default', component)).then(component => component(store)).then(component => promisify(component))); }export default createAsyncThunk;// routes.js const routes = [{path: '/',exact: true,component: AsyncHome,thunk: createAsyncThunk(() => import('../../views/home/thunk')), }, {path: '/user',component: AsyncUser,thunk: createAsyncThunk(() => import('../../views/user/thunk')), }];復制代碼

    服務端核心的頁面渲染模塊:

    const ReactDOM = require('react-dom/server'); const { matchRoutes } = require('react-router-config'); const { Helmet } = require('react-helmet'); const serialize = require('serialize-javascript'); const createHistory = require('history/createMemoryHistory').default; const get = require('lodash/get'); const head = require('lodash/head'); const { getClientInstance } = require('../client');// Initializes the store with the starting url = require( request. function configureStore(req, client) {console.info('server path', req.originalUrl);const history = createHistory({ initialEntries: [req.originalUrl] });const preloadedState = {};return client.app.createStore(history, preloadedState); }// This essentially starts passing down the "context" // object to the Promise "then" chain. function setContextForThenable(context) {return () => context; }// Prepares the HTML string and the appropriate headers // and subequently string replaces them into their placeholders function renderToHtml(context) {const { client, store, history } = context;const appObject = client.app.createApp(store, history);const appString = ReactDOM.renderToString(appObject);const helmet = Helmet.renderStatic();const initialState = serialize(context.store.getState(), {isJSON: true});context.renderedHtml = client.html().replace(/<!--appContent-->/g, appString).replace(/<!--appState-->/g, `<script>window.__INITIAL_STATE__ = ${initialState}</script>`).replace(/<\/head>/g, [helmet.title.toString(),helmet.meta.toString(),helmet.link.toString(),'</head>',].join('\n')).replace(/<html>/g, `<html ${helmet.htmlAttributes.toString()}>`).replace(/<body>/g, `<body ${helmet.bodyAttributes.toString()}>`);return context; }// SSR Main method // Note: Each function in the promise chain beyond the thenable context // should return the context or modified context. function serverRender(req, res) {const client = getClientInstance(res.locals.clientFolders);const { store, history, routes } = configureStore(req, client);const branch = matchRoutes(routes, req.originalUrl);const thunk = get(head(branch), 'route.thunk', () => {});Promise.resolve(null).then(thunk(store)).then(setContextForThenable({ client, store, history })).then(renderToHtml).then((context) => {res.send(context.renderedHtml);return context;}).catch((err) => {console.error(`SSR error: ${err}`);}); }module.exports = serverRender;復制代碼

    在客戶端,我們可以直接在 componentDidMount 中調(diào)用這些 action:

    const mapDispatchToProps = {getUser: action.getUser,getMessage: homeAction.getMessage, };componentDidMount() {this.props.getMessage();this.props.getUser(); }復制代碼

    在分離了服務端與客戶端 dispatch 異步請求的方式后,我們還可以針對性地對服務端的 thunk 做進一步的優(yōu)化,即只請求首屏渲染需要的數(shù)據(jù),剩下的數(shù)據(jù)交給客戶端在 js 加載完畢后再請求。

    但這里又引出了另一個問題,比如在上面的例子中,getUser 和 getMessage 這兩個異步請求分別在服務端與客戶端各請求了一次,即我們在很短的時間內(nèi)重復請求了同一個接口兩次,這是可以避免的嗎?

    這樣的數(shù)據(jù)獲取方式在純服務端渲染時自然是冗余的,但在同構(gòu)的架構(gòu)下,其實是無法避免的。因為我們并不知道用戶在訪問客戶端的某個頁面時,是從服務端路由來的(即首屏),還是從客戶端路由(首屏之后的后續(xù)路由)來的。也就是說如果我們不在組件的 componentDidMount 中去獲取異步數(shù)據(jù)的話,一旦用戶到達了某個頁面,再點擊頁面中的某個元素跳轉(zhuǎn)至另一頁面時,是不會觸發(fā)服務端的數(shù)據(jù)獲取的,因為這時走的實際上是客戶端路由。

    服務端渲染還能做些什么

    除去 SEO 與首屏加速,在額外部署了一套服務端渲染服務后,我們當然希望它能為我們分擔更多的事情,那么究竟有哪些事情放在服務端去做是更為合適的呢?筆者總結(jié)了以下幾點。

    初始化應用狀態(tài)

    除去獲取當前頁面的數(shù)據(jù),在做了同構(gòu)之后,客戶端還可以將獲取應用全局狀態(tài)的一些請求也交由服務端去做,如獲取當前時區(qū),語言,設備信息,用戶等通用的全局數(shù)據(jù)。這樣客戶端在初始化 redux store 時就可以直接獲取到上述數(shù)據(jù),從而加快其他頁面的渲染速度。與此同時,在分離了這部分業(yè)務邏輯到服務端之后,客戶端的業(yè)務邏輯也會變得更加清晰。當然,如果你想做一個純粹的 Universal App,也可以把初始化應用狀態(tài)封裝成一個方法,讓服務端與客戶端都可以自由地去調(diào)用它。

    更早的路由處理

    相較于客戶端,服務端可以更早地對當前 URL 進行一些業(yè)務邏輯上的判斷。比如 404 時,服務端可以直接將另一個 error.html 的模板發(fā)送至客戶端,用戶也就可以在第一時間收到相應的反饋,而不需要等到所有 JavaScript 等客戶端資源加載完畢之后,才看到由客戶端渲染的 404 頁面。

    Node.js 中間層

    有了服務端渲染這一層后,服務端還可以幫助客戶端向 Cookie 中注入一些后端 API 中沒有的數(shù)據(jù),甚至做一些接口聚合,數(shù)據(jù)格式化的工作。這時,我們所寫的 Node.js 服務端就不再是一個單純的渲染服務了,而是進化為了一個 Node.js 中間層,可以幫助客戶端完成許多在客戶端做不到或很難做到的事情。

    要不要做同構(gòu)

    在分析了同構(gòu)的具體實現(xiàn)細節(jié)并了解了同構(gòu)的好處之后,我們也需要知道這一切的好處并不是沒有代價的,同構(gòu)或者說服務端渲染最大的瓶頸就是服務端的性能。

    在用戶規(guī)模大到一定程度之后,客戶端渲染本身就是一個完美的分布式系統(tǒng),我們可以充分地利用用戶的電腦去運行 JavaScript 中那些復雜的運算,而服務端渲染卻將這些工作全部攬了回來并加到了網(wǎng)站自己的服務器上。

    所以,考慮到投入產(chǎn)出比,同構(gòu)可能并不適用于前端需要大量計算(如包含大量圖表的頁面)且用戶量非常巨大的應用,卻非常適用于大部分的內(nèi)容展示型網(wǎng)站,比如知乎就是一個很好的例子。以知乎為例,服務端渲染與客戶端渲染的成本幾乎是相同的,重點都在于獲取用戶時間線上的數(shù)據(jù),這時多頁面的服務端渲染可以很好地加快首屏渲染的速度,又因為運行 renderToString 時的計算量并不大,即使用戶量很大,也仍然是一件值得去做的事情。

    小結(jié)

    結(jié)合之前文章中提到的前端數(shù)據(jù)層的概念,服務端渲染服務其實是一個很好的前端開發(fā)介入服務端開發(fā)的切入點,在完成了服務端渲染服務后,對數(shù)據(jù)接口做一些代理或整合也是非常值得去嘗試的工作。

    一個代碼庫之所以復雜,很多時候就是因為分層架構(gòu)沒有做好而導致其中某一個模塊過于臃腫,集中了大部分的業(yè)務復雜度,但其他模塊又根本幫不上忙。想要做好前端數(shù)據(jù)層的工作,只把眼光局限在客戶端是遠遠不夠的,將業(yè)務復雜度均分到客戶端及服務端,并讓兩方分別承擔各自適合的工作,可能會是一種更好的解法。


    總結(jié)

    以上是生活随笔為你收集整理的服务端渲染与 Universal React App的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。