前言
哈嘍大家周五好,又是一個開開心心的周五了,接下來就是三天小團圓啦,這里先祝大家節日快樂咯,希望都沒有加班哈哈,今天公司發了月餅,嗯~時間來不及了,上周應該搞個活動抽中幾個粉絲發月餅的,下次吧,這里先預告一下,聖誕節活動,給粉絲送蘋果吧哈哈,不過聽起來好 low 呀,大家有好的想法可以下邊評論或者來群里一起交流喲~
說接上文,昨天咱們第一次的接觸到了一個新的框架 Nuxt《二七║ Nuxt 基礎:框架初探》,從概念上,給大家簡單說了下這個框架的產生和應用場景,大家學習這一塊一定要有一定的 vue 基礎,還有就是了解 SSR 服務端渲染的知識和原理,才能做到游刃有余,在之前的一系列文章中,咱們已經很詳細說明了 vue 的客戶端渲染 SPA 的工作原理,今天呢,咱們就重點說一下 Nuxt 的運行機制,雖說 Vue 已經發展了近兩年,不過網上的資料還是比較匱乏,所有的教程也都是一篇搞定,直接就是 如何安裝,如何寫頁面,我是不喜歡這樣的,雖然自己懂得的也不是很多,但是還是想給大家分享下,至少往深層次挖掘一下,這樣大家下次使用的時候,心里才有一個普。好啦,馬上開始今天的講解,今天沒有涉及到代碼,源碼會在下周開始 Code 。
零、今天要完成綠色的部分

一、Nuxt 的初衷 —— 將核心專注於UI渲染
來個官方的說法
Nuxt.js is a minimalistic framework for server-rendered Vue applications (inspired by Next.js)
意思就是:
Nuxt 是服務器呈現的簡約應用程序的框架,通過對客戶端和服務端基礎架構的抽象,Nuxt.js 可以讓開發者更專注於頁面的UI渲染。作用就是在 node.js 上進一步封裝,然后省去我們搭建服務端環境的步驟,只需要遵循這個庫的一些規則就能輕松實現 SSR。
說到這里大家可能稍微不是很明白,舉個最簡單的栗子( 這里就不說那個老生常談的 SEO 了,這個本來就不存在於我們的客戶端渲染中 ):咱們之前在用 vue-cli 腳手架搭建項目的時候,每次添加一個頁面,都需要去配置路由 router ,是吧,然后呢,有時候運氣不好了,在多層嵌套的路徑配置時候,還時不時的不起作用,這些本來不應該屬於我們開發的工作的,Nuxt 就很好的解決了這個問題,Nuxt.js 根據 pages 目錄結構去生成 vue-router 配置,也就是說 pages 目錄的結構直接影響路由結構,還是實時監聽的方式。
再舉個栗子:之前咱們在配置頁面的時候,都是通過 index.html 提供一個模板,然后再一個 app.vue 的入口程序,然后將所有的 *.vue 組件通過路由填充進去,這個咱們第一次使用的時候,感覺還是很嗨皮的,不過長時間的使用的時候,就會發現其實有些問題,比如我們如果不想讓某些組件路由顯示出來,然后只能在配置里處理,通過配置路由是否顯示等操作,顯然又多了一個步驟,不過在 nuxt 框架中,我們有了動態路由,只需要在名字前加上一個下划線 _ ,比如 _id.vue ,這樣就能輕松搞定,是不是很方便,這個咱們以后會講到,這里暫時就不深入了。

再比如我們在之前說到組件的時候,用到了這個圖,在入門頁面中,將我們的所有組件按需加載,但是如果我們的項目有多個布局需要怎么辦?比如我們商城首頁,和個人中心首頁有時候是不一樣的,當時如果你一定要保持一個風格也是可以的,但是感覺這么配置還是怪怪的。

不過在 Nuxt.js 框架中,我們有了新的變化,layouts對應目錄中的layouts文件夾,默認pages下的頁面走的都是 layouts/default.vue 布局方式,但是如果自己定義的話,也可以新增新的布局頁。其中<nuxt/>可以類似vue中slot插槽的概念,(如果對 slot 插槽不是很明白,可以看之前的文章 《二十║Vue基礎終篇:組件詳解+項目說明》的第四節),pages/**.vue中的內容會插在<nuxt/>內。
在咱們的第二個項目,基於 Nuxt 的個人博客里,我就新建了一個 blog.vue 的布局頁面,定制了一個博客布局:

<template> <div class="layout-default"> <cl-header></cl-header>//定制博客頁頭 <nuxt class="layout-main"/>//nuxt 插入內容 <cl-footer></cl-footer>//定制博客頁腳 <div class="layout-bg"></div> </div> </template> <script type="text/javascript"> import clHeader from "~/components/layout/header.vue"; import clFooter from "~/components/layout/footer.vue"; export default { data () { return {}; }, mounted () { this.$store.dispatch("initUser"); }, components: { clHeader, clFooter } }; </script>

這個就是 nuxt 的分層頁面布局。
由上邊的栗子可以看出來,nuxt 是通過封裝,將核心原理隱藏起來,重點實現我們的UI展示,這么做的目的就是更有經歷去做 SSR 服務端渲染了,大家還記得咱們在講 SSR 的時候么,需要很復雜的操作配合才能實現,所以,很好的底層封裝,是我們更好開發 SSR 框架的第一步。
二、Nuxt 的工作流程 —— 四步走
還是老規矩,一言不合就放圖系列

簡單來說,當你訪問一個基於Nuxt.js構建的頁面時,發生了的事情如下:
1、當用戶訪問應用程序, 如果 store 中定義了 nuxtServerInit action,Nuxt.js 將調用它更新 store。
2、將加載即將訪問頁面所依賴的任何中間件。Nuxt 首先從 nuxt.config.js 這個文件中,加載全局依賴的中間件,之后檢測每個相應頁面對應的布局文件 ,最后,檢測布局文件下子組件依賴的中間件。以上是中間件的加載順序。
3、如果要訪問的路由是一個動態路由, 且有一個相應的 validate() 方法路由的validate 方法,講進行路由校驗。
4、Nuxt.js 調用 asyncData() 和 fetch() 方法,在渲染頁面之前加載異步數據。asyncData() 方法用於異步獲取數據,並將 fetch 回來的數據,在服務端渲染到頁面。 用 fetch() 方法取回的將數據在渲染頁面之前填入store。
最后一步, 將所有數據渲染到頁面。
其中核心配置文件 nuxt.config.js 是比較重要的一個文件,整個系統的配置和插件都需要這個文件來執行,感覺就像是我們 vue-cli 腳手架里的 main.js 入口文件一樣,具體的配置以及使用咱們在以后的文章中再說明吧。上邊咱們說到了還是工作流程,那 nuxt 到底封裝了怎么的工作機制,才能很好的執行這個流程呢,重頭戲來了
三、面向源碼簡要分析 Nuxt 的工作機制
1、通過 .nuxt 文件來執行我們的工作流程
剛剛咱們說到了,nuxt.config.js 就像是一個 main.js 入口一樣,這個時候細心的你一定會發現,欸?為什么 nuxt 框架沒有 router 呢,那我們的路由該如何配置呢,不要着急,nuxt 已經給我們封裝了,我們無需把注意力放到這上邊,這個時候我們執行
npm run dev
會發現多了一個文件(因為昨天我們已經執行過了,現在就不需要執行了)。

這個是我們項目生成的臨時文件,我們項目運行時候配置的文件都是在這里,大家可以看到這里的路由文件,沒錯,這個就是系統自動給我們配置的路由文件,根據我們的 pages 文件夾路徑生成的,大家還可以看到,由app.js ,client.js 和 server.js 這兩個就是類似我們的 SSR 中配置的那個 server.js 入口文件,然后還有 middleware.js 中間件文件,其實這個時候我們大概能懂了,上邊我們說的工作流程,走的就是這個 臨時文件.nuxt 文件夾中的內容,但是這個文件夾是如何生成的呢,大家請往下看。
2、.nuxt 是如何產生的
本文主要研究nuxt的運行原理,分析它從接收一條nuxt指令,到完成指令背后所發生的一系列事情,在開始本文之前,請讀者務必親自體驗過nuxt.js的使用,並且具備一定的vue.js相關開發經驗。
通過查看nuxt.js工程目錄下的package.json文件,我們可以看到下列幾條指令:
"scripts": { "dev": "nuxt", // 開啟一個監聽3000端口的服務器,同時提供hot-reloading功能 "build": "nuxt build", //構建整個應用,壓縮合並JS和CSS文件(用於生產環境) "start": "nuxt start", // 開啟一個生產模式的服務器(必須先運行nuxt build命令) "generate": "nuxt generate" //構建整個應用,並為每一個路由生成一個靜態頁面(用於靜態服務器) }
咱們還從來沒有看過我們的依賴包哈,今天就來看看,打開我們的 node_modules 文件夾下的 nuxt工程文件夾 進入到到bin目錄,我們可以看到5個文件:

咱們就說一下 dev 是如何工作的,咱們先找到一個片段,發現基本是執行了以下幾個步驟:
try { nuxt = new Nuxt(options)//實例化一個 nuxt 類 builder = new Builder(nuxt) instance = { nuxt, builder } } 。。。。 return ( Promise.resolve() .then( () => oldInstance && oldInstance.builder ? oldInstance.builder.unwatch() : Promise.resolve() ) // Start build 開始執行 build() 方法 .then(() => builder.build()) // Close old nuxt after successful build .then( () => oldInstance && oldInstance.nuxt ? oldInstance.nuxt.close() : Promise.resolve() ) // Start listening 開啟監聽服務端口 .then(() => nuxt.listen(port, host)) // Pass new nuxt to watch chain .then(() => instance) // Handle errors .catch(err => onError(err, instance)) ) }

3、那什么是 nuxt() 類,它又是執行了什么樣的方法呢?
這個時候,我們繼續看源碼,進入到nuxt/lib目錄,我們可以看到如下的文件目錄結構:
├── app
├── builder
├── common
└── core
├── middleware
│ ├── index.js
│ ├── meta.js
│ ├── module.js
│ ├── muxt.js
│ └── render.js
└── index.js

大家看這個熟悉不熟悉?!沒錯,就是和我們上邊項目執行的時候生成的臨時文件是類似的,請和上邊的 .nuxt 臨時文件夾做對比。
網上有個很好的總結,如圖:

上圖中每一步都可以在具體的代碼中自行瀏覽。在用戶輸入指令並實例化了Nuxt()類以后,實例化出來的nuxt對象就會執行圖中打了綠色對勾的幾個方法:build(), render(), renderRoute(), renderAndGetWindow()以及generate()方法。
同時,Nuxt()類也提供了一個close()公有方法,用於關閉其所開啟的服務器。
4、build() 進行編譯,生成 .nuxt 臨時文件
然后就是執行的 build() 方法:

簡單來說,build()方法在判斷完運行條件后,會先初始化產出目錄 .nuxt ,然后通過不同目錄下的文件結構來生成一系列的配置,寫入模板文件后輸出到.nuxt目錄。接下來,則會根據不同的開發環境來調用不同的webpack 配置,運行不同的 webpack 構建方案。
5、render.js文件 打包輸出渲染
在 nuxt/lib/core 目錄下找到 render.js 文件,它包含着我們即將要分析的三個方法:render(), renderRoute(), renderAndGetWindow()。

通過這張圖片,我們可以知道nuxt對於處理“客戶端渲染”與“服務端渲染”的邏輯其實是非常清晰的。
首先,在render()方法在處理完一系列的路徑問題后,會調用renderRoute()方法,獲取響應所需內容並完成響應。
其中renderRoute()方法會判斷當前響應是否應執行服務端渲染。如果是,則調用vue提供的bundleRenderer()方法,把html內容渲染完畢以后再整體輸出;如果不是,則直接輸出一個<div></div>字符串,交由客戶端渲染。
// Call renderToString from the bundleRenderer and generate the HTML (will update the context as well) let APP = await this.bundleRenderer.renderToString(context) if (!context.nuxt.serverRendered) { APP = '<div id="__nuxt"></div>' }
最后,通過renderAndGetWindow()來檢查輸出的html是否存在問題,然后發出通知,表明html可用。
四、結語
好啦,今天因為時間的問題,暫時就說到這里吧,其實這里還有很多知識點沒有說到,那我們就在之后的項目中,慢慢給大家說吧,請持續關注喲,可以點個贊👍吧,真的是辛苦哈哈哈
