Nuxt生命周期


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 方法則改在瀏覽器端進行,執行順序不變(仍是在匹配出對應路由組件之后)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM