漫谈程序初始化
前言
在软件工程的开发中有生命周期这个概念,它的作用就是定义各个阶段需要处理的事情跟 tcp/ip
协议分层一个意思,今天重点聊一聊初始化这个阶段。
- 在日常使用的
webpack
、vite
等工具会有一个配置收集的过程,这个过程就是初始化; - 在使用
react
、vue
等框架时也会有created
等生命周期函数暴露,在此阶段执行一些请求 api 等操作,这也是初始化; - 在使用
koa
等node
框架启动服务之前进行use
装载也是初始化; - 甚至,一段代码在执行之前通常会经历以下三个阶段,也可以概括为初始化
- 分词/词法分析
- 解析/语法分析
- 生成代码
下面,我把任务分为两部分:
- 可以前置化处理
- 不能前置化处理
前置初始化
上面举例的一大堆,你会发现很多任务我们可以很自然完成,例如:
webpack
、vite
读取配置,如果让你写大概就是根据npm script
填写 config 的路径进行解析,然后通过 node 的fs
模块进行读取koa
在装载插件之前可能还需要自动导入所有符合要求的文件,这里可以通过glob
模块来查找所有符合规则的文件进行批量导入
上面的任务都可以通过 node 提供的同步 api 进行完成,且他们只会运行一次并不会影响到主体的功能。
但是有一些操作,例如请求网络,异步加载模块后续的操作都需要等待加载完成之后才能执行,这种情况下没有同步的语法供使用,没办法完成前置依赖的处理。
下面就抛砖引玉聊聊这种情况如何处理
非前置化处理
下面的例子都以 db 模块为例,它负责连接数据库之后进行读取数据,可能有一个 connect 的方法和 queue 的查询方法。
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
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 到队列中,等到登录的时候进行一个整体的执行。
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
执行成功,释放队列的值
预先队列状态分离
上述的要求我们实现了,但是后面如果还有其他的方法,例如 toArray
、findOne
等方法,一个个写重复的步骤太繁琐了。
按照设计模式单一原则,我们尝试进行分离一下
- 执行 db 相关的操作只执行这部分
- 对未登录状态的操作进行一个统一拦截
这部分拦截可以基于 ES6 的 proxy
,也可以是 class 的 extends
,这里采用 extends 的方式来进行。
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 的出现,让其有一种同步的语法完成这部分工作。
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
后
const db = new Db();
await db.connect();
await db.queue('xxx');
最后
抛砖引玉聊了初始化加载可能遇到的情况,受限于聊天的方向,很多异常情况没有给予考虑,例如数据库如果连接操作,需要手动对队列的操作进行 reject
的错误抛出。
最后如果文章有什么错误或者错别字欢迎指出。
参考: