Rust始めました
随分久しぶりになってしまいました
さて、今年になってから Rust を触り始めてます
なぜ Rust を触り始めたかと言うと
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 を多用する
- https か localhost 上でしか動作しない
ライフサイクル
Web ページとは全く異なるライフサイクルで動作する
赤文字はその時に発火するイベント
登録
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 状態に移行する
なぜすぐに 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 はキャッシュに保存する時の名前です
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
になった時の処理を説明します
- キャッシュ名が変わったので Service Worker が更新されたと判断され、install イベントが発火します
そして、キャッシュ名cache-v2
で保存されます
このようにキャッシュが2つある状態になります - 安全な状態になり、再度ページを開いた時に activate イベントが発火します
CACHE_NAME
はcache-v2
になっているのでcacheWhitelist
にはcache-v2
が設定されます
cache.keys
によって保存されているキャッシュの名前が列挙され、ホワイトリストにないcache-v1
(古いキャッシュ)が削除されます
↑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についての説明はこんな感じです
実際に、オフライン状態でページを読み込んでみると
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
に必要なデータを渡すと通知が表示されます
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.左上のプロジェクト作成を選択
2.プロジェクト名を設定して、作成
3.APIの有効化
ライブラリから「Google Cloud Messaging」を探して
有効化します
4.認証情報の作成
プロジェクトを使用するには認証情報が必要らしいので、「認証情報に進む」をクリック
APIを呼び出す場所で「ウェブブラウザ(Javascript)」を選択して
APIキーに適当な名前を付けて「APIキーを作成する」をクリック
完了
Firebase
https://console.firebase.google.com/
Firebase の場合、新規作成するか Google Developers Console に作成したプロジェクトをインポートすることができます
今回は新規作成を説明します
1.プロジェクト作成をクリック
2.プロジェクト名を設定して、作成
完了
Firebaseで作成すると、プロジェクトを作成すると
自動的にクラウドメッセージング用のAPIと送信者IDを作成してくれるので簡単です
通知を送信
それでは実際に通知を送信してみます
通知には送信者IDとAPIキーが必要なので、Google Developers Console、Firebase で調べます
Google Developers Console
送信者ID
APIキー
Firebase
送信者IDとAPIキー
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キーを記述します
endpoint
、userAuth
、userPublicKey
にページを表示した時に表示される情報を設定します
設定が終了したら下記コマンドを実行します
node push.js
こんな感じで通知が来ます
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 登録時に設定したタグ名が設定されています
このタグ名を使って 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」を使うことが多いです
ですが、「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」フラグを付けると止まります
実行すると、↓このように表示されるので
「chrome-devtools://...」の部分をコピーしChromeで開きます
そうすると↓このように最初の行で止まった状態になるので、任意の場所にブレイクポイントを貼ります
今回は、「http://localhost:3000」にリクエストが来たら止まるようにしました
「http://localhost:3000」にアクセスしてみると↓このようにブレイクポイントで止まり
変数の中身などが確認できます
Node.jsでパッケージのバージョンを固定する方法
package.jsonのdependencies、devDependenciesに記述されるバージョンは
上記の仕様で記述されます
なので、環境(インストールするタイミング等)によってバージョンが異なってしまうことがよくあります
バージョンを固定する方法
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'] } } } }
まとめ
ハマるかと思ったけど、普通に再帰処理できる