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

gatsby入門 チュートリアルをこなす 8. 公開するサイトの準備

gatsbyの作業履歴

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

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

今回実施するgatsbyのチュートリアルはこちら
https://www.gatsbyjs.com/tutorial/part-eight/
基本はこれで最後かな?
チュートリアルの冒頭にこう書かれています。

この最後のセクションでは、Lighthouseと呼ばれる強力なサイト診断ツールを導入して、サイトを稼働させるための一般的な手順をいくつか説明します。途中で、Gatsbyサイトでよく使用するプラグインをいくつか紹介します。

※google翻訳です。
早速やっていきましょう。
ソースは前回作ったやつを使用します。

Preparing a Site to Go Live

Audit with Lighthouse

何やら監査をするそうな。
まずサイトの本番ビルドが必要のようです。

Create a production build

まず、すでに開発サーバが起動している場合は停止する。
そして、以下コマンドを実行する。
gatsby build
そして
gatsby serve
そして以下にアクセス
http://localhost:9000
2020-09-06_22h52_26.jpg
OK!

Run a Lighthouse audit

続いてLighthouseテストを実行します。
手順は以下とチュートリアルに記載

1.Chromeシークレットモードでサイトを開いて、拡張機能がテストに干渉しないようにします。次に、Chrome DevToolsを開きます。

2020-09-06_22h57_05.jpg
2020-09-06_22h58_05.jpg
2020-09-06_22h59_34.jpg
なんか出てきた。
Generate reportを押します。
2020-09-06_23h01_07.jpg
またなんか出てきた。
なんか黄色がいっぱい。。。

Add a manifest file

LighthouseのProgressive Web App不足を見直すみたい。
そしてなんだかmanifestを作れば良いらしい。
そしてGatsby’s manifest pluginを使うみたい

Using gatsby-plugin-manifest

プラグインを以下コマンドでインストール
npm install --save gatsby-plugin-manifest
※サイト直下のディレクトリで実行
とりあえずサーバ停止して実行しました。
何やらicon.pngがsrc/images配下にいるみたい。
適当にどっかから持ってくる。
gatsby-config.jsを修正

gatsby-config.js
module.exports = {
  siteMetadata: {
    title: `Pandas Eating Lots`,
  },
  plugins: [
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `src`,
        path: `${__dirname}/src/`,
      },
    },
    `gatsby-plugin-emotion`,
    `gatsby-transformer-remark`,
    {
      resolve: `gatsby-plugin-typography`,
      options: {
        pathToConfigModule: `src/utils/typography`,
      },
    },
    ↓ここから
    {
      resolve: `gatsby-plugin-manifest`,
      options: {
        name: `GatsbyJS`,
        short_name: `GatsbyJS`,
        start_url: `/`,
        background_color: `#6b37bf`,
        theme_color: `#6b37bf`,
        // Enables "Add to Homescreen" prompt and disables browser UI (including back button)
        // see https://developers.google.com/web/fundamentals/web-app-manifest/#display
        display: `standalone`,
        icon: `src/images/icon.png`, // This path is relative to the root of the site.
      },
    },
    ↑ここまで追記
  ],
}

※過去チュートリアルの場合です。

Add offline support

なんかpwaにservice workerがいるみたい。
Gatsby’s offline pluginをインストールします。
プラグインを以下コマンドでインストール
npm install --save gatsby-plugin-offline
gatsby-config.jsを修正

gatsby-config.js
module.exports = {
  siteMetadata: {
    title: `Pandas Eating Lots`,
  },
  plugins: [
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `src`,
        path: `${__dirname}/src/`,
      },
    },
    `gatsby-plugin-emotion`,
    `gatsby-transformer-remark`,
    {
      resolve: `gatsby-plugin-typography`,
      options: {
        pathToConfigModule: `src/utils/typography`,
      },
    },
    {
      resolve: `gatsby-plugin-manifest`,
      options: {
        name: `GatsbyJS`,
        short_name: `GatsbyJS`,
        start_url: `/`,
        background_color: `#6b37bf`,
        theme_color: `#6b37bf`,
        // Enables "Add to Homescreen" prompt and disables browser UI (including back button)
        // see https://developers.google.com/web/fundamentals/web-app-manifest/#display
        display: `standalone`,
        icon: `src/images/icon.png`, // This path is relative to the root of the site.
      },
    },
    `gatsby-plugin-offline`,←これ追記
  ],
}

Add page metadata

ページにメタデータを追加します。
またまたプラグインをインストール

Using React Helmet and gatsby-plugin-react-helmet

以下コマンドを実行
npm install --save gatsby-plugin-react-helmet react-helmet
gatsby-config.jsを修正

gatsby-config.js
module.exports = {
  siteMetadata: {
    title: `Pandas Eating Lots`,
    description: `A simple description about pandas eating lots...`,←これ追記
    author: `gatsbyjs`,←これ追記
  },
  plugins: [
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `src`,
        path: `${__dirname}/src/`,
      },
    },
    `gatsby-plugin-emotion`,
    `gatsby-transformer-remark`,
    {
      resolve: `gatsby-plugin-typography`,
      options: {
        pathToConfigModule: `src/utils/typography`,
      },
    },
    {
      resolve: `gatsby-plugin-manifest`,
      options: {
        name: `GatsbyJS`,
        short_name: `GatsbyJS`,
        start_url: `/`,
        background_color: `#6b37bf`,
        theme_color: `#6b37bf`,
        // Enables "Add to Homescreen" prompt and disables browser UI (including back button)
        // see https://developers.google.com/web/fundamentals/web-app-manifest/#display
        display: `standalone`,
        icon: `src/images/icon.png`, // This path is relative to the root of the site.
      },
    },
    `gatsby-plugin-offline`,
    `gatsby-plugin-react-helmet`,←これ追記
  ],
}

src/components/seo.jsを追加し以下を記述

src/components/seo.js
import React from "react"
import PropTypes from "prop-types"
import { Helmet } from "react-helmet"
import { useStaticQuery, graphql } from "gatsby"

function SEO({ description, lang, meta, title }) {
  const { site } = useStaticQuery(
    graphql`
      query {
        site {
          siteMetadata {
            title
            description
            author
          }
        }
      }
    `
  )

  const metaDescription = description || site.siteMetadata.description

  return (
    <Helmet
      htmlAttributes={{
        lang,
      }}
      title={title}
      titleTemplate={`%s | ${site.siteMetadata.title}`}
      meta={[
        {
          name: `description`,
          content: metaDescription,
        },
        {
          property: `og:title`,
          content: title,
        },
        {
          property: `og:description`,
          content: metaDescription,
        },
        {
          property: `og:type`,
          content: `website`,
        },
        {
          name: `twitter:card`,
          content: `summary`,
        },
        {
          name: `twitter:creator`,
          content: site.siteMetadata.author,
        },
        {
          name: `twitter:title`,
          content: title,
        },
        {
          name: `twitter:description`,
          content: metaDescription,
        },
      ].concat(meta)}
    />
  )
}

SEO.defaultProps = {
  lang: `en`,
  meta: [],
  description: ``,
}

SEO.propTypes = {
  description: PropTypes.string,
  lang: PropTypes.string,
  meta: PropTypes.arrayOf(PropTypes.object),
  title: PropTypes.string.isRequired,
}

export default SEO

src/templates/blog-post.jsを修正

src/templates/blog-post.js
import React from "react"
import { graphql } from "gatsby"
import Layout from "../components/layout"
import SEO from "../components/seo"←これ追記

export default function BlogPost({ data }) {
  const post = data.markdownRemark
  return (
    <Layout>
      <SEO title={post.frontmatter.title} description={post.excerpt} />←これ追      <div>
        <h1>{post.frontmatter.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: post.html }} />
      </div>
    </Layout>
  )
}

export const query = graphql`
  query($slug: String!) {
    markdownRemark(fields: { slug: { eq: $slug } }) {
      html
      frontmatter {
        title
      }
      excerpt←これ追記
    }
  }
`

これでもう一回Lighthouseを実行か
もう一回ビルドして動かすか。。。
2020-09-06_23h47_53.jpg
なんかあんまり向上はしてねぇな。
まぁでもちょっと向上したからいいか。HTTPSにするとか今のところ無理だし!

Keep making it better

Lighthouseを見ながら良いもの作っていってくださいねー!
だって。
基本は以上であとはまだチュートリアルがあるから見て作っていってね!
だって。

う~ん。。。どうしよう。
とりあえず今回はここまで。

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

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

「' '」はString、「` `」は実行。

「''」はStringで文字列などを入れますが、「``」は実行するためのもの(※文字列も入れられる)です。
この中ではプロパティのデータを持ってきたりすることができます。

Vue.jsでVuexを使っている時を参考にあげると、
下記のような実装が可能です。

index.vue
this.$router.push(
  `/${this.$store.state.hoge.fuga}/index`
)

pushのパス内でstateのプロパティを引っ張ってきて参照したりすることができます。

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

1つ目のAPIが通った場合のみ次のAPIを実行するエラーハンドリング

1つ目のAPIを実行してうまく通った場合にのみ、
次のAPIを実行するような処理を実装する一例。

※下記のサンプルではVuex、Axiosで実装しています

