分享一個Nuxt.js專案實戰經歷

2021-04-26 18:01:35
1.  為什麼使用Nuxt.js?
2.  nuxt專案搭建
3.  非同步資料載入
4.  token的處理邏輯
5.  mysql接入
6.  session的持久化處理

1. 為什麼使用Nuxt.js?

    在瞭解Nuxt.js之前,我們先來了解一下前端頁面發展過程。Web網站從誕生至今的二十多年時間你,前端資源分配比例依次經歷了  少量HTML + 少量CSS、大量HTML + 大量CSS + 少量JavaScript、大量HTML + 大量CSS + 大量JavaScript、少量HTML + 大量CSS + 大量JavaScript。並且all-in-js的趨勢越來越明顯。在React\Vue\Angular等開發框架的支援下,HTML和CSS都可以有js編寫,經編譯後產生CSS程式碼和用於瀏覽器環境下渲染的HTML的JavaScript程式碼。由於JavaScript的起步晚於HTML和CSS,最初的Web頁面都是靜態的HTML,後來才逐步加入CSS和JavaScript。SEO爬蟲軟體通過分析網站的HTML檔案抓取資訊,但並不能支援抓取JavaScript指令碼,即便是在React和Vue等瀏覽器端渲染方案CSR大行其道的今天,也僅有google爬蟲能初步簡單支援SPA,這還需要在編寫程式碼時做特殊處理。傳統的伺服器端渲染SSR方案是藉助伺服器端程式語言或者框架搭配的HTML模板引擎將資料編譯為HTML檔案,比如適用於Java的FreeMaker和適用於Node.js的Pug等。模板引擎可以被理解為一個功能強大的字串處理工廠。

  Nuxt.js是當下常用的的伺服器端渲染方案SSR。SSR相對於CSR最主要的優勢在於其支援SEO(搜尋引擎優化)和首屏時間短,可以在伺服器端把主要內容獲取完成並生成HTML傳送到瀏覽器端,這個過程中可以提供更加詳細的資訊供SEO抓取。而常用是SPA方案則通常是返回一個僅有及時行程式碼的html檔案和一大堆JavaScript檔案到瀏覽器端,HTML檔案中幾乎不包含頁面重要資訊,頁面內容主要由JavaScript檔案中的邏輯產生並新增到dom中。在首屏渲染方面,現在的很多大中型專案在首頁就會請求大量資料,SSR專案中可以把首屏需要的關鍵資料請求放在伺服器端完成,大多數情況下伺服器端網路設施是明顯強於C端瀏覽器的,這樣能節省不少請求時間,使用者看到的頁面,是一個已經帶有關鍵資料的可用頁面。而SPA頁面往往是在頁面載入完成之後再去傳送網路請求,白屏時間會更長,使用者體驗不佳。

   但是並不是所有的前端專案都推薦使用SSR,由於Nuxt.js和Next.js等框架的支援能力目前還不足,SSR專案中開發前端還是有很多地方會有掣肘,比如生命週期和常用勾點不如CSR SPA專案那樣豐富。但是在對SEO和首屏渲染時間具有強需求的專案中,如企業官網、產品列表等專案,我們希望頁面能儘量被搜尋引擎抓取到並且能儘可能減少使用者的白屏等待時間,就可以採用SSR框架進行開發。

  Nuxt.js 是一個基於 Vue.js 的輕量級應用框架,可用來建立伺服器端渲染 應用,也可充當靜態站點引擎生成靜態站點應用,具有優雅的程式碼結構分層和熱載入等特性。Nuxt.js實現了開箱即用的勸勉支援,利用 SSR,Node.js 伺服器將基於 Vue 的元件渲染成 HTML 並傳輸到使用者端,而不是純 javascript。與傳統的 Vue SPA 相比,使用 SSR 將帶來巨大的 SEO 提升、更好的使用者體驗和更多的機會。

2. Nuxt.js專案搭建

   Nuxt.js團隊建立了腳手架工具create-nuxt-app來搭建Nuxt.js專案,可採用如下命令建立:

`px create-nuxt-app <專案名>`

或者

`yarn create nuxt-app <專案名>`
建立中會讓人選擇整合的伺服器框架(如Express\Koa\Hapi)等,還可以選擇需要的UI框架、測試框架、開發模式等等。
我們也可以選擇使用Express-template建立專案
`vue init nuxt-community/express-template nuxt-express`
當然也可以自己手動建立nuxt專案,從package.json安裝nuxt依賴開始自己動手。
假如我們採用express-template建立專案,得到的專案目錄如下:

|—— server        Node.js後臺程式碼存放目錄
    |—— routes                api介面程式碼檔案目錄
      |—— users.js    api介面程式碼檔案
    |—— index.js              核心檔案,介面層通用設定
|—— assets                     用於放置未編譯的靜態資源如 LESS、SASS
    |—— css                        
    |—— img
|—— components          普通元件目錄
    |—— Footer.vue
