20190607のJavaScriptに関する記事は23件です。

Prettier 1.18.0 に大きなバグを作ってしまった話

今日リリースされたPrettier v 1.18.0で、自分のPRのせいでかなり大きなバグを引き起こしてしまいました。

概要

https://github.com/prettier/prettier/issues/6189

僕の出したPRの影響で、以下のようなバグが起こるようになりました。

拡張子が.tsxのファイルでtrailing-commaオプションをnoneにしていても下のような末尾カンマの挿入が起こるバグです。

// Input
function func<T>() {}
interface Interface<T> {}

// Output
function func<T,>() {}
interface Interface<T,> {}

バグの原因である僕のPRの本来の目的についてはブログを読んでいただけると嬉しいです。

原因

もともとこの末尾カンマは、型引数が一つのアロー関数のみに対してつけるべきものでしたが、私の実装ではそうなっていませんでした。結果として、.tsxのファイルでは通常の関数、インターフェース、タイプエイリアス、などあらゆる型引数受け取る構文に対して末尾カンマを挿入してしまうことになりました。

そしてバグをリリース前に発見できなかった理由ですが、フォーマット結果がファイル名(拡張子が .tsx であること)に依存するためにPlayground上でバグを発見できなかったためです。Prettierは https://prettier.io/playground でフォーマットの結果を試してみることができるのですが、これにはファイル名の概念が(現時点では)存在しないため、.tsxとして解釈されないので、Playgroundで確認しても正しく動いているように見えてしまっていたのです。

現在

現在はバージョン 1.18.1 がリリースされ、修正されています。ちなみに、その修正のPRは https://github.com/prettier/prettier/pull/6190 にあります。授業中に死ぬほどドキドキしながら書きました。

ごめんなさい

本当に申し訳なく思っています。もっと慎重にやります、、、。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Prettier 1.18.0 に大きなバグを入れてしまった

今日リリースされたPrettier v 1.18.0で、自分のPRのせいでかなり大きなバグを引き起こしてしまいました。

概要

https://github.com/prettier/prettier/issues/6189

僕の出したPRの影響で、以下のようなバグが起こるようになりました。

拡張子が.tsxのファイルでtrailing-commaオプションをnoneにしていても下のような末尾カンマの挿入が起こるバグです。

// Input
function func<T>() {}
interface Interface<T> {}

// Output
function func<T,>() {}
interface Interface<T,> {}

バグの原因である僕のPRの本来の目的についてはブログを読んでいただけると嬉しいです。

原因

もともとこの末尾カンマは、型引数が一つのアロー関数のみに対してつけるべきものでしたが、私の実装ではそうなっていませんでした。結果として、.tsxのファイルでは通常の関数、インターフェース、タイプエイリアス、などあらゆる型引数受け取る構文に対して末尾カンマを挿入してしまうことになりました。

そしてバグをリリース前に発見できなかった理由ですが、フォーマット結果がファイル名(拡張子が .tsx であること)に依存するためにPlayground上でバグを発見できなかったためです。Prettierは https://prettier.io/playground でフォーマットの結果を試してみることができるのですが、これにはファイル名の概念が(現時点では)存在しないため、.tsxとして解釈されないので、Playgroundで確認しても正しく動いているように見えてしまっていたのです。

現在

現在はバージョン 1.18.1 がリリースされ、修正されています。ちなみに、その修正のPRは https://github.com/prettier/prettier/pull/6190 にあります。授業中に死ぬほどドキドキしながら書きました。

ごめんなさい

本当に申し訳なく思っています。もっと慎重にやります、、、。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptによる共通HTMLの挿入

JQueryを使わずにheader・footerなどの共通のHTML文をJavaScriptで挿入する方法です。
といっても、innerHTMLで挿入するだけなんですけどね・・・

HTML

index.html
<!DOCTYPE html>
<html>
<head>
    <title>innerTest</title>
    <script type="text/javascript">
        function header(){
        var h = document.getElementById("header");
        h.innerHTML = "HelloWorld";
    }
    </script>
</head>
<body>
    <div id="header"></div>
    <script type="text/javascript">header();</script>
</body>
</html>

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ブラウザ側でサイズ圧縮(リサイズ)して画像表示やアップロードを行う(Vue.js)

概要

Webアプリでクライアントサイド(ブラウザ)で画像を圧縮(リサイズ)する方法の紹介です。
サンプルとして下図のようなアップロードした画像をプレビュー表示した上で、確定時にサーバーに画像をアップロードするまでのサンプルコードを紹介します。

※今回はvue.jsとvuetifyによるサンプルになりますが、UIのレイアウト部分以外は基本的に一般的なjavascirptですので
他のフレームワークを使用している場合等でも参考にはなるかと思います

sample.png

動くデモとGitHubのサンプルコードは下記です。
Demo
GitHub

背景

Webアプリで、画像ファイルをアップロードするといったシチュエーションはよくあると思います。
スマホ等で撮影した写真等だと、解像度やファイルサイズも大きいため、そのまま使用せず一度リサイズしたりする事が多いかと思います。
アップロード後にサーバ側で処理するといった手法もありますが、今回はWEBブラウザ側でリサイズする方法とします。

実現方法

当初はCanvasにリサイズした画像を描画して、再度データ化する方法を使用していました。
参考

しかしながら、もっとお手軽にかつファイルサイズも小さくしてアップできる、Browser Image Compression といったものがあるのでそちらを使用します。

画面レイアウト

以下のような、3つの領域を作成します。

  • 画像のプレビュー領域
     アップロードした画像のプレビュー表示します。サンプルではvuetifyのv-imgタグを使用していますが、imgタグに相当するもの配置します。合わせてファイル名も表示します。

  • ファイルのアップロード
     ファイルのアップロードするinputタグです。アップロードするとプレビュー領域に表示されます。

  • Submitボタン
     プレビュー表示した画像をサーバに送信するためのボタン。画像ファイルのアップロードが完了して、プレビュー表示されるまでは押せなくします。

これらをvue.jsのコンポーネントで書くと下記のようになります。
処理等は無く、まだレイアウトだけです。

ImageUpload.vue
<template>
  <v-container>
    <v-layout text-xs-center wrap>
      <v-flex xs12>
        <!-- 画像のプレビュー表示領域 -->
        <v-img :src="upimage.fileUrl" aspect-ratio="2" :contain="true"></v-img>
        <p>{{ upimage.fileName }}</p>
        <p>圧縮前サイズ(MB):{{ fileInfo.before.size }}</p>
        <p>圧縮後サイズ(MB):{{ fileInfo.after.size }}</p>
      </v-flex>
      <v-flex xs12>
        <!-- ファイルの選択 -->
        <input @change="selectedFile" type="file" accept="image/jpeg, image/jpg, image/png">
      </v-flex>
      <v-flex xs12>
        <!-- Submitボタン -->
        <v-btn color="primary" :disabled="isUploading">submit</v-btn>
      </v-flex>
    </v-layout>
  </v-container>
</template>
<script>
export default {
  data() {
    return {
      isUploading: false, // 画像ファイルアップロード中の判断フラグ
      upimage: { fileUrl: "", fileName: "", blob: null } // 画像ファイル
    };
  },
  methods: {
    async selectedFile(e) {
      // ファイルアップロード時の処理 
      // e.target.filesにファイルの情報が格納
    },
    async submit() {
      // 画像をサーバに送信する処理
    }
  }
};
</script>

画面のリサイズ

アップロードされた画像をプレビュー表示前にリサイズする処理を作成します。
まずは、npmかyarnでbrowser-image-compressionをインストールします。

npm install browser-image-compression --save
or
yarn add browser-image-compression

次に以下の2つの処理を作成します。

  1. 入力された画像ファイルの圧縮を行う処理
  2. プレビュー表示用にDataUrlを取得する処理

DataUrlにするのはimgタグにおいてプレビュー表示を行うためですので、直接アップロードしたい場合は不要です。
画像の圧縮時には最大のファイルサイズおよび、解像度を指定します。
今回は最大サイズは1MB, 解像度を800に指定しています。
その他にもオプションがありますので、詳細はbrowser-image-compressionのGitHubを参照してください。

ImageUtil.js
import imageCompression from "browser-image-compression";

export default {
  // アップロードされた画像ファイルを取得
  async getCompressImageFileAsync(file) {
    const options = {
      maxSizeMB: 1, // 最大ファイルサイズ
      maxWidthOrHeight: 800 // 最大画像幅もしくは高さ
    };
    try {
      // 圧縮画像の生成
      return await imageCompression(file, options);
    } catch (error) {
      console.error("getCompressImageFileAsync is error", error);
      throw error;
    }
  },
  // プレビュー表示用のdataurlを取得
  async getDataUrlFromFile(file) {
    try {
      return await imageCompression.getDataUrlFromFile(file);
    } catch (error) {
      console.error("getDataUrlFromFile is error", error);
      throw error;
    }
  }
};

画面への処理の取り込み

この画像処理を先ほどのレイアウト内で呼び出せば終了です。

ImageUpload.vue
// 略
<script>
import ImageUtil from "../lib/imageUtil";
export default {
  // 略
  methods: {
    async selectedFile(e) {
      this.isUploading = true;

      const file = e.target.files[0];
      if (!file) {
        return;
      }

      try {
        // 圧縮した画像を取得
        const compFile = await ImageUtil.getCompressImageFileAsync(file);

        //ファイルサイズの表示
        this.fileInfo.before.size = (file.size / 1024 / 1024).toFixed(4);
        this.fileInfo.after.size = (compFile.size / 1024 / 1024).toFixed(4);
        // 画像情報の設定
        this.upimage.blob = compFile;
        this.upimage.fileUrl = await ImageUtil.getDataUrlFromFile(compFile);
        this.upimage.fileName = file.name;
      } catch (err) {
        // エラーメッセージ等を表示
      } finally {
        this.isUploading = false;
      }
    },
    submit() {
      const fd = new FormData();
      try {
        fd.append(this.upimage.fileName, this.upimage.blob, this.upimage.fileName);
        // ここにサーバーへのアップロード処理を実装する
      } catch (err) {
        // エラーメッセージ等を表示
      }
    }
  }
};
</script>

まとめ

ということで、browser-image-compressionを使うことで、お手軽に画像の圧縮及び表示が可能となります。
是非試してみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【翻訳】より簡潔で高性能なJavascriptを書くための7の便利なコツ

はじめに

読んでいてストックしておきたいなと思ったので、初めて翻訳記事を書きます。
以下、翻訳です。

本文

私たちがJavaScriptを書くとき、私たちは他人のコード、ウェブサイト、そして私たちが使ったチュートリアル以外の場所で見つけたコーディングを効率化するトリックリストを作成しました。

それ以来、私たちはこのリストにトリックを追加してきました。この記事は初心者にも役立つように書かれていますが、JavaScriptを扱えるデザイナーでさえこのリストの中から何か新しいものがないか見つけようとしています。

これらのコードの多くはどのような状況でも役に立ちますが、簡潔さよりも一貫性が重要であることが多いプロダクションレベルのコードにはふさわしくない場合があります。 みなさんにはそれを判断して欲しいと思っています。

1. JSON.stringifyを使用してJSONコードをフォーマットする

あなたは以前にJSON.stringifyを使用したことがあるかもしれませんが、それはJSONをインデントするのにも役立照ることができると知っていましたか?

stringify()メソッドは、文字列化の手順の挙動を変更する関数と出力するJSON文字列に可読性を目的に空白を挿入するために使うスペース値の2つのオプションパラメータを取ります。

スペース値は、必要なスペース数を表す整数または文字列(タブを挿入するための't'など)を取るので、取得したJSONデータを読みやすくすることができます。

console.log(JSON.stringify({ fruit: 'apple', vegetable: 'broccoli' }, null, 't'));
// Result:
// '{
//     "fruit": apple,
//     "vegetable": broccoli
// }'

2.配列の最後の項目を取得する方法

slice()配列メソッドは負の整数を取ることができ、その場合配列の先頭からではなく末尾から値を取ります。

let array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
console.log(array.slice(-1)); // Result: [9]
console.log(array.slice(-2)); // Result: [8, 9]
console.log(array.slice(-3)); // Result: [7, 8, 9]

3.配列を切り捨てる方法

配列の末尾から値を削除したい場合は、 splice()を使用するより速い選択肢があります。 たとえば、元の配列のサイズがわかっている場合は、その長さプロパティを次のように再定義できます。

let array = [a, b, c, d, e, f, g, h, i, l];
array.length = 4;
console.log(array); // Result: [a, b, c, d]

4.固有値をフィルタリングする方法

ES6でspread演算子とともに導入されたsetオブジェクト型を使用して、一意の値のみを持つ新しい配列を作成できます。

