本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
本文是学习 remote-git-tags 笔记, 希望对大家也有帮助.
package.json
下面是本文件的几个重要信息:
{
// 指定 Node 以什么模块加载,缺省时默认是 commonjs
"type": "module",
"exports": "./index.js",
// 指定 nodejs 的版本
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"scripts": {
"test": "xo && ava"
}
}
-
type为module指定node以 ES 模块模式运行代码. -
"exports"(抄自官方api, 感觉翻译的很拗口) 当通过node_modules查找或通过自引用关联其自身的名称导入时, 该字段允许定义包的入口点。 Node.js 12+ 支持它作为"main"的替代方案,它可以支持定义子路径导出和条件导出,同时封装内部未导出的模块。
条件导出也可以在 "exports" 中用于为每个环境定义不同的包入口点,包括包是通过 require 还是通过 import 引用。
"exports" 中定义的所有路径必须是以 ./ 开头的相对文件 URL。
条件导出提供了一种根据特定条件映射到不同路径的方法。 CommonJS 和 ES 模块导入都支持它们。
比如,包想要为 require() 和 import 提供不同的 ES 模块导出可以这样写:
// package.json
{
"main": "./main-require.cjs",
"exports": {
"import": "./main-module.js",
"require": "./main-require.cjs"
},
"type": "module"
}
Node.js 实现了以下条件:
"node-addons"- 类似于"node"并匹配任何 Node.js 环境。 此条件可用于提供使用原生 C++ 插件的入口点,而不是更通用且不依赖原生插件的入口点。 可以通过--no-addons标志禁用此条件。"node"- 匹配任何 Node.js 环境。 可以是 CommonJS 或 ES 模块文件。 在大多数情况下,不需要明确调用 Node.js 平台。"import"- 当包通过import或import(),或者通过 ECMAScript 模块加载器的任何顶层导入或解析操作加载时匹配。 无论目标文件的模块格式如何,都适用。 始终与"require"互斥。"require"- 当包通过require()加载时匹配。 引用的文件应该可以用require()加载,尽管无论目标文件的模块格式如何,条件都匹配。 预期的格式包括 CommonJS、JSON 和原生插件,但不包括 ES 模块,因为require()不支持它。 始终与"import"互斥。"default"- 始终匹配的通用后备。 可以是 CommonJS 或 ES 模块文件。 此条件应始终放在最后。
在 "exports" 对象中,键顺序很重要。 在条件匹配过程中,较早的条目具有更高的优先级并优先于较晚的条目。 一般规则是条件应该按照对象顺序从最具体到最不具体。
使用 "import" 和 "require" 条件会导致一些危害,在双 CommonJS/ES 模块包章节中有进一步的解释。
"node-addons" 条件可用于提供使用原生 C++ 插件的入口点。 但是,可以通过 --no-addons 标志禁用此条件。 当使用 "node-addons" 时,建议将 "default" 视为提供更通用入口点的增强功能,例如使用 WebAssembly 而不是原生插件。
条件导出也可以扩展为导出子路径,例如:
{
"main": "./main.js",
"exports": {
".": "./main.js",
"./feature": {
"node": "./feature-node.js",
"default": "./feature.js"
}
}
}
定义了一个包,其中 require('pkg/feature') 和 import 'pkg/feature' 可以在 Node.js 和其他 JS 环境之间提供不同的实现。
当使用环境分支时,总是尽可能包含 "default" 条件。 提供 "default" 条件可确保任何未知的 JS 环境都能够使用此通用实现,这有助于避免这些 JS 环境必须伪装成特定环境以支持具有条件导出的包。 出于这个原因,使用 "node" 和 "default" 条件分支通常比使用 "node" 和 "browser" 条件分支更可取。
Node 之前一直是 CommonJS 模块机制。 Node 13 添加了对标准 ES6 模块的支持。告诉 Node 它要加载的是什么模块的最简单的方式,就是将信息编码到不同的扩展名中。 如果是 .mjs 结尾的文件,则 Node 始终会将它作为 ES6 模块来加载。 如果是 .cjs 结尾的文件,则 Node 始终会将它作为 CommonJS 模块来加载。
对于以 .js 结尾的文件,默认是 CommonJS 模块。如果同级目录及所有父级目录有 package.json 文件,且 type 属性为module 则使用 ES6 模块。type 值为 commonjs 或者为空或者没有 package.json 文件,都是默认 commonjs 模块加载。
下面还是回到源码学习中来吧, 下载下来代码可以看到源码就那么一二十行:
import {promisify} from 'node:util';
import childProcess from 'node:child_process';
const execFile = promisify(childProcess.execFile);
export default async function remoteGitTags(repoUrl) {
const {stdout} = await execFile('git', ['ls-remote', '--tags', repoUrl]);
const tags = new Map();
for (const line of stdout.trim().split('\n')) {
const [hash, tagReference] = line.split('\t');
// Strip off the indicator of dereferenced tags so we can override the
// previous entry which points at the tag hash and not the commit hash
// `refs/tags/v9.6.0^{}` → `v9.6.0`
const tagName = tagReference.replace(/^refs\/tags\//, '').replace(/\^{}$/, '');
tags.set(tagName, hash);
}
return tags;
}
其实就是用node util 里的promisify把child_process.execFile转化为promise模式,然后用async/await执行shell命令, 然后解析字符串并放到map对象里返回.
代码里的这一行: import util from 'node:util' 引用node原生库的util模块, 避免与util包混淆.
代理里的promisify是怎么将回调模式转为promise模式的? 这个是重要知识点. 其实是利用了Promise对象. 这个队象我们一般都是这么用的:
function(func){
return new Promise((resolve, reject)=>{
func(...参数, function (err, data) {
if(err){
reject(err);
return;
}
resolve(data);
})();
});
};
通过上面的代码可以将func的回调模式转化promise模式返回. node的异步函数的回调函数一般第一个参数一般是err对象, 其他参数才是函数的结果. 那么我们自己使用下较通用的对node异步函数promisify的工具函数是这样的:
function promisify(func){
return (...args)=> new Promise((resolve,reject)=>{
args.push((err,data)=>{
if(err){
reject(err)
return
}
resolve(data)
})
Reflect.apply(func,this,args)
})
}
下面了解下Reflect.apply的用法
Reflect.apply(func, thisArg, args)
Reflect.apply方法等同于Function.prototype.apply.call(func, thisArg, args),用于绑定this对象后执行给定函数。
一般来说,如果要绑定一个函数的this对象,可以这样写fn.apply(obj, args),但是如果函数定义了自己的apply方法,就只能写成Function.prototype.apply.call(fn, obj, args),采用Reflect对象可以简化这种操作。
接着我们用下面这个测试函数来测试它是否可用.
const imageSrc = 'https://www.themealdb.com/images/ingredients/Lime.png';
function loadImage(src, callback) {
const image = document.createElement('img');
image.src = src;
image.alt = '公众号若川视野专用图?';
image.style = 'width: 200px;height: 200px';
image.onload = () => callback(null, image);
image.onerror = () => callback(new Error('加载失败'));
document.body.append(image);
}
const promisifyLoadImage = promisify(loadImage)
promisifyLoadImage(imageSrc)
.then(
function(data){
console.log(content);
}),
function(err){
console.log(err);
})
结果如图:
可以正常运行.修改图片地址为不存在时, 测试结果如图:
异常时也是符合预期. 这里实现太简单, 看看node的源码, 通过注释来学习下:
const kCustomPromisifiedSymbol = SymbolFor('nodejs.util.promisify.custom'); //定义symbol
const kCustomPromisifyArgsSymbol = Symbol('customPromisifyArgs'); //定义symbol
let validateFunction;
function promisify(original) {
// Lazy-load to avoid a circular dependency.
if (validateFunction === undefined)
({ validateFunction } = require('internal/validators'));
validateFunction(original, 'original'); //验证是否是函数
if (original[kCustomPromisifiedSymbol]) { //如果是已经被转化过的
const fn = original[kCustomPromisifiedSymbol];
validateFunction(fn, 'util.promisify.custom');
return ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
value: fn, enumerable: false, writable: false, configurable: true
});
}
// Names to create an object from in case the callback receives multiple
// arguments, e.g. ['bytesRead', 'buffer'] for fs.read.
const argumentNames = original[kCustomPromisifyArgsSymbol];
function fn(...args) {
return new Promise((resolve, reject) => {
// ArrayPrototypePush 类似数组的push
ArrayPrototypePush(args, (err, ...values) => {
if (err) {
return reject(err);
}
// 如果argumentNames已经被用户定义为非undefined并且返回值是数组,
// 就resolve出去按顺序转化的键值对结果
if (argumentNames !== undefined && values.length > 1) {
const obj = {};
for (let i = 0; i < argumentNames.length; i++)
obj[argumentNames[i]] = values[i];
resolve(obj);
} else {
resolve(values[0]);
}
});
ReflectApply(original, this, args);
});
}
// 这是处理原始函数的原型链的移植的
ObjectSetPrototypeOf(fn, ObjectGetPrototypeOf(original));
// 这是标记已经转化过
ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
value: fn, enumerable: false, writable: false, configurable: true
});
// 这是处理原始函数的属性描述符移植的
return ObjectDefineProperties(
fn,
ObjectGetOwnPropertyDescriptors(original)
);
}
promisify.custom = kCustomPromisifiedSymbol;
const argumentNames = original[kCustomPromisifyArgsSymbol]; 这里感觉会始终是undefined, 但是可以从下图,看到这个kCustomPromisifyArgsSymbol symbol最终导出去了, 就是为了用户可以得到特定键值对的结果对象.
总结
这个小组件的原理挺简单: 使用Node.js的child_process模块的execFile方法执行git命令: git ls-remote --tags 仓库地址 来获取仓库的全部tag和对应的hash放到map序列.
通过这个库的学习了解了package.json里export的作用和node.js的promisify的实现.
另外也发现了学习可以深也可以浅, 貌似这个库挺简单, 但是如果分析到关联的node.js内部的实现, 并理清设计原则和目的, 还真的挺难的. 为了能够坚持下去, 要做到适可而止, 不能钻到代码地狱去.