• 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 pathssetDataAnd 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