vue-ssr服务端渲染透析

4,906

背景

spa单页面seo不友好,因为vue的话是只有一个HTML页面,实现页面的切换是通过监听router进行路由分发,结合ajax加载数据进行渲染的,但是搜索引擎爬虫识别不了js,所以就不会有一个好的排名。但是通常情况下移动端不需要做seo优化,甲方这么要求的,说现在市面上也有实现了,这么也顺便实战一下。

什么是ssr(Server-Side Rendering服务端渲染)

简单理解是将组件或页面通过服务器生成html字符串,再发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序

Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,ssr也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。

服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行。所谓同构,通俗的讲,就是一套 vue 代码在服务端上运行一遍,到达浏览器客户端又运行一遍。在服务端vue组件渲染为html字符串(即页面结构),在客户端操作dom。

打开浏览器的network,我们看到了初始化渲染的HTML,并且是我们想要初始化的结构,且完全不依赖于客户端的js文件了。再仔细研究研究,里面有初始化的dom结构,有css,还有一个script标签。script标签里把我们在服务端entry拿到的数据挂载了window上。原来只是一个纯静态的HTML页面啊,没有任何的交互逻辑,所以啊,现在知道为啥子需要服务端跑一个vue客户端再跑一个vue了,服务端的vue只是混入了个数据渲染了个静态页面,客户端的vue才是去实现交互的!

ssr优缺点

优点

  • 更好的SEO

因为SPA页面的内容是通过Ajax获取,而搜索引擎爬取工具并不会等待Ajax异步完成后再抓取页面内容,所以在SPA中是抓取不到页面通过Ajax获取到的内容的;而SSR是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;

  • 更利于首屏渲染

首屏的渲染是node发送过来的html字符串,并不依赖于js文件了,这就会使用户更快的看到页面的内容。尤其是针对大型单页应用,打包后文件体积比较大,普通客户端渲染加载所有所需文件时间较长,首页就会有一个很长的白屏等待时间。

缺点和约束

  • 由于没有动态更新,所有的生命周期钩子函数中,只有 beforeCreate 和 created 会在服务器端渲染 (SSR) 过程中被调用。这就是说任何其他生命周期钩子函数中的代码(例如 beforeMount 或 mounted),只会在客户端执行。
  • 如有在beforeCreat与created钩子中使用第三方的API,需要确保该类API在node端运行时不会出现错误,比如在created钩子中初始化一个数据请求的操作,这是正常并且及其合理的做法。但如果只单纯的使用XHR去操作,那在node端渲染时就出现问题了,所以应该采取axios这种浏览器端与服务器端都支持的第三方库。
  • 当编写纯客户端 (client-only) 代码时,我们习惯于每次在新的上下文中对代码进行取值。但是,Node.js 服务器是一个长期运行的进程。当我们的代码进入该进程时,它将进行一次取值并留存在内存中。这意味着如果创建一个单例对象,它将在每个传入的请求之间共享。所以对于服务器端渲染,我们也希望每个请求应该都是全新的、独立的应用程序实例,以便不会有交叉请求造成的状态污染 (cross-request state pollution)。所以应该使用工厂函数来确保每个请求之间的独立性

ssr原理

解释下这张图:

  • 左边是源代码,app.js被server/client共用,在生产环境下是不用执行的。
  • 右边是编译后的文件,两份打包后的文件。
  • 一份运行在服务器端,在服务器上有一个打包的渲染函数负责渲染输出浏览器所需要的html页面
  • 一份运行在浏览器端,负责跟这份html页面交互,判断当前服务器返回的数据是否是当前URL对应页面的数据。如果不是的话它会请求一次服务器再渲染,如果是的话它就按照正常vue架构接管页面

根据应用的触发时机我们分成以下几个步骤详细讲解SSR是如何运作的:

编译阶段

vue-ssr提供的方式是配置两个入口文件(entry-client.js、entry-server.js),通过webpack把你的代码编译成两个bundle。

  • Server Bundle为vue-ssr-server-bundle.json:
  • Client Bundle为vue-ssr-client-manifest.json

初始化(获取到vue-ssr-server-bundle.json):

  • ssr应用会在node启动时初始化一个renderer单例对象,renderer对象由vue-server-renderer库的createBundleRenderer函数创建,函数接受两个参数,serverBundle(服务端入口文件打包后的)内容和options配置
  • 获取到serverBundle的入口文件代码并解析为入口函数,每次执行实例化vue对象
  • 实例化了render和templateRenderer对象,负责渲染vue组件和组装html

