那些好用的 VS Code 插件,究竟是如何提高編碼效率的?
在上一篇文章中我們已經對 VS Code 插件有了一個初步的認識與了解了,接下去我們就要“揭秘”一下市面上那些好用的 VS Code 插件究竟是如何幫我們提高工作效率的。

作者:HelloGitHub-小夏
一、從「整體」到「局部」
在開始正題之前,我們先回憶一下自己在 VS Code 上常用并且獲得編碼幸福度的是不是包含以下幾個點。
1.1、Snippet - 代碼片段
我們經??梢栽诓煌缶Y的文件還有文件里不同地方都看到代碼片段。輸入約定的幾個短短字符,就可以擁有一片或大或小的代碼段,解放雙手,節(jié)約時間,還能提升每日代碼量。
以下圖片來自插件:vue-vscode-snippets

1.2、代碼提示
解救“懶癌”的另一個常用“解藥”就是代碼提示了。可能平時你并不會注意到它,但是這個功能對于像我一樣單詞記憶水平一般且記不全所有枚舉值的人來說,簡直就是完美!
以下圖片來自插件:vue-helper



二、從「遠觀」到「實踐」
相信看了上面的例子,聰明的你已經深有體感啦。那接下去我們就直奔主題——實現上面所說的代碼片段和代碼提示功能!在這之前,我們先回到 VS Code 官網來看一下 Language Extensions API 以及他可以幫我們實現哪些。
首先 Visual Studio Code 通過語言擴展為不同的編程語言提供了智能編輯功能。雖然他不提供內置語言支持,但卻提供了一組支持豐富語言功能的 API。總的來說,VS Code 插件語言類相關的 API 分為兩大類,一類是「聲明語言特性」,一類是「程序語言特性」。前者主要通過在配置文件中定義,而后者通過在代碼中注冊而激活。

2.1、Snippet Completion
我們首先從「聲明語言特性」的代碼片段入手,看看僅僅一份配置文件是如何幫助我們提高工作效率的。
首先,我們在 package.json 里面增加一個 snippets 的入口,位于 contributes 的下級:
"contributes": {
"commands": [
{
"command": "test.helloGitHub",
"title": "Hello World"
},
{
"command": "test.button",
"title": "按鈕",
"icon": {
"light": "./media/light/preview.svg",
"dark": "./media/dark/preview.svg"
}
}
],
"menus": {
"editor/title": [
{
"command": "test.button",
"group": "navigation",
"when": "resourceLangId == javascript"
}
],
"editor/context": [
{
"command": "test.button",
"group": "navigation",
"when": "resourceLangId == javascript"
}
]
},
// 就是這里了?。?/span>
"snippets": [
{
"language": "javascript",
"path": "./snippets/javascript.json"
}
]
},
也就是這個位置,需要你手動新建一個文件夾和文件:

接下去就是重點、重點、重點。我們如何寫代碼片段的配置文件呢?如果你抱著強烈的好奇心,你可以前往官網查看這份詳細的教程。如果你想先看一眼簡單的配置該如何寫,那就隨著本文一起來看吧~
我們還是先「眼見為實」來看看下面的這份配置,會有什么奇妙的效果,先上配置代碼:
{
"forLoop": {
"prefix": "for",
"body": [
"for(let i = 0; i < ${1: array.length}); i++) {",
"\t$BLOCK_COMMENT_START HelloGitHub: 這里可以寫你的代碼 $BLOCK_COMMENT_END",
"}"
],
"description": "for 循環(huán)"
}
}
再來看看插件運行后的提示效果(一定要看仔細哪個是來自我們插件的哦):

