独书先生 Menu

如何在 JavaScript 中实现事件总线 (Event Bus)

原文:https://dushusir.com/js-event-bus/

介绍

Event Bus 事件总线,通常作为多个模块间的通信机制,相当于一个事件管理中心,一个模块发送消息,其它模块接受消息,就达到了通信的作用。

比如,Vue 组件间的数据传递可以使用一个 Event Bus 来通信,也可以用作微内核插件系统中的插件和核心通信。

原理

Event Bus 本质上是采用了发布-订阅的设计模式,比如多个模块 ABC 订阅了一个事件 EventX,然后某一个模块 X 在事件总线发布了这个事件,那么事件总线会负责通知所有订阅者 ABC,它们都能收到这个通知消息,同时还可以传递参数。

// 关系图
                           模块X
                            ⬇发布EventX
╔════════════════════════════════════════════════════════════════════╗
║                         Event Bus                                  ║
║                                                                    ║
║         【EventX】       【EventY】       【EventZ】   ...           ║
╚════════════════════════════════════════════════════════════════════╝
  ⬆订阅EventX            ⬆订阅EventX           ⬆订阅EventX
 模块A                   模块B                  模块C

分析

如何使用 JavaScript 来实现一个简单版本的 Event Bus

  • 首先构造一个 EventBus 类,初始化一个空对象用于存放所有的事件
  • 在接受订阅时,将事件名称作为 key 值,将需要在接受发布消息后执行的回调函数作为 value 值,由于一个事件可能有多个订阅者,所以这里的回调函数要存储成列表
  • 在发布事件消息时,从事件列表里取得指定的事件名称对应的所有回调函数,依次触发执行即可

以下是代码详细实现,可以复制到谷歌浏览器控制台直接运行检测效果。

代码

class EventBus {
  constructor() {
    // 初始化事件列表
    this.eventObject = {};
  }
  // 发布事件
  publish(eventName) {
    // 取出当前事件所有的回调函数
    const callbackList = this.eventObject[eventName];

    if (!callbackList) return console.warn(eventName + " not found!");

    // 执行每一个回调函数
    for (let callback of callbackList) {
      callback();
    }
  }
  // 订阅事件
  subscribe(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      this.eventObject[eventName] = [];
    }

    // 存储订阅者的回调函数
    this.eventObject[eventName].push(callback);
  }
}

// 测试
const eventBus = new EventBus();

// 订阅事件eventX
eventBus.subscribe("eventX", () => {
  console.log("模块A");
});
eventBus.subscribe("eventX", () => {
  console.log("模块B");
});
eventBus.subscribe("eventX", () => {
  console.log("模块C");
});

// 发布事件eventX
eventBus.publish("eventX");

// 输出
> 模块A
> 模块B
> 模块C

上面我们实现了最基础的发布和订阅功能,实际应用中,还可能有更进阶的需求。

进阶

1. 如何在发送消息时传递参数

发布者传入一个参数到 EventBus 中,在 callback 回调函数执行的时候接着传出参数,这样每一个订阅者就可以收到参数了。

代码

class EventBus {
  constructor() {
    // 初始化事件列表
    this.eventObject = {};
  }
  // 发布事件
  publish(eventName, ...args) {
    // 取出当前事件所有的回调函数
    const callbackList = this.eventObject[eventName];

    if (!callbackList) return console.warn(eventName + " not found!");

    // 执行每一个回调函数
    for (let callback of callbackList) {
      // 执行时传入参数
      callback(...args);
    }
  }
  // 订阅事件
  subscribe(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      this.eventObject[eventName] = [];
    }

    // 存储订阅者的回调函数
    this.eventObject[eventName].push(callback);
  }
}

// 测试
const eventBus = new EventBus();

// 订阅事件eventX
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块A", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块B", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块C", obj, num);
});

// 发布事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);


// 输出
> 模块A {msg: 'EventX published!'} 1
> 模块B {msg: 'EventX published!'} 1
> 模块C {msg: 'EventX published!'} 1

2. 订阅后如何取消订阅

有时候订阅者只想在某一个时间段订阅消息,这就涉及带取消订阅功能。我们将对代码进行改造。

首先,要实现指定订阅者取消订阅,每一次订阅事件时,都生成唯一一个取消订阅的函数,用户直接调用这个函数,我们就把当前订阅的回调函数删除。

// 每一次订阅事件,都生成唯一一个取消订阅的函数
const unSubscribe = () => {
  // 清除这个订阅者的回调函数
  delete this.eventObject[eventName][id];
};

其次,订阅的回调函数列表使换成对象结构存储,为每一个回调函数设定一个唯一 id, 注销回调函数的时候可以提高删除的效率,如果还是使用数组的话需要使用 split 删除,效率不如对象的 delete

代码

class EventBus {
  constructor() {
    // 初始化事件列表
    this.eventObject = {};
    // 回调函数列表的id
    this.callbackId = 0;
  }
  // 发布事件
  publish(eventName, ...args) {
    // 取出当前事件所有的回调函数
    const callbackObject = this.eventObject[eventName];

    if (!callbackObject) return console.warn(eventName + " not found!");

    // 执行每一个回调函数
    for (let id in callbackObject) {
      // 执行时传入参数
      callbackObject[id](...args);
    }
  }
  // 订阅事件
  subscribe(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this.eventObject[eventName] = {};
    }

    const id = this.callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this.eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this.eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this.eventObject[eventName]).length === 0) {
        delete this.eventObject[eventName];
      }
    };

    return { unSubscribe };
  }
}

// 测试
const eventBus = new EventBus();

// 订阅事件eventX
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块A", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块B", obj, num);
});
const subscriberC = eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块C", obj, num);
});

// 发布事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);

// 模块C取消订阅
subscriberC.unSubscribe();

// 再次发布事件eventX,模块C不会再收到消息了
eventBus.publish("eventX", { msg: "EventX published again!" }, 2);

// 输出
> 模块A {msg: 'EventX published!'} 1
> 模块B {msg: 'EventX published!'} 1
> 模块C {msg: 'EventX published!'} 1
> 模块A {msg: 'EventX published again!'} 2
> 模块B {msg: 'EventX published again!'} 2

3. 如何只订阅一次

如果一个事件只发生一次,通常也只需要订阅一次,收到消息后就不用再接受消息。

首先,我们提供一个 subscribeOnce 的接口,内部实现几乎和 subscribe 一样,只有一个地方有区别,在 callbackId 前面的加一个字符 d,用来标示这是一个需要删除的订阅。

// 标示为只订阅一次的回调函数
const id = "d" + this.callbackId++;

然后,在执行回调函数后判断当前回调函数的 id 有没有标示,决定我们是否需要删除这个回调函数。

// 只订阅一次的回调函数需要删除
if (id[0] === "d") {
  delete callbackObject[id];
}

代码

