服务端渲染改造

在编写好正常的客户端渲染逻辑代码后,就可以针对项目的首屏开启服务端渲染改造了。在本项目中,主要是针对 home 页面的改造。主要步骤概括如下:

  • 基于服务端渲染逻辑和客户端渲染逻辑改造 main.js。

  • 跑通正常的客户端渲染开发和生产构建流程。

  • 创建 Node.js 服务端 server.js 逻辑,结合 Vite 跑通基于服务端渲染的开发流程。

  • 改造 Node.js 服务端 server.js 逻辑,跑通服务端渲染生产构建流程。

  • 配置 package.json 中定义的命令,以完成改造。

main.js改造

修改 main.js,使其同时支持服务端和客户端两种渲染逻辑,改造代码如下:

...
export function createApp() {
    // 如果使用服务端渲染,需要将createApp替换为createSSRApp
    const app = createSSRApp(App)
    // 路由
    const router = createRouter()
    // store
    const store = createStore()
    app.use(router)
    app.use(store)
    // 将根实例以及路由暴露给调用者
    return { app, router, store }
}

entry-client.js和entry-server.js

entry-client.js 和 entry-server.js 这两个文件分别作为客户端渲染和服务端渲染的入口,在后面的 server.js 中会被调用,其主要内容和10.2节一致,这里单独讲一下需要后端利用 Vuex 获取数据的逻辑,其区别如下:

entry-server.js
...
export async function render(url, manifest) {
  const { app, router, store } = createApp()

  // 设置默认的路由,/默认走home路由
  router.push(url)
  // 等待路由加载完成
  await router.isReady()
  // 获取首屏需要的异步数据store
  await getAsyncData(router,store, true)

  // store中已经存放了数据 提供渲染出html字符串
  const ctx = {}
  ctx.state = store.state
  const html = await renderToString(app, ctx)

  //处理需要预加载的链接
  const preloadLinks = renderPreloadLinks(ctx.modules, manifest)
  return [html, preloadLinks, store]
}

上述代码中用到的 getAsyncData 方法需要在 home 组件中配置

entry-client.js:
if(window.__INIT_STATE__) {
  // 当使⽤ template 时, context.state 将作为 window.__INIT_STATE__ 状态⾃动嵌⼊到最终的 HTML
  // 在客户端挂载到应⽤程序之前, store 就应该获取到状态:

  store.replaceState(window.__INIT_STATE__._state.data)
}

服务端获取的数据会注入 HTML 模板中挂在 window.INIT_STATE 对象中,在这里可以直接拿到,这样在浏览器端二次渲染时直接使用即可。

home.vue改造

在 home.vue 中,主要的改造点是:提供静态 asyncData 方法获取首屏数据,改造最近热映、正在热映、榜单三个模块对应的 nowplayList、recentplayList、rankList 数据放入 Vuex 的 store 中,其核心代码如下:

export default {
    // 定义静态方法
    asyncData({store}) {
        return store.dispatch('getHomeMovieData')
    },
    ...
    setup(){
        const store = useStore()
        // 从store获取数据
        let nowplayList = computed(()=> store.state.nowplayList)
        let recentplayList = computed(()=> store.state.recentplayList)
        let rankList = computed(()=> store.state.rankList)
        return {
            nowplayList,
            recentplayList,
            rankList
        }
    }
}

由于 asyncData 方法只会在服务端渲染时使用,和本书的组件并无太多逻辑关系,因此定义成静态方法更合适,对应的 store 页要修改部分逻辑。

store改造

新建一个 action,用来异步请求 home 页面所需要的数据,其核心代码如下:

actions: {
  async getHomeMovieData(context, obj){
    // 请求正在热映数据
    let nowplayList = await service.get(configapi.nowmovie,{
      start:0,
      count:50,
    })
    // 请求最近热映数据
    let recentplayList = await service.get(configapi.recentmovie,{
      start:0,
      count:50,
    })
    // 请求榜单映数据
    let rankList = await service.get(configapi.toprank,{
      start:0,
      count:10,
    })

    context.commit('setHomeData',{
      nowplayList,
      recentplayList,
      rankList
    })
  }
}

