React を使って小さなプログラムを書くための Remax フレームワークのコンパイル プロセス分析 (推奨)

React を使って小さなプログラムを書くための Remax フレームワークのコンパイル プロセス分析 (推奨)

Remax は、実行時に構文制限のないソリューションを採用した React を使用して小規模なプログラムを開発するために Ant によって開発されたオープンソース フレームワークです。全体の研究は、主にランタイム原理、テンプレートレンダリング原理、コンパイルプロセスの3つの部分に分かれています。既存の記事のほとんどを読んだ後、それらは主にReamxのランタイムとテンプレートレンダリング原理に焦点を当てていますが、Reactコード全体をミニプログラムにコンパイルするプロセスの紹介はまだ見られませんでした。この記事は、このギャップを埋めるためのものです。
テンプレートレンダリングの原理については、こちらの記事をご覧ください: https://www.jb51.net/article/132635.htm
remax のランタイム原理については、こちらの記事をご覧ください: https://www.jb51.net/article/210293.htm
React カスタム レンダラーの詳細については、次の記事を参照してください: https://www.jb51.net/article/198425.htm

Remaxの基本構造:

1. remax-runtime ランタイムは、カスタム レンダラー、ホスト コンポーネントのパッケージ化、React コンポーネントからミニプログラムのアプリ、ページ、コンポーネントまでの構成ジェネレーターを提供します。

// カスタム レンダラー export { default as render } from './render';
// app.js からミニプログラムへの構成処理 App コンストラクター export { default as createAppConfig } from './createAppConfig';
// React からミニプログラムへの一連の適応プロセス ページ ページ ビルダー export { default as createPageConfig } from './createPageConfig';
// React コンポーネントからミニプログラム カスタム コンポーネントの Component コンストラクターへの一連の適応プロセス export { default as createComponentConfig } from './createComponentConfig';
// 
'./createNativeComponent' から、{default を createNativeComponent として } エクスポートします。
// アプレットによって提供される View、Button、Canvas などのホスト コンポーネントを生成します。export { default as createHostComponent } from './createHostComponent';
'./ReactPortal' から createPortal をエクスポートします。
'@remax/framework-shared' から { RuntimeOptions, PluginDriver } をエクスポートします。
'./hooks' から * をエクスポートします。

'./render' から ReactReconcilerInst をインポートします。
エクスポート const stable_batchedUpdates = ReactReconcilerInst.batchedUpdates;

エクスポートデフォルト{
  不安定なバッチ更新、
};

2. remax-wechatアプレット関連アダプタ
テンプレート関連、テンプレートに関連する処理原理と原則については、こちらをご覧ください https://www.jb51.net/article/145552.htm
テンプレート // レンダリングに関連するテンプレート
src/apiはWeChatミニプログラムに関連するさまざまなグローバルAPIを適応させ、その一部は約束されている

'@remax/framework-shared' から promisify をインポートします。

const wx を宣言します: WechatMiniprogram.Wx;

エクスポート const canIUse = wx.canIUse;
エクスポート const base64ToArrayBuffer = wx.base64ToArrayBuffer;
エクスポート const arrayBufferToBase64 = wx.arrayBufferToBase64;
エクスポート const getSystemInfoSync = wx.getSystemInfoSync;
エクスポート const getSystemInfo = promisify(wx.getSystemInfo);

src/types/config.ts は、ミニプログラムのページとアプリに関連する構成コンテンツを調整するために使用されます。

