前篇回顾
前篇实现了进入组件,这篇将实现点击元素跳转到源码的对应行上去。
本篇是 还在找源文件?从 vue 前端页面一键打开源码所在文件的下篇
没看过前一篇的的最好看下,因为有些东西需要建立在上篇的基础上。
思路
1 遇到 vue 文件额外用一个 loader 解析一下,在 loader 中用 parser5 将元素对应的源码行列信息以 html 属性的方式加在标签中
2 给 document 绑定点击事件(事件代理)发送打开请求
3 webpack-dev-server 提供打开文件的服务
具体实现
loader 实现
// open-source\add-element-location-loader.js
const parse5 = require('parse5');
module.exports = function (source) {
const templateSrc = source
const { resourcePath } = this
function forceTemplate(html) {
// -------------- 解析成 html 语法树
const htmlAst = parse5.parseFragment(html, {
sourceCodeLocationInfo: true,
});
// -------------- 给标签增加源代码位置信息
function addLocation(node) {
const { sourceCodeLocation } = node
if (sourceCodeLocation) {
const { startLine, startCol } = sourceCodeLocation
node.attrs && node.attrs.push({
name: 'source-code-location',
value: `${resourcePath}:${startLine}:${startCol}`
})
}
}
// -------------- 递归
function walk(node) {
addLocation(node)
node.childNodes && node.childNodes.forEach((childNode) => {
walk(childNode)
})
}
walk(htmlAst)
// -------------- 转换回字符串
const str = parse5.serialize(htmlAst);
return str
}
// -------------- 编辑template的内容 --------------
const getForcedTemplate = (content, callback) => {
return content.replace(/<template[\\s\\S]*>([\s\S]*)<\/template>/, (str, str1) => {
return '<template>' + callback(str1) + '<\/template>';
});
}
const res = getForcedTemplate(templateSrc, forceTemplate)
return res
}
找到 template 的部分
使用 replace 处理并替换 template 的部分,这里的是我们要处理的
const getForcedTemplate = (content, callback) => {
return content.replace(/<template[\\s\\S]*>([\s\S]*)<\/template>/, (str, str1) => {
return '<template>' + callback(str1) + '<\/template>';
});
}
const res = getForcedTemplate(templateSrc, forceTemplate)
解析成 html 语法树
将 html 解析成语法树,并在解析中夹带输出源码位置信息
const htmlAst = parse5.parseFragment(html, {
sourceCodeLocationInfo: true,
});
增加源代码位置信息
将行列信息加在 html 元素的属性中,并递归处理一下
// -------------- 给标签增加源代码位置信息
function addLocation(node) {
const { sourceCodeLocation } = node
if (sourceCodeLocation) {
const { startLine, startCol } = sourceCodeLocation
node.attrs && node.attrs.push({
name: 'source-code-location',
value: `${resourcePath}:${startLine}:${startCol}`
})
}
}
// -------------- 递归
function walk(node) {
addLocation(node)
node.childNodes && node.childNodes.forEach((childNode) => {
walk(childNode)
})
}
walk(htmlAst)
转回字符串
别忘了最后再转回字符串
const str = parse5.serialize(htmlAst);
打开文件服务逻辑
在 before 钩子函数中加入打开文件的逻辑,后面要加到 devServer 的配置中
// open-source\server.js
const config = {
before: function (app) {
const child_process = require("child_process");
// 打开源码
app.get("/code", function (req, res) {
child_process.exec(`code ${req.query.filePath}`);
res.send("文件打开成功!");
});
}
};
module.exports = config
webpack 中引入配置
引入刚才的 loader,和 devServer 的配置
// vue.config.js
const path = require('path')
const addElementLocationLoader = path.resolve(__dirname, './open-source/add-element-location-loader.js')
module.exports = {
devServer: {
port: 8700,
...require("./open-source/server")
},
chainWebpack: config => {
// ...
config.module
.rule('vue')
.use('vue-loader')
.loader('vue-loader')
.tap(options => {
options.exposeFilename = true
return options
})
.end()
.use(addElementLocationLoader)
.loader(addElementLocationLoader)
.end()
},
}
前端的交互
快捷键打开源码
给 document 绑定点击事件,使用事件代理的方式分配到每个元素。根据元素中有的位置信息发出打开文件的请求给 devServer
// open-source\client.js
// -------------- 打开编辑器的函数
function launchEditor(filePath) {
if (!filePath) return
console.log(filePath, 'filePath')
const devServerPort = 8700
axios
.get(`http://localhost:${devServerPort}/code`, {
params: {
filePath: `-g ${filePath}`
}
})
}
// 点击元素打开源码 (定位到行列)
function openSourceCode(e) {
if (e.ctrlKey) {
console.log(e.target, 'e.target')
e.preventDefault()
// 找到标签
let targetTag = e.target
// 跳转源码
const filePath = targetTag.getAttribute('source-code-location')
launchEditor(filePath)
}
}
document.addEventListener('click', openSourceCode)
// main.js
import "../open-source/client";
因为打开源码只需要在开发环境中使用,所以 main.js 中需要注意根据打包的环境变量判断是否引入相关逻辑
注意事项
业务代码的 html 属性需要规范
由于 loader 会重新解析标签,所以标签的 html 属性务必要用规范的 kabe-case ,否则会导致编译问题
// 正确
<div custom-prop="xxx">1</div>
// 错误
<div customProp="xxx">1</div>
实际效果
点击元素,直接跳转到源代码的行,再也不用手动找文件了!
结语
到此基本实现了最初的诉求。这只是一个基础的原理解析,如果有错误或更好的思路,欢迎大家指正和补充!!