Vue3 Composition API

2020 年 9 月,它終于來了,盡管很早就上了 Beta 版本,但是尤大確一直沒有推出正式版,如今正式版上線,讓我們來一睹為快,看看 Vue3 帶來了哪些改變吧!

Vue3 文檔地址:https://v3.cn.vuejs.org/

Composition API


setup

什么是 setup ?vue3 之前我們的代碼邏輯會在組件的各個角落,大量碎片化的代碼使得理解和維護組件變得異常困難,在處理單個邏輯關(guān)注點時,我們必須不斷地跳轉(zhuǎn)相關(guān)代碼的選項塊。試想一下如果我們能夠?qū)⑼粋€邏輯的相關(guān)代碼都配置在一起,是不是更容易理解和維護,那么我們將這些邏輯和代碼寫在哪里了?在 vue3 中,我們將此位置稱為 setup

setup 執(zhí)行時,組件實例尚未被創(chuàng)建 (beforeCreate - created),因此在 setup 中沒有 this 。setup 選項應(yīng)該是一個接收 propscontent 的函數(shù),我們這里先來看看 props 的用法,在 setup 函數(shù)中除了 props 之外,我們無法訪問組件中聲明的任何屬性(本地狀態(tài)、計算屬性或方法)。直接上栗子吧:

<!DOCTYPE html>
<html lang="en">
<head>
  <script src="https://unpkg.com/vue@next"></script>
</head>
<body>
  <div id="root"></div>
</body>
<script>
  const app = Vue.createApp({
    template: `
      <test username='張三' />
    `
  })
  app.component('test', {
    props: ['username'],
    setup(props) {
      console.log(props) // Proxy {username: "張三"}
      // console.log(this) // window
    },
    template: `<div>{{username}}</div>`
  })
  const vm = app.mount('#root')
</script>
</html>

上述代碼中,我們創(chuàng)建全局組件 test , 父組件往 test 上傳入 usernametest 中通過 props 接收父組件傳遞過來的 username,此時在子組件的 setup 執(zhí)行時可以接收到 props 中的值,但是打印出 this 是指向 window 的。

但是 setup 函數(shù)返回的所有內(nèi)容都將暴露給組件的其余部分(計算屬性、方法、生命周期鉤子等等)以及組件的模板。我們還是在剛剛的 test 組件中來驗證下這句話:

app.component('test', {
  props: ['username'],
  mounted() { // mounted 聲明周期中可以使用 setup 中返回出來的 username
    console.log(this.username) // 張三  
  },
  setup(props) {
    const username = props.username
    return { username }
  },
  template: `<div>{{username}}</div>` // 張三
  })
const vm = app.mount('#root') 

當(dāng)然我們也可以在組件的生命周期或者方法中直接訪問 setup 函數(shù)里面的方法,栗子如下:

const app = Vue.createApp({
  mounted() {
    // 可以直接在生命周期里面調(diào)用 setup() 里面的方法
    this.$options.setup().handleClick()
  },
  setup(props, context) {
    return {
      handleClick() {
        alert('setup')
    }
  }
}})

總結(jié):執(zhí)行 setup() 是在 created() 之前執(zhí)行,由于尚未創(chuàng)建實例,所以 setup()中無法直接使用 this,但是 methods/computed/watch 等其他地方都可以直接調(diào)用 setup()。并且 setup() 返回的所有內(nèi)容都將暴露給組件的其余部分(計算屬性、方法、生命周期鉤子等等)以及組件的模板。

ref 語法

vue3 之前,我們只需要在 data 中定義變量,那么這個變量在 methods/computed/watch 等中的改變就會直接響應(yīng)到組件模板中了。但是在 vue3 之中我們只有通過 ref 函數(shù)和 reactive 函數(shù)來實現(xiàn)響應(yīng)式變量在組件任何地方起作用。我們先通過小栗子可以了解 ref 函數(shù)的基本用法:

const app = Vue.createApp({
  setup() {
    // 通過 ref 初始化一個響應(yīng)式變量 count 并將其初始值設(shè)為 0
    const { ref } = Vue
    let count = ref(0)
    const increase = () => {
      return count.value += 1
    }
    return { count, increase }
  },
  template: `
    <div>{{count}}</div>
    <button @click="increase">增加</button>
  `
})

