本系列会将html5相关规范及涉及到的技术逐一解读,以完善我们的知识体系,其分为三期(建议按顺序阅读),分别是
- html标准
- dom标准(本文)
- 其他web api(未发布)
document object model (dom)通过表示内存中的文档结构将web页面和script或程序语言关联起来。注意将html,xml,svg作为对象并不是js语言核心的一部分。
dom表示一个带有逻辑树的文档,树的每个分支末端都在一个节点,并且每个节点包含一个对象。可以用dom方法访问该树,修改文档结构、样式或内容。
节点也可以绑定事件处理器,一旦事件触发,处理器就会执行。
本规范定义了和平台无关的事件、取消活动和节点树的模型。
1 Infrastructure
1.1 Trees
一个tree是一个有限层级的树结构,遍历顺序是先序的深度优先遍历。
参与tree的一个对象有一个parent,要么是null要么是另一个对象;有children,是有序的对象。
一个对象和另一个对象共享同一个parent时,则互为sibling。
2 Events
2.1 DOM Events的介绍
整个web平台上的event被发送给都对象都表示一个事件发生,比如网络活动或者用户交互。那些实现了EventTarget接口的对象可以使用addEventListener
添加事件监听器来监测事件。
事件监听器也可以通过传递相同参数使用removeEventListener()
被移除。另外事件监听器也可以通过传递一个取消信号(AbortSignal)并调用abort()
来取消。
尽管事件基本是作为用户交互或者完成某些任务的结果被用户代理触发的,但程序也可以使用合成事件自己来发送事件
// add an appropriate event listener
obj.addEventListener("cat", function(e) { process(e.detail) })
// create and dispatch the event
var event = new CustomEvent("cat", {"detail":{"hazcheeseburger":true}})
obj.dispatchEvent(event)
除了做信号,事件有时候也用来让程序控制在一个操作后下一步干什么,比如表单提交后,如果事件的preventDefault()
被调用,form表单被取消。
当一个事件被发送到一个参与tree的对象(比如element),它可以到达那个对象的的祖先。即要经历捕获和冒泡(比如事件的bubbles为true)两个阶段。
2.2 Event 接口
一个Event对象被称为事件(event),用来表示有些东西发生了,比如图片下载完成。
一个事件可以被用户行为触发,比如鼠标点击或者表示异步行为进度的api生成,也可以由程序触发,比如调用HTMLElement.click()
或者发出自定义事件EventTarget.dispatchEvent()
。
事件的类型有很多,它们由一些使用基于Event的其他接口,Event本身由一些属性和方法供所有事件使用
构造函数
new Event(typeArg[, eventInit])
可以用来创建一个事件实例,其中typeArg表示事件名,第二个表示相关配置(都是布尔值),包括
- bubbles 是否冒泡,默认false
- cancelable 是否可取消,默认false
- composed 是否会触发shadow root外的监听器,默认false
// create a look event that bubbles up and cannot be canceled
const evt = new Event("look", {"bubbles":true, "cancelable":false});
document.dispatchEvent(evt);
// event can be dispatched from any element, not only the document
myDiv.dispatchEvent(evt);
属性
- Event.bubbles 是否可冒泡
- Event.cancelBubble 读写是否可冒泡,是
Event.stopPropagation()
的历史别名 - Event.cancelable 是否可取消
- Event.composed 事件冒泡是否可以穿越shadow dom和常规dom的边界
- Event.currentTarget 指向当前经过的target
- Event.defaultPrevented 是否调用过
event.preventDefault()
取消事件 - Event.eventPhase 事件流所处阶段,0到4分别表示没事件、捕获阶段、在target、冒泡阶段
- Event.returnValue 读写是否被取消,由ie引入为了兼容所以引入标准但不建议使用
- Event.target 最初发送事件的target
- Event.timeStamp 事件创建时的时间戳
- Event.type 事件类型,不分大小写
- Event.isTrusted 表示是由浏览器触发的(true)还是脚本触发的(false)
方法
- Event.composedPath() 返回监听该事件的对象列表
- Event.preventDefault() 取消
- Event.stopImmediatePropagation() 取消监听同一事件的其他监听器
- Event.stopPropagation() 取消事件的进一步传播
2.3 Window接口上遗留的扩展
在window上有一个event属性表示当前的事件对象,不推荐使用
2.4 CustomEvent接口
基于Event接口,用来创建一个自定义事件,其中在Event接口上介绍过的内容参考对应部分,基本使用
// add an appropriate event listener
obj.addEventListener("cat", function(e) { process(e.detail) });
// create and dispatch the event
var event = new CustomEvent("cat", {
detail: {
hazcheeseburger: true
}
});
obj.dispatchEvent(event);
构造函数多了个detail配置
event = new CustomEvent(typeArg, customEventInit);
第二个参数多了个配置项detail,表示事件相关的值,默认null
2.5 EventTarget接口
被对象实现,用来接受事件,并可能有对应事件的监听器。
Element,Document和window是最常见的event target,其他对象也可以是,比如 XMLHttpRequest, AudioNode, AudioContext。
很多eventTarget也支持通过onevent设置事件处理器.
2.5.1 构造函数
var myEventTarget = new EventTarget();
可以通过extends创建新的target
class MyEventTarget extends EventTarget {
constructor(mySecret) {
super();
this._secret = mySecret;
}
get secret() { return this._secret; }
};
let myEventTarget = new MyEventTarget(5);
let value = myEventTarget.secret; // == 5
myEventTarget.addEventListener("foo", function(e) {
this._secret = e.detail;
});
let event = new CustomEvent("foo", { detail: 7 });
myEventTarget.dispatchEvent(event);
let newValue = myEventTarget.secret; // == 7
2.5.2 方法
2.5.2.1 EventTarget.addEventListener()
添加一个监听器,当特定事件传播到target时执行回调
1) 用法
target.addEventListener(type, listener, options);
target.addEventListener(type, listener, useCapture);
其中type是事件类型,listener是回调(参数是event),第三个参数有两种,当为对象时,包含
- capture 布尔值,表示是否在捕获阶段触发
- once 布尔值,是否只执行一次
- passive 布尔值,如果为true,表示回调不会调用
preventDefault()
,如果实际调用了用户代理会报warning
当是布尔值时表示是否在捕获阶段使用,为了兼容旧版浏览器保留该类型参数。
注意 这里的介绍参考的mdn,也是现在的实现,在最新的规范中引入了第三个参数的另一个属性signal,是AbortSignal类型,用来取消监听的回调,目前已在node.js实现。参考Feature: AbortSignal in addEventListener
2) 其他监听器注册方法
该方法是dom2引入的,注册监听器还有一种dom0引入的方式
// Passing a function reference — do not add '()' after it, which would call the function!
el.onclick = modifyText;
// Using a function expression
element.onclick = function() {
// ... function logic ...
};
这种方式会替换直接作为元素属性的事件监听,兼容性好
3) why addEventListener
- 允许注册多个监听器
- 利用第三个参数精细控制
- 不仅对Element有效,还对其他target有效
4)使用 passive 改善的滚屏性能
根据规范第三个参数中的passive默认false,但当使用touch或相关事件处理滚屏时会因需要先检查有没有取消默认行为才能确定要不要滚动,这种检查会使页面卡顿,因此一些浏览器会将passive默认设为true。
另外scroll事件是不能取消默认行为的,因此不受影响。
2.5.2.2 EventTarget.removeEventListener()
移除事件监听
target.removeEventListener(type, listener[, options]);
target.removeEventListener(type, listener[, useCapture]);
注意匹配被移除的监听时,第二个参数和第三个参数表示捕获的参数要一致。
2.5.2.3 EventTarget.dispatchEvent()
用来派发一个自定义事件,同步的,而浏览器派发的事件是通过eventloop异步实现的
const event = new Event('build');
// Listen for the event.
elem.addEventListener('build', function (e) { /* ... */ }, false);
// Dispatch the event.
elem.dispatchEvent(event);
3 Aborting ongoing activities
尽管promise没有提供内置的取消机制,但是很多使用它们的api需要取消的语义。AbortController 通过提供abort方法实现这个需求。对应api只需要接受一个 AbortSignal 参数,然后使用它的状态来决定下一步怎么做。比如假设有个 doAmazingness({ ... })
方法有这个需求
const controller = new AbortController();
const signal = controller.signal;
startSpinner();
doAmazingness({ ..., signal })
.then(result => ...)
.catch(err => {
if (err.name == 'AbortError') return;
showUserErrorMessage();
})
.then(() => stopSpinner());
// …
controller.abort();
为了能够取消,doAmazingness
可以这么实现
function doAmazingness({signal}) {
if (signal.aborted) {
return Promise.reject(new DOMException('Aborted', 'AbortError'));
}
return new Promise((resolve, reject) => {
// Begin doing amazingness, and call resolve(result) when done.
// But also, watch for signals:
signal.addEventListener('abort', () => {
// Stop doing amazingness, and:
reject(new DOMException('Aborted', 'AbortError'));
});
});
}
其中Fetch方法已经实现了这个功能
3.1 AbortController接口
可以通过实例化创建控制器,其有
- 一个实例属性signal,用来表示当前状态,并传递到相关api,它是一个AbortSignal实例,包含属性aborted和事件abort
- 一个实例方法abort,用来修改signal属性和触发abort事件
3.2 AbortSignal
就是signal属性的类型
4 Nodes
4.1 dom介绍
dom是指的一个用来访问和操作document的api,每个document都用一个node tree表示,tree中的一些node可以有children,但是还有一些只能是叶节点。
4.2 Node tree
Document, DocumentType, DocumentFragment, Element, Text, ProcessingInstruction, and Comment object或者叫nodes都参与了一个tree,这个tree就叫做node tree
4.2.1 document tree
是一个root是document的node tree
4.2.2 Shadow tree
是一个root是shadow root的node tree,和Web Components有关,我们要在§7
详细介绍。
4.2.3 Mutation algorithms
这里是插入节点时的算法
4.2.4 Mixin NonElementParentNode
提供了在父元素上调用getElementById
查找对应元素的功能
4.2.5 DocumentOrShadowRoot
用于定义documents and shadow roots共同使用的api
4.2.6 Mixin ParentNode
用于所有可以包含children的节点。
包含属性
- ParentNode.childElementCount chidren数量
- ParentNode.children 一个living HTMLCollection,包含所有子元素
- ParentNode.firstElementChild 第一个子元素
- ParentNode.lastElementChild 最后一个子元素
包含方法
- ParentNode.append(...nodesOrDOMStrings) 添加一系列节点或字符串到最后一个子结点后面,字符串相当于text节点
- ParentNode.prepend(...nodesOrDOMStrings) 插入一系列节点或字符串到第一个子结点之前
- ParentNode.querySelector()
- ParentNode.querySelectorAll()
- ParentNode.replaceChildren()
4.2.7 Mixin NonDocumentTypeChildNode
为那些有父节点的节点提供两个属性
- NonDocumentTypeChildNode.previousElementSibling 前一个同级元素
- NonDocumentTypeChildNode.nextElementSibling 后一个同级元素
4.2.8 Mixin ChildNode
为那些有父节点的节点提供几个方法
- ChildNode.remove()
- ChildNode.before()
- ChildNode.after()
- ChildNode.replaceWith()
4.2.9 Old-style collections: NodeList and HTMLCollection
一个collection表示一个node 列表,注意只是类数组
其中NodeList是节点的集合,比如Node.childNodes或者由document.querySelectorAll()返回,有一个length属性和几个方法
- NodeList.item() 按index返回
- NodeList.entries()
- NodeList.forEach()
- NodeList.keys()
- NodeList.values()
HTMLCollection只包含html元素,比如Node.children,包含一个length属性和两个方法
- HTMLCollection.item()
- HTMLCollection.namedItem() 利用节点id或者name属性获取
4.3 Mutation observers
mutation observer回调是一个微任务
4.3.1 Interface MutationObserver
提供了一个监视dom树变动的能力,按如下步骤使用 首先通过实例化MutationObserver创建一个监测器,当dom变化时触发回调,其中这个回调包含两个参数,一个是变化记录MutationRecord 组成的数组,一个是调用回调的检测器
const observer = new MutationObserver(callback)
调用监测器的observe方法配置监测的节点和监测的哪些变化,是个MutationObserverInit对象
mutationObserver.observe(target[, options])
可以通过takeRecords()方法将已经监测到但没执行回调的变化返回,返回值是MutationRecord数组
const mutationRecords = mutationObserver.takeRecords()
还可以调用disconnect()停止监测
mutationObserver.disconnect()
比如
// Select the node that will be observed for mutations
const targetNode = document.getElementById('some-id');
// Options for the observer (which mutations to observe)
const config = { attributes: true, childList: true, subtree: true };
// Callback function to execute when mutations are observed
const callback = function(mutationsList, observer) {
// Use traditional 'for loops' for IE 11
for(const mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
}
else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
// Later, you can stop observing
observer.disconnect();
4.3.2 Interface MutationRecord
代表一个单独的dom变化的对象,包含这些属性
4.3.3 MutationObserverInit dictionary
描述了一次监测的配置,最少childList, attributes, and/or characterData要为true,包括
- subtree 子树
- childList
- attributes
- attributeFilter
- attributeOldValue
- characterData
- characterDataOldValue
4.4 Interface Node
Node是一个抽象接口,实际使用的node包括Document, DocumentType, DocumentFragment, Element, Text, ProcessingInstruction, and Comment。Node是一种EventTarget 其中包含一些各种node公用的属性和方法
4.4.1 属性
- Node.nodeType 节点类型,用数字表示
- ELEMENT_NODE (1)
- ATTRIBUTE_NODE (2);
- TEXT_NODE (3)
- CDATA_SECTION_NODE (4)
- PROCESSING_INSTRUCTION_NODE (7)
- COMMENT_NODE (8)
- DOCUMENT_NODE (9)
- DOCUMENT_TYPE_NODE (10)
- DOCUMENT_FRAGMENT_NODE (11)
- Node.nodeName 用字符串表示节点名
- Element
- Attr
- Text
- CDATASection
- ProcessingInstruction
- Comment
- Document
- DocumentType
- DocumentFragment
- Node.baseURI
- Node.childNodes 一个包含所有子节点的NodeList
- Node.firstChild
- Node.isConnected 是否和一个上下文连接
- Node.lastChild
- Node.nextSibling
- Node.nodeValue
- Node.ownerDocument 返回对应的document
- Node.parentNode
- Node.parentElement
- Node.previousSibling
- Node.textContent
4.4.2 方法
- Node.appendChild(childNode)
- Node.cloneNode()
- Node.compareDocumentPosition()
- Node.contains()
- Node.getBoxQuads()
- Node.getRootNode()
- Node.hasChildNodes()
- Node.insertBefore()
- Node.isDefaultNamespace()
- Node.isEqualNode()
- Node.isSameNode()
- Node.lookupPrefix()
- Node.lookupNamespaceURI()
- Node.normalize()
- Node.removeChild()
- Node.replaceChild()
4.5 Interface Document
Document表示一个dom tree,为document提供了多种属性和方法
4.5.1 属性
继承和实现了 Node and EventTarget、ParentNode上的属性
- Document.body 返回当前文档的<body> or <frameset>节点
- Document.characterSet 字符集
- Document.compatMode 是严格模式还是混杂模式
- Document.contentType minetype
- Document.doctype 返回当前文档的Document Type Definition (DTD)
- Document.documentElement html元素
- Document.documentURI
- Document.embeds
- Document.fonts
- Document.forms
- Document.head
- Document.hidden
- Document.images
- Document.implementation
- Document.lastStyleSheetSet
- Document.links
- Document.pictureInPictureEnabled 画中画是否可用
- Document.featurePolicy
- Document.preferredStyleSheetSet
- Document.scripts
- Document.scrollingElement
- Document.selectedStyleSheetSet
- Document.styleSheetSets
- Document.timeline
- Document.visibilityState 可见状态
为HTMLDocument扩展的
- Document.cookie
- Document.defaultView
- Document.designMode
- Document.dir
- Document.domain
- Document.lastModified
- Document.location
- Document.readyState
- Document.referrer
- Document.title
- Document.URL
从DocumentOrShadowRoot定义的
- DocumentOrShadowRoot.activeElement
- Document.fullscreenElement
- DocumentOrShadowRoot.pointerLockElement
- DocumentOrShadowRoot.styleSheets
4.5.2 方法
继承和实现了 Node and EventTarget、ParentNode上的方法
- Document.adoptNode()
- Document.createAttribute()
- Document.createAttributeNS()
- Document.createCDATASection()
- Document.createComment()
- Document.createDocumentFragment()
- Document.createElement()
- Document.createElementNS()
- Document.createEvent()
- Document.createNodeIterator()
- Document.createProcessingInstruction()
- Document.createRange()
- Document.createTextNode()
- Document.createTouchList()
- Document.createTreeWalker()
- Document.enableStyleSheetsForSet()
- Document.exitPictureInPicture()
- Document.getElementsByClassName()
- Document.getElementsByTagName()
- Document.getElementsByTagNameNS()
- Document.hasStorageAccess()
- Document.importNode()
- Document.requestStorageAccess()
为HTMLDocument扩展的
- Document.close()
- Document.execCommand()
- Document.getElementsByName()
- Document.hasFocus()
- Document.open()
- Document.queryCommandEnabled()
- Document.queryCommandIndeterm()
- Document.queryCommandState()
- Document.queryCommandSupported()
- Document.queryCommandValue()
- Document.write()
- Document.writeln()
在DocumentOrShadowRoot定义的
- DocumentOrShadowRoot.caretPositionFromPoint()
- DocumentOrShadowRoot.elementFromPoint()
- DocumentOrShadowRoot.elementsFromPoint()
- DocumentOrShadowRoot.getSelection()
4.5.3 事件
对应事件参考mdn
4.6 Interface DocumentType
表示一个包含doctype的节点
4.7 Interface DocumentFragment
文档片段,可以用来批量插入节点,继承了Node,并实现了ParentNode
const list = document.querySelector('#list');
const fruits = ['Apple', 'Orange', 'Banana', 'Melon'];
const fragment = document.createDocumentFragment();
fruits.forEach(fruit => {
const li = document.createElement('li');
li.innerHTML = fruit;
fragment.appendChild(li);
});
list.appendChild(fragment);
4.8 Interface ShadowRoot
参考web component
4.9 Interface Element
Element是所有元素对象的基类,为他们提供了很多属性和方法,其子类HTMLElement对象是html元素的基类。 继承了EventTarget,Node,实现了ParentNode, ChildNode, NonDocumentTypeChildNode
4.9.1 NamedNodeMap
表示Attr对象的集合,没有特定顺序
4.9.2 Attr
用一个对象表示dom元素的一个属性,可以使用Element.getAttributeNode()
获得,用Element.getAttribute()
获得的是字符串。
4.9.3 属性
- Element.attributes 返回一个所有属性组成的NamedNodeMap
- Element.classList class属性组成的列表
- Element.className
- Element.clientHeight 可以计算为CSS height + CSS padding - height of horizontal scrollbar (if present),如果在根元素上使用,则会返回视口高度(不包括滚动条),会四舍五入为整数,精确数据参考
element.getBoundingClientRect()
- Element.clientWidth
- Element.clientLeft left border的宽度,如果左边有滚动条也包括。
display:inline
的元素始终为0 - Element.clientTop
- Element.scrollHeight 当没有滚动条时所占有的高度,包括padding,::before和::after,不包括margin和border
- Element.scrollWidth
- Element.scrollLeft 利用滚动条向左边滚动的距离
- Element.scrollTop
- Element.computedName
- Element.computedRole
- Element.id
- Element.innerHTML
- Element.localName
- Element.namespaceURI
- Element.outerHTML
- Element.part
- Element.prefix
- Element.shadowRoot
- Element.tagName
4.9.4 方法
- Element.attachShadow()
- Element.animate()
- Element.closest()
- Element.computedStyleMap()
- EventTarget.dispatchEvent()
- Element.getAnimations()
- Element.getAttribute()
- Element.getAttributeNames()
- Element.getAttributeNS()
- Element.getBoundingClientRect() 返回一个DOMRect对象,包含元素大小及相对视口的距离,其中大小包括padding和border,如果是标准盒子则为width or height property of the element + padding + border-width,如果是
box-sizing: border-box
则等于width or height
- Element.getClientRects() 返回一个DOMRect对象集合,一般只有一个,但是多行的行内元素有多个对象
- Element.getElementsByClassName()
- Element.getElementsByTagName()
- Element.getElementsByTagNameNS()
- Element.hasAttribute()
- Element.hasAttributeNS()
- Element.hasAttributes()
- Element.hasPointerCapture()
- Element.insertAdjacentElement()
- Element.insertAdjacentHTML()
- Element.insertAdjacentText()
- Element.matches()
- Element.pseudo()
- Element.querySelector()
- Element.querySelectorAll()
- Element.releasePointerCapture()
- Element.removeAttribute()
- Element.removeAttributeNS()
- Element.requestFullscreen()
- Element.requestPointerLock()
- Element.scroll()
- Element.scrollBy()
- Element.scrollIntoView() 移动到视口
- Element.scrollTo()
- Element.setAttribute()
- Element.setAttributeNS()
- Element.setPointerCapture()
- Element.toggleAttribute() 切换属性
4.10 Interface CharacterData
表示字符的抽象接口,被Text、Comment 或 ProcessingInstruction实现。 继承了Node,实现了ChildNode和NonDocumentTypeChildNode
4.10.1 属性
- CharacterData.data
- CharacterData.length
4.10.2 方法
- CharacterData.appendData()
- CharacterData.deleteData()
- CharacterData.insertData()
- CharacterData.replaceData()
- CharacterData.substringData()
4.11 Interface Text
表示 Element or Attr的文本内容,继承CharacterData
4.11.1 属性
- Text.wholeText
- Text.assignedSlot
4.11.2 方法
- Text.splitText
4.12 Interface CDATASection
CDATA 片段,在xml用的
4.13 Interface ProcessingInstruction
在xml用的
4.14 Interface Comment
注释
5 Ranges
StaticRange and Range都表示一个node tree的一部分。
可以结合Selection做批量选择、修改等,参考mdn
6 Traversal
NodeIterator、NodeFilter和TreeWalker可以用来迭代和筛选node tree
7 Sets
7.1 DOMTokenList
这里主要介绍了 DOMTokenList一个接口,表示一组用空格分隔的标记,比如Element.classList, HTMLLinkElement.relList, HTMLAnchorElement.relList, HTMLAreaElement.relList, HTMLIframeElement.sandbox, or HTMLOutputElement.htmlFor的返回值,可以像数组一样从下标0开始处理,对大小写敏感
7.1.1 属性
- DOMTokenList.length
- DOMTokenList.value
7.1.2 方法
- DOMTokenList.item(index)
- DOMTokenList.contains(token)
- DOMTokenList.add(token1[, token2[, ...tokenN]])
- DOMTokenList.remove(token1[, token2[, ...tokenN]])
- DOMTokenList.replace(oldToken, newToken)
- DOMTokenList.supports(token)
- DOMTokenList.toggle(token [, force])
- DOMTokenList.entries()
- DOMTokenList.forEach(callback [, thisArg])
- DOMTokenList.keys()
- DOMTokenList.values()
7.1.3 用例
比如用来添加className
<p class="a b c"></p>
let para = document.querySelector("p");
let classes = para.classList;
para.classList.add("d");
para.textContent = `paragraph classList is "${classes}"`;
8 Web Components
本部分是dom规范和html规范相关内容的整合,是我自己加出来的,参考mdn Web Components是一整套技术方案用来创建可复用的自定义元素,对应技术包括
- 自定义元素,有一系列api可以使我们自定义元素和它们的表现,参考Using custom elements
- shadow dom,绑定一个封装后的shadow dom tree到一个元素,可以将其与主document tree分开处理,参考Using shadow DOM
- html模板,<template> and <slot> 元素可使我们编写模板并作为自定义元素的基础重复使用,参考Using templates and slots
实现web components的基本步骤包括
- 创建一个class来指定web组件的功能
- 使用
CustomElementRegistry.define()
注册新的自定义元素,并向其传递自定义元素的名称、指定元素功能的class、以及可选的所继承的元素 - 如果需要的话,使用
Element.attachShadow()
方法绑定一个shadow dom到这个自定义元素,然后想使用常规dom一样添加子元素、事件监听器等 - 如果需要,使用两个对应元素定义模板,再次使用常规dom的方法克隆模板并将其绑定的shadow dom中
- 在页面任何地方使用自定义元素,就像常规html元素一样
本期撒花完结
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!