/** ページ設定ファイル */
// 参照: https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/page.html
エクスポートインターフェース PageConfig {
  /**
   * デフォルト値: #000000
   * ナビゲーションバーの背景色(#000000 など)
   */
  ナビゲーションバーの背景色?: 文字列;
  /**
   * デフォルト値: 白
   * ナビゲーションバーのタイトルの色は黒/白のみサポートされます
   */
  ナビゲーションバーのテキストスタイル?: 'black' | 'white';
  
  /** グローバル設定ファイル */
// 参照: https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html
エクスポートインターフェースAppConfig {
  /**
   * ページパスリスト */
  ページ: 文字列[];
  /**
   * グローバルデフォルトウィンドウの動作 */
  ウィンドウ?: {
    /**
     * デフォルト値: #000000
     * ナビゲーションバーの背景色(#000000 など)
     */
    ナビゲーションバーの背景色?: 文字列;
    /**
     * デフォルト値: 白
     * ナビゲーションバーのタイトルの色は黒/白のみサポートされます
     */
    ナビゲーションバーのテキストスタイル?: '白' | '黒';

src/types/component.ts WeChat組み込みコンポーネントに関連するパブリックプロパティ、イベント、その他のプロパティの適応

'react' から * を React としてインポートします。

/** WeChat 組み込みコンポーネントのパブリック プロパティ */
// 参照: https://developers.weixin.qq.com/miniprogram/dev/framework/view/component.html
エクスポートインターフェースBaseProps {
  /** カスタム属性: コンポーネントでイベントがトリガーされると、イベント ハンドラー関数に送信されます */
  読み取り専用データセット?: DOMStringMap;
  /** コンポーネントの一意の識別子: ページ全体を一意に保ちます*/
  id?: 文字列;
  /** コンポーネント スタイル クラス: 対応する WXSS で定義されたスタイル クラス*/
  クラス名?: 文字列;
  /** コンポーネントのインライン スタイル: 動的に設定できるインライン スタイル*/
  スタイル?: React.CSSProperties;
  /**コンポーネントが表示されるかどうか: デフォルトではすべてのコンポーネントが表示されます*/
  非表示?: ブール値;
  /** アニメーション オブジェクト: `wx.createAnimation` によって作成されます */
  アニメーション?: Array<Record<string, any>>;

  // 参照: https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxml/event.html
  /** クリックするとトリガーされます*/
  onTap?: (イベント: TouchEvent) => void;
  /** クリックするとトリガーされます*/
  onClick?: (イベント: TouchEvent) => void;
  /** 指タッチアクションが開始します*/
  onTouchStart?: (イベント: TouchEvent) => void;

src/hostComponents は WeChat Mini Program ホストコンポーネントをパッケージ化して適応させるためのものであり、node.ts は Mini Program 関連のプロパティを React 仕様に適応させるためのものである。

エクスポートconstエイリアス = {
  id: 'id',
  クラス名: 'クラス',
  スタイル: 'スタイル',
  アニメーション: 'アニメーション',
  ソース: 'ソース',
  ループ: 'ループ',
  コントロール: 'コントロール',
  ポスター: 'ポスター',
  名前: '名前',
  著者: '著者',
  onError: 'binderror'、
  onPlay: 'bindplay'、
  onPause: 'bindpause'、
  onTimeUpdate: 'bindtimeupdate'、
  onEnded: 'bindended'、
};

エクスポート const props = Object.values(alias);

createHostComponentを使用してさまざまなコンポーネントも生成されます

'react' から * を React としてインポートします。
'@remax/runtime' から createHostComponent をインポートします。

// WeChat はメンテナンスされなくなりました export const Audio: React.ComponentType = createHostComponent('audio');

createHostComponentはReactの要素を生成する

'react' から * を React としてインポートします。
'@remax/framework-shared' から { RuntimeOptions } をインポートします。

デフォルトの関数 createHostComponent<P = any>(name: string, component?: React.ComponentType<P>) をエクスポートします。
  if (コンポーネント) {
    コンポーネントを返します。
  }

  const コンポーネント = React.forwardRef((props, ref: React.Ref<any>) => {
    const { children = [] } = プロパティ;
    element = React.createElement(name, { ...props, ref }, children); とします。
    element = RuntimeOptions.get('pluginDriver').onCreateHostComponentElement(element) を React.DOMElement<any, any> として作成します。
    要素を返します。
  });
  RuntimeOptions.get('pluginDriver').onCreateHostComponent(Component) を返します。
}

3. remax-macro 公式の説明によると、remax-macro は babel-plugin-macros に基づくマクロです。いわゆるマクロはコンパイル中に文字列を静的に置き換えるものであり、Javascript にはコンパイル プロセスがありません。babel がマクロを実装する方法は、コードを ast ツリーにコンパイルし、その後 ast 構文ツリーを操作して元のコードを置き換えることです。詳しい記事については、こちらをご覧ください https://zhuanlan.zhihu.com/p/64346538;
Remax は、マクロを使用して、useAppEvent や usePageEvent などの一部のマクロを remax/runtime からインポートして置き換えます。

'babel-plugin-macros' から {createMacro} をインポートします。


'./createHostComponent' から createHostComponentMacro をインポートします。
'./requirePluginComponent' から requirePluginComponentMacro をインポートします。
'./requirePlugin' から requirePluginMacro をインポートします。
'./usePageEvent' から usePageEventMacro をインポートします。
'./useAppEvent' から useAppEventMacro をインポートします。

関数 remax({ 参照, 状態 }: { 参照: { [name: string]: NodePath[] }; 状態: any }) {
  references.createHostComponent?.forEach(path => createHostComponentMacro(path, state));

  references.requirePluginComponent?.forEach(path => requirePluginComponentMacro(path, state));

  references.requirePlugin?.forEach(path => requirePluginMacro(path));

  const インポーター = slash(state.file.opts.filename);

  Store.appEvents.delete(インポーター);
  Store.pageEvents.delete(インポーター);

  references.useAppEvent?.forEach(path => useAppEventMacro(path, state));

  references.usePageEvent?.forEach(path => usePageEventMacro(path, state));
}

エクスポート宣言関数createHostComponent<P = any>(
  名前: 文字列、
  プロパティ: 配列<文字列 | [文字列, 文字列]>
: React.ComponentType<P>;

エクスポート宣言関数 requirePluginComponent<P = any>(pluginName: string): React.ComponentType<P>;

エクスポート宣言関数 requirePlugin<P = any>(pluginName: string): P;

エクスポート宣言関数 usePageEvent(eventName: PageEventName, callback: (...params: any[]) => any): void;

エクスポート宣言関数 useAppEvent(eventName: AppEventName, callback: (...params: any[]) => any): void;

デフォルトのcreateMacro(remax)をエクスポートします。
'@babel/types' から * を t としてインポートします。
'@remax/shared' から { スラッシュ } をインポートします。
'@babel/traverse' から NodePath をインポートします。
'@remax/build-store' から Store をインポートします。
'./utils/insertImportDeclaration' から insertImportDeclaration をインポートします。

const PACKAGE_NAME = '@remax/runtime';
const FUNCTION_NAME = 'useAppEvent';

関数 getArguments(callExpression: NodePath<t.CallExpression>, インポーター: 文字列) {
  const args = callExpression.node.arguments;
  const eventName = args[0] を t.StringLiteral として;
  constコールバック = args[1];

  Store.appEvents.set(インポーター、Store.appEvents.get(インポーター)?.add(イベント名.値) ?? 新しい Set([イベント名.値]));

  [イベント名、コールバック]を返します。
}

デフォルト関数 useAppEvent(path: NodePath, state: any) をエクスポートします。
  const プログラム = state.file.path;
  const インポーター = slash(state.file.opts.filename);
  const functionName = insertImportDeclaration(プログラム、FUNCTION_NAME、PACKAGE_NAME);
  const callExpression = path.findParent(p => t.isCallExpression(p)) を NodePath<t.CallExpression> として指定します。
  const [eventName, callback] = getArguments(callExpression, importer);

  callExpression.replaceWith(t.callExpression(t.identifier(functionName), [eventName, callback]));
}

個人的には、この設計は少し複雑すぎると感じています。これは remax の設計に関係している可能性があります。remax/runtime では、useAppEvent は実際には remax-framework-shared からエクスポートされます。
しかし、コードの変更に対処する方法も学びました。

4. remax-cli remax スキャフォールディング、remax プロジェクト全体、およびミニプログラムに生成されるコンパイル プロセスもここで処理されます。
まず、React ファイルが Page としてミニプログラムのネイティブ Page コンストラクターとどのように関連付けられるかを見てみましょう。
元のページコードが次のようになっていると仮定します。

'react' から * を React としてインポートします。
'remax/wechat' から { View、Text、Image } をインポートします。
'./index.css' からスタイルをインポートします。

エクスポートデフォルト()=> {
  戻る (
    <ビュー className={styles.app}>
      <View className={styles.header}>
        <画像
          src="https://gw.alipayobjects.com/mdn/rms_b5fcc5/afts/img/A*OGyZSI087zkAAAAAAAAAAAABkARQnAQ"
          クラス名={styles.logo}
          alt="ロゴ"
        />
        <View className={styles.text}>
          開始するには、<Text className={styles.path}>src/pages/index/index.js</Text> を編集します</View>
      </表示>
    </表示>
  );
};

この部分は remax-cli/src/build/entries/PageEntries.ts コードで処理されます。ここでソースコードが変更されていることがわかります。ランタイムの createPageConfig 関数は、React コンポーネントとミニプログラムのネイティブ Page に必要なプロパティを揃えるために導入され、ネイティブ Page コンストラクターが呼び出されてページがインスタンス化されます。

'path' から * をパスとしてインポートします。
'./VirtualEntry' から VirtualEntry をインポートします。

エクスポートデフォルトクラスPageEntryはVirtualEntryを拡張します{
  出力ソース() {
    戻り値 `
      '@remax/runtime' から createPageConfig をインポートします。
      './${path.basename(this.filename)}' からエントリをインポートします。
      ページ(createPageConfig(エントリ、'${this.name}'));
    `;
  }
}

createPageConfigは、Reactコンポーネントをremaxカスタムレンダリングコンテナにマウントし、同時にミニプログラムページのさまざまなライフサイクルをremaxが提供するさまざまなフックに関連付ける役割を担っています。

デフォルトの関数createPageConfig(Page: React.ComponentType<any>, name: string)をエクスポートします。
  const app = getApp() を any として返します。

  定数設定: 任意 = {
    データ: {
      根: {
        子供たち: []、
      },
      モーダルルート: {
        子供たち: []、
      },
    },

    ラッパーRef: React.createRef<any>(),

    ライフサイクルコールバック: {},

    onLoad(this: 任意、クエリ: 任意) {
      const PageWrapper = createPageWrapper(ページ、名前);
      this.pageId = generatePageId();

      this.lifecycleCallback = {};
      this.data = { // Page で定義されたデータは、実際には remax によってメモリ内に生成されたミラー ツリー ルートです: {
          子供たち: []、
        },
        モーダルルート: {
          子供たち: []、
        },
      };

      this.query = クエリ;
      // カスタム レンダラーに定義する必要があるコンテナーを生成します。this.container = new Container(this, 'root');
      this.modalContainer = 新しいコンテナ(this、'modalRoot');
      // ここでページレベルのReactコンポーネントを生成します const pageElement = React.createElement(PageWrapper, {
        ページ: これ、
        クエリ、
        モーダルコンテナ: this.modalContainer、
        参照: this.wrapperRef、
      });

      もし(アプリ&&app._mount){
        this.element = createPortal(pageElement、this.container、this.pageId);
        app._mount(これを) します。
      } それ以外 {
          // レンダリングのためにカスタム レンダラーを呼び出します this.element = render(pageElement, this.container);
      }
      //ライフサイクルでフック関数を呼び出します。 return this.callLifecycle(Lifecycle.load, query);
    },

    onUnload(これ: 任意) {
      this.callLifecycle(Lifecycle.unload);
      this.unloaded = true;
      this.container.clearUpdate();
      app._unmount(これを) します。
    },

Container は、React カスタム レンダリング仕様に従って定義されたルート コンテナです。最後に、applyUpdate メソッドでアプレットのネイティブ setData メソッドが呼び出され、レンダリングされたビューが更新されます。

適用更新() {
  this.stopUpdate || this.updateQueue.length === 0 の場合 {
    戻る;
  }

  定数 startTime = new Date().getTime();

  if (typeof this.context.$spliceData === 'function') {
    $batchedUpdates = (コールバック: () => void) => {
      折り返し電話();
    };

    if (typeof this.context.$batchedUpdates === 'function') {
      $batchedUpdates = this.context.$batchedUpdates;
    }

    $バッチ更新(() => {
      this.updateQueue.map((update, index) => {
        コールバックを未定義にします。
        if (インデックス + 1 === this.updateQueue.length) {
          コールバック = () => {
            ネイティブエフェクターを実行します。
            /* イスタンブール 次を無視 */
            (RuntimeOptions.get('debug'))の場合{
              console.log(`setData => コー​​ルバック時間: ${new Date().getTime() - startTime}ms`);
            }
          };
        }

        (update.type === 'splice')の場合{
          this.context.$spliceData(
            {
              [this.normalizeUpdatePath([...update.path, 'children'])]: [
                アップデート開始、
                更新.削除カウント、
                ...更新アイテム、
              ]、
            },
            折り返し電話
          );
        }

        (update.type === 'set'の場合){
          this.context.setData() は、
            {
              [this.normalizeUpdatePath([...update.path, update.name])]: update.value、
            },
            折り返し電話
          );
        }
      });
    });

    this.updateQueue = [];

    戻る;
  }

  const updatePayload = this.updateQueue.reduce<{ [キー: 文字列]: 任意 }>((acc, update) => {
    (update.node.isDeleted())の場合{
      acc を返します。
    }
    (update.type === 'スプライス')の場合{
      acc[this.normalizeUpdatePath([...update.path, 'nodes', update.id.toString()])] = update.items[0] || null;

      if (update.children) {
        acc[this.normalizeUpdatePath([...update.path, 'children'])] = (update.children || []).map(c => c.id);
      }
    } それ以外 {
      acc[this.normalizeUpdatePath([...update.path, update.name])] = update.value;
    }
    acc を返します。
  }, {});
  // レンダリングされたビューを更新します this.context.setData(updatePayload, () => {
    ネイティブエフェクターを実行します。
    /* イスタンブール 次を無視 */
    RuntimeOptions.get('debug') の場合 {
      console.log(`setData => コー​​ルバック時間: ${new Date().getTime() - startTime}ms`, updatePayload);
    }
  });

  this.updateQueue = [];
}

