基於NUXT.JS搭建一款VUE版SSR前端框架(解決SPA應用的SEO優化優化問題)

2023-03-04 18:00:44

小仙男·言在前

關於框架:為了解決VUE的SPA單頁應用對SEO搜尋引擎優化不友好的問題,這幾天一直在調研各種SSR框架。比如doc.ssr-fc.com/ 和 fmfe.github.io/genesis-do 都是比較不錯,且有自己理念和想法的框架。但是對於公司來說技術規範差異太大,團隊學習成本比較高,思來想去,還是基於NUXT.JS自己搭建一套SSR框架慢慢完善吧。
關於本檔案:本檔案是從官網檔案中摘錄的一些重點內容,以及加入了自己的一些調整和對官網內容的理解和解釋。
關於官網:NUXT中文網 特別適合新手學習,檔案及案例十分清楚詳盡,可以說有手就行。但是,中文網的更新不及時,有些章節(比如fetch勾點中不能使用this)甚至存在明顯錯誤,所以有一定技術水平的寶子,建議直接檢視 NUXT英文官網 。

【一、框架概述】

1、框架介紹

  • SSR 技術(即伺服器端渲染技術),區別於原先純Vue框架的SPA應用(即單頁應用)。SPA應用只有一個index.html的入口檔案,頁面顯示的所有內容均靠使用者端JS進行渲染,對於搜尋引擎(SEO)優化來說,整個網站只有一個空頁面,十分不友好。而伺服器端渲染技術,是藉助node.js作為框架伺服器端,在初次存取一個頁面的時候,先在伺服器端預請求介面,並在伺服器端組裝完成的html頁面後,返回給使用者端呈現。
  • Nuxt.js是基於Vue框架的一款伺服器端渲染框架,提供了特有的框架結構和伺服器端渲染宣告週期。

2、開發環境

  • 本框架基於Node.js+Webpack+vue+Nuxt.js進行開發,提供ElementUI作為UI框架。開發前需全域性安裝Node.jswebpack開發環境。
  • 框架推薦Node.js版本為v16.15.0,最低版本不得低於12,推薦安裝nvmn等node版本管理工具。

3、分支要求

  • 遵循[前端團隊git倉庫及版本管理規範],即master分支只用於拉取框架程式碼,xxx_dev為開發分支,xxx_test為開發分支,xxx為生產分支。

3、關於本檔案

【二、啟動與部署】

# 安裝框架以來
$ npm install

# 啟動本地開發環境,預設埠號:3000
$ npm run dev

# 編譯並在生產環境啟動
$ npm run build
$ npm run start

# 將網站打包成靜態化頁面
$ npm run generate

【三、框架結構】

-- 框架根目錄
  -- .nuxt        Nuxt運營和編譯自動生成
  -- dist         執行Nuxt靜態化時生成
  -- api          全域性通用的Api請求函數(非Nuxt提供)
  -- assets       靜態資源目錄,存放全域性css、image等
  -- components   自定義元件目錄,此目錄下元件無需引入,按需使用即可
  -- layout       佈局檔案,參考https://www.nuxtjs.cn/guide/views
  -- middleware   中介軟體,類似於路由守衛
  -- modules      模組,用於設定全域性監聽等,參考https://www.nuxtjs.cn/guide/modules
  -- pages        頁面目錄,Nuxt會根據此目錄自動生成路由,參考https://www.nuxtjs.cn/guide/routing
  -- plugins      外掛目錄,自定義各種外掛,參考https://www.nuxtjs.cn/guide/plugins
   > global.js    (全域性變數與全域性方法)
   > plugin.js    (全域性引入第三方元件)
   > request.js   (全域性請求封裝)
   > filter.js    (全域性過濾器封裝)
   > util.js      (全域性工具函數封裝)
   > all.client.js(僅在使用者端執行外掛,暫時替代原app.vue)
     
  -- static       不需要webpack編譯的靜態檔案,一般存放ico等檔案
  -- store        Vue狀態樹,與原寫法有所不同,參考https://www.nuxtjs.cn/guide/vuex-store
  -- utils        工具類包 (非Nuxt提供)
  .editorconfig   
  .gitignore
  env.js          環境變數設定,分dev、test、pro三種環境
  nux.config.js   Nuxt的所有設定項,參考https://www.nuxtjs.cn/api/configuration-build
  package-lock.json
  package.json
  README.md       框架使用檔案
  ReleaseNote.md  版本更新說明

