※スマホ対応はしてません。

カテゴリー: JavaScript

ブラウザー拡張(アドオン、エクステンション)の作り方チュートリアル。

カテゴリー: JavaScript

思うところあって今後ちゃんと形にしようと思っているんだけど、その前に下書きというか叩き台くらいの感じで一度世に出してみます。

概要

  • すごいかんたん!
  • ビルドシステム類のない牧歌的構成でもいける
  • API互換性はそこそこ。ポリフィル入れてFirefoxに寄せて作るのが吉か
  • Edgeは闇を感じた
  • Safariは未調査

基本的な作り方

  1. フォルダを作る
  2. manifest.json を用意
  3. それに合わせて各種スクリプト等を用意
  4. ブラウザーで開発中のものを読み込み開発
  5. 公開

動く場所

  • ツールバーのボタン
  • 任意のサイト内
  • バックグラウンド
  • 独自ページ

他に開発者ツールにタブを追加したり、Firefoxならサイドバーを作ったりすることもできます。

できること

拡張用のAPIがいっぱいあるのでそれらを駆使する。

もちろん普通のウェブ開発の知見も再利用できるよ。

最新のJavaScriptを書ける

IE対応の必要がない(し、たぶんEdgeも無視されるだろう)ので、Chromeだけ、あるいはChromeとFirefoxだけが対象になります。すると最新のJavaScriptの書き方をがんがん使えます。

class とか async/await とか、関数引数の初期値とか分割代入とか。

そこら辺の勉強を兼ねて挑戦してみるのはきっと良い選択だと思います。

Hello Worldをインストールしてみる

最初のブラウザー拡張を自作するまえに、一度サンプルをインストールしてみましょう。どうせ自作したらやる作業です。

(さらに…)

Chrome、Edge、Firefox互換のブラウザー拡張を作るポリフィル。

カテゴリー: JavaScript

最近思うところあってブラウザー拡張を作って公開しました。(Macが対応していない絵文字を使っているので、変に見えます。いずれどうにかする。) 仕事中についSNS見ちゃうのを止めるやつです。

スイッチオンにしてTwitterとかを開くと止められます。

作ったものについてはそのうち記事に書きたいと思ってます。あとブラウザー拡張の作り方についてもちゃんとした形にしたいなと思って準備中。

それはそれとして、作成中に得た知見のひとつ、互換性についてです。

先にまとめ

  • ポリフィル使えばすぐコード共通化できるしPromise化する
  • 共通化したコードはFirefoxに寄る
  • Edgeはもうひと手間
  • async/await はいいぞ

互換性

前提として、ブラウザー搭載のAPIは以下のような感じ。

  • Chromeは chrome オブジェクト以下にAPIを持つ
  • Firefoxは chrome 、Edgeは browser にChrome互換APIを持つ
  • Chrome互換APIは、コールバックは引数に与える

Firefoxは互換APIに加えて別にAPIがあります。

  • Firefoxは browser にChromeのAPIと同機能だが別I/FのAPIを持つ
  • Firefox独自APIはPromise化されている
  • async/await を利用できる
  • 後述のポリフィルで、ChromeとEdgeでもいける(いえーい)

つまりどのブラウザーでもだいたい同じ機能がそろってるけど、インターフェイスがちょっと違うよと。表にまとめるとこんな感じ。

ブラウザー chrome browser ポリフィル
Chrome ◎ ✘ 〇
Edge ✘ △ 〇
Firefox 〇 ◎

Promiseだと嬉しいって話

「ちょっと違うインターフェイス」の差はPromise化されているかどうかです。なので共通化する場合は、全部Promise化するか、逆に全部Promiseを剥がすかのどちらかになるわけですが、前者Promise化する方をお勧めします。というわけでその理由なんですけれども。

コールバック関数方式

まずこちらの例。Chrome互換APIで、保存した情報を持ってくるやつです。

chrome.storage.local.get(["item1", "item2"], (result) => {
  console.log('# result', result.item1, result.item2);
});

まあこれはこれで問題ないんだけど。

Promise方式

一方FirefoxがChrome互換APIとは別に独自に持っている browser 系APIだと、これがPromiseになる。(「にもなる」の方が正しいかも。)

browser.storage.local.get(["item1", "item2"]).then((result) => {
  console.log('# result', result.item1, result.item2);
});

これだけだと、まああんまり変わらないように見える。

いちおうメリットとして、Promiseを使うと非同期処理を連結してもコードのインデントが深くならないというものががあります。が、そんなものより、ES2017で導入された async/await を利用できる面が大きい。

async/await

前述のPromiseを使ったコードは、( async な関数の中で)以下のように書ける。

const result = await browser.storage.local.get(["item1", "item2"]);
console.log('# result', result.item1, result.item2);

まっすぐ書けるのでたいそう見やすい。

どうすか

Promiseの方が良くないすか?

APIをPromise化するライブラリ

というわけでPromise化したくなりましたか? なりましたね? なったので、これ↓を導入して実現します。

残念ながらライブラリのファイルが単体で公開されていないようです。ビルドシステムを導入していない場合でも、一度npmでインストールしてからファイルをコピーしてきます。

$ npm install webextension-polyfill
$ cp node_modules/webextension-polyfill/dist/browser-polyfill.min.js .

でもって読み込むようにする。

<script src="/browser-polyfill.min.js"></script>
<script src="/popup.js"></script>

これでChromeでも browser を使って、 async/await 記法でさらさら書けるようになります。やったー!

Edge対応

最終的にできあがったのはこれ↓

ライブラリを有効化する

件のライブラリは現状ではEdgeに対応しておらず、検討中みたい。

詳細省略するけど、ライブラリ読み込み前にこれ↓を実行したらEdgeもうまいこといった。

// ※別途に `cloneDeep()` 的なものをご用意ください
"use strict";

// "SCRIPT5045: Assignment to read-only properties is not allowed in strict mode" を避ける
try {
  if (window.browser) {
    browser.storage.local.get = browser.storage.local.get;
  }
} catch (error) {
  // 上書きできるようにする
  window.chrome = cloneDeep(window.browser);

  // Chrome偽装
  window.browser = undefined;
}

openOptionsPage()

他に、自分の見つけた範囲だと chrome.runtime.openOptionsPage() が実装されていないようなので、自前でpolyfillを実装した。他にもそういうのありそう。

window.chrome.runtime.openOptionsPage = () => {
  const { options_page } = browser.runtime.getManifest();
  browser.tabs.create({
    url: `/${options_page}`,
  });
};

manifest.json の互換性

今回特につまづかなかったけど、いちおうブラウザーによって必須だったり名前が違ったりするところがある。

一覧

options_ui

Chromeでは chrome_style: true が推奨。Edgeでは options_page になる。

{
…
  "options_page": "options_ui/index.html",
  "options_ui": {
    "page": "options_ui/index.html",
    "chrome_style": true
  }
}

あとFirefoxでも browser_style: true が推奨だけど、初期値がそうなっているので記述しなくてもいい。

APIの互換性

ライブラリがあるとは言ってももともと実装されていないものはされていないのであきらめる。

MDNの互換性の表を見るのが良さそう。

例えば先ほどの runtime.openOptions() は、やっぱり対応してなかった。

その他

  • Firefoxの chrome オブジェクトについての記述がMDNで見当たらなかった
  • Firefoxの browser でもChrome互換APIと同様にコールバックを引数に与えられるっぽい。でもMDNでの記述は見当たらなかった
  • Edgeは全く関係ない chrome オブジェクトを持っているっぽいけど詳細不明
  • ブラウザー拡張の標準化がW3Cで進行中 → Browser Extensions
  • 標準化されたAPIはPromise化されている(つまりFirefox風)
  • Chromeは標準APIに寄せる気がないらしい[要出典]
  • EdgeはChromeだけを見ているらしい[要出典]
  • Edge拡張を作るのは簡単だけど、公開するの難しそうでつらい

おしまい

async/await いいわー。

更新履歴

2018-04-24

  • Edge対応コードの "use strict" がなんか抜けてたのを修正
  • Edge対応コード完成版のリンクを追加
  • APIの互換性について追加

Nuxt.js他で時計アプリ (PWA) を作ってみたよ。

カテゴリー: JavaScript

ええいや時計って……という感じはあるんだけど、とにかくNuxt.jsを使ってみたかったのです。あと自分では使う。

できあがったもの

アナログ時計が左に、デジタル時計が右に。あとポモドーロタイマー付き。

使った技術

ライブラリ、フレームワーク

ツール

ウェブ標準API

サービス

技術系感想

Nuxt.js

この記事の影響。

なんか久しぶりに「動くものがさくさく作れる」という快感を味わうことができたフレームワーク。Ruby on Railsにこなれてきた頃の快感を思い出しました。

vue-cli使って入れたんだけど、最初からディレクトリとか開発用サーバーとか、各種ビルドとか即時部分更新 (HMR) とか、追加のあれこれなしに書くだけですぐ動く。黒魔術的な感じなのもRailsっぽい。いや別にRailsと比べるものじゃないんだけど。

今回は静的HTML (SSR) でやったけど、そのうち動的なやつもやってみたい。Firebaseでホストすればできると聞いたような気がする? 未調査。別に # なURLでも構わないけども。

vue-cilで入れるやつのコマンド(公式ドキュメントより):

$ npx vue-cli init nuxt-community/starter-template my-project

Nuxt + PWA

こいつを yarn add するだけでわりとすぐ動いた。お手軽便利。これを導入するだけで用が済んでしまったので、ServiceWorkerも自前では書いてないです。

