颜海镜

颜海镜 查看完整档案

北京编辑华北电力大学(保定)  |  软件工程 编辑美团点评  |  前端 编辑 yanhaijing.com 编辑
编辑

知名技术博主,开源达人,《React状态管理与同构实战》作者,http://yanhaijing.com

个人动态

颜海镜 赞了文章 · 10月21日

图解九种常见的设计模式

在软件工程中,设计模式(Design Pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案。根据模式的目的来划分的话,GoF(Gang of Four)设计模式可以分为以下 3 种类型:

image

1、创建型模式:用来描述 “如何创建对象”,它的主要特点是 “将对象的创建和使用分离”。包括单例、原型、工厂方法、抽象工厂和建造者 5 种模式。

2、结构型模式:用来描述如何将类或对象按照某种布局组成更大的结构。包括代理、适配器、桥接、装饰、外观、享元和组合 7 种模式。

3、行为型模式:用来识别对象之间的常用交流模式以及如何分配职责。包括模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录和解释器 11 种模式。

接下来阿宝哥将结合一些生活中的场景并通过精美的配图,来向大家介绍 9 种常用的设计模式。

一、建造者模式

建造者模式(Builder Pattern)将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。

一辆小汽车 🚗 通常由 发动机、底盘、车身和电气设备 四大部分组成。汽车电气设备的内部构造很复杂,简单起见,我们只考虑三个部分:引擎、底盘和车身。

image

在现实生活中,小汽车也是由不同的零部件组装而成,比如上图中我们把小汽车分成引擎、底盘和车身三大部分。下面我们来看一下如何使用建造者模式来造车子。

1.1 实现代码
class Car {
  constructor(
    public engine: string,
    public chassis: string, 
    public body: string
  ) {}
}

class CarBuilder {
  engine!: string; // 引擎
  chassis!: string; // 底盘
  body!: string; // 车身

  addChassis(chassis: string) {
    this.chassis = chassis;
    return this;
  }

  addEngine(engine: string) {
    this.engine = engine;
    return this;
  }

  addBody(body: string) {
    this.body = body;
    return this;
  }

  build() {
    return new Car(this.engine, this.chassis, this.body);
  }
}

在以上代码中,我们定义一个 CarBuilder 类,并提供了 addChassisaddEngineaddBody 3 个方法用于组装车子的不同部位,当车子的 3 个部分都组装完成后,调用 build 方法就可以开始造车。

1.2 使用示例
const car = new CarBuilder()
  .addEngine('v12')
  .addBody('镁合金')
  .addChassis('复合材料')
  .build();
1.3 应用场景及案例
  • 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性。
  • 需要生成的产品对象的属性相互依赖,需要指定其生成顺序。
  • 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。
  • Github - node-sql-queryhttps://github.com/dresende/n...

二、工厂模式

在现实生活中,工厂是负责生产产品的,比如牛奶、面包或礼物等,这些产品满足了我们日常的生理需求。

image

在众多设计模式当中,有一种被称为工厂模式的设计模式,它提供了创建对象的最佳方式。工厂模式可以分为:简单工厂模式、工厂方法模式和抽象工厂模式

2.1 简单工厂

简单工厂模式又叫 静态方法模式,因为工厂类中定义了一个静态方法用于创建对象。简单工厂让使用者不用知道具体的参数就可以创建出所需的 ”产品“ 类,即使用者可以直接消费产品而不需要知道产品的具体生产细节。

image

在上图中,阿宝哥模拟了用户购车的流程,小王和小秦分别向 BMW 工厂订购了 BMW730 和 BMW840 型号的车型,接着工厂会先判断用户选择的车型,然后按照对应的模型进行生产并在生产完成后交付给用户。

下面我们来看一下如何使用简单工厂来描述 BMW 工厂生产指定型号车子的过程。

2.1.1 实现代码
abstract class BMW {
  abstract run(): void;
}

class BMW730 extends BMW {
  run(): void {
    console.log("BMW730 发动咯");
  }
}

class BMW840 extends BMW {
  run(): void {
    console.log("BMW840 发动咯");
  }
}

class BMWFactory {
  public static produceBMW(model: "730" | "840"): BMW {
    if (model === "730") {
      return new BMW730();
    } else {
      return new BMW840();
    }
  }
}

在以上代码中,我们定义一个 BMWFactory 类,该类提供了一个静态的 produceBMW() 方法,用于根据不同的模型参数来创建不同型号的车子。

2.1.2 使用示例
const bmw730 = BMWFactory.produceBMW("730");
const bmw840 = BMWFactory.produceBMW("840");

bmw730.run();
bmw840.run();
2.1.3 应用场景
  • 工厂类负责创建的对象比较少:由于创建的对象比较少,不会造成工厂方法中业务逻辑过于复杂。
  • 客户端只需知道传入工厂类静态方法的参数,而不需要关心创建对象的细节。

2.2 工厂方法

工厂方法模式(Factory Method Pattern)又称为工厂模式,也叫多态工厂(Polymorphic Factory)模式,它属于类创建型模式。

在工厂方法模式中,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象, 这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类。

image

在上图中,阿宝哥模拟了用户购车的流程,小王和小秦分别向 BMW 730 和 BMW 840 工厂订购了 BMW730 和 BMW840 型号的车子,接着工厂按照对应的模型进行生产并在生产完成后交付给用户。

同样,我们来看一下如何使用工厂方法来描述 BMW 工厂生产指定型号车子的过程。

2.2.1 实现代码
abstract class BMWFactory {
  abstract produceBMW(): BMW;
}

class BMW730Factory extends BMWFactory {
  produceBMW(): BMW {
    return new BMW730();
  }
}

class BMW840Factory extends BMWFactory {
  produceBMW(): BMW {
    return new BMW840();
  }
}

在以上代码中,我们分别创建了 BMW730FactoryBMW840Factory 两个工厂类,然后使用这两个类的实例来生产不同型号的车子。

2.2.2 使用示例
const bmw730Factory = new BMW730Factory();
const bmw840Factory = new BMW840Factory();

const bmw730 = bmw730Factory.produceBMW();
const bmw840 = bmw840Factory.produceBMW();

bmw730.run();
bmw840.run();
2.2.3 应用场景
  • 一个类不知道它所需要的对象的类:在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可,具体的产品对象由具体工厂类创建;客户端需要知道创建具体产品的工厂类。
  • 一个类通过其子类来指定创建哪个对象:在工厂方法模式中,对于抽象工厂类只需要提供一个创建产品的接口,而由其子类来确定具体要创建的对象,利用面向对象的多态性和里氏代换原则,在程序运行时,子类对象将覆盖父类对象,从而使得系统更容易扩展。
继续阅读:Typescript 设计模式之工厂方法

2.3 抽象工厂

抽象工厂模式(Abstract Factory Pattern),提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。

在工厂方法模式中具体工厂负责生产具体的产品,每一个具体工厂对应一种具体产品,工厂方法也具有唯一性,一般情况下,一个具体工厂中只有一个工厂方法或者一组重载的工厂方法。 但是有时候我们需要一个工厂可以提供多个产品对象,而不是单一的产品对象。

image

在上图中,阿宝哥模拟了用户购车的流程,小王向 BMW 工厂订购了 BMW730,工厂按照 730 对应的模型进行生产并在生产完成后交付给小王。而小秦向同一个 BMW 工厂订购了 BMW840,工厂按照 840 对应的模型进行生产并在生产完成后交付给小秦。

下面我们来看一下如何使用抽象工厂来描述上述的购车过程。

2.3.1 实现代码
abstract class BMWFactory {
  abstract produce730BMW(): BMW730;
  abstract produce840BMW(): BMW840;
}

class ConcreteBMWFactory extends BMWFactory {
  produce730BMW(): BMW730 {
    return new BMW730();
  }

  produce840BMW(): BMW840 {
    return new BMW840();
  }
}
2.3.2 使用示例
const bmwFactory = new ConcreteBMWFactory();

const bmw730 = bmwFactory.produce730BMW();
const bmw840 = bmwFactory.produce840BMW();

bmw730.run();
bmw840.run();
2.3.3 应用场景
  • 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。
  • 系统中有多于一个的产品族,而每次只使用其中某一产品族。
  • 系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。
继续阅读:创建对象的最佳方式是什么?

三、单例模式

单例模式(Singleton Pattern)是一种常用的模式,有一些对象我们往往只需要一个,比如全局缓存、浏览器中的 window 对象等。单例模式用于保证一个类仅有一个实例,并提供一个访问它的全局访问点。

image

在上图中,阿宝哥模拟了借车的流程,小王临时有急事找阿宝哥借车子,阿宝哥家的车子刚好没用,就借给小王了。当天,小秦也需要用车子,也找阿宝哥借车,因为阿宝哥家里只有一辆车子,所以就没有车可借了。

对于车子来说,它虽然给生活带来了很大的便利,但养车也需要一笔不小的费用(车位费、油费和保养费等),所以阿宝哥家里只有一辆车子。在开发软件系统时,如果遇到创建对象时耗时过多或耗资源过多,但又经常用到的对象,我们就可以考虑使用单例模式。

下面我们来看一下如何使用 TypeScript 来实现单例模式。

3.1 实现代码
class Singleton {
  // 定义私有的静态属性,来保存对象实例
  private static singleton: Singleton;
  private constructor() {}

  // 提供一个静态的方法来获取对象实例
  public static getInstance(): Singleton {
    if (!Singleton.singleton) {
      Singleton.singleton = new Singleton();
    }
    return Singleton.singleton;
  }
}
3.2 使用示例
let instance1 = Singleton.getInstance();
let instance2 = Singleton.getInstance();

console.log(instance1 === instance2); // true
3.3 应用场景
  • 需要频繁实例化然后销毁的对象。
  • 创建对象时耗时过多或耗资源过多,但又经常用到的对象。
  • 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
继续阅读:TypeScript 设计模式之单例模式

四、适配器模式

在实际生活中,也存在适配器的使用场景,比如:港式插头转换器、电源适配器和 USB 转接口。而在软件工程中,适配器模式的作用是解决两个软件实体间的接口不兼容的问题。 使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体就可以一起工作。

image

4.1 实现代码
interface Logger {
  info(message: string): Promise<void>;
}

interface CloudLogger {
  sendToServer(message: string, type: string): Promise<void>;
}

class AliLogger implements CloudLogger {
  public async sendToServer(message: string, type: string): Promise<void> {
    console.info(message);
    console.info('This Message was saved with AliLogger');
  }
}

class CloudLoggerAdapter implements Logger {
  protected cloudLogger: CloudLogger;

  constructor (cloudLogger: CloudLogger) {
    this.cloudLogger = cloudLogger;
  }

  public async info(message: string): Promise<void> {
    await this.cloudLogger.sendToServer(message, 'info');
  }
}

class NotificationService {
  protected logger: Logger;
  
  constructor (logger: Logger) {    
    this.logger = logger;
  }

  public async send(message: string): Promise<void> {
    await this.logger.info(`Notification sended: ${message}`);
  }
}

在以上代码中,因为 LoggerCloudLogger 这两个接口不匹配,所以我们引入了 CloudLoggerAdapter 适配器来解决兼容性问题。

4.2 使用示例
(async () => {
  const aliLogger = new AliLogger();
  const cloudLoggerAdapter = new CloudLoggerAdapter(aliLogger);
  const notificationService = new NotificationService(cloudLoggerAdapter);
  await notificationService.send('Hello semlinker, To Cloud');
})();
4.3 应用场景及案例
继续阅读:TypeScript 设计模式之适配器模式

五、观察者模式 & 发布订阅模式

5.1 观察者模式

观察者模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使得它们能够自动更新自己。

在观察者模式中有两个主要角色:Subject(主题)和 Observer(观察者)。

image

在上图中,Subject(主题)就是阿宝哥的 TS 专题文章,而观察者就是小秦和小王。由于观察者模式支持简单的广播通信,当消息更新时,会自动通知所有的观察者。

下面我们来看一下如何使用 TypeScript 来实现观察者模式。

5.1.1 实现代码
interface Observer {
  notify: Function;
}

class ConcreteObserver implements Observer{
  constructor(private name: string) {}

  notify() {
    console.log(`${this.name} has been notified.`);
  }
}

class Subject { 
  private observers: Observer[] = [];

  public addObserver(observer: Observer): void {
    console.log(observer, "is pushed!");
    this.observers.push(observer);
  }

  public deleteObserver(observer: Observer): void {
    console.log("remove", observer);
    const n: number = this.observers.indexOf(observer);
    n != -1 && this.observers.splice(n, 1);
  }

  public notifyObservers(): void {
    console.log("notify all the observers", this.observers);
    this.observers.forEach(observer => observer.notify());
  }
}
5.1.2 使用示例
const subject: Subject = new Subject();
const xiaoQin = new ConcreteObserver("小秦");
const xiaoWang = new ConcreteObserver("小王");
subject.addObserver(xiaoQin);
subject.addObserver(xiaoWang);
subject.notifyObservers();

subject.deleteObserver(xiaoQin);
subject.notifyObservers();
5.1.3 应用场景及案例
继续阅读:TypeScript 设计模式之观察者模式

5.2 发布订阅模式

在软件架构中,发布/订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,然后分别发送给不同的订阅者。 同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者存在。

在发布订阅模式中有三个主要角色:Publisher(发布者)、 Channels(通道)和 Subscriber(订阅者)。

image

在上图中,Publisher(发布者)是阿宝哥,Channels(通道)中 Topic A 和 Topic B 分别对应于 TS 专题和 Deno 专题,而 Subscriber(订阅者)就是小秦、小王和小池。

下面我们来看一下如何使用 TypeScript 来实现发布订阅模式。

5.2.1 实现代码
type EventHandler = (...args: any[]) => any;

class EventEmitter {
  private c = new Map<string, EventHandler[]>();

  // 订阅指定的主题
  subscribe(topic: string, ...handlers: EventHandler[]) {
    let topics = this.c.get(topic);
    if (!topics) {
      this.c.set(topic, topics = []);
    }
    topics.push(...handlers);
  }

  // 取消订阅指定的主题
  unsubscribe(topic: string, handler?: EventHandler): boolean {
    if (!handler) {
      return this.c.delete(topic);
    }

    const topics = this.c.get(topic);
    if (!topics) {
      return false;
    }
    
    const index = topics.indexOf(handler);

    if (index < 0) {
      return false;
    }
    topics.splice(index, 1);
    if (topics.length === 0) {
      this.c.delete(topic);
    }
    return true;
  }

  // 为指定的主题发布消息
  publish(topic: string, ...args: any[]): any[] | null {
    const topics = this.c.get(topic);
    if (!topics) {
      return null;
    }
    return topics.map(handler => {
      try {
        return handler(...args);
      } catch (e) {
        console.error(e);
        return null;
      }
    });
  }
}
5.2.2 使用示例
const eventEmitter = new EventEmitter();
eventEmitter.subscribe("ts", (msg) => console.log(`收到订阅的消息:${msg}`) );

eventEmitter.publish("ts", "TypeScript发布订阅模式");
eventEmitter.unsubscribe("ts");
eventEmitter.publish("ts", "TypeScript发布订阅模式");
5.2.3 应用场景
继续阅读:如何优雅的实现消息通信?

六、策略模式

策略模式(Strategy Pattern)定义了一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。策略模式的重心不是如何实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活、可维护、可扩展。

image

目前在一些主流的 Web 站点中,都提供了多种不同的登录方式。比如账号密码登录、手机验证码登录和第三方登录。为了方便维护不同的登录方式,我们可以把不同的登录方式封装成不同的登录策略。

下面我们来看一下如何使用策略模式来封装不同的登录方式。

6.1 实现代码

为了更好地理解以下代码,我们先来看一下对应的 UML 类图:

image

interface Strategy {
  authenticate(...args: any): any;
}

class Authenticator {
  strategy: any;
  constructor() {
    this.strategy = null;
  }

  setStrategy(strategy: any) {
    this.strategy = strategy;
  }

  authenticate(...args: any) {
    if (!this.strategy) {
      console.log('尚未设置认证策略');
      return;
    }
    return this.strategy.authenticate(...args);
  }
}

class WechatStrategy implements Strategy {
  authenticate(wechatToken: string) {
    if (wechatToken !== '123') {
      console.log('无效的微信用户');
      return;
    }
    console.log('微信认证成功');
  }
}

class LocalStrategy implements Strategy {
  authenticate(username: string, password: string) {
    if (username !== 'abao' && password !== '123') {
      console.log('账号或密码错误');
      return;
    }
    console.log('账号和密码认证成功');
  }
}
6.2 使用示例
const auth = new Authenticator();

auth.setStrategy(new WechatStrategy());
auth.authenticate('123456');

auth.setStrategy(new LocalStrategy());
auth.authenticate('abao', '123');
6.3 应用场景及案例

七、职责链模式

职责链模式是使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系。在职责链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。

image

在公司中不同的岗位拥有不同的职责与权限。以上述的请假流程为例,当阿宝哥请 1 天假时,只要组长审批就可以了,不需要流转到主管和总监。如果职责链上的某个环节无法处理当前的请求,若含有下个环节,则会把请求转交给下个环节来处理。

在日常的软件开发过程中,对于职责链来说,一种常见的应用场景是中间件,下面我们来看一下如何利用职责链来处理请求。

7.1 实现代码

为了更好地理解以下代码,我们先来看一下对应的 UML 类图:

image

interface IHandler {
  addMiddleware(h: IHandler): IHandler;
  get(url: string, callback: (data: any) => void): void;
}

abstract class AbstractHandler implements IHandler {
  next!: IHandler;
  addMiddleware(h: IHandler) {
    this.next = h;
    return this.next;
  }

  get(url: string, callback: (data: any) => void) {
    if (this.next) {
      return this.next.get(url, callback);
    }
  }
}

// 定义Auth中间件
class Auth extends AbstractHandler {
  isAuthenticated: boolean;
  constructor(username: string, password: string) {
    super();

    this.isAuthenticated = false;
    if (username === 'abao' && password === '123') {
      this.isAuthenticated = true;
    }
  }

  get(url: string, callback: (data: any) => void) {
    if (this.isAuthenticated) {
      return super.get(url, callback);
    } else {
      throw new Error('Not Authorized');
    }
  }
}

// 定义Logger中间件
class Logger extends AbstractHandler {
  get(url: string, callback: (data: any) => void) {
    console.log('/GET Request to: ', url);
    return super.get(url, callback);
  }
}

class Route extends AbstractHandler {
  URLMaps: {[key: string]: any};
  constructor() {
    super();
    this.URLMaps = {
      '/api/todos': [{ title: 'learn ts' }, { title: 'learn react' }],
      '/api/random': Math.random(),
    };
  }

  get(url: string, callback: (data: any) => void) {
    super.get(url, callback);

    if (this.URLMaps.hasOwnProperty(url)) {
      callback(this.URLMaps[url]);
    }
  }
}
7.2 使用示例
const route = new Route();
route.addMiddleware(new Auth('abao', '123')).addMiddleware(new Logger());

route.get('/api/todos', data => {
  console.log(JSON.stringify({ data }, null, 2));
});

route.get('/api/random', data => {
  console.log(data);
});
7.3 应用场景
  • 可处理一个请求的对象集合应被动态指定。
  • 想在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
  • 有多个对象可以处理一个请求,哪个对象处理该请求运行时自动确定,客户端只需要把请求提交到链上即可。

八、模板方法模式

模板方法模式由两部分结构组成:抽象父类和具体的实现子类。通常在抽象父类中封装了子类的算法框架,也包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

image

在上图中,阿宝哥通过使用不同的解析器来分别解析 CSV 和 Markup 文件。虽然解析的是不同的类型的文件,但文件的处理流程是一样的。这里主要包含读取文件、解析文件和打印数据三个步骤。针对这个场景,我们就可以引入模板方法来封装以上三个步骤的处理顺序。

下面我们来看一下如何使用模板方法来实现上述的解析流程。

8.1 实现代码

为了更好地理解以下代码,我们先来看一下对应的 UML 类图:

image

import fs from 'fs';

abstract class DataParser {
  data: string = '';
  out: any = null;

  // 这就是所谓的模板方法
  parse(pathUrl: string) {
    this.readFile(pathUrl);
    this.doParsing();
    this.printData();
  }

  readFile(pathUrl: string) {
    this.data = fs.readFileSync(pathUrl, 'utf8');
  }

  abstract doParsing(): void;
  
  printData() {
    console.log(this.out);
  }
}

class CSVParser extends DataParser {
  doParsing() {
    this.out = this.data.split(',');
  }
}

class MarkupParser extends DataParser {
  doParsing() {
    this.out = this.data.match(/<\w+>.*<\/\w+>/gim);
  }
}
8.2 使用示例
const csvPath = './data.csv';
const mdPath = './design-pattern.md';

new CSVParser().parse(csvPath);
new MarkupParser().parse(mdPath);
8.3 应用场景
  • 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
  • 当需要控制子类的扩展时,模板方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展。

九、参考资源

十、推荐阅读

查看原文

赞 49 收藏 40 评论 2

颜海镜 赞了文章 · 10月21日

Angular 中自定义 Debounce Click 指令

在这篇文章中,我们将介绍使用 Angular Directive API 来创建自定义 debounce click 指令。该指令将处理在指定时间内多次点击事件,这有助于防止重复的操作。

对于我们的示例,我们希望在产生点击事件时,实现去抖动处理。接下来我们将介绍 Directive API,HostListener API 和 RxJS 中 debounceTime 操作符的相关知识。首先,我们需要创建 DebounceClickDirective 指令并将其注册到我们的 app.module.ts 文件中:

import { Directive, OnInit } from '@angular/core';

@Directive({
  selector: '[appDebounceClick]'
})
export class DebounceClickDirective implements OnInit {
  constructor() { }
  ngOnInit() { }
}


@NgModule({
  imports: [BrowserModule],
  declarations: [
    AppComponent,
    DebounceClickDirective
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Angular 指令是没有模板的组件,我们将使用以下方式应用上面的自定义指令:

<button appDebounceClick>Debounced Click</button>

在上面 HTML 代码中的宿主元素是按钮,接下来我们要做的第一件事就是监听宿主元素的点击事件,因此我们可以将以下代码添加到我们的自定义指令中。

import { Directive, HostListener, OnInit } from '@angular/core';

@Directive({
  selector: '[appDebounceClick]'
})
export class DebounceClickDirective implements OnInit {
  constructor() { }

  ngOnInit() { }

  @HostListener('click', ['$event'])
  clickEvent(event: MouseEvent) {
    event.preventDefault();
    event.stopPropagation();
    console.log('Click from Host Element!');
  }
}

在上面的例子中,我们使用了 Angular @HostListener 装饰器,该装饰器允许你轻松地监听宿主元素上的事件。在我们的示例中,第一个参数是事件名。第二个参数 $event,这用于告诉 Angular 将点击事件传递给我们的 clickEvent() 方法。

在事件处理函数中,我们可以调用 event.preventDefault()event.stopPropagation() 方法来阻止浏览器的默认行为和事件冒泡。

Debounce Events

现在我们可以拦截宿主元素的 click 事件,此时我们还需要有一种方法实现事件的去抖动处理,然后将它重新发送回父节点。这时我们需要借助事件发射器和 RxJS 中的 debounce 操作符。

import { Directive, EventEmitter, HostListener, OnInit, Output } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/debounceTime';

@Directive({
    selector: '[appDebounceClick]'
})
export class DebounceClickDirective implements OnInit {
    @Output() debounceClick = new EventEmitter();
    private clicks = new Subject<any>();

    constructor() { }

    ngOnInit() {
        this.clicks
            .debounceTime(500)
            .subscribe(e => this.debounceClick.emit(e));
    }

    @HostListener('click', ['$event'])
    clickEvent(event: MouseEvent) {
        event.preventDefault();
        event.stopPropagation();
        this.clicks.next(event);
    }
}

在上面的代码中,我们使用 Angular @Output 属性装饰器和 EventEmitter 类,它们允许我们在指令上创建自定义事件。要发出事件,我们需要调用 EventEmitter 实例上的 emit() 方法。

但我们不想立即发出点击事件,我们想做去抖动处理。为了实现这个功能,我们将使用 RxJS 中的 Subject 类。在我们的代码中,我们创建一个主题来处理我们的点击事件。在我们的方法中,我们调用 next() 方法来让 Subject 对象发出下一个值。此外我们也使用 RxJS 中 debounceTime 的操作符,这允许我们通过设置给定的毫秒数来去抖动点击事件。

一旦我们设置好了,我们现在可以在下面的模板中监听我们的自定义去抖动点击事件。

<button appDebounceClick (debounceClick)="log($event)">
  Debounced Click
</button>

现在,当我们点击我们的按钮时,它将延迟 500 毫秒。 500毫秒后,我们的自定义输出属性将会发出点击事件。现在我们有了基本的功能,我们需要做一些清理工作,并增加一些其它的功能。

Unsubscribe

对于 RxJS 中 Observables 和 Subject 对象,一旦我们不再使用它们,我们必须取消订阅事件。如果我们没有执行取消订阅操作,有可能会出现内存泄漏。

import { Directive, EventEmitter, HostListener, OnInit, Output, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Subscription } from "rxjs/Subscription";
import 'rxjs/add/operator/debounceTime';

@Directive({
    selector: '[appDebounceClick]'
})
export class DebounceClickDirective implements OnInit, OnDestroy {
    @Output() debounceClick = new EventEmitter();
    private clicks = new Subject<any>();
    private subscription: Subscription;

    constructor() { }

    ngOnInit() {
        this.subscription = this.clicks
            .debounceTime(500)
            .subscribe(e => this.debounceClick.emit(e));
    }

    ngOnDestroy() {
        this.subscription.unsubscribe();
    }

    @HostListener('click', ['$event'])
    clickEvent(event: MouseEvent) {
        event.preventDefault();
        event.stopPropagation();
        this.clicks.next(event);
    }
}

要取消订阅,我们需要保存订阅时返回的订阅对象。当 Angular 销毁组件时,它将调用 OnDestroy 生命周期钩子,因此我们可以在这个钩子中,执行取消订阅操作。

Custom Inputs

我们指令的功能已基本齐全,它可以正常处理事件。接下来,我们将添加一些更多的逻辑,以便我们可以自定义去抖动时间。为此,我们将使用 @Input 装饰器。

import { Directive, EventEmitter, HostListener, OnInit, Output, OnDestroy, Input } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Subscription } from "rxjs/Subscription";
import 'rxjs/add/operator/debounceTime';

@Directive({
    selector: '[appDebounceClick]'
})
export class DebounceClickDirective implements OnInit, OnDestroy {
    @Input() debounceTime = 500;
    @Output() debounceClick = new EventEmitter();
    private clicks = new Subject<any>();
    private subscription: Subscription;

    constructor() { }

    ngOnInit() {
        this.subscription = this.clicks
            .debounceTime(this.debounceTime)
            .subscribe(e => this.debounceClick.emit(e));
    }

    ngOnDestroy() {
        this.subscription.unsubscribe();
    }

    @HostListener('click', ['$event'])
    clickEvent(event: MouseEvent) {
        event.preventDefault();
        event.stopPropagation();
        this.clicks.next(event);
    }
}

@Input 装饰器允许我们将自定义延迟时间传递到我们的组件或指令中。在上面的代码中,我们可以通过组件的输入属性,来指定我们希望去抖动的时间。默认情况下,我们将其设置为 500 毫秒。

<button appDebounceClick (debounceClick)="log($event)" [debounceTime]="300">
 Debounced Click
</button>

参考资源

查看原文

赞 10 收藏 9 评论 6

颜海镜 赞了文章 · 8月16日

如何从0开发一个Atom组件

最近用Atom写博客比较多,然后发现一个很严重的问题。。
没有一个我想要的上传图片的方式,比如某乎上边就可以直接copy/paste文件,然后进行上传。
然而在Atom上没有找到类似的插件,最接近的一个,也还是需要手动选择文件,然后进行上传。
这个操作流程太繁琐,索性自己写一个插件用好了。

成品插件下载地址:https://atom.io/packages/atom-image-uploader

规划

首先,我们确定了需求,要通过可以直接copy文件,然后在Atom中paste即可完成上传的操作。
确定了以后,我们就要开始搬砖了。

插件开发

因为Atom是一个Electron应用:https://electronjs.org

是使用JavaScript来开发的桌面应用,所以对于一个前端来说,简直是太美好了。
我们先去翻看Atom的官方文档,查看关于创建插件相关的操作:
首先我们在Atom中打开命令面板,然后输入Generate Package

按下回车后,将会弹出一个对话框,在框中输入要建立的包名即可完成一个Package的创建。

Atom会生成一套默认文件,并打开一个新的窗口。

项目结构

生成的插件目录如下:

.
├── keymaps
│   └── first-package.json
├── lib
│   ├── first-package-view.js
│   └── first-package.js
├── menus
│   └── first-package.json
├── package.json
├── spec
│   ├── first-package-spec.js
│   └── first-package-view-spec.js
└── styles
    └── first-package.less

keymaps

这里可以配置要监听的快捷键,我们可以设置一些自定义快捷键来触发一些我们插件的行为。

{
  "atom-workspace": {
    "ctrl-alt-o": "first-package:toggle"
  }
}

我们可以添加各种自定义的快捷键在这里。
Value的定义为:包名:触发的事件名
需要注意的是:
这里配置的快捷键还有一个作用域的概念。也就是JSON外边的那个key
atom-workspace表示在Atom中生效
atom-text-editor表示只在文本编辑器范围内生效。

Atom官方文档

lib

这里就是存放插件主要代码的地方了。
默认会生成两个文件:

  1. package.js
  2. package.view.js

默认插件生成的主入口文件指向这里。

入口文件的表现方式为一个JSON对象,可以实现如下几个函数:

  1. activate: 当Package被激活时会执行该方法,函数的签名表示会接受一个state参数,该参数是通过serialize方法传递过来的(如果有实现它的话)
  2. deactivate: 当Package失效时会出发的方法,这两个方法可以理解为React中的componentWillMountcomponentWillUnmount
  3. serialize: 也就是上边说到的那个方法,可以返回一个JSON对象供下次激活后使用
  4. 自定义快捷键对应的事件名: 每次Package被触发对应快捷键时都会执行的方法

menus

这里存放的是在应用菜单和编辑区域菜单栏的配置文件

{
  "context-menu": {
    "atom-text-editor": [
      {
        "label": "Toggle first-package",
        "command": "first-package:toggle"
      }
    ]
  },
  "menu": [
    {
      "label": "Packages",
      "submenu": [
        {
          "label": "first-package",
          "submenu": [
            {
              "label": "Toggle",
              "command": "first-package:toggle"
            }
          ]
        }
      ]
    }
  ]
}

context-menu对应的元素会在对应的区域内右键触发时显示。
menu则是出现在Atom主菜单栏上:

同样的,context-menu会区分两个环境,text-editorworkspace

spec

这里存放的是一些测试用例,创建Package会生成一些默认的断言。
写测试确实是一个好习惯。

styles

如果Package有很多View要展示的话,可以在这里编写,默认使用的是Less语法。
由于我们只做一个C/V的操作,不会涉及到界面,所以styles直接就删掉了。

开始搬砖

大致结构已经了解了,我们就可以开始搬砖了。
因为是一个Electron应用,所以我们直接在Atom中按下alt + command + i,呼出我们熟悉的控制台界面。

Atom是不会把Electron的各种文档重新写一遍的,所以我们现在控制台里边试一下我们的猜测是否正确。
一些想要的东西是否存在。

经过验证确定了,Electronclipboard对象可以直接在Atom中使用,这就很开心了。

require('electron').clipboard.readImage().toPng()

这样我们就拿到剪切板中的图片数据了,一个二进制的数组对象。
我们在触发Paste操作时,从clipboard中获取,如果剪切板中是图片的话,我们就将它上传并显示到编辑器中。
所以,接下来我们要做的就是:

  1. 进行上传图片的操作
  2. 将上传后的图片显示到编辑器中

上传图片

上传图片我们选择的是七牛,我们选择七牛来作为图床使用,因为他家提供了10GB的免费存储,灰常适合自己这样的笔记型博客。
但是用他家SDK时发现一个问题。。我将二进制数据转换为ReadStream后上传的资源损坏了-.-目前还没有找到原因。
所以我们做了曲线救国的方式。
将剪切板中的数据转换为Buffer然后暂存到本地,通过本地文件的方式来进行上传七牛。
在操作完成后我们再将临时文件移除。

try {
  let buffer = clipboard.readImage().toPng()
  let tempFilePath = 'XXX'
  fs.writeFileSync(tempFilePath, Buffer.from(buffer))
} catch (e) {
  // catch error
} finally {
  fs.unlink(tempFilePath) // 因为我们并不依赖于删除成功的回调,所以直接空调用异步方法即可
}

将上传后的资源显示到编辑器中

因为考虑到上传可能会受到网络影响,从而上传时间不可预估。
所以我们会先在文件中显示一部分占位文字。
通过全局的atom对象可以拿到当前活跃的窗口:

let editor = atom.workspace.getActiveTextEditor()

为了避免同时上传多张图片时出现问题,我们将临时文件名作为填充的一部分。

editor.insertText(`![](${placeHolderText})`, editor)

然后在上传成功后,我们将对应的填充字符替换为上传后的URL就可以了。

editor.scan(new RegExp(placeHolderText), tools => tools.replace(url))

scan方法接收一个正则对象和回调函数。
我们将前边用到的占位文本作为正则对象,然后在回调将其替换为上传后的url
至此,我们的代码已经编写完了,剩下的就是一些交互上的优化。

完成后的效果图:

以及,最后:我们要进行Package的上传。

上传开发完的Package

首先我们需要保证package.json中存在如下几个参数:

  1. name
  2. description
  3. repository

我们可以先使用如下命令来检查包名是否冲突。

apm show 你的包名

如果没有冲突,我们就可以直接执行以下命令进行上传了。

apm publish 你的包名

后续的代码修改,只需在该包的目录下执行:

apm publish

一些可选的参数:

  1. major,增加版本号的第一位1.0.0 -> 2.0.0
  2. minor,增加版本号的第二位0.1.0 -> 0.2.0
  3. patch,增加版本号的第三位0.0.1 -> 0.0.2

通过apm help可以获取到更多的帮助信息。

以上,就是开发一个Atom插件的完整流程咯。

参考资料

hacking-atom
electron-doc

查看原文

赞 5 收藏 9 评论 2

颜海镜 赞了文章 · 8月8日

VSCode插件开发梳理

纸上得来终觉浅,深知此事要躬行,因此来体验一下VSCode的插件开发,网上有很多小伙伴的文章给了很大帮助,本文的代码是直接使用网上的内容,特此整理出来给后来学习的伙伴们参考

https://www.cnblogs.com/lianm...
http://www.cnblogs.com/Leo_wl...

搭建环境

首先Nodejs和VSCode需要提前安装好,这里对这块的安装不再赘述。接下来安装yo 这是vscode团队为插件开发提供的工具

npm install -g yo generator-code

安装成功后,通过yo code生成插件开发项目,这里官方推荐使用typescript,当然我们更熟悉javascript,其余的根据情况默认即可

clipboard.png

一路生成后,通过VSCode打开(File->Open Folder)刚生成的插件项目,在这一堆文件中,我们只关心两个重点,extension.js 是插件的入口文件,package.json包含插件的配置信息

准备完成后,为了先验证下插件项目正常OK,在VSCode中F5运行(或Debug->start)如果你可以看到VSCode又启动了一个窗口运行插件项目,shift+ctrl+p 输入Hello World如果在右下角能看到Hello World的提示信息就OK 了

clipboard.png

如果在编辑插件时内容做了变更,在运行的窗口只需通过Ctrl+r 即可刷新,无需关闭重新运行

插件内容

这里就开始写插件具体的内容了,发挥创造想象力的时候到了,但是一开始无从下手,可以参考下官网的文档,写的很是详细,虽然是英文的但是直接翻译读起来也没特别怪

官方入门示例一:https://code.visualstudio.com...

这里的插件内容是根据官网示例二的Markdown字数统计做了修改后的内容,可以统计任意文件中所编写的字符数量

新建wordCounter.js

class WordCounter {
    constructor(_vscode) {        //构造函数,传入vscode对象
        this.vscode = _vscode;
        this.init();
    }
 
    init() {                      //初始化
        var vscode = this.vscode;
        var StatusBarAlignment = vscode.StatusBarAlignment;
        var window = this.vscode.window;
 
        //statusBar,是需要手动释放的
        this.statusBar = window.createStatusBarItem(StatusBarAlignment.Left);
 
        //跟注册事件相配合的数组,事件的注册,也是需要释放的
        var disposable = [];
        //事件在注册的时候,会自动填充一个回调的dispose到数组
        window.onDidChangeTextEditorSelection(this.updateText, this, disposable);
 
        //保存需要释放的资源
        this.disposable = vscode.Disposable.from(disposable);
 
        this.updateText();
        this.statusBar.show();
    }
 
    updateText() {       //现在快凌晨两点,偷个懒早点睡,临时改成字符数量了。
        var window = this.vscode.window;
        this.editor = window.activeTextEditor;
        var content = this.editor.document.getText();
        var len = content.replace(/[\r\n\s]+/g, '').length;
        this.statusBar.text = `啦啦啦...已经敲了${len}个字符了`;
    }
 
    dispose() {  //实现dispose方法
        this.disposable.dispose();
        this.statusBar.dispose();
    }
}
 
module.exports = WordCounter;

修改extension.js

// vscode这个包,包含了里面所有的api
const vscode = require('vscode');
// 在插件被激活的时候,这个方法会被调用
function activate(context) {
    var WordCounter = require('./wordCounter');
    var counter = new WordCounter(vscode);
    //需要释放的资源都在这里依次push到这个数组里面
    context.subscriptions.push(counter);
}
exports.activate = activate;

// this method is called when your extension is deactivated
function deactivate() {
}
exports.deactivate = deactivate;

修改package.json

{
    "name": "wordcount",
    "displayName": "WordCount",
    "description": "wordcount",
    "version": "0.0.1",
    "engines": {
        "vscode": "^1.29.0"
    },
    "categories": [
        "Other"
    ],
    "activationEvents": [
        "*"
    ],
    "main": "./extension",
    "contributes": {
        "commands": [
            {
                "command": "extension.sayHello",
                "title": "Hello World"
            }
        ]
    },
    "scripts": {
        "postinstall": "node ./node_modules/vscode/bin/install",
        "test": "node ./node_modules/vscode/bin/test"
    },
    "devDependencies": {
        "typescript": "^3.1.4",
        "vscode": "^1.1.25",
        "eslint": "^4.11.0",
        "@types/node": "^8.10.25",
        "@types/mocha": "^2.2.42"
    }
}

然后就shift+ctrl+p进入到插件运行环境下测试就可以在状态栏中的字符数量提示了

本地使用插件

插件编写好了之后,如何正常在VSCode开发过程中使用,这块直接把开发的插件项目拷贝到VSCode存放安装插件的目录下

C:\Users\your name\.vscode\extensions

插件项目拷贝进去后,重启VSCode,然后随意打开一个新的项目,在状态栏也可以看到字符统计的这个功能了

插件发布

官方插件发布流程:https://code.visualstudio.com...

Visual Studio Code的应用市场基于微软自己的Azure DevOps,插件的身份验证、托管和管理都是在这里。要发布到应用市场首先得有应用市场的publisher账号

1.注册账号 https://aka.ms/SignupAzureDevOps
2.得到token,登录后选择Security,按照下图上的方式勾选,这里注意得到token一定要复制下来保存,因为第二次进入就不会再显示了
clipboard.png
3.安装vsce打包工具 npm install -g vsce
4.使用vsce工具命令创建发布账号vsce create-publisher your-publisher-name
5.登录账号vsce login your-publisher-name
6.发布vsce publish

clipboard.png

发布成功后,可以访问https://marketplace.visualstu... 查看

在VSCode中也可以搜索进行安装

clipboard.png

这里在发布时有几个常见错误:
错误1:Error:Missing publisher name. Learn more:https://code.visualstudio.com...
解决方式:在package.json中将刚刚创建好的发布账号配置进去"publisher":"your name",

错误2:Error:Make sure to edit the README.md file before you publish your extension
解决方式:看下README.md文件中是否有http地址

错误3:A ‘repository’field is missing from the 'package.josn' manifest file .Do you want to continue? [y/n]
解决方式:y

代码下载

访问百度网盘地址直接下载即可
https://pan.baidu.com/s/1-veA...

查看原文

赞 7 收藏 6 评论 1

颜海镜 关注了用户 · 7月29日

andypinet @andypinet

没有钱了 肯定要做啊 不做没有钱用 。
打工这方面 打工是不可能打工的 这辈子不可能打工的 。
做生意又不会做 就是偷这种东西 才能维持的了生活这样子
进sf感觉像回家一样 在sf里的感觉比家里感觉好多了! 里面个个都是人才,说话又好听,我超喜欢里面的!

关注 25

颜海镜 赞了文章 · 7月29日

Fabric.js 简单介绍和使用

简介

Fabric.js是一个可以简化canvas程序编写的库。 Fabric.js为canvas提供所缺少的对象模型, svg parser, 交互和一整套其他不可或缺的工具。基于MIT协议开源,在github上有许多人贡献代码。

Why fabric?

canvas提供一个好的画布能力, 但其api超级烂。如果你就想画个简单图形, 其实也可以, 不过做一些复杂的图形绘制, 编写一些复杂的效果,就不是那么好了。
fabric就是为此而开发。

用对象的方式去编写代码

举个例子
传统的画正方形代码

// reference canvas element (with id="c")
var canvasEl = document.getElementById('c');

// get 2d context to draw on (the "bitmap" mentioned earlier)
var ctx = canvasEl.getContext('2d');

// set fill color of context
ctx.fillStyle = 'red';

// create rectangle at a 100,100 point, with 20x20 dimensions
ctx.fillRect(100, 100, 20, 20);

使用fabric

// create a wrapper around native canvas element (with id="c")
var canvas = new fabric.Canvas('c');

// create a rectangle object
var rect = new fabric.Rect({
    left: 100,
    top: 100,
    fill: 'red',
    width: 20,
    height: 20
});

// "add" rectangle onto canvas
canvas.add(rect);

图片描述

好的 其实并没有什么差别 不过我们试着旋转一下角度

var canvasEl = document.getElementById('c');
var ctx = canvasEl.getContext('2d');
ctx.fillStyle = 'red';

ctx.translate(100, 100);
ctx.rotate(Math.PI / 180 * 45);
ctx.fillRect(-10, -10, 20, 20);

fabric

var canvas = new fabric.Canvas('c');

// create a rectangle with angle=45
var rect = new fabric.Rect({
  left: 100,
  top: 100,
  fill: 'red',
  width: 20,
  height: 20,
  angle: 45
});

canvas.add(rect);

如果我们想重新调整位置 怎么办

var canvasEl = document.getElementById('c');

...
ctx.strokRect(100, 100, 20, 20);
...

// erase entire canvas area
ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
ctx.fillRect(20, 50, 20, 20);

fabric

var canvas = new fabric.Canvas('c');
...
canvas.add(rect);
...

rect.set({ left: 20, top: 50 });
canvas.renderAll();

objects

  1. fabric.Circle

  2. fabric.Ellipse

  3. fabric.Line

  4. fabric.Polygon

  5. fabric.Polyline

  6. fabric.Rect

  7. fabric.Triangle

画一个三角形 和一个 圆形

// create a wrapper around native canvas element (with id="c")
var canvas = new fabric.Canvas('c');

var circle = new fabric.Circle({
    radius: 20, fill: 'green', left: 100, top: 100
});
var triangle = new fabric.Triangle({
    width: 20, height: 30, fill: 'blue', left: 50, top: 50
});

canvas.add(circle, triangle);

图片描述

Manipulating objects

可以简单的使用set来控制对象属性
positioning — left, top;
dimension — width, height;
rendering — fill, opacity, stroke, strokeWidth;
scaling and rotation — scaleX, scaleY, angle;
and even those related to flipping — flipX, flipY.

rect.set('fill', 'red');
rect.set({ strokeWidth: 5, stroke: 'rgba(100,200,200,0.5)' });
rect.set('angle', 15).set('flipY', true);

图片描述

有了set 其实也就有了get

对象可以创建时设置属性 也可以先实例化 再赋值

var rect = new fabric.Rect({ width: 10, height: 20, fill: '#f55', opacity: 0.7 });

// or functionally identical

var rect = new fabric.Rect();
rect.set({ width: 10, height: 20, fill: '#f55', opacity: 0.7 });

另外这里的fabric.Rect是函数 大家可以使用class继承

默认值

var rect = new fabric.Rect(); // notice no options passed in

rect.getWidth(); // 0
rect.getHeight(); // 0

rect.getLeft(); // 0
rect.getTop(); // 0

rect.getFill(); // rgb(0,0,0)
rect.getStroke(); // null

rect.getOpacity(); // 1

Hierarchy and Inheritance

fabric.Object 是图像基类

你可以自己扩充方法

fabric.Object.prototype.getAngleInRadians = function() {
  return this.getAngle() / 180 * Math.PI;
};

var rect = new fabric.Rect({ angle: 45 });
rect.getAngleInRadians(); // 0.785...

var circle = new fabric.Circle({ angle: 30, radius: 10 });
circle.getAngleInRadians(); // 0.523...

circle instanceof fabric.Circle; // true
circle instanceof fabric.Object; // true

Canvas

fabric.Canvas 是canvas的wrapper

var canvas = new fabric.Canvas('c');
var rect = new fabric.Rect();

canvas.add(rect); // add object

canvas.item(0); // reference fabric.Rect added earlier (first object)
canvas.getObjects(); // get all objects on canvas (rect will be first and only)

canvas.remove(rect); // remove previously-added fabric.Rect

经典的设计 有options 有对象方法

var canvas = new fabric.Canvas('c', {
  backgroundColor: 'rgb(100,100,200)',
  selectionColor: 'blue',
  selectionLineWidth: 2
  // ...
});

// or

var canvas = new fabric.Canvas('c');
canvas.setBackgroundImage(http://...');
canvas.onFpsUpdate = function(){ /* ... */ };
// ...

Images

使用fabric.Image你可以轻松的加载一个图片
html

<canvas id="c"></canvas>
<img data-original="my_image.png" id="my-image">

js

var canvas = new fabric.Canvas('c');
var imgElement = document.getElementById('my-image');
var imgInstance = new fabric.Image(imgElement, {
  left: 100,
  top: 100,
  angle: 30,
  opacity: 0.85
});
canvas.add(imgInstance);

图片描述

当然也可以通过url加载一张图片到canvas

fabric.Image.fromURL('my_image.png', function(oImg) {
  canvas.add(oImg);
});

可以对加载的图片进行预处理

fabric.Image.fromURL('my_image.png', function(oImg) {
  // scale image down, and flip it, before adding it onto canvas
  oImg.scale(0.5).setFlipX(true);
  canvas.add(oImg);
});

Path and PathGroup

我们已经看了简单的形状,然后图像。更复杂、丰富的形状和内容呢?
路径包括一系列的命令,这基本上模仿一个笔从一个点到另一个。在“移动”,“线”,“曲线”,或“弧”等命令的帮助下,路径可以形成令人难以置信的复杂形状。同组的路径(路径组的帮助),开放更多的可能性。
类似于svg的path

var canvas = new fabric.Canvas('c');
var path = new fabric.Path('M 0 0 L 200 100 L 170 200 z');
path.set({ left: 120, top: 120 });
canvas.add(path);

图片描述

“M” 代表 “move” 命令, 告诉笔到 0, 0 点.
“L” 代表 “line” 画一条0, 0 到 200, 100 的线.
another “L” creates a line to 170, 200.
z” tells forces drawing pen to close current path and finalize the shape.

...
var path = new fabric.Path('M 0 0 L 300 100 L 200 300 z');
...
path.set({ fill: 'red', stroke: 'green', opacity: 0.5 });
canvas.add(path);

path也可以设置canvas属性

图片描述

当然 太困然了 所以你可以使用 fabric.loadSVGFromString or fabric.loadSVGFromURL 方法

Afterword

看些demo

查看原文

赞 17 收藏 45 评论 7

颜海镜 赞了回答 · 7月14日

解决TouchEvent里的targetTouches、touches、changedTouches的区别的具体体现是?

按我的理解是这样的:

  • touches:当前屏幕上所有触摸点的集合列表

  • targetTouches: 绑定事件的那个结点上的触摸点的集合列表

  • changedTouches: 触发事件时改变的触摸点的集合

举例来说,比如div1, div2只有div2绑定了touchstart事件,第一次放下一个手指在div2上,触发了touchstart事件,这个时候,三个集合的内容是一样的,都包含这个手指的touch,然后,再放下两个手指一个在div1上,一个在div2上,这个时候又会触发事件,但changedTouches里面只包含第二个第三个手指的信息,因为第一个没有发生变化,而targetTouches包含的是在第一个手指和第三个在div2上的手指集合,touches包含屏幕上所有手指的信息,也就是三个手指。

关注 17 回答 3

颜海镜 赞了文章 · 6月4日

前端模板的原理与实现

时下流行什么react, avalon, angular, vue什么,其核心都离不开前端模板。理解前端模板,是我们了解MV* 的关键。

前端框架最重要的目的是将页面渲染出来。“渲染”(render)这个词最初不是前端的东西的。前端之前叫做切图,将设计师做的PSD变成一个静态页面,然后加上动态交互。但是我们有许多数据是来自后端,如何将数据加入静态页面呢?于是又多了一套工序叫“套页面”。套页面的过程实际就是将静态页面变成切割成一块块,每一块都是一个php,jsp或vm文件,它们是后端模板引擎的处理对象!

其实模板是不局限于后端还是前端的, 模板的本质是用于从数据(变量)到实际的视觉表现(HTML代码)这项工作的一种实现手段。由于后端近水楼台先得月(取数据比较方便),因此先在后端发展出这种技术。这些后端模板文件是活动于服务器的,然后经过复杂的处理,最后由浏览器渲染出来。这时的渲染是将服务器拼接好的静态文本变成一个DOM树的过程。

如果要实现前端实现MVC或MVP,那些工序必须发生改变。静态文件产出是不变,尤其是大公司,分工够细,有专门的切图组(大多数是妹子)将它们做出来。接着是套页面,这时就不能使用后端模板引擎,需要引入前端模板引擎。由于实现一个前端模板引擎太简单了,经过多年的发展,已经有众多好用的轮子:

https://github.com/janl/musta...
基于JavaScript的Logic-less(无逻辑或轻逻辑)模板。

https://github.com/twitter/ho...
上面的优化版,twitter出品

https://github.com/wycats/han...
完全兼容mustcache的语法

https://github.com/paularmstr...
拥有更强悍的模板继承与block 重写功能

https://github.com/mozilla/nu...
跟django的模板系统相似,可以说swig的升级版,是gitbook的御用前端模板

其它推荐的还有ejs, 易学易用,对有过ASP/PHP/JSP编程经验的人来说,非常亲切自然,缺点就是功能有点简单。

其他doT, xtempalate, Underscore Templates。

最不推荐是jade, 有点华而不实,过度设计,导致套页面工作量大,性能其差。

虚拟DOM时代流行的jsx就是无逻辑模板。之所以流行无逻辑或轻逻辑模板,其主要原因是改动成本比较少,像jade这样自造语法糖太多,从美工手中拿来的html需要大动干戈,进行摧心折骨般的改造才能套数据。对于模板来说,最简单而言,就是将某个可变数据放到适当的地方(填空),而其次,可以控制这个区域输出不输入(if指令),或让其个区域循环输入多次(for指令),更强制,实现模板互相套嵌(layout与block)。为了实现if与for有两种方法,一种是单纯的区域,插入一个js 语句,里面有if语句与for语句,另一种是使用语法糖,比如说 ms-for, ms-repeat, ng-if, ng-repeat。语法糖的用法比直接使用JS语句简单,但是带来学习成本与拓展功能。每一个模板 if, for指令的语法都不一样的,并且你想在循环做一些处理,比如过滤一些数据,或突然在某处中断,这又得引用一些新的语句。随着模板要求前后共用,就有了传输成本,直接写JS语句在模板里面肯定比不过语法糖。因此基于这种种原因,mustache风格的模板就成为主流。

现在三种模板风格:

PHP/ASP/JSP风格:

<% if ( list.length ) { %>
  <ol>
    <% for ( n=0; n<list.length; ++n ) { %>
      <li>
        <%= list[n] %>
      </li>
    <% } %>
  </ol>
<% } %>

mustcache风格,高级语法有限,通常难自定义拓展:

{{#if list.length}}
  <ol>
    {{#each list item}}
      <li>
        {{ item }}
      </li>
    {{/each}}
  </ol>
{{/if}}

属性绑定风格:

<ol ms-if="list.length">
  <li ms-for="item in list">
      {{item}}
  </li>
</ol>

前两者只能出现于script, textarea等容器元素内部。因此<分隔符与标签的<容器造成冲突,并且也不利于IDE的格式化处理。属性绑定风格则是MVVM时期最流行的模板定义风格,某页面某个区域就是一个模板,不需要进行入append等操作。

我们再来看如何实现前端模板。前端模板的本质就是一个可以转换函数(渲染函数)的字符串。渲染函数放进一个充满数据的对象后,还原为一个全新的字符串。因此重点是如何构建一个渲染函数。最简单的方式是正则,还记得第二章的format方法吗,这就是一个轻型的填充数据的方法。

function format(str, object) {
    var array = Array.prototype.slice.call(arguments, 1);
    return str.replace(/\\?\#{([^{}]+)\}/gm, function(match, name) {
        if (match.charAt(0) == '\\')
            return match.slice(1);
        var index = Number(name)
        if (index >= 0)
            return array[index];
        if (object && object[name] !== void 0)
            return  object[name];
        return  '';
    });
}

format方法是通过#{ }来划分静态内容与动态内容的,一般来说它们称之为定界符(delimiter)。#{为前定界符,}为后界符,这个#{}其实是ruby风格的定界符。通常的定界符是<%%>{{}} 。通常在前定界符中还有一些修饰符号,比如=号,表示这个会输出到页面,-号,表示会去掉两旁的空白。
将下例,要编译成一个渲染函数

var tpl = '你好,我的名字啊<%name%>, 今年已经 <%info.age%>岁了'
 var data = {
     name: "司徒正美",
     age: 20
 }

大抵是这样

var body = '你好,我的名字叫'+ data.name+ ', 今年已经 '+data.info.age+ '岁了'
var render = new Function('data', 'return '+ body)

或者聪明一点,使用数组来join:

var array = ['return ']
array.push('你好,我的名字叫')
array.push(data.name)
array.push(', 今年已经')
array.push(data.info.age)
array.push( '岁了')
var render = new Function('data', array.join('+'))

这就得区分静态内容与为变量前加data.前缀。这一步可以用正则来做,也可以用纯字符串。我们试一下纯字符串方式。假令前定界符为openTag,后定界符为closeTag,通过indexOf 与slice方法,就可以将它切成一块块。

function tokenize(str) {
    var openTag = '<%'
    var closeTag = '%>'
    var ret = []
    do {
        var index = str.indexOf(openTag)
        index = index === -1 ? str.length : index
        var value = str.slice(0, index)
        //抽取{{前面的静态内容
        ret.push({
            expr: value,
            type: 'text'
        })
        //改变str字符串自身
        str = str.slice(index + openTag.length)
        if (str) {
            index = str.indexOf(closeTag)
            var value = str.slice(0, index)
            //抽取{{与}}的动态内容
            ret.push({
                expr: value.trim(),//JS逻辑两旁的空白可以省去
                type: 'js'
            })
            //改变str字符串自身
            str = str.slice(index + closeTag.length)
        }
    } while (str.length)
    return ret
}
console.log(tokenize(tpl))

clipboard.png

然后通过render方法将它们拼接起来。

function render(str) {
     var tokens = tokenize(str)
     var ret = []
     for (var i = 0, token; token = tokens[i++]; ) {
         if (token.type === 'text') {
             ret.push('"' + token.expr + '"')
         } else {
             ret.push(token.expr)
         }
     }
     console.log("return "+ ret.join('+'))
 }

打印出来如下

return "你好,我的名字叫"+name+", 今年已经 "+info.age+"岁了"

这个方法还不完整。首先光是在两旁加上双引号是不可靠的,万一里面还有双引号怎么办。因此我们需要引入第二章介绍的quote方法,当类型为文本时,ret.push(+quote(token.expr)+)。其次需要对动态部分的变量加上.data。怎么知道它是一个变量呢。我们回想一下变量的定义,就是以_,$或字母开头的字符组合。为了简洁起见,我们暂时不用里会中文的情况。不过,info.age这个字符串里面,其实有两个符合变量的子串,而我只需要在info前面加data.。这时,我们需要设法在匹配变量前,将对象的子级属性替换掉,替换成不符合变量的字符,然后再替换为去。为此,我搞了一个dig与fill方法,将子级属性变成??12这样的字符串:

 var quote = JSON.stringify//自己到第二章找完整函数
 var rident = /[$a-zA-Z_][$a-zA-Z0-9_]*/g
 var rproperty = /\.\s*[\w\.\$]+/g
 var number = 1
 var rfill = /\?\?\d+/g
 var stringPool = {}
 function dig(a) {
     var key = '??' + number++
     stringPool[key] = a
     return key
 }
 function fill(a) {
     return stringPool[a]
 }
 function render(str) {
     stringPool = {}
     var tokens = tokenize(str)
     var ret = []
     for (var i = 0, token; token = tokens[i++]; ) {
         if (token.type === 'text') {
             ret.push(quote(token.expr))
         } else {
             // 先去掉对象的子级属性,减少干扰因素
             var js = token.expr.replace(rproperty, dig)
             js = js.replace(rident, function (a) {
                 return 'data.' + a
             })
             js = js.replace(rfill, fill)
             ret.push(js)
         }
     }
     console.log("return " + ret.join('+'))
 }
render(tpl)

输出为

return "你好,我的名字叫"+data.name+", 今年已经 "+data.info.age+"岁了"

最后,我们修改一下后面两行,得到我们梦魅以求的渲染函数,它的实现过程比format方法复杂多了,但却是所有扩展性极强的前端模板的一般实现过程。

  function render(str){
  //略。。。
     return new Function("data", "return " + ret.join('+'))
  }
  var fn = render(tpl)
  console.log(fn+"")
  console.log(fn(data))

clipboard.png

我们再看一下如何引入循环语句,比如将上面的模板与数据改成这样

var tpl = '你好,我的名字叫<%name%>, 今年已经 <%info.age%>岁了,喜欢<% for(var i = 0, el; el = list[i++];){%><% el %> <% } %>'
 var data = {
     name: "司徒正美",
     info: {
         age: 20
     },
     list: ["苹果","香蕉","雪梨"]
 }

这时我们就添加一种新的类型,不输出到页面的动态内容。这在token方法中做一些修改

value = value.trim()
if (/^(if|for|})/.test(value)) {
    ret.push({
        expr: value,
        type: 'logic'
    })
} else {
    ret.push({
        expr: value,
        type: 'js'
    })
}

但render方法怎么修改好呢,显示这时继续用已经不行了,否则下场是这样

return "你好,我的名字叫"+data.name+", 今年已经 "+data.info.age+"岁了,喜欢"+for(var i = 0, el; el = list[i++];){+""+data.el+" "+}

这时, 我们需要借用数组,将要输入的数据(text, js类型 )放进去,logic类型不放进去。

function addPrefix(str) {
   // 先去掉对象的子级属性,减少干扰因素
   var js = str.replace(rproperty, dig)
   js = js.replace(rident, function (a) {
       return 'data.' + a
   })
   return js.replace(rfill, fill)
}
function addView(s) {
   return '__data__.push(' + s + ')'
}
function render(str) {
    stringPool = {}
    var tokens = tokenize(str)
    var ret = ['var __data__ = []']
    tokens.forEach(function(token){
       if (token.type === 'text') {
           ret.push(addView(quote(token.expr)))
       } else if (token.type === 'logic') {
           //逻辑部分都经过addPrefix方法处理
           ret.push(addPrefix(token.expr))
       } else {
           ret.push(addView(addPrefix(token.expr)))
       }
    })
    ret.push("return __data__.join('')")
    console.log( ret.join('\n'))
  }
var fn = render(tpl)

得到的内部结构是这样的,显然addPrefix方法出问题,我们应该过滤掉if, for等关键字与保留字。

var __data__ = []
__data__.push("你好,我的名字叫")
__data__.push(data.name)
__data__.push(", 今年已经 ")
__data__.push(data.info.age)
__data__.push("岁了,喜欢")
data.for(data.var data.i = 0, data.el; data.el = data.list[data.i++]){
__data__.push("")
__data__.push(data.el)
__data__.push(" ")
}
return __data__.join('')

但即使我们处理掉关键字与保留字,对于中间生成i, el怎么区分呢?是区分不了。于是目前有两种方法,一是使用with, 这时我们就不需要加data.前缀。第二种引入新的语法。比如,前面是@就替换为data.

先看第一种:

function render(str) {
     stringPool = {}
     var tokens = tokenize(str)
     var ret = ['var __data__ = [];', 'with(data){']
     for (var i = 0, token; token = tokens[i++]; ) {
         if (token.type === 'text') {
             ret.push(addView(quote(token.expr)))
         } else if (token.type === 'logic') {
             ret.push(token.expr)
         } else {
             ret.push(addView(token.expr))
         }
     }
     ret.push('}')
     ret.push('return __data__.join("")')
     return new Function("data", ret.join('\n'))
 }
 var fn = render(tpl)
 console.log(fn + "")
 console.log(fn(data))

clipboard.png

许多迷你模板都是用with减少替换工作。

第二种方法,使用引导符@, avalon2就是这么玩。这样addPrefix方法可以减少许多代码。相对应,模板也要改动一下

 var tpl = '你好,我的名字叫<%@name%>, 今年已经 <%@info.age%>岁了,喜欢<% for(var i = 0, el; el = @list[i++];){%><% el %> <% } %>'
 var rguide = /(^|[^\w\u00c0-\uFFFF_])(@|##)(?=[$\w])/g
 function addPrefix(str) {
     return str.replace(rguide, '$1data.')
 }
 function render(str) {
     stringPool = {}
     var tokens = tokenize(str)
     var ret = ['var __data__ = [];']
     for (var i = 0, token; token = tokens[i++]; ) {
         if (token.type === 'text') {
             ret.push(addView(quote(token.expr)))
         } else if (token.type === 'logic') {
             //逻辑部分都经过addPrefix方法处理
             ret.push(addPrefix(token.expr))
         } else {
             ret.push(addView(addPrefix(token.expr)))
         }
     }
    
     ret.push('return __data__.join("")')
     return new Function("data", ret.join('\n'))
 }
 var fn = render(tpl)
 console.log(fn + "")
 console.log(fn(data))

clipboard.png

第二种比第一种的优势在于,性能更高,并且避开es5严格模式的限制。

我们再认真思考一下,其实循环语句与条件语句,不单是for, if两个,还有while, do while, else什么。因此这需要优化。这也有两种方法,添加更多语法符合,比如上面所说的=就是输出,没有则不输出。这是ASP/JSP/PHP等模板采用的手段:

//tokenize方法
if (value.charAt(0) === '=') {
     ret.push({
         expr: value, 
         type: 'js'
     })
 } else {
     ret.push({
         expr: value, 
         type: 'logic'
     })
 }

另一种,使用语法糖,如#each (el, index) in @list , '#eachEnd', '#if ','#ifEnd'。还是改动tokenize方法

 if (value.charAt(0) === '#') {
    if (value === '#eachEnd' || value === '#ifEnd') {
        ret.push({
            expr: '}',
            type: 'logic'
        })
    } else if (value.slice(0, 4) === '#if ') {
        ret.push({
            expr: 'if(' + value.slice(4) + '){',
            type: 'logic'
        })
    } else if (value.slice(0, 6) === '#each ') {
        var arr = value.slice(6).split(' in ')
        var arrayName = arr[1]
        var args = arr[0].match(/[$\w_]+/g)
        var itemName = args.pop()
        var indexName = args.pop() || '$index'
        value = ['for(var ', ' = 0;', '<' + arrayName + '.length;', '++){'].join(indexName) +
                '\nvar ' + itemName + ' = ' + arrayName + '[' + indexName + '];'
        ret.push({
            expr: value,
            type: 'logic'
        })
    
    }
    
} else{
   //...
}

对应的模板改成

var tpl = '你好,我的名字叫<%@name%>, 今年已经 <%@info.age%>岁了,喜欢<%#each el in @list %><% el %> <% #eachEnd %>'
var fn = render(tpl)
console.log(fn + "")
console.log(fn(data))

clipboard.png

可能有人觉#for, #forEnd这样的语法糖比较丑,无问题,这个可以改,主要我们的tokenize方法足够强大,就能实现mustache这样的模板引擎。但所有模板引擎也基本上是这么实现的,有的还支持过滤器,也就是在js 类型的语句再进行处理,将|后面的字符器再切割出来。

如果虚拟DOM呢?那就需要一个html parser,这个工程巨大,比如reactive这个库,早期不使用html parser与虚拟DOM,只有3,4千行,加入这些炫酷功能后就达到1W6K行。返回一个字符串与返回一个类似DOM树的对象树结构是不一样。

查看原文

赞 40 收藏 162 评论 10

颜海镜 关注了用户 · 5月25日

hiluluke @hiluluke

这个人又没写个人简介

关注 48

颜海镜 赞了文章 · 5月25日

git 高阶用法

日常生活中的git

通常情况下,在进行git操作的时候,大家最熟悉的流程:

可能还有的同学也经常git commit --amend -C head,向上一个commit提交内容。
然而在日常生活中总会有一些其他对代码版本管理的需求,比如需要去修改很久之前的一个commit的内容等。

本文主要记录一些git相对使用较少,但是非常有用的命令。

git rebase

git rebase 有两个常用功能:

  • 从上游分支获取最新commit信息,并有机的将当前分支和上游分支进行合并。

  • 对当前分支的历史commit进行修改,合并,删除等操作

场景

  • 你的分支和master冲突

如果你git merge master就会留下一个merge的commit。在这种情况下,我们推荐使用git rebase master,就能不留commit将当前分支和master有机的合并。

  • 之前某次提交,改错了一个变量

如果你修改那个变量然后再提交一个commit,这显然不是最优的。如果能将某次的提交挑出来修改就好了。当然git提供了这个功能,你可以使用git rebase -i [git-hash| head~n],其中git-hash是你要开始进行rebase的commit的hash,而head~n则是从HEAD向前推n个commit.

当你执行git rebase -i 你会看到如下界面,需要你去选择对应的commit指定一种操作。下面提示了很多操作项。

git stash

git stash 主要功能是:

  • 暂存当前没有提交的更改

git stash save 保存当前更改
git stash 保存当前更改
git stash pop 推出之前stash的内容更改
git stash apply 推出之前stash的内容更改

git stash save --keep-index 只stash没有被add的内容
git stash save --include-untracked stash还未加入git记录的文件

git stash list --stat 显示stash堆栈
git stash show stash@{0} 显示第x次的更改
git stash show 显示最近一次stash的更改
git stash show --patch 显示最近一次stash的详细更改

git stash save "stash msg"

git stash branch new_brach stash@{0}

场景

  • 当做了更改之后但是没有编写完,这个时候发现线上有个bug,你需要停下当前分支去修bug,这个时候你可以提一个commit到当前分支,或则使用git stash

stash多次是以堆栈的形式进行存储的。

git filter-branch

git filter-branch的主要功能是

  • 过滤所有提交记录,进行相应的操作

git filter-branch --tree-filter 'rm -rf .vscode' 删除所有分支的.vscode文件夹
git filter-branch --tree-filter 'rm -f xxx' xxx不存在,不报错
git filter-branch --tree-filter 'rm -rf .vscoe' -- --all(所有分支所有提交)

git filter-branch --index-filter 'git rm --cached --ignore-unmatch password.txt' (只检查password.txt 一个文件)
git filter-branch -f --prune-empty -- --all

场景

  • 你发现你错误的将一个不应该提交的文件提交,然后已经过了很久,已经累计了无数次提交。

这个时候就可以使用git filter-branch将所有提交全部过滤一遍删除掉那个不应该提交的文件

cherry-pick

git cherry-pick的主要功能是

  • 从任何分支,抽取提交到当前分支

git cherry-pick git-hash 

git cherry-pick --edit git-hash

pick 多个
git cherry-pick --no-commit git-hash git-hash

git cherry-pick -x git-hash (添加cherry pick from 那个branch)
git cherry-pick --signoff git-hash不修改author

场景

当需要将多个分支上的提交合在一个分支合并到master的时候,显然如果这3个分支有关联,一次合并更为合理。
这个时候就可以使用git cherry-pick进行精细化的commit的操作。

work together

git config --global core.autocrlf
linux osx是lf
windows 是crlf

如果你的源文件中是换行符是LF,而autocrlf=true, 此时Git add就会遇到 fatal: LF would be replaced by CRLF 的错误。有两个解决办法:

  1. 将你的源文件中的LF转为CRLF即可【推荐】

  2. 将autocrlf 设置为 false

如果你的源文件中是换行符是CRLF,而autocrlf=input, 此时git add也会遇到 fatal: CRLF would be replaced by LF 的错误。有两个解决办法:

  1. 将你源文件中的CRLF转为LF【推荐】

  2. 将autocrlf 设置为true 或者 false

.gitattributes

* text = auto 
*.html text
*.css text

*.jpg binary
*.png binary

*.ssh text eol=lf
*.bat text eol=crlf

一些可能会用到的

git submodule

git submodule add git@example.com:css.gi

git reflog

git log --walk-reflogs

原文链接:http://hiluluke.cn/2017/07/23...

查看原文

赞 15 收藏 111 评论 6

认证与成就

  • 获得 1264 次点赞
  • 获得 30 枚徽章 获得 1 枚金徽章, 获得 4 枚银徽章, 获得 25 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • zepto.fullpage

    专注于移动端的fullPage.js

  • template.js

    template.js 一款javascript模板引擎,简单,好用,支持webpack和fis

注册于 2013-11-14
个人主页被 3k 人浏览