HTML5Experts.jp https://html5experts.jp 日本に、もっとエキスパートを。 Tue, 06 Sep 2016 00:25:47 +0000 ja hourly 1 http://wordpress.org/?v=4.2.9 React Nativeことはじめ─Hello,Worldからネイティブ連携まで https://html5experts.jp/shohey1226/20480/ https://html5experts.jp/shohey1226/20480/#comments Tue, 06 Sep 2016 00:16:23 +0000 https://html5experts.jp/?p=20480 連載: Web技術でアプリ開発2016 (1)

モバイルアプリ開発におけるWeb技術の可能性を探る特集・第一弾でご紹介するのはReact Native。その生い立ち、チュートリアルから、コードベースやUIを記述する言語、パフォーマンスまで解説します。

概要

React Nativeは2013年にFacebook社内のハッカソンで生まれたプロジェクトです。2014年にiOSアプリのFacebook Ads ManagerをReact Nativeを用いて開発し、2015年3月にオープンソースとして公開されました。そして、半年後の2015年9月にAndroidをサポートし、今年のF8では、MicrosoftがReact NativeでWindowsプラットフォーム(PC, Mobile, Xbox)の開発ができます、という発表がありました。また、macOSアプリの開発Ubuntuアプリの開発など他のプラットフォームの開発もできるようにするプロジェクトも走っているようです。

React Nativeのコンセプトは、”Write Once, Run Anywhere”(一つのコードでどこでも動く)ではなく、”Learn Once, Run Anywhere”(一度学べば、どこでも動かせる)です。一度React Nativeを理解すれば、どのプラットフォーム上でも同様のコーディングを行って、プラットフォームネイティブなアプリケーションを開発することが出来ます。Githubのスター数は35,000を超え、リリースの頻度も多く、グローバルな視点でみると、非常に熱い技術であると言えます。

(編集部注: 「Write Once, Run Anywhere」は、あらゆるプラットフォームで同一のコードが動作するという、Javaが喧伝していたコンセプトです)

Hello, World

以下は、macOS上で開発することを前提としたチュートリアルです。

React Native開発に必要なものは、 Node.js, React Nativeのコマンドラインツール、Watchman(Facebookによって開発されているファイル監視ツール)になります。Homebrewで下記のようにインストールします。

$ brew install node
$ brew install watchman
$ npm install -g react-native-cli
$ npm -v
3.6.0
$ node -v
v5.0.0

iOS

iOSの場合はXcodeが必要です。XcodeはMac App Storeからインストールしましょう。 準備が整ったところで、プロジェクトを作って走らせてみます。

$ react-native init AppByWebTech2016
$ cd AppByWebTech2016
$ react-native run-ios

Simulator Screen Shot 2016.08.27 9.57.40

Android

Androidの場合は、Android Studioが必要なのでインストールしておきます。アプリの起動には、実機をUSBで繋げておくか、Emulatorが必要なので適宜用意します。今回は、Genymotion(個人利用は無料)を起動しておいて, 下記のコマンドを走らせます。

$ react-native run-android

スクリーンショット 2016-08-27 10.32.12

NativeBaseを使う

これだけだと味気ないので、NativeBaseを用いてUIを作ってみたいと思います。NativeBaseはWebでいうBootstrapのようなものです。ボタン等のUIを自分でデザインする必要がなく見栄えの良いインターフェイスを簡単に作ることができます。

では、NativeBaseをインストールしてみましょう。(※react-nativeのバージョンは0.32)

$ cd AppByWebTech2016
$ npm install native-base --save
$ react-native link react-native-vector-icons

index.ios.jsを下記のように変更すると、

import React, { Component } from 'react';
import { Container, Content, Button, Header, Title, List, ListItem, Badge } from 'native-base';
import { Col, Row, Grid } from 'react-native-easy-grid';
import { AppRegistry, Text, View } from 'react-native';

class AppByWebTech2016 extends Component {
  render() {
    return (
      <Container>
        <Header>
          <Title>Hello World</Title>
        </Header>
        <Content>
          <List>
            <ListItem >
              <Badge>1</Badge>
            </ListItem>
            <ListItem>
              <Badge primary>2</Badge>
            </ListItem>
            <ListItem>
              <Text>List 3</Text>
            </ListItem>
          </List>
          <Grid>
            <Row>
              <Col>
                <Button style={{margin:10}}> Click Me! </Button>
              </Col>
              <Col style={{backgroundColor: '#204d74'}}>
                <Text style={{fontSize: 20, color: 'white', margin: 10}}>React Native!</Text>
              </Col>
            </Row>
            <Row style={{backgroundColor: '#00c497'}}>
              <Text style={{fontSize: 24, color: '#333', margin: 10}}>Learn once, Write anywhere</Text>
            </Row>
          </Grid>
        </Content>
      </Container>
    );
  }
}

AppRegistry.registerComponent('AppByWebTech2016', () => AppByWebTech2016);

スタイルのことを気にせずボタンを作ることができ、グリッドなども簡単に使用できます。プロトタイピングなどにはもってこいのモジュールとなっています。

スクリーンショット 2016-08-27 14.09.20

プラットフォーム/ライブラリの特徴

項目 説明
対応プラットフォーム iOS, Android, Windows(PC, Mobile, Xbox), macOS※, Ubuntu※
コードベースは(ほぼ)完全に統一できるか? 統一できない(実際は7,8割は共有できる)
UIを記述する言語 JavaScript, React, スタイルシート
UIはネイティブか、Webか WebViewではない。ネイティブもしくはJSとのハイブリットで実現
パフォーマンス ネイティブ同等
ネイティブな機能を呼び出せるか? APIを通じて自由に呼び出せる

※コミュニティがサポート

以下に、上の表を補足します。

対応プラットフォーム

私が確認したのはiOSとAndroidですが、前述した通り、下記のプラットフォームでReact Nativeの方法でNativeアプリを動かすことができるようです。

  • iOS
  • Android
  • Windows(PC, Mobile, Xbox)
  • MacOS
  • Ubuntu
  • Browser※1

※1React Native for WebというChrome, Firefox, Safari >= 7, IE 10, Edge.のBrowser上でReact Nativeコードを動かすというプロジェクトさえ存在します。

コードベースは(ほぼ)完全に統一できるか?

残念ながら完全には統一できません。現実には各プラットフォームの作法(例えば、iOSはtabbarは下部、Androidは上部) が存在し、完全に同じにすべきかというのは別の議論が必要な気がします。ただ85%は同じコードを再利用できたといった話もないわけではありません。ビジネスロジック部分は統一させ、UIは個別のコードで実現するというのが、コードの再利用+ネイティブの作法の両面を考慮に入れたReact Nativeの流儀だと言ってよいでしょう。

UIを記述する言語

コードはJavaScript(ES6), コンポーネントはReact, デザインはスタイルシートを用いて記述していきます。 スタイルシートはCamel記法となるのでCSSのbackground-colorがbackgroundColorという属性になり、非常に馴染みの深いものになります。 React Nativeは、レイアウトを行うためにFlexboxを用います。最近のWebブラウザでは広くサポートされているので、ご存じの方も多いことでしょう。

ES6やReactは確かに学習コストがあります。しかし、Web業界で生きているひとは少なくとも触っておくべき技術でしょう。React Nativeでのアプリ開発の経験が逆にWebに生かすというようなことも起きるかもしれません。

パフォーマンス

ネイティブのコンポーネントを利用する限り、ネイティブと同等のパフォーマンスになると言ってよいでしょう。パフォーマンスの最適化を開発者に意識させないようにすることがReact Nativeが実現しようとしていることです。

しかし、公式のドキュメントによると、現実には何点か難しい箇所があると述べられています。

React Nativeは下図のようにスレッドが走っています。

8E2942F7-B660-46DC-85BA-58DA362472DB

React.js Conf 2016 – Tadeu Zagallo – Optimising React Native: Tools and Tipsより転載

端的に言うと、JavaScriptのビジネスロジックが動くJSスレッド上で時間のかかる処理を行うと、UI出力時に遅延が起きてしまうケースがあります。 もしパフォーマンスに影響が出たときは、このJSスレッドを意識してコードを見直してみる必要があります。

今年のReact Confの発表でも言及されていましたが、Facebookは社内にReact Nativeのパフォーマンスチームを作ってパフォーマンスを継続的に改善しています。彼らがリソースを割いて注力している部分でもあるので、これからも改善されていく部分だと思います。

UIはネイティブか、Webか

WebViewを使わないという観点でWebではありません。ネイティブのライブラリを呼び出すAPIが多いですが、JSとのコードのハイブリッドといったイメージでしょうか。UIの動作は非常にネイティブライクです。

ネイティブな機能を呼び出す方法

React Nativeとネイティブ間を受け渡すようなAPIが用意されています。 これを使うことで容易にネイティブからの値を受け取ったり、受け渡したりすることができます。 この程よい抽象化があることで、現行のiOSアプリにも組み込むことが可能となります。 実際、Facebookのアプリでも一部はReact Nativeで書かれてると言っています。

今回は非常に簡単な例ですが、React Native側で2つの数字を渡しネイティブ側で和を求めて返すメソッドを用いて説明します。(今回はiOS)

まずは、Xcodeでios/AppByWebTech2016.xcodeprojをOpenし、新規作成からCocoaClassを選び、適当なSubクラスを選びます。

ED821C61-EBBF-4046-B1F7-C2C177606302

EF401AE0-3C72-41CE-BA43-95D68531F7B6

すると、AppNativeSum.hとAppNativeSum.mができると思います。 このファイルを下記のように書き換えます。RCT_*(ReaCTの略らしい)の接頭辞がついたAPIをReact Nativeが提供しています。Callbackを用いて返す方法もありますが、今回はPromiseを使います。

#import "RCTBridgeModule.h"

@interface AppNativeSum : NSObject &lt;RCTBridgeModule

@end

#import "AppNativeSum.h"

@implementation AppNativeSum

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(sumNumber:(int)val1 val2:(int)val2 resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)
{
  NSInteger val3 = val1 + val2;
  NSString *val3Str = [NSString stringWithFormat:@"%ld", (long)val3];
  resolve(val3Str);
}

@end

このメソッドの戻り値は、JavaScriptのPromiseです。通常のPromiseと同様に、async/awaitを使って非同期処理の結果を処理することも可能です。

import { NativeModules } from 'react-native';
var AppNativeSum = NativeModules.AppNativeSum;

class AppByWebTech2016 extends Component {

  constructor(props){
    super(props);
    this.state = {
      sum: 0
    }
  }

  async componentDidMount(){
    let sum = await AppNativeSum.sumNumber(1,2);
    this.setState({sum: sum});
  }
...

ネイティブコードに精通していれば、公式ドキュメントにあるAPIに渡すだけで容易にReact Nativeとネイティブのやりとりができます。

まとめ

マレーシアの友人が、彼の国では企業がReact Nativeエンジニアの採用をしていると言っていました(中国でも似たような状況にあるとか)。 日本でもちらほらReact Nativeを採用する会社がでてきているようです。一年半以上経過し、リリース速度が多少遅くなっているといってもどんどん新しいバージョンが出てきて、かつよりよいUIモジュールもでてきています。今こそ、React Nativeを始めてみてはいかがでしょうか。

]]>
https://html5experts.jp/shohey1226/20480/feed/ 0
Qiitaのスライドモードは、mizchiが勝手に作った!?─Incrementsの縛られない開発スタイルを聞いてみた https://html5experts.jp/miyuki-baba/20328/ https://html5experts.jp/miyuki-baba/20328/#comments Mon, 05 Sep 2016 00:00:19 +0000 https://html5experts.jp/?p=20328 及川卓也さんや田中洋一郎さんをはじめ、著名なエンジニアが次々と入社していることで話題のIncrements。8月にはさらにCSSのコードフォーマッターであるStylefmtの作者・morishitterこと森下雅章さんを迎えるなど、さらに開発陣営を強化しています。

今回はさっそく森下さんにも加わっていただき、白石俊平編集長を聞き手に、CTOの髙橋侑久さん、フロントエンドエンジニアmizchiさん、デザイナーの東峰裕之さんに、「Qiita」の開発環境や開発スタイルなどについて聞いてみました。

特定領域でとんがってるスペシャリストが増えてきた

白石:まずは、自己紹介とQiitaの開発チームでの役割についてお聞かせください。

髙橋:IncrementsのCTOを務めている髙橋です。メンバーの中では一番古株ですね。Qiitaは最初、CEOの海野が開発していたのですが、それを引き継ぐかたちでQiitaのプロダクト開発からインフラまでのすべてを見てきました。

最近はJavaScriptのエキスパートであるmizchiのように、特定領域でとんがってるメンバーも増えてきているので、エンジニアの組織作りや環境整備に注力する時間が増えています。

白石:Qiitaはもともと海野社長がコードを書いてたんでしたっけ。

髙橋:はい。Incrementsは代表の海野が大学時代にビジネスコンテストで知り合った3人で創業したんですが、その共同創業者の1人が海野にプログラミングの相談をしたことがQiitaを開発し始めたきっかけでした。

Qiitaはプログラマのための情報共有のプラットフォームで、解決しようとしている課題は明確なんです。プログラムを書く時ってGoogleで検索したり技術書で調べず、頭の中の情報だけでコードを書く人はほとんどいないじゃないですか。

でも結局自分が知りたい情報が見つからなくて、時間を無駄にしている人は多いですよね。誰もが使える情報共有基盤を作って、そういう検索時間を圧縮できたら、日本のソフトウェア開発の生産性を高めていくことができると考えたのが、Qiita誕生のきっかけです。

Qiitaの開発は2011年に始まって、2012年に起業しているんですけど、最初は3人で始めたということもあって、リーンスタートアップみたいなかんじで開発していたんですよね。僕が入社したのは2013年4月。その時にエンジニアのコミュニティに支えられながら、自分が必要だと思っていたQiitaの下書き機能を開発しました。

白石:なるほど。当時も技術系の情報を発信するブログサービスは存在していたと思うんですが、Qiitaが大ブレイクしたことから考えても、まだ不十分だったところがあったんでしょうね。

mizchi:僕は基本的にはフロントエンドの人間で、2年前に教育系のベンチャーから転職してきました。最初は「Kobito」のWindows版をElectronで作ったり。SPA(Single Page Application)とネイティブAPIをたたくアプリを作ってたんですけど、去年の9月からQiitaの開発にも関わるようになりました。

Qiitaは2011年末くらいに開発されたものなので、ちょうどそのくらいの時期にフロントエンドのフレームワークに激動の技術進化があったりして、いろいろ古くなってしまったところをどうにか秩序をもたらせないかと対応してきました。最近やっと落ち着いて、機能開発などもかなりやれるようになってきたかなあという感じです。

例えばQiitaのホーム画面のフィードや編集画面、この前足したスライド機能など、どうしても局所的にJavaScript的な密度が高くなる箇所があるので住み分けし、モジュールとしてAPIがちゃんとお互い交流できるようにきれいに設計しています。

さらに、コンポーネントで密度を厚くし、複雑化したところからはみ出さないように作っていく。でもかなり行数も多いんで、できる範囲でとにかくきれいに区切って、ひたすら分割していくみたいなことをやってました。

東峰:デザイナーの東峰です。Incrementsではデザイナーの業務範囲が結構広くて、PMに近いことからデザインの実装まで一通り行います。ユーザーヒアリングも行くし、エンジニアとコミュニケーションとりながら企画やディレクションなども行うし、GitHubにPull Requestも出すという感じです。

前職はサイボウズでグループウェアを作っていたので、入社した頃は「Qiita:Team」を担当していましたが、最近はQiitaをメインに担当しています。

森下:8月にIncrementsに入社しました。前職はサイバーエージェントで、新卒で入社してフロントエンドエンジニアをやってました。UI周りの実装だったり、CSSの設計が得意領域で、自分でCSSのフォーマッターとかスタイルガイドジェネレーターを自作したりもしてます。

縛りをつけず、最新技術もいろいろ試す

白石:では、さっそく開発環境などの話を聞いていきたいと思います。まずは、Qiita全体のアーキテクチャについて教えてください。

髙橋:アーキテクチャとしてはオーソドックスにRuby on Railsを使っています。バージョンはまだ5にはしてなくて、4.2です。データベースはMySQLを使っています。

白石:フロントエンドの開発ツールやエディタは何を使っているんですか?

mizchi:エディタには特に縛りがないですね。最近はBabelを使ってるんですけど、FlowTypeというTypeAnnotationを付けるやつを部分的に足しています。全部ではないんですけど、新規で書いた分は型を付けながら進めている部分が多いです。世間的にはTypeScriptの方がメジャーだと思うんですけど、TypeScriptはスクラッチの開発じゃないとなかなか導入が難しいので。

最初CoffeeScriptで書かれていたものをJSに変換するコンパイラとかをかけながら、ガリっと書き換えた時期があって、その頃にBabelにして、RailsのSPRocketsを使ってビルドしてました。去年はそれもできるだけ捨てて、書き直してっていうのを延々やってましたね。

白石:たしかmizchiさんって、ブログでCoffeeScriptってヤバいみたいな記事を書かれてませんでしたっけ?

mizchi:CoffeeScriptはあまりメンテされなくなってきたのと、ES2015(ES6)がすごい進化してて、新しい機能を使う時にCoffeeScriptに縛られるのはかなりのリスクがあるなと。CoffeeScript好きな人は全然いいと思うんですけど、自分のようなフロントエンドを一応攻めないといけない人は、CoffeeScriptを使うのはリスクであると認識してます。

白石:先ほどTypeAnnotationを使っていると言ってましたね。型がないのがJavaScriptのいいところでもありますが、やっぱり規模が大きくなってくると型が欲しくなってきたというところでしょうか?

mizchi:これはいろいろ考え方はあると思うんですけど、JavaScriptってとてもテストが書きづらい言語なんですよね。End to End(E2E)だったり、独立させたNodeでユニットテスト書くなりしても、なかなか書きづらい。最近はとても分量が多くなってしまっている中で、その動作を担保するものとして、静的解析っていうのは有用なんじゃないかと。まあもちろん、静的解析するからといって、テストを全く書かないというわけではないんですけれど。

白石:FlowTypeで書いたものをテストケース書くとなったら、テストケース側は何で書くんですか?

mizchi:今はNodeでユニットテストできるようにしています。Reactのサーバーサンドレンダリングは基本的にテンプレートのテストやユニットテストならできるんで。E2Eが足りないのは分かっているんですが、決め手のフレームワークが今ちょっと足りてなくて…(今現在E2Eを導入してないのは)反省しているところがあります。

白石:フロントエンドのほうは、Reactを採用してるんですね。

mizchi:はい。昔はBackbone.jsで書いたものも大分残ってはいるんですけど、新規で書く部分に関しては、Reactで書いています。Reactがいいというよりはここが古くなったから捨てるということが密結合にならずに、個別のモジュールで一気に捨てられるような、代謝のしやすい設計をすることが大事だと思っています。

白石:CSSとReactあたりって、興味がある人が多いと思います。CSSモジュールとかいろいろ聞かせてください。

森下:前職でReactを使っていてたんですが、Reactアプリの中でCSSを書いていくことに対しては自分なりの意見を持っています。CSS Modules等のCSS in JS系ツールを触ってみたりしたんですけど、僕はJSのコンポーネントの単位とCSSのコンポーネントの単位って、1対1で対応するものではないと考えています。

それはデザインの再利用する単位と機能の再利用する単位が異なるからで、よくあるcomponentsディレクトリ配下に、index.jsとindex.cssがあるみたいな構成は結構難しいのかなと思っています。

CSS in JS系ツールは、CSSそのものやその設計手法にあまり詳しくない人が、なんとなくJSのコンポーネントの単位でスタイルの影響範囲を閉じたい、という場合は良いものだと思います。QiitaとQiita:TeamはHTMLのレンダリングを全てReactが行っているわけではないので、部分的にCSS in JSを導入するのもおかしいので使いたくないです。

白石:じゃあ、もうコンポーネントにスタイルは閉じない?

森下:そうですね、Reactのコンポーネントの単位ではCSSのコンポーネントは作らない。

白石:Web ComponentsでCSSを閉じさせられる、例えばShadow DOMとかだと思うんですけど、それはちょっとどうかなって思ってるかんじですか?

森下:Shadow DOMでスタイルの影響範囲に抑られるのはいいことだと思ってますが、そもそも装飾と機能を合わせたものをコンポーネントとしていろんなところで再利用していこうという考え自体が僕にはイマイチ…。

mizchi:そういえば昔、Web Componentsでピアシングってスペックがありましたね。

白石:ピアシング、それ知らないなあ。

mizchi:ピアシングって、外から中に対してCSSをかけるんですよ。でもそれがなんか難しすぎるからなくなったらしいんですよね。やっぱりそこの複雑さよりは、単純な方をWeb Componentsは取ったんだろうなっていう気はします。

白石:なるほど、scope属性みたいな感じで書いていけるんでしたっけ?

森下:あれも今は仕様が変わってて、scoped属性はなくなって、@scopeっていうアットルール(@規則)に変わりました。(注: このあと@scopeルールもなくなった)

白石:そうなんですね。勉強になります。じゃあもうCSSは別に出しちゃって、WebとかそういったものでCSS付けるっていうみたいな感じなんですかね。

デプロイはAWS、ログ収集はBigQueryを利用

白石:ちなみに、バックエンドのほうの話も聞いてみたいです。

髙橋:デプロイ関係はAWSを使ってますね。あんまりAWS固有にベッタリみたいな感じにはしたくないと思っているので、データの収集などはGoogleのBigQueryを使っています。

白石:僕もBigQueryを使いたいんですが、使っているクラウドサービスはAWSなんですよね。Googleのほうはインターネット回線を通じて送っているんでしょうか?

髙橋:今は各アプリケーションサーバーごとにFluentdがあって、それを集約するFluentdサーバーからGoogleのBigQueryに一部分をバックアップとして保存しています。

よくデータ量が多そうですねって言われるんですけど、Qiitaの場合はユーザーがほぼエンジニアなので、トラフィック量という意味では、マスを相手にしているサービスと比較すると少ない部類です。実験しやすい環境なので、いろいろとやっています。

mizchi:監視はDatadogとNewRelicっていうアメリカのサービスを使っています。その2つを使って各種サービスのサーバーモニタリングをしています。

白石:Datadogは知らなかったです。範囲的にはNewRelicみたいなことを?

髙橋:NewRelicはどちからというとアプリケーションそのもののパフォーマンスを見るのが強いんですが、Datadogはサーバーのメトリクスの監視などをします。はてな社のMackerelとかなり近いです。

mizchi:本番環境をDockerによるコンテナベースの環境に移行しようというロードマップがあります。開発環境も本番環境と同じようにVagrantからDockerベースのものに移行しようと取り組んでいます。

白石:仮想環境あたりの話は詳しく聞きたいですね。

mizchi:最近Node.jsのネイティブバイナリをガンガン使ってます。今まで開発環境で使っていたCentOSに引きずられて使いたいツールが導入できないことがありました。例えば、FlowTypeのバイナリがCentOSというかRed Hat系のものがないからできなかったんですよね。Dockerになったことでミドルウェアを更新しやすくなったので開発しやすくなりました。あとVagrantではローカルのファイル変更を仮想環境側でうまく検知できない問題があったのですが、Docker for Macのファイル検知が賢いから、ファイル監視みたいなこともやってます。

本番環境がクラシックなEC2を使っていたので、ミドルウェアの更新はサーバー台数が増えてくるとなかなか簡単に行えなくなってきてたんですよね。そこの本番環境に引きずられて開発環境にも悪影響が出てたんですけど、それをDockerに持っていくことによって本番環境が切り替えやすくなるので、その分開発環境にも新しいコンポーネントを入れやすくなりました。

白石:ちなみに、そのVagrantからDockerへの切り替えていうのは、もう結構苦労しましたか。それとも割とすんなり?

mizchi:そうですね。Dockerなどに強いインフラエンジニアが入ってきたので、その人が主導してガンガン進めてくれている感じです。

白石:なるほど、まさに専門に特化してる方にどんどん移譲していってる段階なんですね。

試行錯誤を繰り返しながら、リモートワークを模索

白石:次は開発体制についてお聞きしたいのですが、そもそもQiitaは何人ぐらいのチームで開発しているんですか?

髙橋:エンジニアが10人、デザイナー3人、PM1人、バックオフィス4人と社長で、社員は現在19人です。社長の海野もエンジニアなんですが、現在は経営に集中しています。開発チームは大きく言うと3つで、QiitaとQiita:Team、そしてKobitoです。Kobitoはそれぞれのメンバーが兼任しています。Qiitaの開発チームは6人ですね。

東峰:QiitaとQiitaチームはかなりコードベースが共通なので、機能作り始めると両方考慮しなくてはいけないことも多いですね。

髙橋:Incrementsは全社的にリモートワークを導入しているのが、大きな特徴ですね。今日はインタビューがあるのでmizchiも来てますけど、基本的にはインターネットの向こうの人です。

白石:リモートワークは話題になっていましたね。実際、コミュニケーションとかうまくいくものですか?ツールなどはどんなものを使っているのでしょうか。

東峰:Google Hangoutsをベースにしています。多人数で繋ぐとどうしても重くなったりとか、回線状況によって接続がうまくいかないこともあるので。いくつかサービスを試しているところです。

白石:やはり常時接続にしているのでしょうか。

東峰:いえ、オフィス側だけですね。モニターとカメラがオフィスの真ん中にあるんですけど、それがHangoutで常にオフィスを映しています。そのチャンネルに入ってくれば、オフィスの様子が見えるように作っています。

白石:なぜ、オフィスだけ見せておくんですか。

東峰:実は社長の海野や髙橋、僕といったメンバーは学生時代にはてなでアルバイトしていた時期があるのですが、当時はてなでは東京と京都のオフィスを常時ビデオ会議で接続していたんですね。何かあるとそこにみんなが集まってくるという状態ができていて、その体験がすごくよかったからなんです。

リモートワークについてのリリースを出したのはつい最近なんですけど、2年近くトライアルはしてきました。週1だけリモートとか2週間スポットでフルリモートやってみたりとか。常に接続して定期的にキャプチャーを撮ってくれるSqwiggleというツールを使ってみたりもしました。でも常に見られてる感じがして嫌だっていう声があったり、マシンスペックもかなり使うので、最終的にはみんなが集まる場所としてのオフィスHangoutを作る形に納まりました。

mizchi:常時接続ってバッテリーの消費が激しいので、外に出歩いてる人にはかなり難しいんです。まあそれと別に僕は個人的にSqwiggle大嫌いだったのはあるんですが。

東峰:まあ実際、そういう人は多いと思うよ。

白石:リモートワークといっても、みんな自宅で仕事しているわけではないんですね。

東峰:そうですね。基本的に場所は自由なので、自宅以外にもカフェやコワーキングスペースとか、地方の実家に帰って作業してみたり、自由度は高いですね。

髙橋:リモートワークは各個人がベストパフォーマンスを発揮できる場所を自分で選んで仕事できるようにすることが目的であって、離れ離れになることが目的ではないので、あくまでも集まれる場所が必要なんですよね。そのために、オフィスのバーチャルな集会場みたいなかんじで常に開放し続けています。

白石:なるほど。リモートから見るとオフィスが全部映ってるんですね。これはHangoutじゃないですよね?

東峰:これはappear.inでやってます。スマホでも見れるんですよ。まるでオフィスにいるみたいな感じで話せるんですが、実際のオフィスには誰もいないということもあります(笑)。

白石:それシュールでいいですね。

東峰:髙橋が言った通り、やはり各自がベストパフォーマンスを出しているかということが大事なんですよね。振り返りもパフォーマンスがどれだけ上がったのかを見ていきたいと考えています。Incrementsはオンラインのコミュニケーションに慣れているメンバーが多いので、(リモートワークでも)できちゃうのはできちゃうんですね。特に大きな問題はないように見えるんですけど、本当に上手くいってるのかちゃんと検証したいので、あえてネガティブな面の洗い出しをするようにしています。

白石:オンラインコミュニケーションというものに対する経験値がみんな高いわけですね。Slackとかも使ってますか?

東峰:使ってますね。SlackとGoogle Hangouts、あとはQiita:Team。

髙橋:リモートワーク取り組み始めた目的の1つには、これから社会がリモートワークにシフトしていくなかで、率先して自分たちがその環境に身を置くことによって、リモート環境で働く組織の情報共有に必要な課題は何か、身を持って体験できることがあります。それをQiita:Teamの開発に還元していきたいですね。

東峰:我々はエンジニアを支援して世界を良くするっていうの社是なんで、新しいことにはどんどんチャレンジしていって、あまり保守的になりたくないと思っています。

OKRで組織目標を設定し、コミュニケーションを効率化

白石:ちなみにタスク管理のツールは何を使ってますか?

東峰:開発に関するものはGitHubとZenHub、Trelloを使ってます。

髙橋:あと最近では及川が入ってきたタイミングで、OKR(Objectives and Key Results)という取り組みを始めました。もともとはGoogleなどで使われていたフレームワークなんですが、会社に導入しようとしてもいきなりうまくはいかなかったんです。四半期単位でゴール設定するんですけど、1クオーター目、2クオーター目はもう全然ダメでしたが、3クオーター目の今になってようやくまともにOKRが回り始めてきました。

白石:では最後に、今後の課題や取り組んでいきたいことをお聞かせください。

mizchi:すごく根本的なことなんですけど、RailsとJavaScriptって相性が良くないんですよ。Railsから内部データ読んでJavaScriptをコードの自動生成するとか、いろいろ試しに作ってはみたんですけど、まあ言語が違うから。turbolinksが独立したJSパッケージもあるんですが、まだしっくりこないですね。個人的にはそこを突き詰めたいと思っています。

白石:言語の違いっていうところの問題が根源なんですかね。サーバーサイドがNodeだったらもっと上手くいくとか。

mizchi:僕はあんまりサーバーサイドにNode使いたいとは思ってない人間なんです。もちろんNode使えばReactのサーバーサイドレンダリングとかはすんなりいくんですけど、シングルスレッドを管理するコストの方が重いし。

髙橋:今のそのJavaScriptとRailsの繋ぎこみの部分というのは結局インターフェイスがRails側で統一できていないので、JS側で試行錯誤しなきゃならないっていうのが一番の問題点なんです。Facebookが最近公開したGraphQLみたいに、中間の緩衝地帯みたいなものを作ることによって、JSクライアントとバックエンド側がもっと上手くインテクレードできるような環境が作ることを考えたりしています。

mizchi:その2つの間で共通するハッシュや配列のフォーマットなどで会話しようと心掛けてるうちは多分上手くいくんですけど、そこからはみ出した瞬間上手くいかないなって感じがします。日付とか困りますね。Unixtimeにしてほしい。Datetimeのフォーマットは信用ならない(笑)。

髙橋:Incrementsの自慢はメンバーの質が高いこと。結局できる人を連れてくることが、最高の環境改善方法だと思っています。JavaScriptで迷ったらmizchiに聞けば、何でもかなりクオリティの高い回答が返ってくる。社内Slackで質問すれば社内の有識者からすぐにコメントをもらえるのがいいですね。

mizchi:僕としても多少無茶してもあんまり止められないっていうのは楽しいですね。勝手にこの前作ったスライド機能も、勝手に作って勝手に出したら、めちゃくちゃ評判良かったっていう。

白石:あれ勝手に作ったんですか?

mizchi:業務中に勝手に作ったんです。ベースは2時間で作りました。

白石:森下さんは入社してチャレンジしていることはありますか?

森下:BootstrapベースのCSSフレームワークを消して、Qiita独自のUIパーツなどを作っておきたいですね。そのスタイルガイドやメンテナンスみたいなことをやっていきたいと思っています。

白石:Qiitaのデザイン言語を作っていきたいみたいなかんじですね。

東峰:さすがに規模も大きくなってきて、多くのメンバーが協働でUIを作っていくのにBoostrapがあるだけではクオリティを保つことが難しくなってきましたから、Bootstrapを捨てるのがメインというよりは、ちゃんとうちに合ったフローやガイドラインを整理して、見た目も含めてトータルのUIデザインを管理できる状態にしていきたいと思っていて。その仕組み作りについては、森下を中心に一緒にチャレンジしていきたいですね。

白石:Qiitaはこれからもまだまだ成長していきそうですね。今日は興味深いお話をありがとうございました!

]]>
https://html5experts.jp/miyuki-baba/20328/feed/ 0
React/Angular2時代のUIフレームワーク考──Ionic2、Onsen UI2を語る https://html5experts.jp/shumpei-shiraishi/20411/ https://html5experts.jp/shumpei-shiraishi/20411/#comments Fri, 02 Sep 2016 00:00:21 +0000 https://html5experts.jp/?p=20411 読者の皆様こんにちは、編集長の白石です。

先日ふとしたきっかけで、本サイトの認定エキスパートでありアシアル株式会社の社長でもある田中 正裕さんと、Web技術について語り合う機会がありました。

ReactやAngular2といった次世代のアプリケーションフレームワークが存在感を増す中で、UIを構築するためのフレームワークはどう進化するのか?

これらのアプリケーションフレームワークをベースとしたIonic2Onsen UI2と言ったUIフレームワークについて、それぞれの立場から語り合ってみました。 (田中さんはOnsen UI2の開発者、白石はIonic2をかなりヘビーに使い倒しています)

Progressive Web Apps (PWApps)というキーワードが認知を広げる中で、ハイブリッドアプリやモバイルサイトを構築する手段として、これらのフレームワークは存在感を増しています。PWApps、React、Angular2、Cordovaといったキーワードに関心のある方は必読です!

では、本編をお楽しみください。

対談参加者のプロフィール

田中 正裕 田中 正裕(アシアル株式会社 代表取締役社長)

ユーザーインタフェース設計からインフラストラクチャー構築まで、最先端の技術を駆使したシステム構築を手がける。特にPHPをはじめとするOSSや、HTML5やJavaScriptといったオープンなアーキテクチャーを用いたシステムの構築に尽力している。モバイルアプリ開発プラットフォーム「Monaca」のプロダクトマネジャーを兼務。

白石 俊平 白石 俊平(HTML5 Experts.jp編集長)

株式会社オープンウェブ・テクノロジーCEO。2015年12月、「最先端は、ここにある。」を謳うテクノロジー情報キュレーションサービスTechFeedをリリース。Web技術者向け情報メディア「HTML5 Experts.jp」編集長。日本最大(6,500名超)のHTML5開発者コミュニティ「html5j」ファウンダー。Google社公認Developer Expert (HTML5)、Microsoft社公認Most Valuable Professional (IE) などを歴任。著書に「HTML5&API入門」(2010, 日経BP)、「Google Gearsスタートガイド」(2007, 技術評論社)など。監訳に「実践jQuery Mobile」(2013, オライリー)など。

Ionic2Onsen UI2

P7141021

白石: 本日はよろしくお願いします。今回はReact/Angular2時代のUIフレームワークということで、最先端のUIフレームワークについて語る場にしたいと思っています。

田中: はい、よろしくお願いします!

白石: まずは、Ionic2Onsen UI2について馴染みのない読者も多いと思うので、それぞれのフレームワークとぼくらの関わりについて話しましょうか。まずは、両フレームワークの共通点としては、以下の様な点が挙げられるかと思います。

  • どちらもコンポーネント指向のUIフレームワーク。独自のタグを元に、HTMLっぽいテンプレートを記述することでUIを構築していける。
  • どちらも最初から数多くのUIコンポーネントを備えており、モバイルネイティブに近い見た目のアプリを作れる。
  • どちらも、Cordovaをベースとしたモバイルアプリ開発(ハイブリッドアプリ)を前提としている。そのため、以下に示すようなCordovaの特徴を備えることになる。
    • ほぼ完全なクロスプラットフォーム性を備える
    • プラグインを通じて、ネイティブの機能を呼び出すことができる
    • WebView上で動作するので、ネイティブアプリに比べて動作が遅い

白石: で、こうした認識をベースとして、それぞれのフレームワークの違いなどに踏み込んでいければと思います。 まずは田中さんとOnsen UIの関係ですが、田中さんはOnsen UIのメイン開発者ということでいいんでしょうか?

田中: 正確に言うと、開発のリードをしている立場ですね。

白石: では、開発の優先順位なども田中さんが決定してらっしゃるということですね。チームは何人くらいいるんですか?

田中: 以前弊社に所属していた久保田 光則さんも含めると、4〜5人くらいがフルコミットしている感じですね。

そもそもOnsen UIとは何かというと、バージョン1の時はAngular1をベースとしたUIフレームワークでした。現在開発を続けているOnsen UI2の大きな特徴は「アプリケーションフレームワーク非依存」ということです。

<!--
Onsen UI2のコード例。
以下の例はAngular2だが、Angular1やReactと組み合わせることも可能
-->
<ons-page>
  <ons-input (change)="onChange()" type="text"></ons-input>
  <ons-button>Click Me!</ons-button>
</ons-page>

Onsen UI2自体はWeb Componentsに則って作られていて、それをReactやAngular2、Angular1、さらにはjQueryなどと組み合わせて利用できます。最近だとVue.jsとか今はMeteorとかいうバインディングも増えていますね。

白石: そこがIonicとの大きな違いですね。

ぼくは、先日リリースしたTechFeedというアプリで、Ionic2を全面的に採用しました。TechFeedはエンジニア向けのニュースアプリなのですが、Web/メール/モバイルアプリと、様々なクライアントがあります。そのモバイルアプリを開発するにあたって、プラットフォームごとにコードベースを分けたくなかったので、Cordova上で動作するモバイルアプリ用UIフレームワークを探していたんですね。

その際、まだβ版が出たばかりだったAngular2が割とアーキテクチャ的に美しくまとまっていたのと、Ionic2が既にAngular2に対応していたので、採用することにしました。 なので、Ionic2はAngular2をベースにしたUIフレームワークです。Angular2以外では動きません。

<!--
  Ionic2のコード例。
  タグの名前などは異なるが、あまり変わらない。
-->
<ion-content>
  <ion-input (change)="onChange()" type="text"></ion-input>
  <button>Click Me!</button>
</ion-content>

この、UIフレームワークとアプリケーションフレームワークが密結合(Ionic2)なのか、それとも疎結合(Onsen UI2)なのかというところが、今日の対談のポイントになりそうですね。

ただこのまま、UIフレームワークありきの議論に入っていく前に、ぼくは一つ確認したいことがありまして。そもそもUIフレームワークって必要とされているんでしょうか?

UIフレームワークは必要か?

P7141034

白石: 「いらないだろう」という前提で話すわけではなくて、Webの世界にとってコンポーネント指向のUIフレームワークというのはまだ新しい存在なので、そもそも「なぜ必要なのか?」を確認したいのです。とりあえず、フレームワークが必要か不要かで言うと、Webデザイナーさんの中には、フレームワークを嫌がる方も多い気がするのですが。

田中: 確かにそういう方もいらっしゃるとは思います。が、(Bootstrapなどのような)CSSフレームワークと、JavaScriptも含めたUIフレームワークは別に考えるべきじゃないかと思います。

白石: 確かにそうですね。CSSフレームワークは、デフォルトでそれなりの見た目になるとか、コーディングスタイルが統一されるなどの利点はあると思いますが、フレームワークのスタイルを変更しようとすると途端に面倒だったりしますしね。フレームワーク独自のルールを覚えなくちゃいけないし、最終的には全部リセットしたくなったり(笑)。

田中: その通りです。それに、ことモバイルアプリやサイトをWeb技術で作る場合に大事なのって、単なるCSSのプリセットがあることではありません。実際に操作してみた時のスムーズさだったり、アプリ全体から受ける印象だったりと、振る舞いも含めたもっと包括的なUXです。

そういう面で、アプリ全体のUXを統一して、なおかつプラットフォームに馴染んだものにするためには、やはり一貫した枠組みが必要です。それは実際に私がMonacaを運営する上で、お客様からご要望をいただいてきたところでもありますし、だからこそOnsen UIを作ることにも繋がりました。ということで、私はUIフレームワークは確実に必要だと思いますね。

白石: なるほど。ただ、UIフレームワークって、寿命が短くないですか?昔はjQuery MobileやKendo UIなんてのもありましたが、今ではあまり使っているという話を聞きません。

UIフレームワークとアプリケーションフレームワーク

P7141008

田中: それは、それらのフレームワークがUI的に古びてしまったというわけではなく、アーキテクチャ的に古いと見なされたから使われなくなってしまったんだと思うんです。JavaScriptアプリケーションのアーキテクチャは、ここ数年目まぐるしく移り変わってきました。jQueryのあとAngular1、React、Angular2, 他にもRiot.jsとかVue.jsなども出てきている。

ですがそれはアーキテクチャの進化であって、UIが進化したわけではありません。例えばUIの進化って、iOS7からフラットデザインになりましたとか、マテリアルデザインが出てきましたとか、そういうことです。こうした進化がすごく早いかと言われると、正直そうは思いません。UIフレームワークが、アプリケーションアーキテクチャの進化に引きずられちゃってたんじゃないか、というのがぼくの思うところです。

白石: なるほど、だからOnsen UI2はアプリケーション・フレームワークに依存しない形で設計されているわけですね。ぼくはTechFeedをIonic2で作りましたが、それはAngular2の採用を先に決めていたから、というのもあります。というか、その時はまだOnsen UI2が今ほど開発が進んでいなかったので、モバイルアプリを包括的に作れるUIフレームワークと、コンポーネント指向のアプリケーションフレームワークの組み合わせが、Ionic2 + Angular2しかなかったんですよね。

田中: Ionic2はAngular2に強く依存しているわけですが、Ionic2を学ぶには先にAngular2を学ばなくてはならないわけですよね。私はそこを少しもったいないな、と思います。元々私がUIフレームワークで実現したかったことって、「JavaScriptのフレームワークを学ばなくても簡単に使える」ということだったので。

白石: 確かに、Onsen UIはそういうところの配慮が行き届いている気がします。ぼくもOnsen UI1の時に使わせていただいていましたが、Webサイト上のサンプルコードが良くできていて、コピペするだけでUIが作れてしまうのが、とても楽ちんでした。

それぞれのフレームワークが目指すもの

P7141052

田中: あと、Ionic2はどことなくUIフレームワークというよりは「プラットフォーム」を目指している気がします。

白石: 確かに。Ionicはモバイルアプリを作る際はCordovaを使用するのですが、CordovaもあくまでIonicが規定するプラットフォームの一部というつもりなのか、cordovaコマンドをラップしたionicコマンドを提供していますね。また、CordovaプラグインのAPIをラップした ionic-native というモジュールも提供しています。これは、各種プラグインをTypeScriptから使えるようにしてくれているので、結構ありがたくはあるんですが。

田中: UIフレームワークを入り口として、プラットフォームに入ってきてください、というスタンスな気がしますよね。 Onsen UIはあくまでUIフレームワークであり、アプリケーションスタックの一部という位置付けなので、そこは思想の違いが現れている気がします。

白石: Ionic2はアプリケーションプラットフォームを限定していて、プラットフォームを志向している。Onsen UI2はアプリケーションプラットフォーム非依存で、ライブラリを指向している。こういう対比になりますね。こうして並べると、個人的にはOnsen UI2の方が設計面で魅力を感じます。

田中: ありがとうございます(笑)。

白石: ただ、まだ結論を出すのは早い気もしますね。どちらも正式リリース前ですし(笑)。

TechFeedがAngular2とIonic2を採用しているからというわけではないですが、アプリケーションプラットフォームを限定しているからこその強みもあるかもしれません。ぼくはまだOnsen UI2を実際に触ってみたことがないので比較ができないのですが、Ionic2のほうがAngular2との相性とかはさすがにいいでしょうし。先ほど出てきたionic-nativeパッケージとかもなかなか便利です。

またぼくらがIonicを採用した理由の一つには、コミュニティの大きさがIonicのほうが大きそうだった、という理由もあります。GitHubのStarの数が、Ionicは25,000、Onsen UIは3,000という違いがあったので。コミュニティの大きさは、Web上の情報量の差にもなって表れてくるので、困ったときに情報を得やすいと思ったのです。

このようにいろんな観点での比較ができそうなので、一概にどちらが優れているとここでは結論付けられなそうですが、こうした議論が読者の皆さんにとって判断の助けになるといいですね。

(編集部: 参考までに、ここまでに出てきた論点や、Web上で得られる内容を比較した表を掲載しておきます)

項目 Ionic2 Onsen UI2
依存するアプリケーションプラットフォーム Angular2 非依存(現時点ではAngular1/2, Reactなど)
フレームワークの実装言語 TypeScript ES2015とTypeScript
目指す地点 プラットフォーム? ライブラリ
GitHub上でのStar数 (2016/8/25時点) 25,370 3,216

(UIコンポーネントは現在のところほぼ同数ですが、どちらかにしか実装されていないコンポーネントもいくつかありますし、今後大きく増えていく部分だと思われるため、表からは除外しています)

今後のWeb技術の展望を語る

P7141001

白石: 最後にお聞きしたいんですが、Onsen UIに限らず、今後のWeb技術って、どういう方向に進んでいくとお考えですか?

田中: Web技術って、今どんどん開発のバリエーションが増えている状態だと思っています。それはフラグメンテーションと呼べるかもしれない。例えば、Angular2がTypeScriptを、ReactがJSXを使用しているように、です。これらが収束して一つの太い流れができていくのか、このまま個別の進化を続けていくのかは、まだ何ともいえません。

白石: テクノロジーのスタックが積み上がっていくにつれ、それぞれの開発手法の間の差異が広がっているというわけですね。こうした状況は、なぜ起きていると考えられますか?

田中: 私はMonacaを通じて「ハイブリッドアプリ」、つまりWeb技術でモバイルアプリを作るというところをずっとやってきました。その経験から言うと、モバイルアプリは大きく2パターンあります。一つは長期間のメンテナンスが必要なアプリ。もう一つはキャンペーンなどのワンタイムなアプリケーションですね。前者には強固な基盤が求められるので、Angular2のようなものが求められます。後者はそうではないですね。逆にさくっと作れるようなものが求められます。

こうしたニーズの二分化も理由の一つじゃないかなあとは思っています。

白石: なるほど、ニーズが分かれていると。そういう意味でいうと、静的な文書中心の「Webサイト」と、Single Page Application化が進む「Webアプリ」の違いも、フロントエンド技術のフラグメンテーションに一役買っていそうです。「Webサイト」の制作においては、まだまだjQueryは現役ですし、無理にそこを変える必要もなさそうですし。

あと、Web技術の適用範囲が増えてきているというのもあるかもしれませんね。WebVRとか、JavaScriptで動かせる組み込み基板とか。ウェアラブルやロボティクスという分野にも、Web技術が広まっていくとは思います。実際にそれがどれくらい使われるかは未知数ですけども。

田中: とはいえWeb技術って、情報技術全体から見るとエッジの技術ではありませんよね。かなりコモディティ化の進んだ、汎用的でメインストリームな技術。

白石: 確かに。これだけ技術が進んでも、結局のところHTML/CSS/JavaScriptという基本的な部分はあまり変わっていません。

田中: こうしたWeb技術の適用範囲をいかに広げていくか…というのは、個人的な興味として強くあります。いちエンジニアとして、Webのスキルセットをいろんなプラットフォームで使えるよう後支えしたいというのは、私の目指すところです。

白石: それは、HTML5 Experts.jpでもぜひ進めていきたいところです。ではそろそろお時間ですね。本日はお付き合いいただき、どうもありがとうございました!

P7140983

]]>
https://html5experts.jp/shumpei-shiraishi/20411/feed/ 0
Firebaseで楽々シグナリング──WebRTC入門2016番外編 https://html5experts.jp/mganeko/20273/ https://html5experts.jp/mganeko/20273/#comments Wed, 31 Aug 2016 00:00:42 +0000 https://html5experts.jp/?p=20273 連載: WebRTC入門2016 (5)

こんにちは! 2014年に連載した「WebRTCを使ってみよう!」シリーズのアップデートとしてお送りしているこの連載ですが、今回はもとの連載にはなかった内容を番外編としてお届けします。

httpsのハードル

前回は複数人、複数会議室で利用できるようにして、実用的なアプリを作る準備ができました。ところが実際に使おうとすると、Chromeのセキュリティポリシーと向き合わなくてはなりません。

「getUserMedia()やService Workerなどの強力なAPIは、セキュアな環境でなくては利用できない」というポリシーは今のWebの状況に合わせたものだと思います。では、その環境をどうやって用意すればよいのでしょうか?

もちろん証明書を取得して、きちんとサーバーを立てるのがまっとうなやり方です。最近はLet’s Encryptなど無料で証明書を発行するサービスもあります(参考:“Let’s Encrypt – how get to free SSL for WebRTC”)。とはいえ、試験的な利用でそこまで準備するのは大変というのも、正直なところです。そこで今回は、比較的とっつきやすい方法をご紹介します。

Webサーバー

HTMLやJavaScriptなどの静的コンテンツを配置できる手段はいくつかあります。エンジニアの皆さんであれば、次の2つをすでに利用されている方々も多いのではないでしょうか?

  • GitHub Pages
  • Google App Engine

どちらも http / https の両方でアクセスできますし、独自ドメインで利用することも可能です。(※利用方法についてはWebに多くの情報がありますので、そちらをご参照ください)

また、シグナリングで利用するFirebaseにも静的コンテンツのホスティング機能があります。

シグナリングサーバー

シグナリングにはsocket.ioなど、WebSocketを活用した仕組みが使われる例が多いようです。もちろん他の方法(例えば手動シグナリング)でもいいのですが、サーバーとクライアント(ブラウザ)で双方向に通信するにはWebSocketが適切なのでしょう。

https://~から取得されたHTML/JavaScriptからWebSocketサーバーに接続する場合には、そちらもセキュアでなくてはなりません。(ws://~ではなく、wss://~)。WebSocketを利用したシグナリングサーバーを自分で用意する場合、そちらでも証明書が必要になります。

そこで今回はリアルタイムメッセージングに利用できるBaaSであるFirebaseを使って、シグナリングの仕組みを構築したいと思います。

Firebaseでシグナリングを実現するには

まずはFirebaseにサインアップし、必要なキーやURLを入手しましょう。

(※手順については今回は省略させていただきます。あしらかず)

ブラウザでFirebaseの機能を利用するために、必要なライブラリを読み込んでおきます。今回はデータベースを利用するので、必要なjsファイルは次の通りとなります。

<script src="https://www.gstatic.com/firebasejs/3.1.0/firebase-app.js"></script>
 <script src="https://www.gstatic.com/firebasejs/3.1.0/firebase-database.js"></script>

それから取得しておいたキーとURLを用いてFirebaseに接続します。

// Initialize Firebase
  let config = {
    apiKey: "yourAPIKey",  // <-- please set your API key
    databaseURL: "https://yourapp.firebaseio.com",  // <-- please set your database URL
  };
  firebase.initializeApp(config);
  let database = firebase.database();

シグナリングで送りたいモノ

前回の記事にもあるように、シグナリングでは2つの通信ケースがあります。

  • ルーム内の他のメンバー全員(接続している他のクライアントすべて)に送る
  • 特定のメンバー(特定のクライアント)だけに送る

また後者のためには、特定のメンバーを識別するための何らかのIDが必要となります。

  • クライアント側で、重ならない/重なりにくい ようにIDを決める(UUID、タイムスタンプ、乱数など)
  • サーバー側でIDを振り出す

Firebaseを使うとデータベースに格納されるオブジェクトのすべてにIDが振られるので、今回はそれを利用します。

データベースの構造

Firebaseはデータベースによる階層構造をもったデータの永続化と、その追加/変更/削除のイベント通知が行えます。今回は次のようなアプリ/ルーム/メンバーの階層構造としました。

firebase_structure_1

例えばルーム”test”にメンバー”bbb”が参加する場合、次の2カ所のイベントを待ち受けています。

  • multi/room_test/_broadcast_ の child_added イベント
  • multi/room_test/_direct_/member_bbb の child_added イベント

また、multi/room_test/_join_ はメッセージのやりとり以外の用途で使います。

メンバーのIDの決定

メンバーを特定してメッセージを送るには、メンバーを識別するIDが必要です。先ほどは仮に”bbb”としましたが、実際には衝突を避けるためにFirebase側で振り出されるキーを利用することにしました。

let databaseRoot = 'myapp/multi/';
    let key = database.ref(databaseRoot + room + '/_join_').push({ joined : 'unknown'}).key
    clientId = 'member_' + key;
    database.ref(databaseRoot + room + '/_join_/' + key).update({ joined : clientId});

データベースにパスを指定してpush()すると、子要素が追加されて、そのキーが返っています。その値を使って先ほどの子要素の内容を更新しています。

ここまで終わった時のDatabaseの内容をFirebaseのコンソールで見てみると、次のようになっています。

firebase_join

ルーム内へのブロードキャスト

シグナリングの流れは前回と同じです。

multi_callme_simple

  • 新たに通信を開始したい人(member_xxxx)が、通信開始の合図のルーム内にブロードキャスする(“call me”)

“test”ルームにブロードキャストする場合には、 multi/room_test/_broadcast_ の下を使います。

let roomBroadcastRef = database.ref(databaseRoot + room + '/_broadcast_');

  // 通信開始の合図
  function callMe() {
    emitRoom({type: 'call me'});
  }

  function emitRoom(msg) {
    msg.from = clientId; // メッセージに送信元(自分のID)をセット
    roomBroadcastRef.push(msg);
  }

各メンバーはこのブロードキャストのイベントを待ち受けていて、typeに応じて処理を行います。

roomBroadcastRef.on('child_added', function(data) {
      let message = data.val();
      let fromId = message.from;
      if (fromId === clientId) {
        // ignore self message (自分自身からのメッセージは無視する)
        return;
      }
      
      if (message.type === 'call me') {
        // 接続処理
      }
    });

