20200909のJavaScriptに関する記事は22件です。

FirebaseのCloud Firestoreと連携して掲示板を作る【nuxt.js】

FirebaseとNuxt.jsで掲示板を作りました。削除ボタンが機能しなかったり未完成の部分があります。

この記事は

  • Firebaseを活用してみたい
  • Nuxt.jsでアプリ作ってみたい

という人向けに書いてみました。
「こんな感じでやれば短時間でFirebaseと連携できます」的なものを記事にしました。作っていく際に参考にした記事と躓いた点も書いていきます。中途半端な出来ですが初心者向けに書いてみました!

Firebaseとは何?

Firebaseはリアルタイムでデータ同期ができるモバイルプラットフォームです。Firebaseを使うとサーバを立てたり、管理や保守がいらなくなるのでフロントサイドの作成に集中できます。

NuxtJSとは何?

NuxtJSは、Vueファイルで記述できるフレームワークです。モジュール構造で拡張でき一部分から徐々に採用することが可能で、静的なページから複雑なwebアプリケーションまで、あらゆるものの作成に使用できます。

以下公式から抜粋

本質的に汎用性があり、さまざまなターゲット(サーバー、サーバーレス、または静的)をサポートし、サーバーサイドのレンダリングは切り替えることができます。
強力なモジュールエコシステムにより拡張可能で、REST や GraphQL エンドポイント、お気に入りの CMS や CSS フレームワークなどさまざまなものに簡単に接続できます。
NuxtJS は Vue.js プロジェクトのバックボーンであり、柔軟でありながら自信を持ってプロジェクトを構築するための構造を提供します。

めちゃくちゃ簡易的な掲示板

できたもの

実際に利用できます!コメントしてみてください!
http://keiziban.tk/about/

機能

名前、コメントの投稿がきる

作成手順

任意のフォルダーに移動してVScodeのTerminalより
yarnを使って以下のコマンドでNuxtプロジェクトを作成することができます。

yarn create nuxt-app cloudfireTest

を実行すると、UIフレームワークなどの機能が必要か聞かれるので選択していき、終了したら
yarn run devで起動させます。無事に終了したら以下のアドレスにアクセスして実行されているか見ます。
http://localhost:3000

続いて

yarn add firebase

でインストールて完了です。
今回にUIフレームワークはBootstrapVueを選択したので、こちらでどのようなスタイルになるのかを調べました。

フォルダ構造

以下のフォルダとファイルで構成されています。
Image from Gyazo

pages

pagesディレクトリに.vueファイルを作成するとページのルーティングができます、index.vue/になり、about.vue/aboutでアクセスできます

about.vue
<template>
  <div class="container">
   <div class="jumbotron">
        <div class="container">
          <h1 class="display-3">掲示板</h1>
        </div>

      <div class="links">
        <client-only placeholder="Loading...">
        <Memo />
        </client-only>

      </div>
    </div>
  </div>
</template>

<script>

import Memo from '~/components/Memo.vue'


export default {
  components: {
    Memo
  }
}
</script>

以下の部分でComponents内に作るMemo.vueが呼び出されます。

<client-only placeholder="Loading...">
        <Memo />
        </client-only>

Components

何度も利用可能な.vueで構成されています。今回のFirebaseとの連携はこちらに書いていきます。まずはComponentsMemo.vueを追加。
Image from Gyazo

Memo.vue
<template>
    <div>
         <p>
            <b-form-input v-model="name" placeholder="名前"></b-form-input>
            <b-form-textarea input v-model="age" placeholder="コメント" id="button"></b-form-textarea> 
            <b-button id="button" size="sm" variant="outline-success" v-on:click="post(); removetext()">投稿</b-button>
        </p>
        <ul v-for="(data, index) in allData" :key="data.id" class="menu-list" >

            <li>

                名前:{{data.name}} <br>
                コメント:{{data.age}}<br>
                <b-button size="sm" variant="outline-danger" class="delete" @click="switchDelateAlarm(); getIndex(index)">
                削除
                </b-button>

            </li>
            </ul>
            <div v-show="showDelateAlarm" id="overlay">
                <div id="delateAlarm">
                    <p>コメントを削除します</p>
                        <b-button size="sm" variant="outline-dark" v-on:click="closeModal">
                        戻る
                        </b-button>
                        <b-button  size="sm" variant="outline-danger" @click="switchDelateAlarm(); deleteItem(data.id)">
                        削除
                        </b-button>
                </div>
            </div>
    </div>
</template>

<script>
import firebase from "firebase/app";
import "firebase/firestore";



export default {
    components: {},

    data(){
        return{
            db: {},
            allData: [],
            name: '',
            age: '',
            showDelateAlarm: false,
            id: [],

        }
    },



    methods: {
        init: () => {
            const config = {
                apiKey: "",
                authDomain: "",
                databaseURL: "",
                projectId: "",
                storageBucket: "",
                messagingSenderId: "",
                appId: "",
                measurementId: ""
            };
            firebase.initializeApp(config);
        },

        post: function(){
            const testId = firebase.firestore().collection('memos').doc().id; //ユニークなIDを生成
            const docRef = firebase.firestore().collection('memos').doc(testId);
            const setAda = docRef.set({
                name: this.name,
                age: this.age,
                createdAt: new Date()
            });
            this.get();
        },

        get: function(){
            this.allData = [];
            firebase.firestore().collection('memos').get().then(snapshot => {
                snapshot.forEach(doc => {
                    this.id = doc.id;
                    console.log(this.id); 
                    const array = [];
                    console.log(array);
                    this.allData.push(doc.data());
                })
            });

        },
        getIndex: function() {
            console.log(firebase.firestore().collection('memos'));

        },
        deleteItem: function(deleteId, getIndex) {
            const db = firebase.firestore();
            db.collection("memos").doc(deleteId).delete().then(function() {
            console.log("Document successfully deleted!");

            }).catch(function(error) {
                console.error("Error removing document: ", error);
            });
            this.showDelateAlarm = false;
            this.get();
        },


        switchDelateAlarm: function() {
            this.showDelateAlarm = true
        },
        closeModal: function(){
           this.showDelateAlarm = false
        },
        removetext: function() {
       this.name = '';
       this.age = '';

    },
    },

    mounted(){
        this.init();
        this.get();
    },


}


</script>

<style>
#delateAlarm{
  z-index:2;
  width:50%;
  padding: 1em;
  background:#fff;
}

#overlay{
  /* 要素を重ねた時の順番 */

  z-index:1;

  /* 画面全体を覆う設定 */
  position:fixed;
  top:0;
  left:0;
  width:100%;
  height:100%;
  background-color:rgba(0,0,0,0.5);

  /* 画面の中央に要素を表示させる設定 */
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 10%;

}
ul {
    display: block;
    margin-block-start: 1em;
    margin-block-end: 1em;
    margin-inline-start: 0px;
    margin-inline-end: 0px;
    padding: 0px;
    list-style: none;
}
#button {
    margin-top: 2px;
}

</style>


Cloud Firestoreの設定

こちらの部分にファイヤーベースの設定→全般の下のほうにあるマイアプリ
からコピー&ペーストします。
Image from Gyazo

Memo.vue
const config = {
                apiKey: "",
                authDomain: "",
                databaseURL: "",
                projectId: "",
                storageBucket: "",
                messagingSenderId: "",
                appId: "",
                measurementId: ""
            };

今回はあらかじめCloud Firestoreのコレクションにmemos作成しておきます。

Image from Gyazo

設定

以下はpostの部分を抜粋しています。

Memo.vue
post: function(){
            const testId = firebase.firestore().collection('memos').doc().id; //ユニークなIDを生成
            const docRef = firebase.firestore().collection('memos').doc(testId);
            const setAda = docRef.set({
                name: this.name,
                age: this.age,
                createdAt: new Date()
            });
            this.get();
        },

Image from Gyazo

躓いた部分

削除ボタンの実装に躓きました。ボタンに紐づいたドキュメントidが取得できずボタンは配置されていますが、機能しません。

参考にした記事

終わりに

読んでいただきありがとうございました、よくわからん部分もあったと思います。私自身わかっていない部分が多いですが、とにかく一言いってやりたいという方!是非こちらで絡んでやってください。

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

Expo x React Native DebuggerでUIデバッグ

概要

SimulatorでWeb開発でやるようなInspectを出来るようにする方法を書いておきます。

React Native Debuggerをインストール

caskでreact-native-debuggerをインストールします。

$ brew update && brew cask install react-native-debugger

React Native Debuggerの設定をExpo用に変更

React Native Debuggerを開いて、Debugger > Open Config Fileを選択します。

image.png

.rndebuggerrcという設定ファイルが開かれるので、defaultRNPackagerPortsをExpo用に19001に変更します。

- defaultRNPackagerPorts: [8080]
+ defaultRNPackagerPorts: [19001],

デバッグ方法

上記までが終わったら、一度expoを再起動します。

これしなくてエラーが出た

再起動したら、Simulatorを立ち上げます。
以下はiOS Simulatorで進めます。

SimulatorでDebug JS RemotelyをONに

Simulator上でCmd + Dでメニューを開きます。
すると以下のようにメニューが出るので、Debug Remote JSをタップします。

image.png

React Native Debuggerを起動

React Native Debuggerを起動して、iOS Simulatorをリフレッシュすると以下のような小さいメニューが出るので、InspectをタップするとWeb開発のようなInspectが出来るようになります。

image.png

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

Ajaxの非同期通信の考察

Ajaxの非同期通信の考察

同期通信では一度のデータ通信で全てのデータが更新されていた。
非同期通信では一部のデータのみでデータ処理を行なっている。

何が違うのか。。。?

