Technote

by sizuhiko

TRY import maps

GW中、最後の記事は import maps です。 先日esm LTでブラウザの最新動向について話しましたでもふれたのですが、import mapsはChrome73から利用可能になっています。

今回は実際に Polymer-Japan/litelement-first-element のコードを使ってimport mapsを試してみたいと思います。

Polymer-Japan/litelement-first-element

これはLitElementを使ってはじめてWebComponentsを作るためのCodeLabs用のサンプルコードです。 通常は、git cloneするかダウンロードした後で npm install を実行し、 npm start すると polymer-cli でWebサーバーを起動します。 polymer serve コマンドでは、import mapsなしでも、自動的にリクエストのパス変換が処理されます。

そこで、PHPやPythonなど別の言語のビルトインサーバー機能を使い、リクエストのパス変換が動かない状況で試してみます。 今回はPHPを使って、以下のコマンドを実行することにしました。

$ php -S localhost:8080 -t .

まずは Origin Trial に申し込む

さてimport mapsは、まだお試し機能なので、有効にするにはChrome Origin Trialに申し込む必要があります。

Origin Trialとは?という人は、Web 標準化のフィードバックサイクルを円滑にする Origin Trials についてを参照ください。とてもわかりやすく解説されています。

Chrome Origin Trialにアクセスしたら、お手持ちのGoogleアカウントでログインし、Trial for KV storage built-in module + import mapsに登録してトークンを取得します。

このとき、Web Origin はローカルホストで試すため http://localhost:8080 のようにポート番号まで指定して登録します。 すると、トークンが発行されます。

実行してみる

まずは何も指定せずPHPのビルトインサーバーを起動します。

$ php -S localhost:8080 -t .

