Nuxt 開發(fā)搭建博客

眾所周知,Vue SPA單頁(yè)面應(yīng)用對(duì)SEO不友好,當(dāng)然也有相應(yīng)的解決方案。 服務(wù)端渲染 (SSR) 就是常用的一種。 SSR 有利于 搜索引擎優(yōu)化(SEO, Search Engine Optimization) ,并且 內(nèi)容到達(dá)時(shí)間(time-to-content) (或稱之為首屏渲染時(shí)長(zhǎng))也有很大的優(yōu)化空間。

Nuxt.js 是一個(gè)基于 Vue.js 的輕量級(jí)應(yīng)用框架,可用來創(chuàng)建 服務(wù)端渲染 (SSR) 應(yīng)用,也可充當(dāng)靜態(tài)站點(diǎn)引擎生成靜態(tài)站點(diǎn)應(yīng)用,具有優(yōu)雅的代碼結(jié)構(gòu)分層和熱加載等特性。

項(xiàng)目地址:明么的博客

初始化項(xiàng)目

運(yùn)行 create-nuxt-app

通過 Nuxt 官方提供的腳手架工具 create-nuxt-app 初始化項(xiàng)目:

$ npx create-nuxt-app <項(xiàng)目名>

// 或者

$ yarn create nuxt-app <項(xiàng)目名>

項(xiàng)目配置

項(xiàng)目創(chuàng)建的時(shí)候會(huì)讓你進(jìn)行一些配置的選擇,可根據(jù)自己需要進(jìn)行選擇。


項(xiàng)目配置

項(xiàng)目運(yùn)行

運(yùn)行完后,它將安裝所有依賴項(xiàng),下一步是啟動(dòng)項(xiàng)目:

$ cd <project-name>
$ yarn dev

在瀏覽器中,打開 http://localhost:3000

項(xiàng)目初始化

目錄結(jié)構(gòu)

.
├── assets        // 用于組織未編譯的靜態(tài)資源
├── components        // 用于組織應(yīng)用的 Vue.js 組件
├── layouts        // 用于組織應(yīng)用的布局組件
├── middleware        // 用于存放應(yīng)用的中間件
├── node_modules
├── pages        // 用于組織應(yīng)用的路由及視圖
├── plugins        // 組織插件。
├── static        // 用于存放應(yīng)用的靜態(tài)文件
├── store        // 狀態(tài)管理
├── nuxt.config.js        // 配置文件
├── package.json
├── jsconfig.json
├── stylelint.config.js
├── README.md
└── yarn.lock

項(xiàng)目開發(fā)

項(xiàng)目啟動(dòng)之后,我們就可以進(jìn)行開發(fā)階段了。

創(chuàng)建頁(yè)面

pages創(chuàng)建頁(yè)面文件:

pages/
└── article/
    ├── index.vue
    ├── _category/
    │   └── index.vue
    └── detail/
        └── _articleId.vue

Nuxt.js 預(yù)設(shè)了利用 Vue.js 開發(fā)服務(wù)端渲染的應(yīng)用所需要的各種配置。所以不需要再安裝 vue-router 了,他會(huì)依據(jù) pages 目錄結(jié)構(gòu)自動(dòng)生成 vue-router 模塊的路由配置。頁(yè)面之間使用路由,官方推薦使用 <nuxt-link> 標(biāo)簽,與 <router-link> 的使用方式是一樣的。上面創(chuàng)建的目錄結(jié)構(gòu)將會(huì)生成對(duì)應(yīng)的路由配置表:

router: {
  routes: [
    {
      name: 'article',
      path: '/article',
      component: 'pages/article/index.vue'
    },
    {
      name: "article-category"
      path: "/article/:category",
      component: 'pages/article/_category/index.vue',
    },
    {
      name: "article-detail-articleId"
      path: "/article/detail/:articleId",
      component: 'pages/article/detail/_articleId.vue'
    }
  ]
}

組件部分

組件這一塊劃分為base、framework、page三個(gè)目錄:

components/
├── base    基本組件
├── framework    布局相關(guān)組件
└── page/    各個(gè)頁(yè)面下的組件
    ├── home
    └── ...

這里需要注意在開發(fā) VUE SPA 應(yīng)用時(shí)我們有時(shí)候會(huì)把頁(yè)面組件放在 pages 下,我將頁(yè)面下的組件全部放到了components下,因?yàn)?Nuxt.js 框架會(huì)讀取 pages 目錄下所有的 .vue 文件并自動(dòng)生成對(duì)應(yīng)的路由配置。

資源的存放

官方介紹的很詳細(xì),資源的存放有兩個(gè)目錄:staticassets

static : 用于存放應(yīng)用的靜態(tài)文件,此類文件不會(huì)調(diào)用 Webpack 進(jìn)行構(gòu)建編譯處理。服務(wù)器啟動(dòng)的時(shí)候,該目錄下的文件會(huì)映射至應(yīng)用的根路徑 / 下。
舉個(gè)例子: /static/banner.png 映射至 /banner.png

assets : 用于組織未編譯的靜態(tài)資源如 LESS、SASSJS。

別名

別名 目錄
~ 或 @ srcDir
~~ 或 @@ rootDir

為了方便引用,nuxt 提供了兩個(gè)別名,如果你需要引入 assets 或者 static 目錄, 使用 ~/assets/your_image.png~/static/your_image.png 方式。

全局樣式

這里我選用 LESS 預(yù)處理語(yǔ)言,安裝:

$ yarn add less less-loader -D

assets/css/ 創(chuàng)建 .less 文件, 通過一個(gè)文件引入:

// assets/css/index.less

@import './normalize.less';
@import './reset.less';
@import './variables.less';
@import './common.less'

nuxt.config.js 中引入

export default {
  ...
  css: ['~/assets/css/index.less'],
  ...
}

LESS 全局變量

在使用預(yù)處理語(yǔ)言的時(shí)候,我們肯定會(huì)使用到變量,以方便統(tǒng)一管理顏色、字體大小等。

首先定義好變量文件 variables.less

/* ===== 主題色配置 ===== */
@colorPrimary: #6bc30d;
@colorAssist: #2db7f5;
@colorSuccess: #67c23a;
@colorWarning: #e6a23c;
@colorError: #f56c6c;
@colorInfo: #909399;

安裝:

$ yarn add @nuxtjs/style-resources -D

nuxt.config.js 中增加配置:

export default {
  ...
  modules: [
    // https://go.nuxtjs.dev/axios
    '@nuxtjs/axios',
    '@nuxtjs/style-resources',
  ],
  styleResources: {
    // your settings here
    // sass: [],
    // scss: [],
    // stylus: [],
    less: ['~/assets/css/variables.less'],
  },
  ...
}

布局layouts

我的博客大概分為這幾種布局方式:


項(xiàng)目布局

在這里我創(chuàng)建了三種布局組件:

layouts/
├── admin.vue // 上圖第四個(gè)
├── default.vue // 上圖第一個(gè)和第三個(gè)只包含nav和footer
└── user.vue //上圖第二個(gè)

admin.vue: 后臺(tái)管理模塊的布局
user.vue: 個(gè)人中心模塊的布局
default.vue: 默認(rèn)的布局

default.vue 舉例,我把 導(dǎo)航頁(yè)腳 放到了一個(gè)組件 AppLayout 中:

<!-- layouts/default.vue -->

<template>
  <app-layout>
    <nuxt />
  </app-layout>
</template>

<script>
import AppLayout from '@/components/framework/AppLayout/AppLayout'

export default {
  name: 'AppLayoutDefault',
  components: {
    AppLayout
  }
}
</script>

然后在頁(yè)面中使用:

<!-- pages/index.vue -->

<template>
  <!-- Your template -->
</template>
<script>
  export default {
    layout: 'default'
    // 指定布局,不指定的話將會(huì)使用默認(rèn)布局: layouts/default.vue
    // 其實(shí)我這里指不指定都可以哈哈。
  }
