HTML5 ネットワークプログラミング特論 資料 [山崎] 2015 年 11 月 18 日 HTML5 の概要について説明する.第 1 章は,HTML5 と HTML4 の違いを述べる.第 2 章は,HTML5 の仕 様書の目次である.第 3 章以降では,同 draft から幾つかの重要なトピックを抜き出して説明する. なお,有名な以下のような技術は,HTML5 には入っていない.(元々は入っていたのに,標準化の過程で別に されたものもある.) • Microdata (15 章) • Web Messaging (16 章) • Web Sockets (17 章) • Server-Sent Events (18 章) • Web Workers (19 章) • Service Workers (20 章) • Web Storage (21 章) • Indexed Database (22 章) • Web Components (23 章) • WebRTC (未調査): ブラウザ間で直接通信する機能.個人的には疑問. • Device API (未調査): センサーなど端末デバイスを利用する機能.これが使えるのはサービス上は大きい が,技術としては特にない. • File API (未調査): ローカルファイルにアクセスする機能 • Streams (未調査): XHR のようなデータ全体をイベント扱うのでなく,ストリームデータとして扱うため の API.Promise を使って記述する. これらについての多くは,WHATWG で検討されている.WHATWG の ”HTML: The Living Standard” (https://developers.whatwg.org/) には検討中の機能一覧がある.HTML5 のドキュメントは,W3C 管理の ものと,WHATWG 管理のものに分かれてしまっている.整合性がどの程度あるかは,よく分からない.W3C は後追いかというとそうでもなく,W3C の WebPlatform WG や DeviceAPI WG などでは新機能の検討が続い ている.(http://www.w3.org/WebPlatform/WG/PubStatus なども参照.) なお,文中の◆は要注目点で原文にはない.この資料は現時点での W3C の Working Draft をベースにしてい るが,資料の原案は何年も前から更新しているものなので,古い記述が残っているかもしれない.また,あくまで Working Draft なので今後も変更されうる.最終的には自分で確認すること. 1 HTML5 の HTML4 からの相違点 HTML5 differences from HTML4, W3C Working Draft 28 May 2013 (http://www.w3.org/TR/html5-diff/) の要約.ただし,このドキュメントはもう更新されてない. 1 1.1 HTML5 の目的 • HTML5 という一つの仕様にする (HTML4, XHTML4, DOM level2 HTML などのさまざまな言語を一 つの言語で置き換える) • processing models をきちんと決める • ドキュメントのマークアップの改良 • Web applications への対応 (API など) 1.2 Impact on Web Architecture この節だけは昔 (2009 年頃?) の版にあったリスト.2013 年の版にはないが,分かりやすいので掲載する. • DOM をベースとして考える.規定されているものは DOM.HTML5 は一つの表現法.実際,規格では DOM interface として定義されている. • browsing context という概念を導入する • author への要求と user agent への要求を分ける.author は昔の機能を使ってはいけない.UA は昔の機 能をサポートし続けないといけない. • 手続き的に定義を書き下す.(抽象的に書かない) • (block と inline の概念に代わる) 新しい content model • 新しい機能については,accessibility を最初から考える • 意味をきちんと定義する • server-Sent event 機能 • datagrid 要素 • origin の概念 • オフライン Web アプリケーションキャッシュ • browsing context を navigate するアルゴリズム,セッションヒストリ移動アルゴリズム • content-type と character encoding を sniff した (?) • パーサーの定義を明確にした • 2 つの構造化ストレージ • contentEditable 機能,UndoManager 機能 • ドラッグ・ドロップ,コピー・ペースト • クロスドキュメントメッセージ (postMessage) • iframe に新しいサンドボックス機能 (iframe に sandbox 属性をつけて,iframe 内の popup 禁止等の制御 ができる) 1.3 Syntax HTML5 のシンタックスは HTML4 と XHTML1 に互換である.ただし,SGML には従わない部分もある.現 在のブラウザ実装にできるだけ合わせる. <!doctype html> で良くなった.HTML5 は SGML ベースではないので,あの長い記述は不要. MathML や SVG が直接書けるようになった.SVG の例: <svg xmlns="http://www.w3.org/2000/svg" 2 xmlns:xlink="http://www.w3.org/1999/xlink"> <circle cx="100" cy="100" r="100" fill="red" /> <rect x="150" y="150" width="300" height="200" fill="blue" /> <line x1="0" y1="0" x2="300" y2="200" stroke="black"/> <text x="50" y="50">sample text</text> </svg> 1.4 Language 1.4.1 新要素 文の構造を表す要素の導入 section, article, main, aside, header, footer, nav, figure, figcaption その他の要素 • video, audio: マルチメディアコンテンツ (プラグイン無しで) • track: video 要素の文章トラック • embed: プラグインの起動 • mark: マークされたテキスト • progress: 進行状況の表示 • meter: 測定量 • time: 日付と時刻 • data: 任意のデータ [HTML5.1] • dialog: ダイアログの表示 • ruby, rt, rp: ルビを振る • bdi: 双方向テキスト形式化用のテキスト • wbr: 改行可 • canvas: ビットマップ • details: 必要に応じて入手できる情報 • datalist: input と一緒に使って combobox を作る • keygen: 鍵生成 (セキュリティ) • output: script を実行して,その結果を出力する • input: 以下の type 属性 tel, search, url, email, datetime, date, month, week, time, datetime-local, number, range, color 1.4.2 新属性 • a と area 要素に download 属性を追加. • area 要素に hreflang,type,rel 属性を追加. • base 要素に target 属性を追加. • meta 要素に charset 属性を追加. • input,select,textarea,button 要素の autofocus 属性を追加.◆ (以下も) • input, textarea 要素に placeholder 属性を追加.デフォルト値を書ける. • input,output,select,textarea,button,label,object,fieldset 要素に form 属性を追加. • input,select,textarea 要素に required 属性を追加. • fieldset 要素に disabled 属性を追加.fieldset を禁止できる. 3 • input 要素に autocomplete, min, max, multiple, pattern,step 属性を追加. • input, textarea 要素に dirname を追加. • textarea 要素に maxlength と wrap を追加. • form 要素に novalidate 属性を追加. • input,button 要素に formaction, formenctype, formmethod, formnovalidate, formtarget 属性を追加. ◆ • style 要素に scoped 属性を追加.scoped style sheet ができる.◆ • script 要素に async 属性を追加.◆ • html 要素に manifest 属性を追加.◆ • link 要素に sizes 属性を追加. • ol 要素に reversed 属性を追加. • iframe 要素に sandbox,srcdoc 属性を追加.◆すべての要素に利用可能な属性 (global attribute) を追加. • object 要素に typemustmutch 属性を追加 • img/video/audio 要素に crossorigin 属性を追加 (CORS の許可) ◆ • contenteditable: エディット可能であることを示す.◆ • data-*: コンテンツを書く人が自由に使って良い属性. • hidden: 素素を表示しない • role, aria-*: 元々障害者用だっだが,ドキュメントの構造記述に使われつつある • spellcheck: スペルチェック対象の指定 • translate: 翻訳すべき対象の指定 [山崎注: img の crossorigin 属性は,canvas の中でイメージをクロスオリジンでアクセスするために使う. (HTML4 では別オリジンの img の中身は一切見えない.)] 1.4.3 要素の意味変更 • address 要素のスコープの定義 • b 要素は style を平文と変えるだけで,意味は伴わない. • cite 要素は何らかの結果,成果だけを示し,人を示すものではない. • dl 要素は名前と値のペアを表す.dialogue のためのものではない. • hr 要素は段落の区切り. • i 要素は平文とは区別すべき意味をもつことを示す. • label 要素があってもフォーカスをラベルからコントロールに移さない. • noscript 要素の解釈 • s 要素は,あまり正確でない部分を表わす. • script 要素をスクリプトの指示に使えるようになった. • small 要素は小さい表示. • strong 要素は,意味的に重要であることを表す. • u 要素は発音されないテキスト. 1.4.4 変更された属性 • input の accept 属性に,audio/*, video/*, image/*が書ける • accesskey 属性に複数の文字が書ける • form の action 属性の URL がないのは禁止 • table の border 属性は”1”か空文字のみが許される 4 • td と th の colspan 属性は 0 より大きいこと • area の coords 属性は,パーセント radius は禁止 • object の data 属性は,相対的でない. • script の defer 属性は,そのページのパース完了時に実行される • dir 属性は,auto 値が書ける • form の encrypt 属性は,text/plain が書ける • img, iframe, object の width 属性と height 属性は,パーセント表現は禁止.またアスペクト変更も禁止. • link の href 属性の URL がないのは禁止 • base の href 属性は相対 URL でも良い • URL を取るすべての属性は,ドキュメントが UTF-8/16 なら IRI をサポートする. • meta の http-equiv 属性をサーバーは解釈しない.UA が pragma として使うだけ.◆ • id 属性は任意の値でよい. • lang 属性は通常の id に加え空文字列で良い • link の media 属性は,media query を受けつける. • イベントハンドラー属性は,常に javascript として解釈される. • li 要素の value 属性や ol 要素の start 属性を使って良い. • style 属性は常に CSS を利用する • tabindex 属性は負の値でもよい • a, area の target 属性を使って良い • script,style 要素の type 属性が一部不要になった. • img の usemap 属性は URL を取らず,ハッシュ値を取る. • 以下はできるだけ使わないこと.img の border, script の language, a の name, table の summary. 1.4.5 削除された要素 (推奨されない要素) • basefont, big, center, font, strike, tt • frame, frameset, noframes • acronym, applet, isindex, dir 1.4.6 削除された属性 省略 1.5 Content Model Changes HTML5 では,block-level や inline という分類はしない.(CSS との誤解を避けるため.) もっと細かく分類す る.下図 (W3C の HTML5 Editor’s Draft より引用) 参照. • Metadata content: link, script • Flow content: span, div (HTML4 の block と inline を併せた概念) • Sectioning content: aside, section • Heading content: h1 • Phrasing content: span, img (HTML4 の inline に近い) • Embedded content: img, iframe, svg • Interactive content: a, button, label (これらはネストできない) 5 これに伴い,以下の要素について変更がある.address, head の object, body の link と meta, noscript, table, thead, tbody, tfoot, tr, ol, ul, dl, table, caption, th, a, ins, del, object, map, fieldset. 1.6 APIs 1.6.1 新しい API • video, audio の再生制御 API • form の制約条件をチェックする API • オフライン Web アプリ (application cache) API ◆ • Web アプリケーションをプロトコルに登録する API • 編集用 API (contenteditable 属性と共に利用) ◆ • ドキュメント URI 取得 API,navigate や redirect の API • 履歴を参照したり追加したりする API ◆ • base64 変換 • タイマーコールバック API • ユーザの入力を促す API • ドキュメント印刷 API • 検索プロバイダを扱う API • Window オブジェクトが定義された HTML5 ではないが WHATWG が定義する API には以下がある. • microdata • ビットマップグラフィクス (canvas の 2d) • クロスドキュメントのメッセージ (PostMessage(), MessageChannel) • バックグラウンドでのスクリプト実行 (Worker, SharedWorker) • クライアントサイドのストレージ (localStorage, sessionStorage) • 双方向通信 (WebSocket) • サーバからクライアントへのプッシュ (EventSource) 1.6.2 変更された API • document.title: 略 • document.domain が代入可能になった (ドメイン制約を緩められる) • document.open(): 略 • document.close(), document.write(), document.writeln(): 利用推奨しない. • document.getElementsByName() は name がマッチした HTML 要素全体を返す • HTMLFormElement の elements: 略 6 • HTMLSelectElement の add(), remove(): 略 • a と area 要素: 略 • click(), focus(), blur(): すべての HTML 要素で使える 1.6.3 Document の拡張 以下については,HTMLDocument インタフェースから Document インタフェースに移した.すべてのドキュ メントが Document インタフェースを利用しているので,すべてから利用可能になる. • location, lastModified, readyState • dir, head, embeds, plugins, scripts, commands • activeElement, hasFocus • designMode, execCommand(), queryCommandEnabled(), queryCommandIndeterm(), queryCommandState(), queryCommandSupported(), queryCommandValue() • すべてのイベントハンドラーの IDL 属性 なお,window.HTMLDocument は Document インタフェースのオブジェクトを返すようになったので, HTMLDocument を使っていた既存スクプリトは動作する. 1.6.4 HTMLElement の拡張 HTMLElement も拡張された. • translate, hidden, tabIndex, dropzone, contentEditable, spellcheck, style: コンテンツの属性を表す • dataset: data-*属性を扱うための機能 • click(), focus(), blur(): click, focus, blur をシミュレートできる • accessKeyLabel: ショートカット • isContentEditable: 要素がエディット可能か • forceSpellCheck(): スペルチェック • すべてのイベントハンドラーの IDL 属性 幾つかのメンバーは,HTMLElement から Element に移した.(id, className, classList など) 1.6.5 その他の拡張 いろんなインタフェースがいろいろ拡張された.メンバーの追加が多い.HTMLIFrameElement に con- tentWindow メンバーが追加されたなど.また,制約条件チェック API 関係も多い.(省略) 1.7 Changelogs 省略 (興味のある人は読んでみて下さい) 例えば,global での this が,今までは Window オブジェクトだったが,HTML5 では WindowProxy オブジェ クトになった. 7 2 HTML5 HTML5 A vocabulary and associated APIs for HTML and XHTML, W3C Recommendation 28 October 2014 の目次 (Recommendation とは W3C の標準化の最終段階なので,これで確定したいということ.) なお,目次だけは最新だが,本文の記述は更新してない部分が多い.必ず http://www.w3.org/TR/html5/ な らびに WHATWG の Living Standard (https://html.spec.whatwg.org/multipage/) を参照のこと. Table of Contents 1 Introduction 1.1 Background 1.2 Audience 1.3 Scope 1.4 History 1.5 Design notes 1.6 HTML vs XHTML 1.7 Structure of this specification 1.8 Privacy concerns 1.9 A quick introduction to HTML 1.10 Conformance requirements for authors 1.11 Suggested reading 2 Common infrastructure 2.1 Terminology 2.2 Conformance requirements 2.3 Case-sensitivity and string comparison 2.4 Common microsyntaxes 2.5 URLs 2.6 Fetching resources ◆ 2.7 Common DOM interfaces 2.8 Namespaces 3 Semantics, structure, and APIs of HTML documents 3.1 Documents 3.2 Elements 4 The elements of HTML 4.1 The root element 4.2 Document metadata 4.3 Sections 4.4 Grouping content 4.5 Text-level semantics 4.6 Edits 4.7 Embedded content 4.8 Links 4.9 Tabular data 4.10 Forms ◆ 4.11 Scripting 4.12 Common idioms without dedicated elements 4.13 Disabled elements 4.14 Matching HTML elements using selectors 5 Loading Web pages 5.1 Browsing contexts ◆ 5.2 The Window object 5.3 Origin 5.4 Sandboxing 5.5 Session history and navigation ◆ 5.6 Browsing the Web 5.7 Offline Web applications ◆ 6 Web application APIs ◆ 6.1 Scripting 6.2 Base64 utility methods 6.3 Dynamic markup insertion 6.4 Timers 6.5 User prompts 6.6 System state and capabilities 7 User interaction ◆ 7.1 The hidden attribute 7.2 Inert subtrees 8 7.3 Activation 7.4 Focus 7.5 Assigning keyboard shortcuts 7.6 Editing 8 The HTML syntax 8.1 Writing HTML documents 8.2 Parsing HTML documents 8.3 Serializing HTML fragments 8.4 Parsing HTML fragments 8.5 Named character references 9 The XHTML syntax 9.1 Writing XHTML documents 9.2 Parsing XHTML documents 9.3 Serializing XHTML fragments 9.4 Parsing XHTML fragments 10 Rendering 10.1 Introduction 10.2 The CSS user agent style sheet and presentational hints 10.3 Non-replaced elements 10.4 Replaced elements 10.5 Bindings 10.6 Frames and framesets 10.7 Interactive media 10.8 Print media 10.9 Unstyled XML documents 11 Obsolete features 11.1 Obsolete but conforming features 11.2 Non-conforming features 11.3 Requirements for implementations 12 IANA considerations 12.1 text/html 12.2 multipart/x-mixed-replace 12.3 application/xhtml+xml 12.4 application/x-www-form-urlencoded 12.5 text/cache-manifest 12.6 web+ scheme prefix 3 Common infrastructure 3.1 Terminology (2.1 節) HTML 属性 (attribute),XML 属性,IDL 属性はしばしば同じ意味で用いられる. コンテンツ属性とは,HTML 属性と XML 属性のことを指す.IDL 属性は,IDL インタフェースで定義される 属性を指す. 「プロパティ」は,JavaScript オブジェクトのプロパティと CSS のプロパティの両方を指す. 3.2 Fetching resources (2.6 節) これは後述. 3.3 Common DOM interfaces (2.7 節) data-foo-bar という HTML 属性は,element.dataset.fooBar という名前の IDL 属性になる.(dataset という 名前は固定で,fooBar が変わる部分.) Transferable オブジェクトとは,オブジェクトをコピーし,コピー元のそれは使えなくすること.つまり,所 有権を移転できる. 4 Forms (HTML5 4.10 節) 【ソースコード→ form.html】 9 • input, select, textarea, button 要素の autofocus 属性を追加. ページが表示されたらすぐにその autofocus された要素への入力が可能な状態にする • input,output,select,textarea,butto,fieldset 要素に form 属性を追加. その要素がどの form 要素に対応するかを明示するための属性 • input 要素に autocomplete, min, max, pattern,step 属性を追加. autocomplete: on のときはブラウザが過去の入力を使って勝手に初期入力をしてもよい. min, max, step: 最小値,最大値,最小単位の指定 pattern: 入力が従うべき正規表現 valueAsDate: 入力が日付でなければならない valueAsNumber: 上に同じく数値 formaction, formmethod 等を追加.action と method は,form 全体で共通だった.formaction と formmethod を使うと,ある button だけ違う action/method といった指定が可能. コントロールタイプとして,email, url, datetime, data... などが増えた 【→ form.html】 5 canvas 要素 これは第 4 回で説明した通り.【→ 04 js dom/canvas.html】 なお,Canvas に 2 次元の絵を描くための API Canvas 2D API は,HTML5 の一部ではない. 6 Video&Audio • これまでの方法は NPAPI/ActiveX 等 • HTML5 からはブラウザがもっている • ただし,コンテナやコーデックはブラウザごとにばらばら • JS から操作したり JS にイベントをあげたりもできる 例: <video src="myvideo.mov"></video> 7 ドキュメントの構造化 以下のような要素を使って,ドキュメントの構造的な意味を与えることができる. section, article, aside, header, footer, nav, figure, figcaption address, time+pubdate 8 Scripting (HTML5 4.11 節) script 要素で,script の動かし方が増えた. <script src="∼.js"></script> <script src="∼.js" defer></script> <script src="∼.js" async></script> 上から順に,一旦ページのパースを停止して実行,ページをパースし終わってから実行,いつでも実行可能なとき に (パース等と関係なく) 非同期に実行. script 禁止の時だけ意味をもつ noscript 要素が加わった. 10 template 要素が新たに入った (23 章). 9 style 要素 scope 属性を使うと,サブツリーに対してのみ有効なスタイルを定義できる.下の例だと,article (とその子供) に対してのスタイルが定義される. 例: <article> <style scoped> div { margin: 1em auto; position: relative; width: 400px; height: 300px;} ∼ </style> <div> ∼ </div> </article> 10 Drag and Drop (HTML5 7.7 節) ● Drag and Drop は,2014 年 9 月に Proposed Recommendation になる時に,削除された模様.【ソースコー ド→ dnd.html】 11 contenteditable 属性 (HTML5 7.6 節) 【ソースコード→ richtext.html】 contenteditable="true" contenteditable="false" contenteditable contenteditable 属性はすべての要素に指定できる.値を指定しないとその上の要素の contenteditable を inherit する.ページ全体を指定するときは,ドキュメントに対して designMode を (JS から) 指定する. execCommand() という関数を使うと,現在の選択領域や現在のカーソル位置に対して編集コマンドを実行でき る.編集コマンドはかなりたくさんあるが,例えば insertText, delete, redo, undo などなど.編集した結果を取 り出したければ,innerHTML などを使う. Editing API は別に議論されている模様.https://w3c.github.io/editing/ 12 iframe 要素 (HTML5 4.7.2 節,5.4 節も関連) sandbox 属性が加わった. <iframe sandbox="値1 値2 ..."> sandbox を付けると,そのコンテンツは完全に孤立したオリジンと見なされる.iframe の中と外は一切通信で きなくなり,その中では,プラグイン,JavaScript,form の submit ができない. 値i に以下のオプションを使うことで,個別に許可できる. • allow-forms: form の発行の許可 11 • allow-pointer-lock: マウスの Pointer Lock を許可 • allow-popups: ポップアップの許可 • allow-same-origin: 孤立したオリジンとはしないための指定.2 つのケースがある: 同じオリジンで指定した時: script だけをサンドボックス化して,その DOM へはアクセスしたい時に使 う. 別のオリジンに対して指定した時: 元のオリジンへの通信を許可しつつも,ポップアップなどを禁止したい 時に使う. • allow-top-navigation: top-level browsing context への navigate だけは許可する • allow-scripts: スクリプトの実行の許可 13 セッションヒストリ (HTML5 5.5 節) browsing context における document の列を session history と呼ぶ.これは window.history で得ることがで きる. history の今の entry と forward の間に state object を挿入できる.戻ろうとしたときに,state object のスク リプトが起動される. window.history.go(delta) → 任意数のページを戻った先送りしたり.(forward()/back() もある) window.history.pushState(data, title [, url]) → data を state object として session history にプッ シュする.state object の TITLE が title になり,URL が url になる.(この url は,今の script と same origin でなければ駄目.) 後で history を traverse (forward や backward) して,そのページのエントリに戻って来たときに,popstate というイベントが発行される.そのイベントの state 属性に pushState の第一引数の data が入って来る.(この 値は,window.history.state にも入る.) 従って,プログラムの骨格は次のような感じになる. window.addEventListener("popstate", function(event){ var myState = event.state; restore_state(myState); // これは自分で定義した関数 }, false); myState = ∼; newURL = ∼; window.history.pushState(myState, "title", newURL); newURL に状態をエンコーディングして入れておくなら,pushState の第一引数は不要である.(url を再パー スする必要がないので第 1 引数を使った方が少し速いという違いはある.) しかし,今だけの Web アプリケー ションの状態を取っておくときなどは,第 1 引数を使う (べきである). なお,state を入れ換えるメソッド replaceState もある. state object が挿入されたページが discard される時は,state object は破棄される.一方,fragment id の形で 状態をエンコーディングした URL をもっている entry は削除されない.これがたぶん state object と fragment id でやる方法の一番の違い. Location インタフェース: window.location.href → 現在のページの URL window.location.assign(url) → その url へ navigate する. 12 location.reload() → 再ロード 等々 14 Offline Web applications (HTML5 5.7) 【第 10 回の図 Offline Web Applications 参照】 【ソースコード→ ofl/】 ただし,LAN ケーブルを抜いたり,無線 LAN を切ったりしないと実験できない. http キャッシュと何が違うか. • そもそも http のキャッシュは HTML 定義の外の話 • 一回もフェッチしたことないリソースも,manifest に書いてあれば取ってくる. • fallback 機能 • 複数リソースの一部だけ捨てられたりしない.consistent に管理される. 14.1 manifest の書き方 オフラインにしたい Web アプリ側で自分の manifest を指定しなければならない.このためには,その Web ページ内に次のように書く. <html manifest="∼.appcache"> manifest の例: CACHE MANIFEST # 必ずこの行が必要 CACHE: # CACHE セクション開始.ただしこの行は省略可. images/sound-icon.png images/background.png NETWORK: # NETWORK セクション開始 comm.cgi CACHE: # CACHE セクションの続き style/default.css FALLBACK: # FALLBACK セクション開始 images/big1.png images/noimg.png images/big2.png images/noimg.png images/big3.png images/noimg.png 一行目は以降は,セクションヘッダ名とそのセクションの指定(複数行)の繰返しである.セクションヘッダに は,CACHE:,NETWORK:,FALLBACK:,SETTINGS:がある. CACHE はキャッシュしたいリソース指定,NETWORK はキャッシュしたくないリソース指定,FALLBACK はオフライン時の代替リソース指定(上の例だと左の画像はキャッシュせず,右の画像を代わりに使う)である. SETTINGS に次のように書くと,オンライン時にはキャッシュを利用しないように指定できる. SETTINGS: prefer-online 13 14.2 サーバーの準備 ブラウザは,manifest かどうかをヘッダの MIME タイプで判断する.このため/etc/mime.types に, text/cache-manifest appcache という行を追加する.appcache 拡張子のコンテンツを送信するとき,text/cache-manifest という MIME ヘッ ダを付けてくれる.(ただし,一部のブラウザではこれをやらなくても大丈夫かも.) 14.3 動作の概要 オフラインを実現するために,ブラウザの動作の様々な個所が修正されている.HTML5 の仕様書にはこれが 正確に記述されており,本稿でも次節でこれを説明する.ただし,全体像が余りにもわかりにくいため,まず概要 を述べる.次節と照らし合わせながら読むとよい. まず,キャッシュも何もない状態で,hyperlink をクリックしてあるページを見ようとしたとする.すると, top-level bc に navigate される.ページに navigate した時の動作が Navigating across documents (5.6.1) であ る.そのステップ 14 において,appropriate application cache (があればそこ) からリソースを得る.ここでは, application cache を利用するだけであり,manifest のチェックなどはまだしない. 上記で cache がない時は,リソースをフェッチする.リソースのフェッチについては,Fetching resources (2.6) に定義されている.ただし,この一部はオフライン対応するために修正されており,browsing context に application cache が関連付けられているときは,changes to the networking model (5.7.6) に書かれた動作と なる. 今はまだ関連付けられていないので,普通に fetch して 5.6.1 に戻る.そのステップ 23 で text/html の処理 を行う.この場合,Parsing HTML document (8.2) に行く.ここで<html manifest=URL> を検出 (8.2.5.4.2) し,application cache selection algorithm (5.7.5) を実行する.これによって,browsing context が application cache に関連付けられる. 関連付けがなされた以降に fetch した場合は,5.7.6 に記述されたステップを実行することになる.つまり, キャッシュに入っている場合は,そっちからもってくるようになる. 15 Microdata http://www.w3.org/TR/microdata/ (29 Oct. 2013) https://html.spec.whatwg.org/multipage/#toc-microdata Microdata を用いると,より深い意味を与えることができる メモ: 対立 (?) する提案として RDFa がある (http://www.w3.org/TR/rdfa-core/). 例: <div itemscope> <p>My name is <span itemprop="name">Elizabeth</span>.</p> I was born on <time itemprop="birthday" datetime="2009-05-10">May 10th 2009</time>. </div> これは My name is Elizabeth. I was born on May 10th 2009. 14 と表示されるだけ.しかし,同時に name 属性が”Elizabeth”で,birthday 属性が”2009-05-10”のデータを定義し ている. itemscope 属性は microdata を作成する.この時,そのデータの種別 (item type) をユニークに指定したい場 合は,itemtype 属性を指定する.itemtype は (1 つ以上の) URL で指定する.例: <div itemscope itemtype="http://microformats.org/profile/hcard"> 幾つかの URL については既に定義されており,例えば上の例では,有名な vCard というデータフォーマット を microdata で定義している.これが参照している,microformat.org は,元々 microformat というのを定義 しようとしていたグループのサイト.microdata との関係はよく分からない. DOM API でアクセスできる. 例: document.getItems(itemtype 名) を実行すると,その itemtype の microdata のリストを得ることができる.その値を見るには,itemValue() を 使う. microdata の利用方法としては,検索エンジンが利用するのがメインだろう.外からロードした JS ライブラリ が利用するというパターンもあるかもしれない. 16 Web Messaging 【第 10 回の図 Cross-document messaging 参照】 http://www.w3.org/TR/webmessaging/ (19 May 2015) https://html.spec.whatwg.org/multipage/comms.html ウィンドウ間通信,worker 間通信 (Web Workers),サーバー (Server-Sent Event) との通信は,すべて同一の API で書ける. メッセージの受信 window.addEventListener("message", listener_fn, false); でリスナー listener_fn を張り付けておく.(第 3 引数については javascript.tex を参照) same origin policy の制約は受けないので,受け取ってよい origin かのチェックは次のように自分でやる. function listener_fn(event){ if (event.origin == "http://hogehoge") { ... } } メッセージの送信 (特にクロスドキュメント) var iframe = document.getElementById(IFrameId); iframe.contentWindow.postMessage(MESSAGE,TARGET_ORIGIN); MESSAGE には JS のオブジェクトを書ける.TARGET ORIGIN には,送り先 (上の例では iframe) の Window オブジェクトと same origin の URL を書くか,”*”と書く.(TARGET ORIGIN は,確認のため?) 15 channel message 上の方法では,(message という) イベントは一つしかないので,複数の通信相手が混在した複雑なコードは書 きにくい.直接通信するには channel を使う. 通信路一つに対して channel を張って,channel の出口 (ポート) にリスナーを張り付けておくような形でプロ グラミングする.これを実現するためには,ポートを相手に渡すことが必要だが,これには postMessage の第 3 引数を使う.(以降は省略.興味がある人は自分で.) 17 Web Sockets 【→ websocket/server.js, client.html】 http://www.w3.org/TR/websockets/ (20 Sep. 2012) https://html.spec.whatwg.org/multipage/comms.html 上記はブラウザの API の仕様である.プロトコルは IETF. Web Socket の作成 var ws = new WebSocket(URL[, PROTOCOLS]); ただし,URL は,”ws://∼” または ”wss://∼”.wss は secure web socket のこと.また,URL は origin 以 外でも良い. 送信 var result = ws.send(メッセージ); メッセージは文字列の他に,Blob,ArrayBuffer,ArrayBufferView オブジェクトも送れる (ArrayBufferView は,ArrayBuffer 内のバイト列を自由に語長を変えて参照できる). 受信 ws.addEventListener("message", function(ev){console.log(ev.data);}, false); ev.origin で送信者のオリジンが分かるので,ちゃんとチェックする.なお,”message”イベントの他に,”open” や”close”のイベントがある. プロトコル websocket は http の上に乗せているのではなく,新しいプロトコルである.このため IETF で標準化している (RFC 6455). http から websocket による通信に移ることを upgrade と呼ぶ.upgrade するには,クライアントから次のよう なメッセージを送ることでハンドシェークを開始する.(これは当然 HTTP に従っている.) GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket 16 Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 ^n:ds[4U サーバーは次のようなメッセージを返す. HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat 8jKS’y:G*Co,Wxaこれによりハンドシェークが確立し,データ通信が始まる.この通信は双方向で,いつでも自由にデータを送れ る.(ここが http との一番の違い.) 実際は,データをただ送るのではなく,フレーム構造が定義されている.これによって,blob や ArrayBufferView などが送れるようになっている. HTTP ではないので,WebSocket のサーバ側にも変更が必要.Apache の場合は,mod pywebsocket を入れ て,Python で記述する.または,Node.js や Jetty などの Web サーバを使う方法もある. WebSocket のクローズは,Close フレームを互いに交換した後で,TCP を閉じる. 18 Server-Sent Events 【第 10 回の図 Server-Sent event 参照】 【ソースコード→ sse/】 http://www.w3.org/TR/eventsource/ (03 Feb. 2015) https://html.spec.whatwg.org/multipage/comms.html サーバから UA にイベントを送ることができる.普通は,UA がリクエストを出してサーバーがレスポンスす るだけなのに注意.サーバから送る方法は HTML4 まではなかった (従来は Comet 技法などを使っていた). 作成 まずクライアント側で以下を実行する. var evs = new EventSource(URL); ここで URL は,CORS 可能な url,またはスクリプトと same origin な url である.多くの場合は,”xxx.cgi”な どと書くはず.これにより,クライアントとサーバーの間にストリームが張られる. イベント受信 evs.addEventListener("message", function(ev){console.log(ev.data);}, 17 false); この後でサーバーからクライアントに対して,イベントが送られると,クライアントはこれを message イベント として受け取り処理する. 個々のイベントにはイベント ID が付与されるので,イベントの重複や抜けを検知しての処理も可能である. message イベントオブジェクトの各属性は次の通り. • data 属性: イベントのデータ • origin 属性: サーバのオリジン • lastEventId 属性: イベント ID なお,サーバ側で event 名を指定すれば,message 以外のイベントを生成することも可能である. サーバー側のプログラム (CGI 等) を書くには,ブラウザ API だけでなく,プロトコルを理解する必要がある ため,次に簡単に説明する. プロトコル EventSource オブジェクト内では,reconnection time と last event ID が保持される.ここで,last event ID は最初は空文字列.EventSource の引数で指定された URL に対して http GET を送るとき,last event ID が空 でないときは,その値を Last-Event-ID ヘッダに入れて送る. リソース取得が,通常の close (山崎注: サーバーは一定時間通信のない http コネクションをクローズすること がある) や予期せぬ close で完了したら,reconnection time 経過後に接続を再確立する. ストリームは UTF-8 で送る.MIME タイプは,”text/event-stream”である.一行が一つの単位であり,次の ような形をしている. フィールド:値 なお,コロン無しのときは値は””.コロンで開始する行はコメント.フィールドは,以下のどれかで,それ以外 は無視. event: 値 → event 名を値にセット data: 値 → データバッファに値をアぺンド id: 値 → 値を last event ID としてセットする retry: 値 → 値を 10 進数と解釈して,reconnection time にセット 空行 → イベントをディスパッチする. :任意文字 → コメント (コネクションの自動切断防止に使う) 【→ sse/sse.c, sse/sse.html】 sse.c は,sse.cgi としてコンパイルする.sse.cgi が終了すると,http がクローズされるので,一定期間後にまた sse.cgi が再オープンされる.cgi 側で Last-Event-ID を見れば,続きができる. getenv("HTTP_LAST_EVENT_ID") で参照できる. WebSocket と Server-Sent Event との違い • SSE は http.(ws も 80 番ポートだが http でないので通らない可能性あり) • SSE はサーバがイベントを一方的に送ってくる. • SSE はコネクション切断時の再開 (Event ID の再送信を含め) まで UA がやってくれる. • WebSocket は双方向.任意のデータが送れる. 18 • WebSocket は UA でなくプログラマが苦労する. 19 Web Workers 【ソースコード→ worker.html, worker.js】 http://www.w3.org/TR/workers/ (24 Sept. 2015) https://html.spec.whatwg.org/multipage/workers.html Web workes を使うと,バックグラウンドでの処理ができる.親と子の間はメッセージで通信する.ただし,子 からは DOM は見えず,純粋な計算処理だけしかできない. ワーカの作り方 var worker = new Worker("worker.js"); Worker の引数の URL は same origin でないといけない. ワーカとの通信 (親→子) 子 (worker.js): addEventListener("message", 関数, false); 親: worker.postMessage(メッセージ); ワーカとの通信 (子→親) 親: worker.addEventListener("message", 関数, false); 子 (worker.js): postMessage(メッセージ); 子のグローバルスコープは,dedicatedWorkerGlobalScope インタフェースをもつオブジェクトである (window オブジェクトではない).上の postMessage もそのインタフェースに対して定義されている. (same origin の) 複数のコードが一つの worker を共有して利用できる shared worker もある. 20 Service Workers http://www.w3.org/TR/service-workers/ (25 June 2015) AppCache は失敗(と言われているらしい).ブラウザ側で処理することが多くて,それから外れたことができ ない.プッシュ通信もできない.Service Worker はこの反省から,できるだけ低レベルにして,道具だけを提供 するという考え方. Service Worker とは,Web Worker の一つ.ただし,一旦登録すると,ブラウザが動いている限りは,バック グランドで動き続ける.(DOM に触れないのは全 Worker 共通.)次のようにして,service worker をスクリプト URL とスコープ URL に対して登録する.なお,https でないと登録できない.(以下も,実験済みのコードでは ない. ) 例: navigator.serviceWorker.register(’sw.js’, {scope: "/"}).then( function(reg){ console.log("registered"+reg);}, function(err) {console.log("failed"+err); } ); 19 sw.js の方は次のように書く. this.addEventListener("install", function(e){...}); this.addEventListener("activate", function(e){...}); this.addEventListener("fetch", function(e){...}); this.addEventListener("message", function(e){...}); このように登録しておくと,このサイトのうち scope(つまり”/”)で始まるページをフェッチした時に,fetch イベントが発生する.scope が違えば,同じオリジンに複数の worker 登録が可能. Cache API を使うと,コンテンツをキャッシュできる.ただし,キャッシュの中を探したり,キャッシュを更 新したりするのも,全部プログラムでやる. 関連する機能として,以下がある. • HTTP Push,https://tools.ietf.org/html/draft-ietf-webpush-protocol-01 • Push API,http://www.w3.org/TR/push-api/ (27 Apr. 2015) • Notifications API, https://notifications.spec.whatwg.org/ HTTP Push は,HTTP/2 の server push 機能を使って,プッシュ通信を行うプロトコル.Publish-Subscribe 型のスキームを提供する. Push API は HTTP Push の上に定義された API.Service Worker に対して,インタフェースを追加してい る.pushManager.subscribe() メソッドで subscription を登録する.プッシュメッセージが来ると,promise が 実行される. Notifications は利用者に通知するための API の規定.Web ブラウザを最小化していても,利用者に通知をす ることを可能とする.どのような通知するかはブラウザの実装依存,OS にも依存.例えば,タスクバーを使う などが考えられる.そのような通知を登録したり,解除したりするための API がある.Service Worker の中で notification を表示するための showNotification() メソッドなど. 21 Web Storage http://www.w3.org/TR/webstorage/ (09 Jun. 2015) https://html.spec.whatwg.org/multipage/webstorage.html UA にデータを保存する方法は,これまでクッキーしかなかった.HTML5 では,2 つの方法が増えた.session storage と local storage である.クッキーは毎回全データを送るのに対して,session/local storage は送らない. このため,巨大なデータを保存できる. クッキーはサイトに対しての保存データなので,例えば,同じサイトを同時にアクセスして違う飛行機チケット を買うことはできない.チケット購入手続き中に保存したクッキーが互いに見えてしまうので,同じチケットを 2 枚買ったり,エラーになったりする.session storage はセッションごとに分かれたデータなので,このような混 乱は生じない. session storage は,top-level browsing context (以下 tlbc) に対して存在するストレージ.個々の tlbc は, session storage の集合をもつ.個々の session storage は origin 毎に作られる.tlbc が終わると,session storage も捨てられる. cloning や,スクリプトや,利用者がリンクを辿ったことにより,ある tlbc から新たに tlbc が作られると,そ の origin の session storage がコピーされる.ただし,その後の修正に対しては独立した storage となり,枝分か れしたようになる.tlbc が破壊されるとき,session storage も破壊される. local storage は,origin ごとに作られ,自動的に消されることはない. tlbc とは関係ないので,例えば iframe 中で作れば,その origin で作られる.(クッキーと同じように) そのような 3rd-party local storage を UA は禁止 するようにしても良い. 20 session storage には sessionStorage 属性で,local storage には localStorage 属性でアクセスできる. 従って,session storage を使った例は次のようになる. sessionStorage.setItem("key", "value"); // データの保存 var x = sessionStorage.getItem("key"); // データの取り出し sessionStorage.removeItem("key"); // データの削除 storage が書き換わった時 (setItem や removeItem) にはイベントが発行されるので,これを利用することもで きる. 22 Indexed Database http://www.w3.org/TR/IndexedDB/ (08-Jan-2015) これは WHATWG でなく,W3C の WebApps WG のみで議論されている. 【ソースコード→ indexdb.html】 Origin ごとに複数のデータベースをもてる.origin 内でデータベースは,名前で識別される.各データベース はバージョン番号をもつ.(バージョン番号が変えられるのは,versionchange トランザクションだけである.) 一 つのデータベースは,複数の接続 (connection) をもてる. データベースとは,object store の集合である.この集合を変更できるのは,versionchange トランザクション だけである (このトランザクションは,upgradeneeded イベントの結果,実行される).object store とは,key value pair (= record) のリストである.リストは key で昇順にソートされている.object store は名前で識別さ れる. key は,Number, String, Date, Array でなければならない.(Array 同士の大小関係は,0 番目の要素から比較 して行って,最初に異なった値の大小関係で決まる.) key range オブジェクトは,キーの範囲を表す.lower と upper 属性で範囲を指定する. cursor オブジェクトは,key range を使って範囲を指定したとき,その中の一つ一つの record を取り出すに使 う.このオブジェクトの advance メソッドを呼ぶと,次の record へと cursor を進めることができる. key path は文字列であり,”attribute1.attribute2.∼” などのように書く.これは,オブジェクトの attribute1 のその attribute2 の∼という意味である.key path は,key の値を示す path である.(key path が Array の場 合もあるが省略.) index オブジェクトは,普通とは逆に,object store の中の record を value の属性によって探すためのものであ る.index は,参照 (referenced) object store をもつ.参照 object store が更新/削除等されると,すぐに index に反映される. 参照 object store 中のレコードが,キー X と値 A をもっていたとする.インデックスの key path を A に対し て実行した結果が Y だったとする.すると,インデックスのレコードは,キーが Y で値が X である.つまり,中 身の値 Y からそのキー X を求めたことになる. この場合の値 A のことを参照値 (referenced value) と呼ぶ.(X 経由で参照 object store を間接参照し,参照値 を得る.参照値がそのまま index に入るわけではない.) 例: ある index の参照オブジェクトストアが,キー 123,値{first: "Alice", last:"Smith"}というレコー ドをもっていたとする.また,index の key path が”first”だったとする.この index は,key が”Alice”で,値が 123 というレコードをもつことになる. transaction は,database の読み書きをするために必須のものである.transaction は,connection を通して作 成され,対象となる (複数の) object store をもつ.これを scope という. transaction には abort メソッドはあるが,commit はない.transaction が active でなくなった時に,自動的 に commit される.通常は,すべてのリクエストが実行完了し,新たなリクエストがなくなった時に active で無 くなる. 21 transaction には,readonly と readwrite と versionchange の 3 種類がある.前 2 者は transaction メソッドで 作る.versionchange のみが,object store や index の追加削除ができる.upgradeneeded イベントの結果として 自動的に作られるものであり,それ以外の方法では作れない. API は,すべて非同期動作である. 主な API: ●データベースのオープン: Window と WorkerUtils インタフェースは,IDBEnvironment インタフェースを実装する.IDBEnvironment は,indexedDB という名前の属性をもつ (インタフェースは IDBFactory).IDBFactory の open メソッドを呼 ぶことで,データベースを作成する.引数はデータベース名.例を参照.(open の第 2 引数でバージョンを指定 できる.新しいバージョンを指定すると,versionchange イベントが起きる.データベースの構成を変更できるの は,このイベントの中でだけ.) ● Database オブジェクト メソッド close(): クローズする メソッド createObjectStore(OS 名): OS 名の objectstore を作成して戻す.これは versionchange トランザク ション中でのみ実行できる. メソッド transaction(OS 名): トランザクションを非同期で作成する. ● Object Store オブジェクト メソッド createIndex(Index 名, keyPath): Index 名のインデックスを作成する.versionchange トランザク ションの中でのみ実行可能. メソッド delete(キー): record を削除する. メソッド get(キー): OS から値を得る (キーは省略可能). メソッド put(値,キー): record を OS に追加する (キーは省略可). ● index オブジェクト: 省略 ● cursor オブジェクト: 省略 ● transaction オブジェクト: メソッド abort(): トランザクションを中止する 注意: commit というメソッドはない. 例は,indexdb.html を参照. メモ: firefox の indexedDB は,以下にある.これを削除すればよい. C:\users\XXXX\AppData\Roaming\Mozilla\Firefox\Profiles\YYYY.default\indexedDB 23 Web Components http://www.w3.org/TR/components-intro/ (24 July 2014) http://www.w3.org/TR/custom-elements/ (16 Dec. 2014) http://www.w3.org/TR/html-imports/ (11 Mar 2014) http://www.w3.org/TR/shadow-dom/ (06 Oct 2015) http://www.w3.org/wiki/WebComponents/ 今は,こちららしい. Web Components は WebApps WG で議論されている.まだ議論の真最中であり,昨年と比べても大きく仕様 が変わっている.ただこの考え方は,必ず入ると思われるので,簡単に紹介する. HTML で繰返し同じパターンが出てくることがある.HTML の記述を再利用したい.実は,これは既存の JS ライブラリを使えば可能 (例えば dojo toolkit にもそのような機能がある).複雑な HTML のパターンにマクロ のように独自の名前を付けることができ,後はこの名前を使えば記述を大幅に簡略化できる.しかし,実行時にマ クロは本当の DOM に展開されてしまうから,HTML の記述と JS から見えるデータ構造がまったく別物になっ 22 てしまう.これを何とかしたい. Web Components というのは概念で,実際は 3 つの要素から構成される. • Custom Elements: 自分独自の要素を定義する • Shadow DOM: DOM の部分木をカプセル化する • HTML Imports: 上記をパッケージ化する この 3 つの機能は互いにまったく関係ない.それぞれが別個に議論中である.更にこの 3 つに加えて,html5 の template 要素を使うことで,Web Components が使えるようになる. Template 要素 (HTML5 4.11.3 節) 例: <template id="mytemp"> <div> sample </div> </template> template は表示されない.表示せずに DOM だけを作成できる.もちろん,スクリプトで操作しなければ意味 がない.例: var temp = document.getElementById("mytemp"); var elem = temp.content.cloneNode(true); document.body.appendChild(elem); content 属性で template の中身(div)を取り出している.そのまま appendChild しても,何も表示されない のが template の元々の仕様なので,何も表示されない. Custom Elements Custom elements によって,オリジナルの要素を定義することができる.custom element を登録するには,次 のようにする: document.registerElement(’x-foo’, { prototype: Object.create(HTMLParagraphElement.prototype, { firstMember: { get: function() { return "foo"; }, enumerable: true, configurable: true }, // specify more members for your prototype. // ... }), extends: ’p’ }); 23 この例では,x-foo という名前の custom element を登録している.(なお,この名前にはハイフンを含まなけ ればならない.今後新規に追加される html 要素と名前が衝突するのを避けるためである.) prototype は,オブジェクトに対するメソッドの委譲先の指示である(つまり,普通の JavaScript のプロトタ イプの指定) . extends を書かなければ,x-foo という新タグを定義することになる.つまり, <x-foo>Sample</x-foo> のように書けるようになる.上の例のように extends を書いた場合は,既存の要素名 (この場合は”p”) に対する 拡張となる.この場合は, <p is="x-foo">Paragraph of amazement</p> このように,is を使って custom element の名前を指定する.custom element の機能がないブラウザでは無視さ れるので,ただの p 要素となる.(is は,新設の属性.) 上記は,既に html にある要素 (上の例では p) を拡張した例だが,まったく新たにオリジナルの要素を作成し, <original-element> ∼ </original-element> などのようにすることもできる. custom element では,様々なタイミングで callback が呼ばれるが省略する. なお custom elements は,まだ一部のブラウザでしか実行できない. Shadow DOM custom elements を使うと,オリジナルの要素を作成できるが,DOM としては置き換えられてしまう.DOM の内部構造も含めて,オリジナルの定義をするには,Shadow DOM を利用する. DOM は木構造のデータで,その根は document である.DOM はそのまま表示にも使われるので,JavaScript で DOM を操作すると直ちに表示が変わる.Shadow DOM を使うとデータ構造としての DOM の裏に表示用 のデータ構造をもつことができる.表の DOM を document tree と呼び,表示用の DOM を composed tree と 呼ぶ. document tree において,裏の DOM をもつノードを shadow host と呼ぶ.また,その裏の DOM のことを shadow tree と呼ぶ.ある shadow host は,shadow tree を複数もつことができる. (http://www.w3.org/TR/shadow-dom/の Fig.1 を参照.) shadow host を shadow tree で展開することで,document tree から composed tree を作成することができる. (木の挿入方法や発生するイベントの定義などは省略する.) shadow tree を作るには例えば次のようにする. var e=document.getElementById("test"); var r = e.createShadowRoot(); r.appendChild(∼); 要素 e に新たに shadow tree が追加される.その根である r に,appendChild 等で要素を追加することで,shadow tree を作成できる. これだけだと,shadow tree と document tree は別の世界であり,いつも決まった shadow tree が表示される だけである.shadow host の子 (これは表の話) を引数のように shadow tree に渡し,shadow tree の中にその引 数を埋め込めれば,多様な表現が可能となる.(多様な,というか,それができなければオリジナルの要素定義と 24 は言えないが.) これをするのが content 要素である.shadow tree の中のデータ埋め込み場所 (insertion point) を指定する. (http://www.w3.org/TR/shadow-dom/ の Fig.3 を参照.) HTML Imports link 要素に,新たに import というリンクタイプが加わった. <head> <link rel="import" href="/imports/heart.html">| </head> 基本的には same origin.CORS 対応していればクロスオリジンも可能.import してきた DOM は表示されな い.次のようなプログラムで import した DOM にアクセスできる. var link = document.querySelector(’link[rel=import]’); var heart = link.import; 25
© Copyright 2024 ExpyDoc