再也不会分不清CSR、SSR、同构、SSG···

前言

现在前端一谈到SSR,不约而同都想到 Nuxt 和 Next 这两个非常有名的SSR框架。

但是在现有的SPA单页面应用中直接使用第三方的SSR框架(Nuxt、Next)重构项目,会大费周章。

但是如果本身对SSR的原理认识比较模糊,那么在现有的项目中,手动集成SSR又会变得无从而手,并且极其复杂。

所以本章带大家从头梳理一下 CSR、SSR、同构、SSG 等相关概念,再带你从头如何在现有项目中手动集成SSR,自己动手实现SSR,也会让你更加了解第三方的SSR框架(Nuxt、Next)的一些底层实现。

CSR、SSR及同构渲染

SSR(传统服务端渲染)

传统的服务端渲染有:asp、jsp、ejs等,服务端语言往往通过这些模板引擎将数据和dom在服务端渲染完成,返回一个完整的静态html页面给客户端,由客户端直接显示。

原理

  1. 客户端发送http请求
  2. 服务端响应http请求,返回拼接好的html字符串给客户端
  3. 客户端渲染html

缺点

  • 前后端分离,不好维护
  • 用户体验不佳,需要重新加载页面
  • 服务端压力大

CSR(客户端渲染)

在现代化的前端项目中,客户端渲染的代表性技术栈是Vue、React、Angular,我们常常使用它们来构建客户端单页应用程序。以SPA构建程序为例,在浏览器端首先渲染的是一套空的HTML,通过JS直接进行页面的渲染和路由跳转等操作,所有的数据通过ajax请求从服务器获取后,在进行客户端的拼装和展示。

原理

  1. 客户端发起http请求
  2. 服务端响应http请求,返回一个空的根元素的 html 文件
  3. 客户端初始化时加载必须的 js文件,请求接口
  4. 将生成的dom插入到 html 中

缺点

  • 首屏加载慢
  • 不利于SEO

同构(现代服务端渲染)

一个由服务端渲染的 Vue.js 应用也可以被认为是"同构的"(Isomorphic) 或"通用的"(Universal),因为应用的大部分代码同时运行在服务端客户端。------Vue3官网

这里说的SSR 特别指支持在 Node.js 中运行前端框架(例如 React、Preact、Vue 和 Svelte),将其预渲染成 HTML,最后在客户端进行水合处理。如果你正在寻找与传统服务器端框架的集成,请查看 后端集成指南。------Vite官网

Vue、React框架的SSR方案(Nuxt、Next)实际上就是同构渲染,这里的SSR,是指在前端单页面应用范畴内,基于Node.js server运行环境的服务端渲染方案,通过在 Node.is 中运行相同应用程序的前端框架(例如 React、Vue等),将其预渲染成 HTML,最后在客户端进行注水化处理。简单来讲,就是应用程序的大部分代码在服务端(node服务端)和客户端上运行,这就是所谓的现代服务端渲染:同构。

原理

  1. 客户端发起 http 请求
  2. 服务端渲染把 Vue 实例转换成了静态的 html 发送给客户端
  3. 客户端渲染需要把事件、响应式特性等 Vue 的特性都绑回去(水合或者叫激活)

优点

  • 首屏速度快 :这一点在慢网速或者运行缓慢的设备上尤为重要。服务端渲染的 HTML 无需等到所有的 JavaScript 都下载并执行完成之后才显示,所以用户将会更快地看到完整渲染的页面。除此之外,数据获取过程在首次访问时在服务端完成,相比于从客户端获取,可能有更快的数据库连接。这通常可以带来更高的核心 Web 指标评分、更好的用户体验,而对于那些"首屏加载速度与转化率直接相关"的应用来说,这点可能至关重要。
  • 统一的心智模型:有一些现成框架(Nuxt.js、Next.js)可以使用相同的语言以及相同的声明式、面向组件的心智模型来开发整个应用,而不需要在后端模板系统和前端框架之间来回切换。
  • 更好的 SEO:搜索引擎爬虫可以直接看到完全渲染的页面。