然后创建对应的 mutations 对数据进行处理,其核心代码如下:

  /*
  * 设置首屏数据
  */
  setHomeData(state, obj){

    state.rankList = obj.rankList.subject_collection_items || [];
    state.nowplayList = obj.nowplayList.subject_collection_items || [];
    // 将数据进行分组
    let toArray = (data)=>{
      let n = 10 // 10个一组
      let len = data.length
      let num = len % n == 0 ? len/n : Math.floor(len/n)+1
      let res = []
      for (var i = 0 ; i < num ; i++) {
        res.push(data.slice(i*n,i*n+n))
      }

      return res
    }
    state.recentplayList = toArray(obj.recentplayList.subject_collection_items || []);
  },

这些改造相当于将原来在 home.vue 的逻辑挪到了 actions 和 mutations 中,为服务端渲染提供支持。

在 store 文件下新建 getAsyncData.js,提供给 server.js 调用,其核心代码如下:

// 调用当前匹配到的组件中的asyncData钩子,预取数据
export const prefetchData = (
    components,
    router,
    store
) => {
// 过滤出有asyncData静态方法的组件
    const asyncDatas = components.filter(
        (i) => typeof i.asyncData === "function"
    );
    return Promise.all(
        asyncDatas.map((i) => {
            return i.asyncData({ router: router.currentRoute.value, store });
        })
    );
};
// ssr自定义钩子
export const getAsyncData = (
    router,
    store,
    isServer
) => {
    return new Promise(async (resolve) => {
        const { matched } = router.currentRoute.value;
        // 当前路由匹配到的组件
        const components = matched.map((i) => {
            return i.components.default;
        });
        // 如果有Vuex的modules,可以选择动态注册modules
        // registerModules(components, router, store);
        if (isServer) {
            // 预取数据
            await prefetchData(components, router, store);
        }
        resolve();
    });
};

其中,如果 registerModules 方法使用的是 Vuex 的 modules,并且需要动态添加时可以调用 registerModules,这里没有使用,就不调用了。getAsyncData.js 主要是一个桥梁,提供给 server.js 可以调用组件和 store 的能力,为了服务端获取数据服务。

server.js改造

服务端渲染的核心能力是利用 Node.js 提供渲染首屏 HTML 的服务,所以可以利用 Express 框架来开启一个 Node.js 服务,需要安装 Express:

cnpm i express -S

在 index.html 同级创建 server.js,其内容和10.2节类似,主要改动如下代码:

// 调用entry-server.js的render方法
const [appHtml, preloadLinks, store] = await render(url, manifest)

// 将服务端获取的数据注入到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)

上面的代码中,和之前的区别主要是调用 entry-server.js 方法获取数据,然后将数据注入生成的 html 字符串中,供客户端渲染使用。同时,利用 Express 提供静态资源的服务,主要配置如下:

// index false表示匹配不到静态文件时 不做处理交给后面逻辑
app.use('/js', express.static(resolve('dist/client/js'),{index: false}));
app.use('/css', express.static(resolve('dist/client/css'),{index: false}));
app.use('/assets', express.static(resolve('dist/client/assets'),{index: false}));
app.use('/json', express.static(resolve('dist/client/json'),{index: false}));
app.use('/favicon', express.static(resolve('dist/client/favicon'),{index: false}));

至此,整个服务端改造完成。本次改造的内容基本上是参照之前章节 Vite 服务端渲染章节所介绍的步骤和内容,在此基础上添加利用 Vuex 来获取服务端渲染首屏数据的逻辑,这部分逻辑的整体流程总结如图12-10所示。

image 2024 02 26 17 57 04 846
Figure 1. 图12-10 Vuex获取数据的流程

小结

本章主要开发了一个模仿豆瓣电影评分系统的实战项目,主要利用本书所讲解的 Vue 基础、Vuex、Vue Router、组合式 API、服务端渲染、Vite 前端工程化构建等内容,建议读者和视频教程结合起来一起学习,有助于完整掌握实战项目的开发流程和难点,祝各位读者学习愉快。