疑问
- vue-devtools如何打开本地文件的
- 怎么知道我有什么编辑器
- 知道其怎么启动本地后,有什么收获,可以用来项目优化?
1. 原理
- 点击按钮后,插件发送了get请求:/__open-in-editor?file=src/App.vue
- 只要监听了该请求,我们才能响应事件,进行回调方法的调用(依赖 @vue/cli-service/lib/commands/serve.js实现该点,进行监听)
- 我们可以在监听的回调函数里,解析该请求的参数,从而得知要打开哪个文件。回调函数最好进行封装,可以通过文件参数的有无,决定是否去执行后续的打开文件操作。(依赖 launch-editor-middleware返回的方法实现该点,回调封装,根据参数有无调用打开文件操作)
- 有了文件参数的路径,便能根据不同系统的实现方式,猜测对应编辑器,执行打开对应编辑器且打开文件的操作。(依赖 launchEditor便实现该点,打开文件)
总结:
- @vue/cli-service/lib/commands/serve.js文件中监听__open-in-editor,以依赖launch-editor-middleware返回的函数launchEditorMiddleware作为响应函数。
- launchEditorMiddleware响应函数中,根据文件有无,调用依赖launch-editor的launch方法去执行打开文件的操作。
- launch方法根据不同系统,且猜测对应编辑器,去实现打开文件操作。
2. serve.js 服务(主入口)
js
// /node_modules/@vue/cli-service/lib/commands/serve.js
const launchEditorMiddleware = require('launch-editor-middleware')
before (app, server) {
// 监听__open-in-editor,并调用launchEditorMiddleware取得回调函数
app.use('/__open-in-editor', launchEditorMiddleware(() => console.log(
`To specify an editor, specify the EDITOR env variable or ` +
`add "editor" field to your Vue project config.\n`
)))
}
3. launch-editor-middleware 启动编辑器中间件
js
const url = require('url')
const path = require('path')
const launch = require('launch-editor')
module.exports = (specifiedEditor, srcRoot, onErrorCallback) => {
// specifiedEditor传过来的是一个函数,里面执行打印操作。() => console.log()
if (typeof specifiedEditor === 'function') {
// 该打印操作的函数赋值给onErrorCallback错误回调函数 本身初始化
onErrorCallback = specifiedEditor
specifiedEditor = undefined
}
// srcRoot如果是函数,会赋值给onErrorCallback错误回调函数
// 此时为undefined,不进入
if (typeof srcRoot === 'function') {
onErrorCallback = srcRoot
srcRoot = undefined
}
// process.cwd() 方法返回 Node.js 进程的当前工作目录。
// 此时srcRoot赋值为vue项目根目录
srcRoot = srcRoot || process.cwd()
// 返回响应函数
return function launchEditorMiddleware (req, res, next) {
// url.parse(urlStr, [parseQueryString], [slashesDenoteHost])
// 参1 url文本,
// 参2 是否解析查询字符串,调用了querystring module 去解析查询字符串
// 参3 斜线表示主机
/*
url.parse('/foo/bar?key=yeah')
{
search: '?key=yeah',
query: 'key=yeah',
pathname: '/foo/bar',
path: '/foo/bar?key=yeah',
href: '/foo/bar?key=yeah'
}
url.parse('/foo/bar?key=yeah', true) // 解析查询字符串
{
search: '?key=yeah',
query: [Object: null prototype] { key: 'yeah' },
pathname: '/foo/bar',
path: '/foo/bar?key=yeah',
href: '/foo/bar?key=yeah'
}
// 斜线表示主机 在 非// 下无效
url.parse('/foo/bar?key=yeah', false, true)
{
hostname: null,
search: '?key=yeah',
query: 'key=yeah',
pathname: '/foo/bar',
path: '/foo/bar?key=yeah',
href: '/foo/bar?key=yeah'
}
// 斜线表示主机 在 // 下有效
url.parse('//foo/bar?key=yeah', false, true)
{
hostname: 'foo',
hash: null,
search: '?key=yeah',
query: 'key=yeah',
pathname: '/bar',
href: '//foo/bar?key=yeah'
}
*/
// http://localhost:8080/__open-in-editor?file=src/App.vue
// query: {file: 'src/App.vue' }
const { file } = url.parse(req.url, true).query || {}
if (!file) { // 无文件相对路径,返回报错
res.statusCode = 500
res.end(`launch-editor-middleware: required query param "file" is missing.`)
} else { // 有文件相对路径,执行打开文件的操作
// 根目录+文件字段 可能包含行列信息, undefined, 错误函数 () => console.log()
launch(path.resolve(srcRoot, file), specifiedEditor, onErrorCallback)
res.end()
}
}
}
4. launch-editor 启动编辑器
4.1 launchEditor 启动编辑器主函数
js
/**
* @params file 文件绝对路径 可能包含行列信息
* @params specifiedEditor 指定编辑器
* @params onErrorCallback 错误回调
*/
function launchEditor (file, specifiedEditor, onErrorCallback) {
// 解析文件路径信息,后面有讲到该方法
const parsed = parseFile(file)
let { fileName } = parsed // 纯粹文件绝对路径
const { lineNumber, columnNumber } = parsed // 行信息 列信息
if (!fs.existsSync(fileName)) { // 文件是否存在 不存在则返回,后面有讲到该方法
return
}
// 上层传进来是undefined 所以此处不进入
// 如果为函数,则会跟launch-editor-middleware内部写的一样,赋值给错误回调函数,本身初始化
if (typeof specifiedEditor === 'function') {
onErrorCallback = specifiedEditor
specifiedEditor = undefined
}
// 错误回调函数进行封装,多了默认错误文件名提示,后面有讲到该方法
onErrorCallback = wrapErrorCallback(onErrorCallback)
// 猜测使用的编辑器,后面有讲到该方法
const [editor, ...args] = guessEditor(specifiedEditor)
if (!editor) { // 没有猜测到对应编辑器,就报错
onErrorCallback(fileName, null)
return
}
// linux平台
// 且 文件名路径以/mnt/开头
// 且 当前系统发行版本是微软标志
if (
process.platform === 'linux' &&
fileName.startsWith('/mnt/') &&
/Microsoft/i.test(os.release()) // process.platform , os.release(),后面有讲到该方法
) {
// Assume WSL / "Bash on Ubuntu on Windows" is being used, and
// that the file exists on the Windows file system.
// `os.release()` is "4.4.0-43-Microsoft" in the current release
// build of WSL, see: https://github.com/Microsoft/BashOnWindows/issues/423#issuecomment-221627364
// When a Windows editor is specified, interop functionality can
// handle the path translation, but only if a relative path is used.
/*
假设 WSL / "在 Windows 上的 Ubuntu 上的 Bash" 正在被使用,并且该文件存在于 Windows 文件系统中。
'os.release() 于 "4.4.0-43-Microsoft" 在 Wsl 的当前版本构建中, 请参阅: https://github.com/Microsoft/BashOnWindows/issues/423#issuecomment-221627364
指定 Windows 编辑器时,互操作功能可以处理路径转换,但前提是使用相对路径。
*/
// 如果满足平台条件,此处会取得文件相对项目的相对地址,后面有讲到该方法
// 项目地址 d:\\vue\\src\\components\\HelloWorld.vue
// 相对地址 src\\components\\HelloWorld.vue
fileName = path.relative('', fileName)
}
//如果有行信息
if (lineNumber) {
// 取得额外的坐标信息数组,后面有讲到该方法
const extraArgs = getArgumentsForPosition(editor, fileName, lineNumber, columnNumber)
// 推入参数数组里
args.push.apply(args, extraArgs)
} else {
args.push(fileName)
}
// 有子进程存在,且是终端文本编辑器
if (_childProcess && isTerminalEditor(editor)) {
// There's an existing editor process already and it's attached
// to the terminal, so go kill it. Otherwise two separate editor
// instances attach to the stdin/stdout which gets confusing.
// 如果现有一个编辑器进程并且连接到终端,就关闭他。否则两个独立编辑器实例将会在输出输入上混乱
// 传入SIGKILL 它会无条件地终止所有平台上的 Node.js。
_childProcess.kill('SIGKILL')
}
if (process.platform === 'win32') {
// On Windows, launch the editor in a shell because spawn can only
// launch .exe files.
// window系统,启动编辑器,并携带参数
// child_process.spawn(command[, args][, options])
// 方法使用给定的 command 和 args 中的命令行参数衍生新进程。
_childProcess = childProcess.spawn(
'cmd.exe', // 要运行的命令
['/C', editor].concat(args), // 字符串参数列表
{ stdio: 'inherit' } // 子进程的标准输入输出配置
)
} else {
_childProcess = childProcess.spawn(editor, args, { stdio: 'inherit' })
}
_childProcess.on('exit', function (errorCode) { // 监听退出子进程
_childProcess = null // 本身初始化
if (errorCode) { // 如果是错误退出的,错误码报错
onErrorCallback(fileName, '(code ' + errorCode + ')')
}
})
_childProcess.on('error', function (error) { // 错误码报错
onErrorCallback(fileName, error.message)
})
}
4.2 parseFile 解析文件路径信息
js
const positionRE = /:(\d+)(:(\d+))?$/
/**
* @params file 文件绝对路径 可能包含行列信息
*/
function parseFile (file) {
// 过滤掉 :数字:数字,提取纯文件绝对路径
const fileName = file.replace(positionRE, '')
const match = file.match(positionRE)
const lineNumber = match && match[1] // 行信息
const columnNumber = match && match[3] // 列信息
return {
fileName,
lineNumber,
columnNumber
}
}
4.3 fs.existsSync 判断路径是否存在
如果路径存在则返回 true,否则返回 false。
js
import { existsSync } from 'fs';
if (existsSync('/etc/passwd')) {
console.log('The path exists.');
}
4.4 wrapErrorCallback 错误回调函数封装
js
function wrapErrorCallback (cb) {
return (fileName, errorMessage) => {
console.log()
// 提示无法打开文件
// path.basename(fileName) 返回路径最后一部分,一般为文件名,后面有讲到该方法
console.log(
chalk.red('Could not open ' + path.basename(fileName) + ' in the editor.')
)
if (errorMessage) {
// 拼接句号
if (errorMessage[errorMessage.length - 1] !== '.') {
errorMessage += '.'
}
// 有错误信息则进行提示错误
console.log(
chalk.red('The editor process exited with an error: ' + errorMessage)
)
}
console.log()
// 错误回调调用
if (cb) cb(fileName, errorMessage)
}
}
4.5 guessEditor 猜测使用的编辑器
js
const childProcess = require('child_process')
/**
* @params specifiedEditor 指定编辑器
*/
function guessEditor (specifiedEditor) {
if (specifiedEditor) {
// shell语句解析,此处传路径,除了shell中的特殊符号,基本按空格分隔数组。
// 目录最好不要含空格,否则无法正常找到
return shellQuote.parse(specifiedEditor)
}
// We can find out which editor is currently running by:
// `ps x` on macOS and Linux
// `Get-Process` on Windows
try {
if (process.platform === 'darwin') { // Mac OS
// 子进程运行指令,该方法在子进程完全关闭之前不会返回。
// 取出现在启用的编辑器
const output = childProcess.execSync('ps x').toString()
// Mac 市场常见的编辑器目录数组 比对成功则返回
const processNames = Object.keys(COMMON_EDITORS_OSX)
for (let i = 0; i < processNames.length; i++) {
const processName = processNames[i]
if (output.indexOf(processName) !== -1) {
return [COMMON_EDITORS_OSX[processName]]
}
}
} else if (process.platform === 'win32') { // windows
// 子进程运行指令,该方法在子进程完全关闭之前不会返回。
// 取出现在启用的编辑器
const output = childProcess
.execSync('powershell -Command "Get-Process | Select-Object Path"', {
stdio: ['pipe', 'pipe', 'ignore']
})
.toString()
const runningProcesses = output.split('\r\n')
for (let i = 0; i < runningProcesses.length; i++) {
// `Get-Process` sometimes returns empty lines
if (!runningProcesses[i]) { // 空行跳过
continue
}
// 路径去空格
const fullProcessPath = runningProcesses[i].trim()
// 取出文件名
const shortProcessName = path.basename(fullProcessPath)
// 市场常见的编辑器数组 比对成功则返回
if (COMMON_EDITORS_WIN.indexOf(shortProcessName) !== -1) {
return [fullProcessPath]
}
}
} else if (process.platform === 'linux') { // linux
// --no-heading No header line
// x List all processes owned by you
// -o comm Need only names column
// 子进程运行指令,该方法在子进程完全关闭之前不会返回。
// 取出现在启用的编辑器
const output = childProcess
.execSync('ps x --no-heading -o comm --sort=comm')
.toString()
const processNames = Object.keys(COMMON_EDITORS_LINUX)
// 市场常见的编辑器数组 比对成功则返回
for (let i = 0; i < processNames.length; i++) {
const processName = processNames[i]
if (output.indexOf(processName) !== -1) {
return [COMMON_EDITORS_LINUX[processName]]
}
}
}
} catch (error) {
// Ignore...
}
// Last resort, use old skool env vars
// 最后一招,使用老式的环境变量
if (process.env.VISUAL) {
return [process.env.VISUAL]
} else if (process.env.EDITOR) {
return [process.env.EDITOR]
}
return [null]
}
4.6 path.basename 返回 path 的最后一部分
方法返回 path 的最后一部分。 传入参2,可以将尾随的目录分隔符忽略
js
path.basename('/foo/bar/baz/asdf/quux.html');
// 返回: 'quux.html'
path.basename('/foo/bar/baz/asdf/quux.html', '.html');
// 返回: 'quux'
4.7 process.platform 运行Node.js进程的操作系统平台
返回标识运行 Node.js 进程的操作系统平台的字符串
js
import { platform } from 'process';
console.log(platform); // 我这是 win32
4.8 os.release 返回操作系统的发行版本的方法
js
os.release(); // 我这是 10.0.19xxx
4.9 path.relative 寻找两个绝对路径的相对关系
path.relative(from, to)
方法根据当前工作目录返回从 from 到 to 的相对路径。 如果 from 和 to 都解析为相同的路径(在分别调用 path.resolve() 之后),则返回零长度字符串。
如果零长度字符串作为 from 或 to 传入,则将使用当前工作目录而不是零长度字符串
js
path.relative('C:\\orandea\\test\\aaa', 'C:\\orandea\\impl\\bbb');
// 返回: '..\\..\\impl\\bbb'
4.10 getArgumentsForPosition 取得坐标信息数组
js
const path = require('path')
// normalize file/line numbers into command line args for specific editors
/**
* @params editor 编辑器路径 但传过来可能也只有编辑器名
* @params fileName 文件名
* @params lineNumber 行信息
* @params columnNumber 列信息
*/
function getArgumentsForPosition (
editor,
fileName,
lineNumber,
columnNumber = 1
) {
// 返回路径最后一部分 且去除.exe|cmd|bat后缀,即只取得编辑器名
const editorBasename = path.basename(editor).replace(/\.(exe|cmd|bat)$/i, '')
// 返回对应编辑器行列信息封装
switch (editorBasename) {
case 'atom':
case 'Atom':
case 'Atom Beta':
case 'subl':
case 'sublime':
case 'sublime_text':
case 'wstorm':
case 'charm':
return [`${fileName}:${lineNumber}:${columnNumber}`]
case 'notepad++':
return ['-n' + lineNumber, fileName]
case 'vim':
case 'mvim':
return [`+call cursor(${lineNumber}, ${columnNumber})`, fileName]
case 'joe':
return ['+' + `${lineNumber}`, fileName]
case 'emacs':
case 'emacsclient':
return [`+${lineNumber}:${columnNumber}`, fileName]
case 'rmate':
case 'mate':
case 'mine':
return ['--line', lineNumber, fileName]
case 'code':
case 'code-insiders':
case 'Code':
return ['-r', '-g', `${fileName}:${lineNumber}:${columnNumber}`]
case 'appcode':
case 'clion':
case 'clion64':
case 'idea':
case 'idea64':
case 'phpstorm':
case 'phpstorm64':
case 'pycharm':
case 'pycharm64':
case 'rubymine':
case 'rubymine64':
case 'webstorm':
case 'webstorm64':
return ['--line', lineNumber, fileName]
}
// For all others, drop the lineNumber until we have
// a mapping above, since providing the lineNumber incorrectly
// can result in errors or confusing behavior.
return [fileName]
}
4.11 isTerminalEditor 是否终端文本编辑器
js
function isTerminalEditor (editor) {
switch (editor) {
case 'vim':
case 'emacs':
case 'nano':
return true
}
return false
}