随着前端组件、包库等工程体系发展,业务组件和工具库关系越来越复杂,非常容易遇到仓库多,库之间互相依赖。导致维护极其困难,发包过程非常繁琐,极大程度地限制了前端同学的开发效率。
此刻,出现了一种新的项目管理方式—— Monorepo。一个仓库管理多个项目。
MultiRepo 是目前常用的项目管理方式。但有些场景是不适用的,存在问题。
此刻就有了 lerna.js
“Lerna (lerna) is a tool that optimizes the workflow around managing multi-package repositories with git and npm.
Lerna 是一个优化基于 git + npm 的多 package 的项目管理工具。
Vue Cli https://github.com/vuejs/vue-cli
create-react-app https://github.com/babel/babel
mint-ui https://github.com/ElemeFE/mint-ui
......
通过阅读本文,你将会学会下图内容:
Lerna 的几个基本常用指令, 不是本文重点哦。文档在这里(https://lerna.js.org/)。
下图是结构目录等。
// package.json 添加
"workspaces":[
"packages/*"
]
// lerna.json 添加
"useWorkspaces":true,
"npmClient": "yarn",
// 配置好后,所有依赖就会安装在最外层的 node_modules 中,且支持软链接方式
// npm 7.x 之后,同样支持工作区域
学习的过程中少不了查看实现过程和运行流程。接下来我们分析一下 Lerna 中的一些代码,希望从中你能学到许多。
我们先 Github 克隆源码(https://github.com/lerna/lerna)
观察一下目录
脚手架入口文件位于 /core/lerna/cli.js
“core/lerna/cli.js 入口
#!/usr/bin/env node
"use strict";
/* eslint-disable import/no-dynamic-require, global-require */
const importLocal = require("import-local");
// 判断是否处于本地包文件,下文会介绍
if (importLocal(__filename)) {
require("npmlog").info("cli", "using local version of lerna");
} else {
// 进入真实的入口执行代码
require(".")(process.argv.slice(2)); // [node, lerna, 指令]
}
如图一和代码入口的文件仅执行了一条判断语句 ,其目的是为了当项目的局部环境和全局环境都存在 Lerna 时优先使用局部环境下的 Lerna 代码
“core/lerna/index.js 初始化
/** 省略相同代码 */
// 导入 @lerna/cli 文件
const cli = require("@lerna/cli");
// ..... 省略相同指令导入
// 导入 publish 指令文件
const publishCmd = require("@lerna/publish/command");
const pkg = require("./package.json");
module.exports = main;
// 最终导出方法
function main(argv) {
const context = {
lernaVersion: pkg.version,
};
return cli()
// ..... 省略
.command(publishCmd)
.parse(argv, context); // 解析注入指令 & 参数(版本号)
}
来到这个代码中,如图二和代码实际上做了这几件事
“core/cli/index.js 全局指令初始化
const dedent = require("dedent"); // 去除空行
const log = require("npmlog");
const yargs = require("yargs/yargs");
const { globalOptions } = require("@lerna/global-options");
module.exports = lernaCLI;
function lernaCLI(argv, cwd) {
const cli = yargs(argv, cwd);
return globalOptions(cli)
.usage("Usage: $0 <command> [options]")
.demandCommand(1, "A command is required. Pass --help to see all available commands and options.") // 期望命令个数
.recommendCommands() // 推荐命令
.strict() // 严格模式
.fail((msg, err) => {
// ... 省略
})
.alias("h", "help") // 别名
.alias("v", "version")
.wrap(cli.terminalWidth()) // 宽高
.epilogue(dedent`
When a command fails, all logs are written to lerna-debug.log in the current working directory.
For more information, find our manual at https://github.com/lerna/lerna
`); // 结尾
}
查看图三全局指令初始化,我们会发现全局指令接受实例的传入,也支持指令的注册。显然这也导出了改 cli 实例(单一实例)
“commands/ 业务指令的注册
可以看到图 4 中 commands 文件包中有着所有 lerna 指令的注册文件,每个文件夹带着 command.js 和 index.js
在 core/lerna/index.js 导入的都是该目录中的 command.js (同入口逻辑在 handler 中执行了该目录下的 index.js )
command.js 包括 yargs 的 command、aliases、describe、builder (执行前的参数操作)、handler (指令执行逻辑)
以 list 指令举例
const { Command } = require("@lerna/command");
const listable = require("@lerna/listable");
const { output } = require("@lerna/output");
const { getFilteredPackages } = require("@lerna/filter-options");
module.exports = factory;
function factory(argv) {
return new ListCommand(argv);
}
class ListCommand extends Command {
get requiresGit() {
return false;
}
initialize() {
let chain = Promise.resolve();
chain = chain.then(() => getFilteredPackages(this.packageGraph, this.execOpts, this.options));
chain = chain.then((filteredPackages) => {
this.result = listable.format(filteredPackages, this.options);
});
return chain;
}
execute() {
// piping to `wc -l` should not yield 1 when no packages matched
if (this.result.text.length) {
output(this.result.text);
}
this.logger.success(
"found",
"%d %s",
this.result.count,
this.result.count === 1 ? "package" : "packages"
);
}
}
module.exports.ListCommand = ListCommand;
“core/command/index.js 所有指令的 Command Class
const { Project } = require("@lerna/project");
// 省略大部分容错 和 log
class Command {
constructor(_argv) {
const argv = cloneDeep(_argv);
// "FooCommand" => "foo"
this.name = this.constructor.name.replace(/Command$/, "").toLowerCase();
// composed commands are called from other commands, like publish -> version
this.composed = typeof argv.composed === "string" && argv.composed !== this.name;
// launch the command
let runner = new Promise((resolve, reject) => {
// run everything inside a Promise chain
// 异步链
let chain = Promise.resolve();
chain = chain.then(() => {
this.project = new Project(argv.cwd);
});
// 配置、环境初始化等
chain = chain.then(() => this.configureEnvironment());
chain = chain.then(() => this.configureOptions());
chain = chain.then(() => this.configureProperties());
chain = chain.then(() => this.configureLogging());
chain = chain.then(() => this.runValidations());
chain = chain.then(() => this.runPreparations());
// 最终执行逻辑
chain = chain.then(() => this.runCommand());
chain.then(
(result) => {
warnIfHanging();
resolve(result);
},
(err) => {
if (err.pkg) {
// Cleanly log specific package error details
logPackageError(err, this.options.stream);
} else if (err.name !== "ValidationError") {
// npmlog does some funny stuff to the stack by default,
// so pass it directly to avoid duplication.
log.error("", cleanStack(err, this.constructor.name));
}
// ValidationError does not trigger a log dump, nor do external package errors
if (err.name !== "ValidationError" && !err.pkg) {
writeLogFile(this.project.rootPath);
}
warnIfHanging();
// error code is handled by cli.fail()
reject(err);
}
);
});
// ...省略部分代码
}
runCommand() {
return Promise.resolve()
// 命令初始化
.then(() => this.initialize())
.then((proceed) => {
if (proceed !== false) {
// 指令执行
return this.execute();
}
// early exits set their own exitCode (if non-zero)
});
}
// 子类不存在 时 抛出错误
initialize() {
throw new ValidationError(this.name, "initialize() needs to be implemented.");
}
execute() {
throw new ValidationError(this.name, "execute() needs to be implemented.");
}
}
module.exports.Command = Command;
在 Class 中最关心的就是 constructor 的逻辑 ,如图 5 和代码。上面写到,每个子指令类会执行 initialize 和 execute 方法。我们整理一下
转过头我们看下脚手架初始化的第一步的 import-local 到底做了什么?
import-local 用于获取 npm 是否包存在本地(当前工作区域),用于判断全局安装的包如果本地有安装,优先用本地的,在 webpack-cli 中等绝大多数 cli 中都有运用。
const path = require('path');
const resolveCwd = require('resolve-cwd');
const pkgDir = require('pkg-dir');
module.exports = filename => {
// '/Users/nvm/versions/node/v14.17.3/lib/node_modules/lerna' 全局文件夹
const globalDir = pkgDir.sync(path.dirname(filename));
const relativePath = path.relative(globalDir, filename); // 'cli.js'
const pkg = require(path.join(globalDir, 'package.json'));
// '/Users/Desktop/person/lerna-demo/node_modules/lerna/cli.js' // 本地文件
const localFile = resolveCwd.silent(path.join(pkg.name, relativePath));
// '/Users/Desktop/person/lerna-demo/node_modules' // 本地文件的 node_modules
const localNodeModules = path.join(process.cwd(), 'node_modules');
const filenameInLocalNodeModules = !path.relative(localNodeModules, filename).startsWith('..') &&
// On Windows, if `localNodeModules` and `filename` are on different partitions, `path.relative()` returns the value of `filename`, resulting in `filenameInLocalNodeModules` incorrectly becoming `true`.
path.parse(localNodeModules).root === path.parse(filename).root;
// 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 !filenameInLocalNodeModules && localFile && path.relative(localFile, filename) !== '' && require(localFile);
};
通过最后一行,可以分析出,最核心的是解析出指定的 npm 包存在全局和 npm 的文件夹、路径。进而判断是 require() 本地还是全局。
对比和查看问题之前,我们要关注一下 Monorepo 单仓库多项目管理的模式带来的优势。
前端工作中你是否会遇到以下问题?
问题 1:
前端同学小明发现了在小红同学的项目中存在相同的业务逻辑
A: 我选择复制一下代码
B: 我选择封装成 npm 包多项目复用
显然 A 方式就不是解决该问题的一种选项,完全不不符合应用程序的代码设计思想。
大多数同学就会异口同声我选择 B
那么如果这个 npm 包在后续迭代过程中发现,包依赖也要随之升级发布,怎么办?
又或者业务中存在大多数这种场景,每个包没有统一管理,花绝大多数时间在包依赖之间升级发布。以及各自包的迭代。
你可能只是删除了一行代码,你却要每个依赖这个包的 npm 包全部执行一遍流程。
问题 2:
在开发中,避免不了对 npm 包的更新,当你更新过程中少不了统一的打 tag 以及当前更新的包的影响面。是小的改动,还是大版本 api 无法兼容的升级。这些操作可能都会导致开发的项目中依赖未及时更新,tag 标记错误出现问题。
就目前来看,Monorepo 解决的是,多仓库之间的依赖变更升级,批量包管理节省时间成本的事情。
所以在开源社区中使用这种模式的一般存在于依赖拆分包,但是彼此之间独立的项目(npm 和脚手架等等)
但是 Lerna 的多包管理也有不足之处
从图中我们可以看出
pnpm 更注重包的管理(像下载,稳定准确性等),相比之下 Lerna 更注重包的发布流程规范指定。
二者适用的场景略有不同。
如图六和下方代码,很显然 resolve-cwd 和 pkg-dir 是实现 import-local 的主要工具包
“resolve-cwd 中使用 resolve-from 工具包解析路径来源
const path = require('path');
const Module = require('module');
// 省略部分代码
const fromFile = path.join(fromDirectory, 'noop.js');
// '/Users/Desktop/home/person/lerna-demo/noop.js'
const resolveFileName = () => Module._resolveFilename(moduleId, {
id: fromFile,
filename: fromFile,
paths: Module._nodeModulePaths(fromDirectory)
});
部分实现可以参考阮一峰老师的 require() 源码解读(https://www.ruanyifeng.com/blog/2015/05/require.html)
“pkg-dir 中使用 find-up 工具包 向上找全局包文件夹
const locatePath = require('locate-path');
const stop = Symbol('findUp.stop');
module.exports.sync = (name, options = {}) => {
let directory = path.resolve(options.cwd || '');
const {root} = path.parse(directory);
const paths = [].concat(name);
const runMatcher = locateOptions => {
if (typeof name !== 'function') {
return locatePath.sync(paths, locateOptions);
}
const foundPath = name(locateOptions.cwd);
if (typeof foundPath === 'string') {
return locatePath.sync([foundPath], locateOptions);
}
return foundPath;
};
// eslint-disable-next-line no-constant-condition
while (true) {
const foundPath = runMatcher({...options, cwd: directory});
if (foundPath === stop) {
return;
}
if (foundPath) {
return path.resolve(directory, foundPath);
}
if (directory === root) {
return;
}
directory = path.dirname(directory);
}
};
fs.symlink(target, path[, type], callback) Node/symlink (http://nodejs.cn/api/fs.html#fssymlinktarget-path-type-callback)
target <string> | <Buffer> | <URL> // 目标文件
path <string> | <Buffer> | <URL> // 创建软链对应的地址
type <string>
该 API 会创建路径为 path 的链接,该链接指向 target。type 参数仅在 Windows 上可用,在其他平台上则会被忽略。可以被设置为 dir
、 file
或 function
。如果未设置 type 参数,则 Node.js 将会自动检测 target 的类型并使用 file
或 dir
。
如果 target 不存在,则将会使用 'file'。Windows 上的连接点要求目标路径是绝对路径。当使用 'function' 时,target 参数将会自动地标准化为绝对路径。
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/7PssD_qpXee0PSusTRarNA
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。