在浏览器控制台中优雅地安装 npm 包 console.install('lodash')

写过爬虫脚本、做过数据清洗的重度 console 使用者都知道:

浏览器控制台就是世界上最随性的 REPL------

没有 package.json,没有 node_modules,

却想立刻用到 lodash、dayjs。

于是"在控制台里 npm install"成了暗网级需求:

复制本文 200 行代码,
install('lodash') 回车,魔法生效,全局变量 _ 从天而降。

引言

在前端开发过程中,我们经常需要在浏览器控制台中快速测试某个 npm 包,比如我们想快速测试某个版本的 lodash 是否存在原型链污染攻击。传统的方法要么过于繁琐,要么缺乏良好的用户反馈。本文将介绍一种直接在控制台中安装 npm 包的解决方案,它不仅能稳健地完成安装,还能智能地检测并提示新添加的全局变量。

效果

一睹为快,效果先行。

随便进入某个网站,比如 nodejs.org/api/vm.html 打开浏览器控制台,输入:

ts 复制代码
// 快速安装 lodash,立即体验!
await console.install('lodash')

控制台立即"吭哧吭哧"开始工作:

ts 复制代码
📦 install script https://unpkg.com/lodash@4.17.21/lodash.js
📦 'lodash' installing ⏳...
📦 'lodash' installed success ✅ costs ⏱️: 1285.686767578125 ms
📦 Try input: `_` in the console.

试试:

ts 复制代码
_.VERSION
'4.17.21'

🎉 🥳!

核心设计理念

API 简洁全程有详细的安装状态反馈,安装完成后,清晰地告知用户哪些全局变量现在可用

难点:如何得知安装的变量。答:通过安装前后全局变量对比来实现。

关键技术实现

1. 浏览器如何安装脚本

1.1 选择 CDN unpkg.com

通过动态注入 script 标签即可,src 我们选择更稳定的 unpkg.com

动态注入使用 document.body.appendChild 在某些页面有坑,我们先讲如何获取 src

ts 复制代码
async function fetchSrc(name) {
  if (/^https?:\/\//.test(name)) {
    return name;
  }
  
  const endpoint = `https://unpkg.com/${name}`
  log(label, 'install script', endpoint);

  return endpoint;
}

我们支持通过完整的 CDN 链接按照 install('https://...'),但是大部分时候都是直接通过包名安装,故我们需要做一个映射。

为什么能直接拼接 https://unpkg.com/${包名}

其实 script 会自动为我们做 302 到原址 unpkg.com/lodash@4.17...

1.2 注入 script

ts 复制代码
const npmInstallScript = document.createElement('script');

npmInstallScript.src = src;

npmInstallScript.onload = (resp) => {
  ...
};

npmInstallScript.onerror = (error) => {
  ...
};

beforeInsert() // 获取会讲到作用

document.body.appendChild(npmInstallScript)

这里一个坑document.body.appendChild 其他网站好好的,但是在 bing.com 无法安装包,原因是这个方法被覆写了,设置了同域白名单导致无法插入来自 unpkg 的script

控制台输入 document.body.appendChild 点击后你会发现 appendChild 被覆写了:

ts 复制代码
Element.prototype.appendChild = function(n) {
  return t(n, "AC") ? u.apply(this, arguments) : null
}

怎么办?将 script 内联嵌入是一种办法,但更简单的是换一个没被"污染"的方法。

diff 复制代码
+ const append = getSafeAppend()

beforeInsert()
- document.body.appendChild(npmInstallScript)
+ append(npmInstallScript);

getSafeAppend 即找一个纯净的插入方法。

js 复制代码
/**
 * 更健壮的 `document.body.appendChild`
 * @param {HTMLScriptElement} script
 * @returns {(element: HTMLElement) => void}
 */
function getSafeAppend() {
  // bing.com 会拦截非本 hostname 的 script 标签的插入,我们需要找到一个原生的插入方法
  const candidates = ['appendChild', 'append', 'prepend', 'before', 'after'];

  let nativeOperation = candidates.find((op) =>
    isNativeCode(document.body[op])
  );

  if (!nativeOperation) {
    nativeOperation = candidates[0];
  }

  const insert = document.body[nativeOperation].bind(document.body);

  return insert;
}

/** func passed in should not be bound otherwise the result is always true */
const isNativeCode = (func) => {
  return func?.toString().includes('[native code]');
};

!Important

注意: insert 方法需要 bind 否则报错 TypeError: Illegal invocation。但在判断 isNativeCode 之前不能 bind 否则一律会被认为是 native code

