2-2 脚手架框架搭建

1 Lerna 简介

  • 是一个优化基于 git + npm多package 项目的管理工具

1.1 原生脚手架开发痛点

痛点一:重复操作

  • Package 本地 link
  • Package 依赖安装多 Package 单元测试
  • Package 代码提交
  • Package 代码发布

痛点二:版本一致性

  • 发布时版本一致性
  • 发布后相互依赖版本升级

package 越多,管理复杂度越高

1.2 优势

  1. 大幅减少重复操作
  2. 提升操作的标准化

项目复杂度提升后,就需要对项目进行架构优化。架构优化的主要目标往往都是 以效能为核心

1.3 Lerna 开发脚手架流程(划重点)

1. 脚手架项目初始化

  • 初始化 npm 项目 -- npm init -y
  • cnpm i -D lerna
  • lerna -v
  • lerna init

2. 创建 package

  • lerna create <name>
    • 创建被 lerna 管理的 package

npmjs 上注册组织,才能发包成功

  • lerna add <package> 会为所有的 package 安装依赖并做软链接

  • lerna add <package> packages/... 为指定的 package 安装依赖

  • lerna clean 删除 package 下已安装的依赖

  • lerna bootstrap 重新安装依赖

  • lerna link

    • 相互依赖 的所有包链接在一起

3. 脚手架开发和测试

lerna exec -- <command> 在每个 package 中执行任意命令
  • lerna exec -- rm -rf ./node_modules
  • lerna exec --scope @zmoon-cli-dev/core(package.json下的name) --rm -rf ./node_modules 删除指定 package 下的 node_modules
npm run 在包含 npm 脚本的每个 package 中运行一个 npm 脚本
  • npm run test
  • npm run --scope @zmoon-cli-dev/core(package.json下的name) test

4. 脚手架发布上线

  • lerna version 提升版本号
  • lerna changed
  • lerna diff
  • npm login
  • package.json 添加 publishConfig
"publishConfig": {
  "access": "public"
}
  • lerna publish

1.4 Lerna 源码分析前导

1. 为什么做源码分析?

成长所需

  • 技术深度
  • 为我所用、应用到实际开发
  • 学习借鉴

2. 为什么要分析 Lerna 源码?

  • 2w+ star
  • lerna 是脚手架,有借鉴价值
  • lerna 项目中蕴含大量最佳实践

3. 学习目标

  1. 分析源码结构 + 执行流程
  2. import-local 库 源码深度精读

4. 学习收获

  1. 如何将源码分析的收获写进简历
  2. 学习明星项目的架构设计
  3. 获得脚手架执行流程的一种思路
  4. 脚手架调试本地源码的另一种方法
  5. nodejs 加载 node_modules 模块的流程
  6. 各种文件操作算法和最佳实践

2 Lerna 源码分析

准备

  • 下载源码 + 安装依赖
  • 找到入口文件
  • 能够本地调试

2.1 入口文件

  • package.json
"bin": {
  "lerna": "core/lerna/cli.js"
}

2.2 npm 项目本地依赖引用方法

  • 理解上下文 -- 先折叠关键流程

设计模式:构造者设计方法 -- 持续地对一个对象不断地调用方法

  • 链接本地依赖
"dependencies": {
  "@lerna/global-options": "file:../global-options"
}

publish > index.js -- resolveLocalDependencyLinks() 会将本地链接解析为线上链接

2.3 脚手架框架 yargs

  • lerna create imooc-test
  • cd packages/imooc-test
  • npm i yargs -S
  • 新建 bin/index.js 文件

学习各个命令的功能

1. yargs(arg).argv

  • argv 对象,用来读取命令行参数
#! /use/bin/env node

const yargs = require('yargs/yargs')
const { hideBin } = require('yargs/helpers')
const arg = hideBin(process.argv)
yargs(arg)
  .argv

2. yargs(arg).strict()

  • strict() 提示不可识别命令
yargs(arg)
  .strict()
  .argv

3. yargs(arg).usage()

yargs(arg)
  .usage('Usage: $0 <command> [options]') // $0 可获取脚手架名称
  .strict()
  .argv

4. yargs(arg).demandCommand()

  • demandCommand(num, tip) 允许输入的参数最少数
yargs(arg)
  .demandCommand(1, "A command is required. Pass --help to see all available commands and options.")
  .strict()
  .argv

