您好,登錄后才能下訂單哦!
這篇文章主要講解了“React服務端渲染和同構怎么實現”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“React服務端渲染和同構怎么實現”吧!
很久以前, 一個網站的開發還是前端和服務端在一個項目來維護, 可能是用php+jquery.
那時候的頁面渲染是放在服務端的, 也就是用戶訪問一個頁面a的時候, 會直接訪問服務端路由, 由服務端來渲染頁面然后返回給瀏覽器。
也就是說網頁的所有內容都會一次性被寫在html里, 一起送給瀏覽器。
這時候你右鍵點擊查看網頁源代碼, 可以看到所有的代碼; 或者你去查看html請求, 查看"預覽", 會發現他就是一個完整的網頁。
但是慢慢的人們覺得上面這種方式前后端協同太麻煩, 耦合太嚴重, 嚴重影響開發效率和體驗。
于是隨著vue/react的橫空出世, 人們開始習慣了純客戶端渲染的spa.
這時候的html中只會寫入一些主腳本文件, 沒有什么實質性的內容. 等到html在瀏覽器端解析后, 執行js文件, 才逐步把元素創建在dom上。
所以你去查看網頁源代碼的時候, 發現根本沒什么內容, 只有各種腳本的鏈接。
后來人們又慢慢的覺得, 純spa對SEO非常不友好, 并且白屏時間很長。
對于一些活動頁, 白屏時間長代表了什么? 代表了用戶根本沒有耐心去等待頁面加載完成.
所以人們又想回到服務端渲染, 提高SEO的效果, 盡量縮短白屏時間.
那難道我們又要回到階段一那種人神共憤的開發模式嗎? 不, 我們現在有了新的方案, 新的模式, 叫做同構。
所謂的同構理解為:同種結構的不同表現形態, 同一份react代碼, 分別在兩端各執行一遍。
renderToString
首先來看看他是個什么東西
它可以渲染一個react元素/組件到頁面中,而且只能用到服務端
所以spa react-dom -> render 相對應的就是spa react-dom/server -> renderToString整一個Hello World
//MyServer.js const { renderToString } = require('react-dom/server'); const React = require('react'); const express = require('express');//commonJS方式引入 var app = express(); const PORT = 3000; const App = class extends React.PureComponent { render(){ return React.createElement('h2',null,'Hello World!'); } } app.get('/',function(req,res){ const content = renderToString(React.createElement(App));//渲染成HTML res.send(content);//返回結果 }) app.listen(PORT,() => { console.log(`server is listening on ${PORT}`); })
啟動服務端之后,手動網頁訪問本地對應的端口
可以看到,返回的就是hello world,這就是一個服務端應用!
webpack配置
應用寫好之后,需要瀏覽器端的webpack配置
const path = require('path'); const nodeExternals = require('webpack-node-externals');//打包的時候不打包node_modules const CopyWebpackPlugin = require('copy-webpack-plugin'); module.exports = { entry:{ index:path.resolve(__dirname,'../server.js') }, mode:'development', target:'node',//不將node自帶的諸如path、fs這類的包打進去,一定要是node devtool: 'cheap-module-eval-source-map',//source-map配置相關,這塊可以理解為提供更快的打包性能 output:{ filename:'[name].js', path:path.resolve(__dirname,'../dist/server')//常用輸出路徑 }, externals:[nodeExternals()], //不將node_modules里面的包打進去 resolve:{ alias:{ '@':path.resolve(__dirname,'../src') }, extensions:['.js'] }, module:{//babel轉化配置 rules:[{ test:/\.js$/, use:'babel-loader', exclude:/node_modules/ }] }, plugins: [//一般應用都會有的public目錄,直接拷貝到dist目錄下 new CopyWebpackPlugin([{ from:path.resolve(__dirname,'../public'), to:path.resolve(__dirname,'../dist') }]) ] }
cli用習慣了,寫配置有點折磨,寫好之后要怎么去使用呢?package.json配置運行腳本:
"scripts": { "build:server": "webpack --config build/webpack-server.config.js --watch", "server": "nodemon dist/server/index.js" }
那么,先打個包
可以看到,已經打包出來了一大堆看不懂的東西
這個時候,運行起來即可
到現在寫了這么多配置,其實只是為了讓服務端支持一下瀏覽器端基本的運行配置/環境
給h2標簽綁定一個click事件
import React from 'react'; import {renderToString} from 'react-dom/server'; const express = require('express'); const app = express(); const App = class extends React.PureComponent{ handleClick=(e)=>{ alert(e.target.innerHTML); } render(){ return <h2 onClick={this.handleClick}>Hello World!</h2>; } }; app.get('/',function(req,res){ const content = renderToString(<App/>); console.log(content); res.send(content); }); app.listen(3000);
這個時候如果你去跑一下,會發現點擊的時候,根 本 沒 反 應 !
這個時候稍微想一下,renderToString是把元素轉成字符串而已, 事件什么的根本沒有綁定
這個時候同構就來了!
那么同構就是:
同一份代碼, 在服務端跑一遍, 就生成了html
同一份代碼, 在客戶端跑一遍, 就能響應各種用戶操作
所以需要將App單獨提取出來
src/app.js
import React from 'react'; class App extends React.PureComponent{ handleClick=(e)=>{ alert(e.target.innerHTML); } render(){ return <h2 onClick={this.handleClick}>Hello World!</h2>; } }; export default App;
src/index.js
就跟正常spa應用一樣的寫法
import React from 'react'; import {render} from 'react-dom'; import App from './app'; render(<App/>,document.getElementById("root"));
build/webpack-client.config.js
處理客戶端代碼的打包邏輯
const path = require('path'); module.exports = { entry:{ index:path.resolve(__dirname,'../src/index.js')//路徑修改 }, mode:'development', /*target:'node',客戶端不需要此配置了昂*/ devtool: 'cheap-module-eval-source-map', output:{ filename:'[name].js', path:path.resolve(__dirname,'../dist/client')//路徑修改 }, resolve:{ alias:{ '@':path.resolve(__dirname,'../src') }, extensions:['.js'] }, module:{ rules:[{ test:/\.js$/, use:'babel-loader', exclude:/node_modules/ }] } }
運行腳本也給他添加一下
"build:client": "webpack --config build/webpack-client.config.js --watch"
運行一下
npm run build:client
server引用打包好的客戶端資源
import express from 'express'; import React from 'react'; import {renderToString} from 'react-dom/server'; import App from './src/app'; const app = express(); app.use(express.static("dist")) app.get('/',function(req,res){ const content = renderToString(<App/>); res.send(` <!doctype html> <html> <title>ssr</title> <body> <div id="root">${content}</div> <script src="/client/index.js"></script> </body> </html> `);//手動創建根節點,把App標簽內容引進來 }); app.listen(3000);
再來測試一下,這時候發現頁面渲染沒問題, 并且也能響應用戶操作, 比如點擊事件了.
hydrate
經過上面的5步, 看起來沒問題了, 但是我們的控制臺會輸出一些warnning
Warning: render(): Calling ReactDOM.render() to hydrate server-rendered markup will stop working in React v18. Replace the ReactDOM.render() call with ReactDOM.hydrate() if you want React to attach to the server HTML.
ReactDOM.hydrate()和ReactDOM.render()的區別就是:
ReactDOM.render()會將掛載dom節點的所有子節點全部清空掉,再重新生成子節點。
ReactDOM.hydrate()則會復用掛載dom節點的子節點,并將其與react的virtualDom關聯上。
也就是說ReactDOM.render()會將服務端做的工作全部推翻重做,而ReactDOM.hydrate()在服務端做的工作基礎上再進行深入的操作.
所以我們修改一下客戶端的入口文件src/index.js, 將render修改為hydrate
import React from 'react'; import { hydrate } from 'react-dom'; import App from './app'; hydrate(<App/>,document.getElementById("root"));
服務端根據React代碼生成html
客戶端發起請求, 收到服務端發送的html, 進行解析和展示
客戶端加載js等資源文件
客戶端執行js文件, 完成hydrate操作
客戶端接管整體應用
在客戶端渲染時, React提供了BrowserRouter和HashRouter來供我們處理路由, 但是他們都依賴window對象, 而在服務端是沒有window的。
但是react-router提供了StaticRouter, 為我們的服務端渲染做服務。
接下來我們模擬添加幾個頁面, 實現一下路由的功能。
構造Login和User兩個頁面
//src/pages/login/index.js import React from 'react'; export default class Login extends React.PureComponent{ render(){ return <div>登陸</div> } }
//src/pages/user/index.js import React from 'react'; export default class User extends React.PureComponent{ render(){ return <div>用戶</div> } }
添加服務端路由
//server.js import express from 'express'; import React from 'react'; import {renderToString} from 'react-dom/server'; import {StaticRouter,Route} from 'react-router';//服務端使用靜態路由 import Login from '@/pages/login'; import User from '@/pages/user'; const app = express(); app.use(express.static("dist")) app.get('*',function(req,res){ const content = renderToString(<div> <StaticRouter location={req.url}> <Route exact path="/user" component={User}></Route> <Route exact path="/login" component={Login}></Route> </StaticRouter> </div>); res.send(` <!doctype html> <html> <title>ssr</title> <body> <div id="root">${content}</div> <script src="/client/index.js"></script> </body> </html> `); }); app.listen(3000);
這個時候會發現一個現象,在頁面上通過url修改路由到Login的時候,界面上登錄兩個字一閃即逝,這是為啥呢?
因為雖然服務端路由配置好了,也確實模塊嵌入進來了,但是!!!客戶端還沒有進行處理
添加客戶端路由
//src/index.js import React from 'react'; import { hydrate } from 'react-dom'; import App from './app'; import { BrowserRouter as Router, Route } from 'react-router-dom'; import User from './pages/user'; import Login from './pages/login'; hydrate( <Router> <Route path="/" component={App}> <Route exact path="/user" component={User}></Route> <Route exact path="/login" component={Login}></Route> </Route> </Router>, document.getElementById("root") );
分別訪問一下/user和/login,發現已經可以正常渲染了,但是!!!明明是一樣的映射規則,只是路由根組件不一樣,還要寫兩遍也太折磨了,于是有了接下來的路由同構
既要在客戶端寫一遍路由, 也要在服務端寫一遍路由, 有沒有什么方法能只寫一遍? 就像app.js一樣?
所以我們先找一下兩端路由的異同:
共同點:路徑和組件的映射關系是相同的
不同點:路由引用的組件不一樣, 或者說實現的方式不一樣
路徑和組件之間的關系可以用抽象化的語言去描述清楚,也就是我們所說路由配置化。
最后我們提供一個轉換器,可以根據我們的需要去轉換成服務端或者客戶端路由。
//新建src/pages/notFound/index.js import React from 'react'; export default ()=> <div>404</div>
路由配置文件
//src/router/routeConfig.js import Login from '@/pages/login'; import User from '@/pages/user'; import NotFound from '@/pages/notFound'; export default [{ type:'redirect',//觸發重定向時,統一回到user exact:true, from:'/', to:'/user' },{ type:'route', path:'/user', exact:true, component:User },{ type:'route', path:'/login', exact:true, component:Login },{ type:'route', path:'*', component:NotFound }]
router轉換器
import React from 'react'; import { createBrowserHistory } from "history"; import {Route,Router,StaticRouter,Redirect,Switch} from 'react-router'; import routeConfig from './routeConfig'; const routes = routeConfig.map((conf,index)=>{ //路由分發,遍歷路由,判斷type走對應的邏輯 const {type,...otherConf} = conf; if(type==='redirect'){ return <Redirect key={index} {...otherConf}/>; }else if(type ==='route'){ return <Route key={index} {...otherConf}></Route>; } }); export const createRouter = (type)=>(params)=>{//區分server/client,因為創建方式不一樣 //params用以處理重定向問題 if(type==='client'){ const history = createBrowserHistory(); return <Router history={history}> <Switch> {routes} </Switch> </Router> }else if(type==='server'){ // const {location} = params; return <StaticRouter {...params}> <Switch> {routes} </Switch> </StaticRouter> } }
客戶端入口
//src/index.js import React from 'react'; import { hydrate } from 'react-dom'; import App from './app'; hydrate( <App />, document.getElementById("root") );
客戶端 app.js
//src/app.js import React from 'react'; import { createRouter } from './router' class App extends React.PureComponent{ render(){ return createRouter('client')(); } }; export default App;
服務端入口
//server.js import express from 'express'; import React from 'react'; import {renderToString} from 'react-dom/server'; import { createRouter } from './src/router' const app = express(); app.use(express.static("dist")) app.get('*',function(req,res){ const content = renderToString(createRouter('server')({location:req.url}) ); res.send(` <!doctype html> <html> <title>ssr</title> <body> <div id="root">${content}</div> <script src="/client/index.js"></script> </body> </html> `); }); app.listen(3000);
重定向問題
這里我們從/重定向到/user的時候, 可以看到html返回的內容和實現頁面渲染的內容是不一樣的。
這代表重定向操作是客戶端來完成的, 而我們期望的是先訪問index.html請求, 返回302, 然后出現一個新的user.html請求
https://v5.reactrouter.com/web/api/StaticRouter react提供了一種重定向的處理方式
import express from 'express'; import React from 'react'; import {renderToString} from 'react-dom/server'; import { createRouter } from './src/router' const app = express(); app.use(express.static("dist")) app.get('*',function(req,res){ const context = {}; const content = renderToString(createRouter('server')({location:req.url, context}) ); //當Redirect被使用時,context.url將包含重新向的地址 if(context.url){ //302 res.redirect(context.url); }else{ res.send(` <!doctype html> <html> <title>ssr</title> <body> <div id="root">${content}</div> <script src="/client/index.js"></script> </body> </html> `); } }); app.listen(3000);
這時候我們再測試一下, 就會發現符合預期, 出現了兩個請求, 一個302, 一個user.html
404問題
我們隨便輸入一個不存在的路由, 發現內容是如期返回了404, 但是請求確實200的, 這是不對的.
//server.js import express from 'express'; import React from 'react'; import {renderToString} from 'react-dom/server'; import { createRouter } from './src/router' const app = express(); app.use(express.static("dist")) app.get('*',function(req,res){ const context = {}; const content = renderToString(createRouter('server')({location:req.url, context}) ); //當Redirect被使用時,context.url將包含重新向的地址 if(context.url){ //302 res.redirect(context.url); }else{ if(context.NOT_FOUND) res.status(404);//判斷是否設置狀態碼為404 res.send(` <!doctype html> <html> <title>ssr</title> <body> <div id="root">${content}</div> <script src="/client/index.js"></script> </body> </html> `); } }); app.listen(3000);
routeConfig.js
//routeConfig.js import React from 'react'; //改造前 component:NotFound //改造后 render:({staticContext})=>{//接收并判斷屬性,決定是否渲染404頁面 if (staticContext) staticContext.NOT_FOUND = true; return <NotFound/> }
感謝各位的閱讀,以上就是“React服務端渲染和同構怎么實現”的內容了,經過本文的學習后,相信大家對React服務端渲染和同構怎么實現這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。