vite-ssr-vue2
Version:
Vite utility for vue2 server side rendering
206 lines (199 loc) • 6.22 kB
JavaScript
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 };