class EventBus {
  constructor() {
    // 初始化事件列表
    this.eventObject = {};
    // 回调函数列表的id
    this.callbackId = 0;
  }
  // 发布事件
  publish(eventName, ...args) {
    // 取出当前事件所有的回调函数
    const callbackObject = this.eventObject[eventName];

    if (!callbackObject) return console.warn(eventName + " not found!");

    // 执行每一个回调函数
    for (let id in callbackObject) {
      // 执行时传入参数
      callbackObject[id](...args);

      // 只订阅一次的回调函数需要删除
      if (id[0] === "d") {
        delete callbackObject[id];
      }
    }
  }
  // 订阅事件
  subscribe(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this.eventObject[eventName] = {};
    }

    const id = this.callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this.eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this.eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this.eventObject[eventName]).length === 0) {
        delete this.eventObject[eventName];
      }
    };

    return { unSubscribe };
  }

  // 只订阅一次
  subscribeOnce(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this.eventObject[eventName] = {};
    }

    // 标示为只订阅一次的回调函数
    const id = "d" + this.callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this.eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this.eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this.eventObject[eventName]).length === 0) {
        delete this.eventObject[eventName];
      }
    };

    return { unSubscribe };
  }
}

// 测试
const eventBus = new EventBus();

// 订阅事件eventX
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块A", obj, num);
});
eventBus.subscribeOnce("eventX", (obj, num) => {
  console.log("模块B", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块C", obj, num);
});

// 发布事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);

// 再次发布事件eventX,模块B只订阅了一次,不会再收到消息了
eventBus.publish("eventX", { msg: "EventX published again!" }, 2);

// 输出
> 模块A {msg: 'EventX published!'} 1
> 模块C {msg: 'EventX published!'} 1
> 模块B {msg: 'EventX published!'} 1
> 模块A {msg: 'EventX published again!'} 2
> 模块C {msg: 'EventX published again!'} 2

4. 如何清除某个事件或者所有事件

我们还希望通过一个 clear 的操作来将指定事件的所有订阅清除掉,这个通常在一些组件或者模块卸载的时候用到。

  // 清除事件
  clear(eventName) {
    // 未提供事件名称,默认清除所有事件
    if (!eventName) {
      this.eventObject = {};
      return;
    }

    // 清除指定事件
    delete this.eventObject[eventName];
  }

和取消订阅的逻辑相似,只不过这里统一处理了。

代码

class EventBus {
  constructor() {
    // 初始化事件列表
    this.eventObject = {};
    // 回调函数列表的id
    this.callbackId = 0;
  }
  // 发布事件
  publish(eventName, ...args) {
    // 取出当前事件所有的回调函数
    const callbackObject = this.eventObject[eventName];

    if (!callbackObject) return console.warn(eventName + " not found!");

    // 执行每一个回调函数
    for (let id in callbackObject) {
      // 执行时传入参数
      callbackObject[id](...args);

      // 只订阅一次的回调函数需要删除
      if (id[0] === "d") {
        delete callbackObject[id];
      }
    }
  }
  // 订阅事件
  subscribe(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this.eventObject[eventName] = {};
    }

    const id = this.callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this.eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this.eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this.eventObject[eventName]).length === 0) {
        delete this.eventObject[eventName];
      }
    };

    return { unSubscribe };
  }

  // 只订阅一次
  subscribeOnce(eventName, callback) {
    // 初始化这个事件
    if (!this.eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this.eventObject[eventName] = {};
    }

    // 标示为只订阅一次的回调函数
    const id = "d" + this.callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this.eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this.eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this.eventObject[eventName]).length === 0) {
        delete this.eventObject[eventName];
      }
    };

    return { unSubscribe };
  }

  // 清除事件
  clear(eventName) {
    // 未提供事件名称,默认清除所有事件
    if (!eventName) {
      this.eventObject = {};
      return;
    }

    // 清除指定事件
    delete this.eventObject[eventName];
  }
}

// 测试
const eventBus = new EventBus();

// 订阅事件eventX
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块A", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块B", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
  console.log("模块C", obj, num);
});

// 发布事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);

// 清除
eventBus.clear("eventX");

// 再次发布事件eventX,由于已经清除,所有模块都不会再收到消息了
eventBus.publish("eventX", { msg: "EventX published again!" }, 2);

// 输出
> 模块A {msg: 'EventX published!'} 1
> 模块B {msg: 'EventX published!'} 1
> 模块C {msg: 'EventX published!'} 1
> eventX not found!

5. TypeScript 版本

鉴于现在 TypeScript 已经被大规模采用,尤其是大型前端项目,我们简要的改造为一个 TypeScript 版本

可以复制以下代码到 TypeScript Playground 体验运行效果

代码

interface ICallbackList {
  [id: string]: Function;
}

interface IEventObject {
  [eventName: string]: ICallbackList;
}

interface ISubscribe {
  unSubscribe: () => void;
}

interface IEventBus {
  publish<T extends any[]>(eventName: string, ...args: T): void;
  subscribe(eventName: string, callback: Function): ISubscribe;
  subscribeOnce(eventName: string, callback: Function): ISubscribe;
  clear(eventName: string): void;
}

class EventBus implements IEventBus {
  private _eventObject: IEventObject;
  private _callbackId: number;
  constructor() {
    // 初始化事件列表
    this._eventObject = {};
    // 回调函数列表的id
    this._callbackId = 0;
  }
  // 发布事件
  publish<T extends any[]>(eventName: string, ...args: T): void {
    // 取出当前事件所有的回调函数
    const callbackObject = this._eventObject[eventName];

    if (!callbackObject) return console.warn(eventName + " not found!");

    // 执行每一个回调函数
    for (let id in callbackObject) {
      // 执行时传入参数
      callbackObject[id](...args);

      // 只订阅一次的回调函数需要删除
      if (id[0] === "d") {
        delete callbackObject[id];
      }
    }
  }
  // 订阅事件
  subscribe(eventName: string, callback: Function): ISubscribe {
    // 初始化这个事件
    if (!this._eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this._eventObject[eventName] = {};
    }

    const id = this._callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this._eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this._eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this._eventObject[eventName]).length === 0) {
        delete this._eventObject[eventName];
      }
    };

    return { unSubscribe };
  }

  // 只订阅一次
  subscribeOnce(eventName: string, callback: Function): ISubscribe {
    // 初始化这个事件
    if (!this._eventObject[eventName]) {
      // 使用对象存储,注销回调函数的时候提高删除的效率
      this._eventObject[eventName] = {};
    }

    // 标示为只订阅一次的回调函数
    const id = "d" + this._callbackId++;

    // 存储订阅者的回调函数
    // callbackId使用后需要自增,供下一个回调函数使用
    this._eventObject[eventName][id] = callback;

    // 每一次订阅事件,都生成唯一一个取消订阅的函数
    const unSubscribe = () => {
      // 清除这个订阅者的回调函数
      delete this._eventObject[eventName][id];

      // 如果这个事件没有订阅者了,也把整个事件对象清除
      if (Object.keys(this._eventObject[eventName]).length === 0) {
        delete this._eventObject[eventName];
      }
    };

    return { unSubscribe };
  }

  // 清除事件
  clear(eventName: string): void {
    // 未提供事件名称,默认清除所有事件
    if (!eventName) {
      this._eventObject = {};
      return;
    }

    // 清除指定事件
    delete this._eventObject[eventName];
  }
}

