銀の弾丸

プログラミングに関して、いろいろ書き残していければと思っております。

WebアプリでGoogleドライブのファイルを読み込む方法

f:id:takamints:20180722212809j:plain FlickrGoogle Self-Driving Car ( Creatorsmoothgroover22, License: CC BY-SA 2.0 )

ここしばらく「できないわけがないじゃないか」と調べていた個人的な大問題、「ブラウザで動作するWebアプリから、ユーザーのGoogleドライブに保存されているファイルを読み込む方法」を解決できたので小躍りしながら書いております。

Googleドライブのファイルを読み込むサンプルコードは、下記APIの説明ページで示されています。 しかしWebページのJavaScript用のがありません。 Node.jsのはありますが、これはWebのJavaScriptでは動きません。fs(ファイルシステム)モジュールを使用しており、パイプでストリームへコンテンツを流しているからです。

先に結論としてまとめてしまうと、Drive REST API Files.get を XHRで直接叩いて読み込みます。 クエリパラメータで alt=mediaを指定して、リクエストヘッダにサインインユーザーのアクセストークンを指定する必要がありますが。 どうして Files.get API の通常の(関数としての)呼び出しで、読み込めないのかわからないのですが、エラーになるので仕方がない。

一方、保存するのはこちらから!
takamints.hatenablog.jp

Node.js のfsモジュール的なメソッドでGoogle Driveを扱うモジュールを、なんとかnpmとしてまとめましたので是非どうぞー

takamints.hatenablog.jp


目次


基礎から学ぶ Vue.js
基礎から学ぶ Vue.js
posted with amazlet at 18.07.22
シーアンドアール研究所 (2018-05-29)
売り上げランキング: 1,625

やりたいことは、それじゃない

ダウンロードではありません

ここで示しているのは、WEBアプリが利用するデータファイルをGoogle Driveから読み込む方法であって、ブラウザの機能を利用してユーザーのローカルファイルシステムへダウンロードするものではありません

Drive REST API Files.get で得られる webContentLink をhrefにセットしたアンカータグ(<a href="...">...</a>)を生成すれば、ユーザーがクリックしてファイルがダウンロードできますが、JavaScriptのコードからは利用できません(CORSのエラーが発生します)。

Google Docのファイルは読み込まない。

Google Docのファイル(Googleスプレッド、ドキュメント、スライドなど)は、以下のコードでは読み込めません。

これらは別途 Files.export API で取り出せるらしいのですが、あくまでもエクスポートで、独自のWebアプリで開く方法ではありません。 各ファイルの専用アプリで表示したり編集するには、ファイルリソースの webViewLink をたどれば可能。

WEBページのJavaScriptGoogle Driveのファイルを読み込むコード

ということで、前置きが長くなりましたが、以下にコードを示します(ざっくり説明は下のほうに)。 このコードは、現在作成中のモジュール gdrive-fs からの抜粋です。

使用しているGoogle Drive APIはV3。 WebアプリケーションがOAuth2の認証を得ている前提で動作します。 ChromeFirefox、Edgeで検証済みです。

/**
 * Get a file content as text from Google Drive.
 * Even if the file is not a text actually, it could be converted
 * to ArrayBuffer, Blob or JSON to use by Web App.
 * @async
 * @param {string} fileId The file id to download.
 * @param {boolean|null} acknowledgeAbuse A user acknowledgment
 * status for the potential to abuse. This parameter is optional.
 * default value is false.
 * @returns {Promise<string>} A downloaded content as text.
 */
GdfsUi.prototype.downloadFile = async function(
    fileId, acknowledgeAbuse)
{
    return await new Promise( (resolve, reject) => {

        // Create download URL
        let apiUrl = "https://www.googleapis.com/drive/v3/files";

        // Query parameter
        let queryParams = { "alt":"media" }; //【1】
        if(acknowledgeAbuse) {
            queryParams.acknowledgeAbuse = acknowledgeAbuse; //【2】
        }
        let urlParam = Object.keys(queryParams).map(
                key => `${key}=${encodeURI(queryParams[key])}` ).join("&");

        let downloadUrl = `${apiUrl}/${fileId}?${urlParam}`; //【3】

        // Get access-token on current session【4】
        let googleUser = gapi.auth2.getAuthInstance().currentUser.get();
        let authResponse = googleUser.getAuthResponse(true);
        let accessToken = authResponse.access_token;

        // Send a request with XHR
        let xhr = new XMLHttpRequest();
        xhr.open("GET", downloadUrl, true);
        xhr.setRequestHeader("Authorization", "Bearer " + accessToken); //【5】
        xhr.onload = e => { resolve(xhr.responseText); };
        xhr.onerror = e => { reject(new Error(xhr.statusText)); };
        xhr.ontimeout = () => { reject(new Error("request timeout")); };
        xhr.timeout = 30000;
        xhr.send(null);

    });
};

