20200318のJavaScriptに関する記事は27件です。

【JavaScript】flatMapで配列の配列を平坦化して各要素に親配列のインデックスを付けちゃう

経緯

  • ページングしろ!一覧なんだから当たり前だろ!
  • でもページ数は教えないし、1 ページあたりの件数も合計件数も教えないぞ!

解決策

  • ページの目次を 0 から順にインクリメントする
  • 要素数が 0 件の配列を引くまで繰り返す
  • ↑ を非同期で実行し、取得結果をローカルに保持
  • 取得結果は以下のような配列の配列となる様にする
  • flatMap を使って平坦化していろいろする
取得結果
[
  [
    { value: '0 番目の要素です。' },
    { value: '1 番目の要素です。' }
  ],
  [
    { value: '0 番目の要素です。' },
    { value: '1 番目の要素です。' }
  ]
]

flatMapで配列の配列を平坦化するやつ

pages.flatMap((page, index) => {
  return page.map(list => {
    return ({ pageNo: `${index} ページ`, ...list });
  });
});

// こう書いてもいい
pages.flatMap((page, index) => page.map(list => ({ pageNo: `${index} ページ`, ...list })));

実行してみる

データを用意
const pages = [];
for (let i = 0; i < 2; i++) {
  const page = [];
  for (let j = 0; j < 2; j++) {
    page.push({ value: `${j} 番目の要素です。` });
  }
  pages.push(page);
}
実行結果
console.log(pages.flatMap((page, index) => {
  return page.map(list => {
    return ({ pageNo: `${index} ページ`, ...list });
  });
}));

[
  { pageNo: "0 ページ", value: '0 番目の要素です。' },
  { pageNo: "0 ページ", value: '1 番目の要素です。' },
  { pageNo: "1 ページ", value: '0 番目の要素です。' },
  { pageNo: "1 ページ", value: '1 番目の要素です。' }
]

Array.prototype.flatMap() - JavaScript | MDN

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

【Vue】喘ぎ声メーカー作ってみた

image.png
スーパー喘ぎ声メーカー
用意した単語をシャッフルして喘ぎ声を生成するツールを作ってみました。
喘ぎ声に限らず、好きな単語を入れて遊べます。

何故作ろうと思ったのか

BL小説書きの友人が「この喘ぎ声の組み合わせさっきも書いたな?
となるので自動生成してくれるツールがほしい
」と言っていたので。
BLに限らずいやらしい小説を書く方への負担を少しでも減らせないかと思い、作成にいたりました。
あとVueの勉強になりそうだと思ったので。
筆者は文系のwebデザイナー(見習い)であり、プログラミングの分野は勉強中です。

使ったもの

作成の流れ

プロトタイプ①

02.png
まずHTMLとJavaScriptのみでプロトタイプを作成。
「生成!」ボタンを押すと配列をシャッフルした結果が表示されます。
決め打ちのデータしか組み合わせられませんが、大体のイメージができあがりました。

ちなみに「やっそん(おそらく「やめて、そんな」→「やっ…そん…」→「やっそん」と転じた)」は
一部界隈のスラングでBLの性行為のことを指します。永遠に使えないムダ知識をあなたに。

使用例:出会って5秒でやっそん

プロトタイプ②

image.png
Vue.jsでよくあるTodoアプリのコードを元にプロトタイプ第二弾を作成。
入力したデータを組み合わせられるようになった点はいい感じです。

Vue.js
random: function () {
  const rnd = Math.floor(Math.random() * this.words.length);
  return this.words[rnd];
}

ただこのメソッドだと同じ単語を何度も使ってしまうため、「あっあっあっ」と
ネフェルピトーに脳を弄くられるポックルみたいになってしまいます。
配列をシャッフルするコードに書き換えなければなりません。

モックアップ

image.png
Bootstrap4で見た目だけのものを作成。レスポンシブにも対応です。

完成品

完成品がこちらです!→スーパー喘ぎ声メーカー

Vue.js
var vm = new Vue({
  el: '#app',
  data: {
    newItem: '',
    array: [
      'やっ',
      'あっ',
      'んっ…',
    ],
  },
  methods: {
    addItem: function () {
      this.array.push(this.newItem);
      this.newItem = '';
    },
    deleteItem: function (index) {
      this.array.splice(index, 1);
    },
    shuffle: function (array) {
      var sArray = [];
      sArray = array.slice(0, array.length);
      var n = sArray.length, t, i;
      while (n) {
        i = Math.floor(Math.random() * n--);
        t = sArray[n];
        sArray[n] = sArray[i];
        sArray[i] = t;
      }
      return sArray.join('');
    },
    reset: function () {
      this.array = []
    }
  }
});

長くなるのでVue部分だけ掲載。(HTML含む全文はgithubで公開してます

Vue.js
shuffle: function (array) {
  var n = array.length, t, i;
  while (n) {
    i = Math.floor(Math.random() * n--);
    t = array[n];
    array[n] = array[i];
    array[i] = t;
  }
  return array.join('');
}

これは過去のコードなのですが、このメソッドだと
シャッフル実行時配列そのものもシャッフルされてしまいます。下図参照。
image.png

Vue.js
shuffle: function (array) {
  var sArray = [];
  sArray = array.slice(0, array.length);
  var n = sArray.length, t, i;
  while (n) {
    i = Math.floor(Math.random() * n--);
    t = sArray[n];
    sArray[n] = sArray[i];
    sArray[i] = t;
  }
  return sArray.join('');
}

コピーした配列にシャッフル処理をするように書き換えたら
単語のリストはシャッフルされなくなりました。
この対処法が正しいかはわかりませんが…

list.pngimage.png
また生成結果を表示するリストは、idを個別に用意するため
clipboard-target1~clipboard-target10までわざわざ一つ一つ<li>要素を作成していました。
何故idを個別に用意する必要があるのかと言うと、
clipboard.jsで特定のdivのテキストをコピーするために
個別のターゲットを指定しなければならないからです。
data-clipboard-targetの指定先がid名になっているのがお分かりいただけると思います。

でもVueならもっとスッキリ書けるんじゃないか?とよく考えたら、
v-forのループ処理でなんとかできそうだと思い至る。

HTML
<li v-bind:id="`clipboard-target${n}`" class="clipboard-target mb-2" v-for="n in 10">
  <span>{{ shuffle(array) }}</span>
  <div class="btn btn-danger btn-sm ml-3 btn-clipboard" data-toggle="tooltip"
    v-bind:data-clipboard-target="`#clipboard-target${n}`">
    コピー
  </div>
</li>

v-bindv-forでidとdata-clipboard-targetが連番になった<li>要素を10個作ることができました。
v-for="n in 10"の10の部分を変更すれば、喘ぎ声セットを10個でも100個でも作成できますね!
JavaScriptでやったら多分めちゃくちゃ面倒くさい処理ですよね…
ありがとうVue。

感想

  • 明確な目的があったためVue勉強のモチベーションが保ててよかった
  • 友達にややウケした

課題・問題点

  • 双方向バインディングすぎてEnterを押すたびに結果が再生成される
  • 最初のイメージに合わせる知識がなかった(本当は「生成」ボタン押下時に結果が表示されるようにしたかった)
  • 一度リセットしてから再度単語を追加して生成すると、ツールチップが表示されない(コピー自体は問題なくできる)
  • 本当はVueのコンポーネント機能を使いたかったが調べてもわからなかった

GitHubでコードを公開しておりますので、アドバイスいただけたら嬉しいです。
https://github.com/mitaru/superxxxmaker
コード見たら私がマジの初心者であることはお分かりになるかと思います…
プルリクお待ちしております。

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

Vue.js 現在時刻の表示 ~時計~

Vue.jsとは

ユーザーインターフェイスを構築するためのプログレッシブフレームワークです。他の一枚板(モノリシック: monolithic)なフレームワークとは異なり、Vue は少しずつ適用していけるように設計されています。中核となるライブラリは view 層だけに焦点を当てています。そのため、使い始めるのも、他のライブラリや既存のプロジェクトに統合するのも、とても簡単です。また、モダンなツールやサポートライブラリと併用することで、洗練されたシングルページアプリケーションの開発も可能です。

参考URL
https://jp.vuejs.org/v2/guide/index.html

Vue.jsを使用するメリット

  • 気軽に使える: Vue.js はjQueryと同様に、scriptタグを1行書くだけで使い始めることができます。
  • DOM操作を自動的に行ってくれる:
    HTMLドキュメント全体の要素の構成をDOM(Document Object Model)といいます。Vue.jsはHTML側の要素とJavaScript側の値やイベントとの対応付を自動で行ってくれます。これにより、jQueryよりも簡潔に分かりやすくコードを記載することができます。

  • 学習コストが低い:
    AngularやReactと比較してフレームワークの規模が小さい分、覚えることも少なくて済みます。JavaScriptやjQueryの基礎知識があれば数時間の学習で開発を開始することができるでしょう。

Vue.jsで現在時刻を取得して表示する

初めに全体像を掴んでもらうために、完成品を記述します。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <p>{{ now }}</p>
    <button v-on:click="time">現在時刻</button> <!-- v-on:event -->
  </div>
  <!-- Vue.jsをインストール -->
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <!-- ここから記述していきます -->
  <script>
    let app = new Vue({
      el: "#app",
      data:{
        now: "00:00:00"
      },
      methods: {
        time: function(e){ function(e) 
          var date = new Date();

          this.now = date.getHours() + ":"
          + date.getMinutes() + ":" +
          date.getSeconds();
        }
      }
    });
  </script>
</body>
</html>

完成品

スクリーンショット 2020-03-18 22.33.27.png

ここから要素ごとに説明していきます。ちなみにVue.jsは始めたばかりになるので、説明が至らぬところがあればコメントお願いします。

  <div id="app">
    <p>{{ now }}</p>
    <button v-on:click="time">現在時刻</button> <!-- v-on:event -->
  </div>

まずはHTMLでのこちら。

{{ }}
こちらの記述がJavaScriptのメッセージに自動的に置換してくれます。
v-on
v-on ディレクティブを使うことで、DOM イベントの購読、イベント発火時の JavaScript の実行が可能になります。

参考URL
https://jp.vuejs.org/v2/guide/events.html#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%81%AE%E8%B3%BC%E8%AA%AD

次にコメントアウトにてscript以降の説明していきます。

let app = new Vue({
  el: "#app", //el: "ID要素"を取得します
  data:{
    //こちらのプロパティでHTMLの{{}}を置換します
  },
})

次にmethod以降の説明していきます。

let app = new Vue({
  el: "#app",
   data:{
     now: "00:00:00" //now: == {{}}
   },

   methods: {
     time: function(e){  //function(e) この引数eは、eventの「e」
       let date = new Date();  //new演算子でオブジェクトのインスタンスを生成
        //現在時刻の取得 **ここからはjavascript**
         this.now = date.getHours() + ":"
         + date.getMinutes() + ":" +
         date.getSeconds();
     }
   }
});

ボタンを押す前

スクリーンショット 2020-03-18 23.03.14.png

どうでしょうか?
クリックイベントで、現在の時間を取得できましたか?

私もまだまだ勉強中ですが、書き方自体はシンプルでとてもわかりやすいですよね。

ちなみに私はこちらをYoutubeで見て学習をしました。
是非そちらもご参考までに見てみてください。

Youtube たにぐち まことのともすたチャンネル様
https://www.youtube.com/watch?v=jdcZ3LvTs78

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

Vuexのmutations/actionsが実行された事をテストする方法を調べたメモ

Store側のテストについては言及があっても、コンポーネント側のテストについてはあまり見かけなかったので調べた内容をメモ。

結論コード

概要

  • Vuex Storeを用意するよ
  • mutationsのStubを用意するよ
  • 上2つをセットしたwrapperでcalledを見るよ

コード

ButtonStuff.vue
<template>
  <button
    type='button'
    @click='doSomething'
  >
    Do something
  </button>
</template>

<script>
export default {
  name: 'ButtonStuff',
  props: {
    // some properties 
  },
  methods: {
    doSomething () {
      // Mutation 
      this.$store.commit('doStoreMutation', {
        hoge: 'hoge',
        foo: 'foo'
      })

    }
  }
}
</script>

ButtonStuff.spec.js
import Vuex from 'vuex'
import chai, { expect, assert } from 'chai'
import sinon from 'sinon'
import sinonChai from 'sinon-chai'
import { shallowMount, createLocalVue } from '@vue/test-utils'
import Component from '@/components/Atoms/ButonStuff.vue'

chai.use(sinonChai)

const localVue = createLocalVue()
localVue.use(Vuex)

describe('ButtonStuff.vue', () => {
    describe('Events', () => {
      describe('Click', () => {
        let store

        // 実際のMutationと同じ名前にする。
        const mutations = {
          doStoreMutationA: sinon.stub(),
          doStoreMutationB: sinon.stub(),
          doStoreMutationC: sinon.stub()
        }

        beforeEach(() => {
          store = new Vuex.Store({
            state: {},
            mutations
          })
        })

        it('should be triggered the mutation', () => {
          const wrapper = shallowMount(Component, { store, localVue })
          wrapper.find('button').trigger('click')
          assert(mutations.doStoreMutationA.called)
        })
      })
    })
  })
})

※ mutationsを例に書いていますが、actionsも同じ方法でいけるはず。

ハマった所

  • テストコード側のmutationsオブジェクトに用意したmutationの名前は実際のstoreにあるmutation名と合わせるところ。あまり意識せずに doStoreMutationMock みたいにしたらキックされなくて悩んだ
  • JestとMocha&chaiでの書き方の違い、それぞれの記法でもう片方はどうするか、実際できるのかがわからず調べるのに時間がかかった。
  • assertを使ってるけど、本当はexpectでやりたいが、書き方がまだわかってない(何パターンか試したがコケる)のでこの項目に関してassertで妥協している
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ExtJS6系、7系でExtJS3系のReader定義を使う

ExtJS 3.4 で使っていた Ext.data.Record の定義を ExtJS 6, 7 でそのまま使いたいとき。
もしくは横着したい人向け。

Ext.override(Ext.data.XmlReader, {
    constructor: function(config) {
        if(!config.model && config.fields) {
            config.model = 'Model-' + Ext.id();
            Ext.define(config.model, {
                extend: 'Ext.data.Model',
                fields: config.fields
            });
        }

        Ext.data.XmlReader.superclass.constructor.apply(this, arguments);
    }
});

Overrideした後は、XmlReader使うときにこんな感じで定義できる。

new Ext.data.XmlReader({
    record: 'data',
    rootProperty: 'data',
    fields: [
        { name: 'RESULT' },
        { name: 'ID' }
    ]
});

CDNで使ってないとあんまり意味ないので注意。

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

Kinx プレビュー版リリース

Kinx プレビュー版のリリース

はじめに

「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」でお送りするスクリプト言語 Kinx。まだまだ途中だがリリースするのも意味があるだろうとの想定から、実行可能な形でパッケージングし、まずはプレビュー・リリースすることにしました。Windows 版の実行ファイルはアイコンもつけていい感じ。Linux は /usr/bin にインストールしてしまうので、一応気にしておいてください。

本題

↓ここです。

もちろん、git clone して make もできます。

本題、おしまい。

振り返り

元々はこの記事での「プログラマに馴染むシンタックスってのは C 系だよね、でも何で Ruby も Python も全然違うのでしょう」というところから始まった今回のプロジェクト。プロジェクト自体は去年末くらいから実質スタートしてたので、約 5 ヶ月くらいか。

元々からしてほぼ JavaScript な文法なので、作りながらではあるものの、既にすっかり私の手には馴染んでます。

まだ初版にはできませんが(おっとさっき気づいたのだが標準入力がない...)、結構色々試せるとは思います。

ほんのちょっと興味があれば

何か試してみてフィードバック貰えると非常に嬉しい。まぁ、既存の何かを置き換えるようなたいそうなモノではないので、ちょっとした提案とかこんなんあるといいんじゃない、とか、そういうライトな感じのご意見ください。

すみません、見返りはあまり無いと思います…。そういうの楽しめる方か、面白そうだと思ってくださる方向け。

おわりに

最後はいつもの以下の定型フォーマットです。

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

レジの店員を呼ぶアプリをつくた android cordova ハイブリッドアプリ

成果物

https://play.google.com/store/apps/details?id=com.itsumen.regi&hl=ja

リポジトリ

https://github.com/yuzuru2/regi_apuri_sozai

環境

  • node.js 12.14.0
  • cordova 9.0.0
  • java 1.8.0

※javaとandroidのsdkは各自インスコしてパスを通しておいてください。

コマンド

$ npm i -g cordova
$ cordova create sample com.example.sample sample
$ cd sample
$ cordova platform add android
$ cordova plugin add cordova-plugin-volume-control
$ rm -rf www/*
$ cd www
$ git clone https://github.com/yuzuru2/regi_apuri_sozai.git .
$ cd ..
$ cordova run android
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Gulp+JQuery(ES6)をTypeScriptでIE対応する

はじめに

「JSでClass使えるんや、使ってみよー」
って言ってIEで動かなくて泣いた人は私です。(今更なんですけどね)

jQuery(ES6)をTypeScriptにゆるく書き換えて静的型付けを行い、
かつES5の形式でコンパイルする、ということをしたので、その備忘録です。

環境

OS

Window 10

Node

11.13.0

npm

6.4.1

Gulp

4.0.2

必要なパッケージのインストール

必要なパッケージをプロジェクト内にインストールします、

インストールするパッケージ一覧

  • typescript
  • gulp
  • gulp-typescript
  • gulp-sourcemaps
  • del
  • gulp-load-plugins
  • gulp-notify
  • gulp-plumber
npm i -D typescript gulp gulp-typescript gulp-sourcemaps del gulp-load-plugins

Gulpタスクの追加

gulpの設定ファイルgulpfile.jspackage.jsonと同じ階層に作成し、設定を記述します。

htdocs
 ├ node_modules
 ├ package.json
 ├ gulpfile.js
 └ index.html

gulpfile.jsに以下の内容を記述します。
(バージョンにより書き方が異なる場合があります。)

gulpfile.js
const gulp        = require('gulp'),
      $           = require('gulp-load-plugins')(),
      typescript  = require('gulp-typescript'),
      sourcemaps  = require('gulp-sourcemaps'),
      del = require('del'),
      setting = {
        ts: {
          path: {
            src: 'ts/**/*.ts',
            dest: 'js/'
          },
          options: {
            target: 'ES5',
            out: 'main.js',
            lib: ['ES5', 'dom']
          }
        }
      };

// TypeScript
const ts = done => {
  gulp.src(setting.ts.path.src)
    .pipe($.plumber({
      errorHandler: $.notify.onError("Error: <%= error.message %>") //<-
    }))
    .pipe(sourcemaps.init())
    .pipe(typescript(setting.ts.options))
    .pipe(sourcemaps.write('./'))
    .pipe(gulp.dest(setting.ts.path.dest));

  // DONE
  done();
}