index.js
async getData({ commit, state }) {
  try {
    await this.$axios.
      $get('URI')
      .then(() => {
        this.$axios
          .$get('URI')
          .then((resp) => {
            commit('setData', resp)
          })
          .catch((error) => {
            console.log(error)
          })
      } catch((error) => {
        console.log(error)
      })
    } catch (error) {
      console.log(error) [
    }
  }
}

Actiosの中での実装を想定しているので、
commitでMutationsにある(想定)setDataにデータを渡しています。

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

JSエコシステムぶらり探訪(1): 原初のJavaScript

JavaScriptはWebブラウザの <script> から始まりました。まずはこの最も基本的な使い方を見ていきます。

目次

JavaScriptの実行モデル

モジュールの同期・非同期読み込みの話と関係するため、あらかじめJavaScriptの実行モデルを整理しておきます。

JavaScriptはシングルスレッドです。つまり以下のようなコードで、別のJavaScriptの処理の割り込みを受けません。

window.counter = 0;
console.log(`counter = ${window.counter}`);
window.counter += 1;
// ←この位置で別のJavaScriptが実行されることはない
console.log(`counter = ${window.counter}`); // ←常に1になる

このことは、長い間スクリプトが実行される場合に問題になります。実際に問題になるのは無限ループの場合です。何かの処理が無限ループになってしまうと、ブラウザはそのページに関する処理をいずれも続行できなくなってしまいます (他のJavaScriptコードを同時に実行するわけにはいかないため)

もし、同期的なsleep関数があると、同じような問題が起きてしまいます。

// もし、同期的なsleep関数があった場合……
console.log("foo");
sleep(1000); // ←この実行中、他のJavaScriptコードは実行できない
console.log("bar");

そのためJavaScriptには上のような同期的な sleep 関数は実際には存在せず、 setTimeout によって非同期的に次の処理を実行するような構成になっています。1

console.log("foo");
// setTimeout自体は一瞬で終わり、1秒後に "baz" が表示される
setTimeout(() => {
  console.log("baz");
}, 1000);
// setTimeout自体は一瞬で終わるので、 "bar" はすぐに表示される
console.log("bar");

その後登場し、ajaxの一時代を築いたXMLHttpRequestを含め、時間のかかる処理をするライブラリ関数は本質的には同じ設計を継承しています。ただし、上のようにコールバックを明示的に書くとコールバック地獄に陥り、実際のロジックに対してコードの見通しが悪くなるため、ライブラリサポートであるPromiseや構文サポートであるasync関数が導入されました。

原初のJavaScript

原初、JavaScriptはブラウザで実行するもので、モジュールシステムはなく、特に指定がなければグローバル空間に値が定義される仕組みでした。

index.html
<!-- script.jsを読み込んで実行 -->
<script type="text/javascript" src="./script.js"></script>
<!-- HTML内に埋め込まれたスクリプトを実行 -->
<script type="text/javascript"><!--
alert("bar");
//--></script>
<noscript>JavaScriptを有効にしてください。</noscript>
script.js
alert("foo");

ブラウザ上のJavaScriptには window という特殊な変数があり、グローバルな文脈で定義された変数や関数は window のメンバとして扱われます。 2 3

var x = 2;
function f() { return 3; }
console.log(window.x); // => 2
console.log(window.f()); // => 3

scriptタグの同期的読み込み

原初のJavaScriptには document.write がありました。

<script>
  document.write("<p");
</script>
>hoge</p>

この恐ろしい機能のため、ブラウザは <script> タグのJavaScriptの読み込みと実行が終わるまでHTMLのパースをブロックする必要がありました。パースが進まないため、続くJavaScriptの読み込みや実行、画像の読み込みなど多くの操作も結果としてブロックされることになります。

実際のところ、JavaScriptで行う多くの作業は、文書が全て読み込まれてから行いたいはずです。 (たとえば、 document.getElementById("root")<div id="root"></div> が読み込まれた後に行いたいはずです。) こういった処理は次のように書かれることがありました。 4

window.onload = function() {
  var root = document.getElementById("root");
  // rootを使った処理……
};

それならば、そもそも <script> タグを文書の最初のほうに置いてもスクリプトの実行開始タイミングには寄与しないことになります。むしろスクリプト以外の文書の構成要素 (HTML本体や画像など) のロードをブロックするデメリットのほうが大きいため、一時期は以下のように文書の最後に <script> を置くのが望ましいとされていました。

  <!-- ... -->
  <script src="jquery.js"></script>
  <script src="script.js"></script>
</body>

scriptタグの非同期的読み込み

上記の古い挙動はHTML5で改善され、 defer, async, type="module" のいずれかが指定された場合にはパーサーをブロックしないようになりました。これらの関係は以下の通りです。

  • type="module"defer を暗黙的に含む
  • asyncdefer より強い
    • defer のほうが登場が古いため、 async を指定するときは defer も同時に指定してフォールバックを狙うことがある

asyncとdeferの違いについては省略します。

当然、これらの属性を指定した場合は document.write は使えなくなります。

scriptタグの非同期的読み込みに対応したブラウザでは、何を読み込むべきであるか早めに指定するほうが効率的であると考えられます。scriptタグは再び、文書の最初のほうに戻ってきました。

  <!-- ... -->
  <script src="jquery.js" defer></script>
  <script src="script.js" defer></script>
</head>
<body>
  <!-- ... -->

スコープを切る

原初のJavaScriptでは、せっかく var で変数宣言しても、それがグローバルスコープだったらグローバル定義になってしまうという問題がありました。

これを回避するために、無名関数を作り、その中で作業をするというパターンが確立されました。

var my_library = (function() {
  // my_libraryを作る作業
  // ここでvarを使っても、それ自体は外には漏れない
  return my_library;
})();

  1. その割には同期的にブロックしてダイアログを出す alert 関数などもあったりするので、過去にはこの考え方は必ずしも徹底されていなかったとも考えられます。後述する document.write もそのような設計のひとつと言えます。 

  2. ブラウザ以外に目を向けると、環境ごとに window, self, frames, global など別の名前があったため、現在では統一的な名前として globalThis が導入されています。 

  3. また、strictモードでない場合は var/const/letを使わない変数代入が可能で、この場合もグローバルになります。 

  4. DOMContentLoadedのほうが適切な場合もありますが、ここではより伝統的なonloadでの例を示しました。 

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

JSエコシステムぶらり探訪(目次)

概要

webpackとかbabelとか、JSのすごいエコシステムを基本 (ブラウザ、Node.js, npm) から順番に調べていきます。

経緯

最近は npx create-react-app ... とやるといい感じに環境ができていいのですが、複雑なことをやろうとしたり、既に複雑になってしまったものを触るにはきちんと中身がわかってたほうがいいな……と思い始めたのでぼちぼち自分で調べ始めました。アウトプットしながら調べたほうが効率がいいので、このように記事として残します。

目次 (予定)

とりあえず思いつくだけ書き出したもので、全ての内容を確約するものではありません。また、調査を進めるにつれて目次も変化します。

  • 原初のJavaScript
  • Node.jsとCommonJS modules
  • NPM
    • コマンドラインツールの解決
  • webpack
    • CSS
    • 各種アセット
    • source map
  • polyfillとmock
    • whatwg-fetch / node-fetch
    • jsdom
    • etc.
  • babel
  • RequireJSとAMD
  • ES Modules
    • webpackとES Modules
    • Node.jsとES Modules
    • WebブラウザとES Modules
  • モジュール応用編
    • 非同期import
    • トップレベルawait
    • tree shaking
  • TypeScript
    • namespaceとmodule
    • tsc transpiler
    • babel transpiler
  • webpack-dev-server
  • クロスオリジン: CORS, crossorigin attribute, subresource integrity
  • Web Worker
  • jest
  • eslint
  • react-scripts
    • react-app-rewired, rescripts, craco
  • Railsアセットパイプライン
    • sprockets
    • webpacker
    • 統合テスト (Capybara, etc.)
  • WebAssembly
  • deno
  • 代替bundler: browserify, rollup, parcel, fusebox, esbuild
  • bower
  • さまざまなトランスパイラ
    • CoffeeScript
    • sass
    • PostCSS
    • jsx (React)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VSCodeで拡張子jsファイルの'Type aliases can be used only in a .ts file' エラーを回避する

VSCodeで、拡張子jsのファイルにてTypeを使うと以下のエラーが起きます。

'Type aliases can be used only in a .ts file' error

ググったら以下のページを発見。
'Type aliases can be used only in a .ts file' error popping up in Flow enabled files
https://github.com/Microsoft/vscode/issues/15171

VSCodeのsetting.jsonに以下を追加することでエラーをとりあえず回避できました。

"javascript.validate.enable": false

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

jQueryで実装できること (Progateでの学習)

この記事について

ProgateでjQueryを学習中の初心者がアウトプットの為に書いている記事です。間違っている点があればご指摘いただけると助かります。

ボタンを押すと対応するスライドに切り替わるようにする。

"display: none"で基本的にスライドを非表示にし、activeクラス(名前は自由)を付加されたものが表示されるようにしておきます。このactiveクラスを付け替えることで表示させるスライドを切り替えます。

eqメソッド

eqメソッドでjQueryオブジェクトの中から引数と同じ数字と同じインデックス番号の要素を取得できます。

index.html
<ul>
  <li>リスト0</li>
  <li>リスト1</li>
  <li>リスト2</li>
</ul>

script.js
//リスト2が赤くなる
$('li').eq(2).css('color', 'red');

indexメソッド

indexメソッドを用いることで、activeクラスが当てられている要素を取得することができます。

index.html
<ul>
  <li>リスト0</li>
  <li class="active">リスト1</li>
  <li>リスト2</li>
</ul>

script.js
//li要素中のactiveクラスを持つインデックス番号1の要素を取得
$('li').index($('.active'));

prevメソッドとnextメソッド

prevメソッドは同じ階層の要素の中から1つ前の要素を、nextメソッドは1つ後ろの要素を取得するメソッドです。

index.html
<ul>
  <li>リスト0</li>
  <li class="active">リスト1</li>
  <li>リスト2</li>
</ul>

script.js
//リスト0が赤くなる
$('.active').prev().css('color', 'red');
//リスト2が青くなる
$('.active').next().css('color', 'blue');

これらのメソッドを駆使して以下のように実装します。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>Progate</title>
    <link rel="stylesheet" href="stylesheet.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
  </head>
  <body>
    <div class="slide-wrapper">
      <h2 class="slide-title">にんじゃわんこの一年</h2>
        <div class="change-btn-wrapper">
          <div class="change-btn prev-btn">← 前へ</div>
          <div class="change-btn next-btn">次へ →</div>
        </div>
        <ul class="slides">
          <li class="slide active"><img src="https://s3-ap-northeast-1.amazonaws.com/progate/shared/images/lesson/jquery/advanced/spring.jpg"></li>
          <li class="slide"><img src="https://s3-ap-northeast-1.amazonaws.com/progate/shared/images/lesson/jquery/advanced/rainy.jpg"></li>
          <li class="slide"><img src="https://s3-ap-northeast-1.amazonaws.com/progate/shared/images/lesson/jquery/advanced/summer.jpg"></li>
          <li class="slide"><img src="https://s3-ap-northeast-1.amazonaws.com/progate/shared/images/lesson/jquery/advanced/autumn.jpg"></li>
          <li class="slide"><img src="https://s3-ap-northeast-1.amazonaws.com/progate/shared/images/lesson/jquery/advanced/winter.jpg"></li>
        </ul>
        <div class="index-btn-wrapper">
          <div class="index-btn">1</div>
          <div class="index-btn">2</div>
          <div class="index-btn">3</div>
          <div class="index-btn">4</div>
          <div class="index-btn">5</div>
        </div>
      </div>
    <script type="text/javascript" src="script.js"></script>
  </body>
</html>

stylesheet.css
body {
  font-family: "Hiragino Maru Gothic ProN", sans-serif;
}

ul {
  list-style: none;
}

.slide-wrapper {
  text-align: center;
  width: 1000px;
  margin: 0 auto;
  color: #5e6f84;
  padding: 60px 0;
}

.slide-title {
  font-size: 40px;
  margin-bottom: 30px;
}

.change-btn-wrapper {
  width: 500px;
  margin: 20px auto;
  font-size: 18px;
}

.change-btn-wrapper:after {
  content: "";
  clear: both;
  display: block;
}

.prev-btn {
  cursor: pointer;
  float: left;
  display: none;
}

.next-btn {
  cursor: pointer;
  float: right;
}

.slides {
  padding: 0;
}

.slide {
  display: none;
}

.active {
  display: block;
}

.slide img {
  width: 500px;
  height: auto;
  border-radius: 5px;
}

.index-btn-wrapper {
  font-size: 16px;
  margin-top: 20px;
}

.index-btn {
  display: inline-block;
  color: #4e90af;
  background-color: #e0f5ff;
  width: 40px;
  padding: 6px 0;
  margin: 0 5px;
  border-radius: 3px;
  cursor: pointer;
}

.index-btn:hover {
  color: #fff;
  background-color: #5cabd0;
}

script.js
$(function() {

  function toggleChangeBtn() {
//activeクラスを持つ要素をindexメソッドで取得して変数に代入
    var slideIndex = $('.slide').index($('.active'));
    $('.change-btn').show();
//if文による条件分岐
    if (slideIndex == 0) {
//一番最初のスライドの時は前へボタンを消す
      $('.prev-btn').hide();
//一番最後のスライドの時は次へボタンを消す
//インデックス番号は0から割りふられるので1引いてあげる
    } else if (slideIndex == $('.slide').length - 1 ) {
      $('.next-btn').hide();
    }
  }

  $('.index-btn').click(function() {
    $('.active').removeClass('active');
//クリックされた要素を変数に代入
    var clickedIndex = $('.index-btn').index($(this));
    $('.slide').eq(clickedIndex).addClass('active');
    toggleChangeBtn();
  });

  $('.change-btn').click(function() {
    var $displaySlide = $('.active');
    $displaySlide.removeClass('active');
//if文による条件分岐
//クリックされた要素に'next-btn'クラスがある場合は次のスライドに移る
    if ($(this).hasClass('next-btn')) {
      $displaySlide.next().addClass('active');
//クリックされた要素に'next-btn'クラスがない場合('prev-btn'がある場合)は次のスライドに移る
    } else {
      $displaySlide.prev().addClass('active');
    }
//関数の呼び出し
    toggleChangeBtn();
  });
});


lengthメソッド

上のscript.js中にある".length"はlengthメソッドです。このメソッド要素の数を取得できます。
これによりスライドの枚数が増えてもscript.jsを書き換える必要がなくなります。

index.html
<ul>
  <li>リスト0</li>
  <li>リスト1</li>
  <li>リスト2</li>
</ul>

script.js
//結果は3となる
$('li').length;

フォームを操作する

jQueryを使ってHTMLから情報を取得するようにします。

textメソッド

引数に指定した文字を要素にセットすることができます。引数を入れてない場合は、要素内の文字を取得することができます。

index.html
<h1>こんにちは</h1>
<p>さようなら</p>

script.js
//h1の"こんにちは"を"こんばんは"に書き換える
$('h1').text('こんばんは');

//p要素の"さようなら"を取得
$('p').text('');

attrメソッド

HTMLの属性を追加及び取得することができます。追加する時は属性名と属性値の2つの引数を指定する必要があります。引数を指定しないことでtextメソッドと同様に取得することができます。

index.html
<h1>こんにちは</h1>
<a href="https://prog-8.com">リンク</a>

script.js
//h1にid "greet"を追加
$('h1').attr('id', 'greet');

//a要素のhref属性の値である"https://prog-8.com"を取得
$('a').attr('href');

HTMLのメソッドは自分で作成して設定することができます。その場合、名前は"data-"から始める必要があります。

index.html
<div data-select="name"></div>

script.js
//nameを取得できる
$('div').attr('data-select');


valメソッド

valメソッドはinputタグに入力されている値を取得するのに使用します。

index.html
<form>
  <div>名前</div>
  <input id="name" type="text">
</form>

script.js
//入力フォームに入力された値(文字)がnameに代入される
var name = $('#name').val();

submitイベント

submitイベントは送信ボタンがクリックされた時及びEnterキーで送信された時に処理を起こすイベントです。

script.js
//入力フォームに入力された値(文字)がnameに代入される
var name = $('#name').val();

Progateでは以下のような実装を行ないました。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>Progate</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Dosis:300,400,500,600,700">
    <link rel="stylesheet" href="stylesheet.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
  </head>
  <body>
    <div class="container">
      <div class="section-title">
        <h3>キャラクター総選挙</h3>  
      </div>
      <div class="section-content">
        <p class="call-to-action">好きなキャラクターをクリック!</p>
        <div class="option-wrapper">
          <div class="option-group">
            <img class="option-image" src="https://s3-ap-northeast-1.amazonaws.com/progate/shared/images/lesson/jquery/advanced/progate_baby_wanko.png">
            <div class="option-btn" data-option="1">1. ベイビーわんこ</div>
          </div>
          <div class="option-group">
            <img class="option-image" src="https://s3-ap-northeast-1.amazonaws.com/progate/shared/images/lesson/jquery/advanced/progate_wanko.png">
            <div class="option-btn" data-option="2">2. にんじゃわんこ</div>
          </div>
          <div class="option-group">
            <img class="option-image" src="https://s3-ap-northeast-1.amazonaws.com/progate/shared/images/lesson/jquery/advanced/progate_hitsuji.png">
            <div class="option-btn" data-option="3">3. ひつじ仙人</div>
          </div>
        </div>
        <form id="form" class="form">
          <div class="form-group">
            <div>キャラクター:</div>
            <select id="select-form" class="select-form">
              <option value="0">選択してください</option>
              <option value="1">1. ベイビーわんこ</option>
              <option value="2">2. にんじゃわんこ</option>
              <option value="3">3. ひつじ仙人</option>
            </select>  
          </div>
          <div class="form-group">
            <p id="error-message" class="error-message"></p>
            <div>理由:</div>
            <textarea id="text-form" class="text-form"></textarea>  
          </div>
          <button type="submit" class="btn btn-submit">送信</button>
        </form>
        <div class="output-wrapper">
          <h4>結果</h4>
          <div class="output-inner">
            <p class="output-item">好きなキャラクター:<span id="output-select"></span></p>
            <p class="output-item">理由:<span id="output-text"></span></p>  
          </div>
        </div>
      </div>
    </div>
    <script type="text/javascript" src="script.js"></script>
  </body>
</html>

script.js
$(function() {
  $('#form').submit(function() {
//選択肢された値を変数に代入
    var selectItem = $('#select-form').val();
//入力された文章
    var textItem = $('#text-form').val();
//テキストフォームが空欄の時にエラーメッセージを表示するようにする。
    if (textItem == '') {
      $('#error-message').text('理由を記入してください');
    } else {
      $('#error-message').text('');
    }
//選択された値を結果として表示する
    $('#output-select').text(selectItem);
//入力された文章を表示する
    $('#output-text').text(textItem);
    return false;
  });

  $('.option-btn').click(function() {
//クリックされたものの名前を変数に代入
    var optionText = $(this).text();
// 変数clickedOptionに、クリックした要素のdata-optionの値を代入
    var clickedOption = $(this).attr('data-option');

    $('#text-form').val(optionText + 'が好きな理由は、');
// 変数clickedOptionを用いて、「#select-form」の値を自動で入力
    $('#select-form').val(clickedOption)

  });
});

アニメーションの追加

動的な変化をjQueryで実装する方法を学習しました。

animateメソッド

$('セレクタ').animate({'プロパティ': '値'},時間);と書くことでアニメーションを付加できます。

script.js
//h1にある文字を1000ミリ秒かけて文字サイズを50pxに変化させる
$('h1').animate({

'font-size': '50px'

}, 1000);

ページ内リンク

縦長のページでは"トップに戻る"などといった名前でページの最上部に戻るボタンがあります。このような機能をページ内リンクと呼びます。

aタグでのページ内リンクの実装

リンクの飛び先でidを設定し、aタグのhref属性に"#id名"とすることで、aタグをクリックするとそのidを含む場所に飛ぶことができます。

index.html
//このaタグをクリックすると下のdivタグの部分に飛ぶ
<a href="#contact">
</a>

<div id="contact">
</div>

scrollTopメソッド

ページ内リンクをjQueryで実装する時はscrollTopメソッドを利用します。$('html, body').scrollTop(値); のように指定し、ページ最上部から値pxの位置に移動することができます。scrollTopは基本的に$('html, body')セレクタに対して使用するものなので丸暗記で大丈夫です。

script.js
$('#top-btn').click(function(){
//上から0pxの場所(最上部)へ移動
  $('html', 'body').scrollTop(0);

});

アニメーションとして付加することもできます。

script.js
$('#top-btn').click(function(){

  $('html', 'body').animate({

  'scrollTop': 0

  }, 1000);

});

offsetメソッド

offsetメソッドは要素の位置情報を取得する時に使用するメソッドです。

script.js
//{top: 180px, left: 100px}といったような位置情報を取得
$('#content').offset();

//topの値だけ取得
$('#content').offset().top;

Progateでは以下のような実装を学習しました。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>Progate</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css" rel="stylesheet">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Dosis:300,400,500,600,700">
    <link rel="stylesheet" href="stylesheet.css">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js"></script>
  </head>
  <body>
    <!-- ヘッダー -->
    <header>
      <div class="container">
        <div class="header-title">
          <div id="top-btn" class="header-logo">にんじゃわんこの部屋</div>
        </div>
        <div class="header-menu">
          <ul class="header-menu-right">
            <li>
              <a href="#stamps">LINEスタンプ</a>
            </li>
            <li>
              <a href="#interview">インタビュー</a>
            </li>
            <li>
              <a href="#contact">お問い合わせ</a>
            </li>
          </ul>
        </div>
      </div>
    </header>

    <!-- トップ部分 -->
    <div class="top-section gray-section">
      <div class="top-inner">
        <div class="container">
          <div class="top-description">
            <h2>ようこそ!にんじゃわんこの部屋へ!</h2>
            <p>Progate公式キャラクター「にんじゃわんこ」の日常です。</p>
          </div>
        </div>
      </div>
    </div>

    <!-- スライダー -->
    <div id="stamps" class="stamps-section">
      <div class="container">
        <div class="section-title">
          <h3>LINEスタンプ大好評発売中!</h3>
        </div>
        <div id="stamp-carousel" class="carousel slide" data-ride="carousel"> 
          <ol class="carousel-indicators">
            <li data-target="#stamp-carousel" data-slide-to="0" class="active"></li>
            <li data-target="#stamp-carousel" data-slide-to="1" class=""></li>
            <li data-target="#stamp-carousel" data-slide-to="2" class=""></li>
          </ol>
          <div class="carousel-inner">
            <div class="item stamp-box active">
              <img src="https://s3-ap-northeast-1.amazonaws.com/progate/shared/images/lesson/jquery/advanced/LineStamp1.jpg" class="stamp-image">
              <h4>にんじゃわんこがLINEスタンプに登場!</h4>
            </div>
            <div class="item stamp-box">
              <img src="https://s3-ap-northeast-1.amazonaws.com/progate/shared/images/lesson/jquery/advanced/LineStamp2.jpg" class="stamp-image">
              <h4>プログラミング関連のスタンプも!</h4>
            </div>
            <div class="item stamp-box">
              <img src="https://s3-ap-northeast-1.amazonaws.com/progate/shared/images/lesson/jquery/advanced/LineStamp3.jpg" class="stamp-image">
              <h4>新キャラ・ベイビーわんこも!</h4>
            </div>
          </div> 
        </div> 
        <p>全40種類!使いやすく、可愛い!にんじゃわんこの日常を表現したスタンプです!<br>
          ご購入は<a class="stamp-link" href="https://store.line.me/stickershop/product/1132359/ja" target="_blank"> こちら</a>(LINE STORE)から!
        </p>
      </div>
    </div>

    <!-- インタビュー -->
    <div id="interview" class="interview-section">
      <div class="interview-section-top gray-section">
        <div class="container">
          <div class="section-title">
            <h3>特別インタビュー</h3>
          </div>
          <img class="interview-image" src="https://s3-ap-northeast-1.amazonaws.com/progate/shared/images/lesson/jquery/advanced/interview_wanko.jpg">
        </div>
      </div>
      <div class="interview-section-bottom">
        <div class="container">
          <div class="interview-box">
            <h5>Q. 普段はどのような活動をされているのですか?</h5>
            基本的に、渋谷にあるProgateのオフィスでくつろぐのが日課でござる。Progateのオフィスは本当に居心地が良いでござる。
            スピーカーで好きな音楽を流しながら、カフェみたいな空間でProgateの宣伝方法について考えてる時間は、最高に充実しているでござる。
          </div>
          <div class="interview-box">
            <h5>Q. にんじゃわんこさんは、プログラミングはできるのですか?また、得意な言語は何ですか?</h5>
            Progateのレッスンは全て終了したから、それなりにはできるでござる。最近は本気でエンジニアを目指して、WankoBook Proを購入したでござる。<br>
            得意な言語は、Rubyでござる。Railsで面白いWebアプリを作りたいでござる。
          </div>
          <div class="interview-box">
            <h5>Q. 好きな食べものは何ですか?やっぱりドッグフードですか?</h5>
            ラーメンが大好きでござる。オフィスの近くには美味しいラーメン屋さんが多くあって幸せでござる。<br>
            ドッグフードはおいしくないでござる。炊きたてのご飯の方が何倍も美味しいと思うでござる。
          </div>
          <div class="interview-box">
            <h5>Q. 最後に、今後の活動展開について教えてください。</h5>
            今後はさらにProgateの宣伝に努めていくでござる。あと、僕のLINEスタンプも。スタンプの売上は拙者のお小遣いになるから、是非ともみんなに買ってほしいでござる。
          </div>
        </div>
      </div>
    </div>

    <!-- Contact -->
    <div id="contact" class="contact-section gray-section">
      <div class="contact-section-inner">
        <div class="container">
          <div class="section-title">
            <h3>お問い合わせ</h3>
          </div>
          <div class="contact-form">
            <h4>メッセージを送る</h4>
            <form>
              <div class="error-message"></div>
              <input type="text" name="name" placeholder="お名前" class="contact-name">
              <div class="error-message"></div>
              <input type="text" name="email" placeholder="メールアドレス" class="contact-email">
              <div class="error-message"></div>
              <textarea name="message" placeholder="メッセージ内容"></textarea>
              <button type="submit" class="btn btn-contact">送信</button>
            </form>
          </div>
          <div class="contact-about">
            <h4>About Me</h4>
            <p>Progateで飼われている柴犬。忍者の格好をするのが趣味(可愛いさが際立つと思っている)。妖精によって深夜4時半頃に召喚され、それ以来Progateのサイトに居座るように。<br>
              Progateでは、ユーザーの癒やしから、学習のサポートまでを担当。また、フィードバック番長も努め、ユーザーにフィードバックを返信しないと毎日22時に怒鳴ってくる。
            </p>
            <h4>Address</h4>
            <p>
              <i class="fa fa-map-marker"></i>東京都渋谷区神宮前6-19-20 第15荒井ビル9階
            </p>
            <p>
              <i class="fa fa-phone"></i>Phone: 03-6455-0950
            </p>
          </div>
        </div>
      </div>
    </div>
    <footer>
      <div class="container">
        <div class="follow-us">
          <a href="https://www.facebook.com/Progate-742679992421539" class="social-icon" target='_blank'>
            <span class="fa fa-facebook"></span>
          </a>
          <a href="https://twitter.com/progatejp" class="social-icon" target='_blank'>
            <span class="fa fa-twitter"></span>
          </a>
        </div>
        <div class="copyright">©2016-
          Progate
        </div>
      </div>
    </footer>

    <script type="text/javascript" src="script.js"></script>
  </body>
</html>

script.js
$(function(){

  // SNSボタン
  $('.social-icon').hover(
    function(){
//マウスカーソルが乗ると大きくなる
      $(this).children('span').animate({
        'font-size':'30px'
      }, 300);
    },
    function(){
//マウスカーソルが外れると元のサイズになる
      $(this).children('span').animate({
        'font-size':'24px'
      }, 300);
    }
  );

  // トップへ戻るボタン
  $('#top-btn').click(function(){
    $('html,body').animate({ 
      'scrollTop': 0 
    }, 'slow');
  });

  // ヘッダー内の<a>タグをクリックしたときのclickイベントを作成
  $('header a').click(function() {
//クリックされた要素のhref属性を変数idに代入
    var id = $(this).attr('href');
//idの位置情報を変数に代入
    var position = $(id).offset().top;
    $('html, body').animate({
//その位置情報に移動する
      scrollTop: position
    }, 500);
  });

});

 感想

これでProgateでのjQueryの学習が終了しました。苦手意識があったにもかかわらず、学習が終わる頃には楽しく感じました。次の言語学習でも楽しめるように頑張っていこうと思います(小並感)。

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

[Vue/Nuxt] プロパティの値の有無によるエラーハンドリング

VuexのStateのプロパティにオブジェクトで値が入っている場合において、
値の有無による条件処理でのエラーハンドリング実装方法の一例を紹介します。

今回は、
プロパティに値が入っていたらdispatchでActionsを発火させる
という場合のコードとなっております。

index.vue
<script>
export default {
  async fetch({ store }) {
    if (
      store.state.index.data &&
      Object.keys(store.state.index.data).length > 0
    ) {
      await store.dispatch('index/fetchData')
    }
  }
}

store.state.index.data
では単にdataプロパティに値があるかの確認。

且つ、
Object.keys(store.state.index.data).length > 0
オブジェクト数が0より多いことを確認しています。

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

map・filter・reduceの書き方・使い方

Reactでポートフォリオ のアプリを作成し始めたのですが、
その時使った配列操作の map、filter、reduceについて、
書き方と使い方をまとめておきます。

※文法に集中する為、Reactの書き方は全て排除しJavaScriptのみでまとめてます。

mapメソッド

mapは元々の配列から操作して、新しい配列を作ります。

具体的な例はこんな感じ↓

incomeItems = [{text: "本の売却", amount: 500, id: 123343434}, {text: "宝くじ", amount: 5000, id: 123454523}]
const incomeAmounts = incomeItems.map(incomeItem => incomeItem.amount);
//結果 incomeAmounts = [500,5000]

incomeItemsはオブジェクトの配列です。
中にはユーザーが入力した「text」と「amount」、ランダムなIDが入ってます。
そのincomeItemsに対してmapメソッドを使ってます。

【アロー関数でのmapの書き方】
配列. map ( value => 新しい配列としてリターンしたい内容 )

第二引数にindex、第三引数に実行しているarray自身を入れることもできます
配列. map(( value, index, array ) => 新しい配列としてリターンしたい内容 )

mapを使ってincomeAmountsという数字だけを取り扱う用の配列が作れました。

filterメソッド

filterメソッドは条件に合致した要素で新しい配列を作ります。

今回のアプリでは、ユーザーがdeleteボタンを押した時に、その項目が削除されるという操作のため使いました。

具体的な例はこんな感じ↓

incomeItems = [{text: "本の売却", amount: 500, id: 1}, {text: "宝くじ", amount: 5000, id: 2}]

//onClickなどでdeleteHandlerを実行させる
const deleteHandler = () => {
  setIncomeItems(incomeItems.filter((e) => e.id !== incomeItem.id));
}
//結果: incomeItems = [{text: "宝くじ", amount: 5000, id: 2}]

incomeItemsにユーザーが入力した内容がオブジェクトの配列として入ってます。
その配列に対してfilterを実行し、各配列の要素(e)のidとincomeItemのid(
消したいitem)を比べてます。
idが一致しない要素だけのincomeItemsの配列が出来上がります。(=削除以外の項目)

【filterの書き方】
配列. filter ( ( element ) => 条件)

これもまた第二引数にindex、第三引数に実行中のarray自身を指定できます。
配列. filter ( ( element, index, array ) => 条件)

filterを使ってユーザーが削除した以外の要素を取り出せました。

reduceメソッド

reduceは配列の各要素を一つにできます。累積系に便利ということです。
今回のアプリでは、配列の中にある数値の合計を出してみました。

具体的な例はこんな感じ↓

incomeAmounts = [500,5000]
const incomeTotal = incomeAmounts.reduce((acc, cur) => acc += cur, 0);
//結果: 5500

incomeAmountsの配列に対してreduceを実行してます。
reduceの第一引数(arr)は、前までに蓄積された値で、第二引数(cur)は、現在の値が入ります。
この場合「合計」を出したいので、蓄積の値(arr)と現在の値(cur)を足して一つの要素として返してます。

【reduceの書き方】
配列. reduce ( ( accumulater, current ) => 条件, 初期値)
今回の例では第二引数までで、0を初期値として渡してます。

なんと引数は4つまで渡せます。
配列. reduce ( ( acc, cur, idx, src ) => 条件, 初期値)
1: (acc) アキュムレーター。前までの蓄積された値
2: (cur) 現在値
3: (idx) インデックス
4: (src) 元々の配列



配列操作のメソッドはもっといっぱいあるで、色々使いながら学んでいきたいですね。

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

[Vuex] 複数のdispatchを全て実行するにはPromise.allを使う

VuexのActionsを実行するトリガーとなるdispatch
複数全て実行したい場合には
Promise.all
を使うと実装できます。

index.vue
<script>
export default {
  async fetch({ store }) {
    await Promise.all([
      store.dispatch('〜〜〜'),
      store.dispatch('〜〜〜'),
      store.dispatch('〜〜〜')
    ])
  }
}
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WindowsでReactの開発環境を整える

はじめに

WindowsでReactの開発環境を作ってみたところ、いくつかエラーが出たのでその手順と解決法を残します。
使用した環境は以下の通り

  • Windows10
  • Node.js v14.8.0
  • NPM v6.14.7
  • Windows パワーシェル(VScode経由)

手順

  1. Node.jsのインストール
  2. create-react-appのインストール
  3. プロジェクトを作成してHello worldしてみる

おまけ

  • ESlintを設定する
  • ディレクトリ構成のかんたんな説明

1.Node.jsのインストール

まずはNode.js公式サイトからダウンロードし、Node.jsをインストールします。
ターミナルで以下のコマンドを打ってVersion情報が表示されればインストール成功しています。

Node.jsのVersion情報を確認する
node -v

Node.jsがインストールされていればnpmも使うことが可能になります。

2.create-react-appのインストール

create-react-appをグローバルインストールします。

create-react-appをインストールする
npm install -g create-react-app

ここまでで準備は完了です。

3.プロジェクトを作成してHello worldしてみる

プロジェクトを作成したいディレクトリに移動し、プロジェクトを作成していきます。
多くのドキュメントではプロジェクトを作成するのに以下のコマンドを見かけると思います。

Reactのプロジェクトを作成する
create-react-app <プロジェクト名>

Windowsでは、インストールしただけでは上記のコマンドにパスが通っておらず使う事ができないことがあるようです。
解決方法は以下
1. パスを通す
2. npxコマンドを使う

今回はnpxコマンドを使う方法で作成していきます。
(以下サンプルのプロジェクト名をhelloworldとして作成します。)

Reactのプロジェクトを作成する
npx create-react-app helloworld

これで、カレントディレクトリにhelloworldというディレクトリが作られ、プロジェクトの作成が完了します。

パスの通し方については現在調査中です。

プロジェクトが作成できたら、早速動かして行きます。
helloworldディレクトリに移動し、仮想サーバー上でWebアプリの動作を確認します。

Webアプリをプレビューする
cd helloworld
npm start

以上のコマンドを打つと、規定のブラウザが起動してWebアプリの動作を確認することができます。
他のブラウザで見たい場合はlocalhost:3000にアクセスすることで見ることができます。
終わる場合はターミナルでCtrl + cを押して終了します。

以降、srcディレクトリ内のApp.jsを編集することでWebアプリを作っていきます。

おまけ

ESlintを設定する

create-react-appを使用してプロジェクトを作成すると、ESlintがモジュールに追加された状態になっています。
なので、設定を行うだけでESlintが使用可能になります。

ESlintの設定ファイルを作成する
npm eslint --init

2020-09-03_19h46_24.png

最後の質問、「Would you like to install them now with npm?」でYesにするとうまく動かなかったので、Noにしています。
ここで改めてインストールしてしまうと、競合するのかもしれません。

Compiles successfully!と表示されたら初期化が完了します。
画像では設定ファイルをJavaScriptにしているので.eslintrc.jsというファイルが作成されます。
環境に合わせて設定を追加してください。

ディレクトリ構成のかんたんな説明

以下は自分のための備忘録としての、ディレクトリ構成のかんたんな説明です。

<プロジェクト>
   L ./node_modules (インストールされているモジュールが入ってる場所。)
   L ./public (テンプレートのHTMLや画像が入っています。)
   L ./src (アプリのソース等、ここのファイルを編集してアプリを作成していきます。)
   L .eslintrc.js (ESlintの設定ファイル)
   L .gitignore (Gitで追跡対象にしないファイルを設定します。)
   L package.json (インストールされているモジュールについての設定ファイル)
   L README.md (Read meファイル)
   L yarn.lock (環境によっては無いこともある)

この他、ビルド後のデータを格納するディレクトリがあります。

最後に

create-react-appを使ってReactの開発環境を作ると、すでに必要なモジュールやファイルが用意されているため素早く開発を始められます。
最初はCDNを使ってみようと思ったのですが手こずってしまい、結局こちらのほうが早くReactの勉強を始めることができました。

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

『こんにちはPython』のスカッシュゲーム(壁打ちテニス)を JavaScript で写経してみた(後編)

『こんにちはPython』のスカッシュゲーム(壁打ちテニス)を JavaScript で写経してみた(後編)

みなさま、こんにちは。ハーツテクノロジーの山崎です。(この記事は、業務とは直接無関係な記事ですが、業務で得られた知見が間接的に随所に織り込まれていると思われます。)

この記事は、前編と後編の2部構成になっていて、こちらは後編です。

前編には、小学生でもわかるように、「ゲームとして遊べるまで」を解説しています。
後編では、小学生に解説するには少々無理がある部分を、こちらにまとめるようにしました。ですので、小学生の方は前編だけ読んでいただき、後編は、学校を卒業してから読むようにすると理解がすすむ、、、かもしれません。

おとなの方は、後編からでもよいですが、前編を軽く流し読みしていただいてからのほうが、この後編の内容がより理解しやすい思います。よろしくお願いします。

こんにちはPython

元ネタはこちら。

ゲームセンターあらしと学ぶプログラミング入門
まんが版『こんにちはPython』
https://www.m-sugaya.jp/manga_python/

よい本です。なんと言っても、まんがであることが最高です。しかも、「ゲームセンターあらし」ですよ!(炎のコマは出ないけど)
当時、小学生~中学生だったわたしは、勉強が嫌いで、よく友達とゲームセンターに行って遊んでいました。
もちろん「ゲームセンターあらし」も読んでました。で、ゲームがめっちゃ好きになって「プログラマになりたい!」って思ったのが最初の人生の分岐点でした。

プログラマになれば、ゲームが作れるようになります。これは事実です。ただ、1人でできる規模は限られちゃいますけど。

さて、後編は、前編では書けなかったおとな向けの説明になります。

主に、
1、オブジェクト指向
2、スマホ対応
3、音対応
の3つについて解説していきます。

1、オブジェクト指向

いきなりですが、class を導入します。いわゆるオブジェクト指向です。戦略でいう「分割統治」ってやつです。

前編で作成したコードは、長くは無いコードではあるのですが、サクッと理解できる行数を超えてきたので、手を加える前に整理します。

プログラムコードは放っておくとすぐに肥大化します。肥大化したコードは散らかった部屋と一緒で、どこになにがあるのかさっぱりわからなくなります。難解なコードは、理解するための時間を奪います。

オブジェクト指向とは、(Qiitaを俳諧しているおとなの人には耳タコだと思いますが、)わたしの解釈を短めに書くと「データ構造を整理して密接に関連するアルゴリズムを近くに置くことでコードを部品化し見通しよく整理すること」です。
複雑で難解なコードをオブジェクト指向の恩恵に預かって、読みやすく、理解しやすくしましょう。というわけです。

(この Twitter の画像は「イメージ」です。プログラミングとは無関係ですが「整理されている価値」を画像で表現できていると思っています。)

プログラミングに慣れたひとは、class & object にしたほうが見やすい(読みやすい)と思います。やはり、変数や定数は、グローバルなエリアには置かず、必要とされている(関係の深い)機能に近い場所に置いて、影響範囲や思考の範囲を狭めることで、思考がシンプルになり、理解しやすくなるからだど思っています。

具体的には、表示エリア、ボール、ラケットをそれぞれ class にして、作成した class から new を使って object を作成します。
ちなみに、class が本領を発揮するのは、複数の object を生成するようになってからです。このコードでは 1つの class に対して 1つの object しか生成していないので、構造体+関数で書くのと大きな違いは無いと言える、、、かもしれません。

あと、ついでに、メッセージをランダムに選んでいるところ↓

    const mes = Math.floor( Math.random() * 5 )
    let message = ''
    if ( mes == 0 )
        message = 'うまい!'
    if ( mes == 1 )
        message = 'グッド!'
    if ( mes == 2 )
        message = 'ナイス!'
    if ( mes == 3 )
        message = 'よしッ!'
    if ( mes == 4 )
        message = 'すてき!'

これは小学生向けの書き方なので、おとななプログラマの方は、この処理をサクッと関数にしましょう。

function get_random_message( ary )
{
    return ary[ Math.floor( Math.random() * ary.length ) ]
}

const message = get_random_message( [
    'うまい!', 'グッド!', 'ナイス!', 'よしッ!', 'すてき!'
] ) 

はい。短く、シンプルになりました。

その昔、JavaScript には class は無く、 function を使って class っぽいコードを書いていましたが、それはもう昔の話です。今(2020年)は、JavaScript で class が使えます。

いったんまとめると以下のようなコードになります。長くなってきたので閉じています。ここ↓「全コード」をクリックすると見られます。

全コード
<body></body><script>
// 「こんにちは Python」のスカッシュゲーム(壁打ちテニス)を JavaScript で写経(音無し版)
// オブジェクト指向対応版

// メッセージをランダムに選択
function get_random_message( ary )
{
    return ary[ Math.floor( Math.random() * ary.length ) ]
}

// canvas表示エリア
class CanvasArea
{
    constructor()
    {
        this.cv = document.createElement( 'canvas' )
        this.ctx = this.cv.getContext( '2d' )

        this.cv_w = 640
        this.cv_h = 400
        this.cv.setAttribute( 'width',  this.cv_w )
        this.cv.setAttribute( 'height', this.cv_h )
        document.body.appendChild( this.cv )

        this.draw()
    }

    // 画面クリア
    draw()
    {
        this.ctx.fillStyle = 'silver' // 'white'
        this.ctx.fillRect( 0, 0, this.cv_w, this.cv_h )
    }
}

// ボール
class Ball
{
    constructor()
    {
        this.init( 0, 250, 15, -15 )
        this.r_size = 10
    }

    init( _x, _y, _sx, _sy )
    {
        this.x = _x
        this.y = _y
        this.speed_x = _sx
        this.speed_y = _sy
    }

    // ボールを描く
    draw()
    {
        area.ctx.beginPath()
        area.ctx.arc( this.x, this.y, this.r_size, 0, 2 * Math.PI )
        area.ctx.fillStyle = 'red'
        area.ctx.fill()
    }

    // ボールの移動
    move()
    {
        if ( is_gameover ) return

        // 左右の壁に当たったのかの判定
        if ( this.x+this.speed_x < 0   ) {
            this.speed_x *= -1
        }
        if ( this.x+this.speed_x > area.cv_w ) {
            this.speed_x *= -1
        }

        // 天井の壁に当たったのかの判定
        if ( this.y+this.speed_y < 0 ) {
            this.speed_y *= -1
        }

        // ラケットに当たったのかの判定
        if ( this.y+this.speed_y > area.cv_h-10 ) {
            if ( racket.x < this.x+this.speed_x ) {
                if ( this.x+this.speed_x < racket.x+racket.w_size ) {
                    this.speed_y *= -1
                    if ( Math.random() < 0.5 )
                        this.speed_x *= -1

                    const message = get_random_message( [
                        'うまい!', 'グッド!', 'ナイス!', 'よしッ!', 'すてき!'
                    ] )
                    point += 10
                    div.innerText = message + ' 得点:' + point
                }
            }
        }

        // ミスした時の判定
        if ( this.y+this.speed_y > area.cv_h ) {
            const message = get_random_message( [
                'へたくそ!','ミスしたね!', 'あーあ、見てられないね!'
            ] )
            div.innerText = message + ' 得点:' + point

            is_gameover = true
            return
        }

        // ボールを移動
        this.x += this.speed_x
        this.y += this.speed_y
    }
}

// ラケット
class Racket
{
    constructor()
    {
        this.x = 0
        this.w_size = 100

        // マウスの動きの処理
        area.cv.onmousemove = ev => {
            this.x = ev.offsetX - 50
        }
    }

    draw()
    {
        area.ctx.fillStyle = 'yellow'
        area.ctx.fillRect( this.x, area.cv_h-10, this.w_size, 8 )
    }
}

// メッセージ表示エリア div の確保
const div = document.createElement( 'div' )
document.body.appendChild( div )
div.innerText = "スカッシュゲーム(音無し版):マウスクリックでスタート!"

const area   = new CanvasArea()
const ball   = new Ball()
const racket = new Racket()

let is_gameover = true
let point = 0

// クリックで再スタート
area.cv.onclick = ev =>
{
    if ( ! is_gameover ) return

    // ゲームの初期化
    is_gameover = false
    point = 0
    ball.init( 0, 250, 15, -15 )
    div.innerText = "スカッシュゲーム:スタート!"
}

// ゲームの繰り返し処理
function game_loop()
{
    area.draw()     // 画面クリア
    ball.draw()     // ボールを描く
    racket.draw()   // ラケットを描く
    ball.move()     // ボールの移動
}

// ゲームのメイン処理開始
setInterval( game_loop, 50 ) // 20fps

</script></html>

ちなみに、現状は、こういう感じ?で動いています。

CodePen1

コードを読んで、「is_gameover とかも class にまとめられるじゃん」と思った方もおられるでしょう。いい質問ですね。それもありだとは思います。ですが、ここは、さじ加減といいますか、やりすぎないところで止めるのが、いい塩梅だと思っています(個人の見解です)。

では、このコードを足がかりにして、スマホ対応と、音を出す対応をしていきます。

2、スマホ対応

スマホ対応では大きく2つの対応をします。

(1) 見た目を整える→リサイズレスポンシブ)に対応
(2) タッチイベントに対応

