vite ssr服务端渲染

8 篇文章 0 订阅
3 篇文章 0 订阅

阅读 Vue文档 这一章里有说过,vue是支持服务端渲染的。
通过createSSRApp创建vue组件实例,并使用renderToString将在服务器渲染好template并返回字符串结构,通过替换占位字符将渲染好的字符串输出到html上,这样的一个过程就实现了服务端渲染。
Vite 文档也提到了如何去渲染SSR并提供了相关示例

在这里插入图片描述
那么今天我们就按照官方给的示例来完成vue ssr的改造
使用Node Koa框架来做服务器,且使用vue全家桶(router,pinia)开发项目。
router配置这里需要注意,在服务器端路由模式为createMemoryHistory,在客户端则history or hash随意

const router = createRouter({
  routes,
  history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory()
});

使用pinia则是为了预取数据,pinia文档中明确说明了支持SSR。而我们使用pinia则是在服务器端获取数据后 基于服务器时期和客户端时期是两个完全不同的环境,在客户端是没法访问到服务器时期的数据的。所以我们需要在服务器获取数据后保存到Pinia里,避免客户端再发起同样的请求。

在根目录的html文件里添加占位符(名字可以随意取 但是在server.js里replace替换的时候记得同步修改)
在这里插入图片描述
<!--preload-links--> 预加载的css style等资源
<!--pinia-state--> pinia里保存的数据
<!--ssr-outlet--> ssr渲染的节点
同时入口文件的地址应由main文件改成enter-client文件!!因为html是运行在浏览器里的,main文件下文已经改造成了通用逻辑,客户端真正的入口文件应该是enter-client。

开始

在这里插入图片描述

server.js

用于启动Node服务来实现服务端渲染,用到的Node模块有fs文件读取,path路径以及url处理,使用的node服务器则是Koa。

import Koa from "koa";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const resolve = (p) => path.resolve(__dirname, p);

需要注意的是 要区分开发环境与生产环境。开发环境读取本地开发的代码,而生产环境则读取打包后的代码。

async function createServerApp(
  root = process.cwd(),
  isProd = process.env.NODE_ENV === "production"
) {
  const app = new Koa();
  const indexProd = isProd
    ? fs.readFileSync(resolve("./dist/client/index.html"), "utf-8")
    : fs.readFileSync(resolve("index.html"), "utf-8");
  const manifest = isProd
    ? fs.readFileSync(resolve("./dist/client/.vite/ssr-manifest.json"), "utf-8")
    : undefined;
  let vite;
  if (isProd) {
    // 压缩
    app.use((await import("koa-compress")).default());
    // 设置静态目录
    app.use(
      (await import("koa-static")).default(resolve("./dist/client"), {
        index: false,
      })
    );
  } else {
    // 开发环境
    vite = await (
      await import("vite")
    ).createServer({
      server: { middlewareMode: true },
      appType: "custom",
      // base,
    });
    app.use((await import("koa-connect")).default(vite.middlewares));
  }
  app.use(async (ctx) => {
    try {
      let template, render;
      if (isProd) {
        template = indexProd;
        render = (await import("./dist/server/entry-server.js")).render;
      } else {
        template = await vite.transformIndexHtml(ctx.originalUrl, indexProd);
        render = (await vite.ssrLoadModule("/src/entry-server.ts")).render;
      }
      const {
        html: appHtml,
        preloadLinks,
        piniaState,
      } = await render(ctx.originalUrl, manifest);
      const html = template
        .replace("<!--ssr-outlet-->", appHtml ?? "")
        .replace("<!--preload-links-->", preloadLinks ?? "")
        .replace("<!--pinia-state-->", piniaState ?? "");

      ctx.type = "text/html";
      ctx.body = html;
    } catch (e) {
      // 兜底 防止报错直接崩溃
      vite && vite.ssrFixStacktrace(e);
      ctx.status = 500;
      ctx.body = e.stack;
    }
  });
  return {
    app,
  };
}

开启服务

createServerApp().then(({ app }) => {
  app.listen(2000, () => {
    console.log(`[ssr server] run http://localhost:2000`);
  });
});

src 目录下改造:

main.js

导出通用的代码,前面提到的vue ssr是通过createSSRApp来创建实例。

这里的initialState就是服务器保存的对象
import.meta.env.SSR 则是vite自带的变量用于判断当前所处的环境

import { createSSRApp } from 'vue';
import App from './App.vue';
import router from './router';
import { createPinia } from 'pinia';
import './assets/styles';

export function createApp() {
  const app = createSSRApp(App);
  const store = createPinia();
  const initialState: {
    pinia: null | typeof store.state.value
  } = {
    pinia: null
  };
  app.use(router).use(store);
  if (import.meta.env.SSR) {
    initialState.pinia = store.state.value
  } else {
    store.state.value = window.__INITIAL_STATE__
  }
  return {
    app,
    router,
    store,
    initialState,
  }
}

entry-client.js

enter-client做的事情很简单,就是等路由处理好后挂载到页面上。

import { createApp } from './main';

const { app, router } = createApp();

router.isReady().then(() => app.mount('#app'))

entry-server.js