◆Railsの同期通信

URIもしくはPermitを元に、MVCでactionによる処理を行い、データ通信をしている。全体の更新を必要とする。

◆非同期通信

DOMでHTML(←階層構造)から作ったDOMツリー(←も階層構造)の先っちょにあるURLパラメーターを元に、XMLHttpRequestにより設定したリクエストとレスポンスの方法により、必要な一部分だけでデータ通信している。

全体を更新しなくても一部の処理を一部だけで行えるようになる!

◆ URIパラメーター達

・queryパラメーター

http://sample.jp/?name=kii
の?以降の情報。
HTMLの要素のことを表してる?

・pathパラメーター

http://tweets.jp/tweets/1
のようなパラメーターで、「リソースを識別する場合」に使用する。
データベースでidとかをつけると使えるのかなってゆう感じ

まだはっきり理解してないので曖昧表現使ってます。(違ったら指摘お願いします。)

↑のやつをElementsかqueryのselectorで指定して、その部分のHTML要素のデータを単独でデータ通信していく。

◆XMLHttpRequest(以降XML)

これを使ってMVCで行なっていたようなデータ通信を作っていく。
ユーザーが見ている非同期通信の行えるwebサイトにはすでこれが組み込まれている?

・open

非同期通信のリクエストを何のHTTPメソッドでどんなパスでどの方式で行うのかを決める。

XML.open(HTTPメソッド, `パス`, true);

trueだと非同期通信。

・responseType

レスポンスをどんなデータの形でしてもらうか決める。

XML.responseType = "json";

これだとjsonの形式で返してくれる。

・send

この記述によって、この内容が組み込まれたwebサイトを見ているユーザー側から、リクエストの送信を実行する。

・・・最後に

まだはっきりと理解できてないので、今のところの認識で書いています。
言い回しの違いや、認識の間違いなどあれば指摘してくださると幸いです。

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

axios を async/await で書き直した時の課題をメモ

概要

request が deprecated になっており、トレンドも axios に移っているようだ。

なので、コードの置き換えを検討している。async/await にすると可読性があがるかもと試した内容を記載する。

結論

結論からいうと因果関係のある処理を async/await に置き換えると異常でも後続する処理が動くため微妙。
因果関係がある場合、論理的にも入れ子で正しいし、今回検証したパターンだと入れ子のままでいいと思う。

サンプルコード: then()を使う

ユーザー認証をして得たトークンを元にリクエストをする例。
簡単すぎるコードでは検証にならないのでやや複雑なコードになっている。

'use strict'

const axios = require('axios');

axios(
  {
    url: 'https://httpbin.org/post?token=XXXXXXXX',
//  url: 'https://httpbin.org/status/500?token=XXXXXXXX',
    headers: {
      'Content-Type': 'application/json'
    },
    method: 'post',
    data: {
      id: 'john.doe',
      pass: '********'
    }
  }
).then(function (responseToken) {
  console.log(`Request URI: ${responseToken.config.url}`);
  const token = responseToken.data.args.token;
  console.log(token);
  axios(
    {
      url: 'https://httpbin.org/get?foo=bar',
      headers: {
        'Authorization': 'Bearer ' + token
     },
     method: 'get'
    }
  ).then(function (response) {
    console.log(`Request URI: ${response.config.url}`);
    console.log(`response body: ${JSON.stringify(response.data.args)}`);
  }).catch(function(error) {
    console.log(`Request URI: ${error.config.url}`);
    console.log(`${error.response.status} ${error.response.statusText}`);
    return `${error.response.status} ${error.response.statusText}`
  })
}).catch(function(error) {
  console.log(`Request URI: ${error.config.url}`);
  console.log(`${error.response.status} ${error.response.statusText}`);
  return `${error.response.status} ${error.response.statusText}`
})

正常時の実行結果

Request URI: https://httpbin.org/post?token=XXXXXXXX
XXXXXXXX
Request URI: https://httpbin.org/get?foo=bar
response body: {"foo":"bar"}

異常時の実行結果
入れ子なので token 取得に失敗したら、 /get?foo=bar の処理は実行はしない。期待通りの動作。

Request URI: https://httpbin.org/status/500?token=XXXXXXXX
500 INTERNAL SERVER ERROR

サンプルコード: await/asyncを使う

ちょっとわかりにくいと思うが、await axios() await axios() と同じレベルでコードが並んでいる。入れ子でないので、前のリクエストの結果に依存する処理が増えても可読性が悪くはならない。

ただ、異常系を考えると微妙。

'use strict'

const axios = require('axios');

async function main() {
  const token = await axios(
    {
//    url: 'https://httpbin.org/post?token=XXXXXXXX',
      url: 'https://httpbin.org/status/500?token=XXXXXXXX',
      headers: {
        'Content-Type': 'application/json'
      },
      method: 'post',
      data: {
        id: 'john.doe',
        pass: '********'
      }
    }
  ).then(function(responseToken) {
    const token = responseToken.data.args.token;
    console.log(`Request URI: ${responseToken.config.url}`);
    console.log(`token: ${token}`);
    return token
  }).catch(function(error) {
    console.log(`Request URI: ${error.config.url}`);
    console.log(`${error.response.status} ${error.response.statusText}`);
    return {code: 1, detail: `${error.response.status} ${error.response.statusText}`}
  })

  await axios(
    {
      url: 'https://httpbin.org/get?foo=bar',
      headers: {
        'Authorization': 'Bearer ' + token
     },
     method: 'get'
    }
  ).then(function(response) {
    console.log(`Request URI: ${response.config.url}`);
    console.log(`response body: ${JSON.stringify(response.data.args)}`);
  }).catch(function(error) {
    console.log(`Request URI: ${error.config.url}`);
    console.log(`${error.response.status} ${error.response.statusText}`);
    return `${error.response.status} ${error.response.statusText}`
  })
}

try {
  main()
} catch(e) {
  console.log(e)
}

正常時の実行結果

Request URI: https://httpbin.org/post?token=XXXXXXXX
token: XXXXXXXX
Request URI: https://httpbin.org/get?foo=bar
response body: {"foo":"bar"}

異常時の実行結果
token 取得に失敗したのに、後続の処理を実行してしまっている。

Request URI: https://httpbin.org/status/500?token=XXXXXXXX
500 INTERNAL SERVER ERROR
Request URI: https://httpbin.org/get?foo=bar
response body: {"foo":"bar"}

完了は待ってくれる が、正常も異常もお構いなし。
ならば、catch句で throw だと思ったが、Promise中のthrowは非推奨。そのうちNode.jsのプロセスを殺害するつもりだという。自プログラムでなく外部サービスの問題でクラッシュさせるのは微妙だ。

参考1
次のように throw してみた結果。
throw {code: 1, detail: `${error.response.status} ${error.response.statusText}`}

Request URI: https://httpbin.org/status/500?token=XXXXXXXX
500 INTERNAL SERVER ERROR
(node:885) UnhandledPromiseRejectionWarning: #<Object>
(node:885) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
(node:885) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

参考2
次のように reject してみた結果。

Promise.reject(`${error.response.status} ${error.response.statusText}`)

Request URI: https://httpbin.org/status/500?token=XXXXXXXX
500 INTERNAL SERVER ERROR
(node:852) UnhandledPromiseRejectionWarning: 500 INTERNAL SERVER ERROR
(node:852) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
(node:852) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Request URI: https://httpbin.org/get?foo=bar
response body: {"foo":"bar"}

参考

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

【JavaScript】関数における、引数値を定数で定義することの利点

同じ変数を用いる複数の関数を実行したい場合、引数値を定数で定義すべき、という内容。

例)

//関数1
function intro(name, food) {
  console.log(`私の名前は${name}です。好物は${food}です。`);
}

//関数2
function lunch(food) {
  console.log(`今日の昼ごはんは${food}を食べます。`);
}

どちらの関数も、仮引数foodを使っている。

これらに対して、引数で定義し出力をする場合は、

intro("田中","カレー"); //仮引数(name,food)に引数を与え、関数の実行。
lunch("カレー"); //上記と同様。

となる。
一方で、引数で定義するのではなく、定数で定義し出力する場合は、

const name = "田中"; //定数nameに文字列"田中"を代入。
const food = "カレー"; //上記と同様。
intro(name,food); //仮引数(name,food)に定数(name,food)を与え、関数の実行。
intro(food); //上記と同様。

となる。

一見、前者の方が記述が簡素で可読性に優れると思うが、メンテナンスの観点でいえば、後者の方が利便性に優れる。
仮に、food = "ラーメン";に修正したい場合、前者は修正箇所が2つ、後者は定数定義1つで済む。

同じ変数を用いる関数が増えれば増えるほど、後者の方がコード管理しやすいと考える。

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

jscodeshift with TypeScript

はじめに

以前、jscodeshitという JavaScriptおよびTypeScriptコードのリファクタリングツールの紹介記事を書きました。

JavaScriptのリファクタリングツール「jscodeshift」の使い方

このツールは、ASTベースでコードを自在に変換でき、とても便利です。jscodeshiftは、Transform File(変換処理を記述するファイル)として変換処理を書くのですが、これまでJavaScriptで変換処理を書くことが多かったです。

しかし、Transform FileはTypeScriptで書くことが可能です。

最近、TypeScriptで変換処理を書いた際、その開発体験がとても良かったので紹介します。ASTベースのコーディングにTypeScriptの型推論や補完があると、開発が快適になることは想像しやすいでしょう。

この記事で出てくるコードやファイルは以下のリポジトリで確認できます。

jscodeshift-with-typescript

セットアップ

まずは、jscodeshiftのTransform FileをTypeScriptで書く準備をします。

ライブラリのインストール

