该系列文章计划中一共有三篇,这是第二篇。在这三篇文章里我将手把手教你们使用 Babel 为 React 实现双向数据绑定。在这系列文章你将:前端
该系列文章实现的 babel-plugin-jsx-two-way-binding 在个人 GitHub 仓库,欢迎参考或提出建议。node
你也可使用 npm install --save-dev babel-plugin-jsx-two-way-binding
来安装并直接使用该 babel-plugin。react
另:本人 18 届前端萌新正在求职,若是有大佬以为我还不错,请私信我或给我发邮件: i@do.codes!(~ ̄▽ ̄)~附:个人简历。git
在上一篇文章里咱们了解了一些基本的关于 JS 编译的知识,而且学会了使用 Babel 来在编译时 JSX AST 为 React 实现最基本的双向数据绑定。github
可是目前的双向数据绑定仍然存在一些问题,如:咱们手动添加的 onChange Handler 会被覆盖掉,而且只能对非嵌套的属性进行绑定!express
在这篇文章,让咱们继续完善咱们的 babel-plugin 来支持嵌套属性的双向数据绑定!npm
如今,当咱们绑定嵌套的属性时,如:数组
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> )} }复制代码
编译时会出现相似这样的错误:babel
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 编译出了这样的代码:markdown
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 }) }) }); }}复制代码
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) ]) ) ) ));复制代码
恭喜!到这里咱们已经实现了嵌套属性的双向数据绑定。完整的 ‘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) ) }复制代码
目前咱们已经实现了达成了本节目标,可是还存在一些缺陷:咱们手动添加的 onChange Handler 会被覆盖掉,而且不能自定义双向数据绑定的 attrName !
接下来的一篇文章里咱们会对这些问题进行解决,欢迎关注个人掘金专栏或 GitHub!
PS:
若是你以为这篇文章或者 babel-plugin-jsx-two-way-binding 对你有帮助,请不要吝啬你的点赞或 GitHub Star!若是有错误或者不许确的地方,欢迎提出!
本人 18 届前端萌新正在求职,若是有大佬以为我还不错,请私信我或给我发邮件: i@do.codes!(~ ̄▽ ̄)~附:个人简历。