Files

39 KiB
Raw Permalink Blame History

xbs isolated-vm API 使用说明

本文档已根据当前随包 xbsVm API 文档同步更新。原始文档也随包保存于 assets/runtime-frameworks/xbs-isolated-vm/XBS_API_USAGE.md 与 assets/runtime-frameworks/xbs-isolated-vm/XBS_DOM_API_USAGE.md。

Skill 使用硬约束

  • 只有用户明确选择 isolated-vm 补环境框架时,才复制并使用随包魔改 xbs-isolated-vm/<platform>-<arch>/isolated_vm.node。
  • window.xbs / globalThis.xbs 只存在于 isolated-vm Context 内;不得桥接旧 addon.node 进入 isolated-vm。
  • xbs.dom.createDocument(options) 是当前 xbs DOM 的唯一公开入口;已删除旧 API 不得继续使用。
  • 需要基础 DOM 时优先通过 xbs.dom.createDocument({ url, html, skeleton, omitApis, features }) 创建 document,再由 runtime 或项目代码决定是否安装到 window.document。
  • document.all 优先使用 xbs.dom.createDocument() 自动提供的 HTMLAllCollection / HTMLDDA 行为;只有不使用 createDocument()、而是手写 document 时,才使用 xbs.createUndetectable(callback, handlers) 手动创建,并且必须配合 HTMLAllCollection.prototype、handlers 和原型链 fallback。
  • 手动创建 document.all 时,length / item / namedItem / constructor 等已有原型链属性不要定义成 document.all 自有属性;更接近浏览器的安装方式是挂到 Document.prototype getter,而不是直接作为 document 自有属性。
  • 如果当前 xbs API 已能覆盖目标行为但代码没有优先使用或参数错误,应先修正用法,不得标记为 native 能力缺口。

XBS API 使用说明

本文档说明当前魔改版 isolated-vm 中 window.xbs 的用法。
xbs 只会注入到 isolated-vm 创建出的浏览器式 Context 内,即 window.xbs / globalThis.xbs,不会污染宿主侧 require("isolated-vm") 的原始 API。

1. 基本使用方式

const ivm = require("./isolated-vm");

const isolate = new ivm.Isolate();
const context = isolate.createContextSync();

const result = isolate.compileScriptSync(`
  ({
    windowIsGlobal: window === globalThis,
    hasXbs: !!window.xbs,
    apiNames: Object.keys(window.xbs).sort(),
  })
`).runSync(context, { copy: true });

在 Context 内可以直接使用:

const fn = xbs.createNativeFunction(false, "demo", 0, function () {
  return "ok";
});

fn(); // "ok"

当前 Context 默认具备以下基础关系:

window === globalThis;                                  // true
self === window;                                       // true
top === window;                                        // true
parent === window;                                     // true
window instanceof Window;                              // true
Object.getPrototypeOf(window) === Window.prototype;    // true
Object.getPrototypeOf(Window.prototype) === WindowProperties.prototype; // true
Object.getPrototypeOf(WindowProperties.prototype) === EventTarget.prototype; // true

2. API 总览

当前 window.xbs 暴露 17 个 API:

API 说明
createNativeObject 创建带 native-like 构造函数、实例与原型链的对象。
createNativeFunction 创建 native-like 函数或构造函数。
createProtoChains 批量创建浏览器风格构造函数、原型链、别名、实例工厂与注册表。
createGetter 创建 native-like getter。
createSetter 创建 native-like setter。
createInterceptor 创建带 V8 属性拦截器的对象。
createNativeCollection 创建 HTMLCollection / NodeList 等浏览器集合风格对象。
getMimeTypesAndPlugins 创建浏览器风格 navigator.mimeTypes 与 navigator.plugins。
createUndetectable 创建 V8 undetectable 对象,常用于近似 document.all。
getPrivate 读取 V8 Private slot。
setPrivate 写入 V8 Private slot。
hasPrivate 判断 V8 Private slot 是否存在。
deletePrivate 删除 V8 Private slot。
getProtoChainRegistry 查看当前 isolate 的原型链注册表。
clearProtoChainRegistry 清空当前 isolate 的原型链注册表。
deleteProtoChainRegistryEntry 删除当前 isolate 注册表中的指定构造函数或别名。
throwTypeError 从 native 层抛出 TypeError。

3. createNativeFunction(isConstructor, name, length, callback)

创建 Function.prototype.toString 结果为 native-like 的函数。

参数

参数 类型 说明
isConstructor boolean 是否允许 new 调用。
name string 函数名。
length number 函数的 length。
callback function 实际 JS 回调。

普通函数示例