【四、生命週期】

-- Nuxt完整宣告週期
  【伺服器端渲染】
    -- 全域性
  nuxtServerInit    第一個:nuxt中第一個執行的生命週期
  RouteMiddleware   第二個:中介軟體,類似於原框架的路由導航守衛
    -- 元件
  validate          是用來校驗url引數符不符合
  asyncData         Nuxt專屬宣告週期,可用於資料請求,只有page可用,子元件內部不可用
  beforeCreate      Vue宣告週期,但是伺服器端會執行(不可用於資料請求,資料請求相關操作會在使用者端執行)
  created           Vue宣告週期,但是伺服器端會執行(同上)
  fetch             Nuxt專屬宣告週期,可用於資料請求, page和子元件都可用 
  
  【使用者端渲染】
    -- 全域性
  * `@/plugins/all.client.js` (並非Nuxt宣告週期,是隻在使用者端執行的外掛。此框架中用於暫時替代原框架中在App.vue中進行的全域性初始化操作。)
    -- 元件
  beforeCreate
  created
  beforeMount
  mounted
  ... (其他Vue後續宣告週期)
  

幾點說明:

  1. beforeCreate/created 是Vue的生命週期,但是會在伺服器端和使用者端各執行一次,但這兩個勾點,僅供瞭解,不能用於資料請求。
  2. asyncDatafetch都是Nuxt提供的宣告週期,都可用於資料請求。只是寫法略有不同(參考後續章節【五、資料請求】)。
  3. @/plugins/all.client.js 並非Nuxt宣告週期,是隻在使用者端執行的外掛。但是Nuxt框架去掉了app.vue,此外掛的宣告週期,近似於原來的app.vue,故暫時用於替代原框架中在App.vue中進行的全域性初始化操作(是否恰當暫時不知)。

【五、資料請求】

1. 資料請求勾點

1.1 勾點相關說明

  • asyncDatafetch都是Nuxt提供的宣告週期,都可用於資料請求,都會在伺服器端預請求資料進行組裝;
  • asyncData只能在pages級別的頁面中呼叫,在子元件內部不能呼叫;fetch則可以同時在頁面和子元件中呼叫;
  • 官方建議資料請求均採用asyncData,但為了保持與老框架寫法的一致,本框架暫時建議採用fetch(後果未知)
  • fetch請求相比於asyncData的已知缺陷有:
    • ① 資料請求較慢,本框架Demo,從index頁進入Detail頁,當使用fetch請求時,可明顯看到瀏覽器索引標籤的title出現一瞬間undefined
  • 儘管beforeCreate/created也可以在伺服器端渲染,但是這兩個勾點的資料請求操作只會在使用者端執行,非特殊情況,切勿用於頁面初始化。

1.2 asyncData

  • asyncData 中不能存取this,但是可以在第一引數中,拿到context上下文,使用context.app存取Vue根範例;
    • context上下文還包含store、route、params、query等資料,詳見context上下文
  • asyncData中無法拿到元件範例,不能存取元件範例中的data method等方法。
  • 詳細介紹:asyncData
  • 【請求範例】
// ① 使用return返回的物件,將直接初始化到元件`data`中
async asyncData({app, params}) {
    const { code, data } = await app.$get('/policy/findById/'+params.id)
    return {detail: data}
},
// ② return一個Promise,將在Promise執行完成後,將資料初始化到元件`data`中
asyncData({app, params}) {
    return app.$get('/policy/findById/'+params.id).then(res => {
      return {detail: data}
    })
},
// ③ 第二個引數為callback回撥函數,可直接傳入資料,初始化到元件`data`中
asyncData({app, params}, callback) {
    app.$get('/policy/findById/'+params.id).then(res => {
      callback(null, {detail: data}) 
    })
},

1.3 fetch

  • fetch 分兩種情況(新版本後支援第二種情況):
    • ① 第一個引數接受context上下文,則與asyncData一樣,不能存取this和元件範例; (這種情況,也不支援像asyncData一樣通過return或者回撥函數修改data內容)
    • ② 不接受任何引數時,則可以正常存取this。(可以近似的看成created的用法,區別是 必須要使用await 或者return一個primary)
  • 詳細介紹:fetch英文檔案 (中文檔案嚴重延遲,存在錯誤)
  • 【請求範例】