必要なライブラリをインストールします。typescriptの他に、@types/jscodeshiftをインストールします。

$ npm i -D jscodeshift typescript @types/jscodeshift

tsconfig.json

tsconfig.jsonを作成します。設定はお好みで調整してください。

$ npx tsc --init

TypeScriptで変換処理を書く

セットアップが終わったら、TypeScriptでTransform Fileを作成します。

jscodeshift + TypeScriptの土台となる記述

jscodeshift + TypeScriptでのコーディングの土台となる記述を書きます。
型推論により、型注釈を消すことはできますが、説明の便宜上書いています。

import { Transform, FileInfo, API } from "jscodeshift";

const transform: Transform = ({ source }: FileInfo, { jscodeshift }: API) => {
  const j = jscodeshift;
  return j(source).toSource();
};

export default transform;

あとは、補完が効くので、快適に開発を進めていくことができます。

変換処理 foo.bar()をfoo.baz()に変換

前回の記事でもシンプルな例として出した、foo.bar()をfoo.baz()に変換処理を書いていきます。

といっても、主に利用する関数や値は型推論があるので、JavaScriptの時と比べても記述量は変わりません。規模が大きくなり、変換処理を関数として切り出す際などに型注釈を付与することになるでしょう。

対象のNodeを見つける

  // ...
  const j = jscodeshift;
  return j(source)
    .find(j.CallExpression, {
      callee: {
        object: { name: "foo" },
        property: { name: "bar" },
      },
    })
  // ...
};

export default transform;

find関数にわたすAST NodeのTypeを選択する際も、エディターでの補完の恩恵を得ることができ、非常に便利です。

Nodeを置き換える

    // ...
    .find(j.CallExpression, {
      callee: {
        object: { name: "foo" },
        property: { name: "bar" },
      },
    })
    .replaceWith((path) => {
      // TODO
    })
    // ....
};

別のAST Nodeへ置き換えるreplaceWith関数に渡ってくるpathオブジェクトの型も推論されます。

新しいNodeの作成

  const j = jscodeshift;
  // ...
    .replaceWith((path) => {
      return j.callExpression(
        j.memberExpression(j.identifier("foo"), j.identifier("baz")),
        path.value.arguments
      );
    })
  // ...
};

export default transform;


置き換えるAST Nodeを作成する際に、利用する便利なBuilderメソッド(e.g. j.memberExpression)へ渡すことが可能な値も検査してくれます。
JavaScriptのときは、このあたりが特に苦労したので、とても助かります。

成果物

最終的には以下のようなコードになりました。

transform.ts

import { Transform, FileInfo, API } from "jscodeshift";

const transform: Transform = ({ source }: FileInfo, { jscodeshift }: API) => {
  const j = jscodeshift;
  return j(source)
    .find(j.CallExpression, {
      callee: {
        object: { name: "foo" },
        property: { name: "bar" },
      },
    })
    .replaceWith((path) => {
      return j.callExpression(
        j.memberExpression(j.identifier("foo"), j.identifier("baz")),
        path.value.arguments
      );
    })
    .toSource();
};

export default transform;

動作確認

jscodeshiftコマンドにTransform Fileと対象ファイルを渡して動作確認しましょう。
-pはprintです。-dはdry runです。
また、対象ファイルとしてTypeScriptファイルを指定する場合は、--parserオプションでtsを指定します。(tsxも指定可能です)

$ npx jscodeshift -t transform.ts index.ts --parser ts -p -d
Processing 1 files...
Spawning 1 workers...
Running in dry mode, no files will be written!
Sending 1 files to free worker...
const foo: any = {};

foo.baz();

All done.
Results:
0 errors
0 unmodified
0 skipped
1 ok
Time elapsed: 0.855seconds

テスト

最後に、テストについても触れておきます。

jscodeshiftのテストについては以下の記事を見ると良いです。

jscodeshiftのテストを書く

今回はjscodeshiftのテストおよびテストのfixture fileにTypeScriptファイルを使う方法を紹介します。

ライブラリのインストール

jestts-jestをインストールします。

$ npm i -D jest ts-jest

jestのセットアップ

jest.config.jspresetts-jestを指定します。

module.exports = {
  preset: "ts-jest",
};

fixture fileとテストファイルの作成

jscodesfhitのドキュメントで指定されているようなディレクトリ構造でfixture fileとテストファイルを作成します。

jscodeshift | Unit Testing

今回は、JavaScriptファイルとTypeScriptファイルのfixture fileを配置します。

/transform.ts
/__tests__/transform.test.js
/__testfixtures__/foobar.input.ts
/__testfixtures__/foobar.output.ts
/__testfixtures__/foobar.input.js
/__testfixtures__/foobar.output.js

__tests__/transform.test.js を以下のように記述しました。
TypeScriptファイルを変換する場合は、parserオプションを指定する必要があるので、defineTest関数で指定しています。

// @ts-ignore
import { defineTest } from "jscodeshift/dist/testUtils";

describe("test with .js file", () => {
  defineTest(__dirname, "transform", null, "foobar");
});

describe("test with .ts file", () => {
  defineTest(__dirname, "transform", null, "foobar", { parser: "ts" });
});

$ npx jest
> jest

 PASS  __tests__/transform.ts
  test with .js file
    transform
      ✓ transforms correctly using "foobar" data (129 ms)
  test with .ts file
    transform
      ✓ transforms correctly using "foobar" data (10 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.14 s

まとめ

jscodeshiftのTransform FileをTypeScriptで書く方法と、TypeScriptファイルでのテストの方法を紹介しました。

ASTベースのプログラミングは、型の恩恵を強く受けることができ、TypeScriptとの相性が非常に良いです。

今後は、jscodeshiftを使う際は、TypeScriptで書くのが良さそうです。

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

AngularとCesium.jsを使って3Dモデルを動かすところまでやってみた

Cesium.jsをAngularアプリケーションの中で使うことになったので、知見を共有しようと思います。
割と基礎的な内容ですが、英語のドキュメントを読むのに辛い思いをしたので備忘録を兼ねて投稿します。願わくば誰かの役に立たんことを……。

Cesium.jsとは?

Cesium.jsの説明についてはこちらの記事で基本的なところは解説しているので、省きます。
https://qiita.com/keijipoon/items/615ebaf7561a27d744f5

準備

まずは新しいAngularのプロジェクトを作ります。
ng new cesium-project

プロジェクトに移動し、ng serveでサーバが起動しているかを確認します。
cd cesium-project
ng serve

確認したらサーバを停止し、AngularCLIからライブラリを追加。ここまでやったら準備は一旦完了です。
ng add angular-cesium

npmコマンドでマニュアルインストール可能だが、設定が面倒くさい。何かの理由でマニュアルインストールする場合は下記参照。
https://docs.angular-cesium.com/getting-started/installation

地形と構造物の表示

さて、まずは地図の表示をしますので新規にコンポーネントを作成しましょう。
下記コマンドでmapコンポーネントを作成します。
cd src/app
ng g c map

map.component.htmlを以下のように書き換えます。

map.component.html
<div id="cesiumContainer"></div>

map.component.tsを以下のように書き換えます。

map.component.ts
import { Component, OnInit } from "@angular/core";

@Component({
  selector: "app-map",
  templateUrl: "./map.component.html",
  styleUrls: ["./map.component.scss"],
})
export class MapComponent implements OnInit {
  constructor() {}

  ngOnInit(): void {
    const viewer = new Cesium.Viewer("cesiumContainer", {
      terrainProvider: Cesium.createWorldTerrain(),//地形情報を表示するオプション
    });
    scene.primitives.add(Cesium.createOsmBuildings());//OpenStreetMapというプロジェクトで作成された簡易なモデルを表示できる
    viewer.camera.flyTo({//目的の座標にカメラを向ける処理
      destination: Cesium.Cartesian3.fromDegrees(139.767125, 35.681236, 1000), //経度,緯度,高さの順に指定
    });
  }
}

ここで、app.component.htmlを以下のように書き換えます。

app.component.html
<router-outlet></router-outlet>

app-routing.module.tsも書き換えましょう。

app-routing.module.ts
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
import { MapComponent } from "./map/map.component";

const routes: Routes = [{ path: "", component: MapComponent }];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

ここまで来たら、再度ng serveでサーバを起動。
すると下記画像のように東京駅周辺の地図情報を3D表示することができました。
キャプチャ2.PNG

軌跡を表示する

map.component.tsのngOnInit()の中に下記の処理を追加します。
もちろんリストの中の値は適当です。

map.component.ts
//軌跡表示
    viewer.entities.add({
      polyline: {
        positions: Cesium.Cartesian3.fromDegreesArrayHeights([
          139.767125,
          35.681236,
          300,
          139.768,
          35.682,
          300,
          139.7685,
          35.681,
          300,
          139.769,
          35.683,
          300,
          139.768,
          35.683,
          300,
        ]),
        width: 5,
        material: Cesium.Color.RED,
      },
    });

保存して画面を確認します。赤いラインが表示されているのがわかります。
キャプチャ3.PNG

軌跡上を動くオブジェクトを表示する

map.component.tsのngOnInit()の中に下記の処理を追加します。
フリーで転がっていたドローンのモデル(.glb形式)をダウンロードし、src/assetsに入れました。

map.component.ts
//アニメーションの処理。
    let czml = [
      {
        //Cesium特有のczmlというJSON形式のデータを作る
        id: "document",
        name: "CZML",
        version: "1.0",
        clock: {
          //アニメーションのループ間隔を指定する
          interval: "2020-09-02T12:00:00Z/2020-09-02T12:00:08Z", //画面下部のタイムバーの始値と終値を定義する。ここにはフライト開始時刻と終了時刻を入れる
          currentTime: "2020-09-02T12:00:00Z", //フライト開始時刻を入れる
          multiplier: 1, //n倍速の指定。固定値にするかは要検討。
          range: "LOOP_STOP"
          step: "SYSTEM_CLOCK_MULTIPLIER",
        },
      },
      {
        id: "drone",
        name: "drone",
        availability: "2020-09-02T12:00:00Z/2020-09-02T12:00:08Z", //ここにフライト開始時刻とフライト終了時刻を指定する
        position: {
          epoch: "2020-09-02T12:00:00Z",
          cartographicDegrees: [
            //秒,経度,緯度,高さの順番
            0, 139.767125, 35.681236, 300,
            2, 139.768, 35.682, 300,
            4, 139.7685, 35.681, 300,
            6, 139.769, 35.683, 300,
            8, 139.768, 35.683, 300,
          ],
        },
        model: {
          gltf: "assets/drone.glb",//3Dモデルの指定
          outlineColor: {
            rgba: [0, 0, 0, 255],
          },
        },
      },
    ];
    viewer.dataSources.add(Cesium.CzmlDataSource.load(czml));

保存して画面を表示し、画面左下の再生ボタンをクリックするとアニメーションが行われるはずです。
image.png

総括

この界隈はあまり日本語のドキュメントがなく、苦労したので何かの役に立てば幸いです。
座標データを取得できるハードウェアなどと連携できれば、いろいろな可能性が見えてきそうです。

参考にしたサイトのURLを張っておきます。
https://sandcastle.cesium.com
https://gis-oer.github.io/gitbook/book/materials/web_gis/Cesium/Cesium.html

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

アイデアの相互評価ができるidea streamerを作りたかったああああ

アイデアの評価むつかしい問題

最近世の中でアート思考だとかデザイン思考だとかがもてはやされるようになった気がします。
でも誰か知り合いが出したアイデアって、忖度なしで評価するの結構難しいと思うんですよね。
例えば上司のアイデアだったら「いいですね!」と言ってしまいそうですし、斬新なアイデアであっても、「こいつ生意気やんけ」といって評価を下げてしまう人もいるかもしれません。

今回は「ひたすらアイデアを投稿しまくって」「それを知らない誰かに評価してもらえるidea streamerというプラットフォームを作ってみました。
途中バグがあったり画面遷移がうまくいかなかったりで、超未完成ですが一旦振り返りの意味で投稿してみたいと思います。
アイデアの相互評価」という意味で面白い仕組みだと思うので、何か刺激になればなと思います。

アプリはこちらから入って、途中で画面遷移が切れるのでアイデア評価からはこちらからお願いします!

動作デモ

アイデアのブレスト

3分間の間にひたすらアイデアを投稿しまくっていきます。
Keywordに自分の好きな単語を入れておくと、「×〇〇」の部分がランダムで変わっていきます(技術不足で途中バグってます)。
普段交じり合わない組み合わせからアイデアを発想して投稿しまくる、という感じですね。

アイデアの評価

アイデア出しが終わったら次は知らない人が投稿したアイデアの評価を行います (画面遷移の実装ができてません)。
DBにあるアイデアを見て面白い・斬新だな、などの観点で0~10の間で評価します。
現状自分のアイデアも見れるようになっていますが、そうすると自己評価になって意味がないので、他の人のアイデアだけ取り出す処理が必要そうですね。

アイデアのスコアの確認

こちらは全く実装できてません。
自分のアイデアがどう評価されたのかを確認できるようにしたいですね。

できたこととできなかったこと

実装できた

・アイデア出しする仕組み
・アイデア評価する画面
・全体の雰囲気

実装できなかった

・アイデア評価の仕組み
・自分のアイデアのスコア確認
・全部0点にしちゃうなど悪ふざけ対策
・画像がデフォルト

悔しい。。。

実装こまごま

yarnでいろいろ

今回は初めてNuxt.jsを使って実装してみました。
結構癖があるけれど使いこなせたら楽しそう。
備忘録的に記録しておきます。

$ yarn create nuxt-app myNuxtTest
Project name: myNuxtTest
Programming language: JavaScript
Package manager: Yarn
UI framework: Bootstrap
Nuxt.js modules: Axios, Progressive Web App (PWA), Content
Linking tools: (Press <space> to select, <a> to toggle all, <i> to invert election)
Testing framework: None
Rendering mode: Universal (SSR / SSG)
Deployment target: Static (Static/JAMStack hosting)
Development tools: None
$ cd myNuxtTest

pagesの中に画面を作ったり、componentsにコンポを作り終わったら、

$ yarn dev

を実行後localhost:3000へアクセスし、画面を確認します。上手く動作していれば、

$ yarn generate

でhtmlを生成し、distフォルダをデプロイ、という流れです。
generateしてフォルダ構造がどう変化するかをちゃんと把握していなかったため、画面遷移がうまくできなかったです。。。悔しい。

Netlifyにデプロイしたのち、freenomで取得したドメインでURLを生成しました。
freenomは今回初めて使いましたがこんなにお手軽にドメインが借りれるのは驚きですね。

タイマーがゼロになったら次へ

カウントダウンタイマーはこちらのサイトを参考にさせていただきました。
カウントがゼロになったら自動で画面遷移するようにしたかったものの、うまく実装ができず。。。
ひとまずはv-showで表示非表示を制御して、ユーザにクリックしてもらう作戦に変更しました。

<template>
<div>
    <div v-show="show">
        <div id="timer">
            <div class="timer">
                <div class="time">
                    {{ formatTime }}
                </div>
            </div>
        </div>

         <h3>
            <input v-model="key" placeholder="KeyWord" class="key"> × {{reactant}} = <input v-model="idea" placeholder="Input Your Idea" class="idea">
         </h3>
         <br>
         <h3>
            <button v-on:click='random' class="button--green">Pass</button>
            <button v-on:click='post' class="button--green">Post</button>
        </h3>
    </div>
    <div v-show="shownext">
        <a
            href="ScoreIdea"
            target="_blank"
            rel="noopener noreferrer"
            class="button--green"
        >
            Next Step
        </a>
    </div>
</div>
</template>

script部分はカウントダウンする部分の処理と、データをDBへPostする処理を実装しました。
vueのライフサイクルをまだ理解しておらず、createdやmountedが使いこなせていないです。。。
いやあこのあたりもちゃんと理解したい。

<script>
import firebase from "firebase/app";
import "firebase/firestore";

export default {
    components: {},

    data(){
        return{
            db: {},
            reactant: 'Click Pass',
            allData: ["文化","コロナ","","飲む","食べ物","おいしい","恋愛","決める","地面"],
            show:true,
            shownext:false,
            min: 2,
            sec: 59,
            timerOn: false,
            timerObj: null,
        }
    },

    methods: {
        //初期化
        init: () => {
            const config = {
                //firebaseの情報
                };

            // Initialize Firebase
            firebase.initializeApp(config);
            },

        //カウントダウン処理
        count: function() {
            if (this.sec <= 0 && this.min >= 1) {
                this.min --;
                this.sec = 59;
            } else if(this.sec <= 0 && this.min <= 0) {
                this.complete();
            } else {
                this.sec --;
            }
        },

        //カウントがゼロになった処理
        complete: function() {
            clearInterval(this.timerObj)
            this.show = false;
            this.shownext = true;
        },

        //ボタンを押したらランダムに表示
        random: function(){
            const rnd = Math.floor(Math.random() * this.allData.length);
            this.reactant = this.allData[rnd]
        },

        //データ追加の処理
        post: function(){
            const testId = firebase.firestore().collection('test').doc().id; //ユニークなIDを生成
            const docRef = firebase.firestore().collection('test').doc(testId);
            const setAda = docRef.set({
                key: this.key,
                reactant:this.reactant,
                idea:this.idea
            });
            this.random();
            this.idea = "";
            //window.location.href = "JoinTable";
        },

        //データ取得の処理
        get: function(){
            //this.allData = [];
            firebase.firestore().collection('test').get().then(snapshot => {
                snapshot.forEach(doc => {
                    console.log(doc);
                    this.allData.push(doc.data());
                })
            });
            this.random();
        }

    },

    //インスタンスが生成したときの処理
    created(){
        let self = this;
        this.timerObj = setInterval(function() {self.count()}, 1000)
        this.timerOn = true; //timerがOFFであることを状態として保持
    },

    //常に動く処理
    computed: {
        formatTime: function() {
        let timeStrings = [
            this.min.toString(),
            this.sec.toString()
        ].map(function(str) {
            if (str.length < 2) {
            return "0" + str
            } else {
            return str
            }
        })
        return timeStrings[0] + ":" + timeStrings[1]
        }
    },

    //マウントされたときの処理
    mounted(){
        this.init();
        this.get();
    },
}
</script> 

あとはデザイン部分ですね。
これはほとんどデフォルトのものを利用しています。

<style>
input{
    text-align: center;
}
input.key{
    width:160px;
    height:50px;
    }

input.idea{
    width:260px;
    height:50px;
    }

#timer {
  display: flex;
  align-items: center;
  justify-content: center;
}
.time {
  font-size: 100px;
}
</style>

さいごに

フレームワークの便利さを感じれるほどまだ慣れておらず、この辺りは経験積んでいくしかないですね。
この辺は精進していかなきゃなあと思います。
アイデア自体は悪くないと思うのに悔しい。。。

最後までご覧になっていただきありがとうございました!
LGTMしていただけると励みになりますので、是非是非よろしくお願いします!

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

JavaScriptを学ぶ:スラスラ読めるJavaScript Chapter1

はじめに

@kuromailserverさんの『会津わろ法則』に参加。
その中で紹介された学習書の中でとりかかりやすく読破できそうな『スラスラ読めるJavaScript』をチョイス。
この投稿は本書を読んで自分なりのノートとしてまとめていく。

注:本書では表なども使って丁寧に解説されているがそれを持ってこれないので他サイトのリンクを載せている。

文字の表示

プログラミングの第一歩、画面に文字を表示。
コンソールへの表示はconsole.logメソッド(メソッド=コンピューターに対する命令文)を使用。
console.log('表示させたい文字');で表示させたい文字がコンソールに表示される。

コンソール
console.log('Hello');

Hello

コンソールにHelloの文字が表示された。

演算子を使って計算

人が四則計算をする場合「+」「-」「×」「÷」の記号を使うが、JavaScriptでする場合は「+」「-」「」「/」の演算子*を使う。

左辺を50、右辺を2として順に計算してみる。

コンソール
console.log(50 + 2);
console.log(50 - 2);
console.log(50 * 2);
console.log(50 / 2);

52

90

100

10

ちゃんと足し算、引き算、掛け算、割り算の計算結果が出た。
この他に「%」(割った余りを表示)「**」(べき乗計算を表示)がある。
上記と同じ値で計算してみる。

コンソール
console.log(50 % 2);
console.log(50 ** 2);

0

2500

参考:式と演算子

演算子には優先順位がある。
参考:演算子の優先順位

表を見ると「+」「-」より「*」「/」「%」のほうが高く、最優先されるのが()を使ったグループ化。
試しにやってみるとこんな感じ。

コンソール
console.log(10 + 4 * 2);
console.log(5 - 8 / 4);
console.log(5 * (4 - 3));

18

3

5

(「式と演算子」や「演算子の優先順位」みたいなのはPDF化してすぐに参照できるようにしておくのが良さげ。)
※負の数をあらわすとき
「-」の演算子は左側が数値以外だと「負の値」となる。
※整数と実数
JavaScriptでは整数と実数は同じNumber型のデータ。
整数と実数同士で計算もできるが、整数同士の計算のほうが圧倒的に早いとのこと。
先に進んだ時に実際に動作させて確認したい。

変数を使って計算

値や文字列などをいれておける箱みたいなもの=「変数」。
let 変数名 = 値や文字列;で新しく変数が作れる。
さっそくやってみましょう。

コンソール
let num = 777;
let word = 'リンゴ'

これだけだとキチンとできたかどうかわからないので、console.log()で表示してみましょう。

コンソール
let num = 777;
let word = 'リンゴ'
console.log(num);
console.log(word);

777

リンゴ

キチンとできていた。
letは新しく作る際には必要だが、1度作った変数の中身を変えるときはいらない。

変数の命名にはルールがある。
1:半角のアルファベット、アンダースコア、数字を組み合わせて作る
2:数字のみ、先頭が数字の命名はNG
3:予約語と同じ名前はNG(予約語と予約語、予約語と他の文字を合わせた場合はOK)
参考:予約語

また、JavaScriptは変数、演算子、メソッドなどを識別する。
なので、変数名に演算子が入っていると変数として機能しない。

promptメソッドを使うとユーザーから入力したものを扱える。
変数 = prompt('ダイアログボックスに表示したい文字列');
実際にやってみると

コンソール
let answer = prompt('文字を入力');
console.log(answer);

ダイアログボックスが表示されるので「カエル」と入力。

prompt.jpg

すると

カエル

と入力したものが表示された。

次は2つの数値を入力してその合計が表示されるプログラムを実行する。
1つ目を「5」、2つ目を「3」と入力。
結果は「8」が表示されるはずだが……

コンソール
let num = prompt('1つ目の数値');
let num2 = prompt('2つ目の数値');
console.log(num + num2);

53

表示されたのは「53」。
「+」の演算子は左右どちらかが文字列だと「連結」という命令に変化。
promptメソッドで入力すると入力したものはすべて文字列として扱われる。
なので「53」という結果が表示された。

「連結」ではなく「足し算」として実行するには文字列を数値に変換する処理が必要。
それにはparseIntという関数がある(関数については後のChapterで説明がある)。
parseIntは指定したものを「整数」として返す。
先ほど入力したプログラムにparseIntをいれれて、1つ目を「5」、2つ目を「3」と入力すると……

コンソール
let num = prompt('1つ目の数値');
let num2 = prompt('2つ目の数値');
console.log(parseInt(num) + parseInt(num2));

8

「5」と「3」を合計した「8」が表示された。

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

@babel/parser を console.log デバッグする

気軽に console.log デバッグがしたかったときのメモ。

Prettier と違って Flow で書かれてるのでそのまま Node.js では動かないのでビルドする必要がある。

$ make watch

で変更を監視してビルドして、/packages/babel-parser/lib/index.js を見れば良い。

なんか適当なファイルを作って

// tmp.js
const { parse } = require("./lib/index.js");

const code = `const foo = "foo";`;

const result = parse(code, { plugins: ["typescript"], errorRecovery: false });

console.log(JSON.stringify(result));

とか好きな内容にして

$ node ./tmp.js

すればうごく。最初はビルドするのがめんどくさいから flow-node で実行しようと思ったけど、flow-node は ESM 対応がされていなくて @babel/parser のソースをそのまま実行できなかったので諦めてビルドすることにした。(ちなみに一瞬 flow-node の ESM 対応をしようとしたけど手元でいじって動かすには Flow をソースからビルドする必要があるっぽく(?)、OCaml 環境のセッティングが面倒くさかったのでやめてしまった。)

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

javascriptで関数をwrapする

javascriptで関数をwrapしたいときー
例えば、非同期処理の完了を待つまでなんかしたいときとか

const wrapper = function(func) {
    /*** なんか処理 ***/
    let result = func();
    /*** なんか処理 ***/
};

async/awaitバージョン

const wrapper = async function(func) {
    /*** なんか処理 ***/
    let result = await func();
    /*** なんか処理 ***/
};

普通ですね

つかうときー

// 引数を取らない関数の場合
wrapper(funcA)

// 引数を取る関数の場合 ここが特殊!
wrapper(funcB.bind(null, "unko"))
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[JavaScript] 文字列のアルファベット大小文字考慮した辞書順ソート

アルファベット順、大文字小文字無視したソート

次のようなコードがよく紹介されていました。

var source = ['a', 'B', 'A', 'b', 'aa', 'Aa', 'AA', 'aA', 'aB', 'ab', 'Ab', 'AB']
source.sort((a, b) => {
  a = a.toLowerCase();
  b = b.toLowerCase();
  if (a < b) {
    return -1;
  } else if (a > b) {
    return 1;
  }
  return 0;
})
console.log(source);
// [ 'a', 'A', 'AA', 'aA', 'aa', 'Aa', 'aB', 'ab', 'Ab', 'AB', 'B', 'b' ]

このコードだとある程度は文字列を並び替えられるのですが、大文字と小文字は同一視されているので、どちらかが優先されることがないです。

いわゆる辞書順を考えると、大文字を優先するか、小文字を優先するか、を判断したいところ。

アルファベット順、大文字、小文字、優先順位をつける。

次のようにします。

var source = ['a', 'B', 'A', 'b', 'aa', 'Aa', 'AA', 'aA', 'aB', 'ab', 'Ab', 'AB']
source.sort((a, b) => {
  const la = a.toLowerCase();
  const lb = b.toLowerCase();
  if (la < lb) {
    return -1;
  } else if (la > lb) {
    return 1;
  } else {
    for(let i = 0, l = a.length; i < l; i += 1) {
      if (a[i] < b[i]) {
        return -1;
      }
      if (b[i] < a[i]) {
        return 1
      }
    }
  }
})
console.log(source);
// [ 'A', 'a', 'AA', 'Aa', 'aA', 'aa', 'AB', 'Ab', 'aB', 'ab', 'B', 'b' ]

アルファベット順で、大小文字区別なしで同一文字の場合には、1文字ごとに並び順を判定するとよいです。

小文字優先にしたい場合は、for の内部の、-1 と 1 を入れ替えるとよいです。

// [ 'a', 'A', 'aa', 'aA', 'Aa', 'AA', 'ab', 'aB', 'Ab', 'AB', 'b', 'B' ]

追記:アルファベット順、大文字、小文字、優先順位をつける。

より効率よいコードを、コメント欄で @sugoroku_y さんに教えていただきました。ありがとうございます!

このように書いても同じ結果が出力されます。

var source = ['a', 'B', 'A', 'b', 'aa', 'Aa', 'AA', 'aA', 'aB', 'ab', 'Ab', 'AB']
source.sort((a, b) => {
  const la = a.toLowerCase();
  const lb = b.toLowerCase();
  if (la < lb) {
    return -1;
  } 
  if (la > lb) {
    return 1;
  } 
  if (a < b) {
    return -1;
  }
  if (b < a) {
    return 1
  }
})
console.log(source);
// ["A", "a", "AA", "Aa", "aA", "aa", "AB", "Ab", "aB", "ab", "B", "b"]

var source = ['a', 'B', 'A', 'b', 'aa', 'Aa', 'AA', 'aA', 'aB', 'ab', 'Ab', 'AB']
const sorted = source.map(
  s => ({o: s, l: s.toLowerCase()})
).sort((a, b) => {
  if (a.l < b.l) return -1;
  if (a.l > b.l) return 1;
  if (a.o < b.o) return -1;
  if (a.o > b.o) return 1;
  return 0;
}).map(e => e.o)
console.log(sorted)
// ["A", "a", "AA", "Aa", "aA", "aa", "AB", "Ab", "aB", "ab", "B", "b"]

