• 首页
  • vue
  • TypeScript
  • JavaScript
  • scss
  • css3
  • html5
  • php
  • MySQL
  • redis
  • jQuery
  • vite + vue3 + ts + SSR 使用 vite-ssr 插件

    vite-ssr,是基于Vite 内置 SSR 功能的插件。适用于 vue3、react。vite-ssr 是由@frandiox 来创建的,作为 Node.js 中 Vite 的一个简单而又强大的 SSR 解决方案。这是将 Vite 的 SSR API 作为高级解决方案公开的另一种方式。@frandiox 还是 vitedge 的创建者,这是一个在 Cloudflare Workers 上运行的全栈 Vite 框架。

    vite-ssr 是为 vite2 ,在 nonde.js 环境中,实现服务端渲染的解决方案,它开发体验简单而功能强大。

    • 快速的热启动,即使在 SSR 环境下。由 vite 提供支持。
    • 一致的开发体验,可以抽离出大部分 SSR,降低复杂性。
    • 小型库,不关心页面路由和 API 逻辑。
    • 体验:快速呈现 SEO 功能,与 SPA 接管快速。
    • 兼容 Vite 的插件生态系统,如基于文件的路由、PWA 等。

    vite-ssr 可以部署到任何 Node.js 或 browser-like 的环境。可以部署到任何服务器平台,如 Vercel、Netlify,甚至 Cloudflare Workers。它还可以与 Express.js 、Fastify 等更传统的服务器一起运行。


    安装

    安装:vite、vue3 TypeScript 版,项目名称:vite-ssr

    cd /var/web/www
    
    yarn create vite vite-ssr --template vue-ts
    
    cd /var/web/www/vite-ssr
    yarn
    yarn dev
    

    安装:vite-ssrvue-router@vueuse/head

    # vite-ssr 依赖 vue-router@4
    yarn add vite-ssr vue-router@4 @vueuse/head
    

    安装:Piniaaxios

    yarn add pinia axios
    

    查看已经安装的包

    yarn list --depth=0
    


    vite 配置别名 alias

    安装 node TypeScript 类型支持

    yarn add -D @types/node
    

    修改vite.config.tstsconfig.json

    // vite.config.ts
    import { fileURLToPath, URL } from 'node:url';
    import { defineConfig } from 'vite';
    import vue from '@vitejs/plugin-vue';
    
    export default defineConfig({
      plugins: [vue()],
      resolve: {
        alias: {
          '@': fileURLToPath(new URL('./src', import.meta.url)),
        },
      },
    });
    
    // tsconfig.json
    
    {
      "compilerOptions": {
        "target": "ESNext",
        "useDefineForClassFields": true,
        "module": "ESNext",
        "moduleResolution": "Node",
        "strict": true,
        "jsx": "preserve",
        "sourceMap": true,
        "resolveJsonModule": true,
        "isolatedModules": true,
        "esModuleInterop": true,
        "lib": ["ESNext", "DOM"],
        "skipLibCheck": true,
        "baseUrl": ".",
        "paths": {
          "@/*": ["./src/*"]
        }
      },
      "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
      "references": [{ "path": "./tsconfig.node.json" }]
    }
    

    修改完毕后,重启 vscode


    格式化工具

    安装 eslint、prettier,说明文档

    yarn add -D eslint eslint-plugin-vue
    yarn add -D --exact prettier
    


    配置完毕后,需要重启 vscode 编辑器。


    vue3 测试数据

    为了便于测试效果,创建一些组件、路由、Pinia 数据


    使用路由 router

    src/router目录下,新建index.ts

    // router/index.ts
    import { createRouter, createWebHistory } from 'vue-router';
    import HomeView from '@/views/HomeView.vue';
    
    const router = createRouter({
      history: createWebHistory(import.meta.env.BASE_URL),
      routes: [
        {
          path: '/',
          name: 'home',
          component: HomeView,
        },
        {
          path: '/about',
          name: 'about',
          component: () => import('@/views/AboutView.vue'),
        },
      ],
    });
    
    export default router;
    


    使用组件

    src目录下,新建views目录,然后创建组件HomeView.vueAboutView.vueCountNunber.vue

    // src/views/HomeView.vue
    <script setup lang="ts">
    import CountNunber from './CountNunber.vue';
    </script>
    
    <template>
      <main>主页</main>
      <CountNunber msg="计数器" />
    </template>
    <style scoped>
    main {
      width: 100%;
      font-size: 36px;
      text-align: center;
      margin-top: 10rem;
    }
    </style>
    
    // CountNunber.vue
    <script setup lang="ts">
    import { ref } from 'vue';
    defineProps<{ msg: string }>();
    const count = ref(0);
    </script>
    <template>
      <div class="card">
        <button type="button" @click="count++">count is {{ count }}</button>
      </div>
      <p>{{ msg }}</p>
    </template>
    <style scoped></style>
    
    // AboutView.vue
    <script setup lang="ts"></script>
    <template>
      <div class="about">
        <h1>关于</h1>
      </div>
    </template>
    
    <style scoped></style>
    

    修改根组件App.vue

    // App.vue
    <script setup lang="ts">
    import { RouterLink, RouterView } from 'vue-router';
    </script>
    
    <template>
      <nav>
        <RouterLink to="/">Home</RouterLink>
        <RouterLink to="/about">About</RouterLink>
      </nav>
      <RouterView />
    </template>
    
    <style scoped>
    nav {
      width: 100%;
      font-size: 18px;
      text-align: center;
      margin-top: 2rem;
    }
    
    nav a.router-link-exact-active {
      color: var(--color-text);
    }
    
    nav a.router-link-exact-active:hover {
      background-color: transparent;
    }
    
    nav a {
      display: inline-block;
      padding: 0 1rem;
      border-left: 1px solid var(--color-border);
    }
    
    nav a:first-of-type {
      border: 0;
    }
    </style>
    


    使用 Pinia

    src下,新建stores/counter.ts

    import { ref, computed } from 'vue';
    import { defineStore } from 'pinia';
    
    export const useCounterStore = defineStore('counter', () => {
      const count = ref(0);
      const doubleCount = computed(() => count.value * 2);
      function increment() {
        count.value++;
      }
    
      return { count, doubleCount, increment };
    });
    


    main.ts

    修改入口文件main.ts

    import { createApp } from 'vue';
    import { createPinia } from 'pinia';
    import { routes, router } from './router';
    import App from './App.vue';
    import './style.css';
    
    const app = createApp(App);
    const pinia = createPinia();
    app.use(router);
    app.use(pinia);
    app.mount('#app');
    


    测试运行

    根目录下,运行命令

    yarn dev
    


    搭建 SSR

    vue3 SPA 开发,有几个重要文件:index.html,入口文件main.ts,根组件App.vue,配置文件vite.config.ts。但使用vite-ssr插件,搭建 SSR,重要文件只有两个:入口文件main.ts,配置文件vite.config.ts。其余的文件与 vue3 SPA 开发相同即可。相对于使用Vite 内置 SSR 功能来搭建 SSR,确实简单很多。

    vite.config.ts

    修改vite.config.ts,导入vitessr()插件:

    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import viteSSR from 'vite-ssr/plugin.js'
    
    export default defineConfig({
      plugins: [
          viteSSR(),
          vue()
      ]
    })
    


    main.ts

    在入口文件main.ts中导入 Vite SSR 主处理程序viteSSR()

    import { routes, router } from './router';
    import App from './App.vue';
    import './style.css';
    import viteSSR from 'vite-ssr';
    import { createPinia } from 'pinia';
    import { createHead } from '@vueuse/head';
    
    export default viteSSR(App, { routes }, (context) => {
      const { app, router, initialState } = context;
      const pinia = createPinia();
      const head = createHead();
    
      pinia.state.value = initialState.pinia;
      app.use(pinia);
      app.use(head);
      return { head };
    });
    



    vite-ssr 文档

    viteSSR()

    默认情况下,只有一个入口文件main.ts(或 main.js),它将负责为代码提供正确的环境。查看完整示例:VueReact

    import { createApp } from 'vue';
    import App from './App.vue';
    import viteSSR from 'vite-ssr';
    
    export default viteSSR(App, { routes }, (context) => {
      /* Vite SSR main hook for custom logic */
      /* const { app, router, initialState, ... } = context */
    });
    

    如果需要只在客户端或服务器中运行的条件逻辑,请使用 Vite 的import.meta.env.SSR布尔值判断,使用tree-shaking将完成其余操作。

    SSR 功能实现,一个方面是服务端生产 HTML,返回给请求。同时,客户端创建虚拟 DOM,激活 HTML,便于交互。viteSSR(),三个参数,分别对应组件、Vue(Vue 本身以及客户端hydration功能)、Vite SSR(服务端 SSR 功能)。

    第一个参数

    App根组件。


    第二个参数

    配置 Vue 以及客户端hydration功能。传递的参数,是一个对象,可以接受以下选项:

    • routes:路由数组,搭配应用程序的路由器。
    • base():是个函数,返回带有 base 路由器的字符串。对于i18n路由或应用程序,部署在根目录时非常有用。
    • routerOptions:其他路由器选项,如 vue 路由器中的scrollBehavior
    • transformState:用于修改,序列化或反序列化的 state。
    • pageProps.passToPage:是否应将每个路由的initialState作为props自动传递给页面组件。
    • debug.mount:传递false以防止在客户端中挂载应用程序 app。您需要自己手动完成此操作,但知道SSRhydration之间的差异。
    • styleCollector:仅在 React 中。JS中提取 CSS 的机制。


    第三个参数

    配置 Vite SSR(服务端)。传递的参数,是一个箭头函数,是 Vite SSR(服务端)的主钩子,它在开始时只运行一次。它接收 SSR 上下文,可用于初始化应用程序或设置状态管理或其他插件的内容。请参见示例:Vue + Pinia

    context:传递给主钩子的上下文,包含:

    • initialState:可以在 SSR 期间更改以保存要序列化的任何数据的对象。同样的对象和数据可以在浏览器中读取。
    • url:初始 URL。
    • isClient:类似于 import.meta.env.SSR 的布尔值。与它不同的是,isClient 不会触发tree shaking
    • request:在 SSR 期间可用。
    • redirect():用于重定向到其他 URL 的同构函数。
    • writeResponse:向响应对象添加状态或头部信息的函数(仅在后端)。
    • router:Vue 中的路由器实例,React 中的自定义路由器,用于访问路由和页面组件。
    • app:App 实例,仅在 Vue 中。
    • initialRoute:初始路由对象,仅在 Vue 中。


    还可以使用useContext钩子,从任何组件访问此上下文context

    import { useContext } from 'vite-ssr'
    
    //...
    function() {
      // In a component
      const { initialState, redirect } = useContext()
      // ...
    }
    



    使用 state

    SSR 中获取 state

    前端开发中,常用到能够跨组件、跨页面,而能共享的统一变量状态,跟 vue 搭配的有 Pinia、vuex储存库。SSR 开发中,需要传递获取状态 state。Vue 向 Vite SSR(服务端)提供初始状态 state,有多种方式:

    通过route.meta.state传递。在输入路由之前调用 Router 的beforeEachbeforeEnter,并填充route.meta.state。Vite SSR(服务端)将获取第一条路由的状态,并将其用作 SSR 初始状态。请参阅:完整示例

    export default viteSSR(
        App, 
        { routes },
        async ({ app, router }) => {
            router.beforEach(async (to, from) => {
            if (to.meta.state) {
               return // Already has state
            }
    
           const response = await fetch('my/api/data/' + to.name)
    
           // This will modify initialState
           to.meta.state = await response.json()
      })
    })
    


    通过Store库传递。在 SSR 期间,在组件serverPrefetch钩子运行时,使用 Pinia(或者 vuex 库),可以获取 state、更改 state、储存 state。在 Nuxt.js 框架中,还可以重新创建 asyncData。

    // 组件
    export default {
      beforeMount() {
        // In browser
        this.fetchMyData()
      },
      async serverPrefetch() {
        // During SSR
        await this.fetchMyData()
      },
      methods: {
        fetchMyData() {
          const store = useStore()
          if (!store.myData) {
            return fetch('my/api/data').then(res => res.json()).then((myData) => {
              store.myData = myData
            })
          }
        },
      },
    }
    
    // Main.ts
    export default viteSSR(App, { routes }, ({ app, initialState }) => {
      // You can pass it to your state management
      // or use `useContext()` like in the Suspense example
      const pinia = createPinia()
    
      // Sync initialState with the store:
      if (import.meta.env.SSR) {
        initialState.pinia = pinia.state.value
      } else {
        pinia.state.value = initialState.pinia
      }
    
      app.use(pinia)
    })
    


    使用Suspense直接从 Vue 组件调用 API,并将结果在 SSR 初始状态 state。在这里查看Suspense的完整示例。如果您喜欢Axios,这里还有一个示例

    import { useContext } from 'vite-ssr'
    import { useRoute } from 'vue-router'
    import { inject, ref } from 'vue'
    
    // This is a custom hook to fetch data in components
    export async function useFetchData(endpoint) {
      const { initialState } = useContext()
      const { name } = useRoute() // this is just a unique key
      const state = ref(initialState[name] || null)
    
      if (!state.value) {
        state.value = await (await fetch(endpoint)).json()
    
        if (import.meta.env.SSR) {
          initialState[name] = state.value
        }
      }
    
      return state
    }
    
    // Page Component with Async Setup
    export default {
      async setup() {
        const state = await useFetchData('my-api-endpoint')
        return { data }
      },
    }
    
    // Use Suspense in your app root
    <template>
      <RouterView v-slot="{ Component }">
        <Suspense>
          <component :is="Component" />
        </Suspense>
      </RouterView>
    </template>
    


    SSR 中序列化 state

    SSR 中initialState,是应用程序数据,是经过序列化处理的,是作为服务端渲染 HTML 的一部分数据,以便以后在浏览器中hydration。通常从 API 代码中,使用 fetch 或 DB requests 收集此数据。

    initialState,由传递给应用程序的普通 JS 对象组成,可以在 SSR 期间随意修改。此对象将被序列化,稍后在浏览器中自动hydration,并再次传递给应用程序,以便您可以将其用作数据源。

    export default viteSSR(App, { routes }, ({ initialState }) => {
      if (import.meta.env.SSR) {
        // Write in server
        initialState.myData = 'DB/API data'
      } else {
        // Read in browser
        console.log(initialState.myData) // => 'DB/API data'
      }
    
      // 根据您的用途,初始状态,可以给提供store、组件等。
    })
    

    Vite SSR(服务端)只使用JSON.stringify用于序列化状态,转义某些字符以阻止 XSS,并将其保存在 DOM 中。如果需要支持 dates、regexp 或函数序列化,可以使用transformState钩子覆盖此行为:

    import viteSSR from 'vite-ssr'
    import App from './app'
    import routes from './routes'
    
    export default viteSSR(App, {
      routes,
      transformState(state, defaultTransformer) {
        if (import.meta.env.SSR) {
          // Serialize during SSR by using,
          // for example, using @nuxt/devalue
          return customSerialize(state)
    
          // -- Or use the defaultTransformer after modifying the state:
          // state.apolloCache = state.apolloCache.extract()
          // return defaultTransformer(state)
        } else {
          // Deserialize in browser
          return customDeserialize(state)
        }
      },
    })
    


    访问响应和请求对象

    在开发中,响应和请求对象都在 SSR 期间传递给主钩子:

    export default viteSSR(
      App,
      { routes },
      ({ initialState, request, response }) => {
        // Access request cookies, etc.
      }
    )
    

    在生产环境中,您可以控制服务器,因此必须将这些对象传递给渲染函数,以便在主钩子中使用它们:

    import render from './dist/server'
    //...
    
    const { html } = await render(url, {
      manifest,
      preload: true,
      request,
      response,
      // Anything here will be available in the main hook.
      initialState: { hello: 'world' }, // Optional prefilled state
    })
    

    请注意,在开发环境中,Vite 使用默认的Node.js+Connect用做中间件。因此,在生产环境,如果使用任何服务器框架(如 Fastify、Express.js 或者 Polka),请求和响应对象,会有所不同。如果您想在开发期间使用自己的服务器,请使用中间件模式。


    编辑响应和重定向

    可以使用writeResponse实用程序为响应设置状态和标头。对于重定向,重定向实用程序可在 SSR(服务器重定向)和浏览器(历史推送)中工作:

    import { useContext } from 'vite-ssr'
    
    // In a component
    function () {
      const { redirect, writeResponse } = useContext()
    
      if (/* ... */) {
        redirect('/another-page', 302)
      }
    
      if (import.meta.env.SSR && /* ... */) {
        writeResponse({
          status: 404,
          headers: {}
        })
      }
    
      // ...
    }
    
    

    在浏览器中,这只会表现为正常的路由器推送。


    仅在客户端/浏览器中渲染

    vite-ssr 导出,仅在浏览器中,渲染其子组件的ClientOnly组件:

    <script setup lang="ts">
    import { ClientOnly } from 'vite-ssr'
    </script>
    
    <template>
    <div>
      <ClientOnly>
        <div>...</div>
      </ClientOnly>
    </div>
    </template>
    


    自定义 TypeScript 类型

    您可以使用 vite-ssr 定义自己的 TypeScript 类型。要声明自定义类型,文件主要需要导入或导出一些内容,而不是破坏其他类型。将请求和响应转换为快递类型的示例:

    import { Request, Response } from 'express'
    
    declare module 'vite-ssr/vue' {
      export interface Context {
        request: Request
        response: Response
      }
    }
    


    使用单独的入口文件:

    尽管 vite-ssr 默认使用 1 个单独的入口文件,从而从应用程序中抽象出复杂性,但如果需要更大的灵活性,您仍然可以为客户端和服务器提供单独的入口文件。例如,在 vite-ssr 上构建库时,可能会发生这种情况。

    只需在 index.html 中,提供客户端入口文件(就像在 SPA 中通常做的那样),并为服务器端提供入口文件,就像传递 CLI 标志一样:vite-ssr[dev|build]--ssr <path/to/entry-server>.

    然后,从vite-ssr/vue/entry-clientvite-ssr/vue/entry-server,导入主处理 SSR 程序文件。为 React,使用vite-ssr/react/*


    head 标签和全局属性

    import { createHead } from '@vueuse/head'
    
    export default viteSSR(App, { routes }, ({ app }) => {
      const head = createHead()
      app.use(head)
    
      return { head }
    })
    
    // In your components:
    // import { useHead } from '@vueuse/head'
    // ... useHead({ ... })
    


    开发环境

    在开发环境中,Vite 使用默认的Node.js+Connect用做中间件。在本机开发环境,运行项目,有两种办法:

    • SPA 模式:vite dev命令直接运行 Vite,无需任何 SSR 服务器。
    • SSR 模式:vite-ssr dev命令启动本地 SSR 服务器。它支持与 Vite CLI 类似的属性,例如Vite-ssr --port 1337 --open

    SPA 模式将稍快一些,但 SSR 模式的行为将更接近生产环境。

    中间件模式

    如果您想运行自己的开发服务器(例如 Express.js)而不是 Vite 的默认Node+Connect,您可以在中间件模式下使用 Vite SSR:

    const express = require('express')
    const { createSsrServer } = require('vite-ssr/dev')
    
    async function createServer() {
      const app = express()
    
      // Create vite-ssr server in middleware mode.
      const viteServer = await createSsrServer({
        server: { middlewareMode: 'ssr' },
      })
    
      // Use vite's connect instance as middleware
      app.use(viteServer.middlewares)
    
      app.listen(3000)
    }
    
    createServer()
    


    生产环境

    运行vite-ssr build命令构建应用程序。这将创建 2 个构建文件(客户端和服务器),您可以从 Node 后端,导入和使用它们。请在此处查看 Express.js 服务器示例

    在 SSR 应用程序中,index.html已经嵌入到服务器构建中,因此从客户端构建中删除,以防止错误地提供它。然而,客户端构建中,若你们想保留index.html的话,(例如,当使用服务器端路由为路由子集选择性地使用 SSR 时),可以设置build选项中的keepIndexHtmltrue

    // vite.config.js
    
    export default {
      plugins: [
        viteSSR({
          build: {
            keepIndexHtml: true,
          },
        }),
        [...]
      ],
    }
    


    @vueuse/head 文档

    @vueuse/head,Vue 组合式 API,用于管理 document head。

    useHead(head: MaybeComputedRef<HeadObject>)

    用于修改 document head。您可以在任何页面或组件中调用此函数。所有值都是响应式的,支持ref和计算属性computedgetter语法。要提供内部内容,您应该使用textContent属性(以前是不推荐使用的子属性)。

    将对提供给useHead的所有值进行编码,以避免XSS注入。如果需要插入原始数据,请使用useHeadRaw

    const myPage = ref({
      description: 'This is my page',
    })
    
    const title = ref('title')
    
    useHead({
      // ref syntax
      title,
      meta: [
        // computer getter syntax
        { name: 'description', content: () => myPage.value.description },
      ],
      style: [
        { type: 'text/css', textContent: 'body { background: red; }' },
      ],
      script: [
        // primitive values are also fine
        { 
          src: 'https://example.com/script.js',
          defer: true
        },
      ],
    })
    

    您可以查看@zhead/schema的完整类型。

    interface HeadObject {
      title?: MaybeRef<string>
      titleTemplate?: MaybeRef<string> | ((title?: string) => string)
      meta?: MaybeRef<HeadAttrs[]>
      link?: MaybeRef<HeadAttrs[]>
      base?: MaybeRef<HeadAttrs>
      style?: MaybeRef<HeadAttrs[]>
      script?: MaybeRef<HeadAttrs[]>
      noscript?: MaybeRef<HeadAttrs[]>
      htmlAttrs?: MaybeRef<HeadAttrs>
      bodyAttrs?: MaybeRef<HeadAttrs>
    }
    


    useHeadRaw(head: MaybeComputedRef<HeadObject>)

    具有与useHead相同的功能,但不编码值。这对于插入原始数据(如脚本和属性事件)很有用。

    插入原始内部内容时,应使用innerHTML

    useHeadRaw({
      bodyAttrs: {
        onfocus: 'alert("hello")',
      },
      script: [
        {
          innerHTML: 'alert("hello world")',
        },
      ],
    })
    

    重复数据消除:对于meta标记,我们使用nameproperty来防止重复标记,如果允许相同的nameproperty,则可以使用key属性:

    useHead({
      meta: [
        {
          property: "og:locale:alternate",
          content: "zh",
          key: "zh",
        },
        {
          property: "og:locale:alternate",
          content: "en",
          key: "en",
        },
      ],
    })
    

    正文标签:要在<body>末尾渲染标记,请在HeadAttrs对象中设置body:true

    useHeadRaw({
      script: [
        {
          children: `console.log('Hello world!')`,
          body: true,
        },
      ],
    })
    

    文本内容:要设置元素的textContent,请使用children属性:

    useHead({
      style: [
        {
          children: `body {color: red}`,
        },
      ],
      noscript: [
        {
          children: `Javascript is required`,
        },
      ],
    })
    

    useHead 还将reactive对象或ref作为参数,例如:

    const head = reactive({ title: "Website Title" })
    useHead(head)
    
    
    const title = ref("Website Title")
    useHead({ title })
    


    <Head>组件

    除了useHead,您还可以使用<Head>组件操作 head 标记:

    <script setup lang="ts">
    import { Head } from "@vueuse/head"
    </script>
    
    <template>
      <Head>
        <title>Hello World</title>
        <base href="/base" />
        <html lang="en-US" class="theme-dark" />
      </Head>
    </template>
    

    请注意,您需要使用<html><body>分别设置 htmlAttrs 和 bodyAttrs,这两个标记的子标记和自动关闭标记(如<meta><link><base>)也会被忽略。


    集成

    用于将@vueuse/head 与框架集成。示例:Vite - SSG

    注册 vue 插件

    import { createApp } from "vue"
    import { createHead } from "@vueuse/head"
    
    const app = createApp()
    const head = createHead()
    
    app.use(head)
    app.mount("#app")
    

    在组件中使用:

    <script>
    import { defineComponent, reactive } from "vue"
    import { useHead } from "@vueuse/head"
    
    export default defineComponent({
      setup() {
        const siteData = reactive({
          title: `My website`,
          description: `My beautiful website`,
        });
        useHead({
          // Can be static or computed
          title: () => siteData.title,
          meta: [
            {
              name: `description`,
              content: () => siteData.description,
            },
          ],
        })
      },
    })
    </script>
    

    服务器端渲染

    import { renderToString } from "@vue/server-renderer"
    import { renderHeadToString } from "@vueuse/head"
    
    const appHTML = await renderToString(yourVueApp)
    
    // `head` is created from `createHead()`
    const { headTags, htmlAttrs, bodyAttrs, bodyTags } = renderHeadToString(head)
    
    const finalHTML = `
    <html${htmlAttrs}>
    
      <head>
        ${headTags}
      </head>
    
      <body${bodyAttrs}>
        <div id="app">${appHTML}</div>
        ${bodyTags}
      </body>
    
    </html>
    `


    API

    createHead(head?: HeadObject | Ref)

    创建 head manager 实例。

    renderHeadToString(head: Head)

    返回HTMLResult

    export interface HTMLResult {
      // Tags in `<head>`
      readonly headTags: string
      // Attributes for `<html>`
      readonly htmlAttrs: string
      // Attributes for `<body>`
      readonly bodyAttrs: string
      // Tags in `<body>`
      readonly bodyTags: string
    }
    

    将 head manager 实例呈现为字符串形式的 HTML 标记。