Recently, our team friend Koike shared the article “BetterScroll 2.0 release: Keep improving, With you” to the team group. After seeing the plug-in architecture design, He suddenly became interested in it, because he had done related sharing in the team before. Now that you’re interested, you’ve decided to start a learning tour of BetterScroll 2.0 source code.

The rest of this article will focus on the plug-in architecture design, but before looking at BetterScroll 2.0’s plug-in architecture, let’s take a quick look at BetterScroll.

BetterScroll source code learning brain diagram 1.0

Introduction to BetterScroll

BetterScroll is a plug-in that focuses on various scrolling scenarios on mobile (already supported on PC). Its core is the implementation of the reference iscroll, its API design is basically compatible with IScroll, iscroll based on the extension of some features and some performance optimization.

More than 30 versions of BetterScroll 1.0 have been released, with 50,000 NPM downloads per month and 12,600 + STAR downloads. So why upgrade 2.0?

The original idea for v2 came from a community need:

  • Does BetterScroll support on-demand loading?

Source: BetterScroll 2.0 release: BetterScroll, with you

To support plug-in loading on demand, BetterScroll 2.0 uses a plug-in architecture. CoreScroll, as the smallest scroll unit, exposes a rich set of events and hooks. The rest of the functionality is extended by different plugins, making BetterScroll more flexible and adaptable to different scenarios.

The following is the overall structure of BetterScroll 2.0:

(Photo credit: Juejin. Im /post/686808…

This project adopts the monorepos organization mode, using LERNA for multi-package management, and each component is an independent NPM package:

Like the Watermelon player, BetterScroll 2.0 is a plug-in design, with the CoreScroll as the minimum scroll unit and the rest of the functionality extended through plug-ins. For example, the pull-up and pull-down functions common to long lists are implemented in BetterScroll 2.0 using pull-up and pull-down plug-ins, respectively.

One of the benefits of plugins is that they can be loaded on demand, and breaking down individual functionality into individual plug-ins makes the core system more stable and robust. Ok, with a brief introduction to BetterScroll, let’s get down to business and analyze some of the things we can learn from the project.

Ii. Development experience

2.1 Better intelligent tips

BetterScroll 2.0 is being developed in TypeScript. To give developers better intelligent tips when using BetterScroll, the BetterScroll team takes advantage of the automatic merging of TypeScript interfaces. Enable developers to use a plug-in, the corresponding Options prompt and bs (BetterScroll instance) can be corresponding method prompt.

2.1.1 Smart Plug-in Options

2.1.2 Intelligent BetterScroll instance method tips

Next, to better understand BetterScroll’s design philosophy, let’s take a quick look at the plug-in architecture.

Introduction to plug-in architecture

3.1 Concept of plug-in architecture

Plug-in Architecture is a feature-oriented extensible Architecture that is often used to implement product-based applications. The plug-in architecture pattern allows you to add additional application functionality as plug-ins to the core application, providing extensibility as well as functional separation and isolation.

The plug-in architecture pattern consists of two types of architectural components: Core systems and plug-in modules. Application logic is divided into separate plug-in modules and core systems, providing scalability, flexibility, functional isolation, and custom processing logic.

In the figure, the function of Core System is relatively stable and will not be constantly modified due to the expansion of business functions, while the plug-in module can be continuously adjusted or expanded according to the needs of actual business functions. The essence of a plug-in architecture is to encapsulate the parts that may need to change constantly in plug-ins so that they can expand quickly and flexibly without affecting the stability of the overall system.

The core system of a plug-in architecture typically provides the minimum set of capabilities required for the system to run. Plug-in modules are stand-alone modules that contain specific processing, additional functionality, and custom code to enhance or extend additional business capabilities to the core system. Plug-in modules are usually independent of each other, and some plug-ins depend on several other plug-ins. It is important to minimize communication between plug-ins to avoid dependency problems.

3.2 Advantages of plug-in architecture

  • High flexibility: Overall flexibility is the ability to respond quickly to environmental changes. Because of the low coupling between plug-ins, changes are usually isolated and can be implemented quickly. Typically, the core system is stable and fast, has some robustness, and requires little modification.
  • Testability: Plug-ins can be tested independently and can be easily emulated, demonstrating or prototyping new features without changing the core system.
  • High performance: While plug-in architectures by themselves do not lead to high performance applications, applications built with plug-in architectures often perform well because they can be customized or tailored to eliminate unwanted functionality.

With the plug-in architecture basics covered, let’s take a look at how BetterScroll 2.0 designs the plug-in architecture.

BetterScroll plug-in architecture

For a plug-in core system design, it involves three key points: plug-in management, plug-in connectivity, and plug-in communication. Here’s a step-by-step look at how BetterScroll 2.0 implements a plug-in architecture around these three key points.

4.1 Plug-in Management

To manage built-in plug-ins in a unified manner, developers can easily develop customized plug-ins based on business requirements. BetterScroll 2.0 agrees on a uniform plug-in development specification. The plug-in for BetterScroll 2.0 needs to be a class and has the following features:

1. Static pluginName attribute;

2. Implement the PluginAPI interface (if and only if it is necessary to proxy plug-in methods to BS);

3. Constructor’s first argument is a BetterScroll instance bs. You can inject your own logic with bs events or hooks.

To get an intuitive understanding of the specification, let’s take the built-in PullUp plug-in as an example and see how it implements the specification. PullUp plugin provides the ability to PullUp loads for BetterScroll extension.

As the name implies, the static pluginName attribute represents the name of the plug-in, and the PluginAPI interface represents the API provided by the plug-in instance. The PluginAPI interface supports four methods:

  • FinishPullUp (): void: end the pullup behavior;
  • openPullUp(config? : PullUpLoadOptions): void: dynamically enable the pull-up function.
  • ClosePullUp (): void: turns off pullup;
  • AutoPullUpLoad (): void: automatically performs pull-up loading.

The plug-in injects the BetterScroll instance bs via its constructor, and then we can inject our logic via bs events or hooks. So why inject bs instances? How to use BS instances? Let’s keep these questions in mind here, and we’ll analyze them later.

4.2 Plug-in Connection

The core system needs to know which plug-ins are currently available, how to load them, and when to load them. A common implementation is the plug-in registry mechanism. The core system provides a plug-in registry (which can be a configuration file, code, or database) that contains information about each plug-in module, including its name, location, and when to load it (either on launch or on demand).

Here we take the PullUp plug-in mentioned earlier as an example to see how to register and use it. First you need to install the PullUp plugin using the following command:

$ npm install @better-scroll/pull-up --save
Copy the code

After successfully installing the Pullup plugin, you need to register the plugin using the BScroll. Use method:

import BScroll from '@better-scroll/core'
import Pullup from '@better-scroll/pull-up'

BScroll.use(Pullup)
Copy the code

Then, to instantiate BetterScroll, pass in the PullUp plug-in configuration item.

new BScroll('.bs-wrapper', {
  pullUpLoad: true
})
Copy the code

Now that we know that plug-ins can be registered through the BScroll. Use method, what does the method do internally? To answer this question, let’s look at the corresponding source code:

// better-scroll/packages/core/src/BScroll.ts
export const BScroll = (createBScroll as unknown) as BScrollFactory
createBScroll.use = BScrollConstructor.use
Copy the code

In the bscroll.ts file, the bscroll.use method refers to the static method BScrollConstructor. Use, which is implemented as follows:

export class BScrollConstructor<O = {} >extends EventEmitter {
  static plugins: PluginItem[] = []
  static pluginsMap: PluginsMap = {}

  static use(ctor: PluginCtor) {
    const name = ctor.pluginName
    const installed = BScrollConstructor.plugins.some(
      (plugin) = > ctor === plugin.ctor
    )
    // Omit part of the code
    if (installed) return BScrollConstructor
    BScrollConstructor.pluginsMap[name] = true
    BScrollConstructor.plugins.push({
      name,
      applyOrder: ctor.applyOrder,
      ctor,
    })
    return BScrollConstructor
  }
}
Copy the code

Looking at the code above, you can see that the use method takes a parameter of type PluginCtor, which describes the characteristics of the plug-in constructor. The PluginCtor type is declared as follows:

interface PluginCtor {
  pluginName: stringapplyOrder? : ApplyOrdernew (scroll: BScroll): any
}
Copy the code

When we call the bscroll.use (Pullup) method, we first get the name of the current plug-in, and then determine whether the current plug-in has been installed. The BScrollConstructor object is returned directly if installed, otherwise the plug-in is registered. The pluginsMap ({}) and plugins ([]) objects are stored in the pluginsMap ({}) and plugins ([]) objects.

In addition, when the use static method is called, the BScrollConstructor object is returned to support chained calls:

BScroll.use(MouseWheel)
  .use(ObserveDom)
  .use(PullDownRefresh)
  .use(PullUpLoad)
Copy the code

Now that we know how the BScroll. Use method registers the plug-in internally, registering the plug-in is only the first step. To use the registered plug-in, we need to initialize the plug-in by passing in the configuration items of the plug-in when we instantiate it. For the PullUp plug-in, we initialize the plug-in in the following way.

new BScroll('.bs-wrapper', {
  pullUpLoad: true
})
Copy the code

So to understand how the plug-in connects to the core system and initializes the plug-in, we need to analyze the BScroll constructor:

// packages/core/src/BScroll.ts
export const BScroll = (createBScroll as unknown) as BScrollFactory

export function createBScroll<O = {}>( el: ElementParam, options? : Options & O ): BScrollConstructor & UnionToIntersection<ExtractAPI<O>> {const bs = new BScrollConstructor(el, options)
  return (bs as unknown) as BScrollConstructor &
    UnionToIntersection<ExtractAPI<O>>
}
Copy the code

Inside the createBScroll factory method, the BScrollConstructor constructor is called with the new keyword to create a BetterScroll instance. So the next focus is to analyze the BScrollConstructor constructor:

// packages/core/src/BScroll.ts
export class BScrollConstructor<O = {} >extends EventEmitter {
  constructor(el: ElementParam, options? : Options & O) {
    const wrapper = getElement(el)
    // Omit part of the code
    this.plugins = {}
    this.hooks = newEventEmitter([...] )this.init(wrapper)
  }
  
  private init(wrapper: MountedBScrollHTMLElement) {
    this.wrapper = wrapper
    // Omit part of the code
    this.applyPlugins()
  }
}
Copy the code

By reading BScrollConstructor source code, we find that init method is called inside BScrollConstructor constructor to initialize, and applyPlugins method is further called inside init method to apply the registered plug-in:

// packages/core/src/BScroll.ts
export class BScrollConstructor<O = {} >extends EventEmitter {  
  private applyPlugins() {
    const options = this.options
    BScrollConstructor.plugins
      .sort((a, b) = > {
        const applyOrderMap = {
          [ApplyOrder.Pre]: -1,
          [ApplyOrder.Post]: 1,}const aOrder = a.applyOrder ? applyOrderMap[a.applyOrder] : 0
        const bOrder = b.applyOrder ? applyOrderMap[b.applyOrder] : 0
        return aOrder - bOrder
      })
      .forEach((item: PluginItem) = > {
        const ctor = item.ctor
				// When the specified plug-in is enabled and the type of the plug-in constructor is a function, create the corresponding plug-in
        if (options[item.name] && typeof ctor === 'function') {
          this.plugins[item.name] = new ctor(this)}})}}Copy the code

Inside the applyPlugins method, the plugins are sorted according to the order in which the plugins are set. Then, the plugins are created by calling the plugin constructor with the BS instance as an argument, and the plug-in instance is saved to the plugins ({}) property inside the BS instance.

Now that we’ve covered plug-in management and plug-in connectivity, let’s move on to the final key point — plug-in communication.

4.3 Plug-in Communication

Plug-in communication refers to the communication between plug-ins. Although plug-ins are completely decoupled during the design, in the actual business operation process, it is inevitable that a certain business process requires the cooperation of multiple plug-ins, which requires the communication between the two plug-ins. Since there is no direct connection between plug-ins, communication must go through the core system, so the core system needs to provide a plug-in communication mechanism.

This situation is similar to the computer, the computer CPU, hard disk, memory, network card is independently designed configuration, but the computer running process, CPU and memory, memory and hard disk must have communication, the computer provides the communication function between these components through the bus on the motherboard.

Also, for systems with plug-in architectures, it is common for the core system to provide a plug-in communication mechanism in the form of an event bus. When it comes to the event bus, it may seem strange to some of you. But it should be easy to understand if you use a publish-subscribe model. I’m not going to expand on the publish/subscribe model here, but I’ll just review it with a picture.

At the heart of BetterScroll is the BScrollConstructor class, which inherits from the EventEmitter:

// packages/core/src/BScroll.ts
export class BScrollConstructor<O = {} >extends EventEmitter {  
  constructor(el: ElementParam, options? : Options & O) {
    this.hooks = new EventEmitter([
      'refresh'.'enable'.'disable'.'destroy'.'beforeInitialScrollTo'.'contentChanged',]),this.init(wrapper)
  }
}
Copy the code

The EventEmitter class is provided internally by BetterScroll, and an instance of it will provide event bus functionality externally. The UML class diagram for the EventEmitter class looks like this:

This brings us to the first question left: “So why inject bs instances?” . Because a BS (BScrollConstructor) instance is also an event dispatcher by nature, the BS instance is injected when a plug-in is created so that plug-ins can communicate through a unified event dispatcher.

Now that we know the answer to the first question, let’s look at the second question: “How to use bs examples? “. To answer this question, let’s continue with the PullUp plug-in and look at how it uses BS instances internally for message communication.

export default class PullUp implements PluginAPI {
  static pluginName = 'pullUpLoad'
  constructor(public scroll: BScroll) {
    this.init()
  }
}
Copy the code

In the PullUp constructor, the BS instance will be stored in the PullUp instance’s internal Scroll property, and then the PullUp plug-in will be able to communicate with the injected BS instance within the PullUp plug-in. For example, if the PullUp is less than the threshold, the pullingUp event is triggered:

private checkPullUp(pos: { x: number; y: number }) {
  const { threshold } = this.options
  if(...). {this.pulling = true
      // Omit part of the code
      this.scroll.trigger(PULL_UP_HOOKS_NAME) // 'pullingUp'}}Copy the code

Now that we know how to use the BS instance to dispatch events, let’s look at how it can be used inside the plug-in to listen for events of interest to the plug-in.

// packages/pull-up/src/index.ts
export default class PullUp implements PluginAPI {
  static pluginName = 'pullUpLoad'
  constructor(public scroll: BScroll) {
    this.init()
  }

  private init() {
    this.handleBScroll()
    this.handleOptions(this.scroll.options.pullUpLoad)
    this.handleHooks()
    this.watch()
  }
}
Copy the code

Within the PullUp constructor, the init method is called to initialize the plug-in. Within the init method, different methods are called to perform different initialization operations. Related to this event are the handleHooks method.

private handleHooks() {
  this.hooksFn = []
  // Omit part of the code
  this.registerHooks(
    this.scroll.hooks,
    this.scroll.hooks.eventTypes.contentChanged,
    () = > {
      this.finishPullUp()
    }
  )
}
Copy the code

Obviously, inside the handleHooks method, there are further calls to the registerHooks method to registerHooks:

private registerHooks(hooks: EventEmitter, name: string, handler: Function) {
  hooks.on(name, handler, this)
  this.hooksFn.push([hooks, name, handler])
}
Copy the code

By looking at the signature of the registerHooks method, you can see that it supports three arguments, the first being an EventEmitter object, and the other two representing the event name and event handler, respectively. Inside the registerHooks method, it simply listens to the specified event via the hooks object.

So when are the this.scroll. Hooks objects created? We find the answer in the BScrollConstructor constructor.

// packages/core/src/BScroll.ts
export class BScrollConstructor<O = {} >extends EventEmitter {
  constructor(el: ElementParam, options? : Options & O) {
    // Omit part of the code
    this.hooks = new EventEmitter([
      'refresh'.'enable'.'disable'.'destroy'.'beforeInitialScrollTo'.'contentChanged',])}}Copy the code

Obviously this. Hooks are also an EventEmitter object, so you can use them for event handling. Ok, so much for plug-in communication, let’s summarize this part with a picture:

After introducing the implementation of the Plug-in architecture for BetterScroll, let’s finish with a brief talk about the engineering aspects of the BetterScroll project.

V. Engineering

On the engineering side, BetterScroll uses some common solutions in the industry:

  • Lerna: Lerna is a management tool for managing JavaScript projects that contain multiple packages.
  • “Prettier” : “prettier” in Chinese means “pretty” and is a popular tool for code formatting.
  • Tslint: TSLint is an extensible static analysis tool for checking TypeScript code for readability, maintainability, and functional errors.
  • Commitizen & CZ-Conventional – Changelog: Used to help us generate a compliant Commit message.
  • Husky: Husky prevents noncanonical code from being committed, pushed, merged, and so on.
  • Jest: JEST is a JavaScript testing framework maintained by Facebook.
  • Coveralls: To get coverage reports for Coveralls. IO and add a nice coverage button to the README file.
  • Vuepress: Vue driven static web site generator for producing documentation for BetterScroll 2.0.

Since the focus of this article is not on engineering, this is a brief list of open source libraries that BetterScroll uses for engineering. If you are interested in the BetterScroll project, you can look at the package.json file in the project and focus on the NPM scripts configuration in the project. Of course, there are still many things to learn from BetterScroll, and the rest is for you to discover. You are welcome to communicate and discuss with Po.

6. Reference Resources

  • BetterScroll 2.0 document
  • BetterScroll 2.0 released: BetterScroll, with you