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

首頁 > 開發 > JS > 正文

react同構實踐之實現自己的同構模板

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

一開始想學學服務端渲染,腦海中第一個浮現出來的就是next.js這種成熟的方案??戳艘粌商?,有趣,優雅,但是封裝好了,原理不甚清楚,也感覺無法靈活嵌合到老項目上去。于是看各種資料,想整理出同構的線索,一步一步地實現自己的同構模板。相關代碼可查看我的GitHub。感謝閱讀??!

TODO List

  • 數據:如何保持前后端應用狀態一致
  • 路由:路由在服務端和客戶端中的匹配方案
  • 代碼:同構,哪些地方可以共享,哪些地方需要差異化
  • 靜態資源:服務端如何引入css/圖片等
  • ssr直出資源:服務端在渲染路由頁面時如何匹配css/chunks資源
  • 打包方案:服務端和瀏覽器端如何寫各自的webpack配置文件
  • SEO: head頭處理方案

同構的基礎

正常的網頁運行,需要生成dom,在dom樹loaded之后由js綁定相關的dom事件,監聽頁面的交互。服務端并不具備dom的執行環境,因而所有的服務端渲染其實都是返回了一個填充了初始數據的靜態文本。在react中,除了常用的render這個用于生成dom的方法,還提供了renderToString,renderToStaticMarkup方法用來生成字符串,由于VitualDOM的存在,結合這些方法就可以像以前的字符串模板那樣生成普通的字符串,返回給客戶端接管,再接著進行事件相關的綁定。最新的React v16+使用hydrate和ssr配套,能讓客戶端把服務端的VitualDOM渲染出來后得以復用,客戶端加載js后不會重刷一邊,減小了開銷,也避免瀏覽器重刷dom時帶來的閃屏體驗。而react的組件,還是和往常寫spa一樣編寫,前后端共享。不同的只是入口的渲染方法換了名字,且客戶端會掛載dom而已。

// clinet.jsReactDom.hydrate(<App />, document.getElementById('app'))// server.jsconst html = ReactDom.renderToString(<App />)

同構后網站運行流程圖

盜用一張圖,來自阿里前端。乍一看,ssr與csr的區別就在于2 3 4 5,spa模式簡單粗暴地返回一個空白的html頁面,然后在11里才去加載數據進行頁面填充,在此之前,頁面都處于空白狀態。而ssr則會根據路由信息,提前獲取該路由頁面的初始數據,返回頁面時已經有了初步的內容,不至于空白,也便于搜索引擎收錄。

react,同構模板

路由匹配

瀏覽器端的路由匹配還是照著spa來做應該無需費心。略過了...

服務端的路由需要關注的,一個是后端服務的路由(如koa-router)匹配的問題,一個是匹配到react應用后react-router路由表的匹配問題。

服務端路由,可通過/react前綴來和api接口等其他區別開來,這種路由匹配方式甚至能讓服務端渲染能同時支持老項目諸如ejs等的模板渲染方式,在系統升級改造方面可實現漸進式地升級。

// app.js文件(后端入口)import reactController from './controllers/react-controller'// API路由app.use(apiController.routes())// ejs頁面路由app.use(ejsController.routes())// react頁面路由app.use(reactController.routes())// react-controller.js文件import Router from 'koa-router'const router = new Router({ prefix: '/react'})router.all('/', async (ctx, next) => { const html = await render(ctx) ctx.body = html})export default router

react-router專供了給ssr使用的StaticRouter接口,稱之為靜態的路由。誠然,服務端不像客戶端,對應于一次網絡請求,路由就是當前的請求url,是唯一的,不變的。在返回ssr直出的頁面后,頁面交互造成地址欄的變化,只要用的是react-router提供的方法,無論是hash方式,還是history方式,都屬于瀏覽器端react-router的工作了,于是完美繼承了spa的優勢。只有在輸入欄敲擊Enter,才會發起新一輪的后臺請求。

import { StaticRouter } from 'react-router-dom' const App = () => {  return (   <Provider store={store}>    <StaticRouter     location={ctx.url}     context={context}>          <Layout />    </StaticRouter>   </Provider>  ) }

應用狀態數據管理

以往的服務端渲染,需要在客戶端網頁下載后馬上能看到的數據就放在服務器提前準備好,可延遲展示,通過ajax請求的數據的交互邏輯放在頁面加載的js文件中去。

換成了react,其實套路也是一樣一樣的。但是區別在于:

