這篇“react中怎么實(shí)現(xiàn)同構(gòu)模板”文章的知識(shí)點(diǎn)大部分人都不太理解,所以小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細(xì),步驟清晰,具有一定的借鑒價(jià)值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來(lái)看看這篇“react中怎么實(shí)現(xiàn)同構(gòu)模板”文章吧。
創(chuàng)新互聯(lián)是一家專注于網(wǎng)站制作、成都網(wǎng)站設(shè)計(jì)與策劃設(shè)計(jì),開(kāi)平網(wǎng)站建設(shè)哪家好?創(chuàng)新互聯(lián)做網(wǎng)站,專注于網(wǎng)站建設(shè)十載,網(wǎng)設(shè)計(jì)領(lǐng)域的專業(yè)建站公司;建站業(yè)務(wù)涵蓋:開(kāi)平等地區(qū)。開(kāi)平做網(wǎng)站價(jià)格咨詢:18982081108
TODO List
數(shù)據(jù):如何保持前后端應(yīng)用狀態(tài)一致
路由:路由在服務(wù)端和客戶端中的匹配方案
代碼:同構(gòu),哪些地方可以共享,哪些地方需要差異化
靜態(tài)資源:服務(wù)端如何引入css/圖片等
ssr直出資源:服務(wù)端在渲染路由頁(yè)面時(shí)如何匹配css/chunks資源
打包方案:服務(wù)端和瀏覽器端如何寫(xiě)各自的webpack配置文件
SEO: head頭處理方案
同構(gòu)的基礎(chǔ)
正常的網(wǎng)頁(yè)運(yùn)行,需要生成dom,在dom樹(shù)loaded之后由js綁定相關(guān)的dom事件,監(jiān)聽(tīng)頁(yè)面的交互。服務(wù)端并不具備dom的執(zhí)行環(huán)境,因而所有的服務(wù)端渲染其實(shí)都是返回了一個(gè)填充了初始數(shù)據(jù)的靜態(tài)文本。在react中,除了常用的render這個(gè)用于生成dom的方法,還提供了renderToString,renderToStaticMarkup方法用來(lái)生成字符串,由于VitualDOM的存在,結(jié)合這些方法就可以像以前的字符串模板那樣生成普通的字符串,返回給客戶端接管,再接著進(jìn)行事件相關(guān)的綁定。最新的React v16+使用hydrate和ssr配套,能讓客戶端把服務(wù)端的VitualDOM渲染出來(lái)后得以復(fù)用,客戶端加載js后不會(huì)重刷一邊,減小了開(kāi)銷,也避免瀏覽器重刷dom時(shí)帶來(lái)的閃屏體驗(yàn)。而react的組件,還是和往常寫(xiě)spa一樣編寫(xiě),前后端共享。不同的只是入口的渲染方法換了名字,且客戶端會(huì)掛載dom而已。
// clinet.js ReactDom.hydrate(<App />, document.getElementById('app')) // server.js const html = ReactDom.renderToString(<App />)
同構(gòu)后網(wǎng)站運(yùn)行流程圖
盜用一張圖,來(lái)自阿里前端。乍一看,ssr與csr的區(qū)別就在于2 3 4 5,spa模式簡(jiǎn)單粗暴地返回一個(gè)空白的html頁(yè)面,然后在11里才去加載數(shù)據(jù)進(jìn)行頁(yè)面填充,在此之前,頁(yè)面都處于空白狀態(tài)。而ssr則會(huì)根據(jù)路由信息,提前獲取該路由頁(yè)面的初始數(shù)據(jù),返回頁(yè)面時(shí)已經(jīng)有了初步的內(nèi)容,不至于空白,也便于搜索引擎收錄。
路由匹配
瀏覽器端的路由匹配還是照著spa來(lái)做應(yīng)該無(wú)需費(fèi)心。略過(guò)了...
服務(wù)端的路由需要關(guān)注的,一個(gè)是后端服務(wù)的路由(如koa-router)匹配的問(wèn)題,一個(gè)是匹配到react應(yīng)用后react-router路由表的匹配問(wèn)題。
服務(wù)端路由,可通過(guò)/react前綴來(lái)和api接口等其他區(qū)別開(kāi)來(lái),這種路由匹配方式甚至能讓服務(wù)端渲染能同時(shí)支持老項(xiàng)目諸如ejs等的模板渲染方式,在系統(tǒng)升級(jí)改造方面可實(shí)現(xiàn)漸進(jìn)式地升級(jí)。
// app.js文件(后端入口) import reactController from './controllers/react-controller' // API路由 app.use(apiController.routes()) // ejs頁(yè)面路由 app.use(ejsController.routes()) // react頁(yè)面路由 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接口,稱之為靜態(tài)的路由。誠(chéng)然,服務(wù)端不像客戶端,對(duì)應(yīng)于一次網(wǎng)絡(luò)請(qǐng)求,路由就是當(dāng)前的請(qǐng)求url,是唯一的,不變的。在返回ssr直出的頁(yè)面后,頁(yè)面交互造成地址欄的變化,只要用的是react-router提供的方法,無(wú)論是hash方式,還是history方式,都屬于瀏覽器端react-router的工作了,于是完美繼承了spa的優(yōu)勢(shì)。只有在輸入欄敲擊Enter,才會(huì)發(fā)起新一輪的后臺(tái)請(qǐng)求。
import { StaticRouter } from 'react-router-dom' const App = () => { return ( <Provider store={store}> <StaticRouter location={ctx.url} context={context}> <Layout /> </StaticRouter> </Provider> ) }
應(yīng)用狀態(tài)數(shù)據(jù)管理
以往的服務(wù)端渲染,需要在客戶端網(wǎng)頁(yè)下載后馬上能看到的數(shù)據(jù)就放在服務(wù)器提前準(zhǔn)備好,可延遲展示,通過(guò)ajax請(qǐng)求的數(shù)據(jù)的交互邏輯放在頁(yè)面加載的js文件中去。
換成了react,其實(shí)套路也是一樣一樣的。但是區(qū)別在于:
傳統(tǒng)的字符串模板,組件模板是彼此分離的,可各自單獨(dú)引入數(shù)據(jù),再拼裝起來(lái)形成一份html。而在react的ssr里,頁(yè)面只能通過(guò)defaultValue和defaultProps一次性render,無(wú)法rerender。
不能寫(xiě)死defaultValude,所以只能使用props的數(shù)據(jù)方案。在執(zhí)行renderToString之前,提前準(zhǔn)備好整個(gè)應(yīng)用狀態(tài)的所有數(shù)據(jù)。全局的數(shù)據(jù)管理方案可考慮redux和mobx等。
需要準(zhǔn)備初始渲染數(shù)據(jù),所以要精準(zhǔn)獲取當(dāng)前地址將要渲染哪些組件。react-router-config和react-router同源配套,是個(gè)支持靜態(tài)路由表配置的工具,提供了matchRoutes方法,可獲得匹配的路由數(shù)組。
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) })
組件的初始數(shù)據(jù)接口請(qǐng)求,最美的辦法當(dāng)然是定義在各自的class組件的靜態(tài)方法中去,但是前提是組件不能被懶加載,不然獲取不到組件class,當(dāng)然也無(wú)法獲取class static method了,很多使用@loadable/component(一個(gè)code split方案)庫(kù)的開(kāi)發(fā)者多次提issue,作者也明示無(wú)法支持。不支持懶加載是絕對(duì)不可能的了。所以委屈一下代碼了,在需要的route對(duì)象中定義一個(gè)asyncData方法。
服務(wù)端
// 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.js import { matchRoutes } from 'react-router-config' import createStore from '../store/redux/index' const store = createStore() const branch = matchRoutes(routes, url) const promises = branch.map(({ route }) => { // 遍歷所有匹配路由,預(yù)加載數(shù)據(jù) return route.asyncData ? route.asyncData(store, query) : Promise.resolve(null) }) // 完成store的預(yù)加載數(shù)據(jù)初始化工作 await Promise.all(promises) // 獲取最新的store const preloadedState = store.getState() const App = (props) => { return ( <Provider store={store}> <StaticRouter location={ctx.url} context={context}> <Layout /> </StaticRouter> </Provider> ) } // 數(shù)據(jù)準(zhǔn)備好后,render整個(gè)應(yīng)用 const html = renderToString(<App />) // 把預(yù)加載的數(shù)據(jù)掛載在`window`下返回,客戶端自己去取 return <html> <head></head> <body> <div id="app">${html}</div> <script> window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState)}; </script> </body> </html>
客戶端
為保證兩端的應(yīng)用數(shù)據(jù)一致,客戶端也要使用同一份數(shù)據(jù)初始化一次redux的store,再生成應(yīng)用。如果兩者的dom/數(shù)據(jù)不一致,導(dǎo)致瀏覽器接管的時(shí)候dom重新生成了一次,在開(kāi)發(fā)模式下的時(shí)候,控制臺(tái)會(huì)輸出錯(cuò)誤信息,開(kāi)發(fā)體驗(yàn)完美。后續(xù)ajax的數(shù)據(jù),在componentDidMount和事件中去執(zhí)行,和服務(wù)端的邏輯天然剝離。
// 獲取服務(wù)端提供的初始化數(shù)據(jù) const preloadedState = window.__PRELOADED_STATE__ || undefined delete 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')) })
服務(wù)端調(diào)用的接口客戶端也必須有。這就帶來(lái)了如何避免重復(fù)請(qǐng)求的問(wèn)題。我們知道componentDidMount方法只執(zhí)行一次,如果服務(wù)器已經(jīng)請(qǐng)求的數(shù)據(jù)帶有一個(gè)標(biāo)識(shí),就可以根據(jù)這個(gè)標(biāo)識(shí)決定是否在客戶端需要發(fā)起一個(gè)新的請(qǐng)求了,需要注意的是判斷完成后重置該標(biāo)識(shí)。
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() } }
打包方案
客戶端打包
我想說(shuō)的是“照舊”。因?yàn)樵跒g覽器端運(yùn)行的還是spa。入門級(jí)的具體見(jiàn)github,至于如何配置得賞心悅目,用起來(lái)得心應(yīng)手,根據(jù)項(xiàng)目要求各顯神通吧。
服務(wù)端打包
和客戶端的異同:
同:
需要bable兼容不同版本的js語(yǔ)法
webpack v4+/babel v7+ ... 真香
... 留白
異:
入口文件不一樣,出口文件不一樣
這里既可以把整個(gè)服務(wù)端入口app.js作為打包入口,也可以把react路由的起點(diǎn)文件作為打包入口,配置輸出為umd模塊,再由app.js去require。以后者為例(好處在于升級(jí)改造項(xiàng)目時(shí)盡可能地降低對(duì)原系統(tǒng)的影響,排查問(wèn)題也方便,斷點(diǎn)調(diào)試什么的也方便):
// webpack.server.js const webpackConfig = { entry: { server: './src/server/index.js' }, output: { path: path.resolve(__dirname, 'build'), filename: '[name].js', libraryTarget: 'umd' } } // app.js const reactKoaRouter = require('./build/server').default app.use(reactKoaRouter.routes())
css、image資源正常來(lái)說(shuō)服務(wù)端無(wú)需處理,如何繞開(kāi)
偷懶,還沒(méi)開(kāi)始研究,占個(gè)坑
require的是node自帶的模塊時(shí)避免被webpack打包
const serverConfig = { ... target: 'node' }
require第三方模塊時(shí)如何避免被打包
const serverConfig = { ... externals: [ require('webpack-node-externals')() ]
生產(chǎn)環(huán)境代碼無(wú)需做混淆壓縮
... 留白
服務(wù)端直出時(shí)資源的搜集
服務(wù)端輸出html時(shí),需要定義好css資源、js資源,讓客戶端接管后下載使用。如果沒(méi)啥追求,可以直接把客戶端的輸出文件全加上去,暴力穩(wěn)妥,簡(jiǎn)單方便。但是上面提到的@loadable/component庫(kù),實(shí)現(xiàn)了路由組件懶加載/code split功能后,也提供了全套服務(wù),配套套裝的webpack工具,ssr工具,幫助我們做搜集資源的工作。
// webpack.base.js const webpackConfig = { plugins: [ ..., new LoadablePlugin() ] } // render.js import { 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插件自動(dòng)生成 ) 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信息
上面已經(jīng)透露了。使用了一個(gè)react-helmet庫(kù)。具體用法可查看官方倉(cāng)庫(kù),信息可直接寫(xiě)在組件上,最后根據(jù)優(yōu)先級(jí)提升到head頭部。
以上就是關(guān)于“react中怎么實(shí)現(xiàn)同構(gòu)模板”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對(duì)大家有幫助,若想了解更多相關(guān)的知識(shí)內(nèi)容,請(qǐng)關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。
當(dāng)前標(biāo)題:react中怎么實(shí)現(xiàn)同構(gòu)模板
本文地址:http://muchs.cn/article14/piopde.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供云服務(wù)器、外貿(mào)建站、動(dòng)態(tài)網(wǎng)站、關(guān)鍵詞優(yōu)化、響應(yīng)式網(wǎng)站、網(wǎng)站建設(shè)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)