コンテナの更新は、レンダリング ファイル内のレンダリング メソッドで実行されます。

関数 getPublicRootInstance(コンテナ: ReactReconciler.FiberRoot) {
  コンテナファイバーの定数を = container.current;
  コンテナファイバーの子の場合
    null を返します。
  }
  containerFiber.child.stateNode を返します。
}

デフォルトの関数をエクスポートします。render(rootElement: React.ReactElement | null, container: Container | AppContainer) {
  // ルートコンテナが存在しない場合は作成する
  コンテナの場合:
    ReactReconcilerInst でコンテナを作成します。
  }

  ReactReconcilerInst.updateContainer(rootElement, container._rootContainer, null, () => {
    // 無視する
  });

  getPublicRootInstance(container._rootContainer) を返します。
}

さらに、ここでレンダリングされるコンポーネントは、主に forward-ref 関連の操作を処理するために、実際には createPageWrapper によってラップされます。
これで、ページ レベルの React コンポーネントがミニプログラムのネイティブ ページに関連付けられました。
コンポーネントの処理はこれに似ています。remax-cli/src/build/entries/ComponentEntry.tsファイルを参照してください。

'path' から * をパスとしてインポートします。
'./VirtualEntry' から VirtualEntry をインポートします。

エクスポートデフォルトクラスComponentEntryはVirtualEntryを拡張します{
  出力ソース() {
    戻り値 `
      '@remax/runtime' から createComponentConfig をインポートします。
      './${path.basename(this.filename)}' からエントリをインポートします。
      コンポーネント(createComponentConfig(Entry));
    `;
  }
}

