名词(术语)了解--SSR/CSR

名词(术语)了解–SSR/CSR

什么是服务器端渲染(SSR)?

服务器端渲染是指由服务器生成完整的 HTML 页面,然后发送给客户端的过程。

这与客户端渲染(CSR)形成对比,后者主要依赖浏览器端的 JavaScript 来渲染页面内容。
在这里插入图片描述

客户端服务器数据库1. 发送页面请求2. 获取必要数据3. 返回数据4. 执行React/Vue组件代码5. 生成HTML6. 返回完整HTML7. 显示内容8. 加载JS9. 激活(Hydration)客户端服务器数据库

详情

  1. 客户端发送页面请求

    • 用户访问网站 URL
    • 浏览器向服务器发送 HTTP 请求
  2. 服务器请求数据

    // 服务器端代码
    async function fetchData() {
      const data = await db.query('SELECT * FROM posts');
      return data;
    }
    
  3. 数据库返回数据

    // 数据示例
    const data = {
      posts: [
        { id: 1, title: '文章1' },
        { id: 2, title: '文章2' }
      ]
    };
    
  4. 服务器执行组件代码

    // React 组件
    function App({ data }) {
      return (
        <div>
          {data.posts.map(post => (
            <article key={post.id}>
              <h2>{post.title}</h2>
            </article>
          ))}
        </div>
      );
    }
    
  5. 生成 HTML

    // 服务器端渲染
    const html = ReactDOMServer.renderToString(
      <App data={data} />
    );
    
  6. 返回完整 HTML

    <!DOCTYPE html>
    <html>
      <head>
        <title>SSR App</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <script>
          window.__INITIAL_STATE__ = ${JSON.stringify(data)}
        </script>
        <script src="/client.js"></script>
      </body>
    </html>
    
  7. 客户端显示内容

    • 浏览器接收到完整的 HTML
    • 用户立即看到页面内容
    • 无需等待 JavaScript 加载
  8. 加载 JavaScript

    // 客户端 JavaScript
    const initialState = window.__INITIAL_STATE__;
    
  9. Hydration 过程

    // 客户端 hydration
    ReactDOM.hydrate(
      <App data={initialState} />,
      document.getElementById('root')
    );
    

SSR 的主要优点:

  1. 更快的首屏加载

    • 用户立即看到完整内容
    • 无需等待 JavaScript 下载和执行
  2. 更好的 SEO

    • 搜索引擎可以直接爬取完整的 HTML 内容
    • 有利于网站的搜索引擎排名
  3. 更好的性能

    • 减少客户端计算负担
    • 适合低性能设备
  4. 更好的用户体验

    • 无空白页面等待
    • 更快的内容可见时间

SSR 的挑战:

  1. 服务器负载

    • 需要更多服务器资源
    • 需要合理的缓存策略
  2. 开发复杂性

    • 需要同构代码(服务端和客户端都能运行)
    • 需要处理服务端特有的问题
  3. 部署要求

    • 需要 Node.js 环境
    • 需要更复杂的部署配置

适用场景:

  • 内容展示型网站
  • 需要 SEO 的网站
  • 首屏加载速度要求高的应用
  • 面向低端设备用户的应用

框架支持:

  • Next.js (React)
  • Nuxt.js (Vue)
  • SvelteKit (Svelte)
  • Remix (React)

什么是客户端渲染(CSR)?

在这里插入图片描述

浏览器服务器React应用API服务器1. 请求页面2. 返回空HTML和JS包3. 加载JS包4. 初始化React应用5. 组件挂载6. 发起API请求7. 返回数据8. 更新状态9. 渲染内容10. 页面可交互浏览器服务器React应用API服务器