const add = xbs.createNativeFunction(false, "add", 2, function (a, b) {
  return a + b;
});

add(1, 2); // 3
Function.prototype.toString.call(add);
// "function add() { [native code] }"

构造函数示例

构造调用时,callback 的第一个参数是 isNew,后面才是实际传入参数。构造调用最终返回 this。

const Storage = xbs.createNativeFunction(true, "Storage", 0, function (isNew) {
  this.createdByNew = isNew;
});

const storage = new Storage();
storage.createdByNew; // true

4. createGetter(name, length, callback)

创建 native-like getter,常配合 Object.defineProperty 使用。

const navigator = {};

Object.defineProperty(navigator, "userAgent", {
  get: xbs.createGetter("userAgent", 0, function () {
    return "Mozilla/5.0";
  }),
  enumerable: true,
  configurable: true,
});

navigator.userAgent; // "Mozilla/5.0"
Function.prototype.toString.call(Object.getOwnPropertyDescriptor(navigator, "userAgent").get);
// "function get userAgent() { [native code] }"

5. createSetter(name, length, callback)

创建 native-like setter。

const location = {};
let href = "https://example.com/";

Object.defineProperty(location, "href", {
  get: xbs.createGetter("href", 0, function () {
    return href;
  }),
  set: xbs.createSetter("href", 1, function (value) {
    href = String(value);
  }),
  enumerable: true,
  configurable: true,
});

location.href = "https://example.org/";
location.href; // "https://example.org/"

6. createProtoChains(descriptors)

批量创建构造函数、原型对象、继承关系、实例工厂和别名。该 API 是补浏览器对象模型时最常用的基础能力。

常用 descriptor 字段

字段 类型 说明
name string 构造函数名称。必填。
length number 构造函数 length。
constructor function 构造函数回调。普通构造函数必填。
prototypeParent string 原型父级构造函数名称。
constructorParent string / null 构造函数对象父级。未设置时默认跟随 prototypeParent;设为 null 可禁用默认继承。
readOnlyPrototypeProperty boolean 是否让构造函数的 .prototype 属性只读。
immutablePrototypeObject boolean 是否禁止修改构造函数 .prototype 对象的原型。
immutableInstancePrototype boolean 是否禁止修改实例对象的原型。
hasToStringTag boolean 是否在 prototype 上设置 Symbol.toStringTag,默认 true。
toStringTag string 自定义 Symbol.toStringTag 值。
aliases string[] 当前构造函数的别名列表。
aliasOf string 创建指向已有构造函数的别名。
internalClassName string 设置内部类名,用于影响 Object.prototype.toString。
constructorBehavior "allow" | "throw" | "illegal" 控制 new Constructor() 行为。
callBehavior "allow" | "throw" | "illegal" 控制 Constructor() 直接调用行为。
constructorErrorMessage string 构造调用报错信息。
callErrorMessage string 直接调用报错信息。
prototypeMethods Array 安装到 prototype 的 native-like 方法。
staticMethods Array 安装到构造函数对象上的 native-like 静态方法。
instanceFactoryName string 额外导出一个内部实例工厂。
instanceInitializer function 实例工厂创建对象后的初始化函数。

创建继承链

const chain = xbs.createProtoChains([
  {
    name: "EventTarget",
    length: 0,
    constructor: function () {},
    readOnlyPrototypeProperty: true,
    immutablePrototypeObject: true,
  },
  {
    name: "Node",
    length: 0,
    constructor: function () {},
    prototypeParent: "EventTarget",
    readOnlyPrototypeProperty: true,
    immutablePrototypeObject: true,
  },
]);

const EventTarget = chain.EventTarget;
const Node = chain.Node;

Object.getPrototypeOf(Node.prototype) === EventTarget.prototype; // true
Object.getPrototypeOf(Node) === EventTarget;                    // true
new Node() instanceof EventTarget;                              // true

非法构造 + 内部实例工厂

适合模拟 Document、Location 等不能直接 new,但框架内部需要创建实例的对象。

const chain = xbs.createProtoChains([
  {
    name: "Document",
    length: 0,
    constructor() {},
    constructorBehavior: "throw",
    constructorErrorMessage: "Illegal constructor",
    callBehavior: "throw",
    callErrorMessage: "Illegal constructor",
    instanceFactoryName: "createDocument",
    instanceInitializer(url) {
      this.URL = url;
    },
  },
]);

const Document = chain.Document;
const document = chain.createDocument("https://example.com/");

document instanceof Document; // true
new Document();               // TypeError: Illegal constructor

7. 原型链注册表 API

