• 首页
  • vue
  • TypeScript
  • JavaScript
  • scss
  • css3
  • html5
  • php
  • MySQL
  • redis
  • jQuery
  • vite + vue3 + ts + SSR 使用 Vite 内置 SSR 功能

    搭建 vue3 ssr,有多种方式:

    • 使用Vite 内置 SSR 功能,它 Vite 的底层 SSR 功能。
    • 使用vite-ssr插件。基于Vite 内置 SSR 功能,简化了服的务器端渲染。只适用于 vue3。
    • 使用vite-plugin-ssr插件。基于Vite 内置 SSR 功能,提供比 Vite 的底层 SSR 更流畅的体验。适用于 vue3、vue2。
    • 使用ssr插件。基于Vite 内置 SSR 功能。适用于 vue3、vue2。


    安装 vue3

    cd /var/web/www
    
    npm init vue@latest
    

    项目名称:exampleSSR

    cd /var/web/www exampleSSR
    
    npm install
    

    安装 axios

    cd /var/web/www exampleSSR
    
    npm install axios
    

    至此,已经安装 vite + vue3 + TypeScript + vue-router + pinia + axios。还有 vitest 单元测试框架、cypress 端到端测试、ESLint 语法规则检测工具、Prettier 代码格式化工具。


    局部安装 express

    Express基于 NodeJs 平台,快速、简洁的 Web 应用框架,为 Web 和移动应用程序提供一组强大的功能。中文:https://www.expressjs.com.cn

    cd /var/web/www/exampleSSR
    
    # 在项目中,安装 Express
    npm install express -S
    
    # 在项目中,安装 TypeScript 依赖
    npm install @types/express -D
    

    node 无法直接运行 ts 文件,需要使用 ts-node。Express 是基于 node 环境的,所以还需要安装 node TypeScript 依赖。

    cd /var/web/www/exampleSSR
    
    # 在项目中,安装 ts-node
    npm install ts-node -D
    
    # 在项目中,安装 TypeScript 依赖
    npm install @types/node -D
    

    查看已安装

    npm list
    npm list express
    npm list ts-node
    


    配置格式化

    项目根目录下,tsconfig.json文件中,新增compilerOptions选项:

    "compilerOptions": {
        "types": ["node", "jsdom", "vite/client", "element-plus/global"],
        "target": "esnext",
        "module": "esnext",
        "lib": ["dom", "esnext"],
        "strict": true
    },
    


    在项目根目录下,修改.prettierrc.json文件

    {
      "semi": true,
      "singleQuote": true,
      "printWidth": 200,
      "endOfLine": "lf"
    }
    


    在项目根目录下,修改.eslintrc.cjs文件,新增rules选项:

    /* eslint-env node */
    require('@rushstack/eslint-patch/modern-module-resolution');
    
    module.exports = {
      root: true,
      extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-typescript', '@vue/eslint-config-prettier'],
      overrides: [
        {
          files: ['cypress/e2e/**.{cy,spec}.{js,ts,jsx,tsx}'],
          extends: ['plugin:cypress/recommended'],
        },
      ],
      parserOptions: {
        ecmaVersion: 'latest',
      },
      rules: {
        'prettier/prettier': [
          'warn',
          {
            semi: true,
            singleQuote: true,
            printWidth: 200,
            endOfLine: 'lf',
          },
        ],
      },
    };
    


    配置完毕后,需要重启 vscode 编辑器。然后运行以下命令,来格式化文件。

    npm run lint
    npm run dev
    


    使用 Vite 内置 SSR 功能

    SSR(Server-Side Rendering):服务器端渲染。Vite 为服务端渲染(SSR)提供了内置支持,一个底层 API,是为制作库和框架准备的。


    SSR 文件结构

    一个典型的 SSR 应用应该有如下的源文件结构:

    - index.html
    - server.ts # 服务端启动文件
    - src/
      - main.ts          # 导出环境无关的(通用的)应用代码
      - entry-client.ts  # 客户端入口,应用挂载元素。将应用挂载到一个 DOM 元素上
      - entry-server.ts  # 服务端入口,处理服务端逻辑和静态资源。使用某框架的 SSR API 渲染该应用
    

    创建三个文件:

    touch server.ts src/entry-client.ts src/entry-server.ts
    


    Node 运行 ESM 模式

    在服务端运行 node,需要开启 esm 模式,另外,需要 ts-node,用node --loader ts-node/esm来执行 TS 文件。官方文档package.json文件中,必须设置"type":"module",实现运行esm模式。tsconfig.json文件中,必须设置"module":"ESNext"。注意:这里是node 16+版本。

    # package.json
    "name": "exampleSSR",
    "version": "0.0.0",
    "type": "module",
    "scripts": {
      "dev": "node --loader ts-node/esm server.ts", // 运行 server.ts 文件
    }
    
    # tsconfig.json
    "compilerOptions": {
        "strict": true,
        "target": "esnext",
        "module": "esnext",
        "lib": ["esnext", "dom", "dom.iterable", "scripthost"],
        "types": ["node", "jsdom", "vite/client", "element-plus/global"]
    },
    "ts-node": {
        "esm": true,
        "compilerOptions": {
          "moduleResolution": "node",
          "allowSyntheticDefaultImports": true,
          "esModuleInterop": true,      
        }
    },
    


    index.html

    index.html将需要引用entry-client.ts,以实现客户端(在浏览器中)动态交互。另外,占位标记<!--app-html-->,服务端渲染时替换为输出的 HTML 内容。静态资源占位符(js 、css 文件):<!--preload-links-->。占位标记可以根据个人喜好,随意起名称。

    <!DOCTYPE html>
    <html lang="zh-hans">
      <head>
        <meta charset="UTF-8" />
        <meta name="applicable-device" content="pc,mobile" />
        <meta name="renderer" content="webkit|ie-comp|ie-stand" />
        <meta name="force-rendering" content="webkit" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
        <meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no" />
        <link rel="icon" href="/favicon.ico" />
        <link rel="Bookmark" href="/favicon.ico" />
        <link rel="Shortcut Icon" href="/favicon.ico" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Vite SSR</title>
        <meta name="keywords" content="Vite SSR" />
        <meta name="description" content="Vite SSR" />
      </head>
      <body>
        <div id="app"><!--app-html--></div>
        <script type="module" src="/src/entry-client.ts"></script>
        <!--app-state-->
      </body>
    </html>
    


    server.ts

    这个server.ts文件的功能是启动一个 Nodejs Web 服务,来响应客户端请求,然后根据请求,读取index.html文件,处理资源后把其中的占位符(注释)进行替换,最后把 HTML 页面内容,发送给请求者阅览。

    在构建 SSR 应用程序时,你可能希望由 Web 服务器控制,并将 Vite 与生产环境脱钩。因此,建议以中间件模式使用 Vite。

    import fs from 'fs';
    import path from 'path';
    import { fileURLToPath } from 'url';
    import express from 'express';
    import { createServer as createViteServer } from 'vite';
    
    const __dirname = path.dirname(fileURLToPath(import.meta.url));
    
    async function createServer() {
      // 创建node服务,设置端口
      const app = express();
      const PORT = 5173;
      app.set('port', PORT);
    
      // 以中间件模式创建 Vite 应用,这将禁用 Vite 自身的 HTML 服务逻辑
      // 并让上级服务器接管控制
      const vite = await createViteServer({
        server: { middlewareMode: true },
        appType: 'custom',
      });
    
      // 使用 vite 的 Connect 实例作为中间件
      // 如果你使用了自己的 express 路由(express.Router()),你应该使用 router.use
      app.use(vite.middlewares);
    
      // 服务 index.html
      // exporss 请求拦截器, * 全部路由, req 参数是 HTTP 请求 request,res 参数是请求响应 respones,next 参数,路由
      app.use('*', async (req, res, next) => {
        const url = req.originalUrl;
    
        try {
          // 1. 读取 index.html
          let 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);
    
          // 3. 加载服务器入口。vite.ssrLoadModule 将自动转换
          //    你的 ESM 源码使之可以在 Node.js 中运行!无需打包
          //    并提供类似 HMR 的根据情况随时失效。
          const { render } = await vite.ssrLoadModule('/src/entry-server.ts');
    
          // 4. 渲染应用的 HTML。这假设 entry-server.ts 导出的 `render`
          //    函数调用了适当的 SSR 框架 API。
          //    例如 ReactDOMServer.renderToString()
          const [appHtml, renderState] = await render(url);
    
          // 传递 Pinia 状态管理。自定义 window 属性 __pinia
          let appState = '';
          if (renderState) {
            appState = "<script>window.__pinia='" + JSON.stringify(renderState) + "'</script>";
          }
    
          // 5. 注入渲染后的应用程序 HTML 到模板中。
          const html = template.replace(`<!--app-html-->`, appHtml).replace(`<!--app-state-->`, appState);
    
          // 6. 返回渲染后的 HTML。
          res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
        } catch (e: any) {
          // 如果捕获到了一个错误,让 Vite 来修复该堆栈,这样它就可以映射回你的实际源码中。
          vite?.ssrFixStacktrace(e);
          next(e);
        }
      });
    
      process.on('warning', (warning) => {
        console.warn(warning.name); // 打印告警名称
        console.warn(warning.message); // 打印告警信息
        console.warn(warning.stack); // 打印堆栈信息
      });
    
      app.listen(PORT);
    }
    
    createServer();
    


    router/index.ts

    import { createRouter as _createRrouter, createMemoryHistory, createWebHistory } from 'vue-router';
    import type { RouteRecordRaw } from 'vue-router';
    import HomeView from '../views/HomeView.vue';
    
    const routes: RouteRecordRaw[] = [
      {
        path: '/',
        name: 'home',
        component: HomeView,
      },
      {
        path: '/about',
        name: 'about',
        component: () => import('../views/AboutView.vue'),
      },
    ];
    
    export function createRouter() {
      return _createRrouter({
        history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(import.meta.env.BASE_URL),
        routes,
      });
    }
    


    stores/index.ts

    import { defineStore, createPinia } from 'pinia';
    import type { MetaObj } from '@/types/book';
    
    export const useUserStore = defineStore('user', {
        state: () => {
            return {
                name: '张三',
                age: 20
            };
        },
        actions: {
            updateName(name: string) {
                this.name = name;
            },
            updateAge(age: number) {
                this.age = age;
            }
        }
    });
    
    export const useBrowserStore = defineStore('browser', {
      state: () => ({
        browserStatus: 0,
      }),
      getters: {
        initBrowserStatus(state) {
          return (state.browserStatus = 0);
        },
      },
    });
    
    export const createStore = () => {
      const pinia = createPinia();
      useBrowserStore(pinia);
      useUserStore(pinia);
      return pinia;
    };
    
    export default createStore;
    


    main.ts

    在 SSR 环境下,服务器只会初始化一次。因为每个请求到达服务端,每次请求必须是全新的实例,为了防止状态污染,所以main.ts每次都返回全新的 vue 实例,router 实例,store 实例等。

    import { createSSRApp } from 'vue';
    import createStore from '@/stores';
    import router from './router';
    import App from './App.vue';
    
    import './assets/main.css';
    
    // SSR 要求每个请求都有一个新的应用程序实例,因此我们导出一个函数。
    // 创建新的应用程序实例。如果使用 Pinia、Vuex,我们也会在这里创建一个新 Store。
    export function createApp() {
      const app = createSSRApp(App);
      const pinia = createPinia();
      app.use(router);
      app.use(pinia);
      return { app, router, pinia };
    }
    


    entry-server.ts

    服务端入口文件entry-server.ts:主要是调用 SSR 的renderToString和收集需要发送的资源数据。

    import type { RouteLocationRaw } from 'vue-router';
    import { renderToString } from 'vue/server-renderer';
    import { createApp } from './main';
    
    export async function render(url: RouteLocationRaw) {
      const { app, router, pinia } = createApp();
    
      router.push(url);
      await router.isReady();
    
      const ctx = {};
      const html = await renderToString(app, ctx);
      return [html, pinia.state.value];
    }
    


    entry-client.ts

    客户端入口文件entry-client.ts:主要用于挂载节点和初始化数据。

    import { createApp } from './main'
    
    const { app, router, pinia } = createApp()
    
    // wait until router is ready before mounting to ensure hydration match
    router.isReady().then(() => {
    if (window.__pinia) {
        pinia.state.value = JSON.parse(window.__pinia);
      }
      app.mount('#app')
    })
    

    TypeScript 文件env.d.ts中添加:

    interface Window {
      __pinia: any;
    }