Author: Ji Zhi

Back in the v1 era

It has been more than three years since The release of BetterScroll V1, which is well received by the community due to its excellent scrolling experience and performance on mobile devices and support for multiple scrolling scenarios. Users can also abstract various complex business scroll components based on BetterScroll. Based on BetterScroll, we also open source the Mobile component library Cube-UI based on Vue2.0.

BetterScroll now has more than 11,000 stars, and GitHub has about 32,000 warehouses using it. Didi’s internal businesses, such as domestic and foreign core businesses, use BetterScroll heavily, and it has withstood the test of various business scenarios.

With the use of a large number of business scenarios and feedback and suggestions from the community, v1 also exposed some problems, which are mainly divided into the following four aspects:

  • Packages are too large to be referenced on demand
  • Expansion is difficult and enhancement is easy to invade core logic
  • Lack of testing, poor stability assurance
  • Documentation is not friendly enough and community q&A costs are high

V2 will be

BetterScroll V2: BetterScroll V2

BetterScroll V2 provides a number of add-ons in addition to core scrolling:

  • Picker highly mimics the native picker component of iOS
  • Mouse-wheel Compatible with PC mouse wheel scenarios
  • Observe-dom automatically detects the DOM change in the Scroll area and calls the refresh method
  • Pulldown listens for pull operations
  • Pullup listens for pull-up action
  • Scrollbar is a beautiful scrollbar that mimics native browsers
  • Slide implements the interactive effect of rotation graphics
  • Zoom provides the zoom capability
  • Nested-scroll coordinates double-layer nested scroll behavior
  • Infinity scrolling list (mostly for large data rendering, otherwise coreScroll will suffice)

The birth of V2 version is to solve the problems exposed by V1. Here, we will reveal the thinking and practice in the process of reconstruction respectively from the above four problems.

1. Large package size

The architecture design of V1 is modeled after the code organization of Vue 2.0, but the different features (picker, Slide, Scrollbar, etc.) are written together with the core scroll, so they cannot be introduced on demand.

Note: On-demand import here refers to the fact that the user may only need to implement a simple list scrolling effect, but is forced to load redundant code, such as all the Feature code, resulting in the package size problem.

In order to solve this problem, we must find a reasonable way to separate and reference each Feature code separately, and the answer is plug-in scheme. One of the key points of v2 was how to design the plug-in mechanism, and we thought about it in three steps:

  1. Core function abstraction, from CoreScroll (CoreScroll) from the top down to split a number of single-function classes, and then combine them together to build a complete core logic;

Due to the breakdown into fine-grained function classes, we have a unified event bubbling layer and property proxy layer to delegate events or properties of internal classes to CoreScroll, considering that old users are manipulating CoreScroll by listening for events or acquiring properties.

  1. Drawing on the extensibility of WebPack Tapable’s hooks concept (which are less powerful than Tapable), hooks(enhancements to EventEmitter’s classic subscriber pattern) handle the hook logic in the process between functional classes.

  2. Learn from the Vue 2.x plug-in registration mechanism (code below) to reduce the mental burden of old users.

    import BScroll from '@better-scroll/core'
    import Slide from '@better-scroll/slide'
    
    // Just register the plugin, no extra mental burden
    BScroll.use(Slide)
    
    let bs = new BScroll('.wrapper', {
      slide: { /* Plug-in configuration item */}})Copy the code

Therefore, the overall prototype of V2 has been established. Considering that there will be many plug-ins in the future to achieve different requirements of business scenarios, Lerna is used to manage multiple packages in V2 version, and @Better-Scroll is used as the package naming prefix, so that users can have better identification. With TypeScript static typing and the mature and rich ecosystem of the community, BetterScroll already has many features and will continue to grow, making it ideal for developing in TypeScript.

TIPS:

Lerna’s failure to issue packages has always been a topic that developers (including authors) cannot get around. At present, there are also many issues and blogs discussing this issue for your reference: solutions after Lerna’s failure to publish, Lerna Issue 1894, publish failure