// 测试
interface IObj {
  msg: string;
}

type PublishType = [IObj, number];

const eventBus = new EventBus();

// 订阅事件eventX
eventBus.subscribe("eventX", (obj: IObj, num: number, s: string) => {
  console.log("模块A", obj, num);
});
eventBus.subscribe("eventX", (obj: IObj, num: number) => {
  console.log("模块B", obj, num);
});
eventBus.subscribe("eventX", (obj: IObj, num: number) => {
  console.log("模块C", obj, num);
});

// 发布事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);

// 清除
eventBus.clear("eventX");

// 再次发布事件eventX,由于已经清除,所有模块都不会再收到消息了
eventBus.publish<PublishType>("eventX", { msg: "EventX published again!" }, 2);

// 输出
[LOG]: "模块A",  {
  "msg": "EventX published!"
},  1
[LOG]: "模块B",  {
  "msg": "EventX published!"
},  1
[LOG]: "模块C",  {
  "msg": "EventX published!"
},  1
[WRN]: "eventX not found!"

6. 单例模式

在实际使用过程中,往往只需要一个事件总线就能满足需求,这里有两种情况,保持在上层实例中单例和全局单例。

  1. 保持在上层实例中单例

将事件总线引入到上层实例使用,只需要保证在一个上层实例中只有一个 EventBus,如果上层实例有多个,意味着有多个事件总线,但是每个上层实例管控自己的事件总线。
首先在上层实例中建立一个变量用来存储事件总线,只在第一次使用时初始化,后续其他模块使用事件总线时直接取得这个事件总线实例。

代码

// 上层实例
class LWebApp {
  private _eventBus?: EventBus;

  constructor() {}

  public getEventBus() {
    // 第一次初始化
    if (this._eventBus == undefined) {
      this._eventBus = new EventBus();
    }

    // 后续每次直接取唯一一个实例,保持在LWebApp实例中单例
    return this._eventBus;
  }
}

// 使用
const eventBus = new LWebApp().getEventBus();
  1. 全局单例

有时候我们希望不管哪一个模块想使用我们的事件总线,我们都想这些模块使用的是同一个实例,这就是全局单例,这种设计能更容易统一管理事件。

写法同上面的类似,区别是要把 _eventBusgetEventBus 转为静态属性。使用时无需实例化 EventBusTool 工具类,直接使用静态方法就行了。

代码

// 上层实例
class EventBusTool {
  private static _eventBus?: EventBus;

  constructor() {}

  public static getEventBus(): EventBus {
    // 第一次初始化
    if (this._eventBus == undefined) {
      this._eventBus = new EventBus();
    }

    // 后续每次直接取唯一一个实例,保持全局单例
    return this._eventBus;
  }
}

// 使用
const eventBus = EventBusTool.getEventBus();

原文:https://dushusir.com/js-event-bus/

总结

以上是小编对 Event Bus 的一些理解,基本上实现了想要的效果。通过自己动手实现一遍发布订阅模式,也加深了对经典设计模式的理解。其中还有很多不足和需要优化的地方,欢迎大家多多分享自己的经验。

参考

李佳琪直播间抢到了娇兰金钻粉底液!

背景

小编的媳妇常年关注李佳琪直播间,所以经常会剁手一些护肤品和化妆品,特别是现在比较火爆流行的化妆品,而且越是火爆的东西越难抢。

前几天就抢到了娇兰金钻粉底液,一个大瓶30ml和6个小样每个5ml。快递到手后立马试用了下,说是感觉效果很不错。

媳妇说,她平时化妆品用的不多,这个化妆品一次买的量又比较大,可以用很久。所以想是不是可以把小样出给集美们,让不想一次买那么多的集美们也能方便的试用一下,试用好的话就可以继续去官网买大瓶的,试用感觉不好的话,反正买的不多也不亏。

福利

我们看下官网的价格,750一个30ml的大瓶,价格已经劝退了没有用过的集美了

这个价格折算一个小样5ml的125一个,可见这个粉底液真的很受欢迎。媳妇说这个价值125的小样,打折给关注咱们公众号的朋友100包邮的价格出。

有朋友注意到,小红书和闲鱼上也有很多更便宜的卖这些护肤品的商家,大家一定要注意甄别,不是说价格便宜的一定不好,而是你想想为啥官网125价值的粉底液,为啥卖你半价不到,而且还能卖那么多?关键脸蛋是自己的,现在网上的假货实在太多了,大家一定要注意。

有意向的小伙伴,关注公众号:技术分社,后台回复:小编,即可添加微信咨询。

微信openHacking

总结

因为这个粉底液实在太抢手了,估计能很快出完。出完了也没有关系,之后抢到了其他好用的护肤品或者化妆品,我们还会考虑出一些给需要的小伙伴。

你可以先关注微信公众号或者直接添加文章底部的微信,有新的东西,我们再第一时间更新公众号和朋友圈。
谢谢大家的支持。

js toFixed精度问题兼容方法

原生的js的toFixed有精确度问题,需要兼容.

问题:

1.35.toFixed(1) // 1.4 正确
1.335.toFixed(2) // 1.33 错误
1.3335.toFixed(3) // 1.333 错误
1.33335.toFixed(4) // 1.3334 正确
1.333335.toFixed(5) // 1.33333 错误
1.3333335.toFixed(6) // 1.333333 错误

解决方法:

// toFixed兼容方法
Number.prototype.toFixed = function (n) {
    if (n > 20 || n < 0) {
        throw new RangeError('toFixed() digits argument must be between 0 and 20');
    }
    const number = this;
    if (isNaN(number) || number >= Math.pow(10, 21)) {
        return number.toString();
    }
    if (typeof (n) == 'undefined' || n == 0) {
        return (Math.round(number)).toString();
    }

    let result = number.toString();
    const arr = result.split('.');

    // 整数的情况
    if (arr.length < 2) {
        result += '.';
        for (let i = 0; i < n; i += 1) {
            result += '0';
        }
        return result;
    }

    const integer = arr[0];
    const decimal = arr[1];
    if (decimal.length == n) {
        return result;
    }
    if (decimal.length < n) {
        for (let i = 0; i < n - decimal.length; i += 1) {
            result += '0';
        }
        return result;
    }
    result = integer + '.' + decimal.substr(0, n);
    const last = decimal.substr(n, 1);

    // 四舍五入,转换为整数再处理,避免浮点数精度的损失
    if (parseInt(last, 10) >= 5) {
        const x = Math.pow(10, n);
        result = (Math.round((parseFloat(result) * x)) + 1) / x;
        result = result.toFixed(n);
    }

    return result;
};