ざっくり説明

※ 以下のリストの項番がコード中の【 】内の数字と合致します。

  1. ファイルの中身を取り出したいときは、クエリパラメータ alt"media"としなくてはなりません。でないとリソース情報が返ってきます。
  2. acknowledgeAbuseはよくわかっていませんが、妙なことをするかもしれないけどユーザーがそれを認識しているかどうかってことでしょうか。少なくとも自分が試した範囲では、指定しなくてもよかったのですけど・・・。Abuseフラグみたいなのがファイルについているみたいで、そのフラグが立っていると、このリクエストパラメータをtrueにしなくてはならないようです。逆に、フラグがfalseの場合に、trueを指定すると怒られます。残念。
  3. ダウンロードURLには、fileId と 上記 alt パラメータが必須です。
  4. ユーザーがサインインしたときに発行されたアクセストークンを取得しています。gapiAPIを読み込み以降利用できるグローバルオブジェクトです。gapi.auth2.getAuthInstance().currentUser.get() で、ユーザーがサインインしたときの情報が得られ、その中にアクセストークンがあります。
  5. このアクセストークンを、リクエストヘッダに設定します。本当ならこの前にアクセストークンの有効期限をチェックして無効になっているならリフレッシュトークンで再発行する必要があるかもしれません。

モジュール全体はコチラ

上記コードを含むモジュール全体は、GitHubのgdrive-fsです。 まだ作成途中ですが、UIのスケルトンやサインインシーケンスなどのコードがあります。 UIをカスタマイズできてWebアプリからGoogle Drive のファイルを扱うモジュールを目指しています。おそらくそのうち npm でPublishすると思います。

github.com

今のところhttp://localhost:8080/ホワイトリストに入れたAPIキーをリポジトリで晒しておりますので、そのままでもご確認いただけます。 セキュリティリスク大丈夫か?って警告を受けていますが、利用者のDriveのファイルが見えるだけなので問題はありません。 利用者側でAPIKeyを取っていただいても大丈夫(というかむしろ推奨)。

所感・あとがき

こんなの、今の時代すぐにでもできると思うじゃないですか。 しかし、情報を探っても辿っても途中でぶつ切れになる感じで、同じところを逡巡して「やりたいのはそれじゃない」って情報がわんさか出てきて自分のググラビリティっていったい・・・。

しかしまあ、自分の Google Drive API に関する知識はチュートリアルをやっと乗り越えられたレベルで、実装経験はほとんどゼロです。 Google Drive APIに関して、根本的な設計思想やドキュメントの行間を読む力が足りていなかったからでしょう。 そういう意味では今回いろんなドキュメントを手繰ってどうにか解決したので少しはレベルアップできたかもしれません。

日々精進。いくつになってもお勉強です。

非同期Lambdaのコンテキストはどこ行った?

f:id:takamints:20180630111519j:plain
Flickrdetective ( Creatorolarte.ollie, License: CC BY-SA 2.0 )


結局どこへも行ってなかったって結論ですがっ!

AWSによるサーバーレスアーキテクチャ
Peter Sbarski
翔泳社
売り上げランキング: 4,634


この4月からAWS Lambda で Node.js 8.10 が使えるようになっており、AWSのコンソールから新規作成するとひな型がasync関数になっています。 しかし引数リストが event ひとつだけになっていて、従来第2引数で渡されていた context はどこに行ったの?と調べてみました。

目次

はじめての Lambda with Node.js 8.10 は非同期関数

現在、Node.js 8.10 のLambda を新規作成すると以下のひな型が作られます。async関数になってますね。

そして、引数が event だけ。えらいスッキリしましたね。context は何処へ?

exports.handler = async (event) => {
    // TODO implement
    return 'Hello from Lambda!'
};

ところで Lambda のコンテキストって何でしょう