1つ目は、見た目を整えます。
現状は、canvas エリアが 640x400 と固定になっています。PCのブラウザであれば、この大きさで固定に表示しても大きな問題にはなりにくいです。
しかし、スマホのブラウザでは、ちょっと遊べません。スマホの画面サイズもいろいろありますし、画面を小さくしてもたのしくありません。
ここは、リサイズレスポンシブ)に対応し、表示しているブラウザのサイズに合わせて、canvas エリアの大きさを変更するコードを書きます。

もう1つは、タッチイベントの対応です。
意外に思われるかもしれませんが、ブラウザでは、マウスでクリックしたイベントと、画面にタッチしたイベントは別のイベントとしてあつかわれています。
ですので、スマホのブラウザでは、マウスを動かしたイベントが発生できず、ラケットを動かすことができません。
ここも、タッチイベントのコードを入れていきます。

(ちなみに、ここで指している「スマホ」とは、Android端末と、iPhone の大きく2種類がありますが、動作を確認しているのは、iPhone の Chrome ブラウザになります。Androidや、Chrome以外のブラウザでは確認していないですが、おそらく、問題なく動くと思っています。動かない環境などありましたら、コメントいただけますとたすかります。)

(1) リサイズレスポンシブ)に対応

まずは、リサイズレスポンシブ)対応から始めます。

いきなりコードを変更するのではなく、試験的に以下のような、HTMLを用意して、希望する動作をするかを確認します。特に、レスポンシブ対応の場合、頭で考えているとおりの動きになるかどうか、「実際に動かしてみないとわからない」ということが多いのです。

html タグ、 body タグを縦方向に 100% とし(横方向は自動的に広がるので、特に指定しなくてもオッケー?)、body タグには margin が効いているので、これを OFF にする意味で、 0 を指定します。

レスポンシブとしては、ヘッダ部分にある「メッセージ」は 30px、フッター部分にある「タップエリア」は 100px 固定の高さにして。中央の「キャンバス表示エリア」はレスポンシブに(ブラウザの大きさにあわせて)最大の広さになるように display: grid;grid-template-rows: 30px 1fr 100px; を指定しています。

<html style="height: 100%;"><meta charset="utf-8" />
<body style="height: 100%; margin: 0;">
<div style="height: 100%; display: grid; grid-template-rows: 30px 1fr 100px;" >
    <div style="background-color: white;"   >メッセージ</div>
    <div style="background-color: silver;"  >キャンバス表示エリア</div>
    <div style="background-color: skyblue;" >タップエリア</div>
</div>
</body></html>

CodePen2

まずは、HTML だけをブラウザで表示して、希望通りレスポンシブに表示されるか、ブラウザの表示エリアを変えて確認します。短いコードで、いったん動作を確認してから、メインのコードに組み込んでいきます。

この HTML の中央 div タグ「キャンバス表示エリア」に canvas タグをハメ込むようにコードを書き換えます。
主に、class CanvasAreaにまとめておいたコードに、リサイズレスポンシブ)の変更を加えます。

リサイズ対応前のコード
// canvas表示エリア
class CanvasArea
{
    constructor()
    {
        this.cv_w = 640
        this.cv_h = 480
        this.cv = document.createElement( 'canvas' )
        this.cv.setAttribute( 'width',  this.cv_w )
        this.cv.setAttribute( 'height', this.cv_h )
        document.body.appendChild( this.cv )
        this.ctx = this.cv.getContext( '2d' )
    }

    // 画面クリア
    draw()
    {
        this.ctx.fillStyle = 'silver' // 'white'
        this.ctx.fillRect( 0, 0, this.cv_w, this.cv_h )
    }
}

リサイズの処理
window.onresize = ()=>{
    this.resize()
}

リサイズ対応後のコード
// canvas表示エリア
class CanvasArea
{
    constructor()
    {
        this.div_area = document.querySelector( 'div#area' )
        this.cv = document.createElement( 'canvas' )
        this.cv.setAttribute( 'style', 'position: absolute;' ) // 自動で反映されないので、あえて子供から浮かせる。
        this.div_area.appendChild( this.cv )
        this.ctx = this.cv.getContext( '2d' )
        window.onresize = ()=>{
            this.resize()
        }

        this.resize()
    }

    resize()
    {   // canvas タグは div タグのように、縦横サイズの自動調整が効かないので、親 div と同じサイズにあわせる
        this.cv_w = this.div_area.clientWidth       // 親の div の縦横サイズを取得
        this.cv_h = this.div_area.clientHeight
        this.cv.setAttribute( 'width',  this.cv_w ) // 親 div の縦横サイズにあわせる
        this.cv.setAttribute( 'height', this.cv_h )

        this.draw()
    }

    // 画面クリア
    draw()
    {
        this.ctx.fillStyle = 'silver' // 'white'
        this.ctx.fillRect( 0, 0, this.cv_w, this.cv_h )
    }
}

コードを修正する範囲は、class CanvasArea に収まっています。オブジェクト指向すげー!

ってか、本当は、canvas タグも自動でサイズ調整が効けば、こんなコードを書かなくてもすむのですがねー(きっとなにかしらの理由があるのでしょう。)

(2) タッチイベントの対応

次は、タッチイベント(タップ)に対応します。

ラケットを動かす class Racket 内にある、 onmousemove イベント処理と同じ場所に、↑の HTML で用意した、水色のフッター部分に相当する、div#touch タグに、タッチイベントを追加するだけです。

    // タッチイベントの処理
    const div_touch = document.querySelector( 'div#touch' )
    div_touch.ontouchstart =
    div_touch.ontouchmove = ev => {
        this.x = ev.pageX - 50
        return false // イベントを伝搬しない(親に返すと、フリックやスクロールに反応していろいろ面倒なので)
    }

ontouchstartontouchmove に同じ関数を設定しているので、ちょっと変な書き方をしていますが、想定内(許容範囲内)ですよね。

タッチイベントの実装に関しては、手前味噌ですが、このあたり↓の記事も参考にしてください。

JavaScript タッチイベントの取得(マルチ対応) サンプルコード
https://qiita.com/yamazaki3104/items/1f550c589b13febade82

さて、スマホ対応の全コードはこのように↓なります。ここ↓「全コード」をクリックで開きます。

全コード
<html style="height: 100%;"><meta charset="utf-8" />
<body style="height: 100%; margin: 0;">
<div style="height: 100%; display: grid; grid-template-rows: 30px 1fr 100px;" >
<div id="mess" ></div>
<div id="area" ></div>
<div id="touch" style="background-color: skyblue;" ></div>
</div>
</body><script>
// 「こんにちは Python」のスカッシュゲーム(壁打ちテニス)を JavaScript で写経(音無し版)
// レスポンシブ対応版

// メッセージをランダムに選択
function get_random_message( ary )
{
    return ary[ Math.floor( Math.random() * ary.length ) ]
}

// canvas表示エリア
class CanvasArea
{
    constructor()
    {
        this.div_area = document.querySelector( 'div#area' )
        this.cv = document.createElement( 'canvas' )
        this.cv.setAttribute( 'style', 'position: absolute;' ) // 自動で反映されないので、あえて子供から浮かせる。
        this.div_area.appendChild( this.cv )
        this.ctx = this.cv.getContext( '2d' )
        window.onresize = ()=>{
            this.resize()
        }

        this.resize()
    }

    resize()
    {   // canvas タグは div タグのように、縦横サイズの自動調整が効かないので、親 div と同じサイズにあわせる
        this.cv_w = this.div_area.clientWidth       // 親の div の縦横サイズを取得
        this.cv_h = this.div_area.clientHeight
        this.cv.setAttribute( 'width',  this.cv_w ) // 親 div の縦横サイズにあわせる
        this.cv.setAttribute( 'height', this.cv_h )

        this.draw()
    }

    // 画面クリア
    draw()
    {
        this.ctx.fillStyle = 'silver' // 'white'
        this.ctx.fillRect( 0, 0, this.cv_w, this.cv_h )
    }
}

// ボール
class Ball
{
    constructor()
    {
        this.init( 0, 250, 15, -15 )
        this.r_size = 10
    }

    init( _x, _y, _sx, _sy )
    {
        this.x = _x
        this.y = _y
        this.speed_x = _sx
        this.speed_y = _sy
    }

