WebpackまたはBrowserify&Gulp:どちらが良いですか?
公開: 2022-03-11Webアプリケーションがますます複雑になるにつれて、Webアプリをスケーラブルにすることが最も重要になります。 以前はアドホックなJavaScriptとjQueryを作成するだけで十分でしたが、現在、Webアプリを構築するには、次のような、はるかに高度な規律と正式なソフトウェア開発手法が必要です。
- コードへの変更が既存の機能を壊さないことを確認するための単体テスト
- エラーのない一貫したコーディングスタイルを保証するためのリンティング
- 開発ビルドとは異なる本番ビルド
Webは、独自の開発課題のいくつかも提供します。 たとえば、ウェブページは多くの非同期リクエストを行うため、ウェブアプリのパフォーマンスは、それぞれ独自の小さなオーバーヘッド(ヘッダー、ハンドシェイクなど)を持つ数百のJSファイルとCSSファイルをリクエストする必要があるために大幅に低下する可能性があります。 この特定の問題は、多くの場合、ファイルをバンドルすることで解決できるため、数百の個別のファイルではなく、単一のバンドルされたJSファイルとCSSファイルのみを要求します。
ES5の互換性を維持しながらES6コードの恩恵を受けるために、ネイティブJSやCSSにコンパイルされるSASSやJSXなどの言語プリプロセッサやBabelなどのJSトランスパイラーを使用することもよくあります。
これは、Webアプリ自体のロジックの記述とは関係のないかなりの数のタスクに相当します。 ここでタスクランナーが登場します。タスクランナーの目的は、これらすべてのタスクを自動化して、アプリの作成に集中しながら、強化された開発環境から利益を得ることができるようにすることです。 タスクランナーを構成したら、ターミナルで1つのコマンドを呼び出すだけです。
Gulpは開発者にとって非常に使いやすく、習得が容易で、理解しやすいため、タスクランナーとして使用します。
Gulpの簡単な紹介
GulpのAPIは、次の4つの関数で構成されています。
-
gulp.src
-
gulp.dest
-
gulp.task
-
gulp.watch
たとえば、次の4つの機能のうち3つを使用するサンプルタスクを次に示します。
gulp.task('my-first-task', function() { gulp.src('/public/js/**/*.js') .pipe(concat()) .pipe(minify()) .pipe(gulp.dest('build')) });
my-first-task
を実行すると、globパターン/public/js/**/*.js
に一致するすべてのファイルが縮小され、 build
フォルダーに転送されます。
これの美しさは、 .pipe()
チェーンにあります。 一連の入力ファイルを取得し、それらを一連の変換にパイプしてから、出力ファイルを返します。 さらに便利にするために、 minify()
などの実際のパイピング変換は、多くの場合NPMライブラリによって実行されます。 その結果、パイプ内のファイルの名前を変更する以外に、独自の変換を作成する必要があることは実際には非常にまれです。
Gulpを理解するための次のステップは、タスクの依存関係の配列を理解することです。
gulp.task('my-second-task', ['lint', 'bundle'], function() { ... });
ここで、 my-second-task
は、 lint
タスクとbundle
タスクが完了した後にのみコールバック関数を実行します。 これにより、関心の分離が可能になりますLESS
からCSS
への変換など、単一の責任を持つ一連の小さなタスクを作成し、タスクの依存関係の配列を介して他のすべてのタスクを呼び出すだけの一種のマスタータスクを作成します。
最後に、 gulp.watch
があります。これは、globファイルパターンの変更を監視し、変更が検出されると、一連のタスクを実行します。
gulp.task('my-third-task', function() { gulp.watch('/public/js/**/*.js', ['lint', 'reload']) })
上記の例では、 /public/js/**/*.js
に一致するファイルに変更を加えると、 lint
およびreload
タスクがトリガーされます。 gulp.watch
の一般的な使用法は、ブラウザーでライブリロードをトリガーすることです。これは、開発に非常に役立つ機能であるため、一度体験すると、それなしでは生きていけません。
そして、ちょうどそのように、あなたはあなたがgulp
について本当に知る必要があるすべてを理解します。
Webpackはどこに収まりますか?
CommonJSパターンを使用する場合、JavaScriptファイルのバンドルはそれらを連結するほど簡単ではありません。 むしろ、ファイルの先頭に一連のrequire
またはimport
ステートメントを持つエントリポイント(通常はindex.js
またはapp.js
と呼ばれます)があります。
ES5
var Component1 = require('./components/Component1'); var Component2 = require('./components/Component2');
ES6
import Component1 from './components/Component1'; import Component2 from './components/Component2';
app.js
の残りのコードの前に依存関係を解決する必要があり、それらの依存関係自体にさらに解決すべき依存関係がある場合があります。 さらに、アプリケーションの複数の場所で同じ依存関係がrequire
になる場合がありますが、その依存関係を解決したいのは1回だけです。 ご想像のとおり、依存関係ツリーを数レベル深くすると、JavaScriptをバンドルするプロセスはかなり複雑になります。 ここで、BrowserifyやWebpackなどのバンドラーが登場します。
開発者がGulpの代わりにWebpackを使用しているのはなぜですか?
Webpackはバンドラーですが、Gulpはタスクランナーであるため、これら2つのツールが一般的に一緒に使用されることを期待できます。 代わりに、特にReactコミュニティでは、Gulpの代わりにWebpackを使用する傾向が高まっています。 どうしてこれなの?
簡単に言えば、Webpackは非常に強力なツールであるため、タスクランナーを介して実行するタスクの大部分をすでに実行できます。 たとえば、Webpackには、バンドルのミニファイとソースマップのオプションがすでに用意されています。 さらに、Webpackは、ライブリロードとホットリロードの両方をサポートするwebpack-dev-server
と呼ばれるカスタムサーバーを介してミドルウェアとして実行できます(これらの機能については後で説明します)。 ローダーを使用することで、ES6からES5へのトランスパイル、およびCSSプリプロセッサーとポストプロセッサーを追加することもできます。 それは本当に、Webpackが独立して処理できない主要なタスクとしてユニットテストとリンティングを残すだけです。 少なくとも半ダースの潜在的なgulpタスクを2つに削減したことを考えると、多くの開発者は代わりにNPMスクリプトを直接使用することを選択します。これにより、プロジェクトにGulpを追加するオーバーヘッドが回避されます(これについては後で説明します)。 。
Webpackを使用することの主な欠点は、構成がかなり難しいことです。これは、プロジェクトをすばやく立ち上げて実行しようとしている場合は魅力的ではありません。
3つのタスクランナーのセットアップ
3つの異なるタスクランナー設定でプロジェクトを設定します。 各セットアップは、次のタスクを実行します。
- 監視対象のファイル変更をライブでリロードする開発サーバーをセットアップする
- 監視対象のファイル変更に対してスケーラブルな方法でJSおよびCSSファイル(ES6からES5への変換、SASSからCSSへの変換およびソースマップを含む)をバンドルします
- スタンドアロンタスクとして、または監視モードで単体テストを実行します
- lintingをスタンドアロンタスクとして、またはウォッチモードで実行します
- ターミナルで1つのコマンドを使用して、上記のすべてを実行する機能を提供します
- 縮小化やその他の最適化を使用して本番バンドルを作成するための別のコマンドがあります
3つのセットアップは次のようになります。
- Gulp + Browserify
- Gulp + Webpack
- Webpack+NPMスクリプト
アプリケーションはフロントエンドにReactを使用します。 もともとはフレームワークにとらわれないアプローチを使用したかったのですが、Reactを使用するとHTMLファイルが1つだけ必要であり、ReactはCommonJSパターンで非常にうまく機能するため、実際にはタスクランナーの責任が簡素化されます。
各セットアップの長所と短所について説明し、プロジェクトのニーズに最適なセットアップのタイプについて情報に基づいた決定を下せるようにします。
アプローチごとに1つずつ、合計3つのブランチを持つGitリポジトリをセットアップしました(リンク)。 各セットアップのテストは次のように簡単です。
git checkout <branch name> npm prune (optional) npm install gulp (or npm start, depending on the setup)
各ブランチのコードを詳しく調べてみましょう…
共通コード
フォルダ構造
- app - components - fonts - styles - index.html - index.js - index.test.js - routes.js
index.html
単純なHTMLファイル。 Reactアプリケーションは<div></div>
にロードされ、バンドルされた単一のJSおよびCSSファイルのみを使用します。 実際、Webpack開発のセットアップでは、 bundle.css
も必要ありません。
index.js
これは、アプリのJSエントリポイントとして機能します。 基本的には、前述のID app
を使用してReactRouterをdiv
にロードしているだけです。
ルート.js
このファイルはルートを定義します。 URL /
、 /about
、および/contact
は、それぞれHomePage
、 AboutPage
、およびContactPage
コンポーネントにマップされます。
index.test.js
これは、ネイティブJavaScriptの動作をテストする一連の単体テストです。 実際の本番品質のアプリでは、Reactコンポーネント(少なくとも状態を操作するコンポーネント)ごとに単体テストを作成し、React固有の動作をテストします。 ただし、この投稿では、ウォッチモードで実行できる機能ユニットテストを実行するだけで十分です。
components / App.js
これは、すべてのページビューのコンテナと考えることができます。 各ページには、 <Header/>
コンポーネントと、ページビュー自体に評価されるthis.props.children
が含まれています(ブラウザの/contact
にある場合はex/ ContactPage
)。
components / home / HomePage.js
これが私たちのホームビューです。 ブートストラップのグリッドシステムはレスポンシブページの作成に優れているため、 react-bootstrap
を使用することを選択しました。 ブートストラップを適切に使用すると、小さいビューポート用に作成する必要のあるメディアクエリの数が大幅に削減されます。
残りのコンポーネント( Header
、 AboutPage
、 ContactPage
)も同様に構造化されています( react-bootstrap
マークアップ、状態操作なし)。
それでは、スタイリングについて詳しく説明しましょう。
CSSスタイリングアプローチ
Reactコンポーネントのスタイルを設定するための私の好ましいアプローチは、コンポーネントごとに1つのスタイルシートを用意することです。そのスタイルは、その特定のコンポーネントにのみ適用されるようにスコープされています。 私のReactコンポーネントのそれぞれで、トップレベルのdiv
にコンポーネントの名前と一致するクラス名があることに気付くでしょう。 したがって、たとえば、 HomePage.js
のマークアップは次のようにラップされています。
<div className="HomePage"> ... </div>
次のように構成された関連するHomePage.scss
ファイルもあります。
@import '../../styles/variables'; .HomePage { // Content here }
なぜこのアプローチはとても便利なのですか? その結果、高度にモジュール化されたCSSが実現し、不要なカスケード動作の問題が大幅に解消されます。
Component1
とComponent2
の2つのReactコンポーネントがあるとします。 どちらの場合も、 h2
フォントサイズを上書きする必要があります。
/* Component1.scss */ .Component1 { h2 { font-size: 30px; } } /* Component2.scss */ .Component2 { h2 { font-size: 60px; } }
Component1
とComponent2
のh2
フォントサイズは、コンポーネントが隣接しているか、一方のコンポーネントがもう一方のコンポーネント内にネストされているかに関係なく独立しています。 理想的には、これはコンポーネントのスタイリングが完全に自己完結型であることを意味します。つまり、コンポーネントがマークアップのどこに配置されていても、コンポーネントはまったく同じように見えます。 実際には、それは必ずしもそれほど単純ではありませんが、それは確かに正しい方向への大きな一歩です。
コンポーネントごとのスタイルに加えて、グローバルスタイルシートglobal.scss
と、特定の責任を処理するSASSパーシャル(この場合、フォントと変数のそれぞれ_fonts.scss
と_variables.scss
)を含むstyles
フォルダーが必要です。 )。 グローバルスタイルシートを使用すると、アプリ全体の一般的なルックアンドフィールを定義できます。ヘルパーパーシャルは、必要に応じてコンポーネントごとのスタイルシートでインポートできます。
各ブランチの共通コードが詳細に調査されたので、最初のタスクランナー/バンドラーアプローチに焦点を移しましょう。
Gulp+Browserifyのセットアップ
gulpfile.js
これは、22のインポートと150行のコードを含む驚くほど大きなgulpfileになります。 したがって、簡潔にするために、 js
、 css
、 server
、 watch
、およびdefault
のタスクについてのみ詳しく説明します。
JSバンドル
// Browserify specific configuration const b = browserify({ entries: [config.paths.entry], debug: true, plugin: PROD ? [] : [hmr, watchify], cache: {}, packageCache: {} }) .transform('babelify'); b.on('update', bundle); b.on('log', gutil.log); (...) gulp.task('js', bundle); (...) // Bundles our JS using Browserify. Sourcemaps are used in development, while minification is used in production. function bundle() { return b.bundle() .on('error', gutil.log.bind(gutil, 'Browserify Error')) .pipe(source('bundle.js')) .pipe(buffer()) .pipe(cond(PROD, minifyJS())) .pipe(cond(!PROD, sourcemaps.init({loadMaps: true}))) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)); }
このアプローチは、いくつかの理由でかなり醜いです。 一つには、タスクは3つの別々の部分に分割されます。 まず、Browserifyバンドルオブジェクトb
を作成し、いくつかのオプションを渡し、いくつかのイベントハンドラーを定義します。 次に、Gulpタスク自体があります。これは、名前付き関数をインライン化するのではなく、コールバックとして渡す必要があります( b.on('update')
はまったく同じコールバックを使用するため)。 これには、 gulp.src
を渡して、いくつかの変更をパイプするだけのGulpタスクの優雅さはほとんどありません。
もう1つの問題は、これにより、ブラウザーでhtml
、 css
、およびjs
をリロードするためのさまざまなアプローチが必要になることです。 Gulp watch
タスクを見てください:
gulp.task('watch', () => { livereload.listen({basePath: 'dist'}); gulp.watch(config.paths.html, ['html']); gulp.watch(config.paths.css, ['css']); gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });
HTMLファイルが変更されると、 html
タスクが再実行されます。
gulp.task('html', () => { return gulp.src(config.paths.html) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });
NODE_ENV
がproduction
環境でない場合、最後のパイプはlivereload()
を呼び出します。これにより、ブラウザーで更新がトリガーされます。
同じロジックがCSSウォッチにも使用されます。 CSSファイルが変更されると、 css
タスクが再実行され、 css
タスクの最後のパイプがlivereload()
をトリガーして、ブラウザーを更新します。
ただし、 js
ウォッチはjs
タスクをまったく呼び出しません。 代わりに、Browserifyのイベントハンドラb.on('update', bundle)
は、まったく異なるアプローチ(つまり、ホットモジュールの交換)を使用してリロードを処理します。 このアプローチの不一致は苛立たしいものですが、残念ながらインクリメンタルビルドを行うために必要です。 bundle
関数の最後で単純にlivereload()
を呼び出すと、個々のJSファイルの変更時にJSバンドル全体が再構築されます。 そのようなアプローチは明らかに拡張性がありません。 JSファイルが多いほど、各リバンドルにかかる時間が長くなります。 突然、500ミリ秒のリバンドルに30秒かかり始めます。これは、アジャイル開発を実際に阻害します。
CSSバンドル
gulp.task('css', () => { return gulp.src( [ 'node_modules/bootstrap/dist/css/bootstrap.css', 'node_modules/font-awesome/css/font-awesome.css', config.paths.css ] ) .pipe(cond(!PROD, sourcemaps.init())) .pipe(sass().on('error', sass.logError)) .pipe(concat('bundle.css')) .pipe(cond(PROD, minifyCSS())) .pipe(cond(!PROD, sourcemaps.write())) .pipe(gulp.dest(config.paths.baseDir)) .pipe(cond(!PROD, livereload())); });
ここでの最初の問題は、面倒なベンダーのCSSを含めることです。 新しいベンダーのCSSファイルがプロジェクトに追加されるたびに、実際のソースコードの関連する場所にインポートを追加するのではなく、gulpfileを変更してgulp.src
配列に要素を追加することを忘れないでください。
もう1つの主な問題は、各パイプの複雑なロジックです。 パイプに条件付きロジックを設定するためだけに、 gulp-cond
というNPMライブラリを追加する必要がありましたが、最終結果はあまり読みやすくありません(どこでもトリプルブラケット!)。
サーバータスク
gulp.task('server', () => { nodemon({ script: 'server.js' }); });
このタスクは非常に簡単です。 これは基本的に、ノード環境でserver.js
を実行するコマンドライン呼び出しnodemon server.js
のラッパーです。 node
の代わりにnodemon
が使用されるため、ファイルに変更を加えるとファイルが再起動します。 デフォルトでは、 nodemon
はJSファイルが変更されると実行中のプロセスを再開します。そのため、そのスコープを制限するためにnodemon.json
ファイルを含めることが重要です。
{ "watch": "server.js" }
サーバーコードを確認しましょう。
server.js
const baseDir = process.env.NODE_ENV === 'production' ? 'build' : 'dist'; const port = process.env.NODE_ENV === 'production' ? 8080: 3000; const app = express();
これにより、ノード環境に基づいてサーバーとポートのベースディレクトリが設定され、expressのインスタンスが作成されます。
app.use(require('connect-livereload')({port: 35729})); app.use(express.static(path.join(__dirname, baseDir)));
これにより、 connect-livereload
ミドルウェア(ライブリロードのセットアップに必要)と静的ミドルウェア(静的アセットの処理に必要)が追加されます。
app.get('/api/sample-route', (req, res) => { res.send({ website: 'Toptal', blogPost: true }); });
これは単純なAPIルートです。 ブラウザでlocalhost:3000/api/sample-route
に移動すると、次のように表示されます。

