欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

React服務(wù)端渲染和同構(gòu)的實(shí)現(xiàn)

 更新時(shí)間:2022年04月27日 10:30:23   作者:大聰明397  
本文主要介紹了React服務(wù)端渲染和同構(gòu)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

背景

第一階段

很久以前, 一個(gè)網(wǎng)站的開發(fā)還是前端和服務(wù)端在一個(gè)項(xiàng)目來維護(hù), 可能是用php+jquery.
那時(shí)候的頁面渲染是放在服務(wù)端的, 也就是用戶訪問一個(gè)頁面a的時(shí)候, 會(huì)直接訪問服務(wù)端路由, 由服務(wù)端來渲染頁面然后返回給瀏覽器。

也就是說網(wǎng)頁的所有內(nèi)容都會(huì)一次性被寫在html里, 一起送給瀏覽器。
這時(shí)候你右鍵點(diǎn)擊查看網(wǎng)頁源代碼, 可以看到所有的代碼; 或者你去查看html請求, 查看"預(yù)覽", 會(huì)發(fā)現(xiàn)他就是一個(gè)完整的網(wǎng)頁。

第二階段

但是慢慢的人們覺得上面這種方式前后端協(xié)同太麻煩, 耦合太嚴(yán)重, 嚴(yán)重影響開發(fā)效率和體驗(yàn)。
于是隨著vue/react的橫空出世, 人們開始習(xí)慣了純客戶端渲染的spa.

這時(shí)候的html中只會(huì)寫入一些主腳本文件, 沒有什么實(shí)質(zhì)性的內(nèi)容. 等到html在瀏覽器端解析后, 執(zhí)行js文件, 才逐步把元素創(chuàng)建在dom上。
所以你去查看網(wǎng)頁源代碼的時(shí)候, 發(fā)現(xiàn)根本沒什么內(nèi)容, 只有各種腳本的鏈接。

第三階段

后來人們又慢慢的覺得, 純spa對SEO非常不友好, 并且白屏?xí)r間很長。
對于一些活動(dòng)頁, 白屏?xí)r間長代表了什么? 代表了用戶根本沒有耐心去等待頁面加載完成.

所以人們又想回到服務(wù)端渲染, 提高SEO的效果, 盡量縮短白屏?xí)r間.

那難道我們又要回到階段一那種人神共憤的開發(fā)模式嗎? 不, 我們現(xiàn)在有了新的方案, 新的模式, 叫做同構(gòu)。

所謂的同構(gòu)理解為:同種結(jié)構(gòu)的不同表現(xiàn)形態(tài), 同一份react代碼, 分別在兩端各執(zhí)行一遍。

創(chuàng)建一個(gè)服務(wù)端渲染應(yīng)用

renderToString

首先來看看他是個(gè)什么東西

在這里插入圖片描述

它可以渲染一個(gè)react元素/組件到頁面中,而且只能用到服務(wù)端
所以spa react-dom -> render 相對應(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īng)的端口

在這里插入圖片描述

可以看到,返回的就是hello world,這就是一個(gè)服務(wù)端應(yīng)用!

webpack配置

應(yīng)用寫好之后,需要瀏覽器端的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í)慣了,寫配置有點(diǎn)折磨,寫好之后要怎么去使用呢?package.json配置運(yùn)行腳本:

"scripts": {
    "build:server": "webpack --config build/webpack-server.config.js --watch",
    "server": "nodemon dist/server/index.js"
  }

那么,先打個(gè)包

在這里插入圖片描述

可以看到,已經(jīng)打包出來了一大堆看不懂的東西
這個(gè)時(shí)候,運(yùn)行起來即可
到現(xiàn)在寫了這么多配置,其實(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í)候,根 本 沒 反 應(yīng) !
這個(gè)時(shí)候稍微想一下,renderToString是把元素轉(zhuǎn)成字符串而已, 事件什么的根本沒有綁定

這個(gè)時(shí)候同構(gòu)就來了!

那么同構(gòu)就是:
同一份代碼, 在服務(wù)端跑一遍, 就生成了html
同一份代碼, 在客戶端跑一遍, 就能響應(yīng)各種用戶操作
所以需要將App單獨(dú)提取出來

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)用一樣的寫法

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)來
});
app.listen(3000);

再來測試一下,這時(shí)候發(fā)現(xiàn)頁面渲染沒問題, 并且也能響應(yīng)用戶操作, 比如點(diǎn)擊事件了.

hydrate
經(jīng)過上面的5步, 看起來沒問題了, 但是我們的控制臺會(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)上。

也就是說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ā)起請求, 收到服務(wù)端發(fā)送的html, 進(jìn)行解析和展示
  • 客戶端加載js等資源文件
  • 客戶端執(zhí)行js文件, 完成hydrate操作
  • 客戶端接管整體應(yīng)用

路由

