请选择 进入手机版 | 继续访问电脑版

我从 Vuejs 中学到了什么

[复制链接]
听见深浅 发表于 2021-1-2 18:58:53 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题
框架设计远没有各人想的那么简单,并不是说只把功能开辟完成,能用就算完事儿了,这内里照旧有许多学问的。比如说,我们的框架应该给用户提供哪些构建产物?产物的模块格式如何?当用户没有以预期的方式使用框架时是否应该打印符合的告诫信息从而提升更好的开辟体验,让用户快速定位问题?开辟版本的构建和生产版本的构建有何区别?热跟新(HMR:Hot Module Replacement)需要框架层面的支持才行,我们是否也应该思量?再有就是当你的框架提供了多个功能,如果用户只需要此中几个功能,那么用户是否可以选择关闭其他功能从而淘汰资源的打包体积?所有以上这些问题我们都会在本节内容举行讨论。
本节内容需要各人对常用的模块打包工具有一定的使用履历,尤其是 rollup.js 以及 webpack。如果你只用过或相识过此中一个也没关系,因为它们许多概念实在是雷同的。如果你没有使用任何模块打包工具那么需要你自行去相识一下,至少有了开端认识之后再来看本节内容会更好一些。
提升用户的开辟体验

权衡一个框架是否足够优秀的指标之一就是看它的开辟体验如何,我们拿 Vue3 举个例子
  1. createApp(App).mount('#not-exist')
复制代码
 当我们创建一个 Vue 应用并试图将其挂载到一个不存在的 DOM 节点时就会得到一个告诫信息:

从这条信息中我们得知挂载失败了,并说明确失败的原因:Vue 根据我们提供的选择器无法找到相应的 DOM 元素(返回 null),正式因为这条信息的存在使得我们可以大概清晰且快速的相识并定位问题,可以试想一下如果 Vue 内部不做任那里置处罚,那么很大概得到的是一个 JS 层面的错误信息,比方:Uncaught TypeError: Cannot read property 'xxx' of null,但是根据此信息我们很难知道问题出在那里。
所以在框架设计和开辟的过程中,提供友好的告诫信息是至关重要的,如果这一点做得欠好那么很大概经常收到用户的诉苦。始终提供友好的告诫信息不但可以大概快速资助用户定位问题,节流用户的时间,还可以大概为框架收获精良的口碑,让用户认为你是非常专业的。
在 Vue 的源码中,你经常可以大概看到 warn() 函数的调用,比方上面图片中的信息就是由这句 warn() 函数调用打印的:
  1. warn(  `Failed to mount app: mount target selector "${container}" returned null.`)
复制代码
对于 warn() 函数来说,由于它需要尽大概的提供有用的信息,因此它需要收集当前发生错误的组件的组件栈信息,所以如果你去看源码你会发现有些复杂,但实在最终就是调用了 console.warn() 函数。
对于开辟体验来说,除了提供须要的告诫信息,尚有许多其他方面可以作为切入口,可以进一步提升用户的开辟体验。比方在 Vue3 中当我们在控制台打印一个 Ref 数据时:
  1. const count = ref(0)console.log(count)
复制代码
打开控制台检察输出,如下图所示:
没有任那里置处罚的输出
可以发现非常的不直观,固然我们可以直接打印 count.value ,这样就只会输出 0,但是有没有办法在打印 count 的时候让输出的信息更有好呢?固然可以,欣赏允许我们编写自界说的 formatter,从而自界说输出的形式。在 Vue 的源码中你可以搜索到名为 initCustomFormatter 的函数,这个函数就是用来在开辟情况下初始化自界说 formatter 的,以 chrome 为例我们可以打开 devtool 的设置,然后勾选 Console -> Enable custom formatters:

然后刷新欣赏器后检察控制台,会发现输出的内容变得非常直观:
控制框架代码的体积

框架的巨细也是权衡框架的尺度之一,在实现同样功能的情况下固然是用越少的代码越好,这样体积就会越小,最后欣赏器加载资源的时间也就越少。这时我们不禁会想,提供越完善的告诫信息就意味着我们要编写更多的代码,这不是与控制代码体积相驳吗?没错,所以我们要想办法管理这个问题。
如果我们去看 Vue 的源码会发现,每一个 warn() 函数的调用都会共同 __DEV__ 常量的查抄,比方:
  1. if (__DEV__ && !res) {  warn(    `Failed to mount app: mount target selector "${container}" returned null.`  )}
复制代码
可以看到,打印告诫信息的前提是:__DEV__ 这个常量一定要为真,这里的 __DEV__ 常量就是到达目的的关键。
Vue 使用的是 rollup.js 对项目举行构建的,这里的 __DEV__ 常量实际上是通过 rollup 的设置来预界说的,其功能雷同于 webpack 中的 DefinePlugin 插件。
Vue 在输出资源的时候,会输出两个版本的资源,此中一个资源用于开辟情况,如 vue.global.js ;另一个与其对应的用于生产情况,如:vue.global.prod.js ,通过文件名称我们也可以大概区分。
当 Vue 构建用于开辟情况的资源时,会把 __DEV__ 常量设置为 true,这时上面那段输出告诫信息的代码就等价于:
  1. if (true && !res) {  warn(    `Failed to mount app: mount target selector "${container}" returned null.`  )}
复制代码
可以看到这里的 __DEV__ 被替换成了字面量 true ,所以这段代码在开辟情况是肯定存在的。
当 Vue 构建用于生产情况的资源时,会把 __DEV__ 常量设置为 false,这时上面那段输出告诫信息的代码就等价于:
  1. if (false && !res) {  warn(    `Failed to mount app: mount target selector "${container}" returned null.`  )}
复制代码
可以看到 __DEV__ 常量被替换为字面量 false ,这时我们发现这段分支代码永远都不会执行,因为判断条件始终为假,这段永远不会执行的代码被称为 Dead Code,它不会出现在最终的产物中,在构建资源的时候就会被移除,因此在 vue.global.prod.js 中是不会存在这段代码的。
这样我们就做到了在开辟情况为用户提供友好的告诫信息的同时,还不会增加生产情况代码的体积
框架要做到精良的 Tree-Shaking

上文中我们提到通过构建工具设置预界说的常量 __DEV__ ,就可以大概做到在生产情况使得框架不包罗打印告诫信息的代码,从而使得框架自身的代码量变少。但是从用户的角度来看,这么做仍然不敷,照旧拿 Vue 来举个例子,我们知道 Vue 提供了内置的组件比方  ,如果我们的项目中根本就没有使用到该组件,那么  组件的代码需要包罗在我们项目最终的构建资源中吗?答案是固然不需要,那如何做到这一点呢?这就不得不提到本节的主角 Tree-Shaking
那什么是 Tree-Shaking 呢?在前端范畴这个概念因 rollup 而遍及,简单的说所谓 **Tree-Shaking **指的就是消除哪些永远不会执行的代码,也就是清除 dead-code,现在无论是 rollup 照旧 webpack 都支持 Tree-Shaking
想要实现 Tree-Shaking 必须满意一个条件,即模块必须是 ES Module,因为 Tree-Shaking 依赖 ESM 的静态结构。我们使用 rollup 通过一个简单的例子看看 Tree-Shaking 如何工作,我们 demo 的目次结构如下:
  1. ├── demo│   └── package.json│   └── input.js│   └── utils.js
复制代码
首先安装 rollup:
  1. yarn add rollup -D # 大概 npm install rollup -D
复制代码
下面是 input.js 和 utils.js 文件的内容:
  1. // input.jsimport { foo } from './utils.js'foo()
复制代码
  1. // utils.jsexport function foo(obj) {  obj && obj.foo}export function bar(obj) {  obj && obj.bar}
复制代码
代码很简单,我们在 utils.js 文件中界说并导出了两个函数,分别是 foo 和 bar,然后在 input.js 中导入了 foo 函数并执行,注意我们并没有导入 bar 函数。
接着我们执行如下下令使用 rollup 构建:
  1. npx rollup input.js -f esm -o bundle.js
复制代码
这句下令的意思是以 input.js 文件问入口,输出 ESM 模块,输出的文件名叫做 bundle.js。下令执行乐成后,我们打开 bundle.js 来检察一下它的内容:
  1. // bundle.jsfunction foo(obj) {  obj && obj.foo}foo();
