找回密码
 立即注册
首页 业界区 业界 探索Web Components

探索Web Components

涅牵 2025-6-6 15:18:02
title: 探索Web Components
date: 2024/6/16
updated: 2024/6/16
author:  cmdragon
excerpt:
这篇文章介绍了Web Components技术,它允许开发者创建可复用、封装良好的自定义HTML元素,并直接在浏览器中运行,无需依赖外部库。通过组合HTML模板、Shadow DOM、自定义元素和HTML imports,Web Components增强了原生DOM的功能,提高了组件化开发的封装性和可维护性,同时支持组件的生命周期管理和高级设计模式,有利于提升网页应用的性能和开发效率。
categories:

  • 前端开发
tags:

  • Web Components
  • 原生DOM
  • 封装性
  • 组件化
  • 生命周期
  • 高级设计
  • 性能优化
1.png

2.jpeg

扫码关注或者微信搜一搜:编程智域 前端至全栈交流与成长
第1章:引言
Web Components的起源与发展
Web Components是一种基于Web标准的新兴技术,旨在解决Web应用程序开发中的可重用组件化问题。Web
Components的核心思想是,将HTML、CSS和JavaScript结合起来,实现可重用、可组合和可封装的组件。
Web Components的起源可以追溯到2011年,由W3C(万维网联盟)提出的一个名为Web Components Specifications(Web
Components规范)的项目。该项目包括四个主要模块:

  • Templates and Slots(模板和插槽):提供一种在HTML中声明模板的方式,并在组件中使用插槽来实现内容分发。
  • Shadow DOM(影子DOM):提供一种在组件内部创建独立的DOM树,与外部DOM树隔离开来,实现样式和内容的封装。
  • Custom Elements(自定义元素):提供一种在HTML中定义和注册新元素的方式,扩展HTML标准元素集。
  • Decorators(装饰器):提供一种在组件生命周期中添加额外功能的方式,如属性观察器、事件监听器和生命周期回调。
为什么选择Web Components
Web Components具有以下优点:

  • 可重用性:组件可以在不同的项目中重用,提高开发效率和一致性。
  • 可组合性:组件可以嵌套和组合,构建更加复杂的UI。
  • 可封装性:组件可以在内部实现细节上进行隔离,提高可维护性和可测试性。
  • 与现有Web技术的兼容性:Web Components基于Web标准,与HTML、CSS和JavaScript高度兼容。
第2章:基础知识
Web Components概述
Web Components是一系列不同的技术,允许你创建可重用的自定义元素,并且包含了自定义的样式和行为。这些自定义元素可以像标准HTML元素一样使用,并且可以在任何地方重用。Web
Components主要由以下三个技术组成:

  • Custom Elements(自定义元素):允许你定义新的HTML元素,这些元素可以包含自己的HTML结构、CSS样式和JavaScript行为。
  • Shadow DOM(影子DOM):提供了一种封装方式,使得自定义元素可以拥有自己的DOM树,与页面的其他部分隔离开来,防止样式冲突。
  • HTML Templates(HTML模板):提供了一种声明性的方式来定义HTML结构,可以在运行时插入到文档中。
  • HTML Imports(HTML导入):允许你导入HTML文档作为模块,虽然这个特性已经被废弃,但它的理念被其他模块化方案所继承。
HTML、CSS和JavaScript基础知识
在深入Web Components之前,你需要具备一定的HTML、CSS和JavaScript基础知识。以下是这些技术的简要概述:

  • HTML:超文本标记语言,用于创建网页的结构和内容。
  • CSS:层叠样式表,用于设置网页元素的样式,如颜色、字体和布局。
  • JavaScript:一种编程语言,用于实现网页的交互性和动态内容。
