1. 环境变量
按照官方文档的提示,根目录新建一个 .env.xxx (development、test、production,或其他自定义模式) 文件,在 package.json 的启动命令中通过 --dotenv 指定文件
"script": {
"start": "nuxt dev --dotenv .env.xxx"
}
在代码中打印 .env.xxx 中定义的变量 process.env.xxx,最终发现服务端能正常打印,而客户端始终为 undefined
解决方案
// nuxt.config.ts 中配置
export default defineNuxtConfig({
runtimeConfig: {
// 需要写在public里面,否则客户端无法访问
public: {
xxx: process.env.xxx
}
}
})
// 需要使用的地方
const runtimeConfig = useRuntimeConfig()
runtimeConfig.public.xxx 即可使用
2. 布局与中间件
nuxt 会根据 pages 文件夹的结构自动生成路由,由于某些地方不太方便,于是使用 app/router.options.js 自定义了路由
项目默认是使用 layout/default.vue 作为布局组件的,但极个别页面需要自定义 layout
于是在 layout 文件夹下新建 custom.vue,并按照文档说明在需要使用的地方添加如下代码
definePageMeta({
layout: 'custom'
})
配置起来挺简单的,但是并没有什么卵用,依然还是使用的默认布局。于是再把官方文档看了一遍,确认没有写错,但就是没有效果,最后在 api 文档的 utils 中看到这样一个方法
果断试了一下
<script setup>
setPageLayout('custom')
</script>
搞定,问题解决,但依然疑惑为啥 definePageMeta 这种写法无效,直到后来用到了中间件
由于某些页面既有web端又有H5,需要在路由中判断设备类型,如果使用电脑访问H5地址,则需要重定向到web端,反之亦然,于是写了一个路由中间件,在存在双端的页面中使用
definePageMeta({
middleware: ['redirect']
// 或 middleware: 'redirect'
})
然而跟配置 layout 一样,根本没有生效,看了一下文档,也没有类似 setPageLayout 这样的方法。
于是各种谷歌百度,最终找到了原因,自定义的路由 definePageMeta 会失效,需要在路由的meta中定义(不知道是看漏了,还是文档确实没写,真的坑)
// app/router.options.js
export default {
routes: _routes => [
{
...,
meta: {
layout: 'custom',
middleware: 'redirect'
}
},
{...},
...
]
}
3. echarts 报错
通过 npm 安装后直接在页面中引入
<script setup>
import * as echarts from 'echarts/core'
</script>
结果报错
Cannot use import statement outside a module
不能在模块外使用 import 语句,一脸懵逼,明明是模块内,突然想起之前在 nuxt2 中是不能在服务端引入 echarts 的,于是将上面代码移到了只在客户端执行的插件中,并挂载在nuxtApp上
// plugins/xxx.client.js
import * as echarts from 'echarts/core'
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.provide('echarts', echarts)
})
接着就可以直接在 vue 文件中使用了
const nuxtApp = useNuxtApp()
const chart = ref(null)
onMounted({
chart.value = nuxtApp.$echarts.init(document.querySelector('#xxx'))
chart.value.setOption({...})
window.addEventListener('resize', () => {
chart.value.resize()
})
})
展示完美,没有问题,然而就在我改变窗口宽度,触发 chart.resize() 的时候,问题来了
看不懂,根本看不懂,无奈只好需求谷歌百度的帮助,结果发现居然没人提到这个问题,不知道是还没人在 nuxt3/vue3 中使用过 echarts,还是我太蠢了。最终在 echarts 官方文档中找到了答案
将 const chart = ref(null) 改为 const chart = null 或 const chart = shallowRef(null) 问题解决,如果需要响应式就使用后面一种
4. 路径别名
自定义组件,由于组件内容较少,不想单独搞一个vue文件,采用了下面的方式
<template>
<div class="test">
<item name="abc" value="123"></item>
<item name="xyz" value="345"></item>
<item name="lmn" value="678"></item>
</div>
</template>
<script setup>
defineOptions({
components: {
item: {
template: `<div>
<span>{{ name }}:</span>
<span>{{ value }}</span>
</div>`,
props: ['name', 'value']
}
}
})
</script>
刷新页面时会看到三条记录都渲染出来了,但是紧接着第一条会消失,如下图所示
可以看出,服务端渲染的时候是没有问题的,但是客户端接管后第一条内容被移除了,并且控制台有警告信息
Component provided template option but runtime compilation is not supported in this build of Vue. Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js".
通过路径别名的方式修改vue为完整版本
// 这是 nuxt3 官网文档中提供的别名配置方式
export default defineNuxtConfig({
alias: {
vue: 'vue/dist/vue.esm-bundler.js'
}
})
配置完成后,问题解决了,三条记录都被完整的渲染了出来。但是,新的问题来了
Could not load F:\nuxt3-demo\node_modules\vue\dist\vue.esm-bundler.js\server-renderer (imported by node_modules/nuxt/dist/core/runtime/nitro/renderer.js)
原因是 nuxt 框架中有引入 vue/server-renderer 文件,配置别名后,相当于引入路径变成了 vue\dist\vue.esm-bundler.js\server-renderer,这显示是不对的,于是加一个$匹配结尾
vue$: 'vue/dist/vue.esm-bundler.js
修改后警告又出现了,之前在 nuxt2 中这样写是没问题的,大概 webpack 支持这种写法,但是 vite 不支持,于是又去翻阅了 vite 的配置文档,改成了如下形式
export default defineNuxtConfig({
alias: [{
find: /^vue$/,
replacement: 'vue/dist/vue.esm-bundler.js'
}]
})
结果ts直接报错,不能将类型“{ find: RegExp; replacement: string; }”分配给类型“string”。
最终正确配置方式如下
export default defineNuxtConfig({
vite: {
resolve: {
alias: [{
find: /^vue$/,
replacement: 'vue/dist/vue.esm-bundler.js'
}]
}
}
})
5. 动态引入图片
在项目中,有些本地图片是需要根据接口字段来确定的,因此没办法直接写死路径,只能动态引入
在 webpack 项目中,可以通过 require 来实现,但是 vite 并不支持,而是通过以下方式来实现的
<img :src="imgSrc" />
...
const imgSrc = computed(() => {
const imgName = data.value.imgName // data 是接口返回的数据
return new URL(`../assets/images/${imgName}.png`, import.meta.url).href
})
于是在 nuxt3 中尝试了一下,本以为轻松搞定,结果却出人意料
可以看到,客户端渲染跟服务端渲染路径是不一样的,服务端渲染的路径有问题,图片显示不出来
于是又去翻阅了 vite 的官方文档,看到下面这段话
可是它也没说用什么方法来替代,只能去网上找答案了,有人说直接写路径就行,紧接着就试了一下
<img :src="`../../assets/rating/${imgName}.png`" /> // 这里比上面多一个 ../,是因为路由有一个 baseURL
...
const imgName = computed(() => data.value.imgName)
不管是客户端渲染还是服务端渲染,都能完美展示,路径还保持一致,完美
本以为这样就算解决了,直到后面打包发布,发现图片加载不出来,只能继续填坑了(把图片放到 public 里面,直接写路径是可以的,但是不想这样做)
既然 new URL 的方式在客户端可以,直接写路径的方式在服务端可以,那根据环境判断是不是就行了?
const imgSrc = computed(() => {
const imgName = data.value.imgName
if (process.server) return `../../assets/rating/${imgName}`
return new URL(`../assets/rating/${imgName}.png`, import.meta.url).href
})
结果还是没什么用,process.server 的变化并不会触发计算属性的重新执行
突然想到之前在 nuxt2 项目中,某些第三方库不能在服务端导入的时候,可以使用 require 或 import() 在mounted 中导入,既然如此,那这里能不能用 import() 呢?迫不及待的试一下
<img :src="img" />
...
const img = ref('')
watchEffect(async () => {
const imgName = data.value.imgName
img.value = (await import(`../assets/rating/${imgName}.png`)).default
})
问题解决,开发环境无论哪端渲染都没有问题,build 后图片也被打包成了 base64(但是对这种方式不太满意,代码有点复杂了)
6. i18n
项目中需要使用国际化,发现 nuxt 有提供一个 @nuxtjs/i18n 模块,于是照着文档三下五除二的就撸完了
结果一运行,报了一堆看不懂的错误
经过我的一番测试,发现是因为在字符串中使用了 p 标签,后面我尝试了其他 html 标签好像都不行,去掉 html 标签后,成功运行,但是结果还是无法让人满意
这他娘的是个什么鬼?我想要的是“超强”,而不是这一坨。字符串中使用 html 标签,以及数组通过下标取值,这两种写法在 vite + vue3 的项目中是没有问题的,不知这里为何如此奇怪,于是我在两个项目中直接打印了 tm('array') 的值
左边是 vite + vue3 打印出来的结果,直接就是一个字符串数组,右边是 nuxt3 打印出来的结果,居然是一个函数数组,同样都是 9.x 的版本,结果却大相径庭
最后经过尝试,发现在 nuxt3 中通过 $t('array[0]') 是可以直接取到值的,但是在实际项目中,这个下标是根据接口返回的数据来确定的,所以最终只能通过模板字符串插入或者字符串拼接的形式,感觉太复杂,而且还不能用 html 标签,很不方便
于是改成了在插件中去创建i18n
export default defineNuxtPlugin(nuxtApp => {
const i18n = createI18n({
legacy: false,
locale: 'cn',
warnHtmlMessage: false,
messages: {
cn: {...}
}
})
nuxtApp.vueApp.use(i18n)
})
完美解决以上两个问题,但是事情还没完,后面有一次在中间件中需要使用 i18n,直接 const {t} = useI18n(),结果报错 Must be called at the top of a `setup` function
只能在 setup 函数中使用,不能在中间件里面用,后面无意中发现 nuxtApp 中有一个 $i18n 的属性,而 useNuxtApp() 是可以在中间件中使用的,这不就解决了吗?结果
Not found 'title' key in 'en-US' locale messages.
这 en-US 是什么东西?locale 明明设置的 cn,然后我打印了一下 useI18n() 跟 nuxtApp.$i18n 的值,发现它们的 id 居然不一样,显然这不是同一个对象
nuxtApp.$i18n 是 @nuxtjs/i18n 帮我们创建并挂载的,同时也挂载到了 vue 上,所以第一种创建方式,它们的 id 是一样的,而当我们在插件中使用 createI18n() 并 nuxtApp.vueApp.use(i18n) 之后,覆盖了原本挂载在 vue 上的对象,所以导致两者不一致
当然也可以直接用 nuxtApp.vueApp.__VUE_I18N__.global,但是太长了,我不喜欢,所以只好来个骚操作,在插件最后面加一句代码
nuxtApp.provide('i18n', i18n.global)
好家伙,直接报错,Cannot redefine property: $i18n,不能覆盖它原本的 $i18n,那就改个名字吧
nuxtApp.provide('vueI18n', i18n.global)
然后在其他地方就可以通过 nuxtApp.$vueI18n 去使用了
最终还是无法理解为啥第一种方式不能在字符串中使用 html 标签,以及使用 tm 获取多语言数组然后通过下标取值,严重怀疑 @nuxtjs/i18n 对 vue-i18n 做了一个恶心的封装
问题并没有完全解决,原本有文案 day:“{x} 天”,通过t('day', {x: 2}) 可以输出文案“2天”,然而,打包后却出了问题,{x}并没有被替换成2,直接输出了 “{x} 天”,并伴有如下警告:
The message format compilation is not supported in this build. Because message compiler isn't included. You need to pre-compilation all message format. So translate function
return 'xxx'.
那是因为默认使用的vue-i18n是运行时版本的,需要替换成完整版本,在配置别名的地方添加如下代码即可
{
find: /^vue-i18n$/,
replacement: 'vue-i18n/dist/vue-i18n.esm-bundler.js'
}
7. useFetch
用惯了 axios,第一次使用 useFetch 相当的不习惯,途中踩了不少的坑
关于服务端/客户端请求,useFetch/useLazyFetch,是否 await 的一些说明
- 未设置 server: false 的请求,只有在页面初次加载时,才是服务端请求,路由跳转时为客户端请求
- 服务端请求时,useFetch 与 useLazyFetch 没有区别
- 服务端请求时,是否 await 对页面渲染没有影响,只决定后面的代码是否会等待请求完成
- 初次加载页面,客户端请求即使 await,后面代码也拿不到数据
- 路由跳转时,请求前加上 await,useFetch 后面能拿到数据,useLazyFetch 不行
- 路由跳转时,会等待 useFetch 请求完成再切换页面,而 useLazyFetch 则不会
a. 触发多次请求
项目中有个协议页面,多种类型,调用同一个接口,通过 valueType 字段来区分,每次切换的时候修改 params.valueType 的值,然后重新发送请求,主要代码如下
const baseURL = 'http://192.168.x.xx:xxxx/api'
const params = reactive({valueType: 1})
const {data, refresh} = await useLazyFetch(baseURL + '/Global/GetProtocolValue', {params})
const changeType = type => {
params.valueType = type
refresh()
}
结果发现每次切换类型,都会重复发送两次请求,并且第一次请求会被取消掉
起初觉得这个 refresh 函数可能有问题,于是把它注释掉看看会怎样,结果居然正常了,就他妈很诡异,切换类型时竟然会自动发请求,后来仔细看了一下文档
问题就出在这里,因为 params 是响应式的,当它改变后就会重新发送请求,所以这里直接修改 params.valueType 的值就行了,没必要再调用 refresh 方法
如果觉得这种方式不好把控,希望自己调用 refresh 方法,可以把 params 改成非响应式的,或者在调用 useFetch 的时候加一个 watch: false 的配置
const {data, refresh} = await useLazyFetch(..., {params, watch: false})
b. await 无效
之前用 axios 时,习惯在前面加一个 await, 然后在之后的代码中处理获取到的数据,然而在使用 useFetch 的时候,似乎出了点问题
当使用服务端渲染的时候,这种做法是可行的,如果换成客户端渲染,结果数据变成了 null
const {data, refresh} = await useFetch(baseURL + '/Global/GetProtocolValue', {params, server: false})
console.log(data.value) // 打印结果为 null
文档中对此同样也有说明
客户端请求时,即使加了 await,后面的代码的执行也不会等待请求完成,如果需要对数据进行处理,可以配置 transform 参数,或者使用 watch 去监听数据的变化再做处理
watch(data, () => {
if (data.value) {
// 在这里对数据进行处理
}
})
c. 数据类型不对
在使用 useFetch 时,api 地址的设置方式有两种,一种是直接拼在 url 前面,另一种则是通过 baseURL 配置
// 方式一
const {data} = await useFetch('https://test.api.com/h5/getUserInfo')
// 方式二
const {data} = await useFetch('/h5/getUserInfo', {baseURL: 'https://test.api.com'})
刚开始是把 api 地址直接拼在 url 前面的,这种写法用起来完全ok,后来尝试了一下设置为 baseURL,结果刷新页面后数据居然没了,没了......
经过我的各种测试,发现了一个奇怪的问题,当客户端请求时,两种方式没有区别,但是服务端请求时,baseURL 方式返回的数据类型有问题,它本该是 object 类型的,但结果它却是 string 类型的
于是加了一个请求拦截器,在请求之前打印了一下请求信息,发现服务端请求比客户端请求多了一坨奇怪的东西
而问题就出在这个 accept 上,它的作用就是告诉服务器,客户端这边想要什么类型的数据,从图中可以看出,它被设置成了 text/html,因此返回的数据类型就变成了 string
手动给 accept 设置一个值,问题就可以解决了
const {data} = await useFetch('/h5/getUserInfo', {
baseURL: 'https://test.api.com',
headers: {accept: 'application/json'} // 也可以设置成 */*
})
以上这个问题取决于服务器代码逻辑,如果服务器不判断 accept 字段,固定返回 json 格式,就没有这个情况
d. 错误处理
公司接口返回的数据格式如下所示
{
bodyMessage: {} // 前端需要的数据
code: 0 // 状态码 0 表示成功,-1 表示鉴权失败
subCode: 'BF11800' // 子状态码 最后两位是 00 表示成功,其他表示失败
message: '' // 错误信息
}
因此每个请求都需要对 code 及 subCode 进行判断,如果成功,则处理 bodyMessage 里面的数据,失败则根据不同的 subCode 做不同的处理
为了方便,对 useFetch 做了一个二次封装,代码如下
export default (url, options) => {
const rtConfig = useRuntimeConfig()
return useFetch(url, {
baseURL: rtConfig.public.apiUrl,
onRequest({options}) {...},
onResponse({response}) {
const {code, subCode, bodyMessage} = response._data
if (!code && subCode.endsWith('00')) {
return (response._data = bodyMessage) // 如果成功,就把 bodyMessage 赋值给 data
}
return Promise.reject(response._data) // 如果失败,就把整个 data 抛出去
}
})
}
这种写法在 axios 拦截器是没有问题的,但这里不行
const {data, error} = await fetch(...)
console.log(error.value)
打印了一下 error.value, 本以为会是整个对象,结果
经过一番尝试,最终得出如下结论
- 无法修改返回的 errorr 对象
- 如果 reject 一个字符串,那么 error 的 message 就是这个字符串
- 如果 reject 一个带 message 字段的对象,那么 error 的 message 就是这个字段
- 如果 reject 一个不带 message 的对象,那么 error 的 message 为空
所以就可以先使用 JSON.stringify 将对象转成字符串,然后通过 JSON.parse(error.value.message) 拿到这个对象
8. 第三方字体(未解决)
项目中用到了第三方字体,将字体文件放到 assets/font 目录下,然后在全局 css 文件中设置
@font-face {
font-family: "GoogleSans";
src: url("../font/GoogleSans-Regular.ttf");
font-display: swap;
font-weight: 400;
}
本地开发没有任何问题,但是打包后字体文件不见了,之后又把路径改成绝对路径,问题依然存在
放在 public 里面可解决,但是不想什么都往 public 里面塞,就想放在 assets 里面
之前在 nuxt2、vue2、vue3 中都是这么写的,完全没问题,这坑爹的 nuxt3