抹桥的博客
Language
Home
Archive
About
GitHub
Language
主题色
250
10823 文字
54 分
フロントエンド開発の標準化 - ベストプラクティス - 開発からリリースまで

この記事は元々社内ATAプラットフォームで公開されたものです。データ匿名化処理後、この記事として再構成しました。そのため、記事内には多くの社内システムに関する紹介やリンクが含まれていますが、これらのリンクやプラットフォームは社内ネットワーク外からはアクセスできません。しかし、記事全体の読解には影響ありません。

前言#

過去1年間、私たちのチームのトラフィックビジネスは一連のリファクタリングと大幅なパフォーマンス最適化を経験しました。最近では、フロントエンド開発標準化プラットフォームの支援のもと、標準化ガバナンスも実施しました。現在、あらゆる観点から見て、アプリケーション全体は以前と比較して大幅に改善されています。

僭越ながら、その中でのいくつかの考察と選択をこの記事にまとめ、皆様の参考になれば幸いです。

技術選定#

開発フレームワークの選択#

なぜ React を選択したのか#

これについては議論の余地がありません。社内のすべてのインフラと全体的な環境が、必然的にReactを選択するよう決定づけています。

なぜ React18 なのか#

元々プロジェクトはReact16でしたが、React18の16からの主な変更点は以下の通りです。

  • コンカレントモード - React18ではデフォルトでコンカレントモードが有効になっています。簡単に言えば、コンポーネントのレンダリングが同期的な中断不可から非同期的な中断可能に変更され、ページの応答効率が向上します。
    • With this capability, React can prepare new screens in the background without blocking the main thread. This means the UI can respond immediately to user input even if it’s in the middle of a large rendering task, creating a fluid user experience.

  • 自動バッチ処理 - 複数の状態更新を1回の再レンダリングにバッチ処理することを可能にし、再レンダリングの回数を減らすことでパフォーマンスを向上させます。
  • 詳細なドキュメントはReact公式ウェブサイト:React v18.0 または社内ATA記事React18 新在哪 - ATA (atatech.org) を参照してください。

アップグレード方法#

デフォルトではReact16からReact18へのアップグレードは非破壊的ですが、React18の新機能であるコンカレントモード自動バッチ処理を利用するには、少し変更が必要です。

から

import React, { useEffect, lazy } from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
	<ShowRoom {...(window.globalUtils.getPageData() as SearchResult.PageData)} />,
	document.getElementById('root')
);

へ変更します。

import React, { useEffect } from 'react';
import { createRoot } from 'react-dom/client';

const container = createRoot(document.querySelector('#root')!);
container.render(
	<ShowRoom {...(window.globalUtils.getPageData() as SearchResult.PageData)} />
);

これによりReact18のコンカレントモード自動バッチ処理が有効になりますが、ここで少し問題が発生する可能性があります。React18以前はイベントハンドリング内のsetStateのみがバッチ処理されていましたが、現在は基本的にすべてバッチ処理されます。 テストが必要ですが、通常は問題が発生する可能性があります。開発環境ではStrictModeモードを使用することをお勧めします。これにより問題が露呈しやすくなります。

  const RootComponent = process.env.NODE_ENV === 'development' ? React.StrictMode : React.Fragment;
  if ($root) {
    const container = createRoot($root);
    container.render(
      <RootComponent>
        <DRMSearch {...(window.globalUtils.getPageData() as DRMSearchResult.PageData)} />
      </RootComponent>
    );
  }

開発言語#

typescript#

TypeScriptはJavaScriptのスーパーセットであり、TypeScriptを採用することには多くの利点があります。

  • 型システム:JavaScriptに型システムを追加します。
  • ツールサポート:コードの自動補完、定義へのジャンプ、インターフェースのヒントなどのIDE機能は、型システムのサポートを必要とします。
  • より低いメンテナンスコストと低コストでのリファクタリング

これらの利点は開発中にリアルタイムで実感でき、言うまでもありません。

TypeScriptを選択するデメリット:

  • 型を追加することによる開発コスト
  • 型システムに煩わされてanyScriptになってしまう人がいるかもしれません。

これはコードレビューでしか回避できませんが、通常、開発者はTypeScriptの利点を体験すると、そのようなことはしなくなります。

tsconfigの設定については、Reactプロジェクトでは通常、create-react-appのデフォルトtsconfigをベースとして拡張し、ほとんどのプロジェクトの要件をデフォルトで満たします。

{
  // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs
  "extends": "@tsconfig/create-react-app/tsconfig.json",
  "include": ["src", "types"],
  "compilerOptions": {
    "outDir": "./build/"
  }
}

もちろん、カスタム要件がある場合は、TypeScript: TSConfig Reference - Docs on every TSConfig option (typescriptlang.org) を参照して変更できます。

less#

スタイルプリプロセッサとして、scsslessstylusの中からlessを選択しました。

  • stylus:社内では使用されていないため、直接除外。
  • scss:fusionを含む多くの社内コンポーネントでscssが使用されていますが、scssは重すぎます。node-scssのインストールとコンパイル、およびプロジェクトのビルドのたびに時間がかかりすぎます。
  • less:実際には、PostCSSのようなポストプロセッサがあれば、これらのプリプロセッサは不要になることもありますが、長年の慣習であるネストや、簡単な変数・関数機能は開発効率を向上させることができます。そのため、機能的にはscssより劣るものの、日常の開発には完全に満足でき、十分に軽量であるlessを選択しました。

前述のpostcssですが、私たちが主に使用しているのは2つの機能です。ブラウザ互換性の解決と、モバイルでのレスポンシブ対応のためのpostcss-px-to-viewportです。設定ファイルは以下の通りです。

const pxtoviewport = require("postcss-px-to-viewport");
const postcssPresetEnv = require("postcss-preset-env");

module.exports = {
  plugins: [
    postcssPresetEnv(),
    pxtoviewport({
      viewportWidth: 375, // (Number) The width of the viewport.
      viewportHeight: 667, // (Number) The height of the viewport.
      unitPrecision: 3, // (Number) The decimal numbers to allow the REM units to grow to.
      viewportUnit: "vw", // (String) Expected units.
      selectorBlackList: [".ignore", ".hairlines", /^\.bc-/, /^\.wlkeeppx/], // (Array) The selectors to ignore and leave as px.
      minPixelValue: 1, // (Number) Set the minimum pixel value to replace.
      mediaQuery: true, // (Boolean) Allow px to be converted in media queries.
    }),
  ],
};