通常のコンポーネントの場合、remax はそれらをカスタム コンポーネントにコンパイルします。ミニ プログラムのカスタム コンポーネントは、json、wxml、wxss、js で構成されます。React コンポーネントからこれらのファイルへの処理は、remax-cli/src/build/webpack/plugins/ComponentAsset で処理され、wxml、wxss、js ファイルが生成されます。

デフォルトクラスComponentAssetPluginをエクスポートします。
  ビルダー: ビルダー;
  キャッシュ: SourceCache = 新しい SourceCache();

  コンストラクター(ビルダー: Builder) {
    this.builder = ビルダー;
  }

  適用(コンパイラ: コンパイラ) {
    コンパイラー.フック.emit.tapAsync(PLUGIN_NAME, async (コンパイル, コールバック) => {
      const { オプション、 api } = this.builder;
      定数meta = api.getMeta();

      const { エントリ } = this.builder.entryCollection;
      Promise.allを待機します(
        Array.from(entries.values()).map(非同期コンポーネント => {
          if (!(コンポーネントインスタンス ComponentEntry)) {
            Promise.resolve() を返します。
          }
          const chunk = compilation.chunks.find(c => {
            c.name === コンポーネント名を返します。
          });
          const modules = [...getModules(chunk), コンポーネント.ファイル名];

          テンプレートPromiseを作成します。
          if (options.turboRenders) {
            // ターボページ
            templatePromise = createTurboTemplate(this.builder.api、オプション、コンポーネント、モジュール、メタ、コンパイル);
          } それ以外 {
            templatePromise = createTemplate(コンポーネント、オプション、メタ、コンパイル、this.cache);
          }

          Promise.all([ を待つ
            テンプレートPromiseを待つ、
            createManifest(this.builder、コンポーネント、コンパイル、this.cache) を待機します。
          ]);
        })
      );

      折り返し電話();
    });
  }
}

