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

小仙男·言在前

關於框架:爲了解決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)(未完待續…)其他任何框架問題,詳詢小仙男

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章