Shadow DOM和模板模式
Shadow DOM
Shadow DOM是Web
Components的核心技术之一,它允许你将一个隐藏的、独立的DOM树附加到一个元素上。这个DOM树被称为“影子DOM”,它与主DOM树(即页面上的其他元素)是隔离的。这意味着影子DOM内的样式和行为不会影响到页面上的其他元素,反之亦然。这种隔离性使得Web
Components能够封装自己的样式和行为,而不必担心与其他元素的冲突。
模板模式
模板模式是Web
Components中用于创建自定义元素的一种方式。它允许你定义一个HTML模板,这个模板包含了自定义元素的HTML结构。然后,你可以使用JavaScript来实例化这个模板,并将其附加到DOM中。模板模式通常与Shadow
DOM结合使用,以实现自定义元素的封装和样式隔离。
通过结合使用Shadow DOM和模板模式,你可以创建出功能强大、可重用的Web Components,这些组件可以在不同的项目中重复使用,并且能够保持自己的样式和行为。
第3章:基础组件开发
template元素和slot的使用
template元素在Web Components中扮演了重要角色,它允许你定义组件的结构和内容。template
标签内可以包含HTML结构,这些结构会被复制到每个组件实例中。slot
元素则用于定义组件内部可以接收内容的地方,外部可以将内容插入到这些slot中,实现了组件的可扩展性。标准中文电码查询 | 一个覆盖广泛主题工具的高效在线平台 (cmdragon.cn)
例如:
  1. <template>
  2.   
  3. <head>
  4.   <link rel="import" href="my-module.html">
  5. </head>
  6. <body>
  7.   <my-element></my-element>
  8. </body><slot name="header">Default Header</slot>
  9. <head>
  10.   <link rel="import" href="my-module.html">
  11. </head>
  12. <body>
  13.   <my-element></my-element>
  14. </body><p>Content goes here</p>
  15. <head>
  16.   <link rel="import" href="my-module.html">
  17. </head>
  18. <body>
  19.   <my-element></my-element>
  20. </body><slot name="footer">Default Footer</slot>
  21.   
  22. </template>
复制代码
在这个例子中,header和footer是slot,外部可以传递自定义内容替换它们。
custom-element定义与注册
custom-element是Web Components的核心,用于创建自定义的HTML元素。定义一个custom-element通常需要以下步骤:

  • 使用标签定义元素:
    1. [/code]
    2. [*]实现connectedCallback和可能的其他生命周期方法,如disconnectedCallback、attributeChangedCallback等:
    3. [code]class MyComponent extends HTMLElement {
    4.   constructor() {
    5. <head>
    6.   <link rel="import" href="my-module.html">
    7. </head>
    8. <body>
    9.   <my-element></my-element>
    10. </body>super();
    11. <head>
    12.   <link rel="import" href="my-module.html">
    13. </head>
    14. <body>
    15.   <my-element></my-element>
    16. </body>this.attachShadow({ mode: 'open' });
    17.   }
    18.   connectedCallback() {
    19. <head>
    20.   <link rel="import" href="my-module.html">
    21. </head>
    22. <body>
    23.   <my-element></my-element>
    24. </body>// 在这里添加组件的初始化代码
    25.   }
    26.   // 其他生命周期方法...
    27. }
    28. customElements.define("my-component", MyComponent);
    复制代码
  • 在connectedCallback中,将自定义元素的shadowRoot(暗影根)添加到模板中:
    1. connectedCallback() {
    2.   this.shadowRoot.appendChild(this.templateContent);
    3. }
    4. // 假设templateContent是template元素的内容
    5. const templateContent = document.querySelector('template');
    复制代码
style和link元素在组件中的应用

style元素用于定义组件的样式,通常放在标签内部,或作为[/code]在组件内部,可以使用this.shadowRoot来访问和操作样式。例如,添加样式到组件的暗影根:
  1. class MyComponent extends HTMLElement {
  2.   connectedCallback() {
  3. <head>
  4.   <link rel="import" href="my-module.html">
  5. </head>
  6. <body>
  7.   <my-element></my-element>
  8. </body>this.shadowRoot.appendChild(this.styleElement);
  9.   }
  10.   // ...
  11.   constructor() {
  12. <head>
  13.   <link rel="import" href="my-module.html">
  14. </head>
  15. <body>
  16.   <my-element></my-element>
  17. </body>super();
  18. <head>
  19.   <link rel="import" href="my-module.html">
  20. </head>
  21. <body>
  22.   <my-element></my-element>
  23. </body>this.styleElement = document.createElement('style');
  24. <head>
  25.   <link rel="import" href="my-module.html">
  26. </head>
  27. <body>
  28.   <my-element></my-element>
  29. </body>this.styleElement.textContent = `
  30. <head>
  31.   <link rel="import" href="my-module.html">
  32. </head>
  33. <body>
  34.   <my-element></my-element>
  35. </body>  /* ... */
  36. <head>
  37.   <link rel="import" href="my-module.html">
  38. </head>
  39. <body>
  40.   <my-element></my-element>
  41. </body>`;
  42.   }
  43. }