entry-server做的事情是调用renderToString于服务器环境去渲染好页面结构并返回字符串结构方便替换占位符。通过render方法把处理好的html节点,预渲染的资源以及pinia保存的数据return 出来。

import { renderToString } from 'vue/server-renderer';
import { createApp } from './main';
import { basename } from 'node:path';
import devalue from '@nuxt/devalue';

export async function render(path: string, manifest: any) {
  const { app, router, initialState } = createApp();
  router.push(path);
  await router.isReady();
  // 在这里预取数据并传回到pinia
  const ctx: any = {};
  const html = await renderToString(app, ctx);
  const preloadLinks = import.meta.env.PROD ? renderPreloadLinks(ctx.modules, manifest) : undefined;

  // https://pinia.vuejs.org/ssr/#state-hydration
  const piniaState = devalue(initialState.pinia)
  return {
    html,
    preloadLinks,
    piniaState,
  }
}

function renderPreloadLinks(modules: string[], manifest: any) {
  let links = ''
  const seen = new Set()
  modules.forEach((id) => {
    const files = manifest[id]
    if (files) {
      files.forEach((file: string) => {
        if (!seen.has(file)) {
          seen.add(file)
          const filename = basename(file)
          if (manifest[filename]) {
            for (const depFile of manifest[filename]) {
              links += renderPreloadLink(depFile)
              seen.add(depFile)
            }
          }
          links += renderPreloadLink(file)
        }
      })
    }
  })
  return links
}

function renderPreloadLink(file: string) {
  if (file.endsWith('.js')) {
    return `<link rel="modulepreload" crossorigin href="${file}">`
  } else if (file.endsWith('.css')) {
    return `<link rel="stylesheet" href="${file}">`
  } else if (file.endsWith('.woff')) {
    return ` <link rel="preload" href="${file}" as="font" type="font/woff" crossorigin>`
  } else if (file.endsWith('.woff2')) {
    return ` <link rel="preload" href="${file}" as="font" type="font/woff2" crossorigin>`
  } else if (file.endsWith('.gif')) {
    return ` <link rel="preload" href="${file}" as="image" type="image/gif">`
  } else if (file.endsWith('.jpg') || file.endsWith('.jpeg')) {
    return ` <link rel="preload" href="${file}" as="image" type="image/jpeg">`
  } else if (file.endsWith('.png')) {
    return ` <link rel="preload" href="${file}" as="image" type="image/png">`
  } else {
    // TODO
    return ''
  }
}

命令行配置

配置package.json 里的 scripts脚本命令

	"dev": "node server",
    "build": "npm run build:client && npm run build:server",
    "build:client": "vite build --ssrManifest --outDir dist/client",
    "build:server": "vite build --ssr src/entry-server.ts --outDir dist/server",
    "preview": "cross-env NODE_ENV=production node server",
    "start": "set NODE_ENV=production && node server"

通过dev命令启动开发环境
生产环境则需要先build构建完后调用start开启服务

本篇代码仓库地址 github仓库直达~

  • 25
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
好的,我会根据您的要求回答您的问题,以下是从0到1塔建vite+vue3的ssr服务端渲染的使用方法,不使用ts: 1. 首先,安装Vite和Vue3 ``` npm install -g vite npm install vue@next ``` 2. 创建项目并初始化 ``` mkdir my-ssr-app cd my-ssr-app npm init -y ``` 3. 安装依赖 ``` npm install vue-server-renderer express ``` 4. 创建服务端入口文件 在项目根目录下创建`server.js`文件,并添加以下内容: ```javascript const express = require('express') const { createRenderer } = require('vue-server-renderer') const { createApp } = require('./src/main') const app = express() // 静态资源目录 app.use(express.static('dist')) const renderer = createRenderer() app.get('*', async (req, res) => { const app = createApp() const html = await renderer.renderToString(app) res.end(` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>SSR Vite Vue 3</title> </head> <body> <div id="app">${html}</div> <script src="/client.js"></script> </body> </html> `) }) app.listen(3000, () => { console.log('Server started at http://localhost:3000') }) ``` 5. 创建客户端入口文件 在项目根目录下创建`src/main.js`文件,并添加以下内容: ```javascript import { createApp } from 'vue' import App from './App.vue' export function createApp () { const app = createApp(App) return { app } } ``` 6. 配置Vite 在项目根目录下创建`vite.config.js`文件,并添加以下内容: ```javascript const { createVuePlugin } = require('vite-plugin-vue2') module.exports = { plugins: [ createVuePlugin() ], build: { ssrManifest: true, outDir: 'dist', rollupOptions: { input: 'src/entry-client.js', output: { format: 'esm', entryFileNames: '[name]-[hash].js' } } }, optimizeDeps: { include: [ 'vue', 'vue-router' ] } } ``` 7. 创建组件 在`src`文件夹下创建`App.vue`文件,并添加以下内容: ```vue <template> <div> <h1>{{ message }}</h1> </div> </template> <script> export default { data () { return { message: 'Hello World!' } } } </script> ``` 8. 运行项目 使用以下命令启动项目: ``` vite build && node server.js ``` 9. 查看效果 在浏览器中访问`http://localhost:3000`,即可看到页面渲染结果。 以上就是从0到1塔建vite+vue3的ssr服务端渲染的使用方法,不使用ts的步骤。希望对您有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值