網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
背景
第一階段
很久以前, 一個(gè)網(wǎng)站的開(kāi)發(fā)還是前端和服務(wù)端在一個(gè)項(xiàng)目來(lái)維護(hù), 可能是用php+jquery.
那時(shí)候的頁(yè)面渲染是放在服務(wù)端的, 也就是用戶訪問(wèn)一個(gè)頁(yè)面a的時(shí)候, 會(huì)直接訪問(wèn)服務(wù)端路由, 由服務(wù)端來(lái)渲染頁(yè)面然后返回給瀏覽器。
也就是說(shuō)網(wǎng)頁(yè)的所有內(nèi)容都會(huì)一次性被寫(xiě)在html里, 一起送給瀏覽器。
這時(shí)候你右鍵點(diǎn)擊查看網(wǎng)頁(yè)源代碼, 可以看到所有的代碼; 或者你去查看html請(qǐng)求, 查看"預(yù)覽", 會(huì)發(fā)現(xiàn)他就是一個(gè)完整的網(wǎng)頁(yè)。
第二階段
但是慢慢的人們覺(jué)得上面這種方式前后端協(xié)同太麻煩, 耦合太嚴(yán)重, 嚴(yán)重影響開(kāi)發(fā)效率和體驗(yàn)。
于是隨著vue/react的橫空出世, 人們開(kāi)始習(xí)慣了純客戶端渲染的spa.
這時(shí)候的html中只會(huì)寫(xiě)入一些主腳本文件, 沒(méi)有什么實(shí)質(zhì)性的內(nèi)容. 等到html在瀏覽器端解析后, 執(zhí)行js文件, 才逐步把元素創(chuàng)建在dom上。
所以你去查看網(wǎng)頁(yè)源代碼的時(shí)候, 發(fā)現(xiàn)根本沒(méi)什么內(nèi)容, 只有各種腳本的鏈接。
第三階段
后來(lái)人們又慢慢的覺(jué)得, 純spa對(duì)SEO非常不友好, 并且白屏?xí)r間很長(zhǎng)。
對(duì)于一些活動(dòng)頁(yè), 白屏?xí)r間長(zhǎng)代表了什么? 代表了用戶根本沒(méi)有耐心去等待頁(yè)面加載完成.
所以人們又想回到服務(wù)端渲染, 提高SEO的效果, 盡量縮短白屏?xí)r間.
那難道我們又要回到階段一那種人神共憤的開(kāi)發(fā)模式嗎? 不, 我們現(xiàn)在有了新的方案, 新的模式, 叫做同構(gòu)。
所謂的同構(gòu)理解為:同種結(jié)構(gòu)的不同表現(xiàn)形態(tài), 同一份react代碼, 分別在兩端各執(zhí)行一遍。
創(chuàng)建一個(gè)服務(wù)端渲染應(yīng)用
renderToString
首先來(lái)看看他是個(gè)什么東西
它可以渲染一個(gè)react元素/組件到頁(yè)面中,而且只能用到服務(wù)端
所以spa react-dom -> render 相對(duì)應(yīng)的就是spa react-dom/server -> renderToString整一個(gè)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('h1',null,'Hello World!');
}
}
app.get('/',function(req,res){
const content = renderToString(React.createElement(App));//渲染成HTML
res.send(content);//返回結(jié)果
})
app.listen(PORT,() => {
console.log(`server is listening on ${PORT}`);
})
啟動(dòng)服務(wù)端之后,手動(dòng)網(wǎng)頁(yè)訪問(wèn)本地對(duì)應(yīng)的端口
可以看到,返回的就是hello world,這就是一個(gè)服務(wù)端應(yīng)用!
webpack配置
應(yīng)用寫(xiě)好之后,需要瀏覽器端的webpack配置
const path = require('path');
const nodeExternals = require('webpack-node-externals');//打包的時(shí)候不打包node_modules
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
entry:{
index:path.resolve(__dirname,'../server.js')
},
mode:'development',
target:'node',//不將node自帶的諸如path、fs這類的包打進(jìn)去,一定要是node
devtool: 'cheap-module-eval-source-map',//source-map配置相關(guān),這塊可以理解為提供更快的打包性能
output:{
filename:'[name].js',
path:path.resolve(__dirname,'../dist/server')//常用輸出路徑
},
externals:[nodeExternals()], //不將node_modules里面的包打進(jìn)去
resolve:{
alias:{
'@':path.resolve(__dirname,'../src')
},
extensions:['.js']
},
module:{//babel轉(zhuǎn)化配置
rules:[{
test:/\.js$/,
use:'babel-loader',
exclude:/node_modules/
}]
},
plugins: [//一般應(yīng)用都會(huì)有的public目錄,直接拷貝到dist目錄下
new CopyWebpackPlugin([{
from:path.resolve(__dirname,'../public'),
to:path.resolve(__dirname,'../dist')
}])
]
}
cli用習(xí)慣了,寫(xiě)配置有點(diǎn)折磨,寫(xiě)好之后要怎么去使用呢?package.json配置運(yùn)行腳本:
"scripts": {
"build:server": "webpack --config build/webpack-server.config.js --watch",
"server": "nodemon dist/server/index.js"
}
那么,先打個(gè)包
可以看到,已經(jīng)打包出來(lái)了一大堆看不懂的東西
這個(gè)時(shí)候,運(yùn)行起來(lái)即可
到現(xiàn)在寫(xiě)了這么多配置,其實(shí)只是為了讓服務(wù)端支持一下瀏覽器端基本的運(yùn)行配置/環(huán)境
給h1標(biāo)簽綁定一個(gè)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 <h1 onClick={this.handleClick}>Hello World!</h1>;
}
};
app.get('/',function(req,res){
const content = renderToString(<App/>);
console.log(content);
res.send(content);
});
app.listen(3000);
這個(gè)時(shí)候如果你去跑一下,會(huì)發(fā)現(xiàn)點(diǎn)擊的時(shí)候,根 本 沒(méi) 反 應(yīng) !
這個(gè)時(shí)候稍微想一下,renderToString是把元素轉(zhuǎn)成字符串而已, 事件什么的根本沒(méi)有綁定
這個(gè)時(shí)候同構(gòu)就來(lái)了!
那么同構(gòu)就是:
同一份代碼, 在服務(wù)端跑一遍, 就生成了html
同一份代碼, 在客戶端跑一遍, 就能響應(yīng)各種用戶操作
所以需要將App單獨(dú)提取出來(lái)
src/app.js
import React from 'react';
class App extends React.PureComponent{
handleClick=(e)=>{
alert(e.target.innerHTML);
}
render(){
return <h1 onClick={this.handleClick}>Hello World!</h1>;
}
};
export default App;
src/index.js
就跟正常spa應(yīng)用一樣的寫(xiě)法
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/
}]
}
}
運(yùn)行腳本也給他添加一下
"build:client": "webpack --config build/webpack-client.config.js --watch"
運(yùn)行一下
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>
`);//手動(dòng)創(chuàng)建根節(jié)點(diǎn),把App標(biāo)簽內(nèi)容引進(jìn)來(lái)
});
app.listen(3000);
再來(lái)測(cè)試一下,這時(shí)候發(fā)現(xiàn)頁(yè)面渲染沒(méi)問(wèn)題, 并且也能響應(yīng)用戶操作, 比如點(diǎn)擊事件了.
hydrate
經(jīng)過(guò)上面的5步, 看起來(lái)沒(méi)問(wèn)題了, 但是我們的控制臺(tái)會(huì)輸出一些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()的區(qū)別就是:
ReactDOM.render()會(huì)將掛載dom節(jié)點(diǎn)的所有子節(jié)點(diǎn)全部清空掉,再重新生成子節(jié)點(diǎn)。
ReactDOM.hydrate()則會(huì)復(fù)用掛載dom節(jié)點(diǎn)的子節(jié)點(diǎn),并將其與react的virtualDom關(guān)聯(lián)上。
也就是說(shuō)ReactDOM.render()會(huì)將服務(wù)端做的工作全部推翻重做,而ReactDOM.hydrate()在服務(wù)端做的工作基礎(chǔ)上再進(jìn)行深入的操作.
所以我們修改一下客戶端的入口文件src/index.js, 將render修改為hydrate
import React from 'react';
import { hydrate } from 'react-dom';
import App from './app';
hydrate(<App/>,document.getElementById("root"));
同構(gòu)流程總結(jié)
- 服務(wù)端根據(jù)React代碼生成html
- 客戶端發(fā)起請(qǐng)求, 收到服務(wù)端發(fā)送的html, 進(jìn)行解析和展示
- 客戶端加載js等資源文件
- 客戶端執(zhí)行js文件, 完成hydrate操作
- 客戶端接管整體應(yīng)用
路由
在客戶端渲染時(shí), React提供了BrowserRouter和HashRouter來(lái)供我們處理路由, 但是他們都依賴window對(duì)象, 而在服務(wù)端是沒(méi)有window的。
但是react-router提供了StaticRouter, 為我們的服務(wù)端渲染做服務(wù)。
接下來(lái)我們模擬添加幾個(gè)頁(yè)面, 實(shí)現(xiàn)一下路由的功能。
構(gòu)造Login和User兩個(gè)頁(yè)面
//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>
}
}
添加服務(wù)端路由
//server.js
import express from 'express';
import React from 'react';
import {renderToString} from 'react-dom/server';
import {StaticRouter,Route} from 'react-router';//服務(wù)端使用靜態(tài)路由
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);
這個(gè)時(shí)候會(huì)發(fā)現(xiàn)一個(gè)現(xiàn)象,在頁(yè)面上通過(guò)url修改路由到Login的時(shí)候,界面上登錄兩個(gè)字一閃即逝,這是為啥呢?
因?yàn)殡m然服務(wù)端路由配置好了,也確實(shí)模塊嵌入進(jìn)來(lái)了,但是!!!客戶端還沒(méi)有進(jìn)行處理
添加客戶端路由
//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")
);
分別訪問(wèn)一下/user和/login,發(fā)現(xiàn)已經(jīng)可以正常渲染了,但是!!!明明是一樣的映射規(guī)則,只是路由根組件不一樣,還要寫(xiě)兩遍也太折磨了,于是有了接下來(lái)的路由同構(gòu)
路由同構(gòu)
既要在客戶端寫(xiě)一遍路由, 也要在服務(wù)端寫(xiě)一遍路由, 有沒(méi)有什么方法能只寫(xiě)一遍? 就像app.js一樣?
所以我們先找一下兩端路由的異同:
- 共同點(diǎn):路徑和組件的映射關(guān)系是相同的
- 不同點(diǎn):路由引用的組件不一樣, 或者說(shuō)實(shí)現(xiàn)的方式不一樣
路徑和組件之間的關(guān)系可以用抽象化的語(yǔ)言去描述清楚,也就是我們所說(shuō)路由配置化。
最后我們提供一個(gè)轉(zhuǎn)換器,可以根據(jù)我們的需要去轉(zhuǎn)換成服務(wù)端或者客戶端路由。
//新建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',//觸發(fā)重定向時(shí),統(tǒng)一回到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轉(zhuǎn)換器
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)=>{
//路由分發(fā),遍歷路由,判斷type走對(duì)應(yīng)的邏輯
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)=>{//區(qū)分server/client,因?yàn)閯?chuàng)建方式不一樣
//params用以處理重定向問(wèn)題
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;
服務(wù)端入口
//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);
重定向問(wèn)題
這里我們從/重定向到/user的時(shí)候, 可以看到html返回的內(nèi)容和實(shí)現(xiàn)頁(yè)面渲染的內(nèi)容是不一樣的。
這代表重定向操作是客戶端來(lái)完成的, 而我們期望的是先訪問(wèn)index.html請(qǐng)求, 返回302, 然后出現(xiàn)一個(gè)新的user.html請(qǐng)求
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}) );
//當(dāng)Redirect被使用時(shí),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);
這時(shí)候我們?cè)贉y(cè)試一下, 就會(huì)發(fā)現(xiàn)符合預(yù)期, 出現(xiàn)了兩個(gè)請(qǐng)求, 一個(gè)302, 一個(gè)user.html
404問(wèn)題
我們隨便輸入一個(gè)不存在的路由, 發(fā)現(xiàn)內(nèi)容是如期返回了404, 但是請(qǐng)求確實(shí)200的, 這是不對(duì)的.
//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}) );
//當(dāng)Redirect被使用時(shí),context.url將包含重新向的地址
if(context.url){
//302
res.redirect(context.url);
}else{
if(context.NOT_FOUND) res.status(404);//判斷是否設(shè)置狀態(tài)碼為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頁(yè)面
if (staticContext) staticContext.NOT_FOUND = true;
return <NotFound/>
}
到此,完整的功能已經(jīng)實(shí)現(xiàn)!
原文鏈接:https://blog.csdn.net/weixin_48391468/article/details/122249022
相關(guān)推薦
- 2022-11-26 kotlin?android?extensions?插件實(shí)現(xiàn)示例詳解_Android
- 2023-04-03 c++?lambda捕獲this?導(dǎo)致多線程下類釋放后還在使用的錯(cuò)誤問(wèn)題_C 語(yǔ)言
- 2022-12-05 Django中使用AJAX的詳細(xì)過(guò)程_python
- 2022-08-12 Android自定義彈出框的方法_Android
- 2024-03-14 SpringBoot中RestTemplate 發(fā)送http請(qǐng)求
- 2022-06-22 android選項(xiàng)卡TabHost功能用法詳解_Android
- 2023-01-31 基于C#實(shí)現(xiàn)獲取本地磁盤(pán)目錄_C#教程
- 2022-01-19 正則——時(shí)間 時(shí)分秒 12小時(shí)制 24小時(shí)制 moment可以轉(zhuǎn)化的時(shí)間 HH:mm:ss hh:m
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門(mén)
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支