忙里偷闲逛了下,看到一个有意思的工具:react-dev-inspector点击页面元素在编辑器打开对应代码
    详细介绍文档可见 我点了页面上的元素,VSCode 乖乖打开了对应的组件?原理揭秘,对于大型项目的开发维护,这样的调试神器还挺不错。
    原理也很巧妙,主要有两个问题需要解决:

    1. 怎样知道点击的元素是哪个组件,对应的文件以及行列?
    2. 编辑器怎样打开对应的文件,并跳转到对应的行列?


    第一个问题,工具的作者利用webpack loader,在webpack构建时,操作生成的AST,给每个元素添加特殊属性表示行列和文件路径,在点击元素时就可以知道对应的文件和行列信息。

    第二个问题,获取到点击元素的信息后,怎样用编辑器打开相应的文件呢?这里作者的方法:借助 fetch 发送了一个请求到本机的服务端,利用服务端执行脚本命令code src/Inspector/index.ts 这样的命令来打开 VSCode。借助react-dev-utils实现,监听一个特殊的url fetch(‘/__open-stack-frame-in-editor?fileName=xxx’)。

    翻了下文章的评论区,有同学提到vue-devtools本身就支持这个功能了,那必须去一探究竟啊。

    安装完vue-devtools后,打开一个vue项目,在Components标签页,选中某个组件,右上角确实有个open in editor的图标,点击,可惜我本地没能打开编辑器和对应的文件,控制台有打印”File src/common/selectComponent.vue opened in editor”,这个按下不表,来看看vue里大概是怎么做的。

    我们发现在vue源码的flushSchedulerQueue方法中有这么一段代码:

    1. // devtool hook
    2. /* istanbul ignore if */
    3. if (devtools && config.devtools) {
    4. devtools.emit('flush');
    5. }

    即在更新后,通知devtools执行flush
    backend.js:

    1. /**
    2. * Called on every Vue.js batcher flush cycle.
    3. * Capture current component tree structure and the state
    4. * of the current inspected instance (if present) and
    5. * send it to the devtools.
    6. */
    7. function flush () {
    8. let start
    9. functionalIds.clear()
    10. captureIds.clear()
    11. if (false) {}
    12. const payload = Object(util["y" /* stringify */])({
    13. // 根据id获取到对应的instance
    14. inspectedInstance: getInstanceDetails(currentInspectedId),
    15. // 此方法会调capture,然后调用mark,设置src_instanceMap,key为instance的id
    16. // rootInstances通过监听一些钩子函数得到
    17. instances: findQualifiedChildrenFromList(rootInstances)
    18. })
    19. if (false) {}
    20. src_bridge.send('flush', payload)
    21. }
    22. /**
    23. * Get the detailed information of an inspected instance.
    24. *
    25. * @param {Number} id
    26. */
    27. function getInstanceDetails (id) {
    28. const instance = src_instanceMap.get(id)
    29. if (!instance) {
    30. const vnode = findInstanceOrVnode(id)
    31. if (!vnode) return {}
    32. const data = {
    33. id,
    34. name: Object(util["m" /* getComponentName */])(vnode.fnOptions),
    35. file: vnode.fnOptions.__file || null,
    36. state: processProps({ $options: vnode.fnOptions, ...(vnode.devtoolsMeta && vnode.devtoolsMeta.renderContext.props) }),
    37. functional: true
    38. }
    39. return data
    40. } else {
    41. const data = {
    42. id: id,
    43. name: getInstanceName(instance),
    44. state: getInstanceState(instance)
    45. }
    46. let i
    47. if ((i = instance.$vnode) && (i = i.componentOptions) && (i = i.Ctor) && (i = i.options)) {
    48. // openInEditor时打开的file路径
    49. data.file = i.__file || null
    50. }
    51. return data
    52. }
    53. }
    54. // src_instanceMap 在哪儿设置的呢?
    55. /**
    56. * Mark an instance as captured and store it in the instance map.
    57. *
    58. * @param {Vue} instance
    59. */
    60. function mark (instance) {
    61. if (!src_instanceMap.has(instance.__VUE_DEVTOOLS_UID__)) {
    62. src_instanceMap.set(instance.__VUE_DEVTOOLS_UID__, instance)
    63. instance.$on('hook:beforeDestroy', function () {
    64. src_instanceMap.delete(instance.__VUE_DEVTOOLS_UID__)
    65. })
    66. }
    67. }
    68. /**
    69. * Capture the meta information of an instance. (recursive)
    70. *
    71. * @param {Vue} instance
    72. * @return {Object}
    73. */
    74. function capture (instance, index, list) {
    75. // 省略...
    76. // instance._uid is not reliable in devtools as there
    77. // may be 2 roots with same _uid which causes unexpected
    78. // behaviour
    79. instance.__VUE_DEVTOOLS_UID__ = getUniqueId(instance)
    80. // Dedupe
    81. if (captureIds.has(instance.__VUE_DEVTOOLS_UID__)) {
    82. return
    83. } else {
    84. captureIds.set(instance.__VUE_DEVTOOLS_UID__, undefined)
    85. }
    86. mark(instance)
    87. const ret = {
    88. uid: instance._uid,
    89. id: instance.__VUE_DEVTOOLS_UID__,
    90. name,
    91. renderKey: getRenderKey(instance.$vnode ? instance.$vnode['key'] : null),
    92. inactive: !!instance._inactive,
    93. isFragment: !!instance._isFragment,
    94. children: instance.$children
    95. .filter(child => !child._isBeingDestroyed)
    96. .map(capture)
    97. .filter(Boolean)
    98. }
    99. // 省略...
    100. return ret
    101. }

    我们看到backend.js中的flush最后执行了src_bridge.send(‘flush’, payload),去github上看看app-fronted的代码。
    src/index.js中

    1. bridge.on('flush', payload => {
    2. store.commit('components/FLUSH', parse(payload))
    3. })

    views/components/modules.js

    1. const mutations = {
    2. FLUSH (state, payload) {
    3. let start
    4. if (process.env.NODE_ENV !== 'production') {
    5. start = window.performance.now()
    6. }
    7. // Instance ID map
    8. // + add 'parent' properties
    9. const map = {}
    10. function walk (instance) {
    11. map[instance.id] = instance
    12. if (instance.children) {
    13. instance.children.forEach(child => {
    14. child.parent = instance
    15. walk(child)
    16. })
    17. }
    18. }
    19. payload.instances.forEach(walk)
    20. // Mutations
    21. state.instances = Object.freeze(payload.instances)
    22. state.inspectedInstance = Object.freeze(payload.inspectedInstance)
    23. state.inspectedInstanceId = state.inspectedInstance ? state.inspectedInstance.id : null
    24. state.instancesMap = Object.freeze(map)
    25. if (process.env.NODE_ENV !== 'production') {
    26. Vue.nextTick(() => {
    27. console.log(`devtools render took ${window.performance.now() - start}ms.`)
    28. if (inspectTime != null) {
    29. console.log(`inspect component took ${window.performance.now() - inspectTime}ms.`)
    30. inspectTime = null
    31. }
    32. })
    33. }
    34. state.loading = false
    35. }
    36. }

    至此,instances,inspectedInstance 等均已存在store中。
    看看模板中的点击open in editor做了什么操作:

    1. // 从store中之前存的取instances,inspectedInstance
    2. computed: mapState('components', [
    3. 'instances',
    4. 'inspectedInstance',
    5. 'loading'
    6. ])
    7. <component-inspector
    8. v-if="defer(3)"
    9. slot="right"
    10. :target="inspectedInstance"
    11. :loading="loading"
    12. />
    13. // component-inspector
    14. <a
    15. v-if="fileIsPath"
    16. v-tooltip="$t('ComponentInspector.openInEditor.tooltip', { file: target.file })"
    17. class="button"
    18. @click="openInEditor"
    19. >
    20. <VueIcon icon="launch" />
    21. <span>Open in editor</span>
    22. </a>
    23. openInEditor () {
    24. // 此处的target即为inspectedInstance
    25. const file = this.target.file
    26. openInEditor(file)
    27. }

    openInEditor方法定义在@utils/util中,发送特殊请求,然后借鉴react-dev-utils的方法实现打开对应编辑器(具体实现可查看文章开头提到的链接)

    1. export function openInEditor (file) {
    2. // Console display
    3. const fileName = file.replace(/\\/g, '\\\\')
    4. const src = `fetch('${SharedData.openInEditorHost}__open-in-editor?file=${encodeURI(file)}').then(response => {
    5. if (response.ok) {
    6. console.log('File ${fileName} opened in editor')
    7. } else {
    8. const msg = 'Opening component ${fileName} failed'
    9. const target = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : {}
    10. if (target.__VUE_DEVTOOLS_TOAST__) {
    11. target.__VUE_DEVTOOLS_TOAST__(msg, 'error')
    12. } else {
    13. console.log('%c' + msg, 'color:red')
    14. }
    15. console.log('Check the setup of your project, see https://github.com/vuejs/vue-devtools/blob/master/docs/open-in-editor.md')
    16. }
    17. })`
    18. if (isChrome) {
    19. chrome.devtools.inspectedWindow.eval(src)
    20. } else {
    21. // eslint-disable-next-line no-eval
    22. eval(src)
    23. }
    24. }

    至此,大致流程就结束了。