ドキュメントはあんまり親切じゃない感じがする。まだ使い方とかがあんまりわかってない。

Visual Studio Code

ずっとVimを使ってきたんだけど(最近はNeoVim)、ちょっと人に教えるのにこれはあれだよなーと思って「普通のエディター」として使い始めてみました。あらでも良いじゃない。

スクリーンショット。公式サイトから。

後発なうえに開発体制が盤石なだけあって、さすがに出来が良い。各種OSに対応している点、インストールしてすぐ使える点、プラグインが豊富かつ導入や管理も楽な点、日本語対応している点から、今後は人様にお勧めしていこうかなと思っております。

最初に導入してほしいプラグイン:

  • Code Spell Checker
  • EditorConfig
  • ESLint(JS系のひと向け)

ただ、スクロールがなかなかちょっと。これはCodeじゃなくてたぶんElectron for Windowsの問題で、Slack.exeなんかでも起こる。どうも onscroll で何かやってるとうまくスクロールが動かなくなるみたい。詳細未調査だけど、たしか自分が作ったアプリだとそんな感じだった。まあだいたいの人はMacだから問題ないね……。

あとVimプラグインはキー操作が衝突するんで、案の定アレでした。

CSS Grid

何度か使ったことあったかな。気がつけば一通りのブラウザサポートが揃っていたので。

CSS Gridは全てのモダンブラウザで動作する。画像はgrid-templateプロパティの場合。

今回複雑なレイアウトをしてはいないけれども、別にそんじょそこらの細かいところでも割と気軽に使えるので、今後はがしがし使って行きたい。

今回使ったのは右側半分。”T”の字に区切ってます。

GitHub Pages + Cloudflare

今回は全部静的ファイルなので、GitHub Pagesで公開してCloudflareでhttps化。

これも簡単にできて楽だったので、今後は同じようにサブドメインにどんどん生やしていく感じで色々作っていこうかなと思いました。

作業内容はこれ↓。

yarn

普段はnpmの方を使っているんだけど、今回はなんとなくこっちに。インストールが早いとかは聞いてるけれども、うーん特に不満はないけど利点を感じるほどでもなかった。npmも今はロックファイルあるし、普通にインストールしたら記録されるしなあ。

アプリの紹介

時計です。

ポモドーロのタイマー付きです。

おしまい

楽しかった。

inputイベントでフォーム入力値をリアルタイム取得できるよ。(あとjQuery例。)

カテゴリー: JavaScript

input イベントを使います。

時間差なしで入力値が取れる。わあかんたん。

jQueryを使った例。

change イベントは入力を終えてフォーカスを外した際に発火するのに対して、 input イベントは入力の度に発火します。

対象要素

  • <input>
  • <textarea>
  • <select> … IE、Edgeでだめ
  • contenteditable 属性付きの要素 … IEでだめ

<select> は change イベントを使います。この場合は変更したらフォーカス残したままでも、すぐ発火する。(よね? しない環境ある?)

contenteditable は、逆に change イベントが発火しない。

チェックボックスとかはだめ

MDNによると、歴史的経緯により <input type="checkbox"> とかは click イベントを使う必要があるそうです。

あ、Firefoxだけ動くみたい。今後動くの増えるかなあ。

動作はIE 9+

基本機能は一般的なブラウザ各種と、IEは9+で利用可能です。

ただし一部機能が使えなかったりするみたいです。まあ普通に <input type="text"> で使う分には、特に。

昔話

昔はこの input イベントがなかったので、 setTimeout() を使って自前で監視してました。それ用のJavaScriptライブラリも多数あったと思う。

今はもう、そんな苦労する必要はないのだ……。

参考

Vue.jsで文字列が空のとき “Mismatching childNodes vs. VNodes” になった。

カテゴリー: JavaScript

Vue.jsで、こんなエラー(というか警告)が出た。

console.error()で &quot:[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content."

[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>. Bailing hydration and performing full client-side render.

再現コード

はこんな感じ。

<template>
  <section>
    <p><img />{{text}}</p>
  </section>
</template>

<script>
export default {
  data () {
    return {
      text: '',
    }
  },
}
</script>

たぶん、 <p> の直下には「 <img> 」と「 {{text}} から成るテキストノード」の二つがあるはずなんだけど、文字列が空だとそこにテキストノードが用意されないので、なんか予定と違うぞーと怒ってる感じだろうか。

解決策

とにかくテキストノードが存在していれば良いので、空白文字とか置いておこう。

text: '' を text: ' ' にするとか、同様に <template> の方にスペース追加するとか。

余白が嫌なら幅なしのやつ &zwnj; にしよう。

<template>
  <section>
    <p><img />&zwnj;{{text}}</p>
  </section>
</template>

とっぴんぱらりのぷう。