给源代码打标记

一、背景

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

二、实现方案

两种方案

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对当前元素编辑。 通过第二种方式让源代码可视化编辑的基础就搞定了。实现细节可以看源代码

相关推荐
七夜zippoe1 分钟前
前端开发中的难题及解决方案
前端·问题
晓13131 小时前
JavaScript加强篇——第七章 浏览器对象与存储要点
开发语言·javascript·ecmascript
Hockor1 小时前
用 Kimi K2 写前端是一种什么体验?还支持 Claude Code 接入?
前端
杨进军1 小时前
React 实现 useMemo
前端·react.js·前端框架
海底火旺1 小时前
浏览器渲染全过程解析
前端·javascript·浏览器
你听得到111 小时前
揭秘Flutter图片编辑器核心技术:从状态驱动架构到高保真图像处理
android·前端·flutter
驴肉板烧凤梨牛肉堡1 小时前
浏览器是否支持webp图像的判断
前端
Xi-Xu1 小时前
隆重介绍 Xget for Chrome:您的终极下载加速器
前端·网络·chrome·经验分享·github
摆烂为不摆烂1 小时前
😁深入JS(九): 简单了解Fetch使用
前端
杨进军1 小时前
React 实现多个节点 diff
前端·react.js·前端框架