Giter VIP home page Giter VIP logo

tinylib-analysis's Introduction

tinylib-analysis's People

Contributors

haiweilian avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

Forkers

handcodes

tinylib-analysis's Issues

005.[mitt]-发布订阅和扩展功能(once、async)

前言

这是一个很小型的发布订阅库 https://github.com/developit/mitt

是的它很小只有 200b,既然小当然功能简洁。作者为了压缩后文件大小绝对不能大于 200b 所以社区提的功能请求并没有解决,这次除了看源码外,再尝试解决一下未实现的功能请求 Async extension for mitt

环境

依赖包内置了 ts-node 如果想直接运行 ts 的文件了使用 npx ts-node xxx.ts

源码

类型声明

从类型声明就可以大概看出存储的什么结构,有一个总集合 MapMap 的一个 Key 对应多个回调函数。

// 事件类型
export type EventType = string | symbol;

// 基础回调函数
export type Handler<T = unknown> = (event: T) => void;
// 通配符回调函数
export type WildcardHandler<T = Record<string, unknown>> = (
  type: keyof T,
  event: T[keyof T]
) => void;

// 基础回调集合
export type EventHandlerList<T = unknown> = Array<Handler<T>>;
// 通配符的回调集合
export type WildCardEventHandlerList<T = Record<string, unknown>> = Array<WildcardHandler<T>>;

// 事件类型和回调的映射 { foo: [fn1, fn2] }
export type EventHandlerMap<Events extends Record<EventType, unknown>> = Map<
  keyof Events | "*",
  EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events>
>;

// Emitter 实例类型
export interface Emitter<Events extends Record<EventType, unknown>> {
  all: EventHandlerMap<Events>;

  // 函数重载 "on"
  // 类型约束 Key extends keyof Events
  // 索引类型查询操作符 keyof Events
  // 索引访问操作符 Events[Key]
  on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void;
  on(type: "*", handler: WildcardHandler<Events>): void;

  off<Key extends keyof Events>(type: Key,handler?: Handler<Events[Key]>): void;
  off(type: "*", handler: WildcardHandler<Events>): void;

  emit<Key extends keyof Events>(type: Key, event: Events[Key]): void;
  emit<Key extends keyof Events>(type: undefined extends Events[Key] ? Key : never): void;
}

功能实现

on

on 的作用就是以 type 为键和分类把回调收集起来。

export default function mitt<Events extends Record<EventType, unknown>>(): Emitter<Events> {
  all = all || new Map();
  return {
    on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
      // 获取到对应类型集合
      const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
      // 如果已存在,直接 push 追加
      if (handlers) {
        handlers.push(handler);
      } else {
        // 反之,创建一个新的集合
        all!.set(type, [handler] as EventHandlerList<Events[keyof Events]>);
      }
    },
  };
}

off

off 的作用就是根据 type 找到对应的函数从集合中删除,如果没传入回调则全部删除。

关于 handlers.indexOf(handler) >>> 0,这有一遍文章 https://segmentfault.com/a/1190000014613703

export default function mitt<Events extends Record<EventType, unknown>>(): Emitter<Events> {
  all = all || new Map();
  return {
    off<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) {
      // 获取到对应类型集合
      const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
      if (handlers) {
        if (handler) {
          // 回调存在,找到对应的函数删除,只删除一个。
          // 关于 -1 >>> 0 : https://segmentfault.com/a/1190000014613703
          handlers.splice(handlers.indexOf(handler) >>> 0, 1);
        } else {
          // 不存在清空此类型收集的回调
          all!.set(type, []);
        }
      }
    },
  };
}

emit

emit 的作用就是以 type 获取到对应的集合,依次运行对应的函数。

关于为什么要用一次 slice developit/mitt#109

export default function mitt<Events extends Record<EventType, unknown>>(): Emitter<Events> {
  all = all || new Map();
  return {
    emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
      let handlers = all!.get(type);
      if (handlers) {
        (handlers as EventHandlerList<Events[keyof Events]>)
          // Why use slice: https://github.com/developit/mitt/pull/109
          .slice()
          // 执行对应类型的所有回调
          .map((handler) => {
            handler(evt!);
          });
      }

      // 每次派发都执行通配符的回调
      handlers = all!.get("*");
      if (handlers) {
        (handlers as WildCardEventHandlerList<Events>)
          .slice()
          .map((handler) => {
            handler(type, evt!);
          });
      }
    },
  };
}

功能扩展

在翻看 Issues 的时候发现有两个功能讨论的比较多(也有给出方案但感觉不完善),有用也是有用就是作者不想大小不想超过预期。所以啊作者不实现的我们就得根据自己的需要去改。所以我对这两个功能尝试在不改动源码的情况下去解决,现在用不到以后不一定了。

once

once 是只触发一次。所以实现就是触发一次之后立刻解除监听。实现方式为对原始的功能进行包装。

import mitt from "../src/index";
import type { EventType, EventHandlerMap, Emitter, Handler } from '../src/index';

// 继承 Emitter 基础接口
export interface EmitterOnce<Events extends Record<EventType, unknown>> extends Emitter<Events> {
  once<Key extends keyof Events>(type: Key,handler: Handler<Events[Key]>): void;
}

export default function mittOnce<Events extends Record<EventType, unknown>>(
  all?: EventHandlerMap<Events>
): EmitterOnce<Events> {
  const emitter = mitt<Events>(all);

  return {
    // 原始方法
    ...emitter,

    // 扩展 once
    once<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>) {
      const fn = (arg: Events[Key]) => {
        // 执行一次,立刻解除监听
        emitter.off(type, fn);
        handler(arg);
      };
      emitter.on(type, fn);
    },
  };
}

测试示例和结果如下。

import mittOnce from "./once";

type Events = {
  foo?: string;
};

const emitter = mittOnce<Events>();

function A() {
  console.log("A");
}

function B() {
  console.log("B");
}

emitter.on("foo", A);
emitter.once("foo", B);
emitter.emit("foo"); // A B
emitter.emit("foo"); // A

async

比如我要 emit 事件,我还想知道触发的事件是否全部执行完毕了。这里我扩展了两个 api 分别是 串行(emitSerial) 和并行(emitParallel)。这两个功能都是对原始的函数使用 Promise 去执行。

import mitt from "../src/index";
import type { EventType, EventHandlerMap, Emitter, EventHandlerList, WildCardEventHandlerList } from '../src/index';

// 继承 Emitter 基础接口
export interface EmitterAsync<Events extends Record<EventType, unknown>>
  extends Emitter<Events> {
  emitSerial<Key extends keyof Events>(type: Key, event: Events[Key]): Promise<void>;
  emitSerial<Key extends keyof Events>(type: undefined extends Events[Key] ? Key : never): Promise<void>;

  emitParallel<Key extends keyof Events>(type: Key, event: Events[Key]): Promise<void>;
  emitParallel<Key extends keyof Events>(type: undefined extends Events[Key] ? Key : never): Promise<void>;
}

export default function mittAsync<Events extends Record<EventType, unknown>>(
  all?: EventHandlerMap<Events>
): EmitterAsync<Events> {
  const emitter = mitt<Events>(all);

  return {
    // 原始方法
    ...emitter,

    // 串行  Promise.then().then()
    async emitSerial<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
      let handlers = emitter.all!.get(type);
      if (handlers) {
        const callbacks = (handlers as EventHandlerList<Events[keyof Events]>).slice();
        // compose run
        await callbacks.reduce(
          (promise, callback) => promise.then(() => callback(evt!)),
          Promise.resolve()
        );
      }

      // 每次派发都执行通配符的回调
      handlers = emitter.all!.get("*");
      if (handlers) {
        const callbacks = (handlers as WildCardEventHandlerList<Events>).slice();
        // compose run
        await callbacks.reduce(
          (promise, callback) => promise.then(() => callback(type, evt!)),
          Promise.resolve()
        );
      }
    },

    // 并行  Promise.all
    async emitParallel<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
      let handlers = emitter.all!.get(type);
      if (handlers) {
        const callbacks = (handlers as EventHandlerList<Events[keyof Events]>).slice();
        // Promise.all run
        await Promise.all(
          callbacks.map((handler) => Promise.resolve(handler(evt!)))
        );
      }

      // 每次派发都执行通配符的回调
      handlers = emitter.all!.get("*");
      if (handlers) {
        const callbacks = (handlers as WildCardEventHandlerList<Events>).slice();
        // Promise.all run
        await Promise.all(
          callbacks.map((handler) => Promise.resolve(handler(type, evt!)))
        );
      }
    },
  };
}

测试示例和结果如下。

import mittAsync from "./async";

type Events = {
  foo?: string;
};

const emitter = mittAsync<Events>();

async function A() {
  await new Promise((reslove) => {
    setTimeout(() => {
      console.log("A");
      reslove("A");
    }, 2000);
  });
}

function B() {
  return new Promise((reslove) => {
    setTimeout(() => {
      console.log("B");
      reslove("B");
    }, 1000);
  });
}

function C() {
  console.log("C");
}

emitter.on("foo", A);
emitter.on("foo", B);
emitter.on("foo", C);

// 原始 C D B A
emitter.emit("foo");
console.log("D");

// 串行 A B C D
(async () => {
  await emitter.emitSerial("foo");
  console.log("D");
})();

// 并行 C B A D
(async () => {
  emitter.emitParallel("foo").then(() => {
    console.log("D");
  });
})();

总结

  1. 高级类型平常用业务的不多,一般也就类库中应用的多,也学也忘了刚好回顾下。

  2. 扩展功能中用到了 compose 刚好是对前几期源码的应用。

009.[validate-npm-package-name]-检测 NPM 包是否符合标准

前言

这个包是 npm 官方出的一个检测 npm 包是否符合标准。https://github.com/npm/validate-npm-package-name

源码

依赖

依赖了一个模块 builtins 里面枚举了 node 的核心模块,我们不能和内置模块起一样的名字,不然不就冲突了吗。

var builtins = require("builtins");

变量

有一个 scopedPackagePattern 长正则是用来验证 命名空间 格式的,用 regulex 来解析下比较清楚。以 @ 开头,中间 一或多个字符,然后包含 /,最后以 一或多个字符 结尾。

1.png

// 命名空间的格式验证
var scopedPackagePattern = new RegExp("^(?:@([^/]+?)[/])?([^/]+?)$");
// 黑名单列表
var blacklist = ["node_modules", "favicon.ico"];

判断

然后就是对名称各种判断了,都是能看得懂的常规判断。

var validate = (module.exports = function (name) {
  // 警告信息
  var warnings = [];
  // 错误信息
  var errors = [];

  // 名称不能为 null
  if (name === null) {
    errors.push("name cannot be null");
    return done(warnings, errors);
  }

  // 名称不能为 undefined
  if (name === undefined) {
    errors.push("name cannot be undefined");
    return done(warnings, errors);
  }

  // 名称必须是一个字符串
  if (typeof name !== "string") {
    errors.push("name must be a string");
    return done(warnings, errors);
  }

  // 名称不能为空
  if (!name.length) {
    errors.push("name length must be greater than zero");
  }

  // 名称不能以 . 开头
  if (name.match(/^\./)) {
    errors.push("name cannot start with a period");
  }

  // 名称不能以 _ 开头
  if (name.match(/^_/)) {
    errors.push("name cannot start with an underscore");
  }

  // 名称前后不能包含空格
  if (name.trim() !== name) {
    errors.push("name cannot contain leading or trailing spaces");
  }

  // 名称不能是黑名单列表的
  blacklist.forEach(function (blacklistedName) {
    if (name.toLowerCase() === blacklistedName) {
      errors.push(blacklistedName + " is a blacklisted name");
    }
  });

  // 名称不能是内置核心模块
  builtins.forEach(function (builtin) {
    if (name.toLowerCase() === builtin) {
      warnings.push(builtin + " is a core module name");
    }
  });

  // 名称最大长度不能超过 214
  if (name.length > 214) {
    warnings.push("name can no longer contain more than 214 characters");
  }

  // 名称不能包含大小字符
  if (name.toLowerCase() !== name) {
    warnings.push("name can no longer contain capital letters");
  }

  // 不能包含特殊字符
  if (/[~'!()*]/.test(name.split("/").slice(-1)[0])) {
    warnings.push('name can no longer contain special characters ("~\'!()*")');
  }

  // 如果是命名空间
  if (encodeURIComponent(name) !== name) {
    // Maybe it's a scoped package name, like @user/package
    var nameMatch = name.match(scopedPackagePattern);
    if (nameMatch) {
      var user = nameMatch[1];
      var pkg = nameMatch[2];
      if (encodeURIComponent(user) === user && encodeURIComponent(pkg) === pkg) {
        return done(warnings, errors);
      }
    }

    errors.push("name can only contain URL-friendly characters");
  }

  return done(warnings, errors);
});

总结

  1. 实话看源码比看文档的规则清楚多了。

  2. 如果要写一个脚手架啥的也可以查看这种规则去写或者可以直接使用。

调试方法

参考资料

VS Code 官方调试文档

自动附加

  1. VS Code 里按下 cmd + shift + p 打开命令面板。

  2. 搜索 toggle auto attach 并确认。

1

  1. 选择仅带标志(仅在给定 "--inspect" 标志时自动附加)。

2

  1. node --inspect xxx.js 运行文件进入调试模式。

3

008.[update-notifier]-检测 NPM 包是否更新

前言

用于提示当前本地的 npm 包是否是最新版本并给予提示。https://github.com/yeoman/update-notifier

1.png

源码

依赖

可以看到依赖了非常多的依赖包,实现是靠这些的组合这也考研了知识储备量。下面从三个阶段在解析整个的流程。

const { spawn } = require("child_process");
const path = require("path");
const { format } = require("util");
// 懒加载模块
const importLazy = require("import-lazy")(require);
// 配置存储
const configstore = importLazy("configstore");
// 终端字符颜色
const chalk = importLazy("chalk");
// 语义化版本
const semver = importLazy("semver");
// 语义化版本比较差异
const semverDiff = importLazy("semver-diff");
// 获取 npm 上的最新版本号
const latestVersion = importLazy("latest-version");
// 检测运行文件的报管理工具 npm or yarn
const isNpm = importLazy("is-npm");
// 检测安装包是否全局安装
const isInstalledGlobally = importLazy("is-installed-globally");
// 检测安装包是否 yarn 全局安装
const isYarnGlobal = importLazy("is-yarn-global");
// 检测项目是否使用 yarn
const hasYarn = importLazy("has-yarn");
// 在终端创建一个框显示
const boxen = importLazy("boxen");
// 配置基础路径
const xdgBasedir = importLazy("xdg-basedir");
// 检测当前环境是否是持续集成环境
const isCi = importLazy("is-ci");
// 占位符的模板
const pupa = importLazy("pupa");

解析配置阶段

这一步主要是对传入的参数进行解析,并存储起来。并利用了 configstore 持久化存储信息。

class UpdateNotifier {
  // 解析配置阶段
  constructor(options = {}) {
    // 解析配置,从不同参数中解析出 packageName 和 packageVersion
    this.options = options;
    options.pkg = options.pkg || {};
    options.distTag = options.distTag || "latest";

    // Reduce pkg to the essential keys. with fallback to deprecated options
    // TODO: Remove deprecated options at some point far into the future
    options.pkg = {
      name: options.pkg.name || options.packageName,
      version: options.pkg.version || options.packageVersion,
    };

    if (!options.pkg.name || !options.pkg.version) {
      throw new Error("pkg.name and pkg.version required");
    }

    this.packageName = options.pkg.name;
    this.packageVersion = options.pkg.version;

    // 检测更新的间隔时间
    this.updateCheckInterval =
      typeof options.updateCheckInterval === "number"
        ? options.updateCheckInterval
        : ONE_DAY;

    // 是否禁用
    this.disabled =
      "NO_UPDATE_NOTIFIER" in process.env ||
      process.env.NODE_ENV === "test" ||
      process.argv.includes("--no-update-notifier") ||
      isCi();

    // npm 脚本时通知
    this.shouldNotifyInNpmScript = options.shouldNotifyInNpmScript;

    if (!this.disabled) {
      try {
        // 存储配置到本地文件
        const ConfigStore = configstore();
        this.config = new ConfigStore(`update-notifier-${this.packageName}`, {
          optOut: false,
          lastUpdateCheck: Date.now(),
        });
      } catch {
        // ...
      }
    }
  }
}

检测更新阶段

这一步主要做检测判断,比如通过时间判断是否应该再次检测,通过本地的包信息和远程最新的包信息检测是否是最新版本。检测的时候开启了一个单独的子进程去检测,并通过本地存储的信息交互结果。

class UpdateNotifier {
  // 检测更新阶段
  check() {
    // ....

    // 是否超过检测的间隔时间
    if (
      Date.now() - this.config.get("lastUpdateCheck") <
      this.updateCheckInterval
    ) {
      return;
    }

    // 执行检测脚本
    spawn(
      process.execPath,
      [path.join(__dirname, "check.js"), JSON.stringify(this.options)],
      {
        detached: true,
        stdio: "ignore",
      }
    ).unref();
  }

  async fetchInfo() {
    // 获取到最新的版本信息
    const { distTag } = this.options;
    const latest = await latestVersion()(this.packageName, {
      version: distTag,
    });
    // 返回两个版本的差异信息
    return {
      latest,
      current: this.packageVersion,
      type: semverDiff()(this.packageVersion, latest) || distTag,
      name: this.packageName,
    };
  }
}

通知更新阶段

最后就是在通过 boxen 在总端输出提示信息。

class UpdateNotifier {
  // 通知更新阶段
  notify(options) {
    const suppressForNpm = !this.shouldNotifyInNpmScript && isNpm().isNpmOrYarn;
    if (
      !process.stdout.isTTY ||
      suppressForNpm ||
      !this.update ||
      !semver().gt(this.update.latest, this.update.current)
    ) {
      return this;
    }

    options = {
      isGlobal: isInstalledGlobally(),
      isYarnGlobal: isYarnGlobal()(),
      ...options,
    };

    // 根据环境提示命令
    let installCommand;
    if (options.isYarnGlobal) {
      installCommand = `yarn global add ${this.packageName}`;
    } else if (options.isGlobal) {
      installCommand = `npm i -g ${this.packageName}`;
    } else if (hasYarn()()) {
      installCommand = `yarn add ${this.packageName}`;
    } else {
      installCommand = `npm i ${this.packageName}`;
    }

    // 创建终端的提示信息
    const defaultTemplate =
      "Update available " +
      chalk().dim("{currentVersion}") +
      chalk().reset(" → ") +
      chalk().green("{latestVersion}") +
      " \nRun " +
      chalk().cyan("{updateCommand}") +
      " to update";

    const template = options.message || defaultTemplate;

    options.boxenOptions = options.boxenOptions || {
      padding: 1,
      margin: 1,
      align: "center",
      borderColor: "yellow",
      borderStyle: "round",
    };

    const message = boxen()(
      pupa()(template, {
        packageName: this.packageName,
        currentVersion: this.update.current,
        latestVersion: this.update.latest,
        updateCommand: installCommand,
      }),
      options.boxenOptions
    );

    if (options.defer === false) {
      console.error(message);
    } else {
      process.on("exit", () => {
        console.error(message);
      });

      process.on("SIGINT", () => {
        console.error("");
        process.exit();
      });
    }

    return this;
  }
}

总结

  1. 看完这个发现这一个小功能依赖的是真多,而我们也要善于通过第三方的各种小功能进行组合达到自己的需求。

006.[launch-editor]-在 Vue Devtools 中打开编辑器文件

源码

Vue Devtools

1

先去看看 vue-devtools 的打开按钮事件做了什么。

https://github.com/vuejs/devtools/blob/main/packages/app-frontend/src/features/components/SelectedComponentPane.vue#L107

<VueButton
  v-if="fileIsPath"
  v-tooltip="{ content: $t('ComponentInspector.openInEditor.tooltip', { file: data.file }), html: true }"
  icon-left="launch"
  class="flat icon-button"
  @click="openFile()"
/>

https://github.com/vuejs/devtools/blob/main/packages/shared-utils/src/util.ts#L655

export function openInEditor(file) {
  const fileName = file.replace(/\\/g, "\\\\");
  const src = `fetch('${
    SharedData.openInEditorHost
  }__open-in-editor?file=${encodeURI(file)}').then(response => {
    if (response.ok) {
      console.log('File ${fileName} opened in editor')
    } else {
      const msg = 'Opening component ${fileName} failed'
      const target = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : {}
      if (target.__VUE_DEVTOOLS_TOAST__) {
        target.__VUE_DEVTOOLS_TOAST__(msg, 'error')
      } else {
        console.log('%c' + msg, 'color:red')
      }
      console.log('Check the setup of your project, see https://devtools.vuejs.org/guide/open-in-editor.html')
    }
  })`;
  if (isChrome) {
    target.chrome.devtools.inspectedWindow.eval(src);
  } else {
    eval(src);
  }
}

当点击按钮的时候向我们的开发服务器发起了一个请求,http://localhost:8080/__open-in-editor?file=src/App.vue,那下面看看服务是怎么处理的。

Vue Cli

找到 vue-cli 中的 vue-cli-project/node_modules/@vue/cli-service/lib/commands/serve.js 文件,通过代码看是在 webpack 服务中添加了一个中间件实现的。

const server = new WebpackDevServer(compiler, {
  // https://v4.webpack.docschina.org/configuration/dev-server/#devserver-before
  // 在服务内部的所有其他中间件之前, 提供执行自定义中间件的功能。 这可以用来配置自定义处理程序
  before(app, server) {
    // launch editor support.
    // this works with vue-devtools & @vue/cli-overlay
    app.use(
      "/__open-in-editor",
      launchEditorMiddleware(() =>
        console.log(
          `To specify an editor, specify the EDITOR env variable or ` +
            `add "editor" field to your Vue project config.\n`
        )
      )
    );
    // allow other plugins to register middlewares, e.g. PWA
    api.service.devServerConfigFns.forEach((fn) => fn(app, server));
    // apply in project middlewares
    projectDevServerOptions.before &&
      projectDevServerOptions.before(app, server);
  },
});

其实如果自己去配置就是这么简单就实现了,主要内部实现是在 launch-editor 这个包里。再看看 vite 里也是用的这个包,一行代码就搞定了,后面主要看这个包的实现。

// https://github.com/vitejs/vite/blob/main/packages/vite/src/node/server/index.ts#L498
middlewares.use("/__open-in-editor", launchEditorMiddleware());

Launch Editor Middleware

先看这个中间件,接收到请求后会执行中间件做一些准备工作。比如获取到当前的项目路径和解析请求参数。

const url = require("url");
const path = require("path");
const launch = require("launch-editor");

module.exports = (specifiedEditor, srcRoot, onErrorCallback) => {
  if (typeof specifiedEditor === "function") {
    onErrorCallback = specifiedEditor;
    specifiedEditor = undefined;
  }

  if (typeof srcRoot === "function") {
    onErrorCallback = srcRoot;
    srcRoot = undefined;
  }

  srcRoot = srcRoot || process.cwd();

  return function launchEditorMiddleware(req, res, next) {
    // 解析 http://localhost:8080/__open-in-editor?file=src/App.vue 的参数
    const { file } = url.parse(req.url, true).query || {};
    if (!file) {
      res.statusCode = 500;
      res.end(
        `launch-editor-middleware: required query param "file" is missing.`
      );
    } else {
      // 我们拿到路径去打开文件
      launch(path.resolve(srcRoot, file), specifiedEditor, onErrorCallback);
      res.end();
    }
  };
};

Launch Editor

检测编辑器

function launchEditor() {
  // ...
  // 猜测是哪个编辑在运行
  const [editor, ...args] = guessEditor(specifiedEditor);
  // ...
}

在不同的系统平台使用对应的命令查找正在运行的应用程序并返回对应的执行命令,如 Vs Code => code 命令。

// 应用程序的枚举
const COMMON_EDITORS_OSX = ['/Applications/Visual Studio Code.app/Contents/MacOS/Electron': 'code', /* ... */]
// `ps x` and `Get-Process` 找到所有在运行的应用程序,在列举的应用程序做查找,找到则返回。
module.exports = function guessEditor(specifiedEditor) {
  // We can find out which editor is currently running by:
  // `ps x` on macOS and Linux
  // `Get-Process` on Windows
  try {
    if (process.platform === "darwin") {
      const output = childProcess.execSync("ps x").toString();
      const processNames = Object.keys(COMMON_EDITORS_OSX);
      for (let i = 0; i < processNames.length; i++) {
        const processName = processNames[i];
        if (output.indexOf(processName) !== -1) {
          return [COMMON_EDITORS_OSX[processName]];
        }
      }
    } else if (process.platform === "win32") {
      const output = childProcess
        .execSync('powershell -Command "Get-Process | Select-Object Path"', {
          stdio: ["pipe", "pipe", "ignore"],
        })
        .toString();
      // ...
    } else if (process.platform === "linux") {
      const output = childProcess
        .execSync("ps x --no-heading -o comm --sort=comm")
        .toString();
      // ...
    }
  } catch (error) {
    // Ignore...
  }

  // 如果没有匹配到,则还可以指定环境变量
  if (process.env.VISUAL) {
    return [process.env.VISUAL];
  } else if (process.env.EDITOR) {
    return [process.env.EDITOR];
  }

  return [null];
};

打开编辑器

假设 editor 匹配到 codeargs['/user/path/src/App.vue']

使用 node 的子进程在终端执行命令 childProcess.spawn('code', ['/user/path/src/App.vue'], { stdio: 'inherit' }) 相当于执行了 code /user/path/src/App.vue 命令。

function launchEditor() {
  // ....
  // 处理执行参数
  if (lineNumber) {
    const extraArgs = getArgumentsForPosition(
      editor,
      fileName,
      lineNumber,
      columnNumber
    );
    args.push.apply(args, extraArgs);
  } else {
    args.push(fileName);
  }

  // ....
  // 用对用的编辑器命令打开编辑器
  if (process.platform === "win32") {
    // On Windows, launch the editor in a shell because spawn can only
    // launch .exe files.
    _childProcess = childProcess.spawn("cmd.exe", ["/C", editor].concat(args), {
      stdio: "inherit",
    });
  } else {
    // 比如 editor = code args = /user/path/src/App.vue
    // code /user/path/src/App.vue
    _childProcess = childProcess.spawn(editor, args, { stdio: "inherit" });
  }
}

002.[vue-dev-server]-从"玩具 Vite"去理解 Vite 原理

前言

项目地址 https://github.com/vuejs/vue-dev-server

示例

运行命令

npm i @vue/dev-server
npx vue-dev-server

如何工作

  • 浏览器请求导入作为原生 ES 模块导入 - 没有捆绑。

  • 服务器拦截对 *.vue 文件的请求,即时编译它们,然后将它们作为 JavaScript 发回。

  • 对于提供在浏览器中工作的 ES 模块构建的库,只需直接从 CDN 导入它们。

  • 导入到 .js 文件中的 npm 包(仅包名称)会即时重写以指向本地安装的文件。 目前,仅支持 vue 作为特例。 其他包可能需要进行转换才能作为本地浏览器目标 ES 模块公开。

环境

配置 launch.json 或直接在 package.json 调试脚本。

源码

搭建静态服务

开启一个本地服务用的 expressapp.use(middleware) 是添加中间件,每个服务都会经过此中间件。

// bin/vue-dev-server.js
const express = require("express");
const { vueMiddleware } = require("../middleware");

const app = express();
const root = process.cwd();

// 自定义中间件
app.use(vueMiddleware());

// 目录作为静态资源
app.use(express.static(root));

app.listen(3000, () => {
  console.log("server running at http://localhost:3000");
});

入口请求页面

启动服务之后看 test/index.html 的内容当做入口去解析。这也是现在 vite 一直采用的方式使用 html 做为入口。

<!-- test/index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Vue Dev Server</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module">
      // 发起入口请求
      import "./main.js";
    </script>
  </body>
</html>

处理 *.js

在中间件里对每个请求处理,下面简写代码先不考虑缓存,后面再看怎么实现缓存。

从入口开启判断 js 文件,做的事情就是把 js 文件解析成 ast 并处理 import 语句。

if (req.path.endsWith(".js")) {
  // 当前 js 结尾,这里指 main.js 入口
  // 读取文件内容并转换 import 语句,最后在加入缓存
  const result = await readSource(req);
  out = transformModuleImports(result.source);
  send(res, out, "application/javascript");
}

转成 ast 解析文件中的 import 语句,这里用的 recast,用什么无所谓只要能解析。如果要学习 ast 还是推荐从 babel 入手毕竟资料多点。

只处理了 npm 包的路径,因为在浏览器中 import vue from 'vue' 并不知道是一个包。通过 validate-npm-package-name 判断是不是 npm 包,加一个特殊的路径标识标记用于后续的判断。

// ./transformModuleImports.js
function transformModuleImports(code) {
  const ast = recast.parse(code);
  recast.types.visit(ast, {
    // 遍历所有的 Import 声明语句
    visitImportDeclaration(path) {
      const source = path.node.source.value;
      // 处理 npm 包的路径, vue -> /__modules/vue
      // 因为实际代理的没有  node_modules 文件夹的
      if (!/^\.\/?/.test(source) && isPkg(source)) {
        path.node.source = recast.types.builders.literal(
          `/__modules/${source}`
        );
      }
      this.traverse(path);
    }
  });
  // 最后再把 ast 转成成 代码字符串 返回
  return recast.print(ast).code;
}

处理 __modules

请求完 main.js 之后,首先第一个 import Vue from 'vue',经过上面的转换已经变成了 import Vue from "/__modules/vue" 内容了。

if (req.path.startsWith("/__modules/")) {
  // 当是 __modules 开头的时候,证明是 npm 包前面已经处理过了,通过 loadPkg 从 node_modules 读取,在返回文件
  const pkg = req.path.replace(/^\/__modules\//, "");
  out = (await loadPkg(pkg)).toString();
  send(res, out, "application/javascript");
}

处理 *.vue

接着 vue 文件,使用 vuecompiler 模块去编译 sfcrender 函数后返回。

if (req.path.endsWith(".vue")) {
  // 把单文件组件编译成 render 函数
  const result = await bundleSFC(req);
  // 让浏览器用 JavaScript 引擎解析。
  // 小知识:浏览器不通过后缀名判断文件类型
  send(res, result.code, "application/javascript");
}

如果文件里再发起请求,那么还是如上述所处理的一样。

LRU 缓存

最后再说一下里面的缓存,缓存是一种常用的优化手段,但是也不能无限的缓存,特别是大内容那内存岂不是要爆炸。所以有种方案是 LRU(Least Recently Used),简单来说就是就是把最不常用的从缓存中删除掉的**。此项目中用的 lru-cache 可以看官方文档。下面用代码简单实现一个缓存。

如果了解 vuekeep-alive 组件,就知道 keep-alive 能缓存组件和设置最大的缓存个数,就是利用 LRU **实现的。

// 缓存的 key 集合
const keys = new Set();
// 最大缓存个数
const max = 5;

// 添加缓存
function add(key) {
  if (keys.has(key)) {
    // 如果缓存中存在: 把这个 key 从集合中删除再添加,保持 key 的活跃度。
    // 旧:[1, 2, 3]
    // add(1)
    // 新:[2, 3, 1]
    keys.delete(key);
    keys.add(key);
  } else {
    // 如果缓存中存在:则添加一个缓存
    keys.add(key);
    // 如果缓存个数大于最大的缓存数,则删除最久不用的 key。
    // 最久是 key 集合中的第一个,因为每次命中缓存都会从新添加到后面。
    if (keys.size > max) {
      keys.delete(keys.values().next().value);
    }
  }
  console.log([...keys]);
}

add(1); // [1]
add(2); // [1, 2]
add(3); // [1, 2, 3]
add(1); // [2, 3, 1]

add(4); // [2, 3, 1, 4]
add(5); // [2, 3, 1, 4, 5]
add(6); // [3, 1, 4, 5, 6] 最大缓存 5,最久不使用 2 的删除了。

总结

  1. 首先又扩展知识储备 recast(AST 解析)validate-npm-package-name(检测包名)lru-cache(LRU 缓存) 的用法和用处。

  2. 了解 Vite 的核心实现原理。

007.[configstore]-配置存储

前言

用于存储应用程序的配置到本地文件。https://github.com/yeoman/configstore

源码

依赖

import path from "path";
import os from "os";
// fs 模块的扩展
import fs from "graceful-fs";
// linux 的基础目录
import { xdgConfig } from "xdg-basedir";
// fs.writeFile 的扩展
import writeFileAtomic from "write-file-atomic";
// 从嵌套对象中获取、设置或删除属性
import dotProp from "dot-prop";
// 生成唯一的随机字符串
import uniqueString from "unique-string";

全局变量

这里有一个知识点是 Linux 中的权限

// 获取配置目录,如果是 linux 则是 xdgCongfig, 反之获取临时文件的默认目录路径
const configDirectory = xdgConfig || path.join(os.tmpdir(), uniqueString());
// 权限错误提示语
const permissionError = "You don't have access to this file.";
// 文件权限配置,0700、0600是 linux 表示权限的方式 https://blog.csdn.net/mlz_2/article/details/105250259
// 0600:拥有者具有文件的读、写权限,其他用户没有
// 0700:拥有者具有文件的读、写、执行权限,其他用户没有
const mkdirOptions = { mode: 0o0700, recursive: true };
const writeFileOptions = { mode: 0o0600 };

存储逻辑

主要实现就是和正常的操作对象差不多,主要就是使用了 getter 在读取属性时从本地文件读取到内容。使用了 setter 在设置属性时写入值到本地文件。

export default class Configstore {
  constructor(id, defaults, options = {}) {
    // 获取到自定义的存储路径
    const pathPrefix = options.globalConfigPath
      ? path.join(id, "config.json")
      : path.join("configstore", `${id}.json`);

    // 获取最终的存储路径,系统路径 + 自定义路径
    // /Users/lianhaiwei/.config/configstore/configstore-debug.json
    this._path = options.configPath || path.join(configDirectory, pathPrefix);

    // 如果传入默认值则初始化
    if (defaults) {
      this.all = {
        ...defaults,
        ...this.all,
      };
    }
  }

  // 读取文件里的所有值
  get all() {
    try {
      return JSON.parse(fs.readFileSync(this._path, "utf8"));
    } catch (error) {
      // ....
    }
  }

  // 设置所有值到文件
  set all(value) {
    try {
      // 文件是否存在,不存在创建
      fs.mkdirSync(path.dirname(this._path), mkdirOptions);
      // 写入文件并格式化了字符串
      writeFileAtomic.sync(
        this._path,
        JSON.stringify(value, undefined, "\t"),
        writeFileOptions
      );
    } catch (error) {
      // ...
    }
  }

  // 获取一个值
  get(key) {
    return dotProp.get(this.all, key);
  }

  // 设置一个值
  set(key, value) {
    const config = this.all;

    if (arguments.length === 1) {
      for (const k of Object.keys(key)) {
        dotProp.set(config, k, key[k]);
      }
    } else {
      dotProp.set(config, key, value);
    }

    // 从新触发 set all 保存最新的值
    this.all = config;
  }
}

总结

  1. 知道了 fs 模块中的 mode 配置代表的具体的含义,与 linux 中的权限。

  2. 在查找依赖的作用的时候,也有作者推荐了更好的依赖库。比如更现代的 confenv-paths获取全平台存储配置路径。

011.[only-allow]-强制统一规范包管理器

前言

应用场景是强制使用统一的包管理器,统一使用 npm or yarn or pnpmhttps://github.com/pnpm/only-allow

源码

利用 npmpreinstall 钩子在安装包之前执行一段检测脚本。

依赖

// 当前运行的包管理工具 npm/yarn/pnpm
const whichPMRuns = require("which-pm-runs");
// 在总端创建一个框
const boxen = require("boxen");

运行环境

先来想一个问题,它是怎么知道我们是使用什么来运行的。

先来看 which-pm-runs 的实现,它是通过 process.env.npm_config_user_agent 来判断的,npm_config_user_agent 是包管理工具实现的一个环境变量字段,类似于浏览器的 navigator.userAgent

通过 npm_config_user_agent 能得到 yarn/1.22.17 npm/? node/v14.17.0 darwin x64,通过解析这个字符串就可以知道通过什么来运行的。

module.exports = function () {
  if (!process.env.npm_config_user_agent) {
    return undefined;
  }
  return pmFromUserAgent(process.env.npm_config_user_agent);
};

// yarn/1.22.17 npm/? node/v14.17.0 darwin x64
function pmFromUserAgent(userAgent) {
  const pmSpec = userAgent.split(" ")[0];
  const separatorPos = pmSpec.lastIndexOf("/");
  // 返回名称和版本
  return {
    name: pmSpec.substr(0, separatorPos),
    version: pmSpec.substr(separatorPos + 1),
  };
}

判断参数

所以我们只需要获取到 process.argv 的参数,和 which-pm-runs 返回的 name 对比是否一致,入股不一致提示出信息。

// 获取命令行参数和判断解析出 npm/yarn/pnpm
const argv = process.argv.slice(2);
if (argv.length === 0) {
  console.log(
    "Please specify the wanted package manager: only-allow <npm|pnpm|yarn>"
  );
  process.exit(1);
}
const wantedPM = argv[0];
if (wantedPM !== "npm" && wantedPM !== "pnpm" && wantedPM !== "yarn") {
  console.log(
    `"${wantedPM}" is not a valid package manager. Available package managers are: npm, pnpm, or yarn.`
  );
  process.exit(1);
}

// 如果现在使用工具和指定的不一致则输出提示
const usedPM = whichPMRuns();
if (usedPM && usedPM.name !== wantedPM) {
  const boxenOpts = { borderColor: "red", borderStyle: "double", padding: 1 };
  switch (wantedPM) {
    case "npm":
      console.log(
        boxen('Use "npm install" for installation in this project', boxenOpts)
      );
      break;
    case "pnpm":
      console.log(
        boxen(
          `Use "pnpm install" for installation in this project.

If you don't have pnpm, install it via "npm i -g pnpm".
For more details, go to https://pnpm.js.org/`,
          boxenOpts
        )
      );
      break;
    case "yarn":
      console.log(
        boxen(
          `Use "yarn" for installation in this project.

If you don't have Yarn, install it via "npm i -g yarn".
For more details, go to https://yarnpkg.com/`,
          boxenOpts
        )
      );
      break;
  }
  process.exit(1);
}

总结

  1. 学到了 npm_config_user_agent 环境变量。

  2. 强制性的规范更有助于避免错误。

001.[create-vue]-Vue 团队公开的全新脚手架工具

前言

Vue 新公开的脚手架工具 https://github.com/vuejs/create-vue

示例

只需在终端执行 npm init vue@next 按照提示即可创建一个项目。

环境

调试环境

因为涉及到终端交互,所以要用总端运行命令。这里采用调试自动附加功能。这里单独记录一次,这个已经单独提取出来以后引用这个地址。

自动附加调试方法

  1. VS Code 里按下 cmd + shift + p 打开命令面板。

  2. 搜索 toggle auto attach 并确认。

1

  1. 选择仅带标志(仅在给定 "--inspect" 标志时自动附加)。

2

  1. node --inspect xxx.js 运行文件进入调试模式。

3

调试问题

调试的时候 __dirname 错误问题,解决办法如下。

// https://stackoverflow.com/questions/64383909/dirname-is-not-defined-in-node-14-version
// https://github.com/nodejs/help/issues/2907

import { fileURLToPath } from 'url'
import { dirname } from 'path'

const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
// 作者:若川
// 链接:https://juejin.cn/post/7018344866811740173
// 来源:稀土掘金
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

源码

解析命令参数

开始先解析命令行传递的参数,用到 minimist 库解析用法参考文档。

async function init() {
  const cwd = process.cwd()
  // http://nodejs.cn/api/process.html#processargv
  // https://github.com/substack/minimist
  const argv = minimist(process.argv.slice(2), {
    alias: {
      typescript: ['ts'],
      'with-tests': ['tests', 'cypress'],
      router: ['vue-router']
    },
    boolean: true
  })
}

交互式询问

总端交互通过 prompts 库实现用法参考文档。

// 最后的结果 { key: boolean } 的格式
let result = {}

try {
  result = await prompts(
    [
      // ....
      {
        name: 'needsVuex',
        type: () => (isFeatureFlagsUsed ? null : 'toggle'),
        message: 'Add Vuex for state management?',
        initial: false,
        active: 'Yes',
        inactive: 'No'
      },
      {
        name: 'needsTests',
        type: () => (isFeatureFlagsUsed ? null : 'toggle'),
        message: 'Add Cypress for testing?',
        initial: false,
        active: 'Yes',
        inactive: 'No'
      }
    ],
    {
      onCancel: () => {
        throw new Error(red('✖') + ' Operation cancelled')
      }
    }
  )
} catch (cancelled) {
  process.exit(1)
}

如下选择之后结果保存在 result 下输入结果如下。

1

{
  projectName: 'vue-create-test'
  needsVuex: true
  needsTypeScript: true
  needsTests: false
  needsRouter: true
  needsJsx: true
}

配置 + 模板 = 文件

先看一个基础怎么生成的。

const templateRoot = path.resolve(__dirname, 'template')
const render = function render(templateName) {
  const templateDir = path.resolve(templateRoot, templateName)
  // 1、根据传入的路径 在 template 文件下读取对应文件并写入目录
  // 2、深度合并两个 package.json 的内容
  renderTemplate(templateDir, root)
}

// 基础模板
render('base')

2

其他的生成都是一样的逻辑,就是读入模板中的片段组合起来最终生成完整的。

// Add configs.
if (needsJsx) {
  render('config/jsx')
}
if (needsRouter) {
  render('config/router')
}
if (needsVuex) {
  render('config/vuex')
}
if (needsTests) {
  render('config/cypress')
}
if (needsTypeScript) {
  render('config/typescript')
}

// Render code template.
// Render entry file (main.js/ts).
// ...

友好提示

提示不同的包管理工具命令,但是这个需要使用对应的命令去初始化,比如 npm inityarn initpnpm init

// Instructions:
// Supported package managers: pnpm > yarn > npm
// Note: until <https://github.com/pnpm/pnpm/issues/3505> is resolved,
// it is not possible to tell if the command is called by `pnpm init`.
const packageManager = /pnpm/.test(process.env.npm_execpath)
  ? 'pnpm'
  : /yarn/.test(process.env.npm_execpath)
  ? 'yarn'
  : 'npm'

最后还可以使用 kolorist 包输出一些带颜色的提示语。

console.log(`\nDone. Now run:\n`)
if (root !== cwd) {
  console.log(`  ${bold(green(`cd ${path.relative(cwd, root)}`))}`)
}
console.log(`  ${bold(green(getCommand(packageManager, 'install')))}`)
console.log(`  ${bold(green(getCommand(packageManager, 'dev')))}`)
console.log()

总结

  1. 首先扩展知识储备 minimist(解析命令参数)prompts(终端交互)kolorist(终端颜色) 的用法和用处。

  2. 回顾整个流程从开始的 参数解析 到根据 解析结果 匹配对应的 预设模板 得出 最终文件 的过程,应该有所谓各种生成不过是 数据 + 模板 = 文件 的感慨。

004.[co]-异步编程之 Generator 自动执行

前言

co 库用于 Generator 函数的自动执行。https://github.com/tj/co

源码

手动执行

如果一个 Generator 函数,我们想让它执行完毕,就需要要不断的调用 next 方法。

function* gen() {
  console.log(1);

  yield new Promise((resolve) => {
    setTimeout(() => {
      resolve();
      console.log(2);
    });
  });

  yield new Promise((resolve) => {
    setTimeout(() => {
      resolve();
      console.log(3);
    });
  });

  console.log(4);
}

// 生成器对象
let g = gen();

// 1、执行一步 输出 1
// 2、返回 Promise,成功后执行 then,输出 2
g.next().value.then(() => {
  // 3、再执行一步,返回 Promise,成功后执行 then,输出 3
  g.next().value.then(() => {
    // 4、再执行一步,结束 输出 4
    g.next();
  });
});

Co

那么 co 怎么自动执行的,主要点在 next 函数,会把多种格式 value 转化为 Promise ,给这个 Promise 对象添加 then 方法,当异步操作成功时执行 then 中的 onFullfilled 函数,onFullfilled 函数中又去执行 g.next,从而让 Generator 继续执行,然后再返回一个 Promise,再在成功时执行 g.next,然后再返回直到结束。

function co(gen) {
  var ctx = this;
  var args = slice.call(arguments, 1);

  return new Promise(function (resolve, reject) {
    if (typeof gen === "function") {
      // 生成器对象
      gen = gen.apply(ctx, args);
    }
    if (!gen || typeof gen.next !== "function") return resolve(gen);

    // 执行一次
    onFulfilled();

    function onFulfilled(res) {
      var ret;
      try {
        // 执行下一步,指针指向下一个,传入参数传入上次 yield 表达式的值。
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);
      // return null;
    }

    function next(ret) {
      // 是否结束,结束直接返回
      if (ret.done) return resolve(ret.value);
      // 把各种值包装成 Promise
      var value = toPromise.call(ctx, ret.value);
      // 添加 then 方法,当前 Promise 完成后调用 onFulfilled 再次调用 next
      if (value && isPromise(value)) {
        return value.then(onFulfilled, onRejected);
      }
    }
  });
}
co(function* () {
  console.log(1);

  yield new Promise((resolve) => {
    setTimeout(() => {
      resolve();
      console.log(2);
    });
  });

  yield new Promise((resolve) => {
    setTimeout(() => {
      resolve();
      console.log(3);
    });
  });

  console.log(4);
});

Async Await

async ... await 是现在的终极方案,简单明了。

(async () => {
  console.log(1);

  await new Promise((resolve) => {
    setTimeout(() => {
      resolve();
      console.log(2);
    });
  });

  await new Promise((resolve) => {
    setTimeout(() => {
      resolve();
      console.log(3);
    });
  });

  console.log(4);
})();

Babel

到这里我有点好奇 babel 是怎么转码 async ... await,所以我在官网把上面代码转码了一下,发现实现方式和 co 的方式基本一致。

"use strict";

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }
  // 判断是否结束
  if (info.done) {
    resolve(value);
  } else {
    // 当前 Promise 完成后调用 then 再次调用 next
    Promise.resolve(value).then(_next, _throw);
  }
}