释义: 既然 appendChild 被覆写了,浏览器还有很多原生插入方法呢,逐个试看看哪一个是原生方法即 includes('[native code]'),直到找到为止,如果没找回退到第一个。

其实除了上述五个,还有 insertBefore insertAdjacentElement insertAdjacentHTML innerHTML 等,但是这些函数需要接受更多参数,还需要特殊操作,故没有纳入,当然如果为了健壮性,还可以继续使用几个兜底,当然逻辑就要增加一些了。

接下来这一部分是健壮性:

ts 复制代码
try {
  if (!document.querySelector(`#${id}`)) {
    throw new Error('Failed to insert script');
  }
} finally {
  npmInstallScript.remove();
}

释义: 如果发现插入后找不到则报错,说明没有插入成功,或许是没有找"纯净的"插入方法。最后清理元素。

至此包安装成功了,我们需要提醒用户如何使用,这也是我们的重点加难点,业界其他同类方法没有的。

2. 探测安装了哪些变量

下面我们将实现『全局变量变化检测机制 』------巧妙利用 iframe 的纯净环境建立基准,准确识别出用户代码添加的全局变量。下面代码值得反复阅读:

javascript 复制代码
// https://mmazzarolo.com/blog/2022-02-14-find-what-javascript-variables-are-leaking-into-the-global-scope/
function getVariablesLeakingIntoGlobalScope() {
  // 创建临时 iframe 获取浏览器默认的全局变量基准
  const iframe = window.document.createElement('iframe');
  iframe.src = 'about:blank';
  window.document.body.appendChild(iframe);
  const browserGlobals = Object.keys(iframe.contentWindow);
  window.document.body.removeChild(iframe);

  // 过滤出运行时添加的全局变量(排除浏览器原生变量)
  return Object.keys(window).filter(key => !browserGlobals.includes(key));
}

释义: 函数返回类型是字符串数组,内容是除了浏览器本身的全局变量之外任何代码导致的全局变量,通常用来检测意外泄露的全局变量,只不过我们这里用来探测安装包。

稍微重构下 :使用 Set 优化过滤逻辑以及增加排序。

ts 复制代码
function getVariablesLeakingIntoGlobalScope() {
  const iframe = window.document.createElement('iframe');

  iframe.src = 'about:blank';

  window.document.body.appendChild(iframe);
  
  // 获取纯净的全局变量
  const builtinGlobals = new Set(Object.keys(iframe.contentWindow));

  window.document.body.removeChild(iframe);

  // 获取当前 window 的全局变量
  // 二者的差集即为"泄露的全局变量"
  const runtimeGlobals = new Set(Object.keys(window)).difference(builtinGlobals);

  return [...runtimeGlobals].sort();
}

很容易懂大家看注释即可。

我们可以在掘金首页执行一下,返回 45 个全局变量:

ts 复制代码
[
  "$nuxt",
  "ComponentRuntime",
  "LogPluginObject",
  "SMS",
  "SlardarWeb",
  "TEAVisualEditor",
  "TTGCaptcha",
  "Vue",
  "VueCompositionAPI",
  "_NOW_",
  "_SdkGlueInit",
  "_SdkGlueLoadingMap",
  "__NUXT__",
  "__SLARDAR_REGISTRY__",
  "__VC_LOG__REPORT__",
  "__bytreplay_bridge__",
  "_vc_intercepted_fetch",
  "_vc_intercepted_pathList",
  "_xssProject",
  "autoRender",
  "bdms",
  "clearImmediate",
  "closeCaptcha",
  "getCaptchaWebId",
  "getFilterXss",
  "initVerifyCenter",
  "initVerifyOptions",
  "isHeadless",
  "isSafeDomain",
  "isSafeProtocol",
  "isSafeUrl",
  "onNuxtReady",
  "onNuxtReadyCbs",
  "onwheelx",
  "regeneratorRuntime",
  "renderCaptcha",
  "renderSecondVerifyWeb",
  "setImmediate",
  "slardarPluginImageReport",
  "verifyCenter",
  "verifyCenterTea",
  "verifySDK",
  "webpackJsonp",
  "xss",
  "xssNamespace"
]

__NUXT__$nuxt 告诉我们掘金是基于 Nuxt 框架的 SSR 应用。

接下来我们通过追踪安装前后全局变量的变化来得知此次安装了哪些变量,从而提醒用户可以输入什么来 play,重中之重

1. 记录 安装前 的全局变量

javascript 复制代码
let globalsBefore;
const beforeInsert = () => {
  globalsBefore = new Set(getVariablesLeakingIntoGlobalScope());
};

!Important

注意: beforeInsert 应该在 script 元素被插入前一刻触发,如果过早触发会有什么问题?可能会导致加入其他脚本引入的全局变量,因为不只是我们一个脚本会导致全局变量的增加,从而最后提示用户的变量有误。

2. 记录 安装后 的全局变量

脚本 <script src> onload 后触发 success,然后依旧利用 Set 的差集方法 difference 过滤出安装后全局变量的增量,即为我们此次安装的包变量。

ts 复制代码
const success = () => {
  const globalsAfter = new Set(getVariablesLeakingIntoGlobalScope());
  const added = [...globalsAfter.difference(globalsBefore)];
  
  // 核心提示功能:告诉用户新增了哪些全局变量
  if (added.length > 0) {
    console.log('📦', 'Package installed successfully! Try input:', 
      added.at(-1), 
      'in the console.'
    );
  }
};

这个提示还是蛮重要的否则安装完毕用户一脸懵我应该输入什么来试用刚刚安装的包?

小结: 我们通过打开一盏"iframe 探照灯":

  1. 开一个 about:blank 的空白 iframe,
  2. 把它的 window 键值表当作"浏览器出厂默认清单",
  3. 安装前后各拍一张"全局变量快照",
  4. 集合做一次 difference,新增的就是"漏网之鱼"。

健壮性保障机制

1. 防御网站对原生方法的篡改

某些网站(如 bing.com)会重写原生 DOM 方法,导致脚本插入失败。我们通过检测真正的原生方法来应对:

javascript 复制代码
const isNativeCode = (func) => {
  return func?.toString().includes('[native code]');
};

const candidates = ['appendChild', 'append', ...];
const nativeOperation = candidates.find((op) => 
  isNativeCode(document.body[op])
);

2. 插入后查询验证是否真实插入

ts 复制代码
  try {
    if (!document.querySelector(`#${id}`)) {
      throw new Error('Failed to insert script');
    }
  } finally {
    npmInstallScript.remove();
  }

插入后移除,防止元素越积越多。

3. 灵活的重复安装控制

javascript 复制代码
// 强制重新安装机制
if (name === 'lodash') {
  const _ = window._;
  if (typeof _ === 'function' && 
      typeof _.flowRight === 'function' && 
      typeof _.VERSION === 'string') {
    console.log(`📦 lodash@${_.VERSION} has been installed already.`);
    
    // 只有明确设置 force: true 才会重新安装
    if (!force) {
      console.log('📦 Use install("lodash", { force: true }) to reinstall');
      return true;
    }
  }
}
  • flowRight VERSION 是否为了判断 lodash 是否已经安装过。
  • 安装后提示无需重复安装,但仍然可以通过 force 参数强制覆盖。

优雅的展示

1. 智能的用户反馈

安装过程详细的安装状态反馈,安装完成后,系统会自动检测并提示新增的全局变量:

javascript 复制代码
📦 'lodash' installing ⏳...
📦 install script https://unpkg.com/lodash@4.17.15/lodash.min.js
📦 'lodash' installed success ✅ costs ⏱️: 1250ms
📦 'lodash' installed failed ‼️ costs ⏱️: 1250ms
📦 Try input: `_` in the console.

2. 多种安装方式支持

javascript 复制代码
// 1. 通过包名安装(自动解析版本)
await install('lodash');

// 2. 安装特定版本
await install('lodash@4.17.15');

// 3. 强制重新安装(即使已存在)
await install('lodash', { force: true });

// 4. 直接使用 CDN URL(跳过解析)
await install('https://cdn.jsdelivr.net/npm/lodash@4.17.15/lodash.min.js');

不支持安装 ES 模块,因为即使安装了也没法在控制台使用。

实际应用场景

快速原型验证

javascript 复制代码
// 想测试 dayjs 的使用?
await install('dayjs');
// 控制台提示:Try input `dayjs` in the console.
dayjs().format('YYYY-MM-DD') // 立即开始测试!

// 需要特定版本?
await install('dayjs@1.10.7');

// 强制更新到最新版本?
await install('dayjs', { force: true });

比如我最近琢磨原型链污染,lodash 低版本存在该风险,那我可以在控制台如此验证:

js 复制代码
console.install('lodash@4.17.15')

// Malicious payload
_.set({}, '__proto__.isAdmin', true);

const obj = { hello: 'world' }
console.log(obj.isAdmin); // Outputs: true 说明原型链被污染了

