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

Sublime TextにJavaScriptのリアルタイム文法チェックをしてくれるSublimeLinter-jshintをインストールする方法

  • 環境
    • CentOS Linux release 7.6.1810 (Core)
    • Sublime Text Version 3.2.1

SublimeLintをインストールする

  1. Package Controlをインストールする
  2. Package ControlでSublimeLintをインストールする

Node.jsをインストールする

CentOS 7 Node.js のインストール手順 - Qiita

1. リポジトリを追加する

$ curl -sL https://rpm.nodesource.com/setup_8.x | sudo bash -
## Installing the NodeSource Node.js 8.x LTS Carbon repo...
# 省略
## Run `sudo yum install -y nodejs` to install Node.js 8.x LTS Carbon and npm.
## You may also need development tools to build native addons:
     sudo yum install gcc-c++ make
## To install the Yarn package manager, run:
     curl -sL https://dl.yarnpkg.com/rpm/yarn.repo | sudo tee /etc/yum.repos.d/yarn.repo
     sudo yum install yarn

2. Node.jsをインストールする

$ sudo yum install nodejs
Loaded plugins: fastestmirror, ovl
Repository google-chrome is listed more than once in the configuration
# 省略
Installed:
  nodejs.x86_64 2:8.16.0-1nodesource

Complete!

3. バージョンを確認する

$ node -v
v8.16.0

jsHintをインストールする

jsHintとSublimeLinter-jshintを入れるメモ for mac - tweeeetyのぶろぐ的めも

# インストールする
$ sudo npm install -g jshint
/usr/bin/jshint -> /usr/lib/node_modules/jshint/bin/jshint
+ jshint@2.10.2
added 30 packages from 15 contributors in 1.614s

# バージョンを確認する
$ jshint -v
jshint v2.10.2

SublimeLinter-jshintをインストールする

  1. Package ControlでSublimeLinter-jshintをインストールする
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webページを高速化するには

はじめに

HTMLコーダー&ウェブ担当者のためのWebページ高速化超入門本を読んだのでまとめてみました。詳しくはそちらを参照してください、分かりやすかったです。

目標

データの転送を最適化し、Webページの初期表示を高速化する

参考

HTMLコーダー&ウェブ担当者のためのWebページ高速化超入門
【電子書籍】Chromeデベロッパーツールを使いこなそう Network編
https://www.seohacks.net/basic/terms/mfi/
https://ferret-plus.com/8056

Webサイト測定ツール

上記2つは診断内容が少し異なるみたいです
違いについて↓
https://ferret-plus.com/8056

昨今のインターネットの利用環境

  • スマホの利用者は85%
  • Googleの2017年の調査でモバイルサイトのページ読み込みに3秒以上かかる場合、53%のユーザが離脱
  • MFIとは「Mobile First Index(モバイルファーストインデックス)」の略称で、インデキシングの対象をPC向けのページから、モバイル向けのページに変。さらに採点要素にWEbサイトが表示されるスピードが追加

大まかなボトルネックの場所

  • 通信環境
  • サーバー側(webサーバなど)
  • アプリケーション(コンテンツの内容)
    • 重い画像
    • 重い動画
    • 動的に生成など
  • クライアント側(PC)

フロントの高速化とは

フロントエンドはもっともWebサイトの高速化に着手しやすい領域

簡単なChrome DevToolsの使い方

  • networkタブでdisable cacheにチェック

    • ページの情報をキャッシュしない
  • 一番下のステータスバー
    cromeinfo.PNG

ステータスバーの各項目

  • Finish:
    • ページが表示されるまでかかった時間
  • DOMContentLoaded:
    • HTMLの読み込みと解析が終わった時間(CSS、画像含まない)
  • Load:
    • ページを完全に読み込み終わった時間
  • requests:
    • サーバーからダウンロードしたファイルの数
  • transferred:
    • サーバーからダウンロードしたファイルの総容量。数十MB~数百MBだと時間がかかる。

waterfall

ChromDevToolのURLが書かれている行のWaterfall列

mttf.PNG

  • Queued at:
    • ネットワークパネルで計測が始まった時間
  • Started at:
    • キューイングの終わった時間
  • Queuing:
    • Waterfall所の白いライン。Queued at + Queueing = Started at
  • ResourceScheduling:
    • リソース取得のキューイングを表示
  • Connection Start:
    • サーバーとの接続状況を表示
  • Stalled: リクエストを送信できるようになるまでの待機時間
    • InitialConnection: TCPハンドシェイクやSSLを含んだ初期接続を確立するまでの時間
  • SSL:

    • SSLのハンドシェイクに要した時間
  • Request/Response

    • 実際にデータのやりとりの状況を扶養時
  • Request sent:

    • リクエストの送信にかかった時間
  • Waiting(TTFB):

    • 最初の1バイト目を受け取るまでにかかった時間。推奨時間:200ミリ秒。これを超えるとサーバーが遅いか回線が遅いかに着目する。
  • ContentDownlad:

    • レスポンスのデータを受信するのにかかった時間

項目について詳細

webページの画像の最適化

  • 一番カンタンに削除効果が期待できる
  • 画像の特徴にあう形式を選び、適切な大きさに縮小し、圧縮する
    • 画像の容量を減らすことが目的

適切な画像選択

  • jpeg
    • 写真ななど素材の色が多い画像に適している
  • PNG
    • イラストやロゴなど色が少ない画像に適している
  • GIF
    • パラパラアニメーション
  • SVG
    • 新しい形式。ロゴや簡単な図形

画像を縮小して圧縮する

  • 画像編集ソフトで縦横の幅を小さく縮小
    • そもそも適正な大きさかの確認はChrome Devtoolでもとの大きさ実際に表示されている大きさが確認できる。これに違いがあれば縮小したほうがよさそう
    • Photoshopやwindowsのペイントで縮小できる
  • 人には感じにくい色の情報を省く
    • Antelope
    • ImageOptimなど

画像のLazyload

  • サイト表示した際に表示されていない画像は読み込まず、スクロール範囲内に近づいてからダウンロードして表示させる
  • 縦に長いページで画像を多用しているようなサイトに有効
  • JavaScriptのプラグインで対応することが多いみたい

テキストファイルの圧縮

圧縮など

  • JavaScriptやCSSをまとめたりするのはWebアプリケーション側で自動でやってくれるので割愛

サーバー側

しかし、サーバ側でHTTP2という通信方式が進化したため、複数ファイルを一つにまとめるというテクニックは過去のものになりつつある

HTTP1

  • 上記のChromeDevToolの画像のRequestsでもわかるように、トップページを表示するだけでも多数のTCP接続が走る → サーバーの負荷、時間がかかる

HTTP2

  • 1つのTCP接続を用いて複数のリクエスト/レスポンスのやり取りできる
    • 3ウェイハンドシェイクが一度だけ → サーバーの負荷、時間の軽減
  • httpsのみでしか通信できない

詳しいHTTP2のメリット↓
https://qiita.com/uutarou10/items/7698ee3336c70a482843

体感的な表示速度の向上(ファーストビュー&Above the fold)

  • 最初に見える領域をその下の領域よりも優先して表示させるテクニック
  • PCのファーストビュー目安
    • 1920×1080
  • スマホのファーストビュー目安
    • 375×667

ファーストビュー関連のJavaScriptの最適化

ブラウザがページをレンダリングし始めるのはJavaScriptファイルをすべてダウンロードして解析し終わったあと。そこでJavaScriptの解析が終わるのを待つことなくHTMLのレンダリングをさせることができるようにするためにdefer属性やasyncを利用する

ファストビュー関連のJavaScriptダウンロードを待つことなくファストビューを表示できるようになる

基本deferを使用してasyncは順不同でjavascriptを読み込むので、他のJavaScriptファイルに依存せず、ファーストビュー付近に関連するJavaScriptであればasyncを使う

CSSの最適化

CSSファイルをダウンロードが全て完了してから解析してブラウザはレンダリングを始める

CSSの非同期

  • レンダリングをブロックせずにCSSを読み込むことができる
  • すべてのCSSの読み込みを非同期にしてしまうとページ表示のときにバラバラにCSS適用がされて見栄えが悪くなる=FOUC(Flash of Unstyled Content)
  • そこでまずファーストビューのCSSだけをheadタグに直接書き込む。これでファーストビューをうまく表示できるようにする。ファストビューより下の領域のCSSを非同期に読み込む
  • その他のCSSにかんしてはpreloadとas=styleを使用して非同期にする

測定方法

Chrome DevTools のAuditsを使うと体感的な表示速度がわかるようになる。

キャッシュの有効活用と不要なコンテンツの排除

  • サーバー側(.htaccessなど)で画像やcssなどのブラウザキャッシュ期間を設定できる
  • もう一度サイトを見直してファーストビューに重い処理はないか、むやみに画像を使ってないかなど調べて不要なものは取り除く

まとめ

Chrome DevToolsのWaiting(TTFB)項目(最初の1バイト目を受け取るまでにかかった時間)をみて200ミリ秒を超えるようだとサーバーチェック、200ミリ秒以内であればフロント側チェックして上記項目を試してみる価値あり

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

amcharts 4 Demos を使ってグラフを作成(piechart編)

About

amcharts関連の日本語文献のあまりの少なさから、誰かのお役に立てればと思い記載しています!

最終的には、このようなグラフを作成することができます。7d0de671ad5679b2edef0a20c7ab9b06.gif

Environment

この記事ではmacbook(unix)にインストールしたruby 2.5.1p57, Rails 5.2.3を使っています。

amchartsとは

前回の記事をご覧ください!

Piechartとは

円グラフです。今回はこちらを作成します。
2e57a92ca3d39c6dbd16f293e967fc7c.gif

導入方法

こちらのサイトのページ内を下にスクロールすると、Demo sourceが置いてありますので、コピペで使用可能です。
基本的にはそのままコードを触らずとも、実装可能です。

Demo_source
<!-- Styles -->
<style>
#chartdiv {
  width: 100%;
  height: 500px;
}

</style>

<!-- Resources -->
<script src="https://www.amcharts.com/lib/4/core.js"></script>
<script src="https://www.amcharts.com/lib/4/charts.js"></script>
<script src="https://www.amcharts.com/lib/4/themes/animated.js"></script>

<!-- Chart code -->
<script>
am4core.ready(function() {

// Themes begin
am4core.useTheme(am4themes_animated);
// Themes end

// Create chart instance
var chart = am4core.create("chartdiv", am4charts.PieChart);

// Add data
chart.data = [ {
  "country": "Lithuania",
  "litres": 501.9
}, {
  "country": "Czech Republic",
  "litres": 301.9
}, {
  "country": "Ireland",
  "litres": 201.1
}, {
  "country": "Germany",
  "litres": 165.8
}, {
  "country": "Australia",
  "litres": 139.9
}, {
  "country": "Austria",
  "litres": 128.3
}, {
  "country": "UK",
  "litres": 99
}, {
  "country": "Belgium",
  "litres": 60
}, {
  "country": "The Netherlands",
  "litres": 50
} ];

// Add and configure Series
var pieSeries = chart.series.push(new am4charts.PieSeries());
pieSeries.dataFields.value = "litres";
pieSeries.dataFields.category = "country";
pieSeries.slices.template.stroke = am4core.color("#fff");
pieSeries.slices.template.strokeWidth = 2;
pieSeries.slices.template.strokeOpacity = 1;

// This creates initial animation
pieSeries.hiddenState.properties.opacity = 1;
pieSeries.hiddenState.properties.endAngle = -90;
pieSeries.hiddenState.properties.startAngle = -90;

}); // end am4core.ready()
</script>

<!-- HTML -->
<div id="chartdiv"></div>

編集方法

style, htmlについては割愛し、scriptについて筆者がわかる範囲で記載していきます。

index.html.erb
<!-- Chart code -->
<script>
am4core.ready(function() {

// Themes begin
// テーマです。
am4core.useTheme(am4themes_animated);
// Themes end

// Create chart instance
// 始めにインスタンスを作成します。Piechart形式であることをここで指定しています。
var chart = am4core.create("chartdiv", am4charts.PieChart);

// Add data
// ここにデータを入力していきます。
chart.data = [ {
  "country": "Lithuania",
  "litres": 501.9
}, {
  "country": "Czech Republic",
  "litres": 301.9
}, {
  "country": "Ireland",
  "litres": 201.1
}, {
  "country": "Germany",
  "litres": 165.8
}, {
  "country": "Australia",
  "litres": 139.9
}, {
  "country": "Austria",
  "litres": 128.3
}, {
  "country": "UK",
  "litres": 99
}, {
  "country": "Belgium",
  "litres": 60
}, {
  "country": "The Netherlands",
  "litres": 50
} ];

// Add and configure Series
// 作成したインスタンスに設定を追加していきます。
var pieSeries = chart.series.push(new am4charts.PieSeries());
// データはlitres(リットル)
pieSeries.dataFields.value = "litres";
// 単位はcountryです
pieSeries.dataFields.category = "country";
pieSeries.slices.template.stroke = am4core.color("#fff");
pieSeries.slices.template.strokeWidth = 2;
pieSeries.slices.template.strokeOpacity = 1;

// This creates initial animation
pieSeries.hiddenState.properties.opacity = 1;
pieSeries.hiddenState.properties.endAngle = -90;
pieSeries.hiddenState.properties.startAngle = -90;

}); // end am4core.ready()
</script>

実装例

最後に、私が作成した"sommeil"というアプリケーションのコードの内、一部のamcharts部分を参考までに記載します。

_piechart.html.erb
<!-- Styles -->
<style>
  #chart2div {
    width: 100%;
    height: 300px;
  }
</style>
<!-- Resources -->
<script src="https://www.amcharts.com/lib/4/core.js"></script>
<script src="https://www.amcharts.com/lib/4/charts.js"></script>
<script src="https://www.amcharts.com/lib/4/themes/animated.js"></script>
<!-- Chart code -->
<script>
  am4core.ready(function () {
    // Themes begin
    am4core.useTheme(am4themes_animated);
    // Themes end
    // Create chart instance
    var chart2 = am4core.create("chart2div", am4charts.PieChart);
    // Add data
    var sleeping_time = '<%= @sleeping_time %>';
    chart2.data = [{
      "time": "Sleep",
      "amount": sleeping_time
    }, {
      "time": "Awake",
      "amount": (24 - sleeping_time)
    }];
    // Add and configure Series
    var pieSeries = chart2.series.push(new am4charts.PieSeries());
    pieSeries.labels.template.disabled = true;
    pieSeries.dataFields.value = "amount";
    pieSeries.dataFields.category = "time";
    pieSeries.slices.template.stroke = am4core.color("#fff");
    pieSeries.slices.template.strokeWidth = 2;
    pieSeries.slices.template.strokeOpacity = 1;
    // This creates initial animation
    pieSeries.hiddenState.properties.opacity = 1;
    pieSeries.hiddenState.properties.endAngle = -90;
    pieSeries.hiddenState.properties.startAngle = -90;
  }); // end am4core.ready()
</script>
<!-- HTML -->
<div id="chart2div"></div>

最後に

注釈や解説できない部分がまだまだありますので、今後検証して追記していきます。
ご覧いただき、ありがとうございました。

筆者について

TECH::EXPERTにて4月よりruby, railsを学習している未経験エンジニアです。
記載内容に不備・不足があればご指摘いただけると幸いです。
至らぬ点ばかりですので、改善点がありましたらどんどんご指摘下さい!

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

GraphQLのクエリーを書けば、上手に型をつけてApolloClientでクエリを飛ばすやつを自動で生成したいと思ったので、Typescriptを書き出すJavascriptを書いた

概要

GraphQLクエリを書いたら、graphql-codegenが型を吐いてくれるけど、型をいちいち書くのもラクしたかったので、graphql-codegenのプラグインを自分で書いた。

できた奴

1.スキーマの例

こんなスキーマがあるとして、

schema.graphql
type Tweet {
  id: ID!
  body: String
  date: String
  Author: User
  Stats: Stat
}

type User {
  id: ID!
  username: String
  first_name: String
  last_name: String
  full_name: String
  name: String @deprecated
  avatar_url: String
}

type Stat {
  views: Int
  likes: Int
  retweets: Int
  responses: Int
}

type Notification {
  id: ID
  date: String
  type: String
}

type Meta {
  count: Int
}

type Comment {
  id: String
  content: String
}

type Query {
  Tweet(id: ID!): Tweet
  Tweets(limit: Int, skip: Int, sort_field: String, sort_order: String): [Tweet]
  TweetsMeta: Meta
  User(id: ID!): User
  Notifications(limit: Int): [Notification]
  NotificationsMeta: Meta
}

type Mutation {
  createTweet(body: String): Tweet
  deleteTweet(id: ID!): Tweet
  markTweetRead(id: ID!): Boolean
}

type Subscription {
  commentAdded(repoFullName: String!): Comment
}

参考: https://github.com/marmelab/GraphQL-example/blob/master/schema.graphql

2.クエリの例

こんなクエリを書いたgraphqlファイルを作るとする。

tweet.graphql
query TweetMeta {
  TweetsMeta {
    count
  }
}

query Tweet($id: ID!) {
  Tweet(id: $id) {
    body
    date
    Author {
      full_name
    }
  }
}

mutation CreateTweet($body: String) {
  createTweet(body: $body) {
    id
  }
}

subscription SubscComment($repoFullName: String!) {
  commentAdded(repoFullName: $repoFullName) {
    id
    content
  }
}

3. 生成例

するとこんな感じにtsを吐いてくれる。(監視対象の.graphqlごとにクラスを生やしてくれる)

generated/class.ts
import * as Type from "./types";
import * as Node from "./nodes";
import * as ApolloType from "apollo-client";
import ApolloClient from "apollo-client";
export interface ClientClass {
  readonly client: ApolloClient<any>;
}

export class TweetClient implements ClientClass {
  constructor(readonly client: ApolloClient<any>) {}

  tweetMeta = (
    options?: Omit<
      ApolloType.QueryOptions<Type.TweetMetaQueryVariables>,
      "query"
    >
  ) =>
    this.client.query<Type.TweetMetaQuery, Type.TweetMetaQueryVariables>({
      ...options,
      ...{ query: Node.TweetMeta }
    });

  tweet = (
    options?: Omit<ApolloType.QueryOptions<Type.TweetQueryVariables>, "query">
  ) =>
    this.client.query<Type.TweetQuery, Type.TweetQueryVariables>({
      ...options,
      ...{ query: Node.Tweet }
    });

  createTweet = (
    options?: Omit<
      ApolloType.MutationOptions<
        Type.CreateTweetMutation,
        Type.CreateTweetMutationVariables
      >,
      "mutation"
    >
  ) =>
    this.client.mutate<
      Type.CreateTweetMutation,
      Type.CreateTweetMutationVariables
    >({ ...options, ...{ mutation: Node.CreateTweet } });

  subscComment = (
    options?: Omit<
      ApolloType.SubscriptionOptions<Type.SubscCommentSubscriptionVariables>,
      "query"
    >
  ) =>
    this.client.subscribe<
      Type.SubscCommentSubscription,
      Type.SubscCommentSubscriptionVariables
    >({ ...options, ...{ query: Node.SubscComment } });
}

4. 使い道

gqlタグとか書かずに、補完バリバリで気持ちよく書ける。嬉しい。

main.ts
import { TweetClient } from "./generated/class";
import ApolloClient from "apollo-boost";
import "isomorphic-fetch";

const client = new TweetClient(
  new ApolloClient({ uri: "http://localhost:4000/" })
);

async function main() {
  const hoge = await client.tweetMeta();
  console.log(JSON.stringify(hoge.data.TweetsMeta));

  const huga = await client.createTweet({
    variables: {
      body: "aaa"
    }
  });
  //dataはnullチェックしないと怒られる
  console.log(JSON.stringify(huga.data && huga.data.createTweet));

  const piyo = await client.tweet({ variables: { id: "hoga" } });
  console.log(JSON.stringify(piyo.data));
}

main();

作り方

環境

yarnなり、npmでよしなに入れる。