createProtoChains 创建的构造函数会登记到当前 isolate 的注册表中。注册表按 isolate 隔离,不同 new ivm.Isolate() 之间不会互相污染。

const registry = xbs.getProtoChainRegistry();
registry.constructors; // 构造函数名称数组
registry.aliases;      // 别名映射对象

xbs.deleteProtoChainRegistryEntry("URL"); // 删除指定构造函数或别名
xbs.clearProtoChainRegistry();            // 清空当前 isolate 的注册表

8. createNativeObject(options)

创建带 native-like 构造函数、实例对象与可选父级原型链的对象。新代码更推荐使用 createProtoChains,但该 API 仍可用于兼容旧逻辑。

const result = xbs.createNativeObject({
  name: "Navigator",
  length: 0,
  constructor: function () {},
  isReadOnlyPrototype: true,
  isImmutableProto: true,
});

result.constructor; // Navigator 构造函数
result.instance;    // Navigator 实例

9. createInterceptor(options)

创建带 V8 属性拦截器的对象,用于模拟需要自定义读取、写入、枚举、描述符等行为的宿主对象。

handlers

handler 签名 说明
getter (target, property) 读取属性时调用。返回 { intercept: false } 可放行默认读取。
setter (target, property, value) 写属性时调用。可返回 { intercept: true, value } 改写保存值。
query (target, property) 属性存在性查询。
deleter (target, property) 删除属性时调用。
enumerator (target) 枚举属性名时调用。
definer (target, property, descriptor) Object.defineProperty 时调用。
descriptor (target, property) Object.getOwnPropertyDescriptor 时调用。

示例

const target = { existing: 1 };

const obj = xbs.createInterceptor({
  target,
  internalClassName: "MagicObject",
  handlers: {
    getter(targetObject, property) {
      if (property === "virtual") {
        return { value: 42 };
      }
      return { intercept: false };
    },
    setter(targetObject, property, value) {
      return { intercept: true, value: String(value) };
    },
    descriptor(targetObject, property) {
      if (property === "virtual") {
        return {
          value: 42,
          writable: true,
          enumerable: true,
          configurable: true,
        };
      }
    },
    enumerator() {
      return ["virtual"];
    },
  },
});

obj.virtual;                         // 42
obj.existing;                        // 1
obj.created = 123;
target.created;                      // "123"
Object.keys(obj);                    // ["virtual"]
Object.prototype.toString.call(obj); // "[object MagicObject]"

10. createNativeCollection(options)

创建浏览器集合对象,例如 HTMLCollection、NodeList、PluginArray 类似对象。

const first = { id: 1 };
const second = { id: 2 };

const result = xbs.createNativeCollection({
  name: "HTMLCollection",
  items: [
    { name: "first", value: first },
    { name: "second", value: second },
  ],
  hasToStringTag: false,
  internalClassName: "HTMLCollection",
});

const collection = result.collection;

collection.length;                         // 2
collection[0] === first;                   // true
collection.first === first;                // true
collection.item(1) === second;             // true
collection.namedItem("second") === second; // true
collection.namedItem("missing");           // null
[...collection];                           // [first, second]
Object.prototype.toString.call(collection); // "[object HTMLCollection]"

返回对象通常包含:

  • collection:集合实例。
  • constructor:集合构造函数。
  • [name]:以集合名称命名的构造函数,例如 result.HTMLCollection。

11. getMimeTypesAndPlugins([config])

创建浏览器风格 PluginArray、MimeTypeArray、Plugin、MimeType,适合补 navigator.plugins 和 navigator.mimeTypes。

默认用法

const {
  mimeTypes,
  plugins,
  PluginArray,
  MimeTypeArray,
  MimeType,
  Plugin,
} = xbs.getMimeTypesAndPlugins();

navigator.plugins = plugins;
navigator.mimeTypes = mimeTypes;

参数化配置

const result = xbs.getMimeTypesAndPlugins({
  plugins: [
    {
      name: "Custom PDF Viewer",
      filename: "custom-pdf-viewer",
      description: "Custom Portable Document Format",
      mimeTypes: [
        {
          type: "application/x-custom-pdf",
          suffixes: "cpdf",
          description: "Custom PDF",
        },
      ],
    },
  ],
});

result.plugins.length;       // 1
result.mimeTypes.length;     // 1
result.plugins[0].name;      // "Custom PDF Viewer"
result.mimeTypes[0].type;    // "application/x-custom-pdf"
result.mimeTypes[0].enabledPlugin === result.plugins[0]; // true

12. createUndetectable(callback[, handlers])

创建 V8 MarkAsUndetectable() 对象,常用于近似 document.all / HTMLDDA 行为。

