JavaScript

modern JavaScript開発の流れ|modules・npm・build toolの役割

modern JavaScript開発を、ES modules、npm、dependency、build tool、transpile、polyfill、lint・test・buildの順に整理します。

この記事の目次
  1. modern JavaScript開発では何が変わったのか
  2. ES modulesでfileを分ける方法
  3. npmとpackage.jsonは何を管理するのか
  4. dependencyとdevDependencyはどう分けるか
  5. build toolはいつ必要か
  6. transpileとpolyfillはどこに入るか
  7. developmentとproductionは何が違うか
  8. lint・test・buildはどの順で実行するか
  9. 最小projectを始める手順
  10. tool選びで迷ったときの判断表

modern JavaScript開発は、ES modulesでcodeを分け、package managerでdependencyとscriptを管理し、必要な場合だけbuild tool・transpile・polyfillを追加する流れです。最初からtoolを全部入れず、browserで直接動く最小構成から要件に応じて増やします。

この記事は開発工程の地図に役割を限定し、個別commandや設定を深掘りしません。npmの操作はnpm command line、bundlerはParcel、互換性変換はBabelとpolyfillを参照してください。

modern JavaScript開発では何が変わったのか

一つのglobal scriptにすべてを書く構成から、標準module、package、automatic test、build pipelineを使う構成へ広がりました。ただし「modern」は特定frameworkやbundlerの名前ではありません。かつてglobal汚染を避けるために使われた即時関数(IIFE)の役割は、module scopeが標準で担うようになりました。

課題 使う仕組み 必須か
codeの責務分割 ES modules 複数fileなら基本
外部package管理 npmなどのpackage manager dependencyがある場合
asset変換・bundle Parcel、Viteなど 要件がある場合
syntax互換 Babelなど targetが未対応の場合
runtime API互換 polyfill targetにAPIがない場合
問題pattern検出 linter、type checker 規模とriskに応じる
回帰確認 test、build、E2E 重要機能ほど必要
スポンサーリンク

ES modulesでfileを分ける方法

JavaScriptのmoduleとは、exportで公開した値だけを他のfileからimportできる、独立したscopeを持つfileです。browser標準のES modulesでは、外に公開する値をexportし、利用側でimportします。依存関係がsourceに明示され、global変数の衝突を減らせます。

importの書き方は3つ覚えれば十分です。名前付きexportはimport { name } from "./file.js"、default exportはimport anyName from "./file.js"、fileの全exportをまとめて受け取るならimport * as ns from "./file.js"です。pathは./または/で始め、browserでは拡張子.jsを省略できません。

// price.js
export function calculateTotal(items) {
  return items.reduce(
    (total, item) => total + item.price * item.quantity,
    0
  );
}
// main.js
import { calculateTotal } from "./price.js";

const total = calculateTotal([
  { price: 1200, quantity: 2 }
]);

console.log(total);
<script type="module" src="/js/main.js"></script>

browserでrelative importを使う場合はfile extensionと正しいURLが必要です。local fileを直接開くのではなく、HTTP serverで配信してCORSやmodule解決を実際の環境に近づけます。type="module"のscriptは自動的にdefer相当で実行され、常にstrict modeになります。読み込みタイミングの違いはasync・deferの基礎知識を参照してください。

npmとpackage.jsonは何を管理するのか

npmはregistry、website、CLIからなるecosystemです。projectではpackage.jsonへdependency、development tool、script、package metadataを記録します。

{
  "name": "task-viewer",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "parcel src/index.html",
    "build": "parcel build src/index.html",
    "test": "node --test"
  },
  "devDependencies": {
    "parcel": "2.15.4"
  }
}

versionは例です。実際にはinstall時にproject policyへ合わせて決めます。application自体がpackage公開目的でない場合はprivate: trueで誤publishを防げます。

dependencyとdevDependencyはどう分けるか

production runtimeでapplication codeが必要とするpackageはdependencies、build、test、lintなど開発工程だけで使うtoolはdevDependenciesが基本です。ただしdeploy方式によってはserver上でbuildするためdevelopment toolもinstallが必要です。

  • UI libraryやruntimeでimportするutility: dependencies候補。
  • bundler、linter、test runner: devDependencies候補。
  • CLIをglobal installせず、project versionをscriptから実行する。
  • lockfileを共有し、CIでは再現可能なinstall commandを使う。

分類名だけで決めず、「production artifactへ含まれるか」「deploy先でbuildするか」「server実行時にrequireされるか」を確認します。

build toolはいつ必要か

browserが直接読めないsource、package解決、asset最適化、development server、code splittingなどが必要になったときにbuild toolを追加します。小さなsiteでnative modulesだけなら不要な場合もあります。

  1. entry HTMLまたはentry moduleを決める。
  2. toolがimport graphを辿る。
  3. 必要なtransformとasset処理を行う。
  4. production用fileをoutput directoryへ生成する。
  5. 生成物をHTTP serverから配信する。

build outputはsourceの正本ではありません。生成方法、Node.js version、package manager version、commandをREADMEとconfigへ記録します。

transpileとpolyfillはどこに入るか

transpileはbuild工程でsource syntaxを変換し、polyfillはtarget runtimeにない機能を補います。bundlerがBabelなどを内部利用する場合もありますが、役割は同じではありません。

問題のlayer 対処候補
optional chainingをparseできない syntax target向けtransform
Promiseが存在しない runtime API polyfillまたはtarget見直し
bare importをbrowserが解決できない module resolution bundlerまたはimport map
CSS featureが未対応 CSS compatibility 別のCSS tool/fallback

support対象を決めずに互換toolを追加すると、過剰な変換と大きなbundleにつながります。analytics、利用環境、support policyからtargetを先に決めます。

developmentとproductionは何が違うか

development modeは高速な再build、source map、詳細errorを重視します。production buildはminify、dead code除去、asset hashなど配信向け最適化を行う場合があります。

  • development serverのURLをproductionへ使わない。
  • source mapの公開範囲と機密情報を確認する。
  • environment変数がclient bundleへ埋め込まれる条件を確認する。
  • API keyやpasswordをfrontend sourceへ置かない。
  • production artifactを実際のbase pathでpreviewする。

名前がprocess.envでも、build toolが文字列としてclient codeへ埋め込めば利用者から見えます。「環境変数だから秘密」という判断はできません。

lint・test・buildはどの順で実行するか

変更の早い段階で安価な検査を行い、最後にproduction artifactを確認します。projectによって順番は調整しますが、少なくともbuild成功だけで完了としません。

  1. formatterで表記を統一する。
  2. linterとtype checkで静的な問題を検出する。
  3. unit testでlogicを確認する。
  4. production buildを作る。
  5. previewで主要操作とnetwork requestを確認する。
  6. 必要ならE2Eとperformanceを確認する。

CIでは同じcommandをcleanな環境で実行します。localだけにあるglobal packageや未追跡fileへ依存しないことを確かめます。

最小projectを始める手順

最初はnative modulesで動作を作り、必要になったtoolだけを導入します。

  1. HTML、CSS、main.jsを作り、HTTP serverで表示する。
  2. 責務が分かれたらES modulesへ分割する。
  3. 外部packageまたはrepeatable scriptが必要ならpackage.jsonを作る。
  4. source変換やasset処理が必要ならbuild toolを選ぶ。
  5. support対象に不足がある場合だけtranspile/polyfillを追加する。
  6. lint、test、buildをscriptへ固定する。
  7. READMEへruntime versionと実行commandを記録する。

frameworkを使う場合も、official starterが作るfileとscriptの役割を確認します。生成された設定を「消すと怖いfile」のままにせず、entry、build、output、environmentの流れを説明できる状態にします。

tool選びで迷ったときの判断表

困りごと 最初の選択 追加手段
fileを分けたい ES modules import map/bundler
packageを使いたい package manager bundler
古いtargetで構文error target確認 Babel等のtransform
runtime APIがない compatibility確認 polyfill/別実装
code品質を揃えたい formatter + linter type checker/test
設定が複雑すぎる 不要toolを外す official starterへ戻す

modernな開発とはtoolの数ではなく、dependencyと変換工程を再現できることです。まずbrowser標準で成立する範囲を知り、解決したい問題が明確になったときだけtoolを追加してください。

スポンサーリンク