const array = [1, 1, 2, 3, 5, 5, 1]
const uniqueArray = [...new Set(array)];
console.log(uniqueArray); // Result: [1, 2, 3, 5]

ES6のおかげで、一意の値を分離するのに多くのコードが必要なくなりました。

このトリックは、undefined、null、boolean、string、numberの配列のプリミティブ型に対して機能します。 アイテム、機能、または追加の配列を含む配列がある場合は、異なる方法が必要になります。

5.Bool値に変換

通常のtrueおよびfalseのBool値とは別に、JavaScriptは他のすべての値も"true"または"false"として扱います。

'falsy'である 0, "", null, undefined, NaNと明らかなfalseを除けば、特に他に定義されていない限りJavaScriptのすべての値は'true'です。

否定演算子!を使うことで、trueとfalseを素早く切り替えることができます。これも型を "boolean"に変換します。

const isTrue  = !0;
const isFalse = !1;
const alsoFalse = !!0;
console.log(isTrue); // Result: true
console.log(typeof true); // Result: "boolean"

条件付きステートメントでは、この種の変換は有用ですが、前述したように、プロダクションでは逆効果になる可能性があります。

6.文字列に変換する

数値を素早く文字列に変換するには、連結演算子+とそれに続く空の引用符 ""を使用します。

const val = 23 + "";
console.log(val); // Result: "23"
console.log(typeof val); // Result: "string"

7.文字列を数値に変換する方法

  • (加算演算子)を使用すると、逆の操作を迅速に実行できます。
let int = "23";
int = +int;
console.log(int); // Result: 23
console.log(typeof int); // Result: "number"

これは、以下に示すように、Bool値を数値に変換するためにも使用できます。

console.log(+true);  // Return: 1
console.log(+false); // Return: 0

ところで、文字列を数値に変換する方法はたくさんあります。 文字列を数値に変換するための少なくとも5つの方法があると、すでに知っている方法を含めて考えられます。

parseInt(num); // default way (no radix)
parseInt(num, 10); // parseInt with radix (decimal)
parseFloat(num) // floating point
Number(num); // Number constructor
~~num //bitwise not
num / 1 // diving by one
num * 1 // multiplying by one
num - 0 // minus 0
+num // unary operator "+"

私たちの考えでは、コンストラクタ以外のコンテキストで(newキーワードなしで)Numberオブジェクトを使用するのが最も良い方法です。

const count = Number('1234') //1234

小数点を含む文字列を解析する必要がある場合は、代わりにIntl.NumberFormatを使用します。

整数に代わるもう1つの良い方法は、 parseInt()関数を使用することです。

const count = parseInt('1234', 10) //1234

2番目のパラメータ、つまり10進数の場合は常に10を忘れないようにしてください。そうしないと、変換によって基数が推測され、予期せぬ結果が生じる可能性があります。

parseInt()は、数字以外を含む文字列から数字を取得しようとします。

parseInt('10 cats', 10) //10

※ 文字列が数字で始まっていない場合は、NaN(Not a Number)が返されます。

parseInt("mouse 10", 10) //NaN

さらに、Numberのように数字の間の区切り文字については信頼できません。

parseInt('10,000', 10) //10     ❌
parseInt('10.00', 10) //10     ✅ (considered decimals, cut)
parseInt('10.000', 10) //10     ✅ (considered decimals, cut)
parseInt('10.20', 10) //10     ✅ (considered decimals, cut)
parseInt('10.81', 10) //10     ✅ (considered decimals, cut)
parseInt('10000', 10) //10000  ✅

小数を使いたい場合は、 parseFloat()使用してください。

parseFloat('10,000', 10) //10     ❌
parseFloat('10.00', 10) //10     ✅ (considered decimals, cut)
parseFloat('10.000', 10) //10     ✅ (considered decimals, cut)
parseFloat('10.20', 10) //10.2     ✅ (considered decimals)
parseFloat('10.81', 10) //10.81     ✅ (considered decimals)
parseFloat('10000', 10) //10000  ✅

+と似ていますが、整数部分を返すのはMath.floor()です。

Math.floor('10,000') //NaN ✅
Math.floor('10.000') //10 ✅
Math.floor('10.00') //10 ✅
Math.floor('10.20') //10 ✅
Math.floor('10.81') //10 ✅
Math.floor('10000') //10000 ✅

以下は最も速いオプションの1つで、演算子+単項演算子のように動作するため、数値がfloatの場合は整数への変換は実行されません。

'10,000' * 1 //NaN ✅
'10.000' * 1 //10 ✅
'10.00' * 1 //10 ✅
'10.20' * 1 //10.2 ✅
'10.81' * 1 //10.81 ✅
'10000' * 1 //10000 ✅

最後に

訳すだけでもちゃんと全部読もうとするので理解が進みます。結構おすすめです

オリジナル:
https://www.ma-no.org/en/useful-tricks-for-writing-more-concise-and-performant-javascript

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WKWebViewでローカルのindex.htmlを読み込んでscriptを実行してみた

ネットを介してのWebView表示ではなく、手元にあるHTMLをWebViewで表示、scriptタグ内のJSメソッドの呼び出し方法をまとめます。

SwiftでWebViewを表示させる場合、WKWebViewを使うことがほとんどだと思います。現在ではUIWebViewは非推奨です。そちらについては以下の記事がとても参考になります。
[参考元]:UIWebViewを使わない理由とWKWebViewを使う理由

環境

OS: Mojave 10.14.5
Xcode: 10.1
Swift 4.2

HTML

今回はindex.htmlをWKWebViewでロードし、<script>タグ内にあるJSメソッドを呼び出すことをします。
HTMLファイルはプロジェクト内に置きます。置き場は自由で構わないのですが、例として以下のように配置しておきます。

project
├─Resource
│ ├index.html
│ └sample.json
│
└─View
  └SomeViewController

HTMLのロード

ローカルのHTMLファイルはパスを取得する必要があり、取得にはBundle.main.pathを使用します。
仕様ではBundle.main.pathはプロジェクトのBuild Setting > Copy Bundle Resourcesにあるファイルを取得するようです。
まずこの項目を確認し、取得したいHTMLファイルがなければ追加しておきましょう。

[参考元]:Swiftでプロジェクト内のファイルを取得する際の注意点2つ

HTMLファイルのパスをURL型にキャストしてWebViewのloadFileURLの引数に与えます。これでWebViewにロードできました。

func loadLocalHTML() {
    guard let path: String = Bundle.main.path(forResource: "index", ofType: "html") else { return }
    let localHTMLUrl = URL(fileURLWithPath: path, isDirectory: false)
    webView.loadFileURL(localHTMLUrl, allowingReadAccessTo: localHTMLUrl)
}

Swift側からscript内のjsを実行

実行タイミング

<script>タグにあるメソッドは、HTML全てがロードし終わってから呼び出さないとメソッド呼び出しに失敗します。
ロードが終わったことを通知するdelegateがWKNavigationDelegateにあります。

delegateの通知先は自分であることの宣言を忘れずに。

SomeViewContoller
webView.navigationDelegate = self
SomeViewController
extension SomeViewController: WKNavigationDelegate {

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        // ここでJSの実行をする
    }
}

JSの実行

evaluateJavaScriptを使います。引数のjavaScriptStringに、実行するメソッドをStringで渡します。
また、ここのStringではjsの文法で書く必要があります。jsの知見が乏しい筆者はここで躓きました。

[公式リファレンス]:evaluateJavaScript(_:completionHandler:)

<script>の中身は以下のようなものと仮定して、initHTML関数に引数を渡し実行する方法を解説します。

<script>
    function initHTML(id, jsonData, onload) {
        console.log('initHTMLだお');
        console.log(id, jsonData);
        onload();
    }
</script>

引数

Swiftは静的型付け、JSは動的型付けなので、ゆるい方向への値渡しです。
initHTMLの引数(id, jsonData, onload)は、Swift側ではそれぞれInt,Json,関数として渡します。

基本は文字列リテラル内での値の展開\()を使います。
Jsonに関しては、Jsonファイルを読み取ってData->Stringに変換して、値展開で渡しています。Stringで渡してもWebView側でJsonとして扱ってくれます。
関数はjsの書き方であるアロー関数を用いて書きましょう。

extension SomeViewController: WKNavigationDelegate {

    func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        let intValue = 10
        guard let json = getLocalJson() else { return }
        guard let strJson = String(data: json, encoding: .utf8) else { return }
        let script = "initHTML(\(intValue), \(strJson), () => { console.log('Swiftから渡された値だお') });"
        webView.evaluateJavaScript(script) { object, error in
            print(error ?? "成功")
        }
    }

    func getLocalJson() -> Data? {
        guard let path = Bundle.main.path(forResource: "sample", ofType: "json") else { return nil }
        let url = URL(fileURLWithPath: path)
        let json: Data?

        do { json = try Data(contentsOf: url) }
        catch { json = nil }

        return json
    }
}

コンソールの確認

JSの実行ができたらコンソールを確認しましょう。
しかしWebViewで実行したJSコンソールログはXcodeでは見れません。
ブラウザであるSafariで見ることができます。開発 -> 端末名 -> index.jsと選択するとWebインスペクターが開きます。

webinspector.png

呼び出しがうまくいってログが出力されています!
表示がない場合は左上らへんにある更新マークを押しましょう。

次回

WebViewのJS内でprojectのソース(画像とか)にアクセスしたときに発生するエラー、Orijin null is not byAccess-Control-Allow-Origin編に続く...

WWDC19あとがき

SwiftUIめっちゃすごい。iOSのUI回りに激震が走りましたね。
ここまでUIの作成ハードルが下がると我々iOSエンジニアの価値が相対的に下がっていくのでは…
Combine frameworkの登場により、Rxライブラリは今後必要なくなるのかな。ただObserverパターンの考え方は必要なのでここはそれなりにハードルは依然高そう。

参考

iOS WKWebView ネイティブとローカルJavascript連携

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.js(Express)からmongoDBをREST API的にたたく2019

やりたいこと

mongoDBを使って、Key=Valueの1ペアを格納して、Keyで値がとれて、KeyでUpdateもDeleteもできる。
ただそれだけのシンプルなDBがほしいから作りました。

_id ではなく Key というのがみそです。

例えば、name=ドルリーレーン とやればそれをただ記録してくれる。nameは? といえばドルリーレーンと返してくれる。othername=エラリークイーン とやれば追加で記録してくれて、othernameは? といえばエラリークイーンと返してくれる。

本当に簡単な実験的な仕組みをつくりたいときにこういうシンプルなDBがあると便利なので作りました。

※ 認証などの仕組みはいっさいないのでローカルのテストとか閉じたネットワークでお試しください
※ Keyは何個でも格納できますが同じKeyが複数あってもエラーにもなりません。いいんです実験用なので。

ついでに expree + mondgoDB の良いサンプルにもなると思いますので公開しておきます

使い方はこうです

$ curl http://xxx/data -X POST -d "name=mio"  ← データ挿入
$ curl http://xxx/data/name  ← mio が返ってくる
$ curl http://xxx/data/  ← 格納している全データが返ってくる
$ curl http://xxx/data/name -X PUT -d "name=miomio"  ← nameの値が更新される
$ curl http://xxx/data/name -X DELETE  ← nameの値が削除される

環境+コード含めて解説

mongoDBのインストール!

EC2 Amazon Linuxでやっていますが他の環境では適宜読み替えを。

sudo vi /etc/yum.repos.d/mongodb-org-4.0.repo
[mongodb-org-4.0]
name=MongoDB Repository
baseurl=https://repo.mongodb.org/yum/amazon/2013.03/mongodb-org/4.0/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-4.0.asc
sudo yum install -y mongodb-org

確認。

mongo -version

OKですね。次に進みます。

Expressのサーバ立ち上げ

Node.jsは入ってること前提で。

npm install -g express-generator
express -e site
cd site
npm install
PORT=8080 npm start

個人的な色々な理由でポート8080で立ち上げてますがこの辺りはご自由に。。

とりあえずブラウザでアクセスしてみて確認。良く見慣れたこれが出ます。

image.png

OKです。
Ctrl+Cとかでサーバは止めておきます。

ExpressからmongoDBの操作のアプリを作る。

Expressのフォルダ(今回はsite)にいる状態て mongodb を扱うためのライブラリをインストール。

npm install mongodb

REST APIの本題であるdata.jsを作ります。
※コードが冗長ですみません…

routes/data.js
ar express = require('express');
var router = express.Router();

var MongoClient = require('mongodb').MongoClient;
var ObjectID = require('mongodb').ObjectID;
var url = "mongodb://localhost:27017/";

