漫談 Webpack 之服務端渲染、客戶端渲染和同構

前端頁面渲染髮展史

服務端渲染

頁面由服務端直接返回給瀏覽器,路由爲服務端路由,URL 的變動會刷新頁面,原理與 ASP,PHP 等傳統後端框架相似。javascript

客戶端渲染(CSR)

頁面在 JavaScript,CSS 等資源文件加載完畢後開始渲染,路由爲客戶端路由,也就是咱們常常談到的 SPA(Single Page Application)。css

同構(即 SSR)

編寫的 JavaScript 代碼可同時運行在瀏覽器及 Node.js 兩套環境中,用服務端渲染來提高首屏的加載速度,首屏以後的路由由客戶端控制,即在用戶到達首屏後,整個應用還是一個 SPA。html

在明確了這三種渲染方案的具體含義後,咱們能夠發現,不管是客戶端渲染仍是服務端渲染,都有着其明顯的缺陷,而同構顯然是結合了兩者優勢以後的一種更好的解決方案。前端

CSR 和 SSR 優缺點

渲染模式 優勢 缺點
CSR 網絡傳輸數據量小、減小服務器壓力、先後端分離、局部刷新、無需每次請求完整頁面、交互好可實現各類效果 不利於 SEO、爬蟲看不到完整的程序源碼、首屏渲染慢
SSR 首屏渲染快、利於 SEO、能夠生成緩存片斷、生成靜態文件、節能(相比客戶端渲染的耗電) 用戶體驗較差、不易維護

使用 SSR 技術的主要因素

  1. CSR 項目中 TTFP (Time To First Page)時間比較長,參考以前的圖例,在 CSR 的頁面渲染流程中,首先要加載 HTML 文件,以後要下載頁面所需的 JavaScript 文件,而後 JavaScript 文件渲染生成頁面。在這個渲染過程當中至少涉及到兩個 HTTP 請求週期,因此會有必定的耗時,這也是爲何你們在低網速下訪問普通的 React 或者 Vue 應用時,初始頁面會有出現白屏的緣由。
  2. CSR 項目的 SEO 能力極弱,在搜索引擎中基本上不可能有好的排名。由於目前大多數搜索引擎主要識別的內容仍是 HTML,對 JavaScript 文件內容的識別都還比較弱。若是一個項目的流量入口來自於搜索引擎,這個時候你使用 CSR 進行開發,就很是不合適了。

啓用 SSR 技術的架構圖

爲何會出現 SSR

SSR 的工程中,React 代碼會在客戶端和服務器端各執行一次。你可能會想到,這沒什麼問題,由於都是 JavaScript 代碼,既能夠在 Node 執行,也能夠在瀏覽器上運行。可是若是你的代碼操做了 DOM,那就有問題了。由於 Node 沒有 DOM。java

好在 React 引入虛擬 DOM 的概念,虛擬 DOM 是真實 DOM 的一個 JavaScript 對象,React 在作頁面操做時,實際上不是操做真實 DOM,而是操做虛擬 DOM,也就是操做普通對象。在服務器,我能夠操做 JavaScript 對象,判斷環境是服務器環境,咱們把虛擬 DOM 轉換成字符串輸出;在客戶端,也一樣能夠判斷是客戶端環境,直接將虛擬 DOM 轉成真實 DOM,完成頁面渲染。react

如何使用 SSR 技術

因爲 SSR 的代碼與 CSR 的代碼不同,因此咱們新建 webpack.server.js 文件而且修改 React 代碼。webpack

  1. 假使你已經擁有 express 服務器,裏面代碼以下:web

    var express = require('express');
    var app = express();
    
    app.get('/', (req, res)=>{
        res.send(` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Hello Wrold</title> </head> <body> <div>Hello Wrold</div> </body> </html> `)
    })
    app.listen(8569)
    複製代碼
  2. 啓動代碼,打開 localhost:8569 能夠看到頁面上顯示了 Hello Wroldexpress

  3. 修改 webpack 與 react 代碼json

    libraryTarget 配置如何暴露 library。 umd 意味着將你的 library 暴露爲全部的模塊定義下均可運行的方式。它將在 CommonJS、AMD 環境下運行,或將模塊導出到 global 下的變量。

    // webpack
    ...
    
    module.exports = {
        mode: 'production',
        entry: './src/module/demo.js',
        output: {
            filename: 'demo-server.js',
            path: path.join(__dirname, 'dist'),
            libraryTarget: 'umd'
        },
        ...
    }
    複製代碼
    //react
    const React = require('react');
    class Box extends React.Component{
        render(){
            return (
                <div>This is a box</div>
            )
        }
    }
    module.exports = <Box /> 複製代碼
  4. 修改 express 代碼

    Node 沒有 window,須要 hack

    if(typeof window === 'undefined'){
        global.window = {}
    }
    ...
    const SSR = require('../dist/demo-server.js');
    const { renderToString } = require('react-dom/server');
    app.get('/', (req, res)=>{
        res.end(` ... <body> ${renderToString(SSR)} </body> ... `)
    })
    
    app.listen(8569)
    複製代碼
  5. 啓動代碼,打開 localhost:8569,你會看到下圖所示。

  1. 大功告成,一個簡單的 SSR 已經實現了。

樣式解析與初始數據加載

你們都知道 Node 沒有樣式解析,也沒有 XMLHttpRequest,這些咱們在作 SSR 的時候應該怎麼處理?咱們能夠用客戶端打出來的代碼部署靜態服務器,在 .html 的代碼裏面加入佔位符,而後在服務器端作佔位符替換。這樣,當客戶端訪問頁面的時候,咱們經過外鏈加載樣式,再經過插入 json 對象來獲取初始頁面數據。

外鏈加載樣式部分代碼

// 打包後的 html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>demo</title>
<link href="main.77f6fd9c.css" rel="stylesheet"></head>
<body>
    <div id="root"><!-- html-placeholder --></div>
<script type="text/javascript" src="https://lib.baomitu.com/react/16.8.6/umd/react.development.js"></script><script type="text/javascript" src="https://lib.baomitu.com/react-dom/16.8.6/umd/react-dom.development.js"></script><script type="text/javascript" src="demo-server.js"></script></body>
</html>
複製代碼
// express
if(typeof window === 'undefined'){
    global.window = {}
}

const express = require('express');
const app = express();
const SSR = require('../dist/demo-server.js');
const { renderToString } = require('react-dom/server');
const fs = require('fs');
const path = require('path');

const template = fs.readFileSync(path.join(__dirname, '../dist/demo.html'), 'utf-8');

app.use(express.static('../dist/'))

app.get('/', (req, res)=>{
    let html = template.replace('<!-- html-placeholder -->', renderToString(SSR));
    res.end(html)
})

app.listen(8569)
複製代碼

參考文獻

相關文章
相關標籤/搜索