選擇編輯器時(shí),想必很多人對 vim 嗤之以鼻,但實(shí)際上,從一定角度來看,你也能發(fā)現(xiàn)別樣的風(fēng)采。本文的目的并不在于強(qiáng)推薦大家使用 vim,而是通過作者將 Vim 作為 Web 開發(fā)項(xiàng)目的主編輯器案例中,讓我們發(fā)現(xiàn)一些不同之處。
作者 | Fidel Sanchez-Bueno
譯者 | 彎月,責(zé)編 | 屠敏
出品 | CSDN(ID:CSDNnews)
以下為譯文:
在學(xué)習(xí)編程的過程中,選擇最適合自己的編輯器或IDE(下文的編輯器指代兩者)是每個(gè)程序員都會經(jīng)歷的過程。對于我而言,大約從9年前開始學(xué)習(xí)Python,我還記得當(dāng)時(shí)每周都會換一種編輯器,一開始是IDLE,后來我記得還試過Boa Constructor、Komodo和Notepad++等。
在尋找最佳編輯器的過程中,你會了解到程序員之間的編輯器之戰(zhàn),并且會對那些Vim和Emacs的笑話會心一笑。
也就是在這個(gè)時(shí)候,你會嘗試學(xué)習(xí)Vim或者Emacs,從此就開始了愛麗絲的探險(xiǎn)之旅。
從這篇文章的標(biāo)題就可以看出來,我選擇的編輯器是Vim,但我并沒打算向你推薦Vim,也不會去說服你花費(fèi)無數(shù)時(shí)間,改變自己的開發(fā)環(huán)境來使用Vim。
這篇文章的目的是分享我在使用Vim作為Web開發(fā)項(xiàng)目的主編輯器時(shí),做出的一些能夠提高效率的定制。
Web開發(fā)的Vim插件
作為Web程序員,大部分時(shí)間都在編寫HTML、CSS和JavaScript文件,根據(jù)個(gè)人喜好或項(xiàng)目需要,你可能還會使用一些框架(如Angular、Vue或React)以及babel、webpack、grunt等各種工具。
我個(gè)人會盡可能減少插件的使用,僅在插件能帶來非常大的好處,而且能真正改進(jìn)工作流程的時(shí)候才會使用。
目前我安裝的插件如下:
Emmet.vim
indentline和vim-jsx-pretty
vim-commentary
ALE(eslint和prettier)
Emmet.vim
Emmet是高速輸入和編輯代碼的絕佳工具,只需要輸入一行代碼就可以創(chuàng)建一整段復(fù)雜的HTML。
indentline和vim-jsx-pretty
這兩個(gè)插件可以改進(jìn)Vim的視覺樣式。indentline可以添加豎線來顯示縮進(jìn)級別,vim-jsx-pretty能給JSX代碼添加高亮,很適合編寫ReactJS等代碼時(shí)使用。
vim-commentary
這個(gè)插件可以方便地注釋掉一段代碼或者取消注釋,只需選中代碼并鍵入<g-c>即可。
ALE(eslint和prettier)
ALE(Asynchronous Lint Engine)可以調(diào)用linter和代碼修整工具,極大地提高工作效率,屬于那種不用不知道,用了絕對不后悔的插件。我使用ALE主要是為了調(diào)用prettier。
實(shí)時(shí)預(yù)覽(熱重載)
實(shí)時(shí)查看修改的效果能夠極大地改善工作流程。很多React或Gatsby等項(xiàng)目已經(jīng)內(nèi)置了該功能,但如果僅僅是創(chuàng)建一個(gè)簡單的網(wǎng)頁(HTML、CSS和JavaScript),那么Atom、Brackets或VSCode等編輯器可以把編輯中的頁面的實(shí)時(shí)預(yù)覽并列顯示在另一個(gè)窗口中。
像我這種愛鉆牛角尖的人很希望在Vim中也使用該功能。盡管有幾個(gè)插件能實(shí)現(xiàn),但我決定選擇另一種方式。
我決定實(shí)現(xiàn)一個(gè)不依賴于編輯器的方案。基本思路就是,運(yùn)行一個(gè)本地服務(wù)器,監(jiān)視文件的改動,每當(dāng)文件更新時(shí)就刷新服務(wù)器上的頁面。
聽起來似乎很復(fù)雜,但實(shí)際上非常簡單,只需要在項(xiàng)目文件夾中安裝并運(yùn)行browser-sync即可。
我假設(shè)你已經(jīng)安裝了nodejs,所以只需要在全局安裝browser-sync。
npm install -g browser-sync
安裝完browser-sync之后,就能在任何文件夾中運(yùn)行,創(chuàng)建一個(gè)本地服務(wù)器,然后自動顯示文件夾內(nèi)的index.html。
browser-sync start --server --files .
如果你使用的是Linux,bane可以創(chuàng)建一個(gè)別名來簡化啟動服務(wù)器的過程。打開主目錄下的.bashrc文件,添加如下內(nèi)容:
# Command line alias to start the browser-sync server
alias serve="browser-sync start --server --files ."
我更進(jìn)一步,允許局域網(wǎng)內(nèi)的其他機(jī)器訪問我的服務(wù)器,這樣就能在別的設(shè)備上進(jìn)行測試:
# browser-sync config
# Get the current local IP address
export SERVER_IP=`hostname -I`
# The command alias to start the browser-sync server
alias serve="browser-sync start --server --files . --no-notify --host $SERVER_IP --port 9000"
感謝閱讀!
原文:https://dev.to/fidelve/using-vim-as-your-main-editor-for-web-development-5a73
作者:Fidel Sanchez-Bueno,化學(xué)工程師,自學(xué)成才的程序員。
本文為 CSDN 翻譯,轉(zhuǎn)載請注明來源出處。
【End】
為一個(gè)現(xiàn)代的代碼編輯器,Atom有著各種流行編輯器都有的特性,功能上非常豐富,支持各種編程語言的代碼高亮與大多數(shù)其他編輯器相比,Atom的語言支持已經(jīng)算是覆蓋非常全面了。另外,它的代碼補(bǔ)全功能(也叫Snippets)也非常好用,你只需輸入幾個(gè)字符即可展開成各種常用代碼,可以極大提高編程效率。Atom預(yù)裝了四種UI和八種語法主題,包括深色和淺色。如果找不到您要查找的內(nèi)容,您還可以安裝由Atom社區(qū)創(chuàng)建的主題或創(chuàng)建自己的主題。
Atom幫助您更快地編寫代碼。在一個(gè)窗口中輕松瀏覽和打開單個(gè)文件,整個(gè)項(xiàng)目或多個(gè)項(xiàng)目。
使用默認(rèn)插件,從v1.5.1開始,在某些方面支持以下語言:HTML,CSS,Less,Sass,GitHub Flavored Markdown,C / C ++,C#,Go,Java,Objective-C,JavaScript,JSON,CoffeeScript ,Python,PHP,Ruby,Ruby on Rails,shell腳本,Clojure,Perl,Git,Make,Property List(Apple),TOML,XML,YAML,Moustache,Julia和SQL。
Atom1.38.0顯著變化:
原子1.38.1的變化:
語言的JSON
原子1.38.2更改日志:
tom是一個(gè)著名的開源編輯器,是由Chris Wanstrath在2008年作為其個(gè)人的編外項(xiàng)目發(fā)展而來。據(jù)說在今年(2022)年底,這款編輯器也將進(jìn)入關(guān)停狀態(tài)。而且目前大部分程序員都把VS Code作為其最主要的開發(fā)工作,但是Atom本身的設(shè)計(jì)和代碼實(shí)現(xiàn)都是非常優(yōu)秀的,通過閱讀它的源碼,我們還是可以學(xué)到很多相關(guān)的編程技巧。
atom的代碼結(jié)構(gòu)非常清晰,整個(gè)項(xiàng)目可以分為兩個(gè)部分,一個(gè)是atom本身的代碼,另一個(gè)是atom的插件。atom本身的代碼又可以分為兩部分,一個(gè)是atom的核心業(yè)務(wù)邏輯,另一個(gè)是atom的UI代碼。核心業(yè)務(wù)邏輯主要
是用來設(shè)置環(huán)境變量,調(diào)度窗口、調(diào)度系統(tǒng)資源等等。UI代碼則主要負(fù)責(zé)處理atom的界面,比如菜單欄,工具欄,狀態(tài)欄等等。
作為使用electron框架編寫的應(yīng)用程序,整體都是使用js來寫的(早期是使用coffee來編寫的),可以從其目錄中看到,整個(gè)項(xiàng)目的目錄結(jié)構(gòu)如下:
|-src // 核心業(yè)務(wù)邏輯
|-|-main-process
|-|-|-atom-application.js
|-|-|-atom-environment.js
|-|-|-atom-window.js
|-static // UI代碼
|-packages // 其它擴(kuò)展包
...
眾所周知,用Electron框架寫成的應(yīng)用,都可以分為主線程和渲染進(jìn)程。對應(yīng)到atom中,主線程的代碼都是在src/main-process目錄下,而渲染線程的代碼則是直接src目錄下。靜態(tài)UI資源則在static目錄下。
我們先從主線程的入口代碼開始看起,代碼位于src/main-process/main.js路徑下:
// 命令行工具入口,
const args = yargs(process.argv)
// Don't handle --help or --version here; they will be handled later.
.help(false)
.version(false)
.alias('d', 'dev')
.alias('t', 'test')
.alias('r', 'resource-path').argv;
// 下面省略大量代碼,主要用于處理命令行參數(shù),用來專門處理使用命令行打開atom的情況
// 真正的入口
const start = require(path.join(resourcePath, 'src', 'main-process', 'start'));
start(resourcePath, devResourcePath, startTime);
可以從上面代碼看出,其實(shí)真正的處理入口還是在start函數(shù)中(src/main-process/start.js):
module.exports = function start(resourcePath, devResourcePath, startTime) {
// 處理錯誤情況
process.on('uncaughtException', function(error = {}) {
});
process.on('unhandledRejection', function(error = {}) {
});
// 初始化各種參數(shù)
app.commandLine.appendSwitch('enable-experimental-web-platform-features');
const args = parseCommandLine(process.argv.slice(1));
const previousConsoleLog = console.log;
console.log = nslog;
args.resourcePath = normalizeDriveLetterName(resourcePath);
args.devResourcePath = normalizeDriveLetterName(devResourcePath);
atomPaths.setAtomHome(app.getPath('home'));
atomPaths.setUserData(app);
const config = getConfig();
const colorProfile = config.get('core.colorProfile');
if (colorProfile && colorProfile !== 'default') {
app.commandLine.appendSwitch('force-color-profile', colorProfile);
}
if (handleStartupEventWithSquirrel()) {
return;
} else if (args.test && args.mainProcess) {
// 處理測試情況
app.setPath(
'userData',
temp.mkdirSync('atom-user-data-dir-for-main-process-tests')
);
console.log = previousConsoleLog;
app.on('ready', function() {
const testRunner = require(path.join(
args.resourcePath,
'spec/main-process/mocha-test-runner'
));
testRunner(args.pathsToOpen);
});
return;
}
const releaseChannel = getReleaseChannel(app.getVersion());
let appUserModelId = 'com.squirrel.atom.' + process.arch;
if (releaseChannel !== 'stable') {
appUserModelId += `-${releaseChannel}`;
}
// 這個(gè)方法可以防止win10在任務(wù)欄中顯示重復(fù)的atom圖標(biāo)
app.setAppUserModelId(appUserModelId);
app.on('open-file', addPathToOpen);
app.on('open-url', addUrlToOpen);
// 當(dāng)應(yīng)用關(guān)閉的時(shí)候,需要上報(bào)一些數(shù)據(jù)
app.on('will-finish-launching', () =>
startCrashReporter({
uploadToServer: config.get('core.telemetryConsent') === 'limited',
releaseChannel
})
);
if (args.userDataDir != null) {
app.setPath('userData', args.userDataDir);
} else if (args.test || args.benchmark || args.benchmarkTest) {
app.setPath('userData', temp.mkdirSync('atom-test-data'));
}
app.on('ready', function() {
app.removeListener('open-file', addPathToOpen);
app.removeListener('open-url', addUrlToOpen);
// 構(gòu)造一個(gè)atomApplication對象
const AtomApplication = require(path.join(
args.resourcePath,
'src',
'main-process',
'atom-application'
));
// 并將之前的參數(shù)傳入
AtomApplication.open(args);
});
};
從上面代碼可以看出,前置處理也是各種參數(shù)的初始化,以及為了便于測試,做的一些定制處理。在應(yīng)用初始化結(jié)束后,就會動態(tài)加載應(yīng)用模塊,構(gòu)造 AtomApplication 實(shí)例。可以注意到,這里使用按需加載的目的是希望能夠在需要的時(shí)候才會去加載對應(yīng)的模塊,這樣可以減少內(nèi)存的占用。
接著,我們來看一下atom-application.js的代碼,這塊代碼量比較大,是整個(gè)atom的核心代碼,我們先來看一下整體的結(jié)構(gòu):
// 是一個(gè)單列模式, 繼承自`EventEmitter`模塊,主要因?yàn)閮?nèi)部會大量應(yīng)用事件處理機(jī)制來分發(fā)邏輯。
class AtomApplication extends EventEmitter {
static open(options) {
// 初始化一些參數(shù)
// 創(chuàng)建一個(gè)atomApplication對象
// 并將之前的參數(shù)傳入
return new AtomApplication(options);
}
exit(status) {
app.exit(status);
}
constructor(options){}
async initialize(options) {}
}
程序啟動的入口只有AtomApplication.open這一個(gè)方法,這個(gè)方法會創(chuàng)建一個(gè)AtomApplication對象,然后調(diào)用它的initialize方法,層層遞進(jìn),再調(diào)用創(chuàng)建窗口、加載配置等方法,最終完成程序的啟動。
其中,比較值得注意的是使用了一個(gè)叫做event-kit的模塊。它是一個(gè)事件處理器模塊,提供了一個(gè)事件處理器的抽象,可以讓我們更容易地處理事件。最重要的作用是它實(shí)現(xiàn)了CompositeDisposable類,可以在需要的時(shí)候,釋放資源。雖然javascript是一個(gè)有垃圾回收機(jī)制的語言,但是如果沒有手動釋放一些資源的話,會造成大量的內(nèi)存占用。
在使用過程中,也十分簡單
class AtomApplication extends EventEmitter {
// 省略其它代碼
constructor(options) {
// 省略其它代碼
this.disposable = new CompositeDisposable();
}
async destroy() {
const windowsClosePromises = this.getAllWindows().map(window => {
window.close();
return window.closedPromise;
});
await Promise.all(windowsClosePromises);
// 在銷毀的時(shí)候統(tǒng)一釋放
this.disposable.dispose();
}
// 注冊事件處理函數(shù)
handleEvents() {
// 省略其它代碼,
// 在注冊事件回調(diào)的時(shí)候,直接將對象添加到disposable的依賴中去
this.disposable.add(
ipcHelpers.on(app, 'before-quit', async event => {...})
);
}
}
atom作為一個(gè)編輯器,它的擴(kuò)展機(jī)制是非常重要的。和其他的IDE類似,擴(kuò)展機(jī)制也是使用的微內(nèi)核模式(或者插件模式)來實(shí)現(xiàn)。微內(nèi)核架構(gòu)是一種十分常見的軟件架構(gòu),它將應(yīng)用系統(tǒng)分為兩個(gè)部分:一個(gè)微內(nèi)核和一組外部的插件。微內(nèi)核負(fù)責(zé)管理插件,提供插件之間的通信機(jī)制,以及提供一些基礎(chǔ)的服務(wù)。插件則負(fù)責(zé)提供具體的功能。這樣的架構(gòu)可以讓我們更容易地?cái)U(kuò)展軟件的功能,而不需要修改軟件的核心代碼。
在atom中,插件主要是通過package類來實(shí)現(xiàn)的。package是atom擴(kuò)展的基本單元,它可以包含一些功能,比如語法高亮、代碼提示、代碼格式化等等,也提供了讓第三方開發(fā)者擴(kuò)展的能力。那么這些擴(kuò)展是如何加載的呢?我們先來看一下package-manager.js的代碼:
// 可以加載、激活、停用、卸載包
// 加載包讀取并解析包的元數(shù)據(jù)和資源,例如快捷鍵、菜單、樣式表等
// 激活包注冊加載的資源并調(diào)用包的主模塊的`activate()`方法
// 停用包取消注冊包的資源并調(diào)用包的主模塊的`deactivate()`方法
// 卸載包從包管理器中完全移除
// 可以通過`core.disabledPackages`配置項(xiàng)和調(diào)用`enablePackage()/disablePackage()`方法來啟用/禁用包
class PackageManager {
preloadPackage(packageName, pack) {
...
}
loadPackages() {
...
}
enablePackage(packageName) {
...
}
// 觸發(fā)事件,用來注冊回調(diào)
onDidActivatePackage(callback) {
}
}
這個(gè)包管理器類PackageManager,可以管理擴(kuò)展包的整個(gè)生命周期,主要負(fù)責(zé)包的加載、卸載、更新等操作。而所有的包都綁定在主內(nèi)核的atom.packages這個(gè)全局變量上,我們可以通過這個(gè)變量來訪問應(yīng)用上加載的所有擴(kuò)展。
那么packageManager是如何負(fù)責(zé)管理包的安裝和卸載呢?:
class PackageManager {
constructor(packages) {
this.packages = packages;
}
getPackages() {
return this.packages;
}
getPackage(name) {
return this.packages.find(pkg => pkg.name === name);
}
// 禁用包,從內(nèi)存中將包去除,然后通知應(yīng)用程序或者擴(kuò)展來執(zhí)行禁用操作
async deactivatePackage(name, suppressSerialization) {
const pack = this.getLoadedPackage(name);
if (pack == null) {
return;
}
if (!suppressSerialization && this.isPackageActive(pack.name)) {
this.serializePackage(pack);
}
const deactivationResult = pack.deactivate();
if (deactivationResult && typeof deactivationResult.then === 'function') {
await deactivationResult;
}
delete this.activePackages[pack.name];
delete this.activatingPackages[pack.name];
this.emitter.emit('did-deactivate-package', pack);
}
}
比如在擴(kuò)展ui-watcher中,就可以在監(jiān)聽到did-deactivate-package事件后,執(zhí)行一些清理操作:
watchForPackageChanges() {
this.subscriptions.add(
atom.packages.onDidDeactivatePackage(pack => {
// This only handles packages - onDidChangeActiveThemes handles themes
const watcher = this.watchedPackages.get(pack.name);
if (watcher) watcher.destroy();
this.watchedPackages.delete(pack.name);
})
);
}
Package類,則包含了包的基礎(chǔ)信息,包括鍵位設(shè)置、配置、樣式等,并且有完整的生命周期。
class Package {
constructor(params) {
this.config = params.config;
this.packageManager = params.packageManager;
this.styleManager = params.styleManager;
this.commandRegistry = params.commandRegistry;
this.keymapManager = params.keymapManager;
this.notificationManager = params.notificationManager;
this.grammarRegistry = params.grammarRegistry;
this.themeManager = params.themeManager;
this.menuManager = params.menuManager;
this.contextMenuManager = params.contextMenuManager;
this.deserializerManager = params.deserializerManager;
this.viewRegistry = params.viewRegistry;
this.emitter = new Emitter();
// 此處省略大量的細(xì)節(jié)
}
preload() {
// do something
}
load() {
// do something
}
unload() {
// do something
}
activate() {
// do something
}
deactivate() {
// do something
}
finishLoading() {
// do something
}
}
Package(擴(kuò)展)實(shí)例本身要和主應(yīng)用進(jìn)行通信,atom是直接通過全局對象的方式進(jìn)行調(diào)用的,這樣做的好處是不用考慮通信的問題,但是也有一些弊端,比如不方便重構(gòu)等。
在應(yīng)用入口處,會將PackageManager實(shí)例掛載在應(yīng)用實(shí)例上。后續(xù)我們可以通過atom.packages來訪問包管理器實(shí)例,從而獲取包的信息。
// atom-application.js
this.packages = new PackageManager({
... // 一堆的配置
});
this.packages.initialize(...);
而在渲染進(jìn)程中,可以通過在window上掛載atom對象來訪問包管理器實(shí)例,從而獲取所有擴(kuò)展包的信息,進(jìn)行預(yù)加載操作。
// initialize-application-window.js
/ 初始化 AtomEnvironment
global.atom = new AtomEnvironment({
clipboard,
applicationDelegate: new ApplicationDelegate(),
enablePersistence: true
});
TextEditor.setScheduler(global.atom.views);
// 初始化應(yīng)用窗口
global.atom.preloadPackages();
// ... 省略大量代碼
module.exports = function({ blobStore }) {
// 省略大量代碼
// 在startEditorWindows內(nèi)部,當(dāng)窗口初始化完成后,會正式調(diào)用`loadPackages`方法來加載所有的擴(kuò)展包
return global.atom.startEditorWindow().then(function() {
// Workaround for focus getting cleared upon window creation
const windowFocused = function() {
window.removeEventListener('focus', windowFocused);
setTimeout(() => document.querySelector('atom-workspace').focus(), 0);
};
window.addEventListener('focus', windowFocused);
ipcRenderer.on('environment', (event, env) => updateProcessEnv(env));
});
}
總體而言,atom的擴(kuò)展機(jī)制還是比較簡單的,在各種擴(kuò)展的生命周期中,都可以通過事件來進(jìn)行通信,從而實(shí)現(xiàn)各種功能。這樣一種實(shí)現(xiàn),其實(shí)也可以在我們?nèi)粘9ぷ鬟^程中加以借鑒。
*請認(rèn)真填寫需求信息,我們會在24小時(shí)內(nèi)與您取得聯(lián)系。