前回の「Angular2のリリース用ソースをgulp + webpack + inlineNg2Templateで1ファイル+αに圧縮してみる」の続編となります。
前回はリリース用として全てのファイルを1つのjsにまとめましたが、ビルドに30秒以上かかってしまうため開発時には使い物になりません。
そのため、ローカル開発用としてビルドの単位を最小限に抑えたgulpのタスクを用意します。
##今回やりたいこと
- Angular2のソースはTypeScriptで書きたい
- src用ディレクトリにビルドされたファイル(.jsや.map)を展開したくない
- styleの指定ではsassとpostcssを使いたい
- コンポーネントのtemplateやstyleは外部ファイルで保存したい
4のファイルはすべてビルド時にインライン化したいhtml, css, js すべて最小化(minity)したい- templateはejsも利用可能にしたい
生成されたスクリプトはファイル名にhash値を含み、index.htmlで取り込みたい- ビルドされたリリース用 or 開発用ソースをローカルサーバーで動作させたい
- 開発時はwebpack化せずに、編集後数秒以内に確認できるようにしたい
5、6、8は前回 の内容となるため、今回は語りません。
今回の主題は10の実現であり、前回の**「リリース時のwebpack化」と、今回の「開発時の最小単位のビルド化」をgulpのタスクで切り分ける**のが目的です。
例:やりたいことをディレクトリ構成で表現
gitリポジトリで管理する用のソース(ビルド前)
├ node_modules/
│ └─ ... 全てのnodeライブラリ
├ src/
│ ├─ app/
│ │ ├─ bank-table
│ │ │ ├─ bank-table.component.html
│ │ │ ├─ bank-table.component.sass -- 該当コンポーネント専用のstyle情報
│ │ │ └─ bank-table.component.ts -- htmlとcss(.sassではない)を外部ファイル読込している
│ │ │
│ │ ├─ _setting.sass -- 各コンポーネントで共通利用するようなsassの変数を格納
│ │ ├─ app.component.ts
│ │ ├─ app.module.ts
│ │ └─ main.ts
│ │
│ ├─ css/
│ │ ├─ _reset.sass
│ │ ├─ _base.sass
│ │ └─ common.sass -- _reset.sassと_base.sassをimportしている共通style情報。
│ │
│ ├─ index.ejs -- 次回のローカル開発用の分岐等が必要なためejsで記述
│ └─ systemjs.config.js -- webpackする場合は不要。今回のローカル開発時で利用する。
│
├ gulpconfig.json -- ※前回の内容と同一なため今回の解説は無し
├ gulpfile.js
├ package.json -- ※前回の内容と同一なため今回の解説は無し
├ tsconfig.json -- ※前回の内容と同一なため今回の解説は無し
└ webpack.config.js -- ※前回の内容と同一なため今回の解説は無し
リリースをする用のソース(上記ディレクトリのリリース用ビルド後)
※前回 の内容なためリリース用環境の説明は今回はありません
dist/
├─ css/
│ └─ common.css
├─ js/
│ ├─ main.bundle.6114f73f73ce49e6cfef.js -- キャッシュ対策でファイル名にハッシュ値を含む
│ └─ main.bundle.6114f73f73ce49e6cfef.js.map -- map情報。(htmlからは読み込まない)
│
└─ index.html
開発をする用のソース(上記ディレクトリのローカル用ビルド後)
今回のメインです。
.tsは.jsへ、.sassは.cssへ、.ejsは.htmlへ
それぞれビルド済みのものが展開されます。
local/
├─ app/
│ ├─ bank-table
│ │ ├─ bank-table.component.css
│ │ ├─ bank-table.component.html
│ │ └─ bank-table.component.js
│ │ └─ bank-table.component.js.map
│ │
│ ├─ app.component.js
│ ├─ app.component.js.map
│ ├─ app.module.js
│ ├─ app.module.js.map
│ ├─ main.js
│ └─ main.js.map
│
├─ css/
│ └─ common.css
│
├─ node_modules -- 一階層上のnode_modulesへのシムリンクです
│
├─ index.html
└─ systemjs.config.js -- webpack時は不要ですが、local実行時には重要な役割を果たします。
前提条件
前回の「Angular2のリリース用ソースをgulp + webpack + inlineNg2Templateで1ファイル+αに圧縮してみる」の続きです。
また、ファイル構成は過去の記事「Angular2でHello world!」で構築した環境をベースにしています。
環境を再掲
- Microsoft Surface Pro4 (i5 2.4GHz, Mem 4GB)
- OS:Windows 10 Pro
- node.js v7.1.9
- npm v3.10.9
コーディング開始
gulpconfig.json
、package.json
、tsconfig.json
は前回の記事と同様のものを利用しますので解説は省きます。
systemjs.config.js
前回は利用しなかったsystemjs.config.js
ですが、今回は利用します。
ベースとなっているのは「Angular2でHello world!」の記事の時のsystemjs.confing.jsです。
今回はcore-js
やzone.js
といったファイルをindex.htmlのscriptタグで読み込むのではなく、main.ts内のimport指定で読み込んでいるしているため、SystemJsで定義しておきます。
(function (global) {
System.config({
paths: {
'npm:*': 'node_modules/*'
},
map: {
app: 'app',
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
'@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js',
'core-js': 'npm:core-js/client/shim.min.js',
'zone.js': 'npm:zone.js/dist/zone.js',
'rxjs': 'npm:rxjs',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js'
},
packages: {
app: {
main: 'main.js',
defaultExtension: 'js'
},
rxjs: {
defaultExtension: 'js'
}
}
});
})(this);
src/app/main.tsとsrc/index.ejs
前回と同じ内容ですが、これは敢えて再掲します。
理由は、この2つが「webpackでのbundle化」と「個別ビルド化」を共存させるためのカギとなる為です。
webpack化する場合、webpack.config.js
でのentryとしてmain.tsを
を指定すると、main.tsの中身はグローバルに展開されるため、scriptが読み込まれた際にmain.tsの内容は即実行されます。
ですが、headタグ内にscriptタグを置いているとDOMの構築が終わる前にmain.tsのbootstrapが実行されてしまい、「対象のelementが無いというエラー」が出てしまいます。例:The selector "my-app" did not match any elements
これを回避するために、webpack化の対応としてbootstrapへの引き渡しはDOMContentLoadedの発火時に行うようにします。
※scriptタグをindex.htmlのbodyの最後に書くことでも回避はできますが、これはダサいのでやりません
document.addEventListener("DOMContentLoaded", function(){
platform.bootstrapModule(AppModule)
});
が、これを行うと今度はローカル実行時にindex.htmlでSystem.import('app');
を行ってもbootstrapが実行されなくなります。
何故かというと、System.import('app');
はmain.js(systemjs.config.js
でappとして定義されているjsファイル)を実行しているだけだからです。
main.js内ではbootstrapの実行をDOMContentLoaded発火時に行うようにしましたが、index.html内のSystem.import('app');
の内部処理はDOMContentLoaded完了後に実行されます。(SystemJs側の挙動)
そのためSystem.import('app');
でmain.jsが実行されるタイミングではDOMContentLoadedが既に完了しており、bootstrapが実行されることはありません。
なんということでしょう!
「なんかもうwebpack時と個別ビルド時用のmain.ts分ければ良くね('A`)?」と諦めそうになりましたが、SystemJSの仕様を追っていると、thenメソッドの高階関数の引数にmain.tsのオブジェクトが渡されている事が分かりました。
それを利用したのが以下のmain.tsとindex.ejs(index.html)です。
import "core-js";
import "zone.js";
import "rxjs/Rx";
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
const platform = platformBrowserDynamic();
export function init(): void {
platform.bootstrapModule(AppModule);
}
document.addEventListener("DOMContentLoaded", init);
<html>
<head>
<title>Angular QuickStart</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/css/common.css" media="all" rel="stylesheet" />
<% if (type == 'local') { %>
<script src="/node_modules/systemjs/dist/system.src.js"></script>
<script src="/systemjs.config.js"></script>
<script>
System.import('app').then(function(m){
m.init();
}).catch(function(err){
console.error(err);
});
</script>
<% } else { %>
<% for (var bundleFilePath of bundleFileList) { %>
<script src="/js/<%= bundleFilePath %>"></script>
<% } %>
<% } %>
</head>
<!-- 3. Display the application -->
<body>
<my-app>Loading...</my-app>
<my-app2>aa</my-app2>
</body>
</html>
ejs側が分かりずらいですが、gulpを通して生成されるindex.htmlが以下です。
<html>
<head>
<title>Angular QuickStart</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/css/common.css" media="all" rel="stylesheet" />
<script src="/node_modules/systemjs/dist/system.src.js"></script>
<script src="/systemjs.config.js"></script>
<script>
System.import('app').then(function(m){
m.init();
}).catch(function(err){
console.error(err);
});
</script>
</head>
<!-- 3. Display the application -->
<body>
<my-app>Loading...</my-app>
<my-app2>aa</my-app2>
</body>
</html>
ここが今回の妙技です。webpackの際はDOMContentLoaded発火時にbootstrapが呼ばれ、個別ビルドの場合はこの記述によりbootstrapが実行されます。
System.import('app').then(function(m){
m.init();
})
これでこれで全てが出そろいました。
あとはgulpfile.jsを書いて完了です。
gulpfile.js
オオトリ。
本来は前回のgulpfile.jsの内容 も合わせて記述されていますが、記事が長くなるので今回はローカル実行分だけ記載します。
前回と今回の内容を1つのgulpfile.jsに書くことで、gulp build
でリリース用のビルド、gulp local
でローカル実行用のビルドが行えるようになります。
やっていることとしては以下です。
- [local-clean] クリーニング処理。localディレクトリの削除、src配下の.cssの削除。
- [sass] sassのビルド。src配下の.sassをビルドし、同階層に最適化済みcssファイルを作成します。(汚点)
- [local-ts][並列] .tsファイルをes5の.jsに変換する処理。./local配下の同じ階層に展開されます。
- [local-watch][並列] src配下の.sass、.ts、.ejs、.html、.css、.jsファイルの更新を検知して自動ビルド化
- [local-statics][並列] 共通cssやjsファイルを ./local配下に同じ階層でコピーします。
- [local-ejs][並列] .ejsファイルを{type:local}として.htmlにビルドし、./local配下に同じ階層でコピーします。
- [local-nodemodules][並列] 直下にあるnode_modulesへのシムリンクを./local/node_modulesとして貼ります。
- [local-server] ./localをrootとしたhttpサーバーを起動。ブラウザを自動で立ち上げます。
var gulp = require('gulp');
var typescript = require('gulp-typescript');
var plumber = require('gulp-plumber');
var runSequence = require('run-sequence');
var inlineNg2Template = require('gulp-inline-ng2-template');
var webpack = require('gulp-webpack');
var sourcemaps = require('gulp-sourcemaps');
var webserver = require('gulp-webserver');
var del = require('del');
var sass = require('gulp-sass');
var postcss = require('gulp-postcss');
var cssnext = require('postcss-cssnext');
var minifycss = require('gulp-minify-css');
var minifyhtml = require('gulp-minify-html');
var symlink = require('gulp-symlink'); //次回で利用
var ejs = require("gulp-ejs");
var fs = require('fs');
var path = require('path');
var conf = {
'gulp' : require('./gulpconfig'),
'webpack' : require('./webpack.config.js'),
'ts' : require('./tsconfig')
}
gulp.task('sass', function () {
var processors = [cssnext()];
return gulp.src(conf.gulp.srcDir + '/**/*.sass', {base: conf.gulp.srcDir})
.pipe(sass().on('error', sass.logError))
.pipe(postcss(processors))
.pipe(minifycss())
.pipe(gulp.dest(conf.gulp.srcDir))
});
gulp.task('local-clean', function (cb) {
return del([
conf.gulp.srcDir + '/**/*.css',
conf.gulp.localDir,
], cb);
});
gulp.task('local-ts', function () {
var tsResult = gulp.src(conf.gulp.srcDir + '/**/*.ts', {base: conf.gulp.srcDir})
.pipe(sourcemaps.init())
.pipe(plumber())
.pipe(typescript(conf.ts.compilerOptions));
tsResult.dts.pipe(gulp.dest(conf.gulp.localDir));
return tsResult.js.pipe(sourcemaps.write("./"))
.pipe(gulp.dest(conf.gulp.localDir));
});
gulp.task('local-nodemodules', function () {
return gulp.src('node_modules')
.pipe(symlink(conf.gulp.localDir + '/node_modules'));
});
gulp.task('local-ejs', function () {
return gulp.src(conf.gulp.srcDir + '/**/*.ejs')
.pipe(plumber())
.pipe(ejs({type: "local"}, {"ext": ".html"}))
.pipe(gulp.dest(conf.gulp.localDir));
});
gulp.task('local-statics', function () {
return gulp.src(
[ conf.gulp.srcDir + '/**/*.html',
conf.gulp.srcDir + '/**/*.css',
conf.gulp.srcDir + '/**/*.js' ],
{ base: conf.gulp.srcDir }
).pipe( gulp.dest(conf.gulp.localDir));
});
gulp.task('local-server', function () {
gulp.src(conf.gulp.localDir)
.pipe(webserver({
host: 'localhost',
port: 8100,
open: true,
livereload: false //自動リロードはしない
}));
});
gulp.task('local', function (cb) {
return runSequence(
'local-clean',
'sass',
['local-ts', 'local-watch', 'local-statics', 'local-ejs', 'local-nodemodules'],
'local-server',
cb
);
});
// 自動ビルド
gulp.task('local-watch', function () {
gulp.watch(conf.gulp.srcDir + '/**/*.sass', ['sass']);
gulp.watch(conf.gulp.srcDir + '/**/*.ts', ['local-ts']);
gulp.watch(conf.gulp.srcDir + '/**/*.ejs', ['local-ejs']);
gulp.watch([ conf.gulp.srcDir + '/**/*.html',
conf.gulp.srcDir + '/**/*.css',
conf.gulp.srcDir + '/**/*.js' ], ['local-statics']);
});
実行
コンソールからgulpを実行します。
>gulp loacl
gulp local
gulp local
[00:40:34] Using gulpfile C:\works\angular\qiita\gulpfile.js
[00:40:34] Starting 'local'...
[00:40:34] Starting 'local-clean'...
[00:40:34] Finished 'local-clean' after 44 ms
[00:40:34] Starting 'sass'...
[00:40:35] Finished 'sass' after 879 ms
[00:40:35] Starting 'local-ts'...
[00:40:35] Starting 'local-watch'...
[00:40:35] Finished 'local-watch' after 59 ms
[00:40:35] Starting 'local-statics'...
[00:40:35] Starting 'local-ejs'...
[00:40:35] Starting 'local-nodemodules'...
[00:40:36] gulp-symlink:C:\works\angular\qiita\node_modules symlinked to ./local/node_modules
[00:40:36] Finished 'local-nodemodules' after 47 ms
[00:40:36] gulp-symlink:C:\works\angular\qiita\node_modules symlinked to ./local/node_modules
[00:40:36] Finished 'local-ejs' after 67 ms
[00:40:40] Finished 'local-statics' after 4.37 s
[00:40:40] Finished 'local-ts' after 4.76 s
[00:40:40] Starting 'local-server'...
[00:40:40] Webserver started at http://localhost:8100
[00:40:40] Finished 'local-server' after 8.76 ms
[00:40:40] Finished 'local' after 5.7 s
初回起動時で5~10秒程度かかりますが、その後のファイル更新における反映はそれなりに速いです。
- tsファイル : 5秒程度
- sass : 1秒程度
- ejs : 0.1秒程度
- html, css, js :5秒程度
プロジェクトが大きくなってくると確実に劣化すると思いますが、tsファイルは差分ビルドにしたり、html, css, jsはwathを分けたりすることで改善は図れると思っています。
gulpなのでその辺は自分次第でいくらでも調整可能、といったところです。
~ではまた次回~