AWS Lambdaに渡される context は以下のキーを持つオブジェクト。 Lambda関数自体の情報を保持しています。 結構重要な情報があるはずなので、これが渡されないとはいぶかしい。

  • callbackWaitsForEmptyEventLoop
  • functionName
  • functionVersion
  • invokedFunctionArn
  • memoryLimitInMB
  • awsRequestId
  • logGroupName
  • logStreamName
  • identity
  • clientContext

参考: Context オブジェクト (Node.js) - AWS Lambda

this === context?」って仮説は否定されました

ジャバスクリプターが「コンテキスト」と聞いて最初に連想するのは this ですよね(本当か?)

そこで「Node.js 8.10 のLambda関数内では、this === context なのではないか?」と仮説を立てて、以下のLambdaを書いてみた。

exports.handler = async (event) => {
    return JSON.stringify({
        callbackWaitsForEmptyEventLoop:
            this.callbackWaitsForEmptyEventLoop,
        functionName: this.functionName,
        functionVersion: this.functionVersion,
        invokedFunctionArn: this.invokedFunctionArn,
        memoryLimitInMB: this.memoryLimitInMB,
        awsRequestId: this.awsRequestId,
        logGroupName: this.logGroupName,
        logStreamName: this.logStreamName,
        identity: this.identity,
        clientContext: this.clientContext,
    });
};

で、コンソールから動かしてみたのですが、全てがnullで、仮説は完全否定されました。 まあ、そんなヤヤコシイことしないか。

じゃあコンテキストはどこ行った?

てことでAWSコンソールでしばらく探ってみたのですが、「もしやどこにも行っていないのでは?」と思い、

exports.handler = async (event, context) => {
    return JSON.stringify({
        callbackWaitsForEmptyEventLoop:
            context.callbackWaitsForEmptyEventLoop,
        functionName: context.functionName,
        functionVersion: context.functionVersion,
        invokedFunctionArn: context.invokedFunctionArn,
        memoryLimitInMB: context.memoryLimitInMB,
        awsRequestId: context.awsRequestId,
        logGroupName: context.logGroupName,
        logStreamName: context.logStreamName,
        identity: context.identity,
        clientContext: context.clientContext,
    });
};

上のLambdaで確認すると、、、お見事大当たり。なんだちゃんと渡って来てるじゃないかと。

コンソールの実行結果: f:id:takamints:20180630102131p:plain

非同期Lambdaのcallbackの扱いは?

じゃ、「コールバックはどこ行った?」と確認してみると、しっかり第3引数でもらってました。何も変わっちゃいなかったのね。

そして、Async なLambdaで、従来通り callback を呼び出した場合は、優先的に戻り値として採用されるようになっています。

以下のLambdaを呼び出すと "OK!" が返されます。

exports.handler = async (event, context, callback) => {
    callback(null, "OK!");
    return JSON.stringify({
        callbackWaitsForEmptyEventLoop:
            context.callbackWaitsForEmptyEventLoop,
        functionName: context.functionName,
        functionVersion: context.functionVersion,
        invokedFunctionArn: context.invokedFunctionArn,
        memoryLimitInMB: context.memoryLimitInMB,
        awsRequestId: context.awsRequestId,
        logGroupName: context.logGroupName,
        logStreamName: context.logStreamName,
        identity: context.identity,
        clientContext: context.clientContext,
    });
};

結論と所感など

この調査で分かったこと:

  • Node.js 8.10 の Async な Lambda にも、contextやcallbackコールバックは渡されている(単にひな型の引数リストからなくなってるだけ)。
  • コールバックで解決した値が、return よりも優先的にLambdaの戻り値として採用される。

contextについて、あえて渡さないことにメリットがないので当たり前と言えば当たり前か。 自分の妙な思い込みが邪魔をしました(笑)。

コールバックの件については、古いLambdaを改造するときに、どうしても await を使いたくなったら、Node.js のランタイムバージョンを8.10に変更して、async を付け加えれるだけで済むのですね。レアケースかも知れんけど。

しかし、なんで初期状態の引数リストからcontextを削除しちゃったのだろうかね?

callbackを、あえて示さないのは理解できるが・・・。

async / await の基本事項 ― やっぱりPromiseは無視できない

f:id:takamints:20180527125137j:plain
photo credit: simmons.kevin4208 Promise via photopin (license)

JavaScriptのES2017で使えるようになった async/await 。 従来Promiseで書いていた非同期処理が、ずいぶん簡潔に書けるようになりました。

