抹桥的博客
Language
Home
Archive
About
GitHub
Language
主题色
250
6689 文字
33 分
Rax から React へのコード移行

背景#

最近、会社のモバイルプロジェクトを引き継ぎました。これはRaxをDSLとして開発されたものです。リリース時には複数のコードがビルドされ、APP側ではweex上で動作するコードにコンパイルされ、H5(ブラウザまたはwebviewで動作するもので、技術に関わらずH5と総称します)側ではweexのダウングレード版が使用されます。

この開発システムは、一度開発すれば三つのプラットフォームで動作するという点で、非常に完璧に見えます。しかし、実際に開発してみると、それほど完璧ではありませんでした。結局のところ、ブラウザではなくweex上で動作するためです。そのため、開発手法もWeb側の開発手法をそのまま移行するのは困難でした。クロスプラットフォームでの動作を実現するため、スタイルはCSSのサブセットのみが実装されており、DOM APIも同様です。開発中、ブラウザでデバッグしているときはすべて正常でしたが、APP側にリリースしてweexで実行すると、また様々な問題が発生し、開発体験は非常にスムーズではありませんでした。

もちろん、weexのAPIを理解していないからだと言う人もいるでしょうし、それはその通りです。これまで、このような独自の非標準システムを構築して様々な魔法のような機能を実現することには、あまり興味がありませんでした しかし、その学習コストがもたらされる利益を上回る場合、私たちにとってその作業を行う必要はありません。

weexはH5と比較して、インタラクション性能が優れている点が最大の利点です。

しかし、スマートフォンの性能向上とwebviewの継続的な最適化により、H5のページもますますスムーズになり、特に純粋な表示形式のページでは顕著です。さらに、H5と比較して、weexはSEO能力を本質的に持たず、共有や拡散が困難という欠点があります。こう考えると、weexを使用する理由はさらに少なくなります。さらに、私たちは新しいビジネスでH5を使用してページを開発し、アイソモーフィック(同形)と事前キャッシュの能力を活用することで、ファーストビューの表示速度をグローバルで瞬時に開くレベルにまで達成しました。そしてビジネスデータも期待通りに達成されたため、既存のストックビジネスをすべてH5に移行する予定です。

これが、私がRaxベースで開発されたモジュールコードをReactコードに変換する理由であり、本記事のきっかけとなりました。 本記事の対象となるRaxのバージョンは0.6.8であり、1.xのバージョンは大幅な変更があるため、本記事の議論の範囲外とします。

期待の目標#

Raxモジュールについて、コンパイル後の期待する目標は以下の通りです。

  1. Reactで動作すること
  2. 可能な限りスタイルをCSSファイルに抽出し、インラインスタイルを使用しないこと

異なる点#

Raxは開発当初、Reactの構文を使ってweexを開発できるようにするために作られたため、当初は構文がReactとほぼ同じでした。その後、Raxの継続的なイテレーションに伴い、徐々にReactとは異なる点が出てきましたが、大きな違いはありません。 同じモジュールのRaxとReactでの実装コードを比較してみましょう。

Raxモジュール

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;

Reactモジュール

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;

両者の違いは以下の通りです。

  1. 参照するフレームワークが異なる

    これはもちろん自明なことですが、 Raxモジュールは rax から ComponentcreateElement などのコンポーネントをインポートし、Reactモジュールは react からインポートします。

    もう一点異なるのは findDOMNode です。rax では rax モジュールに直接マウントされていますが、findDOMNodereact-dom から取得します。

  2. 使用する基本要素が異なる

    Rax自体は、Reactの構文を使ってweexを記述するために誕生しました。weexはiOSとAndroidの互換性を保つため、両システムからいくつかのコンポーネントを抽出し、その上に適応レイヤーを構築しています。

    そしてRaxは、WebとNativeを跨ぐためにweexの上にさらにレイヤーを重ねたため、TextViewImageなどの基本要素が存在し、通常のWeb開発で使用されるspandivimgなどのHTMLタグは使用できません。

  3. スタイルの使用方法が異なる

    Raxでは、すべてのスタイルファイルがCSS-in-JSです。後でビルドなどによって外部CSSファイルの参照がサポートされたとしても、依然としてCSS-in-JSであり、すべてのスタイルはインラインです。

    また、上記のコードからわかるように、Raxはどのバージョンからか不明ですが、style属性にArray型を渡すことをサポートしています。この点はReactとは異なり、ReactはObject型のみをサポートしています。

    Arrayが渡された場合、JavaScriptは動的言語であるため、コンパイル時に要素の型を判断できず、どの要素をクラスに変換し、どの要素を直接インラインスタイルにするかを判断するのが困難になります。変換作業の多くの労力もここに費やされました。