缺点

  • 开发中的限制。浏览器端特定的代码只能在某些生命周期钩子中使用;一些外部库可能需要特殊处理才能在服务端渲染的应用中运行。
  • 更多的与构建配置和部署相关的要求。服务端渲染的应用需要一个能让 Node.js 服务器运行的环境,因为需要 Node.js 来执行JS代码和构建用户页面,不像完全静态的 SPA 那样可以部署在任意的静态文件服务器上。
  • 更高的服务端负载。在 Node.js 中渲染一个完整的应用要比仅仅托管静态文件更加占用 CPU 资源,因此如果你预期有高流量,请为相应的服务器负载做好准备,并采用合理的缓存策略。

判断一个网页是纯SSR、CSR、同构?

如何区分页面是CSR还是SSR?

一般可以通过查看网页的源代码,如果body标签里包含了网页的所有内容html标签,那就是SSR服务端渲染,在服务端生成完整HTML并发送给客户端;

如果body标签里只有少数几个html标签元素,那就是SPA单页面应用,属于CSR,是在客户端通过JS动态构建页面。

也可以通过复制一段页面文本,看网页源代码是否可以搜索到,如果搜索不到就是CSR。反之就是SSR。

如何查看SSR是否是同构渲染?

同构渲染也属于SSR范畴,只不过用的是Vue、React等前端技术栈实现的,比如飞书官网就是利用React技术栈(Nextjs)实现的同构渲染:

Vue项目手动集成SSR

在现有的 Vue 项目中,利用 Vite 脚手架,手动改造成 SSR,包括 Vue-Router、Pinia、数据预取等。

基本原理

  • 通过 Vue 的 server-renderer 模块将 Vue 应用实例转换成一段纯文本的 HTML 字符串
  • 通过 Nodejs 创建一个静态 Web 服务器
  • 通过 Nodejs 将服务端所转换好的 HTML 结构发送到浏览器端进行展示。也就是说部署 SSR 项目,需要服务器提供运行 Node 的环境,即安装 Node。

上图构建过程中的Webpack替换为Vite

根据 Vite 对 SSR渲染的介绍(点击查看Vite官网集成SSR指南),一个典型的 SSR 应用程序的目录结构如下:

markdown 复制代码
- index.html
- server.js 				 # 执行SSR入口文件
- src/
  - main.ts          # 导出环境无关的(通用的)应用代码
  - entry-client.ts  # 激活应用挂载到一个 DOM 元素上
  - entry-server.ts  # 使用 Vue 框架的 SSR API 渲染该应用

下面是从0搭建一个Vue框架的SSR项目模板:首先pnpm create vue@latest创建一个Vue3项目(包括 ts、vue-router、pinia),手动添加三个文件server.jsentry-client.tsentry-server.ts

main.ts

修改原始main.ts文件,为了激活应用,必须使用 createSSRApp() 而不是 createApp()

typescript 复制代码
import { createSSRApp } from 'vue' // 为了激活应用,必须使用 createSSRApp() 而不是 createApp()
import App from './App.vue'
import { createRouter } from './router' // 返回一个方法,每次创建新的 Vue-Router 实例
import { createPinia } from 'pinia'

// 每次请求时调用
export function createApp() {
    // 创建一个和服务端完全一致的应用实例
    const app = createSSRApp(App)
    // 对每个请求都创建新的 Vue-Router 实例
    const router = createRouter()
    // 对每个请求都创建新的 pinia 实例
    const pinia = createPinia()

    app.use(router)
    app.use(pinia)
    return { app, router, pinia }
}

// 预取接口数据
// 有两个地方用到,客户端 entry-client.ts 和服务端 entry-server.ts
// 因为是服务端渲染,所以肯定要预取数据,然后将状态序列化为window.__INITIAL_STATE__,注入到HTML
// 客户端也会预取数据,但是会做限制,只有打开页面才会获取数据,激活页面。
// 刷新页面客户端不会二次获取数据,而是 从window.__INITIAL_STATE__恢复数据,减轻服务器压力;但是服务端会重新获取数据,因为要返回新的 HTML
// 有一点需要注意,现在路由是由前端路由控制,路由跳转的时候不会真实请求服务端,所以只会在客户端预取数据,服务端不会预取数据。
// 所以有时候查看源代码的时候,会发现服务端返回 HTML 里的状态window.__INITIAL_STATE__是旧的。要想永远都是新数据,那就必须改造路由跳转,变成window.location.ref的方式跳转
export function asyncData(actived: any, route: any) {
    return Promise.all(actived.map((Component: any) => {
        if (Component.asyncData) {
            return Component.asyncData({
                route
            })
        }
    }))
}