|—— layouts                   佈局檔案目錄
    |—— default.vue        預設佈局(必須),也可以自己編寫佈局,在page中指定使用該佈局檔案,沒有指定時,預設使用default佈局
    |—— error.vue            頁面出錯時會預設展示的佈局
|—— middleware           用於存放應用的中介軟體
|—— pages                      路由元件目錄,每一個檔案都將作為一個單獨的頁面呈現
   |—— _id.vue                路由元件檔案(編譯後會自動生成路由/:id)
    |—— index.vue            路由元件檔案(對應路由/)
|—— plugins                    公共函數目錄
    |—— axios.js
|—— static                       靜態檔案目錄,該目錄不會被webpack編譯
|—— store                       用於組織應用的vuex狀態樹
|—— .eslintrc.js       
|—— .gitignore
|—— nuxt.config.js         用於自定義設定,可以覆蓋預設設定
|—— package.json
|—— README.md
    

這裡至關重要的就是nuxt.config.js檔案,可以在該檔案中個性化設定nuxt專案的部署方式、生成html的設定項(如title\head\css\plugins)、打包設定選項、路由設定、環境變數設定等等豐富的選項。

3. 非同步資料載入

nuxt.js封裝了$http$axios元件,可以讓我們在前端頁面中,像普通的SPA應用一樣方便地使用這些網路請求模組在瀏覽器中向後臺請求資料。

前面我們提到nuxt.js可以應用於SEO,它的特別之處就在於擴充套件了Vue.js,增加了一個叫asyncData 的方法,使得我們可以在設定元件的資料之前能非同步獲取或處理資料。asyncData方法會在元件(限於頁面元件)每次載入之前被呼叫。它可以在伺服器端或路由更新之前被呼叫。在這個方法被呼叫的時候,方法引數中預設提供了$http和$axios元件, 可以利用 asyncData方法來獲取資料,Nuxt.js 會將 asyncData 返回的資料融合元件 data 方法返回的資料一併返回給當前元件。這也就決定了我們可以在伺服器端完成首屏資料請求,並將資料填充到html中,再返回給瀏覽器。

nuxt專案可以僅僅作為一箇中間層提供介面轉發和伺服器端渲染,也可以涵蓋一個完整的後臺提供服務。在官網專案中,我們的後臺服務和nuxt應用是在同一個專案中的,首先我們在nuxt.config.js檔案中指明瞭後臺介面的請求地址,所有/api開頭的介面都將向根目錄下server目錄中請求:

/*
** Server Middleware
*/
serverMiddleware: {
'/api': '~/server'
},

我們改造了上述server目錄,使其可以成為一個獨立的應用提供對內和對外的服務,整個服務就是一個express應用,改造後的server目錄結構也更加清晰簡單:

|—— server        Node.js後臺程式碼存放目錄
    |—— controllers         控制器目錄,介面的邏輯處理等
      |—— users.js    user相關的控制器,與資料庫表相關的處理時,會參照相應的models
   |—— middleware       介面中介軟體,可以做一些許可權、cookie等方面的通用處理
      |—— check.js    
   |—— models                資料層目錄
      |—— user.js      user有關的表的讀取處理等邏輯,參照db.js
   |—— mysql                  資料庫操作目錄
      |—— db.js        資料庫連線通用設定
       |—— sql.js        sql語句表,我們把所有的sql語句都放到這裡,方便我們統一稽核
    |—— routes                 api介面路由目錄
        |—— index.js    路由分配的中介軟體
      |—— users.js    user相關的路由管理,參照user控制器和介面中介軟體
    |—— index.js              核心檔案,介面層通用設定

4. token的處理邏輯

在系統中有註冊登入等功能模組時,就需要用到token的管理。在nuxt專案後臺中,同express應用一樣,我們可以使用express-jwt來實現。

express-jwt有多種加密方案,我們這裡採用rsa方案,先生成公鑰和私鑰儲存在根目錄。我們一般在使用者登入成功之後生成token,並在返回報文中返回給瀏覽器,由瀏覽器儲存並在下一個介面新增到headers中,express-jwt提供了現成的介面生成token:

// 注意預設情況 Token 必須以 Bearer+空格 開頭
const privateKey = fs.readFileSync('rsa_private_key.pem')
const token = 'Bearer ' + jwt.sign(
  {
    user_id: user.id,
    isLogin: true
  },
  privateKey,
  {
    algorithm: 'RS256'
  }
)
return res.send({
  code: constants.SUCCESS,
  token,
  user,
  msg: '登入成功'
})

token的校驗則是在介面通用邏輯server/index.js中處理,在介面請求中介軟體中使用express-jwt,會自行採用公鑰校驗token,我們可以設定不經過Token解析的介面路徑。如果token校驗通過,則介面進入下一環節,如果沒有通過,則會返回UnauthorizedError的錯誤並被捕捉處理。

import fs from 'fs'
import express from 'express'
import expressJwt from 'express-jwt'
const createError = require('http-errors')