客戶端渲染時(shí), React提供了BrowserRouter和HashRouter來供我們處理路由, 但是他們都依賴window對象, 而在服務(wù)端是沒有window的。
但是react-router提供了StaticRouter, 為我們的服務(wù)端渲染做服務(wù)。
接下來我們模擬添加幾個(gè)頁面, 實(shí)現(xiàn)一下路由的功能。

構(gòu)造Login和User兩個(gè)頁面

//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)象,在頁面上通過url修改路由到Login的時(shí)候,界面上登錄兩個(gè)字一閃即逝,這是為啥呢?
因?yàn)殡m然服務(wù)端路由配置好了,也確實(shí)模塊嵌入進(jìn)來了,但是?。?!客戶端還沒有進(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")
);

分別訪問一下/user和/login,發(fā)現(xiàn)已經(jīng)可以正常渲染了,但是!??!明明是一樣的映射規(guī)則,只是路由根組件不一樣,還要寫兩遍也太折磨了,于是有了接下來的路由同構(gòu)

路由同構(gòu)

既要在客戶端寫一遍路由, 也要在服務(wù)端寫一遍路由, 有沒有什么方法能只寫一遍? 就像app.js一樣?
所以我們先找一下兩端路由的異同:

  • 共同點(diǎn):路徑和組件的映射關(guān)系是相同的
  • 不同點(diǎn):路由引用的組件不一樣, 或者說實(shí)現(xiàn)的方式不一樣

路徑和組件之間的關(guān)系可以用抽象化的語言去描述清楚,也就是我們所說路由配置化。
最后我們提供一個(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走對應(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用以處理重定向問題
  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);

重定向問題

這里我們從/重定向到/user的時(shí)候, 可以看到html返回的內(nèi)容和實(shí)現(xiàn)頁面渲染的內(nèi)容是不一樣的。
這代表重定向操作是客戶端來完成的, 而我們期望的是先訪問index.html請求, 返回302, 然后出現(xiàn)一個(gè)新的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}) );
  //當(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í)候我們再測試一下, 就會(huì)發(fā)現(xiàn)符合預(yù)期, 出現(xiàn)了兩個(gè)請求, 一個(gè)302, 一個(gè)user.html

404問題

我們隨便輸入一個(gè)不存在的路由, 發(fā)現(xiàn)內(nèi)容是如期返回了404, 但是請求確實(shí)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}) );
  //當(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頁面
    if (staticContext) staticContext.NOT_FOUND = true;
    return <NotFound/>
}

到此,完整的功能已經(jīng)實(shí)現(xiàn)!

到此這篇關(guān)于React服務(wù)端渲染和同構(gòu)的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)React服務(wù)端渲染和同構(gòu)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 詳解react-router4 異步加載路由兩種方法

    詳解react-router4 異步加載路由兩種方法

    本篇文章主要介紹了詳解react-router4 異步加載路由兩種方法 ,具有一定的參考價(jià)值,有興趣的可以了解一下
    2017-09-09
  • 詳解React中的todo-list

    詳解React中的todo-list

    這篇文章主要介紹了React中的todo-list的相關(guān)知識,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧
    2018-07-07
  • react-native消息推送實(shí)現(xiàn)方式

    react-native消息推送實(shí)現(xiàn)方式

    這篇文章主要介紹了react-native消息推送實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-02-02
  • 詳解React中函數(shù)式組件與類組件的不同

    詳解React中函數(shù)式組件與類組件的不同

    React?函數(shù)式組件與類組件的主要區(qū)別在于它們的定義和聲明方式以及它們之間的一些特性,所以本文就詳細(xì)的給大家講講React中函數(shù)式組件與類組件有何不同,需要的朋友可以參考下
    2023-09-09
  • React?Streaming?SSR原理示例深入解析

    React?Streaming?SSR原理示例深入解析

    這篇文章主要為大家介紹了React?Streaming?SSR原理示例深入解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-12-12
  • React實(shí)現(xiàn)點(diǎn)擊刪除列表中對應(yīng)項(xiàng)

    React實(shí)現(xiàn)點(diǎn)擊刪除列表中對應(yīng)項(xiàng)

    本文主要介紹了React 點(diǎn)擊刪除列表中對應(yīng)項(xiàng)的方法。具有一定的參考價(jià)值,下面跟著小編一起來看下吧
    2017-01-01
  • React中使用Axios發(fā)起POST請求提交文件方式

    React中使用Axios發(fā)起POST請求提交文件方式

    這篇文章主要介紹了React中使用Axios發(fā)起POST請求提交文件方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-02-02
  • Remix集成antd和pro-components的過程示例

    Remix集成antd和pro-components的過程示例

    這篇文章主要為大家介紹了Remix集成antd和pro-components的過程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-03-03
  • 圖文示例講解useState與useReducer性能區(qū)別

    圖文示例講解useState與useReducer性能區(qū)別

    這篇文章主要為大家介紹了useState與useReducer性能區(qū)別圖文示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-05-05
  • React?Fiber構(gòu)建源碼解析

    React?Fiber構(gòu)建源碼解析

    這篇文章主要為大家介紹了React?Fiber構(gòu)建源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-02-02

最新評論