服務端預渲染之Nuxt - 爬坑
Nuxt
是解決SEO
的比較常用的解決方案,隨著Nuxt
也有很多坑,每當突破一個小技術點的時候,都有很大的成就感,在這段時間裡著實讓我痛並快樂著。在這裡根據個人學習情況,所踩過的坑做了一個彙總和總結。
Nuxt開發跨域
專案可以使用Nginx
來反向代理,將外來的請求(這裡也注意下將Linux
的防火牆放行相應埠)轉發的內部Nuxt
預設的3000
埠上,最簡單的配置檔案如下:
nuxtjs.config.js
{ modules: [ '@nuxtjs/axios', '@nuxtjs/proxy' ], proxy: [ [ '/api', { target: 'http://localhost:3001', // api主機 pathRewrite: { '^/api' : '/' } } ] ] }
@nuxtjs/proxy
需要手動單獨安裝。
Nuxt Store 使用
在Nuxt
中使用Vuex
跟傳統在Vue
中使用Vuex
還不太一樣,首先Nuxt
已經集成了Vuex
,不需要我們進行二次安裝,直接引用就好,在預設Nuxt
的框架模板下有一個Store
的資料夾,就是我們用來存放Vuex
的地方。
Nuxt
官方也提供了相關文件
,可以簡單的過一下,但是官方文件我看來比較潦草。
根據官方文件在store
檔案下面建立兩個.js
檔案,分別是index.js
和todo.js
。並在pages
資料夾下面建立index.vue
。
store - index.js
export const state = () => ({ counter: 0 }) export const mutations = { increment (state) { state.counter++ } }
store - todo.js
export const state = () => ({ list: [] }) export const mutations = { add (state, text) { state.list.push({ text: text, done: false }) }, remove (state, { todo }) { state.list.splice(state.list.indexOf(todo), 1) }, toggle (state, todo) { todo.done = !todo.done } }
pages - index.vue
<template> <section class="container"> <div> <h2 @click="$store.commit('increment')">{{counter}}</h2> <ul> <li v-for="(item,index) of list" :key="index">{{item.text}}</li> </ul> </div> </section> </template> <script> import Logo from '~/components/Logo.vue' import {mapState} from "vuex"; export default { components: { Logo }, computed:{ ...mapState(["counter"]), ...mapState("todos",{ list:state => state.list }) }, created(){ for(let i =0;i<10;i++){ this.$store.commit("todos/add",i); } console.log(this.list) } } </script>
在Nuxt
中可以直接使用this.$store
,並且是預設啟用名稱空間的。再看一下computed
中的程式碼,在使用mapState
的時候,counter
屬性是直接獲取出來的,然而todos
屬性則是通過名稱空間才獲取到的。這又是怎麼回事?
Nuxt
把store
中的index.js
檔案中所有的state、mutations、actions、getters
都作為其公共屬性掛載到了,store
例項上,然而其他的檔案則是使用的是名稱空間,其對應的名稱空間的名字就是其檔名。
執行專案的時候可以在.nuxt
資料夾內找到store.js
看下是怎麼完成的。簡單的解釋一下程式碼作用,以及做什麼用的。
.nuxt - store.js
//引入vue import Vue from 'vue' //引入vuex import Vuex from 'vuex' //作為中介軟體 Vue.use(Vuex) //儲存console 函式 const log = console //vuex的屬性 const VUEX_PROPERTIES = ['state', 'getters', 'actions', 'mutations'] //store屬性容器 let store = {} //沒有返回值的自執行函式 void (function updateModules() { // 初始化根資料,也就是上面所說的index檔案做為共有資料 store = normalizeRoot(require('@/store/index.js'), 'store/index.js') // 如果store是函式,提示異常,停止執行 if (typeof store === 'function') { //警告:經典模式的商店是不贊成的,並將刪除在Nuxt 3。 return log.warn('Classic mode for store is deprecated and will be removed in Nuxt 3.') } // 執行儲存模組 // store - 模組化 store.modules = store.modules || {} // 解決儲存模組方法 //引入todos.js 檔案,即資料 //'todos.js' 檔名 resolveStoreModules(require('@/store/todos.js'), 'todos.js') // 如果環境支援熱過載 if (process.client && module.hot) { // 無論何時更新Vuex模組 module.hot.accept([ '@/store/index.js', '@/store/todos.js', ], () => { // 更新的根。模組的最新定義。 updateModules() // 在store中觸發熱更新。 window.$nuxt.$store.hotUpdate(store) }) } })() // 建立store例項 //- 如果 store 是 function 則使用 store //- 否則建立一個新的例項 export const createStore = store instanceof Function ? store : () => { //返回例項 return new Vuex.Store(Object.assign({ strict: (process.env.NODE_ENV !== 'production') }, store)) } //解決儲存模組方法 //moduleData-匯出資料 //filename-檔名 function resolveStoreModules(moduleData, filename) { // 獲取匯出資料,為了解決es6 (export default)匯出 moduleData = moduleData.default || moduleData // 遠端store src +擴充套件(./foo/index.js -> foo/index) const namespace = filename.replace(/\.(js|mjs|ts)$/, '') // 空間名稱 const namespaces = namespace.split('/') // 模組名稱(state,getters等) let moduleName = namespaces[namespaces.length - 1] // 檔案路徑 const filePath = `store/${filename}` // 如果 moduleName === 'state' //- 執行 normalizeState- 正常狀態 //- 執行 normalizeModule - 標準化模組 moduleData = moduleName === 'state' ? normalizeState(moduleData, filePath) : normalizeModule(moduleData, filePath) // 如果是 (state,getters等)執行 if (VUEX_PROPERTIES.includes(moduleName)) { // module名稱 const property = moduleName // 儲存模組//獲取儲存模組 const storeModule = getStoreModule(store, namespaces, { isProperty: true }) // 合併屬性 mergeProperty(storeModule, moduleData, property) // 取消後續程式碼執行 return } // 特殊處理index.js // 模組名稱等於index const isIndexModule = (moduleName === 'index') // 如果等於 if (isIndexModule) { // 名稱空間彈出最後一個 namespaces.pop() // 獲取模組名稱 moduleName = namespaces[namespaces.length - 1] } // 獲取儲存模組 const storeModule = getStoreModule(store, namespaces) // 遍歷 VUEX_PROPERTIES for (const property of VUEX_PROPERTIES) { // 合併屬性 //storeModule-儲存模組 //moduleData[property]-儲存模組中的某個屬性資料 //property-模組名稱 mergeProperty(storeModule, moduleData[property], property) } // 如果moduleData.namespaced === false if (moduleData.namespaced === false) { // 刪除名稱空間 delete storeModule.namespaced } } //初始化根資料 //moduleData-匯出資料 //filePath-檔案路徑 function normalizeRoot(moduleData, filePath) { // 獲取匯出資料,為了解決es6 (export default)匯出 moduleData = moduleData.default || moduleData // 如果匯入的資料中存在commit方法,則丟擲異常 // - 應該匯出一個返回Vuex例項的方法。 if (moduleData.commit) { throw new Error(`[nuxt] ${filePath} should export a method that returns a Vuex instance.`) } // 如果 moduleData 不是函式,則使用空隊形進行合併處理 if (typeof moduleData !== 'function') { // 避免鍵入錯誤:設定在覆蓋頂級鍵時只有getter的屬性 moduleData = Object.assign({}, moduleData) } //對模組化進行處理後返回 return normalizeModule(moduleData, filePath) } //正常狀態 //- 模組資料 //- 檔案路徑 function normalizeState(moduleData, filePath) { // 如果 moduleData 不是function if (typeof moduleData !== 'function') { //警告提示 //${filePath}應該匯出一個返回物件的方法 log.warn(`${filePath} should export a method that returns an object`) //合併 state const state = Object.assign({}, moduleData) //以函式形式匯出state return () => state } //對模組化進行處理 return normalizeModule(moduleData, filePath) } //對模組化進行處理 //moduleData-匯出資料 //filePath-檔案路徑 function normalizeModule(moduleData, filePath) { // 如果module資料的state存在並且不是function警告提示 if (moduleData.state && typeof moduleData.state !== 'function') { //“state”應該是返回${filePath}中的物件的方法 log.warn(`'state' should be a method that returns an object in ${filePath}`) // 合併state const state = Object.assign({}, moduleData.state) // 覆蓋原有state使用函式返回 moduleData = Object.assign({}, moduleData, { state: () => state }) } // 返回初始化資料 return moduleData } //獲取store的Model //-storeModulestore資料模型 //-namespaces名稱空間名稱陣列 //-是否使用名稱空間預設值 為false function getStoreModule(storeModule, namespaces, { isProperty = false } = {}) { //如果 namespaces 不存在,啟動名稱空間,名稱空間名稱長度1 if (!namespaces.length || (isProperty && namespaces.length === 1)) { //返回model return storeModule } //獲取名稱空間名稱 const namespace = namespaces.shift() //儲存名稱空間中的資料 storeModule.modules[namespace] = storeModule.modules[namespace] || {} //啟用名稱空間 storeModule.modules[namespace].namespaced = true //新增命名資料 storeModule.modules[namespace].modules = storeModule.modules[namespace].modules || {} //遞迴 return getStoreModule(storeModule.modules[namespace], namespaces, { isProperty }) } // 合併屬性 //storeModule-儲存模組 //moduleData-儲存模屬性資料 //property-模組名稱 function mergeProperty(storeModule, moduleData, property) { // 如果 moduleData 不存在推出程式 if (!moduleData) return // 如果 模組名稱 是 state if (property === 'state') { //把state資料分到模組空間內 storeModule.state = moduleData || storeModule.state } else { // 其他模組 // 合併到對應的模組空間內 storeModule[property] = Object.assign({}, storeModule[property], moduleData) } }
以上就是編譯後的store
檔案,大致的意思就是對store
檔案進行遍歷處理,根據不同的檔案使用不同的解決方案,使用名稱空間掛載model
。
頁面loading
Nuxt
有提供載入Loading
元件,一下是配置。
nuxtjs.config.js
module.exports = { loading: { color: '#3B8070' } }
Nuxt
提供的loading
不能滿足專案需求,可能有的專案不需要這樣載入動畫,so~,就需要自己手動配置一個。新增一個loading元件 (官方示例如下,詳情可看官方文件)引用該元件。
nuxtjs.config.js
module.exports = { loading: '~components/loading.vue' }
一個小插曲在Nuxt中,~與@都指向的是根目錄。
components/loading.vue
<template lang="html"> <div class="loading-page" v-if="loading"> <p>Loading...</p> </div> </template> <script> export default { data: () => ({ loading: false }), methods: { start () { this.loading = true }, finish () { this.loading = false } } } </script>
第三方元件庫
專案開發過程中,難免會用到元件庫,與在Vue
中使用的時候是太一樣的,需要新增一些依賴才能正常使用。
plugins - element-ui.js
import Vue from 'vue'; import Element from 'element-ui'; import locale from 'element-ui/lib/locale/lang/en'; export default () => { Vue.use(Element, { locale }) };
nuxtjs.config.js
module.exports = { css: [ 'element-ui/lib/theme-chalk/index.css' ], plugins: [ '@/plugins/element-ui', '@/plugins/router' ] };
使用中介軟體
中介軟體Nuxt
沒有給出具體的使用文件,而是放入了一個編輯器。這一點我感覺到了一絲絲的 差異。為什麼要這樣。。。簡單的研究了一下,弄明白了大概。
在middleware
中建立想要的中介軟體。這裡借用一下官網的例子。
middleware - visits.js
export default function ({ store, route, redirect }) { store.commit('ADD_VISIT', route.path) }
向上面這樣就建立好了一箇中間件,但是應該怎麼使用呢?在使用的時候有兩種方式,一種是全域性使用,另一種是在頁面中單獨使用,檔名會作為其中介軟體的名稱。
++全域性使用++
nuxtjs.config.js
export default { router: { middleware: ['visits'] } }
頁面中單獨使用
export default { middleware: 'auth' }
官網中在頁面中的asyncData
中有一段這樣的程式碼。
export default { asyncData({ store, route, userAgent }) { return { userAgent } } }
持續更新。。。
總結
Nuxt
的學習曲線非常小,就像Vue
框架一樣,已經是一個開箱即用的狀態,我們可以直接跨過配置直接開發。對配置有興趣的可以在Vue
官方文件找到SSR
渲染文件。