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だけなら不要な場合もあります。
- entry HTMLまたはentry moduleを決める。
- toolがimport graphを辿る。
- 必要なtransformとasset処理を行う。
- production用fileをoutput directoryへ生成する。
- 生成物を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成功だけで完了としません。
- formatterで表記を統一する。
- linterとtype checkで静的な問題を検出する。
- unit testでlogicを確認する。
- production buildを作る。
- previewで主要操作とnetwork requestを確認する。
- 必要ならE2Eとperformanceを確認する。
CIでは同じcommandをcleanな環境で実行します。localだけにあるglobal packageや未追跡fileへ依存しないことを確かめます。
最小projectを始める手順
最初はnative modulesで動作を作り、必要になったtoolだけを導入します。
- HTML、CSS、
main.jsを作り、HTTP serverで表示する。 - 責務が分かれたらES modulesへ分割する。
- 外部packageまたはrepeatable scriptが必要ならpackage.jsonを作る。
- source変換やasset処理が必要ならbuild toolを選ぶ。
- support対象に不足がある場合だけtranspile/polyfillを追加する。
- lint、test、buildをscriptへ固定する。
- 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を追加してください。