通過上述代碼我們可以看到 ref 接受參數(shù),并將其包裹在一個帶有 value property 的對象中返回,然后可以使用該 property 訪問或更改響應(yīng)式變量的值。那么問題來了為什么要將值封裝在一個對象中呢?這是因為在 JavaScript 中,NumberString 等基本類型是通過值傳遞的,而不是通過引用傳遞的。我們知道值傳遞一般指在調(diào)用函數(shù)時將實際參數(shù)復(fù)制一份傳遞到函數(shù)中,這樣如果在函數(shù)中如果對參數(shù)進(jìn)行修改,不會影響到實際參數(shù),既占用內(nèi)存空間也無法實現(xiàn)數(shù)據(jù)的響應(yīng)式。而通過引用傳遞其實是直接把內(nèi)存地址傳過去,也就是說在引用傳值的過程中操作的都是源數(shù)據(jù),也就能更好的實現(xiàn)數(shù)據(jù)響應(yīng)式驅(qū)動。

其實簡單點就是基礎(chǔ)類型的值都是存在棧中,引用類型的值都是存在堆中,堆共用一個數(shù)據(jù)源,而每個棧都是一個獨立的空間,所以我們將數(shù)據(jù)存入堆中才能更好的做到數(shù)據(jù)響應(yīng)式改變。

reactive 語法

reactive 語法和 ref 都是支持?jǐn)?shù)據(jù)的響應(yīng)式,但是 reactive 接收的一般是數(shù)組和對象,直接來看小栗子:

const app = Vue.createApp({
  setup() {
    // 通過 reactive 初始化一個響應(yīng)式對象
    const { reactive, computed } = Vue
    const data = reactive({
      count: 0,
      increase: () => data.count++,
      docuble: computed(() => data.count * 2)
    })
    return { data }
  },
  template: `
    <div>{{data.count}}</div>
    <div>{{data.docuble}}</div>
    <button @click="data.increase">增加</button>
  `
})

上述代碼中我們會發(fā)現(xiàn)模板中每次都要寫 data.countdata.docuble 過于繁瑣,如果我們在 return { data } 時使用展開運算符或者對象賦值的方式導(dǎo)出,是否可以直接使用 count 、 docuble 等變量呢。

// 使用展開運算符
return {...data}
// 使用對象賦值
return {
  count: data.count,
  increase: data.increase,
  docuble: data.docuble
}

我們發(fā)現(xiàn)取出來之后,count、increasedocuble 都喪失了響應(yīng)式的活性,他們變成了不同的 javascript 類型。那么我們?nèi)绾谓鉀Q這種情況呢?vue3 為我們推出了 toRefs 函數(shù)。

toRefs 語法

我們先試用 toRefs 改寫上面的代碼:

const app = Vue.createApp({
  setup() {
    // 通過 reactive 初始化一個響應(yīng)式對象
    const { reactive, computed, toRefs } = Vue
    const data = reactive({
      count: 0,
      increase: () => data.count++,
      docuble: computed(() => data.count * 2)
    })
    // toRefs 將 data 轉(zhuǎn)變?yōu)橐粋€響應(yīng)式對象
    const { count, increase, docuble } = toRefs(data)
    return { count, increase, docuble }
},
  template: `
    <div>{{count}}</div>
    <div>{{docuble}}</div>
    <button @click="increase">增加</button>
  `
})

toRefs 函數(shù)接收一個 reactive 對象作為參數(shù),返回一個普通的對象,但是這個普通對象的每一項都變成了 ref 類型的對象,即支持響應(yīng)式。

1、ref / reactive 通過 proxy 對數(shù)據(jù)進(jìn)行封裝,當(dāng)數(shù)據(jù)變化時,觸發(fā)模板等內(nèi)容的更新。
2、ref 處理基礎(chǔ)類型的數(shù)據(jù)。例如 ref('cc') 變成 proxy({value: 'chen'}) 這樣一個響應(yīng)式的引用。
3、reactive 處理非基礎(chǔ)類型的數(shù)據(jù)。例如 reactive({name: 'cc'}) 變成 proxy({name: 'cc'}) 這樣一個響應(yīng)式引用。
4、toRefs 接收一個 reactive 對象作為參數(shù),返回一個普通對象,并將普通對象的每一項都變成了 ref 類型的對象,保持器響應(yīng)式活力。例如:toRefs proxy({name: 'cc'}) 變成 {name: proxy({value: 'cc'})} 。

readonly 語法

獲取一個對象 (響應(yīng)式或純對象) 或 ref 并返回原始 proxy 的只讀 proxy。舉個栗子:

const app = Vue.createApp({
    setup(props, context) {
      const { reactive, readonly, toRefs } = Vue
      let nameObj = reactive([123])
      // 獲取 nameObj 這個響應(yīng)對象為參數(shù)
      let copyNameObj = readonly(nameObj)
      setTimeout(() => {
        nameObj[0] = 456
        // 因為使用了 readonly 無法改變 copyNameObj 的值
        copyNameObj[0] = 456
      }, 2000)
      return { nameObj, copyNameObj }
    }
  })

toRef 語法

toRefs 只差了一個 s,那么它有什么用呢?讓我們先來假設(shè)一個場景,如下栗子:

const app = Vue.createApp({
  template: `
    <div>{{age}}</div>
  `,
  setup(props, context) {
    const { reactive, toRefs, toRef } = Vue
    const data = reactive({ name: 'zhangsan' })
    const { age } = toRefs(data)
    console.log(age) // undefined
    setTimeout(() => {
      age.value = 20 // 報錯 Cannot set property 'value' of undefined
    }, 2000)
    return { age }
  }
}).mount('#root')

我們定義了一個響應(yīng)式變量 data ,toRefs 會將相應(yīng)對象解析成一個普通對象,并保證該普通對象的屬性都是 ref 響應(yīng)式屬性,我們又想在新增加一個 age 的響應(yīng)式變量,結(jié)果只得到了一個 undefined 類型而不是預(yù)料中的 ref 類型。此時我們可以使用 toRef 改造上面的代碼:

const app = Vue.createApp({
  template: `
    <div>{{age}}</div>
  `,
  setup(props, context) {
    const { reactive, toRefs, toRef } = Vue
    const data = reactive({ name: 'zhangsan' })
    const age = toRef(data, 'age')
    setTimeout(() => {
      age.value = 20
    }, 2000)
    return { age }
  }
}).mount('#root')

toRef 可以用來為源響應(yīng)式對象上的某個 property 新創(chuàng)建一個 ref,這里我們就新建了一個 ref 類型的 age 變量,然后 age 就可以完美繼承響應(yīng)活性了。

setup 參數(shù) context

前面我們介紹了 setup 函數(shù)中的第一個參數(shù) props,這里我們再來看看它的第二個參數(shù) context 的用法,根據(jù)官網(wǎng)文檔傳遞給 setup 函數(shù)的第二個參數(shù)是 context。context 是一個普通的 JavaScript 對象,它暴露三個組件的 property

export default {
  setup(props, context) {
    const { attrs, slots, emit } = context
  }
}

context 是一個普通的 JavaScript 對象,也就是說,它不是響應(yīng)式的,這意味著你可以安全地對 context 使用 ES6 解構(gòu)。

export default {
  setup(props, { attrs, slots, emit }) {}
}

那首先我們來看一下 attrs 的使用:

const app = Vue.createApp({
  template: `
    <div>
      <child username="zhangsan"></child>
    </div>
  `
})
app.component('child', {
  template: `
    <div></div>
  `,
  setup(props, context) {
    const { attrs, slots, emit } = context
    // attrs 接收一個 None-Props 屬性
    console.log(attrs.username)
  }
})
const vm = app.mount('#root')

什么是 None-Props 屬性 呢?正常情況下,父組件給子組件傳遞了 username ,子組件應(yīng)該通過 props: ['username'] 去接收,但是如果子組件如果不使用 props 接收,那么就可以在 attrs 處接收到 username。而這個 username 就是 None-Props 屬性 。

slots 看名字應(yīng)該就可以大致猜到,這是用來接收插槽的,同樣是上面的栗子,我們改寫一下:

const app = Vue.createApp({
  template: `
  <div>
    <child>parent</child>
  </div>
`
})
app.component('child', {
  setup(props, context) {
    const { h } = Vue
    const { attrs, slots, emit } = context
    return () => h('div', {}, slots.default())
  }
})
const vm = app.mount('#root')

而如果不在 setup 函數(shù)中,而是在組件外部的生命周期函數(shù)中,我們只能通過 this 去獲取 slots ,現(xiàn)在我們可以完全脫離 vue2 的寫法,將所有的東西都聚合到 setup 函數(shù)中。

mounted() {
  console.log(this.$slots.default())
},

emit 看名字其實應(yīng)該也可以猜出來,子組件像父組件的事件傳遞,直接看栗子:

const app = Vue.createApp({
  template: `
    <div>
      <child @change="change"></child>
    </div>
  `,
  setup() {
    const change = () => {
      alert('change')
    }
    return { change }
  }
})
app.component('child', {
  template: `
    <div @click="handleClick">child</div>
  `,
  setup(props, context) {
    const { attrs, slots, emit } = context
    const handleClick = () => {
      // 使用 emit 直接傳遞給父組件
      emit('change')
    }
    return { handleClick }
  }
})
const vm = app.mount('#root')

watch 語法

熟悉 vue2 的小伙伴對 watch 肯定不陌生,在 vue3 中,我們可以把 watch 集成到 setup 函數(shù)中進(jìn)行使用。watch 需要偵聽特定的數(shù)據(jù)源,并在單獨的回調(diào)函數(shù)中執(zhí)行副作用。默認(rèn)情況下,它也是惰性的——即回調(diào)僅在偵聽源發(fā)生更改時被調(diào)用。

使用 watch 監(jiān)聽單個數(shù)據(jù)源

const app = Vue.createApp({
  setup() {
    const { ref, reactive, watch } = Vue
    let title = ref("")
    const editTitle = () => {
      title.value = 'hello world'
    }
    watch(title, (newValue, oldValue) => {
      console.log('old', oldValue) // 'old', ""
      console.log('new', newValue) // 'new', "hello world"
    })
    return { title, editTitle }
  },
  template: `
    <div>ref 值監(jiān)聽:{{title}}</div>
    <button @click="editTitle">改變title</button>
  `
}).mount('#root')

使用 watch 監(jiān)聽多個數(shù)據(jù)源

const app = Vue.createApp({
  setup() {
    const { ref, reactive, watch } = Vue
    let title = ref("")
    const data = reactive({
      count: 0
    })
    const editTitle = () => {
      title.value = 'hello world'
    }
    const editCount = () => {
      data.count++
    }
    // watch 可以監(jiān)聽一個數(shù)組,里面是需要監(jiān)聽的多個值
    watch([title, data], (newValue, oldValue) => {
      console.log('old', oldValue) //  ["", Proxy]
      console.log('new', newValue) // ["hello world", Proxy]
    })
    return { title, editTitle, data, editCount }
  },
  template: `
    <div>reactive 值監(jiān)聽:{{data.count}}</div>
    <div>ref 值監(jiān)聽:{{title}}</div>
    <button @click="editTitle">改變title</button>
    <button @click="editCount">改變count</button>
  `
}).mount('#root')

上面的栗子中,我們使用監(jiān)聽了 reactive 新增監(jiān)聽了一個 data 對象,但是如果我們只想監(jiān)聽 data 對象中的 count 屬性,而不是監(jiān)聽整個 data 對象,那么我們應(yīng)該如何實現(xiàn)呢?

// watch 中接收一個函數(shù),其返回值為 reactive 函數(shù)對象中想監(jiān)聽的值
watch([title, () => data.count], (newValue, oldValue) => {
  console.log('old', oldValue) // ["", 0]
  console.log('new', newValue) // ["hello world", 1]
})

watchEffect 語法

watchEffectwatch 都是用來對數(shù)據(jù)的偵聽,但是他們有 3 個比較大的差別:

1、watchEffect 立即執(zhí)行,沒有惰性。
2、不需要傳遞你要偵聽的內(nèi)容,自動會感知代碼依賴,不需要傳遞很多參數(shù),只需要傳遞一個回調(diào)函數(shù)。
3、不能獲取之前數(shù)據(jù)的值 。

