1249 字
6 分钟
苦战之登录页的自动保存apiKey功能略有设计感

场景描述#

前端开发, 写的是登录页 (不过没有实际后端登录接口) 整个项目没有账号的概念, 是通过在请求的headers里放key来鉴权的

所以在登录页的时候只需要输入一行key就行

alt text

功能设计#

因而 我自发的设计了两个功能

  1. 缓存key, 并且在每使用时自动调用缓存里的key补全输入框(这样之后要是你还是手动来到login页,就可以直接点击登录不用再输一遍key了)

  2. 检测输入框有无内容,无内容时登录按钮 disabled 无法点击

按钮我使用的是<el-button> 因而disabled功能就是使用它的 :disabled 属性来实现的

初版设计#

既然要进缓存, 那正常来说就得直接调localStorage, 不过这有点不是很优雅, 所以项目里创建了一个 useApiKey.ts来把这份缓存单独管理(这样就不用手动调取localStorage了) 具体代码如下

import { useStorage } from "@vueuse/core";

export const API_KEY_STORAGE_KEY = "xxxxxxxxxxxxxxxxx";

export const useApiKey = () => {
  const apiKey = useStorage(API_KEY_STORAGE_KEY, "");

  return apiKey;
};

useStorage是vueuse的一个方法, 是创建一个localStorage名为 API_KEY_STORAGE_KEY 的对象 所以在login页代码里我只用这么写

<el-input
    v-model="apiKey"
/>
<!-- handleConfirmApiKey相当于登录函数(校验下key对不对) -->
<el-button
    :disabled="apiKey.length <= 0"
    @click="handleConfirmAPIKey"
>
    登录
</el-button>
const apiKey = useApiKey();

然后在要使用到Key的页面里, 也只需要用useApiKey() 这个 Composable 就获取到一个优雅的叫做apiKey的变量啦

问题#

当然, 初版设计一定是有问题的, 不然就不会是初版设计了; 首先这次设计的优点当然是优雅——代码量也少, 使用方式也非常的舒服

问题在于<el-button>:disabled属性, 这个属性传入的是一个 boolean 值, 自动更新非常的烂, 于是就导致了一个bug alt text 这里, 我在已有apiKey缓存的情况下进入页面(或者原地刷新), 会出现输入框里有字, 但是<el-button>:disable=true的情况 大概链路我猜测如下 alt text 大概是这样:

初始化const apiKeyuseApiKey没初始化好, 这一小段真空期里apiKey就是个空字符串,

<el-button>:disable 又刚好挑这时候调用了apiKey的值,所以就错了;

v-model没问题则是因为它本身的一个响应式做的太硬了, 同步性太强了;

当然, 这问题是有解决方案的, 如下

<el-input
    v-model="apiKey"
/>
<el-button
    :disabled="isDisabled"
    @click="handleConfirmAPIKey"
>
    登录
</el-button>
const apiKey = useApiKey();

const isDisabled = ref();

onMounted(() => {
  isDisabled.value = apiKey.value.length <= 0;
})

watch(apiKey, () => {
  isDisabled.value = apiKey.value.length <= 0;
});

也就是我手动把后续:disabled不会自动刷新的问题解决了, 我手动让它进行刷新(onMounted触发比apiKey初始化晚), watch则是让 isDisabled 变量与apiKey动态同步

效果大概如图描述(蓝色笔画部分) alt text

终版设计#

当然, 其实我直接把修改后的初版设计拿来用也是很不赖的, 但是我就是很不爽, 因为太多此一举了, 这解决方案不就是打补丁吗, 并非优秀的设计口牙;

初版里的问题我思考后觉得, 其实是链路设计的问题 初版里我是

初始化时:

  1. useApiKey → apiKey → 输入框;
  2. useApiKey → apiKey → 按钮

后续:

  1. 输入框 → apiKey
  2. apiKey → useApiKey
  3. apiKey → 按钮

后续的链路是没问题的, 但初始化的这两条链路; 有点奇怪不是么? 为什么我的起点一定得是这个useApiKey().

我应当在apiKey初始化时就让它带上正确的值, 而不是调用useApiKey()给到apiKey, 这样子会让apiKey在刚刚初始化好,而useApiKey()没初始化好的一段真空期里, 值是错的.

useApiKey()应当只接受从 apiKey到它的单链路 所以我设计了新的链路

初始化时:

  1. apiKey(直接调取localStorage) → 输入框;
  2. apiKey → 按钮

后续:

  1. 输入框 → apiKey
  2. apiKey → useApiKey
  3. apiKey → 按钮

不过其实写到最后我已经懒得维护这个useApiKey()了, 主要在其他地方也用处不大, 最后索性删了(当然按我这个设计来维护它是没问题的)

最后再展示下我的最终代码

<el-input
    v-model="apiKey"
/>
<el-button
    :disabled="isDisabled"
    @click="handleConfirmAPIKey"
>
    登录
</el-button>
// 写成如下的话, 用pnpm run dev跑会500 localStorage is not defined; 具体好像是ssr和ssg相关的原因, 但是背后的道理我并不是很理解, 此处不讲
// const apiKey = ref<string>(localStorage.getItem(API_KEY_STORAGE_KEY) || "");

const apiKey = ref<string>("");

onMounted(() => {
  apiKey.value = localStorage.getItem(API_KEY_STORAGE_KEY) || "";
})

watch(apiKey, (newVal) => {
  if(newVal) {
    localStorage.setItem(API_KEY_STORAGE_KEY, newVal);//这里还想维护useApiKey的化, 就改成给useApiKey赋值
  }
})

结语#

以上, 大概是对初始化异步的一次思考; 说实话其实感觉要不是<el-button>属性传入参数不能是ref, 同步性不够强, 也不会有这个bug我也不会整半天了

element-plus怎么这么坏啊!

苦战之登录页的自动保存apiKey功能略有设计感
https://zheyi.in/posts/前端/yibu/
作者
折乙
发布于
2025-09-21
许可协议
CC BY-NC-SA 4.0