// GET find
router.get( '/', function ( req, res ) {
  MongoClient.connect(url, { useNewUrlParser: true }, function(err, client) {
    client.db('db').collection("restapi").find().toArray( function(err, r) {
      client.close();
      res.send(r);
    });
  });
} );

// GET find :id
router.get( '/:id', function ( req, res ) {
  MongoClient.connect(url, { useNewUrlParser: true }, function(err, client) {
    let key = {}
    key[req.params.id] = { $regex:".*" }
    client.db('db').collection("restapi").findOne( key, function(err, r) {
      client.close();
      res.send(r);
    });
  });
} );

// POST insert data
router.post( '/', function ( req, res ) {
  MongoClient.connect(url, { useNewUrlParser: true }, function(err, client) {
    let obj = req.body;
    client.db('db').collection("restapi").insertOne(obj , function(err, r) {
      client.close();
      res.send(r);
    });
  });
});

// PUT update data
router.put( '/:id', function ( req, res ) {
  MongoClient.connect(url, { useNewUrlParser: true }, function(err, client) {
    let obj = req.body;
    let key = {}
    key[req.params.id] = { $regex:".*" }
    client.db('db').collection("restapi").findOneAndUpdate( key, {$set:obj}, {}, function(err, r) {
      client.close();
      res.send(r);
    });
  });
} );

// DELETE remove data
router.delete( '/:id', function ( req, res ) {
  MongoClient.connect(url, { useNewUrlParser: true }, function(err, client) {
    let key = {}
    key[req.params.id] = { $regex:".*" }
    client.db('db').collection("restapi").findOneAndDelete( key, function(err, r) {
      client.close();
      res.send(r);
    });
  });
} );

module.exports = router;

ここで作ったdata.jsを使うようにメインのapp.jsも少しだけ書き換えます。

app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index');
var dataRouter = require('./routes/data'); // ★これを追加★

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/data', dataRouter); // ★これを追加★

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

※ 最初にはいってた Users は消しちゃってますが残っててもいいです

テスト

これで動く環境は整ったので実行。

PORT=8080 npm start

クライアント側のシェルなどから適当にコマンドをうってみます。

$ curl http://xxxx:8080/data
[]

最初は空です。
データを2つくらい追加してみます。

$ curl http://xxxx:8080/data -X POST -d "name=mio"
{"result":{"n":1,"ok":1},"connection":{"id":3,"host":"localhost","port":27017},"ops":[{"name":"mio","_id":"5cfa406159b3531cf05214f6"}],"insertedCount":1,"insertedId":"5cfa406159b3531cf05214f6","n":1,"ok":1}

$ curl http://xxxx:8080/data -X POST -d "milk=coffee"
{"result":{"n":1,"ok":1},"connection":{"id":5,"host":"localhost","port":27017},"ops":[{"milk":"coffee","_id":"5cfa40b459b3531cf05214f7"}],"insertedCount":1,"insertedId":"5cfa40b459b3531cf05214f7","n":1,"ok":1}

追加されたっぽいので確認してみます。

$ curl http://xxxx:8080/data/name
{"_id":"5cfa406159b3531cf05214f6","name":"mio"}

$ curl http://xxxx:8080/data/milk
{"_id":"5cfa40b459b3531cf05214f7","milk":"coffee"}

更新してみます。

$ curl http://xxxx:8080/data/name -X PUT -d "name=miomio"

$ curl http://xxxx:8080/data/name
{"_id":"5cfa406159b3531cf05214f6","name":"miomio"}

削除も。

$ curl http://xxxx:8080/data/name -X DELETE

とりあえずこんな感じ。

あとはこれを Vue.js などのフロントからgetでもpostでも何でもうまいこと扱ってあげたら色々簡単にためすにはよいと思います。

謝辞

こちらのQiitaからインスピレーションいただきました。多謝。
https://qiita.com/itagakishintaro/items/a1519998a91061cbfb1e

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

blurイベントで特定のボタンだけでイベントをキャンセルするのに、event.relatedTargetが使えなかったとき。

概要

  • 次はWeb屋さんに転職するので、勉強も兼ねて Vue.js + Vuetify でUIを作っていた矢先。
  • せっかくだからレスポンシブにして、実機で確認していたら、その時は訪れました。
  • 自分へのメモも兼ねています。

環境

プラットフォームの関係からNode.jsを使わずにJavascriptだけで書いています。jqueryも使える環境なので結構使ってます。

最初の実装

今回作っていたのは、名前と予定が一覧表示され、クリックするだけで自由に編集できる、会社によくあるホワイトボードのようなものです。以下、usersにユーザオブジェクトがリストされていてのループでuserを表示するくだりです。

code1.js
  :
<template v-for="(user) in users">
<v-layout wrap class="user">
<template v-if="user.editTask">
<v-flex>
<v-text-field v-model="user.task" @keyup.enter="updateTask(user)" @blur="updateTask(user)" :ref="'edit' + user.id" :placeholder="user.name + 'さんの予定を入力'" single-line outline hide-details />
</v-flex>
</template>
<template v-else>
<v-flex class="user-name" @click="editTask(user)">{{user.name}}</v-flex>
<v-flex class="user-task" @click="editTask(user)">{{user.task}}</v-flex>
</template>
  :