{ website: "Toptal", blogPost: true }
実際のバックエンドでは、APIルート専用のフォルダー全体、DB接続を確立するための個別のファイルなどがあります。 このサンプルルートは、設定したフロントエンドの上にバックエンドを簡単に構築できることを示すために含まれているだけです。
app.get('*', (req, res) => { res.sendFile(path.join(__dirname, './', baseDir ,'/index.html')); });
これはキャッチオールルートです。つまり、ブラウザに入力するURLに関係なく、サーバーは唯一のindex.html
ページを返します。 その場合、クライアント側でルートを解決するのはReactルーターの責任です。
app.listen(port, () => { open(`http://localhost:${port}`); });
これは、指定したポートをリッスンし、指定したURLの新しいタブでブラウザーを開くようにエクスプレスインスタンスに指示します。
これまでのところ、サーバーのセットアップについて私が気に入らないのは次のとおりです。
app.use(require('connect-livereload')({port: 35729}));
gulpfileですでにgulp-livereload
を使用していることを考えると、これにより、livereloadを使用する必要がある2つの別々の場所が作成されます。
さて、最後になりましたが、重要なことです。
デフォルトのタスク
gulp.task('default', (cb) => { runSequence('clean', 'lint', 'test', 'html', 'css', 'js', 'fonts', 'server', 'watch', cb); });
これは、ターミナルにgulp
と入力するだけで実行されるタスクです。 奇妙なことに、タスクを順番に実行するためにrunSequence
を使用する必要があります。 通常、一連のタスクは並行して実行されますが、これが常に望ましい動作であるとは限りません。 たとえば、ファイルを移動する前に宛先フォルダーが空であることを確認するために、 html
の前にclean
タスクを実行する必要があります。 gulp.series
4がリリースされると、gulp.seriesメソッドとgulp.parallel
メソッドがネイティブにサポートされますが、今のところ、セットアップにこのわずかな癖を残しておく必要があります。
それを超えて、これは実際にはかなりエレガントです。 アプリの作成とホスティング全体が1つのコマンドで実行され、ワークフローの任意の部分を理解することは、実行シーケンスで個々のタスクを調べるのと同じくらい簡単です。 さらに、シーケンス全体を小さなチャンクに分割して、アプリを作成およびホストするためのよりきめ細かいアプローチをとることができます。 たとえば、 lint
タスクとtest
タスクを実行するvalidate
という別のタスクを設定できます。 または、 server
とwatch
を実行するhost
タスクを作成することもできます。 タスクを調整するこの機能は、特にアプリケーションが拡張され、より自動化されたタスクが必要になるため、非常に強力です。
開発と本番ビルド
if (argv.prod) { process.env.NODE_ENV = 'production'; } let PROD = process.env.NODE_ENV === 'production';
yargs
NPMライブラリを使用して、コマンドラインフラグをGulpに提供できます。 ここでは、ターミナルでgulp
が--prod
に渡された場合に、ノード環境を本番環境に設定するようにgulpfileに指示します。 次に、 PROD
変数を条件として使用して、gulpfileの開発動作と本番動作を区別します。 たとえば、 browserify
に渡すオプションの1つは次のとおりです。
plugin: PROD ? [] : [hmr, watchify]
これにより、 browserify
は本番モードではプラグインを使用せず、他の環境ではhmr
およびwatchify
プラグインを使用するように指示されます。
このPROD
条件は、本番環境と開発用に別個のgulpfileを作成する必要がなく、最終的には多くのコードの繰り返しが含まれるため、非常に便利です。 代わりに、 gulp --prod
を使用して本番環境でデフォルトのタスクを実行したり、 gulp html --prod
--prodを使用して本番環境でhtml
タスクのみを実行したりできます。 一方、Gulpパイプラインに.pipe(cond(!PROD, livereload()))
などのステートメントを散らかすのは、最も読みやすいものではないことを以前に見ました。 結局、ブール変数アプローチを使用するか、2つの別々のgulpfileを設定するかは好みの問題です。
次に、Gulpをタスクランナーとして使用し続けながら、BrowserifyをWebpackに置き換えた場合に何が起こるかを見てみましょう。
Gulp+Webpackのセットアップ
突然、gulpfileの長さはわずか99行になり、12回のインポートが行われ、以前の設定から大幅に削減されました。 デフォルトのタスクを確認すると、次のようになります。
gulp.task('default', (cb) => { runSequence('lint', 'test', 'build', 'server', 'watch', cb); });
これで、完全なWebアプリのセットアップに必要なタスクは9つではなく5つになり、劇的に改善されました。
さらに、 livereload
の必要性を排除しました。 私たちのwatch
タスクは単純です:
gulp.task('watch', () => { gulp.watch(config.paths.js, () => { runSequence('lint', 'test'); }); });
これは、私たちのgulpwatcherがいかなるタイプの再バンドル動作もトリガーしていないことを意味します。 追加のボーナスとして、 index.html
をapp
からdist
またはbuild
に転送する必要がなくなりました。
タスクの削減に焦点を戻すと、 html
、 css
、 js
、およびfonts
タスクはすべて単一のbuild
タスクに置き換えられました。
gulp.task('build', () => { runSequence('clean', 'html'); return gulp.src(config.paths.entry) .pipe(webpack(require('./webpack.config'))) .pipe(gulp.dest(config.paths.baseDir)); });
十分に単純です。 clean
タスクとhtml
タスクを順番に実行します。 それらが完了したら、エントリポイントを取得し、Webpackにパイプして、 webpack.config.js
ファイルを渡して構成し、結果のバンドルをbaseDir
(ノード環境に応じてdist
またはbuild
)に送信します。
Webpack構成ファイルを見てみましょう。
webpack.config.js
これはかなり大きくて威圧的な設定ファイルなので、 module.exports
オブジェクトに設定されている重要なプロパティのいくつかを説明しましょう。
devtool: PROD ? 'source-map' : 'eval-source-map',
これにより、Webpackが使用するソースマップのタイプが設定されます。 Webpackは、すぐに使用できるソースマップをサポートするだけでなく、実際にはさまざまなソースマップオプションをサポートしています。 各オプションは、ソースマップの詳細と再構築速度(変更時に再バンドルするのにかかる時間)のバランスが異なります。 これは、開発に「安価な」ソースマップオプションを使用して高速リロードを実現し、本番環境ではより高価なソースマップオプションを使用できることを意味します。
entry: PROD ? './app/index' : [ 'webpack-hot-middleware/client?reload=true', // reloads the page if hot module reloading fails. './app/index' ]
これがバンドルのエントリポイントです。 配列が渡されることに注意してください。つまり、複数のエントリポイントを持つことができます。 この場合、予想されるエントリポイントapp/index.js
と、ホットモジュールのリロード設定の一部として使用されるwebpack-hot-middleware
エントリポイントがあります。
output: { path: PROD ? __dirname + '/build' : __dirname + '/dist', publicPath: '/', filename: 'bundle.js' },
これは、コンパイルされたバンドルが出力される場所です。 最も紛らわしいオプションはpublicPath
です。 バンドルがサーバー上でホストされる場所のベースURLを設定します。 したがって、たとえば、 publicPath
が/public/assets
Assetsの場合、バンドルはサーバーの/public/assets/bundle.js
の下に表示されます。
devServer: { contentBase: PROD ? './build' : './app' }
これにより、プロジェクト内のどのフォルダをサーバーのルートディレクトリとして使用するかがサーバーに通知されます。
Webpackがプロジェクトで作成されたバンドルをサーバー上のバンドルにマッピングする方法について混乱した場合は、次の点に注意してください。
-
path
+filename
:プロジェクトのソースコード内のバンドルの正確な場所 contentBase
(ルートとして、/
)+publicPath
:サーバー上のバンドルの場所
plugins: PROD ? [ new webpack.optimize.OccurenceOrderPlugin(), new webpack.DefinePlugin(GLOBALS), new ExtractTextPlugin('bundle.css'), new webpack.optimize.DedupePlugin(), new webpack.optimize.UglifyJsPlugin({compress: {warnings: false}}) ] : [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ],
これらは、何らかの方法でWebpackの機能を強化するプラグインです。 たとえば、 webpack.optimize.UglifyJsPlugin
は縮小を担当します。
loaders: [ {test: /\.js$/, include: path.join(__dirname, 'app'), loaders: ['babel']}, { test: /\.css$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap'): 'style!css?sourceMap' }, { test: /\.scss$/, loader: PROD ? ExtractTextPlugin.extract('style', 'css?sourceMap!resolve-url!sass?sourceMap') : 'style!css?sourceMap!resolve-url!sass?sourceMap' }, {test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'} ]
これらはローダーです。 基本的に、 require()
ステートメントを介してロードされるファイルを前処理します。 ローダーをチェーン接続できるという点で、Gulpパイプにいくぶん似ています。
ローダーオブジェクトの1つを調べてみましょう。
{test: /\.scss$/, loader: 'style!css?sourceMap!resolve-url!sass?sourceMap'}
test
プロパティは、ファイルが指定された正規表現パターン(この場合は/\.scss$/
)に一致する場合に、指定されたローダーが適用されることをWebpackに通知します。 loader
プロパティは、ローダーが実行するアクションに対応します。 ここでは、 style
、 css
、 resolve-url
、 sass
ローダーを連鎖させています。これらは逆の順序で実行されます。
loader3!loader2!loader1
構文があまりエレガントではないことを認めなければなりません。 結局のところ、プログラムの中で右から左に何かを読まなければならないのはいつですか。 それにもかかわらず、ローダーはwebpackの非常に強力な機能です。 実際、先ほど触れたローダーを使用すると、SASSファイルをJavaScriptに直接インポートできます。 たとえば、ベンダーとグローバルスタイルシートをエントリポイントファイルにインポートできます。
index.js
import React from 'react'; import {render} from 'react-dom'; import {Router, browserHistory} from 'react-router'; import routes from './routes'; // CSS imports import '../node_modules/bootstrap/dist/css/bootstrap.css'; import '../node_modules/font-awesome/css/font-awesome.css'; import './styles/global.scss'; render(<Router history={browserHistory} routes={routes} />, document.getElementById('app'));
同様に、Headerコンポーネントに、 import './Header.scss'
を追加して、コンポーネントに関連付けられたスタイルシートをインポートできます。 これは、他のすべてのコンポーネントにも当てはまります。
私の意見では、これはJavaScript開発の世界における革命的な変化とほぼ見なすことができます。 ローダーがこれらすべてを処理するため、CSSのバンドル、縮小、またはソースマップについて心配する必要はありません。 ホットモジュールのリロードでさえ、CSSファイルで機能します。 次に、同じファイルでJSとCSSのインポートを処理できるため、開発が概念的に単純になります。一貫性が高まり、コンテキストの切り替えが少なくなり、推論が容易になります。
この機能の仕組みを簡単に説明すると、WebpackはCSSをJSバンドルにインライン化します。 実際、Webpackは画像やフォントに対してもこれを行うことができます。
{test: /\.(svg|png|jpe?g|gif)(\?\S*)?$/, loader: 'url?limit=100000&name=img/[name].[ext]'}, {test: /\.(eot|woff|woff2|ttf)(\?\S*)?$/, loader: 'url?limit=100000&name=fonts/[name].[ext]'}
URLローダーは、画像とフォントが100 KB未満の場合はデータURLとしてインライン化するように、それ以外の場合は個別のファイルとして提供するようにWebpackに指示します。 もちろん、カットオフサイズを10KBなどの別の値に構成することもできます。
これがWebpackの構成です。 かなりの量のセットアップがあることを認めますが、それを使用することの利点は単に驚異的です。 Browserifyにはプラグインとトランスフォームがありますが、追加機能の点でWebpackローダーと比較することはできません。
Webpack+NPMスクリプトのセットアップ
このセットアップでは、タスクを自動化するためにgulpfileに依存するのではなく、npmスクリプトを直接使用します。
package.json
"scripts": { "start": "npm-run-all --parallel lint:watch test:watch build", "start:prod": "npm-run-all --parallel lint test build:prod", "clean-dist": "rimraf ./dist && mkdir dist", "clean-build": "rimraf ./build && mkdir build", "clean": "npm-run-all clean-dist clean-build", "test": "mocha ./app/**/*.test.js --compilers js:babel-core/register", "test:watch": "npm run test -- --watch", "lint": "esw ./app/**/*.js", "lint:watch": "npm run lint -- --watch", "server": "nodemon server.js", "server:prod": "cross-env NODE_ENV=production nodemon server.js", "build-html": "node tools/buildHtml.js", "build-html:prod": "cross-env NODE_ENV=production node tools/buildHtml.js", "prebuild": "npm-run-all clean-dist build-html", "build": "webpack", "postbuild": "npm run server", "prebuild:prod": "npm-run-all clean-build build-html:prod", "build:prod": "cross-env NODE_ENV=production webpack", "postbuild:prod": "npm run server:prod" }
開発ビルドと本番ビルドを実行するには、それぞれnpmstartとnpm start
npm run start:prod
を入力します。
これは、99〜150行のコードを19 NPMスクリプトに削減したことを考えると、gulpfileよりも確かにコンパクトです。本番スクリプトを除外した場合は12行になります(ほとんどの場合、ノード環境が本番環境に設定された開発スクリプトをミラーリングします)。 )。 欠点は、これらのコマンドが、Gulpタスクの対応するコマンドと比較してやや不可解であり、表現力があまりないことです。 たとえば、(少なくとも私が知っている限りでは)単一のnpmスクリプトで特定のコマンドを直列に実行し、他のコマンドを並列に実行する方法はありません。 それはどちらか一方です。
ただし、このアプローチには大きな利点があります。 コマンドラインから直接mocha
などのNPMライブラリを使用することにより、それぞれに同等のGulpラッパー(この場合はgulp-mocha
)をインストールする必要はありません。
NPMをインストールする代わりに
- gulp-eslint
- gulp-mocha
- gulp-nodemon
- 等
次のパッケージをインストールします。
- eslint
- モカ
- nodemon
- 等
Cory Houseの投稿を引用して、NPMスクリプトのためにGulpとGruntを残した理由:
私はGulpの大ファンでした。 しかし、私の最後のプロジェクトでは、gulpfileに数百行と約12のGulpプラグインが含まれることになりました。 Gulpを使用して、Webpack、Browsersync、ホットリロード、Mochaなどを統合するのに苦労していました。 なんで? さて、いくつかのプラグインは私のユースケースには不十分なドキュメントを持っていました。 一部のプラグインは、必要なAPIの一部のみを公開していました。 1つには、少数のファイルしか監視しないという奇妙なバグがありました。 コマンドラインに出力するときに別のストリップされた色。
彼はGulpの3つの主要な問題を指定しています。
- プラグイン作成者への依存
- デバッグするのが面倒
- ばらばらのドキュメント
私はこれらすべてに同意する傾向があります。
1.プラグイン作成者への依存
eslint
などのライブラリが更新されるたびに、関連するgulp-eslint
ライブラリに対応する更新が必要になります。 ライブラリメンテナが興味を失った場合、ライブラリのgulpバージョンはネイティブバージョンと同期しなくなります。 新しいライブラリが作成されるときも同じことが言えます。 誰かがライブラリxyz
を作成し、それがキャッチした場合、突然、対応するgulp-xyz
ライブラリが必要になり、それをgulpタスクで使用できます。
ある意味で、このアプローチは拡張性がありません。 理想的には、ネイティブライブラリを使用できるGulpのようなアプローチが必要です。
2.デバッグするのにイライラする
gulp-plumber
などのライブラリはこの問題を大幅に軽減するのに役立ちますが、それでもgulp
でのエラー報告はあまり役に立たないのは事実です。 パイプが1つでも未処理の例外をスローした場合、ソースコードで問題を引き起こしている原因とは完全に無関係と思われる問題のスタックトレースを取得します。 これにより、デバッグが悪夢になる場合があります。 エラーが不可解であるか、誤解を招く可能性がある場合、GoogleやStackOverflowで検索しても実際には役に立ちません。
3.ばらばらのドキュメント
多くの場合、小さなgulp
ライブラリのドキュメントは非常に限られている傾向があります。 これは、著者が通常、主に自分で使用するためにライブラリを作成しているためだと思います。 さらに、Gulpプラグインとネイティブライブラリ自体の両方のドキュメントを確認する必要があるのが一般的です。これは、多くのコンテキスト切り替えと2倍の読み取りを行うことを意味します。
結論
それぞれのオプションには長所と短所がありますが、BrowserifyよりもWebpackの方が、GulpよりもNPMスクリプトの方が望ましいことは私にはかなり明らかです。 Gulpは確かにNPMスクリプトよりも表現力があり便利ですが、追加されたすべての抽象化で代償を払うことになります。
すべての組み合わせがアプリに最適であるとは限りませんが、圧倒的な数の開発依存関係とイライラするデバッグエクスペリエンスを回避したい場合は、NPMスクリプトを使用したWebpackが最適です。 この記事が、次のプロジェクトに適したツールを選択するのに役立つことを願っています。
- 制御の維持:WebpackとReactのガイド、Pt。 1
- Gulp Under the Hood:ストリームベースのタスク自動化ツールの構築