本篇文章帶大家從0開發一個vscode變量翻譯插件,本文會從四個方面來完整的展示整個插件從功能設計到發布的完整歷程,希望對大家有所幫助!
需求的起因是英語渣在開發的過程中經常遇到一個變量知道中文叫啥,但是英文單詞可能就忘了,或者不知道,這個時候,我之前做法是打開瀏覽器,打開谷歌翻譯,輸入中文,復制英文,然后切回vscode,粘貼結果。
實在是太麻煩了,年輕的時候還好,記性好,英文單詞大部分都能記住,但隨著年紀越來越大,頭發越來越少,記性也是越來越差,上面的步驟重復的次數也越來越多,所以痛定思痛,就開發了這款插件。
因為自己也是這幾天從零開始學習的插件開發,所以本文完全的記錄了一個小白開發的插件開發之路,內容主要是實戰類的介紹,主要從四個方面介紹來完整的展示整個插件從功能設計到發布的完整歷程。
-
功能設計
-
環境搭建
-
插件功能開發
-
插件打包發布
【推薦學習:《vscode》】
功能設計
功能主要是兩個功能,中譯英,其他語言翻譯成中文
-
將中文變量替換為翻譯后的英文變量,多個單詞需要自動駝峰,解決的場景就是日常開發經常碰到的“英語卡殼”
-
劃詞翻譯,自動將各種語言翻譯成中文,這解決的場景是有時候看國外項目源代碼的注釋碰到不會的單詞不知道啥意思影響效率
環境搭建
上手的第一步,環境搭建
-
安裝腳手架, yo 與 generator-code,這兩個工具可以幫助我們快速構建項目,詳情可見(https://github.com/Microsoft/vscode-generator-code)
//安裝 yarn?global?add?yo?generator-code
-
安裝vsce,vsce可用來將開發的代碼打包成.vsix后綴的文件,方便上傳至微軟插件商店或者本地安裝
yarn?global?add?vsce
-
生成并初始化項目,初始化信息根據自己情況填寫
//初始化生成項目 yo?code
到這一步后,選擇直接打開,Open with code
打開后會自動建立一個工作區,并生成這些文件,可根據自己需要對文件進行刪減,完成這步后,我們可以直接進行開發與調試了
如何進行調試?
去運行與調試面板點擊Run Extention,或者快捷鍵F5,mac可以直接點擊觸控欄的調試按鈕
打開后會彈出一個新的vscode窗口,這個新的窗口就是你的測試環境(擴展開發宿主),你做的插件功能就是在這個新的窗口測試,打印的消息在前一個窗口的調試控制臺中,比如自帶的例子,在我們新窗口 cmd/ctrl+shift+p后輸入Hello world后會在前一個窗口的控制臺打印一些信息
到這里,開發準備環境就準備好了,下一步就是開始正式的插件功能開發
插件功能開發
插件開發中,有兩個重要的文件,一個是 package.json,一個是 extention.js
重要文件說明
package.json
-
activationEvents用來注冊激活事件,用來表明什么情況下會激活extention.js中的active函數。常見的有onLanguage,onCommand…更多信息可查看vscode文檔activationEvents(https://code.visualstudio.com/api/references/activation-events)
-
main表示插件的主入口
-
contributes用來注冊命令(commands),綁定快捷鍵(keybindings),**配置設置項(configuration)**等等,更多可配置項可看文檔(https://code.visualstudio.com/api/references/contribution-points)
extention.js
extention.js主要作用是作為插件功能的實現點,通過active,deactive函數,以及vscode提供的api以及一些事件鉤子來完成插件功能的開發
實現翻譯功能
翻譯這里主要是使用了兩個服務,谷歌和百度翻譯。
谷歌翻譯參考了別人的做法,使用google-translate-token獲取到token,然后構造請求url,再處理返回的body,拿到返回結果。這里還有一個沒搞懂的地方就是請求url的生成很迷,不知道這一塊是啥意思。
const?qs?=?require('querystring'); const?got?=?require('got'); const?safeEval?=?require('safe-eval'); const?googleToken?=?require('google-translate-token'); const?languages?=?require('../utils/languages.js'); const?config?=?require('../config/index.js'); //?獲取請求url async?function?getRequestUrl(text,?opts)?{ ????let?token?=?await?googleToken.get(text); ????const?data?=?{ ????????client:?'gtx', ????????sl:?opts.from, ????????tl:?opts.to, ????????hl:?opts.to, ????????dt:?['at',?'bd',?'ex',?'ld',?'md',?'qca',?'rw',?'rm',?'ss',?'t'], ????????ie:?'UTF-8', ????????oe:?'UTF-8', ????????otf:?1, ????????ssel:?0, ????????tsel:?0, ????????kc:?7, ????????q:?text ????}; ????data[token.name]?=?token.value; ????const?requestUrl?=?`${config.googleBaseUrl}${qs.stringify(data)}`; ????return?requestUrl; } //處理返回的body async?function?handleBody(url,?opts)?{ ????const?result?=?await?got(url); ????let?resultObj?=?{ ????????text:?'', ????????from:?{ ????????????language:?{ ????????????????didYouMean:?false, ????????????????iso:?'' ????????????}, ????????????text:?{ ????????????????autoCorrected:?false, ????????????????value:?'', ????????????????didYouMean:?false ????????????} ????????}, ????????raw:?'' ????}; ????if?(opts.raw)?{ ????????resultObj.raw?=?result.body; ????} ????const?body?=?safeEval(result.body); ????//?console.log('body',?body); ????body[0].forEach(function(obj)?{ ????????if?(obj[0])?{ ????????????resultObj.text?+=?obj[0]; ????????} ????}); ????if?(body[2]?===?body[8][0][0])?{ ????????resultObj.from.language.iso?=?body[2]; ????}?else?{ ????????resultObj.from.language.didYouMean?=?true; ????????resultObj.from.language.iso?=?body[8][0][0]; ????} ????if?(body[7]?&&?body[7][0])?{ ????????let?str?=?body[7][0]; ????????str?=?str.replace(/<b><i>/g,?'['); ????????str?=?str.replace(//g,?']'); ????????resultObj.from.text.value?=?str; ????????if?(body[7][5]?===?true)?{ ????????????resultObj.from.text.autoCorrected?=?true; ????????}?else?{ ????????????resultObj.from.text.didYouMean?=?true; ????????} ????} ????return?resultObj; } //翻譯 async?function?translate(text,?opts)?{ ????opts?=?opts?||?{}; ????opts.from?=?opts.from?||?'auto'; ????opts.to?=?opts.to?||?'en'; ????opts.from?=?languages.getCode(opts.from); ????opts.to?=?languages.getCode(opts.to); ????try?{ ????????const?requestUrl?=?await?getRequestUrl(text,?opts); ????????const?result?=?await?handleBody(requestUrl,?opts); ????????return?result; ????}?catch?(error)?{ ????????console.log(error); ????} } //?獲取翻譯結果 const?getGoogleTransResult?=?async(originText,?ops?=?{})?=>?{ ????const?{?from,?to?}?=?ops; ????try?{ ????????const?result?=?await?translate(originText,?{?from:?from?||?config.defaultFrom,?to:?to?||?defaultTo?}); ????????console.log('谷歌翻譯結果',?result.text); ????????return?result; ????}?catch?(error)?{ ????????console.log(error); ????????console.log('翻譯失敗'); ????} } module.exports?=?getGoogleTransResult;</i></b>
百度翻譯,百度翻譯的比較簡單,申請服務,獲得appid和key,然后構造請求url直接請求就行
不知道如何申請的,可查看我之前的一篇文章 Electron+Vue從零開始打造一個本地文件翻譯器 進行申請https://juejin.cn/post/6899581622471884813
const?md5?=?require("md5"); const?axios?=?require("axios"); const?config?=?require('../config/index.js'); axios.defaults.withCredentials?=?true; axios.defaults.crossDomain?=?true; axios.defaults.headers.post["Content-Type"]?= ????"application/x-www-form-urlencoded"; //?百度翻譯 async?function?getBaiduTransResult(text?=?"",?opt?=?{})?{ ????const?{?from,?to,?appid,?key?}?=?opt; ????try?{ ????????const?q?=?text; ????????const?salt?=?parseInt(Math.random()?*?1000000000); ????????let?str?=?`${appid}${q}${salt}${key}`; ????????const?sign?=?md5(str); ????????const?query?=?encodeURI(q); ????????const?params?=?`q=${query}&from=${from}&to=${to}&appid=${appid}&salt=${salt}&sign=${sign}`; ????????const?url?=?`${config.baiduBaseUrl}${params}`; ????????console.log(url); ????????const?res?=?await?axios.get(url); ????????console.log('百度翻譯結果',?res.data.trans_result[0]); ????????return?res.data.trans_result[0]; ????}?catch?(error)?{ ????????console.log({?error?}); ????} } module.exports?=?getBaiduTransResult;
獲取選中的文本
使用事件鉤子onDidChangeTextEditorSelection,獲取選中的文本
????onDidChangeTextEditorSelection(({?textEditor,?selections?})?=>?{ ????????text?=?textEditor.document.getText(selections[0]); ????})
配置項的獲取更新
通過vscode.workspace.getConfiguration獲取到工作區的配置項,然后通過事件鉤子onDidChangeConfiguration監聽配置項的變動。
獲取更新配置項
const?{?getConfiguration?}?=?vscode.workspace; const?config?=?getConfiguration(); //注意get里面的參數其實就是package.json配置項里面的contributes.configuration.properties.xxx const?isCopy?=?config.get(IS_COPY); const?isReplace?=?config.get(IS_REPLACE); const?isHump?=?config.get(IS_HUMP); const?service?=?config.get(SERVICE); const?baiduAppid?=?config.get(BAIDU_APPID); const?baiduKey?=?config.get(BAIDU_KEY); //更新使用update方法,第三個參數為true代表應用到全局 config.update(SERVICE,?selectedItem,?true);
監聽配置項的變動
const?{?getConfiguration,?onDidChangeConfiguration?}?=?vscode.workspace; const?config?=?getConfiguration(); //監聽變動 const?disposeConfig?=?onDidChangeConfiguration(()?=>?{ ??config?=?getConfiguration(); })
監聽個別配置項的變動
const?disposeConfig?=?onDidChangeConfiguration((e)?=>?{ ????if?(e?&&?e.affectsConfiguration(BAIDU_KEY))?{ ????????//干些什么 ????} })
獲取當前打開的編輯器對象
vscode.window.activeTextEditor代表當前打開的編輯器,如果切換標簽頁,而沒有設置監聽,那么這個這個對象不會自動更新,所以需要使用onDidChangeActiveTextEditor來監聽,并替換之前的編輯器對象
const?{?activeTextEditor,?onDidChangeActiveTextEditor?}?=?vscode.window; let?active?=?activeTextEditor; const?edit?=?onDidChangeActiveTextEditor((textEditor)?=>?{ ??console.log('activeEditor改變了'); ??//更換打開的編輯器對象 ??if?(textEditor)?{ ??????active?=?textEditor; ??} })
劃詞翻譯懸浮提示
通過vscode.languages.registerHoverProvider注冊一個Hover,然后通過activeTextEditor拿到選中的詞語進行翻譯,然后再通過new vscode.Hover將翻譯結果懸浮提示
//?劃詞翻譯檢測 const?disposeHover?=?vscode.languages.registerHoverProvider("*",?{ ????async?provideHover(document,?position,?token)?{ ????????const?service?=?config.get(SERVICE); ????????const?baiduAppid?=?config.get(BAIDU_APPID); ????????const?baiduKey?=?config.get(BAIDU_KEY); ????????let?response,?responseText; ????????const?selected?=?document.getText(active.selection); ????????//?谷歌翻譯 ????????if?(service?===?'google')?{ ????????????response?=?await?getGoogleTransResult(selected,?{?from:?'auto',?to:?'zh-cn'?}); ????????????responseText?=?response.text; ????????} ????????//?百度翻譯 ????????if?(service?===?'baidu')?{ ????????????response?=?await?getBaiduTransResult(selected,?{?from:?"auto",?to:?"zh",?appid:?baiduAppid,?key:?baiduKey?}); ????????????responseText?=?response.dst; ????????} ????????//?懸浮提示 ????????return?new?vscode.Hover(`${responseText}`); ????} })
替換選中的文本
獲取到activeTextEditor,調用他的edit方法,然后使用回調中的replace
//是否替換原文 if?(isReplace)?{ ??let?selectedItem?=?active.selection; ??active.edit(editBuilder?=>?{ ????editBuilder.replace(selectedItem,?result) ??}) }
復制到剪貼板
使用vscode.env.clipboard.writeText;
//?是否復制翻譯結果 if?(isCopy)?{ ??vscode.env.clipboard.writeText(result); }
駝峰處理
function?toHump(str)?{ ????if?(!str)?{ ????????return ????} ????const?strArray?=?str.split('?'); ????const?firstLetter?=?[strArray.shift()]; ????const?newArray?=?strArray.map(item?=>?{ ????????return?`${item.substring(0,1).toUpperCase()}${item.substring(1)}`; ????}) ????const?result?=?firstLetter.concat(newArray).join(''); ????return?result; } module.exports?=?toHump;
快捷鍵綁定
通過vscode.commands.registerCommand注冊綁定之前package.json中設置的keybindings,需要注意的是registerCommand的第一個參數需要與keybindings的command保持一致才能綁定
registerCommand('translateVariable.toEN',?async()?=>?{ ??//do?something }) //package.json "keybindings":?[{ ??"key":?"ctrl+t", ??"mac":?"cmd+t", ??"when":?"editorTextFocus", ??"command":?"translateVariable.toEN" }],
插件打包發布
打包
vsce?package
打包后會在目錄下生成.vsix后綴的插件
發布
插件發布主要是把打包的vsix后綴插件,傳入微軟vscode插件商店,當然也能本地安裝使用。
傳入商店
發布到線上需要到 微軟插件商店管理頁面(https://marketplace.visualstudio.com/manage/createpublisher),創建發布者信息,如果沒有微軟賬號,需要去申請。
創建完成后,選擇發布到vscode商店
本地安裝
本地是可以直接安裝.vsix后綴插件的,找到插件菜單
選擇從VSIX安裝,安裝上面打包的插件就好了
最后
vscode的中文資料有點少,這次開發大多數時間都在看英文文檔,以及上外網尋找資料,真的英語太重要了,后面得多學點英語了,希望后面我使用自己做的這個插件的次數會越來越少,項目已開源,使用說明與源碼傳送門(https://github.com/Kerinlin/translate-variable)
更多關于VSCode的相關知識,請訪問:vscode!!