前言

本篇主要針對nuxtjs中的一些重要概念整理和程式碼實現!

在學習vue服務端渲染之前,先搞清楚幾個概念:

  • 什麼是客戶端渲染(CSR)
  • 什麼是服務端渲染(SSR)
  • CSR和SSR有什麼異同

客戶端渲染(CSR):當用戶在瀏覽器中輸入網址,開啟網頁,此時的頁面只有樣式和一些html程式碼構成的空殼頁面,並沒有資料。這就需要我們通過執行js程式碼,請求相關資料,請求到資料之後,通過模板(vue),將這些資料渲染到頁面,最終呈現給使用者完整的頁面。

服務端渲染(SSR):當用戶在瀏覽器中輸入網址,開啟網頁,此時的後端會根據請求的網址,拿到相關頁面並將資料填入到頁面,完成頁面的渲染,最後將完整的頁面返回給客戶端(瀏覽器),呈現到使用者面前。

利弊:客戶端渲染,就是現在非常流行的前後分離的開發模式,極大程度的減輕後端壓力,但是對SEO不友好;服務端渲染對SEO友好,但是對後端以及伺服器的效能要求較高。

正文:

先用node實現簡易的服務端渲染:

const Vue = require('vue');
const server = require('express')(); server.get('/',(req,res)=>{ //建立 Vue 例項
const app = new Vue({
template:`<div>hello vue SSR!</div>`
}) // 建立 renderer 沒下載的需要手動下載 npm i vue-server-renderer --save改包用於vue的服務端渲染
const renderer = require('vue-server-renderer').createRenderer(); renderer.renderToString(app).then(html=>{
res.send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
${html}
</body>
</html>
`)
}).catch(err=>{
console.log(err)
})
}) server.listen(8001)

檢查原始碼:

下面使用nuxjs來構建專案!

首先,使用nuxt提供的腳手架( create-nuxt-app)構建專案,根據提示一步一步操作。

主要注意倆點:1.Choose custom server framework,選擇服務端渲染的框架,我們這裡選擇的是express

        2.Choose rendering mode 這個選項可以選擇SSR和Single Page APP,因為我們需要構建服務端渲染多頁面專案,所以這裡選擇SSR

下面就是專案建立完成後的目錄結構:

目錄結構:

  • assets::資源目錄,用於組織未編譯的靜態資源(css檔案、圖示等)
  • components: 元件目錄,用於組織應用的vue元件,這些元件不會像頁面元件(pages中的元件)那樣有 asyncData 方法的特性
  • layouts:佈局目錄,用於組織應用的佈局元件,若無額外配置,該目錄不能被重新命名
  • middleware:中介軟體目錄,用於存放應用的中介軟體
  • pages: 頁面目錄,用於組織應用的路由及檢視,Nuxt.js 框架讀取該目錄下所有的 .vue檔案並自動生成對應的路由配置,若無額外配置,該目錄不能被重新命名
  • plugins:外掛目錄,用於組織那些需要在 根vue.js應用例項化之前需要執行的JavaScript外掛
  • server:伺服器目錄,用於服務端渲染
  • static:靜態檔案目錄,用於存放應用的靜態檔案,此類檔案不會被 Nuxt.js 呼叫 Webpack 進行構建編譯處理。伺服器啟動的時候,該目錄下的檔案會對映至應用的根路徑 / 下(/static/robots.txt 對映至 /robots.txt),若無額外配置,該目錄不能被重新命名
  • store:store目錄,用於組織應用的 Vuex狀態樹 檔案,若無額外配置,該目錄不能被重新命名
  • nuxt.config.js:用於組織 Nuxt.js 應用的個性化配置,以便覆蓋預設配置,若無額外配置,該目錄不能被重新命名
  • package.json:用於描述應用的依賴關係和對外暴露的指令碼介面,該檔案不能被重新命名

專案跑起來:

檢查原始碼,頁面的所有資料都能夠看到。

接下來正式學習nuxt,主要分為3部分:

  • 生命週期
  • 路由
  • vuex狀態樹

一、生命週期(鉤子函式)

  • nuxtServerInit => 伺服器初始化
  • middleware => 中介軟體
  • validate() => 引數校驗
  • asyncData()和fetch() => 非同步資料處理
  • render => 客戶端渲染
  • beforeCreate和created => 這倆個鉤子服務端和客戶端都存在,可以拿到服務端的上下文以及元件本身

下面具體使用這些生命週期:

1.nuxtServerInit 

在store目錄中新建index.js:

export const actions = {
nuxtServerInit(store, context){
console.log('我是nuxtServerInit生命週期!', store, context)
}
}

專案跑起來後,在終端會列印相關資訊:

2.middleware 

middleware中介軟體可以在配置檔案中、佈局頁面、頁面元件中使用。

在nuxt.config.js配置檔案中,將middleware寫進router配置中,可以進行全域性的導航守衛:

module.exports = {
mode: 'universal',
router: {
middleware: 'isLogin'
},
...

接下來在middleware目錄中新建isLogin.js檔案:

export default (context) => {
console.log('我是全域性守衛')
}

列印:

在佈局頁面中使用中介軟體,在layout目錄下的default.vue中:

<script>
export default {
// middleware: 'isLogin',
middleware(){
console.log('我是佈局頁面的中介軟體!')
},
}
</script>

在頁面元件內中介軟體可以直接寫外部之前定義的中介軟體,也可以用中介軟體鉤子!

同樣,在頁面元件中,pages目錄下的index.vue中:

<script>
export default {
// middleware: 'isLogin',
middleware(){
console.log('我是頁面元件的中介軟體!')
},
}
</script>

通過列印的順序可以看出,中介軟體的執行順序是:nuxt.config.js檔案 => 佈局檔案 => 頁面檔案,所有中介軟體函式的引數中,都能拿到服務端的上下文資訊context。

3.validate()

validate()需要定義在頁面元件內,即pages目錄下的頁面,可以用來進行引數校驗,攔截是否可以進入該頁面。具體實現,在pages/index.vue中:

//js 程式碼
export default {
// middleware: 'isLogin',
middleware(){
console.log('我是頁面元件的中介軟體!')
},
validate(context){
const {params, query} = context;
//對傳過來的params和query進行校驗
//...
console.log('我是頁面validate引數校驗')
return true; // 校驗通過,校驗失敗返回false,進入到404頁面
},
components: {
Logo
}
}

 4.asyncData()和fetch()

這倆個鉤子也是在頁面元件中使用,pages/index.vue中:

  asyncData(context){
// 非同步讀取資料,處理相關業務邏輯
console.log('我是asyncData')
//最後返回資料
return {
a: 1
};
}, fetch(context){
// 非同步讀取資料,處理相關業務邏輯,將資料提交給vuex
console.log('我是fetch')
},

5.beforeCreate()和created()

在pages/index.vue中:

...
fetch(context){
// 非同步讀取資料,處理相關業務邏輯,將資料提交給vuex
console.log('我是fetch')
},
beforeCreate() {
console.log('我是beforeCreate')
},
created() {
console.log('我是create')
},
...

客戶端:

服務端:

二、路由(約定式、自定義)

1.約定式路由:依據pages目錄結構自動生成 vue-router模組的路由配置。

1>基礎路由:

在layouts目錄中的default.vue:

<template>
<div>
<!-- 宣告式跳轉:nuxt-link跟vue中的router-link一樣,負責路由跳轉 -->
<nuxt-link to="/">首頁</nuxt-link>
<nuxt-link to="/tabA">tabA</nuxt-link>
<nuxt-link to="/tabB">tabB</nuxt-link> <!-- 展示區,跟vue中的router-view一樣 -->
<nuxt/>
</div>
</template>

效果:

nuxt自動生成的路由為:

router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'tabA',
path: '/tabA',
component: 'pages/tabA.vue'
},
{
name: 'tabB',
path: '/tabB',
component: 'pages/tabB.vue'
}
]
}

2>動態路由

在 Nuxt.js 裡面定義帶引數的動態路由,需要建立對應的以下劃線“_”作為字首的 Vue 檔案 或 目錄。

pages/tabA.vue:

<template>
<div>
<h3>
tabA
</h3>
<!-- 倆種路由傳參方式 -->
<nuxt-link to="/tabA/1?a=tabA1">tabA動態路由1</nuxt-link>
<!-- name: 'tabA-id',這裡-id必須是tabA目錄下的_id.vue的名稱,且將 _ 改為 - -->
<nuxt-link :to="{name: 'tabA-id', params:{id: 2}, query:{a: 'tabA2'}}">tabA動態路由2</nuxt-link> <!-- 子路由的展示區 -->
<nuxt/>
</div>
</template>

pages/tabA/_id.vue:

<template>
<div>
<h3>我是tabA的動態路由頁</h3>
</div>
</template>

效果:

這裡會發現一個問題,路由為/tabA的時候,也打開了動態路由頁,這是因為當路由為tabA時,會發現它下面還有子路由,由於沒有指定預設的頁面,因此會找到一個子路由進行渲染。為了解決這個問題,可以在tabA目錄下新建一個index.vue。

tabA/index.vue:

<template>
<h3>我是動態子路由預設頁</h3>
</template>

此時nuxt自動生成的路由資訊為:

router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'tabA-id',
path: '/tabA/:id',
component: 'pages/tabA/_id.vue'
},
...
]
}

3>巢狀路由

建立內嵌子路由,你需要新增一個 Vue 檔案,同時新增一個與該檔案同名的目錄用來存放子檢視元件。

在tabA目錄下新建tabC目錄和tabC.vue

tabA/tabC.vue:

<template>
<div>
<!-- 子路由展示區 -->
<nuxt></nuxt>
</div>
</template>

tabA/tabC/index.vue:

<template>
<div>
<h3>我是tabC</h3>
</div>
</template>

從上圖可以看到倆種顏色的區域,是倆級展示區,通常我們一個頁面只需要一個展示區,因此需要改一下專案的目錄結構:

在pages目錄下的頁面為一級展示區,其中有tabA、tabB,接下來將tabA目錄下的頁面也設定成一級展示區。將tabA.vue內容拷貝至tabA目錄下的index.vue中,並刪除tabA.vue。

2、擴充套件路由

nuxt中支援宣告式路由,同樣也支援自定義路由,根據路由渲染指定的檢視,需要在nuxt.config.js中配置:

router: {
extendRoutes(routes, resolve){
routes.push({
name: 'tabD',
path: '/tabD',
component: resolve(__dirname, 'pages/extendTabD.vue')
})
}
},

在pages中新建extendTabD.vue:

pages/extendTabD.vue

<template>
<div>
<h3>我是擴充套件路由tabD</h3>
</div>
</template>

在layouts/default.vue中增加連結: <nuxt-link to="/tabD">tabD</nuxt-link>

3、路由過渡動畫

1>全域性過渡動畫:

transition.css:

/*  路由統一動效 */

/* 動畫形式 */
.page-enter-active, .page-leave-active{
transition: opacity .5s;
} /* 入 退 */
.page-enter, .page-leave-active{
opacity: 0;
}

nuxt.config.js:

/*
** Global CSS
*/
css: [
'assets/css/transition.css'
],

2>區域性過渡動畫

extendTabD.vue:

<template>
<div>
<h3>我是擴充套件路由tabD</h3>
</div>
</template> <script>
export default {
name: "extendTabD",
transition: 'tabd'
}
</script> <style scoped>
.tabd-enter-active, .tabd-leave-active{
transition: .5s ease all;
}
.tabd-enter, .tabd-leave-active{
margin-left: -1000px;
}
</style>

 4、路由守衛

  • 前置
  • 後置
  • 元件內部

1>前置:

全域性守衛:

  • 在nuxt.config.js的router內引入middleware
  • 在layouts檔案中定義middleware
  • 在外掛中定義前置全域性守衛,beforeEach()

元件獨享守衛:

  • 在頁面中引入middleware或middleware()
  • 跟vue-router一樣的元件內部鉤子函式,比如:beforeRouteEnter

2>後置:

在外掛中定義後置全域性守衛,afterEach()

具體實現:

全域性守衛:

a.藉助middleware實現前置的路由守衛,在生命週期的middleware中已經基本實現。

middleware/isLogin.js:

export default (context) => {
console.log('我是全域性守衛')
const {redirect} = context
redirect('/tabD') // 利用redirect()頁面重定向
}

b.藉助外掛實現前置、後置全域性守衛:

plugins/router.js

export default ({app, redirect})=>{
//app == vue例項
//全域性前置守衛,進入的頁面不是tabA的頁面,都會重定向到tabA
app.router.beforeEach((to, from, next)=>{
if(to.name == 'tabA'){
next()
}else{
redirect({name: 'tabA'})
}
});
//全域性後置守衛
app.router.afterEach((to, from)=>{
console.log('我是後置守衛')
})
}

nuxt.config.js中配置plugins:

plugins: [
'~/plugins/router.js'
],

元件內守衛:

extendTabD.vue:

<template>
<div>
<h3>我是擴充套件路由tabD</h3>
</div>
</template> <script>
export default {
name: "extendTabD",
transition: 'tabd',
beforeRouteEnter(to, from, next){
alert('haha');
}
}
</script>

三、vuex狀態樹

nuxt中集成了vuex,因此我們在專案中不需要下載引入,而是按照約定使用:

nuxt會嘗試找到 src 目錄(預設是應用根目錄)下的 store 目錄,如果該目錄存在,它將做以下的事情:

  1. 引入vuex模組
  2. 將vuex模組加到vendors構建配置中
  3. 設定vue根例項的store配置項

nuxt中的vuex使用方式跟vue中大同小異,除了不需要手動引入之外,在模組化中,store目錄下的每個子模組(除了根模組index.js),都會被轉換成為狀態樹指定命名的子模組,其它使用方式跟vue專案中的vuex基本相同。

store/index.js:

const state = ()=>({

})
const mutations = { } const actions = { }
export default {
state,
mutations,
actions
}

store/user.js:

const state = () => ({
isLogin: false
}) const mutations = {
change_Login(state, isLogin) {
state.isLogin = isLogin
}
} const actions = {
changeLogin({commit},isLogin) {
commit('change_Login', isLogin)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

pages/tabB.vue:

<template>
<div>
<h3>tabB</h3>
<h4>登入狀態:{{isLogin ? '已登入' : '未登入'}}</h4>
</div>
</template> <script>
import { mapState } from 'vuex';
export default {
name: "TabB",
computed:{
...mapState({
isLogin: state => state.user.isLogin
})
},
created() {
setTimeout(()=>{
this.$store.dispatch('user/changeLogin', true)
}, 1000)
}
}
</script>

效果:一秒後,登入狀態從“未登入”變為“已登入”。

以上就是我對nuxt學習過程中一些重要概念的理解和實踐,有什麼不足歡迎評論指正!

腳踏實地行,海闊天空飛