    // ボールを描く
    draw()
    {
        area.ctx.beginPath()
        area.ctx.arc( this.x, this.y, this.r_size, 0, 2 * Math.PI )
        area.ctx.fillStyle = 'red'
        area.ctx.fill()
    }

    // ボールの移動
    move()
    {
        if ( is_gameover ) return

        // 左右の壁に当たったのかの判定
        if ( this.x+this.speed_x < 0   ) {
            this.speed_x *= -1
        }
        if ( this.x+this.speed_x > area.cv_w ) {
            this.speed_x *= -1
        }

        // 天井の壁に当たったのかの判定
        if ( this.y+this.speed_y < 0 ) {
            this.speed_y *= -1
        }

        // ラケットに当たったのかの判定
        if ( this.y+this.speed_y > area.cv_h-10 ) {
            if ( racket.x < this.x+this.speed_x ) {
                if ( this.x+this.speed_x < racket.x+racket.w_size ) {
                    this.speed_y *= -1
                    if ( Math.random() < 0.5 )
                        this.speed_x *= -1

                    const message = get_random_message( [
                        'うまい!', 'グッド!', 'ナイス!', 'よしッ!', 'すてき!'
                    ] )
                    point += 10
                    div.innerText = message + ' 得点:' + point
                }
            }
        }

        // ミスした時の判定
        if ( this.y+this.speed_y > area.cv_h ) {
            const message = get_random_message( [
                'へたくそ!','ミスしたね!', 'あーあ、見てられないね!'
            ] )
            div.innerText = message + ' 得点:' + point

            is_gameover = true
            return
        }

        // ボールを移動
        this.x += this.speed_x
        this.y += this.speed_y
    }
}

// ラケット
class Racket
{
    constructor()
    {
        this.x = 0
        this.w_size = 100

        // マウスの動きの処理
        area.cv.onmousemove = ev => {
            this.x = ev.offsetX - 50
        }

        // タッチイベントの処理
        const div_touch = document.querySelector( 'div#touch' )
        div_touch.ontouchstart =
        div_touch.ontouchmove = ev => {
            this.x = ev.pageX - 50
            return false // イベントを伝搬しない(親に返すと、フリックやスクロールに反応していろいろ面倒なので)
        }
    }

    draw()
    {
        area.ctx.fillStyle = 'yellow'
        area.ctx.fillRect( this.x, area.cv_h-10, this.w_size, 8 )
    }
}

// メッセージ表示エリア div の確保
const div = document.querySelector( 'div#mess' )
div.innerText = "スカッシュゲーム(音無し版):マウスクリックでスタート!"

const area   = new CanvasArea()
const ball   = new Ball()
const racket = new Racket()

let is_gameover = true
let point = 0

// クリックで再スタート
area.cv.onclick = ev =>
{
    if ( ! is_gameover ) return

    // ゲームの初期化
    is_gameover = false
    point = 0
    ball.init( 0, 250, 15, -15 )
    div.innerText = "スカッシュゲーム:スタート!"
}

// ゲームの繰り返し処理
function game_loop()
{
    area.draw()     // 画面クリア
    ball.draw()     // ボールを描く
    racket.draw()   // ラケットを描く
    ball.move()     // ボールの移動
}

// ゲームのメイン処理開始
setInterval( game_loop, 50 ) // 20fps

</script></html>

これでブラウザの大きさを変えても、canvas のエリアが追従するはず、こんな感じ↓。
スマホのブラウザで開いたときに、いい感じの配置になる予定です。

CodePen3

スマホで確認してみたいかたは、こちら↓の QR コードから開いてみてください。

qr.png

動いてますか?

3、音を出す

今回の写経で一番苦労したのが、ここです。ブラウザで音を出すって、意外に難しかったです。

難しい理由は、大きく2つあって、1つ目は
「ブラウザで音を出す」をググると「メディア要素(<audio>タグ)で mp3 を鳴らす」
という記事ばかりで、ブラウザではmp3 以外に音を鳴らすことができないのではないか?と錯覚してしまうほどです。

しばらく探すと、Web Audio API というAPI が用意されていることがわかります。ただ、まだ新しい、実験中の機能のようで、使っている人も少なく、すなわち参考文献がとても少ないです。

さて2つ目の問題は、ブラウザ特有の文化的背景の制限があります。
理解できるものの、なんとなくモヤッとする理由(メディア要素(<video>, <audio>)の自動再生におけるポリシーの変更)から、ブラウザを開いた瞬間に音は出せないようになっています。詳しくは「ユーザーのクリックアクションがキーになって音を出し始めないといけない」と決めたようです。
このヘンテコな仕様からプログラマは泣かされます。コードの書き方によって音が出たり出なかったり、ブラウザによって、動きが異なったり。どう書くのが正解なの??!!と。

で、結局は、試行錯誤をして、動いているコードが正義!とばかりに、「なぜかよくわからないけど、こう書くと動く」というコードが氾濫しています。混乱に混乱が重なります。

これらの問題を乗り越えて、ようやくcreateOscillator()というAPI があることにたどり着けます。
このAPIを呼べば、mp3 という、基本的に巨大なファイルを用意しなくても、ピコピコ音が出せるのです。とは言え、まだドラフト(草案)レベルなので、たまたま鳴っているだけなのですがね。

<body>
音がでます。でるはずです。たぶん。<br />
<button onclick="sine.beep()"> sine!</button>
<button onclick="square.beep()">square!</button>
<button onclick="sawtooth.beep()">sawtooth!</button>
<button onclick="triangle.beep()">triangle!</button>
</body><script>
// 音源の確保
class Beep
{
    constructor( _frq=2000, _typ='square' )
    {
        this.acx       = null   // 最初の一回だけフラグ
        this.type      = _typ
        this.beep_frq  = _frq   // 音程(周波数)
        this.beep_time = 0      // 長さ
    }

    beep( _t=5 )
    {
        this.beep_time = _t
        if ( this.acx != null ) return // 最初の一回だけ

        this.acx = new (window.AudioContext || window.webkitAudioContext)()
        this.osc = this.acx.createOscillator()
        this.vol = this.acx.createGain()
        this.osc.connect(this.vol).connect(this.acx.destination)
        this.vol.gain.value = 0     // 音量
        this.osc.type = this.type   // https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode/type
        this.osc.frequency.value = this.beep_frq // 音程(周波数)
        this.osc.start()

        // 音の処理
        setInterval( ()=>
        {
            if ( this.beep_time <= 0 ) {
                this.vol.gain.value = 0 // 音量 OFF
                return
            }
            this.vol.gain.value = 0.2  // 音量
            this.beep_time -= 1        // 長さ
        }, 10 )
    }
}

const sine     = new Beep( 2000, 'sine' )
const square   = new Beep( 2000, 'square' )
const sawtooth = new Beep( 2000, 'sawtooth' )
const triangle = new Beep( 2000, 'triangle' )

</script>

CodePen4

今回の写経で、いちばん苦労したのがこの音の実装でした。無事に音が鳴って、よかったです。

JavaScript全コード

というわけで、壁やラケット、そして床に落ちてゲームオーバーになった箇所に音を出すコードを埋めていきます。

全コードは以下です。これで最後なので、閉じずに載せます。

<html style="height: 100%;"><meta charset="utf-8" />
<body style="height: 100%; margin: 0;">
<div style="height: 100%; display: grid; grid-template-rows: 30px 1fr 100px;" >
<div id="mess" ></div>
<div id="area" ></div>
<div id="touch" style="background-color: skyblue;" ></div>
</div>
</body><script>
// 「こんにちは Python」のスカッシュゲーム(壁打ちテニス)を JavaScript で写経(音あり版)

// メッセージをランダムに選択
function get_random_message( ary )
{
    return ary[ Math.floor( Math.random() * ary.length ) ]
}

// canvas表示エリア
class CanvasArea
{
    constructor()
    {
        this.div_area = document.querySelector( 'div#area' )
        this.cv = document.createElement( 'canvas' )
        this.cv.setAttribute( 'style', 'position: absolute;' ) // 自動で反映されないので、あえて子供から浮かせる。
        this.div_area.appendChild( this.cv )
        this.ctx = this.cv.getContext( '2d' )
        window.onresize = ()=>{
            this.resize()
        }

        this.resize()
    }

    resize()
    {   // canvas タグは div タグのように、縦横サイズの自動調整が効かないので、親 div と同じサイズにあわせる
        this.cv_w = this.div_area.clientWidth       // 親の div の縦横サイズを取得
        this.cv_h = this.div_area.clientHeight
        this.cv.setAttribute( 'width',  this.cv_w ) // 親 div の縦横サイズにあわせる
        this.cv.setAttribute( 'height', this.cv_h )

        this.draw()
    }

    // 画面クリア
    draw()
    {
        this.ctx.fillStyle = 'silver' // 'white'
        this.ctx.fillRect( 0, 0, this.cv_w, this.cv_h )
    }
}

// ボール
class Ball
{
    constructor()
    {
        this.init( 0, 250, 15, -15 )
        this.r_size = 10
    }

    init( _x, _y, _sx, _sy )
    {
        this.x = _x
        this.y = _y
        this.speed_x = _sx
        this.speed_y = _sy
    }

    // ボールを描く
    draw()
    {
        area.ctx.beginPath()
        area.ctx.arc( this.x, this.y, this.r_size, 0, 2 * Math.PI )
        area.ctx.fillStyle = 'red'
        area.ctx.fill()
    }

    // ボールの移動
    move()
    {
        if ( is_gameover ) return

        // 左右の壁に当たったのかの判定
        if ( this.x+this.speed_x < 0   ) {
            this.speed_x *= -1
            po1320.beep() // ♪
        }
        if ( this.x+this.speed_x > area.cv_w ) {
            this.speed_x *= -1
            po1320.beep() // ♪
        }

        // 天井の壁に当たったのかの判定
        if ( this.y+this.speed_y < 0 ) {
            this.speed_y *= -1
            pi2000.beep() // ♪
        }

        // ラケットに当たったのかの判定
        if ( this.y+this.speed_y > area.cv_h-10 ) {
            if ( racket.x < this.x+this.speed_x ) {
                if ( this.x+this.speed_x < racket.x+racket.w_size ) {
                    this.speed_y *= -1
                    if ( Math.random() < 0.5 )
                        this.speed_x *= -1

                    pi2000.beep() // ♪
                    const message = get_random_message( [
                        'うまい!', 'グッド!', 'ナイス!', 'よしッ!', 'すてき!'
                    ] )
                    point += 10
                    div.innerText = message + ' 得点:' + point
                }
            }
        }

        // ミスした時の判定
        if ( this.y+this.speed_y > area.cv_h ) {
            const message = get_random_message( [
                'へたくそ!','ミスしたね!', 'あーあ、見てられないね!'
            ] )
            div.innerText = message + ' 得点:' + point
            boo200.beep( 80 ) // ♪

            is_gameover = true
            return
        }

        // ボールを移動
        this.x += this.speed_x
        this.y += this.speed_y
    }
}

// ラケット
class Racket
{
    constructor()
    {
        this.x = 0
        this.w_size = 100

        // マウスの動きの処理
        area.cv.onmousemove = ev => {
            this.x = ev.offsetX - 50
        }

        // タッチイベントの処理
        const div_touch = document.querySelector( 'div#touch' )
        div_touch.ontouchstart =
        div_touch.ontouchmove = ev => {
            this.x = ev.pageX - 50
            return false // イベントを伝搬しない(親に返すと、フリックやスクロールに反応していろいろ面倒なので)
        }
    }

    draw()
    {
        area.ctx.fillStyle = 'yellow'
        area.ctx.fillRect( this.x, area.cv_h-10, this.w_size, 8 )
    }
}

// 音源の確保
class Beep
{
    constructor( _frq=2000, _typ='square' )
    {
        this.acx       = null   // 最初の一回だけフラグ
        this.type      = _typ
        this.beep_frq  = _frq   // 音程(周波数)
        this.beep_time = 0      // 長さ
    }

    beep( _t=5 )
    {
        this.beep_time = _t
        if ( this.acx != null ) return // 最初の一回だけ

        this.acx = new (window.AudioContext || window.webkitAudioContext)()
        this.osc = this.acx.createOscillator()
        this.vol = this.acx.createGain()
        this.osc.connect(this.vol).connect(this.acx.destination)
        this.vol.gain.value = 0     // 音量
        this.osc.type = this.type   // https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode/type
        this.osc.frequency.value = this.beep_frq // 音程(周波数)
        this.osc.start()

        // 音の処理
        setInterval( ()=>
        {
            if ( this.beep_time <= 0 ) {
                this.vol.gain.value = 0 // 音量 OFF
                return
            }
            this.vol.gain.value = 0.2  // 音量
            this.beep_time -= 1        // 長さ
        }, 10 )
    }
}

// メッセージ表示エリア div の確保
const div = document.querySelector( 'div#mess' )
div.innerText = "スカッシュゲーム(音あり版):マウスクリックでスタート!"

const area   = new CanvasArea()
const ball   = new Ball()
const racket = new Racket()
const pi2000 = new Beep( 2000 )
const po1320 = new Beep( 1320 )
const boo200 = new Beep(  200 )

let is_gameover = true
let point = 0

// クリックで再スタート
area.cv.onclick = ev =>
{
    if ( ! is_gameover ) return

    // ゲームの初期化
    is_gameover = false
    point = 0
    ball.init( 0, 250, 15, -15 )
    div.innerText = "スカッシュゲーム:スタート!"

    // 音の初期化(最初のクリック時に鳴らす必要がある)
    pi2000.beep( 0 )    // 長さ 0 なので音は出さない
    po1320.beep( 0 )    // 長さ 0 なので音は出さない
    boo200.beep( 0 )    // 長さ 0 なので音は出さない
}

// ゲームの繰り返し処理
function game_loop()
{
    area.draw()     // 画面クリア
    ball.draw()     // ボールを描く
    racket.draw()   // ラケットを描く
    ball.move()     // ボールの移動
}

// ゲームのメイン処理開始
setInterval( game_loop, 50 ) // 20fps

</script></html>

CodePen5

最終的には、このようになります。

さて、動いたでしょうか。例によって、スマホで確認してみたいかたのために、QRコードを置いておきます。

qr.png

おまけ リフレッシュレートを超える 20fps -> 150fps

このゲームは、もとの Python のコードに合わせて 20fps で動いています。20fps とは1秒間に20回、描きなおしているということです。
20fps の速度で更新できれば、おおむねアニメーションしているように見えると言われています。
ただ、最近の PC やブラウザの描画能力はスゴイことになっていて、このゲームのような簡単な描画処理であれば、ディスプレイの表示能力(リフレッシュレート)を超える速度で描画できると思われます。

ディスプレイのリフレッシュレートとは、1秒間に何回表示を更新できるか、というディスプレイの処理性能を示しています。最近のディスプレイはおおむね 60Hz か、それを超えるもの増えています。60Hz というのは、すなわち ゲームプログラムが 60fps で描画してもその変化を漏らさず表示されるということになります(その変化を人がすべてを感じ取れるかどうかは別の話ですが、)。
しかしながら、ゲームプログラムが 120fps で描画して表示を更新できたとしても、ディスプレイが 60Hz では、その更新の半分しかディスプレイに反映されない=見えていないことになります。ですので、表示の更新は、ディスプレイのリフレッシュレートに合わせるのが賢いコードになります。

ブラウザには、requestAnimationFrame というAPIが用意されていて、このAPIは「ディスプレイのリフレッシュのタイミングで関数を呼んでくれる」という大変便利な API なのです。この API を使って描画する関数を登録し、呼ばれたときに表示を更新すると、ちょうどよい動きになる仕組みになっています。

というわけで、「ゲームの繰り返し処理」に requestAnimationFrame を組み込んだコードがこちらです。

// ゲームの繰り返し処理
let request_animation_frame = null
function game_loop()
{
    ball.move()     // ボールの移動

    if ( request_animation_frame != null ) return

    request_animation_frame = window.requestAnimationFrame( ev =>
    {
        area.draw()     // 画面クリア
        ball.draw()     // ボールを描く
        racket.draw()   // ラケットを描く

        request_animation_frame = null // クリア(次の描画イベントの登録を受け付ける)
    } )
}

ループ内で、移動処理をしたあとに描画イベントの登録をします。
ただ、描画処理が終わるくらいループ速度がゆっくりであれば問題ないのですが、描画処理よりもループ速度が早い場合は、イベントを登録しないように null を確認してスキップしています。こうすることで、ディスプレイのリフレッシュレートを気にすること無く、ループ速度を上げることができます。

ということで、最近、手に入るようになってきたゲーミング用ディスプレイのリフレッシュレート 144Hz を超えるフレームレート(ここでは 150fps )に改造して、終わりたいと思います。

全コード
<html style="height: 100%;"><meta charset="utf-8" />
<body style="height: 100%; margin: 0;">
<div style="height: 100%; display: grid; grid-template-rows: 30px 1fr 100px;" >
<div id="mess" ></div>
<div id="area" ></div>
<div id="touch" style="background-color: skyblue;" ></div>
</div>
</body><script>
// 「こんにちは Python」のスカッシュゲーム(壁打ちテニス)を JavaScript で写経(音あり版)

// メッセージをランダムに選択
function get_random_message( ary )
{
    return ary[ Math.floor( Math.random() * ary.length ) ]
}

// canvas表示エリア
class CanvasArea
{
    constructor()
    {
        this.div_area = document.querySelector( 'div#area' )
        this.cv = document.createElement( 'canvas' )
        this.cv.setAttribute( 'style', 'position: absolute;' ) // 自動で反映されないので、あえて子供から浮かせる。
        this.div_area.appendChild( this.cv )
        this.ctx = this.cv.getContext( '2d' )
        window.onresize = ()=>{
            this.resize()
        }

        this.resize()
    }

    resize()
    {   // canvas タグは div タグのように、縦横サイズの自動調整が効かないので、親 div と同じサイズにあわせる
        this.cv_w = this.div_area.clientWidth       // 親の div の縦横サイズを取得
        this.cv_h = this.div_area.clientHeight
        this.cv.setAttribute( 'width',  this.cv_w ) // 親 div の縦横サイズにあわせる
        this.cv.setAttribute( 'height', this.cv_h )

        this.draw()
    }

    // 画面クリア
    draw()
    {
        this.ctx.fillStyle = 'silver' // 'white'
        this.ctx.fillRect( 0, 0, this.cv_w, this.cv_h )
    }
}

// ボール
class Ball
{
    constructor()
    {
        this.init( 0, 250, 15, -15 )
        this.r_size = 10
    }

    init( _x, _y, _sx, _sy )
    {
        this.x = _x
        this.y = _y
        this.speed_x = _sx
        this.speed_y = _sy
    }

    // ボールを描く
    draw()
    {
        area.ctx.beginPath()
        area.ctx.arc( this.x, this.y, this.r_size, 0, 2 * Math.PI )
        area.ctx.fillStyle = 'red'
        area.ctx.fill()
    }

    // ボールの移動
    move()
    {
        if ( is_gameover ) return

        // 左右の壁に当たったのかの判定
        if ( this.x+this.speed_x < 0   ) {
            this.speed_x *= -1
            po1320.beep() // ♪
        }
        if ( this.x+this.speed_x > area.cv_w ) {
            this.speed_x *= -1
            po1320.beep() // ♪
        }

        // 天井の壁に当たったのかの判定
        if ( this.y+this.speed_y < 0 ) {
            this.speed_y *= -1
            pi2000.beep() // ♪
        }

        // ラケットに当たったのかの判定
        if ( this.y+this.speed_y > area.cv_h-10 ) {
            if ( racket.x < this.x+this.speed_x ) {
                if ( this.x+this.speed_x < racket.x+racket.w_size ) {
                    this.speed_y *= -1
                    if ( Math.random() < 0.5 )
                        this.speed_x *= -1

                    pi2000.beep() // ♪
                    const message = get_random_message( [
                        'うまい!', 'グッド!', 'ナイス!', 'よしッ!', 'すてき!'
                    ] )
                    point += 10
                    div.innerText = message + ' 得点:' + point
                }
            }
        }

        // ミスした時の判定
        if ( this.y+this.speed_y > area.cv_h ) {
            const message = get_random_message( [
                'へたくそ!','ミスしたね!', 'あーあ、見てられないね!'
            ] )
            div.innerText = message + ' 得点:' + point
            boo200.beep( 80 ) // ♪

            is_gameover = true
            return
        }

        // ボールを移動
        this.x += this.speed_x
        this.y += this.speed_y
    }
}

// ラケット
class Racket
{
    constructor()
    {
        this.x = 0
        this.w_size = 100

        // マウスの動きの処理
        area.cv.onmousemove = ev => {
            this.x = ev.offsetX - 50
        }

        // タッチイベントの処理
        const div_touch = document.querySelector( 'div#touch' )
        div_touch.ontouchstart =
        div_touch.ontouchmove = ev => {
            this.x = ev.pageX - 50
            return false // イベントを伝搬しない(親に返すと、フリックやスクロールに反応していろいろ面倒なので)
        }
    }

    draw()
    {
        area.ctx.fillStyle = 'yellow'
        area.ctx.fillRect( this.x, area.cv_h-10, this.w_size, 8 )
    }
}

