选择编辑器时,想必很多人对 vim 嗤之以鼻,但实际上,从一定角度来看,你也能发现别样的风采。本文的目的并不在于强推荐大家使用 vim,而是通过作者将 Vim 作为 Web 开发项目的主编辑器案例中,让我们发现一些不同之处。
作者 | Fidel Sanchez-Bueno
译者 | 弯月,责编 | 屠敏
出品 | CSDN(ID:CSDNnews)
以下为译文:
在学习编程的过程中,选择最适合自己的编辑器或IDE(下文的编辑器指代两者)是每个程序员都会经历的过程。对于我而言,大约从9年前开始学习Python,我还记得当时每周都会换一种编辑器,一开始是IDLE,后来我记得还试过Boa Constructor、Komodo和Notepad++等。
在寻找最佳编辑器的过程中,你会了解到程序员之间的编辑器之战,并且会对那些Vim和Emacs的笑话会心一笑。
也就是在这个时候,你会尝试学习Vim或者Emacs,从此就开始了爱丽丝的探险之旅。
从这篇文章的标题就可以看出来,我选择的编辑器是Vim,但我并没打算向你推荐Vim,也不会去说服你花费无数时间,改变自己的开发环境来使用Vim。
这篇文章的目的是分享我在使用Vim作为Web开发项目的主编辑器时,做出的一些能够提高效率的定制。
Web开发的Vim插件
作为Web程序员,大部分时间都在编写HTML、CSS和JavaScript文件,根据个人喜好或项目需要,你可能还会使用一些框架(如Angular、Vue或React)以及babel、webpack、grunt等各种工具。
我个人会尽可能减少插件的使用,仅在插件能带来非常大的好处,而且能真正改进工作流程的时候才会使用。
目前我安装的插件如下:
Emmet.vim
indentline和vim-jsx-pretty
vim-commentary
ALE(eslint和prettier)
Emmet.vim
Emmet是高速输入和编辑代码的绝佳工具,只需要输入一行代码就可以创建一整段复杂的HTML。
indentline和vim-jsx-pretty
这两个插件可以改进Vim的视觉样式。indentline可以添加竖线来显示缩进级别,vim-jsx-pretty能给JSX代码添加高亮,很适合编写ReactJS等代码时使用。
vim-commentary
这个插件可以方便地注释掉一段代码或者取消注释,只需选中代码并键入<g-c>即可。
ALE(eslint和prettier)
ALE(Asynchronous Lint Engine)可以调用linter和代码修整工具,极大地提高工作效率,属于那种不用不知道,用了绝对不后悔的插件。我使用ALE主要是为了调用prettier。
实时预览(热重载)
实时查看修改的效果能够极大地改善工作流程。很多React或Gatsby等项目已经内置了该功能,但如果仅仅是创建一个简单的网页(HTML、CSS和JavaScript),那么Atom、Brackets或VSCode等编辑器可以把编辑中的页面的实时预览并列显示在另一个窗口中。
像我这种爱钻牛角尖的人很希望在Vim中也使用该功能。尽管有几个插件能实现,但我决定选择另一种方式。
我决定实现一个不依赖于编辑器的方案。基本思路就是,运行一个本地服务器,监视文件的改动,每当文件更新时就刷新服务器上的页面。
听起来似乎很复杂,但实际上非常简单,只需要在项目文件夹中安装并运行browser-sync即可。
我假设你已经安装了nodejs,所以只需要在全局安装browser-sync。
npm install -g browser-sync
安装完browser-sync之后,就能在任何文件夹中运行,创建一个本地服务器,然后自动显示文件夹内的index.html。
browser-sync start --server --files .
如果你使用的是Linux,bane可以创建一个别名来简化启动服务器的过程。打开主目录下的.bashrc文件,添加如下内容:
# Command line alias to start the browser-sync server
alias serve="browser-sync start --server --files ."
我更进一步,允许局域网内的其他机器访问我的服务器,这样就能在别的设备上进行测试:
# 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,化学工程师,自学成才的程序员。
本文为 CSDN 翻译,转载请注明来源出处。
【End】
为一个现代的代码编辑器,Atom有着各种流行编辑器都有的特性,功能上非常丰富,支持各种编程语言的代码高亮与大多数其他编辑器相比,Atom的语言支持已经算是覆盖非常全面了。另外,它的代码补全功能(也叫Snippets)也非常好用,你只需输入几个字符即可展开成各种常用代码,可以极大提高编程效率。Atom预装了四种UI和八种语法主题,包括深色和浅色。如果找不到您要查找的内容,您还可以安装由Atom社区创建的主题或创建自己的主题。
Atom帮助您更快地编写代码。在一个窗口中轻松浏览和打开单个文件,整个项目或多个项目。
使用默认插件,从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是一个著名的开源编辑器,是由Chris Wanstrath在2008年作为其个人的编外项目发展而来。据说在今年(2022)年底,这款编辑器也将进入关停状态。而且目前大部分程序员都把VS Code作为其最主要的开发工作,但是Atom本身的设计和代码实现都是非常优秀的,通过阅读它的源码,我们还是可以学到很多相关的编程技巧。
atom的代码结构非常清晰,整个项目可以分为两个部分,一个是atom本身的代码,另一个是atom的插件。atom本身的代码又可以分为两部分,一个是atom的核心业务逻辑,另一个是atom的UI代码。核心业务逻辑主要
是用来设置环境变量,调度窗口、调度系统资源等等。UI代码则主要负责处理atom的界面,比如菜单栏,工具栏,状态栏等等。
作为使用electron框架编写的应用程序,整体都是使用js来写的(早期是使用coffee来编写的),可以从其目录中看到,整个项目的目录结构如下:
|-src // 核心业务逻辑
|-|-main-process
|-|-|-atom-application.js
|-|-|-atom-environment.js
|-|-|-atom-window.js
|-static // UI代码
|-packages // 其它扩展包
...
众所周知,用Electron框架写成的应用,都可以分为主线程和渲染进程。对应到atom中,主线程的代码都是在src/main-process目录下,而渲染线程的代码则是直接src目录下。静态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;
// 下面省略大量代码,主要用于处理命令行参数,用来专门处理使用命令行打开atom的情况
// 真正的入口
const start = require(path.join(resourcePath, 'src', 'main-process', 'start'));
start(resourcePath, devResourcePath, startTime);
可以从上面代码看出,其实真正的处理入口还是在start函数中(src/main-process/start.js):
module.exports = function start(resourcePath, devResourcePath, startTime) {
// 处理错误情况
process.on('uncaughtException', function(error = {}) {
});
process.on('unhandledRejection', function(error = {}) {
});
// 初始化各种参数
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}`;
}
// 这个方法可以防止win10在任务栏中显示重复的atom图标
app.setAppUserModelId(appUserModelId);
app.on('open-file', addPathToOpen);
app.on('open-url', addUrlToOpen);
// 当应用关闭的时候,需要上报一些数据
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);
// 构造一个atomApplication对象
const AtomApplication = require(path.join(
args.resourcePath,
'src',
'main-process',
'atom-application'
));
// 并将之前的参数传入
AtomApplication.open(args);
});
};
从上面代码可以看出,前置处理也是各种参数的初始化,以及为了便于测试,做的一些定制处理。在应用初始化结束后,就会动态加载应用模块,构造 AtomApplication 实例。可以注意到,这里使用按需加载的目的是希望能够在需要的时候才会去加载对应的模块,这样可以减少内存的占用。
接着,我们来看一下atom-application.js的代码,这块代码量比较大,是整个atom的核心代码,我们先来看一下整体的结构:
// 是一个单列模式, 继承自`EventEmitter`模块,主要因为内部会大量应用事件处理机制来分发逻辑。
class AtomApplication extends EventEmitter {
static open(options) {
// 初始化一些参数
// 创建一个atomApplication对象
// 并将之前的参数传入
return new AtomApplication(options);
}
exit(status) {
app.exit(status);
}
constructor(options){}
async initialize(options) {}
}
程序启动的入口只有AtomApplication.open这一个方法,这个方法会创建一个AtomApplication对象,然后调用它的initialize方法,层层递进,再调用创建窗口、加载配置等方法,最终完成程序的启动。
其中,比较值得注意的是使用了一个叫做event-kit的模块。它是一个事件处理器模块,提供了一个事件处理器的抽象,可以让我们更容易地处理事件。最重要的作用是它实现了CompositeDisposable类,可以在需要的时候,释放资源。虽然javascript是一个有垃圾回收机制的语言,但是如果没有手动释放一些资源的话,会造成大量的内存占用。
在使用过程中,也十分简单
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);
// 在销毁的时候统一释放
this.disposable.dispose();
}
// 注册事件处理函数
handleEvents() {
// 省略其它代码,
// 在注册事件回调的时候,直接将对象添加到disposable的依赖中去
this.disposable.add(
ipcHelpers.on(app, 'before-quit', async event => {...})
);
}
}
atom作为一个编辑器,它的扩展机制是非常重要的。和其他的IDE类似,扩展机制也是使用的微内核模式(或者插件模式)来实现。微内核架构是一种十分常见的软件架构,它将应用系统分为两个部分:一个微内核和一组外部的插件。微内核负责管理插件,提供插件之间的通信机制,以及提供一些基础的服务。插件则负责提供具体的功能。这样的架构可以让我们更容易地扩展软件的功能,而不需要修改软件的核心代码。
在atom中,插件主要是通过package类来实现的。package是atom扩展的基本单元,它可以包含一些功能,比如语法高亮、代码提示、代码格式化等等,也提供了让第三方开发者扩展的能力。那么这些扩展是如何加载的呢?我们先来看一下package-manager.js的代码:
// 可以加载、激活、停用、卸载包
// 加载包读取并解析包的元数据和资源,例如快捷键、菜单、样式表等
// 激活包注册加载的资源并调用包的主模块的`activate()`方法
// 停用包取消注册包的资源并调用包的主模块的`deactivate()`方法
// 卸载包从包管理器中完全移除
// 可以通过`core.disabledPackages`配置项和调用`enablePackage()/disablePackage()`方法来启用/禁用包
class PackageManager {
preloadPackage(packageName, pack) {
...
}
loadPackages() {
...
}
enablePackage(packageName) {
...
}
// 触发事件,用来注册回调
onDidActivatePackage(callback) {
}
}
这个包管理器类PackageManager,可以管理扩展包的整个生命周期,主要负责包的加载、卸载、更新等操作。而所有的包都绑定在主内核的atom.packages这个全局变量上,我们可以通过这个变量来访问应用上加载的所有扩展。
那么packageManager是如何负责管理包的安装和卸载呢?:
class PackageManager {
constructor(packages) {
this.packages = packages;
}
getPackages() {
return this.packages;
}
getPackage(name) {
return this.packages.find(pkg => pkg.name === name);
}
// 禁用包,从内存中将包去除,然后通知应用程序或者扩展来执行禁用操作
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);
}
}
比如在扩展ui-watcher中,就可以在监听到did-deactivate-package事件后,执行一些清理操作:
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类,则包含了包的基础信息,包括键位设置、配置、样式等,并且有完整的生命周期。
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();
// 此处省略大量的细节
}
preload() {
// do something
}
load() {
// do something
}
unload() {
// do something
}
activate() {
// do something
}
deactivate() {
// do something
}
finishLoading() {
// do something
}
}
Package(扩展)实例本身要和主应用进行通信,atom是直接通过全局对象的方式进行调用的,这样做的好处是不用考虑通信的问题,但是也有一些弊端,比如不方便重构等。
在应用入口处,会将PackageManager实例挂载在应用实例上。后续我们可以通过atom.packages来访问包管理器实例,从而获取包的信息。
// atom-application.js
this.packages = new PackageManager({
... // 一堆的配置
});
this.packages.initialize(...);
而在渲染进程中,可以通过在window上挂载atom对象来访问包管理器实例,从而获取所有扩展包的信息,进行预加载操作。
// initialize-application-window.js
/ 初始化 AtomEnvironment
global.atom = new AtomEnvironment({
clipboard,
applicationDelegate: new ApplicationDelegate(),
enablePersistence: true
});
TextEditor.setScheduler(global.atom.views);
// 初始化应用窗口
global.atom.preloadPackages();
// ... 省略大量代码
module.exports = function({ blobStore }) {
// 省略大量代码
// 在startEditorWindows内部,当窗口初始化完成后,会正式调用`loadPackages`方法来加载所有的扩展包
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的扩展机制还是比较简单的,在各种扩展的生命周期中,都可以通过事件来进行通信,从而实现各种功能。这样一种实现,其实也可以在我们日常工作过程中加以借鉴。
*请认真填写需求信息,我们会在24小时内与您取得联系。