Nuxt.js是vue官方推荐的一款优秀的服务端渲染(ssr)项目,集成了Vue,Vue-Router,Vuex,Vue-Meta等组件/框架,内置了webpack用于自动化构建,使我们可以更快速地搭建一个具有服务端渲染能力的应用。
今天主要来了解下Nuxt.js中一个非常重要的拓展功能:
asyncData(异步数据)的实现
1、asyncData有什么用?
在日常需求中可能想要在服务器端获取并渲染数据。那么使用asyncData方法可以使得你能够在渲染组件之前异步获取数据。asyncData方法会在组件(限于页面组件)每次加载之前被调用。它可以在服务端或路由更新之前被调用。
在这个方法被调用的时候,第一个参数被设定为当前页面的上下文对象,你可以利用asyncData方法来获取数据并返回给当前组件。
2、为了更好的理解,首先先了解下ssr原理
如上图所示:webpack将 Source 打包出两个bundle文件:
其中 Server Bundle用于服务端渲染,服务端通过渲染器 bundleRenderer 将 bundle 生成首屏html片段;
而 Client Bundle 用于客户端渲染,首屏外的交互和数据处理还是需要浏览器执行 Client Bundle 来完成
我们的主角asyncData()就是在上图中Node Server中处理
3、创建渲染器
我们直接从打包之后说起,Nuxt renderer使用vue-server-renderer插件创建渲染器并解析webpack打好的bundle文件
const { createBundleRenderer } = require('vue-server-renderer’)
async ready() {
...
if (!this.options.dev) {
await this.loadResources()
}
...
}
//加载resource资源
async loadResources(_fs = fs) {
let distPath = path.resolve(this.options.buildDir, 'dist')
let updated = []
// 根据resourceMap配置去相应位置找到webpack打包好的文件
//(打包配置可在源码builder下webpack目录下server.js与client.js查看)
resourceMap.forEach(({ key, fileName, transform }) => {
...
this.resources[key] = data
updated.push(key)
})
...
if (updated.length > 0) {
this.createRenderer()
}
}
createRenderer() {
...
// 创建ssr渲染器
this.bundleRenderer = createBundleRenderer(
// 此文件是webpack 使用 VueSSRServerPlugin插件生成的server-bundle.json(服务器构建清单),
// 一个json文件,后边在调用渲染器方法时会根据这个清单解析不同文件
this.resources.serverBundle,
… //一些渲染器配置
)
}
4 、渲染器调用自身方法返回拼装好的组件html
let APP = await this.bundleRenderer.renderToString(context)
调用renderToString()会在传入包含请求信息的上下文,方法内读取server-bundle.json构建清单(其实它是由我们自定义的一个server.js生成,这里面写着如何去提前取数据),并将上下文环境context传入,该文件返回一个新的vue实例,renderToString()方法会根据返回的vue实例生成一段拼装好数据的html片段
4.1、server.js做了哪些事情
server.js做了如下图红框中这些事情
每个用户通过浏览器访问Vue页面时,都是一个全新的上下文,但在服务端,应用启动后就一直运行着,处理每个用户请求的都是在同一个应用上下文中。为了不串数据,需要为每次SSR请求,创建全新的app, store, router。
const { app, router<%= (store ? ', store' : '') %> } = await createApp(ssrContext)
const _app = new Vue(app)
我们主要关注最后一部分asyncData部分
首先会根据上下文环境中的url调用getMatchedcomponents()将匹配的component返回
const Components = getMatchedComponents(router.match(ssrContext.url))
// 首先会根据上下文环境中的url调用getMatchedComponents()将匹配的component返回
export function getMatchedComponents(route, matches = false) {
return [].concat.apply([], route.matched.map(function (m, index) {
return Object.keys(m.components).map(function (key) {
matches && matches.push(index)
return m.components[key]
})
}))
}
接着遍历每个component,根据component的asyncData配置,执行 promisify()来promise化asyncData方法并将上下文对象赋给asyncData方法
promisify()方法接受两个参数:第一个组件中配置的asyncData()方法;第二个是挂载到新vue实例上的上下文对象
<-- server.js文件-->
let asyncDatas = await Promise.all(Components.map(Component => {
let promises = []
// Call asyncData(context)
if (Component.options.asyncData && typeof Component.options.asyncData === 'function') {
let promise = promisify(Component.options.asyncData, app.context)
promise.then(asyncDataResult => {
ssrContext.asyncData[Component.cid] = asyncDataResult
applyAsyncData(Component)
return asyncDataResult
})
promises.push(promise)
} else {
promises.push(null)
}
// Call fetch(context)
if (Component.options.fetch) {
promises.push(Component.options.fetch(app.context))
}
else {
promises.push(null)
}
return Promise.all(promises)
}))
执行后通过applyAsyncData()方法将得到的数据同步一份给页面中定义的data,asyncData只是在首屏的时候调用一次,后续交互还是交给client处理
export function applyAsyncData(Component, asyncData) {
const ComponentData = Component.options.data || noopData
// Prevent calling this method for each request on SSR context
if (!asyncData && Component.options.hasAsyncData) {
return
}
Component.options.hasAsyncData = true
Component.options.data = function () {
const data = ComponentData.call(this)
if (this.$ssrContext) {
asyncData = this.$ssrContext.asyncData[Component.cid]
}
// 将服务端获取的数据绑定到data上
return { ...data, ...asyncData }
}
if (Component._Ctor && Component._Ctor.options) {
Component._Ctor.options.data = Component.options.data
}
}
5、拼装完整html并return
server.js会将新创建的Vue实例返回,renderToString()会根据实例内容创建好一段已经拼装好的代码片段
最后就是调用ssrTemplate将一些layout的模板拼装好返回整个页面的html
async renderRoute(url, context = {}) {
...
// 非服务端渲染,返回空标签,等待挂载vue实例
if (this.noSSR || spa) {
...
return { html, getPreloadFiles }
}
// Call renderToString from the bundleRenderer and generate the HTML (will update the context as well)
// 服务端渲染,APP即为拼装好的html片段
let APP = await this.bundleRenderer.renderToString(context)
...
APP += `< type="text/java">${serializedSession}`
APP += context.renders()
APP += m..text({ body: true })
APP += m.no.text({ body: true })
HEAD += context.renderStyles()
let html = this.resources.ssrTemplate({
HTML_ATTRS: 'data-n-head-ssr ' + m.htmlAttrs.text(),
BODY_ATTRS: m.bodyAttrs.text(),
HEAD,
APP,
ENV
})
return {
html,
cspSrcHashes,
getPreloadFiles: context.getPreloadFiles,
error: context.nuxt.error,
redirected: context.redirected
}
}