Nuxt 開發搭建博客

衆所周知,Vue SPA單頁面應用對SEO不友好,固然也有相應的解決方案。 服務端渲染 (SSR) 就是經常使用的一種。 SSR 有利於 搜索引擎優化(SEO, Search Engine Optimization) ,而且 內容到達時間(time-to-content) (或稱之爲首屏渲染時長)也有很大的優化空間。css

Nuxt.js 是一個基於 Vue.js 的輕量級應用框架,可用來建立 服務端渲染 (SSR) 應用,也可充當靜態站點引擎生成靜態站點應用,具備優雅的代碼結構分層和熱加載等特性。html

項目地址:明麼的博客vue

初始化項目

運行 create-nuxt-app

經過 Nuxt 官方提供的腳手架工具 create-nuxt-app 初始化項目:node

$ npx create-nuxt-app <項目名>

// 或者ios

$ yarn create nuxt-app <項目名>

項目配置

項目建立的時候會讓你進行一些配置的選擇,可根據本身須要進行選擇。
imagenginx

項目運行

運行完後,它將安裝全部依賴項,下一步是啓動項目:git

$ cd <project-name>
$ yarn dev

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

目錄結構

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

項目開發

項目啓動以後,咱們就能夠進行開發階段了。vue-router

建立頁面

pages建立頁面文件:npm

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

Nuxt.js 預設了利用 Vue.js 開發服務端渲染的應用所須要的各類配置。因此不須要再安裝 vue-router 了,他會依據 pages 目錄結構自動生成 vue-router 模塊的路由配置。頁面之間使用路由,官方推薦使用 <nuxt-link> 標籤,與 <router-link> 的使用方式是同樣的。上面建立的目錄結構將會生成對應的路由配置表:

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'
    }
  ]
}

組件部分

組件這一塊劃分爲baseframeworkpage三個目錄:

components/
├── base    基本組件
├── framework    佈局相關組件
└── page/    各個頁面下的組件
    ├── home
    └── ...

這裏須要注意在開發 VUE SPA 應用時咱們有時候會把頁面組件放在 pages 下,我將頁面下的組件所有放到了components下,由於 Nuxt.js 框架會讀取 pages 目錄下全部的 .vue 文件並自動生成對應的路由配置。

資源的存放

官方介紹的很詳細,資源的存放有兩個目錄:staticassets

static : 用於存放應用的靜態文件,此類文件不會調用 Webpack 進行構建編譯處理。服務器啓動的時候,該目錄下的文件會映射至應用的根路徑 / 下。
舉個例子: /static/banner.png 映射至 /banner.png

assets : 用於組織未編譯的靜態資源如 LESSSASSJS

別名

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

爲了方便引用,nuxt 提供了兩個別名,若是你須要引入 assets 或者 static 目錄, 使用 ~/assets/your_image.png~/static/your_image.png 方式。

全局樣式

這裏我選用 LESS 預處理語言,安裝:

$ yarn add less less-loader -D

assets/css/ 建立 .less 文件, 經過一個文件引入:

// 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 全局變量

在使用預處理語言的時候,咱們確定會使用到變量,以方便統一管理顏色、字體大小等。

首先定義好變量文件 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

個人博客大概分爲這幾種佈局方式:
image

在這裏我建立了三種佈局組件:

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

admin.vue: 後臺管理模塊的佈局
user.vue: 我的中心模塊的佈局
default.vue: 默認的佈局

default.vue 舉例,我把 導航頁腳 放到了一個組件 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>

而後在頁面中使用:

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

<template>
  <!-- Your template -->
</template>
<script>
  export default {
    layout: 'default'
    // 指定佈局,不指定的話將會使用默認佈局: layouts/default.vue
    // 其實我這裏指不指定均可以哈哈。
  }
</script>

關於頁面上路由的跳轉,官方推薦使用 <nuxt-link>,這裏 <nuxt-link><a> 仍是有區別的,nuxt-link 走的是 vue-router 的路由,即頁面爲單頁面,瀏覽器不會重定向。而 <a>標籤走的是 window.location.href ,每次點擊a標籤後頁面,都會進行一次服務端渲染。

全局過濾器

 plugins/  目錄下,新建  filters.js,好比說咱們要對時間進行一個格式化處理 :

Day.js :一個輕量的處理時間和日期的 JavaScript 庫

$ yarn add dayjs
import Vue from 'vue'
import dayjs from 'dayjs'
// 時間格式化
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 方法設置當前頁面的頭部標籤。

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

<script>
  export default {
    ...
    head() {
      return {
        title: '明麼的博客',
        meta: [
          {
            hid: 'description',
            name: 'description',
            content: 'My custom description'
          }
        ]
      }
    }
  }
