Rust始めました

随分久しぶりになってしまいました

さて、今年になってから Rust を触り始めてます
なぜ Rust を触り始めたかと言うと

blog.rust-lang.org

Rust が(実験的にですが) WebAssembly をサポートすることになったそうです
Web のフロントエンドを主にやってる自分としては WebAssembly はおさえとかないといけないので
それなら WebAssembly のサポートを始めた Rust を身に付けようと思ったのがきっかけです

Rust とは

Rust は Mozilla が開発した言語です

Rust が目指したゴールはこの3つ

  • 安全性
  • 速度
  • 並行性

ガベージコレクタがないのも特徴ですね

他言語と違う部分

イミュータブル

変数はデフォルトでイミュータブルです
ミュータブルな変数を宣言したい場合は下記のようにします

let mut guess = String::new();

Shadowing

新しい変数宣言で、前の変数定義を隠すことができます

let mut guess = String::new();

// 省略

let guess: u32 = guess.trim();

最初の宣言では guess はミュータブルな文字列の変数ですが
2回目の宣言で guess はイミュータブルな符号なしの32bit整数となります

guess_str とか別の名前を付けなくて済むのは楽ですね


他にも特徴的な部分はあると思いますが、触れてないのでとりあえずこれぐらいで

まとめ

まだ触ったばかり(チュートリアルを触ったぐらい)なので、ネタがあまりありません(;´∀`)
日本語の情報がまだ少ないので、ちょこちょこ更新していけたらなぁと思ってます

Service Workerの基本とそれを使ってできること

Service Workerとは

ブラウザが Web ページとは別にバックグラウンドで実行するスクリプト
オフラインのアプリを実現・サポートするために作られたものです

ちなみに、ブラウザの対応状況はこんな感じ
http://caniuse.com/#search=service%20workers

特徴

  • DOM にアクセスできない
    • DOM を操作したい場合は、Service Worker がコントロールしているページ(js)と postMessage でメッセージのやり取りをして行う
  • リクエストをプロキシすることが可能
  • Service Worker はブラウザが必要に応じて起動・終了するので、変数の値を保持しておけない
    • Cache、IndexedDB 等で値を保存して、必要になった時に取り出すようにする
  • Promise を多用する
  • httpslocalhost 上でしか動作しない

ライフサイクル

Web ページとは全く異なるライフサイクルで動作する

ServiceWorkerLifeCycle.png

赤文字はその時に発火するイベント

登録

Service Worker を使うにはまず register()関数を呼びだして登録する
すでに登録されているかどうかはブラウザがチェックしてくれるので気にせず呼べばいい

navigator.serviceWorker.register('/service-worker.js');

登録時に重要なのがスコープ
スコープとは Service Worker がコントロールするページのこと
スコープは、service-worker.js が存在する階層が自動的に設定される
また、下記のように明示的に設定することも可能

navigator.serviceWorker.register('/service-worker.js', {scope: '/example'});

上記の場合、/example 配下のページが Service Worker にコントロールされる
つまり、「 http://www.example.com/example/page.html 」はコントロールされるが
http://www.example.com/index.html 」はコントロールされないということ

また、register()で登録し service-worker.js が更新されている場合はonupdatefoundイベントが発火する(初回登録時も)

インストール

Service Worker を新規インストール、もしくは、Service Worker が更新されていると installing 状態になる
この時、oninstall イベントも発火する
インストール時に何か処理させたい場合は、下記のように Service Worker 内で oninstall イベントを監視する

// service-worker.js
self.addEventListener('install', (event) => {
  // 何かの処理
});

更新

ブラウザが保持している Service Worker と、これからダウンロードしようとしている Service Worker に
1byteでも違いがあれば更新されたと判断される

ブラウザをコントロールしている Service Worker が存在しなければ installing 状態のあと
すぐに、active 状態になるが、コントロールしている Service Worker が存在する場合は
waiting 状態に移行する

ServiceWorker更新時.png

なぜすぐに active 状態にしないかというと、古い Service Worker が保持しているデータを
そのまま新しい Service Worker が使おうとした場合、不整合が起きる可能性があるからです
その後、安全な状態(ページが閉じられるなど)になると waiting 状態から active 状態に移行します

Tips

更新時に installing 状態からすぐに active 状態に移行させたい

Service Worker 更新時はデータの不整合等を防ぐために waiting 状態に移行し
安全な状態になるのを待つようになっている

すぐに active 状態にしても問題ない場合は、skipWaiting()を呼ぶことで active 状態にすることができる

// service-worker.js
self.addEventListener('install', (event) => {
  event.waitUntil(self.skipWaiting());
});

waitUntil()はこの関数が呼ばれたイベント終了のライフタイムをその処理が終わるまで待つ
(この関数はよく使うので覚えておくといいです)

active 状態になったらすぐにコントロールさせたい

Service Worker は active 状態になってもすぐにブラウザをコントロールしない
コントロールするのは次にページが表示された時

claim()を呼ぶことすぐにコントロールさせることができる

// service-worker.js
self.addEventListener('activate', (event) => {
  event.waitUntil(self.clients.claim());
});

ここまで、Service Worker の基本を説明してきました
ここからは Service Worker を使ってできることを説明していきます

サンプルコードはここにあるので見てください
動作は、Google Chrome 52.0.2743.116 m (64-bit)で確認済みです

Cache編

Cache API を使用することでオフライン状態でもリソースを取得できるようにします

動作サンプルはこちら
一度オンライン状態でページを取得し、その後オフライン状態で再読み込みしてみてください
オフライン状態でもリソースが取得できることが確認できるはずです

ページ

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Cache Test</title>
    <link rel="stylesheet" href="./styles/main.css">
</head>
<body>
    <h1>Service Worker Cache Test</h1>
    <img src="./images/image.jpg">
    <script src="./script/jquery-3.1.0.js"></script>
    <script src="./script/main.js"></script>
</body>
</html>

CSS、イメージ、スクリプトのリクエストをするだけの簡単なページです
main.js は Service Worker を登録するスクリプトです

Service Worker 登録スクリプト

// main.js
navigator.serviceWorker.register('./service-worker.js')
                       .catch(console.error.bind(console));

Service Worker の登録を行うスクリプトです
register で登録をしてエラーがあった時はコンソール表示するだけの簡単なものです

Service Worker

ちょっと長いです
いったん、全ソースを紹介して、その後細かく説明していきます

// service-worker.js
'use strict';

const CACHE_NAME = 'cache-v1';
const urlsToCache = [
    './',
    './styles/main.css',
    './images/image.jpg',
    './script/main.js'
];

self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open(CACHE_NAME)
              .then((cache) => {
                  console.log('Opened cache');
                  
                  // 指定されたリソースをキャッシュに追加する
                  return cache.addAll(urlsToCache);
              })
    );
});

self.addEventListener('activate', (event) => {
    var cacheWhitelist = [CACHE_NAME];

    event.waitUntil(
        caches.keys().then((cacheNames) => {
            return Promise.all(
                cacheNames.map((cacheName) => {
                    // ホワイトリストにないキャッシュ(古いキャッシュ)は削除する
                    if (cacheWhitelist.indexOf(cacheName) === -1) {
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
});

self.addEventListener('fetch', (event) => {
    event.respondWith(
        caches.match(event.request)
              .then((response) => {
                  if (response) {
                      return response;
                  }
            
                  // 重要:リクエストを clone する。リクエストは Stream なので
                  // 一度しか処理できない。ここではキャッシュ用、fetch 用と2回
                  // 必要なので、リクエストは clone しないといけない
                  let fetchRequest = event.request.clone();
            
                  return fetch(fetchRequest)
                      .then((response) => {
                          if (!response || response.status !== 200 || response.type !== 'basic') {
                              return response;
                          }
                    
                          // 重要:レスポンスを clone する。レスポンスは Stream で
                          // ブラウザ用とキャッシュ用の2回必要。なので clone して
                          // 2つの Stream があるようにする
                          let responseToCache = response.clone();
                    
                          caches.open(CACHE_NAME)
                                .then((cache) => {
                                    cache.put(event.request, responseToCache);
                                });
                    
                          return response;
                      });
              })
    );
});

install時

const CACHE_NAME = 'cache-v1';
const urlsToCache = [
    './',
    './styles/main.css',
    './images/image.jpg',
    './script/main.js'
];

self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open(CACHE_NAME)
              .then((cache) => {
                  // 指定されたリソースをキャッシュに追加する
                  return cache.addAll(urlsToCache);
              })
    );
});

CACHE_NAME はキャッシュに保存する時の名前です
Cacheキャッシュ名1.png
DevTools の Application タブを開くと左側に Cache という項目があり
↑で指定した名前で保存されていることがわかります

CacheStorage.open('キャッシュ名')でキャッシュを開き
urlsToCache で指定されているリソースを Cache.addAll でキャッシュに保存します

activate時

self.addEventListener('activate', (event) => {
    var cacheWhitelist = [CACHE_NAME];

    event.waitUntil(
        caches.keys().then((cacheNames) => {
            return Promise.all(
                cacheNames.map((cacheName) => {
                    // ホワイトリストにないキャッシュ(古いキャッシュ)は削除する
                    if (cacheWhitelist.indexOf(cacheName) === -1) {
                        return caches.delete(cacheName);
                    }
                })
            );
        })
    );
});

この activate 時の処理ですが、新規インストール時は何も処理されません

ここでは、キャッシュ名がcache-v2になった時の処理を説明します

  1. キャッシュ名が変わったので Service Worker が更新されたと判断され、install イベントが発火します
    そして、キャッシュ名cache-v2で保存されます
    Cacheキャッシュ名2.png
    このようにキャッシュが2つある状態になります
  2. 安全な状態になり、再度ページを開いた時に activate イベントが発火します
    CACHE_NAMEcache-v2になっているのでcacheWhitelistにはcache-v2が設定されます
    cache.keysによって保存されているキャッシュの名前が列挙され、ホワイトリストにないcache-v1(古いキャッシュ)が削除されます
    Cacheキャッシュ名3.png
    cache-v1が削除されていることがわかる

fetch時

self.addEventListener('fetch', (event) => {
    event.respondWith(
        caches.match(event.request)
              .then((response) => {
                  if (response) {
                      return response;
                  }
            
                  // 重要:リクエストを clone する。リクエストは Stream なので
                  // 一度しか処理できない。ここではキャッシュ用、fetch 用と2回
                  // 必要なので、リクエストは clone しないといけない
                  let fetchRequest = event.request.clone();
            
                  return fetch(fetchRequest)
                      .then((response) => {
                          if (!response || response.status !== 200 || response.type !== 'basic') {
                              return response;
                          }
                    
                          // 重要:レスポンスを clone する。レスポンスは Stream で
                          // ブラウザ用とキャッシュ用の2回必要。なので clone して
                          // 2つの Stream があるようにする
                          let responseToCache = response.clone();
                    
                          caches.open(CACHE_NAME)
                                .then((cache) => {
                                    cache.put(event.request, responseToCache);
                                });
                    
                          return response;
                      });
              })
    );
);

Service Worker がブラウザをコントロールしている時にリソースのリクエストが発生すると
fetch イベントが発火します
fetch イベントで何もしなければ、普通にネットワーク経由でリクエストが処理されます

ここで行っていることは単純で、リクエストが来たリソースがキャッシュに保存されていればそれを返し、なければサーバーにリクエストを投げ、返って来たリソースをキャッシュに保存しつつ、クライアントに返しているだけです
install 時にキャッシュに保存していなくても、こうすることでキャッシュすることが可能です

重要なことは、リクエストとレスポンスはStreamなので使い回すことができないようです
フェッチしたり、キャッシュしたりする場合は clone する必要があるようです

Cacheについての説明はこんな感じです
実際に、オフライン状態でページを読み込んでみると CacheDevTools.png
Service Worker を使ってリソースを取得できてるのがわかります

Push Notification編

ネイティブアプリのようにプッシュ通知をブラウザに送る方法を説明します

プッシュ通知を試したい場合は、ここにあるリポジトリをクローンし
下記を参考に適宜修正してください
修正が必要なのは、manifest.json の gcm_sender_id と push.js の APIキー、エンドポイントなどです

ページ

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Push Test</title>
    <link rel="manifest" href="./manifest.json">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/primer/2.0.2/primer.css">
</head>
<body>
    <h1>Service Worker Push Test</h1>
    <dl class="form">
        <dt><label>Endpoint URL</label></dt>
        <dd><textarea id="subscription-endpoint" class="input-block"></textarea></dd>
        <dt><label>Auth</label></dt>
        <dd><textarea id="subscription-auth" class="input-block"></textarea></dd>
        <dt><label>Public Key</label></dt>
        <dd><textarea id="subscription-public-key" class="input-block"></textarea></dd>
    </dl>
    <script src="./script/main.js"></script>
</body>
</html>

ページは manifest.json の読み込み以外、大したことはしていません
プッシュ通知に必要な情報を表示するための要素があるぐらいです

manifest.json

{
  "name": "SW Push Notification",
  "short_name": "SW Push Notification",
  "icons": [{
    "src": "https://kanatapple.github.io/service-worker/push/images/image.jpg",
    "sizes": "192x192",
    "type": "image/jpg"
  }],
  "start_url": "./index.html",
  "display": "standalone",
  "gcm_sender_id": "621388437768"
}

プッシュ通知する際は必要なので用意します(詳細はここを参考に)
重要なのはgcm_sender_idです
gcm_sender_idには後述する GCM(Google Cloud Messaging)、FCM(Firebase Cloud Messaging)を使用するプロジェクトID(送信者ID)を指定します

Service Worker 登録スクリプト

今回のサンプルではメッセージをペイロードするために必要な情報を表示するようにしています
メッセージをペイロードする必要がない時は、エンドポイントだけでOKです

document.addEventListener('DOMContentLoaded', () => {
    let endpoint = document.querySelector('#subscription-endpoint');
    let key = document.querySelector('#subscription-public-key');
    let auth = document.querySelector('#subscription-auth');
    
    navigator.serviceWorker.register('./service-worker.js');
    navigator.serviceWorker.ready
             .then((registration) => {
                 return registration.pushManager.subscribe({userVisibleOnly: true});
             })
             .then((subscription) => {
                 var rawKey = subscription.getKey ? subscription.getKey('p256dh') : '';
                 key.value = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : '';
        
                 var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '';
                 auth.value = rawAuthSecret ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : '';
        
                 endpoint.value = subscription.endpoint;
                 console.log(`GCM EndPoint is: ${subscription.endpoint}`);
             })
             .catch(console.error.bind(console));
}, false);

重要なのは registration.pushManager.subscribe({userVisibleOnly: true})
subscribe することでプッシュ通知を購読するようになります

subscribe には {userVisibleOnly: true} を渡す必要があると記載があったんですが
false で渡しても、引数を渡さなくても通知されるので、ちょっと謎です・・
(何かわかったら追記します)

subscribe が終わったら、公開鍵、鍵生成に使用する乱数、エンドポイントを画面上に表示してます
これらの値をプッシュ通知の際に使用します

Service Worker

/*
    fetch以外は省略。リポジトリのコードを見てください
*/

self.addEventListener('push', (event) => {
    console.info('push', event);
    
    const message = event.data ? event.data.text() : '(・∀・)';
    
    event.waitUntil(
        self.registration.showNotification('Push Notification Title', {
            body: message,
            icon: 'https://kanatapple.github.io/service-worker/push/images/image.jpg',
            tag: 'push-notification-tag'
        })
    );
});

メッセージをペイロードしている場合は、event.data にデータが入っているので
text()を呼ぶとメッセージを取得できます

あとは、showNotificationに必要なデータを渡すと通知が表示されます

プッシュ通知.png

showNotificationシグネチャはこんな感じ

Promise<void> showNotification(DOMString title, optional NotificationOptions options);

dictionary NotificationOptions {
  NotificationDirection dir = "auto";
  DOMString lang = "";
  DOMString body = "";
  DOMString tag = "";
  USVString icon;
  USVString badge;
  USVString sound;
  VibratePattern vibrate;
  DOMTimeStamp timestamp;
  boolean renotify = false;
  boolean silent = false;
  boolean noscreen = false;
  boolean requireInteraction = false;
  boolean sticky = false;
  any data = null;
  sequence<NotificationAction> actions = [];
};

この辺を参考に
https://notifications.spec.whatwg.org/#dom-serviceworkerregistration-getnotificationsfilter
https://developer.mozilla.org/ja/docs/Web/API/ServiceWorkerRegistration/showNotification

プロジェクト登録

プッシュ通知を送信する場合は、Google Developers Console、Firebase Console に
プロジェクトを作成する必要があります
現在は Firebase の使用を推奨してるらしいですが、両方説明しときます

Google Developers Console

https://console.developers.google.com/

1.左上のプロジェクト作成を選択 Googleプロジェクト作成1.png

2.プロジェクト名を設定して、作成 Googleプロジェクト作成2.png

3.APIの有効化 GoogleAPI1.png
ライブラリから「Google Cloud Messaging」を探して

GoogleAPI2.png
有効化します

4.認証情報の作成 Google認証情報作成1.png
プロジェクトを使用するには認証情報が必要らしいので、「認証情報に進む」をクリック

Google認証情報作成2.png
APIを呼び出す場所で「ウェブブラウザ(Javascript)」を選択して

Google認証情報作成3.png
APIキーに適当な名前を付けて「APIキーを作成する」をクリック

完了

Firebase

https://console.firebase.google.com/

Firebase の場合、新規作成するか Google Developers Console に作成したプロジェクトをインポートすることができます
今回は新規作成を説明します

1.プロジェクト作成をクリック
Firebaseプロジェクト作成1.png

2.プロジェクト名を設定して、作成
Firebaseプロジェクト作成2.png

完了

Firebaseで作成すると、プロジェクトを作成すると
自動的にクラウドメッセージング用のAPIと送信者IDを作成してくれるので簡単です

通知を送信

それでは実際に通知を送信してみます
通知には送信者IDとAPIキーが必要なので、Google Developers Console、Firebase で調べます

Google Developers Console

送信者ID

Google送信者ID.png

APIキー

GoogleAPIキー.png

Firebase

送信者IDとAPIキー

Firebase情報.png

manifest.jsonの修正

gcm_sender_idに送信者IDを設定します

{
  "name": "SW Push Notification",
  "short_name": "SW Push Notification",
  "icons": [{
    "src": "https://kanatapple.github.io/service-worker/push/images/image.jpg",
    "sizes": "192x192",
    "type": "image/jpg"
  }],
  "start_url": "./index.html",
  "display": "standalone",
  "gcm_sender_id": "118577160855"
}

送信

今回、送信用のライブラリにweb-pushを使います
push.js を用意します

// push.js
'use strict';

const push = require('web-push');

const GCM_API_KEY = '********';
push.setGCMAPIKey(GCM_API_KEY);

const data = {
    'endpoint': '********',
    'userAuth': '********',
    'userPublicKey': '********'
};

push.sendNotification(data.endpoint, {
    payload:       'push test for service worker',
    userAuth:      data.userAuth,
    userPublicKey: data.userPublicKey,
})
    .then((result) => {
        console.log(result);
    })
    .catch((err) => {
        console.error('fail', err);
    });

GCM_API_KEYに上記で調べたAPIキーを記述します
endpointuserAuthuserPublicKeyにページを表示した時に表示される情報を設定します

設定が終了したら下記コマンドを実行します

node push.js

プッシュ通知.png

こんな感じで通知が来ます

Background Sync編

オフライン時にデータを保持しておいて、オンラインになった時にバックグラウンドでデータを送信する方法を説明します

デモ動画

今回はどういう仕組みで動くのかだけ説明します

ページ

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Sync Test</title>
</head>
<body>
    <h1>Service Worker Sync Test</h1>
    <button id="button">sync</button>
    <script src="./script/main.js"></script>
</body>
</html>

syncボタンがあるだけで、他は特殊なものはありません

Service Worker 登録スクリプト

navigator.serviceWorker.register('./service-worker.js');
navigator.serviceWorker.ready
         .then((registration) => {
             document.getElementById('button').addEventListener('click', () => {
                 // ここでIndexedDBなどにデータを保存しておく
                 // 保存が終わったら、↓を呼ぶ
                 registration.sync.register('sync-test')
                             .then(() => {
                                 console.log('sync registerd');
                             })
                             .catch(console.error.bind(console));
             }, false);
         })
         .catch(console.error.bind(console));

登録については他と一緒です

ボタンをクリックした時にサーバに送信したいデータを IndexedDB などに保存します
Service Worker はブラウザが必要に応じて終了させてしまうので、変数の値を保持しておけないためです
グローバル変数に入れておいてもダメということ)

データの保存が終わったらregistration.sync.registerを呼びます
引数にはタグ名を設定します(このタグ名を IndexedDB に保存するキーとかにしておくといい)

Service Worker

/*
    sync以外は省略。リポジトリのコードを見てください
*/

self.addEventListener('sync', (event) => {
    console.info('sync', event);
    
    // ここでIndexedDBからデータを取得して、サーバに送信する
});

sync が登録されて、Service Worker がサーバと Sync できる状態にあると判断した場合
この sync イベントが発火します
オンライン状態で sync が登録されたら、すぐに sync イベントが発火します
オフライン状態で sync が登録されたら、オンライン状態になった時に sync イベントが発火します

event.tag に sync 登録時に設定したタグ名が設定されています BackgroundSyncTag.png

このタグ名を使って IndexedDB に保存しておいたデータを取得しサーバにデータを送信します
こんな感じで、確実にデータを送信することができます


仕組みはこんな感じです
ここにサンプルがあるので、Developer Toolsを開きながら
オフラインの時にsyncボタン、オンラインの時にsyncボタンを押して挙動を確認してみてください

参考

http://www.html5rocks.com/ja/tutorials/service-worker/introduction/
https://blog.jxck.io/entries/2016-04-24/service-worker-tutorial.html

node-inspectorなしでNode.jsをデバッグする

Node.jsをデバッグする時は「node-inspector」を使うことが多いです

www.npmjs.com

ですが、「node-nightly」版を使うと「node-inspector」なしでデバッグすることが可能です!
v6.3.0で標準実装されました!(2016/07/07追記)

準備

ダウンロード

「node-nightly」版を落としてきます

https://nodejs.org/download/nightly/v7.0.0-nightly20160621ecc48a154d/

↑ここに環境ごとのバイナリファイルがあるのでダウンロードします

解凍

tar zxvf node-v7.0.0-nightly20160621ecc48a154d-darwin-x64.tar.gz

解凍したら、binフォルダに実行ファイルがあるので、適当な場所にコピーします

サンプルプログラム

今回は、expressを使った簡単なサンプルプログラムを用意します

インストール

npm init -y
npm install -S express

ソース

const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send('Hello, World!');
});

app.listen(3000);

デバッグ

Nightly版の場合は

node-nightly --inspect --debug-brk app.js

v6.3.0が入ってる方は

node --inspect --debug-brk app.js

「--inspect」フラグを付けることでデバッグが可能です
実行時に最初の行で止めたい場合は、「--debug-brk」フラグを付けると止まります

実行すると、↓このように表示されるので

f:id:jewelofren:20160626171115p:plain

chrome-devtools://...」の部分をコピーしChromeで開きます
そうすると↓このように最初の行で止まった状態になるので、任意の場所にブレイクポイントを貼ります

f:id:jewelofren:20160626172413p:plain

今回は、「http://localhost:3000」にリクエストが来たら止まるようにしました

http://localhost:3000」にアクセスしてみると↓このようにブレイクポイントで止まり
変数の中身などが確認できます

f:id:jewelofren:20160626172903p:plain

Node.jsでパッケージのバージョンを固定する方法

package.jsonのdependencies、devDependenciesに記述されるバージョンは

semver 日本語版

上記の仕様で記述されます

なので、環境(インストールするタイミング等)によってバージョンが異なってしまうことがよくあります

バージョンを固定する方法

shrinkwrap

npm shrinkwrap

パッケージを install した後に上記コマンドを叩く
そうするとnpm-shrinkwrap.jsonというファイルが作成され、npm installした時に
ここに記述されているバージョンでインストールされる

exact

npm install パッケージ名 [--save|--save-dev] [--exact|-E]

インストール時に「--exact|-E」を指定する
そうすると、package.jsonに下記のように記述されるのでバージョンが固定されます
(チルダやキャレットが付かないので)

"パッケージ名": "0.1.6"

まとめ

どちらの方法でバージョン固定できますが、「exact」の方が別ファイルが作られないのでいいかも

Angular2でComponentを再帰的に処理する

Componentを再帰的に処理したいと思い、試してみたところ普通にできたのでやり方を紹介します
Objectの構造やディレクトリの表示とか再帰処理が必要なところで使えそうです
今回はObjectの構造を表示するサンプルです

再帰処理させたいComponent

@Component({
  selector: 'object-view',
  template: `
    <ul>
      <li *ngFor="#property of getKeys()">
        {{property}}: 
        <span *ngIf="!isObject(property)">
          {{getValue(property)}}
        </span>
        <span *ngIf="isObject(property)">
          <object-view [object]="getValue(property)"></object-view>
        </span>
      </li>
    </ul>
  `,
  directives: [ObjectViewComponent]
})
export class ObjectViewComponent {
  @Input() object: any;
  
  private getKeys(): string[] {
    return Object.keys(this.object);
  }
  
  private getValue(property: string): any {
    return this.object[property];
  }
  
  private isObject(property: string): boolean {
    const value = this.getValue(property);
    return !Array.isArray(value) && typeof value === 'object';
  }
}

directivesで自分自身を子Componentとして指定します
そして、templateの部分を見てもらうとわかりますが、プロパティの値がObjectだったら
自身のセレクターで指定したタグをレンダリングします
きちんとObjectかどうかの判定を入れないとブラウザが死ぬので注意w

あとは、親Componentで子Componentの@Inputディレクティブにobjectを渡すだけです

@Component({
  selector: 'my-app',
  template: `
    <object-view [object]="object"></object-view>
  `,
  directives: [ObjectViewComponent]
})
export class App {
  private object: any = {
    string: 'string',
    boolean: true,
    number: 0,
    array: ['a', 'b', 'c'],
    object: {
      prop1: 'prop1',
      prop2: 100
      prop3: {
        array: ['999', '998', '997']
      }
    }
  }
}

動作サンプル

まとめ

ハマるかと思ったけど、普通に再帰処理できる