</script>

關(guān)于頁(yè)面上路由的跳轉(zhuǎn),官方推薦使用 <nuxt-link>,這里 <nuxt-link><a> 還是有區(qū)別的,nuxt-link 走的是 vue-router 的路由,即頁(yè)面為單頁(yè)面,瀏覽器不會(huì)重定向。而 <a>標(biāo)簽走的是 window.location.href ,每次點(diǎn)擊a標(biāo)簽后頁(yè)面,都會(huì)進(jìn)行一次服務(wù)端渲染。

全局過濾器

plugins/ 目錄下,新建 filters.js,比如說我們要對(duì)時(shí)間進(jìn)行一個(gè)格式化處理 :

Day.js :一個(gè)輕量的處理時(shí)間和日期的 JavaScript 庫(kù)

$ yarn add dayjs
import Vue from 'vue'
import dayjs from 'dayjs'
// 時(shí)間格式化
export function dateFormatFilter(date, fmt) {
  if (!date) {
    return ''
  } else {
    return dayjs(date).format(fmt)
  }
}
const filters = {
  dateFormatFilter
}
Object.keys(filters).forEach((key) => {
  Vue.filter(key, filters[key])
})
export default filters

然后,在 nuxt.config.js 中配置,

export default {
  ...
  plugins: ['~/plugins/filters.js']
  ...
}

自定義指令

plugins/directive/focus 目錄下,添加 index.js :

import Vue from 'vue';
const focus = Vue.directive('focus', {
  inserted(el) {
    el.focus();
  },
});
export default focus;

自定義指令和全局過濾器一樣,都需要在 nuxt.config.js 添加配置:

export default {
  ...
  plugins: [
    '~/plugins/filters.js',
    { src: '~/plugins/directive/focus/index.js', ssr: false },
  ],
  ...
}

head 配置SEO

通過使用 head 方法設(shè)置當(dāng)前頁(yè)面的頭部標(biāo)簽。


<template>
  <h1>{{ title }}</h1></template>

<script>
  export default {
    ...
    head() {
      return {
        title: '明么的博客',
        meta: [
          {
            hid: 'description',
            name: 'description',
            content: 'My custom description'
          }
        ]
      }
    }
  }
</script>

注意:為了避免子組件中的 meta 標(biāo)簽不能正確覆蓋父組件中相同的標(biāo)簽而產(chǎn)生重復(fù)的現(xiàn)象,建議利用 hid 鍵為 meta 標(biāo)簽配一個(gè)唯一的標(biāo)識(shí)編號(hào)。

如果頁(yè)面比較多的話,每個(gè)頁(yè)面都需要寫 head 對(duì)象,就會(huì)有些的繁瑣??梢越柚?nuxtplugin 機(jī)制,將其封裝成一個(gè)函數(shù),并注入到每一個(gè)頁(yè)面當(dāng)中:

// plugins/head.js
import Vue from 'vue'

Vue.mixin({
  methods: {
    $seo(title, content) {
      return {
        title,
        meta: [{
          hid: 'description',
          name: 'description',
          content
        }]
      }
    }
  }
})

nuxt.config.js 中增加配置:

export default {
  ...
  plugins: [
    '~/plugins/filters.js',
    { src: '~/plugins/directive/focus/index.js', ssr: false },
    '~/plugins/head.js'
  ],
  ...
}

在頁(yè)面中使用:

head() {
    return this.$seo(this.detail.title, this.detail.summary)
}

axios 請(qǐng)求數(shù)據(jù)

請(qǐng)求數(shù)據(jù),在初始化項(xiàng)目的時(shí)候已經(jīng)選擇了Axios,就不需要再另行安裝了,可以查看 nuxt.config.js 中已經(jīng)配置好了:

export default {
  ...
  modules: [
    // https://go.nuxtjs.dev/axios
    '@nuxtjs/axios',
    ...
  ],
  ...
}