router.ts

Vue-Router 需要针对 CSR 和 SSR 渲染,选择不同的 history 模式:

javascript 复制代码
import {
  createRouter as _createRouter,
  createMemoryHistory, // 在服务端使用 createMemoryHistory() 函数创建历史记录
  createWebHistory,
} from 'vue-router'
// 自动生成./pages 目录下文件路由
// https://vitejs.dev/guide/features.html#glob-import
// const pages = import.meta.glob('./pages/*.vue')

// const routes = Object.keys(pages).map((path) => {
//   const name = path.match(/./pages(.*).vue$/)![1].toLowerCase()
//   return {
//     path: name === '/home' ? '/' : name,
//     component: pages[path], // () => import('./pages/*.vue')
//   }
// })

export function createRouter() {
  return _createRouter({
    // import.meta.env.SSR 由 Vite 注入提供
    history: import.meta.env.SSR
      ? createMemoryHistory('/')
      : createWebHistory('/'),
    // routes,
    routes: [
      {
        path: '/',
        name: 'home',
        // route level code-splitting
        // this generates a separate chunk (About.[hash].js) for this route
        // which is lazy-loaded when the route is visited.
        component: () => import('../views/HomeView.vue'),
        // SEO优化
        meta: {
          title: '首页',
          keywords: 'SSR,Vue,Vite,Home',
          description: '这是vue-ssr-vite项目的首页',
        }
      },
      {
        path: '/about',
        name: 'about',
        component: () => import('../views/AboutView.vue'),
        meta: {
          title: '关于',
          keywords: 'SSR,Vue,Vite,About',
          description: '这是vue-ssr-vite项目的关于页',
        }
      }
    ]
  })
}

index.html

替换默认的入口文件main.tsentry-client.ts,同时放置占位符<!--preload-links--><!--app-html-->等,用于给服务端渲染的时候注入内容;同时设置全局变量__INITIAL_STATE__,用于保存pinia数据,每次刷新页面就不用重新获取接口,直接恢复保存的pinia数据即可:

xml 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title></title>
    <meta name="keywords" content="" />
    <meta name="description" content="" />
    <!--preload-links-->
  </head>
  <body>
    <div id="app"><!--app-html--></div>
    <script type="module" src="/src/entry-client.ts"></script>
    <script>
      window.__INITIAL_STATE__ = '<!--app-state-->';
    </script>
  </body>
</html>

server.js

server.js 是 SSR 的入口文件,处理 index.html,然后返回给客户端:

javascript 复制代码
import fs from 'node:fs/promises';
import express from 'express';

// 常量
const isProduction = process.env.NODE_ENV === 'production';
const port = process.env.PORT || 5173;
const base = process.env.BASE || '/';

// 缓存生产环境资源
const templateHtml = isProduction
  ? await fs.readFile('./dist/client/index.html', 'utf-8')
  : '';
const ssrManifest = isProduction // 在客户端构建过程中会生成ssr-manifest.json预加载配置,该文件包含所有模块Id的映射,可以解决样式错乱问题
  ? await fs.readFile('./dist/client/.vite/ssr-manifest.json', 'utf-8')
  : undefined;

// 创建 http server
const app = express();

let vite; // 开发环境用到,ViteDevServer 的一个实例
if (!isProduction) {
  // 以中间件模式创建 Vite 应用,并将 appType 配置为 'custom',这将禁用 Vite 自身的 HTML 服务逻辑,并让上级服务器接管控制
  // 就是在开发环境下,开启 SSR,用 express 接管返回 HTML
  const { createServer } = await import('vite');
  vite = await createServer({
    server: { middlewareMode: true },
    appType: 'custom',
    base,
  });
  // vite.middlewares 是一个 Connect 实例,它可以在任何一个兼容 connect 的 Node.js 框架中被用作一个中间件。
  // 如果你使用了自己的 express 路由(express.Router()),你应该使用 router.use
  // 当服务器重启(例如用户修改了 vite.config.js 后),
  // `vite.middlewares` 仍将保持相同的引用(带有 Vite 和插件注入的新的内部中间件堆栈)。即使在重新启动后,以下内容仍然有效。
  app.use(vite.middlewares);
} else {
  // 生产环境下,将 Vite 与生产环境脱钩,用 sirv 静态文件服务中间件来服务 dist/client 中的文件。
  // pnpm add -s compression 压缩
  const compression = (await import('compression')).default;
  // pnpm add -s sirv 比Node自带的 serve-static 性能更好
  const sirv = (await import('sirv')).default;
  app.use(compression());
  app.use(base, sirv('./dist/client', { extensions: [] }));
}