渲染阶段:(执行vue-ssr-server-bundle.json)

  • 当用户请求达到node端时,调用bundleRenderer.renderToString函数并传入用户上下文context,context对象可以包含一些服务端的信息,比如:url、ua等等,也可以包含一些用户信息。通过执行serverBundle后得到的应用入口函数,实例化vue对象。
  • renderer对象负责把vue对象递归转为vnode,并把vnode根据不同node类型调用不同渲染函数最终组装为html。
  • 当使用 template 时,context.state 将作为 window.__INITIAL_STATE__ 状态,自动嵌入到最终的 HTML 中返回给客户端。而在客户端,在挂载到应用程序之前,store 就应该获取到状态:

内容输出阶段:

  • 在上一个阶段我们已经拿到了vue组件渲染结果,它是一个html字符串,在浏览器中展示页面我们还需要css、js等依赖资源的引入标签和我们在服务端的渲染数据,这些最终组装成一个完整的html报文输出到浏览器中。

客户端阶段(获取执行客户端代码 vue-ssr-client-manifest.json):

  • 当客户端发起了请求,服务端返回渲染结果和css加载完毕后,用户就已经可以看到页面渲染结果了,不用等待js加载和执行。服务端输出的数据有两种,一个是服务端渲染的页面结果,还有一个在服务端需要输出到浏览器的数据状态window.__INITIAL_STATE__
  • 这些数据需要同步给浏览器,否则会造成两端组件状态不一致。我们一般会使用vuex来存储这些数据状态,之前在服务端渲染完成后把vuex的state复制给用户上下文的context.state。context.state = store.state
  • 当客户端开始执行js时,我们可以通过window全局变量读取到这里的数据状态,并替换到自己的数据状态 store.replaceState(window.__INITIAL_STATE__);实现服务端和客户端的 store 数据同步
  • 之后在我们调用$mount挂载vue对象之前,客户端会和服务端生成的DOM进行Hydration对比(判断这个DOM和自己即将生成的DOM是否相同(vuex store 数据同步才能保持一致)
  • 如果相同就调用app.$mount('#app')将客户端的vue实例挂载到这个DOM上,即去“激活”这些服务端渲染的HTML之后,其变成了由Vue动态管理的DOM,以便响应后续数据的变化,即之后所有的交互和vue-router不同页面之间的跳转将全部在浏览器端运行。
  • 如果客户端构建的虚拟 DOM 树vDOM与服务器渲染返回的HTML结构不一致,这时候,客户端会请求一次服务器再渲染整个应用程序,这使得ssr失效了,达不到服务端渲染的目的了。

V1

ssr有两个入口文件,分别是客户端的入后文件和服务端的入口文件,webpack通过两个入口文件分别打包成给服务端用的server bundle和给客户端用的client bundle.当服务器接收到了来自客户端的请求之后,会创建一个渲染器bundleRenderer,这个bundleRenderer会读取上面生成的server bundle文件,并且执行它的代码, 然后发送一个生成好的html到浏览器,等到客户端加载了client bundle之后,会和服务端生成的DOM进行Hydration(判断这个DOM和自己即将生成的DOM是否相同,如果相同就将客户端的vue实例挂载到这个DOM上)

V2

我们的服务端代码和客户端代码通过webpack分别打包,生成Server Bundle和Client Bundle,前者会运行在服务器上通过node生成预渲染的HTML字符串,发送到我们的客户端以便完成初始化渲染;而客户端bundle就自由了,初始化渲染完全不依赖它了。客户端拿到服务端返回的HTML字符串后,会去“激活”这些静态HTML,是其变成由Vue动态管理的DOM,以便响应后续数据的变化。

V3

  • 首先我们的entry-server会获取到当前router匹配到的组件,调用组件上asyncData方法,将数据存到服务端的vuex中,然后服务端vuex中的这些数据传给我们的context。
  • Node.js服务器通过renderToString将需要首屏渲染的html字符串send道我们的客户端上,这其中混入了window.INITIAL_STATE 用来存储我们服务端vuex的数据。
  • 然后entry-client,此时服务端渲染时候拿到的数据写入客户端的vuex中。
  • 最后就是客户端和服务端的组件做diff了,更新状态更新的组件。

V4

客户端请求服务器,服务器根据请求地址获得匹配的组件,在调用匹配到的组件返回Promise (官方是asyncData方法)来将需要的数据拿到。最后再通过window.__initial_state=data将其写入网页,最后将服务端渲染好的网页返回回去。接下来客户端将用新的store状态把原来的store状态替换掉,保证客户端和服务端的数据同步。遇到没被服务端渲染的组件,再去发异步请求拿数据

能在服务端渲染为html字符串得益于vue组件结构是基于vnode的。vnode是dom的抽象表达,它不是真实的dom,它是由js对象组成的树,每个节点代表了一个dom。因为vnode所以在服务端vue可以把js对象解析为html字符串

同样在客户端vnode因为是存在内存之中的,操作内存总比操作dom快的多,每次数据变化需要更新dom时,新旧vnode树经过diff算法,计算出最小变化集,大大提高了性能。

创建vue实例(main.js/app.js)

app.js是我们的通用entry,它的作用就是构建一个Vue的实例以供服务端和客户端使用,注意一下,在纯客户端的程序中我们的app.js将会挂载实例到dom中,而在ssr中这一部分的功能放到了Client entry中去做了。

当编写纯客户端 (client-only) 代码时,我们习惯于每次在新的上下文中对代码进行取值。但是,Node.js 服务器是一个长期运行的进程。当我们的代码进入该进程时,它将进行一次取值并留存在内存中。这意味着如果创建一个单例对象,它将在每个传入的请求之间共享。

如基本示例所示,我们为每个请求创建一个新的根 Vue 实例。这与每个用户在自己的浏览器中使用新应用程序的实例类似。如果我们在多个请求之间使用一个共享的实例,很容易导致交叉请求状态污染 (cross-request state pollution)。

因此,我们不应该直接创建一个应用程序实例,而是应该暴露一个可以重复执行的工厂函数,为每个请求创建新的应用程序实例。app.js 简单地使用 export 导出一个 createApp 函数:

// app.js
import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'
import { sync } from 'vuex-router-sync'

export function createApp () {
  // 创建 router 和 store 实例
  const router = createRouter()
  const store = createStore()

  // 同步路由状态(route state)到 store
  sync(store, router)

  // 创建应用程序实例,将 router 和 store 注入
  const app = new Vue({
    router,
    store,
    render: h => h(App)
  })

  // 暴露 app, router 和 store。
  return { app, router, store }
}

服务端入口文件(entry-server.js)

在服务器端渲染(SSR)期间,我们本质上是在渲染我们应用程序的"快照",所以如果应用程序依赖于一些异步数据,那么在开始渲染过程之前,需要先预取和解析好这些数据。

Server entry是一个使用export导出的函数。主要负责调用组件内定义的获取数据的方法,获取到SSR渲染所需数据,并存储到上下文环境中。这个函数会在每一次的服务端渲染中重复的调用。

这里要做的就是拿到当前路由匹配的组件,调用组件里定义的一个方法(官网取名叫asyncData)拿到初始化渲染的数据,而这个方法要做的也很简单,就是去调用我们vuex store中的方法去异步获取数据。

这个文件的主要工作是接受从服务端(server.js)传递过来的context参数,context包含当前页面的url,用getMatchedComponents方法获取当前url下的组件,返回一个数组,遍历这个数组中的组件,如果组件有asyncData钩子函数,则传递store获取数据,最后返回一个promise对象。context.state = store.state的作用是将服务端获取到的数据挂载到context对象上,这时候服务端拿到的预渲染数据会存在window.INITIAL_STATE,后面在client-entry.js中调用store.replaceState(window.__INITIAL_STATE__),保证客户端和服务端的state一致。

在 entry-server.js 中,我们可以通过路由获得与 router.getMatchedComponents() 相匹配的组件,如果组件暴露出 asyncData(定义在组件内专门请求数据用的),我们就调用这个方法。然后我们需要将解析完成的状态,附加到渲染上下文(render context)中

// entry-server.js
import { createApp } from './app'

// context是用户上下文对象
export default context => {
  // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise,
  // 以便服务器能够等待所有的内容在渲染前,
  // 就已经准备就绪。
  return new Promise((resolve, reject) => {
    const { app, router } = createApp()

    // 设置服务器端 router 的位置
    router.push(context.url)

    // 等到 router 将可能的异步组件和钩子函数解析完
    router.onReady(() => {
      // 我们可以通过路由获得与 router.getMatchedComponents() 相匹配的组件
      const matchedComponents = router.getMatchedComponents()
      // 匹配不到的路由,执行 reject 函数,并返回 404
      if (!matchedComponents.length) {
        return reject({ code: 404 })
      }

      // 对所有匹配的路由组件调用 `asyncData()`请求获取后端数据
      // 所有请求并行执行
      Promise.all(matchedComponents.map(Component => {
        if (Component.asyncData) {
          return Component.asyncData({
            store,
            route: router.currentRoute
          })
        }
      })).then(() => {
        // 在所有预取钩子(preFetch hook) resolve 后,
        // 我们的 store 现在已经填充入渲染应用程序所需的状态。
        // 当我们将状态附加到上下文,
        // 并且 `template` 选项用于 renderer 时,
        // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
        // context.state 赋值成什么,window.__INITIAL_STATE__ 就是什么
        // 这下你应该明白entry-client.js中window.__INITIAL_STATE__是哪来的了,它是在服务端渲染期间被添加进上下文的
        context.state = store.state

        // Promise 应该 resolve 应用程序实例,以便它可以渲染
        resolve(app)
      }).catch(reject)
    }, reject)
  })
}

客户端入口文件(entry-client.js)

客户端 entry 只需创建应用程序,并且将其挂载到 DOM 中:

// entry-client.js

import { createApp } from './app'
import Vue from 'vue'

Vue.mixin({
  beforeRouteUpdate(to, from, next) {
    const {
      asyncData
    } = this.$options
    if (asyncData) {
      asyncData({
        store: this.$store,
        route: to
      }).then(next).catch(next)
    } else {
      next()
    }
  }
})

const {
  app,
  router,
  store
} = createApp()

// 将服务端渲染时候的状态写入vuex中
// 使用window.__INITIAL_STATE__中的数据替换整个state中的数据,这样服务端渲染结束后,客户端也可以自由操作state中的数据
// 这句的作用是如果服务端的vuex数据发生改变,就将客户端的数据替换掉,保证客户端和服务端的数据同步
if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

// 客户端数据预取 (Client Data Fetching)
// 在路由导航之前解析数据
router.onReady(() => {
  // 添加路由钩子函数,用于处理 asyncData.
  // 在初始路由 resolve 后执行,
  // 以便我们不会二次预取(double-fetch)已有的数据。
  // 使用 `router.beforeResolve()`,以便确保所有异步组件都 resolve。
  router.beforeResolve((to, from, next) => {
    const matched = router.getMatchedComponents(to)
    const prevMatched = router.getMatchedComponents(from)

    // 我们只关心非预渲染的组件
    // 所以我们对比它们的dom结构是否一致
    let diffed = false
    const activated = matched.filter((c, i) => {
      return diffed || (diffed = (prevMatched[i] !== c))
    })

    if (!activated.length) {
      return next()
    }

    // 这里如果有加载指示器 (loading indicator),就触发

    // 遇到没被服务端渲染的组件,再去发异步请求拿数据
    // 客户端构建的虚拟 DOM 树vDOM与服务器渲染返回的HTML结构不一致,这时候,客户端会请求一次服务器再渲染整个应用程序
    Promise.all(activated.map(c => {
      if (c.asyncData) {
        return c.asyncData({ 
          store, 
          route: to 
        })
      }
    })).then(() => {
      // 停止加载指示器(loading indicator)
      next()
    }).catch(next)
  })

  // 用下面这行挂载(mount)应用程序:客户端激活
  // 所谓客户端激活,指的是 Vue 在浏览器端接管由服务端发送的静态 HTML,使其变为由 Vue 管理的动态 DOM 的过程。
  app.$mount('#app')
})

关于store.replaceState(window .__ INITIAL_STATE__)的问题

我为什么要替换状态?我认为服务器渲染后状态不会改变。

之前说过,因为app.js、vue组件等代码在服务端执行一次,在客户端又会执行一次。所以当服务端拿到store并获取数据后,到客户端的时候,客户端的js代码又执行一遍,在客户端代码执行的时候又创建了一个空的store,两个store的数据不能同步。

如果服务端和客户端的数据store 不同步,客户端构建的虚拟 DOM 树virtualDOM与服务器渲染返回的HTML结构不一致,这时候,客户端会请求一次服务器再渲染整个应用程序,这使得ssr失效了,达不到服务端渲染的目的了

数据的注水和脱水(神三元大佬)

如何才能让这两个store的数据同步变化呢?

  • 通过在服务端获取数据之后,服务端的store数据注入到window全局环境中context.state = store.state,即context.state 将作为 window.INITIAL_STATE 状态,自动嵌入到最终的 HTML 中返回给客户端
  • 然后就是脱水处理:在客户端,通过store.replaceState(window.__INITIAL_STATE__)将服务端传过来的vuex store 数据替换成为客户端自己当前的vuex store数据,以此来达到客户端和服务端的数据同步。

创建服务端渲染器(server.js)

// server.js

const Vue = require('vue')
const express = require('express')
const path = require('path')
const LRU = require('lru-cache')
const resolve = file => path.resolve(__dirname, file)
const { createBundleRenderer } = require('vue-server-renderer')
const fs = require('fs')
const net = require('net')
const http = require('http');
const compression = require('compression');


const template = fs.readFileSync('./src/index.template.html', 'utf-8')
const isProd = process.env.NODE_ENV === 'production'

const server = express()

// 返回服务端渲染函数
function createRenderer (bundle, options) {
  return createBundleRenderer(bundle, Object.assign(options, {
    // 模板html文件就是 index.template.html
    template: template,
    cache: LRU({
      max: 1000,
      maxAge: 1000 * 60 * 15
    }),
    basedir: resolve('./dist'),
    runInNewContext: false
  }))
}

let renderer;

let readyPromise
if (isProd) {
  const bundle = require('./dist/vue-ssr-server-bundle.json')
  const clientManifest = require('./dist/vue-ssr-client-manifest.json')
  renderer = createRenderer(bundle, {
    clientManifest
  })
} else {
  readyPromise = require('./build/setup-dev-server')(server, (bundle, options) => {
    renderer = createRenderer(bundle, options)
  })
}


const serve = (path, cache) => express.static(resolve(path), {
  maxAge: cache && isProd ? 1000 * 60 * 60 * 24 * 30 : 0
})
server.use(compression())
server.use('/dist', serve('./dist', true))
server.use('/static', serve('./src/static', true))
server.use('/service-worker.js', serve('./dist/service-worker.js'))


server.get('*', (req, res) => {
  const context = {
    title: '网易严选',
    url: req.url
  }
  renderer.renderToString(context, (err, html) => {
    if (err) {
      res.status(500).end('Internal Server Error')
      return
    }
    // 将服务器端渲染好的html返回给客户端
    res.end(html)
  })
})

function probe(port, callback) {
    let servers = net.createServer().listen(port)
    let calledOnce = false
    let timeoutRef = setTimeout(function() {
        calledOnce = true
        callback(false, port)
    }, 2000)
    timeoutRef.unref()
    let connected = false

    servers.on('listening', function() {
        clearTimeout(timeoutRef)
        if (servers)
            servers.close()
        if (!calledOnce) {
            calledOnce = true
            callback(true, port)
        }
    })

    servers.on('error', function(err) {
        clearTimeout(timeoutRef)
        let result = true
        if (err.code === 'EADDRINUSE')
            result = false
        if (!calledOnce) {
            calledOnce = true
            callback(result, port)
        }
    })
}
const checkPortPromise = new Promise((resolve) => {
    (function serverport(_port = 6180) {
        // let pt = _port || 8080;
        let pt = _port;
        probe(pt, function(bl, _pt) {
            // 端口被占用 bl 返回false
            // _pt:传入的端口号
            if (bl === true) {
                // console.log("\n  Static file server running at" + "\n\n=> http://localhost:" + _pt + '\n');
                resolve(_pt);
            } else {
                serverport(_pt + 1)
            }
        })
    })()

})
checkPortPromise.then(data => {
    uri = 'http://localhost:' + data;
    console.log('启动服务路径'+uri)
    server.listen(data);
});

router.js

类似于 createApp,我们也需要给每个请求一个新的 router 实例,所以文件导出一个 createRouter 函数

// router.js
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export function createRouter () {
  return new Router({
    mode: 'history',  // 改为 history 模式
    routes: [
      { path: '/', component: () => import('./components/Home.vue') },
      { path: '/item/:id', component: () => import('./components/Item.vue') }
    ]
  })
}

store.js

在服务器端渲染(SSR)期间,我们本质上是在渲染我们应用程序的"快照",所以如果应用程序依赖于一些异步数据,那么在开始渲染过程之前,需要先预取和解析好这些数据。

另一个需要关注的问题是在客户端,在挂载 (mount) 到客户端应用程序之前,需要获取到与服务器端应用程序完全相同的数据 - 否则,客户端应用程序会因为使用与服务器端应用程序不同的状态,然后导致混合失败。

为了解决这个问题,获取的数据需要位于视图组件之外,即放置在专门的数据预取存储容器(data store)或"状态容器(state container))"中。首先,在服务器端,我们可以在渲染之前预取数据,并将数据填充到 store 中。此外,我们将在 HTML 中序列化(serialize)和内联预置(inline)状态。这样,在挂载(mount)到客户端应用程序之前,可以直接从 store 获取到内联预置(inline)状态。

为此,我们将使用官方状态管理库 Vuex。我们先创建一个 store.js 文件,里面会模拟一些根据 id 获取 item 的逻辑:

// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

// 假定我们有一个可以返回 Promise 的
// 通用 API(请忽略此 API 具体实现细节)
import { fetchItem } from './api'

export function createStore () {
  return new Vuex.Store({
    state: {
      items: {}
    },
    actions: {
      fetchItem ({ commit }, id) {
        // `store.dispatch()` 会返回 Promise,
        // 以便我们能够知道数据在何时更新
        return fetchItem(id).then(item => {
          commit('setItem', { id, item })
        })
      }
    },
    mutations: {
      setItem (state, { id, item }) {
        Vue.set(state.items, id, item)
      }
    }
  })
}

带有逻辑配置的组件Item.vue

那么,我们在哪里放置「dispatch 数据预取 action」的代码?

我们需要通过访问路由,来决定获取哪部分数据 - 这也决定了哪些组件需要渲染。事实上,给定路由所需的数据,也是在该路由上渲染组件时所需的数据。所以在路由组件中放置数据预取逻辑,是很自然的事情。

我们将在路由组件上暴露出一个自定义静态函数 asyncData。注意,由于此函数会在组件实例化之前调用,所以它无法访问 this。需要将 store 和路由信息作为参数传递进去:

syncData是干嘛用的?其实,这个函数是专门请求数据用的,你可能会问请求数据为什么不在beforeCreate或者created中完成,还要专门定义一个函数?虽然beforeCreate和created在服务端也会被执行(其他周期函数只会在客户端执行),但是我们都知道请求是异步的,这就导致请求发出后,数据还没返回,渲染就已经结束了,所以无法把 Ajax 返回的数据也一并渲染出来。因此需要想个办法,等到所有数据都返回后再渲染组件

<!-- Item.vue -->
<template>
  <div>{{ item.title }}</div>
</template>

<script>
export default {
  asyncData ({ store, route }) {
    // 触发 action 后,会返回 Promise
    // fetchItem 为获取后台数据的api
    return store.dispatch('fetchItem', route.params.id)
  },
  computed: {
    // 从 store 的 state 对象中的获取 item。
    item () {
      return this.$store.state.items[this.$route.params.id]
    }
  }
}
</script>

总结

vuex作用

vuex不是必须的,vuex是实现我们客户端和服务端的状态共享的关键,我们可以不使用vuex,但是我们得去实现一套数据预取的逻辑;

  • 不使用vuex其实很头疼,但又有了点灵感,平时我们在开发项目的时候是如何处理组件间通信的,一个是vuex,另一个是EventBus,EventBus就是个Vue的实例啊,数据存这里不也行么?
  • 在此笔者的思路是:创建一个Vue的实例充当仓库,那么我们可以用这个实例的data来存储我们的预取数据,而用methods中的方法去做数据的异步获取,这样我们只需要在需要预取数据的组件中去调用这个方法就可以了
  • 还有一个思路是在笔者学习的时候看别人博客学到的:只用了vuex的store和一些支持服务端渲染的api,没有走action、mutation那套,而是将数据手动写入state

参考文章