Nuxt生命周期
Nuxt:使用 vue-server-render 插件進行服務端渲染,並集成了vue-router、vuex的服務端渲染框架
一、從命令行啟動服務分析(以 nuxt 命令為例)
命令行調用文件 node_modules/nuxt/bin/nuxt.js:
...
const suffix = require('../package.json').name.includes('-edge') ? '-edge' : ''
require('@nuxt/cli' + suffix).run() //引入對應的nuxt/cli 文件
.catch((error) => {
require('consola').fatal(error)
process.exit(2)
})
...
在 nuxt/cli.js 文件中,根據輸入的命令行參數來加載對應的配置:
// cli.js
async function run(_argv) {
const argv = _argv ? Array.from(_argv) : process.argv.slice(2);
let cmd = await __chunk_1.getCommand(argv[0]);
// 根據命令行參數加載對應的配置,並以NuxtCommand類實例化cmd,cmd則為一個包含客戶端和服務端配置的實例
...
// 加載配置之后,將具體配置傳入 cmd 實例的 run 方法,並運行
if (cmd) {
return __chunk_1.NuxtCommand.run(cmd, argv.slice(1))
}
...
}
在 cmd 實例中,run 方法會根據傳入的配置進行 Nuxt 類的實例化,得到所需的 nuxt 實例:
// cli-chunk.js
class NuxtCommand{
...
async run(cmd) {
const { argv } = cmd;
const nuxt = await this.startDev(cmd, argv, argv.open);
}
async startDev(cmd, argv) {
const config = await cmd.getNuxtConfig({ dev: true, _build: true }); // 讀取環境配置
const nuxt = await cmd.getNuxt(config); // 實例化nuxt
...
await nuxt.server.listen(); //實例化 nuxt 后,服務器進行端口監聽
...
const builder = await cmd.getBuilder(nuxt); // 根據 nuxt 實例配置進行文件打包
await builder.build();
...
return nuxt
}
...
}
其中在實例化 nuxt 的過程中會執行以下操作:
// core.js
class Nuxt extends Hookable {
constructor(options = {}) {
...
this.options = config.getNuxtConfig(options); // 讀取通用配置
this.resolver = new Resolver(this); // 創建路徑解析器
this.moduleContainer = new ModuleContainer(this); // 模塊加載容器
...
this.showReady = () => { this.callHook('webpack:done'); };
if (this.options.server !== false) {
this._initServer(); // 初始化服務器實例,其中利用Node的http模塊啟動服務,利用 connect 框架進行請求響應的處理
}
...
// 調用nuxt實例的ready方法,其中ready方法即調用了 nuxt 實例的 _init 方法
if (this.options._ready !== false) {
this.ready().catch((err) => {
consola.fatal(err);
});
}
}
async _init() {
...
// 添加自定義周期鈎子函數(如vue-renderer:ssr:prepareContext)
if (isPlainObject_1(this.options.hooks)) {
this.addHooks(this.options.hooks);
} else if (typeof this.options.hooks === 'function') {
this.options.hooks(this.hook);
}
...
// 執行 server 的 ready 方法
if (this.server) {
await this.server.ready();
}
await this.callHook('ready', this);
return this
}
...
}
// server.js
async ready() {
...
const context = new ServerContext(this); // 實例化 server 上下文
this.renderer = new VueRenderer(context); // 創建 renderer
await this.renderer.ready();
...
await this.setupMiddleware(); // 設立中間件並進行渲染
...
await this.nuxt.callHook('render:done', this);
return this
}
// 分析setupMiddleware
async setupMiddleware() {
// 生產環境壓縮中間件
if (!this.options.dev) {
const { compressor } = this.options.render;
if (typeof compressor === 'object') {
const compression = this.nuxt.resolver.requireModule('compression');
this.useMiddleware(compression(compressor));
} else if (compressor) {
this.useMiddleware(compressor);
}
}
...
// 處理根目錄static文件夾的路徑映射
const staticMiddleware = serveStatic(
path.resolve(this.options.srcDir, this.options.dir.static),
this.options.render.static
);
staticMiddleware.prefix = this.options.render.static.prefix;
this.useMiddleware(staticMiddleware);
...
this.useMiddleware(nuxtMiddleware({
options: this.options,
nuxt: this.nuxt,
renderRoute: this.renderRoute.bind(this),
resources: this.resources
}));
// nuxtMiddleware 返回的函數利用 vue-server-render 提供的方法生成 html 字符串,並返回給瀏覽器
// 利用 renderer 的 renderRoute 方法,根據不同模式進行 SPA 或者 universal 模式的渲染
...
}
// vue-renderer.js
// renderer 的 ready 方法解析
async _ready() {
...
await this.loadResources(fs); // 加載資源
...
}
async loadResources(_fs) {
...
for (const resourceName in this.resourceMap) {
const { fileName, transform, encoding } = this.resourceMap[resourceName]; // 加載部分模板和配置
let resource = await readResource(fileName, encoding);
}
...
await this.loadTemplates(); // 讀取 loading 和 error 時的模板文件
...
this.createRenderer(); // 調用 vue-server-render 內的方法生成renderer
...
}
async renderRoute(url, context = {}, _retried) {
if (!this.isReady) {
// 生產環境調用
if (!this.context.options.dev) {
if (!_retried && ['loading', 'created'].includes(this._state)) {
await this.ready();
return this.renderRoute(url, context, true)
}
...
}
}
...
if (context.spa === undefined) {
context.spa = !this.SSR || req.spa || (context.res && context.res.spa);
}
await this.context.nuxt.callHook('vue-renderer:context', context);
// 根據SPA模式和SSR模式進行渲染
return context.spa
? this.renderSPA(context)
: this.renderSSR(context)
}
至此,由命令行啟動的服務完成,主要流程概括
二、從瀏覽器訪問 Nuxt 搭建的網站
用戶通過瀏覽器訪問網站時,主要訪問流程根據 .nuxt 文件夾中的文件執行順序一致
首先由 server.js 文件創建根應用(該部分在服務器端完成)
// .nuxt/server.js
export default async (ssrContext) => {
...
// 創建默認配置
ssrContext.nuxt = { layout: 'default', data: [], error: null, state: null, serverRendered: true }
...
// 創建根應用 _app,創建路由實例router和store實例
const { app, router, store } = await createApp(ssrContext)
const _app = new Vue(app)
...
// 匹配對應的組件
const Components = getMatchedComponents(router.match(ssrContext.url))
// 執行 store 中的 nuxtServerInit 方法
if (store._actions && store._actions.nuxtServerInit) {
try {
await store.dispatch('nuxtServerInit', app.context)
} catch (err) {
debug('error occurred when calling nuxtServerInit: ', err.message)
throw err
}
}
// 調用nuxt.config.js中配置的全局中間件
let midd = []
midd = midd.map((name) => {
if (typeof name === 'function') return name
if (typeof middleware[name] !== 'function') {
app.context.error({ statusCode: 500, message: 'Unknown middleware ' + name })
}
return middleware[name]
})
await middlewareSeries(midd, app.context)
// 根據匹配到的組件中的 layout 設置,加載對應的布局方式
let layout = Components.length ? Components[0].options.layout : NuxtError.layout
if (typeof layout === 'function') layout = layout(app.context)
await _app.loadLayout(layout)
if (ssrContext.nuxt.error) return renderErrorPage()
layout = _app.setLayout(layout)
ssrContext.nuxt.layout = _app.layoutName
// 調用組件中設置的中間件方法
midd = []
layout = sanitizeComponent(layout)
if (layout.options.middleware) midd = midd.concat(layout.options.middleware)
Components.forEach((Component) => {
if (Component.options.middleware) {
midd = midd.concat(Component.options.middleware)
}
})
midd = midd.map((name) => {
if (typeof name === 'function') return name
if (typeof middleware[name] !== 'function') {
app.context.error({ statusCode: 500, message: 'Unknown middleware ' + name })
}
return middleware[name]
})
await middlewareSeries(midd, app.context)
// 校驗組件的validate函數是否有效,若函數執行報錯則渲染錯誤頁面並返回,
let isValid = true
try {
for (const Component of Components) {
if (typeof Component.options.validate !== 'function') continue
isValid = await Component.options.validate(app.context)
if (!isValid) break
}
} catch (validationError) {
app.context.error({
statusCode: validationError.statusCode || '500',
message: validationError.message
})
return renderErrorPage()
}
...
if (!Components.length) return render404Page() // 若匹配不到對應的組件則渲染404頁面返回
...
// 服務端預取數據過程
const asyncDatas = await Promise.all(Components.map((Component) => {
const promises = []
// 調用組件的 asyncData 方法獲取數據
if (Component.options.asyncData && typeof Component.options.asyncData === 'function') {
const 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)
}
// 調用組件的 fetch 方法獲取數據
if (Component.options.fetch) {
promises.push(Component.options.fetch(app.context))
} else {
promises.push(null)
}
return Promise.all(promises)
}))
// 將服務器預取得到的數據注入到渲染上下文,最后通過 window.__NUXT__ 屬性返回給瀏覽器
ssrContext.nuxt.data = asyncDatas.map(r => r[0] || {})
// 調用 beforeRender 方法,包括調用組件的 beforeNuxtRender 方法,並將 store 的狀態注入到 渲染上下文
// 並執行第一步分析中的 renderer 實例中的 renderRoute 方法生成 html 字符串返回給瀏覽器
await beforeRender()
return _app
}
瀏覽器獲取到服務器返回的 html 字符串以及預取數據之后,將根據預取的數據進行客戶端根應用的實例化
// .nuxt/client.js
...
const NUXT = window.__NUXT__ || {} // 獲取預取數據
...
createApp()
.then(mountApp)
.catch((err) => {
const wrapperError = new Error(err)
wrapperError.message = '[nuxt] Error while mounting app: ' + wrapperError.message
errorHandler(wrapperError)
})
// 創建客戶端根應用並進行掛載
...
async function mountApp(__app) {
app = __app.app
router = __app.router
store = __app.store
...
const Components = await Promise.all(resolveComponents(router)) // 匹配對應的組件
const _app = new Vue(app) // 創建根應用
...
// 對每個路由跳轉之前進行組件的加載以及數據的預取(asyncData和fetch)
router.beforeEach(loadAsyncComponents.bind(_app))
router.beforeEach(render.bind(_app))
...
// 接收到由服務端渲染好的頁面,直接掛載到相應的DOM節點
if (NUXT.serverRendered) {
mount()
return
}
...
}
三、SPA模式下的生命周期
在 SPA 模式下,nuxt 則會按照普通 vue 單頁應用進行運行,不會進行服務端渲染,而采用客戶端渲染;服務器接收到瀏覽器的請求之后,匹配返回對應路徑的 html 文件之后(dist文件夾中),執行其中的 js 代碼,進行實例化,其中原本在服務端進行的 asyncData 和 fetch 方法則改在瀏覽器端進行,執行順序不變(仍是在匹配出對應路由組件之后)