2. Difficulty in scaling

In v1 version, when features were added, some logic codes were mixed with core rolling codes, resulting in a slow decrease in the later extension maintainability, resulting in an unlimited increase in package volume. Therefore, if Feature is completely separated from core rolling CoreScroll part and Feature is made into plug-in mode, the problem of package volume can be solved, expansion becomes relatively easy, and the stability of iteration becomes better.

In version V2, the general implementation of a plug-in is as follows:

class InfinityScroll {
  static pluginName = 'infinity'
  constructor(public bscroll: BScroll) {
    / /... your own logic}}// Assuming InfinityScroll has been registered
new BScroll('.wrapper', {
  infinity: { /* Plug-in configuration item */ }
  // Infinity should correspond to the pluginName
})
Copy the code

The plugin must have a static attribute pluginName. The value of this attribute must correspond to the key of the configuration object passed in to initialize BetterScroll. Otherwise, the plugin cannot be found internally. This solution takes into account the cost of using it while minimizing the differences with v1.

After the core plug-in mechanism is implemented, the whole ecosystem of BetterScroll is enriched by plug-ins for various features.

3. Lack of testing

In version v1, the test coverage was less than 40%, probably because BetterScroll was a huge class before, and writing unit tests became increasingly difficult, which could lead to poor stability during later iterations.