// 音源の確保
class Beep
{
    constructor( _frq=2000, _typ='square' )
    {
        this.acx       = null   // 最初の一回だけフラグ
        this.type      = _typ
        this.beep_frq  = _frq   // 音程(周波数)
        this.beep_time = 0      // 長さ
    }

    beep( _t=5 )
    {
        this.beep_time = _t
        if ( this.acx != null ) return // 最初の一回だけ

        this.acx = new (window.AudioContext || window.webkitAudioContext)()
        this.osc = this.acx.createOscillator()
        this.vol = this.acx.createGain()
        this.osc.connect(this.vol).connect(this.acx.destination)
        this.vol.gain.value = 0     // 音量
        this.osc.type = this.type   // https://developer.mozilla.org/en-US/docs/Web/API/OscillatorNode/type
        this.osc.frequency.value = this.beep_frq // 音程(周波数)
        this.osc.start()

        // 音の処理
        setInterval( ()=>
        {
            if ( this.beep_time <= 0 ) {
                this.vol.gain.value = 0 // 音量 OFF
                return
            }
            this.vol.gain.value = 0.2  // 音量
            this.beep_time -= 1        // 長さ
        }, 10 )
    }
}

// メッセージ表示エリア div の確保
const div = document.querySelector( 'div#mess' )
div.innerText = "スカッシュゲーム(音あり版):マウスクリックでスタート!"

const area   = new CanvasArea()
const ball   = new Ball()
const racket = new Racket()
const pi2000 = new Beep( 2000 )
const po1320 = new Beep( 1320 )
const boo200 = new Beep(  200 )

let is_gameover = true
let point = 0

// クリックで再スタート
area.cv.onclick = ev =>
{
    if ( ! is_gameover ) return

    // ゲームの初期化
    is_gameover = false
    point = 0
    ball.init( 0, 250, 2, -2 ) // 20fps -> 150fps 対応 15px -> 2px
    div.innerText = "スカッシュゲーム:スタート!"

    // 音の初期化(最初のクリック時に鳴らす必要がある)
    pi2000.beep( 0 )    // 長さ 0 なので音は出さない
    po1320.beep( 0 )    // 長さ 0 なので音は出さない
    boo200.beep( 0 )    // 長さ 0 なので音は出さない
}

// ゲームの繰り返し処理
let request_animation_frame = null
function game_loop()
{
    ball.move()     // ボールの移動

    if ( request_animation_frame != null ) return

    request_animation_frame = window.requestAnimationFrame( ev =>
    {
        area.draw()     // 画面クリア
        ball.draw()     // ボールを描く
        racket.draw()   // ラケットを描く

        request_animation_frame = null // クリア(次の描画イベントの登録を受け付ける)
    } )
}

// ゲームのメイン処理開始
setInterval( game_loop, 1000/150 ) // 150fps

</script></html>

まとめ

お疲れ様でしたー。

前編から書き始めて、どのくらいの月日がたったでしょうか、、、すでに、覚えていないです。

すこしでも、JavaScriptプログラミングの楽しさがお伝えできれば幸いです。

最後に、ゲームを作る楽しさを思い出させていただいた、すがやみつる先生に感謝とお礼をしつつ終わりたいと思います。どうもありがとうございました!

それでは、みなさまの快適なプログラミングライフを願いつつ終わります。

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

【CreateJS】手書き入力インタフェース

はじめに

手書き文字をAIに判別させるデータは、MNISTなどでデータがフリーで公開されています。そのデータを利用すると、簡単に識別モデルを作ったり、推論させたりすることができます。しかし、実際に自分が書いた文字をAIに食わせてみたいと思っても、デジタルデータにするのが意外と面倒くさいですよね。そこで、手書き文字をデジタルデータへ簡単に変換できるInterfaceをjavascript(CreateJS)で作成してみました。

プログラム

javascript(CreateJS)を使用したプログラムです。

tegaki_Interface.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <script src="https://code.createjs.com/1.0.0/createjs.min.js"> </script>
</head>
<body>
  描画キャンバス
  <div><canvas id="WriteCanvas" width="240" height="240"></canvas></div>
  <div><canvas id="ButtonCanvas" width="240" height="100"></canvas></div>
  PNG形式
  <div><img id="copyImg" width="240" height="240" background="#000000"></div>
</body>
</html>

<script>
window.addEventListener("load", init);
function init() {

    // --------------------------------------------------------------
    // Stage1オブジェクト:WriteCanvas
    // --------------------------------------------------------------
    var stage1 = new createjs.Stage("WriteCanvas");

    // タッチイベントが有効なブラウザの場合、
    // CreateJSでタッチイベントを扱えるようにする
    if (createjs.Touch.isSupported()) {
        createjs.Touch.enable(stage1);
    }

    var shape = new createjs.Shape();   // シェイプを作成
    stage1.addChild(shape);             // ステージに配置

    handleClick_reset();

    // ステージ上でマウスボタンを押した時のイベント設定
    stage1.addEventListener("stagemousedown", handleDown);

    // マウスを押した時に実行される
    function handleDown(event) {

        var paintColor = "#FFFFFF"                      // 筆ペンの色

        // 線の描画を開始
        shape.graphics
                .beginStroke(paintColor)                // 指定のカラーで描画
                .setStrokeStyle(20, "round")            // 線の太さ、形
                .moveTo(event.stageX, event.stageY);    // 描画開始位置を指定

        // ステージ上でマウスを動かした時と離した時のイベント設定
        stage1.addEventListener("stagemousemove", handleMove);
        stage1.addEventListener("stagemouseup", handleUp);
    }

    // マウスが動いた時に実行する
    function handleMove(event) {

        // マウス座標への線を引く
        shape.graphics.lineTo(event.stageX, event.stageY);
    }

    // マウスボタンが離された時に実行される
    function handleUp(event) {

        // マウス座標への線を引く
        shape.graphics.lineTo(event.stageX, event.stageY);

        // 線の描画を終了する
        shape.graphics.endStroke();

        // イベント解除
        stage1.removeEventListener("stagemousemove", handleMove);
        stage1.removeEventListener("stagemouseup", handleUp);
    }

    createjs.Ticker.timingMode = createjs.Ticker.RAF;
    createjs.Ticker.addEventListener("tick", onTick);

    function onTick() {
        stage1.update(); // Stageの描画を更新
    }

    // --------------------------------------------------------------
    // Stage2オブジェクト:ButtonCanvas
    // --------------------------------------------------------------
    var stage2 = new createjs.Stage("ButtonCanvas");
    stage2.enableMouseOver();

    // ボタンを作成
    var btn1 = createButton("PNG変換", 80, 30, "#0650c7");
    btn1.x = 20;
    btn1.y = 10;
    stage2.addChild(btn1);

    var btn2 = createButton("Reset!", 80, 30, "#d10a50");
    btn2.x = 110;
    btn2.y = 10;
    stage2.addChild(btn2);

    // イベントを登録
    btn1.addEventListener("click", handleClick_png);
    btn2.addEventListener("click", handleClick_reset);

    // PNG変換ボタン押下イベント
    function handleClick_png(event) {

        // Canvasタグから画像に変換
        stage1.update();
        var png = stage1.canvas.toDataURL();
        document.getElementById("copyImg").src = png;

        /*
        var w = window.open('about:blank');
        w.document.write("<img src='" + png + "'/>");
        */
    }

    // Rest!ボタン押下イベント
    function handleClick_reset(event) {

        // シェイプのグラフィックスを消去
        shape.graphics.clear();
        shape.graphics.beginFill("black");
        shape.graphics.drawRect(0, 0, 240, 240);
        shape.graphics.endFill();
        stage1.update();
        var png = stage1.canvas.toDataURL();
        document.getElementById("copyImg").src = png;
    }

    // 時間経過イベント
    createjs.Ticker.addEventListener("tick", handleTick);
    function handleTick() {

        // Stage2の描画を更新
        stage2.update();
    }

    /**
    * @param {String} text ボタンのラベル文言です。
    * @param {Number} width ボタンの横幅(単位はpx)です。
    * @param {Number} height ボタンの高さ(単位はpx)です。
    * @param {String} keyColor ボタンのキーカラーです。
    * @returns {createjs.Container} ボタンの参照を返します。
    */
    function createButton(text, width, height, keyColor) {

        // ボタン要素をグループ化
        var button = new createjs.Container();
        button.name = text; // ボタンに参考までに名称を入れておく(必須ではない)
        button.cursor = "pointer"; // ホバー時にカーソルを変更する

        // 通常時の座布団を作成
        var bgUp = new createjs.Shape();
        bgUp.graphics
              .setStrokeStyle(1.0)
              .beginStroke(keyColor)
              .beginFill("white")
              .drawRoundRect(0.5, 0.5, width - 1.0, height - 1.0, 4);
        button.addChild(bgUp);
        bgUp.visible = true; // 表示する

        // ロールオーバー時の座布団を作成
        var bgOver = new createjs.Shape();
        bgOver.graphics
              .beginFill(keyColor)
              .drawRoundRect(0, 0, width, height, 4);
        bgOver.visible = false; // 非表示にする
        button.addChild(bgOver);

        // ラベルを作成
        var label = new createjs.Text(text, "18px sans-serif", keyColor);
        label.x = width / 2;
        label.y = height / 2;
        label.textAlign = "center";
        label.textBaseline = "middle";
        button.addChild(label);

        // ロールオーバーイベントを登録
        button.addEventListener("mouseover", handleMouseOver);
        button.addEventListener("mouseout", handleMouseOut);

        // マウスオーバイベント
        function handleMouseOver(event) {
            bgUp.visble = false;
            bgOver.visible = true;
            label.color = "white";
        }

        // マウスアウトイベント
        function handleMouseOut(event) {
            bgUp.visble = true;
            bgOver.visible = false;
            label.color = keyColor;
        }

        return button;
    }
}
</script>

プログラム実行

Tegaki_Interface.html」をローカルPCなどに保存して、実行(ブラウザで表示)してみてください。
手書き文字デジタルデータ(PNG形式)に変換することができました??

参考

ics.mediaさんの「CreateJS でお絵描きツールを作ろう」記事を参考にさせて頂きました?

以上

お疲れ様でした?!

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

React+Redux+Firebaseで読書管理アプリを作るまで

はじめに

初めてアプリを作ったので、その工程を記録に残したいと思い記事を書いています。
個人開発やポートフォリオの作成に役立つ内容があれば幸いです。

作ったもの

Flipper-countup.gif
「Flipper」という、シンプルな読書管理アプリです。
今まで読んだページ数がアニメーションで大きく表示されることで、自分の積み上げを視覚的に実感できることを目指しました。

私について

プログラミングを仕事にしているわけではなく、普段は英語を教えたり、翻訳の仕事をしています。
約3〜4年前に趣味でコードを軽く触ったことはありましたが実務は未経験で、1ヶ月半前にJavaScriptとReactの勉強を始めました。

Flipper

今回制作した「Flipper」の紹介です。

コンセプト

  • シンプルで美しい
  • 積み上げを実感できる

この2つがFlipperのコンセプトです。

読書管理アプリは選択肢が豊富ですが、記録をグラフにする、SNSで共有できるなど、個人的に不要だと感じる機能が搭載されているものが多いです。そこで、自分が本当に必要だと思う機能のみで構成されたシンプルなアプリを作ろうと思いました。

工夫したのは今まで自分が読んだページ数がアニメーションで表示されるところです。繰り返しになりますが、自分の積み上げを視覚的に実感できることでユーザーの読書へのモチベーションを高めることにフォーカスしました。

趣味ではなく、自己研鑽として読書をしているユーザーがターゲットです。

使用技術

  • React
  • redux
  • redux-thunk
  • react-router
  • connected-react-router
  • react-countup
  • Firebase
  • Firebase Authentication
  • Cloud Firestore
  • Firebase Hosting
  • Material-UI
  • Google Books API

機能

  • 登録
    登録.gif
    ISBNコードを入力することで読んだ本を登録することができます。

  • リスト表示
    リストページ.png
    登録した本をリストで表示します。

  • 削除
    削除.gif
    リストページから本を削除できます。

開発準備

コードを書く前の段階で行ったことです。

デザイン

flipper-note_1.jpg

かなり雑ですが、ノートに手書きすることでイメージを固めました。

タスク化

trello.png

ノートへ書き出した画面からどんな機能が必要なのかを考え、何をすべきかを明確にします。
タスク管理にはTrelloを使用しました。

開発をスムーズに進めるため、コードを書き始める前にある程度準備しておくことはとても大切だと感じました。

コードを書く

事前にタスク化を行っていたことで、ここからは「書く」「分からないことを調べる」を繰り返すだけでした。
開発期間は約2日です。

主な処理を書いたコードを記載します。

Google Books APIからデータを取得、データを保存

operations.js
export const searchAndSetBook = (isbn) => {
  return async (dispatch, getState) => {
    // 本のデータを取得
    fetch(
      `https://www.googleapis.com/books/v1/volumes?q=${isbn}&maxResults=1`,
      {}
    )
      .then((response) => {
        if (response.status === 200) {
          return response.json();
        } else {
          throw new Error("データの取得に失敗しました");
        }
      })
      .then((data) => {
        //  必要なデータのオブジェクトを作成
        return {
          title: data.items[0].volumeInfo.title,
          pages: data.items[0].volumeInfo.pageCount,
          image: data.items[0].volumeInfo.imageLinks["thumbnail"],
        };
      })
      .then((book) => {
        const uid = getState().users.uid;
        const pages = getState().users.pages;
        const books = getState().users.books;

        const newPages = pages + book["pages"];
        books.unshift(book);

        // ストアを更新
        dispatch(
          newBookAction({
            pages: newPages,
            books: books
          })
        );

        //  データベースを更新
        db.collection("users").doc(uid).update({
          pages: newPages,
          books: books
        });
      })
      .then(() => {
        alert("登録が完了しました");
      })
      .catch((error) => {
        alert(`エラー!: ${error}`);
      });
  };
};

読んだ本のリストを表示

BookCardList.jsx
const BookCardList = () => {
  const selector = useSelector(state => state)
  const books = getBooks(selector)
  return (
    <div className="section-wrapper">
        {books.length > 0 && (
          books.map((book,index) => (
            <BookCard key={index} book={book} title={book.title} pages={book.pages}/>
          ))
        )}
    </div>
  );
};

本をリストから削除

operations.js
export const deleteBook = (title, pages) => {
  return async (dispatch, getState) => {
    const uid = getState().users.uid;
    const oldPages = getState().users.pages;
    const oldBooks = getState().users.books;

    const newPages = oldPages - pages;
    const newBooks = oldBooks.filter((book) => book.title !== title);

    dispatch(
      newBookAction({
        pages: newPages,
        books: newBooks,
      })
    );

    db.collection("users")
      .doc(uid)
      .update({
        pages: newPages,
        books: newBooks,
      })
      .then(() => {
        alert("削除しました。更新してください");
      })
      .catch((error) => {
        alert(`エラー!: ${error}`);
      });
  };
};

アプリを作ってみて

思ったことは以下の2点です。

  • 何よりも楽しい

学習中にチュートリアルをいくつかやってみましたが、1から初めて10で終わる教科書的な学び方に飽きてしまうことがありました。一方で実際の開発は学んだことが即アウトプットに繋がるのでスピード感があって良いですし、自分の成長と共にアプリが完成に近づいていくので達成感があります。

  • 興味関心の幅が広がる

今回の開発を通して、これから勉強したいことがどんどん出てきました。今回使用したRedux、Firebaseの理解に不十分な点があることにも気がつけましたし、レンダリングの速度が気になるのでSSRができるNext.jsにも興味があります。
また、Google Book APIのドキュメントを読んだ際、バックエンド側の知識不足を実感しました。フロントエンド以外の領域も学んで行きたいと思います。

おわりに

ここまでお読みいただきありがとうございました。

FlipperのURLはこちらです。ゲストユーザーを用意しているので、よければ触ってみてください。
Flipper

FlipperのコードはGitHubで見られます。
GitHub

twitterもやってます。Reactなど技術的なことを呟いているので、ぜひフォローしてください。
@indigo9alpha

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

【JavaScript】var / let / const を本気で使い分けてみた

はじめに

なんとなーくでlet/constを使い分けていたが
間違った理解をしていたので、本気で使い分ける方法をまとめてみました。

ざっくりした結論

  • ほぼ全部constで定義できる
  • 数え上げのみlet
  • varはもう使わない

参考:JavaScriptの変数宣言『let, var, const』の使い分けについて【プログラミング小話】

細かい仕様の説明

var/const/letの違い

const let var
再宣言 × ×
再代入 ×
スコープ ブロック ブロック 関数
ホイスティング エラー エラー undefined

再宣言

一度宣言した変数を、同じ変数名で宣言し直すことを再宣言といいます。
再宣言可能なvarで再宣言した場合、後に宣言した変数が適用され、
再宣言不可能なlet/constで再宣言した場合、エラーになります。
varでは、予期しない再宣言が起こりうるため危険です。

// var
var a = 0;
var a = 1; 
console.log(a) // `1`と出力される

// let
let b = 0;
let b = 1; // SyntaxError: Identifier 'b' has already been declared

// const
const c = 0;
const c = 1; // SyntaxError: Identifier 'c' has already been declared

再代入

宣言した変数に値を設定し直すことを再代入といいます。
再代入可能なvar/letで再代入した場合、値が再代入され、
再代入不可能なconstで再代入した場合、エラーになります。

// var
var a = 0;
a = 1; 
console.log(a) // `1`と出力される

// let
let b = 0; 
b = 1;
console.log(b) // `1`と出力される

// const
const c = 0;
c = 1; // Assignment to constant variable. 

スコープ

実行中のコードから参照できる範囲をスコープといいます。
const/letはブロックスコープ({}で囲われた部分 - if文やfor文など)が適用されますが、varはブロックスコープが適用されません。
※ 関数スコープ(関数宣言の{})は、var/const/letすべてに適用されます。

/*
 * var
 */
{
  var a = 0;
}
// ブロックスコープが適用されないため、ブロック外でも値の参照が可能
console.log(a); // 0


/*
 * let
 */
{
  // ブロックスコープにより、再宣言にならない。
  let a = 1;
  console.log(a); // 1
}
// letはブロックスコープであり参照できないため、varで宣言した値が参照される。
console.log(a); // 0

/*
 * const
 */
{
  const b = 2;
  console.log(b); //2
}
console.log(b) // b is not defined 

ホイスティング(変数の巻き上げ)

変数宣言が常に関数の先頭で行われたことにされる挙動をホイスティング(変数の巻き上げ)といいいます。

// var
{
  console.log(a); // undefined
  var a = 0; 
  console.log(a); // 0
}

// let / const
{
  console.log(b); // Cannot access 'b' before initialization 
  const b = 0; 
  console.log(b); // 0
}

データ型の種類と参照方法

var/const/letの変数宣言を使い分けるには、データ型の種類と参照方法を知っておく必要があります。

データ型の種類

データ型には、大きく分けてプリミティブ型オブジェクトがあります。

  • プリミティブ型(基本型)
    • Boolean - true または false
    • Number - 数値
    • String - 文字列
    • null - 空(参照を保持していない状態)
    • undefined - 未定義
    • BigInt (長整数) - 桁が多い数値(ES6)
    • Symbol (シンボル) - 一意の値(ES6)
  • オブジェクト(複合型) - プリミティブ型以外全部
    • オブジェクトリテラル
    • 配列リテラル  など

詳しい参考:データ型とリテラル - JavaScript Primer

データ型の参照

プリミティブ型とオブジェクトでは、参照方法が異なります。

プリミティブ型の場合

再代入の際に参照先が変更されます。

【図解 - 文字列の再代入の例】
Web 1920 – 1.png

オブジェクトの場合

変数に対して参照するオブジェクトが変更された際に再代入になります。
オブジェクト内のキーや値が変更されても、変数に対する参照先が変わらないため再代入にはなりません。

【図解 - オブジェクトの値変更・再代入の例】
Web 1920 – 2.png

もう一度、結論

  • ほぼ全部constで定義できる
    • オブジェクトや配列の値の変更は再代入にはならないため。
  • プリミティブ型を再代入したい場合はlet
  • varはもう使わない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アロー関数とasync/awaitを使った非同期処理の書き方を整理しよう

はじめに

昨今のモダンブラウザ、スマフォではアロー関数や、Acync/Awaitを利用したPromiseの利用は、Babelでトランスパイルする必要がない。
これらのJavaScriptを利用の仕方を個別で調べると古い書き方が散見して、今風に書くにはどう書いたら動くのか混乱した。

自分が知りたかった

  1. アロー関数で関数を定義して、その中でasync/awaitを利用した場合の書き方
  2. async/awaitが入れ子で呼ばれた場合どういう動きをするのか
  3. 非同期実行するもの複数実行してその結果を待つにはどうすればいいか

に調査しサンプルコードを書いてまとめた。

各関数のブラウザの対応
* アロー関数
* async
* await

下記をリンクから動作を確認できる。値を変えながら動作を確認すると理解しやすい
動作確認環境

アロー関数を使ったasync/awaitの書き方

数100msスリープするメソッドの例

fetchメソッドなどで非同期にデータを取得するような例のダミーとして利用する

// sleep するメソッド fetchなどの時間がかり結果をpromiseで返す関数
const sleep = (m) => new Promise( _ => setTimeout(_, m))