复制代码
这样,外部样式可以影响到组件的渲染,同时保持了组件的封装性。
第4章:原生组件与Web Components的对比
原生DOM元素的特性
原生DOM元素是HTML5中直接提供的,它们具有以下特性:

  • 简单易用:直接操作DOM元素,API直观,易于学习和使用。
  • 广泛支持:所有现代浏览器都内置了对DOM的支持。
  • 性能:对于简单的操作,DOM操作通常很快,但复杂操作可能导致性能问题,特别是当涉及到大量元素时。
  • 事件处理:DOM提供了丰富的事件模型,可以直接监听和响应元素的事件。
  • 样式控制:可以直接通过style属性或者CSS类来控制元素的样式。
Web Components的优势和局限性
优势

  • 封装性:Web Components提供了一种将HTML、CSS和JavaScript封装在一起的方式,提高了代码的复用性和维护性。
  • 组件化:组件可以独立于页面,可以被多个页面复用,减少了代码冗余。
  • 自定义元素:可以创建自定义的HTML元素,扩展HTML元素库。
  • 数据绑定:通过和,可以实现数据驱动的组件结构。
局限性

  • 学习曲线:Web Components的API和概念可能对初学者来说较难理解和掌握。
  • 浏览器支持:虽然大部分现代浏览器支持,但一些旧版本浏览器可能不支持,需要使用polyfills或polymer库来弥补。
  • 性能:对于复杂的组件,如果处理不当,可能会有性能问题,尤其是在处理大量数据时。
  • 工具链:虽然有工具如Web Components Workbox等来优化,但整体工具链相比React、Vue等库可能不够成熟。
兼容性问题与解决方案

  • 浏览器兼容性:使用@webcomponents/webcomponentsjs库或者polyfills(如custom-elements-es5-adapter
    )来提供向后兼容性,确保在不支持Web Components的浏览器中运行。
  • polyfills:对于一些新特性(如Shadow DOM、HTML Templates等),可以使用polyfills来提供支持。
  • Babel和TypeScript:使用这些工具可以将新特性转换为旧版本浏览器可以理解的代码。
  • 测试:确保在各种浏览器和版本上进行充分的测试,确保组件的兼容性。
总的来说,Web Components提供了一种更现代、更模块化的开发方式,但开发者需要在兼容性、学习成本和工具成熟度之间权衡。
第5章:自定义元素API
自定义元素API:生命周期方法
在Web Components中,自定义元素有以下几个关键的生命周期方法:

  • createdCallback: 当元素被创建(但可能尚未插入到文档中)时调用。这是初始化元素内部状态和处理数据的好时机。
  1. class MyCustomElement extends HTMLElement {
  2.   createdCallback() {
  3. <head>
  4.   <link rel="import" href="my-module.html">
  5. </head>
  6. <body>
  7.   <my-element></my-element>
  8. </body>// 初始化元素内部状态
  9.   }
  10. }
复制代码

  • attachedCallback: 当元素被插入到文档中时调用。这时可以绑定事件和处理DOM操作。
  1. attachedCallback() {
  2.   this.addEventListener('click', this.handleClick);
  3. }
复制代码

  • detachedCallback: 当元素从文档中移除时调用,可以在这里清理资源。
  • attributeChangedCallback: 当元素的属性被修改时调用,可以更新内部状态。
  1. attributeChangedCallback(name, oldValue, newValue) {
  2.   // 更新属性值
  3. }
复制代码

  • connectedCallback: 在元素被连接到DOM树中(可能是通过插入)时调用。
属性绑定和事件处理

  • 属性绑定:可以使用元素的和来实现数据绑定,或者使用this.set方法来设置和监听属性。
  1. this.set('myProperty', newValue);
复制代码

  • 事件处理:通过addEventListener方法添加事件监听器,事件处理函数通常在this上下文中。
  1. addEventListener('click', (event) => {
  2.   // 处理点击事件
  3. });
复制代码
与外部数据交互

  • 数据绑定:可以使用和来绑定外部数据,或者通过@property装饰器声明响应式属性。
  1. @property({ type: String, reflect: true })
  2. myData;