5. yargs(arg).alias()

  • alias() 别名
yargs(arg)
  .alias("h", "help")
  .alias("v", "version")
  .argv

6. yargs(arg).wrap()

  • wrap(width) 将当前脚手架宽度置为终端的宽度
const cli = yargs(arg)
cli
  .wrap(cli.terminalWidth())
  .argv

7. yargs(arg).epilogue()

  • epilogue() 脚尾字符串
const dedent = require("dedent")
const cli = yargs(arg)
cli
  .epilogue(dedent`
    your footer description

    111`) // dedent表示没有缩进
  .argv

8. yargs(arg).options()

  • options() 可配置多个选项
const cli = yargs(arg)
cli
  .options({
    debug: {
      type: 'boolean',
      describe: 'Bootstrap debug mode',
      alias: 'd'
    }
  })
  .argv

9. yargs(arg).option()

  • option() 配置单个选项
const cli = yargs(arg)
cli
  .option('registry', {
    type: 'string',
    describe: 'Define global registry',
    alias: 'r'
  })
  .argv

10. yargs(arg).group()

  • group() 给选项分组
const cli = yargs(arg)
cli
  .options({
    debug: {
      type: 'boolean',
      describe: 'Bootstrap debug mode',
      alias: 'd'
    }
  })
  .option('registry', {
    type: 'string',
    describe: 'Define global registry',
    alias: 'r'
  })
  .group(['debug'], 'Dev Options:')
  .group(['registry'], 'Extra Options:')
  .argv

11. yargs(arg).command() -- 重要

  • 自定义命令

.command(command, describe, builder, handler)
builder 执行前完成 -- 定义私有 options
handler 调用时执行

const cli = yargs(arg)
cli
  .command('init [name]', 'Do init a project', yargs => {
    yargs.option('name', {
      type: 'string',
      describe: 'Name of a project',
      alias: 'n'
    })
  }, argv => {
    console.log(argv);
  })
  .argv
const cli = yargs(arg)
cli
  .command('init [name]', 'Do init a project', yargs => {
    yargs.option('name', {
      type: 'string',
      describe: 'Name of a project',
      alias: 'n'
    })
  }, argv => {
    console.log(argv);
  })
  .command({
    command: 'list',
    alias: ['ls', 'la', 'll'],
    describe: 'List local packages',
    builder: yargs => {},
    handler: argv => {
      console.log(argv);
    }
  })
  .argv

12. yargs(arg).recommendCommands() -- 重要

  • 自动做命令提示
const cli = yargs(arg)
cli
  .recommendCommands()

13. yargs(arg).fail() -- 重要

  • 全局错误处理
cli
.fail((err, msg) => {
    console.log(err)
    console.log('msg', msg)
  })

14. yargs(arg).parse() -- 重要

  • 解析参数
const cli = yargs()
const argv = process.argv.slice(2)

const context = {
  zmoonVersion: pkg.version,
};

cli
// ...
  .parse(argv, context)

2.4 lerna 脚手架 command 执行流程详解

1. commands > list > command.js

exports.handler = function handler(argv) {
  return require(".")(argv)
};

2. commands > list > index.js

class ListCommand extends Command {
  get requiresGit() {
    return false
  }
  // ...
}

const { Command } = require("@lerna/command")

"@lerna/command": "file:../../core/command"

3. core > command > index.js

class Command {
  constructor() {
    // ...
    let runner = new Promise((resolve, reject) => {
      // run everything inside a Promise chain
      let chain = Promise.resolve();

      // 微任务队列 -- 排队
      chain = chain.then(() => {})
      // 各种脚手架默认配置初始化
      chain = chain.then(() => this.runCommand()) // 核心
    }
  }
  // ...
  runCommand() {
    return Promise.resolve()
      .then(() => this.initialize())
      .then((proceed) => {
        if (proceed !== false) {
          return this.execute();
        }
      });
  }
  initialize() { // 初始化
    throw new ValidationError(this.name, "initialize() needs to be implemented.");
  }

  execute() { // 执行
    throw new ValidationError(this.name, "execute() needs to be implemented.");
  }
}

4. commands > list > index.js

  • 具体实现 initialize execute
class ListCommand extends Command {
  // 初始化
  initialize() {
    let chain = Promise.resolve();
    // 具体业务逻辑...
    return chain;
  }