// ① 使用return返回一個Promise
fetch() {
    return this.getDetail()
},
// ②  使用await/async
async fetch() {
    await this.getDetail()
},
methods: {
    // ① 使用await編寫methods方法
    async getDetail(id){
        const { code, data } = await this.$get('/policy/findById/'+this.$route.params.id)
        this.detail = data
    }
    // ② 使用return Promise編寫methods方法
    getDetail(id){
        return this.$get('/policy/findById/'+this.$route.params.id).then(resw => {
          this.detail = res.data
        })
    }
}

2. 資料請求方式

2.1 【框架推薦】 使用vue範例直接呼叫

  • 本框架會將$request/$get/$post掛在到vue根範例,建議直接只用this或上下文context.app呼叫
  • 【請求範例】
// 以this呼叫為例,如果是在`asyncData`中,需要使用上下文`context.app`呼叫
// ① get
this.$get('/policy/findById/'+this.$route.params.id)
// ②  post
this.$post('/policy/findAll/',{page:1,size:10,params:{}})
// ③  request
this.$request({
    url: '/policy/findAll/',
    method: 'post',
    data: {page:1,size:10,params:{}}
})

2.2 相容老框架的api分離式呼叫

  • 本框架推薦使用五 2.1的方式呼叫,但是也相容了老框架的api分離式呼叫,用於提取可複用的公共請求
  • 公共請求的api檔案,統一放在@/api/*.js管理。
  • 【請求範例】
/**
 * @/api/index.js
 */ 
import request from '@/utils/request'
export function getPageList(data) {
    return request.post('/policy/findAll', data)
}
/**
 * @/pages/index.vue
 */ 
import { getPageList } from "@/api/index.js"
export default {
    fetch() {
        return this.getPageList(this.pageDto)
    },
    methods: {
        getPageList(pageDto) {
            return getPageList(pageDto).then(res => {
              this.pageList = res.data.result
            })
        }
    },
}

3. 其他注意事項

  • 原則上,所有初始化渲染資料的請求,都要在伺服器端渲染函數(asyncDatafetch)中進行,極個別無法在伺服器端渲染的請求,可以在Vue的生命週期(createdmounted)中初始化;
  • 伺服器端渲染的生命週期(即asyncData/fetch),不能使用任何瀏覽器專屬的對像(如DOM物件),也就是documentwindow,以及window的各種物件和方法,例如setTimeoutsetIntervallocalStoragesessionStorage等;
    有上述需求的初始化邏輯,可以放到createdmounted中初始化。

【六、其他規範與Q&A】

1. 關於pages

  • 本框架路由採用約定式路由,即不再使用route.js進行路由宣告,而是由框架根據pages目錄自動生成路由,詳見路由
  • 資料夾或者檔案,如果以_開頭,表示此為動態路由,可以傳入不同引數,在元件內容,可以使用上下文或者this.$router取到路由引數;
    • 例如: /pages/news/detail/_id.vue/pages/news/detail/_id/index.vue
    • 存取: http://domain.com/pages/news/detail/12345 (上述兩種寫法均為這一路徑)

【注意】

  • ① 使用_id.vue的寫法,表示id為可選引數,即可以通過http://domain.com/pages/news/detail存取。如果要對id進行限制或驗證,可以在元件內使用validate()驗證;
  • ② 使用/_id/index.vue的寫法,表示id為必選引數,存取http://domain.com/pages/news/detail會報404。如果只要求id必填,而沒有其他格式限制,可以使用此方式。
  • validate()驗證範例
// return true表示驗證通過,return false表示驗證失敗 404
validate({ params }) {
    return /^\d+$/.test(params.id)
},

2. 關於plugins

  • 用於自定義框架所需的各種外掛,宣告外掛後在nuxt.config.js中引入外掛即可,類似於原框架main.js相關功能。詳見外掛
  • 框架已有的外掛包(具體使用者參照各外掛的頂部註釋):
    • plugin.js用於全域性引入各種npm包;
    • global.js用於宣告全域性變數與全域性方法;
    • request.js實現了全域性請求封裝(對應@/utils/request.js);
    • filter.js實現了全域性請求封裝(對應@/utils/filter.js);
    • util.js實現了全域性請求封裝(對應@/utils/util.js);
    • all.client.js只在使用者端引入,用於替代原框架中app.vue中的各種初始化操作;
  • 其他外掛可根據需要自行定義,*.js表示伺服器端使用者端均匯入;*.client.js表示僅在使用者端匯入;*.server.js表示只在伺服器端匯入;