参考自:https://www.jianshu.com/p/849b0ae36b36

webrtc 屏幕录制

需求

我们在复现软件问题,或者做教学的时候,需要录屏,一般来说需要下载屏幕录制软件,经常还会带上水印,有没有一种可以不用安装软件在线录屏的方案呢,答案就是webrtc,使用浏览器自带的功能,打开网页就能录制桌面上的任意窗口,而且没有水印。

演示

https://dushusir.com/tool/web-recorder.html

代码

以下代码你将学习到
– 使用navigator.mediaDevices.getDisplayMedia获取用户窗口
– 使用MediaRecorder获取用户窗口录制的视频流
– 使用Blob和window.URL.createObjectURL来将视频流下载下来

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>webrtc screen recorder</title>
</head>

<body>


    <video autoplay playsinline id="player"></video>
    <video playsinline id="recordPlayer"></video>

    <!-- 在开始录制之前播放和下载按钮时禁用的 -->
    <button id="recordBtn">start</button>
    <button id="playBtn">play</button>
    <button id="downloadBtn">download</button>

    <script type="text/javascript">


        var player = document.querySelector("#player");
        var recordPlayer = document.querySelector("#recordPlayer");
        var recordBtn = document.querySelector("#recordBtn");
        var playBtn = document.querySelector("#playBtn");
        var downloadBtn = document.querySelector("#downloadBtn");

        var buffer; // 用于存储录制数据(数组)
        var mediaStream;
        var mediaRecoder;

        // 录制按钮点击事件
        recordBtn.addEventListener('click', function () {
            // console.log(recordBtn.text());
            if (recordBtn.textContent === 'start') {
                startRecord();
                recordBtn.textContent = 'stop';
                playBtn.setAttribute('disabled', true);
                downloadBtn.setAttribute('disabled', true);
            } else if (recordBtn.textContent === 'stop') {
                stopRecord();
                recordBtn.textContent = 'start';
                playBtn.removeAttribute('disabled');
                downloadBtn.removeAttribute('disabled');
            }
        });

        // 播放按钮点击事件
        playBtn.addEventListener('click', function () {
            var blob = new Blob(buffer, { type: 'video/webm' });
            // 根据缓存数据生成url给recordPlayer进行播放
            recordPlayer.src = window.URL.createObjectURL(blob);
            recordPlayer.srcObject = null;
            recordPlayer.controls = true; // 显示播放控件
        });

        // 下载按钮点击事件
        downloadBtn.addEventListener('click', function () {
            var blob = new Blob(buffer, { type: 'video/webm' });
            // 根据缓存数据生成url
            var url = window.URL.createObjectURL(blob);
            // 创建一个a标签,通过a标签指向url来下载
            var a = document.createElement('a');
            a.href = url;
            a.style.display = 'none'; // 不显示a标签
            a.download = 'test.webm'; // 下载的文件名
            a.click(); // 调用a标签的点击事件进行下载
        });

        // 开始录制
        function start() {

            var options = { mimeType: 'video/webm;codecs=vp8' };
            if (!MediaRecorder.isTypeSupported(options.mimeType)) {
                console.log('不支持' + options.mimeType);
                return;
            }

            try {
                buffer = [];
                mediaRecoder = new MediaRecorder(mediaStream, options);
            } catch (e) {
                console.log('创建MediaRecorder失败!');
                return;
            }
            mediaRecoder.ondataavailable = handleDataAvailable;
            // 开始录制,设置录制时间片为10ms(每10s触发一次ondataavilable事件)
            mediaRecoder.start(1000);
        }

        // 停止录制
        function stopRecord() {
            mediaRecoder.stop();
            mediaStream.getTracks().forEach(track => track.stop())
        }

        // 触发ondataavilable事件的回调函数
        function handleDataAvailable(e) {
            if (e && e.data && e.data.size > 0) {
                buffer.push(e.data);
            }
        }

        function startRecord() {
            if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) {
                console.log('不支采集音视频数据!');
            } else {
                // 采集音频数据
                var constrants = {
                    video: true,
                    audio: true
                };
                navigator.mediaDevices.getDisplayMedia(constrants).then(gotMediaStream).catch(handleError);

            }
        }

        // 采集音频数据成功时调用的方法
        function gotMediaStream(stream) {
            mediaStream = stream;
            player.srcObject = stream;

            start()
        }

        // 采集音频数据失败时调用的方法
        function handleError(err) {
            console.log(err.name + ':' + err.message);
        }
    </script>
</body>

</html>

新工具发布:keycode在线识别

需求

前端开发过程当中,我们有时候会需要监听键盘事件,比如使用document.onkeydown去监听,接着就要根据每个键盘的keycode来判断用户按了哪个键。

document.onkeydown = function (event) {
  var e = event || window.event || arguments.callee.caller.arguments[0];

  if (e && e.keyCode == 27) {
    // 按 Esc
    //要做的事情
  }

  if (e && e.keyCode == 113) {
    // 按 F2
    //要做的事情
  }

  if (e && e.keyCode == 13) {
    // enter 键
    //要做的事情
  }
};

为了找到keycode,通常我们都需要去搜索下,很麻烦。有没有快速查找这些keycode码的方法呢?

答案就是小编要推荐的这款工具,快速识键盘的keycode

工具

地址:https://dushusir.com/kcode/

使用方式:

  • 打开网页,直接敲击键盘,就能直接显示对应的keycode了
  • 点击右上角的table还能显示全部的按键对应的keycode,查找起来非常方便
  • 还可以切换暗黑和正常模式

总结

有任何疑问欢迎向作者反馈,更多小工具,欢迎访问:https://lwebapp.com/zh/tools

免费开源的高性能javascript电子表格:x-sheet入门体验

原文:免费开源的高性能JavaScript电子表格:X-Sheet入门体验

背景

随着互联网技术特别是网页应用的发展,我们对在线文档、轻应用的需求越来越高,很多小伙伴都已经习惯云办公,在线协同办公了,常用的就是腾讯文档、金山文档、石墨文档、飞书等,国外主要就是Google Sheet,最近小编发现了一款国人开发高性能前端javascript电子表格,受启发于Google Sheet,使用HTML5 canvas渲染。接下来,小编分享下入门体验。

介绍

首先放上x-sheet官方开源地址

https://gitee.com/eigi/x-sheet

https://github.com/eiji-th/x-sheet

在线体验地址:

https://lwebapp.com/example/x-sheet.html

可以从gitee上拉到最新代码到本地看看,这是一个标准的es6现代化工程,自定义webpack打包,运行后还有代码分析,其中还有好几个案例可以参考。

功能列表

