DIVX テックブログ

catch-img

AIを使いオープンソースのコードを読んでみた


目次[非表示]

  1. 1.はじめに
  2. 2.ソースコードを読む
    1. 2.1.題材
    2. 2.2.axiosとは
    3. 2.3.どこから読んでいけばいいか?
    4. 2.4.AIを使いコードを読む
      1. 2.4.1.lib/axios.js
      2. 2.4.2.lib/defaults/index.js
      3. 2.4.3.lib/core/axios.js
  3. 3.まとめ
  4. 4. DIVX GAIについて
  5. 5.お悩みご相談ください

はじめに

こんにちは。
使用しているライブラリの機能はどのような実装を行って実現しているか気になったことはあるでしょうか?
私は気になったことはあるもののライブラリのソースコードを読むことに抵抗がありました。
複雑で難しそうというイメージがあり、極力読むことを避け続けてきました。
しかし、生成AIが出現したことにより、ライブラリのソースコードを読む難易度も下がってきているのではと思い、思い腰を上げAIの力を借りてソースコードをこの度読んでみようと思います。

ソースコードを読む

題材

今回axiosというライブラリのソースコードを当社が開発を行ったDIVX GAI(以下GAIと記載します)を使い読んでみます。

axiosとは

axiosはブラウザからHTTPリクエストを送りデータの取得や作成を行うJavaScriptのライブラリです。
一応、GAIにも聞いてみようと思います。

GAIからは以下の内容が返ってきました。

  • HTTPリクエストの送信
  • Promiseを使用して非同期処理を行う
  • リクエストとレスポンスのインターセプト
  • JSONデータの変換
  • エラーハンドリング
  • リクエストのキャンセル
  • タイムアウトの設定
  • カスタムヘッダーの設定

詳しくかつ簡潔でわかりやすいように箇条書きで教えてくれました。

どこから読んでいけばいいか?

axiosのソースコードのディレクトリ構成としては以下のようになっています。

.
├── .github
├── bin
├── dist
├── examples
├── lib
├── sandbox
├── templates
└── test

このディレクトリの中からどのディレクトリから読んでいけばいいのかパッとはわからないため、GAIに確認します

2.コードの構成を把握するという箇所でlib/: Axiosの主要な実装が含まれていますと書いてあります。


さらにその先を見ていくと4.コードを追うという箇所でlib/ディレクトリ内のメインファイルaxios.jsや、リクエストやレスポンスの処理に関連するファイルを中心に読み進めます。 と書いてあるので、libディレクトリ内にあるファイルのコードを読み進めばよさそうです。

AIを使いコードを読む

書いている通りlibディレクトリの中を見ると以下のような構造になっています。

.
├── adapters
├── cancel
├── core
├── defaults
├── env
├── helpers
├── platform
├── axios.js
└── utils.js

GAIが言っているaxios.js から読んでみようと思います。

lib/axios.js

このファイルをざっと見ていくとcreateInstance関数が定義されていて、その後createInstanceを呼び出し、おそらくインスタンスを生成した後に各プロパティに値を代入しているんだろうなと読み取りました。
これが合っているかを聞いてみます。

概ね合っていそうです。
補足の説明もしてくれています。
次にリクエストやレスポンスの処理に関連するファイルを中心に読み進めます。 とGAIでは書いていましたが、axios.js を読んでいて他のファイルからインポートをしている値が何をしているのかわからないため、順番に見ていこうかと思います。
const axios = createInstance(defaults); の引数としてdefaults が設定されているのですが、このdefaults をファイル内で検索するとlib/defaults/index.js からインポートされているようなので、このファイルで何をしているかを見てみます。

lib/defaults/index.js

lib/defaults/index.js内でdefaults という変数が設定されているのでこの部分を読み進めていきます。
defaults はオブジェクトを代入しているようです。
最初にtransitionalというキーを定義してtransitionalDefaults という値を設定しています。
transitionalDefaults が変数だと仮定して、ファイル内を検索するとlib/defaults/transitional.jsからインポートしている値だとわかったので、lib/defaults/transitional.js を見ていきます。

このファイル内では以下のオブジェクトが定義されていますが、内容がさっぱりわからないためGAIに聞いてみます。

export default {
  silentJSONParsing: true,
  forcedJSONParsing: true,
  clarifyTimeoutError: false
};

silentJSONParsing はレスポンスがJSONでも自動的にパースする際にエラーをスローするかしないかを設定できるそうです。
forcedJSONParsing はレスポンスのContent-Typeがapplication/jsonでない場合でも、レスポンスデータをJSONとして強制的にパースするかしないかを設定できるそうです。
clarifyTimeoutError はタイムアウトエラーが発生した際に、どのリクエストがタイムアウトしたかの情報を追加するかしないかを設定できるそうです。
これらはaxiosのインスタンス生成時にデフォルトの設定を上書きすることができるそうです。


transitionalDefaults はわかったので、またlib/defaults/index.js に戻り続きのコードを見ていきます。
次にadapter というキーがあり、['xhr', 'http', 'fetch'] が配列としてあります。この時点ではどれかを使ってリクエストを飛ばすのかなと認識します。
transformRequest というキーは配列の中にtransformRequestという名前のメソッドが定義されている値を持っています。
このメソッドは引数のdata、headersに応じて、dataを様々な形式に変換しているように見えます。
詳しいことはGAIに聞いてみます。

聞いてみると以下の3つのことを行っていると返ってきました

  • dataがFormDataインスタンスである場合は FormDataはそのまま使用される
  • dataがオブジェクトである場合は通常はJSON.stringifyを使用してJSON形式に変換する
  • その他のデータ型の場合はデフォルトの処理や他の形式への変換が行われることがあります。

簡潔にわかりやすく教えてもらいました。
上記の3点でが理解できているか怪しい部分があるので確認しようと思います。
「dataがFormDataインスタンスである場合は FormDataはそのまま使用される」は以下のコードのことを言っているのかなと思ったので合っているか確認します。

if (isObjectPayload && utils.isHTMLForm(data)) {
    data = new FormData(data);
}

コードの部分に合致していることを確認できました。
次にtransformResponse というキーについて見ます。
この部分はそのままGAIに聞いてみようかなと思います。

処理の内容を聞いてみたところ主に以下の内容をやっていると回答が返ってきました。

  • レスポンスがJSON形式の場合にJSON.parseでjavascriptで操作できるオブジェクトに変換していること
  • JSONパースが失敗したらエラーハンドリングを行うこと
  • レスポンスがJSON形式以外の場合には別の処理を行う

最初の2点については該当部分はわかりましたが、レスポンスがJSON形式以外の場合には別の処理を行うという箇所については理解が合っているかどうか不安なため該当コードの部分で合っているかを聞いてみます。
該当コードは以下になります。

if (utils.isResponse(data) || utils.isReadableStream(data)) {
  return data;
}

確認したところ認識は合っていました。
ここで聞いたことで新たにutils.isResponse(data) の部分がAxiosのレスポンスオブジェクトかどうかをチェックしているということがわかりました。
なんとなくJSONやストリーム、文字列のデータ処理は記載してあってそれ以外はisResponse の条件に合致するのかなと思ったので、これはいい気づきになりました。
コードを追っていてもAxiosのレスポンスオブジェクトかどうかというのを理解するのに時間がかかるので、GAIが説明をしてくれて助かりました。

lib/core/axios.js

defaults の中身がわかったので再びlib/axiosに戻ります。
次にcreateInstance 関数の中身を見ていきます。
const context = new Axios(defaultConfig); と書かれている箇所があり、defaultsを引数にしてAxiosのインスタンスを生成しています。
インスタンスを生成していることから、class がどこかに定義をされていると思うのでインポートしているファイルを見るとimport Axios from './core/Axios.js'; とあるので、core/Axios.js のファイルの中身を見ようと思います。
このファイルを見るとAxiosがclassとして定義されています。
classなので当然constructorがあり、メソッドがいくつかあります。
まずconstructor から見ていくと、defaults とinterceptors がプロパティとして存在していて、defaults は引数で渡ってきたinstanceConfig を初期値としてセットします。これはlib/defaults/index.js で定義されているdefaults 変数をセットしています。
次にinterceptors プロパティですが、オブジェクトが初期値としてセットされrequest とresponse の2つがオブジェクトのプロパティとしてセットされています。ともにInterceptorManager のインスタンスを初期値としてセットしています。
次にasync request の部分を見ていきます。コードを見ると_request メソッドの処理を呼んで成功したら_request メソッドの返り値の値を取得して、失敗したらエラー処理を行うんだろうなと思うのですがGAIに聞いてみます。

以下の内容が返ってきました。

  • AxiosのインスタンスでHTTPリクエストを行うためのメソッド
  • 内部メソッド_requestを呼び出して、実際にリクエストを送信。Promiseを返すため、awaitを使って非同期的に処理を待つことができる。
  • エラーが発生した場合、if (err instanceof Error)でエラーオブジェクトのインスタンスであるかをチェック。Errorインスタンスでない場合にも、後でエラーをスローするための処理が行われる
  • エラーのスタックトレースをカスタマイズして、より分かりやすい形で提供。

AxiosがHTTPリクエストを非同期に送信する際に、エラーハンドリングとスタックトレースの整形を行うためのメソッドみたいです。
実際に送る処理は_request メソッドのようなので中身を見ていきます。
このメソッドの中身を見ていくと、以下の処理を行っているのかなと思いました。

  • 途中に設定されるconfigのプロパティらしきものに処理に必要な値を入れていく
  • 割り込みの処理があったら先に割り込み処理を行う
  • 割り込み処理後にリクエストを飛ばすような処理を行っている

GAIにどのような処理を行っているかを聞いてみます。

以下の処理を行っていると回答が返ってきました。

  • 引数として渡されたリクエストの設定(configオブジェクト)を適切な形に整形する。これには、URL、HTTPメソッド、ヘッダー、リクエストボディ、タイムアウト設定などが含まれる
  • リクエストを送信する前に、事前に設定されたインターセプターを適用してリクエストを変更することができる。これによって、認証トークンの追加や、リクエストヘッダーの変更、データの変換などが行われることがある。
  • 設定されたリクエスト情報をもとに、実際にHTTPリクエストを送信する。この処理はXMLHttpRequest、fetchやNode.jsのHTTPモジュールを使用して行われる。
  • レスポンスを受け取った後にも、設定されたインターセプターが適用され、必要に応じてレスポンスデータの処理が行われる

レスポンスを受け取った後もインターセプター(割り込み処理)の部分の処理が行われるんですね
おおざっぱにはわかりましたが、axiosの処理の肝はこのメソッドだと思うのでもう少し深ぼっていきたいと思います。
最初のコードの部分ですが、configOrUrlが文字列だったらconfigのurlプロパティに値を代入して、文字列ではなかったらconfigOrUrlをconfigとして定義しています。

if (typeof configOrUrl === 'string') {
    config = config || {};
    config.url = configOrUrl;
  } else {
    config = configOrUrl || {};
  }

その後の

config = mergeConfig(this.defaults, config);

の部分はmergeConfig とあるのでthis.defaults の内容とconfig を合体させているように見えます。
mergeConfig関数はlib/core/mergeConfig.js に定義されています。
ここの処理をGAIに聞いてみます。

やはり、マージ(合体)を行っているようです。ここでユーザーが呼び出し時にセットした値があれば上書きされるようです。
この関数内にあるmergeMapという変数の値が、READMEに記載されているRequest Configに該当しているようです。
lib/core/Axios.jsにまた戻って、次のコードを見ます。

const {transitional, paramsSerializer, headers} = config;

の部分ではマージしたconfigオブジェクトからいくつか分割代入しているようです。

if (transitional !== undefined) {
    validator.assertOptions(transitional, {
      silentJSONParsing: validators.transitional(validators.boolean),
      forcedJSONParsing: validators.transitional(validators.boolean),
      clarifyTimeoutError: validators.transitional(validators.boolean)
    }, false);
  }

ここでアサーションを行っているようですが、詳しいことはGAIに聞きます。

以下の内容が返ってきました

  • validator.assertOptionsは指定されたオプション(この場合はtransitional)が正しい形式であるかどうかを確認する関数。この関数の実行によってtransitionalに含まれる各オプションが設定された検証ルールに従って適切かどうかがチェックされます。
  • validator.assertOptionsに渡される第二引数は、transitionalオプションで使用される各プロパティの検証ルールを定義。各プロパティは、validators.transitional(validators.boolean)によって、真偽値(boolean)の形で設定されていることが検証される。

真偽値のアサーションを行い、このアサーションでエラーが起こっても後続の処理へと続くことがわかりました。

if (paramsSerializer != null) {
    if (utils.isFunction(paramsSerializer)) {
      config.paramsSerializer = {
        serialize: paramsSerializer
      }
    } else {
      validator.assertOptions(paramsSerializer, {
        encode: validators.function,
        serialize: validators.function
      }, true);
    }
  }

ここの処理はparamsSerializerが存在していて、関数であればconfigオブジェクトのparamsSerializerプロパティにserialize をキーとしてparamsSerializer を値としてオブジェクトを格納しています。
もし関数でなければparamsSerializer の各プロパティのアサーションを行っているようです。

validator.assertOptions(config, {
    baseUrl: validators.spelling('baseURL'),
    withXsrfToken: validators.spelling('withXSRFToken')
  }, true);

この部分はbaseUrlとwithXsrfToken のプロパティのスペルチェックをやっているのかなと思うのですが、詳しいことは聞いてみます。

  • baseUrl と withXsrfToken プロパティについて、正しいスペルが使われているかどうかを検証している。
  • validators.spellingを使用して、正しいスペルの提案を行っていて、一部の設定項目は、特定のスペル(大文字と小文字の区別を含む)を持つ必要があるため、この検証は誤字や打ち間違いを防ぐため。

プロパティのスペルミスを防ぐためにチェックを行っていることがわかりました。

let contextHeaders = headers && utils.merge(
    headers.common,
    headers[config.method]
);

上記のコードを見てみるとheaders.commonとheaders[config.method] をマージしているようですが、それぞれどのような値をマージしているかを確認してみます。

headers.common に関しては以下の内容が入るそうです。

  • Axiosの全てのリクエストに共通して適用されるヘッダーを格納するオブジェクト。これには、一般的なHTTPヘッダー(Content-TypeやAccept等)が定義されている

headers[config.method] に関しては以下の内容が入るそうです。

  • 特定のHTTPメソッドに関連するヘッダーを格納するオブジェクト。例えば、Content-Length やCustom-Header等

これらをマージした値をcontextHeaders 変数に格納しています。

headers && utils.forEach(
  ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
  (method) => {
    delete headers[method];
  }
);

この部分は設定したHTTPメソッドに該当するものを削除しているようですが、何のためにやっているかがわからないため聞いてみます。

目的としては以下の内容のためにこの処理があるそうです。

  • Axiosのインスタンスやリクエストを初期化する際に、以前のリクエストで設定されたヘッダーが残っていると不要な影響を与える可能性があり、これを防ぐために、特定のヘッダーを削除している。
  • 各リクエストに対して新しいヘッダーセットを適用するため、古いヘッダーをクリアしてリクエストの状態を整合的に保つ。

不要な影響がピンとこなかったのでここを深ぼります。

不要な影響についての例を挙げてもらいました。
認証情報の流出、データの不一致、キャッシュ制御の誤り、APIエンドポイントの誤用、パフォーマンスの問題があるそうです。
ここでaxiosのインスタンスは一回だけ作成して、axiosの処理を実行するたびに同一インスタンスを利用して、都度各プロパティの内容をリセットしたり再度代入しているのかなと思ったので聞いてみます。

認識合っていました!
特にリクエストに影響を与えないためにもヘッダーのリセット(認証情報のリセット)が重要みたいです。
コードを読んでいるときには気にもとめていませんでしたが、ソースコードを読んだことでこのことに気づけました。
以下のコードはインターセプトを行っているようですが詳しいことはコードを見てもイメージできないため聞いてみます。

const requestInterceptorChain = [];
let synchronousRequestInterceptors = true;
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
  if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
    return;
  }

  synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;

  requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});

const responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
  responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});

  • const requestInterceptorChain = []
    •  リクエストインターセプタを格納するためのもので、後で実際にリクエストを送信する前に、これを実行する
  • let synchronousRequestInterceptors = true
    • インターセプタが同期的に実行されるかどうかを示していて、すべてのインターセプタが同期であればtrueのままですが、非同期のものがあればfalseになる。
  • this.interceptors.request.forEach(...)
    • Axiosのインスタンスに設定されたリクエストインターセプタを順に処理する。
  • synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous
    • 各インターセプタが同期である場合、synchronousRequestInterceptorsの値が変更される。
  • requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected)
    • 各インターセプタの成功時のコールバック(fulfilled)と失敗時のコールバック(rejected)をrequestInterceptorChainの先頭に追加する。これによりリクエストが行われる前に設定されたインターセプタが逆順に実行される。
  • const responseInterceptorChain = []
    • レスポンスに対するインターセプタを格納する配列。
  • this.interceptors.response.forEach(...)
    • 入力されたレスポンスインターセプタを順に処理する
  • responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected)
    • 各レスポンスインターセプタの成功時のコールバックと失敗時のコールバックを、responseInterceptorChainに追加する。この場合インターセプタはリクエストの流れとは逆向きに 実行される。

unshift の部分でリクエストやレスポンスの成功時と失敗時の処理を配列に格納しているようです。

let promise;
let i = 0;
let len;

if (!synchronousRequestInterceptors) {
  const chain = [dispatchRequest.bind(this), undefined];
  chain.unshift.apply(chain, requestInterceptorChain);
  chain.push.apply(chain, responseInterceptorChain);
  len = chain.length;

  promise = Promise.resolve(config);

  while (i < len) {
    promise = promise.then(chain[i++], chain[i++]);
  }

  return promise;
}

chain に格納されている値(先ほどのリクエストとレスポンスのインターセプト等)を順番に非同期処理を行っているように読み取れます。dispatchRequest.bind(this) が何を行うのかわからないため、ここ含めて全体の処理で実行している内容を聞いてみます。

処理としては以下のことを行っているそうです。

  • Promiseによる非同期処理の管理
  • インターセプタのチェーン(配列にデータを格納)を構成
  • 構成したチェーンを通じてリクエストを処理

この部分で気になっていたdispatchRequest.bind(this) ですが、リクエストを行っている関数のようです。
どこでリクエストを行っているかを知りたかったので、この部分でやっていることが知れてよかったです。
dispatchRequest について定義している箇所があるのでさらに見ていきます
dispatchRequestはlib/core/dispatchRequest.js に定義されています。
このファイルにあるコードがどのような処理をしているかを聞いてみます。

このファイルは以下の処理を行っているようです。

  • ヘッダーの設定
  • リクエストデータの変換
  • Content-Typeの設定
  • アダプタの取得
  • リクエストの実行
  • レスポンスの処理

大枠はわかりましたので、コードを見てadapter(config) のところで実際にリクエスト処理を行っていそうだなと思ったので認識が合っているか聞いてみます。

合っているようでした。
adaptersはadapters.getAdapter(config.adapter || defaults.adapter)の実行結果を変数として持ちます。
getAdapterの定義元を見るとlib/adapters/adapters.jsで関数が定義されています
この処理の内容を聞くと以下のような内容が返ってきました。

配列としてアダプタを処理して、既知のアダプタと照合しつつ適切なものが見つからなかった場合のエラーハンドリングを行うようです。
アダプタは何のことかというとlib/adapters/adapters.js で定義されている内容になります

const knownAdapters = {
  http: httpAdapter,
  xhr: xhrAdapter,
  fetch: fetchAdapter
}

httpAdapterはNode.js用、xhrAdapterはブラウザのXMLHttpRequest用、fetchAdapterはfetch API用で使用されるものです。
httpAdapter 、xhrAdapter 、fetchAdapter はそれぞれ別ファイル(httpAdapterはlib/adapters/http.js、xhrAdapterはlib/adapters/xhr.js、fetchAdapterはlib/adapters/fetch.js)で定義されていて、これらのファイル内でhttpリクエストを飛ばす処理がそれぞれ記載されています。
httpAdapter ですとdata.pipe(req) 、xhrAdapter ですとrequest.send(requestData || null) 、fetchAdapterですとawait fetch(request) になります。

len = requestInterceptorChain.length;

let newConfig = config;

i = 0;

while (i < len) {
  const onFulfilled = requestInterceptorChain[i++];
  const onRejected = requestInterceptorChain[i++];
  try {
    newConfig = onFulfilled(newConfig);
  } catch (error) {
    onRejected.call(this, error);
    break;
  }
}

try {
  promise = dispatchRequest.call(this, newConfig);
} catch (error) {
  return Promise.reject(error);
}

i = 0;
len = responseInterceptorChain.length;

while (i < len) {
  promise = promise.then(responseInterceptorChain[i++], responseInterceptorChain[i++]);
}

return promise;

この処理は先程の処理と似ているようですが、違いについて聞いてみようと思います

!synchronousRequestInterceptors の条件に合致しているときの処理はインターセプトの処理を非同期で行っていて、!synchronousRequestInterceptors の条件に合致しないときはインターセプトの処理を同期で行うようです。
lib/core/Axios.js の処理は一通り見れたので、また、lib/axios.js に戻りcreateInstance 関数の残りの部分を見ていこうと思います

instance.create = function create(instanceConfig) {
  return createInstance(mergeConfig(defaultConfig, instanceConfig));
};

この部分はcreate関数を作成して、 設定したconfig情報を格納したaxiosインスタンスを作成しています。
よくこの形でaxiosのインスタンス作成をしているのを見ているので、関数の内容はこの内容になっているのかという勉強になりました。

まとめ

理解するのが難しいコードでもAIを使えば説明をしてもらえるので、理解のスピードが早まるなと実感ができました。
いままで抵抗があったライブラリのソースコードをAIの力を借りればすごい読みやすかったですし、ライブラリを作成した方々に対して感謝の気持ちも出てきました。
また、自分の理解や認識が合っているかを壁打ちできるので、今回極力意識してやったことですが、まずは自分の中で仮説だったり、考えをまとめて問いかけるとただ情報を受取るのではなく、会話みたいな形でやりとりができるので、より理解がしやすくなると思いました。
学習をするときやアイデア出し等AIに聞きながら進めると効率よく作業ができるので、これからもよきパートナーとしてAIとともに併走していきます。

 DIVX GAIについて

今回、使用したDIVX GAIの機能は以下のリンクからご確認いただけますので、もしよろしければご覧ください!

  DIVX GAI | 株式会社DIVX(ディブエックス) DIVX GAIは、法人向けのChatGPTのサービスです。 ChatGPTは、法人でも利用できます。しかし、そのままでは入力内容がAIの学習データとして再利用され、思わぬところで情報が流出するリスクがあります。 DIVX GAIは、AIの学習に使用されないセキュアな環境でChatGPTを利用でき、業務の生産性を向上させるのに役立ちます。 株式会社divx(ディブエックス)

お悩みご相談ください

弊社はAI技術を活用したソフトウェア開発およびソリューション提供を行っています。

DXの推進等の課題や悩みがございましたらぜひお気軽にご相談ください!

  ご相談フォーム | 株式会社divx(ディブエックス) DIVXのご相談フォームページです。 株式会社divx(ディブエックス)




お気軽にご相談ください


ご不明な点はお気軽に
お問い合わせください

サービス資料や
お役立ち資料はこちら

DIVXブログ

テックブログ タグ一覧

人気記事ランキング

関連記事

GoTopイメージ