我不需要新建一个文件夹 npm install lodash......。只需随手打开控制台,在几秒钟就可以完成验证且随用随弃

释义: 我们通过 _.set({}, '__proto__. 污染了 Object.prototype. 同理可以通过 _.merge({}, { '__proto__.isAdmin2': true }) 也会导致原型链污染。

验证结果: lodash@4.17.15 输出 true,说明原型链被污染。

试试 lodash@latest

diff 复制代码
- console.install('lodash@4.17.15')
+ console.install('lodash@latest', { force: true })
ts 复制代码
// Malicious payload
_.set({}, '__proto__.isAdmin2', true);

const obj = { hello: 'world' }
console.log(obj.isAdmin2); // Outputs: true 说明原型链被污染了

释义:

  • @latest 或不写版本号即安装最新版本,force 是因为我们直接安装过,需要强制覆盖。
  • 需要改成 isAdmin2 因为 isAdmin 已经被赋值

验证下是否安装了最新版本:

js 复制代码
_.VERSION
'4.17.21'

验证结果: lodash@4.17.15 输出 undefined,说明原型链未被被污染。

多包安装场景

javascript 复制代码
// 同时安装多个工具库
await install('lodash');
await install('axios');
await install('moment');
// 分别提示每个包引入的全局变量

技术亮点

1. 精准的变量检测

通过 iframe 基准对比,准确识别真正由安装包引入的全局变量,避免误报。

2. 防御性编程

  • 🛡️ 检测并绕过被网站篡改的原生方法
  • 🔄 自动回退机制确保脚本插入成功

3. 灵活的安装策略

  • 🔧 强制更新:通过 { force: true } 明确控制
  • 🎯 版本控制:支持精确版本管理

4. 友好的开发者体验

  • 🎯 精确提示新增的全局变量名
  • ⏱️ 安装耗时监控
  • 🛡️ 完善的错误处理
  • 🔄 重复安装智能检测和提示

完整代码

js 复制代码
const { log, warn, error } = console;

// https://mmazzarolo.com/blog/2022-02-14-find-what-javascript-variables-are-leaking-into-the-global-scope/
function getVariablesLeakingIntoGlobalScope() {
  const iframe = window.document.createElement('iframe');

  iframe.src = 'about:blank';

  window.document.body.appendChild(iframe);

  const builtinGlobals = new Set(Object.keys(iframe.contentWindow));

  window.document.body.removeChild(iframe);

  const runtimeGlobals = new Set(Object.keys(window)).difference(
    builtinGlobals
  );

  return [...runtimeGlobals].sort();
}

function npmDownload(
  src,
  { originName, successCallback, errorCallback, beforeInsert }
) {
  const label = '📦';
  log(label, `'${originName}' installing ⏳...`);

  const successTimerLabel = `${label} '${originName}' installed success ✅ costs ⏱️`;
  const failedTimerLabel = `${label} '${originName}' installed failed 😱 costs ⏱️`;

  console.time(successTimerLabel);
  console.time(failedTimerLabel);

  const npmInstallScript = document.createElement('script');

  const id = [
    'tampermonkey-utils-npm-install',
    originName.replaceAll('@', '-').replaceAll('.', '-'),
    Date.now(),
  ].join('-');

  npmInstallScript.setAttribute('id', id);

  npmInstallScript.src = src;

  // npmInstallScript.setAttribute('crossorigin', '');

  npmInstallScript.onload = (resp) => {
    console.timeEnd(successTimerLabel);
    successCallback(resp);
  };

  npmInstallScript.onerror = (error) => {
    console.timeEnd(failedTimerLabel);
    errorCallback(error);
  };

  const append = getSafeAppend();

  beforeInsert();
  append(npmInstallScript);

  try {
    if (!document.querySelector(`#${id}`)) {
      // console.error(new Error('Failed to insert script'))
      throw new Error('Failed to insert script');
    }
  } finally {
    npmInstallScript.remove();
  }
}

/** func passed in should not be bound otherwise the result is always true */
const isNativeCode = (func) => {
  // console.log('func:', func)
  // console.log('func?.toString():', func?.toString())
  return func?.toString().includes('[native code]');
};

/**
 * 更健壮的 `document.body.appendChild`
 * @param {HTMLScriptElement} script
 * @returns {(element: HTMLElement) => void}
 */
function getSafeAppend() {
  // bing.com 会拦截非本 hostname 的 script 标签的插入,我们需要找到一个原生的插入方法
  const candidates = ['appendChild', 'append', 'prepend', 'before', 'after'];

  let nativeOperation = candidates.find((op) =>
    isNativeCode(document.body[op])
  );
  // console.log('nativeOperation:', nativeOperation)

  if (!nativeOperation) {
    warn(
      label,
      'insert method not found in',
      candidates,
      'but the installment is still trying to insert.'
    );

    nativeOperation = candidates[0];
  }

  const insert = document.body[nativeOperation].bind(document.body);

  return insert;
}

async function npmInstallInBrowser(name, { info, beforeInsert }) {
  const label = '📦';

  const originName = name.trim();
  // console.log(originName);

  const { promise, resolve, reject } = Promise.withResolvers();

  const options = {
    originName,
    info,
    successCallback: resolve,
    errorCallback: reject,
    beforeInsert,
  };

  if (/^https?:\/\//.test(originName)) {
    npmDownload(originName, options);
  } else {
    const url = `https://unpkg.com/${originName}`;

    log(label, 'install script', url);

    npmDownload(url, options);
  }

  return promise;
}

/**
 * Install js package in your console.
 * @param {string} name npm package name or github url
 * @param {{ force?: boolean }} info
 * @returns {Promise<boolean>}
 * @example
 * install('lodash')
 * install('lodash@4.17.15')
 *
 */
async function install(name, info = {}) {
  const { force } = info;

  const label = '📦';

  if (name === 'lodash') {
    const _ = window._;

    if (
      typeof _ === 'function' &&
      typeof _.flowRight === 'function' &&
      typeof _.VERSION === 'string'
    ) {
      log(
        label,
        `lodash@${_.VERSION} has been installed already. Enable \`force\` option to reinstall.`
      );

      if (!force) return true;
    }
  }

  if (!name) {
    error(label, 'invalid params: missing package name or url');
    return false;
  }

  // figure out what installed in global scope
  let globalsBefore;
  const beforeInsert = () => {
    globalsBefore = new Set(getVariablesLeakingIntoGlobalScope());
  };

  const success = () => {
    const globalsAfter = new Set(getVariablesLeakingIntoGlobalScope());
    const added = [...globalsAfter.difference(globalsBefore)];
    if (info.force) {
      console.assert(added.length === 0);
    } else {
      added.length !== 1 &&
        warn(label, 'Should be only one global variable installed', added);
    }

    // console.log('added', added)
    added.length &&
      log(label, 'Try input', `\`${added.at(-1)}\``, 'in the console.');
  };

  try {
    await npmInstallInBrowser(name, { info, beforeInsert });
    success();

    return true;
  } catch (err) {
    error(label, err);
    return false;
  }
}

// 绑定到 console 方便使用
console.install = install;

将完整代码复制到浏览器控制台中,然后开始使用!

javascript 复制代码
await console.install('lodash');  

结语

这个解决方案重新定义了在浏览器控制台中安装 npm 包的体验。它不仅仅是一个安装工具,更是一个智能的开发助手。通过精确的全局变量检测、防御性的环境适应机制和灵活的重装控制,它为开发者提供了可靠且信息丰富的安装体验。

核心价值:在复杂多变的浏览器环境中,稳健地完成包安装,并通过清晰的反馈让开发者立即了解安装结果。

现在就在你的浏览器控制台中尝试,感受这种优雅的安装方式吧!

相关推荐
Moment3 小时前
LangChain 1.0 发布:agent 框架正式迈入生产级
前端·javascript·后端
q***97913 小时前
从零到上线:Node.js 项目的完整部署流程(包含 Docker 和 CICD)
docker·容器·node.js
晓得迷路了3 小时前
栗子前端技术周刊第 106 期 - pnpm 10.21、Node.js v25.2.0、Bun v1.3.2...
前端·javascript·html
码上成长3 小时前
<script setup> 实战模式:大型组件怎么拆?
开发语言·javascript·vue.js
花归去3 小时前
【Vue3】 中的 【unref】:详解与使用
前端·javascript·vue.js
小霖家的混江龙4 小时前
巧用辅助线,轻松实现类拼多多的 Tab 吸顶效果
前端·javascript·react.js
艾小码4 小时前
还在为异步组件加载烦恼?这招让你的Vue应用更丝滑!
前端·javascript·vue.js
清沫10 小时前
VSCode debugger 调试指南
前端·javascript·visual studio code
zhenryx12 小时前
React Native 自定义 ScrollView 滚动条:开箱即用的 IndicatorScrollView(附源码示例)
javascript·react native·react.js·typescript