CSR(客户端渲染)的工作流程说明:

  1. 初始请求

    • 用户在浏览器中输入URL或点击链接
    • 浏览器向服务器发送页面请求
  2. 服务器响应

    • 服务器返回一个基本的空HTML文件
    • HTML文件中包含必要的JS包引用(React应用代码)
  3. 加载过程

    • 浏览器下载JS包
    • 这个阶段用户看到的是空白页面或加载指示器
  4. 应用初始化

    • JS包加载完成后,React应用开始初始化
    • 创建虚拟DOM和应用状态
  5. 组件挂载

    • React组件树开始挂载
    • 初始化组件生命周期
  6. 数据获取

    • 组件挂载后发起API请求
    • 向后端服务器请求需要的数据
  7. 数据响应

    • API服务器返回请求的数据
    • 数据以JSON格式传输
  8. 状态更新

    • React组件接收到数据
    • 更新组件状态(setState)
  9. 内容渲染

    • React根据新状态重新渲染组件
    • 更新实际DOM
  10. 完成显示

    • 用户最终看到完整的页面内容
    • 页面可以交互

    详情:

    1. 浏览器请求页面

      • 用户访问网站 URL
      • 浏览器向服务器发送 HTTP 请求
    2. 服务器响应

      • 返回基础 HTML 文件(通常只包含一个 root div)
      • 返回打包后的 JS 文件(包含 React 应用代码)
      <!DOCTYPE html>
      <html>
        <head>
          <title>React App</title>
        </head>
        <body>
          <div id="root"></div>
          <script src="/static/js/bundle.js"></script>
        </body>
      </html>
      
    3. JS 包加载

      • 浏览器下载 JavaScript 文件
      • 解析并执行 JavaScript 代码
      • 此时用户看到空白页面
    4. React 初始化

      ReactDOM.createRoot(
        document.getElementById('root')
      ).render(<App />);
      
    5. 组件挂载

      function App() {
        useEffect(() => {
          // 组件挂载后执行
        }, []);
        
        return <div>Loading...</div>;
      }
      
    6. 数据请求

      const [data, setData] = useState(null);
      
      useEffect(() => {
        fetch('/api/data')
          .then(res => res.json())
          .then(data => setData(data));
      }, []);
      
    7. 接收数据

      {
        "status": "success",
        "data": {
          "items": [...]
        }
      }
      
    8. 更新状态

      setData(receivedData);
      
    9. 渲染内容

      return (
        <div>
          {data ? (
            <DataDisplay data={data} />
          ) : (
            <Loading />
          )}
        </div>
      );
      
    10. 页面可交互

      • 用户可以看到完整内容
      • 可以进行点击、输入等交互操作

    CSR 的关键特点:

    1. 首次加载时需要下载完整的 JavaScript 包
    2. 所有页面路由和视图转换都在客户端处理
    3. 数据获取和页面更新都是异步进行的
    4. 适合构建单页应用(SPA)
    5. 需要考虑首屏加载优化:
      • 代码分割(Code Splitting)
      • 懒加载(Lazy Loading)
      • 预加载(Preloading)
      • 合理的缓存策略

CSR的优缺点:

优点:

  • 前后端完全分离
  • 用户体验好,切换页面快
  • 减轻服务器压力
  • 客户端缓存友好

缺点:

  • 首屏加载较慢
  • SEO不友好
  • 对JavaScript依赖性强
  • 在低性能设备上体验欠佳

适用场景:

  • 后台管理系统
  • 交互密集型应用
  • 实时数据展示
  • 不需要SEO的应用

不适用场景:

  • 需要良好SEO的网站
  • 首屏加载速度要求极高的页面
  • 低端设备用户较多的应用

SSR的主要特点

  1. 渲染过程

    • 在服务器端完成页面的渲染
    • 生成完整的HTML文档
    • 客户端接收到的是完整的页面内容
  2. 性能特征

    • 更快的首屏加载时间
    • 更好的SEO表现
    • 较高的服务器资源消耗
  3. 适用场景

    • 内容密集型网站
    • 需要良好SEO的网站
    • 首屏加载速度要求高的应用

常见SSR框架

  1. Next.js

    • React生态系统中最流行的SSR框架
    • 提供了自动静态优化
    • 支持增量静态生成(ISR)
  2. Nuxt.js

    • Vue.js的SSR框架
    • 提供自动路由配置
    • 支持静态站点生成
  3. Angular Universal

    • Angular的SSR解决方案
    • 支持预渲染
    • 提供服务器端API

使用建议

  1. 何时使用SSR

    • 网站需要优秀的SEO表现
    • 用户期望快速的首屏加载
    • 网站内容频繁更新
    • 网站有大量动态内容
  2. 何时避免使用SSR

    • 应用交互性很强
    • 服务器资源有限
    • 内容更新频率低
    • 主要面向已登录用户
  3. 性能优化建议

    • 实施缓存策略
    • 使用CDN
    • 合理配置服务器资源
    • 优化数据获取逻辑

SSR场景示例

1. 博客文章页面 (Next.js)

这是一个典型的内容展示场景,非常适合使用SSR:

id: blog-ssr
name: Blog Post with SSR
type: tsx
content: |-
  // pages/posts/[id].tsx
  import React from 'react'
  import { GetServerSideProps } from 'next'
  
  interface Post {
    id: number
    title: string
    content: string
    author: string
    publishDate: string
  }
  
  interface BlogPostProps {
    post: Post
  }
  
  export const getServerSideProps: GetServerSideProps = async ({ params }) => {
    // 在服务器端获取文章数据
    const res = await fetch(`https://api.example.com/posts/${params?.id}`)
    const post = await res.json()
    
    return {
      props: {
        post
      }
    }
  }
  
  const BlogPost: React.FC<BlogPostProps> = ({ post }) => {
    return (
      <div className="max-w-3xl mx-auto p-6">
        <h1 className="text-3xl font-bold mb-4">{post.title}</h1>
        <div className="text-gray-600 mb-4">
          <span>作者:{post.author}</span>
          <span className="ml-4">发布时间:{post.publishDate}</span>
        </div>
        <div className="prose">
          {post.content}
        </div>
      </div>
    )
  }
  
  export default BlogPost

2. 电商产品列表 (Next.js)

电商场景需要良好的SEO和快速的首屏加载:

id: ecommerce-ssr
name: E-commerce Product List
type: tsx
content: |-
  // pages/products.tsx
  import React from 'react'
  import { GetServerSideProps } from 'next'
  import { Select } from "@/components/ui/select"
  
  interface Product {
    id: number
    name: string
    price: number
    description: string
    image: string
  }
  
  interface ProductsPageProps {
    products: Product[]
    categories: string[]
  }
  
  export const getServerSideProps: GetServerSideProps = async ({ query }) => {
    // 获取分类和筛选条件
    const category = query.category || 'all'
    const sort = query.sort || 'default'
    
    // 在服务器端获取商品数据
    const productsRes = await fetch(
      `https://api.example.com/products?category=${category}&sort=${sort}`
    )
    const products = await productsRes.json()
    
    // 获取分类列表
    const categoriesRes = await fetch('https://api.example.com/categories')
    const categories = await categoriesRes.json()
    
    return {
      props: {
        products,
        categories
      }
    }
  }
  
  const ProductsPage: React.FC<ProductsPageProps> = ({ products, categories }) => {
    return (
      <div className="container mx-auto p-6">
        <div className="flex justify-between mb-6">
          <h1 className="text-2xl font-bold">商品列表</h1>
          <Select className="w-48">
            {categories.map(category => (
              <option key={category} value={category}>
                {category}
              </option>
            ))}
          </Select>
        </div>
        
        <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
          {products.map(product => (
            <div key={product.id} className="border rounded-lg p-4">
              <img 
                src={product.image} 
                alt={product.name}
                className="w-full h-48 object-cover mb-4"
              />
              <h2 className="text-xl font-semibold">{product.name}</h2>
              <p className="text-gray-600">{product.description}</p>
              <div className="mt-4 flex justify-between items-center">
                <span className="text-xl text-red-600">¥{product.price}</span>
                <button className="bg-blue-500 text-white px-4 py-2 rounded">
                  加入购物车
                </button>
              </div>
            </div>
          ))}
        </div>
      </div>
    )
  }
  
  export default ProductsPage

3. 新闻门户首页 (Nuxt.js)

新闻网站需要实时性和SEO,很适合SSR:

id: news-ssr
name: News Portal
type: tsx
content: |-
  // pages/index.vue
  <template>
    <div class="container mx-auto p-6">
      <header class="mb-8">
        <h1 class="text-4xl font-bold mb-4">今日头条</h1>
        <div class="flex gap-4">
          <button 
            v-for="category in categories" 
            :key="category"
            class="px-4 py-2 rounded-full"
            :class="selectedCategory === category ? 'bg-blue-500 text-white' : 'bg-gray-200'"
            @click="selectCategory(category)"
          >
            {{ category }}
          </button>
        </div>
      </header>

      <main>
        <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
          <article 
            v-for="news in newsList" 
            :key="news.id"
            class="border rounded-lg overflow-hidden"
          >
            <img :src="news.image" :alt="news.title" class="w-full h-48 object-cover">
            <div class="p-4">
              <h2 class="text-xl font-bold mb-2">{{ news.title }}</h2>
              <p class="text-gray-600 mb-4">{{ news.summary }}</p>
              <div class="flex justify-between items-center text-sm text-gray-500">
                <span>{{ news.source }}</span>
                <span>{{ news.publishTime }}</span>
              </div>
            </div>
          </article>
        </div>
      </main>
    </div>
  </template>

  <script>
  export default {
    async asyncData({ $axios }) {
      const [categories, newsList] = await Promise.all([
        $axios.$get('https://api.example.com/categories'),
        $axios.$get('https://api.example.com/news')
      ])
      
      return {
        categories,
        newsList,
        selectedCategory: 'all'
      }
    },
    
    methods: {
      async selectCategory(category) {
        this.selectedCategory = category
        this.newsList = await this.$axios.$get(
          `https://api.example.com/news?category=${category}`
        )
      }
    },
    
    head() {
      return {
        title: '新闻门户 - 今日头条',
        meta: [
          {
            hid: 'description',
            name: 'description',
            content: '最新、最热门的新闻资讯'
          }
        ]
      }
    }
  }
  </script>

4. 仪表盘页面 (混合渲染)

对于仪表盘这类应用,我们可以使用混合渲染策略:

id: dashboard-ssr
name: Dashboard with Hybrid Rendering
type: tsx
content: |-
  // pages/dashboard.tsx
  import React, { useEffect, useState } from 'react'
  import { GetServerSideProps } from 'next'
  import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts'
  
  interface DashboardData {
    summary: {
      totalUsers: number
      activeUsers: number
      revenue: number
    }
    historicalData: {
      date: string
      users: number
      revenue: number
    }[]
  }
  
  interface DashboardProps {
    initialData: DashboardData
  }
  
  export const getServerSideProps: GetServerSideProps = async () => {
    // 获取初始数据
    const res = await fetch('https://api.example.com/dashboard/initial')
    const initialData = await res.json()
    
    return {
      props: {
        initialData
      }
    }
  }
  
  const Dashboard: React.FC<DashboardProps> = ({ initialData }) => {
    const [realtimeData, setRealtimeData] = useState(null)
    
    useEffect(() => {
      // 客户端连接WebSocket获取实时数据
      const ws = new WebSocket('wss://api.example.com/dashboard/realtime')
      ws.onmessage = (event) => {
        setRealtimeData(JSON.parse(event.data))
      }
      
      return () => ws.close()
    }, [])
    
    return (
      <div className="container mx-auto p-6">
        <h1 className="text-2xl font-bold mb-6">运营数据仪表盘</h1>
        
        <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
          <div className="bg-white rounded-lg shadow p-6">
            <h3 className="text-lg font-semibold mb-2">总用户数</h3>
            <p className="text-3xl">
              {realtimeData?.totalUsers || initialData.summary.totalUsers}
            </p>
          </div>
          <div className="bg-white rounded-lg shadow p-6">
            <h3 className="text-lg font-semibold mb-2">活跃用户</h3>
            <p className="text-3xl">
              {realtimeData?.activeUsers || initialData.summary.activeUsers}
            </p>
          </div>
          <div className="bg-white rounded-lg shadow p-6">
            <h3 className="text-lg font-semibold mb-2">营收(元)</h3>
            <p className="text-3xl">
              {realtimeData?.revenue || initialData.summary.revenue}
            </p>
          </div>
        </div>
        
        <div className="bg-white rounded-lg shadow p-6">
          <h3 className="text-lg font-semibold mb-4">历史趋势</h3>
          <LineChart
            width={800}
            height={400}
            data={initialData.historicalData}
            margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
          >
            <CartesianGrid strokeDasharray="3 3" />
            <XAxis dataKey="date" />
            <YAxis />
            <Tooltip />
            <Line type="monotone" dataKey="users" stroke="#8884d8" />
            <Line type="monotone" dataKey="revenue" stroke="#82ca9d" />
          </LineChart>
        </div>
      </div>
    )
  }
  
  export default Dashboard

各场景特点说明

  1. 博客文章页面

    • 使用 getServerSideProps 在服务器端获取文章数据
    • 完全的SSR渲染,有利于SEO
    • 适合内容不经常变化的场景
  2. 电商产品列表

    • 支持动态分类和筛选
    • 服务器端预渲染商品数据
    • 结合客户端交互(分类选择、加入购物车)
  3. 新闻门户首页

    • 使用Nuxt.js的asyncData进行服务器端数据获取
    • 支持动态切换分类
    • 针对SEO优化的meta信息
  4. 仪表盘页面

    • 混合渲染策略:
      • 初始数据通过SSR加载
      • 实时数据通过客户端WebSocket更新
    • 使用Recharts进行数据可视化
    • 响应式布局设计

最佳实践建议

  1. 数据获取

    • 在服务器端获取关键数据
    • 使用适当的缓存策略
    • 考虑数据的实时性需求
  2. 性能优化

    • 实现增量静态再生成(ISR)
    • 使用适当的缓存策略
    • 优化图片和资源加载
  3. 用户体验

    • 实现平滑的客户端交互
    • 添加适当的加载状态
    • 处理错误情况
  4. SEO优化

    • 添加适当的meta标签
    • 实现结构化数据
    • 确保内容的可访问性

结论

SSR是一种强大的渲染方式,特别适合需要良好SEO和快速首屏加载的应用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值