简述
- 封装一个js库其实没有想象中的那么困难,常见时间格式化,发个npm仓库,搞个cdn,引入就能正常使用。
- 随着诉求的复杂性,往往就不是我们想象的那么简单了,就算代码上面把功能封装得很全面依然会存在真正业务上面不能满足的场景。
真实场景
- 例如公司业务上面希望封装通用axios的请求库,同时给h5端,移动端,pc端,客户端使用。具体到各个场景下面就会出现问题,在客户端请求前后希望写入本地日志,其他端不做处理。
- 不使用通用封装,公用的能力又不想多处重写,那可维护性,通用性上面就不能得到保障。
- 使用封装库能力又得不到满足,两难境地!
核心问题
- 既想使用公共的能力,又想库有扩展的能力,想一想有哪些思路可以匹配这样的场景?
实现思路
实现详细
装饰器模式
- 概念定义:允许向一个现有的对象添加新的功能,同时又不改变其结构。
- 实现一:ts语法糖官方文档最好看官方文档
@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);
}
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;
};
}
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;
};
}
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);
}
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);
}
}
- ES5实现使用对象对扩展循环的方式,插入新的属性和方法
- ES7中自带装饰的语法糖 使用文档
插件方案
问题回归
- 既想要封装功能的能力,也允许各个业务使用方去很好的扩展功能
- 实现一个公共请求库带插件的
import { SyncHook } from 'tapable'
import axios from 'axios'
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
}
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', {
})
.then((res) => {
try {
const data = res.hasOwnProperty('data') ? res.data : {}
resolve(data)
} catch (error) {
resolve(error)
}
})
.catch((err) => {
reject(err)
})
})
}
return request
}
class Test {
constructor() {}
apply(hooks) {
hooks.request.tap('request', (config, error) => {
if (error) {
console.log(error)
} else {
console.log('Test请求正常的')
}
})
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请求正常的')
}
})
hooks.response.tap('response', (response, error) => {
if (error) {
console.log(error)
} else {
console.log(response)
}
})
}
}
var options = {
plugins: [new Test(), new Test1()],
}
const r = index.request(options)
r()
.then((data) => {
console.log(data)
})
.catch((err) => {
console.log(err)
})
总结
- 一个js库怎么让人使用的舒服,两个关键点无侵入面向切片,可扩展提供额外的能力
- 装饰器模式和插件的方式都是基础库开发过程中最常见的实践