package.json
{
  "scripts": {
    "codegen": "graphql-codegen --config codegen.yml --watch",
    "server": "node server.js"
  },
  "devDependencies": {
    "@graphql-codegen/cli": "^1.3.1",
    "@graphql-codegen/typescript": "^1.3.1",
    "@graphql-codegen/typescript-document-nodes": "^1.3.1-alpha-21fe4751.62",
    "@graphql-codegen/typescript-operations": "1.3.1",
    "@types/graphql": "^14.2.2",
    "apollo-client": "^2.6.3",
    "change-case": "^3.1.0",
    "graphql": "^14.4.2",
    "prettier": "^1.18.2",
    "typescript": "^3.5.2"
  },
  "dependencies": {
    "apollo-boost": "^0.4.3",
    "apollo-server": "^2.6.7",
    "isomorphic-fetch": "^2.2.1"
  }
}

適当にモックサーバを立てる。

ApolloServerのmockをtrueにすれば適当にサーバが立つ。

モックサーバーの立て方の例
server.js
const { ApolloServer, gql } = require("apollo-server");

const typeDefs = `
type Tweet {
  id: ID!
  body: String
  date: String
  Author: User
  Stats: Stat
}

type User {
  id: ID!
  username: String
  first_name: String
  last_name: String
  full_name: String
  name: String @deprecated
  avatar_url: String
}

type Stat {
  views: Int
  likes: Int
  retweets: Int
  responses: Int
}

type Notification {
  id: ID
  date: String
  type: String
}

type Meta {
  count: Int
}

type Comment {
  id: String
  content: String
}

type Query {
  Tweet(id: ID!): Tweet
  Tweets(limit: Int, skip: Int, sort_field: String, sort_order: String): [Tweet]
  TweetsMeta: Meta
  User(id: ID!): User
  Notifications(limit: Int): [Notification]
  NotificationsMeta: Meta
}

type Mutation {
  createTweet(body: String): Tweet
  deleteTweet(id: ID!): Tweet
  markTweetRead(id: ID!): Boolean
}

type Subscription {
  commentAdded(repoFullName: String!): Comment
}

`;

const server = new ApolloServer({
  typeDefs,
  mocks: true
});

server.listen().then(({ url }) => {
  console.log(`Server ready at ${url}`);
});

起動

terminal
yarn server

Graphql-codegenを走らす

graphql-codegenの設定ファイルを書く。

codegen.yaml
overwrite: true
schema: "http://localhost:4000" # サーバのアドレス。勝手にスキーマを呼んでくれる
documents: "queries/*.graphql" # 監視対象 ここのファイルに更新があれば自動的に型を作成
generates:
  ./generated/types.ts:
    plugins:
      - typescript
      - typescript-operations
  ./generated/nodes.ts:
    plugins:
      - typescript-document-nodes

監視対象として、さっきの例のtweet.graphqlqueries/に作っておく。

そしてgraphql-codegenを走らせる。

terminal
yarn codegen

すると./generated/types.tsに型を吐くし、./generated/nodes.tsにクエリにgqlタグ付けた定数が吐かれる。また、これらはqueriesのファイルの変化に応じて逐次更新される。

なお、"codegen": "graphql-codegen --config codegen.yml --watch"からwatchオプションを外せば、自動更新は無効になる。

ts:generated/nodes.ts(自動生成)
generated/nodes.ts(自動生成)
import { DocumentNode } from "graphql";
import gql from "graphql-tag";

export const TweetMeta: DocumentNode = gql`
  query TweetMeta {
    TweetsMeta {
      count
    }
  }
`;

export const Tweet: DocumentNode = gql`
  query Tweet($id: ID!) {
    Tweet(id: $id) {
      body
      date
      Author {
        full_name
      }
    }
  }
`;

export const CreateTweet: DocumentNode = gql`
  mutation CreateTweet($body: String) {
    createTweet(body: $body) {
      id
    }
  }
`;

export const SubscComment: DocumentNode = gql`
  subscription SubscComment($repoFullName: String!) {
    commentAdded(repoFullName: $repoFullName) {
      id
      content
    }
  }
`;

ts:generated/types.ts(自動生成)
generated/types.ts(自動生成)
export type Maybe<T> = T | null;
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
  Upload: any;
};

export enum CacheControlScope {
  Public = "PUBLIC",
  Private = "PRIVATE"
}

export type Comment = {
  __typename?: "Comment";
  id?: Maybe<Scalars["String"]>;
  content?: Maybe<Scalars["String"]>;
};

export type Meta = {
  __typename?: "Meta";
  count?: Maybe<Scalars["Int"]>;
};

export type Mutation = {
  __typename?: "Mutation";
  createTweet?: Maybe<Tweet>;
  deleteTweet?: Maybe<Tweet>;
  markTweetRead?: Maybe<Scalars["Boolean"]>;
};

export type MutationCreateTweetArgs = {
  body?: Maybe<Scalars["String"]>;
};

export type MutationDeleteTweetArgs = {
  id: Scalars["ID"];
};

export type MutationMarkTweetReadArgs = {
  id: Scalars["ID"];
};

export type Notification = {
  __typename?: "Notification";
  id?: Maybe<Scalars["ID"]>;
  date?: Maybe<Scalars["String"]>;
  type?: Maybe<Scalars["String"]>;
};

export type Query = {
  __typename?: "Query";
  Tweet?: Maybe<Tweet>;
  Tweets?: Maybe<Array<Maybe<Tweet>>>;
  TweetsMeta?: Maybe<Meta>;
  User?: Maybe<User>;
  Notifications?: Maybe<Array<Maybe<Notification>>>;
  NotificationsMeta?: Maybe<Meta>;
};

export type QueryTweetArgs = {
  id: Scalars["ID"];
};

export type QueryTweetsArgs = {
  limit?: Maybe<Scalars["Int"]>;
  skip?: Maybe<Scalars["Int"]>;
  sort_field?: Maybe<Scalars["String"]>;
  sort_order?: Maybe<Scalars["String"]>;
};

export type QueryUserArgs = {
  id: Scalars["ID"];
};

export type QueryNotificationsArgs = {
  limit?: Maybe<Scalars["Int"]>;
};

export type Stat = {
  __typename?: "Stat";
  views?: Maybe<Scalars["Int"]>;
  likes?: Maybe<Scalars["Int"]>;
  retweets?: Maybe<Scalars["Int"]>;
  responses?: Maybe<Scalars["Int"]>;
};

export type Subscription = {
  __typename?: "Subscription";
  commentAdded?: Maybe<Comment>;
};

export type SubscriptionCommentAddedArgs = {
  repoFullName: Scalars["String"];
};

export type Tweet = {
  __typename?: "Tweet";
  id: Scalars["ID"];
  body?: Maybe<Scalars["String"]>;
  date?: Maybe<Scalars["String"]>;
  Author?: Maybe<User>;
  Stats?: Maybe<Stat>;
};

export type User = {
  __typename?: "User";
  id: Scalars["ID"];
  username?: Maybe<Scalars["String"]>;
  first_name?: Maybe<Scalars["String"]>;
  last_name?: Maybe<Scalars["String"]>;
  full_name?: Maybe<Scalars["String"]>;
  name?: Maybe<Scalars["String"]>;
  avatar_url?: Maybe<Scalars["String"]>;
};
export type TweetMetaQueryVariables = {};

export type TweetMetaQuery = { __typename?: "Query" } & {
  TweetsMeta: Maybe<{ __typename?: "Meta" } & Pick<Meta, "count">>;
};

export type TweetQueryVariables = {
  id: Scalars["ID"];
};

export type TweetQuery = { __typename?: "Query" } & {
  Tweet: Maybe<
    { __typename?: "Tweet" } & Pick<Tweet, "body" | "date"> & {
        Author: Maybe<{ __typename?: "User" } & Pick<User, "full_name">>;
      }
  >;
};

export type CreateTweetMutationVariables = {
  body?: Maybe<Scalars["String"]>;
};

export type CreateTweetMutation = { __typename?: "Mutation" } & {
  createTweet: Maybe<{ __typename?: "Tweet" } & Pick<Tweet, "id">>;
};

export type SubscCommentSubscriptionVariables = {
  repoFullName: Scalars["String"];
};

export type SubscCommentSubscription = { __typename?: "Subscription" } & {
  commentAdded: Maybe<
    { __typename?: "Comment" } & Pick<Comment, "id" | "content">
  >;
};

オレオレプラグインを書く

graphql_codegenのプラグインを自分で書こう。returntsの文字列さえ吐ければ良い。

参考: Write your first Plugin · GraphQL Code Generator

プラグイン名.js
module.exports = {
  plugin: (schema, documents, config) => {
    //graphql_codegenがかき集めた、おおよそ人が読むようにできていない、documentsを読み込みながら、jsでts(文字列)を書く。
    //ここでtsをreturnする。
    //出力は勝手にgraphql_codegenがprettierが走らせて整形してくれるので、改行とかタブとか気にせず書く。
  }
};

自分が作ったプラグインの読み込み

codegen.yaml
overwrite: true
schema: "schema.graphql" # サーバのアドレス。勝手にスキーマを呼んでくれる
documents: "queries/*.graphql" # 監視対象 ここのファイルに更新があれば自動的に型を作成
generates: # 生成先
  ./generated/types.ts:
    plugins:
      - typescript
      - typescript-operations
  ./generated/nodes.ts: # 生成先
    plugins:
      - typescript-document-nodes
  ./generated/class.ts: # 生成先
      - プラグイン名.js # プラグイン読み込み

プラグインを書くにあたっては、キャメルケースなりを変換してくれるchange-caseパッケージが助かった。

結局何を書いたか?

結局こう言うコードを書いた。

type-apollo-class.js
var path = require("path");
var changeCase = require("change-case");

const imp = `
    import * as Type from "./types";
    import * as Node from "./nodes";
    import * as ApolloType from "apollo-client";
    import ApolloClient from "apollo-client";
    export interface ClientClass {readonly client: ApolloClient<any>;}        
`;

function makeClassHeader(basename) {
  return `
    export class ${changeCase.pascalCase(
      basename
    )}Client implements ClientClass{
        constructor(readonly client: ApolloClient<any>) {}          
    `;
}

function makeClassMethods(operations) {
  return operations.map(e => {
    const camelName = changeCase.camelCase(e.name);
    const pascalName = changeCase.pascalCase(e.name);
    const pascalOperation = changeCase.pascalCase(e.operation);

    const queryType = `Type.${pascalName + pascalOperation}`;
    const variableType = `Type.${pascalName + pascalOperation + "Variables"}`;
    const optionType = getOptionName(e.operation, queryType, variableType);

    return `
    ${camelName} = (options?:Omit<${optionType},"${operationQueryName[e.operation]}">) => 
        this.client.${operationName[e.operation]}<${queryType},${variableType}>
        ({...options,...{${operationQueryName[e.operation]}:Node.${pascalName}}})    
    `;
  });
}

const operationName = {
  query: "query",
  mutation: "mutate",
  subscription: "subscribe"
};

function getOptionName(operation, query, variable) {
  switch (operation) {
    case "query":
      return `ApolloType.QueryOptions<${variable}>`;
    case "mutation":
      return `ApolloType.MutationOptions<${query},${variable}>`;
    case "subscription":
      return `ApolloType.SubscriptionOptions<${variable}>`;
  }
}

const operationQueryName = {
    query: "query",
    mutation: "mutation",
    subscription: "query"
};

module.exports = {
  plugin: (schema, documents, config) => {
    const classes = documents
      .map(doc => {
        const filePath = doc.filePath;
        const baseName = path.basename(filePath, path.extname(filePath));

        const classHeader = makeClassHeader(baseName);

        const definitions = doc.content.definitions;
        const operations = definitions.map(e => ({
          operation: e.operation,
          name: e.name.value
        }));
        const methods = makeClassMethods(operations);

        return [classHeader, methods.join("\n"), `}`].join("\n");
      })
      .join("\n");

    return [imp, classes].join("\n");
  }
};

そして設定ファイルをこう書いた

codegen.yaml
overwrite: true
schema: "schema.graphql" # サーバのアドレス。勝手にスキーマを呼んでくれる
documents: "queries/*.graphql" # 監視対象 ここのファイルに更新があれば自動的に型を作成
generates:
  ./generated/types.ts:
    plugins:
      - typescript
      - typescript-operations
  ./generated/nodes.ts:
    plugins:
      - typescript-document-nodes
  ./generated/class.ts:
      - type-apollo-class.js

ごちゃっとしているけど、余は満足。

できた奴

https://github.com/NanimonoDemonai/graphql-query-apollo-client-code-sample/tree/master

まとめ

  • GraphQL楽しい
  • JSでTS(文字列)を書くと楽しい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

graphql-codegenのプラグインを自分で書いた ~ 副題:「GraphQLのクエリーを書けば、上手に型をつけてApolloClientでクエリを飛ばすやつを自動で生成したいと思ったので、Typescriptを書き出すJavascriptを書いた」

概要

GraphQLクエリを書いたら、graphql-codegenが型を吐いてくれるけど、型をいちいち書くのもラクしたかったので、graphql-codegenのプラグインを自分で書いた。

できた奴

1.スキーマの例

こんなスキーマがあるとして、

schema.graphql
type Tweet {
  id: ID!
  body: String
  date: String
  Author: User
  Stats: Stat
}

type User {
  id: ID!
  username: String
  first_name: String
  last_name: String
  full_name: String
  name: String @deprecated
  avatar_url: String
}

type Stat {
  views: Int
  likes: Int
  retweets: Int
  responses: Int
}

type Notification {
  id: ID
  date: String
  type: String
}

type Meta {
  count: Int
}

type Comment {
  id: String
  content: String
}

type Query {
  Tweet(id: ID!): Tweet
  Tweets(limit: Int, skip: Int, sort_field: String, sort_order: String): [Tweet]
  TweetsMeta: Meta
  User(id: ID!): User
  Notifications(limit: Int): [Notification]
  NotificationsMeta: Meta
}

type Mutation {
  createTweet(body: String): Tweet
  deleteTweet(id: ID!): Tweet
  markTweetRead(id: ID!): Boolean
}

type Subscription {
  commentAdded(repoFullName: String!): Comment
}

参考: https://github.com/marmelab/GraphQL-example/blob/master/schema.graphql

2.クエリの例

こんなクエリを書いたgraphqlファイルを作るとする。

tweet.graphql
query TweetMeta {
  TweetsMeta {
    count
  }
}

query Tweet($id: ID!) {
  Tweet(id: $id) {
    body
    date
    Author {
      full_name
    }
  }
}

mutation CreateTweet($body: String) {
  createTweet(body: $body) {
    id
  }
}

subscription SubscComment($repoFullName: String!) {
  commentAdded(repoFullName: $repoFullName) {
    id
    content
  }
}

3. 生成例

するとこんな感じにtsを吐いてくれる。(監視対象の.graphqlごとにクラスを生やしてくれる)

generated/class.ts
import * as Type from "./types";
import * as Node from "./nodes";
import * as ApolloType from "apollo-client";
import ApolloClient from "apollo-client";
export interface ClientClass {
  readonly client: ApolloClient<any>;
}

export class TweetClient implements ClientClass {
  constructor(readonly client: ApolloClient<any>) {}

  tweetMeta = (
    options?: Omit<
      ApolloType.QueryOptions<Type.TweetMetaQueryVariables>,
      "query"
    >
  ) =>
    this.client.query<Type.TweetMetaQuery, Type.TweetMetaQueryVariables>({
      ...options,
      ...{ query: Node.TweetMeta }
    });

  tweet = (
    options?: Omit<ApolloType.QueryOptions<Type.TweetQueryVariables>, "query">
  ) =>
    this.client.query<Type.TweetQuery, Type.TweetQueryVariables>({
      ...options,
      ...{ query: Node.Tweet }
    });

  createTweet = (
    options?: Omit<
      ApolloType.MutationOptions<
        Type.CreateTweetMutation,
        Type.CreateTweetMutationVariables
      >,
      "mutation"
    >
  ) =>
    this.client.mutate<
      Type.CreateTweetMutation,
      Type.CreateTweetMutationVariables
    >({ ...options, ...{ mutation: Node.CreateTweet } });

  subscComment = (
    options?: Omit<
      ApolloType.SubscriptionOptions<Type.SubscCommentSubscriptionVariables>,
      "query"
    >
  ) =>
    this.client.subscribe<
      Type.SubscCommentSubscription,
      Type.SubscCommentSubscriptionVariables
    >({ ...options, ...{ query: Node.SubscComment } });
}

4. 使い道

gqlタグとか書かずに、補完バリバリで気持ちよく書ける。嬉しい。

main.ts
import { TweetClient } from "./generated/class";
import ApolloClient from "apollo-boost";
import "isomorphic-fetch";

const client = new TweetClient(
  new ApolloClient({ uri: "http://localhost:4000/" })
);

async function main() {
  const hoge = await client.tweetMeta();
  console.log(JSON.stringify(hoge.data.TweetsMeta));

  const huga = await client.createTweet({
    variables: {
      body: "aaa"
    }
  });
  //dataはnullチェックしないと怒られる
  console.log(JSON.stringify(huga.data && huga.data.createTweet));

  const piyo = await client.tweet({ variables: { id: "hoga" } });
  console.log(JSON.stringify(piyo.data));
}

main();

作り方

環境

yarnなり、npmでよしなに入れる。

package.json
{
  "scripts": {
    "codegen": "graphql-codegen --config codegen.yml --watch",
    "server": "node server.js"
  },
  "devDependencies": {
    "@graphql-codegen/cli": "^1.3.1",
    "@graphql-codegen/typescript": "^1.3.1",
    "@graphql-codegen/typescript-document-nodes": "^1.3.1-alpha-21fe4751.62",
    "@graphql-codegen/typescript-operations": "1.3.1",
    "@types/graphql": "^14.2.2",
    "apollo-client": "^2.6.3",
    "change-case": "^3.1.0",
    "graphql": "^14.4.2",
    "prettier": "^1.18.2",
    "typescript": "^3.5.2"
  },
  "dependencies": {
    "apollo-boost": "^0.4.3",
    "apollo-server": "^2.6.7",
    "isomorphic-fetch": "^2.2.1"
  }
}

適当にモックサーバを立てる。

ApolloServerのmockをtrueにすれば適当にサーバが立つ。

モックサーバーの立て方の例
server.js
const { ApolloServer, gql } = require("apollo-server");

const typeDefs = `
type Tweet {
  id: ID!
  body: String
  date: String
  Author: User
  Stats: Stat
}

type User {
  id: ID!
  username: String
  first_name: String
  last_name: String
  full_name: String
  name: String @deprecated
  avatar_url: String
}

type Stat {
  views: Int
  likes: Int
  retweets: Int
  responses: Int
}

type Notification {
  id: ID
  date: String
  type: String
}

type Meta {
  count: Int
}

type Comment {
  id: String
  content: String
}

type Query {
  Tweet(id: ID!): Tweet
  Tweets(limit: Int, skip: Int, sort_field: String, sort_order: String): [Tweet]
  TweetsMeta: Meta
  User(id: ID!): User
  Notifications(limit: Int): [Notification]
  NotificationsMeta: Meta
}

type Mutation {
  createTweet(body: String): Tweet
  deleteTweet(id: ID!): Tweet
  markTweetRead(id: ID!): Boolean
}

type Subscription {
  commentAdded(repoFullName: String!): Comment
}

`;

const server = new ApolloServer({
  typeDefs,
  mocks: true
});

server.listen().then(({ url }) => {
  console.log(`Server ready at ${url}`);
});

起動

terminal
yarn server

Graphql-codegenを走らす

graphql-codegenの設定ファイルを書く。

codegen.yaml
overwrite: true
schema: "http://localhost:4000" # サーバのアドレス。勝手にスキーマを呼んでくれる
documents: "queries/*.graphql" # 監視対象 ここのファイルに更新があれば自動的に型を作成
generates:
  ./generated/types.ts:
    plugins:
      - typescript
      - typescript-operations
  ./generated/nodes.ts:
    plugins:
      - typescript-document-nodes

監視対象として、さっきの例のtweet.graphqlqueries/に作っておく。

そしてgraphql-codegenを走らせる。

