- Object-Oriented Programming: Westore forces small programs to use object-oriented programming. Instead of directly writing pages, developers start by abstracting classes, class properties and methods, and the relationships between classes in a responsibility-driven Design.
- Write Once, Use Anywhere(Model): The Model designed through object-oriented analysis can express the entire business Model, developers can transfer 100% of the Model code to other environments without any changes, and use other rendering techniques to host the project’s View. For example, small program WebView, small game, Web browser, Canvas, WebGL.
- Passive View: The Westore View is very thin and does not involve any service logic.
- Simple and Intuitive:Westore uses deepClone + dataDiff internally for shortest paths
setData
And a more intuitive programming experienceupdate
, do not need to be usedsetData
. - Testability: There is no direct dependency between View and Model, and developers can test either with mock object injection.
The Westore architecture is similar to the MODEL-View-Presenter architecture:
- View and Store are two-way communication, View and Store refer to each other
- There is no connection between the View and the Model, they are passed through the Store
- Store refers to instances of objects in the Model, which does not depend on Store
- The View is very thin and does not deploy any business logic. This is called a “Passive View”, meaning that there is no initiative
- The Store is very thin, with only the data needed to complex maintain the View and bridge the View and Model
- The Model is very thick, all the logic is deployed there, and the Model can express all the business/game logic completely without Store and View
The Store layer can be thought of as an intermediary in the intermediary pattern, reducing the number of many-to-many relationships between the View and Model to zero, and is responsible for mediating the interaction between the View object and the Model object.
As the projects carried by applets become more and more complex, a proper architecture can improve the extensibility and maintainability of applets. Writing logic to Page/Component is a sin. As business logic becomes complex Page/Component becomes bloated and difficult to maintain. Every time requirements change, the Page/Component is walking on thin ice. Make the project base more robust, easy to maintain and extensible.
The installation
npm i westore --save
Copy the code
NPM related issues reference: applets official documentation: NPM support
Packages
project | describe |
---|---|
westore | Westore core code |
westore-example | Westore official example |
westore-example-ts | Westore Official Example (TS + SCSS) |
For example
Develop the rename app as shown below
Follow the traditional applets development trilogy:
- Write page structure WXML
- Write page style WXSS
- Write page logic JS/TS
Omit WXML, WXSS, js as follows:
Page({
data: {
nickName: ' '
},
async onLoad() {
const nickName = await remoteService.getNickName()
this.setData({
nickName: nickName
})
},
async modifyNickName(newNickName) {
await remoteService.modifyNickName(newNickName)
},
clearInput() {
this.setData({
nickName: ' '})}})Copy the code
Requirements development is complete.
Refactoring using Westore
Define User entities:
class User {
constructor({ nickName, onNickNameChange }) {
this.nickName = nickName || ' '
this.onNickNameChange = onNickNameChange || function() {}}checkNickName() {
// omit the nickName rule verification
}
modifyNickName(nickName) {
if(this.checkNickName(nickName) && nickName ! = =this.nickName) {
this.nickName = nickName
this.onNickNameChange(nickName)
}
}
}
module.exports = User
Copy the code
Define the UserStore:
const { Store } = require('westore')
const User = require('.. /models/user')
class UserStore extends Store {
constructor(options) {
super(a)this.options = options
this.data = {
nickName: ' '}}init() {
const nickName = await remoteService.getNickName()
this.user = new User({
nickName,
onNickNameChange: (newNickName) = >{
this.data.nickName = newNickName
this.update()
await remoteService.modifyNickName(newNickName)
}
})
}
async saveNickName(newNickName) {
this.user.modifyNickName(newNickName)
},
modifyInputNickName(input) {
this.data.nickName = input
this.update()
}
}
module.exports = new UserStore
Copy the code
The page uses UserStore:
const userStore = require('.. /.. /stores/user-store')
Page({
data: userStore.data,
onLoad() {
/* Bind the view to the store userstore. bind('userPage', This.update () updates the view */ in store with this.update('userPage')
userStore.bind(this)},saveNickName(newNickName) {
userStore.saveNickName(newNickName)
},
onInputChange(evt) {
userStore.modifyInputNickName(evt.currentTarget.value)
},
clearInput() {
userStore.modifyInputNickName(' ')}})Copy the code
The generic Model is framework agnostics, and for such a simple program it’s not even worth splitting the logic, but you’ll see huge benefits as requirements swell. So let’s do a slightly more complicated example.
Snake case
Game screenshots:
Design Class diagram:
The light blue parts of the figure can be reused in the applets snake, snake, and Web snake projects without changing a single line of code.
TodoApp case
App Screenshot:
Design Class diagram:
The light blue parts of the figure can be reused in applets TodoApp and Web TodoApp projects without changing a single line of code.
The official case
The official example puts Snake and TodoApp into a small program directory as follows:
├─ ├─ ├─ exercises, ├─ exercises, exercises, exercises, exercises, exercises, exercises, exercises, exercises, exercises, exercises, exercises │ ├─ Game │ ├─ Index │ ├─ logs │ ├─ else.js │ ├─ stores ├─ ├─ else.js │ ├─ else.js │ ├─ else.js │ ├─ else.js │ ├─ else.js │ ├─ ├─ utilsCopy the code
Click here for detailed code
Scan code experience:
The principle of
Where did setData go?
Answer: Where did setData go? The first thing to think about is why weStore encapsulates this API so that users don’t use it directly. In the applet, change the view with setData.
this.setData({
'array[0].text':'changed text'
})
Copy the code
But the intuitive programming experience is:
this.data.array[0].text = 'changed text'
Copy the code
If data is not responsive, you need to manually update it:
this.data.array[0].text = 'changed text'
this.update()
Copy the code
The programming experience above is intuitive and more developer-friendly. Therefore, WeStore hides setData and does not expose it directly to developers. Instead, weStore uses diffData internally to provide the shortest update path, and only the update method is exposed to developers.
Diff Data
Take a look at weStore diffData’s capabilities first:
diff({
a: 1.b: 2.c: "str".d: { e: [2, { a: 4 }, 5]},f: true.h: [1].g: { a: [1.2].j: 111}}, {a: [].b: "aa".c: 3.d: { e: [3, { a: 3}},f: false.h: [1.2].g: { a: [1.1.1].i: "delete" }, k: 'del'
})
Copy the code
The results of Diff are:
{ "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 }
Copy the code
The Diff principle:
- Synchronize all keys to the current store.data
- Recursive traversal with path and result compares all key values
export function diffData(current, previous) {
const result = {}
if(! previous)return current
syncKeys(current, previous)
_diff(current, previous, ' ', result)
return result
}
Copy the code
The key of the previous round of state.data is synchronized mainly to detect elements removed from array or keys removed from OBJ.
Westore implementation details
While improving the programming experience, it also avoids the problem that setData transmits a large amount of new data each time, because the patch after each diff is the shortest path update of setData.
So when you’re not using WeStore you often see code like this:
After using WeStore:
this.data.a.b[1].c = 'f'
this.update()
Copy the code
summary
From the current point of view, most of the small program projects are stacked business logic in the small program Page constructor, basically no readability, to the later maintenance of a huge cost, the goal of the WeStore architecture to decouple business/game logic out, Page is pure Page, It is only responsible for displaying and receiving user input, click, slide, long press or other gesture instructions, and transferring the instructions to Store, which then calls the real program logic model. This hierarchical boundary is clear, with strong maintainability, expansibility and testability, and the size of a single file module can be controlled very appropriately.
Making the address
Github.com/Tencent/wes…
contributors
License
MIT
Copy the code