SAP Fiori Direct Mode个性化存储实战:从localStorage到API Mode迁移 SAP Fiori开发里有一个常被低估、但实际非常趁手的工具就是Personalization个性化存储服务。很多人以为个性化数据一定要走后端、配OData服务、部署BSP应用流程繁琐得让人劝退。其实在简单场景下SAP Fiori提供了Direct Mode允许直接把个性化数据落在浏览器localStorage里前端一套API就能搞定。这篇文章我就用实际项目里的一个简单场景把Direct Mode从原理到落地的完整链路讲清楚包括配置方法、会踩的坑、以及从Direct Mode平滑迁移到API Mode的思路希望对正在做Fiori应用个性化需求的同学有帮助。1. 项目概述Direct Mode是什么解决什么问题1.1 从记不住用户偏好说起做SAP Fiori应用几乎每个项目都会遇到一类需求用户希望系统记住他的一些小偏好。比如上次选中了哪个视图、列表页的列宽调整过、常用筛选条件想保留、某个新手引导提示以后不再看。这类需求本身不大但如果每次都要求搭一套后端存储方案反而把简单问题搞复杂了。Fiori框架其实提供了一套现成的用户个性化存储机制统一入口叫 Personalization 服务可以通过sap.ushell.Container获取。这套服务有两种工作模式一种是API Mode数据经过后端服务存储适合企业级跨端同步另一种就是我们今天的主角 Direct Mode数据直接存到浏览器本地不需要任何后端依赖特别适合上面这类轻量偏好场景。用生活化一点的类比API Mode相当于把东西存进银行的保险柜安全、能跨网点取用但你要办理开通手续Direct Mode是直接把东西放自家抽屉里随手取用但换个房子就带不走。理解这个差别就理解了Direct Mode的设计初衷为轻量场景提供最低成本的存储通道。1.2 Direct Mode与API Mode的核心差异我会把两者的差异整理成一个表对照起来看更直观。SAP Fiori个性化存储的两种模式在几个维度上的表现是很不一样的维度Direct ModeAPI Mode存储位置浏览器localStorage后端服务器存储后端依赖无需要对应后端服务数据同步不支持跨设备/浏览器同步支持多端一致数据持久性用户清缓存或无痕模式会丢失持久保留可控性高响应速度快无网络请求相对较慢受网络影响安全管控低任何前端代码都可读取高运维和权限可控适用场景视图偏好、布局记忆、本地临时设置跨设备个人设置、企业级定制这其中的关键区别其实不在于存哪而在于存储通道是谁提供的。Direct Mode下前端组件拿到的是同一个Personalization API但这个API底层直接对接了浏览器localStorageAPI Mode下底层对接的是一个部署在后端的OData服务。正因为接口一致应用代码可以做到无感切换这也为后面从Direct Mode迁移到API Mode埋下伏笔。1.3 到底什么时候该用Direct Mode基于上面这些差别总结一下Direct Mode的推荐使用时机应用只是Fiori Launchpad里的一个嵌入式页面个性化数据只需要在本地浏览器保存就满足业务要求。数据本身的敏感性不高比如用户界面的展示偏好、操作习惯记录不含敏感业务字段。团队想快速做原型验证不想投入后端存储配置的开发和运维成本。需要本地缓存一些临时状态比如向导式页面的步骤记录、条件筛选的草稿。反过来如果有跨设备同步需求、有管理员集中维护个性化策略的合规要求、或者数据涉及权限管控那应该老老实实用API Mode。这里给一个实操经验我在项目中一般会把是否需要跨端一致作为第一判断条件。只要答案是不需要Direct Mode就是性价比最高的选择如果答案不确定也完全可以先用Direct Mode把前端业务跑通后面再切后端存储成本并不高。2. Direct Mode的内部机制数据都去了哪里2.1 从Personalization服务到localStorage的链路要真正用好Direct Mode值得先看一下它的内部链路。当我们调用sap.ushell.Container.getService(Personalization)的时候框架会返回一个Personalization服务实例。这个实例的get、set等方法会先走一套统一的前端SPI再根据当前模式的配置把数据写入不同的存储后端。在Direct Mode下默认存储后端就是浏览器的localStorage。这里有一点很关键Personalization服务并不是简单往里丢键值而是按照一定命名空间规则来组织数据通常包含应用ID、用户身份、数据分类等维度保证同一个浏览器里即使有多个应用或者多个用户登录痕迹也不会相互覆盖。我们在浏览器DevTools里看到的那些带SAP_USHELL_PERSONALIZATION前缀的键就是这个服务写入的数据。这条链路的另一个意义在于它把数据存储方式这个易变因素隔离在了业务之外。这意味着在业务代码中我们完全不需要关心底层是localStorage还是后端服务只要操作服务实例的API即可。所以我一直建议团队统一走Personalization服务不要图省事直接写window.localStorage不然将来切API Mode的时候业务代码要全部重改。2.2 核心API与数据结构Personalization服务对外暴露的方法不多但足以覆盖大部分个性化存储需求。逐个说get(sKey)根据键读取数据返回一个Promiseresolve出来的就是存储的值。set(sKey, vData, fnCallback[, fnErrorCallback])写入数据传入键、值和成功回调也可以再加一个错误回调参数。setMultiple(oData, fnCallback)批量写入多个键值。getAll()读取当前命名空间下的全部个性化数据返回Promise。要注意的是虽然Direct Mode底层是localStorage理论上localStorage只能存字符串但Personalization服务会自动处理对象序列化。所以在业务侧我们可以直接用JSON对象作为vData不必手动JSON.stringify/parse。这是我用过之后最爽的一点不用关心序列化细节框架全给处理了。数据结构上官方推荐使用扁平键值对键名建议带上明确的语义前缀例如layoutPreference、filterDraft、onboardingDismissed。不建议把一大堆数据塞到一个键里因为你一旦把多个不相关的字段合并成一个对象将来要单独更新某一个字段就不得不把整个对象读出来再改回去容易引发并发覆盖问题。2.3 Direct Mode下的数据生命周期Direct Mode的数据生命周期要分几个阶段看写入阶段调用set后数据先同步写入浏览器内存再由localStorage持久化。这里有个细节localStorage的写入是同步的所以不需要担心像IndexedDB那样有异步回调延迟。会话阶段同一浏览器标签页内数据立即可读。即使是重新加载页面只要localStorage没被清数据依然在。但同一个浏览器里打开新的标签页两个页面之间不会自动同步实时变更这是localStorage的固有限制后面会讲到如何用storage事件缓解。清理阶段localStorage本身没有过期时间概念数据会一直保留除非用户主动清除浏览器站点数据、使用无痕模式、或者浏览器因存储压力进行清理。这是我们做Direct Mode方案时必须向业务方讲清楚的边界。容量阶段localStorage单个域名下的容量通常在5MB到10MB左右对于纯粹保存偏好字符串来说一般是足够的但如果拿来存大字段或二进制内容就会很快触及上限。遇到存储已满时set方法会抛出QuotaExceededError业务侧最好做try/catch兜底。3. 实操在SAP Fiori应用中启用并封装Direct Mode个性化存储3.1 环境准备与配置要点开始之前先确认工程环境能正常跑在Fiori Launchpad中。Direct Mode是FLP框架提供的服务能力所以应用必须运行在FLP容器里直接单独打开Component的沙盒页面是拿不到sap.ushell.Container的。这一点新手很容易卡住。配置要点需要分版本和部署方式说明。一般来说Direct Mode的启用是在FLP端配置完成的常见位置包括FLP的Configuration配置或应用集成配置中。如果你使用的是SAP Build Work Zone或者SAP BTP环境需要在对应站点设置中确认个性化存储的配置如果在经典ABAP环境上自建FLP注意检查启动配置里的个性化设置项。有些版本还会提供bootstrap参数比如在FLP启动页的脚本配置中设置>sap.ui.define([ sap/ushell/Container ], function(Container) { use strict; return { getPersonalizationService: function() { return Container.getService(Personalization); } }; });在UI5旧版中sap.ushell.Container可能尚未就绪需要保证调用时机在FLP框架初始化完成后一般是Component的init钩子或者页面onLoad事件之后。拿到服务实例后不建议业务代码到处直接调用get和set最好封装一个统一工具类把错误处理、日志、Promise转换都收敛起来。这里我额外说一句很多朋友在项目里一旦用上了Direct Mode就喜欢直接去操作localStorage觉得反正都是存本地何必绕一层服务。这个习惯我强烈建议改掉因为一旦你某天要切API Mode所有直接依赖localStorage的代码都会变成定时炸弹。统一走Personalization服务这个抽象层未来切换成本几乎为零。3.3 场景实战记住用户最后选择的界面布局这个场景我挑得很典型应用的主页有一个分段视图允许用户在卡片视图和列表视图之间切换。产品经理给的需求很简单下次用户进入应用时保持他上次选择的视图不要每次都默认回卡片视图。用Direct Mode实现这个场景实际上只需要三个步骤读取、写入、应用。先看完整代码。sap.ui.define([ sap/ui/core/mvc/Controller, sap/ushell/Container, sap/m/MessageToast ], function(Controller, Container, MessageToast) { use strict; return Controller.extend(my.app.MainController, { onInit: function() { var oPersonalization null; Container.getService(Personalization).then(function(oService) { oPersonalization oService; return oService.get(homeLayoutPreference); }).then(function(oValue) { var sPreferredLayout (oValue oValue.layout) ? list : card; this.getView().byId(segmentLayout).setSelectedKey(sPreferredLayout); }.bind(this)); }, onLayoutChange: function(oEvent) { var sKey oEvent.getParameter(selectedKey) || oEvent.getSource().getSelectedKey(); Container.getService(Personalization).then(function(oService) { return new Promise(function(resolve, reject) { oService.set(homeLayoutPreference, { layout: sKey }, resolve, reject); }); }).catch(function(oError) { MessageToast.show(视图偏好保存失败); }); } }); });这几段代码把前面讲到的API基本都用上了。在onInit里先获取服务再异步读取homeLayoutPreference。读取结果里包含layout字段就说明用户上次选了列表视图否则默认卡片视图。在切换视图的onLayoutChange事件里把新的选择实时写回个性化存储。看起来简单但有几个细节值得展开。先说读取。get出来的对象在API Mode下会完整还原成写入时的JSON结构在Direct Mode下同样如此。不过要注意一个兼容性问题如果应用以前曾经用纯字符串存过某个键现在改成了存对象读取时就需要做类型判断避免value.layout直接报错。这是我踩过的一个真实坑在改造老代码时特别容易出现。再说写入。set的回调包含成功和失败两个分支。使用Promise封装后写入成功进入resolve失败进入reject业务侧只要挂catch就能处理异常。这里我特别强调把错误分支加上因为Direct Mode下如果localStorage容量满了set会静默失败没有错误处理的话用户看着界面好像保存成功下次进来却发现没记住体验很糟糕。最后说应用这一步。在这个场景里应用偏好发生在onInit阶段也就是页面初始化时。如果你要做的场景是在运行中动态切换比如用户点击一个应用到全部视图按钮那就需要再拉取一次存储值并主动刷新视图状态。无论如何读取后应用的逻辑一定要放在Promise的then里不要放在外层同步代码中否则很可能拿到的是null。3.4 实战延伸用Direct Mode存储表格列偏好视图布局只是开胃菜更能体现Direct Mode价值的场景是配合SAPUI5的表格控件使用。比如SmartTable或者sap.ui.table.Table用户经常会调整列顺序、列宽、排序方式这些调整如果能记住会让日常操作舒服很多。如果你用的是sap.ui.comp.smarttable.SmartTable并且嵌在Fiori Launchpad中其实框架的Table Personalization会自动与Personalization服务打通。用户对表格变体的保存、列的开启关闭调整会被系统写进个性化存储服务。这就意味着在Direct Mode下这些操作天然就落在localStorage里不需要我们写任何额外的保存逻辑。但需要注意前提条件SmartTable的API中有一个personalization属性默认是自适应的一般情况下开启usePersonalization为true即可。如果你的表格是常规的sap.m.Table而非SmartTable框架不会自动处理列调整你需要监听列变化事件并自己调用Personalization服务来保存原理和3.3中的分段视图完全一样。我多说一句关于变体Variant与个性化存储关系的理解。不要混为一谈。变体是显式的、用户能自主命名保存的视图设置一般由VariantManagement控件管理可以持久化到后端个性化存储更像是隐式的、用户无感知的偏好记录适合保存用户上次排序字段是哪一个之类的前端状态。两份能力可以并存但各自存储路径和生命周期不同。如果你的需求是提供带名称的表格变体管理那属于VariantManagement的使用范畴不应该用个性化存储硬扛。4. 常见问题与排查技巧速查4.1 set回调一直不触发这是我在新手项目里遇到最多的问题。现象是Personalization服务获取成功调用set传了回调和数据但回调迟迟不执行也不报错。排查思路确认当前页面运行在FLP容器内。sap.ushell.Container只有在FLP环境下才有可用实现单独调试非FLP页面时getService调用可能会失败或者返回一个非标准的空实现。确认网络和权限配置。如果当前配置变成了API模式但后端服务没有正确部署set请求会一直挂起并最终超时。这种情况我建议在控制台打断点检查Personalization服务的mode字段。确认回调参数是否传错。set的签名是set(key, data, callback, errorCallback)有开发者把回调放在了errorCallback位置或者把Promise式的then方法误当成回调在用都会造成不触发。4.2 localStorage里找不到Direct Mode的数据这个问题多半出在键名检查方式上。在浏览器DevTools的Application标签里展开Local Storage后你需要选择的是FLP站点对应的域名而不是应用静态资源所在的域名。如果应用资源和FLP站点部署在不同域名比如应用在SAP BTPFLP在另一套环境Personalization服务会把数据写到FLP域名下的localStorage这个非常容易找错。另外Direct Mode的数据会有统一前缀建议直接在Local Storage的过滤框里输入PERSONALIZATION或USHELL进行检索比肉眼翻快很多。4.3 同一应用多标签页数据不同步localStorage不具备跨标签页的实时监听能力两个标签页同时修改同一个键后写的会覆盖先写的而且先打开的页面不会自动感知到数据变化。这里的缓解方案是监听storage事件。浏览器在localStorage数据变化时会向其他同源标签页派发storage事件页面上可以注册事件处理器拿到变化后的数据并刷新UI。要注意的是触发事件的那个标签页本身不会收到事件所以业务上通常还要结合sessionStorage或BroadcastChannel来做本页内的即时同步。如果应用以单标签页为主这部分可以忽略如果业务经常多开标签页建议还是回到API Mode否则数据一致性很难保证。4.4 Direct Mode与API Mode混用的坑有些项目在开发环境用Direct Mode跑通了测试环境却接入了API Mode这里最容易踩的坑是数据兼容性。第一个坑是键名冲突。两个模式共享同一个Personalization API但如果后端API Mode的存储规则里对键名做了长度或字符限制你在Direct Mode里已经设置的长键名可能在后端存储时被截断或者拒绝。写业务代码时我建议把键名控制在30个字符以内、只用字母数字和下划线给未来的模式切换留好余地。第二个坑是数据格式差异。API Mode返回的对象可能经过后端序列化后带上额外的类型信息而Direct Mode直接存原始JSON。同样的get调用两个模式下拿到的结构不一定100%一致。如果业务代码做了深层的结构判断切换模式前一定要用真实数据做一轮兼容性验证。第三个坑是清理时机。后端API Mode的数据由管理员统一清理而Direct Mode的数据随用户浏览器走。同一个用户在不同浏览器登录看到的偏好状态会不一致这一点要在需求评审阶段就跟产品说清楚别到时候说是bug。5. 进阶优化让Direct Mode用得更顺手5.1 封装一个Promise版通用工具类前面在3.2已经展示过工具类的雏形这里给出一个更完善、可直接复制到项目里的版本。它会把Personalization服务的所有方法统一转换为Promise并加上统一日志和错误冒泡。sap.ui.define([ sap/ushell/Container, sap/base/Log ], function(Container, Log) { use strict; var oServicePromise null; function getService() { if (!oServicePromise) { oServicePromise Container.getService(Personalization); } return oServicePromise; } function toPromise(fnExecutor) { return new Promise(function(resolve, reject) { try { fnExecutor(resolve, reject); } catch (e) { reject(e); } }); } return { get: function(sKey) { return getService().then(function(oService) { return oService.get(sKey); }).catch(function(oError) { Log.error(Personalization get failed, oError); return null; }); }, set: function(sKey, vData) { return getService().then(function(oService) { return toPromise(function(resolve, reject) { oService.set(sKey, vData, resolve, reject); }); }).catch(function(oError) { Log.error(Personalization set failed, oError); throw oError; }); }, getAll: function() { return getService().then(function(oService) { return oService.getAll(); }); } }; });这个工具类最实用的一个点是我把get的失败分支默认收敛成了返回null。在实际项目中读取个性化数据往往发生在前端初始化流程里失败时整个流程不应该被阻断而set的失败则不同它意味着用户偏好没保存成功业务上需要显式感知所以我保留了throw逻辑。这样一套封装基本可以避免业务层到处重复写try/catch。5.2 数据清理与缓存压缩策略Direct Mode把数据存在localStorage随之而来的问题就是可能越积越多。虽然偏好数据单个很小但架不住应用功能多、开发过程中反复调整数据结构最终可能残留大量废弃键。我建议在工具类里增加一个简单的数据清理方法cleanup: function(aPreserveKeys) { return getService().then(function(oService) { return oService.getAll().then(function(oAllData) { var aRemoveKeys Object.keys(oAllData).filter(function(sKey) { return aPreserveKeys.indexOf(sKey) -1; }); aRemoveKeys.forEach(function(sKey) { oService.set(sKey, null); }); return aRemoveKeys; }); }); }这个方法的思路很简单你传入还需要保留的键名白名单它会把白名单之外的所有个性化数据全部清掉。这个方法建议在应用升级后调用一次避免老版本数据结构残留。存储压缩方面我比较保守不建议在Direct Mode下做压缩因为localStorage里存的是序列化后的明文字符串。gzip这类压缩算法需要额外引入库和CPU开销。更合理的做法是控制单条数据大小比如只存必要的字段不要一整个JSON对象全塞进去。如果确实有大字段比如用户设置里包含Base64图片那就应该换IndexedDB而不是继续用Personalization服务。5.3 从Direct Mode平滑迁移到API Mode前面反复提到迁移这里给出一个我实际用过的迁移路径。迁移的核心思路是利用统一API这个层面。由于业务代码操作的是Personalization服务与底层模式无关所以迁移时的主要工作有几步在配置层面把模式从Direct切到API确保后端存储环境已经就绪对应OData服务已部署、权限已授予。写一个一次性数据迁移脚本在应用初始化时调用从Direct Mode的localStorage中读取全部数据逐条set到后端API Mode。这个脚本本身也可以运行在FLP环境中利用Personalization服务直接迁移。迁移完成并验证后清除localStorage中的旧数据。注意在清除前保留一份备份以防有字段在后端因为类型或长度限制迁移失败。关键的坑是迁移期间用户可能在另一台设备上也在使用旧的Direct数据和新写的API数据会产生冲突。最简单的策略是迁移脚本只在检测到本机有Direct数据但配置已经是API模式时执行一次。比如读取到当前服务模式不是Direct那么就把localStorage里的旧数据push上去。执行完再标记一个迁移完成标识防止重复执行。迁移完成之后用户体验上会有一个细微差别数据从仅本机变成了全端这一点一般不会影响前端逻辑但需要让产品团队知道因为涉及用户隐私和存储策略的变更可能需要更新隐私声明之类的文案。写在最后个人实际体会是Direct Mode最适合的场景就是轻量级偏好记录这个边界清晰的需求域。它不需要后端部署、没有复杂的权限配置、API又足够简洁用在视图布局、列偏好这类需求上非常舒服。但它也有明确的边界跨端同步、集中管控、高安全等级这些需求Direct Mode确实无能为力。理解了这些边界才知道什么场景该用什么方案。我自己的建议是前端开发团队可以把Direct Mode作为个性化需求的默认选项先跑通业务把存储切到后端当作后续可选项这个顺序通常能帮你在项目前期省下大量配置工作也让用户的实际体验提升一个明显的档次。