傳統的字符串模板,組件模板是彼此分離的,可各自單獨引入數據,再拼裝起來形成一份html。而在react的ssr里,頁面只能通過defaultValue和defaultProps一次性render,無法rerender。

不能寫死defaultValude,所以只能使用props的數據方案。在執行renderToString之前,提前準備好整個應用狀態的所有數據。全局的數據管理方案可考慮redux和mobx等。

需要準備初始渲染數據,所以要精準獲取當前地址將要渲染哪些組件。react-router-config和react-router同源配套,是個支持靜態路由表配置的工具,提供了matchRoutes方法,可獲得匹配的路由數組。

import { matchRoutes } from 'react-router-config'import loadable from '@loadable/component'const Root = loadable((props) => import('./pages/Root'))const Index = loadable(() => import("./pages/Index"))const Home = loadable(() => import("./pages/Home"))const routes = [ {  path: '/',  component: Root,  routes: [   {    path: '/index',    component: Index,   },   {    path: '/home',    component: Home,    syncData () => {}    routes: []   }  ] }]router.all('/', async (url, next) => { const branch = matchRoutes(routes, url)})

組件的初始數據接口請求,最美的辦法當然是定義在各自的class組件的靜態方法中去,但是前提是組件不能被懶加載,不然獲取不到組件class,當然也無法獲取class static method了,很多使用@loadable/component(一個code split方案)庫的開發者多次提issue,作者也明示無法支持。不支持懶加載是絕對不可能的了。所以委屈一下代碼了,在需要的route對象中定義一個asyncData方法。

服務端

// routes.js{ path: '/home', component: Home, asyncData (store, query) {  const city = (query || '').split('=')[1]   let promise = store.dispatch(fetchCityListAndTemperature(city || undefined))    let promise2 = store.dispatch(setRefetchFlag(false))   return Promise.all([promise, promise2])  return promise }}
// render.jsimport { matchRoutes } from 'react-router-config'import createStore from '../store/redux/index'const store = createStore()const branch = matchRoutes(routes, url)const promises = branch.map(({ route }) => { // 遍歷所有匹配路由,預加載數據 return route.asyncData  ? route.asyncData(store, query)  : Promise.resolve(null)})// 完成store的預加載數據初始化工作await Promise.all(promises)// 獲取最新的storeconst preloadedState = store.getState()const App = (props) => { return (  <Provider store={store}>   <StaticRouter    location={ctx.url}    context={context}>        <Layout />   </StaticRouter>  </Provider> )}// 數據準備好后,render整個應用const html = renderToString(<App />)// 把預加載的數據掛載在`window`下返回,客戶端自己去取return   <html>   <head></head>   <body>    <div id="app">${html}</div>    <script>     window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState)};    </script>   </body>  </html>

客戶端

為保證兩端的應用數據一致,客戶端也要使用同一份數據初始化一次redux的store,再生成應用。如果兩者的dom/數據不一致,導致瀏覽器接管的時候dom重新生成了一次,在開發模式下的時候,控制臺會輸出錯誤信息,開發體驗完美。后續ajax的數據,在componentDidMount和事件中去執行,和服務端的邏輯天然剝離。

// 獲取服務端提供的初始化數據const preloadedState = window.__PRELOADED_STATE__ || undefineddelete window.__PRELOADED_STATE__// 客戶端store初始化const store = createStore(preloadedState)const App = () => { return (  <Provider store={store}>   <BrowserRouter>    <Layout />       </BrowserRouter>  </Provider> )}// loadableReady由@loadabel/component提供,在code split模式下使用loadableReady().then(() => {  ReactDom.hydrate(<App />, document.getElementById('app'))})

服務端調用的接口客戶端也必須有。這就帶來了如何避免重復請求的問題。我們知道componentDidMount方法只執行一次,如果服務器已經請求的數據帶有一個標識,就可以根據這個標識決定是否在客戶端需要發起一個新的請求了,需要注意的是判斷完成后重置該標識。

import { connect } from 'react-redux'@connect( state => ({  refetchFlag: state.weather.refetchFlag,  quality: state.weather.quality }), dispatch => ({  fetchCityListAndQuality: () => dispatch(fetchCityListAndQuality()),  setRefetchFlag : () => dispatch(setRefetchFlag(true)) }))export default class Quality extends Component { componentDidMount () {  const {   location: { search },   refetchFlag,   fetchCityListAndQuality,   setRefetchFlag  } = this.props  const { location: city } = queryString.parse(search)  refetchFlag    ? fetchCityListAndQuality(city || undefined)   : setRefetchFlag() }}

