给源代码打标记

一、背景

为什么需要要给源代码打标记? 给源代码打标记是为了给源码项目提供可编辑能力。源码项目有了编辑能力可以做很多事情,比如可视化开发、可视化埋点、修改文案,代码定位等等;埋点和修改文案这种需求完全可以交给需求方自己来做。 如下图可以对源代码项目进行编辑

二、实现方案

两种方案

1.基于代码位置

先来个图 如图标记路径和位置可定位到具体的文件和行号就可以定位到具体的代码上,实现编辑能力。 实现思路: 实现无非是在打包构建时给jsx代码增加path属性,以vite为例给babel写个jsx插件,设置在开发环境起作用:

markdown 复制代码
react(
  {
    babel: {
      plugins: [
        [
          'firefly-babel-jsx/dist',
          {
            env: 'development',
          },
        ],
      ],
    },
  },
),

具体的代码实现

markdown 复制代码
export default function transformPathJsComponents(babel: Babel): {
	return {
    visitor: {
      Program: {
        enter(path, state) {
          path.traverse({
            FunctionDeclaration: {
              enter(path, state) {
                
              },
              exit(path, state) {
              
              },
              TaggedTemplateExpression(path) {
                
              },
              JSXElement(path) {
                  // 重点在这里
                  const id = addExpressionToStorage({
                    name,
                    loc: path.node.loc,
                    wrappingComponentId: currentWrappingComponentId,
                  });
                  const newAttr = t.jSXAttribute(
                    t.jSXIdentifier('data-path-id'),
                    t.jSXExpressionContainer(
                      t.stringLiteral(
                        createDataId(fileStorage, id),
                      ),
                    ),
                  );

                  const dataUnique = t.jSXAttribute(
                    t.jSXIdentifier('data-unique'),
                    t.jSXExpressionContainer(
                      t.stringLiteral(
                        `${path.node.start}::${path.node.end}`,
                      ),
                    ),
                  );
                  path.node.openingElement.attributes.push(newAttr, dataUnique);
              }
            }  
        }
      }
    }
  }  
}

上面代码只是提供思路的示例代码,重点是对ast进行操作,可以配合这个网站写代码 ts-ast-viewer.com/,具体的代码在这里:github.com/sparrow-js/... 使用位置标记方案的问题: 1.上下逻辑不清晰,实际在结合可视化编辑时比如我们新增了一个组件节点然后在格式化想要找到新增的节点是哪个就比较困难。 2.性能体验问题,改一下代码很可能导致所有标记都变了。

2.使用属性+文件路径

先来个图 如图标记变成了一个看似随机数的属性data-uid="424" 实现思路 1.生成ID

markdown 复制代码
import Hash from 'object-hash';
// 创建Hash值
const hash = Hash({
    fileName: sourceFile.fileName,
    name: parseJSXElementName((node as any).openingElement, sourceFile),
    props,
});

export const atoz = [
    'a',
    'b',
    'c',
    'd',
    'e',
    'f',
    'g',
    'h',
    'i',
    'j',
    'k',
    'l',
    'm',
    'n',
    'o',
    'p',
    'q',
    'r',
    's',
    't',
    'u',
    'v',
    'w',
    'x',
    'y',
    'z',
];

export function generateConsistentUID(
    possibleStartingValue: string,
    ...existingIDSets: Array<Set<string>>
): string {
    function alreadyExistingID(idToCheck: string): boolean {
        return existingIDSets.some((s) => s.has(idToCheck));
    }
    
    // 截取字符串的三个字符,判断是否存在存在往后移动三位
    if (possibleStartingValue.length >= 3) {
        const maxSteps = Math.floor(possibleStartingValue.length / 3);
        for (let step = 0; step < maxSteps; step++) {
          const possibleUID = possibleStartingValue.substring(step * 3, (step + 1) * 3);

          if (!alreadyExistingID(possibleUID)) {
            return possibleUID;
          }
        }
    }

  	// 循环取出uid
    for (let firstChar of atoz) {
        for (let secondChar of atoz) {
          for (let thirdChar of atoz) {
            const possibleUID = `${firstChar}${secondChar}${thirdChar}`;

            if (!alreadyExistingID(possibleUID)) {
              return possibleUID;
            }
          }
        }
    }
}

通过上面代码获取到uid,hash值是通过属性标签+属性+文件,属性变化会影响生成的uid,实际在可视化编辑时改变元素的属性需要让uid保持不变,避免当前元素包裹的其他元素路径结构变化,所以就需要做diff操作,存一份旧的ast树,生成的新的ast树跟旧的做对比如果只是属性变化就把旧的uid替换到新的上面去大致代码如下

markdown 复制代码
function fixArrayElements<T>(
    oldElements: any[],
    newElements: any[],
) {
    let oldElementIndexesUsed: Set<number> = new Set();
    let workingArray: T[] = [];
    newElements.forEach((newElement, newElementIndex) => {
        const { uid } = newElement;
        let possibleOldElement: T | undefined;

        if (!oldElements) {
            workingArray[newElementIndex] = newElement;
        } else {
            oldElements.forEach((oldElement, oldElementIndex) => {
                const oldUid = oldElement.uid;
                if (oldUid === uid) {
                    possibleOldElement = oldElement;
                    oldElementIndexesUsed.add(oldElementIndex);
                }
            });
        }

        if (possibleOldElement != null) {
            workingArray[newElementIndex] = fixJSXElementUIDs(possibleOldElement, newElement);
        }
    });

    return newElements.map((newElement, newElementIndex) => {
        if (newElementIndex in workingArray) {
            return workingArray[newElementIndex];
        } else if (!oldElementIndexesUsed.has(newElementIndex)) {
            const oldElement = oldElements?.[newElementIndex];
            return fixJSXElementUIDs(oldElement, newElement);
        } else {
            return fixJSXElementUIDs(newElement, newElement);
        }
    });
}

具体代码在这里:github.com/sparrow-js/... 可以看上面class从App改到Appuid,data-uid="c0d",uid没有变化。还可以使用原id对当前元素编辑。 通过第二种方式让源代码可视化编辑的基础就搞定了。实现细节可以看源代码

相关推荐
掘金安东尼17 小时前
官方:什么是 Vite+?
前端·javascript·vue.js
柒崽17 小时前
ios移动端浏览器,vh高度和页面实际高度不匹配的解决方案
前端
渣哥17 小时前
你以为 Bean 只是 new 出来?Spring BeanFactory 背后的秘密让人惊讶
javascript·后端·面试
烛阴17 小时前
为什么游戏开发者都爱 Lua?零基础快速上手指南
前端·lua
大猫会长17 小时前
tailwindcss出现could not determine executable to run
前端·tailwindcss
Moonbit18 小时前
MoonBit Pearls Vol.10:prettyprinter:使用函数组合解决结构化数据打印问题
前端·后端·程序员
533_18 小时前
[css] border 渐变
前端·css
云中雾丽18 小时前
flutter的dart语言和JavaScript的消息循环机制的异同
前端
地方地方18 小时前
Vue依赖注入:provide/inject 问题解析与最佳实践
前端·javascript·面试
云中雾丽18 小时前
dart的继承和消息循环机制
前端