最普通的一个

最普通的一个 查看完整档案

上海编辑沈阳大学  |  通信工程 编辑小厂  |  前端开发 编辑填写个人主网站
编辑

永远不要做你擅长的事。

个人动态

最普通的一个 发布了文章 · 2020-11-29

lodash源码之_.property

功能

创建一个返回给定对象的 path 的值的函数。

官网给的demo。

var objects = [
   { 'a': { 'b': 2 } },
  { 'a': { 'b': 1 } }
];
_.map(objects, _.property('a.b'));

// => [2,1]

由此可以推断,_.property('a.b')返回的是一个取出对象的a>b属性的方法。

源码

function property(path) {
  return isKey(path) ? baseProperty(toKey(path)) : basePropertyDeep(path);
}

isKey用来判断是一个property name还是property path

property path,lodash中定义了一个属性路径的概念,用来快速访问对象属性。{a: {b: ['data']}},希望访问到data这个值,可以通过 'a.b[0]'来访问。

isKey

import isArray from './isArray.js';
import isSymbol from './isSymbol.js';
/** Used to match property names within property paths. */
var reIsDeepProp = /\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,
    reIsPlainProp = /^\w*$/;
function isKey(value, object) {
  if (isArray(value)) {
    return false;
  }
  var type = typeof value;
  if (type == 'number' || type == 'symbol' || type == 'boolean' ||
      value == null || isSymbol(value)) {
    return true;
  }
  return reIsPlainProp.test(value) || !reIsDeepProp.test(value) ||
    (object != null && value in Object(object));
}

逻辑很简单。

toKey

function toKey(value) {
  if (typeof value == 'string' || isSymbol(value)) {
    return value;
  }
  var result = (value + '');
  return (result == '0' && (1 / value) == -INFINITY) ? '-0' : result;
}

要注意的是Symbol类型提前返回。其它类型会转换成字符串。

result == '0' && (1 / value) == -INFINITY

这里用来区分0或者-0

baseProperty

function baseProperty(key) {
  return function(object) {
    return object == null ? undefined : object[key];
  };
}

该方法就更简单了,对于没有path property参数的,就执行该方法。 只是区传入的object的制定属性的值。

basePropertyDeep

function basePropertyDeep(path) {
  return function(object) {
    return baseGet(object, path);
  };
}

可以去翻baseGet的源码了。

function baseGet(object, path) {
  path = castPath(path, object);

  var index = 0,
      length = path.length;

  while (object != null && index < length) {
    object = object[toKey(path[index++])];
  }
  return (index && index == length) ? object : undefined;
}

简单过一遍这个方法。就能确定path返回的是property path的路径数组。

 while (object != null && index < length) {
    object = object[toKey(path[index++])];
  }

通过上边的while循环一层层拿到目标值。

查看原文

赞 0 收藏 0 评论 0

最普通的一个 发布了文章 · 2020-11-26

lodash源码之_.fill方法

功能

从start开始使用value填充数组到end为止。

 
  _.fill([4, 6, 8, 10], '*', 1, 3);
 // => [4, '*', '*', 10]

fill源码

function fill(array, value, start, end) {
  var length = array == null ? 0 : array.length;
  if (!length) {
    return [];
  }
  if (start && typeof start != 'number' && isIterateeCall(array, value, start)) {
    start = 0;
    end = length;
  }
  return baseFill(array, value, start, end);
}
  • array参数如果不存在,就直接返回[]
  • start参数存在,不是number类型的话,(这里还有个isIterateeCall判断,后边再统一分析该方法。)设置start为0,end为数组的长度,就是填充整个数组
  • 经过对参数的处理之后,再去调用baseFill这个核心逻辑

baseFill

function baseFill(array, value, start, end) {
  var length = array.length;

  start = toInteger(start);
  if (start < 0) {
    start = -start > length ? 0 : (length + start);
  }
  end = (end === undefined || end > length) ? length : toInteger(end);
  if (end < 0) {
    end += length;
  }
  end = start > end ? 0 : toLength(end);
  while (start < end) {
    array[start++] = value;
  }
  return array;
}
 if (start < 0) {
    start = -start > length ? 0 : (length + start);
  }

对于入参start来讲,如果传的是一个负数,如果是-5,实际上数组长度是4,那么这么start显然是不符合条件的。 所以这里才以-start > end来做判断条件

end = (end === undefined || end > length) ? length : toInteger(end);
if (end < 0) {
end += length;
}
  • end不存在(不传end的时候)或者end大于数组长度,end等于数组长度length,也就是填充到最后
  • 对于负数end,需要追加这个length就是目标的end值。
end = start > end ? 0 : toLength(end);
while (start < end) {
    array[start++] = value;
}
return array;

通过这个遍历赋值完成fill操作。最后返回结果。

总结

_.fill先是对入参进行简单的清洗。再通过baseFill方法处理返回结果。

查看原文

赞 0 收藏 0 评论 0

最普通的一个 关注了用户 · 2020-11-24

蒋鹏飞 @jiangpengfei_5ecce944a3d8a

前端工程师,底层技术人。
思否2020年度“Top Writer”!
掘金“优秀作者”!
开源中国2020年度“优秀源创作者”!
分享各种大前端进阶知识!
关注公众号【进击的大前端】第一时间获取高质量原创。
更多文章和示例源码请看:https://github.com/dennis-jia...

关注 1863

最普通的一个 发布了文章 · 2020-11-19

lodash源码-toPairs

功能

创建一个object对象自身可枚举属性的键🈯值对数组。如果Object是map或者set,返回其条目。

function Foo() {

  this.a = 1;

  this.b = 2;

}

Foo.prototype.c = 3;
_.toPairs(new Foo);

// => [['a', 1], ['b', 2]] 

源码实现

var createToPairs = require('./_createToPairs'),
    keys = require('./keys');

var toPairs = createToPairs(keys);

从这里来看先理解keys方法和createToPairs方法的实现。

keys就是_.keys方法,功能是创建一个object自身可枚举属性名的数组 (暂时解释,接下来会写一篇keys的文章)

toPairs来看,createToPairs接收一个函数返回一个方法。

func => object => {}

createToPairs

function createToPairs(keysFunc) {
  return function(object) {
    var tag = getTag(object);
    if (tag == mapTag) {
      return mapToArray(object);
    }
    if (tag == setTag) {
      return setToPairs(object);
    }
    return baseToPairs(object, keysFunc(object));
  };
}

module.exports = createToPairs;

keysFunc是用来获取给定对象object的keys的方法。对toPairstoPairsIn的区别就是keysFunc不同。

对于返回的函数

 function(object) {
    var tag = getTag(object);
    if (tag == mapTag) {
      return mapToArray(object);
    }
    if (tag == setTag) {
      return setToPairs(object);
    }
    return baseToPairs(object, keysFunc(object));
  };
}

对其来讲,setmap类型会分别由mapToArraysetToPairs来处理。除了二者的类型都会由baseToPairs处理。

在这之前,可以先了解下getTag的实现。

getTag

关于getTag百度出来一个很好的文章

在es5规范中,调用Object.prototype.totring会采用下面的步骤

  1. 如果 this 的值是 undefined, 返回 "[object Undefined]".
  2. 如果 this 的值是 null, 返回 "[object Null]".
  3. 令 O 为以 this 作为参数调用 ToObject 的结果 .
  4. 令 class 为 O 的 [[Class]] 内部属性的值 .
  5. 返回三个字符串 "[object ", class, and "]" 连起来的字符串

对es5来讲,除了undefinednull会返回[object Undefined]和[object Null]。

对于其它的类型,都会返回一个[Object [[class]]]的字符串。[[Class]]为内部属性的值。

在es6中的,没有了[[class]]这个内部属性,这部分逻辑可以查看深入理解Object.prototype.toString方法来理解。

对稳重的步骤17,需要引用一下令 tag 为 Get(O, @@toStringTag) 的返回值( Get(O, @@toStringTag) 方法,既是在 O 是一个对象,并且具有 @@toStringTag 属性时,返回 O[Symbol.toStringTag] )

let obj = {}

Object.defineProperty(obj, Symbol.toStringTag, {
    get: function() {
        return "newClass"
    }
})

