feat(deploy): production deployment setup and fixes
- Debian-based api image (bookworm-slim), docker/debian mirrors, prisma binaryTargets for openssl 3.0 - nginx: admin SPA under /admin, TLS via acme.sh (ZeroSSL) + auto-renewal cron, http->https redirect - prisma: add origin_goods.delisted migration, sync missing schema (good_image/tag_font_color/good_tags), fix users.createdAt Timestamptz - api: CORS wildcard reflection, helmet CORP cross-origin, price backfill in persistProductDetail, categoryIcon ancestor fallback, mediaByColor per-color gallery in public goods detail - admin: /admin base path (vite + router) - import-data.mjs: udt_name casting, serial sequence advance fix
This commit is contained in:
+184
-184
@@ -1,184 +1,184 @@
|
||||
---
|
||||
title: Mounted and Unmounted Hooks Do Not Run During SSR
|
||||
impact: MEDIUM
|
||||
impactDescription: SSR applications may fail if mounted-only code is essential for functionality
|
||||
type: capability
|
||||
tags: [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:**
|
||||
```javascript
|
||||
// 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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// 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:**
|
||||
```javascript
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- 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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```vue
|
||||
<!-- 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>
|
||||
```
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```vue
|
||||
<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
|
||||
- [Vue.js SSR Guide](https://vuejs.org/guide/scaling-up/ssr.html)
|
||||
- [Nuxt.js Lifecycle](https://nuxt.com/docs/api/composables/use-nuxt-app#lifecycle-hooks)
|
||||
- [Vue SSR Hydration](https://vuejs.org/guide/scaling-up/ssr.html#client-hydration)
|
||||
---
|
||||
title: Mounted and Unmounted Hooks Do Not Run During SSR
|
||||
impact: MEDIUM
|
||||
impactDescription: SSR applications may fail if mounted-only code is essential for functionality
|
||||
type: capability
|
||||
tags: [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:**
|
||||
```javascript
|
||||
// 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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// 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:**
|
||||
```javascript
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- 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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```vue
|
||||
<!-- 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>
|
||||
```
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```vue
|
||||
<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
|
||||
- [Vue.js SSR Guide](https://vuejs.org/guide/scaling-up/ssr.html)
|
||||
- [Nuxt.js Lifecycle](https://nuxt.com/docs/api/composables/use-nuxt-app#lifecycle-hooks)
|
||||
- [Vue SSR Hydration](https://vuejs.org/guide/scaling-up/ssr.html#client-hydration)
|
||||
|
||||
Reference in New Issue
Block a user