- 投稿日:2020-03-18T23:55:26+09:00
【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 番目の要素です。' } ]
- 投稿日:2020-03-18T23:38:34+09:00
【Vue】喘ぎ声メーカー作ってみた
スーパー喘ぎ声メーカー
用意した単語をシャッフルして喘ぎ声を生成するツールを作ってみました。
喘ぎ声に限らず、好きな単語を入れて遊べます。何故作ろうと思ったのか
BL小説書きの友人が「この喘ぎ声の組み合わせさっきも書いたな?
となるので自動生成してくれるツールがほしい」と言っていたので。
BLに限らずいやらしい小説を書く方への負担を少しでも減らせないかと思い、作成にいたりました。
あとVueの勉強になりそうだと思ったので。
筆者は文系のwebデザイナー(見習い)であり、プログラミングの分野は勉強中です。使ったもの
- HTML
- CSS
- JavaScript
- clipboard.js
- Vue.js
- jQuery
- Bootstrap4
- Font Awesome
作成の流れ
プロトタイプ①
まずHTMLとJavaScriptのみでプロトタイプを作成。
「生成!」ボタンを押すと配列をシャッフルした結果が表示されます。
決め打ちのデータしか組み合わせられませんが、大体のイメージができあがりました。ちなみに「やっそん(おそらく「やめて、そんな」→「やっ…そん…」→「やっそん」と転じた)」は
一部界隈のスラングでBLの性行為のことを指します。永遠に使えないムダ知識をあなたに。使用例:出会って5秒でやっそん
プロトタイプ②
Vue.jsでよくあるTodoアプリのコードを元にプロトタイプ第二弾を作成。
入力したデータを組み合わせられるようになった点はいい感じです。Vue.jsrandom: function () { const rnd = Math.floor(Math.random() * this.words.length); return this.words[rnd]; }ただこのメソッドだと同じ単語を何度も使ってしまうため、「あっあっあっ」と
ネフェルピトーに脳を弄くられるポックルみたいになってしまいます。
配列をシャッフルするコードに書き換えなければなりません。モックアップ
Bootstrap4で見た目だけのものを作成。レスポンシブにも対応です。完成品
完成品がこちらです!→スーパー喘ぎ声メーカー
Vue.jsvar 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.jsshuffle: 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(''); }これは過去のコードなのですが、このメソッドだと
シャッフル実行時配列そのものもシャッフルされてしまいます。下図参照。
Vue.jsshuffle: 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(''); }コピーした配列にシャッフル処理をするように書き換えたら
単語のリストはシャッフルされなくなりました。
この対処法が正しいかはわかりませんが…
また生成結果を表示するリストは、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-bind
とv-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
コード見たら私がマジの初心者であることはお分かりになるかと思います…
プルリクお待ちしております。
- 投稿日:2020-03-18T22:49:58+09:00
Vue.js 現在時刻の表示 ~時計~
Vue.jsとは
ユーザーインターフェイスを構築するためのプログレッシブフレームワークです。他の一枚板(モノリシック: monolithic)なフレームワークとは異なり、Vue は少しずつ適用していけるように設計されています。中核となるライブラリは view 層だけに焦点を当てています。そのため、使い始めるのも、他のライブラリや既存のプロジェクトに統合するのも、とても簡単です。また、モダンなツールやサポートライブラリと併用することで、洗練されたシングルページアプリケーションの開発も可能です。
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>完成品
ここから要素ごとに説明していきます。ちなみに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 の実行が可能になります。次にコメントアウトにて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(); } } });ボタンを押す前
どうでしょうか?
クリックイベントで、現在の時間を取得できましたか?私もまだまだ勉強中ですが、書き方自体はシンプルでとてもわかりやすいですよね。
ちなみに私はこちらをYoutubeで見て学習をしました。
是非そちらもご参考までに見てみてください。Youtube たにぐち まことのともすたチャンネル様
https://www.youtube.com/watch?v=jdcZ3LvTs78
- 投稿日:2020-03-18T22:33:10+09:00
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.jsimport 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
で妥協している
- 投稿日:2020-03-18T20:49:11+09:00
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で使ってないとあんまり意味ないので注意。
- 投稿日:2020-03-18T19:47:45+09:00
Kinx プレビュー版リリース
Kinx プレビュー版のリリース
はじめに
「見た目は JavaScript、頭脳(中身)は Ruby、(安定感は AC/DC)」でお送りするスクリプト言語 Kinx。まだまだ途中だがリリースするのも意味があるだろうとの想定から、実行可能な形でパッケージングし、まずはプレビュー・リリースすることにしました。Windows 版の実行ファイルはアイコンもつけていい感じ。Linux は /usr/bin にインストールしてしまうので、一応気にしておいてください。
本題
↓ここです。
もちろん、git clone して make もできます。
本題、おしまい。
振り返り
元々はこの記事での「プログラマに馴染むシンタックスってのは C 系だよね、でも何で Ruby も Python も全然違うのでしょう」というところから始まった今回のプロジェクト。プロジェクト自体は去年末くらいから実質スタートしてたので、約 5 ヶ月くらいか。
元々からしてほぼ JavaScript な文法なので、作りながらではあるものの、既にすっかり私の手には馴染んでます。
まだ初版にはできませんが(おっとさっき気づいたのだが標準入力がない...)、結構色々試せるとは思います。
ほんのちょっと興味があれば
何か試してみてフィードバック貰えると非常に嬉しい。まぁ、既存の何かを置き換えるようなたいそうなモノではないので、ちょっとした提案とかこんなんあるといいんじゃない、とか、そういうライトな感じのご意見ください。
すみません、見返りはあまり無いと思います…。そういうの楽しめる方か、面白そうだと思ってくださる方向け。
おわりに
最後はいつもの以下の定型フォーマットです。
- 最初の動機は スクリプト言語 KINX(ご紹介) を参照してください(もし宜しければ「
いいねLGTM」ボタンをポチっと)。- リポジトリは ここ(https://github.com/Kray-G/kinx) です。こちらももし宜しければ★をポチっと。
- 投稿日:2020-03-18T18:25:22+09:00
レジの店員を呼ぶアプリをつくた 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
- 投稿日:2020-03-18T18:19:34+09:00
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-pluginsGulpタスクの追加
gulpの設定ファイル
gulpfile.js
をpackage.json
と同じ階層に作成し、設定を記述します。htdocs ├ node_modules ├ package.json ├ gulpfile.js └ index.htmlgulpfile.jsに以下の内容を記述します。
(バージョンにより書き方が異なる場合があります。)gulpfile.jsconst 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 typingsjQueryの型定義ファイルのインストールをします。
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.jsclass Test { hello() { console.log('test') } } const onClick = elm => { const test = new Test(); elm.on('click', () => { test.hello(); }) } $(() => { onClick($('#btn')) })書き換え後のコード
TypeScriptに書き換えた後のコードです。
拡張子を.ts
に変更し、gulpfile.js
のsrc
で記述したディレクトリにコピペします。型推論で自動で型判別してくれるため、必要のない方は記入していません。
必要に応じて型を設定してください。
今回はonClick
の関数に引数elm
がJQeryであることを明示しています。
JQuery
のJ
は大文字なので気をつけてください。先程インストールした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.jsmodule 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')) })
- 投稿日:2020-03-18T18:19:34+09:00
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-pluginsGulpタスクの追加
gulpの設定ファイル
gulpfile.js
をpackage.json
と同じ階層に作成し、設定を記述します。htdocs ├ node_modules ├ package.json ├ gulpfile.js └ index.htmlgulpfile.jsに以下の内容を記述します。
(バージョンにより書き方が異なる場合があります。)gulpfile.jsconst 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 typingsjQueryの型定義ファイルのインストールをします。
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.jsclass Test { hello() { console.log('test') } } const onClick = elm => { const test = new Test(); elm.on('click', () => { test.hello(); }) } $(() => { onClick($('#btn')) })書き換え後のコード
TypeScriptに書き換えた後のコードです。
拡張子を.ts
に変更し、gulpfile.js
のsrc
で記述したディレクトリにコピペします。型推論で自動で型判別してくれるため、必要のない方は記入していません。
必要に応じて型を設定してください。
今回はonClick
の関数に引数elm
がJQeryであることを明示しています。
JQuery
のJ
は大文字なので気をつけてください。先程インストールした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.jsmodule 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')) })
- 投稿日:2020-03-18T18:09:37+09:00
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-buildNode.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 --versionVue.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
をインストールしておくのも必須のようです。
- 投稿日:2020-03-18T17:00:56+09:00
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.jslet 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.jslet 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で足りるからかもしれません
(スプレットシートのシートに直に書く、という方法もレガシーながらあり)
- 投稿日:2020-03-18T16:05:44+09:00
脚本上で物事が起こる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』
- 投稿日:2020-03-18T14:59:49+09:00
スムーススクロールとフェードインで動きがおかしくなった時
両方使ったら、飛ぶ位置がおかしくなった
ナビバーのメニュークリックで各コンテンツに飛ぶようにしてたのに、各コンテンツをフェードインさせるアニメーションつけたら、違う位置で止まるようになった。
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にアンカーポイントをつけた。
これで解決!
説明が下手すぎるけど自分用だからいいかな
- 投稿日:2020-03-18T14:12:57+09:00
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
- 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.
- 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 JSTBTW 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してメンテするよりほかにないが、一体誰がやるねん。
- 投稿日:2020-03-18T13:59:50+09:00
ボタンが急に効かなくなった
すみません。
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.jsfunction 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; }
- 投稿日:2020-03-18T11:57:22+09:00
Node.jsの設計をつらつらと概観する
株式会社Global Mobility ServiceでソフトウェアエンジニアのインターンをさせてもらっているShirubaです。グローバルな環境で利用されている社会的サービスの開発の一端を担いたい志ある方は、ぜひ緩くお話ししましょう〜。バックエンドはNode.jsを使っています。?♂️→ 採用ページ
Node.jsについて色々資料を読んでメモをとったりしていたので、一度まとめておきたくて、この記事を書くことにしました。V8やLibuvなど低レイヤ技術の設計をベースにNode.jsを概観していきます。
Node.jsとは
Node.js公式によるNode.jsの定義は以下です。
Node.js はスケーラブルなネットワークアプリケーションを構築するために設計された非同期型のイベント駆動の JavaScript 環境です。
https://nodejs.org/ja/about/Node.jsを理解する上で重要な特徴を定義から抽出すると、以下の3つです。
- スケーラブル
- 非同期型
- イベント駆動
この3つの特徴については後で触れていきます。
Node.jsの内部構造
画像引用:https://blog.insiderattack.net/event-loop-and-the-big-picture-nodejs-event-loop-part-1-1cb67a182810Node.jsは、いくつかのモジュールを組み合わせて構成されています。Node.jsを理解する上で重要なのは「V8」と「Libuv」です。この2つが、サーバーサイドでのJavascript実行環境を作っています。(クライアントサイドでは、chrome組み込みのv8とhtml5(イベントループ等を提供)でJavascript実行環境が実現されているそう。)
V8
どうでもいいですが、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
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の内部は以下のようにデザインされています。
画像引用: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_bindingNode.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の場合のサーバの典型的なコードは以下のようになる。
[画像引用:https://www.slideshare.net/NodejsFoundation/nodes-event-loop-from-the-inside-out-sam-roberts-ibm]acceptというシステムコールを通して接続されたコネクションをpthread_createで別のスレッドに渡して、別のスレッドでそのコネクションを処理させます。メインスレッドは、acceptでのブロッキング状態にすぐに戻り、ユーザーからの新しい接続に備えるという流れです。
つまり、ユーザーからのコネクション1つにつきスレッドを1つ作成して、そのスレッドでコネクションに対応しているという訳です。これだとスレッドの無駄使いだし、コンテキストスイッチも発生してしまいます。
このサーバアーキテクチャを図で表すと以下のようになります。
[画像引用: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モデルが存在します。
- ブロッキングI/O
- 非ブロッキングI/O
- I/Oの多重化(selectとpoll)
- シグナル駆動I/O(SIGIO)
- 非同期I/O(Posix.1のaio_関数群)
Node.jsで使われているのは「非同期I/O」です。
処理をカーネルに任せ、処理が完了したらカーネルが元のスレッドに通知をよこすというI/Oモデルです。ちなみによく聞く「ノンブロッキングI/O」は以下のようなI/Oモデルです。
図から分かるように、アプリケーション側からカーネルに「データの準備が完了したか」を尋ねる作業をループで繰り返す必要があり、リソースが勿体無いので、イベント駆動型では非同期I/Oモデルが採用されています。
この非同期I/Oモデルを用いることで実現されるのが「イベントループ」です。通知を発生させるイベントを常にループ文で監視していることから「イベントループ」です。また、このおかげでユーザーからのコネクションをシングルスレッドで処理することが可能になります。
画像引用:Node.jsデザインパターン第2版リアクタパターン
このイベントループを用いたイベント駆動型モデルは、リアクタパターンと呼ばれます。(非同期I/Oを用いたイベント駆動型モデルなので、プロアクタパターンと呼ぶのだろうか。「Node.jsデザインパターン第2版」に沿って、ここではリアクタパターンと呼ぶことにします。)
リアクタパターンの定義は以下。
リアクタパターンではI/Oの処理はいったんブロックされる。監視対象のリソース(群)で新しいイベントが発生することでブロックが解消され、この時、イベントに結びつけられたハンドラ(コールバック関数)に制御を渡すことで呼び出し側に反応(react)する。
引用:Node.jsデザインパターン第2版Node.jsでは、非同期処理を使う場合、イベントにコールバックを持たせて、イベントが終了したものからコールバックを実行しています。ちなみに、Javascriptの関数は第1級オブジェクトなので、関数にコールバック関数を持たせるのが非常に容易です。
リアクタパターンを図で表すと以下のようになる。
画像引用: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のイベントループは、いくつかのフェーズから構成されています。このフェーズごとの挙動は、ここでは省略させてもらいます。イベントループに関する分かりやすかった図を載せておきます。
[画像引用:https://drive.google.com/file/d/0B1ENiZwmJ_J2a09DUmZROV9oSGc/view]この図内の「黄色いJSの箱」の部分を詳細に見ると以下のようなループになっています。
[画像引用: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モジュールを使ったコードを見てみます。
[画像引用:https://www.slideshare.net/nebrius/the-nodejs-event-loop-not-so-single-threaded]ここでcrypt.pdkdf2は非同期に実行されています。(引数の最後に、非同期処理特有のcallbackであるarrow関数が見られます。これが無ければcrypt.pdkdf2は同期処理で実行されます。)for文でループさせてcrypt.pdkdf2を2回使用していることに注意して下さい。これをマルチコアで実行すると、実行にかかる時間は以下のようになります。
[画像引用:https://www.slideshare.net/nebrius/the-nodejs-event-loop-not-so-single-threaded]同じくマルチコア環境で、今度は繰り返し回数を4回にしてみると以下のようになります。
[画像引用:https://www.slideshare.net/nebrius/the-nodejs-event-loop-not-so-single-threaded]マルチコアで実行しているため前の場合と比べて2倍の時間がかかってしまっていることに注意です。また、preemptiveなマルチタスクとして処理されている(それぞれのタスクを割り当てられたtime sliceごとに実行していく)ので、4回全て同じくらいの処理時間で終了しています。
今度は繰り返し回数を6回にすると実行時間は以下のようになります。
[画像引用: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モジュールを使った例も動画で紹介されていたので、見ていきます。
[画像引用:https://www.slideshare.net/nebrius/the-nodejs-event-loop-not-so-single-threaded](上のスライドではfor文を2回繰り返していますが、)for文を6回繰り返した場合の実行時間は以下です。
[画像引用: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を使うのかっていう話になりますが、以下の画像を参照してください。
[画像引用: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と載っています。
V8
Libuvを一通り見たところで、次はV8を見ていきます。V8は、Javascirptで書かれているソースコードを受け取って、それを解析しcompileして実行します。
V8の機能群
[画像引用:JavaScript V8 Engine Explained]
- コードを実行する環境として、call stackとheapという概念があります。Javascriptはシングルスレッドで実行される言語であり、call stackは1つだけです。また、オブジェクトをheapに割り当てたりするわけですが、オブジェクトを使い終わったのに割り当てたメモリを解放しないとメモリリークが起きちゃいます。そこでOrinocoというGarbage Collectorの出番となります。
- V8はJavascriptで書かれたソースコードを受け取って、それを機械語にする必要があります。その際に使われるのがIgnitionというインタプリタとTurboFanという最適化コンパイラです。
(Liftoffについてはこの記事では触れていません。)
V8の処理の流れ
[画像引用:Understanding V8’s Bytecode]
- ソースコードをV8に渡す
- Perserを使ってソースコードを解析。そしてASTという抽象構文木を作る。
- IgnitionというInterpreterを使ってASTをBytecodeに変換する。(変換されたBytecodeは同じくIgnitionによって実行される。)
- IgnitionはASTをBytecodeに変換しつつ、その時の変換情報を蓄えている。(Profiilng)
- 特定の条件下でコードを最適化するために、Ignitionは、蓄えた情報とBytecodeをTurboFanに渡す。
- TurboFanは、そのコードを最適化された機械語に変換して実行
IgnitionとTurboFanの部分が少しややこしいので、別の画像でも確認しておきましょう。
[画像引用: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
[画像引用: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は以下。
[画像引用:
How JavaScript works: Parsing, Abstract Syntax Trees (ASTs) + 5 tips on how to minimize parse time]ちなみに以下のサイトで、JavascriptコードをASTに変換して見ることができます。
ASTに変換するこのParse作業っていうのは結構時間を食うものらしくて、最適化が重要になってきます。以下の画像は、どれだけParseに時間がかかっているかを示す画像。
[画像引用: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では使われています。
[画像引用: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に変わりました。
[画像引用: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の実行が改善されていきます。
[画像引用:JavaScript engine fundamentals: Shapes and Inline Caches]Bytecodeっていうのは、機械語を抽象化したものです。
[画像引用: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のスコープを超えて保持すべきデータに対してメモリが割り当てられる場所です。
[画像引用:Confused about Stack and Heap?]Javascriptをブラウザで実行する際に、以下のようなエラー画面を見たことがあると思います。
(画像引用: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/103000The 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-nodejsasynchronous 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-PhaseNode.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-99948ad7c957process.nextTick()
https://www.slideshare.net/shigeki_ohtsu/processnext-tick-nodejsNode.jsでのイベントループの仕組みとタイマーについて
https://blog.hiroppy.me/entry/nodejs-event-loopループの中で
https://www.youtube.com/watch?v=cCOL7MC4Pl0&t=1011slibuv
https://www.youtube.com/watch?v=nGn60vDSxQ4Node'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=8aGhZQkoFbQV8
JavaScript V8 Engine Explained
https://hackernoon.com/javascript-v8-engine-explained-3f940148d4efUnderstanding V8’s Bytecode
https://medium.com/dailyjs/understanding-v8s-bytecode-317d46c94775Explaining JavaScript VMs in JavaScript - Inline Caches
https://mrale.ph/blog/2012/06/03/explaining-js-vms-in-js-inline-caches.htmlAn 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-b0bacc073cfHow 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-abfcf7e8a0c8Confused 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-f357378cc922JavaScript Internals: Execution Context
https://medium.com/better-programming/javascript-internals-execution-context-bdeee6986b3bHow Does JavaScript Really Work? (Part 1)
https://blog.bitsrc.io/how-does-javascript-really-work-part-1-7681dd54a36dHow 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=14V8 javascript engine for フロントエンドデベロッパー
https://www.slideshare.net/ssuser6f246f/v8-javascript-engine-forJavaScript engines - how do they even? | JSConf EU
https://www.youtube.com/watch?v=p-iiEDtpy6I&feature=youtu.be&t=722V8: an open source JavaScript engine
https://www.youtube.com/watch?v=hWhMKalEicY&feature=emb_titleMarja Hölttä: Parsing JavaScript - better lazy than eager? | JSConf EU 2017
https://www.youtube.com/watch?v=Fg7niTmNNLgV8公式
https://v8.dev/blog
- 投稿日:2020-03-18T11:33:21+09:00
なんとなく、簡単に入門できそうな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のビューが出てきます。こんなの。
とりあえずこれで問題なく動くことは確認できました。
このテンプレートをそのまま加工してもいいですが、とりあえず今回はネイティブな状態に戻すためsrc/
にあるものを全て消して、index.js
を新規作成してください。
おそらくエラーが出ますが一旦無視で構いません。実際に作る-HTMLをレンダリングしてみる
先程作った
index.js
に追記してみてください。index.jsimport 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
にある)にレンダリングされる、という流れになっています。実際に追記されるとこのように出ます。
これだけしか出ませんが、正常です。次に進みましょう。
ここまでで覚えておくこと
- コンポーネントは関数もしくはクラスで宣言して作る
- コンポーネント命名は頭文字が大文字である必要あり(小文字だとHTMLと解釈される)
- コンポーネントを使うときは
<ComponentName />
のようにタグっぽい文法で書く- HTML(XML)を記述して
return
することでレンダリングされる実際に作る-表を作ってみる(+コンポーネントを分ける)
多分ネイティブなReactを触る上で一番恩恵デカイのが表の生成だと思うのでそれで試してみようと思います。
まずは表を表現するためのコンポーネントを新しいファイルに分けて生成します。
今回はtable.jsx
というファイル名で作ります。table.jsxconst Head = () => { // theadの構成を作る }; const Body = () => { // tbodyの構成を作る }; export default function Table(){ return( <table> <Head /> <Body /> </table> ) };
index.js
でコンポーネントを受け取れないと困るので、テーブル本体を返すコンポーネントのみexport
修飾子で渡せるようにしておきます。default
は気にすんなそして
index.js
で受け取りをして、レンダリングをさせます。index.jsimport 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の得意分野を活かしてHead
とBody
の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);
と書いて保存してみてください。
こんな感じでコンポーネント側に配列が渡されていることが確認できます。
あとは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です。
また、配列を減らしたり増やしたりして行が増減するのを確認してみてください。
ここまでで覚えておくこと
- コンポーネントを小分けにしつつ、propsで値をバケツリレーして作ると煩雑化が防げる
- propsを使う際はコンポーネントの引数を入れることを忘れないようにする(必要なければ入れなくてOK)
おわりに
今回はReactのさわりのさわりだけ解説してみましたがいかがでしょうか。
Reactは自分でコンポーネントを作るだけでなく、npmからパッケージを引っ張ってimportすることでそのまま利用することもできます。
その時の話はここで纏めてありますので、そちらもよかったらぜひご覧いただければと思います。ちなみに今回のサンプルで作ったソースコードは以下に全て放り込んでおいたので、上手く行かなかった場合は見比べてみてください。
https://github.com/huequica/qiita_react_sampleそれでは今回はこの辺で。
- 投稿日:2020-03-18T11:30:01+09:00
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) } }
- 投稿日:2020-03-18T11:25:18+09:00
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"]
- 投稿日:2020-03-18T09:18:49+09:00
普通じゃ満足できない脱 webpack マニアのあなたに贈る Snowpack
Snowpack を使い React + TypeScript な環境構築をする具体的な設定を紹介します。
Snowpack is 何
npm パッケージを、ブラウザーから読み込めるよう ES modules 形式1に変換してくれるバンドラーです。
次のように使います:
npm install
後にsnowpack
コマンドを実行し、node_modules 内のパッケージをブラウザーから読み込めるようバンドルする。たとえばnode_modules/react
をweb_modules/react.js
のようなファイルにまとめる。- アプリケーションコードは 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.tsximport 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.tsximport 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.tsximport 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#prepare
はnpm 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 -- --watch
とnpm run server
をそれぞれ別のターミナルで実行します6。http://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.jsmodule.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 設定として、
react
をpreact/compat
に解決してバンドルするプラグインを指定します。Alias プラグインを追加することにより、React を peerDependency に持つようなパッケージ(React-Router や styled-components など)を Snowpack によってバンドルできるようになります。好きなパッケージを試してみてください:
snowpack.config.jsconst 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 への移行を検討すれば、スピードと柔軟性の両方の恩恵を受けられると思います。
参考
- モジュールバンドラを使わないモダン Web 開発を実現する「Snowpack」 - Qiita
- Snowpack で実現する未来のフロントエンド開発 - Qiita
- ESModules について - Qiita
- ES6のexportについて詳しく調べた - Qiita
npm install some-package
で新規パッケージを追加したときには実行されません。改めてnpm install
するか、npm run prepare
するかが必要です。 ↩できるものの、自動判定がうまくいかなかったときのデバッグが面倒なので、明示的に指定するのをおすすめします。 ↩
デフォルト値は
"*": ["node_modules/@types/*", "node_modules/*"]
なので、設定なしのときはreact
と書けばnode_modules/@types/react
に解決されます。webpack を使うときはあまり意識しない設定値かもしれません。 ↩
--watch
込みの scripts を追加したり、https://www.npmjs.com/package/concurrently を使ったりするとスマート。 ↩このために .babelrc.json ではなく .babelrc.js が必要です ↩
React はマルチプラットフォームに対応しているものの、現実のプロダクトでは web 専用で使うケースが多いと思っていて、それならば軽量な Preact で代替するのは良い選択肢だと思います。Hooks も使えますし。 ↩
rollup.js が出てきたのは、Snowpack の中身が rollup.js だからです。 ↩
とはいえ webpack エコシステムは強力なので、そこに乗っかるのはまったく悪い選択肢ではないです。よほどマイナーなローダーを使わない限りほぼ問題はないでしょう。 ↩
- 投稿日:2020-03-18T08:05:39+09:00
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
- 投稿日:2020-03-18T07:46:15+09:00
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); // objectnull だけ 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
for
やwhile
で特定の場合の時にスキップさせたり、処理を抜けたい時に使う構文がある。
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;スコープ(有効範囲について)
ブロック内で定数や変数が宣言されて場合は基本的にはそのブロック内だけで有効となる。
例として関数、if
やfor
、while
でもブロックのあるところでは定数や変数のスコープが分かれる。したがって、ブロック外で呼び出そうとすると未定義となり有効にならないルールがある。
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を使う場合は、ブロックで囲んであげる事を注意しておきましょう。
- 投稿日:2020-03-18T07:00:29+09:00
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;ツールバーにメニューが表示されています。デフォルトでは以下のメニューが表示されます。
メニュー 説明 表記 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;マークダウンエディタを拡張する
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;
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;これで、エディタで入力したマークダウンを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;
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
の方が個人的には好みです。
もし使用する機会があれば検討してみてください。
- 投稿日:2020-03-18T03:20:38+09:00
[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行ずつ挟んで確認すれば原因を特定しやすくなり、エラーを見つけることができます。
- 投稿日:2020-03-18T02:17:16+09:00
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"
- 投稿日:2020-03-18T02:09:52+09:00
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なら配列のサイズがすぐに分かって、
- 値の格納場所にダイレクトにたどり着ける
そんなデータ構造のシリアライザが欲しい!
ということで作ってみましたよ。使い方(sample.js)もGistに載せてますが一応再掲。
一般的なシリアライザ(JSON/messagePack等)と同じようにごくシンプル。sample.jsconst 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長くなったのでいったんここまで。
工夫ポイント、ベンチマークなどは次回!
そんなヘビーな用途にNode.jsを使うなんて、なんてツッコミはなしの方向で。 ↩
OSの機能でプロセス間共有メモリってのがありますからそういうので共有できないことはないです。Node.jsから使う猛者がいるかどうかは知りませんが。 ↩
昔の知識なので最新は違うかもしれないしOSにもよるんでしょうが、プロセスをforkするとすぐにはメモリはコピーされなくて(つまり共有されていて)、書き込んだ時点でページ単位でコピーしてそれぞれの道を歩むようになってました。大部分のメモリに書き込みが発生しないならそれに頼るのも一つの見識ですね。 ↩
そういうデータを使うのって機械学習とかCG系とかのイメージ。そういう用途Node.js使いますかね・・・。 ↩
あと、JSON.stringify/parseはJSエンジンネイティブ実装だし、最適化されまくってるので速さで勝てません。憎いです。 ↩
- 投稿日:2020-03-18T00:39:35+09:00
Trelloの完了したタスクをGASで自動アーカイブする
はじめに
仕事のタスク整理にTrelloを使いますが、完了したタスクを手動でアーカイブすると判断に迷ったり、手間を感じることがあるので、完了してから一定期間経過したものをGoogleAppsScript(GAS)で自動アーカイブするようにしました。
準備
TrelloのAPIをGASで使用するにはAPIキーやトークンの取得が必要になります。
方法はこちらの記事を参考にしてください。処理の流れ
- 完了のリストにあるカードを取得する
- 取得したカードの最終更新日が7日経過していれば、アーカイブする
- アーカイブしたカードの情報をスプレッドシートに出力(追記)する
コード
①カードの取得とアーカイブ対象の選定
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時くらいに起動するように設定しました。最後に
タスク管理は運用ルールを決めて実施しないと意味がなく、手動だと忘れてしまうことがあるので、自動化できるものは手作業から無くしてしまいましょう!
参考リンク
- TrelloのカードをGASから自動でアーカイブする。
- 初心者向け実務で使えるGoogle Apps Script完全マニュアル
- 日付&時刻の便利ライブラリ「Moment.js」をGoogle Apps Scriptで使う方法
- GASを使ってTrelloのボードID・リストID・カードIDを取得する方法
- GAS版Moment.jsライブラリで超簡単に日時の比較をする方法
- 時限式のイベントトリガーを設置して決まった時刻にBotを送信する方法
- GASでログ出力する2つの方法(Logger.logとconsole.log)の紹介と使い分け