打包方案

客戶端打包

我想說的是“照舊”。因為在瀏覽器端運行的還是spa。入門級的具體見github,至于如何配置得賞心悅目,用起來得心應手,根據項目要求各顯神通吧。

服務端打包

和客戶端的異同:

同:

需要bable兼容不同版本的js語法

webpack v4+/babel v7+ ... 真香

... 留白

異:

入口文件不一樣,出口文件不一樣

這里既可以把整個服務端入口app.js作為打包入口,也可以把react路由的起點文件作為打包入口,配置輸出為umd模塊,再由app.js去require。以后者為例(好處在于升級改造項目時盡可能地降低對原系統的影響,排查問題也方便,斷點調試什么的也方便):

// webpack.server.jsconst webpackConfig = { entry: {  server: './src/server/index.js' }, output: {  path: path.resolve(__dirname, 'build'),  filename: '[name].js',  libraryTarget: 'umd' }}// app.jsconst reactKoaRouter = require('./build/server').defaultapp.use(reactKoaRouter.routes())

css、image資源正常來說服務端無需處理,如何繞開

偷懶,還沒開始研究,占個坑

require的是node自帶的模塊時避免被webpack打包

const serverConfig = { ... target: 'node' }

require第三方模塊時如何避免被打包

const serverConfig = { ... externals: [ require('webpack-node-externals')() ]

生產環境代碼無需做混淆壓縮

... 留白

服務端直出時資源的搜集

服務端輸出html時,需要定義好css資源、js資源,讓客戶端接管后下載使用。如果沒啥追求,可以直接把客戶端的輸出文件全加上去,暴力穩妥,簡單方便。但是上面提到的@loadable/component庫,實現了路由組件懶加載/code split功能后,也提供了全套服務,配套套裝的webpack工具,ssr工具,幫助我們做搜集資源的工作。

// webpack.base.jsconst webpackConfig = { plugins: [ ..., new LoadablePlugin() ]}// render.jsimport { ChunkExtractor } from '@loadable/server'const App = () => { return (  <Provider store={store}>   <StaticRouter    location={ctx.url}    context={context}>        <Layout />   </StaticRouter>  </Provider> )}const webStats = path.resolve( __dirname, '../public/loadable-stats.json', // 該文件由webpack插件自動生成)const webExtractor = new ChunkExtractor({  entrypoints: ['client'],  // 為入口文件名 statsFile: webStats})const jsx = webExtractor.collectChunks(<App />)const html = renderToString(jsx)const scriptTags = webExtractor.getScriptTags()const linkTags = webExtractor.getLinkTags() const styleTags = webExtractor.getStyleTags()const preloadedState = store.getState()const helmet = Helmet.renderStatic()return ` <html>  <head>   ${helmet.title.toString()}   ${helmet.meta.toString()}   ${linkTags}   ${styleTags}  </head>  <body>   <div id="app">${html}</div>   <script>    window.STORE = 'love';    window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState)};   </script>   ${scriptTags}  </body> </html>`

SEO信息

上面已經透露了。使用了一個react-helmet庫。具體用法可查看官方倉庫,信息可直接寫在組件上,最后根據優先級提升到head頭部。

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


注:相關教程知識閱讀請移步到JavaScript/Ajax教程頻道。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
国产精品久久一区| 欧美xxxx做受欧美| 色综合色综合久久综合频道88| 成人免费网站在线观看| 欧美日韩综合视频网址| 亚洲嫩模很污视频| 国产成人自拍视频在线观看| 日韩精品中文字幕在线| 国产亚洲欧洲高清| 欧美国产日产韩国视频| 国产视频丨精品|在线观看| 成人福利在线观看| 国产精品免费电影| 精品国产一区二区三区久久| 亚洲香蕉成视频在线观看| www.日韩不卡电影av| 亚洲成人精品在线| 69影院欧美专区视频| 日韩精品有码在线观看| 亚洲精品乱码久久久久久按摩观| 久久资源免费视频| 亚洲美女精品成人在线视频| 操日韩av在线电影| 91大神在线播放精品| **欧美日韩vr在线| 成人福利在线观看| 欧美视频在线视频| 91精品免费视频| 欧美激情中文字幕乱码免费| 8090成年在线看片午夜| 国产一区二区三区免费视频| 懂色av中文一区二区三区天美| 久久久中文字幕| 操日韩av在线电影| 日韩精品中文字幕视频在线| 亚洲区bt下载| 欧美激情免费看| 精品亚洲一区二区三区| 久久精品国产亚洲| 国产精品久久久久秋霞鲁丝| 亚洲国产一区自拍| 午夜精品久久久久久久99热| 久久精品国产精品| 精品美女久久久久久免费| 久久久女人电视剧免费播放下载| 中文字幕欧美日韩精品| 欧美老女人性视频| 成人欧美一区二区三区在线湿哒哒| 亚洲成人免费在线视频| 亚洲韩国日本中文字幕| 精品久久久一区二区| 色噜噜狠狠狠综合曰曰曰88av| 亚洲美腿欧美激情另类| 青青精品视频播放| 亚洲男人天堂古典| 日韩精品亚洲元码| 国产成+人+综合+亚洲欧美丁香花| 欧美怡红院视频一区二区三区| 97精品一区二区视频在线观看| 亚洲国产精品推荐| 欧美性色xo影院| 亚洲欧美在线看| 在线观看欧美www| 欧美日本啪啪无遮挡网站| 亚洲精品wwww| 欧美有码在线视频| 91久久国产精品| 久久这里只有精品视频首页| 77777少妇光屁股久久一区| 日韩av在线免费| 国产乱人伦真实精品视频| 午夜精品久久久久久久99热浪潮| 欧美巨大黑人极品精男| 亚洲天堂第一页| 美日韩丰满少妇在线观看| 国产亚洲精品久久久久动| 久久久极品av| 2020国产精品视频| 国产精品狠色婷| 欧美高清无遮挡| 日韩免费av在线| 在线成人一区二区| 久久精品人人爽| 日韩经典一区二区三区| 亚洲国产欧美精品| 久久久久在线观看| 国产成人+综合亚洲+天堂| 精品激情国产视频| 久久综合伊人77777| 亚洲欧美日韩精品| 国产精品96久久久久久| 最新亚洲国产精品| 欧美亚洲视频一区二区| 激情懂色av一区av二区av| 亚洲**2019国产| 欧美性猛交xxxx乱大交3| 欧美激情一区二区三级高清视频| 日韩av在线影院| 色系列之999| 久久亚洲精品毛片| 第一福利永久视频精品| 久久久在线免费观看| 欧美俄罗斯性视频| 亚洲欧美日韩爽爽影院| 国产性猛交xxxx免费看久久| 色青青草原桃花久久综合| 欧美视频免费在线观看| 亚洲精品v欧美精品v日韩精品| 亚洲最新视频在线| 国产精品国产自产拍高清av水多| 亚洲天堂免费视频| zzijzzij亚洲日本成熟少妇| 久久久成人精品视频| 97国产精品免费视频| 久久6免费高清热精品| 91精品国产91久久久久久不卡| 国产美女精彩久久| 91久久精品国产91性色| 2019中文在线观看| 精品偷拍一区二区三区在线看| 日韩欧美中文字幕在线播放| 日本免费一区二区三区视频观看| 97香蕉超级碰碰久久免费的优势| 亚洲一区二区三区视频| 色老头一区二区三区在线观看| 久久久爽爽爽美女图片| 亚洲va久久久噜噜噜| 国产精品自拍小视频| 91久久久久久久久久久| 狠狠躁天天躁日日躁欧美| 中文字幕亚洲一区| 亚洲qvod图片区电影| 欧美成人剧情片在线观看| 国产精品久久久久久久久久免费| 亚洲精品国产福利| 久久久久久成人精品| 国产精品一区二区三区毛片淫片| 亚洲娇小xxxx欧美娇小| 亚洲理论在线a中文字幕| 亚洲美女免费精品视频在线观看| 国产精品爱啪在线线免费观看| 欧美精品www在线观看| 国产精品久久久久免费a∨大胸| 中文字幕日韩在线播放| 亚洲区一区二区| 欧美疯狂xxxx大交乱88av| 一区二区三区视频免费| 国产精品18久久久久久麻辣| 51视频国产精品一区二区| 欧美激情久久久| 精品视频一区在线视频| 日本韩国在线不卡| 国产欧美日韩中文字幕在线| 亚洲精品永久免费| 夜夜躁日日躁狠狠久久88av| 欧美xxxx综合视频| 亚洲天堂av图片| 日韩在线免费视频观看| 精品一区二区三区电影| 亚洲iv一区二区三区| 国产一区二区视频在线观看| 国产在线精品成人一区二区三区| 国产一级揄自揄精品视频| 一区二区在线视频|