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

HTML5のdraggable属性で自由にドラッグ&ドロップ

HTML5のdraggable属性とjavascriptを使用して自由にドラッグドロップできるようにしました。

下のコードを完コピしてもらえれば確認していただけると思いますが、

左側にドラッグ対象の要素(numberBox)が縦に20個並んでいるボックス(drop_back_box)があり、

右側には横長のボックス(dropBox)が縦に5個並んでいます。

numberBoxをdropBoxやdrop_back_boxにドラッグ・ドロップするのは簡単にできたのですが、

dropBoxに複数ドロップされたnumberBox間で左右にドラッグドロップできるようにするのに

かなり手こずってしまいました。

ドロップされたnumberBox間で左右にドラッグドロップするためにはnumberBox自体に

ondrop属性を持たせてjavascriptのinsertBeforeを使用することで簡単にできました。。。
こんな簡単なことをなぜすぐに気づかなかったのか。。。

HTML5のdraggable属性はドラッグした要素やドロップ先の要素の情報を取得できるのでかなり便利です。

コードは下記の通りです!

<style>
.left_box {
    width: 110px;
    height:700px;
    background:#fff;
    margin-right: 56px;
}

.drag_box {
    width: 100px;
    height: 30px;
    background: red;
    margin: 2px;
}

.right_box {
    background: #fff;
    height: 700px;
    width: 110px;
}

.drop_box {
    width:800px;
    height:38px;
    margin:2px;
    background:yellow;
    display: flex;
}
</style>

<!-- 左のボックス -->
<div style="display:flex;">
    <div class="left_box">
        <div class="drop_back_box" ondragover="dragover(event)" ondrop="dropBack(event)">
            <?php for ($i=1; $i < 21; $i++): ?>
                <div class="drag_box" id="numberBox<?php echo $i; ?>" draggable="true" ondragstart="f_dragstart(event)" ondrop="dropLR(event)">
                    ナンバー:<?php echo $i; ?>
                </div>
            <?php endfor; ?>
        </div>
    </div>


<!-- 右のボックス -->
    <div class="right_box">
        <?php $i=1; for($i=1; $i < 6; $i++): ?>
            <div id="dropBox<?php echo $i; ?>" class="drop_box" ondragover="dragover(event)" ondrop="drop(event)">
            </div>
        <?php endfor; ?>
    </div>

</div>
<script>
/***** ドラッグ開始時の処理 *****/
function f_dragstart(event){
    //ドラッグするデータのid名をDataTransferオブジェクトにセット
    event.dataTransfer.effectAllowed = 'move';
    event.dataTransfer.setData("text", event.target.id);
}

/***** ドラッグ要素がドロップ要素に重なっている間の処理 *****/
function dragover(event){
    event.dataTransfer.effectAllowed = 'move';
    //dragoverイベントをキャンセルして、ドロップ先の要素がドロップを受け付けるようにする
    event.preventDefault();
}

/***** 右のボックスにドロップ時の処理 *****/
function drop(event){
    // ドラッグされたデータのid名をDataTransferオブジェクトから取得
    const boxId = event.dataTransfer.getData("text");
    const numberBox = document.getElementById(boxId);
    const dropBox = event.currentTarget;
    dropBox.appendChild(numberBox);
    // エラー回避のため、ドロップ処理の最後にdropイベントをキャンセルしておく
    event.preventDefault();
};

function dropLR(event){
    // ドラッグされたデータのid名をDataTransferオブジェクトから取得
    const draggedEleId = event.dataTransfer.getData("text");
    // ドラッグされた要素を取得
    const draggedElement = document.getElementById(draggedEleId);
    // ドロップされた要素を取得
    const dropedElement = event.currentTarget;
    // ①ドロップ先の親要素のid名を取得
    const dropedParentId = event.currentTarget.parentNode.id;

    const dropBox = document.getElementById(dropedParentId);


    // 左のボックスに戻す時以外は要素間の左右にドロップできる処理
    dropBox.insertBefore(draggedElement, dropedElement)

    // エラー回避のため、ドロップ処理の最後にdropイベントをキャンセルしておく
    event.preventDefault();
    event.stopPropagation();
};


// 左のボックスにドロップする処理
function dropBack(event){
// エラー回避のため、ドロップ処理の最後にdropイベントをキャンセルしておく
    event.preventDefault();
}
</script>

まだまだ自分の応用力のなさを痛感してばかりですが、これからもプログラミング頑張ろうと思います!

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

Gatsby静的サイトジェネレータで爆速な動的サイトを作る

最近Reactのプロジェクトは全部Gatsbyベースで作ればいいじゃないかな(大体適用できる)と思ったので
なぜそう思ったのかまとめてみます。(Gatsbyサイト実装の雛形付き)

Reactに代表されるSPAのメリットとしては

  • history APIによる疑似ルーティングでバックエンド無しでページ間遷移ができる(API無しの場合はファイルをホストするだけでWebページが作れる)
  • フロントエンドとバックエンド(API)の棲み分けがはっきりしてる(SSRしない場合)
  • 仮想DOMツリーによる差分レンダリングが高速

動的に仮想DOMツリーを生成するSPAの欠点として以下のことが挙げられます。

  • bundle.jsのファイルサイズが肥大化して読み込みが遅い
  • そもそも仮想DOMツリーの構築に初回のJS実行時間がかかる
  • TwitterやFacebookシェアのOGPを実装しようとするとSSRの実装に迫られる(複雑度が増すのでSSRしたくない・・・)

サイトパフォーマンス(特に初回レンダリング)やOGPに問題をかかえています。
Gatsbyを使うことでこれらの問題を解決しつつ、Reactアプリケーション(SPA)を作成することができます。

超えられない速度的な観点

参考:How Gatsby is so blazing fast

HTMLの事前生成

Gatsbyは、Reactを使用する静的なWebサイトジェネレーターです。
Webサイトの各ページのHTMLファイルを作成します。
つまり、「リクエストされたときにページを生成するのを待つ代わりに、Gatsbyはページを事前に構築します」

ユーザーがHTMLファイルを介してページにアクセスすると、ブラウザーはコンテンツをレンダリングします。
キャッシュやJavaScriptがない場合、aタグを使用すると、クリックされたときに別のHTMLファイルが読み込まれます。その結果、ユーザーは待たなければならない場合があります。さらに悪いことに、コンテンツのレンダリング中に空白のページが表示されます。

これは、シングルページアプリケーション(SPA)が登場するまでWebが設計されたときからの最も伝統的な方法です。

SPAは、JavaScriptでコンテンツを更新することでページをレンダリングします。静的ファイルをダウンロードするよりも更新がはるかに高速です。単一のHTMLファイルをロードし、ユーザーが操作するときにそのページを動的に更新するためです。

Reactは、SPAのビューレイヤーを処理するライブラリです。 Reactのようなフレームワークとライブラリは、JavaScriptコードの実行が開始されない限り、何をレンダリングするかを知りません。そのため、SPAとして構築すると、クリティカルレンダリングパスに大きな影響を与えます。
(つまり、JavaScriptで動的にレンダリングすることが初回レンダリングのボトルネックになります。)

1_aAoiF2OcphAPg8DoYwXqpA.png

Gatsbyには、初回レンダリングに最適化されたwebpack構成があります。

  • HTMLタグの事前生成
  • 非同期なJavaScriptコード実行。ユーザーインタラクションには必要ですが、初回レンダリングには必要ありません
  • 生成されたページのCSSはインラインなので、CSSファイルをダウンロードする必要はありません

Code Splittingとキャッシュ

ページを構築する際、Gatsbyはページに必要なコンポーネントを確認し、webpackにCode Splittingを自動的に実行させます。これは、Dynamic importを設定することにより適用されます。

この方法により、ブラウザーはWebサイト全体ではなく、ページに必要なファイルのみをリクエストし、ページのロード時間を短縮します。

該当ページのファイルしか読み込まない欠点は、ユーザーがリンクをクリックしたときにのみ他ページのファイルをダウンロードするため、ページ遷移が遅くなります。

この問題を回避するために、GatsbyのWebpack構成では、Link prefetchと呼ばれる手法を適用します。
(Link prefetchはブラウザのメカニズムであり、ブラウザのアイドル時間を利用して、近い将来ユーザーがアクセスする可能性のあるドキュメントをダウンロードまたは事前リクエストします。)

ブラウザーがページのロードを完了すると、ブラウザーはそれらをダウンロードするためのprefetch属性を持つlinkタグを探します。次に、ユーザーがリンクをクリックすると、ページにリクエストされたファイルが既にキャッシュにある可能性が高くなります。

すべてのページはReactアプリである

静的なWebサイトのページを遷移するには、HTMLファイルをロードする必要がありますが、Gatsbyには必要ありません。これらはReactアプリです。
(つまり、初回レンダリング時は事前に生成したhtmlを使い、2回目以降のレンダリングはReactでの動的なレンダリングを行います。)

「GatsbyはサイトのHTMLページを生成しますが、最初のHTMLが読み込まれるとブラウザーで引き継ぐJavaScriptランタイムも作成します」

別のページの各aタグは、Reach Routerによるルーティングになります。実際には、ページ上のコンテンツを更新する際は、あるHTMLファイルから別のHTMLファイルに変化しているように見えます。

画像最適化

HTTP Archiveは多くの人気のあるWebサイトをトラッキングしています。
ページがリクエストするデータの種類のほとんどは画像です。(約1/3は画像データ!)

1_ymIv-FahsytuypmNU560UQ.png

合計KB - ページがリクエストするすべてのリソースの転送サイズの合計は、モバイルの場合約1285.5 KBです。

1_AyrHOyvvLoTQAoBzRyBP-A.png

画像KB - ページでリクエストされたすべての外部画像の転送サイズの合計は、モバイルの場合491.0KBです。

画像の最適化は、Webサイトで最高のパフォーマンス向上の1つです。
ダウンロードするバイト数が少ないことは、必要な帯域幅が少ないことを意味するため、ブラウザはコンテンツをより速くダウンロードしてレンダリングできます。これらは、実行可能な最適化の一部です。

  • 必要な表示領域と同じ画像サイズに変更します
  • デスクトップとモバイル用に異なる解像度のレスポンシブ画像(srcset属性で表示を切り分ける)を生成します
  • 画像のメタデータを削除して画像圧縮を適用する
  • lazy loadを適用して、最初のページの読み込みを高速化する
  • 画像の読み込み中にプレースホルダーを表示する

これには多大な労力がかかり、Gatsbyには解決策があります。
gatsby-imageプラグインを使うことでプロセス全体を自動化できます。
Gatsbyの多くのツールと同様に、gatsby-imageプラグインはGraphQLを使用しています。
このプラグインは、imgタグにレスポンシブ画像を設定します。
レスポンシブ画像を作成し、画像圧縮を適用します。
これらはすべて、ビルド時に行われます。
(注意:ビルド時最適化なので動的画像リソースには適応できません!)

画像が読み込まれると、「blur-up」手法により、すでにHTMLファイル(または背景のみ)にある非常に低品質の画像でプレビューが表示されます。その後、高品質の画像に切り替わります。(Mediumでも使われている)

1_NtTh_CL3BXESFTWvMfzR9w.gif

minifyとユニークファイル名

これらの手法は、人気のあるフレームワークとライブラリですでに広く使用されており、Gatsbyではそれほど大きな違いはありません。
webpack(productionモード)を使用してビルドすると、デフォルトですべてのファイルがminifyされます。

ファイルは、ファイル名にハッシュを割り当てることによってビルドされたときに一意です。
何かが変更されると、ファイルに新しい名前が付けられます。
この背景は、これらのファイルをブラウザーキャッシュからの取得を長期間許可できるようにするためです。
そのため、ユーザーがWebサイトに戻ってきたとき、すでにファイルをブラウザに持っています。

ファイルを更新すると、ビルド時に新しいファイル名が付けられます。
この場合、キャッシュからのファイルと一致しないため、ブラウザは更新されたファイルをダウンロードします。


以上のようにGatsbyには(特に初回)レンダリングを最適化する構成がされています。
Gatsbyと同様のことを素のReactプロジェクトでやろうとすると事前レンダリングの実装やwebpackのCode Splittingなどの複雑な構成を自前で作成しなければなりません。(この辺の最適化は自前だとGatsbyに勝てる気がしない・・・)
何でGatsbyに負けたか、明日まで考えてきてください
hqdefault.jpg

Gatsbyで動的なサイトを作る

さて、本題です。
まず、Gatsbyでの簡単なプロジェクトの作成の仕方に関してわからない人は秒速で理解してきてください。
静的サイトジェネレータGatsbyを秒速で理解する

Gatsbyはプリレンダリングの仕組みを用意してくれてるReactアプリケーションという認識です。
この辺は普通のReactアプリケーションと同じでページでstateも持てますし、reduxも使用できます。
ただ、いくつかのGatsbyプラグインをいれたり、Gatsby独自のルールに従う必要があります。
今回のサンプル(雛形)を元に話します。

サンプルの構成です。

  • TypeScript
  • ESLint
  • Redux
  • Material-UI
  • express
  • mongoose

ビルド設定

Gatsbyは裏側ではwebpackでビルドされているため、babelのビルド設定ファイルなどもビルドに反映します。
.babelrcにbabel関連のプラグインを導入します。
babel-preset-gatsbyのpresetを導入します。このpresetにはbabel-preset-envとbabel-preset-reactのpresetが含まれています。
クラス関数文法サポートの@babel/plugin-proposal-class-propertiesとオプショナルチェーン文法の@babel/plugin-proposal-optional-chainingを導入しています。

.babrlrc
{
  "presets": [
    [
      "babel-preset-gatsby",
      { "targets": { "browsers": [">0.25%", "not dead"] }}
    ]
  ],
  "plugins": [
    "@babel/plugin-proposal-optional-chaining",
    "@babel/plugin-proposal-class-properties"
  ]
}

gatsby-config.jsにgatsbyのプラグイン関連の設定を記述します。
スタータ構成のプラグイン設定から足したものは
TypeScript、ESLint、Material-UIテーマサポートのプラグインです。
TypeScriptサポートのgatsby-plugin-typescriptプラグイン、
TypeScript形チェックのgatsby-plugin-typescript-checkerプラグイン、
graphqlデータ型の自動生成のgatsby-plugin-graphql-codegenプラグイン、
ESLintのlintチェックのgatsby-plugin-eslintプラグイン、
Material-UIのテーマサポートのgatsby-theme-material-uiプラグイン
を追加しています。
proxyは開発時(gatsby develop)にapiパスのリクエストをサーバに向けたいために設定しています。(Gatsbyの開発サーバとAPIサーバのポートが違うため、CORSのエラーになるのを防ぐ)
本番ビルド時はproxy設定は使われません。

gatsby-config.js
module.exports = {
  siteMetadata: {
    title: 'Gatsbyサンプル',
    description: 'Gatsbyサイトのサンプルです',
  },
  plugins: [
    'gatsby-plugin-react-helmet',
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        name: 'images',
        path: `${__dirname}/src/images`,
      },
    },
    'gatsby-transformer-sharp',
    'gatsby-plugin-sharp',
    {
      resolve: 'gatsby-plugin-manifest',
      options: {
        name: 'gatsby-starter-default',
        short_name: 'starter',
        start_url: '/',
        background_color: '#663399',
        theme_color: '#663399',
        display: 'minimal-ui',
        icon: 'src/images/gatsby-icon.png', // This path is relative to the root of the site.
      },
    },
    // this (optional) plugin enables Progressive Web App + Offline functionality
    // To learn more, visit: https://gatsby.dev/offline
    'gatsby-plugin-offline',
    'gatsby-plugin-typescript',
    'gatsby-plugin-typescript-checker',
    {
      resolve: 'gatsby-plugin-graphql-codegen',
      options: {
        fileName: 'types/graphql-types.d.ts',
      },
    },
    {
      resolve: 'gatsby-plugin-eslint',
      options: {
        test: /\.(j|t)sx?$/,
        exclude: /(node_modules|.cache|public)/,
        stages: ['develop'],
        options: {
          emitWarning: true,
          failOnError: false
        }
      }
    },
    'gatsby-theme-material-ui',
  ],
  proxy: {
    prefix: "/api",
    url: "http://localhost:8080",
  },
}

package.jsonです。
スタータ構成のパッケージからTypeScript、ESlint、express、mongodb、redux、Material-UI、react-final-form関連のパッケージを追加しています。

package.json
{
  "private": true,
  "name": "gatsby",
  "version": "0.1.0",
  "author": "teradonburi",
  "license": "MIT",
  "dependencies": {
    "@material-ui/core": "^4.9.1",
    "@material-ui/icons": "^4.9.1",
    "@types/body-parser": "^1.19.0",
    "@types/express": "^4.17.2",
    "@types/mongoose": "^5.7.1",
    "@types/react": "^16.9.19",
    "@types/react-dom": "^16.9.5",
    "@types/react-helmet": "^5.0.15",
    "@types/react-redux": "^7.1.7",
    "@typescript-eslint/eslint-plugin": "^2.19.0",
    "@typescript-eslint/parser": "^2.19.0",
    "babel-preset-gatsby": "^0.2.28",
    "eslint": "^6.8.0",
    "eslint-plugin-react": "^7.18.3",
    "express": "^4.17.1",
    "final-form": "^4.18.7",
    "gatsby": "^2.19.7",
    "gatsby-image": "^2.2.39",
    "gatsby-plugin-eslint": "^2.0.8",
    "gatsby-plugin-graphql-codegen": "^2.2.1",
    "gatsby-plugin-manifest": "^2.2.39",
    "gatsby-plugin-offline": "^3.0.32",
    "gatsby-plugin-react-helmet": "^3.1.21",
    "gatsby-plugin-sharp": "^2.4.3",
    "gatsby-plugin-typescript": "^2.1.27",
    "gatsby-plugin-typescript-checker": "^1.1.1",
    "gatsby-source-filesystem": "^2.1.46",
    "gatsby-theme-material-ui": "^1.0.8",
    "gatsby-transformer-sharp": "^2.3.13",
    "mongoose": "^5.8.12",
    "mongoose-lean-virtuals": "^0.5.0",
    "nodemon": "^2.0.2",
    "npm-run-all": "^4.1.5",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-final-form": "^6.3.5",
    "react-helmet": "^5.2.1",
    "react-redux": "^7.1.3",
    "redux": "^4.0.5",
    "redux-thunk": "^2.3.0",
    "ts-node": "^8.6.2",
    "typescript": "^3.7.5",
    "typescript-fsa": "^3.0.0",
    "typescript-fsa-reducers": "^1.2.1"
  },
  "devDependencies": {},
  "scripts": {
    "start": "npm run develop",
    "develop": "run-p develop:*",
    "develop:client": "gatsby develop",
    "develop:server": "nodemon api/server.ts",
    "build": "gatsby build",
    "serve": "ts-node api/server.ts",
    "clean": "gatsby clean",
    "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1"
  }
}

