原文链接: babel 插件为react元素自动添加属性
上一篇: clip-path 绘制css常见图形 制作有趣的动画
下一篇: js 生成器 协程
参考
安装
yarn add babel
yarn add @babel/plugin-transform-react-jsx
webpack 处理 React 文件(js/jsx)使用 babel-loader,babel 就是我们的 JavaScript 编译器,它接收我们的源代码作为输入,产出编译后的可运行于浏览器的目标代码作为输出。babel 支持插件(plugin),可以视作编译器前端与后端之间的中间件:前端根据源代码生成抽象语法树(AST)等,后端根据抽象语法树生成目标代码,而插件作为中间件则是在生成目标代码之前对抽象语法树做相应的修改。
转换简单的js
const parser = require('@babel/parser')
let code = 'let result = x + y'
let ast = parser.parse(code)
console.log(ast)
console.log(ast.program.body)
Node {
type: 'File',
start: 0,
end: 18,
loc: SourceLocation {
start: Position { line: 1, column: 0 },
end: Position { line: 1, column: 18 }
},
errors: [],
program: Node {
type: 'Program',
start: 0,
end: 18,
loc: SourceLocation { start: [Position], end: [Position] },
sourceType: 'script',
interpreter: null,
body: [ [Node] ],
directives: []
},
comments: []
}
[
Node {
type: 'VariableDeclaration',
start: 0,
end: 18,
loc: SourceLocation { start: [Position], end: [Position] },
declarations: [ [Node] ],
kind: 'let'
}
]
Process finished with exit code 0
可以看到抽象语法树是由一个个 Node 组成的,每个 Node 有很多属性,而 Node 还会有一些 body、left 之类的属性,这些属性的值的类型也可能是 Node。对抽象语法树的修改就是修改这些 Node 的值或者属性。
babel会调用插件提供的函数, 并将ast作为参数传给插件处理
访问者模式
访问者模式是一种将算法与对象结构分离的软件设计模式。
这个模式的基本想法如下:首先我们拥有一个由许多对象构成的对象结构,这些对象的类都拥有一个accept方法用来接受访问者对象;访问者是一个接口,它拥有一个visit方法,这个方法对访问到的对象结构中不同类型的元素作出不同的反应;在对象结构的一次访问过程中,我们遍历整个对象结构,对每一个元素都实施accept方法,在每一个元素的accept方法中回调访问者的visit方法,从而使访问者得以处理对象结构的每一个元素。我们可以针对对象结构设计不同的实在的访问者类来完成不同的操作。
———— 维基百科
具体来说,我们的 AST 的每一个 Node 有一个 accept 方法,当我们用一个 visitor 来遍历我们的 AST 时,每遍历到一个 Node 就会调用这个 Node 的 accept 方法来 接待
这个 visitor,而在 accept 方法内,我们会回调 visitor 的 visit 方法。我们来用访问者模式来实现一个 旅行者访问城市景点
的逻辑。
* 实际上 Node 是有两个方法,enter 和 exit,指遍历进入和离开 Node 的时候。通常访问者的 visit 方法会在 enter 内被调用。
// 旅游景点
class ScenicPoint {
constructor(name) {
this.name = name
}
// 景点的 accept 方法接收 visitor,函数内调用 visitor 的 visit 方法来 visit 景点的实例
accept(visitor) {
visitor.visit(this)
}
}
class Park extends ScenicPoint {
}
class Museum extends ScenicPoint {
}
// 我们的城市
class City {
constructor(name, scenicPointList) {
this.name = name
this.scenicPointList = scenicPointList
}
accept(visitor) {
for (let scenicPoint of this.scenicPointList) {
scenicPoint.accept(visitor)
}
}
}
// visitors: Alice 与 Bob
let Alice = {
name: 'Alice',
visit(scenicPoint) {
if (scenicPoint instanceof Park) {
console.log(`${scenicPoint.name} is a wonderful park~`)
} else {
console.log(`${this.name} visiting ${scenicPoint.name}`)
}
}
}
let Bob = {
name: 'Bob',
visit(scenicPoint) {
if (!(scenicPoint instanceof Museum)) {
console.log(`I want to go to some Museum.`)
} else {
console.log(`${scenicPoint.name} is a wonderful Museum~`)
}
}
}
let BeiJing = new City('BeiJing', [
new ScenicPoint('八达岭长城'),
new Park('玉渊潭公园'),
new Museum('国家博物馆'),
])
BeiJing.accept(Alice)
// Alice visiting 八达岭长城
// 玉渊潭公园 is a wonderful park~
// Alice visiting 国家博物馆
BeiJing.accept(Bob)
// I want to go to some Museum.
// I want to go to some Museum.
// 国家博物馆 is a wonderful Museum~
传入我们的代码, 并提供需要添加的属性配置
const input = `let b1 = <Button color="red" />;
let b2 = <Button size="big" />;
let d1 = <div size="big" />;
`
plugins: [
plugin,
[
ButtonParser, {
'Button': {
size: 'small',
className: "common-btn"
},
"div": {
className: "common-div"
}
}]],
可以看到最后已经成功为各个元素添加了属性值, 不过对于同名属性, 还是有优化空间的, 至于如何去除, 以后再说吧....
const plugin = require('@babel/plugin-transform-react-jsx')
const {transformAsync} = require('@babel/core');
const input = `let b1 = <Button color="red" />;
let b2 = <Button size="big" />;
let d1 = <div size="big" />;
`
const defaults = {
plugins: [
plugin,
[
ButtonParser, {
'Button': {
size: 'small',
className: "common-btn"
},
"div": {
className: "common-div"
}
}]],
sourceType: 'module',
};
async function main() {
const {code} = await transformAsync(input, {
...defaults,
sourceType: 'module',
});
console.log('code:\n', code)
}
main()
const types = require('@babel/types')
const parser = require('@babel/parser')
function ButtonParser() {
return {
// 我们的 visitor
visitor: {
// 针对函数调用的单独逻辑处理
CallExpression(path, state) {
// 我们只处理 React.createElement 函数调用
let {callee} = path.node
if (
!(
types.isMemberExpression(callee) &&
types.isIdentifier(callee.object) &&
callee.object.name === 'React' &&
types.isIdentifier(callee.property) &&
callee.property.name === 'createElement'
)
) {
return
}
// 从第一个参数获取组件名称(Button)
// 从第二个参数获取组件属性
let [element, propsExpression] = path.node.arguments
let elementType
if (types.isStringLiteral(element)) {
elementType = element.value
} else if (types.isIdentifier(element)) {
elementType = element.name
}
// 我们的插件支持自定义选项,针对不同的组件类型传入不同的额外自定义属性
const options = state.opts
console.log('plugin-args', options)
let extraProps = options[elementType]
// 如果没有针对次组件类型的额外参数,我们的插件什么都不做
if (!extraProps) {
return
}
// 否则,我们利用 parser.parseExpression 方法以及我们的自定义属性生成一个 ObjectExpression
let stringLiteral = JSON.stringify(extraProps)
console.log('stringLiteral', stringLiteral, extraProps)
let extraPropsExpression = parser.parseExpression(stringLiteral)
console.log('extraPropsExpression', extraPropsExpression)
// 如果组件原本 props 为空,我们直接将我们的自定义属性作为属性参数
if (types.isNullLiteral(propsExpression)) {
path.node.arguments[1] = extraPropsExpression
} else if (types.isObjectExpression(propsExpression)) {
// 否则,我们将我们的自定义属性与原属性进行合并(只处理对象类型的 props)
path.node.arguments[1] = types.objectExpression(
propsExpression.properties.concat(
extraPropsExpression.properties,
),
)
}
},
},
}
}
/*
// babel.config.js
const plugins = [
[
'babel-plugin-react-auto-props',
{
'Button': {
size: 'small',
},
},
],
]
module.exports = { presets, plugins }
*/