LoginSignup
4
5

More than 5 years have passed since last update.

Angular2開発をTypeScript+Sass+ejsで行う & gulpでsrcと別ディレクトリに構築

Last updated at Posted at 2016-11-24

前回の「Angular2のリリース用ソースをgulp + webpack + inlineNg2Templateで1ファイル+αに圧縮してみる」の続編となります。
前回はリリース用として全てのファイルを1つのjsにまとめましたが、ビルドに30秒以上かかってしまうため開発時には使い物になりません。
そのため、ローカル開発用としてビルドの単位を最小限に抑えたgulpのタスクを用意します。

今回やりたいこと

  1. Angular2のソースはTypeScriptで書きたい
  2. src用ディレクトリにビルドされたファイル(.jsや.map)を展開したくない
  3. styleの指定ではsassとpostcssを使いたい
  4. コンポーネントのtemplateやstyleは外部ファイルで保存したい
  5. 4のファイルはすべてビルド時にインライン化したい
  6. html, css, js すべて最小化(minity)したい
  7. templateはejsも利用可能にしたい
  8. 生成されたスクリプトはファイル名にhash値を含み、index.htmlで取り込みたい
  9. ビルドされたリリース用 or 開発用ソースをローカルサーバーで動作させたい
  10. 開発時はwebpack化せずに、編集後数秒以内に確認できるようにしたい

5、6、8は前回 の内容となるため、今回は語りません。
今回の主題は10の実現であり、前回の「リリース時のwebpack化」と、今回の「開発時の最小単位のビルド化」gulpのタスクで切り分けるのが目的です。

例:やりたいことをディレクトリ構成で表現

gitリポジトリで管理する用のソース(ビルド前)

srcディレクトリ
├ 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ディレクトリ
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ディレクトリ
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.jsonpackage.jsontsconfig.json前回の記事と同様のものを利用しますので解説は省きます。

systemjs.config.js

前回は利用しなかったsystemjs.config.jsですが、今回は利用します。
ベースとなっているのは「Angular2でHello world!」の記事の時のsystemjs.confing.jsです。
今回はcore-jszone.jsといったファイルをindex.htmlのscriptタグで読み込むのではなく、main.ts内のimport指定で読み込んでいるしているため、SystemJsで定義しておきます。

systemjs.config.js
(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)です。

src/app/main.ts
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);
src/index.ejs
<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が以下です。

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でローカル実行用のビルドが行えるようになります。

やっていることとしては以下です。
1. [local-clean] クリーニング処理。localディレクトリの削除、src配下の.cssの削除。
2. [sass] sassのビルド。src配下の.sassをビルドし、同階層に最適化済みcssファイルを作成します。(汚点)
3. [local-ts][並列] .tsファイルをes5の.jsに変換する処理。./local配下の同じ階層に展開されます。
3. [local-watch][並列] src配下の.sass、.ts、.ejs、.html、.css、.jsファイルの更新を検知して自動ビルド化
5. [local-statics][並列] 共通cssやjsファイルを ./local配下に同じ階層でコピーします。
6. [local-ejs][並列] .ejsファイルを{type:local}として.htmlにビルドし、./local配下に同じ階層でコピーします。
7. [local-nodemodules][並列] 直下にあるnode_modulesへのシムリンクを./local/node_modulesとして貼ります。
8. [local-server] ./localをrootとしたhttpサーバーを起動。ブラウザを自動で立ち上げます。

gulpfile.js
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なのでその辺は自分次第でいくらでも調整可能、といったところです。

~ではまた次回~

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5