function _asyncToGenerator(fn) {
  return function () {
    var self = this,
      args = arguments;
    return new Promise(function (resolve, reject) {
      // 对象生成器对象
      var gen = fn.apply(self, args);
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
      }
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
      }
      // 执行一次
      _next(undefined);
    });
  };
}

// 由于 babel 只是完整的模拟了 生成器函数 所有我们把 babel 实现的 regeneratorRuntime 函数直接替换原生的 生成器函数 是可以的。
_asyncToGenerator(function* () {
  console.log(1);

  yield new Promise((resolve) => {
    setTimeout(() => {
      resolve();
      console.log(2);
    });
  });

  yield new Promise((resolve) => {
    setTimeout(() => {
      resolve();
      console.log(3);
    });
  });

  console.log(4);
})();

// Generator 的实现,内部实现复杂我们替换成 Generator。
// _asyncToGenerator(
//   /*#__PURE__*/ regeneratorRuntime.mark(function _callee() {
//     return regeneratorRuntime.wrap(function _callee$(_context) {
//       while (1) {
//         switch ((_context.prev = _context.next)) {
//           case 0:
//             console.log(1);
//             _context.next = 3;
//             return new Promise(function (resolve) {
//               setTimeout(function () {
//                 resolve();
//                 console.log(2);
//               });
//             });

