背景
最近接手公司的一個移動端項目,是經過 Rax 做爲 dsl 開發的,在發佈的時候構建多分代碼,在 APP 端編譯爲可以運行在 weex 上的代碼,在 H5(跑在瀏覽器或者 webview 裏面,無論什麼技術咱們統稱 H5) 端使用降級的 weexcss
這一套開發體系,看起來很完美,一次開發,三端運行。可是真實在開發的時候,就不是這麼完美了。因爲畢竟是跑在 weex 上的,而不是瀏覽器。因此在開發方式上也很難直接從 web 端的開發方式平移過去,爲了實現跨端運行,因此在樣式上只實現了 Css 的子集, DOM API 也是如此,開發的時候,在瀏覽器裏面調試的時候,一切正常,可是等發佈到 APP 端用 weex 跑的時候又是各類問題,開發體驗很不流暢。html
固然,有人會說那是由於你對 weex 的 api 不瞭解,也對,一直以來對與這種本身搞一套非標準體系來實現各類魔法功能的東西都不怎麼感興趣 可是若是瞭解它的成本大於了它帶來的收益,那麼對咱們來講,就沒有必要作這件事情。前端
weex 相對於 H5 ,最大的優勢在於交互性能上要更好一點。node
而隨着手機性能的提高,以及 webview 的不斷優化,H5 的頁面也愈來愈流暢了,尤爲是純展現形頁面上。並且相較於 H5 ,weex 天生不具有 seo 能力,同時存在分享傳播困難的缺點,這樣看來,使用 weex 的理由就更少了。並且咱們在一個新業務上使用 H5 開發了一個頁面,藉助同構以及提早緩存的能力,已經把首屏打開速度作到了全球秒開,並且業務數據也達到預期,因此咱們打算把現有的存量業務都遷移到 H5 上。react
這就是我爲何要把基於 Rax 開發的模塊代碼轉換爲 React 代碼,也就有了本篇文章。
本文針對的 rax 版本是 0.6.8 ,1.x 的版本改動很大,不在本文討論範圍內。android
指望的目標
對於一個 rax 模塊,咱們指望經過編譯後:webpack
可以在 react 下運行
儘量提取樣式到 css 文件裏,不使用內聯
不一樣之處
Rax 在開發之處,就是爲了可以使用 React 的語法來開發 weex ,因此一開始在語法上和 React 幾乎一致。後續隨着 rax 的不斷迭代,漸漸和 react 有了一些不同的地方,可是差距不大。
咱們對比一下同一個模塊在 rax 和 react 的實現代碼:ios
rax moduleweb
import { Component, createElement, findDOMNode } from 'rax';
import Text from 'rax-text';
import View from 'rax-view';
import styles from './index.css';json
class Kisnows extends Component {
constructor(props) {
super(props); this.state = { count: 1 };
}
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
const { count } = this.state; const { name } = this.props; return ( <View style={styles.wrap}> <Text style={[styles.name, { color: 'red' }]}>{name}</Text> <View onClick={this.handleClick}> 怕什麼真理無窮,進一步有進一步的好。 </View> <View>點擊進步:{count}</View> </View> );
}
}
export default Kisnows;
react module
import { Component } from 'react';
import { findDOMNode } from 'react-dom';
import styles from './index.css';
class Kisnows extends Component {
constructor(props) {
super(props); this.state = { count: 1 };
}
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
const { count } = this.state; const { name } = this.props; return ( <div className="wrap"> <h1 className="name" style={{ color: red }}> {name} </h1> <div onClick={this.handleClick}> 怕什麼真理無窮,進一步有進一步的好。 </div> <div>點擊進步:{count}</div> </div> );
}
}
export default Kisnows;
能夠看到他們的區別以下:
引用的框架不一樣
這個固然是廢話,
rax 模塊從 rax 上引入 Component, createElement 等組件的, react 模塊從 react 上引入。
還有一點的不一樣的地方,就是 findDOMNode , rax 上是直接掛載在 rax 模塊上的,而 findDOMNode 從 react-dom 上獲取。
使用的基礎元件不一樣
rax 自己是爲了使用 react 語法去寫 weex 的而誕生的,weex 爲了兼容 ios 和 android ,因此在兩個系統中抽了一些組件出來,在上面作了一層適配。
而 rax 爲了跨 Web 和 Native 在 weex 之上又包了一層,因而就有了 Text, View, Image 等基礎元件,而沒法使用普通 Web 開發使用的 span, div, img 等 html 標籤。
樣式使用的不一樣
rax 下全部的樣式文件都是 css in js ,即便後面經過構建等支持了引用外部 css 文件的樣式,但依然仍是 css in js ,全部的樣式都是內聯的。
同時從上面的代碼能夠看到, rax 不知道從哪一個版本開始支持了 style 屬性傳入 Array 類型。這一塊和 react 下不一致,react 只支持 Object 類型。
而一旦傳入的是個 Array ,js 又是動態語言,因此沒法在編譯時判斷裏面元素的類型,就很差判斷裏面的元素哪些是要轉換爲 class ,哪些是直接內聯的樣式了。咱們轉換上不少的精力也是花在了這裏。
如何轉換
知道了兩者的不一樣,那麼如何轉換咱們也就有方向了。
對代碼進行轉換,如今最多見的方法就是基於 babel 的插件來進行,首先把代碼轉換成 ast 語法樹,而後在此基礎上對 ast 進行轉換,最後把 ast 轉換成咱們須要的代碼。
經過開發 babel 插件來轉換代碼,在如今的前端開發中很常見,我也不作過多介紹。但以前我也僅限於使用上, 沒有本身開發過,此次也是現學現賣。
介紹幾個學習中參考的文檔和工具:
babel-handbook
幾乎涵蓋開發 babel 插件須要的一切。
AST Explorer
一個在線進行語法解析的網站,能夠實時驗證你的想法。
因爲 babel-handbook 已經對如何開發 babel 插件講解的很詳細了,對一些基本概念本文就再也不贅述,默認往下看的讀者都已經有了這些知識基礎。
引用的框架不一樣
這個很好解決,咱們須要的就是把:
import { Component, createElement, PureComponent, findDOMNode } from 'rax';
這樣的代碼轉換成:
import { Component, createElement, PureComponent } from 'react';
import { findDOMNode } from 'react-dom';
看起來很簡單是吧,咱們打開 AST Explorer ,在這上面輸入
import { Component, createElement, PureComponent, findDOMNode } from 'rax';
會獲得以下結果:
import
圖中左側的 import 語句轉換成 ast 後就是圖中右側紅框中內容。
整個 import 語句是一個 ImportDeclaration , 引入的每一個方法 Component、 createElement 等對應一個 ImportSpecifier 節點。
咱們要作的就是把 ImportDeclaration.source.value 改成 react, 而後把 findDOMNode 從 rax 中抽出來,改成從 react-dom 裏面引入就行了。
具體到操做上,就是咱們在 visitor 裏面添加對 ImportDeclaration 類型節點的遍歷:
visitor: {
ImportDeclaration(path) {
// 對於不符合咱們要修改條件的節點,直接 return ,節省無用遞歸調用的時間 if ( path.node.source.value !== 'rax' || path.node.source.type !== 'StringLiteral' ) { return; }
}
}
而後區分哪些模塊是要從 react 裏面引用的,哪些是須要從 react-dom 裏面引用的,再對 ImportSpecifier 節點進行遍歷,找到裏面符合咱們符合條件的模塊,方便後面生成新的 import 語句。
visitor: {
ImportDeclaration(path) {
if ( path.node.source.value !== 'rax' || path.node.source.type !== 'StringLiteral' ) { return; } const REACT_METHODS = [ 'createElement', 'Component', 'PureComponent', 'PropTypes' ]; const REACT_DOM_METHODS = ['findDOMNode']; const reactMethods = new Set(); const reactDOMMethods = new Set(); path.traverse({ ImportSpecifier(importSpecifierPath) { importSpecifierPath.traverse({ Identifier(identifierPath) { const methodName = identifierPath.node.name; // console.log('importSpecifierPath:Identifier:methodName', methodName) if (REACT_DOM_METHODS.includes(methodName)) { reactDOMMethods.add(methodName); } else if (REACT_METHODS.includes(methodName)) { reactMethods.add(methodName); } else { reactMethods.add(methodName); console.warn( `當前方法 ${methodName} 沒有進行配置,直接從React上獲取,若有問題請檢查此方法。` ); } } }); } }); },
}
最後一步,用前面找到的 react 和 react-dom 的模塊,藉助 bable 提供 template 來從新生成 import 語句,並刪除原有對 rax 的引用。
visitor: {
ImportDeclaration(path) {
if ( path.node.source.value !== 'rax' || path.node.source.type !== 'StringLiteral' ) { return; } const REACT_METHODS = [ 'createElement', 'Component', 'PureComponent', 'PropTypes' ]; const REACT_DOM_METHODS = ['findDOMNode']; const reactMethods = new Set(); const reactDOMMethods = new Set(); path.traverse({ ImportSpecifier(importSpecifierPath) { importSpecifierPath.traverse({ Identifier(identifierPath) { const methodName = identifierPath.node.name; // console.log('importSpecifierPath:Identifier:methodName', methodName) if (REACT_DOM_METHODS.includes(methodName)) { reactDOMMethods.add(methodName); } else if (REACT_METHODS.includes(methodName)) { reactMethods.add(methodName); } else { reactMethods.add(methodName); console.warn( `當前方法 ${methodName} 沒有進行配置,直接從React上獲取,若有問題請檢查此方法。` ); } } }); } }); // 使用前面的 reactMethods 和 reactDOMMethods ,來生成新的 import 語句。 const importReactTemplate = template.ast(` import {${Array.from(reactMethods).join(',')} } from 'react'; `); const importReactDOMTemplate = template.ast(` import { ${Array.from(reactDOMMethods).join( ',' )} } from 'react-dom'; `); // 插入到當前 path 前面 path.insertBefore(importReactTemplate); path.insertBefore(importReactDOMTemplate); // 刪除當前 path ,也就是 rax 的 import 語句 path.remove();
},
}
這樣,咱們就完成了引入模塊的轉換,如圖:
import
使用的基礎元件不一樣
對於基礎元件這一塊,咱們能夠不作任何處理。由於 rax 下每一個元件自己就是一個普通的 rax 組件,咱們能夠經過 babel 直接把他們看成其餘 rax 組件轉換爲 react 的代碼來使用。
可是咱們轉換後的代碼只須要在 Web 上跑,而若是你看過基礎元件好比用 rax-text 舉例的代碼:rax-text 。
裏面有不少代碼是僅在 weex 下運行的,咱們根本不須要,因此能夠經過一些粗暴的方法來精簡這些元件。直接使用 Webpack 的 alias ,來把對 rax-text 等基礎元件的引用直接指定到咱們的本身的一個組件下,好比咱們本身就把如下元件加入到了 webpack 的 alias 裏:
resolve: {
modules: ['node_modules'],
extensions: ['.json', '.js', '.jsx'],
alias: {
rax: 'react', 'rax-image': require.resolve('./components/rax-image'), 'rax-view': require.resolve('./components/rax-view'), 'rax-scrollview': require.resolve('./components/scroll-view'), '@ali/ike-splayer': require.resolve('./components/ike-splayer'), '@ali/ike-image': require.resolve('./components/ike-image')
},
},
對於命中了 alias 規則的元件,直接替換爲咱們本身精簡的組件。刪除裏面的對 weex 的判斷,以及僅僅會在 weex 下運行的代碼。
這一塊很簡單,沒什麼好說的。
樣式使用的不一樣
這裏是處理起來比較麻煩的地方,首先看看咱們訴求:
樣式提取
外部引入的 css 文件,再也不經過 css in js 的方式內聯到每個標籤上,而是提取爲常規 css 文件,並配合 class 來實現常規的樣式佈局方式
style 支持 Array
支持 rax 的特殊語法,即 style 能夠傳入一個 Array 類型
樣式提取
咱們看一下 rax 下引用和使用外部 css 的方式:
import { Component, createElement, findDOMNode } from 'rax';
import Text from 'rax-text';
import View from 'rax-view';
// 引入樣式文件
import styles from './index.css';
class Kisnows extends Component {
constructor(props) {
super(props); this.state = { count: 1 };
}
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
const { count } = this.state; const { name } = this.props; return ( // 使用樣式文件 <View style={styles.wrap}> <Text style={[styles.name, { color: 'red' }]}>{name}</Text> <View onClick={this.handleClick}> 怕什麼真理無窮,進一步有進一步的好。 </View> <View>點擊進步:{count}</View> </View> );
}
}
export default Kisnows;
<View style={styles.wrap}> 這種使用方式和社區的 css modules 幾乎一致,那咱們就能夠用 css module 來解決這件事情。
思路是這樣的,對於賦值到 style 屬性上的外部 css 屬性(即從 css 文件裏面引入,而非直接 js 裏面定義的樣式),咱們給它生成一個惟一字符串,而後組合起來放到 className 屬性上。對於 js 內部定義的 css ,繼續做爲內聯樣式賦值到 style 屬性上。
在 webpack 的 css rule 裏面引入 css-loader,而後開啓 css module 功能:
{
test: /\.(css)$/, use: [ { loader: require.resolve('css-loader'), options: { importLoaders: 2, modules: { mode: 'local', localIdentName: '[path][name]__[local]--[hash:base64:5]', }, // sourceMap: !!DEV, }, } ],
}
這樣配置了的話,經過 import styles from './index.css' 引入的 styles 就會被 css-loader 處理,而且裏面對相似 styles.wrap 的引入,就會變成 _3zyde4l1yATCOkgn-DBWEL 這樣的計算出來的惟一 id 字符串,而後賦值到 class 屬性上就行了。
至於如何把這個 id 放到 className 上,咱們放到後面一塊兒說。
轉換後的效果以下:
import
目前還面臨兩個問題:
非標準 css 文件處理
rax 下使用 weex 爲了作移動端適配,css 寫起來和普通的 css 有一點區別,也就是 rax 的 css 屬性都是沒有單位的。而咱們提取出來的 css 確定是須要單位才行的;
樣式優先級問題
之前全部的樣式都是內聯的,每一個樣式的優先級也都是同樣的,不管是直接寫在 js 裏面的仍是從 css 文件裏面引入的,最終會在 js 內部根據處理內聯 styles 的 Object.assign 入參的順序決定最終優先級。可是咱們提取出來之後就不同了,外鏈的 css 經過 class 做用與元素上,它的優先級是低於內聯樣式的,這樣就會致使
非標準 css 文件處理
rax 下 css 文件樣例:
.wrap {
width: 750;
}
.name {
width: 750;
height: 124;
font-size: 24;
}
能夠看到寬高之類屬性都是沒有單位的,這裏須要處理成對應的單位。處理起來也很簡單,postcss 是一個用來對 css 作後處理的工具,咱們常用的 autoprefix 就是 postcss 一個有名的插件。這裏咱們也藉助它的插件能力來處理 css, 插件代碼以下:
const postcss = require('postcss');
const _ = require('lodash');
// 定義全部須要添加單位的屬性
const props = [
'width',
'height',
'padding',
'margin',
'margin-top',
'margin-bottom',
'top',
'bottom',
'right',
'left',
'border',
'box-shadow',
'border-radius',
'font-size'
];
/**
*/
module.exports = postcss.plugin('realCss', function(options) {
return function(css) {
options = options || {}; const reg = /(\d+)\b(?!%)/gm; css.walkDecls(decl => { // 1. 遍歷全部 css 屬性,找到咱們定義的須要添加單位的項 if ( _.find(props, props => { return decl.prop.includes(props); }) ) { // 2. 簡單粗暴,直接添加 px decl.value = decl.value.replace(reg, a => a + 'px'); } // 3. 給全部屬性添加 !important ,提升優先級 decl.value += '!important'; });
};
});
相應的咱們在 webpack 的 css rule 裏面加上 postcss 的配置:
{
loader: require.resolve('postcss-loader'),
options: {
plugins: [ postcssPresetEnv(), // 咱們本身的開發 postcss 插件, realCss realCss(), post2Rem({ remUnit: 100 }), ], sourceMap: !!DEV,
},
},
樣式優先級問題
一開始還沒注意到這個問題,後來再測試的時候才發現有的樣式老是不生效,排查後才發現原來是由於樣式優先級的問題。
舉例:
import { Component } from 'rax';
class View extends Component {
render() {
let props = this.props; let styleProps = { ...styles.initial, ...props.style }; return <div {...props} style={styleProps} />;
}
}
const styles = {
initial: {
border: '0 solid black', position: 'relative', boxSizing: 'border-box', display: 'flex', flexDirection: 'column', alignContent: 'flex-start', flexShrink: 0
}
};
export default View;
上面是 rax-view 的代碼,能夠看到它有一個初始化的樣式,直接內聯到了元素標籤上。若是都是用 rax 開發,全部樣式內聯,那麼外部經過 props 傳下來的樣式 props.style 的優先級是高於它本身的初始化樣式 styles.initial ,這樣是沒有問題的。
一旦咱們把外部的 css 提取出去,那麼這裏的 props.styles 也基本就是咱們要提取出來的東西,這裏的一旦提取爲外聯 css 文件,那麼它的優先級就會永遠低於 styles.initial , 這樣咱們的樣式就會出錯。
這裏我沒有想到什麼好的辦法來提高外聯 css 的優先級,因此直接粗暴的給每一個外聯 css 的屬性添加了 !important ,參考上面 realCss 的註釋 3 。方法搓了一點,可是很能解決問題。並且不是硬編碼到 css 裏,後續去掉也很簡單。
style 支持 Array
這裏咱們跟着上面提取外聯 css 的屬性並放到 class 屬性上一塊兒說。
咱們要處理的 style 標籤有以下四種:
1<View style={{color: red}}>
;
2<View style={styles.wrap}>
;
3<Text style={[styles.name, { color: 'red' }, {fontSize: 24}]}>{name}</Text>
;
4<View style={sty}>
;
對於這三種狀況咱們指望的結果應該是這樣的:
保持不變,就是一個普通的 js 內聯樣式
<View style={{color: red}}>
咱們認爲 styles 都是從外部 css 文件引入的,因此須要替換 styles.wrap 爲 class 字符串,並賦值到 class 屬性上
const cls = 'className';
return <View className={cls} />;
這種狀況是前面兩種的組合,對於內聯樣式,繼續內聯到 style 屬性上,對於外部引用的 styles ,替換爲 class 字符串放到 className 上
const cls = 'className';
const style = Object.assign({}, { color: 'red' }, { fontSize: 24 });
return <View className={cls} style={style} />;
須要向上查找當前變量的類型,若是是 Array,那就參照上一條處理,不然認爲是內聯樣式,按照 1 處理。
有了指望的結果,那解決問題的思路也就比較容易了。咱們要作的就是對現有 style 標籤的屬性作判斷:
若是是 object ,對應 ast 解析後的 ObjectExpression ,那麼直接看成內聯樣式處理
若是是一個從 styles (爲了簡化判斷,咱們默認 styles 是從外部 css 引入的)上讀值,對應 ast 解析後的 MemberExpression, 認爲是從外部 css 引入的樣式
若是是一個 Array ,對應 ast 解析後的 ArrayExpression ,把裏面的東西遍歷一邊,找到要轉換成 class 字符串的須要內聯的樣式,通過處理後,再放到當前元素的 className 屬性和 style 屬性上
還有一種是值是另一個定義的變量,也就是 ast 解析後的 Identifier ,這種須要判斷裏面的值是否是 Array ,是的話按照上一條處理,不然認爲是內聯樣式
對上面三種狀況的操做抽象一下:
找到有 style 元素的標籤,而後判斷 style 的值,提取處理裏面要內聯的 css 和要轉換爲 class 的樣式,
根據上一步的結果重建 style 屬性,並添加 className 屬性
打開 https://astexplorer.net/ 看一下,每一個元素對應的都是一個 JSXOpeningElement ,咱們要作的就是遍歷 JSXOpeningElement ,而後對每一個標籤|組件作處理。
import
具體實現邏輯:
第一步:找內聯 css 和須要轉換的 class
JSXOpeningElement: {
enter(path) {
const node = path.node; // 用來存放找到的內聯 css const styles = []; // 用來存放被轉換的 class const classNames = []; let newStyleAttr = null; let newClassNameAttr = null; let styleAttrPath = null; path.traverse({ JSXAttribute(path) { if (path.node.name.name !== 'style') return; styleAttrPath = path; path.traverse({ /** * 從離元素最近的一個方法往下找,判斷 style 的值是不是一個 Array, * 僅限查找直接的變量,而非從對象上讀取的。 * eg: style={[list, obj.arr]} ,則只查找 list 而無論 obj.arr */ Identifier(identifyPath) { const name = identifyPath.node.name; const parent = identifyPath.parent; if (t.isMemberExpression(parent)) return false; let isArray = false; // 從當前 path 向上查找最近的一個方法 const par = identifyPath.findParent(p => { if (t.isClassMethod(p) || t.isFunction(p)) { // 從 render 方法裏面往下找當前變量的定義, p.traverse({ VariableDeclarator(path) { if ( t.isArrayExpression(path.node.init) && path.node.id.name === name ) { isArray = true; } } }); } }); if (isArray) { // TODO: 若是是 Array ,則從新走一下後面的 ArrayExpression 的處理 // 建立當前做用域下的惟一變量 const arrayStyle = identifyPath.scope.generateUidIdentifier( 'arrayStyle' ); // 生成新的變量定義語句, // 若是是 Array ,那麼認爲裏面每一個元素都是內聯樣式,經過 Object.assign 把它們組合到一塊兒 const preformArrayStyle = template.ast(` const ${arrayStyle.name} = {} ${name}.forEach(sty => { if (typeof sty === 'object') { Object.assign(${arrayStyle.name}, sty) } }) `); const jsxParent = identifyPath.findParent(p => { if ( t.isReturnStatement(p) || t.isVariableDeclaration(p) ) { return true; } }); // 在最近的 return 語句上插入生成的語句 jsxParent.insertBefore(preformArrayStyle); // 把當前 style 的值賦值爲咱們新建的變量名 arrayStyle identifyPath.node.name = arrayStyle.name; } }, /** * 若是是變量上讀取的屬性,則認爲是從外部 css 引入的樣式。經過 css-loader 的處理後, * 引入的值已經變成了一個包含全部 class 的 object,咱們直接把它替換爲 style 就行了 * */ MemberExpression(path) { // function replaceStyle(path) { // if (!path.parentPath.parent.name) return; // path.parentPath.parent.name.name = 'className'; // } replaceStyle(path); }, /** * 若是是 Array ,那麼判斷裏面的值,規則按照上面兩種處理方式處理。 * */ ArrayExpression(arrayExpressionPath) { const eles = arrayExpressionPath.node.elements; // 遍歷 Array 裏面的元素 eles.forEach(e => { // MemberExpression 認爲是處理後的 class string if (t.isMemberExpression(e)) { classNames.push(e); } else if (t.isObjectExpression(e)) { // 若是是 Object 表達式,認爲是內聯樣式 styles.push(e); } else if (t.isIdentifier(e)) { // 若是是自定義變量,粗暴的認爲是內聯樣式 styles.push(e); } else if (t.isLogicalExpression(e)) { // 因爲很差判斷最終返回的值類型, 因此直接假定返回的 string ,看成 className處理 classNames.push(e); } }); } }); } });
}
這樣咱們就能夠拿到對應 styles 和 classNames ,接下來就是用它們來重建咱們的 style 和 className 屬性,看代碼:
if (!styles.length && !classNames.length) return;
/**
*/
// 嘗試獲取最近的一個 render 方法
const renderPath = getRenderPath(path);
// 獲取最近的一個 return 方法
let returnPath = getReturnPath(path);
// NOTE: 生成惟一 id ,並插入合併 styles 的代碼,
styleAttrPath.remove();
if (styles.length) {
if (!renderPath) return false;
// 爲 style 值建立當前做用域惟一變量名
const styleUid = path.scope.generateUidIdentifier('style_UID');
function buildStyleScript(styleUidName, styles) {
const test = t.callExpression( t.memberExpression(t.identifier('Object'), t.identifier('assign')), styles ); const newScript = t.variableDeclaration('const', [ t.variableDeclarator(styleUidName, test) ]); return newScript;
}
const newScript = buildStyleScript(styleUid, styles);
// 在 return 語句前添加當前 style_UID 的變量定義
returnPath.insertBefore(newScript);
newStyleAttr = t.jsxAttribute(
t.jsxIdentifier('style'), getAttributeValue({ value: styleUid.name, literal: true })
);
path.node.attributes.push(newStyleAttr);
}
if (classNames.length) {
// 構建並插入 className 字段
if (!renderPath) return;
// 爲 className 建立當前做用域惟一變量名
const classNameUid = path.scope.generateUidIdentifier('className_UID');
function buildClassNameScript(classNameUid, nodes) {
// DONE: 構建一個 List ,用來建立 className 字符串 const array = t.arrayExpression(nodes); const call = t.callExpression( t.memberExpression(array, t.identifier('join')), [t.stringLiteral(' ')] ); const newScript = t.variableDeclaration('const', [ t.variableDeclarator(classNameUid, call) ]); return newScript;
}
const newScript = buildClassNameScript(classNameUid, classNames);
// 在 return 前插入當前 className_UID 的變量定義
returnPath && returnPath.insertBefore(newScript);
// 構建 className 屬性節點
newClassNameAttr = t.jsxAttribute(
t.jsxIdentifier('className'), getAttributeValue({ value: classNameUid.name, literal: true })
);
// 給當前 jsx 標籤添加 className 屬性節點
path.node.attributes.push(newClassNameAttr);
}
這樣處理下來,整個 rax 組件就能夠編譯到 react 了,涉及到的工具備 webpack ,babel,完整的示例代碼請參考:
總結上面在對 ast 語法樹的轉換上,有不少假定,好比咱們認定 styles.xxx 中的 styles 就是從外部 css 引入的變量,在此基礎上作了後面的提取爲 class 的事情。還有一些比較粗暴的作法,畢竟若是要考慮全部狀況的話,成本就有點高了,不過目前已經能知足咱們的需求了。