socket.ioでは自分自身からメッセージは飛んで来ませんが、Firebaseでは自分でpush()してもイベントが飛んでくるので、それは無視しています。

特定のメンバーへのメッセージ

通信開始の合図である”call me”以外は、特定のメンバー宛のメッセージのやり取りになります。例えば member_bbb 宛のメッセージは、この下にpush()します。

  • multi/room_test/direct/member_bbb

function emitTo(id, msg) {
    msg.from = clientId; // メッセージに送信元(自分のID)をセット
    database.ref(databaseRoot + room + '/_direct_/' + id).push(msg);
  }

Offer/Answerの交換やICE Candidateのやり取りは、すべてこちらのメンバー宛のメッセージになります。

Firebaseのルール設定

Firebaseのデータベースにはアクセスのルール指定があります。デフィルトではread/writeにAuth必要なルールが生成されていますが、今回のサンプルでは特定のパス以下はAuth不要で読み書きできるよう、ルールを追加しました。

{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null",
    "myapp" : {
      "multi" : {
        ".read": true,
        ".write": true
      }
    }
  }
}

NAT越えの設定 STUNの設定

せっかくFirebaseでメッセージをやり取りできるので、NATを超えてWebRTCで通信できるようにしましょう。

NATを超えて通信を行うには、ローカルネットワークでのIPアドレスではなく、グローバルIPを伝える必要があります。今回は詳しく説明しませんが、そのための仕組みがSTUNです。GoogleがSTUNサーバーを公開しているので、それを使わせてもらいましょう。

function prepareNewConnection(id) {
    let pc_config = {"iceServers":[{"urls": "stun:stun.l.google.com:19302"}]}; // for STUN server
    let peer = new RTCPeerConnection(pc_config);
    // ... 省略 ...
  }

2014年の記事では”url” と指定していましたが、現在の仕様では”urls”と指定する必要があります。

このようにSTUNサーバーを指定することで、通信経路の候補(ICE Candidate)に、STUN経由で取得した情報も含まれるようになります。

動かしてみよう

今回のサンプルをGitHub Pagesで公開しています。Firebaseもしばらく(〜2016年9月末の予定)使える状態にしておきますので、皆さんもお試しください。

※シグナリングの手段以外は、前回のソースと同様の処理になっています。

使い方

  • URLの後ろに ?room という形で、好きなルーム名を指定してください
    • https://mganeko.github.io/webrtcexpjp/basic2016/multi_firebase.html?お好きなルーム名
  • ルーム名を指定せずに multi_firebase.html を開くと、ランダムにルーム名を決定します
  • [Start Video]をクリックし、カメラの映像とマイクの音声を取得します
  • 通信相手にも同じルーム名を指定してブラウザ(Chrome/Firebox)でアクセスしてもらいます
    • ?room も含むURLを伝えてください
    • “Mail link of this room” をクリックすると、URLを送るためのメーラーが開きます。宛先を指定して送信してください
  • 通信相手にも[Start Video]をクリックし、カメラの映像とマイクの音声を取得してもらいます
  • 自分、あるいは相手から[Connect]ボタンを押してください
  • Firebase経由で情報が交換され、P2P通信が始まります
  • このサンプルでは、同じルームに同時に4人まで(3人の相手と)通信することができます

トラブルシューティング

  • 異なるPCで通信できない場合
    • → ルーム名が一致しているか確認していください
    • ルーム名を指定せずにブラウザでアクセスすると、ランダムにルーム名が決定されます
    • 異なるPCでは、それぞれ異なるルーム名がランダムに決定されます
  • 一度接続できたが、その後できなくなった場合
    • ルーム内の過去のブロードキャストメッセージが残っている可能性があります
    • ルーム名を変更して、再度接続してみてください
  • (例えば)会社と自宅で通信できない場合
    • → NATだけでなく、FirewallでUDP通信やポートが制限されている可能性があります
    • ※Firewallを超えての通信については、次回の記事で取り上げる予定です

Edge同士での通信

webrtc.orgが提供しているadapter.jsを使うと、多くのブラウザの差異を吸収することができます。最新のhttps://webrtc.github.io/adapter/adapter-latest.jsを読み込めば、Microsoft EdgeのサポートしてるORTCを、WebRTCのオブジェクトのインターフェイスを通して利用することができます。

残念ながらサポートしているビデオコーデックの制限で、ChromeやFirefoxとのビデオ通信はできませんが、Edge同士であれば今回のFirebaseを使ったシグナリングでビデオ/オーディオのP2P通信を行うことができます。

edge_firebase

注意点

  • Edgeでは、同一のカメラを複数のウィンドウ/タブで利用することができません。同一PCで通信する場合は、複数のカメラを用意してください
  • Edgeでは現在のところSTUNはサポートされていない(TURNのみサポート)ため、今回のサンプルでNATを越えた通信はできません

次回は

今回はFirebaseを使ってシグナリングを実現しました。次回はNAT/Firewallを超えてのWebRTC通信についてお届けする予定です。

]]>
https://html5experts.jp/mganeko/20273/feed/ 0
Chrome 52新機能、最新macOS「Sierra」パブリックベータ公開─2016年7月のブラウザ関連ニュース https://html5experts.jp/myakura/20202/ https://html5experts.jp/myakura/20202/#comments Mon, 15 Aug 2016 00:26:44 +0000 https://html5experts.jp/?p=20202 連載: WEB標準化動向 (15)

2016年7月のブラウザ関連ニュースは、7月20日にリリースされたChrome 52、macOSの最新バージョン「Sierra」のパブリックベータ公開についてお伝えします!

Chrome 52リリース

7月20日にChrome 52がリリースされました。

Mac版は見た目がすっきりしました。

スクリーンショット:Chrome 51とChrome 52のウインドウを並べたもの

グラデーションや丸みがなくなりすっきりした。タブも少し大きくなった

新しい見た目は、モバイル版ChromeやChrome OSと同じものになっています。細かいところについては、GoogleでChromeのデザインをしているSebastien Gabrielが自身のサイトやMediumでまとめています。

新しい機能の紹介をしましょう。今回はパフォーマンス向上に貢献しそうなものが多く入っています。

Service Worker/Fetch API関連で、レスポンスを ReadableStream で作成し返せるようになりました。

うまく使えば段階的にコンテンツを表示できるので、体感速度が上がります。

CSSではCSS Containmentというものが実装されました。指定した要素について、レイアウトや描画のスコープを設けられる仕組みです。

基本的にレイアウトや描画はドキュメント全体がスコープになるため、一部のみ変更する際にはドキュメント全体が対象となる無駄な処理が走ります。それを抑制できるので、とくに複雑なページで使うと恩恵が大きいでしょう。

また、新機能ではありませんが、外部スタイルシートが起こすレンダリングの挙動が変更されました。

これまでは外部スタイルシートをパーザが発見した場合、それが読み込まれるまで描画をブロックしていたのですが、すでにパーザが処理したDOMの描画も止めていたため、段階的なレンダリングができませんでした。新しい挙動は外部スタイルシートが見つかった箇所以降のみレンダリングのブロックが発生します。段階的なレンダリングにやさしくなります。

この挙動とHTTP/2を組みあわせ、必要なときに必要なCSSを読みこむやり方が検討されています。

パフォーマンス計測のためのPerformance Observerも実装されました。Navigation Timingをはじめとした各種パフォーマンス情報をObserverで取得する新しいAPIです。ポーリングなどをしなくてもよくなりますし、必要なパフォーマンス情報を絞れるので、扱いやすくなるかと思います。

パフォーマンス関連以外の機能もちょこちょこあります。

  • Canvas APIに ctx.filter が追加され、フィルタを指定できるようになりました
  • CSS Color Module Level 4で定義されている #rrggbbaa 記法が使えるようになりました
    8月25日追記リリース直前に無効にされていました…
  • V8も更新され、ES7のexponentiation operator(**)が実装されました

macOS Sierraパブリックベータ公開

OS Xから名前が変わったmacOSの最新バージョン、Sierraのパブリックベータが公開されました。

Safari 10が入っていますが、開発者向けのドキュメントで取り上げられていない機能も一部入っています。

ひとつはCSSアニメーションのの spring() です。「ばね」と名のつくように、ぼよんぼよんと動くアニメーションを簡単に使えるものです。Appleが内部で必要として試験的に実装されたもので、後にAppleより標準化も提案されています。

頑張れば既存の仕組みでも実現できるかもしれませんが、専用の関数が用意されているとだいぶ楽そうです。 また、タイミング関数を自作できる仕組みもほしいという話もコミュニティから出ています。

動きもコンポーネント的に定義できれば、ライブラリとしての提供もしやすそうです。

もうひとつは、初期のベータだけですが、WebPがOSレベルでサポートされていました。

しかし、後のベータでは削除されベータに含める意図がなかったことも明らかになっています。Appleが実装するまでWebPに興味があったというのが、うれしいニュースでしょうか。

]]>
https://html5experts.jp/myakura/20202/feed/ 0
Microsoft EdgeのUI機能が強化!ーWindows 10 Anniversary Updateにおける新機能一挙解説 https://html5experts.jp/osamum_ms/20244/ https://html5experts.jp/osamum_ms/20244/#comments Wed, 03 Aug 2016 01:09:34 +0000 https://html5experts.jp/?p=20244 Windows 10 が公開されてちょうど一年、Windows 10 の無償アップグレード期間が終了したのと入れ替わるように本日 (2016/8/3 日本時間) Windows 10 Anniversary Update が公開されました。

この Windows 10 Anniversary Update では、Windows 10 が発表された際の様々なビジョンのいくつかが実装され、さらにその当時は想像だにできなかった  Windows Subsystem for Linux (WSL) といったようなまったく新しい機能も搭載されています。

Windows 10 から搭載された新しい Web ブラウザーである Edge にも、当初の計画にあった新しい拡張 (エクステンション)モデルの実装や、Windows フィードバック、Developer FeedBack (旧 Edge Suggestion Box) に寄せられたフィードバックや提案をもとにした新しい機能が実装されています。

今回の記事では Windows 10 Anniversary Update の新機能、とくにユーザーが直接対話するデスクトップ UI の新機能と、新しく追加された Edge 関連のグループポリシーの設定について紹介します。

Microsoft Edge のデスクトップ UI 機能の強化

Windows 10 がリリースされてからこれまで、Edge にもたゆまず機能強化が行われてきましたが、その内容はパフォーマンスの向上であったり、サポートする API の数を増やすであるとか、どちらかというと開発者向けのものが多かった印象があります。

これら機能は UI を持たないため、開発者以外の多くのユーザーは Edge の機能向上の進捗を肌感として感じれなかったかもしれません。

しかし、Windows 10 Anniversary Update での Edge では、誰しもがアクセス可能なデスクトップ UI に複数の新機能を搭載しています。

この機能は、おおまかに分類すると以下の 4 つに別けられます。

  1. ナビゲーション
  2. お気に入り
  3. ダウンロード
  4. コンテキストメニュー

以降は、上記 4 つの項目について、具体的にどのどのような機能が追加されたのかを紹介していきます。

ナビゲーション

Web ブラウジングにおける “ナビゲーション” はページを遷移させるための操作や、その動作そのものを指します。

このナビゲーション関連では以下の機能が追加されています。

右クリックでのナビゲーション履歴表示

Edge のナビゲーションバーの左側にある 戻るボタン/進むボタン上で、マウスの右ボタンをクリックすることで、それまでの履歴と現在の位置(履歴内のどこをブラウズしているか)が表示され、クリックすることでそのページに遷移できるようになりました。

従来の 戻る/進む ボタンでは、ブラウザーを起動してからの履歴に対し、ひとつずつしか前後に遷移できませんでしたが、この機能を使用すると履歴の任意の位置に直接移動できます。

context_history


 

貼り付けて移動/検索

クリップボードにコピーした URL のページを Edge で表示させる場合、ナビゲーションバーに一度 URL を貼り付けてからキーボードの [ Enter] キーを押下する必要がありました。

また同様に、クリップボードにコピーしたキーワードで検索を行う場合には、 ナビゲーションバーに一度 キーワードを貼り付けてからキーボードの [ Enter] キーを押下する必要がありました。

Windows 10 Anniversary Update からの Edge では、ナビゲーションバー上でマウスの右ボタンをクリックした際に表示されるコンテキストメニューに [貼り付けて移動] もしくは [貼り付けて検索] というメニューが追加されており、このメニューを選択することで [Enter] キーを押下しなくてもクリップボードにある URL への移動、あるいはクリップボードにあるキーワードで検索を行うことができます。なお、「移動」か「検索」かは、クリップボードの内容により自動的に判断されます。

paste_and_go
(クリップボードの中身が URL の場合)


 

paste_and_find
(クリップボードの中身が URL ではない場合)


 

スワイプによるナビゲーション

スワイプによるナビゲーションに対応しました。

Windows Phone や タッチ対応のモニタの PC で Edge を使用する場合、画面を左/右にスワイプすることで一度遷移した URL について遷移することができます。

左にスワイプするとひとつ前に戻り、右にスワイプするとひとつ進みます。

お気に入り

Edge において「お気に入り」 (favorite) はユーザーが任意の URL を Web ブラウザーに登録しておく機能です。

お気に入り関連では以下の機能が追加されています。

ピン留め

ピン留めは Internet Explorer 8 からサポートされた機能で、当時はピン留め対応した Web サイトのショートカット アイコンとナビゲーションメニューを Windows のタスクバーに登録することができました。

Windows 10 Anniversary Update で提供される Edge には、ショートカットアイコンの登録場所や提供される機能は異なりますが、ピン留め機能が復活しています。

また、Web サイト側での特別な設定は不要となっています。

Edge のピン留め機能を使用するには、任意のページを表示したページのタブの上で右クリックし、表示されたコンテキストメニューから [ピン留めする] メニューを選択します。

Pinned tab

これによりタブの左端にページがピン留めされ、Edge が起動されると同時にページがロードされるようになります。

image


 

[お気に入り] メニューのツリー表示と並べ替え機能

これまで Edge の [お気に入り] メニューでは、同一階層にあるリンクのみが一覧で表示されていましたが、Windows 10 Anniversary Update で提供される Edge ではツリー表示されるようになりました。

favorit_tree

また、名前で並べ替えも出来るようになっています。

name_sort


 

[お気に入り] のインポート元の表示

Edge では他の Web ブラウザーから [お気に入り] をインポートできますが、インポート元の Web ブラウザーの名前がついたフォルダが作成され、どの Web ブラウザーからインポートされたのかわかるようになりました。

Inport


 

お気に入りバーのメニュー追加

お気に入りバー上にショートカットの表示で、アイコンだけを表示できる設定が追加されました。

favorite_setting


また、これまでお気に入りバーにはコンテキストメニューは設定されていませんでしたが、今回のアップデートで [新しいフォルダの作成] と [アイコンのみ表示する] メニューが追加されました。

FavoritesBar

ダウンロード

Edge のダウンロード機能については、保存場所や保存のさいのアクションについて、以下のような機能が追加されています。

保存先フォルダの指定

これまでの Edge では、既定のダウンロード先のフォルダは %user%\ダウンロード となっており、これを変更することは出来ませんでしたが、今回のアップデートでは [詳細設定] メニューからこれを変更できるようになりました。

image


 

ダウンロード先の指定

これまで、Edge ではファイルをダウンロードする際に保存先の指定を任意で行うことはできませんでしたが、今回のアップデートでは、ダウンロード時に表示されるダイアログボックスに [名前を付けて保存] ボタンが追加され、保存先の指定や、保存する際のファイル名の指定ができるようになりました。

dl_dialog


 

ダウンロード中に Edge 終了の際の警告

ファイルをダウンロード中に Edge をクローズ場合、警告メッセージが表示されるようになりました。

コンテキストメニューへの機能追加と変更

Web ページを表示する部分で、右クリックした際に表示されるコンテキストメニューにも機能が追加されています。

Cortana との連携

Edge に Cortana との連携機能が実装されました。

文字列、もしくは画像を選択し、マウスの右クリックメニューから [Cortana に質問] を選択すると、選択された内容に関連する情報を判断して検索を行い結果を列挙します。

Cortana


 

たとえば、以下のように画像を問い合わせた場合、Cortana は関連する情報と類似する画像を調査して列挙します。

image

 

開発者用メニューの表示タイミング

これまでの Edge では、表示されている Web ページ上での右クリックメニューに [要素の検査] と [ソースの表示] という開発者向けのメニューが、初期状態から表示されるようになっていました。

今回のアップデートでは、初期状態ではこれら開発者向けのメニューは表示されず、F12 開発者ツールが起動されてからはじめて表示されるようになっています。

 

ここまで Windows 10 Anniversary Update で追加される Edge の新機能について紹介してきました。

Edge の新機能については、かねてからのロードマップと、ユーザーから寄せられたフィードバックにもとづき実装しています。

しかしながら、期待していた機能が実装されていなかった、ということもあるでしょう。そういった場合には Edge に拡張インストールし、昨日を追加することでご要望を満たすことができるかもしれません。

拡張 (エクステンション) による Edge の機能強化

Windows 10 Anniversary Update の Edge では、拡張がサポートされました。

拡張とは、外部で作られたプログラムで、Edge にプラグインすることで Edge の機能を文字どおり拡張します。

Edge の拡張は Windows ストアから入手することができ、すでに Ad ブロックやマウスジェスチャー、翻訳等、さまざまな機能を提供する拡張が用意されています。

拡張の入手と Edge へのインストール

拡張の具体的な入手とインストール方法は以下のとおりです。

  1. Edge のツールバー上の  […] アイコンをクリックして [拡張機能] メニューをクリックします。
    image

     
  2. [拡張]パネルが表示されるので [ストアから拡張機能を取得する] リンクをクリックします。
    image

     
  3. Windows ストアの 「Microsoft Edge の拡張機能」 画面が開くので任意の拡張のアイコンをクリックします。
    ExtensionList

     
  4. 選択した拡張の説明画面に遷移するので [無料] 、もしくは金額の書かれた(※有料の拡張の場合) ボタンをクリックします。
    image