AWS Lambdaでも、2018年4月から Node.js v8.10(LTS) が使えるようになっており、新規作成したハンドラーは async 関数になっています。

しかし「async/await を使えば、Promiseについて知らなくてもよい」とは言えません。 むしろしっかり理解しておく必要がありますよと。

つまり、async / await は Promiseを置き換えるものではなく「Promiseによる非同期処理を同期処理的に記述するための記法」なのです。

ということで、ここには async / await の基本事項と気づいた点やら困った点などを、まとめておきたいと思います(Promiseを理解している人向けの内容です)。

関連記事:
takamints.hatenablog.jp takamints.hatenablog.jp

目次:

これからWebをはじめる人のHTML&CSS、JavaScriptのきほんのきほん
たにぐちまこと
マイナビ出版 (2017-03-27)
売り上げランキング: 6,710

async関数とはどういうものか?

async関数は、その記述内容に関わらず、必ずPromiseオブジェクトを返す関数です。 必ずしも非同期動作するわけではないことには軽く注意が必要です(これ後述します)。 また、構文的には関数内で後述の await を使えることも示しています。

以降、これらをもう少し細かく書いてます。

見かけによらずPromiseオブジェクトを返します

async関数は、どのように記述されていようと、Promiseオブジェクトを返します。 以下の何もしなさそうに見える関数 foo も、しっかりPromiseを返します。

async function foo() {   }

これをPromiseを明示的に使って書くとこうなります。

function foo() {
    return new Promise(
        (resolve,reject) => {
            resolve();
        }
    );
}

上に示したどちらのfooも、Promiseを返しますので、以下のコードは正しく動作します。

foo().then(()=>{
    console.log("ほらね!");// =>ほらね!
});
asyncだからって非同期だとは限らない

関数定義でasyncを指定しただけで非同期関数にはなりません。 同期的に解決するPromiseはあり得ます。 上の foo も、同期的に解決しています。

Promiseのコンストラクタ内でresolveハンドラが呼ばれており生成されたPromiseを返しているので、明らかに同期的に動作してます。

もうひとつ。以下のスクリプトを実行してコンソールに先に表示されるのは "foo" でしょうか、"bar"でしょうか。

async function foo() {
    //await new Promise(r=>setTimeout(()=>r(),0));
    console.log("foo");
}
function bar() {
    foo();
    console.log("bar");
}
bar();

これ実は、”foo” なんですよ。

Async関数だから「"foo"は後から表示される」と思っていましたが、手元のNode.jsで検証した結果、そうなりました。 つまり同期的に実行されているのですね。

ちなみに foo のコメントアウトを外せば"foo" はあとから表示されます(タイマー値が0でも)。

この動き、地味ですけれど覚えておいたほうがよさそうですね。

そのPromiseは見かけ上の戻り値で解決(resolve)される

上のfooが返すPromiseオブジェクトは、undefinedで解決(resolve)されます。 それは見かけ上この関数が何も返していない(≒undefinedを返している)からです。

async function foo() {  return  "ちゃんと動いた!"; }

foo().then( ( result )=>{
    console.log( result ); // => ちゃんと動いた!
});

エラー投入でrejectされる。

Promiseを明示的に生成するときには、エラーの発生はrejectコールバックで伝えましたが、async関数ではErrorをthrowすればよろしい。 そうすると、実質的な戻り値のPromiseオブジェクトはrejectされて、呼び元の catchハンドラが呼ばれます。

async function foo() {
    throw new Error("エラーだよ!");
}
foo().then( () => {} ).catch( err => {
    console.log( err.message ); // => エラーだよ!
});

asyncがコールバック地獄から救ってくれるわけじゃない

妙な言い方になっていますが、つまり、コールバック関数による旧式非同期処理を最新式のPromise化するには直接Promiseを使うしか方法はなく、asyncは直接的に関わらないということです。

例えば、コールバックによる非同期処理の fs.readFile をPromise化するラッパーを作るには、以下のようになります。

function readFile(filename) {
    return new Promise((resolve, reject)=>{
        fs.readFile(filename, (err, data) => {
            if(err) {
                 reject(err);
            } else {
                resolve(data);
            }
        });
    });
}

これを、async関数にもできますが下のようになる程度。 上の実装で既にawaitできますから「確実にPromiseを返すことを宣言する」以外に意味はありません。

