該系列文章計劃中一共有三篇,這是第二篇。在這三篇文章裏我將手把手教你們使用 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!(~ ̄▽ ̄)~附:個人簡歷。