新春伊始,想必在座的各位都正在嗷嗷待哺的等待需求中(ps:划水摸鱼),不好意思,理直气壮的说我也是。几天鱼摸下来,心里也不是滋味,看着身边的同学一个个每天都在学这学那,搞得我也不是很好意思。于是趁着现在各种完全体的方案和框架还没出来之前,那我们把vue3.0的服务端搭一搭吧,自己写写还是很有意思的。

好了,废话就先到此,开局调研了基于webpack和vue-cli去搭,途中碰到了一些问题就放弃了,(后记:不好意思,我胡汉三又回来了,怎么说放弃就放弃,反手就是甩一套教程),由于尤大最近很痴迷于vite,vue-cli相关的生态也有些滞后。于是反手就一手vite搞起,看看到底有什么魔力让我们尤大大年三十晚上还在撸代码。

最开始的学习毫无疑问就是看文档,刚好碰上vite2.0发布,简直是可喜可贺。轻车熟路的就找到了ssr的demo
在这里插入图片描述
一看到这句话,就说明接下来的旅途非常的刺激,可以动手发挥想象力的地方非常的多。
话不多说,先download下来,再根据自己想要的去改造就可以了。

打开demo项目,大体的逻辑已经帮我们写的差不多了,剩余服务端预取数据,store状态接管等这些没有去弄,可以说稍加改造就可以用于生产了,非常的nice。

下面讲解下代码吧。
首先看到server.js文件,这个文件其实就是帮我们启动一个ssr的服务器。

// @ts-check
const fs = require('fs')
const path = require('path')
const express = require('express')
const serialize = require('serialize-javascript');

const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD

async function createServer(
  root = process.cwd(),
  isProd = process.env.NODE_ENV === 'production'
) {
  const resolve = (p) => path.resolve(__dirname, p)

  const indexProd = isProd
    ? fs.readFileSync(resolve('dist/client/index.html'), 'utf-8')
    : ''

  const manifest = isProd
    ? // @ts-ignore
      require('./dist/client/ssr-manifest.json')
    : {}

  const app = express()

  /**
   * @type {import('vite').ViteDevServer}
   */
  let vite
  if (!isProd) {
    vite = await require('vite').createServer({
      root,
      logLevel: isTest ? 'error' : 'info',
      server: {
        middlewareMode: true
      }
    })
    app.use(vite.middlewares)
  } else {
    app.use(require('compression')())
    // 把打包好的css,js等文件,放到静态文件服务器
    app.use(
      require('serve-static')(resolve('dist/client'), {
        index: false
      })
    )
  }

  app.use('*', async (req, res) => {
    try {
      const url = req.originalUrl

      let template, render
      // 读取index.html模板文件
      if (!isProd) {
        console.log('当前请求路径', url);
        template = fs.readFileSync(resolve('index.html'), 'utf-8')
        template = await vite.transformIndexHtml(url, template)
        render = (await vite.ssrLoadModule('/src/entry-server.js')).render
      } else {
        template = indexProd
        // @ts-ignore
        render = require('./dist/server/entry-server.js').render
      }
      // 调用服务端渲染方法,将vue组件渲染成dom结构,顺带分析出需要预加载的js,css等文件。
      const [appHtml, preloadLinks, store] = await render(url, manifest)
      // 新加 + 将服务端预取数据的store,插入html模板文件
      const state =  ("<script>window.__INIT_STATE__" + "=" + serialize(store, {isJSON: true}) + "</script>");
      // 把html中的展位符替换成相对应的资源文件
      const html = template
        .replace(`<!--preload-links-->`, preloadLinks)
        .replace(`<!--app-html-->`, appHtml)
        .replace(`<!--app-store-->`, state)

      res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
    } catch (e) {
      vite && vite.ssrFixStacktrace(e)
      console.log(e.stack)
      res.status(500).end(e.stack)
    }
  })

  return { app, vite }
}
// 创建node服务器用作ssr
if (!isTest) {
  createServer().then(({ app }) =>
    app.listen(3000, () => {
      console.log('http://localhost:3000')
    })
  )
}

// for test use
exports.createServer = createServer