复制代码

  • 事件通信:自定义元素可以通过customEvent来触发自定义事件,外部可以通过addEventListener监听这些事件。
  1. this.dispatchEvent(new CustomEvent('myCustomEvent', { detail: data }));
复制代码

  • 数据交互API:使用fetch、XMLHttpRequest或Web API(如localStorage、IndexedDB)来获取和存储数据。
第6章:高级组件设计
高阶组件(Higher-Order Components, HOCs)

高阶组件(HOCs)是React中用于重用组件逻辑的高级技术。HOC是一个函数,它接受一个组件并返回一个新的组件。HOC可以用来封装组件,使其更易于重用和测试。
示例代码:
  1. import React from 'react';
  2. // 定义一个HOC
  3. function withSubscription(WrappedComponent, selectData) {
  4.   // ...并返回一个新组件...
  5.   return class extends React.Component {
  6. <head>
  7.   <link rel="import" href="my-module.html">
  8. </head>
  9. <body>
  10.   <my-element></my-element>
  11. </body>constructor(props) {
  12. <head>
  13.   <link rel="import" href="my-module.html">
  14. </head>
  15. <body>
  16.   <my-element></my-element>
  17. </body>  super(props);
  18. <head>
  19.   <link rel="import" href="my-module.html">
  20. </head>
  21. <body>
  22.   <my-element></my-element>
  23. </body>  this.handleChange = this.handleChange.bind(this);
  24. <head>
  25.   <link rel="import" href="my-module.html">
  26. </head>
  27. <body>
  28.   <my-element></my-element>
  29. </body>  this.state = {
  30. <head>
  31.   <link rel="import" href="my-module.html">
  32. </head>
  33. <body>
  34.   <my-element></my-element>
  35. </body><head>
  36.   <link rel="import" href="my-module.html">
  37. </head>
  38. <body>
  39.   <my-element></my-element>
  40. </body>data: selectData(DataSource, props)
  41. <head>
  42.   <link rel="import" href="my-module.html">
  43. </head>
  44. <body>
  45.   <my-element></my-element>
  46. </body>  };
  47. <head>
  48.   <link rel="import" href="my-module.html">
  49. </head>
  50. <body>
  51.   <my-element></my-element>
  52. </body>}
  53. <head>
  54.   <link rel="import" href="my-module.html">
  55. </head>
  56. <body>
  57.   <my-element></my-element>
  58. </body>componentDidMount() {
  59. <head>
  60.   <link rel="import" href="my-module.html">
  61. </head>
  62. <body>
  63.   <my-element></my-element>
  64. </body>  // ...那数据源...并订阅变化...
  65. <head>
  66.   <link rel="import" href="my-module.html">
  67. </head>
  68. <body>
  69.   <my-element></my-element>
  70. </body>  DataSource.addChangeListener(this.handleChange);
  71. <head>
  72.   <link rel="import" href="my-module.html">
  73. </head>
  74. <body>
  75.   <my-element></my-element>
  76. </body>}
  77. <head>
  78.   <link rel="import" href="my-module.html">
  79. </head>
  80. <body>
  81.   <my-element></my-element>
  82. </body>componentWillUnmount() {
  83. <head>
  84.   <link rel="import" href="my-module.html">
  85. </head>
  86. <body>
  87.   <my-element></my-element>
  88. </body>  DataSource.removeChangeListener(this.handleChange);
  89. <head>
  90.   <link rel="import" href="my-module.html">
  91. </head>
  92. <body>
  93.   <my-element></my-element>
  94. </body>}
  95. <head>
  96.   <link rel="import" href="my-module.html">
  97. </head>
  98. <body>
  99.   <my-element></my-element>
  100. </body>handleChange() {
  101. <head>
  102.   <link rel="import" href="my-module.html">
  103. </head>
  104. <body>
  105.   <my-element></my-element>
  106. </body>  this.setState({
  107. <head>
  108.   <link rel="import" href="my-module.html">
  109. </head>
  110. <body>
  111.   <my-element></my-element>
  112. </body><head>
  113.   <link rel="import" href="my-module.html">
  114. </head>
  115. <body>
  116.   <my-element></my-element>
  117. </body>data: selectData(DataSource, this.props)
  118. <head>
  119.   <link rel="import" href="my-module.html">
  120. </head>
  121. <body>
  122.   <my-element></my-element>
  123. </body>  });
  124. <head>
  125.   <link rel="import" href="my-module.html">
  126. </head>
  127. <body>
  128.   <my-element></my-element>
  129. </body>}
  130. <head>
  131.   <link rel="import" href="my-module.html">
  132. </head>
  133. <body>
  134.   <my-element></my-element>
  135. </body>render() {
  136. <head>
  137.   <link rel="import" href="my-module.html">
  138. </head>
  139. <body>
  140.   <my-element></my-element>
  141. </body>  // ...并将新的数据传递给被包装的组件!
  142. <head>
  143.   <link rel="import" href="my-module.html">
  144. </head>
  145. <body>
  146.   <my-element></my-element>
  147. </body>  return <WrappedComponent data={this.state.data} {...this.props} />;
  148. <head>
  149.   <link rel="import" href="my-module.html">
  150. </head>
  151. <body>
  152.   <my-element></my-element>
  153. </body>}
  154.   }
  155. }