In v2 version, in order to ensure the stability of the overall function and control the quality of the release, we not only added unit testing, but also introduced additional functional testing to further guarantee.

  1. Unit testing

    The single test of Cube-UI I participated in before adopted karma + Mocha scheme, but it required installing various plug-ins and doing a lot of configuration. It has been 202 years, and the final comparison found that Jest is suitable for the existing BetterScroll scenario. It integrates Mock, Test Runner, Snapshot, and other powerful features, and basically meets the needs out of the box.

    At best, the powerful manual-Mocks capabilities are used in writing unit tests.

    A simple scenario illustrates how we think about unit testing and how to solve problems with Jest Manual-Mocks.

    Suppose our source file structure is as follows:

    - src
      - Core.ts
      - Helper.ts
    Copy the code

    The code for Core and Helper is as follows:

    // Core. Ts code is as follows
    
    export default class Core {
      constructor (helper: Helper) {
        this.helper = helper
      }
    
      getHammer (type: string) {
        if (this.helper.isHammer(type)) {
          return ('Got hammer')}else {
          return ('No hammer is available')}}}// helper. ts code is as follows
    
    export default class Helper {
      isHammer (type: string) {
        return type= = ='hammer'}}Copy the code

    Now that we’re ready to test the Core#getHammer function, we hear two different voices among our core developers.

    Solution 1: Import the original Helper code (SRC/helper.ts) and let it go through the whole process.

    Scheme 2: Unit tests should use functions or classes as a minimum of granularity. The approach tends to be the traditional testing industry concept that Helper should be mock out (using SRC /__mocks__/ helper.ts), in other words, Helper as another unit of testing, It must ensure that its functionality is completely correct, but primitive helpers should not be introduced for single tests of Core.ts.

    Finally, in the end, we chose the more rigorous plan two.

    With the power of Jest Manual-Mocks, writing tests becomes more enjoyable and unambiguous.

    1. Changing file structure

      src
      + __mocks__
      + Helper.ts
      + __tests__
      + Core.spec.ts
        Core.ts
        Helper.ts
      Copy the code

      Added the __mocks__ and __mocks__/ helper. ts files, and added the __tests__ and core.spec.ts test directories.

    2. Improve the manual – away

      // __mocks__/Helper.ts
      const Helper = jest.fn().mockImplementation((a)= > {
        return {
          isHammer: jest.fn().mockImplementation((type) = > {
            return type= = ='MockedHammer'})}})export default Helper
      Copy the code
    3. Write the Core. The spec. Ts

      import Helper from '.. /Helper.ts'
      import Core from '.. /Core.ts'
      / / use '__mocks__ / Helper. Ts'
      // The introduced Helper is the mock ~
      jest.mock('.. /Helper.ts')
      
      describe('Core tests'.(a)= > {
        it('should work well with "MockedHammer"'.(a)= > {
      
          const core = new Core(new Helper() // Mock Helper)
      
          expect(core.getHammer('MockedHammer')).toBe('Got hammer') / / by})})Copy the code

      As can be seen from the above, we used Jest to change the helper. ts export to __mocks__ instead of the original helper. ts, so that each module needs to guarantee its own logical correctness, and at the same time, it will be easier to test the logic of abnormal branches.

      Interesting, right?

  2. A functional test

    Because BetterScroll is a browser-specific scroll library, unit testing is used to ensure that the input and output of a single module are correct, so we need additional tools to ensure that the core scroll and plug-ins behave as expected, so we used jest puppeteer. The idea is to Run your tests using Jest & Puppeteer, which is worth mentioning here.

    Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol

    My work site English translation is:

    Puppeteer is a Node library that controls Chrome behavior through the DevTools protocol and provides a more elegant API.

    The DevTools protocol is an important one and will be covered later.

    If you go to its website, you’ll find that it does a lot of things, including PDF generation, forms, UI testing, Google plugins testing, and there are many articles on how to use it for crawlers.

    Here is a snippet of core scroll functional test code:

    describe('CoreScroll/vertical'.(a)= > {
      beforeAll(async() = > {await page.goto('http://0.0.0.0:8932/#/core/default')
      })
    
      it('should render corrent DOM'.async() = > {const wrapper = await page.$('.scroll-wrapper')
        const content = await page.$('.scroll-content')
    
        expect(wrapper).toBeTruthy()
        await expect(content).toBeTruthy()
      })
    
      it('should trigger eventListener when click wrapper DOM'.async() = > {let mockHandler = jest.fn()
        page.once('dialog'.async dialog => {
          mockHandler()
          await dialog.dismiss()
        })
    
        // wait for router transition ends
        await page.waitFor(1000)
        await page.touchscreen.tap(100.100)
    
        await expect(mockHandler).toHaveBeenCalled()
      })
    })
    Copy the code

    As you can see from the example code above, Puppeteer’s apis are very semantic and all internal apis return promises.

    It was fun as we gradually enriched the functional testing, but the problems still popped up.

    BetterScroll is strongly associated with Touch, Mouse, MouseWheel events, but Puppeteer(V1.17.0) does not provide all interfaces.

    Since Puppeteer is a class library that controls Chrome by protocol, why not take a quick look at its internal implementation?

    With this in mind, the core implementation of Puppeteer is explored, and the result is that there is only one thread that needs to be sorted out, and the rest is to follow the DevTools Protocol documentation.

    Below is a brief flow chart.

    Step 1: Start Chromium using node’s child_process module;

    Step 2: Listen to the output of the command line and obtain browserWSEndpoint, which is a URL to the WebSocket, so that the bidirectional push relationship between Puppeteer and Chromium is established.

    Step 3: instantiate Connection, establish a Session and instantiate the Browser class. Then the user operates on the Browser instance, such as opening a page TAB (browser.newPage()). In the instantiation of Connection, there are a lot of details. DevTools Protocol is an existing API document. In other words, as long as we press the API document to send messages to Chromium via WebSocket, Can drive it to respond to behavior.

    Then combined with the documentation and source code, we found that as long as send Input. SynthesizePinchGesture and Input. SynthesizeScrollGesture message (in this document), Can drive Chromium to make scroll, Zoom, mouseWheel and other event interaction effects, so BetterScroll plug-ins and core function testing is easy!

    Therefore, we have extended Puppeteer to extendTouch and extendMouseWheel for functional testing.

    Then the functional test writing task can be completed.

    The functional testing is over, but a new problem arises: run the functional testing, which relies on the examples code to launch the service, then use the Puppeteer to access the examples code service, and finally run all the test cases. This means that the service needs to be ready for functional testing before functional testing, so we need a more engineering approach to solve this problem!

    The key to this question is how to ensure that the examples code service starts and then runs functional tests, and if so, is it possible to start with webpack, especially webpackDevServer? Looking at its source code implementation, we found an internal reference to Webpack-dev-Middleware, which has an API called waitUntilValid that accepts a callback. This API ensures that the service is started and the bundle is accessible.

    The solution is to inject the webpack configuration in vue.config.js:

    module.exports.configureWebpack = {
      devServer: {
        before (app, server) {
          server.middleware.waitUntilValid((a)= > {
            // The service is ready to start e2E testing
            execa('npm'['run'.'test:e2e'] and {stdio: 'inherit'})})}}}Copy the code

So far, this is the test part of the exploration and practice, after this part, for ourselves, there is a biggest experience: the value of engineers is to explore and solve problems.

4. Documentation is not friendly

The v1 version of the documentation and sample code is a bit of a joke, especially the sample section is a great mental burden for new players, such as no actual code snippets in the documentation, the sample coupled with various irrelevant Vue logic. In V2, these problems will be improved.

First of all, as our technology stack is Vue, VuePress is a very useful document framework surrounding it, which gives full play to the capabilities of Vue, Webpack and Markdown, and can also customize themes and achieve internationalization. Moreover, its plug-in architecture design brings great flexibility and expansion ability to VuePress, so we chose VuePress to complete relevant API documentation. While VuePress meets most of our documentation requirements right out of the box, it still needs some additional extensions.

In order to realize the function of the above picture, we need to have the TWO-DIMENSIONAL code, the code snippet of the component, and we need to actually render the components in the examples directory in Markdown. The first and third points are particularly easy to implement, and VuePress provides this capability, but the second point, showing the code corresponding to the Examples component simultaneously in Markdown, is a tricky one.

Then, it is necessary to delve into the implementation of VuePress, which internally compiles files with the MD extension using markdown-it. To solve this problem, it seems that we need to take a deep look at the underlying implementation of Markdown-it, which also produced markdown-it source code and plug-in interpretation series; We found that vuepress-based plug-in mechanism could meet our requirements for customization, so we wrote the extract-code plug-in and agreed that the markdown file would be extract-code as long as it contained the following code.

/ / draw the default vue file content of the template tag < < < @ / examples/vue/components/infinity/default. Vue? Default template / / extraction. The vue script tags file < < < @ / examples/vue/components/infinity/default. The vue? scriptCopy the code

This way, every time we change the sample code under the Examples, the document is synchronized to the corresponding section.

Note: In order to speed up the compilation of markdown files, VuePress uses cache-loader internally for caching. This means that if the markdown contents have not changed, the cached contents are directly retrieved. Although the sample code has changed, for markdown files, The content is essentially unchanged.

TIPS: If you don’t like the subject of code blocks, take a look at the famous Prism, because the interior of VuePress is highlighted with this plug-in.

Summary plan

Looking back on the general process of making BetterScroll 2.0, there have been some ups and downs along the way, but there are more harvest, summary and precipitation.

Of course, all of this is the joint efforts of the students in the team, the core students: Ji Zhi, Feng Weiyao, Cui Jing, the community students YuLe’s many contributions, but also many students put forward good suggestions, thank you for your hard work, contribution, this is a process of learning from each other, grow together. I also want to thank Huang Yi, the original author of BetterScroll, for his trust.

BetterScroll 2.0 has undergone more than 20 alpha versions and has already been released in beta, but it is a stable version and has been widely downloaded and used internally and in the community. In the future, we will continue to do a number of things:

  1. Optimize code & package size
  2. Provide more plug-ins and rich examples (welcome PR or submit your ideas)
  3. Improve documentation and expose more details
  4. The test complete

In addition, a new version of BetterScroll component library will be produced based on BetterScroll 2.0 to improve the optimized and improved performance for services.

I hope that more and more people will use it, and more and more of you will join us to make BetterScroll Better.