该系列文章计划中一共有三篇,在这三篇文章里我将手把手教你们使用 Babel 为 React 实现双向数据绑定。在这系列文章你将:前端
该系列文章实现的 babel-plugin-jsx-two-way-binding 在个人 GitHub 仓库,欢迎参考或提出建议。node
你也可使用 npm install --save-dev babel-plugin-jsx-two-way-binding
来安装并直接使用该 babel-plugin。git
另:本人 18 届前端萌新正在求职,若是有大佬以为我还不错,请私信我或给我发邮件: i@do.codes!(~ ̄▽ ̄)~附:个人简历。github
在 Angular、Vue 等现代前端框架中,双向数据绑定是一个颇有用的特性,为处理表单带来了很大的便利。express
React 官方一直提倡单向数据流的思想,虽然我我的十分喜欢 React 的设计哲学,但在实际需求中,有时会遇到 View 层与 Model 层存在大量的数据须要同步的状况,这时为每个表单都添加一个 Handler 反而会让事情变得更加繁琐。npm
不难发现,这种状况在 React 中老是有相同的的处理方法:经过 “value” 属性实现 Model => View 的数据流,经过绑定 “ onChange” Handler 实现 View => Model 的数据流。浏览器
因为 JSX 不能直接在浏览器运行,须要使用 Babel 编译成普通的 JS 文件, 所以这让咱们有机会在编译时对代码进行处理实现无需 Runtime 的双向数据绑定。前端框架
如: 在 JSX 中,在 “Input” 标签中使用 “model” 属性来指定要绑定的数据:babel
class App extends React.Component { constructor(props) { super(props); this.state = { name: 'Joe' } } render() { return ( <div> <h1>I'm {this.state.name}</h1> <input type="text" model={this.state.name}/> </div> )} }复制代码
绑定 “model” 属性的标签在编译时将会同时被绑定的 “value” 属性和 “onChange” Handler:markdown
class App extends React.Component { constructor(props) { super(props); this.state = { name: 'Joe' } } render() { return ( <div> <h1>I'm {this.state.name}</h1> <input type="text" value={this.state.name} onChange={e => this.setState({ name: e.target.value })} /> </div> )} }复制代码
下面须要了解一些知识:
Babel 编译 JS 文件的步骤分为解析(parse),转换(transform),生成(generate)三个步骤。
解析步骤接收代码并输出 AST(Abstract syntax tree: 抽象语法树, 参考: en.wikipedia.org/wiki/Abstra… 这个步骤分为两个阶段:词法分析(Lexical Analysis)和语法分析(Syntactic Analysis)。
转换步骤接收 AST 并对其进行遍历,在此过程当中对节点进行添加、更新及移除等操做。
代码生成步骤深度优先遍历最终的 AST 转换成字符串形式的代码,同时还会建立源码映射(source maps)。
要达到咱们的目标,咱们须要在转换步骤操做 AST 并对其进行更改。 AST 在 Babel 中以 JS 对象的形式存在,所以咱们须要遍历每个 AST 节点。
在 Babel 及其余不少编译器中,都使用访问者模式来遍历 AST 节点(参考: Visitor pattern - Wikipedia)。当咱们谈及遍历到一个 AST 节点时,实际上咱们是在访问它,这时 Babel 将会调用该类型节点的 Handler。如,当访问到一个函数声明时(FunctionDeclaration),将会调用 FunctionDeclaration() 方法并将当前访问的节点做为参数传入该函数。咱们须要作的工做就是编写对应访问者的 Handler 来处理添加了双向数据绑定的标签的 AST 并为其添加 “value” 属性 和 “onChange” handler。
一个重要的工具:
AST Explorer(AST explorer):能够把咱们的代码转换为 Babel AST 树,咱们须要参考它来对咱们的 AST 树进行修改。
一些参考资料:
BabelHandBook (GitHub - thejameskyle/babel-handbook: A guided handbook on how to use Babel and how to create plugins for Babel.):教你如何使用 Babel 以及如何编写 Babel 插件和预设。
BabelTypes 文档(babel/packages/babel-types at master · babel/babel · GitHub):咱们须要查阅该文档来构建新的的 AST 节点。
首先,使用 npm init
建立一个空的项目,而后在项目目录下建立 “index.js”:
module.exports = function ({ types: t }) { return { visitor: { JSXElement: function(node) { // TODO } } } };复制代码
在 “index.s” 中咱们导出一个方法做为该 babel-plugin 的主体,该方法接受一个 babel 对象做为参数,返回一个包含各个 Visitor 方法的对象。传入的 babel 对象包含一个 types 属性,它用来构造新的 AST 节点,如,可使用 t.jSXAttribute(name, value)
来构造一个新的 JSX 属性节点; 每一个 Visitor 方法接受一个 Path 做为参数。AST 一般会有许多节点,babel 使用一个可操做和访问的巨大可变对象表示节点之间的关联关系。Path 是表示两个节点之间链接的对象。
由于咱们要修改 JSX 标签的属性并对其添加 “value” 和 “onChange” 属性,所以咱们须要在 JSXElement Visitor Handler 中遍历 JSXAttribute。Visitor Handler 中传入的的 Path 参数中有个 traverse 方法能够用来遍历全部的节点。如今,咱们来添加一个遍历 JSX 属性的方法:
module.exports = function ({ types: t }) { function JSXAttributeVisitor(node) { // TODO } function JSXElementVisitor(path) { path.traverse({ JSXAttribute: JSXAttributeVisitor }); } return { visitor: { JSXElement: JSXElementVisitor } } }复制代码
而后咱们来具体实现 JSXAttributeVisitor 方法。首先,咱们须要拿到双向数据绑定的值,并保存到一个变量(咱们默认使用 “model” 属性来进行双向数据绑定),而后把 “model” 属性名改成 “value”:
function JSXAttributeVisitor(node) { if (node.node.name.name === 'model') { const model = node.node.value.expression; // 将 model 属性名改成 value node.node.name.name = 'value'; } }复制代码
这时咱们拿到的 model 属性是一个 expression 对象,咱们须要将其转化成相似 “this.state.name” 这样的字符串方便咱们在后面使用,在这里咱们实现一个方法将 expression 对象转换成字符串:
// 把 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; }复制代码
由于咱们须要在自动绑定的 handler 里面使用 “this.setState” 方法,所以咱们暂时只考虑对 State 对象的数据绑定进行处理。让咱们继续改进 JSXAttributeVisitor 方法:
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('.'); node.node.name.name = 'value'; } }复制代码
而后咱们开始构建 onChange Handler 的 AST 节点,由于咱们调用 “this.setState” 时须要以对象的形式传入参数,所以咱们建立两个方法,objPropStr2AST 方法以字符串传入 key 和 value,返回一个对象 AST 节点;objValueStr2AST 方法以字符串传入 value,返回对象的属性的值的 AST 节点:
// 把 key - value 字符串转换为 { key: value } 这样的对象 AST 节点 function objPropStr2AST(key, value, t) { return t.objectProperty( t.identifier(key), objValueStr2AST(value, t) ); }复制代码
// 把相似 “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 AST ,接着刚刚的 JSXAttributeVisitor 方法,在后面加上:
// 建立一个函数调用节点(建立 AST 节点须要参阅 BabelTypes 文档) // 须要传入 callee(调用的方法)和 arguments(调用时传入的参数)两个参数 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)] )] );复制代码
终于,让咱们加上 onChange Handler:
// 使用 insertAfter 方法在当前 JSXAttribute 节点后添加一个新的 JSX 属性节点 node.insertAfter(t.JSXAttribute( // 属性名为 “onChange” t.jSXIdentifier('onChange'), // 属性值为一个 JSX 表达式 t.JSXExpressionContainer( // 在表达式中使用箭头函数 t.arrowFunctionExpression( // 该函数接受参数 ‘e’ [t.identifier('e')], // 函数体为一个包含刚刚建立的 ‘setState‘ 调用的语句块 t.blockStatement([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'; 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)] )] ); node.insertAfter(t.JSXAttribute( // 属性名为 “onChange” t.jSXIdentifier('onChange'), // 属性值为一个 JSX 表达式 t.JSXExpressionContainer( // 在表达式中使用箭头函数 t.arrowFunctionExpression( // 该函数接受参数 ‘e’ [t.identifier('e')], // 函数体为一个包含刚刚建立的 ‘setState‘ 调用的语句块 t.blockStatement([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; } // 把相似 “this.state.name” 这样的字符串转换为 AST 节点 function objPropStr2AST(key, value, t) { return t.objectProperty( t.identifier(key), objValueStr2AST(value, t) ); } // 把 key - value 字符串转换为 { key: value } 这样的对象 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) ) }复制代码
如今咱们已经可以成功使用 ‘model’ 属性绑定数据并自动为其添加 ‘value’ 属性与 ‘onChange’ Handler 来实现双向数据绑定!
让咱们试试效果:编辑 ‘.babelrc’ 配置文件:
{ "plugins": [ "path/to/your/index.js(咱们建立的 index.js 文件路径)", ... ] }复制代码
而后编写一个 React 组件,你会发现,使用 ‘model’ 属性便可实现双向数据绑定,就像在 Angular 或 Vue 里那样,简单而天然!
目前咱们已经实现了基本的双向数据绑定,可是还存在一些缺陷:咱们手动添加的 onChange Handler 会被覆盖掉,而且只能对非嵌套的属性进行绑定!
接下来的两篇文章里咱们会对这些问题进行解决,欢迎关注个人掘金专栏或 GitHub!
PS:
若是你以为这篇文章或者 babel-plugin-jsx-two-way-binding 对你有帮助,请不要吝啬你的点赞或 GitHub Star!若是有错误或者不许确的地方,欢迎提出!
本人 18 届前端萌新正在求职,若是有大佬以为我还不错,请私信我或给我发邮件: i@do.codes!(~ ̄▽ ̄)~附:个人简历。