Nuxt.js实战和配置

前段时间刚好公司有项目使用了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 <项目名>
3106926941.jpg

大概选上面这些选项。

值得一说的是,关于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 和 contextctx-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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266