#ReactApp項(xiàng)目構(gòu)建流程【2】

ReactApp項(xiàng)目構(gòu)建流程【2】

React服務(wù)端渲染

  • 為什么會(huì)有服務(wù)端渲染?
    • webapp開(kāi)發(fā)模式很多框架都由瀏覽器渲染HTML內(nèi)容,而seo抓取url內(nèi)容時(shí)并不會(huì)執(zhí)行js代碼蔫浆,抓取webapp時(shí)得到的是一個(gè)HTML[空內(nèi)容]+js[內(nèi)容寫(xiě)在js中],seo難以進(jìn)行推廣
    • 并且由瀏覽器渲染的webapp HTML內(nèi)容必須等到j(luò)s文件加載完成,首次請(qǐng)求時(shí)等待時(shí)間會(huì)比較長(zhǎng)紊浩,體驗(yàn)差
    • React團(tuán)隊(duì)運(yùn)用nodejs,使得用react構(gòu)建的app能夠在nodejs環(huán)境中進(jìn)行渲染句携,以得到內(nèi)容,再返回給瀏覽器愉豺,此時(shí)的HTML就能夠被seo以及爬蟲(chóng)抓取篓吁,用戶等待時(shí)間也會(huì)變短

服務(wù)端渲染準(zhǔn)備

工具 react-dom下的server模塊

 reat-dom是React專門(mén)為web端開(kāi)發(fā)的渲染工具。
 
 在客戶端蚪拦,我們可以使用react-dom的render方法渲染組件
 
 在服務(wù)端杖剪,react-dom/server提供我們將react組件渲染成HTML的方法

基礎(chǔ)配置

  • 打開(kāi)我們之前做好的項(xiàng)目,這里是之前的項(xiàng)目鏈接驰贷,未上傳至github

  • 首先打開(kāi)app.js盛嘿,發(fā)現(xiàn)之前是將<App />這個(gè)JSX標(biāo)簽render(中文:傳遞)到document.body中的,而問(wèn)題是在服務(wù)端中并沒(méi)有瀏覽器環(huán)境括袒,自然也就沒(méi)有document這個(gè)dom對(duì)象

    • 同級(jí)目錄下新建server-entry.js,將需要服務(wù)端渲染的內(nèi)容export出去
      import React from 'react' //用到了JSX就需要import 'react'
      import App from './App.jsx' //需要添加 [./],因?yàn)槟J(rèn)的import目錄是node_modules
      export default <App /> //返回的應(yīng)該是jsx語(yǔ)法再轉(zhuǎn)義
      至此一個(gè)簡(jiǎn)單的server-entry.js文件就寫(xiě)好了
      該文件的作用:在服務(wù)端渲染時(shí)使用該文件次兆,要把該文件單獨(dú)打包出來(lái),因?yàn)槭荍SX語(yǔ)法锹锰,需要用babel

    • webpack.config.js同級(jí)建立webpack.config.server.js
      內(nèi)容拷貝自webpack.config.js芥炭,與entry選項(xiàng)同級(jí)添加

       target:"node",
           //指定webpack打包出來(lái)的文件是使用在哪個(gè)執(zhí)行環(huán)境中的,選項(xiàng)有且不僅有web/node/...
       
       entry中的app修改為server-entry.js
       
       output中的filename修改為'server-entry.js'而非hash化文件名
           //此項(xiàng)可用于輸出版本號(hào)用于緩存更新恃慧,而服務(wù)端無(wú)瀏覽器緩存概念蚤认,并且這會(huì)加大import難度
           并且添加一項(xiàng)libraryTarget:"commonjs2" 
               //打包出js時(shí)使用的模塊方案,有多種global/cmd/amd/umd/commjs/...糕伐,commonjs2適用于node端
       
       刪除plugins:[new HTMLPlugin()],
           同時(shí)刪除其引用const HTMLPlugin=require('html-webpack-plugin')
           因?yàn)榉?wù)端負(fù)責(zé)渲染而非輸出HTML文件
      
  • 配置完上步之后就可以把上部分的js打包出來(lái)蘸嘶,為方便后續(xù)打包良瞧,修改下package.json配置

         * 刪除`scripts`下的`test`,當(dāng)前暫時(shí)用不到
             "test": "echo \"Error: no test specified\" && exit 1",
             
         * 為分別build`客戶端`和`服務(wù)端`以及區(qū)分不同端文件训唱,在`scripts`添加兩個(gè)`script`并修改`build`用于褥蚯,將`webpack.config.js`修改為下端代碼中的文件名
             "build:client":"webpack --config webpack.config.client.js",
             "build:server":"webpack --config webpack.config.server.js",
             "clear":"rimraf dist",
             //用于每次build時(shí)覆蓋dist目錄,該包專門(mén)用于刪除文件夾况增,需要安裝[該段第五步]
             "build":"npm run clear && npm run build:client && npm run build:server"
             //使用npm 按順序 run clear赞庶,build:client 和build:server 腳本
             
             $npm i rimraf -D
             //安裝完會(huì)自動(dòng)更新package.json文件依賴關(guān)系
    