,
methods: {
  editTask: function(user) {
    user.editTask = true;
    this.$nextTick(function() {
      var ref = this.$refs['edit' + user.id][0];
      ref.focus();
    });
  },
  updateTask: function(user) {
    user.editTask = false;
    // ここでDBを更新する処理など...
  },
  :

ユーザの名前と予定が一覧表示されていて、クリックすると editTaskメソッドが呼ばれ、予定を入力するためのテキストフィールドが現れます。尚、$nextTickのくだりは、出現したテキストフィールドにフォーカスを当てるためのものです。Enterを押すか、フォーカスが外れたらupdateTaskメソッドが呼ばれ、名前と予定の表示に戻ります。ここまでは大きな問題もなく、PCでもモバイルでも想定通り動作しました。例にはちゃんと書いてませんが、レイアウトはVuetifyのGrid systemを活用しました。元々Bootstrapを使っていたので、特に迷うことなく移行できました。

使い勝手を改善する

その後、予定を文章で入力するのがめんどくさい、という人のために、テキストフィールドの横に一発で文章を入力するためのボタンを置こうと思いつき、次のようなテンプレートに変更しました。

code2.js
  :
<template v-if="user.editTask">
<v-flex>
<v-text-field v-model="user.task" @keyup.enter="updateTask(user)" @blur="updateTask(user)" :ref="'edit' + user.id" :placeholder="user.name + 'さんの予定を入力'" single-line outline hide-details />
</v-flex>
<v-flex>
<v-btn small round class="quick-task" color="primary" text-color="white" @click="addTask(user,'打合せ')">打合せ</v-btn>
</v-flex>
</template>
  :
,
methods: {
  :
  addTask: function(user,text) {
    user.task = user.task + text;
  },

UI自体は簡単にできたのですが、ボタンのクリックよりもフォーカスが外れるイベントが先に飛ぶのでしょうね、ボタンをクリックしようとしてもボタンは消え、実際にはクリックされずにaddTaskメソッドが呼ばれません。なんとなく押したような感じにはなるのが憎いです。で、何か手立てはないか探してみたところ、event.relatedTargetに関連する要素が入るようで、ボタンをクリックしたときはここにボタンの要素が入ってました。そこで、以下のように修正しました。

code3.js
  :
<v-text-field v-model="user.task" @keyup.enter="updateTask($event,user)" @blur="updateTask($event,user)" :ref="'edit' + user.id" :placeholder="user.name + 'さんの予定を入力'" single-line outline hide-details />
  :
,
methods: {
  :
  updateTask: function(event, user) {
    if (event && event.relatedTarget && $(event.relatedTarget).hasClass('quick-task')) {
      event.stopPropagation();
      return;
    }
    user.editTask = false;
    // ここでDBを更新する処理など...
  },
  addTask: function(user,text) {
    user.task = user.task + text;
    this.updateTask(null, user);
  },
  :

先にupdateTaskメソッドが呼ばれ、event.relatedTargetがあれば、クラスを使って該当のボタンであればイベントをキャンセルするようにします。そうすると消えずにボタンがクリックできるので、addTaskメソッドが呼ばれます。指定した文章を追加した後に、updateTaskメソッドをevent引数をnullで呼び、イベントチェックの部分をパスさせます。そうして、名前と予定の表示に戻す処理を行うことで、最初のUIと同じ動きになりました。Chromeのdevice toolbarでiPhoneのエミュレートなどに切り替えても問題無く動作しました。しかし、そう甘くは無かったのです!

実機だと動かない

実現しようとしているプラットフォームは、Viewやスクリプトでカスタマイズできるようになっていて、モバイル端末用のアプリやMobile Safariなんかでも動作します。既述のようにエミュレータでも動作したので、実機で確認してみました。が、ダメでしたorz... event.relatedTargetが null のままのようです。stack overflow には以下のような記事が。

Javascript: on blur getting the event's relatedTarget on mobile safari

記事にあったリンク先に、mousedownで予めフラグを立てといてblur時にごにょごにょする、というようなアイディアがあったので、touchstartでごにょごにょすることにしました。まず、以下のようにイベントハンドラを追加します。

code4.js
<v-btn small round class="quick-task" color="primary" text-color="white" @touchstart="touch" @click="addTask(user,'打合せ')">打合せ</v-btn>

touchメソッドではevent.targetをチェックしてフラグをtrueにします。が、ハンドラを記述した要素からしか呼ばれないでしょうから、要素のチェック処理は要らないかもしれません。updateTaskメソッドの方は、this.touch = true の場合もblurイベントを止めるようにします。

code5.js
  touch: function(event) {
    if (event && event.target && $(event.target).parent().hasClass('quick-task')) {
      this.touch = true;
    }
  },
  updateTask: function(event, user) {
    if (this.touch || event && event.relatedTarget && $(event.relatedTarget).hasClass('quick-task')) {
      this.touch = false;
      event.stopPropagation();
      return;
    }
    this.touch = false;
    user.editTask = false;
    // ここでDBを更新する処理など...
  },

最後に

この実装で、実機はもちろんですが、Chromeのエミュレータ環境でも動作するようになりました。ちょっとworkaroundな気もしますが、アイディアの一つとしてシェアしておきます。Vue.jsやVuetifyは本質的には関係無いと思いますが、あくまで題材ということでご容赦ください。また、潜在的な課題として、PC環境でTABでフォーカスを移すとちょうど該当のボタンに当たってしまい、フォーカス処理が働かない(クリックも飛ばない)というのがありますが、そちらはまたなんとかしようと思います。なにぶんフロント初心者ですので、それはやっちゃダメ、こういうやり方もあるよ、というご意見が助けになりますので、ぜひお待ちしております。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PlayCanvasのhtmlファイルなどをローカルのテキストエディタからuploadする [REST API]

注意:このREST APIを使用するには有料のORGANIZATIONプランに加入する必要があります(2019/06/07現在)

PlayCanvas

GLSLやらwebGLやらをよくみるようになってから勉強するようになったんですが、
如何せん理解するのが難しいこの頃です。

もっと簡単にwebGLとか触れないかなと探して見つけたのが、PlayCanvasでした。
Unityみたいなエディター画面ですが、この画面からwebGLのコンテンツを作れるようで。すごいなー…

Flashが完全廃止され、HTML5ゲームとかが出てきている最中、こんなツール?が出てきたら開発の時間のコストも下がりそうですね。
そんな私はゲームは作ったことがないですが…これを機に作ってみたいですね。

ちなみにPlayCanvasはクラウドのサービスで、その中にコードエディターもあるのですが…
今まで使っていたコードエディターと比べるとsnippetが入っていないだけで結構打ちづらくなりまして…なんとか出来ないかなあ。

そこで、REST APIを使ってローカルのエディターからPlayCanvasのクラウド上になんとかuplaod出来ないかなと。

gulp-playcanvasなるREST APIの必要な情報を入れるだけでuploadが出来ちゃうものがありました。ステキ。
早速中身を見てみましょう!!

gulp-playcanvas

読んでみる

https://github.com/yushimatenjin/gulp-playcanvas

これを読んでみる。
(上記githubのreadmeから以下のコードを引用 ※2019/06/07現在)

npmでインストールして

npm i -D gulp-playcanvas

config.jsに必要な情報を入れて

//config.js
module.exports = {
  accessToken: "accessToken",
  scenes: [scene],
  projectId: projectId,
  branchId: "branchId", 
  projectName: "projectName", 
  remotePath: "remotePath" //PlayCanvasエディター上で配置したフォルダ (例, dev, web...)
};

gulpfile.jsに設置…

//gulpfile.js
const gulp = require("gulp");
const playcanvas = require("gulp-playcanvas");
const pcOptions = require("./config");
const pug = require("gulp-pug");
const sass = require("gulp-sass");

gulp.task("pug", () => {
  return gulp
    .src(["src/**/*.pug", "!src/**/_*.pug"])
    .pipe(pug())
    .pipe(gulp.dest("dist/"))
    .pipe(playcanvas(pcOptions));
});

gulp.task("js", () => {
  return gulp
    .src(["src/**/*.js", "!src/**/_*.js"])
    .pipe(gulp.dest("dist/"))
    .pipe(playcanvas(pcOptions));
});

gulp.task("sass", () => {
  return gulp
    .src("src/**/*.+(scss|sass)")
    .pipe(sass())
    .pipe(gulp.dest("dist/"))
    .pipe(playcanvas(pcOptions));
});

gulp.task("watch", function() {
  gulp.watch(["src/**/*.pug", "!src/**/_*.pug"], gulp.task("pug"));
  gulp.watch(["src/**/*.js", "!src/**/_*.js"], gulp.task("js"));
  gulp.watch("src/**/*.+(scss|sass)", gulp.task("sass"));
});
gulp.task("default", gulp.parallel("watch"));

pugとsassとjsを更新するたびにplaycanvas(pcOptions)でuploadするようですね。

早速自分のgulp環境でもやってみよう!


自分のgulpfile.js

ちょっと突貫で作ったので結構汚いのですが許して…

const gulp = require("gulp");
const pug = require("gulp-pug");
const sass = require("gulp-sass");
const postcss = require("gulp-postcss");
const autoprefixer = require("autoprefixer");
const cache = require("gulp-cache");
const terser = require("gulp-terser");
const rename = require("gulp-rename");
const notify = require("gulp-notify");
const changed = require("gulp-changed");
const plumber = require("gulp-plumber");

const browserSync = require("browser-sync");
const runSequence = require("run-sequence");

const playcanvas = require("gulp-playcanvas");
const pcOptions = require("./config");


const sassOptions = {
outputStyle: "expanded",
sourceMap: true,
sourceComments: false
};

const autoprefixerOptions = {
browsers: ["last 2 version", "ie >= 11", "Android >= 4.0"]
};

gulp.task('clear', function (done) {
  return cache.clearAll(done);
});

gulp.task("pug", () => {
  return gulp
    .src(["src/pug/**/*.pug", "!src/pug/**/_*.pug"])
    .pipe(plumber({errorHandler: notify.onError('<%= error.message %>')}))
    .pipe(pug({
      pretty: true,
      locals: {
        playcanvas: false
      }
    }))
    .pipe(changed("dist", {extension: '.html'}))
    .pipe(gulp.dest("dist/"))
    .pipe(browserSync.stream())
});
gulp.task("pug2", () => {
  return gulp
    .src(["src/pug/**/*.pug", "!src/pug/**/_*.pug"])
    .pipe(plumber({errorHandler: notify.onError('<%= error.message %>')}))
    .pipe(pug({
      pretty: true,
      locals: {
        playcanvas: false
      }
    }))
    .pipe(gulp.dest("dist/"))
    .pipe(browserSync.stream())
});
gulp.task("pug_pc", () => {
  return gulp
    .src(["src/pug/**/*.pug", "!src/pug/**/_*.pug"])
    .pipe(plumber({errorHandler: notify.onError('<%= error.message %>')}))
    .pipe(pug({
      pretty: true,
      locals: {
        playcanvas: true
      }
    }))
    .pipe(changed("pc", {extension: '.html'}))
    .pipe(gulp.dest("pc/"))
    .pipe(playcanvas(pcOptions));
});
gulp.task("pug2_pc", () => {
  return gulp
    .src(["src/pug/**/*.pug", "!src/pug/**/_*.pug"])
    .pipe(plumber({errorHandler: notify.onError('<%= error.message %>')}))
    .pipe(pug({
      pretty: true,
      locals: {
        playcanvas: true
      }
    }))
    .pipe(gulp.dest("pc/"))
    .pipe(playcanvas(pcOptions));
});



gulp.task("js", () => {
  return gulp
    .src(["src/js/*.js", "!src/js/_*.js"])
    .pipe(plumber({errorHandler: notify.onError('<%= error.message %>')}))
    .pipe(terser())
    .pipe(rename({extname: ".min.js"}))
    .pipe(gulp.dest("dist/js"))
    .pipe(browserSync.stream())
    .pipe(gulp.dest("pc/"))
    .pipe(playcanvas(pcOptions));
});

gulp.task("sass", () => {
  return gulp
    .src("src/sass/*.+(scss|sass)")
    .pipe(plumber({errorHandler: notify.onError('<%= error.message %>')}))
    .pipe(sass(sassOptions))
    .pipe(postcss([autoprefixer({autoprefixerOptions})]))
    .pipe(gulp.dest("dist/css"))
    .pipe(browserSync.stream())
    .pipe(sass({outputStyle: "compressed"}))
    .pipe(gulp.dest("pc/"))
    .pipe(playcanvas(pcOptions));
});

gulp.task("browser-sync", () => {
  browserSync.init({
    online: true,
    ui: false,
        server: {
            baseDir: "./dist",
        },
    port: 1234
  });
});
//監視開始
gulp.task("watch", () => {
  gulp.watch(['src/pug/**/*.pug','!src/pug/**/_*.pug'], gulp.task("pug"));
  gulp.watch(['src/pug/**/_*.pug'], gulp.task("pug2"));
  gulp.watch("src/sass/**/*.+(scss|sass)", gulp.task("sass"));
  gulp.watch(["src/js/*.js","!src/js/*.min.js"],gulp.task("js"));

  gulp.watch(['src/pug/**/*.pug','!src/pug/**/_*.pug'], gulp.task("pug_pc"));
  gulp.watch(['src/pug/**/_*.pug'], gulp.task("pug2_pc"));
});

gulp.task('default', gulp.parallel('clear', 'watch', "browser-sync"));

多分綺麗な書き方ではないのかもしれないですが、自分の環境に入れられました!

変数

const playcanvas = require("gulp-playcanvas");
const pcOptions = require("./config");

requireでREST APIの情報のあるconfig.jsも一緒に持ってて、

コンパイル

gulp.task("pug_pc", () => {
  return gulp
    .src(["src/pug/**/*.pug", "!src/pug/**/_*.pug"])
    .pipe(plumber({errorHandler: notify.onError('<%= error.message %>')}))
    .pipe(pug({
      pretty: true,
      locals: {
        playcanvas: true
      }
    }))
    .pipe(changed("pc", {extension: '.html'}))
    .pipe(gulp.dest("pc/"))
    .pipe(playcanvas(pcOptions));

コンパイル後のデータをplaycanvas(pcOptions))でREST APIを叩きuploadします。

ディレクトリを区分

ローカルではbrowser-syncで確認したいファイルとuploadするファイルとで区別したかったのでgulp.taskを増やしています。
(もっと綺麗な書き方があったら教えてください…)

/*-- ローカルで確認するディレクトリ --*/
.pipe(gulp.dest("dist/"))
/*-- PlayCanvasへuploadするディレクトリ --*/
.pipe(gulp.dest("pc/"))

htmlのheadタグなどPlayCanvasでは不要な要素

PlayCanvasでhtmlファイルを使い時に、<html>とか<head>とかいらないタグが出てきまして。
これを捨てるためにgulpからpugに変数を送っています。

    .pipe(pug({
      locals: {
        playcanvas: true
      }
    }))

playcanvasという変数でtrue/falseを取り、pugファイル内でifを使って区分けしていきます。

- if(!playcanvas) {
    doctype html
    html(lang='ja')
- }
    - if(!playcanvas) {
        head(prefix='og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# article: http://ogp.me/ns/article#')
            meta(charset='utf-8')
            title #{_title_str}
            meta(http-equiv='X-UA-Compatible', content='IE=edge')
            meta(name='description', content='_desc_str')
            meta(name='keywords', content='_keys_str')
            meta(name='viewport', content='width=device-width,user-scalable=yes')
            link(rel='stylesheet', href=DIR_CSS + 'master.css')
            block link
    - }
    - if(!playcanvas) {
        body
    - }
        .wrapper
            main.main
                .container
                    block body
    - if(!playcanvas) {
        //- 全体共通のjs
        script(src= DIR_JS + 'libs.min.js')
        script(src= DIR_JS + 'functions.min.js')
        block script
    - }

動いた!

そんなこんなで動きました!
更新したら以下の画像のファイルたちがplaycanvasのアセットとしてuploadされていました〜
スクリーンショット 2019-06-07 16.02.41.png
スクリーンショット 2019-06-07 16.04.00.png

更新するたびにREST APIが叩かれるので連打することがなければ問題なく使えそうです。
ただ、PlayCanvasにはディレクトリを作成するREST APIが無いようで…
そのため、今回uploadするためのディレクトリ/pcにコンパイルされるファイルたちは階層を保持しないようにする必要があります。
また、uploadする先のPlayCanvasのディレクトリも階層を持たないroot直下のディレクトリに限られるそうです。

このREST APIを使ったやり方もパワープレイではありますが、おかげで開発が前より捗るようになりました。
もっと綺麗な書き方ややり方があるとは思いますが、とりあえずはこれ…

今回のこのプロジェクトファイルと使用したい人がいたらgithubにあげているので確認してみてください。
このまま使用しても多分sassやらpugやら消し忘れているコードがあったりしてエラーが出るので、自分なりに書き換えて使用するよう注意してください。
https://github.com/sutobu000/playcanvas-codingSet


参考・引用

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Magic Leap MagicScript Typings settings.

Prepare
magic-script-cli v2.2.0
https://www.npmjs.com/package/magic-script-cli

magic-script-typings v1.4.0
https://www.npmjs.com/package/magic-script-typings

Create Project

magic-script init
? What is the name of your application? my typ
? What is the app ID of your application? org.magicscript.typ
? In which folder do you want to save this project? my-typ
? Do you want a Landscape App? Yes
cd my-typ

Before
Capture_000001.JPG

Install

npm i magic-script-typings

After
Capture_000002.JPG

Reference

magic-script-typings (Github)
https://github.com/magic-script/magic-script-typings

magicscript
https://www.magicscript.org/

Thanks!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jQueryに頼らない! DOM操作を伴うブックマークレットの作り方

概要

  • jQuery使わないよ
  • 基本的なDOM情報の取得・操作はJavaScriptのquerySelector querySelectorAllを使うよ
  • ネイティブのメソッド(querySelectorなど)を別の変数に入れて使おうとしたら怒られたよ
  • querySelectorで取得できるNodeListは配列ではないので、配列のメソッドで処理する時は配列化しておくと便利だよ

ブックマークレットとは

  1. ブックマークに追加
  2. 適当な名前をつけ、URLとして「javascript:」から始まるコードを保存。
  3. 任意のページで実行すると、そのスクリプトが実行できる。

ES2015

自分が使えればいいので、ブラウザ対応は考えなくて良い。
思う存分最新のJS文法で書きましょう。

(function() {
  "use strict";
  r();
  // 実行本体
  function r() {
    // ここに色々書いていく
  }
})();

取得→配列化

配列のメソッドを使って抽出・操作したいので、任意のDOMを取得したら即配列化します。
domArrは配列なので、.map().filter().reduce() など自由に使えます。
※callメソッドを使えばNodeListのまま関数適用は可能ですが、繰り返しメソッドを適用する場合には面倒です。

// 取得
const $dom = document.querySelectorAll('.className');

// 配列化
const domArr = [].slice.call($dom);

// スプレッド演算子を使っても良い
const domArr = [...$dom];

抽出

クラスで大まかに絞った後、「要素内のタイトルが「にゃーん」の要素だけに絞りたい」なんていうことをしないといけなかったりします。
配列のfilter、findメソッドの出番です。メソッド内で、任意の要素を取得し、そのtextContentを文字列比較して true を返すようにすれば抽出できます。
そもそも要素自体が取得できないケースもあるため、その場合はfalseを返すようにします。

filter

複数の要素を抽出する場合に使います。

const $target = domArr.filter(el => {
  const $title = el.querySelector('.title');
  if(!$title) { // そもそも要素が取得できないケースもあるので falseを返すようにする
    return false;
  }
  return $title.textContent === 'にゃーん';
});

// $targetは配列なので、抽出した1つの要素を操作する場合は $target[0] でアクセスする 

find

最初に見つかった1つの要素のみで良い場合は、filterよりもfindの方が向いています。
挙動・返り値が変わるだけで、条件の書き方はfilterと同じです。
つまりリファクタリングしやすい、ということ。

const $target = domArr.find(el => {
  const $title = el.querySelector('.title');
  if(!$title) { // そもそも要素が取得できないケースもあるので falseを返すようにする
    return false;
  }
  return $title.textContent === 'にゃーん';
});

// $targetはDOM要素

合計する

今回はターゲットとなるカラムのアイテム1つ1つに含まれるポイント数を合計したかったので、
さくっとポイントの文字列を抽出して合計します。

// ポイントが含まれている要素を抽出
const $pointDom = $target.querySelectorAll('.point');

// 配列化
const pointDomArr = [...$pointDom];

// ポイント文字列を数値に変換しつつ抽出 → さらにundefinedなど、合計すると NaN になる要素を除外
// [1, 3, 5...] のような配列が生成される
const pointArr = pointDomArr.map(v => v.textContent * 1).filter(v => v);

// 合計
const points = pointArr.reduce((a, b) => a + b, 0);

セミコロンを忘れないこと

minifyして1行にするため、各処理の最後にセミコロンがないと意味が変わってエラーが生じてしまいます。省略しないこと。

minify

素晴らしいWebサービスが存在するので使わせていただきましょう。
エディタ上で出来るけど? という方はそれで構いません。
下記のサービスの場合、ブックマークレット化に必要な「javascript:」も補ってくれます。

備考

怒られました。

// TypeError: Illegal invocation
const qAll = document.querySelectorAll;

// これならOK
const qAll = function(selector){
  return document.querySelectorAll(selector);
};

作ったもの

今回はAsanaの「DONE」とタイトルの付いたリストのポイント(スクリーンショット「2」の部分)を合計するためのブックマークレットを作りました。
日頃のタスクをちょっと便利にするブクマ、気軽に作っていきましょう〜。

スクリーンショット 2019-06-07 午後3.19.44.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ω分で作る無料のジオコーディングAPI with Google Apps Script

小ネタです。

3 分で作る無料の翻訳 API with Google Apps Scriptという記事をみて、

Google翻訳のAPIが作れるのなら、ジオコーディングのAPIも作れるのでは?
と思って適当にちょこっと改変してみたらできました。どうぞ。

function doGet(e){
  var p = e.parameter;
  var response = Maps.newGeocoder().geocode(p.location);
  var result = response.results[0];
  return ContentService.createTextOutput(result.geometry.location.lat + " " + result.geometry.location.lng);
}

参考文献

3 分で作る無料の翻訳 API with Google Apps Script

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2019年版: JavaScriptのループの考察



このツイートを起点に、パフォーマンスの話が出て、紀平さんも計測されていたんですが自分でも思うところがあって計測して考察してみました。

実測前の僕の予想(というか過去の経験)は

  • 普通のforが最速
  • for-inは速度以前に使ってはいけない
  • for-ofとforEachは関数呼び出しがループごとに挟まるのでどちらも遅いが同じ水準
  • for ofは言語標準なので最適化が行われる期待!

でした。さて、結果はいかに?

それぞれのループの解説

伝統的なfor

伝統的なループが一番軽いというのはみんなが認めるところです。というのは、ループごとに新たなスコープ(名前空間)が作られることがないからですね。

他の手法は何かしらの関数呼び出しが入ります。関数を実行するということはスタックを操作し、名前空間ができ、変数の巻き上げが行なわれたりもろもろ発生します。

そういうペナルティがないところが強みです。一方、ループ変数が必要だったり書き方がダルい(個人の感想です)とか、ループ変数を書き間違って終わらないループを作ってデバッグに苦労したことが過去にあってイヤだ(個人の感想です)とか、気分的に敬遠されがちです。

for-in

そもそも、配列の要素を舐めるための機能ではないので、使うべきではないというのは置いといて、これは配列オブジェクトのプロトタイプまで探しに行っちゃったりと余計なことまでするあたりが速度的に期待できないポイントですね。

まあ、かつては辞書型のように使えるデータ構造がオブジェクトしかなく、それの列挙はこれしかなかったので仕方ない点はありますが今どきは選択の俎上に挙げる必要もないでしょう。

forEach

AirBnBのコーディング規約ではこれを推奨しており、eslintの設定済みのconfigが手に入るということでそれなりに人気のある選択肢ですね。JSで関数型にかぶれたスカした人が支持しているイメージ(個人の感想です)がありますが、実装上は毎回関数が実行されるので、パフォーマンス上は期待できない方法です。

for-of

ESの規格にあるイテレータプロトコルを使ったループです。出自とか実装はPythonっぽい感じがします。

このfor ofをサポートするオブジェクトは、array[Symbol.iterator]()という感じでメソッドを呼び出すと、イテレータオブジェクトが帰ってきます。これにはnext()というメソッドがあります。ループごとにこれを呼ぶと、イテレータリザルトオブジェクトが帰ってきます。valueで要素が、doneにループが終了したかどうかのフラグが入っています。

まあ、関数呼び出しはあるし、新しいオブジェクトは発生するしで速度的には遅そうに思うかもしれませんが、このあたりの処理は処理系内部でネイティブコードで処理されるはずなので、最適化が進めば遅さは解消されることが期待されます。

Babel/TypeScript

今どきは、ES2018とかの最新機能を使っても、ES5/2015相当に変換して書き出すというのが一般的なウェブのJSの実態です。

Babelのサイトで変換してみたfor ofのコードの結果がこれです。イテレータプロトコルなのに、配列だと分かっているからか、配列専用コードになっていますね。TypeScriptの方も同じ結果になります。

for (var _i = 0, _array = array; _i < _array.length; _i++) {
  var value = _array[_i];
  sum += value;
}

おいおい、そんなはずはないだろ、前に見たお前はもっと素直なコードにしていただろ?ということで即時実行関数で型をごまかしてみました。

(function(array) {
let sum = 0;
for (const value of array) {
    sum += value;
}
})(array)

その結果がこれです。仕様通りの素直なコードが出てきました。なお、TypeScriptはここまでやっても最適化されたコードが出てきたので、静的型付け言語は最適化に強いですね。

(function (array) {
  var sum = 0;
  var _iteratorNormalCompletion = true;
  var _didIteratorError = false;
  var _iteratorError = undefined;

  try {
    for (var _iterator = array[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
      var value = _step.value;
      sum += value;
    }
  } catch (err) {
    _didIteratorError = true;
    _iteratorError = err;
  } finally {
    try {
      if (!_iteratorNormalCompletion && _iterator.return != null) {
        _iterator.return();
      }
    } finally {
      if (_didIteratorError) {
        throw _iteratorError;
      }
    }
  }
})(array);

結果

計測コードは以下のところにあります。

実際の結果を各ブラウザとかごとに集計したのがこれです。伝統的なforを100%とした相対値にしています。forEachは、通常の関数、1行のアロー関数、複数行のアロー関数(returnが自動でされないので少し早いかもと期待)の3通りやってみました。macOS版EdgeのDeveloper Preview版もやってみましたが、Chromeとほぼ同じだったので割愛します。

for for-in forEach(1) forEach(2) forEach(3) for-of for-of (Babel/TS) for-of (Babel)
Chrome x86 100% 0.6% 7.0% 7.0% 6.8% 64.7% 113% 27.5%
Chrome ARM 100% 1.1% 8.6% 8.8% 8.9% 9.6% 99.5% 9.0%
Safari x86 100% 0.2% 10.7% 10.7% 10.9% 1.4% 120.8% 9.2%
Safari ARM 100% 0.3% 15.2% 15.2% 15.3% 2.0% 54.5% 11.8%
Firefox 100% 0.2% 7.5% 7.2% 7.4% 9.5% 105.6% 2.5%

for inが圧倒的に遅いですね。for ofはPC版のChromeはなかなか良い結果が出ています。しかし、スマホ(Huawei P20 Lite)とFirefoxではforEachとだいたい同じ(気持ち早い)になりました。超予想外だったのがSafariで、for of惨敗です。

最適化後のBabelおよびTypeScriptの変換結果は伝統的なforと遜色ない、場合によっては何故か早い結果となりました。

考察

ChromeはARM版の速度がforEachと同等だったので、本来のV8のエンジンの処理型の速度はこの水準だったのではと思われます。PC版Chromeが速いのはアーキテクチャ固有のJIT実装が効いたんじゃないかと思います。

for ofのSafariの結果はショッキングですね。これはコントリビュートのチャンスでは?というぐらい。

そして一番多くの人をがっかりさせるかもなのがBabel/TypeScriptの結果ですね。EdgeがIEを吸収してくれるのでPCブラウザがみんなモダンになるし、Google botのバージョンアップも発表されて、Babelとか捨てられる(or 出力ターゲットバージョンを上げられる)じゃんと期待で夢を膨らませていた人は多いと思いますが、まだまだ最適化が進んでなくて、枯れた最適化されたコードへの変換が有効な場面があるということです。まあ、ループだけじゃないので、そこまで大げさに考える必要もないかもですが。

今後もブラウザの最適化には期待しています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Google MAP】名称から場所を検索・特定する

Google Map API v3 名称から場所を検索・特定する

はじめに

  • Googleに限らずMapを扱うときには、まずは、その場所の緯度経度の情報が必要というのが基本ですが、やりたいことによっては緯度経度の情報がわからない場合もあります。
  • そんなときはGeocodingを使用します。(Geocoding:名称・地名・住所などの情報を、緯度経度の座標値に変換してくれる)

やりたいこと

  • 名称を検索ボックスに入れ、検索ボタンを押下すると、該当の場所にピンを立てる。
  • ピンに吹き出しをつけ、名称(google検索の結果へのリンクつき)、緯度経度、住所、画像検索へのリンクをいれる。

前提

  • Google Map API keyの取得

イメージ

 デモサイト

googleMap1.png

コード

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>Placeサーチ</title>
    <style>
      #header {
          background-color: darkblue;
          padding: 3px;
          width: 1195px;
          font-family: Meriyo UI;
          font-size: 14px;
          color: white;
      }
      #target {
        width: 1200px;
        height: 700px;
      }
    </style>
  </head>

  <body>
    <div id="header"><b>Google Maps - 場所検索</b></div>
    <div>施設名称検索 (例:マチュピチュ、万里の長城)</div>
    <input type="text" id="keyword"><button id="search">検索実行</button>
    <button id="clear">結果クリア</button>
    <div id="target"></div>

    <script src="https://maps.googleapis.com/maps/api/js?language=ja&region=JP&key=キーをセット&callback=initMap" async defer></script>

    <script>

      var map;
      var marker;
      var infoWindow;

      function initMap() {

        //マップ初期表示の位置設定
        var target = document.getElementById('target');
        var centerp = {lat: 37.67229496806523, lng: 137.88838989062504};

        //マップ表示
        map = new google.maps.Map(target, {
          center: centerp,
          zoom: 2,
        });

        // 検索実行ボタンが押下されたとき
        document.getElementById('search').addEventListener('click', function() {

          var place = document.getElementById('keyword').value;
          var geocoder = new google.maps.Geocoder();      // geocoderのコンストラクタ

          geocoder.geocode({
            address: place
          }, function(results, status) {
            if (status == google.maps.GeocoderStatus.OK) {

              var bounds = new google.maps.LatLngBounds();

              for (var i in results) {
                if (results[0].geometry) {
                  // 緯度経度を取得
                  var latlng = results[0].geometry.location;
                  // 住所を取得
                  var address = results[0].formatted_address;
                  // 検索結果地が含まれるように範囲を拡大
                  bounds.extend(latlng);
                  // マーカーのセット
                  setMarker(latlng);
                  // マーカーへの吹き出しの追加
                  setInfoW(place, latlng, address);
                  // マーカーにクリックイベントを追加
                  markerEvent();
                }
              }
            } else if (status == google.maps.GeocoderStatus.ZERO_RESULTS) {
              alert("見つかりません");
            } else {
              console.log(status);
              alert("エラー発生");
            }
          });

        });

        // 結果クリアーボタン押下時
        document.getElementById('clear').addEventListener('click', function() {
          deleteMakers();
        });

      }

      // マーカーのセットを実施する
      function setMarker(setplace) {
        // 既にあるマーカーを削除
        deleteMakers();

        var iconUrl = 'http://maps.google.com/mapfiles/ms/icons/blue-dot.png';
          marker = new google.maps.Marker({
            position: setplace,
            map: map,
            icon: iconUrl
          });
        }

        //マーカーを削除する
        function deleteMakers() {
          if(marker != null){
            marker.setMap(null);
          }
          marker = null;
        }

        // マーカーへの吹き出しの追加
        function setInfoW(place, latlng, address) {
          infoWindow = new google.maps.InfoWindow({
          content: "<a href='http://www.google.com/search?q=" + place + "' target='_blank'>" + place + "</a><br><br>" + latlng + "<br><br>" + address + "<br><br><a href='http://www.google.com/search?q=" + place + "&tbm=isch' target='_blank'>画像検索 by google</a>"
        });
      }

      // クリックイベント
      function markerEvent() {
        marker.addListener('click', function() {
          infoWindow.open(map, marker);
        });
      }

    </script>

  </body>
</html>

まとめ

  • Google Mapで「名称」はわかるけど、場所がわからない or なんとなくしかわならない場合に活用できるかと思います。
  • これを使用して、世界絶景ランキングの場所を自動でポイントすることを実施しました。(リンク:【個人開発】ランキング自動生成サイト(Python/Django)

参考URL

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

kintoneにてログインユーザーの氏名を取得する

kintoneで入力フォームを表示した際にログインユーザー名をフォームに入れる

kintoneで入力フォームを作成し、新規レコード登録画面を開いた際にログインユーザー名を取得、フォームに表示させる
スクリプト。
※氏名の入力フォームのフィールドコードは「氏名」。

inputForm.js
   // レコード新規作成時
   kintone.events.on(["app.record.create.show"], function(event){

      // ログインユーザ名を取得
      var record = event.record;
      var loginUser = kintone.getLoginUser()['name'];

      record['氏名']['value'] = loginUser;

      return event;

   });

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MoOx/pjaxでブラウザーキャッシュ対策のクエリー文字列が付かないようにする方法

内容

MoOx/pjaxでは、cacheBustオプションがtrue(デフォルト)になっていると、ページ遷移時にブラウザーキャッシュ対策のクエリー文字列が追加されます。
このクエリー文字列だけを自動で削除するコードです。

コード

JavaScript
const pjaxInstance = new Pjax({
  // 省略
});

pjaxInstance.latestChance = function(href) {
  Pjax.prototype.latestChance.call(
    this,
    this.options.cacheBust ? removeTimestampQuery(href) : href,
  );
};

pjaxInstance.afterAllSwitches = function() {
  if (this.options.cacheBust) {
    this.state.href = removeTimestampQuery(this.state.href);
  }
  Pjax.prototype.afterAllSwitches.call(this);
};

function removeTimestampQuery(href) {
  const anchor = document.createElement('a');
  let searchString;
  let parameters;

  anchor.setAttribute('href', href);
  searchString = anchor.search;
  if (searchString.length > 0) {
    parameters = searchString.slice(1).split('&');
    parameters = parameters.filter(function(element) {
      return element.indexOf('t=') !== 0;
    });
    anchor.search = parameters.length > 0 ? '?' + parameters.join('&') : '';
    href = anchor.href.replace(/\?$/, ''); // for IE
  }
  return href;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】バッテリーホイミ機能を実装しました

個人開発のWebアプリまちかどルート」v6.1に、名づけて《バッテリーホイミ機能》なるものを実装したときのメモです。

TL; DR

まちかどルートは

「道ばたのゴミを拾おう」
「お年寄りに席をゆずろう」
「〇〇に行って〇〇しよう」 など。

みんなの日常からちょっとした一日一善やおつかいなどをお題にしてサブクエストを作りクリアしあうことで世界がちょっと明るくなる、そんなリアルRPG系Webアプリです。

そしてユーザーには、レベルや経験値のほかHP (ヒットポイント:ドラクエのアレ)という属性値を持たせています。

Battery Status API

どうやらPCとAndroidのChrome限定のようですが、バッテリーの状態変化でJavaScriptを発火させることのできるBattery Status APIというものがあります。

今回はそれを応用してみました。

こんな流れです。
1. まちかどルートを開く
2. 充電を始める
3. APIが充電を検知したことを画面に表示
4. バッテリー残量値を取得
5. 「ホイミ!」ボタンを押す
6. バッテリー残量値をHPに加算してDBに保存

view / JavaScript

index.html.erb
<div id="posts">
 # ここに下記のJavaScriptからDOM要素が挿入されます
</div>
application.js
navigator.getBattery().then(function(battery) {
  // 充電を検知したら発火 
  battery.onchargingchange = function(){
    // 充電開始(true)のときだけ処理
    if (battery.charging === true) {
    // viewのid="posts"のところにDOM要素を挿入
    var element = document.getElementById("posts");
    element.insertAdjacentHTML("afterbegin", "
       <p>充電を検知</p>
       <form action='battery' accept-charset='UTF-8' method='get'>
       <input name='utf8' type='hidden' value='✓'>
          <input type='text' id='battery' name='battery' readonly=''>
          </input>
          <button name='button' id='battery_btn' type='submit' disabled>
            ホイミ!
          </button>
       </form>
     ");
    }
  }
  // 充電の残量が変化したら発火
  battery.onlevelchange = function(){
    // 充電の残量値を入力フォームに表示
    document.getElementById( "battery" ).value = battery.level * 100;
    // 入力フォームのdisabledを無効にして「ホイミ!」ボタンを押せるようにする
    document.getElementById( "battery_btn" ).disabled = "";
  }
 });

controllerでホイミ!

posts_controller.rb
    def battery
        # viewから送られてきたバッテリー残量値をHPに加算してDB保存
        if params[:battery] != nil
            battery = params[:battery]
            @current_user.hp += battery.to_i
            @current_user.save
            flash[:notice] = "HPが回復しました!"
            redirect_to root_path
        else
            flash[:error] = "呪文が失敗しました!"
            redirect_to root_path
        end
    end

ルーティング

config\routes.rb
  get 'battery' , as: 'battery', to: 'posts#battery'

あとがき

とてもザックリな感じになってしまいましたが、要点は上記のとおりです。HPが残り少なくなったらバッテリーを充電して「ホイミ!」

けっこう楽しいです。

オープンソース開発用のリポジトリを公開しました。今回の件、こちらにもアップしてあります。
https://github.com/west2538/machiroute_oss

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「ボタンを押してコンテンツを切り替えるシンプルなやり方」を React + TypeScirpt にしてみる

何なのこれは?

@ngron さんの

を React、しかも TypeScript で 馬鹿げた 冗長なコードにしてみる実験。

デモ

See the Pen jojBZm by Kenta Konno (@sprout2000) on CodePen.

コードの中身

index.html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
</head>

<body>
  <div id="root"></div>

</body>

</html>
index.tsx
import React from 'react';
import ReactDOM from 'react-dom';

import App from './App';
import './index.css';

ReactDOM.render(<App />, document.getElementById('root'));
App.tsx
import React from 'react';

import './App.css';

interface State {
  status: boolean;
}

interface Props {
  status: boolean;
}

const DoneContent = (props: Props): JSX.Element => {
  if (!props.status) {
    return <div style={{ display: 'none' }} />;
  } else {
    return (
      <div>
        <li>完了 1</li>
        <li>完了 2</li>
        <li>完了 3</li>
      </div>
    );
  }
};

const PendingContent = (props: Props): JSX.Element => {
  if (props.status) {
    return <div style={{ display: 'none' }} />;
  } else {
    return (
      <div>
        <li>未完了 1</li>
        <li>未完了 2</li>
        <li>未完了 3</li>
      </div>
    );
  }
};

class App extends React.Component {
  public state: State = {
    status: true,
  };

  public handleOnToggle = (): void => {
    this.setState({ status: !this.state.status });
  };

  public render(): JSX.Element {
    return (
      <div>
        <button
          onClick={this.handleOnToggle}
          style={{ color: this.state.status ? 'blue' : 'black' }}>
          完了
        </button>
        <button
          onClick={this.handleOnToggle}
          style={{ color: this.state.status ? 'black' : 'blue' }}>
          未完了
        </button>
        <DoneContent status={this.state.status} />
        <PendingContent status={this.state.status} />
      </div>
    );
  }
}
index.css
html {
  margin: 0;
  padding: 0;
}

body {
  margin: 0;
  padding: 0;
  background-color: #ffffff;
}

#root {
  margin: 0;
  padding: 0;
}
App.css
button {
  margin: 10px;
  padding: 10px 20px;
  font-size: 20px;
  border-radius: 20px;
}

li {
  margin: 10px;
  font-size: 23px;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

marked.js本体を弄って、オレオレ変換ルールを追加する(今回はtextileのテーブル風)

21時就寝、3時起床がデフォになりつつある、majirouです。

  • marked.jsの字句解析器(lexer)と構文解析器(parser)を弄って、独自の記述方法を実装します。
  • ザッカーバーグの「たぶん動くと思うからリリースしようぜ!」的精神で、結果が出ることを優先にしています。
  • ソースコード中の...は前略、中略、以下略の意です。
  • 実装内容は以下。
    • textileのテーブル表記で<table>タグが生成される。
    • rowspan,colspanに対応させる。
    • <th>を縦のカラムに対応させる。
|_. ABC|DEF|
|/2 GHI|JKL|
|\3 MNO|PQR|

と書くと、以下のように変換させたい。

<table>
 <tr>
   <th>ABC</th>
   <td>DEF</td>
 </tr>
 <tr>
   <td rowspan="2">GHI</td>
   <td>JKL</td>
 </tr>
 <tr>
   <td colspan="3">MNO</td>
   <td>PQR</td>
 </tr>
</table>

カスタマイズ結果

いつもの通り、まず結果から。

  • marked.jsに追記、改造した箇所の差分です
  • 5箇所ほどなので、各変更箇所にそれぞれ(1),...(5)として、記事内の参照印とします。

カスタマイズしたmarked.jsの差分
// (1)
var block = {
  ...
- text: /^[^\n]+/
+ text: /^[^\n]+/,
+ textileTable: noop
}

...

// (2)
block.tables = merge({}, block.gfm, {
  ...
  nptable: /^ *([^|\n ].*\|.*)\n *([-:]+ *\|[-| :]*)(?:\n((?:.*[^>\n ].*(?:\n|$))*)\n*|$)/,
- table: /^ *\|(.+)\n *\|?( *[-:]+[-| :]*)(?:\n((?: *[^>\n ].*(?:\n|$))*)\n*|$)/
+ table: /^ *\|(.+)\n *\|?( *[-:]+[-| :]*)(?:\n((?: *[^>\n ].*(?:\n|$))*)\n*|$)/,
+ textileTable: /^\|(((((_\.|\/[0-9]+|\\[0-9]+)?\s+).*|.+)\|)+\n)+/
});

...

// (3)
Lexer.prototype.token = function(src, top) {
  ...
  while (src) {
    ...
+    if (cap = this.rules.textileTable.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'textileTable',
+        text: cap[0]
+      });
+      continue;
+    }

...

// (4)
Renderer.prototype.tablecell = function(content, flags) {
  var type = flags.header ? 'th' : 'td';
-  var tag = flags.align
-    ? '<' + type + ' align="' + flags.align + '">'
-    : '<' + type + '>';
-  return tag + content + '</' + type + '>\n';
+  // rowspan, colspanを追記するため
+  var attr = [];
+  if (flags.align != null) {
+    attr.push(` align="${flags.align}"`)
+  }
+  if (flags.rowspan != null) {
+    attr.push(` rowspan="${flags.rowspan}"`)
+  }
+  if (flags.colspan != null) {
+    attr.push(` colspan="${flags.colspan}"`)
+  }
+  return '<' + type + attr.join('') + '>'+ content + '</' + type + '>\n';
};

...

// (5)
Parser.prototype.tok = function() {
  switch (this.token.type) {
    ...
+   case 'textileTable': {
+     var body = '',
+         i,
+         ii,
+         rows = this.token.text.split('\n'),
+         cols,
+         tempCols = '',
+         max,
+         regexpTh = /^_\. +.*/ ,
+         regexpRowspan = /^\/[0-9]+ +.*/ ,
+         regexpColspan = /^\\[0-9]+ +.*/ ,
+         temp
+         ;
+     for (i = 0; i < rows.length; i++) {
+       cols = rows[i].split('|')
+       max = cols.length-1
+       for (ii = 1; ii < max; ii++) {
+         if (regexpTh.test(cols[ii])) {
+           tempCols += this.renderer.tablecell(
+             cols[ii].replace(/^_\. /,''),
+             { header: true }
+           )
+         } else if(regexpRowspan.exec(cols[ii])) {
+           temp = cols[ii].split(' ')
+           const rowspan = temp.shift().substr(1)
+           tempCols += this.renderer.tablecell(
+             temp.join(''),
+             { header: false , rowspan }
+           )
+         } else if(regexpColspan.exec(cols[ii])) {
+           temp = cols[ii].split(' ')
+           const colspan = temp.shift().substr(1)
+           tempCols += this.renderer.tablecell(
+             temp.join(''),
+             { header: false , colspan }
+           )
+         } else {
+           // 普通のtd
+           tempCols += this.renderer.tablecell(cols[ii], { header: false })
+         }
+       }
+       body += this.renderer.tablerow(tempCols)
+       tempCols = ''
+     }
+     return '<table>' + body + '</table>'
+   }

...

経緯

  • 記事管理をするために、Markdownで編集するウェブアプリを実装してほしい
  • でも、テーブルは、セル結合したいから『textile』風に書きたい

という要望を受けました。

marked.js customizeなどで検索すると、該当記事がちらほら出てきますが、marked.jsに用意されているルールをオーバーライドする方法が紹介されています。

公式 でも「独自ルール追加なら、オーバーライドで!」って書いてありますが、今回やりたいのはそれではありません。以下は公式のオーバーライドの例。

Example: Overriding default heading token by adding an embedded anchor tag like on GitHub.

// Create reference instance
var myMarked = require('marked');

// Get reference
var renderer = new myMarked.Renderer();

// Override function
renderer.heading = function (text, level) {
  var escapedText = text.toLowerCase().replace(/[^\w]+/g, '-');

  return `
          <h${level}>
            <a name="${escapedText}" class="anchor" href="#${escapedText}">
              <span class="header-link"></span>
            </a>
            ${text}
          </h${level}>`;
};

// Run marked
console.log(myMarked('# heading+', { renderer: renderer }));

カスタマイズ説明

まずは、ざっくり何をしているかを掴むため全体像を把握します。

  • メイン処理marked関数がメイン処理と思われます。
  • 解析処理: 大きく分けて、字句解析Lexerと構文解析Parserがあり、入力テキストを解析(分類)していきます。
  • レベルとルール: htmlなので、blockinlineに2レベルに分けられ、さらにその中に、「この構文なら、こういうHTMLを書き出す」といったルールを定義をし、処理を行うようです。
  • HTML変換Rendererにて、上記レベル配下の定義に沿って、変換していくようです。

marked関数

  • lexerで字句解析のルールになるトークンを取得。
  • それを元にparse処理をするdone関数を定義し、実行。
  • コードをハイライト化するためにいろいろ分岐していますが、今回は割愛。
  • ソースを見る限り、lexer,parserそれぞれを見様見真似でカスタマイズできそうとあたりをつける。
function marked(src, opt, callback) {
    ...
    try {
      tokens = Lexer.lex(src, opt);
    } catch (e) {
      return callback(e);
    }
    ...
    var done = function(err) {
      ...
      try {
        out = Parser.parse(tokens, opt);
      } catch (e) {
        err = e;
      }

      opt.highlight = highlight;

      return err
        ? callback(err)
        : callback(null, out);
    };
...

Lexer

  • Lexer関数にて、字句解析処理のLexer.prototype.tokenがあり、この中で正規表現を使って、入力されたテキストを解析しています。
    1. execで判別
    2. その長さ分のテキストを切り出し
    3. 最終的にtokens配列に追加
      この連想配列の要素は、MD→HTMLのレンダリング時に使用します。typeはその際の分岐で使うので必須
  • カスタマイズ箇所は(3)が該当し、textileのtable表記のテキストをtype: 'textileTable'として、tokensに追加します。
Lexer.prototype.token = function(src, top) {
  ...
    // heading
    if (cap = this.rules.heading.exec(src)) {
      src = src.substring(cap[0].length);
      this.tokens.push({
        type: 'heading',
        depth: cap[1].length,
        text: cap[2]
      });
      continue;
    }
    ...
    // (3)
    // textile table
    if (cap = this.rules.textileTable.exec(src)) {
      src = src.substring(cap[0].length);
      this.tokens.push({
        type: 'textileTable',
        text: cap[0]
      });
      continue;
    }

Lexer ルール

  • 字句解析のルールは以下のように、blockinlineが宣言されていて、これに独自のカスタマイズルールを追加して、字句解析時に引っ掛けます。
  • 今回は、tableを真似るので、blockレベルに対して追加します。
  • カスタマイズ箇所は(1)で、textileTableとし、table同様のnoopを指定しました。
    • noop自体はfunction noop() {}と定義されており、その名の通り何もしないことになります。
/**
 * Block-Level Grammar
 */
var block { 
  ...
  heading: /^ *(#{1,6}) *([^\n]+?) *(?:#+ *)?(?:\n+|$)/,
  ...
  table: noop,
  ...
  // (1)
  textileTable: noop
};

...

/**
 * Expose Block Rules
 */

Lexer.rules = block;

...

Parser

  • 構文解析を行い、それに対応した処理を行わせるようで、switch文で分岐しています。
  • カスタマイズ箇所は(5)で、case 'textileTable'時に、tableタブを作成するようにしています。
    • _.始まりなら<th>にする
    • /始まりならそのあとの数値分rowspanを指定する。
      など力技で処理しています。あとは、割愛。
  • (あとで気づいたこと) 本来、Renderサイドでやるタグ生成処理を、このパーサー部で力技として、やってました。
Parser.prototype.tok = function() {
  switch (this.token.type) {
    case 'space': {
      return '';
    }
    ...
    // (5)
    case 'textileTable': {
      ...
    }

Renderer

  • tablecellレンダーが、colspan,rowspanに対応していいので改造。
  • カスタマイズ箇所は(4)になります。
Renderer.prototype.tablecell = function(content, flags) {
  var type = flags.header ? 'th' : 'td';
  // var tag = flags.align
  //   ? '<' + type + ' align="' + flags.align + '">'
  //   : '<' + type + '>';
  // return tag + content + '</' + type + '>\n';

  var attr = [];
  if (flags.align != null) {
    attr.push(` align="${flags.align}"`)
  }
  if (flags.rowspan != null) {
    attr.push(` rowspan="${flags.rowspan}"`)
  }
  if (flags.colspan != null) {
    attr.push(` colspan="${flags.colspan}"`)
  }
  return '<' + type + attr.join('') + '>'+ content + '</' + type + '>\n';
};

感想

  • markdown楽なんですが、今回のようにテーブルを結合したいとか、ヘッダーを縦一列にしたいという場合、textileの方が融通が効くなぁ〜という印象。
  • 大学の授業で、コンパイラとか作った時以来で久しぶりにlexerという単語に出くわし、「(詳細は覚えてないけど)過去の勉強した」という経験は大事だなと再認識。
  • 正規表現さえあれば、力技で基本どうにかなる。
  • できなかったら、htmlを直書きすべし。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オブジェクトを生成し情報を保持する

オブジェクトの生成と情報の保持

todoリスト作成時に

新規タスクを追加すると情報が上書きされてしまう

という状態になり、大分悩まされましたので、ここに記述します。
「オブジェクトを使うと情報が保持される」、ということを書きたいので、コード短縮の為に細かい機能は端折ってあります。ご了承下さい。
こんな感じの、作業中/完了の切り替えボタンが付いているものです。
スクリーンショット 2019-06-07 1.29.42.png

HTML

まずHTMLから。ここは問題ありません。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Todo List</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <h1>ToDo List</h1>
    <table>
        <thead>
            <tr>
                <th>コメント</th>
                <th>状態</th>
            </tr>
        </thead>
        <tbody id="todo-list"> 
        </tbody>
    </table>
    <h2>新規タスクの追加</h2>
    <div>
        <input id="input-todo" type="text">
        <button id="add-btn">追加</button>
    </div>
    <script src="script.js"></script>
</body>
</html>

情報が上書きされてしまうjavascriptコード

const todos = [];

const todoList = document.getElementById('todo-list');
const addBtn = document.getElementById('add-btn');

// todoのコメント内容を取得し、配列todoに追加
addBtn.addEventListener('click', e => {
    const inputTodo = document.getElementById('input-todo').value;
    todos.push(inputTodo);
    showTodo();  //HTMLにtodoコメントと作業中ボタンを表示させる関数
});

const showTodo = () => {
    while (todoList.firstChild) {
        todoList.textContent = '';
    }
    todos.map( todo => {
        const tr = document.createElement('tr');
        const tdComment = document.createElement('td');
        const tdState = document.createElement('td');
        tdComment.textContent = `${todo}`; 

        // 作業中ボタンの生成
        const workBtn = document.createElement('button');
        workBtn.textContent = '作業中';

        workBtn.addEventListener('click', e => {
            if(workBtn.textContent === '作業中') {
                 workBtn.textContent = '完了';
            } else {
                 workBtn.textContent = '作業中';
            }
       })

       todoList.appendChild(tr);
       tr.appendChild(tdComment);
       tr.appendChild(tdState);
       tdState.appendChild(workBtn);
    })
};

上書きされてしまう原因

新規タスクを追加する度にworkBtn.textContent = '作業中';のコードが働いてしまう為、既存のタスクが完了でも'作業中'になる。

解決したjavascriptコード

const todos = [];

const todoList = document.getElementById('todo-list');
const addBtn = document.getElementById('add-btn');

addBtn.addEventListener('click', e => {
    const inputTodo = document.getElementById('input-todo').value;

    // オブジェクト生成前に記述する
    const workBtn = document.createElement('button');
    workBtn.textContent = '作業中';

// todoのコメント内容と作業中ボタンのオブジェクト生成
    const todo = new Object();
    todo.value = inputTodo;
    todo.state = workBtn;    
    workBtn.addEventListener('click', e => {
        if(workBtn.textContent === '作業中') {
             workBtn.textContent = '完了';
        } else {
             workBtn.textContent = '作業中';
        }
   })
    // オブジェクトtodoを配列todosに追加
    todos.push(todo);
    showTodo();
});

const showTodo = () => {
    while (todoList.firstChild) {
        todoList.textContent = '';
    }
    todos.map( todo => {
        const tr = document.createElement('tr');
        const tdComment = document.createElement('td');
        const tdState = document.createElement('td');

        tdComment.textContent = todo.value; 
        const stateBtn = todo.state;

        todoList.appendChild(tr);
        tr.appendChild(tdComment);
        tr.appendChild(tdState);
        tdState.appendChild(stateBtn);
    })
};

このコードでは、新規タスクを追加する度にオブジェクトを生成しており、そのオブジェクトを配列に追加しています。
作業中ボタンはオブジェクトに格納されているキーと値を参照している為、新規タスクを追加しても既存のタスクの情報が守られます。

以上です。
わかりやすく説明する為に余計なコードは極力省いたつもりですが、見辛さはご勘弁下さい。
補足や訂正などありましたら、ぜひご教授いただければ嬉しいです。
最後まで見ていただきありがとうございます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactでシンプルなDIを行う

Reactを使っているとき、Componentにレンダリング以外のロジックを混入させないために、他のモジュールに処理を切り出すということはよく行われると思います。
また、テストの時だけComponentの依存モジュールの実装を差し替えたい、というのもよくあることです。
ReactのuseContextフックを使えばこれらがシンプルに解決できそうだ、というのがこの記事の趣旨です。

そして自分の中でのユースケースに合わせ、ライブラリを作りました。
基本的にTypeScriptで利用することを想定しています。
@mozisan/diact
(とりあえずコードを書いてGitHub上にpushした程度なのでREADMEも書いていませんが…)

$ npm install @mozisan/diact

以下はこのライブラリを使ってDIを行う方法を紹介します。
(ライブラリのソースを見ればわかる通り、ライブラリでラッピングしている処理はとても単純なので、あえてライブラリを使わず直接useContextを使っても同じようなことはできます。)

DIコンテナを作る

import { createDIContainer } from '@mozisan/diact';

type Foo = {
  readonly doFoo: () => void;
};

type Bar = {
  readonly doBar: () => void;
};

type Deps = {
  readonly foo: Foo;
  readonly bar: Bar;
};

const { DepsProvider, useDeps } = createDIContainer<Deps>();

export { DepsProvider, useDeps };

createDIContainer()によってDepsProvideruseDepsを得ます。
DepsProviderは実際に依存モジュールを注入するComponentで、useDepsはその注入された依存モジュールを子Componentで利用するためのカスタムHookです。

依存モジュールを参照する

先ほど得たuseDepsを使います。

import React from 'react';
import { useDeps } from 'path/to/di-container';

export const App = () => {
  const { foo, bar } = useDeps();

  return (
    <div>
      <button onClick={foo.doFoo}>Do foo</button>
      <button onClick={bar.doBar}>Do bar</button>
    </div>
  );
};

依存モジュールを注入する

先ほど得たDepsProviderを使います。

import React from 'react';
import ReactDOM from 'react-dom';
import { DepsProvider } from 'path/to/di-container';
import { App } from 'path/to/app';

const foo = {
  doFoo: () => {
    console.log('foo');
  },
};

const bar = {
  doBar: () => {
    console.log('bar');
  },
};

ReactDOM.render(
  <DepsProvider deps={{ foo, bar }}>
    <App />
  </DepsProvider>,
  document.getElementById('container'),
);

(Optional) Componentに必要なモジュールだけを参照するようにする

DepsProviderは、Component群から参照される全ての依存モジュールを注入しなければいけません。
そのため、useDeps()から得られるモジュールの一部は、あるComponentにとって不要なことがあります。
そこで、Componentごとに必要とする依存モジュールを絞り込む機能も紹介します。
(と言っても、これはComponentのテストのために導入した機能であり、これを使うとComponentの実装の観点で何かが便利になるというわけではありません。)

DIコンテナを作る

実はcreateDIContainer()から、createLocalという関数も提供されているので、これをexportしておきます。

const { DepsProvider, useDeps, createLocal } = createDIContainer<Deps>();

export { DepsProvider, useDeps, createLocal };

依存モジュールを絞り込んで参照する

先ほど得たcreateLocalを使います。

import React from 'react';
import { createLocal } from 'path/to/di-container';

const { useLocalDeps } = createLocal((deps) => ({ foo: deps.foo }));

export const Foo = () => {
  const { foo } = useLocalDeps();

  return (
    <div>
      <button onClick={foo.doFoo}>Do foo</button>
    </div>
  );
};
import React from 'react';
import { createLocal } from 'path/to/di-container';

const { useLocalDeps } = createLocal((deps) => ({ bar: deps.bar }));

export const Bar = () => {
  const { bar } = useLocalDeps();

  return (
    <div>
      <button onClick={bar.doBar}>Do bar</button>
    </div>
  );
};

依存モジュールを注入する

これは同じく、先ほど得たDepsProviderを使います。

import React from 'react';
import ReactDOM from 'react-dom';
import { DepsProvider } from 'path/to/di-container';
import { Foo } from 'path/to/foo';
import { Bar } from 'path/to/bar';

const foo = {
  doFoo: () => {
    console.log('foo');
  },
};

const bar = {
  doBar: () => {
    console.log('bar');
  },
};

ReactDOM.render(
  <DepsProvider deps={{ foo, bar }}>
    <Foo />
    <Bar />
  </DepsProvider>,
  document.getElementById('container'),
);

(Optional) テスト時に、useLocalDeps()で参照されるモジュールだけを注入する

前述の通り、createLocal()はComponentのテストのために作った機能です。
createLocal()の返り値からはLocalDepsProviderというComponentも得られ、これを使って次のようにテストを書けます。

import React from 'react';
import { createLocal } from 'path/to/di-container';

const { LocalDepsProvider, useLocalDeps } = createLocal((deps) => ({ foo: deps.foo }));

export { LocalDepsProvider as FooDepsProvider };

export const Foo = () => {
  const { foo } = useLocalDeps();

  return (
    <div>
      <button onClick={foo.doFoo}>Do foo</button>
    </div>
  );
};
import { Foo, FooDepsProvider } from 'path/to/foo';

describe('Foo', () => {
  it('works', () => {
    const fooMock = {
      doFoo: () => {
        console.log('foo');
      },
    };

    render(
      <FooDepsProvider deps={{ foo: fooMock }}>
        <Foo />
      </FooDepsProvider>
    );
  });
});

テスト時にDepsProviderを直接使っても構いませんが、全ての依存モジュールのモックをいちいち注入するのは面倒ですし、テストコードにノイズが増えてしまいます。
createLocal()から得られるLocalDepsProviderを使うと、必要なモジュールのモックだけ注入すればよいので、テストがシンプルになります。

おわりに

ライブラリ自体がシンプルなので、使い方もシンプルに収まります。
createLocal()を使わなくても結構ですし、なんならComponentごとにcreateDIContainer()によってDIコンテキストを作り、アプリケーション全体をDepsProviderで包むことはしないという方法もあるでしょう。
このライブラリを使うとも使わずとも、Reactにおけるモジュール設計の参考になれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【phina.js】パスに沿ったオブジェクト移動

パスに沿ったオブジェクトの移動

ゲームを作成していると、動く床などのように一定のルートに従ってオブジェクトを移動させたい時があると思います。 phina.jsを使って、自分なりにその処理を実装してみました。

動作サンプル

まずは以下のサンプルを確認してみて下さい。 描画された線(パス)に沿ってオブジェクトが移動します。

20190529073024.gif
runstantで表示

頂点データの用意

  • パスの実体は頂点の集まりに過ぎません。
  • 頂点データとして、x,y座標値を内部に保持できるVector2クラスを利用し、それを配列に格納する形にしました。
  • 位置指定にはGridを利用しています。
var vec = Vector2;
// 頂点データ
var verts = [vec(gx.span(1), gy.center()),
             vec(gx.span(3), gy.center(-2)),
             vec(gx.span(5), gy.center()),
             vec(gx.span(7), gy.center(-2)),
             vec(gx.span(9), gy.center()),
             vec(gx.span(11), gy.center(-2)),
             vec(gx.span(13), gy.center()),
             vec(gx.span(15), gy.center(-2))];

パスの描画

  • パスの描画にはPathShapeを利用しています。コンストラクタのpathsプロパティに頂点データを与えることで簡単にパスが描画できる便利なクラスです。
  • 今回のように画面サイズを変える場合には、main関数、Sceneのコンストラクタ、PathShapeのコンストラクタで、widthheightに同じ値を指定するようにします。
// パス描画用
var path = PathShape({
  width: SCREEN_WIDTH,
  height: SCREEN_HEIGHT,
  paths: verts,
}).addChildTo(this);

パスに沿ってオブジェクトを移動させる処理

ある程度の汎用性を持たせるために、moveAlongPathという関数を作ってみました。

// ターゲットを与えられたパスに沿って移動させる
  moveAlongPath: function(target, path) {
    // 最初の頂点へ移動
    target.position = path.first;
    // 頂点群をループ
    path.each(function(vert, i) {
      if (i > 0) {
        // tweenをスタック
        target.tweener._add({
          type: 'tween',
          mode: 'to',
          props: {x: vert.x, y: vert.y},
          duration: 1000,
        });
      }
    });
  },
  • オブジェクトの移動には、非同期処理が行えるTweenerを利用します。
  • tweenerの内部処理で使われている_add関数で処理をスタックさせています。
  • tweenerは、基本的にスタックされた順に非同期処理されるので、次の移動先である各頂点の位置を順番に与えることで、結果としてパスに沿った移動が可能になります。

課題

  • 今回のサンプルでは、移動速度が一定になるように各頂点の距離が同一になるように配置しています。
  • 一定の時間で移動させるのがtweenerの処理ですので、距離が変わると移動速度も変わることになります。
  • 各頂点の距離に応じてdurationを変えると速度を一定にすることが可能になると思われますが、その辺は次回の課題にしたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【phina.js】色々なスクロールを試す

はじめに

横スクロールアクションゲームでは、画面のスクロール処理が欠かせませんが、スクロール1つでも結構奥が深いです。今回はphina.jsで主だったスクロール処理を実装してみました。

プレイヤー固定スクロール

まずは以下のサンプルを確認してみて下さい。 プレイヤーの位置は画面の中心固定で画面がスクロールします。画面タッチでプレイヤーがジャンプして、障害物に当たると反転移動します。 中心には分かりやすいようにラインを表示しています。

20190529174013.gif
runstantで動作確認

このパターンのスクロールは、特に難しい実装はないかと思います。 プレイヤーは動かさずに固定して、プレイヤー以外のオブジェクトをプレイヤーの移動方向と逆向きに動かすことで、プレイヤーが移動しているように見えます。

// 画面スクロール
moveX: function() {
  var self = this;
  this.objectGroup.children.each(function(obj) {
    obj.x += -self.player.vx;    
  });
},

変則スクロール

始めは固定スクロールと同じようにスクロールしますが、ステージの端に行くとスクロールが止まります。 そして、画面中央を超えると再びスクロールします。

20190529180258.gif
runstantで動作確認

このパターンのスクロールのポイントは、プレイヤー以外のオブジェクトの移動とプレイヤーのみの移動の切り替えです。 プレイヤーの横方向の状態をチェックする関数を用意して、その中で切り替えを行います。

// 横方向の状態チェック
 checkHorizontalState: function() {
   var player = this.player;
   var state = this.player.horizontalState;
   // 左端のオブジェクト
   var first = this.objectGroup.children.first;
   // 右端のオブジェクト
   var last = this.objectGroup.children.last;
   // プレイヤーの状態で分ける
   switch (state) {
     // 左移動中
     case 'MOVING_LEFT':
       // ヒットしたら反転
       if (this.collisionX()) {
         player.reflectX();
         player.horizontalState = 'MOVING_RIGHT';
       }
       else {
         // 左端のオブジェクトが見えたらプレイヤーのみ動かす
         if (first.x > 0) {
           player.horizontalState = 'MOVING_LEFT_SELF';
         }
         else {
           this.moveX();
         }
       }
       break;
  • 横方向の状態で、MOVING_LEFTはプレイヤーを固定して背景を動かす場合、MOVING_LEFT_SELFは背景を固定してプレイヤーを動かす場合としました。
  • this.objectGroup.children.firstで一番最初(左端)のオブジェクトが参照できますので、それをfirstという変数に代入して位置の比較を行い、画面左端に現れたらプレイヤーのみの移動に切り替えています。
// 左移動中(プレイヤーのみ)
case 'MOVING_LEFT_SELF':
  if (this.collisionX()) {
    player.reflectX();
    player.horizontalState = 'MOVING_RIGHT_SELF';
  }
  else {
    // 右端のオブジェクトが画面から消えてプレイヤーが中央を超えたら
    if (last.x < SCREEN_WIDTH && player.x < this.gx.center()) {
      player.x = this.gx.center();
      // 通常スクロールへ
      player.horizontalState = 'MOVING_LEFT';
    }
    else {
      player.moveX();
    }
  }
  break;
  • 左向きのプレイヤーのみの移動からスクロールへの切り替えは、右端のオブジェクトが画面から見えなくなって、かつプレイヤーが画面中央を超えた時で判定しています。
  • これらの判定を右方向の移動に対しては、逆に行えば良いということになります。

スライドスクロール

初期のゼルダの伝説に代表されるように、プレイヤーが画面の端に到達すると画面がスライドしてスクロールするタイプです。

20190529181107.gif

runstantで動作確認

このパターンのスクロールは、基本プレイヤーのみを動かしますが、画面をスライドする時には背景オブジェクトとプレイヤーをまとめて動かす必要があります。

// 右移動中
case 'MOVING_RIGHT':
  if (this.collisionX()) {
    player.reflectX();
    player.horizontalState = 'MOVING_LEFT';
  }
  else {
    if (player.x > SCREEN_WIDTH) {
      player.horizontalState = 'STOP';
      player.verticalState = 'STOP';
      player.vy = 0;
      this.scrollX();
    }
    else {
      player.moveX();
    }
  }
  break;
// 停止中
case 'STOP':
  break;
  • プレイヤーが画面端に達したらプレイヤーの動きを止めて、STOPという新しい状態に遷移させます。
  • STOPでは、その名の通り何も処理も行いません。
  • 新たに作ったscrollXという関数を呼んで画面をスライドさせます。
// 画面スクロール
  scrollX: function() {
    var player = this.player;
    var flows = [];
    // スクロール方向を決める
    var dir = player.vx > 0 ? -1 : 1;
    // 背景オブジェクトの移動
    this.objectGroup.children.each(function(obj) {
      var flow = Flow(function(resolve) {
        obj.tweener.by({x: dir * SCREEN_WIDTH}, 1000)
                   .call(function() {
                     resolve();
                   }).play();
      });
      // flow配列に追加
      flows.push(flow);
    });
    // プレイヤー
    var flow = Flow(function(resolve) {
      player.tweener.by({x: dir * SCREEN_WIDTH}, 1000)
                    .call(function() {
                      resolve();
                    }).play();
    });

    flows.push(flow);
    // 非同期処理
    Flow.all(flows).then(function() {
      // プレイヤー移動再開
      player.horizontalState = player.vx > 0 ? 'MOVING_RIGHT': 'MOVING_LEFT';
      player.verticalState = 'FALLING';
    });
  },
  • スライドスクロールは、tweenerと非同期処理を行うFlowで実装しています。
  • プレイヤーの移動方向とは逆に、オブジェクトとプレイヤーを1画面分まとめて動かしています。
  • Flowはオブジェクトとプレイヤーの分を作成して配列に追加して、Flow.allで全ての移動終了を検知した後に、プレイヤーを動かす状態に遷移させています。
  • Flowについては、phina.js Tips集のFlowで非同期処理を行うを参考にして下さい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む