漫谈程序初始化

1975 字
7 分钟
0

前言

在软件工程的开发中有生命周期这个概念,它的作用就是定义各个阶段需要处理的事情跟 tcp/ip 协议分层一个意思,今天重点聊一聊初始化这个阶段。

  • 在日常使用的 webpackvite 等工具会有一个配置收集的过程,这个过程就是初始化;
  • 在使用 reactvue 等框架时也会有 created 等生命周期函数暴露,在此阶段执行一些请求 api 等操作,这也是初始化;
  • 在使用 koanode 框架启动服务之前进行 use 装载也是初始化;
  • 甚至,一段代码在执行之前通常会经历以下三个阶段,也可以概括为初始化
    • 分词/词法分析
    • 解析/语法分析
    • 生成代码

下面,我把任务分为两部分:

  • 可以前置化处理
  • 不能前置化处理

前置初始化

上面举例的一大堆,你会发现很多任务我们可以很自然完成,例如:

  • webpackvite 读取配置,如果让你写大概就是根据 npm script 填写 config 的路径进行解析,然后通过 node 的 fs 模块进行读取
  • koa 在装载插件之前可能还需要自动导入所有符合要求的文件,这里可以通过 glob 模块来查找所有符合规则的文件进行批量导入

上面的任务都可以通过 node 提供的同步 api 进行完成,且他们只会运行一次并不会影响到主体的功能。

但是有一些操作,例如请求网络,异步加载模块后续的操作都需要等待加载完成之后才能执行,这种情况下没有同步的语法供使用,没办法完成前置依赖的处理。

下面就抛砖引玉聊聊这种情况如何处理

非前置化处理

下面的例子都以 db 模块为例,它负责连接数据库之后进行读取数据,可能有一个 connect 的方法和 queue 的查询方法。

js
class Db {
  constructor() {
    this.signIn = false;
  }
  connect() {
    if (this.signIn) {
      return Promise.resolve(true);
    }
    return new Promise((resolve) => {
      setTimeout(() => {
        this.signIn = true;
        resolve(true);
      }, 3000);
    });
  }
  async queue(str) {
    if (!this.signIn) {
      throw new Error(`必须连接数据库才能使用queue!`);
    }
    console.log(str);
  }
}
const db = new Db();

const app = async () => {
  await db.connect();
  await db.queue(`xx`);
};
app();

为了演示,后面代码全部为简化版本,不执行具体操作

我们在程序中调用这个 db 模块,你会发现发现 await db.connect() 这段代码省略不了,我们的程序依赖 connect 这一步。

且因为只是演示没有传递具体的密码和账号,但想象一下每次调用 db 都需要手动传递一次账号和密码也太糟心了。

有什么办法可以简化这个过程呢?可以对 connect 进行一次封装,最后暴露 db 模块出去。

封装 connect

js
class Db {
  constructor() {
    this.signIn = false;
  }
  connect() {
    if (this.signIn) {
      return Promise.resolve(true);
    }
    return new Promise((resolve) => {
      setTimeout(() => {
        this.signIn = true;
        resolve(true);
      }, 3000);
    });
  }
  async queue(str) {
    if (!this.signIn) {
      throw new Error(`必须连接数据库才能使用queue!`);
    }
    console.log(str);
  }
}
const proxyDb = (() => {
  const db = new Db();
  let sign = false;
  return async () => {
    if (!sign) {
      sign = await db.connect();
    }
    return db;
  };
})();
const app = async () => {
  const db = await proxyDb();
  await db.queue('xxx');
};
app();

使用一个代理将 connect 进行缓存起来,确保执行一次,后续使用直接 await 调用。

不过这种方法虽然实现简单,但是体验只能说一般,有没有更加优雅的方法呢?

预先队列

观察上面 db 的操作,可以看到两部分

  • 登录操作
  • 依赖登录操作的后续

