一、它解决了什么问题商城移动端要同时跑在H5、微信公众号、微信小程序、支付宝小程序、App上。这些平台的差异是巨大的:跳转 API 不同(navigateTo/switchTab/location.href);支付 SDK 不同(JSSDK /requestPayment/ 支付宝 SDK);登录态、网络、胶囊按钮、导航栏高度都不同;甚至同一份对象存储,阿里云/腾讯云/七牛的图片裁剪参数都不一样。如果在业务代码里到处写#ifdef条件编译,代码会迅速腐化。sheep 框架的核心目标就是:把所有平台差异下沉到框架层,业务代码只面向sheep.$xxx这套统一 API 编程。二、整体架构:薄入口 全局能力聚合应用入口非常薄。main.js只做一件事——创建 SSR App 并装上 Pinia:// main.js import App from ./App; import { createSSRApp } from vue; import { setupPinia } from ./sheep/store; export function createApp() { const app createSSRApp(App); setupPinia(app); return { app }; }App.vue在onLaunch时调用ShoproInit()完成所有底层初始化:// App.vue onLaunch(() { uni.hideTabBar({ fail: () {} }); // 隐藏原生导航栏,使用自定义底部导航 ShoproInit(); // 加载 Shopro 底层依赖 });真正的核心在sheep/index.js,它把所有能力挂到一个sheep对象上:// sheep/index.js:17-38 const sheep { $api, // 业务接口集合 $store, // Pinia 状态 $url, // 资源地址(CDN/OSS 裁剪) $router, // 跨端路由 $platform, // 平台抽象 $helper, // 工具函数 $zIndex: zIndex, }; export async function ShoproInit() { await $store(app).init(); // 应用初始化(网络/租户/模板/登录) $platform.load(); // 平台前置加载(各 provider 的初始化) if (process.env.NODE_ENV development) ShoproDebug(); }设计要点:业务页面只需import sheep from /sheep,就能拿到sheep.$router.go()、sheep.$platform.pay()、sheep.$url.cdn()等全部能力。这是一个典型的门面(Facade)模式,把复杂子系统收敛成单一入口。三、应用初始化:init()的启动编排sheep/store/app.js的init()是整个框架的启动核心,它编排了一系列顺序依赖的初始化步骤:// sheep/store/app.js:57-79 async init(templateId null) { // 1. 检查网络 const networkStatus await $platform.checkNetwork(); if (!networkStatus) $router.error(NetworkError); // 2. 检查配置 if (typeof baseUrl undefined) $router.error(EnvError); // 3. 加载站点访问令牌(H5 环境,从 URL 或 Cookie 读取) loadAccessToken(); // 4. 加载租户 await adaptTenant(); // 5. 识别分销 URL(命中白名单则存标识,触发访客模式) await adaptDistributionUrl(); // 6. 加载装修配置 await adaptTemplate(this.template, templateId); // 7. 已登录则补齐登录后逻辑;未登录不再自动游客登录 const userStore user(); if (userStore.isLogin) userStore.loginAfter(); }这个编排的精妙之处在于每一步都为后续步骤铺路:网络不通直接报错,避免后续无意义的请求;租户先于业务请求确定,保证所有 API 带正确的tenant-id;装修模板加载后,首页/个人中心/tabbar 才知道渲染什么;登录态补齐放在最后,因为loginAfter要拉用户信息、钱包、购物车——这些都依赖前面的租户和网络。四、请求层:统一封装 无感刷新 端到端加密sheep/request/index.js基于luch-request封装,是整个框架最厚的一层。它统一处理了:loading、鉴权、租户、终端、加密、错误提示、token 刷新。4.1 请求拦截器// sheep/request/index.js:73-122(节选) http.interceptors.request.use((config) { // 1. 需登录的接口,未登录则弹登录窗 if (config.custom.auth !$store(user).isLogin) { showAuthModal(); return Promise.reject(); } // 2. loading 计数(支持并发请求合并一个 loading) if (config.custom.showLoading) { LoadingInstance.count; LoadingInstance.count 1 uni.showLoading({ title: config.custom.loadingMsg, mask: true }); } // 3. 注入 token / terminal / tenant-id / site_access_token if (token) config.header[Authorization] token; config.header[terminal] getTerminal(); config.header[tenant-id] getTenantId(); config.header[X-Site-Access-Token] uni.getStorageSync(site_access_token) || ; // 4. 全局加密标识 加密请求体 config.header[X-Api-Encrypt] true; if (config.data [POST,PUT,DELETE,PATCH].includes(...)) { config.data encryptRequest(config.data); } return config; });几个值得学习的细节:Loading 计数器(LoadingInstance.count):并发多个请求时只显示一个 loading,全部完成才关闭,避免闪烁;custom选项透传:每个 API 可以单独配置showError、showSuccess、auth、isToken,而不是全局一刀切;加密不改 Content-Type:注释里写得很清楚——保持application/json,后端 Filter 用getInputStream读原始字节流解密,SpringRequestBody能正常反序列化。这是一个踩过坑才会有的设计。4.2 响应拦截器:401 无感刷新401 处理是请求层的难点。sheep 实现了请求队列 单一刷新的经典方案:// sheep/request/index.js:287-337(核心逻辑) let requestList []; // 等待刷新的请求队列 let isRefreshToken false; // 是否正在刷新 const refreshToken async (config) { if (config.url.indexOf(/member/auth/refresh-token) 0) { return Promise.reject(error); // 刷新接口本身 401,直接失败 } if (!isRefreshToken) { isRefreshToken true; const refreshToken getRefreshToken(); if (!refreshToken) return handleAuthorized(); // 无刷新令牌,登出 try { const result await AuthUtil.refreshToken(refreshToken); if (result.code ! 0) throw new Error(刷新令牌失败); // 刷新成功:回放队列 重发当前请求 config.header.Authorization Bearer getAccessToken(); requestList.forEach((cb) cb()); requestList []; return request(config); } catch (e) { // 刷新失败:只回放队列,不回放当前请求(避免递归) requestList.forEach((cb) cb()); return handleAuthorized(); } finally { requestList []; isRefreshToken false; } } else { // 已有刷新在进行,当前请求入队等待 return new Promise((resolve) { requestList.push(() { config.header.Authorization Bearer getAccessToken(); resolve(request(config)); }); }); } };这个实现的关键在于isRefreshToken互斥锁 requestList队列:当第一个 401 触发刷新时,后续并发的 401 请求全部入队,刷新成功后统一回放。避免了 N 个请求同时刷新 token 的雪崩。还有一个贴心设计:handleAuthorized里对游客 token 过期做了特殊处理——自动用guest_id重新游客登录,而不是弹登录窗,让游客无感知地继续浏览参考地址优享惠商城