3. 關於layout

  • 用於定義框架中的各種佈局檔案,可根據需要自行定義,詳見佈局與檢視
  • 預設檢視為default.vue,預設所有頁面都將呼叫;error.vue是錯誤檢視,當頁面出現問題時,自動呼叫;
  • 其他檢視,可根據需要自行定義,並在元件內部宣告參照。
  • 【元件呼叫範例】
export default {
    // 需要呼叫的檢視名稱,不寫預設呼叫default.vue
    layout: 'onlyBody',
    data(){
      return {}
    }
}

4. 關於components

  • 用於定義框架中的各種自定義元件,可根據需要自行定義。
  • 自定義元件中的資料,一般應從頁面傳入,如果需要再元件內部獲取資料,應該使用fetch(子元件中不支援asyncData)。
  • components中宣告的各種元件,在使用時,無需import匯入。直接使用元件名按需呼叫即可。
  • 【使用範例】
<template>
  <div>
    // Header元件
    <Header />
  </div>
</template>

5. 關於store

  • store資料夾為Nuxt提供用於定義Vuex狀態樹的資料夾,詳細檔案參照:Vuex狀態樹
  • 此資料夾下面的xxx.js,分別表示一個模組,例如index.js對應$store.state.xxx,而user.js對應$store.state.user.xxx
  • 本框架中store中模組的定義與普通Vue框架大體相同,只是Nuxt框架會自動參照Vuex並載入到構建設定重,無需我們自己new Vuex()
  • 【使用範例】
/**
 * 【注意區別】
 * state mutations action不再是包裹在一個物件中,並在new Vuex()的時候傳入。 而是分別作為單獨模組使用export匯出即可。
 */
export const state = () => ({
    counter: 0
})
export const mutations = {
    increment(state) {
        state.counter++
    }
}

6. 關於middleware

  • middleware是框架中用於宣告中介軟體的資料夾,宣告後在nuxt.config.js中設定中介軟體即可,詳細檔案參照:中介軟體
  • @/middleware/router.js為已經升級宣告好的路由守衛中介軟體,可替代原框架中router.beforeEach中的路由守衛功能;

7. 關於modules

  • 用於自定義模組的資料夾,可以在模組中對Nuxt啟動部署的各種宣告週期設定監聽,詳細檔案參照:模組
  • @/modules/generator.ts實現了一個對靜態化結束generate:done時進行監聽並處理的範例。
const generator: any = function () {
    this.nuxt.hook('generate:done', (context: any) => {
        // TODO samething
    })
}
export default generator
  • 類似this.nuxt.hook('generate:done',() => {})的Nuxt框架hooks還有很多,例如:readyerrorrender:beforebuild:compile 等等……詳細參見INTERNALS

8. 其他Q&A

1)每個頁面,必須使用head設定title,必要時還需在詳情頁設定description。(!!!切記!!!)

export default {
    head() {
        return {
            // title必須設定!!! 列表可以直接寫「xxx列表」,詳情頁等有不同標題的,要用新聞標題、商品標題等作為title字首。
            title: this.detail.title + '_新聞詳情',
            meta: [
                // 詳情頁,需要設定不同的description。 this.$getDesc 為全域性封裝的從富文字中擷取100字元的description
                { hid: 'description', name: 'description', content: this.$getDesc(this.detail.details) },
            ],
        }
    }
}

2)pages目錄中的層級結構,務必按照功能梳理清楚,比如「news(新聞)」的列表、詳情都要在一個資料夾中。

(!!!目錄結構一旦確定,原則上不可再調整!!!)

3)框架中的其他重要檔案之【CSS篇】!!

  • 框架各種css檔案,位於@/assets/css/中。框架推薦使用scss語言,使用"sass": "~1.32.13"進行編譯;
  • common.scss 為全域性公共CSS,請將全域性樣式表宣告於此。或自行定義CSS檔案,並在此檔案中import匯入;
  • font.scss 用於定義本框架各種字型、圖示庫等;
  • variables.scss 宣告了框架的各種全域性Scss變數,可以在所有頁面使用。
    • 注意:全域性主題色,請用$mainColor表示,不要在各自檔案中自行宣告!
  • element-variables.scss 是ElementUI的主題宣告檔案,如需全域性調整ElementUI的配色,請在此調整;

4)(未完待續…)其他任何框架問題,詳詢小仙男