index.html
就是server.js里面的模板文件,可以看到里面有对应要替换的展位符
在这里插入图片描述
src/main.ts
因为每个请求到达服务端,都需要一份全新的不受上个请求污染的代码,所以这个文件其实就是当前运行环境的工厂函数,每次都返回全新的vue实例,router实例,store实例等。
在这里插入图片描述
src/entry-server.js
服务端渲染入口函数

import { createApp } from "./main";
import { renderToString } from "@vue/server-renderer";

import { getAsyncData } from '@src/utils/publics';

export async function render(url, manifest) {
  const { app, router, store } = createApp();

  // 同步url
  router.push(url);
  store.$setSsrPath(url);
  await router.isReady();
  // 新加 + 当路由准备完毕,调用自定义钩子,在服务端获取数据
  await getAsyncData(router, store, true);

  // 生成html字符串
  const ctx = {};
  const html = await renderToString(app, ctx);

  // 根据打包时生成的服务端预取清单manifest,生成资源预取数组
  const preloadLinks = ctx.modules
    ? renderPreloadLinks(ctx.modules, manifest)
    : [];
  return [html, preloadLinks, store];
}
 省略......

src/entry-client.js
客户端渲染入口函数

import { createApp } from './main'
const { app, router, store } = createApp()

// 这里需要先进行客户端状态同步 - 服务端携带过来的store
// 假设同学们用到的是vuex,我这边用的是自己写的状态管理包,就不写了
// 获取服务端渲染时,注入的__INITIAL_STATE__信息,并同步到客户端的vuex store中
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

router.isReady().then(() => {
  // 挂在当前vue实例于id为app的dom上
  app.use(VueRescroll)
     .use(VueImageLazyLoad)
     .mount('#app');
})
// 开启路由后置钩子,进行页面数据请求
router.afterEach(() => {
  getAsyncData(router, store, false);
})

getAsyncData
像vue2.0的做法那样,新加asyncData钩子作为数据预取的钩子

// 执行注册store钩子
export const registerModules = (
  components: Component[],
  router: Router,
  store: BaseStore
) => {
  return components
    .filter((i: any) => typeof i.registerModule === "function")
    .forEach((component: any) => {
      component.registerModule({ router: router.currentRoute, store });
    });
};

// 调用当前匹配到的组件里asyncData钩子,预取数据
export const prefetchData = (
  components: Component[],
  router: Router,
  store: BaseStore
) => {
  const asyncDatas: any[] = components.filter(
    (i: any) => typeof i.asyncData === "function"
  );
  return Promise.all(
    asyncDatas.map((i) => {
      return i.asyncData({ router: router.currentRoute.value, store });
    })
  );
};

// ssr自定义钩子
export const getAsyncData = (
  router: Router,
  store: BaseStore,
  isServer: boolean
): Promise<void> => {
  return new Promise(async (resolve) => {
    const { matched, fullPath } = router.currentRoute.value;

    // 当前路由匹配到的组件
    const components: Component[] = matched.map((i) => {
      return i.components.default;
    });
    // 动态注册store
    registerModules(components, router, store);

    if (isServer || store.ssrPath !== fullPath) {
      // 预取数据
      await prefetchData(components, router, store);
      !isServer && store.$setSsrPath("");
    }

    resolve();
  });
};

.vue里面预取数据
跟data,computed同级

	async asyncData({ store, router }: any) {
		if (!store.blog) return;
		const { blogDetail } = store.blog;
        blogDetail.$assignParams({
            id: router.query.id
        })
        await blogDetail.loadData();
	},

对于ssr的改造做了上述这些,还有些项目优化,比如模块化,ts,store的按需注册,以及一些自定义插件等,就不一一道来了,喜欢的同学可以download源码或者fork过去玩玩。
有需要交流的同学,也欢迎评论区交流交流。

项目仓库:https://github.com/Vitaminaq/cfsw-vue-cli3.0/tree/vue3.0-ssr
项目中用到的插件仓库:https://github.com/Vitaminaq/plugins-vue(喜欢的同学可以自取,欢迎同学们加入开发)
有同学想了解vue2.0最开始服务端渲染做法的,可以参考我之前的文章
有想了解vue3.0 + vue-cli服务端渲染的,可参考我最新的文章

2023-05-06补充
在这里插入图片描述
感谢上面的这位同学,让我又活过来了,最新的hooks写法还有点简陋,有时间再补全封装下,参考下这个仓库

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