赞
踩
关于yarn
yarn
和npm
一样也是JavaScript
包管理工具,同样我们还发现有cnpm
、pnpm
等等包管理工具,包管理工具有一个就够了,为什么又会有这么多轮子出现呢?
Tip:这里对比的npm
是指npm2
版本
yarn
在下载和安装依赖包采用的是多线程的方式,而npm
是单线程的方式执行,速度上就拉开了差距 yarn
会在用户本地缓存已下载过的依赖包,优先会从缓存中读取依赖包,只有本地缓存不存在的情况才会采取远端请求的方式;反观npm
则是全量请求,速度上再次拉开差距 yarn
把所有的依赖躺平至同级,有效的减少了相同依赖包重复下载的情况,加快了下载速度而且也减少了node_modules
的体积;反观npm
则是严格的根据依赖树下载并放置到对应位置,导致相同的包多次下载、node_modules
体积大的问题cnpm
国内镜像速度更快(其他工具也可以修改源地址) cnpm
将所有项目下载的包收拢在自己的缓存文件夹中,通过软链接把依赖包放到对应项目的node_modules
中 yarn
一样有一个统一管理依赖包的目录 pnpm
保留了npm2
版本原有的依赖树结构,但是node_modules
下所有的依赖包都是通过软连接的方式保存一个项目的依赖包需要有指定文件来说明,JavaScript
包管理工具使用package.json
做依赖包说明的入口。
- {
- "dependencies": {
- "lodash": "4.17.20"
- }
- }
以上面的package.json
为例,我们可以直接识别package.json
直接下载对应的包。
- import fetch from 'node-fetch';
-
- function fetchPackage(packageJson) {
- const entries = Object.entries(packageJson.dependencies);
- entries.forEach(async ([key, version]) => {
- const url = `https://registry.yarnpkg.com/${key}/-/${key}-${version}.tgz`,
- const response = await fetch(url);
-
- if (!response.ok) {
- throw new Error(`Couldn't fetch package "${reference}"`);
- }
-
- return await response.buffer();
- });
- }
接下来我们再看看另外一种情况:
- {
- "dependencies": {
- "lodash": "4.17.20",
- "customer-package": "../../customer-package"
- }
- }
"customer-package": "../../customer-package"
在我们的代码中已经不能正常工作了。所以我们需要做代码的改造:
- import fetch from 'node-fetch';
- import fs from 'fs-extra';
-
- function fetchPackage(packageJson) {
- const entries = Object.entries(packageJson.dependencies);
- entries.forEach(async ([key, version]) => {
- // 文件路径解析直接复制文件
- if ([`/`, `./`, `../`].some(prefix => version.startsWith(prefix))) {
- return await fs.readFile(version);
- }
-
- // 非文件路径直接请求远端地址
- // ...old code
- });
- }
目前我们的代码可以正常的下载固定版本的依赖包、文件路径。但是例如:"react": "^15.6.0"
这种情况我们是不支持的,而且我们可以知道这个表达式代表了从15.6.0
版本到15.7.0
内所有的包版本。理论上我们应该安装在这个范围中最新版本的包,所以我们增加一个新的方法:
- import semver from 'semver';
-
- async function getPinnedReference(name, version) {
- // 首先要验证版本号是否符合规范
- if (semver.validRange(version) && !semver.valid(version)) {
- // 获取依赖包所有版本号
- const response = await fetch(`https://registry.yarnpkg.com/${name}`);
- const info = await response.json();
- const versions = Object.keys(info.versions);
- // 匹配符合规范最新的版本号
- const maxSatisfying = semver.maxSatisfying(versions, reference);
-
- if (maxSatisfying === null)
- throw new Error(
- `Couldn't find a version matching "${version}" for package "${name}"`
- );
-
- reference = maxSatisfying;
- }
-
- return { name, reference };
- }
- function fetchPackage(packageJson) {
- const entries = Object.entries(packageJson.dependencies);
-
- entries.forEach(async ([name, version]) => {
- // 文件路径解析直接复制文件
- // ...old code
-
- let realVersion = version;
- // 如果版本号以 ~ 和 ^ 开头则获取最新版本的包
- if (version.startsWith('~') || version.startsWith('^')) {
- const { reference } = getPinnedReference(name, version);
- realVersion = reference;
- }
-
- // 非文件路径直接请求远端地址
- // ...old code
- });
- }
那么这样我们就可以支持用户指定某个包在一个依赖范围内可以安装最新的包。
现实远远没有我们想的那么简单,我们的依赖包还有自己的依赖包,所以我们还需要递归每一层依赖包把所有的依赖包都下载下来。
- // 获取依赖包的dependencies
- async function getPackageDependencies(packageJson) {
- const packageBuffer = await fetchPackage(packageJson);
- // 读取依赖包的package.json
- const packageJson = await readPackageJsonFromArchive(packageBuffer);
- const dependencies = packageJson.dependencies || {};
- return Object.keys(dependencies).map(name => {
- return { name, version: dependencies[name] };
- });
- }
现在我们可以通过用户项目的package.json获取整个依赖树上所有的依赖包。
可以下载依赖包还不够的,我们要把文件都转移到指定的文件目录下,就是我们熟悉的node_modules
里。
- async function linkPackages({ name, reference, dependencies }, cwd) {
- // 获取整个依赖树
- const dependencyTree = await getPackageDependencyTree({
- name,
- reference,
- dependencies,
- });
-
- await Promise.all(
- dependencyTree.map(async dependency => {
- await linkPackages(dependency, `${cwd}/node_modules/${dependency.name}`);
- })
- );
- }
我们虽然可以根据整个依赖树下载全部的依赖包并放到了node_modules
里,但是我们发现依赖包可能会有重复依赖的情况,导致我们实际下载的依赖包非常冗余,所以我们可以把相同依赖包放到一个位置,这样就不需要重复下载。
- function optimizePackageTree({ name, reference, dependencies = [] }) {
- dependencies = dependencies.map(dependency => {
- return optimizePackageTree(dependency);
- });
-
- for (let hardDependency of dependencies) {
- for (let subDependency of hardDependency.dependencies)) {
- // 子级依赖是否和父级依赖存在相同依赖
- let availableDependency = dependencies.find(dependency => {
- return dependency.name === subDependency.name;
- });
-
- if (!availableDependency) {
- // 父级依赖不存在时,把依赖插入到父级依赖
- dependencies.push(subDependency);
- }
-
- if (
- !availableDependency ||
- availableDependency.reference === subDependency.reference
- ) {
- // 从子级依赖中剔除相同的依赖包
- hardDependency.dependencies.splice(
- hardDependency.dependencies.findIndex(dependency => {
- return dependency.name === subDependency.name;
- })
- );
- }
- }
- }
-
- return { name, reference, dependencies };
- }
我们通过逐级递归一层层将依赖从层层依赖展平,减少了重复的依赖包安装。
截止到这一步我们已经实现了简易的yarn了~
看完代码后给我最直观的就是yarn把面向对象的思想发挥的淋漓尽致
这里我们已yarn add lodash
为例,看看一下yarn
都在内部做了哪些事情。yarn
在安装依赖包时会分为主要5个步骤:
.yarnrc
、命令行参数、package.json
信息等)、兼容性(cpu
、nodejs
版本、操作系统等)是否符合package.json
中的约定 package.json
的依赖形成一颗依赖树,并且会解析出整个依赖树上所有包的具体版本信息 我们继续以yarn add lodash
为例
- // 获取yarnrc文件配置
- // process.cwd 当前执行命令项目目录
- // process.argv 用户指定的yarn命令和参数
- const rc = getRcConfigForCwd(process.cwd(), process.argv.slice(2));
-
- /**
- * 生成Rc文件可能存在的所有路经
- * @param {*} name rc源名
- * @param {*} cwd 当前项目路经
- */
- function getRcPaths(name: string, cwd: string): Array<string> {
- // ......other code
-
- if (!isWin) {
- // 非windows环境从/etc/yarn/config开始查找
- pushConfigPath(etc, name, 'config');
- // 非windows环境从/etc/yarnrc开始查找
- pushConfigPath(etc, `${name}rc`);
- }
- // 存在用户目录
- if (home) {
- // yarn默认配置路经
- pushConfigPath(CONFIG_DIRECTORY);
- // 用户目录/.config/${name}/config
- pushConfigPath(home, '.config', name, 'config');
- // 用户目录/.config/${name}/config
- pushConfigPath(home, '.config', name);
- // 用户目录/.${name}/config
- pushConfigPath(home, `.${name}`, 'config');
- // 用户目录/.${name}rc
- pushConfigPath(home, `.${name}rc`);
- }
- // 逐层向父级遍历加入.${name}rc路经
- // Tip: 用户主动写的rc文件优先级最高
- while (true) {
- // 插入 - 当前项目路经/.${name}rc
- unshiftConfigPath(cwd, `.${name}rc`);
- // 获取当前项目的父级路经
- const upperCwd = path.dirname(cwd);
- if (upperCwd === cwd) {
- // we've reached the root
- break;
- } else {
- cwd = upperCwd;
- }
- }
-
- // ......read rc code
- }
- /**
- * -- 索引位置
- */
- const doubleDashIndex = process.argv.findIndex(element => element === '--');
- /**
- * 前两个参数为node地址、yarn文件地址
- */
- const startArgs = process.argv.slice(0, 2);
- /**
- * yarn子命令&参数
- * 如果存在 -- 则取 -- 之前部分
- * 如果不存在 -- 则取全部
- */
- const args = process.argv.slice(2, doubleDashIndex === -1 ? process.argv.length : doubleDashIndex);
- /**
- * yarn子命令透传参数
- */
- const endArgs = doubleDashIndex === -1 ? [] : process.argv.slice(doubleDashIndex);
在初始化的时候,会分别初始化config配置项、reporter日志。
- this.workspaceRootFolder = await this.findWorkspaceRoot(this.cwd);
- // yarn.lock所在目录,优先和workspace同级
- this.lockfileFolder = this.workspaceRootFolder || this.cwd;
-
- /**
- * 查找workspace根目录
- */
- async findWorkspaceRoot(initial: string): Promise<?string> {
- let previous = null;
- let current = path.normalize(initial);
- if (!await fs.exists(current)) {
- // 路经不存在报错
- throw new MessageError(this.reporter.lang('folderMissing', current));
- }
-
- // 循环逐步向父级目录查找访问package.jsonyarn.json是否配置workspace
- // 如果任意层级配置了workspace,则返回该json所在的路经
- do {
- // 取出package.jsonyarn.json
- const manifest = await this.findManifest(current, true);
-
- // 取出workspace配置
- const ws = extractWorkspaces(manifest);
- if (ws && ws.packages) {
- const relativePath = path.relative(current, initial);
- if (relativePath === '' || micromatch([relativePath], ws.packages).length > 0) {
- return current;
- } else {
- return null;
- }
- }
-
- previous = current;
- current = path.dirname(current);
- } while (current !== previous);
-
- return null;
- }
yarn.lock
地址读取yarn.lock
文件。 package.json
中的生命周期执行对应script
脚本- /**
- * 按照package.json的script配置的生命周期顺序执行
- */
- export async function wrapLifecycle(config: Config, flags: Object, factory: () => Promise<void>): Promise<void> {
- // 执行preinstall
- await config.executeLifecycleScript('preinstall');
- // 真正执行安装操作
- await factory();
- // 执行install
- await config.executeLifecycleScript('install');
- // 执行postinstall
- await config.executeLifecycleScript('postinstall');
- if (!config.production) {
- // 非production环境
- if (!config.disablePrepublish) {
- // 执行prepublish
- await config.executeLifecycleScript('prepublish');
- }
- // 执行prepare
- await config.executeLifecycleScript('prepare');
- }
- }
package.json
的dependencies
、devDependencies
、optionalDependencies
内所有依赖包名+版本号
workspace
项目则读取的为项目根目录的package.json
workspace
项目,还需要读取workspace
项目中所有子项目的package.json
的相关依赖config
的fetchRequestFromCwd
方法取出第一层的全部依赖 - // 获取当前项目目录下所有依赖
- pushDeps('dependencies', projectManifestJson, {hint: null, optional: false}, true);
- pushDeps('devDependencies', projectManifestJson, {hint: 'dev', optional: false}, !this.config.production);
- pushDeps('optionalDependencies', projectManifestJson, {hint: 'optional', optional: true}, true);
-
- // 当前为workspace项目
- if (this.config.workspaceRootFolder) {
- // 收集workspace下所有子项目的package.json
- const workspaces = await this.config.resolveWorkspaces(workspacesRoot, workspaceManifestJson);
- for (const workspaceName of Object.keys(workspaces)) {
- // 子项目package.json
- const workspaceManifest = workspaces[workspaceName].manifest;
- // 将子项目放到根项目dependencies依赖中
- workspaceDependencies[workspaceName] = workspaceManifest.version;
- // 收集子项目依赖
- if (this.flags.includeWorkspaceDeps) {
- pushDeps('dependencies', workspaceManifest, {hint: null, optional: false}, true);
- pushDeps('devDependencies', workspaceManifest, {hint: 'dev', optional: false}, !this.config.production);
- pushDeps('optionalDependencies', workspaceManifest, {hint: 'optional', optional: true}, true);
- }
- }
- }
在上一步我们已经收集到了用户项目的所有依赖包+依赖版本,接下来我们开始获取这些依赖包准确的信息(应该下载哪个版本的依赖包)
package resolver
的find
方法通过package request
获取依赖包信息,获取到信息后递归调用find
方法,查找每个依赖包的dependencies
、optionalDependecncies
中依赖包信息。在解析包的同时使用一个fetchingPatterns
的Set<string>
来保存已经解析和正在解析的package
,减少重复请求操作。
fetchingPatterns
中是否存在相同字符串)lockfile
中获取到精确的版本信息, 如果lockfile
中存在对应依赖包的信息
lockfile
中对应版本是否已经过时,如果过时则在lockfile
中删除关于这条依赖包的相关信息lockfile
中不存在该依赖包信息,则向npm
源发起请求获取满足range
的已知最高版本的依赖包信息delayedResolveQueue
中先不处理 delayedResolveQueue
,在已经解析过的包信息中,找到符合可用版本信息的最高版本结束后,我们就确定了依赖树中所有依赖包的具体版本,以及该包地址等详细信息。
package resolver
的find
方法)- /**
- * 查找依赖包版本号
- */
- async find(initialReq: DependencyRequestPattern): Promise<void> {
- // 优先从缓存中读取
- const req = this.resolveToResolution(initialReq);
- if (!req) {
- return;
- }
-
- // 依赖包请求实例
- const request = new PackageRequest(req, this);
- const fetchKey = `${req.registry}:${req.pattern}:${String(req.optional)}`;
- // 判断当前是否请求过相同依赖包
- const initialFetch = !this.fetchingPatterns.has(fetchKey);
- // 是否更新yarn.lock标志
- let fresh = false;
-
- if (initialFetch) {
- // 首次请求,添加缓存
- this.fetchingPatterns.add(fetchKey);
- // 获取依赖包名+版本在lockfile的内容
- const lockfileEntry = this.lockfile.getLocked(req.pattern);
- if (lockfileEntry) {
- // 存在lockfile的内容
- // 取出依赖版本
- // eq: concat-stream@^1.5.0 => { name: 'concat-stream', range: '^1.5.0', hasVersion: true }
- const {range, hasVersion} = normalizePattern(req.pattern);
- if (this.isLockfileEntryOutdated(lockfileEntry.version, range, hasVersion)) {
- // yarn.lock版本落后
- this.reporter.warn(this.reporter.lang('incorrectLockfileEntry', req.pattern));
- // 删除已收集的依赖版本号
- this.removePattern(req.pattern);
- // 删除yarn.lock中对包版本的信息(已经过时无效了)
- this.lockfile.removePattern(req.pattern);
- fresh = true;
- }
- } else {
- fresh = true;
- }
- request.init();
- }
-
- await request.find({fresh, frozen: this.frozen});
- }
- for (const depName in info.dependencies) {
- const depPattern = depName + '@' + info.dependencies[depName];
- deps.push(depPattern);
- promises.push(
- this.resolver.find(......),
- );
- }
- for (const depName in info.optionalDependencies) {
- const depPattern = depName + '@' + info.optionalDependencies[depName];
- deps.push(depPattern);
- promises.push(
- this.resolver.find(.......),
- );
- }
- if (remote.type === 'workspace' && !this.config.production) {
- // workspaces support dev dependencies
- for (const depName in info.devDependencies) {
- const depPattern = depName + '@' + info.devDependencies[depName];
- deps.push(depPattern);
- promises.push(
- this.resolver.find(.....),
- );
- }
- }
主要是对缓存中没有的依赖包进行下载。
Map<string, PackageReference>
。遍历全部依赖包数组,每一个依赖包拼接专属的缓存目录地址dest
:缓存路径 + npm源-包名-版本-integrity + node_modules + 包名
,通过dest
做去重操作。dest
缓存目录是否存在
package reference
的地址多种情况,如:npm源
、github源
、gitlab源
、文件地址
等,所以yarn
会根据reference地址
调用对应的fetcher
获取依赖包 - /**
- * 拼接缓存依赖包路径
- * 缓存路径 + npm源-包名-版本-integrity + node_modules + 包名
- */
- const dest = config.generateModuleCachePath(ref);
-
- export async function fetchOneRemote(
- remote: PackageRemote,
- name: string,
- version: string,
- dest: string,
- config: Config,
- ): Promise<FetchedMetadata> {
- if (remote.type === 'link') {
- const mockPkg: Manifest = {_uid: '', name: '', version: '0.0.0'};
- return Promise.resolve({resolved: null, hash: '', dest, package: mockPkg, cached: false});
- }
-
- const Fetcher = fetchers[remote.type];
- if (!Fetcher) {
- throw new MessageError(config.reporter.lang('unknownFetcherFor', remote.type));
- }
-
- const fetcher = new Fetcher(dest, remote, config);
- // 根据传入的地址判断文件是否存在
- if (await config.isValidModuleDest(dest)) {
- return fetchCache(dest, fetcher, config, remote);
- }
- // 删除对应路径的文件
- await fs.unlink(dest);
-
- try {
- return await fetcher.fetch({
- name,
- version,
- });
- } catch (err) {
- try {
- await fs.unlink(dest);
- } catch (err2) {
- // what do?
- }
- throw err;
- }
- }
经过fetchStep后,我们本地缓存中已经有了所有的依赖包,接下来就是如何将这些依赖包复制到我们项目中的node_modules
下。
peerDependences
,如果找不到匹配的peerDependences
,进行warning
提示 flatTree
中的dest
(要拷贝到的目标目录地址),src
(包的对应cache目录地址)中,执行将copy
任务,将package
从src
拷贝到dest
下根据上图分析,A、B、C是项目第一层依赖
yarn
对于扁平化其实非常简单粗暴,先按照依赖包名的Unicode
做排序,然后根据依赖树逐层扁平化
可以增加网络请求并发量:--network-concurrency <number>
可以设置网络请求超时时长:--network-timeout <milliseconds>
- "@babel/code-frame@^7.0.0-beta.35":
- version "7.0.0-beta.55"
- resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.55.tgz#71f530e7b010af5eb7a7df7752f78921dd57e9ee"
- integrity sha1-cfUw57AQr163p993UveJId1X6e4=
- dependencies:
- "@babel/highlight" "7.0.0-beta.55"
我们随机截取了一段yarn.lock
的代码,如果只修改version
和resolved
字段是不够的,因为yarn
还会根据实际下载的内容生成的integrity
和yarn.lock
文件的integrity
字段做对比,如果不一致就代表本次下载是错误的依赖包。
首先我们要看是如何引用依赖包的。
前置场景:
首先我们根据当前依赖关系和yarn安装特性可以知道实际安装结构为:
- |- A@1.0.0
- |- B@1.0.0
- |--- D@2.0.0
- |----- C@2.0.0
- |- C@1.0.0
- |- D@1.0.0
我们可以通过yarn list
来检查是否存在问题。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。