以上の手順で拡張のダウンロードとインストールが行われます。

拡張の設定とアンインストール

Edge にインストールした拡張のアンインストールやオプション設定は、Edge の [拡張機能] メニューで行います。

具体的な手順は以下のとおりです。

  1. Edge のツールバー上の  […] アイコンをクリックして [拡張機能] メニューをクリックします。
  2. [拡張]パネルが表示され、インストール済の拡張のリストが表示されるので、任意の拡張をクリックします。
  3. 拡張の詳細設定画面が表示されるので以下から目的の操作を行います。
    • 拡張を有効、もしくは無効に –  トグルボタンを操作
    • 拡張のオプション設定を行う – [オプション] ボタンをクリック
    • 拡張をアンインストールする – [アンインストール] ボタンをクリック
    image


 

Edge への拡張のインストール/アンインストール方法は以上です。

Edge 用の拡張を自分で作成する方法については以下の記事で紹介していますので、興味のある方はぜひご覧ください。

 

Windows 10 Anniversary Update で追加される Edge のグループポリシー設定

Windows 10 Anniversary Update では Edge のグループポリシーで設定できる内容も追加が行われています。

グループポリシーとは、IT 管理者が管理対象の使用者に対してさまざまな設定を一括で行うための機能です。

従来から Edge では、以下の内容について IT 管理者がグループポリシーを用いて Windows ドメイン ユーザーの使用する Edge を管理することができます。

設定 説明
オートフィルを無効にする Edge の使用中にフォームのフィールドにオートフィルで自動入力できるかどうか
開発者ツールを無効にする F12開発者ツールの使用を許可するかどうか
トラッキング拒否ヘッダーの送信を従業員に許可する トラッキング情報が要求される Web サイトに従業員がトラッキング拒否ヘッダーを送信を許可するかどうか
InPrivate ブラウズを無効にする InPrivate ブラウズの使用の有無
パスワードマネージャーを無効にする パスワードマネージャーを使用してパスワードのローカル保存を許可するかどうか
ポップアップブロックを無効にする ポップアップブロック機能を使用可とするかどうか
アドレスバーの検索候補を無効にする アドレスバーに検索候補を表示するかどうか
SmartScreen フィルターを無効にする SmartScreen フィルターを有効にするかどうか
Open a new tab with an empty tab ([新しいタブ]ページでのWebコンテンツの許可) 新しいタブを開いたときに表示するページの種類を構成
Cookie を構成する Cookie の扱いを構成
エンタープライズモードサイト一覧を構成する エンタープライズ モードとエンタープライズ モード サイト一覧を使用するかどうかを構成
お気に入りを構成する ユーザーに表示される既定のお気に入りを構成できます
WebRTC による LocalHost IP アドレスの共有をしない WebRTC プロトコルを使用した通話中にユーザーの LocalHost IP アドレスが表示されるかどうかを指定
企業のホームページを構成する ドメインに参加しているデバイス用に企業のホーム ページを構成
SmartScreen フィルター機能の警告の上書きを許可しない 有害である可能性のある Web サイトに関する SmartScreen フィルター機能の警告を従業員が上書きできるかどうかを指定
確認されていないファイルに関するSmartScreen フィルター機能の警告の上書きを許可しない 確認されていないファイルのダウンロードに関する SmartScreen フィルター機能の警告をユーザーが上書きできるかどうかを指定
すべてのイントラネットサイトを Internet Explorer 11 に送る イントラネット サイトを Internet Explorer 11 で表示するかどうかを指定

 

今回のアップデートでは、新たに以下の内容が制御可能となりました。

設定 説明
Microsoft Edge で about:flags ページへのアクセスを禁止する ユーザーが about:flags ページにアクセスできるかどうかを指定
拡張機能の許可 ユーザーが拡張機能を読み込めるかどうかを許可
Interner Explorer でサイトを開くときのメッセージ表示 Internet Explorer 11 でサイトが開かれたことを示す追加ページを Microsoft Edge で従業員に表示するのかどうかを指定

これらグループポリシーの新しい設定を使用することで、IT 管理者は今回のアップデートで追加された Edge の新機能の使用の有無を管理することかできます。

Microsoft Edge の更新履歴について

Windows 10 のビルド単位での、より詳しい更新履歴については、以下のページで確認することができます。

Microsoft Edge がサポートする API については以下をご参照ください。

まとめ

これまでの Edge では、Web コンテンツの閲覧中 Internet Explorer 11 と比較して機能が少ないと感じる場面が多々あったかもしれません。しかし、そういった不満は Windows 10 Anniversary Update によってずいぶんと解決されていると思います。

また、それでも足らないと感じる機能については、拡張を追加することである程度解消できることでしょう。

Edge を使ってみて他の Web ブラウザーを使うようになってしまった人も、Edge をまだ使用したことがない人もぜひこの機会に Edge をお試しくださいませ。

]]>
https://html5experts.jp/osamum_ms/20244/feed/ 0
シグナリングを拡張して、複数人で通信してみよう ーWebRTC入門2016 https://html5experts.jp/mganeko/20112/ https://html5experts.jp/mganeko/20112/#comments Mon, 01 Aug 2016 00:00:18 +0000 https://html5experts.jp/?p=20112 連載: WebRTC入門2016 (4)

こんにちは! 2014年に連載した「WebRTCを使ってみよう!」シリーズのアップデート記事も4回目となり、佳境に入りました。前回の1対1の通信をベースに、今回はより実用的なビデオチャットを目指して複数人で通信可能なように拡張してみましょう。

複数人、複数会議室を目指して

前回作ったのは、1つのシグナリングサーバーに対して、同時に1ペアだけが利用できる仕組みでした。これを複数人で、複数会議室で利用できるようにしていきましょう。こちらの図の左のBeforeの状態から、右のAfterの状態を実現します。

rtc_11_to_nn

複数人で通信するためには

WebRTCはPeer-to-Peer (P2P) で通信する仕組みです。なので複数人と通信するためには、複数のRTCPeerConnectionを用意する必要があります。図にするとこんな感じです。

PeerConnection_multi

また、P2P通信を開始するためにSDP(Offer/Answer)を交換する必要がありますが、それぞれの相手ごとに生成、交換しなければなりません。

PeerConnection_sdp_multi

P2Pなので当然かもしれませんが、私は最初に複数の相手と通信しようとして混乱してしまいました。この考え方を踏まえていれば、あとは力技になります。

シグナリングサーバーの対応

シグナリングサーバーは前回の1対1の時と同じく、Node.jsを使って用意しましょう。複数会議室を実現するのに便利なため、今回はwsモジュールではなく、より高機能のsocket.ioを使います。Node.jsのインストールは終わっていると思うので、コマンドプロンプト/ターミナルから、次のコマンドを実行してください。 ※必要に応じて、sudoなどをご利用ください。

npm install socket.io

2014年にはsocket.ioはv0.9でしが、今回はv1.4.xになっています。

シグナリングサーバーのコードは前回の1対1と同様にメッセージを中継するのが役目ですが、接続してきたクライアントがルーム(会議室)に入室要求を送ってきたら、socket.ioのサーバ側でそのルームに join() してあげます。

// ---- multi room ----
    socket.on('enter', function(roomname) {
      socket.join(roomname);
      console.log('id=' + socket.id + ' enter room=' + roomname);
      setRoomname(roomname);
    });

    function setRoomname(room) {
      socket.roomname = room;
    }

クライアントからのメッセージ送信には、次の2つのパターンがあります。

  • ルーム内の他のメンバー全員(接続している他のクライアントすべて)に送る
  • 特定のメンバー(特定のクライアント)だけに送る

シグナリングサーバーでは、送信先が指定されていればその相手だけに、指定されていなければルーム内の全員(送信者以外)にメッセージを送ります。 ※その際に、送信元を特定できるID(socket.ioが管理しているID)を追加しています。

socket.on('message', function(message) {
        message.from = socket.id; // 送信元のIDをメッセージに追加

        // get send target
        var target = message.sendto;
        if (target) { // 特定の相手に送る場合
          socket.to(target).emit('message', message); 
          return;
        }

        // broadcast in room
        emitMessage('message', message);
    });

    // ルーム内の全員に送る場合
    function emitMessage(type, message) {
      // ----- multi room ----
      var roomname = getRoomname();

      if (roomname) {
        // ルーム内に送る
        socket.broadcast.to(roomname).emit(type, message);
      }
      else {
        // ルーム未入室の場合は、全体に送る
        socket.broadcast.emit(type, message);
      }
    }

サーバーの全体のソースは次の通りです。これを例えば signaling_room.js というファイル名で保存します。

"use strict";

var srv = require('http').Server();
var io = require('socket.io')(srv);
var port = 3002;
srv.listen(port);
console.log('signaling server started on port:' + port);

// This callback function is called every time a socket
// tries to connect to the server
io.on('connection', function(socket) {
    // ---- multi room ----
    socket.on('enter', function(roomname) {
      socket.join(roomname);
      console.log('id=' + socket.id + ' enter room=' + roomname);
      setRoomname(roomname);
    });

    function setRoomname(room) {
      socket.roomname = room;
    }

    function getRoomname() {
      var room = socket.roomname;
      return room;
    }

    function emitMessage(type, message) {
      // ----- multi room ----
      var roomname = getRoomname();

      if (roomname) {
        console.log('===== message broadcast to room -->' + roomname);
        socket.broadcast.to(roomname).emit(type, message);
      }
      else {
        console.log('===== message broadcast all');
        socket.broadcast.emit(type, message);
      }
    }

    // When a user send a SDP message
    // broadcast to all users in the room
    socket.on('message', function(message) {
        var date = new Date();
        message.from = socket.id;
        console.log(date + 'id=' + socket.id + ' Received Message: ' + JSON.stringify(message));

        // get send target
        var target = message.sendto;
        if (target) {
          console.log('===== message emit to -->' + target);
          socket.to(target).emit('message', message);
          return;
        }

        // broadcast in room
        emitMessage('message', message);
    });

    // When the user hangs up
    // broadcast bye signal to all users in the room
    socket.on('disconnect', function() {
        // close user connection
        console.log((new Date()) + ' Peer disconnected. id=' + socket.id);

        // --- emit ----
        emitMessage('user disconnected', {id: socket.id});

        // --- leave room --
        var roomname = getRoomname();
        if (roomname) {
          socket.leave(roomname);
        }
    });

});

コマンドプロンプト/ターミナルから、 次のように起動してください。(ファイル名は適宜置き換えてくださいね)

node signaling_room.js

クライアント側の拡張

次はクライアントとなるブラウザ側の処理を拡張していきます。

socket.io サーバーへの接続

今回はシグナリングサーバーを同じPCの3002番ポートで動かしていると想定します。HTMLファイルの先頭で、socket.ioのクライアント用のjsファイルを読み込みます。

<!doctype html>
<html>
<head>
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 <title>multi party</title>
 <script src="http://localhost:3002/socket.io/socket.io.js"></script>
</head>
... 省略 ...
</htm>

クライアントではsocket.ioのサーバー (localhost:3002) に接続します。このとき、ws://~ ではなく、http://~ となることがWebSocketを直接利用した場合と異なります。

// ----- use socket.io ---
  let port = 3002;
  let socket = io.connect('http://localhost:' + port + '/');
  socket.on('connect', function(evt) {
    // 接続したときの処理
  });

※実際のシグナリングサーバーの環境に合わせて、URLやポート番号は変更してください。

ルーム(会議室)への入室

クライアンではsocket.ioのサーバーに接続したら、希望のルームに入室を依頼します。ルーム名は今回はURLの後ろに ?部屋名 という形で指定することにしてみました。(お好きな方法で指定してください)

let room = getRoomName();
  socket.on('connect', function(evt) {
    console.log('socket.io connected. enter room=' + room );
    socket.emit('enter', room);
  });

  // -- room名を取得 --
  function getRoomName() { // たとえば、 URLに  ?roomname  とする
    let url = document.location.href;
    let args = url.split('?');
    if (args.length > 1) {
      let room = args[1];
      if (room != '') {
        return room;
      }
    }
    return '_testroom';
  }

複数通信の流れ

1対1の時は相手が1人しかいない前提だったので、ただちにOffer SDP / Answer SDPを送信していました。今回は相手が何人いるか分からない状況からスタートしますし、相手ごとに個々にOffer SDP / Answer SDPを送受信する必要があります。そこで、相手を確認するやりとりを追加しました。図にすると、次のような流れになります。

multi_callme_simple

もう少し細かく見ると、次のような処理を行っています。

  • 新たに通信を開始したい人(ブラウザA)が、通信開始の合図のルーム内にブロードキャストする(“call me”)
  • 受け取った人(ブラウザB、ブラウザC)は、Offer SDPを生成して、ブラウザAを宛先に指定して送ります
    • RTCPeerConnectionを生成
    • RTCPeerConnection.createOffer()でOffer SDPを生成
    • setLocalDescription()で覚える
    • ブラウザA宛てに送信
  • ブラウザAは、Offer SDPを受け取り、Answer SDPを生成してそれぞれの相手に送り返します
    • 相手ごとにRTCPeerConnectionを生成
    • それぞれ受け取ったOffer SDPをRTCPeerConnection.setRemoteDescription()で覚える
    • RTCPeerConnection.createAnswer()でAnswer SDPを生成
    • setLocalDescription()で覚える
    • それぞれの相手にAnswer SDPを返信
  • ブラウザB、ブラウザCは、Answer SDPを受け取る
    • 受け取ったAnswer SDPをRTCPeerConnection.setRemoteDescription()で覚える

multi_callme

ソースの修正:複数通信の準備

それでは、ブラウザ側のソースも手を入れていきましょう。まず、複数のRTCPeerConnectionを扱えるように用意します。

// ---- for multi party -----
  let peerConnections = [];
  const MAX_CONNECTION_COUNT = 3;

  // --- RTCPeerConnections ---
  function getConnectionCount() {
    return peerConnections.length;
  }

  function canConnectMore() {
    return (getConnectionCount() < MAX_CONNECTION_COUNT);
  }

  function isConnectedWith(id) {
    if (peerConnections[id])  {
      return true;
    }
    else {
      return false;
    }
  }

  function addConnection(id, peer) {
    peerConnections[id] = peer;
  }

  function getConnection(id) {
    let peer = peerConnections[id];
    return peer;
  }

  function deleteConnection(id) {
    delete peerConnections[id];
  }

相手の映像を表示するvideoタグも、動的に生成して複数管理できるようにします。

let remoteVideos = [];
  let container = document.getElementById('container');

  // --- video elements ---
  function addRemoteVideoElement(id) {
    let video = createVideoElement('remote_video_' + id);
    remoteVideos[id] = video;
    return video;
  }

  function getRemoteVideoElement(id) {
    let video = remoteVideos[id];
    return video;
  }

  function deleteRemoteVideoElement(id) {
    removeVideoElement('remote_video_' + id);
    delete remoteVideos[id];
  }

  function createVideoElement(elementId) {
    let video = document.createElement('video');
    video.width = '160';
    video.height = '120';
    video.id = elementId;

    video.style.border = 'solid black 1px';
    video.style.margin = '2px';

    container.appendChild(video);

    return video;
  }

  function removeVideoElement(elementId) {
    let video = document.getElementById(elementId);
    container.removeChild(video);
    return video;
  }

さらにRTCPeerConnectionの接続や、相手からのメディアストリーム、videoタグを連動して扱う処理も追加しておきましょう。

// --- video elements ---
  function attachVideo(id, stream) {
    let video = addRemoteVideoElement(id);
    playVideo(video, stream);
    video.volume = 1.0;
  }

  function detachVideo(id) {
    let video = getRemoteVideoElement(id);
    pauseVideo(video);
    deleteRemoteVideoElement(id);
  }
  
  function isRemoteVideoAttached(id) {
    if (remoteVideos[id]) {
      return true;
    }
    else {
      return false;
    }
  }

  // --- RTCPeerConnections ---
  function stopConnection(id) {
    detachVideo(id);

    if (isConnectedWith(id)) {
      let peer = getConnection(id);
      peer.close();
      deleteConnection(id);
    }
  }

  function stopAllConnection() {
    for (let id in peerConnections) {
      stopConnection(id);
    }
  }

ソースの修正:シグナリングの変更

WebSocket直接利用から、socket.ioの利用に変わったので、シグナリングも変更します。

// ----- use socket.io ---
  let port = 3002;
  let socket = io.connect('http://localhost:' + port + '/');
  let room = getRoomName();
  socket.on('connect', function(evt) {
    socket.emit('enter', room);
  });
  socket.on('message', function(message) {
    let fromId = message.from;

    if (message.type === 'offer') {
      // -- got offer ---
      let offer = new RTCSessionDescription(message);
      setOffer(fromId, offer);
    }
    else if (message.type === 'answer') {
      // --- got answer ---
      let answer = new RTCSessionDescription(message);
      setAnswer(fromId, answer);
    }
    else if (message.type === 'candidate') {
      // --- got ICE candidate ---
      let candidate = new RTCIceCandidate(message.ice);
      addIceCandidate(fromId, candidate);
    }
    else if (message.type === 'call me') {
      if (! isReadyToConnect()) {
        console.log('Not ready to connect, so ignore');
        return;
      }
      else if (! canConnectMore()) {
        console.warn('TOO MANY connections, so ignore');
      }

      if (isConnectedWith(fromId)) {
        // already connnected, so skip
        console.log('already connected, so ignore');
      }
      else {
        // connect new party
        makeOffer(fromId);
      }
    }
    else if (message.type === 'bye') {
      if (isConnectedWith(fromId)) {
        stopConnection(fromId);
      }
    }
  });
  socket.on('user disconnected', function(evt) {
    let id = evt.id;
    if (isConnectedWith(id)) {
      stopConnection(id);
    }
  });

  // --- broadcast message to all members in room
  function emitRoom(msg) {
    socket.emit('message', msg);
  }

  function emitTo(id, msg) {
    msg.sendto = id;
    socket.emit('message', msg);
  }

