还在找源文件?在 vue 前端页面一键打开源码所在行

4,134 阅读2分钟

前篇回顾

前篇实现了进入组件,这篇将实现点击元素跳转到源码的对应行上去。

本篇是 还在找源文件?从 vue 前端页面一键打开源码所在文件的下篇

没看过前一篇的的最好看下,因为有些东西需要建立在上篇的基础上。

思路

1 遇到 vue 文件额外用一个 loader 解析一下,在 loader 中用 parser5 将元素对应的源码行列信息以 html 属性的方式加在标签中

2 给 document 绑定点击事件(事件代理)发送打开请求

3 webpack-dev-server 提供打开文件的服务

具体实现

github 地址

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>

实际效果

点击元素,直接跳转到源代码的行,再也不用手动找文件了!

结语

到此基本实现了最初的诉求。这只是一个基础的原理解析,如果有错误或更好的思路,欢迎大家指正和补充!!