// 处理供给服务端渲染的 index.html。
// 主要步骤如下:
// 1. 读取并且转换index.html,比如客户端访问 /home 页面,此时 index.html 的内容就是 Home 页面的内容
// 2. 然后交给 entry-server.ts 进行注入处理
app.use('*', async (req, res) => {
  try {
    const url = req.originalUrl.replace(base, '');
    let template;
    let render;
    if (!isProduction) {
      // 1. 读取 index.html。开发环境总是读取最新的index.html
      template = await fs.readFile('./index.html', 'utf-8');
      // 2. 应用Vite HTML 转换。这将会注入 Vite HMR 客户端
      //    同时也会从 Vite 插件应用 HTML 转换
      //    例如:@vitejs/plugin-react-refresh 中的 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(vite)
      // render = (await runtime.executeEntrypoint('./src/entry-server.ts')).render
    } else {
      template = templateHtml;
      // @ts-ignore
      render = (await import('./dist/server/entry-server.js')).render;
    }
    // 4. 渲染应用的 HTML。这假设 entry-server.ts 导出 `render`
    //    函数调用了适当的 SSR 框架 API。 例如 ReactDOMServer.renderToString()
    const rendered = await render(url, ssrManifest);
    // 5. 替换 index.html 里的占位符,注入渲染后的应用程序 HTML 到模板中。
    const { title, keywords, description } = rendered.route.meta;
    const html = template
      .replace(`<!--preload-links-->`, rendered.preloadLinks ?? '')
      .replace(`<!--app-html-->`, rendered.html ?? '')
      .replace(`<!--app-state-->`, JSON.stringify(rendered.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' }).send(html);
  } catch (e) {
    // 如果捕获到了一个错误,让 Vite 来修复该堆栈,这样它就可以映射回你的实际源码中。
    vite?.ssrFixStacktrace(e);
    console.log('🚀', e.stack);
    res.status(500).end(e.stack);
  }
});

// 启用 http server
app.listen(port, () => {
  console.log(
    `node server 运行 http://localhost:${port}`,
    isProduction ? '生产环境' : '开发环境'
  );
});

entry-server.ts

entry-server.ts 是将客户端的页面组件转换成服务端的 HTML 字符串:在 entry-server.ts 文件中,我们需要创建一个 render 函数,初始化一个 Vue 实例,配置必要的中间件(如路由器和存储),并将 URL 路径作为参数。然后导出该实例,供服务器使用,以便将应用程序呈现为一个字符串,供服务器端呈现。

kotlin 复制代码
import { basename } from "node:path";
import { renderToString } from 'vue/server-renderer' // 利用Vue的 renderToString API, 可以将组件转成 HTML 字符串j
import { createApp, asyncData } from './main'

export async function render(url: string, manifest: any) {
    const { app, router, pinia } = createApp()

    await router.push(url)
    await router.isReady()


    const matchedComponents = router.currentRoute.value.matched.flatMap(record =>
        Object.values(record.components!)
    )
    console.log('匹配组件', matchedComponents)
    // 对所有匹配的路由组件调用 `asyncData()`,在服务端进行数据预取,并将状态序列化为window.__INITIAL_STATE__,注入到HTML
    await asyncData(matchedComponents, router.currentRoute)
    const ctx: any = {}
    // 传递 SSR context 对象,可以通过 useSSRContext() api 获取
    // @vitejs/plugin-vue injects code into a component's setup() that registers
    // itself on ctx.modules. After the render, ctx.modules would contain all the
    // components that have been instantiated during this render call.
    const html = await renderToString(app, ctx)
    const state = pinia.state.value
    if (import.meta.env.PROD) {
        const preloadLinks = renderPreloadLinks(ctx.modules, manifest)
        return { html, state, preloadLinks }
    } else {
        return { html, state }
    }
}

function renderPreloadLinks(modules: any, manifest: any) {
    let links = "";
    const seen = new Set();
    modules.forEach((id: string) => {
        const files = manifest[id];
        if (files) {
            files.forEach((file: any) => {
                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: any) {
    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 {
        return "";
    }
}

entry-client.ts

entry-client.ts 作用就是在客户端激活(hydrate)挂载#app

typescript 复制代码
/** 
 * 客户端浏览器使用
*/

import './assets/main.css' // 一些样式文件
import { createApp, asyncData } from './main'

const { app, router, pinia } = createApp()

// 在 index.html 里,window.__INITIAL_STATE__ = '<!--app-state-->'
// 在 server.ts 中已经处理将 pinia 数据赋值给了 window.__INITIAL_STATE__
// 所以需要在客户端激活 pinia 的 state。使用场景为首次加载和页面刷新(页面刷新的时候,不重新获取数据,减轻服务器压力,而是从这里恢复数据)
if ((window as any).__INITIAL_STATE__) {
    pinia.state.value = JSON.parse(((window as any).__INITIAL_STATE__))
}

router.isReady().then(() => {
    // 1. 先进行激活(hydrate),主要是数据和交互事件绑定
    router.beforeResolve((to, from, next) => {
        const toComponents = router.resolve(to).matched.flatMap(record =>
            Object.values(record.components!)
        )
        const fromComponents = router.resolve(from).matched.flatMap(record =>
            Object.values(record.components!)
        )
        // 防止前端数据的二次预取(只有打开页面才会获取数据,刷新页面客户端不会重新获取数据,减轻服务器压力。但是刷新页面服务端还是会预取一下数据)
        const actived = toComponents.filter((c, i) => {
            return fromComponents[i] !== c
        })
        // 可以先跳转到路由页面,然后再获取数据,填充页面
        // next()
        // asyncData(actived, router.currentRoute).then(() => {
        //     console.log('结束loading。。。。。')
        // })

        // 但一般都是客户端数据预取,在路由导航之前拿到数据,然后再处理视图
        // 两种区别就是,在哪里等待。一个是跳过去等待数据刷新,一个是等待数据后再跳过去
        if (!actived.length) {
            return next()
        }
        console.log('开始loading。。。。。') // 这里可以模拟loading
        asyncData(actived, router.currentRoute).then(() => {
            console.log('结束loading。。。。。')
            next()
        })

    })
    // 2. 然后进行挂载
    app.mount('#app')
})

// 修改 title 和 meta 信息,进行 SEO 优化
// 虽然服务端渲染的时候,会拼接好当前页面的所有信息返回给客户端
// 但由于是前端路由管理,跳转页面不会请求服务端,所以 title 和 meta 等信息都是旧的,所以客户端需要在下面修改一下
router.afterEach((to, from, next) => {
    const { title, keywords, description } = to.meta
    if (title) {
        document.title = `${title}`
    } else {
        document.title = ""
    }

    const keywordsMeta = document.querySelector('meta[name="keywords"]')
    keywordsMeta && keywordsMeta.setAttribute("content", `${keywords}`)

    const descriptionMeta = document.querySelector('meta[name="description"]')
    descriptionMeta?.setAttribute("content", `${description}`)
})

package.json

配置 ssr 脚本命令

perl 复制代码
"scripts": {
  "dev": "vite",
  "dev:ssr": "cross-env NODE_ENV=development node server", // 本地预览开发环境SSR
  "build:ssr": "pnpm run build:client && pnpm run build:server", // 打包客户端和服务端文件
  "build:client": "vite build --ssrManifest --outDir dist/client", // 打包客户端文件。--ssrManifest 标志会在客户端构建输出目录中生成一份 .vite/ssr-manifest.json
  "build:server": "vite build --ssr src/entry-server.js --outDir dist/server", // 打包服务端文件。--ssr 标志表明这是一个 SSR 构建,同时需要指定 SSR 的入口
  "prod:ssr": "cross-env NODE_ENV=production node server", // 本地预览生产环境开启SSR
  "preview": "vite preview"
},
"dependencies": {
  "compression": "^1.7.4",
  "express": "^4.19.2",
  "pinia": "^2.1.7",
  "sirv": "^2.0.4",
  "vue": "^3.4.21",
  "vue-router": "^4.3.0"
},
"devDependencies": {
  "@tsconfig/node20": "^20.1.2",
  "@types/node": "^20.11.28",
  "@vitejs/plugin-vue": "^5.0.4",
  "@vitejs/plugin-vue-jsx": "^3.1.0",
  "@vue/tsconfig": "^0.5.1",
  "cross-env": "^7.0.3",
  "npm-run-all2": "^6.1.2",
  "typescript": "~5.4.0",
  "vite": "^5.1.6",
  "vue-tsc": "^2.0.6"
}

本地/线上测试

本地测试

利用项目创建时候自带的 useCounterStore,同时改造下 AboutView.vue

javascript 复制代码
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  function increment() {
    count.value++
  }

  return { count, doubleCount, increment }
})

注意:这里没有使用 setup 语法糖,因为 asyncData() 是和 setup() 同级:

xml 复制代码
<template>
  <div class="about">
    <h1>计数器: {{ store.count }}</h1>
  </div>
</template>

<script lang="ts">
  import { defineComponent, onMounted } from 'vue';
  import { useCounterStore } from '../stores/counter';

  export default defineComponent({
    setup() {
      // 不要在这里直接调用浏览器端特定的代码,服务端渲染的时候会报错
      // 可以将执行浏览器端的代码放到onMounted()生命周期里,服务端渲染的时候会自动过滤掉
      onMounted(() => {});
      const store = useCounterStore();
      return {
        store,
      };
    },
    // 预取接口数据,执行在 setup() 函数之前
    // 这里没有使用真实接口,只是用pinia模拟一下返回,可以在store里定义异步数据获取
    asyncData({ route }: any) {
      // const { id } = route.value.params;
      const store = useCounterStore();
      return store.increment();
    },
  });
</script>

<style>
  @media (min-width: 1024px) {
    .about {
      min-height: 100vh;
      display: flex;
      align-items: center;
    }
  }
</style>

跑一下项目看看效果,执行pnpm run dev:ssr本地开发环境启动SSR,整个过程会发生:

  1. 打开网址,客户端会发起请求
  2. 服务端收到请求,执行server.js,本地会启动一个express服务
  3. 继续读取 index.html文件,加载 entry-server.ts服务端渲染入口文件,将对应页面内容替换,返回index.html
  4. 客户端收到返回的 index.html,这里 index.html里面有<script type="module" src="/src/entry-client.ts"></script>,所以会执行entry-client.ts客户端渲染文件,进行注水激活页面

线上测试

线上部署,就是将distpackage.jsonserver.js等文件放到服务器中,然后服务器需要安装 Node 环境,配置好 Nginx,然后安装依赖后执行npm run build:prod命令启动项目,即可公网访问项目。

SSG/预渲染

静态站点生成 (Static-Site Generation,缩写为 SSG),也被称为预渲染,是另一种流行的构建快速网站的技术。如果用服务端渲染一个页面所需的数据对每个用户来说都是相同的,那就可以只渲染一次,提前在构建过程中完成,而不是每次请求进来都重新渲染页面。预渲染的页面生成后作为静态 HTML 文件被服务器托管。

SSG 保留了和 SSR 应用相同的性能表现:它带来了优秀的首屏加载性能。同时,它比 SSR 应用的花销更小,也更容易部署,因为它输出的是静态 HTML 和资源文件。这里的关键词是静态:SSG 仅可以用于消费静态数据的页面,即数据在构建期间就是已知的,并且在多次部署期间不会改变。每当数据变化时,都需要重新部署。

如果你调研 SSR 只是为了优化为数不多的营销页面的 SEO (例如 /、/about 和 /contact 等),那么你可能需要 SSG 而不是 SSR。SSG 也非常适合构建基于内容的网站,比如文档站点或者博客。VitePress 就是一个由 Vite 和 Vue 驱动的静态站点生成器。

Vite脚手架渲染SSG代码示例

相关推荐
GISer_Jing1 分钟前
前端面试通关:Cesium+Three+React优化+TypeScript实战+ECharts性能方案
前端·react.js·面试
落霞的思绪1 小时前
CSS复习
前端·css
咖啡の猫3 小时前
Shell脚本-for循环应用案例
前端·chrome
百万蹄蹄向前冲5 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5816 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路6 小时前
GeoTools 读取影像元数据
前端
ssshooter6 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Jerry7 小时前
Jetpack Compose 中的状态
前端
dae bal8 小时前
关于RSA和AES加密
前端·vue.js
柳杉8 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化