通信開始要求の”call me”や、切断要求の”bye”の処理を追加しています。また接続準備が整っていない場合(カメラやマイクを取得していない場合)や、すでに接続中の相手からの接続要求は無視するようにしています。

SDPやICE candidateのハンドリング

複数の相手とOffer/Answer SDPやICE candidateをやり取りするので、相手を意識した処理に拡張します。といっても違いは対応するRTCPeerConnectionのオブジェクトを生成したり取り出したりするところだけで、他は1対1の場合と同様です。すべてを掲載すると長いので全体はGitHubを参照してくただくとして、ここでは例を取り上げます。

Offer SDPの送信

新たに RTCPeerConnectionを生成し、Offer SDPの作成、送信を行います。

function makeOffer(id) {
    peerConnection = prepareNewConnection(id);
    addConnection(id, peerConnection);

    peerConnection.createOffer()
    .then(function (sessionDescription) {
      console.log('createOffer() succsess in promise');
      return peerConnection.setLocalDescription(sessionDescription);
    }).then(function() {
      console.log('setLocalDescription() succsess in promise');

      // -- Trickle ICEの場合は、初期SDPを相手に送る -- 
      sendSdp(id, peerConnection.localDescription);

      // -- Vanilla ICEの場合には、まだSDPは送らない --
    }).catch(function(err) {
      console.error(err);
    });
  }

  function sendSdp(id, sessionDescription) {
    let message = { type: sessionDescription.type, sdp: sessionDescription.sdp };
    emitTo(id, message);
  }

Answer SDPを受け取った場合の処理

対応するRTCPeerConnectionを取り出し、Answer SDPを覚えさせます。

function setAnswer(id, sessionDescription) {
    let peerConnection = getConnection(id);
    if (! peerConnection) {
      console.error('peerConnection NOT exist!');
      return;
    }

    peerConnection.setRemoteDescription(sessionDescription)
    .then(function() {
      console.log('setRemoteDescription(answer) succsess in promise');
    }).catch(function(err) {
      console.error('setRemoteDescription(answer) ERROR: ', err);
    });
  }

ICE candidateのやりとりなども、同様に対応するRTCPeerConnectionのオブジェクトを取り出して行います。また、手動シグナリングで使っていたSDPをテキストエリアに表示する部分も取り除きました。

カメラ、マイクの取得

前回までは映像だけでしたが、今回はマイクの音声も取得してより実用的にしましょう。ただし1台のPCでやる場合はハウリングしてしまうので、ヘッドフォンを使ってください。

// start local video
  function startVideo() {
    getDeviceStream({video: true, audio: true})
    .then(function (stream) { // success
      localStream = stream;
      playVideo(localVideo, stream);
    }).catch(function (error) { // error
      console.error('getUserMedia error:', error);
      return;
    });
  }

getDeviceStream()は、新旧getUserMedia()をラップするために用意した関数です。これにaudio:trueの指定を渡してマイクも取得しています。

今回マイクの音声も取得するようにしたため、メディアストリームにはビデオとオーディオの2つのトラックが含まれます。このため、相手側のRTCPeerConnectionontrack()イベントが2回呼び出されますが、2回目は無視するように修正しました。(RTCPeerConnection.ontrack()は現在Firefoxのみがサポートしています)

function prepareNewConnection(id) {
    let pc_config = {"iceServers":[]};
    let peer = new RTCPeerConnection(pc_config);

    // --- on get remote stream ---
    if ('ontrack' in peer) {
      peer.ontrack = function(event) {
        let stream = event.streams[0];
        if (isRemoteVideoAttached(id)) {
          console.log('stream already attached, so ignore'); // <--- 同じ相手からの2回目以降のイベントは無視する
        }
        else {
          //playVideo(remoteVideo, stream);
          attachVideo(id, stream);
        }
      };
    }
    else {
      peer.onaddstream = function(event) {
        let stream = event.stream;
        console.log('-- peer.onaddstream() stream.id=' + stream.id);
        //playVideo(remoteVideo, stream);
        attachVideo(id, stream);
      };
    }

    // ... 省略 ....
  }

全体のソース

主要な部分は以上の通りですが、他にも細かい修正があります。全体のソースはGitHubでご覧ください。

接続してみよう

今回のサンプルでは4人まで同時に通信できるようにしてみました。それぞれブラウザを立ち上げて[Start Video]→[Connect]の順にボタンをクリックし接続してください。このように複数の相手と接続できるはずです。

multi_connected_4

※localhost以外に繋ぐときは、Chromeではカメラ/マイクの取得に失敗します。その場合はFirefoxをご利用ください。

次回は

今回のサンプルでは複数人で同時にビデオチャットができるようにしました。が、Chromeではhttp://~でgetUserMedia()が許可されていないため、他のPCと通信するのは厄介です。

Webサーバだけであれば、GitHub Pagesなどでhttps://~を利用することができますが、シグナリングサーバーのWebSocket通信も暗号化する必要がありまり、そちらは自分で証明書を取って対処しなけばなりません。

そこで次回は番外編として、Firebaseを使った暗号化通信をシグナリングに利用した例をご紹介したいと思います。

]]>
https://html5experts.jp/mganeko/20112/feed/ 0
Webブラウザで高速な演算を可能にする低水準言語asm.jsと、WebAssembly詳解ーasm.jsの仕組みとコーディング例 https://html5experts.jp/chikoski/18980/ https://html5experts.jp/chikoski/18980/#comments Mon, 25 Jul 2016 00:06:51 +0000 https://html5experts.jp/?p=18980 連載: 低水準言語asm.jsとWebAssembly詳解 (2)

連載の第1回目は asm.jsの紹介と、asm.jsが導入された背景を概観しました。

Just in Timeコンパイルによって高速にJavaScriptを実行できるようになりましたが、立ち上がりが遅い、やり直しが発生する、コンパイルによって一時的に負荷が向上する、といった問題が残されています。

これを解決するためにプログラムの実行を行うより前にネイティブコードへとコンパイルするAhead of Timeコンパイルを導入したいのですが、JavaScriptは柔軟すぎて効率の良いネイティブコードを出力することが難しい、という問題がありました。

asm.jsはこの問題に一定の解をあたえるものとなります。今回はそのasm.jsがどのようなものなのか、JavaScriptの関数を asm.js化しながら解説していきます。

asm.jsがコンパイルされるまで

前述したとおりasm.jsで記述されたプログラムは、実行以前にコンパイルされます。コンパイルは下図のような過程で行われます。

asm.jsがコンパイルされるまで。意味解析とリンク時のチェックが行われ、失敗すると通常のJSとして実行される。

ソースコードが字句解析、構文解析されてAST(Abstract Syntax Tree:抽象構文木)になるところまでは一緒ですが、その後意味解析が行われます。この意味解析でそれぞれの式や変数、関数の型がチェックされます。

意味解析が終了後、プログラムはコンパイルされネイティブコードが出力されます。このネイティブコードはメモリ上に展開されたあと、プログラム中で利用しているJavaScriptの関数やasm.jsに用意されている標準ライブラリとのリンクが行われます。

意味解析とリンク、この2つに失敗する場合もあります。プログラム中に型エラーが発見された場合は前者の失敗します。 後者はメソッドの呼び出しや、JSにエキスポートできない種類のデータを引数に指定した関数呼び出しを行った場合に失敗します。

このような場合、プログラムはasm.jsとして処理されるのではなく、通常のJSとして処理されます。asm.jsがJSのサブセットであることによって、このようなフォールバックも可能になっています。

asm.jsモジュール

asm.jsで書かれたプログラムとして事前コンパイルされる際の単位は、モジュールです。ファイル単位でコンパイルが行われるわけではないので、1つのJSファイルのうち高速化が必要な部分をasm.jsで書き、それ以外の部分は通常のJSとして書くといったことが可能です。

asm.jsのモジュールは、次のようにCやC++のソースコードと似た構造となっています。

function ModuleName(stdlib, ffi, heap){
"use asm";
 // (1) 外部からインポートするシンボルの宣言
 // (2) 関数宣言
 // (3) 関数表の宣言
 // (4) モジュールのエキスポート
}

1の部分では利用する標準ライブラリや、JSの関数、定数などのシンボルを列挙します。ちょうどCのextern宣言と同じような役割です。

2の部分で、それぞれの処理を関数として定義します。asm.jsではオブジェクトやクラスの定義が許されていません。そのため処理はクラスやオブジェクトとしてではなく、あくまで関数として定義します。

3の部分では同じ型の関数をまとめた表を定義できます。ちょうどCでの関数ポインタの機能を代替するものです。関数を直接呼び出すのではなく、この表を参照する形で呼び出すことで、呼び出す関数の振る舞いを変えられるので、多態性を持った関数を定義したい場合に有用です。

とはいえ、いまいちイメージがつかめないかと思います。そこで足し算を行うモジュールをasm.js化しながら、構成を見てゆきましょう。変更するのは以下のようなモジュールです。

function AddFunctions(){
  function add1(value){
    var result;
    result = value + 1;
    return result;
  }
  return {
    add1: add1
  }
}

このモジュールは次のように利用できます。

const module = AddFunctions();
const one = module.add1(0);    // 1
const two  = module.add1(one); // 2

asm.jsディレクティブ

asm.js化の第一歩は、ディレクティブの追加です。”use strict” ディレクティブをつけると、その関数はstrict modeで解釈されるのと同様に、”use asm”ディレクティブをつけることで、処理系はその関数をasm.jsのモジュール定義として処理します。

function AddFunctions(){
  "use asm";
  function add1(value){
    var result;
    result = value + 1;
    return result;
  }
  return {
    add1: add1,
  }
}

 型アノテーション

次にAOTを行うための型アノテーションを行います。型アノテーションは、TypeScriptなどのように型を直接記述する方法が一般的かと思いますが、JavaScriptとしても解釈できなくてはいけないasm.jsでは異なります。同値となるような式を追加することで、型情報を明示します。

明示的に型アノテーションを行う対象は次の3つです。

  • 関数の引数
  • 変数
  • 関数の返り値

これらの情報を元に、関数の型や式の型が決定されます。

引数に対する型アノテーション

引数に対する型アノテーションは、関数本体の先頭で行います。次の例では、add1の引数valueの型はintであることを示す型アノテーションが加わっています。value = value | 0; が型アノテーションを行っている部分です。

function AddFunctions(){
  "use asm";
  function add1(value){
    value = value | 0;
    var result;
    result = value + 1;
    return result;
  }
  return {
    add1: add1
  }
}

引数で利用できる型はint, doubleの2つです。それぞれの型はは次の表のようにアノテーションします。

型アノテーション
int value = value | 0
doube value = +value
float value = f(value)

尚、外部で定義された関数呼び出しを行った場合は、floatとして解釈されます。

変数宣言

asm.jsでは、関数内で利用する変数に対しても型アノテーションを行います。これは宣言時に初期値として代入するを適切に選ぶ形で行います。整数値を代入すればintに、実数値の場合はdoubleとなります。

尚、1.0のような小数点以下の数字が0のものは実数値として扱われます。変数宣言に型アノテーションをつけると、先ほどまでのモジュールは以下のようになります。

function AddFunctions(){
  "use asm";
  function add1(value){
    value = value | 0;
    var result = 0; // intとして宣言
    result = value + 1;
    return result;
  }
  return {
    add1: add1
  }
}

返り値に対する型アノテーション

返り値に対して型アノテーションを行います。この情報と引数の型情報とを組み合わせて、関数の型が決定されます。

返り値で利用できるのはdouble, signed, float, そしてvoidの4つの型です。それぞれのアノテーション方法は以下の表の通りです。また即値を書く場合は、アノテーションは必要ありません。

アノテーション例 メモ
double return +result;
signed return result 0|
float return f(result) fは関数
void return;

asm.jsの型システム

これまでintやdoubleといった型を利用してきましたが、asm.jsで利用できる型を列挙し、それぞれの継承関係を示すと次の図となります。 矢印の元にある型は先の型を継承しています。

asm.js の型システム http://asmjs.org/spec/latest より引用

継承するということは、何かの制約が厳しくなっていくということです。asm.jsの型システムではnullを許容するか、実行時に割り当てられるレジスタがdoubleかintか、といった点での制約が厳しくなっていきます。

また、JavaScriptで定義されている関数に渡せる型も決まっています。背景色が薄い灰色になっているfixnum / signed / extern / double型のデータのみが許可されています。

型のキャスト

先ほどから変更しているプログラムは、意味解析に失敗します。それは次の部分に原因があります。

result = value + 1;

これはint+fixnumの計算を行い、その結果をint型の変数に代入しようとしています。型エラーの入り込む余地はなさそうに思えます。しかし、この加算の評価値の型はinterishとなっています。そのため、int型へinterish型の値を代入することになり、型エラーが起きるというわけです。

そこでキャストを行い、演算結果の型を明示します。この変更をおこないasm.jsの意味解析に成功するコードは次のようになります。

function AddFunctions(){
  "use asm";
  function add1(value){
    value = value | 0;
    var result = 0; // int として宣言
    result = (value + 1) | 0; // int へキャスト
    return result;
  }
  return {
    add1: add1
  }
}

JavaScriptとの組み込み

以上で、AddFunctionsモジュールをasm.js化することができました。これをJavaScriptのプログラムに組み込みんでいきます。

ここでは 上記で作成したAddFunctionsモジュールをJavaScript側から利用する方法と、AddFunctionsモジュール内で JavaScriptの関数を利用する方法について説明します。

JavaScriptからの利用

asm.jsのモジュールとJavaScriptのモジュールは、JavaScriptからみると区別できません。下記のようにJSのモジュールを呼ぶように利用できます。

const module = AddFunctions();
var one = module.add1(0);
var two = module.add1(one);

function add2(n){
  return module.add1(module.add1(n));
}

JavaScriptの関数をasm.jsから呼ぶには

まず、asm.jsの内部からJavaScriptで定義された関数を呼ぶことはできます。また、Mathオブジェクトの持っているいくつかのメソッドは、標準ライブラリ中の関数として提供されています。

これら関数への参照はモジュールを定義する関数の引数として与えます。例えば、AsmModuleにasm.jsモジュールが定義されている場合、次のように呼び出すことでJavaScriptの関数をasm.js内から呼び出せます。

const ffi = {
  put: n => console.log(n)
};

const module = AsmModule(window, ffi);

asm.jsで定義される関数はオブジェクトの解決ができません。そのため利用する関数はあらかじめ外部からインポートするシンボルとして宣言しておきます。

次の例では、標準ライブラリ中のMath.expとMath.log、そして自作関数であるputを外部からインポートするシンボルとして宣言しています。

function AsmModule(stdlib, ffi, heap){
  "use asm";
  var exp = std.lib.Math.exp;
  var log = std.lib.Math.log;

  var put = ffi.put;

これらの関数は、関数定義内でasm.js内部で定義された関数と同様に呼び出せます。ただ1点注意しなくてはならないのは、引数に渡すデータの型です。標準ライブラリ以外の外部関数に渡せるのはfixnum、signed、extern、doubleのいずれかです。 それ以外の値を渡すとリンクエラーとなり、通常のJSとして実行されます。演算の結果を適切にアノテーションすることで、リンクエラーを避けられます。

var value = 1;
put(value + 1); // リンクエラー
put((value + 1) | 0); // OK

ヒープの利用

asm.jsで定義される関数は、数値演算しかできません。また、オブジェクトの解決もできません。つまり、次のような関数は定義できないことになります。

function caesar(string, key){
  var result = "";
  for(let i = 0; i < string.length; i++){
    result += String.fromCharCode(a.charCodeAt(0)+key);
  }
  return result;
}

ところでC言語では文字列を数値の配列として扱います。この考えを応用すれば、asm.jsでも文字列を数値演算の範囲で扱えるようになります。

上記の関数をasm.jsに書き直すと以下のようになります。

function Caesar(stdlib, ffi, heap){
  "use asm";
  var HEAP = new stdlib.Int8Array(heap);

  function encrypt(key){
    key = key | 0;
    var i = 0;
    for(;(HEAP[i << 0 >> 0] | 0) != 0; i = i + 1 | 0){
      buffer[i << 0 >> 0] = ((buffer[i << 0 >> 0] | 0) + key) | 0;
    }
    return;
  }

文字列はHEAPというArrayBufferに格納されています。このArrayBufferはモジュールの定義時に引数として与えられます。 ArrayBufferのビューは標準ライブラリとして提供されているため、上記のようにモジュール内のHEAPを大域変数として宣言する際にビューもあわせて定義します。

HEAPの添字は、ビューの各要素の大きさに合わせてシフトする必要があります。シフトするビット数は、2を底として要素のバイトサイズのlogをとると求まります。

上記の例で利用しているInt8Arrayの場合、各要素の大きさは1バイトのため、0ビットシフトしています。ビューとシフトするビット数の対応は次の表を参照してください。

ビュー 要素のサイズ(バイト) シフトするビット数 ロード時の型 保存時の型
Uint8Array 1 0 intish intish
Int8Array 1 0 intish intish
Uint16Array 2 1 intish intish
Int16Array 2 1 intish intish
Uint32Array 4 2 intish intish
Int32Array 4 2 intish intish
Float32Array 4 2 float? floatish, double?
Float64Array 8 3 double? float?, double?

ArrayBufferを与えてモジュールの作成と関数の呼び出しを行うと、次のようなコードとなります。

気をつけなければならいのは、TypedArrayの大きさです。212 以上、224バイト未満の大きさになるようにするか、224バイトの整数倍の大きさになるようにしてください。そうしなければ、リンクに失敗してします。

これが原因でリンクに失敗した場合は、コンソールに適切なサイズが表示されます。それを参考に大きさを際設定すればようでしょう。

const heap = new Int8Array(0x10000)
const caesar = Caesar(window, {}, heap);
heap[0] = 72; heap[1] = 65; heap[2] = 76; // HALと設定
caesar.encrypt(1); // HALが1文字ずつシフトされる

まとめ

以上のように、JavaScriptと比べてasm.jsは随分と書きづらく、できることも限られています。ArrayBufferを駆使すればベクトルの計算も可能ですが、オブジェクトと平坦なArrayBufferとの相互変換を自分で実装しなくてはならず、なかなか骨が折れる作業であることは否めません。

その代わり得られる効果は絶大です。JITによって処理が重たくなることもなく、高速な実行が可能となります。またコンパイルされた結果はキャッシュされるため、2回目以降は高速に起動できるようになります。

とはいえ、手で書くのは骨が折れます。

「人間のやることではない」

「高級言語で実装したい」

そう思う方も多いでしょう。そのために用意されているツールがEmscriptenです。次回はEmscriptenを利用したC言語やC++で実装されたコードのasm.jsへの変換について解説します。

]]>
https://html5experts.jp/chikoski/18980/feed/ 0
シグナリングサーバーを動かそう ーWebRTC入門2016 https://html5experts.jp/mganeko/20013/ https://html5experts.jp/mganeko/20013/#comments Thu, 14 Jul 2016 00:45:36 +0000 https://html5experts.jp/?p=20013 連載: WebRTC入門2016 (3)

