标签: nuxt3

  • 【视频教程】技术栈大升级:Vue3 到 Nuxt3(4)深入理解 SSR 和 `useAsyncData`

    【视频教程】技术栈大升级:Vue3 到 Nuxt3(4)深入理解 SSR 和 `useAsyncData`

    2023 年,我个人最大的变化,是从 Vue3 SPA 应用向 Nuxt3 SSR 应用过渡,在预期可能存在 SSR 需求的项目中,都尽量使用 SSR。包括 React 应用,也尽量使用 Next.js,而不是 React SPA。

    这个过程中,面临很多问题,很多思路需要转换,很多以前没关注的点需要关注。本系列视频试图快速教会大家这些要点,帮助大家顺利从 SPA 切换到 SSR。

    这次的视频更偏理论,重点讲解 Nuxt3 如何处理 useAsyncData,以及为了兼顾 SSR 和前端开发所做的渲染策略设计。这部分知识我其实很晚才掌握,因为文档里说的也不太详细;所以既是好消息也是坏消息。好消息是,哪怕你没有掌握,也不太耽误使用 Nuxt3 开发项目;坏消息是,保不齐就会遇到一些奇怪的问题,难以复现和排错。

    视频要点:

    1. 现代化 SSR 的优势
    2. 深入理解 useAsyncData
    3. 使用 Pinia 传递数据
    4. 理解生命周期钩子变化

    如果你对 Vue3 开发、Nuxt3 开发、SSR 感兴趣,欢迎关注我的本系列。如果你对这些话题有疑问,欢迎留言讨论。

  • 【视频教程】技术栈大升级:Vue3 到 Nuxt3(2-3)升级实战 – 基础知识,适配 SSR,页面路由

    【视频教程】技术栈大升级:Vue3 到 Nuxt3(2-3)升级实战 – 基础知识,适配 SSR,页面路由

    2023 年,我个人最大的变化,是从 Vue3 SPA 应用向 Nuxt3 SSR 应用过渡,在预期可能存在 SSR 需求的项目中,都尽量使用 SSR。包括 React 应用,也尽量使用 Next.js,而不是 React SPA。

    这个过程中,面临到很多问题,很多思路需要转换,很多以前没关注的点需要关注。本系列视频试图快速教会大家这些要点,帮助大家顺利从 SPA 切换到 SSR。

    继上次的知识科普视频之后,我们开始实战。连续三次直播都比较失败,所以更新比较慢。第二期视频包含以下升级要点:

    1. 处理项目开发脚手架
    2. 迁移静态资源
    3. 修改引用地址
    4. 利用 process.client<client-only> 组件处理不能 SSR 的功能
    5. 处理 TailwindCSS
    6. 使用 useCookie 替代 localStorage

    第三期视频包含以下要点:

    1. 处理页面路由,理解页面嵌套
    2. 全局使用 css
    3. 附赠内容(拖时长):关于读源码

    后面应该还会再录制两期,分别是:服务器 API 开发;深入理解 SSR 与 useAsyncData+Pinia。

    如果你对 Vue3 开发、Nuxt3 开发、SSR 感兴趣,欢迎关注我的本系列。如果你对这些话题有疑问,欢迎留言讨论。

  • iOS Safari 播放音频的技巧分享

    iOS Safari 播放音频的技巧分享

    开发 Web App 一直是个蛮尴尬的事情。一方面,Google 不断在推;PWA 等技术也越来越好;另一方面,当我们真的像用 Web 开发 App 的时候,总会在不同地方踩到各种坑,猝不及防。尤其是 iOS Safari,各种 bug 层出不穷,坑死个人。今天分享一下在 iOS Safari 播放音频的一些技巧,希望节省大家将来的时间。

    基础:使用 new Audio() 创建播放器

    播放声音看起来并不难,在桌面上实现也比较简单,直接 const audio = new Audio(音频地址); audio.play() 就好了。只要你的音频文件可以正常访问,那你应该很快就能听到声音。

    这个过程实际上创建了一个 HTMLAudioElement 的实例。所以,你也可以在页面上直接插入一个 <audio>,然后通过某种方式拿到它的引用,再使用它播放音频。

    除了初始化的时候传入音频地址,在任何时候改变音频地址,然后再 .play() 都可以,很简单。

    在用户点击时提前创建播放器

    但是在 iOS Safari 上,就有问题。因为手机端的限制更严格,只有用户主动操作(点击、敲击)时,新建的 <audio> 才能正常播放。如果你在其它时间创建 <audio> 并播放,一切看起来都是正常的,没有任何报错,网络也会正常产生请求,但就是不会发出声音。

    解决方案就是在用户产生点击、敲击的时候,就把 <audio> 创建好,然后在需要播放的时候让它播放声音。

    双播放器切换实现连续播放

    有时候我们需要循环连续播放一首歌,比如游戏背景音乐,或者打坐的时候播一些背景音。这个时候我们也有两个选择:

    1. 使用 <audio>
      • 好处:流式播放,速度会更快;有 loop 属性,更方便。
      • 坏处:如果是长音频,由于流式播放的原因,用过的数据可能被随时丢弃,所以新的一遍开始时可能会卡 0.N 秒,无法避免
    2. 使用 Web Audio API 等更高级的 API
      • 好处:音频在内存里,想怎么播怎么播,还可以加入各种效果
      • 坏处:第一次播需要完成加载,或者手动实现音频流,很麻烦

    我建议用第二个方法,使用起来比较简单。只要在开始的时候创建两个 <aduio> 对象,然后在第一个播放结束前 250ms 启动第二个即可。至于为什么是 250ms,因为按照规范,onTimeUpdate 的触发周期是最长不超过 250ms,所以这样可以比较好的切换播放器。

    以下是我在 Nuxt3 里使用的 composable,供大家参考:

    import type { AudioOptions } from '~/types';
    
    export function useAudio({
      src,
      loop,
      loopOverlap,
      autoplay,
    }: AudioOptions = {}) {
      const audio = shallowRef<HTMLAudioElement>();
      const backup = shallowRef<HTMLAudioElement>();
      const isStarting = ref<boolean>(false);
      const isPlaying = ref<boolean>(false);
      const playhead = ref<number>(0);
      const duration = ref<number>(0);
    
      async function playAudio(src: string, overlap: number = 0.25): Promise<void> {
        if (!audio.value) {
          initAudio(src);
        }
        if (audio.value) {
          audio.value.src = src;
          if (loop && backup.value) {
            backup.value.src = src;
          }
        }
        if (overlap !== undefined) {
          loopOverlap = overlap;
        }
        isStarting.value = true;
        await audio.value?.play();
      }
      function pauseAudio(): void {
        audio.value?.pause();
        backup.value?.pause();
      }
      function initAudio(src: string): void {
        audio.value = new Audio(src);
        if (src) {
          audio.value.preload = 'auto';
        }
        audio.value.autoplay = autoplay || false;
        if (loop) {
          backup.value = new Audio(src);
          backup.value.preload = 'auto';
          backup.value.autoplay = false;
          backup.value.addEventListener('ended', onEnded);
          backup.value.addEventListener('timeupdate', onTimeUpdate);
          backup.value.addEventListener('play', onPlay);
          backup.value.addEventListener('playing', onPlaying);
          backup.value.addEventListener('pause', onPause);
          backup.value.addEventListener('loadedmetadata', onMetadata);
        }
        audio.value.addEventListener('timeupdate', onTimeUpdate);
        audio.value.addEventListener('loadedmetadata', onMetadata);
        audio.value.addEventListener('ended', onEnded);
        audio.value.addEventListener('pause', onPause);
        audio.value.addEventListener('play', onPlay);
        audio.value.addEventListener('playing', onPlaying);
      }
      function destroy(): void {
        if (audio.value) {
          audio.value.removeEventListener('timeupdate', onTimeUpdate);
          audio.value.removeEventListener('loadedmetadata', onMetadata);
          audio.value.removeEventListener('ended', onEnded);
          audio.value.removeEventListener('pause', onPause);
          audio.value.removeEventListener('play', onPlay);
          audio.value.removeEventListener('playing', onPlaying);
          audio.value = undefined;
        }
        if (!backup.value) return;
        backup.value.removeEventListener('ended', onEnded);
        backup.value.removeEventListener('loadedmetadata', onMetadata);
        backup.value.removeEventListener('timeupdate', onTimeUpdate);
        backup.value.removeEventListener('play', onPlay);
        backup.value.removeEventListener('playing', onPlaying);
        backup.value.removeEventListener('pause', onPause);
        backup.value = undefined;
      }
    
      function onPlay(): void {
        isPlaying.value = true;
      }
      function onPlaying(): void {
        isStarting.value = false;
      }
      function onPause(): void {
       isPlaying.value = false;
     }
      function onEnded(event: Event): void {
        if (loop) {
          (event.target as HTMLAudioElement).currentTime = 0;
        } else {
          isPlaying.value = false;
        }
      }
      function onTimeUpdate(): void {
        playhead.value = audio.value?.currentTime || 0;
        if (!loop) return;
        if (!playhead.value || !duration.value || playhead.value < duration.value - loopOverlap) return;
    
        backup.value?.play();
        // swap audio and backup
        const temp = audio.value;
        audio.value = backup.value;
        backup.value = temp;
      }
      function onMetadata(event: Event): void {
        duration.value = (event.target as HTMLAudioElement).duration || 0;
      }
    
      if (src) {
        initAudio(src);
      }
    
      return {
        audio,
        isStarting,
        isPlaying,
        playhead,
    
        playAudio,
        pauseAudio,
        destroy,
      };
    }
    

    使用 macOS Safari 调试 iOS Safari

    Safari 烂归烂,但是有一点:iOS Safari 的 bug 大多可以在 macOS 上复现。所以大多数时候,我们可以直接在 macOS 完成调试。但是有时候,我们必须在 iOS 上进行调试,比如涉及到输入框影响屏幕高度、或者上面提到的播放问题。

    于是,使用 macOS Safari 调试 iOS Safari 也是必备技能,需要的同学请看我的这个视频:

    总结

    我有时候会怀疑,苹果是不是故意劣化 Safari,以避免被 Web App 抢走原生 App 的市场份额,不然 iOS Safari 的 bug 不应该这么多。

    如果大家对 Web App 开发感兴趣,有相关的问题,欢迎留言讨论。

  • 【视频】技术栈大升级:Vue3 到 Nuxt3(1)基础知识篇

    【视频】技术栈大升级:Vue3 到 Nuxt3(1)基础知识篇

    2023 年,我个人最大的变化,是从 Vue3 SPA 应用向 Nuxt3 SSR 应用过渡,在预期可能存在 SSR 需求的项目中,都尽量使用 SSR。包括 React 应用,也尽量使用 Next.js,而不是 React SPA。

    这个过程中,面临到很多问题,很多思路需要转换,很多以前没关注的点需要关注。本系列视频试图快速教会大家这些要点,帮助大家顺利从 SPA 切换到 SSR。

    本期视频主要介绍 SSR 所需的知识和概念,为下一阶段正式重构项目做准备。

    1. 什么是 SSR?为什么要用 SSR?
    2. SSR 的一般构成
    3. Nuxt3 的 SSR 组件
    4. Nuxt3 的渲染规则与缓存处理
    5. 如何鉴别用户身份

    视频中的课件:从 SPA 到 SSR,从 Vue3 到 Nuxt3

    有任何问题、意见、建议,欢迎留言弹幕私信与我交流。如果你觉得视频对你有所帮助,还请留下宝贵的一键三连,并完播分享,谢谢。

  • Nuxt3 如何传递 Token

    Nuxt3 如何传递 Token

    昨天我在思否上看到这个问题:nuxt3 请求 token 要怎么传?我之前也被这个问题困扰过,我猜测不少刚刚从 Vue3(SPA) 转向 Nuxt3(SSR) 开发的同学也会受困于这个问题,所以就把答案扩展一下,写成博文,分享给大家。

    Vue3(SPA)里使用 Token

    在 Vue3 开发的单页应用(Single Page Applicati,SPA)里使用 Token 比较简单。一般来说,我们会在项目初始化的时候读取 localStorage,拿到标记用户身份的 token,然后放到内存当中,在请求时放到 header 里面发出。如果找不到 token,就认为用户没有登录,那么就通过路由将用户指引到登录页,或者不需要登录也能看的页面。

    使用 Token 可以避免 CSRF 攻击,因为从外部网站无法同时满足访问 localStorage,和将信息放入 http header 这两个条件。所以在单页应用里,使用 token 验证用户身份非常常见,有着各种开源、强壮、高效的实现。

    这个过程相信做过 SPA 的同学都很熟悉,不多说了。

    Nuxt3 请求的特点

    从 Vue3(SPA) 到 Nuxt3(SSR),主要涉及一个思路的转换。

    在 Vue(SPA)里,所有请求都是  请求,即完成网页加载、JS 执行完毕后,再发起请求。这些请求,有以下特点:

    1. 可以认为完全由开发者控制,即开发者确定请求发起的条件,然后在基本确定的时候发起请求。
    2. 这些请求大部分只请求数据,且有明显的特征,比如 HTTP Header 里包含 Authorization
    3. 这些请求里基本不包含静态资源。

    在 Nuxt 里,因为 SSR 的存在,所以请求至少可以分成两类:

    1. 数据交互类,跟 SPA 里基本一致。
    2. 页面渲染类,SSR 核心所在。

    后者最大的变化是:它会影响到 HTML 的内容,而通常来说,HTML 会被视作静态资源。我们知道,在网络环境里,存在大量缓存节点,假如跟用户相关的敏感数据渲染成 HTML,缓存到 CDN 当中,会是非常严重的安全问题。所以 Nuxt 在 SSR 时,不会携带 cookie;只有当页面渲染完成,由于用户操作而主动发起的请求里,才会携带 cookie。

    怎么识别 SSR 请求

    Nuxt2 时期,使用 Option API 的时候,有一个很明确的 asyncData() 函数,它会在 SSR 阶段被调用,发起请求获取数据。

    Nuxt3 时期,Composition API 会稍嫌模糊一些。这个时候,整个 setup() 函数都会在 SSR 期间执行,而 useFetchuseAsyncData 则负责在这个阶段发起请求,获取数据。因为这个阶段是在服务器的 node.js 环境里执行的,所以自然无法使用用户的 localStorage,也无法使用用户本地存储的 token。

    但是因为打开网页的请求是由用户发起的,所以其实用户有把自己的 cookie 发给服务器。如果我们希望 SSR 阶段用 cookie 鉴别用户身份,可以手动将 cookie 放在 useFetchuseAsyncData 产生的请求当中,参考 官方文档 useRequestHeaders

    基本上,假如我们不了解 Nuxt3 的设计思路,简单看了眼文档就上手写代码,大概率所有页面数据相关的请求都会是 SSR 请求。至少我是如此。

    使用 server: false 仅在网页里发起请求

    当然,Nuxt3 提供了只在网页里发起请求的方法,也就是基本等效于我们之前在 SPA 里发起请求的方法,那就是在 useFetchoptions 里标记 server: false

    const { pending, data: posts } = useFetch('/api/comments', {
    server: false
    })

    再演示个更扣题的方式

    const { pending, data: posts } = useAsyncData(
    'posts',
    async () => {
    const token = localStorage.getItem('token');
    const data = await $fetch('/api/comments', {
    headers: {
    Authorization: `Bearer ${token}`,
    },
    });
    return data;
    },
    {
    server: false,
    }
    );

    (代码高亮插件还是崩的,大家凑合看,等我回头修……)

    详情请看 官方文档

    这样的请求就会在页面完成渲染后,才会发起请求。在这些请求里,如果需要使用 token,就按照以往的方式使用,即可。

    server: falselazy: true 的区别

    如果大家仔细看 Nuxt3 官方文档,会发现还有一个参数:lazy: true,以及两个语法糖函数:useLazyFetchuseLazyAsyncData

    这两个函数也是先加载页面,再加载数据。它们与 server: false 的区别在于,后者是把全部请求都放在网页端进行;而 lazy: true 则只在网页跳转的时候起作用。

    不使用 lazy: true 的情况下,当我们从 A 页面进入 B 页面时,如果 B 页面里需要加载数据,Nuxt3 就会等待数据加载完成之后,再进行页面渲染。如果你使用一个比较慢的网络,页面切换就会卡卡的,体验非常明显。

    如果使用 lazy: true,页面切换一下就完成了,但是数据可能还没加载到,此时我们需要手动处理加载状态。比如放个转转的菊花在页面中间。但是如果我们直接打开 B 页面,Nuxt3 还是会先把数据请求回来,完成页面渲染,然后呈现完整的 HTML 给我们,这样可以确保 SEO 效果。

    server: false,则会彻底放弃这部分数据的 SSR,对于用户身份相关的数据,就比较合适。

    总结

    其实就跟当年从前端学 Node.js 一样,新技术最大的挑战在于:使用场景不同、运行环境不同,思路需要很大变化,需要不同的理解,不能照搬以前的做法。

    Nuxt3 涉及到网页渲染,涉及到网络链路上的缓存层,为了防止出现信息泄漏,它会主动放弃一些数据传输。同时,由于它的一部分运行环境在服务器上,所以有一些用户数据它没法使用。于是在开发 Nuxt3 应用的时候,我们必须搞清楚这些变化,然后作出适当的调整。

    希望上面文章对大家有所帮助。如果文中有问题,请不吝指出。如果你对 Vue3、Nuxt3 开发有兴趣,有疑问,欢迎留言评论。

  • Bug求助+悬赏

    Bug求助+悬赏

    哎,老革命遇到新问题,创业+远程工作之后,想拉几个人跟我一起排查 bug 也找不到,只好公开求助+悬赏。

    首先请大家看下面这段视频:

    故障描述

    简单来说,我们做了一个网站:Dailylift.io。在这个网站上,你能描述自己今天的状态,然后得到一些心灵慰藉:你信仰的目标会给你写一封信。

    这个功能并不复杂,我选择用 Nuxt3 + Supabase 作为技术栈。我的理由是:

    1. 因为我们需要 SEO,我希望利用 Nuxt3 SSR 功能提升 SEO 效果。
    2. Supabase 提供包括开箱即用的用户系统在内的一系列 Serverless 功能,可以帮助我们的开发提速。
    3. Supabase 提供 Nuxt3 SDK,比 Auth0 更方便。
    4. Supabase 提供 VectorPG,方便我们使用 Embedding

    但是实际运行起来之后,却发现一些我未曾预料到的问题,主要跟用户登录状态有关。具体复现步骤为:

    1. 用户未登录时,先填写感受和目标,然后点“收信”,我们会要求他登录
    2. 用户登录后,我们需要先检查他的收信记录,确保每天只有收一封信
    3. 接下来,如果他今天还没有收信,我们就继续之前的过程,给他写信
    4. 但是偶尔,前面都正常,登录之后就卡住了,无法进入发信流程

    我尝试了很多方案,想解决这个问题,每次我都觉得自己搞定了,我甚至还写了一篇博客作为分享:使用 Vercel、Supabase、Stripe 打造 OpenAI 计费系统:2. 处理用户注册/登录。但是之后老板就会再次遇到这个问题,然后把我一通数落。我怎么也找不到稳定重现的方式,只能反复查验代码,希望找到其中的漏洞。

    我有几个怀疑:

    1. Nuxt + Supabase,理论上存在两个环境有用户态:SSR与浏览器。我不确定这里会不会有问题。比如,为了防止 refresh token 的时候触发 onAuthStateChange,我用 useSupabaseUser 获取初始用户状态。会不会是它导致后面判断出错了(很小概率)?
    2. 我需要先检查用户的邮件历史,再决定是否请求信件。这里可能也会有问题,比如两次请求之间的先后顺序。
    3. Supabase SSO 只支持 redirect,必须跳走再回来,所以浏览器里的状态无法保存。从第三方回来,处理 access_token 期间,可能因为网络状态、本地内存状态等,进入到某个我没处理的分支,导致失败。
    4. 登录之后,onAuthStateChange 到用户登录彻底完成之前,中间其实有一小段时间,会不会是网络问题导致它被卡住了?

    总之吧,我现在一个人开发,面对这些问题有些焦头烂额,所以希望所有看到这篇博客的同学,能帮我测试一下网站:Dailylift.io,帮我想想其中可能存在的坑,如果您有处理类似问题的经验,也请多分享一些。无论您:

    1. 帮我找到稳定重现的方式
    2. 帮我固定住 bug 出现的现场
    3. 帮我找到代码中潜藏的问题
    4. 引导我找到正确的方向
    5. 分享有价值的经验

    我一定大红包伺候!感谢大家,期待帮忙。


    更新:

    2023-08-31

    今天我早上测试的时候,遇到一个很诡异的问题:我通过 Google 登录后,遇到一个加载信件列表 400 错误,导致获取新信件失败。

    看路径和参数,这个请求应该是正常的。最诡异的是,我在 Devtools 里看不到这个错误,只能看到两个 GET /letters 的请求,都是正常 200。

    我现在怀疑这个问题是不是这样的:

    1. SSR 阶段它也会发起一个请求
    2. 这个请求在某个情况下会失败
    3. 这个失败会导致渲染出的页面携带着失败的状态,继而无法继续。

    另外,因为我们使用 TiDB Serverless,会不会是冷启动时会产生长延迟,导致 Vercel Serverless 失败?不过我看了数据库,也有很多是几个小时后成功的,也不太像。

  • 使用 Vercel、Supabase、Stripe 打造 OpenAI 计费系统:2. 处理用户注册/登录

    使用 Vercel、Supabase、Stripe 打造 OpenAI 计费系统:2. 处理用户注册/登录

    嗯,是的,我回来填坑了。既然要打广告,就要对得起各位金主,总是水文是不行的,还是要输出干货。

    广告继续。本博客即日起开始常年招商,欢迎各位想推广产品的老板投广告,目前定价 4800/年,亦可增加评测文章、教学文章配合,详情可与我联系。具体详情更新在 本站广告 2023 年招商


    Supabase 用户体系入门

    Supabase 提供 Serverless 数据库,自带用户体系,开发用户系统非常简单。而且 SDK 很完善,适配各种框架,大部分功能开箱即用;官方教程方面做得也很好。所以,首先请大家先认真阅读这两篇官方文档、教程:

    第二篇教程文章详细的介绍了使用 Supabae 开发用户管理系统的过程,配合上面的文档,相信大家都可以轻松入门。唯一的难点(对部分同学来说)可能是英文,我就先不复述。

    接下来的文章主要介绍官方没有细说的最佳实践和各种实用细节。

    邮件登录与邮件模版

    虽然官方提供了很多方案,但是我想,对于大部分同学来说,邮件+密码登录才是最常用的,最多再加上第三方比如 Gmail 登录。

    Supabase 提供了发邮件的功能,方便用户校验邮箱、找回密码。但是,正如很多邮件服务器那样,它的到达率并不理想。我猜测有些用户即使没有滥用 Supabase 的邮件服务,也会因为邮件模版过于简单而被误杀。所以大家第一步应该先配置邮件模版,尽量多体现自己的产品相关信息,尽量跟大家的通用模版区分开,减少误报误杀的可能。

    Supabase 默认要求用户校验邮箱之后才能登录,如果你暂时不关心这一点,可以考虑在 Auth ProviderS > Email关掉这个功能。此后,用户完成注册后就可以登录,不过他们的用户状态在数据里仍然是未验证,我们也可以借此对用户进行一些差异化的控制。

    注册与登录,自动注册

    有一点我不太喜欢。Supabase 提供两个方法:signUpsignInWithPassword,分别用来注册和登录。但是通常情况下,我并不希望区分这两个方法。我认为我们的网站对用户来说,只是一个小工具,我觉得他们不会在意是否在这里注册过,他们只希望做他们要做的事情,注册登录只是我们为了方便自己而做的步骤。

    所以我一般仍然会通过不同的路径区分用户是想注册,还是想登录。但是我会在用户 signUp 后检查错误信息,如果是重复注册,就自动帮他们登录。

    const { data: { user }, error } = await supabase.auth[method]({
      email,
      password,
      options,
    });
    if (error) {
      // registered, login
      if (error.message === 'User already registered') {
        return login(email, password);
      }
    throw error;
    }

    Nuxt3+Pinia 实现用户身份校验及额度查询

    教程中的例子比较简单,通常来说我们的产品会复杂很多,比如,我们可能需要全局状态管理工具,方便不同页面不同组件中使用。在 Nuxt3 中使用 Pinia 需要用到特有的 Module:@pinia/nuxt,简单安装官方教程配置一下即可。这里主要分享我摸索的其它未提及要点。

    额度查询

    Supabase 在创建项目的时候已经帮我们建好了用户表,并且放在默认不对外的库里面。所以我们最好不要直接修改表,而是如教程所讲,创建一个新的表,把用户账户相关的其它数据放进去,比如在这样一款典型的 ChatGPT 产品中,就是帐户额度,然后通过关联 id 的方式访问。

    这就带来一个问题:什么时候去读这个表?

    对 SPA 来说,这个问题比较简单,登录之后,以及网页初始化的时候读取即可,反正所有请求都由我们全权控制。但是对于 Nuxt3 来说就值得讨论一下。因为 Nuxt3 要进行服务器端渲染,所以我们通常希望它在渲染页面的时候,这个数据已经就位了。

    这里我推荐配合 Pinia Store,使用 supabase.auth.onAuthStateChange 函数。我会建立一个 userStore,在里面完成用户注册、登录、登出等动作,同时注册 onAuthStateChange 事件,完成用户状态变更后的处理。

    const useUserStore = defineStore('user', () => {
      const supabase = useSupabaseClient();
      const user = useSupabaseUser();
    
      async function login(email: string, password: string, isSignUp = false): Promise<void> {}
      async function loginWithGoogle(): Promise<void> {}
      async function logout(): Promise<void> {}
      
      // 注册事件处理函数
      supabase.auth.onAuthStateChange((event, session) => {
        switch (event) {
          case 'INITIAL_SESSION':
          case 'SIGNED_IN':
          // 在这里处理登录后和初始化时拿到用户身份后的操作,比如加载用户账户的额度
          // 这里需要注意:此时 `user.value` 尚未被填充,所以我们不能依赖它。有两个方案:1. 延迟执行 `setTimeout`;2. 直接从 `session` 参数里拿到用户信息然后处理
          // 我的做法是,如果需要请求 API,那就选择方案2;否则,使用方案1,等 100ms
        }
      });
    });
    export useUserStore;

    请仔细看上方代码的注释,非常重要,看起来很简单,但其实是血泪铺就的道路。

    判断第三方登录,及 UI 呈现

    当用户选择第三方登录,比如 Gmail 的时候,Supabase 目前只支持跳转这一种方式,不如 Auth0,可以用 popup,省很多事。

    完整的第三方登录流程是:

    1. 跳转到第三方登录页面
    2. 登录
    3. 跳转到 Supabase 中间页
    4. 带着 access_token 跳回我们的网站,Supabase SDK 负责读取 access_token,验证用户身份等工作

    其中涉及到两个(或更多)网站的配置,因为文档中写的有,我就不再详述,需要的同学留言我再补充。

    最麻烦的是(4),跳转回来之后,从页面初始化成功到完成登录,中间可能会隔很久,短则1、2秒,长则10s 都有可能。这期间用户可以继续操作,会产生各种误会和误操作。这里我建议:

    1. 登录时传递 redirectTo,要求最终跳转页面是某个登录落地页
    2. 初始化的时候,检查 URL 里的 #access_token=
    3. 如果有,说明是第三方登录返回,然后就显示登录中的状态
    4. 等待 user.value 变化,说明完成登录,跳转到原先应该去的页面,或者继续之前没做完的操作
    5. 如果没有 access_token,直接跳转到其他落地页。

    总结

    上面这些内容看起来不复杂,但实际上对用户体验影响很大,并且文档、教程说得也不清楚,导致我的老板对产品一直不太满意。希望这篇文章可以节省大家的宝贵时间。

    如果有关于 Supabase、Vercel、Nuxt3 的问题,欢迎评论里面留言。

    系列文章

    1. 使用 Vercel、Supabase、Stripe 打造 OpenAI 计费系统:1. 系统篇
    2. 【本文】使用 Vercel、Supabase、Stripe 打造 OpenAI 计费系统:2. 处理用户注册/登录

    建议阅读

  • Nuxt 3 最佳实践与常见问题

    Nuxt 3 最佳实践与常见问题

    最近比较忙,也有些犯懒,所以博客视频都没怎么更新。今天一看,竟然断更将尽一个月,那无论如何得攒点东西。就把前阶段的笔记贴出来吧,是我使用 Nuxt3 的一些经验心得,希望对大家有所帮助。

    如果大家在使用 Nuxt3 期间有经验可以分享,或者问题需要讨论,欢迎留言。

    Nuxt 3 简介

    Nuxt.js 是一个基于 Vue.js 的服务端渲染应用框架,可以帮助开发者快速构建出高性能、SEO 友好的 Web 应用。Nuxt.js 3 是该框架的最新版本,它具有更快的启动时间、更快的构建时间和更先进的开箱即用功能。

    然而,在使用 Nuxt.js 3 进行项目开发时,我遇到了不少问题。本文会记录这些问题,以及我的经验。

    最佳实践

    下面是我从之前的问题里找到的最佳实践,应该问题不大,可以照搬。

    部署

    Nuxt 3 可以直接部署在 Vercel,兼具 SEO 与高用户体验。

    Serverless function

    默认情况下 Nuxt3 /api 和渲染都走 serverless function,理论上要弱于 Edge Function。我曾试着切换到 Edge Function,服务立刻就挂了,回头找个小服务摸索一下试试看。

    目前来看效能还可以,暂时没有体感延迟。我建议大家不要强求,把切换功能留给 Nuxt 核心团队来处理吧。

    加载远程数据

    SSR

    使用 useFetchuseAsyncData,Nuxt 会先加载远程数据,然后渲染页面,再进行页面的跳转。

    注意:SSR 阶段,组件发出的请求不会携带 cookie,这个设计是有意为之,避免个性化数据错误的出现在缓存层里面。如果希望 SSR 中包含个性化数据,可以手动操作,但请务必小心,不要泄漏不必要的请求头,因为可能带来安全隐患。

    具体做法可以参考官方文档:Passing Headers and cookies

    先跳转,再加载

    使用 useLazyFetchuseLazyAsyncData,Nuxt 不会等待请求完成,它会先渲染页面并跳转,然后在数据加载完成之后,更新视图。

    平时加载数据

    如果我们要像平时使用 Vue 那样加载数据并渲染,最好使用 $fetch

    使用 pendingrefresh 等复用请求代码

    比如页面里我们远程加载了一个对象,然后接下来,我们要重新加载这个对象,或者我们要根据某个参数加载别的对象,就可以利用 useFetchuseAsyncData 等返回的 pending 来渲染状态,或者用 refresh 重新发起请求,复用请求代码。

    尽量使用 default() 返回默认值

    很多时候我们远程加载的数据不是简单对象(文本、数字等),而是某个比较复杂的对象,比如一篇文章、一组人员数据。通常来说组件里的模版就负责渲染这组数据,如果不返回默认值,渲染可能会出错,可能导致页面跳转失败。

    所以我建议大家尽量返回默认值。

    配置 routes

    Nuxt 默认会对每个页面进行服务器端渲染,但很多时候我们并不需要,比如各种后台页面。所以我建议项目起始阶段就把各页面的渲染策略制定好。

    参考文档:Rendering Modes · Nuxt Concepts对首页、关于我们之类的纯静态页面,可以启用预渲染(preredering);对后台页面,就禁用 ssr;等。

    但是,大家也要小心错误的缓存机制,可能会把不应该缓存的页面缓存下来,造成渲染出错。

    使用 localStorage

    我们知道,Nuxt3 最大的特性就是 SSR,它会在服务器端渲染完页面,再返回给客户端。而 localStorage 顾名思义,只存在于用户本地,服务器端并不知道它的存在,也无法使用。所以如果我们要像平时那样使用 localStorage,我们必须控制好只在用户本地使用。如果要在 Nuxt3 里读取 localStorage 里的数据,我建议在 layout 里进行。经我测试,这是我们最初能插入 JS 的位置。

    // <script setup>
    import useStore from '~/store';
    
    const store = useStore();
    // 只有在客户端的时候才执行
    if (process.client) {
      store.init();
    }

    已有解,寻求最优解

    接下来的问题,我的方案能解决,但我不确定是否是最好的方案。所以我也在寻找更好的做法。

    1. useAsyncData 难以打断点

    问题

    useAsyncData 会在服务器端执行,所以在 Devtools 里打断点可能拦截不到。

    解法

    我目前的解法是在里面插入 console.log() ,利用热更新,切换页面,让断点生效。

    2. onBeforeMount 读取 localStorage 会丢失响应式

    版本

    v3.4(新版本待测试)

    问题

    非常诡异。

    我有一个变量 myRate ,因为一些理由,所以要把数据存在 localStorage 。我们知道 nuxt 在服务器端渲染的时候没有 window 对象,所以我就把它放在 onBeforeMount 。

    但是这样就会导致 myRate 丢失响应式,且只在 input className 丢失,非常诡异。

    解决

    我暂时放在 onMounted,虽然会造成页面抖动,但是工作正常。

    问题

    页面中有一个 nuxt-link 生成的 <a> ,点击无效,没有报错,浏览器和开发环境均无报错。

    解决

    实际上是 Vue 模版中某个变量名拼错了,渲染失败。但是不知道为何没有错误信息。找到并修正后,使用正常。

    暂时无解

    使用 route.hash 加载数据的话,SSR 会缺少数据

    当我们请求包含 hash 的地址,route.hash 在服务器端可能为空,导致无法正常启动服务器端渲染。请求会在渲染后再发出,造成界面抖动 CLS。

    解决方案就是不要用 hash 作为数据标记。

    @nuxtjs/i18n no_prefix 策略下,无法 SSR

    版本

    Nuxt@3.5 + @nuxtjs/i18n@8.0.0-beta.12

    问题

    1. 使用 no_prefix 策略
    2. detectBrowserLanguage: false
    3. 没有 SSR,渲染出来的是 key,然后变成目标语言

    Tips

    GET 请求里不能包含 body,否则可能 405

    如果用 useFetch 发起的 GET 请求包含 body/server/api 的接口会报告 405 Method Not Allowed 错误。

    这个问题主要不好排查。


    总结

    开始用 Nuxt 的时候,我没想到会有这么多问题:不就是提前渲染一下模版嘛,能麻烦到哪儿去?实际操作之后,发现很多熟悉的操作都不好处理,比如从 localStorage 里面取数据然后渲染到页面去,就要区分函数执行环境,否则就会报错;要渲染用户相关数据,得考虑服务器运行环境的复杂性,包括网络和多层缓存,不能想当然的使用 cookie 与 token。

    总之,心怀敬畏,持续学习,多看文档。有问题,欢迎提问;有建议,欢迎指教。大家一起进步。

  • 【视频】Nuxt3+Vercel+Serverless 全栈开发(5):部署到 Vercel+开发统计功能

    【视频】Nuxt3+Vercel+Serverless 全栈开发(5):部署到 Vercel+开发统计功能

    课程继续。仍然结合近期的开发经验,分享最近我比较喜欢的全栈+高效+免费+云原生技术方案。

    https://www.bilibili.com/video/BV16h411N762/ (请 B 站有号的同学帮忙点赞)

    本次课程内容:

    • 将作品部署到 Vercel
    • 使用自定义域名保障国内访问
    • 开发统计功能页面

    终于讲到部署环节了。其实 Vercel 对现代化 Web 全栈开发是非常重要的组成部分,它自带的 Serverless 功能是实现服务器端渲染(SSR)的关键,而且我们也需要使用 Serverless 来完成数据交互。部署到 Vercel 非常简单,点几下就能完成。

    Vercel 官方网站 vercel.com 在中国大陆可以无障碍访问,但是免费提供的二级域名 *.vercel.app 不行。解决方案很简单,自己注册一个域名,然后 CNAME 过去即可。注册域名的选项很多,各大云平台如阿里云、腾讯云均可。自己的小域名如果不提供大规模服务,不备案也可以用。价格大概一年十几块吧。

    然后讲解制作统计页面,主要涉及 Redis 的操作和前后端数据交互。之前埋下的数据结构设计,在此可以更好的演示。

    至此,这个系列基本连载完成。从技术选型、到项目搭建、到前后端开发、到最终部署上线,都覆盖到了。其它想讲的内容还有很多,不过都属于锦上添花,对于有开发经验的同学来说,相信现在已经可以动手。未来当然会继续深入,把统计功能加强一些,把关系型数据库也纳入进来。进阶课也在规划中,比如面对内容更庞大的网站,这个项目中太过简单的数据交互、缓存处理、SEO 都不够用,需要进阶强化。敬请期待吧。

    本周正好端午节,休息一周,周三(6月21日)晚上直播答疑,欢迎围观。


    这期视频也剪得很细,还做了字幕,希望大家能学到东西。

    如果你有任何问题、建议,欢迎留言讨论。请 b 站有号的同学帮忙分享完播一键三连,谢谢大家。

    另外,我也在 YouTube 上上传了一份,大家有空的话,麻烦帮忙关注下我的油管频道,感谢感谢。肉山全栈小课堂 – YouTube

  • 【视频】Nuxt3+Vercel+Serverless 全栈开发(4):投票接口+数据处理+了解服务器端渲染

    【视频】Nuxt3+Vercel+Serverless 全栈开发(4):投票接口+数据处理+了解服务器端渲染

    课程继续。仍然结合近期的开发经验,分享最近我比较喜欢的全栈+高效+免费+云原生技术方案。

    本次课程内容:

    • 创建 Post 接口,完成投票
    • 显示投票结果数据
    • 了解服务器端渲染

    我们先利用 Nuxt3 的路由机制,创建 /api/rate.post.ts 接口,用来处理用户投票,将数据记录进入 Redis 数据库的动作。

    为了保证数据安全,我每次会写入两次数据,一个键值是 $$$_{uid},主 key,用来日常显示;另一个键值是 {uid}_{小时},用来备份。这样假如某些情况下我们的投票系统被攻击,也能快速会滚到最近一小时的数据。并且这个备份频率也可以按需调整。

    然后我们在前端使用 $fetch 方法调用接口完成投票。并介绍了计算成绩的逻辑。

    最后介绍了 SSR 的结果,这样就能解释,为什么我们要使用 Nuxt3 内建的 useAsyncDatauseFetch 而不是平时常见的远程请求库手动请求。

    这期视频也剪得很细,还做了字幕,希望大家能学到东西。

    如果你有任何问题、建议,欢迎留言讨论。请 b 站有号的同学帮忙分享完播一键三连,谢谢大家。

    另外,我也在 YouTube 上上传了一份,大家有空的话,麻烦帮忙关注下我的油管频道,感谢感谢。肉山全栈小课堂 – YouTube