const all = xbs.createUndetectable(function () {
  return undefined;
});

typeof all;        // "undefined"
Boolean(all);      // false
all == null;       // true
all === undefined; // false
all();             // undefined

handler 行为

createUndetectable 支持与 createInterceptor 类似的 handlers,常用字段如下:

handler 签名 说明
getter (target, property) 读取属性时调用。返回 { value } 可直接返回虚拟值;返回 { intercept: false } 会放行,让 V8 继续查找 backing target 或原型链。
query (target, property) 属性存在性查询。可返回属性 attribute 数值;也可配合 descriptor 生成属性描述。
descriptor (target, property) Object.getOwnPropertyDescriptor 时调用。可返回标准 descriptor;返回 { intercept: false } 表示放行。
enumerator (target) 枚举属性名时调用。
setter / definer / deleter 同 createInterceptor 用于写入、Object.defineProperty 和删除。

注意:当前实现已经支持原型链 fallback。也就是说,如果 handler 不存在,或 handler 返回 { intercept: false },并且 backing target 没有该自有属性,读取会继续查找对象原型链。这对手动模拟 document.all.length、document.all.item()、Symbol.toStringTag 很重要。

模拟 document.all 时,建议 all 创建都带 handlers,并把 length / item / namedItem / Symbol.toStringTag 放到 HTMLAllCollection.prototype 上,避免 length 变成 document.all 的自有属性:

const document = { _elements: [] };

const chain = xbs.createProtoChains([{
  name: "HTMLAllCollection",
  length: 0,
  constructor() {},
  constructorBehavior: "throw",
  callBehavior: "throw",
  readOnlyPrototypeProperty: true,
  hasToStringTag: true,
  toStringTag: "HTMLAllCollection",
}]);

const HTMLAllCollection = chain.HTMLAllCollection;

Object.defineProperty(HTMLAllCollection.prototype, "length", {
  get: xbs.createGetter("length", 0, function () {
    return document._elements.length;
  }),
  enumerable: true,
  configurable: true,
});

Object.defineProperty(HTMLAllCollection.prototype, "item", {
  value: xbs.createNativeFunction(false, "item", 1, function (index) {
    index = Number(index) >>> 0;
    return index < document._elements.length ? document._elements[index] : null;
  }),
  enumerable: true,
  configurable: true,
});

Object.defineProperty(HTMLAllCollection.prototype, "namedItem", {
  value: xbs.createNativeFunction(false, "namedItem", 1, function (name) {
    name = String(name);
    return document._elements.find(el => el.id === name || el.name === name) || null;
  }),
  enumerable: true,
  configurable: true,
});

function isIndex(property) {
  return typeof property === "number" || /^(0|[1-9]\d*)$/.test(String(property));
}

function resolveAll(property) {
  if (isIndex(property)) {
    const index = Number(property) >>> 0;
    return index < document._elements.length
      ? { value: document._elements[index], enumerable: true }
      : null;
  }
  const name = String(property);
  if (name === "length" || name === "item" || name === "namedItem") {
    return null; // 放行给 HTMLAllCollection.prototype
  }
  const element = document._elements.find(el => el.id === name || el.name === name);
  return element ? { value: element, enumerable: false } : null;
}

function defineAllProperty(target, property) {
  const hit = resolveAll(property);
  if (!hit) return false;
  Object.defineProperty(target, String(property), {
    value: hit.value,
    writable: false,
    enumerable: hit.enumerable,
    configurable: true,
  });
  return true;
}

const all = xbs.createUndetectable(function (value) {
  if (arguments.length === 0 || value == null) return null;
  const hit = resolveAll(value);
  return hit ? hit.value : null;
}, {
  getter(target, property) {
    if (typeof property === "symbol" || !defineAllProperty(target, property)) {
      return { intercept: false };
    }
  },
  query(target, property) {
    if (typeof property === "symbol" || !defineAllProperty(target, property)) {
      return { intercept: false };
    }
  },
  descriptor(target, property) {
    if (typeof property === "symbol" || !defineAllProperty(target, property)) {
      return { intercept: false };
    }
  },
  enumerator(target) {
    for (let i = 0; i < document._elements.length; i++) {
      defineAllProperty(target, String(i));
    }
    return Object.getOwnPropertyNames(target);
  },
});

Object.setPrototypeOf(all, HTMLAllCollection.prototype);

Object.defineProperty(document, "all", {
  value: all,
  enumerable: true,
  configurable: true,
});

Object.hasOwn(document.all, "length"); // false
Object.getOwnPropertyDescriptor(document.all, "length"); // undefined
document.all.length; // 来自 HTMLAllCollection.prototype.length

