news 2026/10/2 15:43:52

使用 Hanko Elements Web Components 构建现代化登录与注册体验:安装、集成、定制与国际化实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用 Hanko Elements Web Components 构建现代化登录与注册体验:安装、集成、定制与国际化实战指南
  • 后端
  • 认证鉴权
  • 前端

【免费下载链接】hanko

Modern authentication, on your terms. Open source alternative to Auth0, Clerk, WorkOS, Stytch.

项目地址:https://gitcode.com/GitHub_Trending/ha/hanko
点击查看免费下载

Hanko Elements 是 Hanko 项目(一个开源的现代身份认证方案)提供的一套 Web Components 组件库,帮助开发者以最少的代码为网站带来现代化的登录、注册与个人资料管理体验。它基于 frontend/frontend-sdk 与后端的 Hanko API 协同工作,支持无密码认证(Passkey/WebAuthn)、邮箱验证码(Passcode)、密码、OTP 等多种认证方式。读完本文,你将掌握从 npm/CDN 安装、register()注册、嵌入<hanko-auth>等组件、通过 frontend-sdk 管理会话,到利用 CSS 变量与 Shadow Parts 深度定制 UI、配置多语言翻译的完整实战链路。

核心特性一览

Hanko Elements 的设计目标是"开箱即用且高度可定制",其核心能力包括:

  • 用户认证:提供安全、友好的用户认证处理方式,可嵌入任意 Web 应用;
  • 多种认证流程:支持无密码认证(Passkey)与生物识别认证(WebAuthn),同时支持 Passcode、密码、OTP 等流程;
  • Web 组件库:提供可定制的 Web Components,通过标准 HTML 标签即可嵌入;
  • 个人资料管理:通过<hanko-profile>组件让用户查看并管理自己的资料信息(邮箱、密码、Passkey 等);
  • 事件处理:为认证与会话相关事件提供监听机制,方便自定义用户体验;
  • 本地化与国际化:支持多语言并提供翻译选项,服务全球用户;
  • 集成灵活性:支持 CDN 或 npm 引入,同时兼容 TypeScript 与非 TypeScript 环境;
  • 视觉定制:可通过 CSS 变量、Shadow Parts 等方式对齐品牌风格与应用整体设计。

从仓库的 package.json 可以看到,@teamhanko/hanko-elements当前的入口为dist/elements.js,内部依赖@teamhanko/hanko-frontend-sdk、Preact 以及@teamhanko/preact-custom-element(用于将 Preact 组件注册为原生自定义元素)。

安装

可通过 npm、yarn 或 pnpm 将 Hanko Elements 安装到项目中;也可以直接通过 CDN 以模块方式引入(见下文"导入模块"):

# npm npm install @teamhanko/hanko-elements # yarn yarn add @teamhanko/hanko-elements # pnpm pnpm install @teamhanko/hanko-elements

快速使用:三步接入

要集成 Hanko,你需要从hanko-elements模块导入并调用register()函数,随后即可在 HTML 中使用这些 Web Components。一个可用的页面至少需要:

  1. 放置<hanko-auth>元素,让用户能够登录;
  2. 添加onSessionCreated事件处理器,在认证流程完成后自定义后续行为(例如跳转到其他页面)。

导入模块

使用 webpack、Parcel 等模块打包器时,在 TypeScript/JavaScript 文件中导入:

import { register } from "@teamhanko/hanko-elements";

偏好 CDN 时,使用<script type="module">指向托管hanko-elements包的 CDN 地址:

<script type="module"> import { register } from "https://cdn.jsdelivr.net/npm/@teamhanko/hanko-elements/dist/elements.js"; </script>

注册 Web Components

调用register()函数,将 Hanko API 的 URL 作为参数传入。该函数会把 Hanko 元素注册到浏览器的CustomElementRegistry中:

const { hanko } = await register("https://hanko.yourdomain.com");

也可以传入一组选项。从 src/Elements.tsx 的源码可以看到,register()内部会用下列默认值合并你传入的选项,并一次性注册hanko-auth、hanko-login、hanko-registration、hanko-profile、hanko-events五个组件:

const defaultOptions = { shadow: true, // 设为 false 时不将 web component 挂载到 shadow DOM。 injectStyles: true, // 设为 false 时不注入任何默认样式。 enablePasskeys: true, // 设为 false 时不展示 passkey 相关内容。 hidePasskeyButtonOnLogin: false, // 隐藏登录页上用 passkey 登录的按钮。 translations: null, // 在此补充额外翻译。不传或传 null 时使用英文; // 传空对象 {} 则阻止元素展示任何翻译。 translationsLocation: "/i18n", // 翻译文件所在 URL 或路径。 fallbackLanguage: "en", // 翻译不可用时的回退语言。 storageKey: "hanko", // 会话 token 存储的 cookie 名称,同时也是本地存储 key 的前缀/名称。 cookieDomain: undefined, // SDK 设置的 cookie 生效域名;为 undefined 时默认使用创建 cookie 的页面域名。 cookieSameSite: "lax", // 指定 cookie 何时随跨站请求发送。 sessionCheckInterval: 30000, // 会话有效性检查间隔(毫秒),必须大于 3000(3 秒)。 }; const { hanko } = await register( "https://hanko.yourdomain.com", defaultOptions );

将"https://hanko.yourdomain.com"替换为你的 Hanko API 实际地址。除了文档列出的选项外,源码中的RegisterOptions还暴露了sessionTokenLocation(会话 token 的存储位置,来自 frontend-sdk 的SessionTokenLocation),供需要控制 token 存放方式的场景使用。

嵌入 Web Components

完成上述步骤后,就可以在 HTML<body>中任意位置放置组件,最小示例如下:

<hanko-auth id="authComponent"></hanko-auth> <script type="module"> import { register } from "https://cdn.jsdelivr.net/npm/@teamhanko/hanko-elements/dist/elements.js"; await register("https://hanko.yourdomain.com"); const authComponent = document.getElementById("authComponent"); authComponent.addEventListener("onSessionCreated", () => { // 跳转到其他页面 }); </script>

各组件说明如下。

<hanko-auth>、<hanko-login>与<hanko-registration>

这三个组件提供登录/注册的友好界面。区别在于:<hanko-auth>可以在登录与注册界面之间切换;<hanko-login>只负责登录;<hanko-registration>只负责注册。

Markup(标记)

登录与注册的组合 UI:

<hanko-auth></hanko-auth>

仅登录的 UI:

<hanko-login></hanko-login>

仅注册的 UI:

<hanko-registration></hanko-registration>

Attributes(属性)

  • prefilled-email:预填邮箱输入框;
  • prefilled-username:预填用户名输入框;
  • lang:指定元素内容的语言(参见"翻译"章节);
  • mode:接受"login"或"registration",用于初始化<hanko-auth>组件的登录/注册流程;
  • nonce:在应用了内容安全策略(CSP)时,允许加载内联样式的 nonce 值。

在源码层面,mode对应AppProvider中的HankoAuthMode类型("registration" | "login"),AppProvider.tsx 会据此初始化对应的 Flow 名称(login或registration)。

<hanko-profile>

允许用户管理邮箱、密码与 Passkey 的组件。

<hanko-profile></hanko-profile>

属性:lang(语言)、nonce(CSP nonce)。

<hanko-events>

一个不展示 UI、仅用于将事件处理器绑定到特定事件的组件。事件也可以通过<hanko-auth>、<hanko-profile>组件以相同方式订阅,或通过 frontend-sdk 绑定(见下一节)。

<hanko-events id="events"></hanko-events> <script> document .getElementById("events") .addEventListener("onSessionCreated", console.log); // 还有更多事件可用(参见 frontend-sdk 文档)... </script>

从 AppProvider.tsx 的实现可以看到,组件内部会把 frontend-sdk 的会话相关回调(onSessionCreated、onSessionExpired、onUserLoggedOut、onUserDeleted、onBeforeStateChange、onAfterStateChange)重新派发为对应的 DOM 自定义事件,这正是<hanko-events>能够在页面层统一监听这些事件的原因。

使用 Frontend-SDK

register()返回的hanko实例(来自@teamhanko/hanko-frontend-sdk)可以完成会话验证、用户信息获取、登出等操作,常用示例:

// 验证当前会话 const session = await hanko.validateSession(); console.log("Session valid:", session.is_valid, "Claims:", session.claims); // 获取会话 token const token = hanko.getSessionToken(); console.log("Session token:", token); // 获取用户资料 const user = await hanko.getCurrentUser(); console.log("User profile:", user.user_id, user.emails); // 登出用户 await hanko.logout(); console.log("User logged out"); // 处理会话创建事件 hanko.onSessionCreated(({ claims }) => { console.log("Session created with JWT claims:", claims); }); // 处理会话过期事件 hanko.onSessionExpired(() => { console.log("Session expired, redirecting to login"); }); // 处理用户登出事件 hanko.onUserLoggedOut(() => { console.log("User logged out successfully"); }); // 处理用户删除账号事件 hanko.onUserDeleted(() => { console.log("User account deleted"); });

frontend-sdk 的完整实现位于 frontend/frontend-sdk/src:Hanko.ts定义主类,lib/client/下按职责拆分SessionClient、UserClient等客户端,lib/events/则包含事件调度、会话通道与窗口活动管理等底层机制。

一个值得参考的综合示例是仓库内的 src/example.html:它在单个 HTML 文件中演示了<hanko-auth>、<hanko-profile>、<hanko-events>的协同使用——通过onSessionCreated在认证完成后从登录组件切换到资料组件、用onSessionExpired弹出会话过期对话框、用onUserLoggedOut恢复登录界面,并配合语言下拉框实时切换lang属性,还演示了hanko.validateSession()在页面初始化时判断会话有效性。

UI 定制

CSS 变量

CSS 变量可用于按需定制hanko-auth与hanko-profile元素的样式,全部变量及默认值如下:

hanko-auth, hanko-profile { /* 配色方案 */ --color: #333333; --color-shade-1: #8f9095; --color-shade-2: #e5e6ef; --brand-color: #506cf0; --brand-color-shade-1: #6b84fb; --brand-contrast-color: white; --background-color: white; --error-color: #e82020; --link-color: #506cf0; /* 字体样式 */ --font-weight: 400; --font-size: 16px; --font-family: sans-serif; /* 边框样式 */ --border-radius: 8px; --border-style: solid; --border-width: 1px; /* 条目样式 */ --item-height: 34px; --item-margin: 0.5rem 0; /* 容器样式 */ --container-padding: 30px; --container-max-width: 410px; /* 标题样式 */ --headline1-font-size: 24px; --headline1-font-weight: 600; --headline1-margin: 0 0 1rem; --headline2-font-size: 16px; --headline2-font-weight: 600; --headline2-margin: 1rem 0 0.5rem; /* 分隔线样式 */ --divider-padding: 0 42px; --divider-visibility: visible; /* 链接样式 */ --link-text-decoration: none; --link-text-decoration-hover: underline; /* 输入框样式 */ --input-min-width: 14em; /* 按钮样式 */ --button-min-width: max-content; }

在 example.html 的样式中可以看到实际应用:通过覆盖--color、--brand-color、--background-color、--border-radius等变量即可快速打造一套深色主题,并通过#hankoAuth单独调整认证组件的--container-max-width与--container-padding,实现不同组件之间的差异化布局。

CSS Shadow Parts

除了 CSS 变量,还可以使用::part选择器定制各类元素。注意:Shadow Parts 仅在组件挂载到 Shadow DOM 时生效(这是默认行为),可通过如下代码显式开启:

register("https://hanko.yourdomain.com", { shadow: true }); // 等价于 register("https://hanko.yourdomain.com");

可用 Shadow Parts 列表

  • container—— UI 容器
  • headline1—— "h1" 标题
  • headline2—— "h2" 标题
  • paragraph—— 段落元素
  • button—— 所有按钮
  • primary-button—— 主按钮
  • secondary-button—— 邮箱登录页上的次要按钮
  • input—— 所有输入框
  • text-input—— 非 passcode 的输入框
  • passcode-input—— passcode 输入框
  • link—— 页脚区域的链接
  • error—— 错误消息容器
  • error-text—— 错误消息文本
  • divider—— 登录页的水平分隔线
  • divider-text—— 分隔线文本
  • divider-line——divider-text前后的线段
  • form-item—— 表单项(如输入框或按钮)的容器

使用示例

示例 1:强制hanko-auth内的输入框与按钮垂直堆叠(::part(form-item)配合标签名定位):

<style> hanko-auth::part(form-item) { /* 让输入框和按钮上下排列 */ min-width: 100%; } </style> <hanko-auth></hanko-auth>

示例 2:通过.hankoComponent::part(headline1)统一调整所有带hankoComponent类组件的标题:

<style> .hankoComponent::part(headline1) { /* 调整所有 hanko 组件的主标题 */ font-size: 1.3em; font-weight: 400; } </style> <hanko-auth class="hankoComponent"></hanko-auth> <hanko-profile class="hankoComponent"></hanko-profile>

示例 3:借助 ID 选择器#hankoAuth::part(button):hover,在悬停时为按钮添加阴影:

<style> #hankoAuth::part(button):hover { box-shadow: 3px 3px 2px #888; } </style> <hanko-auth id="hankoAuth"></hanko-auth>

CSS Classes(不推荐)

当组件未挂载到 Shadow DOM 时,也可以提供自己的 CSS 规则:

register("https://hanko.yourdomain.com", { shadow: false });

可以查看仓库内的 example.css 了解可用的 CSS 规则。如果只想修改特定属性,覆盖预定义的即可,例如修改背景色:

.hanko_container { background-color: blue !important; }

也可以完全禁止注入样式:

register("https://hanko.yourdomain.com", { shadow: false, injectStyles: false, });

这样就不需要覆盖属性,而是提供全部 CSS 规则:

.hanko_container { background-color: blue; } /* 更多 css 规则... */

如果偏好这种方式,建议以 example.css 为起点,按需修改后引入页面。

需要了解的是:官方提供 CSS Classes 与 light DOM 支持,仅仅是因为 Safari 存在一个 bug——组件挂载在 Shadow DOM 时输入元素的自动补全会失效。正常情况下更推荐将组件挂载到 Shadow DOM,并在 CSS 变量不够用时使用 CSS Parts 进行 UI 定制。

翻译与国际化

默认行为

hanko-elements默认自带英文翻译,lang属性可以省略:

register("https://hanko.yourdomain.com");
<hanko-auth></hanko-auth>

安装更多翻译

当前提供的语言如下:

  • "bn"—— 孟加拉语
  • "de"—— 德语
  • "en"—— 英语
  • "fr"—— 法语
  • "it"—— 意大利语
  • "nl"—— 荷兰语
  • "ptBR"—— 巴西葡萄牙语
  • "zh"—— 简体中文

可以逐个导入:

// 若使用 CDN,请将下列路径替换为 // "https://cdn.jsdelivr.net/npm/@teamhanko/hanko-elements/dist/i18n/{en|de|all|...}.js" import { bn } from "@teamhanko/hanko-elements/i18n/bn"; import { de } from "@teamhanko/hanko-elements/i18n/de"; import { en } from "@teamhanko/hanko-elements/i18n/en"; import { fr } from "@teamhanko/hanko-elements/i18n/fr"; import { it } from "@teamhanko/hanko-elements/i18n/it"; import { nl } from "@teamhanko/hanko-elements/i18n/nl"; import { ptBR } from "@teamhanko/hanko-elements/i18n/pt-BR"; import { zh } from "@teamhanko/hanko-elements/i18n/zh";

或者一次性导入全部翻译:

import { all } from "@teamhanko/hanko-elements/i18n/all";

导入后通过register()提供:

register("https://hanko.yourdomain.com", { translations: { bn, de, en, fr, it, nl, ptBR, zh } }); // 或 register("https://hanko.yourdomain.com", { translations: all });

随后即可用lang属性指定元素语言:

<hanko-auth lang="de"></hanko-auth>

仓库的 src/i18n 目录保存了各语言文件(en.ts、de.ts、zh.ts、pt-BR.ts等),all.ts汇总导出全部语言。从 package.json 的exports/typesVersions配置可以看到,@teamhanko/hanko-elements/i18n/*子路径在 npm 包中是公开导出、可被单独导入的。

修改现有翻译

可以像下面这样直接修改导入的翻译对象:

import { en } from "@teamhanko/hanko-elements/i18n/en"; en.errors.somethingWentWrong = "Aww, snap!"; register("https://hanko.yourdomain.com", { translations: { en } });

新增翻译

如果要创建新语言,传入一个实现(或部分实现)Translation接口的对象:

import { all } from "@teamhanko/hanko-elements/i18n/all"; import { Translation } from "@teamhanko/hanko-elements"; // TypeScript 环境 const myLang: Translation = {...} register("https://hanko.yourdomain.com", {translations: {...all, myLang}});
<hanko-auth lang="myLang"></hanko-auth>

Translation接口的结构定义在 src/i18n/translations.ts,涵盖headlines、texts、labels、errors、flowErrors五个分组,其中flowErrors与后端 Flow API 的错误码一一对应(如flow_expired_error、passcode_invalid、rate_limit_exceeded等),实现新语言时可以此为准。

使用外部文件

对于通过元素lang属性或fallbackLanguage指定、但未包含在translations选项对象中的语言,组件会从translationsLocation选项指定的位置拉取 JSON 文件。例如下面示例中由于传入了空对象,即使默认的 "en" 也不可用,组件会拉取名为/i18n/en.json的文件:

register("https://hanko.yourdomain.com", { translations: {}, // 空对象,连默认的 "en" 翻译也不可用 translationsLocation: "/i18n", // 存放语言文件的公开目录,例如 "en.json" });
<!-- 将拉取 "/i18n/en.json" --> <hanko-auth lang="en"></hanko-auth>

回退语言

fallbackLanguage选项用于指定回退语言:当某语言的翻译缺失或不完整时,自动从回退语言补取缺失的字符串。若回退语言在translations选项中也不可用,组件会尝试从外部文件拉取:

import { en } from "@teamhanko/hanko-elements/i18n/en"; import { Translation } from "@teamhanko/hanko-elements"; const symbols: Partial<Translation> = { labels: { continue: "➔" }, }; register("https://hanko.yourdomain.com", { fallbackLanguage: "en", translations: { en, symbols }, });
<!-- 界面整体显示英文,但 "continue" 按钮文案为 "➔" --> <hanko-auth lang="symbols"></hanko-auth>

这一机制在源码中体现为 AppProvider.tsx 通过TranslateProvider组合translations、fallbackLang与root(即translationsLocation)三个参数,交给@denysvuika/preact-translate运行时解析。

对 Hanko 外发邮件语言的影响

使用 Hanko Elements 时,组件上lang属性的语言也会传递给 Hanko API,用于决定外发邮件的语言。如果停用了 Hanko 的邮件投递、并为email.send事件配置了 Webhook,那么lang属性的值会体现在 Webhook 请求所携带 token 的 JWT payload 的languageclaim 中。

官方示例与框架集成

仓库内提供了可直接运行的参考实现,演示在纯 JavaScript 与主流前端框架中的集成方式:

  • frontend/elements/src/example.html:单个 HTML 文件实现了本页提及的大多数功能,关键细节以注释说明,可在任意 HTTP 服务器(含本地)上托管运行;
  • frontend/examples/README.md:Todo 示例应用,展示 Hanko 在 Angular、React、Vue、Next.js 等框架中的集成,并讲解后端通信与 JWT 校验的管理方式。

包导出内容

@teamhanko/hanko-elements导出以下函数与接口,并额外导出 frontend-sdk 的全部声明(src/index.ts 通过export *实现):

函数

  • register—— 将 Web Components 注册到浏览器自定义元素注册表。

接口

  • RegisterOptions——register()函数的选项;
  • RegisterResult——register()函数的返回值(包含hanko实例);
  • Translation—— 可通过RegisterOptions提供的翻译结构;
  • HankoAuthElementProps——<hanko-auth>元素属性;
  • HankoProfileElementProps——<hanko-profile>元素属性;
  • HankoEventsElementProps——<hanko-events>元素属性。

上述属性接口在 Elements.tsx 中声明,并且该文件同时为 JSX 命名空间补充了hanko-auth、hanko-login、hanko-registration、hanko-profile、hanko-events五个内建元素类型,支持在 React 19 及 TypeScript 环境中获得类型提示。

浏览器支持

  • Safari
  • Firefox
  • Opera
  • 基于 Chromium 的浏览器(Chrome、Edge、Brave 等)

已知问题

  • 可定制 UI:在 Chrome 中,::part选择器与某些伪类组合时存在缺陷,例如:disabled当前无法正常工作。

许可证

elements项目以 MIT 许可证发布,详见 frontend/elements/LICENSE。


结合本文内容,一个典型的落地路径是:先通过 npm/CDN 安装并调用register()接入<hanko-auth>,配合onSessionCreated事件与 frontend-sdk 的validateSession()完成基础认证闭环;随后利用 CSS 变量与 Shadow Parts 对齐品牌视觉;最后通过 i18n 模块、lang属性与fallbackLanguage完成国际化。如果需要在认证成功/失败/过期等关键时刻介入业务逻辑,<hanko-events>与 frontend-sdk 的事件订阅是推荐的统一入口。

  • 后端
  • 认证鉴权
  • 前端

【免费下载链接】hanko

Modern authentication, on your terms. Open source alternative to Auth0, Clerk, WorkOS, Stytch.

项目地址:https://gitcode.com/GitHub_Trending/ha/hanko
点击查看免费下载
上一篇:5分钟掌握JDspyder:京东自动化抢购脚本的终极使用指南
下一篇:Open Mercato销售模块实战指南:从报价、订单到发货的完整流程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 15:41:56

掉线重连不一定是网卡问题:RPC协议与域控排查实战

“回购协议掉线重连”&#xff0c;我第一眼看到这个说法也愣了一下。结合你发的场景和关键词&#xff0c;这大概率是“RPC/回话协议”的口语化误写&#xff0c;说的就是远程过程调用、域会话这一类连接断断续续的问题。干运维这么多年&#xff0c;我接到最多的反馈就是“网卡又…

作者头像 李华
网站建设 2026/10/2 15:39:37

最值钱的职场技能——用 TaoToken 搭建 AI 智能体并跑通变现闭环

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 15:39:22

RISC-V车规芯片量产路径:从车身控制到动力总成的真实进度

摘要&#xff1a;RISC-V车规芯片正在从概念走向量产。紫荆半导体M100已在长城汽车量产上车&#xff0c;单车最多搭载17颗&#xff0c;用于组合大灯、氛围灯、组合开关等车身控制场景。东风DF30基于RISC-V多核架构实现ASIL-D功能安全等级&#xff0c;已在奕派007、猛士M817等车型…

作者头像 李华
网站建设 2026/10/2 15:38:24

PlumeLog实践:自研分布式日志采集框架的缓冲设计与调优

开篇先说实话&#xff1a;我做日志系统这块也有些年头了&#xff0c;从最早的直接tail -f&#xff0c;到后来用ELK&#xff0c;再到现在维护着日均几十TB日志的采集链路。但第一次见到PlumeLog这个项目名的时候&#xff0c;我还是愣了一下——"Plume"是羽毛、羽流的意…

作者头像 李华
网站建设 2026/10/2 15:38:07

端侧Agent部署指南:从模型选型到落地实践

做端侧 Agent 的人&#xff0c;最终都会卡在同一堵墙上&#xff1a;模型放在云端时&#xff0c;Agent 是个乖巧的玩具&#xff1b;一旦你把设备扔到车库、农田、产线&#xff0c;或者一辆没有稳定网络的车里&#xff0c;LLM 部署就变成了整个链路里最先爆掉的一环。这篇是「深入…

作者头像 李华
网站建设 2026/10/2 15:37:58

从GitHub热榜到PR:开源项目筛选与落地全流程

每天早上的固定节目&#xff0c;是打开GitHub热榜扫一遍日榜。2026-09-24这一天的榜单&#xff0c;我盯着看了很久&#xff0c;倒不是上面有多少颠覆性的东西&#xff0c;而是这个切片特别能反映当下开源的审美和需求&#xff1a;AI相关项目依旧是绝对主流&#xff0c;开发者效…

作者头像 李华