{"id":21708502,"url":"https://github.com/ufologist/model-adapter","last_synced_at":"2025-04-12T16:30:32.633Z","repository":{"id":57300356,"uuid":"200787416","full_name":"ufologist/model-adapter","owner":"ufologist","description":"模型适配器","archived":false,"fork":false,"pushed_at":"2023-01-04T06:24:52.000Z","size":315,"stargazers_count":22,"open_issues_count":3,"forks_count":5,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-11T20:06:24.634Z","etag":null,"topics":["adapter","backend","bridge","model"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ufologist.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-08-06T06:09:41.000Z","updated_at":"2022-02-18T10:11:37.000Z","dependencies_parsed_at":"2023-02-02T01:16:28.957Z","dependency_job_id":null,"html_url":"https://github.com/ufologist/model-adapter","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ufologist%2Fmodel-adapter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ufologist%2Fmodel-adapter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ufologist%2Fmodel-adapter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ufologist%2Fmodel-adapter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ufologist","download_url":"https://codeload.github.com/ufologist/model-adapter/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248596420,"owners_count":21130700,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["adapter","backend","bridge","model"],"created_at":"2024-11-25T22:27:18.508Z","updated_at":"2025-04-12T16:30:32.555Z","avatar_url":"https://github.com/ufologist.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# model-adapter\n\n[![NPM version][npm-image]][npm-url] [![Build Status][ci-status-image]][ci-status-url] [![Coverage Status][coverage-status-image]][coverage-status-url] [![Known Vulnerabilities][vulnerabilities-status-image]][vulnerabilities-status-url] [![changelog][changelog-image]][changelog-url] [![license][license-image]][license-url]\n\n[vulnerabilities-status-image]: https://snyk.io/test/npm/model-adapter/badge.svg\n[vulnerabilities-status-url]: https://snyk.io/test/npm/model-adapter\n[ci-status-image]: https://travis-ci.org/ufologist/model-adapter.svg?branch=master\n[ci-status-url]: https://travis-ci.org/ufologist/model-adapter\n[coverage-status-image]: https://coveralls.io/repos/github/ufologist/model-adapter/badge.svg?branch=master\n[coverage-status-url]: https://coveralls.io/github/ufologist/model-adapter\n[npm-image]: https://img.shields.io/npm/v/model-adapter.svg?style=flat-square\n[npm-url]: https://npmjs.org/package/model-adapter\n[license-image]: https://img.shields.io/github/license/ufologist/model-adapter.svg\n[license-url]: https://github.com/ufologist/model-adapter/blob/master/LICENSE\n[changelog-image]: https://img.shields.io/badge/CHANGE-LOG-blue.svg?style=flat-square\n[changelog-url]: https://github.com/ufologist/model-adapter/blob/master/CHANGELOG.md\n\n[![npm-image](https://nodei.co/npm/model-adapter.png?downloads=true\u0026downloadRank=true\u0026stars=true)](https://npmjs.com/package/model-adapter)\n\n模型适配器: 后端数据与前端数据的桥梁\n\n专注于解决前端那些老生常谈的问题(没碰到过算你赢), 如果你遇到过以下场景, 请试用一下\n- 嵌套数据: 哎呀\\~报错了; 哦\\~访问 xxx 为空了啊\n- 空数据: 咦\\~怎么没有头像; 哦\\~需要一个默认头像啊\n- 格式化数据: 诶\\~要显示年月日; 但返回的数据是时间戳啊\n\n## 初衷\n\n在 `Vue` 或者其他视图层框架中, 如果直接使用如下插值表达式, 当嵌套对象(通常是后端返回的数据)中的某一层级为空时就会报错 `TypeError: Cannot read property 'xxx' of undefined`, **造成整个组件都无法渲染**.\n\n```javascript\n{{a.aa.aaa}}\n```\n\n为了解决这种问题, 让前端的视图层能够容错增强代码的健壮性, 我们可能要写出如糖葫芦一般的防御性代码, 例如这样 `{{a \u0026\u0026 a.aa \u0026\u0026 a.aa.aaa}}`, 要是再多嵌套几层, 简直不忍直视啊.\n\n舒服一些的处理方式是通过 `object path get` 之类的库事先处理好数据, 形成前端的视图层模型, 尽量避免嵌套数据, 再到视图层中使用, 例如\n\n```javascript\n// 在视图中使用: {{aaa}}\nvar vm = {\n    aaa: _.get('a.aa.aaa')\n};\n```\n\n## 核心思路\n\n建立一个新的模型, 通过设置**默认值来补齐**源数据(模型)上可能缺少的对象嵌套层次. 这样我们就能够以访问源数据一致的方式来访问新模型上的数据, 即可以理解为是对源数据的增强.\n\n例如要访问源数据上的 `a.aa.aaa`, 如果源数据的 `a` 为 `null`, 那么我们直接访问肯定是会报错的.\n\n因此我们可以准备一份默认数据, 来补齐源数据上可能缺失的数据.\n* 当源数据上没有数据(`undefined` 或者 `null`)时, 模型返回默认数据上的数据\n* 当源数据上有数据时, 模型返回源数据上的数据\n\n```\n新模型(target)                       源数据(source)          默认值(default)\n{                                   {                       {\n    a: {                        \u003c─       a: null,       \u003c─      a: {\n        aa: {                                                       aa: {\n            aaa: 'default-aaa'                                            aaa: 'default-aaa'\n        }                                                           }\n    },                                                          },\n    b: 'source-b'               \u003c─       b: 'source-b'          b: 'default-b',\n    c: 'default-c'              \u003c─                      \u003c─      c: 'default-c'\n}                                   }                       }\n```\n\n另外一种**映射**属性的实现思路可以参考[v0.0.1](https://github.com/ufologist/model-adapter/tree/v0.0.1)版本\n\n--------\n\n针对格式化数据的需求, 采取的思路为将属性改写为 `setter/getter`, 以输入和输出的概念来适配新模型上的属性\n* `setter` 做为输入(input), 以源数据上的值为标准来接收数据\n  * 例如源数据返回的字段值为时间戳, 那么我们设置属性值时, 始终设置为时间戳: `a.aa.aaa = 1566814067549`\n* `getter` 做为输出(output), 将源数据做转换后返回我们需要的格式\n  * 例如将时间戳格式化为日期字符串 `a.aa.aaa // 2019-08-26`\n\n```javascript\n// setter 时间戳\na.aa.aaa = 1566814067549 // 输入(input)\n// getter 格式化\na.aa.aaa // 2019-08-26   // 输出(output)\n```\n\n保持输入和输出是有关联的因果关系\n* `输入 -\u003e 输出`: 因为有什么输入, 所以有什么输出, 类似函数式编程思维\n* 输入是原始值, 由输入值推导出输出, 输入是对外的唯一接口\n\n## 示例\n\n### 嵌套数据/空数据: 用默认值来补齐(重点是补齐嵌套对象)\n\n```javascript\nimport ModelAdapter from 'model-adapter';\n\n// 这里示例由后端接口返回的数据\nvar ajaxData = {\n    name: null,\n    age: 18,\n    extData: null\n};\n\nvar model = new ModelAdapter(ajaxData, {\n    name: 'Guest',\n    extData: {\n        country: {\n            name: 'China'\n        }\n    }\n});\n\nconsole.log(model.name);                 // 'Guest'\nconsole.log(model.age);                  // 18\nconsole.log(model.extData.country.name); // 'China'\n```\n\n### 格式化数据: 变形\n\n```javascript\nimport ModelAdapter from 'model-adapter';\n\nvar ajaxData = {\n    foo: {\n        bar: {\n            date: 1565001521464\n        }\n    }\n};\n\nvar model = new ModelAdapter(ajaxData, null, {\n    'foo.bar.date': {\n        transformer: function(value, source) { // 变形器负责格式化数据\n            return new Date(value).toISOString();\n        }\n    }\n});\n\nvar restored = model.$restore();\n\nconsole.log(model.foo.bar.date);    // '2019-08-05T10:38:41.464Z'\nconsole.log(restored.foo.bar.date); // 1565001521464\n```\n\n### 数组: 在 `transformer` 中适配数组元素的模型\n\n```javascript\nimport ModelAdapter from 'model-adapter';\n\nvar ajaxData = {\n    users: [{\n        name: null,\n        age: 18,\n        extData: null\n    }, {\n        name: 'Shine',\n        age: 19,\n        extData: {\n            country: {\n                name: 'USA'\n            }\n        }\n    }]\n};\n\nvar model = new ModelAdapter(ajaxData, null, {\n    users: {\n        transformer: function(value) {\n            return value.map(function(item) {\n                return new ModelAdapter(item, {\n                    name: 'Sun',\n                    extData: {\n                        country: {\n                            name: 'China'\n                        }\n                    }\n                });\n            });\n        }\n    }\n});\n\nconsole.log(model.users[0].name);                 // 'Sun'\nconsole.log(model.users[0].age);                  // 18\nconsole.log(model.users[0].extData.country.name); // 'China'\n\nconsole.log(model.users[1].name);                 // 'Shine'\nconsole.log(model.users[1].age);                  // 19\nconsole.log(model.users[1].extData.country.name); // 'USA'\n```\n\n### 先声明模型再设置源数据\n\n```javascript\nimport ModelAdapter from 'model-adapter';\n\n// 声明模型(预先定义好 defaults 和 propertyAdapter)\nvar model = new ModelAdapter(null, {\n    name: 'Guest',\n    extData: {\n        country: {\n            name: 'China'\n        }\n    }\n});\n\nvar ajaxData = {\n    name: null,\n    age: 18,\n    extData: null\n};\n// 设置源数据\nmodel.$setSource(ajaxData);\n\nconsole.log(model.name);                 // 'Guest'\nconsole.log(model.age);                  // 18\nconsole.log(model.extData.country.name); // 'China'\n```\n\n### 声明模型类\n\n```javascript\nimport ModelAdapter from 'model-adapter';\n\n// 声明模型类(预先定义好 defaults 和 propertyAdapter)\nclass User extends ModelAdapter {\n    constructor(source) {\n        super(source, {\n            name: 'Guest',\n            extData: {\n                country: {\n                    name: 'China'\n                }\n            }\n        });\n    }\n}\n\nvar ajaxData = {\n    name: null,\n    age: 18,\n    extData: null\n};\n\n// 使用模型类时, 只需要设置源数据\nvar user = new User(ajaxData);\n\nconsole.log(user);                      // \u003cUser\u003e\nconsole.log(user.name);                 // 'Guest'\nconsole.log(user.age);                  // 18\nconsole.log(user.extData.country.name); // 'China'\n```\n\n### 与其他框架集成\n\n* [Vue](https://raw.githack.com/ufologist/model-adapter/master/test/vue-with-model-adapter.html)\n  * [如何让 Vue 为 data 上新增的属性创建 getter/setter](https://github.com/ufologist/model-adapter/blob/master/vue-dynamic-add-property-problem.md)\n* [React](https://raw.githack.com/ufologist/model-adapter/master/test/react-with-model-adapter.html)\n\n### 建议的接入方式\n\n* 方式一: 在前端服务层中接入\n* 方式二: 在后端(`Node`)中间层中接入\n\n例如\n```javascript\n// service/user.js\nexport function getUser() {\n    return axios('/user').then(function(response) {\n        return new ModelAdapter(response.data, {\n            name: 'Guest',\n            extData: {\n                country: {\n                    name: 'China'\n                }\n            }\n        });\n    });\n}\n```\n\n## API 概览\n\n* 构造函数\n\n  ```javascript\n  var model = new ModelAdapter(source, defaults, propertyAdapter);\n  ```\n\n  * `source`: 源数据\n  * `defaults`: 源数据的默认值\n  * `propertyAdapter`: 属性适配器\n\n    结构为\n    ```javascript\n    {\n        propertyPath1: \u003cadapter\u003e,\n        propertyPath2: \u003cadapter\u003e,\n        ...\n    }\n    ```\n\n    * **属性名**为新模型的属性名, 用于指定要适配的属性的 path 路径\n    * **属性值**用于配置适配器, 支持的配置方式详见 [API文档](https://doc.esdoc.org/github.com/ufologist/model-adapter/class/src/model-adapter.js~ModelAdapter.html)\n* 设置源数据\n\n  ```javascript\n  model.$setSource(source);\n  ```\n* 获取源数据(支持通过 `propertyPath` 参数安全地获取源数据)\n\n  ```javascript\n  var source = model.$getSource(propertyPath);\n  ```\n\n  适用于你设置了 `defaults`, 但又需要判断原始值是否为\"空\"的情况\n* 新增/更新/删除属性适配器(当传入的适配器为 `null` 时, 删除该适配器)\n\n  ```javascript\n  model.$setAdapter(propertyPath, adapter);\n  ```\n* 还原数据(支持通过 `propertyPath` 参数安全地获取还原的数据)\n\n  ```javascript\n  var restored = model.$restore(propertyPath);\n  ```\n\n  适用于你设置了 `transformer`, 但又需要根据原始值来进行判断的逻辑\n\n## 参考\n\n* [「数据模型」是如何助力前端开发的](https://mp.weixin.qq.com/s/q6xybux0fhrUz5HE5TY0aA)\n\n  \u003e 场景\n  \u003e * 在这种场景下，我们在开发中就不得不写一些防御性的代码，久而久之，项目中类似代码会越来越多，**碰到层级深的，防御性代码就会写的越来越恶心**。另外还有的就是，如果服务端在这中间某个字段删掉了，那就又得特殊处理了，否则会有一些未知的非空错误报错，这种编码方式会导致前端严重依赖服务端定义的数据结构，非常不利于后期维护。\n  \u003e * 平时开发中，我们拿到了服务端返回的数据，有些不是标准格式的，是无法直接在视图上直接使用的，是需要额外**格式化处理**的，比如我司服务端返回的的价格字段单位统一是分，跟时间相关的字段统一是毫秒值，这个时候我们在组件的生命周期内，就不得不而外增加一些对数据处理的逻辑，还有就是这部分处理在很多组件都是公用的，我们就不得不频繁编写类似的代码，数据处理逻辑没有得到复用。\n  \u003e * 在用户做了一些交互后，需要将一些数据存储到服务端，这个时候我们拿到的数据往往也是非标准的，就比如你要提交个表单，其中有个价格字段，你拿到价格单位可能是百位的，而服务端需要的单位必须是分位的，这个时候**在提交数据之前，你又得对这部分数据进行处理**，还有就是有些接口的参数是json字符串形式的，可能是多级嵌套的，你还要需要特意构造这样的参数数据格式，导致开发中编写了太多与业务无关的逻辑，随着项目逐渐扩大或者维护人员更迭，项目会越来越不好维护。\n  \u003e\n  \u003e 总结\n  \u003e * 前后端数据结构没有解耦，前端在应对不定的服务端数据结构前提下，需要编写过多的保护性代码，不利于维护的同时，代码健壮性也不高。\n  \u003e * 基础数据逻辑处理没有和UI视图解耦，容易阻塞视图渲染，同时，在视图组件上存在太多的基础数据逻辑处理，没有有效复用。","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fufologist%2Fmodel-adapter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fufologist%2Fmodel-adapter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fufologist%2Fmodel-adapter/lists"}