開発時はnpm-run-allパッケージの-pオプションでGatsbyの開発サーバ起動とAPIサーバの同時起動を行っています。

    "develop": "run-p develop:*",
    "develop:client": "gatsby develop",
    "develop:server": "nodemon api/server.ts",

サーバーの起動はnodemon(サーバ修正時に再起動)とts-node(TypeScriptのままnodeサーバ実行)で行っています。
nodemon.jsonにnodemonの起動設定を記載してます。

nodemon.json
{
  "verbose": true,
  "ignore": ["public", "src"],
  "exec": "ts-node"
}

Gatsby(クライアント)で埋め込まれる環境変数を.env.developmentに記載します。(本番ビルド時は.env.productionが使われる)
今回はAPIコールのurlを指定するため(後述)、APIサーバのurlを環境変数と指定します。

env.development
SERVER=http://localhost:8000

本番ビルドはgatsby buildで行います。
publicフォルダに最適化されたフロントエンドのコードが生成されます。
本番APIサーバの起動はnodemonを外して起動します。

    "build": "gatsby build",
    "serve": "ts-node api/server.ts",

.env.productionにはgatsby-configのproxyの設定は使わないので本番APIサーバのURLを指定します。
(APIサーバのpublicフォルダでホスティングする想定の場合)

env.production
SERVER=http://localhost:8080

この場合、APIサーバの公開フォルダをpublicフォルダに設定しておくとAPIサーバを起動するだけでフロントエンドもホスティングできるので楽です。

server.js
app.use(express.static(path.join(__dirname, '../public')))

今回のサンプルで本番ビルド後、APIサーバ立ち上げのhttp://localhost:8080で表示されます。

フロントエンド(Gatsby)の実装

Material-UI(v4)テーマとReduxの初期化をします。
gatsby-theme-material-uiプラグインにより、
src/gatsby-theme-material-ui-top-layout/components/top-layout.tsx以下にテーマの初期化を行います。
このコンポーネントを作成すると一番最初に呼ばれますので初期化処理をここに書きます。
TopLayoutのchildrenに渡ってくるのがすべてのページのRootコンポーネントとなります。
Reduxも使うのでReduxの初期化もここで行います。

src/gatsby-theme-material-ui-top-layout/components/top-layout.tsx
import React from 'react'
import { Provider } from 'react-redux'
import ThemeTopLayout from 'gatsby-theme-material-ui-top-layout/src/components/top-layout'
import { Theme } from '@material-ui/core'

import createStore from '../../state/createStore'

export default function TopLayout({ children, theme }: {children: JSX.Element | JSX.Element[]; theme: Theme}): JSX.Element {
  const store = createStore()

  return (
    <Provider store={store}>
      <ThemeTopLayout theme={theme}>{children}</ThemeTopLayout>
    </Provider>
  )
}

src/gatsby-theme-material-ui-top-layout/theme.tsにMaterial-UIのカスタムテーマを実装します。

theme.ts
import { createMuiTheme } from '@material-ui/core'

// A custom theme for this app
const theme = createMuiTheme({
  ...(中略)
})

export default theme

ちなみにThemeTopLayoutコンポーネントの型定義がないので自作してます。
(types/gatsby-theme-material-ui-top-layout/src/components/top-layout/index.d.ts)

types/gatsby-theme-material-ui-top-layout/src/components/top-layout/index.d.ts
import { Theme } from '@material-ui/core'

declare function ThemeTopLayout({ children, theme }: {children: JSX.Element | JSX.Element[]; theme: Theme}): JSX.Element;
export = ThemeTopLayout;

型定義ファイルを自作する場合、tsconfig.jsonのpathsに型定義ファイルのパスを指定する必要があります。

