乐趣区

关于vite:vite-vue3-项目ssr架构改造

这可能是最简略的革新计划了

简介

SSR 是 Server Side Render 简称。
在服务端应用 node 渲染出 html 页面,再传输到客户端。惯例单页利用 html 只返回极少局部 dom 字符串 +script 动静生成网页,不利于 google 爬虫的抓取。因而为了 google 更好的搜录网页,须要在服务端(node)执行 js 以渲染出 html 给 google 爬虫收录。

通用代码

  1. 因为 js 会在 node 端执行,所以不能蕴含客户端浏览器植入的对象,比方 window,location,localStorage,navigator,document 等对象。
  2. 服务端防止状态净化
    官网阐明
    解释: 上面一段代码记录以后用户发动的申请数量

    export const http = axios.create({baseURL:'/api'})
    let count = 0
    http.interceptors.request.use((config) => {
     count++
     return config
    })

上述写法将使 count 长久化,当第二个用户发动申请时,会累加第一个用户的 count,导致统计不正确。

  1. 不能在 node 端运行 setInterval 等代码,因为此类代码对于服务端染没有任何意义,只会造成 node 内存透露,必须限度在客户端运行。

必须的 api

  1. renderToString 能够将组件渲染成 html 字符串(能够用 renderToNodeStream 等流式渲染办法)
  2. Suspense 组件,此组件为 node 端获取数据后渲染的要害,使得由此组件包裹的后辈组件,能够应用 async setup 函数
  3. vite.ssrLoadModule 利用 vite 将 vue 打包后执行,此为调试模式要害。

官网示例代码
但还有 2 个问题

  1. 如何获取数据后渲染
  2. 如何将服务端的申请数据长久化,防止客户端二次申请
1. 获取数据后渲染的办法

在 About 组件中须要获取数据后渲染:

<script setup lang="ts">
const get = () => {
  return new Promise(res => {setTimeout(() => {res(Math.random())
    }, 3000)
  })
}
const b = await get()
</script>
<template>
<div>{{b}}</div>
</template>

app 组件:

<template>
    <Suspense>
          <About/>
    </Suspense>
</template>

服务端代码:

const ctx = {}
const html = await renderToString(App,ctx)

上述 html 会渲染出 b 的数值。其中在组件中能够应用 useSSRContext 获取 ctx。ctx.modules 含有以后所渲染页面须要的依赖 js,css,图片等。可将这些依赖退出预下载,放慢渲染速度。

2. 解决客户端反复申请数据

在服务端,申请数据后,将数据保留到 window 变量中,在客户端再次申请时,发现曾经有数据了,那么就不用在此申请。间接应用 window 中的数据。是否删除次数据,按照状况而定。
此处附一个 axios 的申请做法。
ssrCache.ts

import type {AxiosInstance, AxiosRequestConfig, AxiosResponse, Method} from 'axios'
import md5 from 'md5'
type MapData = {[k: string]: {
        data: any,
        count: number
    }
}
let map = {} as MapData
const getKey = (config: AxiosRequestConfig<any>) => {const { url, data, baseURL, params} = config
    let paramsStr = ''
    for (let key in params) {if (Object.prototype.hasOwnProperty.call(params, key))
            paramsStr += `${key}=${params[key]}`
    }
    return md5(baseURL || '' + url + data + paramsStr).substring(0, 8)
}


export const createProxy = (http: AxiosInstance) => {
    const ssr = typeof window === 'undefined'
    const request = async (config: AxiosRequestConfig<any>) => {const key = getKey(config)

        if (ssr) {if (map[key]) {
                // 只有当页面的数据申请数据做缓存。并且记录申请次数
                map[key].count++
                return map[key].data
            }
            try {const res = await http(config)
                let data = res
                if (res.request) data = res.data
                map[key] = {data, count: 1}
                return res
            } catch (err) {return Promise.reject(err)
            }

        }
        if (window.__SSRDATA__ !== undefined) {
            const map = window.__SSRDATA__
            if (map[key]) {map[key].count-- 
                if (map[key].count === 0) {delete map[key]
                }
                return map[key].data
            }
        }

        return http(config)
    }
    const fn = (method: string) => {return (url: string, config: AxiosRequestConfig<any>) => {return request({ url, method, ...config})
        }
    }

    const set = new Set(["get", "post", "options", "delete", "put", "patch"])
    // 此处做一层 proxy,因为 axios 能够 axios()应用,也能够 axios.get()应用
    // 须要将他们转换成一样的申请模式
    return new Proxy(http, {get(target, key: keyof AxiosInstance) {if (set.has(key.toLowerCase())) {return fn(key)
            }
            return http[key]
        },
        apply(v) {return request(v)
        }
    }) as AxiosInstance
}

export const collect =() => {map={}
    return () => `<script>window.__SSRDATA__=${JSON.stringify(map)}</script>`
}

应用:

const http = createProxy(axios.create({baseURL:''}))

当在 ssr 中应用 http 获取数据,会将所有返回记录到 map 中, 再将其转换为字符串潜入到 html 字符串中

const getScriptString = collect()
let html = await renderToString(App,ctx)
html = getScriptString() + html
退出移动版