postcss-preset-envbrowserslistの設定に依存します。これは自身のビジネスにおけるブラウザの分布状況に応じて具体的に検討してください。

  "browserslist": [
    "android >= 7",
    "ios >= 13"
  ],

コンポーネントライブラリ#

Fusion Next コンポーネント#

バイヤー向けのコンポーネントライブラリは現在Fusionしか選択肢がありません。注意すべき点は、@alife/nextではなく@alifd/nextパッケージを使用する必要があることです。後者はすでに更新されていないはずです。

Fusionのコンポーネントを使用すると、直接使用した場合のサイズが大きすぎるという問題があります。gzip圧縮後でも、JSとCSSを合わせると300KB以上になります。また、プロジェクトのビルドに参加させると、開発サービスのビルド速度が大幅に低下します。

サイズと開発体験の問題を解決するため、TrafficModlibを開発しました。

TrafficModLib#

これは私たちのトラフィックビジネスの共通コンポーネントライブラリで、Fusionの一部のコンポーネントと私たちのビジネスコンポーネントを含んでいます。このような分割により、Fusionを直接参照することによるサイズの問題やアプリケーション開発体験の問題を回避します。これについては後で詳しく説明します。

ビルドツール#

元々トラフィックのアプリケーションはいくつかの独立したアプリケーションに分散して個別にビルドされていましたが、以前に一度改修を行い、統一ビルダfieにアップグレードしました。これにより、複数のプロジェクトにおけるビルドツールのメンテナンスコストと潜在的なリスクを低減しました。

しかし、グループの分割という背景とfieのサポート問題を考慮し、最終的にfieの基盤を放棄し、グループ間の依存関係がないicbuBuyerビルダに改造することを選択しました。

その過程は紆余曲折がありました。当初はdefをベースにスキャフォールディングとビルダを開発しようとしましたが、defのコマンドラインツールが更新されなくなり、o2にアップグレードされました。しかし、o2のコマンドラインサポートも現状では懸念があります。最初からdefをベースにし、そこからo2にアップグレードする過程で多くの遠回りをしてしまい、最終的にdef-dev-kitに対応するアップグレード後のo2コマンドのドキュメントは一切見つかりませんでした。そのため、最終的にはdefo2といったものをすべて放棄し、シンプルで一般的なコマンドラインツールを自社で開発することを選択しました。

icbuBuyer#

私たちは最終的にこのビルダをicbu-buyerと名付け、バイヤー向けフロントエンドビルダと位置付けました。現在の使用ドキュメントは以下です:脚手架 icbu-buyer 使用文档 (antfin.com)

このビルドツールは非常にシンプルなことしか行っていません。単一ページおよび複数ページ開発をサポートするWebpack設定をラップし、プロジェクト初期化時に開発標準に準拠した環境設定(例:ESLint設定、Prettier、Commitlintなど)を行うだけです。そこには何の魔法もありません。最終ビルド時には、SSRシナリオ向けに特別に最適化されたリソースが個別にビルドされます。コアとなるWebpack設定は以下の通りです。

export default function (userCofig: Partial<userConfig>): Configuration {
  const { port, production, debug, entryName, type } = userCofig;
  return {
    context: cwd,
    entry: getEntry({ dev: !production, entryName }),
    output: {
      publicPath: '',
      path: getBuildPath(),
      filename: '[name].js',
      chunkFilename: '[name].chunks.js',
      crossOriginLoading: 'anonymous',
    },

    mode: !production ? 'development' : 'production',
    devtool: !production && 'source-map',
    devServer: !production
      ? comboProxy({
          port,
          host: '0.0.0.0',
          hot: true,
        })
      : {},

    resolve: getResolve(),

    optimization: getOptimization(),

    module: {
      rules: getLoaderRules({ production, debug, entryName }),
    },

    externals: getExternals(false, userCofig),

    plugins: [
      debug && new BundleAnalyzerPlugin(),
      new MiniCssExtractPlugin({
        filename: '[name].css',
        chunkFilename: '[name].chunk.css',
      }),
      !production &&
        new ForkTsCheckerWebpackPlugin({
          typescript: {
            memoryLimit: 1024,
          },
        }),
    ].filter(item => !!item) as Configuration['plugins'],
  };
}

コードアドレス @ali/builder-icbu-buyer - Alibaba NPM Registry (alibaba-inc.com)

統一ビルダの利点とは#

大規模なビジネスにおける複数のアプリケーションで統一ビルダを採用することで、多くのメンテナンスや選択に関する問題を減らすことができます。つまり、アプリケーションを独立して分割するメリットを享受しつつ、分割後のメンテナンスコストの問題を軽減することができます。

主な利点:

  • Babelの統一設定と管理:あるプロジェクトで動作するコードが、別のプロジェクトでBabel設定の問題によりブラウザ互換性の問題に遭遇するといった事態が発生しません。
  • コードスタイルの統一管理:プロジェクト起動時に現在のプロジェクトの様々な設定が規範に準拠しているかを確認し、準拠していない場合は強制的に統一できます。これにより、各プロジェクトでESLint、Stylelint、Prettier、Huskyなどの面倒な設定を手動で行う必要がなくなります。
  • 依存関係の認知負荷の軽減:各アプリケーションは、devDependentsにBabelやWebpackなどの依存関係を大量に記述する必要がなくなります。

欠点:

  • ローカルの開発サービスおよびビルド関連の設定が統一されたicbuBuyerに集約されているため、直接的で分かりやすいWebpack設定ファイルと比較して、理解コストが若干上昇します。

また、iceumiなどのソリューションを採用するビジネスが増えているのも、同様のビルド関連設定のメンテナンスを面倒に感じるためです。異なる点は、iceのようなツールはより汎用的に多くのビジネスシナリオをカバーするため、比較的肥大化し、同時に技術的な自律性が低くなることです。

これらの点を考慮した結果、私たちはビジネスシナリオにより適合し、コストが低く、より制御しやすい、独自のビルドツールを最小限のシンプルな形でカプセル化することを選択しました。

アプリケーションの接続方法#

プロジェクトでtnpm install @ali/builder-icbu-buyer && npx ibuyer setupを実行するだけで、プロジェクトにicbubuyer.config.jsが書き込まれ、クラウドビルドのabc.json設定が変更されます。

icbubuyer.config.jsはアプリケーションのカスタム設定であり、一般的なアプリケーションではデフォルト設定で十分ですが、カスタマイズが必要な場合はicbubuyer.config.jsの内容を変更できます。

/**
 * ICBU 买家前台通用构建器配置
 * 只要 abc.json 中的 type 以及 builder 为以下字段
 *    "type": "icbu-buyer"
 *    "builder": "@ali/builder-icbu-buyer"
 * 那么就不要删除该文件,否则构建器将无法正常工作。
 */
const fs = require("fs");
const path = require("path");
const cwd = process.cwd();
const { DEV, DEBUG } = process.env;

module.exports = {
  /**
   * 项目类型, 取值为以下值之一,
   * const PROJECT_TYPE = {
      pc: 'web-app-pc',
      m: 'web-app-m',
      npm: 'npm-package',
    };
   */
  type: "web-app-pc",
  port: 3000,
  // 目前仅支持 webpack 作为构建器,后续可能支持别的比如 vite 等
  builder: "webpack",
  // 如果有提取业务功能组件作为独立的包,挂载在全局的 window 上,需要在这里配置
  modLib: {
    // 公共包 package.json 中的 name,用来作为 import 时候的 id
    // 比如 import {Button} from "@alife/icbu-mod-lib",packageName 为 @alife/icbu-mod-lib
    packageName: "@alife/icbu-traffic-mod-lib",
    // 公共包挂载到全局变量上的命名,比如 IcbuModLib
    modName: "TrafficModLib",
  },
  extendWebpackConfig: (webpackConfig) => {
    // NOTE: 如果一定要自定义 webpack 的配置,可以在这里做一些修改。
    // 但是请注意,如果你的配置有问题,可能会导致构建失败,而且可能会和后续的脚手架升级不兼容【所以强烈不建议修改】
    // webpackConfig.externals = {
    //   react: 'window.React',
    //   'react-dom': 'window.ReactDOM',
    // };
    // TODO: 待七巧板的引用改为 index.js 后,删除 showroom 的配置
    return webpackConfig;
  },
};

技術規約#

コード規約#

コード規約については特に言うことはありません。JS以外は以前のグループフロントエンド技術部の全体規約を採用し、JSについては以前のeslint-config-ali規約が古く、メンテナンスされていないため、現在最新のeslint-config-attを採用しています。

{
    extends: ['stylelint-config-ali'],
    customSyntax: 'postcss-less',
    plugins: ['stylelint-order'],
    rules: {
      'no-descending-specificity': null,
    },
};
  • コードスタイルの統一:Prettierは通常の規約に加えてimportの順序規約を追加しており、すべての人のコードのimport順序が一致することを保証します。参考:
module.exports = {
  printWidth: 100,
  tabWidth: 2,
  useTabs: false,
  semi: true,
  singleQuote: true,
  trailingComma: "es5",
  bracketSpacing: true,
  arrowParens: "avoid",
  proseWrap: "always",
  importOrder: [
    "public-path",
    "<THIRD_PARTY_MODULES>",
    "^@ali/(.*)$",
    "^@alife/(.*)$",
    "^@alifd/(.*)$",
    "^[./]",
    "^[./](.*).(json)$",
    "^[./](.*).(css|less|scss|sass)$",
  ],
  importOrderSeparation: true,
  importOrderSortSpecifiers: true,
  plugins: ["@trivago/prettier-plugin-sort-imports"],
};
  • lint-stagehuskyを通じて、コミットされるコードがすべて合格であることを保証します。

コミットインターセプト#

上記の規約を設定しても、誰も実行しなければ意味がありません。そのため、コミット時に統一されたフックを追加し、提出されるすべてのコードが規約に準拠していることを保証しています。ここではhuskylint-stageを使用しており、具体的な設定方法は公式ドキュメントを参照してください。私たちの設定は以下の通りです。

  "lint-staged": {
    "*.{json,md,scss,css,less,html}": "npx prettier --write --ignore-unknown",
    "src/**/*.{js,jsx,ts,tsx}": [
      "npx prettier  --write",
      "npx eslint --fix"
    ]
  },

クイック接続#

これらのコード規約やコミットインターセプトは設定がかなり面倒なので、私たちは迅速に接続できるツールを用意しています。プロジェクトでtnpm install @ali/builder-icbu-buyer && npx ibuyer setup --configOnlyを実行するだけで、この一連の規約に素早く接続できます。

アーキテクチャ設計#

以前のアプリケーションアーキテクチャは以下の通りでした。

应用架构-before

これらのビジネスはすべて検索アプリケーションのsearchBoostsclistに混在しており、まさに「一髪が全身を動かす」状態でした。SEOのリリース一つで検索フロントエンドがダウンする可能性さえありました。もちろん、これには歴史的な理由や組織構造上の理由もありますが、過去の問題について深入りせず、現在のビジネスにより適応するためにどのように調整すべきかを見ていきましょう。

背景分析#

トラフィックビジネスドメインで具体的にサポートする必要があるビジネスシナリオ:

  • 無料ランディングページ:主にSEOシナリオのshowroomcountrySearchで、PCとモバイルの両方を含みます。
  • 有料ランディングページ:PPC、PLA、DRMなどで、PCとモバイルの両方を含みます。

現在、私たちは最終的にこれらビジネスをそれぞれ4つのアプリケーションで受け持つことを選択しました。

  • traffic-free-pc:無料ランディングのPCアプリケーション
  • traffic-free-wap:無料ランディングのモバイルアプリケーション
  • traffic-pay-pc:有料ランディングのPCアプリケーション
  • traffic-pay-wap:有料ランディングのモバイルアプリケーション

このアプリケーションの分割粒度は比較的適切であり、ビジネスドメイン分離とデバイスタイプ分離の理念に従っています。

  • ビジネスドメイン分離:まず、ビジネスドメイン間は独立して分割される必要があります。つまり、無料と有料のビジネスは同じアプリケーション内にあってはならず、互いに影響し合ってはなりません。
  • デバイスタイプ分離:一つのビジネスドメイン内では、PCとモバイルはアプリケーションを分離する必要があります。両者にはフロントエンドでの共通点がありません。

同時に、私たちはこれに加えてさらに2つのアプリケーションを構築しました。

  • traffic-base:グローバルな基本メソッドを提供
  • traffic-mod-lib:ビジネスドメインの共通コンポーネントを提供

なぜさらに2つのアプリケーションを抽出する必要があるのかというと、その核心は複雑度の分割にあります。つまり、複雑度の高い部分を可能な限りtraffic-baseまたはtraffic-mod-libに抽出し、上位のビジネスアプリケーションは最終的なページ表示のみに関心を持つようにするためです。複雑度の分割以外にも、付随的な利点としてパフォーマンスと開発体験があります。これについては後で詳しく説明します。

最終的な解決策#

最終的なアプリケーションアーキテクチャは以下の通りです。 应用架构-before

新アーキテクチャの利点#

複雑度管理(Complexity Management)#
  • 上位ビジネスアプリケーションは主にビジネス関連ロジックとフロントエンドレンダリングを担当し、そのロジックをより独立させます。
  • 高度な複雑性を持つロジックはtraffic-mod-libtraffic-baseに下層化され、これにより複雑度の効果的な分割が実現されます。
コード再利用性(Code Reusability)#
  • 基本コンポーネントとメソッドをtraffic-basetraffic-mod-libに抽出することで、コードの再利用性が向上するだけでなく、リリースリスクも低減されます。
  • 基本メソッドはtraffic-baseに抽出され、開発者がグローバルでビジネスに依存しないメソッドを独立させることを奨励します。
安定性(Stability)#
  • 上位ビジネスのリリース頻度が最も高く、共通コンポーネントと基本メソッドのリリース頻度は比較的低いため、このような階層設計により全体のリリースリスクが低減されます。
  • ビジネスドメイン(無料および有料アプリケーションなど)間のリリースは互いに影響せず、システムの安定性をさらに向上させます。
開発体験(Development Experience)#
  • 依存ライブラリ(React, ReactDOM, Fusionなど)がビルドごとに再コンパイルされる必要がなく、アプリケーションの分割により各アプリケーション内の無関係なコードが大幅に削減されるため、ローカル開発サービスのコールドスタートとホットコンパイルにかかる時間が大幅に短縮され、開発体験が劇的に向上します。
  • 各コードリポジトリ内の無関係なコードが減少することで、開発者の認知負荷も軽減されます。
パフォーマンス(Performance)#
  • リリース頻度の低い基本アプリケーション(traffic-mod-libtraffic-base)は、CDNおよびユーザーブラウザ側でキャッシュされやすいため、受動的にページパフォーマンスが向上します。

最終的にビルドされたコードサイズは70%削減され、ビルド時間は50%以上短縮されました。詳細はドキュメント:流量业务重构升级 (antfin.com) を参照してください。

基本アプリケーションの詳細#

traffic-base#

グローバルソリューションを提供します。具体的には、統一されたログ記録、統一されたイベントリスニング、統一されたイベント発行/購読メカニズム、統一されたサードパーティリソースロード制御などです。

ログ収集#

例えば、ログ記録を標準化するために打点规范 (antfin.com)を策定しましたが、全員のログ記録が規範に準拠していることをどのように保証するのでしょうか?コードレビューはもちろん可能ですが、コストが高すぎます。そのため、私たちのソリューションは、すべてのログ記録メソッドをtraffic-baseに集約することです。自動露出ログ記録はtraffic-baseによって直接制御され、クリックログ記録はtraffic-baseが提供するlogClickメソッドを統一して使用します。これにより、すべてのログ記録が規範に準拠していることを保証できます。

  • 統一された露出ログ記録コード
  • 統一されたクリックログ記録メソッド
イベントバス管理#

多くのビジネスシナリオでは、コンポーネントやモジュールがイベントメカニズムを介して通信する必要があります。このようなイベント発行-購読パターンは、顕著な疎結合の利点がありますが、過度な疎結合は欠点でもあります。時間の経過とともにプロジェクトの複雑さが増すと、明確な制約やドキュメントがない場合、プロジェクトが制御不能な様々なイベントで溢れてしまう可能性があります。 このため、私たちは以下の統一された制御ソリューションを採用しました。

統一されたリスニングと購読メソッド

統一されたTrafficEventクラスを使用することで、すべてのイベントを一元的に管理できます。このクラスはEventEmitterを継承し、事前定義されたEventTypeオブジェクトを含んでいます。

import { EventEmitter } from 'events';
import { EventType } from './constants';

class TrafficEvent extends EventEmitter {
  EventType: typeof EventType;

  constructor() {
    super();
    this.EventType = EventType;
  }

  on<T extends string | symbol>(event: T, fn: (...args: any[]) => void, context?: any): this {
    if (typeof event === 'string') {
      if (!Object.values(EventType).includes(event)) {
        throw new Error(`无效的事件类型:${event}`);
      }
      console.log(`订阅事件:${event}`);
    }
    // 如果类型是 symbol,则不进行 EventType 校验
    return super.on(event, fn, context);
  }

  emit<T extends string | symbol>(event: T, ...args: any[]): boolean {
    if (typeof event === 'string') {
      if (!Object.values(EventType).includes(event)) {
        throw new Error(`无效的事件类型:${event}`);
      }
      console.log(`发布事件:${event}`);
    }
    // 如果类型是 symbol,则不进行 EventType 校验
    return super.emit(event, ...args);
  }
}

export default new TrafficEvent();

統一されたイベントID管理

すべてのイベントタイプはEventTypeオブジェクトに事前に定義されています。これにより、コードの可読性が向上するだけでなく、イベントの一意性も保証されます。

export const EventType = {
  scrollToEnd: "scrollToEnd",
  scrolling: "scrolling",
  videoChange: "videoChange",
};

このような設計を使用することで、イベント通信を必要とするすべてのコンポーネントはbaseに新しいeventTypeを登録する必要があります。同時に、このソリューションは、事前に定義されたイベントIDのみを発行および購読するように制限します。この方法により、イベント通信システム全体が制御可能で管理可能な状態になります。必要であれば、TrafficEventバスにさらなる管理および監視機能を追加することもできます。

このように、私たちは疎結合の利点を維持しつつ、統一された管理メカニズムを通じて、プロジェクトの保守性と制御性を向上させました。

例:

export function useScrollToEnd(fn: () => void): void {
  const { globalUtils } = useGlobalUtils();
  React.useEffect(() => {
    if (!globalUtils) return;
    const { eventEmitter } = globalUtils;
    eventEmitter.on(eventEmitter.EventType.scrollToEnd, fn);
    return () => {
      eventEmitter.off(eventEmitter.EventType.scrollToEnd, fn);
    };
  }, [fn, globalUtils]);
}
ページデータ取得#

統一されたページ同期データ取得メソッドを提供しており、上位のビジネスアプリケーションはページデータを取得する際に、window下のグローバル変数を直接読み取るのではなく、このメソッドを呼び出すことで取得します。

  getPageData(): Record<string, any> {
    return window._PAGE_DATA_;
  }

非常にシンプルに見えますが、これは開発理念を体現しています。つまり、グローバル変数の使用は管理される必要があり、同時に詳細が隠蔽されるべきだということです。上位のビジネスアプリケーションにとって、getPageDataメソッドを通じてページデータを取得すればよいということを知っていれば十分であり、それがどのようにして取得されるかは重要ではありません。

これにより、上位のビジネスアプリケーションはこのグローバル変数を直接操作する必要がなくなります。もしある日このグローバル変数が他の変数と衝突した場合でも、グローバル変数の読み書きが複数のシステムの様々なコードファイルに散らばっているために変更できず、手詰まりになるという事態を避けて、安心して修正を行うことができます。

さらに一歩進んで、この読み取り専用データについては、取得時に直接Object.freeze()で凍結し、読み取り専用にすることで、どこかで変更されてしまうことによる潜在的なバグを防ぐことができます。

その他#

その他にも、グローバルなページスクロールリスニング、統一されたサードパーティリソースロード制御によるページコア機能の優先順位付けなどのメソッドがあります。

traffic-mod-lib#

共通コンポーネントアプリケーションとして、主にFusionNextコンポーネントと私たち自身のビジネス共通コンポーネントを担います。

FusionNextコンポーネントは非常に多岐にわたるため、サイズも驚くほど大きいです。私たちのシナリオではその一部のコンポーネントしか使用しないため、個別の処理を行いました。

// ---------------Next 组件 start---------------
import Balloon from "@alifd/next/lib/balloon";
import Button from "@alifd/next/lib/button";
import Checkbox from "@alifd/next/lib/checkbox";
import Collapse from "@alifd/next/lib/collapse";
import ConfigProvider from "@alifd/next/lib/config-provider";
import Dialog from "@alifd/next/lib/dialog";
import Drawer from "@alifd/next/lib/drawer";
import Icon from "@alifd/next/lib/icon";
import Input from "@alifd/next/lib/input";
import Loading from "@alifd/next/lib/loading";
import Pagination from "@alifd/next/lib/pagination";
import Select from "@alifd/next/lib/select";
import Slider from "@alifd/next/lib/slider";
import Tag from "@alifd/next/lib/tag";
// ---------------Next 组件 end---------------

export {
  Balloon,
  Button,
  Checkbox,
  Collapse,
  Dialog,
  Loading,
  Pagination,
  Icon,
  ConfigProvider,
  Tag,
  Select,
  Slider,
  Drawer,
  Input,
  NextLocalEnUS,
};

その他はビジネス共通コンポーネントであり、特に説明することはありません。

import "./public-path";

import classnames from "classnames";
import React from "react";
import ReactDOM from "react-dom";

// import IcbuIcon from './components/icon';
import IcbuIcon, { Certificate, FlagIcon, GsYear } from "@alife/bc-icbu-icon";
import safeLog from "@alife/bc-safe-log";

import Alitalk from "./components/alitalk";
import "./index.less";
import "./index.scss";
// ---------------PC流量Footer三巨头-------------
import {
  FloatingWindow,
  RecommendLayer,
  RfqLayer,
  Swiper,
  SwiperRefType,
} from "./modules";
import {
  Balloon,
  Button,
  Checkbox,
  Collapse,
  ConfigProvider,
  Dialog,
  Drawer,
  Icon,
  Input,
  Loading,
  NextLocalEnUS,
  Pagination,
  Select,
  Slider,
  Tag,
} from "./next";

IcbuIcon.GsYear = GsYear;
IcbuIcon.FlagIcon = FlagIcon;
IcbuIcon.Certificate = Certificate;

export {
  // 基础框架&方法
  React,
  ReactDOM,
  classnames,
  safeLog,

  // next 组件
  Balloon,
  Button,
  Checkbox,
  Collapse,
  Dialog,
  Loading,
  Pagination,
  Icon,
  ConfigProvider,
  Select,
  Tag,
  Slider,
  Drawer,
  Input,
  NextLocalEnUS,

  // 流量业务组件
  FloatingWindow,
  RfqLayer,
  RecommendLayer,
  Swiper,
  // 三方业务组件
  IcbuIcon,
  Alitalk,
};

export type { SwiperRefType };

注意すべき点は、上位ビジネスアプリケーションがtraffic-mod-lib内のコンポーネントをプロジェクトでどのように使用するかです。ここでtraffic-mod-libのビルド処理について触れる必要があります。私たちはこれを通常のアプリケーションとしてではなく、独立したパッケージとしてビルドする必要があり、プロジェクト下のicbubuyer.config.jsを変更します。

  extendWebpackConfig: (webpackConfig, env) => {
    const isSSR = env === 'ssr';
    // 本应用会作为一个基础包加载到其他应用中,所以需要将打包后的代码挂载到全局变量上
    webpackConfig.output.library = 'TrafficModLib';
    webpackConfig.output.libraryTarget = isSSR ? 'global' : 'window';
    webpackConfig.output.globalObject = isSSR ? 'global' : 'window';
    return webpackConfig;
  },

このように、ページがtraffic-mod-libのJSをロードするだけで、グローバル変数にTrafficModLibという変数がマウントされます。その後、上位ビジネスアプリケーションのicbubuyer.config.jsで設定を行う必要があります。

  modLib: {
    // 公共包 package.json 中的 name,用来作为 import 时候的 id
    // 比如 import {Button} from "@alife/icbu-mod-lib",packageName 为 @alife/icbu-mod-lib
    packageName: '@alife/icbu-traffic-mod-lib',
    // 公共包挂载到全局变量上的命名,比如 IcbuModLib
    modName: 'TrafficModLib',
  },