</script>
注意:爲了不子組件中的 meta 標籤不能正確覆蓋父組件中相同的標籤而產生重複的現象,建議利用 hid 鍵爲 meta 標籤配一個惟一的標識編號。

若是頁面比較多的話,每一個頁面都須要寫 head 對象,就會有些的繁瑣。能夠藉助 nuxtplugin 機制,將其封裝成一個函數,並注入到每個頁面當中:

// 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'
  ],
  ...
}

在頁面中使用:

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

axios 請求數據

請求數據,在初始化項目的時候已經選擇了Axios,就不須要再另行安裝了,能夠查看 nuxt.config.js 中已經配置好了:

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

頁面中經過 this.$axios.$get 來獲取數據,不須要在每一個頁面都單獨引入 axios.
可是通常來講咱們會對 axios 作一下封裝,集中處理一些數據或者是錯誤信息。
plugins 目錄下新建 axios.jsapi-repositories.js,下面是個人一些簡單的配置:

//  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 請求,參數序列化
    if (config.method === 'get') {
      config.paramsSerializer = function(params) {
        return qs.stringify(params, { arrayFormat: 'repeat' }) // params是數組類型如arr=[1,2],則轉換成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('登陸過時了,請從新登陸!')
      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/'
  },
}

這樣就能夠直接在頁面中使用了:

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': ''
      }
    },
    ...
  },
}

在單頁面開發中,打包發佈上線還須要 nginx 代理才能實現跨域,在 nuxt 中,打包發佈上線以後,請求是在服務端發起的,不存在跨域問題,因此不須要在另外再作 nginx 代理。

asyncData

該方法是 Nuxt 一大賣點, asyncData 方法會在組件(限於頁面組件)每次加載以前被調用。它能夠在服務端或路由更新以前被調用,服務端渲染的能力就在這裏。

注意:因爲 asyncData 方法是在組件 初始化 前被調用的,因此在方法內是沒有辦法經過 this 來引用組件的實例對象。

另外說起一點,當 asyncData 在服務端執行時,是沒有 documentwindow 對象的。

asyncData 第一個參數被設定爲當前頁面的上下文對象,能夠利用 asyncData 方法來獲取數據,Nuxt.js 會將 asyncData 返回的數據融合組件 data 方法返回的數據一併返回給當前組件。

export default {
  asyncData (ctx) {
    ctx.app // 根實例
    ctx.route // 路由實例
    ctx.params  //路由參數
    ctx.query  // 路由問號後面的參數
    ctx.error   // 錯誤處理方法
  }
}

服務端渲染:

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

asyncData渲染出錯

在使用 asyncData 時可能因爲服務器錯誤或api錯誤致使頁面沒法渲染,針對這種狀況的出現,咱們還須要作一下處理。nuxt 提供了 context.error 方法用於錯誤處理,在 asyncData 中調用該方法便可跳轉到錯誤頁面。

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

當出現異常時會跳轉到默認的錯誤頁,錯誤頁面能夠經過 /layout/error.vue 自定義。

context.error的參數必須是相似 { statusCode: 500, message: '服務器開了個小差~' }statusCode必須是 http狀態碼

爲了方便,全局統一處理錯誤方法,在 plugins 目錄下建立 ctx-inject.js

// plugins/ctx-inject.js
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',
    ...
  ],
  ...
}

在頁面中使用:

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 方法用於在渲染頁面前填充應用的狀態樹(store)數據, 與 asyncData 方法相似,不一樣的是它不會設置組件的數據。它會在組件每次加載前被調用(在服務端或切換至目標路由以前)。和 asyncData 同樣,第一個參數也是頁面的上下文對象,一樣沒法在內部使用 this 來獲取組件實例。

<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 中使用狀態管理,只須要在 store/ 目錄下建立文件便可。

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
}
不管使用那種模式,您的 state 的值應該始終是 function,爲了不返回引用類型,會致使多個實例相互影響。

構建部署

開發完畢後,就能夠進行打包部署了,通常來講先在本地測試一下:

$ yarn build
$ yarn start

而後,雲服務器安裝 node 環境 和 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",  
  }
}

把咱們項目中 .nuxt , static , package.json , nuxt.config.js , yarn.lock 或者是 package.lock 上傳到服務器。進入上傳的服務器目錄,安裝依賴:

$ yarn install

而後,運行:

$ npm run pm2

在設置服務器開放 3000 端口後,就能夠經過端口訪問了。後面加個端口號總歸是不合適,還須要使用 nginx 代理到默認端口 80(http) 或 433(https)。

記錄一個小問題:3000 端口沒問題,項目啓動也正常,經過 http://60.***.***.110:3000 就是訪問不了。在 nuxt.config.js 增長:
{
  ...
  server: {
    port: 3000,
    host: '0.0.0.0'
  }
}

從新啓動項目便可。

相關文章
相關標籤/搜索