复制代码
可以看到,此中并不包罗 bar 函数,这说明 Tree-Shaking 起了作用,由于我们并没有使用 bar 函数,因此它作为 dead-code 被删除了。但是如果我们仔细观察会发现,foo 函数的执行也没啥意义呀,就是读取了对象的值,所以它执行照旧不执行也没有本质的区别呀,所以纵然把这段代码删了,也对我们的应用没啥影响,那为什么 rollup 不把这段代码也作为 dead-code 移除呢?
这就涉及到 Tree-Shaking 中的第二个关键点,即副作用。如果一个函数调用会产生副作用,那么就不能将其移除。什么是副作用?简单地说副作用的意思是当调用函数的时候,会对外部产生影响,比方修改了全局变量。这时你大概会说,上面的代码显着是读取对象的值怎么会产生副作用呢?实在是有大概的,想想一下如果 obj 对象是一个通过 Proxy 创建的署理对象那么当我们读取对象属性时就会触发 Getter ,在 Getter 中是大概产生副作用的,比方我们在 Getter 中修改了某个全局变量。而到底会不会产生副作用,这个只有代码真正运行的时候才华知道, JS 自己是动态语言,想要静态的分析哪些代码是 dead-code 是一件很有难度的事儿,上面只是举了一个简单的例子。
正因为静态分析 JS 代码很困难,所以诸如 rollup 等这类工具都会给我提供一个机制,让我们有本事明确的告诉 rollup :”放心吧,这段代码不会产生副作用,你可以放心移除它“,那详细怎么做呢?如下代码所示,我们修改 input.js 文件:
  1. import {foo} from './utils'/*#__PURE__*/ foo()
复制代码
注意这段注释代码 /*#__PURE_*_/,该注释的作用就是用来告诉 rollup 对于 foo() 函数的调用不会产生副作用,你可以放心的对其举行 Tree-Shaking,此时再次执行构建下令并检察 bundle.js 文件你会发现它的内容是空的,这说明 Tree-Shaking 生效了。
基于这个案例各人应该明确的是,在编写框架的时候我们需要公道的使用 /*#__PURE_*_/ 注释,如果你去搜索 Vue 的源码会发现它大量的使用了该注释,比方下面这句:
  1. export const isHTMLTag = /*#__PURE__*/ makeMap(HTML_TAGS)
复制代码
也许你会以为这会不会对编写代码带来很大的心智负担?实在不会,这是因为通常产生副作用的代码都是模块内函数的顶级调用,什么是顶级调用呢?如下代码所示:
  1. foo() // 顶级调用function bar() {  foo() // 函数内调用}
复制代码
可以看到对于顶级调用来说是大概产生副作用的,但对于函数内调用来说只要函数 bar 没有被调用,那么 foo 函数的调用固然不会产生副作用。因此你会发现在 Vue 的源码中,根本都是在一些顶级调用的函数上使用 /*#__PURE__*/ 注释的。固然该注释不但仅作用与函数,它可以使用在任何语句上,这个注释也不是只有 rollup 才华识别,webpack 以及压缩工具如 terser 都能识别它。
框架应该输出怎样的构建产物

上文中我们提到 Vue 会为开辟情况和生产情况输出差异的包,比方 vue.global.js 用于开辟情况,它包罗了须要的告诫信息,而 vue.global.prod.js 用于生产情况,不包罗告诫信息。实际上 Vue 的构建产物除了有情况上的区分之外,还会根据使用场景的差异而输出其他形式的产物,这一节我们将讨论这些产物的用途以及在构建阶段如何输出这些产物。

