Author: Chen Dafu

Graduate student of The Chinese University of Hong Kong, one of the authors of mobile Web Front-end Efficient Development Practice, one of the translators of Front-end Developer Guide 2017, delivered keynote speeches at China Front-end Developer Conference, Mesozoic Technology Conference and other technical conferences, focusing on the research and use of new technologies.

This article is original, please indicate the author and source

Recently, I have been reconstructing an SDK within the company. Here I summarize some experience and share it with you.

Type checking and intelligence hints

As an SDK, our goal is to reduce the amount of time the user has to look at the document, so we need to provide some type of checks and smart hints. We usually provide JsDoc. Most editors provide a quick way to generate JsDoc. Our more common vscode can use Document This.

The alternative is to use Flow or TypeScript. The main reason for choosing TypeScript is that the auto-generated JsDoc is relatively primitive and we still need to edit on it, so JsDoc maintenance is separate from code development, and JsDoc forgets to update when the code is updated.

TypeScript allows us to make fewer errors and reduce debugging time. On the other hand, the SDK is relatively simple to compress when it is delivered to ensure that the volume of the SDK is as large as possible. So you’ll want to compress JsDoc, whereas TypeScript can use separate D.ts files with declaration set to true in tsconfig.json.

An SDK with hints:

Finally, even if you don’t use TypeScript, it is strongly recommended that vscode provide the //@ts-check annotation. This annotation will check your code through some type derivation and reduce the number of bugs in development.

As a tip, if the library you are using does not provide intelligent hints, you can install @types/{pkgname} in NPM/yarn -d to enjoy the intelligent hints provided by vscode during development, and -d to devDependencies. It doesn’t increase the size of your code at build time.

interface

ES6 supports some of the most advanced syntaxes of the past. Here are a few common ones that JavaScript developers are not used to.

Many people start out with TypeScript obsessed with using any or the default any. It is recommended that strict and noImplicitAny in tsConfig be turned on during development to ensure that any is used as little as possible. Abusing any means that your type checking is useless.

For objects whose contents cannot be determined, use {[key: string]: Any} instead of using any directly, you can slowly extend the interface until you eliminate any completely. Also, TypeScript types support inheritance. During development, you can dismantle the interface and use composite inheritance to reduce duplication of definitions.

However, there is a small pain point in the interface. Currently vscode’s smart reminder does not correspond to the interface well. When you enter the corresponding variable, it will be highlighted, but only the interface with the name defined will be highlighted. There is no way to directly see what is defined in the interface. However, vscode will give you a hint of the full key when you enter the part of the key defined in the interface. While this is a little less friendly to the development process, the vscode development team says it was deliberately designed so that the API parameters can be used directly with essential (and important) parameters, and some configuration can be put into an object defined as an interface.

The enumeration

Have you used it in your code:

const Platform = {
    ios: 0.android: 1
}
Copy the code

You should use enumerations in TypeScript:

enum Platform {
  ios,
  android
}
Copy the code

In this way, enumerations can increase the maintainability of your code by using intelligent prompts to ensure that you type the correct number and that no magic number appears. It ensures that the type of input (the object you define may one day no longer have a value of type number) requires no additional type judgment over the object.

A decorator

Decorators are both familiar and unfamiliar to many developers. In redux and Mobx, it is common to call decorators in code, but most developers are not in the habit of pulling their code logic into decorators.

For example, in the development of this SDK, we need to provide some facade to be compatible with different platforms (iOS, Android or Web), and this facade will be registered by the developer in the form of plug-ins, and the SDK will maintain an injected object. The normal way to use a function is to use the environment and then determine whether the object has the desired plug-in, if so, use the plug-in.

In practice, a plugin is an interceptor, and we only need to prevent the actual function from running. The logic goes something like this:

export function facade(env: number) {
  return function(
    target: object,
    name: string,
    descriptor: TypedPropertyDescriptor<any>
  ) {
    let originalMethod = descriptor.value;
    let method;

    return{... descriptor, value(... args:any[]) :any {
        let [arg] = args;
        let { param, success, failure, polyfill } = arg;   // This section can be customized
        if ((method = polyfill[env])) {
          method.use(param, success, failure);
          return;
        }
        originalMethod.apply(this, args); }}; }; }Copy the code

Another common occurrence in SDK development is the checksum reencapsulation of many parameters, which can also be done using decorators:

export function snakeParam(
  target: object,
  name: string,
  descriptor: TypedPropertyDescriptor<any>
) {
  letcallback = descriptor.value! ;return{... descriptor, value(... args:any[]) :any {
      let [arg, ...other] = args;
      arg = convertObjectName(arg, ConvertNameMode.toSnake);
      callback.apply(this, [arg, ...other]); }}; } presentCopy the code

A generic form

The simplest example is that generics can determine the output based on the user’s input

function identity<T> (arg: T) :T {
    return arg;
}
Copy the code

