1. 環(huán)境變量
按照官方文檔的提示惩激,根目錄新建一個(gè) .env.xxx (development、test眶明、production艾扮,或其他自定義模式) 文件既琴,在 package.json 的啟動(dòng)命令中通過 --dotenv 指定文件
"script": {
"start": "nuxt dev --dotenv .env.xxx"
}
在代碼中打印 .env.xxx 中定義的變量 process.env.xxx,最終發(fā)現(xiàn)服務(wù)端能正常打印泡嘴,而客戶端始終為 undefined
解決方案
// nuxt.config.ts 中配置
export default defineNuxtConfig({
runtimeConfig: {
// 需要寫在public里面甫恩,否則客戶端無法訪問
public: {
xxx: process.env.xxx
}
}
})
// 需要使用的地方
const runtimeConfig = useRuntimeConfig()
runtimeConfig.public.xxx 即可使用
2. 布局與中間件
nuxt 會(huì)根據(jù) pages 文件夾的結(jié)構(gòu)自動(dòng)生成路由,由于某些地方不太方便酌予,于是使用 app/router.options.js 自定義了路由
項(xiàng)目默認(rèn)是使用 layout/default.vue 作為布局組件的磺箕,但極個(gè)別頁面需要自定義 layout
于是在 layout 文件夾下新建 custom.vue,并按照文檔說明在需要使用的地方添加如下代碼
definePageMeta({
layout: 'custom'
})
配置起來挺簡(jiǎn)單的抛虫,但是并沒有什么卵用松靡,依然還是使用的默認(rèn)布局。于是再把官方文檔看了一遍建椰,確認(rèn)沒有寫錯(cuò)雕欺,但就是沒有效果,最后在 api 文檔的 utils 中看到這樣一個(gè)方法
果斷試了一下
<script setup>
setPageLayout('custom')
</script>
搞定棉姐,問題解決屠列,但依然疑惑為啥 definePageMeta 這種寫法無效,直到后來用到了中間件
由于某些頁面既有web端又有H5谅海,需要在路由中判斷設(shè)備類型脸哀,如果使用電腦訪問H5地址,則需要重定向到web端扭吁,反之亦然撞蜂,于是寫了一個(gè)路由中間件炮捧,在存在雙端的頁面中使用
definePageMeta({
middleware: ['redirect']
// 或 middleware: 'redirect'
})
然而跟配置 layout 一樣表鳍,根本沒有生效,看了一下文檔看疙,也沒有類似 setPageLayout 這樣的方法枫吧。
于是各種谷歌百度浦旱,最終找到了原因,自定義的路由 definePageMeta 會(huì)失效九杂,需要在路由的meta中定義(不知道是看漏了颁湖,還是文檔確實(shí)沒寫,真的坑)
// app/router.options.js
export default {
routes: _routes => [
{
...,
meta: {
layout: 'custom',
middleware: 'redirect'
}
},
{...},
...
]
}
3. echarts 報(bào)錯(cuò)
通過 npm 安裝后直接在頁面中引入
<script setup>
import * as echarts from 'echarts/core'
</script>
結(jié)果報(bào)錯(cuò)
Cannot use import statement outside a module
不能在模塊外使用 import 語句例隆,一臉懵逼甥捺,明明是模塊內(nèi),突然想起之前在 nuxt2 中是不能在服務(wù)端引入 echarts 的镀层,于是將上面代碼移到了只在客戶端執(zhí)行的插件中镰禾,并掛載在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()
})
})
展示完美,沒有問題,然而就在我改變窗口寬度吴侦,觸發(fā) chart.resize() 的時(shí)候屋休,問題來了
看不懂,根本看不懂备韧,無奈只好需求谷歌百度的幫助劫樟,結(jié)果發(fā)現(xiàn)居然沒人提到這個(gè)問題,不知道是還沒人在 nuxt3/vue3 中使用過 echarts盯蝴,還是我太蠢了毅哗。最終在 echarts 官方文檔中找到了答案
將 const chart = ref(null) 改為 const chart = null 或 const chart = shallowRef(null) 問題解決听怕,如果需要響應(yīng)式就使用后面一種
4. 路徑別名
自定義組件捧挺,由于組件內(nèi)容較少,不想單獨(dú)搞一個(gè)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>
刷新頁面時(shí)會(huì)看到三條記錄都渲染出來了闽烙,但是緊接著第一條會(huì)消失,如下圖所示
可以看出声搁,服務(wù)端渲染的時(shí)候是沒有問題的黑竞,但是客戶端接管后第一條內(nèi)容被移除了,并且控制臺(tái)有警告信息
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 官網(wǎng)文檔中提供的別名配置方式
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 文件遏匆,配置別名后,相當(dāng)于引入路徑變成了 vue\dist\vue.esm-bundler.js\server-renderer谁榜,這顯示是不對(duì)的幅聘,于是加一個(gè)$匹配結(jié)尾
vue$: 'vue/dist/vue.esm-bundler.js
修改后警告又出現(xiàn)了,之前在 nuxt2 中這樣寫是沒問題的窃植,大概 webpack 支持這種寫法帝蒿,但是 vite 不支持,于是又去翻閱了 vite 的配置文檔巷怜,改成了如下形式
export default defineNuxtConfig({
alias: [{
find: /^vue$/,
replacement: 'vue/dist/vue.esm-bundler.js'
}]
})
結(jié)果ts直接報(bào)錯(cuò)葛超,不能將類型“{ find: RegExp; replacement: string; }”分配給類型“string”。
最終正確配置方式如下
export default defineNuxtConfig({
vite: {
resolve: {
alias: [{
find: /^vue$/,
replacement: 'vue/dist/vue.esm-bundler.js'
}]
}
}
})
5. 動(dòng)態(tài)引入圖片
在項(xiàng)目中延塑,有些本地圖片是需要根據(jù)接口字段來確定的绣张,因此沒辦法直接寫死路徑,只能動(dòng)態(tài)引入
在 webpack 項(xiàng)目中页畦,可以通過 require 來實(shí)現(xiàn)胖替,但是 vite 并不支持,而是通過以下方式來實(shí)現(xiàn)的
<img :src="imgSrc" />
...
const imgSrc = computed(() => {
const imgName = data.value.imgName // data 是接口返回的數(shù)據(jù)
return new URL(`../assets/images/${imgName}.png`, import.meta.url).href
})
于是在 nuxt3 中嘗試了一下,本以為輕松搞定独令,結(jié)果卻出人意料
可以看到端朵,客戶端渲染跟服務(wù)端渲染路徑是不一樣的,服務(wù)端渲染的路徑有問題燃箭,圖片顯示不出來
于是又去翻閱了 vite 的官方文檔冲呢,看到下面這段話
可是它也沒說用什么方法來替代,只能去網(wǎng)上找答案了招狸,有人說直接寫路徑就行敬拓,緊接著就試了一下
<img :src="`../../assets/rating/${imgName}.png`" /> // 這里比上面多一個(gè) ../,是因?yàn)槁酚捎幸粋€(gè) baseURL
...
const imgName = computed(() => data.value.imgName)
不管是客戶端渲染還是服務(wù)端渲染裙戏,都能完美展示乘凸,路徑還保持一致,完美
本以為這樣就算解決了累榜,直到后面打包發(fā)布营勤,發(fā)現(xiàn)圖片加載不出來,只能繼續(xù)填坑了(把圖片放到 public 里面壹罚,直接寫路徑是可以的葛作,但是不想這樣做)
既然 new URL 的方式在客戶端可以,直接寫路徑的方式在服務(wù)端可以猖凛,那根據(jù)環(huán)境判斷是不是就行了赂蠢?
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
})
結(jié)果還是沒什么用,process.server 的變化并不會(huì)觸發(fā)計(jì)算屬性的重新執(zhí)行
突然想到之前在 nuxt2 項(xiàng)目中辨泳,某些第三方庫不能在服務(wù)端導(dǎo)入的時(shí)候虱岂,可以使用 require 或 import() 在mounted 中導(dǎo)入,既然如此漠吻,那這里能不能用 import() 呢量瓜?迫不及待的試一下
<img :src="img" />
...
const img = ref('')
watchEffect(async () => {
const imgName = data.value.imgName
img.value = (await import(`../assets/rating/${imgName}.png`)).default
})
問題解決,開發(fā)環(huán)境無論哪端渲染都沒有問題途乃,build 后圖片也被打包成了 base64(但是對(duì)這種方式不太滿意绍傲,代碼有點(diǎn)復(fù)雜了)
6. i18n
項(xiàng)目中需要使用國(guó)際化,發(fā)現(xiàn) nuxt 有提供一個(gè) @nuxtjs/i18n 模塊耍共,于是照著文檔三下五除二的就擼完了
結(jié)果一運(yùn)行烫饼,報(bào)了一堆看不懂的錯(cuò)誤
經(jīng)過我的一番測(cè)試,發(fā)現(xiàn)是因?yàn)樵谧址惺褂昧?p 標(biāo)簽试读,后面我嘗試了其他 html 標(biāo)簽好像都不行杠纵,去掉 html 標(biāo)簽后,成功運(yùn)行钩骇,但是結(jié)果還是無法讓人滿意
這他娘的是個(gè)什么鬼比藻?我想要的是“超強(qiáng)”铝量,而不是這一坨。字符串中使用 html 標(biāo)簽银亲,以及數(shù)組通過下標(biāo)取值慢叨,這兩種寫法在 vite + vue3 的項(xiàng)目中是沒有問題的,不知這里為何如此奇怪务蝠,于是我在兩個(gè)項(xiàng)目中直接打印了 tm('array') 的值
左邊是 vite + vue3 打印出來的結(jié)果拍谐,直接就是一個(gè)字符串?dāng)?shù)組,右邊是 nuxt3 打印出來的結(jié)果馏段,居然是一個(gè)函數(shù)數(shù)組轩拨,同樣都是 9.x 的版本,結(jié)果卻大相徑庭
最后經(jīng)過嘗試院喜,發(fā)現(xiàn)在 nuxt3 中通過 $t('array[0]') 是可以直接取到值的亡蓉,但是在實(shí)際項(xiàng)目中,這個(gè)下標(biāo)是根據(jù)接口返回的數(shù)據(jù)來確定的够坐,所以最終只能通過模板字符串插入或者字符串拼接的形式寸宵,感覺太復(fù)雜,而且還不能用 html 標(biāo)簽元咙,很不方便
于是改成了在插件中去創(chuàng)建i18n
export default defineNuxtPlugin(nuxtApp => {
const i18n = createI18n({
legacy: false,
locale: 'cn',
warnHtmlMessage: false,
messages: {
cn: {...}
}
})
nuxtApp.vueApp.use(i18n)
})
完美解決以上兩個(gè)問題,但是事情還沒完巫员,后面有一次在中間件中需要使用 i18n庶香,直接 const {t} = useI18n(),結(jié)果報(bào)錯(cuò) Must be called at the top of a `setup` function
只能在 setup 函數(shù)中使用简识,不能在中間件里面用赶掖,后面無意中發(fā)現(xiàn) nuxtApp 中有一個(gè) $i18n 的屬性,而 useNuxtApp() 是可以在中間件中使用的七扰,這不就解決了嗎奢赂?結(jié)果
Not found 'title' key in 'en-US' locale messages.
這 en-US 是什么東西?locale 明明設(shè)置的 cn颈走,然后我打印了一下 useI18n() 跟 nuxtApp.$i18n 的值膳灶,發(fā)現(xiàn)它們的 id 居然不一樣,顯然這不是同一個(gè)對(duì)象
nuxtApp.$i18n 是 @nuxtjs/i18n 幫我們創(chuàng)建并掛載的立由,同時(shí)也掛載到了 vue 上轧钓,所以第一種創(chuàng)建方式,它們的 id 是一樣的锐膜,而當(dāng)我們?cè)诓寮惺褂?createI18n() 并 nuxtApp.vueApp.use(i18n) 之后毕箍,覆蓋了原本掛載在 vue 上的對(duì)象,所以導(dǎo)致兩者不一致
當(dāng)然也可以直接用 nuxtApp.vueApp.__VUE_I18N__.global道盏,但是太長(zhǎng)了而柑,我不喜歡文捶,所以只好來個(gè)騷操作,在插件最后面加一句代碼
nuxtApp.provide('i18n', i18n.global)
好家伙媒咳,直接報(bào)錯(cuò)拄轻,Cannot redefine property: $i18n,不能覆蓋它原本的 $i18n伟葫,那就改個(gè)名字吧
nuxtApp.provide('vueI18n', i18n.global)
然后在其他地方就可以通過 nuxtApp.$vueI18n 去使用了
最終還是無法理解為啥第一種方式不能在字符串中使用 html 標(biāo)簽恨搓,以及使用 tm 獲取多語言數(shù)組然后通過下標(biāo)取值,嚴(yán)重懷疑 @nuxtjs/i18n 對(duì) vue-i18n 做了一個(gè)惡心的封裝
問題并沒有完全解決筏养,原本有文案 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'.
那是因?yàn)槟J(rèn)使用的vue-i18n是運(yùn)行時(shí)版本的宪郊,需要替換成完整版本,在配置別名的地方添加如下代碼即可
{
find: /^vue-i18n$/,
replacement: 'vue-i18n/dist/vue-i18n.esm-bundler.js'
}
7. useFetch
用慣了 axios拖陆,第一次使用 useFetch 相當(dāng)?shù)牟涣?xí)慣弛槐,途中踩了不少的坑
關(guān)于服務(wù)端/客戶端請(qǐng)求,useFetch/useLazyFetch依啰,是否 await 的一些說明
- 未設(shè)置 server: false 的請(qǐng)求乎串,只有在頁面初次加載時(shí),才是服務(wù)端請(qǐng)求速警,路由跳轉(zhuǎn)時(shí)為客戶端請(qǐng)求
- 服務(wù)端請(qǐng)求時(shí)叹誉,useFetch 與 useLazyFetch 沒有區(qū)別
- 服務(wù)端請(qǐng)求時(shí),是否 await 對(duì)頁面渲染沒有影響闷旧,只決定后面的代碼是否會(huì)等待請(qǐng)求完成
- 初次加載頁面长豁,客戶端請(qǐng)求即使 await,后面代碼也拿不到數(shù)據(jù)
- 路由跳轉(zhuǎn)時(shí)忙灼,請(qǐng)求前加上 await匠襟,useFetch 后面能拿到數(shù)據(jù),useLazyFetch 不行
- 路由跳轉(zhuǎn)時(shí)缀棍,會(huì)等待 useFetch 請(qǐng)求完成再切換頁面宅此,而 useLazyFetch 則不會(huì)
a. 觸發(fā)多次請(qǐng)求
項(xiàng)目中有個(gè)協(xié)議頁面,多種類型爬范,調(diào)用同一個(gè)接口父腕,通過 valueType 字段來區(qū)分,每次切換的時(shí)候修改 params.valueType 的值青瀑,然后重新發(fā)送請(qǐng)求璧亮,主要代碼如下
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()
}
結(jié)果發(fā)現(xiàn)每次切換類型萧诫,都會(huì)重復(fù)發(fā)送兩次請(qǐng)求,并且第一次請(qǐng)求會(huì)被取消掉
起初覺得這個(gè) refresh 函數(shù)可能有問題枝嘶,于是把它注釋掉看看會(huì)怎樣帘饶,結(jié)果居然正常了,就他媽很詭異群扶,切換類型時(shí)竟然會(huì)自動(dòng)發(fā)請(qǐng)求及刻,后來仔細(xì)看了一下文檔
問題就出在這里,因?yàn)?params 是響應(yīng)式的竞阐,當(dāng)它改變后就會(huì)重新發(fā)送請(qǐng)求缴饭,所以這里直接修改 params.valueType 的值就行了,沒必要再調(diào)用 refresh 方法
如果覺得這種方式不好把控骆莹,希望自己調(diào)用 refresh 方法颗搂,可以把 params 改成非響應(yīng)式的,或者在調(diào)用 useFetch 的時(shí)候加一個(gè) watch: false 的配置
const {data, refresh} = await useLazyFetch(..., {params, watch: false})
b. await 無效
之前用 axios 時(shí)幕垦,習(xí)慣在前面加一個(gè) await丢氢, 然后在之后的代碼中處理獲取到的數(shù)據(jù),然而在使用 useFetch 的時(shí)候先改,似乎出了點(diǎn)問題
當(dāng)使用服務(wù)端渲染的時(shí)候疚察,這種做法是可行的,如果換成客戶端渲染盏道,結(jié)果數(shù)據(jù)變成了 null
const {data, refresh} = await useFetch(baseURL + '/Global/GetProtocolValue', {params, server: false})
console.log(data.value) // 打印結(jié)果為 null
文檔中對(duì)此同樣也有說明
客戶端請(qǐng)求時(shí)稍浆,即使加了 await,后面的代碼的執(zhí)行也不會(huì)等待請(qǐng)求完成猜嘱,如果需要對(duì)數(shù)據(jù)進(jìn)行處理,可以配置 transform 參數(shù)嫁艇,或者使用 watch 去監(jiān)聽數(shù)據(jù)的變化再做處理
watch(data, () => {
if (data.value) {
// 在這里對(duì)數(shù)據(jù)進(jìn)行處理
}
})
c. 數(shù)據(jù)類型不對(duì)
在使用 useFetch 時(shí)朗伶,api 地址的設(shè)置方式有兩種,一種是直接拼在 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,后來嘗試了一下設(shè)置為 baseURL猾漫,結(jié)果刷新頁面后數(shù)據(jù)居然沒了点晴,沒了......
經(jīng)過我的各種測(cè)試,發(fā)現(xiàn)了一個(gè)奇怪的問題悯周,當(dāng)客戶端請(qǐng)求時(shí)粒督,兩種方式?jīng)]有區(qū)別,但是服務(wù)端請(qǐng)求時(shí)禽翼,baseURL 方式返回的數(shù)據(jù)類型有問題屠橄,它本該是 object 類型的族跛,但結(jié)果它卻是 string 類型的
于是加了一個(gè)請(qǐng)求攔截器,在請(qǐng)求之前打印了一下請(qǐng)求信息锐墙,發(fā)現(xiàn)服務(wù)端請(qǐng)求比客戶端請(qǐng)求多了一坨奇怪的東西
而問題就出在這個(gè) accept 上礁哄,它的作用就是告訴服務(wù)器,客戶端這邊想要什么類型的數(shù)據(jù)溪北,從圖中可以看出桐绒,它被設(shè)置成了 text/html,因此返回的數(shù)據(jù)類型就變成了 string
手動(dòng)給 accept 設(shè)置一個(gè)值之拨,問題就可以解決了
const {data} = await useFetch('/h5/getUserInfo', {
baseURL: 'https://test.api.com',
headers: {accept: 'application/json'} // 也可以設(shè)置成 */*
})
以上這個(gè)問題取決于服務(wù)器代碼邏輯茉继,如果服務(wù)器不判斷 accept 字段,固定返回 json 格式敦锌,就沒有這個(gè)情況
d. 錯(cuò)誤處理
公司接口返回的數(shù)據(jù)格式如下所示
{
bodyMessage: {} // 前端需要的數(shù)據(jù)
code: 0 // 狀態(tài)碼 0 表示成功馒疹,-1 表示鑒權(quán)失敗
subCode: 'BF11800' // 子狀態(tài)碼 最后兩位是 00 表示成功,其他表示失敗
message: '' // 錯(cuò)誤信息
}
因此每個(gè)請(qǐng)求都需要對(duì) code 及 subCode 進(jìn)行判斷乙墙,如果成功颖变,則處理 bodyMessage 里面的數(shù)據(jù),失敗則根據(jù)不同的 subCode 做不同的處理
為了方便听想,對(duì) useFetch 做了一個(gè)二次封裝腥刹,代碼如下
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) // 如果失敗汉买,就把整個(gè) data 拋出去
}
})
}
這種寫法在 axios 攔截器是沒有問題的衔峰,但這里不行
const {data, error} = await fetch(...)
console.log(error.value)
打印了一下 error.value, 本以為會(huì)是整個(gè)對(duì)象蛙粘,結(jié)果
經(jīng)過一番嘗試垫卤,最終得出如下結(jié)論
- 無法修改返回的 errorr 對(duì)象
- 如果 reject 一個(gè)字符串,那么 error 的 message 就是這個(gè)字符串
- 如果 reject 一個(gè)帶 message 字段的對(duì)象出牧,那么 error 的 message 就是這個(gè)字段
- 如果 reject 一個(gè)不帶 message 的對(duì)象穴肘,那么 error 的 message 為空
所以就可以先使用 JSON.stringify 將對(duì)象轉(zhuǎn)成字符串,然后通過 JSON.parse(error.value.message) 拿到這個(gè)對(duì)象
8. 第三方字體(未解決)
項(xiàng)目中用到了第三方字體舔痕,將字體文件放到 assets/font 目錄下评抚,然后在全局 css 文件中設(shè)置
@font-face {
font-family: "GoogleSans";
src: url("../font/GoogleSans-Regular.ttf");
font-display: swap;
font-weight: 400;
}
本地開發(fā)沒有任何問題,但是打包后字體文件不見了伯复,之后又把路徑改成絕對(duì)路徑慨代,問題依然存在
放在 public 里面可解決,但是不想什么都往 public 里面塞啸如,就想放在 assets 里面
之前在 nuxt2侍匙、vue2、vue3 中都是這么寫的组底,完全沒問題丈积,這坑爹的 nuxt3