差异范例的产物一定是有对应的需求配景的,因此我们从需求讲起。首先我们希望用户可以直接在 html 页面中使用   [/code] 为了可以大概实现这个需求,我们就需要输出一种叫做 IIFE 格式的资源,IIFE 的全称是 Immediately Invoked Function Expression ,即”立即调用的函数表达式“,可以很容易的用 JS 来表达:
  1. (function () {  // ...}())
复制代码
如上代码所示,这就是一个立即执行的函数表达式。实际上 vue.globale.js 文件就是 IIFE形式的资源,各人可以看一下它的代码结构:
  1. var Vue = (function(exports){  // ... exports.createApp = createApp;  // ...  return exports}({}))
复制代码

这样当我们使用 [/code] 为了输出 ESM 格式的资源就需要我们设置 rollup 的输特别式为:format: 'esm'。
你大概已经注意到了,为什么 vue.esm-browser.js 文件中会有 -browser 字样,实在对于 ESM 格式的资源来说,Vue 还会输出一个 vue.esm-bundler.js 文件,此中 -browser 酿成了 -bundler。为什么这么做呢?我们知道无论是 rollup 照旧 webpack 在寻找资源时,如果 package.json 中存在 module 字段,那么会优先使用 module 字段指向的资源来取代 main 字段所指向的资源。我们可以打开 Vue 源码中的 packages/vue/package.json 文件看一下:
  1. { "main": "index.js",  "module": "dist/vue.runtime.esm-bundler.js",}
复制代码
此中 module 字段指向的是 vue.runtime.esm-bundler.js 文件,意思就是说如果你的项目是使用 webpack 构建的,那你使用的 Vue 资源就是 vue.runtime.esm-bundler.js ,也就是说带有 -bundler 字样的 ESM 资源是给 rollup 或 webpack 等打包工具使用的,而带有 -browser字样的 ESM 资源是直接给 <script type="module"> 去使用的。
那他们之间的区别是什么呢?那这就不得不提到上文中的 __DEV__ 常量,当构建用于 <script> 标签的 ESM 资源时,如果是用于开辟情况,那么 __DEV__ 会设置为 true;如果是用于生产情况,那么 __DEV__ 常量会被设置为 false ,从而被 Tree-Shaking 移除。但是当我们构建提供给打包工具的 ESM 格式的资源时,我们不能直接把 __DEV__ 设置为 true 或false,而是使用 (process.env.NODE_ENV !== &#39;production&#39;) 替换掉 __DEV__ 常量。比方下面的源码:
  1. if (__DEV__) { warn(`useCssModule() is not supported in the global build.`)}
复制代码
在带有 -bundler 字样的资源中会酿成:
  1. if ((process.env.NODE_ENV !== &#39;production&#39;)) {  warn(`useCssModule() is not supported in the global build.`)}
复制代码
这样用户侧的 webpack 设置可以自己决定构建资源的目的情况,但是最终的效果实在是一样的,这段代码也只会出现在开辟情况。
用户除了可以直接使用 <script> 标签引入资源,我们还希望用户可以在 Node.js 中通过 require 语句引用资源,比方:
  1. const Vue = require(&#39;vue&#39;)
复制代码
为什么会有这种需求呢?答案是服务端渲染,当服务端渲染时 Vue 的代码是运行在 Node.js情况的,而非欣赏器情况,在 Node.js 情况下资源的模块格式应该是 CommonJS ,简称cjs。为了可以大概输出 cjs 模块的资源,我们可以修改 rollup 的设置:format: &#39;cjs&#39; 来实现:
  1. // rollup.config.jsconst config = {  input: &#39;input.js&#39;,  output: {    file: &#39;output.js&#39;,    format: &#39;cjs&#39; // 指定模块形式  }}export default config
复制代码
特性开关

在设计框架时,框架会提供诸多特性(或功能)给用户,比方我们提供 A、B、C 三个特性给用户,同时呢我们还提供了 a、b、c 三个对应的特性开关,用户可以通过设置 a、b、c 为true 和 false 来代表开启和关闭,那么将会带来许多收益:

  • 对于用户关闭的特性,我们可以使用 Tree-Shaking 机制让其不包罗在最终的资源中。
  • 该机制为框架设计带来了机动性,可以通过特性开关任意为框架添加新的特性而不消担心用不到这些特性的用户侧资源体积变大,同时当框架升级时,我们也可以通过特性开关来支持遗留的 API,这样新的用户可以选择不适用遗留的 API,从而做到用户侧资源最小化。
那怎么实现特性开关呢?实在很简单,原理和上文提到的 __DEV__ 常量一样,本质是使用 rollup 的预界说常量插件来实现,那一段 Vue3 的 rollup 设置来看:
  1. { __FEATURE_OPTIONS_API__: isBundlerESMBuild ? `__VUE_OPTIONS_API__` : true,}
复制代码
此中 __FEATURE_OPTIONS_API__ 雷同于 __DEV__,我们可以在 Vue3 的源码中搜索,可以找到许多雷同如下代码这样的判断分支:
  1. // support for 2.x optionsif (__FEATURE_OPTIONS_API__) {  currentInstance = instance  pauseTracking()  applyOptions(instance, Component)  resetTracking()  currentInstance = null}
复制代码
当 Vue 构建资源时,如果构建的资源是用于给打包工具使用的话(即带有 -bundler 字样的资源),那么上面代码在资源中会酿成:
  1. // support for 2.x optionsif (__VUE_OPTIONS_API__) { // 这一这里  currentInstance = instance  pauseTracking()  applyOptions(instance, Component)  resetTracking()  currentInstance = null}
复制代码
此中 __VUE_OPTIONS_API__ 就是一个特性开关,用户侧就可以通过设置__VUE_OPTIONS_API__ 来控制是否包罗这段代码。通常用户可以使用 webpack.DefinePlugin插件实现:
  1. // webpack.DefinePlugin 插件设置new webpack.DefinePlugin({  __VUE_OPTIONS_API__: JSON.stringify(true) // 开启特性})
复制代码
最后再来详细表明一下 __VUE_OPTIONS_API__ 开关是干嘛用的,在 Vue2 中我们编写的组件叫做组件选项 API:
  1. export default { data() {}, // data 选项  computed: {}, // computed 选项 //  其他选项...}
复制代码
但是在 Vue3 中,更推荐使用 Composition API 来编写代码,比方:
  1. export default { setup() {  const count = ref(0)    const doubleCount = computed(() => count.value * 2) // 相当于 Vue2 中的 computed 选项 }}
复制代码
但是在 Vue3 中,更推荐使用 Composition API 来编写代码,比方:
  1. export default { setup() {  const count = ref(0)    const doubleCount = computed(() => count.value * 2) // 相当于 Vue2 中的 computed 选项 }}
复制代码
但是为了兼容 Vue2,在 Vue3 中仍然可以使用选项 API 的方式编写代码,但是对于明确知道自己不会使用选项 API 的用户来说,它们就可以选择使用 __VUE_OPTIONS_API__ 开关来关闭该特性,这样在打包的时候 Vue 的这部门代码就不会包罗在最终的资源中,从而减小资源体积。
错误处理处罚

错误处理处罚是开辟框架的过程中非常重要的环节,框架的错误处理处罚做的优劣可以大概直接决定用户应用步伐的坚固性,同时还决定了用户开辟应用时处理处罚错误的心智负担。
为了让各人对错误处理处罚的重要性有更加直观的感受,我们从一个小例子说起。假设我们开辟了一个工具模块,代码如下:
  1. // utils.jsexport default {  foo(fn) {    fn && fn()  }}
复制代码
该模块导出一个对象,此中 foo 属性是一个函数,吸收一个回调函数作为参数,调用 foo函数时会执行回调函数,在用户侧使用时:
  1. import utils from &#39;utils.js&#39;utils.foo(() => {  // ...})
复制代码
各人思考一下如果用户提供的回调函数在执行的时候堕落了怎么办?此时有两个办法,其一是让用户自行处理处罚,这需要用户自己去 try...catch:
  1. import utils from &#39;utils.js&#39;utils.foo(() => {  try {   // ...  } catch (e) {   // ... }})
复制代码
但是这对用户来说是增加了负担,试想一下如果 utils.js 不是仅仅提供了一个 foo 函数,而是提供了几十上百个雷同的函数,那么用户在使用的时候就需要逐一添加错误处理处罚步伐。
第二种办法是我们取代用户统一处理处罚错误,如下代码所示:
  1. // utils.jsexport default {  foo(fn) {    try {      fn && fn()     } catch(e) {/* ... */}  },  bar(fn) {    try {      fn && fn()     } catch(e) {/* ... */}  },}
复制代码
这中办法实在就是我们取代用户编写错误处理处罚步伐,实际上我们可以进一步封装错误处理处罚步伐为一个函数,假设叫它 callWithErrorHandling:
  1. // utils.jsexport default {  foo(fn) {    callWithErrorHandling(fn)  },  bar(fn) {    callWithErrorHandling(fn)  },}function callWithErrorHandling(fn) {  try {    fn && fn()  } catch (e) {    console.log(e)  }}
复制代码
可以看到代码变得轻便多了,但轻便不是目的,这么做真正的利益是,我们有时机为用户提供统一的错误处理处罚接口,如下代码所示:
  1. // utils.jslet handleError = nullexport default {  foo(fn) {    callWithErrorHandling(fn)  },  // 用户可以调用该函数注册统一的错误处理处罚函数  resigterErrorHandler(fn) {    handleError = fn  }}function callWithErrorHandling(fn) {  try {    fn && fn()  } catch (e) {    // 捕获到的错误通报给用户的错误处理处罚步伐    handleError(e)  }}
复制代码
我们提供了 resigterErrorHandler 函数,用户可以使用它注册错误处理处罚步伐,然后在 callWithErrorHandling 函数内部捕获到错误时,把错误对象通报给用户注册的错误处理处罚步伐。
这样在用户侧的代码就会非常轻便且坚固:
  1. import utils from &#39;utils.js&#39;// 注册错误处理处罚步伐utils.resigterErrorHandler((e) => {  console.log(e)})utils.foo(() => {/*...*/})utils.bar(() => {/*...*/})
复制代码
这时错误处理处罚的本事完全由用户控制,用户既可以选择忽略错误,也可以调用上报步伐将错误上报到监控系统。
实际上这就是 Vue 错误处理处罚的原理,你可以在源码中搜索到 callWithErrorHandling 函数,别的在 Vue 中我们也可以注册统一的错误处理处罚函数:
  1. import App from &#39;App.vue&#39;const app = createApp(App)app.config.errorHandler = () => {  // 错误处理处罚步伐}
复制代码
精良的 Typescript 范例支持

Typescript 是微软开源的编程语言,简称 TS,它是 JS 的超集可以大概为 JS 提供范例支持。现在越来越多的人和团队在他们的项目中使用 TS 语言,使用 TS 的利益许多,如代码即文档、编辑器的自动提示、一定水平上可以大概制止低级 bug、让代码的可维护性更强等等。因此对 TS 范例支持的是否完善也成为评价一个框架的重要指标。
那如何权衡一个框架对 TS 范例支持的优劣呢?这里有一个常见的误区,许多同学以为只要是使用 TS 编写就是对 TS 范例支持的友好,实在使用 TS 编写框架和框架对 TS 范例支持的友好是两件关系不大的事儿。思量到有的同学大概没有打仗过 TS,所以这里不会做深入讨论,我们只举一个简单的例子,如下是使用 TS 编写的函数:
  1. function foo(val: any) {  return val}
复制代码
这个函数很简单,它担当一个参数 val 而且参数可以是任意范例(any),该函数直接将参数作为返回值,这说明返回值的范例是由参数决定的,参数如果是 number 范例那么返回值也是 number 范例,然后我们可以实验使用一下这个函数,如下图所示:
在调用 foo 函数时我们通报了一个字符串范例的参数 &#39;str&#39;,按照之前的分析,我们得到的效果 res 的范例应该也是字符串范例,然而当我们把鼠标 hover 到 res 常量上时可以看到其范例是 any,这并不是我们想要的效果,为了到达理想状态我们只需要对 foo 函数做简单的修改即可:
  1. function foo(val: T): T {  return val}
复制代码
可以看到 res 的范例是字符字面量 &#39;str&#39; 而不是 any 了,这说明我们的代码生效了。
通过这个简单的例子我们认识到,使用 TS 编写代码与对 TS 范例支持友好是两件事,在编写大型框架时想要做到完美的 TS 范例支持是一件很不容易的事情,各人可以检察 Vue 源码中的 runtime-core/src/apiDefineComponent.ts 文件,整个文件里真正会在欣赏器运行的代码实在只有 3 行,但是当你打开这个文件的时候你会发现它整整有靠近 200 行的代码,实在这些代码都是在做范例支持方面的事情,由此可见框架想要做到完善的范例支持是需要付出相当大的积极的。
除了要花大力大肆气做范例推导,从而做到更好的范例支持外,还要思量对 TSX 的支持。
以上泉源前端森林
 
 
 
 
 
 
 

来源:https://blog.csdn.net/elijip/article/details/112059499
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则


专注素材教程免费分享
全国免费热线电话

18768367769

周一至周日9:00-23:00

反馈建议

27428564@qq.com 在线QQ咨询

扫描二维码关注我们

Powered by Discuz! X3.4© 2001-2013 Comsenz Inc.( 蜀ICP备2021001884号-1 )