  // 执行
  execute() {
    if (this.result.text.length) {
      output(this.result.text);
    }
    this.logger.success(
      "found",
      "%d %s",
      this.result.count,
      this.result.count === 1 ? "package" : "packages"
    );
  }
}

3 import-local 执行流程深度分析

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))
  const localNodeModules = path.join(process.cwd(), 'node_modules')
  const filenameInLocalNodeModules = !path.relative(localNodeModules, filename).startsWith('..')
  
  return !filenameInLocalNodeModules && localFile && path.relative(localFile, filename) !== '' && require(localFile)
};

3.1 用途

  • 本地 & 全局node 同时存在一个脚手架命令,优先选用本地 (node_modules) 的脚手架功能

3.2 获取全局路径

1. const globalDir = pkgDir.sync(path.dirname(filename))

  • path.dirname(filename) -- 查找文件的上级目录

const pkgDir = require('pkg-dir')

2. pkg-dir

module.exports.sync = cwd => {
  // 从cwd向上寻找package.json
  const filePath = findUp.sync('package.json', { cwd })
  return filePath && path.dirname(filePath)
};

const findUp = require('find-up')

3. find-up 往上级找

module.exports.sync = (name, options = {}) => {
  // options.cwd 为 . 时,返回当前目录
  let directory = path.resolve(options.cwd || '');
  const { root } = path.parse(directory)
  const paths = [].concat(name)
  while(true) {
    const foundPath = runMatcher({...options, cwd: directory})
    // ...
    if (foundPath) {
      return path.resolve(directory, foundPath)
    }
    // ...
  }
  const runMatcher = locateOptions => {
    if (typeof name !== 'function') {
      // locatePath -- 寻找是否存在这个路径,存在则返回第一个
      return locatePath.sync(paths, locateOptions)
    }
    // ...
  };
};
path.resolve()path.join() 的区别
  • path.resolve('/Users', '/sam', '..') -- (cd, '/sam', 返回上级) -- '/' -- 解析为绝对路径
  • path.join('/Users', '/sam', '..') -- /Users/sam -- 返回上级 -- /User -- 拼接
path.parse() 解析当前路径
  • { root, dir, base, ext, name }
locatePath.sync() 寻找是否存在这个路径,存在则返回第一个

const locatePath = require('locate-path')

4. local-path 寻找是否存在这个路径

module.exports.sync = (paths, options) => {
  // ...
  const statFn = options.allowSymlinks ? fs.statSync : fs.lstatSync; // 判断路径是否存在
  for (const path_ of paths) {
    try {
      const stat = statFn(path.resolve(options.cwd, path_))
      if (matchType(options.type, stat)) {
        return path_
      }
    } catch (_) {
    }
  }
};

3.3 resolve-from 源码解析

彻底搞懂 node_modules 模块加载逻辑

前导

const localFile = resolveCwd.silent() 当前路径下找文件

const resolveCwd = require('resolve-cwd')

resolve-cwd
module.exports.silent = moduleId => resolveFrom.silent()

const resolveFrom = require('resolve-from')

resolve-from

const resolveFrom = (fromDirectory, moduleId, silent) => {
  // ...
  fromDirectory = path.resolve(fromDirectory) // 处理相对路径
  // ...
	const fromFile = path.join(fromDirectory, 'noop.js') // 生成一个文件
	// 关键 -- Module._resolveFilename() 计算绝对路径
	const resolveFileName = () => Module._resolveFilename(moduleId, {
		id: fromFile,
		filename: fromFile,
		// Module._nodeModulePaths() -- 所有 node_modules 的可能路径
		paths: Module._nodeModulePaths(fromDirectory)
	})
  // ...
	return resolveFileName()
};

require() 源码解读

3.4 Module._nodeModulePaths()

生成 node_modules 所有可能路径

流程

// 'node_modules' 字符代码颠倒
const nmChars = [ 115, 101, 108, 117, 100, 111, 109, 95, 101, 100, 111, 110 ]
const nmLen = nmChars.length
Module._nodeModulePaths = function(from) {
  // 将 from 转为绝对路径 /Users/sam/Desktop/arch/lerna/lerna-main
  from = path.resolve(from)
  if (from === '/')
    return ['/node_modules']

  // 注意: 此方法*仅*在路径为绝对路径时有效
  const paths = []
  for (let i = from.length - 1, p = 0, last = from.length; i >= 0; --i) {
    const code = StringPrototypeCharCodeAt(from, i)
    // CHAR_FORWARD_SLASH: 47, /* / */
    if (code === CHAR_FORWARD_SLASH) {
      if (p !== nmLen)
        ArrayPrototypePush(
          paths,
          // -> /Users/sam/Desktop/arch/lerna/lerna-main
          StringPrototypeSlice(from, 0, last) + '/node_modules'
        )
      last = i
      p = 0
    } else if (p !== -1) {
      if (nmChars[p] === code) { ++p }
      else { p = -1 }
    }
  }
  ArrayPrototypePush(paths, '/node_modules')
  return paths
};

