總覽
什麽是 SSR?
Vue.js 是一(yī)個用于構建客戶端應用的框架。默認情況下(xià),Vue 組件的職責是在浏覽器中生成和(hé)操作(zuò) DOM。然而,Vue 也支持将組件在服務端直接渲染成 HTML 字符串,作(zuò)為(wèi)服務端響應返回給浏覽器,最後在浏覽器端将靜态的 HTML“激活”(hydrate) 為(wèi)能(néng)夠交互的客戶端應用。
一(yī)個由服務端渲染的 Vue.js 應用也可(kě)以被認為(wèi)是“同構的”(Isomorphic) 或“通(tōng)用的”(Universal),因為(wèi)應用的大部分代碼同時(shí)運行(xíng)在服務端和(hé)客戶端。
為(wèi)什麽要(yào)用 SSR?
與客戶端的單頁應用 (SPA) 相比,SSR 的優勢主要(yào)在于:
更快(kuài)的首屏加載:這(zhè)一(yī)點在慢(màn)網速或者運行(xíng)緩慢(màn)的設備上(shàng)尤為(wèi)重要(yào)。服務端渲染的 HTML 無需等到所有的 JavaScript 都(dōu)下(xià)載并執行(xíng)完成之後才顯示,所以你的用戶将會更快(kuài)地(dì)看(kàn)到完整渲染的頁面。除此之外(wài),數(shù)據獲取過程在首次訪問(wèn)時(shí)在服務端完成,相比于從(cóng)客戶端獲取,可(kě)能(néng)有更快(kuài)的數(shù)據庫連接。這(zhè)通(tōng)常可(kě)以帶來更高的核心 Web 指标評分、更好的用戶體驗,而對于那(nà)些“首屏加載速度與轉化率直接相關”的應用來說,這(zhè)點可(kě)能(néng)至關重要(yào)。
統一(yī)的心智模型:你可(kě)以使用相同的語言以及相同的聲明(míng)式、面向組件的心智模型來開(kāi)發整個應用,而不需要(yào)在後端模闆系統和(hé)前端框架之間(jiān)來回切換。
更好的 SEO:搜索引擎爬蟲可(kě)以直接看(kàn)到完全渲染的頁面。
TIP
截至目前,Google 和(hé) Bing 可(kě)以很(hěn)好地(dì)對同步 JavaScript 應用進行(xíng)索引。這(zhè)裏的“同步”是關鍵詞。如(rú)果你的應用以一(yī)個 loading 動畫(huà)開(kāi)始,然後通(tōng)過 Ajax 獲取內(nèi)容,爬蟲并不會等到內(nèi)容加載完成再抓取。也就是說,如(rú)果 SEO 對你的頁面至關重要(yào),而你的內(nèi)容又是異步獲取的,那(nà)麽 SSR 可(kě)能(néng)是必需的。
使用 SSR 時(shí)還有一(yī)些權衡之處需要(yào)考量:
開(kāi)發中的限制。浏覽器端特定的代碼隻能(néng)在某些生命周期鈎子中使用;一(yī)些外(wài)部庫可(kě)能(néng)需要(yào)特殊處理才能(néng)在服務端渲染的應用中運行(xíng)。
更多的與構建配置和(hé)部署相關的要(yào)求。服務端渲染的應用需要(yào)一(yī)個能(néng)讓 Node.js 服務器運行(xíng)的環境,不像完全靜态的 SPA 那(nà)樣可(kě)以部署在任意的靜态文件服務器上(shàng)。
更高的服務端負載。在 Node.js 中渲染一(yī)個完整的應用要(yào)比僅僅托管靜态文件更加占用 CPU 資源,因此如(rú)果你預期有高流量,請(qǐng)為(wèi)相應的服務器負載做(zuò)好準備,并采用合理的緩存策略。
在為(wèi)你的應用使用 SSR 之前,你首先應該問(wèn)自(zì)己是否真的需要(yào)它。這(zhè)主要(yào)取決于首屏加載速度對應用的重要(yào)程度。例如(rú),如(rú)果你正在開(kāi)發一(yī)個內(nèi)部的管理面闆,初始加載時(shí)的那(nà)額外(wài)幾百毫秒對你來說并不重要(yào),這(zhè)種情況下(xià)使用 SSR 就沒有太多必要(yào)了(le)。然而,在內(nèi)容展示速度極其重要(yào)的場景下(xià),SSR 可(kě)以盡可(kě)能(néng)地(dì)幫你實現最優的初始加載性能(néng)。
SSR vs. SSG
靜态站點生成 (Static-Site Generation,縮寫為(wèi) SSG),也被稱為(wèi)預渲染,是另一(yī)種流行(xíng)的構建快(kuài)速網站的技術(shù)。如(rú)果用服務端渲染一(yī)個頁面所需的數(shù)據對每個用戶來說都(dōu)是相同的,那(nà)麽我們可(kě)以隻渲染一(yī)次,提前在構建過程中完成,而不是每次請(qǐng)求進來都(dōu)重新渲染頁面。預渲染的頁面生成後作(zuò)為(wèi)靜态 HTML 文件被服務器托管。
SSG 保留了(le)和(hé) SSR 應用相同的性能(néng)表現:它帶來了(le)優秀的首屏加載性能(néng)。同時(shí),它比 SSR 應用的花銷更小,也更容易部署,因為(wèi)它輸出的是靜态 HTML 和(hé)資源文件。這(zhè)裏的關鍵詞是靜态:SSG 僅可(kě)以用于消費(fèi)靜态數(shù)據的頁面,即數(shù)據在構建期間(jiān)就是已知的,并且在多次部署期間(jiān)不會改變。每當數(shù)據變化時(shí),都(dōu)需要(yào)重新部署。
如(rú)果你調研 SSR 隻是為(wèi)了(le)優化為(wèi)數(shù)不多的營銷頁面的 SEO (例如(rú) /、/about 和(hé) /contact 等),那(nà)麽你可(kě)能(néng)需要(yào) SSG 而不是 SSR。SSG 也非常适合構建基于內(nèi)容的網站,比如(rú)文檔站點或者博客。事實上(shàng),你現在正在閱讀的這(zhè)個網站就是使用 VitePress 靜态生成的,它是一(yī)個由 Vue 驅動的靜态站點生成器。
基礎教程
渲染一(yī)個應用
讓我們來看(kàn)一(yī)個 Vue SSR 最基礎的實戰示例。
創建一(yī)個新的文件夾,cd 進入
執行(xíng) npm init -y
在 package.json 中添加 "type": "module" 使 Node.js 以 ES modules mode 運行(xíng)
執行(xíng) npm install vue
創建一(yī)個 example.js 文件:
js
// 此文件運行(xíng)在 Node.js 服務器上(shàng)
import { createSSRApp } from 'vue'
// Vue 的服務端渲染 API 位于 `vue/server-renderer` 路徑下(xià)
import { renderToString } from 'vue/server-renderer'
const app = createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`
})
renderToString(app).then((html) => {
console.log(html)
})
接着運行(xíng):
sh
> node example.js
它應該會在命令行(xíng)中打印出如(rú)下(xià)內(nèi)容:
<button>1</button>
renderToString() 接收一(yī)個 Vue 應用實例作(zuò)為(wèi)參數(shù),返回一(yī)個 Promise,當 Promise resolve 時(shí)得到應用渲染的 HTML。當然你也可(kě)以使用 Node.js Stream API 或者 Web Streams API 來執行(xíng)流式渲染。查看(kàn) SSR API 參考獲取完整的相關細節。
然後我們可(kě)以把 Vue SSR 的代碼移動到一(yī)個服務器請(qǐng)求處理函數(shù)裏,它将應用的 HTML 片段包裝為(wèi)完整的頁面 HTML。接下(xià)來的幾步我們将會使用 express:
執行(xíng) npm install express
創建下(xià)面的 server.js 文件:
js
import express from 'express'
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'
const server = express()
server.get('/', (req, res) => {
const app = createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`
})
renderToString(app).then((html) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Vue SSR Example</title>
</head>
<body>
<div id="app">${html}</div>
</body>
</html>
`)
})
})
server.listen(3000, () => {
console.log('ready')
})
最後,執行(xíng) node server.js,訪問(wèn) http://localhost:3000。你應該可(kě)以看(kàn)到頁面中的按鈕了(le)。
在 StackBlitz 上(shàng)試試
客戶端激活
如(rú)果你點擊該按鈕,你會發現數(shù)字并沒有改變。這(zhè)段 HTML 在客戶端是完全靜态的,因為(wèi)我們沒有在浏覽器中加載 Vue。
為(wèi)了(le)使客戶端的應用可(kě)交互,Vue 需要(yào)執行(xíng)一(yī)個激活步驟。在激活過程中,Vue 會創建一(yī)個與服務端完全相同的應用實例,然後将每個組件與它應該控制的 DOM 節點相匹配,并添加 DOM 事件監聽器。
為(wèi)了(le)在激活模式下(xià)挂載應用,我們應該使用 createSSRApp() 而不是 createApp():
js
// 該文件運行(xíng)在浏覽器中
import { createSSRApp } from 'vue'
const app = createSSRApp({
// ...和(hé)服務端完全一(yī)緻的應用實例
})
// 在客戶端挂載一(yī)個 SSR 應用時(shí)會假定
// HTML 是預渲染的,然後執行(xíng)激活過程,
// 而不是挂載新的 DOM 節點
app.mount('#app')
代碼結構
想想我們該如(rú)何在客戶端複用服務端的應用實現。這(zhè)時(shí)我們就需要(yào)開(kāi)始考慮 SSR 應用中的代碼結構了(le)——我們如(rú)何在服務器和(hé)客戶端之間(jiān)共享相同的應用代碼呢?
這(zhè)裏我們将演示最基礎的設置。首先,讓我們将應用的創建邏輯拆分到一(yī)個單獨的文件 app.js 中:
js
// app.js (在服務器和(hé)客戶端之間(jiān)共享)
import { createSSRApp } from 'vue'
export function createApp() {
return createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`
})
}
該文件及其依賴項在服務器和(hé)客戶端之間(jiān)共享——我們稱它們為(wèi)通(tōng)用代碼。編寫通(tōng)用代碼時(shí)有一(yī)些注意事項,我們将在下(xià)面讨論。
我們在客戶端入口導入通(tōng)用代碼,創建應用并執行(xíng)挂載:
js
// client.js
import { createApp } from './app.js'
createApp().mount('#app')
服務器在請(qǐng)求處理函數(shù)中使用相同的應用創建邏輯:
js
// server.js (不相關的代碼省略)
import { createApp } from './app.js'
server.get('/', (req, res) => {
const app = createApp()
renderToString(app).then(html => {
// ...
})
})
此外(wài),為(wèi)了(le)在浏覽器中加載客戶端文件,我們還需要(yào):
在 server.js 中添加 server.use(express.static('.')) 來托管客戶端文件。
将 <script type="module" src="/client.js"></script> 添加到 HTML 外(wài)殼以加載客戶端入口文件。
通(tōng)過在 HTML 外(wài)殼中添加 Import Map 以支持在浏覽器中使用 import * from 'vue'。
在 StackBlitz 上(shàng)嘗試完整的示例。按鈕現在可(kě)以交互了(le)!
更通(tōng)用的解決方案
從(cóng)上(shàng)面的例子到一(yī)個生産就緒的 SSR 應用還需要(yào)很(hěn)多工(gōng)作(zuò)。我們将需要(yào):
支持 Vue SFC 且滿足其他(tā)構建步驟要(yào)求。事實上(shàng),我們需要(yào)為(wèi)同一(yī)個應用執行(xíng)兩次構建過程:一(yī)次用于客戶端,一(yī)次用于服務器。
TIP
Vue 組件用在 SSR 時(shí)的編譯産物(wù)不同——模闆被編譯為(wèi)字符串拼接而不是 render 函數(shù),以此提高渲染性能(néng)。
在服務器請(qǐng)求處理函數(shù)中,确保返回的 HTML 包含正确的客戶端資源鏈接和(hé)最優的資源加載提示 (如(rú) prefetch 和(hé) preload)。我們可(kě)能(néng)還需要(yào)在 SSR 和(hé) SSG 模式之間(jiān)切換,甚至在同一(yī)個應用中混合使用這(zhè)兩種模式。
以一(yī)種通(tōng)用的方式管理路由、數(shù)據獲取和(hé)狀态存儲。
完整的實現會非常複雜(zá),并且取決于你選擇使用的構建工(gōng)具鏈。因此,我們強烈建議(yì)你使用一(yī)種更通(tōng)用的、更集成化的解決方案,幫你抽象掉那(nà)些複雜(zá)的東西(xī)。下(xià)面推薦幾個 Vue 生态中的 SSR 解決方案。
Nuxt
Nuxt 是一(yī)個構建于 Vue 生态系統之上(shàng)的全棧框架,它為(wèi)編寫 Vue SSR 應用提供了(le)絲滑的開(kāi)發體驗。更棒的是,你還可(kě)以把它當作(zuò)一(yī)個靜态站點生成器來用!我們強烈建議(yì)你試一(yī)試。
Quasar
Quasar 是一(yī)個基于 Vue 的完整解決方案,它可(kě)以讓你用同一(yī)套代碼庫構建不同目标的應用,如(rú) SPA、SSR、PWA、移動端應用、桌面端應用以及浏覽器插件。除此之外(wài),它還提供了(le)一(yī)整套 Material Design 風(fēng)格的組件庫。
Vite SSR
Vite 提供了(le)內(nèi)置的 Vue 服務端渲染支持,但(dàn)它在設計上(shàng)是偏底層的。如(rú)果你想要(yào)直接使用 Vite,可(kě)以看(kàn)看(kàn) vite-plugin-ssr,一(yī)個幫你抽象掉許多複雜(zá)細節的社區(qū)插件。
你也可(kě)以在這(zhè)裏查看(kàn)一(yī)個使用手動配置的 Vue + Vite SSR 的示例項目,以它作(zuò)為(wèi)基礎來構建。請(qǐng)注意,這(zhè)種方式隻有在你有豐富的 SSR 和(hé)構建工(gōng)具經驗,并希望對應用的架構做(zuò)深入的定制時(shí)才推薦使用。
書(shū)寫 SSR 友(yǒu)好的代碼
無論你的構建配置或頂層框架的選擇如(rú)何,下(xià)面的原則在所有 Vue SSR 應用中都(dōu)适用。
服務端的響應性
在 SSR 期間(jiān),每一(yī)個請(qǐng)求 URL 都(dōu)會映射到我們應用中的一(yī)個期望狀态。因為(wèi)沒有用戶交互和(hé) DOM 更新,所以響應性在服務端是不必要(yào)的。為(wèi)了(le)更好的性能(néng),默認情況下(xià)響應性在 SSR 期間(jiān)是禁用的。
組件生命周期鈎子
因為(wèi)沒有任何動态更新,所以像 mounted 或者 updated 這(zhè)樣的生命周期鈎子不會在 SSR 期間(jiān)被調用,而隻會在客戶端運行(xíng)。隻有 beforeCreate 和(hé) created 這(zhè)兩個鈎子會在 SSR 期間(jiān)被調用。
你應該避免在 beforeCreate 和(hé) created中使用會産生副作(zuò)用且需要(yào)被清理的代碼。這(zhè)類副作(zuò)用的常見(jiàn)例子是使用 setInterval 設置定時(shí)器。我們可(kě)能(néng)會在客戶端特有的代碼中設置定時(shí)器,然後在 beforeUnmount 或 unmounted 中清除。然而,由于 unmount 鈎子不會在 SSR 期間(jiān)被調用,所以定時(shí)器會永遠存在。為(wèi)了(le)避免這(zhè)種情況,請(qǐng)将含有副作(zuò)用的代碼放到 mounted 中。
訪問(wèn)平台特有 API
通(tōng)用代碼不能(néng)訪問(wèn)平台特有的 API,如(rú)果你的代碼直接使用了(le)浏覽器特有的全局變量,比如(rú) window 或 document,他(tā)們會在 Node.js 運行(xíng)時(shí)報錯,反過來也一(yī)樣。
對于在服務器和(hé)客戶端之間(jiān)共享,但(dàn)使用了(le)不同的平台 API 的任務,建議(yì)将平台特定的實現封裝在一(yī)個通(tōng)用的 API 中,或者使用能(néng)為(wèi)你做(zuò)這(zhè)件事的庫。例如(rú)你可(kě)以使用 node-fetch 在服務端和(hé)客戶端使用相同的 fetch API。
對于浏覽器特有的 API,通(tōng)常的方法是在僅客戶端特有的生命周期鈎子中惰性地(dì)訪問(wèn)它們,例如(rú) mounted。
請(qǐng)注意,如(rú)果一(yī)個第三方庫編寫時(shí)沒有考慮到通(tōng)用性,那(nà)麽要(yào)将它集成到一(yī)個 SSR 應用中可(kě)能(néng)會很(hěn)棘手。你或許可(kě)以通(tōng)過模拟一(yī)些全局變量來讓它工(gōng)作(zuò),但(dàn)這(zhè)隻是一(yī)種 hack 手段并且可(kě)能(néng)會影響到其他(tā)庫的環境檢測代碼。
跨請(qǐng)求狀态污染
在狀态管理一(yī)章(zhāng)中,我們介紹了(le)一(yī)種使用響應式 API 的簡單狀态管理模式。而在 SSR 環境中,這(zhè)種模式需要(yào)一(yī)些額外(wài)的調整。
上(shàng)述模式在一(yī)個 JavaScript 模塊的根作(zuò)用域中聲明(míng)共享的狀态。這(zhè)是一(yī)種單例模式——即在應用的整個生命周期中隻有一(yī)個響應式對象的實例。這(zhè)在純客戶端的 Vue 應用中是可(kě)以的,因為(wèi)對于浏覽器的每一(yī)個頁面訪問(wèn),應用模塊都(dōu)會重新初始化。
然而,在 SSR 環境下(xià),應用模塊通(tōng)常隻在服務器啓動時(shí)初始化一(yī)次。同一(yī)個應用模塊會在多個服務器請(qǐng)求之間(jiān)被複用,而我們的單例狀态對象也一(yī)樣。如(rú)果我們用單個用戶特定的數(shù)據對共享的單例狀态進行(xíng)修改,那(nà)麽這(zhè)個狀态可(kě)能(néng)會意外(wài)地(dì)洩露給另一(yī)個用戶的請(qǐng)求。我們把這(zhè)種情況稱為(wèi)跨請(qǐng)求狀态污染。
從(cóng)技術(shù)上(shàng)講,我們可(kě)以在每個請(qǐng)求上(shàng)重新初始化所有 JavaScript 模塊,就像我們在浏覽器中所做(zuò)的那(nà)樣。但(dàn)是,初始化 JavaScript 模塊的成本可(kě)能(néng)很(hěn)高,因此這(zhè)會顯著影響服務器性能(néng)。
推薦的解決方案是在每個請(qǐng)求中為(wèi)整個應用創建一(yī)個全新的實例,包括 router 和(hé)全局 store。然後,我們使用應用層級的 provide 方法來提供共享狀态,并将其注入到需要(yào)它的組件中,而不是直接在組件中将其導入:
js
// app.js (在服務端和(hé)客戶端間(jiān)共享)
import { createSSRApp } from 'vue'
import { createStore } from './store.js'
// 每次請(qǐng)求時(shí)調用
export function createApp() {
const app = createSSRApp(/* ... */)
// 對每個請(qǐng)求都(dōu)創建新的 store 實例
const store = createStore(/* ... */)
// 提供應用級别的 store
app.provide('store', store)
// 也為(wèi)激活過程暴露出 store
return { app, store }
}
像 Pinia 這(zhè)樣的狀态管理庫在設計時(shí)就考慮到了(le)這(zhè)一(yī)點。請(qǐng)參考 Pinia 的 SSR 指南以了(le)解更多細節。
激活不匹配
如(rú)果預渲染的 HTML 的 DOM 結構不符合客戶端應用的期望,就會出現激活不匹配。最常見(jiàn)的激活不匹配是以下(xià)幾種原因導緻的:
組件模闆中存在不符合規範的 HTML 結構,渲染後的 HTML 被浏覽器原生的 HTML 解析行(xíng)為(wèi)糾正導緻不匹配。舉例來說,一(yī)個常見(jiàn)的錯誤是 <div> 不能(néng)被放在 <p> 中:
html
<p><div>hi</div></p>
如(rú)果我們在服務器渲染的 HTML 中出現這(zhè)樣的代碼,當遇到 <div> 時(shí),浏覽器會結束第一(yī)個 <p>,并解析為(wèi)以下(xià) DOM 結構:
html
<p></p>
<div>hi</div>
<p></p>
渲染所用的數(shù)據中包含随機生成的值。由于同一(yī)個應用會在服務端和(hé)客戶端執行(xíng)兩次,每次執行(xíng)生成的随機數(shù)都(dōu)不能(néng)保證相同。避免随機數(shù)不匹配有兩種選擇:
利用 v-if + onMounted 讓需要(yào)用到随機數(shù)的模闆隻在客戶端渲染。你所用的上(shàng)層框架可(kě)能(néng)也會提供簡化這(zhè)個用例的內(nèi)置 API,比如(rú) VitePress 的 <ClientOnly> 組件。
使用一(yī)個能(néng)夠接受随機種子的随機數(shù)生成庫,并确保服務端和(hé)客戶端使用同樣的随機數(shù)種子 (比如(rú)把種子包含在序列化的狀态中,然後在客戶端取回)。
服務端和(hé)客戶端的時(shí)區(qū)不一(yī)緻。有時(shí)候我們可(kě)能(néng)會想要(yào)把一(yī)個時(shí)間(jiān)轉換為(wèi)用戶的當地(dì)時(shí)間(jiān),但(dàn)在服務端的時(shí)區(qū)跟用戶的時(shí)區(qū)可(kě)能(néng)并不一(yī)緻,我們也并不能(néng)可(kě)靠的在服務端預先知道(dào)用戶的時(shí)區(qū)。這(zhè)種情況下(xià),當地(dì)時(shí)間(jiān)的轉換也應該作(zuò)為(wèi)純客戶端邏輯去執行(xíng)。
當 Vue 遇到激活不匹配時(shí),它将嘗試自(zì)動恢複并調整預渲染的 DOM 以匹配客戶端的狀态。這(zhè)将導緻一(yī)些渲染性能(néng)的損失,因為(wèi)需要(yào)丢棄不匹配的節點并渲染新的節點,但(dàn)大多數(shù)情況下(xià),應用應該會如(rú)預期一(yī)樣繼續工(gōng)作(zuò)。盡管如(rú)此,最好還是在開(kāi)發過程中發現并避免激活不匹配。
自(zì)定義指令
因為(wèi)大多數(shù)的自(zì)定義指令都(dōu)包含了(le)對 DOM 的直接操作(zuò),所以它們會在 SSR 時(shí)被忽略。但(dàn)如(rú)果你想要(yào)自(zì)己控制一(yī)個自(zì)定義指令在 SSR 時(shí)應該如(rú)何被渲染 (即應該在渲染的元素上(shàng)添加哪些 attribute),你可(kě)以使用 getSSRProps 指令鈎子:
js
const myDirective = {
mounted(el, binding) {
// 客戶端實現:
// 直接更新 DOM
el.id = binding.value
},
getSSRProps(binding) {
// 服務端實現:
// 返回需要(yào)渲染的 prop
// getSSRProps 隻接收一(yī)個 binding 參數(shù)
return {
id: binding.value
}
}
}
Teleports
在 SSR 的過程中 Teleport 需要(yào)特殊處理。如(rú)果渲染的應用包含 Teleport,那(nà)麽其傳送的內(nèi)容将不會包含在主應用渲染出的字符串中。在大多數(shù)情況下(xià),更推薦的方案是在客戶端挂載時(shí)條件式地(dì)渲染 Teleport。
如(rú)果你需要(yào)激活 Teleport 內(nèi)容,它們會暴露在服務端渲染上(shàng)下(xià)文對象的 teleports 屬性下(xià):
js
const ctx = {}
const html = await renderToString(app, ctx)
console.log(ctx.teleports) // { '#teleported': 'teleported content' }
跟主應用的 HTML 一(yī)樣,你需要(yào)自(zì)己将 Teleport 對應的 HTML 嵌入到最終頁面上(shàng)的正确位置處。
TIP
請(qǐng)避免在 SSR 的同時(shí)把 Teleport 的目标設為(wèi) body——通(tōng)常 <body> 會包含其他(tā)服務端渲染出來的內(nèi)容,這(zhè)會使得 Teleport 無法确定激活的正确起始位置。
推薦用一(yī)個獨立的隻包含 teleport 的內(nèi)容的容器,例如(rú) <div id="teleported"></div>。
網站建設開(kāi)發|APP設計開(kāi)發|小程序建設開(kāi)發