こんにちは! 2014年に連載した「WebRTCを使ってみよう!」シリーズのアップデート記事も3回目となりました。今回は、前回の「手動」で行ったP2P通信の準備を、自動で行えるようにしてみましょう。

シグナリングサーバーを立てよう

前回は手動でコピー&ペーストを行い、WebRTCのP2P通信を始めるために次の情報を交換しました。

  • SDP
  • ICE candidate

今回はこれを仲介するサーバー(シグナリングサーバー)を動かしてみましょう。方法として次の2つをご用意しました。

  • Node.jsを使ったシグナリングサーバー
  • Chromeアプリ

Node.jsを準備しよう

まず、WebSocketを使ってシグナリングを行う方法をご紹介します。WebSocketの扱いやすさから、ここではNode.jsを使います。(もちろん他の言語を使っても同様にシグナリングサーバーを作ることができます)こちらの公式サイトから、プラットフォームに対応したNode.jsを入手してインストールしてください。今回私は 4.4.7 LTSを使いました。

Node.jsのインストールが完了したら、次はWebSocketサーバー用のモジュールをインストールします。コマンドプロンプト/ターミナルから、 次のコマンドを実行してください。 ※必要に応じて、sudoなどをご利用ください。

npm install ws

以前の連載ではsocket.ioを使いましたが、今回はよりプリミティブなwsを使っています。

シグナリングサーバーを動かそう

次のコードを好きなファイル名で保存してください。(例えば signaling.js)

"use strict";

let WebSocketServer = require('ws').Server;
let port = 3001;
let wsServer = new WebSocketServer({ port: port });
console.log('websocket server start. port=' + port);

wsServer.on('connection', function(ws) {
  console.log('-- websocket connected --');
  ws.on('message', function(message) {
    wsServer.clients.forEach(function each(client) {
      if (isSame(ws, client)) {
        console.log('- skip sender -');
      }
      else {
        client.send(message);
      }
    });
  });
});

function isSame(ws1, ws2) {
  // -- compare object --
  return (ws1 === ws2);     
}

ポート番号は必要に応じて変更してください。起動するにはコマンドプロンプト/ターミナルから、 次のコマンドを実行します。

node signaling.js

シグナリングサーバーの動作はシンプルで、クライアントからメッセージを受け取ったら他のクライアントに送信するだけです。

Chromeアプリを使う場合は

場合によってはNode.jsをインストールして動かすのは、ハードルが高くて難しいケースもあるかもしれません。そんな人のために、Chromeアプリで「simple message server」というものを作ってみました。 simple_message_server_store
Chromeを利用したアプリとしてインストールし、アプリタブから起動して利用します。デスクトップ用のChromeが動く環境(Windows, MaxOS X, Linux, ChromeOS)で動くはずです。

起動すると、 ws://localhost:3001/ でクライアントからの接続を待ち受けます。※実装があまいので時々不安定になります。その場合は[restart]ボタンを押してリセットし、ブラウザもリロードして接続しなおしてください。

シグナリング処理を変更しよう

それでは前回の手動シグナリングのコードを、少しずつ変更していきましょう。まずWebSocketで用意したシグナリングサーバーに接続します。JavaScriptに次の処理を追加してください。(URLは使っているポートに合わせて修正してください)

let wsUrl = 'ws://localhost:3001/';
  let ws = new WebSocket(wsUrl);
  ws.onopen = function(evt) {
    console.log('ws open()');
  };
  ws.onerror = function(err) {
    console.error('ws onerror() ERR:', err);
  };

次に、WebSocketでメッセージを受け取った場合の処理を追加します。

ws.onmessage = function(evt) {
    console.log('ws onmessage() data:', evt.data);
    let message = JSON.parse(evt.data);
    if (message.type === 'offer') {
      // -- got offer ---
      console.log('Received offer ...');
      textToReceiveSdp.value = message.sdp;
      let offer = new RTCSessionDescription(message);
      setOffer(offer);
    }
    else if (message.type === 'answer') {
      // --- got answer ---
      console.log('Received answer ...');
      textToReceiveSdp.value = message.sdp;
      let answer = new RTCSessionDescription(message);
      setAnswer(answer);
    }
  };

JSONテキストからオブジェクトを復元し、typeに応じて前回用意したsetOffer()/setAnswer()を呼び出し、RTCPeerConnectionに渡しています。

SDPの送信

Offer/AnswerのSDPの送信も、WebSocket経由で行います。前回要したsendSdp()を次のように変更します。

function sendSdp(sessionDescription) {
    console.log('---sending sdp ---');

    textForSendSdp.value = sessionDescription.sdp;
    /*--- テキストエリアをハイライトするのを止める
    textForSendSdp.focus();
    textForSendSdp.select();
    ----*/

    // --- シグナリングサーバーに送る ---
    let message = JSON.stringify(sessionDescription);
    console.log('sending SDP=' + message);
    ws.send(message);
  }

SDPをJSONテキストに変換してWebSocketでシグナリングサーバーに送信しています。

実際に動かしてみよう

シグナリングサーバーを起動して、ChromeかFirefoxのウィンドウを2つ開いて修正したHTMLを読み込んでください。ChromeとFirefoxの間で通信することもできます。

Webサーバーを立てるのが難しい場合は、GitHub Pages にもサンプルを公開しているので、そちらで試すこともできます。その場合でもシグナリングサーバーは自分で用意する必要があるのでご注意ください。

(1) カメラの取得

両方のウィンドウで[Start Video]ボタンをクリックします。カメラのアクセスを許可すると、それぞれリアルタイムの映像が表示されます。
ws_signaling_startvideo

(2) 通信開始

どちらかのウィンドウで[Connect]ボタンを押します。(3)SDP(ICE candidateを含む)が自動で交換され、(4)ビデオ通信が始まります。
ws_signaling_connect

手動シグナリングに比べて操作がずっと簡単になりました。これなら実際に利用できそうですね。

Trickle ICE を使ってみよう

コピー&ペーストを手動で行う必要がなくなったので、ICE candidateを発生するたびに交換するTrickle ICE を使ってみましょう。流れはこのような形になります。
hand2016_trickle
すべてのICE candidateが出そろう前にP2P通信が確立する(ことがある)メリットがあります。(※2014年の記事では「すべてのICE candidateの交換が終わるとP2P通信が始まる」と書いていましたが、これは誤りです)

SDPをすぐに送信する

Offer SDP/Answer SDPを生成したら、すぐに相手に送るように変更します。

function makeOffer() {
    peerConnection = prepareNewConnection();
    peerConnection.createOffer()
    .then(function (sessionDescription) {
      console.log('createOffer() succsess in promise');
      return peerConnection.setLocalDescription(sessionDescription);
    }).then(function() {
      console.log('setLocalDescription() succsess in promise');

      // -- Trickle ICE の場合は、初期SDPを相手に送る -- 
      sendSdp(peerConnection.localDescription); // <--- ここを加える

      // -- Vanilla ICE の場合には、まだSDPは送らない --
    }).catch(function(err) {
      console.error(err);
    });
  }

  function makeAnswer() {
    console.log('sending Answer. Creating remote session description...' );
    if (! peerConnection) {
      console.error('peerConnection NOT exist!');
      return;
    }
    
    peerConnection.createAnswer()
    .then(function (sessionDescription) {
      console.log('createAnswer() succsess in promise');
      return peerConnection.setLocalDescription(sessionDescription);
    }).then(function() {
      console.log('setLocalDescription() succsess in promise');

      // -- Trickle ICE の場合は、初期SDPを相手に送る -- 
      sendSdp(peerConnection.localDescription); // <--- ここを加える

      // -- Vanilla ICE の場合には、まだSDPは送らない --
    }).catch(function(err) {
      console.error(err);
    });
  }

ICE candidateも、すぐに交換する

ICE candidateを収集した際も、すぐに送るように変更します。

function prepareNewConnection() {
    // ... 省略 ...

    // --- on get local ICE candidate
    peer.onicecandidate = function (evt) {
      if (evt.candidate) {
        console.log(evt.candidate);

        // Trickle ICE の場合は、ICE candidateを相手に送る
        sendIceCandidate(evt.candidate); // <--- ここを追加する

        // Vanilla ICE の場合には、何もしない
      } else {
        console.log('empty ice event');

        // Trickle ICE の場合は、何もしない
        
        // Vanilla ICE の場合には、ICE candidateを含んだSDPを相手に送る
        //sendSdp(peer.localDescription); // <-- ここをコメントアウトする
      }
    };

    // ... 省略 ....
  }

  function sendIceCandidate(candidate) {
    console.log('---sending ICE candidate ---');
    let obj = { type: 'candidate', ice: candidate };
    let message = JSON.stringify(obj);
    console.log('sending candidate=' + message);
    ws.send(message);
  }

合わせてICE candidateをWebSocket経由で受け取った場合の処理も追加しましょう。相手からICE candidateを受け取ったら、その度にRTCPeerConnection.addIceCandidate()で覚えさせます。

ws.onmessage = function(evt) {
    console.log('ws onmessage() data:', evt.data);
    let message = JSON.parse(evt.data);
    if (message.type === 'offer') {
      // -- got offer ---
      console.log('Received offer ...');
      textToReceiveSdp.value = message.sdp;
      let offer = new RTCSessionDescription(message);
      setOffer(offer);
    }
    else if (message.type === 'answer') {
      // --- got answer ---
      console.log('Received answer ...');
      textToReceiveSdp.value = message.sdp;
      let answer = new RTCSessionDescription(message);
      setAnswer(answer);
    }
    else if (message.type === 'candidate') { // <--- ここから追加
      // --- got ICE candidate ---
      console.log('Received ICE candidate ...');
      let candidate = new RTCIceCandidate(message.ice);
      console.log(candidate);
      addIceCandidate(candidate);
    }
  };

  function addIceCandidate(candidate) {
    if (peerConnection) {
      peerConnection.addIceCandidate(candidate);
    }
    else {
      console.error('PeerConnection not exist!');
      return;
    }
  }

さあ、これで修正は完了です。

Trickle ICEを実行しよう

手順はVanilla ICEの場合と同じです。シグナリングサーバーを起動して、ChromeかFirefoxのウィンドウを2つ開いて修正したHTMLを読み込んでください。あとは同様に[Start Video]→[Connect]です。

見た目も特に変わりはありません。もしかしたら人によっては早く繋がるのを実感できるかもしれません。

GitHub Pages/GitHubも用意しています。

2台のPC間の通信

ここまできたら、せっかくなので2台の別々のPCで通信してみたくなります。同じネットワークに属するPC同士ならば通信できるはずです。例として次のような状況を考えてみましょう。

  • IPアドレスが 192.168.0.2 と、 192.168.0.3 の2台のPCがある
  • 前者(192.168.0.2)のポート:8080でWebサーバー、ポート:3001でNode.jsのシグナリングサーバーが動いている
    2pc_firefox

Firefoxの場合は、接続するURLを変更すれば問題なく動きます。やっかいなのはChromeの場合です。

  • Chromeでは、カメラやマイクにアクセスするためのgetUserMedia()が、原則としてhttp://~では許可されていない
  • http://localhost/~ は例外的な扱いで許可されている
    2pc_chrome

きちんと対処すると、次のような対策が必要です。ちょっと試すにはハードルが高いですよね。

  • 証明書を取得して https://~ でアクセスするように、Webサーバーに設定
  • 合わせて、シグナリングサーバーも wss://~ の暗号化通信を使うように設定

そこで実験的に無理やり動かすには、次のような方法があります。Webサーバーとシグナリングサーバーは同一である必要はなく、また異なるWebサーバーでも構わないことを利用しています。
2pc_chrome_force

お勧めはしませんが、どうしてもやりたい場合の参考としてどうぞ。

次回は

今回はNode.jsとWebSocketを使ったシグナリングを実現しました。残念ながら今回の仕組みでは、1対1の通信しか行うことができません。次回はこれを拡張し、複数人で同時に通信できるようにしたいと思います。

オマケ:WebRTCの仕様の差分のおさらい

オマケとして、今回のWebRTC再入門2016シリーズで取り上げているWebRTC関連仕様の変更箇所について、おさらいしおきましょう。(2016年6月現在)

getUserMeida

  • navigator.mediaDevices.getUserMedia() が新しく用意された
    • 旧APIの navigator.getUserMedia()は Firefoxでは非推奨
  • ベンダープレフィックスが取れた
  • コールバックではなくPromiseベースになった
  • Firefox, Edge で利用可能。Chromeではフラグ指定が必要

ベンダープレフィックスの除去

  • Firefoxでは、主要なオブジェクトのベンダープレフィックスが取れた。mozプレフィックス付は非推奨に
    • 新:RTCPeerConnection, RTCSessionDescription, RTCIceCandidate
    • 旧:mozRTCPeerConnection, mozRTCSessionDescription, mozRTCIceCandidate (非推奨)
  • ただしChromeでは、一部ベンダープレフィックス付のまま
    • プレフィックス有り: webkitRTCPeerConnection
    • プレフィックス無し: RTCSessionDescription, RTCIceCandidate

RTCPeerConnection

  • 主要なメソッドがPromiseベースになった
    • createOffer(), createAnswer()
    • setLocalDescription(), setRemoteDescription()
  • メディアストリーム処理の新しいイベントハンドラontrack()が追加、onaddstream()は非推奨
    • Firefoxではサポート済、Chromeでは未サポート

仕様は常に更新されていますし、ブラウザの実装状況も異なります。最新の情報もご確認ください。

]]>
https://html5experts.jp/mganeko/20013/feed/ 0
HTTPSにまつわる怪しい伝説を検証する──Google I/O 2016 セッションレポート https://html5experts.jp/takoratta/20061/ https://html5experts.jp/takoratta/20061/#comments Wed, 13 Jul 2016 02:00:08 +0000 https://html5experts.jp/?p=20061 今年はGoogle I/Oに初めて社員ではない立場で参加しました。全体の感想は Google I/O 2016まとめ(Web的視点) で公開していますが、今回はその中で、気に入ったセッションの1つである”Mythbusting HTTPS: Squashing security’s urban legends”について書いてみたいと思います。

セッションは大変良くまとまっていますので、YouTubeにあがっている動画を見れる人はそちらを見てもらえればいいのですが、時間がないという人のために、その内容をまとめました。基本的には文字起こしに近いものです。

重要だとわかっているけど、なかなか導入に踏み切れない人も多いHTTPS。これについて、最新の状況が理解できるコンテンツとしてお役に立てるならば嬉しいです。

※この記事は、Qiitaに投稿された記事を、Qiitaの許可を得て転載したものです。

TL;DR

  • HTTPSはPWAppなどWebにとって必須。
  • しかし、パフォーマンス悪化するかも!? でもお高いんでしょ? それに見合うだけのメリットあるの?などなどの疑問(=都市伝説的なものも含む)があるので、それを1つ1つ検証。
  • 結論: 確かに懸念すべきものもあるが、やはり必須。正しく、早急に導入しよう。

Mythbusting HTTPS: Squashing security’s urban legends

“Mythbusting HTTPS: Squashing security’s urban legends” はGoogle I/O 2016の初日の夕方に行われたセッションです。ChromeのセキュリティチームでSSL/TSLを担当するエンジニアであるエミリー(Emily Stark)自らが登壇しました。

Mythbustingとは

Mythとは英語で「神話」を意味します。Bustにはいくつか意味がありますが、ここではゴーストバスターズなどと同じように、「退治する」、「撃退する」の意味になります。実は、オーストラリアで制作され、米国でも放映されているMythbustersという人気テレビ番組があります。都市伝説のような伝説を検証する科学エンターテイメント番組なのですが、タイトルのMythbustingはもしかしたらそこからとったのかもしれません。

Mythbustersは日本でもケーブルテレビなどで放映されておりますが、そのタイトルは「怪しい伝説」となっています。この投稿のタイトルもそれに習いました。

セッション動画

セッション動画はここから。英語が苦手な方も、Google Developers Japan: 技術と英語を同時に、しかも無料で勉強できる画期的な方法 にあるように字幕付きならば、技術的な内容ですので、十分内容を掴めるはずです。

HTTPSのおさらい

セッションでは、スピーカーであるエミリーが2010年の世界から見ると、2016年の今日のWebは大きく進化したというところから話し始めます。ホーム画面への追加(Add to Home screen)やプッシュ通知、デバイスオリエンテーション、ジオロケーションなどプログレッシブWebアプリケーションをはじめとするWebをよりアプリに近づける技術が使われるようになっています。

一方で、そのようなアプリケーション的なWebが普及する中、未だに課金や商取引、個人情報をやりとりするようなデータを扱っていながら、第三者から盗み見されるようなネットワークで運用されているところもあったり、ISPやWiFiプロバイダーのような第三者がWebサイト運営者の意図しないようにコンテンツを書きかえることさえ起きています。

データ盗聴の危険性

意図しないコンテンツの挿入

エミリーはだからこそHTTPSが重要だと強調します。

現在のブラウザはHTTPSでアクセスしているサイトには、それがHTTPSであることをインディケーターで示します。ですが、すべてのWebサイトがHTTPSをサポートし、この緑のインディケーターを必要としない世界こそ、望むべきものです。