tip: 如何判断一个字符串=另一个字符串

3.5 Module._resolveFilename()

解析模块的真实路径
node 模块加载核心方法

流程

Module._resolveFilename = function(request, parent, isMain, options) {
  if (NativeModule.canBeRequiredByUsers(request)) {
    return request
  }

  let paths

  if (typeof options === 'object' && options !== null) {
    // ... 其它逻辑
  } else {
    // 将 paths 和环境变量 node_modules 合并
    paths = Module._resolveLookupPaths(request, parent)
  }
  // ...
  // 在 paths 中解析模块的真实路径
  const filename = Module._findPath(request, paths, isMain, false)
  if (filename) return filename
  // ...
};

1. Module._resolveLookupPaths()paths 和环境变量 node_modules 合并

Module._resolveLookupPaths = function(request, parent) {
  if (NativeModule.canBeRequiredByUsers(request)) {
    debug('looking for %j in []', request)
    return null
  }

  // 判断是否为相对路径
  if (...) {

    // modulePaths -- 环境变量中存储的 node_modules 路径
    let paths = modulePaths;
    if (parent?.paths?.length) {
      paths = ArrayPrototypeConcat(parent.paths, paths)
    }

    debug('looking for %j in %j', request, paths)
    return paths.length > 0 ? paths : null
  }
  // ...
};

2. Module._findPath()paths 中解析模块的真实路径

流程

const trailingSlashRegex = /(?:^|\/)\.?\.$/
Module._findPath = function(request, paths, isMain) {
  const absoluteRequest = path.isAbsolute(request)
  if (absoluteRequest) {
    paths = ['']
  } else if (!paths || paths.length === 0) {
    return false
  }

  // cacheKey.split('\x00') -- 空格
  const cacheKey = request + '\x00' + ArrayPrototypeJoin(paths, '\x00')
  const entry = Module._pathCache[cacheKey] // 缓存
  if (entry) return entry

  let exts
  // 判断是否 / 结尾
  let trailingSlash = request.length > 0 &&
    StringPrototypeCharCodeAt(request, request.length - 1) ===
    CHAR_FORWARD_SLASH //  47, /* / */
  if (!trailingSlash) {
    // /../. .. .
    trailingSlash = RegExpPrototypeExec(trailingSlashRegex, request) !== null
  }

  // 遍历所有 path
  for (let i = 0; i < paths.length; i++) {
    const curPath = paths[i]
    // stat() 1-文件夹 0-文件
    if (curPath && stat(curPath) < 1) continue
	  // 文件夹存在
     
    if (!absoluteRequest) {
      // 生成文件路径
      const exportsResolved = resolveExports(curPath, request)
      if (exportsResolved) return exportsResolved
    }

    const basePath = path.resolve(curPath, request)
    let filename
    const rc = stat(basePath)
    if (!trailingSlash) {
      if (rc === 0) {  // 文件
        if (!isMain) {
          if (preserveSymlinks) {
            filename = path.resolve(basePath)
          } else {
            // 生成真实路径 -- 难点
            filename = toRealPath(basePath)
          }
        } else if (preserveSymlinksMain) {
          filename = path.resolve(basePath)
        } else {
          filename = toRealPath(basePath)
        }
      }
      //  ...
    }
    if (filename) {
      Module._pathCache[cacheKey] = filename
      return filename
    }
  }

  return false
}

3. fs.realpathSync()

流程