tsconfig.json
"paths": {
  "interface": ["types/interface"],
  "mongoose-lean-virtuals": ["types/mongoose-lean-virtuals"],
  "gatsby-theme-material-ui-top-layout/src/components/top-layout": ["types/gatsby-theme-material-ui-top-layout/src/components/top-layout"]
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */

Reduxストアの初期化はsrc/state/createStore.tsにて実装しています。
APIコールをするためにredux-thunkとaxiosをapplyMiddlewareしてactionで使えるようにしています。
axiosのcreate時にprocess.env.SERVERでサーバURLを指定しています。

createStore.ts
import { combineReducers, createStore as reduxCreateStore, Store, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import axios from 'axios'
import user from '../reducers/user'

const client = axios.create({baseURL: process.env.SERVER})
const thunkWithClient = thunk.withExtraArgument(client)
const reducer = combineReducers({
  user,
})

const initialData = {}
const createStore = (): Store => reduxCreateStore(reducer, initialData, compose(applyMiddleware(thunkWithClient)))

export default createStore

src/actions/user.tsに実際のAPIコールのアクション処理を記述しています。
typescript-fsaでアクションの型を記述しています。

actions/user.ts
import actionCreatorFactory from 'typescript-fsa'
import { AxiosInstance } from 'axios'
import { Store } from 'redux'
import { route } from 'interface'

const actionCreator = actionCreatorFactory()

// typescript-fsaで<params,result,error>の型を定義
export const loadAction = actionCreator.async<{}, {users: route.User[]}, {error: Error}>('user/LOAD')
export const createAction = actionCreator.async<{user: route.User}, {user: route.User}, {error: Error}>('user/CREATE')

// actionの定義
export function load() {
  // clientはaxiosの付与したクライアントパラメータ
  // 非同期処理をPromise形式で記述できる
  return (dispatch: Store['dispatch'], getState: Store['getState'], client: AxiosInstance): Promise<void> => {
    return client
      .get('/api/users')
      .then(res => res.data)
      .then(users => {
        // 成功
        dispatch(loadAction.done({
          params: {},
          result: { users },
        }))
      })
      .catch(error => {
        // 失敗
        dispatch(loadAction.failed({params: {}, error}))
      })
  }
}

export function create(user: route.User) {
  // clientはaxiosの付与したクライアントパラメータ
  // 非同期処理をPromise形式で記述できる
  return (dispatch: Store['dispatch'], getState: Store['getState'], client: AxiosInstance): Promise<void> => {
    return client
      .post('/api/users', user)
      .then(res => res.data)
      .then(user => {
        // 成功
        dispatch(createAction.done({
          params: { user },
          result: { user },
        }))
      })
      .catch(error => {
        // 失敗
        dispatch(createAction.failed({params: { user }, error}))
      })
  }
}

src/reducer/user.tsに実際のAPIコールの結果のredux保存処理を記述しています。
typescript-fsa-reducersでアクション結果によって、処理を分岐しています。

reducer/user.ts
import { reducerWithInitialState } from 'typescript-fsa-reducers'
import { redux } from 'interface'
import { loadAction, createAction } from '../actions/user'

// 初期化オブジェクト
const initialState: redux.User = {
  users: [],
  user: null,
}

const reducer = reducerWithInitialState(initialState)
  .case(loadAction.done, (state, data) => ({...state, users: data.result.users}))
  .case(loadAction.failed, (state, data) => ({...state, error: data.error}))
  .case(createAction.done, (state, data) => ({...state, user: data.result.user}))
  .case(createAction.failed, (state, data) => ({...state, error: data.error}))

export default reducer

src/pages/index.tsxを修正します。
Material-UIのテーマが反映された状態で各Material-UIのコンポーネントが使えます。
APIコールはreact-reduxのconnectでReduxアクションの呼び出しとReduxストアの結果を取得しています。
普通にReactアプリケーションなので動的なページが作れます。
フォームはReact-Final-FormでMaterial-UIの入力欄をwrapして使用しています。

index.tsx
import React from 'react'
import { Link } from 'gatsby'
import { connect, ConnectedProps } from 'react-redux'

import { Form, Field } from 'react-final-form'
import { ValidationErrors, SubmissionErrors } from 'final-form'

import {
  Card,
  CardContent,
  Button,
  Dialog,
  DialogTitle,
  DialogContent,
  MenuItem,
} from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles'
import { Email } from '@material-ui/icons'
import { orange } from '@material-ui/core/colors'

import Layout from '../components/layout'
import SEO from '../components/seo'
import TextInput from '../components/mui-form/TextInput'

import { load, create } from '../actions/user'
import { redux } from 'interface'

const connector = connect(
  // propsに受け取るreducerのstate
  ({user}: {user: redux.User}) => ({
    users: user?.users,
  }),
  // propsに付与するactions
  { load, create }
)

const useStyles = makeStyles(theme => ({
  root: {
    fontStyle: 'italic',
    fontSize: 21,
    minHeight: 64,
    // 画面サイズがモバイルサイズのときのスタイル
    [theme.breakpoints.down('xs')]: {
      fontStyle: 'normal',
    },
  },
  card: {
    background: (props: {bgcolor: string}): string => `${props.bgcolor}`, // props経由でstyleを渡す
  },
  img: {
    width: 150,
    height: 150,
  },
  name: {
    margin: 10,
    color: theme.palette.primary.main,
  },
  gender: {
    margin: 10,
    color: theme.palette.secondary.main, // themeカラーを参照
  },
}))

interface FormValues {
  gender?: string;
  first?: string;
  last?: string;
  email?: string;
}

type Props = ConnectedProps<typeof connector>

const IndexPage: React.FC<Props> = (props) => {
  const { users, load, create } = props
  const [open, setOpen] = React.useState(false)
  const classes = useStyles({bgcolor: 'ff00ff'})

  React.useEffect(() => {
    load()
  }, [])

  const validate = (values: FormValues): ValidationErrors => {
    const errors: FormValues = {}
    if (!values.first) {
      errors.first = '必須項目です'
    }
    if (!values.last) {
      errors.last = '必須項目です'
    }
    if (!values.email) {
      errors.email = '必須項目です'
    }
    return errors
  }

  const submit = (values: FormValues): (SubmissionErrors | Promise<SubmissionErrors | undefined> | void) => {
    const data = {
      gender: values.gender,
      name: {
        first: values.first,
        last: values.last,
      },
      email: values.email,
    }
    create(data)
      .then(() => load())
      .finally(() => setOpen(false))
  }

  return (
    <Layout>
      <SEO title='Home' />
      {/* 配列形式で返却されるためmapで展開する */}
      {users &&
        users.map(user => {
          return (
            // ループで展開する要素には一意なkeyをつける(ReactJSの決まり事)
            <Card key={user.email} style={{ marginTop: '10px' }}>
              <CardContent className={classes.card}>
                <img className={classes.img} src={user.picture?.thumbnail} />
                <p className={classes.name}>
                  {'名前:' + user?.name?.last + ' ' + user?.name?.first}
                </p>
                <p className={classes.gender}>
                  {'性別:' + (user?.gender == 'male' ? '男性' : '女性')}
                </p>
                <div style={{ textAlign: 'right' }}>
                  <Email style={{ marginRight: 5, color: orange[200] }} />
                  {user?.email}
                </div>
              </CardContent>
            </Card>
          )
        })}
        <Button
          variant="contained"
          color="primary"
          onClick={(): void => setOpen(true)}
          style={{marginTop: 30}}
        >
          新規ユーザ作成
        </Button>
        <Link style={{display: 'block', marginTop: 30}} to="/hoge">存在しないページ</Link>
        <Dialog
          open={!!open}
          onClose={(): void => setOpen(false)}
        >
          <DialogTitle>新規ユーザ</DialogTitle>
          <DialogContent>
            <Form onSubmit={submit} validate={validate}>
              {({handleSubmit}): JSX.Element =>
                <form onSubmit={handleSubmit}>
                  <Field name='gender' initialValue='male' component={TextInput} label='性別' select >
                    <MenuItem value='male'>男性</MenuItem>
                    <MenuItem value='female'>女性</MenuItem>
                  </Field>
                  <Field name='last' component={TextInput} label='' />
                  <Field name='first' component={TextInput} label='' />
                  <Field name='email' component={TextInput} label='Email' type='email' />
                  <Button type='submit' variant='contained' color='primary'>送信</Button>
                </form>
              }
            </Form>
          </DialogContent>
        </Dialog>
      <Link to="/page-2/">Go to page 2</Link>
    </Layout>
  )
}

export default connector(IndexPage)

バックエンドの実装

普通のexpressサーバアプリケーションです。特に注意すべき点はないです。
TypeScriptで書いているため、慣れてない人は戸惑うかもしれませんが・・・
mongodbの接続設定はmongodb://localhost/gatsby-templateスキーマにしてるので適宜変えて下さい。

server.ts
import path from 'path'
import { Request, Response, NextFunction } from 'express'
import express from 'express'
import bodyParser from 'body-parser'
import {default as mongoose} from 'mongoose'
mongoose.connect('mongodb://localhost/gatsby-template', { useNewUrlParser: true, useUnifiedTopology: true})

const app = express()

// APIエラーハンドリング
const wrap = (fn: (req: Request, res: Response, next?: NextFunction) => Promise<Response | undefined>) => (req: Request, res: Response, next?: NextFunction): Promise<Response | undefined> => fn(req, res, next).catch((err: Error) => {
  console.error(err)
  if (!res.headersSent) {
    return res.status(500).json({message: 'Internal Server Error'})
  }
})
// NodeJSエラーハンドリング
process.on('uncaughtException', (err) => console.error(err))
process.on('unhandledRejection', (err) => console.error(err))

app.use(express.static(path.join(__dirname, '../public')))
app.use(bodyParser.urlencoded({extended: true}))
app.use(bodyParser.json())

import { users } from './routes'

app.use(
  '/api/users',
  express.Router()
    .get('/', wrap(users.index))
    .post('/', wrap(users.create))
)

// サーバを起動
app.listen(8080, () => console.log('Server started http://localhost:8080'))

mongooseのuserモデル定義です(TypeScript)。
画像等はuserのidのurlパスでS3などのファイルストレージに上げることが多いので
モデルのフィールドとして持つのでなく、virtualでfind時に画像パスを作成する構造が吉です。
(今回はサンプルなので省略してます)
find時にleanを併用すると検索が高速化するのですが、virtualが呼ばれなくなる対策としてmongoose-lean-virtualsを使用してます。

user.ts
import {default as mongoose} from 'mongoose'
const Schema = mongoose.Schema
import mongooseLeanVirtuals from 'mongoose-lean-virtuals'
import { model } from '../../types/interface'

const schema = new Schema({
  gender: String,
  first: String,
  last: String,
  email: String,
}, {
  timestamps: true,
  toObject: {
    virtuals: true,
  },
  toJSON: {
    virtuals: true,
    transform: (doc, m): model.User => {
      delete m.__v
      return m
    },
  },
})

schema.pre('update', async function(next) {
  this.setOptions({runValidators: true})
  return next()
})
schema.pre('findOneAndUpdate', async function(next) {
  this.setOptions({runValidators: true, new: true})
  return next()
})

schema.virtual('thumbnail').get(function () {
  // TODO:固定じゃなくて変える
  return 'https://avatars1.githubusercontent.com/u/771218?s=460&v=4'
})

schema.plugin(mongooseLeanVirtuals)

export default mongoose.model<model.User>('User', schema)

routes/user.tsにてleanでfind検索するときにvirtualsのオプションをtrueにします。

user.ts
  const users: model.User[] = await User.find().lean({virtuals: true})

mongoose-lean-virtualsパッケージは型定義がないため、型定義をtypes/mongoose-lean-virtuals/index.d.tsに自作してます。

types/mongoose-lean-virtuals/index.d.ts
import { Schema } from 'mongoose'

// moongoose-lean-virtualsライブラリの型定義がないので書く
declare function mongooseLeanVirtuals(schema: Schema): void;
export = mongooseLeanVirtuals;

Gatsbyのメリット・デメリットまとめ

総括するとこんな感じになります

メリット

メリットはGatsby自体のビルドの仕組み(プリレンダリング)、構成が優れていることが挙げられます。

  • 素のReactより表示(特に初回レンダリング)が早い(Link prefetchで次ページのリソースの先読みもする)
  • ページ別にOGP(Twitter、Facebookシェア)埋め込みしたい場合、SSRしないといけなかった弱点も克服できる(Gatsbyの場合は複雑になりがちなSSRの実装を基本的に考えなくて良い)
  • 普通にReactアプリケーションなのでstateも使えるしreduxも入れられるので動的サイトも作れる、もちろんページ間遷移もできる
  • gatsby-imageプラグインを使えば画像リソースも解像度別に最適化してくれる
  • ビルド設定が(webpackに比べて)楽(デフォルトでページ単位にCode Splittingしてくれるし、Dynamic importもしてくれる、PWAの設定もプラグイン使えば楽)

デメリット

デメリットも予めビルドするというところにあります。ただし、パフォーマンス面に関してはGatsbyは他の追従を許さないくらい最適化されていますのでメリットのほうが大きいです。

  • 動的なページ生成が実行時にできない(ページ生成は実行時に基本的にできないので事前にビルドしておかないといけない、OGP埋め込みも)
  • Gatsby独自のルールがある(graphqlでのデータ埋め込み、ルーティングがpagesフォルダのファイル名に該当する、Linkコンポーネントはgatsbyのものを使うなど)

kachi_1.png

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

お前らのReactは遅すぎる(SSG編)

最近Reactのプロジェクトは全部Gatsbyベースで作ればいいじゃないかな(大体適用できる)と思ったので
なぜそう思ったのかまとめてみます。(Gatsbyサイト実装の雛形付き)

SPAのメリット・デメリット

Reactに代表されるSPAのメリットとしては

  • history APIによる疑似ルーティングでバックエンド無しでページ間遷移ができる(API無しの場合はファイルをホストするだけでWebページが作れる)
  • フロントエンドとバックエンド(API)の棲み分けがはっきりしてる(SSRしない場合)
  • 仮想DOMツリーによる差分レンダリングが高速

逆に動的に仮想DOMツリーを生成するSPAのデメリットとして以下のことが挙げられます。

  • bundle.jsのファイルサイズが肥大化して読み込みが遅い
  • 仮想DOMツリーの構築に初回のJS実行時間がかかる
  • TwitterやFacebookシェアのOGPを実装しようとするとSSRの実装に迫られる(複雑度が増すのでSSRしたくない・・・)

規模が大きくなってきたときのサイトパフォーマンス(特に初回レンダリング)やOGPに問題をかかえています。
Gatsby(静的サイトジェネレーション)を使うことでこれらの問題を解決しつつ、Reactアプリケーション(SPA)を作成することができます。

超えられない速度的な観点

参考:How Gatsby is so blazing fast

HTMLの事前生成

Gatsbyは、Reactを使用する静的なWebサイトジェネレーターです。
Webサイトの各ページのHTMLファイルを作成します。
つまり、「リクエストされたときにページを生成するのを待つ代わりに、Gatsbyはページを事前に構築します」

ユーザーがHTMLファイルを介してページにアクセスすると、ブラウザーはコンテンツをレンダリングします。
キャッシュやJavaScriptがない場合、aタグを使用すると、クリックされたときに別のHTMLファイルが読み込まれます。その結果、ユーザーは待たなければならない場合があります。さらに悪いことに、コンテンツのレンダリング中に空白のページが表示されます。

これは、シングルページアプリケーション(SPA)が登場するまでWebが設計されたときからの最も伝統的な方法です。

SPAは、JavaScriptでコンテンツを更新することでページをレンダリングします。静的ファイルをダウンロードするよりも更新がはるかに高速です。単一のHTMLファイルをロードし、ユーザーが操作するときにそのページを動的に更新するためです。

Reactは、SPAのビューレイヤーを処理するライブラリです。 Reactのようなフレームワークとライブラリは、JavaScriptコードの実行が開始されない限り、何をレンダリングするかを知りません。そのため、SPAとして構築すると、クリティカルレンダリングパスに大きな影響を与えます。
(つまり、JavaScriptで動的にレンダリングすることが初回レンダリングのボトルネックになります。

1_aAoiF2OcphAPg8DoYwXqpA.png

Gatsbyには、初回レンダリングに最適化されたwebpack構成があります。

  • HTMLタグの事前生成
  • 非同期なJavaScriptコード実行。ユーザーインタラクションには必要ですが、初回レンダリングには必要ありません
  • 生成されたページのCSSはインラインなので、CSSファイルをダウンロードする必要はありません

Code Splittingとキャッシュ

ページを構築する際、Gatsbyはページに必要なコンポーネントを確認し、webpackにCode Splittingを自動的に実行させます。これは、Dynamic importを設定することにより適用されます。

この方法により、ブラウザーはWebサイト全体ではなく、ページに必要なファイルのみをリクエストし、ページのロード時間を短縮します。

該当ページのファイルしか読み込まない欠点は、ユーザーがリンクをクリックしたときにのみ他ページのファイルをダウンロードするため、ページ遷移が遅くなります。

この問題を回避するために、GatsbyのWebpack構成では、Link prefetchと呼ばれる手法を適用します。
(Link prefetchはブラウザのメカニズムであり、ブラウザのアイドル時間を利用して、近い将来ユーザーがアクセスする可能性のあるドキュメントをダウンロードまたは事前リクエストします。)

ブラウザーがページのロードを完了すると、ブラウザーはそれらをダウンロードするためのprefetch属性を持つlinkタグを探します。次に、ユーザーがリンクをクリックすると、ページにリクエストされたファイルが既にキャッシュにある可能性が高くなります。

プリレンダリングとReactアプリのハイブリット

静的なWebサイトのページを遷移するには、HTMLファイルをロードする必要がありますが、Gatsbyには必要ありません。これらはReactアプリです。
(つまり、初回レンダリング時は事前に生成したhtmlを使い、2回目以降のレンダリングはReactでの動的なレンダリングを行います。)

「GatsbyはサイトのHTMLページを生成しますが、最初のHTMLが読み込まれるとブラウザーで引き継ぐJavaScriptランタイムも作成します」

別のページの各aタグは、Reach Routerによるルーティングになります。実際には、ページ上のコンテンツを更新する際は、あるHTMLファイルから別のHTMLファイルに変化しているように見えます。

画像最適化

HTTP Archiveは多くの人気のあるWebサイトをトラッキングしています。
ページがリクエストするデータの種類のほとんどは画像です。(約1/3は画像データ!)

1_ymIv-FahsytuypmNU560UQ.png

合計KB - ページがリクエストするすべてのリソースの転送サイズの合計は、モバイルの場合約1285.5 KBです。

1_AyrHOyvvLoTQAoBzRyBP-A.png

画像KB - ページでリクエストされたすべての外部画像の転送サイズの合計は、モバイルの場合491.0KBです。

画像の最適化は、Webサイトで最高のパフォーマンス向上の1つです。
ダウンロードするバイト数が少ないことは、必要な帯域幅が少ないことを意味するため、ブラウザはコンテンツをより速くダウンロードしてレンダリングできます。これらは、実行可能な最適化の一部です。

  • 必要な表示領域と同じ画像サイズに変更します
  • デスクトップとモバイル用に異なる解像度のレスポンシブ画像(srcset属性で表示を切り分ける)を生成します
  • 画像のメタデータを削除して画像圧縮を適用する
  • lazy loadを適用して、最初のページの読み込みを高速化する
  • 画像の読み込み中にプレースホルダーを表示する

これには多大な労力がかかり、Gatsbyには解決策があります。
gatsby-imageプラグインを使うことでプロセス全体を自動化できます。
Gatsbyの多くのツールと同様に、gatsby-imageプラグインはGraphQLを使用しています。
このプラグインは、imgタグにレスポンシブ画像を設定します。
レスポンシブ画像を作成し、画像圧縮を適用します。
これらはすべて、ビルド時に行われます。
(注意:ビルド時最適化なので実行時にアップロードされるような動的な画像リソースには適応できません!)

画像が読み込まれると、「blur-up」手法により、すでにHTMLファイル(または背景のみ)にある非常に低品質の画像でプレビューが表示されます。その後、高品質の画像に切り替わります。(Mediumでも使われている)

1_NtTh_CL3BXESFTWvMfzR9w.gif

minifyとユニークファイル名

これらの手法は、人気のあるフレームワークとライブラリですでに広く使用されており、Gatsbyではそれほど大きな違いはありません。
webpack(productionモード)を使用してビルドすると、デフォルトですべてのファイルがminifyされます。

ファイルは、ファイル名にハッシュを割り当てることによってビルドされたときに一意です。
何かが変更されると、ファイルに新しい名前が付けられます。
この背景は、これらのファイルをブラウザーキャッシュからの取得を長期間許可できるようにするためです。
そのため、ユーザーがWebサイトに戻ってきたとき、すでにファイルをブラウザに持っています。

ファイルを更新すると、ビルド時に新しいファイル名が付けられます。
この場合、キャッシュからのファイルと一致しないため、ブラウザは更新されたファイルをダウンロードします。


以上のようにGatsbyには(特に初回)レンダリングを最適化する構成がされています。
Gatsbyと同様のことを素のReactプロジェクトでやろうとすると事前レンダリングの実装やwebpackのCode Splittingなどの複雑な構成を自前で作成しなければなりません。(この辺の最適化は自前だとGatsbyに勝てる気がしない・・・)
何でGatsbyに負けたか、明日まで考えてきてください
hqdefault.jpg

Gatsbyで動的なサイトを作る

さて、本題です。
まず、Gatsbyでの簡単なプロジェクトの作成の仕方に関してわからない人は秒速で理解してきてください。
静的サイトジェネレータGatsbyを秒速で理解する

Gatsbyはプリレンダリングの仕組みを用意してくれてるReactアプリケーションという認識です。
この辺は普通のReactアプリケーションと同じでページでstateも持てますし、reduxも使用できます。
ただ、いくつかのGatsbyプラグインをいれたり、Gatsby独自のルールに従う必要があります。
今回のサンプル(雛形)を元に話します。

サンプルの構成です。

  • TypeScript
  • ESLint
  • Redux
  • Material-UI
  • express
  • mongoose

ビルド設定

Gatsbyは裏側ではwebpackでビルドされているため、babelのビルド設定ファイルなどもビルドに反映します。
.babelrcにbabel関連のプラグインを導入します。
babel-preset-gatsbyのpresetを導入します。このpresetにはbabel-preset-envとbabel-preset-reactのpresetが含まれています。
クラス関数文法サポートの@babel/plugin-proposal-class-propertiesとオプショナルチェーン文法の@babel/plugin-proposal-optional-chainingを導入しています。

.babrlrc
{
  "presets": [
    [
      "babel-preset-gatsby",
      { "targets": { "browsers": [">0.25%", "not dead"] }}
    ]
  ],
  "plugins": [
    "@babel/plugin-proposal-optional-chaining",
    "@babel/plugin-proposal-class-properties"
  ]
}

gatsby-config.jsにgatsbyのプラグイン関連の設定を記述します。
スタータ構成のプラグイン設定から足したものは
TypeScript、ESLint、Material-UIテーマサポートのプラグインです。
TypeScriptサポートのgatsby-plugin-typescriptプラグイン、
TypeScript形チェックのgatsby-plugin-typescript-checkerプラグイン、
graphqlデータ型の自動生成のgatsby-plugin-graphql-codegenプラグイン、
ESLintのlintチェックのgatsby-plugin-eslintプラグイン、
Material-UIのテーマサポートのgatsby-theme-material-uiプラグイン
を追加しています。
proxyは開発時(gatsby develop)にapiパスのリクエストをサーバに向けたいために設定しています。(Gatsbyの開発サーバとAPIサーバのポートが違うため、CORSのエラーになるのを防ぐ)
本番ビルド時はproxy設定は使われません。

gatsby-config.js
module.exports = {
  siteMetadata: {
    title: 'Gatsbyサンプル',
    description: 'Gatsbyサイトのサンプルです',
  },
  plugins: [
    'gatsby-plugin-react-helmet',
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        name: 'images',
        path: `${__dirname}/src/images`,
      },
    },
    'gatsby-transformer-sharp',
    'gatsby-plugin-sharp',
    {
      resolve: 'gatsby-plugin-manifest',
      options: {
        name: 'gatsby-starter-default',
        short_name: 'starter',
        start_url: '/',
        background_color: '#663399',
        theme_color: '#663399',
        display: 'minimal-ui',
        icon: 'src/images/gatsby-icon.png', // This path is relative to the root of the site.
      },
    },
    // this (optional) plugin enables Progressive Web App + Offline functionality
    // To learn more, visit: https://gatsby.dev/offline
    'gatsby-plugin-offline',
    'gatsby-plugin-typescript',
    'gatsby-plugin-typescript-checker',
    {
      resolve: 'gatsby-plugin-graphql-codegen',
      options: {
        fileName: 'types/graphql-types.d.ts',
      },
    },
    {
      resolve: 'gatsby-plugin-eslint',
      options: {
        test: /\.(j|t)sx?$/,
        exclude: /(node_modules|.cache|public)/,
        stages: ['develop'],
        options: {
          emitWarning: true,
          failOnError: false
        }
      }
    },
    'gatsby-theme-material-ui',
  ],
  proxy: {
    prefix: "/api",
    url: "http://localhost:8080",
  },
}

package.jsonです。
スタータ構成のパッケージからTypeScript、ESlint、express、mongodb、redux、Material-UI、react-final-form関連のパッケージを追加しています。

package.json
{
  "private": true,
  "name": "gatsby",
  "version": "0.1.0",
  "author": "teradonburi",
  "license": "MIT",
  "dependencies": {
    "@material-ui/core": "^4.9.1",
    "@material-ui/icons": "^4.9.1",
    "@types/body-parser": "^1.19.0",
    "@types/express": "^4.17.2",
    "@types/mongoose": "^5.7.1",
    "@types/react": "^16.9.19",
    "@types/react-dom": "^16.9.5",
    "@types/react-helmet": "^5.0.15",
    "@types/react-redux": "^7.1.7",
    "@typescript-eslint/eslint-plugin": "^2.19.0",
    "@typescript-eslint/parser": "^2.19.0",
    "babel-preset-gatsby": "^0.2.28",
    "eslint": "^6.8.0",
    "eslint-plugin-react": "^7.18.3",
    "express": "^4.17.1",
    "final-form": "^4.18.7",
    "gatsby": "^2.19.7",
    "gatsby-image": "^2.2.39",
    "gatsby-plugin-eslint": "^2.0.8",
    "gatsby-plugin-graphql-codegen": "^2.2.1",
    "gatsby-plugin-manifest": "^2.2.39",
    "gatsby-plugin-offline": "^3.0.32",
    "gatsby-plugin-react-helmet": "^3.1.21",
    "gatsby-plugin-sharp": "^2.4.3",
    "gatsby-plugin-typescript": "^2.1.27",
    "gatsby-plugin-typescript-checker": "^1.1.1",
    "gatsby-source-filesystem": "^2.1.46",
    "gatsby-theme-material-ui": "^1.0.8",
    "gatsby-transformer-sharp": "^2.3.13",
    "mongoose": "^5.8.12",
    "mongoose-lean-virtuals": "^0.5.0",
    "nodemon": "^2.0.2",
    "npm-run-all": "^4.1.5",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "react-final-form": "^6.3.5",
    "react-helmet": "^5.2.1",
    "react-redux": "^7.1.3",
    "redux": "^4.0.5",
    "redux-thunk": "^2.3.0",
    "ts-node": "^8.6.2",
    "typescript": "^3.7.5",
    "typescript-fsa": "^3.0.0",
    "typescript-fsa-reducers": "^1.2.1"
  },
  "devDependencies": {},
  "scripts": {
    "start": "npm run develop",
    "develop": "run-p develop:*",
    "develop:client": "gatsby develop",
    "develop:server": "nodemon api/server.ts",
    "build": "gatsby build",
    "serve": "ts-node api/server.ts",
    "clean": "gatsby clean",
    "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1"
  }
}

開発時はnpm-run-allパッケージの-pオプションでGatsbyの開発サーバ起動とAPIサーバの同時起動を行っています。

    "develop": "run-p develop:*",
    "develop:client": "gatsby develop",
    "develop:server": "nodemon api/server.ts",

サーバーの起動はnodemon(サーバ修正時に再起動)とts-node(TypeScriptのままnodeサーバ実行)で行っています。
nodemon.jsonにnodemonの起動設定を記載してます。

nodemon.json
{
  "verbose": true,
  "ignore": ["public", "src"],
  "exec": "ts-node"
}

Gatsby(クライアント)で埋め込まれる環境変数を.env.developmentに記載します。(本番ビルド時は.env.productionが使われる)
今回はAPIコールのurlを指定するため(後述)、APIサーバのurlを環境変数と指定します。

env.development
SERVER=http://localhost:8000

本番ビルドはgatsby buildで行います。
publicフォルダに最適化されたフロントエンドのコードが生成されます。
本番APIサーバの起動はnodemonを外して起動します。

    "build": "gatsby build",
    "serve": "ts-node api/server.ts",

.env.productionにはgatsby-configのproxyの設定は使わないので本番APIサーバのURLを指定します。
(APIサーバのpublicフォルダでホスティングする想定の場合)

env.production
SERVER=http://localhost:8080

この場合、APIサーバの公開フォルダをpublicフォルダに設定しておくとAPIサーバを起動するだけでフロントエンドもホスティングできるので楽です。

server.js
app.use(express.static(path.join(__dirname, '../public')))

今回のサンプルで本番ビルド後、APIサーバ立ち上げのhttp://localhost:8080で表示されます。

フロントエンド(Gatsby)の実装

Material-UI(v4)テーマとReduxの初期化をします。
gatsby-theme-material-uiプラグインにより、
src/gatsby-theme-material-ui-top-layout/components/top-layout.tsx以下にテーマの初期化を行います。
このコンポーネントを作成すると一番最初に呼ばれますので初期化処理をここに書きます。
TopLayoutのchildrenに渡ってくるのがすべてのページのRootコンポーネントとなります。
Reduxも使うのでReduxの初期化もここで行います。

src/gatsby-theme-material-ui-top-layout/components/top-layout.tsx
import React from 'react'
import { Provider } from 'react-redux'
import ThemeTopLayout from 'gatsby-theme-material-ui-top-layout/src/components/top-layout'
import { Theme } from '@material-ui/core'

import createStore from '../../state/createStore'

export default function TopLayout({ children, theme }: {children: JSX.Element | JSX.Element[]; theme: Theme}): JSX.Element {
  const store = createStore()

  return (
    <Provider store={store}>
      <ThemeTopLayout theme={theme}>{children}</ThemeTopLayout>
    </Provider>
  )
}

src/gatsby-theme-material-ui-top-layout/theme.tsにMaterial-UIのカスタムテーマを実装します。

theme.ts
import { createMuiTheme } from '@material-ui/core'

// A custom theme for this app
const theme = createMuiTheme({
  ...(中略)
})

export default theme

ちなみにThemeTopLayoutコンポーネントの型定義がないので自作してます。
(types/gatsby-theme-material-ui-top-layout/src/components/top-layout/index.d.ts)

types/gatsby-theme-material-ui-top-layout/src/components/top-layout/index.d.ts
import { Theme } from '@material-ui/core'

declare function ThemeTopLayout({ children, theme }: {children: JSX.Element | JSX.Element[]; theme: Theme}): JSX.Element;
export = ThemeTopLayout;

型定義ファイルを自作する場合、tsconfig.jsonのpathsに型定義ファイルのパスを指定する必要があります。

tsconfig.json
"paths": {
  "interface": ["types/interface"],
  "mongoose-lean-virtuals": ["types/mongoose-lean-virtuals"],
  "gatsby-theme-material-ui-top-layout/src/components/top-layout": ["types/gatsby-theme-material-ui-top-layout/src/components/top-layout"]
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */

Reduxストアの初期化はsrc/state/createStore.tsにて実装しています。
APIコールをするためにredux-thunkとaxiosをapplyMiddlewareしてactionで使えるようにしています。
axiosのcreate時にprocess.env.SERVERでサーバURLを指定しています。

createStore.ts
import { combineReducers, createStore as reduxCreateStore, Store, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import axios from 'axios'
import user from '../reducers/user'

const client = axios.create({baseURL: process.env.SERVER})
const thunkWithClient = thunk.withExtraArgument(client)
const reducer = combineReducers({
  user,
})

const initialData = {}
const createStore = (): Store => reduxCreateStore(reducer, initialData, compose(applyMiddleware(thunkWithClient)))

export default createStore

src/actions/user.tsに実際のAPIコールのアクション処理を記述しています。
typescript-fsaでアクションの型を記述しています。

actions/user.ts
import actionCreatorFactory from 'typescript-fsa'
import { AxiosInstance } from 'axios'
import { Store } from 'redux'
import { route } from 'interface'

const actionCreator = actionCreatorFactory()

// typescript-fsaで<params,result,error>の型を定義
export const loadAction = actionCreator.async<{}, {users: route.User[]}, {error: Error}>('user/LOAD')
export const createAction = actionCreator.async<{user: route.User}, {user: route.User}, {error: Error}>('user/CREATE')

// actionの定義
export function load() {
  // clientはaxiosの付与したクライアントパラメータ
  // 非同期処理をPromise形式で記述できる
  return (dispatch: Store['dispatch'], getState: Store['getState'], client: AxiosInstance): Promise<void> => {
    return client
      .get('/api/users')
      .then(res => res.data)
      .then(users => {
        // 成功
        dispatch(loadAction.done({
          params: {},
          result: { users },
        }))
      })
      .catch(error => {
        // 失敗
        dispatch(loadAction.failed({params: {}, error}))
      })
  }
}

export function create(user: route.User) {
  // clientはaxiosの付与したクライアントパラメータ
  // 非同期処理をPromise形式で記述できる
  return (dispatch: Store['dispatch'], getState: Store['getState'], client: AxiosInstance): Promise<void> => {
    return client
      .post('/api/users', user)
      .then(res => res.data)
      .then(user => {
        // 成功
        dispatch(createAction.done({
          params: { user },
          result: { user },
        }))
      })
      .catch(error => {
        // 失敗
        dispatch(createAction.failed({params: { user }, error}))
      })
  }
}

src/reducer/user.tsに実際のAPIコールの結果のredux保存処理を記述しています。
typescript-fsa-reducersでアクション結果によって、処理を分岐しています。

reducer/user.ts
import { reducerWithInitialState } from 'typescript-fsa-reducers'
import { redux } from 'interface'
import { loadAction, createAction } from '../actions/user'

// 初期化オブジェクト
const initialState: redux.User = {
  users: [],
  user: null,
}

const reducer = reducerWithInitialState(initialState)
  .case(loadAction.done, (state, data) => ({...state, users: data.result.users}))
  .case(loadAction.failed, (state, data) => ({...state, error: data.error}))
  .case(createAction.done, (state, data) => ({...state, user: data.result.user}))
  .case(createAction.failed, (state, data) => ({...state, error: data.error}))

export default reducer

src/pages/index.tsxを修正します。
Material-UIのテーマが反映された状態で各Material-UIのコンポーネントが使えます。
APIコールはreact-reduxのconnectでReduxアクションの呼び出しとReduxストアの結果を取得しています。
普通にReactアプリケーションなので動的なページが作れます。
フォームはReact-Final-FormでMaterial-UIの入力欄をwrapして使用しています。

index.tsx
import React from 'react'
import { Link } from 'gatsby'
import { connect, ConnectedProps } from 'react-redux'

import { Form, Field } from 'react-final-form'
import { ValidationErrors, SubmissionErrors } from 'final-form'

import {
  Card,
  CardContent,
  Button,
  Dialog,
  DialogTitle,
  DialogContent,
  MenuItem,
} from '@material-ui/core'
import { makeStyles } from '@material-ui/core/styles'
import { Email } from '@material-ui/icons'
import { orange } from '@material-ui/core/colors'

import Layout from '../components/layout'
import SEO from '../components/seo'
import TextInput from '../components/mui-form/TextInput'

import { load, create } from '../actions/user'
import { redux } from 'interface'

const connector = connect(
  // propsに受け取るreducerのstate
  ({user}: {user: redux.User}) => ({
    users: user?.users,
  }),
  // propsに付与するactions
  { load, create }
)

const useStyles = makeStyles(theme => ({
  root: {
    fontStyle: 'italic',
    fontSize: 21,
    minHeight: 64,
    // 画面サイズがモバイルサイズのときのスタイル
    [theme.breakpoints.down('xs')]: {
      fontStyle: 'normal',
    },
  },
  card: {
    background: (props: {bgcolor: string}): string => `${props.bgcolor}`, // props経由でstyleを渡す
  },
  img: {
    width: 150,
    height: 150,
  },
  name: {
    margin: 10,
    color: theme.palette.primary.main,
  },
  gender: {
    margin: 10,
    color: theme.palette.secondary.main, // themeカラーを参照
  },
}))

interface FormValues {
  gender?: string;
  first?: string;
  last?: string;
  email?: string;
}

type Props = ConnectedProps<typeof connector>

const IndexPage: React.FC<Props> = (props) => {
  const { users, load, create } = props
  const [open, setOpen] = React.useState(false)
  const classes = useStyles({bgcolor: 'ff00ff'})

  React.useEffect(() => {
    load()
  }, [])

  const validate = (values: FormValues): ValidationErrors => {
    const errors: FormValues = {}
    if (!values.first) {
      errors.first = '必須項目です'
    }
    if (!values.last) {
      errors.last = '必須項目です'
    }
    if (!values.email) {
      errors.email = '必須項目です'
    }
    return errors
  }

  const submit = (values: FormValues): (SubmissionErrors | Promise<SubmissionErrors | undefined> | void) => {
    const data = {
      gender: values.gender,
      name: {
        first: values.first,
        last: values.last,
      },
      email: values.email,
    }
    create(data)
      .then(() => load())
      .finally(() => setOpen(false))
  }

  return (
    <Layout>
      <SEO title='Home' />
      {/* 配列形式で返却されるためmapで展開する */}
      {users &&
        users.map(user => {
          return (
            // ループで展開する要素には一意なkeyをつける(ReactJSの決まり事)
            <Card key={user.email} style={{ marginTop: '10px' }}>
              <CardContent className={classes.card}>
                <img className={classes.img} src={user.picture?.thumbnail} />
                <p className={classes.name}>
                  {'名前:' + user?.name?.last + ' ' + user?.name?.first}
                </p>
                <p className={classes.gender}>
                  {'性別:' + (user?.gender == 'male' ? '男性' : '女性')}
                </p>
                <div style={{ textAlign: 'right' }}>
                  <Email style={{ marginRight: 5, color: orange[200] }} />
                  {user?.email}
                </div>
              </CardContent>
            </Card>
          )
        })}
        <Button
          variant="contained"
          color="primary"
          onClick={(): void => setOpen(true)}
          style={{marginTop: 30}}
        >
          新規ユーザ作成
        </Button>
        <Link style={{display: 'block', marginTop: 30}} to="/hoge">存在しないページ</Link>
        <Dialog
          open={!!open}
          onClose={(): void => setOpen(false)}
        >
          <DialogTitle>新規ユーザ</DialogTitle>
          <DialogContent>
            <Form onSubmit={submit} validate={validate}>
              {({handleSubmit}): JSX.Element =>
                <form onSubmit={handleSubmit}>
                  <Field name='gender' initialValue='male' component={TextInput} label='性別' select >
                    <MenuItem value='male'>男性</MenuItem>
                    <MenuItem value='female'>女性</MenuItem>
                  </Field>
                  <Field name='last' component={TextInput} label='' />
                  <Field name='first' component={TextInput} label='' />
                  <Field name='email' component={TextInput} label='Email' type='email' />
                  <Button type='submit' variant='contained' color='primary'>送信</Button>
                </form>
              }
            </Form>
          </DialogContent>
        </Dialog>
      <Link to="/page-2/">Go to page 2</Link>
    </Layout>
  )
}

export default connector(IndexPage)

バックエンドの実装

普通のexpressサーバアプリケーションです。特に注意すべき点はないです。
TypeScriptで書いているため、慣れてない人は戸惑うかもしれませんが・・・
mongodbの接続設定はmongodb://localhost/gatsby-templateスキーマにしてるので適宜変えて下さい。

server.ts
import path from 'path'
import { Request, Response, NextFunction } from 'express'
import express from 'express'
import bodyParser from 'body-parser'
import {default as mongoose} from 'mongoose'
mongoose.connect('mongodb://localhost/gatsby-template', { useNewUrlParser: true, useUnifiedTopology: true})

const app = express()

// APIエラーハンドリング
const wrap = (fn: (req: Request, res: Response, next?: NextFunction) => Promise<Response | undefined>) => (req: Request, res: Response, next?: NextFunction): Promise<Response | undefined> => fn(req, res, next).catch((err: Error) => {
  console.error(err)
  if (!res.headersSent) {
    return res.status(500).json({message: 'Internal Server Error'})
  }
})
// NodeJSエラーハンドリング
process.on('uncaughtException', (err) => console.error(err))
process.on('unhandledRejection', (err) => console.error(err))

app.use(express.static(path.join(__dirname, '../public')))
app.use(bodyParser.urlencoded({extended: true}))
app.use(bodyParser.json())

import { users } from './routes'

app.use(
  '/api/users',
  express.Router()
    .get('/', wrap(users.index))
    .post('/', wrap(users.create))
)

// サーバを起動
app.listen(8080, () => console.log('Server started http://localhost:8080'))

mongooseのuserモデル定義です(TypeScript)。
画像等はuserのidのurlパスでS3などのファイルストレージに上げることが多いので
モデルのフィールドとして持つのでなく、virtualでfind時に画像パスを作成する構造が吉です。
(今回はサンプルなので省略してます)
find時にleanを併用すると検索が高速化するのですが、virtualが呼ばれなくなる対策としてmongoose-lean-virtualsを使用してます。

user.ts
import {default as mongoose} from 'mongoose'
const Schema = mongoose.Schema
import mongooseLeanVirtuals from 'mongoose-lean-virtuals'
import { model } from '../../types/interface'

const schema = new Schema({
  gender: String,
  first: String,
  last: String,
  email: String,
}, {
  timestamps: true,
  toObject: {
    virtuals: true,
  },
  toJSON: {
    virtuals: true,
    transform: (doc, m): model.User => {
      delete m.__v
      return m
    },
  },
})

schema.pre('update', async function(next) {
  this.setOptions({runValidators: true})
  return next()
})
schema.pre('findOneAndUpdate', async function(next) {
  this.setOptions({runValidators: true, new: true})
  return next()
})

schema.virtual('thumbnail').get(function () {
  // TODO:固定じゃなくて変える
  return 'https://avatars1.githubusercontent.com/u/771218?s=460&v=4'
})

schema.plugin(mongooseLeanVirtuals)

export default mongoose.model<model.User>('User', schema)

routes/user.tsにてleanでfind検索するときにvirtualsのオプションをtrueにします。

user.ts
  const users: model.User[] = await User.find().lean({virtuals: true})

mongoose-lean-virtualsパッケージは型定義がないため、型定義をtypes/mongoose-lean-virtuals/index.d.tsに自作してます。

types/mongoose-lean-virtuals/index.d.ts
import { Schema } from 'mongoose'

// moongoose-lean-virtualsライブラリの型定義がないので書く
declare function mongooseLeanVirtuals(schema: Schema): void;
export = mongooseLeanVirtuals;

Gatsbyのメリット・デメリットまとめ

総括するとこんな感じになります

メリット

メリットはGatsby自体のビルドの仕組み(プリレンダリング)、構成が優れていることが挙げられます。

  • 素のReactより表示(特に初回レンダリング)が早い(Link prefetchで次ページのリソースの先読みもする)
  • ページ別にOGP(Twitter、Facebookシェア)埋め込みしたい場合、SSRしないといけなかった弱点も克服できる(Gatsbyの場合は複雑になりがちなSSRの実装を基本的に考えなくて良い)
  • 普通にReactアプリケーションなのでstateも使えるしreduxも入れられるので動的サイトも作れる、もちろんページ間遷移もできる
  • gatsby-imageプラグインを使えば画像リソースも解像度別に最適化してくれる
  • ビルド設定が(webpackに比べて)楽(デフォルトでページ単位にCode Splittingしてくれるし、Dynamic importもしてくれる、PWAの設定もプラグイン使えば楽)

デメリット

デメリットも予めビルドするというところにあります。ただし、パフォーマンス面に関してはGatsbyは他の追従を許さないくらい最適化されていますのでメリットのほうが大きいです。

  • 動的なページ生成が実行時にできない(ページ生成は実行時に基本的にできないので事前にビルドしておかないといけない、OGP埋め込みも)
  • Gatsby独自のルールがある(graphqlでのデータ埋め込み、ルーティングがpagesフォルダのファイル名に該当する、Linkコンポーネントはgatsbyのものを使うなど)

kachi_1.png

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

Xorshiftなどの擬似乱数をプロットして比較してみた

Xorshift で得られた乱数が適度にばらけているか視覚的に確認するため、平面に分布をプロットしました。

比較のためアルゴリズムが選べるようにしましたが、残念ながら違いは感じられませんでした。

See the Pen Xorshift32 by 七誌 (@7shi) on CodePen.

この記事には Haskell 版があります。

実装

Xorshift は実装が簡単な割に良好な擬似乱数を生成します。

JavaScript で実装します。

const UINT32_MAX_NEXT = 65536 * 65536;
let getSeed = () => Math.floor(Math.random() * (UINT32_MAX_NEXT - 1)) + 1;
let getNext = (() => {
  let s = Uint32Array.of(getSeed());
  return seed => {
    if (seed) s[0] = seed;
    s[0] ^= s[0] << 13;
    s[0] ^= s[0] >> 17;
    s[0] ^= s[0] << 5;
    return s[0];
  };
})();
  • 型付きで演算するため 1 要素の TypedArray を使用しました。
  • 引数でシード値が指定できるようにしました。指定しなければ保持していた値を使います。初期値は JavaScript 組み込みの乱数から得ます。

Math.random と同じ使い方ができるように、結果を 0 以上 1 未満の浮動小数点数に変換します。

let getFloat = u32 => u32 / UINT32_MAX_NEXT;
let rand = seed => getFloat(getNext(seed));

Math.random

Math.random ではシード値が指定できません。

実装側で乱数生成アルゴリズムの初期シードを選択し、ユーザーが初期シードを選択、またはリセットすることは出来ません。

シード値が必要な場合に Xorshift は手軽な代替手段となるでしょう。

線形合同法

従来用いられていた単純なアルゴリズムです。

Stephen K. Park と Keith W. Miller が、彼らのサーベイ中で「最低基準」として示したもので、より良い選択肢が無いのならば、自作などせずにこれを使うべしというもの。

X_{n+1}=\left(48,271\times X_{n}\right)\ {\bmod {\ }}(2^{31}-1)

4. comp.lang.c FAQ list · Question 13.15 ただしソースコードでは乗数が 16807 になっているので注意すること。48271 を使ったほうが(もしかしたら計算がわずかに重くなるかもしれないが)少し良い乱数列になる。ソースコード中のその数の部分を書き換えるだけでよい。

これを JavaScript に移植しました。シード値の扱いは Xorshift の getNext に合わせています。

let [PMgetSeed, PMrand, PMrandF] = (() => {
  const a = 48271;
  const m = 2147483647;
  const q = Math.floor(m / a);
  const r = m % a;
  let PMgetSeed = () => Math.floor(Math.random() * (m - 1)) + 1;
  let s = Int32Array.of(PMgetSeed(), 0, 0, 0);
  let PMrand = seed => {
    if (seed) s[0] = seed;
    s[1] = s[0] / q;
    s[2] = s[0] % q;
    s[3] = a * s[2] - r * s[1];
    s[0] = s[3] > 0 ? s[3] : s[3] + m;
    return s[0];
  };
  let PMrandF = seed => PMrand(seed) / m;
  return [PMgetSeed, PMrand, PMrandF];
})();

Web Crypto API

MDN には次のように言及があります。

Math.random() の提供する乱数は、暗号に使用可能な安全性を備えていません。セキュリティに関連する目的では使用しないでください。代わりに Web Crypto API(より正確にはwindow.crypto.getRandomValues() メソッド)を使用してください。

こちらも比較に使ってみました。

let cryptoRand = (() => {
  let buf = new Uint32Array(1);
  return () => getFloat(window.crypto.getRandomValues(buf)[0]);
})();

プロット

Canvas にサイズ 1×1 の長方形を描くことで点を打ちます。最初の 20 個の乱数は値を表示します。

function test() {
  result.innerHTML = "";
  let ctx = canvas.getContext("2d");
  let w = canvas.width;
  let h = canvas.height;
  ctx.fillStyle = "white";
  ctx.fillRect(0, 0, w, h);
  ctx.fillStyle = "black";
  let count = parseInt(dots.value);
  let rnd = select.selectedOptions[0].function;
  for (let i = 0; i < count; i++) {
    let rx = rnd(), ry = rnd();
    if (i < 10) { log(rx); log(ry); }
    x = Math.floor(rx * w);
    y = Math.floor(ry * h);
    ctx.fillRect(x, y, 1, 1);
  }
}

この種のプロットについて Wikipedia に言及があります。

線形合同法一般の欠点に、多次元で規則的に分布するという性質がある。線形合同法による乱数列$r _ 0$, $r _ 1$, $r _ 2$, $r _ 3$, ... から($r _ 0$, $r _ 1$), ($r _ 1$, $r _ 2$), ($r _ 2$, $r _ 3$), ... のように順番に割り当ててプロットすると、それが疎になるのはしょうがないのだが(例として、全部で10000個しかない点を、10000x10000 の矩形にプロットすることになるのだから、稠密にはなりえない)、一定の間隔の平面上に点が並んでしまうのが問題である。

今回のプロットは 100×100 と小さかったため、目で見て分かるほどには表面化しませんでした。

参考

出力の log() は次の実装を使っています。

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

MongoDB: jsonの数値データをIntやDecimal128でインポートする

MongoDBで、データをjsonからインポートしたい場合、mongoimportを使用するのが一般的だと思います。

ですが、jsonでは数値のTypeは区別されないため、mongoimportをそのまま使用すると、全てがfloating point numberと扱われてしまうようです。

Schema Validationを定義しており、double以外の数値型を設定している場合、これでは弾かれてしまうので、なんとかする必要があります。

しばらく詰まって辛かったのでメモ書きしておきます。

やり方は色々あるっちゃある

jsonとmongoimportの組み合わせでは、どう頑張っても無理そうなので、回避策を探します。

  1. CSVにする
  2. jsでやる
  3. Schema Validationを無効化して入れて、その後でここのデータを正しいTypeに更新する
  4. BSONでやる <= これを選択

CSVに変換してから

最初、jsonとmongoimportでなんとかできないかと思い、ドキュメントを読んでいたところ見つけたのが、columnsHaveTypesオプションです。

これはどうやら、フィールド毎にTypeを指定できるもののようですが、残念ながらCSVとTSVからのインポートにのみ対応しているオプションのようで、ご丁寧に「json噛ませるとエラーになるからな」と注意書きがなされています。

まぁなら、何かしらの方法でjsonをCSVに変換してからインポートすればいけそうです。(未検証)

jsでスクリプト書く

Mongo Shellはjsを呼べるので、スクリプトを書くのも手です。

ですが、どうやらMongoShellでjsonファイルを直接読み込むのは無理で、jsファイルに変換する必要があるようです。

ref: https://stackoverflow.com/questions/27670996/import-json-with-mongo-script

ちょっと流石にめんどいし何より気持ち悪いのでやめました。

まぁMongoShellを使わず、どっか適当なところにnpmの環境を作って、MongoDBのNode.js Driverとかを使ってやれば、そんなことせずともいけそうです。(未検証、けどこれが一番順当な方法かも)

Schema Validationを無効化

とりあえず入れてしまって、後からTypeを変える方法も考えました。

後から、Bypass Document Validationする方法は色々あるようで、findAndModifyとかが使えれば、サクサク終わりそうと期待しました。

ですが、このfindAndModifyだと、ヒットしたデータのCurrent Valueを、修正時に参照することができなさそうです。

ref: https://stackoverflow.com/questions/3788256/mongodb-updating-documents-using-data-from-the-same-document

つまり、やるなら1つ1つのデータを取得して、そのデータを元に新しいオブジェクトを$setするみたいなスクリプを書く必要があります。

くそ

BSONにしてしまえば良い

よくよく考えれば、jsonだと表現できる幅が限られているからimport時にType情報がなくなるわけで、最初からbsonでインポートすれば問題は解決です。

"bson"というnpmライブラリが公式から出てるので、それを使ってjsonをbsonに変換してから、インポートさせれば良いです。

MongoDBにbsonで直接インポートするには、mongoimportではなくmongorestoreを使います。

jsonからbsonへの変換

npm add bsonしたら、変換するスクリプトを書きます。もちろんtsで書くので、npm add @types/bsonも忘れずにします。

import fs from 'fs';
import { promisify } from 'util';
import BSON from 'bson';

const Int32 = BSON.Int32;
const Double = BSON.Double;

type NewData = {
  code: BSON.Int32;
  price: BSON.Double;
}

const newData: NewData = {
  code: new Int32(9064),
  price: new Double(2500.05)
}

const newData2: NewData = {
  code: new Int32(9984),
  price: new Double(5200)
}

const save = async () => {
  await promisify(fs.writeFile)('path/to/file.bson', BSON.serialize(newData));
  await promisify(fs.appendFile)('path/to/file.bson', BSON.serialize(newData2));
}

save();

中身はこんな感じです。(あくまで参考例レベルですので悪しからず)
ファイルに書き込むときにBSON.serialize()することで、bsonとして正しく保存できます。

複数データを入れる場合の注意:Arrayにしない

const save = async () => {
  await promisify(fs.writeFile)('path/to/file.bson', BSON.serialize([newData, newData2]));
}

最初、このような感じで配列をBSONに書き込んでいました。

しかしその場合、インポートしようとすると1つの配列が1つのデータとして登録されてしまいます。

mongoimportの場合は--jsonArrayオプションがあったので、mongorestoreにも--bsonArray的なオプションがあるのかと思ったのですが、ないです。

この解決方法を探して小一時間さまよっていましたが、結論は「そもそもArrayにする必要がない」です。

ref: https://stackoverflow.com/questions/56585231/how-to-deserialize-dumped-bson-with-arbitrarily-many-documents-in-javascript

Which clearly breaks the JSON syntax. The internal BSON is of similar shape, but it seems BSON allows this kind of multi-object stuffing in one file.

これを知るために結構時間を無駄にした。。。。?

Arrayにせず、直接appendFileで追記していきましょう。

保存はmongoimportではなくmongorestoreで

dumpしたデータの実態はただのbsonファイルなので、mongorestoremongodumpで出力したデータでなくとも、.bsonであれば使用できます。

restore.sh
DB_NAME=$1
COLLECTION_NAME=$2
FILE_PATH=$3

// authはあくまで例
mongorestore \
  --authenticationDatabase admin -u root -p root \ 
  -d $DB_NAME -c $COLLECTION_NAME $FILE_PATH

疲れた ?

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

React hooksで複数の状態を定義する

はじめに

「React hooksを試してみた」という記事の続きです。
今回も個人的なメモレベルです。

hooksで複数の状態を定義する

まぁタイトルのままなのですが、コンポーネントを作る時に、hookを利用して、状態を複数持ちたいというのは、当然要件としてあります。その場合は、複数定義する事は全然問題ありません。

以下に例を示します。

また、この関数コンポーネントは外からpropsを受け取ってます。その際に、defaultPropsで、propsに値が渡されなかった際のデフォルト値を定義しています。

状態を3つ定義する
const App = (props) => {

    const [title, setTitle] = useState(props.title);
    const [author, setAuthor] = useState(props.author);
    const [price, setPrice] = useState(props.price);

    const reset = () => {
        setPrice(1000);
        setAuthor(props.author);
        setTitle(props.title);
    }

    return(
        <>
            <p>
                {author}の新譜が発表されました。
                タイトルは{title}です。<br />

                値段はあたなが自由につけられます。<br />
                いくらで買いますか?<br />
            </p>
            <p>
                {price}円で買います。
            </p>

            <div>
                <button onClick={()=> setPrice(price + 100)}>+100</button>
                <button onClick={()=> setPrice(price - 100)}>+100</button>
                <button onClick={reset}>Reset</button>
            </div>

            <label htmlFor="author">Author</label>
            <input id="author" type="text" value={author} onChange={(e) => setAuthor(e.target.value)}/>

            <label htmlFor="title">Title</label>
            <input id="title" type="text" value={title} onChange={(e) => setTitle(e.target.value)}/>

        </>
    )
}

App.defaultProps = {
    title: '',
    author: '',
    price: 1000
}

export default App;

もう少しリファクタしてみる

useStateで渡す初期値の引数はオブジェクトを渡すことが出来ます。
より抽象的に状態を定義できます。

以下、実装例です。

リファクタ
const App = (props) => {

    const [state, setState] = useState(props);
    const reset = () => {
        setState(props);
    };

    return(
        <>
            <p>
                {state.author}の新譜が発表されました。
                タイトルは{state.title}です。<br />

                値段はあたなが自由につけられます。<br />
                いくらで買いますか?<br />
            </p>
            <p>
                {state.price}円で買います。
            </p>

            <div>
                <button onClick={()=> setState({...state, price: state.price + 100})}>+100</button>
                <button onClick={()=> setState({...state, price: state.price - 100})}>+100</button>
                <button onClick={reset}>Reset</button>
            </div>

            <label htmlFor="author">Author</label>
            <input id="author" type="text" value={state.author} onChange={(e) => setState({...state, author: e.target.value})}/>

            <label htmlFor="title">Title</label>
            <input id="title" type="text" value={state.title} onChange={(e) => setState({...state, title: e.target.value})}/>

        </>
    )
}

App.defaultProps = {
    title: '',
    author: '',
    price: 1000
}

export default App;

基本的には関数コンポーネントで実装をしていく方針でよさそうです。

クラスコンポーネントで実装する機会とかあるとすれば、既存のクラスコンポーネントを継承して機能を拡張するケースや、サードパーティーのクラスコンポーネントを拡張したい時ぐらい?

いずれにせよ早く使いこなせるように精進精進:muscle:

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

ティラノスクリプトのglink選択肢ボタンにキーをマッピング

今作ってるノベルゲームは、選択肢が常に二択になってます。
以下のように。

image.png

それで
『←』キーで左を選択、
『→』キーで右を選択
にしたかったです。

でもボタンがglinkタグで作られていて、キーボードでの操作ができなかったです。
(linkタグだったら基本的にキーボードでの操作ができます)
でもこのゲームもキーボードだけでプレイできるようにしたかったんです。

それで、簡単なトリックを見つけました。
選択肢は以下のように、glink タグで作ってます。

[glink name="choose_btn_a" x=200 y=300 width="300" height="100" ……
[glink name="choose_btn_b" x=680 y=300 width="300" height="100" ……

ボタンのnameタグに固有の値を割り当てます。
左の選択肢にはchoose_btn_aという値を、
右の選択肢にはchoose_btn_bを。

そしてそれをライダーでボタンを分析してみたら、glinkボタンは以下のようなhtmlタグに変換されてました。

<div class=“button_graphic event-setting-element” data-event-tag=“glink" data-event-pm="...

name に指定した値は、classの中に入るんです。
jqueryで操作が可能って事です。
あとはキーを割り当てるだけ。

KeyConfig.jsファイルを開きます。
そこで方向キーの割当ができます。

  • KeyConfig.js

    "37" : function(){
        try{
            $(".choose_btn_a").click();
        }catch(e){
            console.log(e.message);
        }
    },
    "39" : function(){
        try{
            $(".choose_btn_b").click();
        }catch(e){
            console.log(e.message);
        }
    },
    

37が『←』キー、
39が『→』キーです。
そのキーを押すとjqueryでそれぞれのglinkにマウスクリックイベントを発生させるわけです。
以上です。簡単です。

でももし後でティラノスクリプトのバージョンアップによりglinkのnameタグの実装が変わったら、このjqueryは動かなくなるかも知れませんから、一応try-catchで防御しました。
(ちなみに僕が使ってるティラノスクリプトはv461fです)

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

初心者によるプログラミング学習ログ 240日目

100日チャレンジの240日目

twitterの100日チャレンジ#タグ、#100DaysOfCode実施中です。
すでに100日超えましたが、継続。

100日チャレンジは、ぱぺまぺの中ではプログラミングに限らず継続学習のために使っています。

240日目は、(↓は239になってますが、240です)

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

kintoneUIComponentでボタンを設置する

kintoneUIComponentを使ってボタンを設置する

英語ドキュメントしか出てこなかったので、参考までに書いておく。なお、作ったのはタイムカードアプリなので、サンプルの定義はそれらしい記述になっているのでご了承を。

大前提

kintoneUIComponentのライブラリはアプリに読み込んでおくこと。

ボタン定義

まず、ボタンの定義をする。ボタンに表示するテキストと、ボタンのタイプ、submitとnormalを選べる。

kintoneui.js
        // 開始打刻ボタン定義
        var restStartButton = new kintoneUIComponent.Button({
           text: '休憩開始',
           type: 'submit'
        });

ボタンを設置する

レコード詳細画面で、ボタンを設置したい場所に「スペース」を定義して、その上で上記で定義したボタンを設置する。getSpaceElementで取っているのが、スペースのID。

kintoneui.js
        // 開始打刻ボタン定義
        var restStartButton = new kintoneUIComponent.Button({
           text: '休憩開始',
           type: 'submit'
        });
        // 開始打刻ボタン設置
        let elStartButton = kintone.app.record.getSpaceElement('restStart');
        elStartButton.appendChild(restStartButton.render());

ボタンを押下した時の挙動を定義する

ボタンの場合は「押下(クリック)」した時の動作を定義する。ちなみに別関数を呼び出しているだけ。

kintoneui.js
        // 開始打刻ボタン定義
        var restStartButton = new kintoneUIComponent.Button({
           text: '休憩開始',
           type: 'submit'
        });
        // 開始打刻ボタン設置
        let elStartButton = kintone.app.record.getSpaceElement('restStart');
        elStartButton.appendChild(restStartButton.render());
        // 開始打刻
        restStartButton.on('click', function(){
            setRestStartTime();
        });


参考までに、呼び出し先はこんな感じ。

kintoneui.js
    // 休憩開始打刻
    function setRestStartTime(){
        var record = kintone.app.record.get();
        record['record']['休憩開始']['value'] = moment().format('YYYY-MM-DD HH:mm');
        kintone.app.record.set(record);
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Puppeteer Tips

Puppeteer?

読み方は「ぱぺてぃあ」。

Node.jsのライブラリでChromeを操作しDOMの要素を取得、ステータスコードを取得、レスポンスタイムを計測・・等々できます。
※Chromeのデベロッパーツールで見れる情報は(たぶん)全てpuppeteerで取得できる

Sample

「サイト内の各ページのタイトルが予測した値になっているか?」という自動テストをPuppeteerを利用して処理してみます。

大まかな流れは下記のようになります。

1. テストデータをCSVファイルから読み込み
2. 1行ずつループし、取得した値と予測した値を比較
3. 結果表示

Code

https://github.com/yusukeito58/puppeteer-template

$ tree -I node_modules
.
├── package.json
└── src
    ├── data
    │   └── title.test.csv
    ├── lib
    │   └── output.js
    └── test
        └── title          // テスト・処理内容応じてにディレクトリを切って、実行ファイル(index.js)と説明ファイル(README.md)を置くといい感じ
            ├── README.md  // テスト概要や実行方法を記載
            └── index.js   // 実行ファイル

index.js
const puppeteer = require('puppeteer');
const papa = require('papaparse');
const assert = require('assert');
const fs = require('fs');

const root = '../../'
const { showTestStart, showResult  } = require(root + 'lib/output');

// メイン処理
(async () => {
  console.time('Processing time');

  // テスト対象のサイト
  const domain = 'https://www.google.com';

  // テスト対象のデータ ※ Listを直接コードに書く、CSVから読み込む etc...
  file = fs.readFileSync(root + 'data/title.test.csv', 'utf8')
  dataList = papa.parse(file, {
    header: true,
    skipEmptyLines: true
  }).data;

  // カウンタ初期化
  let count = 0;
  // エラー一覧
  let errorList = [];

  // ブラウザ起動
  const browser = await puppeteer.launch();
  for (const data of dataList) {
    count += 1;

    // アクセス先のURLを生成
    const url = domain + data.path;

    // 進捗を表示
    showTestStart(url, count, dataList);

    // ページ生成
    const page = await browser.newPage();

    // JSやCSSの読み込みを無視
    await page.setRequestInterception(true);
    page.on('request', (interceptedRequest) => {
      if (url === interceptedRequest.url()) {
        interceptedRequest.continue();
      } else {
        interceptedRequest.abort();
      }
    });

    // テスト対象のURLにアクセス(返り値にresponseが返る)
    await page.goto(url);

    // ページタイトル取得
    const title = await page.title();

    try {
      // 予期された結果と比較
      assert.equal(title, data.title);
      console.log('' + 'Expected result');
    } catch (err) {
      console.log('' + 'Unexpected result');
      console.log(err.message);

      errorList.push(err.message);
    }
    console.log('\n');

    // ページ閉じる
    await page.close();
  }

  // ブラウザ閉じる
  await browser.close()

  showResult(errorList);
  console.timeEnd('Processing time');
})();

lib/output.js
exports.showTestStart = (currentUrl, index, urls) => {
  const color = '\u001b[44m\u001b[37m';
  const reset = '\u001b[0m';

  console.log(`${color} ?  ${currentUrl} | ${index} / ${urls.length} ${reset}`);
};

exports.showResult = (errorList) => {
  let msg = '';
  if (errorList.length === 0) {
    msg = '✅  ???Congratulation for passing!!???';
  } else {
    msg = '❌  Failed the test...?';
  }

  console.log('\n' + msg + '\n');
};

data/title.test.csv
url,title
/,Google
/search/howsearchworks/,Google Search - Discover How Google Search Works
package.json
{
  "name": "puppeteer-template",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "directories": {
    "test": "test"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "yusukeito58",
  "license": "MIT",
  "dependencies": {
    "assert": "^2.0.0",
    "papaparse": "^5.1.1",
    "puppeteer": "^2.1.1"
  }
}

実行

サンプルで用意したプログラムを実行してみます。

$ git clone https://github.com/yusukeito58/puppeteer-template.git

$ npm i
$ cd src/test/title

# 実行
$ node index.js

結果

うまくいくと下記のように表示されます。
スクリーンショット 2020-02-15 19.37.17.png

Tips

個人的に利用頻度が多い処理をまとめておきます。

特定要素の有無を判定

例. 検索ボタンがあるか確認

// 要素を取得
const hasElement = async (page, selector) => {
  const item = await page.$(); // selectorを引数で受け取るようにするともっと汎用的に使える
  if (item) {
    return true;
  } else {
    return false;
  }
};

(async () => {   
  for (...) {
    :
    const selector = 'center > input.gNO89b';
    ret = await hasElement(page, selector);
    :
  }
}();

UserAgentを指定

例.デバイスを「iPhone X」に設定

const devices = require('puppeteer/DeviceDescriptors');
// テストデバイス
const device = devices['iPhone X'];

(async () => {   
  for (...) {
    :
    // デバイス設定
    await page.emulate(device);
    :
  }
}();

JSやCSSなどの読み込みを無視

処理速度が数倍違ってきます。

(async () => {   
  for (...) {
    :
    await page.setRequestInterception(true);

    page.on('request', (interceptedRequest) => {
    if (url === interceptedRequest.url()) {
        interceptedRequest.continue();
    } else {
        interceptedRequest.abort();
    }
    });
    :
  }
}();

テストデータをCSVファイルから取得​

url,title
/,Google
/search/howsearchworks/,Google Search - Discover How Google Search Works
const papa = require('papaparse');
const fs = require('fs');
:

(async () => { 
  :
  // テストデータ取得
  file = fs.readFileSync(root + 'data/title.test.csv', 'utf8')
  dataList = papa.parse(file, {
    header: true,
    skipEmptyLines: true
  }).data;
  :
)();

非同期に対象ページにアクセス​

データが大量にあった場合、直列的に対象ページにアクセスすると処理時間を要するため、非同期に同時処理すると処理時間が短縮されます。

安定しないケースもあるので、検証が必要かも知れません。(もっと良い方法ありそう)

例. 大量の対象URLが全て正常(ステータスコードが200である)か確認する

// ステータスコードを取得
const getStatusCode = (browser, url) => {
  return new Promise(async (resolve) => {
    const page = await browser.newPage();

    await page.setDefaultNavigationTimeout(0);

    await page.setRequestInterception(true);
    page.on('request', (interceptedRequest) => {
      if (url === interceptedRequest.url()) {
        interceptedRequest.continue();
      } else {
        interceptedRequest.abort();
      }
    });

    const response = await page.goto(url);

    // ステータスコードを返却(true:200台)
    const result = {
      'url': url,
      'status': response.ok()
    }

    await page.close();

    resolve(result);
  })
}

:

(async () => { 
  :
  for (let url of urls) {
    allResponse.push(getStatusCode(browser, url))  // あまり大量だとPCがが唸る・・・
  }

  errors = await Promise.all(allResponse) // 非同期実行
    .then(results => {
       // エラーとなった情報だけにフィルタリング
       return results.filter(result => !result.status)
  })
  :
)();

配列を〇〇個ごとに分割

Puppeteerから少し外れますが、前項の非同期処理を行う前処理です。同時実行する個数に調整する際に利用します。

const divideArrIntoPieces = (arr, n) => {
  let arrList = [];
  let idx = 0;
  while(idx < arr.length){
    arrList.push(arr.splice(idx, idx + n));
  }
  return arrList;
}

(async () => {
  :
  urlList = divideArrIntoPieces(allUrl, 10);

  for (let urls of urlList) {
    for (let url of urls) {
      :
    }
  }
}();

画像を保存​

// ダウンロード対象
imgUrl = 'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png';

// ローカルの保存先
const imgPath = './images/image.jpg';

// 画像ダウンロード
const viewSource = await page.goto(imgUrl);

// ローカルに保存
fs.writeFileSync(imgPath, await viewSource.buffer());

参考​

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

Lodashの備忘録

Lodashとは?

便利関数をまとめたJavaScriptライブラリです。
配列処理や単なる値の操作だけでなく、イベント処理を制限したりなど様々な関数があります。
ドキュメントを眺めていると面白い関数がたくさんあったので、気になった関数たちを紹介していきます。

_xor

  • 配列の同じ値をまとめて、新しい配列を返す
const arr2 = [2,3,4,4]
  const arr=[...arr2]

  const _arr = _.xor(arr)

  console.log(_arr)
// [2,3,4]

_has

  • 引数に渡したオブジェクトのキーを検索し、キーが存在すればtrueを返す
var object = { 'a': { 'b': 2 } };
_.has(object, 'a');
// => true

_sortBy

  • 第一引数に対象の配列、第二引数にソートしたいkey名

faker.js というダミーデータを作成してくれるライブラリを使用しています。

const targets: any[] = [];

for (let i = 1; i < 1500; i++) {
  targets.push({
    name: faker.company.companyName(),
    owner: faker.name.firstName()
  });
}
const res = _.sortBy(targets, ['name','owner']);

console.log(res);
// name,ownerの昇順で並び替えられる

_compact

  • falsyな値を取り除き、新しい配列を返す
const target = [0, 1, false, 2, '', 3]
const res = _.compact(target);

console.log(res)

// [1,2,3]

_flatten

  • 入れ子の配列を一階層なくす
const target = [['hello'],'world']
const res = _.flatten(target);

console.log(target)

// ["hello", "world"]

_pull

  • 第一引数に対象の配列、第二引数に取り除きたい値を渡し、配列を返す
for (let i = 1; i < 10; i++) {
  targets.push('トマト');
}

const _res = _.pull(targets, "トマト");

console.log(_res);

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

【Rails】リロードしないとJavaScriptが動かない!【簡単に解決】

はじめに

以前jQueryを使ってタブメニューの切り替えを実装したときに、
ビューを開いた初回は非同期で切り替わるのに、
別のページに移動したり
「戻る」ボタンを押したあと再びタブメニュー画面に戻ると、、、、

動かない、、、!なぜだ、、!

なんて経験をして1日無駄にしたことがあります。
jQueryやビュー画面のコードは間違ってないはず!なぜ!って人向けの記事です。

解決方法①

link_toごとにturbolinksとやらを無効にすればOKです。簡単です。

<%= link_to "ホーム", root_path, data: {"turbolinks" => false} %>

data: {“turbolinks" => false}は遷移先ページのみturbolinksを切ることが出来ます。

turbolinksとは、ページ遷移をAjaxに置き換え、JavaScriptCSSのパース(解析・変換)を省略することで高速化するgemで、Rails4からはデフォルトで使用されています。↓

gem 'turbolinks', '~> 5'

解決方法②

jsファイルに"turbolinks:load"を記述すればOKです。簡単です。

document.addEventListener("turbolinks:load", function() {
  // ...
})

まとめ

turbolinksを無効化する方法として、gemファイルから直接削除する方法もあるようですが、
元々turbolinksはページの読み込みを高速化するgemです。

ボタンを押すたびに画面が初期化され、
一旦真っさらになった後、再度jsやらcssを読み直す。。。待ってられません。

その辺の処理をTurbolinks5が全部やってくれているんです。

なるべく
①個別のページ(link_toごと)に無効にするか、
②jsファイルにコードを加えるだけにしましょう。

参考
https://qiita.com/suzy1031/items/d3a8e96fe4d31111d22e

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

Aureliaの環境構築メモ

Aureliaってなに

知らないよー昨日飲み会で布教されたばっかりだもん(白目
取り合えずは、現存色々あるReactやVueと違ってあまり主張のないframeworkっていう地位を築いているようでした。

その発想自体は個人的にどストライクだー( ͡° ͜ʖ ͡°)
とおもって、早速いじってみようとおもいry。。。

なんと、日本ではまだあまり布教されていない&2015年の記事多すぎわロタだったので最新の記事はほとんど英語のものばかりですた!!!(しかも出てきた記事のトップが昨日飲んだ方の愛のこもったmidiumだった笑)

ということで、一旦あれですね・・・。

最新の環境構築方法だけ載せておくね!!!!!笑

環境

$ npm -v
6.13.7

$ yarn -v
1.17.3

構築順序

1.Aureliaのコマンドを使えるようにする

まずはAureliaのコマンドを入れないとお戯れできないので、早速入れていきましょう

$ npm install -g aurelia-cli

以下を叩いてバージョンが出ればOK

$ npm au -v
6.13.7

2.Aureliaをインストール

auが使えるようになったら、Aureliaをインストールしたい階層まで移動して、以下を叩きます

$ npx au new

「YOU、プロジェクト名何にしたいの?」って聞かれてます。
任意の名前入れましょう

Please enter a name for your new project: aurelia-app

「どんな感じで開発する?」って聞かれてます。
個人的趣味で今回はtypeScriptを使う選択をしました。

Would you like to use the default setup or customize your choices?

Default TypeScript App

3.Aureliaを起動

早速起動してみませう
インストールしたファイル配下に移動し、以下を叩きましょう

$ yarn au run --open

ブラウザが開き、以下が表示されたら成功。
地味だなぁ・・・。
ロゴくらいは入れて欲しいお気持ちだった。
スクリーンショット 2020-02-15 17.21.16.png

まとめ

英語アレルギーだから環境構築だけで満足した。
もし興味がある方はいじってみてくださいーーーーー!

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

JavaScriptの継承について確認

概要

昔、勉強したが、知識が曖昧になってきているので再度調べてみる。
( Backbone.jsの学習時に登場したため... )

サンプルコード

JavaScriptの継承を行ったサンプルを作ってみる。

./src/html/extend.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>JavaScriptの継承について</title>
</head>
<body>
    <h1>prototypeを使った継承</h1>
    <p>オブジェクトを継承してみる。</p>

    <script>
      // Animalオブジェクト
      function Animal(name) {
        this.name = name;
      }
      Animal.prototype.talk = function() {
        console.log(this.phrase);
      };

      // Animal2オブジェクト (static変数あり)
      var Animal2 = (function() {
        var uid = 0;
        return function () {
          this.uid = uid;
          uid++;
        };
      })();

      // -------------------------------------------------------------------
      // オブジェクトの継承

      (function() {
        // Dogオブジェクト (Animalを継承する)
        function Dog(phrase) {
          this.constructor('Dog');
          this.phrase = phrase;
        }
        Dog.prototype = new Animal(); // prototype, constructorのコピー (talk関数)
        var myDog = new Dog('Hello World.');
        myDog.talk();            // → Hello World.
        console.log(myDog);      // → Dog {name: "Dog", phrase: "Hello World."}
      })();

      (function() {
        // Dog2オブジェクト (Animal2を継承する)
        function Dog2() {
          this.constructor();
        }
        Dog2.prototype = new Animal2(); // prototype, constructorのコピー (talk関数)
        console.log('dog2(1)', new Dog2()); // dog2(1) Dog2 {uid: 1}
        console.log('dog2(2)', new Dog2()); // dog2(2) Dog2 {uid: 2}
        console.log('dog2(3)', new Dog2()); // dog2(3) Dog2 {uid: 3}
      })();

      // -------------------------------------------------------------------
      // Object.createメソッド

      (function() {
        function Dog (phrase) {
          Animal.call(this, 'Dog');
          this.phrase = phrase;
        }
        Dog.prototype = Object.create(Animal.prototype);
        Dog.prototype.constructor = Dog;

        var myDog = new Dog('Hello World.');
        myDog.talk();            // → Hello World.
        console.log(myDog);      // → Dog {name: "Dog", phrase: "Hello World."}
      })();

      (function() {
        // Dog2オブジェクト (Animal2を継承する)
        function Dog2() {
          Animal2.call(this);
        }
        Dog2.prototype = new Animal2();
        console.log('dog2(1)', new Dog2()); // dog2(1) Dog2 {uid: 4}
        console.log('dog2(2)', new Dog2()); // dog2(2) Dog2 {uid: 5}
        console.log('dog2(3)', new Dog2()); // dog2(3) Dog2 {uid: 6}
      })();
      // -------------------------------------------------------------------
    </script>
</body>
</html>

動作確認

どちらの方法でも継承できているようです。
※ chromeでの確認
sample.png

まとめ

古い内容になり、現在では書くことがほとんどないと思いますが、動きを確認するため、書いてみました。

参考サイト

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

Javascript (GoogleAppsScript) の入れ子になっている Object をすべて Map に変換する

概要

WebAPI などから取得した JSON を JSON.parse() した Object など、値部分にさらに Object が格納されているような場合に、入れ子部分もすべて Map オブジェクトに変換する。
イテラブルなオブジェクトとして処理したい場合に有効。
(GoogleAppsScript では V8 ランタイム使用時のみ可能)

コード

以下ようなのクラスを作成した。

class objectToMap {

  constructor(obj) {

    this.object = obj;
    this.map    = this.objToMapProc(obj);

  }

  objToMapProc(obj) {

    let resMap = new Map();

    for (const key in obj) {

      const objValue = obj[key];
      const dataType = typeof objValue;
      let setMapArr  = [];

      if (dataType === 'object') {

        const setMapValue = this.objToMapProc(objValue);
        setMapArr         = [key, setMapValue];

      }else{

        setMapArr = [key, objValue];

      }

      resMap.set(...setMapArr);

    }

    return resMap;

  }

}

使用方法

function testJsonToMap(){

  const json      = '{"test1": "text1", "test2": {"test2_1": "text2"}, "test3": {"test3_1": {"test3_1_1": "text3"}}}';
  const jsonObj   = JSON.parse(json);
  const jsonToMap = new objectToMap(jsonObj);
  const map       = jsonToMap.map;

  console.log(map.get('test3').get('test3_1').get('test3_1_1')); //'text3'

}

このように、入れ子部分も Map オブジェクトに変換される。

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

JavaScript (GoogleAppsScript) の入れ子になっている Object をすべて Map に変換する

概要

WebAPI などから取得した JSON を JSON.parse() した Object など、値部分にさらに Object が格納されているような場合に、入れ子部分もすべて Map オブジェクトに変換する。
イテラブルなオブジェクトとして処理したい場合に有効。
(GoogleAppsScript では V8 ランタイム使用時のみ可能)

コード

以下ようなのクラスを作成した。

class objectToMap {

  constructor(obj) {

    this.object = obj;
    this.map    = this.objToMapProc(obj);

  }

  objToMapProc(obj) {

    let resMap = new Map();

    for (const key in obj) {

      const objValue = obj[key];
      const dataType = typeof objValue;
      let setMapArr  = [];

      if (dataType === 'object') {

        const setMapValue = this.objToMapProc(objValue);
        setMapArr.push(key, setMapValue);

      }else{

        setMapArr.push(key, objValue);

      }

      resMap.set(...setMapArr);

    }

    return resMap;

  }

}

使用方法

function testJsonToMap(){

  const json      = '{"test1": "text1", "test2": {"test2_1": "text2"}, "test3": {"test3_1": {"test3_1_1": "text3"}}}';
  const jsonObj   = JSON.parse(json);
  const jsonToMap = new objectToMap(jsonObj);
  const map       = jsonToMap.map;

  console.log(map.get('test3').get('test3_1').get('test3_1_1')); //'text3'

}

このように、入れ子部分も Map オブジェクトに変換される。

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

Backbone.js - コレクション

概要

Collectionで任意のリストを作り操作してみる

ローカル環境準備

dockerでnginxを起動して環境を整える。

ターミナル
$ docker-compose build
$ docker-compose up -d

※ 学習ように作ったコードはgithubにアップしています
https://github.com/reflet/sample-backbone.js

サンプルコード

Ajax通信などせず、任意にリストを生成して制御してみる。

./src/html/collection2.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>Backbone.js学習 [Collection]</title>
</head>
<body>
    <h1>Collection</h1>
    <p>Collectionを使ってみる</p>

    <script src="https://cdn.jsdelivr.net/npm/underscore@1.9.1/underscore.min.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/jquery/jquery@3.4.1/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/backbone@1.4.0/backbone.min.js"></script>
    <script>
        // Model
        const Person = Backbone.Model.extend({
            defaults: {
                country: '日本',
                prefecture: '東京',
                name: '',
                age: null,
            }
        });

        // Collection
        const Users = Backbone.Collection.extend({model: Person});
        const users = new Users();
        users.add({name:'次郎', age:28});
        users.add({name:'花子', age:25, prefecture:'大阪'});
        users.add({name:'三郎', age:32});
        users.add({name:'田中', age:25, prefecture:'名古屋'});
        console.log('dump1', users.toJSON());

        // 名前だけ取得する
        console.log(users.pluck('name'));

        // 全件取得する
        users.each(function(user) {
            console.log(user.toJSON());
        });

        // 年齢が25歳のユーザーのみ取得する
        $.each(users.where({age:25}), function(e,user) {
            console.log(user.toJSON());
        });

        // 最後のデータ要素を削除する
        users.pop();
        console.log('dump2', users.toJSON());

        // 指定した要素を削除する
        users.remove(users.at(1)); // 2番目
        console.log('dump3', users.toJSON());

        //すべてのデータを削除する
        users.reset();
        console.log('dump4', users.toJSON());
    </script>
</body>
</html>

動作確認

正常に処理できているようです。
sample.png

まとめ

1つのデータは、Modelで取り扱い、データの集合としてCollectionがあるといった感じで
割とシンプルな構成でわかりやすいですね。

参考サイト

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

Backbone.js - コレクション (Ajax通信)

概要

Backbone.jsのコレクションでAjax通信してリストを動的に取得してみる。

ローカル環境整備

dockerでnginxを起動して環境を整える。

ターミナル
$ docker-compose build
$ docker-compose up -d

※ 学習ように作ったコードはgithubにアップしています
https://github.com/reflet/sample-backbone.js

Ajax通信の準備

Collectionで扱うデータをAjaxで非同期通信にて取得するため、jsonファイルを準備する。

./src/html/api/users.json
[
  {"country": "日本", "prefecture": "東京都", "name": "太郎", "age": "30"},
  {"country": "日本", "prefecture": "大阪府", "name": "次郎", "age": "28"},
  {"country": "日本", "prefecture": "愛知県", "name": "花子", "age": "25"},
  {"country": "日本", "prefecture": "福岡県", "name": "三郎", "age": "15"},
  {"country": "日本", "prefecture": "東京都", "name": "四郎", "age": "25"}
]

サンプルコード

Ajaxで動的にデータを取得するCollectionを作成してみました。
※ データの取得先: GET http://localhost:8080/api/users.json

./src/html/collection1.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>Backbone.js学習 [Collection]</title>
</head>
<body>
    <h1>Collection</h1>
    <p>CollectionでAjax通信して動的にデータを取得する</p>

    <script src="https://cdn.jsdelivr.net/npm/underscore@1.9.1/underscore.min.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/jquery/jquery@3.4.1/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/backbone@1.4.0/backbone.min.js"></script>
    <script>
        // Model
        const Person = Backbone.Model.extend({
            defaults: {
                country: '日本',
                prefecture: '東京',
                name: '',
                age: null,
            }
        });

        // Collection
        const Users = Backbone.Collection.extend({
            model: Person,
            url: '/api/users.json',
            parse: function(response){
                console.log(response);
                return response;
            }
        });
        const users = new Users();
        users.fetch().done(function(users){
            $.each(users, function(index, user){
                console.log(index, user);
            });
        });
    </script>
</body>
</html>

動作確認

正常にデータが取得できたようです。
sample.png

参考サイト

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

Backbone.js - モデル(Validate)

概要

Backbone.jsのモデルで入力検証のやり方を調べてみる

ローカル環境準備

dockerでnginxを起動して環境を整える。

ターミナル
$ docker-compose build
$ docker-compose up -d

※ 学習ように作ったコードはgithubにアップしています
https://github.com/reflet/sample-backbone.js

サンプルコード

Modelで入力検証するサンプルを作ってみる。

./src/html/model2-validate.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>Backbone.js学習 [Model]</title>
</head>
<body>
    <h1>Model - 入力検証</h1>
    <p>モデルで入力検証をやってみる</p>

    <script src="https://cdn.jsdelivr.net/npm/underscore@1.9.1/underscore.min.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/jquery/jquery@3.4.1/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/backbone@1.4.0/backbone.min.js"></script>
    <script>
        const Person = Backbone.Model.extend({
            initialize: function (attrs, options) {
                console.log('initialize', attrs, options);
            },
            validate: function (attrs) {
                console.log('validate', attrs);

                var errors = [];
                if (_.isEmpty(attrs.name)) {
                    errors.push({name: 'name', message: '名前が入力されていません'});
                }
                if (_.isEmpty(attrs.age)) {
                    errors.push({name: 'age', message: '年齢が入力されていません'});
                }
                return errors.length > 0 ? errors : false;
            }
        });

        const hanako = new Person();
        hanako
            .bind('invalid', function (model, error) {
                console.log('invalid', error);
            })
            .bind('change', function(){
                console.log('change');
            })
            .bind('change:name', function(){
                console.log("change:name");
            });

        var check = hanako.set({name: '', age: ''}, {validate: true}); // validate option required!!
        if (check !== false) {
            console.log('success', hanako.toJSON());
        } else {
            console.log('error', hanako.toJSON());
        }
    </script>
</body>
</html>

動作確認
正常に入力検証ができたようです。
sample.png

まとめ

下記のように個別にデータをセットしてvalidate実行したかったが、うまく行かず断念。

サンプル
hanako.set('name', '');
hanako.set('age', '');
if (hanako.validate === true) {
    console.log('success', hanako.toJSON());
} else {
    console.log('error', hanako.errors);
}

validateionなどのプラグインなどを追加したらできそうな感じがするが、
今回は標準機能を学習するためここではやらないことにします。

参考サイト

以上

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

Backbone.js - モデルでAjax通信してみる

概要

Backbone.jsのモデルを使ってみる

ローカル環境整備

dockerでnginxを起動して環境を整える。

ターミナル
$ docker-compose build
$ docker-compose up -d

※ 学習ように作ったコードはgithubにアップしています
https://github.com/reflet/sample-backbone.js

AJAX通信の準備

Modeで扱うデータをAjaxで非同期通信にて取得するため、jsonファイルを準備する。

./src/html/api/user/1.json
{"country": "日本", "prefecture": "東京都", "name": "太郎", "age": "30"}

サンプルコード

Ajaxで動的にデータを取得するModelを作成してみました。
※ データの取得先: GET http://localhost:8080/api/user/1.json

./src/html/model1.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>Backbone.js学習 [Model]</title>
</head>
<body>
    <h1>Model</h1>
    <p>ModelでAjax通信して動的にデータを取得する</p>
    <script src="https://cdn.jsdelivr.net/npm/underscore@1.9.1/underscore.min.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/jquery/jquery@3.4.1/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/backbone@1.4.0/backbone.min.js"></script>
    <script>
        const Person = Backbone.Model.extend({
            urlRoot: '/api/user',
            url: function () {
                var url = Backbone.Model.prototype.url.apply(this);
                return (this.isNew()) ? url : url + '.json';
            },
            parse: function (response) {
                response.datetime = new Date(response.datetime);
                return response;
            }
        });

        var user = new Person({id: 1});
        console.log(user.url()); // /api/user/1.json

        // Ajax通信でデータを取得する
        // ※ GET /api/user/1.json
        user.fetch()
            .done(function(user){
                console.log(user);
            });

        // Ajax通信でデータを取得する
        // ※ GET /api/user/1.json?limit=5&offset=0
        user.fetch({data:{limit: 5, offset: 0}})
            .done(function(user){
                console.log(user);
            });
    </script>
</body>
</html>

動作確認

正常にデータが取得できたようです。
asmple.png

まとめ

Ajax通信する際、IDを自動でURLに追加してくれるのはいいのですが、.jsonという拡張子を追加したくてちょっと時間がかかりました。

めぼしいサンプルがネットで見つからなかったため、backbone.jsのコードを確認して実装してみました。

参考サイト

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

Backbone.js - モデル(Ajax通信)

概要

Backbone.jsのモデルを使ってみる

ローカル環境整備

dockerでnginxを起動して環境を整える。

ターミナル
$ docker-compose build
$ docker-compose up -d

※ 学習ように作ったコードはgithubにアップしています
https://github.com/reflet/sample-backbone.js

AJAX通信の準備

Modeで扱うデータをAjaxで非同期通信にて取得するため、jsonファイルを準備する。

./src/html/api/user/1.json
{"country": "日本", "prefecture": "東京都", "name": "太郎", "age": "30"}

サンプルコード

Ajaxで動的にデータを取得するModelを作成してみました。
※ データの取得先: GET http://localhost:8080/api/user/1.json

./src/html/model1.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>Backbone.js学習 [Model]</title>
</head>
<body>
    <h1>Model</h1>
    <p>ModelでAjax通信して動的にデータを取得する</p>
    <script src="https://cdn.jsdelivr.net/npm/underscore@1.9.1/underscore.min.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/jquery/jquery@3.4.1/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/backbone@1.4.0/backbone.min.js"></script>
    <script>
        const Person = Backbone.Model.extend({
            urlRoot: '/api/user',
            url: function () {
                var url = Backbone.Model.prototype.url.apply(this);
                return (this.isNew()) ? url : url + '.json';
            },
            parse: function (response) {
                response.datetime = new Date(response.datetime);
                return response;
            }
        });

        var user = new Person({id: 1});
        console.log(user.url()); // /api/user/1.json

        // Ajax通信でデータを取得する
        // ※ GET /api/user/1.json
        user.fetch()
            .done(function(user){
                console.log(user);
            });

        // Ajax通信でデータを取得する ← GETパラメータを追加してみる
        // ※ GET /api/user/1.json?limit=5&offset=0
        user.fetch({data:{limit: 5, offset: 0}})
            .done(function(user){
                console.log(user);
            });
    </script>
</body>
</html>

動作確認

正常にデータが取得できたようです。
asmple.png

まとめ

Ajax通信する際、IDを自動でURLに追加してくれるのはいい。
しかし、.jsonという拡張子をリクエストするURLに追加したくて調べるたがなかなか見つからず...

そのため、backbone.jsのurlのプログラムを参照して上書きする形で書いてみる。

参考サイト

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

backbone.js - 環境準備

概要

普段は、vue.jsなどを使っていますが、backbone.jsを使うことがあったので、学習してみる。

ローカル環境整備

dockerでnginxを起動して環境を整える。

ターミナル
$ docker-compose build
$ docker-compose up -d

※ 学習ように作ったコードはgithubにアップしています
https://github.com/reflet/sample-backbone.js

サンプルコード

とりあえず、必要なjsを読み込んで、動かしてみました。

./src/html/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>Backbone.js学習</title>
</head>
<body>
    <h1>TOP Page</h1>
    <p>ひとまず動く環境を整えてみる。</p>
    <script src="https://cdn.jsdelivr.net/npm/underscore@1.9.1/underscore.min.js"></script>
    <script src="https://cdn.jsdelivr.net/gh/jquery/jquery@3.4.1/dist/jquery.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/backbone@1.4.0/backbone.min.js"></script>
    <script>
        const Person = Backbone.Model.extend({
            defaults: {
                country: '日本',
                prefecture: '東京',
            },
            hello: function() {
                console.log('こんにちは、' + this.get('name') + 'さん');
            }
        });
        const user = new Person({name: '太郎', age: 30});

        // データ確認
        console.log(user.toJSON()); // {country: "日本", prefecture: "東京", name: "太郎", age: 30}

        // 名前を取得する
        console.log(user.get('name'));

        // 独自定義した関数を実行してみる
        user.hello();
    </script>
</body>
</html>

動作確認

正常に処理され、コンソールに結果が表示されたようです。
sample.png

まとめ

underscore.min.js, jquery.min.js, backbone.min.js を読み込めば使える点は、vue.jsと同じでお手軽で導入コストは低そうですね。

参考サイト

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

webpack入門して仲良くなりたい~Loader編~

仲良くなりたい

基本的なバンドルの方法と開発がラクになるように設定を行ったので、次はLoadersと仲良くなりたいと思います。
webpackの神髄はloaderらしいです

Loaderってなに?

Loaderは、jsファイル以外のものをバンドルするための機能です。
役目はjsファイルとして読み取れる形に変換すること

ここでは、cssファイル・画像ファイルとTypeScriptのコンパイルついて扱います。

cssファイルをバンドルする

cssファイルをバンドルするには2つのloaderを使います。

  • css-loader
    • cssファイルの読み込み  
  • style-loader
    • 読み込んだcssファイルをstyleとして反映させる

css-loaderとstyle-loader

まずはcss-loaderstyle-loaderをインストールしてwebpack.config.jsの設定を追加します。

npm install css-loader@3.4.2 style-loader@1.1.3 -D
webpack.config.js
// loaderの設定
module.exports = {
//   .
//   .
//   .
  rules: [
    {
      // 適用するファイルの拡張子を正規表現で記述
      test: /\.css$/,
      // 使用するloaderを指定
      use: ["style-loader", "css-loader"]
    }
  ]
}
  • rules
    • 今後、loaderの設定をするときはこのrules配列の中で行う
  • test
    • ここでは、loaderを適用するファイルの拡張子を正規表現で記述
  • use
    • 使うloaderを記述。loaderは実行される順番が大切です。cssファイルが変換していないのにstyleに適用することはできません。実行順番は後ろからです。注意します。

次にcssファイルを用意します。

src/css/style.css
.back {
  background-color: #d2d2d2;
}

app.jsをわかりやすくします

src/js/app.js
import "../css/style.css";

document.body.classList.add("back");

これで準備完了!
実行して画面を確認してみます。

image.png

背景が灰色になりました。さらに、開発者ツールを確認すると<style>タグにスタイルが反映されています。

画像ファイルをバンドルする

url-loader

次は画像ファイルをバンドルします。
そのために必要なloaderはurl-loaderです

npm install url-loader@3.0.0 -D

次にwebpack.config.jsの設定を加えます。loaderの設定なのでrules配列の中に記述します。

webpack.config.js
 {
     test: /\.(png|jpg|gif)$/,
     loader: "url-loader"
 }

設定を書いたら、自分の好きなところに画像ファイルを入れてapp.jsを書き換えます

app.js
import "../css/style.css";
import backImage from "../images/back-image.jpg";

document.body.classList.add("back");

const image = new Image();
image.src = backImage;
document.body.appendChild(image);

image.png

画像ファイルがurlの形で組み込まれています。

file-loader

もし、この画像ファイルの容量が大きかった時にindex.htmlに埋め込むのは得策ではありません。
そこでfile-loaderを使ってこの問題を解決します。

まずは、file-loaderを入れます

npm install file-loader@5.0.2 -D

次にwebpack.config.jsで設定を追加します。

webpack.config.js
// loaderの設定
  module: {
    rules: [
      {
        // 適用するファイルの拡張子を正規表現で記述
        test: /\.css$/,
        // 使用するloaderを指定
        use: ["style-loader", "css-loader"]
      },
        //ここを追加
      {
        // 適用するファイルの拡張子を正規表現で記述
        test: /\.(png|jpg|gif)$/,
        // 使用するloaderを指定
        loader: "file-loader",
        // 使用するloaderの追加設定
        options: {
          //出力時のファイル名
          name: "[name].[ext]",
          //出力場所の指定
          outputPath: "images/"
        }
      }
    ]
  },

実行して、ブラウザで確認をするとhtmlから画像のファイルパスを指定してます

image.png

実際に画像ファイルをhtmlファイルとは別でとってきています。Networkでurl-loaderfile-loaderを比較してみます。

file-loaderではTypeがtext/plainでテキストを読み取っている?

image.png

url-loaderを使った場合はTypeがjpegでデータを読みとっている?

image.png

TypeScript

次は、TypeScriptを使って書いたファイルを出力時にJavaScriptにコンパイルする方法を書いていきます。

まずは今までのコードをTypeScriptに書き換えます。
TypeScriptで記述するために必要なパッケージを入れます。

npm i -D typescript@3.7.5

ここでは、TypeScriptを使った開発ができればいいので
四則演算のファイルを使います
ここでは加算の例のみ記します

add.ts
export function add(a: number, b: number) {
  console.log(a + b);
}
app.ts
import { add } from "../modules/add";
import { subtract } from "../modules/subtract";
import { multiply } from "../modules/multiply";
import { divide } from "../modules/divide";

add(1, 2);
subtract(1, 2);
multiply(2, 5);
divide(10, 2);

ts-loader

TypeScriptコードをJavaScriptコードにコンパイルするのに必要なLoaderがts-loaderです。
まずは、今までのJavaScriptコードをTypeScriptコードにします。

npm install -D ts-loader@6.2.1

次はwebpack.config.jsの設定です。追加部分だけを記します。

webpack.config.js
entry: "./src/ts/app.ts",
module: {
  rules: [
      {
        test: /\.ts$/,
        use: "ts-loader"
      }
   ]
},
resolve: {
 // import時の拡張子省略
 extensions: [".ts", ".js", ".json"]
}

resolve

ここで指定しているものは、import関連の設定ができます。
extensionsではimport文のPathの拡張子を省略したいものを記します。
デフォルトだと.js.jsonが指定されていますが、明示的に指定するときはこの2つも含める必要があります。

最後に、TypeScriptのコンパイルについての設定ファイルを作ります

npx tsc --init

これで実行してみてください!

image.png

image.png

使ってる感が出できた!

ここまでやってきて、webpackを使ってる感じが出てきました!

次の記事ではBabelとPluginと仲良くなりたいと思います。

何か、ご意見ありましたら教えてくれると嬉しいです。

最後にファイル構成を示しておきます。

 webpack-friendly-basic
 ├ ─ ─ node_ modules
 ├ ─ ─ package.json
 ├ ─ ─ package-lock.json
 ├ ─ ─ dist
 │     ├ ─ ─ bundle.js
 │     └ ─ ─ index.html
 ├ ─ ─ src
 │     ├ ─ ─ ts
 │     │     └ ─ ─ app.ts
 │     └ ─ ─ modules
 │           ├ ─ ─ add.ts
 │           ├ ─ ─ subtract.ts 
 │           ├ ─ ─ multiply.ts 
 │           └ ─ ─ divide.ts      
 ├ ─ ─ tsconfig.json 
 └ ─ ─ webpack.config.js
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

nvm環境でnpm自体を一発アップデートするコマンド

コマンド一発でnpm自身を最新化する方法

nvm環境でnpm自体のアップデートがうまく行かなかったのでコマンド一発で成功する方法を公開しときます。

やり方

以下、bashコマンドラインで実行してください。
(ヒアドキュメント使ってるのでcatから下部のEOFまでコピーしてね)

$ cat <<EOF > npm_update.sh && chmod +x npm_update.sh && ./npm_update.sh && rm -rf ./npm_update.sh
#!/usr/bin/bash
cd "$PROGRAMFILES"/nodejs
rm npm npx npm.cmd npx.cmd
mv node_modules/npm node_modules/npm2
node node_modules/npm2/bin/npm-cli.js i -g npm@latest
rm -rf node_modules/npm2/
EOF

やってる事

実行ファイル生成

実行権限を付与

実行してnpmアップデート

削除して終了

上記にたどり着くまでの失敗

失敗1:「npm install -g npm」を実行してみた

まずは正攻法のやり方として「$ npm install -g npm」を実行してみると以下エラー。

C:\WINDOWS\system32>npm install -g npm
npm ERR! code EEXIST
npm ERR! path C:\Program Files\nodejs\npm.cmd
npm ERR! Refusing to delete C:\Program Files\nodejs\npm.cmd: is outside C:\Program Files\nodejs\node_modules\npm and not a link
npm ERR! File exists: C:\Program Files\nodejs\npm.cmd
npm ERR! Remove the existing file and try again, or run npm
npm ERR! with --force to overwrite files recklessly.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\inukujira\AppData\Roaming\npm-cache\_logs\2020-02-15T00_43_01_988Z-debug.log

失敗2:「npm-windows-upgrade」を実行してみた

「npm-windows-upgrade」というモジュールを使えばいいというissueを見つけたのでインストールして実行してみた。
しかしうまくいかず...。

C:\WINDOWS\system32>npm-windows-upgrade
npm-windows-upgrade v6.0.1
? Which version do you want to install? 6.13.7
Checked system for npm installation:
According to PowerShell: C:\Program Files\nodejs
According to npm:        C:\Program Files\nodejs
Decided that npm is installed in C:\Program Files\nodejs
Upgrading npm... -

Upgrading npm (fallback method)... \

You wanted to install npm 6.13.7, but the installed version is 6.13.4.

A common reason is an attempted "npm install npm" or "npm upgrade npm". As of today, the only solution is to completely uninstall and then reinstall Node.js. For a small tutorial, please see https://github.com/felixrieseberg/npm-windows-upgrade#usage.

Please consider reporting your trouble to https://aka.ms/npm-issues.

Debug Information:

node: 12.16.0 | v8: 7.8.279.23-node.31 | uv: 1.34.0 | zlib: 1.2.11 | brotli: 1.0.7 | ares: 1.15.0 | modules: 72 | nghttp2: 1.40.0 | napi: 5 | llhttp: 2.0.4 | http_parser: 2.9.3 | openssl: 1.1.1d | cldr: 35.1 | icu: 64.2 | tz: 2019c | unicode: 12.1 | os: win32 x64
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

nvm環境のnpm自体をコマンド一発で最新化する方法

コマンド一発でnpm自身を最新化する方法

nvm環境でnpm自体のアップデートがうまく行かなかったのでコマンド一発で成功する方法を公開しときます

やり方

以下、bashコマンドラインで実行するだけの簡単なお仕事
ヒアドキュメント使ってるのでcatから下部のEOFまでコピーしてね
(Macの場合は「$PROGRAMFILES」環境変数とれないので書き換えればOK)

$ cat <<EOF > npm_update.sh && chmod +x npm_update.sh && ./npm_update.sh && rm -rf ./npm_update.sh
#!/usr/bin/bash
cd "$PROGRAMFILES"/nodejs
rm npm npx npm.cmd npx.cmd
mv node_modules/npm node_modules/npm2
node node_modules/npm2/bin/npm-cli.js i -g npm@latest
rm -rf node_modules/npm2/
EOF

やってる事

実行ファイル生成

実行権限を付与

実行してnpmアップデート

削除して終了

上記にたどり着くまでの失敗

失敗1:「npm install -g npm」を実行してみた

まずは正攻法のやり方として「$ npm install -g npm」を実行してみると以下エラー。

C:\WINDOWS\system32>npm install -g npm
npm ERR! code EEXIST
npm ERR! path C:\Program Files\nodejs\npm.cmd
npm ERR! Refusing to delete C:\Program Files\nodejs\npm.cmd: is outside C:\Program Files\nodejs\node_modules\npm and not a link
npm ERR! File exists: C:\Program Files\nodejs\npm.cmd
npm ERR! Remove the existing file and try again, or run npm
npm ERR! with --force to overwrite files recklessly.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\inukujira\AppData\Roaming\npm-cache\_logs\2020-02-15T00_43_01_988Z-debug.log

失敗2:「npm-windows-upgrade」を実行してみた

「npm-windows-upgrade」というモジュールを使えばいいというissueを見つけたのでインストールして実行してみた。
しかしうまくいかず...。

C:\WINDOWS\system32>npm-windows-upgrade
npm-windows-upgrade v6.0.1
? Which version do you want to install? 6.13.7
Checked system for npm installation:
According to PowerShell: C:\Program Files\nodejs
According to npm:        C:\Program Files\nodejs
Decided that npm is installed in C:\Program Files\nodejs
Upgrading npm... -

Upgrading npm (fallback method)... \

You wanted to install npm 6.13.7, but the installed version is 6.13.4.

A common reason is an attempted "npm install npm" or "npm upgrade npm". As of today, the only solution is to completely uninstall and then reinstall Node.js. For a small tutorial, please see https://github.com/felixrieseberg/npm-windows-upgrade#usage.

Please consider reporting your trouble to https://aka.ms/npm-issues.

Debug Information:

node: 12.16.0 | v8: 7.8.279.23-node.31 | uv: 1.34.0 | zlib: 1.2.11 | brotli: 1.0.7 | ares: 1.15.0 | modules: 72 | nghttp2: 1.40.0 | napi: 5 | llhttp: 2.0.4 | http_parser: 2.9.3 | openssl: 1.1.1d | cldr: 35.1 | icu: 64.2 | tz: 2019c | unicode: 12.1 | os: win32 x64
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SalesforceからJavascriptボタンを追放する!アクションボタンとフロービルダーを組み合わせた(前編)

はじめに

SalesforceのClassicでJavascriptボタンはかなり便利で使い勝手のよかったものの一つです。特別プログラムの経験がない私でも以下のようにレコード作成時や更新時の利便性向上のために、Javascriptボタンを使用していました。

レコード作成時に関連レコードや遷移元レコードの値を項目の初期値、デフォルト値を設定する方法

よく以下のようにJavascriptボタンを作っていました。
Salesforceのjavascriptボタンの一例.png

取引先や商談のレコードページにJavascriptボタンを置くだけでなく、レポートを作成してそこにカスタム項目を追加して動的にレコードの作成、更新ができるようによく設定を行っていました。

ClassicからLightningへ移行する際に一番困ったことはこのJavascriptボタンの存在です。今まで当たり前のように使っていたJavascriptボタンが使えないとなると、ユーザがなかなかLightningを使ってくれません。レポートやダッシュボードだけはグラフィカルで見やすいLightningで見るけど、日常業務は比較的表示スピードの早くて慣れているclassicになりがちです。

今回はアクションボタンを用いてフローを呼び出してみたいと思います。それに関連してLightningへ移行途中でもclassicで使えるようにフローのボタンもご紹介できればと思います。

またいつものごとく長文になってしまいましたので、前編・後編にわけたいと思います。

親取引先に紐づいた取引先の一括行動作成フローを作成

行動一括作成全体像.png

今回作成するフローは「画面フロー」で、親取引先が紐づいている取引先はまとめて活動が作成できるフローになります。ただし、商談がない取引先には活動は作らないようにします。また親取引先がない取引先は対象の取引先だけ活動が作成するようにします。

レコードIDと一致する「recordId」の意味を理解する

アクションボタンにフローを組み合わせて使う場合、一番重要で理解すべきことは「レコードを取得」でレコードを絞り込む条件として「レコードIDと一致する『recordId』」の意味を理解しなくてはいけません。

結論をいうとレコードを絞り込む条件として変数「redordId」を指定してあげれば現在表示しているレコードのIDをSalesforce側で自動的に取得してくれます。これはSalesforceのヘルプにもある通りです。

フローアクションの考慮事項

フローに recordId というテキスト入力変数があれば、アクションの実行時にレコードの ID がその変数に渡されます。

これを実感できる非常に簡単なフローアクションを作成してみます。
recordIdを含んだ画面.png

画面の要素を追加し、画面コンポーネントの「表示テキスト」を真ん中の画面イメージにドラッグ&ドロップを行います。右側にある「リソースを挿入」をクリックし「新規リソース」をクリックします。

作成する変数は以下の通りです。
API名:recordId
データ型:テキスト
フロー外部での可用性:入力で使用にチェック

上記の画像のようになったら、「開始」と「画面」をつないで保存し、このフローを有効にします。

次に任意のオブジェクトでフローを用いたアクション(フローアクション)を作成します。

以下のようにアクション種別をフロー、フローは先ほど作成したフローを選択し、任意の表示ラベルと名前をつけて保存します。保存したアクションをページレイアウトの「Salesforce モバイルおよび Lightning Experience のアクション」に追加します。

フローアクション.png

そうしてアクションボタンを実行すると次のような感じの画像が出てくるかと思います。

レコードIDと一致するrecordId.png

画像ように赤で囲んでいるレコードIDを確認するとまったく同じIDが確認できると思います。
このようにフローアクションを作成するときは最初のレコードを取得するときにrecordIdの変数を用いて今現在表示しているレコードIDを取得します。これを起点に様々な情報を取得することが可能になります。

この画面フローでやること

箇条書きでこのフローをまとめると以下の通りです。
1.現在表示している取引先と商談の情報を取得
2.画面にデータを入力
3.条件分岐で親取引先がない場合は最初の取引先の活動だけ作成
親取引先がある場合は親に関連している全ての取引先を取得
4.3.で取得した情報をループに渡し最新の商談を取得後、行動を作成
5.取得した行動の件名がないものは削除し、ループに戻る。
件名はあるものはループにもどり取引先の数だけ繰り返す

レコードを取得する

取引先のレコードを取得する際に変数recordIdで絞込み今現在の取引先の情報を取得します。
商談の情報を取得するのは商談のフェーズをもとに件名を自動的に入力するようにするためです。

画面を作成し、事前に値を設定しておく

フロービルダーの画面コンポーネント.png

画面には色々な要素があってここで全てを説明するのは難しいうえに、フローの本質からずれてしまうので細かい部分は解説しません。また時間があるときにまとめようと思いますが、ここでは必要な部分だけかいつまんで説明していきます。

今回画面に使っているコンポーネントは上から
1.表示テキスト
2.日時
3.チェックボックス
4.ルックアップ

となります。

1.表示テキスト

文字通り文字を入力できるコンポーネントになります。文字を打ち込むだけでなく、作成した変数も挿入できますので動的な文字表示が可能となります。また文字の大きさや装飾も可能です。

2.日時

日付と時刻を入力するコンポーネントです。デフォルト値に日時に対応した変数を入れておくことができます。開始では現時刻を表示するために{!$Flow.CurrentDateTime}、終了では数式を以下のように作成して今より1時間後を表示していまます。

{!$Flow.CurrentDateTime} + 60/1440
(1日24時間、1時間は60分となりますので分単位で表現するには分母を1440として分子に任意の分を入力します(24(時間)×60(分)=1440分))

3.チェックボックス

チェックはBoolean型になりますのでこちらのデフォルト値にはチェックなしであれば {!$GlobalConstant.False}、 ありであれば{!$GlobalConstant.True} とします。

4.ルックアップ

指定した参照関係の項目を表示することができます。今回は任命先なのでユーザに関連している参照項目を指定します。事前にレコードを入力する場合はレコードIDを入力します。

今現在操作しているユーザを事前にいれておきたいので数式を作成し、次の数式を入力します。
{!$User.Id}

数式を作成すれば他にもProfileやLabel、APIなど8種類のグローバル変数を用いることが可能です。

前編はここまでとなります。

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

vuelidateを使ってバリデーションを実装する。

インストール

npm install vuelidate --save

グローバルで使用するときは、下記のように記述

import Vue from "vue";
import Vuelidate from "vuelidate";
Vue.use(Vuelidate);

コンポーネント内で直接使用するときは、

import { validationMixin } from 'vuelidate'

var Component = Vue.extend({
  mixins: [validationMixin],
  validations: { ... }
})

のようにコンポーネントファイルに追加してください。

例えば、age と message という 2 つの属性にそれぞれ必須のバリデーションを、
加えて、名前のほうに 4 文字以上で入力させたい場合は、下記のように validations
の中でそれぞれrequired、name に minLength を書いていきます。

import { required, minLength, between } from "vuelidate/lib/validators";

export default {
  data() {
    form {
      name: "",
      message: ""
    };
  },
  validations: {
    name: {
      required,
      minLength: minLength(4)
    },
    message: {
      required
    }
  }
};

最後に、下記のようにフォームに使用する場合は、下記のように
v-if="!$v.form.属性.バリデーション"でエラーメッセージを
表示させることができます。(下記はVueの例です。)

<form @submit.prevent="validateForm">
  <div class="sender-info">
    <md-field :class="getValidationClass('name')">
      <label for="name" class="label">名前</label>
      <md-input
        name="name"
        id="name"
        autocomplete="given-name"
        v-model="form.name"
        class="md-accent"
      />
      <span class="md-error md-primary" v-if="!$v.form.name.required"
        >名前を入力してください</span
      >
      <span class="md-error md-primary" v-else-if="!$v.form.name.minlength"
        >名前は4文字以上で入力して下さい</span
      >
    </md-field>
  </div>
</form>
  methods: {
    getValidationClass(fieldName) {
      const field = this.$v.form[fieldName];
      if (field) {
        return {
          "md-invalid": field.$invalid && field.$dirty
        };
      }
    }
  },
  validateForm() {
    this.$v.$touch();
    if (!this.$v.$invalid) {
      this.submit();
    }
  },
  submit () {
    // Instead of this timeout, here you can call your API
    window.setTimeout(() => {
      this.lastUser = `${this.form.firstName} ${this.form.lastName}`
      this.userSaved = true
      this.sending = false
      this.clearForm()
    }, 1500)
  }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

webサービスを運営してみた(2020/2/15)

はじめに

アルバイトの勤怠管理を無料でできるサービスTimestampを個人で運営しています。
ここでは開発まわりの記録を残していきます。

ユーザー数

スクリーンショット 2020-02-15 3.25.45.png
googleアナリティクスのグラフです。
ユーザ数が先週より伸びてますね。嘘です伸びてないですね。
ほとんどがQiita記事のリンクからの流入で、google検索からは1日0~5人ぐらいみたいです。
僕自身がgoogle検索してもなかなか見つからないのに、0人でないことに驚きです。

雑記

googleアドセンスの審査に落ちました

googleアドセンスの審査に落ちました。
審査基準を把握していないのですが、ページビューが足りないとかなのかな?
ユーザがいない状態でアドセンスを導入しても意味がないので将来的に対応ということにします。

httpsになりました

Let's Encrypt で Nginx にSSLを設定しました。
記事に感謝。

開発予定

しばらくは機能開発に時間がかかりそうです。
そのあとはSEO対策、英語翻訳を外注、LPのデザインを外注しようかなと考えているのですが、
外注できるかは料金次第ですね。。

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

ブラウザにデータを保存する方法

概要

Web システムを開発していて、データを保存しておきたいけどクライアント側だけでどうにかしたいという場合の方法をまとめました。

調査結果

調べた結果から書くと、Web Storageを使用しましょう。同じような仕組みとしてcookieもありますが、以下の 3 つの違いから、Web Storageを使用した方が良いと思います。

  1. 扱えるデータがWeb Storageのほうが大きい(cookieは 4KB、Web Storageは 5MB)。
  2. JavaScript による制御がより簡単。
  3. cookieはリクエスト毎に自動送信されてしまう。

使用方法

まずWeb StoragelocalStoragesessionStorageに分かれています。2つの主な違いはデータの有効期限で、localStorageは永続的、sessionStorageはウィンドウまたはタブを閉じるまでです。

2つの使用方法に大きな違いは無く、アクセスするオブジェクトが異なるだけです。

  • localStorage
  /* storageに値を設定 */
  localStorage.setItem("key", "value");

  /* storageから値を取得 */
  let value = localStorage.getItem("key");

  /* storageの値を削除 */
  localStorage.removeItem("key");
  • sessionStorage
  /* storageに値を設定 */
  sessionStorage.setItem("key", "value");

  /* storageから値を取得 */
  let value = sessionStorage.getItem("key");

  /* storageの値を削除 */
  sessionStorage.removeItem("key");

感想

今回調べてみて、Web Storageというものを初めて知りました。Jsonのような使い方ができるので使い勝手も良いと感じました。

記事に間違いや追加してほしい事がありましたら、コメントにご記入ください。
(感想も欲しいです!!?)

参考文献

MDN Web Strorage API

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

Debugger for Chrome の launch で Google アカウントでのログインができなくなった件

Firebase Auth を使った Web アプリを VSCode にて開発中、昨年(2019年)末からだと思うのですけど、Google アカウントでのログインができなくなったことに気づきました。

あ、これを書いてる時点の Chrome バージョンは 80.0.3987.106(Official Build) (64 ビット)で、OS は Windows 10 Pro で依然として再現しています。

Web アプリ開発で、VSCode に Debugger for Chrome を追加して launch.json に次のように記述する方法はよく知られています。

{
    "version": "0.2.0",
    "configurations": [
      {
        "type": "chrome",
        "request": "launch",
        "name": "Debug with Chrome",
        "url": "http://localhost:4200",
        "webRoot": "${workspaceFolder}"
      }
    ]
}

が、これで起動される Chrome では、Google アカウントでのログインができず、次のようなエラーになってしまいます。

image.png

なぜこれが発生するのかは私は完全に理解していませんが、起動された Chrome がセキュリティ要件を満たさないものと推測されます。(同じ時期に、Google アカウントを利用したゲームにログインできなくなった、というような話題を主に海外で発見しました。)

解決方法

ということをツイートしたら、有用な情報を頂きました。ありがとうございます。

さらに調べたところ、 launch.json に次のように userDataDir を追記することで解決できました。

{
    "version": "0.2.0",
    "configurations": [
      {
        "type": "chrome",
        "request": "launch",
        "name": "Debug with Chrome",
        "url": "http://localhost:4200",
        "webRoot": "${workspaceFolder}",
        "userDataDir": "" 
      }
    ]
}

"userDataDir": "" と空文字を指定すると、起動する Chrome がいつものユーザー、いつものプロファイルになり、Google アカウントでのログインも行えるようになりました。

image.png

注意点は、この指定で起動される Chrome は、普段使いの Chrome が既に起動済の場合は、そのプロセスを流用する、ということです。

そのため、

  • 開発中のアプリが死ぬと普段使いのタブも道連れに死ぬ
  • 普段使いの Chrome もデバッグ用のポートを開けておかないとデバッグできない

という注意点があります。

普段使う Chrome のショートカットに引数を追加しておきます(最初から付いてたっけ?)。

image.png

"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --profile-directory=Default --remote-debugging-port=9222

その他の解決方法

Debugger for Edge でも問題を解決することができました。
Debugger for Firefox は未確認で「たぶん解決できるだろう」という程度です。
attach は launch より面倒な感じがして試してません、みんな常用してるのでしょうか。。

参考

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