简述
- 封装一个js库其实没有想象中的那么困难,常见时间格式化,发个npm仓库,搞个cdn,引入就能正常使用。
- 随着诉求的复杂性,往往就不是我们想象的那么简单了,就算代码上面把功能封装得很全面依然会存在真正业务上面不能满足的场景。
真实场景
- 例如公司业务上面希望封装通用axios的请求库,同时给h5端,移动端,pc端,客户端使用。具体到各个场景下面就会出现问题,在客户端请求前后希望写入本地日志,其他端不做处理。
- 不使用通用封装,公用的能力又不想多处重写,那可维护性,通用性上面就不能得到保障。
- 使用封装库能力又得不到满足,两难境地!
核心问题
- 既想使用公共的能力,又想库有扩展的能力,想一想有哪些思路可以匹配这样的场景?
实现思路
- 装饰器模式
- 插件设计方案
实现详细
装饰器模式
概念定义:允许向一个现有的对象添加新的功能,同时又不改变其结构。
实现一:ts语法糖最好看官方文档
代码语言:javascript复制//方法装饰器 当装饰器 @enumerable(false)被调用时,它会修改属性描述符的enumerable属性。
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;
};
}
代码语言:javascript复制// 访问器装饰器(@configurable)的例子,应用于Point类的成员上:
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;
};
}
代码语言:javascript复制// 属性装饰器当 @format("Hello, %s")被调用时,它添加一条这个属性的元数据,通过reflect-metadata库里的Reflect.metadata函数。 当 getFormat被调用时,它读取格式的元数据。
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);
}
代码语言:javascript复制//参数装饰器 @required装饰器添加了元数据实体把参数标记为必需的。 @validate装饰器把greet方法包裹在一个函数里在调用原先的函数前验证函数参数。
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中自带装饰的语法糖 使用文档
插件方案
- 知名案例webpack插件,umijs插件文档
- 核心库基于tapable,大神的源码解读掘金地址
问题回归
- 既想要封装功能的能力,也允许各个业务使用方去很好的扩展功能
- 实现一个公共请求库带插件的
/*
* @Description:
* @version: 1.0.0
* @Author: 吴文周
* @Date: 2021-03-31 19:38:15
* @LastEditors: 吴文周
* @LastEditTime: 2021-03-31 19:41:59
*/
import { SyncHook } from 'tapable'
import axios from 'axios'
/**
* 初始化hooks
* @param options 初始化参数
*/
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
}
/**
* 封装的请求方法
* @param options 初始化参数
*/
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', {//测试代码
// 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
}
代码语言:javascript复制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)
}
})
}
}
//插件Test,Test1
var options = {
plugins: [new Test(), new Test1()],
}
// 实例化对象
const r = index.request(options)
// 调用请求,插件扩展能力
r()
.then((data) => {
console.log(data)
})
.catch((err) => {
console.log(err)
})
总结
- 一个js库怎么让人使用的舒服,两个关键点无侵入面向切片,可扩展提供额外的能力
- 装饰器模式和插件的方式都是基础库开发过程中最常见的实践