The paper

  • In fact, packaging a JS library is not as difficult as imagined, common time formatting, send a NPM warehouse, make a CDN, can be introduced to normal use.
  • With the complexity of the appeal, it is often not as simple as we imagined. Even if the function is fully encapsulated in the code, there will still be scenarios that the real business cannot meet.

The real scene

  • For example, the company business wants to encapsulate the universal AXIos request library for h5, mobile, PC, and client. Specific to each scenario, the problem occurs when the client wants to write to the local log before and after the request, and the other end does not process it.
  • Without the use of universal packaging, common capabilities and do not want to rewrite multiple places, then the maintainability, versatility can not be guaranteed.
  • The use of encapsulated library capabilities is not satisfied, dilemma!

The core problem

  • If you want to use both the common capabilities and the extended capabilities of the library, what ideas might match this scenario?

Implementation approach

  • Decorator mode
  • Plug-in design Scheme

Implementation details

Decorator mode

  • Conceptual definition: Allows you to add new functionality to an existing object without changing its structure.
  • Implementation one: TS syntax sugar official document most beautiful official document
Class decorator when @sealed is executed, it will seal the constructor and prototype of this class
@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting; }}function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}
Copy the code
When the @Enumerable (false) decorator is called, it modifies the Enumerable property of the property descriptor.
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }

    @enumerable(false)
    greet() {
        return "Hello, " + this.greeting; }}function enumerable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.enumerable = value;
    };
}
Copy the code
// An example of an accessor decorator (@ signals) that works with members of the Point class:
class Point {
    private _x: number;
    private _y: number;
    constructor(x: number, y: number) {
        this._x = x;
        this._y = y;
    }

    @configurable(false)
    get x() { return this._x; }

    @configurable(false)
    get y() { return this._y; }}function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    };
}
Copy the code
// Attribute decorator When @format("Hello, %s") is called, it adds a piece of metadata to the attribute, via the reflect.metadata function in the reflect-metadata library. When getFormat is called, it reads metadata for the format.
class Greeter {
    @format("Hello, %s")
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        let formatString = getFormat(this."greeting");
        return formatString.replace("%s".this.greeting); }}import "reflect-metadata";

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
    return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
Copy the code
// The parameter decorator @required adds a metadata entity to mark the parameter as required. The @validate decorator wraps the greet method in a function that validates its arguments before calling the original one.
class Greeter {
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }

    @validate
    greet(@required name: string) {
        return "Hello " + name + "," + this.greeting; }}import "reflect-metadata";

const requiredMetadataKey = Symbol("required");

function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
    let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
    existingRequiredParameters.push(parameterIndex);
    Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}

function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
    let method = descriptor.value;
    descriptor.value = function () {
        let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
        if (requiredParameters) {
            for (let parameterIndex of requiredParameters) {
                if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
                    throw new Error("Missing required argument."); }}}return method.apply(this.arguments); }}Copy the code
  • The ES5 implementation inserts new properties and methods using an object to extend loop
  • ES7 syntax sugar usage documentation comes with decorations

Plug-in package

  • Well-known cases webpack plug-in, umiJS plug-in documentation
  • The core library is based on Tapable, god’s source code to interpret the nuggets address

The problem back

  • The ability to encapsulate functionality also allows business users to extend functionality well
  • Implement a common request library with plug-ins
/* * @description: * @version: 1.0.0 * @author: Wu Wenzhou * @Date: 2021-03-31 19:38:15 * @Lasteditors: Wu Wenzhou * @Lastedittime: The 2021-03-31 19:41:59 * /
import { SyncHook } from 'tapable'
import axios from 'axios'
/** * Initialize hooks *@param Options Initialization parameters */
const initHooks = (options: any) = > {
  const hooks = {
    request: new SyncHook(['config'.'error']),
    response: new SyncHook(['response'.'error']),}if (Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      if (typeof plugin === 'function') {
        plugin.call(hooks)
      } else {
        plugin.apply(hooks)
      }
    }
  }
  return hooks
}
/** * encapsulates the request method *@param Options Initialization parameters */
export const request = (options: any) = > {
  const hooks = initHooks(options)
  const http = axios.create()
  http.interceptors.request.use(
    (config) = > {
      hooks.request.call(config, ' ')
      return config
    },
    (error) = > {
      hooks.request.call(' ', error)
      return Promise.reject(error)
    },
  )
  http.interceptors.response.use(
    (response) = > {
      hooks.response.call(response, ' ')
      return response
    },
    (error) = > {
      hooks.response.call(' ', error)
      return Promise.reject(error)
    },
  )
  const request = (args: any) = > {
    return new Promise((resolve, reject) = > {
      http
        .get('http://localhost:3000/xx', {// Test the code
          // params: params ? params : "",
        })
        .then((res) = > {
          try {
            const data = res.hasOwnProperty('data')? res.data : {} resolve(data) }catch (error) {
            resolve(error)
          }
        })
        .catch((err) = > {
          reject(err)
        })
    })
  }
  return request
}
Copy the code
class Test {
    constructor() {}
    apply(hooks) {
      hooks.request.tap('request'.(config, error) = > {
        if (error) {
          console.log(error)
        } else {
          console.log('Test request normal ')
        }
      })
      hooks.response.tap('response'.(response, error) = > {
        if (error) {
          console.log(error)
        } else {
          console.log(response)
        }
      })
    }
  }
  class Test1 {
    constructor() {}
    apply(hooks) {
      hooks.request.tap('request'.(config, error) = > {
        if (error) {
          console.log(error)
        } else {
          console.log('Test1 requested normal ')
        }
      })
      hooks.response.tap('response'.(response, error) = > {
        if (error) {
          console.log(error)
        } else {
          console.log(response)
        }
      })
    }
  }
  //插件Test,Test1
  var options = {
    plugins: [new Test(), new Test1()],
  }
  // instantiate the object
  const r = index.request(options)
  // Call request, plug-in extension capabilities
  r()
    .then((data) = > {
      console.log(data)
    })
    .catch((err) = > {
      console.log(err)
    })
Copy the code

conclusion

  • How does a JS library make people comfortable to use, two key points non-intrusive oriented slicing, extensible to provide additional capabilities
  • Both the decorator pattern and the plug-in approach are common practices in base library development