// Clean
const clean = done => {
  del([
      // assets/js/app.jsを削除
      setting.ts.path.dest+setting.ts.options.out,

      // assets/js/app.js.mapを削除
      setting.ts.path.dest+setting.ts.options.out+".map",
  ]);

  // DONE
  done();
}

// Build
gulp.task('build', gulp.series(
  gulp.parallel(
    clean,
    ts
  ),
  done => {
    done();
  }
 )
);

// Watch
gulp.task('watch', () => {
  gulp.watch([setting.ts.path.src], ts);
});

gulp.task('default', gulp.task('watch'));

jQueryを使えるようにする

このままだと$をjQueryと認識してもらえなかったり、jQueryで取得したDOMがすべてany型に判別されたりしてしまうため、TypeScript内でもjQueryを使用できるよう、型定義ファイルをインストールします。

型定義ファイルの管理を行うパッケージ、typingsをインストールします。

npm i -g typings

jQueryの型定義ファイルのインストールをします。

typings install jquery --save --global

すると/typing/のディレクトリが生成されます。
この中のファイルはこのあとTypeScriptの記述部分で使用します。

コードの書き換え

HTML

検証のため、以下のようなHTMLを用意します。
jQueryのライブラリ自体はサイト側で読み込む必要があるため、HTMLの<head>内で読み込みます。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>ts test</title>

    <!-- jQueryの読み込み -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>

    <!-- コンパイルされたJSファイルの読み込み -->
    <script src="./js/main.js"></script>
</head>
<body>
    <button type="button" id="btn">ボタン</button>
</body>
</html>

書き換え前のコード

jQueryで書いた元のコードです。

/js/main.js
class Test {
    hello() {
        console.log('test')
    }
}

const onClick = elm => {
    const test = new Test();

    elm.on('click', () => {
        test.hello();
    })
}

$(() => {
    onClick($('#btn'))
})

書き換え後のコード

TypeScriptに書き換えた後のコードです。
拡張子を.tsに変更し、gulpfile.jssrcで記述したディレクトリにコピペします。

型推論で自動で型判別してくれるため、必要のない方は記入していません。
必要に応じて型を設定してください。
今回はonClickの関数に引数elmがJQeryであることを明示しています。
JQueryJは大文字なので気をつけてください。

先程インストールしたjQeryの型定義ファイルをtsファイルの先頭で読み込みます。

main.ts
// jQueryの型定義ファイルの読み込み
/// <reference path="../typings/globals/jquery/index.d.ts" />

class Test {
    hello() {
        alert("Hello World")
    }
}

// TypeScriptの型定義を行っている↓
const clickFunc = (elm: JQuery) => {
    const test = new Test()

    elm.on('click', () => {
        test.hello()
    })
}

// jQueryもコンパイルを通すことができる
$(() => {
    clickFunc($('#btn'))
})

gulpの実行

gulp buildでdistファイルの削除、TypeScriptのコンパイルができるように設定しているため、それを実行します。

gulp build

/js/main.js/js/main.js.mapが生成されます。
.mapの方は、書き出されたJavaScriptの記述はTypeScriptでいうと何行目にあたる、というような情報を持っているファイルでブラウザ側で勝手に解釈してくれるため、見る必要はないです。

ファイルを監視し、保存のたびにコンパイルを行うには、以下のコマンドを実行してください。

gulp

書き出されたJSファイル

Classなどの記述がTypeScriptのオプションで設定したES5に合わせて書き出されています。

/// <reference path="../typings/globals/jquery/index.d.ts" />
var Test = /** @class */ (function () {
    function Test() {
    }
    Test.prototype.hello = function () {
        alert("Hello World");
    };
    return Test;
}());
var onClick = function (elm) {
    var test = new Test();
    elm.on('click', function () {
        test.hello();
        console.log("test");
    });
};
$(function () {
    onClick($('#btn'));
});

//# sourceMappingURL=main.js.map

これでjQueryを残したまま、レガシーなブラウザも気にせずにコーディングをすることができるようになりました。

余談

JSフレームワークを使うときのようモジュールで分けて読み込んだ方がコンポーネントで分けて管理できるので良さそうですね。

test.js
module Module_Test {
    export class Test {
        hello() {
            alert('Hello World')
        }
    }
}
main.js
/// <reference path="../typings/globals/jquery/index.d.ts" />
/// <reference path="./test.ts" />

import Test = Module_Test.Test;

module clickFunc {
    export function run(elm: JQuery) {
        const test = new Test()

        elm.on('click', () => {
            test.hello()
        })
    }
}

$(() => {
    clickFunc.run($('#btn'))
})
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JQuery(ES6)をGulp+TypeScriptでIE対応する

はじめに

「JSでClass使えるんや、使ってみよー」
って言ってIEで動かなくて泣いた人は私です。(今更なんですけどね)

jQuery(ES6)をTypeScriptにゆるく書き換えて静的型付けを行い、
かつES5の形式でコンパイルする、ということをしたので、その備忘録です。

環境

OS

Window 10

Node

11.13.0

npm

6.4.1

Gulp

4.0.2

必要なパッケージのインストール

必要なパッケージをプロジェクト内にインストールします、

インストールするパッケージ一覧

  • typescript
  • gulp
  • gulp-typescript
  • gulp-sourcemaps
  • del
  • gulp-load-plugins
  • gulp-notify
  • gulp-plumber
npm i -D typescript gulp gulp-typescript gulp-sourcemaps del gulp-load-plugins

Gulpタスクの追加

gulpの設定ファイルgulpfile.jspackage.jsonと同じ階層に作成し、設定を記述します。

htdocs
 ├ node_modules
 ├ package.json
 ├ gulpfile.js
 └ index.html

gulpfile.jsに以下の内容を記述します。
(バージョンにより書き方が異なる場合があります。)

gulpfile.js
const gulp        = require('gulp'),
      $           = require('gulp-load-plugins')(),
      typescript  = require('gulp-typescript'),
      sourcemaps  = require('gulp-sourcemaps'),
      del = require('del'),
      setting = {
        ts: {
          path: {
            src: 'ts/**/*.ts',
            dest: 'js/'
          },
          options: {
            target: 'ES5',
            out: 'main.js',
            lib: ['ES5', 'dom']
          }
        }
      };

// TypeScript
const ts = done => {
  gulp.src(setting.ts.path.src)
    .pipe($.plumber({
      errorHandler: $.notify.onError("Error: <%= error.message %>") //<-
    }))
    .pipe(sourcemaps.init())
    .pipe(typescript(setting.ts.options))
    .pipe(sourcemaps.write('./'))
    .pipe(gulp.dest(setting.ts.path.dest));

  // DONE
  done();
}

// Clean
const clean = done => {
  del([
      // assets/js/app.jsを削除
      setting.ts.path.dest+setting.ts.options.out,

      // assets/js/app.js.mapを削除
      setting.ts.path.dest+setting.ts.options.out+".map",
  ]);

  // DONE
  done();
}

// Build
gulp.task('build', gulp.series(
  gulp.parallel(
    clean,
    ts
  ),
  done => {
    done();
  }
 )
);

// Watch
gulp.task('watch', () => {
  gulp.watch([setting.ts.path.src], ts);
});

gulp.task('default', gulp.task('watch'));

jQueryを使えるようにする

このままだと$をjQueryと認識してもらえなかったり、jQueryで取得したDOMがすべてany型に判別されたりしてしまうため、TypeScript内でもjQueryを使用できるよう、型定義ファイルをインストールします。

型定義ファイルの管理を行うパッケージ、typingsをインストールします。

npm i -g typings

jQueryの型定義ファイルのインストールをします。

typings install jquery --save --global

すると/typing/のディレクトリが生成されます。
この中のファイルはこのあとTypeScriptの記述部分で使用します。

コードの書き換え

HTML

検証のため、以下のようなHTMLを用意します。
jQueryのライブラリ自体はサイト側で読み込む必要があるため、HTMLの<head>内で読み込みます。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>ts test</title>

    <!-- jQueryの読み込み -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>

    <!-- コンパイルされたJSファイルの読み込み -->
    <script src="./js/main.js"></script>
</head>
<body>
    <button type="button" id="btn">ボタン</button>
</body>
</html>

書き換え前のコード

jQueryで書いた元のコードです。

/js/main.js
class Test {
    hello() {
        console.log('test')
    }
}

const onClick = elm => {
    const test = new Test();

    elm.on('click', () => {
        test.hello();
    })
}

$(() => {
    onClick($('#btn'))
})

書き換え後のコード

TypeScriptに書き換えた後のコードです。
拡張子を.tsに変更し、gulpfile.jssrcで記述したディレクトリにコピペします。

型推論で自動で型判別してくれるため、必要のない方は記入していません。
必要に応じて型を設定してください。
今回はonClickの関数に引数elmがJQeryであることを明示しています。
JQueryJは大文字なので気をつけてください。

先程インストールしたjQeryの型定義ファイルをtsファイルの先頭で読み込みます。

main.ts
// jQueryの型定義ファイルの読み込み
/// <reference path="../typings/globals/jquery/index.d.ts" />

class Test {
    hello() {
        alert("Hello World")
    }
}

// TypeScriptの型定義を行っている↓
const clickFunc = (elm: JQuery) => {
    const test = new Test()

    elm.on('click', () => {
        test.hello()
    })
}

// jQueryもコンパイルを通すことができる
$(() => {
    clickFunc($('#btn'))
})

gulpの実行

gulp buildでdistファイルの削除、TypeScriptのコンパイルができるように設定しているため、それを実行します。

gulp build

/js/main.js/js/main.js.mapが生成されます。
.mapの方は、書き出されたJavaScriptの記述はTypeScriptでいうと何行目にあたる、というような情報を持っているファイルでブラウザ側で勝手に解釈してくれるため、見る必要はないです。

ファイルを監視し、保存のたびにコンパイルを行うには、以下のコマンドを実行してください。

gulp

書き出されたJSファイル

Classなどの記述がTypeScriptのオプションで設定したES5に合わせて書き出されています。

/// <reference path="../typings/globals/jquery/index.d.ts" />
var Test = /** @class */ (function () {
    function Test() {
    }
    Test.prototype.hello = function () {
        alert("Hello World");
    };
    return Test;
}());
var onClick = function (elm) {
    var test = new Test();
    elm.on('click', function () {
        test.hello();
        console.log("test");
    });
};
$(function () {
    onClick($('#btn'));
});

//# sourceMappingURL=main.js.map

これでjQueryを残したまま、レガシーなブラウザも気にせずにコーディングをすることができるようになりました。

余談

JSフレームワークを使うときのようモジュールで分けて読み込んだ方がコンポーネントで分けて管理できるので良さそうですね。

test.js
module Module_Test {
    export class Test {
        hello() {
            alert('Hello World')
        }
    }
}
main.js
/// <reference path="../typings/globals/jquery/index.d.ts" />
/// <reference path="./test.ts" />

import Test = Module_Test.Test;

module clickFunc {
    export function run(elm: JQuery) {
        const test = new Test()

        elm.on('click', () => {
            test.hello()
        })
    }
}

$(() => {
    clickFunc.run($('#btn'))
})
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js セットアップメモ(Mac)

この記事の目的

Vue.jsの学習を始める際に実施したセットアップ手順のメモです。

開発環境、必要なもの

  • Mac OS Catalina 10.15.2 ←自分の場合(参考程度)

セットアップ

ndenvのインストール

  • 複数バージョンのNodeを切り替えて使用することになると思いますので、ndenv をインストールします。
$ brew install ndenv
  • PATHを追加します
echo 'export PATH="$HOME/.ndenv/bin:$PATH"'
echo 'eval "$(ndenv init -)"'
  • Nodeをインストールするために node-buildをインストールします。
$ git clone https://github.com/riywo/node-build.git $(ndenv root)/plugins/node-build

Node.jsのインストール

  • インストールできるNode.jsバージョンを確認
$ ndenv install -l
  • Node.jsのインストール
$ ndenv install v12.16.1  #LTS版がおすすめ
  • 使用するNode.jsの設定
$ ndenv versions # インストールしているNode.jsのバージョンを確認する
$ ndenv global v12.16.1 # デフォルトバージョンを設定するとき
$ ndenv local v12.16.1 # プロジェクトごとに変更するとき
  • Node.jsのバージョン確認
$ node --version

Vue.jsのインストール

  • Vue CLIのインストール
$ npm install -g @vue/cli
  • Vue cli-service-globalのインストール
$ npm install -g @vue/cli-service-global

動作確認

  • vueファイルを作成します。
$ cat app.vue
<template>
<div id="app">
    <h1>Hello World</h1>
</div>
</template>
  • 実行する
$ vue serve
  • ブラウザで確認する。
    • http://localhost:8080 にアクセスしてみてください。
    • 期待通りにブラウザに表示されていたら、セットアップ完了です!

終わりに

  • 最低限のセットアップになりますが、上記の手順で一通りの実行環境は整います。
  • editor用のプラグインの設定等の設定も追記予定です。

補足

  • Google Chrome用にvue-devtoolsをインストールしておくのも必須のようです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GAS DeveloperMetadata

とある理由で、PropertiesやCache以外でデータを保存する方法を検討しました

1.PropertiesとCache

どちらも、そのスクリプト内でしか利用できないんですね。
なんでこんな話になったかというと、

スプシAのスクリプトから、スプシBのスクリプトへデータを渡す

をやりたかったのですが、Propertiesは

let props = PropertiesService.getScriptProperties();
props.setProperty("key","value");

なので、スプシAのスクリプトからプロパティを呼び出すと、当然、スプシAのスクリプトのプロパティとなり、スプシBのスクリプトのプロパティをとることはできません
(設定も同じ)

Cacheも同じ構成をとっているので、こちらも同一スクリプトなり同一ドキュメント内に限定されます。
ユーザープロパティを使えばできるのかもしれませんが、別なユーザーでも動かしたい、と。

2.DeveloperMetadata

クラスリファレンスをつらつら見ていたら、Metadataというものがあるようで、スプシやレンジ等に設定できる模様。
正しい使い方か微妙でしたが、スプシのMetadataに渡したいデータを埋め込んでみました
埋め込む側

b.js
    let spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
    let meta = spreadsheet.getDeveloperMetadata();
    let value = meta.find(item => item.getKey() == "my_Meta_key");
    if(value === undefined){
      spreadsheet.addDeveloperMetadata("my_Meta_key","my_Meta_Value");
      return "my_Meta_value";
    }
    return value.getValue();

読み込む側

a.js
    let spreadsheet = SpreadsheetApp.openById("B_fileId");
    let meta = spreadsheet.getDeveloperMetadata();
    let value = meta.find(item => item.getKey() == "my_Meta_key");
    if(value === undefined){
      return "no meta-data";
    }
    return value.getValue();

3.ScriptId

実は、スクリプトのIDを集めるために上記のことをやるハメになったのですが、ScriptIdも

let scriptId = ScriptApp.getScriptId();

なので、自分のスクリプトIDしかわからないようです。

4.Metadataのスコープ

スコープ、がただしい表現か微妙ですが、以下の3つにMetadataは設定できるようです
Spreadsheet
Sheet
Range

すべて、
addDeveloperMetadata
で作成できるようになっています。

5.DeveloperMetadata

私のGoogle力がたりないのか、ほとんど情報みつからなかったんですが、だれも使わないのでしょうか。
普通は、Propertiesで足りるからかもしれません
(スプレットシートのシートに直に書く、という方法もレガシーながらあり)

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

脚本上で物事が起こる15のポイントがだいたい何ページに来るべきかを計算するツール

作ったもの

脚本上で物事が起こる15のポイントがだいたい何ページに来るべきかを計算するツール

なんで作ったのか

ストーリー構成手法のひとつ「三幕構成」の解説本「SAVE THE CAT」
→脚本上で物事が起こるポイントが15項目に分けられている
→それぞれの項目が起こるページ数が決められている
→一般的な脚本の110ページ(英語)をベースにしたページ数しかのってない
→110ページベースだと同人誌を書くときにイメージがわきにくい
→いちいち計算するのもめんどう
→ページ数計算するツールを作ろう
→送信ボタン押すのめんどう
→ページ数を入れた瞬間に数値が出てほしい

この記事のポイント

JSは3日前に書き始めたレベル、プログラム書き自体も初心者なので、
1ヶ月後忘れてまた「アレ ドウヤルンダッケ ハヤクテスゴイヤツ?」
となったとき見るようにこの記事を書きました。
GlobalEventHandlers.oninputを忘れないためと、「SAVE THE CAT」はいいぞするための記事です。

GlobalEventHandlers.oninput

詳しいことは

GlobalEventHandlers.oninput - Web API | MDN
を見ればわかるんですが

要はquerySelectorでフォームを選択して、それの入力をGlobalEventHandlers.oninputで感知して、入力を検出したときにJSが呼び出されるみたい。

ちなみに、今回はフォーム1つなのでこれで問題ないけど、フォームが2つ以上あって同じようなことをしたい場合、querySelectorでもう細かい指定ができるみたい。
https://developer.mozilla.org/ja/docs/Web/API/Document/querySelector

あと、oninputと似たやつでonchangeというのもあります。
oninputは入力中にJSが動いて随時計算しますが、onchangeは入力が確定したあとJSが呼び出されます。
入力中にガチャガチャ動くのが目障りなときはonchangeを使おう。

コード

test.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>ブレイク・スナイダー・ビート・シート ページ数計算</title>
    </head>
    <body>
    <h2>ブレイク・スナイダー・ビート・シート ページ数計算</h2> 
    ページ数をいれてください(半角数字):<input type="tel" placeholder="128" maxlength="4" size="20">
    <table>
    <tr><td>1. オープニングイメージ (1):</td><td id="val1"></td></tr>
    <tr><td>2. テーマの提示 (5):</td><td id="val2"></td></tr>
    <tr><td>3. セットアップ (1~10):</td><td id="val3"></td></tr>
    <tr><td>4. きっかけ (12):</td><td id="val4"></td></tr>
    <tr><td>5. 悩みのとき (12~25):</td><td id="val5"></td></tr>
    <tr><td>6. 第一ターニング・ポイント (25):</td><td id="val6"></td></tr>
    <tr><td>7. サブプロット (30):</td><td id="val7"></td></tr>
    <tr><td>8. お楽しみ (30~55):</td><td id="val8"></td></tr>
    <tr><td>9. ミッド・ポイント (55):</td><td id="val9"></td></tr>
    <tr><td>10. 迫り来る悪い奴ら (55~75):</td><td id="val10"></td></tr>
    <tr><td>11. すべてを失って (75):</td><td id="val11"></td></tr>
    <tr><td>12. 心の暗闇 (75~85):</td><td id="val12"></td></tr>
    <tr><td>13. 第二ターニング・ポイント (85):</td><td id="val13"></td></tr>
    <tr><td>14. フィナーレ (85~110):</td><td id="val14"></td></tr>
    <tr><td>15. ファイナル・イメージ (110):</td><td id="val15"></td></tr>
    </table>
<script>
let input = document.querySelector('input');
input.oninput = handleInput;
function handleInput(e) {
    val1.textContent = `1(${e.target.value*1/110})`;
    val2.textContent = `${Math.round(e.target.value*5/110)}${e.target.value*5/110})`;
    val3.textContent = `1〜${Math.round(e.target.value*10/110)}`;
    val4.textContent = `${Math.round(e.target.value*12/110)}${e.target.value*12/110})`;
    val5.textContent = `${Math.round(e.target.value*12/110)}${Math.round(e.target.value*25/110)}`;
    val6.textContent = `${Math.round(e.target.value*25/110)}${e.target.value*25/110})`;
    val7.textContent = `${Math.round(e.target.value*30/110)}${e.target.value*30/110})`;
    val8.textContent = `${Math.round(e.target.value*30/110)}${Math.round(e.target.value*55/110)}`;
    val9.textContent = `${Math.round(e.target.value*55/110)}${e.target.value*55/110})`;
    val10.textContent = `${Math.round(e.target.value*55/110)}${Math.round(e.target.value*75/110)}`;
    val11.textContent = `${Math.round(e.target.value*75/110)}${e.target.value*75/110})`;
    val12.textContent = `${Math.round(e.target.value*75/110)}${Math.round(e.target.value*85/110)}`;
    val13.textContent = `${Math.round(e.target.value*85/110)}${e.target.value*85/110})`;
    val14.textContent = `${Math.round(e.target.value*85/110)}${Math.round(e.target.value)}`;
    val15.textContent = `${Math.round(e.target.value)}`;
}
</script>
    </body>
</html>

課題

自分用なので今回は気にしないことにしたけど、半角数字以外を入れるとエラーになります。
全角数字の認識とか、エラー検知とかできると親切かも。

あとがき

同人誌のストーリー、感覚で作るといつも途中でどうするか悩むことになるので最初にこういうのきめておくとすごく良いです。プロット作成時点で足りないものがわかるので書いてる途中で悩んで大幅に書き直し、 みたいなことが少なくなります。よき。

参考

ブレイク・スナイダー 『SAVE THE CATの法則 本当に売れる脚本術』 菊池淳子訳、フィルムアート社、2010年、264頁。

『GlobalEventHandlers.oninput - Web API | MDN』
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スムーススクロールとフェードインで動きがおかしくなった時

両方使ったら、飛ぶ位置がおかしくなった

ナビバーのメニュークリックで各コンテンツに飛ぶようにしてたのに、各コンテンツをフェードインさせるアニメーションつけたら、違う位置で止まるようになった。
ezgif-2-2565a44d6d2f.gif
myskillをクリックしているが、一回目では位置がずれてる。

各コンテンツをdivにいれて修正

修正前

                <section id="profile" class="animated">
                <h2>Profile</h2>
                    <div class="prof">
                        <div class="profile-img">
                            <img src="../img/profile.jpg" alt="プロフィール画像">
                        </div>
                        <h3>Name</h3>
                        <p>松江 渚</p>
                        <h3>Birthday</h3>
                        <p>1998/5/14</p>
                        <h3>Summary</h3>
                        <p>
                            2017年 関西大学に入学、2021年卒業予定。<br>
                            2020年2月からTechAcademyでフロントエンドについて学び始める。<br>
                            現在も勉強中!
                        </p>
                        <h3>Hobby</h3>
                        <p>
                            絵を描くことや、ゲームをすること。<br>
                            雑貨集めなどなど。
                        </p>
                    </div>
                    <!--プロフィールの下にあるSNSなどの画像リンク-->
                    <div class="SNS">
                        <p>SNSもやってます。</p>
                        <a href="https://twitter.com/mnagisa2" class="twitter" target="_blank"><i class="fab fa-twitter fa-3x twitter-ico"></i></a>
                    </div>
            </section>

修正後

<div id="profile">
                <section class="animated">
                <h2>Profile</h2>
                    <div class="prof">
                        <div class="profile-img">
                            <img src="../img/profile.jpg" alt="プロフィール画像">
                        </div>
                        <h3>Name</h3>
                        <p>松江 渚</p>
                        <h3>Birthday</h3>
                        <p>1998/5/14</p>
                        <h3>Summary</h3>
                        <p>
                            2017年 関西大学に入学、2021年卒業予定。<br>
                            2020年2月からTechAcademyでフロントエンドについて学び始める。<br>
                            現在も勉強中!
                        </p>
                        <h3>Hobby</h3>
                        <p>
                            絵を描くことや、ゲームをすること。<br>
                            雑貨集めなどなど。
                        </p>
                    </div>
                    <!--プロフィールの下にあるSNSなどの画像リンク-->
                    <div class="SNS">
                        <p>SNSもやってます。</p>
                        <a href="https://twitter.com/mnagisa2" class="twitter" target="_blank"><i class="fab fa-twitter fa-3x twitter-ico"></i></a>
                    </div>
            </section>
            </div>

アンカーポイント位置がフェードインする前は確定していないのでずれていた?
位置を確定させるためにdivにアンカーポイントをつけた。
ezgif-2-00f4e752a312.gif

これで解決!
説明が下手すぎるけど自分用だからいいかな:thinking:

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

core-jsがメンテされていない理由

core-jsとは

core-jsをみなさんご存知だろうか。直接は知らなくてもbabelでpolyfillを当てているなら間接的にお世話になっているはずだ。

https://github.com/zloirock/core-js

メンテされない

そのcore-jsは当分メンテされないらしい。というか2020/01/14を最後にパタッと活動が途絶えている。

なんとこの巨大projectはzloirockというたった一人によってメンテされてきた。

ここで彼のコメントをいくつか引っ張っておこう。

https://github.com/zloirock/core-js/issues/548#issuecomment-494112872
2019年5月21日 4:06 JST
Dear @jpike88!

Almost 5 years almost every day I spend some hour for maintenance core-js. It's not a library from some lines which I can write and forget about it - it should react on any change in JavaScript standard or proposals, on any new JS engine release, on any significant bug in JS engines. core-js has become the de facto standard of JavaScript standard library features polyfill.

I was working on the project in my spare time. No one paid me for it, more other - I didn't use it actively in my work, I worked on it since I thought that it was required for JavaScript community. No one of browser vendors, TC39, big companies which use core-js helped me. Users started actively contribute to this project only some months ago.

Some previous months I worked almost fulltime on core-js@3 and polyfilling-related Babel features instead of making money as I thought it was important for JavaScript community and planned to find a new fulltime work after release.

2 months ago I started raising funds to core-js maintenance. Current result - 7\$ / month on Patreon, 50$ / month on Open Collective. Not seriously, but better than nothing - users use Babel or their frameworks for polyfilling and just don't know that they use core-js indirectly. Not a problem since anyway I didn't think about open source as about a way to earn any serious money.

However, shit happens. Because of one accident, now I have some serious problems for tens or even hundreds of thousands of dollars and a real chance to be in prison - doubtful pleasure - interesting, who will maintain core-js in this case? Since previous months I worked on open source, I have not any financial pillow for solving those problems.

After a little discussion, I understood that I can't count to any help from Babel. Nothing to say.

So why not to make this little experiment? I think that some lines in NPM installation log, which can be hidden if it's required, are an acceptable price for using core-js. I don’t think that I’ll be able to get a however significant part of required at this moment money, however, each dollar makes sense. And some more people will know that I am ready to consider job offers.

And after that, someone says that I'm not right because I added a message on postinstall... The right thing for you is somehow supporting core-js instead of creating issues like this.

Initially, I wanted to add a message on postinstall as an experiment for some days, but because of your reaction I see that adding a message on postinstall was the right thing, so I leave it here. Thank you for this issue.

Let this issue be opened a little more time.

https://github.com/zloirock/core-js/issues/548#issuecomment-495075412
2019年5月23日 14:42 JST

  1. Because at that moment I didn't think that Babel would betray me? I worked on polyfilling stuff for Babel almost from the start of the project, but when I asked about any help and it was really required - I received a refusal.
  2. Seems so, thanks to our stupid law.

If you are in prison, who will maintain it then?

See above - I haven't any options.


https://github.com/zloirock/core-js/issues/747#issuecomment-573318269
2020年1月11日 22:45 JST

BTW since I was not able to find required funds, after some days I'll be in prison and it could be my final goodbye for pieces of shit like you.

つまりどういうことだってばよ?

  • core-jsはbabelのほとんど初期からポリフィルの作成のために貢献してきた
  • ところが多くのユーザーは(babelのことは知っていても)core-jsのことは知らない
  • core-jsはzloirock一人によって5年以上メンテされてきた
  • しかしcore-jsを使用しているTC39のブラウザベンダーは誰も助けてくれなかった
  • babelも助けてくれなかった
  • 2019年3月からメンテナンスのための資金を集め始めたが、一ヶ月あたりPatreonから7ドル、Open Collectiveから50ドル(訳注: 1ドル100円として月額たったの5700円!)
  • まあしかしOSSで稼ごうと思ったわけではないのでそれ自体が問題ではない
  • ところがzloirockがオートバイを時速60km/hで運転中に横断歩道を歩行中の女性をひくという交通事故を引き起こし、被害者は死亡した。
  • さらに控訴審を経て1年半の収監確定した(訳注: いつから?2020/01/14を最後に姿を消したから2021/08くらいまで??)

交通事故そのものはどういうもの?

割愛。自分で
https://kraevoy--alt.sudrf.ru/modules.php?name=sud_delo&srv_num=1&name_op=doc&number=1733512&delo_id=4&new=4&text_number=1
を読んでどうぞ(ロシア語)

core-jsはだれがメンテするの?

Babel projectがメンテすることはあるのだろうか。zloirockの要請には拒否したようだが。

だれかがforkしてメンテするよりほかにないが、一体誰がやるねん。

https://github.com/zloirock/core-js/issues/767

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

ボタンが急に効かなくなった

すみません。
mas masと申します。
初めての質問で緊張します。
登録ボタンを押下したら画面遷移しなくなった。

javascriptでonclickを使用する場合、javascriptは効きますがjavaが効かなくなりました。ctrl + F5で更新?をしたとたんに動作しなくなりました。
javascriptは有効になっています。

環境:
pl : java,javascript
ml : CSS,HTML
db : MySQL
OS : Windows10
FW : Struts

下記がソースコードになります。

jsp.Register.jsp
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://struts.apache.org/tags-html" prefix="html"%>
<%@ taglib uri="http://struts.apache.org/tags-bean" prefix="bean"%>

<html:html>
<head>
<title>Welcome Register</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script type="text/javascript" src="js/alert.js"></script>
</head>
<body>
    <h1>登録画面</h1>
    <html:form action="/Register">
        <%-- 入力項目 --%>
        <p>userid:</p>
        <html:text property="userid" />
        <br>
        <p>password:</p>
        <html:text property="password" />
        <br>
        <br>
        <p>name:</p>
        <html:text property="name" />
        <br>
        <br>
        <p>adress:</p>
        <html:text property="adress" value="" />
        <br>
        <br>
        <p>age:</p>
        <html:text property="age" />
        <br>

        <html:submit property="submit" value="登録" onclick="return clickBtn1();"/>
    </html:form>
    <a href="http://localhost:8022/SiteM/main.jsp">メイン画面へ戻る</a>
</body>
</html:html>
javascript.alert.js
function clickBtn1(){
    /*
     * jsの入力を取得する方法は、「struts-config.xml」.「property」.value
     * */
    var userid = RegistForm.userid.value;
    var password = RegistForm.password.value;
    var name = RegistForm.name.value;
    var adress = RegistForm.adress.value;
    var age = RegistForm.age.value;
    //入力空チェック
    if (userid == "" ){
        alert("userid入力してまへんで");
        return false;
    }else if(password == ""){
        alert("password入力してまへんで");
        return false;
    }else if(name == ""){
        alert("name入力してまへんで");
        return false;
    }else if(adress == ""){
        alert("adress入力してまへんで");
        return false;
    }else if(age == ""){
        alert("age入力してまへんで");
        return false;
    }else if(isNaN(age)){
        alert("数値じゃないで");
        return false
    }
    //メールチェック
    var mail_regex1 = new RegExp( '(?:[-!#-\'*+/-9=?A-Z^-~]+\.?(?:\.[-!#-\'*+/-9=?A-Z^-~]+)*|"(?:[!#-\[\]-~]|\\\\[\x09 -~])*")@[-!#-\'*+/-9=?A-Z^-~]+(?:\.[-!#-\'*+/-9=?A-Z^-~]+)*' );
    var mail_regex2 = new RegExp( '^[^\@]+\@[^\@]+$' );
    if( adress.match( mail_regex1 ) && adress.match( mail_regex2 ) ) {
        return false;
    } else {
        alert("メールアドレスの内容を確認の上\n入力して下さい。");
        return false;
    }
    return true;
}
function clickBtn(){
    /*
     * jsの入力を取得する方法は、「struts-config.xml」.「property」.value
     *
     *
     * */
    var userid = DeleteForm.userid.value;
    var password = DeleteForm.password.value;

    //入力空チェック
    if (userid == "" || password == ""){
        alert("空白やで");
        return false;
    }
    return true;
}

function clickBtn2(){
    /*
     * jsの入力を取得する方法は、「struts-config.xml」.「property」.value
     * */
    var userid = LoginForm.userid.value;
    var password = LoginForm.password.value;

    //入力空チェック
    if (userid == "" || password == ""){
        alert("空白やで");
        return false;
    }
    return true;
}
function clickBtn3(){
    /*
     * jsの入力を取得する方法は、「struts-config.xml」.「property」.value
     *
     *
     * */
    var aduser = AdminForm.aduser.value;
    var adpass = AdminForm.adpass.value;

    //入力空チェック
    if (aduser == "" || adpass == ""){
        alert("空白やで");
        return false;
    }
    return true;
}
function clickBtn4(){
    /*
     * jsの入力を取得する方法は、「struts-config.xml」.「property」.value
     *
     *
     * */
    var password = UpdateForm.password.value;
    var name = UpdateForm.name.value;
    var adress = UpdateForm.adress.value;
    var age = UpdateForm.age.value;
    //入力空チェック
    if(password == ""){
        alert("password入力してまへんで");
        return false;
    }else if(name == ""){
        alert("name入力してまへんで");
        return false;
    }else if(adress == ""){
        alert("adress入力してまへんで");
        return false;
    }else if(age == ""){
        alert("age入力してまへんで");
        return false;
    }
    //メールチェック
    var mail_regex1 = new RegExp( '(?:[-!#-\'*+/-9=?A-Z^-~]+\.?(?:\.[-!#-\'*+/-9=?A-Z^-~]+)*|"(?:[!#-\[\]-~]|\\\\[\x09 -~])*")@[-!#-\'*+/-9=?A-Z^-~]+(?:\.[-!#-\'*+/-9=?A-Z^-~]+)*' );
    var mail_regex2 = new RegExp( '^[^\@]+\@[^\@]+$' );
    if( adress.match( mail_regex1 ) && adress.match( mail_regex2 ) ) {
        // 全角チェック
        if( adress.match( /[^a-zA-Z0-9\!\"\#\$\%\&\'\(\)\=\~\|\-\^\\\@\[\;\:\]\,\.\/\\\<\>\?\_\`\{\+\*\} ]/ ) ) {
            return false;
        }

        // 末尾TLDチェック(〜.co,jpなどの末尾ミスチェック用)
        if( !mail.match( /\.[a-z]+$/ ) ) {
            return false;
        }

    } else {
        alert("メールアドレスの内容を確認の上\n入力して下さい。");
        return false;
    }
    return true;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Node.jsの設計をつらつらと概観する

株式会社Global Mobility ServiceでソフトウェアエンジニアのインターンをさせてもらっているShirubaです。グローバルな環境で利用されている社会的サービスの開発の一端を担いたい志ある方は、ぜひ緩くお話ししましょう〜。バックエンドはNode.jsを使っています。?‍♂️→ 採用ページ


Node.jsについて色々資料を読んでメモをとったりしていたので、一度まとめておきたくて、この記事を書くことにしました。V8やLibuvなど低レイヤ技術の設計をベースにNode.jsを概観していきます。

Node.jsとは

1180px-Node.js_logo.svg.png

Node.js公式によるNode.jsの定義は以下です。

Node.js はスケーラブルなネットワークアプリケーションを構築するために設計された非同期型のイベント駆動の JavaScript 環境です。
https://nodejs.org/ja/about/

Node.jsを理解する上で重要な特徴を定義から抽出すると、以下の3つです。

  • スケーラブル
  • 非同期型
  • イベント駆動

この3つの特徴については後で触れていきます。

Node.jsの内部構造

1*-0Sa0i_g-gcL9sJqvecKEw.png
画像引用:https://blog.insiderattack.net/event-loop-and-the-big-picture-nodejs-event-loop-part-1-1cb67a182810

Node.jsは、いくつかのモジュールを組み合わせて構成されています。Node.jsを理解する上で重要なのは「V8」と「Libuv」です。この2つが、サーバーサイドでのJavascript実行環境を作っています。(クライアントサイドでは、chrome組み込みのv8とhtml5(イベントループ等を提供)でJavascript実行環境が実現されているそう。)

V8

1024px-V8_JavaScript_engine_logo_2.svg.png

どうでもいいですが、V8の読み方は「ヴィーエイト」です。謎に「ブイハチ」って読んでた自分を恥じたい。

V8の定義を公式から引用します。

V8 is Google’s open source high-performance JavaScript and WebAssembly engine, written in C++. It is used in Chrome and in Node.js, among others. It implements ECMAScript and WebAssembly, and runs on Windows 7 or later, macOS 10.12+, and Linux systems that use x64, IA-32, ARM, or MIPS processors. V8 can run standalone, or can be embedded into any C++ application.
https://v8.dev

  • V8っていうのは、Javascript Engineを指します。要するに、Javascriptで書かれているソースコードを受け取って、機械語に変換してOS上で実行してくれるのがV8です。
  • chromeとnode.jsはJavascript EngineとしてV8を採用していますが、それ以外は違います。例えばSafariではV8ではなくJavascriptCoreを採用しています。

ちなみに、EngineだとかRuntimeだとか単語がややこしいのですが、Javascript Engine、 Javascript Runtime、A compiler、Virtual Machineは全てV8を指すと考えて良いそうです。(参考:https://www.youtube.com/watch?v=PsDqH_RKvyc)

また、V8の定義に「ECMAScript」という単語が入っているので定義を引用しておきます。

ECMAScript(エクマスクリプト)は、JavaScriptの標準であり、Ecma Internationalのもとで標準化手続きなどが行われている。
引用:https://ja.wikipedia.org/wiki/ECMAScript

要するに、Javascirptの文法の標準がECMAScriptです。「(Javacsriptで書かれている)ソースコードが何を意味しているのか」を表します。V8が受け取る、Javascriptで書かれているソースコードは極論ただのテキストの塊です。V8は、Javascriptで書かれたソースコードをECMAScriptを用いて解析しています。

V8を理解していなくてもNode.jsのアーキテクチャは理解できるので、V8は後回しにして、この記事の最後で見ていきます。

Libuv

20190111205332.png

Libuvの定義を引用します。

libuv is a multi-platform support library with a focus on asynchronous I/O. It was primarily developed for use by Node.js, but it’s also used by Luvit, Julia, pyuv, and others.
引用:http://docs.libuv.org/en/v1.x/#overview

非同期I/Oは、OSごとに実現方法が異なります。epollを使うOSがあったり、kqueueを使うOSがあったり。(非同期I/Oについては後述。)そこでepollやkqueueなど低レイヤの技術を抽象化したインタフェースを作って、OSを気にすることなく非同期I/Oを使えるようにしようとして作られたのがLibuvです。

Libuvの内部は以下のようにデザインされています。

architecture-2.png
画像引用:http://docs.libuv.org/en/v1.x/design.html#design-overview

ちなみにNode.jsで使われているイベントループを提供してくれているのもLibuvです。

Node.js Bindings

これは、概念的なものです。

v8やlibuvはc++で書かれている一方で、Node.jsを使ってapplicationを作るときに私たちはjavascriptを用います。これがNode.jsの旨みでもあるのですが、私たちはJavascriptで開発しているのに、内部的にはc++で記述されているv8とかlibuvを利用できるのです。

このJavascriptと他のプログラミング言語の橋渡しをしているのがNode.js Bindingsです。

ちなみにNode.js Bindingsは、「Language Bindings」のことを指しています。ということで、「Language Bindings」の定義をwikipediaから引用します。

In computing, a binding is an application programming interface (API) that provides glue code specifically made to allow a programming language to use a foreign library or operating systemservice (one that is not native to that language).
Binding generally refers to a mapping of one thing to another. In the context of software libraries, bindings are wrapper libraries that bridge two programming languages, so that a library written for one language can be used in another language.[1] Many software libraries are written in system programming languages such as C or C++. To use such libraries from another language, usually of higher-level, such as Java, Common Lisp, Scheme, Python, or Lua, a binding to the library must be created in that language, possibly requiring recompiling the language's code, depending on the amount of modification needed.[2] However, most languages offer a foreign function interface, such as Python's and OCaml's ctypes, and Embeddable Common Lisp's cffi and uffi.[3][4][5]
https://en.wikipedia.org/wiki/Language_binding

Node.js Bindingsについて詳しくは触れませんが、Internals of Node- Advance node ✌️が面白かったです。

コアモジュール

Node.jsには組み込みのコアモジュールというものが存在します。コアモジュールは沢山あるので、それぞれの重要度とかはNode.js徹底攻略 ─ ヤフーのノウハウに学ぶ、パフォーマンス劣化やコールバック地獄との戦い方を参考にされたし。

サーバのアーキテクチャ

Node.jsの内部を雑に見渡したところで、Node.jsの設計を見ていきます。
Node.jsで特徴的なのが、採用しているサーバアーキテクチャです。

サーバーのアーキテクチャには、一般的に「Thread Based」と「Event Driven」があります。Node.jsの採用しているサーバアーキテクチャは「Event Driven」、つまり「イベント駆動型」です。(参考:Server Architectures

Thread-based

Thread Basedの場合のサーバの典型的なコードは以下のようになる。

nodes-event-loop-from-the-inside-out-sam-roberts-ibm-5-1024.jpg
[画像引用:https://www.slideshare.net/NodejsFoundation/nodes-event-loop-from-the-inside-out-sam-roberts-ibm]

acceptというシステムコールを通して接続されたコネクションをpthread_createで別のスレッドに渡して、別のスレッドでそのコネクションを処理させます。メインスレッドは、acceptでのブロッキング状態にすぐに戻り、ユーザーからの新しい接続に備えるという流れです。

つまり、ユーザーからのコネクション1つにつきスレッドを1つ作成して、そのスレッドでコネクションに対応しているという訳です。これだとスレッドの無駄使いだし、コンテキストスイッチも発生してしまいます。

このサーバアーキテクチャを図で表すと以下のようになります。

スクリーンショット 2020-03-13 10.08.44.png
[画像引用:Node.jsデザインパターン第2版]

Idle timeも多くなってしまっていることが分かります。このサーバアーキテクチャで出現した問題が「c10k問題」。c10k問題はThe c10k Problemを参考されたし。

wikipediaからc10k問題の定義を引用しときます。

C10K問題(英語: C10K problem)とは、Apache HTTP ServerなどのWebサーバソフトウェアとクライアントの通信において、クライアントが約1万台に達すると、Webサーバーのハードウェア性能に余裕があるにも関わらず、レスポンス性能が大きく下がる問題である。
引用:https://ja.wikipedia.org/wiki/C10K問題

またまた引用します。

preforkモデルのApatchでは、クライアントの接続要求から始まる一連の処理を各プロセスで1接続ずつ処理します。そのため大量の接続を同時に処理するにはその分だけプロセス(またはスレッド)を起動しなければなりません。これでも複数の接続を並行して処理することはできますが、あまり大量のプロセスを起動するとプロセス間コンテキストスイッチのオーバーヘッドが大きくなって性能が劣化します。これがC10K問題の本質です。
引用: nginx実践入門

このc10k問題を解決するのが、非同期I/Oであり、非同期I/Oを用いたサーバアーキテクチャである「Event-Driven」(イベント駆動型)です。

Event-Driven

イベント駆動型のサーバアーキテクチャを理解するためには、まず「非同期I/O」を理解する必要があります。

非同期I/O

Unixには、以下の5種類のI/Oモデルが存在します。

  1. ブロッキングI/O
  2. 非ブロッキングI/O
  3. I/Oの多重化(selectとpoll)
  4. シグナル駆動I/O(SIGIO)
  5. 非同期I/O(Posix.1のaio_関数群)

Node.jsで使われているのは「非同期I/O」です。

スクリーンショット 2020-02-16 9.51.18.png
画像引用:Unix Network Programming

処理をカーネルに任せ、処理が完了したらカーネルが元のスレッドに通知をよこすというI/Oモデルです。ちなみによく聞く「ノンブロッキングI/O」は以下のようなI/Oモデルです。

スクリーンショット 2020-02-16 9.50.52.png
画像引用:Unix Network Programming

図から分かるように、アプリケーション側からカーネルに「データの準備が完了したか」を尋ねる作業をループで繰り返す必要があり、リソースが勿体無いので、イベント駆動型では非同期I/Oモデルが採用されています。

この非同期I/Oモデルを用いることで実現されるのが「イベントループ」です。通知を発生させるイベントを常にループ文で監視していることから「イベントループ」です。また、このおかげでユーザーからのコネクションをシングルスレッドで処理することが可能になります。

スクリーンショット 2020-03-17 21.49.45.png
画像引用:Node.jsデザインパターン第2版

リアクタパターン

このイベントループを用いたイベント駆動型モデルは、リアクタパターンと呼ばれます。(非同期I/Oを用いたイベント駆動型モデルなので、プロアクタパターンと呼ぶのだろうか。「Node.jsデザインパターン第2版」に沿って、ここではリアクタパターンと呼ぶことにします。)

リアクタパターンの定義は以下。

リアクタパターンではI/Oの処理はいったんブロックされる。監視対象のリソース(群)で新しいイベントが発生することでブロックが解消され、この時、イベントに結びつけられたハンドラ(コールバック関数)に制御を渡すことで呼び出し側に反応(react)する。
引用:Node.jsデザインパターン第2版

Node.jsでは、非同期処理を使う場合、イベントにコールバックを持たせて、イベントが終了したものからコールバックを実行しています。ちなみに、Javascriptの関数は第1級オブジェクトなので、関数にコールバック関数を持たせるのが非常に容易です。

リアクタパターンを図で表すと以下のようになる。

スクリーンショット 2020-03-13 10.33.21.png
画像引用:Node.jsデザインパターン第2版

Node.jsでは、ここで説明した「イベント駆動型」モデルが採用されています。ただ、注意したいのは、Node.jsで用いられているイベントループのデザインはこれとは少し異なるということです。

まずNode.jsでは、非同期I/Oを使っている処理もありますが、内部的にスレッドプールを使っている処理もあります。そして2つにNode.jsではイベントキューが複数存在するということです。全てのイベントのハンドラが同一のイベントキューに入れられていくのではなく、イベントの種類に応じて積まれていくイベントキューが異なります。

Libuvが提供する非同期処理のアーキテクチャ

Node.jsで用いられる「イベントループ」を提供しているのがLibuvです。ここではLibuvが提供する以下の概念について見ていきます。

  • Event Loop
  • Handles
  • Requests
  • Thread Pool

イベントループ

イベントループの定義を公式から引用します。

The event loop is what allows Node.js to perform non-blocking I/O operations — despite the fact that JavaScript is single-threaded — by offloading operations to the system kernel whenever possible.
Since most modern kernels are multi-threaded, they can handle multiple operations executing in the background. When one of these operations completes, the kernel tells Node.js so that the appropriate callback may be added to the poll queue to eventually be executed.
引用:The Node.js Event Loop, Timers, and process.nextTick

先ほど紹介したように、「非同期I/O」を可能にするのが「イベントループ」です。ちなみに、イベントループはNode.jsのメインスレッドで、ひたすらクルクル回っています。(ループ文)

メインスレッドを止めてしまうようなタスク(I/Oに関するタスクなど)を入れてしまうと、その処理に時間を食ってしまい、そこでイベントループが止まってしまい、他の処理ができなくなります。そのため、そういった処理に関しては、カーネル内のマルチスレッドを使った非同期I/Oモデルに処理を依頼する訳です。そして依頼したI/O処理が完了したら、登録しておいたハンドラ(コールバック関数)を実行する訳ですが、このハンドラはqueueに入って、メインスレッド(イベントループが回っているスレッド)で順次実行されていきます。この挙動によって、Node.jsの非同期I/Oでは、「競合状態」を気にせずに開発することができます。

Node.jsのイベントループは、いくつかのフェーズから構成されています。このフェーズごとの挙動は、ここでは省略させてもらいます。イベントループに関する分かりやすかった図を載せておきます。

スクリーンショット 2020-03-11 21.49.08.png
[画像引用:https://drive.google.com/file/d/0B1ENiZwmJ_J2a09DUmZROV9oSGc/view]

この図内の「黄色いJSの箱」の部分を詳細に見ると以下のようなループになっています。

スクリーンショット 2020-03-11 21.47.21.png
[画像引用:https://drive.google.com/file/d/0B1ENiZwmJ_J2a09DUmZROV9oSGc/view]

Node.jsのサーバを開始する際にも、イベントループが利用されています。公式ドキュメントの、Node.jsを使ったサーバーを作るためのコードを引用します。

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

server.listenで内部的にepollなど非同期I/Oが用いられています。ハンドラは、arrow関数の部分ですね。tcp connectionをacceptした時のコールバックとしてアプリケーションが非同期に実行されるようにコードが書かれています。

HandleとRequest

イベントループ内で処理されるタスクはHandleオブジェクトとRequestオブジェクトの2種類存在します。

Handleは長期間存在することができるオブジェクトで、I/Oが発生していない時でもイベントループを維持します。Requestは短期間存在するオブジェクトで、I/Oが発生している時のみイベントループを維持します。

イベントループは、アクティブなHandlesもしくはRequestsがなければ止まります。

スレッドプール

Node.jsはイベント駆動型のサーバアーキテクチャを採用していることからも、よく「シングルスレッド」だと表現されます。しかしここで注意しておきたいのですが、Node.jsは処理によって、内部的にスレッドプールを使った並行処理を行なっています。

ここから動画「The Node.js Event Loop: Not So Single Threaded」から画像を大量拝借しています。(すごく分かりやすかった。)

例えばCPU intensiveな処理であるcryptモジュールを使ったコードを見てみます。

the-nodejs-event-loop-not-so-single-threaded-15-638.jpg
[画像引用:https://www.slideshare.net/nebrius/the-nodejs-event-loop-not-so-single-threaded]

ここでcrypt.pdkdf2は非同期に実行されています。(引数の最後に、非同期処理特有のcallbackであるarrow関数が見られます。これが無ければcrypt.pdkdf2は同期処理で実行されます。)for文でループさせてcrypt.pdkdf2を2回使用していることに注意して下さい。これをマルチコアで実行すると、実行にかかる時間は以下のようになります。

the-nodejs-event-loop-not-so-single-threaded-16-638.jpg
[画像引用:https://www.slideshare.net/nebrius/the-nodejs-event-loop-not-so-single-threaded]

同じくマルチコア環境で、今度は繰り返し回数を4回にしてみると以下のようになります。

the-nodejs-event-loop-not-so-single-threaded-17-638.jpg
[画像引用:https://www.slideshare.net/nebrius/the-nodejs-event-loop-not-so-single-threaded]

マルチコアで実行しているため前の場合と比べて2倍の時間がかかってしまっていることに注意です。また、preemptiveなマルチタスクとして処理されている(それぞれのタスクを割り当てられたtime sliceごとに実行していく)ので、4回全て同じくらいの処理時間で終了しています。

今度は繰り返し回数を6回にすると実行時間は以下のようになります。

the-nodejs-event-loop-not-so-single-threaded-18-638.jpg
[画像引用:https://www.slideshare.net/nebrius/the-nodejs-event-loop-not-so-single-threaded]

なぜこのようになるかというと、Node.jsが内部的に4つのthread poolをデフォルトで持っているからです。4つのタスクは4つのスレッドを用いてマルチタスクで処理され同時に終わっていますが、後の2つはスレッドが空いてから実行されます。(このthread poolはLibuvによって提供されているもので、環境変数UV_THREADPOOL_SIZEをいじることでthread poolの個数を変えることができます。)

これでNode.jsでは内部的にスレッドプールが用いられていることが分かりました。一方で、先に紹介した通り非同期I/Oも用いられています。非同期I/Oを示すためにhttpsモジュールを使った例も動画で紹介されていたので、見ていきます。

the-nodejs-event-loop-not-so-single-threaded-22-638.jpg
[画像引用:https://www.slideshare.net/nebrius/the-nodejs-event-loop-not-so-single-threaded]

(上のスライドではfor文を2回繰り返していますが、)for文を6回繰り返した場合の実行時間は以下です。

the-nodejs-event-loop-not-so-single-threaded-25-638.jpg
[画像引用:https://www.slideshare.net/nebrius/the-nodejs-event-loop-not-so-single-threaded]

httpモジュールは、thread poolを使わず、OSに依頼してepollなどを使っているため、6 Requestsの場合でもほぼ同時にタスクが終了しています。

では、どの処理がthread poolを使って、どの処理がepollやkqueueなど非同期I/Oを使うのかっていう話になりますが、以下の画像を参照してください。

the-nodejs-event-loop-not-so-single-threaded-29-638.jpg
[画像引用:https://www.slideshare.net/nebrius/the-nodejs-event-loop-not-so-single-threaded]

基本的にはthread poolではなく、OSが提供する非同期I/Oが使われます。じゃあ何故全てOSの非同期I/Oを使わずにthread poolを使う必要があるのか。それは設計上の難しさがあったからだそうです。詳しくはasynchronous disk I/Oを参考にしてください。

繰り返しますが、このthread poolはLibuvが提供しています。Libuvのデザインの画像をよく見ると右にthread poolと載っています。

architecture-2.png

V8

Libuvを一通り見たところで、次はV8を見ていきます。V8は、Javascirptで書かれているソースコードを受け取って、それを解析しcompileして実行します。

V8の機能群

1*QG6GNe2ag-4puxpjc5Y2iw.png
[画像引用:JavaScript V8 Engine Explained]

  • コードを実行する環境として、call stackとheapという概念があります。Javascriptはシングルスレッドで実行される言語であり、call stackは1つだけです。また、オブジェクトをheapに割り当てたりするわけですが、オブジェクトを使い終わったのに割り当てたメモリを解放しないとメモリリークが起きちゃいます。そこでOrinocoというGarbage Collectorの出番となります。
  • V8はJavascriptで書かれたソースコードを受け取って、それを機械語にする必要があります。その際に使われるのがIgnitionというインタプリタとTurboFanという最適化コンパイラです。

(Liftoffについてはこの記事では触れていません。)

V8の処理の流れ

1*ZIH_wjqDfZn6NRKsDi9mvA.png
[画像引用:Understanding V8’s Bytecode]

  • ソースコードをV8に渡す
  • Perserを使ってソースコードを解析。そしてASTという抽象構文木を作る。
  • IgnitionというInterpreterを使ってASTをBytecodeに変換する。(変換されたBytecodeは同じくIgnitionによって実行される。)
  • IgnitionはASTをBytecodeに変換しつつ、その時の変換情報を蓄えている。(Profiilng)
  • 特定の条件下でコードを最適化するために、Ignitionは、蓄えた情報とBytecodeをTurboFanに渡す。
  • TurboFanは、そのコードを最適化された機械語に変換して実行

IgnitionとTurboFanの部分が少しややこしいので、別の画像でも確認しておきましょう。

スクリーンショット 2019-12-12 16.44.07.png
[画像引用:Parsing JavaScript - better lazy than eager? ]

ASTがBytecode generatorによってBytecodeになり、それがIgnitionによって実行されます。(ASTをBytecodeに変換するBytecode generatorと、Bytecodeを実行するBytecode Handlerを合わせてIgnitionと総称しているぽいです。)
TurboFanは、Bytecodeを受け取り、機械語を生成し、それをそのまま実行しています。

ScannerとParserとAST

Scanner

スクリーンショット 2020-03-16 9.55.54.png
[画像引用:Blazingly fast parsing, part 1: optimizing the scanner]

Scannerは、V8に渡されたJavascriptソースコードを字句解析(tokenizer/ lexical analysis )して、tokenに分解します。tokenの定義は以下。

Tokens are blocks of one or more characters that have a single semantic meaning: a string, an identifier, an operator like ++.
引用:https://v8.dev/blog/scanner

パフォーマンス向上のためにScannerで使われている仕組みなど、詳細はBlazingly fast parsing, part 1: optimizing the scannerを参照して下さい。

Parser

字句解析を終えてparserに流れてきたtokenを、ECMAScriptで決められている構文に沿ってabstract syntax tree(AST)にします。この作業をparseといいます。

AST(Abstract Syntax Tree)っていうのは、抽象構文木のことで、プログラムの構造を示すオブジェクトです。

このASTは、Engineのみでなく、transpilerや静的解析にも使われるものです。V8では、このASTを基にBytecodeがIginitionによって作成されます。

ASTの例を1つ見ておきます。(参考:How JavaScript works: Parsing, Abstract Syntax Trees (ASTs) + 5 tips on how to minimize parse time)

以下のJavascriptコードをASTにします。

function foo(x) {
  if (x > 10) {
    var a = 2;
    return a * x;
  }
return x + 10;
}

ASTは以下。

0*mSOIiWpkctkD0Gfg..png
[画像引用:
How JavaScript works: Parsing, Abstract Syntax Trees (ASTs) + 5 tips on how to minimize parse time]

ちなみに以下のサイトで、JavascriptコードをASTに変換して見ることができます。

https://astexplorer.net

ASTに変換するこのParse作業っていうのは結構時間を食うものらしくて、最適化が重要になってきます。以下の画像は、どれだけParseに時間がかかっているかを示す画像。

スクリーンショット 2020-03-10 17.18.37.png
[画像引用:https://docs.google.com/presentation/d/1b-ALt6W01nIxutFVFmXMOyd_6ou_6qqP6S0Prmb1iDs/present?%20slide=id.p&slide=id.g2220ee5730_1_7]

そこでParseを最適化するために「Preparser」と「Parser」の2つのParserがV8では使われています。

スクリーンショット 2019-12-13 0.56.17.png
[画像引用:https://docs.google.com/presentation/d/1b-ALt6W01nIxutFVFmXMOyd_6ou_6qqP6S0Prmb1iDs/present?%20slide=id.p&slide=id.g1d5daf2403_0_153]

関数を除く、トップレベルに記述されているコードは、全て実行されるのでparse作業をしてASTに変換します。一方で関数にはトップレベルに書かれていたとしても結局呼ばれない関数も存在します。その結局実行されない関数に対して、フルでparse作業をすると、parseにかかる時間もメモリも無駄なのです。そこで「Preparse」の出番です。

Preparseでは、ASTを作ったりせず、とりあえず最低限必要な情報だけ作っておき、あとで関数が実際に呼び出されたらフルでParseします。

ちなみに、以下のようなJavascriptソースコードは、Parseという観点では非効率なコードです。

function sayHi(name){
  var message = "Hi " + name + "!"
  print(message)
}

sayHi("Sparkle")

このようなコードでは、関数をPreparseした後、その関数をすぐ呼び出すことになるのでフルでParseされます。つまりすぐにParseするのに一度Preparseさせてしまっているのです。詳しくは、How JavaScript works: Parsing, Abstract Syntax Trees (ASTs) + 5 tips on how to minimize parse timeを参照して下さい。

また、Preparserについて詳しく知りたい方は、公式のBlazingly fast parsing, part 2: lazy parsingを参照してください。

IgnitionとTurbofan

CranksahftとFull-codegen

V8ではInterpreterとCompilerとしてCrankshaftとFull-codegenが使われてきたけど、それらがIgnitionとTurbofanに変わりました。

20170803092506.jpg
[画像引用:Node8.3.0でデフォルトになるTF/Iに関わるベンチマークについて]

Crankshaftはtryスコープ、ES2015の最適化(e.g. scope, class literal, for of, etc…)などができなかったことや、低レイヤと高レイヤとの分離がうまくできておらずV8チームがアーキテクチャ依存のコードを大量生成しなければいけなかったことなどから、TurboFanに代わったそうです。

またFull-codegenは機械語にコンパイルするため、メモリを食うこと、またIgnitionが生成するBytecodeが最適化に利用できてそのスピードがFull-codegenよりも早かったことからIgnitionに代わったそうです。

処理の流れ

  • まずASTがIgnitionに渡され、そしてBytecode(機械語を抽象化したコード)が生成され、実行が開始されます。最適化はされていないにしてもIgnitionが素早くBytecodeを生成するため、アプリケーションは素早く動作し始めることができます。
  • Profilerがコードを監視していて、何度も繰り返しコンパイルされている部分、つまり最適化できると推測される部分を見つけます。ここではinline cachesという最適化手法が利用されています。
  • 最適化できる場合は、Ignitionもその情報を活用し最適化しますが、Turbofanも活用します。TurboFanにBytecodeが渡され、speculative optimizationという最適化手法を用いて、最適化された機械語が生成されます。最適化できるという推測が間違っていた場合は、deoptimizeされます。
  • profilerとcompilerのおかげで、徐々にJavascriptの実行が改善されていきます。

interpreter-optimizing-compiler-20180614.png
[画像引用:JavaScript engine fundamentals: Shapes and Inline Caches]

Bytecodeっていうのは、機械語を抽象化したものです。

1*aal_1sevnb-4UaX8AvUQCg.png
[画像引用:Understanding V8’s Bytecode]

BytecodeについてV8の人の説明を引用しておきます。

Bytecode is an abstraction of machine code. Compiling bytecode to machine code is easier if the bytecode was designed with the same computational model as the physical CPU. This is why interpreters are often register or stack machines. Ignition is a register machine with an accumulator register.
引用:Understanding V8’s Bytecode

まとめると、Ignitionは、Bytecode(抽象化された機械語)を素早く生成できるけど、最適化はされていません。Bytecodeは、メモリをあまり食わないという特徴もあります。一方でTurboFanは、最適化に少し時間はかかるけど、最適化された機械語を生成できます。生成されるのが機械語なので、抽象化された機械語であるBytecodeよりもメモリを多く使います。

またIgnitionとTurbofanには様々な最適化テクが使われていますが、ここでは省略します。

  • Speculative Optimization
  • Hidden Classes
  • inline caches

Call stack と Heap

Call stackの定義をwikipediaから引用します。

コールスタック (Call Stack)は、プログラムで実行中のサブルーチンに関する情報を格納するスタックである。実行中のサブルーチンとは、呼び出されたが処理を完了していないサブルーチンを意味する。
引用:https://ja.wikipedia.org/wiki/コールスタック

Call Stackは、コードが実行されていくにつれ、Stack Frameが積み重なっていきます。Call Stackを見ることで、プログラムが今どこにいるのか分かります。一方でHeapは、オブジェクトなどStack Frameのスコープを超えて保持すべきデータに対してメモリが割り当てられる場所です。

img2.jpeg
[画像引用:Confused about Stack and Heap?]

Javascriptをブラウザで実行する際に、以下のようなエラー画面を見たことがあると思います。

1*T-W_ihvl-9rG4dn18kP3Qw.png
(画像引用:How JavaScript works: an overview of the engine, the runtime, and the call stack

これはCall Stackを表していて、この場合だと以下のようなコードが順次Stack Frameとして積み重なっていったということになります。

function foo() {
    throw new Error('SessionStack will help you resolve crashes :)');
}
function bar() {
    foo();
}
function start() {
    bar();
}
start();

最後に

4月からNode.jsを離れることになるので、ここまで読んだ記事とか知ったこととかをまとめてみました。間違っている部分とかあれば、指摘くださるとありがたいです。

参考

Node.js(Libuv含む)

Event Loop and the Big Picture — NodeJS Event Loop Part1 https://blog.insiderattack.net/event-loop-and-the-big-picture-nodejs-event-loop-part-1-1cb67a182810?

Don't Block the Event Loop (or the Worker Pool)
https://nodejs.org/ja/docs/guides/dont-block-the-event-loop/

Node.js徹底攻略 ─ ヤフーのノウハウに学ぶ、パフォーマンス劣化やコールバック地獄との戦い方
https://employment.en-japan.com/engineerhub/entry/2019/08/08/103000

The Node.js Event Loop, Timers, and process.nextTick()
https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/#event-loop-explained

そうだったのか! よくわかる process.nextTick() node.jsのイベントループを理解する
https://www.slideshare.net/shigeki_ohtsu/processnext-tick-nodejs

asynchronous disk I/O
https://blog.libtorrent.org/2012/10/asynchronous-disk-io/

http://nikhilm.github.io/uvbook/

Node.jsでのイベントループの仕組みとタイマーについて
https://blog.hiroppy.me/entry/nodejs-event-loop#Poll-Phase

Node.js event loop architecture
https://medium.com/preezma/node-js-event-loop-architecture-go-deeper-node-core-c96b4cec7aa4

今日から始めるNode.jsコードリーディング - libuv / V8 JavaScriptエンジン / Node.jsによるスクリプトの実行
https://blog.otakumode.com/2014/08/14/nodejs-code-reading-startup-script/

Nonblocking I/O
https://medium.com/@copyconstruct/nonblocking-i-o-99948ad7c957

process.nextTick()
https://www.slideshare.net/shigeki_ohtsu/processnext-tick-nodejs

Node.jsでのイベントループの仕組みとタイマーについて
https://blog.hiroppy.me/entry/nodejs-event-loop

ループの中で
https://www.youtube.com/watch?v=cCOL7MC4Pl0&t=1011s

libuv
https://www.youtube.com/watch?v=nGn60vDSxQ4

Node's Event Loop From the Inside Out by Sam Roberts, IBM
https://www.youtube.com/watch?v=P9csgxBgaZ8

イベントループとは一体何ですか? | Philip Roberts | JSConf EU
https://www.youtube.com/watch?v=8aGhZQkoFbQ

V8

JavaScript V8 Engine Explained
https://hackernoon.com/javascript-v8-engine-explained-3f940148d4ef

Understanding V8’s Bytecode
https://medium.com/dailyjs/understanding-v8s-bytecode-317d46c94775

Explaining JavaScript VMs in JavaScript - Inline Caches
https://mrale.ph/blog/2012/06/03/explaining-js-vms-in-js-inline-caches.html

An Introduction to Speculative Optimization in V8
https://benediktmeurer.de/2017/12/13/an-introduction-to-speculative-optimization-in-v8/

How JavaScript works: an overview of the engine, the runtime, and the call stack
https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf

How JavaScript works: Parsing, Abstract Syntax Trees (ASTs) + 5 tips on how to minimize parse time
https://blog.sessionstack.com/how-javascript-works-parsing-abstract-syntax-trees-asts-5-tips-on-how-to-minimize-parse-time-abfcf7e8a0c8

Confused about Stack and Heap?
https://fhinkel.rocks/2017/10/30/Confused-about-Stack-and-Heap/

JavaScript Internals: Under The Hood of a Browser
https://medium.com/better-programming/javascript-internals-under-the-hood-of-a-browser-f357378cc922

JavaScript Internals: Execution Context
https://medium.com/better-programming/javascript-internals-execution-context-bdeee6986b3b

How Does JavaScript Really Work? (Part 1)
https://blog.bitsrc.io/how-does-javascript-really-work-part-1-7681dd54a36d

How JavaScript works: Optimizing the V8 compiler for efficiency
https://blog.logrocket.com/how-javascript-works-optimizing-the-v8-compiler-for-efficiency/

V8のIgnition Interpreterについて
https://speakerdeck.com/brn/v8falseignition-interpreternituite?slide=14

V8 javascript engine for フロントエンドデベロッパー
https://www.slideshare.net/ssuser6f246f/v8-javascript-engine-for

JavaScript engines - how do they even? | JSConf EU
https://www.youtube.com/watch?v=p-iiEDtpy6I&feature=youtu.be&t=722

V8: an open source JavaScript engine
https://www.youtube.com/watch?v=hWhMKalEicY&feature=emb_title

Marja Hölttä: Parsing JavaScript - better lazy than eager? | JSConf EU 2017
https://www.youtube.com/watch?v=Fg7niTmNNLg

V8公式
https://v8.dev/blog

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

なんとなく、簡単に入門できそうなReact

タイトルが強弁すぎますね、ごめんなさい?

すすさんの記事のリリースツイートを見て「おっこれはワイもReact記事書くムーブかな???」となったので書いてみます。

本項のゴール

  • Reactのコンポーネントの作り方を理解する
  • propsを渡して動的にUIを作ってみる(Stateは気力があれば別記事で書こうと思います)

諸注意

間違ってる箇所とかバッドプラクティスがあるかもしれません
コメントで指摘していただければと思います。

そもそもReactってなに?

フロントのUIライブラリの話になるとVueと比較される事が多いので高機能なのを想像しがちですが
React自体、及びReact-DOMは HTMLのレンダリングをするだけのライブラリです。

そもそもReact自体はHTML周りの機能すら持ってません。その辺りは色々あってReact-DOMに分割されました。

この話についてはGoogleで検索するといくらでも出てくるので本稿では省略します。

HTMLをそのまま書いていくのとどう違う?

HTMLを直書きして document.querySelector('div#hoge').~~~ のようにJSでターゲットを指定して操作するの(いわゆるDOM操作)も悪くはないですが、UI部品が増えると似たようなコードが増え、非常にメンテナンスのしづらい煩雑なコードになっていきます。

またUIを小分けにせずHTMLに全てベタ書きすると
どこからがNavの部分でどこからがメインビューなのかわかりづらくなります。

これを解決するのがReactです。

Reactだと何が良いのか

Reactを使うことによって何がどう変わるのか、という話ですが結論的には以下のようになります。

  • JSの中にHTMLを混ぜたような文が書ける(ビューに出す要素とJSで操作する部分を纏めて書ける)
  • UIパーツをコンポーネントという単位で小分けしながら作れる

あんまり話が長くなっても面白くないので実際に作りましょう。

実際に作る-環境を建てる

注意事項です。

nodeとnpmはインストール済みである前提で解説します。

まだされていない方はnodenv等でインストールをしておいてください。
nodeのバージョンは偶数系が無難です。(Firebaseに興味があるとかでなければv10以降がおすすめ)

ES6の構文については先に理解をしておいてください。

主に const let ()=> import, exportが多用されます。これらが何なのか知らない方はMDNドキュメントを先に参照していただくか、Googleで検索していただくことをおすすめします。


$ npx create-react-app sample

このコマンドを叩くと ./sample/ の中にReactのアプリの開発に必要なものがすべてインストールされます。あるオプションを入れるとTypeScriptを使うようにすることもできますが今回は省略します。

$ cd sample/
$ npm start 

package.json のあるディレクトリで npm start を叩くことで開発サーバが立ちあがり、自動的にブラウザにlocalhostのビューが出てきます。こんなの。
Screen Shot 2020-03-16 at 23.52.08.png

とりあえずこれで問題なく動くことは確認できました。
このテンプレートをそのまま加工してもいいですが、とりあえず今回はネイティブな状態に戻すため src/にあるものを全て消して、index.jsを新規作成してください。
おそらくエラーが出ますが一旦無視で構いません。

実際に作る-HTMLをレンダリングしてみる

先程作ったindex.jsに追記してみてください。

index.js
import React from 'react';
import ReactDOM from 'react-dom';

const App = () => {
  return (
    <div>
      <h1>Hello Mr.React!</h1>
    </div>
  );
};

ReactDOM.render(<App />, document.querySelector('div#root'));

順を追って説明します。
まず2行目まででReact、そしてReact-DOMを呼び出して使えるようにします。
ちなみに2行目のReact-DOMは使わない限りはimportしなくて大丈夫です。

const App = () => {
  return (
    <div>
      <h1>Hello Mr.React!</h1>
    </div>
  );
};

Reactではコンポーネントを作る際に 大文字から始まる関数オブジェクト を用いてオブジェクトの体型を表現します。
大文字からという一文は絶対守ってください。小文字だと蹴られます。

また、Reactアプリケーションの開発においてはよくHTML(XML)をそのまま書けるようにJSXという特殊なフォーマットを使います。
これはbabelという変換器を使ってブラウザも理解できるJSの構文に変換をしています。
(一般的にJSXを使う場合は拡張子を .jsx にして明示します。index.jsは例外)

そしてこのJSXにも制限があり、復数の要素をルートに横並びさせることができません
必ず1つの要素の下に居る必要があります。回避する方法もありますがこちらは後述しますので一旦忘れてください。

このAppというコンポーネントは、単純に <div> の中に<h1>~~~</h1>を包んだものを返しているだけです。
変化したりとかもさせていません。

ちなみにクラスの構文でも表現できますが個人的には関数オブジェクトでの宣言がおすすめです。(理由とかはまた機会があれば)

ReactDOM.render(<App />, document.querySelector('div#root'));

そして、そのAppのコンポーネントをReactDOM.renderに渡すことで指定したターゲット( public/index.html にある)にレンダリングされる、という流れになっています。

実際に追記されるとこのように出ます。

Screen Shot 2020-03-17 at 0.51.41.png

これだけしか出ませんが、正常です。次に進みましょう。


ここまでで覚えておくこと

  • コンポーネントは関数もしくはクラスで宣言して作る
  • コンポーネント命名は頭文字が大文字である必要あり(小文字だとHTMLと解釈される)
  • コンポーネントを使うときは <ComponentName /> のようにタグっぽい文法で書く
  • HTML(XML)を記述して return することでレンダリングされる

実際に作る-表を作ってみる(+コンポーネントを分ける)

多分ネイティブなReactを触る上で一番恩恵デカイのが表の生成だと思うのでそれで試してみようと思います。
まずは表を表現するためのコンポーネントを新しいファイルに分けて生成します。
今回はtable.jsxというファイル名で作ります。

table.jsx
const Head = () => {
  // theadの構成を作る
};

const Body = () => {
  // tbodyの構成を作る
};

export default function Table(){
  return(
    <table>
      <Head />
      <Body />
    </table>
  )
};

index.jsでコンポーネントを受け取れないと困るので、テーブル本体を返すコンポーネントのみexport修飾子で渡せるようにしておきます。 defaultは気にすんな

そしてindex.jsで受け取りをして、レンダリングをさせます。

index.js
import React from 'react';
import ReactDOM from 'react-dom';
// `table.jsx` からTableをimportする
import Table from './table';

const App = () => {
  return (
    <div>
      <h1>Hello Mr.React!</h1>
      <Table />
    </div>
  );
};

ReactDOM.render(<App />, document.querySelector('div#root'));

受け取り側のコードが書けたので、表の実装に入ります。
しかし、Tableコンポーネントの中に全ての構成を突っ込むとコードがかなり長くなってしまうので、Reactの得意分野を活かしてHeadBodyの2つのコンポーネントに分けてそれぞれ別々にレンダリングしようと思います。

Headを作る

それでは Head から作っていこう、と言いたいところですがストップです。
Tableコンポーネントのところでヘッダー行の名前や数量を配列で表現することで動的に変更できる、といった構造に出来ればかなり改変がしやすくなるような気がします。長年の勘がそう言っています。

そのためには、先程作ったTableコンポーネントに少しだけ変更を加えます。

export default function Table(){
  // 配列を入れた変数を追加
  const headList = ['ID', 'Name', 'isOtaku'];

  // `<Head>` の横に`list={headList}` と追記
  return(
    <table>
      <Head list={headList}/>
      <Body />
    </table>
  )
};

追記した <Head list={}>ですが、これはpropsと呼ばれており、コンポーネントに対してデータを渡すことができる仕組みです。
今回の場合listというpropsの中にheadListという配列を渡しています。
これを Head のコンポーネントの中で取得する際は以下のコードのようになります。

// 引数に `props` が入ってくる必要がある
const Head = (props) => {
  // `props.~~~` でpropsの中身を参照できる
  const array = props.headList;
};

試しにconsole.log(array);と書いて保存してみてください。
Screen Shot 2020-03-17 at 23.49.39.png

こんな感じでコンポーネント側に配列が渡されていることが確認できます。
あとはArray.mapで回しながら<th></th>の中に内包してやれば head は完成です。
Head の完成形としてはこんな感じになりました。

const Head = (props) => {
  // `props.~~~` でpropsの中身を参照できる
  console.log(props.list);

  return (
    <thead>
      <tr>
        { props.list.map((item) => {
          return (
            <th>
              { item }
            </th>
          );
        }) }
      </tr>
    </thead>
  );
};

少し難解ですが()の中の{}で括った部分はJSのコードとして評価されるので、これを利用してmapで回しながら<th>を生成し、その中に配列の要素を挿入することでヘッダー行が出来上がりです。

Bodyを作る

続いてBodyコンポーネントも作っていきます。
こちらも同様に変数を渡すことで動的に生成させようと思います。そのためには配列の中にオブジェクトを入れるのが最適かなあという感じなので、Tableの中身をこうしてみます。

export default function Table(props){
  const headList = ['ID', 'Name', 'isOtaku'];

// tbodyを構成するためのオブジェクトの集まりを作る
  const bodyElements = [
    {
      id: 1,
      name: 'Sister Cleaire',
      isOtaku: false
    },
    {
      id: 2,
      name: 'Yashiro Kidsuku',
      isOtaku: true
    },
    {
      id: 3,
      name: 'Otogibara Era',
      isOtaku: true
    }
  ];
  return(
    <table>
      <Head list={headList}/>
      <Body list={bodyElements} />
    </table>
  )
};

ここでやっていることも同じで、オブジェクトの入った配列をprops経由で渡しているだけです。
あとはBodyコンポーネント内でループなどの実装をすればOKです。

const Body = (props) => {
  // `<tr>`を纏めて生成するための
  // ローカル内のコンポーネント
  const Tds = (props) => {
    return(
      <>
        <td>
          {props.object.id}
        </td>

        <td>
          {props.object.name}
        </td>

        <td>
          {props.object.isOtaku.toString()}
        </td>
     </>

    );
  };

  return(
    <tbody>
      { props.list.map((item) => {
        return(
          <tr>
            <Tds object={item} />
          </tr>
        );
      }) }
    </tbody>
  )
};

少しややこしいですが落ち着いて、まずはreturn内部から入りましょう。

return(
    <tbody>
      { props.list.map((item) => {
        return(
          <tr>
            <Tds object={item} />
          </tr>
        );
      }) }
    </tbody>
  )

<tbody>の中で配列をmapを使ってループさせ、<Tds>をレンダリングしています。
このときに配列の中に入っていたオブジェクトが一つずつprops.objectとして入ります。
このことを抑えてTdsを見てみましょう。

const Tds = (props) => {
  return(
    <>
      <td>
        {props.object.id}
      </td>

      <td>
        {props.object.name}
      </td>

      <td>
        {props.object.isOtaku.toString()}
      </td>
     </>
  );
};

Tdsは複数の<td>タグを返すことから命名しています。
復数の <td> タグの中で props.object で渡されたオブジェクトの値を参照しています。
また、isOtakuだけ真理値型なので文字列に明示的に変換しています。

ここで「おい待てや」ってなった方は優秀です。最初のあたりで説明したことを思い出してください。

そしてこのJSXにも制限があり、復数の要素をルートに横並びさせることができません

そう、横並びした<td>は通常だと返せません。そう。通常は
ここで登場するのが <> </> というカオナシのようなやつです。
これはReact.Fragmentと呼ばれるコンポーネントの糖衣構文で、これに包むとレンダリングの際に横並びした状態でレンダリングしてくれます。
今回はこれを利用することで横並びする要素をそのまま分離しています。

これでレンダリングするものが完成です。
保存していただいて、表がきちんと出来ていればOKです。
また、配列を減らしたり増やしたりして行が増減するのを確認してみてください。

Screen Shot 2020-03-18 at 0.48.43.png


ここまでで覚えておくこと

  • コンポーネントを小分けにしつつ、propsで値をバケツリレーして作ると煩雑化が防げる
  • propsを使う際はコンポーネントの引数を入れることを忘れないようにする(必要なければ入れなくてOK)

おわりに

今回はReactのさわりのさわりだけ解説してみましたがいかがでしょうか。
Reactは自分でコンポーネントを作るだけでなく、npmからパッケージを引っ張ってimportすることでそのまま利用することもできます。
その時の話はここで纏めてありますので、そちらもよかったらぜひご覧いただければと思います。

ちなみに今回のサンプルで作ったソースコードは以下に全て放り込んでおいたので、上手く行かなかった場合は見比べてみてください。
https://github.com/huequica/qiita_react_sample

それでは今回はこの辺で。

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

addEventListenerにアロー関数で値を渡すとremoveEventのとき困る

mountedでイベントリスナーを動かし、destroyedで削除するパターンでうまくイベント削除が行えなかったのでメモ。

下記はVueを使っていますが、別にVueに限らず問題起こるみたい。

mounted () {
    window.addEventListener('mousedown', (e) => {
        this.hoge(e)
    })
},
destroyed () {
    window.removeEventListener('mousedown',this.hoge)
},
methods: {
    hoge (e) {
        console.log(e.clientX)
    }
}

下記のように書き換えました。

mounted () {
    window.addEventListener('mousedown', this.hoge)
},
destroyed () {
    window.removeEventListener('mousedown',this.hoge)
},
methods: {
    hoge (e) {
        console.log(e.clientX)
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptで連想配列から特定のキーだけ抽出

PHPのarray_column($array, 'column')みたいなことをJavaScriptでやりたいなぁと思ったときのやつ

const fruits = [
  {
    id: 1,
    name: "Apple",
  },
  {
    id: 2,
    name: "Banana",
  },
  {
    id: 3,
    name: "Grape",
  },
]

fruits.map(item => item["name"]) // ["Apple", "Banana", "Grape"]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

普通じゃ満足できない脱 webpack マニアのあなたに贈る Snowpack

Snowpack を使い React + TypeScript な環境構築をする具体的な設定を紹介します。

Snowpack is 何

npm パッケージを、ブラウザーから読み込めるよう ES modules 形式1に変換してくれるバンドラーです。

次のように使います:

  1. npm install 後に snowpack コマンドを実行し、node_modules 内のパッケージをブラウザーから読み込めるようバンドルする。たとえば node_modules/reactweb_modules/react.js のようなファイルにまとめる。
  2. アプリケーションコードは ES modules の作法に則って記述し、必要であれば web_modules/react.js を import して使う。

この結果嬉しいのは、公式に

No more waiting for your bundler to rebuild your site every time you hit save. Instead, every change is reflected in the browser instantly.

とあるように、アプリケーションコードを変更するたびビルド待ちになることがない点です。アプリケーションコードをバンドルしないのだから当然ですね。

とはいえ現実には Babel や tsc のトランスパイルはほぼ必須で、それらは待つ必要があります。それでも、大量の npm パッケージ群やアプリケーションコードを都度バンドルし直すよりは、開発体験が良いと思われます。

コード + 設定ファイルの具体例 3 つ

以下のパターンを紹介します。

tsc Babel
React ✅ Snowpack 入門 ✅ import を「通常どおり」書く方法の紹介
Preact ✅ alias 方法も紹介

tsc + React(Snowpack 入門)

TypeScript のトランスパイルに tsc コマンドを使うパターン。最低限のファイル構成は次のようになります:

public/           # デプロイする最終成果物一式
    dist/         # トランスパイル後のアプリケーションコードを出力する。gitignore 対象
    web_modules/  # Snowpack による出力結果。gitignore 対象
    index.html    # 画面のエントリーポイント。自分で、ブラウザーに読める HTML を書く

src/           # アプリケーションコード。中の構成は自由
    index.tsx  # スクリプトのエントリーポイント
    App.tsx
    ...

package.json          # 依存関係やタスクの管理
snowpack.config.json  # Snowpack 設定
tsconfig.json         # TypeScript 設定

アプリケーションコード

一つずつ見ていきます。

まず index.html です。Snowpack は webpack や Parcel と異なり index.html の面倒は見てくれないので、最終状態の HTML を書いておく必要があります。ポイントは script 要素で、次の 2 点が重要です。

  • type="module" 属性が必要(ES module として読み込むため)
  • src 属性にはトランスパイル後のパスが必要。つまり ./src/index.tsx ではなく /dist/index.js を指定する
public/index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>DOCUMENT TITLE</title>

    <script type="module" src="/dist/index.js"></script>
  </head>

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

次に、index.tsx は以下のようになります。特徴的なのは import パスで、ES modules として読み込めるように、トランスパイル先のフォルダー名や拡張子 .js を含んだものになっています(この点をなんとかする方法は後述の Babel + React 編で):

src/index.tsx
import React from '/web_modules/react.js'
import ReactDOM from '/web_modules/react-dom.js'

// Dynamic import の例。import App from './App.js' でも構わない
import('./App.js').then(({ default: App }) => {
  ReactDOM.render(<App />, document.getElementById('root')!)
})

ほかのファイルも同様に import パスは ES modules として記述します。一方 export は webpack などを使うときと同じ書き方が可能です:

src/App.tsx
import React, { useState } from '/web_modules/react.js'
import Title from './Title.js'

export default function App() {
  const [active, setActive] = useState(false)
  const toggle = () => setActive(v => !v)

  return (
    <div onClick={toggle}>
      <Title label="tsc + React" active={active} />
    </div>
  )
}

Title.tsx
src/Title.tsx
import React from '/web_modules/react.js'

export default function Title({
  label,
  active,
}: {
  label?: string
  active?: boolean
}) {
  return <h1 style={{ color: active ? 'red' : 'initial' }}>{label}</h1>
}

設定ファイル

package.json の内容は次のとおりです:

  • scripts#preparenpm install 後に自動で実行されるタスクです2。Snowpack によって node_modules 内のパッケージを変換しつつ ./public/web_modules に出力します。
  • scripts#compile./public/dist へ TS ファイルをトランスパイルするタスクです。開発中は --watch オプションをつけると便利です。
  • scripts#server は、オプションではあるものの実質必須となる開発サーバーの起動タスクです。Snowpack の思想的に snowpack-dev-server の類は存在しないので、任意のサーバーを選びます。
  • dependencies は依存関係の定義場所ですが、React, React-DOM のバージョン文字列が特徴的です。React 本体は ES modules 対応がなされていないため、Snowpack 開発元が提供するバージョンを使う必要があるためです3
package.json
{
  "scripts": {
    "prepare": "snowpack --optimize --clean --dest ./public/web_modules/",
    "compile": "tsc --outDir ./public/dist",
    "server": "live-server --quiet ./public"
  },
  "dependencies": {
    "react": "npm:@pika/react@^16.13.1",
    "react-dom": "npm:@pika/react-dom@^16.13.1"
  },
  "devDependencies": {
    "@types/react": "^16.9.23",
    "@types/react-dom": "^16.9.5",
    "live-server": "^1.2.1",
    "snowpack": "^1.6.0",
    "typescript": "^3.8.3"
  }
}

Snowpack オプションを設定する snowpack.config.json は次のとおりです。webDependencies に、node_modules 内のどのパッケージを web_modules として変換するか指定します。この設定を省略し、ソースコード内の import 文から自動判定させることもできます4

snowpack.config.json
{
  "webDependencies": ["react", "react-dom"]
}

tsconfig.json は次のとおりです。Snowpack を使う上で重要なのは compilerOptions#paths で、これにより /web_modules/react.js の型定義を node_modules/@types/react と紐づけないと、tsc の型検査に失敗します5。Snowpack の出力先を web_modules 以外にしたときはこの項目を変更してください:

tsconfig.json
{
  "compilerOptions": {
    // Choose your target based on which browsers you'd like to support.
    "target": "ES2017",

    // Required: Use module="ESNext" so that TS won't compile/disallow any ESM syntax.
    "module": "ESNext",

    // Required for some packages.
    "moduleResolution": "Node",

    // `import React` instead of `import * as React`
    "allowSyntheticDefaultImports": true,

    // <div /> => React.createElement("div")
    "jsx": "react",

    // Required: Map "/web_modules/*" imports back to their node_modules/ TS definition files.
    "baseUrl": ".",
    "paths": {
      "/web_modules/*.js": ["node_modules/@types/*", "node_modules/*"]
    },

    // Redirect output structure to a directory.
    // The directory is defined in the package.json#scripts.
    "noEmit": false,

    // Generate source maps including their original sources.
    "sourceMap": true,
    "inlineSources": true,

    // Useful type checks.
    "strictNullChecks": true
  },

  "include": ["src/**/*"]
}

起動手順

package.json と同じ階層で npm install したら、npm run compile -- --watchnpm run server をそれぞれ別のターミナルで実行します6http://localhost:8080 を開けば "tsc + React" の文字が見られるはずです。

Babel + React(import を「通常どおり」書く方法の紹介)

TypeScript のトランスパイルに babel-cli を使うパターンです。Babel が型検査をしてくれないのでその点のフォローが必要になるものの、import パスを webpack と同じように書くことができる利点があります。

ファイル構成はほとんど一緒ですが、.babelrc.js が増えます:

 public/
 src/
+.babelrc.js           # Babel 設定
 package.json
 snowpack.config.json
 tsconfig.json

アプリケーションコード

tsc + React の場合とほとんど変わりませんが、import パスを webpack のときと同じように書いています:

src/App.tsx
-import React, { useState } from '/web_modules/react.js'
-import Title from './Title.js'
+import React, { useState } from 'react'
+import Title from './Title'

 export default function App() {
 ...

設定ファイル

上記の書き味を実現しているのが、.babelrc.js で指定する Babel のプラグイン設定です。プラグインには Snowpack が提供しているもの (snowpack/assets/babel-plugin.js) を使います。

検証中にハマった点として importMap オプションがあります。プロジェクト直下に web_modules フォルダーを配置 しない 場合、Snowpack が import-map.json というメタ情報を探して迷子にならないよう、明示的にパスを指定する必要があるのです。相対パスでは期待どおりにならないため、絶対パスで指定します7

optionalExtensions オプションは、from './Title'from './Title.js' にトランスパイルするために有効化します:

.babelrc.js
module.exports = {
  presets: ['@babel/preset-typescript', '@babel/preset-react'],

  plugins: [
    [
      'snowpack/assets/babel-plugin.js',
      {
        importMap: `${__dirname}/public/web_modules/import-map.json`,
        optionalExtensions: true,
      },
    ],
  ],
}

package.json と tsconfig.json は次のとおりで、トランスパイルは Babel に任せ、tsc は型検査のみを担う構成になっています:

package.json
   "scripts": {
     "prepare": "snowpack --optimize --clean --dest ./public/web_modules/",
-    "compile": "tsc --outDir ./public/dist",
+    "compile": "babel ./src --extensions '.ts,.tsx' --source-maps --out-dir ./public/dist",
+    "test": "tsc",
     "server": "live-server --quiet ./public"
   },
   "dependencies": {
     "react": "npm:@pika/react@^16.13.1",
     "react-dom": "npm:@pika/react-dom@^16.13.1"
   },
   "devDependencies": {
+    "@babel/cli": "^7.8.4",
+    "@babel/core": "^7.8.7",
+    "@babel/preset-react": "^7.8.3",
+    "@babel/preset-typescript": "^7.8.3",
     "@types/react": "^16.9.23",
     "@types/react-dom": "^16.9.5",
tsconfig.json
-    // Required: Map "/web_modules/*" imports back to their node_modules/ TS definition files.
-    "baseUrl": ".",
-    "paths": {
-      "/web_modules/*.js": ["node_modules/@types/*", "node_modules/*"]
-    },
-
-    // Redirect output structure to a directory.
-    // The directory is defined in the package.json#scripts.
-    "noEmit": false,
-
-    // Generate source maps including their original sources.
-    "sourceMap": true,
-    "inlineSources": true,
+    // Only for type checks.
+    "noEmit": true,

     // Useful type checks.
     "strictNullChecks": true

起動手順

tsc + React の場合と同じです。

tsc + Preact(alias 方法も紹介)

TypeScript のトランスパイルに tsc を使い、React の代わりに Preact を使うパターンです8。Preact を React の代用とするには alias 機能が欲しいところですが、これを rollup.js プラグインによって実現します9

ファイル構成は tsc + React の場合とほとんど一緒で、snowpack.config.json の代わりに snowpack.config.js を使う点だけが違います:

 public/
 src/
 package.json
-snowpack.config.json
+snowpack.config.js
 tsconfig.json

アプリケーションコード

こちらも tsc + React の場合とほぼ同一で、差異は、React ではなく preact/compat を import している点です:

src/index.tsx
-import React from '/web_modules/react.js'
-import ReactDOM from '/web_modules/react-dom.js'
+import preact from '/web_modules/preact/compat.js'

 import('./App.js').then(({ default: App }) => {
-  ReactDOM.render(<App />, document.getElementById('root')!)
+  preact.render(<App />, document.getElementById('root')!)
 })
src/App.tsx
-import React, { useState } from '/web_modules/react.js'
+import preact from '/web_modules/preact/compat.js'
+import { useState } from '/web_modules/preact/hooks.js'
 import Title from './Title.js'

 export default function App() {
 ...

設定ファイル

Preact は、本家が ES modules 変換に対応しているので、通常のパッケージをインストールすれば大丈夫です。型定義が内包されたり、DOM レイヤーが内包されたりしているので、dependencies 定義が減るのも嬉しい点です。

Alias のための rollup.js プラグインも追加しておきます:

package.json
   "dependencies": {
-    "react": "npm:@pika/react@^16.13.1",
-    "react-dom": "npm:@pika/react-dom@^16.13.1"
+    "preact": "^10.3.4"
   },
   "devDependencies": {
-    "@types/react": "^16.9.23",
-    "@types/react-dom": "^16.9.5",
+    "@rollup/plugin-alias": "^3.0.1",
     "live-server": "^1.2.1",
     "snowpack": "^1.6.0",

JSX を React.createElement ではなく preact.createElement に変換するため、jsxFactory を明示的に指定する必要があります。もしくは、import preact from の代わりに import React from とし、jsxFactory の指定を省いてください:

tsconfig.json
-    // <div /> => React.createElement("div")
+    // <div /> => preact.createElement("div")
     "jsx": "react",
+    "jsxFactory": "preact.createElement",

     // Required: Map "/web_modules/*" imports back to their node_modules/ TS definition files.
     "baseUrl": ".",

snowpack.config.js は次のようになります。

  • webDependencies として、Preact のサブパッケージである preact/compat, preact/hooks を指定します。
  • rollup.js 設定として、reactpreact/compat に解決してバンドルするプラグインを指定します。

Alias プラグインを追加することにより、React を peerDependency に持つようなパッケージ(React-Router や styled-components など)を Snowpack によってバンドルできるようになります。好きなパッケージを試してみてください:

snowpack.config.js
const alias = require('@rollup/plugin-alias')

module.exports = {
  webDependencies: ['preact/compat', 'preact/hooks'],

  rollup: {
    plugins: [
      alias({
        entries: {
          react: 'preact/compat',
        },
      }),
    ],
  },
}

起動手順

tsc + React の場合と同じです。

まとめ

  • 3 種類の設定方法を紹介
    • tsc + React(Snowpack 入門)
    • Babel + React(import を「通常どおり」書く方法の紹介)
    • tsc + Preact(alias 方法も紹介)

Snowpack を使った感想

手軽でいいですね

webpack と比べるとだいぶ手軽に web アプリの開発環境が整います。最も楽なのは、ライブラリーまで含んだ巨大なアプリバンドルを生成してしまわないようにあれこれ積んでいたチャンクの設定、そういったものが一切不要になる点です。バンドルを生成しないからですね。非常に良い開発体験なんじゃないでしょうか。

Parcel も環境構築の早さという点で気に入っていましたが、Snowpack のほうがより好みです。Web 標準をできる限り使い倒そうとする姿勢がいいですね。webpack のように XX ローダー、YY ローダーと依存を増やし泥沼に陥る10より、web 標準外のことはできないと受け入れるほうが潔い。

紹介した 3 つの設定だと、個人的には tsc + Preact の構成が、依存が少なくて良いと思っています。React のほうがユニバーサルなコンポーネントを書けますが、本当にユニバーサルにして使い回すことなど滅多にない(と思う)ので、web に振り切っていいと思います。

バンドラーの移行という観点では厳しいものがある

既存の webpack プロジェクトからの移行は厳しいでしょう。大体の場合 JS 以外の資材を webpack ローダーの力によって import しているはずで、その点は Snowpack の思想と相容れないからです。それとも Babel をゴリゴリに使えばなんとかなるんでしょうか?わからないです。

逆に、Snowpack から webpack や Parcel、その他への移行は現実的です。ES modules 形式の import パスであったとしても、一括置換すればよいだけで、本質的な障害にはなりません。そのため、初期開発は Snowpack で始め、アプリの拡大に合わせて webpack への移行を検討すれば、スピードと柔軟性の両方の恩恵を受けられると思います。

参考


  1. Webpack を使わずに import 文を使う - Qiita 

  2. npm install some-package で新規パッケージを追加したときには実行されません。改めて npm install するか、npm run prepare するかが必要です。 

  3. https://www.snowpack.dev/#react 

  4. できるものの、自動判定がうまくいかなかったときのデバッグが面倒なので、明示的に指定するのをおすすめします。 

  5. デフォルト値は "*": ["node_modules/@types/*", "node_modules/*"] なので、設定なしのときは react と書けば node_modules/@types/react に解決されます。webpack を使うときはあまり意識しない設定値かもしれません。 

  6. --watch 込みの scripts を追加したり、https://www.npmjs.com/package/concurrently を使ったりするとスマート。 

  7. このために .babelrc.json ではなく .babelrc.js が必要です 

  8. React はマルチプラットフォームに対応しているものの、現実のプロダクトでは web 専用で使うケースが多いと思っていて、それならば軽量な Preact で代替するのは良い選択肢だと思います。Hooks も使えますし。 

  9. rollup.js が出てきたのは、Snowpack の中身が rollup.js だからです。 

  10. とはいえ webpack エコシステムは強力なので、そこに乗っかるのはまったく悪い選択肢ではないです。よほどマイナーなローダーを使わない限りほぼ問題はないでしょう。 

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

JavascriptのDate型の注意

月計算で何度も出くわしたので、注意書きとしてメモしておく。

月取得

getMonthで月を取得できるが返り値は0からはじまっている。

つまり、1月の場合は0、10月の場合は9が出力される。

月の取得
> var today = new Date();
  // Tue Mar 17 2020 14:00:11 GMT+0900 (日本標準時)
> var month = today.getMonth() + 1; // 現在の月を表すために +1 する
  // 3

ちなみにgetDateの返り値は1から始まる。

日の取得
> var tosay = new Date();
  // Tue Mar 17 2020 14:00:11 GMT+0900 (日本標準時)
> var date = today.getDate();
  // 17

おまけ

実際に試してみると違いを体感できます。

> var testDate1 = new Date("2020-02-02 00:00:00");
  // Sun Feb 02 2020 00:00:00 GMT+0900 (日本標準時)
> var testDate1.getMonth();
  // 1

> var testDate2 = new Date("2020","02", "02", "00");
  // Mon Mar 02 2020 00:00:00 GMT+0900 (日本標準時)
> var testDate2.getMonth();
  // 2
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript 基礎文法入門

JavaScript基礎文法入門

JavaScriptの基礎文法を学びます。

参考資料

はじめてのJavaScript

console.logでデバッグメッセージが出力される

console.log('Hello World');

HTML内で呼び出す場合は<body>タグの閉じカッコの直前で宣言する。

<body>
*
*
<script>
console.log('Hello World');
</script>
</body>

別ファイルから呼び出す場合は以下のように宣言する

<body>
<script src="hello.js"></script>
</body>

コメントアウト

一行

// コメント

複数行

/*
コメント
になります。
*/

文字列を扱う

シングルクォーテーション(')を文字列として扱う場合は\でエスケープしてあげるか、ダブルクオーテーション(")で囲む

console.log("It's me!");
// \' で特殊文字を表示する
console.log('It\'s me!');

文字列を連結させる時は+を使う

console.log('Hello' + 'World!');

定数

constで定数を宣言する

const price

定数を扱うと後から変更ができない。再代入もできなくなる。

変数

letで変数を宣言する。

letで宣言した変数は後から値が変更できる。

定数と変数の使いわけですが、最近のプログラミングでは、ある名前に割り当てた値がころころ変わるとわかりづらいので、なるべく const を使いつつ、どうしても必要なときに let を使うという方法がよく取られます。

昔はvarがあったが、今は使わない。
変数は英数字、$_のみ。数値から始まらない。
予約後は使えない。

変数を使った計算方法については以下の通りです。

let price = 500;

// price = price + 100
price += 100;

// price = price * 2
price *= 2;

// price = price + 1
price++;
// price = price - 1
price--;

データ型について

  • String(文字列)
  • Number(数値)
  • Underfined
    • 定義されていない。
  • Null
    • 値がそもそもない
  • Boolean
    • true,false
  • Obj(オブジェクト)
    • {a:3,b:5}

typeofメソッドを使うと、値がどのオブジェクトかを判定できる。

console.log(typeof 'Hi!'); // string
console.log(typeof 5); // boolean
console.log(typeof true); // boolean
console.log(typeof undefined); // underfined
console.log(typeof null); // object

null だけ object となっていますが、これは有名は JavaScript のバグなので、そういうものだと思っておけば OK

数字から文字列を扱うに注意

JavaScriptは数字からなる文字列も数値に変換して、演算できます。

// 文字列があるが、数値型として扱われる
console.log('5' * 3); // 15
console.log('5' - 3); // 2
console.log('15' / 3); // 5

// +は文字列連携で扱われる
console.log('5' + 3); // 53

ただし、+は例外で、文字列連携として扱われる。
数値計算をする場合は文字列を整数値に戻せばよい。

console.log(parseInt('5') + 3); // 8

parseIntで変換できない文字列を渡した場合はNot a Numberが返ってくる。
数値にしようとしたけどできなかった場合にこうした値が出てくる。

console.log(parseInt('a') + 3); // NaN

比較演算子

falseとして評価されるもの

  • 0
  • null
  • underfind
  • ''
  • false

それ以外はすべてtrueと評価される

console.log(Boolean(0)); // false
console.log(Boolean('Hello')); // true

条件分岐

const score = 70;

if (score >= 80) {
  console.log('great!');
} else if (score >= 60) {
  console.log('Good!!');
} else {
  console.log('OK...');
}

上記の内容を条件演算子で短く書き直す事もできます。

score >= 80 ? console.log('Great!') : console.log('OK...');

短かく書けるという利点の一方で場合によってはコードが読みにくくなるので、読みやすさのバランスと考えて書いたほうが良い。

switch文で条件分岐

switch(value)で、value値によって条件分岐ができる。

const signal = 'red';

switch (signal) {
  case 'red' :
    console.log('Stop!');
    break;
  // 複数の条件を加える
  // この場合は blue もしくは redの場合はという意味になる
  case 'blue' :
  case 'green':
    console.log('Go!!');
    break;
  default:
    console.log('Wrong Signal');
    break;
}

forを使ったループ処理

グレイヴ・アクセント(``)で囲んだ中で変数を展開できる。
これはテンプレートリテラルと呼ばれます。
テンプレートリテラル内で宣言した変数は${}で展開できる。

JavaScript の テンプレートリテラル を極める! - Qiita

ここではlet i${i}で展開している。

for (let i = 1; i <= 10; i++) {
  console.log(`Hi ${i}`);
}

whileを使ったループ処理

基本構文

let hp = 100;

while (hp >= 0) {
  console.log(`${hp} HP Left!`);
  hp -= 15;
}

while()の中で判定処理がtrueになるまでループ処理を行う。
またwhileの判定処理を後にして1度だけ最初に実行する場合はdo while()が使える。

let hp2 = -50;

do {
  console.log(`${hp2} HP Left!`); //-50 HP Left!
  hp2 -= 15;
} while (hp2 > 0);

continue,break

forwhileで特定の場合の時にスキップさせたり、処理を抜けたい時に使う構文がある。

  • continute: ループ処理の中で特定の条件の時だけ、処理をスキップさせる
for (let i = 1; i <= 10; i++) {
  // 3の倍数の時だけスキップさせる
  if (i % 3 === 0) {
    continue;
  }
  console.log(i);
}
  • break: ループ処理の中で特定の条件に、ループ処理を抜ける
for (let i = 1; i <= 10; i++) {
  // iが4になったらbreakして処理が終了する
  if (i === 4) {
    break;
  }
  console.log(i);
}

関数

functionを使って関数を定義する。
例では処理の中で広告を挟む処理をshowAd()で関数化して、showAd()を呼び出せば、どこからでも広告の処理を実行する事ができる例を紹介します。

'use strict';

function showAd () {
  console.log('-------');
  console.log('---AD--');
  console.log('-------');
}

console.log('Tom is Great');
showAd(); //  関数呼び出し

引数を使う

function内の値を、引数によって表示する文字列を変えてみます。

関数を定義するときのmessageは、値を仮置しているので仮引数、実際に関数を呼び出すときに渡されるHeader AD実引数と呼ぶ

function showAd (message) { // 仮引数
  console.log('-------');
  // テンプレートリテラルを追加
  console.log(`---${message}--`);
  console.log('-------');
}

console.log('Tom is Great');
showAd('Header AD'); // 実引数
console.log('Takahasi is Great');
showAd('Footer AD');

実行結果

Tom is Great
-------
---Header AD--
-------
Takahasi is Great
-------
---Footer AD--
-------

関数のデフォルト値を設定する

また関数を呼び出す時に仮引数に=を使って値を入れておく事で、デフォルト値を設定する事ができる

function showAd (message = 'AD') {
  console.log('-------');
  console.log(`---${message}--`);
  console.log('-------');
}

returnで値を返してみる

returnを実行するとそれ以降の処理は実行されない事に注意する

// 合計値を返す関数を作成する
function sum (a, b, c) {
  // この時点で値が戻されてそれ以降の処理は実行されない
  return a + b + c;
  // ここが無視される
  console.log('Hi!')
}
const total = sum(1, 2, 3) + sum(3, 4, 5);
console.log(total);

関数式

いままではfunctionで関数名を定義して呼び出す、一般的な関数宣言でしたが、関数を定数や変数の値として代入する関数式という構文があります。

変数に代入するような式のときは文末に ;(セミコロン)が必要です。

const sum = function (a, b, c) { //無名関数
  return a + b + c;
};

console.log(`${sum(1, 2, 3)}`);

このとき定義する関数は無名関数と呼びます。

アロー関数

前回の関数式よりももっと簡略化できる構文があります。
それがアロー関数です。

step1 functionをなくして、=>に置き換える

const sum = (a, b, c) => {
  return a + b + c;
};

step2 returnしかしない場合は、returnで行っている処理を=>後に移動させる

const sum = (a, b, c) => a + b + c;

最終的には2行でまとめる事ができます。

const sum = (a, b, c) => a + b + c;
console.log(`${sum(1, 2, 3)}`);

さらにアロー関数は引数が1つの時は()を省略する事ができる。

(関数式の例)

const double = function (a) {
  return a * 2;
};

上記の関数にstep1,step2を反映させると、以下の例になる

(アロー関数化した例)

const double = (a) => a * 2;

step3 引数が1つの時は()が省略できる為、以下のように置き換えができます。

const double = a => a * 2;

スコープ(有効範囲について)

ブロック内で定数や変数が宣言されて場合は基本的にはそのブロック内だけで有効となる。
例として関数、ifforwhile でもブロックのあるところでは定数や変数のスコープが分かれる。

したがって、ブロック外で呼び出そうとすると未定義となり有効にならないルールがある。

function f () {
  // 定数や変数がブロック内で宣言された場合
  // その定数や変数はこのブロックの中でだけ有効というルールがある
  const x = 1;
  console.log(x);
}

f();
console.log(x); // x is not defined

ブロック外で宣言された定数や変数については、グローバルスコープと呼ばれ、ブロック内でも呼ぶことが可能となる。
ブロック内で同名の変数がある場合はそちらが優先されるが、無ければグローバル変数が優先される。

const x = 2; // グローバルスコープ

function f () {
  const x = 1; // ローカルスコープ
  console.log(x); //1 ローカルスコープが呼び出される
}

f();
console.log(x); //2 グローバルスコープが呼び出される

コードをブロックで囲っておこう

idnex.htmlでJavaScriptのコードを呼びだす場合は、<script> タグを分けて書いてもスコープが分かれるわけではない事に注意が必要です。

(main.js)

const x = 100;
console.log(x);

(index.html)

<script src="js/main.js"></script>
<script>
'use strict';

const x = 300;
console.log(x);

</script>

一見main.jsとindex.html内の<script>タグ内で宣言されている内容について別々で別れているように見えて同じファイルで宣言されていると見なされます。
なので、この場合はxがすでに宣言されているとエラーで怒られる。
したがって、ブロック({})で囲ってあげる必要がある。

{
  const x = 100;
  console.log(x);
}

HTMLでJavaScriptを使う場合は、ブロックで囲んであげる事を注意しておきましょう。

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

Reactでマークダウンエディタ作成とマークダウンからHTMLに変換(ハイライト付き)

Reactでマークダウンエディタの作成と、マークダウン表記をHTMLに変換(ハイライト付き)してみました。
ライブラリを組み合わせることで、楽に実装することができます。

環境

  • OS : macOS Catalina 10.15.3
  • node.js : 12.13.1
  • React : 16.12.0

Reactのライブラリは以下を使用します。

  • react-simplemde-editor(EasyMDE) : 4.1.0
  • marked : 0.8.0
  • highitjs : 9.16.2

参考までに、紹介するコードはGitHubにあります。

https://github.com/txkxyx/react-mde

マークダウンエディタを作成する

Webの画面にマークダウンエディタを埋め込む場合、元々SimpleMDEというライブラリが使用されていましたが、今はEasyMDEというSimpleMDEをフォークして作られたライブラリが使用されています。

↓EasyMDE

https://github.com/Ionaru/easy-markdown-editor

今回はEasyMDEのReactラッパーコンポーネントであるreact-simplemde-editorを使用してマークダウンエディタを作成します。

↓react-simplemde-editor

https://www.npmjs.com/package/react-simplemde-editor

まずは使用するパッケージをインストールします。

$ npm install react-simplemde-editor

コンポーネントを作成します。
実装はとてもシンプルです。SimpleMDEのコンポーネントとEasyMDEのcssファイルをインポートします。
SimpleMDEコンポーネントにはonChangeイベントで起動する関数を渡す必要があります。この関数は合成イベントを受け取り、その値がエディターで入力したマークダウンになります。

import React, { useState } from "react";
import SimpleMDE from 'react-simplemde-editor';
import 'easymde/dist/easymde.min.css';

const MarkDownEditor = () => {

    const [markdown, setMarkdown] = useState('');

    return(
        <SimpleMDE onChange={(e) => setMarkdown(e)}/>
    )
}
export default MarkDownEditor;

video1.gif

ツールバーにメニューが表示されています。デフォルトでは以下のメニューが表示されます。

メニュー 説明 表記
bold 強調 ****
italic 強勢 **
heading 見出し #
quote 引用 >
unordered-list 箇条書き *
ordered-list 番号付きリスト 1.
link リンク挿入  [](https://)
image 画像挿入  ![](https://)
preview プレビューモード なし
side-by-side 入力とプレビューで画面分割 なし
fullscreen 全画面でプレビューモード なし
guide マークダウン記法ガイド なし 

メニューの一覧は以下を確認してください。

https://github.com/Ionaru/easy-markdown-editor#toolbar-icons

メニューを追加する場合は以下のように実装します。
メニューを追加するとデフォルトのメニューが消えてしまうので、デフォルトのメニューを追加したい場合は別途追加します。

import React, { useState } from "react";
import SimpleMDE from 'react-simplemde-editor';
import 'easymde/dist/easymde.min.css';

const toolbar = [
        {
            name: "save",
            action: function customFunction(editor) {
                alert(editor.value())
                // save action
            },
            className: "fa fa-save",
            title: "Save"
        },
        '|',
        'bold',
        'italic',
        'heading',
        '|',
        'quote',
        'unordered-list',
        'ordered-list',
        '|',
        'link',
        'image',
        '|',
        'preview',
        'side-by-side',
        'fullscreen',
        '|',
        'guide',
    ]

const MarkDownEditor = () => {

    const [markdown, setMarkdown] = useState('');

    return(
        <SimpleMDE onChange={(e) => setMarkdown(e)} options={{toolbar:toolbar}}/>
    )
}
export default MarkDownEditor;

video2.gif

マークダウンエディタを拡張する

EasyMDEは機能を拡張することができます。その一つのCodeMirrorのイベント処理を利用して、JavaScriptのイベントをハンドリングしてみます。

CodeMirrorのイベントは以下から確認してください。

https://codemirror.net/doc/manual.html#events

画像などのファイルをマークダウンエディタにドロップして、そのデータを取得してみます。

import React, { useState } from "react";
import SimpleMDE from 'react-simplemde-editor';
import 'easymde/dist/easymde.min.css';

const MarkDownEditor = () => {

    const [markdown, setMarkdown] = useState('');

    function handleDrop(data, e){
        let files = e.dataTransfer.files;
        if(files.length > 0){
            let file = files[0];
            alert('FileName : ' + file.name );
            // any action
        }
    } 

    return(
        <SimpleMDE onChange={(e) => setMarkdown(e)} events={{'drop':handleDrop}}/>
    )
}
export default MarkDownEditor;

video3.gif

SimpleMDEのコンポーネントのeventsのpropsにドロップ時に起動する関数を指定します。
この関数内でファイルオブジェクトを取得することができます。この関数内で画像アップロードをすると、画像アップロード込みのマークダウンエディタを実装することができます。

マークダウン表記をHTMLに変換する

マークダウン形式の文字列をHTMLに変換するために、markedライブラリを使用します。

$ npm install marked

先ほど作成したエディタで入力されたマークダウンをmarkedでHTMLに変換し画面に表示します。
インポートしたmarkedの引数に、マークダウン形式の文字列を渡すだけです。

import React, { useState } from "react";
import SimpleMDE from 'react-simplemde-editor';
import 'easymde/dist/easymde.min.css';
import marked from "marked";

const MarkDownEditor = () => {

    const [markdown, setMarkdown] = useState('');

    return(
        <div>
            <SimpleMDE onChange={(e) => setMarkdown(e)}/>
            <div id="body" >
                <span dangerouslySetInnerHTML={{ __html: marked(markdown)}}/>
            </div>
        </div>

    )
}
export default MarkDownEditor;

video4.gif

これで、エディタで入力したマークダウンをHTMLに変換して表示することができます。

ではコードを挿入している部分にハイライトを追加していきます。

ハイライトをつける

コードを挿入している部分にハイライトを付けるために、highlightjsを導入します。
JavaScriptのハイライトのライブラリの中でも対応言語が多く、パレットの種類も豊富です。(185言語、91スタイル)

https://highlightjs.org/static/demo/

ライブラリをインストールします。

$ npm install highlightjs

実装は以下のようになります。

import React, { useState } from "react";
import SimpleMDE from 'react-simplemde-editor';
import 'easymde/dist/easymde.min.css';
import marked from "marked";
import highlight from 'highlightjs';
import 'highlightjs/styles/docco.css';

const MarkDownEditor = () => {

    const [markdown, setMarkdown] = useState('');

    return(
        <div>
            <SimpleMDE onChange={(e) => setMarkdown(e)}/>
            <div id="body" >
                <span dangerouslySetInnerHTML={{ __html: marked(markdown)}}/>
            </div>
        </div>

    )
}
export default MarkDownEditor;

video5.gif

highlightjsと適用したいスタイルのCSSファイルをインポートします。今回はdoccoというスタイルを使用したので、docco.cssをインポートしています。
CSSファイルはhighlightjs/styles/スタイル名.cssで指定できます。たまに違う場合もありますので、その時はハイフンを付けたりしてみてください。
highlightjsは表示する<code>タグのclass属性に指定されている言語名に応じてスタイルを適用します。

例えば、マークダウンで記載したPythonのコードは以下のような流れで表示されます。

  • マークダウン形式

```python

コード

```

  • markedでHTMLに変換

<span><code class="language-python">コード</code></span>

マークダウンで言語をPythonに指定しているので、markedでHTMLに変換したい際にcodeタグのclass属性にlanguage-pythonが指定され、highlightjsのスタイルが適用されます。
```python:test.pyのように言語名の後にファイル名を記載している場合、class属性にファイル名まで適用されてしまうため、markedのオプションを使用しレンダリング前にファイル名を除去することもできます。

実装は以下のようになります。

import React, { useState } from "react";
import SimpleMDE from 'react-simplemde-editor';
import 'easymde/dist/easymde.min.css';
import marked from "marked";
import highlight from 'highlightjs';
import 'highlightjs/styles/docco.css';

// delete file name
marked.setOptions({
    highlight: function (code, lang) {
        return highlight.highlightAuto(code, [lang.split(':')[0]]).value;
    }
});

const MarkDownEditor = () => {

    const [markdown, setMarkdown] = useState('');

    return(
        <div>
            <SimpleMDE onChange={(e) => setMarkdown(e)}/>
            <div id="body" >
                <span dangerouslySetInnerHTML={{ __html: marked(markdown)}}/>
            </div>
        </div>

    )
}
export default MarkDownEditor;

スタイルは豊富に用意されているので、サイトに合わせたハイライトを選択してみてください。

まとめ

Reactでライブラリを使用して、マークダウンエディタとマークダウンをHTMLに変換してみました。
コンポーネント一つ追加するだけで、簡単にマークダウンエディタを導入できます。拡張もできるので自由度はかなり高いかなと思っています。
他にもreact-mdeなどのエディタがありますが、デザインなどの面からreact-simplemde-editorの方が個人的には好みです。
もし使用する機会があれば検討してみてください。

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

[payjp]tokenは発行できているのに呼び出せるはずのデータがテーブルに入らないエラーの解決方法

1.どのような状態だったか

javascriptでpayjp-token取得はコンソールで確認すると全て成功しており、ページ遷移の部分でもエラーは出ず、validationに関するエラーも出ないのに、コントローラーでpayjp-tokenの値が取り出せないという状態でした。(エラー文が出ず、正常にnewアクションに戻るという症状でした。)

下記の二つの記事を参考にpay.jp機能を実装をしていました。(大元が上の方で、それの詳細まで記述されていたのが下記の方でした。)

https://qiita.com/takachan_coding/items/f7e70794b9ca03b559dd
https://qiita.com/emincoring/items/ce29dbbd182aa3c49c6b

なのでとりあえず動作を見るべく細部まで記述のあった下記のブログを参考に下記のようにviewを作成しました

※今回はviewの記述によるエラーだったので、その部分についてのみ記述します

app/view/cards/new.html.haml
.content__title
  %h2 クレジットカード情報入力
.content__credit-card
  .content__credit-card__inner
    = form_with url: cards_path, method: :post, html: { name: "inputForm" } do |f| -# createアクションのパスを指定
      = f.label :カード番号, class: 'label'
      %span 必須
      = f.text_field :card_number, type: 'text', class: 'input-number', placeholder: '半角数字のみ', maxlength: "16"
      .cards-expire
        = f.label :有効期限, class: 'label'
        %span 必須
        %br
        .cards-expire__wrap
          = f.select :exp_month, [["01",1],["02",2],["03",3],["04",4],["05",5],["06",6],["07",7],["08",8],["09",9],["10",10],["11",11],["12",12]],{} , class: 'input-expire'
          %span.expire-text%br
        .cards-expire__wrap
          = f.select :exp_year, [["19",2019],["20",2020],["21",2021],["22",2022],["23",2023],["24",2024],["25",2025],["26",2026],["27",2027],["28",2028],["29",2029]],{} , class: 'input-expire'
          %span.expire-text.cards-expire
        = f.label :セキュリティコード, class: 'label'
        %span 必須
        = f.text_field :cvc, type: 'text', class: 'input-number', placeholder: 'カード背面4桁もしくは3桁の番号', maxlength: "4"
      .content-bottom#card_token
        = f.submit '追加する', class: 'content-bottom--add-btn', id: 'token_submit'

2.原因

payjp-tokenの受け渡しを図解すると、
submitを押してjavascriptに入力情報送信→javascriptでpayjp-tokenに変換→form_withの実行でcontrollerにpayjp-token受け渡し
となります。
この時、数値の受け渡し毎にそれぞれidが1つずつ必要になるのですが、自分は1つしか用意していませんでした。
そのため、受け取りid=受け渡しidとなり、入力情報(カードの期日など)を素のままでform_withで受け取り、コントローラに値を返してしまっていたというのが原因でした。

3.解決方法

form_withにも新たにidを定義すれば希望通りの挙動をしました

app/views/cards/new
〜前略〜
= form_with url: pay_cards_path, method: :post, id: 'charge-form',html: { name: "inputForm" }do |f|
# idの名前はどのようなものでも大丈夫です。筆者はpayjpのホームページで記述のあったid名を引用しました。
〜後略〜

4.(補足)エラーの見つけ方

まず今回のように、railsのエラー文やjavascriptのエラー文(Google Chromeの検証のconsole上のもの)が出なければ数値の受け取りミスであることがほとんどだと思われます。

したがって筆者はまずコントローラでbinding.pryを挟んでどこまで数字が取れているか確認し、次にjavascriptにもconsole.log();を挟んで確認しました。結果、javascriptは問題ないということがわかり、コントローラとビューの確認を行いました。

その結果、javascriptには送れているから、そのあとの受け取りのタイミングで間違いがあると考え、submitの後に処理されるのはなにかと考えた時、残るのはform_withだけなのでその記述に関する資料を検索していたところ、このメソッドもid指定できることがわかり解決に至りました。

このようにエラー文が出なくて困ったら、デバックのコードを1行ずつ挟んで確認すれば原因を特定しやすくなり、エラーを見つけることができます。

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

JavaScriptでタイムゾーン付きの文字列(ISO8601)をJSTの"yyyy/mm/dd HH24:mi:ss"で表示する

概要

タイムゾーン付きの文字列(ISO8601)をJSTで表示する。

実装

a = new Date('2020-03-09T19:21:13+09:00')
// Mon Mar 09 2020 19:21:13 GMT+0900 (日本標準時)
a.toLocaleDateString("ja-JP", {timeZone: "Asia/Tokyo"})
// "2020/3/9"
a.toLocaleString("ja-JP", {timeZone: "Asia/Tokyo"})
// "2020/3/9 19:21:13"
a.toLocaleString("ja-JP", {timeZone: "America/New_York"})
// "2020/3/9 6:21:13"

おまけ

月日を0パディングしたい場合は、

a = new Date('2020-03-09T19:21:13+09:00')
a.toLocaleDateString("ja-JP", {timeZone: "Asia/Tokyo", year: "numeric", month: "2-digit", day: "2-digit"})
// "2020/03/09"

※参考
* options部分(timeZone指定している部分)

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

Node.jsのworker_threadsに何を渡すべきか

久々にScalaの世界からJSの世界に帰ってきました。

1. 本日の課題

本来Node.jsは非同期処理をストイックに突き詰めることでマルチスレッドやマルチプロセスのようなオーバーヘッドを伴う方法よりも高効率に並列処理を実現しているわけです。
ただし、それが有効なのは頻繁に「待ち」≒「I/O処理」が発生する場合に限られます。
ひたすらI/OなしでCPUをぶん回す処理、を複数同時にやれって言われたらシングルスレッドなNodeはマルチスレッドに勝てません。

ですがですが、Nodeにだってマルチスレッド/マルチプロセスの仕組みはあります。

さて今回は、

  • 数百MB~数GB程度のデータ構造(変数として持っている)に対して
  • 秒オーダーの時間をかけておこなう検索処理を
  • 複数回おこなう
  • 元のデータ構造は更新しない(Read Only)

という要件で、この「複数回おこなう」というところをマルチスレッドかマルチプロセスで並列化して時間を短縮したい、というお題になります。1

2.候補

child_process/cluster

どちらもマルチプロセスなアプローチです。
マルチプロセスなので、メモリは各自が持ちます2。GB単位のメモリをそれぞれのプロセスが持ってたらちょっともったいないですね3。ということで早々に候補から排除。

worker_threads

今回の本命です。
本家にはこのように書いてあります。

child_processやclusterとは異なり、worker_threadsはメモリを共有できます。

ふむふむ、スレッドですからね。そりゃそうですよね。ですが続けてこうも書いてあります。

これは、ArrayBufferインスタンスを転送するか、SharedArrayBufferインスタンスを共有することにより行われます。

What's?
つまり、何も考えなくてもメモリが共有されるわけじゃないようですよ。
それが今回の本題というわけです。

3. Bufferを使うということ

ArrayBuffer/SharedArrayBufferというのはつまり中身はバイナリですから、

{
    "こんな": 1,
    "好き勝手な": "aaa",
    "形をした": [
      "JSONから",
      "作った",
      "object/arrayなんか",
    ],
    "面倒みないよ": true
}

てなもんですよ。最後のtrueがなんだかムカつきますね。
なんとかBufferの基本的な使い方は、Uint32ArrayみたいなTypedArrayをViewとして使うことが多いんではないかと思います。4

メモリは大事にしたい(共有したい)、JSON-likeな構造も扱いたい、そんなわがままに答えてくれるものはないでしょうか。

JSON

メインスレッドでJSON.stringifyしてBufferに載せて、ワーカースレッドでJSON.parseする・・・えーと、parseしてしまったらメモリの共有になってません。ダメです。

JSON以外のシリアライザ

messagePackとかありますね。これもデコードせねばならないのでメモリの共有にはなりません。messagePackならJSONよりエンコード状態のサイズが若干小さいという利点はありますが、デコードしてしまうのでささいな差ですね。5

4. ないものは自分で作ればいいじゃない

そう、それ!
やっと本題だ。
要するに、

  • SharedArrayBufferに載せられて
  • デコードせずとも中身にObjectやArrayのようにアクセスできる

何かを作ってしまえばいいじゃないか、という話。

「デコードせずとも中身にObjectやArrayのようにアクセスできる」?は?

と思ったあなた!
あるんですよ、JSにはいにしえより伝わる黒魔術、その名もProxyが。
つまり、「Object(あるいはArray)に見える」Proxyが動的にBufferをデコードしてあげればいいんじゃないか、と思ったわけです。

この用途だとJSONやmessagePackなどのシリアライザでエンコードしておいて、SharedArrayBufferに載せて、ワーカー側では全体のデコードは「せず」に読み出しの時だけ必要部分をエンコードするようにすればいいわけですが、ここでもう一つ問題が。
JSONは、全体をスキャンしないと、構造が分からない。keyがあるのかどうか、あった場合に何byte目に入っているのかは、先頭から順に追っていかないとわからない。メモリが節約できても、それではあまりに遅すぎる。messagePackも「ほぼバイナリ形式のJSON」であり同じこと。

そういうわけですから、

  • Objectならkeyの有無、Arrayなら配列のサイズがすぐに分かって、
  • 値の格納場所にダイレクトにたどり着ける

そんなデータ構造のシリアライザが欲しい!
ということで作ってみましたよ。

roJson(Gistに飛びます)

使い方(sample.js)もGistに載せてますが一応再掲。
一般的なシリアライザ(JSON/messagePack等)と同じようにごくシンプル。

sample.js
const roJson = require('./roJson');

const buffer = roJson.encode({a: "Hello roJson.", b: 1, c: [0, 100, 200]});
const proxiedObject = roJson.getProxy(buffer);

console.log(proxiedObject.a); // "Hello roJson."
console.log(proxiedObject.c[1]); // 100

長くなったのでいったんここまで。
工夫ポイント、ベンチマークなどは次回!


  1. そんなヘビーな用途にNode.jsを使うなんて、なんてツッコミはなしの方向で。 

  2. OSの機能でプロセス間共有メモリってのがありますからそういうので共有できないことはないです。Node.jsから使う猛者がいるかどうかは知りませんが。 

  3. 昔の知識なので最新は違うかもしれないしOSにもよるんでしょうが、プロセスをforkするとすぐにはメモリはコピーされなくて(つまり共有されていて)、書き込んだ時点でページ単位でコピーしてそれぞれの道を歩むようになってました。大部分のメモリに書き込みが発生しないならそれに頼るのも一つの見識ですね。 

  4. そういうデータを使うのって機械学習とかCG系とかのイメージ。そういう用途Node.js使いますかね・・・。 

  5. あと、JSON.stringify/parseはJSエンジンネイティブ実装だし、最適化されまくってるので速さで勝てません。憎いです。 

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

Trelloの完了したタスクをGASで自動アーカイブする

はじめに

仕事のタスク整理にTrelloを使いますが、完了したタスクを手動でアーカイブすると判断に迷ったり、手間を感じることがあるので、完了してから一定期間経過したものをGoogleAppsScript(GAS)で自動アーカイブするようにしました。

準備

TrelloのAPIをGASで使用するにはAPIキーやトークンの取得が必要になります。
方法はこちらの記事を参考にしてください。

処理の流れ

  1. 完了のリストにあるカードを取得する
  2. 取得したカードの最終更新日が7日経過していれば、アーカイブする
  3. アーカイブしたカードの情報をスプレッドシートに出力(追記)する

コード

①カードの取得とアーカイブ対象の選定

cardArchived.gs
// カードのアーカイブ
function cardArchived() {

  // スクリプトプロパティのオブジェクト取得
  var scriptProperty = PropertiesService.getScriptProperties().getProperties();

  //TrelloのAPIキーの取得 
  var Key = scriptProperty.API_KEY; 

  //TrelloのAPIトークンの取得
  var Trello_Token = scriptProperty.TOKEN; 

  //削除対象のリストIDの取得
  var ListId = scriptProperty.LIST_ID_DONE; 

  /*
  リストにあるカードの一覧の取得
   【取得するfrield】
   id = カードのID
   name = カードの名称
   dateLastActivity = 最終更新日
   shortUrl = カードのURL
   desc = カードの説明
  */
  console.log("リストにあるカードの一覧の取得");
  var cardListURL = 'https://api.trello.com/1/lists/' + ListId + '/cards'
  + '?key=' + Key
  + '&token=' + Trello_Token
  + '&fields=id,name,dateLastActivity,shortUrl,desc';

  //カードの一覧取得要求
  var response = UrlFetchApp.fetch(cardListURL);

  //取得した情報をJSON形式に変換
  var json = JSON.parse(response.getContentText("UTF-8"));

  ///現在時刻の取得
  var nowDate = Moment.moment(); 
  // yyyy/mm/dd形式に変換
  var nowDate_format = nowDate.format("YYYY/MM/DD"); 

  // 取得したカードの判定
  for (var i in json) {
    // 最終更新日をMoment型にパース      
    var dateLastActivity_moment =  Moment.moment(json[i]["dateLastActivity"]); 
    //最終更新日 + 7日
    var dateLastActivity_moment_add = dateLastActivity_moment.add(7, "days"); 
    // 表示形式を変換
    var dateLastActivity_format = dateLastActivity_moment_add.format("YYYY/MM/DD");   

    // 日付の比較(現在>日付加算後)
    //  Moment.moment(現在日付).isAfter(最終更新日+7日);
    if (!Moment.moment(nowDate_format).isAfter(dateLastActivity_format)){

      console.log("完了してから最終更新日から7日経過したカードをアーカイブする");

      // 対象のカード内容(ログ出力)
      console.log("カードの名称 = "+json[i]["name"]);
      console.log("最終更新日 = "+json[i]["dateLastActivity"]);
      console.log("カードのURL = "+json[i]["shortUrl"]);

      // スプレッドシート出力
      outputSpreadsheet(nowDate_format,dateLastActivity_format,json[i]);

      // 対象のカードをアーカイブ
      var cardURL = 'https://api.trello.com/1/cards/' + json[i]["id"]
      + '?key=' + Key
      + '&token=' + Trello_Token
      + '&closed=true';
      UrlFetchApp.fetch(cardURL, {'method' : 'put'});
    }
  }       
}

②アーカイブしたカードをスプレッドシートに出力(累積させる)

outputSpreadsheet.gs
// スプレッドシートにアーカイブしたカードを出力
function outputSpreadsheet(nowDate_format,dateLastActivity_format,json) {

  // スプレッドシートの取得 
  const sheet = SpreadsheetApp.getActive().getSheetByName('archived_log');  

  // 最終行の取得
  const lastRow = sheet.getLastRow();

  // 出力
  var outputRow = lastRow + 1;
  sheet.getRange(outputRow, 1).setValue(nowDate_format);  // アーカイブ実施日
  sheet.getRange(outputRow, 2).setValue(json["name"]); // カード名称
  sheet.getRange(outputRow, 3).setValue(json["desc"]); // カードの説明
  sheet.getRange(outputRow, 4).setValue(dateLastActivity_format); // カードの最終更新日
  sheet.getRange(outputRow, 5).setValue(json["shortUrl"]); // カードのショートURL   
}

コード作成後の作業

あとはスクリプトを時限式のイベントトリガーで指定した時間に自動起動するように設定するだけです。
私は上記①のスクリプトを毎朝7~8時くらいに起動するように設定しました。

最後に

タスク管理は運用ルールを決めて実施しないと意味がなく、手動だと忘れてしまうことがあるので、自動化できるものは手作業から無くしてしまいましょう!

参考リンク

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