我们先看看README上的功能说明

  • 撤销 & 反撤销
  • 格式刷
  • 清空格式
  • 文本格式
  • 字体设置
  • 字体大小
  • 字体加粗
  • 斜体字
  • 下划线
  • 删除线
  • 文字颜色
  • 单元格颜色
  • 单元格边框
  • 字体倾斜
  • 边框倾斜
  • 背景倾斜
  • 合并单元格
  • 水平对齐
  • 自动换行
  • 冻结单元格
  • 单元格函数 (处理中)
  • 行高和列宽设置
  • 复制, 剪切, 粘贴 (处理中)
  • 自动填充
  • 插入行, 列 (处理中)
  • 删除行, 列 (处理中)
  • 隐藏行, 列 (处理中)
  • 支持多个sheet表
  • 打印 (处理中)
  • 数据验证 (处理中)
  • 导出XLSX
  • 导入XLSX (处理中)
  • 导出CVS (处理中)
  • 导入CVS (处理中)
  • 导入图片 (处理中)
  • 数据筛选 (处理中)

使用

那么如何启动一个x-sheet demo呢?由于现在x-sheet还处在开发阶段,暂未提供npm包安装方式,我们可以从源码打包使用。

  1. clone代码
git clone https://gitee.com/eigi/x-sheet.git
  1. 进入代码目录,安装依赖
cd x-sheet
npm i
  1. 打包
npm run build
  1. 打包之后,我们可以自己新建一个html文件,引入打包后的源码使用
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>X-XWorkSheet</title>
        <link href="XSheet.css" rel="stylesheet">
        <style>
            * {
                padding: 0;
                margin: 0;
            }

            html, body {
                height: 100%;
                display: block;
            }
        </style>
        <script src="XSheet.js" type="text/javascript"></script>
    </head>
    <body id="demo">
        <script>
          const dome = document.getElementById('demo');
          const xSheet = new XSheet(demo);
        </script>
    </body>
</html>

或者直接进入源代码的example目录,直接用浏览器打开里面的.html文件就能看到官方给的丰富的本地模板和案例了,都是支持在线编辑的excel界面。

模板和案例

以下截图就展示了官方给出的案例

  1. 一个采购订单模板
    x-sheet模板-采购订单

    体验地址 https://lwebapp.com/example/x-sheet-purchase-order.html

  2. 一个出库单模板
    x-sheet模板-出库单

    体验地址 https://lwebapp.com/example/x-sheet-outbound-order.html

  3. 一个库存明细模板
    x-sheet模板-库存明细

    体验地址 https://lwebapp.com/example/x-sheet-inventory-details.html

  4. 一个旅行计划模板
    x-sheet模板-旅行计划

    体验地址 https://lwebapp.com/example/x-sheet-travel-plan.html

  5. 一个项目计划模板
    x-sheet模板-项目计划

    体验地址 https://lwebapp.com/example/x-sheet-project-plan.html

  6. x-sheet展示2019年日历,同样我们根据这个模板,可以构造更多年份的日历,做一个DIY日历
    x-sheet日历2020

    体验地址 https://lwebapp.com/example/x-sheet-calendar-2019.html

  7. x-sheet展示2020年日历
    x-sheet日历2020

    体验地址 https://lwebapp.com/example/x-sheet-calendar-2020.html

  8. x-sheet支持多实例,一个界面展示多个电子表格,可以分别编辑
    x-sheet多实例Demo

    体验地址 https://lwebapp.com/example/x-sheet-multi-instance.html

  9. x-sheet实测5万行25列的单元格数据,滑动很流畅
    x-sheet5万行数据测试

    体验地址 https://lwebapp.com/example/x-sheet-50000-rows-test.html

  10. x-sheet实测100万行25列的单元格数据,滑动也很流畅
    x-sheet100万行数据测试

    体验地址 https://lwebapp.com/example/x-sheet-1000000-rows-test.html

注意事项

  1. x-sheet支持Modern browsers(chrome, firefox)
  2. x-sheet现在的开源协议是 MOZILLA PUBLIC LICENSE

体验总结

大致体验了下x-sheet,可以说可圈可点

  • 功能上,丰富的单元格样式,富文本,边框倾斜,合并单元格,冻结,撤销重做,格式刷等核心功能,还支持导入导出xlsx文件,多实例,支持大数据量
  • 界面上,风格类似Google Sheet,清爽舒适,操作体验非常棒
  • 轻量,代码模块拆分清晰,二次开发可扩展性好

当然也有些不足,比如不支持协同,快捷键支持不完善等,小编体验x-sheet时,x-sheet还处于开发阶段,所以难免有些bug存在,如果要在生产使用,需要自己多做测试。这是一款国人开发的开源免费的电子表格,希望大家多多给予支持,也欢迎踊跃提交issue和PR交流探讨,希望作者能添加更多功能,继续维护好这个表格插件。

原文:免费开源的高性能JavaScript电子表格:X-Sheet入门体验

WordPress入门零基础搭建网站:Elementor教程操作步骤

Elementor是目前最强大的页面构建器。完全拖放式可视化编辑,是WordPress中最快,最直观的编辑器。只需拖放即可自定义,对于新手非常友好,非常易于使用和掌握。无需编码; 生成的页面代码紧凑且针对每个设备进行了优化,Elementor有300多种精美的模板,从几十个小部件中进行选择,以创建你需要的任何内容:按钮,标题,表格,只要你能想到的,没有什么是你不能做的。

很多人认为建站太难了,又要懂代码、网页设计,数据库,又要配置服务器等等,无从下手。那怎么办?曾经的不可能,现在可以轻易的去现实它,这里教大家如何零基础用页面构建器插件 Elementor主题(超轻框架) ,快速学会用 WordPress 建立你理想中的网站。

Elementor 页面编辑器

Elementor是目前最强大的页面构建器。完全拖放式可视化编辑,是WordPress中最快,最直观的编辑器。只需拖放即可自定义,对于新手非常友好,非常易于使用和掌握。无需编码; 生成的页面代码紧凑且针对每个设备进行了优化,Elementor有300多种精美的模板,从几十个小部件中进行选择,以创建你需要的任何内容:按钮,标题,表格,只要你能想到的,没有什么是你不能做的。

安装 Elementor 插件

在WordPress仪表板中,导航到“  插件”>“安装插件”。搜索Elementor并单击“  安装”。安装Elementor后,单击“  激活”

免费版的功能不全,本教程是按专业版来操作,Elementor Pro才能充分发挥Elementor的价值。访问Elementor并购买该插件的专业版,然后以与上述相同的方式安装和激活它。安装Elementor Pro后,再通过转到的WordPress的仪表板并导航到Elementor>许可证来输入许可证

 

创建网站页面

前面我们已经介绍了基本准备工作设置,并且你已准备好所有内容,现在我们开始创建页面。在本教程中,我们将构建一个简单的网站,其中包含主页,菜单页面,关于页面和联系我们页面。我们将在本教程的其余部分使用Elementor Pro  模板导入,让我们开始吧。

使用Elementor创建你的主页