変換方法#

両者の違いが分かれば、変換方法の方向性も見えてきます。

コードを変換する際、現在最も一般的な方法はBabelのプラグインを利用することです。まずコードをAST(抽象構文木)に変換し、そのASTを変換し、最後にASTを必要なコードに変換します。

Babelプラグインを開発してコードを変換することは、現在のフロントエンド開発では非常に一般的であり、ここでは詳しい説明はしません。私も以前は使用するだけで、自分で開発したことはありませんでしたが、今回は付け焼き刃で挑戦しました。

学習中に参考にしたドキュメントとツールをいくつか紹介します。

  • babel-handbook Babelプラグイン開発に必要なほぼすべてを網羅しています。
  • AST Explorer オンラインで構文解析を行うサイトで、リアルタイムでアイデアを検証できます。

babel-handbookではBabelプラグインの開発方法が非常に詳しく解説されているため、基本的な概念については本記事では繰り返しません。読み進める読者は、これらの知識の基礎があることを前提とします。

参照するフレームワークが異なる#

これは簡単に解決できます。必要なのは、以下のコードを:

import { Component, createElement, findDOMNode, PureComponent } from "rax";

のようなコードに変換することです。

import { Component, createElement, PureComponent } from "react";
import { findDOMNode } from "react-dom";

簡単そうに見えますよね。では、AST Explorer を開いて、そこに以下を入力してみましょう。

import { Component, createElement, findDOMNode, PureComponent } from "rax";

すると、以下の結果が得られます。

import

図の左側のimport文をASTに変換すると、図の右側の赤枠内の内容になります。 import文全体はImportDeclarationであり、ComponentcreateElementなどの各インポートされたメソッドはImportSpecifierノードに対応します。 私たちがすべきことは、ImportDeclaration.source.valuereactに変更し、findDOMNoderaxから抽出し、react-domからインポートするように変更することだけです。

具体的な操作としては、visitorImportDeclaration型ノードのトラバースを追加します。

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上获取,如有问题请检查此方法。`
              );
            }
          }
        });
      }
    });
    },
}

最後のステップとして、先ほど見つけたreactreact-domのモジュールを使用し、Babelが提供する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下でのみ実行されるコードは削除します。

この部分は非常に簡単で、特に説明することはありません。

スタイルの使用方法が異なる#

ここは処理が比較的面倒な部分です。まず、私たちの要件を見てみましょう。

  1. スタイルの抽出

    外部からインポートされたCSSファイルは、CSS-in-JS方式で各タグにインライン化するのではなく、通常のCSSファイルとして抽出し、クラスと組み合わせて通常のスタイルレイアウトを実現します。

  2. styleのArrayサポート

    Raxの特殊な構文、つまりstyleArray型を渡せることをサポートします。

スタイルの抽出#

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 Modulesを使ってこの問題を解決できます。

考え方はこうです。style属性に割り当てられる外部CSSプロパティ(つまり、CSSファイルからインポートされ、JS内で直接定義されていないスタイル)については、一意の文字列を生成し、それを組み合わせてclassName属性に配置します。JS内部で定義されたCSSについては、引き続きインラインスタイルとしてstyle属性に割り当てます。

WebpackのCSSルールにcss-loaderを導入し、CSSモジュール機能を有効にします。

{
    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' でインポートされたstylescss-loaderによって処理され、styles.wrapのような参照は、計算された一意のID文字列(例: _3zyde4l1yATCOkgn-DBWEL)に変換され、class属性に割り当てられるようになります。

このIDをclassNameにどのように配置するかについては、後でまとめて説明します。 変換後の効果は以下の通りです。

import

現在、まだ2つの問題に直面しています。

  1. 非標準CSSファイルの処理 Raxではweexを使用してモバイル対応を行うため、CSSの記述が通常のCSSとは少し異なります。つまり、RaxのCSSプロパティには単位がありません。しかし、私たちが抽出するCSSには単位が必要になります。
  2. スタイルの優先順位の問題 以前はすべてのスタイルがインラインであり、各スタイルの優先順位も同じでした。JS内に直接記述されたものでも、CSSファイルからインポートされたものでも、最終的にはJS内部でインラインスタイルを処理するObject.assignの引数の順序に基づいて最終的な優先順位が決定されていました。しかし、抽出後は異なります。外部CSSはクラスを通じて要素に適用されるため、その優先順位はインラインスタイルよりも低くなり、これにより問題が発生します。
非標準CSSファイルの処理#

RaxでのCSSファイルの例:

.wrap {
  width: 750;
}

.name {
  width: 750;
  height: 124;
  font-size: 24;
}

幅や高さなどのプロパティに単位がないことがわかります。ここでは対応する単位に変換する必要があります。処理は非常に簡単で、PostCSSはCSSの後処理を行うツールであり、私たちがよく使用するAutoprefixerも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",
];
/**
 * main function
 */
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ルールに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タグは以下の4種類です。

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}>`;

