溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

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

發(fā)布時間:2022-04-27 13:37:07 來源:億速云 閱讀:143 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要講解了“React服務(wù)端渲染和同構(gòu)怎么實現(xiàn)”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“React服務(wù)端渲染和同構(gòu)怎么實現(xiàn)”吧!

    背景

    第一階段

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

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

    第二階段

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

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

    第三階段

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

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

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

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

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

    renderToString

    首先來看看他是個什么東西

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

    它可以渲染一個react元素/組件到頁面中,而且只能用到服務(wù)端
    所以spa react-dom -> render 相對應(yīng)的就是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);//返回結(jié)果
    })
    
    app.listen(PORT,() => {
        console.log(`server is listening on ${PORT}`);
    })

    啟動服務(wù)端之后,手動網(wǎng)頁訪問本地對應(yīng)的端口

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

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

    webpack配置

    應(yīng)用寫好之后,需要瀏覽器端的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這類的包打進(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)用都會有的public目錄,直接拷貝到dist目錄下
        new CopyWebpackPlugin([{
          from:path.resolve(__dirname,'../public'),
          to:path.resolve(__dirname,'../dist')
        }])
      ]
    }

    cli用習(xí)慣了,寫配置有點折磨,寫好之后要怎么去使用呢?package.json配置運(yùn)行腳本:

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

    那么,先打個包

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

    可以看到,已經(jīng)打包出來了一大堆看不懂的東西
    這個時候,運(yùn)行起來即可
    到現(xiàn)在寫了這么多配置,其實只是為了讓服務(wù)端支持一下瀏覽器端基本的運(yùn)行配置/環(huán)境

    給h2標(biāo)簽綁定一個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);

    這個時候如果你去跑一下,會發(fā)現(xiàn)點擊的時候,根 本 沒 反 應(yīng) !
    這個時候稍微想一下,renderToString是把元素轉(zhuǎn)成字符串而已, 事件什么的根本沒有綁定

    這個時候同構(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 <h2 onClick={this.handleClick}>Hello World!</h2>;
        }
    };
    
    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>
        `);//手動創(chuàng)建根節(jié)點,把App標(biāo)簽內(nèi)容引進(jìn)來
    });
    app.listen(3000);

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

    hydrate
    經(jīng)過上面的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()的區(qū)別就是:

    ReactDOM.render()會將掛載dom節(jié)點的所有子節(jié)點全部清空掉,再重新生成子節(jié)點。

    ReactDOM.hydrate()則會復(fù)用掛載dom節(jié)點的子節(jié)點,并將其與react的virtualDom關(guān)聯(lián)上。

    也就是說ReactDOM.render()會將服務(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)用

    路由

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

    構(gòu)造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>
      }
    }

    添加服務(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);

    這個時候會發(fā)現(xiàn)一個現(xiàn)象,在頁面上通過url修改路由到Login的時候,界面上登錄兩個字一閃即逝,這是為啥呢?
    因為雖然服務(wù)端路由配置好了,也確實模塊嵌入進(jìn)來了,但是?。。?strong>客戶端還沒有進(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)可以正常渲染了,但是?。。∶髅魇?strong>一樣的映射規(guī)則,只是路由根組件不一樣,還要寫兩遍也太折磨了,于是有了接下來的路由同構(gòu)

    路由同構(gòu)

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

    • 共同點:路徑和組件的映射關(guān)系是相同的

    • 不同點:路由引用的組件不一樣, 或者說實現(xiàn)的方式不一樣

    路徑和組件之間的關(guān)系可以用抽象化的語言去描述清楚,也就是我們所說路由配置化。
    最后我們提供一個轉(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ā)重定向時,統(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,因為創(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的時候, 可以看到html返回的內(nèi)容和實現(xiàn)頁面渲染的內(nèi)容是不一樣的。
    這代表重定向操作是客戶端來完成的, 而我們期望的是先訪問index.html請求, 返回302, 然后出現(xiàn)一個新的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被使用時,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);

    這時候我們再測試一下, 就會發(fā)現(xiàn)符合預(yù)期, 出現(xiàn)了兩個請求, 一個302, 一個user.html

    404問題

    我們隨便輸入一個不存在的路由, 發(fā)現(xiàn)內(nèi)容是如期返回了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}) );
      //當(dāng)Redirect被使用時,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/>
    }

    感謝各位的閱讀,以上就是“React服務(wù)端渲染和同構(gòu)怎么實現(xiàn)”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對React服務(wù)端渲染和同構(gòu)怎么實現(xiàn)這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI