20190625のJavaScriptに関する記事は30件です。

初めてGAS触ったときの使った関数メモとGASの良かったところについて

はじめに

この記事はGAS(Google Apps Script)を触ったことない人向けの記事です。
自分がまず初めて触ったので、作ったものの作り方とGASのいいなと思ったところとかいろいろまとめていこうかなという感じです。

GASいじってく

準備

まずGoogleスプレッドシートを開いてください。

そこのヘッダーのツールから

スクリーンショット 2019-06-21 2.13.33のコピー.png

スクリプトエディタを開いて

スクリーンショット 2019-06-21 2.13.55のコピー.png

このページに来ます。

スクリーンショット 2019-06-21 2.15.33.png

ここに実行したい関数を書いていきます。

書き方

  • 普通に変数宣言
var hogeNumber = 1

var hogeString = "わーい"
  • アクティブなシートを取得(もともと開いたスプレッドシートのファイルのオブジェクトを取得)
var hogeSheet = SpreadsheetApp.getActiveSpreadsheet()
  • そのファイルの中のシートをシート名から指定
var sheetName = "シート1"
var srcSheet = hogeSheet.getSheetByName(sheetName);
  • for文、if文
for (var i = 0; i < 10 ;i++} {
  // 処理書いてく
}
  • foreach
var hogeArray = [11, 12, 13]

hogeArray.forEach(function(value) {
  Loger.log(value)
  Loger.log("hoge")
});

// 出力結果
// 11hoge12hoge13hoge

  • ログ出力
Loger.log("hoge")

// ↑を書いて一回ファイル保存
// ↑を通る処理を書いている関数を「関数を選択」という部分で選んで実行ボタン(▶)を押す
// その後にcommand + Enterを押すとログ画面が見れます

GAS触ってみて良いと思った部分

  1. ちょー書きやすい

宣言の仕方さえわかれば感覚的に書いていける。あとcommand + sでファイル保存したときに文法的に間違ってる場所を指摘してくれる。

  1. デバッグ簡単

上に書いてる方法で出力できるので簡単に実装できる。

  1. サーバーレス

サーバーにアップしなくても動かせるので、UIを作くらなくてもいいシステムを作るときにサクッと作れる。

  1. スプレッドシートがDB代わりになる

普段動的なサービスを作っていくときにDBの設定とかいろいろ気にしないといけない。でもGAS使えばスプレッドシートとつながってるからそれをDB代わりにして使える。マイグレーションとかもしなくていいからすぐに列とか増やせる。

まとめ

今回GASを使ってLINEのBotを作ったんですが、初めてにもかかわらずよくできたかなと思いますた。
次はslackからコマンド打って動かすGASスクリプトでも書こうかなと。

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

Nuxtのsentryモジュールでエラーを検知するときの注意点

こんにちは、プログラミングスクールのレビューサイト「スクールレポート」を運営しているアカネヤ(@ToshioAkaneya)です。

Nuxtのsentryモジュールでエラーを検知するときの注意点

以下のようにtryの中で例外が発生する場合は注意が必要です。このとき、sentryで通知するにはthis.$sentry.captureException(e)が必要なことに注意してください。

デフォルトでは、Uncaught Exceptionにしかsentryは機能しないからです。

    try {
        const a = 1
        a()
      } catch (e) {
        this.$sentry.captureException(e) // これが必要!
      }

参考 公式 https://github.com/nuxt-community/sentry-module

はてなブックマーク・Pocketはこちらから

はてなブックマークに追加
Pocketに追加

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

マンガのような画像を並べるHTML

とりあえず、マンガのように並べて表示出来るように作成しました。

静的に作っていますが、フレームワーク的に使えるように作成しています。
クリック場所でページ移動とキーボード移動に対応しています。

改造は今後に期待

https://github.com/karosuwindam/mangahtml

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

[初心者向け]VueとExpressを使ってパラメーターの受け渡しをしてみよう

はじめまして、PMをやっているtatsukenと申します。はじめまして
研修の一環でvue.js、expressを書くことがあったので、そのことを中心にまとめていきたいと思います.

はじめに

Vue.jsでaxiosを使ってパラメーターをpostしexpressで作った、apiで受取るというところまでやりたいと思います。

必要な環境

Vue.jsの実装

  • まずこちらの記事を参考にしてサーバーを立ち上げてください。
  • http://localhost:8080にアクセスして、このような画面が表示されれば成功です。 スクリーンショット 2019-06-25 20.11.51.png
  • 次にaxiosをインストール
    • npm install axios -s

画面を作る

  • 次にsrc/components/HelloWorld.vueをいかのように書き換えてください
HelloWorld.vue
<template>
  <div class="hello">
    <form action>
      <input type="text" placeholder="text" v-model="text">
      <input type="submit" value="decide" @click="submitClick">
    </form>
  </div>
</template>

画面はこのようになっています
スクリーンショット 2019-06-25 20.36.00.png

HelloWorld.vueの</template>以下に<script>を追加

HelloWorld.vue
<script>
import Axios from "axios";
export default {
  name: "HelloWorld",
  data() {
    return {
      text: null
    };
  },
  methods: {
    submitClick() {
      alert(this.text)
    }
  }
};
</script>

このようにinputに文字を入力してその文字がalertに表示されれば成功です
スクリーンショット 2019-06-25 20.42.23.png

<script></script>を書き換え

次に<script></script>を以下のように書き換えてください

HelloWorld.vue
<script>
import Axios from "axios";
export default {
  name: "HelloWorld",
  data() {
    return {
      text: null
    };
  },
  methods: {
    submitClick() {
      const body = {
        text: this.text
      };
      Axios.get(`http://localhost:3000/hoge/${this.text}`)
        .then(res => {
          console.log(res.data.name)
        })
        .catch(err => {
          console.log(err);
        });
    }
  }
};
</script>

Expressの実装

簡単なapiを作ってみる

  • アプリ名/app.jsに以下のことを追記しましょう
app.js
app.get('/hoge', (req, res) => {
  res.json({
    name: "hoge"
  })
});

スクリーンショット 2019-06-25 21.03.04.png

apiを書き換える

app.js
app.get('/hoge/:name', (req, res) => {
  let data = req.params
  console.log(data.name);
  res.json({
    name: data.name
  })
});

これでコードを書く部分はすべて終了です。

動かしてみる

  • http://localhost:8080にアクセスし任意の文字を入力し送信してください。

  • Expressを動かしているconsole上にfrontから送信した文字が表示されていれば成功です
    スクリーンショット 2019-06-25 21.25.26.png
    スクリーンショット 2019-06-25 21.25.38.png

最後に

初心者向けにVue.jsからExpressのapiへのパラメーターの受け渡しを行いました。
これから始める方の手助けになれば幸いです。
なにか間違いがあればご連絡ください。

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

[Vue]テキストをクリックすると入力欄に変わる、といった実装には$nextTickが便利な件

とりあえず成功したサンプル

See the Pen [Vue]テキストを押したら入力欄が現れて、かつフォーカスする(1) by riotam (@riotam4) on CodePen.

使い方

「ここをクリックしてね」自体をクリックしたら、入力欄が現れて、そのまま編集できる。
という想定で作ってみました。

失敗したコード

See the Pen [Vue]テキストを押したら入力欄が現れて、かつフォーカスする(2) by riotam (@riotam4) on CodePen.

問題点

入力欄は出せたけど、フォーカスができていません。

失敗コードの解説

HTML側
<div id="app">
  <p v-if="inputField"><input ref="focusThis" value="ここをクリックしてね"></p>
  <p @click="show" v-else>ここをクリックしてね</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
JS側
  new Vue({
    el: "#app",
    data: {inputField: false},
    methods: {
      show() {
        this.inputField = true;
        this.$refs.focusThis.focus();
      },
    },
  })

まず、「ここをクリックしてね」テキストをクリックすると、v-on:clickディレクティブによって、showメソッドが発火されます。
showメソッドでdataのsがtrueに変更され、すぐにthis.$refs.focusThis.focus();が発火し、ref="focusThis"のついた要素を探し、そこにフォーカスします。

一方、その頃HTML側では、inputFieldがtrueになったことで、HTML側のv-ifもtrueとなり、DOM操作によりinputタグが表示されます。
それと同時に、「ここをクリックしてね」テキストはv-elseなので消え。テキストと入力欄が入れ替わります。

原因

参考サイト等を調べた結果、上の動きに間違いは無いのですが、DOM操作のタイミングがやや遅いことが原因と考えられます。
つまり、inputタグが表示されるより先に、this.$refs.focusThis.focus();が実行されてしまうことで、フォーカスできないという問題が発生します。

成功したコードの解説

JS側
  new Vue({
    el: "#app",
    data: {inputField: false},
    methods: {
      show() {
        this.inputField = true
        this.$nextTick(() => this.$refs.focusThis.focus())
      },
    },
  })

基本的には失敗したコードと同じですが、ここでは$nextTickというものを使っています。
以下に説明していきます。

$nextTickとは

$nextTickは、Vue公式インスタンスメソッド(ライフサイクル)です。公式には小難しく書かれていますが、とても簡単です。
要は、this.$nextTick()の引数の中に、関数の形で実行したいコードを入れているだけです。
(ちなみに、() => this.$refs.r.focus()これはアロー関数の書き方です。その記事についてはこちら
これによって、this.$refs.focusThis.focus()はDOM操作が終わってから実行されるようになります。

さいごに

今回は非同期処理的な実装によく使うであろう、$nextTickを中心に説明させて頂きつつ、サンプルコードの共有を致しました。
最後までありがとうございました。

参考にしたもの

参考サイト①
参考サイト②

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

Why is AngularJS the Best Fit for a Responsive Web App Development?

Responsive web development.jpg

We all know that websites continue to be one of the most productive and reliable sources for organizations, the world over, to promote their businesses and products. However, it’s not just about how strong and secure, a web application is. Customers equally look at how responsive, a particular web application is. By Responsive, we don’t just mean how much time it takes to load initially and how quick is the transition, in between pages. Responsive also relates to how well the application works with and adjusts to a variety of electronic devices with varied screen sizes.

We have come across various frameworks which are prominently used to build web applications. But one framework, which fits the bill most and works wonderfully well, is AngularJS. AngularJS Development is being preferred by most organizations as far as responsive web development goes and as a result, AngularJS developers are in great demand. But why exactly is AngularJS been a preferred choice? Reasons are not one, but many.

The Phenomenon called AngularJS

What more do we need, to convince ourselves about the abilities of the AngularJS framework, than the fact that it is maintained by none other than Google! But frankly speaking, this is not the only reason why AngularJS web development, has gained a lot of traction for the developer community, over the years. Apart from it being a front end, open source JavaScript framework, it is the amazing features it brings to web application development.

Despite of so many web development technologies around, it is the simplicity, quality and efficiency of AngularJS, that has left developers with no other choice, for a responsive web app development. Additionally, AngularJS Developers have continued to benefit immensely from this framework as it,

Facilitates Better Programming Practices

Many organizations these days look to hire AngularJS developers, as it has some incredible features that support better programming practices. Some of these include directives that support reusable components and the REST API connection to servers, which use lesser bandwidth and enables HTTP requests, for efficient data handling process.

Faster Development Process

The AngularJS web development helps in faster development of apps with fewer efforts. With responsive development calling for compatibility across various devices, AngularJS developers possess the desired skillsets, to achieve the final objective for the companies. In addition, implementing the two way data binding process and saving data to the server requires you to write only a few lines of code in AngularJS as compared to some of the other frameworks.

Ensures a secured web application development

At any given stage, web applications are prone to cyber-attacks and as such, data security continues to be one of the major concerns for companies. However, with AngularJS web development, these concerns are addressed to a great extent, as it uses the HTTP interface, either in the form of a web service or a REST API, as discussed a little while earlier in this article. AngularJS developers have also found it to be very flexible as it allows the user to integrate third-party security systems with your existing Angular application.

Easy to collaborate

Generally it’s the teamwork that matters for the success of any given project, including a responsive web app development project. Apart from AngularJS developers being subjected to lots of technical challenges, they have to collaborate with the entire team, to ensure a smooth process. With an ability to share components within the team, the Dependency Injection feature, associated with the AngularJS web application development process, has excited the developer community by making collaboration very easy.

Enables real time testing

A given application might be subjected to changes at any stage with a view to optimize performance and also to address issues, if any. Where AngularJS web development process is a big plus is during periodic testing as it supports real-time testing. The AnglarJS framework comes with in-built modules to facilitate a comprehensive testing for each and every module you develop.

Facilitates maximum use of existing data

AngularJS developers are always at an advantage compared to some of the other popularly used web development frameworks as they applications allow you to access your existing database without any restrictions, thanks largely to the RESTful APIs This is considered one of the primary advantages of AngularJS that leads to its popularity.

Code Reusability Option

This is a fascinating prospect for AngularJS developers. Code reusability is yet another feature which attracts developers as they can reuse the code. This is advantageous especially while building complex, responsive, enterprise level web applications. It also ensures faster completion and easy implementation.

Uninterrupted and flawless design

AngularJSgives developers an opportunity to work on a more advanced framework with the option to add Markup, which can be used to identify elements, within a document, without necessarily having to break the entire application. This can be done by re-organizing the specific portions of code easily and conveniently.

Wrap Up

The benefits with AngularJS web app development are evident. Especially when it comes to a responsive web app development requirement, AngularJS development has a simple architecture with facilities for improved design. In addition, the code reusability and dependency injection features, add to the stability and security of the application, which is there for every AngularJS developer to experience. All in all,it provides all the win-win benefits with efficient features and productive methods. AngularJs Developers are helped with the smooth web application development framework which allows easy and effective responsive web application development.

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

Array.mapが分からないからArray.mapもどきを作ってみた

業務中ワイ

ワイ「なあ、ハスケル子ちゃん」

ハスケル子「はい」

ワイ「今、ハスケル子ちゃんの書いたJSのコードを見てたんやけど」
ワイ「これどういう意味?」

const nodes = document.querySelectorAll(".list > li");
const nodesArray = Array.from(nodes);
const nodesHeightArray = nodesArray.map(getHeight);

ハスケル子「えっと、うーん・・・」
ハスケル子「(見ての通りなんだけど、逆にどこが分からないんだろう・・・)」
ハスケル子「どこが分かりにくかったですか?」

ワイ「このnodesArray.mapいうメソッドは何なん?」

ハスケル子「
haskellko.jpeg

ハスケル子「なんでそんなことが分からないんだろう・・・

ワイ「お、おい・・・心の声が出てもうてるで・・・

ハスケル子「(す、すいません!)」

ワイ「なんで謝罪は心の声になっとんねん
ワイ「どんな忖度や
ワイ「判断基準どうなってんねん

ハスケル子「ごめんなさい」
ハスケル子「お詫びにmapメソッドについて説明しますね」

mapメソッドとは

ハスケル子「mapメソッドは、配列が持ってるメソッド1です」
ハスケル子「ある配列を元に、新しく別の配列を作ることができるメソッドですね」
ハスケル子「さっきのコードでいうと───」

const nodes = document.querySelectorAll(".list > li");

ハスケル子「↑まずquerySelectorAllで、いくつかのli要素を取得して」
ハスケル子「それをnodesとします」

const nodesArray = Array.from(nodes);

ハスケル子「↑次にnodesArray.fromメソッドで配列に変換します」
ハスケル子「nodes配列みたいだけど配列じゃない2ですからね」
ハスケル子「そしてnodesから作った配列nodesArrayの───」

const nodesHeightArray = nodesArray.map(getHeight);

ハスケル子「───mapメソッドを実行します」
ハスケル子「nodesArrayの各要素に対して何らかの処理を施して」
ハスケル子「新しくnodesHeightArrayという配列を作る、って感じです」
ハスケル子「その何らかの処理というのを、関数として渡します

ワイ「なるほどな」
ワイ「getHeightが関数なんやね」

ハスケル子「はい」

const getHeight = element => element.clientHeight;

ハスケル子「getHeight関数の中身は↑これです」

ワイ「elementという引数を1つ受け取ってelement.clientHeightを返すということは」
ワイ「要素の高さを返す関数やな」

ハスケル子「はい」
ハスケル子「さっきのコードを実行すると、最終的にnodesHeightArrayには」

[24, 48, 72, 120, 24, 72, 48]

ハスケル子「↑という配列が入ることになります」

ワイ「ほえ〜、DOM要素のリストを元に、各要素の高さを集めた配列を作れるんか」
ワイ「たまに便利そうやな」

ワイの気になったこと

ワイ「でもハスケル子ちゃん」

const getHeight = element => element.clientHeight;

ワイ「↑このgetHeightの引数であるelementどこで渡してんの?

ハスケル子「どこで渡すっていうか、勝手に入ってくるんです」

ワイ「え・・・勝手に入ってくる・・・?」
ワイ「空き巣みたいに・・・?

ハスケル子「やめ太郎さんも、自分でmapメソッドを実装してみたら分かりますよ」
ハスケル子「やってみましょう」

Object.defineProperty(Array.prototype, "myMap", {
  value: function(callback) {
    /* ここに処理内容を書く */
  }
});

ハスケル子「↑こうやって」
ハスケル子「Object.definePropertyメソッドを使うことで」
ハスケル子「配列にmyMapというメソッドを追加できるんです3

ワイ「おお、そんな機能があるんか」
ワイ「ほなちょっと挑戦してみるわ」
ワイ「でも・・・このcallbackいう引数はどこから入ってくんの」

ハスケル子「だから・・・」
ハスケル子「その辺を理解するために今からコードを書いてもらうんです」
ハスケル子「再帰的な質問をしないでください
ハスケル子「もうとりあえず空き巣が勝手に入ってくるとでも思っておいてください」

ワイ「お、おう・・・」

ハスケル子「配列のmapメソッドには、関数を1つ渡して実行しましたよね?」

ワイ「ああ、さっきしてたな」

ハスケル子「そのときmapメソッドに渡した関数が」
ハスケル子「今から書く関数のcallbackという引数として入ってくる・・・」
ハスケル子「そんなイメージでmyMapメソッドも実装してみてください」

ワイ「なるほど、やってみるで」

Object.defineProperty(Array.prototype, "myMap", {
  value: function(callback) {
    const resultArray = []; // まず空の配列を作成
    /* ここで配列に色々する */
    return resultArray; // その配列を戻り値として返す
  }
});

ワイ「まずは↑こんな感じやな」
ワイ「空の配列を作って、なんか色々して、最終的にその配列を返す、と」

ハスケル子「いいですね」

ワイ「ほんで、元々の配列の各要素に対して何らかの処理をしていけばいいんやな」
ワイ「そうか」
ワイ「その何らかの処理が、callbackとして受け取った関数に入ってるというテイで考えればいいんやな」

ハスケル子「そうです」
ハスケル子「ちなみに今書いてる関数の中ではthisって書けばその配列自身にアクセスできますよ」

ワイ「なるほど、thisが配列自身やな」
ワイ「ほな、配列の分だけfor文で回して───」

for (let i = 0; i < this.length; i++) {
  const newElement = callback(this[i]); // i番目の要素にcallbackを適用。
  resultArray.push(newElement); // それを「戻り値として返す配列」に追加。
}
return resultArray;

ワイ「↑こうやな!」

ワイ「this[i]、つまり配列のi番目の要素に対してcallback関数を実行して」
ワイ「それをnewElementとして配列にpushする」
ワイ「そして出来上がった配列resultArrayを戻り値として返す」

ハスケル子「いいですね!」

ワイ「よっしゃ、じゃあワイのmyMapメソッド試してみるで!」

const nodesHeightArray = nodesArray.myMap(getHeight);
console.log(nodesHeightArray);

// コンソール結果: [24, 48, 72, 120, 24, 72, 48]

ワイ「おお、さっきのmapメソッドと同じ結果や!」

ハスケル子「いい感じです」
ハスケル子「実際のmapメソッドはもっと複雑なアルゴリズムですけど」
ハスケル子「イメージ的はこんな感じです」

ワイ「なるほどな〜」
ワイ「mapメソッドに対して渡したgetHeightいう関数を」
ワイ「mapメソッドの中で、配列の各要素に対して実行してくれてたんやな」
ワイ「そのときにthis[i]的な感じで」
ワイ「callback関数の引数として、元の配列の各要素を渡してくれてたんやな」
ワイ「せやから引数が勝手に入って来てたんか〜」
ワイ「謎が解けましたわ〜」
ワイ「ありがとう、ハスケル子ちゃん」

ハスケル子「実際のmapメソッドは、callback関数を実行するときにthis[i]だけじゃなく」
ハスケル子「ithisも渡してくれるので、さらに色々できますよ」

ワイ「なるほどな」
ワイ「第二引数に今何番目の要素かを表すインデックス」
ワイ「第三引数には配列自身が入ってくると」
ワイ「工夫次第で色々できそうやな」
ワイ「array[i - 1]で一つ前の要素を見たりとかな」

ハスケル子「そうですそうです」

コールバック関数について

ワイ「今みたいな感じで、関数を渡して使うタイプの関数を」
ワイ「自分で作ってみてもオモロそうやな〜」
ワイ「例えば共通化できそうな処理があったとして」
ワイ「ほとんどの処理を関数として共通化できるけど」
ワイ「最後にちょっとだけ挙動を変えたい」
ワイ「しかも何パターンかの挙動があるから、引数で何かパラメータを受け取ってやる方法やと」
ワイ「if文が増えすぎて見通しが悪くなる・・・そんな時には」
ワイ「関数の最後にこの処理をやってくれい!
ワイ「っていう処理の内容を、コールバック関数として受け取って」
ワイ「最後に実行してやればええんやな〜」
ワイ「そうすれば」
ワイ「同じ関数を使うけど、一部だけ自由に挙動を変えることが出来んねやな」

ハスケル子「だんだん中級者っぽくなってきましたね」

ワイ「(いや、君よりだいぶJS歴長いねんけどな・・・)」

Object.definePropertyについて

ワイ「あと、Object.definePropertyすごいな」

ハスケル子「Vue.jsなんかも、このObject.definePropertyで」
ハスケル子「Arraypushメソッド等を上書きすることで」
ハスケル子「配列に要素を追加しただけDOMが更新されるようにしてるんですよ」

ワイ「せやろな〜(まじか!)」
ワイ「よっしゃ、ワイも明日から配列にメソッド追加しまくるで〜!」
ワイ「あ、メソッドの上書きもできるんやったな!」

ハスケル子「レッツ、プロトタイプ汚染っ!

社長「(ハスケル子ちゃん、そこはちゃんと止めてや・・・!)」

そうこうしてるうちに18時

ワイ「やば、ワイがコード読めへんせいでもう定時やないかい」
ワイ「コーディング全然進んでへん・・・」

Riot兄さん「俺に任しとけ

ワイ「あ、兄貴・・・!」

Riot兄さん「俺が界王拳2倍でコーディングして、なんとかしたるわ」
Riot兄さん「よっしゃ、今日から毎日7時間残業するで〜!」

社長「おっ、兄貴の物理界王拳やな!」

ハスケル子「物理界王拳・・・?(兄貴、すごい・・・///)」

社長「せや」
社長「ただし物理界王拳は3倍までしか使えへん」
社長「24時間超えてしまうからな」

ワイ「物理界王拳て」
ワイ「ただのブラック会社4やないかい!

〜おしまい〜

追記

ワイ「っていうかホンマの界王拳も物理やろ!


  1. 実際にはArray.prototypeのメソッドとして定義されてるけど、きっとやめ太郎さん分からないから・・・ 

  2. ノードリストですね。 

  3. グローバルなオブジェクトに手をつけることになるので、むやみにやらない方がいいと思います。 

  4. 実際には残業はほぼありません。(フロントエンドチーム以外は知りません) 

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

JSでマルチスレッド(ユーザスレッド)してくれるライブラリConcurrent.Threadを試してみる

JSはシングルスレッドなのがなあ (I/O処理等、ランタイム内で一部例外があるのは知っています)

以前からJSでスレッド使えないのかなあと思っていたのですが、2008年のこんな記事を少し前に見つけて、
 
JavaScriptによるマルチスレッドの実現‐Concurrent.Threadの裏側 - InfoQ

これはすごい!
と思ったのですが、実装を探すとSourceForgeにあるものの、
 
https://sourceforge.net/projects/jsthread/

最終コミットが4年前とかなので、これは動くか厳しいか・・・と思いました。

ダメ元で試してみる

成果物のReleaseのページから
 
https://sourceforge.net/projects/jsthread/files/release/

  • Concurrent.Thread-full-20090713.js
  • Concurrent.Thread.ScriptExecuter+Http.js
  • Concurrent.Thread.Compiler+Http.js

の3つをダウンロードしてきて、以下のようなHTMLおよびJSコードを書いたらちゃんとマルチスレッドとして動きました!

<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>jsthread</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <script type="text/javascript" src="Concurrent.Thread.Compiler+Http.js">
    </script>
    <script type="text/javascript" src="Concurrent.Thread.ScriptExecuter+Http.js">
    </script>
    <script type="text/javascript" src="Concurrent.Thread-full-20090713.js">
    </script>
  </head>
  <body>
    <script type = "text/javascript">
        function getRandomInt(max) {
          return Math.floor(Math.random() * Math.floor(max));
        };
        function f1(){
              var i = 0;
              while(1){
                  document.body.innerHTML += i++ + "<br>";
              }
        };
        function f2(){
              while(1){
                  console.log(getRandomInt(1000));
              }
        };
        Concurrent.Thread.create(f1)
        Concurrent.Thread.create(f2)
    </script>
  </body>
</html>

 

書いたHTML&JSでやっていること

  • よろしくスレッドに渡す関数を定義
  • 2つスレッドを立ち上げる
  • 1つ目のスレッドはウェブページ?に無限ループで数字を出力し続ける
  • 2つ目のスレッドはコンソールに0-1000の乱数をこれまた無限ループで出力する

ってな感じです。

普通だと終了しない、かつヘビーな処理をする無限ループとか書いたら、終了のお知らせ(UI自体が固まると思われる)って感じですが、このコードはそうはなりません。

動いているところを見てみよう

repl.it に動作デモを置いておきましたにで、アクセスしてみて下さい。
 
https://repl.it/@ryo_grid/ConcurrentThreadtestshareqiita

Runすると動作が始ります。
Web画面では数字がダーッと出続けますが、一方でコンソールを見るとそちらでも乱数がダーッと出続けています。

async/awaitでいいのでは?

以下は私の理解で、誤っている可能性もあるので、その場合はご指摘をお願いします〇刀乙

最近のJSだとasync/awaitとかが入って、awaitしている間、他の処理が走れるので、例えばヘビーな無限ループとかあってもawaitで待つ処理でもいれてやれば、マルチスレッド的なコードが書けそうな気がするのですが、よくよく調べてみたところ、awaitかけても、その対象の処理の中を辿って行ってI/Oだとかの本当にただ待つだけの処理が存在しないと、awaitかけた行を持つasync関数はスレッドを手放さないみたいなんですよね。
 
なので、上でConcurrent.Threadを使って書いたような処理を並列に動作させることはおそらくできませんし、例えば、純粋に数値計算だけをする2つの関数があったとしたら、間違いなく対応不可です。

一方で、Concurrent.Threadは黒魔術を駆使して、各関数を分割して、一定のタイムスライスごとにスレッド機構(など)で言う、コンテキストスイッチをしてくれるらしいので、そのような場合でも対応することができます(現実的にそんなユースケースがあるかは別として)。
 
というわけでConcurrent.Threadすごい!
 
以上!

追記:
async/awaitでマルチスレッドっぽく書くのは無理、というようなことを書きましたが、以下の記事の冒頭にあるように、

ES2017 async/await で sleep 処理を書く

スレッドの実行権を手放すタイプのsleep関数がPromiseを使えば書けるので、そのsleep関数を定期的に呼び出してawaitするようにすれば、ノンプリエンプティブなタイプのスレッドみたいなものは実現できそうです(Fiberと呼ぶのは違うんだよな多分)。

追記2:
async/await と Promise を使って、なんちゃってマルチスレッドを書いてみました。
2つの疑似スレッドが10msごとにコンテキストスイッチ?して、コンソールに自分だ!という感じの内容の出力を行います。
処理は20秒で終了します(その前にrepl.itに打ち切られるかも)
 
https://repl.it/@ryo_grid/trypseudomultithreadwithasyncawait
 
なお、node.js の v10.13.0 でも動作することを確認しました。
全部まとめたコードも貼っておきますね。

const sleep = msec => new Promise(resolve => setTimeout(resolve, msec));

function get_unixtime_msec(){
  var date = new Date() ;
  var cur_date = date.getTime() ;
  return cur_date;
}

async function th1(){
  var start_time = get_unixtime_msec();
  var past_time = get_unixtime_msec();
  var cur_time = null;
  while(1){
    cur_time = get_unixtime_msec()
    if(cur_time - past_time >= 10){ // if there is diff longer than 1 sec
      if(cur_time - start_time >= 10 * 1000){
        // finish this thread if 20sec elapsed
        break;
      }
      await sleep(10);
      past_time = get_unixtime_msec();
    }
    console.log("I am th1!!!");
  }
}

async function th2(){
  var start_time = get_unixtime_msec();
  var past_time = get_unixtime_msec();
  var cur_time = null;
  while(1){
    cur_time = get_unixtime_msec()
    if(cur_time - past_time >= 10){ // if there is diff longer than 1 sec
      if(cur_time - start_time >= 10 * 1000){
        // finish this thread if 20sec elapsed
        break;
      }
      await sleep(10);
      past_time = get_unixtime_msec();
    }
    console.log("I am th2!!!");
  }
}

async function exec_two_threads(){
  th1();
  await th2();
}

function success(result){
  console.log("successfully two thread finished.");
}

function failed(error){
  console.log(error)
  console.log("thread execution is failed.");
}

// get Promise object
var two_thread_promise = exec_two_threads();

// wait finish of two threds
two_thread_promise.then(success, failed);

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

Chromeのストレージ永続化仕様を詳しく追ってみた

1. はじめに

ChromeやFirefoxで使えるストレージ永続化機能はご存知でしょうか。

https://developers.google.com/web/updates/2016/06/persistent-storagePersistent Storage という題で詳しく触れられていますが、通常 LRU で消去されてしまう IndexedDBCacheStorage の内容を永続化してくれる機能だと理解しています。

CacheStoragePersistent Storage の組み合わせは強力で、理論上大容量のアセットを永続化できるようになることから、PWAでのオフライン戦略や通信量削減戦略に対して強力な武器になると考えています。

しかし残念ながら、例によって Safari はサポートしていません。そのため、WebKit の利用を強制される iOS では、例え Chrome を使っていたとしてもこの恩恵を受けることはできません。

ただし Android では可能なので、Android だけでもユーザー体験を良くしたいという場合には効果があると考えています。

実際に自分が開発しているサービスでも Android だけ利用可能にしようと開発していましたが、実装・検証していく中で上で紹介した記事には言及されていない挙動をとる場合があることに気づきました。

具体的には、ストレージ永続化をおこなうためには window.navigator.storage.persist() という API をたたいてストレージ永続化用のパーミッションを取得しなければならず、パーミッション取得のために必要な条件がいくつか存在するのですが、その条件がどうにも記事中のものとは異なる気がしたのです。

そこで今回、検証中に遭遇した謎挙動の理由を探るべく chromium のソースを追ったり、デバッグ実行しながら詳細な挙動の確認をおこなったことで理由が掴めた気がしたので、忘れないうちにまとめようと思いました。

本記事では、 Persistent Storage 機能を利用するためにクリアしなければいけない条件を整理するために、 Google 側が記事中で提示している条件を挙げたあと、開発・検証中に遭遇した条件にそぐわない挙動を挙げ、chromium ソースコードの該当箇所を示しながらなぜそういった挙動になったのかを解き明かし、最終的により具体的なパーミッション取得条件を示します。

2. 公式で挙げられているパーミッション取得のための条件

条件を挙げる前に、この機能がサポートされたのが Chrome 55 からであるため、それ以前のバージョンではサポート外にしたほうが良いでしょう。

ですので、まず API を利用できるかどうかの判断として

  • Chrome 55 以上
  • window.navigator.storage.persistwindow.navigator.storage.persisted が存在する

を判定条件とするのが良さそうです。

これをクリアしたブラウザで、次に示す条件をひとつでもクリアするとストレージ永続化が利用できるようになります。

  • The site is bookmarked (and the user has 5 or less bookmarks)
  • The site has high site engagement
  • The site has been added to home screen
  • The site has push notifications enabled

https://developers.google.com/web/updates/2016/06/persistent-storage から引用

つまり

  • 対象サイトがブックマークされていること ( ただしブックマークの数は 5以下 でなければならない )
  • 対象サイトが high レベルのエンゲージメントスコアを出していること
  • 対象サイトが ホームスクリーン に追加されていること
  • 対象サイトがPUSH通知を許可していること

と書かれています。 site engagement というのは https://www.chromium.org/developers/design-documents/site-engagement で言及されているのですが、 chrome://site-engagement で確認することのできる各サイトのスコア ( 0 ~ 100 の間 ) です。スコアの計算方法は先に挙げた記事中で書かれていますが、サイト上でタッチ操作やスクロール操作をおこなったり、対象サイトをURL直打ちで開いたりすると上がります。ようは その人にとってどれだけ時間を割いて利用しているサイトかどうか というのを定量的に測ろうとした仕組みということで、例えば Google側がもっている対象サイトにおける絶対評価 とかではないというところがポイントになります。

つまり人によって超有名サイトであってもスコアが低くなる可能性はあるし、逆に誰も知らないような個人サイトであってもスコアが高くなる可能性はあります。

さて、この時点ですでに疑問が湧きました。 high って具体的にいくつだろう...?

3. 開発・検証する中で遭遇した謎挙動

3.1 high の定義がわからない

パーミッション取得のための条件は、 先に挙げた条件のどれかひとつでも満たしていれば良い。と書かれています。つまり、ブックマークしておらず、ホームスクリーンにも追加しておらず、PUSH通知の許可もしていないサイトであれば

  • 対象サイトが high レベルのエンゲージメントスコアを出していること

を満たさなければならないはずです。スコアは数値であり、 high という閾値がいくつなのかわからないにせよ、

50 でパーミッションが取得でき、 30 で取得できず、 10 で取得できるなんてことにはならないはずです。....ならない...はずなのです。

ですが実際は上記のようなケースが存在したため、 high の定義がなんなのか更にわからなくなりました。

3.2 どれかひとつの条件を満たせばいいという話ではなかったのか

いったんエンゲージメントスコアのことは忘れて、今度はブックマークを試してみました。

もちろんブックマーク数は 5 つ以下におさえた状態であるサイトを登録しました。

window.navigator.storage.persist() をたたくと true が返ってきます、成功です。

つづけて別のサイトをブックマークしてみました ( まだ 5 つ以下です )。

... false が返ってきました。

ブックマークした A というサイトも B というサイトも、他の条件はほぼ同じです。エンゲージメントスコアは共に 3 程度。ブックマーク前の状態で両方のサイトとも window.navigator.storage.persist() の結果で false が返ってくることも確認していました。

となると、条件を満たしていたとしてもダメな場合があるとしか想像できません。

何も信用できなくなってきました。

同様の現象は、自分では検証していないのですが Android 端末でホームスクリーンに追加した場合にも起きたようです。

Aという端末ではホームスクリーンに追加したとたん利用可能になったが、Bではホームスクリーンに追加してあるのにダメだったとのことでした。

4. ソースコードから仕様を追う

明らかにドキュメント化されていない仕様がありそうな気配を感じたので、 chromium のソースを落としてきてビルド、lldbでデバッグしたり printデバッグなどをおこないました。

(余談) 自分は macOS で作業したのですが、当初 GitHub 上の mirror からソースを落としてきてビルドしようと試みて途中でビルドエラーで怒られてしまったので、 https://chromium.googlesource.com/chromium/src/+/master/docs/mac_build_instructions.md にならって素直にその通りやるのがよかったです

4.1. 該当箇所のソースコードを見つける

今回どうやって window.navigator.storage.persist() の実装箇所を見つけたのか、その過程をできるだけ詳細に残しておこうと思います。ソースコードを追っていく流れをそのまま書いているので、過程に興味がない方はこの章をスキップしていただくと、ピンポイントで関係のあるソースコードの箇所からの説明からになります。

まずはじめに、 window.navigator.storage.persist() をたたいたときに呼ばれるソースコードを見てみます。これは https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/modules/quota/storage_manager.cc#L82-L104 に書かれていて、中で RequestPermission を呼んでいるのがわかります。

ソースコードの読み始めのポイントとしては、 JavaScript 側でのAPI呼び出しに対応するC++側のAPIが必ずあるはずなので、その処理を見つけることです。今回であれば persist という名前で検索すればある程度しぼりこむことができます。

  • storage_manager.cc
ScriptPromise StorageManager::persist(ScriptState* script_state) {
  auto* resolver = MakeGarbageCollected<ScriptPromiseResolver>(script_state);
  ScriptPromise promise = resolver->Promise();
  ExecutionContext* execution_context = ExecutionContext::From(script_state);
  DCHECK(execution_context->IsSecureContext());  // [SecureContext] in IDL
  const SecurityOrigin* security_origin =
      execution_context->GetSecurityOrigin();
  if (security_origin->IsOpaque()) {
    resolver->Reject(V8ThrowException::CreateTypeError(
        script_state->GetIsolate(), kUniqueOriginErrorMessage));
    return promise;
  }

  Document* doc = To<Document>(execution_context);
  GetPermissionService(ExecutionContext::From(script_state))
      .RequestPermission(
          CreatePermissionDescriptor(PermissionName::DURABLE_STORAGE),
          LocalFrame::HasTransientUserActivation(doc->GetFrame()),
          WTF::Bind(&StorageManager::PermissionRequestComplete,
                    WrapPersistent(this), WrapPersistent(resolver)));

  return promise;
}

ここでひとつ重要なことがわかりました。 chromium の中で Persistent Storage のパーミッションを扱う際のキーワードが DURABLE_STORAGE だということです。

RequestPermission の実装は https://github.com/chromium/chromium/blob/master/content/browser/permissions/permission_service_impl.cc#L162-L171 にあり

読み進めていくと
https://github.com/chromium/chromium/blob/master/content/browser/permissions/permission_service_impl.cc#L215-L222 のあたりで

  • permission_service_impl.cc
int id =
      PermissionControllerImpl::FromBrowserContext(browser_context)
          ->RequestPermissions(
              types, context_->render_frame_host(), origin_.GetURL(),
              user_gesture,
              base::Bind(&PermissionServiceImpl::OnRequestPermissionsResponse,
                         weak_factory_.GetWeakPtr(), pending_request_id));

に気づきます。ここで BrowserContext なるものを使って PermissionController 経由で RequestPermissions を呼び出しています。 PermissionController::RequestPermissions の処理は https://github.com/chromium/chromium/blob/master/content/browser/permissions/permission_controller_impl.cc#L199-L229 に書かれていて、 PermissionControllerDelegate のインスタンスを BrowserContextGetPermissionControllerDelegate() 経由で取得し、delegateRequestPermissions を呼んでいます。

  • permission_controller_impl.cc
int PermissionControllerImpl::RequestPermissions(
    const std::vector<PermissionType>& permissions,
    RenderFrameHost* render_frame_host,
    const GURL& requesting_origin,
    bool user_gesture,
    const base::Callback<
        void(const std::vector<blink::mojom::PermissionStatus>&)>& callback) {
  for (PermissionType permission : permissions)
    NotifySchedulerAboutPermissionRequest(render_frame_host, permission);

  auto it = devtools_permission_overrides_.find(requesting_origin.GetOrigin());
  if (it != devtools_permission_overrides_.end()) {
    std::vector<blink::mojom::PermissionStatus> result;
    for (auto& permission : permissions)
      result.push_back(GetPermissionOverrideStatus(it->second, permission));
    callback.Run(result);
    return kNoPendingOperation;
  }

  PermissionControllerDelegate* delegate =
      browser_context_->GetPermissionControllerDelegate();
  if (!delegate) {
    std::vector<blink::mojom::PermissionStatus> result(
        permissions.size(), blink::mojom::PermissionStatus::DENIED);
    callback.Run(result);
    return kNoPendingOperation;
  }
  return delegate->RequestPermissions(permissions, render_frame_host,
                                      requesting_origin, user_gesture,
                                      callback);
}

この GetPermissionControllerDelegateBrowserContext では pure virtual として宣言されており、継承先で実装するようになっています。継承先はブラウザの種類によって多岐にわたり、 chromecast や headless browser などいくつかの実装があるようです。

今回見たいのはおそらく https://github.com/chromium/chromium/blob/master/chrome/browser/profiles/profile_impl.cc#L1215-L1217 で実装されているところで、中で PermissionManagerFactory をつかっているのがわかります。

  • profile_impl.cc
content::PermissionControllerDelegate*
ProfileImpl::GetPermissionControllerDelegate() {
  return PermissionManagerFactory::GetForProfile(this);
}

PermissionManagerFactory::GetForProfile の実装は https://github.com/chromium/chromium/blob/master/chrome/browser/permissions/permission_manager_factory.cc#L14-L18 に書かれており、 GetServiceForBrowserContext を通して PermissionManager インスタンスを作っていることがわかります。 つまり PermissionControllerDelegate の正体は PermissionManager クラスで、結局のところこのクラスの RequestPermissions を呼んでいることがわかりました。

  • permission_manager_factory.cc
PermissionManager*
PermissionManagerFactory::GetForProfile(Profile* profile) {
  return static_cast<PermissionManager*>(
      GetInstance()->GetServiceForBrowserContext(profile, true));
}

PermissionManager::RequestPermissions の実装は https://github.com/chromium/chromium/blob/master/chrome/browser/permissions/permission_manager.cc#L393-L445 に書かれており、読み進めると

  • permission_manager.cc
 PermissionContextBase* context = GetPermissionContext(permission);
    DCHECK(context);

    context->RequestPermission(
        web_contents, request, canonical_requesting_origin, user_gesture,
        base::BindOnce(
            &PermissionResponseCallback::OnPermissionsRequestResponseStatus,
            std::move(response_callback)));

GetPermissionContextpermission を引数に ( ここでは DURABLE_STORAGE ) 呼んでいるのがわかります。 GetPermissionContext の実装は

PermissionContextBase* PermissionManager::GetPermissionContext(
    ContentSettingsType type) {
  const auto& it = permission_contexts_.find(type);
  return it == permission_contexts_.end() ? nullptr : it->second.get();
}

のようになっていて、 ContentSettingsTypeCONTENT_SETTINGS_TYPE_DURABLE_STORAGE で検索していることがわかります。ではこの permission_contexts_ がどうやって初期化されているかというと、 https://github.com/chromium/chromium/blob/master/chrome/browser/permissions/permission_manager.cc#L280-L334 に初期化処理があり

PermissionManager::PermissionManager(Profile* profile) : profile_(profile) {
  ... () ...
  permission_contexts_[CONTENT_SETTINGS_TYPE_DURABLE_STORAGE] =
      std::make_unique<DurableStoragePermissionContext>(profile);
  ... () ...
}

正体が DurableStoragePermissionContext だとわかりました!
では この実装を読みにいきます。

4.2 該当箇所のソースを読む

window.navigator.storage.persist() を呼んだ時にめぐりめぐって呼ばれるのは DurableStoragePermissionContext::DecidePermission という部分です。

https://github.com/chromium/chromium/blob/master/chrome/browser/storage/durable_storage_permission_context.cc#L35-L99

  • durable_storage_permission_context.cc
void DurableStoragePermissionContext::DecidePermission(
    content::WebContents* web_contents,
    const PermissionRequestID& id,
    const GURL& requesting_origin,
    const GURL& embedding_origin,
    bool user_gesture,
    BrowserPermissionCallback callback) {
  DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
  DCHECK_NE(CONTENT_SETTING_ALLOW,
            GetPermissionStatus(nullptr /* render_frame_host */,
                                requesting_origin, embedding_origin)
                .content_setting);
  DCHECK_NE(CONTENT_SETTING_BLOCK,
            GetPermissionStatus(nullptr /* render_frame_host */,
                                requesting_origin, embedding_origin)
                .content_setting);

  // Durable is only allowed to be granted to the top-level origin. Embedding
  // origin is the last committed navigation origin to the web contents.
  if (requesting_origin != embedding_origin) {
    NotifyPermissionSet(id, requesting_origin, embedding_origin,
                        std::move(callback), false /* persist */,
                        CONTENT_SETTING_DEFAULT);
    return;
  }

  scoped_refptr<content_settings::CookieSettings> cookie_settings =
      CookieSettingsFactory::GetForProfile(profile());

  // Don't grant durable for session-only storage, since it won't be persisted
  // anyway. Don't grant durable if we can't write cookies.
  if (cookie_settings->IsCookieSessionOnly(requesting_origin) ||
      !cookie_settings->IsCookieAccessAllowed(requesting_origin,
                                              requesting_origin)) {
    NotifyPermissionSet(id, requesting_origin, embedding_origin,
                        std::move(callback), false /* persist */,
                        CONTENT_SETTING_DEFAULT);
    return;
  }

  const size_t kMaxImportantResults = 10;
  std::vector<ImportantSitesUtil::ImportantDomainInfo> important_sites =
      ImportantSitesUtil::GetImportantRegisterableDomains(profile(),
                                                          kMaxImportantResults);

  std::string registerable_domain =
      net::registry_controlled_domains::GetDomainAndRegistry(
          requesting_origin,
          net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
  if (registerable_domain.empty() && requesting_origin.HostIsIPAddress())
    registerable_domain = requesting_origin.host();

  for (const auto& important_site : important_sites) {
    if (important_site.registerable_domain == registerable_domain) {
      NotifyPermissionSet(id, requesting_origin, embedding_origin,
                          std::move(callback), true /* persist */,
                          CONTENT_SETTING_ALLOW);
      return;
    }
  }

  NotifyPermissionSet(id, requesting_origin, embedding_origin,
                      std::move(callback), false /* persist */,
                      CONTENT_SETTING_DEFAULT);
}

このうち、 persist() の結果で true を返してくれるのは

const size_t kMaxImportantResults = 10;
  std::vector<ImportantSitesUtil::ImportantDomainInfo> important_sites =
      ImportantSitesUtil::GetImportantRegisterableDomains(profile(),
                                                          kMaxImportantResults);

  std::string registerable_domain =
      net::registry_controlled_domains::GetDomainAndRegistry(
          requesting_origin,
          net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
  if (registerable_domain.empty() && requesting_origin.HostIsIPAddress())
    registerable_domain = requesting_origin.host();

  for (const auto& important_site : important_sites) {
    if (important_site.registerable_domain == registerable_domain) {
      NotifyPermissionSet(id, requesting_origin, embedding_origin,
                          std::move(callback), true /* persist */,
                          CONTENT_SETTING_ALLOW);
      return;
    }
  }

の部分で、 ImportantSitesUtil::GetImportantRegisterableDomains で返ってきた結果の中に、対象サイトから取得した registerable_domain が存在していれば true が返っているとわかります。ここで非常に重要なことは、 ImportantSitesUtil::GetImportantRegisterableDomains の結果が 最大10件 だということ。もうひとつは、 registerable_domain の決定方法 です。

registerable_domain は、ドメイン名によるでしょうが第2・第3レベルまでのドメイン名になるようです。例えば、 https://mail.google.com であれば google.comregisterable_domain になり、 https://www.google.co.jp の場合は google.co.jp になります。このルールが先に挙げた謎の挙動のヒントになってきます。

つづいて、ImportantSitesUtil::GetImportantRegisterableDomains を見にいきます。
実装は https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/important_sites_util.cc#L383-L424 に書かれています

  • important_sites_util.cc
std::vector<ImportantDomainInfo>
ImportantSitesUtil::GetImportantRegisterableDomains(Profile* profile,
                                                    size_t max_results) {
  std::map<std::string, ImportantDomainInfo> important_info;
  std::map<GURL, double> engagement_map;

  PopulateInfoMapWithEngagement(profile, blink::mojom::EngagementLevel::MEDIUM,
                                &engagement_map, &important_info);

  PopulateInfoMapWithContentTypeAllowed(
      profile, CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
      ImportantReason::NOTIFICATIONS, &important_info);

  PopulateInfoMapWithContentTypeAllowed(
      profile, CONTENT_SETTINGS_TYPE_DURABLE_STORAGE, ImportantReason::DURABLE,
      &important_info);

  PopulateInfoMapWithBookmarks(profile, engagement_map, &important_info);

  std::unordered_set<std::string> blacklisted_domains =
      GetBlacklistedImportantDomains(profile);

  std::vector<std::pair<std::string, ImportantDomainInfo>> items(
      important_info.begin(), important_info.end());
  std::sort(items.begin(), items.end(), &CompareDescendingImportantInfo);

  std::vector<ImportantDomainInfo> final_list;
  for (std::pair<std::string, ImportantDomainInfo>& domain_info : items) {
    if (final_list.size() >= max_results)
      return final_list;
    if (blacklisted_domains.find(domain_info.first) !=
        blacklisted_domains.end()) {
      continue;
    }
    final_list.push_back(domain_info.second);
    RECORD_UMA_FOR_IMPORTANT_REASON(
        "Storage.ImportantSites.GeneratedReason",
        "Storage.ImportantSites.GeneratedReasonCount",
        domain_info.second.reason_bitfield);
  }

  return final_list;
}

ここではじめに紹介したパーミッション取得のための条件を思い出します。

  • 対象サイトがブックマークされていること ( ただしブックマークの数は 5以下 でなければならない )
  • 対象サイトが high レベルのエンゲージメントスコアを出していること
  • 対象サイトが ホームスクリーン に追加されていること
  • 対象サイトがPUSH通知のパーミッションを許可していること

上記の条件がまさにコード中に書かれているのが分かります。
ざっと処理の流れを説明すると

std::map<std::string, ImportantDomainInfo> important_info;

に対してひとつずつ条件を照らし合わせながら該当するものを追加していき、

CompareDescendingImportantInfo のルールにのっとってこれを sort した後、blacklist にのっているドメインを除きつつ、最大10件になるように final_list を構築して返却しています。

ここで先ほど 最大10件 というのが重要だと説明しましたが、 important_info に10件より多くの registerable_domain が入っていたとしても、

sortの結果によって11件目以降になると、 final_list の中には現れないということがわかります。

つまり、具体的にどういった条件で important_info に追加されていくのかという情報と同じかそれ以上に、 sort の条件が気になります。

CompareDescendingImportantInfo の実装はどうなっているでしょうか。

https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/important_sites_util.cc#L192-L203 に書かれており

bool CompareDescendingImportantInfo(
    const std::pair<std::string, ImportantDomainInfo>& a,
    const std::pair<std::string, ImportantDomainInfo>& b) {
  int score_a = GetScoreForReasonsBitfield(a.second.reason_bitfield);
  int score_b = GetScoreForReasonsBitfield(b.second.reason_bitfield);
  int bitfield_diff = score_a - score_b;
  if (bitfield_diff != 0)
    return bitfield_diff > 0;
  return a.second.engagement_score > b.second.engagement_score;
}

ImportantDomainInfo に含まれる reason_bitfield の値によって得たスコアどうしを比較しており、これが大きい方が優先されるとわかります。
もし reason_bitfield が同じ値の場合は、純粋な engagement_score ( chrome://site-engagement で見れる値 ) の大きい方が優先されるようです。 

reason_bitfield の値は、複数の reason に分解した後、ひとつずつ以下の GetScoreForReason を通してスコアに変換して足し合わせています。

int GetScoreForReason(ImportantReason reason) {
  switch (reason) {
    case ImportantReason::ENGAGEMENT:
      return 1 << 0;
    case ImportantReason::DURABLE:
      return 1 << 1;
    case ImportantReason::BOOKMARKS:
      return 1 << 2;
    case ImportantReason::HOME_SCREEN:
      return 1 << 3;
    case ImportantReason::NOTIFICATIONS:
      return 1 << 4;
    case ImportantReason::REASON_BOUNDARY:
      return 0;
  }
  return 0;
}

たとえば、 ImportantReason::ENGAGEMENTImportantReason::DURABLE を同時に満たすreason_bitfield のスコアは 3 になります。

ここからわかることは、各条件にはそれぞれ優先度が存在し、例えばブックマークに追加することよりもホームスクリーンに追加したりPUSH通知を許可するほうが優先的に選ばれることになるとわかります。

更に、条件は重ねて適応することができ、例えば同じブックマークに追加したサイトであっても ImportantReason::ENGAGEMENT を満たしているかでスコアが 1 点 変わることがわかります。

ここまででだいぶ理解は進みましたが、最後に各条件がより具体的にどういったことを求めているかを見ていきたいと思います ( engagement score の high レベルの定義も気になります )。

4.2.1 high site engagement の意味を知る

PopulateInfoMapWithEngagement の中を見ます (
https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/important_sites_util.cc#L236-L270 )

void PopulateInfoMapWithEngagement(
    Profile* profile,
    blink::mojom::EngagementLevel minimum_engagement,
    std::map<GURL, double>* engagement_map,
    std::map<std::string, ImportantDomainInfo>* output) {
  SiteEngagementService* service = SiteEngagementService::Get(profile);
  std::vector<mojom::SiteEngagementDetails> engagement_details =
      service->GetAllDetails();
  std::set<GURL> content_origins;

  // We can have multiple origins for a single domain, so we record the one
  // with the highest engagement score.
  for (const auto& detail : engagement_details) {
    if (detail.installed_bonus > 0) {
      // This origin was recently launched from the home screen.
      MaybePopulateImportantInfoForReason(detail.origin, &content_origins,
                                          ImportantReason::HOME_SCREEN, output);
    }

    (*engagement_map)[detail.origin] = detail.total_score;

    if (!service->IsEngagementAtLeast(detail.origin, minimum_engagement))
      continue;

    std::string registerable_domain =
        ImportantSitesUtil::GetRegisterableDomainOrIP(detail.origin);
    ImportantDomainInfo& info = (*output)[registerable_domain];
    if (detail.total_score > info.engagement_score) {
      info.registerable_domain = registerable_domain;
      info.engagement_score = detail.total_score;
      info.example_origin = detail.origin;
      info.reason_bitfield |= 1 << ImportantReason::ENGAGEMENT;
    }
  }
}

engagement_details というのは、 chrome://site-engagement で見れるリストが返ってきていると思ってもらっても大丈夫です。このリストを traverse しながら、 installed_bonus の値が 0 より大きければ ホームスクリーン追加用の reason_bitfield ( ImportantReason::HOME_SCREEN ) を追加しつつ、 minimum_engagement 以上のスコアであるもののみに reason_bitfield ( ImportantReason::ENGAGEMENT ) を追加します。

ここで high の具体的な条件がわかりました。 minimum_engagement です。これは引数で与えられたものだったので、呼び出し元に戻ると blink::mojom::EngagementLevel::MEDIUM を渡していることがわかります。この上で IsEngagementAtLeast の実装を読んでみます。

https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/site_engagement_service.cc#L248-L270 に書かれており、 blink::mojom::EngagementLevel::MEDIUM に相当する条件が score >= SiteEngagementScore::GetMediumEngagementBoundary() だとわかります。

  • site_engagement_service.cc
bool SiteEngagementService::IsEngagementAtLeast(
    const GURL& url,
    blink::mojom::EngagementLevel level) const {
  DCHECK_LT(SiteEngagementScore::GetMediumEngagementBoundary(),
            SiteEngagementScore::GetHighEngagementBoundary());
  double score = GetScore(url);
  switch (level) {
    case blink::mojom::EngagementLevel::NONE:
      return true;
    case blink::mojom::EngagementLevel::MINIMAL:
      return score > 0;
    case blink::mojom::EngagementLevel::LOW:
      return score >= 1;
    case blink::mojom::EngagementLevel::MEDIUM:
      return score >= SiteEngagementScore::GetMediumEngagementBoundary();
    case blink::mojom::EngagementLevel::HIGH:
      return score >= SiteEngagementScore::GetHighEngagementBoundary();
    case blink::mojom::EngagementLevel::MAX:
      return score == SiteEngagementScore::kMaxPoints;
  }
  NOTREACHED();
  return false;
}

そこで SiteEngagementScore::GetMediumEngagementBoundary() を読みにいくと
https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/site_engagement_score.cc#L146-L148

  • site_engagement_score.cc
double SiteEngagementScore::GetMediumEngagementBoundary() {
  return GetParamValues()[MEDIUM_ENGAGEMENT_BOUNDARY].second;
}

と書かれており、
https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/site_engagement_score.cc#L85-L86

から 15 点が閾値だとわかりました!

ここで先ほど軽く説明して終わったホームスクリーンへの追加についても深掘りしたいと思います。

4.2.2 「ホームスクリーンに追加」の具体的な条件を知る

4.2.1 の PopulateInfoMapWithEngagement の実装の説明の中で、 installed_bonus > 0 であれば ImportantReason::HOME_SCREENreason_bitfield に足されると説明しましたが、では具体的に installed_bonus が 0 より大きくなるためにはどうなればいいのでしょうか。

これを知るためには

 std::vector<mojom::SiteEngagementDetails> engagement_details =
      service->GetAllDetails();

の中を知る必要があります。
この処理は最終的にサイトごとに SiteEngagementScore::GetDetails() が呼ばれるのですが、 https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/site_engagement_score.cc#L265-L276 のあたりを見ると、どうやって mojom::SiteEngagementDetails ができているのかがわかります

  • site_engagement_score.cc
double SiteEngagementScore::GetTotalScore() const {
  return std::min(DecayedScore() + BonusIfShortcutLaunched(), kMaxPoints);
}

mojom::SiteEngagementDetails SiteEngagementScore::GetDetails() const {
  mojom::SiteEngagementDetails engagement;
  engagement.origin = origin_;
  engagement.base_score = DecayedScore();
  engagement.installed_bonus = BonusIfShortcutLaunched();
  engagement.total_score = GetTotalScore();
  return engagement;
}

ここで注目したいのは BonusIfShortcutLaunched() です。この結果が installed_bonus の値にもなるし、 total_score にも反映されます。

double SiteEngagementScore::BonusIfShortcutLaunched() const {
  int days_since_shortcut_launch =
      (clock_->Now() - last_shortcut_launch_time_).InDays();
  if (days_since_shortcut_launch <= kMaxDaysSinceShortcutLaunch)
    return GetWebAppInstalledPoints();
  return 0;
}

実装は上記のようになっていて、kMaxDaysSinceShortcutLaunchhttps://github.com/chromium/chromium/blob/master/chrome/browser/engagement/site_engagement_score.cc#L34 に書かれていますが 10 です。

つまり最後にホームスクリーン上に追加したアプリから起動したのが10日以内であれば GetWebAppInstalledPoints() ( ちなみに 5 点です https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/site_engagement_score.cc#L81 ) を付与するよということを表しています。

ここから、例えホームスクリーンに追加していたとしても、10日以上起動していなければインストールボーナスはつかない。つまり reason_bitfieldImportantReason::HOME_SCREEN は追加しないということがわかります。

ここまででもう一度 ImportantSitesUtil::GetImportantRegisterableDomains の実装に戻りましょう。 4.2.1 と 4.2.2 では PopulateInfoMapWithEngagement の中を具体的にみていきました。

そのあとに書かれている

PopulateInfoMapWithContentTypeAllowed(
      profile, CONTENT_SETTINGS_TYPE_NOTIFICATIONS,
      ImportantReason::NOTIFICATIONS, &important_info);

の部分は読んでそのままで、 PUSH通知をすでに許可している場合は、 ImportantReason::NOTIFICATIONSreason_bitfield に加えられます。

続けて書いてある

PopulateInfoMapWithContentTypeAllowed(
      profile, CONTENT_SETTINGS_TYPE_DURABLE_STORAGE, ImportantReason::DURABLE,
      &important_info);

は、 すでにDURABLE_STORAGEの条件をみたしている場合ImportantReason::DURABLEreason_bitfield に加えるという意味になります。つまり、一度でも window.navigatgor.storage.persist() でストレージ永続化できていた場合はここで必ず reason_bitfield に 2点 加算されます。これも registerable_domain の優先度を決める上で非常に重要になってきます。

こうなると残りは

PopulateInfoMapWithBookmarks(profile, engagement_map, &important_info);

だけなので、次でブックマークに関する具体的な条件をみていきます

4.2.3 「ブックマークに追加」の具体的な条件を知る

実装は https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/important_sites_util.cc#L307-L352 に書かれており、ブックマークの数が kMaxBookmarks を超えているかどうかで処理が異なることがわかります。

kMaxBookmarkshttps://github.com/chromium/chromium/blob/master/chrome/browser/engagement/important_sites_util.cc#L61 に書かれていますが 5 です。

つまり、条件になっていた「ブックマーク数が5以下だったら」の数ですね。

void PopulateInfoMapWithBookmarks(
    Profile* profile,
    const std::map<GURL, double>& engagement_map,
    std::map<std::string, ImportantDomainInfo>* output) {
  SiteEngagementService* service = SiteEngagementService::Get(profile);
  BookmarkModel* model =
      BookmarkModelFactory::GetForBrowserContextIfExists(profile);
  if (!model)
    return;
  std::vector<UrlAndTitle> untrimmed_bookmarks;
  model->GetBookmarks(&untrimmed_bookmarks);

  // Process the bookmarks and optionally trim them if we have too many.
  std::vector<UrlAndTitle> result_bookmarks;
  if (untrimmed_bookmarks.size() > kMaxBookmarks) {
    std::copy_if(untrimmed_bookmarks.begin(), untrimmed_bookmarks.end(),
                 std::back_inserter(result_bookmarks),
                 [service](const UrlAndTitle& entry) {
                   return service->IsEngagementAtLeast(
                       entry.url.GetOrigin(),
                       blink::mojom::EngagementLevel::LOW);
                 });
    // TODO(dmurph): Simplify this (and probably much more) once
    // SiteEngagementService::GetAllDetails lands (crbug/703848), as that will
    // allow us to remove most of these lookups and merging of signals.
    std::sort(
        result_bookmarks.begin(), result_bookmarks.end(),
        [&engagement_map](const UrlAndTitle& a, const UrlAndTitle& b) {
          auto a_it = engagement_map.find(a.url.GetOrigin());
          auto b_it = engagement_map.find(b.url.GetOrigin());
          double a_score = a_it == engagement_map.end() ? 0 : a_it->second;
          double b_score = b_it == engagement_map.end() ? 0 : b_it->second;
          return a_score > b_score;
        });
    if (result_bookmarks.size() > kMaxBookmarks)
      result_bookmarks.resize(kMaxBookmarks);
  } else {
    result_bookmarks = std::move(untrimmed_bookmarks);
  }

  std::set<GURL> content_origins;
  for (const UrlAndTitle& bookmark : result_bookmarks) {
    MaybePopulateImportantInfoForReason(bookmark.url, &content_origins,
                                        ImportantReason::BOOKMARKS, output);
  }
}

ブックマーク数が5以下の場合は、 result_bookmarks にそのままそれらが入り、 ImportantReason::BOOKMARKSreason_bitfield に追加されます。

そうでない場合は 全ブックマークの中から blink::mojom::EngagementLevel::LOW ( https://github.com/chromium/chromium/blob/master/chrome/browser/engagement/site_engagement_service.cc#L259-L260 から 1 ) 以上のスコアのものを集めたあと、 engagement_score の値によって sort し、上から 5件 を result_bookmarks に追加するという挙動になるようです。

つまり、5件以下であればそのまま ImportantReason::BOOKMARKS が追加され、そうでなかったとしても、ブックマークしたサイトのスコアが 1 以上であり、かつスコアの大きい順の上位5つ以内に入っていれば同様に ImportantReason::BOOKMARKSreason_bitfield に足されるとわかります。

5. 謎だった挙動を解明する

4章でソースコードを追いながら詳細な仕様を把握できました。

このことから不可解だった次の挙動にも説明ができるようになります。

  • スコアが 50 でパーミッションが取得でき、 30 で取得できず、 10 で取得できてしまう
  • ブックマークした A というサイトと B というサイトがあるが A しかパーミッションを取得できない
  • ホームスクリーンに追加してもパーミッションを取得できない

まず条件をあらためて整理します。

もともとの条件はこうでした。

  • 対象サイトがブックマークされていること ( ただしブックマークの数は 5以下 でなければならない )
  • 対象サイトが high レベルのエンゲージメントスコアを出していること
  • 対象サイトが ホームスクリーン に追加されていること
  • 対象サイトがPUSH通知のパーミッションを許可していること

その上、調べていくと次のことがわかりました

  • パーミッションを許可できるのは上位10件のサイトまで
  • 別のサイトであっても registerable_domain は被る可能性がある
  • エンゲージメントスコアだけを条件にする場合は15点以上必要
  • ホームスクリーンに追加することを条件にするなら、最後に起動してから10日以内でなければならない
  • すでにパーミッションを与えているサイトには reason_bitfield2 点が加わる
  • ブックマークが5件を超える場合は、スコアが1点以上のサイトを対象に上位5件に対して reason_bitfield4 点を加える

これをふまえると

  • スコアが 50 でパーミッションが取得でき、 30 で取得できず、 10 で取得できてしまう

この場合は、 50 でパーミションがとれたのはスコアが 15 点以上だったからなのは良いとして、 10 でも取得できたのはドメイン名が 50 のものと同じだった可能性が高いです( 実際の自分の場合は https://mail.google.com が上位にきていて、 10 のは別の google.com ドメインのサイトでした )。逆になぜ 30 でもパーミッションがとれなかったのかは、上位 10件 に漏れてしまっていたからだと考えられます。

罠だったのは、検証のために複数のサイトで window.navigator.storage.persist() を実行してまわっていったため、一度許可された場合に DURABLE_STORAGE の条件が満たされ、エンゲージメントスコアが 15点以上のサイトは常に reason_bitfield3 になるという挙動をとる点でした ( ImportantReason::ENGAGEMENT が 1点、 ImportantReason::DURABLE_STORAGE が 2点 )。この条件を満たしたものが 10 件を超えていると、新しいサイトでパーミッションを取得しようとした場合に、仮に 15点以上だったとしても、 reason_bitfield の値が ImportantReason::ENGAGEMENT だけのため ( 1 点 )、永遠にパーミッションを許可できない状態になっていました。

同様に

  • ブックマークした A というサイトと B というサイトがあるが A しかパーミッションを取得できない

この件でも、すでに対象のサイトが10件を超えている場合に問題になります。

もしブックマークの条件が満たされていたとしても、サイトのスコアが 15 点未満の場合は ImportantReason::BOOKMARKS ( 4 点 ) しか適応されていません。

このとき、他にホームスクリーンに追加したアプリが複数あったり、ブックマークとスコア15以上を同時に満たすサイトが10件以上ある状況だと、例えブックマークの登録が5件以内だったとしてもパーミッションを取得できない事態になります。

  • ホームスクリーンに追加してもパーミッションを取得できない

この場合は、ホームスクリーンへ追加することの優先度がとても高いことから、他のサイトに優先順位で負けることがあまり考えられないため、アプリを最後に起動したのが10日以上前である可能性が高いです。

6. まとめ

Persistent Storage のパーミッションを取得するための条件は

  • 対象サイトがブックマークされていること ( ただしブックマークの数は 5以下 でなければならない )
  • 対象サイトが high レベルのエンゲージメントスコアを出していること
  • 対象サイトが ホームスクリーン に追加されていること
  • 対象サイトがPUSH通知のパーミッションを許可していること

だけでなく、ソースコードを追うことでさらに細かな条件があることがわかりました。

具体的には

  • パーミッションを許可できるのは上位 10 件の registerable_domain に合致するサイトだけ
  • 別のサイトであっても registerable_domain は被る可能性がある ( a.google.comb.google.com はどちらも google.comregisterable_domain )
  • エンゲージメントスコアだけを条件にする場合は 15 点以上必要
  • ホームスクリーンに追加することを条件にするなら、最後に起動してから 10 日以内でなければならない
  • ブックマークが5件を超える場合は、エンゲージメントスコアが1点以上のサイトを対象に上位5件が加点対象になる
  • エンゲージメントスコアの条件を満たすと reason_bitfield1 点が加わる
  • すでにパーミッションを与えているサイトには reason_bitfield2 点が加わる
  • ブックマークに追加しているサイトにはreason_bitfield4 点を加える
  • ホームスクリーンに追加されているサイトには reason_bitfield8 点が加わる
  • PUSH通知を許可しているサイトには reason_bitfield16 点が加わる
  • reason_bitfield の合計が大きい順に10件を抽出する。 reason_bitfield が同じ場合はエンゲージメントスコアが高い方を優先する

のような条件が裏にあり決められています。

これをふまえると、なかなかサービスの機能としてストレージ永続化を提供するのは難しいなと感じましたが、詳細な条件を知れたことで大分スッキリしましたし、対策も考えやすそうです。

もしすでに謎挙動を観測された経験がある方がいらっしゃれば、
この記事の結論と相違ないかコメントをいただければ幸いです!

7. FAQ ( 追記 )

記事中にわかりにくいなという箇所がいくつかあったので、疑問になりそうなところをあらかじめ解説しておこうと思います。

7.1 異なるサイトで registerable_domain が一緒だった場合の挙動をもう一度

例えば a.google.comb.google.com があって、それぞれエンゲージメントスコアが 201 だったとします。
条件から考えると a.google.com はエンゲージメントスコアが 15 より大きいためパーミッションを取得できますが、 b.google.com では一見ダメそうです。しかし、これらは registerable_domain が同じ google.com なので、 google.com が 許可すべきドメイン 10 件の中に入っていさえすればパーミッションは取得できます

7.2 得点の説明が reason_bitfield と エンゲージメントスコアと 2種類あってわかりにくい

エンゲージメントスコアは chrome://site-engagement でみれる 0 ~ 100 の間の点数です。サイトによって異なります。

この値は、 最大10件の registerable_domain のリストを作るときに利用されます。

また、 reason_bitfield の値も同様にリストを作るときに利用する値ですが、基本的には reason_bitfield の値で 降順に並べ、同じ reason_bitfield のサイトどうしを比較する際にはじめてエンゲージメントスコアを参照する流れです。

エンゲージメントスコアは 15 点以上であるかが基準になっており、 15 以上であれば reason_bitfield に 1点加算されます。

7.3 一度許可したサイトのパーミッションは消せるのか

ブラウザの再起動等では消えないので、別な削除手段をとる必要がありそうです。一度許可してしまうと、常に reason_bitfield に 2点 加算されます。

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

[node.js]Expressでパスワードのハッシュ化を行おう

はじめまして、PMをやっているtatsukenと申します。はじめまして
研修の一環でvue.js、expressを書くことがあったので、そのことを中心にまとめていきたいと思います

はじめに

パスワードをデータベースに保存するときにはその情報をハッシュ化する必要があります。
なぜパスワードをハッシュ化しないといけないかなどを詳しく知りたい方はこちらを参考にしてみてください。
今回はbcryptというライブラリを使ってデータのハッシュ化を行って行きたいと思います。

実装

インストール

npm install bcrypt -s

パスワードのハッシュ化

まずExpress内の自分がデータをハッシュ化したいファイル内で以下のように記述してください

hash.js
const bcrypt = require('bcrypt');
const password = "hoge"
let hashed_password = bcrypt.hashSync(data, 10);
console.log(hash_data)

"hoge"という文字列がランダムな文字に変換されています。

ハッシュ化されたパスワードの照合

ハッシュ化されたパスワードと自分の知っているパスワードの照合を行っていきます

hash_confirm.js
const bcrypt = require('bcrypt');
bcrypt.compareSync(hash_password, "hoge") // =>ture
bcrypt.compareSync(hash_password, "fake_hoge") // =>false
  • hash_dataはもともと"hoge"をハッシュ化したものなので、bcrypt.compareSync(hash_data, "hoge")trueとなります。
  • bcrypt.compareSync(hash_data, "fake_hoge")はハッシュ化される前のデータと比較されているデータ(今回は"hoge"face_hogeを比較している)が異なるのでfalseとなる。

最後に

今回は非常に簡単なhash化の方法をご紹介しました。
もっと詳しいことが知りたい場合はこちらを参照ください。callbackやasync/awaitでエラーハンドリングを行うことも出来ます。
なにか間違いなどありましたら指摘していただけると幸いです。

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

[node.js]Expressでデータのハッシュ化を行おう

はじめまして、PMをやっているtatsukenと申します。はじめまして
研修の一環でvue.js、expressを書くことがあったので、そのことを中心にまとめていきたいと思います

はじめに

パスワードなどの重要な情報をデータベースに保存するときにはその情報をハッシュ化する必要があります。
なぜ重要な情報をハッシュ化しないといけないかなどを詳しく知りたい方はこちらを参考にしてみてください。
今回はbcryptというライブラリを使ってデータのハッシュ化を行って行きたいと思います。

実装

インストール

npm install bcrypt -s

データのハッシュ化

まずExpress内の自分がデータをハッシュ化したいファイル内で以下のように記述してください

hash.js
const bcrypt = require('bcrypt');
const data = "hoge"
let hash_data = bcrypt.hashSync(data, 10);
console.log(hash_data)

"hoge"という文字列がランダムな文字に変換されています。

ハッシュ化されたデータの照合

ハッシュ化されたデータと自分の知っているデータの照合を行っていきます

hash_confirm.js
const bcrypt = require('bcrypt');
bcrypt.compareSync(hash_data, "hoge") // =>ture
bcrypt.compareSync(hash_data, "fake_hoge") // =>false
  • hash_dataはもともと"hoge"をハッシュ化したものなので、bcrypt.compareSync(hash_data, "hoge")trueとなります。
  • bcrypt.compareSync(hash_data, "fake_hoge")はハッシュ化される前のデータと比較されているデータ(今回は"hoge"face_hogeを比較している)が異なるのでfalseとなる。

最後に

今回は非常に簡単なhash化の方法をご紹介しました。
もっと詳しいことが知りたい場合はこちらを参照ください。callbackやasync/awaitでエラーハンドリングを行うことも出来ます。
なにか間違いなどありましたら指摘していただけると幸いです。

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

babylon.js で Material を自作する方法

WebGL フレームワークである babylon.js には、モデルの材質表現を扱うクラスとして Material があります。

このあたりのドキュメントで概要がつかめると思いますが、テクスチャや色などのパラメータを割り当てて(bind)、ポリゴンがどういう風に光を受け描画されるかを設定することが出来る機能です。

デフォルトでは StandardMaterial というマテリアルが適用されますが、これには様々な機能が搭載されています。

See the Pen babylon.js Variable Materials by 山岸 "あかいいぬ?" Masaru (@akai_inu) on CodePen.

例えばこのような感じで、手軽にマテリアルを設定可能です。視差マッピングがデフォルトであるのも面白いです。

これらのマテリアルは WebGL Shader によって実現されています。マテリアルと 1 対 1 でシェーダーが Effect クラスの中に用意されています。

実行時はこの Effect を使って、割り当てられたメッシュをレンダリングしていきます。

また、 物理ベースレンダリングマテリアルも実装されているので、 glTF で PBR 設定がされているものに関しては自動で適用されます。

さらに、別パッケージとして Materials Library があり、こちらで溶岩や水、セルシェーディングなどのカスタムマテリアルが用意されています。自作 Material を作る際の参考になりますね。

今回は私が実装した babylon-mtoon-material を作る際に参考にしたこと、注意したことをまとめておきます。

事前に、 WebGL シェーダを理解する必要があるので、 Introduction to Shaders - Babylon.js Documentation を読みます。今回は WebGL シェーダについての紹介は省略します。

Material を自作する方法

  1. CYOS を使う: ブラウザで手軽にシェーダーを書いてそれを ShaderMaterial としてエクスポートして利用
  2. ShaderMaterial を使う: 任意のシェーダーを読み込んで利用
  3. Material Library を使って拡張する: 公式の拡張マテリアル一覧に新しいマテリアルを追加して利用
  4. CustomMaterial を使って拡張する: StandardMaterial の特定のシェーダー部分を置換して利用
  5. 完全新規に作成する: Material を一から作成
  6. [WIP]NodeMaterial を使って視覚的に作成する

1. CYOS を使う

Put Shader Code in BJS - Babylon.js Documentation

image.png

babylon.js の Create Your Own Shader ページを利用すると、リアルタイムにシェーダーの変更を反映させながらシェーダーを作ることが出来ます。

結果を zip 出力すると、後述する ShaderMaterial を使って、書いたシェーダーを読み込めるようになります。

これは非常に簡単にシェーダーをいじることが出来るので楽ですが、シェーダーに入力する値を変更出来ないので拡張しづらいです。

2. ShaderMaterial を使う

Use ShaderMaterial - Babylon.js Documentation

ShaderMaterial を利用することで、任意のシェーダー文字列を読み込み、値をバインドし、動作させることが出来ます。

  1. の CYOS でもこの ShaderMaterial 用のスクリプトが出力されます。
// ファイル ./cyos.vertex.fx と ./cyos.fragment.fx を読み込むマテリアルを作成する
const shaderMaterial = new BABYLON.ShaderMaterial('cyos', scene, './cyos');

// <script type="application/vertexShader" id="cyosVertexShaderCode">
// <script type="application/fragmentShader" id="cyosFragmentShaderCode">
// のタグに書かれたシェーダを読み込むマテリアルを作成する
const shaderMaterial2 = new BABYLON.ShaderMaterial('cyos2', scene, { vertexElement: 'cyosVertexShaderCode', fragmentElement: 'cyosFragmentShaderCode' });

// 文字列からマテリアルを作成する
BABYLON.Effect.ShadersStore['cyosVertexShader'] = `
precision highp float;

// Attributes
attribute vec3 position;
attribute vec2 uv;

// Uniforms
uniform mat4 worldViewProjection;

// Varying
varying vec2 vUV;

void main(void) {
    gl_Position = worldViewProjection * vec4(position, 1.0);

    vUV = uv;
}
`;
BABYLON.Effect.ShadersStore['cyosFragmentShader'] = `
precision highp float;

varying vec2 vUV;

uniform sampler2D textureSampler;

void main(void) {
    gl_FragColor = texture2D(textureSampler, vUV);
}
`;
const shaderMaterial3 = new BABYLON.ShaderMaterial('cyos3', scene, { vertex: 'cyos', fragment: 'cyos' });

最後の Effect.ShadersStore にマテリアル文字列を代入してそれを読み込む方法が、環境に依存しなくなるので安全ですね(静的変数なので、名前だけ被らないように注意)。

3. Material Library を使って拡張する

Create a Material for the Materials Library - Babylon.js Documentation

image.png

パッケージは @babylonjs/materials となっていますが、同じリポジトリで開発されている materialsLibrary を拡張する形で、新しいマテリアルを実装出来ます。

まず、 BabylonJS/Babylon.js リポジトリ自体を git clone してきます(かなり重いので git clone --depth 1 で shallow clone する方が良いかもしれません)。

/Tools/Gulp ディレクトリに移動し、下記を打ちます。

Babylon.js/Tools/Gulp $ npm install --global gulp
Babylon.js/Tools/Gulp $ npm install
...
Babylon.js/Tools/Gulp $ npm start

これで http://localhost:1338/materialsLibrary/ にアクセスすることで上記の画像のページを開くことが出来ます。

このページでは、 /materialsLibrary/src 内に記述された各種マテリアルを、任意のパラメータと共にテストすることが出来ます。

/materialsLibrary/index.html を直接書き換えて自分のマテリアルを UI に表示する必要があるのが少し面倒ですが、比較的手軽にいくつかのメッシュに対してテストが可能になります。

4. CustomMaterial を使って拡張する

materialsLibrary の中にある CustomMaterial を利用すると、 StandardMaterial のシェーダコードの特定の部分を置換し、 StandardMaterial に処理を加えることが出来るようになります。

正直、 StandardMaterial 自体を詳しく把握しないと追記することが難しいので、上級者向け(というか使う場所あるか?)です。

5. 完全新規に作成する

マテリアルは Material クラスを継承していればなんでも良いので、 babylon.js が用意している環境に依存せず単体で作成していくことももちろん可能です。

babylon-mtoon-material は上記を検討した結果、ここにたどり着きました。

シェーダは webpack の raw-loader を利用して文字列として import 出来るようにし、 Material のコンストラクタで Effect.ShadersStore に代入しています。

また、 babylon.js 独自の仕様で、シェーダ内に #include<instancesDeclaration> などと書くことで別のファイルを読み込むことが出来ます(GLSL 系列は include 処理がないので、シェーダのコンパイル前にファイルを取得してきて文字列連結させています)。

include 出来るファイルは Effect.IncludesShadersStore で管理されます。

マテリアルの値のテストを行う

開発時は、マテリアルに渡す値(色やテクスチャなど)をリアルタイムに変更して、見た目がどうなるか確認したくなります。

materialsLibrary のページではその機能がありましたが、単体でマテリアルを作成した場合は全て自前で GUI を用意しなければなりません。

それは面倒なので、 babylon.js のインスペクタを拡張することで実現しました。

Display and Use the Inspector - Babylon.js Documentation

image.png

material.inspectableCustomProperties というプロパティに変更可能なプロパティを列挙することで、上記画像のようにインスペクタから値を変更することが出来るようになりました。

現在この方式でサポートされているのは Checkbox, Slider, Color3, Vector3, Quaternion だけですが、ある程度はカバーできるのではないでしょうか。

テクスチャなどカバー出来ない範囲は別のメッシュに対してマテリアルを適用させて、それで確認出来るようにしています。

Babylon.js MToon Material Test

6. [WIP] NodeMaterial を使って視覚的に作成する

現在 v4.1 向けに、 Unity の ShaderGraph や UE4 の Node Graph のようなノードベースのマテリアル作成機能が開発中のようです。

image.png

Inspector と同様 React ベースのエディタが搭載されるようです。

ref. node based material - Questions - Babylon.js


現状では新しいマテリアルを作るのは結構大変です。 StandardMaterial が出来ることも移植しようとすると、 babylon.js と WebGL の知識がそれなりに必要になります。

ノードベースのマテリアル作成ツールがリリースされて、わかりやすくマテリアルが作成出来るようになれば良いですね。

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

Netlify Functions を使って CORS エラーを回避する

はじめに

現在 Web アプリを Nuxt で開発していて、nuxt generateで静的コンテンツを作成し、Netlify にホスティングしている
そのアプリでは、別ドメインの API にリクエストを投げてデータを取得しているので、CORS ポリシーでブロックされないようにproxy-moduleを使って API と同一ドメインになるようにプロキシしている

nuxt.config.js
modules: ['@nuxtjs/axios', '@nuxtjs/proxy'],
proxy: {
  '/atnd': {
    target: 'http://api.atnd.org/events',
    pathRewrite: {
      '^/atnd': '/'
    }
  },
  '/connpass': {
    target: 'https://connpass.com/api/v1/event',
    pathRewrite: {
      '^/connpass': '/'
    }
  }
}

ただし、上記の proxy モジュールによる書き換えはサーバーが必要で、generate 機能を使って静的コンテンツをホスティングしている場合は機能しない
ちなみにローカルでは webpack-dev-server などを使って、Node サーバー上で動かしていれば、ちゃんとプロキシされる

この辺のことは以下に詳しく書いてある
https://github.com/nuxt-community/proxy-module/issues/1

Nuxt の generate 機能を使っていても、ちゃんと別ドメインの API が叩けるようにするというのが今回の開発の目的です

作業リポジトリはこちら
https://github.com/kurosame/event-search

Netlify Functions を使う

ブラウザには別ドメインへの通信を拒否する仕組みが標準で備わっている
Nuxt のプロキシ機能はサーバーが無いと使えないし、API 側にオリジンを許可させることもできない

これらのことを踏まえて、今回は Netlify Functions を使って Lambda 関数内で API リクエストを実行することで CORS エラーを回避することにした

Netlify Functions の実行環境は AWS Lambda だが、AWS 側の設定は一切やらなくて良い
てか AWS アカウントも不要

Netlify のアカウントがあれば使えるが、フリープランだと以下の制限がある

  • 125000 リクエスト/月
  • 100 時間/月

言語は JS と Go をサポートしている
(本記事では JS で実装している)

ローカルで動作確認

以下のモジュールをインストール

yarn add -D netlify-lambda

設定ファイルを用意する

netlify.toml
[build]
  functions = "functions/dist"

Lambda にやらせたい処理を書いて、handler 関数を export

functions/src/sample.ts
export async function handler(event, context) {
  return {
    statusCode: 200,
    body: 'Hello, World'
  }
}
package.json
"scripts": {
  "lambda": "netlify-lambda serve functions/src"
},
yarn lambda

以下のようにリクエストする
http://localhost:9000/sample

Functions から connpassAPI を叩いてみる

以下のコードが Functions 関数の実装
API の仕様上、100 件以上レスポンスが存在していても、MAX100 件しかデータが取れないので再帰関数にしているが、やっていることは API から取ってきたデータを body に JSON.stringify にして渡しているだけです
IConnpassEventResponse と IConnpassResponse の型インターフェースは後述してます

yarn add -D @types/aws-lambda
functions/src/connpass.ts
import { IConnpassEventResponse, IConnpassResponse } from '@/store/connpass'
import axios, { AxiosResponse } from 'axios'
import { APIGatewayProxyEvent } from 'aws-lambda'

export async function handler(event: APIGatewayProxyEvent) {
  const ymd = (event.queryStringParameters || { period: '' }).period
  const count = 100
  const getEvents = (
    events: IConnpassEventResponse[] = [],
    start: number = 1
  ): Promise<IConnpassEventResponse[]> =>
    axios
      .get('https://connpass.com/api/v1/event', {
        params: { ymd, start, count }
      })
      .then((res: AxiosResponse<IConnpassResponse>) =>
        res.data.results_returned === count
          ? getEvents([...events, ...res.data.events], start + count)
          : [...events, ...res.data.events]
      )

  const events: IConnpassEventResponse[] = await getEvents()

  return {
    statusCode: 200,
    body: JSON.stringify(events)
  }
}

TS で書いた場合は、以下を functions ディレクトリに追加

yarn add -D @babel/preset-typescript
functions/.babelrc
{
  "presets": ["@babel/preset-typescript", "@babel/preset-env"],
  "plugins": [["@babel/plugin-transform-runtime", { "regenerator": true }]]
}

基本的に presets だけでコンパイルエラーは回避できると思うが、plugins はエラーになったらそのエラー名で検索するとたぶん必要なプラグインが分かると思うので、必要に応じて設定する
作った後に気づいたけど、ここに書いてた

yarn lambda

正常に起動したら、以下にアクセス
http://localhost:9000/connpass

たぶん Lambda 側でタイムアウトする(デフォルト 10 秒)ので、日付で絞るかタイムアウト時間を伸ばすかしてみてください
http://localhost:9000/connpass?period=20190624
or
yarn lambda -t 20

Nuxt から Functions にリクエストする

Vuex のアクションで axios を使って Functions にリクエストしている

?period=${period}の書き方について
axios.getの param オプションを使うと、Functions 側の event 引数に入らなかったので、直接 URL にパラメータを書いている

store/connpass.ts
import { IEventState } from '@/store/events'

export interface IConnpassEventResponse {
  title: string
  catch: string
  description: string
  event_url: string
  started_at: string
  ended_at: string
  limit: number
  address: string
  place: string
}

export interface IConnpassResponse {
  results_returned: number
  events: IConnpassEventResponse[]
}

export const actions = {
  async getConnpassEvents({ commit }, period: string) {
    const events: IConnpassEventResponse[] = await (this as any).$axios.$get(
      `/connpass?period=${period}`
    )

    // Functionsのレスポンスを色々加工してStoreに入れている
    // (ここから以下は本記事の目的とは関係ない部分)
    commit(
      'events/setEvents',
      events
        .filter((e: IConnpassEventResponse) => e.limit >= 30)
        .map(
          (e: IConnpassEventResponse) =>
            ({
              title: e.title,
              catch: e.catch,
              description: e.description,
              eventUrl: e.event_url,
              startedAt: (this as any)
                .$moment(e.started_at)
                .format('YYYY-MM-DD HH:mm:ss (ddd)'),
              endedAt: (this as any)
                .$moment(e.ended_at)
                .format('YYYY-MM-DD HH:mm:ss (ddd)'),
              address: `${e.address} ${e.place}`
            } as IEventState)
        ),
      { root: true }
    )
  }
}

ローカルで netlify-lambda を使って起動するとポート 9000 で実行される
Nuxt はポート 3000 で起動しているので、以下のようにプロキシしないと CORS に引っかかる

Netlify にホスティングして動かす場合は、Nuxt と Functions は同じドメイン上で動くので、プロキシは不要
(そうじゃないと今回 Functions を使った意味がないので、、)

nuxt.config.js
proxy: {
  '/connpass': {
    target: 'http://localhost:9000'
  }
}

最後にアクションを Dispatch する

components/Summary.vue
this.$store.dispatch('connpass/getConnpassEvents', '20190624,20190625')

動作確認
ローカルで Lambda を起動して、Nuxt でアクションを Dispatch する

yarn lambda -t 100

ホスティングして動作確認

netlify.toml がルートディレクトリに存在する場合、Netlify の管理画面上の設定は無視される
その場合、netlify.toml にデプロイ設定を書く必要がある

netlify.toml
[build]
  command = "yarn generate"
  functions = "functions/dist"
  publish = "dist"

Netlify にホスティングすると、Functions はhttps://[Site name].netlify.com/.netlify/functionsというパスになるので、axios の baseURL を Functions の URL にする

nuxt.config.js
axios: { baseURL: '/.netlify/functions' },
proxy: {
  '/.netlify/functions/connpass': {
    target: 'http://localhost:9000'
  }
}

ちなみに netlify-lambda を使ってローカルでビルドした場合はhttp://localhost:9000http://localhost:9000/.netlify/functionsのどちらでも良いので、上記の proxy 設定でアクセスできる

ただし、axios の baseURL を Functions に固定するのは後々不便になるかもしれない
その場合は、環境変数を使って対応した方がいいかも

後は Netlify 上で GitHub リポジトリを紐づけて、その GitHub リポジトリのブランチに push すればホスティングされて動作するはずです

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

Nuxtでビルド時にAPIを静的化して、完全にサーバーへのリクエストをなくすト

ビルド時にAPIを静的化するジェネレータを自作しましたので、ご紹介です。(コード有りです。)

最近はやりのヘッドレスCMSをやっていこうと思ったのですが、APIのリクエスト制限きびしいんですよね...。

現状だとNuxtは静的generateモードでも、ページ遷移時にリクエストが発生してしまうので、完全に静的化してしまう必要がありました。

以下のような流れになります。

  • Nuxtビルド時にJSONを生成
  • <link rel="prefetch"> にJSONを追加
  • ルート生成
  • コンポーネントからは /_nuxt/articles/xxx.json にリクエスト

<link rel="prefetch"> の指定で、生成したJSONは前もって準備するようにしています。)

ジェネレータの中身は以下。

modules/generator.js
import axios from 'axios'

module.exports = function generateModule(moduleOptions) {
  this.nuxt.hook('build:before', async ({ app }) => {

    // 全データを取得
    const { data } = await axios.get('/hogehoge')
    const posts = data

    // JSONを生成
    this.options.build.plugins.push({
      apply(compiler) {
        compiler.plugin('emit', (compilation, cb) => {
          posts.forEach(post => {
            compilation.assets[`articles/${post.slug}.json`] = {
              source: () => JSON.stringify(post),
              size: () => {}
            }
          })
          cb()
        })
      }
    })

    // link rel="prefetch"にJSONを追加
    const url = this.options.dev ? '' : 'https://xxx'
    this.options.head.link = [
      ...this.options.head.link,
      ...posts.map(post => ({
        rel: 'prefetch',
        href: `${url}/_nuxt/articles/${post.slug}.json`
      })),
      {
        rel: 'prefetch',
        href: `${url}/_nuxt/articles/master.json`
      }
    ]

    if (this.options.dev) return

    // ルート生成
    this.options.generate.routes = posts.map(post => `/${post.slug}`)
  })
}

こんな感じにモジュールを書いて、nuxt.config.js で設定します。

nuxt.config.js
module.exports = {
  // ジェネレータ内で開発orプロダクションの書き分けに使用しています
  dev: process.env.NODE_ENV !== 'production',

  // モジュールを指定
  modules: ['~/modules/generator'],

  ...
}

コンポーネントからは以下のようにデータを取得します。

pages/_id.vue
<script>
export default {
  async asyncData({ app, params }) {
    const url = process.env.NODE_ENV === 'development' ? '' : 'https://xxx'
    const { data } = await app.$axios.get(
      `${url}/_nuxt/articles/${params.id}.json`
    )
    return { article: data }
  }
}
</script>

これで表示・遷移時ともにサーバーへデータ取得のリクエストを送らずにすむようになりました?

こちらの情報を参考にさせていただきました。
Firebase、Flamelink、Nuxt、Netlify、PWAを使ってJAMstackなブログを作る

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

MathBox を使ってみる

MathBox

 以前から気になっていた "MathBox" を使ってみました。Making WebGL Danceで使われていると言われればどんなことが出来るか分かるかと思います。公式サイト には "PowerPoint Must Die" と過激な事が書いてある通り、スライドを作ることもできます。

 いくつかの記事に分けて使い方を書いていこうと思います。

自分にとっての魅力

  • ベクトル、曲線や曲面など大抵は描画出来る
  • スライド間の遷移やグラフのアニメーションが可能
  • JavaScript である(ので、その他 JavaScript で出来ることはなんでも出来る)
    • TeX 記法で式がかける(KaTeX)
    • 3D モデルの描画が出来る(Three.js)

といったところが魅力的でした。

使い方

 使い方は GitHub の README公式サイト を見ればなんとなく分かるのですが、自分で思い描いているものを書こうとすると骨が折れました。どちらかというとコードが書けた後にドキュメントを読み返すと「あぁ、こういうことが言いたかったのね」となる感じでした。

 まずは直線と Sin カーブを描画するサンプル examples/test/xyzw.html を基にして見ていきましょう。コードは以下の通りです。

examples/test/xyzw.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>MathBox - XYZW Test</title>
  <script src="../../build/mathbox-bundle.js"></script>
  <link rel="stylesheet" href="../../build/mathbox.css">
  <meta name="viewport" content="initial-scale=1, maximum-scale=1">
</head>
<body>
  <script>
    mathbox = mathBox({
      plugins: ['core', 'controls', 'cursor'],
      controls: {
        klass: THREE.OrbitControls
      },
    });
    three = mathbox.three;

    three.camera.position.set(2.3, 1, 2);
    three.renderer.setClearColor(new THREE.Color(0xFFFFFF), 1.0);

    view = mathbox.cartesian({
      range: [[-6, 6], [-1, 1], [-1, 1]],
      scale: [6, 1, 1],
    });

    view.interval({
      width: 128,
      expr: function (emit, x, i, time) {
        var d = Math.sin((x + time) * 2);

        emit(x, 0);
        emit(x, d * .5);
      },
      items: 2,
      channels: 2,
    });
    view.line({
      color: 0x3090FF,
      width: 10,
    });

  </script>
</body>
</html>

こちらをブラウザで開くと時間的に変化する水色の直線とサインカーブが描かれ、Three.js の OrbitControls と同じ操作で視点移動が可能です。こちらを少しずつ変更していってみます。

メソッドチェーン

 view.interval(...) の部分は以下のように書くことも可能です。サンプルでもこのように書かれていることが多いです。

view.interval({
  width: 128,
  expr: function (emit, x, i, time) {
    var d = Math.sin((x + time) * 2);

    emit(x, 0);
    emit(x, d * .5);
  },
  items: 2,
  channels: 2,
}).line({
  color: 0x3090FF,
  width: 10,
});

描画色を変える

 直線と Sin カーブで色を変えてみます。先ほどまでは同じ view.interval(...) 内で書いていたのですが、それぞれ分けて描画します。
 直線のプロパティの width は配列の幅を表しているのですが、直線なので始点と終点だけあれば良いため 2 に変更しました。また items は先ほどの直線と Sin カーブの二種だったのが、直線一種のみになったので 1 としています。 Sin カーブの items も同じく 1 となります。

// 直線
view.interval({
  width: 2,
  expr: function (emit, x, i, time) {
    emit(x, 0);
  },
  items: 1,
  channels: 2,
}).line({
  color: 0x3090FF,
  width: 10,
});

// Sin カーブ
view.interval({
  width: 128,
  expr: function (emit, x, i, time) {
    var d = Math.sin((x + time) * 2);
    emit(x, d * .5);
  },
  items: 1,
  channels: 2,
}).line({
  color: 0xFF3090,
  width: 10,
});

グリッド

 このままでは位置が分かりにくいので、グリッドを追加します。下記のコードを追加します。

view.grid({
  stroke: 'dashed',
});

 stroke'dashed' を指定しているので、ダッシュ線でグリッドが表示されます。座標軸が伸びて見えますが正常です。ドキュメントを見ながら、下記の箇所を変更しながら観察してみてください。

view = mathbox.cartesian({
  range: [[-6, 6], [-1, 1], [-1, 1]],
  scale: [6, 1, 1],
});

コード全体

 ここまでのコード全体は下記の通りです。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>MathBox - XYZW Test</title>
  <script src="../../build/mathbox-bundle.js"></script>
  <link rel="stylesheet" href="../../build/mathbox.css">
  <meta name="viewport" content="initial-scale=1, maximum-scale=1">
</head>
<body>
    <script>
     mathbox = mathBox({
     plugins: ['core', 'controls', 'cursor'],
     controls: {
             klass: THREE.OrbitControls
     },
     });
     three = mathbox.three;

     three.camera.position.set(2.3, 1, 2);
     three.renderer.setClearColor(new THREE.Color(0xFFFFFF), 1.0);

     view = mathbox.cartesian({
     range: [[-6, 6], [-1, 1], [-1, 1]],
     scale: [6, 1, 1],
     });

     view.interval({
     width: 2,
     expr: function (emit, x, i, time) {
             emit(x, 0);
     },
     items: 1,
     channels: 2,
     }).line({
     color: 0x3090FF,
     width: 10,
     });

     view.interval({
     width: 128,
     expr: function (emit, x, i, time) {
             var d = Math.sin((x + time) * 2);
             emit(x, d * .5);
     },
     items: 1,
     channels: 2,
     }).line({
     color: 0xFF3090,
     width: 10,
     });

     view.grid({
     stroke: 'dashed',
     });
    </script>
</body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsとExpressを使って画像アップロードをしてみる

はじめまして、PMをやっているtatsukenと申します。はじめまして
研修の一環でvue.js、expressを書くことがあったので、そのことを中心にまとめていきたいと思います

はじめに

Vue.jsからリクエストを投げてExpressのpublicディレクトリに画像をアップロードしてみたいとおもいます。
今回画像アップロードにはmulterというライブラリを使って行きます。

実装

使用するライブラリのインストール

  • axios
    • npm install axios -s Vueプロジェクトの中でinstallしてください
  • multer
    • npm install multer -s Expressプロジェクトの中でinstallしてください

Vueの実装

まず一つvueファイルを作ってください

Uplord.vue
<template>
  <div>
    <form>
      <input type="file" id="file" v-on:change="onFileChange">
      <input type="submit" value="decide" @click="submitClick">
    </form>
  </div>
</template>
<script>
import Axios from "axios";
export default {
    data() {
        return {
            imageFile: null
        };
    },
    methods: {
        //画像が選択されたとき呼ばれる
        onFileChange(e) {
            this.imageFile = e.target.files || e.dataTransfer.files;
        },
        //submitされたときに呼ばれる
        async submitClick() {
            try {
                const formData = new FormData();
                formData.append("file", this.imageFile[0]);
                const config = {
                    headers: {
                        "content-type": "multipart/form-data",
                    }
                };
                let res = await Axios.post("/image", formData, config);
                console.log(res);
                if (res.data.status === "error") {
                    alert(res.data.error);
                } else {
                    alert("登録完了")
                }
            } catch (error) {
                alert("画像の送信に失敗しました");
            }
        }
    }
}
</script>
  • ここではformDataを使って画像をpostしています。
  • formData.append("key",value)を使うことでkey:valueを対応させる形で任意のデータを乗せることが出来ます。

注意点

  • valueにはオブジェクトなどは乗せることが出来ません。基本的にkeyとvalueを一つずつ載せてください。
  • formDataに載せたvalueはStingになってしまいます。

Expressの実装

src/index.jsに以下を書いていきましょう

index.js
const express = require('express')
const multer = require('multer');
const app = express()
const storage = multer.diskStorage({
    // ファイルの保存先を指定(今回はsrc/public/image以下に画像を保存)
    destination: function (req, file, cb) {
        cb(null, 'src/public/image')
    },
    // ファイル名を指定(オリジナルのファイル名を指定)
    filename: function (req, file, cb) {
        // Math.random().toString(36).slice(-9)で乱数を生成
        const imageName = `${Math.random().toString(36).slice(-9)}_${Date.now()}.png`
        cb(null, imageName)
    }
})

const upload = multer({
    storage: storage
}).single('file')

app.post('/image', (req, res) => {
    upload(req, res, (err) => {
        if (err) {
            //アップロード失敗した場合
            res.json({
                status: "error",
                error: "fail to uplord image"
            })
        } else {
            //アップロード成功した場合
            res.json({
                status: "sucess",
                // ファイル名を返す
                path: res.req.file.filename
            })
        }
    })
});
  • const storageで画像の保存先をファイル名を指定
  • ここではファイル名を乱数と時間を組み合わせていますがこれではユニークとは言えません。ユニークにしたい方はuserIdと時間を組み合わせるなどしてください。
  • /imageのエンドポイントでuplordを呼び出しています。ただアップロードするだけでしたらもっと簡単に出来ます。(詳しくはこちら)しかしここではアップロード出来たかどうかのエラーハンドリングを行いたいのでこのような形になっています。
  • アップロードされた画像のファイル名はres.req.file.filenameで取得する事ができます。
  • 無事フロントでres.data.statussucsessになっていれば登録完了です。

最後に

multeraxiosでお手軽にVueからExpressに画像をアップロードすることが出来ました。
機会があれば是非試してみてください
なにか間違いなどありましたら教えていただけると幸いです。

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

かわいい3Dエンジン「Zdog」の紹介

はじめに

先日、CodePenを眺めていたところ、Zdogというものを見つけて、とてもよかったので一通りドキュメントをみていろいろ作ってみました。

いつもならそれで終わりなのですが、6月4日にリリースされたばかり1で日が浅く、まだ日本語の情報が少なかったため、自分のための備忘録も兼ねて簡単にご紹介します。

Zdogとは

zzzdog.png

Round, flat, designer-friendly pseudo-3D engine for canvas & SVG

フラットで丸みを帯びたレンダリングがなされる擬似3Dエンジンです。

sushidesu2.gif
こんなのや、

zdog.gif
こんなのが簡単に作れます。
ロゴがこうやってぐりぐり動かせるのとてもいいと思います。

Zdog v1 is a beta-release, of sorts.

まだベータ版ということで、V2が現在開発中2のようです。

Zdog 入門

せっかくなので、公式ドキュメントのGetting startedを参考にしながら、Zdogで3Dモデルを表示して、ドラッグで動かすまでをやっていこうと思います。

インストール

公式ドキュメントには3通りの導入方法が紹介されていました。詳しくは公式サイトをご覧ください。

今回は、CDNを使います。

0. 準備

下記のサンプルコードはCDNを利用する場合の例です。この記事ではこのサンプルコードを使って説明していきます。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Zdog Tutorial</title>
  <script src="https://unpkg.com/zdog@1/dist/zdog.dist.min.js"></script>
</head>
<body>
  <canvas width="240" height="240" class="zdog-canvas"></canvas>
  <script>
    // ここにコードを記述
  </script>
</body>
</html>

1. Illustrationインスタンスを作成

まず初めに親となるIllustrationインスタンスを作成します。

// ここにコードを記述
const illo = new Zdog.Illustration({
  element: '.zdog-canvas'
})

element: '.zdog-canvas'とすることで表示する<canvas>要素を指定しています。

illust.jpg

<canvas>要素と結び付けたIllustrationに、この後登場するEllipseShapeなどの図形を追加していき、最後にIllustrationをレンダリングするという流れになります。

2. 図形を追加

今回は円を表示してみようと思います。

// ここにコードを記述
const illo = new Zdog.Illustration({
  element: '.zdog-canvas'
})

// 円を追加
new Zdog.Ellipse({
  addTo: illo,
  diameter: 80,    // 直径
  stroke: 20,      // 線の太さ
  color: '#E62'    // 図形の色
})

addTo: illoとすることで先程作成したIllustrationに図形を追加しています。
ただ、まだ追加しただけでレンダリングを行っていないので図形は表示されません。

3. レンダリング

// ここにコードを記述
const illo = new Zdog.Illustration({
  element: '.zdog-canvas'
})

// 円を追加
new Zdog.Ellipse({
  addTo: illo,
  diameter: 80,    // 直径
  stroke: 20,      // 線の太さ
  color: '#E62'    // 図形の色
})

// 図形を描画
illo.updateRenderGraph()

IllustrationクラスのupdateRenderGraphメソッドを呼び出すことで図形をレンダリングします。

start3.jpg

たった3ステップでフラットな3D図形を表示することができました!

しかし、このままだと動かないので3Dっぽくありません。ということで、次は図形をアニメーションさせてみようと思います。

4. アニメーション

アニメーション用の関数を作成して、requestAnimationFrame()に渡します。

// - 省略 -
const animate = () => {
  illo.rotate.x += 0.03
  illo.rotate.y += 0.03             // 図形を回転
  illo.updateRenderGraph()          // 回転後の図形を描画

  requestAnimationFrame( animate )  // 再度呼び出し
}
animate()

move2.gif

くるくる回ります!

5. ドラッグで回転

// ここにコードを記述
const illo = new Zdog.Illustration({
  element: '.zdog-canvas',
  dragRotate: true    // ←これを追加
})

dragRotate: trueと設定するだけです。簡単にぐりぐりできます!

drag.gif

完成

初心者の私でも特につまずくことなく3D図形を表示して、ドラッグでぐりぐりすることができました!

Zdog is friendly. Modeling is done with a straight-forward declarative API.

とあるようにとても直感的でわかりやすいですね。

最後に、完成したコードを改めて記しておきます。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Zdog Tutorial</title>
  <script src="https://unpkg.com/zdog@1/dist/zdog.dist.min.js"></script>
</head>
<body>
  <canvas width="240" height="240" class="zdog-canvas"></canvas>
  <script>
    // ここにコードを記述
    const illo = new Zdog.Illustration({
      element: '.zdog-canvas',
      dragRotate: true
    })

    new Zdog.Ellipse({
      addTo: illo,
      diameter: 80,    // 直径
      stroke: 20,      // 線の太さ
      color: '#E62'    // 図形の色
    })

    const animate = () => {
      illo.rotate.x -= 0.03
      illo.rotate.y += 0.03             // 図形を回転
      illo.updateRenderGraph()          // 回転後の図形を描画

      requestAnimationFrame( animate )  // 再度呼び出し
    }
    animate()
  </script>
</body>
</html>

番外編:ネコをつくる:crying_cat_face:

これだけだとドキュメントまんまなので、番外編としてネコをつくってみます。少し長いので隠してあります。

クリックして開く

頭をつくる

neko_1.jpg

const head = new Zdog.Shape({
  addTo: illo,
  stroke: 200,
  color: '#9ED'
})

頭を作ります。Shapeクラスはパスで様々な形状の図形を作ることができますが、何も指定しない場合、ただの点が作られます。今回はその点の太さを調整して、球をつくっています。

右耳を作る

ネコといえば耳なので耳を作ります。Polygonクラスを使うと多角形を作ることができますが、今回は先ほど使ったShapeにパスを指定して三角形を作っています。

const ear_right = new Zdog.Shape({
  addTo: illo,
  stroke: 20,
  fill: true,
  path: [
    { x: 0, y: -10 },
    { x: 30, y: 30 },
    { x: -30, y: 30 }
  ],
  translate: { x: -90, y: -80, z: 10 },
  rotate: { z: -0.6 }
})

translaterotateを指定することで、図形の位置を調整することができます。

neko_2.jpg

まだネコではありません。

左耳を作る

目や耳など、同じものを何個も作るときには、copy()メソッドを使うと便利です。

// ear_left
ear_right.copy({
  addTo: illo,
  translate: { x: 90, y: -80, z: 10 },
  rotate: { z: 0.6 }
})

引数として、様々なオプションを渡すことで、位置や大きさなどを調整できます。

neko_3.jpg

まだネコではありません(目がないので)

目を作る

Ellipseで円を作る際にquartersを指定することで半円が作れます。

const eye_right = new Zdog.Ellipse({
  addTo: illo,
  diameter: 40,
  quarters: 2,
  stroke: 10,
  rotate: { z: -Zdog.TAU / 4 },
  translate: { x: -40, z: 90 }
})
// eye_left
eye_right.copy({
  translate: { x: 40, z: 90 }
})

図形を1/4や1/2などキリの良い単位で回転させたい場合はZdog.TAUを使います。

neko_4.jpg

これはネコです。(目があるので)

口があるとかわいいので

Shapeを使って線を引いていきます。頂点のx,y,z軸を示すオブジェクトを渡すだけなのでとても簡単です。

const mouth = new Zdog.Shape({
  addTo: illo,
  stroke: 8,
  path: [
    { x: -14 },
    { x: 14 }
  ],
  translate: { y: 20, z: 90 }
})

neko_5.jpg

よりネコらしくなりました。でもまだ何か足りない気もします。

ひげも必要でした

口と同じ要領でpathをひいて位置を調整します。

// ひげ
const hige_left = new Zdog.Group({
  addTo: illo,
  translate: { x: -90, y: 10, z: 60 }
})

// 1本目のひげ
const hige = new Zdog.Shape({
  addTo: hige_left,
  stroke: 9,
  path: [
    { x: 15 },
    { x: -15 }
  ],
  rotate: { z: 0.4 }
})
// 2本目のひげ
hige.copy({
  translate: { y: 40 },
  rotate: { z: -0.2 }
})

// まとめて複製
hige_left.copyGraph({
  rotate: { z: Zdog.TAU / 2 },
  translate: { x: 90, y: 50, z: 60 }
})

ひげのように複数の図形をまとめるときに便利なのがGroupです。addToに作成したGroupを指定することでまとめることができます。

copy()メソッドは、子孫のアイテムまでコピーしてくれないので、まとめた図形を複製する際はcopyGraph()メソッドを使います。

完成

neko_6.jpg

かわいいネコができました!一応、こちらにコードも載せておきます。

おわりに

簡単な説明ということで物足りない部分もあるかと思いますが、参考になれば幸いです。

また、このZdogをいたるところに使って3作成したホームページがあります。よろしければぜひご覧ください。

参考


  1. v1.0.0 - Initial public release 

  2. https://github.com/metafizzy/zdog#beta 

  3. 言うほど使っていません、、、 

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

すべてのチェックボックスにチェックを入れたい

  • すべてのチェックボックスにチェックを入れたい
[...document.querySelectorAll('input[type=checkbox]')].forEach(x => x.checked = true);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[node.js] Express,Mysql2/promiseでqueryをasync/await使って簡潔に書く

はじめまして、普段はPMをやっているtatsukenと申します。はじめまして
研修の一環でvue.js、expressを書くことがあったので、そのことを中心にまとめていきたいと思います

はじめに

ExpressでMysqlを使用する際Mysql2をしようすることがあると思いますが、関連するテーブルが多くなれば多くなるほどqueryの中にqueryを書くと言うようにネストが深くなっていき、どんどん可読性が下がってしまいます。
そのような問題を解決するためにMysql2/promiseが用意されています。

Mysql2/promise使わないずコールバックを使用する場合

sample.js
const express = require('express')
const mysql = require('mysql2');
const app = express()
const db_setting = {
    host: 'db',
    user: 'hoge',
    password: 'hoge',
    database: 'hoge',
}
app.get('/', (req, res) => {
    const connection = mysql.createConnection(db_setting)
    connection.connect()
    connection.beginTransaction(function (err) {
        if (err) {
            connection.rollback(function () {
                throw err;
            });
            res.json({
                status: "error",
                error: "fail to uplord data"
            })
            connection.end()
            return
        }
        const hoge1 = {
            name: "hoge1",
        }
        connection.query('insert into table_name set ?', hoge1, (err, rows) => {
            if (err) {
                connection.rollback(function () {
                    throw err;
                });
                res.json({
                    status: "error",
                    error: "fail to uplord data"
                })
                connection.end()
            } else {
                const hoge2 = {
                    name: "hoge2"
                    insertId: rows.insertId,
                }
                connection.query('insert into table_name set ?', hoge2, (err, rows,) => {
                    if (err) {
                        connection.rollback(function () {
                            throw err;
                        });
                        res.json({
                            status: "error",
                            error: "fail to uplord data"
                        })
                        connection.end()
                    } else {
                        connection.commit(function (err) {
                            if (err) {
                                connection.rollback(function () {
                                    throw err;
                                });
                                connection.end()
                                return
                            }
                            res.json({
                                status: "success",
                                id: rows.insertId
                            })
                            connection.end()
                        });
                    }
                })
            }
        })
    })
});

ここではコールバックを使用しているのでネストの深さやコード量のせいで可読性が下がっています。

解決策

Mysql2/promiseを使いasync/awaitで書く

sample.js
const express = require('express');
const app = express.Router();
const mysql = require('mysql2/promise');
app.get('/', (req, res) => {
    const hoge1 = {
        name: "hoge1",
    }
    let connection
    try {
        connection = await mysql.createConnection(db_setting)
        await connection.beginTransaction();
        const [row1] = await connection.query('insert into table_name set ?', hoge1);
        const hoge2 = {
            name: "hoge2"
            insertId: row1.insertId,
        }
        const [row2] = await connection.query('insert into table_name set ?', hoge2);
        await connection.commit();
        res.json({
            status: "success",
            id: row2.insertId
        });
    } catch (err) {
        await connection.rollback();
        res.json({
            status: "error",
            error: "fail to uplord data"
        })
    } finally {
        connection.end()
        return
    }

});

このようにasync awaitを使うことで大幅にコード量を減らすことができ、ネストが深くなるという問題も解決する事が出来ました。

注意点

  • mysql2をインポートするだけではasync/awaitを使うことは出来ません。
  • mysql2/promiseを使う際はasync/awaitを使ってください。
  • mysql2/promiseを使うとコールバックは認識されなくなります。

最後に

mysql2/promiseを使うと非常に簡潔にコードを書くことが出来ます。機会があればぜひ使って見てください。
なにか間違いなどあれば、教えていただけると幸いです。

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

[node.js] Express,mysql2/promiseでqueryをasync/await使って簡潔に書く

はじめまして、普段はPMをやっているtatsukenと申します。はじめまして
研修の一環でvue.js、expressを書くことがあったので、そのことを中心にまとめていきたいと思います

はじめに

ExpressでMysqlを使用する際Mysql2をしようすることがあると思いますが、関連するテーブルが多くなれば多くなるほどqueryの中にqueryを書くと言うようにネストが深くなっていき、どんどん可読性が下がってしまいます。
そのような問題を解決するためにMysql2/promiseが用意されています。

Mysql2/promise使わないずコールバックを使用する場合

sample.js
const express = require('express')
const mysql = require('mysql2');
const app = express()
const db_setting = {
    host: 'db',
    user: 'hoge',
    password: 'hoge',
    database: 'hoge',
}
app.get('/', (req, res) => {
    const connection = mysql.createConnection(db_setting)
    connection.connect()
    connection.beginTransaction(function (err) {
        if (err) {
            connection.rollback(function () {
                throw err;
            });
            res.json({
                status: "error",
                error: "fail to uplord data"
            })
            connection.end()
            return
        }
        const hoge1 = {
            name: "hoge1",
        }
        connection.query('insert into table_name set ?', hoge1, (err, rows) => {
            if (err) {
                connection.rollback(function () {
                    throw err;
                });
                res.json({
                    status: "error",
                    error: "fail to uplord data"
                })
                connection.end()
            } else {
                const hoge2 = {
                    name: "hoge2"
                    insertId: rows.insertId,
                }
                connection.query('insert into table_name set ?', hoge2, (err, rows,) => {
                    if (err) {
                        connection.rollback(function () {
                            throw err;
                        });
                        res.json({
                            status: "error",
                            error: "fail to uplord data"
                        })
                        connection.end()
                    } else {
                        connection.commit(function (err) {
                            if (err) {
                                connection.rollback(function () {
                                    throw err;
                                });
                                connection.end()
                                return
                            }
                            res.json({
                                status: "success",
                                id: rows.insertId
                            })
                            connection.end()
                        });
                    }
                })
            }
        })
    })
});

ここではコールバックを使用しているのでネストの深さやコード量のせいで可読性が下がっています。

解決策

Mysql2/promiseを使いasync/awaitで書く

sample.js
const express = require('express');
const app = express.Router();
const mysql = require('mysql2/promise');
app.get('/', (req, res) => {
    const hoge1 = {
        name: "hoge1",
    }
    let connection
    try {
        connection = await mysql.createConnection(db_setting)
        await connection.beginTransaction();
        const [row1] = await connection.query('insert into table_name set ?', hoge1);
        const hoge2 = {
            name: "hoge2"
            insertId: row1.insertId,
        }
        const [row2] = await connection.query('insert into table_name set ?', hoge2);
        await connection.commit();
        res.json({
            status: "success",
            id: row2.insertId
        });
    } catch (err) {
        await connection.rollback();
        res.json({
            status: "error",
            error: "fail to uplord data"
        })
    } finally {
        connection.end()
        return
    }

});

このようにasync awaitを使うことで大幅にコード量を減らすことができ、ネストが深くなるという問題も解決する事が出来ました。

注意点

  • mysql2をインポートするだけではasync/awaitを使うことは出来ません。
  • mysql2/promiseを使う際はasync/awaitを使ってください。
  • mysql2/promiseを使うとコールバックは認識されなくなります。

最後に

mysql2/promiseを使うと非常に簡潔にコードを書くことが出来ます。機会があればぜひ使って見てください。
なにか間違いなどあれば、教えていただけると幸いです。

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

[node.js] Express,mysql2/promiseでasync/await使って簡潔に書く

はじめまして、普段はPMをやっているtatsukenと申します。はじめまして
研修の一環でvue.js、expressを書くことがあったので、そのことを中心にまとめていきたいと思います

はじめに

ExpressでMysqlを使用する際Mysql2をしようすることがあると思いますが、関連するテーブルが多くなれば多くなるほどqueryの中にqueryを書くと言うようにネストが深くなっていき、どんどん可読性が下がってしまいます。
そのような問題を解決するためにMysql2/promiseが用意されています。

Mysql2/promise使わずコールバックを使用する場合

sample.js
const express = require('express')
const mysql = require('mysql2');
const app = express()
const db_setting = {
    host: 'db',
    user: 'hoge',
    password: 'hoge',
    database: 'hoge',
}
app.get('/', (req, res) => {
    const connection = mysql.createConnection(db_setting)
    connection.connect()
    connection.beginTransaction(function (err) {
        if (err) {
            connection.rollback(function () {
                throw err;
            });
            res.json({
                status: "error",
                error: "fail to uplord data"
            })
            connection.end()
            return
        }
        const hoge1 = {
            name: "hoge1",
        }
        connection.query('insert into table_name set ?', hoge1, (err, rows) => {
            if (err) {
                connection.rollback(function () {
                    throw err;
                });
                res.json({
                    status: "error",
                    error: "fail to uplord data"
                })
                connection.end()
            } else {
                const hoge2 = {
                    name: "hoge2"
                    insertId: rows.insertId,
                }
                connection.query('insert into table_name set ?', hoge2, (err, rows,) => {
                    if (err) {
                        connection.rollback(function () {
                            throw err;
                        });
                        res.json({
                            status: "error",
                            error: "fail to uplord data"
                        })
                        connection.end()
                    } else {
                        connection.commit(function (err) {
                            if (err) {
                                connection.rollback(function () {
                                    throw err;
                                });
                                connection.end()
                                return
                            }
                            res.json({
                                status: "success",
                                id: rows.insertId
                            })
                            connection.end()
                        });
                    }
                })
            }
        })
    })
});

ここではコールバックを使用しているのでネストの深さやコード量のせいで可読性が下がっています。

解決策

Mysql2/promiseを使いasync/awaitで書く

sample.js
const express = require('express');
const app = express.Router();
const mysql = require('mysql2/promise');
app.get('/', (req, res) => {
    const hoge1 = {
        name: "hoge1",
    }
    let connection
    try {
        connection = await mysql.createConnection(db_setting)
        await connection.beginTransaction();
        const [row1] = await connection.query('insert into table_name set ?', hoge1);
        const hoge2 = {
            name: "hoge2"
            insertId: row1.insertId,
        }
        const [row2] = await connection.query('insert into table_name set ?', hoge2);
        await connection.commit();
        res.json({
            status: "success",
            id: row2.insertId
        });
    } catch (err) {
        await connection.rollback();
        res.json({
            status: "error",
            error: "fail to uplord data"
        })
    } finally {
        connection.end()
        return
    }

});

このようにasync awaitを使うことで大幅にコード量を減らすことができ、ネストが深くなるという問題も解決する事が出来ました。

注意点

  • mysql2をインポートするだけではasync/awaitを使うことは出来ません。
  • mysql2/promiseを使う際はasync/awaitを使ってください。
  • mysql2/promiseを使うとコールバックは認識されなくなります。

最後に

mysql2/promiseを使うと非常に簡潔にコードを書くことが出来ます。機会があればぜひ使って見てください。
なにか間違いなどあれば、教えていただけると幸いです。

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

MicrostrategyにCustom Pluginを組み込む際のノウハウ

Custom Pluginについて

  • ほぼ全てのPageにはglobal.jsというJavascriptが含まれている。

    • Custom Javascipt(Pluginに入れるやつ)だと認証を組み込まないといけないが、global.jsを使えば組み込む必要がない?
  • JavascriptでPromptを弄るのであればreportPage.jsとrwdPageを弄る必要がある。それぞれ、レポートの実行・ドキュメントの実行時に使われるJSファイルである。

  • Custom Pluginを作るときはMicrostrategy SDKを使うと良い。SDKのなかに含まれる「Web Customization Editor」で作成・編集できる。

  • Custom Plugin置き場は以下である。

    • plugins\pluginName\javascript
    • 慣習的にJSファイルの名前は以下にすること。
    • [Name of the page]Page.js

DOM操作(一般的なJavascriptの話ですが参考までに)

 

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

JavaScriptにRustっぽい列挙体を作る

概要

JavaScriptで列挙子に値を持たせられる列挙体を実装したパッケージが見つからず辛かったので、列挙体を実装してgithub及びnpmに公開しました。

この記事はその実装についての説明と日本語ドキュメントを兼ねています。記事が長くなるので、実装については次回にします。

githubリポジトリ

使い方

インストール

> npm install sr-data-manipulator

列挙体と列挙子を生成する

$ から始まらない文字列を列挙子名として Enum のコンストラクタに任意の個数の列挙子名を渡すと、その列挙体を返します。

const Result = new Enum("Ok", "Err");

[列挙体].[列挙子名] のコンストラクタに値を渡すと、その値を持った列挙子が生成されます。

const ok = new Result.Ok(some_value);

列挙子のメソッド・シンボル

value() メソッドにより、列挙子が持っている値を取得できます。

const value = ok.value("hello enum");
console.log(value); //hello enum

name() メソッドにより、列挙子の名前を文字列で取得できます。

const name = ok.name();
console.log(name); //Ok

tag() メソッドにより、列挙子を表す Symbol そ取得できます。Symbol はそれぞれの列挙体のそれぞれの列挙子ごとに共通です。

const Result_1 = new Enum("Ok", "Err");
const Result_2 = new Enum("Ok", "Err");

const ok_1_1 = new Result_1.Ok();
const ok_1_2 = new Result_1.Ok();
const ok_2 = new_Result_2.Ok();
const er_2 = new_Result_2.Err();

console.log(ok_1_1.tag() == ok_1_2.tag());   // true
console.log(ok_1_1.tag() == ok_2.tag());     // false
console.log(er_2.tag() == ok_2.tag());       // false

console.log(ok_1_1.name() == ok_1_2.name()); // true
console.log(ok_1_1.name() == ok_2.name());   // true
console.log(er_2.name() == ok_2.name());     // false

[列挙体].$[列挙子名] により、列挙子を表す Symbol を取得できます。

console.log(ok.tag() == Result.$Ok); // true

expect

列挙子は expect メソッドを持ちます。 expect(Symbol).then(function_1).catch(function_2) により、列挙子が Symbol であったときに function_1 を、そうでないとこに function_2 を実行します。この時、それぞれの関数にはそのタグが持っている値が引数として渡されます。

const mayBeOk = new Result.Ok("success");
mayBeOk.expect(Result.$Ok)
    .then(value => console.log(value))
    .catch(console.log("something errored")); //success
const mayBeOk = new Result.Err("unsuccess");
mayBeOk
    .expect(Result.$Ok).then(value => console.log(value))
    .catch(_ => console.log("something errored")); //something errored

then の後で、さらに expect することもできます。

const Hoge = new Enum("Fuga", "Piyo", "Hogera");
const mayBePiyo = new Hoge.Piyo("piyo piyo");

mayBePiyo
    .expect(Hoge.$Fuga).then(value => console.log(value))
    .expect(Hoge.$Piyo).then(value => console.log(value + " !!"))
    .catch(value => console.log(value + " ??")); //piyo piyo !!

コールバック関数の戻り値を取得することもできます。

const mayBeOk = new Result.Err("unsuccess");
const result = mayBeOk
    .expect(Result.$Ok).then(value => value)
    .catch(_ => "something errored"); //something errored

match

match(列挙子).with(Arms) により、match式のような挙動をします。_ は列挙子がどのシンボルにも当てはまらない場合に実行されます。

const Hoge = new Enum("Fuga", "Piyo", "Hogera");
const mayBePiyo = new Hoge.Piyo("piyo piyo");

match(mayBePiyo).with({
    [Hoge.$Fuga] : value => console.log(value),
    [Hoge.$Piyo] : value => console.log(value + " !!"),
    _            : value => console.log(value + " ??")
}); //piyo piyo !!

matchexpect と同様にコールバック関数の戻り値を取得できます。

const mayBeOk = new Result.Err("unsuccess");
const result = match(mayBeOk).with({
    [Result.$Ok] : value => value,
    _            : _ => "something errored"
}); //something errored
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

firebase functionsとSlack coustom slash commandとSlack appで適当になんかして表示する

準備

firebaseでプロジェクトを作りましょう、適当で大丈夫です。
以下を実行して適当なプロジェクトでアレします。
今回は適当なのでjsを選択します。

firebase init functions

Slack coustom slash command

なんかよくわからんけど、カスタムコマンドが設定できます。
この辺 見れば分かると思うよ。
適当なのを設定しておきます。
実行するとAPIリクエストします。今回はFirebase functionsのURLにPOSTします。

Slack App

Firebase functionsからリクエストして適当なチャンネルにpostするために使用します。
Incoming Webhooksって言うんか、よく知らんけど。
この辺 見とく。

コード

index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const axios = require('axios');

const URL = 'https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXX';

admin.initializeApp(functions.config().firebase);
const db = admin.firestore();
exports.postSlack = functions.https.onRequest((req, res) => { 
  try {
    if(req.method === 'POST'){
      let user_id = req.body.user_id;
      let now = new Date();
      let dateString = `${now.getFullYear()}-${('0' + (now.getMonth() + 1)).slice(-2)}-${('0' + now.getDate()).slice(-2)} ${('0' + now.getHours()).slice(-2)}:${('0' + now.getMinutes()).slice(-2)}:${('0' + now.getSeconds()).slice(-2)}`
      db.collection('hoge').doc(user_id).get().then(
        (doc)=> { 
            if(doc.exists){
              //前のデータと見比べてなんかする
              res.status(200).send('ok');
            }else{
              //そのユーザーのアレがないのでなんかする
              res.status(200).send('ok');
            }
        }).catch( 
          (error)=> {
            console.log("Error getting document:", error);
            res.status(200).send('ok');
        });
    }else{
      throw new Error('only post method is accepted');
      res.status(200).send('ok');
    }
  }
  catch (error) {
    console.error('error');
    res.status(500).send(error);
  } 
});

なんかする

その他

なんかNodeのバージョンで頭を抱えたので適当に固定しとく

package.json
{
  ...
  engines:{
  "node": "8"
  },
  ...
}

なんか 適当

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

Vueで複数条件(and,or,フリーワード)の絞り込みを実装する

まずはサンプル

https://kackie.github.io/sort/vue.html

こちらの記事で作ったお店のリストを改良しましたw
今回は管理するデータがより複雑になったので、データはjson形式でまとめてaxiosで読み込んでいます。

絞り込みの条件

グループ化されたカテゴリがあり、同グループ内を複数選択した場合はor(和食||洋食)、グループを跨いで選択した場合はand(和食||洋食&&肉)での絞り込みになります。
またフリーワード検索を作り、これもフリーワード&&選択したカテゴリというふうにandで絞り込まれます。

jQuery版との比較

データバインディングの面でVueが圧倒的に優れています。さすが。
そのおかげでjsのソースもシンプルに書くことができました。
必然的にオブジェクト指向に則って書いていくので、ソースが整理されて見やすくわかりやすいです。
またVueのtransitionクラスを利用して切り替えのインタラクションを簡単に作ることができました。

今後実装したいもの

  • URLにパラメータ付与
    jQuery版にあったやつ。管理する情報が増えたので書き方を新しく考えないと..。
  • 時間による絞り込み
    開店時間で絞り込んだり、早い順でソートしたりできればよいかなと。
    インターフェースはプルダウン?
  • json更新のシステム
    手動でもいいけど、PHP等の勉強用に作ってみたい。
  • 複数キーワードでの検索
    今は単一キーワードでの絞り込みしかできないので、スペース区切りで複数入力された場合の処理が必要。
    • その場合はand検索のほうが良いか?
  • 非表示時のインタラクション
    これはどうしようもないかもしれないけど、非表示のものが左上に集約されていくのがちょっと気持ち悪いw
    おそらくflex配下の要素にposition:absolute;を指定すると親要素基準で左上に移動してしまうせい(初めて知った)。
    flexをやめると高さ揃えをjsでやることになるしうーん。

感想

jsonを手動で作成し管理するなんてことを初めてやりましたが、このインターフェースを組み合わせれば色々できそうですね。
何かの実績を整理してみたり、読んだ本や見た映画をまとめてみたり、料理のレシピを材料やジャンル別でカテゴライズしてみたり...。
思いついたら自分用にでも使ってみようと思います。

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

JavaScript で ISO 8601 に準拠した週番号を計算する

ISO 8601と言えば日付や時刻を201906242019-06-242019-06-24T09:00:00+09:00というように表現する規格だ。
ISO 8601では日付を表す方法は一般的な年月日による表現の他にも以下のような表現が用意されている。

  • 年とその年の1月1日からの経過日数 (例: 2019-175)
  • 年と週番号、曜日番号(1=月曜日, 7=日曜日) (例: 2019-W26-1)

今回は JavaScript を用いて、渡されたDateからISO 8601の週番号を用いた文字列を返す関数を作成する。

定義

週番号を用いた表記は以下のように定義される

  • 一週間は月曜日で始まり、日曜日で終わる
  • その年の第一週は最初の木曜日を含む週
  • そのため年の境目では、年月日の場合と異なる年を表記する場合がある (例: 2018年12月31日は 2019-W01-1 となる)

実装

function dateToIso8601Week(date){
  // 引数のDateと同じ週の木曜日を計算
  // 259200000 = 3days * 24hour * 60min * 60s * 1000ms
  // 604800000 = 1week * 7days * 24hour * 60min * 60s * 1000ms
  const thursday = new Date(Math.ceil((date.getTime() - 259200000) / 604800000) * 604800000);

  // 木曜日と同じ年の1月1日を計算
  const firstDayOfYear = new Date(thursday.getFullYear(), 0, 1);

  // 木曜日がその年の第何週かを計算する
  const weekOfYear = Math.floor((thursday.getTime() - firstDayOfYear.getTime()) / 604800000) + 1;

  // 曜日をISO 8601に直す (jsは0=日~6=金、ISO 8601は1=月~7=日)
  const dayOfWeek = date.getDay() !== 0 ? date.getDay() : 7;

  // yyyy-Www-D 形式に直す
  return `${thursday.getFullYear()}-W${('0' + weekOfYear).slice(-2)}-${dayOfWeek}`;
}

// input: 2018-12-31
// output: 2019-W01-1
console.log(dateToIso8601Week(new Date(2018, 11, 31)));

参考文献

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

Gulpのプラグインを書いたらPlayCanvasでの開発がめちゃくちゃ便利になった。

@yushimatenjinです。PlayCanvasのコードエディターに付属されているリンターが、JSHintという古いものでなおかつカスタマイズができなかったので、gulp-playcanvas playcanvas-node playcanvas-cliと3つのライブラリを作りました。gulpのpluginを作ったのでその知見の共有をさせていただきます。

問題

PlayCanvasを使う上で気になった点が付属のコードエディターのLintが古く具体的には
- constを使うと黄色くなる
- セミコロンを入れないと、赤くエラーが出る
この状況だったので自分のエディターを使って開発をしたい欲が出てきました。

Image 37

作ったプラグイン

gulp-playcanvas

npmのパッケージとして公開いたしましたので下記のコマンドでインストールできます。

yarn add gulp-playcanvas

ローカル側のgulpfile.jsはwatchをして、トランスパイルしたものをPlayCanvasにREST APIを使用してアップロードしています。

examples - hot-reload

gulpfile.jsファイル

const gulp = require("gulp");
const playcanvas = require("gulp-playcanvas");
const pcOptions = require("./playcanvas.json");
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"));

パッケージのインストール後、playcanvas.jsonという設定ファイルを同じディレクトリに置き、yarn gulpコマンドで監視対象をwatchしておきファイルの中身に変更があった場合に同期をする形で動きます。

playcanvas.json
{
  "accessToken": "",
  "scenes": [],
  "projectId": "",
  "branchId": "",
  "projectName": "",
  "remotePath": ""
}

動かした結果

Hot Reload + ローカルのeslint + prettierで開発ができるのでかなり快適になりました。

タスクは4つの動きをします。

1. 監視

コードの変更をwatch ソースコードの変更を検知しタスクを実行

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"));
});

この部分ですね、pug, js, sassに変更があった場合にそれぞれのタスクを実行します。

2. ビルド & 3. アップロード

PugやSassなどをHTML, CSSへ変換し、REST APIを使用しビルドしたコードをアップロードします

const pug = require("gulp-pug");
/*
gulp.task("pug", () => {
  return gulp
    .src(["src/**/*.pug", "!src/**/_*.pug"])
    .pipe(pug())
    .pipe(gulp.dest("dist/"))
------------自作のプラグインを実行------------------
    .pipe(playcanvas(pcOptions));
------------自作のプラグインを実行--------------------
});
*/

https://github.com/yushimatenjin/gulp-playcanvas/blob/master/index.js#L1

Gulpのプラグインの中身はストリームで流れてくるファイルの情報を使用してアップロードしています。
オプションの渡し方などは、gulp-ftpを参考にしました。

const PlayCanvas = require("playcanvas-node").default;
const through = require("through2");
const gutil = require("gulp-util");
const path = require("path");

module.exports = options =>
  through.obj(function(file, enc, callback) {
    if (file.isNull()) {
      return callback(null, file);
    }

    if (file.isStream()) {
      return cb(
        new gutil.PluginError("gulp-playcanvas", "Streaming not supported")
      );
    }
    const playcanvas = new PlayCanvas(options);
    playcanvas.updateAssets(
      options.remotePath,
      path.basename(file.path),
      file.path
    );

    return callback(null, file);
  });

1行目でplaycanvas-nodeという自作のライブラリを読み込んでいるのですが、このライブラリを作ったことで、APIとのやり取りをシンプルに書けてとてもよかた、REST APIのラッパーを書いておくのはめちゃめちゃ良いという知見がたまりました。

参考リンク

PlayCanvas.jp
PlayCanvas/Engine
playcanvas-node
playcanvas-gulp

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

Gulpのプラグインを書いたらPlayCanvasでの開発がめちゃくちゃ便利になった

@yushimatenjinです。PlayCanvasのコードエディターに付属されているリンターが、JSHintという古いものでなおかつカスタマイズができなかったので、gulp-playcanvas playcanvas-node playcanvas-cliと3つのライブラリを作りました。gulpのpluginを作ったのでその知見の共有をさせていただきます。

問題

PlayCanvasを使う上で気になった点が付属のコードエディターのLintが古く具体的には
- constを使うと黄色くなる
- セミコロンを入れないと、赤くエラーが出る
この状況だったので自分のエディターを使って開発をしたい欲が出てきました。

Image 37

作ったプラグイン

gulp-playcanvas

npmのパッケージとして公開いたしましたので下記のコマンドでインストールできます。

yarn add gulp-playcanvas

ローカル側のgulpfile.jsはwatchをして、トランスパイルしたものをPlayCanvasにREST APIを使用してアップロードしています。

examples - hot-reload

gulpfile.jsファイル

const gulp = require("gulp");
const playcanvas = require("gulp-playcanvas");
const pcOptions = require("./playcanvas.json");
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"));

パッケージのインストール後、playcanvas.jsonという設定ファイルを同じディレクトリに置き、yarn gulpコマンドで監視対象をwatchしておきファイルの中身に変更があった場合に同期をする形で動きます。

playcanvas.json
{
  "accessToken": "",
  "scenes": [],
  "projectId": "",
  "branchId": "",
  "projectName": "",
  "remotePath": ""
}

動かした結果

Hot Reload + ローカルのeslint + prettierで開発ができるのでかなり快適になりました。

タスクは4つの動きをします。

1. 監視

コードの変更をwatch ソースコードの変更を検知しタスクを実行

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"));
});

この部分ですね、pug, js, sassに変更があった場合にそれぞれのタスクを実行します。

2. ビルド & 3. アップロード

PugやSassなどをHTML, CSSへ変換し、REST APIを使用しビルドしたコードをアップロードします

const pug = require("gulp-pug");
/*
gulp.task("pug", () => {
  return gulp
    .src(["src/**/*.pug", "!src/**/_*.pug"])
    .pipe(pug())
    .pipe(gulp.dest("dist/"))
------------自作のプラグインを実行------------------
    .pipe(playcanvas(pcOptions));
------------自作のプラグインを実行--------------------
});
*/

https://github.com/yushimatenjin/gulp-playcanvas/blob/master/index.js#L1

Gulpのプラグインの中身はストリームで流れてくるファイルの情報を使用してアップロードしています。
オプションの渡し方などは、gulp-ftpを参考にしました。

const PlayCanvas = require("playcanvas-node").default;
const through = require("through2");
const gutil = require("gulp-util");
const path = require("path");

module.exports = options =>
  through.obj(function(file, enc, callback) {
    if (file.isNull()) {
      return callback(null, file);
    }

    if (file.isStream()) {
      return cb(
        new gutil.PluginError("gulp-playcanvas", "Streaming not supported")
      );
    }
    const playcanvas = new PlayCanvas(options);
    playcanvas.updateAssets(
      options.remotePath,
      path.basename(file.path),
      file.path
    );

    return callback(null, file);
  });

1行目でplaycanvas-nodeという自作のライブラリを読み込んでいるのですが、このライブラリを作ったことで、APIとのやり取りをシンプルに書けてとてもよかた、REST APIのラッパーを書いておくのはめちゃめちゃ良いという知見がたまりました。

参考リンク

PlayCanvas.jp
PlayCanvas/Engine
playcanvas-node
playcanvas-gulp

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

React.js 学習メモ 第8回

最近Reactの勉強を始めたので、ノート代わりにまとめていきます。
主に自分の学習の流れを振り返りで残す形なので色々、省いてます。
Webエンジニアの諸先輩方からアドバイスやご指摘を頂けたらありがたいです!

importしている理由

import は別ファイルに切り出した JavaScript (module) から変数、関数、クラス等を読み込むための宣言!
変数、関数、クラス等を、別ファイルに切り出し、それを読みこむために import を使う。
そのようなモジュールを読み込むためにimportを利用します。

PHPであればrequireという構文があります。

外部で利用したいクラスに『export』を付けることで『import』が可能となります。

JavaScript におけるモジュールとは、別ファイルに切り分けられた変数、関数、クラス等のこと。これを用いるために import / export を使う。

読み込む側では、import {名前} from 'ファイル名' とする。

//importで外部クラスを読み込む際のコード
import {'module neme'} from 'path to module'

exportしている理由

外部ファイルのクラスの読み込み
アプリケーションが大きくなるとモジュール毎に機能を管理していく必要があります。
出力する側では export をしておく。

exportの方法は2種類

export class Member {} ・・・名前付きexport

出力する側で変数、関数、クラスの前に export をつける。
この場合のファイル名は、Hello のように、拡張子をつけなくていい。

export class Hello extends React.Component {
  render() {
    return <div>Hello</div>;
  }
}

export defualt class {}・・・デフォルトのexport

export default したものに関しては、import するときに {} を使わない。
1 ファイル内で、1 export default だけしかできない。(通常の export は複数できる)

//名前付きexportの場合
import { Hello } from '../'
//デフォルトのexportの場合
import Hello from '../'

そのため、1ファイルに1コンポーネントだけを作る場合には、default を使う場合がスタンダード

export default する際の書き方

// class の定義をすると同時に 
// export default する書き方
export default class Hello extends React.Component {
  render() {
    return <div>Hello</div>;
  }
}
// 一旦 class の定義をしてから
class Hello extends React.Component {
  render() {
    return <div>Hello</div>;
  }
}

// 定義した class を export default するやり方
export default Hello;

振り返り

今回は、import,exportについてまとめました。export defaultと名前付きのexportがどのような物であるのかを明確にできたのは初めよく分からない点だったので良かったです。

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

JavaScriptの新しい構文まとめ

概要

JavaScript には年々新しいシンタックスが追加されている。それらを活用すればコードが簡潔になる、読みやすくなる、バグを回避できるなどのメリットがある。IE などのブラウザでは使用できないこともあるが、babel などの transpiler や polyfill を使用することで解決できる(そもそも「モダン」でないブラウザは使用されるべきではないが)。以下に最近の JavaScript (正確には ECMAScript)のバージョンで追加されたシンタックスを紹介する。それ以外の method (Object.entries()など) などについてはここでは紹介しない。

ECMAScript 2015 (ES6)

ECMASCript 2015 では非常に多くのシンタックスが追加され、それらをフルに使いこなせばそれ以前のコードと見比べると別言語にすら見えると感じる。const と let によりバグの少ないコードが書きやすくなり、arrow function は functional programming をしやすくし、class の導入は Java などのオブジェクト指向言語に慣れている人たちに喜ばれるものと思われる。また、object や array の作成、分解がしやすくなるよう様々な構文が追加された。

const & let

constletで変数を宣言できるようになった。varとは異なりconstletで宣言された変数は block scope1 である。constで宣言された変数は再代入ができず、letは再代入ができる。

最近では、基本的にconstを使って変数を宣言し、再代入をする必要があるときのみletを使い、varは使わないというスタイルが推奨されることが多い2

// 再宣言
var a = 3
var a = 4 // エラーにならない

let b = 3
let b = 4 // Uncaught SyntaxError

const c = 3
const c = 4 // Uncaught SyntaxError

// スコープ
if (true) {
  var a = 3
}
console.log(a) // 3

if (true) {
  let b = 3
}
console.log(b) // Uncaught ReferenceError

// 再代入
var a = 3
a = 3

let b = 3
b = 3

const c = 3
c = 3 // Uncaught TypeError

Arrow Function

function expression (function () {}) をより短い形で書ける。しかし機能的な違いもあり、function expression 内のthisの値は呼び出され方によって変わるのに対し、arrow function 内のthisは外のthisと常に同じとなる。この性質は、例えばオブジェクト A のメソッドの中で作った function 内から A を参照したい時に便利である。ちなみに arrow という名前は=>が文字通り arrow(矢印)に見えるからである。他言語ではラムダ式とも呼ばれる。

Before

const f = function(x) {
  return x * 2
}

After

const f = x => {
  return x * 2
}
// return文だけの場合 {} と return を省略できる
const f = x => x * 2

Template Literal

クオート("')ではなくバックティック(`)で囲むことにより、string の literal に直接変数や式の値を埋め込んだり、複数行の文字列を作ることができる。複数行に渡る場合は、インデントも文字列に含まれることに気をつける必要がある。

const x = 3
const equation = `2x = ${x * 2}` // "2x = 6"

const program = `function () {
  return 3
}` // "function () {\n  return 3\n}"

for ... of

Array などの iterable3な object について、それに含まれる値を一つづつ処理できる。

let sum = 0
for (const x of [33, 67]) {
  sum += x
}
console.log(sum) // 100

Default Prameters

function の parameter(引数)のデフォルトの値を指定できる。該当の引数が指定されないとき、あるいはundefinedが指定されたとき、デフォルト値が代わりに使用される。

Before

function double(x) {
  if (x === undefined) {
    x = 0
  }

  return x * 2
}

After

function double(x = 0) {
  return x * 2
}

Rest Parameters

function の引数で、「残り(rest)すべて」を配列として受け取ることができる。他言語でいう可変長引数のシンタックスに似ている(JS の function はいつでも任意個の引数を受け取れるという点で特殊ではあるが)。

function f(a, b, ...rest) {
  console.log(rest)
}

f(1, 2, 3, 4, 5) // [3, 4, 5]

Destructuring

Array の要素やオブジェクトのプロパティを取り出して変数に取り出す時に短く書ける。"structure" は組み立てる、構造を作るという意味で、意味を逆にする接頭辞 "de-" がつくことにより、構造をもつものをバラバラに分解する、という意味になる。

Before

const person = {
  age: 10,
  name: 'John',
  job: 'programmer',
  weight: '60kg',
}
const age = person.age
const name = person.name
const rest = {
  job: person.job,
  weight: person.weight,
}

const arr = [10, 20, 30, 40]
const first = arr[0]
const second = arr[1]
const rest = arr.slice(2)

After

const person = {
  age: 10,
  name: 'John',
  job: 'programmer',
  weight: '60kg',
}
const { age, name, ...rest } = person

const arr = [10, 20, 30, 40]
const [first, second, ...rest] = arr

これらは function expression や arrow function の parameter でも使える。

const extractName = ({ name }) => name
console.log(extractName({ name: 'John' })) // John

Property Definition / プロパティ定義

オブジェクトの property の literal でキーと、値として指定された変数名が一致するとき短く書ける。

After

const a = 1
const b = 2
const o = {
  a: a,
  b: b,
}

After

const a = 1
const b = 2
const o = {
  a,
  b,
}

Spread Syntax

rest parameters は受け取る側で複数のものをまとめるものであるのに対し、spread(広げる、ばら撒く) syntax は渡す側で array などの iterable3をバラバラにするものである。

function middle(x, y, z) {
  return y
}
const arr = [2, 3]
console.log(middle(1, ...arr)) // 2

const arr2 = [1, ...arr] // [1, 2, 3]

object の literal では iterable ではなく object を spread することができる(ECMAScript 2018 で追加)。

const o1 = { a: 1, b: 2 }
const o2 = { c: 3, d: 4 }
const o3 = {
  ...o1,
  ...o2,
  e: 5,
}
console.log(o3) // { a: 1, b: 2, c: 3, d: 4, e: 5 }

Method Definition / メソッド定義

オブジェクトの property として function expression を書くときに短く書ける。

Before

{
  bla: function (x) {
    // ...
  }
}

After

{
  bla(x) {
    // ...
  }
}

Computed property name / 動的なプロパティ名

オブジェクトの literal にて key に expression を指定できる。

Before

const key = 'foo'
const o = {}
o[key] = 1

After

const key = 'foo'
const o = {
  [key]: 1,
}

Class / クラス

オブジェクト指向言語でお馴染みのクラスを使用できるようになった。しかし内部的には以前からある prototype が用いられていることに注意する必要がある。クラスについては説明すべき機能や性質が多すぎるため、ここでは割愛する。

class Vector2D {
  // コンストラクタ
  constructor(x, y) {
    this.x = x
    this.y = y
  }

  // 長さを返すメソッド
  magnitude() {
    const { x, y } = this
    return Math.sqrt(x ** 2 + y ** 2)
  }
}

const vec = new Vector2D(3, 4)
console.log(vec.magnitude()) // 5

Generator Function

iterable を生成する function を簡単に作るためのシンタックスである。リクエストされるたびに一つづつ値を返すため、うまく使えばメモリ効率のよいプログラムとなる。通常の function expression あるいは function declaration に「*」をつけることで、その中でyieldを使用できるようになり、yieldしたものがアウトプットされる。

// python の range と同等
function* range(end) {
  for (let i = 0; i < end; i++) {
    yield i
  }
}

for (const x of range(5)) {
  console.log(x)
}
// 0
// 1
// 2
// 3
// 4

ECMAScript 2017 (ES8)

ECMAScript 2017 で追加された主なシンタックスは async/await のみである。

async/await

Promiseベースの非同期処理を、あたかも同期処理であるかのように書くことができ、これにより非同期処理が非常に書きやすくなる。asyncをつけた function 内ではawaitが使えるようになり、awaitの右においたPromiseが resolve するまで待つ4ようになる。Promiserejectした場合はエラーが発生しtry-catchでキャッチすることができる。

async function fetchPosts() {
  const posts = await fetch('/posts').then(res => res.json())
  return posts
}

async function logPosts() {
  const posts = await fetchPosts()
  console.log(posts)
}

  1. block scope とは、ブロック(for文、while文、if文、function () { ... }{ ... }など)の中で宣言されたとき、その中でしか使えない(正確にはその変数名を参照できない)という性質である。逆にvarで宣言された変数は function scope であり、それが宣言された function 内でどこでも参照できる。 

  2. 例えばGoogleAirbnbの style guide でそう指定されている。 

  3. iterable とは、直感的には「値を一つづつ取り出せる」ような object であり、Array がその代表例である。形式的な定義は[Symbol.Iterator]プロパティを持っているオブジェクトである。詳しくはMDNを参照されたい。 

  4. 待つといってもwhile(true){}のようにそこで JavaScript の処理が止まるわけではなく、他にやることがあれば実行される。実はawait xと書くことは、Promiseに対して.then()でコールバックを設定することと本質的に同じである。 

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