async function readFile(filename) {
    return await new Promise((resolve, reject)=>{
        fs.readFile(filename, (err, data) => {
            if(err) {
                 reject(err);
            } else {
                resolve(data);
            }
        });
    });
}

awaitはPromiseの解決を待つ記法

await は、async関数の中だけで使えて、Promiseオブジェクトの解決した値を同期的な記述で得る記法です。 あくまでも記法なので、Promiseと本質的に動作の違いはありません。だからawaitは処理をブロックしたりしません。

だから async関数でなくても await できます。そもそも関数でなくてもかまいません。

async 関数はPromiseを返す関数だからawait可能なだけなんですね。 Promiseを明示的に返す関数でも await できるし、Promiseオブジェクトを直接 await したってかまいません。

これらは結構最初に勘違いしやすいところだと思います。

以下のconsole.log はすべてOK!を表示します。

async function foo() { return "OK!"; }
function bar() {
    return new Promise( resolve => { resolve("OK!"); });
}
async function main() {
    let result = await foo();
    console.log(result);
    let p = foo();
    console.log(await p);
    console.log(await bar());
    console.log(await (new Promise( resolve => {
            resolve("OK!");})));
    // => OK!
    // => OK!
    // => OK!
    // => OK!
}

main();

※ awaitはasync関数でしか使えないので、async mainを定義しています。無名関数の即時実行でも構いません。

上のmainをPromiseで書き直すと以下のようになりますね。 長いだけじゃなくて、処理のまとまりがわかりにくいですね。 thenを分割すれば、さらにコードが長くなります。

function main() {
    foo().then( result => {
        console.log( result );
        let p = foo();
        return p;
    }). then( result => { 
        console.log( result );
        return bar();
    }). then( result => { 
        console.log( result );
        return new Promise( resolve => {
            resolve("OK!");});
    }). then( result => { 
        console.log( result );
    });
}

awaitしているPromiseがrejectされたらエラー投入

async関数と対になっている感じですけど、awaitしているPromiseがrejectされたら、エラーがthrowされます。

エラーを捕捉したければ、普通に try~catch で括ればよろしいです。

await は async関数の中だけで使えますから、try~catchがないなら、前述の通り async関数が暗黙的に返すPromiseオブジェクトはrejectされます。

その他いろいろ

複数のPromiseを全部待つ

複数の非同期処理(Promise)すべてが解決するまで待つのは await だけではできなくて、Promise.all を await します。 Promise.all には Promise配列を渡します。そして、全てが解決すれば解決するPromiseを返します。 なので await できるのですね。 この Promise.all が返したPromiseの解決値(=await結果)は、パラメータで与えたPromise配列の各要素の解決値の配列です。

これらをきちんと理解すれば、かなり楽に書けますね。

function delayedSquare(n) {
    return new Promise(
        (resolve, reject) => {
            setTimeout( ()=> {
                resolve(n * n);
            }, 3000);
        });
}

(async () => {
    let result = await Promise.all(
        [0,1,2,3].map( async n => {
            return await delayedSquare(n);
        }));
    console.log(JSON.stringify(result));
})();

順次実行(非同期処理の直列化)

ちなみに、非同期処理を順次実行するには「Array.reduce で出来ます」てなことが検索結果によくでてきますが、「できる=そうすべき」ってことでもないので、その辺のことを以下に書きました。

takamints.hatenablog.jp

await書き忘れ問題(未解決)

awaitを書き忘れることがたまにありますが、これ厄介です。 eslintなどでもエラーにならないんですよね。 実行時にも単に処理を待っているだけの(値をawaitしていない)場合はエラーになりません。

async function foo() {
    await bar();
    await baz();
}

上のように順次処理を書いてるつもりが、下のようにawaitをすっかり忘れてもエラーにならず、単に2つのPromiseが宙に浮いて非同期実行されるだけになってしまう。

async function foo() {
    bar();
    baz();
}

リファクタリングしている場合に何度かハマりました。処理順がごちゃごちゃになってしまうんですよね。 どうにかならんかなと思っていますが、未解決です。

async 関数内にしか await が書けない理由?

(これは独り言)

awaitが、async宣言された関数内でしか使えないというのは、必ずしも設けなくてもよい制限のように思えますが、非同期処理の間違いを防ぐための仕様なのだろうと思っています。

