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

Promiseは何時呼ばれるのか?

Promiseは何時呼ばれるのか?
使う分には今まであまり意識してこなかったのですが、async/awaitを呼ぶことで処理がブロッキングされるのではないかというのを懸念していたのと、そもそもどのタイミングでPromiseのcallbackがされるのか気になったので今更ですが調べてみました。
Promiseが呼ばれる仕組みについては先にEventLoopとmicrotaskについて知る必要があります。

先に結論から書くと以下の感じです。

  • PromiseはEventLoop内のmicrotaskキューでFIFO実行される。
  • Timer系の処理(setImmediateやsetTimeout)はmicrotaskが全て実行された後に実行される。(つまり、setTimeout(fn, 0)はmicrotaskを全て実行した後にfnを実行するという意味)
  • async/awaitはPromiseの箇所でsuspendしているに過ぎない(イベントループをブロッキングするかどうかはPromise内部の処理に依存する)、ジェネレータ文法(yield)やコルーチンの概念と同じ。
  • NodeJS v12以降ではasync/awaitによるパフォーマンスの劣化は改善されているので、処理の実行順序の見やすさ的に積極的に書いて問題ない。(ただし、async/await関係なしに待たなくて良い処理に関してはレスポンスを返した後に実行すべしなのとI/O系のSyncメソッドは使わないほうが良い)
  • ブラウザでもPromiseはqueueMicrotaskによって実装されている(そもそもPromise、async/awaitサポートされてないブラウザもまだ生き残っているのでトランスパイル必須だが)

NodeJSの場合

NodeJSはV8エンジン(Google ChromeのChromiumでも使われている)によるJavaScriptで動く実行環境です。
元々はC10K問題(サーバーのハードウェア性能は問題ないにもかかわらず、クライアントの同時接続数が多くなるとサービスの応答が遅くなる)を解決するバックエンド環境(アプリケーションサーバ)として開発されました。
C10K問題はハードウェア性能ではなく、OSの制限によって引き起こされるクライアント同時接続数の上限です。

  • プロセス数の上限
  • コンテキストスイッチ(切り替え)のコスト
  • ファイルディスクリプタの上限

これらの問題を解決するためにNodeJSはシングルプロセス・シングルスレッドでリクエストを捌くという設計になっています。(実際にはマルチプロセスもマルチスレッドも作れるのですが根本的な設計思想はこれです。)
シングルプロセス・シングルスレッドで全部のリクエストを処理することでプロセス数の上限にもひっかからず、大量のマルチプロセス、マルチスレッドでのコンテキストスイッチも発生しない。
DBコネクションもマルチプロセス単位で都度接続するのでなく、シングルスレッド内で使いまわしをすることでファイルディスクリプタの上限にならない。
ただ、ファイルの読み書きに関してはシングルスレッドで行うと他の処理をブロッキングするほど重たいため、非同期でのI/Oをサポート・推奨しています。
シングルスレッドで飛んでくるリクエストを管理するための仕組みがイベントキュー(イベントループ)です。
従来の1リクエストにつき1プロセス立ち上がるマルチプロセス型のアプリケーションサーバに対し、
シングルプロセス、シングルスレッド内でイベントキューにリクエストを順次詰め込み、DBやファイルからのデータ取得を非同期に取得し、レスポンスを返すことを可能にしています。

event-queue.png

参考:いまさら聞けないNode.js

以上により、NodeJSはシングルプロセス・シングルスレッドによるイベントループでC10K問題を解決しています。
ただし、シングルスレッドならではの問題として
CPU負荷が高いループ処理などが発生するとイベントループをブロッキングしてしまい、全てのリクエストに遅延や最悪レスポンスが返って来ないほどの影響を与えてしまいます。

さて、ようやくNodeJS上でPromiseがいつ呼ばれるのかという話しなのですが

setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
(() => console.log(5))();

実行結果は次のようになります。
同期実行→nextTick→Promise(microtask)→setTimeout→setImmediate
の順番です。

5
3
4
1
2

(() => console.log(5))()のみ同期タスクで他は非同期タスクです。
同期タスクが一番最初なのは理解しやすいとして、非同期タスクの順番はどのように決まっているのでしょうか?
これはなぜなのかはもう少し踏み込んでイベントループについて見てみる必要があります。
イベントループはlivuvにより実装されていて、
NodeJSが起動すると以下のイベントループが初期化されます。
(ただし、nextTickQueue、microTaskQueueはNodeJS側で実装されている)

20180925232715.png

まず、イベントループに入る前にもしくはイベントループの各フェーズの後にキューのタスクがある場合はキューが空になるまで実行されます。
(イベントループはシングルスレッドで複数のtaskを同時に処理することはできないため)

  • nextTickQueueは全てのキューの中で最速に処理される→nextTickが実行される
  • microTaskQueueはnextTickQueueが空になり次第、実行(Promisesオブジェクトのコールバックはここに所属)→Promiseが実行される

キューが解消されるとTimerフェーズからイベントループに入ります

  • TimerフェーズでsetTimeoutが呼ばれる
  • CheckフェーズでsetImmediateが必ず呼ばれる

なので同期実行→nextTick→Promise(microtask)→setTimeout→setImmediateとなります。

参考:Node.jsでのイベントループの仕組みとタイマーについて

先程シングルスレッドならではの問題点として、他の処理をブロッキングしてしまうという問題点をあげました。
次の例のようにasync functionだとしても裏側はシングルスレッドで動いているため、高負荷な処理を実行すると全てのリクエストをブロッキングしてしまいます。

app.get('/compute-async', async function computeAsync(req, res) {
  log('computing async!');

  const hash = crypto.createHash('sha256');

  const asyncUpdate = async () => hash.update(randomString());

  for (let i = 0; i < 10e6; i++) {
    await asyncUpdate();
  }
  res.send(hash.digest('hex') + '\n');
});

実は次のようにsetTimeoutを挟むことで他のリクエストをブロッキングすることなく、
高負荷な処理を継続することができます。
先程のイベントループが理解できていれば、Promise await(microtask)の間にsetTimeoutを挟むことでmicrotaskに大量の処理を全て詰め込んでから実行するのでなく、microtask→setTimeout→microtask→setTimeoutとインターバルを挟むようになるので他の処理をブロッキングするのを防ぐことができます。
(あくまでイベントループの仕様に基づいた回避策なのでそもそも重すぎるCPU処理の実行を直接サーバ上で行うのはNodeJSは向いていません)

app.get('/compute-with-set-timeout', async function computeWSetTimeout (req, res) {
  log('computing async with setTimeout!');

  function setTimeoutPromise(delay) {
    return new Promise((resolve) => {
      setTimeout(() => resolve(), delay);
    });
  }

  const hash = crypto.createHash('sha256');
  for (let i = 0; i < 10e6; i++) {
    hash.update(randomString());
    await setTimeoutPromise(0);
  }
  log('done ' + req.url);
  res.send(hash.digest('hex') + '\n');
});

参考:Node.js: How even quick async functions can block the Event-Loop, starve I/O

このようにNodeJSでは1つの処理がボトルネックになってサーバ全体のパフォーマンスを下げてしまうという危険があります。
後は同一プロセスで実行し続けるため、メモリリークが起きるとサーバの継続実行ができなくなってしまうのでデバッグツールや計測ツールでどこに問題があるのか調査する必要があります。

参考:0から始めるNode.jsパフォーマンスチューニング

さて、Promiseが何時実行されるかはわかりました。
(EventLoopの合間のmicrotaskキューで実行される)

microtasks-vs-tasks.png

ではasync/awaitで実行した場合はどうなるでしょうか?
async/awaitの関数は次のような関数に変換されてV8上で実行されます。
suspendされて、microtaskキューの実行でsuspendの戻り値を返却し、
implicit_promiseをPromiseの結果として最終的に返します。

await-under-the-hood.png

後は懸念すべき点のasync/awaitを使った場合のパフォーマンス低下ですがNodeJS v12以降では素のPromiseとほぼ問題ないくらいの速度は出ているのでパフォーマンスを気にすることなく積極的にasync/awaitは使って良いレベルだと思います。

benchmark-optimization.png

つまり、async/awaitそのものでパフォーマンス低下やイベントループがブロッキングされるということはないです。
(Promise内部の実装次第ではありえますが)

参考:Faster async functions and promises

ブラウザの場合

ブラウザの場合もJavaScriptの実行フローはNodeJS同様EventLoopに基づいています。
PromiseやMutationObserverはmicrotaskで実行されます。

setTimeout(() => console.log("0"));

Promise.resolve()
  .then(() => console.log("1"));

console.log("2");

NodeJSのときと同様に同期処理→Promise(microtask)→setTimeout(Timer)
の順番に実行されます。

2
1
0

もう一つ大事な点はDOMのイベントハンドリングやレンダリングを挟む場合は
すべてのmicrotaskは他のイベントハンドリングやレンダリングやTimer系処理の前に完了します。
(つまりイベントハンドリングしている最中にPromiseのネットワーク処理でデータが書き換わるなんてことはない)

スクリーンショット 2020-06-21 21.04.17.png

参考:イベントループ(event loop): microtask と macrotask

ChromeのWebWorkerを実装したJake氏のブログのほうがサンプル付きで実行例がわかりやすいかもしれません。
補足としてはブラウザ間の差異は解消され、FireFox、Safari、EdgeはすべてChromeと同じ挙動になっているのを確認しました。(2020/06/21現在)
元記事自体が2015年と古いため、古いブラウザでは差異があったのでしょう。

参考:Tasks, microtasks, queues and schedules

ちなみに以上がわかると次記事の上級問題が解けます
参考:何問分かる?Promise に関するクイズ13問【解説付き】

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

React hooks 【シュチュエーション別 hooks利用方法と使い方まとめ】

モダンReactの代表になっているhooksですが、使いこなせるととても便利です。
今まで、クラスコンポーネントや
SFC(Stateless Functional Component)として高階コンポーネントやレンダープロップス、Recomposeなどを使っていた方は使い勝手の良さに驚くでしょう。

また、これからReactを学習する人に向けてもhooksを使いこなすことはとても価値があり、
FC(Function Component)+hooksから入門するべきだと思います。
実際の現場でclassやSFCはレガシーであり、FC+hooksへの移行が進んでいるからです。

復習がてら、どんなhooksがあるのか、どんな時にどう使うのかをまとめました。

前提として

React version 16.8.0 以降
hooksはFCでないと使用できない

Hooks.jsx
import React, { useState, useEffect, useContext } from 'react'


const hooks = () => {
  //関数コンポーネント内にhooksを書く

  return null
}

読み込みが必要ですがここから先ははしょりますので、上記のように読込んでください。
またはReact.hooksの名前でも可能です。

コンポーネント内で状態管理をしたい

useState

const hooks = () => {
  const [count, setCount] = useState(0)
  return(
    <>
      <div>{count}</div>
      <button onClick={() => setCount(count + 1)}>ボタン</button>
    </>
  )  
}

ステートフルな値と、それを変化させるための関数を分裂代入します。
上記は、ボタンを押すたびにsetCountによって状態が変化して数字が増えていきます。

また、useStateの第一引数はinitialState(初期値)となっていて、ロジックが含まれている場合は、関数を渡すこともできます。

  const [count, setCount] = useState(()=> 1*2*3)

オブジェクトの更新の仕方に注意

クラスコンポーネントの setState メソッドとは異なり、下記では正常にstateが更新されません。

const hooks = () => {
  const [user, setUser] = useState({ name: "test", age: "20" })
  return (
    <>
      <div>{user.name}</div>
      <button onClick={() => 
        setUser({user.name = 'testNEW'})
      }>
        ボタン
      </button>
    </>
  )
}

consoleでuserを出すとtestNEWが出力されてしまいます。
正常にnameの値のみ変更するためには、下記のようにスプレッド構文と併用してマージさせるように書きます。

const hooks = () => {
  return (
    <button onClick={() => 
      setUser({...user, ...{ name: "testNEW"}})
    }>
      ボタン
    </button>
  )
}

renderされた後に何か処理をしたい

useEffect

いままでcomponentDidMountをつかって、render後にAPIリクエストを行うといったアクションを取っていたと思いますが、useEffectを使ってそれを実現できます。

正確に言うとcomponentDidMountcomponentDidUpdatecomponentWillUnmount がまとまったものだと考えることができます。

const hooks = () => {
  const [user, setUser] = useState({ name: "test", age: "20" })

  //APIリクエストのサンプルです
  const sampleGetRequest = () => {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve({ name: "NEW" })
      }, 2000)
    })
  }
  //render後に発火
  useEffect(() => {
    sampleGetRequest().then(response => {
      setUser({ ...user, ...response })
    })
  }, [])

  return <div>{user.name}</div>
}

上記のコードを実行すると、renderされてから2秒後に
useStateで持っている、userが更新され、viewが書き換わります。

第二引数について

第二引数を引数なし、空の配列、値を入れた配列で使い分けることによって、3種類の使い方ができます。

引数なし
useEffect(() => {})

コンポーネントがマウントされた後、更新された後に関わらず、毎回のレンダー時に処理を実行します。
componentDidMount と componentDidUpdateを併用しているのと似ています。

空の配列
useEffect(() => {}, [])

コンポーネントが初回にマウントされた後のみ処理を実行します。
componentDidMountを記述しているのと似ています。

値を入れた配列
const [name, setName] = useState('test')
const [age, setAge] = useState(25)

useEffect(() => {
  console.log(`${name}${age}才だ`)
}, [count])

配列内に入れた値を監視してくれます。
上記の例だと、配列の中に記入したcountのデータの内容が書き換わった時に処理が実行されます。
なので、ageのデータ内容が更新されても何も起きず、countが更新された時のみlogが出るようになります。

無限ループに注意

引数を無し、配列に値を入れた引数の場合、無限ループになる可能性があります。
useEffectの説明の最初のコードの引数を無くしてみました。
これだと無限ループに陥って、処理を繰り返してしまいます。
レンダー => setUser({ ...user, ...response }) => レンダー => setUser({ ...user, ...response }) => レンダー => 続く...

const sampleGetRequest = () => {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve({ name: "NEW" })
      }, 2000)
    })
  }

useEffect(() => {
  sampleGetRequest().then(response => {     
    setUser({ ...user, ...response })
  })
})

なぜならsetUser({ ...user, ...response })によってuserの参照は毎回切れてしまうからですね。
{ name: "NEW", age: "20" }から{ name: "NEW", age: "20" }に変更されても、値は変わってないように見えて、参照は変わっているので、仮想DOMの差分を検知して再レンダーしてuseEffect発火という流れができてしまいます。
空の配列を引数に指定すれば治ります。
useEffect内で状態を変化させる際は気をつけましょう。

もっとuseEffectについて詳し知りたい方はuseEffect完全ガイドをご覧ください。

コンポーネントツリー内で、値を共有したい

useContext

通常だとデータを親から子に渡す際、propsとしてバケツリレーで渡していく必要があります。
ですが、useContextを使えば、バケツリレーの必要がなく、一気に下層コンポーネントに値を渡すことができます。
多くの階層を経由していくつかの props を渡すことを避けたいときはうってつけです。
クラスコンポーネントでもコンテクストは使えましたが、それのhooks版という感じです。
下記は、APP.jsxからTitle.jsxまで一気に(Header.jsxを飛び越えて)値を渡している例です。

App.jsx
import React, { createContext } from "react"
import Header from "./Header"

//最上部のコンポーネントではクラスコンポーネントと同じように、
//値を共有するためのコンテクストをcreateContextで作成
export const PageMetaContext = createContext("");

const App = () => {
  const PageMeta = {
    title: "タイトル",
    description: "詳細です"
  };

  return(
  //Header以下のツリーに共有できるようにプロバイダを使用、
  //valueに値を設定することで、ツリー内のどの子コンポーネントにも渡せる(今回は PageMeta)
    <PageMetaContext.Provider value={PageMeta}>
      <Header />
    </PageMetaContext.Provider>
  )
}

export default App;

値を使用する必要のないコンポーネントは何も特別なことはしなくてもいい

Header.jsx
import React from "react"
import Title from "./Title"

const Header = () => (
  <header>
    <Title />
  </header>
)

export default Header;

親のプロバイダで設定された値を使いたいコンポーネントのみuseContextを使う
注意すべき点は、useContext に渡す引数はコンテクストオブジェクト自体であること

Title.jsx
import React, { useContext } from "react";
//ツリーの親で作成されたコンテキストを読み込む
import { PageMetaContext } from "./App";

const Title = () => {
//ここでuseContextを使う
  const { title, description } = useContext(PageMetaContext);

  return (
    <>
      <h1>{title}</h1>
      <span>{description}</span>
    </>
  )
}

export default Title;

これで親で設定したPageMetaの中の文字列がviewに表示されます。

useContext に渡す引数に注意

useContext に渡す引数はコンテクストオブジェクト自体でなければいけません。

上記の例だと、titleのみ使いたいからといって、

const title = useContext(PageMetaContext.title);

にするとエラーが起きてしまうので気をつけましょう。

複数の複雑なstateを1つにして、templateをスッキリさせたい

useReducer

useReducerはuseStateと同じく、コンポーネント内で状態管理をするためのhookで、
useStateの状態管理をより堅牢であり、複雑なロジックが絡んだステートを更新するのに適しています。
Reduxに馴染みがあれば簡単ですが、初見だとわかりにくく使用するのにも気がひけるので順をおって説明します。

下記のようなテキストボックスに入力するした値をリストにできるサンプルをつくりました。

スクリーンショット 2020-06-20 19.58.12.png

2つのコンポーネントによって構成され、
下記はuseRecucerをもち、子コンポーネントからの入力によってリストをレンダーさせる親側のコンポーネントです。

Todo.jsx
import React, { useReducer } from "react"
import InputArea from "./InputArea"

const initialState = { input: "", items: [] }

const reducer = (state, action) => {
  switch (action.type) {
    case "updateInput":
      return { ...state, input: action.payload };
    case "resetInput":
      return { ...state, input: "" };
    case "addItem":
      return { ...state, items: [...state.items, action.payload] };
    case "removeItem":
      const filteredItems = state.items.filter(v => v.key !== action.payload);
      return {
        ...state,
        items: filteredItems
      };
    case "resetItems":
      return initialState;
    default:
      throw new Error();
  }
}

const Lists = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <ul>
        {state.items.map((item, index) => (
          <li key={index}>
            {item.title}
            <button
              onClick={() =>
                dispatch({ type: "removeItem", payload: item.key })
              }
            >
              削除
            </button>
          </li>
        ))}
      </ul>
      <InputArea input={state.input} dispatch={dispatch} />
    </div>
  )
}

export default Lists

まずuseReducerの宣言部分ですが、

const [state, dispatch] = useReducer(reducer, initialState);
  • state :ステートフルな値
  • dispatch :値を更新したいという旨をreducerに通知するための関数
  • useReducerの第1引数のreducer:値を更新するためのロジックが書いてある関数
  • useReducerの第2引数のinitialState:値の初期値、関数でもOK。

実際にレンダリングのために扱う値は、stateの中身になります。

値の更新は、reduceractionの2つの重要な役割によって行われます。

reducer

const reducer = (state, action) => {
  switch (action.type) {
    case "updateInput":
      return { ...state, input: action.payload };
    case "resetInput":
      return { ...state, input: "" };
    case "addItem":
      return { ...state, items: [...state.items, action.payload] };
    case "removeItem":
      const filteredItems = state.items.filter(v => v.key !== action.payload);
      return {
        ...state,
        items: filteredItems
      };
    case "resetItems":
      return initialState;
    default:
      throw new Error();
  }
};

reducer関数は、actionのtypeによって行う処理を分岐させるため、switch文で書きます。

