服務端預渲染之Nuxt(爬坑篇)

Nuxt是解決SEO的比較經常使用的解決方案,隨着Nuxt也有不少坑,每當突破一個小技術點的時候,都有很大的成就感,在這段時間裏着實讓我痛並快樂着。在這裏根據我的學習狀況,所踩過的坑作了一個彙總和總結。javascript

Nuxt開發跨域css

項目可使用Nginx來反向代理,將外來的請求(這裏也注意下將Linux的防火牆放行相應端口)轉發的內部Nuxt默認的3000端口上,最簡單的配置文件以下:html

nuxtjs.config.jsvue

{
    modules: [
      '@nuxtjs/axios',
      '@nuxtjs/proxy'
    ],
    proxy: [
        [
            '/api',
            { 
              target: 'http://localhost:3001', // api主機
              pathRewrite: { '^/api' : '/' }
            }
        ]
    ]
}

@nuxtjs/proxy須要手動單獨安裝。java

Nuxt Store 使用ios

Nuxt中使用Vuex跟傳統在Vue中使用Vuex還不太同樣,首先Nuxt已經集成了Vuex,不須要咱們進行二次安裝,直接引用就好,在默認Nuxt的框架模板下有一個Store的文件夾,就是咱們用來存放Vuex的地方。es6

Nuxt官方也提供了相關文檔,能夠簡單的過一下,可是官方文檔我看來比較潦草。vuex

根據官方文檔在store文件下面建立兩個.js文件,分別是index.jstodo.js。並在pages文件夾下面建立index.vueelement-ui

store - index.jsaxios

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屬性則是經過命名空間才獲取到的。這又是怎麼回事?

Nuxtstore中的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
//      -   storeModule         store數據模型
//      -   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渲染文檔。

相關文章
相關標籤/搜索