Vite2 Vue3 SSR


目標:用 vite2 + vue3 + Ts 搭建一個開箱即用的最簡 ssr 通用項目,  包含必要的 vuex vue-router asyncData header管理。

 

一 通過官方腳手架搭建一個 vue-ts 的 SPA 項目

首先安裝 yarn 包管理工具: 

 

創建一個簡單的 vue-ts 項目: 

 

// 選擇 vue-ts 模版

cd demo

yarn

yarn dev

  

 

http://localhost:3000/

瀏覽器打開 http://localhost:3000/ 一個最簡單的 vue3 + typescript 的 SPA 單頁應用就搭建好了。

 

 

 

二 對 SPA 單頁應用,進行 ssr 渲染改造。

在 src 目錄下添加兩個入口文件 

 

項目目錄下 修改 index.html文件

 

// entry-client.ts

import { createSSRApp } from 'vue';

import App from './App.vue';



const app = createSSRApp(App);

app.mount('#app', true);

  

// entry-server.js

import { createSSRApp } from 'vue';

import App from './App.vue';

import { renderToString } from '@vue/server-renderer';



export async function render(url, manifest) {

  const app = createSSRApp(App);

  const context = {};

  const appHtml = await renderToString(app, context);

  return { appHtml };

}

  

 

新建node端web服務器入口文件(開發環境): server-env.js ,官方推薦 express,安裝node包: yarn add -D express

 

const fs = require('fs');

const path = require('path');

const express = require('express');

const { createServer: createViteServer } = require('vite');



async function createServer() {

  const app = express();



  const vite = await createViteServer({

    server: { middlewareMode: true },

  });



  app.use(vite.middlewares);



  app.use('*', async (req, res) => {

    // serve index.html - we will tackle this next

    const url = req.originalUrl;



    try {

      // 1. Read index.html

      let template = fs.readFileSync(path.resolve(__dirname, 'index.html'), 'utf-8');



      // 2. Apply vite HTML transforms.

      template = await vite.transformIndexHtml(url, template);



      // 3. Load the server entry. vite.ssrLoadModule

      const { render } = await vite.ssrLoadModule('/src/entry-server.js');



      // 4. render the app HTML.

      const { appHtml } = await render(url);



      // 5. Inject the app-rendered HTML into the template.

      const html = template.replace(``, appHtml);



      // 6. Send the rendered HTML back.

      res.status(200).set({ 'Content-Type': 'text/html' }).end(html);

    } catch (e) {

      // If an error is caught,

      vite.ssrFixStacktrace(e);

      console.error(e);

      res.status(500).end(e.message);

    }

  });



  app.listen(3000, () => {

    console.log('http://localhost:3000');

  });

}



createServer();

  

package.json文件 新增dev命令

 

// package.json

"scripts": {

    "dev": "node server-env.js"

  },

  

終端運行 yarn dev, 瀏覽器打開:http://localhost:3000/  網頁右鍵“顯示頁面源碼”、

 

生產環境打包,package.json新增 build 相關命令

 

//package.json

"scripts": {

    "dev": "node server-env.js",

    "build:client": "vite build --outDir dist/client --ssrManifest",

    "build:server": "vite build --outDir dist/server --ssr src/entry-server.js ",

    "build": "yarn build:client && yarn build:server",

    "preview": "yarn build && node server.js"

  },

  

新建 node 端web服務器入口文件(生產環境): server.js ,個人選擇 koa搭建生產環境服務器,安裝 node 包:yarn add -D koa koa-static

 

// server.js

const fs = require('fs');

const path = require('path');

const Koa = require('koa');

const staticPath = require('koa-static');



const app = new Koa();

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



const template = fs.readFileSync(resolve('./dist/client/index.html'), 'utf-8');

const manifest = require('./dist/client/ssr-manifest.json');

const render = require('./dist/server/entry-server.js').render;



app.use(staticPath(resolve('./dist/client'), { index: false }));



app.use(async (ctx, next) => {

  const url = ctx.req.url;

  try {

    const { appHtml } = await render(url, manifest);



    let html = template.replace(``, appHtml);



    ctx.body = html;

  } catch (error) {

    console.log(error);

    next();

  }

});



app.listen(3000, () => {

  console.log('http://localhost:3000');

});

  

終端運行: yarn preview,瀏覽器打開:http://localhost:3000。最簡 ssr 改造完成。

 

三 安裝生產上必備的 vue 全家桶: scss vuex vue-router

首先安裝scss支持: yarn add -D sass.  

 

安裝vue-router 和 vuex :  yarn add vuex@next vue-router@next  vuex-router-sync@next

 

新建 src/store/index.ts 和 src/router/index.ts 兩個文件

 

// src/router/index.ts

 

import { createRouter, createWebHistory, createMemoryHistory } from 'vue-router';