terminal
yarn codegen

すると./generated/types.tsに型を吐くし、./generated/nodes.tsにクエリにgqlタグ付けた定数が吐かれる。また、これらはqueriesのファイルの変化に応じて逐次更新される。

なお、"codegen": "graphql-codegen --config codegen.yml --watch"からwatchオプションを外せば、自動更新は無効になる。

ts:generated/nodes.ts(自動生成)
generated/nodes.ts(自動生成)
import { DocumentNode } from "graphql";
import gql from "graphql-tag";

export const TweetMeta: DocumentNode = gql`
  query TweetMeta {
    TweetsMeta {
      count
    }
  }
`;

export const Tweet: DocumentNode = gql`
  query Tweet($id: ID!) {
    Tweet(id: $id) {
      body
      date
      Author {
        full_name
      }
    }
  }
`;

export const CreateTweet: DocumentNode = gql`
  mutation CreateTweet($body: String) {
    createTweet(body: $body) {
      id
    }
  }
`;

export const SubscComment: DocumentNode = gql`
  subscription SubscComment($repoFullName: String!) {
    commentAdded(repoFullName: $repoFullName) {
      id
      content
    }
  }
`;

ts:generated/types.ts(自動生成)
generated/types.ts(自動生成)
export type Maybe<T> = T | null;
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
  Upload: any;
};

export enum CacheControlScope {
  Public = "PUBLIC",
  Private = "PRIVATE"
}

export type Comment = {
  __typename?: "Comment";
  id?: Maybe<Scalars["String"]>;
  content?: Maybe<Scalars["String"]>;
};

export type Meta = {
  __typename?: "Meta";
  count?: Maybe<Scalars["Int"]>;
};

export type Mutation = {
  __typename?: "Mutation";
  createTweet?: Maybe<Tweet>;
  deleteTweet?: Maybe<Tweet>;
  markTweetRead?: Maybe<Scalars["Boolean"]>;
};

export type MutationCreateTweetArgs = {
  body?: Maybe<Scalars["String"]>;
};

export type MutationDeleteTweetArgs = {
  id: Scalars["ID"];
};

export type MutationMarkTweetReadArgs = {
  id: Scalars["ID"];
};

export type Notification = {
  __typename?: "Notification";
  id?: Maybe<Scalars["ID"]>;
  date?: Maybe<Scalars["String"]>;
  type?: Maybe<Scalars["String"]>;
};

export type Query = {
  __typename?: "Query";
  Tweet?: Maybe<Tweet>;
  Tweets?: Maybe<Array<Maybe<Tweet>>>;
  TweetsMeta?: Maybe<Meta>;
  User?: Maybe<User>;
  Notifications?: Maybe<Array<Maybe<Notification>>>;
  NotificationsMeta?: Maybe<Meta>;
};

export type QueryTweetArgs = {
  id: Scalars["ID"];
};

export type QueryTweetsArgs = {
  limit?: Maybe<Scalars["Int"]>;
  skip?: Maybe<Scalars["Int"]>;
  sort_field?: Maybe<Scalars["String"]>;
  sort_order?: Maybe<Scalars["String"]>;
};

export type QueryUserArgs = {
  id: Scalars["ID"];
};

export type QueryNotificationsArgs = {
  limit?: Maybe<Scalars["Int"]>;
};

export type Stat = {
  __typename?: "Stat";
  views?: Maybe<Scalars["Int"]>;
  likes?: Maybe<Scalars["Int"]>;
  retweets?: Maybe<Scalars["Int"]>;
  responses?: Maybe<Scalars["Int"]>;
};

export type Subscription = {
  __typename?: "Subscription";
  commentAdded?: Maybe<Comment>;
};

export type SubscriptionCommentAddedArgs = {
  repoFullName: Scalars["String"];
};

export type Tweet = {
  __typename?: "Tweet";
  id: Scalars["ID"];
  body?: Maybe<Scalars["String"]>;
  date?: Maybe<Scalars["String"]>;
  Author?: Maybe<User>;
  Stats?: Maybe<Stat>;
};

export type User = {
  __typename?: "User";
  id: Scalars["ID"];
  username?: Maybe<Scalars["String"]>;
  first_name?: Maybe<Scalars["String"]>;
  last_name?: Maybe<Scalars["String"]>;
  full_name?: Maybe<Scalars["String"]>;
  name?: Maybe<Scalars["String"]>;
  avatar_url?: Maybe<Scalars["String"]>;
};
export type TweetMetaQueryVariables = {};

export type TweetMetaQuery = { __typename?: "Query" } & {
  TweetsMeta: Maybe<{ __typename?: "Meta" } & Pick<Meta, "count">>;
};

export type TweetQueryVariables = {
  id: Scalars["ID"];
};

export type TweetQuery = { __typename?: "Query" } & {
  Tweet: Maybe<
    { __typename?: "Tweet" } & Pick<Tweet, "body" | "date"> & {
        Author: Maybe<{ __typename?: "User" } & Pick<User, "full_name">>;
      }
  >;
};

export type CreateTweetMutationVariables = {
  body?: Maybe<Scalars["String"]>;
};

export type CreateTweetMutation = { __typename?: "Mutation" } & {
  createTweet: Maybe<{ __typename?: "Tweet" } & Pick<Tweet, "id">>;
};

export type SubscCommentSubscriptionVariables = {
  repoFullName: Scalars["String"];
};

export type SubscCommentSubscription = { __typename?: "Subscription" } & {
  commentAdded: Maybe<
    { __typename?: "Comment" } & Pick<Comment, "id" | "content">
  >;
};

オレオレプラグインを書く

graphql_codegenのプラグインを自分で書こう。returntsの文字列さえ吐ければ良い。

参考: Write your first Plugin · GraphQL Code Generator

プラグイン名.js
module.exports = {
  plugin: (schema, documents, config) => {
    //graphql_codegenがかき集めた、おおよそ人が読むようにできていない、documentsを読み込みながら、jsでts(文字列)を書く。
    //ここでtsをreturnする。
    //出力は勝手にgraphql_codegenがprettierが走らせて整形してくれるので、改行とかタブとか気にせず書く。
  }
};

自分が作ったプラグインの読み込み

codegen.yaml
overwrite: true
schema: "schema.graphql" # サーバのアドレス。勝手にスキーマを呼んでくれる
documents: "queries/*.graphql" # 監視対象 ここのファイルに更新があれば自動的に型を作成
generates: # 生成先
  ./generated/types.ts:
    plugins:
      - typescript
      - typescript-operations
  ./generated/nodes.ts: # 生成先
    plugins:
      - typescript-document-nodes
  ./generated/class.ts: # 生成先
      - プラグイン名.js # プラグイン読み込み

プラグインを書くにあたっては、キャメルケースなりを変換してくれるchange-caseパッケージが助かった。

結局何を書いたか?

結局こう言うコードを書いた。

type-apollo-class.js
var path = require("path");
var changeCase = require("change-case");

const imp = `
    import * as Type from "./types";
    import * as Node from "./nodes";
    import * as ApolloType from "apollo-client";
    import ApolloClient from "apollo-client";
    export interface ClientClass {readonly client: ApolloClient<any>;}        
`;

function makeClassHeader(basename) {
  return `
    export class ${changeCase.pascalCase(
      basename
    )}Client implements ClientClass{
        constructor(readonly client: ApolloClient<any>) {}          
    `;
}

function makeClassMethods(operations) {
  return operations.map(e => {
    const camelName = changeCase.camelCase(e.name);
    const pascalName = changeCase.pascalCase(e.name);
    const pascalOperation = changeCase.pascalCase(e.operation);

    const queryType = `Type.${pascalName + pascalOperation}`;
    const variableType = `Type.${pascalName + pascalOperation + "Variables"}`;
    const optionType = getOptionName(e.operation, queryType, variableType);

    return `
    ${camelName} = (options?:Omit<${optionType},"${operationQueryName[e.operation]}">) => 
        this.client.${operationName[e.operation]}<${queryType},${variableType}>
        ({...options,...{${operationQueryName[e.operation]}:Node.${pascalName}}})    
    `;
  });
}

const operationName = {
  query: "query",
  mutation: "mutate",
  subscription: "subscribe"
};

function getOptionName(operation, query, variable) {
  switch (operation) {
    case "query":
      return `ApolloType.QueryOptions<${variable}>`;
    case "mutation":
      return `ApolloType.MutationOptions<${query},${variable}>`;
    case "subscription":
      return `ApolloType.SubscriptionOptions<${variable}>`;
  }
}

const operationQueryName = {
    query: "query",
    mutation: "mutation",
    subscription: "query"
};

module.exports = {
  plugin: (schema, documents, config) => {
    const classes = documents
      .map(doc => {
        const filePath = doc.filePath;
        const baseName = path.basename(filePath, path.extname(filePath));

        const classHeader = makeClassHeader(baseName);

        const definitions = doc.content.definitions;
        const operations = definitions.map(e => ({
          operation: e.operation,
          name: e.name.value
        }));
        const methods = makeClassMethods(operations);

        return [classHeader, methods.join("\n"), `}`].join("\n");
      })
      .join("\n");

    return [imp, classes].join("\n");
  }
};

そして設定ファイルをこう書いた

codegen.yaml
overwrite: true
schema: "schema.graphql" # サーバのアドレス。勝手にスキーマを呼んでくれる
documents: "queries/*.graphql" # 監視対象 ここのファイルに更新があれば自動的に型を作成
generates:
  ./generated/types.ts:
    plugins:
      - typescript
      - typescript-operations
  ./generated/nodes.ts:
    plugins:
      - typescript-document-nodes
  ./generated/class.ts:
      - type-apollo-class.js

ごちゃっとしているけど、余は満足。

できた奴

https://github.com/NanimonoDemonai/graphql-query-apollo-client-code-sample/tree/master

まとめ

  • GraphQL楽しい
  • JSでTS(文字列)を書くと楽しい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vuexをモックしてシンプルに単体テストを行う。〜With Jest〜

はじめに

肥大化していく複雑さに対抗しうるはテストの存在。
Vuexにてテストを欲するはmutationsとactions。
テストフレームワークには手に馴染むJest
いざ書かん。

Failure.test.js
import store from '@pass/to/store'

describe('Test Vuex module', () => {
  /**
   * store内部では
   * state = {
   *  items: []
   * }
   * で初期化されています。
   */

  test('itemsでstate.itemsを初期化', () => {
    const items = [
      { id: 1, name: 'hoge' }
    ]
    store.commit('models/User/setItems', items)
    expect(store.state.items).toEqual(items) //pass
  })

  test('state.itemに要素を追加', () => {
    const item = { id: 1, name: 'hoge' }
    store.commit('models/User/addItem', item)
    expect(store.state.items.length).toBe(1) //failure, received 2
  })
})

そして敗れる我がテスト。

上記のテストでは1つ目のテストは成功するものの、2つ目のテストでは配列の要素数が期待どおりではなく失敗しています。
Vuex自体の動作としては望ましいですが、stateの状態をテストスイート全体で追いかけるのは不便で面倒です。
公式のチュートリアルではmutationやactionを個別でインポートし、stateをモックすることでテストを行っています。)

テストはそれ自体、書きやすくあってほしいものです。

そこで本記事ではVuexにまつわる諸々をモック化し、mutationやactionのテストが楽になるような方法を提案します。
本記事で使用したコードはここにあるので気になった方は見てみてください。

モック後のテストコードのイメージはこんな感じです。

mockedVuex.test.js
describe('Test your Vuex module', () => {

  test('mutationでstateを初期化', () => {
    const items = [
      { id: 1, name: 'hoge' }
    ]
    const store = new MockedVuexStore(state, mutations, actions)
    store.commit('setItems', items)
    expect(store.state.items).toEqual(items)
  })

  test('非同期なactionで要素を追加', () => {
    const item = { id: 1, name: 'hoge' }

    const store = new MockedVuexStore(state, mutations, actions)

    store.dispatch('add', item).then(res => {
      expect(store.state.items.length).toBe(1)
    })
  })
})

mutation、action、同期、非同期関わらず割と見通しの良いテストコードではないでしょうか。

さっそく書いていきます。

Vuex.StoreクラスをモックしたものをここではMockedVuexStoreクラスとします。

MockedVuexStoreクラスに期待するは、
- テストケース毎に独立したStateを持てる
- commit(mutationName, payload)メソッドからmutationを発火させる
- dispatch(actionName, payload)メソッドからactionを発火させる

てところでしょうか。

下記がクラス本文です。

mocks.js
export class MockedVuexStore {
  constructor(state, mutations, actions) {
    this.state = state
    this.mutations = {
      ...mutations
    }
    this.actions = {
      ...actions
    }
  }

  commit(type, payload = null) {
    try {
      this.mutations[type](this.state, payload)
    } catch (e) {
      if (!e instanceof TypeError) {
        throw e
      }
      console.log(`unknown mutation "${type}" is called`)
    }
  }

  dispatch(type, payload = null) {
    try {
      return this.actions[type](
        {
          commit: (type, payload) => this.commit(type, payload)
        },
        payload
      )
    } catch (e) {
      if (!e instanceof TypeError) {
        throw e
      }
      console.log(`unknown action "${type}" is called`)
    }
  }
}

コンストラクタの引数としてstate, mutations, actionsを外部から受け取っております。
- stateは、テストケース内で定義されたもの
- mutationsは、テスト対象モジュールのmutation
- actionsは、テスト対象モジュールのaction
です。

dispatchメソッド内の

return this.actions[type](
  {
    commit: (type, payload) => this.commit(type, payload)
  },
  payload
)

ですが、呼び出し先のactionにおいてgetters,dispatch,stateが使用される場合は、それらについて適宜モックする必要があります。(今回は言及しません。)

ともあれこれで私たちはシンプルにモック化されたVuex.Storeを手に入れることができました。

テスト書きます。

肥大化していく複雑さに対抗しうるはテストの存在。
Vuexにてテストを欲するはmutationsとactions。
テストフレームワークには手に馴染むJest
モックされるはVuex.Store
いざ書かん。

Success.test.js
import $axios from '../store/HttpClient'
import { mutations, actions } from '../store/Models/User'
import { MockedVuexStore } from './mocks'

/* axiosをモック */
jest.mock('../store/HttpClient')

describe('Test your Vuex module', () => {

  test('mutationでstateを初期化', () => {
    const state = {
      items: []
    }

    const items = [
      { id: 1, name: 'hoge' }
    ]

    const store = new MockedVuexStore(state, mutations, actions)
    store.commit('setItems', items)

    expect(store.state.items).toEqual(items)
  })

  test('非同期なactionで要素を追加', () => {
    const state = {
      items: []
    }

    const item = { id: 1, name: 'hoge' }

    /* axiosのメソッドをモック */
    $axios.post.mockResolvedValue({
      data: item,
      status: 200
    })

    const store = new MockedVuexStore(state, mutations, actions)

    store.dispatch('add', item).then(res => {
      expect(store.state.items.length).toBe(1)
    })
  })
})

そして成功する我がテスト。

なお、外部のAPIを叩くのに使っているaxiosはJestのMock Functions でモックしています。

import $axios from '../store/HttpClient'

/* axiosをモック */
jest.mock('../store/HttpClient')

/* axiosのpostメソッドをモック */
$axios.post.mockResolvedValue({
  data: item,
  status: 200
})

その他もろもろ

ディレクトリ構成
.
├── babel.config.js
├── jest.config.js
├── node_modules
├── package.json
├── store
│   ├── HttpClient.js
│   ├── Models
│   │   ├── User.js
│   │   └── index.js
│   └── index.js
├── tests
│   ├── Failure.test.js
│   ├── Success.test.js
│   ├── User.test.js
│   └── mocks.js
└── yarn.lock

テスト対象のUserストア
./store/Models/User.js
import $axios from '../HttpClient'

const state = {
  items: []
}

const getters = {}

export const mutations = {
  setItems(state, data) {
    state.items = data
  },

  addItem(state, data) {
    state.items.push(data)
  },

  updateItem(state, data) {
    const index = state.items.findIndex(item => item.id == data.id)
    Object.assign(state.items[index], data)
  },

  deleteItem(state, id) {
    const index = state.items.findIndex(item => item.id == id)
    state.items.splice(index, 1)
  }
}

export const actions = {
  async fetch(context) {
    const response = await $axios.get('/api/users')
    context.commit('setItems', response.data)
    return 'OK'
  },

  async add(context, data) {
    const response = await $axios.post(`/api/users/add`, data)
    context.commit('addItem', response.data)
    return 'OK'
  },

  async edit(context, data) {
    const response = await $axios.post(`/api/users/${data.id}/edit`, data)
    context.commit('updateItem', response.data)
    return 'OK'
  },

  async delete(context, id) {
    await $axios.delete(`/api/users/${id}/delete`)
    context.commit('deleteItem', id)
    return false
  }
}

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
}

HTTP通信用のaxiosインスタンス
./store/HttpClient.js
import axios from 'axios'

let baseURL = '/'
const $axios = axios.create({
  headers: {
    'X-Requested-With': 'XMLHttpRequest'
  },
  baseURL: baseURL
})

export default $axios

終わりに

なるべくいつものVuexに近い形でテストが実行できるようにモックしてみました。
今後の課題としては、
- {root: true}なcommitやdispatchへの対応
- TypeScriptによる型安全な開発

あたりを頑張ればより複雑なケースに対しても対応できるのかなあとと思います。
本記事で使用したコードはここにあります。

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

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

MaterializecssのCarouselを使用して、3秒ごとに画像が自動で切り替わるページを作る

概要

TECH::EXPERTのカリキュラムでオリジナルのミニアプリを作成する機会があり、
その一部のページでMaterializecssのCarouselを使用し、3秒ごとに画像が切り替わるページを作成したので紹介します。

MaterializecssのCarouselとは

画像をくるくると回せる機能です。
Image from Gyazo
https://materializecss.com/carousel.html

自分が作成したページ紹介

Image from Gyazo

作成する前提

MaterializecssがCDNで読み込めている

編集するファイル

・ビューファイル
・CSSファイル
・jsファイル

ビューファイル

about.html.erb
<section class="about-main" >
  <div class="carousel carousel-slider" data-indicators="true" id="big3" >
    <div class="carousel-fixed-item">
      <div class="container">
        <h1 class="white-text">Work Hard See Result</h1>
      <% if user_signed_in? %>
        <a class="btn waves-effect white black-text darken-text-2" href="/" target="_blank">HOME</a>
      <%else%>
        <a class="btn waves-effect white black-text darken-text-2" href="/users/sign_in" target="_blank">Log in</a>
      <%end%>
      </div>
    </div>
    <div class="carousel-item big3pic" id="benchpress">
      <div class="container">
        <h3 class="white-text">Bench Press</h3>
        <p class="white-text">chest</p>
      </div>
    </div>
    <div class="carousel-item big3pic" id="deadlift">
      <div class="container">
        <h3 class="white-text">Dead Lift</h3>
        <p class="white-text">back</p>
      </div>
    </div>
    <div class="carousel-item big3pic"  id="squat">
      <div class="container">
        <h3 class="white-text">Squat</h3>
        <p class="white-text">legs</p>
      </div>
    </div>
  </div>
</section>

"carousel carousel-slider" を用いました。
"carousel-fixed-item"は固定して表示したいものを書きます。
"carousel carousel-item"の部分がそれぞれの画像のクラスです。

CSSファイル

style.css
#big3{
  height: 100vh;
}
#benchpress{
  background-image: url('benchpress.jpg');
  background-size: 100%;
}
#deadlift{
  background-image: url('deadlift.jpg');
  background-size: 100%;
}
#squat{
  background-image: url('squat.jpg');
  background-size: 100%;
}

それぞれidを付与してるので、idによって画像を変更してください。

jsファイル

about.js
$(document).ready(function(){
  $('#big3').carousel(
  {
    dist: 0,
    padding: 0,
    fullWidth: true,
    indicators: true,
    duration: 100,
  }
  );
  autoplay()
  function autoplay() {
      $('#big3').carousel('next');
      autoplay: true,
      setTimeout(autoplay, 3000);
  }
});