頁(yè)面中通過 this.$axios.$get 來獲取數(shù)據(jù),不需要在每個(gè)頁(yè)面都單獨(dú)引入 axios.
但是一般來說我們會(huì)對(duì) axios 做一下封裝,集中處理一些數(shù)據(jù)或者是錯(cuò)誤信息。
plugins 目錄下新建 axios.jsapi-repositories.js,下面是我的一些簡(jiǎn)單的配置:

//  plugins/axios.js
import qs from 'qs'

export default function(ctx) {
  const { $axios, store, app } = ctx
  // $axios.defaults.timeout = 0;
  $axios.transformRequest = [
    (data, header) => {
      if (header['Content-Type'] && header['Content-Type'].includes('json')) {
        return JSON.stringify(data)
      }
      return qs.stringify(data, { arrayFormat: 'repeat' })
    }
  ]

  $axios.onRequest((config) => {
    const token = store.getters.token
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    // 如果是 get 請(qǐng)求,參數(shù)序列化
    if (config.method === 'get') {
      config.paramsSerializer = function(params) {
        return qs.stringify(params, { arrayFormat: 'repeat' }) // params是數(shù)組類型如arr=[1,2],則轉(zhuǎn)換成arr=1&arr=2
      }
    }
    return config
  })

  $axios.onRequestError((error) => {
    console.log('onRequestError', error)
  })

  $axios.onResponse((res) => {
    // ["data", "status", "statusText", "headers", "config", "request"]
    // 如果 后端返回的碼正常 則 將 res.data 返回
    if (res && res.data) {
      if (res.headers['content-type'] === 'text/html') {
        return res
      }
      if (res.data.code === 'success') {
        return res
      } else {
        return Promise.reject(res.data)
      }
    }
  })

  $axios.onResponseError((error) => {
    console.log('onResponseError', error)
  })

  $axios.onError((error) => {
    console.log('onError', error)

    if (error && error.message.indexOf('401') > 1) {
      app.$toast.error('登錄過期了,請(qǐng)重新登錄!')
      sessionStorage.clear()
      store.dispatch('changeUserInfo', null)
      store.dispatch('changeToken', '')

    } else {
      app.$toast.show(error.message)
    }
  })
}
// plugins/api-repositories.js
export default ({ $axios }, inject) => {
  const repositories = {
    GetCategory: (params, options) => $axios.get('/categories', params, options),
    PostCategory: (params, options) => $axios.post('/categories', params, options),
    PutCategory: (params, options) => $axios.put(`/categories/${params.categoryId}`, params, options),
    DeleteCategory: (params, options) => $axios.delete(`/categories/${params.categoryId}`, params, options)
    ...
  }

  inject('myApi', repositories)
}

然后在 nuxt.config.js 中增加配置:

export default {
  ...
  plugins: [
    ...
    { src: '~/plugins/axios.js', ssr: true },
    { src: '~/plugins/api-repositories.js', ssr: true },
  ],
    /*
   ** Axios module configuration
   ** See https://axios.nuxtjs.org/options
   */
  axios: {
    baseURL: 'http://localhost:5000/'
  },
}

這樣就可以直接在頁(yè)面中使用了:

this.$myApi.GetCategory()

proxy 代理

使用 proxy 解決跨域問題:

$ yarn add @nuxtjs/proxy

nuxt.config.js 中增加配置,下面是我的配置:

export default {
  ...
  modules: [
    ...
    '@nuxtjs/proxy',
    ...
  ],
  axios: {
    proxy: true,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'X-Requested-With': 'XMLHttpRequest',
      'Content-Type': 'application/json; charset=UTF-8'
    },
    prefix: '/api',
    credentials: true
  },
  /*
   ** 配置代理
   */
  proxy: {
    '/api': {
      target: process.env.NODE_ENV === 'development' ? 'http://localhost:5000/' : 'http://localhost:5000/',
      changeOrigin: true,
      pathRewrite: {
        '^/api': ''
      }
    },
    '/douban/': {
      target: 'http://api.douban.com/v2',
      changeOrigin: true,
      pathRewrite: {
        '^/douban': ''
      }
    },
    ...
  },
}

