0. Something To Say
该系列文章计划中一共有三篇,这是第二篇。在这三篇文章里我将手把手教大家使用 Babel 为 React 实现双向数据绑定。在这系列文章你将:
- 了解一些非常基本的编译原理中的概念
- 了解 JS 编译的过程与原理
- 学会如何编写 babel-plugin
- 学会如何修改 JS AST 来实现自定义语法
该系列文章实现的 babel-plugin-jsx-two-way-binding 在我的 GitHub 仓库,欢迎参考或提出建议。
你也可以使用 npm install --save-dev babel-plugin-jsx-two-way-binding
来安装并直接使用该 babel-plugin。
另:本人 18 届前端萌新正在求职,如果有大佬觉得我还不错,请私信我或给我发邮件: i@do.codes!(~ ̄▽ ̄)~附:我的简历。
1. Welcome Back!
在上一篇文章里我们了解了一些基本的关于 JS 编译的知识,并且学会了使用 Babel 来在编译时 JSX AST 为 React 实现最基本的双向数据绑定。
但是目前的双向数据绑定仍然存在一些问题,如:我们手动添加的 onChange Handler 会被覆盖掉,并且只能对非嵌套的属性进行绑定!
2. Any Problem?
在这篇文章,让我们继续完善我们的 babel-plugin 来支持嵌套属性的双向数据绑定!
现在,当我们绑定嵌套的属性时,如:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
profile: {
name: {
type: 'str',
value: 'Joe'
},
age: {
type: 'int',
value: 21
}
}
}
}
render() { return (
<div>
<h1>{this.state.profile.name.value}</h1>
<input type="text" model={this.state.profile.name.vaue}/>
</div>
)}
}
编译时会出现类似这样的错误:
ERROR in ./index.js
Module parse failed: Unexpected token (59:35)
You may need an appropriate loader to handle this file type.
| _react2.default.createElement('input', { type: 'text', value: this.state.profile.name.vaue, onChange: function onChange(e) {
| _this2.setState({
| profile.name.vaue: e.target.value
| });
| }
根据报错信息,是因为我们目前的 babel-plugin 编译出了这样的代码:
onChange = { e =>this.setState({
profile.name.vaue: e.target.value
})}
这显然不符合 JS 的语法。为了实现嵌套属性值的绑定,我们需要使用 ES6 中新增的 Object.assign 方法(参考:Object.assign() - JavaScript | MDN)。
我们的目标是编译出这样的代码:
onChange = { e => {
const _state = this.state;
this.setState({
profile: Object.assign({}, _state.profile, {
name: Object.assign({}, _state.profile.name, {
value: e.target.value
})
})
});
}}
3. Let’s Do It?
OK,问题出在 setStateCall,目前的 setStateCall AST 是这样的:
const setStateCall = t.callExpression(
// 调用的方法为 ‘this.setState’
t.memberExpression(
t.thisExpression(),
t.identifier('setState')
),
// 调用时传入的参数为一个对象
// key 为刚刚拿到的 modelStr,value 为 e.target.value
[t.objectExpression(
[objPropStr2AST(modelStr, 'e.target.value', t)]
)]
);
如果我们绑定了 model = {this.state.profile.name.value}
, 经过 objPropStr2AST 方法的转换,相当于调用了 this.setState({ this.state.profile.name.value: e.target.value })
。先让我们改进 objPropStr2AST 方法:
function objPropStr2AST(key, value, t) {
// 将 key 转换为数组形式
key = key.split('.');
return t.objectProperty(
t.identifier(key[0]),
key2ObjCall(key, value, t)
);
}
在这里我们调用了一个 key2ObjCall 方法, 这个方法将类似{ profile.name.value: value }
这样的 key-value 结构转换为类似下面这样的这样的 AST 节点:
{
profile: Object.assign({}, _state.profile, {
name: Object.assign({}, _state.profile.name, {
value: value
})
})
}
让我们开始构建 key2ObjCall 方法,该方法接受数组形式的 key 和字符串形式的 value 为参数。在这里我们需要使用递归地遍历 key 数组,因此我们还需第三个参数表示遍历到的 key 的元素的索引:
function key2ObjCall(key, value, t, index) {
// 初始化 index 为 0
!index && (index = 0);
// 若 key 只含有一个元素(key.length - 1 < index)
// 或遍历到 key 的最后一个元素(key.length - 1 === index)
if (key.length - 1 <= index)
// 直接返回 value 形式的 AST
return objValueStr2AST(value, t);
// 否则,返回 Object.assign({}, ...) 形式的 AST
// 如:key 为 ['profile', 'name', 'value'],
// value 为 e.target.value,index 为 0
// 将返回 Object.assign({},
// indexKey2Str(0 + 1, ['profile', 'name', 'value']),
// { ['profile', 'name', 'value'][0 + 1]: key2ObjCall(['profile', 'name', 'value'], t, 0 + 1) }
// ) 即 Object.assign({},
// this.state.profile,
// { name: key2ObjCall(['profile', 'name', 'value'], t, 1) }
// ) 的 AST
return t.callExpression(
t.memberExpression(
t.identifier('Object'),
t.identifier('assign')
),
[
t.objectExpression([]),
objValueStr2AST(indexKey2Str(index + 1, key), t),
t.objectExpression([
t.objectProperty(
t.identifier(key[index + 1]),
key2ObjCall(key, t, index + 1)
)
])
]
);
}
在上面我们调用了一个 indexKey2Str 方法,传入 key 和 index,以字符串返回对象属性名。如,传入 1 和 ['profile', 'name', 'value’],返回 ‘_state.profile.name’,让我们来实现这个方法:
function indexKey2Str(index, key) {
const str = ['_state'];
for (let i = 0; i < index; i++) str.push(key[i]);
return str.join('.')
}
现在,我们需要更改 JSXAttributeVisitor 方法,在 setStateCall 后面创建一个变量声明 AST 用于在 onChange Handler 里声明 const _state = this.state
:
const stateDeclaration = t.variableDeclaration(
'const', [
t.variableDeclarator(
t.identifier('_state'),
t.memberExpression(
t.thisExpression(),
t.identifier('state')
)
)
]
);
终于,最后一步!我们需要更改我们插入的 JSXAttribute AST 节点:
node.insertAfter(t.JSXAttribute(
// 属性名为 “onChange”
t.jSXIdentifier('onChange'),
// 属性值为一个 JSX 表达式
t.JSXExpressionContainer(
// 在表达式中使用箭头函数
t.arrowFunctionExpression(
// 该函数接受参数 ‘e’
[t.identifier('e')],
// 函数体为一个包含刚刚创建的 ‘setState‘ 调用的语句块
t.blockStatement([
// const _state = this.state 声明
stateDeclaration,
// setState 调用
t.expressionStatement(setStateCall)
])
)
)
));
4. Well Done!
恭喜!到这里我们已经实现了嵌套属性的双向数据绑定。完整的 ‘index.js’ 代码为:
module.exports = function ({ types: t}) {
function JSXAttributeVisitor(node) {
if (node.node.name.name === 'model') {
let modelStr = objExpression2Str(node.node.value.expression).split('.');
// 如果双向数据绑定的值不是 this.state 的属性,则不作处理
if (modelStr[0] !== 'this' || modelStr[1] !== 'state') return;
// 将 modelStr 从类似 ‘this.state.name.value’ 变为 ‘name.value’ 的形式
modelStr = modelStr.slice(2, modelStr.length).join('.');
// 将 model 属性名改为 value
node.node.name.name = 'value';
// setState 调用 AST
const setStateCall = t.callExpression(
// 调用的方法为 ‘this.setState’
t.memberExpression(
t.thisExpression(),
t.identifier('setState')
),
// 调用时传入的参数为一个对象
// key 为刚刚拿到的 modelStr,value 为 e.target.value
[t.objectExpression(
[objPropStr2AST(modelStr, 'e.target.value', t)]
)]
);
// const _state = this.state 声明
const stateDeclaration = t.variableDeclaration(
'const', [
t.variableDeclarator(
t.identifier('_state'),
t.memberExpression(
t.thisExpression(),
t.identifier('state')
)
)
]
);
node.insertAfter(t.JSXAttribute(
// 属性名为 “onChange”
t.jSXIdentifier('onChange'),
// 属性值为一个 JSX 表达式
t.JSXExpressionContainer(
// 在表达式中使用箭头函数
t.arrowFunctionExpression(
// 该函数接受参数 ‘e’
[t.identifier('e')],
// 函数体为一个包含刚刚创建的 ‘setState‘ 调用的语句块
t.blockStatement([
// const _state = this.state 声明
stateDeclaration,
// setState 调用
t.expressionStatement(setStateCall)
])
)
)
));
}
}
function JSXElementVisitor(path) {
path.traverse({
JSXAttribute: JSXAttributeVisitor
});
}
return {
visitor: {
JSXElement: JSXElementVisitor
}
}
};
// 把 expression AST 转换为类似 “this.state.name” 这样的字符串
function objExpression2Str(expression) {
let objStr;
switch (expression.object.type) {
case 'MemberExpression':
objStr = objExpression2Str(expression.object);
break;
case 'Identifier':
objStr = expression.object.name;
break;
case 'ThisExpression':
objStr = 'this';
break;
}
return objStr + '.' + expression.property.name;
}
// 把 key - value 字符串转换为 { key: value } 这样的对象 AST 节点
function objPropStr2AST(key, value, t) {
// 将 key 转换为数组形式
key = key.split('.');
return t.objectProperty(
t.identifier(key[0]),
key2ObjCall(key, value, t)
);
}
function key2ObjCall(key, value, t, index) {
// 初始化 index 为 0
!index && (index = 0);
// 若 key 只含有一个元素(key.length - 1 < index)
// 或遍历到 key 的最后一个元素(key.length - 1 === index)
if (key.length - 1 <= index)
// 直接返回 value 形式的 AST
return objValueStr2AST(value, t);
// 否则,返回 Object.assign({}, ...) 形式的 AST
// 如:key 为 ['profile', 'name', 'value'],
// value 为 e.target.value,index 为 0
// 将返回 Object.assign({},
// indexKey2Str(0 + 1, ['profile', 'name', 'value']),
// { ['profile', 'name', 'value'][0 + 1]: key2ObjCall(['profile', 'name', 'value'], t, 0 + 1) }
// ) 即 Object.assign({},
// this.state.profile,
// { name: key2ObjCall(['profile', 'name', 'value'], t, 1) }
// ) 的 AST
return t.callExpression(
t.memberExpression(
t.identifier('Object'),
t.identifier('assign')
),
[
t.objectExpression([]),
objValueStr2AST(indexKey2Str(index + 1, key), t),
t.objectExpression([
t.objectProperty(
t.identifier(key[index + 1]),
key2ObjCall(key, value, t, index + 1)
)
])
]
);
}
// 传入 key 和 index,以字符串返回对象属性名
// 如,传入 1 和 ['profile', 'name', 'value’],返回 ‘_state.profile.name’
function indexKey2Str(index, key) {
const str = ['_state'];
for (let i = 0; i < index; i++) str.push(key[i]);
return str.join('.')
}
// 把类似 “this.state.name” 这样的字符串转换为 AST 节点
function objValueStr2AST(objValueStr, t) {
const values = objValueStr.split('.');
if (values.length === 1)
return t.identifier(values[0]);
return t.memberExpression(
objValueStr2AST(values.slice(0, values.length - 1).join('.'), t),
objValueStr2AST(values[values.length - 1], t)
)
}
5. So What‘s Next?
目前我们已经实现了达成了本节目标,但是还存在一些缺陷:我们手动添加的 onChange Handler 会被覆盖掉,并且不能自定义双向数据绑定的 attrName !
接下来的一篇文章里我们会对这些问题进行解决,欢迎关注我的掘金专栏或 GitHub!
PS:
如果你觉得这篇文章或者 babel-plugin-jsx-two-way-binding 对你有帮助,请不要吝啬你的点赞或 GitHub Star!如果有错误或者不准确的地方,欢迎提出!
本人 18 届前端萌新正在求职,如果有大佬觉得我还不错,请私信我或给我发邮件: i@do.codes!(~ ̄▽ ̄)~附:我的简历。