autoplayの関数を定義してます。
id=big3に対して3000ミリ秒で次のcarousel-itemを
表示するように設定してます。

オプションは下記を参照しました。

  • [1] dist: 0, => 遠近ズーム0
  • [2] padding: 0, => 中央以外の項目の余白0
  • [3] fullWidth: true, => carouselを全幅のスライダーへ
  • [4] indicators: true, => インジケータを表示
  • [5] duration: 100, => 次のスライドへ移動しきるまでの時間100ミリ秒

Image from Gyazo

最後に

この記事を書いた目的

・自分なりに工夫した点をアウトプットして、理解を深める。
・あわよくば有識者にフィードバックをもらいたい。
・私と同じ初学者からも奇譚のない意見をもらいたい。(自分だったらどうこうする的な)

筆者について

TECH::EXPERTにて4月27日より52期夜間・休日コースでruby/railsを学習している未経験エンジニアです。
ご不備等ありましたら、ご指摘ください。ちなみに本記事が初投稿になります。
言わずもがなかもしれませんが、趣味はボディメイク・筋トレでございます。
余談ですが、120kg⇨66kgまで減量して大会出場した経験があり
ダイエットについての質問はなんでも答えられるかと思います:muscle:

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

【Nuxt.js】layoutsに設定をまとめつつ、タイトルはpagesから設定したい

これって、Qiita記事の種になりませんか?

つまり、こういうことになります

  • /layoutsにヘッダーのコンポーネントを含めつつ
  • /pagesのファイルから/layoutsで使用しているコンポーネントの値を設定する(値を渡す)

基本構成

  • レイアウト内でヘッダーコンポーネントを読み込み、表示している。
  • 上記レイアウトを適用してpagesを表示。
  • layout(components) > pagesの親子関係

フォルダ

/components
  - Header.vue 
/layouts
  - ore.vue
/pages
  - index.vue 

ファイル

components/Header.vue
<template>
  <h1>{{ title }}</h1>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      default: ''
    }
  }
}
</script>
layouts/ore.vue
<template>
  <div>
    <Header />
    <nuxt />
  </div>
</template>

<script>
import Header from '@/components/Header'

export default {
  components: {
    Header
  }
}
</script>
pages/index.vue
<template>
  <div>
    abc
  </div>
</template>

<script>
export default {
  name: 'sample',
  layout: 'ore'
}
</script>

方法

1. page側から$emitでlayout側に値を渡す。

  1. dataに値を定義。
  2. methodsに$emitで渡すメソッドを定義。
  3. mountedのタイミングで2のメソッドを実行。
pages/index.vue
<template>
  <div>
    abc
  </div>
</template>

<script>
export default {
  name: 'sample',
  layout: 'ore',
  data(){
    return {
      header: {
        title: 'ページタイトル'
      }
    }
  },
  mounted() {
    this.updateHeader()
  },
  methods: {
    updateHeader() {
      // タイトルとして使いたい情報を渡す
      $nuxt.$emit('updateHeader', this.header.title)
      // thisはあってもなくてもOKだった
      // this.$nuxt.$emit('updateHeader', this.header.title)
    }
  }
}
</script>

2. layout側でイベントを受け取るようにする

  • dataにタイトルの初期値を定義。
  • createdのタイミングでイベントリスナーを設定。$nuxt.onで定義する。
    • 要素に@eventNameの形式で記述する形だとmounted以後にしか設定されない。
    • そのため、mounted直後の子要素のイベント発火を検知できなかった。
  • イベントを検知した後、タイトルを書き換える処理を追加。
layouts/ore.vue
<template>
  <div>
    <Header />
    <nuxt />
  </div>
</template>

<script>
import Header from '@/components/Header'

export default {
  components: {
    Header
  },
data() {
    return {
      title: ''
    }
  },
  created() {
    this.setListener()
  },
  methods: {
    setListener() {
      // emitで発火させたイベント名にする
      // こちらはthisがないとダメ
      this.$nuxt.$on('updateHeader', this.setHeader)
    },
    setHeader(title) {
      // 第1引数にはemitで渡した値が入ってくる。
      // 第2引数以降を渡す場合も同様に、それ以降の引数で受け取れる
      this.title = title || ''
    }
  }
}
</script>

3.受け取った値を反映させる

  • レイアウトのHeaderコンポーネントに対し、:propsの名前="dataで定義している値" の記述を追加する
layouts/ore.vue
<template>
  <div>
    <Header :title="title" />
    <nuxt />
  </div>
</template>

...略

最終的なファイルの状態

※コンポーネントは最初と変化なし

components/Header.vue
<template>
  <h1>{{ title }}</h1>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      default: ''
    }
  }
}
</script>
layouts/ore.vue
<template>
  <div>
    <Header :title="title" />
    <nuxt />
  </div>
</template>

<script>
import Header from '@/components/Header'

export default {
  components: {
    Header
  },
data() {
    return {
      title: ''
    }
  },
  created() {
    this.setListener()
  },
  methods: {
    setListener() {
      this.$nuxt.$on('updateHeader', this.setHeader)
    },
    setHeader(title) {
      this.title = title || ''
    }
  }
}
</script>
pages/index.vue
<template>
  <div>
    abc
  </div>
</template>

<script>
export default {
  name: 'sample',
  layout: 'ore',
  data(){
    return {
      header: {
        title: 'ページタイトル'
      }
    }
  },
  mounted() {
    this.updateHeader()
  },
  methods: {
    updateHeader() {
      $nuxt.$emit('updateHeader', this.header.title)
    }
  }
}
</script>

まとめ

  • $nuxt.$emitでイベント発火+親に値を渡す。
  • this.$nuxt.$on('イベント名')で、イベントと値を受け取る。
  • イベントリスナーはcreatedのタイミングで付与しないとまともにイベントが受け取れない。
  • $nuxtの存在を知らないと設定方法で詰む
  • propsの直接の書き換えは怒られるので、dataでやる。

さらにカスタマイズ

  • $emitで複数渡す場合はlayout側のdataを増やし、コンポーネントのバインドも複数にすればOK。
  • setListenerに複数のイベントリスナーを定義すれば、いろんな更新イベントを受け取れる。
  • 今回はヘッダーしか更新しないからupdateHeaderだけど、複数のコンポーネントを更新するならupdateLayoutになりそう。
    • イベント発火はコンポーネント単位で分割したほうが良い
    • 複数のイベント発火をする処理をupdateLayoutで実行するイメージ

参考文献

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

JavaScriptの配列操作メソッドまとめ

自分の備忘録として、JavaScriptの配列操作メソッドをまとめてみました。

1.push

配列の最後に新しい要素を追加するメソッド。
pushメソッドの後の()の中に追加したい要素を入力する。
下記の例では、pushメソッドの引数「くまさん」が配列の最後に追加されている。

    const animals = ['ねこちゃん', 'わんちゃん', 'うさぎさん'];
    animals.push('くまさん');

2.forEach

配列の中の要素を1つずつ取り出して、全ての要素に繰り返し同じ処理を行うメソッド。
下記の例では、配列animalの要素が順番にすべて出力されている。
forEachメソッドの引数にはアロー関数が組み込まれていて、配列内の要素が1つずつ順番に
アロー関数の引数に代入され、処理が繰り返し実行される。
この引数に入っている関数部分はコールバック関数と呼ばれる。

    const animals = ['ねこちゃん', 'わんちゃん', 'うさぎさん'];

    animals.forEach((animal)=> {
      console.log(animal)
    });

3.find

コールバック関数の処理部分に記述した条件式に合う最初の要素を配列の中から取り出すメソッド。
下記の例では、配列animalsの中からオブジェクト
{ name: 'ねこちゃん, age = 3 }
が取り出され、foundAnimalに代入されてコンソールに出力されている。

    const animals = [
      { name: 'ねこちゃん', age: 3 }, 
      { name: 'わんちゃん' },
      { name: 'うさぎさん' },
      { name: 'ねこちゃん', age: 5 }
    ];

    const foundAnimal = animals.find((animal)=> {
      return animal.name === 'ねこちゃん'
    });

    console.log(animal);

4.filter

記述した条件に合う要素のみを取り出して新しい配列を作成するメソッド。
以下の例ではfindメソッドとは異なり、配列animalsから「ねこちゃん」を含むオブジェクト
{ name: 'ねこちゃん', age: 3 }
{ name: 'ねこちゃん', age: 5 }
をどちらも取り出している。

    const animals = [
      { name: 'ねこちゃん', age: 3 }, 
      { name: 'わんちゃん' },
      { name: 'うさぎさん' },
      { name: 'ねこちゃん', age: 5 }
    ];

    const foundAnimal = animals.filter((animal)=> {
      return animal.name === 'ねこちゃん'
    });

    console.log(animal);

5.map

配列内のすべての要素に処理を行い、その戻り値から新しい配列を作成するメソッド。
以下の例では配列animalsの全ての要素に「のにくきゅう」という文字を追加した要素を持つ、
新しい配列を作成しています。

    const animals = ['ねこちゃん', 'わんちゃん', 'うさぎさん'];

    const foundAnimal = animals.map((animal)=> {
      return animal + 'のにくきゅう';
    });

    console.log(animal);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React hooksこんな風に使ってみたよ

バイトでReactを使うことになり、hooksの利用を強行提案して書いて見たところ、楽しすぎたのでどんな感じで使っているかを紹介したい。

あくまでこんな感じで使ってみたよって話なので、良いプラクティスかどうかは自己判断でお願いします

そもそもなんでhooksを利用するのか

公式ドキュメントから引用すれば

ステートフルなロジックをコンポーネント間で再利用する

ためです。今まではHoCとかで管理してたものをhooksでやりやすくした感じですね。

例1) 初期化するときの非同期通信の共通化

まずは単純にuseEffect(あるいはcomponentDidMount)内のfetchしてjsonにしてみたいなロジックを一箇所にまとめてみる

useInitFetch.js
import { useEffect } from 'react';

export function useInitFetch(path, callback) {
  useEffect(() => {
    fetch(`http://path/to/api/${path}`)
      .then(res => res.json())
      .then(res => ({data: res}),
            e => ({error: e}))
      .then(res => callback(res));
  }, []);
}

使い方は

MyComponent.jsx
import React, { useState } from 'react';
import { useInitFetch } from './useInitFetch'

export const MyComponent = () => {
  const [value, setValue] = useState({});
  useInitFetch('path/to/value', (res) => setValue(res));
  return <div>{value}</div>
}

例2) socket.ioの通信をどのコンポーネントからも呼び出せるようにする

useSocketはこんなことができるようにしました

  • いろんなところからsocket.emitを呼び出したいが、socket自体はコンポーネントの表現には関わらないので、バケツリレーを避けたい -> context apiを利用
  • 間違ってio.connectをなんども呼び出さないようにしたい -> 同じくcontext apiで解決可能
  • あるコンポーネントがマウントするまでイベントを登録したくない・アンマウント後はイベントを削除したい -> useEffectのクリーンアップを利用
useSocket.jsx
import React, { useContext, useEffect, useState, createContext } from 'react';
import io from 'socket.io-client';

export const socketContext = createContext();

export const SocketProvider = ({children}) => {

  const [socket, _] = useState(() => io.connect('http://path/to/socketapi'));

  useEffect(() => {
    return function cleanup() {
      socket.close();
    }
  }, []);

  return (
    <socketContext.Provider value={socket}>
      {children}
    </socketContext.Provider>
  )
}

export const useSocket = (setEvent, removeEvent) => {

  const socket = useContext(socketContext);

  useEffect(() => {
    setEvent(socket);
    return () => {
      removeEvent(socket);
    }
  }, []);

  return socket;
}

使い方:

MyComponent.jsx
import React, { useState } from 'react';
import { SocketProvider, useSocket } from './useSocket';