引数の中身は

  • state:現在のステート
  • action:typeとpayloadが入ったオブジェクト
    • type:"updateInput"のような処理をするための名前
    • pyload:action関数から渡させる任意の値

今回のreducerの中には下記のしたい処理が書いてあります。

  • inputの値を更新
  • inputの値を空にする
  • 配列にinputの値を追加
  • 配列から、keyが同じオブジェクトを削除
  • inputと配列の内容を初期化
  • typeがない時エラーを投げる

それぞれのtypeごとに実行される関数から返される値は、新しいステートになります。
それにより、新しいレンダーが行われます。
そして何を元にtypeがわかり、処理が振分けされるのかというと、actionによって行われます。

action

dispatch({ type: "removeItem", payload: item.key })

actionは簡単にいうと、reducerへの更新依頼です。
useReducerで宣言時に代入されたdispatchを使うことで実現します。
{ type: "removeItem", payload: item.key }はreducer内のactionで使うことができます。

下位コンポーネントでstateの更新をさせる

下記は先ほどのTodo.jsxの子コンポーネントで、テキストボックスから値を入力し、stateを更新する役割を持っているコンポーネントです。

inputArea.jsx
import React from "react";

const InputArea = ({ input, dispatch }) => {
  const hundleChange = event => {
    dispatch({ type: "updateInput", payload: event.currentTarget.value });
  };

  const addItem = () => {
    dispatch({
      type: "addItem",
      payload: { title: input, key: new Date().getTime() + Math.random() }
    });
    dispatch({ type: "resetInput" });
  };

  const resetItems = () => {
    dispatch({ type: "resetItems" });
  };
  return (
    <div>
      <input type="text" onChange={e => hundleChange(e)} value={input} />
      <button onClick={addItem}>追加</button>
      <button onClick={resetItems}>リセット</button>
    </div>
  );
};

export default InputArea;

もしuseStateを使っている場合は親コンポーネントからコールバック関数を受け取り、発火させることでstateの更新ができましたが、
useReducerを使っている場合は、dispatchを渡し、子コンポーネント内でstate変更のactionを記述すれば良いので、stete更新のためのロジックが親コンポーネントに溜まらないので、見通しがよくなりますし、コンポーネントの役割が明確になります。
また、コンポーネントツリーが大きくなっている場合は、propsでなく、useContextと組み合わせて使えば、小規模アプリならreduxを使わずに、useContextuseReducerでコードの肥大化を防ぐことができるでしょう。

レンダーごとに計算を実行されないように処理をキャッシュしたい

useMomo

useMemoはメモ化されたを返します。
メモ化というのはプログラムの高速化のための最適化する技術の1つで、処理結果を保持しておき、あるトリガーがあるまで処理を行わずに保持してある値を返すことを言います。

下記の例はuseMomeを使っていないコンポーネントで、

  • テキストボックスの値を入力することで、stateのinputが更新
  • ボタンを押すと、stateのcountの三乗が計算され、consoleが出る

というものです

import React, { useState, useMemo } from "react";

const Memo = () => {
  const [count, setCount] = useState(2);
  const [input, setInput] = useState("");

  const newCount = () => {
    console.log("計算します");
    return Math.pow(count, 3);
  };

  return (
    <div>
      <input
        type="text"
        value={input}
        onChange={e => setInput(e.target.value)}
      />
      <p>{newCount()}</p>
      <button onClick={() => setCount(prev => prev + 1)}>increment</button>
    </div>
  );
};

export default Memo;

上記だとonChangeが発火されるたびに、consoleに計算しますが出力されます。
これは計算結果が変わることはないのにnewCount関数が実行され、無駄な計算を繰り返していることになります。
今回の例は、そこまで複雑な計算ではないですが、もっと複雑になったり、他の処理も重なってくるとレンダリング速度のパフォーマンスの低下に繋がります。

ここでuseMomeを使います

 const newCount = useMemo(() => {
    console.log("render");
    return Math.pow(count, 3);
 }, [count]);

これによりnewCountはメモ化された値の入る変数になり、引数として配列に入っているcountに変更があるまで処理は実行されません。
先ほどの問題は解消され、onChangeが発火して、stateのinputが書き換わっても、再度計算されなくなりました。

useMemoの第二引数の指定に注意

  • 引数なし:全てのstate, propに依存します。結果的に不必要に処理が行われるためメモ化する意味がなくなります。
  • 空配列[]:何にも依存しません。処理が行われるのはレンダー直後のみになります。
  • 値を入れた配列:配列内の変数に変更があるたびに、処理が実行されます。複数入力可能です。useMemoを使う場合は、忘れないようにしましょう。

親からpropsとして渡される関数による無駄なレンダーを避けたい

useCallback

簡単にいうとuseMomeの関数版です。
useCallbackの場合はメモ化された関数を返します。

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

【JavaScript】値がundefined/nullの時だけtrue判定にする

値がundefinednullの時だけtrueとして判定したい(0false判定にしたい)

タイトルの通り、こんな場合どうするか。

[JavaScript] null とか undefined とか 0 とか 空文字('') とか false とかの判定についてに全て書いてあるが、このトピックの部分だけまとめてみた。

if (!value)

まず初めにこれを使っていたのだが、JSでは0false判定なので普通に間違い

勘違いして使っていたらバグの元になってしまう。

if (value === undefined || value === null)

これならOKだが、どこか野暮ったい。

この書き方が最も明示的で誰にもわかりやすいと思うが、もっとシンプルに書きたい場合もあるだろう。

if (!value && value !== 0)

2番目の書き方よりは短くなったが、少し分かりづらくなった気がする。

if (value == null)

この書き方だとvalueの値が0の時はfalse判定になる。厳密等価演算子ではなく等価演算子「==」を使っているため、valueの値がnullundefinedの時は同様にtrueとして判定されるようだ。2、3番目の書き方よりも簡潔な条件式になった。

厳密には次のようなカラクリらしい。

比較演算子 - JavaScript | MDN

等価演算子は、2 つのオペランドが同じ型でないならばオペランドを変換して、それから厳密な比較を行います。両方のオペランドがオブジェクトならば、 JavaScript は内部参照を比較するので、オペランドがメモリ内の同じオブジェクトを参照するときに等しくなります。

ということで、if (value == undefined)でも同じ結果となるが、文字数的にはnullの方が簡潔になる。

否定の時はif (value != null)でOK。(valueがnullとundefined以外の時にtrue判定となる)

ESLintで引っかからないか気になったが、大丈夫だった(設定によっては指摘されるかも?)

等価演算子「==」を使いたくなかったり、分かりやすさを求める場合は2、3番目の書き方が良さそうです。

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

WebでMIDIを記録して保存できるものをつくっておきました

概要

WebでMIDI入力を受け取って記録し、SMF(Standard MIDI File)の書き出しまで行えるものをつくりました。

動作には以下の条件が必要です。
・Web MIDI API対応のブラウザであること(Can I use)
・MIDI入力ができるデバイス等があること

デモ

https://cagpie.net/web-midi-recording/
UIの作りがだいぶアレですが、

  • 1) Web MIDI API start を押すとMIDIデバイスへアクセスを開始し、
    • この状態から、繋いだMIDIデバイスから入力を送ると音が出ます(音が出る部分はソースコード的には Sample にあたります)
  • 2) recording start で記録を開始し、 recording stop で終了してSMFの書き出しが走ります。
  • 3) play で書き出したSMFの再生、また download からSMFのダウンロードができます。
    • playでの再生は、WebAudioAPIを用いてSMFを再生する PicoAudio.js を用いています。

ソースコード

https://github.com/cagpie/web-midi-recording
記録部分のみを本体としており、音がなる部分は本体に入っていません。
上記のデモは Sample の内容になります。

MIDIを記録して保存する とは

例えば、
MIDIデバイスとして使用するピアノの真ん中の方の「ミ」の鍵盤を押したとします。
すると、 0x90 0x40 0x7F というMIDIメッセージが送られます。
(このデバイスの接続と、MIDIメッセージの受け取りにWeb MIDI APIを用いています)

このイベント(MIDIメッセージ)を、押したタイミングと一緒に記録しておきます。

最終的に保存する際に、SMF形式にするのですが、
これは、イベントの内容と、それがいつ実行されるか(正確には前回のイベントからの差分時間)の情報が交互になっているだけです。
ざっくり言いましたが、本当はヘッダ情報なども含める必要があります。

バイナリデータに起こし保存すれば、
DAWやMIDIシーケンスソフトで開いたりすることができます。

おわり

おわりです。
Web×MIDI Web×音楽をもっと見たいです。

参考

MIDIメッセージ一覧
https://www.g200kg.com/jp/docs/tech/midi.html

SMF (Standard MIDI Files) の構造
https://sites.google.com/site/yyagisite/material/smfspec

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

iOS 13 以降ではウェブ上でセンサー値を扱う際にユーザからの許可が必要

はじめに

iOS 12 までは、Safari の設定からセンサー値を利用していいか権限を与えていました。
iOS 13 からは仕様が変わり、サイトごとにユーザーからセンサー値の権限を JavaScript で与える必要があります。
※ 永続化は不可能、セッション(?)ごとに許可を取る必要がある
IMG_0758.PNG
ご覧の通り項目がなくなっていますね…(iOS 13.3.1 にて)

DeviceMotionEvent.requestPermission();
DeviceOrientationEvent.requestPermission();

このコードを実行するとセンサー値の権限を制御できるウィンドウが出てくるのですが
どうやらタップやクリックなどの意図的なユーザの行動から有効化の流れを作る必要があるみたいです…
iOS(特に 13 以降)でのモーションセンサー有効化 - http://dotnsf.blog.jp/archives/1076737232.html
こちらサイト様に有効化の手段は書いてあったのですが、他にも方法がないか検証してみたので共有しておきます。

ユーザに確認を取ったが駄目 ? だったパターン

1. window.confirm

if (typeof DeviceMotionEvent.requestPermission === 'function' && sessionStorage.getItem('isPermission') === null) {
    const isPermission = confirm('このサイトでは、センサー値を扱います。');

    if (isPermission) {
        const isDeviceOrientationEvent = await DeviceOrientationEvent.requestPermission();
        const isDeviceMotionEvent = await DeviceMotionEvent.requestPermission();

        // 許可したあとはまた許可が必要になるまでポップアップが出ないようにする
        if (isDeviceOrientationEvent && isDeviceMotionEvent) {
            sessionStorage.setItem('isPermission', 'true');
        }
    }
}

これでは動きませんでした。
これを禁止にしている Apple さんの意図が正直よくわからない

2. DOM でボタンを生成し、 JS 内で仮想クリックを行う

if (typeof DeviceMotionEvent.requestPermission === 'function' && sessionStorage.getItem('isPermission') === null) {
    const confirmElement = document.createElement('div');
    confirmElement.style.display = 'none';

    confirmElement.onclick = () => {
        const isDeviceOrientationEvent = await DeviceOrientationEvent.requestPermission();
        const isDeviceMotionEvent = await DeviceMotionEvent.requestPermission();

        // 許可したあとはまた許可が必要になるまで生成しないようにする
        if (isDeviceOrientationEvent && isDeviceMotionEvent) {
            sessionStorage.setItem('isPermission', 'true');
        }

        document.body.removeChild(confirmElement);
    }

    document. body.appendChild(confirmElement);
    window.onload = confirmElement.click();
}

ボタンをクリックしたイベントを呼び出して動作させればイケるんじゃないか ? と思いましたが駄目でした。
ブラウザ側でタップやクリックの動作も監視しており、それも同時に実行されてないと呼び出しできない仕様になってるんですかねえ

3. 外部ライブラリのクリック時の動作メソッドを使う

if (typeof DeviceMotionEvent.requestPermission === 'function' && sessionStorage.getItem('isPermission') === null) {
    const tingleLinkElement = document.createElement('link');
    tingleLinkElement.rel = 'stylesheet';
    tingleLinkElement.href = 'https://cdnjs.cloudflare.com/ajax/libs/tingle/0.15.3/tingle.min.css';
    document.head.appendChild(tingleLinkElement);

    tingleLinkElement.onload = () => {
        const tingleScriptElement = document.createElement('script');
        tingleScriptElement.src = 'https://cdnjs.cloudflare.com/ajax/libs/tingle/0.15.3/tingle.min.js';
        document.body.appendChild(tingleScriptElement);

        tingleScriptElement.onload = () => {
            const modal = new tingle.modal({
                footer: true
            });

            modal.setContent('<p>このサイトでは、センサー値を扱います。</p>');
            modal.addFooterBtn('Cancel', 'tingle-btn tingle-btn--default tingle-btn--pull-right', () => modal.close());
            modal.addFooterBtn('OK', 'tingle-btn tingle-btn--primary tingle-btn--pull-right', () => {
                const isDeviceOrientationEvent = await DeviceOrientationEvent.requestPermission();
                const isDeviceMotionEvent = await DeviceMotionEvent.requestPermission();

                // 許可したあとはまた許可が必要になるまでポップアップが出ないようにする
                if (isDeviceOrientationEvent && isDeviceMotionEvent) {
                    sessionStorage.setItem('isPermission', 'true');
                }

                modal.close();
            });

            modal.open();
        };
    };
}

今回、Tingle.js というモーダルプラグインを使用した際にハマった点です。
クリックイベントも取ってるだろうし、これなら大丈夫っしょ!!って思って書きましたが駄目でした。
外部ライブラリを使いたい際は気をつけたほうが良いかもしれません。
毎回毎回、自作のコンポーネントを用意できるわけではないのでこの仕様は少し困ってしまいますね…

結局うまく行ったパターン

ライブラリのメソッドは使わず addEventListener でクリックやタップ動作に対応した

if (typeof DeviceMotionEvent.requestPermission === 'function' && sessionStorage.getItem('isPermission') === null) {
    const tingleLinkElement = document.createElement('link');
    tingleLinkElement.rel = 'stylesheet';
    tingleLinkElement.href = 'https://cdnjs.cloudflare.com/ajax/libs/tingle/0.15.3/tingle.min.css';
    document.head.appendChild(tingleLinkElement);

    tingleLinkElement.onload = () => {
        const tingleScriptElement = document.createElement('script');
        tingleScriptElement.src = 'https://cdnjs.cloudflare.com/ajax/libs/tingle/0.15.3/tingle.min.js';
        document.body.appendChild(tingleScriptElement);

        tingleScriptElement.onload = () => {
            const modal = new tingle.modal({
                footer: true
            });

            modal.setContent('<p>このサイトでは、センサー値を扱います。</p>');
            modal.addFooterBtn('Cancel', 'tingle-btn tingle-btn--default tingle-btn--pull-right', () => modal.close());
            modal.addFooterBtn('OK', 'tingle-btn tingle-btn--primary tingle-btn--pull-right', () => {
                modal.close();
            });

            document.querySelector('.tingle-btn.tingle-btn--primary.tingle-btn--pull-right').addEventListener('click', async () => {
                const isDeviceOrientationEvent = await DeviceOrientationEvent.requestPermission();
                const isDeviceMotionEvent = await DeviceMotionEvent.requestPermission();

                // 許可したあとはまた許可が必要になるまでポップアップが出ないようにする
                if (isDeviceOrientationEvent && isDeviceMotionEvent) {
                    sessionStorage.setItem('isPermission', 'true');
                }
            });

            modal.open();
        };
    };
}

動くことには動きましたが、なんか微妙に納得のいかない書き方に…
IMG_BF5F9E0E6ABB-1.jpeg IMG_0760.PNG

結論

ユーザからの動作であれば何でも良いわけではなく onclick か addEventListener を使って実装しなくてはいけない
セキュリティの観点からこういう風にサイトごとに許可を取るスタイルはしょうがないとは思うんだけど
メソッドを呼んでくれる基準がよくわからないから使う側としてはすごく困るなあと…
iOS のブラウザ(というか Safari )はこんな感じの謎独自機能と草案の機能の実装スピードをもう少し早くしてくれればなあ… と最近思うことが多いです。
正直、ブラウザに関しては Android ブラウザのほうが圧勝だなあと思いますね ?

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

Promiseとは・非同期処理時にPromiseを使うメリットとは

Promiseとは

非同期処理のコードを扱いやすくするもの。
Promiseを用いると、非同期処理のコールバックの扱いがより簡単になる。

Promiseを使った実装・使わない実装

・Promiseを使わずに実装

//doWorkCallback関数の定義
const doWorkCallback = (callback) => {

//非同期処理。2秒後に[1,2,3]を返す
setTimeOut(() => {
callback(undefined, [1,2,3])
}, 2000)
}

//doWork関数の実行
//第一引数:エラー時の返り値、第二引数:処理成功時の返り値
doWorkCallback((error, result) => {

if(error){
return console.log(error)
}
console.log(result)
})


2秒後に[1,2,3]が返る
または、2秒後にerrorが返る

・Promiseを使って実装

//doWorkPromise関数の定義
const doWorkPromise = new Promise((resolve, reject) => {

//非同期処理。2秒後に[1,2,3]を返す
setTimeOut(() => {


//処理成功時の処理をresolve関数として実装
resolve([1,2,3])

//エラー時の処理をreject関数として実装
reject('Things went wrong')
}
},2000)

//doWorkPromise関数の実行
//成功時の処理を、「.then()」をつなげることによって記述
doWorkPromise.then((result) => {
console.log('success', result)

//エラー時の処理を、「.catch()」をつなげることによって記述
}).catch((error) => {

console.log('Error',error)
})


2秒後に「success, [1,2,3]」が返る
または、2秒後に「Error Things went wrong」が返る

Promiseを使用するメリット

Promiseを使って実装すると・・・

①成功時の処理はresolve関数、エラー時の処理はreject関数と、2つの別々の関数て処理している為、何が起こっているかわかりやすい。

Promiseを使わないと: 関数1つで成功時とエラー時の処理を行うことになる、
コールバックの全ての呼び出しを調べてから、引数errorと引数result、どちらが提供されたか判断することになる。

②処理結果によって呼び出される関数はresolve関数かreject関数かのどちらか一つ。
呼び出されるとPromiseは終了する為、後から実行結果の値や状態が上書きされることがない。

Promiseを使わないと: コールバックが2回呼び出され、結果が変わるリスクがある。

Promise処理中におこっている事

Promiseが作成される

Pending:非同期処理実行中。Promiseの結果は保留

①fulfilled:処理成功
②rejected:処理失敗

Promiseをチェインさせる

複数の非同期メソッドをつなげて処理を行う時、Promiseをチェインさせて書くことができる。

・Promiseチェインを使わず実装

//add関数の定義。Promiseを使った非同期処理とし、2秒後にa+bの結果を返す
const add = (a, b) => {
  return new Promise ((resolve, reject) => {
    setTimeout(() => {
      resolve(a + b)
    }, 2000)
  })
}

add(1, 2).then((sum) => {  
  console.log(sum)

  add(sum, 5).then((sum2) => {  //add関数で、「add(1,2)の結果」と「5」を処理する
    console.log(sum2)
  }).catch((e) => {
    console.log(e)
  })

}).catch((e) => {  //add(1,2)のエラー処理
  console.log(e)
})


2秒後に「3」、さらに2秒後に「8」が返る

・Promiseチェインを使って実装

//add関数の定義。Promiseを返し、2秒後にa+bの結果を返す
const add = (a, b) => {
  return new Promise ((resolve, reject) => {
    setTimeout(() => {
      resolve(a + b)
    }, 2000)
  })
}

add(1,1).then((sum) => {
  console.log(sum)
  return add(sum, 4)  //add関数で、「add(1,1)の結果」と「4」を処理する。2つ目の非同期処理を returnで返すようにしている
}).then((sum2) => {  //2つ目の非同期処理を「.then()」で返す

  console.log(sum2)
}).catch((e) => { 
  console.log(e)
})


2秒後に「2」、さらに2秒後に「6」が返る

・Promiseチェインを使うメリット

①ネストを深くせずに、複数の非同期処理を繋げて行える
②エラー時の処理コードを重複して書かなくてすむ

参考文献

「The complete Node.js Developer Course」
https://www.udemy.com/share/101WGiAEIedVpTTX4D/

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

プログラミング TypeScript:第 2 章 TypeScript 全体像

はじめに

TypeScript の学習のために「プログラミング TypeScript ―スケールする JavaScript アプリケーション開発」を購入しましたので、自身の学習のためにも本プログで数回に渡って、重要な部分に絞って紹介していければと思います。

プログラミングTypeScript

学習用にサンプルプログラミングも Github で紹介されていますので、合わせて紹介していきます。
『プログラミングTypeScript』のリポジトリ

本記事についての内容は以下を参照してください。
プログラミング TypeScript:第 2 章 TypeScript 全体像

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

TypeScript をはじめてみた!

はじめに

ArcGIS API for JavaScript を使用した簡単な Web マッピングアプリの開発に TypeScript を使用してみました。

ArcGIS API for JavaScript は、Google Map API と同じような Web マッピングアプリを開発するための API です。ArcGIS API for JavaScript は、Google Map とは異なり 空間検索などの GIS の機能やデータのビジュアライゼーションも豊富に対応しています。

今回は、ArcGIS API for JavaScript で TypeScript を使用するための準備として、開発環境の構築や簡単なサンプルアプリを作成までの手順を紹介したいと思います。また、TypeScript を使用するメリットについても紹介します。

本記事についての内容は以下を参照してください。
TypeScript をはじめてみた!

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

【Vue】filtersでthisが使えない

Vueのfiltersthisが使えなかった話

Vue.jsのfiltersで↓のようなコードを書いて実行しようとしたらエラーがでた。

最初はMixinとして使っていたためMixinではfiltersは使えないのかと思い、普通にコンポーネント内のfiltersプロパティ内で使っても同じエラーがでたので調べることにした。

ちなみにfiltersというのはVue固有のプロパティの一つで、ここに定義した関数をムスタッシュ構文内で例えば{{ <data> | oneFilter }}のようにして使うと<data>oneFilterで加工することができる。

<template>
  <div id="app">
    <p>{{ '選択してください' |  processChoice }}</p>
  </div>
</template>

<script>
new Vue({
  el: '#app',

  computed: {
    getName (obj) {
        if (obj) {
            return obj.name
        }
    }
  },

  filters: {
    processChoice(value) {
      if (this.getName) {
        return this.getName
      }
      return value
    }
  }
})
</script>
エラーメッセージ
Cannot read property 'getName' of undefined...

結論

先に結論を書いてしまうと、「Vueのfiltersプロパティではthis(Vueインスタンス)にはアクセスできないようなので、computedmethodsを使いましょう」ということらしいです。

filtersで定義した関数内からthisにアクセスできないので、undefinedエラーが出ているということですね。

調べたこととか

  1. this undefined in filters #5998
  2. VueJSのfilters内でmethodが使えない

1番目のリンクのissueに対する回答で、Evan You(Vue.jsの作者)がfiltersでは敢えてVueインスタンスにアクセスできないようににしていると言っています。filtersでは純粋なJSの関数を使うことしか現状できないようです。どうしてもthisを使う必要がある時は結論に書いたようにfiltersではなく、computedmethodsに処理をまとめて書いてそれを使いましょう。(compotedmethodsもムスタッシュ構文に埋め込むことが可能です。)

何人かの開発者がfilters内でthisを使えるようにして欲しいと書いていますが、
作者が↓のように言っているのでVue3系でも同じ仕様になるような気がします。

I'm absolutely convinced that filters must have a way access the context. The question's what core team is going to do about it?

Sorry but my opinion has not changed: filters should not, and will not have access to context. If you need context, use a method.

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

いろいろ試そう Parcel v2 (Beta)!

はじめに

去る 2020年6月19日、Parcel v2のベータバージョンがリリースされました。

? Parcel 2 beta 1: improved stability, tree shaking, source map performance, and more! ?

この記事では、Parcel v2の基本的な使い方から、いくつかのAltJS・ライブラリをParcelでコンパイルするテンプレートについて説明します。

なお、この記事の内容はv2.0.0-beta.1時点でのドキュメント、ソースコードをベースにしています。
また、無印のParcelを Parcel v1、新しいv2を Parcel v2と表記します。

オフィシャルドキュメント

Parcel v1のドキュメントは https://parceljs.org/ でしたが、v2ではURLが代わり https://v2.parceljs.org/ (未完成)がドキュメントとして提供されています。

image.png
▲ Parcel v2のドキュメント

インストール

Parcel v1のパッケージはparcel-bundlerという名前で公開されていましたが、Parcel v2からパッケージ名がparcelとなりました。

また、現在のParcel v2はベータ版のため、バージョンを@nextと指定してインストールします。

$ yarn add -D parcel@next

設定ファイル

Parcel v2では、.parcelrcという設定ファイルを使用できるようになりました。
.parcelrcを指定しなかった場合は、デフォルトの設定(@parcel/config-default)でバンドルされます。

.parcelrcはParcelのプラグインを種類ごとに設定します。

設定できるプラグインの種類は次のとおりです。

  • Resolvers
    • ファイルやパッケージ、アセットを解決するプラグイン
  • Transformers
    • ソースコードを変換(トランスパイル)するプラグイン
    • 公式でBabelやTypeScript、Vue、Yamlなどのトランスフォーマが提供されている
  • Bundlers
    • アセットをバンドルするプラグイン
  • Namers
    • ファイルの命名やコンテントハッシュなどを付与するプラグイン
  • Runtimes
    • バンドラーフェーズのあとに最終的にバンドルに含まれるアセット(HMRのコードなど)を生成するプラグイン
  • Packagers
    • 異なるアセットタイプを単一のバンドルにマージするプラグイン
  • Optimizers
    • バンドルを最適化するプラグイン
  • Reporters
    • Parcelでのイベントを受け取ってロガーに出力したり、ログファイルを生成するプラグイン
  • Validators
    • ビルド後にソースコードの検証を行いエラーを表示するプラグイン

いずれのプラグインも自作が可能ですが、基本的な機能は公式パッケージとして公開されているものを使用すれば問題ないでしょう。

もし、個別の.parcelrcを設定する場合はデフォルトの設定に追加する形で定義するとよいでしょう。

{
  // デフォルトの設定を継承する
  "extends": "@parcel/config-default",
  "optimizers": {
    "*.{png,jpg}": ["@parcel/optimizer-awesome-images"]
  }
}

また、ある拡張子にプラグインを追加する際は、デフォルトの設定を引き継ぐよう...を最後に記載するようにしましょう。

{
  "extends": "@parcel/config-default",
  "transforers": {
    // デフォルト設定が引き継がれないためコンパイルできない
    "*.{ts,tsx}": [],
    // ... で extends で継承された設定を引き継ぐ
    "*.{ts,tsx}": ["..."],
  }
}

いろいろ試してみよう

ここからは、Parcel v2でいろいろなAltJS、ライブラリをバンドルする設定を見ていきましょう。
それぞれ、GitHubでソースコードを公開しています。

また、公式のリポジトリにもいくつかのサンプルプロジェクトが用意されています。

Pug + Sass

テンプレートエンジンのPugとCSSプリプロセッサのSassを使用したサンプルです。

サンプルリポジトリ

開発環境構築

サンプルリポジトリの開発環境構築の手順を紹介します。

ここでは、次の構成でファイルを配置しています。
それぞれのファイルの内容はサンプルリポジトリでご確認ください。

.
├── src/
│   ├── index.pug
│   └── styles.sass
└── package.json

PugとSassの設定はデフォルトで定義されているため、.parcelrcファイルでの設定は必要ありません。
Pugの設定Sassの設定

インストールした依存関係はparcel@nextと次のパッケージです。

$ yarn add -D pug sass

# reset.css
$ yarn add reset-css

ここまでできたら、Parcelで開発サーバーを起動します。
serveコマンドは省略できます。

$ yarn parcel serve src/index.pug

ポートを指定していない場合、http://localhost:1234で開発サーバーが起動します。

image.png

あとは、Pugファイル、Sassファイルへ変更を加えるとHMRで表示中の内容が入れ替わります。

以上でPugとSassの開発環境ができました!かんたんですね。

まとめ

  • yarn add -D parcel@next pug sass
  • PugとSassを記述
  • yarn parcel serve src/index.pug
  • http://localhost:1234 を開く

余談

Parcelでは、Sassからnode_modulesへインストールされたパッケージをインポートする際、パッケージ名のプレフィックスとして~必要としません

~package-nameの指定は本来、Webpackでのシンタックスのようで、ParcelのCLI上では次のようにレポートされます。

image.png

具体的な理由まで書かれていて、とても親切なエラーメッセージですね!

React + CSS Modules

React(JSX)とCSS Modulesによるサンプルです。

サンプルリポジトリ

開発環境構築

サンプルリポジトリの開発環境構築の手順を紹介します。

ここでは、次の構成でファイルを配置しています。
それぞれのファイルの内容はサンプルリポジトリでご確認ください。

.
├── src/
│   ├── Hello/
│   │   ├── index.jsx
│   │   └── index.modules.css
│   ├── entry.jsx
│   ├── index.html
│   └── styles.css
├── .postcssrc
└── package.json

JSXやCSSのバンドルに関する設定はデフォルトで定義されているため、.parcelrcファイルでの設定は必要ありません。
JSXの設定

ただし、CSS Modulesを使用するためのPostCSSの設定が必要です。
ここでは、postcss-modulesパッケージを使用してCSS Modulesを実現しています。

{
  "modules": true,
  "plugins": []
}

インストールした依存関係はparcel@nextと次のパッケージです。

$ yarn add react react-dom prop-types
$ yarn add -D postcss-modules

# reset.css
$ yarn add reset-css

ここまでできたら、Parcelで開発サーバーを起動します。
serveコマンドは省略できます。

$ yarn parcel serve src/index.html

ポートを指定していない場合、http://localhost:1234で開発サーバーが起動します。

image.png

あとは、JSXファイルやCSSファイルへ変更を加えるとHMRで表示中の内容が入れ替わります。

以上でReactとCSS Modulesの開発環境が構築できました。こちらもかんたんでしたね!

まとめ

  • yarn add -D parcel@next postcss-modules
  • yarn add react react-dom prop-types
  • .postcssrcに設定を記述
  • Reactを記述
  • yarn parcel serve src/index.html
  • http://localhost:1234 を開く

余談

今回はプレーンなCSSを使用したCSS Modulesでしたが、JSXファイルから参照するファイルを.sass.stylへ変更し、必要なパッケージ(sassstylus)をインストールすれば、設定を変更することなくほかのCSSプリプロセッサを使用したCSS Modulesで開発ができます。

Vue

残念ながら、Parcel v2 beta.1時点ではVueのトランスフォーマプラグインが開発されていません。

開発については次のIssue内で議論が行われています。

Parcel 2: Vue transformer · Issue #3364 · parcel-bundler/parcel

状況として、Parcel v2のコアチームにVueを使用するメンバーが少なくSFCの取り扱いについて議論しているようです。

開発が終了し、公開されれば次のように.parcelrcに記載すればVueもバンドルできるようになるのではないかなと思います。

{
  "extends": "@parcel/config-default",
  "transformers": {
    "*.vue": ["@parcel/transformer-vue"]
  }
}

TypeScript + ESLint

TypeScriptとESLintを使用したサンプルです。

サンプルリポジトリ

開発環境構築

サンプルリポジトリの開発環境構築の手順を紹介します。

ここでは、次の構成でファイルを配置しています。
それぞれのファイルの内容はサンプルリポジトリでご確認ください。

.
├── src/
│   └── index.ts
├── .eslintrc
└── package.json
└── tsconfig.json

インストールした依存関係はparcel@nextと次のパッケージです。

yarn add -D typescript @parcel/validator-typescript @parcel/validator-eslint

# ESLint
yarn add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

TypeScriptの設定はデフォルトで定義されているため、.parcelrcファイルでの必要はありませんが、型をチェックするには別途、バリデータプラグイン@parcel/validator-typescriptを追加する必要があります。
また、ESLintチェックもTypeScriptの型チェックと同様、バリデータプラグイン@parcel/validator-eslintが必要です。

バリデータプラグインを設定した.parcelrcファイルは次のとおりです。

{
  "extends": "@parcel/config-default",
  "validators": {
    "*.{js,jsx,ts,tsx}": ["@parcel/validator-eslint", "@parcel/validator-typescript"]
  }
}

ここまでできたら、Parcelで.tsファイルをビルドします。

$ yarn parcel build src/index.ts

ソースコードに問題がなければビルドに成功しますが、エラーがあった場合は次のようにCLI上にエラーが表示されます。

image.png
▲ 型エラー

image.png
▲ ESLintエラー

なお、ESLintエラーのAuto Fixには対応していないようです。

まとめ

  • yarn add -D parcel@next typescript eslint @parcel/validator-typescript @parcel/validator-eslint
  • .eslintrcにESLint設定を記述
  • yarn parcel build src/index.ts
  • エラーがあればCLI上に表示される

おわりに

Parcel v2でv1時代より柔軟にオプションを設定できるようになりましたが、設定不要でバンドルできる手軽さも健在です。

まだベータ版ですが、一般的な使い方をする分には申し分ない機能が提供されているなという印象でした。
ただ、新しいツールのため、調べられる情報や公開されているプラグインが少なく真に柔軟に使えるようになるにはまだまだ時間がかかるかなと思います。

2019/8/14にα版が公開されてから10ヶ月、ついにParcel v2も正式リリースの日が近づいてきました。
これからのParcelの発展が楽しみです!

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

いろいろ試そう Parcel 2 (Beta)!

はじめに

去る 2020年6月19日、Parcel v2のベータバージョンがリリースされました。

? Parcel 2 beta 1: improved stability, tree shaking, source map performance, and more! ?

この記事では、Parcel v2の基本的な使い方から、いくつかのAltJS・ライブラリをParcelでコンパイルするテンプレートについて説明します。

なお、この記事の内容はv2.0.0-beta.1時点でのドキュメント、ソースコードをベースにしています。
また、無印のParcelを Parcel v1、新しいv2を Parcel v2と表記します。

オフィシャルドキュメント

Parcel v1のドキュメントは https://parceljs.org/ でしたが、v2ではURLが代わり https://v2.parceljs.org/ (未完成)がドキュメントとして提供されています。

image.png
▲ Parcel v2のドキュメント

インストール

Parcel v1のパッケージはparcel-bundlerという名前で公開されていましたが、Parcel v2からパッケージ名がparcelとなりました。

また、現在のParcel v2はベータ版のため、バージョンを@nextと指定してインストールします。

$ yarn add -D parcel@next

設定ファイル

Parcel v2では、.parcelrcという設定ファイルを使用できるようになりました。
.parcelrcを指定しなかった場合は、デフォルトの設定(@parcel/config-default)でバンドルされます。

.parcelrcはParcelのプラグインを種類ごとに設定します。

設定できるプラグインの種類は次のとおりです。

  • Resolvers
    • ファイルやパッケージ、アセットを解決するプラグイン
  • Transformers
    • ソースコードを変換(トランスパイル)するプラグイン
    • 公式でBabelやTypeScript、Vue、Yamlなどのトランスフォーマが提供されている
  • Bundlers
    • アセットをバンドルするプラグイン
  • Namers
    • ファイルの命名やコンテントハッシュなどを付与するプラグイン
  • Runtimes
    • バンドラーフェーズのあとに最終的にバンドルに含まれるアセット(HMRのコードなど)を生成するプラグイン
  • Packagers
    • 異なるアセットタイプを単一のバンドルにマージするプラグイン
  • Optimizers
    • バンドルを最適化するプラグイン
  • Reporters
    • Parcelでのイベントを受け取ってロガーに出力したり、ログファイルを生成するプラグイン
  • Validators
    • ビルド後にソースコードの検証を行いエラーを表示するプラグイン

いずれのプラグインも自作が可能ですが、基本的な機能は公式パッケージとして公開されているものを使用すれば問題ないでしょう。

もし、個別の.parcelrcを設定する場合はデフォルトの設定に追加する形で定義するとよいでしょう。

{
  // デフォルトの設定を継承する
  "extends": "@parcel/config-default",
  "optimizers": {
    "*.{png,jpg}": ["@parcel/optimizer-awesome-images"]
  }
}

また、ある拡張子にプラグインを追加する際は、デフォルトの設定を引き継ぐよう...を最後に記載するようにしましょう。

{
  "extends": "@parcel/config-default",
  "transforers": {
    // デフォルト設定が引き継がれないためコンパイルできない
    "*.{ts,tsx}": [],
    // ... で extends で継承された設定を引き継ぐ
    "*.{ts,tsx}": ["..."],
  }
}

いろいろ試してみよう

ここからは、Parcel v2でいろいろなAltJS、ライブラリをバンドルする設定を見ていきましょう。
それぞれ、GitHubでソースコードを公開しています。

また、公式のリポジトリにもいくつかのサンプルプロジェクトが用意されています。

Pug + Sass

テンプレートエンジンのPugとCSSプリプロセッサのSassを使用したサンプルです。

サンプルリポジトリ

開発環境構築

サンプルリポジトリの開発環境構築の手順を紹介します。

ここでは、次の構成でファイルを配置しています。
それぞれのファイルの内容はサンプルリポジトリでご確認ください。

.
├── src/
│   ├── index.pug
│   └── styles.sass
└── package.json

PugとSassの設定はデフォルトで定義されているため、.parcelrcファイルでの設定は必要ありません。
Pugの設定Sassの設定

インストールした依存関係はparcel@nextと次のパッケージです。

$ yarn add -D pug sass

# reset.css
$ yarn add reset-css

ここまでできたら、Parcelで開発サーバーを起動します。
serveコマンドは省略できます。

$ yarn parcel serve src/index.pug

ポートを指定していない場合、http://localhost:1234で開発サーバーが起動します。

image.png

あとは、Pugファイル、Sassファイルへ変更を加えるとHMRで表示中の内容が入れ替わります。

以上でPugとSassの開発環境ができました!かんたんですね。

まとめ

  • yarn add -D parcel@next pug sass
  • PugとSassを記述
  • yarn parcel serve src/index.pug
  • http://localhost:1234 を開く

余談

Parcelでは、Sassからnode_modulesへインストールされたパッケージをインポートする際、パッケージ名のプレフィックスとして~必要としません

~package-nameの指定は本来、Webpackでのシンタックスのようで、ParcelのCLI上では次のようにレポートされます。

image.png

具体的な理由まで書かれていて、とても親切なエラーメッセージですね!

React + CSS Modules

React(JSX)とCSS Modulesによるサンプルです。

サンプルリポジトリ

開発環境構築

サンプルリポジトリの開発環境構築の手順を紹介します。

ここでは、次の構成でファイルを配置しています。
それぞれのファイルの内容はサンプルリポジトリでご確認ください。

.
├── src/
│   ├── Hello/
│   │   ├── index.jsx
│   │   └── index.modules.css
│   ├── entry.jsx
│   ├── index.html
│   └── styles.css
├── .postcssrc
└── package.json

JSXやCSSのバンドルに関する設定はデフォルトで定義されているため、.parcelrcファイルでの設定は必要ありません。
JSXの設定

ただし、CSS Modulesを使用するためのPostCSSの設定が必要です。
ここでは、postcss-modulesパッケージを使用してCSS Modulesを実現しています。

{
  "modules": true,
  "plugins": []
}

インストールした依存関係はparcel@nextと次のパッケージです。

$ yarn add react react-dom prop-types
$ yarn add -D postcss-modules

# reset.css
$ yarn add reset-css

ここまでできたら、Parcelで開発サーバーを起動します。
serveコマンドは省略できます。

$ yarn parcel serve src/index.html

ポートを指定していない場合、http://localhost:1234で開発サーバーが起動します。

image.png

あとは、JSXファイルやCSSファイルへ変更を加えるとHMRで表示中の内容が入れ替わります。

以上でReactとCSS Modulesの開発環境が構築できました。こちらもかんたんでしたね!

まとめ

  • yarn add -D parcel@next postcss-modules
  • yarn add react react-dom prop-types
  • .postcssrcに設定を記述
  • Reactを記述
  • yarn parcel serve src/index.html
  • http://localhost:1234 を開く

余談

今回はプレーンなCSSを使用したCSS Modulesでしたが、JSXファイルから参照するファイルを.sass.stylへ変更し、必要なパッケージ(sassstylus)をインストールすれば、設定を変更することなくほかのCSSプリプロセッサを使用したCSS Modulesで開発ができます。

Vue

残念ながら、Parcel v2 beta.1時点ではVueのトランスフォーマプラグインが開発されていません。

開発については次のIssue内で議論が行われています。

Parcel 2: Vue transformer · Issue #3364 · parcel-bundler/parcel

状況として、Parcel v2のコアチームにVueを使用するメンバーが少なくSFCの取り扱いについて議論しているようです。

開発が終了し、公開されれば次のように.parcelrcに記載すればVueもバンドルできるようになるのではないかなと思います。

{
  "extends": "@parcel/config-default",
  "transformers": {
    "*.vue": ["@parcel/transformer-vue"]
  }
}

TypeScript + ESLint

TypeScriptとESLintを使用したサンプルです。

サンプルリポジトリ

開発環境構築

サンプルリポジトリの開発環境構築の手順を紹介します。

ここでは、次の構成でファイルを配置しています。
それぞれのファイルの内容はサンプルリポジトリでご確認ください。

.
├── src/
│   └── index.ts
├── .eslintrc
└── package.json
└── tsconfig.json

インストールした依存関係はparcel@nextと次のパッケージです。

yarn add -D typescript @parcel/validator-typescript @parcel/validator-eslint

# ESLint
yarn add -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

TypeScriptの設定はデフォルトで定義されているため、.parcelrcファイルでの必要はありませんが、型をチェックするには別途、バリデータプラグイン@parcel/validator-typescriptを追加する必要があります。
また、ESLintチェックもTypeScriptの型チェックと同様、バリデータプラグイン@parcel/validator-eslintが必要です。

バリデータプラグインを設定した.parcelrcファイルは次のとおりです。

{
  "extends": "@parcel/config-default",
  "validators": {
    "*.{js,jsx,ts,tsx}": ["@parcel/validator-eslint", "@parcel/validator-typescript"]
  }
}

ここまでできたら、Parcelで.tsファイルをビルドします。

$ yarn parcel build src/index.ts

ソースコードに問題がなければビルドに成功しますが、エラーがあった場合は次のようにCLI上にエラーが表示されます。

image.png
▲ 型エラー

image.png
▲ ESLintエラー

なお、ESLintエラーのAuto Fixには対応していないようです。

まとめ

  • yarn add -D parcel@next typescript eslint @parcel/validator-typescript @parcel/validator-eslint
  • .eslintrcにESLint設定を記述
  • yarn parcel build src/index.ts
  • エラーがあればCLI上に表示される

おわりに

Parcel v2でv1時代より柔軟にオプションを設定できるようになりましたが、設定不要でバンドルできる手軽さも健在です。

まだベータ版ですが、一般的な使い方をする分には申し分ない機能が提供されているなという印象でした。
ただ、新しいツールのため、調べられる情報や公開されているプラグインが少なく真に柔軟に使えるようになるにはまだまだ時間がかかるかなと思います。

2019/8/14にα版が公開されてから10ヶ月、ついにParcel v2も正式リリースの日が近づいてきました。
これからのParcelの発展が楽しみです!

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

mongoDB, mongooseを使ってCRUDを実装する

CRUDとは

CRUD(クラッド)とは、ほとんど全てのコンピュータソフトウェアが持つ永続性[1]の4つの基本機能のイニシャルを並べた用語。その4つとは、Create(生成)、Read(読み取り)、Update(更新)、Delete(削除)である。

(Wikipediaより)

・CRUDそれぞれに対応するHTTPメソッド

・Create:Postメソッド
・Read:Getメソッド
・Update:Patchメソッド
・Delete:Deleteメソッド

※CRUD処理の為にHTTPメソッドを実装した後は、ちゃんと動くか、Postmanを用いてテストすると良い。
<参考>『Postmanを使ってHTTPに送ったデータを確認する』(https://qiita.com/Molly95554907/items/e367e83129ea1173c317)

CRUDの実装

const express = require('express')
require('./db/mongoose')            //別ファイル(mongooseの読み込み)
const User = require('./models/user')  //別ファイル(mongooseによるuserの定義の読み込み)

const app = express()
//process.env.PORTはherokuで動かす時のポート
const port = process.env.PORT || 3000

//HTTPへ送ったデータを情報の読み取れるようにする
app.use(express.json())

Create

<公式ドキュメント>
・データをデータベースに送る

app.post('/users', (req, res) => {
    const user = new User(req.body)  //リクエストで送ったデータをUserインスタンスに入力

    user.save().then(() => {     //新しく作成したUserインスタンスをDBに保存
        res.status(201).send(user)  //保存成功したら、HTTPステータス201と、保存したデータを返す
    }).catch((e) => {         //保存失敗したら、HTTPステータス400とエラーメッセージを返す
        res.status(400).send(e)
    })
})

<非同期処理(Promise)を用いた実装>

app.post('/users', async (req, res) => {  //asyncで処理した関数はPromiseを返す
    const user = new User(req.body)     //リクエストで送ったデータをUserインスタンスに入力

    try {              //保存成功したら、HTTPステータス201と、保存したデータを返す
        await user.save()
        res.status(201).send(user)
    } catch (e) {
        res.status(400).send(e)   //保存失敗したら、HTTPステータス400とエラーメッセージを返す
    }
})

Read

<公式ドキュメント>
・データをデータベースからとってくる

app.get('/users', (req, res) => {
    User.find({}).then((users) => {   //find({})で全てのデータを取得
        res.send(users)          //取得した全てのデータを返す
    }).catch((e) => {
        res.status(500).send()      //取得失敗したら、HTTPステータス500を返す
    })
})

<非同期処理(Promise)を用いた実装>

app.get('/users', async (req, res) => {
    try {
        const users = await User.find({})  //find({})で全てのデータを取得
        res.send(users)             //取得した全てのデータを返す
    } catch (e) {
        res.status(500).send()         //取得失敗したら、HTTPステータス500を返す
    }
})

Updaet

・データの更新
<公式ドキュメント>
「API > Model」の項目から探す。
・findById
https://mongoosejs.com/docs/api/model.html#model_Model.findById
・findByIdAndUpdate
https://mongoosejs.com/docs/api/model.html#model_Model.findByIdAndUpdate

app.get('/users/:id', (req, res) => {
    const _id = req.params.id  //「:id」に打ち込んだ値を取得

    User.findById(_id).then((user) => {  //「:id」に打ち込んだIDと同じドキュメントを探す
        if (!user) {
            return res.status(404).send() //「:id」に打ち込んだIDと同じドキュメントがなければ、HTTPステータス404を返す
        }

        res.send(user)  //「:id」に打ち込んだIDと同じドキュメントが見つかればそれを返す
    }).catch((e) => {
        res.status(500).send()  //取得失敗したら、HTTPステータス500を返す
    })
})

<非同期処理(Promise)を用いた実装>

app.patch('/users/:id', async (req, res) => {
    const updates = Object.keys(req.body)  //リクエストで送った更新データを取得
    const allowedUpdates = ['name', 'email', 'password', 'age']  //updateできる項目を限定する
    const isValidOperation = updates.every((update) => allowedUpdates.includes(update))  //リクエストで送った更新データがupdateできる項目に当てはまっているか

    if (!isValidOperation) {
        return res.status(400).send({ error: 'Invalid updates!' }) //当てはまっていなければ、HTTPステータス404とエラーメッセージを返す
    }

    try {
        const user = await User.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true })

        if (!user) {  //「:id」に打ち込んだIDと同じドキュメントを探し、なければHTTPステータス404を返す
            return res.status(404).send()
        }

        res.send(user)   //「:id」に打ち込んだIDと同じドキュメントが見つかればそれアップデートし返す
    } catch (e) {
        res.status(500).send(e)  //取得失敗したら、HTTPステータス500を返す
    }
})

Delete

・データの削除
<公式ドキュメント>
findByIdAndDelete
https://mongoosejs.com/docs/api/model.html#model_Model.findOneAndDelete

app.delete('/users/:id', async (req, res) => {
    try {
        const user = await User.findByIdAndDelete(req.params.id)  //「:id」に打ち込んだIDと同じドキュメントを探し削除

        if (!user) {
            return res.status(404).send()  //「:id」に打ち込んだIDと同じドキュメントがなければHTTPステータス404を返す
        }

        res.send(user)   //「:id」に打ち込んだIDと同じドキュメントが見つかればそれを削除し返す
    } catch (e) {
        res.status(500).send()  //取得失敗したら、HTTPステータス500を返す
    }
})

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

WebAssemblyとJavaScriptの最適化による処理速度を比較してみる

はじめに

前回のコメントで「サンプルが簡単すぎてCの最適化が早いのは当然」や「JavaScriptも最適化がある」と言ったことを教えてもらえたので再度比較してみました。

環境

  • windows 10
  • python:3.6.5
  • emcc:1.39.16
  • clang:11.0.0

実験ブラウザ

  • Google Chrome:83.0.4103.106

純粋なループの比較

前回と同様にそれぞれの中でループをさせてみました。JavaScriptの最適化はprepackとClosure Compilerの2つを使いました。

C/C++ファイルの作成とコンパイル

前回と全く同じなので省略します。

htmlの作成する

前回とほぼ同じなので省略します。

JavaScriptの作成と最適化

最適化用JavaScriptの作成

今回は最適化するためにループ部分だけ別ファイルにします。

jsmod.js
function loop_func_ori(){
    var count = 0;
    for(var i = 0; i < 100000000; i++){
        count = count + 1;
    }
    return count;
}

最適化ツール(prepack)のインストール

npmを使用してprepackをインストールします。

npm install prepack 

インストールしたフォルダ内のnode_modulesにprepackが追加されています。

JavaScriptの最適化(prepack)

prepackコマンドを使用して最適化を行います。

>.\node_modules\.bin\prepack jsmod.js --out jsmod-prepa.js
Prepacked source code written to jsmod-prepa.js.

--outで指定したファイルに最適化後のJavaScriptができます。

jsmod-prepa.js
var loop_func;
(function () {
  var _$0 = this;

  var _1 = function () {
    var count = 0;

    for (var i = 0; i < 100000000; i++) {
      count = count + 1;
    }

    return count;
  };

  _$0.loop_func = _1;
}).call(this);

JavaScriptの最適化(Closure Compiler)

Closure Compilerは公式から最適化を行います。

jsmod-clo.js
function loop_func_clo(){for(var a=0,b=0;1E8>b;b++)a+=1;return a};

最適化しましたが、両方ともあまり処理が短縮されていないように感じます。

実行

サーバを再起動して上のhtmlを実行します。

回数 JavaScript JavaScript prepack最適化あり JavaScript Closure Compiler最適化あり C++最適化なし C++最適化01 C++最適化02 C++最適化03
1 84.914 82.774 82.875 426.709 0.0699 0.0949 0.0499
2 83.609 82.669 82.170 421.660 0.0449 0.0500 0.0550
3 54.994 55.779 55.080 422.435 0.0499 0.0350 0.0499
4 54.495 57.089 55.375 429.959 0.0450 0.2099 0.0599
5 54.455 55.075 55.044 421.464 0.0299 0.0399 0.0500

結果を見ると、予想通りJavaScriptの最適化はあまり早くなっていないことがわかりました。
それ以外は、前回と同様にC++の方が早く、C++は最適化を強くするたびに処理速度が速くなっています。
JavaScriptの最適化はC++の最適化とは異なり、抽象化の解消や関数の事前実行など若干使い方が異なるので早くならなかったのかなと思います。

一応以下を最適化すれば処理時間が短くなりそうなのですが、最適化がいつまでたっても終わらないので諦めました。

jsmod2.js
function loop_func_ori(){
    var count = 0;
    for(var i = 0; i < 100000000; i++){
        count = count + 1;
    }
    return count;
}

var i = loop_func_ori()

引数のある関数の比較

C++の引数のある関数をJavaScriptから呼び出した時の速度を試してみました。

C/C++ファイルの作成

引数のあるC++関数をJavaScriptから呼び出す方法は前に書いた独自のC ++関数の連携に引数を追加するだけです。

cmain.cpp
#include <emscripten/emscripten.h>

extern "C" {
    int EMSCRIPTEN_KEEPALIVE loop_func(int n) {
        int count = 0;
        while(true){ 
            count = count + 1;
            if (n == 0){
                break;
            }
            n = n - 1;
        }
        return count;
    }
}

wasmを呼び出すhtmlを作成する

htmlはほぼ同じなので省略します。

JavaScriptの作成と最適化

基本は先ほどと同じです。今回は関数の処理が変わるため、そこだけ変更しています。
各関数に与えている整数は1000000000にしています。

jsmod.js
function loop_func_js_ori(n){
    var count = 0;
    while(true){
        count = count + 1
        if (n == 0){
          break;
        }
        n = n -1;
    }
    return count;
}

実行

サーバを再起動して上のhtmlを実行します。

回数 JavaScript JavaScript prepack最適化あり JavaScript Closure Compiler最適化あり C++最適化01 C++最適化02 C++最適化03
1 672.230 672.750 672.200 0.0600 0.0549 0.0999
2 669.505 675.559 674.194 0.0450 0.0499 0.0550
3 665.550 669.945 678.404 0.0550 0.0350 0.0450
4 670.449 678.560 698.879 0.0450 0.0550 0.0399
5 672.009 671.985 720.339 0.0550 0.0400 0.0350

結果を見ると単純なループと同じようにC++の方がJavaScriptより速いことがわかりました。これはループでもわかっていたため、単純にそうなんだーぐらいの感想でした。

おわりに

コメントを受けて他のパターンを試してみました。意外!と思えなかったので、個人的にはあまり面白くない結果でした。今回は、JavaScriptの最適化に不利な処理での比較になってしまったためJavaScriptの最適化はあまりすごくないイメージができてしまいましたが、大きなJavaScriptを最適化すれば意味のあるものになると思います。JavaScriptの最適化に有利な方法を思いついたら比較してみようと思います。実験の結果では前回と感想は変わりませんでした。

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

【Vue】学習開始2週目で覚える内容

2週目で学ぶべきこと

  • v-model
  • 名前付きslot
  • スコープ付きslot
  • 動的コンポーネント
  • ライフサイクルフック

v-model

  • v-modelディレクティブ:formのinput要素に対して、データバインディングを行う際に使用する
  • データバインディング:データ表示を結びつけ、双方向に変更を反映させること
App.vue
<template>
  <div>
     <!-- sampleオブジェクト内のanswerを参照する -->
    <input v-model="sample.answer">
    <p>{sample.answer}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      //オブジェクト名:sample  プロパティ名:answer
      sample: {
        answer: "Hello World!"
      }
    }
  }
};
</script>

名前付きslot

  • slot:親コンポーネントから子コンポーネントにテンプレートを差し込む機能

◆ 親コンポーネント

App.vue
<template>
  <div>
    <Child>
      <!-- 子コンポーネントの"slot name"で参照される -->
      <template v-slot:sample>
        <h1>親コンポーネントの表示</h1>
    </template>
       <!-- 子コンポーネントの"slot name"で参照される -->
       <template v-slot:answer>
          <!-- dataプロパティ参照 -->
          <p>{{word}}</p>
       </template>
    </Child>
  </div>
</template>

<script>
export default {
  data() {
    return {
      word: "good morning!"
    }
  }
};

◆ 子コンポーネント

Child.vue
<template>
  <div>
    <!-- 親コンポーネント"v-slot:sample"を参照する -->
    <slot name="sample"></slot>
     <hr>
     <p>Hello World!</p>
     <hr>
    <!-- 親コンポーネント"v-slot:answer"を参照する -->
    <slot name="answer"></slot>
  </div>
</template>

スコープ付きslot

  • 子コンポーネントslotに渡されたpropsに、親コンポーネントからアクセスすること

◆ 子コンポーネント

Child.vue
<template>
  <div>
    <!-- dataプロパティの"word"を、slotに設定する -->
    <!-- ※sampleは"任意の属性名"を設定する -->
    <slot name="sample" v-bind:sample="word"></slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      word: "good morning!"
    }
  }
};
App.vue
<template>
  <div>
    <Child>
      <!-- 子コンポーネントの v-bind sample="word" が参照される -->
      <!-- "slotProps"は任意の属性名"を設定する -->
      <template v-slot:sample="slotProps">
      <!-- slotPropsは"template内"で使用可能 -->
        <h1>{{ slotProps }}</h1>
    </template>
    </Child>
  </div>
</template>

動的コンポーネント

  • コンポーネント間の切り替えをスムーズに行う目的で使用する

◆ 子コンポーネント

Child.vue
<template>
  <p>Child</p>
</template>

◆ 親コンポーネント

App.vue
<template>
  <div>
  <!-- is:"別のコンポーネントを参照する"属性 -->
  <component v-bind:is="sample"></component>
  </div>
</template>

<script>
import Child from "./components/Child.vue";