JS Bin で動かせるコードを載せました。
https://jsbin.com/busucecotu/2/edit?html,js,console

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

javascriptのfor文について

javascriptを触ってて
for文周りって色々あるけどfor以外使ったことなかったので調べてみました。

for

まずはスタンダード?なfor文から。

const arr = ["banana", "apple", "orange"];
for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]);
}

出力結果としては

banana
apple
orange

となります。

for(初期値; 条件式; 増減値) {}
のように3つの引数を入れてあげれば出力できました。

配列などでよく使用されるイメージです。

for-in

次はfor-inで、これはあまり使ったことがないのですがオブジェクトの中身などを取り出したいときに使用されるみたいです。

const obj = {
  name: "太郎",
  age: 15
}

for(let key in obj) {
  console.log(key + ":" + obj[key]);
}

出力結果としては

name: 太郎
age: 15

となります。

for (変数 in オブジェクト)とすれば
オブジェクトのプロパティの数だけ繰り返してくれます。

for-of

for-ofは配列に対して使用できます。

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

for (let item of items) {
  console.log(item);
}

出力結果としては

1
2
3
4
5
6

となります。

for (変数 of 配列)とすれば配列の数だけ繰り返します。
for文のように条件式を入れないのでこっちの方がわかりやすい印象です。

foreach

最後はforeachですがこれは配列に対してつかえます。

var items = [
  {
    id: 1,
    name: "太郎"
  },
  {
    id: 2,
    name: ""
  }
];

items.forEach( function( value ) {
    console.log( value.name );
});

配列の要素一つ一つに対して処理を実行できるので使いどころが多そうな印象です。

まとめ

今回はjavascriptのfor系を調べてみました。
同じようなものでも違いがあるので色々調べてみて使ってみたいと思います。

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

(JS)絶対にDeep Mergeを自力で実装しないでください

tl;dr

  • JavaScriptでオブジェクトのdeep mergeをしたいならこの記事を読むか、あるいは最新版のlodashを使ってください
  • 自力で実装したdeep mergeを使う場合は、引数にユーザーの入力(JSONなど)を使わないでください

deep merge? なにそれ

オブジェクトの引数を再帰的に設定する関数で、lodashでいえばmerge関数とかに相当。

プロトタイプ汚染の闇

JavaScriptのオブジェクトには99%プロトタイプが存在します。偉い人はオブジェクトをObject.create(null)で作ったりしますが、そういう人は稀です。

プロトタイプの属性は、同名の属性が元のオブジェクトに存在しない場合のフォールバックとして使われます。たとえば、いつも使うhasOwnPropertyは、実はObjectのプロトタイプ内に存在するのです。

では、そのプロトタイプを書き換えられたらどうでしょうか。たとえば、hasOwnProperty1になったら、({}).hasOwnProperty("foo")といったコードがエラーを出します。もし認証系のコードで「isAdmin属性が存在すれば管理者だ」をconst isAdmin = ({}).isAdmin;のように実装していれば、立派な権限昇格です。これは「プロトタイプ汚染」(Prototype Pollution)と呼ばれます。

そして、プロトタイプ汚染が怖いのは、deep mergeという簡単な操作で、引き起こせるからです。

Deep Merge クイックテスト

もし下のオブジェクトを空のオブジェクトにdeep mergeして、Object.prototype.isAdminがセットされたら、プロトタイプ汚染成功です。(自分のコードでのみ試してください... 本番環境のAPIに投げるのはお勧めしません。)

{
  "__proto__": {"isAdmin": 1},
  "constructor": {"prototype": {"isAdmin": 1}}
}

安全なdeep merge

一番安全なのは、lodashのようなライブラリーを使うことです。なお、最新版を使うことをお勧めします。

それでも自力で実装したい場合は

  • __proto__constructorprototypeのキーが出てきたら無視する。
  • merge先のhasOwnPropertyの結果がfalseの場合は、空のオブジェクトを作成する。(typeof merge先 === "object"のみだと不十分。)

のような安全策を取らないといけません。荒技としてはObject.freeze(Object.prototype)で汚染をできないようにすることもできますが、あくまで荒技です。

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

[ServiceNow] サービスカタログに引数を渡したい

きっかけ

とあるサービスカタログに前のページで表示している値を引き継いで遷移したいという要望があり、URLから取得する方法を実装しました。

参考にした記事

https://jace.pro/post/2018-07-15-sp-set-variables-via-url/

プラットフォーム

NewYork

サンプル

以下のカタログ(レコードプロデューサー)のAsset No.フィールドに値を渡したい。
スクリーンショット 2020-09-08 19.31.06.png

方法1 変数のDefault Valueを使用する

スクリーンショット 2020-09-08 20.57.26.png

変数のDefault Valueに以下のスクリプトを書きます。

hoge.js
javascript: (function(){
  try{
    return $sp.getParameter('var_asset_no') || '';
  } catch(e){
    return RP.getParameterValue('var_asset_no');
  }
})()

以下の様にURLパラメータを渡してカタログを開きます。

https://(インスタンス名).service-now.com/(カタログページ)&var_asset_no=P1000023

結果

スクリーンショット 2020-09-08 20.54.07.png

方法2 マクロ型の変数を使用する

マクロ型の変数を設定します。
スクリーンショット 2020-09-08 21.10.16.png

ウィジェットのクライアントスクリプトに以下のコードを書きます。

client_script.js
function($scope, $window) {
    var params = $window.location.href.split('?')[1];
    var paramArr = params.toString().split('&');
    paramArr.map(function(keyval) {
        var key = keyval.split('=')[0];
        var value = keyval.split(key + '=').join('');
        value = decodeURIComponent(value);
        try {
            $scope.page.g_form.setValue(key, value);
        } catch (e) {
            console.log('Error set params', e);
        }
    });
}

以下の様にURLパラメータを渡してカタログを開く

https://(インスタンス名).service-now.com/(カタログページ)&category=hardware&cmdb_ci=P1000023

結果

CategoryとAsset No.に値を設定できました。
スクリーンショット 2020-09-08 21.13.31.png

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

【JavaScript】配列の中にオブジェクトを追加する。

JSの初歩的な部分で躓いたので、備忘として記録する。

下記のとおり、科目(キー名subject)と点数(キー名points)を代入した変数testがあるとする。
let test = [{subject: 'sociology', points: 75}, {subject: 'english', points: 50}, {subject: 'biology', points: 85}];
この中に、点数95点をとった科目scienceを入れたい場合、

ハッシュ名[キー] = 追加したい値;

では入れられない。

あくまでも、配列[]に追加したいわけなので、pushを使う。

test.push = ({subject: 'science', points: 95});

これで追加完了。ここから、points'95'だけを抽出したい場合は、

console.log(test[3]["points"]); ・・・配列testのインデックス番号[3]の['points']プロパティを出力、という意味。

まとめると下記のとおり。

2020-09-09.png

※追記
 下記の書き方も可能。

2020-09-09 (1).png

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

無料でSSR・ホスティング・API鯖を立てれるVercel。無料でホスティングするサンプル。Parcel・ReactでSPA。

Vercel
https://vercel.com

バックナンバー
無料でSSR・ホスティング・API鯖を立てれるVercel。TypeScript・ExpressでAPI鯖を立てる。

ソースコード

package.json
{
  "scripts": {
    "start": "rimraf local && parcel src/index.html -d local",
    "build": "rimraf public && parcel build src/index.html -d public --no-source-maps"
  },
  "dependencies": {
    "connected-react-router": "^6.8.0",
    "history": "^5.0.0",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "react-redux": "^7.2.1",
    "react-router": "^5.2.0",
    "redux": "^4.0.5",
    "vercel": "^20.1.0"
  },
  "devDependencies": {
    "parcel-bundler": "^1.12.4",
    "rimraf": "^3.0.2"
  }
}
src/index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vercelホスティング</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="/index.js"></script>
  </body>
</html>
src/index.js
import * as React from 'react';
import { render } from 'react-dom';
import { combineReducers } from 'redux';
import { Provider } from 'react-redux';
import { Route, Switch } from 'react-router';
import {
  ConnectedRouter,
  routerMiddleware,
  connectRouter,
} from 'connected-react-router';
import { applyMiddleware, compose, createStore } from 'redux';
import { createBrowserHistory } from 'history';

const App = () => {
  return (
    <>
      <h1>Hello world!</h1>
    </>
  );
};

const history = createBrowserHistory();

const store = createStore(
  combineReducers({
    router: connectRouter(history),
  }),
  {},
  compose(applyMiddleware(routerMiddleware(history)))
);

render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <Switch>
        <Route exact path={'/'} component={App} key="home" />
        <Route exact path={'/sub'} component={App} key="sub" />
      </Switch>
    </ConnectedRouter>
  </Provider>,
  document.getElementById('app')
);
vercel.json
{
  "routes": [
    { "handle": "filesystem" },
    {
      "src": "/.*",
      "dest": "/index.html"
    }
  ]
}

コマンド

ローカルで実行 ポート番号3000で鯖が立ち上がる

$ npx vercel dev

デプロイ

$ npx vercel --prod

ワイの成果物

https://qiita.com/yuzuru2/items/b5a34ad07d38ab1e7378

①コード共有サイト(SPA) React
https://code.itsumen.com

②画像共有サイト(SPA) React
https://gazou.itsumen.com

③チャット(SSR) Nuxt.js
https://nuxtchat.itsumen.com

④チャット(SPA) React
https://chat4.itsumen.com

⑤掲示板(SSR) Next.js
https://board.itsumen.com

⑥掲示板(SPA) Vue
https://board.itsumen.com

⑦レジの店員を呼ぶスマホアプリ(Android)
https://play.google.com/store/apps/details?id=com.itsumen.regi&hl=ja

⑧ブログ(静的サイトジェネレータ) Hugo
https://yuzuru.itsumen.com

ワイのLINE: https://line.me/ti/p/-GXpQkyXAm

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

vue.js コンポーネントの書き方

Vue cliを使わないcdn版でのコンポーネントの書き方です。
いくつか書き方があり迷うので自分メモ的に投稿致します。
大きくわけてグローバル登録とローカル登録があります。

コンポーネントの書き方(グローバル登録)

コンポーネント名はHTMLタグになるもの
テンプレート名は直接HTMLタグを書くか、HTML内に記述したx-templateの対応するidを設定

//コンポーネント1
Vue.component('コンポーネント名A', {
    template: 'HTMLタグを記述、もしくはテンプレートのid',
    data: function() {
        return {
            number: 12
        }
    }
})

//コンポーネント2
Vue.component('コンポーネント名B', {
    template: 'HTMLタグを記述、もしくはテンプレートのid',
    data: function() {
        return {
            msg: 'hello!'
        }
    }
})

//HTML内で表示したいブロック
new Vue({ el: '#app' })

new Vue({ el: '#app2' })

グローバルなので#app、#app2どちらのブロックでもすべてのコンポーネント名がHTMLタグとして使用できます。

  <div id="app">
    <コンポーネント名A></コンポーネント名A>
  </div>
  <div id="app2">
    <コンポーネント名A></コンポーネント名A>
    <コンポーネント名B></コンポーネント名B>
  </div>

コンポーネントの書き方(ローカル登録) 推奨

定義したコンポーネントの定数名を、new Vue内で使いたいel:に使いたいタグ名で登録する

//コンポーネントを定義
const 定数名 = {
    template: 'HTMLタグを記述、もしくはテンプレートのid',
    data: function() {
      return {
        msg: 'キーワードを入力してください',
      }
    }
  }

new Vue({
  el: 'HTML内の使いたいブロックをCSSセレクタで記述',
  components: {
//上のelでマウントしたHTML内のブロックに
//下の 'HTMLタグ名':定数名 を使います宣言
    'search-component': Search
  }
})

[Vue warn]: Unknown custom element: - did you register the component correctly? For recursive components, make sure to provide the "name" option. - did you register the component correctly? For recursive components, make sure to provide the "name" option.

というエラーを吐かれたときはコンポート名が間違っていないか確認してください。

まとめ

基本的にローカル登録を推奨

多くの場合、グローバル登録は理想的ではありません。例えば Webpack のようなビルドシステムを利用しているときに、グローバルに登録した全てのコンポーネントは、たとえ使用しなくなっても、依然として最終ビルドに含まれてしまうことでしょう。これは、ユーザがダウンロードしなくてはならない JavaScript のファイルサイズを不要に増加させてしまいます。
このような場合に、コンポーネントを素の JavaScript オブジェクトとして定義することができます。

ローカル登録手順まとめ

  • constでコンポーネントを定義
  • templateを作成(HTML内にx-templateかコンポーネントのtemplate:に直接記述)
    ※vue cli で使える.vueファイルだとファイル毎にコンポーネントをまとめて書くやり方が使える
  • template:でテンプレートを定義(直接記述でない場合x-templateのidを指定する)
  • data、methodsなどオプションを定義
    ※dataはコンポーネント側に記述する(new Vueの方にdataを記述した場合反映されない感じ?)
  • new Vue で使いたいブロックをel:に、使いたいコンポーネントをcomponents:に
  • el:複数登録不可、components:は複数登録可能
  • components:に登録したコンポーネントだけがそのel:内で使える
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gatsby入門 チュートリアルをこなす ソースプラグインの作成(1)

チュートリアルをこなす!

以前にgatsbyの基本のチュートリアルをこなしたのですが、まだチュートリアルが残っているので最後までやっていこうと思いました。
今回実施するgatsbyのチュートリアルはこちら
https://www.gatsbyjs.com/tutorial/plugin-and-theme-tutorials/
https://www.gatsbyjs.com/tutorial/source-plugin-tutorial/
早速やっていきましょう。

Plugin & Theme Tutorials

https://www.gatsbyjs.com/tutorial/plugin-and-theme-tutorials/
ここではプラグインとテーマのチュートリアルの概略が記載されていました。
ざっくり言うと
プラグインは、Gatsby APIを実装するNode.jsパッケージ。
テーマは、事前設定された機能やデータソーシング、UIコードをギャツビーサイトに追加するプラグインの一種。
要はサイト構築に便利な物が作れまっせ(しかも共有出来まっせ)ってことで理解しました。
次行きましょう。

Creating a Source Plugin

https://www.gatsbyjs.com/tutorial/source-plugin-tutorial/
ここでは独自のソースプラグインを作成するようです。
ソースプラグインについてはチュートリアルにこう書かれています。

ソースプラグインは、任意のソースからのデータをGatsbyが処理できる形式に変換します。 Gatsbyサイトでは、いくつかのソースプラグインを使用して、興味深い方法でデータを組み合わせることができます。

つまり構築サイト内のソースからデータを抜き出して、良い感じのデータに変換できるってことかな?
とにかく次次!

How to create a source plugin

なんだか色々な事を書いてあるけど英語よくわかんねぇから、とりあえず実技に進もう。

Set up an example site

以下コマンドでサンプルサイトを作成
gatsby new example-site https://github.com/gatsbyjs/gatsby-starter-hello-world
2020-09-09_01h30_27.jpg

Set up a source plugin

以下コマンドでソースプラグインを作成
gatsby new source-plugin https://github.com/gatsbyjs/gatsby-starter-plugin
2020-09-09_01h54_05.jpg

Install your plugin in the example site

サンプルサイトにソースプラグインをインストールします。
example-site/gatsby-config.jsを以下のように修正します。

example-site/gatsby-config.js
module.exports = {
  /* Your site config here */
  plugins: [require.resolve(`../source-plugin`)],←ここ修正
}

example-siteを起動します。example-siteディレクトリに移動して以下を実行
gatsby develop
2020-09-09_02h03_06.jpg
ロードされてる!
このログはsource-plugin/gatsby-node.jsに出力コマンドがあります。

source-plugin/gatsby-node.js
exports.onPreInit = () => console.log("Loaded gatsby-starter-plugin")

Source data and create nodes

source-plugin/gatsby-node.jsを以下のように書き換え

source-plugin/gatsby-node.js
// constants for your GraphQL Post and Author types
const POST_NODE_TYPE = `Post`

exports.sourceNodes = async ({
  actions,
  createContentDigest,
  createNodeId,
  getNodesByType,
}) => {
  const { createNode } = actions

  const data = {
    posts: [
      { id: 1, description: `Hello world!` },
      { id: 2, description: `Second post!` },
    ],
  }

  // loop through data and create Gatsby nodes
  data.posts.forEach(post =>
    createNode({
      ...post,
      id: createNodeId(`${POST_NODE_TYPE}-${post.id}`),
      parent: null,
      children: [],
      internal: {
        type: POST_NODE_TYPE,
        content: JSON.stringify(post),
        contentDigest: createContentDigest(post),
      },
    })
  )

  return
}

再起動
graphqlを見ると
2020-09-09_02h17_38.jpg
allPostという項目が増えています。
チュートリアル通りのクエリを実行するとこんな感じ。
うん。source-plugin/gatsby-node.jsに書かれたPost情報が記載されてる。
postもあるね。
なるほどね。サイトのディレクトリとファイルの内容だけでSQLみたいなクエリ作って取得するようにイメージしておこう。

だめだ超眠い。

今回はここまで。

ありがとうございました。

gatsby 過去の作業履歴

gatsby入門 チュートリアルをこなす 0.開発環境をセットアップする
gatsby入門 チュートリアルをこなす 1. ギャツビービルディングブロックについて知る(1)
gatsby入門 チュートリアルをこなす 1. ギャツビービルディングブロックについて知る(2)
gatsby入門 チュートリアルをこなす 2. ギャツビーのスタイリングの概要
gatsby入門 チュートリアルをこなす 3. ネストされたレイアウトコンポーネントの作成
gatsby入門 チュートリアルをこなす 4. ギャツビーのデータ
gatsby入門 チュートリアルをこなす 5. ソースプラグインとクエリされたデータのレンダリング
gatsby入門 チュートリアルをこなす 6. 変圧器プラグイン※Transformer pluginsのgoogle翻訳
gatsby入門 チュートリアルをこなす 7. プログラムでデータからページを作成する
gatsby入門 チュートリアルをこなす 8. 公開するサイトの準備
gatsby入門 ブログ作ってサーバーにアップしてみる

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

Mac ショートカットコマンド

今回は超初心者が、jQuryのmapメソッドを学んだので、
処理内容を書いて行きます。

mapメソッドの処理内容

画像とコード元に解説します。
画像
スクリーンショット 2020-09-09 1.42.35.png
コード

sample.js
//配列オブジェクト
let names = [aaa,bbb,ccc];

//関数(コールバック関数)
function disp(text) {
    console.log(text);
}

//mapメソッドで配列を呼び出し。
names.map(disp);

処理内容
今回は、コンソール上にaaa,bbbと表示させるコードを書いました。

まず、配列オブジェクト「names」を、mapメソッドを使用して呼び出します。

マップメソッドの引数には、コールバック関数である関数「disp」を引数として
設定。

関数「disp」の仮変数「text」に呼び出し元である配列「names」
各要素が入る。
イメージとしては、こんな感じになるのかな?
text = "aaa"
text = "bbb"
処理の回数は、変数の要素数分だけ行われる。
今回は、各要素を要素数分、コンソールに表示させて処理が終わる。

