- 为什么要做源码分析?
- 为什么要分析 Lerna 源码?
- 学习目标
- 学习收获
- 准备源码
- 源码结构
- 入口
- 命令注册
- package
- 源码精读
- import-local 源码精读
- path 用法总结
- node 的 module 模块
- 源码分析收获
- 本地依赖
- javascript 事件循环
- 从 Lerna 中学到的优秀开源库
- VSCode 代码调试技巧
为什么要做源码分析?
- 自我成长、提升编码能力和技术深度的需要
- 为我所用、应用到实际开发,实际产生效益
- 学习借鉴、站在巨人肩膀上,登高望远
当然, 总体目标只有一条:赚回学费、走上“人生巅峰”
为什么要分析 Lerna 源码?
- 2w+ Star 的明星项目
- Lerna 是脚手架,对我们开发脚手架有借鉴价值
- Lerna 项目中蕴含大量的最佳实践,值得深入研究和学习
学习目标
- Lerna 源码结构和执行流程分析
import-local源码深度精读
学习收获
- 如何将源码分析的收获写进简历
- 学习明星项目的架构设计
- 获得脚手架执行流程的一种实现思路
- 脚手架调试本地源码的另外一种方法
- Node.js加载node_modules模块的流程(全网罕见资源)
- 各种文件操作算法和最佳实践
学习建议:需要具备一定的 Node 基础,并做好迎接困难和挑战的准备
准备源码
源码阅读前准备工作:
- 下载源码
- 安装依赖
- IDE 打开
源码阅读准备完成的标准(划重点):
- 找到入口文件
- 能够本地调试
源码结构
入口
入口文件:
"bin": {
"lerna": "core/lerna/cli.js"
}
入口代码:
const importLocal = require("import-local");
if (importLocal(__filename)) {
require("npmlog").info("cli", "using local version of lerna");
} else {
require(".")(process.argv.slice(2));
}
命令注册
function main(argv) {
const context = {
lernaVersion: pkg.version,
};
return cli()
.command(addCmd)
.command(bootstrapCmd)
.command(changedCmd)
.command(cleanCmd)
.command(createCmd)
.command(diffCmd)
.command(execCmd)
.command(importCmd)
.command(infoCmd)
.command(initCmd)
.command(linkCmd)
.command(listCmd)
.command(publishCmd)
.command(runCmd)
.command(versionCmd)
.parse(argv, context);
}
package
关于 package 的配置位于 lerna.json
"packages": [
"commands/*",
"core/*",
"utils/*"
]
源码精读
import-local 源码精读
import-local的用途是如果处于lerna代码根目录下,执行全局lerna命令时,会优先执行当前目录下的lerna代码
调用部分源码:
importLocal(__filename)
import-local 源码:
const path = require('path');
const resolveCwd = require('resolve-cwd');
const pkgDir = require('pkg-dir');
module.exports = filename => {
const globalDir = pkgDir.sync(path.dirname(filename));
const relativePath = path.relative(globalDir, filename);
const pkg = require(path.join(globalDir, 'package.json'));
const localFile = resolveCwd.silent(path.join(pkg.name, relativePath));
// Use `path.relative()` to detect local package installation,
// because __filename's case is inconsistent on Windows
// Can use `===` when targeting Node.js 8
// See https://github.com/nodejs/node/issues/6624
return localFile && path.relative(localFile, filename) !== '' ? require(localFile) : null;
};
处理流程:
- 执行
lerna全局命令,此时相当于执行:
node /Users/sam/.nvm/versions/node/v12.11.1/lib/node_modules/lerna/cli.js
此时
import-local各变量计算结果为:
globalDir=/Users/sam/.nvm/versions/node/v12.11.1/lib/node_modules/lerna/relativePath=cli.jspkg=/Users/sam/.nvm/versions/node/v12.11.1/lib/node_modules/lerna/package.json的值localFile=/Users/sam/Desktop/arch/lerna/lerna-main/core/lerna/cli.js
所以最终会执行:
require(localFile)
此时会开始执行 /Users/sam/Desktop/arch/lerna/lerna-main/core/lerna/cli.js 中的 import-local 逻辑
此时
import-local各变量计算结果为:
globalDir=/Users/sam/Desktop/arch/lerna/lerna-main/core/lernarelativePath=cli.jspkg=/Users/sam/Desktop/arch/lerna/lerna-main/core/lerna/package.json的值localFile=/Users/sam/Desktop/arch/lerna/lerna-main/core/lerna/cli.js
此时会执行:
require(".")(process.argv.slice(2));
至此 import-local 逻辑执行完毕
path 用法总结
path.resolvepath.joinpath.dirnamepath.parsepath.isAbsolute
node 的 module 模块
Module._resolveFilenameModule._nodeModulePaths
执行逻辑图: 
源码分析收获
本地依赖
package.json 中引用本地依赖:
"dependencies": {
"lerna": "file:core/lerna"
}
官方文档:https://docs.npmjs.com/cli/v6/configuring-npm/package-json#local-paths
lerna publish 发布时会将 file: 进行替换
resolveLocalDependencyLinks() {
// resolve relative file: links to their actual version range
const updatesWithLocalLinks = this.updates.filter(node =>
Array.from(node.localDependencies.values()).some(resolved => resolved.type === "directory")
);
return pMap(updatesWithLocalLinks, node => {
for (const [depName, resolved] of node.localDependencies) {
// regardless of where the version comes from, we can't publish "file:../sibling-pkg" specs
const depVersion = this.updatesVersions.get(depName) || this.packageGraph.get(depName).pkg.version;
// it no longer matters if we mutate the shared Package instance
node.pkg.updateLocalDependency(resolved, depVersion, this.savePrefix);
}
// writing changes to disk handled in serializeChanges()
});
}
javascript 事件循环
课程中案例:
console.log('start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
new Promise(() => {
let chain = Promise.resolve();
chain.then(() => console.log('chain1'));
chain.then(() => console.log('chain2'));
chain.then(() => console.log('chain3'));
})
let chain = Promise.resolve();
chain.then(() => console.log('chain4'));
setTimeout(() => {
console.log('setTimeout2');
let chain = Promise.resolve();
chain.then(() => console.log('chain5'));
}, 0);
console.log('end');
强化练习,请试着解答下面这段代码中的执行逻辑:
let chain = Promise.resolve();
chain = chain.then(async () => {
console.log('chain1');
await new Promise(resolve => setTimeout(() => {
console.log('chain1 timeout');
resolve();
}, 1000));
});
chain = chain.then(() => { console.log('chain2') });
chain = chain.then(() => { console.log('chain3') });
Promise.resolve().then(async () => {
console.log('chain4');
await new Promise(resolve => setTimeout(resolve, 1000));
}).then(() => {
console.log('chain5');
})
Promise.resolve().then(async () => {
console.log('chain6');
await new Promise(resolve => setTimeout(resolve, 1000));
})
console.log('do something async');
setInterval(() => {
console.log('ok');
}, 1000);
从 Lerna 中学到的优秀开源库
- import-local
- resolve-cwd
- resolve-from
- module(node内置)
- pkg-dir
- find-up
- locate-path
- path-exists
推荐大家继续学习node require的实现原理,参考:http://www.ruanyifeng.com/blog/2015/05/require.html
VSCode 代码调试技巧
参考:[https://www.yuque.com/docs/share/faa9343a-42c7-4493-b2a7-aafd8e369005?# 《VSCode调试技巧》](https://www.yuque.com/docs/share/faa9343a-42c7-4493-b2a7-aafd8e369005?# 《VSCode调试技巧》)
Copyright © 2022 @filway