如果要更贴近浏览器,all 最好安装在 Document.prototype 的 getter 上,而不是直接作为 document 自有属性:

Object.defineProperty(Document.prototype, "all", {
  get: xbs.createGetter("all", 0, function () {
    return all;
  }),
  enumerable: true,
  configurable: true,
});

13. Private slot API

基于 V8 Private::ForApi 实现,适合把内部状态存放到 JS 对象上,同时避免普通属性枚举或直接访问。

const obj = {};

xbs.hasPrivate(obj, "slot");       // false
xbs.setPrivate(obj, "slot", 123);  // true
xbs.hasPrivate(obj, "slot");       // true
xbs.getPrivate(obj, "slot");       // 123
xbs.deletePrivate(obj, "slot");    // true
xbs.hasPrivate(obj, "slot");       // false

14. throwTypeError(message)

从 native 层抛出 TypeError。

function assertIllegalInvocation(thisValue, Ctor) {
  if (!(thisValue instanceof Ctor)) {
    xbs.throwTypeError("Illegal invocation");
  }
}

注意:

  • 不传参数会抛出参数数量错误。
  • 第一个参数不是字符串会抛出类型错误。

15. 推荐实践

  1. 所有非 WebAPI 的辅助能力都放在 window.xbs 下,不要直接污染 window。
  2. 对外可见的浏览器对象优先使用 createProtoChains 创建构造函数与原型链。
  3. 对手写 document.all 这类特殊对象才优先使用 createUndetectable(callback, handlers);如果需要基础 DOM,应优先使用 xbs.dom.createDocument() 自动提供的 document.all。
  4. 对 navigator.plugins / navigator.mimeTypes 优先使用 getMimeTypesAndPlugins。
  5. 对集合对象优先使用 createNativeCollection,不要直接用普通数组代替。
  6. 对内部状态优先使用 private slot,减少普通属性泄露。
  7. 多 isolate 场景下,原型链注册表会按 isolate 隔离;清理 isolate 时会同步清理对应 XBS 注册表。
  8. 宿主侧 isolated-vm API 保持原样,xbs 只存在于创建出的 Context 内。

16. 快速自检

const expected = [
  "clearProtoChainRegistry",
  "createGetter",
  "createInterceptor",
  "createNativeCollection",
  "createNativeFunction",
  "createNativeObject",
  "createProtoChains",
  "createSetter",
  "createUndetectable",
  "deletePrivate",
  "deleteProtoChainRegistryEntry",
  "getMimeTypesAndPlugins",
  "getPrivate",
  "getProtoChainRegistry",
  "hasPrivate",
  "setPrivate",
  "throwTypeError",
];

Object.keys(window.xbs).sort().join("\n") === expected.join("\n");

也可以直接运行项目内测试:

node --no-node-snapshot tests\xbs-basic.js

XBS DOM API 使用说明

本文说明当前 xbsVm 在 isolated-vm Context 内提供的 DOM 能力。入口统一为 window.xbs.dom,不会修改 host 侧 isolated-vm 的原始 API。

1. 当前公开 API

当前 xbs.dom 只保留一个公开方法:

xbs.dom.createDocument(options)

以下 API 已删除,不再对外暴露:

  • attachDocument() / detachDocument()
  • installConstructors() / getConstructors()
  • createElement()
  • createIframeDocument() / attachIframeDocument() / detachIframeDocument()
  • patchApi() / removeApi()
  • createProfile() / getProfile() / listProfiles()
  • snapshotSurface() / restoreSurface()
  • getLastError()

createDocument() 中也不再支持 profile 参数。

2. 设计原则

  • 默认不会把 document 挂到 window 上;调用方自行决定是否执行 window.document = document。
  • createDocument() 会在返回的 document 对象上安装基础 DOM 属性、方法和原型链。
  • DOM 构造函数不会默认挂到 window 上;对象仍然具有内部构造函数和原型链,例如 document.constructor.name === "HTMLDocument"。
  • 删除或禁用某个 DOM API 应在创建 document 前通过配置声明,而不是运行时强删已发布的不可配置属性。
  • C++ 文件中不再通过 JS bootstrap 字符串实现 DOM;DOM 相关逻辑位于 src/xbs/dom/。

3. 快速示例

const ivm = require("isolated-vm");

const isolate = new ivm.Isolate();
const context = isolate.createContextSync();

const result = isolate.compileScriptSync(`
(() => {
  const document = xbs.dom.createDocument({
    url: "https://example.com/",
    html: '<main id="app"><span class="hot">hello</span></main>'
  });

  return {
    hasWindowDocument: "document" in window,
    text: document.querySelector(".hot").textContent,
    url: document.URL
  };
})()
`).runSync(context, { copy: true });