复制代码
集成状态管理(如Redux或Vue.js)

状态管理库(如Redux)可以帮助管理大型应用程序的状态,使其更易于维护和测试。Redux是一个可预测的状态容器,用于JavaScript应用。
示例代码:
  1. import { createStore } from 'redux';
  2. // 创建一个reducer
  3. function todos(state = [], action) {
  4.   switch (action.type) {
  5. <head>
  6.   <link rel="import" href="my-module.html">
  7. </head>
  8. <body>
  9.   <my-element></my-element>
  10. </body>case 'ADD_TODO':
  11. <head>
  12.   <link rel="import" href="my-module.html">
  13. </head>
  14. <body>
  15.   <my-element></my-element>
  16. </body>  return state.concat([action.text]);
  17. <head>
  18.   <link rel="import" href="my-module.html">
  19. </head>
  20. <body>
  21.   <my-element></my-element>
  22. </body>default:
  23. <head>
  24.   <link rel="import" href="my-module.html">
  25. </head>
  26. <body>
  27.   <my-element></my-element>
  28. </body>  return state;
  29.   }
  30. }
  31. // 创建store
  32. let store = createStore(todos);
  33. // 添加一个todo
  34. store.dispatch({
  35.   type: 'ADD_TODO',
  36.   text: 'Read the docs'
  37. });
  38. // 打印state
  39. console.log(store.getState());
复制代码
使用Shadow DOM实现封装和样式隔离

Shadow DOM提供了一种封装Web组件的方式,可以隔离样式和行为,防止与其他组件冲突。
示例代码:
  1. class MyElement extends HTMLElement {
  2.   constructor() {
  3. <head>
  4.   <link rel="import" href="my-module.html">
  5. </head>
  6. <body>
  7.   <my-element></my-element>
  8. </body>super();
  9. <head>
  10.   <link rel="import" href="my-module.html">
  11. </head>
  12. <body>
  13.   <my-element></my-element>
  14. </body>// 创建一个shadow root
  15. <head>
  16.   <link rel="import" href="my-module.html">
  17. </head>
  18. <body>
  19.   <my-element></my-element>
  20. </body>this.attachShadow({ mode: 'open' });
  21. <head>
  22.   <link rel="import" href="my-module.html">
  23. </head>
  24. <body>
  25.   <my-element></my-element>
  26. </body>// 添加一些内容
  27. <head>
  28.   <link rel="import" href="my-module.html">
  29. </head>
  30. <body>
  31.   <my-element></my-element>
  32. </body>this.shadowRoot.innerHTML = `<h1>Hello, World!</h1>`;
  33.   }
  34. }
  35. // 定义custom element
  36. customElements.define('my-element', MyElement);
复制代码
通过以上方法,可以设计出更高级、更易于维护和测试的组件
第7章:复用与模块化
元素的rel="import"和模块导入

HTML Imports是HTML和JavaScript的一种模块格式,允许在HTML文档中导入外部资源。可以使用元素的rel="import"属性来导入模块。
示例代码:
  1. <head>
  2.   <link rel="import" href="my-module.html">
  3. </head>
  4. <body>
  5.   <my-element></my-element>
  6. </body>
复制代码
Web Components库和框架(如Polymer、lit-element等)