function realpathSync(p, options) {
  options = getOptions(options);
  p = toPathIfFileURL(p);
  if (typeof p !== 'string') {
    p += '';
  }
  validatePath(p);
  // 相对路径 转 绝对路径
  p = pathModule.resolve(p);

  const cache = options[realpathCacheKey];
  // 查缓存
  const maybeCachedResult = cache?.get(p);
  if (maybeCachedResult) {
    return maybeCachedResult;
  }

  // 所有软连接的缓存
  const seenLinks = new SafeMap();
  const knownHard = new SafeSet();
  // original 缓存最初的路径
  const original = p;

  // 当前字符在p中的位置
  let pos;
  // 到目前为止的部分路径,包括末尾的斜杠(如果有的话)
  let current;
  // 没有末尾斜杠的部分路径(除非指向根路径)
  let base;
  // 上一轮扫描的部分路径,带有斜杠
  let previous;

  // 找到 p 中的根路径 -- /
  current = base = splitRoot(p);
  pos = current.length;

  // 循环查找 -- 路径中是否存在 /
  while (pos < p.length) {
    // 查找下一个路径分隔符之前的(部分)路径的下一部分
    // '/xxx/yyy'.indexOf('/', 1)
    const result = nextPart(p, pos);
    previous = current;
    if (result === -1) {
      const last = StringPrototypeSlice(p, pos);
      current += last;
      base = previous + last;
      pos = p.length;
    } else {
      // current: /Users/  pos: 1  result: 6
      current += StringPrototypeSlice(p, pos, result + 1);
      // base: /Users  previous: /
      base = previous + StringPrototypeSlice(p, pos, result);
      pos = result + 1;
    }

    // 如果不是软链接则继续; 如果是管道/套接字则中断
    if (knownHard.has(base) || cache?.get(base) === base) {
      if (isFileType(binding.statValues, S_IFIFO) ||
          isFileType(binding.statValues, S_IFSOCK)) {
        break;
      }
      continue;
    }
    // 判断是不是软链接
    let resolvedLink;
    const maybeCachedResolved = cache?.get(base);
    if (maybeCachedResolved) {
      resolvedLink = maybeCachedResolved;
    } else {
      const baseLong = pathModule.toNamespacedPath(base);
      const ctx = { path: base };
      // 文件的状态
      const stats = binding.lstat(baseLong, true, undefined, ctx);
      handleErrorFromBinding(ctx);

      // 根据文件的状态判断是否软链接
      if (!isFileType(stats, S_IFLNK)) {
        knownHard.add(base);
        cache?.set(base, base);
        continue;
      }

      let linkTarget = null;
      let id;
      if (!isWindows) {
        // 设备id
        const dev = BigIntPrototypeToString(stats[0], 32);
        // 文件id
        const ino = BigIntPrototypeToString(stats[7], 32);
        id = `${dev}:${ino}`; // 唯一性
        if (seenLinks.has(id)) { // 缓存软链接
          linkTarget = seenLinks.get(id);
        }
      }
      if (linkTarget === null) {
        const ctx = { path: base };
        binding.stat(baseLong, false, undefined, ctx);
        handleErrorFromBinding(ctx);
        // 拿到相对路径
        linkTarget = binding.readlink(baseLong, undefined, undefined, ctx);
        handleErrorFromBinding(ctx);
      }
      // 拿到真实路径
      resolvedLink = pathModule.resolve(previous, linkTarget);

      cache?.set(base, resolvedLink);
      if (!isWindows) seenLinks.set(id, linkTarget);
    }

    // 解析链接,然后重新开始
    p = pathModule.resolve(resolvedLink, StringPrototypeSlice(p, pos));

    // 跳过根
    current = base = splitRoot(p);
    pos = current.length;
  }

  cache?.set(original, p);
  return encodeRealpathResult(p, options);
}

3.6 正则

console.log(/(?:^|\/)\.?\.$/.test('/User'));
const str = 'a'
console.log(str.match(/./)); // [ 'a', index: 0, input: 'a', groups: undefined ]
const str = '/..'
console.log(str.match(/(\.?)\.$/)); // [ '..', '.', index: 0, input: '..', groups: undefined ]
// ?: 非匹配分组 -- 分组的内容不显示
console.log(str.match(/(?:\.?)\.$/)); // [ '..', index: 0, input: '..', groups: undefined ]
console.log(str.match(/^/)); // [ '', index: 0, input: '..', groups: undefined ]
console.log(str.match(/^|\//)); // [ '', index: 0, input: '..', groups: undefined ]
console.log(str.match(/(?:^|\/)\.?\./)); // [ '', index: 0, input: '..', groups: undefined ]
posted on 2022-09-07 10:34  pleaseAnswer  阅读(40)  评论(0编辑  收藏  举报