console.log(result);

4. createDocument(options)

参数

const document = xbs.dom.createDocument({
  url: "https://example.com/path",
  html: '<div id="app">ok</div>',
  skeleton: true,
  omitApis: ["document.all"]
});

支持字段:

字段 类型 说明
url string 设置 document.URL,默认是 about:blank。
html string 创建后解析到 document.body,当前为简化 HTML 解析器。
skeleton boolean 是否创建 html/head/body 骨架,默认 true。
omitApis string[] 创建前不安装指定 API。
disabledApis string[] omitApis 的兼容别名。
removedApis string[] omitApis 的兼容别名。
features.documentAll boolean 为 false 时不安装 document.all。
features.iframeContentDocument boolean 为 false 时不安装 iframe 的 contentDocument/contentWindow。

5. 创建前禁用 API

示例:创建一个不带 document.all 的 document。

const document = xbs.dom.createDocument({
  omitApis: ["document.all"]
});

console.log("all" in document); // false

示例:创建一个不带 Document.prototype.createComment 的 document。

const document = xbs.dom.createDocument({
  omitApis: ["Document.prototype.createComment"]
});

console.log(typeof document.createComment); // undefined

示例:禁用 iframe 的 contentDocument。

const document = xbs.dom.createDocument({
  omitApis: ["HTMLIFrameElement.prototype.contentDocument"]
});

const iframe = document.createElement("iframe");
console.log(typeof iframe.contentDocument); // undefined

也支持通配前缀:

xbs.dom.createDocument({
  omitApis: ["Element.prototype.*"]
});

6. 常用 DOM 能力

当前基础实现包含以下对象和能力:

  • HTMLDocument / Document
  • Node / Element / HTMLElement / HTMLIFrameElement
  • Text / Comment / CDATASection / ProcessingInstruction / DocumentFragment
  • Attr
  • DOMTokenList / NodeList / HTMLCollection
  • HTMLAllCollection / HTMLFormControlsCollection / HTMLOptionsCollection / RadioNodeList
  • NamedNodeMap / DOMStringMap

常用示例:

const document = xbs.dom.createDocument();
const div = document.createElement("div");

div.id = "box";
div.className = "a b";
div.classList.add("c");
div.innerHTML = '<span name="s1">text</span>';

document.body.appendChild(div);

console.log(document.querySelector("#box") === div); // true
console.log(div.querySelector("span").textContent);  // text
console.log(div.children.namedItem("s1").tagName);   // SPAN

7. 本阶段新增 DOM API

说明:xbs.dom 对外仍然只暴露 createDocument()。以下新增 API 是通过 createDocument() 返回的 document、其节点对象和原型链对外可用。

7.1 Document 新增属性

const document = xbs.dom.createDocument();

document.documentURI;     // 与 document.URL 保持一致
document.embeds;          // HTMLCollection
document.plugins;         // HTMLCollection

当前 Document 可用的主要属性包括:

  • documentElement
  • head
  • body
  • title
  • readyState
  • URL
  • documentURI
  • referrer
  • characterSet
  • compatMode
  • contentType
  • defaultView
  • activeElement
  • doctype
  • implementation
  • currentScript
  • all
  • scripts
  • forms
  • images
  • links
  • anchors
  • embeds
  • plugins
  • children

7.2 Document 新增创建方法

const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg:rect");
console.log(svg.namespaceURI); // http://www.w3.org/2000/svg
console.log(svg.prefix);       // svg
console.log(svg.localName);    // rect

const attr = document.createAttribute("data-foo");
attr.value = "bar";

const attrNS = document.createAttributeNS("urn:x", "x:key");

const cdata = document.createCDATASection("abc");
console.log(cdata.nodeType);   // 4

const pi = document.createProcessingInstruction("xml-stylesheet", "href=x");
console.log(pi.nodeType);      // 7

当前 Document 可用的主要查询和创建方法包括:

  • getElementById()
  • querySelector()
  • querySelectorAll()
  • getElementsByTagName()
  • getElementsByClassName()
  • getElementsByName()
  • createElement()
  • createElementNS()
  • createTextNode()
  • createComment()
  • createDocumentFragment()
  • createAttribute()
  • createAttributeNS()
  • createCDATASection()
  • createProcessingInstruction()

7.3 Node 新增方法

const node = document.createTextNode("hello");
const clone = node.cloneNode(false);

console.log(node.isEqualNode(clone)); // true

