該系列文章計劃中一共有三篇,在這三篇文章裏我將手把手教你們使用 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 的數據流。json
因爲 JSX 不能直接在瀏覽器運行,須要使用 Babel 編譯成普通的 JS 文件, 所以這讓咱們有機會在編譯時對代碼進行處理實現無需 Runtime 的雙向數據綁定。瀏覽器
如: 在 JSX 中,在 「Input」 標籤中使用 「model」 屬性來指定要綁定的數據:前端框架
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: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" 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!(~ ̄▽ ̄)~附:個人簡歷。