一連の Page ファイルは remax-cli/src/build/webpack/plugins/PageAsset 内で処理されます。同時に、createMainifest 内で Page とカスタムコンポーネント間の依存関係が解析され、usingComponents の関連関係が自動生成されます。

これで、React を使用して小さなプログラムを作成するための Remax フレームワークのコンパイル プロセス分析に関するこの記事は終了です (推奨)。React を使用して小さなプログラムを作成するためのより関連性の高いコンテンツについては、123WORDPRESS.COM で以前の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM を応援してください。

以下もご興味があるかもしれません:
  • Vue、React、WeChat アプレットのリッチテキストとフィルターの HTML タグを削除する方法

<<:  コンピュータが予期せずシャットダウンした後、VMware で Linux がインターネットに接続できない問題の解決策

>>:  MySQL ページングパフォーマンスの調査

推薦する

yum の基本的な使い方と例(推奨)

yumコマンドYum (フルネームは Yellow dog Updater, Modified) ...

Reactのようなフレームワークをゼロから作成する

最近、インターネットで「Build your own React」という記事を見ました。著者は、シン...

MySQL8 ベースの docker-compose デプロイメント プロジェクトの実装

1. まず、次のパスに従って対応するフォルダを作成します。 ローカルのdockerでmysqlを実行...