寫(xiě)到這里我們可以發(fā)現(xiàn)npm run這個(gè)命令其實(shí)是是運(yùn)行package.json中的scripts中相應(yīng)的script片段的key-value中的value,比如npm run clear實(shí)際是運(yùn)行npm run rimraf dist
  • npm run build
    編譯后可以在output目錄dist中,分別看到客戶端js文件[name].[hash].js以及服務(wù)端js文件server-entry.js
    對(duì)比可以發(fā)現(xiàn)server-entry.js在頂部使用了module.exports這一node語(yǔ)法,因此可以發(fā)現(xiàn)是可以被node執(zhí)行各種操作包括渲染的

  • root下newserver文件夾用于添加node server歧强,用到express澜薄,需要安裝,$npm i express -S摊册,因?yàn)樵摲?wù)是正常服務(wù)中需要用到的肤京,所以要使用-S安裝到依賴模塊當(dāng)中
    ```
    安裝好后在server文件夾內(nèi)新建server.js并加入以下代碼

    const express=require('express');                //node方式引入expeess
    const ReactSSR=require('react-dom/server');      //node方式引入react-dom/serve
    const serverEntry=require('../dist/server-entry').default;
    //此處為何需要添加.default呢,因?yàn)槿绻惶砑觗efault時(shí)茅特,下面的打印說(shuō)明了問(wèn)題
    const app=express();
    console.log(serverEntry);
    /*會(huì)發(fā)現(xiàn)打印出來(lái)的是這么個(gè)東西忘分,其中default中的東西才是nodeServer需要渲染的東西
    { __esModule: true,
         default:
             { 
                '$$typeof': Symbol(react.element),
                type: [Function: App],
                key: null,
                ref: null,
                props: {},
                 _owner: null,
                _store: {} 
               }
      }
    */
    因?yàn)楫?dāng)前使用的是commonjs2模塊方案,而commonjs2默認(rèn)使用export.default來(lái)export模塊白修,
    可以回到server-entry.js看下代碼
    
    export default <APP /> 對(duì)應(yīng)的引入方法 import app from './App.jsx'】
    export const app=App   對(duì)應(yīng)的引入方法 import {app} from '..'     】->ES6中解構(gòu)的寫(xiě)法
    node中使用的是require引入妒峦,require默認(rèn)不會(huì)讀取default內(nèi)容
                 
     app.get('*',function (req,res) {
            const appString=ReactSSR.renderToString(serverEntry);
            res.send(appString)
         });
              
         app.listen(3000,function () {         //監(jiān)聽(tīng)端口,成功函數(shù)
             console.log('server is listening')
         })
    
  • $npm start
    ```
    進(jìn)入http://localhost:3000端口兵睛,打開(kāi)network查看Headers以及Response

    至此最簡(jiǎn)單的服務(wù)端渲染已經(jīng)完成肯骇,此時(shí)的返回內(nèi)容html少以及未引用客戶端的業(yè)務(wù)代碼js
    為解決該問(wèn)題,需要把服務(wù)端渲染出來(lái)的內(nèi)容(當(dāng)前為server-entry.js)插入到index.html
    (html-webpack-plugin插件通過(guò)編譯成的HTML)的body當(dāng)中并整體返回body卤恳,這才算打通一個(gè)整的
    服務(wù)端渲染過(guò)程
    
  • 添加模板文件template.html以及修改webpack.config.client.js配置

    在client目錄下新建`template.html`,在body中添加以下代碼
    
    <div id="root"><app></app></div>
    //這個(gè)名為root的div就可以替代之前app.js中App組件掛載的document.body了累盗,而內(nèi)部的app標(biāo)簽則是用于覆蓋,下面講怎么覆蓋
    
    在webpack.config.client.js的HTMLPlugin中添加剛才新建的template模板
         {
           template:path.join(__dirname,'../client/template.html')
         }
         
     app.js中之前掛載的document.body修改為document.getElementById('root')
     ReactDOM.render(<App />,document.body)
     ->
     ReactDOM.render(<App />,document.getElementById('root'))
     //把渲染出來(lái)的內(nèi)容render到root節(jié)點(diǎn)中
    
    
  • 配置好template后突琳,在server.js中讀取template.html并且替換掉<app>標(biāo)簽

    const fs=require('fs')            //node文件模塊
    const path=require('path')        //node路徑模塊
    const template=fs.readFileSync(path.join(__dirname,'../client/template.html'),'utf-8')    
    //同步讀取絕對(duì)路徑文件若债,并且以u(píng)tf-8格式輸出,不指定的話回默認(rèn)輸出buffer格式
               
    //修改app的get方法
    app.get('*',function (req,res) {
           const appString=ReactSSR.renderToString(serverEntry);
           res.send(template.replace('<app></app>',appString));//修改用于替換掉<app>標(biāo)簽
    });
    
    
  • $npm run build ->$npm start

    進(jìn)入http://localhost:3000拆融,成功蠢琳,但是network時(shí)發(fā)現(xiàn)請(qǐng)求localhost和請(qǐng)求js文件時(shí)返回結(jié)果一樣,
    也就是說(shuō)有資源被重復(fù)返回的情況出現(xiàn)
    因?yàn)槲覀兊姆?wù)接受到的所有請(qǐng)求都會(huì)返回服務(wù)端渲染的內(nèi)容
    針對(duì)此問(wèn)題镜豹,需要添加配置以確定哪些是不需要多次返回的靜態(tài)資源文件
                    
    app.use('/public',express.static(path.join(__dirname,'../dist')));
    //express中為我們提供的專門(mén)處理此問(wèn)題的模塊傲须,用于確定哪些文件夾下的資源是靜態(tài)文件
    因?yàn)槲覀兊腸lient下的所有內(nèi)容都在webpack.config.client.js中配置編譯到dist(output->path)下,
    所以此處的靜態(tài)文件路徑就join dist趟脂,其中的publicPath在上次在跟走代碼時(shí)置為空了泰讽,現(xiàn)在重新加回去,
    webpack.config.server.js中也需要加回去
                    
    #我們可以在編譯好的index.html中查看資源路徑是否正確
    
             
    
  • $npm run build ->$npm start

    瀏覽器查看昔期,無(wú)異常已卸,但是有個(gè)warning
        app.cad52c1053474fca53c1.js:14238 Warning: render(): Calling ReactDOM.render() to 
    hydrate server-rendered markup will stop working in React v17. Replace the ReactDOM.render() 
    call with ReactDOM.hydrate() if you want React to attach to the server HTML
                
    這是在react16中,原本是使用ReactDom.render方法去渲染硼一,新加了一個(gè)方法累澡,如果我們使用了服務(wù)端渲染,那么需要使用hydrate()在客戶端的js中去渲染客戶端中的內(nèi)容般贼,因?yàn)閞eact會(huì)比對(duì)server和client生成的代碼愧哟。如果有差別奥吩,則會(huì)使用客戶端的代碼
                
    替換app.js中的render為hydrate()
        ReactDOM.render(<App />,document.getElementById('root'));
        ->
        ReactDOM.hydrate(<App />,document.getElementById('root'));
             
    

