気づいていなかったことに気づいちゃったので、よりシンプルなセットアップにしようと頑張った成果が出たので、共有。
というか
絶対にwebpackの設定はやりたくない。絶対にだ。
という気持ちがほとんど。そしてそれは(もちろん内容によるけど)
考え方を逆転させれば叶うぞ
という話。
これまでを整理
まずは前回日記で書いた時点までに分かっている GAS 開発に関する制約や考えたこと、行なったことをまとめるところから始める。
当たり前の開発を行いたい
当たり前の考え方として以下が挙げられると思う。
- 1 ファイルにすべての function を並べるような開発は見通しが悪いのでやりたくない
- すべてのコードをゼロから書くのではなく適切に npm などに依存したい
- テストコードを書き、パーツごとにテストを行いたい
- 手元のコードのバージョンを管理したい
Apps Script上のコードの制限
一方で GAS ならではの制限としては以下のようなものがある。
- CommonJS や AMD などの方法ではコードの共有を行えない
- 実行する function は global に定義されている必要がある
- GAS 固有の「ライブラリ」はあるが、これを npm で管理することはできない
以前採用した解決方法
以前 GAS 開発をモダンにしたいと思った際には情報としてすでに多く出回っていた
require を GAS 上で再現する
という方向で頑張ってみた。
- Google Apps Script + Node.jsで簡単なツールを作ってみた - あーありがち(2015-10-31)
- Google Apps Script開発をもうちょっとモダンにしてみる - あーありがち(2017-05-27)
これはこれでよかったが、この辺りのツールチェインは残念ながら流行り廃りが激しい。
bundler選びをやめて考え方を変えてみよう
じゃあ今改めてやるとして、しかし Browserify はさすがにもう厳しいかなと思い、じゃあ Webpack にするのかなぁ、Webpack でも Parcel でも GAS 化するところには実は必ず fossamagna 氏がいて、どれでも実現はできそうではある。
- fossamagna/gasify: Browserify plugin for Google Apps Script
- fossamagna/gas-webpack-plugin: Webpack plugin for Google Apps Script
- fossamagna/gas-entry-generator
ただいずれにせよこの bundler の設定自体をやりたくないんだよなぁと思っていたところ、一つ新しく気が付いたことがある。それは
Apps Scriptはブラウザ上で実行されているJavaScriptのように他のファイルの内容にアクセスできる
というもので、実は Script Editor 上で異なるタブで開いているコードで定義されている内容は import / export なしにアクセスできるのだ。ということは
Node.js 上で import / export なしに他のファイルの内容にアクセスすることができれば問題解決では?
となる。いや、言ってることが矛盾してるように聞こえるんだけど、条件を限定さえしてしまえば実現できることが分かった。それが
mzagorny/gas-local: Execute and test your google app scripts locally in node.js
だ。
実は上に挙げた Google Apps Script開発をもうちょっとモダンにしてみる - あーありがち(2017-05-27) の中でも先の fossamagna 氏の資料には触れていて、その
Apps Scriptによるより高度な開発プロセス/More Advanced Development Process with Apps Script // Speaker Deck
中ですでに gas-local には触れられているのだが、この時は正直 gas-local の意味がよく分かっていなかった。今回調べていて改めて分かったことは、gas-local を利用して
const gas = require('gas-local')
const app = gas.require(<コードの置かれているディレクトリ>, { globalObject })
app.myFunction()
と呼んであげることで、直接 export / import ( require ) していない function を実行することができる。しかもその中で他のコードに書かれている class などに依存していてもまったく問題なく動く。 のだ。これすごくない?
書き方としては
- gas-local で読み込むファイルの置かれているディレクトリを指定してまとめて読み込むことで GAS の実行環境を作る
- できた実行環境上の function を call する
という形になっている。
ということはこのコードさえ書けばあとは Node.js の普通のテスティングフレームワークでテストを実行できるわけだ。
gas-local利用上の注意点ふたつ
注意点の一つは上のコードに書いてある globalObject である。実は gas-local はこれを書いているバージョン 1.3.1 の時点で Node.js の VM という機能を利用していて、VM に共有するオブジェクトを渡してあげないといけないのだ。分かる人はこれだけ書けば十分話が伝わると思う。
要するに例えば
gas.require('./src', { console })
として console オブジェクトを渡してあげないと console を利用した log の出力を gas.require を呼んだ側と共有できないので、どんなに console.log() を書こうが、標準出力には何も表示されない。残念ながらここは GAS の知識ではなく Node.js の知識が必要になってくる。
もう一つは、外からアクセス可能なのは function のみということである。これは GAS の制限と同じと考えれば分かりやすい。例えば
class Klass {
}
というクラスが定義されていても上のサンプルコードで言う app.myFunction() と同じ要領で app.Klass と書いても Klass にはアクセスできない。
この解決方法は簡単で、function が必要なら function を作ればよい。
class Klass {
constructor(..) {
..
}
}
createKlass(..) {
return new Klass(..)
}
この createKlass には外から自由にアクセスすることができる。これで Klass クラスのテストを自在に行うことができる。
gas-localを利用したGASのlocal開発
ここまでをおさらい。
- gas-local を利用することで export / import なしに他のファイルの内容にアクセスできる GAS と同じ動作を実現できる
ということが分かった。残る課題は
- GAS 固有のオブジェクトは Node.js 上では動作しないので stub/mock する必要がある
- 外部の依存パッケージは依然として import ( require ) が必要なのでは?
- TypeScript と仲良くしたい
になる。
stub/mockはglobalについてはご自由に
1 については gas-local 自体が mock の機能を持っており、これで解決するのが早そうだ。この mock は default で Logger などが定義されており、また独自の mock の追加も普通にオブジェクトを組み立てていくだけなので簡単にできる。
一方でこの gas-local の用意してくれる mock は単に GAS のコードをなんとか動かせるようにするためには使えるが、結局ただの global object なので、TDD 好きとしては完全にスペック不足と言わざるを得ない。
そこで毎度おなじみの sinon で以下のように自分で定義してもよい。
const gas = require('gas-local')
const sinon = require('sinon')
const Logger = { log: () => {} }
sinon.stub(Logger, 'log').callsFake((message) => console.log(message))
const app = gas.require('./src', {
Logger,
console
})
app.myFunction()
ここでは事前に stub 済みのオブジェクトを渡しているが、share されているので app 初期化後に sinon 側で手を加えてもよい。stub だけだと恩恵が薄いが、sinon なのでちゃんと mock もできる。
外部の依存パッケージはrollupでiifeとして持ち込む
もう一度改めて。
このアプローチは考え方を逆にする。
ここまでで gas-local を利用することで local でも import / export しないで外部のコードを利用することはできた。次にやるのは
build 時に bundle するのではなく、必要な依存パッケージは事前に準備する
になる。
依存パッケージは通常 CommonJS や ES Modules として提供されているが、これをこのまま持ち込んでも結局「import / export しない」を実現できない。しかし
rollupでiifeとしてコピーすれば自分で書いた素のコードと同じ扱いにできる
のだ。
例えば Ruby の Object#dig のような lodash.get というパッケージがあるが、これを利用する場合、以下のように rollup.config.js を書いて rollup -c すればよい。
import commonjs from '@rollup/plugin-commonjs'
export default {
input: './node_modules/lodash.get/index.js',
output: {
file: 'src/dig.js',
format: 'iife',
name: 'dig'
},
plugins: [
commonjs()
]
}
これで .src/dig.js に dig という名前で必要な function が置かれる。できあがった dig.js をそのまま git add してやる。
複数のパッケージを持ち込みたければ
export default [
{},
{}
]
のようにして複数の設定を書けばよい。
※ ただし、そのままでは独自の名前空間を持ちかつそれに対して plugin システムを持つようなものをうまく動作させるのは難しそうだ。何しろ名前自体は global でも実体としては IIFE として閉じているので。その場合は plugin まで解決し終わった class や function を export するような、rollup を利用した独自のスクリプトを書いてあげる形で解決する必要がありそうだ。
IIFEとは
IIFE - MDN Web Docs Glossary: Definitions of Web-related terms | MDN
Immediately Invoked Function Expression の略で、以下のようなコードのことを言う。jQuery 時代によく見た、あまりよい印象を持てないコードだ。
(function () {
statements
})();
ただ rollup で生成されたコードは以下のようになるので、これで GAS の機能を利用して global にアクセスできる function を持ち込むことができる。
var dig = (function () {
'use strict';
..
function get(object, path, defaultValue) {
var result = object == null ? undefined : baseGet(object, path);
return result === undefined ? defaultValue : result;
}
var lodash_get = get;
return lodash_get;
}());
TypeScriptと仲良くするには
残念ながら gas-local は TypeScript には対応していない。つまり
const gas = require('gas-local')
const app = gas.require('./src')
としても src の中身が TypeScript だったら require してくれないのだ。そこで test を実行する前に一度 tsc でコンパイルしてあげる必要がある。設定は clasp の利用している ts2gas のデフォルトの設定を参考にすればよい。ドキュメントには
{
isolatedModules: true,
module: "None",
noLib: true,
noResolve: true,
}
のように書かれているがこれは嘘で、コードの中を見ると
{
experimentalDecorators: true,
noImplicitUseStrict: true,
target: ScriptTarget.ES3,
..
emitDeclarationOnly: false,
module: "None"
}
しか書かれていなくて、下二つは必ず適用されるということになっている。もろもろ考え合わせると以下のような感じにしておくと快適に開発できそう。
{
"compilerOptions": {
"target": "es2015",
"lib": ["es2015"],
"allowJs": true,
"module": "None",
"outDir": "./dist/",
"types": [
"google-apps-script"
],
"typeRoots": [
"./node_modules/@types"
]
},
"include": [
"./src/**/*"
]
}
src/ に TypeScript を置いて dist/ にコンパイル済みの JavaScript が生成される設定だ。で、先ほどの例で言うと gas-local では
gas.require('./dist')
としてコンパイル済みの JavaScript を読み込むようにすればテストを行うことができる。
もしかしたら必要ないかもしれないが、このコンパイル済みのコードを Google 上でも利用したい場合は .clasp.json に保存されている rootDir も dist に設定しておく。
また、@types/google-apps-script を利用できるようにすると Node.js 向けの Type 定義と合わない、そもそも runtime は Node.js ではないなどの問題が出るので、lib と types で絞っておくのがよいようだ。
まとめ
Webpack などの asset bundler を利用しなくても GAS の local 開発をモダンに行うことはできる。
- GAS の仕様に合わせて import / export を使わずにコードを書いて、テスト時には gas-local と組み合わせるとよい
- gas-local の生成する VM では function しか呼び出せないので、class を定義した場合はインスタンス生成用の function も定義すること
- Google 上ではなく local での動作になるので Google の runtime 上にしか存在しないオブジェクトは stub/mock してあげる必要アリ
- 依存パッケージは rollup などを利用して iife として持ち込むとよい
- gas-local は TypeScript に対応していないので TypeScript を利用したい場合はテスト実行前に自前で tsc で変換する。ディレクトリ丸ごと扱わないといけないので ts-node ではダメ。
以上をいい具合に package.json に定義してやると、結構快適に開発できる。
ここで「rollup で iife にしてリポジトリに持ち込むとかダサくね?」とか「もっと普通に Node.js の流儀で書けるべき」とか「Webpack の設定くらい書けて当然でしょ」みたいな考え方もあるかもしれない。そう考える人はそうすればよいのだが、自分はこのやり方の方がよいと考えている。判断のポイントは
どちらの API の方が寿命が長いか?
である。
Webpack などの bundler は恐らくまだまだ変化する。一方で GAS の API は今回の大型アップデートである V8 runtime の追加が行われても基本的には何も変わらなかった。gas-local の開発も3年前で止まっているが、なんら問題なく利用できる。
rollup は利用しているがこれは後発の bundler の割にかなり安定した開発になっており、今回の目的に利用している API も恐らく変わることはほぼないだろう。変えるメリットがないはずだ。
以上を踏まえて、あえて Webpack などの利用を避けていくのがよい選択なのではないかと考えている。
※ もちろん HTML Service を利用した Web アプリ開発に GAS を利用するのであれば Webpack の利用はよい選択肢になると思うが、最近自分はそもそも GAS を Web アプリとして使うこと自体に否定的な考えを持っている。Workspace add-ons が登場し、そこで利用できる Card Service が登場した。Card Service はデスクトップ向け Web UI にもモバイルアプリにも適用することができる。(残念ながらモバイル向けの Web UI には適用できない。)Google の考える未来の体験としては この Card Service を通じて HTTP で何らかのサービスと通信しながら機能を果たすものになるのだろう。Web アプリは GAE で作って Cloud IAP で制限を掛ける方がよほど素直に作ることができるので、今後は無理に GAS で Web アプリを作るのはやめた方がよさそう。
サンプルリポジトリ
最近は自宅サーバの Wiki じゃなくてあえて Evernote を使うことが増えていました。自宅サーバの Wiki に繋ぐ必要がなく、iPod touch でも自由に編集可能、ローカルにキャッシュしておけばネットワークが切れていても大丈夫、など Wiki にないメリットがやはり大きかったのです。
しかしあるとき気づきました。
Evernote は勝手にデータに手を加えている
ということに。
Evernoteは正確なテキストデータを保持しない
Evernote の考え方がどうなのかはよく分からないのですが、Evernote で構造化テキストが使えたら便利だなとは以前から思っていましたし、そういう意見は自分以外にも目にしていました。
自分も Markdown を使ったり reST を使ったり1していたのですが、ある日、箇条書きをネストさせたときに気がついてしまいました。
ホワイトスペースを維持できていない
ことに。
Evernote を使って長文を書くときは以前紹介した mobile版 + 外部エディタを使う方法 で書いているので、エディタ上で正確にホワイトスペースの調整をしながら書いています。勘違いのはずがありません。2
これは困ります。普通の文章だけを書いているならいいんですが、何らかの記法を使っている場合はホワイトスペース一つが命取りになります。少しの間だましだまし使っていましたが、待っていても恐らく何も解決しないことから、別な選択肢を試してみることにしました。
Evernoteそのものの置き換えは志向しないことに
最初は Evernote そのものをやめる方向に持っていこうと思ったのですが、以下の理由で断念しました。
- 他のアプリやサービスとの連携の面で Evernote が抜きん出ている
- 例えば Twitter アプリとの連携において Evernote のように使うことができるサービスを探すのは難しいです
- タグ付け、検索性など、純粋に書くだけでなく保存し、検索し、再利用するシーンにおいても他のサービスより便利
Evernote を完全に置き換えるのは容量の問題を除いても難しいです。Google Docs と連携するタイプのノート、メモサービスもありますが、Evernote 並みの完成度を期待するのは難しいですし、より「書くこと」に特化したノート、メモサービスではテキスト以外のデータを保存できなかったりして、根本的に置き換えるのは不可能だったりします。
Simplenoteを併用する
ということで「書き終わりがあるものを書く」場合は Simplenote を使うことにしました。「書き終わりがあるもの」というのは
- 日記などにアウトプットすることが目的
- ファイルに落とし、rst2ody.py などで紙前提のデータとすることが目的
で、最終的には Evernote や Simplenote から削除されるものを言います。
なぜ Simplenote になったかというと、
- スマートフォンアプリ, PC/Mac アプリ, Web UI が揃っている
- 最初に目についた
- 十分速かった
という理由です。
SimpleText という似たサービスも試したのですが、サービスの全体像が分かりにくく、Mac 上では結局「テキストファイルを編集する」というスタイルになりそうなのでやめました。データストアの実体がファイルであってもよいのですが、編集段階でファイルであるということは意識したくなかったからです。それだったら Dropbox と大差ないわけで。(少なくとも PC 上においては。)
現在の構成
- Evernote ( iPod touch / Mac / Win / Web)
- Simplenote (iPod touch / Web )
- Notational Velocity ( Mac )
- JustNotes ( Mac )
です。Windows 用のアプリは調べていませんが、Simplenote のサイト上で紹介されています。
基本的には Evernote は Evernote 社の提供するもので閉じていますが、Simplenote の場合は PC 上のアプリがないみたいで、これは第三者の提供するものを利用するようです。
それが JustNotes と Netational Velocity ですが、JustNotes は enex ( Evernote の export データ ) の読み込み用で、実際に読み書きに使っているのは Notational Velocity です。
サービスの比較
以下は自分で比較したときのメモです。何かのお役に立てば幸いです。
Evernote
すべてを記憶する | Evernote Corporation
- リッチテキストを扱える
- 様々な形式のデータを扱える
- 画像や PDF を OCR に掛けてテキストで検索できる
- タグ付けの他にノートも複数作れる
ただし、リッチテキストやマルチメディアデータ周りで iPhone などモバイルデバイスに比較的強い制約がある。
また、プレーンテキストデータを勝手に加工してしまうためにコピー&ペーストした際に改行が落ちる、各種 Wiki, Markdown, reST など空白文字に意味のある記法を使うとそれを保持できないなどの問題あり。
改行の問題は比較的有名だが、改行以外にもホワイトスペースで問題が出るとはしばらく使っていても気づかなかった。
Simplenote
Simplenote. An easy way to keep notes, lists, ideas, and more.
- テキストしか書けない
- 単純なので iPhone アプリも PC 向けアプリもとにかく速い
- PC向けアプリは公式版はない?
- free版でもバージョン管理できる(max 10 / premium 30)
- free版ではメールからノートを作成できない
- Web UI を「広げる」user script 有り
PC向け公式アプリはないが API は private beta 状態。
freeアカウントでも編集履歴を保持してくれるのは場合によって便利そう。仕事やじっくり PC に向かってしか書かないようなものなら自分でバージョン管理するのであまり必要ないかもしれないが。
また、iPhone アプリで Evernote のように明確にローカルにキャッシュするアクションがないように見える。もしかするとすべてのデータを自動的にキャッシュするのかもしれない。プレインテキストしか扱えないのならそれで十分現実的か。
Mac用アプリの比較
Notational Velocity
- 標準的な UI の Mac 用 Simplenote クライアント
- 上下2分割しかないのが個人的には不満
- ノートの内容とタイトルは分離(JustNotes は本文の1行目がタイトル)
- 明示的に Synchro する UI がない(ただちに保存されるらしいが feedback がない)
- 検索と新しいノートの作成が同じアクションでできる
- 操作の速さに対するこだわりが感じられる。説明を読んでもたぶん分かりにくいので実際に試してみてほしい。Google Chrome のアドレスバーのようなもの、と言えば少し分かりやすいか。
- ノートの保存方法を選べる
- plain text, HTML, RTF, DB
- ノートの暗号化が可能
書いている途中で指定のフォントとは明らかに異なる見た目の文字が出てしまうのはやはり日本語フォントが指定不能だからだろうか。
現状ではタグがうまく同期できていないようだ。今後に期待。ただし書き終わったら捨てるのを前提にするのなら不要かもしれない。
JustNotes
JustNotes - notes application for your Mac
- Mac 用 Simplenote クライアント
- タイトルと本文の区別がない(1行目がタイトル)のですぐに書き始められるしすぐにタイトルを変更できる
- 保存は sqlite
- 全体に動作は機敏
- かなり頻繁に synchro する
- UI は Mac の標準的なものよりは Widget, Gadget 的
- Markdown 記法対応(アプリ内でレンダリングできる)
- Evernote の enex を import 可能
enex の import 機能はとても魅力的。メタデータを捨てずに Evernote からデータを移行するには enex が import できなければいけない。
ただしフォントを Monaco にしても正しく反映されていない気がする。
今後
基本的には
- 保存して見返すものは Evernote
- 他のサービスとの連携は Evernote
- 構造化テキストを使ってがっつり書いてアウトプットするものは Simplenote
- とにかく今すぐ書きたい場合は Simplenote
という形でいこうかと思っています。Evernote はやはり当初怖いなと思っていた通りデータが無為に溜まってしまいがちでちょっとまずい状況です。できるだけ早めの吐き出し、移行を目指して作業していこうと思います。
Simplenote での吐き出しはとにかく速いのが気に入っています。上に挙げた自分の目的とは違いますが、書いて捨てるという意味では付箋ソフトなどの代わりにも使えるくらい十分に速いです。しかも自動的に様々なデバイスで同期が取れるので、一昔前の「特定のデスクトップに束縛されてしまう付箋」よりずっと便利だと思います。実際、ちょっとしたメモをその場で書くにも使っちゃってます。Notational Velocity はその辺りのとにかく「(速く|早く)書く」ことにこだわって作られているのがいいですね。
Linux の /bin/sh て基本的に bash ですな。で、これで sh スクリプトを書いてあると、あるときこれを *BSD とかでも使おうと思ったときにハマります。*BSD の /bin/sh は bash じゃないから1。
というようなことは 横着プログラミング 第11回: 小粒なツールたち でも触れられていて、ここで高林さんは
私はちょっと複雑なシェルスクリプトを書く場合には /bin/zsh を使うようにしている。
これは zsh の拡張機能を使いたいためということもあるが、「bashism」なシェルスクリプトを書くのを避けるためという意味も大きい。bashism なシェルスクリプトとは、#! /bin/sh で始まっているにも関わらず、 bash の拡張機能を使っているもののことである。
という豪快な解決法に辿り付いているんだけど、自分は逆に「Linux でもストイックに /bin/sh の機能に絞って書けばいい」という方向に傾けてみる。とは言えやることは簡単で、Debian で
apt-get install ash
するだけ。これは実際には dash に link を張るだけのシロモノで、dash ってのは The Debian Almquist Shell のこと。
Almquist shell - Wikipedia, the free encyclopedia
によれば
- *BSD の default shell は ash で
- これの Debian implementation が dash
なので、Debian を使っているならこれを使って
/bin/ash -xv SCRIPT
/bin/dash -xv SCRIPT
としてデバッグしていけば安心して使える sh スクリプトが書けるって塩梅。
ちなみに最近試していた CentOS の方ではそんな shell は見つからないので、これをやるには Debian を使うのがよさげ。あるいは OSX だと Fink には ash がある。MacPorts は知らない。そういえば cygwin に ash ありますね。
もう一つの方法として zsh の emulation が使えるかなと思ったんだけど、sh って名前で立ち上げても sh そのものの挙動をエミュレートするわけじゃないみたい。とりあえず sh ではエラーになる書き方でもそのまま動いてしまった。
ash 依存はいいのか! Bourne shell で動かなかったらどうする! とか言われても困ります。そんな shell を実際に触ったことがありませんごめんなさい。
OSX は bash なので問題ない。 ↩
へー。
eclipse は一時試したけど全然使ってないんだよな。でも同じプロトコルでやりとりできれば嬉しいことは…そのうちあるかもしんない。現状でその可能性はゼロだけど。
G4 落としましたorz
すげーショック。ThinkPad より滑りやすかったのを忘れてうっかり。しかも砂利の上。左側面を下にほぼ垂直に落下し、一部へこむ。
心もへこんでおります。
上のリンクはバラして内側から力を加えてへこみを戻そうと思ったときに参考にしたもの。頑張ったんだけど、1本だけ、バッテリ部分のネジが尋常じゃないくらいにカタく(てゆーかここシールされててすごい大変ですよ)て外れず、思いを遂げることはできませんでした。。。
とりあえずケースを買うことを決意しました。下調べは実はすでについている。
from ITmedia
仕事用の IM が社内の話なら、社内に IRC サーバを立ててそっちに移行しちゃえばいいと思うけど、外の人の場合はかなりややこしいな。
てゆーか IM に対する明確なビジョンを、どこそこのレポートとかっつって思いっきり省略した記事じゃなくて、セキュリティ上の問題があるなんて簡単な話じゃなくて、ちゃんとした文章で公開する人って出てこないのかな。まだそういうことを書いてもウケないという状況なのだろうか。
例えば主要な IM サーバの情報が出回るだけでフィルタリングが容易になり、安全なネットワークの実現に近づくと思うのだが、そういう情報ってなぜか出てこないよね。
from ITmedia
- IE 6 SP2 は確かに SP1 より安全である
- 「急いで入力しろ」というメールは信用するな
- JavaScript をはじめ、基本的なセキュリティの確認はユーザーがちゃんと知っているべき
ってことだと思う。分かりやすく、参考になる。しかし、Opera や Firefox や Safari でどうなるのかも見てみたかったなぁ。
ああいう状態が活発な状態なんだろうか?(^^; だとしたらちょっと見直す。あの荒れは起電力として必要なものだったのか。MoonWolf 氏はもうああいうキャラとして認知されている、ruby-list に大人が揃っている。比率はともかく両方の条件が満たされているってことか。
面白いものを見させてもらった。
iBook 熱も iPod 熱もけっこう冷めて順調な感じなんだけど、ふとしたきっかけで無線 LAN 熱が再び高まってきた。802.11g であれば、少なくとも iBook や ipod よりは長い間安定してお世話になるので無駄になるまい。
高いというイメージがあったが、今はどうなんだろう。CF で 802.11g 対応なんてのがいいなぁ。