舉個栗子:

const app = Vue.createApp({
  template: `
    <input v-model="nameObj.name" />
  `,
  setup() {
    const { reactive, watchEffect } = Vue
    const nameObj = reactive({ name: 'cc' })
    watchEffect(() => {
      // 初次加載就會執(zhí)行打印出 `abc`
      // 但是自動檢測到 `abc` 和頁面沒有任何關(guān)系后面就不會在執(zhí)行 
      console.log('abc')
      // nameObj.name 綁定了頁面的 input,所以每次輸入改變 name 的值都會執(zhí)行一次
      console.log(nameObj.name)
    })
    return { nameObj }
  }
}).mount('#root')

如果我們要在 5 秒后停止 watch 或者 watchEffect 對屬性的監(jiān)聽?wèi)?yīng)該如何做呢,如下栗子:

const app = Vue.createApp({
  template: `
    <input v-model="nameObj.name" />
  `,
  setup() {
    const { reactive, watchEffect } = Vue
    const nameObj = reactive({ name: 'cc' })
    // 將函數(shù)變成一個命名函數(shù),然后 5s 后在執(zhí)行一次這個命名函數(shù)(watch 同理)
    const stop = watchEffect(() => {
      console.log(nameObj.name)
      setTimeout(() => {
        stop()
      }, 5000)
    })
    return { nameObj }
  }
}).mount('#root')

老瓶新酒 - 生命周期

vue3 組件中的生命周期和 vue2 組件生命周期基本相同,唯一不同的是將 beforeDestroydestroyed 改成了 beforeUnmountunmounted,根據(jù)尤大的說法是后者語義性更強,更能表達(dá)組件卸載的說法。通過代碼簡單回顧下:

// 生命周期函數(shù):在某一時刻會自動執(zhí)行的函數(shù)
const app = Vue.createApp({
  data() {
    return {
      message: 'see you'
    }
  },
  template: `
    <div @click="handleClickItem">{{message}}</div>
  `,
  methods: {
    handleClickItem() {
      this.message = 'bye bye'
      setTimeout(() => {
        app.unmount()
      }, 1000)
    }
  },
  // 在實例生成之前會自動執(zhí)行的函數(shù)
  beforeCreate() {
    console.log('beforeCreated', this.message)
  },
  // 在實例生成之后會自動執(zhí)行的函數(shù)
  created() {
    console.log('created', this.message)
  },
  // 在組件內(nèi)容被渲染到頁面之前自動執(zhí)行的函數(shù)
  beforeMount() {
    console.log('beforeMounte', document.getElementById('root').innerHTML)
  },
  // 在組件內(nèi)容被渲染到頁面之后自動執(zhí)行的函數(shù)
  mounted() {
    console.log('mounted', document.getElementById('root').innerHTML)
  },
  // 在組件內(nèi)容被修改之前自動執(zhí)行的函數(shù)
  beforeUpdate() {
    console.log('beforeUpdate', document.getElementById('root').innerHTML)
  },
  // 在組件內(nèi)容被修改之后自動執(zhí)行的函數(shù)
  updated() {
    console.log('updated', document.getElementById('root').innerHTML)
  },
  // 在 Vue 應(yīng)用失效時,自動執(zhí)行的函數(shù) 可以通過 app.unmount() 觸發(fā)
  beforeUnmount() {
    console.log('beforeUnmount', document.getElementById('root').innerHTML)
  },
  // 當(dāng) Vue 應(yīng)用失效且 Dom 完全銷毀之后,自動執(zhí)行的函數(shù)
  unmounted() {
    console.log('unmount', document.getElementById('root'))
  }
})
const vm = app.mount('#root')

setup 函數(shù)中的調(diào)用生命周期鉤子

  • beforeCreate -> 不需要
  • created -> 不需要
  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeUnmount -> onBeforeUnmount
  • unmounted -> onUnmounted
  • errorCaptured -> onErrorCaptured
  • renderTracked -> onRenderTracked
  • renderTriggered -> onRenderTriggered

因為 setup 是圍繞 beforeCreatecreated 生命周期鉤子運行的,所以不需要顯式地定義它們。換句話說,在這些鉤子中編寫的任何代碼都應(yīng)該直接在 setup 函數(shù)中編寫。

