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

コンポーネント設計で悩んでいる人へ。簡単にコンポーネント設計できるESLint Plugin作りましたよ?‍?

この記事について

この記事は、筆者が作ったESLint プラグインの紹介記事なります。
「 なぜ作ったのか? 」「 コンポーネント設計とは何か? 」という議題に触れ、コンポーネント設計の難しさこのプラグインの有意性について語っています。
筆者は専門家では無いので間違っている所や認識のズレなどがあるかもしれませんが、もしあった場合は編集リクエストやコメントにて指摘してくれると嬉しいです。

後、現在この記事で紹介するプラグインは「 コンポーネントは JSX で記述されている 」ことを想定しています。

作ったモノ

Specific Component Designを導入するためのELint プラグインです。
プラグインの詳細は、github のドキュメントに日本語で書いていますので、是非見てみて下さい。
以下の画像は、github へのリンクカードです。


Githubリポジトリカード

インストール方法

npmに公開しているので、以下のコマンドでインストールする事が出来ます。

npmでインストール
$> npm install eslint-plugin-scd

yarn でも大丈夫です。

yarnでインストール
$> yarn add eslint-plugin-scd

eslint-plugin-scd の使い方

使い方は、他のESLint プラグインと同じです。この記事では、.eslintrc.jsで設定する方法を紹介します。

.eslintrc.jsで設定
module.exports = {
  extends: ["plugin:scd/recommended"], // デフォルトのルールを適用
  plugins: ["scd"], // eslint-plugin-scdを適用
  rules: {
    // オプションを渡してカスタマイズ出来ます。
    // 詳しくはgihubリポジトリに書いています。( 日本語ドキュメントあります )
    "scd/nue-element-size": [
      "error",
      {
        max: 10,
      },
    ],
  },
};

現在のところjsxtsxの方では動作している事を確認しています。
typescriptでは、以下の設定で動くことを確認しました。

.eslintrc.js(typescriptに対応)
module.exports = {
  env: {
    browser: true,
    es2020: true,
  },

  extends: ["eslint:recommended", "plugin:scd/recommended"],

  parser: "@typescript-eslint/parser",

  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    sourceType: "module",
  },

  plugins: ["scd", "@typescript-eslint"],
};

Specific Component Design について

Specific Component Design( 以下 SCD と表記します )は、具体的なコンポーネント基準によってコンポーネントを分類して設計するための設計思想です。

SCDの目的は、コンポーネントに明確な基準を設け、より設計しやすく・より管理しやすくすることです。
またSCDは、コンポーネントを「 フロントエンドとしてのコンポーネント 」と捉え、フロントエンドフレームワークでのコンポーネント設計で役立つことを想定しています。なので、CSS などの設計についてはSCDに含まれていませんので、ご注意ください。

具体的なコンポーネント基準とは?

SCD はコンポーネント単位が小さい順に、

  1. Nue
  2. Otem
  3. Pafe
  4. Page
  5. Module

の 5 つの分類があり、それぞれの分類には明確な分類基準があります。
それら明確な基準は以下のようになります。

Nue

Nue は最小単位のコンポーネントを分類します。

規約

  • コンポーネントファイルをnuesまたはnueフォルダー下に入れる
  • 要素数( html タグやコンポーネントの数 )は、0~5 個以内する
  • State は持たせない
  • Hooks の使用禁止

Otem

Nueよりも大きなコンポーネントを分類します。

規約

  • コンポーネントファイルをotemsまたはotemフォルダー下に入れる
  • 要素数( html タグやコンポーネントの数 )は、5~10 個以内する
  • State は持たせない
  • Hooks の使用禁止

Pafe

NueOtemよりも大きなコンポーネント、または State を保持ているコンポーネントを分類します。

規約

  • コンポーネントファイルをpafesまたはpafeフォルダー下に入れる
  • 要素数( html タグやコンポーネントの数 )が10 個以上、または State を保持している
  • Hooks を使用してもよい

Page

Pageは、ページを構成するコンポーネントを分類します。

規約

  • コンポーネントファイルをpagesまたはpageフォルダー下に入れる
  • <main></main>を最低一つ持たせる
  • Hooks を使用してもよい

Module

Moduleは、上記のどれにも分類できない例外的なコンポーネントを分類します
Moduleには明確な規約はありません。自由に設計できます。

※ Module に分類するのは、どうしても他のモノに分類出来ない時です。なるべくNueOtemPafePageに分類できるようにして下さい。

使ってみた感じ

以下の gif 画像は、プラグインを導入してNueコンポーネントを定義した時の挙動です。( VSCode の拡張を使ってます )

scd-play.gif

Nueコンポーネントは、要素数が 5 個以下かつHooks を使ってはダメなので、要素数が 6 個以上またはHooks を使った時にエラーになります。

また、分かりにくいですがnueフォルダー下にコンポーネントファイルがあることも確認できます。( src > nue > sample.jsx ってところ )

コンポーネント設計の難しいさ

ここからはコンポーネント設計の難しさについて解説したいと思いますが、皆さんはコンポーネント設計は簡単だと思いますか?
私は難しいと思っています。そして、その理由は大きく分けて再利用性拡張性の二つがあると思ってます。
これら二つはコンポーネントが求めるもの・作る意義であり、より良いコンポーネントを作るためには両者は必要になってきます。しかし、再利用性拡張性相反しており双方を維持することはできません。なので、私たちがコンポーネント設計する場合は、必ず再利用性拡張性のどちらかを捨て、どちらかを高めるという設計を状況に応じてする必要があります

この「 状況に応じて再利用性拡張性のどちらかを捨てる 」という事が、コンポーネント設計における最大の難しさだと思います。

さて、その難しいさを作っている再利用性拡張性ですが具体的にはどんなモノなのでしょうか?
それを理解するために実際に具体例を出して見てみましょう。

コンポーネント設計における再利用性

再利用性というのは、名前の通り再利用しやすいかどうかを主眼に置いており、コンポーネントを扱いやすくするためにはとても大事な要素です。ですが、「 再利用しやすい 」とはどういう事でしょうか?そこをちゃんと考えてみましょう。

以下に具体例を示します。

// 記事をカードで表示
const EntryCard = ({ entry }) => (
  <li className="entry-card">
    <div className="thumbnail">
      <img src={entry.thumbnail} />
    </div>

    <h3>{entry.title}</h3>

    <div className="author">
      <p>{entry.author.name}</p>
    </div>
  </li>
);

// 記事一覧ページ
const ExplorePage = ({ entries }) => (
  <main>
    <ul>
      {entries.map((entry) => (
        <EntryCard entry={entry} />
      ))}
    </ul>
  </main>
);

// 投稿した記事一覧ページ
const MyEntriesPage = ({ entries }) => (
  <main>
    <ul>
      {entries.map((entry) => (
        <EntryCard entry={entry} />
      ))}
    </ul>
  </main>
);

上記のソースコードは、記事情報からカードを表示するコンポーネント<EntryCard />があり、そのコンポーネントを<ExplorePage /><MyEntriesPage />の二つのコンポーネントが使っています。
注目すべき点は、ExplorePageMyEntriesPageの双方ともに<EntryCard />同じ方法で使っていて、同じ表示をすることが出来ています。これは普通に hmlt タグで書いた時よりもシンプルに書けているのが分かりますね。

この「 同じ方法で同じ表示が出来ている 」という所が非常に重要です。これはつまり、同じ方法でも違う挙動( 表示 )をしてたり、同じ挙動( 表示 )でも違う使い方をしている場合は、「 再利用しやすい 」とは言えません。

例えば、以下のようなコンポーネントがそれです。

// ランダムで記事をカードで表示
const RandomEntryCard = ({ entry }) => {
  const isShow = Math.floor(Math.random() * 100) < 50;

  if (!isShow) return null; // falseならnullを返す

  return (
    <li className="entry-card">
      <div className="thumbnail">
        <img src={entry.thumbnail} />
      </div>

      <h3>{entry.title}</h3>

      <div className="author">
        <p>{entry.author.name}</p>
      </div>
    </li>
  );
};

<RandomEntryCard />ランダムで渡された entry 情報を表示します。このコンポーネントはさっきの<EntryCard />と同じ方法で使うことができますが、明らかに使いにくい事が分かります。

これが「 同じ方法でも違う挙動( 表示 )をしている 」という事です。

では次に、「 同じ挙動( 表示 )でも違う使い方をしている 」はどうでしょうか?
以下のコンポーネントがそれです。

// 記事をカードで表示
const EntryOrBlogCard = ({ entry, blog }) => {
  entry = entry || blog;

  return (
    <li className="entry-card">
      <div className="thumbnail">
        <img src={entry.thumbnail} />
      </div>

      <h3>{entry.title}</h3>

      <div className="author">
        <p>{entry.author.name}</p>
      </div>
    </li>
  );
};

<EntryOrBlogCard />は、<EntryOrBlogCard entry={entry} />または<EntryOrBlogCard blog={entry} />で同じ挙動を示します。
しかし、コンポーネントの内部を知らない人が<EntryOrBlogCard />を使っている所を見た時、Props が違う事から連想して、何か違う挙動をするように見えてしまいます。そうなると、このコンポーネントを使うのにわざわざ内部構造を理解しなければならなくなり、非常に利用しにくい状態になってしまいます。

これが「 同じ挙動( 表示 )でも違う使い方をしている 」という事です。

コンポーネント設計における拡張性

それでは、次に拡張性について見ていきましょう。
この記事での拡張性とは「 後付けがしやすい 」という事ではなく、「 挙動( 表示 )を変化させやすい 」という事です。1

言葉だけじゃ分かりにくいと思うので、例を見てみましょう。

// 記事をカードで表示
const EntryCard = ({ entry }) => (
  <li className="entry-card">
    <div className="thumbnail">
      <img src={entry.thumbnail} />
    </div>

    <h3>{entry.title}</h3>

    <div className="author">
      <p>{entry.author.name}</p>
    </div>
  </li>
);

// 記事一覧ページ
const ExplorePage = ({ entries }) => (
  <main>
    <ul>
      {entries.map((entry) => (
        <EntryCard entry={entry} />
      ))}
    </ul>
  </main>
);

// 投稿した記事一覧ページ
const MyEntriesPage = ({ entries }) => (
  <main>
    <ul>
      {entries.map((entry) => (
        <EntryCard entry={entry} />
      ))}
    </ul>
  </main>
);

上記のコードは、再利用性の時と同じソースコードです。現状では<EntryCard />は表示のみをしており、その他の事はしていません。
それを Author 情報の表示非表示を出来るように修正したいと思います。
理由は、このコードの<MyEntriesPage />は自分が投稿した記事しか表示されない為、Author 情報を表示する必要が無いためです。( Author 情報が全て自分( ページを見ている人 )になるため )

<EntryCard />を以下のように修正します。

EntryCardを修正
const EntryCard = ({ entry, showAuthor }) => (
  <li className="entry-card">
    <div className="thumbnail">
      <img src={entry.thumbnail} />
    </div>

    <h3>{entry.title}</h3>

    {showAuthor ? (
      <div className="author">
        <p>{entry.author.name}</p>
      </div>
    ) : null}
  </li>
);

showAuthor という Props によって、author 情報の表示を切り替えられるようにしました。
この修正によって、<EntryCard />を使っているコンポーネントは以下のように修正します。

ExplorePageとMyEntriesPageを修正
// 記事一覧ページ
const ExplorePage = ({ entries }) => (
  <main>
    <ul>
      {entries.map((entry) => (
        <EntryCard entry={entry} showAuthor={true} /> // Author情報を表示
      ))}
    </ul>
  </main>
);

// 投稿した記事一覧ページ
const MyEntriesPage = ({ entries }) => (
  <main>
    <ul>
      {entries.map((entry) => (
        <EntryCard entry={entry} showAuthor={false} /> // Author情報を非表示
      ))}
    </ul>
  </main>
);

上記の修正により、author 情報は必ず表示していた所をshowAuthorの値によって表示非表示を出来るように<EntryCard />を修正しました。

これにより<EntryCard />の出来ることが増え、より対応できる場面が増えたと分かります。

再利用性と拡張性は相反する

さて、再利用性拡張性を説明してきましたが、ここで一つ問題が発生しました。
それはExplorePageとMyEntriesPageを修正のソースコードの以下の二つ部分です。

ExplorePageコンポーネントより抜粋
(
  /* ... */
  <EntryCard entry={entry} showAuthor={true} /> // Author情報を表示
  /* ... */
)
MyEntriesPageコンポーネントより抜粋
(
  /* ... */
  <EntryCard entry={entry} showAuthor={false} /> // Author情報を非表示
  /* ... */
)

上記の二つのコードは<EntryCard />拡張性を上げるためにshowAuthorを Props に追加したため、<EntryCard />showAuthorの値を渡しています。これは特段問題が無いように見えますが、再利用性という観点から見ると問題が発生しています。
以下のソースコードを見てください。

showAuthorにtrueを渡す例
(
  /* ... */
  <EntryCard entry={entry} showAuthor={true} />
  /* ... */
)

(
  /* ... */
  <EntryCard entry={entry} showAuthor />
  /* ... */
)
showAuthorにfalseを渡す例
(
  /* ... */
  <EntryCard entry={entry} showAuthor={false} />
  /* ... */
)

(
  /* ... */
  <EntryCard entry={entry} /> // showAuthorはundefinedとなるが、undefinedはfalseとして扱われるため同じ挙動を示す
  /* ... */
)

上記の書き方でも、<EntryCard />は同じ挙動をします。
再利用性とは「 同じ方法で同じ挙動( 表示 )を扱える 」という事でした。
しかしどうでしょう?上記のコードは、 同じ方法でしょうか?
showAuthorに boolean を渡すのに、truefalseそれぞれ二通りずつあります。これは明らかに、再利用性が損なわれていることが分かります。

つまり、拡張性を上げた結果、再利用性が失われています!

これが再利用性拡張性が相反すると言った所以です。
また、これらはboolean型だけの話ではなく、Propsの数が多くなったり、別のデータ型になったとしても同じです。

再利用性と拡張性にどう向き合うか?

再利用性拡張性はコンポーネント作るための意義であり、双方とても大事な要素です。
しかし双方が相反しているため両者を高く保つことは困難で、状況に応じてどちらかを高く保つ必要があるかという事にコンポーネント設計者は頭を悩ませています。

私はこの問題について再利用性と拡張性を維持することを諦め明確な基準を設けることで、再利用性拡張性を最低限保持することで対応しています。

そして、この「 明確な基準を設ける 」というのがこの記事で紹介している ESLint プラグインSCDを作った理由です。

長々と話してきましたが、結局のところ再利用性拡張性を高く保つ事は難しく、考えた末の結果は最低限の再利用性拡張性は保持するという所に落ち着きました。これが良いか悪いかで言えば、それはたぶん人それぞれでしょう。

あなたは eslint-plugin-scd を使うべきか?

もしあなたがコンポーネント設計を複数人で行う場合は、この記事で紹介している ESLint プラグインのようなコンポーネント設計を強制するESLint プラグインを使うべきであると思います。
なぜなら、人によってコンポーネント設計の基準は違っており、それが色々な人為的ミスにつながるからです。
もしそうでないなら、この記事で紹介している ESLint プラグインを使っても使わなくても大丈夫でしょう。

つまり、このプラグインを使うべき人は以下のような人たちだと思います。

  • コンポーネント設計に自信が無い人
  • コンポーネント設計に労力をかけたくない人
  • 複数人でコンポーネント設計を行う必要がある人

あとがき

ここまでコンポーネント設計について語ってきましたが、正直説明するのが難しく、私の考えている事の一割も説明できてないように思えて、自分の説明力の無さに落胆していますが、あくまでESLint プラグインの紹介記事なので、プラグインの事だけでも知ってもらえたら幸いです。

コンポーネント設計の闇は、State とかキャッシュが入ってきた途端に一気に深くなりますよね ?
私はフロントエンドで一番難しいのがコンポーネント設計だと思ってますが、皆さんはどうでしょうか?そこまで難しくないでしょうか?
ここら辺の認識は、使うフレームワークなどによって結構変わってくるかもしれませんね。Anguler だと悩まないかも ?

またフロントエンドはデザインのところばかり注目されて、こういった設計があるって事を外部の人から理解してもらえない事が多々あるので、この記事で少しでもコンポーネント設計の難しさについて理解してもらえたらいいなと思っています。

最後に、今回作ったESLint プラグインですが、もしかしたら既存のプラグインで同じような事は出来るかもしれません。
ただ今回は調べるのが面倒くさかったのと、前々から ESLint プラグイン作ってみたかったので作ってみました。
色々と学べたのでいい機会だったと思います ?

もしSCDのルールが気に入らない人は.eslintrc.jsなどの設定ファイルで、ルールをカスタマイズして使うこともできますので、是非使ってみて下さい。

それでは ?


  1. 拡張性という言葉はあまり良くないと思ってますが、他にいい言葉が思い浮かばなかったので拡張性としています。 

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

【 javascript】分割

javascriptには構造化の概念がサポートされているがその逆の分割のための構文はなかった。

オブジェクトの分割

まずは簡単な分割の例を見てみる。

let person = {//データ構造を作成
  name: 'Tanaka'
}
let {name} = person;//データ構造を分割

console.log(name);//Tanaka

まず、オブジェクトリテラルを使ってデータ構造を作成している。これによりプロパティを簡単な方法で指定できる。
sonogo
データ構造の分割をしている。nameプロパティを変数に取り出すための分割分が使用されている。

let person = {//データ構造を作成
  name: 'Tanaka',
  age:25
}
let {name,age} = person;//データ構造を分割

console.log(name);//Tanaka 25

上記では、オブジェクトwから抽出したいフィールドの名前を指定するとフィールドと同じマナエの変数にフィールド値を代入される。
ES5で同じことをするとこうなる。

let name=person.name;
let age=person.age;

別名を使用したい場合にはこうする。

let {name:firstName,age:yearOld}=person;

これによりfirstNameとyearOldにperson.name、person.ageの値が割り当てられる。
データ構造の構築時とまさに逆であるところがミソ。

データ構造の異なるレベルのデータの分割

の場合は入れ子に(ネスト)する。

let geoLocation = {
  "location": {
    "lat": 51.0,
    "lng": -0.1
  },
  "accuracy": 1200.4
}
let {
  location: {
    lat,
    lng
  },accuracy
} = geoLocation;
console.log(lat)//51
console.log(lng)//-0.1
console.log(accuracy)//1299.4
console.log(location)//undefined

location変数は作成されないことに注意。
このプロパティは調べただけ。

配列の分割

配列もオブジェクトと同様に逆の構文。

let coords=[51.0,-0.1];
let [lat,lng,]=coords;
console.log(lat);//51.0
console.log(lng);//-0.1

オブジェクトはそれらの値をキー(名前)で追跡するため、それらの値は同じキー/名前で抽出しなければならない。
配列は値のインデックスで追跡する。配列を分割するときは変数に割り当てる変数が何を分割するのかを指定する。

入れ子で分割

配列でも入れ子の分割ができる。

let location = ['str', [1234, 5678]];
let [moji, [suji, suji2]] = location;
console.log(moji) //str
console.log(suji) //1234
console.log(suji2) //5678

使う場面

使う場面として値の入れ替えがある。

if (stop < start) {
  //値を入れ替える  
  let tmp = start;
  start = stop;
  stop = tmp;
}

上の例だと入れ替えるのに一時的な変数を用意しているが、分割を使えばもっと単純になる。

if (stop < start) {
  //値を入れ替える  
  [start, stop] = [stop, start];
}

オブジェクトと配列の分割の組み合わせ

let res = {
  num: [51.0, -0.1]
};
let {
  num: [ar, ar2]
} = res;
console.log(ar, ar2);

上記ではオブジェクトと配列の分割を組み合わせている。これも入れ子の一種。

ここでもnumという変数は作成していない。作成したのはarとar2の2つである。

分割可能なデータ

オブジェクトの分割は配列を含め、任意のオブジェクトで使用できるが数字は変数名として使えないので、分割代入では別名にする。

const {
  0: a,
  1: b,
  length
} = ['foo', 'bar']
console.log(a)//foo
console.log(b)//bar
console.log(length)//2

配列の分割には以下のような用途もできる。

const [one, two, three, ] = 'abc';
console.log(one)//a
console.log(two)//b
console.log(three)//c

iterableプロトコルを実装しているオブジェクトはすべて配列の分割で分割できる。

まとめ

  • 分割はデータ構造から値を出すためのシュガーシンタックス
  • 分割はオブジェクトリテラルと配列リテラルを高文化する問の逆の構文でできる。
  • オブジェクトの分割ではプロパティ名を使って値を指定する。
  • 配列の分割は、インデックスを使って指定。
  • 分割は入れ子も取り出せる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ESLintのカスタムルールでin+配列を禁止しよう

なぜin+配列を禁止するのか?

Pythonで

Python
>>> 0 in [1,2]
False
>>> 1 in [1,2]
True

となり、JavaScriptで

JavaScript
> 0 in [1,2]
true
> 1 in [1,2]
true

となるので、間違いやすい。

このルールの正しいコードの例は

JavaScript
> [1,2].includes(0)
false
> [1,2].includes(1)
true

となる。

ESLintにin+配列を禁止するルールがない。

ESLintのカスタムルールを作ってin+配列を禁止しよう。

禁止する方法

.eslintrc.js の rules に "no-in-array": "error" を追加。

rules フォルダーに no-in-array.js を作成。

動作確認

test.js
const k = [1, 2];

console.log(1 in [3, 4]); // エラー

for (const i in [5, 6]) // エラーなし
    for (const j in [7, 8]) // エラーなし
        console.log(i in k, j); // エラー
> eslint --rulesdir rules test.js

  4:13  error  in + array is a troublemaker                 no-in-array
  8:21  error  in + array is a troublemaker(variable init)  no-in-array

--fix にも対応した。

> eslint --rulesdir rules test.js --fix
test.js
const k = [1, 2];

console.log([3, 4].includes(1)); // エラーなし

for (const i in [5, 6]) // エラーなし
    for (const j in [7, 8]) // エラーなし
        console.log(k.includes(i), j); // エラーなし

for (const i in [5, 6])が文字列型で"0", "1"を返し[1, 2].includes("1")がfalseになることも間違いやすい。

参考にしたサイト

ESLint のカスタムルールを作ろう! (その1) - Qiita
書いて覚える ESLint ルールの作り方:キマリは通さない - Qiita
Working with Rules - ESLint - Pluggable JavaScript linter

おわりに

ESLintに追加されたら嬉しい。

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

スクロール位置に応じてバーが追従するグローバルメニュー

はじめに

コーディングを行う際、ヘッダーのメニューにある下線をスクロール位置ごとに動かしたかったのですが、躓いてしまったため残したいと思います。
私はコーディング歴も浅く、手探りで実装したので、もしより良い方法があればご教授いただけると幸いです。

完成形

menu.gif

コード

下線が追従する基本的な形はこちらを参考にしました。

sample.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Sample</title>

  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.css">
  <link rel="stylesheet" href="css/style.css">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
  <script src="js/script.js"></script>
</head>
<body>
  <header class="header">
    <nav id="navigation">
      <ul>
        <li class="current">
          <a href="#">content</a>
        </li>
        <li class="menu-2">
          <a href="#">second-content</a>
        </li>
        <li class="menu-3">
          <a href="#">3rd-content</a>
        </li>
        <li class="menu-4">
          <a href="#">4</a>
        </li>
      </ul>
      <span id="slide-line"></span>
    </nav>
  </header>
  <div class="contents">
    <div class="content" id="content1">ひとつめ</div>
    <div class="content" id="content2">ふたつめ</div>
    <div class="content" id="content3">みっつめ</div>
    <div class="content" id="content4">よっつめ</div>
  </div>
</body>
style.css
header{
  width: 100%;
  height: 30px;
  position: fixed;
}
ul li {
  display: inline-block;
}
a{
  display: inline-block;
  padding: 30px 20px;
  font-size:14px;
  text-decoration:none;
}
#navigation{
  position: relative;
}
#slide-line{
  position: absolute;
  bottom: 0;
  height: 2px;
  background-color:#75C2FF;
  -webkit-transition: all .3s ease;
  transition: all .3s ease;
}

.content{
  height: 700px;
  padding: 80px;
}
.content:nth-child(odd){
  background-color: #f5f5f5;
}
script.js
$(window).load(function(){
  $('nav span').css({
    width: $('nav .current').width(),
    left: $('nav .current').position().left
  });

  $(window).scroll(function () {
    let content2 = $('#content2').offset().top;
    let content3 = $('#content3').offset().top;
    let content4 = $('#content4').offset().top;
    if ($(this).scrollTop() < content2) {
      if ( ! $('header').hasClass("current1") ) {
        $('header').addClass("current1");
        $('header').removeClass("current2 current3 current4");
        $('nav span').animate({
          width: $('nav .current').width(),
          left: $('nav .current').position().left
        });
      }
    }else if ($(this).scrollTop() > content2 && $(this).scrollTop() < content3) {
      if ( ! $('header').hasClass("current2") ) {
        $('header').addClass("current2");
        $('header').removeClass("current1 current3 current4");
        $('nav span').animate({
          width: $('.menu-2').width(),
          left: $('.menu-2').position().left
        });
      }
    }else if ($(this).scrollTop() > content3 && $(this).scrollTop() < content4) {
      if ( ! $('header').hasClass("current3") ) {
        $('header').addClass("current3");
        $('header').removeClass("current1 current2 current4");
        $('nav span').animate({
          width: $('.menu-3').width(),
          left: $('.menu-3').position().left
        });
      }
    }else if ($(this).scrollTop() > content4) {
      if ( ! $('header').hasClass("current4") ) {
        $('header').addClass("current4");
        $('header').removeClass("current1 current2 current3");
        $('nav span').animate({
          width: $('.menu-4').width(),
          left: $('.menu-4').position().left
        });
      }
    }
  });
});

仕組み

まずメニューバーが動く仕組みについてですが、animateメソッドを使って横幅と位置を指定することで動きをつけています。
こちらの詳細は参考サイトにございますのでご覧ください。

let content2 = $('#content2').offset().top;
let content3 = $('#content3').offset().top;
let content4 = $('#content4').offset().top;

次にスクロールによる、スクロールをした時の区切りとなる要素の位置を上記で定義します。
以下のif文に関しての説明です。
現在のスクロール位置が特定の区切りの中(例えばcontent2の中)にいるか、かつheaderが特定のクラス(前の例えだとcontent2というクラス)を持っていないとメニューバーが動くという仕組みです。

悩んだこと

今回はheaderのクラスのつけ外しを行うことでうまく動かしているのですが、これをする前はうまくいっていませんでした。
console.logなどで確かめてみるとわかるのですが、scrollアクションは少しのスクロールで何回も発火するため、その度にanimateメソッドが読み込まれ非常に処理が重くなってしまいました。
そこで私は、animateメソッドが読み込まれるのをスクロール位置が切り替わった時のみにしたいと考え、クラスの有無を条件式としてあげることで重くなることを防ぐことができました。

おわりに

最後までご覧いただきありがとうございます。
この方法は初心者の私が考えたやり方のため、無理のある記述の可能性があります。
ぜひ有識の方がいらしたらご意見くださると幸いです。

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

【Javascript】JS 変数 定数 違い  一言でまとめました

【ゴール】

javascriptの変数、定数の違い

【メリット】

* javascript理解度向上

【コード】

変数(var , let)

中身を何回も更新できる

var

*再宣言可能
*再宣言の際に(var)は不要

hoge.js
var name = "太郎";

console.log(name); // 太郎が出力


name = "次郎"

console.log(name); //次郎が出力

var name = "三郎"; //再宣言可能

let

*同変数名は一度しか宣言できない

hoge.js
let name = "太郎";

console.log(name);  //太郎が出力

let name ="次郎"; //エラー出る...

定数

中身は一回だけ決めれる

const

hoge.js
 const name = "太郎";

 console.log(name); //太郎と出力

【合わせて読みたい】

■JavaScriptで書く「let,var,const」の違い・使い分け
https://techacademy.jp/magazine/14872

■ 【JavaScript】 js 繰り返し文 for / while
https://qiita.com/tanaka-yu3/items/942d9d4838ebe14be1c2

■ 【jQuery】初心者でもよく理解できたやつ
https://qiita.com/tanaka-yu3/items/a03734b248c3f2ee8461

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

kintone から LINE に投稿する

概要

先日に LINE チャンネルの友達登録や投稿情報を kintone に保管 で、LINEのチャンネル登録をして、友達登録や投稿情報を kintone に保管したり、AWS Lambda から返信できることまでは確認しました。
今回は、kintone から LINE チャンネルを友達登録した方に対して個別でメッセージを送る方法を試してみました。
LINE2-04.png

kintone アプリの項目追加

先ずは、前回作成した LINE の友達登録や投稿情報を保管する kintone アプリに、LINE に送信するメッセージ用のフィールドを追加します。
LINE2-03.png

フィールド名 タイプ フィードコート・要素ID
lineユーザID 文字列(1行) lineUserId
日時 日時 lineDateTime
lineタイプ 文字列(1行) lineType
lineモード 文字列(1行) lineMode
lineメッセージ 文字列(複数行) lineMessage
(追加)lineに送るメッセージ 文字列(複数行) sendMessage
(追加) スペース sendSubmit

スペースは「LINEにメッセージ送信」ボタンを表示するために追加しています。

kintone の JavaScript プログラム作成

kintone の 詳細画面を表示した時に「LINEにメッセージ送信」ボタンを表示し、クリックすると同時に LINE に、レコードのLINEユーザID宛てに「lineに送るメッセージ」フィールドの値を送る JavaScript プログラムを作成します。

LINE のユーザ宛てにメッセージを送るためには、Messaging API の https://api.line.me/v2/bot/message/push で以下のヘッダーで、JSON データを POST します。

・ヘッダー

let headers = {
    'Authorization': ' Bearer ' + '{' + token + '}',
    'Content-Type' : 'application/json'
};

token の値は、 LINE チャンネルの友達登録や投稿情報を kintone に保管 で説明したチャンネルアクセストークンです。

・POST データ

let json = {
    "to":userId,
    "messages":[
        {
            "type":"text",
            "text":message
        }
    ]
};

message にLINEユーザに連絡するテキスト文書をセットしておきます。

・CORS によるエラーに注意

kinton の JavaScript で外部APIを利用する場合は、XMLHttpRequest() は使えません。ついつい私も最初 XMLHttpRequest() で実装しようとして後になって気づくのですが、 kintone.proxy() を利用します。うっかり XMLHttpRequest() で実装しようものなら、エラーでデバッグ画面で以下を確認することになります。また、うっかりやってしました、、、(汗)

Access to XMLHttpRequest at 'https://api.line.me/v2/bot/message/push' from origin 'https://yukataoka.cybozu.com' has been blocked by CORS policy: 
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

・今回実装したコード例

今回は以下のように実装して、kintone から LINE にメッセージが送れるようになりました。
kintone.proxy() は Promise でなくて良かったのですが、同期処理になっています。(汗)

sendLineMessage.js
(function() {
    "use strict";

    const Protocol   = "https://";
    const LineHost   = "api.line.me";
    const LinePath   = "/v2/bot/message/push";
    const LineToken  = "LINEのチャンネルアクセストークン";

    // レコード詳細表示時イベント
    var eventsDetailShow = [
        'app.record.detail.show',
        'mobile.app.record.detail.show'];
    kintone.events.on(eventsDetailShow, function(event) {
        let sendButton  = makeSendButton(LineToken, event.record.lineUserId.value, event.record.sendMessage.value);
        if(event.type !== 'mobile.app.record.detail.show') {
            kintone.app.record.getSpaceElement('sendSubmit').appendChild(sendButton);
        }else{
            kintone.mobile.app.record.getSpaceElement('sendSubmit').appendChild(sendButton);
        }
    });

    // LINEにメッセージ送信ボタン作成
    function makeSendButton(token, userId, message) {
        let submitButton = document.createElement('div');
        let sendButton       = document.createElement('button');
        sendButton.innerText = ' LINEにメッセージ送信 ';
        sendButton.className = "gaia-ui-actionmenu-save";
        sendButton.onclick   = function() {
            let json = {
                "to":userId,
                "messages":[
                    {
                        "type":"text",
                        "text":message
                    }
                ]
            };
            let url = Protocol + LineHost + LinePath;
            let res = funcPostLine(url, token, json);
        };
        submitButton.appendChild(sendButton);
        return submitButton;
    }

    // line にメッセージを送信
    function funcPostLine(url, token, json)
    {
        let headers = {
            'Authorization': ' Bearer ' + '{' + token + '}',
            'Content-Type' : 'application/json'
        };
        kintone.proxy(url, 'POST', headers, json).then(function(args) {
            if (args[1] === 200) {
                alert("LINEにメッセージを送信しました!");
                return true;
            }else{
                alert("LINEのメッセージ送信にエラーがありました!\n" + args[0]);
                return false;
            }
        }, function(error) {
            alert("LINEのメッセージ送信にエラーがありました!\n" + error);
            return false;
        });
    }   
})();

実行結果

kintone の詳細画面で「LINEにメッセージ送信」ボタンを押すと、即座に「LINEにメッセージを送信しました!」の alert() が表示され、LINEへのメッセージ送信を確認できました。
LINE2-02.png
IMG_7854-B.png

まとめ

先日の LINE チャンネルの友達登録や投稿情報を kintone に保管 から、今回の kintoe から LINE に投稿するまでで、以下を確認できました。

・LINE チャンネルを開設する
・LINE チャンネルの友達登録や投稿情報を kintone に保管
・LINE チャンネルの投稿に返信を送信
・kintone から LINE にメッセージを送信

特に kintone との連携の例が少なく相当な苦戦を予想していましたが、実際はわりと簡単に実装できました。実装のためのコストは以外とかからないと推測します。

ポイントとしては、チャンネルの友達登録時に LINE ユーザID を取得し、kintone で管理するアプリ(例えば顧客管理アプリ)のどう紐づけるかが鍵になりそうです。チャンネルの友達登録時や、その後でも kintone のアプリの情報と紐ずくしくみをどのように実装するかが、kintone と LINE を連携して活用するための鍵になりそうです。

参考情報

line 送信設定
https://developers.line.biz/ja/reference/messaging-api/#send-push-message
外部APIの実行
https://developer.cybozu.io/hc/ja/articles/202166320

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

Kinx ライブラリ - JIT ライブラリ

Kinx ライブラリ - JIT ライブラリ

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」 でお届けしているスクリプト言語 Kinx。JIT コンパイルのためのライブラリを作ってみました。

JIT やりたいよね。今回は Kinx - native でも使っている SLJIT を使いやすくしようということで、ライブラリ化しました。SLJIT 自体、ドキュメントが少なくてソースから解読して使っているので、SLJIT そのものの使い方を備忘録的に書こうかとも思ったのだけど、今回は保留。どこかでやるかもしれない。

しかし、SLJIT をそのまま使うよりも勿論使いやすくなっているので、こっちのほうが良いと思う。ホスト言語もスクリプト なので、手軽に楽しめるでしょう。

どんな感じ?

先にどんな感じのプログラムになるか、サンプルを出しておきます。色々細かい話が続いてここまでたどり着いてくれなさそうなので。。。

using Jit;

var c = new Jit.Compiler();
var entry1 = c.enter();
    var jump0 = c.ge(Jit.S0, Jit.IMM(3));
    c.ret(Jit.S0);
    var l1 = c.label();
    c.sub(Jit.R0, Jit.S0, Jit.IMM(2));
    c.call(entry1);
    c.mov(Jit.S1, Jit.R0);
    c.sub(Jit.R0, Jit.S0, Jit.IMM(1));
    c.call(entry1);
    c.add(Jit.R0, Jit.R0, Jit.S1);
    c.ret(Jit.R0);

jump0.setLabel(l1);
var code = c.generate();

for (var i = 1; i <= 42; ++i) {
    var tmr = new SystemTimer();
    var r = code.run(i);
    System.println("[%8.3f] fib(%2d) = %d" % tmr.elapsed() % i % r);
}

Jit.Compiler オブジェクトを作って、enter で関数エントリを作り、色々レジスタをいじくりまわして ret するコードを書きます。で、実行するときは generate() して run()、となります。generate() して dump() とすると、アセンブル・リストを見ることもできます。

色々とばしたい方は サンプル へ Go! → サンプルでは Ruby、Python、PyPy とのベンチマークもしてますよ。

SLJIT

そもそも SLJIT とは何か。

一言でいえば 抽象化アセンブラ で、一つの書き方で複数の環境をサポートできてしまうという、CPU ごとに異なるので作り直さなければならないというアセンブラの問題を解決してくれるライブラリです。現状サポートしているというプラットフォームは以下の通り。

  • SLJIT のサポート・プラットフォーム
    • Intel-x86 32
    • AMD-x86 64
    • ARM 32 (ARM-v5, ARM-v7 and Thumb2 instruction sets)
    • ARM 64
    • PowerPC 32
    • PowerPC 64
    • MIPS 32 (III, R1)
    • MIPS 64 (III, R1)
    • SPARC 32

ただし、ここで紹介する Kinx 版 JIT ライブラリは 64bit しかサポートしていないこと、および x64 Windows と x64 Linux しか確認して(できて)いませんので、そこは悪しからずご了承ください。

公式? 説明文書

私が知る限り、参考になる文書は以下程度しか見つかりませんでした。

参考にはなります。

GitHub のリポジトリは以下です。

Jit

さて、Kinx ライブラリとしての JIT ライブラリ。C のまま使うより便利にはしてます。もちろん C のライブラリを使えばもっとより細かく制御できると思いますが、それなりのことはできます。

using Jit

Jit ライブラリは標準組み込みではないため、using ディレクティブを使用して明示的に読み込む。

using Jit;

Jit オブジェクト

Jit オブジェクトはパラメータ用のメソッドとコンパイラ・クラスが定義されている。

Jit パラメータ用メソッド

Jit のパラメータとして、即値、レジスタ、メモリアクセスの 3 種類がある。以下の形で利用する。

即値、メモリアクセス

即値、メモリアクセスは以下のメソッドで利用する。Jit.VAR() はローカル変数領域を使うための特別なメソッド。スタック領域にローカル変数領域が自動的に確保され、その領域を使う。

メソッド 備考
Jit.IMM(v) 64bit 整数、浮動小数点数どちらも同じ書き方。代入先のレジスタと合わせる。
Jit.VAR(n) ローカル変数領域。1 変数 8 バイト固定。
Jit.MEM0(address) address として即値を代入、ただし現在実アドレスをスクリプトから指定できないので、スクリプトからは使えない。
Jit.MEM1(r1, offset) r1 に指定したレジスタをアドレスとみて、offset 位置(バイト単位)のメモリアドレスを示す。
Jit.MEM2(r1, r2, shift) shift は 0 なら 1 バイト、1 なら 2 バイト、 2 なら 4 バイト、3 なら 8 バイトを示し、r1 + r2 * (shiftで示すバイト分) の位置のメモリアドレスを示す。
レジスタ

以下のレジスタを使用可能。関数内で使えるレジスタの数は自動的に計算され、関数(enter() で区切った範囲)ごとに変わる。

レジスタ 用途
Jit.R0Jit.R5 汎用レジスタ。一時的に利用。別関数呼び出し後に破棄される可能性あり。
Jit.S0Jit.S5 汎用レジスタ。別関数呼び出し後に破棄されない保証。
Jit.FR0Jit.FR5 浮動小数点レジスタ。一時的に利用。別関数呼び出し後に破棄される可能性あり。
Jit.FS0Jit.FS5 浮動小数点レジスタ。別関数呼び出し後に破棄されない保証。

尚、Floating Point 用のレジスタは FR/FS 合わせて最大で 6 個までなので、FR4 まで使用した場合、FS0 しか使えません。FR5 まで使うと FS* は全て使えません。以下のような感じですのでご注意を。

FR* レジスタ FS* レジスタ
(使えない) FS0, FS1, FS2, FS3, FS4, FS5
FR0 FS0, FS1, FS2, FS3, FS4
FR0, FR1 FS0, FS1, FS2, FS3
FR0, FR1, FR2 FS0, FS1, FS2
FR0, FR1, FR2, FR3 FS0, FS1
FR0, FR1, FR2, FR3, FR4 FS0
FR0, FR1, FR2, FR3, FR4, FR5 (使えない)

Jit コンパイラ

Jit 命令を作っていくためには Jit コンパイラ・オブジェクトを作成する。

var c = new Jit.Compiler();

Jit コンパイラには以下のメソッドがある。

Jit コンパイラ・メソッド 復帰値 概要
Jit.Compiler#label() label 現在の場所にラベルを付加する。
Jit.Compiler#makeConst(reg, init) ConstTarget コード生成後に即値を設定するための仮定義コードを出力する。
Jit.Compiler#localp(dst, offset) ローカル変数の実アドレスを取得するコードを出力する。dst に示したレジスタに格納される。offset はローカル変数番号。
Jit.Compiler#enter(argType) label 関数の入り口を作成。引数タイプを指定できる(省略可)。
Jit.Compiler#fastEnter(reg) label 関数の入り口を作成。ただし、余計なエピローグ、プロローグは出力せず、復帰アドレスを reg に保存する。
Jit.Compiler#ret(val) Return コードを出力する。val を返す。val は浮動小数点数は FR0 レジスタ、それ以外は R0 レジスタで返却される。
Jit.Compiler#f2i(dst, op1) double を int64_t にキャストするコードを出力する。dst は汎用レジスタ。op1 は浮動小数点レジスタ。
Jit.Compiler#i2f(dst, op1) int64_t を double にキャストするコードを出力する。dst は浮動小数点レジスタ。op1 は汎用レジスタ。
Jit.Compiler#mov(dst, op1) dstop1 を代入するコードを出力する。浮動小数点とそれ以外の型は自動的に認識する。
Jit.Compiler#neg(dst, op1) op1 の符号反転した結果を dst に格納するコードを出力する。
Jit.Compiler#clz(dst, op1) op1 の先頭から 0 であるビットの数を数え、dst に格納するコードを出力する。
Jit.Compiler#add(dst, op1, op2) op1op2 を加算した結果を dst に格納するコードを出力する。
Jit.Compiler#sub(dst, op1, op2) op1op2 を減算した結果を dst に格納するコードを出力する。
Jit.Compiler#mul(dst, op1, op2) op1op2 を乗算した結果を dst に格納するコードを出力する。
Jit.Compiler#div(dst, op1, op2) 浮動小数点数のみ、op1op2 を除算した結果を dst に格納するコードを出力する。
Jit.Compiler#div() 汎用レジスタで符号なしとして除算した値を R0 レジスタに格納するコードを出力する。
Jit.Compiler#sdiv() 汎用レジスタで符号付きとして除算した値を R0 レジスタに格納するコードを出力する。
Jit.Compiler#divmod() 汎用レジスタで符号なしとして除算した値を R0 レジスタに格納し、余りを R1 レジスタに格納するコードを出力する。
Jit.Compiler#sdivmod() 汎用レジスタで符号付きとして除算した値を R0 レジスタに格納し、余りを R1 レジスタに格納するコードを出力する。
Jit.Compiler#not(dst, op1) op1 のビット反転した結果を dst に格納するコードを出力する。
Jit.Compiler#and(dst, op1, op2) op1op2 でビット AND した値を dst に格納するコードを出力する。
Jit.Compiler#or(dst, op1, op2) op1op2 でビット OR した値を dst に格納するコードを出力する。
Jit.Compiler#xor(dst, op1, op2) op1op2 でビット XOR した値を dst に格納するコードを出力する。
Jit.Compiler#shl(dst, op1, op2) op1op2 ビット分、左シフトした値を dst に格納するコードを出力する。
Jit.Compiler#lshr(dst, op1, op2) op1op2 ビット分、論理右シフトした値を dst に格納するコードを出力する。
Jit.Compiler#ashr(dst, op1, op2) op1op2 ビット分、算術右シフトした値を dst に格納するコードを出力する。
Jit.Compiler#call(label) JumpTarget enter() 定義した関数呼び出しを行うコードを出力する。後で呼び出し先を設定する JumpTarget を返す。label を指定した場合は後から設定する必要はない。
Jit.Compiler#fastCall(label) JumpTarget fastEnter() で定義した関数呼び出しを行うコードを出力する。後で呼び出し先を設定する JumpTarget を返す。
Jit.Compiler#jmp(label) JumpTarget jmp コマンドを出力する。label を指定した場合は後から設定する必要はない。
Jit.Compiler#ijmp(dst) JumpTarget jmp コマンドを出力する。dst はアドレスを示すレジスタ、または即値。
Jit.Compiler#eq(op1, op2) JumpTarget op1 == op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#neq(op1, op2) JumpTarget op1 != op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#lt(op1, op2) JumpTarget 符号なしとして op1 < op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#le(op1, op2) JumpTarget 符号なしとして op1 <= op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#gt(op1, op2) JumpTarget 符号なしとして op1 > op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#ge(op1, op2) JumpTarget 符号なしとして op1 >= op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#slt(op1, op2) JumpTarget 符号付きとして op1 < op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#sle(op1, op2) JumpTarget 符号付きとして op1 <= op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#sgt(op1, op2) JumpTarget 符号付きとして op1 > op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#sge(op1, op2) JumpTarget 符号付きとして op1 >= op2 を確認するコードを出力。条件が真となった場合のジャンプ先を指定する JumpTarget を返す。
Jit.Compiler#generate() JitCode コード生成を行う。