export default function () {

  const routerHistory = import.meta.env.SSR === false ? createWebHistory() : createMemoryHistory();



  return createRouter({

    history: routerHistory,

    routes: [

      {

        path: '/',

        name: 'home',

        component: () => import('../views/Home.vue'),

      },

      {

        path: '/about',

        name: 'about',

        component: () => import('../views/About.vue'),

      },

      {

        path: '/:catchAll(.*)*',

        name: '404',

        component: () => import('../views/404.vue'),

        meta: {

          title: '404 Not Found',

        },

      },

    ],

  });

}

  

// src/store/index.ts

 

import { createStore as _createStore } from 'vuex';



export default function createStore() {

  return _createStore({

    state: {

      message: 'Hello vite2 vue3 ssr',

    },

    mutations: {},

    actions: {

      fetchMessage: ({ state }) => {

        return new Promise((resolve) => {

          setTimeout(() => {

            state.message = 'Hello vite2 vue3 ssr typescript scss vuex vue-router';

            resolve(0);

          }, 200);

        });

      },

    },

    modules: {},

  });

}

  

新建對應的 src/views/頁面 Home.vue  About.vue  404.vue, 略。

 

修改 entry-client.ts 和 entry-server.js文件,加入相應的 vuex 和 router

 

// entry-client.ts

 

import { createSSRApp } from 'vue';

import App from './App.vue';

import { sync } from 'vuex-router-sync';



import createStore from './store';

import createRouter from './router';



const router = createRouter();

const store = createStore();

sync(store, router);



const app = createSSRApp(App);

app.use(router).use(store);



router.beforeResolve((to, from, next) => {

  next();

});



router.isReady().then(() => {

  app.mount('#app', true);

});

  

// entry-server.js

 

import { createSSRApp } from 'vue';

import App from './App.vue';

import { renderToString } from '@vue/server-renderer';



import createStore from './store';

import createRouter from './router';

import { sync } from 'vuex-router-sync';



export async function render(url, manifest) {

  const router = createRouter();

  const store = createStore();

  sync(store, router);



  const app = createSSRApp(App);

  app.use(router).use(store);



  router.push(url);



  await router.isReady();



  const context = {};

  const appHtml = await renderToString(app, context);

  return { appHtml };

}

  

App.vue

Home.vue

終端運行: yarn dev 查看開發環境效果。終端運行: yarn preview 查看生產環境效果。

 

 

 

 

四 服務端預取數據 asyncData

服務端預取數據采用 vue2的 asyncData 方式。

 

新建 vue-extend.d.ts 文件

 

// vue-extend.d.ts

 

import { RouteRecordRaw } from 'vue-router';



export interface AsyncDataContextType {

  route: RouteRecordRaw;

  store: any; // 類型不決 用 any。  -.-!

}



declare module '@vue/runtime-core' {

  interface ComponentCustomOptions {

    asyncData?(context: AsyncDataContextType): Promise;

  }

}

  

在Home.vue 添加 asyncData,store里用 setTimeout 模擬異步請求。

 

// Home.vue

 

export default defineComponent({

  setup() {

    const store = useStore();



    return { store };

  },

  asyncData({ store }) {

    return store.dispatch('fetchMessage');

  },

});

  

修改entry-client.ts中路由守衛, router.beforeResolve( ) 相關。

 

// entry-client.ts

 

router.beforeResolve((to, from, next) => {

  let diffed = false;

  const matched = router.resolve(to).matched;

  const prevMatched = router.resolve(from).matched;



  if (from && !from.name) {

    return next();

  }



  const activated = matched.filter((c, i) => {

    return diffed || (diffed = prevMatched[i] !== c);

  });



  if (!activated.length) {

    return next();

  }



  const matchedComponents: any = [];

  matched.map((route) => {

    matchedComponents.push(...Object.values(route.components));

  });

  const asyncDataFuncs = matchedComponents.map((component: any) => {

    const asyncData = component.asyncData || null;

    if (asyncData) {

      const config = {

        store,

        route: to,

      };



      return asyncData(config);

    }

  });

  try {

    Promise.all(asyncDataFuncs).then(() => {

      next();

    });

  } catch (err) {

    next(err);

  }

});

  

修改entry-server.js中 render 函數。

 

// entry-server.js

 

export async function render(url, manifest) {

  const router = createRouter();

  const store = createStore();

  sync(store, router);



  const app = createSSRApp(App);

  app.use(router).use(store);



  router.push(url);

  await router.isReady();



  const to = router.currentRoute;

  const matchedRoute = to.value.matched;

  if (to.value.matched.length === 0) {

    return '';

  }



  const matchedComponents = [];

  matchedRoute.map((route) => {

    matchedComponents.push(...Object.values(route.components));

  });



  const asyncDataFuncs = matchedComponents.map((component) => {

    const asyncData = component.asyncData || null;

    if (asyncData) {

      const config = {

        store,

        route: to,

      };

      return asyncData(config);

    }

  });



  await Promise.all(asyncDataFuncs);



  const context = {};

  const appHtml = await renderToString(app, context);

  return { appHtml };

}

  

