百科狗-知识改变命运!
--

vite + vue3 + ts + SSR 使用 Vite 内置 SSR 功能 - vue3 项目实战

梵高1年前 (2023-11-21)阅读数 31#技术干货
文章标签文件

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 文件结构

vite + vue3 + ts + SSR 使用 Vite 内置 SSR 功能 - vue3 项目实战

一个典型的 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,以实现客户端(在浏览器中)动态交互。另外,占位标记,服务端渲染时替换为输出的 HTML 内容。静态资源占位符(js 、css 文件):。占位标记可以根据个人喜好,随意起名称。

 Vite SSR 


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 = "";
      }

      // 5. 注入渲染后的应用程序 HTML 到模板中。
      const html = template.replace(``, appHtml).replace(``, 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;
}

鹏仔微信 15129739599 鹏仔QQ344225443 鹏仔前端 pjxi.com 共享博客 sharedbk.com

免责声明:我们致力于保护作者版权,注重分享,当前被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理!邮箱:344225443@qq.com)

图片声明:本站部分配图来自网络。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

内容声明:本文中引用的各种信息及资料(包括但不限于文字、数据、图表及超链接等)均来源于该信息及资料的相关主体(包括但不限于公司、媒体、协会等机构)的官方网站或公开发表的信息。部分内容参考包括:(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供参考使用,不准确地方联系删除处理!本站为非盈利性质站点,本着为中国教育事业出一份力,发布内容不收取任何费用也不接任何广告!)