HTTPSには3つの役割があります。

HTTPSの役割

  • 認証(Identity): https://google.com にアクセスすると、ブラウザはgoogle.comから証明書を受信し、それにより今アクセスしているgoogle.comが真正のgoogle.comであることを確認します。
  • 守秘性(Confidentiality): 一度、真正のgoogle.comにアクセスしていることが確認されると、その後の通信はgoogle.comとブラウザ以外には盗聴されない形で行われます。
  • 完全性(Integrity): google.comとブラウザの間でのデータは第三者により改ざんされないことが保証されます。

このようなセキュリティ上の利点があるものの、HTTPSを導入しようとしたときに、コスト、パフォーマンス、そして保守の面で課題があるのではないかと心配されるかもしれません。しかし、それは過去のものです。そのようにエミリーは言います。

HTTPSに関しての誤解

HTTPSにまつわる話

これらの誤解は結局のところ、5つほどの話に整理できます。それらのうちのいくつかは過去には正しかったものの、現在では都市伝説になっているものもあります。エミリーは5つの話を現在でも事実の話と怪しい伝説とに分類していきます。

5

その5つの話は次の通りです。

  1. 私のサイトはHTTPSを必要とするほど重要ではない
  2. HTTPSを導入すると遅くなる
  3. HTTPSに関しての攻撃がたくさんあるみたい
  4. HTTPSにはお金がたくさんかかる
  5. 私のサイトはHTTPSに移行できるけど、使っている(依存している)サードパーティ(のサービス)はどうしたらいいの?

それでは、エミリーが1つ1つをどのように解説していったかをお伝えしましょう。

「私のサイトはHTTPSを必要とするほど重要ではない」

エミリーはアリスの話を始めます。

アリスというWebデベロッパーがいます。彼女の旅行ガイドのWebサイトはログインフォームもなければ、クレジットカード番号の入力フォームもない。そこでアリスは自分のサイトにはHTTPSは必要ないと考えます。ですが、ある日、彼女の友人からアリスの旅行ガイドサイトが遅いと言われて、調べてみると、友人が見ている彼女のサイトには彼女が入れたのではない様々な広告が挿入されていたことに気づくのです。それらにはマルウェアが埋め込まれていて、アリスのサイトを信じた友人はサイト内の広告もクリックし、マルウェアに感染してしまっていたのです。

アリスはまた新しい機能をジオロケーションAPIを用いて実装しようとします。ユーザーがどこにいるかを把握し、適切なトラベル情報を提供しようと試みたのです。しかし、彼女はChromeでは動作しないことに気づきます。

ジオロケーションAPI

現在、Chromeを始めとする多くのモダンブラウザはジオロケーションAPIをはじめとするパワフルな新しいAPIの利用はセキュアはHTTPS上でしか動作しないようにし始めています。

HTTPSを必要とする機能

このように、HTTPSはプライバシーやセキュリティ上の重要なデータのやりとりが発生するようなサイトだけでなく、ユーザー体験を向上させるすべてのサイトに必要なものとなっています。

つまり、この話はです。

「HTTPSを導入すると遅くなる」

次はボブの話。ボブはEコマースサイトを持っています。彼はサイトを分析し、コンバージョン最適化のためにはレイテンシーを下げることが必要と判断しましたが、HTTPSとパフォーマンスについてあまりよくない話を聞きました。

HTTPSに移行したyell.comはパフォーマンスの差(HTTPとHTTPS)は相対的に大きかったと言っています。しかしながら、2010年にGmailをHTTPSに移行したGoogleは無視できるほどの差しかなかったとしています。

ボブはこれらの話を知り、さらに自分自身でも調査をしました。

エミリーはボブや一般のWebデベロッパーのためにHTTPSにするとネットワーク層で何が起きるのかを説明し、どのように遅延を避けるかを解説します。

HTTPS移行で起きること

一般のユーザーはブックマークするなどした以前のHTTPのURLでサイトにアクセスしてきますので、まずはHTTPからHTTPSへのリダイレクトが起きます。そしてその次にTLSハンドシェイクで証明書のやりとりと双方がTLSを使えることの確認が行われます。

HSTS

まず最初のHTTPからHTTPSへのリダイレクトに関しては、HTTP Strict Transport Security(HSTS)の導入を勧めます。HTTPヘッダーにHSTSの記述をすることで、次回からのアクセス時にブラウザはHTTPでのURLを自動的に内部でHTTPSに置き換えます。これにより2度目以降はこのリダイレクトは発生しなくなります。

注意事項としては、これは一度セットされると、HTTPヘッダーが失効するまで有効ですので、HTTPSのテスト時や完全にHTTPSでのアクセスをサポートできるまでは行ってはいけません。

TLS False Start

次のTLSハンドシェイクにおける最適化は2ウェイラウンドトリップをいかに高速化するかに関わるのですが、ここではTLS False Startの利用を勧めます。

TLS False Start

TLS False Startは2ウェイラウンドトリップを待つことなく、クライアントであるブラウザがHTTPSでの通信をリクエストするものです。

TLS Session Resumption

もう1つの最適化はTLS Session Resumptionと呼ばれるもので、一度TLS通信を行った場合、以前に使ったセッションIDを用いることで、同じTLSハンドシェイクは必要ないと省略するものです。

TLS Session Resumption

HTTP/2

ここでエミリーはHTTP/2のいくつかの特徴を話します。

HTTP/2はリクエストとレスポンスを並列で行うパイプライン化により、1つのリクエストのレスポンスを待たずに次のリクエストを送れます。また、サーバープッシュにより、ブラウザがHTMLをパースし、追加のリソースであるスタイルシート(CSS)やJavaScript、イメージなどをリクエストするのを待たずに、サーバー側から必要となるリソースをプッシュすることが可能です。

HTTP/2には他にもさまざまな利点があります。

ここで大事なのは、このHTTP/2はHTTPSでのみ利用可能なことです。

エミリーはHTTP/2がHTTPSでのみ利用可能になっているのには、2つの理由があると言います。1つがHTTPSを普及させるためのインセンティブとして、つまりHTTP/2を使いたいならば、HTTPSを使わなければいけないとすることにより普及を目論んでいるということです。もう1つがプロキシサーバーなどの中継機器によりHTTP/2の通信が阻害されることを避けるためです。

ここで、さきのボブの話に戻ります。ボブが知ったyell.comのHTTPS移行の話ですが、実は彼らが遭遇したパフォーマンス上の課題は古いロードバランサーによるものであったそうです。彼らは将来的にはHTTP/2にアップグレードすることで、TLSネゴシエーションを改良し、RTTを減らすことで、HTTPSへの移行をネガティブなインパクトではなく、ポジティブなインパクトに変えたいと言っています。

実際に、別の事例としてweather.comがあります。彼らはHTTPSに移行した際にネガティブなインパクトがあったのですが、最終的にHTTP/2にアップグレードすることで、それらはほとんど解消できたそうです。

ボブはこれらの調査結果を踏まえて、解説されたような設定を行うとともに、HTTP/2にアップグレードすることで、パフォーマンスの心配なく、HTTPSに移行することができました。

結論として、この話、「HTTPSを導入すると遅くなる」は(ほとんど)嘘とエミリーは言います。「ほとんど」となっているのは、HTTP/2へのアップグレードなどを含めて総合的にはという意味でしょう。

参考情報

HTTPSに移行したyell.comの話はHTTPS is Hard – The Yell Blogに詳しく書かれています。これは大変読み応えのある記事なので、英語ですがお勧めです。

また、HSTSについては以前書いたHSTS (HTTP Strict Transport Security) の導入 – Qiitaに簡潔にまとめてあります。

HSTSを含むTLSの設定などは、IPA(情報処理推進機構)のSSL/TLS暗号設定ガイドライン~安全なウェブサイトのために(暗号設定対策編)~がお勧めです。

「HTTPSに関しての攻撃がたくさんあるみたい」

今度はイブの話。技術的な知識もあるイブはちょっとお金に困っていました。そこでHTTPSの脆弱性の話に目をつけ、あるHTTPSの脆弱性を持ったまま放置されているサイトを探します。クレジットカードやログイン情報を持っているサイトなどで脆弱性が放置されているサイトにアクセスします。

これは実際に起こりうる話です。

TLS脆弱性

HTTPSのセキュリティの研究は最近とても注目されており、単なる攻撃の可能性を理論で示すだけではなく、より実践的な形で攻撃を示したり、インターネット上で実際に脆弱なサイトを見つけるツールやスクリプトを公開したりすることも多くなっています。

実際に図に示したように、多くの脆弱性と攻撃が見つかっています。

エミリーはこう考えたらどうかと言います。

「HTTPSはライフジャケットのようなもの。いろいろと問題はあるけれど、HTTPはライフジャケット無しのようなもの」

HTTPSを使わないのではなく、ツールなどを用いて、サイトで用いる技術を常に最新の状態に保つのが良いでしょう。例えば、SSL Labsスキャニングツールを提供していて、サイトのURLを入力することで、サイトのレイティングや必要となる設定などを示してくれます。

https://qiita.comのスキャン結果

類似のツールとして、MozillaはSSL Configuration Generatorを提供しています。利用するサーバーやバージョンなどを入力することで、設定が生成されます。

Mozilla SSL Configuration Generator

さて、この話の結論は事実(ただし、心配しすぎることは無い)です。「心配しすぎることはない」のは、ライフジャケットなしのHTTPよりも、多少の問題があってもライフジャケットとなるHTTPSは使ったほうが良いし、問題もツールなどを用いて常に最新ソフトウェアを使い、構成を正しくすることで解決できるからです。

「HTTPSにはお金がたくさんかかる」

次の主人公はチャーリー。

チャーリーは2年前にスタートアップ向きの素晴らしいアイデアを思いつきました。彼はサイトを作り、そこで人々がチャットを行えるようにし、無事ベンチャーキャピタルからも資金を調達できました。それから2年経ち、資金調達で得た資金も枯渇しつつありこともあって、彼はコストには厳しくなってきています。HTTPSにはお金がかかるという話が気になっています。

14

お金はHTTPSのいろいろなところに関係しています。

パフォーマンスもお金に関係しますし、最後に紹介する広告もお金の話です。ここでは、特にHTTPSの財務上の課題について話します。HTTPSを支える暗号技術における認証は証明書が必要です。これは従来は認証局から有償で購入する必要がありました。身分証明や法人登記書類などを提示して、認証局より証明書を購入し、それをブラウザに対しての自らの認証として使います。

さて、この証明書はいくらかかるのでしょう? 確かに以前はお金がかかりましたが、今ではいくつかの安価な解決策があります。例えば、プロジェクトSSLMate(https://sslmate.com )というものが数年前から始まっていますが、ここでは1つのドメイン年額$15.95です。複数証明書や追加オプションなどを使うとしても、財務にさほど大きな負担を与えません。新しいプロジェクトのLet’s Encrypt(https://letsencrypt.org/ )は無料で証明書を発行します。

このSSLMateとLet’s Encryptは無料もしくは安価での証明書の取得を実現するだけでなく、自動化のためのコマンドラインツールも提供しているので、期限切れを防ぐことなどの管理作業を行うことができます。

このように、証明書はあまりお金がかからないものとなりました。

もう1つのお金にまつわる心配は、検索ランクへの影響です。HTTPとHTTPSという2つのバージョンのサイトがあったならば、検索エンジンが混乱してしまい、検索ランクが下がってしまうのではないでしょうか?

Googleはサイトの移動に関していくつかのガイドラインを示しています。1つはサイトを移動した場合、301リダイレクトを返すようにと勧めています。また、検索エンジンのクローラーがアクセスしてきた時のために、canonicalリンク要素を提供する必要があります。

15

他のガイドラインとしては、次のようなものがあります。

https://support.google.com/webmasters/answer/6033049 https://support.google.com/webmasters/answer/6073543 https://plus.google.com/+JohnMueller/posts/PY1xCWbeDVC

これらのガイドラインに従うことで、検索ランクへの影響は最小限に抑えることができ、その影響もしばらくしたら回復します。逆に、GoogleはHTTPSのサイトにランキングブーストを行っています。現在は小さいブーストだが、将来的には変更もあり得ることをエミリーは示唆しています。

証明書のコストも無料か低価格で抑えられ、検索結果への影響も無視できるレベルなので、この話の結論はとなります。

「私のサイトはHTTPSに移行できるけど、使っている(依存している)サードパーティ(のサービス)はどうしたらいいの?」

最後の話の登場人物はフランシスコ。彼によるサードパーティコンテンツにまつわる話です。 皆さんのサイトも多くのサードパーティに依存していることでしょう。

フランシスコは大きなニュースサイトを運営しています。このニュースサイトには多くのレガシーなコンテンツも掲載されています。フランシスコはサイトをHTTPS化すると、すべてのサードパーティコンテンツも同様にHTTPS対応していなければならないと聞いていたので、サイトのHTTPS化はかなり大変ではないかと心配しています。

これは事実で、サードパーティコンテンツもHTTPSに対応していなければなりません。

まず一番最初に心配したのが、サイトの収入源でもある広告です。広告がHTTPSに対応していれば、サイトの移行もすぐに行なえます。GoogleのAdSenseは現在では常にHTTPS上で提供されるようになっています。そのため、フランシスコが自分のサイトをHTTPSに移行する前であっても、実はAdSenseはHTTPSですでに提供されています。つまり、AdSenseについては心配の必要はありません。ただし、HTTPS通信がブロックされているいくつかの国は例外です。

これはGoogleだけの動きではありません。業界全体のトレンドです。2015年、IAB(Interactive Advertising Bureau)はブログ記事の中で約80%の広告ネットワークがすでにHTTPSに対応していると明かしています。

Adopting Encryption: The Need for HTTPS

このように最近では広告がHTTPSに対応していないから、サイトのHTTPS化ができないというのはほとんどなくなっています。もし、広告ネットワークがHTTPSに対応していないようだったら、是非確認してみてください。近い将来に対応予定であることがほとんどでしょうし、もしそうでなかったならば何故対応しないか、是非対応してほしいと言ってみましょう。

次に心配なサードパーティコンテンツはHTTPリファラーヘッダーに依存しているものです。アクセス解析のためなどのいくつかの理由によりパートナーサイトであるサードパーティはHTTPリファラーヘッダーを見て、トラフィックがフランシスコのサイトから来たものであることを確認します。問題は、HTTPSでホストされているサイト上のHTTPでホストされているサイトへのリンクをユーザーがクリックした場合、ブラウザはプライバシーの理由からリファラーヘッダーを取り去ってしまうのです。フランシスコにとって、これは困ります。

ここで解決のために必要となるのが、Referrer Policyです。a要素などに付けられるこのReffer Policyとして、”origin-when-cross-origin”を指定することで、リファラーヘッダーがHTTPサイトに対して送られることになります。他にも複数のポリシーがあるので、適切なものを選び、必要な状態でリファラーを送ることができます。

&lt;a href="http://external-partner.com/..." referrerpolicy="origin-when-cross-origin"&gt;Click here!&lt;/a&gt;

最後のフランシスコの心配は少し一般的なものです。Mixed Contentsと言われる、HTTPSサイトでホストされているページの中にHTTPサイトでホストされるセキュアではないコンテンツが含まれるケースです。このようなコンテンツがロードされることで、HTTPSにより高められるセキュリティが妥協したものになってしまうため、厳しい措置がとられています。スクリプトやiFrameのようなものはブラウザによりブロックされます。

また、フランシスコのサイトでは多くの古い記事にロードされている写真などがあったのですが、これらはHTTPでホストされています。このようなイメージに対してはブラウザはブロックはしませんので、イメージも見ることはできますが、ブラウザのアドレスバーのHTTPSアクセスを示す緑色は消えてしまいます。

CSP(Content Security Policy)を用いてこの問題は対処することができます。HTTPレスポンスヘッダーに次のように指定します。

Content-Security-Policy-Report-Only: default-src https:
  'unsafe-inline' 'unsafe-eval'; report-uri
  https://example.com/reportEndpoint

default-src https:という指定から、ブラウザはすべてのコンテンツはHTTPSでロードしようとします。ただし、'unsafe-inline' 'unsafe-eval'という指定から、動的に生成されたものやインラインスクリプトは例外とします。ブラウザがコンテンツをロード中に、もしこのポリシーに従わなかったものを見つけたら、https://example.com/reportEndpointにレポートするように指示されています。

このポリシーの例は、”Report-Only”なので、ユーザーから見た時の挙動は変わりません。セキュアでないコンテンツのロードがブロックされることもありません。ただ、もしそのようなコンテンツがロードされたことがあったならば、フランシスコはレポートを通じて知ることができます。

このレポーティングのためのインフラを用意するのも面倒であった場合、report-uri.ioというサービスを使うことも可能です。エンドポイントを用意してくれるだけでなく、解析やビジュアリゼーションまでも行ってくれます。

report-uri.io

ここで説明したことは、Chrome DevToolsのセキュリティパネルで見ることができるようになっています。

DevTools Security Panel

この話の結論は真実です。ですが、ここ数年で状況は改善されつつあり、業界全体として対応に取り組んでいるものです。

まとめ

確かに、10年や15年前はHTTPSは遅く、導入にはコストがかかり、セットアップは手間のかかるものでしたが、現在では多くの障壁は取り払われています。

このセッションでエミリーが解説したように、HTTPS関連のツールやサービスなどが充実し、導入は昔の比ではないほど敷居が下がっています。インターネットのセキュリティの強化は誰かひとりや一社の取り組みで一日にして成るものではありません。HTTPSの導入を進めることで、安全に快適なインターネット空間が広がるよう、皆で努力していきましょう。

補足

各お話に出てくる人物の名前にはそれぞれ由来があります。

  • アリスとボブは暗号技術を勉強したことがある人ならばご存知だと思いますが、暗号通信を二者で行うときの例として出てくるのが常にボブとアリス。それが由来。
  • イブはEvil(邪悪な)から。
  • チャーリーはどこから来たのか不明。
  • フランシスコはサンフランシスコからか。
]]>
https://html5experts.jp/takoratta/20061/feed/ 0