これにより、@alife/icbu-traffic-mod-libからのすべての参照は最終的にグローバル変数TrafficModLibから取得されるようになります。例えば:

import {
  Alitalk,
  Balloon,
  Button,
  IcbuIcon,
  Icon,
  Swiper,
  SwiperRefType,
} from "@alife/icbu-traffic-mod-lib";

基本フレームワーク#

React、ReactDOMのような基本フレームワークは、独立したJSファイルとして導入しています。グループは対応するCDNを提供しており、トラフィックビジネスでは現在最新のReactバージョンを参照しています。

  • 生産環境:https://s.alicdn.com/@g/code/lib/??react/18.2.0/umd/react.production.min.js,react-dom/18.2.0/umd/react-dom.production.min.js
  • 開発環境:https://s.alicdn.com/@g/code/lib/??react/18.2.0/umd/react.development.js,react-dom/18.2.0/umd/react-dom.development.js
    • lightProxyを使用してローカルプロキシを行う場合、開発時にこのコマンド https://s.alicdn.com/@g/code/lib/??react/18.2.0/umd/react.production.min.js,react-dom/18.2.0/umd/react-dom.production.min.js https://s.alicdn.com/@g/code/lib/??react/18.2.0/umd/react.development.js,react-dom/18.2.0/umd/react-dom.development.js を追加することで開発体験を向上させることができます。

独立したCDNを使用して基本フレームワークコードを導入する利点:

  • より永続的なキャッシュと比較的高いキャッシュヒット率
  • 開発サービスのコンパイル・ビルド時間をさらに短縮し、開発体験を向上させます。これにより、最終世代のIntel最高峰と謳われながらも実際には性能が低く、16GBメモリが常に満杯になり、ファンが激しく回転し、システムが頻繁にフリーズ寸前になり、さらにCloud Shellに酷使されているMacBookを持つ開発者に配慮し、慈悲深い人間となることができます。

開発とデバッグ#

プロジェクトディレクトリ構造#

これは私たちのビジネスアプリケーションのディレクトリ規約です。

.
├── README.md                   # 项目文档
├── abc.json                    # 云构建的配置
├── demo                        # 示例代码
│   ├── data.json               # 示例数据
│   ├── index.html              # 示例HTML
│   ├── index.tsx               # 示例TypeScript React文件
│   └── pla.json                # 示例配置
├── icbubuyer.config.js         # icbubuyer 构建器配置
├── package.json                # 依赖管理
├── src                         # 源代码
│   ├── component               # 共用组件
│   ├── hook                    # 自定义 Hooks
│   ├── module                  # 业务模块
│   ├── pages                   # 页面级组件
│   ├── public-path.js          # webpack 的chunk 引入路径配置
│   ├── style                   # 样式文件
│   ├── types                   # 类型定义
│   └── utils                   # 工具函数
├── tsconfig.json               # TypeScript配置
└── yarn.lock                   # Yarn锁文件

すべてのプロジェクトが同じ規約を維持することの利点は言うまでもありませんが、あえて言うなら以下の通りです。

  1. コードの構成と保守性: 明確なディレクトリはメンテナンスの難易度とエラー率を低減します。
  2. 参入障壁の低減: 一つのプロジェクトのディレクトリ構造を理解すれば、他のすべてのプロジェクトも理解できます。
  3. コードの可読性: 明確で分かりやすいディレクトリは、プロジェクトの理解コストを低減し、コードの可読性を向上させます。
  4. コラボレーション効率の向上: 設定よりも規約が優先され、規約は不要なコミュニケーションを減らし、チームの効率を向上させます。

ローカル開発#

すべてのプロジェクトは統一されたnpmスクリプト規約を持っています。

  • npm run startでローカルデバッグサービスを開始
  • npm run buildでローカルビルドを実行

開発サービスを起動する際に、あるプロジェクトではnpm run start、別のプロジェクトではnpm run devといった違いはありません。package.jsonを見ればわかることではありますが、見ないで済む方が手間がかからず、心身ともに快適です。

npm run startを実行すると、コンソールにいくつかの情報が出力されます。ビルダの更新が必要か、最終的に使用される設定、開発サービスのポートアドレスなどです。

本地开发

その後、以下のhostsまたはプロキシをバインドする必要があります。主な目的は、すべてのフロントエンドリソースをローカル開発サービスにプロキシすることです。バックエンドのプリプロダクション環境をテストするか、本番環境をテストするかは、自身の要件に応じて適切なプロキシを設定してください。

// 前端资源代理
127.0.0.1 assets.alicdn.com
127.0.0.1 s.alicdn.com
127.0.0.1 dev.g.alicdn.com

この時点で、デバッグが必要な本番ページを開くと、コンソールに次のような出力が表示されます。 本地开发 これは、現在開いているページがローカル開発サービスのコードを使用していることを示しており、直接コードの変更とデバッグを行うことができます。

複数アプリケーションの開発と連携デバッグ#

前述のアーキテクチャ部分で述べたように、どのページも実際には少なくとも3つのアプリケーションに依存しています。

  • 上位ビジネスアプリケーション
  • traffic-mod-lib
  • traffic-base

これによりいくつかの問題に直面します。

  1. 上位ビジネスアプリケーションでtraffic-mod-libのコンポーネントやtraffic-baseのメソッドを呼び出す際、どのようなコンポーネントやメソッドがあるのかをどうやって知るのでしょうか?毎回コードを見る必要があるのでしょうか?
  2. ある要件で上位ビジネスアプリケーション、traffic-mod-libtraffic-baseを同時に変更する必要がある場合、どのようにデバッグするのでしょうか?毎回他のアプリケーションをプリプロダクション環境にデプロイしたり、大量のプロキシルールを記述してローカルデバッグを行う必要があるのでしょうか?

これらの問題に順次答えていきます。

アプリケーション間API呼び出しのヒント#

ビジネスアプリケーションでtraffic-mod-libのコンポーネントやtraffic-baseのメソッドを使用する際、使うたびにコードやドキュメントを調べるのは不可能であり、効率が低く、エラーも発生しやすいため、開発体験に悪影響を与え、アプリケーションの分割が無駄な手間だと感じさせてしまいます。

この問題の答えは、実際には以前にTypeScriptを採用した理由への回答でもあります。つまり、2つの下位アプリケーションが両方ともd.ts定義ファイルをビルドし、各上位ビジネスアプリケーションがそれを参照するだけでよいということです。

例えばtraffic-baseには、いくつかのコマンドがあります。

"build-types": "./node_modules/.bin/tsc --emitDeclarationOnly",
"publish-patch": "rm -rf ./typings && tnpm run build-types && tnpm run build  && tnpm version patch && tnpm publish && git push"

build-typesを実行すると、プロジェクト下にアプリケーション全体の型定義ファイルが生成され、npmパッケージとして公開されます。ビジネスアプリケーションのd.tsファイルでそれを参照するだけでよいのです。

/// <reference types="@alife/cdn-traffic-base/typings/pc" />

これにより、実際に使用する際に型安全なコードヒントが得られます。

  1. ビジネスアプリケーションがグローバル変数から直接メソッドを読み出して呼び出すことは推奨されないため、traffic-baseメソッドの呼び出しをカプセル化しました。 traffic-base
  2. 使用時に、完全なAPIヒントが得られます。 traffic-base

traffic-mod-libはさらに簡単で、型定義ファイルをビルドしてnpmパッケージとして公開するだけで、上位ビジネスアプリケーションは直接使用できます。 traffic-base

注意すべき点は、@alife/icbu-traffic-mod-lib@alife/cdn-traffic-baseも、ビジネスアプリケーションで参照されていても、プロジェクトのビルド、コンパイル、パッケージングには実際には関与していないことです。Webpackではすべて外部化されています。icbubuyer.config.jsの設定を参照してください。

  modLib: {
    // 公共包 package.json 中的 name,用来作为 import 时候的 id
    // 比如 import {Button} from "@alife/icbu-mod-lib",packageName 为 @alife/icbu-mod-lib
    packageName: '@alife/icbu-traffic-mod-lib',
    // 公共包挂载到全局变量上的命名,比如 IcbuModLib
    modName: 'TrafficModLib',
  },

複数アプリケーションのローカル連携デバッグ#

ある要件で上位ビジネスアプリケーション、traffic-mod-libtraffic-baseを同時に変更する必要がある場合、どのようにデバッグするのでしょうか?私たちのビルダicbu-buyerでは、Webpackのローカル開発サービスとしてcomboProxyを使用しています。ビルダ内の具体的なコードは以下の通りです。

    devServer: !production
      ? comboProxy({
          port,
          host: '0.0.0.0',
          hot: true,
        })
      : {},

ここでのcomboProxyは、私たちが独自にメンテナンスしているパッケージであり、社内アドレスは@ali/webpack-server-combo-proxy - Alibaba NPM Registry (alibaba-inc.com)です。現在、店舗、トラフィック、およびいくつかのバックエンドアプリケーションはすべて、このパッケージをローカル開発サービスとして使用しています。

webpack-server-combo-proxy#

このプラグインは主に以下のことを行います。

  • バックグラウンドサービスを起動し、80番および443番ポートで実行します。
  • 現在のアプリケーションをこのバックグラウンドサービスに登録します。
  • ブラウザからのリクエストがローカルサービスにプロキシされた際、登録されたアドレスに基づいて具体的なローカルアプリケーションに転送します。

webpack-server-combo-proxy

例として、私がcdn-traffic-pay-pcアプリケーションを開発しているとします。ローカルでnpm run startを実行して開発サービスを起動すると:

  1. バックグラウンドサービスが起動しているか確認し、起動していなければ新しいサービスを起動して80番および443番ポートを占有します。
  2. このバックグラウンドサービスに、cdn-traffic-pay-pcアプリケーションのGitアドレスをIDとして登録します。127.0.0.1を開くと、以下のように表示されます。
    1. webpack-server-combo-proxy
  3. ブラウザが有料ランディングページ(例:alibaba.com/premium/Seals.html?src=sem_ggl&mark=drm0611&tagId=62431584548&product_id=62431584548&pcate=1407&cid=1407&ali_creative_id=7396748df8d7b7171c8c69322fc97f56&ali_image_material_id=0f7f01f815bf5e66f0fe539b6a5802ff)にアクセスすると、drm-search.cssなどの依存リソースは、まずローカルのバックグラウンドサービスを経由し、判断を経て最終的にローカルのwebpack-dev-serverに転送されるか、またはプリプロダクションフロントエンドまたは本番環境のアドレスに直接リクエストされます。

この時、他のプロジェクトも同時にデバッグする必要がある場合、デバッグ対象のプロジェクトでnpm run startを実行するだけで、同様のロジックに基づいて新しいポートとサービスアドレスが登録されます。例えば、traffic-mod-libを開発する必要がある場合、ローカル開発サービスを起動してから127.0.0.1にアクセスすると、cdn-traffic-mod-libの登録アドレスが追加されていることがわかります。 webpack-server-combo-proxy

このようにして、再度フロントエンドページにアクセスすると、cdn-traffic-pay-pcのリンクに合致するリクエストパスはポート3000下の開発サービスに転送され、cdn-traffic-mod-libのリンクに合致するリクエストはポート3001下に転送され、複数のプロジェクトの同時連携デバッグが実現されます。

このメカニズムのいくつかの重要なポイント:

  1. フロントエンドアプリケーションがCDNに公開された後のアドレスとGitリポジトリのアドレスの間には1対1の対応関係があります。例えば、このCDNリソース:https://s.alicdn.com/@g/ife/cdn-traffic-mod-lib/1.0.4/index.jsに対応するコードリポジトリのGitアドレスパスは[email protected]:ife/cdn-traffic-mod-lib.gitです。したがって、私たちのcomboProxyはリソースリンクを通じて対応するコードリポジトリを解析し、正しいローカルサービスアドレスに登録および転送することができます。
  2. サービスは80番および443番ポートで実行される必要があり、その後、hostsバインディングまたはプロキシを通じて、すべてのフロントエンドリソースリクエストをこのローカルサービスにプロキシします。これにより、すべてのリソースリクエストはローカルプロキシサービスを経由することになります。

パフォーマンス#

ランディングページはGoogleのアルゴリズムランキングや有料のランディングレートに直接影響するため、パフォーマンス体験を非常に重視しています。そのため、パフォーマンスに関しても多くの取り組みを行ってきました。本記事ではコードエンジニアリングの側面のみに焦点を当てていますが、完全なパフォーマンス最適化ガイドについては、Alibaba.com 性能优化经验总结を参照してください。

アプリケーションレベルでは、以下のことを行いました。

  • 非コア機能の非同期化
  • CDN設定
  • 共通ライブラリの独立した導入

非コア機能の非同期化#

ページ上の非コア機能については、Webpackの非同期importを使用してロードできます。例えば、私たちのページの左側フィルタモジュールです。

function LeftFilterAsync({ data }: { data: PPCSearchResult.PageData }) {
  const TrafficLeftFilter = lazy(() =>
    import(/* webpackChunkName: "left-filter" */ "@alife/traffic-left-filter")
  );
  const { snData, i18nText } = data;
  const handleChange = (link: string) => {
    window.location.href = link;
    window.globalUtils.logUtils.logModClick(
      {
        moduleName: "left-filter",
      },
      {
        action: "filter",
        type: link,
      }
    );
  };
  return (
    <SSRCompatibleSuspense
      fallback={
        <Icon type="loading" style={{ marginLeft: 40, marginTop: 50 }} />
      }
    >
      <TrafficLeftFilter
        data={snData}
        i18n={i18nText}
        handleChange={handleChange}
      />
    </SSRCompatibleSuspense>
  );
}

import(/* webpackChunkName: "left-filter" */ '@alife/traffic-left-filter')を通じて@alife/traffic-left-filterモジュールを非同期でロードします。注意すべき点は、非同期ロードされたモジュールをSSRCompatibleSuspenseコンポーネントでラップしていることです。これにより、ロードが完了するまでローディング表示をプレースホルダーとして表示し、ユーザーの待機による不安やページのちらつきを防ぐことができます。

export default function SSRCompatibleSuspense(
  props: Parameters<typeof Suspense>["0"]
) {
  const isMounted = useMounted();

  if (isMounted) {
    return <Suspense {...props} />;
  }
  return <>{props.fallback}</>;
}

同時に注意すべき点として、私たちのJSはすべてCDN上にあり、ドメイン名がページのメインドメイン名と一致しないことが一般的であるため、実行時に正しいチャンクアドレスをロードできるように設定を行う必要があります。

文件名:public-path
/**
 * 处理 webpack 代码切分的 path 路径
 */
const REG_SRC = /^(https?:)?\/\/(.+)\/msite\/(.*)cdn-traffic-pay-pc\/([\d.]+)\//;

function getSrcMatch() {
  if (document.currentScript) {
    return document.currentScript.src.match(REG_SRC);
  } else {
    const elScripts = document.querySelectorAll('script[src]');
    // render脚本一般在最后,这里从后往前找,以便提升查找效率
    for (let i = elScripts.length - 1; i >= 0; i--) {
      const matches = elScripts[i].src.match(REG_SRC);
      if (matches) {
        return matches;
      }
    }
  }
}

export function getPublicPath() {
  const matches = getSrcMatch();
  return `//${matches[2]}/msite/cdn-traffic-pay-pc/${matches[4]}/`;
}

try {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = getPublicPath();
} catch (err) {
  console.error(err);
}

Webpackのpublic_pathを手動で設定し、アプリケーションのメインエントリJSでインポートする必要があります。

import "../../public-path";

import React from "react";
import classnames from "classnames";

これにより、非同期チャンクは現在のページのドメインからではなく、設定されたwebpack_public_pathからロードされるようになります。

CDN設定#

プロジェクトのルートディレクトリには、CDNサーバーに現在のリソースのキャッシュ時間を伝えるための.assetsmetafileファイルが必要です。1年間キャッシュする設定の例は以下の通りです。

cache-control:max-age=31536000,s-maxage=31536000

備考:アーキテクチャチームの同僚との話し合いによると、これはプライベートな設定であり、いつか使えなくなる可能性が高いとのことです。

共通ライブラリの独立した導入#

前述のReact、ReactDOMのような、通常変更がなく、部門全体で同じ選択肢が使われる共通ライブラリについては、独立したCDNアドレスからの導入を推奨します。グループは常用ライブラリのCDNアドレスプラットフォーム https://work.def.alibaba-inc.com/lib を提供しており、トラフィックビジネスでは現在Reactの最新バージョンをs.alicdn.comに変換したリソースを使用しています。

  • コンボ後のリソース:
https://s.alicdn.com/@g/code/lib/??react/18.2.0/umd/react.production.min.js,react-dom/18.2.0/umd/react-dom.production.min.js

独立して参照する利点:リリース頻度が低く、キャッシュヒット率が高い。 既存のグループ提供CDNアドレスを採用する利点:利用するビジネス側が多くなり、CDNの各エッジノードでのキャッシュ確率も高くなります。

安定性#

開発プロセスでは、TypeScriptを使用してより強力な制約を提供し、ESLintやStylelintなどのLintツールを使用して開発段階でのコードの問題を減らしています。また、アプリケーションの分割により、上位ビジネスの複雑度が低くなり、日常のリリースリスクが大幅に低減されました。

同時に、リリースプロセスではグループの安全なリリース規範に従い、すべてのリリースは以下のプロセスを経る必要があります。

  • プリプロダクション環境でのテスト合格
  • コードレビュー:コアプロジェクトは2名以上の承認が必要
  • フィールズ自動テスト合格
  • 安全な本番環境での検証

すべて問題がなければリリースし、リリース後しばらくの間、監視指標 [流量业务的监控地址] が正常であるかを確認します。異常があれば速やかにロールバックします。

まとめ#

本記事では、開発からリリースまでの流れに沿って、私たちのビジネスにおける技術選定、規約、アーキテクチャ設計、開発・デバッグ方法、および安定性とパフォーマンスに関する取り組みを詳細に整理しました。一部には実際のコードも掲載し、より理解しやすくなるよう努めました。高度な技術や巧妙な実装は特にありませんが、これらはすべて業務を通じて培われた、私たちが最善と考えるプラクティスです。

本文中の方法、選択、およびソリューションについて、ご質問やご意見がございましたら、ぜひ議論にご参加ください。

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

フロントエンド開発の標準化 - ベストプラクティス - 開発からリリースまで
https://blog.kisnows.com/ja-JP/2023/09/15/frontend-development-standardization-best-practices/
作者
Kisnows
公開日
2023-09-15
ライセンス
CC BY-NC-ND 4.0