安装插件后,现在我们开始创建主页。转到“页面”选项,然后单击“新建页面”。

我们把页面标题为“主页”,然后单击“保存草稿”按钮。一定要养成不断保存工作的习惯。如果发生紧急情况,我们可以恢复,这可以减少失去劳动成果的机会。

在标题下,单击“使用Elementor编辑”按钮以打开插件的编辑器。

导入Elementor模板

刚开始使用Elementor的最简单的方法就是套用模板库里的模板。这样你就可以找出这个效果是如何设置的并探索该工具在实时环境中可以执行的操作。

单击位于屏幕中心附近的“添加模板”按钮。

Elementor有多种模板可供选择。有免费的和付费Pro版本的插件。找到你喜欢的布局,然后单击“插入”按钮。

更改页面布局

将鼠标悬停在模板上时,你会看到一个浅蓝色控制面板。中间图标是该部分的编辑器。单击此图标左侧面板视图出现,编辑模板布局。布局下面可以看到内容宽度。单击将布局更改为全宽。

更改页面的内容

你可以开始自定义页面的内容。这可以通过插入各种工具来完成。要编辑页面的任何元素,只需单击它们,左侧面板将更改为该特定部分的编辑器。

你可以更改字体,颜色,大小甚至每个标题标签在页面上的显示方式。

更改背景图像

另一个重要的部分是自定义背景图像。按照自己的意图来更改背景。

返回“编辑部分”屏幕,然后单击“样式”。

删除那里的图像后,选择一个你想要的图像。

在更改背景时,有多个选项。在Elementor中背景可以使用渐变,和视频内容。

更改字体颜色

如果要更改页面的字体颜色,单击要更改的元素,然后单击“样式”选项卡。从这里,你可以使用颜色选择器修改文本颜色,或者输入十六进制代码(如果有)。还可以访问各种字体供你选择。

选择字体外观时,最好不要包含太多不同颜色设置。过多的色往往会分散注意力,并可能导致访客几乎立即离开。

完成更改后,单击左下角的“保存”按钮。

在Elementor保存页面后,单击左下角的的“全局设置”选项。

将状态下拉菜单切换为“已发布”。

单击左上角的“菜单”图标退出到仪表板。

将页面设置为你网站的主页

现在页面已创建并发布,是时候将其设置为主页了。转到WordPress的“设置”菜单,然后单击“阅读”。

在“首页显示”选项下,你将看到一个显示静态页面的单选按钮。这告诉WordPress你要使用特定页面作为你网站的主页。单击“静态页面”旁边的单选按钮。

现在可以更改首页和帖子页面选项。现在,单击下拉窗口,然后单击刚才创建的“主页”页面。

拉到设置页面的底部,然后单击“保存更改”按钮。

现在我们完成了在Elementor中创建主页。

创建其余的网站页面

接下来我们用同样的方法创建“关于我们”或“联系我们”页面。

自定义你的网站

上面我们在讲创建主页时有讲到自定义,下面我们以另一个模板做一个补充。

编辑区域部分

当你将鼠标悬停在某个区域部分上时,你会看到一个蓝色边框,其中包含一个句柄,可让你编辑该部分,添加新区域部分或完全删除该区域页面。>你也可以从那里右键单击手柄和访问区域控件,如下面的屏幕截图所示。

单击+图标可以在现有区域上方添加新部分。然后,你必须单击粉红色+或文件夹图标以创建部分的结构或选择预制模板。

编辑内部部分小部件或列元素

以类似的方式,你可以单击灰色边框和相应的手柄来编辑内部部分。您可以修改列的宽度并更改列之间的间距。右键单击句柄,可以添加更多列,复制现有列,删除它们等。

编辑小部件

最后,你可以通过单击窗口小部件句柄中的铅笔图标或右键单击它来编辑现有窗口小部件。你可以更改它的内容以及访问样式控件,复制窗口小部件,删除它们等。

还可以通过从屏幕左侧拖动它们并将它们放到页面上来添加新的小部件。

自定义颜色

你可能最想要做的自定义是调整模板的颜色。调整为符合你的业务或品牌颜色,并使用自己的风格个性化模板。你可以在全局级别和单个窗口小部件级别修改颜色。在全局级别编辑颜色意味着你使用Elementor创建的任何新页面都将继承这些样式,因此你只需创建结构并输入内容即可。要修改全局颜色设置,请单击页面编辑屏幕上的左汉堡菜单。然后,单击“样式”菜单下的“默认颜色”链接。你也可以通过输入自己的十六进制颜色代码来调整默认颜色方案,或者如果你没有自己的颜色方案,则可以应用预先制定的颜色方案。可以通过单独自定义每个窗口小部件来覆盖全局样式设置。要自定义单个窗口小部件的颜色,请选择要设置样式的窗口小部件,然后单击小部件编辑屏幕上的“样式”选项卡

自定义排版

与颜色类似,你可以在全局级别和单个窗口小部件级别编辑站点的排版。

在全局范围内,你可以为主要和次要标题,正文和重音文本设置字体系列和字体粗细。

要设置单个窗口小部件的排版样式,请单击要编辑的窗口小部件,然后单击样式。从Family旁边的下拉菜单中,可以选择上百种Google字体,也可以选择默认,并应用在全局设置下设置的默认字体。

自定义图像

上面我们讲了自定义颜色和字体,图像替换也一样的简单。单击要换的部分。在这种情况下,我想更改“精选菜单”部分中的图像。我点击了Kiwi Bliss列,然后转到Style选项卡。首先,你需要通过单击“ 删除”删除现有图像,然后单击“ +”以上载您自己的图像。加载媒体库后,单击“上载文件”,然后单击“ 选择文件”按钮。然后,你将选择所需的图像,然后单击“ 插入媒体”按钮。还可以更改其位置并控制其大小。还可以选择更改背景覆盖或完全删除它,以及设置图像文本的颜色。如果你使用常规图像而不是背景图像,则选项会略少。例如,要更改和自定义主页的“膳食定价”部分中的图像,您将按照删除现有图像然后上传自己的图像的相同步骤进行操作。然后,你可以更改图像不透明度并添加各种CSS滤镜,例如“模糊”,“亮度”等。你可以为图像悬停状态应用不同的样式,并且一旦访问者用鼠标悬停在图像上,这些样式将可见。要自定义网站上的其他图片,发布以保存它们。

自定义页面布局

根据自己的喜好配置所有样式后,就可以开始调整布局了。例如,我们正在使用的主页有3列,但如果你的企业有4个服务怎么办?在这种情况下,您需要添加另一列。

要执行此操作,请右键单击内部区域手柄,然后选择“  添加”列。将创建一个新列,然后你可以继续添加所需的小部件。或者,你可以复制现有列以节省时间。复制完列后,将内容替换为你自己的内容。