新增或补齐的方法:

  • isEqualNode(otherNode)
  • lookupNamespaceURI(prefix)
  • lookupPrefix(namespaceURI)
  • isDefaultNamespace(namespaceURI)

当前 Node 可用的主要属性和方法包括:

  • 属性:nodeType、nodeName、nodeValue、textContent、parentNode、parentElement、childNodes、firstChild、lastChild、nextSibling、previousSibling、ownerDocument、isConnected、baseURI
  • 方法:appendChild()、removeChild()、insertBefore()、replaceChild()、cloneNode()、contains()、hasChildNodes()、getRootNode()、isSameNode()、isEqualNode()、compareDocumentPosition()、normalize()、lookupNamespaceURI()、lookupPrefix()、isDefaultNamespace()

7.4 Element 新增属性和方法

const el = document.createElementNS("http://www.w3.org/2000/svg", "svg:rect");

console.log(el.namespaceURI); // http://www.w3.org/2000/svg
console.log(el.prefix);       // svg
console.log(el.localName);    // rect

el.setAttributeNS("urn:x", "x:key", "value");
console.log(el.getAttributeNS("urn:x", "key")); // value
console.log(el.hasAttributeNS("urn:x", "key")); // true

const attr = document.createAttribute("data-user-id");
attr.value = "10001";
el.setAttributeNode(attr);

console.log(el.getAttributeNode("data-user-id").ownerElement === el); // true
console.log(el.attributes.getNamedItem("data-user-id").value);        // 10001

console.log(el.dataset.userId); // 10001
el.dataset.token = "abc";
console.log(el.getAttribute("data-token")); // abc
delete el.dataset.token;

新增或补齐的属性:

  • namespaceURI
  • prefix
  • localName
  • slot
  • dir
  • shadowRoot
  • assignedSlot
  • attributes
  • dataset

新增或补齐的方法:

  • getAttributeNS()
  • setAttributeNS()
  • hasAttributeNS()
  • getAttributeNode()
  • setAttributeNode()
  • removeAttributeNode()
  • webkitMatchesSelector()

当前 Element 可用的主要属性和方法包括:

  • 属性:tagName、id、className、classList、attributes、innerHTML、outerHTML、innerText、textContent、children、firstElementChild、lastElementChild、nextElementSibling、previousElementSibling、childElementCount、namespaceURI、prefix、localName、style、dataset、slot、dir、shadowRoot、assignedSlot
  • 方法:getAttribute()、setAttribute()、hasAttribute()、removeAttribute()、toggleAttribute()、getAttributeNames()、getAttributeNS()、setAttributeNS()、hasAttributeNS()、getAttributeNode()、setAttributeNode()、removeAttributeNode()、matches()、webkitMatchesSelector()、closest()、querySelector()、querySelectorAll()、getElementsByTagName()、getElementsByClassName()、append()、prepend()、before()、after()、remove()、replaceWith()、replaceChildren()、insertAdjacentHTML()、insertAdjacentElement()、insertAdjacentText()、focus()、blur()、click()、getBoundingClientRect()、getClientRects()、scrollIntoView()

7.5 Attr

Attr 表示属性节点,可由 document.createAttribute() 或 document.createAttributeNS() 创建,也可通过 element.getAttributeNode() 取得。

const attr = document.createAttribute("data-id");
attr.value = "1";

console.log(attr.name);        // data-id
console.log(attr.value);       // 1
console.log(attr.nodeType);    // 2
console.log(attr.nodeName);    // data-id
console.log(attr.nodeValue);   // 1
console.log(attr.textContent); // 1

主要属性:

  • name
  • value
  • namespaceURI
  • prefix
  • localName
  • specified
  • ownerElement
  • nodeType
  • nodeName
  • nodeValue
  • textContent

7.6 NamedNodeMap

element.attributes 返回 NamedNodeMap,用于访问元素属性节点。

const el = document.createElement("div");
el.setAttribute("id", "box");

console.log(el.attributes.length);                 // 1
console.log(el.attributes.item(0).name);           // id
console.log(el.attributes.getNamedItem("id").value); // box

主要 API:

  • length
  • item(index)
  • getNamedItem(name)
  • getNamedItemNS(namespaceURI, localName)
  • setNamedItem(attr)
  • setNamedItemNS(attr)
  • removeNamedItem(name)
  • removeNamedItemNS(namespaceURI, localName)

7.7 DOMStringMap

element.dataset 返回 DOMStringMap,会映射到元素的 data-* 属性。

const el = document.createElement("div");

el.dataset.userId = "10001";
console.log(el.getAttribute("data-user-id")); // 10001
console.log(el.dataset.userId);               // 10001

delete el.dataset.userId;
console.log(el.hasAttribute("data-user-id")); // false