// Create express instance
const app = express()
const publicKey = fs.readFileSync('rsa_public_key.pem')
app.use(expressJwt({
  secret: publicKey, // 簽名的金鑰
  algorithms: ['RS256'] // 設定演演算法(官方檔案上沒有寫這個,但是不設定的話會報錯)
}).unless({
  path: ['/api/users/login', '/api/users/getcaptcha', '/api/test', /\/api\/portal/i] // 不經過 Token 解析的路徑
}))
// error handler
app.use(function (err, req, res, next) {
  if (err.name === 'UnauthorizedError') {
    //  可以根據自己的業務邏輯來處理
    return res.send({
      code: '401',
      msg: '您還未登入,請先登入'
    })
  }
  // set locals, only providing error in development
  res.locals.message = err.message
  res.locals.error = req.app.get('env') === 'development' ? err : {}
  // render the error page
  res.status(err.status || 500)
  res.render('error')
})
module.exports = app

5. mysql的接入

不妨先了解一下config-lite這款小工具。它預設讀取根目錄下config資料夾下的檔案,根據當前的系統環境development\production一次降級查詢對應名稱的js\json\node\yml\yaml等檔案設定,並和default設定合併,產生當前環境下最終組態檔。我們可以通過它讀取當前設定,而不用在切換環境變數時更改我們使用的設定。

我們可以使用常用的各種型別的資料庫,express應用服務架構均由良好的接入能力。在研究院入口網站中我們採用常用的mysql資料庫,這裡需要使用到express接入mysql的連線工具mysql npm包。首先在config\development.js中我們定義資料庫連線相關設定

module.exports = {
  mysql: {
    // 測試伺服器
    host: '127.0.0.1',
    port: '3306',
    user: 'your-database-username',
    password: 'your-password',
    database: 'your-database-name',
    waitForConnections: true,
    connectionLimit: 50,
    queueLimit: 0
  }
}

在server\mysql\db.js中,我們定義資料庫的接入方式。資料庫的連線可以採用單個請求連線connection,也可以採用連線池pool,還有poolcluster等方案,具體的api可以在github中mysql包中檢視。我們這裡範例採用普通連線池,mysql npm工具會在連線完成之後自動關閉連線池。

import mysql from 'mysql'
const config = require('config-lite')({
  filename: process.env.NODE_ENV,
  config_basedir: __dirname,
  config_dir: 'config'
})
const pool = mysql.createPool(config.mysql)
export default pool

在models中我們就可以參照pool來進行資料庫相關操作,如userModels,pool.query提供了資料庫連線池的操作方法,方法第一個引數是一條sql語句

import pool from '../mysql/db'
import { UserSql } from '../mysql/sql'

findOne (param) {
    return new Promise(function (resolve, reject) {
      pool.query(UserSql.findOne, [param.username], function (error, results, fields) {
        if (error) {
          console.error(error)
          reject(error)
        }
        resolve(results[0])
      })
    })
}

6. session的持久化處理

對談我們同樣需要用到express的小工具express-session,在config\default.js中我們需要設定session的引數:

module.exports = {
  port: parseInt(process.env.PORT, 10) || 8001,
  session: {
    name: 'aaa',
    secret: 'bbb',
    id: '',
    captcha: '',
    cookie: {
      httpOnly: true,
      secure: false,
      maxAge: 365 * 24 * 60 * 60 * 1000
    }
  }
}

設定引數中宣告了cookie的相關引數、token的生成金鑰等相關引數。在server\index.js檔案中,我們引入session中介軟體

import session from 'express-session'
app.use(session({
  name: config.session.name,
  secret: config.session.secret,
  resave: false,
  captcha: config.session.captcha,
  saveUninitialized: false,
  cookie: config.session.cookie
}))

但是這樣並沒有實現session持久化儲存,在開發過程中重新啟動專案後發生session丟失。express有針對session在mysql資料庫中持久化儲存的小工具,我們需要在在server\mysql\db.js中初始化sessionStore:

import mysql from 'mysql'
const config = require('config-lite')({
  filename: process.env.NODE_ENV,
  config_basedir: __dirname,
  config_dir: 'config'
})
const session = require('express-session')
const MySQLStore = require('express-mysql-session')(session)
const pool = mysql.createPool(config.mysql)
export const sessionStore = new MySQLStore({}/* session store options */, pool)
export default pool

在server\index.js檔案中,判斷介面session時,加入持久化設定,這樣會自動在mysql資料庫中新增一張sessions表,儲存session_id、expires、data等資訊。我們可以在必要的時候對持久化的session做destroy clear等操作,也可以將持久化的session reload到記憶體中。

import { sessionStore } from './mysql/db'
import session from 'express-session'
app.use(session({
  name: config.session.name,
  secret: config.session.secret,
  resave: false,
  captcha: config.session.captcha,
  saveUninitialized: false,
  cookie: config.session.cookie,
  store: sessionStore
}))

以上就是第一次用nuxt.js做專案的一點小小的總結,歡迎大家批評指正