最后我們自信的按下「Enter」回車鍵,就會看到一段代碼已經在我們的 js 文件里了
for(let i = 0; i < array.length); i++) {
/* HelloGitHub: 這里可以寫你的代碼 */
}
那我們就來回顧一下上面那份配置文件,究竟是如何生成這一份代碼的。
| 字段 | 含義 |
|---|---|
| forLoop | 是代碼段名稱。如果未提供 description,則通過 IntelliSense 顯示 |
| prefix | 定義一個或多個在 IntelliSense 中顯示摘要的觸發(fā)詞。 |
| body | 一或多個內容行,插入時將作為多行加入。換行符和嵌入的選項卡將根據插入代碼段的上下文進行格式化 |
| description | IntelliSense 顯示的代碼段的描述(非必填) |
首先這份配置會有一個名字即 forLoop ,是可以用戶隨意自定義的,我們可以看到它支持大小寫,加空格還有加橫杠,當然你或許要問它支不支持中文,那我可以告訴你:支持。但是并不建議這么寫,因為我們的眼界要放大嘛,走向國際(international)~
其次如果你想要匹配多個 prefix ,你可以修改你的代碼如下:
{
"forLoop": {
"prefix": ["for", "for-const"],
"body": [
"for(let i = 0; i < ${1:array.length}); i++) {",
"\t$BLOCK_COMMENT_START HelloGitHub: 這里可以寫你的代碼 $BLOCK_COMMENT_END", // \t 表示縮進,$BLOCK_COMMENT_START 和 $BLOCK_COMMENT_END 表示注釋的開始和結束。 // 和 /**/ 這兩種都支持
"}"
],
"description": "for 循環(huán)"
}
}
效果如下:

而且子字符串匹配是在前綴上執(zhí)行的,因此,在這種情況下,fc 可以匹配 for-const :

呈現的代碼片段:

1、Tabstops
控制編輯器光標在代碼內移動。你可以使用$1 ,$2 指定游標的位置,數字表示 Tab 鍵訪問的順序,出現相同的會被同步更新,$0 表示光標最后一個位置,當光標位于指定位置的情況下就會退出這個模式。
可能光看文字你會有點迷糊,那我們直接修改上面的 for 循環(huán):
{
"For Loop": {
"prefix": ["for", "for-const"],
"body": ["for (const ${2:element} of ${1:array}) {", "\t$0", "}"],
"description": "A for loop."
}
}
效果(用 tab 切換),順序是 $1 > $2 > $0 :

2、占位符
其實從前面的例子你應該就知道了占位符這個東西就是一個帶有默認值的語法,例如${1:foo} 。占位符文本將被插入和選擇,以便用戶可以輕松更改。并且占位符還可以進行嵌套,例如${1:another ${2:placeholder}}
3、選擇
當然啦對于喜歡偷懶的“我們”來說,能省一點時間是一點時間,因此占位符也可以讓我們只動動上下鍵就可以完成輸入。語法是用逗號分隔的值枚舉,觸發(fā)插入代碼段并選擇占位符后,選項將提示用戶選擇其中一個值。
修改我們的代碼如下:
{
"forLoop": {
"prefix": ["for", "for-const"],
"body": [
"for(let i = 0; i < ${1:array.length}); i++) {",
"\t$BLOCK_COMMENT_START HelloGitHub: 這里可以寫你的代碼 $BLOCK_COMMENT_END",
"\t\t${2|one,two,three|}",
"}"
],
"description": "for 循環(huán)"
},
}
效果:

4、變量
不知道你有沒有注意上面代碼中的一個小注釋:
{
...
"\t$BLOCK_COMMENT_START HelloGitHub: 這里可以寫你的代碼 $BLOCK_COMMENT_END", // \t 表示縮進,$BLOCK_COMMENT_START 和 $BLOCK_COMMENT_END 表示注釋的開始和結束。 // 和 /**/ 這兩種都支持
...
}
里面就用到了一個注釋的變量 $BLOCK_COMMENT_START 和 $BLOCK_COMMENT_END 。這個語法允許我們使用$name 或${name:default} 這兩種方式來設置插入的變量值。未設置變量時,將插入其默認值或空字符串。當變量未知(即未定義其名稱)時,將插入該變量的名稱,并將其轉換為占位符。從 VS Code 官網上可以看到所有支持的變量:

比如我們修改我們的例子如下:
{
"forLoop": {
"prefix": ["for", "for-const"],
"body": [
"for(let i = 0; i < ${1:array.length}); i++) {",
"\t$BLOCK_COMMENT_START HelloGitHub: 這里可以寫你的代碼 $BLOCK_COMMENT_END",
"\t\tconsole.log('choice', ${2|one,two,three|})",
"\t\tconsole.log('year', ${CURRENT_YEAR})",
"\t\treturn ${name:value}",
"}"
],
"description": "for 循環(huán)"
}
效果:

到這個例子為止你會發(fā)現我們的代碼片段變得越來越長,越來越豐富,也就是我們可以偷的懶就“越來越多”,不經意間就可以提高開發(fā)效率有沒有?
可能我的例子太簡單你沒有體感,那我們來看一個這個,應該有非常多的人眼熟:

對應的代碼配置其實也就是我們上面說的那幾個語法:
{
"hellogithub": {
"prefix": "swiper",
"body": [
"<swiper $0 indicator-dots=\"{{${1:indicatorDots}}}\" autoplay=\"{{${2:autoplay}}}\" interval=\"{{${3:interval}}}\" duration=\"{{${4:duration}}}\">",
"\t<block wx:for=\"{{${5:imgUrls}}}\">",
"\t\t<swiper-item>",
"\t\t\t<image src=\"{{${6:item}}}\" class=\"slide-image\" />",
"\t\t</swiper-item>",
"\t</block>",
"</swiper>$7"
],
"description": "滑塊視圖容器"
}
}
當然啦如果你有志于寫一個非常好用的代碼片段,上面這些可能還不能滿足你的話,可以學習一下 TextMate 更多高級的語法(上文中其實算是 TextMate 的基礎語法,言外之意就是比較常用而且看起來就很簡單易懂)。簡單的介紹一下 TextMate,它是 Mac下的著名的文本編輯器軟件,它可以根據一定的語言規(guī)則可以匹配文檔的結構,也可以按照一定的語法規(guī)則快速生成代碼片段。
2.2、Completion Provider
1、初窺
上面介紹了通過配置就可以完成的「聲明類語言特性」,讓我們再來看一個「程序類語言特性」—— registerCompletionItemProvider 。
我們首先看個圖,是不是也覺得是個“偷懶”神器呀!但是你有沒有疑惑過,為什么這個編輯器知道我們即將要寫的是什么?為什么它還可以給我們推薦寫什么?如果你覺得這是計算機時代智慧的結晶的話,那我也不能說你錯。那么今天,我們就親自來“揭秘”這個功能,可以用registerCompletionItemProvider 這個來實現。

接下去我們就進入代碼實現了,還記得上一篇文章的 extension.js 嗎?我們在這里加上這么一段代碼:
const completion = vscode.languages.registerCompletionItemProvider(
'javascript',
{
provideCompletionItems(document, position) {
const linePrefix = document.lineAt(position).text.substr(0, position.character);
if (!linePrefix.endsWith('hello.')) {
return undefined;
}
return [
new vscode.CompletionItem('HelloGitHub', vscode.CompletionItemKind.Property),
new vscode.CompletionItem('HelloWorld', vscode.CompletionItemKind.Property),
new vscode.CompletionItem('HelloPeople', vscode.CompletionItemKind.Property),
];
}
},
'.' // triggered whenever a '.' is being typed
);
context.subscriptions.push(completion);
然后先來看一下效果:

這里可能會有小伙伴掉進“坑里”——如果你在實現的過程中發(fā)現效果出不來可以按下面的思路先判斷和解決試試:
1、看一下當前文件的后綴是不是正確的。比如上面代碼里規(guī)定了
javascript,那就要在.js后綴的文件里面才有效2、注冊命令當然也和插件的生命周期息息相關,如果你發(fā)現上一步是正確的,那你就要去
package.json文件里面看看activationEvents里面的命令是否觸發(fā)了。如果你忘記如何觸發(fā)插件激活的生命周期,那你就改成這樣。
...
"activationEvents": [
"*"
],
...
3、如果上面兩個還沒有解決你的問題的話,那肯定是你上面代碼 ctrl+c ctrl+v 的不對!開個玩笑,如果你還是不能實現的話……那你就留言評論點個贊來個三聯么么噠~
回歸一下正題,我們來分析一下上面的代碼是如何實現的:
const completion = vscode.languages.registerCompletionItemProvider(
// 這里是注冊這個 Provider 有效的相關文件,支持字符串類型或 DocumentFilter 對象。
// 如果你要對多個后綴的文件做操作的話可以用數組的形式,例如 ['javascript', 'plaintext']
// DocumentFilter 對象包含三個字段(均非必須),例如:{ language: 'json', scheme: 'untitled', pattern: '**/package.json' }
'javascript',
...
}
...
{
// 這是代表了一個 provider
provideCompletionItems(document, position) {
// 拿到當前 `position` 的 text 并且判斷一下是否以 `hello.` 開頭
const linePrefix = document.lineAt(position).text.substr(0, position.character);
// 沒有匹配到則不予提示
if (!linePrefix.endsWith('hello.')) {
return undefined;
}
// 如果匹配成功就返回 CompletionItem 有:HelloGitHub、HelloWorld、HelloPeople
return [
new vscode.('HelloGitHub', vscode.CompletionItemKind.Property),
new vscode.CompletionItem('HelloWorld', vscode.CompletionItemKind.Property),
new vscode.CompletionItem('HelloPeople', vscode.CompletionItemKind.Property),
];
}
},
...
可能你會疑惑, vscode.CompletionItemKind.Property 是什么東西呢?說簡單一點其實就是個圖標的配置。我們可以換幾個屬性來看看差別:
...
return [
new vscode.CompletionItem('HelloGitHub', vscode.CompletionItemKind.Method),
new vscode.CompletionItem('HelloWorld', vscode.CompletionItemKind.Enum),
new vscode.CompletionItem('HelloPeople', vscode.CompletionItemKind.Property),
];
...

從 index.d.ts 可以看到它支持以下這么多類型的圖標,可以根據不同的需求來選擇你想要的圖標,當然啦這里就不重點展開啦,有興趣的可以自己把這些圖標都整理一下~
/**
* Completion item kinds.
*/
export enum CompletionItemKind {
Text = 0,
Method = 1,
Function = 2,
Constructor = 3,
Field = 4,
Variable = 5,
Class = 6,
Interface = 7,
Module = 8,
Property = 9,
Unit = 10,
Value = 11,
Enum = 12,
Keyword = 13,
Snippet = 14,
Color = 15,
Reference = 17,
File = 16,
Folder = 18,
EnumMember = 19,
Constant = 20,
Struct = 21,
Event = 22,
Operator = 23,
TypeParameter = 24,
User = 25,
Issue = 26,
}
最后就解釋一下這個觸發(fā)條件:
...
'.' // 當鍵盤打 . 的時候觸發(fā),支持多個觸發(fā)
...
我們可能會遇到不同場景需要不同的觸發(fā)條件,這時候就盡管往后加就好了,例如我們新加幾個特殊符號的觸發(fā)條件(這里先去掉匹配字符串的邏輯,以便于更好的觸發(fā)):
const completion = vscode.languages.registerCompletionItemProvider(
['javascript', 'xml'],
{
provideCompletionItems(document, position) {
// const linePrefix = document.lineAt(position).text.substr(0, position.character);
// if (!linePrefix.endsWith('hello')) {
// return undefined;
// }
return [
new vscode.CompletionItem('HelloGitHub', vscode.CompletionItemKind.Method),
new vscode.CompletionItem('HelloWorld', vscode.CompletionItemKind.Enum),
new vscode.CompletionItem('HelloPeople', vscode.CompletionItemKind.Property),
];
}
},
'.',
',',
' '
);

2、進階
但是正常情況下,我們往往需要去解析用戶輸入的不同內容,來給與不同對應的 completion item。所以接下去我們就以 xml 文件為例,來寫一個“功能強大”的 Completion Proviwder。
先來分析一下 xml 這種文件常見的 Completion Provider 大致有這么三種:
標簽名
屬性名
屬性值
當然啦,如果像是 vue 里面 template 模板的寫法,其實還有事件名這類等。那我們就以 @ 符號作為事件名提示的觸發(fā)條件,以 < 作為標簽名提示的觸發(fā)條件,以空格、回車作為屬性名的觸發(fā)條件,以單雙引號作為屬性值的觸發(fā)條件,先寫一個簡單的實現:
// 引入兩個 mock 文件
const testEventName = require("./mock/testEventName");
const testTagName = require("./mock/testTagName");
...
const completion = vscode.languages.registerCompletionItemProvider(
'xml',
{
provideCompletionItems(
document, // 命令被調用的文檔
position, // 命令被調用的位置
token, // 取消令牌
context // 自動補全是怎么觸發(fā)的
) {
// 如果校驗命中了取消令牌,就不提示
if (token.isCancellationRequested) {
return Promise.resolve([])
}
let char = context.triggerCharacter
switch (char) {
case '<': // 標簽名提示
// todo
case '@': // 綁定事件
// todo
default: // 屬性名、屬性值等
// todo
}
}
},
'@',
'\n',
' ',
'"',
"'",
'<'
)
mock 文件可以隨便定一個結構,下面是本文例子中用到的 mock 數據結構(兩個文件):
// ./mock/testEventName
module.exports = [
{
name: 'onTap',
id: 'ontap',
desc: '這是一個點擊事件的描述'
},
{
name: 'for',
id: 'for',
desc: '這是一個循環(huán)事件的描述'
}
]
// ./mock/testTagName
module.exports = [
{
name: 'HelloGitHub',
id: 'hg',
description: '這是我們的名字'
},
{
name: 'Welcome',
id: 'wlc',
description: '歡迎關注和喜歡我們'
}
]
先來實現一下標簽名的 Completion Provider:
const completionArr = []
for (let i = 0; i < testTagName.length; i++) {
const commandCompletion = new vscode.CompletionItem(testTagName[i].name);
commandCompletion.kind = vscode.CompletionItemKind.Property;
commandCompletion.documentation = new vscode.MarkdownString(testTagName[i].description);
let snippet = `${testTagName[i].name}\n` +
' name="${1:HelloGitHub}"\n' +
' desc="${2:We are serious about open source}"\n' +
'>\n' +
`</${testTagName[i].name}>`;
commandCompletion.insertText = new vscode.SnippetString(snippet);
completionArr.push(commandCompletion)
}
return completionArr;
我們可以看到和上面講過的內容差不多,也是需要 new 一個 CompletionItem 對象,但是這里把這個對象更加的“豐富化”了,通過增加屬性的方式給這個 CompletionItem 增加了圖標——kind、說明——documentation、還有片段——insertText 。
讓我們來看一下效果,如果沒有自動出現說明,就點一下 Completion 最右側的小箭頭:

同樣的我們也來寫一下事件的 Completion Provider,簡直就是 ctrl+c 和 ctrl+v:
if (testEventName && testEventName.length > 0) {
const arr = []
for(let i = 0; i < testEventName.length; i++) {
const item = testEventName[i]
const commandCompletion = new vscode.CompletionItem(item.name);
commandCompletion.kind = vscode.CompletionItemKind.Property;
commandCompletion.documentation = new vscode.MarkdownString(item.desc || '暫無介紹');
let snippet = `${item.name}{}`;
commandCompletion.insertText = new vscode.SnippetString(snippet);
arr.push(commandCompletion)
}
return arr
}
return []
效果:

接下去我們就要攻克最后的一個點:屬性值和屬性名。這就涉及到分析當前文本的結構,我們默認單雙引號所在的位置標示屬性值,挨著 < 符號的是標簽名,剩下的就都是作為屬性值。
所以第一步,我們寫一個方法,用來解析和獲取我們上面想要知道的文檔結構,這一部分的代碼我們寫到一個新的文件引用過去(getTagAtPosition.js ):
function getTagAtPosition(doc, pos) {
let offset = doc.offsetAt(pos);
let text = doc.getText();
// 因為引號里可能會有任何字符,所以做一層替換處理
let attrFlagText = text.replace(/("[^"]*"|'[^']*')/g, replacer('%'));
// 標簽起始位置 [start,length]
const range = getBracketRange(attrFlagText, offset);
if (!range) {
return null
}
const [start, end] = range;
offset = offset - start;
text = text.substr(start, end);
attrFlagText = attrFlagText.substr(start, end);
const tagNameMatcher = attrFlagText.match(/^<([\w-:.]+)/);
if (!tagNameMatcher) {
return null;
}
const name = tagNameMatcher[1]; // 標簽名稱
const isOnAttrValue = attrFlagText[offset] === '%';
const attrName = isOnAttrValue ? getAttrName(attrFlagText.substring(0, offset)) : '' // 當前輸入對應的屬性
const isOnTagName = offset <= name.length + 1;
const isOnAttrName = !isOnTagName && !isOnAttrValue
return {
name, // 標簽名
attrName, // 屬性名
isOnTagName, // 是否處于 tag 上
isOnAttrName, // 是否處于屬性名上
isOnAttrValue, // 是否處于屬性值上
}
}
// 字符替換的方法
const replacer = (char) => (raw) => char.repeat(raw.length);
// 獲取 <> 標簽的位置
function getBracketRange(text, pos) {
const textBeforePos = text.substr(0, pos)
const startBracket = textBeforePos.lastIndexOf('<')
if (startBracket < 0 || textBeforePos[startBracket + 1] === '!' || textBeforePos.lastIndexOf('>') > startBracket) {
// 前沒有開始符<,
// 或者正在注釋中: <!-- | -->
// 或者不在標簽中: <view > | </view>
return null
}
// 從光標位置后面找 > 標簽
let endBracket = text.indexOf('>', pos + 1)
if (endBracket < 0) {
// 未找到閉合 > 文件結束位置為結束
// 如 <image ... | EOF
endBracket = text.length
}
// 可能尚未輸入閉合標簽,取下一個標簽的頭<
// 此時找到的閉合標簽是下一個標簽
// <view xxx | ... <view ></view>
const nextStart = text.indexOf('<', pos + 1)
if (nextStart > 0 && nextStart < endBracket) {
endBracket = nextStart
}
return [startBracket, endBracket - startBracket]
}
對應 extension.js 里面加上我們新寫的邏輯:
...
default: // 屬性、標簽等
// step1. 找最近的標簽名
let tag = getTagAtPosition(document, position);
if (!tag) {
return null
}
// 屬性值提示
if (tag.isOnAttrValue) {
return getAttrValueCompletionArr(tag.attrName || '', targetObj.children)
} else {
// 屬性提示
return getAttrCompletionArr(targetObj.children)
}
...
接下來我們加一個新的 mock 數據,并且結構是一個樹狀結構,每個標簽下面都有它可能的屬性名列表(children),同時每一個屬性名都有對應的屬性值列表(children):
module.exports = [
{
name: 'HelloGitHub',
id: 'hg',
description: '這是我們的名字',
children: [
{
name: 'hgAttrName1',
children: [
{
name: 'hgAttrVal1'
},
{
name: 'hgAttrVal2'
}
]
},
{
name: 'hgAttrName2'
}
]
},
{
name: 'Welcome',
id: 'wlc',
description: '歡迎關注和喜歡我們'
}
]
看一下上面 getAttrCompletionArr 這個方法做的事情,其實就是從數據里取值出來展示這么簡單:
function getAttrCompletionArr (completionArr) {
const arr = []
if (completionArr.length > 0) {
for(let j = 0; j < completionArr.length; j++) {
if (completionArr[j] && completionArr[j].name) {
const commandCompletion = new vscode.CompletionItem(completionArr[j].name);
commandCompletion.kind = vscode.CompletionItemKind.Property;
arr.push(commandCompletion)
}
}
}
return arr
}
module.exports = getAttrCompletionArr;
那屬性值的列表的話,我們就要知道它是在哪個標簽名下的屬性名下面了:
function getAttrValueCompletionArr (attrName, completionArr) {
const enumValue = completionArr.find(item => item.name === attrName) || {};
if (enumValue.children && enumValue.children.length > 0) {
const arr = []
for(let i = 0; i < enumValue.children.length; i++) {
const commandCompletion = new vscode.CompletionItem(enumValue.children[i].name);
commandCompletion.kind = vscode.CompletionItemKind.Property;
arr.push(commandCompletion)
}
return arr
}
return []
}
最后的效果:

可能有的朋友對于上面一串解析文檔的方法有很多疑惑,代碼里雖然有注釋,但是可能還是沒有體感,這時候就建議最好動手實踐一下,因為都是 VS Code Extension 提供的方法,所以這里不會過多展開,畢竟也不是這篇文章的重點內容嘛~
三、「總結」和「預告」
那今天給大家介紹了兩種“偷懶”并且可以幫助我們提高打代碼效率的兩種方法:
代碼片段(Snippet)
自動補充(Completion Provider)
也是眾多 VS Code 插件中非常常見的功能之一,其實走近了看也不是很難吧~
今天的內容可能略多一點,如果你看完了第一篇,第二篇是在第一篇基礎上改的,相信你一定可以跟得上。那下篇文章,我們就要來看看 VS Code 插件中另一個非常強大的功能——WebView。也就是支持在插件中打開網頁、和網頁通信、還可以寫酷炫的 CSS 樣式等等。雖然它的功能很強大,但是像一把雙刃劍,他對于資源的占用也是很大的,想知道可以怎么用嗎?請期待下一期。

??「點擊關注」第一時間收到更新??
