1 Lerna 简介
- 是一个优化基于
git + npm
的多package
项目的管理工具
1.1 原生脚手架开发痛点
痛点一:重复操作
- 多
Package
本地link
- 多
Package
依赖安装多Package
单元测试 - 多
Package
代码提交 - 多
Package
代码发布
痛点二:版本一致性
- 发布时版本一致性
- 发布后相互依赖版本升级
package
越多,管理复杂度越高
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. 学习目标
- 分析源码结构 + 执行流程
import-local
库 源码深度精读
4. 学习收获
- 如何将源码分析的收获写进简历
- 学习明星项目的架构设计
- 获得脚手架执行流程的一种思路
- 脚手架调试本地源码的另一种方法
nodejs
加载node_modules
模块的流程- 各种文件操作算法和最佳实践
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()
};
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