import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' import express from 'express' import serverStatic from 'serve-static' import { createServer as createViteServer } from 'vite' import ssrManifest from './dist/client/.vite/ssr-manifest.json' assert { type: "json" } import * as PRODserver from './dist/server/entry-server.js' const isProd = process.env.NODE_ENV === 'production' const __dirname = path.dirname(fileURLToPath(import.meta.url)) async function createServer() { const app = express() // 以中间件模式创建 Vite 应用,并将 appType 配置为 'custom' // 这将禁用 Vite 自身的 HTML 服务逻辑 // 并让上级服务器接管控制 const vite = await createViteServer({ server: { middlewareMode: 'ssr' }, appType: 'custom' }) if(!isProd){ // 使用 vite 的 Connect 实例作为中间件 // 如果你使用了自己的 express 路由(express.Router()),你应该使用 router.use // 当服务器重启(例如用户修改了 vite.config.js 后), // `vite.middlewares` 仍将保持相同的引用 // (带有 Vite 和插件注入的新的内部中间件堆栈)。 // 即使在重新启动后,以下内容仍然有效。 app.use(vite.middlewares) }else{ app.use(serverStatic(path.resolve(__dirname,'dist/client'),{index:false})) } // 参考https://cn.vitejs.dev/guide/ssr.html app.use('*', async(req, res) => { const url = req.originalUrl let template let render let html try { if(!isProd){ console.log("开启开发服务端渲染") // 1. 读取 index.html template = fs.readFileSync( path.resolve(__dirname, 'index.html'), 'utf-8', ) // 2. 应用 Vite HTML 转换。这将会注入 Vite HMR 客户端, // 同时也会从 Vite 插件应用 HTML 转换。 // 例如:@vitejs/plugin-react 中的 global preambles template = await vite.transformIndexHtml(url, template) // 3a. 加载服务器入口。vite.ssrLoadModule 将自动转换 // 你的 ESM 源码使之可以在 Node.js 中运行!无需打包 // 并提供类似 HMR 的根据情况随时失效。 render = (await vite.ssrLoadModule('/src/entry-server.ts')).render // 3b. 从 Vite 5.1 版本开始,你可以试用实验性的 createViteRuntime // API。 // 这个 API 完全支持热更新(HMR),其工作原理与 ssrLoadModule 相似 // 如果你想尝试更高级的用法,可以考虑在另一个线程,甚至是在另一台机器上, // 使用 ViteRuntime 类来创建运行环境。 // const runtime = await vite.createViteRuntime(server) // const { render } = await runtime.executeEntrypoint('/src/entry-server.ts') }else{ console.log("开启生产服务端渲染") template = fs.readFileSync( path.resolve(__dirname, 'dist/client/index.html'), 'utf-8', ) render = PRODserver.render } // 4. 渲染应用的 HTML。这假设 entry-server.js 导出的 `render` // 函数调用了适当的 SSR 框架 API。 // 例如 ReactDOMServer.renderToString() if(!isProd){ const { appHtml, state } = await render(url) const { title,keywords, description } = state.meta // 5. 注入渲染后的应用程序 HTML 到模板中。 html = template .replace(``, appHtml) .replace('\'\'', JSON.stringify(state)) .replace('',`<title>${title}`) .replace('<meta name="keywords" content="">',`<meta name="keywords" content="${keywords}">`) .replace('<meta name="description" content="">',`<meta name="description" content="${description}">`) }else{ const manifest = ssrManifest const { appHtml, state,preloadLinks } = await render(url,manifest) const { title,keywords, description } = state.meta html = template .replace(`<!--preload-links-->`, preloadLinks) .replace(`<!--ssr-outlet-->`, appHtml) .replace('\'<!--vuex-state-->\'', JSON.stringify(state)) .replace('<title>',`<title>${title}`) .replace('<meta name="keywords" content="">',`<meta name="keywords" content="${keywords}">`) .replace('<meta name="description" content="">',`<meta name="description" content="${description}">`) } // 6. 返回渲染后的 HTML。 res.status(200).set({ 'Content-Type': 'text/html' }).end(html) } catch (e) { // 如果捕获到了一个错误,让 Vite 来修复该堆栈,这样它就可以映射回 // 你的实际源码中。 // vite.ssrFixStacktrace(e) // console.log(e) // next(e) } }) app.listen(5173, () => { console.log("node server.js",isProd ? `运行 生产环境` : `运行 开发环境`) }) } createServer()