Web Components是一种模块化的方法,用于构建可重用和可组合的UI组件。可以使用各种Web Components库和框架来简化开发过程。
示例代码:
  1. // Polymerimport { PolymerElement, html } from '@polymer/polymer';class MyElement extends PolymerElement {  static get template() {<head>
  2.   <link rel="import" href="my-module.html">
  3. </head>
  4. <body>
  5.   <my-element></my-element>
  6. </body>return html`<head>
  7.   <link rel="import" href="my-module.html">
  8. </head>
  9. <body>
  10.   <my-element></my-element>
  11. </body>  Hello, World!<head>
  12.   <link rel="import" href="my-module.html">
  13. </head>
  14. <body>
  15.   <my-element></my-element>
  16. </body>`;  }}customElements.define('my-element', MyElement);// lit-elementimport { LitElement, html } from 'lit-element';class MyElement extends LitElement {  render() {<head>
  17.   <link rel="import" href="my-module.html">
  18. </head>
  19. <body>
  20.   <my-element></my-element>
  21. </body>return html`<head>
  22.   <link rel="import" href="my-module.html">
  23. </head>
  24. <body>
  25.   <my-element></my-element>
  26. </body>  Hello, World!<head>
  27.   <link rel="import" href="my-module.html">
  28. </head>
  29. <body>
  30.   <my-element></my-element>
  31. </body>`;  }}customElements.define('my-element', MyElement);
复制代码
Web Components的模块化最佳实践

为了确保Web Components的可重用性和可维护性,需要遵循一些最佳实践。

  • 组件应该是可重用的:组件应该是独立的、可重用的,并且不应该依赖于特定的应用程序状态。
  • 组件应该是可组合的:组件应该可以与其他组件组合在一起,以创建更大的组件。
  • 组件应该是可测试的:组件应该是可测试的,可以通过单元测试和集成测试来验证其功能。
  • 组件应该是可维护的:组件应该易于理解和维护,并且应该遵循一致的编码风格和架构。
  • 组件应该是可访问的:组件应该遵循可访问性的最佳实践,以确保所有用户都可以使用它们。
通过遵循这些最佳实践,可以确保Web Components的可重用性和可维护性,并使得应用程序更加模块化和可扩展。
第8章:现代Web开发中的Web Components
Web Components与现代Web框架的集成

现代Web框架(如Angular、React、Vue)虽然各自有其独特的组件系统,但它们也支持与Web Components的集成,以利用Web
Components的可重用性和模块化优势。以下是一些集成方式:

  • Angular: Angular通过ng-content和@Input、@Output等特性,可以方便地使用Web Components。可以将Web
    Components作为Angular组件的一部分,或者在Angular应用中作为自定义元素使用。
  • React: React通过forwardRef和useRef等API,可以与自定义元素(Custom Elements)配合使用。通过React.forwardRef将Web
    Components包装成React组件,可以在React应用中直接使用。
  • Vue: Vue通过v-bind、v-on等指令,可以与自定义元素或使用Vue.extend创建的组件一起工作。Vue的Composition API也可以与Web
    Components无缝集成。
Web Components在服务端渲染(SSR)中的应用

服务端渲染(SSR)是现代Web开发中的一种策略,它允许在服务器端生成完整的HTML,然后发送到客户端,提高首屏加载速度。对于Web
Components,SSR需要特别处理,因为它们依赖于浏览器环境来创建和渲染。

  • 使用服务器端库:一些库(如@webcomponents/web-component-server)提供了服务端渲染Web
    Components的能力,它允许在服务器上创建虚拟DOM,然后在客户端上进行渲染。
  • 预渲染:在客户端首次渲染时,可以将组件的HTML结构和数据一起发送到客户端,然后在客户端通过JavaScript初始化这些组件。
  • 状态管理:确保在服务器端和客户端之间同步状态,因为Web Components可能依赖于组件内部的状态。
在SSR中使用Web Components时,需要考虑到浏览器环境和服务器环境的差异,确保组件可以在两种环境下正确工作。同时,由于Web
Components的模块化特性,它们通常更容易适应SSR的场景。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

2025-11-10 13:18:37

举报

喜欢鼓捣这些软件,现在用得少,谢谢分享!
昨天 01:32

举报

喜欢鼓捣这些软件,现在用得少,谢谢分享!
您需要登录后才可以回帖 登录 | 立即注册