小結(jié)

有時(shí)間再寫(xiě)吧
問(wèn)題:
    * 每次修改client后,都需要 npm run build
    * 每次修改server后蕊梧,都需要npm start
    * 下篇文章再接觸此內(nèi)容
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末霞赫,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子望几,更是在濱河造成了極大的恐慌绩脆,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件橄抹,死亡現(xiàn)場(chǎng)離奇詭異靴迫,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)楼誓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)玉锌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人疟羹,你說(shuō)我怎么就攤上這事主守。” “怎么了榄融?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵参淫,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我愧杯,道長(zhǎng)涎才,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任力九,我火速辦了婚禮耍铜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘跌前。我一直安慰自己棕兼,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布抵乓。 她就那樣靜靜地躺著伴挚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪灾炭。 梳的紋絲不亂的頭發(fā)上章鲤,一...
    開(kāi)封第一講書(shū)人閱讀 51,631評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音咆贬,去河邊找鬼。 笑死帚呼,一個(gè)胖子當(dāng)著我的面吹牛掏缎,可吹牛的內(nèi)容都是我干的皱蹦。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼眷蜈,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼沪哺!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起酌儒,我...
    開(kāi)封第一講書(shū)人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤辜妓,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后忌怎,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體籍滴,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年榴啸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了孽惰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鸥印,死狀恐怖勋功,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情库说,我是刑警寧澤狂鞋,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站潜的,受9級(jí)特大地震影響骚揍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜夏块,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一疏咐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧脐供,春花似錦浑塞、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至歇由,卻和暖如春卵牍,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背沦泌。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工糊昙, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谢谦。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓释牺,卻偏偏與公主長(zhǎng)得像萝衩,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子没咙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容