你也可以在页面中添加其他部分,如前所述。以下是添加示例关于部分的分步过程:

  1. 单击菜单定价下方部分句柄中的+图标。
  2. 将结构设置为一列
  3. 将列调整为全宽
  4. 拖动标题窗口小部件并键入标题
  5. 拖动内部窗口小部件并选择两列
  6. 将图像添加到左列,并将文本小部件添加到右列
  7. 从计算机中选择一张图像并将其上传到图像小部件
  8. 在“文本”小部件中输入业务的简要说明
  9. 拖动文本小部件下方的按钮小部件
  10. 设置按钮以链接到“关于”页面。

完成所需更改后,单击绿色“  发布”按钮,更改就会马上生效。

添加社交媒体链接

如上图,我们正在使用的模板带有页脚中的社交媒体链接。单击图标并使用链接到首选社交媒体配置文件的图标替换它们来轻松编辑它们。单击“ 添加项目” 按钮添加更多链接。通过更改形状自定义图标,然后单击“ 样式” 选项卡以控制图标颜色,大小,填充等。从下面的屏幕截图中可以看出,我添加了一个Pinterest图标,并将图标更改为蓝色的方形。值得一提的是,拖动社交链接在页面的任何位置添加社交图标小部件。然后,选择要显示的图标,并更改形状,大小,颜色等来配置它们的显示方式。在此示例中,我已将图标添加到上一步中创建的“关于”部分。完成修改主页后,单击“ 发布” ,然后单击“ 退出到仪表板” 按钮。

自定义联系表单

到目前为止,大多数自定义都发生在主页上。让我们来看看如何调整联系我们页面上的联系表单等其他元素。首先返回“ 页面”>“所有页面” 并选择“ 联系我们” 页面。单击使用Elementor编辑

单击表单小部件,然后开始修改信息。单击各个表单元素以添加电子邮件,电话号码,日期,文本,网站等字段。单击“ 添加项目” 按钮添加更多字段。

在这个例子中,我为座位选项添加了一个字段,以便潜在客户可以选择室内或室外桌子。

该表单还允许你选择将接收表单提交的电子邮件以及填写表单后应发生的情况。选择接收电子邮件通知,将访问者重定向到特殊的感谢页面,将其添加到您的电子邮件列表等。完成后,单击绿色“ 发布” 按钮,您的更改将在页面上生效。

自定义复制

直接编辑小部件并用你自己的文本替换文本。也可以从任何页面开始,转到“页面”>“所有页面” ,然后将鼠标悬停在要编辑的页面上。单击Edit with Elementor 。在下面的示例中,我正在编辑“关于”页面副本。

加载编辑屏幕后,单击要编辑的文本。你会注意到左侧显示的文本编辑器看起来像常规的WordPress帖子编辑器。然后,你可以编辑器里更改文本,添加图像,表单,链接,格式化文本等。

要编辑站点上的其他文本区域,只需重复上述步骤即可。单击文本,删除现有副本,然后输入你自己的内容。完成后,单击绿色的“  发布” 按钮,更改将在页面上生效。

在编辑文本时,你仍然可以对排版进行调整并调整文本的颜色,如前面所述。

添加页头和页脚模板

使用Elementor模块功能,你可以添加自定义或预制的页眉和页脚模板,并在网站的每个页面上使用它们,或者你可以为每个页面创建不同的标题。

添加页头

要创建页头,请单击任何页面上最顶部分的图标,然后单击“  文件夹” 图标。加载模板库后,单击模块,然后从下拉菜单中选择Header 类别。

选择你喜欢的页头,然后单击“  插入” 按钮。然后,单击“  部分设置” ,在“  布局”设置下将HTML标记设置页头。还可以将宽度设置为全宽,调整窗口小部件之间的间距,向其添加更多窗口小部件等。

在下面的截图中看到的,我删除了搜索图标并将其替换为社交图标。完成调整标题后,右键单击其句柄并选择另存为模板。然后,你可以在网站的所有页面上重复使用标头。

添加自定义页脚

以类似的方式,为页面创建页脚部分。首先单击页面上最后一部分下方的文件夹图标,然后从下拉菜单中选择“ 模块****”,然后选择“  页脚  ”。选择喜欢的页脚模块,然后单击“  插入”。可以使用与页面上任何其他部分和窗口小部件相同的方式设置页脚元素的样式。

完成页脚设计后,单击其句柄并按“  另存为”模板。为其命名并单击“  保存”,然后可以在你网站的任何页面上重复使用它。完成页眉和页脚元素的添加和样式设置后,只需单击“  发布”即可屏幕左侧的按钮,你的页面将完全自定义并在你的网站上生效。

保存页眉和页脚后,返回并将其应用到其他页面。要执行此操作,请转到“  页面”>“所有页面”,然后选择要使用Elementor编辑的页面。然后,单击最上面部分上方的+图标,单击文件夹图标并转到我的模板。找到已保存的标题部分,然后单击“  插入” 按钮。要添加页脚,请滚动到页面底部,然后单击最底部的文件夹图标。单击我的模板,然后选择已保存的页脚块。单击更新  保存更改。

自定义WordPress

完成页面上的所有更改后,我们还需要在WordPress中进行一些最后的自定义。上传LOGO,创建导航菜单。

上传网站标志-LOGO

首先我们来上传一个LOGO。为此,在WordPress仪表板中转到“  外观”>“自定义”。加载定制程序后,单击“  站点身份”,然后单击“  选择图标” 

加载媒体库时,单击“ 上传****文件****”,上传好后,单击“  发布”以保存更改。

创建一个导航菜单

转到外观>菜单,然后输入菜单名称。然后,单击“ 创建菜单”。从那里,检查创建并在屏幕右侧上方自定义的页面。

你可以拖动页面以自定义它们显示的顺序,并选择此菜单的位置。完成自定义菜单后,单击“ 保存菜单”

最后,我们已经在教程中介绍了很多方法,但愿你已经成功的学会了从头开始为你的业务创建了一个站点。正如你所看到的,当你知道如何处理网站设计以及所涉及的内容时,你会发现,真的不难啊。

原文:https://blog.csdn.net/weixin_49617016/article/details/108979433,感谢

js颜色值按深浅排序 支持rgb/hex/hsl

根据颜色深浅排序

需求

最近参照组件库的设计,获取常用的组件颜色,比如这样的一系列灰色值

#fafafc
#f8f8f8
#f5f5f5
#efefef
#e0e0e0
#d9d9d9
#cccccc
#cbcbcb
#bfbfbf

然后要给它们由浅到深排个序,便于使用时知道加深程度,有个对比。

解决方案

将获取到的颜色统一转化为rgb格式,用公式r*0.299 + g*0.587 + b*0.114计算得到每一个颜色的灰度值,从小到大进行由浅到深的排序。

其中还要解决颜色的转化,以支持识别各类颜色。

代码

转换十六进制色值为rgb格式