Jit.Compiler#enter(argType)

関数の入り口を enter メソッドで定義するが、argType を指定しなかった場合は Jit.ArgType.SW_SW_SW を指定されたものとみなす。引数は 3 つまで(仕様)で、それぞれの型を指定する。

  • SW ... Signed Word (64bit)
  • UW ... Unsigned Word (64bit)
  • FP ... Floating Point (64bit)

実際問題としては受け取るレジスタのビット列が同じになるため SWUW は変わらないが、もしかしたら将来何か違いを出すかもしれない。尚、最後の引数から SW は省略可能。なので、以下は全部同じ意味となる。

  • Jit.ArgType.SW_SW_SW
  • Jit.ArgType.SW_SW
  • Jit.ArgType.SW

引数で渡されるレジスタは決まっており、以下のようになっている。

  • 呼び出し側
第 1 引数 第 2 引数 第 3 引数
Integer Jit.R0 Jit.R1 Jit.R2
Double Jit.FR0 Jit.FR1 Jit.FR2
  • 受け取り側
第 1 引数 第 2 引数 第 3 引数
Integer Jit.S0 Jit.S1 Jit.S2
Double Jit.FS0 Jit.FS1 Jit.FS2

呼び出し側でセットするレジスタと、受け取り側で受け取るレジスタが異なることに注意。

ConstTarget

ラベルアドレスを setLabel() で設定する。
ラベルのアドレスを即値としてレジスタやメモリに格納したいときに使う。あまり使う機会は無いか。ジャンプテーブルの代わりになるかとも思うが、テーブルの作り方がイマイチうまい仕組みを用意できていないので。

ちなみに、即値を setValue() で設定することもできるが、普通に Jit.IMM(100) とか、浮動小数点数でも Jit.IMM(0.1) とかできるようにしたので、こちらを使う意味はあまりない。

仮にジャンプテーブルに使うとした際の例は後述。

JumpTarget

ジャンプ先、もしくは関数コールのためのアドレスを setLabel() で設定する。

例えば、比較した結果で分岐させる場合、以下のようになる。

var c = new Jit.Compiler();
// 関数のエントリポイント。
c.enter();
// S0レジスタ値 >= 3
var jump0 = c.ge(Jit.S0, Jit.IMM(3));
... // 条件が偽のときのコード
var jump1 = c.jmp();
var label0 = c.label();
... // 条件が真のときのコード
var label1 = c.label();
...

jump0.setLabel(label0);
jump1.setLabel(label1);

JitCode

generate() メソッドでコード生成に成功すると、JitCode オブジェクト返る。JitCode オブジェクトのメソッドは以下の通り。尚、引数は 3 つまでしか指定できないことに注意(仕様)。抽象化アセンブラなので、様々なアーキテクチャに対応するために必要な仕様です。必要ならローカル変数領域を確保して、その先頭アドレスを渡すなどの工夫が必要。サンプルは後述。

メソッド 概要
JitCode#run(a1, a2, a3) 復帰値を Integer として受け取る。
JitCode#frun(a1, a2, a3) 復帰値を Double として受け取る。
JitCode#dump() ジェネレートされたアセンブル・リストを出力する。

サンプル

フィボナッチ数列(再帰版)

では、恒例のフィボナッチ数列を算出する再帰版のコードを書いてみましょう。サンプルとして最初に提示したものそのままです。

var c = new Jit.Compiler();
var entry1 = c.enter();
    var jump0 = c.ge(Jit.S0, Jit.IMM(3));
    c.ret(Jit.S0);
    var l1 = c.label();
    c.sub(Jit.R0, Jit.S0, Jit.IMM(2));
    c.call(entry1);
    c.mov(Jit.S1, Jit.R0);
    c.sub(Jit.R0, Jit.S0, Jit.IMM(1));
    c.call(entry1);
    c.add(Jit.R0, Jit.R0, Jit.S1);
    c.ret(Jit.R0);

jump0.setLabel(l1);
var code = c.generate();

for (var i = 1; i <= 42; ++i) {
    var tmr = new SystemTimer();
    var r = code.run(i);
    System.println("[%8.3f] fib(%2d) = %d" % tmr.elapsed() % i % r);
}

結果は以下の通り。

[   0.000] fib( 1) = 1
[   0.000] fib( 2) = 2
[   0.000] fib( 3) = 3
[   0.000] fib( 4) = 5
[   0.000] fib( 5) = 8
[   0.000] fib( 6) = 13
[   0.000] fib( 7) = 21
[   0.000] fib( 8) = 34
[   0.000] fib( 9) = 55
[   0.000] fib(10) = 89
[   0.000] fib(11) = 144
[   0.000] fib(12) = 233
[   0.000] fib(13) = 377
[   0.000] fib(14) = 610
[   0.000] fib(15) = 987
[   0.000] fib(16) = 1597
[   0.000] fib(17) = 2584
[   0.000] fib(18) = 4181
[   0.000] fib(19) = 6765
[   0.000] fib(20) = 10946
[   0.000] fib(21) = 17711
[   0.000] fib(22) = 28657
[   0.000] fib(23) = 46368
[   0.000] fib(24) = 75025
[   0.000] fib(25) = 121393
[   0.001] fib(26) = 196418
[   0.001] fib(27) = 317811
[   0.001] fib(28) = 514229
[   0.002] fib(29) = 832040
[   0.002] fib(30) = 1346269
[   0.004] fib(31) = 2178309
[   0.006] fib(32) = 3524578
[   0.009] fib(33) = 5702887
[   0.016] fib(34) = 9227465
[   0.035] fib(35) = 14930352
[   0.042] fib(36) = 24157817
[   0.066] fib(37) = 39088169
[   0.119] fib(38) = 63245986
[   0.181] fib(39) = 102334155
[   0.289] fib(40) = 165580141
[   0.476] fib(41) = 267914296
[   0.773] fib(42) = 433494437

ちなみに、fib(42) の結果を Ruby, Python, PyPy, PHP, HHVM, Kinx, Kinx(native) で計測して比較してみた。JIT ライブラリ版は上記は run() の時間しか計ってないので、スクリプト解釈と JIT コード生成まで含め、全て公平にプロセス全体の user time で算出。

速い順に並べると以下の通り。やはり JIT で直接ネイティブ・コードを出力させると際立って速い。何気に Kinx(native) が PyPy より速かったのは嬉しい誤算。HHVM とどっこいくらい。スクリプトでは Ruby 速くなったなー。1.8 時代とか知ってると感慨深いですねー。

言語 版数 User 時間
Kinx(Jit-Lib) 0.10.0 0.828
HHVM 3.21.0 2.227
Kinx(native) 0.10.0 2.250
PyPy 5.10.0 3.313
PHP 7.2.24 11.422
Ruby 2.5.1p57 14.877
Kinx 0.10.0 27.478
Python 2.7.15+ 41.125

尚、先の JIT ライブラリでジェネレートされたアセンブル・リストはこちら。Windows と Linux で違うのだが、今回は Linux。

       0:   53                                          push rbx
       1:   41 57                                       push r15
       3:   41 56                                       push r14
       5:   48 8b df                                    mov rbx, rdi
       8:   4c 8b fe                                    mov r15, rsi
       b:   4c 8b f2                                    mov r14, rdx
       e:   48 83 ec 10                                 sub rsp, 0x10
      12:   48 83 fb 03                                 cmp rbx, 0x3
      16:   73 0d                                       jae 0x25
      18:   48 89 d8                                    mov rax, rbx
      1b:   48 83 c4 10                                 add rsp, 0x10
      1f:   41 5e                                       pop r14
      21:   41 5f                                       pop r15
      23:   5b                                          pop rbx
      24:   c3                                          ret
      25:   48 8d 43 fe                                 lea rax, [rbx-0x2]
      29:   48 89 fa                                    mov rdx, rdi
      2c:   48 89 c7                                    mov rdi, rax
      2f:   e8 cc ff ff ff                              call 0x0
      34:   49 89 c7                                    mov r15, rax
      37:   48 8d 43 ff                                 lea rax, [rbx-0x1]
      3b:   48 89 fa                                    mov rdx, rdi
      3e:   48 89 c7                                    mov rdi, rax
      41:   e8 ba ff ff ff                              call 0x0
      46:   49 03 c7                                    add rax, r15
      49:   48 83 c4 10                                 add rsp, 0x10
      4d:   41 5e                                       pop r14
      4f:   41 5f                                       pop r15
      51:   5b                                          pop rbx
      52:   c3                                          ret

Const の例

Const の例として、あえて書くならこんな感じ。ローカル変数にジャンプテーブルを作っているので、毎回テーブルを作り直していてイマイチ。テーブルだけ作成してアドレスを渡せるようなインターフェースを別途用意すれば解決しそうではある(やるかも)。

var c = new Jit.Compiler();
c.enter();
    c.mov(Jit.R1, Jit.IMM(-1));
    var jump0 = c.slt(Jit.S0, Jit.IMM(0));
    var jump1 = c.sgt(Jit.S0, Jit.IMM(3));
    var const0 = c.makeConst(Jit.VAR(0));
    var const1 = c.makeConst(Jit.VAR(1));
    var const2 = c.makeConst(Jit.VAR(2));
    var const3 = c.makeConst(Jit.VAR(3));
    // ローカル変数のアドレスを S0 レジスタ(第一引数)のオフセットで取得し R0 レジスタに格納。
    c.localp(Jit.R0, Jit.S0);
    // ローカル変数の値自体を取得。
    c.mov(Jit.R0, Jit.MEM1(Jit.R0));
    // ローカル変数の中身をアドレスと見立ててジャンプ。
    c.ijmp(Jit.R0);
    var l0 = c.label();
    c.mov(Jit.R1, Jit.IMM(102));
    c.ret(Jit.R1);
    var l1 = c.label();
    c.mov(Jit.R1, Jit.IMM(103));
    c.ret(Jit.R1);
    var l2 = c.label();
    c.mov(Jit.R1, Jit.IMM(104));
    c.ret(Jit.R1);
    var l3 = c.label();
    c.mov(Jit.R1, Jit.IMM(105));
    var l4 = c.label();
    c.ret(Jit.R1);

// ジャンプアドレスはコード生成前にセットする。
jump0.setLabel(l4);
jump1.setLabel(l4);

var code = c.generate();
// const 値はコード生成後にセットする。
const0.setLabel(l0);
const1.setLabel(l1);
const2.setLabel(l2);
const3.setLabel(l3);

for (var i = -1; i < 5; ++i) {
    var r = code.run(i);
    System.println(r);
}

結果。

-1
102
103
104
105
-1

コード出力はこんな感じ。こちらは試しに Windows 版で出してみた。

       0:   53                                          push rbx
       1:   56                                          push rsi
       2:   57                                          push rdi
       3:   48 8b d9                                    mov rbx, rcx
       6:   48 8b f2                                    mov rsi, rdx
       9:   49 8b f8                                    mov rdi, r8
       c:   4c 8b 4c 24 b0                              mov r9, [rsp-0x50]
      11:   48 83 ec 50                                 sub rsp, 0x50
      15:   48 c7 c2 ff ff ff ff                        mov rdx, 0xffffffffffffffff
      1c:   48 83 fb 00                                 cmp rbx, 0x0
      20:   0f 8c 94 00 00 00                           jl 0xba
      26:   48 83 fb 03                                 cmp rbx, 0x3
      2a:   0f 8f 8a 00 00 00                           jg 0xba
      30:   49 b9 95 ff 57 61 89 01 00 00               mov r9, 0x1896157ff95
      3a:   4c 89 4c 24 20                              mov [rsp+0x20], r9
      3f:   49 b9 a7 ff 57 61 89 01 00 00               mov r9, 0x1896157ffa7
      49:   4c 89 4c 24 28                              mov [rsp+0x28], r9
      4e:   49 b9 b9 ff 57 61 89 01 00 00               mov r9, 0x1896157ffb9
      58:   4c 89 4c 24 30                              mov [rsp+0x30], r9
      5d:   49 b9 cb ff 57 61 89 01 00 00               mov r9, 0x1896157ffcb
      67:   4c 89 4c 24 38                              mov [rsp+0x38], r9
      6c:   48 8d 44 24 20                              lea rax, [rsp+0x20]
      71:   48 6b db 08                                 imul rbx, rbx, 0x8
      75:   48 03 c3                                    add rax, rbx
      78:   48 8b 00                                    mov rax, [rax]
      7b:   ff e0                                       jmp rax
      7d:   48 c7 c2 66 00 00 00                        mov rdx, 0x66
      84:   48 89 d0                                    mov rax, rdx
      87:   48 83 c4 50                                 add rsp, 0x50
      8b:   5f                                          pop rdi
      8c:   5e                                          pop rsi
      8d:   5b                                          pop rbx
      8e:   c3                                          ret
      8f:   48 c7 c2 67 00 00 00                        mov rdx, 0x67
      96:   48 89 d0                                    mov rax, rdx
      99:   48 83 c4 50                                 add rsp, 0x50
      9d:   5f                                          pop rdi
      9e:   5e                                          pop rsi
      9f:   5b                                          pop rbx
      a0:   c3                                          ret
      a1:   48 c7 c2 68 00 00 00                        mov rdx, 0x68
      a8:   48 89 d0                                    mov rax, rdx
      ab:   48 83 c4 50                                 add rsp, 0x50
      af:   5f                                          pop rdi
      b0:   5e                                          pop rsi
      b1:   5b                                          pop rbx
      b2:   c3                                          ret
      b3:   48 c7 c2 69 00 00 00                        mov rdx, 0x69
      ba:   48 89 d0                                    mov rax, rdx
      bd:   48 83 c4 50                                 add rsp, 0x50
      c1:   5f                                          pop rdi
      c2:   5e                                          pop rsi
      c3:   5b                                          pop rbx
      c4:   c3                                          ret

7b 行目の jmp rax がポイント。テーブルを静的に定義できるようになればジャンプテーブルとして機能するようになるかと(今は簡単にできる方法が無い...)。

4 つ以上の引数の例

ちょっと面倒くさいが、4 つ以上引数を渡したい場合は、ローカル変数領域に値を格納し、そのアドレス(ポインタ)を引数として渡す。以下の例では、最初に引数をローカル変数領域にセットするためのフック関数を経由させている。ちなみに、ローカル変数は全て 8 バイトで確保されるため、直接 Jit.MEM1() などでアクセスする場合のオフセットは 8 の倍数でないと合わないので注意。

var c = new Jit.Compiler();
var entry1 = c.enter();
    c.mov(Jit.VAR(0), Jit.S0);
    c.mov(Jit.VAR(1), Jit.IMM(3));
    c.mov(Jit.VAR(2), Jit.IMM(2));
    c.mov(Jit.VAR(3), Jit.IMM(1));
    c.localp(Jit.R0);
    var call1 = c.call();
    c.ret(Jit.R0);
var entry2 = c.enter();
    c.mov(Jit.R1, Jit.S0);
    c.mov(Jit.S0, Jit.MEM1(Jit.R1, 0));
    var jump0 = c.ge(Jit.S0, Jit.MEM1(Jit.R1, 8));
    c.ret(Jit.S0);
    var l1 = c.label();
    c.sub(Jit.R3, Jit.S0, Jit.MEM1(Jit.R1, 16));
    c.mov(Jit.VAR(0), Jit.R3);
    c.mov(Jit.VAR(1), Jit.IMM(3));
    c.mov(Jit.VAR(2), Jit.IMM(2));
    c.mov(Jit.VAR(3), Jit.IMM(1));
    c.localp(Jit.R0);
    c.call(entry2);
    c.mov(Jit.S1, Jit.R0);
    c.sub(Jit.R3, Jit.S0, Jit.MEM1(Jit.R1, 24));
    c.mov(Jit.VAR(0), Jit.R3);
    c.mov(Jit.VAR(1), Jit.IMM(3));
    c.mov(Jit.VAR(2), Jit.IMM(2));
    c.mov(Jit.VAR(3), Jit.IMM(1));
    c.localp(Jit.R0);
    c.call(entry2);
    c.add(Jit.R0, Jit.R0, Jit.S1);
    c.ret(Jit.R0);

jump0.setLabel(l1);
call1.setLabel(entry2);
var code = c.generate();

for (var i = 1; i <= 42; ++i) {
    var tmr = new SystemTimer();
    var r = code.run(i);
    System.println("[%8.3f] fib(%2d) = %d" % tmr.elapsed() % i % r);
}

出力はさっきと同じ。

Double の引数と復帰値

Double 紹介していないのでそれも。こちらもフィボナッチでいきましょう。しかし、俺はフィボナッチ大好きだな。気づかなかったけど。0.1 刻みバージョンです。

var c = new Jit.Compiler();
var entry1 = c.enter(Jit.ArgType.FP);
    c.mov(Jit.FR0, Jit.IMM(0.3));
    var jump0 = c.ge(Jit.FS0, Jit.FR0);
    c.ret(Jit.FS0);
    var l1 = c.label();
    c.mov(Jit.FR0, Jit.IMM(0.2));
    c.sub(Jit.FR0, Jit.FS0, Jit.FR0);
    c.call(entry1);
    c.mov(Jit.FS1, Jit.FR0);
    c.mov(Jit.FR0, Jit.IMM(0.1));
    c.sub(Jit.FR0, Jit.FS0, Jit.FR0);
    c.call(entry1);
    c.add(Jit.FR0, Jit.FR0, Jit.FS1);
    c.ret(Jit.FR0);

jump0.setLabel(l1);
var code = c.generate();

for (var i = 0.1; i < 3.5; i += 0.1) {
    var tmr = new SystemTimer();
    var r = code.frun(i);
    System.println("[%8.3f] fib(%3.1f) = %.1f" % tmr.elapsed() % i % r);
}

浮動小数点数の即値は直接比較メソッドで使えるようにしていないので(すればいいんだけど)一旦レジスタに格納して使う必要がある。

frun() することで Double 値を受け取れる。結果は以下の通り。

[   0.000] fib(0.1) = 0.1
[   0.000] fib(0.2) = 0.2
[   0.000] fib(0.3) = 0.3
[   0.000] fib(0.4) = 0.5
[   0.000] fib(0.5) = 0.8
[   0.000] fib(0.6) = 1.3
[   0.000] fib(0.7) = 2.1
[   0.000] fib(0.8) = 3.4
[   0.000] fib(0.9) = 5.5
[   0.000] fib(1.0) = 8.9
[   0.000] fib(1.1) = 14.4
[   0.000] fib(1.2) = 23.3
[   0.000] fib(1.3) = 37.7
[   0.000] fib(1.4) = 61.0
[   0.000] fib(1.5) = 98.7
[   0.000] fib(1.6) = 159.7
[   0.000] fib(1.7) = 258.4
[   0.000] fib(1.8) = 418.1
[   0.000] fib(1.9) = 676.5
[   0.000] fib(2.0) = 1094.6
[   0.000] fib(2.1) = 1771.1
[   0.000] fib(2.2) = 2865.7
[   0.000] fib(2.3) = 4636.8
[   0.000] fib(2.4) = 7502.5
[   0.000] fib(2.5) = 12139.3
[   0.001] fib(2.6) = 19641.8
[   0.001] fib(2.7) = 31781.1
[   0.002] fib(2.8) = 51422.9
[   0.003] fib(2.9) = 83204.0
[   0.004] fib(3.0) = 134626.9
[   0.006] fib(3.1) = 217830.9
[   0.015] fib(3.2) = 352457.8
[   0.020] fib(3.3) = 570288.7
[   0.027] fib(3.4) = 922746.5

出力コードは以下の通り。これも Windows 版。浮動小数点数を引き渡すために、最初に簡単なフック関数がある。SLJIT は関数のエントリポイントでの引数で浮動小数点数を指定できないので、こういう形で回避している。

そういう意味でも、SLJIT を直接使うよりこっちを使うほうが色々面倒見てくれて良い。ローカル変数領域で必要なサイズを自動的に計算したり、非破壊レジスタのための一時保存コードなども必要な数行うように自動で計算もさせているので。

       0:   53                                          push rbx
       1:   56                                          push rsi
       2:   57                                          push rdi
       3:   48 8b d9                                    mov rbx, rcx
       6:   48 8b f2                                    mov rsi, rdx
       9:   49 8b f8                                    mov rdi, r8
       c:   4c 8b 4c 24 d0                              mov r9, [rsp-0x30]
      11:   48 83 ec 30                                 sub rsp, 0x30
      15:   0f 29 74 24 20                              movaps [rsp+0x20], xmm6
      1a:   f2 0f 10 03                                 movsd xmm0, qword [rbx]
      1e:   48 89 f2                                    mov rdx, rsi
      21:   49 89 f8                                    mov r8, rdi
      24:   48 89 c1                                    mov rcx, rax
      27:   e8 0d 00 00 00                              call 0x39
      2c:   0f 28 74 24 20                              movaps xmm6, [rsp+0x20]
      31:   48 83 c4 30                                 add rsp, 0x30
      35:   5f                                          pop rdi
      36:   5e                                          pop rsi
      37:   5b                                          pop rbx
      38:   c3                                          ret
      39:   53                                          push rbx
      3a:   56                                          push rsi
      3b:   57                                          push rdi
      3c:   48 8b d9                                    mov rbx, rcx
      3f:   48 8b f2                                    mov rsi, rdx
      42:   49 8b f8                                    mov rdi, r8
      45:   4c 8b 4c 24 b0                              mov r9, [rsp-0x50]
      4a:   48 83 ec 50                                 sub rsp, 0x50
      4e:   0f 29 74 24 20                              movaps [rsp+0x20], xmm6
      53:   f2 0f 11 6c 24 38                           movsd [rsp+0x38], xmm5
      59:   f2 0f 10 f0                                 movsd xmm6, xmm0
      5d:   49 b9 33 33 33 33 33 33 d3 3f               mov r9, 0x3fd3333333333333
      67:   4c 89 4c 24 40                              mov [rsp+0x40], r9
      6c:   f2 0f 10 44 24 40                           movsd xmm0, qword [rsp+0x40]
      72:   66 0f 2e f0                                 ucomisd xmm6, xmm0
      76:   73 17                                       jae 0x8f
      78:   f2 0f 10 c6                                 movsd xmm0, xmm6
      7c:   f2 0f 10 6c 24 38                           movsd xmm5, qword [rsp+0x38]
      82:   0f 28 74 24 20                              movaps xmm6, [rsp+0x20]
      87:   48 83 c4 50                                 add rsp, 0x50
      8b:   5f                                          pop rdi
      8c:   5e                                          pop rsi
      8d:   5b                                          pop rbx
      8e:   c3                                          ret
      8f:   49 b9 9a 99 99 99 99 99 c9 3f               mov r9, 0x3fc999999999999a
      99:   4c 89 4c 24 40                              mov [rsp+0x40], r9
      9e:   f2 0f 10 44 24 40                           movsd xmm0, qword [rsp+0x40]
      a4:   f2 0f 10 e6                                 movsd xmm4, xmm6
      a8:   f2 0f 5c e0                                 subsd xmm4, xmm0
      ac:   f2 0f 11 e0                                 movsd xmm0, xmm4
      b0:   48 89 c1                                    mov rcx, rax
      b3:   e8 81 ff ff ff                              call 0x39
      b8:   f2 0f 10 e8                                 movsd xmm5, xmm0
      bc:   49 b9 9a 99 99 99 99 99 b9 3f               mov r9, 0x3fb999999999999a
      c6:   4c 89 4c 24 40                              mov [rsp+0x40], r9
      cb:   f2 0f 10 44 24 40                           movsd xmm0, qword [rsp+0x40]
      d1:   f2 0f 10 e6                                 movsd xmm4, xmm6
      d5:   f2 0f 5c e0                                 subsd xmm4, xmm0
      d9:   f2 0f 11 e0                                 movsd xmm0, xmm4
      dd:   48 89 c1                                    mov rcx, rax
      e0:   e8 54 ff ff ff                              call 0x39
      e5:   f2 0f 58 c5                                 addsd xmm0, xmm5
      e9:   f2 0f 10 6c 24 38                           movsd xmm5, qword [rsp+0x38]
      ef:   0f 28 74 24 20                              movaps xmm6, [rsp+0x20]
      f4:   48 83 c4 50                                 add rsp, 0x50
      f8:   5f                                          pop rdi
      f9:   5e                                          pop rsi
      fa:   5b                                          pop rbx
      fb:   c3                                          ret

おわりに

JIT 面白いですね。これでパーサ・コンビネータとか実装して組み合わせたらたら、ちょっとした JIT 付き言語処理系が作れますね。そういう道を目指してもいいかも。

おそらく想定する使い方としては次の 2 通りでしょうか。

  1. Kinx のライブラリ作成の際に数値計算等の範囲で JIT 化し、高速化する。
  2. DSL(ドメイン固有言語)やオレオレ言語のホストとなり、バックエンドの出力に使う。

ではまた。

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

【Docker入門】Deno環境をDockerで構築する??

はじめに

Docker入門者の学習記事です!

Deno が盛り上がって来ているように見えます。?

興味をそそられたので、学習も兼ねてさっそくDockerでコンテナ化してみました。

ゴール

  • Dockerfileを自作して、イメージを作ってみる
  • コンテナにDenoをインストールして、触れる環境を作る
  • Denoの標準APIを使用して、Webサーバーを立ち上げる

Dockerfileを作成

コンテナイメージの元となるDockerfileを作成します。

最終的には、以下のようなものが出来上がりました。

FROM debian:stable-slim

WORKDIR /var/www/html

RUN apt-get -qq update \
  && apt-get -qq -y install curl zip unzip \
  && curl -fsSL https://deno.land/x/install/install.sh | sh \
  && apt-get -qq remove curl zip unzip \
  && apt-get -qq remove --purge -y curl zip unzip \
  && apt-get -qq -y autoremove \
  && apt-get -qq clean \
  && echo 'export DENO_INSTALL="/root/.deno"' >> ~/.bash_profile \
  && echo 'export PATH="$DENO_INSTALL/bin:$PATH"' >> ~/.bash_profile

CMD ["/bin/bash", "-c", "source ~/.bash_profile && bash"]

Dockerfileの内容

基本設定

元となるOSイメージ: 今回は軽量なdebian-slimを選択

FROM debian:stable-slim

今回はWebサーバーを起動させるので、/var/www/htmlを作業ディレクトリに指定してみました。

WORKDIR /var/www/html

Denoのインストール

基本的には、Deno公式のやり方と同じです。

まずは、Denoのインストールと、シェルスクリプトの実行に必要なcurl zip unzipをインストールします。

RUN apt-get -qq -y install curl zip unzip

Deno公式のやり方と同じようにインストールコマンドを実行。

RUN curl -fsSL https://deno.land/x/install/install.sh | sh

あとしまつ

不要なファイルを削除します。

—purgeオプションを使用すると設定ファイルごとすべて削除します。再び使わないようなパッケージに使用。

RUN apt-get -qq remove --purge -y curl zip unzip

不要になった依存パッケージまで削除

RUN apt-get -qq -y autoremove

パスを通す

denoコマンドを使用するために、パスを通してやる必要があります。

RUN echo 'export DENO_INSTALL="/root/.deno"' >> ~/.bash_profile \
  && echo 'export PATH="$DENO_INSTALL/bin:$PATH"' >> ~/.bash_profile

たった今変更したプロファイルを、bashに適用します。

CMDにはコンテナ起動直後に実行されるコマンドを記述します。

コンテナ起動後も引き続きbashを使用するので、最後に&& bashを引っ付けておきました。

CMD ["/bin/bash", "-c", "source ~/.bash_profile && bash"]

イメージのビルド

-tオプションをつけることで、名前をつけてイメージをビルドします。今回はdeno-dockerと名付けました。

Dockerfileを作ったディレクトリで、以下のコマンドを実行します。

docker build ./ -t deno-docker

コンテナの起動

以下のコマンドで、コンテナを起動します。

ちょっと長いですが...

docker container run -it --rm -p 8080:8080 --mount src=`pwd`,target=/var/www/html,type=bind --disable-content-trust deno-docker

1行づつコメントを以下につけました。

docker container run \ # コンテナ起動
-it \ # コンテナに入って標準入力を待ち受ける
-p 8080:8080 \ # ポートの指定(ホストのポート:コンテナのポート)
--rm \ # コンテナを停止したら自動でコンテナ廃棄する
--mount src=`pwd`,target=/var/www/html,type=bind\ # バインドマウント
--disable-content-trust\ # 証明書のないイメージを信頼する
deno-docker # 先ほど作成したイメージ

DenoでWebサーバーを立ち上げる

コチラも公式チュートリアルのまんまです。

詳しい解説は、公式から確認できます。

helloDeno.ts
import { serve } from "https://deno.land/std@0.50.0/http/server.ts";

const s = serve({ port: 8080 }); // さっきコンテナに指定したポート
console.log("http://localhost:8080/");

for await (const req of s) {
  req.respond({ body: "<h1>Hello Deno</h1>\n" });
}
  • Denoは標準でTypescriptをサポートしています! 面倒な設定ともオサラバ?✋

  • asyncなしで、awaitが使用可能です!

denoコマンドで実行する

先ほどバインドマウントしたディレクトリに、Typescriptファイル(今回はhelloDeno.ts)を保存し、
以下のコマンドを実行します。

deno run --allow-net helloDeno.ts

Denoは標準でファイル、ネットワーク、環境変数等にアクセスすることができないセキュアな設定になっています。

ネットワークを許可するには--allow-netオプションを付与して実行します。

スクリーンショット 2020-06-29 22.37.40.png

見事、Webサーバーが起動できました!

まとめ

  • Dockerfileを自作して、イメージを作ることができた
  • コンテナにDenoをインストールして、(少しだけど)Denoプログラムを書けた
  • Denoの標準APIを使用して、Webサーバーを立ち上げた

参考

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

nodejsを使ってウエブサーバーを作ろう - majidai

Build StatusGitHub license

majidaiとは

majidaiとは、nodejs用のWebフレームワークです。
サードパーティーライブラリが使っていないため非常に軽いです。
データ量 < 50KB

インストール

nodejsがインストールはこの記事のスコープ外とします。

1. プロジェクトの初期化

タミナルから作業フォルダーに移動し、プロジェクトの初期化を行います。

npm init -y
2. majidaiインストール

以下のコマンドで「majidai」をインストールします。

npm install majidai
3. サーバーコード

以下のコマンドで「majidai」をインストールします。

server.js
// majidaiの読み込み
const majidai = require("majidai");

// インスタンス化
const server = new majidai();

// 受付開始
server.start();
4. serverの起動

以下のコマンドでウエブサーバーの起動します。

node server.js

そして、ブラウザを起動し、http://localhostにアクセスします。以下の画面が表示されればOKです。

デフォルトでは80ポートで受け付けしますが、以下のようにポートを指定する事が可能です。

const config = {
    http: {
        port: 8000
    }
};
const server = new majidai(config);

静的コンテンツの配信

デフォルトでは静的コンテンツの配信が無効にしてますが、以下のように「Document Root」指定する事で指定のフォルダーは以下のものを配信する事が可能です。

const config = {
    http: {
        documentRoot: './public'
    }
};
const server = new majidai(config);

使い方

majidaiの書き方は以下のようになります。

server.METHOD(PATH, CALLBACK)
METHODとは

以下の3種類があります。
- get : http GETリクエストのみ受け付けます
- post : http POSTリクエストのみ受け付けま
- listen :複数のhttpメソッドを一つのPATHで受け付けます
※詳細についてはこちら

PATHとは

"/" , "/top", "/user/dakc",..みたいにどこでリクエストを受け付けるかの事です。

CALLBACKとは

これはコールバック関数の事です。指定のPATHにリクエストを受け付けたら、この関数が実行されます。関数の引数が以下の2つです。
- request : <http.IncomingMessage>
- response : <http.ServerResponse>
majidaiが両方のオブジェクトに「mj」オブジェクトを追加します。

request.mj

requestに追加したmjオブジェクトがクライアントから送られて来たデータを操作するために以下の2つの関数(メソッド)を有します。

  • getParams : GETで送られて来た情報をJSONオブジェクトとして返します。キーを引数として指定することで特定のキーに対する値のみを取得する事が可能です。
  • postParams: POSTで送られて来た情報をJSONオブジェクトとして返します。キーを引数として指定することで特定のキーに対する値のみを取得する事が可能です。※x-www-form-urlencoded, application/json, form-dataどれでデータが送られて来てもJSONオブジェクトとして返します。
response.mj

クライアントにレスポンスを返すために以下のメソッドを有します。

  • text : plain/textとしてレスポンス
  • json : application/jsonとしてレスポンス
  • static : 静的コンテンツのレスポンス
  • redirect : 別のURLへリダイレクト
  • error : HTTPエラーとしてレスポンス

※ 詳細についてはこちら

samples

サンプルを見ながらどのようなことできるかやってみましょう!

1. GET受付
get-sample.js
const server = new majidai();

server.get("/home", (request,response) => {
    // リクエストがhttp://〇〇/home?price=230)
    var price = request.mj.getParams("price");
    console.log(price);
    // 230

    // OR
    // 全データをJsonオブジェクトとして取得
    var getData = request.mj.getParams();
    console.log(getData);
    // { price: '230' }

    // 「price」の値を取得
    var price_ = getData.price;
    console.log(price);
    // 230


    return "ここで返すものがクライアントへのレスポンスになります";
});

server.start();
2. POST受付
post-sample.js
const server = new majidai();

server.post("/login", (request,response) => {
    // リクエストがhttp://〇〇/login
    // <form action="/login" method="POST">
    //   <input name="id" type="text" value="abc">
    //   <input name="pass" type="password" value="p@ss">
    // </form>

    // 「id」の値を取得
    var id= request.mj.postParams("id");
    console.log(id);
    // abc

    // OR
    // 全データをJsonオブジェクトとして取得
    var postData = request.mj.postParams();
    console.log(postData);
    // { id: 'abc', pass: 'p@ass' }

    // 「id」の値を取得
    var id_ = postData.id;
    console.log(id_);
    // abc


    return "ここで返すものがクライアントへのレスポンスになります";
});

server.start();
3. 複数HTTPメソッド受付
multiple-methods.js
const server = new majidai();

server.listen({ method: ["GET", "POST"], path: "/profile" }, (request, response) => {
    // GETでもPOSTでもこのの処理を実行されます。
    // DO SOMETHING

    return "something";
});

server.start();

Parameterized URL

どのような表現が分かりやすいかよくわからなかったため、上記の表記にしました。ごめんなさい。何か気付くところがありましたら是非コメントに残してやってください。

この機能がどうしても実現したかった機能です。
「http://〇〇/user?id=abdf&command=edit」より
「http://〇〇/user/abdf/edit」
ほうが好きだったからです。

parameterized-url.js
// {}の中のものがその名前のキーでデータを取得する事ができます。
server.get("/books/{year}/{price}", function (req, res) {
    // リクエストがhttp://〇〇/books/2020/5000

    // {}の中のものがその名前のキーでデータを取得する事ができます。
    // 「year」の値を取得
    var year = req.mj.getParams("year");
    console.log(year );
    // 2020

    // 「price」の値を取得
    var price= req.mj.getParams("price");
    console.log(price);
    // 5000

    // GETと受けたものをレスポンスとして返す
    return req.mj.getParams();
});

※post,listenの時もParameterizedURL機能が使えます。

Docker

以下の1行のコマンドを実行する事でブラウザーからmajidaiの動作を確認できます。

docker run -it --rm -p 80:80 dakc/majidai npx /data/server.js

最後に

nodejsの標準の機能のみで動く軽量なフレームワークを欲しかったため、majidaiを開発しました。MITライセンスでリリースしておりますのでご自由にお使いください。

DOCUMENTATION - https://dakc.github.io/majidai.html

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

ある一定の所までスクロールすると下に出てくるウザいバナーの出し方。コピペで使える。

くそサイトでよく見かけるアレです。

成果物

https://youthful-mccarthy-ce6430.netlify.app

リポジトリ

https://github.com/yuzuru2/annoying_banner

index.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>ウザいバナー</title>
    <style>
      body {
        margin: 0;
      }

      #banner {
        display: none;
        width: 100%;
        position: fixed;
        bottom: 0;
      }

      #banner img {
        width: 100%;
        position: relative;
      }
    </style>
  </head>
  <body>
    <div>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
      <h3>ある一定の所までスクロールするとウザいバナーが出る</h3>
    </div>

    <footer>
      <!-- ウザいバナー -->
      <div id="banner">
        <img src="img/banner.png" />
      </div>
    </footer>

    <!-- jquery読み込み -->
    <script src="https://code.jquery.com/jquery-3.5.1.js"></script>

    <script>
      $(function () {
        var _dom = $('#banner');

        // スクロールした時
        $(window).on('scroll', function () {
          if (document.documentElement.scrollTop <= 800) {
            if (_dom.css('display') === 'block') {
              _dom.fadeOut(1000);
            }
            return;
          }

          if (document.documentElement.scrollTop > 800) {
            if (_dom.css('display') === 'none') {
              _dom.fadeIn(500);
            }
          }
        });
      });
    </script>
  </body>
</html>

img/banner.png

banner.png

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

【javascript】Object.assign

Object.assignについて

Object.assignは引数として任意のオブジェクトを受け取り、後続のオブジェクトの値をすべて最初のオブジェクトに割り当てルシ組になってます。

いずれかのオブジェクトに同じキーが含まれている場合はリスト右側のオブジェクトによってオブジェクトが上書きされます。

let a = { x: 9, y: 2, z: 5};
let b = { x: 4, y: 28};
let c = { x: 21};

Object.assign(a, b, c);
console.log(a) //{x:21,y:28,z:5};

不完全なオブジェクトを受け取り欠けている値を補う、またはカスタマイズが可能になります。

Object.assing使用時のミューテーション回避

Object.assignが変更するのはパラメータリストの最初のオブジェクトであり、残りは変更されない。
上記の例だとオブジェクトaを上書きしていることになる。aは変更せずコピーを作成し、それを変更したい時にはどうするか。
常套手段は、最初のオブジェクトを空のオブジェクトリテラルにすること。

let newObject = {};
Object.assign(newObject, { foo: 1}, { bar: 2});
console.log(newObject); //{foo: 1, bar: 2}

これは以下のように簡略化もできる。

let newObject = Object.assign({}, { foo: 1}, { bar: 2});
console.log(newObject); //{foo: 1, bar: 2}

Object.assignが値を割り当てる仕組み

ここまでを見ているとObject.assignは右のオブジェクトを左にオブジェクトにマージすると思ってしまうが、必ずしもそうではない。

  1. Object.assignは右のオブジェクトを左のオブジェクトに割り当てているだけ。
  2. 代入を実行するだけで再定義をしない。
  3. 割当の対象となるのはオブジェクト自体の列挙可能なプロパティだけ 3に関してはオブジェクトのプロトタイプチェーンのプロパティには割り当てられないよ言うことである。

Object.assignまとめ

  • Object.assign複数オブジェクトのプロパティからの値をベースオブジェクトに割り当てる。
  • Object.assignはデフォルト値を設定するために使用できる。
  • Object.assignは。オブジェクトを新しいプロパティで拡張するために使用できる。
  • Object.assignは変更されたオブジェクトを返すため、最初の引数として{}を使用するとコピーできる。
  • Object.assignはプロパティに値を割り当てるだけで、プロパティを再定義しない。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】画像プレビュー機能の実装

目標

ezgif.com-video-to-gif.gif

開発環境

・Ruby: 2.5.7
・Rails: 5.2.4
・Vagrant: 2.2.7
・VirtualBox: 6.1
・OS: macOS Catalina

前提

下記実装済み。

Slim導入
Bootstrap3導入
Font Awesome導入
ログイン機能実装
投稿機能実装

実装

1.ビューを編集

users/edit.html.slim
/ 追記
= f.file_field :profile_image, class: 'img_field', style: 'display:none;'
= attachment_image_tag @user, :profile_image, fallback: 'no-image.png', onClick: "$('.img_field').click()", class: 'center-block img-thumbnail img-responsive img_prev'
br

【解説】

file_fielddisplay:noneの非表示にし、クラスを付与する。

= f.file_field :profile_image, class: 'img_field', style: 'display:none;'

で付与したクラスのHTML(file_field)がクリックされたらJavaScriptの処理を実行する。

onClick: "$('.img_field').click()"

2.appliacation.scssを編集

appliacation.scss
// 追記
.img_prev:hover {
  cursor: pointer; 
  opacity: 0.7;
  transform: scale(1.01, 1.01);
}

【解説】

① 画像にマウスが乗っている時にCSSを反映させる。

.img_prev:hover {}

② マウスカーソルをポインターに変更する。

cursor: pointer; 

③ 不透明度を低くして、画像を少し白くする。

opacity: 0.7;

④ 少しだけ画像を拡大する。

transform: scale(1.01, 1.01);

3.JavaScriptファイルを作成・編集

ターミナル
$ touch app/assets/javascripts/image_preview.js
image_preview.js
$(function () {
  function readURL(input) {
    if (input.files && input.files[0]) {
      var reader = new FileReader();
      reader.onload = function (e) {
        $('.img_prev').attr('src', e.target.result);
      }
      reader.readAsDataURL(input.files[0]);
    }
  }

  $('.img_field').change(function () {
    readURL(this);
  });
});

【解説】

1.ビューを編集の② で実行する処理。

$('.img_field').change(function () {
  readURL(this);
});
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScriptの型記法「オブジェクトリテラル 」と「インデックスシグネチャ」を理解する

TypeScriptの型において、「オブジェクトリテラル」「インデックスシグネチャ」について、まとめます。

「オブジェクトリテラル」も「インデックスシグネチャ」も、あまり聞き慣れない言葉で、雰囲気で理解した気になっていませんか?

私はなっていました。

私の場合、この記法に遭遇するたびに「これって、なんだっけ?」ってなることが多いです。

自分の理解のために、ここにまとめておきたいと思います。

誰かの理解の助けになれば幸いです。

初見殺しな「オブジェクトリテラル」と「インデックスシグネチャ」

例えば、以下のコードにおいて

let obj: {
    [key: string]: string // インデックスシグネチャ
} // オブジェクトリテラル

変数objの型が、何を意味しているか理解できますでしょうか。
私には、初見殺しすぎて、さっぱりわからず「???」となりました。

この型の意味は、「変数objは複数のkeyを含む可能性があり、そのすべてのstring型のkeyは、string型の値をもつ」となります。

以下のコードにおいて、Errorの箇所では、idにnumber型の値を入れているのでエラーとなります。

let obj: {
    [key: string]: string // インデックスシグネチャ
} // オブジェクトリテラル

// OK
obj = {
    id: "123"
    token: "hogetoken"
    name: "fuganame"
}

// Error
// Type 'number' is not assignable to type 'string'.
obj = {
    id: 123
    token: "hogetoken"
    name: "fuganame"
} 

オブジェクトリテラルとは

let obj: {
    id: string
    token: string
    name: string
} // オブジェクトリテラル

このように中括弧({})を用いて、オブジェクトの型を定義する記法のことを指します。
オブジェクトは、ハッシュテーブルの一種で、キーと値の組みを持ちます。
TypeScriptでは、多くの場合、オブジェクトの型は「クラス」を用いて定義されます。

しかし、用途によってはクラスを定義せずに、即席の型定義を記述する方が便利な場合があります。
その際に用いられるのが「オブジェクトリテラル」です。
「オブジェクトリテラル」は、匿名のオブジェクト型を宣言したい場合に用いられます。
(例えばJavaScriptからTypeScriptへの移行期に、"とりあえず"の型を定義したい場合など)

インデックスシグネチャとは

let obj: {
    // [key: T]: U
    [key: string]: string // インデックスシグネチャ
}

インデックスシグネチャは、角括弧([])を用いて「[key: T]: U」という構文で記述されます。
これは「オブジェクトが、複数のkey(型T)を含む可能性があること」を示します。
そして、「すべてのkey(型T)は型Uの値を持つこと」も示します。
オブジェクトのキーと値の型を抽象化して記述することができます。

備考

例えば、idだけnumber型で、その他のパラメータはstringの場合、以下のように書けるのかなと思ったのですが、これはエラーになります。

let obj: {
    id: number // エラー
    [key: string]: string
}

// Property 'id' of type 'number' is not assignable to string index type 'string'.
// Type '{ id: number; token: string; name: string; }' is not assignable to type 
// '{ [key: string]: string; id: number; }'. Property 'id' is incompatible with 
// index signature. Type 'number' is not assignable to type 'string'.

obj = {
    id: 123
    token: "hogetoken"
    name: "fuganame"
} 

keyがstringの場合は、stringの値を持つと宣言してしまっているので、

    [key: string]: string

idをnumber型だと定義すると矛盾が発生してしまうためです。

    id: number

よって、この場合はおとなしく、インデックスシグネチャを用いないで書きましょう。

let obj: {
    id: number
    token: string
    name: string
} // オブジェクトリテラル

まとめ

・オブジェクトリテラル({})は、オブジェクトの型を即席で定義するために用いられる。
・インデックスシグネチャ([key: T]: U)は、オブジェクトが、複数のkey(型T)を含む可能性があり値はU型になることを示している。

結論としては、オブジェクトリテラルもインデックスシグネチャもクセが強いので、あまり用いたくないですが、そうは言っても現場ではすでにオブジェクトリテラルやインデックスシグネチャを使用している箇所については、コードを読むために理解しておきたいと思います。

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

CLS とは?ブラウザとNode.jsで CLS を実装してみます

資料を調べる際に、CLS の存在を知りました。エンジニアリングで結構いいデカップリングのやり方と感じまして、シェアしたいと思います。

シチュエーション

ブラウザか、サーバーのNode.jsか、どっちでもエラーハンドリング、ユーザートラッキングのニーズは日常茶飯事。例えユーザーを特定しなくても、id をつけて、ユーザーの行為を追跡して、エラーの再現にも重要し、プロダクトの改善にも役たちます。

仮に今エラーハンドリングを書こうと思って、このエラーハンドリングはすべてのエラーを処理しますが、どのリクエストから生み出したエラーを知りたいと

log.error("Error occured", req);

このハンドリングは req と結合しちゃった

仮に今このエラーどのユーザーから出たエラー、ユーザーが何をやったかを知りたいと

log.info("User has done xxx", user);
log.error("Error occured by", user);

ユーザーとも結合しちゃった

この2つの例は一見するとそんなに大きいな問題ではなさそう、ただ2つのパラメータが増えただけじゃ。
だけど、大型サービスを作る時、どんどん増えた機能に対して、関数の引数と関数の長さと共にどんどん伸びちゃって気持ち悪くてリファクタリングしようとしょうもないこと、少なくありませんでしょうか?

解決してみよう

関数が同期のであれば、グローバルで変数につけたらいいじゃんー

const global = {};
$("button").click((event) => {
  global.event = event;
  log("button clicked");
});

function log(...args) {
  console.log(global.event, ...args); // { x: xxx, y: xxx, target: xxx } 'button clicked'
  // other logic
}

だが、非同期関数のであれば

const global = {};
$("button").click((event) => {
  global.event = event;
  setTimeout(() => {
    log("button clicked");
  }, 1000);
});

function log(...args) {
  console.log(global.event, ...args);
  // other logic
}

すべての global.event は同じイベントになちゃった(´;ω;`)!それはだめですね。

我々必要なのは非同期呼び出しチェーンに最初から最後まで持続的なストレージ、
もしくは今走ってる非同期関数の呼び出しの唯一の識別子。

CLS が登場

他の言語では、Thread-local storageと呼ばれるものがあります。が JavaScript はマルチスレッドはありません(Web Workerなどはメインと関係ないし、自分でもマルチスレッドしない)。CLS という名前は TLS みたいに関数型プログラミングからの Continuation-passing style 名前をもらって、Continuation-local Storage、そのチェインの呼び出しの中で持続的データストレージをメンテナンスする。

ブラウザの解決 Zone.js

どうのように解決したかちょっと見てみましょう

$('button').click(event => {
  Zone.current.fork({
    name: 'clickZone',
    properties: {
      event
    }
  }).run(
    setTimeout(() => {
      log('button clicked');
    }, 1000);
  );
});

function log(...args) {
  console.log(global.event, ...args);
  // other logic
}

Zone.js は Angular 2.0 から誕生したもので、もちろん他の機能も持ってる。

この方法は残念なところがあります

考えてみましょう、 Zone.js はどうやってこれを実現しました。ブラウザは呼び出しに対して唯一の識別子を提供するAPIがなければ、すべての非同期関数をリライトしかできなく、そうすれば非同期が入る時と出る時 hook できて、この効果が実装できますね。

自分も書いてみました。

zone-simulation.js

const Zone = {
  _currentZone: {},
  get current() {
    return {
      ...this._currentZone,
      fork: (zone) => {
        this._currentZone = {
          ...this._currentZone,
          ...zone,
        };
        return this;
      },
      set: (key, value) => {
        this._currentZone[key] = value;
      },
    };
  },
};

(() => {
  const _setTimeout = global.setTimeout;
  global.setTimeout = (cb, timeout, ...args) => {
    const _currentZone = Zone._currentZone;
    _setTimeout(() => {
      const __after = Zone._currentZone;
      Zone._currentZone = _currentZone;
      cb(...args);
      Zone._currentZone = __after;
    }, timeout);
  };
})();

for (let i = 0; i < 10; i++) {
  const value = Math.floor(Math.random() * 100);
  console.log(i, value);
  Zone.current.fork({ i, value });
  setTimeout(() => {
    console.log(Zone.current.i, Zone.current.value);
  }, value);
}

また問題なさそうだけど、

angular with tsconfig target ES2017 async/await will not work with zone.js

ブラウザ今では完璧の解決方法はありません

実験をやってみましょう、console で下のコードを打ったら、

const _promise = Promise;
Promise = function () { console.log('rewrite by ourselves') };
new Promise(() => {}) instanceof Promise
// rewrite by ourselves
// true

async function test() {}
test() instanceof Promise
// false
test() instanceof _promise
// true

async function test() { return new Promise() }

test() instanceof Promise
// rewrite by ourselves
// false
test() instanceof _promise
// rewrite by ourselves
// true

ブラウザは、async 関数のリターンをネイティブの Promise で再ラッピングします。ネイティブ文法なので、async 関数はリライトできない。
もちろん transpiler で async 関数を generator もしくは Promise にすることは可能ですが、完璧とは言わないでしょう。

Node.js の解決 async_hooks

Node.js バージョン 8 以降出た async_hook モジュール、バージョン 14 の今でも Experimental ステータスから脱却してない。出たごろ性能に関しての議論もあったが、今はどうなってるかまだわからない状態ですが

Experimental ステータスにしても安定性としては問題なさそう、大量な Node.js のトラッキング / APM が依存していて、問題があったら issue が立てられるはずです。

性能に関する問題はここは展開しない、コードの低結合と少しパフォーマンスの低下を交換するかしないかによりますね。

使い方

async_hookscreateHook という関数を提供した、これが非同期関数のライフサイクルに hook できます、しかも唯一識別子も提供してくれますので、CLS を簡単に作れます。

cls.js

const {
  executionAsyncId,
  createHook,
} = require("async_hooks");

const { writeSync: fsWrite } = require("fs");
const log = (...args) => fsWrite(1, `${args.join(" ")}\n`);

const Storage = {};
Storage[executionAsyncId()] = {};

createHook({
  init(asyncId, _type, triggerId, _resource) {
    // log(asyncId, Storage[asyncId]);
    Storage[asyncId] = {};
    if (Storage[triggerId]) {
      Storage[asyncId] = { ...Storage[triggerId] };
    }
  },
  after(asyncId) {
    delete Storage[asyncId];
  },
  destroy(asyncId) {
    delete Storage[asyncId];
  },
}).enable();

class CLS {
  static get(key) {
    return Storage[executionAsyncId()][key];
  }
  static set(key, value) {
    Storage[executionAsyncId()][key] = value;
  }
}


// --- seperate line ---

function timeout(id) {
  CLS.set('a', id)
  setTimeout(() => {
    const a = CLS.get('a')
    console.log(a)
  }, Math.random() * 1000);
}

timeout(1)
timeout(2)
timeout(3)

Node.js バージョン 13 からオフィシャルの実装も

コミュニティの中でたくさんの CLS ライブラリーがあった上に、Node.js 13.10 から AsyncLocalStorage の API がありました。

https://nodejs.org/api/async_hooks.html#async_hooks_class_asynclocalstorage

実はこれはすでにすぐに使える CLS です。

const {
  AsyncLocalStorage,
} = require("async_hooks");

const express = require("express");
const app = express();

const session = new AsyncLocalStorage();

app.use((_req, _res, next) => {
  let userId = Math.random() * 1000;
  console.log(userId);
  session.enterWith({ userId });
  setTimeout(() => {
    next();
  }, userId);
});

app.use((_req, res, next) => {
  const { userId } = session.getStore();
  res.json({ userId });
});

app.listen(3000, () => {
  console.log("Listen 3000");
});


const fetch = require('node-fetch')

new Array(10).fill(0).forEach((_, i) => fetch('http://localhost:3000/test', {
  method: 'GET',
}).then(res => res.json()).then(console.log))

// Output:
// Listen 3000
// 355.9573987560112
// 548.3773445851497
// 716.2437886469793
// 109.84756385607896
// 907.6261832949347
// 308.34659685842513
// 407.0145853469649
// 525.820449114568
// 76.91502437038133
// 997.8611964598299
// { userId: 76.91502437038133 }
// { userId: 109.84756385607896 }
// { userId: 308.34659685842513 }
// { userId: 355.9573987560112 }
// { userId: 407.0145853469649 }
// { userId: 525.820449114568 }
// { userId: 548.3773445851497 }
// { userId: 716.2437886469793 }
// { userId: 907.6261832949347 }
// { userId: 997.8611964598299 }

参照

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

連想配列を指定した順番に並び替える

例えば、

const hoge = [
  {
    eto: '',
  },
  {
    eto: '',
  },
  {
    eto: '',
  },
  {
    eto: '',
  },
  {
    eto: '',
  },
];

のような連想配列があって、これを

const etoIndex = ['', '', '', '', ''];

の順番で並び替えたいみたいな場合、
以下のコードで解決です。

hoge.sort((a, b) =>
  etoIndex.indexOf(a.eto) - etoIndex.indexOf(b.eto),
);

sort()は1か-1を返す使い方しか知りませんでした!

参考:Array.prototype.sort() - JavaScript | MDN

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

【vue】トランジションのあらゆるタイミングでjsを走らせる【トランジションフック】

概要

トランジションが発生したタイミングでjsを処理させたいときに便利な機能

トランジションの中でも、以下のタイミングでjsの処理をさせてみる。

  1. DOMに要素が追加された後(enter)
  2. トランジションが終わるとき (after-enter)

規定に沿ったフック名をtransition要素に与えることで、各タイミングで処理が走る。

.html
<div id="app">
  <button v-on:click="hook = !hook">hook</button>
  <transition v-on:enter="enter" v-on:after-enter="afterEnter">
    <div v-if="hook">example</div>
  </transition>
</div>
.js
new Vue({
  el: '#app',
  data: {
    hook: true
  },
  methods: {
    /*1.DOMに要素が追加された後の処理*/
    enter: function(el, done) {
      console.log('enter')
      setTimeout(done, 1000)
    },
    /*2. トランジションが終わるときの処理*/
    afterEnter: function(el) {
      console.log('after-enter')
    }
  },
})

トランジションのタイミング

トランジションが発生するタイミングとして下記のように分類できる。
- 要素が追加されるとき
- 要素が削除されるとき

さらに細かく分類したときのフック名は以下の通り

要素が追加されるとき(Enterフェーズ)

  1. DOMに要素を追加: before-enter
  2. v-enterを追加: enter
  3. トランジション終了: after-enter
  4. Enterフェーズが途中でキャンセル: enter-cancelled

要素が削除されるとき(leaveフェーズ)

  1. クラス付与前: before-leave
  2. .v-leaveが追加後: leave
  3. 要素削除後: after-leave
  4. leaveフェーズが途中でキャンセル: leave-cancelled
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DenoでGoogle Chromeでもブックマークニックネーム!

はじめに

Google Chrome でもブックマークニックネーム機能を使うために、検索エンジンを追加する拡張機能を Deno で自動作成し、それを読み込むことにしました。

旧記事: Chrome でもブックマークニックネームがしたかった私は拡張機能を自動作成することにした。
削除済み旧旧記事: Python で検索エンジン拡張機能を自動生成するツールを作った(for Chrome)

Google Chrome とは

天下の Google 様がお作りになった Web ブラウザです。
残念ながら私にとって必要な機能が存在しないため使えません。
私も普段は使っていません。Firefox のほうがいいです。

Q. 必要な機能って何だよ
A. ↓

ブックマークニックネームとは

ブックマークに一意のニックネーム(キーワード)を与えることで、アドレスバー(オムニボックス)での補完をより使いやすくする機能です。

あらかじめブックマークに与えた一意の文字列をアドレスバーに入力すると、該当するブックマークが強制的に 1 位にサジェストされ、選択状態になります。
そして、そのまま Enter キーを押下することでそのブックマークが開かれます。

キーボード操作だけですぐにブックマークを開くことができるのです。
とても素晴らしい機能なのですが、なぜか天下のGoogle Chrome様にはありません。

Google Chrome でのその機能の再現方法

しかし、裏技的な感じにはなりますが再現は可能です。

カスタム検索エンジンとは

検索エンジンをカスタマイズする機能です。
検索エンジンの名前・検索エンジンを呼び出すキーワード・URLを設定画面で登録します。
通常はそのURLとして検索語句を%sに置換した文字列を指定するのですが、%sが含まれない文字列を指定した場合、ブックマークニックネームと同じような挙動をするようになります。

その自動化方法

しかし設定画面(chrome://settings/searchEngines)は恐ろしいほど使い勝手が悪いです。
腱鞘炎になります。

そこで…

拡張機能の作成

本題です。
検索エンジンを追加する拡張機能を作成し、それをWebブラウザに自動で読み込ませることで省力化を図ります。

拡張機能の仕様

細かな説明は省きますが、manifest.jsonにてchrome_settings_overridesキーを使えばsearch_providerを追加することが可能です。

しかし、拡張機能では一つしかmanifest.jsonを使えず、一つのmanifest.jsonでは一回しかchrome_settings_overridesできません。
よって、複数のブックマークを登録するためにはその分の拡張機能を作る必要があります。

拡張機能の自動作成

Denoを使いました。
src/main.tsが、src/bookmarks.tsという設定を記述したモジュールをインポートして生成します。

特に解説することがないのでGitHubを参照してください。


GitHub見に行くのが怠い人用
main.ts
#!/usr/bin/env deno run --allow-read --allow-write

// config /////////////////////////////////////////////////
const chromePath =
  '"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"';

import { bookmarks } from './bookmarks.ts';

// init ///////////////////////////////////////////////////
const tmpDir = Deno.makeTempDirSync();
Deno.chdir(tmpDir);

const textEncoder = new TextEncoder();

const generateUUID = () => {
  const $ = (a: number, b: number) =>
    (Math.floor(Math.random() * a) + b).toString(16);

  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'
    .replace(/x/g, () => $(16, 0))
    .replace(/y/g, () => $(4, 8));
};

// main ///////////////////////////////////////////////////
const extensions = bookmarks
  .map(({ key, name, url }) => {
    const searchUrl = new URL(url.replace('%s', '{searchTerms}'));

    const faviconUrl = new URL('https://www.google.com/s2/favicons');
    faviconUrl.searchParams.set('domain', searchUrl.host);

    const manifest = {
      chrome_settings_overrides: {
        search_provider: {
          encoding: 'utf-8',
          favicon_url: faviconUrl.toString(),
          is_default: false,
          keyword: key,
          name,
          search_url: searchUrl.toString(),
        },
      },
      manifest_version: 2,
      name,
      version: '1.0.0',
    };

    return manifest;
  })
  .map((manifest) => {
    const uuid = generateUUID();

    Deno.mkdirSync(uuid);
    Deno.writeFileSync(
      `${uuid}/manifest.json`,
      textEncoder.encode(JSON.stringify(manifest)),
    );

    return uuid;
  })
  .map((uuid) => Deno.realPathSync(uuid));

console.log(chromePath + ' --load-extension=' + extensions.join(','));


拡張機能の自動読み込み

できません。
よってコマンドラインからの起動時に引数として指定します。

"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --load-extension=path/to/extension-foo,path/to/extension-bar

みたいな感じです。

src/main.tsを実行すると起動用コマンドが標準出力(console.log)されるのでshにパイプして使っています。

おわりに

特に解説することがないので感想を。

Denoはいいぞ。
小難しいことなくTypeScript使えるのいいぞ。
ESModulesも標準でいい感じだしいいぞ。

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

【Javascript】ワンライナーで現在時刻をフォーマットした日付で表示する

getDay()、getHours()とかやってから繋げるのめんどくさい:confounded:

コード:writing_hand:

ワンライナー
new Date().toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
↑改行するとこう
new Date().toLocaleString(undefined, {
  month: "short",
  day: "numeric",
  hour: "2-digit",
  minute: "2-digit"
});
結果
8月10日 19:19

別のやり方

new Intl.DateTimeFormat(undefined, { [ここにオプション] }).format(new Date());

使い方

dateObj.toLocaleString([locales[, options]])

詳細:Date.prototype.toLocaleString()

  • locales "ja"とかが入ります。undefinedなら、OSのデフォルトです
  • options フォーマットを指定できます(自由度は低い)。詳しくはMDNで

※ localesが日本だと"2-digit"で表示されないことがあります:innocent:

まとめ

moment.jsのが楽です:hugging:

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

Web Components 入門 〜Class ライブラリー編〜

最初に

Web Components 入門シリーズへまたようこそ。このシリーズであなたとあなたの運命の Web Components ライブラリーの出会いをプロデュースしてみています!

前の記事で Web Components の標準仕様や Web Components を作る場合ライブラリーを使う方がいい理由について話しました。

今回は Web Component を作るためのライブラリーが使うパターンの一つ、Class ベースのパターンについて説明したいと思います。

しかし、その前に記事の後半に出てくる bundle サイズについてのお知らせです。

各ライブラリーの minify + gzip 後の bundle データの元としてBundlePhobiaを使います。

ですが、各ライブラリーのコードの書き方によってこれらを実際にアプリの一部として使う場合でのサイズの影響は大きく変わる場合はあります。

WebComponents.dev のみなさんはこういうデータを細かく分析していますので興味があればそれも読んでみてください。(このシリーズでカバーするライブラリーはだいたいこのサービスの Web IDE で試せます。)

さて、そろそろ始めましょう。


Class ベースのパターンは何ですか?

このシリーズの最初の記事でも言った通り、Web Component を作るためにはまずHTMLElementを継承する class を作ってそれをCustomElementRegistryに登録しなければなりません。

もちろん、HTMLElement を継承する class を自分の class で継承することでも可能です。

このパターンのライブライーはこれを利用して、HTMLElementを継承する class を作って、それにコンポーネントを作りやすくするコードを入れます。

例えば、下記にあるSuperAwesomeElementと言う class を使えばHTMLElementを継承しているときよりコンポネントの属性の変更がある場合でのアプデートがしやすくなります。

コードのデモはここにあります

export class SuperAwesomeElement extends HTMLElement {
  constructor() {
    super();
    this.state = {};
  }

  static get attributes() {
    return {};
  }

  static get observedAttributes() {
    return Object.keys(this.attributes);
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue === newValue) {
      return;
    }
    // 属性をタイプごとにパーズしてステートを更新します
    const type = this.attributes[name].type;
    if (/array|object/i.test(type)) {
      this.state[name] = JSON.parse(newValue);
    } else if (/number/i.test(type)) {
      this.state[name] = parseFloat(newValue);
    } else {
      this.state[name] = newValue;
    }
    this.update();
  }
}

この class を使ってコンポーネントを作るとこういう風になります

import { SuperAwesomeElement } from "super-awesome-element";

const template = document.createElement("template");
template.innerHTML = `
  <p>Text: <span class="text"></span></p>
  <p>Number: <span class="int"></span></p>
  <p>Object: <span class="obj"></span></p>
  <p>Array: <span class="arr"></span></p>
`;

export class MyComponent extends SuperAwesomeElement {
  constructor() {
    super();
    this.state = { text: "", int: 0, obj: {}, arr: [] };

    this.attachShadow({ mode: "open" });
    this.shadowRoot.appendChild(template.content.cloneNode(true));

    this._textNode = this.shadowRoot.querySelector(".text");
    this._intNode = this.shadowRoot.querySelector(".int");
    this._objNode = this.shadowRoot.querySelector(".obj");
    this._arrNode = this.shadowRoot.querySelector(".arr");
  }

  static get attributes() {
    return {
      text: { type: "string" },
      int: { type: "number" },
      obj: { type: "object" },
      arr: { type: "array" },
    };
  }

  update() {
    this._textNode.textContent = this.state.text;
    this._intNode.textContent = this.state.int;
    this._objNode.textContent = JSON.stringify(this.state.obj);
    this._arrNode.textContent = JSON.stringify(this.state.arr);
  }
}

customElements.define("my-component", MyComponent);

もちろん、これはものすごく簡単な例で実際にプロジェクトに使えるというレベルではありません。

しかし、こういった簡単なものでもコンポーネントの作成に必要なコードがかなり縮小されます。

ちゃんとしたライブラリーでできることは段違いで多くなります。 ?


良し悪し

Class ベースのライブラリーを使うと作るコンポーネントは標準に近いものになります。

これはそのままいくつかのいいところと悪いところはあります。

いいところ

  • マイグレーションが簡単 - バニラ JS や他の class ベースのライブラリーにマイグレーションすることになったら他のパターンのライブラリーを使っているときよりスムーズにできます。
  • 拡張可能 - ライブラリーにないフィーチャーが複数のコンポーネントに必要になったら mixin class を通してコンポーネントに追加できます。しかも、mixin class の作り方によっては、このパターンのどのライブラリーにも使えることもあります。
  • 標準仕様を学べる - このパターンのライブラリーを使うと標準仕様への理解が深まる。

悪いところ

  • ボイラープレートが若干多い - このパターンのライブラリーを使うとコンポーネントのコードがかなり縮小されますが、JS の class の性質のせいで他のパターンより若干多いです。
    • 属性が変更後に副作用を入れるときなどに見られます。
    • ですが、これでビルドサイズが多くなるとは限りません、あくまで自分の書くコードの話です。

このパターンを使うライブラリー

スターやバージョンやサイズのデータは投稿時の最新です。

CanJS

サイト | Github

スター ライセンス 最新バージョン TS サポート バンドルサイズ テンプレートシステム
1.8k+ MIT 1.1.2 (2020/06) 見つけられなかった 66kB can-stache (mustache みたい)

豆知識

CanJS はこのシリーズで紹介するライブラリーの中ではかなり大きいサイズのものです。

なぜかというと、他のライブラリーと違って、CanJS はコンポーネントを作るためのライブラリーというより Web Components ベースのフレームワークからです。

なので、自分のアプリを全て CanJS で作るならいいですけど、単に色々なプロジェクトで利用できるコンポーネントを作るなら他のライブラリーを使った方がいいです。

HyperHTML Element

Github

スター ライセンス 最新バージョン TS サポート バンドルサイズ テンプレートシステム
0.1k+ ISC 3.12.3 (2020/03) あり 8.7kB hyperHTML (JS テンプレート文字列)

豆知識

このライブラリーは主に hyperHTML でレンダーされた Web Components を作るためのヘルパーです。

ちなみに、hyperHTML はパーフォマンスで言えばトップクラスのレンダリングライブラリーです。⚡️

LitElement

サイト | Github

スター ライセンス 最新バージョン TS サポート バンドルサイズ テンプレートシステム
3.5k+ BSD 3-Clause 2.3.1 (2020/03) あり、decorator 含む 7.1kB lit-html (JS テンプレート文字列)

豆知識

Polymer v3 が存在する中 LitElement も Polymer Project のチームが作っているということは色々とややこしいですけど。

簡単に言うと、LitElement は Polymer v4 です、ですが、作り方もパフォーマンスも劇的に変わったのでライブラリーの名前も帰られました。

なので「Polymer」を使いたいのなら LitElement を使うといいです。?

ちなみに LitElement の最初のリリースは v2.0.0 でした、なぜかというと lit-element の npm package はもともと別の人が持っていましたので、所有権はもらえたものの、v1.0.0 はすでにあったのでそれは使えませんでした。

LitElement の姉妹ライブラリーである lit-html は先ほどメンションした hyperHTML と色々と共通点はあります、トップクラスのパフォーマンスの点も含めてです。 ⚡️

Omi

スター ライセンス 最新バージョン TS サポート バンドルサイズ テンプレートシステム
11.1k+ MIT 6.19.3 (2020/05) あり、decorator 含む decorators 8.3kB JSX (Preact)

豆知識

Omi はこの記事のライブラリーの中で唯一公式ドキュメントが複数言語対応しています。

ドキュメントが全部英語と中国語にあって、一部は韓国語もあります。??????

SkateJS

Website | Github

スター ライセンス 最新バージョン TS サポート バンドルサイズ テンプレートシステム
3.1k+ MIT 0.0.1 (2018/12) 見つけられなかった 1.8kB + レンダーライブラリー hyperHTML/lit-html (JS テンプレート文字列), JSX

豆知識

SkateJS は結構独特なライブラリーでして、公式なテンプレートシステムを持っていません。

それに変わって、JS テンプレート文字列か JSX 系なレンダリングライブラリーと一緒に使うように設計されています。

ただ、コアチームはどうも今は SSR 周りの作業だけをしていてコンポーネントを作成するためのライブラリーは最近アプデートされていない様子です。

SlimJS

Website | Github

スター ライセンス 最新バージョン TS サポート バンドルサイズ テンプレートシステム
0.7k+ MIT 4.0.7 (2019/04) あり、decorator 含む 3.3kB 自分のライブラリー (mustache みたい)

豆知識

名前の通り SlimJS はかなり小さいライブラリーです、この記事の中では一番小さめでシリーズ全体的でもかなり小さめの方です。

一つだけ注意点ですが、プロジェクトはここ一年ぐらい前から更新されていません。☹️


次は?

今回は class ベースパターンについて色々と話しました。

この記事で紹介したライブラリーの中で気に入ったものがあれば幸いです。 ?

まだ見つけていないのならご安心ください、このシリーズで紹介するパターンはまだありますので、次の記事をぜひ読んでみてください。

ご意見やご感想もぜひコメントしてください。

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

[JavaScript]配列を極めたい奴はこれを見ろ

配列とは何か?

複数の値をまるで1つのデータのように取り扱うことができるようになるもの。
配列データを作成することで大量の値を1つのデータのように扱えるわけです。
よく言う例えとしては、データを入れる箱のようなものと覚えておけばいいでしょう。

配列の作成方法

[]のなかにデータを入れていく。

const array = ['リンゴ', 'バナナ', 'イチゴ'];

配列の要素を取得する方法

配列の0番目から数えて取得したい値の番号を指定する。

const array = ['リンゴ', 'バナナ', 'イチゴ'];

console.log(array[0]);  // =>リンゴ

配列の要素を検索する方法

「indexOf()」は、指定した「値」が配列データに存在する場合にその場所を「インデックス番号」で取得できる機能を持っています。なのでこちらのメソッドを使用します。

const array = ['リンゴ', 'バナナ', 'イチゴ'];

const result = items.indexOf('リンゴ');

console.log( result );  // =>0

配列要素の追加

pushメソッドを使用することで配列の最後に要素を追加できる。

const array = ['リンゴ', 'バナナ', 'イチゴ'];

array.push('メロン');

console.log(array);  // =>['リンゴ', 'バナナ', 'イチゴ', 'メロン']

配列の要素を削除する方法

deleteメソッドを使用することで、指定した要素を削除できます。
※削除した部分は空になります。

const array = ['リンゴ', 'バナナ', 'イチゴ'];

delete array[1];

console.log(array);  // =>['リンゴ', '', 'イチゴ']

配列から要素を削除・追加して組み替える

splice()メソッドを使用することで、配列の要素を自由に削除・追加してデータを組み替えることが出来ます。

const array = ['リンゴ', 'バナナ', 'イチゴ'];

// const newArray = array(追加・削除する位置, 削除する要素の数, 追加する要素...);
const newArray = array.splice(1, 2, 'メロン', 'マンゴー');


console.log(newArray);  // =>['リンゴ', 'メロン', 'マンゴー']

配列を連結・結合する方法

join()メソッドを使用することで、配列の各要素を連結・結合して「文字列(String)」として出力することができる。
※引数に何も指定しなければカンマ区切りになります。

const items = [2020, 6, 29];

const result = items.join('/');

console.log( result );  // =>2020/6/29

配列を分割する方法

slice()を使用することで、指定した配列のインデックス番号を境目に末尾まで分割して、新しい配列データを取得することができます。
※第一引数=開始位置、第二引数=終了位置 と言う指定方法も可能。

const array = ['リンゴ', 'バナナ', 'イチゴ'];

const result = array.slice(2);

console.log(newArray);  // =>['イチゴ']

配列要素を抽出する方法

filter()メソッドは、任意の条件に合致する配列要素だけを抽出して新しい配列データを作成することができます。

const items = [55,20,70,80,30,15,65,85,40];

const result = items.filter( function( value ) {

    //50よりも小さい数値だけを抽出
    return value < 50;

})

console.log( result );  // =>[55, 70, 80, 65, 85]

処理した要素を新しい配列で作成する方法

map()メソッドを使用することで、処理した後に新たに配列を作成してくれます。

const items = [1,2,3,4,5];

const result = items.map(function( value ) {

    //配列の各要素を2倍にする
    return value * 2;

});

console.log( result );  // =>[2, 4, 6, 8, 10]

配列要素を繰り返し処理する方法

forEach()メソッドを使用することで、配列データに対してのみ実行することが可能で、各要素1つずつに任意の処理を実行させることができます。

let items = [1,2,3,4,5];

let result = items.map(function( value ) {

    //配列の各要素を2倍にする
    return value * 2;

});

// =>2
// =>4
// =>6
// =>8
// =>10

配列をコピーする方法

concat()メソッドを使用することで、指定した配列を対象の配列に連結出来ます。
空の配列に連結させることでコピーしているような形になります。

const array = ['リンゴ', 'バナナ', 'イチゴ'];

const newArray = array.concat();

console.log(array);     // => ['リンゴ', 'バナナ', 'イチゴ']
console.log(newArray);  // => ['リンゴ', 'バナナ', 'イチゴ']

配列の要素数を調べる方法

lengthメソッドを使用することで配列内の要素数を調べることが出来ます。

const array = ['リンゴ', 'バナナ', 'イチゴ'];

array.length;

配列をソートする方法

sort()メソッドを利用することで、効率よくソート(並び替え)を行うことができます。
※文字列の場合はそのままで良いが、数値やオブジェクトの場合は比較関数を使います。

//文字列の場合

const str = ['sa', 'mu', 'ra', 'i'];
str.sort();

console.log(str);  // => ["i", "mu", "ra", "sa"]

//数値の場合

function compareFunc(a, b) {
  return a - b;
}

var num = [5, 3, 10, 6, 55];
num.sort(compareFunc);

console.log(num);  // => [55, 10, 6, 5, 3]

//オブジェクトの場合
const lists = [
  {name: 'メロン', price: 500},
  {name: 'バナナ', price: 100},
  {name: 'スイカ', price: 280},
  {name: 'イチゴ', price: 300}
];

lists.sort(function(a, b) {
  if (a.name > b.name) {
    return 1;
  } else {
    return -1;
  }
})

console.log(lists);
// =>
[{name: "イチゴ", price: 300}
 {name: "スイカ", price: 280}
 {name: "バナナ", price: 100}
 {name: "メロン", price: 500}]

スプレッド構文

配列やオブジェクトの要素を文字通り展開する構文のこと。

const foo = [1, 2];

// 要素を追加して新しい配列を生成
const baz = [...foo, 3, 4]; // => [1, 2, 3, 4]

レスト構文

複数の要素を集約して 1 つのオブジェクトにまとめることが出来ます。
関数の引数や分割代入での不特定多数の値を配列として受け取る際に利用します。

const arr = [1, 2, 3, 4, 5];
const [a, b, ...c] = arr;

console.log(a, b, c);   // =>  1  2  [3, 4, 5]

まとめ

JavaScriptで使うであろう配列の知識をまとめました。
これらを使いこなせば基礎は身につくんではないかと思います!

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

備忘:JSONPでサーバへアクセス時に、タイムアウト検知する

備忘です

特徴

JSON P のためドメインの違うサイトからデータを取得できます。

  • タイムアウト検知処理
    • 一定時間、サーバから応答が無い場合を検知できる

コード

// JSONPでアクセスする関数
function accessByJSONP(url, errfunc) {
    var key = createUniqueStr();
    console.log(" key created by AccessSuccessCheckList [" + key + "]");

    var script = document.createElement("script");
    script.src = url + "&key=" + key; //一意となるkeyを勝手に追加
    document.head.appendChild(script);

    AccessSuccessCheckList[key] = url;

    script.onerror = function (e) {
        console.log(" Error on accessByJSONP");
        errfunc();
    };
    //呼び出しタイムアウトした場合の処理
    window.setTimeout(function (e) {
        if (accessSuccessCheck(key)) {
            console.log(" access Timeout key is cleared. " + key);
        }
        else {
            console.log(" Err access Timeout key [" + key + "]");
            var url_1 = accessSuccessURL(key);
            console.log(" URL " + url_1);
            //errfunc();
        }
    }, 30000); //タイムアウト待ち30秒

    //↓関数である必要はない。。。
    function accessSuccessURL(key) {
        return AccessSuccessCheckList[key];
    }
    function accessSuccessCheck(key) {
        if (AccessSuccessCheckList[key]) {
            return false;
        }
        else {
            return true;
        }
    }
    function createUniqueStr(myStrong) {
        //let strong = 1000;//copy元
        var strong = 100;
        if (myStrong)
            strong = myStrong;
        return ("k" + new Date().getTime().toString(16) +
            Math.floor(strong * Math.random()).toString(16));
    }
}

グローバル変数で、タイムアウト待ちリストを保持

// JSONPでアクセス中のリスト
// レスポンスタイムアウトをチェックする
var AccessSuccessCheckList = {};

コールバック関数で、タイムアウト待ちを解除する

// keyはcallbackとしてサーバから返してもらうこと
function callback(data, key) {
    AccessSuccessCheckList[key] = null;
}

github

https://github.com/sktn3/accessByJSONP

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

2Dのドット絵作成ツールを作ってみる

2Dドット絵作成ツール開発をしました。

参考にした資料

  • 『いちばんやさしいJavascript 入門教室』 (廣瀬豪)

実施できたこと

  • 任意の色を選択してドット絵を作成できる機能 (16×16)
  • 削除機能
  • ドット絵を画像として保存する機能
  • 実際に手元にあるナノブロックの色に近いカラーパレット作成

コード

<!DOCTYPE html>
<html lang="ja">
 <head>
  <meta charset="utf-8">
  <title>ピクセルアート作成</title>
  <style>
  table { border-collapse: collapse; }
  td {
   width: 30px;
   height: 30px;
   border: solid 1px;
  }
  </style>
          <a href="index.html"><img src="ant-logo4.png" alt="街を作ろう"></a>
 </head>
 <body>
  <div>
   <table id="pixelTBL"></table>
   <br>
   <table id="paletteTBL"><tr></tr></table>
  </div>
  <br>

  <div>
   <input type="image" src="clear.png" alt="削除" onclick="clearCell()">
   <input type="image" src="download.png" alt="画像保存" value="画像保存" onclick="drawCanvas()">
   <canvas id="picture" width="16" height="16" style="border:solid 1px;"></canvas>
  </div>

  <script>
  const Matrix_Rows = 16;
  const Matrix_Cols = 16;
  let Color_Index = "black";

  const pixelTBL = document.getElementById("pixelTBL");
  const paletteTBL = document.getElementById("paletteTBL");

  const cvs = document.getElementById("picture");
  const ctx = cvs.getContext("2d");

  function pixelTable() {
   for(let i=0; i<Matrix_Rows; i++) {
    let row = pixelTBL.insertRow(-1); 
    for(let j=0; j<Matrix_Cols; j++) {
     let cell = row.insertCell(-1); 
     cell.onclick = function(){
      this.style.backgroundColor = Color_Index; 
     }
    }
   }
  }

  let COL_palette = [ "#ffffff", "#000000", "#ff4500", "#1e90ff", "#f5deb3", "#a52a2a", "#fffacd", "#808080", "#800080", "#ff0000", "#32cd32", "#00ff00", "#d2691e", "#5f9ea0", "#008b8b" , "#00bfff", 
  "#ffff00", "#8b4513", "#c71585", "#ffb6c1"];

/*
白 #ffffff
黒 #000000
オレンジレッド   #ff4500
青 #1e90ff
アイボリー #f5deb3
ワイン   #a52a2a
黄色  #fffacd
グレー   #808080
紫 #800080
赤 #ff0000ß
緑 #32cd32
黄緑  #00ff00
チョコレートブラウン  #d2691e
深緑1 #5f9ea0
深緑2 #008b8b
水色  #00bfff
黄色  #ffff00
こげ茶   #8b4513
ショッキングピンク #c71585
薄ピンク    #ffb6c1
*/
  function paletteTable() {
   for (let j=0; j<COL_palette.length; j++) {
    const cell = paletteTBL.rows[0].insertCell(-1); 
    cell.style.backgroundColor = COL_palette[j];
    cell.innerHTML = "<img src='cube1.png'>";
    cell.style.padding = "0px";
    cell.style.border = "0px";
    cell.onclick = function(){
     Color_Index = this.style.backgroundColor; 
    }
   }
  }

  function clearCell() {
   for (let i=0; i<Matrix_Rows; i++) {
    for (let j=0; j<Matrix_Cols; j++) {
     pixelTBL.rows[i].cells[j].style.backgroundColor = "white";
    }
   }
   ctx.fillStyle = "white";
   ctx.fillRect( 0, 0, 16, 16 );
  }

  function drawCanvas() {
   for (let i=0; i<Matrix_Rows; i++) {
    for (let j=0; j<Matrix_Cols; j++) {
     let col = pixelTBL.rows[i].cells[j].style.backgroundColor;
     ctx.fillStyle = col;
     ctx.fillRect( j, i, 1, 1 );
    }
   }
  }

  window.onload = function() { 
   pixelTable();        
   paletteTable();  
   clearCell();     
  }
  </script>
 </body>
</html>

動き

https://3dblock.jp/

image.png

image.png

以下が削除ボタンです。

image.png

以下がダウンロードボタンです。
image.png

クリックすると画像として保存できます。
image.png

実際の画像→ ダウンロード.png

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

私の学習記録

私の学習記録

 4月から6月にかけて、自己学習をする機会がありました。
 私はシステムエンジニアとして、自宅学習ならではの学習テーマを選びました。以下の点を踏まえるように心がけました。

・プログラミング(コーディング)がプロジェクトの全てではない
・プログラミングはスキルの全部ではなく一部である
・業務の幅の拡大あるいは業務効率化のために役立つが、今の自分に抜けがあるものは何か

 実際に学んだのは以下のテーマです。(Github)

プログラミング "周り" のスキルに関する学習

プロジェクトの "流れ" に関する学習

"コンピュータサイエンス" に関する学習

"業務自動化" に関する学習

"Javaが一定以上できるようになったときに合わせて身に付けたい技術" に関する学習

"JavaScriptフレームワーク" に関する学習

"Java関連のスキルアップ" に関する学習

 今後は、資格取得を目指したり、これらの学習内容を線で結んで(勿論、抜けのある、身に付ける必要のある技術はまだまだ盛沢山です)大規模なアプリケーションを自分の手で実装してみる(Githubで星の数を上げられるようにしたい)といったことをしようと考えています。さらには、ITの専門職大学院で専門性をさらに極めていくということも検討しています。

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

「ElectronJSベースのプロジェクト」ElectronJSの機能に沿ったユーザーインターフェース(UI)の作成

高性能なキーバリューデータベースであるRedisは、ビッグデータアプリケーションに欠かせない要素となっています。

本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。

電子テンプレートの作成

まずはElectronテンプレートの基本的なバージョンから始めてみましょう。これは、Official Electron RepositoryからフォークされたテンプレートのGitHubリンクです。試してみたい方は、以下のリンクにアクセスしてコードをクローンしてみてください: https://github.com/saichandu415/electron-quick-start

以下は、コードをクローンした後に表示されるファイル構造です。

image.png

各ファイルが何を担当しているのかを簡単に説明します。

.gitignore
多くの人がこのファイルを当たり前のように受け取っていますが、このファイルの本当の目的は、コードをコミット / git リポジトリにプッシュしている間、ファイルを無視することです。これにより、不要なファイルがリポジトリにプッシュされないようにします。

LICENSE.md
これは、オープンソースとしてリリースするコードのライセンス構造の概要を示します。すべてのライセンスをチェックして、自分に合ったものを選ぶようにしましょう。

README.md
これはリポジトリを開いたときに表示されるフロントページです。マークダウンはドキュメントを書くための特別なフォーマットで、内容を読みやすくするための十分な書式オプションがあり、非常に簡単です。

Index.html
プレゼンテーションのロジックを保存するファイルです。HTML と CSS を使用したすべての UI デザインはここにあります。あなたが従うUIアーキテクチャに基づいて、異なるファイルやアプリケーション全体のための1つのHTMLページを管理することが起こるかもしれません。また、タグが動作するようにするためには、いくつかの変更が必要です。

main.js
これはElectronアプリケーションの心臓部であり、IPCMainプロセスからIPCRendererプロセスへの完全な通信で構成されています。このファイルがどのように動作するかについては、コードを説明するときに詳しく説明します。このファイルはすべてのプロセス間通信を担当しています。

Package.json
このファイルは、Node.jsの経験がある人には馴染みのあるファイルでしょう。これと同等のものは、pom.xmlのようなjavaでも見つけることができます。

Renderer.js
これはプロセス間通信のレンダラー部分です。複数のrenderer.jsを持つことができますが、イベントの連鎖を維持したり追跡したりするのが非常に難しいので、複数のmain.jsを持つことはお勧めできません。

ElectronJSを使用したデスクトップ請求書発行アプリケーション

ElectronJSをベースに請求書発行アプリケーションを作成しています。アプリケーションのUIを作成することから始めます。

UIとUX
私はBoomerang UI Kit (https://github.com/webpixels/boomerang-ui-kit)を選びました。これは、レスポンシブで美しいユーザーインターフェースを簡単に開発する方法を提供してくれるbootstrapフレームワークの上に作成されています。このキットには、あらかじめ定義されたコードスニペットがたくさん用意されているので、簡単に始めることができます。

今日ご紹介するコードスニペットは、以下のレポから取得したものです。

https://github.com/saichandu415/Electron-Invoice-ApsaraDB-MongoDB

コードをクローンすると、"Electron_Invoice "と "Invoice_Backend "があるはずです。Electron_Invoiceは、この記事で説明しているフロントエンドのコード全体で構成されています。

<link href="https://fonts.googleapis.com/css?family=Nunito:400,600,700,800|Roboto:400,500,700" rel="stylesheet">
<!-- Theme CSS -->
<link type="text/css" href="./assets/css/theme.css" rel="stylesheet">
<!-- Font Awesome -->
<link href='http://fonts.googleapis.com/css?family=Grand+Hotel' rel='stylesheet' type='text/css'>

基本的なBootstrapフレームワークの要素をスタイリングするために必要なCSSの構成要素をすべて含んだ「theme.css」をリンクしています。CSS のリンクは、Web 開発で行うように、簡単にできます。
bodyタグの最後に移動すると、いくつかのスクリプトがあります。そのうちの一つが

<script>
// You can also require other files to run in this process
require('./renderer.js');
</script>

ここではrenderer.jsを注入しています。renderer.jsでは、完全なDOMとNPM(Node Package Manager)モジュールにアクセスできます。

<script>
if (typeof module === 'object') {
window.module = module;
module = undefined;
}
</script>
<!-- Start Script dependencies -->
<!-- Core -->
<script src="./assets/vendor/jquery/jquery.min.js"></script>
<script src="./assets/vendor/popper/popper.min.js"></script>
<script src="./assets/js/bootstrap/bootstrap.min.js"></script>
<!-- FontAwesome 5 -->
<script src="./assets/vendor/fontawesome/js/fontawesome-all.min.js" defer></script>
<!-- Page plugins -->
<script src="./assets/vendor/bootstrap-select/js/bootstrap-select.min.js"></script>
<script src="./assets/vendor/bootstrap-tagsinput/bootstrap-tagsinput.min.js"></script>
<script src="./assets/vendor/input-mask/input-mask.min.js"></script>
<script src="./assets/vendor/nouislider/js/nouislider.min.js"></script>
<script src="./assets/vendor/textarea-autosize/textarea-autosize.min.js"></script>
<!-- Theme JS -->
<script src="./assets/js/theme.js"></script>
<!-- Chart JS Plugin -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.bundle.min.js"></script>
<!-- Mustache JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.3.0/mustache.js"></script>
<!-- Ending Script dependencies -->
<script>
if (window.module) module = window.module;
</script>

これは、依存関係が適切に統合されるのを妨げるので、理解しておく必要がある最も重要なコードです。.jsの依存関係を追加し始める前に、「スクリプトの依存関係を開始する」というコメントの上にスクリプトを追加する必要があることに注意してください。そうしないと、JQueryなどのモジュールや他のライブラリが利用できなくなります。

Electronではすべてのライブラリがモジュールとして利用できるようになっているはずです。Starting & Ending script dependencies」のスクリプトの間にインポートを挟むことで 全てのライブラリが正常にインポートされ、利用可能になります。

使用しているインポートは以下の通りです。

Jquery, Popper, Bootstrap - Boomerang UIの相互依存性のためのもので、UIキットはその上に構築されています。

Font Awesome - フォントとして統合された完全にスケーラブルなアイコン。

Page plugins - HTMLの一部として使用している既存の要素に追加機能を作成するために使用します。

Theme - このJavaScriptコードは、アニメーション、補間などを担当するtheme.cssにバインドされています。

ChartJS - 分析結果をグラフィカルに表示するためにChartJSを使用しています。このライブラリはオープンソースであり、カスタマイズが容易な多くのチャートを提供しています。

MustacheJS - 実行時に簡単に動的にhtmlを作成するためにMustacheを使用しています。このライブラリは動的なhtmlを生成し、リストを使って値を置き換える作業を大幅に簡略化してくれます。

他のコードスニペットについて説明する前に、最終製品がどのようになるか見てみましょう。

image.png

image.png

image.png

販売の見える化

現在デザインしている2つの画面です。

  • New Invoice これは、ユーザーが新しい請求書を作成するためのフォームを持っているウィンドウです。
  • Dashboard ダッシュボードは、ユーザーの売上分析、総注文数、総売上高をグラフィカルな方法で見ることができます。

image.png

上記のUI部分は、以下のコードで実現しています。

<div class='navigationBar'>
        <ul class="nav nav-pills nav-fill flex-column flex-sm-row" id="myTab" role="tablist">
          <li class="nav-item">
            <a class="nav-link mb-sm-3 active" id="home-tab" data-toggle="tab" href="#newInvoice" role="tab" aria-controls="home" aria-selected="true">New Invoice</a>
          </li>
          <li class="nav-item">
            <a class="nav-link mb-sm-3" id="profile-tab" data-toggle="tab" href="#dashboard" role="tab" aria-controls="profile" aria-selected="false">DashBoard</a>
          </li>
        </ul>
        <div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="newInvoice" role="tabpanel" aria-labelledby="home-tab">
** Content of Home **
</div>
          <div class="tab-pane fade" id="dashboard" role="tabpanel" aria-labelledby="profile-tab">
** Content of DashBoard **
</div>

我々は、単一のページのUIアーキテクチャに従っており、1つのhtmlファイルで一度に全体のページを作成し、我々はそれが必要であることを示しています。

myTab "には新規請求書とダッシュボードのタブの完全なリストが表示されています。HTMLタグに適用されているクラスは無視しても良いかもしれません。

他の

の "tab-pane "は "New Invoice "と "Dashboard "に関連した内容を保持しています。
<div class="input-group-prepend">
   <span class="input-group-text">
       <i class="fas fa-user"></i>
   </span>
</div>
<input type="text" class="form-control" id="customerName" placeholder="Customer Name">

image.png

これはフォームで使用したコンポーネントの一つで、アイコンにはタグが作成され、先ほど追加したフォント awesome で利用可能になります。

インプット

image.png

<table class="table">
              <thead class="thead-dark">
                <tr>
                  <th scope="col" class='centerme'>Item Details</th>
                  <th scope="col" class='centerme'>Qty</th>
                  <th scope="col" class='centerme'>Rate</th>
                  <th scope="col" class='centerme'>Amount</th>
                  <th scope="col"></th>

                </tr>
              </thead>
              <tbody>
                <tr>
                  <td>
                    <div class="form-group">
                      <input type="text" id="itemDetails" class="form-control border-0" placeholder="Item Details">
                    </div>
                  </td>
                  <td>
                    <div class="form-group">
                      <input type="number" id="quantity" class="form-control border-0" placeholder="Quantity">
                    </div>
                  </td>
                  <td>
                    <div class="form-group">
                      <input type="number" id="rate" class="form-control border-0" placeholder="Rate">
                    </div>
                  </td>
                  <td>
                    <div class="form-group">
                      <input type="number" id="amount" class="form-control border-0" placeholder="Amount" readonly>
                    </div>
                  </td>
                  <td class=''>
                    <button type="button" onclick="addItemCard()" class="btn btn-success btn-icon-only rounded-circle">
                      <span class="btn-inner--icon">
                        <i class="fas fa-check"></i>
                      </span>
                    </button>
                  </td>
                </tr>
              </tbody>
            </table>

この特定のコードは、テーブルヘッダーとそれに対するボタンと一緒に入力のセットを作成します。

緑色のボタンは、項目の詳細を提供するエントリを作成し、次のエントリを見ることができます。

image.png

アイテム詳細のカード入力は、以下のコードで作成されます。

<!-- Template here Starts -->
<script id="tutorial-template" type="text/template">
  <tr class="bg-white">
    <th scope="row">
      <div class="media align-items-center">
        <span class="avatar avatar-lg bg-pink mr-3">{{itemShortName}}</span>
        <div class="media-body">
          <h6 class="h5 font-weight-normal mb-0">{{ItemName}}</h6>
          <span class="font-weight-normal text-muted">Quantity :
            <b>{{qty}}</b>
          </span>
        </div>
      </div>
    </th>
    <td>Rate/pc : {{rate}}</td>
    <td>Amount : {{amount}}</td>
  </tr>
  <tr class="table-divider"></tr>
</script>
<!-- Template here ends -->

このテンプレートは動的に生成され、以下の関数を使用して実行時に以下の表に追加されます。

function addItemCard() {
    var data = {};

    data.itemShortName = ($("#itemDetails").val()).substring(0, 2).toUpperCase();
    data.ItemName = $("#itemDetails").val();
    data.qty = $("#quantity").val();
    data.rate = $("#rate").val();
    data.amount = $("#amount").val();

    if (!itemsObj.customerName) {
      itemsObj.customerName = $("#customerName").val();
    }

    if (!itemsObj.invoiceNumber) {
      itemsObj.invoiceNumber = $("#invoiceNumber").val();
    }
    if (!itemsObj.invoiceDate) {
      itemsObj.invoiceDate = $("#invoiceDate").val();
    }
    if (!itemsObj.dueDate) {
      itemsObj.dueDate = $("#dueDate").val();
    }
    itemsObj.itemsData.push(data);
    var template = $("#tutorial-template").html();
    var html = Mustache.render(template, data);
    $("#cardsList").append(html);

  }

クリックすると、"addItemCard() "が呼び出され、データがItemsObjにプッシュされます(後にサーバーにデータをプッシュするために使用されます)。また、Mustacheライブラリを使用してカードテンプレートを作成し、テーブルに追加します。

<table class="table table-hover table-cards align-items-center">
              <tbody id="cardsList">
                <!-- Dynamically generated template goes here -->
              </tbody>
            </table>

Reset
「リセット」ボタンを押すと、フォーム内のデータがリセットされ、新しい請求書フォームのようになります。

Submit
これは、データベースへのデータの挿入の世話をするAPIコールを行うもう一つの機能です。

function submitData() {
    console.log('submit Clicked');
    var itemsArr = itemsObj.itemsData;
    var totalAmount = 0;
    console.log(itemsArr);

    for (var i = 0; i < itemsArr.length; i++) {
      totalAmount = totalAmount + parseFloat(itemsArr[i].amount);
    }
    console.log(totalAmount);
    itemsObj.totalAmount = totalAmount;
    console.log(JSON.stringify(itemsObj));

    $.ajax({
      type: 'POST',
      data: JSON.stringify(itemsObj),
      contentType: 'application/json',
      url: 'http://149.129.130.26:443/data/invoice',
      success: function (data) {
        console.log('success');
        console.log(JSON.stringify(data));
        $("#modal_5").modal("show");
      }
    });
  }

これは、ECSインスタンスで動作している上記URIへのPOSTコールで、MongoDBインスタンスへのデータの挿入を担当しています。バックエンドについては近日中に詳しく説明します。

ダッシュボード
ダッシュボードは以下のコンポーネントで構成されています。

image.png

上記のコンポーネントは、以下のコードで作成しています。

<div class="card">
                  <div class="card-header">
                    <div class="row align-items-center">
                      <div class="col-8">
                        <h4 class="heading h5 mb-0">Today Orders</h4>
                      </div>
                      <div class="col-4">
                        <div class="card-icon-actions text-right">
                          <a href="#" class="favorite" data-toggle="tooltip" data-original-title="Save as favorite">
                            <i class="fas fa-star"></i>
                          </a>
                          <a href="#" class="love active" data-toggle="tooltip" data-original-title="Love this">
                            <i class="fas fa-heart"></i>
                          </a>
                        </div>
                      </div>
                    </div>
                  </div>
                  <div class="card-body">
                    <p class="card-text" id="saleCount">Count : 0</p>
                  </div>
                  <div class="card-footer">
                    <div class="row align-items-center">
                      <div class="col-6">
                        <a href="#" class="btn btn-sm btn-primary" onclick="fetchAndLoadValues()">Refresh Now</a>
                      </div>
                      <div class="col-6 text-right">
                        <span class="text-muted" id="saleCountUpdateValue">2 hrs ago</span>
                      </div>
                    </div>
                  </div>

上記のコードでは、MongoDBから最近更新された時間とともに、アイコン、ヘッダー、ボタンを持つカードを作成しています。

また、売上をそれぞれの日付でグラフィカルに表示するために chart.js を使用しています。スクリプトセクションの

タグを参考にしてください。非常にわかりやすいはずです。
データを取得して分析するために使用した機能については、コード内で

  function fetchAndLoadValues() {
    var ajaxResult = {};
    $.ajax({
      url: "http://149.129.130.26:443/data/dashboard", success: function (result) {
        console.log(result);
        ajaxResult = result;
        dataVariable.data.labels = ajaxResult.datesArr;
        dataVariable.data.datasets[0].data = ajaxResult.salesArr;
        $('#saleValueUpdateTime').text(timeNow());
        $('#saleCountUpdateValue').text(timeNow());
        $('#saleValue').text("sale Value : $ " + (ajaxResult.salesArr)[0]);
        $('#saleCount').text("Count : " + (ajaxResult.ordersArr)[0]);
        loadLineChart();
      }
    });

fetchAndLoadValues()関数は、ECSインスタンス上のAPIにAJAX(Asyncronous JavascriptとXML)呼び出しを行い、値を取得し、以下の操作を行います。

  • セールバリューリフレッシュタイムの値を更新
  • 総受注件数更新時間の値を更新
  • 現在の総売却額
  • その日の全注文の合計注文数
  • グラフ用の総売却額と過去5日間の日付の配列の作成

次は何をするのか?

今回は、このアプリケーションで使用するバックエンドサービスをどのように作成したのか、また、アプリケーションでどのような操作ができるのかを説明します。もしまだElectronの利点について読んでいないのであれば、このチュートリアルシリーズの前回の記事をぜひチェックしてみてください。

アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ

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

Power Apps component framework で使用できる CDS データ型と、対応する JavaScript/TypeScript の型

目的

CDS の TwoOptions などのフィールドタイプに対応する JS/TS の型をまとめたかった。

Power App component framework で使用できる CDS データ型

https://docs.microsoft.com/ja-jp/powerapps/developer/component-framework/manifest-schema-reference/property

マニフェスト定義と生成される JS/TS データ型

前提

$ npm list --depth=0
pcf-project@1.0.0 C:\Users\...
+-- @types/node@10.17.6
+-- @types/powerapps-component-framework@1.2.1
+-- pcf-scripts@1.0.6
`-- pcf-start@1.0.6

挙動

ControlManifest.Input.xml
...
    <property name="newAttr1"  display-name-key="newAttr1_Display_Key"  description-key="newAttr1_Desc_Key"  of-type="Currency"                 usage="bound" required="true" />
    <property name="newAttr2"  display-name-key="newAttr2_Display_Key"  description-key="newAttr2_Desc_Key"  of-type="DateAndTime.DateAndTime"  usage="bound" required="true" />
    <property name="newAttr3"  display-name-key="newAttr3_Display_Key"  description-key="newAttr3_Desc_Key"  of-type="DateAndTime.DateOnly"     usage="bound" required="true" />
    <property name="newAttr4"  display-name-key="newAttr4_Display_Key"  description-key="newAttr4_Desc_Key"  of-type="Decimal"                  usage="bound" required="true" />
    <property name="newAttr5"  display-name-key="newAttr5_Display_Key"  description-key="newAttr5_Desc_Key"  of-type="Enum"                     usage="bound" required="true" />
    <property name="newAttr6"  display-name-key="newAttr6_Display_Key"  description-key="newAttr6_Desc_Key"  of-type="FP"                       usage="bound" required="true" />
    <property name="newAttr7"  display-name-key="newAttr7_Display_Key"  description-key="newAttr7_Desc_Key"  of-type="Multiple"                 usage="bound" required="true" />
    <property name="newAttr8"  display-name-key="newAttr8_Display_Key"  description-key="newAttr8_Desc_Key"  of-type="OptionSet"                usage="bound" required="true" />
    <property name="newAttr9"  display-name-key="newAttr9_Display_Key"  description-key="newAttr9_Desc_Key"  of-type="SingleLine.Email"         usage="bound" required="true" />
    <property name="newAttr10" display-name-key="newAttr10_Display_Key" description-key="newAttr10_Desc_Key" of-type="SingleLine.Phone"         usage="bound" required="true" />
    <property name="newAttr11" display-name-key="newAttr11_Display_Key" description-key="newAttr11_Desc_Key" of-type="SingleLine.Text"          usage="bound" required="true" />
    <property name="newAttr12" display-name-key="newAttr12_Display_Key" description-key="newAttr12_Desc_Key" of-type="SingleLine.TextArea"      usage="bound" required="true" />
    <property name="newAttr13" display-name-key="newAttr13_Display_Key" description-key="newAttr13_Desc_Key" of-type="SingleLine.Ticker"        usage="bound" required="true" />
    <property name="newAttr14" display-name-key="newAttr14_Display_Key" description-key="newAttr14_Desc_Key" of-type="SingleLine.URL"           usage="bound" required="true" />
    <property name="newAttr15" display-name-key="newAttr15_Display_Key" description-key="newAttr15_Desc_Key" of-type="TwoOptions"               usage="bound" required="true" />
    <property name="newAttr16" display-name-key="newAttr16_Display_Key" description-key="newAttr16_Desc_Key" of-type="Whole.None"               usage="bound" required="true" />
...
ManifestTypes.d.ts
export interface IOutputs {
    newAttr1?: number;
    newAttr2?: Date;
    newAttr3?: Date;
    newAttr4?: number;
    newAttr5?: string;
    newAttr6?: number;
    newAttr7?: string;
    newAttr8?: number;
    newAttr9?: string;
    newAttr10?: string;
    newAttr11?: string;
    newAttr12?: string;
    newAttr13?: string;
    newAttr14?: string;
    newAttr15?: boolean;
    newAttr16?: number;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ElectronJSの入門編

ElectronJSは、クロスプラットフォームのアプリケーション開発を支援します。アプリ開発に特別なスキルは必要ありません。

本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。

ElectronJSの話をする前に、Desktopアプリケーションがどのように作られているのかを見てみましょう。また、現在のDesktopアプリケーションの開発方法の問題点を分析します。その上で、ElectronJSを使ってフル機能の請求書アプリケーションのバックエンドを作る方法を探っていきます。

デスクトップでのアプリ開発

Windows
ウィンドウズベースのアプリはどのように作られているのか、という話をしてみましょう。ほとんどの人はC++、C# (C Sharp)、VB (Visual Basic)を使って開発しています。これには、Windowsストア、Windows電話、WindowsデスクトップアプリケーションのようなすべてのWindowsエコシステム全体でアプリケーションを作成するために、いくつかのオープンソースの.NETライブラリを使用/使用することが含まれている場合があります。

MacOS
Macでは、アプリケーションを作成するために使用する技術を明確に抽象化しています。「Cocoa」レイヤーには、アプリケーションのユーザーインターフェイスを作成するために必要なすべての技術が含まれています。「メディア」レイヤーには、2Dおよび3Dアニメーション、写真やビデオの編集を含むメディア操作に必要なすべてのツールと技術が含まれています。

また、"Core Services "エリアでは、低レベルのネットワーク操作、文字列やデータの操作を行うことができます。"Core OS "は、すべてのCPUやGPUが高性能なタスクを実行するためのすべてのAPIを公開/提供しています。"Kernel & Device Drivers "は、ファイルシステム、ネットワーク、セキュリティ、プロセス間通信、プログラミング言語、デバイスドライバ、カーネルの拡張機能を提供します。このレベルでは、Machカーネル環境、デバイスドライバ、BSDライブラリ関数(libSystem)、その他の低レベルコンポーネントも公開しています。

ご覧のように、MacOS上でアプリを開発するためには、技術やフレームワークについて学ぶ必要があることがたくさんあります。私たちが使用しているWindowsの技術スタックとは類似性がないことに注意してください。

Linux/Ubuntu
Linuxは開発者にとって最もポピュラーなOSの一つなので、自社の製品がLinuxユーザーに届くようにしたいと考えています。今回はUbuntuとLinuxの両方について一緒にお話しします。Linuxは一般的にアプリケーションのユーザーインターフェースにPythonなどのライブラリを使用します。Linuxの場合、3Dや2Dレンダリングのようなサービスが必要な場合は、OpenGLが必要になります。Linuxカーネルは、低レベルの機能を公開しています。

Web技術とは何か?

Web開発は今最もホットな話題の一つであり、Web開発者の人口がかなり多いのは当然のことです。また、素晴らしいUI/UXを持つアプリをデザインし、作成することができるウェブ開発者もたくさんいます。これらの技術は標準的なものであり、世界のトップ企業が制作プロジェクトで使用しているため、見つけるのは簡単です。業界の大手技術者に支えられ、他のチームを支援するフレームワークは無数に存在します。

テンポの速い世界では、企業はテック業界の急速で絶え間ない変化に対応する必要があります。つまり、製品をより早くプロトタイプ化し、すべてのプラットフォームで利用できるようにし、単一のコードベース、より多くの機能のサポート、優れたUI/UXを提供する必要があるということです。従来の方法だけに頼っていると、これを実現するためには多くの資本と時間が必要になります。その代わり、Web技術を利用すれば、多くのオープンソースフレームワークを再利用し、効率的に課題を解決することができます。

ElectronJSとは何か?

ElectronJSは、既存のWeb技術を利用したクロスプラットフォームアプリケーションの開発を支援します。ほとんどの場合、特別なスキルは必要ありません。Web開発者の方で、Electronを使ったアプリ開発に興味がある方は、このシリーズの以下のチュートリアルをチェックしてみてください。

ここまで説明したすべてのプラットフォームでアプリケーションを利用できるようにするには、異なる技術を使ってアプリをデプロイする必要があります。これは非常にマニュアル化されており、時間がかかります。さて、ElectronJSの話をすれば、これがJavaScriptベースのフレームワークであることは明らかです。すべてのプラットフォームがWeb技術をサポートしているため、ElectronJSはクロスプラットフォームアプリの開発を容易にします。ElectronJSを使用している一般的なアプリケーションには、Visual Studio Code、Slack、Atom Editorなどがあります。

ElectronJSの機能

セキュリティ
作成するアプリケーションはデスクトップアプリケーションであり、データはシステムのローカルに留まるため、既存のアプリケーションをElectronJSに移行する際にはあまり考える必要はありません。そのため、データのセキュリティを確保することができます。クラウドにデータを保存する必要がある場合は、事前にクラウドネットワークに十分なセキュリティ機能があるかどうかを確認して、不意を突かれないようにしましょう。

低レベルのアクセシビリティ
また、始める前に、デスクトップアプリケーションに提供している機能がすべてElectronJSでも利用できるかどうかを確認する必要があります。私の経験では、ElectronJSはキーボードショートカットなどの拡張インタラクティブ機能をアプリケーションに搭載するのに十分なコントロールを提供しています。また、ハードウェアやオペレーションシステムのコンポーネントへの低レベルのアクセシビリティも提供しています。

ハードウェアのアクセシビリティ
開発者は、JavaScript/Pluginを介して公開されているすべてのハードウェアレベルのアクセスAPIに完全にアクセスすることができます。Electron JSに移行したい場合は、この機能に妥協する必要はありません。

パフォーマンス
ElectronJSはこの点で成功しています。開発中に適切な注意を払っていれば(必要なものだけをロードする)、ネイティブアプリケーションと比較して、ElectronJSはパフォーマンスの面で大きな向上を示すことができます。ElectronJSはすべての主要なプラットフォームに対応した単一のコードベースを持つことで、多くの時間を節約し、遊びや開発のオプションを提供します。これらはアプリケーションのネイティブ開発を扱う際の大きな問題ですが、これらはすべてElectronJSによって効果的に解決されています。

しかし、多くの人がElectronJSは非常にメモリを消費すると言います。私もこの意見には賛成ですが、適切な注意を払わずにアプリケーションを開発する場合に限ります。

コードとアプリの管理
プロダクトオーナーとして、プラットフォームごとに異なるチームを維持する必要はなく、異なるチームに要件を再度説明する手間から解放されます。また、製品がプラットフォーム間で同じ機能を持っていることを確認するための監査作業も軽減されます。

開発者として、異なるコードベースについて心配する必要はありません。どのプラットフォームでもバグが発生した場合は、コードベースで修正することができます。バグが他のプラットフォームに現れることはありません。しかし、それでもOSレベルの機能には目を光らせておく必要があります。

再利用性
単一のコードベースを使用しているので、ウェブアプリケーションとデスクトップアプリケーションの両方に使用できることを意味します。ある意味では、「一度コードを書き、どこにでも配布する」ので、異なるプラットフォーム間でもベースコードを再利用することができます。

本番環境
有名なフレームワークを使えば使うほど、より多くのサポートを得ることができます。これにより、再利用できるオープンソースのライブラリが増え、生産までの時間が短縮されますが、より多くの機能を備えています。

デプロイ/ビルド
これはElectronJSの興味深い側面の一つです。Electron-packagerモジュールというものがあり、これはコードベース全体をそれぞれのパッケージにバンドルするのに役立ちます。Electronはメモリを大量に消費するという議論がありますが、これを避けるためには開発中に少し注意が必要です。

UI / UX
ウェブ技術を使えば、すべてのユーザーに快適なユーザーインターフェース(UI)とユーザーエクスペリエンス(UX)を提供する複数の技術を利用することができます。また、異なるプラットフォーム上のすべてのユーザーに同じ体験を提供していることを確認することができます。

コストと時間
私たちが使用している技術スタックでは、より少ないコストで良い結果を出すことができる開発者がたくさんいるので、開発時間とコストを大幅に節約することができます。単一のコードベースを使用することで多くの時間を節約することができ、どんな開発者でも何でも取り組むことができます。

エレクトロンの歴史

どんな技術の話をするときも、その背景にある歴史について少し話をする必要があります。ここでは、Electronの歴史を簡単に見てみましょう。

Electronは2013年7月15日にCheng Zhao氏によって設立されたクロスプラットフォームのテキストエディタAtomの開発中に設立されました。C++、JavaScript、Objective C、Pythonをコアにしたオープンソースで、GitHubで開発・サポートされています。クロスプラットフォームでのAtom作成を容易にすることを目的としています。

Electron JSへのディープダイブ

典型的なElectronJSアプリケーションは、Chromiumエンジンの上にHTML、CSS、JavaScript、Node JSを使用して開発します。Chromiumという言葉に怯えてはいけません。ブラウザやクライアントサイドのアプリケーションを開発しているウェブ開発者であれば、Chromiumについて学ぶ必要はありません。多くのネイティブ機能にアクセスしているのであれば、Chromiumエンジン(Chromium)について詳しく知る必要があります。

また、Next.js、Vue.js、Angular 5など、基本的な設定の上に他のフレームワークを使うこともできます。こうすることで、アプリケーションをより事前に定義してモジュール化しておくことで、デバッグや理解が容易になり、開発が楽になります。また、ユニットテストケースを作成して、変更が既存の機能に影響を与えていないことを確認することもできます。

エレクトロンのハイレベルなアーキテクチャとユースケース

電子がコアレベルでどのように働くかを示す簡単なユースケース図を作成しました。

image.png

それでは、ElectronJSの動作を見てみましょう。

1、クライアントは、通常、Windows/MacOS/LinuxベースのUbuntuなどのプラットフォームからElectronJSアプリケーションを起動します。

2、アプリケーションは、メインプロセスを使用してウィンドウにリクエストを行います。

a、メインプロセス。これは、リクエストの送受信、異なるウィンドウ間のデータ転送を担当しています。ウィンドウの作成と破壊のようなすべてのメモリ管理はここで処理されます。

3、メインプロセスはrenderer.js(Rendererプロセス)と一緒にウィンドウの起動要求を行います。

a、Rendererプロセス。ウィンドウ内の全てのhtmlで参照する追加のjsファイルです。Renderer Processで完全なDOMにアクセスすることができます。HTMLの中で使用することもできますが、Renderer Processを使用した方がよりクリーンな方法です。アプリケーションが成長するにつれて、このアプローチを評価するようになるでしょう。

4、サインインウィンドウが起動され、メインプロセスにアタッチされ、操作が可能になります。

5、ユーザーがログイン情報を入力した後、レンダラプロセスでボタンクリックを処理し、製品ウィンドウを起動するためのメインプロセスへの要求を行います。

6、メインプロセスは、イベントリスナーを介してそれぞれの情報を受信し、レンダラプロセスを使用して要求に基づいて製品ウィンドウを起動します。

7、このループは、完全なユースケースが解決されるまで、アプリケーション全体のために続けられます。

もしまだわからない場合は、心配する必要はありません。以下のチュートリアルでは、ElectronJSを使用して作成したライブアプリケーションで、バックエンドデータベースをAlibaba Cloud ApsaraDB for MongoDBとした場合のコンセプトを説明します。

バックエンドとしてのMongo DBのためのアリババクラウドApsaraDB

デスクトップアプリケーションは、システムクラッシュや自然災害の際にデータを失う可能性があります。貴重なデータを保存するために、クラウド上に配置されたバックエンドとプロジェクトを接続します。保存されたデータに対してデータ分析を行う予定です。

MongoDBにAlibaba Cloud ApsaraDBを選んだ理由は、シンプルで低コスト、高可用性にあります。この人気のサービスにセキュアに接続して、どのように活用していくのか、私の体験談を交えてデモを行います。

次のチュートリアルは?

次回のチュートリアルでは、ElectronJSに初めて触れる方のために、多くの概念を明確にするシンプルな2部構成のステップバイステップのチュートリアルを予定しています。

ipcRenderer、ipcMain、印刷機能などの様々なコンポーネントを使って、どのようにしてElectronJSアプリをゼロから作成するのかを詳しく見ていきます。また、生産や配布を目的としたElectronアプリのパッケージ化やビルド方法についても探ります。チュートリアルの最後には、インタラクティブなチャートを作成したり、オープンソースのライブラリを使用して、素晴らしいUIをすぐに作成できるようになるはずです。

新しい技術を使うときはいつも、いくつかの障害に遭遇する可能性が高いです。だからこそ、私が開発中に直面したいくつかの落とし穴について、多くの時間を節約できるヒントやコツをお伝えします。

次に、ExpressJSノードモジュールを使用してNodeJSベースのバックエンドサービスを作成し、ECSインスタンスでホストします。これをElectron Appがアクセスできるように公開します。

アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ

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

Nuxt.js のライフサイクルを踏まえてトラッキングタグ埋め込みを実装する

Web サイトのエンハンスやマーケティングといった目的でユーザー分析や広告表示用のトラッキングタグを埋め込むことは開発の現場でよくあると思います。
Google Tag Manager などのツールを利用すれば比較的簡単に設定できますが、ユーザー ID など Web アプリケーション固有のデータをタグと併せて設定したい場合など、アプリケーション側にタグ埋め込み用のコードを書かなければならない場面もあります。

Nuxt.js のユニバーサルモードにおけるライフサイクル処理には幾つか種類があり、実行タイミングおよび SSR/CSR の実行有無がそれぞれ異なります。

これらを踏まえ、タグやカスタム変数を設定する際にどのライフサイクル処理を選択すれば良いのか整理します。

TL;DR

  • クライアントサイドで一度だけ実行したい処理(タグ埋め込みなど)は plugins を利用する。
  • ページ遷移ごとに実行したい処理(ページ遷移時の状態を表す変数の設定など)は Vue Router を利用する。

シチュエーション

以下のようなタグやカスタム変数に関する処理をまとめた composition を適宜 import して利用するようなシチュエーションを想定します。
コードは TypeScript で記述していますが、本記事の要点に関しては JavaScript の場合と特に大きな相違はありません。

compositions/sample-tag.ts
export function useSampleTag() {
  return {
    addTag() {
      // タグの埋め込み
    },
    addCustomValue() {
      // カスタム変数の設定
    }
  }
}

タグの埋め込み

タグの埋め込みは直アクセスされたタイミングで行い、 nuxt-link での遷移時には実行したくないケースが多いと思います。
このような用途では直アクセス時のみ実行される plugins が利用できます。

plugins/sample-tag.ts
import { useSampleTag } from '~/compositions/sample-tag'

export default () => {
  const st = useSampleTag()
  st.addTag()
}

カスタム変数の設定

ページ遷移ごとにログイン状態やユーザー ID などの値を設定したい場合は Vue Router の afterEach() が利用できます。

plugins/router.ts
import { useSampleTag } from '~/compositions/sample-tag'

export default () => {
  ctx.app.router!.afterEach((to, from) => {
    if (process.client) {
      const st = useSampleTag()
      st.addCustomValue()
    }
  })
}

middleware を利用する場合

middleware を用いても同様のコードで似たような処理は実現できます。
ただしユニバーサルモードでは直アクセス時に SSR でしか処理されないため、 window オブジェクトの利用といった CSR が前提となる処理が失敗する可能性があります。
特別な理由がない限りは Vue Router を利用するのが無難だと思います。

Vuex の Store を利用する場合

Vuex の Store から getter で値を取得したい場合、 addCustomValue() の引数に Store を取るようにして内部で getter を呼び出す作りにしても良いかもしれません。

compositions/sample-tag.ts
import { Store } from 'vuex/types'
import { AuthenticationState } from '~/store/authentication'

export function useSampleTag() {
  return {
    addTag() {
      // タグの埋め込み
    },
    addCustomValue(store: Store<AuthenticationState>) {
      // カスタム変数の設定
    }
  }
}
plugins/router.ts
import { Context } from '@nuxt/types'
import { useSampleTag } from '~/compositions/sample-tag'

export default (ctx: Context) => {
  ctx.app.router!.afterEach((to, from) => {
    if (process.client) {
      const st = useSampleTag()
      st.addCustomValue(ctx.store)
    }
  })
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React hooksを基礎から理解する (useCallback編) と React.memo

React hooksとは

React 16.8 で追加された新機能です。
クラスを書かなくても、 stateなどのReactの機能を、関数コンポーネントでシンプルに扱えるようになりました。

useCallbackとは

useCallbackはパフォーマンス向上のためのフックで、メモ化したコールバック関数を返します。

useEffectと同じように、依存配列(=[deps] コールバック関数が依存している要素が格納された配列)の要素のいずれかが変化した場合のみ、メモ化した値を再計算します。

メモ化とは

メモ化とは同じ結果を返す処理について、初回のみ処理を実行記録しておき、値が必要となった2回目以降は、前回の処理結果を計算することなく呼び出し値を得られるようにすることです。

イベントハンドラーのようなcallback関数をメモ化し、不要に生成される関数インスタンスの作成を抑制、再描画を減らすことにより、都度計算しなくて良くなることからパフォーマンスを向上が期待できます。

基本形

useCallback(callbackFunction, [deps]);

sampleFuncは、再レンダーされる度に新しく作られますが、a,bが変わらない限り、作り直す必要はありません。

const sampleFunc = () => {doSomething(a, b)}

usecallbackを使えば、依存配列の要素a,bのいずれかが変化した場合のみ、以前作ってメモ化したsampleFuncの値を再計算します。一方で全て前回と同じであれば、前回のsampleFuncを再利用します。

const sampleFunc = useCallback(
  () => {doSomething(a, b)}, [a, b]
);

再レンダーによるコストについて検証してみる

Text,Count,Buttonコンポーネントを子に持つ親コンポーネントCounterコンポーネントを作成しました。

testVol1.jsx
import React, {useState} from 'react'

//Titleコンポーネント(子)
const Title = () => {
  console.log('Title component')
  return (
    <h2>useCallBackTest vol.1</h2>
  )
}

//Buttonコンポーネント(子)
const Button = ({handleClick,value}) => {
  console.log('Button child component', value)
  return <button type="button" onClick={handleClick}>{value}</button>
}

//Countコンポーネント(子)
const Count = ({text, countState}) => {
  console.log('Count child component', text)
  return <p>{text}:{countState}</p>
}

//Counterコンポーネント(親)
const Counter = () => {

  const [firstCountState, setFirstCountState] = useState(0)
  const [secondCountState, setSecondCountState] = useState(10)

//+ 1 ボタンのstateセット用関数
  const incrementFirstCounter = () => setFirstCountState(firstCountState + 1)

//+ 10 ボタンのstateセット用関数
  const incrementSecondCounter = () => setSecondCountState(secondCountState + 10)

//子コンポーネントを呼び出す
  return (
    <>
      <Title/>
      <Count text="+ 1 ボタン" countState={firstCountState}/>
      <Count text="+ 10 ボタン" countState={secondCountState}/>
      <Button handleClick={incrementFirstCounter} value={'+1 ボタン'}/>
      <Button handleClick={incrementSecondCounter} value={'+10 ボタン'}/>
    </>
  )
}

export default Counter

console.logを実行しているだけですが、すべてのコンポーネントが再レンダーされています。この部分で高コストな処理を行っていれば、その分だけパフォーマンスに悪影響を与えることになりますし、サイトが大きくなると、負荷も大きくなっていきます。

React.memoについて

React.memoでは、コンポーネントが返した React 要素を記録し、再レンダーされそうになった時に本当に再レンダーが必要かどうかをチェックして、必要な場合のみ再レンダーします。
デフォルトでは、等価性の判断にshallow compareを使っており、オブジェクトの1階層のみを比較することになります。

React.memoは、メモ化したいコンポーネントをラップして使います。

//Titleコンポーネント(子)
const Title = React.memo(() => {
  console.log('Title component')
  return (
    <h2>useCallBackTest vol.1</h2>
  )
})

//Buttonコンポーネント(子)
const Button = React.memo(({handleClick,value}) => {
  console.log('Button child component', value)
  return <button type="button" onClick={handleClick}>{value}</button>
})

//Countコンポーネント(子)
const Count = React.memo(({text, countState}) => {
  console.log('Count child component', text)
  return <p>{text}:{countState}</p>
})

コンポーネントをReact.memoでラップしてメモ化すると、初回ににTitleコンポーネント、Countコンポーネント2つ、Buttonコンポーネント2つがすべてレンダリングされました。

2回目以降、Titleコンポーネントについてはpropsがないので再レンダリングされていません。

Countコンポーネントについては、数字が更新されたコンポーネントについてのみ再レンダーされているので、最適化されています。

Buttonコンポーネントについては、ボタンのどちらかをクリックしたときにクリックされていないボタンも合わせ、2つのボタンが再レンダーされているので、最適化出来ていないようです。

Buttonコンポーネントは何故再レンダーされたか

Counter.jsx
//Counterコンポーネント(親)
const Counter = () => {

  const [firstCountState, setFirstCountState] = useState(0)
  const [secondCountState, setSecondCountState] = useState(10)

//+ 1 ボタンのstateセット用関数
  const incrementFirstCounter = () => setFirstCountState(firstCountState + 1)

//+ 10 ボタンのstateセット用関数
  const incrementSecondCounter = () => setSecondCountState(secondCountState + 10)

//子コンポーネントを呼び出す
  return (
    <>
      <Title/>
      <Count text="+ 1 ボタン" countState={firstCountState}/>
      <Count text="+ 10 ボタン" countState={secondCountState}/>
      <Button handleClick={incrementFirstCounter} value={'+1 ボタン'}/>
      <Button handleClick={incrementSecondCounter} value={'+10 ボタン'}/>
    </>
  )
}

<Button handleClick={incrementFirstCounter} value={'+1 ボタン'}/><Button handleClick={incrementSecondCounter} value={'+10 ボタン'}/>の2つのButtonコンポーネントについて、いずれかのボタンをクリックしたときに、stateが更新されるので再レンダーされますが、更新されていないほうのstateのボタンも再レンダーされています。一方のボタンがクリックされて親コンポーネントであるCounterコンポーネントが再レンダーされたタイミングで関数も再生成されており、再生成された関数をReact.memoが別の関数と認識したことによります。

React.memoについてもう少し詳しく

React.memoの第二引数には関数を渡すことができます。
第一引数として前回のprops(prevProps)を、第二引数として今回のprops(nextProps)を受け取ることが出来、真偽値を返すように書くことが出来ます。(areEqual)

const メモ化されたコンポーネント = React.memo(元のコンポーネント, (prevProps, nextProps) => {/* true or flase */})

このareEqual関数はpropsが等しいときにtrueを返し、propsが等しくないときにfalseを返します。
trueを返したときは再レンダーをスキップ、falseを返したときは再レンダーを行います。
(areEqualを省略した場合は、propsのshallow compareで等価性を判断することになります。)
また等価性のチェックにも、当然コストがかかることを考慮しなければなりません。

React公式サイト(React.memo)

useCallbackとReact.memoを組み合わせて最適化

親コンポーネントであるCounterコンポーネントが再レンダーされたタイミングで関数が生成性されないようにするため、useCallbackを使って最適化していきます。

//ReactからuseCallbackをimport
import React, {useState, useCallback} from 'react'

//Titleコンポーネント(子)
//React.memoでラップ
const Title = React.memo(() => {
  console.log('Title component')
  return (
    <h2>useCallBackTest vol.1</h2>
  )
})

//Buttonコンポーネント(子)
//React.memoでラップ
const Button = React.memo(({handleClick,value}) => {
  console.log('Button child component', value)
  return <button type="button" onClick={handleClick}>{value}</button>
})

//Countコンポーネント(子)
//React.memoでラップ
const Count = React.memo(({text, countState}) => {
  console.log('Count child component', text)
  return <p>{text}:{countState}</p>
})

//Counterコンポーネント(親)
const Counter = () => {

  const [firstCountState, setFirstCountState] = useState(0)
  const [secondCountState, setSecondCountState] = useState(10)

//+ 1 ボタンのstateセット用関数
//useCallbackで関数をラップし、依存配列には関数内で利用しているfirstCountStateを入れます。
  const incrementFirstCounter = useCallback(() => setFirstCountState(firstCountState + 1),[firstCountState])

//+ 10 ボタンのstateセット用関数
//useCallbackで関数をラップし、依存配列には関数内で利用しているsecondCountStateを入れます。
  const incrementSecondCounter = useCallback(() => setSecondCountState(secondCountState + 10),[secondCountState])

//子コンポーネントを呼び出す
  return (
    <>
      <Title/>
      <Count text="+ 1 ボタン" countState={firstCountState}/>
      <Count text="+ 10 ボタン" countState={secondCountState}/>
      <Button handleClick={incrementFirstCounter} value={'+1 ボタン'}/>
      <Button handleClick={incrementSecondCounter} value={'+10 ボタン'}/>
    </>
  )
}

export default Counter

うまく最適化出来ました!
関数をCounterコンポーネントの外に出せばいいんじゃないの!?って、そうなんですが、ここはuseCallbackの動作サンプルを作りたかったので。。。。:pray_tone2:
React.memoとuseCallback、さっそくリファクタリングに役立ちそうです:smiley:

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

sortをいれるとUnexpected side effect in computed propertyってエラー

Vue.jsで
Unexpected side effect in "xxx" computed property
というエラーがでたのでメモ

computedで配列をsortして返そうとしたらエラー

computed: {
  members() {
    return this.xxx.sort((a, b) => {
      if (a < b) return -1
      if (a > b) return 1
      return 0
    })
  }
}

computed: {
  members() {
    return this.xxx.slice().sort((a, b) => {
      if (a < b) return -1
      if (a > b) return 1
      return 0
    })
  }
}

これでエラー回避できた。

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

Vueで開発するときに整形ルールがごっちゃになるのをどうにかしたい

はじめに

vscodeでvueの開発をしているときに、しばしばコード整形ルールがごっちゃになってしまう。

自分の場合、慣例的にGoogle JavaScript Styleを採用しているので更にややこしいことになってしまっている。

特に困るのが以下の問題である:

  • シングルクオートにならない
  • 配列の末尾にカンマが入らない
  • 変数の終わりのセミコロンが消える
  • <template>内のタグの属性が同じ行になってしまう

結論

結論から先にいうと、vscodeのprettierがeslintの値を読まないのが原因だった。eslintのprettier/prettierの値と同じ内容の.prettierrc.jsを作成することで解決できた。

<template>タグ内の整形ルールがおかしいのはprettierのバグのようなのでoffで無効化し、eslint-plugin-vueのルールを採用するようにする。(参考:prettier not fixing vue/max-attributes-per-line

なお、vueのバージョンが新しくなってエラー扱いになるのは嫌なのでvueのコード整形ルールはvue3-recommendedを採用。filtersmethodsと統合され非推奨になるので注意。

npm install -D eslint-plugin-vue@next eslint-config-google
.eslintrc.js
module.exports = {
  root: true,
  env: {
    node: true,
  },
  extends: [
    'google',
    'plugin:vue/vue3-recommended',
    'eslint:recommended',
    '@vue/prettier',
  ],
  parserOptions: {
    parser: 'babel-eslint',
  },
  plugins: ['vue', 'prettier'],
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    quotes: ['error', 'single'],
    'prettier/prettier': [
      'error',
      {
        htmlWhitespaceSensitivity: 'ignore',
        semi: true,
        singleQuote: true,
        trailingComma: 'all',
      },
    ],
    'vue/max-attributes-per-line': 'off',
  },
};
.prettierrc.js
module.exports = {
  htmlWhitespaceSensitivity: 'ignore',
  semi: true,
  singleQuote: true,
  trailingComma: 'all',
};

おまけ

さて、vueのプロジェクトをつくったときにしばしば以下の行が追加される。

.eslintrc.js
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',

しかし、要はプロダクションモードのときに圧縮するついでにconsole.log()を削除すれば済む話なのでわざわざルールとして入れる必用はないと思う。

これらの行は削除し、vue.config.jsの圧縮時の設定をいじって前回使ったTerserPluginを使って圧縮とconsole.log()の削除を同時に行う。

npm install -D terser-webpack-plugin
vue.config.js
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  configureWebpack: {
    // ...
    optimization: {
      minimize: process.env.NODE_ENV === 'production',
      minimizer: [
        new TerserPlugin({
          terserOptions: {
            ascii_only: true,
            compress: { drop_console: true },
            mangle: true,
            ecma: 6,
            output: { comments: false, beautify: false },
          },
        }),
      ],
    },
  },
  // ...省略
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む