awaitを使っているということは明示されていないPromiseオブジェクトのthenのハンドラーで処理されています。 async関数以外では、この暗黙のPromiseを返さないように記述できてしまいますが、そうするとawaitの結果が宙に浮いてしまうことになります。 暗黙的なPromiseの結果を待つ await を使用するのは、暗黙的にPromiseを返す関数、つまり async 関数の中だけに限定しているのではないかなと。

まとめ

上に書いた以外にも、「async取り除き忘れ問題」とか、「asyncいちいちつけるのメンドクセー問題」とか言いがかりに近いものも含めるといろいろありますが、「async / awaitで非同期処理を簡潔で分かりやすく書けるようになった」のは事実。 しかしPromiseのことが分かっていないとコードを追いかけられないというのもまた事実なんですね(←これが言いたかった)。

AWS Lambda でも、2018年4月から ES2017に対応していて、デフォルトのハンドラーがAsync関数になりました。以下関連記事ですー。

takamints.hatenablog.jp

mouseenterとmouseoverの違いなどDOMイベントの発生状況を可視化して調べてみたよ

f:id:takamints:20180507065734p:plain

HTML5のDOMイベントに、mouseentermouseoverという、よく似たマウスイベントがあります。ここには、その違いについて調べたことを書いておきます。

どちらもマウスポインターが要素の上に入ってきた時に発生するイベントで、それぞれに対応する「マウスポインタ―が要素から外れた」時のイベントとして mouseleavemouseout もありますね(mouseenter には mouseleavemouseover には mouseout が対応します)。

この2種類のイベント間には、発生要因や伝播(バブリング/プロパゲーション)に関する違いがあります。

私は最近まで、この違いを意識しておらず「歴史的理由による別名?」かと思っていて、その場で適当に思いついた方を使っていました。 先日ふと疑問に思ってMDNで調べてみたら、どうやら上記のように明確な違いがあると知ったのですが、はっきりイメージがつかみきれなかったので確認用に作ってみたのが以下のものです。

マウスイベントを可視化する

以下、4つのDOM要素が入れ子になっていて、各要素が拾ったマウスイベントを表示します(前述の4つのイベントだけ調べるつもりでしたが、せっかくなので他のイベントもListenしました)。要素の上でマウスを動かせば各インジケータがビカビカします(ちょっとうるさいですが目立つと思って)。

各要素には、preventDefaultstopPropagationというチェックボックスがあります。 それぞれチェックを付けると、その要素のイベント処理で event.preventDefault()event.stopPropagation() を実行します。 例えば、‘#Div4‘の上でマウスを動かすとmousemoveが全要素に発生しますが、stopPropagationにチェックを入れた要素の親へは伝播しません (preventDefaultはこれらのイベントに関して変化がなさそうなのですが、とりあえず残しています)。

マウスポインタが要素の「外側から内側」へ移動するとき

全てのstopPropagationのチェックを外した状態で、#Div1の左右または下の外側から*1#Div4の内側までマウスポインタ―を動かしていったとき、以下のことが観察されます(実際にやってみてください)。

*1 - マウスポインターが各要素の上側を通過すると情報を表示している要素の境目でイベントが発生してしまうので避けるべき。

マウスポインタが要素の「内側から外側」へ移動するとき

次に、#Div4 の内側から、左右または下に向かって、#Div1 の外側までマウスポインタ―を動かすと、以下のことが観察できます(全てのstopPropagationのチェックは外したままです)。

各イベントの伝播を確認

上と同じく、全てのstopPropagationのチェックを外したままで、各イベントが発生した場合、

  • mouseovermouseout は親要素へ伝播しますが、
  • mouseentermouseleave は伝播しません。

そして、伝播を抑制するには、イベントハンドラーの中で event.stopPropagation() を呼び出します。

MDNに書いてあること

MDNのmouseenterには、以下のように書かれています。

With deep hierarchies, the amount of mouseenter events sent can be quite huge and cause significant performance problems. In such cases, it is better to listen for mouseover events.

翻訳すると「深い階層では、送信される mouseenter イベントが大量になる可能性があり、重大なパフォーマンスの問題の原因になり得る。その場合は mouseoverイベントを使うといい」とのことですが、これって上で見たことと少し雰囲気が違ってないか?と。 mouseover が全ての親に伝播していて、たくさん発生しているように見えましたけど?

