前言
近期開發(fā)的移動(dòng)端項(xiàng)目直接上了 vue3 ,新特性 composition api 確實(shí)帶來了全新的開發(fā)體驗(yàn).開發(fā)者在使用這些特性時(shí)可以將高耦合的狀態(tài)和方法放在一起統(tǒng)一管理,并能視具體情況將高度復(fù)用的邏輯代碼單獨(dú)封裝起來,這對(duì)提升整體代碼架構(gòu)的健壯性很有幫助.
如今新啟動(dòng)的每個(gè)移動(dòng)端項(xiàng)目基本上都包含注冊(cè)登錄模塊,本次實(shí)踐過程中針對(duì)登錄注冊(cè)中的表單控件做了一些經(jīng)驗(yàn)上的總結(jié),通過抽離提取共性代碼來提升代碼的可維護(hù)性和開發(fā)效率.
接下來觀察一下美工同學(xué)提供的圖片.
注冊(cè)頁面
登錄頁面
忘記密碼頁面
修改密碼頁面
通過觀察上面幾張產(chǎn)品圖片,可以清晰看出構(gòu)成整個(gè)登錄注冊(cè)模塊的核心組件就是 input 輸入框.只要把輸入框組件開發(fā)完備,其他頁面直接引用就行了.
輸入框開發(fā)完了只實(shí)現(xiàn)了靜態(tài)頁面的展示,另外我們還要設(shè)計(jì)一套通用的數(shù)據(jù)校驗(yàn)方案應(yīng)用到各個(gè)頁面中的表單控件.
輸入框組件
從上面分析可知,輸入框組件是整個(gè)登錄注冊(cè)模塊的核心內(nèi)容.我們先看一下輸入框組件有哪幾種 UI 形態(tài).
形態(tài)一
左側(cè)有文字 +86 ,中間是輸入框,右側(cè)如果檢測(cè)到輸入框有數(shù)據(jù)輸入顯示叉叉圖標(biāo),如果沒有數(shù)據(jù)為空隱藏圖標(biāo).
形態(tài)二
左側(cè)只有一個(gè)輸入框,右側(cè)是文案.文案的內(nèi)容可能是驗(yàn)證碼,也可能是點(diǎn)擊驗(yàn)證碼后顯示的倒計(jì)時(shí)文案.
形態(tài)三
左側(cè)依舊只有一個(gè)輸入框,右側(cè)如果檢測(cè)到輸入框有內(nèi)容顯示叉叉圖標(biāo),如果內(nèi)容為空隱藏圖標(biāo).
布局
依據(jù)上面觀察而來的現(xiàn)象分析,我們?cè)O(shè)計(jì)這款 input 組件時(shí)可以將其分為左中右三部分.左側(cè)可能是文案,也可能是空.中間是一個(gè)輸入框.右側(cè)可能是文案也可能是叉叉圖標(biāo).
模板內(nèi)容如下:
<template> <div class="input"> <!--左側(cè),lt是左側(cè)內(nèi)容--> <span class="left-text"> {{ lt }} </span> <!--中間--> <input class="content" v-bind="$attrs" :value="value" @input="onChange" /> <!--右側(cè),rt判端是驗(yàn)證碼還是叉叉圖標(biāo)--> <div v-if="rt == "timer"" class="right-section"> {{ timerData.content }} <!--可能是"驗(yàn)證碼",也可能是倒計(jì)時(shí) --> </div> <div v-else-if="rt == "close"" class="right-section" > <van-icon name="close" /> <!--叉叉圖標(biāo)--> </div> </div> </template>
布局上將左中右的父級(jí)設(shè)置為 display:flex ,子級(jí)的三個(gè)元素全部設(shè)置成 display:inline-block 行內(nèi)塊模式,目的是為了讓左側(cè)和右側(cè)依據(jù)自身內(nèi)容自適應(yīng)寬度,而中間的 input 設(shè)置成 flex:1 充滿剩余的寬度.
理論上這樣的布局是可行的,但實(shí)踐中發(fā)現(xiàn)了問題.
Demo 效果圖如下:
右側(cè)持續(xù)增加寬度時(shí),中間 input 由于默認(rèn)寬度的影響導(dǎo)致讓右側(cè)向外溢出了,這并不是我們想要的.
解決這個(gè)問題的辦法很簡(jiǎn)單,只需要將中間 input 的 width 設(shè)置為 0 即可,如下便達(dá)到了我們想要的效果.
v-model
外部頁面引用上述封裝的組件結(jié)構(gòu)如下:
<InputForm lt="+86" <!--左側(cè)顯示+86--> rt="close" <!--右側(cè)顯示叉叉圖標(biāo)--> placeholder="請(qǐng)輸入手機(jī)號(hào)碼" />
外部頁面創(chuàng)建了一個(gè)表單數(shù)據(jù) form_data 如下,但希望能通過 v-model 的形式將 form_data 的數(shù)據(jù)與子組件輸入框的值建立雙向數(shù)據(jù)綁定.
const form_data = reactive({ number_number: "", //用戶名 password: "", //密碼 ppassword: "", //重復(fù)密碼 captcha: "", //驗(yàn)證碼 })
在 vue3 實(shí)現(xiàn) v-model 非常簡(jiǎn)便,在父組件中使用 v-model:xx 完成綁定,這里的 xx 對(duì)應(yīng)著子組件要綁定的狀態(tài)名稱,如下所示.
<InputForm lt="+86" <!--左側(cè)顯示+86--> rt="close" <!--右側(cè)顯示叉叉圖標(biāo)--> placeholder="請(qǐng)輸入手機(jī)號(hào)碼" v-model:value="form_data.password" />
接下來子組件里首先聲明要綁定的屬性 value ,并監(jiān)聽輸入框的 oninput事件 .代碼如下:
<template> <div class="input"> ... <input class="content" v-bind="$attrs" :value="value" @input="onChange" /> ... </div> </template> export default defineComponent({ props: { lt:String, rt: String, value: String }, setup(props, context) { const onChange = (e:KeyboardEvent) => { const value = (e.target as HTMLInputElement).value; context.emit("update:value",value); }; return { onChange } } })
oninput事件 的回調(diào)函數(shù)將獲取到的值使用 context.emit("update:value",value) 返回回去.
其中 update:value 里前面部分 update: 為固定寫法,后面填寫要建立雙向綁定的狀態(tài)名稱.如此一來就輕易的完成了 v-model 的綁定.
數(shù)據(jù)校驗(yàn)
一般來說只要頁面上涉及到表單控件(比如輸入框),那么就要針對(duì)相應(yīng)的值做數(shù)據(jù)校驗(yàn).如果按照原始的方法,當(dāng)用戶點(diǎn)擊按鈕, js 接受響應(yīng)依次獲取每個(gè)表單項(xiàng)的值一一校驗(yàn).
這樣的做法當(dāng)然可以實(shí)現(xiàn)功能,但并不高效和精簡(jiǎn).因?yàn)楹芏囗撁娑家鲂r?yàn),大量的校驗(yàn)邏輯是重復(fù)書寫的.
我們接下來設(shè)計(jì)一套通用的校驗(yàn)方案,將那些可以復(fù)用的邏輯代碼都封裝起來,并且能夠快速的應(yīng)用到每個(gè)頁面上,提升開發(fā)效率.
依注冊(cè)頁面為例,模板代碼如下.創(chuàng)建四個(gè)輸入框組件:手機(jī)號(hào),手機(jī)驗(yàn)證碼,密碼和確認(rèn)密碼.最后面再放置一個(gè)注冊(cè)按鈕.(為了看起來更清晰,下面的代碼將所有 ts 類型刪除)
<Form ref="form" :rules="rules"> <InputForm lt="+86" rt="close" v-model:value="form_data.number_number" placeholder="請(qǐng)輸入手機(jī)號(hào)碼" propName="number_number" /> <InputForm rt="timer" v-model:value="form_data.captcha" placeholder="請(qǐng)輸入手機(jī)驗(yàn)證碼" propName="captcha" /> <InputForm rt="close" v-model:value="form_data.password" placeholder="請(qǐng)輸入密碼" type="password" propName="password" /> <InputForm rt="close" v-model:value="form_data.ppassword" placeholder="請(qǐng)輸入確認(rèn)密碼" type="password" propName="ppassword" /> <Button text="注 冊(cè)" @sub="onSubmmit" /> <!--注冊(cè)按鈕--> </Form>
在借鑒了一些其他優(yōu)秀框架的表單實(shí)踐后,我們首先是在最外層增加了一個(gè)組件 Form ,其次給每個(gè)輸入框組件增加了一個(gè)屬性 propName .這個(gè)屬性是配合 rules 一起使用的, rules 是手動(dòng)定義的校驗(yàn)規(guī)則,當(dāng)它傳遞給 Form 組件后,子組件(輸入框組件)就能通過 propName 屬性拿到屬于它的校驗(yàn)規(guī)則.
整體的實(shí)現(xiàn)思路可以從頭串聯(lián)一遍.首先是前端開發(fā)者定義好當(dāng)前頁面的校驗(yàn)規(guī)則 rules ,并將它傳遞給 Form 組件. Form 組件接受到后會(huì)將校驗(yàn)規(guī)則分發(fā)給它的每個(gè)子組件(輸入框組件).子組件拿到校驗(yàn)規(guī)則后就能夠針對(duì)輸入框的值做相應(yīng)的數(shù)據(jù)校驗(yàn).
當(dāng)用戶點(diǎn)擊注冊(cè)按鈕時(shí),點(diǎn)擊事件會(huì)獲取 Form 組件的實(shí)例,并運(yùn)行它的 validate 方法,此時(shí) Form 組件就會(huì)對(duì)它旗下的每個(gè)子組件做一輪數(shù)據(jù)校驗(yàn).一旦所有校驗(yàn)成功了, validate 方法返回 true .存在一個(gè)校驗(yàn)沒通過, validate 方法就返回 false ,并彈出錯(cuò)誤信息.
注冊(cè)頁面邏輯如下:
export default defineComponent({ components: { InputForm, //輸入框 Button, //注冊(cè)按鈕 Form, //Form組件 }, setup(props) { const form_data = ...; //省略 const rules = ...; //獲取最外層Form組件的實(shí)例 const form = ref(null); const onSubmmit = ()=>{ if (!form.value || !form.value.validate()) { return false; } //校驗(yàn)通過了,可以請(qǐng)求注冊(cè)接口了 } return { form, rules, onSubmmit, form_data }; }, });
定義一個(gè)變量 form ,用它來獲取 Form 表單的實(shí)例.模板上 <Form ref="form" :rules="rules"> 只需要加上一個(gè) ref 屬性就可以了.
用戶點(diǎn)擊注冊(cè)按鈕觸發(fā) onSubmmit 函數(shù),因?yàn)?form 是使用 ref 創(chuàng)建的變量,獲取值要調(diào)用 .value .運(yùn)行 form.value.validate() 函數(shù),就能讓 Form 表單下面的每一個(gè)子組件開始執(zhí)行校驗(yàn)邏輯,如果全部通過就會(huì)返回 true ,存在一個(gè)沒通過返回 false .
從上面分析可知, Form 控件只對(duì)外暴露一個(gè) validate 函數(shù),通過調(diào)用該函數(shù)就能知道校驗(yàn)是否通過.那么 validate 如何知道該采用什么規(guī)則來校驗(yàn)?zāi)?所以我們要先設(shè)計(jì)一套校驗(yàn)的規(guī)則 rules ,把它傳給 Form 組件,那么它內(nèi)部的 validate 函數(shù)就能采用規(guī)則來執(zhí)行校驗(yàn).
rules設(shè)計(jì)
rules 是一個(gè)對(duì)象,例如上述注冊(cè)頁面的 rules 定義如下:
const rules = { number_number:[{ type: "required", msg:"請(qǐng)輸入正確的手機(jī)號(hào)" } "phone" ], captcha:[ { type: "required", msg: "驗(yàn)證碼不能為空" } ], password: [ { type: "required", msg: "請(qǐng)輸入密碼", }, { type: "minLength", params: 6, msg: "密碼長(zhǎng)度不能小于6位", }, ], ppassword:[ { type: "custome", callback() { if (form_data.password !== form_data.ppassword) { return { flag: false, msg: "兩次輸入的密碼不一致", }; } return { flag: true, }; }, }, ] }
我們定義的 rules 是一個(gè)鍵值對(duì)形式的對(duì)象. key 對(duì)應(yīng)著模板上每個(gè)輸入框組件的 propName ,值是一個(gè)數(shù)組,對(duì)應(yīng)著該輸入框組件要遵守的規(guī)則.
現(xiàn)在細(xì)致的看下每個(gè)對(duì)象下的值的構(gòu)成,值之所以組織成數(shù)組形式,是因?yàn)檫@樣可以給輸入框增加多條規(guī)則.而規(guī)則對(duì)應(yīng)著兩種形式,一種是對(duì)象,另外一種是字符串.
字符串很好理解,比如上面的 number_number 屬性,它就對(duì)應(yīng)著字符串 phone .這條規(guī)則的意義就是該輸入框的值要遵守手機(jī)號(hào)的規(guī)則.當(dāng)然字符串如果填 email ,那就要當(dāng)做郵箱來校驗(yàn).
規(guī)則如果為對(duì)象,那么它包含了以下幾個(gè)屬性:
{ type, // 類型 msg, //自定義的錯(cuò)誤信息 params, //傳過來的參數(shù)值 比如 {type:"minLength",params:6},值最小長(zhǎng)度不能低于6位 callback //自定義校驗(yàn)函數(shù) }
type 是校驗(yàn)類型,它如果填 required ,表示是必填項(xiàng).如果用戶沒填,點(diǎn)擊注冊(cè)按鈕提交時(shí)就會(huì)報(bào)出 msg 定義的錯(cuò)誤信息.
另外 type 還可以填 minLength 或者 maxLength 用來限定值的長(zhǎng)度,那到底限定為幾位呢,可以通過 params 傳遞過去.
最后 type 還可以填 custome ,那么就是讓開發(fā)者自己來定義該輸入框的校驗(yàn)邏輯函數(shù) callback .該函數(shù)要求最后返回一個(gè)帶有 flag 屬性的對(duì)象,屬性 flag 為布爾值,它會(huì)告訴校驗(yàn)系統(tǒng)本次校驗(yàn)是成功還是失敗.
Form表單
rules 被定義好后傳給 Form 組件, Form 組件需要將校驗(yàn)邏輯分發(fā)給它的子組件.讓其每個(gè)子組件都負(fù)責(zé)生成自己的校驗(yàn)函數(shù).
<!-- 表單組件 --> <template> <div class="form"> <slot></slot> </div> </template> <script lang="ts"> import { ref, provide } from "vue"; export default defineComponent({ name: "Form", props:{ rules:Object }, setup(props) { ...//省略 provide("rules",props.rules); // 將校驗(yàn)規(guī)則分發(fā)下去 const validate = ()=>{ //向外暴露的校驗(yàn)函數(shù) } return { validate } } }) </script>
從上面結(jié)構(gòu)可以看出, Form 組件模板提供了一個(gè)插槽的作用,在邏輯代碼里利用 provide 將校驗(yàn)規(guī)則傳給后代,并向外暴露一個(gè) validate 函數(shù).
子組件生成校驗(yàn)函數(shù)
這一次又回到了登錄注冊(cè)模塊的核心組件 InputForm ,我們現(xiàn)在要給該輸入框組件添加校驗(yàn)邏輯.
import { inject,onMounted } from "vue"; ... setup(props, context) { const rules = inject("rules"); const rule = rules[props.propName];// 通過propName拿到校驗(yàn)規(guī)則 const useValidate = () => { const validateFn = getValidate(rule); // 獲取校驗(yàn)函數(shù) const execValidate = () => { return validateFn(props.value); //執(zhí)行校驗(yàn)函數(shù)并返回校驗(yàn)結(jié)果 }; onMounted(() => { const Listener = inject("collectValidate"); if (Listener) { Listener(execValidate); } }); }; useValidate(); //初始化校驗(yàn)邏輯 ... }
rules 結(jié)構(gòu)類似如下.通過 inject 和 propName 可以拿到 Form 分發(fā)給該輸入框要執(zhí)行的規(guī)則 rule .
{ captcha:[{ type: "required", msg: "驗(yàn)證碼不能為空" }], password:[{ type: "required", msg: "請(qǐng)輸入密碼", }] }
再將規(guī)則 rule 傳遞給 getValidate 函數(shù)(后面會(huì)講)獲取校驗(yàn)函數(shù) validateFn .校驗(yàn)函數(shù) validateFn 傳入輸入框的值就能返回校驗(yàn)結(jié)果.在這里把 validateFn 封裝了一層賦予 execValidate 給外部使用.
在上面的代碼中我們還看到了 onMounted 包裹的邏輯代碼.當(dāng)組件掛載完畢后,使用 inject 拿到 Form 組件傳遞下來的一個(gè)函數(shù) Listener ,并將校驗(yàn)函數(shù) execValidate 作為參數(shù)傳遞進(jìn)去執(zhí)行.
我們?cè)倩氐较旅娲a中的 Form 組件,看一下 Listener 是一個(gè)什么樣的函數(shù).
setup(props) { const list = ref([]);//定義一個(gè)數(shù)組 const listener = (fn) => { list.value.push(fn); }; provide("collectValidate", listener); //將監(jiān)聽函數(shù)分發(fā)下去 //驗(yàn)證函數(shù) const validate = (propName) => { const array = list.value.map((fn) => { return fn(); }); const one = array.find((item) => { return item.flag === false; }); if (one && one.msg) { //驗(yàn)證不通過 Alert(one.msg);//彈出錯(cuò)誤提示 return false; } else { return true; } }; ...
從上面可以看出, Form 組件將 listener 函數(shù)分發(fā)了下去.而子組件在 onMounted 的生命周期鉤子里,獲取到分發(fā)下來的 listener 函數(shù),并將子組件內(nèi)部定義的校驗(yàn)函數(shù) execValidate 作為參數(shù)傳遞進(jìn)去執(zhí)行.
這樣一來就可以確保每個(gè)子組件一旦掛載完畢就會(huì)把自己的校驗(yàn)函數(shù)傳遞給 Form 組件中的 list 收集.而 Form 組件的 validate 方法只需要循環(huán)遍歷 list ,就可以依次執(zhí)行每個(gè)子組件的校驗(yàn)函數(shù).如果都校驗(yàn)通過了,給外部頁面返回 true .存在一個(gè)不通過,彈出錯(cuò)誤提示返回 false .
走到這里整個(gè)校驗(yàn)的流程已經(jīng)打通了. Form 首先向子組件分發(fā)校驗(yàn)規(guī)則,子組件獲取規(guī)則生成自己的校驗(yàn)函數(shù),并且在其掛載完畢后將校驗(yàn)函數(shù)再返回給 Form 收集起來.這個(gè)時(shí)候 Form 組件向外暴露的 validate 函數(shù)就可以實(shí)現(xiàn)針對(duì)所有表單控件的數(shù)據(jù)校驗(yàn).
接下來最后一步研究子組件如果通過規(guī)則來生成自己的校驗(yàn)函數(shù).
校驗(yàn)
首先編寫一個(gè)管理校驗(yàn)邏輯的類 Validate .代碼如下.我們可以不斷的根據(jù)新需求擴(kuò)充該類的方法,比如另外再增加 email 或者 maxLength 方法.
class Validate { constructor() {} required(data) { //校驗(yàn)是否為必填 const msg = "該信息為必填項(xiàng)"; //默認(rèn)錯(cuò)誤信息 if (data == null || (typeof data === "string" && data.trim() === "")) { return { flag:false, msg } } return { flag:true } } //校驗(yàn)是否為手機(jī)號(hào) phone(data) { const msg = "請(qǐng)?zhí)顚懻_的手機(jī)號(hào)碼"; //默認(rèn)錯(cuò)誤信息 const flag = /^1[3456789]d{9}$/.test(data); return { msg, flag } } //校驗(yàn)數(shù)據(jù)的最小長(zhǎng)度 minLength(data, { params }) { let minLength = params; //最小為幾位 if (data == null) { return { flag:false, msg:"數(shù)據(jù)不能為空" } } if (data.trim().length >= minLength) { return {flag:true}; } else { return { flag:false, msg:`數(shù)據(jù)最小長(zhǎng)度不能小于${minLength}位` } } } }
Validate 類定義的所有方法中,第一個(gè)參數(shù) data 是被校驗(yàn)的值,第二個(gè)參數(shù)是在頁面定義每條 rule 中的規(guī)則.形如 {type: "minLength", params: 6, msg: "密碼長(zhǎng)度不能小于6位"} .
Validate 類中每個(gè)方法最終的返回的數(shù)據(jù)結(jié)構(gòu)形如 {flag:true,msg:""} .結(jié)果中 flag 就來標(biāo)識(shí)校驗(yàn)是否通過, msg 為錯(cuò)誤信息.
校驗(yàn)類 Validate 提供了各種各樣的校驗(yàn)方法,接下來運(yùn)用一個(gè)單例模式生成該類的一個(gè)實(shí)例,將實(shí)例對(duì)象應(yīng)用到真實(shí)的校驗(yàn)場(chǎng)景中.
const getInstance = (function(){ let _instance; return function(){ if(_instance == null){ _instance = new Validate(); } return _instance; } })()
通過調(diào)用 getInstance 函數(shù)就可以得到單例的 Validate 實(shí)例對(duì)象.
輸入框組件通過給 getValidate 函數(shù)傳入一條 rule ,就能返回該組件需要的校驗(yàn)函數(shù).接下來看一下 getValidate 函數(shù)是如何通過 rule 來生成校驗(yàn)函數(shù)的,代碼如下:
/** * 生成校驗(yàn)函數(shù) */ export const getValidate = (rule) => { const ob = getInstance();//獲取 Validate類 實(shí)例對(duì)象 const fn_list = []; //將所有的驗(yàn)證函數(shù)收集起來 //遍歷rule數(shù)組,根據(jù)其類型獲取Validate類中的校驗(yàn)方法放到fn_list中收集起來 rule.forEach((item) => { if (typeof item === "string") { // 字符串類型 fn_list.push({ fn: ob[item], }); } else if (isRuleType(item)) { // 對(duì)象類型 fn_list.push({ //如果item.type為custome自定義類型,校驗(yàn)函數(shù)直接使用callback.否則從ob實(shí)例獲取 ...item, fn: item.type === "custome" ? item.callback : ob[item.type], }); } }); //需要返回的校驗(yàn)函數(shù) const execuate = (value) => { let flag = true, msg = ""; for (let i = 0; i < fn_list.length; i++) { const item = fn_list[i]; const result = item.fn.apply(ob, [value, item]);//item.fn對(duì)應(yīng)著Validate類定義的的校驗(yàn)方法 if (!result.flag) { //驗(yàn)證沒有通過 flag = false; msg = item.msg ? item.msg : result.msg;//是使用默認(rèn)的報(bào)錯(cuò)信息還是用戶自定義信息 break; } } return { flag, msg, }; }; return execuate; };
rule 的數(shù)據(jù)結(jié)構(gòu)形類似如下代碼.當(dāng)把 rule 傳入 getValidate 函數(shù),它會(huì)判端是對(duì)象還是字符串,隨后將其類型對(duì)應(yīng)的校驗(yàn)函數(shù)從 ob 實(shí)例中獲取存儲(chǔ)到 fn_list 中.
[ { type: "required", msg: "請(qǐng)輸入電話號(hào)碼" }, "phone" ]
getValidate 函數(shù)最終返回 execuate 函數(shù),此函數(shù)也正是輸入框組件得到的校驗(yàn)函數(shù).在輸入框組件里是可以拿到輸入框值的,如果將值傳給 execuate 方法調(diào)用.方法內(nèi)部就會(huì)遍歷之前緩存的校驗(yàn)函數(shù)列表 fn_list ,將值傳入每個(gè)校驗(yàn)方法運(yùn)行就能獲取該輸入框組件對(duì)當(dāng)前值的校驗(yàn)結(jié)果并返回回去.
以上校驗(yàn)的邏輯也已經(jīng)走通了.接下來不管是開發(fā)登錄頁,忘記密碼或者修改密碼的頁面,只需要使用 Form 組件和輸入框 InputForm 組件組織頁面結(jié)構(gòu),并寫一份當(dāng)前頁面的 rules 校驗(yàn)規(guī)則即可.剩下的所有校驗(yàn)細(xì)節(jié)和交互動(dòng)作全部交給了 Form 和 InputForm 內(nèi)部處理,這樣會(huì)極大的提升開發(fā)效率.
最終效果
總結(jié)
到此這篇關(guān)于vue3如何優(yōu)雅的實(shí)現(xiàn)移動(dòng)端登錄注冊(cè)模塊的文章就介紹到這了,更多相關(guān)vue3移動(dòng)端登錄注冊(cè)模塊內(nèi)容請(qǐng)搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!
原文鏈接:https://juejin.cn/post/6944346735540076575