家喻户晓,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.jsimport 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.jsimport 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.jsexport 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.jsexport 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.jsconst actions = {  changeToken({ commit }, token) {    commit('setToken', token)  },  ...}export default actions// store/getters.jsexport const token = (state) => state.tokenexport const userInfo = (state) => state.userInfo...// store/mutations.jsconst mutations = {  setToken(state, token) {    state.token = token  },  ...}export default mutations// store/state.jsconst state = () => ({  token: '',  userInfo: null,  ...})export default state// store/index.jsimport 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'  }}

重新启动我的项目即可。