lerna源码分析
正在加载今日诗词....
2022-04-04

源码仓库:https://github.com/lerna/lerna

为什么要做源码分析?

  • 自我成长、提升编码能力和技术深度的需要
  • 为我所用、应用到实际开发,实际产生效益
  • 学习借鉴、站在巨人肩膀上,登高望远

当然, 总体目标只有一条:赚回学费、走上“人生巅峰”

为什么要分析 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.js
    • pkg = /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/lerna
    • relativePath = cli.js
    • pkg = /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.resolve
  • path.join
  • path.dirname
  • path.parse
  • path.isAbsolute

node 的 module 模块

  • Module._resolveFilename
  • Module._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

  • ☀️
  • 🌑