前段時間剛好公司有專案使用了Nuxt.js來搭建,而剛好在公司內部做了個分享,稍微再整理一下發出來。本文比較適合初用Nuxt.js的同學,主要講下搭建過程中做的一些配置。建議初次使用Nuxt.js的同學先過一遍官方文件,再回頭看下我這篇文章。
一、為什麼要用Nuxt.js
原因其實不用多說,就是利用Nuxt.js的服務端渲染能力來解決Vue專案的SEO問題。
二、Nuxt.js和純Vue專案的簡單對比
1. build後目標產物不同
vue: dist
nuxt: .nuxt
2. 網頁渲染流程
vue: 客戶端渲染,先下載js後,通過ajax來渲染頁面;
nuxt: 服務端渲染,可以做到服務端拼接好html後直接返回,首屏可以做到無需發起ajax請求;
3. 部署流程
vue: 只需部署dist目錄到伺服器,沒有服務端,需要用nginx等做Web伺服器;
nuxt: 需要部署幾乎所有檔案到伺服器(除node_modules,.git),自帶服務端,需要pm2管理(部署時需要reload pm2),若要求用域名,則需要nginx做代理。
4. 專案入口
vue: /src/main.js
,在main.js可以做一些全域性註冊的初始化工作;
nuxt: 沒有main.js入口檔案,專案初始化的操作需要通過nuxt.config.js
進行配置指定。
三、從零搭建一個Nuxt.js專案並配置
新建一個專案
直接使用腳手架進行安裝:
npx create-nuxt-app <專案名>
複製程式碼
大概選上面這些選項。
值得一說的是,關於Choose custom server framework
(選擇服務端框架),可以根據你的業務情況選擇一個服務端框架,常見的就是Express、Koa,預設是None,即Nuxt預設伺服器,我這裡選了Express
。
- 選擇預設的Nuxt伺服器,不會生成
server
資料夾,所有服務端渲染的操作都是Nuxt幫你完成,無需關心服務端的細節,開發體驗更接近Vue專案,缺點是無法做一些服務端定製化的操作。 - 選擇其他的服務端框架,比如
Express
,會生成server
資料夾,幫你搭建一個基本的Node服務端環境,可以在裡面做一些node端的操作。比如我公司業務需要(解析protobuf)使用了Express
,對真正的服務端api做一層轉發,在node端解析protobuf後,返回json資料給客戶端。
還有Choose Nuxt.js modules
(選擇nuxt.js的模組),可以選axios
和PWA
,如果選了axios,則會幫你在nuxt例項下注冊$axios
,讓你可以在.vue檔案中直接this.$axios
發起請求。
開啟eslint檢查
在nuxt.config.js
的build屬性下新增:
build: {
extend (config, ctx) {
// Run ESLint on save
if (ctx.isDev && ctx.isClient) {
config.module.rules.push({
enforce: 'pre',
test: /\.(js|vue)$/,
loader: 'eslint-loader',
exclude: /(node_modules)/
})
}
}
}
複製程式碼
這樣開發時儲存檔案就可以檢查語法了。nuxt預設使用的規則是@nuxtjs(底層來自eslint-config-standard),規則配置在/.eslintrc.js
:
module.exports = {
root: true,
env: {
browser: true,
node: true
},
parserOptions: {
parser: 'babel-eslint'
},
extends: [
'@nuxtjs', // 該規則對應這個依賴: @nuxtjs/eslint-config
'plugin:nuxt/recommended'
],
// add your custom rules here
rules: {
'nuxt/no-cjs-in-config': 'off'
}
}
複製程式碼
如果不習慣用standard
規則的團隊可以將@nuxtjs
改成其他的。
使用dotenv和@nuxtjs/dotenv統一管理環境變數
在node端,我們喜歡使用dotenv
來管理專案中的環境變數,把所有環境變數都放在根目錄下的.env
中。
- 安裝:
npm i dotenv
複製程式碼
- 使用:
- 在根目錄下新建一個
.env
檔案,並寫上需要管理的環境變數,比如服務端地址APIHOST
:
APIHOST=http://your_server.com/api
複製程式碼
- 在
/server/index.js
中使用(該檔案是選Express服務端框架自動生成的):
require('dotenv').config()
// 通過process.env即可使用
console.log(process.env.APIHOST) // http://your_server.com/api
複製程式碼
此時我們只是讓服務端可以使用.env
的檔案而已,Nuxt客戶端並不能使用.env
,按Nuxt.js文件所說,可以將客戶端的環境變數放置在nuxt.config.js
中:
module.exports = {
env: {
baseUrl: process.env.BASE_URL || 'http://localhost:3000'
}
}
複製程式碼
但如果node端和客戶端需要使用同一個環境變數時(後面講到API鑑權時會使用同一個SECRET變數),就需要同時在nuxt.config.js
和.env
維護這個欄位,比較麻煩,我們更希望環境變數只需要在一個地方維護,所以為了解決這個問題,我找到了@nuxtjs/dotenv
這個依賴,它使得nuxt的客戶端也可以直接使用.env
,達到了我們的預期。
- 安裝:
npm i @nuxtjs/dotenv
複製程式碼
客戶端也是通過process.env.XXX
來使用,不再舉例啦。
這樣,我們通過dotenv
和@nuxtjs/dotenv
這兩個包,就可以統一管理開發環境中的變數啦。
另外,@nuxtjs/dotenv
允許打包時指定其他的env檔案。比如,開發時我們使用的是.env
,但我們打包的線上版本想用其他的環境變數,此時可以指定build時用另一份檔案如/.env.prod
,只需在nuxt.config.js
指定:
module.exports = {
modules: [
['@nuxtjs/dotenv', { filename: '.env.prod' }] // 指定打包時使用的dotenv
],
}
複製程式碼
@nuxtjs/toast模組
toast可以說是很常用的功能,一般的UI框架都會有這個功能。但如果你的站點沒有使用UI框架,而alert又太醜,不妨引入該模組:
npm install @nuxtjs/toast
複製程式碼
然後在nuxt.config.js
中引入
module.exports = {
modules: [
'@nuxtjs/toast',
['@nuxtjs/dotenv', { filename: '.env.prod' }] // 指定打包時使用的dotenv
],
toast: {// toast模組的配置
position: 'top-center',
duration: 2000
}
}
複製程式碼
這樣,nuxt就會在全域性註冊$toast
方法供你使用,非常方便:
this.$toast.error('伺服器開小差啦~~')
this.$toast.error('請求成功~~')
複製程式碼
API鑑權
對於某些敏感的服務,我們可能需要對API進行鑑權,防止被人輕易盜用我們node端的API,因此我們需要做一個API的鑑權機制。常見的方案有jwt,可以參考一下阮老師的介紹:《JSON Web Token 入門教程》。如果場景比較簡單,可以自行設計一下,這裡提供一個思路:
- 客戶端和node端在環境變數中宣告一個祕鑰:SECRET=xxxx,注意這個是保密的;
- 客戶端發起請求時,將當前時間戳(timestamp)和
SECRET
通過某種演算法,生成一個signature
,請求時帶上timestamp
和signature
; - node接收到請求,獲得
timestamp
和signature
,將timestamp
和祕鑰用同樣的演算法再生成一次簽名_signature
- 對比客戶端請求的
signature
和node用同樣的演算法生成的_signature
,如果一致就表示通過,否則鑑權失敗。
具體的步驟:
客戶端對axios進行一層封裝:
import axios from 'axios'
import sha256 from 'crypto-js/sha256'
import Base64 from 'crypto-js/enc-base64'
// 加密演算法,需安裝crypto-js
function crypto (str) {
const _sign = sha256(str)
return encodeURIComponent(Base64.stringify(_sign))
}
const SECRET = process.env.SECRET
const options = {
headers: { 'X-Requested-With': 'XMLHttpRequest' },
timeout: 30000,
baseURL: '/api'
}
// The server-side needs a full url to works
if (process.server) {
options.baseURL = `http://${process.env.HOST || 'localhost'}:${process.env.PORT || 3000}/api`
options.withCredentials = true
}
const instance = axios.create(options)
// 對axios的每一個請求都做一個處理,攜帶上簽名和timestamp
instance.interceptors.request.use(
config => {
const timestamp = new Date().getTime()
const param = `timestamp=${timestamp}&secret=${SECRET}`
const sign = crypto(param)
config.params = Object.assign({}, config.params, { timestamp, sign })
return config
}
)
export default instance
複製程式碼
接著,在server端寫一個鑑權的中介軟體,/server/middleware/verify.js
:
const sha256 = require('crypto-js/sha256')
const Base64 = require('crypto-js/enc-base64')
function crypto (str) {
const _sign = sha256(str)
return encodeURIComponent(Base64.stringify(_sign))
}
// 使用和客戶端相同的一個祕鑰
const SECRET = process.env.SECRET
function verifyMiddleware (req, res, next) {
const { sign, timestamp } = req.query
// 加密演算法與請求時的一致
const _sign = crypto(`timestamp=${timestamp}&secret=${SECRET}`)
if (_sign === sign) {
next()
} else {
res.status(401).send({
message: 'invalid token'
})
}
}
module.exports = { verifyMiddleware }
複製程式碼
最後,在需要鑑權的路由中引用這個中介軟體, /server/index.js
:
const { Router } = require('express')
const { verifyMiddleware } = require('../middleware/verify.js')
const router = Router()
// 在需要鑑權的路由加上
router.get('/test', verifyMiddleware, function (req, res, next) {
res.json({name: 'test'})
})
複製程式碼
靜態檔案的處理
根目錄下有個/static
資料夾,我們希望這裡面的檔案可以直接通過url訪問,需要在/server/index.js
中加入一句:
const express = require('express')
const app = express()
app.use('/static', express.static('static'))
複製程式碼
四、Nuxt開發相關
生命週期
Nuxt擴充套件了Vue的生命週期,大概如下:
export default {
middleware () {}, //服務端
validate () {}, // 服務端
asyncData () {}, //服務端
fetch () {}, // store資料載入
beforeCreate () { // 服務端和客戶端都會執行},
created () { // 服務端和客戶端都會執行 },
beforeMount () {},
mounted () {} // 客戶端
}
複製程式碼
asyncData
該方法是Nuxt最大的一個賣點,服務端渲染的能力就在這裡,首次渲染時務必使用該方法。 asyncData會傳進一個context引數,通過該引數可以獲得一些資訊,如:
export default {
asyncData (ctx) {
ctx.app // 根例項
ctx.route // 路由例項
ctx.params //路由引數
ctx.query // 路由問號後面的引數
ctx.error // 錯誤處理方法
}
}
複製程式碼
渲染出錯和ajax請求出錯的處理
- asyncData渲染出錯
使用asyncData
鉤子時可能會由於伺服器錯誤或api錯誤導致無法渲染,此時頁面還未渲染出來,需要針對這種情況做一些處理,當遇到asyncData錯誤時,跳轉到錯誤頁面,nuxt提供了context.error
方法用於錯誤處理,在asyncData中呼叫該方法即可跳轉到錯誤頁面。
export default {
async asyncData (ctx) {
// 儘量使用try catch的寫法,將所有異常都捕捉到
try {
throw new Error()
} catch {
ctx.error({statusCode: 500, message: '伺服器開小差了~' })
}
}
}
複製程式碼
這樣,當出現異常時會跳轉到預設的錯誤頁,錯誤頁面可以通過/layout/error.vue
自定義。
這裡會遇到一個問題,context.error
的引數必須是類似{ statusCode: 500, message: '伺服器開小差了~' }
,statusCode
必須是http狀態碼,
而我們服務端返回的錯誤往往有一些其他的自定義程式碼,如{resultCode: 10005, resultInfo: '伺服器內部錯誤' }
,此時需要對返回的api錯誤進行轉換一下。
為了方便,我引入了/plugins/ctx-inject.js
為context註冊一個全域性的錯誤處理方法: context.$errorHandler(err)
。注入方法可以參考:注入 $root 和 context,ctx-inject.js
:
// 為context註冊全域性的錯誤處理事件
export default (ctx, inject) => {
ctx.$errorHandler = err => {
try {
const res = err.data
if (res) {
// 由於nuxt的錯誤頁面只能識別http的狀態碼,因此statusCode統一傳500,表示伺服器異常。
ctx.error({ statusCode: 500, message: res.resultInfo })
} else {
ctx.error({ statusCode: 500, message: '伺服器開小差了~' })
}
} catch {
ctx.error({ statusCode: 500, message: '伺服器開小差了~' })
}
}
}
複製程式碼
然後在nuxt.config.js
使用該外掛:
export default {
plugins: [
'~/plugins/ctx-inject.js'
]
}
複製程式碼
注入完畢,我們就可以在asyncData
介個樣子使用了:
export default {
async asyncData (ctx) {
// 儘量使用try catch的寫法,將所有異常都捕捉到
try {
throw new Error()
} catch(err) {
ctx.$errorHandler(err)
}
}
}
複製程式碼
- ajax請求出錯
對於ajax的異常,此時頁面已經渲染,出現錯誤時不必跳轉到錯誤頁,可以通過this.$toast.error(res.message)
toast出來即可。
loading方法
nuxt內建了頁面頂部loading進度條的樣式
推薦使用,提供頁面跳轉體驗。
開啟: this.$nuxt.$loading.start()
完成: this.$nuxt.$loading.finish()
打包部署
一般來說,部署前可以先在本地打包,本地跑一下確認無誤後再上傳到伺服器部署。命令:
// 打包
npm run build
// 本地跑
npm start
複製程式碼
除node_modules,.git,.env,將其他的檔案都上傳到伺服器,然後通過pm2
進行管理,可以在專案根目錄建一個pm2.json
方便維護:
{
"name": "nuxt-test",
"script": "./server/index.js",
"instances": 2,
"cwd": "."
}
複製程式碼
然後配置生產環境的環境變數,一般是直接用.env.prod
的配置:cp ./.env.prod ./.env
。
首次部署或有新的依賴包,需要在伺服器上npm install
一次,然後就可以用pm2
啟動程式啦:
// 專案根目錄下執行
pm2 start ./pm2.json
複製程式碼
需要的話,可以設定開機自動啟動pm2: pm2 save && pm2 startup
。
需要注意的是,每次部署都得重啟一下程式:pm2 reload nuxt-test
。
五、最後
Nuxt.js引入了Node,同時nuxt.config.js
替代了main.js
的一些作用,目錄結構和vue專案都稍有不同,增加了很多的約定,對於初次接觸的同學可能會覺得非常陌生,更多的內容還是得看一遍官方的文件。
demo原始碼: fengxianqi/front_end-demos/src/nuxt-test。