做Scratch二次开发走到第四步基本都会撞上同一道坎登录功能。前面三篇我分别讲了环境搭建、项目整体结构和界面定制这篇聚焦最实际的问题——怎么把一个真正可用的账号体系嵌进这套React代码里。如果你做的不是个人玩具项目而是面向学校、机构或某个社区用户的编程平台那“用户登录”基本绕不开作品要存到云端、老师要管理班级、学生要认领自己的作品这些全部依赖登录。但麻烦在于Scratch官方源码本身并没有给你预留一套可以直接对接的登录方案它默认你是官方社区用户体系挂在官方服务器上。所以我们得自己搭一套。这篇内容适合两类人一是已经在本地跑通scratch-gui、正准备加账号功能但不知道从哪下手的开发者二是做二次发行版但还没想清楚登录模块该怎么设计的技术负责人。我会从入口位置、接口设计、前端实现、联调踩坑到登录后的收尾工作完整走一遍。1. 先摸清登录入口在Scratch源码里的位置1.1 你改的到底是哪一套代码登录功能不是说加就能加的你得先知道你正在动的是哪一层代码。Scratch官方前端仓库通常叫scratch-gui它把界面分为几个核心区域项目舞台区、脚本区、绿旗区、菜单栏还有扩展库等整体是React技术栈。登录入口一般在顶部菜单栏MenuBar官方版在右上角放着一个登录/注册按钮点开之后会跳转到官方社区或弹出一个登录界面。如果你用的是官方原版scratch-gui目录下大概有这么几个重要位置src/views主页面View组件这里组装了整个Scratch界面。src/components/menu-bar顶部菜单栏登录按钮的“物理位置”。src/redux状态管理官方用Redux管理很多全局状态比如项目是否保存、当前打开的是哪个扩展。src/lib各种库函数和工具方法。大多数二次开发项目都是基于这个仓库改的。如果你拿到的是别人已经封装好的二次发行版那第一件事是先找到MenuBar组件的源码文件别急着在界面上找按钮因为“登录按钮”很可能被包在一堆条件渲染里直接搜更靠谱。1.2 导航栏是唯一合理入口吗很多第一次做二次开发的人会纠结登录按钮放哪其实答案很明确就是右上角菜单栏。理由有三第一用户习惯已经养成。Scratch官方把登录放在右上角学生、老师打开编辑器都会先看右上角如果我把登录入口藏到别的地方用户反而不容易找到。第二菜单栏是全局组件不管用户当前在哪个页面都能随时登录。Scratch编辑器是一个单页应用舞台区和代码区几乎总是可见放在这里最不打扰操作。第三Scratch官方本身就在MenuBar的props里设计了一个登录相关的插槽我们替换成本控件改动范围最小不容易影响其他逻辑。有人可能会问能不能做成“点保存时弹登录框”这也是一个方案适合那些“作品必须登录才能存”的策略。但即便要做这种强制登录也建议先有一个独立登录入口让愿意登录的用户主动登录然后在保存动作里再做二次拦截。如果一上来就把登录强绑在保存按钮上新手很容易一脸懵以为程序卡死了。1.3 一个原则改动集中到菜单栏组件别到处铺这是我在实际开发里特别想强调的一点。Scratch这个项目很大组件树非常深如果你为了实现登录今天改一下这个页面明天改一下那个组件后患无穷。我当时定了一个规矩所有登录相关的UI和逻辑统一放在一起不让它散落在各个view里。具体做法是在src/components下新建一个auth目录里面放登录按钮、登录弹窗、用户信息展示这几个组件。在src/lib下新建一个auth模块专门处理登录请求、token存取、用户信息缓存。MenuBar组件只负责“调用auth模块”不直接写登录逻辑。这样做的直接好处是后面如果要升级scratch-gui版本或者把登录从账号密码改成手机验证码/扫码登录你只需要动auth目录里的文件MenuBar和其他组件的改动非常小。2. 账号体系的取舍为什么必须自己做一套登录接口2.1 官方登录能不能直接复用有人一开始会天真地想Scratch官网不是有登录功能吗我直接把请求地址指向官方就行了。这里有两个致命问题。第一官方登录是依托于scratch.mit.edu的Session体系Session存在官方域名的Cookie里。你的应用跑在自己的域名下官方服务器根本不会给你返回有效的登录态而且官方有反爬策略不会允许第三方站点滥用它的账号体系。第二你的平台用户跟官方用户根本不是同一批人。做二次开发的目的一般都是特定场景某学校的信息课、某个编程比赛、某个机构的内部平台。这些场景下的用户数据、角色权限需要自己管理官方账号体系满足不了。所以结论很简单二次开发版几乎都要自己做一套登录接口不去碰官方登录。这里说的“自己”既包括自建一个完整的用户中心也包括对接你们公司已有的统一认证体系。2.2 最简可用的登录协议设计如果你是从零开始我不建议一上来就设计一套繁重的标准——什么OAuth2、刷新令牌、权限分组、多端互斥统统往后放。先跑通一套“最小可用协议”让前端能登录、能拿用户信息、能退登就够了。我推荐的最小接口集是下面这三个接口方法入参返回错误处理/api/auth/loginPOST{ username, password }{ token, userId, nickname, avatar }401表示用户名或密码错误/api/auth/meGETHeader带Authorization: Bearer token{ id, nickname, avatar }401表示token失效/api/auth/logoutPOSTHeader带token空无这个协议有几个细节值得解释。第一个是token。我建议用随机字符串token后端存一份“token到用户id”的映射表设置过期时间。前端的任何一个请求只要带了合法的token服务端就知道你是谁。这个方案比“把userId直接明文传来传去”安全得多也比一开始就上JWT简单。第二个是/api/auth/me。这个接口非常重要前端启动时要用它来恢复登录态。很多新手会把用户信息直接存在localStorage里刷新页面直接读出来用。这样做的问题在于你无法判断这个用户信息是不是最新的也无法判断token在服务端是否已经失效。所以踏实一点每次页面刷新都拿token去问一次服务端“我是谁”。接口返回正常再展示用户信息返回401就说明登录失效了。第三个是错误码。前后端一定要统一错误返回格式比如{ code: 401, message: 用户名或密码错误 }前端拿到code之后做对应提示不要靠“HTTP 401”这一个状态码走天下因为有些网络代理会吞掉状态码。2.3 本地开发先用Mock再切真实后端后端接口开发完之前前端不要干等。尤其Scratch这种大项目编译一次都挺久等后端恢复再联调效率太低了。我的习惯是本地直接Mock登录接口让前端完全独立开发。最省事的做法是利用webpack devServer的before钩子写一个小小的mock路由。比如// webpack.dev.js 片段 devServer: { before(app) { app.post(/api/auth/login, (req, res) { const { username, password } req.body; if (username demo password 123456) { res.json({ token: mock-token-123, userId: 1, nickname: 演示账号, avatar: }); } else { res.status(401).json({ code: 401, message: 用户名或密码错误 }); } }); app.get(/api/auth/me, (req, res) { const auth req.headers.authorization; if (auth Bearer mock-token-123) { res.json({ id: 1, nickname: 演示账号, avatar: }); } else { res.status(401).json({ code: 401, message: 登录已过期 }); } }); } }这样在开发环境里前端所有登录逻辑都能正常跑。等后端接口好了只需要把Mock代码去掉把请求地址指到真实接口即可。Mock阶段还能顺便把前端的状态管理、token存储、错误提示全都调通后面联调几乎不用改前端逻辑。3. 动手实现把登录逻辑一点点嵌进Scratch3.1 菜单栏按钮的替换这是第一个动手点。打开src/components/menu-bar/menu-bar.jsx找到右上角那部分。官方源码里这里通常会有一个变量控制的登录入口或者社区链接我们把它替换成自己的组件。我当时是新建了一个AuthButton组件代替原来的按钮// menu-bar.jsx 中的改动示意 import { AuthButton } from ../auth/auth-button.jsx; // 在组件内部原来渲染登录按钮的位置 AuthButton user{user} onLogin{handleOpenLoginModal} onLogout{handleLogout} /需要注意现在scratch-gui的MenuBar组件本身也和Redux有联系很多状态是从props传进来的。你不要去动那些props的传递方式只是在“渲染按钮”这一小块上换成自己的组件原来的tabs、项目控制按钮、语言切换这些逻辑一概别碰。这样改动最小回归测试最省事。3.2 登录弹窗自己写还是引第三方我强烈建议不要为了登录弹窗引入一套完整UI库。Scratch项目对依赖很敏感引入一个库可能带来样式覆盖、主题冲突、打包体积变大等问题。一个登录弹窗而已自己写也就几十行。我自己写的登录弹窗大概包含这几个部分遮罩层点击遮罩关闭弹窗。标题写着“登录”或者logo。用户名输入框、密码输入框。登录按钮点击时按钮进入loading状态防止重复提交。错误提示区域显示接口返回的错误信息。校验逻辑很简单用户名不能为空、密码不能少于6位这些在前端做一遍后端再严格校验一遍。前端校验主要是为了用户体验不是为了安全。这里有一个容易忽略的小细节弹窗打开时不要把焦点默认放在密码框上有些不习惯键盘操作的用户可能直接敲回车结果密码没输完就提交了把焦点放在用户名框用户输完用户名按Tab到密码框回车提交这个交互最自然。3.3 token存哪这次我认了localStorage前端拿到token之后存在哪里常见选项是cookie、sessionStorage、localStorage。我用一张表说清楚区别存储位置生命周期特点适合场景localStorage永久除非手动清除刷新页面、关闭浏览器都在保存登录态推荐sessionStorage关闭浏览器标签即失效刷新页面还在但新开窗口没有临时数据不适合登录态cookie按Expires/Max-Age控制会自动带上同源请求需要服务端Set-Cookie的场景结论是登录token存localStorage。原因很简单用户刷新页面、关了浏览器再回来我们希望他还处于登录状态这是登录功能的基本体验。存sessionStorage的话用户开个新标签页又要重新登录体验很割裂。cookie也行但需要后端配合设置Set-Cookie并且跨域情况下处理起来更麻烦所以我对这个场景首选localStorage。同时要强调一个安全问题token里不要存敏感信息。不管是localStorage还是cookie前端存储的东西都不能视为绝对安全。token本身可以是一个无含义的随机字符串代表“这个人登录过”用户昵称、ID都通过接口去拿敏感数据不出现在token里。3.4 用户状态如何在React组件树里传Scratch的组件树非常深菜单栏、画布、积木区、预览区层层嵌套。如果每个组件都用props去拿用户信息代码会变得非常痛苦中间隔着一堆你根本不关心的组件。这种情况我推荐使用React Context。做法是在src/lib/auth里建一个UserContext// UserContext.jsx 的简化示意 const UserContext React.createContext({ user: null, isLogin: false, login: () {}, logout: () {} }); export const useUser () React.useContext(UserContext);然后在应用最外层挂Provider。MenuBar组件、保存功能、作品列表这些需要感知用户身份的地方通过useUser()拿状态。这样无论组件在组件树多深的位置都能直接访问用户信息不用层层传props。如果你所在团队本来就在用Redux管理其他状态也可以把用户信息挂进Redux两种方式都能跑。我个人倾向Context因为登录这个功能相对独立用Context隔离得更好升级Scratch主体代码时不容易把Redux的依赖搞乱。3.5 刷新页面恢复登录态这是登录功能里最容易被做坏的一步。很多人的第一版实现是刷新页面时从localStorage读token如果有就直接拿缓存里的用户信息展示。这个方案有一个隐患它不校验token是否还有效。用户改了密码、账号被禁用、token过期前端完全不知道还一直展示着旧用户信息用户实际请求时会莫名其妙地报错。正确的做法是在App组件挂载时调一次/api/auth/me接口useEffect(() { async function restoreLogin() { const token localStorage.getItem(token); if (!token) return; try { const user await authApi.getMe(token); setUser(user); } catch (err) { if (err.status 401) { localStorage.removeItem(token); } } } restoreLogin(); }, []);这段逻辑的要点有两个。第一只在组件挂载时跑一次不要放在每次渲染时跑否则每切换一次页面状态都要发一次请求浪费流量也影响性能。第二接口返回401时要清除本地token让应用自动回到未登录状态否则用户下次操作还会带着一个无效token到处碰壁。4. 联调阶段最常见的四个坑4.1 开发端口跨域给devServer配代理Scratch-gui默认开发端口是8601后端接口如果跑在3000端口前端直接请求http://localhost:3000/api/auth/login就会跨域。如果不处理浏览器会拦截响应控制台报“No Access-Control-Allow-Origin header”。最快的解法是给webpack devServer加一个代理让前端代码里统一用相对路径/api开头由开发服务器转发到后端。配置如下// webpack.dev.js 示意 devServer: { port: 8601, proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } }这样前端请求fetch(/api/auth/login)时浏览器看到的是同源请求不涉及跨域。后端不用改任何CORS配置本地开发体验非常顺。4.2 字段命名习惯不一致前端做一层映射联调时最容易出现的不是技术问题而是命名之争。比如前端约定的是userId后端返回uid前端叫nickname后端叫name甚至更离谱的是后端把登录返回包成了一层data前端要直接解析token结果对不上。我的建议是如果后端接口是你们团队自己的尽量前期就对齐字段命名规范减少传递损耗。但如果后端是其他团队维护的历史系统字段已经定死了改不了那前端就在auth模块里统一做一次字段映射// auth-api.js 里的字段映射示意 function normalizeLoginResponse(raw) { return { token: raw.access_token || raw.token, userId: raw.uid || raw.userId || raw.id, nickname: raw.displayName || raw.nickname || }; }调了两个小时接口发现是字段问题这种经历我遇到过不止一次。提前做这层映射后面能省很多事。4.3 fetch不会自动带Authorization如果你用的是fetch封装登录请求有一个坑fetch不会像axios那样自动把自定义Header合并进去。你必须在每个需要鉴权的请求里手动加Authorizationfetch(/api/auth/me, { headers: { Authorization: Bearer localStorage.getItem(token) } });我见过有人封装了一个统一的请求函数结果只在登录请求里写了Header其他请求全都漏了最后排查很久才发现是“压根没把token传给后端”。如果你打算写一个带鉴权的请求封装就把Header处理放在封装函数内部别散落在各处。4.4 刷新就丢登录态按这个顺序排查这个坑出现频率极高我建议按照固定顺序排查打开开发者工具看Application里的localStoragetoken到底存进去没有。如果token存在看Network面板刷新页面后有没有发起/api/auth/me请求。如果请求发出了看请求响应是200还是401。如果响应是401看后端token校验逻辑是不是有问题比如token过期时间设成1分钟内。大部分“刷新丢登录态”的问题都出在第二步或者第三步要么前端没发me请求要么后端返回401。把排查顺序理清基本十分钟内定位到问题。5. 登录完成后还有哪些细节要补5.1 导航栏显示昵称和头像登录成功后右上角的“登录”按钮要替换成用户昵称和头像。头像没设置的时候最好有一个默认占位图不然图片加载失败会显示一个裂图很难看。这里有个交互细节用户点击昵称时除了展示个人信息可以顺便把“退出登录”按钮放在同一个下拉区域里这样用户不用满页面找退出入口。Scratch官方也是放在同一个区域的符合用户直觉。5.2 退出登录别忘了清理localStorage之外的缓存退出登录的代码很简单调logout接口清空localStorage里的token把用户状态重置为未登录。但如果你在登录成功之后还把用户信息放到了别的地方比如Redux、某个全局对象退出时也要一并清掉否则页面上一部分区域显示未登录另一部分还显示旧用户非常混乱。另外如果你们做了“记住我”之类的功能退出登录之后要确认localStorage里的token真的被删了而不是覆盖成空字符串。空字符串也是“有值”会导致一些判断逻辑误判为已登录。5.3 登录改造之后作品保存、用户数据隔离是下一步登录做完紧接着就要面对一个更重要的问题作品保存到哪去。Scratch编辑器里点了保存默认是把项目存到浏览器本地或官方服务器。二次开发平台一定希望作品回到自己的服务器并且关联到当前登录用户。所以登录做完之后的自然延伸是保存作品时带上token后端按userId区分每个人自己的作品。作品列表页面只展示当前登录用户的作品。未登录用户点击保存时提示登录或提供“游客模式保存到本地”的降级方案。这一块内容比较多我打算放在下一篇详细讲这里先挖个坑。最后分享一个我自己的体会Scratch二次开发的登录功能工作量不在“写一个登录框”而在于你要在一个超大前端项目里控制改动范围。Scratch的代码不是你一个人写的以后要升级版本、要改样式、要加功能如果因为一个登录把一堆组件搅乱了后面会非常痛苦。所以做登录之前先把“登录这个功能应该住在哪个模块”想清楚再把这块地方修得干干净净后面无论怎么发展都不会太难受。