Nuxtjs实战和配置

26次阅读

共计 7524 个字符,预计需要花费 19 分钟才能阅读完成。

前段时间刚好公司有项目使用了 Nuxt.js 来搭建,而刚好在公司内部做了个分享,稍微再整理一下发出来。本文比较适合初用 Nuxt.js 的同学,主要讲下搭建过程中做的一些配置。建议初次使用 Nuxt.js 的同学先过一遍官方文档,再回头看下我这篇文章。

一、为什么要用 Nuxt.js

原因其实不用多说,就是利用 Nuxt.js 的服务端渲染能力来解决 Vue 项目的 SEO 问题。

二、Nuxt.js 和纯 Vue 项目的简单对比

1. build 后目标产物不同

vue: dist

nuxt: .nuxt

2. 网页渲染流程

vue: 客户端渲染,先下载 js 后,通过 ajax 来渲染页面;

nuxt:服务端渲染,可以做到服务端拼接好 html 后直接返回,首屏可以做到无需发起 ajax 请求;

3. 部署流程

vue:只需部署 dist 目录到服务器,没有服务端,需要用 nginx 等做 Web 服务器;

nuxt:需要部署几乎所有文件到服务器(除 node_modules,.git),自带服务端,需要 pm2 管理(部署时需要 reload pm2),若要求用域名,则需要 nginx 做代理。

4. 项目入口

vue: /src/main.js,在 main.js 可以做一些全局注册的初始化工作;
nuxt: 没有 main.js 入口文件,项目初始化的操作需要通过 nuxt.config.js 进行配置指定。

三、从零搭建一个 Nuxt.js 项目并配置

新建一个项目

直接使用脚手架进行安装:

npx create-nuxt-app < 项目名 >


大概选上面这些选项。

值得一说的是,关于Choose custom server framework(选择服务端框架),可以根据你的业务情况选择一个服务端框架,常见的就是 Express、Koa,默认是 None,即 Nuxt 默认服务器,我这里选了Express

  • 选择默认的 Nuxt 服务器,不会生成 server 文件夹,所有服务端渲染的操作都是 Nuxt 帮你完成,无需关心服务端的细节,开发体验更接近 Vue 项目,缺点是无法做一些服务端定制化的操作。
  • 选择其他的服务端框架,比如 Express,会生成server 文件夹,帮你搭建一个基本的 Node 服务端环境,可以在里面做一些 node 端的操作。比如我公司业务需要(解析 protobuf)使用了Express,对真正的服务端 api 做一层转发,在 node 端解析 protobuf 后,返回 json 数据给客户端。

还有 Choose Nuxt.js modules(选择 nuxt.js 的模块),可以选axiosPWA, 如果选了 axios,则会帮你在 nuxt 实例下注册 $axios,让你可以在.vue 文件中直接this.$axios 发起请求。

开启 eslint 检查

nuxt.config.js 的 build 属性下添加:

  build: {extend (config, ctx) {
      // Run ESLint on save
      if (ctx.isDev && ctx.isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }
    }
  }

这样开发时保存文件就可以检查语法了。nuxt 默认使用的规则是 @nuxtjs(底层来自 eslint-config-standard),规则配置在/.eslintrc.js:

module.exports = {
  root: true,
  env: {
    browser: true,
    node: true
  },
  parserOptions: {parser: 'babel-eslint'},
  extends: [
    '@nuxtjs', // 该规则对应这个依赖:@nuxtjs/eslint-config
    'plugin:nuxt/recommended'
  ],
  // add your custom rules here
  rules: {'nuxt/no-cjs-in-config': 'off'}
}

如果不习惯用 standard 规则的团队可以将 @nuxtjs 改成其他的。

使用 dotenv 和 @nuxtjs/dotenv 统一管理环境变量

在 node 端,我们喜欢使用 dotenv 来管理项目中的环境变量,把所有环境变量都放在根目录下的 .env 中。

  • 安装:
npm i dotenv
  • 使用:
  1. 在根目录下新建一个 .env 文件,并写上需要管理的环境变量,比如服务端地址APIHOST:
APIHOST=http://your_server.com/api
  1. /server/index.js 中使用(该文件是选 Express 服务端框架自动生成的):
require('dotenv').config()

// 通过 process.env 即可使用
console.log(process.env.APIHOST) // http://your_server.com/api

此时我们只是让服务端可以使用 .env 的文件而已,Nuxt 客户端并不能使用 .env,按 Nuxt.js 文档所说,可以将客户端的环境变量放置在nuxt.config.js 中:

module.exports = {
  env: {baseUrl: process.env.BASE_URL || 'http://localhost:3000'}
}