終端運行 yarn dev查看效果, 服務端預取數據渲染正確,但devtools 有一個報錯:Hydration completed but contains mismatches.  是客戶端和服務端的 store 未同步

 

 

同步方式如下:

 

index.html 文件添加相應的 window.__INITIAL_STATE__  標識

 

 

修改entry-server.js 的 render函數 返回 state

 

// entry-server.js

 

export async function render(url, manifest) {

  //...

  const appHtml = await renderToString(app, context);

  const state = store.state;

  return { appHtml, state };

}

  

在 server-env.js  和 server.js 修改 html模版 注意 `' '`。

 

// server-env.js 和 server.js

 

const { appHtml, state } = await render ;



const html = template

      .replace(``, appHtml)

      .replace(`''`, JSON.stringify(state))

  

 entry-client.ts 文件末添加  store.replaceState()函數同步state。

 

// entry-client.ts if (window.__INITIAL_STATE__) {   store.replaceState(window.__INITIAL_STATE__); }

在shims-vue.d.ts 添加  typescript支持 

 

// shims-vue.d.ts

 

interface Window {

  __INITIAL_STATE__: any;

}

  

終端運行 yarn dev 和 yarn preview 查看效果。

 

這一階段源碼: https://github.com/damowangzhu/vite2-vue3-ssr_steps/tree/v2

 

五 Head管理,ssr for SEO

以 title 為例,description 和 keywords 雷同。

 

在 src/router/indext.ts 寫入 meta 信息 

 

// src/router/index.ts

      {

        path: '/',

        name: 'home',

        component: () => import('../views/Home.vue'),

        meta: {

          title: 'Home title',

        },

      },

  

在index.html文件添加 title 標記

 

<!--title-->

在server.js 和 server-env.js 修改模版

 

// server.js server-env.js

 

const html = template

      .replace(``, appHtml)

      .replace(`''`, JSON.stringify(state))

      .replace('', state.route.meta.title || 'Index');

  

在entry-client.ts 文件做個前端路由跳轉兼容 

 

// entry-client.ts

 

if (from && !from.name) {

    return next();

  } else {

    window.document.title = (to.meta.title || '首頁') as any;

  }

  

六 增加配置文件,開發環境和生產環境

項目目錄下新建 .env.development 和 .env.production 文件

 

// .env.development

NODE_ENV=development

VITE_API_URL=/

VITE_ASSET_URL=/

  

// .env.production

VITE_API_URL=/

VITE_ASSET_URL=/

  

修改 vite.config.ts 文件, 配置文件通過 loadEnv 獲取.env files 環境變量, 

 

如果靜態資源要發布CDN,可設置 例如: VITE_ASSET_URL=https://cdn.domain.com/

 

程序內部通過 

 

 

// vite.config.ts

 

import { defineConfig, loadEnv } from 'vite';

import vue from '@vitejs/plugin-vue';



export default defineConfig(({ mode }) => {

  const env = loadEnv(mode, process.cwd());



  return {

    base: env.VITE_ASSET_URL,

    plugins: [vue()],

  };

});

  

七 代碼格式化和 typescript 類型檢查

官方推薦 Vscode + Volar, 通過 ide 的插件做類型檢查等

 

Vscode安裝 volar 和 Prettier 插件, 新建 .prettierrc.js 文件 , Vscode默認格式化選擇 prettier

 

// .prettierrc.js

 

module.exports = {

  trailingComma: "none",

  printWidth: 130,

  bracketSpacing: true,

  arrowParens: "always",

  tabWidth: 2,

  semi: true,

  singleQuote: true,

  jsxBracketSameLine: true,

};

  

修改 ts.config.json 增加兩條驗證規則。

 

//  ts.config.json

 

  "noUnusedLocals": true, // 不允許未使用的變量

  "noImplicitReturns": true, // 函數不含隱式返回值

  

終端運行 yarn dev 和 yarn preview 查看效果。

 

若生產環境編譯需要Ts類型檢查 可通過 vue-tsc 插件,但編譯會慢很多,修改package.json 配置文件。

 

// package.json

 

"build:client": "vue-tsc --noEmit && vite build --ssrManifest --outDir dist/client",

最后 升級vue3 到最新版本: yarn add vue@next;

 

  

本文源碼:  https://github.com/ygunoil/vite2-vue3-ssr_steps

 

 

 

參考資料: 

 

https://vitejs.dev/guide/ssr.html  

https://cn.vitejs.dev/config/#async-config

https://www.bookstack.cn/read/vitejs-2.4.4-zh/guide-ssr.md

https://github.com/vitejs/vite/tree/main/packages/playground/ssr-vue

 

https://github.com/vok123/vue3-ts-vite-ssr-starter

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM