12451 b553c4e519 feat: API 统一 + 设备详情补全 + H5 导航优化
- API 路由统一: 移除 /api/h5/,统一 /api/pms/ 入口
- 二进制改名: api-server -> pms-api(与服务名一致)
- nginx: 合并 /api/ + /pms/api/ 为 /api/pms/
- 新增 docs/API.md 接口文档
- 后端: CabinetDetail 增加 rssi/pow_fail/address/soc 等字段
- Redis 管道批量读取仓体实时数据(SOC/电压/电流/功率)
- DB: 所有 TIMESTAMP 列改为 DATETIME(修复类型不匹配)
- 用户管理: 新增创建时间/修改时间列,状态改用 Switch
- 角色管理/用户管理: 操作按钮改为图标
- PC 设备详情: 基本信息补全 RSSI/ICCID/电源/地址
- PC 卡片: 通道统计改用 Lucide 图标,颜色统一
- PC 二维码: canvas 渲染,含编号文字,支持右键下载
- H5: 按 uni-app 规范修正导航(navigateTo 替代 reLaunch)
- H5: 详情页加返回导航条
- H5: 扫码支持摄像头(jsQR)+ ?id= 参数
- H5: 底部 tabBar 在所有页面常显
- H5: 首页统计改为 6 项(设备/通道/空闲/充电/充满/故障)
- H5: 下拉刷新 / 上拉加载
- 产品首页: 添加业务联系
- 文档: 清理过时文件,补充缺失规范章节
2026-07-04 04:03:02 +08:00

5.0 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
Mounted and Unmounted Hooks Do Not Run During SSR MEDIUM SSR applications may fail if mounted-only code is essential for functionality capability
vue3
lifecycle
ssr
server-side-rendering
nuxt
onMounted
mounted
hydration

Mounted and Unmounted Hooks Do Not Run During SSR

Impact: MEDIUM - During server-side rendering (SSR), lifecycle hooks like mounted, onMounted, unmounted, and onUnmounted are never called on the server. This can cause differences between server-rendered and client-rendered content, hydration mismatches, and missing functionality if critical logic is placed only in these hooks.

On the server, only beforeCreate, created, and their Composition API equivalents run. Client-specific operations (DOM access, browser APIs, third-party libraries) must be in mounted hooks, but you must handle the SSR case appropriately.

Task Checklist

  • Place browser-specific code (window, document, localStorage) in mounted/onMounted
  • Ensure critical data fetching happens in hooks that run on server (created)
  • Handle hydration mismatches for content that differs client vs server
  • Use <ClientOnly> wrapper (Nuxt) or conditional rendering for client-only components
  • Check for browser environment before using browser APIs

Incorrect:

// WRONG: Accessing browser APIs in created - breaks SSR
export default {
  created() {
    // These don't exist on the server!
    this.width = window.innerWidth // ReferenceError: window is not defined
    this.savedData = localStorage.getItem('data') // ReferenceError: localStorage is not defined
  }
}
// WRONG: Critical initialization only in mounted - won't run on server
export default {
  data() {
    return { user: null }
  },
  async mounted() {
    // This won't run on server - page renders without user data
    // Then hydrates with user data - causes flash of content
    this.user = await fetchCurrentUser()
  }
}

Correct:

// CORRECT: Data fetching in created (runs on server), DOM in mounted
export default {
  data() {
    return {
      user: null,
      windowWidth: 0
    }
  },
  async created() {
    // This runs on both server and client
    this.user = await fetchCurrentUser()
  },
  mounted() {
    // Browser-specific code safely in mounted
    this.windowWidth = window.innerWidth
    window.addEventListener('resize', this.handleResize)
  },
  unmounted() {
    window.removeEventListener('resize', this.handleResize)
  }
}
<!-- CORRECT: Composition API with SSR awareness -->
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const user = ref(null)
const windowWidth = ref(0)

// This runs on both server and client (during setup)
user.value = await useFetch('/api/user')

// These only run on client
onMounted(() => {
  windowWidth.value = window.innerWidth
  window.addEventListener('resize', handleResize)
})

onUnmounted(() => {
  window.removeEventListener('resize', handleResize)
})

function handleResize() {
  windowWidth.value = window.innerWidth
}
</script>

Checking for Browser Environment

// CORRECT: Guard browser API access
export default {
  data() {
    return { theme: 'light' }
  },
  created() {
    // Check if we're in browser before accessing browser APIs
    if (typeof window !== 'undefined') {
      this.theme = localStorage.getItem('theme') || 'light'
    }
  },
  mounted() {
    // mounted only runs in browser, so this is always safe
    this.applyTheme()
  }
}

Nuxt.js Specific Patterns

<!-- CORRECT: Using Nuxt's ClientOnly for client-specific components -->
<template>
  <div>
    <!-- This content renders on both server and client -->
    <h1>Dashboard</h1>

    <!-- This only renders on client - no hydration mismatch -->
    <ClientOnly>
      <ChartComponent :data="chartData" />
      <template #fallback>
        <p>Loading chart...</p>
      </template>
    </ClientOnly>
  </div>
</template>
// CORRECT: Using Nuxt's process.client/process.server
export default {
  created() {
    if (process.client) {
      // Only runs in browser
      this.initAnalytics()
    }
    if (process.server) {
      // Only runs on server
      this.logServerRequest()
    }
  }
}

Handling Hydration Mismatches

<script setup>
import { ref, onMounted } from 'vue'

// Start with a value that matches what server renders
const currentTime = ref(null)

onMounted(() => {
  // Update to real value only on client
  // This prevents hydration mismatch
  currentTime.value = new Date().toLocaleTimeString()
})
</script>

<template>
  <!-- Renders null on server, then updates on client -->
  <span v-if="currentTime">{{ currentTime }}</span>
  <span v-else>Loading...</span>
</template>

Reference