function hextoRgb(color) {
  color = color.slice(1);
  var rgb = "";
  for (var i = 0; i < color.length; i += 2) {
    var end = i + 2;
    rgb += parseInt(color.slice(i, end), 16).toString() + ",";
  }
  rgb = rgb.slice(0, rgb.length - 1);
  rgb = "rgb(" + rgb + ")";
  return rgb;
}

hsl格式 转rgb

function getMid(str) {
  var left = str.indexOf("(") + 1;
  var right = str.indexOf(")");
  return str.slice(left, right);
}
//  hsl格式 转rgb
function hslToRgb(color) {
  var arr = getMid(color).split(",");
  var r, g, b;
  var h = toNum(arr[0] / 360 + ""),
    s = toNum(arr[1]),
    light = toNum(arr[2]); // h(色相) s(饱和度) l(亮度)
  var temp2, temp1;
  if (s == 0) {
    r = g = b = light;
  } else {
    temp2 = light < 0.5 ? light * (1 + s) : light + s - light * s;
    temp1 = 2 * light - temp2;

    var h2rgb = function (p, q, t) {
      if (t < 0) t += 1;
      if (t > 1) t -= 1;
      if (t < 1 / 6) return p + (q - p) * 6 * t;
      if (t < 1 / 2) return q;
      if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
      return p;
    };
    r = h2rgb(temp1, temp2, h + 1 / 3);
    g = h2rgb(temp1, temp2, h);
    b = h2rgb(temp1, temp2, h - 1 / 3);
  }
  return (
    "rgb(" +
    Math.round(r * 255) +
    "," +
    Math.round(g * 255) +
    "," +
    Math.round(b * 255) +
    ")"
  );
}
function toNum(str) {
  var rex = /[0-9]+/g;
  if (str.indexOf("%") > 0) {
    return str.match(rex)[0] / 100;
  } else {
    if (typeof +str === "number") {
      return +str;
    }
  }
}

获取颜色灰色值

function grayLevel(color) {
  color = isRgb(color)
    ? color
    : isHex(color)
    ? hextoRgb(color)
    : hslToRgb(color);
  var arr = getMid(color).split(",");
  var r = arr[0],
    g = arr[1],
    b = arr[2];
  return r * 0.299 + g * 0.587 + b * 0.114;
}

各类颜色判断

// 是否为rgb颜色值
function isRgb(color) {
  return /^rgb/.test(color);
}
// 是否为十六进制颜色值
function isHex(color) {
  return /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(color);
}
// 是否为hsl颜色值
function isHsl(color) {
  return /^hsl/.test(color);
}

工具

小编直接做了一个颜色转换工具,方便直接使用

颜色排序

总结

主要代码也是参照网上大神来做的,小编觉得还有很多优化空间,包括各类颜色的转换,颜色的处理,颜色json的处理等。

参考

Tree树形控件展示当前路径

问题

现有一个树形结构的菜单,支持无限的嵌套层级,需要根据用户点击的某个子菜单,获取到这个子菜单所在节点的路径,类似于面包屑效果。

一个合格的树形菜单数据结构

const menu = [{
          id:'1',
          label: '一级 1',
          children: [{
            id:'11',
            label: '二级 1-1',
            children: [{
              id:'111',
              label: '三级 1-1-1'
            }]
          }]
        }, {
          id:'2',
          label: '一级 2',
          children: [{
            id:'21',
            label: '二级 2-1',
            children: [{
              id:'211',
              label: '三级 2-1-1'
            }]
          }, {
            id:'22',
            label: '二级 2-2',
            children: [{
              id:'221',
              label: '三级 2-2-1'
            }]
          }]
        }, {
          id:'3',
          label: '一级 3',
          children: [{
            id:'31',
            label: '二级 3-1',
            children: [{
              id:'311',
              label: '三级 3-1-1'
            }]
          }, {
            id:'32',
            label: '二级 3-2',
            children: [{
              id:'321',
              label: '三级 3-2-1'
            }]
          }]
        }]

解决方案

思路:

递归整个树形菜单,记录下每次循环时候的当前路径名称,匹配到之后再把所有节点名称拼接起来。

其中有一个关键处理点是每次循环到同级数组,需要重新开始记录路径,否则会出现路径重复的情况

代码:

/**
 * @param {array[][]} data 整个菜单数组
 * @param {object[][]} v 当前节点对象
 * @return {string[]} 当前节点对象路径
 */

function findPath(data, v){
            var find = false; //找到id后标记,结束递归
            var paths = [];

            findId(data, v.id);

            return paths.join('/');

            function findId(root, vid) {

                var currentPathsLength = paths.length;

                root.forEach((ele, i, root) => {
                    // 后面不再循环找了
                    if (find) {
                        return
                    }

                    // 每次循环到同级数组,重新开始记录路径
                    paths = paths.slice(0,currentPathsLength);
                    paths.push(ele.label)


                    // 找到即返回
                    if (ele.id === vid) {
                        find = true;
                        return;
                    } else if (Array.isArray(ele.children) && ele.children.length > 0) { //有子节点继续寻找
                        return findId(ele.children, vid)
                    }

                });

            }
        }

在线演示:

See the Pen
zYKeVJe
by alex (@dushusir)
on CodePen.

base64转blob并下载文件

需求

支持文件转为base64,base64转blob,blob下载为文件

代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h2>base64 to file</h2>
    <textarea name="" id="text-area" cols="30" rows="10" style='width:100%;height: 200px;' placeholder="Paste base64 string here"></textarea>
    <button class="download-btn" style='width: 150px;height:50px;display: block;margin:10px auto;'>Download</button>
    <script>

            /**
            * 上传附件转base64
            * @param {File} file 文件流
            */
            const fileByBase64 = (file, callback) => {
                var reader = new FileReader();
                // 传入一个参数对象即可得到基于该参数对象的文本内容
                reader.readAsDataURL(file);
                reader.onload = function (e) {
                    // target.result 该属性表示目标对象的DataURL
                    console.log(e.target.result);
                    callback(e.target.result)
                };
            }
            /**
            * base64转Blob
            * @param {*} data 
            */
            const base64ByBlob = (base64, callback) => {
                var arr = base64.split(','), mime = arr[0].match(/:(.*?);/)[1],
                    bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
                while (n--) {
                    u8arr[n] = bstr.charCodeAt(n);
                }
                console.log(new Blob([u8arr], { type: mime }))
                callback(new Blob([u8arr], { type: mime }))
            }

            document.querySelector('.download-btn').addEventListener('click',function(e) {
                const base64 = document.querySelector('.text-area').inneerText;

                // fileByBase64(file, (base64) => {
                    base64ByBlob(base64, (blob) => {
                    // var url = window.URL.createObjectURL(blob)


                    const fileName = `${new Date().valueOf()}.txt`;
                    const link = document.createElement('a');
                    link.href = window.URL.createObjectURL(blob);
                    link.download = fileName;
                    link.click();
                    window.URL.revokeObjectURL(link.href);
                })
                // })

            })


    </script>
</body>
</html>