首页 > 其他分享 >2-2 脚手架框架搭建

2-2 脚手架框架搭建

时间:2022-09-07 10:44:05浏览次数:95  
标签:paths const 框架 -- lerna path 脚手架 搭建 yargs

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 ]

标签:paths,const,框架,--,lerna,path,脚手架,搭建,yargs
From: https://www.cnblogs.com/pleaseAnswer/p/16664483.html

相关文章

  • Bean复制的几种框架性能比较(Apache BeanUtils、PropertyUtils,Spring BeanUtils,Cglib
    引用于:https://www.cnblogs.com/kaka/archive/2013/03/06/2945514.html     比较的是四种复制的方式,分别为Apache的BeanUtils和PropertyUtils,Spring的BeanUtils......
  • 如何使用 cloudflare 快速搭建个人域名邮箱 All In One
    如何使用cloudflare快速搭建个人域名邮箱AllInOnecustomdomainemailaddresscloudflare电子邮件电子邮件路由为您的域创建自定义电子邮件地址并将传入电子......
  • Python3 环境搭建
    我们将向大家介绍如何在本地搭建Python3开发环境。Python3可应用于多平台包括Windows、Linux和MacOSX。Unix(Solaris,Linux,FreeBSD,AIX,HP/UX,SunOS,IR......
  • MyBatis框架
    MyBatis简介MyBatis是一款优秀的持久层框架,它支持自定义SQL、存储过程以及高级映射。MyBatis免除了几乎所有的JDBC代码以及设置参数和获取结果集的工作。MyBatis......
  • neovim环境搭建
    neovim环境搭建安装新版本neovimsudoaptinstallsoftware-properties-commonsudoaptupdatesudoadd-apt-repositoryppa:neovim-ppa/stablesudoaptinstallneo......
  • C#/.NET/.NET Core优秀项目框架推荐
    思维导航:前言Blog.CoreAspNetCoreWeiXinMPSDKABPFrameworkUtilsiteserver/cmsOSharpVue.NetCoreOpenAuth.Netant-design-blazorNetModularpaymentFurion......
  • Linux环境搭建
    Linux环境搭建安装VNware虚拟机我在腾讯下载中心直接下载点普通下载就可以了https://pc.qq.com/detail/0/detail_21600.html来到安装目录选择一个自己喜欢的目录主要不......
  • QT4.8.6+mingw+qtcreator4.13.3 搭建环境+调试QT源码
    本文测试环境:win7x64由于考虑到跨平台的原因,本安装不基于visualstudio的插件来安装,这样的开发环境和linux更接近.三个文件请准备好:i686-4.8.2-release-posix-dwarf-r......
  • 3.搭建SSM
    1.创建项目在Eclipse中创建项目,右键解决报错即可导入MyEclipse中,防止Myeclipse中总是报错问题2.导包(以后可能会补充)org.springframeworkspring-webmvc3.2.8.REL......
  • django框架-模型层
    正反向进阶操作#正向查询1.查询电话号码问1234的学校名称#res=models.School.objects.filter(schoolinfo_fo__phone='1234').values('name')#print(res)......