UNPKG

vite-ssr-vue2

Version:

Vite utility for vue2 server side rendering

206 lines (199 loc) 6.22 kB
import { createRenderer } from 'vue-server-renderer'; import { renderSSRHead } from '@unhead/ssr'; import Vue from 'vue'; const UNSAFE_CHARS_REGEXP = /[<>\/\u2028\u2029]/g; const ESCAPED_CHARS = { "<": "\\u003C", ">": "\\u003E", "/": "\\u002F", "\u2028": "\\u2028", "\u2029": "\\u2029" }; const escape = (unsafeChar) => ESCAPED_CHARS[unsafeChar]; const serialize = (state) => { try { return JSON.stringify(JSON.stringify(state || {})).replace(UNSAFE_CHARS_REGEXP, escape); } catch (e) { throw new Error(`[SSR] On state serialization - ${e.message}`); } }; const createUrl = (url, base = "http://e.g") => { url = url || "/"; if (url instanceof URL) { return url; } if (!(url || "").includes("://")) { url = base + (url.startsWith("/") ? url : `/${url}`); } return new URL(url); }; const fileType = (file) => { const ext = file.split(".").pop()?.toLowerCase() || ""; if (ext === "js") { return "script"; } else if (ext === "css") { return "style"; } else if (/jpe?g|png|svg|gif|webp|ico/.test(ext)) { return "image"; } else if (/woff2?|ttf|otf|eot/.test(ext)) { return "font"; } return ""; }; const findDependencies = (modules, manifest, shouldPreload, shouldPrefetch) => { const preload = new Set(); const prefetch = new Set(); for (const id of modules || []) { for (const file of manifest[id] || []) { const asType = fileType(file); if (!shouldPreload && asType !== "script" && asType !== "style") { continue; } if (typeof shouldPreload === "function" && !shouldPreload(file, asType)) { continue; } preload.add(file); } } for (const id of Object.keys(manifest)) { for (const file of manifest[id]) { if (!preload.has(file)) { const asType = fileType(file); if (!shouldPrefetch) { continue; } if (shouldPrefetch && !shouldPrefetch(file, asType)) { continue; } prefetch.add(file); } } } return { preload: [...preload], prefetch: [...prefetch] }; }; const renderPreloadLinks = (files) => { const link = []; for (const file of files || []) { const asType = fileType(file); const ext = file.split(".").pop()?.toLowerCase() || ""; if (asType === "script") { link.push(`<link rel="modulepreload" crossorigin href="${file}">`); } else if (asType === "style") { link.push(`<link rel="stylesheet" href="${file}">`); } else if (asType === "font") { link.push(`<link rel="stylesheet" href="${file}" type="font/${ext}" crossorigin>`); } else { link.push(`<link rel="stylesheet" href="${file}">`); } } return link; }; const renderPrefetchLinks = (files) => { const link = []; for (const file of files || []) { link.push(`<link rel="prefetch" href="${file}">`); } return link; }; const findFilesRoute = (route) => { const matched = route?.matched || []; return [...new Set(matched.reduce((ac, row) => { const files = searchFiles(row.components); ac.push(...files); return ac; }, []))]; }; const searchFiles = (components, files = [], level = 0) => { Object.keys(components).forEach((key) => { const current = components[key]; if (current.__id) { files.push(current.__id); } if (current.components) { searchFiles(current.components, files, ++level); } if (current.options) { if (current.options.__id) { files.push(current.options.__id); } if (current.options.components && level < 2) { searchFiles(current.options.components, files, ++level); } } }); return files; }; const createViteSsrVue = (App, options = {}) => { return async (url, { manifest, ...extra } = {}) => { const serializer = options.serializer || serialize; const ssrContext = { url: createUrl(url, `${extra.context.protocol}://${extra.context.hostname}`), isClient: false, initialState: {}, ...extra }; const { head, router, store, inserts, context, app } = options.created && await options.created({ ...ssrContext }) || {}; const vueInst = app || new Vue({ ...options.rootProps ? { propsData: options.rootProps } : {}, ...router ? { router } : {}, ...store ? { store } : {}, render: (h) => h(App) }); if (router && url) { await router.push(url); await new Promise((resolve) => router.onReady(() => resolve(true), (e) => { throw e; })); } options.mounted && await options.mounted({ app: vueInst, router, store, ...ssrContext }); if (store) { ssrContext.initialState.state = store.state; } const renderer = createRenderer(); const body = inserts?.body || await renderer.renderToString(vueInst, Object.assign(ssrContext, context || {})); let headTags = inserts?.headTags || "", htmlAttrs = inserts?.htmlAttrs || "", bodyAttrs = inserts?.bodyAttrs || "", dependencies = inserts?.dependencies || []; if (head) { ({ headTags, htmlAttrs, bodyAttrs } = await renderSSRHead(head)); } if (extra.logModules) { console.log(findFilesRoute(vueInst.$route)); } if (manifest) { const modules = ssrContext.modules || vueInst.$route?.meta?.modules || findFilesRoute(vueInst.$route); const { preload, prefetch } = findDependencies(modules, manifest, options.shouldPreload, options.shouldPrefetch); dependencies = preload; if (preload.length > 0) { const links = renderPreloadLinks(preload); headTags += links.length ? "\n" + links.join("\n") : ""; } if (prefetch.length > 0) { const links = renderPrefetchLinks(prefetch); headTags += links.length ? "\n" + links.join("\n") : ""; } } options.rendered && await options.rendered({ app: vueInst, router, store, ...ssrContext }); const initialState = await serializer(ssrContext.initialState || {}); return { html: `__VITE_SSR_VUE_HTML__`, htmlAttrs, bodyAttrs, headTags, body, initialState, dependencies }; }; }; export { createViteSsrVue as default };