前段时间刚好公司有项目使用了 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 的模块),可以选axios
和PWA
, 如果选了 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
- 使用:
- 在根目录下新建一个
.env
文件,并写上需要管理的环境变量,比如服务端地址APIHOST
:
APIHOST=http://your_server.com/api
- 在
/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 入门教程》。如果场景比较简单,可以自行设计一下,这里提供一个思路:
- 客户端和 node 端在环境变量中声明一个秘钥:SECRET=xxxx,注意这个是保密的;
- 客户端发起请求时,将当前时间戳 (timestamp) 和
SECRET
通过某种算法,生成一个signature
,请求时带上timestamp
和signature
; - node 接收到请求,获得
timestamp
和signature
,将timestamp
和秘钥用同样的算法再生成一次签名_signature
- 对比客户端请求的
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。