まとめ

mapメソッドを使用すると、eachと違い配列の中身である要素だけを、
呼び出して処理ができる。
ざっくりとした処理イメージだと、
names(配列)
⬇︎
mapの引数に設定している関数を呼び出し
( names.map(disp)dispが呼び出し関数 )
⬇︎
呼び出されて関数の仮変数(仮引数)に、配列の中身(要素)が代入される
( 呼び出し元の配列オブジェクトnamesの中身が仮変数「text」に代入される )
⬇︎
配列の要素数だけ関数内の処理が行われる。
色々と、呼び方が間違っていたらすいませんm(_ _)m
スクリーンショット 2020-09-09 1.42.35.png

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

jQuery mapメソッドについて

今回は超初心者が、jQuryのmapメソッドを学んだので、
処理内容を書いて行きます。

mapメソッドの処理内容

画像とコード元に解説します。
画像
スクリーンショット 2020-09-09 1.42.35.png
コード

sample.js
//配列オブジェクト
let names = [aaa,bbb,ccc];

//関数(コールバック関数)
function disp(text) {
    console.log(text);
}

//mapメソッドで配列を呼び出し。
names.map(disp);

処理内容
今回は、コンソール上にaaa,bbbと表示させるコードを書いました。

まず、配列オブジェクト「names」を、mapメソッドを使用して呼び出します。

マップメソッドの引数には、コールバック関数である関数「disp」を引数として
設定。

関数「disp」の仮変数「text」に呼び出し元である配列「names」
各要素が入る。
イメージとしては、こんな感じになるのかな?
text = "aaa"
text = "bbb"
処理の回数は、変数の要素数分だけ行われる。
今回は、各要素を要素数分、コンソールに表示させて処理が終わる。

まとめ

mapメソッドを使用すると、eachと違い配列の中身である要素だけを、
呼び出して処理ができる。
ざっくりとした処理イメージだと、
names(配列)
⬇︎
mapの引数に設定している関数を呼び出し
( names.map(disp)dispが呼び出し関数 )
⬇︎
呼び出されて関数の仮変数(仮引数)に、配列の中身(要素)が代入される
( 呼び出し元の配列オブジェクトnamesの中身が仮変数「text」に代入される )
⬇︎
配列の要素数だけ関数内の処理が行われる。
色々と、呼び方が間違っていたらすいませんm(_ _)m
スクリーンショット 2020-09-09 1.42.35.png

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

JSエコシステムぶらり探訪(2): Node.jsとCommonJS modules

JSエコシステムの進化を語るにはNode.jsを避けて通ることはできません。Node.jsと、それ自身の持つモジュール機能について歴史的な背景を踏まえつつ説明します。

←前 目次

Node.js

Node.jsは非同期I/Oを備えたサーバーサイドJavaScriptのための実行環境として2009年に登場しました。1 現在はサーバーサイドJavaScriptだけではなく、JavaScriptのビルド環境として無くてはならないものになっています。

要するにNode.jsは、PerlスクリプトやRubyスクリプトと同じようにJavaScriptのコードを実行するための環境です。

main.js
console.log("Hello, world!");
$ node main.js
Hello, world!

CommonJS modules (CJS)

CommonJS modulesはNode.jsで主に使われているモジュール形式です。CommonJSという規格群の一部として2009年に登場し、生まれて間もないNode.jsのモジュールシステムもすぐにCommonJSに適合しました。 2

後述するESM (ES Modules) で置き換えられつつあり、CJSのほうが伝統的な形式といえます。

util.js
var private_value = 42;
// exports.square = ... でもよい
module.exports.square = function(x) {
  return x * x;
};
main.js
var util = require('./util');
console.log(util.square(2)); // => 4
console.log(util.private_value); // => undefined

また、 module.exports に別の値を代入することもできます。この場合は exports にだけ代入しても有効ではないので注意する必要があります。

square.js
// module.exportsへの代入は必須。exportsにも代入しておくと後々便利
module.exports = exports = function(x) {
  return x * x;
};
// JavaScriptの関数はそれ自体オブジェクトなので、プロパティーに代入できる
exports.version = "0.1.0";

上の例では、 require は関数を返します。

main.js
var square = require('./square');
console.log(square(2)); // => 4
console.log(square.version); // => "0.1.0"

Node.jsにおいては最初に呼ばれるJavaScriptファイルもモジュールです。

main.js
var x = 42;
// global is Node.js equivalent of windows
// (There's also globalThis usable in both environments)
console.log(global.x); // => undefined

Node.jsのCJSの仕組み

CJSのrequireはNode.js (など、それぞれの処理系) が提供するプリミティブ関数ですが、その動作は比較的シンプルに理解できます。つまり、ファイルを発見して読み取り、関数に包んで eval する処理と考えることができます。3

実際にevalされるときは以下のような関数の本体として扱われます。(つまり、 exports, require, module, __filename, __dirname という5つのローカル変数があらかじめ存在するものとして扱われます。)

(function(exports, require, module, __filename, __dirname) {
  // ファイルの中身
});

moduleexports はあらかじめ以下のように初期化されたものと考えることができます。

var module = {}, exports = {};
module.exports = exports;
// その他、moduleの様々なプロパティーを設定

requiremodule オブジェクトをキャッシュしておき、eval終了後に module.exports の値を戻り値として返すと考えることができます。これにより module.exportsexports の関係についても説明がつきます。

モジュールの副作用とオブジェクトの同一性

以下のようなモジュールを考えます。

module1.js
console.log("Hello, world!");

var counter = 1;
exports.fresh = function() {
  return counter++;
};

このモジュールには以下の特徴があります。

  • モジュールのトップレベル処理に副作用がある。
  • モジュールが状態を持っている。

このような場合、モジュールの同一性を気にする必要が出てきます。全く同じ内容の module2.js をコピーとして作成し、以下のように main.js から呼び出してみます。

main.js
var module1 = require('./module1'); // => Hello, world!
var module2 = require('./module2'); // => Hello, world!

console.log(module1.fresh()); // => 1
console.log(module1.fresh()); // => 2
console.log(module2.fresh()); // => 1
console.log(module2.fresh()); // => 2
console.log(module1.fresh === module2.fresh); // => false

両方の console.log が実行され、 fresh は別々にカウントされ、 fresh のオブジェクトとしての同一性も false になりました。

Node.jsでは、パスが同じものは同一モジュールになります4。先ほどの main.js を書き換えて ./module1 を2回インポートするようにしてみます。

main.js
var module1a = require('./module1'); // => Hello, world!
var module1b = require('./module1'); // (no output)

console.log(module1a.fresh()); // => 1
console.log(module1a.fresh()); // => 2
console.log(module1b.fresh()); // => 3
console.log(module1b.fresh()); // => 4
console.log(module1a.fresh === module1b.fresh); // => true

console.log は1回しか実行されず、 fresh は同じカウンタを使うようになり、2つの fresh 関数はオブジェクトとしても同一になりました。

冠頭形モジュール

CJSのインポートは単なる require という関数であり、どこでも呼び出すことができます。これは一見すると便利で妥当な設計に見えますが、実際はNode.js以外の環境にモジュールシステムを移植するにあたってこの「どこでも呼び出せる」という性質が邪魔になってきます。

そこで、以降で解説するモジュールシステムの理解を助けるために、本稿独自の用語として「(CJSの)冠頭形モジュール」という概念を導入します5

定義. あるCommonJSモジュールが冠頭形である (is a prenex-form module) とは、以下を満たすことである。

  • そのモジュールファイルはヘッダ部と本体に分けられる。 (ヘッダ部に続いて本体が来るものとする)
  • ヘッダ部の各文は以下のいずれかの形式である。
    • var <変数名> = require(<文字列リテラル>);
    • let <変数名> = require(<文字列リテラル>);
    • const <変数名> = require(<文字列リテラル>);
    • require(<文字列リテラル>);
  • 本体では require 関数は使われていない。

冠頭形であれば、モジュールファイルの中身を実際にevalしなくても、あらかじめ依存先モジュールを決定することができます。

冠頭形ではないものの例としては以下のようなものがあります6

  • 条件つきインポート
  • require の引数が動的に決まるようなインポート
  • 当該モジュール読み込み時ではなく、あとで必要になってから行うインポート

まとめ

特に重要なのが以下の点です。

  • Node.jsによって「ブラウザー以外のJavaScript実行環境」が大きな地位を獲得した。
  • Node.jsによって、JavaScriptに優れたモジュールシステムがもたらされた。

このことがJavaScriptに2つの大きな課題をもたらしました:

  • Webブラウザーもモジュールシステムの恩恵を受けられるようにすること。
  • Node.jsとWebブラウザーの間のコードの相互運用性を高めること。

これらの課題がJavaScriptバンドラーの誕生、そして各種の新しいモジュールシステムの提案へとつながっていくと考えられます。が、次回はその前に、Node.jsのパッケージシステムについて扱います。

←前 目次


  1. Wikipediaの記述によると、それ以前にもサーバーサイドJavaScriptの技術自体は存在していたようです。 

  2. 根拠を探す余裕がなかったのでこのように書きましたが、実際のところNode.jsの初期のモジュールシステムをベースにしてCommonJSが生まれた可能性が高いと思います。 

  3. 他に、ファイルの読み取りが同期的に行われる点、複数回requireしたときにキャッシュする仕組み、巡回参照の処理などを考える必要がある 

  4. シンボリックリンクについては次回言及予定 

  5. これについて、より広く使われている名称があれば教えていただけるとありがたいです。 

  6. バンドラーによっては、ここに挙げたような例をうまく処理できてしまう場合もありますが、それでも一般的な場合を全てカバーするのは困難です。 

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