It doesn’t mean anything, of course, but it shows that the return is based on the type of the ARG, and in general development, you can’t get away from the stereotype is Promise or the TypedPropertyDescriptor that’s built in, where you need type input, so don’t just use any, If your back end returns a standard structure like this:

export interface IRes {
  status: number;
  message: string; data? : object; }Copy the code

So you can use Promise like this:

function example() :Promise<IRes> {
    return new Promise. }Copy the code

Of course, there are many advanced applications of generics, such as generics constraints and generic-creation factory functions, that are beyond the scope of this article and can be found in the official documentation.

build

If your build tool is Webpack, in SDK development, try to use node mode call (namely webpack.run execution), because SDK build often deal with many different parameter changes, Node mode can be more flexible than pure configuration mode to adjust the input and output parameters. You can also consider using rollup, whose build code is more programmatically oriented.

Note that building in Webpack3 and Rollup can be built using ES6 modularity, so that the business code introduced into your SDK can be deconstructed to reduce the volume of the final business code. If you only provide commonJS packages, The tree Sharking of the build tool will not work. If you use Babel, close the compilation of the Module.

Another way to reduce the size of a single package is to use Lerna to build multiple NPM packages in a Git repository. It is more convenient to use the common part of the code than to tear the repository apart. However, it is important to take care that changes to the common part of the code do not affect other packages.

In fact, for most OF the SDK, Webpack3 and rollup feel similar, the more common plug-ins have almost the same name. Rollup receives inputOptions to generate bundles. It can also generate sourcemap and write to generate outputs. We can do some careful work in this process.

Rollup returns a promise, which means we can use async to write build code, while webpack.run is still a callback function. Although developers can encapsulate promises, I think rollup is a bit more fun.

Unit testing

Last week, my colleague did an online sharing, and I found that many students were very interested in and confused about single test. In front-end development, it is difficult to develop single test for business code involving UI, but for SDK, unit test is definitely a necessary and sufficient condition for approval. Of course, I also don’t like to write single test, because single test is often boring, but not to write single test will certainly be the old drivers “education” ~_~.

The general single test uses Mocha as the testing framework, Expect as the assertion library, and NYC as the single test report. A rough single test is as follows:

describe('xxx api test'.function() {			// Note that if you want to call mocha with this, do not use arrow functions
  this.timeout(6000);
  it('xxx'.done= > {
    SDK.file
      .chooseImage({
        count: 10,
        cancel: (a)= > {
          console.log('Select image to cancel ----');
        }
      })
      .then(res= > {
        console.dir(res);
        expect(res).to.be.an('object');
        expect(res).to.have.keys('ids');
        expect(res.ids).to.be.an('array');
        expect(res.ids).to.have.length.above(0);
        uploadImg(res.ids);
        done();
      });
  });
});
Copy the code

You can also use TypeScript to write single tests, which don’t need to be compiled. You can register TS-Node with Mocha and execute them directly. Write tests for TypeScript projects with Mocha and chai — in TypeScript! . However, there is a point to remind you that when writing single tests, try to rely on documentation and not intelligent prompts. Because your code is wrong, your intelligent prompts may also be wrong, and your single tests based on the wrong intelligent prompts will certainly be the same.

The nock library can be used to simulate network requests, adding a beforeEach method before IT:

describe('proxy', () => {
  beforeEach((a)= > {
    nock('http://test.com')
      .post('/test1')
      .delay(200)
      .reply(200, {			// body
        test1: 1.test2: 2
      }, {
        'server-id': 'test' // header
      });
  });
  it(...
}
Copy the code

Finally we use an NPM script with NYC in front of Mocha to get our single test report.

I’ve also included a few tips on how to use TypeScript.

Tips: How to add declarations to internal libraries without sending packages

The SDK relies on an internal NPM package during development. To support TypeScript calls to NPM, we do several things:

  • Add the d.ts file to the original package and publish it.

  • @types/@scope/{pkgname} is not supported by NPM. For private packages, @types/scope_{pkgname} can be used.

  • This method is suitable for development. If you think your D.ts is not perfect, or the D.ts file is only needed by this SDK, you can use it like this and modify it in tsconfig.json:

    "baseUrl": "./",
    "paths": {
        "*": ["/type/*"]
    }
    Copy the code

Tips: How do you handle the different types of promise callbacks in resolve and Reject

The default reject returns a reject argument of type any, which may or may not satisfy our requirements. Here is a solution that is not optimal:

interfaceIPromise<T, U> { then<TResult1 = T, TResult2 = never>( onfulfilled? : |((value: T) => TResult1 | PromiseLike<TResult1>)
      | undefined
      | null.onrejected? : | ((reason: U) => TResult2 | PromiseLike<TResult2>)
      | undefined
      | null) :IPromise<TResult1 , TResult2>;
  catch<TResult = never> (onrejected? : | ((reason: U) => TResult | PromiseLike<TResult>)
      | undefined
      | null
  ) :Promise<TResult>;
Copy the code


In 2019, iKcamp’s original new book Koa and Node.js Development Actual Combat has been sold on JD.com, Tmall, Amazon and Dangdang!