これら3つのケースに対する期待される結果は以下の通りです。

  1. 変更なし。通常のJSインラインスタイルとして扱います。

<View style={{color: red}}>

2.  `styles`はすべて外部CSSファイルからインポートされたものとみなし、`styles.wrap`をクラス文字列に置き換え、`class`属性に割り当てます。
 ```js
const cls = "className";
return <View className={cls} />;
  1. このケースは上記2つの組み合わせです。インラインスタイルは引き続きstyle属性にインライン化し、外部参照のstylesはクラス文字列に置き換えてclassNameに配置します。

const cls = “className”; const style = Object.assign({}, { color: “red” }, { fontSize: 24 }); return ;

4.  現在の変数の型を上位に遡って検索する必要があります。`Array`であれば、前の項目に従って処理し、そうでなければインラインスタイルとみなし、1の処理に従います。

期待される結果が分かれば、問題解決の考え方も比較的容易になります。私たちがすべきことは、既存の`style`タグの属性を判断することです。

1.  `object`の場合、AST解析後の`ObjectExpression`に対応するため、直接インラインスタイルとして処理します。
2.  `styles`(判断を簡素化するため、`styles`は外部CSSからインポートされたものと仮定します)から値を読み取る場合、AST解析後の`MemberExpression`に対応するため、外部CSSからインポートされたスタイルとみなします。
3.  `Array`の場合、AST解析後の`ArrayExpression`に対応するため、その中の要素をすべてトラバースし、クラス文字列に変換する必要があるインラインスタイルを見つけ、処理後に現在の要素の`className`属性と`style`属性に配置します。
4.  もう一つは、値が別の定義済み変数である場合、つまりAST解析後の`Identifier`です。この場合は、その中の値が`Array`であるかどうかを判断し、`Array`であれば前の項目に従って処理し、そうでなければインラインスタイルとみなします。

上記の3つのケースの操作を抽象化すると、以下のようになります。

1.  `style`要素を持つタグを見つけ、`style`の値を判断し、インライン化すべきCSSとクラスに変換すべきスタイルを抽出します。
2.  前のステップの結果に基づいて`style`属性を再構築し、`className`属性を追加します。

[https://astexplorer.net/](https://astexplorer.net/) を開いて見てみましょう。各要素は`JSXOpeningElement`に対応しており、私たちがすべきことは`JSXOpeningElement`をトラバースし、各タグ/コンポーネントを処理することです。

<!-- @import "./transform-rax-to-react/jsx-element.png" -->

![import](/imgs/transform-rax-to-react/jsx-element.png)

具体的な実装ロジック:

ステップ1:インラインCSSと変換が必要なクラスを探す

```js
   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);
                 }
               });
             }
           });
         }
       });
   }

これで対応するstylesclassNamesを取得できます。次に、これらを使ってstyleclassName属性を再構築します。コードを見てみましょう。

if (!styles.length && !classNames.length) return;
/**
 * NOTE: 重建样式属性
 * 1. 删除 style 属性节点
 * 2. 用 styles 创建新的 style 节点
 * 3. 用 classNames 创建 className 节点
 */
// 尝试获取最近的一个 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です。

最終的な変換効果は非常に完璧です。結局のところ、スタイル関連以外は何も処理していませんから。 しかし、今回の実践を通じてBabelプラグインの開発方法を学び、今後同様の問題に遭遇した際に、また一つ処理方法が増えました。 —>

まとめ#

やはり標準に準拠した開発を行い、コミュニティを後ろ盾にすることが、より将来性があると言えるでしょう。

この記事は 2019年8月1日 に公開され、2019年8月1日 に最終更新されました。2257 日が経過しており、内容が古くなっている可能性があります。

Rax から React へのコード移行
https://blog.kisnows.com/ja-JP/2019/08/01/transform-rax-to-react/
作者
Kisnows
公開日
2019-08-01
ライセンス
CC BY-NC-ND 4.0