但如果 node 端和客户端需要使用同一个环境变量时 (后面讲到 API 鉴权时会使用同一个 SECRET 变量),就需要同时在nuxt.config.js.env维护这个字段,比较麻烦,我们更希望环境变量只需要在一个地方维护,所以为了解决这个问题,我找到了 @nuxtjs/dotenv 这个依赖,它使得 nuxt 的客户端也可以直接使用.env,达到了我们的预期。

  • 安装:
npm i @nuxtjs/dotenv

客户端也是通过 process.env.XXX 来使用,不再举例啦。

这样,我们通过 dotenv@nuxtjs/dotenv这两个包,就可以统一管理开发环境中的变量啦。

另外,@nuxtjs/dotenv允许打包时指定其他的 env 文件。比如,开发时我们使用的是 .env,但我们打包的线上版本想用其他的环境变量,此时可以指定 build 时用另一份文件如/.env.prod,只需在nuxt.config.js 指定:

module.exports = {
    modules: [['@nuxtjs/dotenv', { filename: '.env.prod'}] // 指定打包时使用的 dotenv
  ],
}

@nuxtjs/toast 模块

toast 可以说是很常用的功能,一般的 UI 框架都会有这个功能。但如果你的站点没有使用 UI 框架,而 alert 又太丑,不妨引入该模块:

npm install @nuxtjs/toast

然后在 nuxt.config.js 中引入

module.exports = {
    modules: [
    '@nuxtjs/toast',
    ['@nuxtjs/dotenv', { filename: '.env.prod'}] // 指定打包时使用的 dotenv
  ],
  toast: {// toast 模块的配置
    position: 'top-center', 
    duration: 2000
  }
}

这样,nuxt 就会在全局注册 $toast 方法供你使用,非常方便:

this.$toast.error('服务器开小差啦~~')
this.$toast.error('请求成功~~')

API 鉴权

对于某些敏感的服务,我们可能需要对 API 进行鉴权,防止被人轻易盗用我们 node 端的 API,因此我们需要做一个 API 的鉴权机制。常见的方案有 jwt,可以参考一下阮老师的介绍:《JSON Web Token 入门教程》。如果场景比较简单,可以自行设计一下,这里提供一个思路:

  1. 客户端和 node 端在环境变量中声明一个秘钥:SECRET=xxxx,注意这个是保密的;
  2. 客户端发起请求时,将当前时间戳 (timestamp) 和SECRET通过某种算法,生成一个 signature,请求时带上timestampsignature
  3. node 接收到请求,获得 timestampsignature,将 timestamp 和秘钥用同样的算法再生成一次签名_signature
  4. 对比客户端请求的 signature 和 node 用同样的算法生成的_signature,如果一致就表示通过,否则鉴权失败。

具体的步骤:

客户端对 axios 进行一层封装:

import axios from 'axios'
import sha256 from 'crypto-js/sha256'
import Base64 from 'crypto-js/enc-base64'
// 加密算法,需安装 crypto-js
function crypto (str) {const _sign = sha256(str)
  return encodeURIComponent(Base64.stringify(_sign))
}

const SECRET = process.env.SECRET

const options = {headers: { 'X-Requested-With': 'XMLHttpRequest'},
  timeout: 30000,
  baseURL: '/api'
}

// The server-side needs a full url to works
if (process.server) {options.baseURL = `http://${process.env.HOST || 'localhost'}:${process.env.PORT || 3000}/api`
  options.withCredentials = true
}

const instance = axios.create(options)
// 对 axios 的每一个请求都做一个处理,携带上签名和 timestamp
instance.interceptors.request.use(
  config => {const timestamp = new Date().getTime()
    const param = `timestamp=${timestamp}&secret=${SECRET}`
    const sign = crypto(param)
    config.params = Object.assign({}, config.params, { timestamp, sign})
    return config
  }
)

export default instance

接着,在 server 端写一个鉴权的中间件,/server/middleware/verify.js

const sha256 = require('crypto-js/sha256')
const Base64 = require('crypto-js/enc-base64')

function crypto (str) {const _sign = sha256(str)
  return encodeURIComponent(Base64.stringify(_sign))
}
// 使用和客户端相同的一个秘钥
const SECRET = process.env.SECRET

function verifyMiddleware (req, res, next) {const { sign, timestamp} = req.query
  // 加密算法与请求时的一致
  const _sign = crypto(`timestamp=${timestamp}&secret=${SECRET}`)
  if (_sign === sign) {next()
  } else {res.status(401).send({message: 'invalid token'})
  }
}

module.exports = {verifyMiddleware}

最后,在需要鉴权的路由中引用这个中间件, /server/index.js