次にサンプルコードの完成版(http://localhost:8080/icon-toggle-finished/demo/)にアクセスします。 すると、Developer Console にURLが見つからないというエラーが出てきます。

地味な作業になりますが、これを1つづつimport mapsのJSON定義に追加していきます(今は適切なツールがないため)。

こうなる

以下のようになるまでJSONに追加していくと、すべてのパスが解決されて動作するようになりました。

<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">
    <meta
      http-equiv="origin-trial"
      data-feature="KV storage built-in module + import maps"
      data-expires="2019-6-16"
      content="発行されたトークンをコピペする">

    <title>icon-toggle demo</title>

    <script type="importmap">
      {
        "imports": {
          "@polymer/iron-demo-helpers/demo-pages-shared-styles": [
            "/node_modules/@polymer/iron-demo-helpers/demo-pages-shared-styles.js"
          ],
          "@polymer/iron-demo-helpers/demo-snippet": [
            "/node_modules/@polymer/iron-demo-helpers/demo-snippet.js"
          ],
          "@polymer/polymer/polymer-legacy.js": [
            "/node_modules/@polymer/polymer/polymer-legacy.js"
          ],
          "@polymer/iron-flex-layout/iron-flex-layout.js": [
            "/node_modules/@polymer/iron-flex-layout/iron-flex-layout.js"
          ],
          "@polymer/font-roboto/roboto.js": [
            "/node_modules/@polymer/font-roboto/roboto.js"
          ],
          "@polymer/polymer/lib/utils/html-tag.js": [
            "/node_modules/@polymer/polymer/lib/utils/html-tag.js"
          ],
          "@webcomponents/shadycss/entrypoints/apply-shim.js": [
            "/node_modules/@webcomponents/shadycss/entrypoints/apply-shim.js"
          ],
          "@webcomponents/shadycss/entrypoints/custom-style-interface.js": [
            "/node_modules/@webcomponents/shadycss/entrypoints/custom-style-interface.js"
          ],
          "@polymer/marked-element/marked-element.js": [
            "/node_modules/@polymer/marked-element/marked-element.js"
          ],
          "@polymer/prism-element/prism-highlighter.js": [
            "/node_modules/@polymer/prism-element/prism-highlighter.js"
          ],
          "@polymer/prism-element/prism-theme-default.js": [
            "/node_modules/@polymer/prism-element/prism-theme-default.js"
          ],
          "@polymer/polymer/lib/legacy/polymer-fn.js": [
            "/node_modules/@polymer/polymer/lib/legacy/polymer-fn.js"
          ],
          "@polymer/polymer/lib/legacy/polymer.dom.js": [
            "/node_modules/@polymer/polymer/lib/legacy/polymer.dom.js"
          ],
          "marked/lib/marked.js": [
            "/node_modules/marked/lib/marked.js"
          ],
          "prismjs/prism.js": [
            "/node_modules/prismjs/prism.js"
          ],
          "@polymer/polymer/lib/elements/dom-module.js": [
            "/node_modules/@polymer/polymer/lib/elements/dom-module.js"
          ],
          "lit-element": [
            "/node_modules/lit-element/lit-element.js"
          ],
          "lit-html": [
            "/node_modules/lit-html/lit-html.js"
          ],
          "lit-html/lib/shady-render": [
            "/node_modules/lit-html/lib/shady-render.js"
          ],
          "lit-html/lit-html": [
            "/node_modules/lit-html/lit-html.js"
          ],
          "@polymer/iron-icons/iron-icons.js": [
            "/node_modules/@polymer/iron-icons/iron-icons.js"
          ],
          "@polymer/iron-icon/iron-icon.js": [
            "/node_modules/@polymer/iron-icon/iron-icon.js"
          ],
          "@polymer/iron-iconset-svg/iron-iconset-svg.js": [
            "/node_modules/@polymer/iron-iconset-svg/iron-iconset-svg.js"
          ],
          "@polymer/iron-meta/iron-meta.js": [
            "/node_modules/@polymer/iron-meta/iron-meta.js"
          ]
        }
      }
    </script>

    <script src="../../node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>

すごいimport mapsちゃんと動いてる!

ということで、まだChrome(73以上)でないと試せませんが、これで WebComponents を動かすのに Webpack しなくて良い未来を体験することができます。 JSON定義を自動生成するスクリプトが出ないか期待したいですね(がんばって自作するのもあり)。

BracketsにLSPがやってきた

やぁやぁやぁ、LSPがやってきたよ!

Brackets 1.14 has landed!というブログ記事にも書かれているように、今回のリリースの目玉はLSP(Language Server Protocol)のサポートです。

ついにBracketsにもLSP対応が入りましたね。これまでAtomとはVS Codeに遅れをとっていましたが、これで色々なプログラミング言語でも利用しやすくなるはずです。 詳しくはリリースノートにも書かれています。

対応したissueを見ると、 Move vscode-languageserver-protocol to Thirdparty と書いて有るので、vscodeの実装を参考にしたようです。

Language Server Protocol Support in Bracketsのページにアーキテクチャの解説や、LSP拡張を実装する場合の方法について解説があるので現時点 PHP, Python, TypeScript だけのサポートのようなので、それ以外の言語にも対応させようと思ったときに役立ちそうです。

私の手元のBracketsもさっそくアップデートしました。

さっそく使ってみよう

アップデートをインストールしたら、環境設定を開きます。 すると、2ペイン表示になるので、左側(defaultPreferences.json)から php で検索します。 以下のようなデフォルト設定になっているはずなので、コピーして右のペイン(brackets.json)に貼り付けます。

    // PHP ツールのデフォルト設定
    "php": {
        // デフォルト: true
        "enablePhpTooling": true,

        // デフォルト: php
        "executablePath": "php",

        // デフォルト: 4095M
        "memoryLimit": "4095M",

        // デフォルト: false
        "validateOnType": "false"
    },

右ペインにコピーしたらコメント行は削除してくださいね。忘れるとJSONのパースエラーになります。

ここで重要なのは executablePath の設定です。 もしシステム(OS)に入っているPHPが7以上であれば問題ないのですが、デフォルトが7以上でない場合は、ここを書き換える必要があります。

たとえば私はphpbrewで複数バージョン切り替えているので、たまにPHP5.5とかに変更しますので、以下のように固定パスを指定するように変更します。

    "php": {
        "enablePhpTooling": true,
        "executablePath": "/Users/sizuhiko/.phpbrew/php/php-7.3.1/bin/php",
        "memoryLimit": "4095M",
        "validateOnType": "false"
    }

これからは、Bracketsを使ったPHPアプリケーション開発も快適になりますね!

Polymer.co-edo day 2019 春を開催しました

2019年4回目となる Polymer.co-edo ミートアップ を開催しました。

edo-blogcardに最初のコードをアップしました。 developブランチにpushしています。

まだ何も動きはないのですが、LitLoader を使ってビルドするところまで実装しています。

ES Module import との戦い

Web Components はHTMLファイルからだと以下のように読み込みます。

<script type="module" src="node_modules/xx-element/xx-element.js"></script>

独自のカスタムエレメントから別のエレメントを利用する場合は、以下のように記述します。

import '@polymer/iron-ajax/iron-ajax.js';

ここでブラウザは @polymer/iron-ajax/iron-ajax.js のようなパスを解決できないため、webpackでコンパイルすることが必要になります。 この話は、以前の記事PHPer Kaigi 2019に参加しましたでも書いているとおり、import maps待ちな状況です。

2つのビルド結果

そこで、2つのビルド結果を保持するようにしました。

/dist/edo-blogcard.js

これは

import '@edo-elements/edo-blogcard/dist/edo-blogcard.js';

のように利用するためのビルド結果で、LitLoader形式で記述された .lit ファイルを .js にコンパイルしたものです。

/dist/edo-blogcard.bundle.js

これは

<script type="module" src="node_modules/@edo-elements/edo-blogcard/dist/edo-blogcard.bundle.js"></script>

のように利用するためのビルド結果で、HTMLからブログパーツとして利用したい(webpackなどを実行しないWebページで使いたい)場合のためにコンパイルしたものです。 LitLoaderとwebpackを使ってHTMLから利用可能な状態になっています。

工夫したところ

後者の bundle.js については、普通にLitLoaderとwebpackを使うやり方なので、特にはありません。 LitLoaderはwebpackのloaderとして実装されているので、configに設定を追加するだけです。

一方前者はwebpackのloaderようにできているLitLoaderを、webpack使わずにどうやって呼び出すか?というところを工夫しました。

それが build.js です。 そして loader-runner という webpack のモジュールを使っています。 このモジュールは Runs (webpack) loaders という説明のとおり、loaderを実行することに特化しています。

const fs = require("fs");
const path = require("path");
const { runLoaders } = require("loader-runner");

runLoaders(
  {
    resource: path.resolve(__dirname, "edo-blogcard.lit"),
    loaders: [require.resolve("lit-loader")]
  },
  function(err, result) {
    if (err) throw err;
    fs.writeFileSync("dist/edo-blogcard.js", result.result[0]);
  }
);

これだけで .lit ファイルを .js ファイルに(webpackなしで)コンパイルできます。

次回

通常どおりの、もくもく会として開催予定しています。 皆様の参加をお待ちしております。

Doorkeeperのコミュニティページ

PSR-17 HTTP Factories のライブラリ

令和最初の記事も、PHP。 この記事はPSR-15リクエストハンドラーのライブラリ比較の続編です。

前回のサンプルプログラムでは zendframework/zend-diactorosHTTP Message Factories を使って PSR-7のHTTPレスポンス(Psr\Http\Message\ResponseInterface)を作っていました。

今回は、他のライブラリを使っても大丈夫か考えてみたいと思います。

PHP-17 の HTTP Factories に対応したライブラリ

PackagistでPSR-17を検索してみました。 PSR-15ほどはないようです。ということで独断と偏見(ダウンロード数が多いもの)を選んで比較してみたいと思います。 なお「フレームワーク」と書いてあるものや、特定のPSR-7ライブラリ だけ に依存したもの(たとえばhttp-factory-guzzleなど)は除外しています。

それぞれのライブラリの説明を読んで気づくでしょう。

PSR-17 HTTP-Factory with auto-discovery support

そう先ほど「特定のPSR-7ライブラリ だけ に依存したもの」と書きましたが、実は特定のライブラリに依存します。 それはそうですよね。PSR-7のHTTPレスポンスインターフェースの実装インスタンスを 生成 しないといけないので、何かしらに依存します。

ただ、こういった auto-discovery してくれるライブラリを使っておくと、PSR-7のライブラリを変更したときも影響が少ないですね。 でもそれ、さっきのサンプルで DI してたから、それで良くない?というのは、まさにそのとおりです。

で、結局どうすると良いの?

  • PSR-7/PSR-11/PSR-15/PSR-17 を使うアーキテクチャにするなら、FactoryInterfaceをDIすれば影響範囲は設定箇所だけになります
  • PSR-7/PSR-15/PSR-17 を使う(PSR-11を使わない)アーキテクチャにするなら、上記のような PSR-7 を自動判定してくれるライブラリを使うと良い

ということになると思います。

それならPSR-7のライブラリも使い分けたサンプルが見たかった!とかあるかもしれませんが、それはまた今度!(機会があれば)

おまけ

さて、PSR-7/PSR-15/PSR-17 の話は面白かったですか? これに加えて、フロントエンドも含めてフレームワークに依存しないアプリケーションとは? みたいな話をPHPカンファレンス福岡2019でします。 午後一のセッション。また @soudai1025 の裏になりましたが、私の方がマニアックな話なのかな。

では6月に福岡でお会いしましょう!

PSR-15リクエストハンドラーのライブラリ比較

平成最後の記事は、PHP。 久々のPHP記事ですが、PSR-15に準拠したリクエストハンドラーライブラリの実装を比較してみようと思います。

PSR-15とは?

HTTPメッセージを使用するリクエストハンドラーと、ミドルウェアコンポーネントの共通インターフェースです。

それPSR-7なのでは?という鋭い人は、 @tanakahisateruさんのスライド PHP-FIGのHTTP処理標準の設計はなぜPSR-7/15/17になったのかを参照ください。

PSR-7については、だいぶ知れ渡っていると思うので、PSR-15のリクエストハンドラーに着目していきます。

PHP-15のリクエストハンドラーに対応したライブラリ

PackagistでPSR-15を検索してみました。 たくさんありますね。ということで独断と偏見(ダウンロード数が多いもの)を選んで比較してみたいと思います。 なお「フレームワーク」と書いてあるものは除外しています。 例えば zend-expressiveminimalist PSR-7 middleware framework for PHP と書いてあるとおりなので。 Slimなどのマイクロフレームワークも同様です。

の4つを使ってサンプルプログラム集を作ってみました。

sizuhiko/psr15-requesthandler-examples

PSR-7

まず前提事項として、PSR-7対応のライブラリが必要です。 本サンプルでは、主に zendframework/zend-diactoros を使っています。 sunrise/http-routerの例ついては、同系統にsunrise/http-messagesunrise-php/http-server-requestがあるので、それを利用しています。 PSR-7のライブラリが違うと、どのような記述の違いがあるのかわかりやすいでしょう。

サンプルの内容

サンプルプログラム集clone するか、ダウンロードして、READMEの手順に従ってください。

ドキュメントルート(/)にアクセスすると、Hello, World! 出すだけの簡単なものです。

サンプルコードの流れ

で、結局PSR-15とは?みたいな話になるわけですが、どの例も以下のような流れで動いています

  • PSR-7 でHTTPリクエスト(Psr\Http\Message\RequestInterface)を受け取る
  • PSR-15の Psr\Http\Server\MiddlewareInterfacePsr\Http\Server\RequestHandlerInterface でいい感じに PSR-7のHTTPレスポンス(Psr\Http\Message\ResponseInterface) を生成する。
  • PSR-7 のHTTPレスポンスをエミットする

つまり、真ん中の部分を処理することになります。

リクエストハンドラ

サンプルのsrcフォルダの下には

  • Middlewares
  • RequestHandlers

があり、それぞれ Psr\Http\Server\MiddlewareInterfacePsr\Http\Server\RequestHandlerInterface を実装したクラスが置いてあります。 で、それぞれのリクエストハンドラーからは、適したハンドラが呼び出されるようになっています。

このサンプルでは PSR-17 (HTTP Factories) を先行導入し、PSR-11 (Container Interface) に対応したライブラリでDIしてますが、 そのあたりは、次回以降で解説します。

ミドルウエアもリクエストハンドラも同じようにHello, World!をレスポンスボディに入れるだけです。

このサンプルをとおして言いたかったこと

このサンプルでは、ドキュメントルートだけの簡単な実装でしたが、ルーターの設定を追加するだけで、 実際のアプリケーションでも使えるようになることがわかると思います。

もちろん実際にはDBにアクセスする必要もあったりするわけですが、そこにはPHP Data Objects(PDO)があり、 実際のDBの差分を吸収してくれます。 そこで利用するSQLは標準であり、Webの標準よりも長い期間利用されています。

フレームワークを利用してアプリケーションを作ることは、速く簡単に構築できます。 しかしフレームワークに依存せずPHPの標準を使うことで、柔軟にライブラリを変えても、自分のビジネスロジックを流用することができるようになります。

もちろん今日のフレームワークの多くが PSR 対応を進めていますので、フレームワークを使っていても、より互換性が高いアプリケーションを作ることができるようになるかもしれません。

次回はPSR-17について、いくつかのライブラリを紹介します。