[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-10849":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":16,"stars7d":16,"stars30d":17,"stars90d":16,"forks30d":16,"starsTrendScore":16,"compositeScore":18,"rankGlobal":10,"rankLanguage":10,"license":10,"archived":19,"fork":19,"defaultBranch":20,"hasWiki":21,"hasPages":19,"topics":22,"createdAt":10,"pushedAt":10,"updatedAt":39,"readmeContent":40,"aiSummary":41,"trendingCount":16,"starSnapshotCount":16,"syncStatus":42,"lastSyncTime":43,"discoverSource":44},10849,"westore","Tencent\u002Fwestore","Tencent","小程序MVVM分层架构","",null,"JavaScript",4291,477,147,61,0,1,57.14,false,"master",true,[23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,5],"diff","json-diff","miniprogram","model","mp","mvc","mvp","mvvm","setdata","setstate","state","state-management","store","update","weapp","web","2026-06-12 04:00:52","# Westore - 小程序MVVM分层架构\n\n* **Object-Oriented Programming:** Westore 强制小程序使用面向对象程序设计，开发者起手不是直接写页面，而是使用职责驱动设计 (Responsibility-Driven Design)的方式抽象出类、类属性和方法以及类之间的关联关系。\n* **Write Once, Use Anywhere(Model):** 通过面向对象分析设计出的 Model 可以表达整个业务模型，开发者可移植 100% 的 Model 代码不带任何改动到其他环境，并使用其他渲染技术承载项目的 View，比如小程序WebView、小游戏、Web浏览器、Canvas、WebGL。\n* **Passive View:** Westore 架构下的 View 非常薄，没有参杂任何业务逻辑，只做被动改变。\n* **Simple and Intuitive:** Westore 内部使用 deepClone + dataDiff 换取最短路径 `setData` 和更符合直觉的编程体验，只需 `update`，不需要再使用 `setData`。\n* **Testability:** View 和 Model 之间没有直接依赖，开发者能够借助模拟对象注入测试两者中的任一方。\n\n![](.\u002Fassets\u002Fwestore.png)\n\nWestore 架构和 MVP(Model-View-Presenter) 架构很相似:\n\n* View 和 Store 是双向通讯，View 和 Store 互相引用\n* View 与 Model 不发生联系，都通过 Store 传递\n* Store 引用 Model 里对象的实例，Model 不依赖 Store\n* View 非常薄，不部署任何业务逻辑，称为\"被动视图\"（Passive View），即没有任何主动性\n* Store 非常薄，只复杂维护 View 需要的数据和桥接 View 和 Model\n* Model 非常厚，所有逻辑都部署在那里，Model 可以脱离 Store 和 View 完整表达所有业务\u002F游戏逻辑\n\nStore 层可以理解成 **中介者模式** 中的中介者，使 View 和 Model 之间的多对多关系数量减少为 0，负责中转控制视图对象 View 和模型对象 Model 之间的交互。\n\n随着小程序承载的项目越来越复杂，合理的架构可以提升小程序的扩展性和维护性。把逻辑写到 Page\u002FComponent 是一种罪恶，当业务逻辑变得复杂的时候 Page\u002FComponent 会变得越来越臃肿难以维护，每次需求变更如履薄冰， westore 定义了一套合理的小程序架构适用于任何复杂度的小程序，让项目底座更健壮，易维护可扩展。\n\n## 安装\n\n```bash\nnpm i westore --save\n```\n\nnpm 相关问题参考：[小程序官方文档: npm 支持](https:\u002F\u002Fdevelopers.weixin.qq.com\u002Fminiprogram\u002Fdev\u002Fdevtools\u002Fnpm.html)\n\n## Packages\n\n| **项目**                         | **描述**                           |\n| ------------------------------- | ----------------------------------- |\n| [westore](https:\u002F\u002Fgithub.com\u002FTencent\u002Fwestore\u002Ftree\u002Fmaster\u002Fpackages\u002Fwestore)  | westore 的核心代码 |\n| [westore-example](https:\u002F\u002Fgithub.com\u002FTencent\u002Fwestore\u002Ftree\u002Fmaster\u002Fpackages\u002Fwestore-example) | westore 官方例子|\n| [westore-example-ts](https:\u002F\u002Fgithub.com\u002FTencent\u002Fwestore\u002Ftree\u002Fmaster\u002Fpackages\u002Fwestore-example-ts)|  westore 官方例子(ts+scss) |\n\n## 举个例子\n\n\u003Cimg src=\".\u002Fassets\u002Fhome-demo.png\" width=\"400px\">\n\n其类图如下所示：\n\n\u003Cimg src=\".\u002Fassets\u002Fclass-diagram.png\" width=\"800px\">\n\n```js\n\u002F\u002F 平台无关的 Model\nimport Counter from '..\u002Fmodels\u002Fcounter'\n\u002F\u002F 平台无关的 Model\nimport User  from '..\u002Fmodels\u002Fuser'\nimport { Store }  from 'westore'\n\n\u002F\u002F 页面 store，一个页面一个\nclass HomeStore extends Store {\n  constructor() {\n    super()\n    this.data = {\n      count: 0,\n      motto: 'Hello World',\n      userInfo: null\n    }\n    \u002F\u002F 消费 Model\n    this.counter = new Counter()\n    \u002F\u002F 消费 Model\n    this.user = new User({\n      onUserInfoLoaded: () => {\n        this.syncUserModel()\n      }\n    })\n    this.syncCountModel()\n  }\n\n  \u002F\u002F 同步 Model 的数据到 ViewModel 并更新视图\n  syncCountModel () {\n    this.data.count = this.counter.count\n    this.update()\n  }\n\n  \u002F\u002F 同步 Model 的数据到 ViewModel 并更新视图 \n  syncUserModel () {\n    this.data.motto = this.user.motto\n    this.data.userInfo = this.user.userInfo\n    this.update()\n  }\n\n  increment() {\n    this.counter.increment()\n    this.syncCountModel()\n  }\n\n  decrement() {\n    this.counter.decrement()\n    this.syncCountModel()\n  }\n\n  getUserProfile() {\n    this.user.getUserProfile()\n  }\n}\n\nmodule.exports = new HomeStore\n```\n\n通用 Model 是框架无关的，对于这样简单的程序甚至不值得把这种逻辑分开，但是随着需求的膨胀你会发现这么做带来的巨大好处。所以下面举一个复杂一点点的例子。\n\n## 贪吃蛇案例\n\n游戏截图：\n\n\u003Cimg src=\".\u002Fassets\u002Fsnake-game.jpg\" width=\"300px\">\n\n设计类图：\n\n![](.\u002Fassets\u002Fsnake.png)\n\n图中浅蓝色的部分可以在小程序贪吃蛇、小游戏贪吃蛇和Web贪吃蛇项目复用，不需要更改一行代码。\n\n## TodoApp 案例\n\n应用截图:\n\n\u003Cimg src=\".\u002Fassets\u002Ftodo-app.jpg\" width=\"250px\">\n\n设计类图：\n\n![](.\u002Fassets\u002Ftodo-app-class.png)\n\n图中浅蓝色的部分可以在小程序 TodoApp 和 Web TodoApp项目复用，不需要更改一行代码。\n\n## 官方案例\n\n官方例子把**贪吃蛇**和**TodoApp**做进了一个小程序目录如下:\n\n```\n├─ models    \u002F\u002F 业务模型实体\n│   └─ snake-game\n│       ├─ game.js\n│       └─ snake.js   \n│  \n│  ├─ log.js\n│  ├─ todo.js   \n│  └─ user.js   \n│\n├─ pages     \u002F\u002F 页面\n│  ├─ game\n│  ├─ index\n│  ├─ logs   \n│  └─ other.js  \n│\n├─ stores    \u002F\u002F 页面的数据逻辑，page 和 models 的桥接器\n│  ├─ game-store.js   \n│  ├─ log-store.js      \n│  ├─ other-store.js    \n│  └─ user-store.js   \n│\n├─ utils\n\n```\n\n详细代码[点击这里](https:\u002F\u002Fgithub.com\u002FTencent\u002Fwestore\u002Ftree\u002Fmaster\u002Fpackages\u002Fwestore-example)\n\n## 原理\n\n### setData 去哪了？\n\n回答 **setData 去哪了？** 之前先要思考为什么 westore 封装了这个 api，让用户不直接使用。在小程序中，通过 `setData` 改变视图。\n\n```js\nthis.setData({\n  'array[0].text':'changed text'\n})\n```\n\n但是符合直觉的编程体验是:\n\n```js\nthis.data.array[0].text = 'changed text'\n```\n\n如果 data 不是响应式的，需要手动 update:\n\n```js\nthis.data.array[0].text = 'changed text'\nthis.update()\n```\n\n上面的编程体验是符合直觉且对开发者更友好的。所以 westore 隐藏了 setData 不直接暴露给开发者，而是内部使用 diffData 出最短更新路径，暴露给开发者的只有 update 方法。\n\n### Diff Data\n\n先看一下 westore [diffData](https:\u002F\u002Fgithub.com\u002FTencent\u002Fwestore\u002Fblob\u002Fmaster\u002Fpackages\u002Fwestore\u002Findex.esm.js#L6-L12) 的能力:\n\n``` js\ndiff({\n    a: 1, b: 2, c: \"str\", d: { e: [2, { a: 4 }, 5] }, f: true, h: [1], g: { a: [1, 2], j: 111 }\n}, {\n    a: [], b: \"aa\", c: 3, d: { e: [3, { a: 3 }] }, f: false, h: [1, 2], g: { a: [1, 1, 1], i: \"delete\" }, k: 'del'\n})\n```\n\nDiff 的结果是:\n\n``` js\n{ \"a\": 1, \"b\": 2, \"c\": \"str\", \"d.e[0]\": 2, \"d.e[1].a\": 4, \"d.e[2]\": 5, \"f\": true, \"h\": [1], \"g.a\": [1, 2], \"g.j\": 111, \"g.i\": null, \"k\": null }\n```\n\n![diff](.\u002Fassets\u002Fdiff.jpg)\n\nDiff 原理:\n\n* 同步所有 key 到当前 store.data\n* 携带 path 和 result 递归遍历对比所有 key value\n\n``` js\nexport function diffData(current, previous) {\n  const result = {}\n  if (!previous) return current\n  syncKeys(current, previous)\n  _diff(current, previous, '', result)\n  return result\n}\n```\n\n同步上一轮 state.data 的 key 主要是为了检测 array 中删除的元素或者 obj 中删除的 key。\n\n### Westore 实现细节\n\n![](.\u002Fassets\u002Fwestore-detail.png)\n\n提升编程体验的同时，也规避了每次 setData 都传递大量新数据的问题，因为每次 diff 之后的 patch 都是 setData 的最短路径更新。\n\n所以没使用 westore 的时候经常可以看到这样的代码:\n\n![not-westore](.\u002Fassets\u002Fnot-westore.png)\n\n使用完 westore 之后:\n\n```js\nthis.data.a.b[1].c = 'f'\nthis.update()\n```\n\n## 小结\n\n从目前来看，绝大部分的小程序项目都把业务逻辑堆积在小程序的 Page 构造函数里，可读性基本没有，给后期的维护带来了巨大的成本，westore 架构的目标把业务\u002F游戏逻辑解耦出去，Page 就是纯粹的 Page，它只负责展示和接收用户的输入、点击、滑动、长按或者其他手势指令，把指令中转给 store，store 再去调用真正的程序逻辑 model，这种分层边界清晰，维护性、扩展性和可测试性极强，单个文件模块大小也能控制得非常合适。\n\n## 贡献者\n\n\u003Ca href=\"https:\u002F\u002Fgithub.com\u002FTencent\u002Fwestore\u002Fgraphs\u002Fcontributors\">\n  \u003Cimg src=\"https:\u002F\u002Fcontrib.rocks\u002Fimage?repo=Tencent\u002Fwestore\" \u002F>\n\u003C\u002Fa>\n\n\n## License\n\nMIT \n","Westore 是一个用于小程序的 MVVM 分层架构框架。它通过强制使用面向对象编程和职责驱动设计，帮助开发者更好地组织代码结构。其核心功能包括：一次编写即可在多个环境（如小程序、Web 浏览器等）中复用 Model 代码；采用被动视图模式使 View 层保持简洁；内部利用 deepClone 和 dataDiff 提供直观的数据更新方式，简化了 `setData` 的调用过程；支持良好的测试性，View 和 Model 可以独立进行单元测试。适用于需要构建复杂业务逻辑且重视可维护性和扩展性的微信小程序项目。",2,"2026-06-11 03:30:29","top_topic"]