const {Router} = require('express')
const {verifyMiddleware} = require('../middleware/verify.js')
const router = Router()

// 在需要鉴权的路由加上
router.get('/test', verifyMiddleware, function (req, res, next) {res.json({name: 'test'})
})

静态文件的处理

根目录下有个 /static 文件夹,我们希望这里面的文件可以直接通过 url 访问,需要在 /server/index.js 中加入一句:

const express = require('express')
const app = express()

app.use('/static', express.static('static'))

四、Nuxt 开发相关

生命周期

Nuxt 扩展了 Vue 的生命周期,大概如下:

export default {middleware () {}, // 服务端
  validate () {}, // 服务端
  asyncData () {}, // 服务端
  fetch () {}, // store 数据加载
  beforeCreate () {  // 服务端和客户端都会执行},
  created () { // 服务端和客户端都会执行},
  beforeMount () {}, 
  mounted () {} // 客户端
}

asyncData

该方法是 Nuxt 最大的一个卖点,服务端渲染的能力就在这里,首次渲染时务必使用该方法。
asyncData 会传进一个 context 参数,通过该参数可以获得一些信息,如:

export default {asyncData (ctx) {
    ctx.app // 根实例
    ctx.route // 路由实例
    ctx.params  // 路由参数
    ctx.query  // 路由问号后面的参数
    ctx.error   // 错误处理方法
  }
}

渲染出错和 ajax 请求出错的处理

  • asyncData 渲染出错

使用 asyncData 钩子时可能会由于服务器错误或 api 错误导致无法渲染,此时页面还未渲染出来,需要针对这种情况做一些处理,当遇到 asyncData 错误时,跳转到错误页面,nuxt 提供了 context.error 方法用于错误处理,在 asyncData 中调用该方法即可跳转到错误页面。

export default {async asyncData (ctx) {
        // 尽量使用 try catch 的写法,将所有异常都捕捉到
        try {throw new Error()
        } catch {ctx.error({statusCode: 500, message: '服务器开小差了~'})
        }
    }
}

这样,当出现异常时会跳转到默认的错误页,错误页面可以通过 /layout/error.vue 自定义。

这里会遇到一个问题,context.error的参数必须是类似 {statusCode: 500, message: '服务器开小差了~'}statusCode 必须是 http 状态码,
而我们服务端返回的错误往往有一些其他的自定义代码,如{resultCode: 10005, resultInfo: '服务器内部错误'},此时需要对返回的 api 错误进行转换一下。

为了方便,我引入了 /plugins/ctx-inject.js 为 context 注册一个全局的错误处理方法:context.$errorHandler(err)。注入方法可以参考:注入 $root 和 context,ctx-inject.js:

// 为 context 注册全局的错误处理事件
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']
}

注入完毕,我们就可以在 asyncData 介个样子使用了:

export default {async asyncData (ctx) {
        // 尽量使用 try catch 的写法,将所有异常都捕捉到
        try {throw new Error()
        } catch(err) {ctx.$errorHandler(err)
        }
    }
}
  • ajax 请求出错

对于 ajax 的异常,此时页面已经渲染,出现错误时不必跳转到错误页,可以通过this.$toast.error(res.message) toast 出来即可。

loading 方法

nuxt 内置了页面顶部 loading 进度条的样式
推荐使用,提供页面跳转体验。
打开:this.$nuxt.$loading.start()
完成:this.$nuxt.$loading.finish()

打包部署

一般来说,部署前可以先在本地打包,本地跑一下确认无误后再上传到服务器部署。命令:

// 打包
npm run build
// 本地跑
npm start

除 node_modules,.git,.env,将其他的文件都上传到服务器,然后通过 pm2 进行管理, 可以在项目根目录建一个 pm2.json 方便维护:

{
  "name": "nuxt-test",
  "script": "./server/index.js",
  "instances": 2,
  "cwd": "."
}

然后配置生产环境的环境变量,一般是直接用 .env.prod 的配置:cp ./.env.prod ./.env
首次部署或有新的依赖包,需要在服务器上 npm install 一次,然后就可以用 pm2 启动进程啦:

// 项目根目录下运行
pm2 start ./pm2.json

需要的话,可以设置开机自动启动 pm2: pm2 save && pm2 startup
需要注意的是,每次部署都得重启一下进程:pm2 reload nuxt-test

五、最后

Nuxt.js 引入了 Node,同时 nuxt.config.js 替代了 main.js 的一些作用,目录结构和 vue 项目都稍有不同,增加了很多的约定,对于初次接触的同学可能会觉得非常陌生,更多的内容还是得看一遍官方的文档。

demo 源码: fengxianqi/front_end-demos/src/nuxt-test。

正文完
 0