百科狗-知识改变命运!
--

vite + vue3 + ts + SSR 使用 vite-ssr 插件 - vue3 项目实战

梵高1年前 (2023-11-21)阅读数 22#技术干货
文章标签组件

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-ssr、vue-router、@vueuse/head

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

安装:Pinia、axios

yarn add pinia axios

查看已经安装的包

yarn list --depth=0


vite 配置别名 alias

安装 node TypeScript 类型支持

yarn add -D @types/node

修改vite.config.ts、tsconfig.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.vue、AboutView.vue、CountNunber.vue:

// src/views/HomeView.vue



  主页
  


main {
  width: 100%;
  font-size: 36px;
  text-align: center;
  margin-top: 10rem;
} 
// CountNunber.vue 
count is {{ count }}

{{ msg }}

// AboutView.vue 

关于

修改根组件App.vue:

// App.vue



  
    Home
    About
  
  



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;
} 


使用 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),它将负责为代码提供正确的环境。查看完整示例:Vue、React。

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。您需要自己手动完成此操作,但知道SSR和hydration之间的差异。
  • 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 


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组件:

...


自定义 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 .

然后,从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)

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

vite + vue3 + ts + SSR 使用 vite-ssr 插件 - vue3 项目实战

将对提供给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
  titleTemplate?: MaybeRef | ((title?: string) => string)
  meta?: MaybeRef
  link?: MaybeRef
  base?: MaybeRef
  style?: MaybeRef
  script?: MaybeRef
  noscript?: MaybeRef
  htmlAttrs?: MaybeRef
  bodyAttrs?: MaybeRef
}


useHeadRaw(head: MaybeComputedRef)

具有与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",
    },
  ],
})

正文标签:要在末尾渲染标记,请在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 })


组件

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

 Hello World 

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


集成

用于将@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")

在组件中使用:

服务器端渲染

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 = `


  
    ${headTags} 
${appHTML}
${bodyTags} `


API

createHead(head?: HeadObject | Ref)

创建 head manager 实例。

renderHeadToString(head: Head)

返回HTMLResult

export interface HTMLResult {
  // Tags in ``
  readonly headTags: string
  // Attributes for ``
  readonly htmlAttrs: string
  // Attributes for ``
  readonly bodyAttrs: string
  // Tags in ``
  readonly bodyTags: string
}

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

鹏仔微信 15129739599 鹏仔QQ344225443 鹏仔前端 pjxi.com 共享博客 sharedbk.com

免责声明:我们致力于保护作者版权,注重分享,当前被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理!邮箱:344225443@qq.com)

图片声明:本站部分配图来自网络。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

内容声明:本文中引用的各种信息及资料(包括但不限于文字、数据、图表及超链接等)均来源于该信息及资料的相关主体(包括但不限于公司、媒体、协会等机构)的官方网站或公开发表的信息。部分内容参考包括:(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供参考使用,不准确地方联系删除处理!本站为非盈利性质站点,本着为中国教育事业出一份力,发布内容不收取任何费用也不接任何广告!)