export default {
  data() {
    return {
      //sample:属性名 default:値
      sample: "default"
    };
  },
components: {
  //子コンポーネントを参照する
  Child
}

ライフサイクルフック

  • activated:生き続けたコンポーネントを活性化する際に、参照される
  • deactivated:生き続けたコンポーネントを非活性化する際に、参照される
  • destroyed:Vueインスタンスが破棄された際に、参照される
  • keep-alive:コンポーネントの内容を保持したい時に使用する

◆ destroyedメソッド

Destroy.vue
<script>
export default {
  destroyed() {
    //Vueインスタンスが破棄された際に、出力される
    console.log("Hello World!");
  }
}
</script>

◆ keep-alive

Keepalive.vue
<template>
  <div>
  <keep-alive>
  <!-- 保持したい"コンポーネント"を"keep-alive"で囲む -->
  <component v-bind:is="sample"></component>
  </keep-alive>
  </div>
</template>

◆ activated / deactivatedメソッド

Sample.vue
<script>
export default {
  activated() {
    //コンポーネントが"活性化状態"の時に出力される
    console.log("Hello World!");
  },
  deactivated() {
   //コンポーネントが"非活性化状態"の時に出力される
    console.log("Good morning");
  }
};
</script>

参考文献

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

Web Crypto APIでJavaScriptによる暗号処理を行う(ECDH, AES)

こんにちわ。

JavaScriptによる暗号アルゴリズムの実装は幾つかありますが、今回はWeb Crypto APIというブラウザのネイティブ実装による暗号化を試したいと思います。

Web Crypto API

ネイティブ実装は実行パフォーマンス面で有利ですが関数や入出力などの"お作法"が複雑、逆にソフトウェア実装はパフォーマンスこそ及びませんが、その"お作法"が上手い具合に取り回しやすくなっている傾向があります。

ゆえに、ネイティブ実装のコーディングは少しややこしく感じますが、紐解いていくと実はそんなに難しい話ではないので、順を追って解説していきます。

はじめに

Web Crypto APIは、各種暗号処理(鍵生成/鍵交換/鍵導出/暗号化/復号/署名/検証...)をJavaScriptで安全に実行するためのAPIです。

Web Crypto APIの全機能はwindow.crypto.subtleオブジェクトに集約されており、いずれの関数もPromiseを返します。

ただMath.random()より暗号強度の高い乱数を取得できるgetRandomValues()だけは例外で、関数はwindow.cryptoオブジェクトに存在し、戻り値はTypedArrayとなります。

window.crypto.getRandomValues() // => TypedArray
window.crypto.subtle.xxx()      // => Promise

Web Crypto APIはいずれの機能も、使用するためのクラスは存在せず静的関数のみで構成され、都度パラメータを入力することで結果を得るかたちとなっています。

鍵オブジェクトは公開鍵/秘密鍵/共通鍵と種類を問わず、全てCryptoKeyというオブジェクト型で管理されます。
このCryptoKeyは、後述のインポート/エクスポート関数を使用してバイナリデータとして入出力が可能です。

Web Crypto APIはWebWorker内で使用可能です。

やること

今回は、楕円曲線暗号による鍵交換(ECDH)と、共通鍵暗号による暗号化(AES)を行います。

Web Crypto APIに実装されている楕円曲線は以下の通りです。

  • P-256
  • P-384
  • P-521

2020年6月時点においては、いずれのブラウザもモンゴメリ曲線(Curve25519)やエドワーズ曲線(Ed25519)といった楕円曲線には対応していません。

Web Crypto APIに実装されている共通鍵暗号アルゴリズムは以下の通りです。

  • AES-CTR
  • AES-CBC
  • AES-GCM

これらの実装状況から、ECDHはP-384を、AESはGCMを使用して暗号処理を実装していきます。

手順としては、以下のような流れになります。

  1. 楕円曲線による公開鍵と秘密鍵の生成
  2. 公開鍵/秘密鍵のエクスポート
  3. 公開鍵/秘密鍵のインポート
  4. ECDHによる共通鍵の導出
  5. AESによるデータの暗号化
  6. AESによるデータの復号

楕円曲線による公開鍵と秘密鍵の生成

鍵生成
function keyGen(){
    const ec = {
        name: "ECDH",
        namedCurve: "P-384"
    };
    const usage = ["deriveKey"];

    return crypto.subtle.generateKey(ec, true, usage);
}

generateKey()で、公開鍵と秘密鍵の鍵ペアを生成します。

戻り値はpublicKeyプロパティとprivateKeyプロパティにそれぞれCryptoKeyが入ったCryptoKeyPairというオブジェクトになります。

ecで、使用する暗号アルゴリズムと楕円曲線を指定します。

uaageで、鍵の用途を指定します。
この鍵は鍵交換にのみ使うのでderiveKeyを設定します。

第2引数のbooleanは、鍵のエクスポートを許可するかを指定します。
公開鍵は相手へ送るためにエクスポートしたいのでtrueを設定します。

公開鍵/秘密鍵のエクスポート

エクスポート
async function keyExport(key, isPub){
    const encode = isPub ? "spki" : "pkcs8";

    return new Uint8Array(await crypto.subtle.exportKey(encode, key));
}

exportKey()で、CryptoKeyを各フォーマットのバイナリデータとして出力できます。

戻り値はArrayBufferです。

楕円曲線暗号の場合、秘密鍵はPKCS#8、公開鍵はSPKIでの出力に対応しています。

公開鍵/秘密鍵のインポート

インポート
function keyImport(key, isPub){
    const encode = isPub ? "spki" : "pkcs8";
    const ec = {
        name: "ECDH",
        namedCurve: "P-384"
    };
    const usage = isPub ? [] : ["deriveKey"];

    return crypto.subtle.importKey(encode, key, ec, false, usage);
}

importKey()で、バイナリデータとして出力した鍵をCryptoKeyに戻せます。

戻り値はCryptoKeyです。

使用する暗号アルゴリズム/楕円曲線/用途とエクスポート時のフォーマットは、それぞれ鍵生成/エクスポート時と同一の必要があります。

第4引数のbooleanはエクスポート許可ですが、一度インポートした鍵を再エクスポートするケースは考えにくいのでfalseとします。

ECDHによる共通鍵の導出

共通鍵導出
function keyDerive(pub, priv){
    const aes = {
        name: "AES-GCM",
        length: 256
    };
    const ec = {
        name: "ECDH",
        public: pub
    };
    const usage = ["encrypt", "decrypt"];

    return crypto.subtle.deriveKey(ec, priv, aes, false, usage);
}

deriveKey()で、公開鍵と秘密鍵を使用して共通鍵を導出します。

aesで、導出した鍵を使用する暗号アルゴリズムを指定します。
ここでのlengthプロパティはAESの鍵長(bit数)となります。

ecで、鍵導出を行う暗号アルゴリズムと公開鍵を指定します。

usageは、導出した鍵を暗号化と復号に使うのでencryptdecryptを設定します。

第4引数のbooleanはエクスポート許可ですが、共通鍵がエクスポート出来てしまっては、せっかく安全性を保つために公開鍵で鍵交換した意味が台無しなのでfalseとします。

AESによるデータの暗号化

暗号化
async function aesEncrypt(key, data){
    const aes = {
        name: "AES-GCM",
        iv: crypto.getRandomValues(new Uint8Array(16)),
        tagLength: 128
    };

    const result = await crypto.subtle.encrypt(aes, key, data);

    const buffer = new Uint8Array(aes.iv.byteLength + result.byteLength);
    buffer.set(aes.iv, 0);
    buffer.set(new Uint8Array(result), aes.iv.byteLength);

    return buffer;
}

encrypt()でデータを暗号化します。

入力データはTypedArrayとなります。
戻り値(暗号データ)はArrayBufferとなります。

初期ベクトル(IV)はgetRandomValues()で16byteの乱数を取得します。
IVは公開しても問題ない代わりに、復号時も同じものが必要となるので、処理結果の暗号データの先頭に結合しています。

AESによるデータの復号

復号
async function aesDecrypt(key, data){
    const aes = {
        name: "AES-GCM",
        iv: data.subarray(0, 16),
        tagLength: 128
    };

    return new Uint8Array(await crypto.subtle.decrypt(aes, key, data.subarray(16)));
}

decrypt()で、暗号データを復号します。

入力データはTypedArrayとなります。
戻り値はArrayBufferとなります。

IVは暗号化時に暗号データの先頭に結合したので、今度は先頭16byteをsubarray()で切り分けます。

17byte以降が暗号データ本体なので、これもsubarray()で切り分けます。

試してみる

上記のラッパー関数を用いて実際に暗号化/復号を試してみます。

(async()=>{
    const key1 = await keyGen();
    const key2 = await keyGen();

    const key1ExPub = await keyExport(key1.publicKey, true);
    const key1ExPriv = await keyExport(key1.privateKey, false);
    const key2ExPub = await keyExport(key2.publicKey, true);
    const key2ExPriv = await keyExport(key2.privateKey, false);

    console.log("--- Exported Keys ---");
    for(const key of [key1ExPub, key1ExPriv, key2ExPub, key2ExPriv]){
        console.log(key);
    }

    const key1ImPub = await keyImport(key1ExPub, true);
    const key1ImPriv = await keyImport(key1ExPriv, false);
    const key2ImPub = await keyImport(key2ExPub, true);
    const key2ImPriv = await keyImport(key2ExPriv, false);

    console.log("--- Imported Keys ---");
    for(const key of [key1ImPub, key1ImPriv, key2ImPub, key2ImPriv]){
        console.log(key);
    }

    const keyDe1Pub2Priv = await keyDerive(key1ImPub, key2ImPriv);
    const keyDe2Pub1Priv = await keyDerive(key2ImPub, key1ImPriv);

    console.log("--- Derived Keys ---");
    for(const key of [keyDe1Pub2Priv, keyDe2Pub1Priv]){
        console.log(key);
    }

    const raw = new TextEncoder().encode("hogehoge");

    const enc = await aesEncrypt(keyDe1Pub2Priv, raw);
    const dec = await aesDecrypt(keyDe2Pub1Priv, enc);

    console.log("--- Results ---");
    console.log(new TextDecoder().decode(raw));
    console.log(enc);
    console.log(new TextDecoder().decode(dec));
})();

無事に暗号化と復号が出来てると思います。

プレイグラウンド

(番外編)気になったこと

今回の記事とはあまり関係ないのですが、Web Crypto APIはCurve25519とEd25519には対応していないんだなぁと思って色々調べてるうちに疑問に思った事が...

楕円曲線 鍵交換(ECDH) 署名(ECDSA)
モンゴメリ曲線(Curve25519) X25519 ???
エドワーズ曲線(Ed25519) ??? EdDSA

モンゴメリ曲線を使用した鍵交換はX25519という仕様があり、エドワーズ曲線を使用した署名はEdDSAという仕様があります。

この2つの曲線は双有理同値という、超ざっくり解釈で"対"となる存在との事です。
(ここら辺は詳しくないので深追い言及は避けておきます)

そして、上記テーブルの "???" の部分が問題で、以前 elliptic という楕円曲線暗号ライブラリを用いて鍵交換を実装した時に、たまたま間違えてEd25519鍵をECDH関数に入力してしまったら、実際に共通鍵を導出できてしまいました。

これは、仕様化されていないが対となる曲線だから出来た必然なのか、それともライブラリのバグなのか...
また、同じ理論でCurve25519を使用した署名も可能なのかも気になるところです。

有識者の方がいらっしゃいましたら、ご教示お願い致します。

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

JavaScript for Automation (JXA)

java scriptでMacを自動化できる

アプリケーションの起動

function run(){
    var app = Application("apps");
}

引数を渡してiTuneで曲を再生

function run(argv){
    if(argv.length == 2){
        console.log(argv[0] + " : " + argv[1] + " を再生します");
        var app = Application("iTunes");
        app.playlists[argv[0]].tracks[argv[1]].play();
    }

    else{
        console.log("'プレイリスト' '曲名' を指定してください");
        }
}

scriptの実行

実行

osascript -l JavaScript sample.js

引数を渡して実行

osascript -l JavaScript sample.js testPlay hoge  
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javascript 数字の並び替えゲーム(自分なりの解説)

今回は、書籍ゲームで学ぶJavascript入門で紹介されている
15puzzle知識定着のために自分なりに解説をしてみます。

15puzzleは数字を順番に並び替えていくゲームです。
(これは画像です)
15puzzle.jpg

15puzzle.html
<body onload="init()">
    <table id="table"></table>
</body>
</html>

まず読み込んだら初期化のinit()が起動します。
body onload ="init()"の所です。

15puzzle.html
<body onload="init()">
    <table id="table"></table>
</body>
</html>

初期化(数タイルを表示させる)

初期化ですることは、
1~15までの数字を順番に表示をさせ、その後、順番をバラバラに表示させます。
このようなhtmlを出力できたらOKです。

sample.html
<table id="table">
      <tr>
        <td class="tile">1</td>
        <td class="tile">2</td>
        <td class="tile">3</td>
        <td class="tile">4</td>
      </tr>
      <tr>
        <td class="tile">5</td>
        <td class="tile">6</td>
        <td class="tile">7</td>
        <td class="tile">8</td>
      </tr>
      <tr>
        <td class="tile">9</td>
        <td class="tile">10</td>
        <td class="tile">11</td>
        <td class="tile">12</td>
      </tr>
      <tr>
        <td class="tile">13</td>
        <td class="tile">14</td>
        <td class="tile">15</td>
        <td class="tile">16</td>
      </tr>
    </table>

これをjsで段階的に書いてみると

var tr = document.createElement("tr")
~tdのfor処理4回分
最後にtrの閉じタグをつける

tr-repeat.js
var table = docment.getElementById("table");
var tr = document.createElement("tr");

for (var j=0;j<4;j++){
var td = document.createElement("td");
td.textContent =  j;
td.className ="tile";
tr.appendChild(td); //trの最後の所にtdを追加。閉じタグにて
}
table.appendChild(tr);

これだと0~3までしかできないので
trが4回分をつなげるとこうなります。

tr-repeat.js
var table = document.getElementById("table");

//子のtrが4回分
for (var i = 0;i<4;i++){
var tr = document.createElement("tr");

//孫のtdが4回分
for (var j=0;j<4;j++){
var td = document.createElement("td");
td.textContent =  i*4+j;//0-15まで
td.className ="tile";
tr.appendChild(td); //trの最後の所にtdを追加。閉じタグにて
}
//子の最後にtrの閉じタグを入れる

table.appendChild(tr);
}

見た目的にこれでOKですが
パズル動作をするために取得した要素のプロパティを入れておく必要があります。
また値が0の所は、空白にしてパズルの入れ替えポイントにします。
また数字の入れ替え処理ができるようにclick関数を、onclickに設定します。
click関数は後ほど出ます。

tr-repeat.js
var table = document.getElementById("table");

//子のtrが4回分
for (var i = 0;i<4;i++){
var tr = document.createElement("tr");

//孫のtdが4回分
for (var j=0;j<4;j++){
var td = document.createElement("td");
td.className ="tile";

//値をindexとして
var index = i*4+j;
td.textContent =  index;
td.index = index;
td.value = index;
td.textContent = index == 0 ? "" : index; //三項演算子
td.onclick = click; //onclickにclick関数を設定

tr.appendChild(td); //trの最後の所にtdを追加。閉じタグにて
}
//子の最後にtrの閉じタグを入れる

table.appendChild(tr);
}

三項演算子はif文の省略した書き方です。
index が0なら、""を。 0じゃないならindexを 
「index == 0 ? "" :index;」

その結果を
td.textContentに「代入=」しています。

まだ未完成ですが並び替えを実行するための
click関数を考えます。

click関数は並び替えを行いますが、
並びがどうなってるかjavascript側であらかじめ覚えてくれたほうが楽です。
click関数を呼び出したときに、「えーと、現在の並びがこうだから。。。」と取得してると効率が悪いです。

(googleのスプレットシートとGASの処理で、何度もスプレットシートの値を取得するのでなく、
スプレット全体を配列で一旦全部記憶させて、配列からデータを探させるのと同じ考え方です。)

15puzzle.js
// グローバル変数(並びを入れとくやつ)
        var tiles = [];
// 初期化関数
        function init() {
            var table = document.getElementById("table");

            for (var i = 0 ; i < 4 ; i++) {
                var tr = document.createElement("tr");
                for (var j = 0 ; j < 4 ; j++) {
                    var td = document.createElement("td");
                   td.className = "tile";
                    var index = i * 4 + j;
                  td.index = index;
                    td.value = index;
                    td.textContent = index == 0 ? "" : index;
                    td.onclick = click;
                    tr.appendChild(td);

                  //並びを覚えさせる
                    tiles.push(td);
                }
                table.appendChild(tr);
            }
           }

これでやっと並び替えclick処理を考えられます。

クリック処理

クリックをした時の処理は
「押した数字が隣が空白で、押した数字が空白に移動する」です。
移動の仕方は上下左右なので4パターンとなります。
後ほど記述するswap関数には、入れ替えの場所を引数で渡します。

narabi.jpg

その4パターンのうち、どの処理を行うか選別してるようにします。
画像のようにindexの一般化から値を様子を見ます。

click.js
   function click(e) {
    //クリックした場所を取得
        var i = e.srcElement.index;

        if (tiles[i - 4].value == 0) {
          swap(i, i - 4);//上移動の処理
        } else if (tiles[i + 4].value == 0) {
          swap(i, i + 4);//下移動
        } else if (tiles[i - 1].value == 0) {
          swap(i, i - 1);//左移動
        } else if (tiles[i + 1].value == 0) {
          swap(i, i + 1);//右移動
        }

      }

これで良さそうに見えますが
i=1のときに、tiles[-3]の値を見ようとします。そんな値はないのでエラーが出ちゃいます。
なので、
上移動なら、まずクリックされたものが一番上でないこと。
下移動なら、まずクリックされたものが一番下でないこと。とする必要があります。

click.js
     function click(e) {
        var i = e.srcElement.index;

        if (i - 4 >= 0 && tiles[i - 4].value == 0) {//最上位ではなく、上の値が0なら上移動
          swap(i, i - 4);
        } else if (i + 4 < 16 && tiles[i + 4].value == 0) {
          swap(i, i + 4);
        } else if (i % 4 != 0 && tiles[i - 1].value == 0) {
          swap(i, i - 1);
        } else if (i % 4 != 3 && tiles[i + 1].value == 0) {
          swap(i, i + 1);
        }

      }

並び替えswap関数

並び替える場所を引数に渡せば、並び替えをする関数です。
textContentとvalueを入れ替えています。

swap.js
 function swap(i, j) {
        //一旦格納する
        var tmp = tiles[i].value;
        tiles[i].textContent = tiles[j].textContent;
        tiles[i].value = tiles[j].value;
        tiles[j].textContent = tmp;
        tiles[j].value = tmp;
      }

バラバラに並び替える

ここまでのプログラムですと、順番に数字が並ぶだけです。
一応ゲームのため、あらかじめバラバラに並び替えておく必要があります。
今回はclickしたら、並び替えswapが発動するのでそれを使います。
適当なところをランダムに押すプログラムを置きます。

random.js
for(var i = 0 ;i<1000;i++){
click({srcElement:{index:Math.floor(Math.random()*16)}})
}

0-15のランダムにとり、その位置のsrcElementをクリックされます。
click→swapまでたまたま行ったもので、並び替えが行われます。

最後に

全体図はサンプルコードの配布になってしまうので控えます。
詳しくはこちらの書籍を購入お願いします。

ゲームで学ぶJavascript入門

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

Ajaxを使った非同期通信化実装STEP

1.jQueryを記述するFile作成

image.png

  • app/assets/javascripts内にcomment.js(非同期通信を行うcontroller名.js)を作成。

2.イベントの発火を設定

  • pictweetのcomment-formが送信された時、のイベント設定
comment.js
$(function(){
  $('#new_comment').on('submit', function(e) {
    e.preventDefault();
    var formData = new FormData(this);
    var url = $(this).attr('action')
    $.ajax({
      url: url,
      type: 'POST',
      data: formData,
      dataType: 'json',
      processData: false,
      contentType: false
    })
  })
})
  • フォームが送信された時、というイベントを設定したい場合は、form要素を取得してonメソッドを使う。
  • フォームが送信される時、何も設定していない状態(デフォルトの状態)だとフォームを送信するための通信が行われるため、preventDefault()を使用してデフォルトのイベントを止めます。
  • FormData image.png *new FormData(フォーム要素)とすることでFormDataを作成できます。 今回FormDataオブジェクトの引数はthisとなっていますが、イベントで設定したfunction内でthisを利用した場合は、イベントが発生したノード要素を指します。今回の場合は、new_commentというIDがついたフォームの情報を取得しています。

3.非同期通信で保存する

comment.js
$(function(){
  $('#new_comment').on('submit', function(e) {
    e.preventDefault();
    var formData = new FormData(this);
    var url = $(this).attr('action')
    $.ajax({
      url: url,
      type: 'POST',
      data: formData,
      dataType: 'json',
      processData: false,
      contentType: false
    })
  })
})
  • attrメソッド:要素が持つ指定属性の値を返します。今回はイベントが発生した要素のaction属性の値を取得しており、今回のaction属性にはフォームの送信先のurlの値が入っています。 これでリクエストを送信する先のURLを定義することができました。
  • processDataオプション: デフォルトではtrueになっており、dataに指定したオブジェクトをクエリ文字列に変換する。
  • contentTypeオプション: サーバにデータのファイル形式を伝えるヘッダ。こちらはデフォルトでは「text/xml」でコンテンツタイプをXMLとして返してきます。 ajaxのリクエストがFormDataのときはどちらの値も適切な状態で送ることが可能なため、falseにすることで設定が上書きされることを防ぎます。

4.コメントを保存し、respond_toを使用してHTMLとJSONの場合で処理を分ける

comments_controller.rb
  def create
    @comment = Comment.create(comment_params)
    respond_to do |format|
      format.html { redirect_to tweet_path(params[:tweet_id])  }
      format.json
    end
  end
  • ローカル変数commentは、スコープの関係でこの後のjbuilder側で使用できないので、インスタンス変数@commentに編集します

5. jbuilderを使用して、作成したメッセージをJSON形式で返す

views/comments/create.json.jbuilder
  json.text  @comment.text
  json.user_id  @comment.user.id
  json.user_name  @comment.user.nickname

こうすることによってJavaScriptファイルに返ってきたデータをjbuilderで定義したキーとバリューの形で呼び出して使うことができます。

6.返ってきたJSONをdoneメソッドで受取り、HTMLを作成する

comment.js
$(function(){
  function buildHTML(comment){
    var html = `<p>
                  <strong>
                    <a href=/users/${comment.user_id}>${comment.user_name}</a>
                    :
                  </strong>
                  ${comment.text}
                </p>`
    return html;
  }
  $('#new_comment').on('submit', function(e){
    e.preventDefault();
    var formData = new FormData(this);
    var url = $(this).attr('action');
    $.ajax({
      url: url,
      type: "POST",
      data: formData,
      dataType: 'json',
      processData: false,
      contentType: false
    })
    .done(function(data){
      var html = buildHTML(data);
      $('.comments').append(html);
      $('.textbox').val('');
      $('.form__submit').prop('disabled', false);
    })
  })
});

$('.form__submit').prop('disabled', false);は、htmlの仕様でsubmitボタンを一度押したらdisabled属性という、ボタンが押せなくなる属性が追加されいるので、falseで解除。

7.エラー時の処理を行う

comment.js
    .done(function(data){
      var html = buildHTML(data);
      $('.comments').append(html);
      $('.textbox').val('');
      $('.form__submit').prop('disabled', false);
    })
    .fail(function(){
      alert('error');
    })
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

環境を汚さず(選ばず)Nuxtプロジェクトを作成しGitHub Pagesで公開するまでの一部始終

事前準備

docker がインストールされていることが前提です。
docker が入ってさえいれば Windows, Mac, Linux いずれでも同じように操作できるはずです。

WindowsにDockerを導入する方法は以下にまとめています。

Windows 10 Home に Docker for Desktop をインストールする手順

また、プログラムの編集にはVisualStudioCodeを使いますのでインストールしておいてください。

VisualStudioCodeのインストール手順(Windows)

GitHubからクローンしたりプッシュしたりするのでGitHubのアカウントを用意し git もインストールしておいてください。

Windows に git をインストールする手順


プロジェクト作成

プロジェクトを作る親フォルダに移動
(Windows の場合は例えば C:\dev を作ってコマンドプロンプトで cd \dev で親フォルダとする dev に移動します。)


node.jsが入ったLinuxを起動するため以下のコマンドを実行します。

windows
docker run --rm -itv %cd%:/app node:alpine sh
mac,linux
docker run --rm -itv $PWD:/app node:alpine sh

docker run は指定したイメージ(node:alpnine)からコンテナを作り、指定したコマンド(sh)を実行します。
--rm オプションは実行後にコンテナを削除します。これをつけないと docker run するごとにコンテナが増えていきディスクを圧迫します。
今回は作成したファイルはローカルに同期して残りますので、作業時以外コンテナは不要です。
-it は起動したコンテナに対して入力できるようにするためのオプションです。指定しないとコマンドが即終了してしまいます。
-v でローカルの$PWD or %cd%(カレントディレクトリ)とコンテナ内の /app を同期(ファイルやフォルダの内容がおなじになる)します。


コマンドが実行されると、Dockerコンテナ内のシェルに切り替わります。

Dockerコンテナ内
/ # 

/ は現在ルートディレクトリにいるということ、
# はルートユーザを表しています。


node:alphine イメージを使っているのでnode環境は既にあります。
以下のコマンドで同期している /app フォルダに移動して first_nuxt という名前のプロジェクトを作成します。

Nuxtプロジェクト作成
/ # cd /app && yarn create nuxt-app first_nuxt

yarn create next-app としてしまうとReactベースの Next.js のプロジェクトになってしまいますので間違わないように注意しましょう。


実行すると以下のように表示されます。

プロジェクト名入力
yarn create v1.22.4
[1/4] Resolving packages...
warning create-nuxt-app > sao > micromatch > snapdragon > source-map-resolve > resolve-url@0.2.1: https://github.com/lydell/resolve-url#deprecated
warning create-nuxt-app > sao > micromatch > snapdragon > source-map-resolve > urix@0.1.0: Please see https://github.com/lydell/urix#deprecated
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Installed "create-nuxt-app@3.0.0" with binaries:
      - create-nuxt-app

create-nuxt-app v3.0.0
✨  Generating Nuxt.js project in first_nuxt
? Project name (first_nuxt) 

プロジェクト名を聞かれていますが、コマンド実行時に指定している first_nuxt でいいので、そのまま Enter を押します。


言語選択
? Choose programming language (Use arrow keys)
❯ JavaScript 
  TypeScript 

使うプログラミング言語を聞かれています。今回は JavaScript にしますのでそのまま Enter を押します。


パッケージ管理選択
? Choose the package manager (Use arrow keys)
❯ Yarn 
  Npm 

使うパッケージ管理を聞かれています。今回は最初に Yarn を使っていますので、そのまま Enter を押します。


UIフレームワーク選択
? Choose UI framework (Use arrow keys)
❯ None 
  Ant Design Vue 
  Bootstrap Vue 
  Buefy 
  Bulma 
  Element 
  Framevuerk 
  iView 
  Tachyons 
  Tailwind CSS 
  Vuesax 
  Vuetify.js 

使う UIフレームワークを聞かれています。今回は自分でUIを作るので None(なし)にします。そのまま Enter を押します。
この辺りからはプロジェクト作成後に追加することもできますので、よくわからなければ None で大丈夫です。


モジュール選択
? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◯ Axios
 ◯ Progressive Web App (PWA) Support
 ◯ Content

使うモジュールを聞かれています。
Axios はWebAPIを呼ぶときに使うものです。
PWA Support は Webサービスをスマートフォンのアプリのように使うようにするためのしくみです。
Content はブログ作成のためのモジュールです。
上下矢印キーで選択し、スペースキーを押すと選択/未選択を切り替えられます。
必要なら後から追加できるので、今回はそのまま Enter を押します。


チェックツール選択
? Choose linting tools (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◯ ESLint
 ◯ Prettier
 ◯ Lint staged files
 ◯ StyleLint

使うチェックツールを聞かれています。
今回はVSCodeの拡張機能でチェック等を行うので、そのまま Enter を押します。


テストツール選択
? Choose test framework (Use arrow keys)
❯ None 
  Jest 
  AVA 
  WebdriverIO 

使うテストフレームワークを聞かれています。
後から使いできるのでそのまま、Enter を押します。


レンダリング方式選択
❯ Universal (SSR / Static) 
  Single Page App 

使うレンダリング方式を聞かれています。
SSR(Server Side Rendering)というのはサーバサーイドでHTMLを組み立ててブラウザに返す方式です。
Single Page App(SPA)はブラウザ側でページを組み立てる方式です。
今回は静的ファイルを生成して GitHub Pages に置くのでどちらでもいけますが、そのまま Enter を押します。


開発ツール選択
? Choose development tools (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◯ jsconfig.json (Recommended for VS Code)
 ◯ Semantic Pull Requests

使う開発ツールを聞かれています。
今回はVSCodeを使うので、 jsconfig.json をスペースで選択して Enter を押します。


質問はこれで終わりです。しばらく待っていれば以下のような完了表示が出ます。

Nuxtプロジェクト作成完了
? Choose development tools jsconfig.json (Recommended for VS Code)

?  Successfully created project first_nuxt

  To get started:

    cd first_nuxt
    yarn dev

  To build & start for production:

    cd first_nuxt
    yarn build
    yarn start

Done in 2842.73s.

ls で生成されたフォルダ、ファイルを確認してみましょう。

生成されたファイル確認
/app # ls first_nuxt

ローカルの first_nuxt フォルダにコンテナの first_nuxt 内と同様下記のファイルが作成されていることが確認できます。

生成されたファイル一覧
README.md       components      layouts         node_modules    package.json    plugins         store
assets          jsconfig.json   middleware      nuxt.config.js  pages           static          yarn.lock

これでプロジェクト作成は完了です。


開発モードで起動してみましょう。

まず、プロジェクトフォルダに移動します。

プロジェクトフォルダへ移動
/app # cd first_nuxt

yarn dev コマンドを実行します。

開発サーバ起動
/app/first_nuxt # yarn dev
yarn run v1.22.4
$ nuxt

ℹ NuxtJS collects completely anonymous data about usage.                                                                                                     12:06:56
  This will help us improving Nuxt developer experience over the time.
  Read more on https://git.io/nuxt-telemetry

? Are you interested in participation? (Y/n) 

使用状況の匿名データ収集をしてもいいか聞かれますので、OKならY、NGならnを入力し、Enterを押します。


   ╭───────────────────────────────────────╮
   │                                       │
   │   Nuxt.js @ v2.13.0                   │
   │                                       │
   │   ▸ Environment: development          │
   │   ▸ Rendering:   server-side          │
   │   ▸ Target:      server               │
   │                                       │
   │   Listening: http://localhost:3000/   │
   │                                       │
   ╰───────────────────────────────────────╯

ℹ Preparing project for development                                                                                                                          12:11:17
ℹ Initial build may take a while                                                                                                                             12:11:17
✔ Builder initialized                                                                                                                                        12:11:17
✔ Nuxt files generated                                                                                                                                       12:11:17

✔ Client
  Compiled successfully in 10.22s

✔ Server
  Compiled successfully in 10.14s

ℹ Waiting for file changes                                                                                                                                   12:11:33
ℹ Memory usage: 125 MB (RSS: 211 MB)                                                                                                                         12:11:33
ℹ Listening on: http://localhost:3000/                                                                                                                       12:11:33

Listening on の行が表示されれば起動完了です。
コンテナ内の3000番ポートで動作しています。


ブラウザでの動作確認

起動した画面をブラウザで確認したいですよね。
でも今の状態だと、ブラウザで http://localhost:3000 にアクセスしても何も表示されません。
image.png

なぜかというと、コンテナ内の 3000 ポートは何も設定しないとローカル(PC)からアクセスできないようになっているからです。

ですので、いったんコンテナを終了して、設定を追加してもう一度コンテナを起動します。
Ctrl+Cを押して開発サーバを終了させて、 exit コマンドでコンテナから抜けます。

/app/first_nuxt # exit

まず、作成したプロジェクトフォルダに移動します。

共通
cd first_nuxt

以下のコマンドで開発サーバを起動します。

windows
docker run --rm -itv %cd%:/app -p 80:3000 -e "HOST=0.0.0.0" -e "NUXT_TELEMETRY_DISABLED=1" -w /app node:alpine yarn dev
max,linux
docker run --rm -itv $PWD:/app -p 80:3000 -e "HOST=0.0.0.0" -e "NUXT_TELEMETRY_DISABLED=1" -w /app node:alpine yarn dev

いくつかオプションが増えていますが、意味は次の通りです。
-v %cd%:/app は先程と同じ記述ですが、first_nuxtがカレントフォルダなので、first_nuxt とコンテナ内の /app が同期するようなります。
-p 80:3000 とするとコンテナの3000番ポートがローカルの80番ポートにつながります。
-e "HOST=0.0.0.0" でコンテナの環境変数 HOST に 0.0.0.0 が設定され開発サーバがコンテナ以外のホスト(今回はローカル)からつながるようになります。
-e "NUXT_TELEMETRY_DISABLED=1" で使用状況の匿名データ収集について聞いてこなくなります。
-w でコンテナのカレントディレクトリをプロジェクトのディレクトリに設定しています。
sh を yarn dev に変えていきなり開発サーバを起動するようにします。

これで、ブラウザを開いて http://localhost と入れればコンテナ内に起動したページを見ることができます。

image.png


ソース編集と自動更新の設定

これで開発できる状態になりました。
さっそく、表示されたトップページを変更してみます。

first_nuxt フォルダを VisualStudioCode で開きます。
image.png
Windowsならフォルダ右クリックで Code で開くを選択します。
(VisualStudioCodeを起動してからフォルダを開くでもOKです)

image.png
作成されたフォルダやファイルが表示されています。


トップページの編集

pages フォルダ内に index.vue というファイルがあります。
image.png
これがトップページの内容になります。

<template> タグ内が表示内容を構成するHTMLです。(通常のHTMLだけではなくVueの記法が使えるようになっています)

<h1>タグ内にタイトルの文字 first_nuxt があるので、
「はじめてのNuxt」に変えてみましょう。
image.png
(ファイルを編集するとファイル名タブに●がつきます。保存すると消えます。)


変更の反映

WindowsならCtrl+S、MacならCommand+Sでファイルが保存できます。

保存すると、おそらくMacやLinux環境ならすぐにブラウザの内容が自動更新され文字が変わります。
image.png

Windows環境やうまく自動更新されない場合は、次の設定をしてみてください。


(Windowsで)自動更新が効かない場合の設定

nuxt.config.js ファイルを開き、末尾に以下の記述を追加して保存します。

変更前
  build: {
  }
}
変更後
  build: {
  },
  watchers: {
    webpack: {
        poll: true
    }
  }
}

そしていったん docker コンテナから抜けて(CTRL+C)以下のコマンドで再度開発環境を起動します。

windows
docker run --rm -itv %cd%:/app -p 80:3000 -e "HOST=0.0.0.0" -e "NUXT_TELEMETRY_DISABLED=1" -w /app node:alpine yarn dev
max,linux
docker run --rm -itv $PWD:/app -p 80:3000 -e "HOST=0.0.0.0" -e "NUXT_TELEMETRY_DISABLED=1" -w /app node:alpine yarn dev

これでソースを変更して保存するとすぐにブラウザの表示内容が切り替わるようになります。
image.png

試しに、タイトルを「Nuxtはじめました」に変えてみましょう。
image.png

保存してブラウザを見るとすぐに内容が変わります。
image.png


拡張機能を入れる

vueファイルを見やすくしたり、ミスを教えてくれたりする拡張機能を入れておきます。

画面左の上から6番目のボタンimage.pngを押します。

検索欄に vetur と入れて Vetur という拡張機能を探します。
見つけたら install ボタンを押しまてインストールします。

image.png

インストールできると以下のようにに内容が色分けして表示されます(シンタックスハイライトといいます)
image.png


余分なポートの通信を止める

ブラウザでF12を押し開発者ツールを出します。
そうすると、以下のようにエラーが出ているのが確認できると思います。
image.png
エラーを見ると localhost の 40791 ポートにアクセスしようとしてエラーになっています。
(このポート番号はプロジェクトごとにランダムで変わります。)
このポートも -p 80:3000 としたように Docker 内のポートにつなげないと通信できません。

これは、Nuxtの自動ロード時に進捗率を表示するために使われているようです。
https://ja.nuxtjs.org/api/configuration-build/#indicator
image.png
エラーは出てほしくないのでこの設定をOFFにします。

nuxt.config.js ファイルを開き、以下のように変更して保存します。

変更前
  build: {
  },
変更後
  build: {
    indicator: false
  },

もしくは、ONのままにしたい場合は、docker run 実行時に -p 40791:40791 のようにポートの通信を通すようにします。


github pages に公開する

まだトップページしかありませんが、動作はするので、これを Github pages にデプロイしてみましょう。

静的ファイルの生成

docker コンテナを一度終了して、以下のコマンドを実行します。

windows
docker run --rm -itv %cd%:/app -e "NUXT_TELEMETRY_DISABLED=1" -w /app node:alpine yarn generate
max,linux
docker run --rm -itv $PWD:/app -e "NUXT_TELEMETRY_DISABLED=1" -w /app node:alpine yarn generate
実行結果
yarn run v1.22.4
$ nuxt generate

ℹ NuxtJS collects completely anonymous data about usage.                                             07:35:21
  This will help us improving Nuxt developer experience over the time.
  Read more on https://git.io/nuxt-telemetry

? Are you interested in participation? No

ℹ Production build                                                                                   07:35:42
ℹ Bundling for server and client side                                                                07:35:42
ℹ Target: static                                                                                     07:35:42
✔ Builder initialized                                                                                07:35:42
✔ Nuxt files generated                                                                               07:35:42

✔ Client
  Compiled successfully in 20.32s

✔ Server
  Compiled successfully in 601.69ms


Hash: f6c77d51cae28b7be227
Version: webpack 4.43.0
Time: 20320ms
Built at: 06/20/2020 7:36:19 AM
                         Asset       Size  Chunks                         Chunk Names
../server/client.manifest.json   6.68 KiB          [emitted]
                      LICENSES  389 bytes          [emitted]
                app.656ba39.js   51.3 KiB       0  [emitted] [immutable]  app
        commons.app.4dd2efa.js    154 KiB       1  [emitted] [immutable]  commons.app
        pages/index.573dd1d.js   2.85 KiB       2  [emitted] [immutable]  pages/index
            runtime.b17d028.js   2.32 KiB       3  [emitted] [immutable]  runtime
 + 2 hidden assets
Entrypoint app = runtime.b17d028.js commons.app.4dd2efa.js app.656ba39.js

Hash: 28f709d14cc9b922eefa
Version: webpack 4.43.0
Time: 603ms
Built at: 06/20/2020 7:36:19 AM
               Asset       Size  Chunks             Chunk Names
      pages/index.js   11.9 KiB       1  [emitted]  pages/index
           server.js   80.5 KiB       0  [emitted]  app
server.manifest.json  207 bytes          [emitted]
 + 2 hidden assets
Entrypoint app = server.js server.js.map
ℹ Generating output directory: dist/                                                                 07:36:19
ℹ Generating pages                                                                                   07:36:19
✔ Generated route "/"                                                                                07:36:19
✔ Client-side fallback created: 200.html                                                             07:36:20
Done in 72.08s.

dist というフォルダに静的ファイルが生成されました。
image.png

これらのファイルをレンタルサーバ等に置けばインターネット上に公開できます。

今回はこのファイルを GitHub Pages で公開します。
GitHub には GitHub Actions というコマンド実行機能がありますので、それを使って、push 時今の生成操作を自動実行して公開できるように設定していきます。


GitHub Actions 用ファイルの作成

FIRST_NUXT フォルダ直下に .github/workflows フォルダを作成します。

具体的には、nuxt.config.jsなど直下のファイルを選択した状態で
フォルダ作成ボタンimage.pngを押します。

出てきたフォルダ名入力欄に .github/workflows を入力します。(先頭のピリオドを忘れないでください)
image.png

フォルダが作成されます。
image.png


作成したworkflowフォルダを選択した状態で、ファイル作成ボタンimage.pngを押して、gh-pages.yml ファイルを作成します。

image.png

ファイルの内容は以下の通り入力し、保存します。
image.png

.github/workflows/gh-pages.yml
name: github pages

on:
  push:
    branches:
      - master

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v1 
      - run: yarn generate
      - uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./dist

GitHubリポジトリの作成

下記の記事にGitHubのアカウント作成からプッシュするまでをまとめていますので参考にして、
リポジトリ作成まで(「ローカルにクローンする」の手前まで)進めてください。

GitHubのアカウントを作成して、リポジトリ作成、クローン、コミット、プッシュまでの手順


ローカルのGit管理開始

ローカルで Git の初期化を行います。

first_nuxt フォルダ上で git init コマンドを実行します。

git初期化
git init

image.png

初期化したあと、VisualStudioCodeのソース管理image.pngを見ると、
gitでまだコミットされていないファイルが列挙されてきます。
image.png


GitHubリポジトリをリモートリポジトリとしてセット

ローカルのgitリポジトリとGitHubのリポジトリを関連付けます。

リモートリポジトリ設定
git remote add origin https://github.com/GitHubアカウント名/リポジトリ名.git

※GitHubアカウント名、リポジトリ名はご自身で作成したものを指定します。

登録されたかどうかは git remote -v で確認できます。
image.png


コミットする

VisualStudioCodeでソース管理の上部分にコミットコメントを入力します。
今回は「初コミット」としています。
image.png

コミットボタンimage.pngを押します。

ステージされていないファイルをコミットするかの確認が出ます。全ファイルコミットするので Yes を押します。
image.png

以下の画面が出る場合はCancelボタンを押して、git config コマンドで名前とメールアドレスを設定してから再度コミットしてください。
image.png

名前、メールアドレス設定
git config user.name あなたの名前
git config user.email GitHubに登録したメールアドレス

コミットされると、コメント欄、一覧が空になります。
image.png


GitHub に push

その他ボタンimage.pngを押し、Push to...を選択します。
image.png

origin を選択します。
image.png

2回目以降はPush先が記録されるので Push to ではなく Push で実行可能です。


GitHubにログインしていない場合は以下の画面が出ます。ログインします。
image.png


上向きの矢印が出ない状態になっていればPushされています。

●Pushされている状態
image.png

●Pushされていない状態
image.png


動作確認

Pushすると GitHub Actions が実行され GitHub Pages にページが公開されているはずです。

公開先URLは以下の通りです。
https://GitHubのユーザ名.github.io/プロジェクト名(今回はfirst_nuxt)/

例えば私が作ったURLは
https://github-japanese-user.github.io/first_nuxt/
になります。
image.png

もし、404エラーが表示される場合は正しくHTMLが登録されている確認します。
image.png


GitHub Pages に公開されているかを確認する

GitHubのリポジトリページに行き、Branchボタンを押して、gh-pages を選択します。
gh-pages というのが生成されたHTMLが格納されているブランチになります。

image.png

gh-pages のファイル内に index.html があれば生成は成功しています。

image.png


次に、設定を確認します。

同じ画面の右上にある Settings ボタンimage.pngを押します。
image.png

設定画面の下を見ていくと、GitHub Pages の設定部分があります。
image.png

ここの Source が gh-pages branch になっているか確認します。
また、緑で Your site・・・と表示されていればリンクをクリックすればサイトが表示されます。
もし、設定もあっていて、index.html も生成されているのに、リンクが表示されていない場合は、
一旦、Sourceの部分を master に切り替えて、

https://GitHubのユーザ名.github.io/プロジェクト名(今回はfirst_nuxt)/README.md
がブラウザで表示できることを確認してから、
再度、Sourceの部分を gh-pages に切り替えるとうまくいくようです。


URLの調整

これで、完成のように見えますが、トップページの裏でエラーが発生しています。

F12で開発者ツールを出し、Networkタブを開くと、404エラーになっているファイルが見つかります。
image.png

これは、通常Nuxtで作ったプロジェクトはルートで動作する設定になっているためです。

https://GitHubのユーザ名.github.io/
が基準となっており、

実際のルートとなる
https://GitHubのユーザ名.github.io/プロジェクト名(今回はfirst_nuxt)/
とズレがあるため、相対パスがおかしくなるのが原因です。

最後にこれを解決する設定を行います。


VisualStudioCode で nuxt.config.js を開き、以下の通り変更します。

nuxt.config.js変更前
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  /*
  ** Global CSS
  */
nuxt.config.js変更後
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/first_nuxt/favicon.ico' }
    ]
  },
  router: {
    base: '/first_nuxt/'
  }
  /*
  ** Global CSS
  */

linkのhrefの修正と、router: の追加です。

これで、相対パスのルートの設定が / から /first_nuxt/ に変更されます。


最終確認

変更したソースをコミットして、プッシュします。
変更内容はソース管理の nuxt.config.js を選択すると確認できます。
image.png
変更前と後の内容が左右に並び、変更した行がマークされています。
これをみて間違いがないかを確認できたら、コミットコメント
image.png
を入力して、コミットボタンimage.pngを押します。


つづけて、その他ボタンから Push を実行します。
image.png


Push すると GitHub Action が動作しますので、GitHubのリポジトリページから Actions タブを開いて
動作が完了しているか確認してみましょう。

image.png
コミットコメントの左が黄色マークの場合はまだ実行中です。

image.png
緑のチェックマークになれば実行完了です。


ブラウザでトップページをリロードして404エラーが消えていることを確認します。
image.png


長くなりましたが、以上ですべての作業が完了しました!

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

P5.js 日本語リファレンス(textFont)

このページでは「P5.js 日本語リファレンス」 の textFont関数を説明します。

textFont()

説明文

text() で描画される現在のフォントを設定します。

WEBGLモードのとき:loadFont() を介して読み込まれたフォントのみがサポートされます。

構文

textFont()

textFont(font, [size])

パラメタ

  • font
    Object | String:loadFont() を介して読み込まれたフォント、またはWebセーフフォント(すべてのシステムで一般的に利用可能なフォント)を表す文字列

  • size
    Number:使用するフォントサイズ(オプション、デフォルト:12)

戻り値

Object:現在のフォント

例1

let fontRegular,  fontBold;

function preload() {
  fontRegular = loadFont('assets/Inconsolata-Regular.ttf');
  fontBold = loadFont('assets/Inconsolata-Bold.ttf');
}

function setup()  {
  createCanvas(300, 300);
  background(210);
  fill(0)
   .strokeWeight(0)
   .textSize(24);
  textFont(fontRegular);
  text('Font Style Normal', 10, 30);
  textFont(fontBold);
  text('Font Style Bold', 10, 70);
}

実行結果

https://editor.p5js.org/bit0101/sketches/t1EsW2OD_

著作権

p5.js was created by Lauren McCarthy and is developed by a community of collaborators, with support from the Processing Foundation and NYU ITP. Identity and graphic design by Jerel Johnson.

ライセンス

Creative Commons(CC BY-NC-SA 4.0) に従います。

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

P5.js 日本語リファレンス(text)

このページでは「P5.js 日本語リファレンス」 の text関数を説明します。

text()

説明文

画面にテキストを描画します。
パラメータで指定された位置に、画面の最初のパラメータで指定された情報を表示します。

textFont() でフォントが設定されていないときはデフォルトのフォントが使用され、textSize() でフォントが設定されていないときはデフォルトのサイズが使用されます。 fill() を使用してテキストの色を変更できます。
stroke() および strokeWeight() を使用して、テキストのアウトラインを変更できます。

テキストは、座標の左、右および中心に描画するオプションを提供する textAlign() に関連して表示されます。

x2およびy2パラメータはテキストを表示する長方形の領域を定義します。これらのパラメータを指定すると、現在のrectMode() 設定に基づいて解釈されます。指定された長方形に完全に収まらないテキストは画面に描画されません。
x2とy2が指定されていない場合、ベースラインの配置がデフォルトになります。つまり、テキストはxとyから上方に描画されます。

WEBGL モードのとき:opentype / truetype フォントのみがサポートされています。 loadFont() を使用してフォントをロードする必要があります。(例2参照) 現在、stroke() は WEBGL モードでは効果がありません。

構文

テキスト(str, x, y, [x2], [y2])

パラメタ

  • str
    String | Object | Array | Number | Boolean:表示される英数字記号

  • x
    Number:テキストのx座標

  • y
    Number:テキストのy座標

  • x2
    Number:デフォルトではテキストボックスの幅。詳細については rectMode() を参照してください(オプション)

  • y2
    Number:デフォルトではテキストボックスの高さ。詳細については rectMode() を参照してください(オプション)

例1

function setup(){
  createCanvas(300,300);
  let s = 'The quick brown fox jumped over the lazy dog.';
  fill(0, 102, 183);
  text(s, 10, 10, 90, 80); //テキストボックス内でテキストを折り返します
}

実行結果

https://editor.p5js.org/bit0101/sketches/xw0xWCaqK

例2

let inconsolata;
function preload()  {
  inconsolata = loadFont('assets/Inconsolata-Regular.ttf');
}
function setup()  {
  createCanvas(300, 300, WEBGL);
  textFont(inconsolata);
  textSize(width / 3);
  textAlign(CENTER, CENTER);
}
function draw()  {
  background(0);
  let time = millis() ;
  rotateX(time / 1000);
  rotateZ(time / 1234);
  text('inconsolata', 0, 0);
}

実行結果

https://editor.p5js.org/bit0101/sketches/05dmkLqex

著作権

p5.js was created by Lauren McCarthy and is developed by a community of collaborators, with support from the Processing Foundation and NYU ITP. Identity and graphic design by Jerel Johnson.

ライセンス

Creative Commons(CC BY-NC-SA 4.0) に従います。

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

P5.js 日本語リファレンス(p5.Font)

このページでは「P5.js 日本語リファレンス」 の p5.Font を説明します。

p5.Font

説明文

フォントの基本クラス

構文

new p5.Font([pInst])

パラメタ

  • pInst
    P5:p5インスタンス(オプション)

Fields

  • font
    根本的なOpenTypeフォントの実装

Methods

  • textBounds()
    このフォントを使用して、指定されたテキスト文字列のタイトなバウンディングボックスを返します(現在は単一行のみをサポートしています)

  • textToPoints()
    指定されたテキストのパスをたどる点の配列を計算します

著作権

p5.js was created by Lauren McCarthy and is developed by a community of collaborators, with support from the Processing Foundation and NYU ITP. Identity and graphic design by Jerel Johnson.

ライセンス

Creative Commons(CC BY-NC-SA 4.0) に従います。

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

P5.js 日本語リファレンス(loadFont)

このページでは「P5.js 日本語リファレンス」 の loadFont関数を説明します。

loadFont()

ファイルまたはURLからOpenTypeフォントファイル(.otf, .ttf)を読み込み、PFontオブジェクトを返します。このメソッドは非同期です。つまり、スケッチの次の行が実行される前に終了しない場合があります。

フォントへのパスは、スケッチでリンクするHTMLファイルからの相対パスである必要があります。ブラウザの組み込みセキュリティにより、URLまたはその他のリモートの場所からのフォントの読み込みがブロックされる場合があります。

preload() 内でloadFont() を呼び出すと、setup() および draw() が呼び出される前にロード操作が完了することが保証されます。

構文

loadFont(path, [callback], [onError])

パラメタ

  • path
    String:ロードするファイルまたはURLの名前

  • callback
    Function:loadFont() の完了後に実行される関数(オプション)

  • onError
    Function:エラーが発生した場合に実行される関数(オプション)

戻り値

p5.Font:p5.Fontオブジェクト

例1

let myFont;
function preload() {
  myFont = loadFont('assets/Inconsolata-Regular.ttf');
}

function setup() {
  createCanvas(300, 300);

  fill(' #ED225D');
  textFont(myFont);
  textSize(28);
  text('Inconsolata', 10, 50);
}

実行結果

https://editor.p5js.org/bit0101/sketches/NWNLqM70C

例2

preload() の外で、オブジェクトを処理するためのコールバック関数を提供できます。

function setup() {  // preload() でないことに注意
  loadFont('assets / inconsolata.otf', drawText);
}

function drawText(font){
  fill(' #ED225D');
  textFont(font, 36);
  text('p5*js', 10, 50);
}

実行結果

https://editor.p5js.org/bit0101/sketches/6D-zSrbwm

著作権

p5.js was created by Lauren McCarthy and is developed by a community of collaborators, with support from the Processing Foundation and NYU ITP. Identity and graphic design by Jerel Johnson.

ライセンス

Creative Commons(CC BY-NC-SA 4.0) に従います。

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

診断アプリの作る方法(jQueryのみで実装)

概要

診断アプリを作りました。

こんな感じ
Image from Gyazo
こちらの診断アプリをJS(jQuery)のみで実装したので記事をしました。

まずは、ビューを用意する。

#wrapper
  .question
    %h1.title ソーシャルスタイル診断
    %ul.questions
      %li.textBox
        %span.text Q1. 冷静で人からの指示は嫌い
        %label.yes
          %input.typeA.typeD{:name => "q01", :type => "radio"}>/
          YES
        %label.no
          %input.typeB.typeC{:name => "q01", :type => "radio"}>/
          NO
      %li.textBox
        %span.text Q2. 自分の話をすることを好む
        %label.yes
          %input.typeB.typeA{:name => "q02", :type => "radio"}>/
          YES
        %label.no
          %input.typeC.typeD{:name => "q02", :type => "radio"}>/
          NO
      %li.textBox
        %span.text Q3. 周りの意見を大事にする方だ
        %label.yes
          %input.typeC.typeD{:name => "q03", :type => "radio"}>/
          YES
        %label.no
          %input.typeA.typeB{:name => "q03", :type => "radio"}>/
          NO
      %li.textBox
        %span.text Q4. 情報を集めたり分析することが得意だ
        %label.yes
          %input.typeD.typeC{:name => "q04", :type => "radio"}>/
          YES
        %label.no
          %input.typeA.typeB{:name => "q04", :type => "radio"}>/
          NO
      %li.textBox
        %span.text Q5. 戦略立案や勝負事への興味が強い
        %label.yes
          %input.typeA.typeD{:name => "q05", :type => "radio"}>/
          YES
        %label.no
          %input.typeB.typeC{:name => "q05", :type => "radio"}>/
          NO
      %li.textBox
        %span.text Q6. ノリがよくムードメーカーだと言われる
        %label.yes
          %input.typeB.typeC{:name => "q06", :type => "radio"}>/
          YES
        %label.no
          %input.typeA.typeD{:name => "q06", :type => "radio"}>/
          NO
      %li.textBox
        %span.text Q7. 気配り上手、聞き上手と言われる
        %label.yes
          %input.typeC.typeD{:name => "q07", :type => "radio"}>/
          YES
        %label.no
          %input.typeA.typeB{:name => "q07", :type => "radio"}>/
          NO
      %li.textBox
        %span.text Q8. 感情を表に出すのは、苦手だ
        %label.yes
          %input.typeD.typeA{:name => "q08", :type => "radio"}>/
          YES
        %label.no
          %input.typeC.typeB{:name => "q08", :type => "radio"}>/
          NO
    %button 診断する
  .result.ResultA
    .ResultA__recommend あなたの性格は・・・・
    .ResultA__Amenu
      .ResultA__Amenu__text 前進型・行動派
    %p あなたは迅速かつ合理的に仕事を進めるタイプです。ビジネスライクな性格で、プロセスよりも結果を重視し、決断力に優れています。経営者に多いタイプに多いとされます。
    = image_tag("close-up-face-fashion-fine-looking-450212.jpg", width: "306px",class: "image")
  .result.ResultB
    .ResultB__recommend あなたの性格は・・・・
    .ResultB__Bmenu
      .ResultB__Bmenu__text 直感型・感覚派
    %p あなたは、周りから注目されることを好むタイプです。ビジネス面では、自ら先頭に立って人を率いていく傾向にあります。
    = image_tag("men-s-white-button-up-dress-shirt-708440.jpg", width: "306px",class: "image")
  .result.ResultC
    .ResultC__recommend あなたの性格は・・・・
    .ResultC__Cmenu
      .ResultC__Cmenu__text 温和型・協調派
    %p あなたは、どこにいてもみんなの調停役になるタイプです。周囲の気持ちに敏感で、自分の話をするよりも相手の話に耳を傾ける傾向にあります。
    = image_tag("woman-wearing-teal-dress-sitting-on-chair-talking-to-man-2422280.jpg", width: "306px",class: "image")
  .result.ResultD
    .ResultD__recommend あなたの性格は・・・・
    .ResultD__Dmenu
      .ResultD__Dmenu__text 分析型・思考派
    %p あなたは、独特の価値観や雰囲気を持っていて、周囲に影響されにくいマイペースな人です。仕事においては、データの収集や分析に黙々と取り組みます。
    = image_tag("person-using-a-laptop-3183131.jpg", width: "306px",class: "image")

次にJS(jQuery)を用意する。

$(function(){
  //ボタンがクリックされた時
  $("button").on("click", function(){
  //一度結果を非表示にする
    $(".result").hide();   
    //問題数を取得
    var qNum = $("ul li").length;
    if( $("ul li input:checked").length < qNum ){
      //全てチェックしていなかったらアラートを出す
      alert("未回答の問題があります");
    } 
    else {
        //チェックされているinputの数を取得
        var typeANum = $(".typeA:checked").length,
            typeBNum = $(".typeB:checked").length,
            typeCNum = $(".typeC:checked").length,
            typeDNum = $(".typeD:checked").length,
            typeENum = $(".typeD:checked").length;
        if( typeANum >= typeBNum && typeANum >= typeCNum && typeANum >= typeDNum && typeANum >= typeENum) {
            $(".ResultA").fadeIn();
        } else if( typeBNum >= typeANum && typeBNum >= typeCNum && typeBNum >= typeDNum && typeBNum >= typeENum) {
            $(".ResultB").fadeIn();
        } else if( typeCNum >= typeANum && typeCNum >= typeBNum && typeCNum >= typeDNum && typeCNum >= typeENum) {
            $(".ResultC").fadeIn();
        } else if( typeDNum >= typeBNum && typeDNum >= typeCNum && typeDNum >= typeANum && typeDNum >= typeENum) {
            $(".ResultD").fadeIn();
        } else if( typeENum >= typeBNum && typeENum >= typeCNum && typeENum >= typeANum && typeENum >= typeDNum) {
            $(".ResultE").fadeIn();
      }
    }
  });
});

解説

ちょい説明します。

  .result.ResultA
    .ResultA__recommend あなたの性格は・・・・
    .ResultA__Amenu
      .ResultA__Amenu__text 前進型・行動派
    %p あなたは迅速かつ合理的に仕事を進めるタイプです。ビジネスライクな性格で、プロセスよりも結果を重視し、決断力に優れています。経営者に多いタイプに多いとされます。
    = image_tag("close-up-face-fashion-fine-looking-450212.jpg", width: "306px",class: "image")

回答結果となる.result.ResultAは、CSSでdisplay: none;により普段は非表示にしています。

if( typeANum >= typeBNum && typeANum >= typeCNum && typeANum >= typeDNum && typeANum >= typeENum) {
    $(".ResultA").fadeIn();

もし.typeA.typeB .typeC .typeD .typeEよりinputの数(.length)が多い場合は、

$(".ResultA").fadeIn();

fadeInメソッドで、非表示にされていた.result.ResultAをフェードイン表示させます。


こんな感じで.typeB以下のif文が同じ要領で続きます。

これを応用すれば、色んな診断アプリを作れそうです。

補足

hamlからHTMLに変換する場合は、以下の記事を参考にしてください。
https://qiita.com/chezou/items/0e9bd4f9eb8314dc2aec#hamlhtml%E5%A4%89%E6%8F%9B

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

【Rails】ancestryを用いた多階層カテゴリー機能の実装『Bootstrap3でウィンドウ作ってみた編』

目標

ezgif.com-video-to-gif.gif

開発環境

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

前提

下記実装済み。

Slim導入
Bootstrap3導入
Font Awesome導入
ログイン機能実装
投稿機能実装
多対多のカテゴリー機能実装
多階層カテゴリー機能実装(準備編)
多階層カテゴリー機能実装(seed編)
多階層カテゴリー機能実装(作成フォーム編)
多階層カテゴリー機能実装(編集フォーム編)

1.コントローラーを編集

homes_controller.rb
# 追記
def category_window
  @children = Category.find(params[:parent_id]).children
end

【解説】

① Ajax通信で送られてきたパラメーターに対応するカテゴリーの、子カテゴリーを抽出し、インスタンス変数に代入する。

@children = Category.find(params[:parent_id]).children

2.json.jbuilderファイルを作成・編集

ターミナル
$ touch app/views/homes/category_window.json.jbuilder
category_window.json.jbuilder
json.array! @children do |children|
  json.id children.id
  json.name children.name
end

【解説】

get_category_childrenアクションで抽出したレコードを繰り返し処理し、配列を作成する。

json.array! @children do |children|

② 各IDと名前を で作成した配列に格納する。

json.id children.id
json.name children.name

◎ 親カテゴリー(ビジネス)にマウスが乗っている場合の返り値

[
  {
    "id": 2, 
    "name": "金融"
  },
  {
    "id": 6, 
    "name": "経済"
  },
  {
    "id": 9, 
    "name": "経営"
  },
  {
    "id": 13, 
    "name": "マーケティング"
  },
]

◎ 子カテゴリー(金融)にマウスが乗っている場合の返り値

[
  {
    "id": 3, 
    "name": "株"
  },
  {
    "id": 4, 
    "name": "為替"
  },
  {
    "id": 5, 
    "name": "税金"
  },
]

3.ルーティングを追加

routes.rb
# 追記
get 'get_category/new', to: 'homes#category_window', defaults: { format: 'json' }

4.ビューを編集

application.html.slim
body
  header
    nav.navbar.navbar-default.navbar-fixed-top
      .container-fluid
        ul.nav.navbar-nav.navbar-right
          li.dropdown role='presentation'
            a.dropdown-toggle data-toggle='dropdown' href='#' role='button' aria-expanded='false'
              i.fas.fa-list-ul
              span
                |  カテゴリーから探す
              span.caret
            ul.dropdown-menu role='menu'
              li role='presentation'
                - Category.where(ancestry: nil).each do |parent|
                  = link_to parent.name, root_path, id: "#{parent.id}", class: 'parent-category'
              br
              li role='presentation' class='children-list'
              br
              li role='presentation' class='grandchildren-list'

【解説】

※Bootstrapの書き方については省略します。

① ancestryの値がnil、つまり親カテゴリーを全て抽出し、プルダウンメニューに表示する。

- Category.where(ancestry: nil).each do |parent|
  = link_to parent.name, root_path, id: "#{parent.id}", class: 'parent-category'

② 子カテゴリーを表示する場所を用意する。

li role='presentation' class='children-list'

③ 孫カテゴリーを表示する場所を用意する。

li role='presentation' class='grandchildren-list'

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

ターミナル
$ touch app/assets/javascripts/category_window.js
category_window.js
$(function() {
  function buildChildHTML(children) {
    let html = `
      <a class="children-category" id="${children.id}" href="/">
        ${children.name}
      </a>
    `;
    return html;
  }

  $('.parent-category').on('mouseover', function() {
    let id = this.id;
    $('.children-category').remove();
    $('.grandchildren-category').remove();
    $.ajax({
      type: 'GET',
      url: '/get_category/new',
      data: {
        parent_id: id,
      },
      dataType: 'json',
    }).done(function(children) {
      children.forEach(function(child) {
        let html = buildChildHTML(child);
        $('.children-list').append(html);
      });
    });
  });

  function buildGrandChildHTML(children) {
    let html = `
      <a class="grandchildren-category" id="${children.id}" href="/">
        ${children.name}
      </a>
    `;
    return html;
  }

  $(document).on('mouseover', '.children-category', function() {
    let id = this.id;
    $.ajax({
      type: 'GET',
      url: '/get_category/new',
      data: {
        parent_id: id,
      },
      dataType: 'json',
    }).done(function(children) {
      children.forEach(function(child) {
        let html = buildGrandChildHTML(child);
        $('.grandchildren-list').append(html);
      });
      $(document).on('mouseover', '.children-category', function() {
        $('.grandchildren-category').remove();
      });
    });
  });
});

【解説】

① 子カテゴリーのHTMLを作成する。

function buildChildHTML(children) {
  let html = `
    <a class="children-category" id="${children.id}" href="/">
      ${children.name}
    </a>
  `;
  return html;
}

② どの親カテゴリーにマウスが乗っているかによって、子カテゴリーの表示内容を変更する。

  $('.parent-category').on('mouseover', function() {
    let id = this.id;
    $('.children-category').remove();
    $('.grandchildren-category').remove();
    $.ajax({
      type: 'GET',
      url: '/get_category/new',
      data: {
        parent_id: id,
      },
      dataType: 'json',
    }).done(function(children) {
      children.forEach(function(child) {
        let html = buildChildHTML(child);
        $('.children-list').append(html);
      });
    });
  });

◎ 親カテゴリーにマウスが乗った時に発火するイベントを作成する。

$('.parent-category').on('mouseover', function() {});

category_window.json.jbuilderから送られてきたIDを、変数へ代入する。

let id = this.id;

◎ とりあえず子カテゴリー以下を削除しておく。

$('.children-category').remove();
$('.grandchildren-category').remove();

◎ パラメーター(parent_id)に先ほど作成した変数を設定して、category_windowアクションを非同期で実行する。

  $.ajax({
    type: 'GET',
    url: '/get_category/new',
    data: {
      parent_id: id,
    },
    dataType: 'json',
  })

◎ Ajax通信が成功した場合は対応する子カテゴリーのHTMLを作成し、表示する。

.done(function(children) {
  children.forEach(function(child) {
    var html = buildChildHTML(child);
    $('.children-list').append(html);
  });
});

③孫カテゴリーのHTMLを作成する。

function buildGrandChildHTML(children) {
  var html = `
    <a class="grandchildren-category" id="${children.id}" href="/">
      ${children.name}
    </a>
  `;
  return html;
}

④ どの子カテゴリーにマウスが乗っているかによって、孫カテゴリーの表示内容を変更する。( とほぼ同じなので説明は省略)

$(document).on('mouseover', '.children-category', function() {
  var id = this.id;
  $.ajax({
    type: 'GET',
    url: '/get_category/new',
    data: {
      parent_id: id,
    },
    dataType: 'json',
  }).done(function(children) {
    children.forEach(function(child) {
      var html = buildGrandChildHTML(child);
      $('.grandchildren-list').append(html);
    });
    $(document).on('mouseover', '.children-category', function() {
      $('.grandchildren-category').remove();
    });
  });
});

注意

turbolinksを無効化しないとプルダウンメニューが非同期で動作しないので、必ず無効化しておきましょう。

turbolinksを無効化する方法

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

ancestryを用いた多階層カテゴリー機能の実装『編集フォーム編』

目標

ezgif.com-video-to-gif.gif

開発環境

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

前提

下記実装済み。

Slim導入
Bootstrap3導入
Font Awesome導入
ログイン機能実装
投稿機能実装
多対多のカテゴリー機能実装
多階層カテゴリー機能実装(準備編)
多階層カテゴリー機能実装(seed編)
多階層カテゴリー機能実装(作成フォーム編)

実装

1.コントローラーを編集

books_controller.rb
def edit
  unless @book.user == current_user
    redirect_to books_path
  end
  @category_parent_array = Category.category_parent_array_create
end

def update
  if @book.update(book_params)
    book_categories = BookCategory.where(book_id: @book.id)
    book_categories.destroy_all
    BookCategory.maltilevel_category_create(
      @book,
      params[:parent_id],
      params[:children_id],
      params[:grandchildren_id]
    )
    redirect_to @book
  else
    @category_parent_array = Category.category_parent_array_create
    render 'edit'
  end
end

【解説】

① 中間テーブルから編集する本に対応するレコードを全て抽出し、削除する。

book_categories = BookCategory.where(book_id: @book.id)
book_categories.destroy_all

2.ビューを編集

books/edit.html.slim
/ 追記
.category-form
  = label_tag 'ジャンル'
  = select_tag 'parent_id', options_for_select(@category_parent_array), class: 'form-control', id: 'parent-category'
  i.fas.fa-chevron-down
br

注意

turbolinksを無効化しないとセレクトボックスが非同期で動作しないので、必ず無効化しておきましょう。

turbolinksを無効化する方法

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

【Rails】ancestryを用いた多階層カテゴリー機能の実装『編集フォーム編』

目標

ezgif.com-video-to-gif.gif

開発環境

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

前提

下記実装済み。

Slim導入
Bootstrap3導入
Font Awesome導入
ログイン機能実装
投稿機能実装
多対多のカテゴリー機能実装
多階層カテゴリー機能実装(準備編)
多階層カテゴリー機能実装(seed編)
多階層カテゴリー機能実装(作成フォーム編)

実装

1.コントローラーを編集

books_controller.rb
def edit
  unless @book.user == current_user
    redirect_to books_path
  end
  @category_parent_array = Category.category_parent_array_create
end

def update
  if @book.update(book_params)
    book_categories = BookCategory.where(book_id: @book.id)
    book_categories.destroy_all
    BookCategory.maltilevel_category_create(
      @book,
      params[:parent_id],
      params[:children_id],
      params[:grandchildren_id]
    )
    redirect_to @book
  else
    @category_parent_array = Category.category_parent_array_create
    render 'edit'
  end
end

【解説】

① 中間テーブルから編集する本に対応するレコードを全て抽出し、削除する。

book_categories = BookCategory.where(book_id: @book.id)
book_categories.destroy_all

2.ビューを編集

books/edit.html.slim
/ 追記
.category-form
  = label_tag 'ジャンル'
  = select_tag 'parent_id', options_for_select(@category_parent_array), class: 'form-control', id: 'parent-category'
  i.fas.fa-chevron-down
br

注意

turbolinksを無効化しないとセレクトボックスが非同期で動作しないので、必ず無効化しておきましょう。

turbolinksを無効化する方法

続編

多階層カテゴリー機能実装(Bootstrapeでウィンドウ作ってみた編)

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

ESLint v7.3.0

v7.2.0 | 次 (2020-07-04 JST)

ESLint 7.3.0 がリリースされました。小さな機能追加とバグ修正が含まれています。

  • Optional Chaining 構文対応はこちらで進行中です。もうしばらくお待ちください。
  • airbnb共有設定の利用者にリグレッションが発生しているようです: #13427

質問やバグ報告等ありましたら、お気軽にこちらまでお寄せください。

? 日本語 Issue 管理リポジトリ
? 日本語サポート チャット (招待リンク)
? 本家リポジトリ
? 本家サポート チャット (招待リンク)


[PR] ESLint は開発リソースを確保するための寄付を募っています。
応援してくださると嬉しいです。


✨ 本体への機能追加

特になし

? 新しいルール

no-promise-executor-return

? #12648

Promiseコンストラクタに渡す関数にて値を返すreturn文を書くとエラーにするルールが追加されました。値を返すのではなくresolve()を呼ぶ必要があります。

/* eslint no-promise-executor-return: error */

//✘ BAD
new Promise((resolve, reject) => {
    if (someCondition) {
        return defaultResult;
    }
    getSomething((err, result) => {
        if (err) {
            reject(err);
        } else {
            resolve(result);
        }
    });
});

//✔ GOOD
new Promise((resolve, reject) => {
    if (someCondition) {
        resolve(defaultResult);
        return;
    }
    getSomething((err, result) => {
        if (err) {
            reject(err);
        } else {
            resolve(result);
        }
    });
});

» Online Demo

no-unreachable-loop

? #12660

ループしないループ構文をエラーにするルールが追加されました。

/* eslint no-unreachable-loop: error */

//✘ BAD
for (let i = 0; i < arr.length; i++) {
    if (arr[i].name === myName) {
        doSomething(arr[i]);
        // break was supposed to be here
    }
    break;
}

» Online Demo

? オプションが追加されたルール

特になし

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

call()を使うとthisをコントロールすることができる(ドラゴンボール編)

thisのコントロール

開眼!javascriptを読んでて
thisとcallの動きについて知ったことがあり、書きますその2。
(※from 6章の6.5より。)

その1はこちら。
◼️グローバルなとこで使うthisはまじでグローバル

thisレベル(thisに対する自分の理解感覚値)

関数とかの中でthis出てきたら「これ」の意味通り、その関数自体を指し示している、、、くらいの知識を持っていて、よくわかんなくなってもconsole.log()で中身見ればいいやくらいの理解度でした。
あとアロー関数の書き方くらいはわかるくらいの感じです!

call()を使うとthisの生き方を決めれるぞ

どういう話かというと・・・

通常、「関数の中のthis」は、その関数自体を指し示します。
が、関数のcall()というものを使うと、任意の引数をthisの向き先に変えることができるぞ!というもの。

みんな大好きドラゴンボールの孫悟飯に例えます。
孫悟飯は子供の頃、学者になりたかったのですが、ピッコロに出会い、修行を受け、戦闘能力が上がり戦闘民族としての才能を開花させていきます。サイヤ人襲来編あたりの話です。

まず、「孫悟飯の情報を出力する無名関数」を作り、gohan変数に入れてみます。

test.js
//gohan変数に、関数を入れる。
var gohan = function(a,b){
    this.life = a; //御飯の人生は引数a
    this.sentouryoku = b; //御飯の戦闘力は引数b
    console.log(this.life,this.sentouryoku) //御飯の人生と戦闘力を表示。
};

この関数ですが、受け取ったa、bの引数をもとに生活と戦闘力を決め、
gohan自体の情報を出力するというものです。
実際に、学者を目指してた子供自体の孫悟飯でやってみましょう。
初期のご飯の戦闘力は1ですね。ラディッツに切れた一発ぶちかましたあとがくんと戦闘力が下がるやつです。

test.js
var gohan = function(a,b){
    this.life = a;
    this.sentouryoku = b;
    console.log(this.life,this.sentouryoku) 

};

gohan("学者",1);
//この結果は 学者 1 となります。


で、call()関数。

ここまでの内容を前提として、call()のなかのthisの扱いについてです。
これは、関数.call(a,b,c)とすると、その関数のthisの向き先を、「関数自体」ではなく、「a」にする、、、というものです。

わかりづらいので、とりあえず
孫悟飯の人生と戦闘力を、ピッコロに会うことで変えてみましょう。

孫悟飯の人生をピッコロと会うことで変える

test.js
//ピッコロオブジェクトを作る
var picoro = {} 

//この関数自体は同じです。
var gohan = function(a,b){
    this.life = a;
    this.sentouryoku = b;
    console.log(this.life,this.sentouryoku) 
};

//call関数。
gohan.call(picoro,"戦闘民族",2300);
//結果は、、、戦闘民族 2300

何が起きたのか?

gohan.call(picoro,"戦闘民族",2300);

この記述の解説です。

先ほど書いたように、call関数は
一つ目の引数(つまりピッコロ)、を、this(孫悟飯の人生)の向き先にします。

孫悟飯は、出会った人の影響で人生が変わるわけなので、
つまりピッコロオブジェクトの中に、御飯の情報が入るわけです。

ここまで書いて少しこの例は違うなと気づき始めましたが、軌道修正も大変なのでこのまま行きます。

ピッコロオブジェクトに対して、つまりピッコロの人生の影響を受けて
御飯の人生もピッコロと同じようなものになったということです。
何を言ってるのかわからなくなってきましたが、どうあれこの結果は
戦闘民族 2300 とこうなります!

ちなみに、この記述の末尾に
console.log(picoro);
すると、きちんとピッコロオブジェクトの中身が出力されます。

赤字のとこ。
Document.png
ピッコロオブジェクトの中にきちんと御飯の人生が入ってますね。
やっぱりこの例はなんか違いますが、call()の動きはこういうものです!はい成功!

捕捉

ちなみに、apply()
という関数も同じようなことができます。
違いとしては、applyでは、引数を配列で渡せる、というものです。

gohan.call(picoro,"戦闘民族",2300);

のところを

gohan.apply(picoro,["戦闘民族",2300]);

とできるということです。
※結果は同じになります。

・参考
call関数

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

Next.jsの環境変数の使い方の基本と、Vercelの環境変数との関係性について

Next.jsの公式サイトDefault Environment Variables を参考に、Next.jsの環境変数の取り扱い方法と、Vercel上の環境変数との関係性について調べ、ユースケースをまとめてみました。

Next.jsの環境変数の取り扱い方法について

  • 後述の通り、環境変数の設定ファイルにXXXXX=Yの形で環境変数を定義しておけば、プログラム中でprocess.env.XXXXX の形式で環境変数を参照できる。

  • ただし、普通に環境変数を定義した場合、これらはブラウザ上からは見えないので(サーバーサイドで実行されるコードでのみ見える)、ブラウザ上で環境変数を使えるようにしたい場合(例えば、クライアントサイドでHTTP Requestを送信したい場合に環境変数を使いたい場合)には、環境変数名をNEXT_PUBLIC_で始めるようにすれば(ex. NEXT_PUBLIC_XXXXX)、クライアントサイドのJavaScript上から参照できる。

.env系

特徴

リポジトリに含めても良い環境変数を書く。

各ファイルの特徴

  • .env

    • すべての環境のデフォルト設定となる。
  • .env.development

    • next dev で起動した際に使われる。
    • .env に優先する。
  • .env.production

    • next start で起動した際に使われる。
    • .env に優先する。

.env.local系

特徴

リポジトリに含めてはいけない環境変数を書く。(シークレット等)
リポジトリのトラッキング対象とはしない。

各ファイルの特徴

  • .env.local

    • 上記の.env系のすべて(.env, .env.developmentもしくは.env.production)に優先する。
  • .env.development.local

    • next dev で起動した際に使われる。
    • .env, .env.development, .env.local に優先する。
  • .env.production.local

    • next start で起動した際に使われる。
    • .env, .env.production, .env.local に優先する。

Next.jsでどこに何を記述すべきなのか

  • .env

    • 環境に影響されない設定かつシークレット系ではない設定を記述。
      • ex. リージョン名とか
    • リポジトリでのトラッキング対象に含めるファイル。
  • .env.{ENV}

    • 環境によって変わる設定かつシークレット系ではない設定を記述。
      • ex. バケット名とか
    • リポジトリでのトラッキング対象に含めるファイル。
  • .env.local

    • 環境に影響されない設定かつシークレット系の設定を記述。
      • ex. 環境に左右されない接続情報とか
    • リポジトリでのトラッキング対象に含めてはいけないファイル。
    • この情報をVercel上に管理する画面はないので、このファイルは使わずに冗長的に.env.{ENV}.localに書くのも一つの手。
  • .env.{ENV}.local

    • 環境によって変わる設定かつシークレット系の設定を記述。
      • ex. 環境ごとのアクセスキーとか
    • リポジトリでのトラッキング対象に含めてはいけないファイル。

Next.jsの各環境変数ファイルの優先度

  • .env.{ENV}.local > .env.local > .env.{ENV} > .env となる。

  • つまり、全てのファイルに環境変数を定義した場合には、.env.{ENV}.localの値が使われることになる。

Vercel上の環境変数とVercelコマンドについて

  • Vercel上では、Next.jsで設定した環境変数ファイル(.env, .env.production, .env.development)がそのまま使用される。

    • .env.developmentで設定された環境変数は、Previewで使用される。
  • .env.{ENV}.localで設定していた環境変数は、GUI上で、Production用、Preview/Development用にそれぞれ設定できるので、画面から普通にセットすればよい。

    • ただし、環境によらないシークレットを保存する方法がVercelにはないので、.env.localの情報をVercelでは設定できない。
  • Vercel上でDevelopment環境変数を設定している場合、vercel env pull [ファイル名。デフォルトは.env] で、指定したファイル名、もしくは.envにdevelopment系の環境変数を引き抜くことができる。

    • 上述の通り、.env.local を管理する方法がVercelにはないので、.local.envを使っている場合にはその内容もpullされてくることになる。
  • 上記のNext.jsの環境変数のスタンダードとして、シークレット系は.localに書くのがいいので、もしVercel上に環境変数を設定していたら、vercel env pull .env.local で環境変数を引き抜きが良いかな・・・。

参考

Default Environment Variables

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