使用Vite+Vue3搭建前端SSR应用(二)搭建SSR服务端渲染

1 引言

使用Vite+Vue3搭建前端SSR应用

相关文章

  • 使用Vite+Vue3搭建前端SSR应用(一)新建项目并配置基础环境
  • 使用Vite+Vue3搭建前端SSR应用(二)搭建SSR服务端渲染
  • 使用Vite+Vue3搭建前端SSR应用(三)SEO网页头配置
  • 使用Vite+Vue3搭建前端SSR应用(四)应用的部署

关键词

  • Vite
  • Vue
  • SSR
  • SEO

源代码地址
https://gitee.com/bitem/vite-vue-ssr-demo.git

2 安装依赖

# 有些依赖前面已经装过了,重复安装没关系的
yarn add vite-ssr vue-router @vueuse/head axios
yarn add -D @types/node vite-plugin-pages less
1
2
3

3 关键代码改造

// vite.config.ts 中加入viteSSR插件
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Pages from 'vite-plugin-pages'
import viteSSR from 'vite-ssr/plugin.js'

export default defineConfig({
  plugins: [
    viteSSR({
      build: {
        keepIndexHtml: true,
      },
    })
  ]
})
// 无关代码忽略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/route.ts 中删除多余代码直接导出routes即可
import routes from '~pages'
export { routes }
1
2
3
// src/main.ts
import '@/assets/index.less'
import App from './App.vue'
import { routes } from "./route"
import viteSSR, { ClientOnly } from 'vite-ssr'
import { createHead } from '@vueuse/head'
import { ApiBase } from './api/config/api.config'

const options = {
  routes: routes
}

// 导出viteSSR函数,vite-ssr只有一个入口,服务端和客户端共用一个端口,系统会自动完成环境区分
export default viteSSR(App, options, (context) => {
  const { app, router } = context

  const head = createHead()
  app.use(head)

  app.component(ClientOnly.name, ClientOnly)

  router.beforeEach(async (to: any, from: any, next: any) => {
    if (!!to.meta.state && Object.keys(to.meta.state).length > 0) {
      // 已经存在数据,不需要在请求了
      return next()
    }
    try {
      // 这里为了演示,直接请求固定路径的接口,正式环境,可根据路由匹配对应的后端接口
      console.log("router =>",to.name)
      // 这里的ApiBase需要根据环境判断,如果是SSR,则必须是全路径
      // export const ApiBase = import.meta.env.SSR ? "http://www.bitem.cn" : ""
      const res = await fetch(ApiBase + '/api/web/article/a8feb225-ae66-288a-5f21-0f9deafe6a80',
        {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json',
          },
        }
      )
      // 存储数据
      to.meta.state = await res.json()
    } catch (error) {
      // 跳转错误页
      console.error(error)
    }
    next()
  })

  return { head }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<!-- src/views/about.vue -->
<script lang="ts">
import { defineComponent, ref } from 'vue'
import { useContext } from 'vite-ssr'

export default defineComponent({

  methods: {
    onButtonClick() {
      this.count++
    }
  },
  setup() {
    const count = ref(0)
    // 这里通过useContext取出初始数据,页面就可以进行SSR渲染了
    const { initialState } = useContext()
    return {
      count,
      initialState
    }
  }
})
</script>

<template>
  <div class="view-about">
    <div>
    这是关于 <button @click="onButtonClick">{{ count }}</button>
  </div>
  <p>
    {{initialState}}
  </p>
  </div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

4 结果展示

ssr-result.png

5 直接在Vue组件中请求数据进行服务端渲染

这是一个非常友好的功能,我们可以像平常开发那样开发SSR

删除main.ts中的router.beforeEach的代码

// main.ts 只保留以下代码
import '@/assets/index.less'
import App from './App.vue'
import { routes } from "./route"
import viteSSR, { ClientOnly } from 'vite-ssr'
import { createHead } from '@vueuse/head'

const options = {
  routes: routes
}

export default viteSSR(App, options, (context) => {
  const { app } = context
  const head = createHead()
  app.use(head)
  app.component(ClientOnly.name, ClientOnly)
  return { head }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

新建请求函数

// src/api/init/about.ts
import { useContext } from 'vite-ssr'
import { useRoute } from 'vue-router'
import { ref } from 'vue'
import { ApiBase } from '../config/api.config'

export async function getInitData() {
  const { initialState } = useContext()
  // 这只是一个唯一值,这里直接取路由的名称,也可以直接自定义,如:view-about 或者 component-helloworld
  const { name } = useRoute()
  const state = ref(initialState[name as string] || null)

  if (!state.value) {
    state.value = await (await fetch(ApiBase + '/api/web/article/a8feb225-ae66-288a-5f21-0f9deafe6a80')).json()
    // SSR需要将state的值赋值给initialState,这样任何地方都可以直接调用这个值,并且,如果不赋值的话服务端和客户端会请求两次数据造成浪费
    if (import.meta.env.SSR) {
      initialState[name as string] = state.value
    }
  }

  return state
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

调用初始化数据请求函数

<!-- src/views/about.vue -->
<script lang="ts">
import { defineComponent, ref } from 'vue'
import { getInitData } from "@/api/init/about"

export default defineComponent({
  methods: {
    onButtonClick() {
      this.count++
    }
  },
  async setup() {
    const count = ref(0)
    
    // setup异步函数直接同步请求数据后进行SSR渲染
    const state = await getInitData()

    return {
      count,
      state
    }
  }
})
</script>

<template>
  <div class="view-about">
    <div>
      这是关于 <button @click="onButtonClick">{{ count }}</button>
    </div>
    <p>
      {{ state }}
    </p>
  </div>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

6 结果展示

这张图是将src/api/init/about.ts中赋值initialState注释掉的结果,可以看到没有__INITIAL_STATE__照样能够SSR渲染,只不过请求过来的数据是一次性的,客户端会再次执行数据请求(仔细看图能够发现有一个请求接口的记录)
ssr-result-no-initial-state.png

这张图是正常的结果,请求只发生了一次,服务端请求后,客户端共用服务端的数据,所以图中没有请求接口的记录
ssr-result-with-initial-state.png