7.8 集合对象

本阶段新增和补齐的集合对象:

  • HTMLAllCollection
    • 来源:document.all
    • 特性:保持不可检测语义,typeof document.all === "undefined",但仍可访问 length、item()、namedItem()
  • HTMLFormControlsCollection
    • 预留给表单控件集合能力
    • 支持 length、item()、namedItem()
  • HTMLOptionsCollection
    • 预留给 select.options 类场景
    • 支持 length、selectedIndex、item()、namedItem()、add()、remove()
  • RadioNodeList
    • 预留给同名 radio 控件集合
    • 支持 length、value、item()、namedItem()

示例:

const document = xbs.dom.createDocument({
  html: '<main id="app"><span name="s1">text</span></main>'
});

console.log(document.all.length >= 1);           // true
console.log(document.all.item(0));               // html 元素
console.log(document.body.children.namedItem("s1").tagName); // SPAN

7.9 手动创建 document.all

如果用户不使用 xbs.dom.createDocument(),而是自己创建 document,再通过 xbs.createUndetectable() 创建 all 并挂载到 document 上,需要注意:

  • xbs.createUndetectable() 只负责 HTMLDDA/不可检测语义,例如 typeof all === "undefined"、Boolean(all) === false、all == null。
  • document.all.length、item()、namedItem()、[0]、命名属性、枚举和描述符,需要由 HTMLAllCollection.prototype 与 handlers 配合实现。
  • all 创建建议都传入 handlers。handler 未命中时返回 { intercept: false },让 V8 继续查找 backing target 或原型链。
  • length / item / namedItem / constructor 等原型链已有属性不要定义成 document.all 自有属性,否则会和真实浏览器不一致。

最小结构示例:

const all = xbs.createUndetectable(function (value) {
  if (arguments.length === 0 || value == null) return null;
  // 这里根据自己的 document 元素表处理索引或 id/name 查询
  return null;
}, {
  getter(target, property) {
    // 命中索引或 id/name 时,可 Object.defineProperty(target, property, descriptor)
    // 未命中或遇到 length/item/namedItem 等原型属性时必须放行
    return { intercept: false };
  },
  query(target, property) {
    return { intercept: false };
  },
  descriptor(target, property) {
    return { intercept: false };
  },
  enumerator(target) {
    return Object.getOwnPropertyNames(target);
  },
});

Object.setPrototypeOf(all, HTMLAllCollection.prototype);

如果直接挂到 document:

Object.defineProperty(document, "all", {
  value: all,
  enumerable: true,
  configurable: true,
});

则 Object.hasOwn(document, "all") 会是 true。如果要更接近真实浏览器,应挂到 Document.prototype:

Object.defineProperty(Document.prototype, "all", {
  get: xbs.createGetter("all", 0, function () {
    return all;
  }),
  enumerable: true,
  configurable: true,
});

8. 新增 API 的 omitApis 路径

新增 API 仍然遵循创建前禁用规则。常用路径示例:

const document = xbs.dom.createDocument({
  omitApis: [
    "Document.prototype.createElementNS",
    "Document.prototype.createAttribute",
    "Document.prototype.createCDATASection",
    "Document.prototype.createProcessingInstruction",
    "Element.prototype.dataset",
    "Element.prototype.getAttributeNS",
    "Element.prototype.setAttributeNS",
    "Element.prototype.webkitMatchesSelector",
    "Node.prototype.isEqualNode",
    "Node.prototype.lookupNamespaceURI",
    "Attr.prototype.value",
    "NamedNodeMap.prototype.getNamedItem"
  ]
});

也可以继续使用通配前缀:

xbs.dom.createDocument({
  omitApis: [
    "Document.prototype.*",
    "Element.prototype.*",
    "Node.prototype.*"
  ]
});

9. iframe 行为

不再提供 createIframeDocument() 或 attachIframeDocument()。现在 iframe 的 contentDocument 会按需创建。

const document = xbs.dom.createDocument({
  html: '<iframe id="f"></iframe>'
});

const iframe = document.getElementById("f");
console.log(iframe.contentDocument.body.tagName); // BODY
console.log(iframe.contentWindow.document === iframe.contentDocument); // true

10. 当前边界

当前 DOM 是补环境用的基础实现,不是完整浏览器内核:

  • HTML parser 为简化实现,适合基础标签、属性、文本和注释。
  • selector 目前支持标签、#id、.class、简单属性选择器和逗号分组。
  • 暂不支持完整 HTML5 解析、复杂 CSS selector、真实布局、CSSOM、事件系统、MutationObserver、Canvas、WebGL 等。