console.log(Object.prototype.toString.call(obj)) 

// => [object newClass]

在es6规范table1中Symbol.toStringTag被解释为 该属性值是一个字符串,创建对象时它是对其的默认描述。通过Object.prototype.toString才能访问这个属性

也就是当调用Object.prototype.toString如果Symbol.toStringTag存在会返回它的属性值。

通过上边的这些认识,我们再去查看baseGetTag就容易理解了。

baseTag

function baseGetTag(value) {
  if (value == null) {
    return value === undefined ? undefinedTag : nullTag;
  }
  return (symToStringTag && symToStringTag in Object(value))
    ? getRawTag(value)
    : objectToString(value);
}

对于是symToStringTag会调用getRawTag方法,其它类型直接调用Object.prototype.toString返回tag
正如在es6规范所描述。

getRawTag

function getRawTag(value) {
  var isOwn = hasOwnProperty.call(value, symToStringTag),
      tag = value[symToStringTag];

  try {
    value[symToStringTag] = undefined;
    var unmasked = true;
  } catch (e) {}

  var result = nativeObjectToString.call(value);
  if (unmasked) {
    if (isOwn) {
      value[symToStringTag] = tag;
    } else {
      delete value[symToStringTag];
    }
  }
  return result;
}

getRawTag方法实际做的就是去除掉symToStringTag对获取tag的影响。先通过value[symToStringTag] = undefined;移除掉该属性。再通过nativeObjectToString.call(value)获取其tag.

if (unmasked) {
    if (isOwn) {
      value[symToStringTag] = tag;
    } else {
      delete value[symToStringTag];
    }
  }

这段代码也很简单,做了一个修正。

总结

toParis依次调用了toParis,getTag,baseTag,getRawTag.

至于Object.prototype.toString的由来可结合代码再自行体会。
参考

查看原文

赞 0 收藏 0 评论 0

最普通的一个 发布了文章 · 2020-11-19

nestjs中winston日志模块使用

winston 日志模块

winston模块介绍

levels

const levels = { 
  error: 0,
  warn: 1,
  info: 2,
  http: 3,
  verbose: 4,
  debug: 5,
  silly: 6
};

winston.createLogger参数

