乐趣区

关于vue.js:Nuxt-开发搭建博客

家喻户晓,Vue SPA 单页面利用对 SEO 不敌对,当然也有相应的解决方案。服务端渲染 (SSR) 就是罕用的一种。SSR 有利于 搜索引擎优化 (SEO, Search Engine Optimization),并且 内容达到工夫 (time-to-content)(或称之为 首屏渲染时长)也有很大的优化空间。

Nuxt.js 是一个基于 Vue.js 的轻量级利用框架, 可用来创立 服务端渲染 (SSR) 利用, 也可充当动态站点引擎生成动态站点利用, 具备优雅的代码构造分层和热加载等个性。

我的项目地址:明么的博客

初始化我的项目

运行 create-nuxt-app

通过 Nuxt 官网提供的脚手架工具 create-nuxt-app 初始化我的项目:

$ npx create-nuxt-app < 我的项目名 >

// 或者

$ yarn create nuxt-app < 我的项目名 >

我的项目配置

我的项目创立的时候会让你进行一些配置的抉择, 可依据本人须要进行抉择。

我的项目运行

运行完后,它将装置所有依赖项,下一步是启动我的项目:

$ cd <project-name>
$ yarn dev

在浏览器中,关上 http://localhost:3000

目录构造

.
├── 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

我的项目开发

我的项目启动之后,咱们就能够进行开发阶段了。

创立页面

pages 创立页面文件:

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

我的博客大略分为这几种布局形式:

在这里我创立了三种布局组件:

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

重新启动我的项目即可。

退出移动版