// promiseをasync awaitを使ってpromise終了後に文字を出力する
// ランダムに数100ms待つ
const asyncFunc = async (mes) => {
    const waitTime = Math.floor((Math.random() * 100) + 1)
    await sleep(waitTime)
    console.log(mes)
    return `result_${mes}`
}

asyncをメソッドの頭につけて囲い、const asyncFunc = async (mes) => {

非同期実行するメソッドの完了を待つ際にawaitをつける await sleep(waitTime)

このように定義すると、asyncFunc自体実行した結果result_${mes}を得るためにはawait する必要がある。ここが非常にややこしい。
メソッドの最後にreturnしているだけなのに、その時の返り値は文字列ではなくPromiseオブジェクトなのだ。これはもうJSのややこしくしている。

returnの結果を得るためには、呼び出し元でawait asyncFunc("message")awaitを頭につけてあげる必要がある。

async/awaitが入れ子で呼ばれた場合どういう動きをするのか

下記の例のようにawaitを実行するためにはasyncを使ったparentFuncを作成しその中で実行する必要がある。
この関数内で基本、基本2、基本3の動作を確認しどういうあたりが返ってくるか確認してもらいたい。
直感的この関数の一番最後の行実行時にそれ以前の命令した結果がすべて完了させるには基本3のように書けば良い。

要するに asyncで定義している関数の手前にawaitを書いてあげるだけである。

const parentFunc = async () => {
  // asyncを使っているメソッドはpromiseが返ってくるので、asyncを使って呼び出さないとawitが使えず非同期処理になる
  const async = asyncFunc
  const sync = asyncFunc

  // 基本
  console.log(async("async")) // Promiseオブジェクトが返ってくるだけ
  console.log("---1---") // 1
  console.log(await sync("sync")) // 2

  // 基本2
  console.log(await sync("sync2")) // 1
  console.log(async("async2")) // Promiseオブジェクトが返ってくるだけ
  console.log("---2---") // 2

  // 基本3
  const resultSync = await sync("sync3") // 1
  console.log(await sync(resultSync)) // 2 resultSyncの結果を待ってから実行する
  console.log("---3---") // 3
}

parentFunc() //ここで上で定義したparentFuncを実行

無名関数でasyncで定義した関数を実行する

parentFuncを定義せずに無名関数で実行することもできるが、わかりにくいカッコをたくさん書く必要があり書き方が直感的ではない。
おまじないと考えてもいいかもしれない。こういうのはあまり好きじゃないなぁ

(async () => {
  console.log(await asyncFunc("無名関数をasyncで定義して非同期実行するpromiseのメソッドを動かす"))
})()

非同期実行するもの複数実行してその結果を待つにはどうすればいいか

残念なことにせっかくasync/awaitで隠蔽化したPromiseをコードに書く必要がある。
そのキーワードはPromise.all。
感覚的にはスレッドの完了をすべて待つ処理に似ている。

const parentFunc = async () => {
  // asyncを使っているメソッドはpromiseが返ってくるので、asyncを使って呼び出さないとawitが使えず非同期処理になる
  const async = asyncFunc
  const sync = asyncFunc

  // map関数内で非同期処理が入る場合
  // idの配列を受け取って各idをパラメタにWeb apiにアクセスするような場合
  const arr = [1, 2, 3, 4, 5]

  // mapの中の各メソッドが実行完了する順番が変わっても結果の配列の順番はarrの順
  const result_arr1 = await Promise.all(arr.map((id)=>{
    return sync(id);
  }))
  console.log(result_arr1)
}

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

Nuxt.jsで、AxiosとVuexを使ったサーバ通信からデータ表示までの流れ

はじめに

今回はNuxt.jsでAxiosやVuexを使う方法について解説していきます。

Nuxt.jsでAxiosを使うには、@nuxtjs/axiosを用いることで、
サーバー側からデータを取得したり、更新したりすることができます。

またそのデータを今度はVuexを使ってフロント側で状態管理します。

そしてそのデータをヘルパー関数を使ってVuexから引っ張ってきて、
実際に画面に表示していきます。

簡単にまとめると、
①Axiosでサーバー側からデータ取得
②Vuexでデータの管理
③データを画面に表示
という流れです!

実際に手もとで試したいという方は、まずNuxtのプロジェクトを作成しましょう。

// npmを使う場合
npx create-nuxt-app プロジェクト名

// yarnを使う場合
yarn create nuxt-app プロジェクト名

※プロジェクト作成時の細かい設定については、他の記事をご参照ください。

では本題に入っていきます。

@nuxtjs/axiosのインストール・設定

まずは@nuxtjs/axiosをインストールします。

npm install --save @nuxtjs/axios

package.jsonに@nuxtjs.axiosが入っていたら、ちゃんとインストールされていることになります。

package.json
"dependencies": {
  "@nuxtjs/axios": "バージョン"
}

次にNuxt.js用のaxiosをの設定を行います。
nuxt.config.jsの、pluginsとmodulesに記述します。

nuxt.config.js
plugins: [
  '@/plugins/axios.js'
],
modules: [
  '@nuxtjs/axios'
]

Axiosを使ってデータを取得する

早速データ取得のコードを書いていきます。

データ取得はVuexでいうActiosで行いますが、Vuexについてはまた後ほど解説します。
storeのJSファイルに記述します。

{ store/index.js ]

index.js
export const actions = {
  async fetchData({ commit, state }) {
    try {
      const resp = await this.$axios.$get('ここにAPIのエンドポイントを入れます')
      commit('setData', resp)
    } catch (error) {
      console.log(error)
    }
  }
}

Actionsの中にfetchDataという関数を作り(名前は何でも良い)、
try/catch文の中でデータ取得を行っています。

this.$axios.$get('APIのエンドポイント')
でデータ取得が行えます。
また、データを更新したい時には$getの部分を$postにします。

resp(名前は何でも良い)という変数に取ってきたデータを格納し、
commitを使ってMutationsのsetDataという関数の方に渡しています。
(※Mutationsについては後ほど)

Vuexで状態管理

今度は、先ほどAxiosで取ってきたデータを、Vuexを使ってフロント側で管理していきます。

VuexにはState, Getters, Mutations, Actionsがありますが、
今回使うのは先程Axiosのところで出てきたActionsと、
これから出てくるStateMutationsです。
※Gettersは今回登場しません。

先程と同じstoreのJSファイルに追加で記述します。

[ store/index.js ]

index.js
export const state = () => ({
  data: null
})

export const mutations = {
  setData: (state, value) => {
    state.Data = value
  }
}

処理の順番としては、
先程Actionsのところで取得してきたデータが、
MutationsのsetData関数へ渡されています。

そしてさらに、そのデータをStateのdataに渡しています。

なので、Vuexの流れをもう一度まとめると、
①ActiosでAxiosを使ってサーバからデータ取得
②Actios→Mutationsにデータをコミット
③Mutations→Stateにデータをセット
という流れです!

実際にデータを画面に表示する

これまでで基盤は整いましたので、あとはデータを表示していきます。

と言いたいところですが、、
実はこれまでのコードだけでは実際にはデータが取得できていません!

先程Actionsで書いたAxiosのコードですが、
これはVueファイル側で呼び出さないと実行されません。

なので、その呼びだす処理を書いていきましょう。
※呼び出す処理を書く場所についてはいろいろなパターンがありますので、今回はあくまでも一例です。

pagesディレクトリのVueファイルに記述していきます。

[ pages/index.vue ]

index.vue
<script>
export default {
  fetch({ store }) {
    store.dispatch('index/fetchData')
  }
}
</script>

fetch内でstore.dispatchを使うことで、
index.js内のfetchData関数を実行することができます。

また、今回は実際にデータを表示する部分を
conponentsディレクトリ配下のVueファイル(今回はTop.vueというファイルを作ります)に記述するので、
Top.vueをインポートするコードを追記します。
以下全体のコードです。

[ pages/index.vue ]

index.vue
<template>
  <Top />
</template>

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

export default {
  components: {
    Top
  },
  fetch({ store }) {
    store.dispatch('index')
  }
}
</script>

では最後に、データを表示していきましょう!

cpmponentsディレクトリ配下のVueファイルに記述していきます。(今回はTop.vueを作成)

[ comopnents/Top.vue ]

Top.vue
<template>
  <div>{{ data }}</div>
</template>

<script>
import { createNamespacedHelpers } from 'vuex'
const { mapState } = createNamespacedHelpers('index')

export default {
  computed: {
    ...mapstate(['data'])
  }
}
</script>

まずはscriptの一行目
import { createNamespacedHelpers } from 'vuex'
でvuexをインポートしてきています。
また今回はcreateNamespacedHelpersを使用するということも記述しています。

その下の
const { mapState } = createNamespacedHelpers('index')
では、index.jsのStateからデータを引っ張ってこれるように設定しています。

そしてcomputedの中で
...mapstate(['data'])
を使ってVuexのStateからdataプロパティを引っ張ってきています。

最後に、templateタグ内で二重中括弧{{ }}を使って、
dataを参照することで画面に表示することができます!

最後に

今回の流れをおさらいすると、

①Axiosをインストールして設定する
②Axiosを使ってサーバーと通信し、データ取得処理の実装
③Vuex(State, Mutations, Actions)を使って状態管理を行う
 ・データの流れは、Actions→Mutations→Stateの順

④store.dispatchを使って②の処理を実行
⑤mapStateを使ってVuexのStateからデータを持ってくる
⑥画面に表示

という形で実装を行ってきました。

実装パターンはいくつかあると思いますが、
今回はあくまでも一例を紹介させていただきました。

ご参考になれば幸いです。

最後までお読みくださりありがとうございました。

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

Nuxt.jsでAxiosとVuexを使ってみよう

はじめに

今回はNuxt.jsでAxiosやVuexを使う方法について解説していきます。

Nuxt.jsでAxiosを使うには、@nuxtjs/axiosを用いることで、
サーバー側からデータを取得したり、更新したりすることができます。

またそのデータを今度はVuexを使ってフロント側で状態管理します。

そしてそのデータをヘルパー関数を使ってVuexから引っ張ってきて、
実際に画面に表示していきます。

簡単にまとめると、
①Axiosでサーバー側からデータ取得
②Vuexでデータの管理
③データを画面に表示
という流れです!

実際に手もとで試したいという方は、まずNuxtのプロジェクトを作成しましょう。

// npmを使う場合
npx create-nuxt-app プロジェクト名

// yarnを使う場合
yarn create nuxt-app プロジェクト名

※プロジェクト作成時の細かい設定については、他の記事をご参照ください。

では本題に入っていきます。

@nuxtjs/axiosのインストール・設定

まずは@nuxtjs/axiosをインストールします。

npm install --save @nuxtjs/axios

package.jsonに@nuxtjs.axiosが入っていたら、ちゃんとインストールされていることになります。

package.json
"dependencies": {
  "@nuxtjs/axios": "バージョン"
}

次にNuxt.js用のaxiosをの設定を行います。
nuxt.config.jsの、pluginsとmodulesに記述します。

nuxt.config.js
plugins: [
  '@/plugins/axios.js'
],
modules: [
  '@nuxtjs/axios'
]

Axiosを使ってデータを取得する

早速データ取得のコードを書いていきます。

今回、データ取得はVuexでいうActiosというところで行いますが、Vuexの詳細については後ほど解説します。
storeのJSファイルに記述します。

{ store/index.js ]

index.js
export const actions = {
  async fetchData({ commit, state }) {
    try {
      const resp = await this.$axios.$get('ここにAPIのエンドポイントを入れます')
      commit('setData', resp)
    } catch (error) {
      console.log(error)
    }
  }
}

Actionsの中にfetchDataという関数を作り(名前は何でも良い)、
try/catch文の中でデータ取得を行っています。

this.$axios.$get('APIのエンドポイント')
でデータ取得が行えます。
また、データを更新したい時には$getの部分を$postにします。

resp(名前は何でも良い)という変数に取ってきたデータを格納し、
commitを使ってMutationsのsetDataという関数の方に渡しています。
(※Mutationsについては後ほど)

Vuexで状態管理

今度は、先ほどAxiosで取ってきたデータを、Vuexを使ってフロント側で管理していきます。

VuexにはState, Getters, Mutations, Actionsがありますが、
今回使うのは先程Axiosのところで出てきたActionsと、
これから出てくるStateMutationsです。
※Gettersは今回登場しません。

先程と同じstoreのJSファイルに追加で記述します。

[ store/index.js ]

index.js
export const state = () => ({
  data: null
})

export const mutations = {
  setData: (state, value) => {
    state.Data = value
  }
}

処理の順番としては、
先程Actionsのところで取得してきたデータが、
MutationsのsetData関数へ渡されています。

そしてさらに、そのデータをStateのdataに渡しています。

なので、Vuexの流れをもう一度まとめると、
①ActiosでAxiosを使ってサーバからデータ取得
②Actios→Mutationsにデータをコミット
③Mutations→Stateにデータをセット
という流れです!

実際にデータを画面に表示する

これまでで基盤は整いましたので、あとはデータを表示していきます。

と言いたいところですが、、
実はこれまでのコードだけでは実際にはデータが取得できていません!

先程Actionsで書いたAxiosのコードですが、
これはVueファイル側で呼び出さないと実行されません。

なので、その呼びだす処理を書いていきましょう。
※呼び出す処理を書く場所についてはいろいろなパターンがありますので、今回はあくまでも一例です。

pagesディレクトリのVueファイルに記述していきます。

[ pages/index.vue ]

index.vue
<script>
export default {
  fetch({ store }) {
    store.dispatch('index/fetchData')
  }
}
</script>

fetch内でstore.dispatchを使うことで、
index.js内のfetchData関数を実行することができます。

また、今回は実際にデータを表示する部分を
conponentsディレクトリ配下のVueファイル(今回はTop.vueというファイルを作ります)に記述するので、
Top.vueをインポートするコードを追記します。
以下全体のコードです。

[ pages/index.vue ]

index.vue
<template>
  <Top />
</template>

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

export default {
  components: {
    Top
  },
  fetch({ store }) {
    store.dispatch('index')
  }
}
</script>

では最後に、データを表示していきましょう!

cpmponentsディレクトリ配下のVueファイルに記述していきます。(今回はTop.vueを作成)

[ comopnents/Top.vue ]

Top.vue
<template>
  <div>{{ data }}</div>
</template>

<script>
import { createNamespacedHelpers } from 'vuex'
const { mapState } = createNamespacedHelpers('index')

export default {
  computed: {
    ...mapstate(['data'])
  }
}
</script>

まずはscriptの一行目
import { createNamespacedHelpers } from 'vuex'
でvuexをインポートしてきています。
また今回はcreateNamespacedHelpersを使用するということも記述しています。

その下の
const { mapState } = createNamespacedHelpers('index')
では、index.jsのStateからデータを引っ張ってこれるように設定しています。

そしてcomputedの中で
...mapstate(['data'])
を使ってVuexのStateからdataプロパティを引っ張ってきています。

最後に、templateタグ内で二重中括弧{{ }}を使って、
dataを参照することで画面に表示することができます!

最後に

今回の流れをおさらいすると、

①Axiosをインストールして設定する
②Axiosを使ってサーバーと通信し、データ取得処理の実装
③Vuex(State, Mutations, Actions)を使って状態管理を行う
 ・データの流れは、Actions→Mutations→Stateの順

④store.dispatchを使って②の処理を実行
⑤mapStateを使ってVuexのStateからデータを持ってくる
⑥画面に表示

という形で実装を行ってきました。

実装パターンはいくつかあると思いますが、
今回はあくまでも一例を紹介させていただきました。

ご参考になれば幸いです。

最後までお読みくださりありがとうございました。

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

Nuxt.jsで、AxiosとVuexってどう使えば良いの?

はじめに

今回はNuxt.jsでAxiosやVuexを使って、サーバー通信からデータを表示するまでの流れについて解説します。

Nuxt.jsでAxiosを使うには、@nuxtjs/axiosを用いることで、
サーバー側からデータを取得したり、更新したりすることができます。

またそのデータを今度はVuexを使ってフロント側で状態管理します。

そしてそのデータをヘルパー関数を使ってVuexから引っ張ってきて、
実際に画面に表示していきます。

簡単にまとめると、
①Axiosでサーバー側からデータ取得
②Vuexでデータの管理
③データを画面に表示
という流れです!

実際に手もとで試したいという方は、まずNuxtのプロジェクトを作成しましょう。

// npmを使う場合
npx create-nuxt-app プロジェクト名

// yarnを使う場合
yarn create nuxt-app プロジェクト名

※プロジェクト作成時の細かい設定については、他の記事をご参照ください。
※また、今回解説する実装方法以外にも色々なパターンがあるかと思いますが、今回はあくまでも一例を紹介いたします。

では本題に入っていきます。

@nuxtjs/axiosのインストール・設定

まずは@nuxtjs/axiosをインストールします。

npm install --save @nuxtjs/axios

package.jsonに@nuxtjs.axiosが入っていたら、ちゃんとインストールされていることになります。

package.json
"dependencies": {
  "@nuxtjs/axios": "バージョン"
}

次にNuxt.js用のaxiosをの設定を行います。
nuxt.config.jsの、pluginsとmodulesに記述します。

nuxt.config.js
plugins: [
  '@/plugins/axios.js'
],
modules: [
  '@nuxtjs/axios'
]

Axiosを使ってデータを取得する

早速データ取得のコードを書いていきます。

今回、データ取得はVuexでいうActiosというところで行いますが、Vuexの詳細については後ほど解説します。
storeのJSファイルに記述します。

{ store/index.js ]

index.js
export const actions = {
  async fetchData({ commit, state }) {
    try {
      const resp = await this.$axios.$get('ここにAPIのエンドポイントを入れます')
      commit('setData', resp)
    } catch (error) {
      console.log(error)
    }
  }
}

Actionsの中にfetchDataという関数を作り(名前は何でも良い)、
try/catch文の中でデータ取得を行っています。

this.$axios.$get('APIのエンドポイント')
でデータ取得が行えます。
また、データを更新したい時には$getの部分を$postにします。

resp(名前は何でも良い)という変数に取ってきたデータを格納し、
commitを使ってMutationsのsetDataという関数の方に渡しています。
(※Mutationsについては後ほど)

Vuexで状態管理

今度は、先ほどAxiosで取ってきたデータを、Vuexを使ってフロント側で管理していきます。

VuexにはState, Getters, Mutations, Actionsがありますが、
今回使うのは先程Axiosのところで出てきたActionsと、
これから出てくるStateMutationsです。
※Gettersは今回登場しません。

先程と同じstoreのJSファイルに追加で記述します。

[ store/index.js ]

index.js
export const state = () => ({
  data: null
})

export const mutations = {
  setData: (state, value) => {
    state.Data = value
  }
}

処理の順番としては、
先程Actionsのところで取得してきたデータが、
MutationsのsetData関数へ渡されています。

そしてさらに、そのデータをStateのdataに渡しています。

なので、Vuexの流れをもう一度まとめると、
①ActiosでAxiosを使ってサーバからデータ取得
②Actios→Mutationsにデータをコミット
③Mutations→Stateにデータをセット
という流れです!

実際にデータを画面に表示する

これまでで基盤は整いましたので、あとはデータを表示していきます。

と言いたいところですが、、
実はこれまでのコードだけでは実際にはデータが取得できていません!

先程Actionsで書いたAxiosのコードですが、
これはVueファイル側で呼び出さないと実行されません。

なので、その呼びだす処理を書いていきましょう。
※呼び出す処理を書く場所についてはいろいろなパターンがありますので、今回はあくまでも一例です。

pagesディレクトリのVueファイルに記述していきます。

[ pages/index.vue ]

index.vue
<script>
export default {
  fetch({ store }) {
    store.dispatch('index/fetchData')
  }
}
</script>

fetch内でstore.dispatchを使うことで、
index.js内のfetchData関数を実行することができます。

また、今回は実際にデータを表示する部分を
conponentsディレクトリ配下のVueファイル(今回はTop.vueというファイルを作ります)に記述するので、
Top.vueをインポートするコードを追記します。
以下全体のコードです。

[ pages/index.vue ]

index.vue
<template>
  <Top />
</template>

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

export default {
  components: {
    Top
  },
  fetch({ store }) {
    store.dispatch('index')
  }
}
</script>

では最後に、データを表示していきましょう!

cpmponentsディレクトリ配下のVueファイルに記述していきます。(今回はTop.vueを作成)

[ comopnents/Top.vue ]

Top.vue
<template>
  <div>{{ data }}</div>
</template>

<script>
import { createNamespacedHelpers } from 'vuex'
const { mapState } = createNamespacedHelpers('index')

export default {
  computed: {
    ...mapstate(['data'])
  }
}
</script>

まずはscriptの一行目
import { createNamespacedHelpers } from 'vuex'
でvuexをインポートしてきています。
また今回はcreateNamespacedHelpersを使用するということも記述しています。

その下の
const { mapState } = createNamespacedHelpers('index')
では、index.jsのStateからデータを引っ張ってこれるように設定しています。

そしてcomputedの中で
...mapstate(['data'])
を使ってVuexのStateからdataプロパティを引っ張ってきています。

最後に、templateタグ内で二重中括弧{{ }}を使って、
dataを参照することで画面に表示することができます!

最後に

今回の流れをおさらいすると、

①Axiosをインストールして設定する
②Axiosを使ってサーバーと通信し、データ取得処理の実装
③Vuex(State, Mutations, Actions)を使って状態管理を行う
 ・データの流れは、Actions→Mutations→Stateの順

④store.dispatchを使って②の処理を実行
⑤mapStateを使ってVuexのStateからデータを持ってくる
⑥画面に表示

という形で実装を行ってきました。

実装パターンはいくつかあると思いますが、
今回はあくまでも一例を紹介させていただきました。

ご参考になれば幸いです。

最後までお読みくださりありがとうございました。

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

AxiosとVuexってどう使えば良いの?

はじめに

今回はNuxt.jsでAxiosやVuexを使って、サーバー通信からデータを表示するまでの流れを紹介します。

Nuxt.jsでAxiosを使うには、@nuxtjs/axiosを用いることで、
サーバー側からデータを取得したり、更新したりすることができます。

またそのデータを今度はVuexを使ってフロント側で状態管理します。

そしてそのデータをヘルパー関数を使ってVuexから引っ張ってきて、
実際に画面に表示していきます。

簡単にまとめると、
①Axiosでサーバー側からデータ取得
②Vuexでデータの管理
③データを画面に表示
という流れです!

実際に手もとで試したいという方は、まずNuxtのプロジェクトを作成しましょう。

// npmを使う場合
npx create-nuxt-app プロジェクト名

// yarnを使う場合
yarn create nuxt-app プロジェクト名

※プロジェクト作成時の細かい設定については、他の記事をご参照ください。
※また、今回解説する実装方法以外にも色々なパターンがあるかと思いますが、今回はあくまでも一例を紹介いたします。

では本題に入っていきます。

@nuxtjs/axiosのインストール・設定

まずは@nuxtjs/axiosをインストールします。

npm install --save @nuxtjs/axios

package.jsonに@nuxtjs.axiosが入っていたら、ちゃんとインストールされていることになります。

package.json
"dependencies": {
  "@nuxtjs/axios": "バージョン"
}

次にNuxt.js用のaxiosをの設定を行います。
nuxt.config.jsの、pluginsとmodulesに記述します。

nuxt.config.js
plugins: [
  '@/plugins/axios.js'
],
modules: [
  '@nuxtjs/axios'
]

Axiosを使ってデータを取得する

早速データ取得のコードを書いていきます。

今回、データ取得はVuexでいうActiosというところで行いますが、Vuexの詳細については後ほど解説します。
storeのJSファイルに記述します。

{ store/index.js ]

index.js
export const actions = {
  async fetchData({ commit, state }) {
    try {
      const resp = await this.$axios.$get('ここにAPIのエンドポイントを入れます')
      commit('setData', resp)
    } catch (error) {
      console.log(error)
    }
  }
}

Actionsの中にfetchDataという関数を作り(名前は何でも良い)、
try/catch文の中でデータ取得を行っています。

this.$axios.$get('APIのエンドポイント')
でデータ取得が行えます。
また、データを更新したい時には$getの部分を$postにします。

resp(名前は何でも良い)変数に取ってきたデータを格納し、
commitを使ってMutationsのsetDataという関数の方に渡しています。
(※Mutationsについては後ほど)

Vuexで状態管理

今度は、先ほどAxiosで取ってきたデータを、Vuexを使ってフロント側で管理していきます。

VuexにはState, Getters, Mutations, Actionsがありますが、
今回使うのは先程Axiosのところで出てきたActionsと、
これから出てくるStateMutationsです。
※Gettersは今回登場しません。

先程と同じstoreのJSファイルに追加で記述します。

[ store/index.js ]

index.js
export const state = () => ({
  data: null
})

export const mutations = {
  setData: (state, value) => {
    state.Data = value
  }
}

処理の順番としては、
先程Actionsのところで取得してきたデータが、
MutationsのsetData関数へ渡されています。

そしてさらに、そのデータをStateのdataに渡しています。

なので、Vuexの流れをもう一度まとめると、
①ActiosでAxiosを使ってサーバからデータ取得
②Actios→Mutationsにデータをコミット
③Mutations→Stateにデータをセット
という流れです!

実際にデータを画面に表示する

これまでで基盤は整いましたので、あとはデータを表示していきます。

と言いたいところですが、、
実はこれまでのコードだけでは実際にはデータが取得できていません!

先程Actionsで書いたAxiosのコードですが、
これはVueファイル側で呼び出さないと実行されません。

なので、その呼びだす処理を書いていきましょう。
※呼び出す処理を書く場所についてはいろいろなパターンがありますので、今回はあくまでも一例です。

pagesディレクトリのVueファイルに記述していきます。

[ pages/index.vue ]

index.vue
<script>
export default {
  fetch({ store }) {
    store.dispatch('index/fetchData')
  }
}
</script>

fetch内でstore.dispatchを使うことで、
index.js内のfetchData関数を実行することができます。

また、今回は実際にデータを表示する部分を
conponentsディレクトリ配下のVueファイル(今回はTop.vueというファイルを作ります)に記述するので、
Top.vueをインポートするコードを追記します。
以下全体のコードです。

[ pages/index.vue ]

index.vue
<template>
  <Top />
</template>

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

export default {
  components: {
    Top
  },
  fetch({ store }) {
    store.dispatch('index')
  }
}
</script>

では最後に、データを表示していきましょう!

cpmponentsディレクトリ配下のVueファイルに記述していきます。(今回はTop.vueを作成)

[ comopnents/Top.vue ]

Top.vue
<template>
  <div>{{ data }}</div>
</template>

<script>
import { createNamespacedHelpers } from 'vuex'
const { mapState } = createNamespacedHelpers('index')

export default {
  computed: {
    ...mapstate(['data'])
  }
}
</script>

まずはscriptの一行目
import { createNamespacedHelpers } from 'vuex'
でvuexをインポートしてきています。
また今回はcreateNamespacedHelpersを使用するということも記述しています。

その下の
const { mapState } = createNamespacedHelpers('index')
では、index.jsのStateからデータを引っ張ってこれるように設定しています。

そしてcomputedの中で
...mapstate(['data'])
を使ってVuexのStateからdataプロパティを引っ張ってきています。

最後に、templateタグ内で二重中括弧{{ }}を使って、
dataを参照することで画面に表示することができます!

最後に

今回の流れをおさらいすると、

①Axiosをインストールして設定する
②Axiosを使ってサーバーと通信し、データ取得処理の実装
③Vuex(State, Mutations, Actions)を使って状態管理を行う
 ・データの流れは、Actions→Mutations→Stateの順

④store.dispatchを使って②の処理を実行
⑤mapStateを使ってVuexのStateからデータを持ってくる
⑥画面に表示

という形で実装を行ってきました。

実装パターンはいくつかあると思いますが、
今回はあくまでも一例を紹介させていただきました。

ご参考になれば幸いです。

最後までお読みくださりありがとうございました。

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

AxiosとVuexって実際どう使えば良いの?

はじめに

今回はNuxt.jsでAxiosやVuexを使って、サーバー通信からデータを表示するまでの流れを紹介します。

Nuxt.jsでAxiosを使うには、@nuxtjs/axiosを用いることで、
サーバー側からデータを取得したり、更新したりすることができます。

またそのデータを今度はVuexを使ってフロント側で状態管理します。

そしてそのデータをヘルパー関数を使ってVuexから引っ張ってきて、
実際に画面に表示していきます。

簡単にまとめると、
①Axiosでサーバー側からデータ取得
②Vuexでデータの管理
③データを画面に表示
という流れです!

実際に手もとで試したいという方は、まずNuxtのプロジェクトを作成しましょう。

// npmを使う場合
npx create-nuxt-app プロジェクト名

// yarnを使う場合
yarn create nuxt-app プロジェクト名

※プロジェクト作成時の細かい設定については、他の記事をご参照ください。
※また、今回解説する実装方法以外にも色々なパターンがあるかと思いますが、今回はあくまでも一例を紹介いたします。

では本題に入っていきます。

@nuxtjs/axiosのインストール・設定

まずは@nuxtjs/axiosをインストールします。

npm install --save @nuxtjs/axios

package.jsonに@nuxtjs.axiosが入っていたら、ちゃんとインストールされていることになります。

package.json
"dependencies": {
  "@nuxtjs/axios": "バージョン"
}

次にNuxt.js用のaxiosをの設定を行います。
nuxt.config.jsの、pluginsとmodulesに記述します。

nuxt.config.js
plugins: [
  '@/plugins/axios.js'
],
modules: [
  '@nuxtjs/axios'
]

Axiosを使ってデータを取得する

早速データ取得のコードを書いていきます。

今回、データ取得はVuexでいうActiosというところで行いますが、Vuexの詳細については後ほど解説します。
storeのJSファイルに記述します。

{ store/index.js ]

index.js
export const actions = {
  async fetchData({ commit, state }) {
    try {
      const resp = await this.$axios.$get('ここにAPIのエンドポイントを入れます')
      commit('setData', resp)
    } catch (error) {
      console.log(error)
    }
  }
}

Actionsの中にfetchDataという関数を作り(名前は何でも良い)、
try/catch文の中でデータ取得を行っています。

this.$axios.$get('APIのエンドポイント')
でデータ取得が行えます。
また、データを更新したい時には$getの部分を$postにします。

resp(名前は何でも良い)変数に取ってきたデータを格納し、
commitを使ってMutationsのsetDataという関数の方に渡しています。
(※Mutationsについては後ほど)

Vuexで状態管理

今度は、先ほどAxiosで取ってきたデータを、Vuexを使ってフロント側で管理していきます。

VuexにはState, Getters, Mutations, Actionsがありますが、
今回使うのは先程Axiosのところで出てきたActionsと、
これから出てくるStateMutationsです。
※Gettersは今回登場しません。

先程と同じstoreのJSファイルに追加で記述します。

[ store/index.js ]

index.js
export const state = () => ({
  data: null
})

export const mutations = {
  setData: (state, value) => {
    state.Data = value
  }
}

処理の順番としては、
先程Actionsのところで取得してきたデータが、
MutationsのsetData関数へ渡されています。

そしてさらに、そのデータをStateのdataに渡しています。

なので、Vuexの流れをもう一度まとめると、
①ActiosでAxiosを使ってサーバからデータ取得
②Actios→Mutationsにデータをコミット
③Mutations→Stateにデータをセット
という流れです!

実際にデータを画面に表示する

これまでで基盤は整いましたので、あとはデータを表示していきます。

と言いたいところですが、、
実はこれまでのコードだけでは実際にはデータが取得できていません!

先程Actionsで書いたAxiosのコードですが、
これはVueファイル側で呼び出さないと実行されません。

なので、その呼びだす処理を書いていきましょう。
※呼び出す処理を書く場所についてはいろいろなパターンがありますので、今回はあくまでも一例です。

pagesディレクトリのVueファイルに記述していきます。

[ pages/index.vue ]

index.vue
<script>
export default {
  fetch({ store }) {
    store.dispatch('index/fetchData')
  }
}
</script>

fetch内でstore.dispatchを使うことで、
index.js内のfetchData関数を実行することができます。

また、今回は実際にデータを表示する部分を
conponentsディレクトリ配下のVueファイル(今回はTop.vueというファイルを作ります)に記述するので、
Top.vueをインポートするコードを追記します。
以下全体のコードです。

[ pages/index.vue ]

index.vue
<template>
  <Top />
</template>

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

export default {
  components: {
    Top
  },
  fetch({ store }) {
    store.dispatch('index')
  }
}
</script>

では最後に、データを表示していきましょう!

cpmponentsディレクトリ配下のVueファイルに記述していきます。(今回はTop.vueを作成)

[ comopnents/Top.vue ]

Top.vue
<template>
  <div>{{ data }}</div>
</template>

<script>
import { createNamespacedHelpers } from 'vuex'
const { mapState } = createNamespacedHelpers('index')

export default {
  computed: {
    ...mapstate(['data'])
  }
}
</script>

まずはscriptの一行目
import { createNamespacedHelpers } from 'vuex'
でvuexをインポートしてきています。
また今回はcreateNamespacedHelpersを使用するということも記述しています。

その下の
const { mapState } = createNamespacedHelpers('index')
では、index.jsのStateからデータを引っ張ってこれるように設定しています。

そしてcomputedの中で
...mapstate(['data'])
を使ってVuexのStateからdataプロパティを引っ張ってきています。

最後に、templateタグ内で二重中括弧{{ }}を使って、
dataを参照することで画面に表示することができます!

最後に

今回の流れをおさらいすると、

①Axiosをインストールして設定する
②Axiosを使ってサーバーと通信し、データ取得処理の実装
③Vuex(State, Mutations, Actions)を使って状態管理を行う
 ・データの流れは、Actions→Mutations→Stateの順

④store.dispatchを使って②の処理を実行
⑤mapStateを使ってVuexのStateからデータを持ってくる
⑥画面に表示

という形で実装を行ってきました。

実装パターンはいくつかあると思いますが、
今回はあくまでも一例を紹介させていただきました。

ご参考になれば幸いです。

最後までお読みくださりありがとうございました。

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

入力した値段を非同期的に表示したい

【概要】

1.結論

2.どのように記述するのか

3.ここから学んだこと

1.結論

javascript(jquery)で、inputイベントを使う!


2.どのように記述するのか

app/javascript/price.js
$(function(){
  $('#item-price').on('input', function(){
    let tax = $('#item-price').val();
    $('#tax-price').text(Math.ceil(tax * 0.1));
    $('#benefit').text(Math.ceil(tax - (tax * 0.1)));
  });
});

自分が書いたプログラムはこのようになります!

即時関数宣言をしています。
item-price/tax-price/benefit はHTMLの各表示部分の”id名”になります!価格が表示される部分と繋がっています。

inputイベントは値が変更された時に発火するもので、
item-priceに2000円と入力すればtax-price(200円)/benefit(1800円)と表示されるようになっています!

またtext(Math.ceil(tax * 0.1))の部分で、
textは'#tax-price'の中身を取得し、Math.ceilは小数点を切り上げています!

valも'#item-price'の中身を取得しています!

助けていただいたURL:
イベント: change, input, cut, copy, paste

.val()について


3.ここから学んだこと

最初はinputイベントの部分をchaengeイベントで使用していました。そうすると、changeイベントでは、入力したあとでページのどこかしらを一度クリックしないと金額変更が反映されないことに気づきました。なのでinputイベントに変更しました。ただ戻るボタンを押して以前入力した金額を残した状態で金額変更を反映させる部分についてはキャッシュも絡んでくるので、もっと勉強しておきます!

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

JavaScriptの配列を学ぶ演習・課題

配列を学ぶ意味は?

配列は複数のデータを扱うための仕組み。
SpreadSheetからデータを取得するとき等に複数データを扱う必要がでてくる。

本記事のゴール

配列から任意の値を取り出せるようになること。

演習1 配列の作成

とりあえず配列で動くコードを実装

const helloArray = () => {
    const values = [1, 2, 3, 4, 5]; // 配列の作成
    console.log(values[0]); // 配列の中の値の一つを取得
}

helloArray();

スクリーンショット 2020-09-05 17.42.18.png

ポイント

配列の作成

const 配列の変数名 = [配列の要素1, 配列の要素2, 配列の要素3]; 

[]でくくって、,で区切る。

配列の要素の取得

配列[何番目かを示す数]

今回のコードでは

values[0]

0番目を指定している。
プログラミング初心者の方は0番目って何?と思うかもしれない。

多くのプログラミング言語では配列の要素を取得するときには0番目から数える。
つまりこういうこと

スクリーンショット 2020-09-05 18.05.22.png

演習2 for文

とりあえず動くコードを書く。
まずは配列は使わない。

const helloFor = () => {
    for (let i = 0; i < 3; i++) {
        console.log("HelloFor " + i);
    }
}

helloFor();

スクリーンショット 2020-09-06 12.07.05.png

上記のような結果になる。

なぜこんな結果になるのか処理順序順に見ていく。
まずfor文の構文

for.js
for (最初に実行される処理; ループ続行条件; ループごとに実行される処理){
    繰り返す処理
}

日本語が多くて見づらい。多くの場合は形が決まってるので以下のような形で覚えると良い。

for.js
for(let i = 0; i < ループ数; i++){
    繰り返す処理
}

ここで変数iはindexの頭文字。iっていう変数名でなくても問題ない。

これを踏まえて演習2のコードを見ていく。
演習2のfor文を分解すると以下のようになる

let i = 0; // 最初に実行する処理
// --- ポイント1
if (i < 3){ // ループ続行条件
    console.log("HelloFor" + i);
    i++; // iに1足す. ループごとに実行される処理
    // ポイント1に戻る
}

最初i=0から始まり、ループごとに1足されていく。
iが3になったらループ続行条件を満たさなくなる。

つまり、iが0, 1, 2のときに中の処理が実行される。
したがって3回処理が呼ばれる。

課題1

0 ~ 10までの数字を出力してみよう

課題2

0 ~ 10までの数字の和を出力してみよう。
ヒント: for文の前にlet sum = 0;を定義する。

演習3 配列の中身を全て出力する。

演習2のforを配列と組み合わせることによって、配列の中身を全部出力することができる。

const logAll = () => {
    const values = ["一郎", "二郎", "三郎"];
    for (let i = 0; i < values.length; i++) {
        console.log(values[i]);
    }
}

logAll();

スクリーンショット 2020-09-06 12.32.13.png

ポイント

配列.lengthで配列の要素の数を返す。
この例題では3

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

【JavaScript】String配列の要素をキーとするオブジェクトを作成する方法

はじめに

ループしながら、配列内の要素をキーとするオブジェクトを作成する方法を紹介します。

方法

const array = ['key1', 'key2', 'key3',]


const createObj = (array) => {
    const obj = new Object();
    array.forEach(item => (Object.defineProperty(obj, item, {
      enumerable: true, // ループのために必要!
      value: 'ここに値'
    })));
    return obj;
}

console.log(createObj(array));
// {key1: 'ここに値', key2: 'ここに値', key3: 'ここに値'}

オプションとして設定する、enumerable: trueが重要です。
これがないと動きません。

まとめ

配列内の要素をキーとするオブジェクトを作成することができました。

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

NCMB(Nifty Cloud Mobile Backend)データストアの基本操作チートシート(JavaScript SDK)

自分なりにまとめてみました。足りないものは随時更新。

使用するための準備

SDKの入手やセットアップは下記リンクを参考に。
https://mbaas.nifcloud.com/doc/current/introduction/sdkdownload_javascript.html#gsc.tab=0

var ncmb = new NCMB("ApplicationKey", "ClientKey");

レコードの取得(Select)

1レコードだけの取得の場合

let object = ncmb.DataStore("DataStoreClassName");

object.equalTo("objectId", value)
      .fetch()
      .then(function(result){
          // 取得時処理
      }).catch(function(err){
          // 取得エラー処理
      });

複数レコード取得の場合

let objects = ncmb.DataStore("DataStoreClassName");

objects.equalTo("fieldName", value)
       .fetchAll()
       .then(function(result){
           // 取得時処理
       }).catch(function(err){
           // 取得エラー処理
       });

件数取得(結果の総数を取得)

let objects = ncmb.DataStore("DataStoreClassName");

objects.equalTo("fieldName", value)
       .count()
       .fetchAll()
       .then(function(result){
           // 取得時処理
       }).catch(function(err){
           // 取得エラー処理
       });

主に検索で使用するクエリ(その他は共通クエリオペレータを参照)

// 取得最大件数の指定(1~1,000件の範囲での指定のみ)
.limit(limitValue)

// 検索結果の開始位置指定(0番目からカウントしてvalue番目以降を取得)
.skip(value)

// 昇順ソート
.order("fieldName")
.order("fieldName", false)

// 降順ソート
.order("fieldName", true)

// 複数ソート ※先に指定したキーが優先
.order("fieldName1", true).order("fieldName2")

レコードの挿入(Insert)

各fieldNameとvalueをセットして挿入。

let object = new ( ncmb.DataStore("DataStoreClassName") );

object.set("fieldName", value)
      .fetch()
      .then(function(insertObject){
          insertObject.set("fieldName", value)
                      .save()
                      .then(function(result){
                          // 挿入完了時処理
                      }).catch(function(err){
                          // 挿入エラー処理
                      });
      });

レコードの更新(Update)

objectIdが一致するレコードに対し、セットしたフィールドを更新。

let object = ncmb.DataStore("DataStoreClassName");

object.equalTo("objectId", objectId)
      .fetch()
      .then(function(updateObject){
          updateObject.set("fieldName", value)
                      .update()
                      .then(function(result){
                        // 更新完了時処理
                      }).catch(function(err){
                        // 更新エラー処理
                      });
      });

レコードの削除(Delete)

objectIdが一致するレコードを削除。

let object = ncmb.DataStore("DataStoreClassName");

object.equalTo("objectId", objectId)
      .fetch()
      .then(function(deleteObject){
          deleteObject.delete()
                      .then(function(result){
                          // 削除完了時処理
                      }).catch(function(err){
                          // 削除エラー処理
                      });
      });

共通クエリオペレータ

文字列・数値・真偽値等で使用

// fieldValue === value
.equalTo("fieldName", value)

// fieldValue !== value
.notEqualTo("fieldName", value)

// fieldValue > value
.greaterThan("fieldName", value)

// fieldValue >= value
.greaterThanOrEqualTo("fieldName", value)

// fieldValue < value
.lessThan("fieldName", value)

// fieldValue <= value
.lessThanOrEqualTo("fieldName", value)


// 部分一致 ( fieldValue LIKE %value% )
.regularExpressionTo("fieldName", ".*" + value + ".*$")


// (fieldValue === value1) || (fieldValue === value2) || (fieldValue === value3)
.in("fieldName", [value1, value2, value3])

// (fieldValue !== value1) && (fieldValue !== value2) && (fieldValue !== value3)
.notIn("fieldName", [value1, value2, value3])

// fieldValue != null
.exists("colName")

配列で使用

// value のいずれかと一致
.equalTo("arrayFieldName", value)

// arrayValue の値の内、いずれかを含む
.inArray("arrayFieldName", arrayValue)

// arrayValue の値の内、いずれとも一致しない
.notInArray("arrayFieldName", arrayValue)

// arrayValue の値の内、全てが一致
.allInArray("arrayFieldName", arrayValue)

クエリの合成

AND合成

各メソッドを続けて記述すればOK。

let object= ncmb.DataStore("DataStoreClassName");
object.lessThan("fieldName1", value1)
      .equalTo("fieldName2", value2)
      .fetchAll()
      .then(function(results){
          // 取得時処理
      }).catch(function(err){
          // エラー時処理
      });

OR合成

orメソッドで合成。ただしorメソッドを複数使用して複雑なクエリを作ることはできないので注意。

let object= ncmb.DataStore("DataStoreClassName");
let query1 = object.lessThan("fieldName1", value1);
let query2 = object.equalTo("fieldName2", value2);
let query = [ query1 , query2 ];

object.or([])
      .fetchAll()
      .then(function(results){
          // 取得時処理
      }).catch(function(err){
          // エラー時処理
      });

おまけ

自動でセットされるcreateDate/updateDate(timestamp)をDateに変換

let objects = ncmb.DataStore("DataStoreClassName");
objects.equalTo("fieldName", value)
       .fetchAll()
       .then(function(result){
           for(let i=0; i < result.length; i++){
               let date = new Date( result[i].createDate );
               console.log(date);
           }
       }).catch(function(err){
           // 取得エラー処理
       });
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【PHP/JavaScript】配列に大きい添字を指定して値を代入したときのサイズ

動機

 Webアプリを開発する際、ユーザーID(整数)をインデックスにした配列を作ったときに、無駄に大きい配列になるのでは??

結論

 PHPでは配列、JavaScriptではMapを使用すると、サイズが無駄に大きくならない。

PHPの場合

 大きい添字を指定しても、問題はなさそう。

$array = [];
$array[100] = 'hoge';

echo count($array);    // 1

JavaScriptの場合

 大きい添字を指定すると、あいているところにemptyが挿入された配列になった。

let array = [];
array[100] = 'hoge';

console.log(array);         // [empty x 100, "hoge"]
console.log(array.length);  // 101

Mapを使用すると、PHPの配列と似たような振る舞いになった。

let map = new Map([[100, 'hoge']]);

console.log(map);           // Map(1) {100 => "hoge"}
console.log(map.size);      // 1

上限は?

PHPの場合

 整数の上限は、プラットフォームに依存しており、約20億 or 約900京となる。(参考
 自分の環境では900京(9 * 10^18)まで使用できた。

$array = [];
$n = 9 * 10 ** 18;
$array[$n] = 'hoge';

echo count($array);    // 1

 900京のユーザーがいても、ちゃんと違うユーザーであると識別できるらしい。

$n = 9 * 10 ** 18;
$m = $n - 1;
var_dump($n == $m);    // false

JavaScriptの場合

 安全な整数値の上限(2^53 - 1)があり、Number.MAX_SAFE_INTEGERで利用できる(参考
 PHPよりは上限が小さい(~9.0 * 10^15)
 Mapを使用する場合は、この上限値以内であれば、問題なさそう。

let n = Number.MAX_SAFE_INTEGER;
let map = new Map([[n - 1, 'hoge']]);

console.log(map);               // Map(1) {9007199254740990 => "hoge"}
console.log(map.size);          // 1

let m = n - 1;
console.log(n === m);           // false
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

配列に大きい添字を指定して値を代入したときのサイズ

動機

 Webアプリを開発する際、ユーザーID(整数)をインデックスにした配列を作ったときに、無駄に大きい配列になるのでは??

結論

 PHPでは配列、JavaScriptではMapを使用すると、サイズが無駄に大きくならない。

PHPの場合

 大きい添字を指定しても、問題はなさそう。

$array = [];
$array[100] = 'hoge';

echo count($array);    // 1

JavaScriptの場合

 大きい添字を指定すると、あいているところにemptyが挿入された配列になった。

let array = [];
array[100] = 'hoge';

console.log(array);         // [empty x 100, "hoge"]
console.log(array.length);  // 101

Mapを使用すると、PHPの配列と似たような振る舞いになった。

let map = new Map([[100, 'hoge']]);

console.log(map);           // Map(1) {100 => "hoge"}
console.log(map.size);      // 1

上限は?

PHPの場合

 整数の上限は、プラットフォームに依存しており、約20億 or 約900京となる。(参考
 自分の環境では900京(9 * 10^18)まで使用できた。

$array = [];
$n = 9 * 10 ** 18;
$array[$n] = 'hoge';

echo count($array);    // 1

 900京のユーザーがいても、ちゃんと違うユーザーであると識別できるらしい。

$n = 9 * 10 ** 18;
$m = $n - 1;
var_dump($n == $m);    // false

JavaScriptの場合

 安全な整数値の上限(2^53 - 1)があり、Number.MAX_SAFE_INTEGERで利用できる(参考
 PHPよりは上限が小さい(~9.0 * 10^15)
 Mapを使用する場合は、この上限値以内であれば、問題なさそう。

let n = Number.MAX_SAFE_INTEGER;
let map = new Map([[n, 'hoge']]);

console.log(map);               // Map(1) {9007199254740991 => "hoge"}
console.log(map.size);          // 1

let m = n - 1;
console.log(n === m);           // false
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでスマホアプリを作る7つ方法

・ヮ・)あ、おはようございまーす

JavaScript でスマートフォンアプリを作る方法を調べる機会があったのでまとめました!

細かいアーキテクチャに関しては割愛します:sob:

ネイティブアプリ

1. React Native 89.9k

React でネイティブアプリを開発できるフレームワーク
JavaScript でアプリ開発といえばこれですね!

Expo という React Native での開発をサポートするプラットホームを使うと
ネイティブ機能を使うためのプラグインに制限がかかりますが
簡単に実機で確認したりAppleの審査なしにアプリをアップデートできるらしいです

Playground(expo)

2 Vue Native 7.4k

Vue記法で書ける React Native です

ビルドするときに .vue ファイルを React に変換するプラグインのようなもので
実質 React Native といっても過言ではないです

Vue Native と React Native のコードを混在させたり
React Native 用のライブラリを使うこともできます

3 NativeScript 19k

Angular、Vue、JavaScript(TypeScript) でネイティブアプリを開発できるフレームワークです

Svelte Native というプラグインで Svelte で開発することもできるみたいです

Playground

4 Weex 13.8k

Vue でネイティブアプリを開発できるフレームワークです

UIコンポーネントが若干HTMLライクなのが特徴
他のフレームワークと比べてプラグインが少ない印象

Playground

WebViewアプリ

ハイブリッドアプリ(WebViewアプリ)と呼ばれるもので
WebView(簡易ブラウザのようなもの)を使い、アプリ内外に仕込ませたWEBページを表示するものです
HTML、CSS、JavaScript といったWEB開発と同じ技術で開発できます

Android と iOS で全く同じUIを作れるのも特徴です

5. Cordova

WebView ではカメラなどの JavaScript の API は使用できず専用のプラグインを使う必要があります
しかし、ネイティブ機能へアクセスするためのプラグインが古くなっている印象があります

ionicMonacaPhoneGap といった開発プラットホームを使うことが一般的なようです

6. Capacitor

Cordova よりスマートにネイティブのコードを実行できるらしいです
Cordova のプラグインに加え、自分で書いたネイティブのコードを簡単に使うこともできます

ionic という開発プラットホームを使うことが一般的なようです

7. PWA

番外になりますが PWA も紹介しておきます

PWA はWebブラウザから直接インストールできるアプリケーションです
通常のWEB開発と同じで HTML、CSS、JavaScript で開発します

Qiitaのこのページを「ホーム画面」に追加してみると PWA を体験できます

ネイティブアプリと PWA の比較は こちら が参考になります

まとめ

ネイティブ特有のリッチなUIや速度を求めるならネイティブアプリ

  • React Native が圧倒的人気
  • Vueが好きな方は、何かあったら安心の React Native へすぐシフトできる Vue Native が良さそう
  • Angular、Svelteで開発したいなら NativeScript

HTML、CSS、JavaScript のWEB開発技術を使いたいなら WebViewアプリ or PWA

  • Webアプリケーションとスマホアプリ同時に作りたいなら PWA
  • PWAでは足りないネイティブ機能を使いたいなら WebViewアプリ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gatsby入門 チュートリアルをこなす 7. プログラムでデータからページを作成する

これまでの作業履歴

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

チュートリアル

今回実施するgatsbyのチュートリアルはこちら
https://www.gatsbyjs.com/tutorial/part-seven/
チュートリアルの冒頭にこう書かれています。

Reactコンポーネントをsrc / pagesに配置することで、引き続きページを作成できます。ただし、プログラムからデータからページを作成する方法を学習します。

※google翻訳です。
早速やっていきましょう。
ソースは前回作ったやつを使用します。

Programmatically create pages from data

Creating slugs for pages

マークダウンページを作成するには、onCreateNodeとcreatePagesの2つのGatsby APIを使用します。
gatsby-node.jsを作成し、以下を記述します。

gatsby-node.js
exports.onCreateNode = ({ node }) => {
  console.log(`Node created of type "${node.internal.type}"`)
}

チュートリアルはこのように記載されています。

このonCreateNode関数は、新しいノードが作成(または更新)されるたびにGatsbyによって呼び出されます。

なるほど。
gatsby開発サーバを再起動します。
起動はgatsby developでできます。
2020-09-06_02h21_53.jpg
なんかログがいっぱい出てその中にマークダウンの文字が。
gatsby-node.jsを修正します。

gatsby-node.js
exports.onCreateNode = ({ node }) => {
  ↓ここから
  if (node.internal.type === `MarkdownRemark`) {
    console.log(node.internal.type)
  }
  ↑ここまで修正
}

マークダウンだけログだしするのね。
再起動
2020-09-06_02h26_55.jpg
出てる出てる
マークダウンファイルのファイル名からURLのパスを作っていきましょう。
。。gatsbyだとパスをslugっていうのか。
gatsby-node.jsを修正

gatsby-node.js
exports.onCreateNode = ({ node, getNode }) => {←ここ修正
  if (node.internal.type === `MarkdownRemark`) {
    const fileNode = getNode(node.parent)←ここ修正
    console.log(`\n`, fileNode.relativePath)←ここ修正
  }
}

再起動
2020-09-06_02h32_44.jpg
これか!
ここからgatsby-source-filesystem pluginを使用
gatsby-node.jsを修正

gatsby-node.js
const { createFilePath } = require(`gatsby-source-filesystem`)←ここ追記

exports.onCreateNode = ({ node, getNode }) => {
  if (node.internal.type === `MarkdownRemark`) {
    console.log(createFilePath({ node, getNode, basePath: `pages` }))←ここ修正
  }
}

再起動
2020-09-06_02h36_42.jpg
出来てる!
ここからcreateNodeFieldを使用してslug(パス)を作ります。
gatsby-node.jsを修正

gatsby-node.js
const { createFilePath } = require(`gatsby-source-filesystem`)
exports.onCreateNode = ({ node, getNode, actions }) => {←ここ修正
  const { createNodeField } = actions←ここ修正
  if (node.internal.type === `MarkdownRemark`) {
    ↓ここから
    const slug = createFilePath({ node, getNode, basePath: `pages` })
    createNodeField({
      node,
      name: `slug`,
      value: slug,
    })
    ↑ここまで追記
  }
}

再起動
GraphiQLで以下を実行

query MyQuery {
  allMarkdownRemark {
    edges {
      node {
        fields {
          slug
        }
      }
    }
  }
}

2020-09-06_02h45_04.jpg
キタコレ!

Creating pages

ページ作りだ!
gatsby-node.jsを修正

gatsby-node.js
const { createFilePath } = require(`gatsby-source-filesystem`)

exports.onCreateNode = ({ node, getNode, actions }) => {
  const { createNodeField } = actions
  if (node.internal.type === `MarkdownRemark`) {
    const slug = createFilePath({ node, getNode, basePath: `pages` })
    createNodeField({
      node,
      name: `slug`,
      value: slug,
    })
  }
}

↓ここから
exports.createPages = async ({ graphql, actions }) => {
  // **Note:** The graphql function call returns a Promise
  // see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise for more info
  const result = await graphql(`
    query {
      allMarkdownRemark {
        edges {
          node {
            fields {
              slug
            }
          }
        }
      }
    }
  `)
  console.log(JSON.stringify(result, null, 4))
}
↑ここまで追記

この時点でどうなるのかさっぱりわからんが続けよう。
src/templatesディレクトリを作成
src/templates/blog-post.jsを作成し以下を記述

blog-post.js
import React from "react"
import Layout from "../components/layout"

export default function BlogPost() {
  return (
    <Layout>
      <div>Hello blog post</div>
    </Layout>
  )
}

gatsby-node.jsを修正

gatsby-node.js
const path = require(`path`)←ここ追記
const { createFilePath } = require(`gatsby-source-filesystem`)

exports.onCreateNode = ({ node, getNode, actions }) => {
  const { createNodeField } = actions
  if (node.internal.type === `MarkdownRemark`) {
    const slug = createFilePath({ node, getNode, basePath: `pages` })
    createNodeField({
      node,
      name: `slug`,
      value: slug,
    })
  }
}

exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions←ここ追記
  const result = await graphql(`
    query {
      allMarkdownRemark {
        edges {
          node {
            fields {
              slug
            }
          }
        }
      }
    }
  `)
  ↓ここから
  result.data.allMarkdownRemark.edges.forEach(({ node }) => {
    createPage({
      path: node.fields.slug,
      component: path.resolve(`./src/templates/blog-post.js`),
      context: {
        // Data passed to context is available
        // in page queries as GraphQL variables.
        slug: node.fields.slug,
      },
    })
  })
  ↑ここまで追記
}

再起動
以下でアクセス
http://localhost:8000/sdf
※sdfは何でもいい
2020-09-06_03h00_20.jpg
あ、パスが出来てる
アクセス
2020-09-06_03h01_14.jpg
マークダウンファイルはまだ出てこない。
なるほど、ちょっとわかったぞ。
gatsby-node.jsのonCreateNodeでマークダウンのファイル名を取得してslug(パス)を作成。
createPagesでそのslugを取得し、繰り返し処理の中でパスとsrc/templates/blog-post.jsを同期させるのね!
つづき!
src/templates/blog-post.jsの修正

src/templates/blog-post.js
import React from "react"
import { graphql } from "gatsby"
import Layout from "../components/layout"

export default function BlogPost({ data }) {
  const post = data.markdownRemark
  return (
    <Layout>
      <div>
        <h1>{post.frontmatter.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: post.html }} />
      </div>
    </Layout>
  )
}

export const query = graphql`
  query($slug: String!) {
    markdownRemark(fields: { slug: { eq: $slug } }) {
      html
      frontmatter {
        title
      }
    }
  }
`

うんうん。graphqlでマークダウンのhtml情報を取得してsrc/templates/blog-post.jsに書き込む感じか。
2020-09-06_03h15_57.jpg
出来た!
そしてsrc/pages/index.jsを修正

src/pages/index.js
import React from "react"
import { css } from "@emotion/core"
import { Link, graphql } from "gatsby"←ここ修正
import { rhythm } from "../utils/typography"
import Layout from "../components/layout"

export default function Home({ data }) {
  return (
    <Layout>
      <div>
        <h1
          css={css`
            display: inline-block;
            border-bottom: 1px solid;
          `}
        >
          Amazing Pandas Eating Things
        </h1>
        <h4>{data.allMarkdownRemark.totalCount} Posts</h4>
        {data.allMarkdownRemark.edges.map(({ node }) => (
          <div key={node.id}>
            ↓ここから
            <Link
              to={node.fields.slug}
              css={css`
                text-decoration: none;
                color: inherit;
              `}
            >
            ↑ここまで修正
              <h3
                css={css`
                  margin-bottom: ${rhythm(1 / 4)};
                `}
              >
                {node.frontmatter.title}{" "}
                <span
                  css={css`
                    color: #555;
                  `}
                >
                   {node.frontmatter.date}
                </span>
              </h3>
              <p>{node.excerpt}</p>
            </Link>←ここ修          </div>
        ))}
      </div>
    </Layout>
  )
}

export const query = graphql`
  query {
    allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
      totalCount
      edges {
        node {
          id
          frontmatter {
            title
            date(formatString: "DD MMMM, YYYY")
          }
          ↓ここから
          fields {
            slug
          }
          ↑ここまで修正
          excerpt
        }
      }
    }
  }
`

再起動
index.jsからpostにアクセスできるようになった!
チュートリアルはここまでかな。

これ、src/pages内にmdファイルあるのが気になるから移動できないかしら?
src/mdディレクトリ作成し、mdファイルを移動
2020-09-06_03h26_25.jpg
再起動
2020-09-06_03h27_21.jpg
うん、問題ないね!
今回はここまで。

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

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

YouTubeチャンネルにアップロードされた動画を埋め込みHTML形式で取得してみた

はじめに

YouTube Data API を使って、YouTubeチャンネルにアップロードされた動画を埋め込みHTML形式で取得してみました。

JavaScriptのYoutube用ライブラリもいくつかあるみたいですが、
今回はURLを直打ちする形で実装しました。

GET URLやJSONの仕様は下記を参照しました。
https://developers.google.com/youtube/v3/docs

環境は下記になります。
macOS Catalina 10.15.6
Node.js v14.8.0
(Node.js Library)request 6.14.7

コードと実行結果

コードは下記になります。

youtube.js
const request = require('request');

const URL_YOUTUBE_API='https://www.googleapis.com/youtube/v3/';
const YOUTUBE_APP_KEY=process.env.YOTUBE_APP_KEY;
const MY_YOUTUBE_CHANNEL_ID=process.env.YOUTUBE_CHANNEL_ID;
const URL_YOUTUBE_CHANNELS=URL_YOUTUBE_API + 'channels?key=' + YOUTUBE_APP_KEY + '&part=snippet,statistics,contentDetails&id=' + MY_YOUTUBE_CHANNEL_ID;
const URL_YOUTUBE_PLAYLISTITEMS=URL_YOUTUBE_API + 'playlistItems?key=' + YOUTUBE_APP_KEY + '&part=snippet,contentDetails&playlistId=';
const URL_YOUTUBE_VIDEOS=URL_YOUTUBE_API + 'videos?key=' + YOUTUBE_APP_KEY + '&part=player&id=';

var get_yotube_options_base = {
    url: '',
    method: 'GET',
    json: true
};

var html_data = [];

var get_youtube_options_1 = get_yotube_options_base;
get_youtube_options_1.url = URL_YOUTUBE_CHANNELS;
request(get_youtube_options_1, function (error, response, ch_info) {

    var playlistId = ch_info.items[0].contentDetails.relatedPlaylists.uploads;
    var get_youtube_options_2 = get_yotube_options_base;
    get_youtube_options_2.url = URL_YOUTUBE_PLAYLISTITEMS + playlistId;
    request(get_youtube_options_2, function (error, response, playlist_info) {
        var videoId_list = [];
        playlist_info.items.forEach(item => {
            videoId_list.push(item.snippet.resourceId.videoId);
        });
        var get_youtube_options_3 = get_yotube_options_base;
        get_youtube_options_3.url = URL_YOUTUBE_VIDEOS + videoId_list.join(',');
        request(get_youtube_options_3, function (error, response, video_info) {
            video_info.items.forEach(item => {
                html_data.push(item.player.embedHtml);
            });
            console.log(html_data);
        });
    });
});

実行結果は下記になります。
「XXXXXXXXXXXXX」の部分は自分の情報に書き換えてください。

% export YOTUBE_APP_KEY=XXXXXXXXXXXXX
% export YOUTUBE_CHANNEL_ID=XXXXXXXXXXXXX
% node youtube.js
[
  '<iframe width="480" height="270" src="//www.youtube.com/embed/uRML4cHJKsE" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>',
  '<iframe width="480" height="270" src="//www.youtube.com/embed/BuHrzydYE2s" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>'
]

コードの説明

YouTubeに対して3回HTTPリクエストをする形になってます。

(1回目) チャンネルIDから再生リストIDを取得 => URL:「URL_YOUTUBE_CHANNELS」
(2回目) 再生リストIDから動画IDを取得 => URL:「URL_YOUTUBE_PLAYLISTITEMS」
(3回目) 動画IDから埋め込みHTMLを取得 => URL:「URL_YOUTUBE_VIDEOS」

事前準備が必要です。
・App Keyについては、
「Google APIs Console」というサイトで発行しておきます。

・YouTube Channel IDはIDを取得したいチャンネルのページに行って、ソースを表示=>「CHANNEL_ID」で文字列検索すると出て来ます。

下記の画像で言うと「UCD0it6kq_2HBn3uMACiX_3A」の部分です。
スクリーンショット 2020-09-06 0.55.16.png

以上です。

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