mysql5.7.14 解凍版インストールと設定方法 グラフィックチュートリアル (win10)

Win10はmysql5.7の解凍版をインストールします。参考までに、具体的な内容は次のとおりです...

JavaScriptオフセットは、ウィンドウ内でのマウス座標の取得とモジュールのドラッグを実装します。

オフセットOffset はオフセットです。関連プロパティの offset シリーズを使用すると、次の...

CSS マスクを使用して PNG 画像のサイズを大幅に最適化します (推奨)

この記事は共有および集約することを歓迎します。全文を転載する必要はありません。著作権を尊重してくださ...

CentOS7 カーネル カーネル5.0 バージョンアップグレード

アップグレードプロセス:元のシステム: CentOS7.3 [root@my-e450 ~]# un...

Web でよく使われるフォントの紹介 (iOS および Android ブラウザでサポートされているフォント)

年末なので仕事も少なくなっています。私が何もせずにいるのを見ると、上司はきっと不快に思うでしょう。そ...

CSSで半透明の背景色を実現する2つの方法について簡単に説明します。

ページをレイアウトする際、ユーザーに異なる視覚効果を与えるために、div の背景色を半透明の状態に設...

MySQL pt-slave-restart ツールの使い方の紹介

目次MySQL マスター スレーブ レプリケーション環境を設定する場合、マスター データベースとスレ...

LinuxにPython 3.6をインストールして落とし穴を避ける

Python 3のインストール1. 依存環境をインストールするPython3 はインストール プロセ...

JavaScript を使用してテーブル情報を追加および削除する

JavaScript 入門JavaScript は軽量なインタープリタ型の Web 開発言語です。言...

MySQL の隠し列の詳細表示

目次1. 主キーが存在する2. 主キーはないが、一意のインデックスが存在する3. 共同主キーまたは共...

重複したMySQLレコードを現場でチェックし、処理する実践的な記録

目次序文分析するデータ合計繰り返し率どこにあるかと持っているかの違い要約する序文私はソフトウェアの導...

React、Angular、Vueの3つの主要なフロントエンド技術の詳細説明

目次1. 反応する基本的な使い方注目すべき機能クラスコンポーネント仮想DOMライフサイクルメソッドJ...