美文网首页程序员
Nuxt3踩坑记录

Nuxt3踩坑记录

作者: codingZero | 来源:发表于2023-07-05 18:04 被阅读0次

    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 做了一个恶心的封装

    7. useFetch

    用惯了 axios,第一次使用 useFetch 相当的不习惯,途中踩了不少的坑

    关于服务端/客户端请求,useFetch/useLazyFetch,是否 await 的一些说明

    1. 未设置 server: false 的请求,只有在页面初次加载时,才是服务端请求,路由跳转时为客户端请求
    2. 服务端请求时,useFetch 与 useLazyFetch 没有区别
    3. 服务端请求时,是否 await 对页面渲染没有影响,只决定后面的代码是否会等待请求完成
    4. 初次加载页面,客户端请求即使 await,后面代码也拿不到数据
    5. 路由跳转时,请求前加上 await,useFetch 后面能拿到数据,useLazyFetch 不行
    6. 路由跳转时,会等待 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, 本以为会是整个对象,结果

    经过一番尝试,最终得出如下结论

    1. 无法修改返回的 errorr 对象
    2. 如果 reject 一个字符串,那么 error 的 message 就是这个字符串
    3. 如果 reject 一个带 message 字段的对象,那么 error 的 message 就是这个字段
    4. 如果 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

    相关文章

      网友评论

        本文标题:Nuxt3踩坑记录

        本文链接:https://www.haomeiwen.com/subject/uwufudtx.html