Type checking and intelligence hints

As an SDK, our goal is to reduce the amount of time users spend looking at documents, so we need to provide some type of checks and intelligent hints. In general, we provide JsDoc. Most editors can provide a quick way to generate JsDoc

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.

In addition to the development process we will be able to enjoy the type checking is important for the SDK development characteristics, such as TypeScript allows us to reduce mistakes, reduce debugging time, on the other hand the development of the SDK in providing out will be a relatively simple compression, ensure that after the introduction of volume, so will want compressed JsDoc, TypeScript uses separate D.ts files with declaration set to true in tsconfig.json.

An SDK with hints:

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; 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> ) { let callback = descriptor.value! ; return { ... descriptor, value(... args: any[]): any { let [arg, ...other] = args; arg = convertObjectName(arg, ConvertNameMode.toSnake); callback.apply(this, [arg, ...other]); }}; }Copy 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

In front end development, it is difficult to develop single tests for business code involving the UI, but for SDKS, unit tests are definitely a necessary and sufficient condition for approval. Of course, I also do not like to write single test, because single test is often more boring, but do not 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 the arrow function this.timeout(6000); It (' XXX ', done => {sdk.file.chooseImage ({count: 10, cancel: () => {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! . But one thing to remind you of is that when you write a single test, try to rely on documentation and not intelligence prompts, because if your code is wrong, your intelligence prompts will also be wrong, and so will any single test you write based on the wrong intelligence prompts.

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

describe('proxy', () => {
 beforeEach(() => {
  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 time, a folder is used to store the corresponding D. ts files, which 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:

interface IPromise<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