服务端渲染(SSR)中模块化有哪些注意事项?

SSR 模块化挑战

  1. Node.js 和浏览器环境差异
  2. CJS vs ESM 在 SSR 中的处理
  3. 模块副作用在服务端的行为
  4. 条件导出的服务端入口

1. 环境差异

// ❌ 在服务端报错
import { DOMPurify } from 'dompurify'  // 依赖 window

// ✅ 条件加载
let DOMPurify
if (typeof window !== 'undefined') {
  DOMPurify = (await import('dompurify')).default
}

// 或使用 isomorphic 版本
import DOMPurify from 'isomorphic-dompurify'

2. package.json 条件导出

{
  "exports": {
    ".": {
      "browser": "./dist/browser.js",   // 浏览器入口
      "node": "./dist/server.js",        // Node.js 入口
      "import": "./dist/index.mjs",      // ESM 入口
      "require": "./dist/index.cjs"      // CJS 入口
    }
  }
}

3. SSR 中的模块副作用

// ❌ 模块加载时操作 DOM(服务端没有 DOM)
// theme.js
document.documentElement.classList.add('dark')

// ✅ 延迟到客户端执行
export function initTheme() {
  if (typeof window === 'undefined') return
  document.documentElement.classList.add('dark')
}

// 只在客户端调用
import { useEffect } from 'react'
useEffect(() => {
  initTheme()  // useEffect 只在客户端执行
}, [])

4. 'use client' / 'use server'(React Server Components)

// React Server Components (Next.js App Router)

// 默认是 Server Component(服务端执行)
// app/page.js
export default async function Page() {
  const data = await fetch('https://api.example.com/data')
  return <div>{data.title}</div>
}

// 客户端组件
'use client'  // 标记为客户端组件
import { useState } from 'react'
export default function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

// Server Action
'use server'
export async function submitForm(formData) {
  // 在服务端执行
  await saveToDatabase(formData)
}

5. Nuxt 中的模块处理

<!-- Vue 组件默认在服务端和客户端都渲染 -->
<script setup>
// 服务端和客户端都会执行的代码
const { data } = await useFetch('/api/data')

// 只在客户端执行
onMounted(() => {
  window.scrollTo(0, 0)
})

// 只在服务端执行
if (import.meta.server) {
  // 服务端专用逻辑
}

// 只在客户端执行
if (import.meta.client) {
  // 客户端专用逻辑
}
</script>

6. SSR 中的动态导入

// 某些库只能在客户端运行
import { defineAsyncComponent } from 'vue'

const ClientOnlyChart = defineAsyncComponent(() => 
  import('chart.js').then(mod => mod.default)
)

// React
import dynamic from 'next/dynamic'

const Chart = dynamic(() => import('react-chartjs-2'), {
  ssr: false  // 不在服务端渲染
})

7. 服务器模块缓存

// ⚠️ 服务端模块是单例的,会在请求间共享
// 全局状态会导致数据泄露

// ❌ 危险:全局变量跨请求共享
let currentUser = null
export function setUser(user) { currentUser = user }

// ✅ 使用请求上下文
export function createAppContext(req) {
  return {
    user: req.user,
    // 每个请求独立
  }
}

8. 序列化与水合

// 服务端获取的数据需要传给客户端
// app.js (server)
export async function getServerSideProps() {
  const data = await fetchData()
  return { props: { data } }  // 会被 JSON.stringify
}

// 客户端水合时使用相同的数据
// 避免客户端重新请求

9. CSS 模块在 SSR 中

// CSS Modules 在 SSR 中需要特殊处理
import styles from './Button.module.css'
// 服务端:需要提取 CSS 到单独文件
// 客户端:正常加载

// Next.js / Nuxt.js 自动处理

10. 第三方库兼容性

// 有些库不支持 SSR
// 检查 package.json 是否有 "browser" 字段
// 或使用条件导入

// next.config.js
export default {
  webpack: (config) => {
    config.resolve.fallback = {
      fs: false,    // 不在客户端引入 Node.js 模块
      path: false,
    }
    return config
  }
}

SSR 模块化检查清单

同分类其他题目