我们可以把需要依赖登录的操作进行一个封装,如果没有登录就 push 到队列中,等到登录的时候进行一个整体的执行。

js
class Db {
  constructor() {
    this.signIn = false;
    this.list = [];
  }
  connect() {
    if (this.signIn) {
      return Promise.resolve(true);
    }
    return new Promise((resolve) => {
      setTimeout(() => {
        this.signIn = true;
        this.list.forEach((fn) => fn());
        this.list = [];
        resolve(true);
      }, 3000);
    });
  }
  queue(str) {
    return new Promise((resolve) => {
      const fn = () => {
        console.log(str);
        resolve(undefined);
      };
      if (this.signIn) {
        return fn();
      }
      this.list.push(fn);
    });
  }
}
const db = new Db();
db.connect();
const app = async () => {
  await db.queue('xxx');
};
app();
  • 在直接执行 queue 的时候状态还没有登录,添加到 list 中;
  • connect 执行成功,释放队列的值

预先队列状态分离

上述的要求我们实现了,但是后面如果还有其他的方法,例如 toArrayfindOne 等方法,一个个写重复的步骤太繁琐了。

按照设计模式单一原则,我们尝试进行分离一下

  • 执行 db 相关的操作只执行这部分
  • 对未登录状态的操作进行一个统一拦截

这部分拦截可以基于 ES6 的 proxy,也可以是 class 的 extends,这里采用 extends 的方式来进行。

js
class Db {
  constructor() {
    this.signIn = false;
  }
  connect() {
    if (this.signIn) {
      return Promise.resolve(true);
    }
    return new Promise((resolve) => {
      setTimeout(() => {
        this.signIn = true;
        resolve(true);
      }, 3000);
    });
  }
  async queue(str) {
    console.log(str);
  }
}
class ProxyDb extends Db {
  constructor() {
    super();
    this.list = [];
    ['queue'].forEach((item) => {
      this[item] = (...rest) => {
        return new Promise((resolve) => {
          const fn = () => {
            const result = super[item].apply(this, rest);
            return resolve(result);
          };
          if (!super.signIn) {
            this.list.push(fn);
            return;
          }
          fn();
        });
      };
    });
  }
  async connect() {
    await super.connect();
    this.list.forEach((fn) => fn());
  }
}
const db = new ProxyDb();
db.connect();
db.queue('xxx');

通过继承重写 connect 操作,在 constructor 阶段把需要代理的方法手写到子类中,最后利用 Promise 的特性,等待 connect 完成之后 resolve

顶层 await

除了上述的两种方法,ES6 的最新 顶层 await 也可以帮助实现效果,顶层 await 是为了解决模块异步加载问题,对于本文刚好可以用到。

我们之所以要对 db 模块 进行缓存和队列等一系列操作,就是因为初始化这部分我们没办法完成前置,不能像读取配置文件一样通过同步 api 语法完成。

但是顶层 await 的出现,让其有一种同步的语法完成这部分工作。

js
class Db {
  constructor() {
    this.signIn = false;
  }
  connect() {
    if (this.signIn) {
      return Promise.resolve(true);
    }
    return new Promise((resolve) => {
      setTimeout(() => {
        this.signIn = true;
        resolve(true);
      }, 3000);
    });
  }
  async queue(str) {
    if (!this.signIn) {
      throw new Error(`必须连接数据库才能使用queue!`);
    }
    console.log(str);
  }
}

这是最初我们的 db 模块,使用顶层 await 只需要,直接 import

js
const db = new Db();
await db.connect();
await db.queue('xxx');

最后

抛砖引玉聊了初始化加载可能遇到的情况,受限于聊天的方向,很多异常情况没有给予考虑,例如数据库如果连接操作,需要手动对队列的操作进行 reject 的错误抛出。

最后如果文章有什么错误或者错别字欢迎指出。

参考:

版权声明

本文采用 CC BY-NC-SA 4.0 协议进行许可。转载请保留原文链接及作者。