普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月21日技术

Polyfill方式解决前端兼容性问题:core-js包结构与各种配置策略

作者 漂流瓶jz
2026年1月20日 23:57

简介

在之前我介绍过Babel:解锁Babel核心功能:从转义语法到插件开发,Babel是一个使用AST转义JavaScript语法,提高代码在浏览器兼容性的工具。但有些ECMAScript并不是新的语法,而是一些新对象,新方法等等,这些并不能使用AST抽象语法树来转义。因此Babel利用core-js实现这些代码的兼容性。

core-js是一个知名的前端工具库,里面包含了ECMAScript标准中提供的新对象/新方法等,而且是使用旧版本支持的语法来实现这些新的API。这样即使浏览器没有实现标准中的新API,也能通过注入core-js代码来提供对应的功能。

像这种通过注入代码实现浏览器没有提供的API特性,叫做Polyfill。这个单词的本意是填充材料,在JavaScript领域中,这些注入的代码就类似“填充材料”一样,帮助我们提高代码的兼容性。另外core-js还提供了一些还在提议中的API的实现。

core-js使用方式

使用前后对比

要想看到core-js使用前后的效果对比,首先需要确定某个特性和对应的执行环境,在这个环境中对应的特性不存在。我本地是Node.js v18.19.1版本,这个版本并没有实现Promise.try这个方法,因此我们就用这个方法进行实验。首先是没有引入core-js的场景:

Promise.try(() => {
  console.log('jzplp!')
})

/* 执行结果
Promise.try(() => {
           ^
TypeError: Promise.try is not a function
*/

可以看到没有引入core-js,直接使用Promise.try时,会因为没有该方法而报错。然后再试试引入core-js的效果:

require('core-js')
Promise.try(() => {
  console.log('jzplp!')
})

/* 输出结果
jzplp!
*/

可以看到引入core-js后,原本不存在的API被填充了,我们的代码可以正常执行并拿到结果了。这就是core-js提高兼容性的效果。

单个API引入

core-js不仅可以直接引入全部语法,还可以仅引入单个API,比如某个对象或某个方法。首先看下只引入Promise对象:

// require('core-js/full') 等于 require('core-js')
require('core-js/full/promise')
Promise.try(() => {
  console.log('jzplp!')
})

/* 输出结果
jzplp!
*/

然后再看下直接引入对象中的某个方法:

require('core-js/full/promise/try')
Promise.try(() => {
  console.log('jzplp!')
})

/* 输出结果
jzplp!
*/

不注入全局对象

前面展示的场景,core-js都是将API直接注入到全局,这样使用这些API就如环境本身支持一样,基本感受不到区别。但如果我们不希望直接注入到全局时,core-js也提供了使用方式:

const promise = require('core-js-pure/full/promise');
promise.try(() => {
  console.log('jzplp!')
})
Promise.try(() => {
  console.log('jzplp!2')
})
/* 输出结果
jzplp!
Promise.try(() => {
           ^
TypeError: Promise.try is not a function
*/

可以看到,使用core-js-pure这个包之后,可以直接导出我们希望要的API,而不直接注入到全局。此时直接使用全局对象方法依然报错。而core-js这个包虽然也能导出,但它还是会直接注入全局,我们看下例子:

const promise = require('core-js/full/promise');
promise.try(() => {
  console.log('jzplp!')
})
Promise.try(() => {
  console.log('jzplp!2')
})

/* 输出结果
jzplp!
jzplp!2
*/

因此,如果希望仅使用导出对象,还是需要使用core-js-pure这个包。core-js-pure也可以仅导出对象方法:

const try2 = require("core-js-pure/full/promise/try");
Promise.try = try2;
Promise.try(() => {
  console.log("jzplp!");
});

/* 输出结果
jzplp!
*/

因为导出的对象方法不能独立使用,因此在例子中我们还是将其注入到Promise对象后使用。

特性分类引入

core-js中包含非常多API特性的兼容代码,有些是已经稳定的特性,有些是还处在提议阶段的,不稳定的特性。我们直接引入core-js会把这些特性全部引入,但如果不需要那些不稳定特性,core-js也提供了多种引入方式:

  • core-js 引入所有特性,包括早期的提议
  • core-js/full 等于引入core-js
  • core-js/actual 包含稳定的ES和Web标准特性,以及stage3的特性
  • core-js/stable 包含稳定的ES和Web标准特性
  • core-js/es 包含稳定的ES特性

这里我们举两个例子尝试下。首先由于ECMAScript标准一直在更新中,有些特性现在是提议,未来可能就已经被列入正式特性了。因此这里的例子需要明确环境和core-js版本。这里我们使用Node.js v18.19.1和core-js@3.47.0版本,以写这篇文章的时间为准。

首先第一个特性是:数组的lastIndex属性,这是一个stage1阶段的API,这里针对不同的引入方式进行尝试:

// 不引入core-js尝试
const arr = ["jz", "plp"];
console.log(arr.lastIndex);
/* 输出结果
undefined
*/

// 引入core-js/full
require("core-js/full");
const arr = ["jz", "plp"];
console.log(arr.lastIndex);
/* 输出结果
1
*/

// 引入core-js/actual
require("core-js/actual");
const arr = ["jz", "plp"];
console.log(arr.lastIndex);

/* 输出结果
undefined
*/

首先当不引入core-js时,因为不支持这个API,所以输出undefined。core-js/full支持stage1阶段的API,可以正确输出结果。但core-js/actual仅支持stage3阶段的API,因此还是不支持这个API。

然后我们再看下另外一个API,数组的groupBy方法。这是一个stage3阶段的API:

// 不引入core-js尝试
const arr = [
  { group: 1, value: "jz" },
  { group: 2, value: "jz2" },
  { group: 1, value: "plp" },
];
const arrNew = arr.groupBy(item => item.group);
console.log(arrNew)
/* 输出结果
const arrNew = arr.groupBy(item => item.group);
                   ^
TypeError: arr.groupBy is not a function
*/

// 引入core-js/actual
require("core-js/actual");
const arr = [
  { group: 1, value: "jz" },
  { group: 2, value: "jz2" },
  { group: 1, value: "plp" },
];
const arrNew = arr.groupBy(item => item.group);
console.log(arrNew)
/* 输出结果
[Object: null prototype] {
  '1': [ { group: 1, value: 'jz' }, { group: 1, value: 'plp' } ],
  '2': [ { group: 2, value: 'jz2' } ]
}
*/

// 引入core-js/stable
require("core-js/stable");
const arr = [
  { group: 1, value: "jz" },
  { group: 2, value: "jz2" },
  { group: 1, value: "plp" },
];
const arrNew = arr.groupBy(item => item.group);
console.log(arrNew)
/* 输出结果
const arrNew = arr.groupBy(item => item.group);
                   ^
TypeError: arr.groupBy is not a function
*/

可以看到,不引入core-js时不支持,引入了core-js/actual(包含stage3阶段的API)后支持并能输出正确的结果。core-js/stable中不支持又报错了。

core-js源码结构

前面描述了很多core-js的引入方式,这里我们看一下源码结构,看看core-js内部是如何组织的。

core-js源码目录

core-js
├─actual
│   ├─array
│   │  ├─at.js
│   │  ├─concat.js
│   │  └─...
│   ├─set
│   │  └─...
│   └─...
├─es
│   └─...
├─features
│   └─...
├─index.js
└─...

首先列出core-js源码目录的示意图,可以看到core-js内部有很多目录,对应前面的各种引入方式。这里我们列出每个目录的内容:

  • actual 包含稳定的ES和Web标准特性,以及stage3的特性
  • es 包含稳定的ES特性
  • features 没有说明,猜测和full类似
  • full 所以特性包括早期提议
  • internals 包内部使用的逻辑
  • modules 实际特性的代码实现
  • proposals 包含提议的特性
  • stable 包含稳定的ES和Web标准特性
  • stage 按照stage阶段列出提议特性
  • web 包含Web标准特性
  • configurator.js 是否强制引入逻辑,后面会描述
  • index.js 内容为导出full目录,因此导入core-js等于导入core-js/full

层层引用

在目录中actual, es, full, stable, es是我们已经介绍过的。另外还有web目录仅包含web标准的特性,features和full类似(index.js中直接导出full目录)。

proposals目录包含提议的特性,以特性名来命名文件名。而stage目录中包含0.js, 1.js, 2.js等等,是根据stage阶段来整理的,方便整理和引入对应阶段的特性。

这样整理目录虽然清晰,但这些目录中的特性都是重复的,不可能在每个目录中把特性都实现一遍。因此上面这些目录的文件中,存放的都是实现的引用,并不是特性代码实现本身。真正的实现在modules目录中。modules目录中是以特性名作为命名的文件,文件有固定的前缀名:es.表示ES标准;esnext.表示提议中的标准;web.表示web标准。

这里以我们上面提到过的两个特性为例,看看引用路径,首先是Promise.try:

  • 使用者引入 core-js/full/promise/try.js
  • 引入 actual/promise/try.js
  • 引入 actual/promise/try.js
  • 引入 stable/promise/try.js
  • 引入 es/promise/try.js
  • 最终引入 modules/es.promise.try.js

然后是groupBy方法:

  • 使用者引入 core-js/actual/array/group-by.js
  • 最终引入 modules/esnext.array.group-by.js

可以看到,core-js内部的特性是经过层层引入,最终引入具体的实现代码的。

core-js-pure与core-js-bundle

除了core-js之外,core-js-pure与core-js-bundle这两个包也提供了兼容性。core-js-pure内部的目录结构与core-js一致,只不过core-js-pure不将特性注入到全局。core-js-bundle比较特殊,它是将core-js代码经过打包后再提供,它的结构如下:

core-js-bundle
├─index.js
├─minified.js
├─minified.js.map
└─...

其中index.js是打包过后的特性集合代码,minified.js是经过压缩混淆后的代码。core-js-bundle只能全部引入并注入到全局,不能引入部分目录或者导出某个属性。

打包和浏览器效果

创建Webpack示例

首先创建一个Webapck项目,方便后续打包查看效果。首先执行:

# 创建项目
npm init -y
# 安装webpack依赖
npm add webpack webpack-cli html-webpack-plugin
# 安装core-js依赖
npm add core-js core-js-pure core-js-bundle

创建src/index.js,内容如下:

const arr = ["jz", "plp"];
console.log(arr.lastIndex);

在package.json文件的scripts中增加命令:"build": "webpack"。最后是Webpack配置文件webpack.config.js:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'production', // 生产模式
  entry: './src/index.js', // 源码入口
  plugins: [
    new HtmlWebpackPlugin({ // 生成HTML页面入口
      title: 'jzplp的core-js实验', // 页面标题
    }),
  ],
  output: {
    filename: 'main.js', // 生成文件名
    path: path.resolve(__dirname, 'dist'),  // 生成文件目录
    clean: true, // 生成前删除dist目录内容
  },
};

命令行运行npm run build,即可使用Webpack打包。在dist目录中生成了两个文件,一个是main.js,里面是打包后的js代码;index.html可以让我们在浏览器查看效果。由于我们没有引入core-js,浏览器没有预置lastIndex这个提议中的特性,因此输出undefined。

core-js打包

这里引入core-js,然后打包查看效果。首先是全量引入:

require("core-js");
const arr = ["jz", "plp"];
console.log(arr.lastIndex);

此时浏览器输出1,表示core-js注入成功,lastIndex特性生效了。但是我们查看main.js,发现居然有267KB。这是因为它把所有特性都引入了。

如果引入require("core-js/full/array"),此时新特性也可以生效。因为只引入了数组相关特性,因此main.js的大小为59.3KB,比全量引入小很多。

如果引入require("core-js/full/array/last-index"),此时新特性也可以生效。因为只引入了这一个特性,因此main.js的大小为12.2KB。

Babel与core-js

从前面打包的例子中可以看到,core-js整个打包进项目中是非常巨大的,可能比你正常项目的大小还要更大。这样明显会造成占用资源更多,页面加载时间变慢等问题。一个解决办法是,只引入我们代码中使用到的特性,以及我们要适配的浏览器版本中不兼容的特性,用不到的特性不打包进代码中。Babel就提供了这样的功能。

创建Babel示例

# 创建项目
npm init -y
# 安装webpack依赖
npm add @babel/core @babel/cli @babel/preset-env
# 安装core-js依赖
npm add core-js core-js-pure core-js-bundle

创建src/index.js,内容如下:

require('core-js');
const jzplp = 1;

在package.json文件的scripts中增加命令:"babel": "babel src --out-dir lib"。最后是Babel配置文件babel.config.json:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "chrome": "100"
        }
      }
    ]
  ]
}

targets中表示我们需要兼容的浏览器版本。执行npm run babel,生成结果再lib/index.js中,内容如下。可以看到未对core-js做任何处理。

"use strict";

require('core-js');
const jzplp = 1;

preset-env配置entry

@babel/preset-env是一个Babel预设,可以根据配置为代码增加兼容性处理。前面创建Babel示例时已经增加了这个预设,但是没有增加core-js配置。这里我们加一下:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "chrome": "100"
        },
        "useBuiltIns": "entry",
        "corejs": "3.47.0"
      }
    ]
  ]
}

这里增加了corejs版本和useBuiltIns配置,值为entry。配置这个值,会使得@babel/preset-env根据配置的浏览器版本兼容性,选择引入哪些core-js中的特性。这里再执行命令行,结果如下:

"use strict";

require("core-js/modules/es.symbol.async-dispose.js");
require("core-js/modules/es.symbol.dispose.js");
// ... 更多es特性省略
require("core-js/modules/esnext.array.filter-out.js");
require("core-js/modules/esnext.array.filter-reject.js");
// ... 更多esnext特性省略
require("core-js/modules/web.dom-exception.stack.js");
require("core-js/modules/web.immediate.js");
// ... 更多web特性省略
const jzplp = 1;

可以看到core-js被拆开,直接引入了特性本身。在配置chrome: 100版本时,引入的特性为215个。我们修改配置chrome: 140版本时,再重新生成代码,此时引入的特性为150个。可以看到确实时根据浏览器版本选择不同的特性引入。这对于其它core-js的引入方式也生效:

// 源代码
require('core-js/stable');
const jzplp = 1;

// 生成代码
"use strict";

require("core-js/modules/es.symbol.async-dispose.js");
require("core-js/modules/es.symbol.dispose.js");
// ... 更多es特性省略
require("core-js/modules/web.dom-exception.stack.js");
require("core-js/modules/web.immediate.js");
// ... 更多web特性省略
const jzplp = 1;

我们引入core-js/stable,可以看到生成代码中不引入esnext特性了。在配置chrome: 100版本时,引入的特性为71个,配置chrome: 100版本时,引入的特性为6个。同样的,如果引入换成core-js/full/array,就会只引入数组相关特性,而且也是根据浏览器兼容版本引入。

preset-env配置usage

@babel/preset-env的useBuiltIns配置值为usage时,Babel不仅会跟根据配置的浏览器版本兼容性,还会根据代码中实际使用的特性来选择引入哪些core-js中的特性。首先是Babel配置:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "chrome": "100"
        },
        "useBuiltIns": "usage",
        "corejs": "3.47.0"
      }
    ]
  ]
}

然后是要处理的代码,注意配置usage时是不需要手动引入core-js的。我们配置不同的Chrome浏览器版本,看看输出结果如何:

// 源代码
const jzplp = new Promise();
const b = new Map();

// chrome 50 生成代码 
"use strict";

require("core-js/modules/es.array.iterator.js");
require("core-js/modules/es.map.js");
require("core-js/modules/es.promise.js");
require("core-js/modules/web.dom-collections.iterator.js");
const jzplp = new Promise();
const b = new Map();

// chrome 100 生成代码 
"use strict";

const jzplp = new Promise();
const b = new Map();

首先可以看到,引入core-js中的特性数量变得非常少了,代码中没有用到的特性不再引入。其次不同的浏览器版本引入的特性不一样,因此还是会根据浏览器兼容性引入特性。我们再修改一下源代码试试:

// 源代码
const jzplp = new Promise();
const b = new Map();
Promise.try(() =>{});

// chrome 50 生成代码 
"use strict";

require("core-js/modules/es.array.iterator.js");
require("core-js/modules/es.map.js");
require("core-js/modules/es.promise.js");
require("core-js/modules/es.promise.try.js");
require("core-js/modules/web.dom-collections.iterator.js");
const jzplp = new Promise();
const b = new Map();
Promise.try(() => {});

// chrome 100 生成代码 
"use strict";

require("core-js/modules/es.promise.try.js");
const jzplp = new Promise();
const b = new Map();
Promise.try(() => {});

可以看到,源代码中增加了Promise.try,引入的特性也随之增加了对应的core-js特性引入。因此,使用@babel/preset-env的usage配置,可以保证兼容性的同时,最小化引入core-js特性。另外这个配置并不会自动引入提议特性,如果需要则额外配置proposals为true。

@babel/polyfill

@babel/polyfill是一个已经被弃用的包,推荐直接使用core-js/stable。查看@babel/polyfill源码,发现他就是引入了core-js特性与regenerator-runtime这个包。regenerator-runtime也是一个兼容性相关的包,可以帮助添加generatore和async/await相关语法。作为替代可以这样引入:

import 'core-js/stable';
import 'regenerator-runtime/runtime';

@babel/runtime

@babel/runtime就像自动引入版的core-js-pure。它还是根据代码实际使用的特性来注入core-js特性,但它不注入到全局,而是引入这些API再调用。这里我们使用@babel/plugin-transform-runtime插件,里面包含了@babel/runtime相关逻辑。首先看下Babel配置:

{
  "plugins": [["@babel/plugin-transform-runtime", { "corejs": 3 }]]
}

再转义上一节中的代码,结果如下:

// 源代码
const jzplp = new Promise();
const b = new Map();
Promise.try(() =>{});

// 生成代码
import _Promise from "@babel/runtime-corejs3/core-js-stable/promise";
import _Map from "@babel/runtime-corejs3/core-js-stable/map";
const jzplp = new _Promise();
const b = new _Map();
_Promise.try(() => {});

可以看到,虽然没有直接引入core-js-pure,但效果是一样的。打开@babel/runtime-corejs3这个包查看,里面实际上就是导出了core-js-pure中的特性。例如:

// @babel/runtime-corejs3/core-js-stable/map.js 文件内容
module.exports = require("core-js-pure/stable/map");

core-js/configurator强制控制

如果希望在正常引入core-js时,对于部分特殊属性进行引入或者不引入的控制,就需要用到core-js/configurator。这个工具可以配置三种选项:

  • useNative: 当环境中有这个特性时不引入,当确定没有时才引入
  • usePolyfill: 明确引入这个特性
  • useFeatureDetection: 默认行为,和不使用core-js/configurator一致

useNative不引入

首先试试不引入特性,这里我们使用Promise这个特性为例。首先是不引入core-js的效果,可以看到全局Promise对象被我们改掉了。

const jzplp = {};
Promise = jzplp;
console.log(Promise, Promise === jzplp);

/* 输出结果
{} true
*/

然后在中间引入core-js试试。可以看到我们改掉的Promise,被core-js给改回去了。

const jzplp = {};
Promise = jzplp;
require("core-js/actual");
console.log(Promise, Promise === jzplp);

/* 输出结果
[Function: Promise] false
*/

这时候,如果不希望core-js改掉我们自定义的Promise,可以利用useNative配置,强制core-js不引入这个特性。看结果core-js引入之后,我们自定义的Promise依然存在。

const configurator = require("core-js/configurator");
configurator({
  useNative: ["Promise"],
});
const jzplp = {};
Promise = jzplp;
require("core-js/actual");
console.log(Promise, Promise === jzplp);

/* 输出结果
{} true
*/

usePolyfill强制引入

想要验证usePolyfill的效果,需要找一个环境中本来存在的特性,core-js即使引入也不会修改的特性。Promise不行,因为core-js引入时会对这个Promise增加子特性。Promise.try也不行,因为原来环境中不存在。这里试一下Promise.any,这是环境中本来就存在的特性:

console.log(Promise.any);
const jzplp = () => {};
Promise.any = jzplp;
console.log(Promise.any, Promise.any === jzplp);

/* 输出结果
[Function: any]
[Function: jzplp] true
*/

可以看到,Promise.any原来就存在,但是被我们修改成了新函数。再引入core-js试试:

console.log(Promise.any);
const jzplp = () => {};
Promise.any = jzplp;
require('core-js');
console.log(Promise.any, Promise.any === jzplp);

/* 输出结果
[Function: any]
[Function: jzplp] true
*/

引入了core-js之后,结果没有变化。这说明core-js并不会修改我们自定义的函数。这时候就可以试一下usePolyfill的效果了:

const configurator = require("core-js/configurator");
configurator({
  usePolyfill: ["Promise.any"],
});
console.log(Promise.any);
const jzplp = () => {};
Promise.any = jzplp;
require('core-js');
console.log(Promise.any, Promise.any === jzplp);

/* 输出结果
[Function: any]
[Function: any] false
*/

可以看到,Promise.any又被改为了真正起效果的函数,这说明usePolyfill的强制引入特性是有效的。

core-js中的特性选择

前面我们体验了Babel根据浏览器兼容性,选择不同的core-js特性引入,那么不同浏览器兼容哪些特性的数据是从哪里获取呢?core-js本身就提供了这个功能。

core-js-compat

core-js-compat提供了不同浏览器对应特性的兼容性数据。它有好几个参数,这里先列举一下含义:

  • targets: Browserslist格式的浏览器兼容配置
  • modules: 需要设置兼容性配置的模块,可以是core-js/full,也可以是某个特性,甚至是正则
  • exclude: 需要排除的模块
  • version: 使用的core-js版本
  • inverse: 反向输出,即输出不需要兼容的特性列表

这里举几个例子试一下:

const compat = require("core-js-compat");
const data = compat({
  targets: "> 10%",
  modules: ["core-js/actual"],
  version: "3.47",
});
console.log(data);

/* 输出结果
{
  list: [
    'es.iterator.concat',
    'es.math.sum-precise',
    'es.async-iterator.async-dispose',
    'esnext.array.group',
    'esnext.array.group-by',
    ...其它特性
  ],
  targets: {
    'es.iterator.concat': { 'chrome-android': '143' },
    'es.math.sum-precise': { 'chrome-android': '143' },
    'es.async-iterator.async-dispose': { 'chrome-android': '143' },
    'esnext.array.group': { 'chrome-android': '143' },
    'esnext.array.group-by': { 'chrome-android': '143' },
    ...其它特性
  }
}
*/

compat会根据我们设置的浏览器兼容性配置,输出特性列表,包含两个字段:list是一个特性名称列表;targets是一个Map结构,key为特性名,值为可以兼容的浏览器。假设我们把上面的 targets改成 > 50%,此时会输出空值:

const compat = require("core-js-compat");
const data = compat({
  targets: "> 50%",
  modules: ["core-js/actual"],
  version: "3.47",
});
console.log(data);

/* 输出结果
{ list: [], targets: {} }
*/

我们增加exclude,排除部分属性,可以看到特性数量大大减少:

const compat = require("core-js-compat");
const data = compat({
  targets: "> 10%",
  modules: ["core-js/actual"],
  exclude: ["esnext"],
  version: "3.47",
});
console.log(data);

/* 输出结果
{
  list: [
    'es.iterator.concat',
    'es.math.sum-precise',
    'es.async-iterator.async-dispose',
    'web.dom-exception.stack',
    'web.immediate',
    'web.structured-clone'
  ],
  targets: {
    'es.iterator.concat': { 'chrome-android': '143' },
    'es.math.sum-precise': { 'chrome-android': '143' },
    'es.async-iterator.async-dispose': { 'chrome-android': '143' },
    'web.dom-exception.stack': { 'chrome-android': '143' },
    'web.immediate': { 'chrome-android': '143' },
    'web.structured-clone': { 'chrome-android': '143' }
  }
}
*/

再试一下inverse的效果:

const compat = require("core-js-compat");
const data = compat({
  targets: "> 10%",
  modules: ["core-js/actual"],
  version: "3.47",
  inverse: true
});
console.log(data);

/* 输出结果
{
  list: [
    'es.symbol',
    'es.symbol.description',
    ...其它特性
  ],
  targets: {
    'es.symbol': {},
    'es.symbol.description': {},
    ...其它特性
  }
}
*/

因为输出的是不需要引入core-js兼容的特性,所以特性数量非常多,而且targets中没有列出支持的浏览器版本。

core-js-builder

前面介绍的core-js-compat是接收参数之后,输出core-js的特性列表数组。而core-js-builder接收类似的参数,直接输出引用core-js的代码。我们首先列举一下参数:

  • targets: Browserslist格式的浏览器兼容配置
  • modules: 需要设置兼容性配置的模块,可以是core-js/full,也可以是某个特性,甚至是正则
  • exclude: 需要排除的模块
  • format: 'bundle'输出打包后的源码;'cjs'和'esm'输出对应格式的引用代码
  • filename: 输出的文件名

我们先试一下例子。首先是format格式的:

const builder = require("core-js-builder");
async function funJzplp() {
  const data = await builder({
    targets: "> 30%",
    modules: ["core-js/actual"],
    format: 'bundle',
  });
  console.log(data);
}
funJzplp();

/* 输出结果
...代码很长,这里节选部分
 (function(module, exports, __webpack_require__) {
"use strict";
var NATIVE_BIND = __webpack_require__(8);
var FunctionPrototype = Function.prototype;
*/

可以看到,builder函数输出了非常长的代码,内容实际为输出的特性经过打包之后的结果代码。再试一下'cjs'和'esm',输出的是对应木块的引用代码:

// format: 'cjs' 输出结果
...代码很长,这里节选部分
require('core-js/modules/es.iterator.concat');
require('core-js/modules/es.math.sum-precise');
require('core-js/modules/es.async-iterator.async-dispose');
*/

// format: 'esm' 输出结果
...代码很长,这里节选部分
import 'core-js/modules/es.iterator.concat.js';
import 'core-js/modules/es.math.sum-precise.js';
import 'core-js/modules/es.async-iterator.async-dispose.js';
*/

如果设置了filename,core-js-builder会创建该名称的文件,并将代码写入到文件中。

总结

这篇文章描述了core-js相关包的代码内容和使用方式。core-js实际上就是提供了JavaScript中一些API特性的兼容实现方式。它与实现语法兼容的Babel一起,可以做到大部分JavaScript的兼容性。当然core-js和Babel也不是万能的,它们都有各自无法转义和兼容的语法和特性。

core-js这个包名字起的非常好,一听就是JavaScript的“核心”包。由于它实现了很多API特性,而且引用数量非常非常大,因此叫“核心”也不为过。虽然这个包引用量很大,但不如React/Vue或者一些其它包出名。因为这个包是处理的更底层的兼容性有问题,因此用户感知不强。core-js包的作者还因为没有钱而遇到很多问题,这个包并没有让他变的富有。

参考

昨天 — 2026年1月20日技术

回首 jQuery 20 年:从辉煌到没落

作者 冴羽
2026年1月20日 18:53

2006 年 1 月 14 日,John Resig 发布了名为 jQuery 的 JavaScript 库。

至今已经过去了 20 年!

20 周年之际,jQuery 4.0 正式发布了!

是的,就是那个被无数人宣布“已死”的 jQuery,经过 10 年的等待后迎来了重大更新。

更让人意想不到的是,根据 W3Techs 的数据,jQuery 仍然被全球 78% 的网站使用

这个数字意味着什么?

在 React、Vue、Angular 等现代框架横行的今天,那个曾经被我们嫌弃“老掉牙”的 jQuery,依然在互联网的角落里默默发光发热。

从 2006 年 John Resig 在 BarCampNYC 大会上首次发布,到今天 4.0 版本的现代化重生,jQuery 走过了整整 20 年。

它不仅是一个 JavaScript 库,更是一个时代的缩影,见证了前端技术从混沌到繁荣的完整历程。

本篇让我们一起回顾 jQuery 的 20 年,见证它的辉煌与没落。

1. 混沌时代

回望 2006 年,彼时正值第一次浏览器战争的尾声,微软 IE 与网景 Navigator 刚刚打完仗,但遗留下来的兼容性问题却让无数前端开发者头疼不已。

当时开发者需要面对各种浏览器的“奇技淫巧”,光是一个事件绑定就要写一大串兼容代码。

来看看这段早期的 jQuery 源码:

// 如果使用Mozilla
if (jQuery.browser == "mozilla" || jQuery.browser == "opera") {
    jQuery.event.add(document, "DOMContentLoaded", jQuery.ready);
}
// 如果使用IE
else if (jQuery.browser == "msie") {
    document.write("<scr" +="" "ipt="" id="__ie_init" defer="true" "="" "src="javascript:void(0)"><\/script>");
    var script = document.getElementById("__ie_init");
    script.onreadystatechange = function() {
        if (this.readyState == "complete") jQuery.ready();
    };
}
// 如果使用Safari
else if (jQuery.browser == "safari") {
    jQuery.safariTimer = setInterval(function(){
        if (document.readyState == "loaded" || document.readyState == "complete") {
            clearInterval(jQuery.safariTimer);
            jQuery.ready();
        }
    }, 10);
}
</scr">

看到没?仅仅是处理页面加载事件就要写这么多兼容代码!这在今天是难以想象的。

2. 横空出世

就在这时,jQuery 横空出世,彻底改变了游戏规则。

John Resig 提出了一个简单而优雅的理念:

Write Less,Do More

jQuery 通过精简常见的重复性任务,去除所有不必要的标记,使代码简洁、高效且易于理解,从而实现这一目标。

jQuery 带来了两大革命性改变:

  1. 强大的选择器引擎:不再局限于简单的 ID 和类选择,可以进行复杂的关系选择
  2. 优雅的 API 设计:链式操作让代码既简洁又易读

看看这个对比:

// 传统DOM操作
var elements = document.getElementById("contacts").getElementsByTagName("ul")[0].getElementsByClassName("people");
for (var i = 0; i < elements.length; i++) {
  var items = elements[i].getElementsByTagName("li");
  for (var j = 0; j < items.length; j++) {
    // 操作每个item
  }
}

// jQuery方式
$("#contacts ul.people li").each(function () {
  // 操作每个item
});

差距一目了然!

jQuery 的出现让前端开发变得如此优雅,以至于迅速在开发者群体中传播开来。

3. 辉煌岁月

随着 jQuery 的普及,一个庞大的插件生态迅速建立起来。

从日期选择器到轮播图,从表单验证到动画效果,几乎你能想到的功能都有对应的 jQuery 插件。

那时候前端开发的标准流程是:

  1. 下载 jQuery 核心库
  2. 搜索并下载所需的 jQuery 插件
  3. 组合这些插件完成项目

同时,jQuery 的管理也变得正式。

2011 年,jQuery 团队正式成立了 jQuery 理事会。2012 年,jQuery 理事会成立了 jQuery 基金会。

4. 影响深远

jQuery 的影响力远远超出了技术本身,它推动了整个前端行业的发展:

  • **大幅降低了前端开发的门槛:**让更多的开发者能够参与到前端开发中来
  • 提升了前端工程师的社会地位:让前端开发变得更加专业和重要
  • 促进了浏览器厂商的标准化:jQuery 的成功证明了统一 API 的重要性
  • 催生了现代前端工具链:为后来的模块化、构建工具奠定了基础

甚至连 jQuery 的选择器引擎 Sizzle 后来都被提取出来,影响了整个选择器标准的发展。

5. 价值动摇

jQuery 之所以能够快速普及,很大程度上是因为浏览器的“不争气”。

而当浏览器厂商开始认真对待标准化问题时,jQuery 的核心价值就开始动摇了。

2009 年后,浏览器标准化进程大幅加速:

  • querySelectorquerySelectorAll的出现
  • classList API 的普及
  • fetch API 替代 Ajax 需求
  • CSS3 动画替代 JavaScript 动画

现代浏览器 API 的完善,让很多 jQuery 功能都有了原生替代品:

// jQuery方式
$("#btn").on("click", () => $("#box").addClass("active"));

// 原生方式
document.querySelector("#btn").addEventListener("click", () => {
  document.querySelector("#box").classList.add("active");
});

你可以发现,差距已经不再那么明显!

6. 框架打击

2010 年,React、Angular、Vue 等现代框架相继登场,带来了革命性的变化:

  1. 组件化思维:从 DOM 操作转向组件构建
  2. 声明式编程:描述“什么”而不是“如何”
  3. 状态管理:解决了复杂应用的维护问题
  4. 工具链完善:从构建到部署的完整解决方案

这些框架从架构层面解决了 jQuery 时代的问题,就像从手工制作转向了工业化生产。

7. 惨遭背叛

2018 年,GitHub 公开宣布从其前端移除 jQuery,这个标志性事件被广泛解读为“jQuery 时代的终结”。

GitHub 在博客中详细说明了迁移的理由:现代浏览器 API 已经足够完善,React 的组件化模式更适合大型应用的维护。

这个“背叛”对 jQuery 的声誉造成了重大打击,也加速了它在新技术栈中的衰落。

8. 瘦死骆驼

尽管在技术前沿领域失势,但 jQuery 在存量市场中的地位依然稳固:

  • 78% 的顶级网站仍在使用 jQuery
  • WordPress 等 CMS 系统大量依赖 jQuery
  • 企业级应用中 jQuery 代码基数庞大

为什么企业不直接抛弃 jQuery?

因为现实远比理想复杂:

  1. 业务逻辑与 DOM 深度耦合:重构成本巨大
  2. 第三方插件依赖:很多插件没有现代替代方案
  3. 迁移风险:新功能开发受阻,影响营收
  4. 技能断层:团队对旧技术熟悉,对新技术陌生

比如一个电商网站如果要重构支付流程的 jQuery 代码,任何 bug 都可能导致直接的经济损失。这种风险评估让很多公司望而却步。

此外,WordPress 支撑着全球 43% 的网站,它的核心仍然依赖 jQuery。这个庞大的生态系统意味着:

  • 数十万主题和插件依赖 jQuery
  • 内容管理系统对稳定性的要求远超先进性
  • 托管服务商倾向于保持现有技术栈

所以即使所有前端开发者都不再使用 jQuery,仅 WordPress 生态系统就能让它继续存在很多年。

9. 拥抱现代

2026 年 1 月 17 日,jQuery 4.0 正式发布,在这次发布中:

  • 移除对 IE11 以下版本的支持:摆脱历史包袱
  • 迁移到 ES 模块:与现代构建工具兼容
  • 增加 Trusted Types 支持:提升安全性
  • 移除已弃用 API:清理技术债务

这次更新像是 jQuery 面向现代 Web 的断舍离。

10. 结语:一个时代的完结

jQuery 20 年的发展史,就是一部前端技术的缩影。

它从解决现实问题出发,推动了整个行业的发展,最终也随着时代的变化而淡出主流。

这并不意味着 jQuery 是失败的。恰恰相反,它超额完成了自己的历史使命

  • 它让无数人学会了前端开发
  • 它推动了浏览器厂商的标准化
  • 它催生了现代前端生态
  • 它证明了开源协作的力量

正如那句经典的台词:“并不是英雄迟暮,而是时代需要新的英雄。”

jQuery 4.0 的发布不是回光返照,它告诉我们:技术没有绝对的对错,只有是否适合那个时代的需求

今天,当我们在 React、Vue 的组件化世界中忙碌时,偶尔回望一下 jQuery 的简单优雅,也许能获得一些关于技术本质的思考:

好的工具应该让人更专注于创造价值,而不是被技术本身所困扰。

我是冴羽,10 年笔耕不辍,专注前端领域,更新了 10+ 系列、300+ 篇原创技术文章,翻译过 Svelte、Solid.js、TypeScript 文档,著有小册《Next.js 开发指南》、《Svelte 开发指南》、《Astro 实战指南》。

欢迎围观我的“网页版朋友圈”,关注我的公众号:冴羽(或搜索 yayujs),每天分享前端知识、AI 干货。

用 Intersection Observer 打造丝滑的级联滚动动画

作者 阿明Drift
2026年1月20日 17:49

无需任何动画库,仅用原生 Web API 实现滚动时丝滑的淡入滑入效果,兼顾性能与体验。

你是否见过这样的交互动效:

  • 用户滚动页面时,一组卡片像被“唤醒”一样,依次从下方滑入并淡入;

滚动触发动画示例

  • 如果这些元素在页面加载时已在视口内,它们也会自动按顺序浮现。

初始加载动画示例

这种效果不仅视觉流畅,还能有效引导用户注意力,提升内容层次感。更重要的是——它不依赖 GSAP、AOS 等第三方库,仅靠 Intersection Observer + CSS 动画 + 少量 JavaScript,就能实现高性能、可访问、且高度可控的滚动触发型级联动画。

今天,我们就来一步步拆解这个经典动效,并给出一套可直接复用的轻量级方案


🔧 核心原理概览

整个动画系统依赖三个关键技术点:

技术 作用
IntersectionObserver 监听元素是否进入视口,避免频繁 scroll 事件
CSS @keyframes 定义滑入 + 淡入动画
--animation-order 自定义属性 通过 calc() 动态设置 animation-delay,实现“逐个延迟”的级联感

最关键的设计哲学是:动画只在用户能看到它的时候才执行,既节省性能,又避免“闪现”。


🧱 HTML 结构(简化版)

为便于理解,我们剥离业务逻辑,只保留动效核心:

<div class="container">
    <ul class="card-list">
        <li class="card scroll-trigger animate--slide-in" data-cascade style="--animation-order: 1;"
            >Card 1</li
        >
        <li class="card scroll-trigger animate--slide-in" data-cascade style="--animation-order: 2;"
            >Card 2</li
        >
        <li class="card scroll-trigger animate--slide-in" data-cascade style="--animation-order: 3;"
            >Card 3</li
        >
        <!-- 更多卡片... -->
    </ul>
</div>

💡 类名与属性说明

  • .scroll-trigger:表示该元素需要被滚动监听;
  • .animate--slide-in:启用滑入动画;
  • data-cascade:JS 识别“需设置动画顺序”的标志;
  • --animation-order:CSS 自定义属性,用于计算延迟时间(如第 2 个元素延迟 150ms)。

🎨 CSS 动画定义

:root {
    --duration-extra-long: 600ms;
    --ease-out-slow: cubic-bezier(0, 0, 0.3, 1);
}

/* 仅在用户未开启“减少运动”时启用动画(晕动症用户友好) */
@media (prefers-reduced-motion: no-preference) {
    .scroll-trigger:not(.scroll-trigger--offscreen).animate--slide-in {
        animation: slideIn var(--duration-extra-long) var(--ease-out-slow) forwards;
        animation-delay: calc(var(--animation-order) * 75ms);
    }

    @keyframes slideIn {
        from {
            transform: translateY(2rem);
            opacity: 0.01;
        }
        to {
            transform: translateY(0);
            opacity: 1;
        }
    }
}

✨ 参数说明

属性 作用
transform translateY(2rem) → 0 由下往上滑入
opacity 0.01 → 1 淡入(避免完全透明导致布局跳动)
animation-delay n × 75ms 第1个延迟75ms,第2个150ms……形成级联
animation-fill-mode forwards 动画结束后保持最终状态

无障碍提示:通过 @media (prefers-reduced-motion) 尊重用户偏好,对晕动症用户更友好。


🕵️ JavaScript:Intersection Observer 监听逻辑

为什么不用 scroll 事件?

传统方式:

// ❌ 性能差,频繁触发
window.addEventListener('scroll', checkVisibility);

现代方案:

// ✅ 高性能,浏览器底层优化
const observer = new IntersectionObserver(callback, options);

完整监听逻辑

const SCROLL_ANIMATION_TRIGGER_CLASSNAME = 'scroll-trigger';
const SCROLL_ANIMATION_OFFSCREEN_CLASSNAME = 'scroll-trigger--offscreen';

function onIntersection(entries, observer) {
    entries.forEach((entry, index) => {
        const el = entry.target;

        if (entry.isIntersecting) {
            // 进入视口:移除 offscreen 类,允许动画播放
            el.classList.remove(SCROLL_ANIMATION_OFFSCREEN_CLASSNAME);

            // 若为级联元素,动态设置顺序(兜底)
            if (el.hasAttribute('data-cascade')) {
                el.style.setProperty('--animation-order', index + 1);
            }

            // 只触发一次,停止监听
            observer.unobserve(el);
        } else {
            // 离开视口:加上 offscreen 类,禁用动画
            el.classList.add(SCROLL_ANIMATION_OFFSCREEN_CLASSNAME);
        }
    });
}

function initScrollAnimations(root = document) {
    const triggers = root.querySelectorAll(`.${SCROLL_ANIMATION_TRIGGER_CLASSNAME}`);
    if (!triggers.length) return;

    const observer = new IntersectionObserver(onIntersection, {
        rootMargin: '0px 0px -50px 0px', // 元素进入视口 50px 后才触发
        threshold: [0, 0.25, 0.5, 0.75, 1.0],
    });

    triggers.forEach((el) => observer.observe(el));
}

// 页面加载完成后启动
document.addEventListener('DOMContentLoaded', () => {
    initScrollAnimations();
});

🎯 关键设计细节

  • rootMargin: '0px 0px -50px 0px':确保元素完全进入用户视野后再触发动画,避免“刚看到就结束”;
  • 初始所有 .scroll-trigger 元素默认带有 .scroll-trigger--offscreen 类,阻止 CSS 动画生效;
  • unobserve:动画只播放一次,避免重复触发,节省资源。

📊 两种场景下的行为对比

场景 初始状态 触发时机 动画表现
卡片已在视口内 --offscreen 页面加载后立即 依次淡入(基于 --animation-order
卡片在视口外 --offscreen 滚动到视口(超过 50px) 滚动时依次淡入

这正是你感受到的“丝滑感”来源:无论用户如何进入页面,动画总是在最合适的时机出现


💡 总结:这套方案的优势

能力 说明
高性能 使用 IntersectionObserver 替代 scroll 事件,避免频繁计算
精准控制 通过 rootMarginthreshold 灵活调整触发时机
无障碍友好 尊重 prefers-reduced-motion 用户偏好
轻量可复用 无依赖,仅 50 行 JS + 简洁 CSS,适合嵌入任何项目
懒加载兼容 可扩展用于图片懒加载、广告曝光统计等场景

完整 Demo 已上传 CodePen:
👉 codepen.io/AMingDrift/…

如果你正在开发电商、博客、SaaS 产品页等内容密集型网站,不妨将这套方案集成进去,给用户带来更优雅的浏览体验!


学习优秀作品,是提升技术的最佳路径。本文既是我的学习笔记,也希望对你有所启发。

Bipes项目二次开发/扩展积木功能(八)

2026年1月20日 17:22

Bipes项目二次开发/扩展积木功能(八)

新年第一篇文章,这一篇开发扩展积木功能。先看一段VCR。 广告:需要二开Bipes,Scratch,blockly可以找我。 项目地址:maxuecan.github.io/Bipes/index…

VCR

[video(video-CjWu9kdf-1768899636737)(type-csdn)(url-live.csdn.net/v/embed/510…)]

第一:模式选择

在这里插入图片描述 在三种模式中,暂时对海龟编程加了扩展积木功能,点击选择海龟编程,就可以看到积木列表多了个添加按钮。其它模式下不会显示

第二:积木扩展

在这里插入图片描述

点击扩展按钮,会弹窗一个扩展积木弹窗,接着点击卡片,会显示确认添加按钮,最后点击确认添加,就能动态添加扩展积木。

第三:代码解析

ui/components/extensions-btn.js(扩展积木按钮)

import EventEmitterController from '../utils/event-emitter-controller'
import { resetPostion } from '../utils/utils'

export default class extensionsBtn {
    constructor(props) {
        this.settings = props.settings
        this.resetPostion = resetPostion
        if (document.getElementById('content_blocks')) {
            $('#content_blocks').append(this.render())
            this.initEvent()
        }

        // 根据模式,控制扩展按钮的显示
        setTimeout(() => {
            let { mode } = this.settings
            resetPostion()
            $('#extensions-btn').css('display', mode === 'turtle' ? 'block' : 'none')
        }, 1000);
    }
    // 初始化事件
    initEvent() {
        window.addEventListener('resize', (e) => {
            this.resetPostion()
        })

        $('#extensions-btn').on('click', () => {
            EventEmitterController.emit('open-extensions-dialog')
        })
    }

    render() {
        return `
            <div id="extensions-btn">
                <div class="extensions-add"></div>
            </div>
        `
    }
}
ui/components/extensions-dialog.js(扩展积木弹窗)

import ExtensionsList from '../config/extensions-blocks.js'
import { resetPostion } from '../utils/utils'

export default class extensionsDialog {
    constructor() {
        this._xml = undefined
        this._show = false
        this.list = ExtensionsList
        this.use = []
        this.after_extensions = [] // 记录已经添加过的扩展积木
    }
    // 初始化事件
    initEvent() {
        $('.extensions-modal-close').on('click', this.close.bind(this))
        $('.extensions-modal-confirm').on('click', this.confirm.bind(this))
        $('.extensions-modal-list').on('click', this.select.bind(this))
    }
    // 销毁事件
    removeEvent() {
        $('.extensions-modal-close').off('click', this.close.bind(this))
        $('.extensions-modal-confirm').off('click', this.confirm.bind(this))
        $('.extensions-modal-list').off('click', this.select.bind(this))
    }
    // 显示隐藏弹窗
    show() {
        if (this._show) {
            $('.extensions-dialog').remove()
            this.removeEvent()
        } else {
            $('body').append(this.render())
            this.initEvent()
            this.createList()
        }

        this._show = !this._show
    }
    // 创建扩展列表
    createList() {
        $('.extensions-list').empty()
        for (let i in this.list) {
            let li = $('<li>')
                    .attr('key', this.list[i]['type'])
                    .css({
                        background: `url(${this.list[i]['image']}) center/cover no-repeat`,
                    })
            let box = $('<div>')
                    .addClass('extensions-list-image')
                    .attr('key', this.list[i]['type'])
            let detail = $('<div>')
                .addClass('extensions-list-detail')
                .attr('key', this.list[i]['type'])

            let name = $('<h4>').text(this.list[i]['name']).attr('key', this.list[i]['type'])
            let remark = $('<span>').text(this.list[i]['remark']).attr('key', this.list[i]['type'])
            detail.append(name).append(remark)
            $('.extensions-modal-list').append(li.append(box).append(detail))
        }
    }
    // 选择列表
    select(e) {
        let key = e.target.getAttribute('key')
        if (key !== null) {
            let index = this.use.indexOf(key)
            let type = undefined
            if (index !== -1) {
                this.use.splice(index, 1)
                type = 'delete'
            } else {
                this.use.push(key)
                type = 'add'
            }
            this.highlightList(type, key)
            this.showConfirm()
        }
    }
    // 高亮列表项
    highlightList(action, key) {
        $('.extensions-modal-list li').each(function(index) {
            let c_key = $(this).attr('key')
            if (key === c_key) {
                if (action === 'add') {
                    $(this).addClass('extensions-modal-list-act')
                } else if (action === 'delete') {
                    $(this).removeClass('extensions-modal-list-act')
                }
            }
        })
    }
    // 显示确认按钮
    showConfirm() {
        if (this.use.length > 0) {
            $('.extensions-modal-footer').css('display', 'block')
        } else {
            $('.extensions-modal-footer').css('display', 'none')
        }
    }
    // 关闭
    close() {
        this.show()
    }
    // 确认操作
    confirm() {
        let str = ''
        this.use.forEach(item => {
            let index = this.after_extensions.indexOf(item)
            if (index === -1) {
                this.after_extensions.push(item)
                str += this.getExtendsionsXML(item)
            }
        })

        if (str) {
            if (!this._xml) this._xml = window._xml.cloneNode(true)
            let toolbox = this._xml
            toolbox.children[0].innerHTML += str
            Code.reloadToolbox(toolbox)
        }

        this.show()
        resetPostion()
    }
    /* 获取扩展积木的XML */
    getExtendsionsXML(type) {
        let item = ExtensionsList.filter(itm => itm.type === type)
        return item[0].xml
    }
    // 重置toolbox
    resetToolbox() {
        return new Promise((resolve) => {
            this._xml = window._xml.cloneNode(true)
            Code.reloadToolbox(this._xml)
            this.use = []
            this.after_extensions = []
            setTimeout(resolve(true), 200)
        })
    }

    render() {
        return `
            <div class="extensions-dialog">
                <div class="extensions-modal">
                    <div class="extensions-modal-header">
                        <h4></h4>
                        <ul class="extensions-modal-nav">
                            <li class="extensions-modal-nav-act" key="basic">
                                <span key="basic">扩展积木</span>
                            </li>
                        </ul>
                        <div class="extensions-modal-close"></div>
                    </div>

                    <div class="extensions-modal-content">
                        <ul class="extensions-modal-list"></ul>
                    </div>

                    <div class="extensions-modal-footer">
                        <button class="extensions-modal-confirm">确认添加</button>
                    </div>
                </div>
            </div>
        `
    }
}
ui/config/extensions-blocks.js(扩展积木配置)

let turtle = require('./turtle.png')

module.exports = [
  {
    type: 'turtle',
    name: '海龟函数',
    image: turtle,
    remark: '可以调用海龟编辑器中对应Python函数。',
    xml: `
            <category name="海龟" colour="%{BKY_TURTLE_HUE}">
                <block type="variables_set" id="fg004w+XJ=maCm$V7?3T" x="238" y="138">
                    <field name="VAR" id="dfa$SFe(HK(10)Y+T-bS">海龟</field>
                    <value name="VALUE">
                        <block type="turtle_create" id="Hv^2jr?;yxhA=%oCs1=d"></block>
                    </value>
                </block>
                <block type="turtle_create"></block>
                <block type="turtle_move">
                    <value name="VALUE">
                        <block type="variables_get">
                            <field name="VAR">{turtleVariable}</field>
                        </block>
                    </value>
                    <value name="distance">
                        <shadow type="math_number">
                            <field name="NUM">50</field>
                        </shadow>
                    </value>
                </block>
                <block type="turtle_rotate">
                    <value name="VALUE">
                        <block type="variables_get">
                            <field name="VAR">{turtleVariable}</field>
                        </block>
                    </value>
                    <value name="angle">
                        <shadow type="math_number">
                            <field name="NUM">90</field>
                        </shadow>
                    </value>
                </block>
            <block type="turtle_move_xy">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="x">
                    <shadow type="math_number">
                        <field name="NUM">50</field>
                    </shadow>
                </value>
                <value name="y">
                    <shadow type="math_number">
                        <field name="NUM">50</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_set_position">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="position">
                    <shadow type="math_number">
                        <field name="NUM">50</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_draw_circle">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="radius">
                    <shadow type="math_number">
                        <field name="NUM">50</field>
                    </shadow>
                </value>
                <value name="extent">
                    <shadow type="math_number">
                        <field name="NUM">50</field>
                    </shadow>
                </value>
                <value name="steps">
                    <shadow type="math_number">
                        <field name="NUM">50</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_draw_polygon">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="num_sides">
                    <shadow type="math_number">
                        <field name="NUM">5</field>
                    </shadow>
                </value>
                <value name="radius">
                    <shadow type="math_number">
                        <field name="NUM">30</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_draw_point">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="diameter">
                    <shadow type="math_number">
                        <field name="NUM">50</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_write">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="text">
                    <shadow type="text">
                        <field name="TEXT">Hello</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_set_heading">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="angle">
                    <shadow type="math_number">
                        <field name="NUM">90</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_pendown">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
            </block>
            <block type="turtle_set_pensize">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="size">
                    <shadow type="math_number">
                        <field name="NUM">5</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_set_speed">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="speed">
                    <shadow type="math_number">
                        <field name="NUM">5</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_get_position">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
            </block>
            <block type="turtle_show_hide">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
            </block>
            <block type="turtle_clear">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
            </block>
            <block type="turtle_stop">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
            </block>
            <block type="turtle_set_bgcolor">
                <value name="COLOUR">
                    <block type="colour_picker"></block>
                </value>
            </block>
            <block type="turtle_set_pencolor">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="COLOUR">
                    <block type="colour_picker"></block>
                </value>
            </block>
            <block type="turtle_set_fillcolor">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="COLOUR">
                    <block type="colour_picker"></block>
                </value>
            </block>

            <block type="turtle_set_colormode">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="COLOUR">
                    <shadow type="math_number">
                        <field name="NUM">255</field>
                    </shadow>
                </value>
            </block>
            <block type="turtle_set_fill">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
            </block>
            <block type="turtle_set_color">
                <value name="VALUE">
                    <block type="variables_get">
                        <field name="VAR">{turtleVariable}</field>
                    </block>
                </value>
                <value name="COLOUR">
                    <block type="colour_picker"></block>
                </value>
            </block>
        </category>
        `,
  },
]

总结

扩展积木功能改动挺多的,功能也时不断的完善,讲解可能比较粗糙,也在尽量写注解,有需要可以看下提交日志,信息会比较全。

告别笨重的 Prometheus,这款 5 分钟部署的 Nginx 监控工具凭什么刷屏 GitHub?

2026年1月20日 16:49
前言 作为后端开发者,Nginx 几乎是我们每天都要打交道的“基础设施”。但说实话,Nginx 的运维体验一直很割裂: 原生监控太简陋:stub_status 只能看个连接数,想看接口响应耗时?想看

学术界最大的室内运动捕捉设施为世界领先的无人机研究提供支持

作者 爱迪斯通
2026年1月20日 16:42
亚利桑那州立大学跟踪体积为 230,000 立方英尺的无人机工作室是世界上学术机构中最大的室内无人机研究动捕设施。该设施前身是一个篮球馆,经过五年多的建造,由亚利桑那州立大学机器人研究员和副教授Pan

执行了 git commit 然后 git pull,现在想要回退到 pull 之前的状态

作者 新晨437
2026年1月20日 15:45

如果你已经执行了 git commit 然后 git pull,现在想要回退到 pull 之前的状态,这里有几个解决方案:

解决方案

1. 使用 git reset(最常用)

# 查看提交历史,找到你想要回退到的commit
git log --oneline

# 回退到指定的commit(保留工作区更改)
git reset --soft <commit-hash>

# 或者回退并丢弃更改
git reset --hard <commit-hash>

2. 回退到 pull 之前的状态

# 使用 reflog 查看操作历史
git reflog

# 找到 pull 之前的 HEAD 位置,然后重置
git reset --hard HEAD@{n}  # n 是 reflog 中的编号

3. 如果你想要完全撤销 pull 和 commit

# 查看最近的操作,找到 pull 之前的提交
git log --oneline

# 重置到 pull 之前的提交
git reset --hard HEAD~2  # 回退2个提交(假设你commit了1次,pull了1次)

具体操作步骤建议:

  1. 先查看当前状态
git status
git log --oneline
  1. 安全方案(保留更改)
# 先保存当前工作状态(如果有未提交的更改)
git stash

# 回退到 pull 之前的commit
git reset --hard HEAD~1

# 如果需要,恢复stash的更改
git stash pop
  1. 如果你只是想修改 commit 信息
# 修改最后一次commit
git commit --amend

注意事项:

  • 如果已经 push 到远程仓库,需要谨慎操作,可能需要 git push --force
  • 如果是多人协作的项目,最好先和团队沟通
  • 使用 git reset --hard 会丢失所有未提交的更改,请确保已备份

你具体遇到了什么情况?我可以提供更针对性的建议。

Vue组件变量值更新过程记录

作者 cj8140
2026年1月20日 15:45

从 Vue 2.x 源码角度分析将组件变量 a 从空值修改为 1 的完整调用栈如下:


1. 组件初始化阶段

在组件创建时,Vue 会初始化响应式数据:

// 调用栈:
Vue.prototype._init (init.js)
  └── initState (state.js)
      └── initData (state.js)
          └── observe (observer/index.js)
              └── new Observer (observer/index.js)
                  └── walk (observer/index.js)
                      └── defineReactive (observer/index.js) // 为属性 `a` 创建响应式

关键步骤

  • defineReactivea 创建 getter/setter
    • 初始化 dep 实例(依赖收集器)。
    • 通过 Object.defineProperty 重写 a 的访问器:
      Object.defineProperty(obj, key, {
        get() { /* 依赖收集 */ },
        set(newVal) { /* 触发更新 */ }
      })
      

2. 修改 a 的值

执行 this.a = 1 时触发 setter

// 调用栈:
this.a = 1
  └── a 的 setter (defineReactive 内部)
      └── dep.notify() (observer/dep.js)
          └── subs[i].update() (observer/watcher.js)
              └── queueWatcher (scheduler.js)
                  └── nextTick (scheduler.js)
                      └── flushSchedulerQueue (scheduler.js)
                          └── watcher.run (observer/watcher.js)
                              └── watcher.get (observer/watcher.js)
                                  └── 组件重新渲染 (render 函数)

关键步骤详解

  1. setter 触发

    • 检查新值 1 是否与旧值不同(newVal !== oldVal)。
    • 若不同,调用 dep.notify() 通知所有依赖。
  2. dep.notify()

    • 遍历 dep.subs(存储所有订阅该属性的 Watcher)。
    • 调用每个 watcher.update()
  3. watcher.update()

    • 将 Watcher 加入异步队列(queueWatcher)。
    • 通过 nextTick 异步执行更新。
  4. flushSchedulerQueue

    • 遍历队列中的 Watcher,调用 watcher.run()
    • watcher.run()watcher.get() → 重新执行组件的 render 函数。
  5. 重新渲染

    • render 函数执行时访问 a,触发 getter 重新收集依赖。
    • 生成新的虚拟 DOM,对比差异后更新真实 DOM。

3. 依赖收集机制

在首次渲染和后续更新时,getter 负责收集依赖:

// getter 调用栈:
组件访问 a (render 函数)
  └── a 的 getter (defineReactive 内部)
      └── Dep.target (全局唯一 Watcher)
          └── dep.depend() (observer/dep.js)
              └── 将当前 Watcher 添加到 dep.subs

关键点

  • Dep.target:全局唯一变量,指向当前正在执行的 Watcher(如渲染 Watcher)。
  • dep.depend():将当前 Watcher 加入 dep.subs,建立 属性 → Watcher 的依赖关系。

4. 异步更新队列

Vue 使用异步队列合并更新:

// nextTick 流程:
queueWatcher (scheduler.js)
  └── nextTick (util/next-tick.js)
      └── 异步任务 (Promise/MutationObserver/setTimeout)
          └── flushSchedulerQueue (scheduler.js)

优化逻辑

  • 多次修改 a 会被合并为一次更新(避免重复渲染)。
  • 通过 nextTick 确保在 DOM 更新后执行回调。

Vue 3 Proxy 版本的差异

若使用 Vue 3(基于 Proxy):

  1. 初始化:通过 reactive 创建响应式代理。
  2. 修改值:直接触发 Proxy.set 拦截器,后续流程类似(依赖收集、异步更新)。
  3. 核心差异
    • 无需 Object.defineProperty,支持动态属性。
    • 依赖收集通过 Track 操作,更新通过 Trigger 操作。

总结

阶段 核心操作 关键函数/类
初始化 a 创建响应式 getter/setter defineReactiveDep
修改值 触发 setter → 通知依赖 dep.notify()
依赖更新 异步队列合并更新 queueWatchernextTick
重新渲染 执行 render 函数 Watcher.run()

整个流程体现了 Vue 响应式系统的核心:依赖收集getter)和 派发更新setter),通过 异步队列 优化性能。

rxjs基本语法

作者 米诺zuo
2026年1月20日 15:38

RxJS (Reactive Extensions for JavaScript) 是 Angular 中处理异步编程的核心库。 它通过使用 Observable(可观察对象) 序列来编写异步和基于回调的代码。


一、 核心概念

在 RxJS 中,一切基于数据流。

  • Observable (被观察者): 数据的源头,发出数据。
  • Observer (观察者): 数据的消费者,接收数据。
  • Subscription (订阅): 连接 Observable 和 Observer 的桥梁。注意:必须取消订阅,否则会内存泄漏。
  • Operators (操作符): 纯函数,用来处理、转换数据流(如 map, filter)。
  • Subject (主题): 既是 Observable 又是 Observer,可以多播数据(常用于组件通信)。

二、 基础写法

1. 创建 Observable 和 订阅

import { Observable } from 'rxjs';
// 1. 创建 Observable
const observable$ = new Observable(subscriber => {
  subscriber.next(1); // 发出数据
  subscriber.next(2);
  subscriber.next(3);
  subscriber.complete(); // 结束
  // subscriber.error('出错了'); // 抛出异常
});
// 2. 订阅
const subscription = observable$.subscribe({
  next: (x) => console.log('收到数据:', x),
  error: (err) => console.error('错误:', err),
  complete: () => console.log('流结束')
});
// 3. 取消订阅 (非常重要)
subscription.unsubscribe();

2. 简写订阅 (只关心 next)

observable$.subscribe(data => console.log(data));

三、 常用创建操作符

用于生成数据流。

import { of, from, interval, fromEvent, throwError } from 'rxjs';
// 1. of: 依次发出参数
of(1, 2, 3).subscribe(console.log); // 输出: 1, 2, 3
// 2. from: 将数组/Promise 转为 Observable
from([10, 20, 30]).subscribe(console.log); // 输出: 10, 20, 30
// 3. interval: 周期性发出数字 (每1秒发一个)
interval(1000).subscribe(n => console.log(n)); // 0, 1, 2...
// 4. fromEvent: 监听 DOM 事件
fromEvent(document.querySelector('button')!, 'click')
  .subscribe(() => console.log('按钮被点击'));
// 5. throwError: 创建一个只报错的流
// throwError(() => new Error('哎呀出错了')).subscribe();

四、 常用转换操作符

这是 RxJS 最强大的部分,管道 语法是 Angular 18+ 的标准写法。

import { map, filter, pluck } from 'rxjs/operators';
of(1, 2, 3, 4, 5).pipe(
  // 1. map: 转换数据 (类似数组的 map)
  map(x => x * 10), 
  
  // 2. filter: 过滤数据 (只有 true 才会通过)
  filter(x => x > 20)
).subscribe(console.log); 
// 输出: 30, 40, 50
// 3. pluck: 提取对象属性 (已废弃,推荐用 map)
// 旧写法: source$.pipe(pluck('user', 'name'))
// 新写法:
interface User { name: string; age: number; }
const user$: Observable<User> = of({ name: 'Tom', age: 18 });
user$.pipe(map(user => user.name)).subscribe(console.log);

五、 工具操作符 (面试高频)

用于处理流的逻辑,如限流、防抖、错误处理。

import { delay, tap, catchError, takeUntil, debounceTime } from 'rxjs/operators';
import { of, Subject, throwError } from 'rxjs';
// 1. tap: 副作用操作 (不修改数据,通常用于打印日志、存 LocalStorage)
of('Hello').pipe(
  tap(val => console.log('处理前:', val)), 
  delay(1000) // 延迟1秒发射
).subscribe(val => console.log('处理后:', val));
// 2. catchError: 错误捕获 (让流不中断)
throwError(() => new Error('网络错误')).pipe(
  catchError(err => {
    console.error(err);
    // 捕获错误后,返回一个新的 Observable 给下游,防止程序崩溃
    return of('默认数据'); 
  })
).subscribe(console.log); // 输出: 默认数据
// 3. debounceTime: 防抖 (用户停止输入 300ms 后才发送请求)
fromEvent(document.querySelector('input')!, 'input').pipe(
  debounceTime(300)
).subscribe((event: any) => console.log(event.target.value));
// 4. takeUntil: 立即取消订阅 (在 Angular 组件销毁时最常用)
const destroy$ = new Subject<void>();
interval(1000).pipe(
  takeUntil(destroy$) // 当 destroy$ 发出值时,上面的流自动停止
).subscribe(console.log);
// 模拟组件销毁
setTimeout(() => {
  destroy$.next(); // 停止上面的 interval
  destroy$.complete();
}, 5000);

六、 高阶操作符 (处理嵌套流)

当一个 Observable 发出的数据还是一个 Observable 时使用。

import { mergeMap, switchMap, concatMap, exhaustMap } from 'rxjs/operators';
// 场景:点击按钮 -> 发送 HTTP 请求
// 假设 click$ 是点击事件流, getData(id) 返回 Observable
// 1. mergeMap (并行): 点击一次发一次请求,不管上一个有没有完成。
// 适用:并发上传,互不干扰。
click$.pipe(
  mergeMap(() => this.http.get('/api/data'))
).subscribe();
// 2. switchMap (切换): **面试必考**。如果有新请求,取消旧请求。
// 适用:搜索框输入。
searchInput$.pipe(
  switchMap(keyword => this.http.search(keyword)) 
).subscribe();
// 3. concatMap (串行): 等前一个请求完成,再发下一个。
// 适用:必须按顺序执行的任务。
// 4. exhaustMap (排他): 如果有请求正在进行,忽略新的点击。
// 适用:防止重复提交表单。
submitBtn$.pipe(
  exhaustMap(() => this.http.submit())
).subscribe();

七、 Subject (多播)

普通的 Observable 是单播的;Subject 可以让多个订阅者共享同一个数据源。

import { Subject, BehaviorSubject, ReplaySubject } from 'rxjs';
// 1. Subject: 只有订阅后发出的数据才会收到。
const subject = new Subject<number>();
subject.subscribe(n => console.log('A:', n));
subject.next(1); // A 收到 1
subject.subscribe(n => console.log('B:', n));
subject.next(2); // A 收到 2, B 收到 2 (B 错过了 1)
// 2. BehaviorSubject: 必须有初始值,新订阅者会立即收到**最新**的值。
const bs = new BehaviorSubject<number>(0); // 初始值 0
bs.subscribe(n => console.log('C:', n)); // C 立即收到 0
bs.next(100);
// 3. ReplaySubject: 可以缓存最近的 N 个值,新订阅者会收到缓存的历史记录。
const rs = new ReplaySubject(2); // 缓存最近 2 个
rs.next(1);
rs.next(2);
rs.next(3);
rs.subscribe(n => console.log('D:', n)); // D 收到 2 和 3

八、 Angular 实战:AsyncPipe (语法糖)

在 Angular 中,你甚至不需要手动调用 .subscribe()

// 组件 TS
export class MyComponent {
  // 自动处理订阅、取消订阅、变化检测
  data$ = of([{ name: 'Tom' }, { name: 'Jerry' }]); 
}
// 组件 HTML
<div *ngFor="let item of data$ | async">
  {{ item.name }}
</div>

注意: 如果你需要拿到数据后在 TS 逻辑里做复杂处理,还是需要手动 subscribe 并配合 takeUntil 使用。

总结速查表

类别 操作符 作用
创建 of, from, interval 造数据
转换 map, filter 改数据
工具 tap, delay, debounceTime 辅助/拦截
组合 switchMap, mergeMap 处理嵌套流 (HTTP)
生命周期 takeUntil, first, take 管理订阅
错误 catchError, retry 异常处理
多播 Subject, BehaviorSubject 跨组件通信
❌
❌