依旧是迟到很久的阅读源码系列 非常感谢若川大佬的阅读源码活动~
1.阅读前准备
曾经npm install之后会发现会自动弹出npm依赖包升级的页面
现在就来研究下到底是怎么做到的
2.源码地址
https://github.com/yeoman/update-notifier
3.开始阅读
UpdateNotifier构造函数中,分为三部分函数:check()检查函数;fetchInfo获取信息函数;notify通知函数
整个代码中引用了很多第三方库
先从UpdateNotifier构造函数入手
3.1 构造函数
constructor(options = {}) {// 主要是对传入的options对象中的参数进行校验this.options = options;options.pkg = options.pkg || {};options.distTag = options.distTag || 'latest';// Reduce pkg to the essential keys. with fallback to deprecated options// TODO: Remove deprecated options at some point far into the futureoptions.pkg = {name: options.pkg.name || options.packageName,version: options.pkg.version || options.packageVersion};// 如果缺少必要 则抛出异常if (!options.pkg.name || !options.pkg.version) {throw new Error('pkg.name and pkg.version required');}this.packageName = options.pkg.name;this.packageVersion = options.pkg.version;// 检查传入的时间戳,如果不是时间则采用默认时间戳this.updateCheckInterval = typeof options.updateCheckInterval === 'number' ? options.updateCheckInterval : ONE_DAY;this.disabled = 'NO_UPDATE_NOTIFIER' in process.env ||process.env.NODE_ENV === 'test' ||process.argv.includes('--no-update-notifier') ||isCi();this.shouldNotifyInNpmScript = options.shouldNotifyInNpmScript;if (!this.disabled) {try {const ConfigStore = configstore();this.config = new ConfigStore(`update-notifier-${this.packageName}`, {optOut: false,// Init with the current time so the first check is only// after the set interval, so not to bother users right awaylastUpdateCheck: Date.now()});} catch {// Expecting error code EACCES or EPERMconst message =chalk().yellow(format(' %s update check failed ', options.pkg.name)) +format('\n Try running with %s or get access ', chalk().cyan('sudo')) +'\n to the local update config store via \n' +chalk().cyan(format(' sudo chown -R $USER:$(id -gn $USER) %s ', xdgBasedir().config));process.on('exit', () => {console.error(boxen()(message, {align: 'center'}));});}}}
3.2 check检查函数
check() {// 如果出现以下几种情况时,则直接退出if (!this.config ||this.config.get('optOut') ||this.disabled) {return;}// 获取包更新信息(第一次获取的时候为undefined)this.update = this.config.get('update');if (this.update) {// 如果存在,则赋值最新版本this.update.current = this.packageVersion;// 并清理缓存this.config.delete('update');}// 如果最后一次获取更新的时间小于用户设置的检查时间 则直接退出if (Date.now() - this.config.get('lastUpdateCheck') < this.updateCheckInterval) {return;}// 调用子进程执行check文件// 这里的unref 方法用于断绝与父进程的关系,父进程退出不会造成子进程的退出spawn(process.execPath, [path.join(__dirname, 'check.js'), JSON.stringify(this.options)], {detached: true,stdio: 'ignore'}).unref();}
在进行check函数的执行后,会进入check.js文件的执行,进一步看check.js中又发生了什么
let updateNotifier = require('.');const options = JSON.parse(process.argv[2]);updateNotifier = new updateNotifier.UpdateNotifier(options);(async () => {setTimeout(process.exit, 1000 * 30);// 在这里继续调用updateNotifier中的获取数据的方法const update = await updateNotifier.fetchInfo();// 并更新最后更新检查时间的时间字段updateNotifier.config.set('lastUpdateCheck', Date.now());// 如果此时时间不是最新,则会更新为最新if (update.type && update.type !== 'latest') {updateNotifier.config.set('update', update);}// Call process exit explicitly to terminate the child process,// otherwise the child process will run forever, according to the Node.js docsprocess.exit();})().catch(error => {console.error(error);process.exit(1);});
check.js这里主要为开启子进程,获取最新版本信息的步骤,执行完成后将退出子进程
3.3 fetchInfo()获取信息函数
async fetchInfo() {const {distTag} = this.options;// 这里主要通过懒加载的方式执行获取包信息的步骤const latest = await latestVersion()(this.packageName, {version: distTag});return {latest,current: this.packageVersion,// 这里会做信息的difftype: semverDiff()(this.packageVersion, latest) || distTag,name: this.packageName};}
3.4 notify()通知函数
notify(options) {const suppressForNpm = !this.shouldNotifyInNpmScript && isNpm().isNpmOrYarn;if (!process.stdout.isTTY || suppressForNpm || !this.update || !semver().gt(this.update.latest, this.update.current)) {return this;}options = {// 是否为全局安装isGlobal: isInstalledGlobally(),// 是否为yarn全局安装isYarnGlobal: isYarnGlobal()(),...options};let installCommand;// 根据yarn和npm的判断 展示不同的命令指示符给用户if (options.isYarnGlobal) {installCommand = `yarn global add ${this.packageName}`;} else if (options.isGlobal) {installCommand = `npm i -g ${this.packageName}`;} else if (hasYarn()()) {installCommand = `yarn add ${this.packageName}`;} else {installCommand = `npm i ${this.packageName}`;}const defaultTemplate = 'Update available ' +chalk().dim('{currentVersion}') +chalk().reset(' → ') +chalk().green('{latestVersion}') +' \nRun ' + chalk().cyan('{updateCommand}') + ' to update';const template = options.message || defaultTemplate;options.boxenOptions = options.boxenOptions || {padding: 1,margin: 1,align: 'center',borderColor: 'yellow',borderStyle: 'round'};const message = boxen()(pupa()(template, {packageName: this.packageName,currentVersion: this.update.current,latestVersion: this.update.latest,updateCommand: installCommand}),options.boxenOptions);if (options.defer === false) {console.error(message);} else {process.on('exit', () => {console.error(message);});process.on('SIGINT', () => {console.error('');process.exit();});}return this;}
4.总结
整个过程比较简单,主体就为updateNotifier构造函数,其中执行的步骤为:
- new了一个updateNotifier对象
- 执行check()函数
- 判断是否需要执行check.js
- 执行fetchInfo()函数获取信息
- set(‘lastUpdateCheck’)设置最后获取更新的时间
- set(‘update’)设置一个需要更新的列表
- notify()函数,展示给用户目前可更新的依赖包,并根据包类型展示命令符