export const MyComponent = () => {
  const [value, setValue] = useState({});
  const socket = useSocket((socket) => {
    socket.on('myevent', (res) => {
      setValue(res);
    });
  }, (socket) => {
    socket.off('myevent');
  });

  return (
    <>
      {value}
      <button onClick={() => socket.emit('myevent2', 'hoge')>Click!</button>
    </>
  )

  export const App = () => {
    return (
      <SocketProvider>
         <MyComponent />
      </SocketProvider>
}

これに限ったことではないですが、hooks + context APIは特にグローバルに唯一存在してほしい変数がある時にとても強力であると感じました

例3) 別Windowで開く&別Windowは最大1つまで

同じくcontext api + hooksを利用できます。

useNewWindow.jsx
import { useState, createContext } from 'react';


export const windowContext = createContext();

export const WindowProvider = ({children}) => {
  const [win, setWin] = useState(null);

  return (
    <windowContext.Provider value={[win, setWin]}>
      {children}
    </windowContext.Provider>
  )
}

export const useNewWindow = (title, features) => {
  const [win, setWin] = useContext(windowContext);

  return (url) => {
    if(win === null || win.isClosed) {
      setWin(window.open(url, title, features));
    }
  }
}

使い方:

MyComponent.jsx
import React, { useState } from 'react';
import { WindowProvider, useNewWindow } from './useNewWindow';

export const MyComponent = () => {
  const openWindow = useNewWindow('HogeTitle', 'resizable=yes,scrollbars=yes');

  return (
    <button onClick={() => openWindow('path/to/newWindow')}>Open Window</button>
  )

  export const App = () => {
    return (
      <WindowProvider>
         <MyComponent />
      </WindowProvider>
    )
  };
}

まとめ

なんかhooksがいいっていうよりcontext apiがいいみたいな記事になってしもうた

useRefとかuseMemoとかまだまだ面白そうな機能がいっぱいなので、使う場面があったら更新していきます

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

【備忘録】React & TypeScript & Webpack4 & Babel7 & dev-server の最小構成ボイラープレートの作成 -画像ファイル読み込み設定-

【備忘録】React & TypeScript & Webpack4 & Babel7 & dev-server の最小構成ボイラープレートの作成 -画像ファイル読み込み設定-

前回の記事 で作成したボイラープレートに画像の読み込み設定を実装する。

使用するnpmモジュール

今回使用するモジュールは以下の通り。(使用するモジュールはすべてlatest)

モジュール名 バージョン 説明
url-loader 2.0.1 画像ファイル読み込みに必要

画像ファイルの読み込み用モジュールはurl-loaderとfile-loaderがあるが、指定したファイルのみを読み込むfile-loaderよりもurl-loaderの方がシンプルに実装できるので今回は、url-loaderを使用。(用途によって使い分け)

package.jsonへのモジュール追加とインストール

使用するnpmモジュールをpackage.jsonに追加する。

{
  "name": "react-ts-webpack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server --open",
    "build": "webpack",
    "build-prod": "webpack --mode=production"
  },
  "keywords": [],
  "author": "kento75 <kento2github@gmail.com> (https://overreact.tk)",
  "license": "ISC",
  "dependencies": {
    "@babel/core": "^7.4.5",
    "@babel/preset-env": "^7.4.5",
    "@babel/preset-react": "^7.0.0",
    "@babel/preset-typescript": "^7.3.3",
    "@types/react": "^16.8.22",
    "@types/react-dom": "^16.8.4",
    "autoprefixer": "^9.6.1",
    "babel-loader": "^8.0.6",
    "css-loader": "^3.0.0",
    "node-sass": "^4.12.0",
    "postcss-loader": "^3.0.0",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.1",
    "url-loader": "^2.0.1",
    "webpack": "^4.35.0",
    "webpack-cli": "^3.3.5"
  },
  "devDependencies": {
    "webpack-dev-server": "^3.7.2"
  }
}

追加後、以下のコマンドを実行してnpmモジュールをインストールする。

$ npm install

webpack.config.jsファイルに設定追加

web pack.config.jsファイルにファイル読み込み用の設定を追加する。

また、前回css-loaderに設定したurl: falseをcss側からのファイル読み込みを行えるようにurl: trueに修正する。

const path = require ('path');
const rules = [
  /* TypeScript用の設定 */
  {
    // 対象とする拡張子を指定
    test: /\.tsx?/,
    // 対象から外すディレクトリを指定
    exclude: /node_modules/,
    // babelを使用する
    loader: 'babel-loader',
  },
  /* Sass用設定 */
  {
    // 対象とする拡張子を指定
    test: /\.scss$/,
    use: [
      // linkタグへの出力用
      'style-loader',
      // CSSのバンドル設定
      {
        loader: 'css-loader',
        options: {
///////////////  ここをtrueに変更  ///////////////
          // css内のurl()メソッドを取り込む設定
          url: true,
          // // ソースマップの有効化 development と production で勝手に切り替わるのでコメントアウト
          // sourceMap: true,

          // sass-loader と postcss-loader を使用するので 2 を設定
          // ここを参考に設定 https://github.com/webpack-contrib/css-loader#importloaders
          importLoaders: 2,
        },
      },
      'postcss-loader',
      {
        loader: 'sass-loader',
        // options: {
        // // ソースマップの有効化 development と production で勝手に切り替わるのでコメントアウト
        //   sourceMap: true,
        // }
      },
    ],
  },
/////  ここから追加  /////
  /* 画像ファイル用設定 */
  {
    // 対象となるファイルの拡張子を設定
    test: /\.(gif|png|jpg|jpeg|svg|ttf|eot|wof|woff|woff2)$/,
    // 画像をBase64で取り込み
    loader: 'url-loader',
  },
/////  ここまで追加  /////
];

module.exports = {
  // ブラウザ環境で使用するためwebをtargetとする
  target: 'web',
  // モード値を production に設定すると最適化された状態で、
  // development に設定するとソースマップ有効でJSファイルが出力される
  mode: 'development',
  // 起点となるTSXファイル(エントリーポイント)
  entry: './src/index.tsx',
  // ビルド後の出力先設定
  output: {
    // 出力先パス
    path: path.resolve (__dirname, 'build'),
    // ファイル名
    filename: 'bundle.js',
  },
  module: {
    // ビルド時に使用するルール(上で設定)を設定
    rules,
  },
  resolve: {
    // 対象とする拡張子を指定
    extensions: ['.ts', '.tsx', '.js'],
  },
  // webpack-dev-serverの設定
  devServer: {
    // 起点となるパス
    contentBase: './',
    // ポート番号
    port: 5000,
  },
};

ここまでの設定で使用する準備は完了。

動作確認

動作を確認するためにindex.tsxでimageファイルを読み込む修正を行う。

あらかじめ、読み込み画像ファイルを<プロジェクトルート>src/assets/の下に配置しておく。

import React from 'react';
import ReactDOM from 'react-dom';
import './scss-style.scss';

// import でもできるが面倒なので、妥協して require を使用
const reactImg = require('./assets/img/react.png');

function App(): JSX.Element {
  const sum = (a: number, b: number): number => a + b;

  return (
    <React.Fragment>
      <div>
        <h1>React & TypeScript!</h1>
        <p>Test: {sum(15, 15)} </p>
      </div>
      <div className="scss-style" />
      <div className="sass-img" />
      <div>
        <img src={reactImg} alt="react" />
      </div>
    </React.Fragment>
  );
}

export default App;

const root = document.getElementById('app-root');

ReactDOM.render(<App />, root);

Sassファイルからも読み込みができることを確認するため、scss-style.scssを修正する。

.scss-style {
  background: #e47474;
  height: 15px;
  // ベンダープレフィックス確認用に適当に入れただけ
  transform: scale(2);
}

// Sassから画像を読み込む
.sass-img {
  height: 400px;
  padding: 0;
  margin: 0;
  background: url("./assets/img/vue.png") no-repeat;
}

実装が完了したら、以下のコマンドを実行して開発用サーバー起動することを確認する。

$ npm start

> react-ts-webpack@1.0.0 start /Users/kento/Programing/VScodeProjects/ts-react-sass-simple-boiler-v2
> webpack-dev-server --open

ℹ 「wds」: Project is running at http://localhost:5000/
ℹ 「wds」: webpack output is served from /
ℹ 「wds」: Content not from webpack is served from ./
ℹ 「wdm」: wait until bundle finished: /
ℹ 「wdm」: Hash: 8f73de6147b59cb9288f
Version: webpack 4.35.0
Time: 2220ms
Built at: 2019-07-08 19:20:38
    Asset      Size  Chunks             Chunk Names
bundle.js  1.29 MiB    main  [emitted]  main
Entrypoint main = bundle.js
[0] multi (webpack)-dev-server/client?http://localhost:5000 ./src/index.tsx 40 bytes {main} [built]
[./node_modules/react-dom/index.js] 1.33 KiB {main} [built]
[./node_modules/react/index.js] 190 bytes {main} [built]
[./node_modules/webpack-dev-server/client/index.js?http://localhost:5000] (webpack)-dev-server/client?http://localhost:5000 4.29 KiB {main} [built]
[./node_modules/webpack-dev-server/client/overlay.js] (webpack)-dev-server/client/overlay.js 3.51 KiB {main} [built]
[./node_modules/webpack-dev-server/client/socket.js] (webpack)-dev-server/client/socket.js 1.53 KiB {main} [built]
[./node_modules/webpack-dev-server/client/utils/createSocketUrl.js] (webpack)-dev-server/client/utils/createSocketUrl.js 2.77 KiB {main} [built]
[./node_modules/webpack-dev-server/client/utils/log.js] (webpack)-dev-server/client/utils/log.js 964 bytes {main} [built]
[./node_modules/webpack-dev-server/client/utils/reloadApp.js] (webpack)-dev-server/client/utils/reloadApp.js 1.63 KiB {main} [built]
[./node_modules/webpack-dev-server/client/utils/sendMessage.js] (webpack)-dev-server/client/utils/sendMessage.js 402 bytes {main} [built]
[./node_modules/webpack-dev-server/node_modules/strip-ansi/index.js] (webpack)-dev-server/node_modules/strip-ansi/index.js 161 bytes {main} [built]
[./node_modules/webpack/hot sync ^\.\/log$] (webpack)/hot sync nonrecursive ^\.\/log$ 170 bytes {main} [built]
[./src/assets/img/react.png] 31.3 KiB {main} [built]
[./src/index.tsx] 810 bytes {main} [built]
[./src/scss-style.scss] 1.35 KiB {main} [built]
    + 37 hidden modules
ℹ 「wdm」: Compiled successfully.

起動後、localhost:5000 にアクセスする。

画像ファイルが表示されること。画像ファイルのパスを確認。

スクリーンショット 2019-07-08 19.20.53.png
スクリーンショット 2019-07-08 19.21.19.png

以上で確認完了。

作成したボイラープレートは こちら

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

【備忘録③】React & TypeScript & Webpack4 & Babel7 & dev-server の最小構成ボイラープレートの作成 -画像ファイル読み込み設定-

【備忘録】React & TypeScript & Webpack4 & Babel7 & dev-server の最小構成ボイラープレートの作成 -画像ファイル読み込み設定-

前回の記事 で作成したボイラープレートに画像の読み込み設定を実装する。

使用するnpmモジュール

今回使用するモジュールは以下の通り。(使用するモジュールはすべてlatest)

モジュール名 バージョン 説明
url-loader 2.0.1 画像ファイル読み込みに必要

画像ファイルの読み込み用モジュールはurl-loaderとfile-loaderがあるが、指定したファイルのみを読み込むfile-loaderよりもurl-loaderの方がシンプルに実装できるので今回は、url-loaderを使用。(用途によって使い分け)

package.jsonへのモジュール追加とインストール

使用するnpmモジュールをpackage.jsonに追加する。

{
  "name": "react-ts-webpack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack-dev-server --open",
    "build": "webpack",
    "build-prod": "webpack --mode=production"
  },
  "keywords": [],
  "author": "kento75 <kento2github@gmail.com> (https://overreact.tk)",
  "license": "ISC",
  "dependencies": {
    "@babel/core": "^7.4.5",
    "@babel/preset-env": "^7.4.5",
    "@babel/preset-react": "^7.0.0",
    "@babel/preset-typescript": "^7.3.3",
    "@types/react": "^16.8.22",
    "@types/react-dom": "^16.8.4",
    "autoprefixer": "^9.6.1",
    "babel-loader": "^8.0.6",
    "css-loader": "^3.0.0",
    "node-sass": "^4.12.0",
    "postcss-loader": "^3.0.0",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "sass-loader": "^7.1.0",
    "style-loader": "^0.23.1",
    "url-loader": "^2.0.1",
    "webpack": "^4.35.0",
    "webpack-cli": "^3.3.5"
  },
  "devDependencies": {
    "webpack-dev-server": "^3.7.2"
  }
}

追加後、以下のコマンドを実行してnpmモジュールをインストールする。

$ npm install

webpack.config.jsファイルに設定追加

web pack.config.jsファイルにファイル読み込み用の設定を追加する。

また、前回css-loaderに設定したurl: falseをcss側からのファイル読み込みを行えるようにurl: trueに修正する。

const path = require ('path');
const rules = [
  /* TypeScript用の設定 */
  {
    // 対象とする拡張子を指定
    test: /\.tsx?/,
    // 対象から外すディレクトリを指定
    exclude: /node_modules/,
    // babelを使用する
    loader: 'babel-loader',
  },
  /* Sass用設定 */
  {
    // 対象とする拡張子を指定
    test: /\.scss$/,
    use: [
      // linkタグへの出力用
      'style-loader',
      // CSSのバンドル設定
      {
        loader: 'css-loader',
        options: {
///////////////  ここをtrueに変更  ///////////////
          // css内のurl()メソッドを取り込む設定
          url: true,
          // // ソースマップの有効化 development と production で勝手に切り替わるのでコメントアウト
          // sourceMap: true,

          // sass-loader と postcss-loader を使用するので 2 を設定
          // ここを参考に設定 https://github.com/webpack-contrib/css-loader#importloaders
          importLoaders: 2,
        },
      },
      'postcss-loader',
      {
        loader: 'sass-loader',
        // options: {
        // // ソースマップの有効化 development と production で勝手に切り替わるのでコメントアウト
        //   sourceMap: true,
        // }
      },
    ],
  },
/////  ここから追加  /////
  /* 画像ファイル用設定 */
  {
    // 対象となるファイルの拡張子を設定
    test: /\.(gif|png|jpg|jpeg|svg|ttf|eot|wof|woff|woff2)$/,
    // 画像をBase64で取り込み
    loader: 'url-loader',
  },
/////  ここまで追加  /////
];

module.exports = {
  // ブラウザ環境で使用するためwebをtargetとする
  target: 'web',
  // モード値を production に設定すると最適化された状態で、
  // development に設定するとソースマップ有効でJSファイルが出力される
  mode: 'development',
  // 起点となるTSXファイル(エントリーポイント)
  entry: './src/index.tsx',
  // ビルド後の出力先設定
  output: {
    // 出力先パス
    path: path.resolve (__dirname, 'build'),
    // ファイル名
    filename: 'bundle.js',
  },
  module: {
    // ビルド時に使用するルール(上で設定)を設定
    rules,
  },
  resolve: {
    // 対象とする拡張子を指定
    extensions: ['.ts', '.tsx', '.js'],
  },
  // webpack-dev-serverの設定
  devServer: {
    // 起点となるパス
    contentBase: './',
    // ポート番号
    port: 5000,
  },
};

ここまでの設定で使用する準備は完了。

動作確認

動作を確認するためにindex.tsxでimageファイルを読み込む修正を行う。

あらかじめ、読み込み画像ファイルを<プロジェクトルート>src/assets/の下に配置しておく。

import React from 'react';
import ReactDOM from 'react-dom';
import './scss-style.scss';

// import でもできるが面倒なので、妥協して require を使用
const reactImg = require('./assets/img/react.png');

function App(): JSX.Element {
  const sum = (a: number, b: number): number => a + b;

  return (
    <React.Fragment>
      <div>
        <h1>React & TypeScript!</h1>
        <p>Test: {sum(15, 15)} </p>
      </div>
      <div className="scss-style" />
      <div className="sass-img" />
      <div>
        <img src={reactImg} alt="react" />
      </div>
    </React.Fragment>
  );
}

export default App;

const root = document.getElementById('app-root');

ReactDOM.render(<App />, root);

Sassファイルからも読み込みができることを確認するため、scss-style.scssを修正する。

.scss-style {
  background: #e47474;
  height: 15px;
  // ベンダープレフィックス確認用に適当に入れただけ
  transform: scale(2);
}

// Sassから画像を読み込む
.sass-img {
  height: 400px;
  padding: 0;
  margin: 0;
  background: url("./assets/img/vue.png") no-repeat;
}

実装が完了したら、以下のコマンドを実行して開発用サーバー起動することを確認する。

$ npm start

> react-ts-webpack@1.0.0 start /Users/kento/Programing/VScodeProjects/ts-react-sass-simple-boiler-v2
> webpack-dev-server --open

ℹ 「wds」: Project is running at http://localhost:5000/
ℹ 「wds」: webpack output is served from /
ℹ 「wds」: Content not from webpack is served from ./
ℹ 「wdm」: wait until bundle finished: /
ℹ 「wdm」: Hash: 8f73de6147b59cb9288f
Version: webpack 4.35.0
Time: 2220ms
Built at: 2019-07-08 19:20:38
    Asset      Size  Chunks             Chunk Names
bundle.js  1.29 MiB    main  [emitted]  main
Entrypoint main = bundle.js
[0] multi (webpack)-dev-server/client?http://localhost:5000 ./src/index.tsx 40 bytes {main} [built]
[./node_modules/react-dom/index.js] 1.33 KiB {main} [built]
[./node_modules/react/index.js] 190 bytes {main} [built]
[./node_modules/webpack-dev-server/client/index.js?http://localhost:5000] (webpack)-dev-server/client?http://localhost:5000 4.29 KiB {main} [built]
[./node_modules/webpack-dev-server/client/overlay.js] (webpack)-dev-server/client/overlay.js 3.51 KiB {main} [built]
[./node_modules/webpack-dev-server/client/socket.js] (webpack)-dev-server/client/socket.js 1.53 KiB {main} [built]
[./node_modules/webpack-dev-server/client/utils/createSocketUrl.js] (webpack)-dev-server/client/utils/createSocketUrl.js 2.77 KiB {main} [built]
[./node_modules/webpack-dev-server/client/utils/log.js] (webpack)-dev-server/client/utils/log.js 964 bytes {main} [built]
[./node_modules/webpack-dev-server/client/utils/reloadApp.js] (webpack)-dev-server/client/utils/reloadApp.js 1.63 KiB {main} [built]
[./node_modules/webpack-dev-server/client/utils/sendMessage.js] (webpack)-dev-server/client/utils/sendMessage.js 402 bytes {main} [built]
[./node_modules/webpack-dev-server/node_modules/strip-ansi/index.js] (webpack)-dev-server/node_modules/strip-ansi/index.js 161 bytes {main} [built]
[./node_modules/webpack/hot sync ^\.\/log$] (webpack)/hot sync nonrecursive ^\.\/log$ 170 bytes {main} [built]
[./src/assets/img/react.png] 31.3 KiB {main} [built]
[./src/index.tsx] 810 bytes {main} [built]
[./src/scss-style.scss] 1.35 KiB {main} [built]
    + 37 hidden modules
ℹ 「wdm」: Compiled successfully.

起動後、localhost:5000 にアクセスする。

画像ファイルが表示されること。画像ファイルのパスを確認。

スクリーンショット 2019-07-08 19.20.53.png
スクリーンショット 2019-07-08 19.21.19.png

以上で確認完了。

作成したボイラープレートは こちら

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

コードのチェックはESLintに任せよう! ブリボンの初めてのESLint

作成趣旨

前回、prettierでコードの自動整形を試してみたブリボン。
prettierのQiita記事はこちら
ESLintと組み合わせれば、もっと自動にコードの整形をしてくれるぞ!という情報を得まして、早速練習用リポジトリでESLintを導入してみました。

ESLintとは?

・JavaScriptの構文チェッカー
・細かな設定をすることができる

要は、変なコードを書いていないか( ; 忘れや import ミスなど)を教えてくれる便利なやつです。
prettierと組み合わせると、変なコードを自動認識した上で、コードの整形までやってくれます。

早速使ってみよう!!

yarn add ESLint

eslint --initをターミナルで呼び出すことで、対話的にeslintの設定ファイルを作ってくれるらしいです。
今回は、Qiitaの記事を元にファイルを生成しました。

eslintrc.json
{
  "extends": ["eslint:recommended"],
  "plugins": [],
  "parserOptions": {},
  "env": { "browser": true},
  "globals": {},
  "rules": {}
}

まずは、こんな感じで簡単に作ってみましょう!!
参考: ESLint 最初の一歩

eslint コマンドを使おう!

eslint src/App.js

上記のコマンドで、src配下のApp.jsファイルの構文チェックができる。

yarn lintでESLintを使う

package.json
"lint": "eslint src/**/*.js"

上記でsrc配下の全ファイルが、ESLintによりふるいにかけられます。

以下余談です

やったこと

yarn add eslint-plugin-prettier
yarn add eslint-plugin-react

解決したエラー

Parsing error: Unexpected token <

解決方法

yarn add babel-eslint
eslintrc.json
{
    "parser": "babel-eslint",
}

yarn startできない

    "babel-eslint": "10.0.1",
    "eslint": "^5.16.0",

上記のバージョンを使用する

関数コンポーネントで React を使っていなくても、宣言しないと使えないのに、明示的に使っていないという理由で、エラーを吐きだす。

yarn add eslint-plugin-react
eslintrc.json
"extends": ["eslint:recommended", "plugin:react/recommended"],

参考

参考文献

ESLint 最初の一歩
ESlint rules
ESLintで構文チェック(Webpack)
JSプログラマーのイラッとする「クセ」はESLintを導入して対処しよう

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

textareaの入力後の値を取得したい

<textarea class="form-control" rows="5" id="hoge">huga</textarea>     
var hoge = $('#hoge').text(); //huga

textareaの「入力後の」値を取得したい。
text()ではデフォルト値(huga)を取得してしまった。

var hoge = $('#hoge').val(); //入力後の値

val()だと、入力後の値を取得できた。

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

Pixi.jsでつくるスワイプスタイルのクイズアプリ[その1]

Webブラウザ上でインタラクティブなコンテンツを出す仕組みとして近年HTML5がよくつかわれています。
WebGLで描画できるフレームワークを探していたところ、Pixi.Jsというものをみつけました。

今回、技術検証も含めてPixi.Jsで簡易的な○×をゲームを作ってみます。

まずは導入

npm install pixi.js

もちろんコンテンツネットワークからももってくることができます。

<script src = " https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.5.1/pixi.min.js"> </script >

とくに、CDNなら問題ないのですが、
Bundleをダウンロードしてインポートしたとき、どのjsを呼びだせばいいんじゃ?
と悩む人も多いと思います。

\node_modules\pixi.js\dist\
というディレクトリの中に
pixi.jsとpixi.min.jsがありました。
最小限のコンテンツがpixi.min.jsでフルに使いたいならpixi.jsで良いかと
どちらにどの機能があるのか一覧を載せているDocumentが見当たりませんでした。

とりあえず、落ちていたコードを拾い集めてタイトル画面とボタンだけ作ってみました。
あとで整形出術が必要です。

<body>
  <div id="pixiview"></div>
<script src="../pixi/pixi.min.js"></script>
<script>
 const app = new PIXI.Application({
    width: 800, height: 600, backgroundColor: 0x1099bb, resolution: window.devicePixelRatio || 1,});
  document.body.appendChild(app.view);
  const container = new PIXI.Container();
  app.stage.addChild(container);
  // Create a new texture
  const texture = PIXI.Texture.from('img/logo2.png');
    const logo2 = new PIXI.Sprite(texture);
    container.addChild(logo2);
  // Move container to the center
  container.x = app.screen.width / 4;
  container.y = app.screen.height / 12;

  //以下ボタン サンプルからそのまま なんでテクスチャアトラスで読み込まんの?
  //ついでに長いのであとでボタンクラスとか作る。
  var textureButton = PIXI.Texture.fromImage('img/start_button1.png');
  var textureButtonDown = PIXI.Texture.fromImage('img/start_button2.png');
  var textureButtonOver = PIXI.Texture.fromImage('img/start_button3.png');

      var button = new PIXI.Sprite(textureButton);
      button.buttonMode = true;

      button.x = app.screen.width / 3;
      button.y = app.screen.height / 1.4;

      // make the button interactive...
      button.interactive = true;
      button.buttonMode = true;

      button

        .on('pointerdown', onButtonDown)
        .on('pointerup', onButtonUp)
        .on('pointerupoutside', onButtonUp)
        .on('pointerover', onButtonOver)
        .on('pointerout', onButtonOut);


      app.stage.addChild(button);


      function onButtonDown() {
        this.isdown = true;
        this.texture = textureButtonDown;
        this.alpha = 1;
      }

      function onButtonUp() {
        this.isdown = false;
        if (this.isOver) {
          this.texture = textureButtonOver;
        }
        else {
          this.texture = textureButton;
        }
      }

      function onButtonOver() {
        this.isOver = true;
        if (this.isdown) {
          return;
        }
        this.texture = textureButtonOver;
      }

      function onButtonOut() {
        this.isOver = false;
        if (this.isdown) {
          return;
        }
        this.texture = textureButton;
      }

</script>

</body>

*はまりどころ
ローカルに置いてある画像はなぜかChromeでは表示されません。
なんで??現在調査中です。

その2につづく

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

ES6{}でブロックスコープを作ったら中身が グローバル変数になった話

はじめに

弊社はES6で記載したコードをbabelでES5にトランスパイルしてます。
いつか問題が起きそうだなって思ってたら本当に起きたので備忘録として残しておきます。

前提

今回問題が起きたページでは複数のjsファイルが読み込まれていて、トータルするとかなりのjsコードが書かれています。

やりたかったこと

モーダルを表示後、「ばつボタン」もしくは「モーダル外」をクリックするとモーダルが閉じる。

問題が起きたコード

common.js
{
    const
         target = document.getElementById('commonPopup')
        ,targetCloseTrigger = document.getElementsByClassName('_close')[0]
    ;   

    const popupAni = {
        popupDisplay : ()=> {
            target.classList.remove('_no_show');
            target.classList.add('_is_show');
        },

        popupClose : ()=> {
            target.style.transition = ".7s";
            target.classList.remove('_is_show');
            target.classList.add('_no_show');
        }
    }

    window.addEventListener('load', ()=> {
        setTimeout(popupAni.popupDisplay(),5000);
    });
    targetCloseTrigger.addEventListener('click',()=> {
        popupAni.popupClose();
    });
}

上記はモーダルの表示と閉じる時に使われる関数です。
エラー内容をコピるの忘れてました。すみません。
ざっくりとtargetの中のtransitionはundefinedだよって感じです。

起きた問題

結論、モーダルのtarget変数が、後読みされる別のtarget変数に上書きされていました。

ES6をトランスパイルした際、{}はグローバルスコープとして認識されない

ES5にはconstletという概念は存在せず、トランスパイルでvarに変換され、
{}で括られた変数たちはグローバル変数になりました。

解決策

{}ではなく即時関数で括る

common.js
(function(){
    const
         target = document.getElementById('commonPopup')
        ,targetCloseTrigger = document.getElementsByClassName('_close')[0]
    ;   

    const popupAni = {
        popupDisplay : ()=> {
            target.classList.remove('_no_show');
            target.classList.add('_is_show');
        },

        popupClose : ()=> {
            target.style.transition = ".7s";
            target.classList.remove('_is_show');
            target.classList.add('_no_show');
        }
    }

    window.addEventListener('load', ()=> {
        setTimeout(popupAni.popupDisplay(),5000);
    });
    targetCloseTrigger.addEventListener('click',()=> {
        popupAni.popupClose();
    });
})();

いったんはこれで解決しました。
そもそも変数名が単調すぎるって問題もありますが。。。

参考サイト

https://teratail.com/questions/68576

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

非同期処理の闇と光

はじめに

今回の記事はNode.jsを使っている際に、非同期処理という便利な機能な弊害となったため、ここに解決策を書き記す。同じく悩む人に伝わってくれ。

非同期処理とは

まずはコードを見てもらおう。ローカルディレクトリのファイル情報を取得するための処理と同時に別の処理を行うと仮定したコードである。

sample.js
const fs = require('fs')

function hogehoge(){
    //ファイルを読み取る処理
    fs.readFile("<ファイルのパス>",
    function(error,data){//=処理Aと置く
        if(error)return -1;
        //終わったときに表示する
        console.log("ファイルの読み込みを完了しました!");
        console.log("データの中身は:" + data);
    }
}

function fugafuga(){//=処理Bとする
    console.log("fuga~");
}

function hogehoge();//ファイルの中身を出す。
function fugafuga();//fuga~と表示

この場合に表示されることが以下のときがある。

fuga~
ファイルの読み込みを完了しました!
データの中身は:うんたらかんたら

なんと、処理の順番が変わっている
処理A⇒処理Bとなるはずが、処理B⇒処理Aとなっているじゃないか。
これはファイル読み込みに時間がかかることを見越して、「ファイルを読み込む処理をすると同時にほかの処理も行う」という非同期処理がされているからだ。
うわぁ!なんて便利な機能なんだろう!!!
そう思うことだろう。
しかし、この機能には実は危険性があるのだ...

非同期処理の危険性

sample2.js
const fs = require('fs')

let filestr = "";

function hogehoge(){
    //ファイルを読み取る処理
    fs.readFile("<ファイルのパス>",
    function(error,data){
        if(error)return -1;
        filestr = "" + data;//fileの中身をfilestrに移す
        console.log("ファイルの読み込みを完了しました!");
    }
}

function fugafuga(){
    console.log("データの中身は:" + filestr);//filestrの中身を表示
}

function hogehoge();//ファイルを読み込んで
function fugafuga();//中身を表示

こうなった場合どうだろう?勘の鋭い方ならもうお気づきかもしれないが以下の通りになる可能性がある。

データ中身は:
ファイルの読み込みを完了しました!

「ちょい!ちょい!ちょい!ちょーい!!!」と思うだろう。そう、非同期処理の欠点として、同期処理でなければならない処理も非同期処理になってしまうということである。世に出されている便利な機能が非同期処理になっているというケースが少なくない。ではどう対応すればいいのだろうか。

くらえ!同期処理化アタック!!!

ならば、簡単だ。同期処理に戻してやればいい。
そこでPromiseという機能をつかおう。説明は省くがわかりやすいリンクを乗せておくので、ぜひ参考にしてくれ。
Promiseについて0から勉強してみた

出来上がったコードが以下の通りだ。

sample3.js
const fs = require('fs')

let filestr = "";

function hogehoge(){
    //ファイルを読み取る処理
    fs.readFile("<ファイルのパス>",
    function(error,data){
        if(error)return -1;

        filestr = "" + data;//fileの中身をfilestrに移す
        console.log("ファイルの読み込みを完了しました!");
        return new Promise(function(resolve,error){
            resolve(data)//処理が終わったらresolveでdataを返す。
        });
    }
}

function fugafuga(let str){
    console.log("データの中身は:" + filestr);//filestrの中身を表示
}

function hogehoge()//ファイルを読み込んで
.then(function(value){//ファイルが読み込み終わったら
    fugafuga(value);//中身を表示
}

こうすることで、非同期処理を同期処理にしてやることができる。やはり、同期処理が必要とされる場面が多くみられると思うので、Promiseは覚えておいて損はないだろう。

最後に

自分も理解したばかりでまだわからないことも多いので、ぜひ指摘をお願いしたいです。
間違えに気づいた方、改良点に気づいた方はぜひアドバイスをお願いします。

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

自動で競馬投票~入金編~

背景

お馬さんに夢を託すプログラムをつくりたい。それも自動で。

やりかた

自動的に投票してほしいが、そんなAPIはない。
なので、GASにやらせることにした。

今回は楽天競馬を採用。
楽天銀行の口座からスムーズに入金が可能です。

完成品

ソースコード

GASで動作するJavaScriptです。

使い方

  1. google driveなどから、Google Apps Scriptを新規作成
    image.png

  2. ソースコードを貼り付けて保存(ファイル名は任意)
    image.png

  3. 環境変数に各情報をセットする
    image.png
    image.png

  4. 定期実行設定をする
    image.png
    image.png

まとめ

今回は、入金までやりました。
次回はランダムで馬券を買う処理を実装してみたいと思います。

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

jQueryの複数条件指定

$('.hoge, .fuga').on('click',function(e){~~~ ~~~})

hogeかhugaのどちらかに引っかかるとイベントが発火する

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

コーディング規約

JavaScript本格入門(ISBN 978-4774184111)で基礎からJavaScriptを勉強するシリーズです。今回はChapter8よりコーディング規約についてです。

概要

複数人で開発していると、コーディング規約が大切ということについては言わずもがなだと思います。
JavaScriptのコーディング規約については、下記の記事に、有名どころが纏まっていますので参考にさせていただきました。

筆者がNode.jsで開発することが多いので、Felix's Node.js Style Guideのコーディング規約を使って、書いたコードをチェックするようにしてみます。

環境

記事作成時点の環境は以下の通りです。

  • Windows 10 Home edition(64bit)
  • Node.js 10.15.1
  • npm 6.4.1

ESlintのインストール

ESlint(リンター)を使ってコーディング規約に則っているかをチェックしますので入れます。

インストール
$ npm install eslint
バージョン確認
$ eslint --version
v6.0.1

実践

コーディングしてみる

何も考えずに、テキトーに動くサンプルを作ってみました。
さてチェックするとどうなるでしょうか。

main.js
var Main = function(arg1, arg2) {
    console.log("Your input is " + arg1 + ".")
}

Main("hoge");

Felix's Node.js Style Guideの設定を持ってくる

ESlintは.eslintrc.jsonファイルをESlintの設定として読み込んでくれます。

今回はFelix's Node.js Style Guideを利用するのでGitHubから持ってきます。
(ほんとはgit clone & バージョン指定して持ってくるべき)

参照:https://github.com/felixge/node-style-guide/blob/master/.eslintrc

.eslintrc.json
{
  "env": {
    "node": true
  },
  "rules": {
    "array-bracket-spacing": [2, "never"],
    "block-scoped-var": 2,
    "brace-style": [2, "1tbs"],
    "camelcase": 1,
    "computed-property-spacing": [2, "never"],
    "curly": 2,
    "eol-last": 2,
    "eqeqeq": [2, "smart"],
    "max-depth": [1, 3],
    "max-len": [1, 80],
    "max-statements": [1, 15],
    "new-cap": 1,
    "no-extend-native": 2,
    "no-mixed-spaces-and-tabs": 2,
    "no-trailing-spaces": 2,
    "no-unused-vars": 1,
    "no-use-before-define": [2, "nofunc"],
    "object-curly-spacing": [2, "never"],
    "quotes": [2, "single", "avoid-escape"],
    "semi": [2, "always"],
    "keyword-spacing": [2, {"before": true, "after": true}],
    "space-unary-ops": 2
  }
}

ESlintでチェックしてみる

$ eslint main.js
  1:27  warning  'arg2' is defined but never used                                                               no-unused-vars
  2:17  error    Strings must use singlequote                                                                   quotes
  2:43  error    Strings must use singlequote                                                                   quotes
  2:47  error    Missing semicolon                                                                              semi
  3:2   error    Missing semicolon                                                                              semi
  5:1   warning  A function with a name starting with an uppercase letter should only be used as a constructor  new-cap
  5:6   error    Strings must use singlequote                                                                   quotes
  5:14  error    Newline required at end of file but not found                                                  eol-last

✖ 8 problems (6 errors, 2 warnings)
  6 errors and 0 warnings potentially fixable with the `--fix` option.

いっぱい怒られました!直しましょう!

コードを直す

fixed
var main = function(arg1) {
    console.log('Your input is ' + arg1 + '.');
};

main('hoge');

警告に従って直しました。

最後にもう一度チェック

$ eslint main.js

何も言われませんでした。これで安心してプルリクエストできます。

まとめ

Grunt(タスクランナー)や、CircleCIなどと組み合わせれば、コミットが来るたびに自動チェック等が出来ますね。

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

【モーダルなどの一時スクロール禁止】スクロールジャンクとは?

やりたいこと

モーダルを開いたときに、「パソコンの一時的にスクロールを止めたいな」
と思うことがありました。
とりあえずかいてみました。

window.addEventListener('mousewheel',(e)=>{
   e.preventDefault();
});

preventDefaulでスクロールの動きを封じるやつです。
でもエラーが出て全然動きません。

スクロールジャンク防止が原因だった

↓こちらのサイトでスクロールジャンクを知りました。
https://blog.webico.work/passive-event-listeber01

どうやら、スクロール系のイベント(mousewheel,touchmove,wheelなど)は、
スクロールするたびに登録された全てのイベントにpreventDefault()がないか確認が終わるまで処理が行えないそうです。これが所謂スクロールジャンク。

それを防ぐためにaddEventListenerにはpassive:trueという値が初期値で設定されています。
これは実行前に、「preventDefault()実行しませんよ〜」と明示するものらしいです。

でも!
今回はprevcentDefault()を実行するので、ここの値をいじります。

解決方法は?

passiveにfalseを渡してあげましょう。

window.addEventListener('mousewheel',(e)=>{
e.preventDefault();},{passive:false});

ちょっと待って!
昨日調べたことによると、イベントリスナの第三引数はuseCaptureというイベント伝播に関する引数だったはず...

addEventListenerの第三引数の謎

https://qiita.com/kozy4324/items/85831e2c990d92b8397b
こちらの記事に答えがありました。
元々はuseCaptureのみだったみたいですが、拡張されたようです。現在では

element.addEventListener('click',Handler(),{
   once: true,
   passive: true,
   capture: true
});

と書けるとのこと。
(以前の書き方と互換性があり、第三引数にboolean型のみ渡した場合はcaptureの値として判断されるそうです。)

onceってなに?

onceにすると、イベントハンドラが一度だけ処理されます。
一度しか読み込まないイベントを明示することで、メモリの節約になる!ってことですね。
毎回removeEventListenerを使うより随分楽ですね。

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

JavaScriptの同時実行モデルについて

概要

JavaScriptが多くの言語と異なる点のひとつに、イベントループベースの同時実行モデルがあります。これは、JavaScript自体というよりも、ブラウザの仕組みにも関わりがあります。

今回はそれについて書いてみようと思います。

ラインタイム

MDNの図を拝借すると、JavaScriptのラインタイムは以下のような要素で構成されています。ひとつずつ説明してゆきます。

runtime.png

ヒープ

ヒープはヒープですね。今回のテーマとは少し外れるので省略します。

スタック

JavaScriptで処理を呼び出すと、コールスタックが積み上がります

JavaScriptはシングルスレッドなので、一度にひとつの処理しか実行できません。例えば、以下のように関数を呼び出すような処理を考えます。

function subA() { console.log('A'); }
function subB() { console.log('B'); }

function main() {
  subA();
  subB();
}

main();

これは、以下のようなスタックを形成します。

stack.gif

  1. mainのフレーム
  2. mainから呼び出されたsubAのフレーム
  3. subAの実行が完了するとsubAのフレームはスタックからポップアウト
  4. mainから呼び出されたsubBのフレーム
  5. subBの実行が完了するとsubBのフレームはスタックからポップアウト
  6. すべての実行が完了するとmainのフレームはスタックからポップアウト

上記のように同期的な関数実行では、処理がひとつずつ行われてゆくことがわかります。

キュー

JavaScriptにおけるキューとは、次に実行すべき処理の一覧といえます。

ここではsetTimeoutを使用した以下のコードを考えてみます。

function setTimeoutCallback() {
  console.log('A');
}

function subA() {
  setTimeout(setTimeoutCallback, 7000)
}

function subB() { console.log('B'); }

function main() {
  subA();
  subB();
}

main();

これは、以下のようなスタックとキューを形成します。

queue.gif

  1. mainのフレーム
  2. mainから呼び出されたsubAのフレーム
  3. subAから呼び出されたsetTimeoutのフレーム
  4. setTimeoutの実行が完了するとsetTimeoutのフレームはスタックからポップアウト。ブラウザ側でタイマーの計算開始。
  5. mainから呼び出されたsubBのフレーム
  6. subBの実行が完了するとsubBのフレームはスタックからポップアウト
  7. 4のタイマーが終了すると、ブラウザがイベントをキューに追加する
  8. キューに追加された処理をイベントループで検知、処理を実行する
  9. すべての実行が完了するとmainのフレームはスタックからポップアウト

4でタイマーの処理がJavaScriptのメインスレッドを離れており、キューの仕組みがあることでノンブロッキングに処理が進んでいます。

さらに、setTimeoutを連続で呼び出す場合も考えてみます。

function setTimeoutCallback() {
  console.log('A');
}

function subA() {
  setTimeout(setTimeoutCallback, 7000)
  setTimeout(setTimeoutCallback, 7000)
  setTimeout(setTimeoutCallback, 7000)
  setTimeout(setTimeoutCallback, 7000)
  setTimeout(setTimeoutCallback, 7000)
}

function subB() { console.log('B'); }

function main() {
  subA();
  subB();
}

main();

これは、以下のようなスタックとキューを形成します。

Qiita、size>10MBアップできなかった.gif

上記のように、呼び出し自体は関数ひとつずつですが、処理自体はブラウザAPIによって同時実行され、完了したものからJavaScript側のキューにたまってゆくという仕組みです。

これはスレッドなどで実現できる並列(parallel)処理とはいえませんが、実際の処理をJavaScriptランタイムの外にだすことで、並行(concurrent)に処理を実行していると言えます。

(並行/並列/非同期/ノンブロッキングあたりは勘違いされやすいので、違いをおさえておくといいと思います)。

イベントループ

上記でも言及した、キューを検知できる仕組みをイベントループと呼びます。イベントループは、イベントを待機する実装のひとつで、MDNの例を借りるなら、以下のような実装に似ています。

while(queue.waitForMessage()){
  queue.processNextMessage();
}

シングルスレッドでのイベントループは、同時多発的に発生するイベントを、軽量に扱うのに向いています。これはプロセスが大量に立ち上がり、メモリやコンテキストスイッチで処理が重くなるのを防ぐことができるからです。(ただし、大量の計算など時間のかかる処理は苦手で、処理が終わるまで後続処理をブロックしてしまいます)。

JavaScript以外でシングルスレッドのイベント駆動アーキテクチャを採用しているものとしては、nginxが有名です。

nginxは、上記のような方針でC10K問題を解決しようとしていて、Node.jsもその文脈で話されているのをたまにみかけます。しかし、もともとJavaScriptは、90年代のウェブブラウザという、十分なリソースがない環境のために開発された言語です。限られたCPUで少しでもまともに動くように実装されたのがシングルスレッド+イベントループというアーキテクチャだった、というのが正しい背景かと思います。

つまり、イベントループのアーキテクチャは、ブラウザで発生する大量のイベントを処理するのに(少なくとも当時は)最適なユースケースであったと考えることができます。現在では、Goのように軽量スレッドを使用した方が線がいい場合も多いかもしれません。

応用例

ここで少し実際に役に立ちそうな例を見てみます。

有名な例ではありますが、setTimeout(func, 0)を使って処理をキューに外出しし、描画タイミングを早くするという手法があります。例えば、以下のように、DOMの変更処理のあとに、かなり時間のかかる処理があったとします。

// DOMの変更処理
var target = document.getElementById('target');
target.textContent = 'updated';

// 時間のかかる処理
var calcResult;
function longTask() {
  for (var i = 0; i < 1000000000; i++) {
    calcResult = i * 3;
  }
}
longTask();

このようなコードを書くと、実際の画面変更はすぐには行われません。ブラウザで描画の処理が走る前に、10億回のループでスレッドが埋まってしまうので、描画が後回しにされてしまいます。メインスレッドを見るとlongTaskに約4秒かかっているので、その分ユーザーはインタラクションを待つことになるでしょう。

long-task.png

描画を早くする方法のひとつとして、以下のように「setTimeout(func, 0)で処理を一旦キューに押し込める」などがあります。

// DOMの変更処理
var target = document.getElementById('target');
target.textContent = 'updated';

// 時間のかかる処理
var calcResult;
window.setTimeout(function longTask() {
  for (var i = 0; i < 1000000000; i++) {
    calcResult = i * 3;
  }
}, 0)

こうすることで、描画処理を先に走らせることができるため、パフォーマンスを改善することができます。

long-task-queued.png

ただし、後続の処理をブロックしてしまうことには変わりないので、処理中の表示にしたり、Workerを使ったりするなども検討したほうがよさそうです。いずれにせよ、仕組みを理解しておくことは思わぬ事故へのリスクヘッジにもなると考えます。

まとめ

今回はJavaScriptの同時実行モデルについて紹介しました。以下の言葉を聞いてスッとイメージができればこの記事のまとめになっているかなと思います。

  • スタック、キュー
  • シングルスレッド、イベントループ
  • 並列、並行

お役に立てれば幸いです。

参考

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

JavaScriptの同時実行モデルについて

概要

JavaScriptが多くの言語と異なる点のひとつに、イベントループベースの同時実行モデルがあります。これは、JavaScript自体というよりも、ブラウザの仕組みにも関わりがあります。

今回はそれについて書いてみようと思います。

ラインタイム

MDNの図を拝借すると、JavaScriptのラインタイムは以下のような要素で構成されています。ひとつずつ説明してゆきます。

runtime.png

ヒープ

ヒープはヒープですね。今回のテーマとは少し外れるので省略します。

スタック

JavaScriptで処理を呼び出すと、コールスタックが積み上がります

JavaScriptはシングルスレッドなので、一度にひとつの処理しか実行できません。例えば、以下のように関数を呼び出すような処理を考えます。

function subA() { console.log('A'); }
function subB() { console.log('B'); }

function main() {
  subA();
  subB();
}

main();

これは、以下のようなスタックを形成します。

stack.gif

  1. mainのフレーム
  2. mainから呼び出されたsubAのフレーム
  3. subAの実行が完了するとsubAのフレームはスタックからポップアウト
  4. mainから呼び出されたsubBのフレーム
  5. subBの実行が完了するとsubBのフレームはスタックからポップアウト
  6. すべての実行が完了するとmainのフレームはスタックからポップアウト

上記のように同期的な関数実行では、処理がひとつずつ行われてゆくことがわかります。

キュー

JavaScriptにおけるキューとは、次に実行すべき処理の一覧といえます。

ここではsetTimeoutを使用した以下のコードを考えてみます。

function setTimeoutCallback() {
  console.log('A');
}

function subA() {
  setTimeout(setTimeoutCallback, 7000)
}

function subB() { console.log('B'); }

function main() {
  subA();
  subB();
}

main();

これは、以下のようなスタックとキューを形成します。

queue.gif

  1. mainのフレーム
  2. mainから呼び出されたsubAのフレーム
  3. subAから呼び出されたsetTimeoutのフレーム
  4. setTimeoutの実行が完了するとsetTimeoutのフレームはスタックからポップアウト。ブラウザ側でタイマーの計算開始。
  5. mainから呼び出されたsubBのフレーム
  6. subBの実行が完了するとsubBのフレームはスタックからポップアウト
  7. 4のタイマーが終了すると、ブラウザがイベントをキューに追加する
  8. キューに追加された処理をイベントループで検知、処理を実行する
  9. すべての実行が完了するとmainのフレームはスタックからポップアウト

4でタイマーの処理がJavaScriptのメインスレッドを離れており、キューの仕組みがあることでノンブロッキングに処理が進んでいます。

さらに、setTimeoutを連続で呼び出す場合も考えてみます。

function setTimeoutCallback() {
  console.log('A');
}

function subA() {
  setTimeout(setTimeoutCallback, 7000)
  setTimeout(setTimeoutCallback, 7000)
  setTimeout(setTimeoutCallback, 7000)
  setTimeout(setTimeoutCallback, 7000)
  setTimeout(setTimeoutCallback, 7000)
}

function subB() { console.log('B'); }

function main() {
  subA();
  subB();
}

main();

これは、以下のようなスタックとキューを形成します。

Qiita、size>10MBアップできなかった.gif

上記のように、呼び出し自体は関数ひとつずつですが、処理自体はブラウザAPIによって同時実行され、完了したものからJavaScript側のキューにたまってゆくという仕組みです。

これはスレッドなどで実現できる並列(parallel)処理とはいえませんが、実際の処理をJavaScriptランタイムの外にだすことで、並行(concurrent)に処理を実行していると言えます。

(並行/並列/非同期/ノンブロッキングあたりは勘違いされやすいので、違いをおさえておくといいと思います)。

イベントループ

上記でも言及した、キューを検知できる仕組みをイベントループと呼びます。イベントループは、イベントを待機する実装のひとつで、MDNの例を借りるなら、以下のような実装に似ています。

while(queue.waitForMessage()){
  queue.processNextMessage();
}

シングルスレッドでのイベントループは、同時多発的に発生するイベントを、軽量に扱うのに向いています。これはプロセスが大量に立ち上がり、メモリやコンテキストスイッチで処理が重くなるのを防ぐことができるからです。(ただし、大量の計算など時間のかかる処理は苦手で、処理が終わるまで後続処理をブロックしてしまいます)。

JavaScript以外でシングルスレッドのイベント駆動アーキテクチャを採用しているものとしては、nginxが有名です。

nginxは、上記のような方針でC10K問題を解決しようとしていて、Node.jsもその文脈で話されているのをたまにみかけます。しかし、もともとJavaScriptは、90年代のウェブブラウザという、十分なリソースがない環境のために開発された言語です。限られたCPUで少しでもまともに動くように実装されたのがシングルスレッド+イベントループというアーキテクチャだった、というのが正しい背景かと思います。

つまり、イベントループのアーキテクチャは、ブラウザで発生する大量のイベントを処理するのに(少なくとも当時は)最適なユースケースであったと考えることができます。現在では、Goのように軽量スレッドを使用した方が線がいい場合も多いかもしれません。

応用例

ここで少し実際に役に立ちそうな例を見てみます。

有名な例ではありますが、setTimeout(func, 0)を使って処理をキューに外出しし、描画タイミングを早くするという手法があります。例えば、以下のように、DOMの変更処理のあとに、かなり時間のかかる処理があったとします。

// DOMの変更処理
var target = document.getElementById('target');
target.textContent = 'updated';

// 時間のかかる処理
var calcResult;
function longTask() {
  for (var i = 0; i < 1000000000; i++) {
    calcResult = i * 3;
  }
}
longTask();

このようなコードを書くと、実際の画面変更はすぐには行われません。ブラウザで描画の処理が走る前に、10億回のループでスレッドが埋まってしまうので、描画が後回しにされてしまいます。メインスレッドを見るとlongTaskに約4秒かかっているので、その分ユーザーはインタラクションを待つことになるでしょう。

long-task.png

描画を早くする方法のひとつとして、以下のように「setTimeout(func, 0)で処理を一旦キューに押し込める」などがあります。

// DOMの変更処理
var target = document.getElementById('target');
target.textContent = 'updated';

// 時間のかかる処理
var calcResult;
window.setTimeout(function longTask() {
  for (var i = 0; i < 1000000000; i++) {
    calcResult = i * 3;
  }
}, 0)

こうすることで、描画処理を先に走らせることができるため、パフォーマンスを改善することができます。

long-task-queued.png

ただし、後続の処理をブロックしてしまうことには変わりないので、処理中の表示にしたり、Workerを使ったりするなども検討したほうがよさそうです。いずれにせよ、仕組みを理解しておくことは思わぬ事故へのリスクヘッジにもなると考えます。

まとめ

今回はJavaScriptの同時実行モデルについて紹介しました。以下の言葉を聞いてスッとイメージができればこの記事のまとめになっているかなと思います。

  • スタック、キュー
  • シングルスレッド、イベントループ
  • 並列、並行

お役に立てれば幸いです。

参考

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

矢印キーでQiitaのページを移動するユーザースクリプト

概要

QiitaのtagsのページでAutoPagerize1が動作していたのが動作しなくなった :sob:
tagsのページだけXHR化したことが原因だ。

そこでXHR化したtagsとorganizationsのページ2だけ左右の矢印キーで前後のページに移動するユーザースクリプトを作った。
このユーザースクリプトは左右の矢印キーを押したときイベントを拾って前後のページに移動するエレメントをJavaScriptでクリックする。

クリックする手間を省くだけでなく最近の記事の位置までスクロールしてくれる親切設計だ。

GitHubで公開中。
https://github.com/querykuma/snippets/blob/master/qiita/qiitaAddKeysLRArrow.user.js

使い方

拡張機能のTampermonkeyなどを使ってユーザースクリプトを登録する。

このユーザースクリプトは次の指定によりtagsとorganizationsのページだけ動くように指定してある。

// @match        https://qiita.com/tags/*
// @match        https://qiita.com/organizations/*

Tampermonkeyはマッチしたページでユーザースクリプトを実行する拡張機能だ :sunglasses:

動作検証環境

Chrome 75.0.3770.100
Tampermonkey 4.8.41

ユーザースクリプト

ユーザースクリプト
// ==UserScript==
// @name         Qiita Add LRArrow ShortcutKeys
// @namespace    http://tampermonkey.net/
// @version      0.5
// @description  QiitaのXHRで前後のページを取得するページにおいて左右矢印キーで前後のページに移動
// @author       Query Kuma
// @match        https://qiita.com/tags/*
// @match        https://qiita.com/organizations/*
// @grant        none
// ==/UserScript==

(function () {
 'use strict';

 const delayed = () => {

  if (!document.getElementsByClassName("st-Pager_next").length) return;

  if (document.getElementsByClassName("st-Pager_next")[0].querySelector("a[href]")) return;

  const keydown_pager = e => {

   const scroll_to_node = () => {
    let scroll_node = document.querySelector(".p-tagShow_mainBottom") /* tags 最近の記事 */;
    if (!scroll_node) {
     scroll_node = document.querySelector('[data-hyperapp-app="OrganizationNewestArticles"]'); /* organization 最新の記事 */
    }
    if (scroll_node) {
     scrollTo(0, scroll_node.offsetTop);
    }
   }

   if (e.key === 'ArrowRight') {
    document.getElementsByClassName("st-Pager_next")[0].firstElementChild.click();
    scroll_to_node();
   } else if (e.key === 'ArrowLeft') {
    document.getElementsByClassName("st-Pager_prev")[0].firstElementChild.click();
    scroll_to_node();
   };

  };

  document.addEventListener('keydown', keydown_pager);
 };

 setTimeout(delayed, 500);

})();

  1. AutoPagerizeは次のページを自動取得してくれるブラウザの拡張機能。 

  2. organizationsのページはOrganization一覧(週間、月間)とそのOrganizationのページの3つあるがXHR化したのは最後の1つのみ。 

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

javascriptのブックマークレットが動作しない

下記のような形に、クラスの値を変更するためのブックマークレットを作成したいと考えております。

 .result_headline {
    display: block!important;
}

 #resultBox{
    height:1000px!important;
}

試したのは、以下のコードです。
ブックマーク内の記述を下記に致しました。

javascript:(function show(){
        var e = document.getElementsByClassName("result_headline");
        e.style.display = "block";
    })

 #resultBoxの場合、getElementById
 を使うことまでは、分かりましたが、実際の記述の仕方は分かりませんでした。

お詳しい方に、アドバイスなど頂けますと幸いです。
何卒どうぞよろしくお願い致します。

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

関数について

JavaScript本格入門(ISBN 978-4774184111)で基礎からJavaScriptを勉強するシリーズです。今回はChapter4から関数についてです。

関数の定義

方法は主に4つあります。

  1. function命令を使う
  2. Functionコンストラクタを使う
  3. 関数リテラル表現を使う
  4. アロー関数(ES2015)を使う

しかし結論として、日常的に使うのはアロー関数でよさそうです。

function命令を使う

function 関数名(引数) {
  関数本体
};
function myFunc(arg) {
  return arg * arg;
};
let squared = myFunc(3);
console.log(squared);     // 出力:9

有名なfunction命令を使った関数定義ですが、静的な構造の宣言であったり意外とトリッキーな動きをします。宣言していないのに関数が使えるような動きをするのでバグを生みやすいので使わないほうがいいです。

コード解析時に関数の登録が行われるので、先に使おうとしても使える
let squared = myFunc(3);
console.log(squared);     // 出力:9

function myFunc(arg) {
  return arg * arg;
};

Functionコンストラクタを使う

let 変数名 = new Function('引数','関数本体');
let myFunc = new Function('arg','return arg*arg');
let squared = myFunc(3);
console.log(squared);

実行時に文字列をコードとして実行するのは、eval()等と同じくセキュリティ的によくないので使わないほうがいいです。

関数リテラル表現を使う

let 変数名 = function(引数) {関数本体};
let myFunc = function(arg) {
    return arg * arg;
}
let squared = myFunc(3);
console.log(squared);

function命令を使った場合と似ていますが、無名関数を定義してからmyFuncという変数に格納しています。
後述のアロー関数が使えない場合はこれを使うのがベターだと思います。

function命令とは違い静的な関数定義とはならない
let squared = myFunc(3);      // ReferenceError: myFunc is not defined となる
console.log(squared);

let myFunc = function(arg) {
    return arg * arg;
}

アロー関数を使う

ES2015からはアロー関数によるユーザー定義関数が使えます。
functionに比べて、静的な宣言にならず挙動は幾分分かりやすいです。
またthisが固定化されるといった特徴もあります。
利用可能なら基本的にはアロー関数での定義を使う方がよさそうです。

これにより、関数の挙動が分かりやすく、バグを減らすことにつながりそうです。

let 変数名 = (引数1, 引数2, ...) => { 関数本体 };
let getTriangleArea = (base, height) => {
  return base * height / 2;
};
console.log(getTriangleArea(5, 2));

引数が1つの場合、括弧が不要だったり色々省力出来ます。

関数はデータ型の一種

JavaScriptの世界では、関数は変数のデータ型の一種です。

function myFunc1(arg) {                             // 1の方法
  return arg * arg;
};
console.log(typeof myFunc1);

let myFunc2 = new Function('return arg * arg;');    // 2の方法
console.log(typeof myFunc2);

let myFunc3 = function(arg) {                       // 3の方法
    return arg * arg;
};
console.log(typeof myFunc3);

let myFunc4 = () => {                               // 4の方法
    return arg * arg;
};
console.log(typeof myFunc4);
実行結果
function
function
function
function

関数とは言えデータ型のの1種なので、別の型のリテラルを代入すると変数は別の型になります。

function myFunc(arg) {
  return arg * arg;
};
console.log(typeof myFunc);     // 出力:function

myFunc = 1;                     // 数値型のリテラルを代入
console.log(typeof myFunc);     // 出力:number
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScriptによるバックエンドとフロントエンドの両面開発

TypeScriptによるバックエンドとフロントエンドの両面開発

 ※全てをTypeScriptで記述したツリー型情報掲載システムを運用中です

1.言語の選択

 Web開発を一つの言語で行おうと思った場合、事実上の選択肢は素のJavaScriptを使うか、AltScript系を使うことになります。

 前者のJavaScriptは気軽にコーディングできる反面、気付かぬうちに危険なコードが混入します。JavaScriptは仕様をきっちり把握していればある程度回避できますが、実は地雷原をサンダルで散歩するに等しい言語なのです。今日は無事でも、明日は足が吹っ飛ばされるかもしれません。

 JavaScriptの危険性を回避するにはAltScript、中でも急速に伸びているTypeScriptを強くお勧めします。TypeScriptが一つ使えれば、フロントエンドとバックエンド両方が同じ言語で、しかもある程度の安全を担保した状態で開発出来るのです。

2.TypeScriptによる両面開発の利点

  • 同じ文法なので効率が上がる

    例えばバックエンドをPHP、フロントエンドをJavaScriptで開発したケースです。この二つの言語、文法が似ています。似ているが故に同時開発すると、拡張for文などでミスります。PHPでfor-inを使おうとしてしまったりと、一瞬で気が付くミスですが、けっこうやってしまいがちです。完全に同じ文法の言語を使えば、こういった部分でのストレスがありません。

  • ソースコードが使いまわせる

     フロントエンドとバックエンドで同じような処理を書く場合があります。言語が違えばいちいち書き直しですが、同じ言語ならコピペで終了です。共通ライブラリとして整備しておけば、コピペの必要すらありません。

  • データのやり取りが楽

     TypeScriptなら型が組めるわけですが、同じ言語ならこの型が共通化できます。特に威力を発揮するのは通信部分です。バックエンドとフロントエンドの通信は、Ajaxを用いた非同期通信を用います。やりとりの標準的な方法では、いったんJSON形式を通してパースし、お互いの終端でデコードして使います。TypeScriptなら、このデコード後の型が共通化できるのです。これが出来るか否かで、開発効率がまったく変わってきます。感覚的にはローカルファンクションを呼び出す感覚で通信が可能となるのです。

3.両面開発で気を付けるべきこと

3.1 開発環境の設定

 VSCodeなどの開発環境は、TypeScriptの文法チェックをするときにtsconfig.jsonを参照します。この設定値を元にチェック項目を判断するわけです。当然ですがバックエンドとフロントエンドでこの設定値が異なります。特に入出力ディレクトリは確実に異なった設定になるので、きっちり区別をつけておかないと、大量のエラーに悩まされることになります。

 まず最初にやらないといけないのは、プロジェクトルートのディレクトリに配置するtsconfig.jsonです。

tsconfig.json
{
  "exclude": [
    "."
  ]
}

 これを置いておかないと、全てのディレクトリの.tsが混合でチェックされてしまうので悲惨なことになります。これを配置したら、あとはフロントエンド用とバックエンド用のディレクトリそれぞれにtsconfig.jsonを作ればOKです。コンパイル時、手動でそれぞれコンパイルするなら

tsc -b configファイルのパス

とします。

 また、異なるtsconfig.jsonをトップのtsconfig.jsonから呼び出すこともできます。

tsconfig.json
{
  "references": [
    {
      "path": "フロントエンドパス"
    },{
      "path": "バックエンドパス"
    }
  ],
  "exclude": [
    "."
  ]
}

としておけば、

tsc -b

だけで、両方同時にビルドすることができます。ただし、フロントエンドはWebPackなどのモジュールバンドラを使うケースが多いのでその場合、ここに記述するのはバックエンドの参照のみになります。また、参照を受け付ける場合は、参照先の設定に

tsconfig.json
{
  "compilerOptions": {
    "composite": true
  }
}

を入れておく必要があります。

3.2 出来ることの違い

 バックエンドはファイルの入出力やDBアクセスなど、一通り何でもできます。ただしDOM操作などは標準対応しておらず、自分で組むか、jsdomのようなパッケージをnpmからとってくる必要があります。

 フロントエンドはDOM操作が標準でできますが、当然ファイル操作などは出来ません。ブラウザ内で許可されているlocalstrageを使ったり、バックエンドに入出力要求する必要があります。

 ということで使用可能なAPIはまったく異なるので、ここだけはきちんと区別して開発しなければなりません。

4.最終的な開発環境とビルド手順

 現在私の開発方法はフロントエンドにWebPack、バックエンドにtscという形で行っています。tscの方は参照設定で複数のプロジェクトを一コマンドでコンパイル出来るのですが、WebPackにはプロジェクトの参照設定がないので、ちょっと面倒くさいことになっています。

  • tsc - バックエンドメインプロジェクト - Active-Module-Framework
  • WebPack - フロントエンドメインプロジェクト
  • WebPack - JavaScript-Window-Framework

 バックエンドはActive-Module-Frameworkというオレオレフレームワークを使っており、バックエンドのメインプロジェクトとは独立させた構成になっています。tscは参照設定を使うことによって、この異なるプロジェクトの同時ビルドが可能です。

 フロントエンドはJavaScript-Window-Frameworkというオレオレフレームワークを使っており、フロントエンドのメインプロジェクトとは独立させた構成になっています。WebPackには参照設定がないので、フロントエンドのビルドには、WebPackを二回立ち上げます。-wオプションを入れて監視ビルドさせるので、最初にそれぞれ一回起動する手間があるだけなのですが、やっぱりちょっと面倒くさいです。

5.利点は大きいが移行するまでの壁も大きい

 TypeScriptによる両面開発は数々のメリットがある反面、壁が大きいのも確かです。まずNode.jsの非同期処理に慣れるまでそれなりに時間がかかります。これは素のJavaScriptでも同じですが、他の言語には無い特徴故に慣れるまでに時間を要します。さらにTypeScriptの型の扱いも、ケースごとにどう対処するのかという知識を蓄積するまでに、多少の時間をとられます。この壁を乗り越えた先にたどり着けるかどうかは、各々のやり方次第です。しかしWeb開発という面に限って言えば、とても大きな効率化を図ることが出来るのです。

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

「素数」という響きへのあこがれ(JavaScript)

はじめに

  • 私はバリバリの文系出身者ですが、「素数」という響きにものすごいあこがれと凄まじい数学の世界を感じます。
  • 「最大の素数を発見・・・」、「エラトステネスの篩(ふるい)にて・・・」、「スーパーコンピュータにて、これくらいまでの素数を出すと、〇〇秒・・・」などと聞くと、あこがれとともに、関係ないな・・、と思っていました。

※余談:次のような数学を題材にした映画が、数学がわからないながら地味に好きです。
・イミテーション・ゲーム/エニグマと天才数学者の秘密
・奇蹟がくれた数式
・博士が愛した数式

  • 素数を求めることに対する「処理速度」や「方法」の探求を抜きにすれば、プログラミング初学者にとっては、「if文」と「ループ」の勉強になると思います。そこで、JavaScriptの基礎を学んだあとに、素数を求める、というプログラムを書こうと思いました。

注意点

ということで、いかに早く素数を求めるか、とか、きれいなロジックで、といったことを追求しているわけではなく、「素数もしくは数学に対するあこがれ」からくる「プログラミング練習」となります。

やったこと

①与えた秒数で、どれくらいの数まで素数か否かの確認でき、その中にどのくらいの素数を含まれているかを求めてみる
②与えた数字までに、いくつの素数が含まれているか、それを求めるまでどのくらいの秒数がかかるかを求めてみる

実際のもの・コード

①与えられた時間で素数カウント

1pg.png

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="utf-8">
  <title>素数カウント</title>
</head>

<body>
  <h1>与えられた時間で素数カウント</h1>
  <span>時間(秒):</span>
  <input type="number" id="time" min="1" max="30" value="">
  <p><button id="calcBtn">カウント</button></p>

  <p id="msg"></p>
  <p>確認できた数:<span id="number"></span></p>
  <p>素数の数:<span id="outputNum"></span></p>
  <p>素数:<span id="output"></span></p>

  <script>
    // 計算ボタンが押下されたとき
    var calcBtn = document.getElementById('calcBtn');
    calcBtn.addEventListener('click', function(){

      // 初期処理
      document.getElementById('msg').innerHTML = '';
      document.getElementById("number").innerHTML = '';
      document.getElementById("outputNum").innerHTML = '';
      document.getElementById("output").innerHTML = '';

      // 入力値チェック
      var setTime = document.getElementById('time').value;
      if(setTime > 30){
        document.getElementById('msg').innerHTML = "30秒以内にしてください。息切れします";
        return;
      }

      var i = 3;
      var nowTime = 0;
      var outputNum = 1;
      var output = '2,  ';
      var noPrime;

      // 開始時間セット
      var startTime = new Date();

      while(nowTime < setTime) {

        // 2で割り切れなければ(奇数ならば)、確認ロジックにはいる
        if((i % 2) != 0) {

          noPrime = '';

          for(var j = 2; j < i; j++){
            //割り切れた時点で素数ではない
            if((i % j) == 0){
              noPrime = 'on';
              break;
            }
          }

          if(noPrime == ''){
            outputNum++;              // 素数の数をカウントアップ
            output += i + ',  ';      // 素数を格納
          }
        }

        i++;       // 確認数字のカウントアップ

        var stopTime = new Date();      // 終了時間
        var ms = stopTime.getTime() - startTime.getTime();    // 経過時間をミリ秒で取得
        var nowTime = ms / 1000;

      }

      document.getElementById("number").innerHTML = i;
      document.getElementById("outputNum").innerHTML = outputNum;
      document.getElementById("output").innerHTML = output;

    });
  </script>
</body>
</html>

②与えられた数字で素数カウント

2pg.png

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="utf-8">
  <title>素数カウント</title>
</head>

<body>
  <h1>与えられた数字で素数カウント</h1>
  <span>数字:</span>
  <input type="number" id="num" min="3" max="999999" value="">
  <p><button id="calcBtn">カウント</button></p>

  <p id="msg"></p>
  <p>処理にかかった時間:<span id="procTime"></span></p>
  <p>素数の数:<span id="outputNum"></span></p>
  <p>素数:<span id="output"></span></p>

  <script>
    // 計算ボタンが押下されたとき
    var calcBtn = document.getElementById('calcBtn');
    calcBtn.addEventListener('click', function(){

      // 初期処理
      document.getElementById('msg').innerHTML = '';
      document.getElementById("outputNum").innerHTML = '';
      document.getElementById("output").innerHTML = '';

      // 入力値チェック
      var setNum = document.getElementById('num').value;
      if(setNum > 999999){
        document.getElementById('msg').innerHTML = "数は999999以下にしてください";
        return;
      }

      var outputNum = 1;
      var output = '2,  ';
      var noPrime;

      // 開始時間セット
      var startTime = new Date();

      // 数字をループして調査
      for(var i = 3; i <= setNum; i++) {

        // 2で割り切れなければ(奇数ならば)、確認ロジックにはいる
        if((i % 2) != 0) {

          noPrime = '';

          for(var j = 2; j < i; j++){
            // 割り切れた時点で素数ではない
            if((i % j) == 0){
              noPrime = 'on';
              break;
            }
          }

          if(noPrime == ''){
            outputNum++;              // 素数の数をカウントアップ
            output += i + ',  ';      // 素数を格納
          }
        }
      }

      // 終了時間をセット
      var stopTime = new Date();

      // 経過時間をミリ秒で取得
      var ms = stopTime.getTime() - startTime.getTime();
      var s = ms / 1000

      document.getElementById("procTime").innerHTML = s;
      document.getElementById("outputNum").innerHTML = outputNum;
      document.getElementById("output").innerHTML = output;

    });
  </script>
</body>
</html>

まとめ

・言語の基礎を学んだ後は、もしくは、学んでいる最中には、素数を出してみるプログラムをやってみると意外と勉強になると思いますので、「数学」・「素数」という言葉の響きにあこがれている方でもそうでない方も実践してみるとよいかもしれません。そして、おもしろいと思ったら探求と開始すればよいかなと思います。

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

「時間バッテリー」を表示する(JavaScript)

はじめに

  • JavaScriptの基礎を学んだ後に2番目に作成したミニアプリです。
  • どの言語でもそうだと思いますが、日付や時間を扱うのはプログラミング基礎を学んだ後にやっておくとよいと思っております。(そう勝手に思っています)
  • そこで、「時間バッテリー」というタイトルで、現在を基準に、年末まで・月末まで・今日の終わりまでを算出してプログレスバーを表示してみようと思いました。

やったこと

  • 現在日付から年末までの日数カウント、バッテリー%表示
  • 現在日付から今月末までの日数カウント、バッテリー%表示
  • 現在日付から今月末までの日数カウント、バッテリー%表示
  • 基準日からターゲット日までの日数カウント、バッテリー%表示

デモサイト
time.png

コード(HTML/CSS/JavaScript)

timebattery.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial=1.0">
    <title>TimeBattery</title>
    <link type="text/css" rel="stylesheet" href="https://code.jquery.com/ui/1.10.3/themes/cupertino/jquery-ui.min.css">
    <link rel="stylesheet" href="./timebattery.css">
  </head>

  <body>
    <h1>Time Battery</h1>
    <p id="nowtime"></p>
    <p id="year"></p>
    <div class="progress1"><span id="loading1"></span></div>
    <br>
    <p id="month"></p>
    <div class="progress2"><span id="loading2"></span></div>
    <br>
    <p id="today"></p>
    <div class="progress3"><span id="loading3"></span></div>
    <br>
    <p>[カレンダー対応ブラウザ] Chrome、Opera、MS Edge</p>
    <p>
      基準日 <input id="cal_start" type="date" name="calendar" max="9999-12-31">
      ~ ターゲット日 <input id="cal_end" type="date" name="calendar" max="9999-12-31">
    </p>
    <p id="result"></p>
    <div class="progress4"><div id="loading4"></div></div>

    <script type="text/javascript" src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
    <script type="text/javascript" src="https://code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script>
    <script type="text/javascript" src="./timebattery.js"></script>
  </body>
</html>
timebattery.css
body {
    text-align: center;
    font-family: Arial, Helvetica, sans-serif;
    font-size: 14px;
}
.progress1, .progress2, .progress3, .progress4 {
    margin: 0 auto;
    width: 50%;
}
#loading1, #loading2, #loading3, #loading4 {
    position: absolute;
    left: 50%;
    font-weight: bold;
}
timebattery.js
$(function() {
    'use strict';

    var nowDate = new Date();
    var year = nowDate.getFullYear();     // 年(4桁の西暦)
    var mon  = nowDate.getMonth();        // 月(0~11)
    var date = nowDate.getDate();         // 日(1~31)
    var hour = nowDate.getHours();        // 時(0~23)
    var min  = nowDate.getMinutes();      // 分(0~59)
    var sec  = nowDate.getSeconds();      // 秒(0~59)

    //現在時刻の表示
    var month = mon + 1;
    var nowtime = "現在: " + year + "年" + month + "月" + date + "日";
    document.getElementById("nowtime").innerHTML = nowtime;

    //目的の日付をセット
    var yearEnd = new Date(year, 11, 31);         //年末の日付をセット
    var monthEnd = new Date(year, mon + 1, 0);    //今月末をセット
    var todayEnd = new Date(year, mon, date, 23, 59, 59);   //今日の最終時刻をセット

    //それぞれを数値化
    var dnumNow = nowDate.getTime();        //現在時刻の数字をget
    var dnumYearEnd = yearEnd.getTime();    //年末の数字をget
    var dnumMonthEnd = monthEnd.getTime();  //今月末の数字をget
    var dnumTodayEnd = todayEnd.getTime();  //今日の数字をget

    //それぞれのENDと現在との差を計算
    var diffYear = dnumYearEnd - dnumNow;
    var diffMonth = dnumMonthEnd - dnumNow;
    var diffToday = dnumTodayEnd - dnumNow;

    //差と%を計算(年)
    var diffDays1 = diffYear / ( 1000 * 60 * 60 * 24 );       // 日数を割り出す
    var showDays1 = Math.ceil( diffDays1 );                   // 小数点以下を切り上げる
    var yearValue = Math.ceil( showDays1 / 365.25 * 100 );    // %を計算
    //差と%を計算(月)
    var diffDays2 = diffMonth / ( 1000 * 60 * 60 * 24 );      // 日数を割り出す
    var showDays2 = Math.ceil( diffDays2 );                   // 小数点以下を切り上げる
    var monthValue = Math.ceil( showDays2 / 30 * 100 );       // %を計算
    //差と%を計算(時間)
    var todayValue = Math.ceil(diffToday / ( 1000 * 60 * 60 * 24 ) * 100);     // %を計算
    var dHour  = diffToday / ( 1000 * 60 * 60 );   // 時間
    diffToday = diffToday % ( 1000 * 60 * 60 );
    var dMin   = diffToday / ( 1000 * 60 );        // 分
    diffToday = diffToday % ( 1000 * 60 );
    var dSec   = diffToday / 1000;                 // 秒
    var showDays3 = Math.floor(dHour) + "時間" + Math.floor(dMin) + "分" + Math.floor(dSec) + "秒";

    //年末までの日数を表示
    var yearEnd = "年末まで あと" + showDays1 + "日";
    document.getElementById("year").innerHTML = yearEnd;

    //今月末までの日数を表示
    var monthEnd = "今月末まで あと" + showDays2 + "日";
    document.getElementById("month").innerHTML = monthEnd;

    //今日の終わりまでの時間を表示
    var todayEnd = "今日の終わりまで あと" + showDays3;
    document.getElementById("today").innerHTML = todayEnd;

    //プログレスバーと%テキスト表示
    progress('.progress1', yearValue, '#loading1');
    progress('.progress2', monthValue, '#loading2');
    progress('.progress3', todayValue, '#loading3');

    function progress(barNum, value, loadNum){
        // プログレスバーを生成
        $(barNum).progressbar({
            value: value,
            max: 100
        });
        // %のテキスト表示
        var per = $(barNum).progressbar('value') / $(barNum).progressbar('option', 'max');
        $(loadNum).text(Math.ceil(per * 100) + '%');
        // %の色つけ
        $(barNum).each(function(){
            var selector = $(this).find('div');
            var value = this.getAttribute("aria-valuenow");

            if (value >= 50 ){
                $(selector).css({ 'background': 'LightGreen' });
            } else if (value >= 30){
                $(selector).css({ 'background': 'LightYellow' });
            } else {
                $(selector).css({ 'background': 'Pink' });
            }
        });
    }

    //カレンダーにデフォルト日付を設定
    var mm = ("0"+(nowDate.getMonth()+1)).slice(-2);
    var dd = ("0"+ nowDate.getDate()).slice(-2);
    var calStartYear = year - 1;
    document.getElementById("cal_start").value = calStartYear + '-' + mm + '-' + dd;
    var calEndYear = year + 1;
    document.getElementById("cal_end").value = calEndYear + '-' + mm + '-' + dd;

    //変数設定
    var cal_startForm = document.getElementById('cal_start');
    var cal_endForm = document.getElementById('cal_end');

    //カレンダー日が変更されたときfunction timecalcを呼び出す
    cal_startForm.addEventListener('change', timecalc);
    cal_endForm.addEventListener('change', timecalc);

    function timecalc(){

        var startDate = new Date(cal_startForm.value);
        var targetDate = new Date(cal_endForm.value);

        //設定された日付が過去日付であったらメッセージ表示
        if(targetDate < nowDate) {
            document.getElementById("result").innerHTML = "未来日付を設定してください";
        } else {
            //数値化
            var dnumStart = startDate.getTime();
            var dnumNow = nowDate.getTime();
            var dnumTarget = targetDate.getTime();

            //それぞれのENDと現在との差を計算
            var calDiff1 = dnumTarget - dnumNow;
            var calDiff2 = dnumTarget - dnumStart;

            //差と%を計算(月)
            var calDiffDays1 = calDiff1 / ( 1000 * 60 * 60 * 24 );   // 日数を割り出す
            var calDiffDays2 = calDiff2 / ( 1000 * 60 * 60 * 24 );   // 日数を割り出す

            var calShowDays1 = Math.ceil( calDiffDays1 );                // 小数点以下を切り上げる
            var calShowDays2 = Math.ceil( calDiffDays2 );                // 小数点以下を切り上げる

            var dateValue1 = Math.ceil(calShowDays1 / calShowDays2 * 100);      //  %を計算
            var dateValue2 = calShowDays2 - calShowDays1;

            var dateResult = "ターゲット日付まであと" + calShowDays1 + "日" + "(" + dateValue2 + "日経過)";
            document.getElementById("result").innerHTML = dateResult;

            // プログレスバーとテキスト表示
            progress('.progress4', dateValue1, '#loading4');

        }
    };
});

まとめ

  • 言語の基礎を学んだ後は、日付や時間計算のプログラムをやるとよい、と個人的に思います。
  • 「時間バッテリー」というタイトルで、プログレスバーの表示をやってみました。
  • バッテリー形式にしているので、年末に向けてエネルギーがなくなるように見えるなぁ・・と思ったりもしますが(汗)。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「その会議いくら?」を算出する(JavaScript)

はじめに

  • JavaScriptの基礎を学んだ後に最初に作成したミニアプリです。

動機

  • 仕事で比較的な大きなプログジェクトに関わっているとき、30人以上が参加する「定例進捗会議」に出ることがありました。
  • 大きな声では言えませんが、お決まりの進捗会議ですので、本当にみんなで集まる必要あるの?という疑問が、おそらく全員が持っているような状態な感じです。(最近は少なくなりましたが、いまだにあるところにはありますよね、そんな会議。。)
  • そこで、「いったい、いくらこの会議にかかっているんだ?」と疑問に思いましたので、学習を兼ねて、JavaScriptで算出してみることにしました。
  • 探してみると、How much does this meeting pay? といったサイト(英語)もあるようです。

やろうと思ったこと

  • 「参加人数」「平均月給」「月の仕事時間」を入力パラメータにしました。(それしか思いつかなかった・・・)

  • 会議参加者の「平均月給」「月の仕事時間」をどうやって知るんだ、というツッコミは置いておいて、そこから、会議にかけた総合計時間とコストを計算しようと思いました。

注意点

  • 「参加人数」「平均月給」「月の仕事時間」というパラメータしか思いつきませんでしたが、本当の会議の価値・コストをこれだけで測ることはできないと思います。
  • 解決案をまとめる・ものごとが進む・Before/Afterで変化がある、などといったことが会議では重要ですので。
  • 改善アイデアとして、「その会議の活発度」も一緒に取り込めるとよいのかな、と思ったりもします。(もうそんなアプリあるのかな・・) 例:「無音状態の少なさ」「喋っている人の数」「雰囲気(感情分析:ToneAnalyzerとか?)」 など

やったこと

  • 「参加人数」「平均月給」「月の仕事時間」をパラメータに、会議にかけた総合計時間とコストを計算する。休憩なども考慮して、START/STOPを何度でもできるようにする。

  • デモサイト

1.gif

コード(HTML/CSS/JavaScript)

conference.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial=1.0">
    <title>その会議いくら?</title>
    <link rel="stylesheet" href="./conference.css">
  </head>

  <body>
    <h1>その会議いくら?</h1>
    <p>参加人数:<input type="text" name="people" id="people" value="30"></p>
    <p>平均月給:<input type="text" name="salary" id="salary" value="350000"></p>
    <p>月の仕事時間:<input type="text" name="worktime" id="worktime" value="160" > 時間</p>
    <p>時:<span id="hourly">0</span>円 / 分:<span id="minute">0</span>円 / 秒:<span id="second">0</span></p>
    <br>
    <p>かけた時間:<span id="timeText">0</span></p>
    <p>かかったコスト:<span id="costText">0</span></p>
    <br>
    <p>経過時間:<span id="timerText">0</span></p>
    <br>
    <div id="start">会議START</div>
    <div id="stop">会議STOP</div>
    <div id="reset">リセット</div>

    <script type="text/javascript" src="https://code.jquery.com/jquery-1.10.2.min.js"></script>
    <script type="text/javascript" src="https://code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script>

    <script type="text/javascript" src="./conference.js"></script>

  </body>
</html>
conference.css
body {
    text-align: center;
    font-family: Arial, Helvetica, sans-serif;
    font-size: 16px;
}
#people, #salary, #worktime {
    width: 80px;
    text-align: right;
}
#timerText {
    color: blue;
    font-size: 32px;
}
#timeText, #costText {
    color: red;
    font-size: 32px;
}
.btn {
    display: inline-block;
    width: 90px;
    padding: 6px;
    border-radius: 5px;
    box-shadow: 0 4px 0 #3a00cc;
    color: #fff;
    background: #7b00ee;
    cursor: pointer;
}
.btn + .btn {
    margin-left: 5px;
}
.btn.active {
    opacity: 1.0;
}
.btn.inactive {
    opacity: 0.5;
}
conferenece.js
(function() {
    'use strict';

    //平均月給フォーカスアウト
    $('#salary').on('blur', function(){
        var num = $(this).val();
        num = num.replace(/(\d)(?=(\d\d\d)+$)/g, '$1,');
        $(this).val(num);
    });
    //平均月給フォーカス
    $('#salary').on('focus', function(){
        var num = $(this).val();
        num = num.replace(/,/g, '');
        $(this).val(num);
    });

    //デフォルト表示の取得
    var people = document.getElementById('people').value;
    var salary = document.getElementById('salary').value;
    var worktime = document.getElementById('worktime').value;
    //デフォルト値の計算
    var hSalary = ( people * salary ) / worktime;
    var mSalary = hSalary / 60;
    var sSalary = mSalary / 60;
    //デフォルト値のセット
    document.getElementById('hourly').innerHTML = Math.round(hSalary).toLocaleString();
    document.getElementById('minute').innerHTML = Math.round(mSalary).toLocaleString();
    document.getElementById('second').innerHTML = Math.round(sSalary).toLocaleString();

    //項目が変更されたときは再計算を実施
    $('#people').change(function() {
        people = document.getElementById('people').value;
        calcSalary();
    });

    $('#salary').change(function() {
        salary = document.getElementById('salary').value;
        calcSalary();
    });

    $('#worktime').change(function() {
        worktime = document.getElementById('worktime').value;
        calcSalary();
    });

    var hSalary_recalc;
    var mSalary_recalc;
    var sSalary_recalc;

    function calcSalary(){
        hSalary_recalc = ( people * salary ) / worktime;
        mSalary_recalc = hSalary_recalc / 60;
        sSalary_recalc = mSalary_recalc / 60;
        document.getElementById('hourly').innerHTML = Math.round(hSalary_recalc).toLocaleString();
        document.getElementById('minute').innerHTML = Math.round(mSalary_recalc).toLocaleString();
        document.getElementById('second').innerHTML = Math.round(sSalary_recalc).toLocaleString();
        sSalary = sSalary_recalc;
    };

    //変数設定
    var startTime;
    var timerId;
    var elapsedTime = 0;
    var isRunning = false;

    var startButton = document.getElementById('start');
    var stopButton = document.getElementById('stop');
    var resetButton = document.getElementById('reset');
    var timerText = document.getElementById('timerText');
    var timeText = document.getElementById('timeText');
    var costText = document.getElementById('costText');

    //ボタンステータス管理
    function setButtonState(start, stop, reset){
        startButton.className = start ? 'btn active' : 'btn inactive';
        stopButton.className = stop ?  'btn active' : 'btn inactive';
        resetButton.className = reset ?  'btn active' : 'btn inactive';
    }

    //初期画面(startボタンのみON)
    setButtonState(true, false, false);

    //startボタンが押下されたとき
    startButton.addEventListener('click', function(){
        if(isRunning){
            return;
        }
        isRunning = true;
        startTime = Date.now();               //19700101 00:00:00からの経過ミリ秒
        updateTimerText();
        setButtonState(false, true, false);   //ボタンのステータス:STOPをON
    });

    //stopボタンが押下されたとき
    stopButton.addEventListener('click', function(){
        if(!isRunning){
            return;
        }
        isRunning = false;
        elapsedTime += Date.now() - startTime;
        clearTimeout(timerId);
        setButtonState(true, false, true);     //ボタンのステータス:Start/ResetをON
    });

    //resetボタンが押下されたとき
    resetButton.addEventListener('click', function(){
        if(isRunning){
            return;
        }
        timerText.innerHTML = '0';
        timeText.innerHTML = '0';
        costText.innerHTML = '0';
        elapsedTime = 0;
        setButtonState(true, false, false);    //ボタンのステータス:StartをON
    });

    //経過時間・時間・コストの更新
    function updateTimerText(){
        timerId = setTimeout(function(){
            var t = Date.now() - startTime + elapsedTime;
            timerText.innerHTML = (t / 1000).toFixed(0);
            timeText.innerHTML = ((timerText.innerHTML * people) / 60).toFixed(1);
            costText.innerHTML = Math.round(sSalary * timerText.innerHTML).toLocaleString();
            updateTimerText();
        }, 10);
    }

})();

まとめ

  • 言語の基礎を学んだ後は、日付や時間計算のプログラムをやるとよい、と個人的に思います。
  • 今回は「会議」というテーマで時間のプログラムを学習してみましたが、他のテーマでも自分が楽しんでやれれば学習テーマはなんでもありだと思います(例:素数日を出してみるなど)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む