漫谈 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)
复制代码

参考文献

相关文章
相关标签/搜索