//           case 3:
//             _context.next = 5;
//             return new Promise(function (resolve) {
//               setTimeout(function () {
//                 resolve();
//                 console.log(3);
//               });
//             });

//           case 5:
//             console.log(4);

//           case 6:
//           case "end":
//             return _context.stop();
//         }
//       }
//     }, _callee);
//   })
// )();

总结

  1. 从开始就一直用的 async 没怎么关注过 generator,这次也算是了解了。

  2. 从最初 callback,到现在使用 promisegeneratorasync 尽量把异步编程同步写法。个人理解:再到 cobabel 转码的源码理解到我们的同步写法是对以前回调写法进一步的封装。

003.[koa-compose]-洋葱模型串联中间件

前言

先来看下洋葱中间件机制,这种灵活的中间件机制也让 koa 变得非常强大。

1

源码

核心实现

可以看到只有短短的几十行,本质上就是一个嵌套的高阶函数,外层的中间件嵌套着内层的中间件。利用递归的机制一层嵌套一层,调用 next 之前是 递(req),之后是 归(res)

function compose(middleware) {
  return function (context, next) {
    // last called middleware #
    let index = -1;
    // 从下标为 0 开始执行中间件。
    return dispatch(0);

    function dispatch(i) {
      if (i <= index)
        return Promise.reject(new Error("next() called multiple times"));
      index = i;
      // 找出数组中存放的相应的中间件
      let fn = middleware[i];

      // 不存在返回,最后一个中间件调用 next 也不会报错。
      if (i === middleware.length) fn = next;
      if (!fn) return Promise.resolve();

      try {
        return Promise.resolve(
          // 执行当前中间件
          fn(
            // 第一个参数是 ctx。
            context,
            // 第二个参数是 next,代表下一个中间件。
            dispatch.bind(null, i + 1)
          )
        );
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

Koa 示例

简单写个示例看它是如何在 Koa 中应用的。

首先通过 use 收集了所有的中间件,在执行的时候当前中间的 next 参数是下一个中间件,那么执行 next 自然就进入了下一个中间件。

其次把这种调用行为看做递归行为,当我们达到终点的时候(最后一个),发生回溯行为直到最初的调用。

const compose = require("../index");

class App {
  middlewares = [];
  use(fn) {
    this.middlewares.push(fn);
  }
  run() {
    compose(this.middlewares)();
  }
}

const app = new App();

// 收集中间件
app.use(async (ctx, next) => {
  console.log(1);
  await next();
  console.log(6);
});
app.use(async (ctx, next) => {
  console.log(2);
  await next();
  console.log(5);
});
app.use(async (ctx, next) => {
  console.log(3);
  await next();
  console.log(4);
});

// 执行中间件
app.run(); // 1->2->3->4->5->6

VueRouter 示例

通过上面我们会发现调用 next 尤为重要,那么还在那见过 next 参数呢?

如果知道 VueRouter 的使用,那么在导航守卫中有 next 如果定义了未调用是不会进入下一个路由的。

其实在 VueRouter 并不是使用递归去实现的,而是巧妙的利用了 Promise 链。

如下有个简单的实现,把守卫函数都包装成 Promise,并且定义 next 函数,只有调用 next 函数才会执行 resolve(),然后使用 reduce 依次追加上 promise.then 实现串联。

class VueRouter {
  guards = [];
  beforeEach(guard) {
    this.guards.push(guardToPromiseFn(guard));
  }
  run() {
    runGuardQueue(this.guards);
  }
}

const router = new VueRouter();
router.beforeEach((to, from, next) => {
  console.log(1);
  next();
});
router.beforeEach((to, from) => {
  console.log(2);
});

router.run(); // 1 -> 2

// 串行执行守卫
function runGuardQueue(guards) {
  // Promise.resolve().then(() => guard1()).then(() => guard2())
  // guard() 执行后返回的 Promise
  return guards.reduce(
    (promise, guard) => promise.then(() => guard()),
    Promise.resolve()
  );
}

// 把守卫包装成 Promise
function guardToPromiseFn(guard, to, from) {
  return () => {
    return new Promise((resolve, reject) => {
      // 定义 next ,当执行 next 的时候这个 Promise 才会从 pending -> resolve
      const next = () => resolve();
      // 执行守卫函数并把 next 函数传递过去
      guard(to, from, next);
      // 如果守卫函数没有定义 next,默认执行 next
      if (guard.length < 3) next();
    });
  };
}

总结

复习一遍,简单实现两个示例。

010.[promisify]-将基于 Callback 的函数转换 Promise

前言

node 的工具函数有 util.promisify 函数将基于 Callback 的函数转换 Promise 使用。因为 node 中有依赖不方便观看,所有找了一个和它实现基本一致的 es6-promisify 单独的实现。那么来看看怎么实现一个这样的函数吧。

源码

总体概括高阶函数的应用,首先就是包装原始函数,并返回一个 Promise,然后接管原始函数的 callback 参数,当原始函数调用 callback 的时候,就可以根据返回的信息去调用 Promiseresolvereject

示例

一个简单的示例。

import { promisify } from "../lib/promisify.js";

// 原始函数
function load(src, callback) {
  setTimeout(() => {
    callback(null, "name");
  });
}

// 返回一个内部函数
const loadPromise = promisify(load);

// 调用函数并返回一个 Promise
loadPromise("src").then((res) => {
  console.log(res); // name
}).catch((err) => {
  console.log(err);
});

实现

核心实现处理了单参数和多参数的实现。

export function promisify(original) {
  // 判断是否是一个函数
  if (typeof original !== "function") {
    throw new TypeError("Argument to promisify must be a function");
  }

  // 多个参数自定义的参数名称
  const argumentNames = original[customArgumentsToken];

  // 自定义的 Promise 或者 原生 Promise
  const ES6Promise = promisify.Promise || Promise;

  // promisify(load) 返回一个函数,执行这个函数返回一个 Promise。
  return function (...args) {
    return new ES6Promise((resolve, reject) => {
      // 忘 args 里追加 callback,那么在普通函数执行回调的时候,就是直接追加的这个函数
      args.push(function callback(err, ...values) {
        // 根据 node 错误优先的写法,判断是否有错误信息
        if (err) {
          return reject(err);
        }

        // 如果参数剩余参数只有一个或者没有指定参数名称返回第一个
        if (values.length === 1 || !argumentNames) {
          return resolve(values[0]);
        }

        // 如果指定了参数名称,转化为对象返回。
        const o = {};
        values.forEach((value, index) => {
          const name = argumentNames[index];
          if (name) {
            o[name] = value;
          }
        });

        resolve(o);
      });

      // 执行调用函数
      original.apply(this, args);
    });
  };
}

总结

  1. 虽然代码很少,函数的灵活性运用的很巧妙。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.