vue3 模塊化開發(fā)

前面我們所有的邏輯代碼都寫進(jìn)了 setup 函數(shù)中,雖然所有的代碼都寫在 setup 函數(shù)中是可以將邏輯都聚合到一起,但是如果一個頁面所有的邏輯代碼都寫入一個函數(shù),只會讓代碼顯得更加難以維護。也許你寫完當(dāng)前也頁面的所有邏輯代碼有 1000 行,但是三天之后你在想維護這段代碼,估計你自己也會懵逼,不知從何寫起。所以日常開發(fā)中我們更應(yīng)該將 setup 當(dāng)做一個流程處理函數(shù),將該頁面的所有邏輯抽離成一個一個模塊,將每個模塊的結(jié)果導(dǎo)出到 setup 函數(shù)中供頁面模板使用。我們先來看一個簡單的栗子:

我們這里實現(xiàn)一個簡單的功能,就是記錄每次鼠標(biāo)點擊屏幕時打印出鼠標(biāo)當(dāng)前的坐標(biāo)值!

// 將鼠標(biāo)坐標(biāo)值更新邏輯抽離成一個單獨的函數(shù)
const { reactive, toRefs, onMounted, onUnmounted } = Vue
const updateMouseEffect = () => {
    const mouseObj = reactive({
      x: 0,
      y: 0
    })
    const updateMouse = (e) => {
      mouseObj.x = e.pageX
      mouseObj.y = e.pageY
    }
    onMounted(() => {
      document.addEventListener('click', updateMouse)
    })
    onUnmounted(() => {
      document.removeEventListener('click', updateMouse)
    })
    const { x, y } = toRefs(mouseObj)
    return { x, y }
}
const app = Vue.createApp({
    // 頁面上的每一個邏輯都抽離成對應(yīng)的函數(shù)
    // setup 函數(shù)只負(fù)責(zé)將封裝的邏輯中導(dǎo)出的值引入,不涉及具體邏輯編寫
    setup() {
      const { x, y } = updateMouseEffect()
      return { x, y }
    },
    template: `
      <h1>x: {{x}} y: {{y}}</h1>
    `
}).mount('#root')

看了上面的代碼,是不是覺得 vue3 可以完美實現(xiàn)各種 hooks ,接下來我們結(jié)合 axios 來實現(xiàn)一個涉及外部請求的封裝函數(shù)。這里都是用的 cdn ,如需復(fù)現(xiàn)栗子,記得頭部引入 axioscdn 鏈接。

const { ref } = Vue
const useURLLoader = (url) => {
    const result = ref(null) // 響應(yīng)結(jié)果
    const loading = ref(true) // 是否顯示loading
    const loaded = ref(false) // 是否加載完成
    const error = ref(null) // 是否響應(yīng)錯誤

    axios.get(url).then((rawData) => {
      loading.value = false
      loaded.value = true
      result.value = rawData.data
    }).catch((e) => {
      error.value = e
    })

    return { result, loading, loaded, error }
}
const url = 'https://dog.ceo/api/breeds/image/random'
const app = Vue.createApp({
    setup() {
        const { result, loading, loaded, error } = useURLLoader(url)
        return { result, loading, loaded, error }
  },
    template: `
        <h1 v-if="loading">Loading!...</h1>
        <img v-if="loaded" :src="result.message" >
    `
}).mount('#root')

這個栗子其實就是我們在發(fā)請求時希望頁面顯示 loading ,拿到請求結(jié)果之后將 loading 狀態(tài)取消然后展示我們拿到的結(jié)果數(shù)據(jù),代碼很簡單,也是將所有的邏輯代碼都抽離到一個單獨的函數(shù)中,setup 函數(shù)只負(fù)責(zé)流程控制和數(shù)據(jù)導(dǎo)出。

常用的一些都整理出來了,本來想繼續(xù)整理一些 vue3 中的新特性,但是篇幅太長了,就準(zhǔn)備在下一篇中繼續(xù)整理了,如有錯誤或不正確的地方歡迎指正,每天進(jìn)步一點點,加油?。?!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容