一、背景
为什么需要要给源代码打标记? 给源代码打标记是为了给源码项目提供可编辑能力。源码项目有了编辑能力可以做很多事情,比如可视化开发、可视化埋点、修改文案,代码定位等等;埋点和修改文案这种需求完全可以交给需求方自己来做。 如下图可以对源代码项目进行编辑
二、实现方案
两种方案
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对当前元素编辑。 通过第二种方式让源代码可视化编辑的基础就搞定了。实现细节可以看源代码