Vuex-状态管理(24)
Vuex狀態(tài)管理
課程目標
- 組件通信方式回顧
- Vuex核心概念和基本使用
- 購物車案例
- 模擬實現(xiàn)Vuex
組件內(nèi)的狀態(tài)管理流程
Vue最核心的兩個功能:數(shù)據(jù)驅(qū)動和組件化
組件化開發(fā)給我們帶來了:
- 更快的開發(fā)效率
- 更好的可維護性
每個組件都有自己的狀態(tài)、視圖和行為等組成部分
new Vue({ // statedata () {return {count: 0}}, // viewtemplate: ` <div>{{ count }}</div> `, // actionsmethods: {increment () {this.count++}} })狀態(tài)管理包含一下幾部分:
- state,驅(qū)動應(yīng)用的數(shù)據(jù)源
- view,以聲明方式將state映射到視圖
- actions,響應(yīng)在view上的用戶輸入導致的狀態(tài)變化
組件間通信方式回顧
大多數(shù)場景下的組件都并不是獨立存在的,而是相互協(xié)作共同構(gòu)成了一個復(fù)雜的業(yè)務(wù)功能。在 Vue 中為不同的組件關(guān)系提供了不同的通信規(guī)則。
父傳子:Props Down
- 子組件中通過props接受數(shù)據(jù)
- 父組件中給子組件通過相應(yīng)屬性傳值
Props
Prop 的大小寫 (camelCase vs kebab-case)
HTML 中的 attribute 名是大小寫不敏感的,所以瀏覽器會把所有大寫字符解釋為小寫字符。這意味著當你使用 DOM 中的模板時,camelCase (駝峰命名法) 的 prop 名需要使用其等價的 kebab-case (短橫線分隔命名) 命名:
Vue.component('blog-post', { // 在 JavaScript 中是 camelCase 的 props: ['postTitle'], template: '<h3>{{ postTitle }}</h3>' }) <!-- 在 HTML 中是 kebab-case 的 --> <blog-post post-title="hello!"></blog-post>重申一次,如果你使用字符串模板,那么這個限制就不存在了。
Prop 類型
到這里,我們只看到了以字符串數(shù)組形式列出的 prop:
props: ['title', 'likes', 'isPublished', 'commentIds', 'author']但是,通常你希望每個 prop 都有指定的值類型。這時,你可以以對象形式列出 prop,這些 property 的名稱和值分別是 prop 各自的名稱和類型:
props: {title: String,likes: Number,isPublished: Boolean,commentIds: Array,author: Object,callback: Function,contactsPromise: Promise // or any other constructor }這不僅為你的組件提供了文檔,還會在它們遇到錯誤的類型時從瀏覽器的 JavaScript 控制臺提示用戶。你會在這個頁面接下來的部分看到類型檢查和其它 prop 驗證。
子傳父:Event Up
非父子組件:Event Bus
我們可以使用一個非常簡單的Event Bus來解決這個問題:
eventbus.js
export default new Vue()然后在需要通信的兩端:
使用$on訂閱:
// 沒有參數(shù) bus.$on('自定義事件名稱', () => {// 執(zhí)行操作 })// 有參數(shù) bus.$on('自定義事件名稱', data => {// 執(zhí)行操作 })使用$emit發(fā)布:
// 沒有自定義傳參 bus.$emit('自定義事件名稱')// 有自定義傳參 bus.$emit('自定義事件名稱', 參數(shù)數(shù)據(jù))父直接訪問子組件:通過ref獲取子組件
ref有兩個作用:
- 如果把它作用到普通HTML標簽上,則獲取的是DOM
- 如果把它作用到組件標簽上,則獲取到的是組件實例
創(chuàng)建base-input
<template><div><h1>ref Child</h1> <input ref="input" type="text" v-model="value"></div> </template> <script>export default {data() {return {value: ''}}, methods: {focus() {this.$refs.input.focus()}} }</script>在使用子組件的時候,添加ref屬性:
<base-input ref='usernameInput'></base-input>然后在父組件等渲染完畢后使用$refs訪問:
mounted() {this.$refs.usernameInput.focus() }$refs只會在組件渲染完成之后生效,并且它們不是響應(yīng)式的。這僅作為一個用于直接操作子組件的“逃生艙”——你應(yīng)該避免在模板或計算屬性中訪問$refs
簡易的狀態(tài)管理方案
如果多個組件之間要共享狀態(tài)(數(shù)據(jù)),使用上面的方式雖然可以實現(xiàn),但是比較麻煩,而且多個組件之間互相傳值很難跟蹤數(shù)據(jù)的變化,如果出現(xiàn)問題很難定位問題。
當遇到多個組件需要共享狀態(tài)的時候,典型的場景:購物車。我們?nèi)绻褂蒙鲜龅姆桨付疾缓线m,會遇到以下問題:
- 多個視圖依賴同一狀態(tài)
- 來自不同視圖的行為需要變更同一狀態(tài)
對于問題一,傳參的方法對于多層嵌套的組件將會非常繁瑣,并且對于兄弟組件間的狀態(tài)傳遞無能為力。
對于問題二,我們經(jīng)常會采用父子組件直接引用或者通過事件來變更和同步狀態(tài)的多份拷貝。以上的這些模式非常脆弱,通常會導致無法維護的代碼。
因此,我們?yōu)槭裁床话呀M件的共享狀態(tài)抽取出來,以一個全局單例模式管理呢?在這種模式下,我們的組件樹構(gòu)成了一個巨大的“視圖”,不管在樹的哪個位置,任何組件都能獲取狀態(tài)或者觸發(fā)行為!
通過定義和隔離狀態(tài)管理中的各種概念并通過強制規(guī)則維持視圖和狀態(tài)間的獨立性,我們的代碼將會變得更結(jié)構(gòu)化且易維護。
我們可以把多個組件的狀態(tài),或者整個程序的狀態(tài)放到一個集中的位置存儲,并且可以檢測到數(shù)據(jù)的更改。你可能已經(jīng)想到了 Vuex。
這里我們先以一種簡單的方式來實現(xiàn)
- 首先創(chuàng)建一個共享的倉庫 store 對象
- 把共享的倉庫 store 對象,存儲到需要共享狀態(tài)的組件的 data 中
接著我們繼續(xù)延伸約定,組件不允許直接變更屬于 store 對象的 state,而應(yīng)執(zhí)行 action 來分發(fā)(dispatch) 事件通知 store 去改變,這樣最終的樣子跟 Vuex 的結(jié)構(gòu)就類似了。這樣約定的好處是,我們能夠記錄所有 store 中發(fā)生的 state 變更,同時實現(xiàn)能做到記錄變更、保存狀態(tài)快照、歷史回滾/時光旅行的先進的調(diào)試工具。
Vuex回顧
什么是Vuex
Vuex 是一個專為 Vue.js 應(yīng)用程序開發(fā)的狀態(tài)管理模式。它采用集中式存儲管理應(yīng)用的所有組件的狀態(tài),并以相應(yīng)的規(guī)則保證狀態(tài)以一種可預(yù)測的方式發(fā)生變化。Vuex 也集成到 Vue 的官方調(diào)試工具 devtools extension,提供了諸如零配置的 time-travel 調(diào)試、狀態(tài)快照導入導出等高級調(diào)試功能。
- Vuex 是專門為 Vue.js 設(shè)計的狀態(tài)管理庫
- 它采用集中式的方式存儲需要共享的數(shù)據(jù)
- 從使用角度,它就是一個 JavaScript 庫
- 它的作用是進行狀態(tài)管理,解決復(fù)雜組件通信,數(shù)據(jù)共享
什么情況下使用Vuex
官方文檔:
Vuex 可以幫助我們管理共享狀態(tài),并附帶了更多的概念和框架。這需要對短期和長期效益進行權(quán)衡。
如果您不打算開發(fā)大型單頁應(yīng)用,使用 Vuex 可能是繁瑣冗余的。確實是如此——如果您的應(yīng)用夠簡單,您最好不要使用 Vuex。一個簡單的 store 模式就足夠您所需了。但是,如果您需要構(gòu)建一個中大型單頁應(yīng)用,您很可能會考慮如何更好地在組件外部管理狀態(tài),Vuex 將會成為自然而然的選擇。引用 Redux 的作者 Dan Abramov 的話說就是:Flux 架構(gòu)就像眼鏡:您自會知道什么時候需要它。
當你的應(yīng)用中具有以下需求場景的時候:
- 多個視圖依賴于同一狀態(tài)
- 來自不同視圖的行為需要變更同一狀態(tài)
建議符合這種場景的業(yè)務(wù)使用 Vuex 來進行數(shù)據(jù)管理,例如非常典型的場景:購物車。
注意:Vuex 不要濫用,不符合以上需求的業(yè)務(wù)不要使用,反而會讓你的應(yīng)用變得更麻煩。
核心概念回顧
- Store:倉庫,store是使用Vuex應(yīng)用程序的核心,每一個應(yīng)用僅有一個store。store是一個容器,包含應(yīng)用中的大部分狀態(tài),當然我們不能直接改變store中的應(yīng)用狀態(tài),我們需要通過提交mutation的方式改變狀態(tài)。
- State:就是狀態(tài),保存在store中。因為store是唯一的,所以state狀態(tài)也是惟一的,稱為單一狀態(tài)樹。但是所有的狀態(tài)都保存在state中的話,會讓程序難以維護,可以通過后續(xù)的模塊解決該問題。
- 注意:這里的state狀態(tài)是響應(yīng)式的
- Getter:getter就像是Vuex中的計算屬性,方便從一個屬性派生出其他的值,它內(nèi)部可以對計算的結(jié)果進行緩存,只有當內(nèi)部依賴的state狀態(tài)發(fā)生改變時才會重新計算。
- Mutation:state狀態(tài)的變化必須要通過提交mutation來完成
- Action:action和mutation類似,不同的是action可以進行異步的操作,內(nèi)部改變狀態(tài)的時候都需要提交mutation
- Module:由于使用單一狀態(tài)樹,應(yīng)用的所有狀態(tài)會集中到一個比較大的對象上來,當應(yīng)用變得十分復(fù)雜時,,store對象就有可能編的相當臃腫。為了解決以上問題,Vuex允許我們將store分隔成模塊,每個模塊擁有自己的state、mutation、action、getter甚至是嵌套的子模塊
示例演示
使用vue create vuex-demo創(chuàng)建包含router和vuex的空項目
基本結(jié)構(gòu)
src/store/index.js
import Vue from 'vue' // 1.導入Vuex import Vuex from 'vuex'// 2.注冊Vuex Vue.use(Vuex)export default new Vuex.Store({state: {},mutations: {},actions: {},modules: {} })src/main.js
import Vue from 'vue' import store from './store'Vue.config.productionTip = falsenew Vue({store, // 3.注入$store到Vue實例render: h => h(App) }).$mount('#app')State
Vuex使用單一狀態(tài)樹,用一個對象就包含了全部的應(yīng)用層級狀態(tài)。
使用mapState簡化State在視圖中的使用,mapState返回計算屬性
mapState有兩種使用方式:
-
接收數(shù)組參數(shù)
// 該方式是Vuex提供的,所以使用前需要先導入 import { mapState } from 'vuex' // mapState返回名稱為count和msg的計算屬性 // 在模板中直接使用count和msg computed: {...mspState(['count', 'msg']) }使用數(shù)組參數(shù)
<h1>Vuex - Demo</h1> <!-- count: {{ $store.state.count }}<br>--> <!-- msg: {{ $store.state.msg }}-->count: {{ count }}<br> msg: {{ msg }} -
接受對象參數(shù)
如果當前視圖中已經(jīng)有了count和msg,如果使用上述方式的話會有命名沖突,解決的方式:
import {mapState} from 'vuex'export default {computed: {// count: state => state.count// ...mapState(['count', 'msg'])...mapState({num: 'count', message: 'msg'}) // 當store中存在count和msg時,使用對象參數(shù)重命名count和msg} }使用對象參數(shù)
<h1>Vuex - Demo</h1> count: {{ num }}<br> msg: {{ message }}
Getter
Getter就是store中的計算屬性,使用mapGetter簡化視圖中的使用
App.vue
import {mapGetters, mapState} from 'vuex'export default {computed: {// count: state => state.count// ...mapState(['count', 'msg'])...mapState({num: 'count', message: 'msg'}),...mapGetters(['reverseMsg'])} }使用
<h2>Getter</h2> <!-- reverseMsg: {{ $store.getters.reverseMsg }}--> reverseMsg: {{ reverseMsg }}src/store/index.js
import Vue from 'vue' import Vuex from 'vuex'Vue.use(Vuex)export default new Vuex.Store({state: {count: 1,msg: 'Hello Vuex'},getters: {reverseMsg(state) {return state.msg.split('').reverse().join('')}},mutations: {},actions: {},modules: {} })Mutation
更改Vuex的store中的狀態(tài)的唯一方法是提交mutation。Vuex中的mutation非常類似于事件:每個mutation都有一個字符串的事件類型(type)和一個回調(diào)函數(shù)(handler)。這個回調(diào)函數(shù)就是我們實際進行狀態(tài)更改的地方,并且它會接受state作為第一個參數(shù)。
使用Mutation改變狀態(tài)的好處是:集中的一個位置對狀態(tài)修改,不管在什么地方修改,都可以追蹤到狀態(tài)的修改。可以實現(xiàn)高級的time-travel調(diào)試功能
App.vue
import {mapGetters, mapMutations, mapState} from 'vuex'export default {computed: {// count: state => state.count// ...mapState(['count', 'msg'])...mapState({num: 'count', message: 'msg'}),...mapGetters(['reverseMsg']),},methods: {...mapMutations(['increate'])} }使用
<h2>Mutation</h2> <!-- <button @click="$store.commit('increate', 2)">Mutation</button>--> <button @click="increate(3)">Mutation</button>src/store/index.js
import Vue from 'vue' import Vuex from 'vuex'Vue.use(Vuex)export default new Vuex.Store({state: {count: 1,msg: 'Hello Vuex'},getters: {reverseMsg(state) {return state.msg.split('').reverse().join('')}},mutations: {increate(state, payload) {state.count += payload}},actions: {},modules: {} })Action
Action類似于mutation,不同在于:
- Action提交的是mutation,而不是直接變更狀態(tài)
- Action可以包含任意異步操作
App.vue
import {mapActions, mapGetters, mapMutations, mapState} from 'vuex'export default {computed: {// count: state => state.count// ...mapState(['count', 'msg'])...mapState({num: 'count', message: 'msg'}),...mapGetters(['reverseMsg']),},methods: {...mapMutations(['increate']),...mapActions(['increateAsync'])} }使用
<h2>Action</h2> <!-- <button @click="$store.dispatch('increateAsync', 5)">Action</button>--> <button @click="increateAsync(5,1)">Action</button>src/store/index.js
import Vue from 'vue' import Vuex from 'vuex'Vue.use(Vuex)export default new Vuex.Store({state: {count: 1,msg: 'Hello Vuex'},getters: {reverseMsg(state) {return state.msg.split('').reverse().join('')}},mutations: {increate(state, payload) {state.count += payload}},actions: {increateAsync(context, payload) {console.log(payload)setTimeout(() => {context.commit('increate', payload)}, 2000)}},modules: {} })Module
由于使用單一狀態(tài)樹,應(yīng)用的所有狀態(tài)會集中到一個比較大的對象。當應(yīng)用變得非常復(fù)雜時,store 對象就有可能變得相當臃腫。
為了解決以上問題,Vuex 允許我們將 store 分割成模塊(module)。每個模塊擁有自己的 state、mutation、action、getter、甚至是嵌套子模塊。在案例中體會 Module 的使用。
目錄結(jié)構(gòu):
嚴格模式
之前在介紹核心概念時說過,所有的狀態(tài)變更必須通過提交mutation,但是這僅僅是一個約定。如果你想的話,可以在組建中隨時獲取$store.state.msg,對它進行修改。從語法層面來說,這是沒有問題的,但是這樣操作破壞了Vuex的約定。如果在組件中直接修改state,那在dev-tools中無法追蹤狀態(tài)的變更。開啟嚴格模式之后,如果在組件中直接修改state狀態(tài),會拋出錯誤。演示如下:
store/index.js中添加strict: true
在App.vue中添加如下代碼,點擊按鈕直接修改store中state.msg的值
打開瀏覽器進行測試,發(fā)現(xiàn)$store.state.msg的值確實被修改了,但是console中會拋出異常。
需要注意的是:不要再生產(chǎn)模式下開啟嚴格模式,嚴格模式會深度檢查狀態(tài)樹,來檢查不合規(guī)的狀態(tài)改變,會影響性能。可以再開發(fā)環(huán)境中啟用嚴格模式,在生產(chǎn)模式下關(guān)閉嚴格模式。調(diào)整后的代碼:
- 當npm run serve時,process.env.NODE_ENV為development開發(fā)環(huán)境;
- 當npm run build時,process.env.NODE_ENV是production。這樣就可以根據(jù)環(huán)境來動態(tài)的設(shè)置嚴格模式。
購物車案例
接下來我們通過一個購物車案例來演示 Vuex 在項目中的使用方式,首先把購物車的項目模板下載下來。
模板地址
案例演示
server.js,在訪問數(shù)據(jù)時,必須先使用node server.js啟動server接口
const express = require('express') const cors = require('cors') const app = express()app.use(cors())const hostname = '127.0.0.1' const port = 3000const _products = [{ id: 1, title: 'iPad Pro', price: 500.01 },{ id: 2, title: 'H&M T-Shirt White', price: 10.99 },{ id: 3, title: 'Charli XCX - Sucker CD', price: 19.99 } ]app.use(express.json())app.get('/products', (req, res) => {res.status(200).json(_products) })app.post('/checkout', (req, res) => {res.status(200).json({success: Math.random() > 0.5}) })app.listen(port, hostname, () => {console.log(`Server is running at http://${hostname}:${port}/`) })功能列表
- 商品列表組件
- 商品列表中彈出框組件(購物車彈出框)
- 購物車列表組件
商品列表
商品列表功能
-
Vuex中創(chuàng)建兩個模塊,分別用來記錄商品列表和購物車的狀態(tài),stroe的結(jié)構(gòu):
store--modulescart.jsproducts.jsindex.js -
products模塊,store/modules/products.js
- store/index.js中注冊products.js模塊
- views/products.vue中實現(xiàn)商品列表的功能
添加購物車
- cart 模塊實現(xiàn)添加購物車功能,store/modules/cart.js
- store/index.js 中注冊 cart 模塊
- view/products.vue 中實現(xiàn)添加購物車功能
- 測試,通過 vue-devtools 觀察數(shù)據(jù)的變化
商品列表-彈出購物車窗口
購物車列表
- components/pop-cart.vue中展示購物車列表
刪除
- cart 模塊實現(xiàn)從購物車刪除的功能,store/modules/cart.js
- components/pop-cart.vue 中實現(xiàn)刪除功能
小計
- cart 模塊實現(xiàn)統(tǒng)計總數(shù)和總價,store/modules/cart.js
- components/pop-cart.vue 中顯示徽章和小計
購物車
購物車列表
<template><el-popoverwidth="350"trigger="hover"><el-table :data="cartProducts" size="mini"><el-table-column property="title" width="130" label="商品"></el-table-column><el-table-column property="price" label="價格"></el-table-column><el-table-column property="count" width="50" label="數(shù)量"></el-table-column><el-table-column label="操作"><template v-slot="scope"><el-button @click="deleteFromCart(scope.row.id)" size="mini">刪除</el-button></template></el-table-column></el-table><div><p>共 {{ totalCount }} 件商品 共計¥{{ totalPrice }}</p><el-button size="mini" type="danger" @click="$router.push({ name: 'cart' })">去購物車</el-button></div><el-badge :value="totalCount" class="item" slot="reference"><el-button type="primary">我的購物車</el-button></el-badge></el-popover> </template><script> import { mapState, mapGetters, mapMutations } from 'vuex' export default {name: 'PopCart',computed: {...mapState('cart', ['cartProducts']),...mapGetters('cart', ['totalCount', 'totalPrice'])},methods: {...mapMutations('cart', ['deleteFromCart'])} } </script><style></style>全選功能
- cart 模塊實現(xiàn)更新商品的選中狀態(tài),store/modules/cart.js
- views/cart.vue,實現(xiàn)全選功能
- 使用事件拋出一個值
數(shù)組文本框
- cart 模塊實現(xiàn)更新商品數(shù)量,store/modules/cart.js
- views/cart.vue,實現(xiàn)數(shù)字文本框功能
刪除
小計
- cart 模塊實現(xiàn)統(tǒng)計選中商品價格和數(shù)量,store/modules/cart.js
- views/cart.vue,實現(xiàn)小計
本地存儲
Vuex插件
- Vuex的插件就是一個函數(shù)
- 這個函數(shù)接受一個store的參數(shù)
- 這個函數(shù)內(nèi)可以注冊一個函數(shù),讓它可以在mutaions之后再執(zhí)行
- 就像在axios中的過濾器,在所有請求之后統(tǒng)一完成一件事
- mutation的結(jié)構(gòu)
- 如果想在cart模塊中的mutation之行結(jié)束之后再來調(diào)用調(diào)用,product模塊中不需要,可以使用mutation
- 注冊插件
-
最終實現(xiàn)
import Vue from 'vue' import Vuex from 'vuex' import products from './modules/products' import cart from './modules/cart'Vue.use(Vuex)const myPlugin = store => {// 當store初始化后調(diào)用// subscribe的作用是用來訂閱store中的mutation,會在每個mutation完成之后調(diào)用// 參數(shù):mutation、state// 如果想在cart模塊中的mutation之行結(jié)束之后再來調(diào)用調(diào)用,product模塊中不需要,可以使用mutationstore.subscribe((mutation, state) => {// 每次調(diào)用mutation之后調(diào)用// mutation的格式為 { type, payload }if (mutation.type.startsWith('cart/')) {//記錄到localStoragewindow.localStorage.setItem('cart-products', JSON.stringify(state.cart.cartProducts))}}) }export default new Vuex.Store({state: {},mutations: {},actions: {},modules: {cart,products},plugins: [myPlugin] })
Vuex模擬實現(xiàn)
回顧基礎(chǔ)示例,自己模擬實現(xiàn)一個Vuex實現(xiàn)同樣的功能
import Vue from 'vue' import Vuex from 'vuex'Vue.use(Vuex) export default new Vuex.Store({state: {count: 0,msg: 'Hello World'},getters: {reverseMsg(state) {return state.msg.split('').reverse().join('')}},mutations: {increate(state, payload) {state.count += payload.num}},actions: {increate(context, payload) {setTimeout(() => {context.commit('increate', {num: 5})}, 2000)}} })實現(xiàn)思路
- 實現(xiàn)install方法
- Vuex是Vue的一個插件,所以和模擬VueRouter類似,縣實現(xiàn)Vue插件約定的install方法
- 實現(xiàn)Store類
- 實現(xiàn)構(gòu)造函數(shù),接受options對象參數(shù)
- state的響應(yīng)式處理
- getter的實現(xiàn)
- commit、dispatch方法
install方法
let _Vue = null function install (Vue) {_Vue = Vue_Vue.mixin({beforeCreate () {if (this.$options.store) {Vue.prototype.$store = this.$options.store}}}) }Store類
let _Vue = nullclass Store {constructor(options) {const {state = {},getters = {},mutations = {},actions = {}} = optionsthis.state = _Vue.observable(state)// 此處不直接 this.getters = getters,是因為下面的代碼中要方法 getters 中的 key// 如果這么寫的話,會導致 this.getters 和 getters 指向同一個對象// 當訪問 getters 的 key 的時候,實際上就是訪問 this.getters 的 key 會觸發(fā) key 屬性的getter// 會產(chǎn)生死遞歸this.getters = Object.create(null)Object.keys(getters).forEach(key => {Object.defineProperty(this.getters, key, {get: () => getters[key](state)})})this._mutations = mutationsthis._actions = actions}commit(type, payload) {this._mutations[type](this.state, payload)}dispatch(type, payload) {this._actions[type](this, payload)}}// install方法可以接受兩個參數(shù),一個是Vue構(gòu)造函數(shù),另外一個是額外的選項,這里只需要Vue構(gòu)造函數(shù) function install(Vue) {_Vue = Vue_Vue.mixin({beforeCreate() {// 首先判斷當前Vue實例的$options中是否有store,如果是組件實例的話沒有store選項,就不需要做這件事if (this.$options.store) {// 這里注冊插件的時候會混入beforeCreate,當創(chuàng)建根實例的時候就會把$store注入到Vue實例上_Vue.prototype.$store = this.$options.store}}}) }export default {Store,install }使用自己實現(xiàn)的Vuex
src/store/index.js 中修改導入 Vuex 的路徑,測試
import Vuex from '../myvuex' // 注冊插件 Vue.use(Vuex)總結(jié)
以上是生活随笔為你收集整理的Vuex-状态管理(24)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 路由器里的DHCP是什么?如何开启路由器
- 下一篇: 807-C++多继承下,派生类对象有几张