zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    少走弯路的10条忠告
    思考
    哈弗经典校训
    项目导出excel引发的一些问题
    hibernate 缓存设置
    dubbo简单用法
    sql 类型问题
    spring this.logger.isDebugEnabled()
    红黑树
    归并排序
  • 原文地址:https://www.cnblogs.com/ygunoil/p/15718936.html
Copyright © 2011-2022 走看看