一、Vue服务端渲染(SSR)
1.1 SSR是什么
1.1.1 MPA
传统的 web 开发(如 JSP):客户端向服务端发送请求,服务端查询数据库,拼接 HTML 字符串(模板),通过一系列的数据处理之后,把整理好的 HTML 返回给客户端,浏览器相当于打开了一个页面。MPA 的应用构成是由多个完整页面构成的。
1.1.2 SPA
SPA 渲染过程:由客户端访问 URL 发送请求到服务端,返回 HTML 结构。但是 SPA 的返回的 HTML 结构只是有一个基本结构的外壳。客户端接收到返回结果之后开始渲染 HTML,之后可能会再次向服务端发送数据请求,服务端返回 json 格式数据由客户端接收后,完成最终渲染。页面之间跳转是把页面片段删除或者隐藏。
1.1.3 SSR
SPA虽然给服务器减轻了压力,但是也是有缺点的:
- 首屏渲染时间比较长:必须等待
JS加载完毕,并且执行完毕,才能渲染出首屏。 SEO不友好:爬虫只能拿到一个div元素,认为页面是空的,不利于SEO。
MPA缺点:
- 页面之间切换加载慢,不流畅。(其页面之间的跳转是由一个页面跳转到另一个页面)
为了解决如上两个问题,出现了
SSR解决方案,后端渲染出首屏的DOM结构返回,前端拿到内容带上首屏,后续的页面操作,再用单页面路由和渲染,称之为服务端渲染(SSR)。
SSR 渲染流程是这样的,客户端发送 URL 请求到服务端,服务端读取对应的 URL 的模板信息,在服务端做出 HTML 和数据的渲染,渲染完成之后返回整个 HTML 结构给客户端。所以用户在浏览首屏的时候速度会比较快,并不是做了 SSR 我们的页面就不属于 SPA 应用了,它仍然是一个独立的 SPA 应用。SSR 是处于 MPA 与 SPA 应用之间的一个折中的方案,仅是首屏时候在服务端做出了渲染,其他页面还是需要在客户端渲染的。
1.2 SSR的缺点
- 开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数 (
lifecycle hook) 中使用;一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行。 - 涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于
Node.jsserver运行环境。 - 更多的服务器端负载。在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源,因此如果你预料在高流量环境 (
high traffic) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。
二、目录结构
2.1 .nuxt
构建目录,是 Nuxt 项目 nuxt build 或者 nuxt dev 后动态生成的隐藏文件,每次运行都会重新生成,git 提交应该自动忽略此目录。
2.2 assets
与 vue-cli 的 assets 用法一致,用于存放静态资源文件,该目录下可存放全局 style 文件、image、font 等。
2.3 components
要从组件中的 API 访问异步数据,可以使用 fetch(),asyncData() 是不适用于组件的。组件可以分为两部分,业务组件与公共组件。在 components 下建立的 .vue 文件是会自动嵌套目录注册到全局的。
2.4 layouts
目录中的每个文件(顶级) layouts 都将创建一个可通过 layout 页面组件中的属性访问的自定义布局 。
2.5 middleware
路由切换之前运行的自定义函数。全局的中间件可以防止 nuxt.config.js 中配置。
2.6 pages
页面组件,Nuxt.js 读取此目录中的所有文件并自动创建路由器配置。其中项目启动默认地址访问会直接定位到 pages 下的 index.vue。
2.7 plugins
全局注入自定义插件的主要路径。
2.8 static
该目录下的文件是不会被 wabpack 处理的,它们会被直接复制到最终的打包目录下,并可以直接通过项目的根 URL 访问。
2.8 store
Vuex 状态管理器。
三、生命周期
通过上面的流程图可以看出,当一个客户端请求进入的时候,服务端有通过 nuxtServerInit 这个命令执行在 store 的 action 中,在这里接收到客户端请求的时候,可以将一些客户端信息存储到 store 中,比如项目中的用户信息。nuxtServerInit 钩子只有 store 目录下的 index.js 有效,其他模块文件是无法调用的。
之后使用了中间件机制,中间件其实就是一个函数,会在每个路由执行之前去执行,可以理解为是路由器的拦截器的作用。设置 middleware 的位置有三个,一是 nuxt.config.js 全局设置,二是 layout 组件引用 middleware 属性,三是 pages 组件引用 middleware 属性。执行顺序从全局到布局再到页面。
然后在 validate 执行的时候对客户端携带的参数进行校验,在 asyncData 与 fetch 进入正式的渲染周期,asyncData 向服务端获取数据,把请求到的数据合并到data 中;
然后进入 Vue 的生命周期 beforeCreate & created & ……。在此 beforeMount之前都是无法获取到 window 对象的。
假设 page下有一页面,配置了以下属性:
<script>
export default {
middleware: "middleware1", // 服务端
validate({ params }) { // 服务端
console.log("page validate")
},
asyncData() { // 服务端
console.log("page asyncData")
},
fetch(){ // 服务端 store 数据加载
console.log("fetch")
},
head() {
console.log("head")
},
beforeCreate() { // 服务端和客户端都会执行
console.log("beforeCreate")
},
created() { // 服务端和客户端都会执行
console.log("created")
},
beforeMount() { // 客户端
console.log("beforeMount")
},
mounted() { // 客户端
console.log("mounted")
},
}
</script>
nuxt.config.js 有配置 plugins 与 route
plugins: [
'@/plugins/plugin1',
'@/plugins/plugin2',
],
router: {
middleware: ['middleware2'],
},
我们可以得出以下打印日志:
首先执行的是中间件以及插件的注册 =》状态管理器 store/index.js/action/nuxtServerInit =》全局配置的 middleware =》 页面 middleware =》页面 validate 校验 =》 异步数据 asyncData 执行 =》 beforeCreate =》 created =》 fetch 填充 store =》 head 设置头部 =》 beforeMount =》mounted
四、上下文 context
context 对象在特定的 Nuxt 函数中可用如 asyncData、 plugins、 middleware 和 nuxtServerInit。我们还可以通过注册 plugins 向 context.app 中注入我们常用的方法,如:封装后的 axios 实例等。
| 属性 | 类型 | 描述 |
|---|---|---|
| app | vue根实例 | 包含所有插件的 Vue 根实例。由于在服务端的生命周期中,如 asyncData 钩子,无 this 去获取实例上的方法和属性,便由app来访问 |
| isDev | boolean | 是否是开发 dev 模式 |
| isHMR | boolean | 是否是通过模块热替换 |
| route | Vue router | 路由实例 |
| query | object | router.query |
| params | object | router.params |
| from | 路由from路径 | |
| req | http.Request | 服务端发送http的请求 |
| res | http.Response | 服务端发送http的响应 |
| store | Vuex数据 | Vuex.Store 实例 |
| error | Function | 用这个方法展示错误页:error(params) 。params 参数应该包含 statusCode 和 message 字段 |
| env | Object | nuxt.config.js 中配置的环境变量 |
| redirect | Function | 用这个方法重定向用户请求到另一个路由。状态码在服务端被使用,默认 302。redirect([status,] path [, query]) |
五、路由 router
5.1 生成规则
Nuxt.js 依据 pages 目录结构自动生成 vue-router 模块的路由配置。假设 pages 的目录结构如下:
pages/
--| firstpage/
-----| index.vue
--| index.vue
那么,Nuxt.js 自动生成的路由配置如下:
router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'firstpage',
path: '/firstpage',
component: 'pages/firstpage/index.vue'
}
]
}
5.2 动态路由
要创建动态路由,需要在 .vue 文件名或目录名之前添加下划线 ( _ ) 。假设 pages 的目录结构如下:
pages/
--| secondpage/
-----| _id.vue
--| index.vue
会自动生成:
router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'secondpage-id',
path: '/secondpage/:id?',
component: 'pages/secondpage/_id.vue'
}
]
}
在 page 的 vue 文件中,可以通过 this.$route.params 去获取当前路由参数。
5.3 路由守卫
除了中间件拦截以外,也可以使用 Vue Router 的拦截方式。区别在于,中间件拦截在服务端渲染进行的,而Vue Router 的拦截是在客户端进行拦截的。中间件拦截将在下文中间件中介绍,这里介绍 Vue Router 拦截方式。
前提:
在 plugins 新增文件 promission.js
import { tokenKey, whiteList, VUE_APP_LOGINURL } from '~/setting/interface'
export default ({ app, store }) => {
app.router.beforeEach(async (to, from, next) => {
// 需要判断是否客户端
if (!process.client) return
// $cookies 是注册到plugins的插件,用于cookies的操作
const token = app.$cookies.get(tokenKey)
const hasPermission = store.state.user.userInfo && store.state.user.userInfo.userId
if (token) {
// 已经获取用户信息
if (hasPermission) {
next()
} else {
const res = await store.dispatch('user/getUserInfo')
next()
}
} else if (whiteList.includes(to.path)) {
next()
} else {
// 这里也可以使用 redirect 方法
window.location.href = `${VUE_APP_LOGINURL}/?redirect_uri=${window.location.href}`
}
})
}
之后在 nuxt.config.js 添加此插件便实现了前端鉴权
plugins: [
'~/plugins/promission.js',
],
5.4 拓展路由
有多种方法可以使用 Nuxt 扩展路由:
router-extras-module自定义页面中的路由参数- 组件
@nuxtjs/router覆盖Nuxt路由器并编写自己的router.js文件 - 在
nuxt.config.js使用router.extendRoutes属性重新配置
router: {
// 扩展路由,除了pages文件夹下自动生成的路由,可添加一些自定义路由
extendRoutes(routes, resolve) {
routes.push(
{
path: '/error',
name: 'error',
component: resolve(__dirname, 'layouts/error.vue'),
},
)
},
},
router: {
// 完全自定义路由,pages下的文件不生成路由
extendRoutes(routes, resolve) {
return [
{
name: 'index',
path: '/',
component: resolve(__dirname, 'pages/index.vue'),
meta: {
title: '首页',
},
}
]
},
},
六、布局 layouts
6.1 默认布局
可以通过添加 layouts/default.vue 文件来扩展主布局 。它将用于所有未指定布局的页面。确保 <Nuxt> 在创建布局时添加 组件以实际包含页面组件。<Nuxt> 相当于 vue-router 中的 router-view;<NuxtLink> 则相当于 router-link。
如:
<template>
<div>
<NuxtLink to='/'> back home page </NuxtLink>
<div>My blog navigation bar here</div>
<Nuxt />
</div>
</template>
6.2 错误页面 error.vue
错误页面是一个页面组件,定义在 layout.error.vue。它在非服务端抛出错误时始终显示。路由不会替换,是直接把error 页面的全部 HTML 替换掉当前页面。也可以使用 error() 方法调起错误页。
error({ statusCode: 500, message: "服务器错误" })
6.3 其他布局
在 layouts 文件夹下建立的 vue 文件,可以用于 pages 下页面的 layouts 属性,如此可以在不同的页面设立不同的布局。如:在 layouts 下有文档布局 document.vue;
需要引用此布局的 page,可以设置
layouts: "document",
七、视图 pages
每个 Page 组件都是一个 Vue 组件。Nuxt 路由是由 page 目录自动生成的,如果想忽略页面以便它们不包含在生成的router.js文件中,那么您可以通过在它们前面加上-。
例如,pages/-about.vue 将被忽略。
除了 vue 支持的属性外,Nuxt.js 添加了特殊的属性和功能。按照执行先后顺序给大家介绍一下。
7.1 validate
如果是动态页面,如详情页
pages/
--| secondpage/
-----| _id.vue
需要获取到 params 里有 id 才能进入页面。我们可以在 validate() 钩子 进行校验。返回 true 则会进入下一步,否则会自动显示 error 页。
validate({ params }) {
if (!params.id) return false
return true
},
7.2 asyncData
支持异步数据处理,另外该方法的第一个参数为当前页面组件的上下文对象 context。返回的对象将与数据对象 data 合并。如以下方法,在 asyncData 请求数据将替换 data 函数里的 form 数据。
async asyncData({ app, params }) {
const res = await app.$api.fetchIsp({ id: params.id })
return {
form: res,
}
},
data() {
return {
form: {
name: '',
},
}
},
asyncData() 首屏时是在服务端执行的,后续在客户端执行。
7.3 fetch
fetch() 与 asyncData() 类似,可以用于获取异步数据。 fetch 得到的数据不用于替换 data 的值,而是用于填充 store。当然,我觉得这个属性有点废,因为我想获取数据填充 store 完全可以在 asyncData 里获取。
7.3 head
为当前页面设置特定的 head,可以是对象,也可以是函数,return 一个对象。head 是可以通过 this 获取到 vue 实例的,所以完全可以在 asyncData 返回值,替换 data 之后,根据返回数据进行设置 head 标签。
7.4 layout
指定布局目录中定义的目录
7.5 middleware
定义此页面的中间件。中间件将在呈现页面之前被调用。
7.6 scrollToTop
scrollToTop 属性可让您告诉 Nuxt.js 在呈现页面之前滚动到顶部。默认情况下,当您转到另一个页面时,Nuxt.js 会滚动到顶部,但是对于子路由,Nuxt.js 会保持滚动位置。如果你想告诉 Nuxt.js 在渲染你的子路由时滚动到顶部,设置 scrollToTop 为 true。
八、组件 components
8.1 业务组件
由于 Nuxt 的路由由 pages 生成,所以现在不能随意在 page 文件夹下建立 components 文件夹,再建立对应的组件。而业务组件需要按需引入,首先需要在 nuxt.config.js 设置 components: false, 否则在 components 文件夹下创建的组件会直接挂载在 vue 实例上,直接全局注册
可以在 components 文件夹下建立三个文件夹,一个 business 用于装载业务组件,一个 common 用于装载公共组件,一个 layouts 用于装载布局组件。
components/
--| business/
-----| button.vue
在 layout 引入与之前 vue 项目引入一致。
<template>
<div class="app">
<Button @click="handlerClick">按钮</Button>
</div>
</template>
<script>
import Button from '~/components/business/firstpage/Button.vue'
export default {
name: 'Firstpage',
components: {
Button,
},
methods: {
handlerClick() {
// do something ...
},
},
}
</script>
8.2 公共组件
如 8.1 所讲,我们建立公共组件文件夹如下
components/
--| common/
-----| button.vue
那怎么全局注册 common 文件夹下所有组件呢?
可以在 components 下添加 js 文件,我们命名为 componentsInstall.js(除了 index 以外的名称哦)
内容如下:
export default {
install(Vue) {
// 批量注册公用组件
// 如果第二个参数为 true ,程序将会遍历 components/common 目录下的子目录,并引用其中的 .vue 文件
const components = require.context('~/components/common', true, /.vue$/)
components.keys().forEach((path) => {
// 组件实例
const reqCom = components(path)
const fileName = path.replace(/(.*/)*([^.]+).*/gi, '$2') // 获取组件文件名
// 组件挂载
// reqCom.default.name为组件内部的name属性,当未命名时自动默认用文件名称作为组件名
// nuxt若没有定义组件name属性,会自动根据目录填充的
Vue.component(reqCom.default.name || fileName, reqCom.default || reqCom)
})
},
}
并且在 plugins 加入 vue-global.js
import Vue from 'vue'
import componentsInstall from '~/components/componentsInstall'
Vue.use(componentsInstall)
最后在 nuxt.config.js 的 plugin 加上 vue-global.js 便可以全局引用 common 文件夹下的文件了。
plugins: [
'~/plugins/vue-global.js',
],
九、插件 plugin
plugins 是全局注入的主要途径,有时候我们希望在整个应用程序中使用某个函数,需要注入到 vue 实例(客户端),context(服务端),那么我们就会使用上 plugins。下列介绍三个我们常用的:
9.1 按需引入 UI 框架
因为 Nuxt 项目主要是针对C端产品的,而C端产品需要用到比较轻量级的UI库。然而往往像 popup、$message、form 这些如果要我们一一封装往往增大了不少开发压力。所以这里选用从 ant-design-vue 按需引入常用组件。
1.下面先安装 antd 和 babel-plugin-import
yarn add ant-design-vue --save
yarn add babel-plugin-import --save-dev
2.在 nuxt.config.js 同级目录下建立 .babelrc 文件,文件内容:
{
"plugins": [
[
"import",
{
"libraryName": "antd",
"libraryDirectory": "lib",
"style": "css"
}
]
]
}
3.在 nuxt.config.js 中引入了 antd 样式
css: [ 'ant-design-vue/dist/antd.css'],
如果没有使用 babel-plugin-import 引入,这样配置相当于把整个 antd 的样式全局引入了哦!
4.在 plugins 建立文件 antd-ui.js 按需引入我们需要的组件:
import Vue from 'vue'
import message from 'ant-design-vue/lib/message';
import Button from 'ant-design-vue/lib/input';
Vue.use(Button)
Vue.prototype.$message = message
5.在 nuxt.config.js 引入此 plugin
plugins: [
'@/plugins/antd-ui',
],
这样就完成了按需引入。使用如下
<template>
<div>
<a-button> click </a-button>
</div>
</template>
当然虽然是按需引入,使用 plugin 引入是将组件注册到全局了。这样引入每个页面都会引入 antd-ui.js 里引用的所有组件的 js,不论当前页面是否有引用到。如果,我们想每个页面只加载当前需要的 js,可以去掉上 4、5 步骤
直接在组件内引入:
<template>
<div>
<a-button> click </a-button>
</div>
</template>
<script>
import Vue from 'vue'
import { Button } from 'ant-design-vue'
Vue.use(Button)
</script>
9.2 axios的封装
通常 axios 的封装包含其发送接口前拦截设置信息、发送接口前错误拦截、返回信息拦截、返回信息报错拦截。
前置条件,我们把关于接口的配置信息按照个人习惯放置,我这边是放到与 nuxt.config.js 同级的 setting 文件夹里:
/
--| setting/
-----| interface.js
--| nuxt.config.js
interface.js 内容设置如下:
// 错误拦截 接口返回非 200 状态
export function checkStatus(status, msg) {
switch (status) {
case 400:
return (`${msg}`)
case 401:
return ('用户没有权限!')
case 403:
return ('用户得到授权,但是访问是被禁止的!')
case 404:
return ('网络请求错误,未找到该资源!')
case 405:
return ('网络请求错误,请求方法未允许!')
case 408:
return ('网络请求超时!')
case 500:
return ('服务器错误,请联系管理员!')
case 501:
return ('网络未实现!')
case 502:
return ('网络错误!')
case 503:
return ('服务不可用,服务器暂时过载或维护!')
case 504:
return ('网络超时!')
case 505:
return ('http版本不支持该请求!')
default:
return ('服务器错误,请联系管理员!')
}
}
// 返回 200 状态码后の拦截
export const ResultEnum = {
SUCCESS: 200, // 调用成功
TOKEN_OVERDUE: 300, // 用户 token 过去
BAD_REQUEST: 400,
INTERNAL_SERVER_ERROR: 500,
FAIL: 600
}
然后开始封装我们的 axios 啦。当我们安装项目默认选了 axios 时,项目会自动安装 @nuxtjs/axios 依赖;如果没有选的,要先装依赖。
import { message } from 'ant-design-vue'
import { whiteList, tokenKey } from '../setting/auth'
import { checkStatus, ResultEnum } from '~/setting/interface'
export default ({ app, route, redirect, store }, inject) => {
// $axios.defaults.baseURL = process.env.baseUrl
const instance = app.$axios.create()
instance.defaults.timeout = 30 * 1000
// 设置withCredentials属性为true,使请求自动携带cookie
instance.defaults.withCredentials = true
const beforeRequest = (config) => {
// 设置 cookie, 只有服务端才需要且只有服务端可以手动设置
// 因为 req 属性只有服务端可以获取。 去掉的原因是,现在放到中间件设置了
// if (req && req.headers.cookie) config.headers.Cookie = req.headers.cookie
config.headers.syscode = 'admin'
return config
}
const requestError = (err) => {
return Promise.reject(err)
}
const resPreHandle = (response) => {
const { data } = response
if (!data) {
handlerError()
}
// 这里 code,data,message为 后台统一的字段
const { code, data: result } = data
// 接口请求成功 code 200,直接返回结果
if (code === ResultEnum.SUCCESS) {
return result
}
// 接口请求错误,统一处理
switch (code) {
case ResultEnum.BAD_REQUEST:
case ResultEnum.INTERNAL_SERVER_ERROR:
case ResultEnum.FAIL:
// 错误异常处理
handlerError(code)
break
case ResultEnum.TOKEN_OVERDUE:
// 用户信息过期处理
hanlerTokenOverdue()
break
default:
handlerError(code, '其他状态')
break
}
return response
}
const responseError = (err) => {
// console.log("responseError", req)
const { code, message } = err || {}
const msg = checkStatus(code, message)
handlerError(code, msg)
}
// 处理报错异常
const handlerError = (code = 500, msg = '系统错误,请联系管理员!') => {
if (process.client) {
// 客户端弹出提示
message.error(`${code}, ${msg}`)
} else {
// 服务端直接跳转错误页面
// 前提是我们拥有 “/error” 路由“/error” 路由layouts下的error.vue组件
// 如果没有 “/error” 路由将会进入死循环 报错堆栈溢出。
redirect("/error")
}
return Promise.reject(msg)
// 首屏的时候 Promise.reject(new Error('something'))
// 是不会跳转到 error 页面的,会直接报程序错误
}
// 处理用户信息过期
const hanlerTokenOverdue = () => {
// 清除 cookie 与 vuex 的用户状态信息
app.$cookies.remove(tokenKey)
store.commit('user/SET_USER_INFO', null)
// 判断是否在白名单,非白名单页面需重定向到登录页
if (whiteList.includes(route.path)) return
redirect('/login')
}
instance.interceptors.request.use(beforeRequest, requestError)
instance.interceptors.response.use(resPreHandle, responseError)
inject('request', instance)
}
这里需要注意的是,之前我有习惯,将拦截写在 $axios 上:如
app.$axios.defaults.baseURL = process.env.baseUrl
app.$axios.interceptors.request.use(beforeRequest, requestError)
app.$axios.interceptors.response.use(resPreHandle, responseError)
最后才建立 axios 实例,注册
const instance = app.$axios.create()
在 vue 项目中,我们这样使用确实没有问题,也可以达到拦截作用。但 Nuxt 不行,必须先建立实例再进行设置,否则无效。
这样设置之后,我们再在 plugin 建立一个 api.js 文件,装载我们所有的 axios 请求,方便调用。设置 nuxt.config.js
plugins: [
'~/plugins/request.js',
'~/plugins/api.js',
],
api.js 内容如下:
export default ({ app: { $request } }, inject) => {
inject('api', {
/**
* 获取登录用户基本信息
* 无参数
*/
fetchUserInfo() {
return $request.post(`/api/getUserInfo`)
},
/**
* 获取详情信息
* @id 必填,详情id
*/
fetchUserInfo(params) {
return $request.post(`/api/getDetail`, params)
},
// ... other apis
})
}
这样我们在页面便可以通过 context.app.$api 方式调用接口。
async asyncData({ app, params }) {
const res = await app.$api.fetchIsp({ id: params.id })
return {
form: res,
}
},
9.3 svg引入
当UI库提供的 icon 没办法满足我们需求的时候,便会自行引入 svg 或者 iconfont。我们这里介绍两种方法引入 svg 图片。
9.3.1 build 配置 svg-sprite-loader 方式
- 安装
svg-sprite-loader
yarn add svg-sprite-loader --save--dev
- 将
svg矢量图放置~assets/icons/svg目录下 - 在
components下建立组件SvgIcon/index.vue:
<template>
<svg
:class="svgClass"
:style="iconStyle"
aria-hidden="true"
v-on="$listeners"
>
<use :xlink:href="iconName" />
</svg>
</template>
<script>
export default {
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true,
},
className: {
type: String,
default: '',
},
iconStyle: {
type: Object,
default: () => ({}),
},
},
computed: {
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
styleExternalIcon() {
return {
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`,
}
},
},
}
</script>
<style lang="less" scoped>
.svg-icon {
display: inline-block;
width: 1.2rem;
height: 1.2rem;
}
</style>
- 在
plugins文件夹创建icon.js文件,代码如下
import Vue from 'vue'
import SvgIcon from '~/components/common/SvgIcon/index.vue' // svg component
Vue.component('svg-icon', SvgIcon)
const req = require.context('~/assets/icons/svg', false, /.svg$/)
const requireAll = requireContext => requireContext.keys().map(requireContext)
requireAll(req)
- 最后一步,配置
nuxt.config.js:
首先配置 plugins:
plugins: [ '@/plugins/icon'],
然后我们需要配置 build:
build: {
extend(config, ctx) {
const svgRule = config.module.rules.find((rule) => rule.test.test('.svg'))
svgRule.exclude = [resolve(__dirname, 'assets/icons/svg')]
// Includes /assets/icons/svg for svg-sprite-loader
config.module.rules.push({
test: /.svg$/,
include: [resolve(__dirname, 'assets/icons/svg')],
loader: 'svg-sprite-loader',
options: {
symbolId: 'icon-[name]',
},
})
},
},
现在我们可以使用已经配置好的 Icon 组件:
<svg-icon icon-class="user"/>
9.3.2 引入插件 @nuxt/svg
安装依赖 @nuxt/svg
yarn add @nuxt/svg --save--dev
// nuxt.config.js
export default {
modules: ["@nuxtjs/svg"],
};
之后引入:
<template>
<img src="~assets/nuxt.svg" />
</template>
十、中间件 middleware
middleware 目录包含您的应用程序中间件。中间件允许您定义可以在呈现页面或一组页面(布局)之前运行的自定义函数。可以在三个位置设置中间件,执行顺序如下:
- 全局设置:
nuxt.config.js layouts文件的middleware属性pages文件的middleware属性
10.1 服务端请求 header 加上 cookie 设置
我们项目,需要在服务端请求数据的时候带上 cookie,但是在 axios 拦截时,我们发现无法获取到 cookie,但是打印 req 对象又能读取到 cookie。怎么办呢?
首先在 middleware 下加入 cookies.js 文件,内容如下:
export default function ({ req }) {
if (process.server) {
process.cookie = req.headers.cookie || ''
}
}
类似于这种全局的中间件,我们是设置在 nuxt.config.js 的。
router: {
middleware: ['cookie'],
},
然后在 axios 的请求拦截加上 withCredentials 属性,具体参考 9.2 axios 的封装。
10.1.1 服务端 操作 cookies
为了更好得操作 cookies,我们引入 cookie-universal-nuxt 插件进行管理
yarn add cookie-universal-nuxt --save
然后在 nuxt.config.js 设置
modules: [
'cookie-universal-nuxt',
],
这样便可以在 context 通过 $cookie 进行操作和读取 cookie,例子:
asyncData({ app }){
app.$cookies.set("token", "admin")
// …… other handler
}
10.2 middleware 处理鉴权
已知我们的项目逻辑如下:
判断页面是否白名单内,如果非白名单又未登录则跳转登录页,否则给予正常跳转。
middleware 文件夹下建立文件: auth.js。内容如下:
import { whiteList, tokenKey } from '../setting/auth'
export default function ({ app, route, redirect }) {
// 当前路由是否匹配白名单
if (whiteList.includes(route.path)) return
const token = app.$cookies.get(tokenKey)
if (!token) {
redirect(`/login`)
}
}
十一、状态树 store
store 包含了我们 Vuex Store 的文件。不需要引入,直接在 store 目录下建立 index.js 文件即可。与 vue 项目使用不一样的是,模块不需要创建一个 modules 文件夹,再在 index.js 中一一引入。store 目录下的每一个除了 index.js 外的js文件就是一个命名空间模块,名称与文件名一致。如:
/ store
--| index.js
--| user.js
那么,我们 store 就拥有了 user 这个命名空间模块。
下列介绍,根据携带的 cookies 获取登录用户的用户信息,存于 store 的 user 模块。
首先在 user.js 文件中,将 vuex 的属性都导出为函数:(最好是以导出形式,旧的形式 Nuxt3 即将废弃。)
import { tokenKey } from '../setting/auth'
export const state = () => ({
userInfo: {},
})
export const mutations = {
SET_USER_INFO(state, userInfo) {
state.userInfo = userInfo
},
}
export const actions = {
// 除了 nuxtServerInit 外自定义的 actions 是没有 context 的
// 这个 context 我是在 dispatch 调用这个 action 的时候传入的
async getUserInfo({ commit }, { context }) {
const { app } = context
// 判断有 token 才抓取信息
if (app.$cookies.get(tokenKey)) {
const userInfo = await this.$api.fetchUserInfo()
commit('SET_USER_INFO', userInfo)
} else {
commit('SET_USER_INFO', {})
}
},
}
如果 nuxtServerInit 在 store 中定义了 action 并且 mode 为 universal,Nuxt.js 将使用上下文调用它(仅从服务器端,且是项目初始化最开始的时候),context 将作为 nuxtServerInit 的第二个参数。
import { tokenKey } from '../setting/auth'
export const actions = {
nuxtServerInit({ dispatch }, context) {
const { app, route } = context
// 若初始化报错则跳入 error 页面,不再进行初始化,防止死循环
if (route.path === '/error') return
const promiseList = [dispatch('user/getUserInfo', { context })]
return Promise.all(promiseList)
},
}
这样我们就实现了初始化,如果 cookie 有 token 信息,我们就去获取用户信息,将返回信息存入到 store 的 user 模块下。
在我们的页面或者组件可以通过 context.store.state.user.userInfo 来获取。
十二、css 预处理器
我们项目使用的是 less。
12.1 引入全局样式
先安装 less 与 less-loader
yarn add less less-loader --save--dev
nuxt 会自动识别导入的后缀名,在 nuxt.config.js 中配置:
css: [
'./assets/styles/index.less',
],
12.2 引入less全局变量
为页面注入变量可以使用 @nuxtjs/style-resources 来实现
yarn add @nuxtjs/style-resources --save--dev
配置 nuxt.config.js
modules: [
'@nuxtjs/style-resources',
],
// styleResources 配置的资源路径不能使用 ~ 和 @,要使用绝对或者相对路径
styleResources: {
less: ['./assets/styles/variable.less'],
},
十三、环境变量的配置
Nuxt.js 允许您在客户端创建环境变量,也可以从服务器端共享。
安装 cross-env 插件,允许运行跨平台设置和使用环境变量。
yarn add --save-dev cross-env
在 Nuxt.config.js 同级目录下新增 env.js 文件包含我们的环境变量:
// 环境变量
module.exports = {
// 开发环境
dev: {
NODE_ENV: 'development',
NUXT_BASE_API: 'https://test.domain.com', // 服务器地址
NUXT_LOGIN_API: 'https://testlogin.domain.com', // 登录地址
},
// 测试环境
test: {
NODE_ENV: 'test',
NUXT_BASE_API: 'https://test.domain.com',
NUXT_LOGIN_API: 'https://testlogin.domain.com',
},
// ... others
}
在 nuxt.config.js 设置 env
import env from './env' // 环境配置文件
export default {
env: env[process.env.NODE_ENV],
}
配置 pakage.json
"scripts": {
"dev": "cross-env NODE_ENV=dev nuxt",
"test": "cross-env NODE_ENV=test nuxt build",
},
运用则:
data() {
return {
test:{
NODE_ENV: process.env.NODE_ENV,
NUXT_BASE_API: process.env.NUXT_BASE_API,
NUXT_LOGIN_API: process.env.NUXT_LOGIN_API,
}
}
},