在單頁(yè)面開發(fā)中,打包發(fā)布上線還需要 nginx 代理才能實(shí)現(xiàn)跨域,在 nuxt 中,打包發(fā)布上線之后,請(qǐng)求是在服務(wù)端發(fā)起的,不存在跨域問題,所以不需要在另外再做 nginx 代理。

asyncData

該方法是 Nuxt 一大賣點(diǎn), asyncData 方法會(huì)在組件(限于頁(yè)面組件)每次加載之前被調(diào)用。它可以在服務(wù)端或路由更新之前被調(diào)用,服務(wù)端渲染的能力就在這里。

注意:由于 asyncData 方法是在組件 初始化 前被調(diào)用的,所以在方法內(nèi)是沒有辦法通過 this 來引用組件的實(shí)例對(duì)象。

另外提及一點(diǎn),當(dāng) asyncData 在服務(wù)端執(zhí)行時(shí),是沒有 documentwindow 對(duì)象的。

asyncData 第一個(gè)參數(shù)被設(shè)定為當(dāng)前頁(yè)面的上下文對(duì)象,可以利用 asyncData 方法來獲取數(shù)據(jù),Nuxt.js 會(huì)將 asyncData 返回的數(shù)據(jù)融合組件 data 方法返回的數(shù)據(jù)一并返回給當(dāng)前組件。

export default {
  asyncData (ctx) {
    ctx.app // 根實(shí)例
    ctx.route // 路由實(shí)例
    ctx.params  //路由參數(shù)
    ctx.query  // 路由問號(hào)后面的參數(shù)
    ctx.error   // 錯(cuò)誤處理方法
  }
}

服務(wù)端渲染:

export default {
  data () {
    return { categoryList: [] };
  },
  async asyncData({ app }) {
    const res = await app.$myApi.GetCategory();
    return {
      categoryList: res.result.list
    };
  },
}

asyncData渲染出錯(cuò)

在使用 asyncData 時(shí)可能由于服務(wù)器錯(cuò)誤或api錯(cuò)誤導(dǎo)致頁(yè)面無(wú)法渲染,針對(duì)這種情況的出現(xiàn),我們還需要做一下處理。nuxt 提供了 context.error 方法用于錯(cuò)誤處理,在 asyncData 中調(diào)用該方法即可跳轉(zhuǎn)到錯(cuò)誤頁(yè)面。

export default {
    async asyncData({ app, error}) {
    app.$myApi.GetCategory()
      .then(res => {
        return { categoryList: res.result.list }
      })
      .catch(e => {
        error({ statusCode: 500, message: '服務(wù)器出錯(cuò)了啦~' })
      })
  },
}

當(dāng)出現(xiàn)異常時(shí)會(huì)跳轉(zhuǎn)到默認(rèn)的錯(cuò)誤頁(yè),錯(cuò)誤頁(yè)面可以通過 /layout/error.vue 自定義。

context.error的參數(shù)必須是類似{ statusCode: 500, message: '服務(wù)器開了個(gè)小差~' },statusCode必須是http狀態(tài)碼

為了方便,全局統(tǒng)一處理錯(cuò)誤方法,在 plugins 目錄下創(chuàng)建 ctx-inject.js

// plugins/ctx-inject.js
export default (ctx, inject) => {
  ctx.$errorHandler = (err) => {
    try {
      const res = err.data
      if (res) {
        // 由于nuxt的錯(cuò)誤頁(yè)面只能識(shí)別http的狀態(tài)碼,因此statusCode統(tǒng)一傳500,表示服務(wù)器異常。
        ctx.error({ statusCode: 500, message: res.resultInfo })
      } else {
        ctx.error({ statusCode: 500, message: '服務(wù)器出錯(cuò)了啦~' })
      }
    } catch {
      ctx.error({ statusCode: 500, message: '服務(wù)器出錯(cuò)了啦~' })
    }
  }
}

然后,在 nuxt.config.js 中增加配置:

export default {
  ...
  plugins: [
    ...
    '~/plugins/ctx-inject.js',
    ...
  ],
  ...
}

在頁(yè)面中使用:

export default {
  data() {
    return { categoryList: [] }
  },
  async asyncData(ctx) {
    const { app } = ctx
    // 盡量使用try catch的寫法,將所有異常都捕捉到
    try {
      const res = await app.$myApi.GetCategory()
      return {
        categoryList: res.result.list,
      }
    } catch (err) {
      ctx.$errorHandler(err)
    }
  },
}

fetch

fetch 方法用于在渲染頁(yè)面前填充應(yīng)用的狀態(tài)樹(store)數(shù)據(jù), 與 asyncData 方法類似,不同的是它不會(huì)設(shè)置組件的數(shù)據(jù)。它會(huì)在組件每次加載前被調(diào)用(在服務(wù)端或切換至目標(biāo)路由之前)。和 asyncData 一樣,第一個(gè)參數(shù)也是頁(yè)面的上下文對(duì)象,同樣無(wú)法在內(nèi)部使用 this 來獲取組件實(shí)例。

<template>
  ...
</template>

<script>
  export default {
    async fetch({ app, store, params }) {
      let res = await app.$myApi.GetCategory()
      store.commit('setCategory', res.result.list)
    }
  }
</script>

store

nuxt 中使用狀態(tài)管理,只需要在 store/ 目錄下創(chuàng)建文件即可。

store/
├── actions.js
├── getters.js
├── index.js
├── mutations.js
└── state.js
// store/actions.js
const actions = {
  changeToken({ commit }, token) {
    commit('setToken', token)
  },
  ...
}
export default actions



// store/getters.js
export const token = (state) => state.token
export const userInfo = (state) => state.userInfo
...




// store/mutations.js
const mutations = {
  setToken(state, token) {
    state.token = token
  },
  ...
}
export default mutations



// store/state.js
const state = () => ({
  token: '',
  userInfo: null,
  ...
})
export default state




// store/index.js
import state from './state'
import * as getters from './getters'
import actions from './actions'
import mutations from './mutations'

export default {
  state,
  getters,
  actions,
  mutations
}

無(wú)論使用那種模式,您的 state 的值應(yīng)該始終是 function,為了避免返回引用類型,會(huì)導(dǎo)致多個(gè)實(shí)例相互影響。

構(gòu)建部署

開發(fā)完畢后,就可以進(jìn)行打包部署了,一般來說先在本地測(cè)試一下:

$ yarn build
$ yarn start

然后,云服務(wù)器安裝 node 環(huán)境 和 pm2。

增加pm2配置,在 server/ 目錄下,新建 pm2.config.json 文件:

{
  "apps": [
    {
      "name": "my-blog",
      "script": "./server/index.js",
      "instances": 0,
      "watch": false,
      "exec_mode": "cluster_mode"
    }
  ]
}

然后,在 package.jsonscripts 配置命令:

{
  "scripts": {
    ...
    "pm2": "cross-env NODE_ENV=production pm2 start ./server/pm2.config.json",  
  }
}

把我們項(xiàng)目中 .nuxt , static , package.json , nuxt.config.js , yarn.lock 或者是 package.lock 上傳到服務(wù)器。進(jìn)入上傳的服務(wù)器目錄,安裝依賴:

$ yarn install

然后,運(yùn)行:

$ npm run pm2

在設(shè)置服務(wù)器開放 3000 端口后,就可以通過端口訪問了。后面加個(gè)端口號(hào)總歸是不合適,還需要使用 nginx 代理到默認(rèn)端口 80(http) 或 433(https)。

記錄一個(gè)小問題:3000 端口沒問題,項(xiàng)目啟動(dòng)也正常,通過 http://60.***.***.110:3000 就是訪問不了。在 nuxt.config.js 增加:

{
  ...
  server: {
    port: 3000,
    host: '0.0.0.0'
  }
}

重新啟動(dòng)項(xiàng)目即可。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容