const logger = winston.createLogger({
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

如上代码可以创建一个Logger.

参数

  • level 默认info 只记录 小于等于info级别的日志。
  • levels 默认``,日志的优先级别和颜色
  • format 格式化
  • transports 日志的输出目标,可以是文件,控制台,http服务,或者流
  • exitOnError true ,如果设置为false,处理到的异常不会造成 退出
  • silent false,如果为真,所有异常都被禁用

### winston.formats

winston.formats由这个三方库实现logform

const alignedWithColorsAndTime = format.combine(
  format.colorize(),   // info.level在控制台中输出会有颜色
  format.timestamp(), // 添加这个后,info中会有timestamp属性
  format.align(),
  format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`)
);

关于info对象,format.printf中的info就是info对象。

它至少有两个属性 level,message.

format.colorize,format.timestamp 这些被称为format。那format是什么呢,实际上是对info的进一层加工,返回一个更改过后的info

const { format } = require('logform');

const volume = format((info, opts) => {
  if (opts.yell) {
    info.message = info.message.toUpperCase();
  } else if (opts.whisper) {
    info.message = info.message.toLowerCase();
  }

  return info;
});

多个format可以通过format.combine合并成一个。正如上边第一个demo中使用的方法一样。

一些内置的format
    • align 对齐
    • colorize 着色
      format.colorize({
         colors: {
           info: "red"
         }
       }),
    • combine 合并多个format
    • errors
    • json
    • label 一个label标签 [label]....
    
    format.label({
      label: ""
    })
    • Logstash 转成纯json对象
    • Metadata 除了level和message属性,其它都放入到meta属性下边
    • padLevels 格式长度相同
    • PrettyPrint 这个最好不要在生产模式使用
    • simple 简单的模式
    const { format } = require('logform');
    const MESSAGE = Symbol.for('message');
    
    const simpleFormat = format.simple();
    
    const info = simpleFormat.transform({
     level: 'info',
     message: 'my message',
     number: 123
    });
    console.log(info[MESSAGE]);
    // info: my message {number:123}
    • timestamp 时间,接收一个[fecha]()库可以理解的字符串。
    format.timestamp({
       format: 'HH:mm:ss YY/MM/DD'
     })
    • Uncolorize 去除颜色

    自定义levels

    winston定义了两种levels,默认用的是npm levels

    { 
      error: 0, 
      warn: 1, 
      info: 2, 
      http: 3,
      verbose: 4, 
      debug: 5, 
      silly: 6 
    }

    当然支持自定义levels

    const myCustomLevels = {
      levels: {
        foo: 0,
        bar: 1,
        baz: 2,
        foobar: 3
      },
      colors: {
        foo: 'blue',
        bar: 'green',
        baz: 'yellow',
        foobar: 'red'
      }
    };
    
    const customLevelLogger = winston.createLogger({
      levels: myCustomLevels.levels
    });
    
    customLevelLogger.foobar('some foobar level-ed message');

    多个传输

    如果需要把成功级别的消息传入一个log文件,错误的传入另一个log文件,那么可以这么使用

    由level针对级别。

    const logger = winston.createLogger({
      transports: [
        new winston.transports.File({
          filename: 'combined.log',
          level: 'info'
        }),
        new winston.transports.File({
          filename: 'errors.log',
          level: 'error'
        })
      ]
    });

    常见的transport

    每个不同的transport都可以使用不同的format参数。毕竟要写入的格式与内容不同。

    const logger = winston.createLogger({
      transports: [
        new winston.transports.File({
          filename: 'error.log',
          level: 'error',
          format: winston.format.json()
        }),
        new transports.Http({
          level: 'warn',
          format: winston.format.json()
        }),
        new transports.Console({
          level: 'info',
          format: winston.format.combine(
            winston.format.colorize(),
            winston.format.simple()
          )
        })
      ]
    });

    异常处理

    winston的异常处理。

    Handling Uncaught Exceptions with winston

    未捕获的异常,winston可以处理。

    const logger = createLogger({
      transports: [
        new transports.File({ filename: 'combined.log' }) 
      ],
      exceptionHandlers: [
        new transports.File({ filename: 'exceptions.log' })
      ]
    });

    阅读更多

    winston将异常保留在对应的log文件中后会退出

    设置exitOnError: false,可以取消退出。

    未捕获的promise

    使用rejectionHandlers

    const { createLogger, transports } = require('winston');
    
    // Enable rejection handling when you create your logger.
    const logger = createLogger({
      transports: [
        new transports.File({ filename: 'combined.log' }) 
      ],
      rejectionHandlers: [
        new transports.File({ filename: 'rejections.log' })
      ]
    });

    阅读更多

    nest中集成winston模块

    安装nest-winston

    npm install nest-winston

    main.ts全局winston

    import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
    
    
    async function bootstrap() {
      // 关闭默认的logger
      const app = await NestFactory.create(AppModule,{
        logger: false,
      });
    
      const nestWinston = app.get(WINSTON_MODULE_NEST_PROVIDER);
      //全局的logger
      app.useLogger(nestWinston);
      // 异常拦截写入日志
      app.useGlobalFilters(new  HttpExceptionFilter(nestWinston.logger))
    
      await app.listen(3003);
    }
    import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Inject } from '@nestjs/common';
    import { Error as MongoError } from "mongoose"
    import { Request, Response } from 'express';
    import * as _ from "lodash"
    import {Logger} from "winston";
    
    import {ErrorCode} from "../constant/error"
    @Catch()
    export class HttpExceptionFilter implements ExceptionFilter {
      constructor(private readonly logger: Logger) {}
    
      catch(exception: any, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<Response>();
        const request = ctx.getRequest<Request>();
        let status = HttpStatus.OK;
        let message;
        let errorCode: number;
        if(exception instanceof MongoError) {
           errorCode = ErrorCode.Mongoose.CODE;  //Mongo Error Code
           message = exception.message || ErrorCode.Mongoose.MESSAGE
         
        }else if (exception.getStatus){
         
          const httpException: HttpException = exception as HttpException;
          if(httpException.message && typeof httpException.message.errorCode !== 'undefined' ) {
            errorCode = httpException.message.errorCode;
            message = httpException.message.message || ErrorCode.getMessageByCode(httpException.message.errorCode); //
          }else{
            errorCode = httpException.getStatus(); // 未登陆 401,
            message = _.isObject(httpException.message) ? ((httpException.message as any).error) : ""
            status = errorCode;
          }
        }else{
          errorCode = ErrorCode.JSERROR.CODE;
          message = exception.message;
          this.logger.error( {message: [ exception.message, exception.stack ].join('\n'),})
        }
        response.status(status).json({
          errorCode,
          message,
          data: ""
        });
    
        
      }
    }

    app.module.ts

    import {WinstonModule} from "nest-winston";
    import * as winston from 'winston';
    import DailyRotateFile = require("winston-daily-rotate-file");
    const format = winston.format;
    
    @Module({
      imports: [
        WinstonModule.forRoot({
          exitOnError: false,
          format: format.combine(
            format.colorize(),
            format.timestamp({
              format: 'HH:mm:ss YY/MM/DD'
            }),
            format.label({
              label: "测试"
            }),
          
            format.splat(),
            format.printf( info => {
              return `${info.timestamp} ${info.level}: [${info.label}]${info.message}`
            }),
          ),
          transports: [
            new winston.transports.Console({
              level: 'info',
    
            }),
            new DailyRotateFile({
              filename: 'logs/application-%DATE%.log',
              datePattern: 'YYYY-MM-DD-HH',
              zippedArchive: true,
              maxSize: '20m',
              maxFiles: '14d',
            }),
         
          ],
        }),
        ],
      controllers: [],
      providers: [AppService],
    })
    export class AppModule {
      
    }
    
    查看原文

    赞 0 收藏 0 评论 0

    最普通的一个 发布了文章 · 2020-11-09

    lodash.js源码-flatten

    功能

    调用方式

    _.flatten(array)

    减少一级array嵌套深度

    _.flatten([1,[2,3,]]) 
    
    // =< [1,2,3]

    数组内的成员如果是数组,那么就会被展开到,内部的元素成为上一级数组的元素。

    这种的功能实现起来很简单,首先入参是一个数组,返回也是一个数组

    
    function flatten(array){
       let result = []
       for(let i = 0; i< array.length; i++){
    
            if(Array.isArray(array[i])) {
                // 处理数组中的元素 ,push到result中
            }else {
                result.push(array[i])
            }
       }
       
       
       return result
    }
    

    lodash源码实现

    function flatten(array) {
      var length = array == null ? 0 : array.length;
      return length ? baseFlatten(array, 1) : [];
    }

    通过源码,我们可以很清晰的得到flatten方法的核心功能都是由baseFlatten实现的。

    经常使用lodash的开发者会发现,flattenDeepflattenDepth中有baseFlatten

    function flattenDepth(array, depth) {
      const length = array == null ? 0 : array.length
      if (!length) {
        return []
      }
      depth = depth === undefined ? 1 : +depth
      return baseFlatten(array, depth)
    }
    
    
    function flattenDeep(array) {
      const length = array == null ? 0 : array.length
      return length ? baseFlatten(array, INFINITY) : []
    }

    baseFlatten正是flatten**方法的核心实现。

    探秘baseFlatten

    function baseFlatten(array, depth, predicate, isStrict, result) {
      // predicate默认为isFlattenable, 传入的array如果是可以`flatten`化,返回true,
      predicate || (predicate = isFlattenable)
      result || (result = [])
    
      if (array == null) {
        return result
      }
    
      for (const value of array) {
        if (depth > 0 && predicate(value)) {
          if (depth > 1) {
            // Recursively flatten arrays (susceptible to call stack limits).
            baseFlatten(value, depth - 1, predicate, isStrict, result)
          } else {
            result.push(...value)
          }
        } else if (!isStrict) {
          result[result.length] = value
        }
      }
      return result
    }

    predicate默认为isFlattenable, 传入的array如果是可以flatten化,返回true.

    假定入参baseFlatten(array, 1)

    接下来看具体的处理部分.抽离出代码

    for (const value of array) {
        if (depth > 0 && predicate(value)) {
          if (depth > 1) {
            baseFlatten(value, depth - 1, predicate, isStrict, result)
          } else {
            result.push(...value)
          }
        } else if (!isStrict) {
          result[result.length] = value
        }
      }

    第一个判断条件

    depth > 0 && predicate(value)

    flatten来讲,depth = 1,当for...of迭代出的元素predicate(value)true

    看一下默认的实现

    function isFlattenable(value) {
      return Array.isArray(value) || isArguments(value) ||
        !!(value && value[spreadableSymbol])
    }

    lodash默认认为数组,arguments 和 value[spreadableSymbol]Flattenable的。

    predicate也可以手动传入。

    如果第一轮的遍历出的元素是一个数组。执行的是如下的代码。没什么好说的。

     result.push(...value)

    当迭代出的不是Flattenable(或者说是predicate(value)为false),会执行如下代码

    result[result.length] = value

    接下来思考depth为2的时候是下边的逻辑是如何执行的。

    假设此时的入参数遍历出来的value为 ['a','b','c'].此时

     if (depth > 1) {
            baseFlatten(value, depth - 1, predicate, isStrict, result)
     } 

    执行的就是baseFlatten(['a','b','c'],1,isFlattenable,undefined,[]).

    实际上执行的是flatten(['a','b','c']).只不过传入了一个result来存放处理后的结果。depath减少以后,就不会再向下继续执行递归了。

    即便是depth为3甚至更高的元素,也会通过递归,每一次递归的结果保存到result中,执行到最后,便是返回最后的结果。

    总结

    flatten,flattenDeep,flattenDepth等方法都是通过baseFlatten衍生出来的。

    再实际开发中,我们也要学习作者的思路去抽象封装代码。

    查看原文

    赞 0 收藏 0 评论 0

    最普通的一个 赞了文章 · 2020-11-09

    如何利用AOP+IOC思想解构前端项目开发

    本文将通过 TypeClient 架构来阐述如何利用AOP+IOC思想来解构前端项目的开发。

    首先声明,AOP+IOC思想的理解需要有一定的编程架构基础。目前,这两大思想使用的场景,基本都在nodejs端,在前端的实践非常少。我本着提供一种新的项目解构思路的想法,而非推翻社区庞大的全家桶。大家看看就好,如果能给你提供更好的灵感,那么再好不过了,非常欢迎交流。

    以下我们将以 TypeClient 的 React 渲染引擎为例。

    AOP

    一种面向切面编程的思想。它在前端的表现是前端的装饰器,我们可以通过装饰器来拦截函数执行前与执行后的自定义行为。

    AOP的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后, 再通过“动态织入”的方式掺入业务逻辑模块中。 AOP的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用日志统计等功能模块。

    以上是网络上对AOP的简单解释。那么实际代码也许是这样的

    @Controller()
    class Demo {
      @Route() Page() {}
    }
    复制代码

    但很多时候,我们仅仅是将某个class下的函数当作一个储存数据的对象而已,而在确定运行这个函数时候拿出数据做自定义处理。可以通过 reflect-metadata 来了解更多装饰器的作用。

    IOC

    Angular难以被国内接受很大一部分原因是它的理念太庞大,而其中的DI(dependency inject)在使用时候则更加让人迷糊。其实除了DI还有一种依赖注入的思想叫 IOC。它的代表库为 inversify。它在github上拥有6.7K的star数,在依赖注入的社区里,口碑非常好。我们可以先通过这个库来了解下它对项目解构的好处。

    例子如下:

    @injectable()
    class Demo {
      @inject(Service) private readonly service: Service;
      getCount() {
        return 1 + this.service.sum(2, 3);
      }
    }
    复制代码
    当然,Service已经优先被注入到inversify的container内了,才可以通过 TypeClient 这样调用。

    重新梳理前端项目运行时

    一般地,前端项目会经过这样的运行过程。

    1. 通过监听hashchange或者popstate事件拦截浏览器行为。
    2. 设定当前获得的window.location 数据如何对应到一个组件。
    3. 组件如何渲染到页面。
    4. 当浏览器URL再次变化的时候,我们如何对应到一个组件并且渲染。

    这是社区的通用解决方案。当然,我们不会再讲解如何设计这个模式。我们将采用全新的设计模式来解构这个过程。

    重新审视服务端路由体系

    我们聊的是前端的架构,为什么会聊到服务端的架构体系?

    那是因为,其实设计模式并不局限在后端或者前端,它应该是一种比较通用的方式来解决特定的问题。

    那么也许有人会问,服务端的路由体系与前端并不一致,有何意义?

    我们以nodejs的http模块为例,其实它与前端有点类似的。http模块运行在一个进程中,通过http.createServer的参数回调函数来响应数据。我们可以认为,前端的页面相当于一个进程,我们通过监听相应模式下的事件来响应得到组件渲染到页面。

    服务端多个Client发送请求到一个server端端口处理,为什么不能类比到前端用户操作浏览器地址栏通过事件来得到响应入口呢?

    答案是可以的。我们称这种方式为 virtual server 即基于页面级的虚拟服务。

    既然可以抽象称一种服务架构,那当然,我们可以完全像nodejs的服务化方案靠拢,我们可以将前端的路由处理的如nodejs端常见的方式,更加符合我们的意图和抽象。

    history.route('/abc/:id(d+)', (ctx) => {
      const id = ctx.params.id;
      return <div>{id}</div>;
      // 或者: ctx.body = <div>{id}</div>; 这种更加能理解
    })
    复制代码

    改造路由设计

    如果是以上的书写方式,那么也可以解决基本的问题,但是不符合我们AOP+IOC的设计,书写的时候还是比较繁琐的,同时也没有解构掉响应的逻辑。

    我们需要解决以下问题:

    1. 如何解析路由字符串规则?
    2. 如何利用这个规则快速匹配到对应的回调函数?

    在服务端有很多解析路由规则的库,比较代表的是 path-to-regexp,它被使用在KOA等著名架构中。它的原理也就是将字符串正则化,使用当前传入的path来匹配相应的规则从而得到对应的回调函数来处理。但是这种做法有一些瑕疵,那就是正则匹配速度较慢,当处理队列最后一个规则被匹配的时候,所有规则都将被执行过,当路由过多时候性能较差,这一点可以参看我之前写的 koa-rapid-router超越koa-router性能的100多倍。还有一点瑕疵是,它的匹配方式是按照你编写顺序匹配的,所以它具有一定的顺序性,开发者要非常注意。比如:

    http.get('/:id(d+)', () => console.log(1));
    http.get('/1234', () => console.log(2));
    复制代码

    如果我们访问/1234,那么它将打印出1,而非2

    为了解决性能以及优化匹配过程的智能性,我们可以参考 find-my-way 的路由设计体系。具体请看官自己看了,我不解析。总之,它是一种字符串索引式算法,能够快速而智能地匹配到我们需要的路由。著名的 fastify 就是采用这个架构来达到高性能的。

    TypeClient 的路由设计

    我们可以通过一些简单的装饰器就能快速定义我们的路由,本质还是采用find-my-way的路由设计原则。

    import React from 'react';
    import { Controller, Route, Context } from '@typeclient/core';
    import { useReactiveState } from '@typeclient/react';
    @Controller('/api')
    export class DemoController {
      @Route('/test')
      TestPage(props: Reat.PropsWithoutRef<Context>) {
        const status = useReactiveState(() => props.status.value);
        return <div>Hello world! {status}</div>;
      }
    }
    // --------------------------
    // 在index.ts中只要
    app.setController(DemoController);
    // 它就自动绑定了路由,同时页面进入路由 `/api/test` 的时候
    // 就会显示文本 `Hello world! 200`。
    复制代码
    可见,TypeClient 通过 AOP 理念定义路由非常简单。

    路由生命周期

    当从一个页面跳转到另一个页面的时候,前一个页面的生命周期也随即结束,所以,路由是具有生命周期的。再此,我们将整个页面周期拆解如下:

    1. beforeCreate 页面开始加载
    2. created 页面加载完成
    3. beforeDestroy 页面即将销毁
    4. destroyed 页面已经销毁

    为了表示这4个生命周期,我们根据React的hooks特制了一个函数useContextEffect来处理路由生命周期的副作用。比如:

    import React from 'react';
    import { Controller, Route, Context } from '@typeclient/core';
    import { useReactiveState } from '@typeclient/react';
    @Controller('/api')
    export class DemoController {
      @Route('/test')
      TestPage(props: Reat.PropsWithoutRef<Context>) {
        const status = useReactiveState(() => props.status.value);
        useContextEffect(() => {
          console.log('路由加载完成了');
          return () => console.log('路由被销毁了');
        })
        return <div>Hello world! {status}</div>;
      }
    }
    复制代码

    其实它与useEffect或者useLayoutEffect有些类似。只不过我们关注的是路由的生命周期,而react则关注组件的生命周期。

    其实通过上面的props.status.value我们可以猜测出,路由是有状态记录的,分别是100200还有500等等。我们可以通过这样的数据来判断当前路由处于什么生命周期内,也可以通过骨架屏来渲染不同的效果。

    中间件设计

    为了控制路由生命周期的运行,我们设计了中间件模式,用来处理路由前置的行为,比如请求数据等等。中间件原则上采用与KOA一致的模式,这样可以大大兼容社区生态。

    const middleware = async (ctx, next) => {
      // ctx.....
      await next();
    }
    复制代码

    通过AOP 我们可以轻松引用这个中间件,达到页面加载完毕状态前的数据处理。

    import React from 'react';
    import { Controller, Route, Context, useMiddleware } from '@typeclient/core';
    import { useReactiveState } from '@typeclient/react';
    @Controller('/api')
    export class DemoController {
      @Route('/test')
      @useMiddleware(middleware)
      TestPage(props: Reat.PropsWithoutRef<Context>) {
        const status = useReactiveState(() => props.status.value);
        useContextEffect(() => {
          console.log('路由加载完成了');
          return () => console.log('路由被销毁了');
        })
        return <div>Hello world! {status}</div>;
      }
    }
    复制代码

    设计周期状态管理 - ContextStore

    不得不说这个是一个亮点。为什么要设计这样一个模式呢?主要是为了解决在中间件过程中对数据的操作能够及时响应到页面。因为中间件执行与react页面渲染是同步的,所以我们设计这样的模式有利于数据的周期化。

    我们采用了非常黑科技的方案解决这个问题:@vue/reactity

    对,就是它。

    我们在react中嵌入了VUE3最新的响应式系统,让我们开发快速更新数据,而放弃掉dispatch过程。当然,这对中间件更新数据是及其有力的。

    这里 我非常感谢 sl1673495 给到的黑科技思路让我们的设计能够完美兼容react。

    我们通过@State(callback)来定义ContextStore的初始化数据,通过useContextState或者useReactiveState跟踪数据变化并且响应到React页面中。

    来看一个例子:

    import React from 'react';
    import { Controller, Route, Context, useMiddleware, State } from '@typeclient/core';
    import { useReactiveState } from '@typeclient/react';
    @Controller('/api')
    export class DemoController {
      @Route('/test')
      @useMiddleware(middleware)
      @State(createState)
      TestPage(props: Reat.PropsWithoutRef<Context>) {
        const status = useReactiveState(() => props.status.value);
        const count = useReactiveState(() => props.state.count);
        const click = useCallback(() => ctx.state.count++, [ctx.state.count]);
        useContextEffect(() => {
          console.log('路由加载完成了');
          return () => console.log('路由被销毁了');
        })
        return <div onClick={click}>Hello world! {status} - {count}</div>;
      }
    }
    
    function createState() {
      return {
        count: 0,
      }
    }
    复制代码

    你可以看到不断点击,数据不断变化。这种操作方式极大简化了我们数据的变化写法,同时也可以与vue3响应式能力看齐,弥补react数据操作复杂度的短板。

    除了在周期中使用这个黑科技,其实它也是可以独立使用的,比如在任意位置定义:

    // test.ts
    import { reactive } from '@vue/reactity';
    
    export const data = reactive({
      count: 0,
    })
    复制代码

    我们可以在任意组件中使用

    import React, { useCallback } from 'react';
    import { useReactiveState } from '@typeclient/react-effect';
    import { data } from './test';
    
    function TestComponent() {
      const count = useReactiveState(() => data.count);
      const onClick = useCallback(() => data.count++, [data.count]);
      return <div onClick={onClick}>{count}</div>
    }
    复制代码

    利用IOC思想解构项目

    以上的讲解都没有设计IOC方面,那么下面将讲解IOC的使用。

    Controller 服务解构

    我们先编写一个Service文件

    import { Service } from '@typeclient/core';
    
    @Service()
    export class MathService {
      sum(a: number, b: number) {
        return a + b;
      }
    }
    复制代码

    然后我们可以在之前的Controller中直接调用:

    import React from 'react';
    import { Controller, Route, Context, useMiddleware, State } from '@typeclient/core';
    import { useReactiveState } from '@typeclient/react';
    import { MathService } from './service.ts';
    @Controller('/api')
    export class DemoController {
      @inject(MathService) private readonly MathService: MathService;
    
      @Route('/test')
      @useMiddleware(middleware)
      @State(createState)
      TestPage(props: Reat.PropsWithoutRef<Context>) {
        const status = useReactiveState(() => props.status.value);
        const count = useReactiveState(() => props.state.count);
        const click = useCallback(() => ctx.state.count++, [ctx.state.count]);
        const value = this.MathService.sum(count, status);
        useContextEffect(() => {
          console.log('路由加载完成了');
          return () => console.log('路由被销毁了');
        })
        return <div onClick={click}>Hello world! {status} + {count} = {value}</div>;
      }
    }
    
    function createState() {
      return {
        count: 0,
      }
    }
    复制代码

    你可以看到数据的不断变化。

    Component 解构

    我们为react的组件创造了一种新的组件模式,称IOCComponent。它是一种具备IOC能力的组件,我们通过useComponent的hooks来调用。

    import React from 'react';
    import { Component, ComponentTransform } from '@typeclient/react';
    import { MathService } from './service.ts';
    
    @Component()
    export class DemoComponent implements ComponentTransform {
      @inject(MathService) private readonly MathService: MathService;
    
      render(props: React.PropsWithoutRef<{ a: number, b: number }>) {
        const value = this.MathService.sum(props.a, props.b);
        return <div>{value}</div>
      }
    }
    复制代码

    然后在任意组件中调用

    import React from 'react';
    import { Controller, Route, Context, useMiddleware, State } from '@typeclient/core';
    import { useReactiveState } from '@typeclient/react';
    import { MathService } from './service.ts';
    import { DemoComponent } from './component';
    @Controller('/api')
    export class DemoController {
      @inject(MathService) private readonly MathService: MathService;
      @inject(DemoComponent) private readonly DemoComponent: DemoComponent;
    
      @Route('/test')
      @useMiddleware(middleware)
      @State(createState)
      TestPage(props: Reat.PropsWithoutRef<Context>) {
        const status = useReactiveState(() => props.status.value);
        const count = useReactiveState(() => props.state.count);
        const click = useCallback(() => ctx.state.count++, [ctx.state.count]);
        const value = this.MathService.sum(count, status);
        const Demo = useComponent(this.DemoComponent);
        useContextEffect(() => {
          console.log('路由加载完成了');
          return () => console.log('路由被销毁了');
        })
        return <div onClick={click}>
          Hello world! {status} + {count} = {value} 
          <Demo a={count} b={value} />
        </div>;
      }
    }
    
    function createState() {
      return {
        count: 0,
      }
    }
    复制代码

    Middleware 解构

    我们完全可以抛弃掉传统的中间件写法,而采用能加解构化的中间件写法:

    import { Context } from '@typeclient/core';
    import { Middleware, MiddlewareTransform } from '@typeclient/react';
    import { MathService } from './service';
    
    @Middleware()
    export class DemoMiddleware implements MiddlewareTransform {
      @inject(MathService) private readonly MathService: MathService;
    
      async use(ctx: Context, next: Function) {
        ctx.a = this.MathService.sum(1, 2);
        await next();
      }
    }
    复制代码

    为react新增Slot插槽概念

    它支持Slot插槽模式,我们可以通过useSlot获得Provider与Consumer。它是一种通过消息传送节点片段的模式。

    const { Provider, Consumer } = useSlot(ctx.app);
    <Provider name="foo">provider data</Provider>
    <Consumer name="foo">placeholder</Consumer>
    复制代码

    然后编写一个IOCComponent或者传统组件。

    // template.tsx
    import { useSlot } from '@typeclient/react';
    @Component()
    class uxx implements ComponentTransform {
      render(props: any) {
        const { Consumer } = useSlot(props.ctx);
        return <div>
          <h2>title</h2>
          <Consumer name="foo" />
          {props.children}
        </div>
      }
    }
    复制代码

    最后在Controller上调用

    import { inject } from 'inversify';
    import { Route, Controller } from '@typeclient/core';
    import { useSlot } from '@typeclient/react';
    import { uxx } from './template.tsx';
    @Controller()
    @Template(uxx)
    class router {
      @inject(ttt) private readonly ttt: ttt;
      @Route('/test')
      test() {
        const { Provider } = useSlot(props.ctx);
        return <div>
          child ...
          <Provider name="foo">
            this is foo slot
          </Provider>
        </div>
      }
    }
    复制代码

    你能看到的结构如下:

    <div>
      <h2>title</h2>
      this is foo slot
      <div>child ...</div>
    </div>
    复制代码

    解构项目的原则

    我们可以通过对IOC服务与Middleware还有组件进行不同纬度的解构,封装成统一的npm包上传到私有仓库中供公司内部开发使用。

    类型

    1. IOCComponent + IOCService
    2. IOCMiddleware + IOCService
    3. IOCMiddlewware
    4. IOCService

    原则

    1. 通用化
    2. 内聚合
    3. 易扩展

    遵循这种原则的化可以使公司的业务代码或者组件具有高度的复用性,而且通过AOP能够很清楚直观的表现代码即文档的魅力。

    通用化

    即保证所封装的逻辑、代码或者组件具体高度的通用化特性,对于不太通用的没必要封装。比如说,公司内部统一的导航头,导航头有可能被用到任意项目中做统一化,那么就非常适合封装为组件型模块。

    内聚性

    通用的组件需要得到统一的数据,那么可以通过IOCComponent + IOCService + IOCMiddleware的形式将其包装,在使用的适合只需要关注导入这个组件即可。还是举例通用导航头。比如导航头需要下拉一个团队列表,那么,我们可以这样定义这个组件:

    一个service文件:

    // service.ts
    import { Service } from '@typeclient/core';
    @Service()
    export class NavService {
      getTeams() {
        // ... 这里可以是ajax请求的结果
        return [
          {
            name: 'Team 1',
            id: 1,
          },
          {
            name: 'Team 2',
            id: 1,
          }
        ]
      }
    
      goTeam(id: number) {
        // ...
        console.log(id);
      }
    }
    复制代码

    组件:

    // component.ts
    import React, { useEffect, setState } from 'react';
    import { Component, ComponentTransform } from '@typeclient/react';
    import { NavService } from './service';
    
    @Component()
    export class NavBar implements ComponentTransform {
      @inject(NavService) private readonly NavService: NavService;
      render() {
        const [teams, setTeams] = setState<ReturnType<NavService['getTeams']>>([]);
        useEffect(() => this.NavService.getTeams().then(data => setTeams(data)), []);
        return <ul>
          {
            teams.map(team => <li onClick={() => this.NavService.goTeam(team.id)}>{team.name}</li>)
          }
        </ul>
      }
    }
    复制代码

    我们将这个模块定义为@fe/navbar,同时导出这个个对象:

    // @fe/navbar/index.ts
    export * from './component';
    复制代码

    在任意的IOC组件中就可以这样调用

    import React from 'react';
    import { Component, ComponentTransform, useComponent } from '@typeclient/react';
    import { NavBar } from '@fe/navbar';
    
    @Component()
    export class DEMO implements ComponentTransform {
      @inject(NavBar) private readonly NavBar: NavBar;
      render() {
        const NavBar = useComponent(this.NavBar);
        return <NavBar />
      }
    }
    复制代码

    你可以发现只要加载这个组件,相当于请求数据都自动被载入了,这就非常有区别与普通的组件模式,它可以是一种业务型的组件解构方案。非常实用。

    易扩展

    主要是让我们对于设计这个通用型的代码或者组件时候保持搞扩展性,比如说,巧用SLOT插槽原理,我们可以预留一些空间给插槽,方便这个组件被使用不同位置的代码所传送并且替换掉原位置内容,这个的好处需要开发者自行体会。

    演示

    我们提供了一个demo来表现它的能力,而且可以从代码中看到如何解构整个项目。我们的每个Controller都可以独立存在,使得项目内容迁移变得非常容易。

    大家可以通过以上的两个例子来了解开发模式。

    总结

    新的开发理念并不是让你摒弃掉传统的开发方式和社区,而且提供更好的思路。当然,这种思路的好与坏,各有各的理解。但是我还是想声明下,我今天仅仅是提供一种新的思路,大家看看就好,喜欢的给个star。非常感谢!

    查看原文

    赞 6 收藏 1 评论 0

    最普通的一个 赞了文章 · 2020-11-09

    设计模式学习笔记(九):适配器模式

    1 概述

    1.1 引言

    有的笔记本电脑工作电压为20V,而我国家庭用电为220V,如何让20V的笔记本在220V的电压下工作?答案就是引入一个电源适配器,有了这个电源适配器笔记本就能在220V的电压下工作。

    在软件开发中,有时也会存在这类不兼容的状况,需要引入一个像电源适配器这样的称之为适配器的角色来协调这些不兼容的结构,这种设计方案就是适配器模式。

    1.2 定义

    将一个接口转换为客户希望的另一个接口,使接口不兼容的那些类可以一起工作,别名为包装器。

    适配器中的接口是广义的接口,可以表示一个方法或者方法的集合。

    适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

    1.3 分类

    根据适配器与适配者类的关系不同,可以分为对象适配器模式以及类适配器模式。

    1.3.1 对象适配器模式

    对象适配器模式就是适配器与适配者之间是关联关系

    结构图如下:

    在这里插入图片描述

    1.3.2 类适配器模式

    类适配器模式就是适配器与适配者之间是继承或实现关系。

    结构图如下:

    在这里插入图片描述

    由于语言特性的限制,比如Java,C#不支持多重继承,类适配器模式受到很多限制,例如Target如果不是接口而是一个类,就无法使用类适配器模式。此外如果适配者为final类也无法使用适配器模式,在Java等语言中大部分情况下使用对象适配器模式。

    1.4 角色

    • Target(目标抽象类):目标抽象类定义客户所需的接口,可以是一个抽象类或接口,也可以是一个具体类
    • Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配。适配器类是适配器模式的核心,在对象适配器模式中,它通过继承Target并关联一个Adaptee对象使两者产生联系,在类适配器模式,通过继承Adaptee并实现Target使两者产生联系
    • Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码

    2 典型实现

    2.1 步骤

    • 定义目标抽象类:接口/抽象类/具体类,客户端需要的接口,比如上面电源的例子,目标类就是给笔记本充电
    • (可选)定义适配者类:定义适配者类,但是一般来说适配者已经存在,比如上面电源的例子,适配者类就是220V的电压,而且对于某些库来说可能没有适配者的源码
    • 定义适配器类:继承/实现目标抽象类,并通过转发请求到适配者来完成操作

    2.2 目标抽象类

    这里实现为具体类:

    class Target
    {
        public void request()
        {
            System.out.println("Target方法");
        }
    }

    2.3 适配者类

    适配者一般为具体类,但是很多情况下已经存在并且没有源码实现:

    class Adaptee
    {
        public void specificRequest()
        {
            System.out.println("Adaptee方法");
        }
    }

    2.4 适配器类

    2.4.1 对象适配器

    对象适配器种适配器与适配者是关联关系,适配器中包含一个适配者成员,代码如下:

    class Adapter extends Target
    {
        private Adaptee adaptee = new Adaptee();
        @Override
        public void request()
        {
            adaptee.specificRequest();
        }
    }

    适配器覆盖目标抽象类的request,并将请求转发,交由适配者完成。

    2.4.2 类适配器

    类适配器中适配器与适配者是继承关系,其中适配者为父类,适配器为子类。但是在Java中由于不支持多重继承,因此想要在Java中实现类适配器模式,并且如果适配者是具体类的话,那么必须将目标抽象类指定为接口:

    interface Target
    {
        void request();
    }
    
    class Adaptee
    {
        public void specificRequest()
        {
            System.out.println("Adaptee方法");
        }
    }
    
    class Adapter extends Adaptee implements Target
    {
        @Override
        public void request()
        {
            super.specificRequest();
        }
    }

    在上述对象适配器的基础上,将目标抽象类修改为接口,同时适配器继承了适配者并实现了Target,并取消了适配者作为成员变量,在方法内直接调用super.xxx,也就是适配者的方法。

    2.5 客户端

    客户端的代码很简单,针对目标抽象类进行编程:

    public static void main(String[] args) 
    {
        Target adapter = new Adapter();
        adapter.request();
    }

    3 实例

    假设目前只有一条Micro USB线以及一台只有Type-C接口的手机,需要对其进行充电,这时候就需要一个转接头把Micro USB转为Type-C接口,才能给手机充电,使用适配器模式对其进行设计。

    设计如下:

    • 目标抽象类:TypeC
    • 适配者类:MicroUSB
    • 适配器:MicroUSBToTypeC

    简化实现代码如下:

    public class Test
    {
        public static void main(String[] args) {
            TypeC typeC = new MicroUSBToTypeC();
            typeC.chargeWithTypeC();
        }
    }
    
    //Target:给TypeC接口的手机充电
    interface TypeC
    {
        void chargeWithTypeC();
    }
    
    //Adaptee:适配者,MicroUSB线
    class MicroUSB
    {
        public void chargeWithMicroUSB()
        {
            System.out.println("MicroUSB充电");
        }
    }
    
    //Adapter:适配器,MicroUSB到TypeC的转接头
    class MicroUSBToTypeC implements TypeC
    {
        private MicroUSB microUSB = new MicroUSB();
        @Override
        public void chargeWithTypeC()
        {
            microUSB.chargeWithMicroUSB();
        }
    }

    4 双向适配器

    在对象适配器的使用过程中,如果在适配器中同时包含对Target类和Adaptee类的引用,Adaptee类可以通过适配器调用Target类中的方法,Target类也可以通过适配器调用Adaptee类的方法,那么该适配器就是一个双向适配器。例子如下:

    public class Test
    {
        public static void main(String[] args) {
            Adapter adapter = new Adapter();
            adapter.request();
            adapter.specificRequest();
        }
    }
    
    //适配者
    interface Adaptee
    {
        void specificRequest();
    }
    
    //Target类
    interface Target
    {
        void request();
    }
    
    //Target实现
    class TargetImpl implements Target
    {
        @Override
        public void request()
        {
            System.out.println("Target方法");
        }
    }
    
    //适配者实现
    class AdapteeImpl implements Adaptee
    {
        @Override
        public void specificRequest()
        {
            System.out.println("Adaptee方法");
        }
    }
    
    //适配器
    class Adapter implements Adaptee,Target
    {
        private Target target = new TargetImpl();
        private Adaptee adaptee = new AdapteeImpl();
        @Override
        public void request()
        {
            //Target的方法调用适配者方法
            adaptee.specificRequest();
        }
    
        @Override
        public void specificRequest()
        {
            //适配者方法调用Target的方法
            target.request();        
        }
    }

    5 缺省适配器

    5.1 定义

    缺省适配器:当不需要实现一个接口所提供的所有方法时,可先设计一个抽象类实现该接口,并为接口中的每个方法都提供一个默认实现(空实现),那么该抽象类子类可以选择性覆盖父类的某些方法来实现需求,它适用于不想使用一个接口中所有方法的情况,又叫单接口适配器模式。

    5.2 结构图

    在这里插入图片描述

    5.3 角色

    • ServiceInterface(适配者接口):通常是一个声明了大量方法的接口
    • AbstractServiceClass(缺省适配器类):缺省适配器模式的核心类,使用空方法的形式实现了在ServiceInterface接口中声明的方法,通常定义为抽象类
    • ConcreteServiceClass(具体业务类):是缺省适配器类的子类,只需要有选择性地覆盖适配器者中定义的方法,其他的方法在缺省适配器类中提供了空实现

    5.4 实例

    Java AWT中一般可以通过两种方式来处理窗口事件:

    • 实现WindowListener
    • 继承WindowAdapter

    其中WindowAdapter实现了WindowListener接口,但是都是提供了空实现,也就是说实现WindowsListener的话需要实现里面所有的方法,而继承WindowAdapter只需要选择性地覆盖方法即可,结构图:
    在这里插入图片描述

    6 主要优点

    类适配器以及对象适配器的共同优点如下:

    • 解耦:将Target与Adaptee解耦,引入适配器来重用现有的适配者类,无须修改原有结构
    • 提高复用性:将具体的业务实现过程封装在适配者类中,对于客户端而言是透明的,而且提高了适配者类的复用性,同一个适配者类可以在多个不同的系统复用
    • 扩展性好:可以很方便地更换适配器,也可以在不修改代码的基础上增加了新的适配器类,完全符合开闭原则,扩展灵活

    类适配器的独有优点如下:

    • 由于适配器类是适配者的子类,因此在适配器类中置换一些适配者的方法,使得适配器的灵活性更强。

    对象适配器的独有优点如下:

    • 一个对象适配器可以把多个不同的适配者适配到同一个Target
    • 可以适配一个适配者的子类,由于适配器与适配者之间是关联关系,根据LSP(里氏代换原则),适配者的子类也可以通过该适配器进行适配

    7 主要缺点

    类适配器缺点:

    • 对于Java,C#等不支持多重继承的语言,一次最多只能适配一个适配者类
    • 适配者不能是“不能继承的类”,比如Java的final类,C#的sealed
    • 在Java,C#等Target只能是接口不能是类

    对象适配器缺点:

    • 置换麻烦:相比起类适配器,在适配器中置换适配者的某些方法比较麻烦,需要先创建一个适配者类的子类,在子类将适配者类的方法置换掉,再把适配者的子类作为真正的适配者类进行适配,实现较为复杂

    8 适用场景

    • 系统需要使用一些现有的类,而这些类的接口(如方法名)不符合系统的需求,甚至没有这些类的源代码
    • 想创建一个可以重复使用的类,用于与彼此之间没有太大关联的类,包括可能在将来引进的类一起工作

    9 总结

    在这里插入图片描述

    如果觉得文章好看,欢迎点赞。

    同时欢迎关注微信公众号:氷泠之路。

    在这里插入图片描述

    查看原文

    赞 1 收藏 0 评论 0

    最普通的一个 发布了文章 · 2020-09-28

    nest中常用的swagger写法

    nest使用中,除了在DTO中使用class-validator去做校验。

    还可以去写swagger文档。

    nestcontroller写法中,代码如下

    //person.controller.ts
    
    @Post()
    async create(@Body() person: PersonDTO): Promise<Person> {
      return  ....
    }
    

    SwaggerModule会查找到@Body这个装饰器来生成API文档。同样的装饰器还有@Query,@Param。至于详尽的api说明都是从PersonDTO中解析出来。

    可以查阅中文文档链接

    下面是一些常用的装饰器

    ApiProperty

    参数说明

    • type 类型
    • required 是否必须
    • description 描述
    • default 默认值
    • name 属性名称,默认是装饰器修饰的属性名,但是显性的设置name文档中按照这个name的value为最终输入值

    type

    DTO的写法

    class PersonDTO {
      name: string;
      
      app: number; //或者
      
      links: string[]// 数组 
      
    }

    这是ts的写法

    而ApiProperty支持的TypeString,Number,Function,以及数组[String]

    数组的写法就是这样,一定要在type属性声明为[type]或者设置isArray属性为true

    ApiBody

    TypeScript 不会存储有关泛型或接口的元数据.

    需要使用ApiBody显性设置类型。

    对于没用参数装饰器三兄弟的,又需要写文档的,就用ApiBody制定一个DTO

    @ApiBody({ type: [CreateUserDto] })
    createBulk(@Body() usersDto: CreateUserDto[])

    设置可选 ApiPropertyOptional

    ApiProperty默认是必填的,如果期望是选填的。

    可以使用ApiPropertyOptional来代替。可以不需要去{required: false}

    ApiPropertyOptional其它参数参考ApiProperty.

    返回可选的装饰器 PartialType()

    对于create操作,所有的参数可能都是必填。

    而对于update操作,只需要更新部分操作。

    通过PartialType可以返回一个所有输入都是可选的参数

    export class UpdatePersonDto extends PartialType(CreatePersonDto) {}

    在更新的Controller使用这个CreatePersonDto就可以了。

    我在写本地demo的时候,发现PartialType在@nest/swagger下不存在。如果你也有这个问题,需要升级swagger这个包。~~~~

    more

    PickType()

    功能从一个输入类型中选择一部分属性来创建一个新类型(类)

    重点是选择一部分

    class PersonDTO {
     @ApiProperty({
        message: '',
        type: String,
     })
     name: string;
     
     @ApiProperty({
        message: '',
        type: String,
     })
     hintText: string;
     ...
    }

    我们只需要hintTextname

    只需要这么写

    class updateDTO extends PickType(PersonDTO,['name','hintText']){}

    updateDTO只有namehintText两个属性。

    PickType显然很有用

    OmitType

    OmitTypePickType功能是相反的,写法也一样。
    移除指定的输入属性。

    IntersectionType

    IntersectionType()函数将两种类型组合成一个新类型.

    export class UpdateCatDto extends IntersectionType(CreateCatDto, AdditionalCatInfo) {}

    注意是将两种输入类变成一个输入类,把两个类的所有属性合并为一个类

    • PartialType是类的所有成员全部变成可选的。
    • PickType对指定输入类选择指定的成员并返回一个类。
    • OmitType对指定输入类排除指定的成员并返回一个类。
    • IntersectionType是合并两个输入类,合并所有成员。

    更强的组合写法

    函数类映射是支持组合的写法的。

    export class UpdateCatDto extends PartialType( OmitType(CreateCatDto, ['name'] as const), ) {}

    常用的枚举类型

    ts中有一个枚举类型为enum.

    在日常的业务中,常有一些业务类型的判断。

    如以下场景 ,porductType,1对应类型A,2对应类型B,未来会继续拓展。

    我们在代码中如果直接判断vlaue,维护起来就会难以理解

    if(data.porductType === 1) {
      dosomething
    }else {
    
      其它产品
    }

    此时引入一个枚举就很适用

    enum prodcTypeEnum {
        typeA = '1',
        typeB = '2',
        ...
    }

    业务代码判断类型就成为了如下的形式

    
    if( data.type === prodcTypeEnum[typeA]){
    }

    枚举在业务中很常见,某个字段局限于确定的几个值,这便是枚举的使用场景.

    设置ApiProperty类型为具体的数组值。如果被装饰的字段是数组,还需要设置IsArraytrue.

    class User {
    @ApiProperty({
        enum: ['Admin','SuperAdmin','User'],
        isArray: true,
    })
    role: UserRole;
    }
    
    enum UserRole {
     Admin = `Admin`,
     SuperAdmin = `SuperAdmin`,
    ...
    }
    查看原文

    赞 1 收藏 1 评论 0

    最普通的一个 发布了文章 · 2020-09-25

    mongoose delete & winston

    Mongoose Populate

    Mongoose Delete 插件

    功能

    • 添加了 delete() 方法。
    • 添加了 deleteById() 方法。
    • 在documents上添加了 deleted 这个属性(Boolean)
    • 添加deteledAt属性用来保存删除的时间。
    • 添加deletedBy属性来记录谁删除了document。
    • 使用restore回复已经删除的文档。
    • 批量删除和恢复。
    • 可选择是否重写静态的方法(count, countDocuments, find, findOne, findOneAndUpdate, update, updateMany))
    • 对于可重写的方法额外提供了两个方法: methodDeleted,methodWithDeleted
    • 在删除时禁用模型的校验
    • 可选择 在删除的fields创建索引
    • 可选择是否禁用 $ne操作符 通过使用{use$neOperator: false}

    使用

    mongoose中的删除操作有remove,findByIdAndRemove,findOneAndRemove.这些方法都不会执行软删除

    对于删除操作,要是用deletedeleteById方法来删除数据是软删除。

    谁删除了数据。 添加 {deletedBy : true } link

    重写方法

    可选择重写所有或者指定的方法。

    目的: 重写方法将排除掉删除的document,(删除的document都有deleted = true)

    每个重写的方法都会有两个额外的方法。(methodDeleted,methodWithDeleted

    支持的可选重写方法。 ['count', 'countDocuments', 'find', 'findOne', 'findOneAndUpdate', 'update', 'updateMany', ]

    find方法为例。

    配置为

    
    {
    
    overrideMethods: ['find']
    
    }
    

    除了find方法以外会有findDeleted,findWithDeleted

    • find 方法返回所有除了deleted=false的数据
    • findDeleted 方法返回所有delted=true的数据
    • findWithDeleted返回所有数据

    删除时禁用model的校验

    默认情况下,该插件开启校验。

    
    Schema.plugin(mongoose_delete)
    
    Schema.plugin(mongoose_delete,{
    
    validateBeforeDelete: true
    
    })
    

    validateBeforeDelete设置为false,就不会校验

    创建索引

    
    PetSchema.plugin(mongoose_delete, { indexFields: 'all' });
    
    // or
    
    PetSchema.plugin(mongoose_delete, { indexFields: true });
    

    对于deleted,deletedAt,deletedBy三个字段都添加索引

    
    // Index only specific fields
    
    PetSchema.plugin(mongoose_delete, { indexFields: ['deleted', 'deletedBy'] });
    
    // or
    
    PetSchema.plugin(mongoose_delete, { indexFields: ['deletedAt'] });
    

    对指定的字段添加索引。(指定也是在3者之间)

    winston 日志模块

    levels

    
    const levels = {
    
    error: 0,
    
    warn: 1,
    
    info: 2,
    
    http: 3,
    
    verbose: 4,
    
    debug: 5,
    
    silly: 6
    
    };
    

    winston.createLogger参数

    
    const logger = winston.createLogger({
    
    transports: [
    
    new winston.transports.Console(),
    
    new winston.transports.File({ filename: 'combined.log' })
    
    ]
    
    });
    

    如上代码可以创建一个Logger.

    参数

    • level 默认info 只记录 小于等于info级别的日志。
    • levels 默认``,日志的优先级别和颜色
    • format 格式化
    • transports 日志的输出目标,可以是文件,控制台,http服务,或者流
    • exitOnError true ,如果设置为false,处理到的异常不会造成 退出
    • silent false,如果为真,所有异常都被禁用

    winston.formats

    winston.formats由这个三方库实现logform

    
    const alignedWithColorsAndTime = format.combine(
    
    format.colorize(), // info.level在控制台中输出会有颜色
    
    format.timestamp(), // 添加这个后,info中会有timestamp属性
    
    format.align(),
    
    format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`)
    
    );
    

    关于info对象,format.printf中的info就是info对象。

    它至少有两个属性 level,message.

    format.colorize,format.timestamp 这些被称为format。那format是什么呢,实际上是对info的进一层加工,返回一个更改过后的info

    
    const { format } = require('logform');
    
    const volume = format((info, opts) => {
    
    if (opts.yell) {
    
    info.message = info.message.toUpperCase();
    
    } else if (opts.whisper) {
    
    info.message = info.message.toLowerCase();
    
    }
    
    return info;
    
    });
    

    多个format可以通过format.combine合并成一个。正如上边第一个demo中使用的方法一样。

    一些内置的format
    • align 对齐
    • colorize 着色
    
    format.colorize({
    
    colors: {
    
    info: "red"
    
    }
    
    }),
    
    • combine 合并多个format
    • errors
    • json
    • label 一个label标签 [label]....
    
    format.label({
    
    label: ""
    
    })
    
    • Logstash 转成纯json对象
    • Metadata 除了level和message属性,其它都放入到meta属性下边
    • padLevels 格式长度相同
    • PrettyPrint 这个最好不要在生产模式使用
    • simple 简单的模式
    
    const { format } = require('logform');
    
    const MESSAGE = Symbol.for('message');
    
    const simpleFormat = format.simple();
    
    const info = simpleFormat.transform({
    
    level: 'info',
    
    message: 'my message',
    
    number: 123
    
    });
    
    console.log(info[MESSAGE]);
    
    // info: my message {number:123}
    
    • timestamp 时间,接收一个[fecha]()库可以理解的字符串。
    
    format.timestamp({
    
    format: 'HH:mm:ss YY/MM/DD'
    
    })
    
    • Uncolorize 去除颜色

    自定义levels

    winston定义了两种levels,默认用的是npm levels

    
    {
    
    error: 0,
    
    warn: 1,
    
    info: 2,
    
    http: 3,
    
    verbose: 4,
    
    debug: 5,
    
    silly: 6
    
    }
    

    当然支持自定义levels

    
    const myCustomLevels = {
    
    levels: {
    
    foo: 0,
    
    bar: 1,
    
    baz: 2,
    
    foobar: 3
    
    },
    
    colors: {
    
    foo: 'blue',
    
    bar: 'green',
    
    baz: 'yellow',
    
    foobar: 'red'
    
    }
    
    };
    
    const customLevelLogger = winston.createLogger({
    
    levels: myCustomLevels.levels
    
    });
    
    customLevelLogger.foobar('some foobar level-ed message');
    

    多个传输

    如果需要把成功级别的消息传入一个log文件,错误的传入另一个log文件,那么可以这么使用

    由level针对级别。

    
    const logger = winston.createLogger({
    
    transports: [
    
    new winston.transports.File({
    
    filename: 'combined.log',
    
    level: 'info'
    
    }),
    
    new winston.transports.File({
    
    filename: 'errors.log',
    
    level: 'error'
    
    })
    
    ]
    
    });
    

    常见的transport

    每个不同的transport都可以使用不同的format参数。毕竟要写入的格式与内容不同。

    
    const logger = winston.createLogger({
    
    transports: [
    
    new winston.transports.File({
    
    filename: 'error.log',
    
    level: 'error',
    
    format: winston.format.json()
    
    }),
    
    new transports.Http({
    
    level: 'warn',
    
    format: winston.format.json()
    
    }),
    
    new transports.Console({
    
    level: 'info',
    
    format: winston.format.combine(
    
    winston.format.colorize(),
    
    winston.format.simple()
    
    )
    
    })
    
    ]
    
    });
    

    异常处理

    winston的异常处理。

    Handling Uncaught Exceptions with winston

    未捕获的异常,winston可以处理。

    
    const logger = createLogger({
    
    transports: [
    
    new transports.File({ filename: 'combined.log' })
    
    ],
    
    exceptionHandlers: [
    
    new transports.File({ filename: 'exceptions.log' })
    
    ]
    
    });
    

    阅读更多

    winston将异常保留在对应的log文件中后会退出

    设置exitOnError: false,可以取消退出。

    未捕获的promise

    使用rejectionHandlers

    
    const { createLogger, transports } = require('winston');
    
    // Enable rejection handling when you create your logger.
    
    const logger = createLogger({
    
    transports: [
    
    new transports.File({ filename: 'combined.log' })
    
    ],
    
    rejectionHandlers: [
    
    new transports.File({ filename: 'rejections.log' })
    
    ]
    
    });
    

    阅读更多

    查看原文

    赞 0 收藏 0 评论 0

    认证与成就

    • 获得 40 次点赞
    • 获得 5 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 5 枚铜徽章

    擅长技能
    编辑

    开源项目 & 著作
    编辑

    (゚∀゚ )
    暂时没有

    注册于 2016-10-24
    个人主页被 1.8k 人浏览