经常听到前端死不死的,做好你能做的,做别人不会的,你把这个态度养成了,死不死是无法阻挡你向上的--比如每天学点编译原理 编译原理是很多前端比较薄弱的,但是因为我工作项目需要,接触编译原理比较多,所以决定开个小系列,引导大家在工作,学习中运用编译原理相关思想
背景
需要借助babel实现框架升级,同时也支持解决业内可视化圈选元素ID稳定性问题,中间会用到一些AST语法树相关点,记录并供需要babel核心点掌握以及运用点的同学如项目库升级插件,国际化等可以运用里面的通用思路。
相关内容及代码随着学习,项目进展本文会持续更新敬请收藏
一键替换旧规则代码
现在由于公司项目 组件引用规则变了。比如 import {Button, Form} from '@xxx/xxx-ui' 变更为; import {Button} from '@xxx/xxx-ui'; import Form from '@xxx/xxx-form' ;有些历史项目存在几百个文件需要人工替换,考虑开发插件帮助他们替换。,仅仅核心代码,工具库暂时不放了。 其实核心:思路就是dfs整个ast树,然后寻找过滤目标节点,之后替换里面的AST节点,及遍历路径,之后再通过反写源代码解决
module.exports = (file, api, options) => {
const j = api.jscodeshift;
const root = j(file.source);
const windUiPkgNames = parseStrToArray('@xxx/xxx-ui', 'moment');
function importDeprecatedComponent(j, root) {
let hasChanged = false;
// import { Form, Mention } from '@xxx/xxx-ui';
root
.find(j.Identifier)
.filter(
path =>
deprecatedComponentNames.includes(path.node.name) &&
path.parent.node.type === 'ImportSpecifier' &&
windUiPkgNames.includes(path.parent.parent.node.source.value),
)
.forEach(path => {
hasChanged = true;
const importedComponentName = path.parent.node.imported.name;
const windUiPkgName = path.parent.parent.node.source.value;
// remove old imports
const importDeclaration = path.parent.parent.node;
importDeclaration.specifiers = importDeclaration.specifiers.filter(
specifier =>
!specifier.imported ||
specifier.imported.name !== importedComponentName,
);
// add new import from '@xxx/xxx-ui-form'..
const localComponentName = path.parent.node.local.name;
const config = [{
name:'Form',
moduleName: '@xxx/xxx-ui-form'
},{
name:'Table',
moduleName: '@xxx/xxx-ui-table'
},
{
name: 'Mention',
moduleName: '@xxx/xxx-ui-mention'
}
]
config.forEach((item)=>{
if(item.name === path.node.name){
addModuleDefaultImport(j, root, {
moduleName: item.moduleName,
importedName: importedComponentName,
localName: localComponentName,
before: windUiPkgName,
});
}
})
});
return hasChanged;
}
// step1. import deprecated components from '@xxx/xxx-ui'
// step2. cleanup antd import if empty
let hasChanged = false;
hasChanged = importDeprecatedComponent(j, root) || hasChanged;
if (hasChanged) {
windUiPkgNames.forEach(windUiPkgName => {
removeEmptyModuleImport(j, root, windUiPkgName);
});
}
return hasChanged
? root.toSource()
: null;
};
如何通过jscodeshift来实现标记稳定
背景:由于,目前可视化圈选为了能找到当前元素,需要稳定ID作为标识,传统的Xpath,不稳定,遂用babel ,遍历为当前目标元素构建具备当前元素属性的ast节点,但是由于项目迭代升级会导致每次都重新生成,还是会出现紊乱,学习jscodeshift ,想到可以把id直接加到源代码上,如果已绑定id就不再重新生成,这样就能彻底避免id紊乱问题。
业内方案
- 神策:之前做项目导调研,像圈选可视化埋点,如何确定组件唯一性,一般简化的直接是用Xpath 来解决,但是这种是依赖于元素之间的相对位置,一旦发生元素位置变动就会变,要么直接要求业务开发方,去直接手动标记对应特殊字符,但是这样的弊端点在于手动繁琐,而且容易出错。
- 之前项目有直接通过babel插件去获取当前组件的动态属性,或内部文字,但是发现还是不稳定,因为属性值很有可能重复,而且考虑的负担很重,因为要考虑多种可能性在实际落地项目的时候,发现属性多样,同时发现这个组件内部有可能还要嵌套组件,这种情况是非常难以控制的,初始对于属性获取,做了优先级队列,但是这个具体哪个属性应该优先被获取,绑定到当前元素尚,这个没有一个统一的标准,因此考虑迭代,方案
- 现有方案是,基于之前帮助团队做了一个升级插件,用到了jscodeshift,因此受到了启发,觉得可以通过反向修改源代码的方式来自动化帮组件设置自定义属性,并附带唯一uuid值,直接彻底确保当前组件完全唯一,从而从根本上去解决问题。
jscodeshift
- 这个库的作用在于基于ast操作的二次封装,同时支持cli 执行,方便快捷
- 比如当下很多antd, react,vue 都有推出类似的codemod库,来辅助升级,也因此受到启发公司项目可以借助jscodeshift来进行处理。
介绍梳理一下这个jscodeshift
能够修改AST节点,并基于recast ast-ast修改的库,并最终修改源代码的库。reast最大的价值在于能够做到修改源代码的时候,不动原有格式,并支持自定义修正美化原有的格式。 再本质上说,这个jscodeshift就是基于recast的高阶函数,就是装饰器思路
核心概念
AST 节点是一个带有一组特定字段的纯 JavaScript 对象,再去换言之,所就是包含当前元素信息的js对象。 而recast 本身又依赖于这个astTypes
在 jscodeshift 中,Collection 是一个非常重要的概念,它表示一组 AST(抽象语法树)节点。你可以将 Collection 理解为一个包含了一些 AST 节点的数组,但这个 "数组" 提供了很多额外的方法,可以让我们更方便地处理 AST。
Collection 提供的方法包括但不限于:
find:查找所有满足一定条件的节点。你可以给它传递一个节点类型(例如j.Identifier),那么它就会查找所有的这种类型的节点。你也可以给它传递一个对象,那么它就会查找所有的属性与这个对象匹配的节点。forEach:对Collection中的每个节点执行一段代码。replaceWith:将Collection中的每个节点替换为一个新的节点。remove:删除Collection中的所有节点。filter:基于一个函数筛选Collection中的节点。at:获取Collection中的某个特定节点。nodes:获取Collection中所有节点的原始数据(一个数组)。
ast节点
所谓的path就是在遍历整个ast树的时候的一个路径,并可以对其中某个遍历到的ast节点进行增删改 path { // 属性: node parent parentPath scope hub container key listKey
// 方法
get(key)
set(key, node)
inList()
getSibling(key)
getNextSibling()
getPrevSibling()
getAllPrevSiblings()
getAllNextSiblings()
isXxx(opts)
assertXxx(opts)
find(callback)
findParent(callback)
insertBefore(nodes)
insertAfter(nodes)
replaceWith(replacement)
replaceWithMultiple(nodes)
replaceWithSourceString(replacement)
remove()
traverse(visitor, state)
skip()
stop()
} 那path有哪些方法呢 同样也不需要记:
- get(key) 获取某个属性的 path
- set(key, node) 设置某个属性的值
- getSibling(key) 获取某个下标的兄弟节点
- getNextSibling() 获取下一个兄弟节点
- getPrevSibling() 获取上一个兄弟节点
- getAllPrevSiblings() 获取之前的所有兄弟节点
- getAllNextSiblings() 获取之后的所有兄弟节点
- find(callback) 从当前节点到根节点来查找节点(包括当前节点),调用 callback(传入 path)来决定是否终止查找
- findParent(callback) 从当前节点到根节点来查找节点(不包括当前节点),调用 callback(传入 path)来决定是否终止查找
- inList() 判断节点是否在数组中,如果 container 为数组,也就是有 listkey 的时候,返回 true
- isXxx(opts) 判断当前节点是否是某个类型,可以传入属性和属性值进一步判断,比如path.isIdentifier({name: 'a'})
- assertXxx(opts) 同 isXxx,但是不返回布尔值,而是抛出异常
- insertBefore(nodes) 在之前插入节点,可以是单个节点或者节点数组
- insertAfter(nodes) 在之后插入节点,可以是单个节点或者节点数组
- replaceWith(replacement) 用某个节点替换当前节点
- replaceWithMultiple(nodes) 用多个节点替换当前节点
- replaceWithSourceString(replacement) 解析源码成 AST,然后替换当前节点
- remove() 删除当前节点
- traverse(visitor, state) 遍历当前节点的子节点,传入 visitor 和 state(state 是不同节点间传递数据的方式)
- skip() 跳过当前节点的子节点的遍历
- stop() 结束所有遍历