「逆じゃないの?」って、かなりMDNを疑ってみたのですが、しかし、これはこれでどうやら間違いではないみたい。 すべての要素でイベントをListenしているので、大量の mouseover が発生していましたが、通常そんなことはしませんね。 イベントはaddEventListenerしている要素に発生して後は親要素に向かって伝播するだけ。 イベントを処理すべき要素だけにハンドラを設定しておけばなんら問題がないはず。

一方「mouseenterが大量に発生して」という部分はよくわかりません。 そのようなケースが今のところ想定できないのですが。 mouseenterは、そもそも親要素に伝播しないし、論理的な自要素の領域内にあればmouseleaveは発生しないので、必要なところでListenしておけば問題ないような気がするし、そもそも用途が違うんじゃないのか?と。

ちょっとモヤモヤしてますが、とりあえず・・・。

リンク

Heroku CLI 7.0.2? のエラーを修正

f:id:takamints:20180422144244p:plain

Git for Windowsから最新版の Heroku CLI を起動するとエラーが発生。バージョンは下に書いてますけど、そもそもエラーが出るので確認できない。Power Shellからなら大丈夫。32ビット版でも64ビット版でも同様ですね。未確認ですが、MSYS とか Cygwinでも同じ現象が起きるんじゃないかなと思います。

とりあえずはインストールされたシェルスクリプトに1文字追加すれば治ります。

直接的な原因が分かったところでGitHubにはIssueを上げてひと安心。「さてIssueも上げたしForkしてCommitしてプルリクしてどや顔するかな相手は天下のセールスフォースドットコム・・・」と、ヤケに日曜朝から心拍数を上げたのですが(おちつけ自分)、結局どこを直せばよいのかわからなくてWatchだけして、そっとGitHubを閉じました。 が、なんと4時間後にはこの問題が改修されててIssueもClose。素早い対応ありがとうございますー。

クラウド開発徹底攻略 (WEB+DB PRESS plus)
菅原 元気 磯辺 和彦 山口 与力 澤登 亨彦 内田 誠悟 小林 明大 石村 真吾 相澤 歩 柴田 博志 伊藤 直也 登尾 徳誠
技術評論社
売り上げランキング: 88,620

インストール直後にエラー

Windows用Heroku CLIの最新版をインストールしてGit Bashから実行すると以下のエラーが発生しました。

$ heroku
sed: -e expression #1, char 7: unterminated `s' command
/c/Program Files/heroku/bin/heroku: line 4: ./../client/bin/heroku.cmd: No such file or directory

sedのsコマンドが終わってない」と言っている。

原因究明

コマンドはC:\Program Files\heroku\bin\herokuという名のシェルスクリプトなので中身を見てみると。

#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\,/,g')")

"$basedir/../client/bin/heroku.cmd" "$@"
ret=$?
exit $ret

2行目で、sedでバックスラッシュ(円記号)をスラッシュに変換しようとしているが、バックスラッシュがエスケープされていませんね。

応急対策

これエスケープするだけで動くんじゃないかね?と

f:id:takamints:20180422095556p:plain

Git Bashを管理者権限で開いてvimで編集したところ、ホントにこれだけで正しく動くようになりました。 (Program Files 以下を変更するには管理者権限が必要)

バージョンは 7.0.2、、、ん?

バージョンは以下のように7.0.2だったのですが、これってホントにリリースバージョンなんですかね。 GitHubリポジトリでは現時点でv6.16.16です。Windows用は別管理ってことはないと思うんだけど。

これ、ダウンロードページから公開しちゃいけないものを公開しちゃっているとかではないのかな。 よくわからなくなってきました。

$ heroku --version
heroku-cli/7.0.2 win32-x64 node-v9.11.1

とりあえずIssueはポスト

しかしまあ、しばしGoogle翻訳と格闘してから、とりあえずIssueをポストしておいた。

で、現在のリポジトリ内の /resources/exe/heroku が問題のスクリプトなんだけど、不思議なことに半年前に追加されたときから、きちんとエスケープされている。 ということで、どこを直せばよいのかわかりません。インストーラを作るところか動作上の問題なのか。

まあ偉い人が直してくれるんだろう。なにしろ天下のセールスフォースドットコムだし。

追記)この記事書いた時点で既にIssueがクローズされていました。早っ! 修正箇所はやっぱりresources/exe/herokuで。バックスラッシュが4つになっていましたわ。 二重にエスケープしなきゃなんなかったのかー。

関連リンク

devcenter.heroku.com

github.com

github.com