20190915のJavaScriptに関する記事は26件です。

送信フォームで二重送信が起きる場合に疑う場所

比較的レアな状況だと思うが2度ほど同じ過ちに出会ったので記録。

フォームを送信する場合になぜかバリデーションが誤作動した。
原因を辿ると以下のようなコードが。

<body>
    <form action="testresult.php" method="POST" name="testform">
    <input type="text" name="test1">
    <input type="submit" onclick="submit_form()">
</body>
<script>
    function submit_form(){
        document.testform.submit();
    }
</script>

testresult.php にフォームを送信したくて、そのためにinput type="submit"としている。
その時点でフォームは送信できているが、更にJavaScriptでもsubmitする関数をonClickで呼び出してしまっている。結果的にこのフォームは二重で送信されてしまう。
これを防ぐには、input type="submit"input type="button"に変えてしまうか、JavaScriptの関数を削除する。後者の方がすっきりする。

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

送信フォームで二重送信が起きる場合には

比較的レアな状況だと思うが2度ほど同じ過ちに出会ったので記録。

フォームを送信する場合になぜかバリデーションが誤作動した。
原因を辿ると以下のようなコードが。

<body>
    <form action="testresult.php" method="POST" name="testform">
    <input type="text" name="test1">
    <input type="submit" onclick="submit_form()">
</body>
<script>
    function submit_form(){
        document.testform.submit();
    }
</script>

testresult.php にフォームを送信したくて、そのためにinput type="submit"としている。
その時点でフォームは送信できているが、更にJavaScriptでもsubmitする関数をonClickで呼び出してしまっている。結果的にこのフォームは二重で送信されてしまう。
これを防ぐには、input type="submit"input type="button"に変えてしまうか、JavaScriptの関数を削除する。後者の方がすっきりする。

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

WebアプリSHISHOWを開発したときの備忘録④firestore設定編

Firestoreの設定

前回でfirebaseの基本的な設定を終えたので,今回はfirestoreに関する設定を行っていきます。

まず,`~/plugins/firestore.jsに追加で書き込みをします。

firestore.js
import firebase from 'firebase';

// Your web app's Firebase configuration
var firebaseConfig = {
    apiKey: "自分のAPIキー",
    authDomain: "project1-aae93.firebaseapp.com",
    databaseURL: "https://project1-aae93.firebaseio.com",
    projectId: "project1-aae93",
    storageBucket: "project1-aae93.appspot.com",
    messagingSenderId: "598499664640",
    appId: "1:598499664640:web:fdeefdfb150f0d823009e5"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);

export default firebase;

firestore.jsをあとで違うファイルから読み込みたかったので,最後にexport default firebaseを追加しました。

そして,次は~/pages/index.vueに移動してfirestoreに関する記述を書いていきます。

index.vue
<template>
  <div class="container">
    <div>
      <logo />
      <h1 class="title">
        shishow
      </h1>
      <h2 class="subtitle">
        My legendary Nuxt.js project
      </h2>
      <div class="links">
        <a
          href="https://nuxtjs.org/"
          target="_blank"
          class="button--green"
        >
          Documentation
        </a>
        <a
          href="https://github.com/nuxt/nuxt.js"
          target="_blank"
          class="button--grey"
        >
          GitHub
        </a>
      </div>
    </div>
  </div>
</template>

<script>
import Logo from '~/components/Logo.vue'

import firebase from '~/plugins/firestore';
import firestore from 'firebase/firestore';

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

<style>
.container {
  margin: 0 auto;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}

.title {
  font-family: 'Quicksand', 'Source Sans Pro', -apple-system, BlinkMacSystemFont,
    'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
  display: block;
  font-weight: 300;
  font-size: 100px;
  color: #35495e;
  letter-spacing: 1px;
}

.subtitle {
  font-weight: 300;
  font-size: 42px;
  color: #526488;
  word-spacing: 5px;
  padding-bottom: 15px;
}

.links {
  padding-top: 15px;
}
</style>

import firestore from 'firebase/firestore'
という記述を加えただけですが,これでfirestoreが使えるようになっているはずです。

実際に記述してみる

今のindex.vueの状態は

こんな感じですが,
このshishowという文字をクリックしたらfirestoreに"shishow"という文字列が"SHISHOW"コレクションの中の自動生成されるドキュメントの中に登録してみましょう。

まずは,クリックイベントを発生させます。

index.vue
<h1 class="title" @click="addSHISHOW()">
  shishow
</h1>

これで文字をクリックした時にaddSHISHOW()関数が呼び出されるようになりました。

次は,クリックイベントで呼び出されるaddSHISHOW関数を作成しましょう。

index.vue
<script>
import Logo from '~/components/Logo.vue'

import firebase from '~/plugins/firestore';
import firestore from "firebase/firestore";

export default {
  components: {
    Logo
  },

  methods: {
    addSHISHOW: function() {
      console.log('shishow');
    }
  }
}
</script>

これで,Webコンソールにshishowと表示されたらクリックイベントの呼び出しには成功しているので,次はfirestoreの記述を書いていきましょう。

index.vue
<script>
import Logo from '~/components/Logo.vue'

import firebase from '~/plugins/firestore';
import firestore from "firebase/firestore";

const db = firebase.firestore();

export default {
  components: {
    Logo
  },

  methods: {
    addSHISHOW: function() {
      db.collection('SHISHOW')
        .doc()
        .set({
          key: 'shishow',
        })
    }
  }
}
</script>

これで,shishowの文字のところをクリックすると

このような感じでデータが追加されていると思います。
これでfirestoreが利用できるようになったかと思います。

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

WebAssemblyでお絵描きチャット"8bitpaint chat"を作った

公開先

https://minordaimyo.net/8bitpaintchat/
(現在デバッグ中)

8bitpaint chatの主な特徴・使い方

  • 高解像度のキャンバス(A4 600dpi相当)で軽快な描き味
  • 筆圧と傾き検知に対応
  • 参加人数は8人まで(ROMは現状16人まで)
  • 使える色は入室時に割り当てられた1色のみ
  • 任意の色をミュート(非表示)にできる機能
  • 2本指のタッチ操作でキャンバスのスクロール・拡縮
  • 2本指タップでUndo、3本指タップでRedo
  • キャンバスのダウンロード機能(pngとpsd形式)

動作環境

Windows上のChromeとペンタブレット・液晶タブレット
iPad上のSafariとApple-pencil
Android上のChrome
等々

ぎじゅつてきなこと

クライアントサイドはjs+WebAssembly(主にc言語)
サーバーサイドはnode.js+WebAssembly(クライアントと共通のコード)
で作成。

キャンバスサイズは、7016x4961pixel(A4 600dpiと同じ)。
クライアント側の消費メモリは300MBくらい、
サーバー側の消費メモリは150MBくらい。
サーバー側にもキャンバスを保持しているため、メモリ消費が大きくなっています。

ユーザーインターフェース周りを中心にjsを使用。
お絵描き機能部分はほぼc言語で記述(WebAssembly)。

苦労したこと

c言語は割と得意だけどWebプログラミングは全くの素人。
そのためjsでの記述部分はかなりの糞コードになってしまった…
あとまだデバッグ中なので、不具合は結構あります。

その他

初めてQiitaで発信しました。
必要なことは後から書き足す予定です。

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

【Nuxt.js】WebアプリSHISHOWを開発したときの備忘録③firebase基本設定編

もう一度プロジェクトを作成して確認しながら書きました。

Firebaseの導入

データベースやらホスティングやらが便利なので,firebaseの機能を導入します。

まずはfirebaseのページでプロジェクトを作成し,
そのプロジェクトにWebアプリを追加します。

そしてCLIに

npm install -g firebase-tools

そしてgoogleのアカウントでログインをします。

firebase login

ログインが完了したら,

firebase init

を実行します。

? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your ch
oices. Database: Deploy Firebase Realtime Database Rules, Firestore: Deploy rules and create indexes for Firestore, Functions: Con
figure and deploy Cloud Functions, Hosting: Configure and deploy Firebase Hosting sites

=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add, 
but for now we'll just set up a default project.

? Please select an option: Use an existing project
? Select a default Firebase project for this directory: project1-aae93 (project1)
i  Using project project1-aae93 (project1)

=== Database Setup

Firebase Realtime Database Rules allow you to define how your data should be
structured and when your data can be read from and written to.

? What file should be used for Database Rules? database.rules.json
? File database.rules.json already exists. Do you want to overwrite it with the Database Rules for project1-aae93 from the Firebas
e Console? Yes
✔  Database Rules for project1-aae93 have been downloaded to database.rules.json.
Future modifications to database.rules.json will update Database Rules when you run
firebase deploy.

=== Firestore Setup

Firestore Security Rules allow you to define how and when to allow
requests. You can keep these rules in your project directory
and publish them with firebase deploy.

? What file should be used for Firestore Rules? firestore.rules

Firestore indexes allow you to perform complex queries while
maintaining performance that scales with the size of the result
set. You can keep index definitions in your project directory
and publish them with firebase deploy.

? What file should be used for Firestore indexes? firestore.indexes.json

=== Functions Setup

A functions directory will be created in your project with a Node.js
package pre-configured. Functions can be deployed with firebase deploy.

? What language would you like to use to write Cloud Functions? JavaScript
? Do you want to use ESLint to catch probable bugs and enforce style? No
✔  Wrote functions/package.json
✔  Wrote functions/index.js
✔  Wrote functions/.gitignore
? Do you want to install dependencies with npm now? Yes

> protobufjs@6.8.8 postinstall /Users/kunosouichirou/Desktop/polp/functions/node_modules/protobufjs
> node scripts/postinstall

npm notice created a lockfile as package-lock.json. You should commit this file.
added 233 packages from 180 contributors and audited 662 packages in 7.21s
found 0 vulnerabilities


=== Hosting Setup

Your public directory is the folder (relative to your project directory) that
will contain Hosting assets to be uploaded with firebase deploy. If you
have a build process for your assets, use your build's output directory.

? What do you want to use as your public directory? public
? Configure as a single-page app (rewrite all urls to /index.html)? No
✔  Wrote public/404.html
✔  Wrote public/index.html

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...

✔  Firebase initialization complete!

これでfirebaseの初期設定はできたはずです。

そして,npmでfirebaseをインストールします。

npm install --save firebase

このとき,インストールのコマンドを実行するディレクトリを間違えるとうまく動かないかもしれません。

次は,firebaseのキーを取得してきます。

この歯車のアイコンのところの「プロジェクトの設定」を選択し,Firebase SDK snippetのところにあるスクリプトをコピーします。
// Your web app's Firebase configuration
  var firebaseConfig = {
    apiKey: "自分のapiキー",
    authDomain: "project1-aae93.firebaseapp.com",
    databaseURL: "https://project1-aae93.firebaseio.com",
    projectId: "project1-aae93",
    storageBucket: "project1-aae93.appspot.com",
    messagingSenderId: "598499664640",
    appId: "1:598499664640:web:fdeefdfb150f0d823009e5"
  };
  // Initialize Firebase
  firebase.initializeApp(firebaseConfig);
  firebase.analytics();

そして,Nuxtのプロジェクトの中のpluginフォルダの中にfirestore.jsを作成し,その中にコピーしていきます。

firestore.js
  var firebaseConfig = {
    apiKey: "AIzaSyBbyxTMzjGkPhSyKDl_-gAEsBjSPoqWukM",
    authDomain: "project1-aae93.firebaseapp.com",
    databaseURL: "https://project1-aae93.firebaseio.com",
    projectId: "project1-aae93",
    storageBucket: "project1-aae93.appspot.com",
    messagingSenderId: "598499664640",
    appId: "1:598499664640:web:fdeefdfb150f0d823009e5"
  };
  // Initialize Firebase
  firebase.initializeApp(firebaseConfig);

僕は今回analyticsは使っていないのでfirebaseConfigのところからその部分は削除してあります。

これでfirebaseの基本的な設定はできました。

次回はfirestoreなどの設定について書いていきます。

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

【Vue.js】ミックスインでコンポーネントオプションを追加する

ミックスインとは

  • オプションを他のコンポーネントに混ぜ込む(マージする)機能
  • 複数のコンポーネントで共通の処理を実行したい場合に使える
  • ミックスインオブジェクトとコンポーネントの定義がコンフリクトした場合は、コンポーネントのデータが優先される
  • フック(createdなど)は、ミックスインに定義されているものから先に呼び出される
  • 1つのコンポーネントから複数のミックスインオブジェクトを呼び出すことも可能
  • 複数のミックスインオブジェクト同士で定義がコンフリクトしている場合は、後で呼び出されているオブジェクトの定義が優先される
  • 上記の場合、createフックは先に呼び出されているミックスインオブジェクトから実行される

補足

extendとdataオプション

extendを使用する場合、extendしたいオブジェクトのdataオプションの記述方法が少し異なります。

この記述方法だとエラーが発生します。

vue_mixin_test.html
<script>
data: {
    foo: 'abc',
    message: 'hello'
},
</script>
[Vue warn]: The "data" option should be a function that returns a per-instance value in component definitions.

メッセージの通り、dataオプションをfunctionで返せば問題ありません。
以下の記述方法はextendを使用しない場合も使えるので、こちらの方が無難かもしれません。

vue_mixin_test.html
<script>
data: function () {
    return {
        foo: 'abc',
        message: 'hello'
    }
},
</script>

公式サイトにも説明があります。

コンポーネントオプション〜データ

ライフサイクルフック

createdフックなど、インスタンス生成時に初期化として実行される関数のことです。

ミックスインオブジェクトに定義されているフック関数は、コンポーネントのフック関数より先に呼び出されます。
ミックスインオブジェクトはこの時点で既にマージされているので、定義がコンポーネントとコンフリクトしている場合は、コンポーネントのデータが呼び出されます。

Vue インスタンス〜インスタンスライフサイクルフック

フック関数は複数あり、それぞれ実行されるタイミングが異なります。

Vue インスタンス〜ライフサイクルダイアグラム

ソースコード

公式サイトからお借りしました。

vue_mixin_test.html
<!DOCTYPE>
<head>
  <meta charset="UTF-8">
  <title>Vue.js_mixin_test</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <script>
      var mixin = {
          data: function () {
              return {
                  hoge: 'hoge',
                  foo: 'abc',
                  // 重複するプロパティ
                  message: 'hello'
              }
          },
          methods: {
              fooMethod: function () {
                  console.log('foo')
              },
              // 重複するメソッド
              conflicting: function () {
                  console.log('from mixin')
              }
          },
          // this.$dataはmixinがマージされた後の状態で表示される
          created: function () {
              console.log('mixin', this.$data)
              this.fooMethod()
              this.conflicting()
          }
      }

      var mixin2 = {
          data: function () {
              return {
                  foo: 'abc2',
                  message: 'hello2'
              }
          },
          methods: {
              fooMethod: function () {
                  console.log('foo2')
              },
              conflicting: function () {
                  console.log('from mixin2')
              }
          },
          created: function () {
              console.log('mixin2', this.$data)
              this.fooMethod()
              this.conflicting()
          }
      }

      // 複数のmixinを呼び出すことも可能
      var Component = Vue.extend({
          mixins: [mixin, mixin2]
      })

      new Component()

      var vm = new Vue({
          // mixinをマージする
          mixins: [mixin],
          // mixinと定義が重複している場合、コンポーネントのデータが優先される
          data: function () {
              return {
                  bar: 'def',
                  // 重複するプロパティ
                  message: 'goodbye'
              }
          },
          methods: {
              barMethod: function () {
                  console.log('bar')
              },
              // 重複するメソッド
              conflicting: function () {
                  console.log('from self')
              }
          },
          created: function () {
              console.log('self', this.$data)
              this.barMethod()
              this.conflicting()
          }
      })

      vm.fooMethod()
      vm.barMethod()
      vm.conflicting()
  </script>
</body>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

東工大ポータルに自動ログインする

内容

Tampermonkey

Chrome版 https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=ja
Firefox版(スマホ対応) https://addons.mozilla.org/ja/firefox/addon/tampermonkey/
Opera版 https://addons.opera.com/ja/extensions/details/tampermonkey-beta/

を使ってパソコン・スマホから1クリックで自動ログインさせる

(最終編集日 2019/09/15)

やり方

※パソコン・スマホ版もやり方は同じ

手順1

・上のURLからTampermonkeyをインストールする

・以下のURLからTampermonkey用のスクリプトをインストールする
https://greasyfork.org/ja/scripts/390145-tokyotech-portal-login-%E6%9D%B1%E5%B7%A5%E5%A4%A7%E3%83%9D%E3%83%BC%E3%82%BF%E3%83%AB%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3

・スクリプト内の

(document,window,location,0,'00B00000','password',['1111111111','2222222222','3333333333','4444444444','5555555555','6666666666','7777777777']);

の部分を自分の学籍番号,パスワード,マトリックスコードに書き換える

手順2

・以下のログイン画面をブックマークに追加する
https://portal.nap.gsic.titech.ac.jp/GetAccess/Login?Template=userpass_key&AUTHMETHOD=UserPassword

・ブックマークより自動ログインできるか確認する

おわり

補足

Tampermonkeyに対応していないブラウザを使っている場合には以下のスクリプトをブックマークレットとして追加する
該当部分は学籍番号,パスワード,マトリックスコードに書き換える
ただし3回もクリックしないといけないのでめんどくさい

javascript:(
    function(d,w,n,j,i,p,m){
        var l=d.login,f=d.getElementsByTagName('input'), t, c;
        switch(n.search.replace(/[&?]Template=([^&]*)(&.*)?/,'$1')){
            case 'userpass_key':
                l.usr_name.value=i;
                l.usr_password.value=p;
                l.submit();
                break;
            case 'idg_key':
                while(++j-4){
                    t=f.item(j);
                    c=t.parentNode.parentNode.parentNode.parentNode.getElementsByTagName('th')[0].innerHTML;
                    t.value=m[c.match(/[1-7]/)[0].charCodeAt(0)-'1'.charCodeAt(0)].charAt(c.match(/[A-J]/)[0].charCodeAt(0)-'A'.charCodeAt(0));
                    }
                    l.submit();
                break;
            default:
                if(n.host+n.pathname=='wlanauth.noc.titech.ac.jp/fs/customwebauth/login.html'){
                    d.getElementById('username').value=i;
                    d.getElementById('password').value=p;
                    submitAction();
                }else{
                    w.open('https://portal.nap.gsic.titech.ac.jp/GetAccess/Login?Template=userpass_key&AUTHMETHOD=UserPassword', '_blank');
                }

        }
    }
)
(document,window,location,0,'00B00000','password',['1111111111','2222222222','3333333333','4444444444','5555555555','6666666666','7777777777']);

参考

東工大ポータルとtitech-pubnetのログイン自動化のスクリプト(とブックマークレット)書いた
http://l1048576.blogspot.com/2014/05/titech-automatrix1.html

Chrome拡張「Tampermonkey」で閲覧ウェブサイトをカスタマイズ!
https://www.lisz-works.com/entry/chrome-tampermonkey

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

Vue.js で Moment.js を使ってお手軽に日付フォーマットする

 Vue.js のフィルタでフォーマット

Moment.js を使ったフィルタを用意すると以下のようにお手軽に日付のフォーマットが可能です。

<template>
  <h1>{{ new Date() | moment('LTS') }}</h1>
</template>

JavaScript の日付処理は罠が多い

  • Date#getYearで1900年からの経過年数が返る
  • Date#getMonthで 0 ~ 11 が返る。

JavaScript の日付処理ライブラリである Moment.js を使えばそういった罠を回避し、お手軽に JavaScript の日付を扱うことができます。

https://momentjs.com

現在、Github のスターは 40000 を超え JavaScript の日付処理ライブラリでは最もメジャーなものの一つです。

フィルタの作り方

サンプルプロジェクトを作成します。
既存のプロジェクトに組み込む場合、ここは必要ありません。

# Vue CLI のインストール ※すでにインストール済みなら必要なし
$ npm install -g @vue/cli

$ npm install -g @vue/cli
# yarn global add @vue/cli

$ cd 任意のディレクトリ

$ vue create moment-filter

$ cd moment-filter

Moment.js をインストールします。

# Moment.jsをインストールする
$ npm install moment --save 
# yarn add moment 

Moment.js を使ったフィルタを作成します。

import moment from "moment";

/* 中略 */

  filters: {
    /**
     * @param {Date} value    - Date オブジェクト
     * @param {string} format - 変換したいフォーマット
     */
    moment(value, format) {
      return moment(value).format(format);
    }
  }

上記で完成です。簡単。

テストの表示

テストデータを用意して試してみましょう。

data() {
    const formats = [
      "MMMM Do YYYY, h:mm:ss a",
      "dddd",
      "MMM Do YY",
      "YYYY [escaped] YYYY",
      "LTS"
    ];
    return {
      tests: new Array(formats.length)
        .fill(new Date())
        .map((date, i) => ({ date, format: formats[i] }))
    };
  }

今回はとりあえず以下のように並べて表示してみます。

<template>
  <div id="app">
    <h2 v-for="({date, format}, index) in tests" :key="index">{{date | moment(format)}}</h2>
  </div>
</template>

ローカルサーバーを立ち上げます。

$ npm run serve
# yarn serve

フォーマットされた日付が表示できました。

今回作成したコンポーネントの全体は以下のとおりです。

./src/App.vue
<template>
  <div id="app">
    <h2 v-for="({date, format}, index) in tests" :key="index">{{date | moment(format)}}</h2>
  </div>
</template>

<script>
import moment from "moment";

export default {
  name: "app",
  data() {
    const formats = [
      "MMMM Do YYYY, h:mm:ss a",
      "dddd",
      "MMM Do YY",
      "YYYY [escaped] YYYY",
      "LTS"
    ];
    return {
      tests: new Array(formats.length)
        .fill(new Date())
        .map((date, i) => ({ date, format: formats[i] }))
    };
  },
  filters: {
    moment(value, format) {
      return moment(value).format(format);
    }
  }
};
</script>

<style>
#app {
  font-family: "Avenir", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】分割代入はネストできる

分割代入 (Destructuring) は下のように配列やオブジェクトを分解して代入することができる。

const [x, y] = [1, 2, 3]
// x = 1, y = 2

const {a, b, c} = {a: 1, b: 2}
// a = 1, b = 2, c = undefined

さらに destructuring は下のようにネストできる。

const [[x, y], [z, w]] = [[3, 2, 5], [4]]
// x = 3, y = 2, z = 4, w = undefined

const {a: {a1, a2}, b: {b1}} = {a: {a1: 1}, b: {}}
// a1 = 1, a2 = undefined, b1: undefined

オブジェクトと配列を組み合わせることもできる。

const [{x, y}, [z, w]] = [{x: 1}, [2, 3]]
// x = 1, y = undefined, z = 2, w = 3

エラーと回避

以下の場合エラーが発生するので注意する必要がある。

  • undefinednull が オブジェクトとして destructure されるとき
  • iterable1 でない値が配列として destructure されるとき
// プロパティ a の値 undefined がオブジェクトとして destructure されている
const {a: {b}} = {}  // Uncaught TypeError: Cannot destructure property `b` of 'undefined' or 'null'.

// 2番目の要素 5 が配列として destructure されている
const [a, [b, c]] = [3, 5]  // Uncaught TypeError: undefined is not a function

上記のケースのうち、「存在しない要素/プロパティを destructure するとき」と「undefined を destructure するとき」に発生するエラーはデフォルト値 {} [] を指定することで回避できる

const {a: {b} = {}} = {}
// b: undefined

const [a, [b, c] = []] = [3]
// a = 3, b = undefined, c = undefined

これは API の JSON レスポンスなどから値を取り出すときに非常に便利である。

Twitterの例
const {
  user: {
    name,
    entities: {
      url: {
        urls = []
      } = {}
    } = {}
  } = {}
} = tweetObject

  1. iterable とは簡単に言えば値を一つづつ取り出せるオブジェクトのことで、配列や Set などがこれにあたる。詳細はMDN。 

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

【Vue.js】computedとmethodとwatchの使い分け

以下3つのオプションの区別が曖昧だったので、メモしておきます。
ソースコードは公式サイトよりお借りしました。

computedオプション

  • 算出プロパティ
  • リアクティブな依存関係にもとづきキャッシュされる
  • リアクティブな依存関係が更新されたときにだけ再評価されるので、逆に言えばリアクティブな依存がない場合は二度と更新されない
  • ゲッター(getter関数)とセッター(setter関数)の両方が利用できる(デフォルトはゲッターのみ)

ゲッターとセッター

computedでセッターを使用した場合、dataオプションで設定されたプロパティを更新することもできます。

ゲッターとセッターの定義

ゲッター

特定のプロパティ値を取得するためのメソッド

セッター

特定のプロパティ値を設定するためのメソッド

ソースコード

firstNameプロパティやlastNameプロパティを変更すると再描画が起こりますが、nowプロパティはリアクティブな依存関係にないため、最初に読み込まれた以降は変化しません。

vue_test01.html
<!DOCTYPE>
<head>
  <meta charset="UTF-8">
  <title>Vue.js_test01</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <input v-model="firstName" placeholder="firstName">
    <input v-model="lastName" placeholder="lastName">
    <p>firstName: {{ firstName }}</p>
    <p>lastName: {{ lastName }}</p>
    <p>fullName: {{ fullName }}</p>
    <p>now: {{ now }}</p>
  </div>

  <script>
      var vm = new Vue({
          el: '#app',
          data: {
              firstName: '',
              lastName: ''
          },
          computed: {
              // fullName/now: 算出プロパティ名
              fullName: function () {
                  return this.firstName + ' ' + this.lastName
              },
              now: function () {
                  return Date.now()
              }
          }
      })
  </script>
</body>

methodsオプション

  • メソッド
  • 再描画が起きると常に関数を実行する

ソースコード

firstNameプロパティやlastNameプロパティを変更すると再描画が起こるので、それに合わせてnowプロパティも変化します。

vue_test01.html
<!DOCTYPE>
<head>
  <meta charset="UTF-8">
  <title>Vue.js_test01</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <input v-model="firstName" placeholder="firstName">
    <input v-model="lastName" placeholder="lastName">
    <p>firstName: {{ firstName }}</p>
    <p>lastName: {{ lastName }}</p>
    <p>fullName: {{ fullName() }}</p>
    <p>now: {{ now() }}</p>
  </div>

  <script>
      var vm = new Vue({
          el: '#app',
          data: {
              firstName: '',
              lastName: ''
          },
          methods: {
              fullName: function () {
                  return this.firstName + ' ' + this.lastName
              },
              now: function () {
                  return Date.now()
              }
          }
      })
  </script>
</body>

watchオプション

  • 監視プロパティ
  • 既にセットされているプロパティを監視する
  • 監視するプロパティの名前と、そのプロパティに変化(トリガー)があった場合に実行する関数(ハンドラ)を対にして指定する
  • 関数は、更新後・更新前のプロパティの値を引数に取ることができる
  • 処理は実行するが、データは返さない
  • computedでは行えないコストの高い処理を実行できる

以下、公式サイトより。

この場合では、watch オプションを利用することで、非同期処理( API のアクセス)の実行や、処理をどのくらいの頻度で実行するかを制御したり、最終的な answer が取得できるまでは中間の状態にしておく、といったことが可能になっています。これらはいずれも算出プロパティでは実現できません。

ソースコード

fullNameプロパティの表示はwatchオプションで描画することもできますが、逆に冗長なコードになってしまいます。

vue_test01.html
<!DOCTYPE>
<head>
  <meta charset="UTF-8">
  <title>Vue.js_test01</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <input v-model="firstName" placeholder="firstName">
    <input v-model="lastName" placeholder="lastName">
    <p>firstName: {{ firstName }}</p>
    <p>lastName: {{ lastName }}</p>
    <p>fullName: {{ fullName }}</p>
    <p>now: {{ now }}</p>
  </div>

  <script>
      var vm = new Vue({
          el: '#app',
          data: {
              firstName: '',
              lastName: '',
              // プロパティはあらかじめセットしておく
              fullName: '',
              now: '',
          },
          watch: {
              // firstName/lastName/now: 監視対象のプロパティ名
              // newValue: 更新後のプロパティの値
              // oldValue: 更新前のプロパティの値
              firstName: function (newValue, oldValue) {
                  console.log(newValue, oldValue);
                  this.fullName = newValue + ' ' + this.lastName
              },
              lastName: function (newValue, oldValue) {
                  console.log(newValue, oldValue);
                  this.fullName = this.firstName + ' ' + newValue
              },
              // 監視しているが、値に変化がないので変更されることはない
              now: function () {
                  return Date.now()
              }
          }
      })
  </script>
</body>

参考サイト

リアクティブの探求
【Vue.js】v-modelを使ってた時になんか動なかったお話
算出プロパティとウォッチャ

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

【Vue.js】computedとmethodsとwatchの使い分け

以下3つのオプションの区別が曖昧だったので、メモしておきます。
ソースコードは公式サイトよりお借りしました。

computedオプション

  • 算出プロパティ
  • リアクティブな依存関係にもとづきキャッシュされる
  • リアクティブな依存関係が更新されたときにだけ再評価されるので、逆に言えばリアクティブな依存がない場合は二度と更新されない
  • ゲッター(getter関数)とセッター(setter関数)の両方が利用できる(デフォルトはゲッターのみ)

ゲッターとセッター

computedでセッターを使用した場合、dataオプションで設定されたプロパティを更新することもできます。

ゲッターとセッターの定義

ゲッター

特定のプロパティ値を取得するためのメソッド

セッター

特定のプロパティ値を設定するためのメソッド

ソースコード

firstNameプロパティやlastNameプロパティを変更すると再描画が起こりますが、nowプロパティはリアクティブな依存関係にないため、最初に読み込まれた以降は変化しません。

vue_test01.html
<!DOCTYPE>
<head>
  <meta charset="UTF-8">
  <title>Vue.js_test01</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <input v-model="firstName" placeholder="firstName">
    <input v-model="lastName" placeholder="lastName">
    <p>firstName: {{ firstName }}</p>
    <p>lastName: {{ lastName }}</p>
    <p>fullName: {{ fullName }}</p>
    <p>now: {{ now }}</p>
  </div>

  <script>
      var vm = new Vue({
          el: '#app',
          data: {
              firstName: '',
              lastName: ''
          },
          computed: {
              // fullName/now: 算出プロパティ名
              fullName: function () {
                  return this.firstName + ' ' + this.lastName
              },
              now: function () {
                  return Date.now()
              }
          }
      })
  </script>
</body>

methodsオプション

  • メソッド
  • 再描画が起きると常に関数を実行する

ソースコード

firstNameプロパティやlastNameプロパティを変更すると再描画が起こるので、それに合わせてnowプロパティも変化します。

vue_test01.html
<!DOCTYPE>
<head>
  <meta charset="UTF-8">
  <title>Vue.js_test01</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <input v-model="firstName" placeholder="firstName">
    <input v-model="lastName" placeholder="lastName">
    <p>firstName: {{ firstName }}</p>
    <p>lastName: {{ lastName }}</p>
    <p>fullName: {{ fullName() }}</p>
    <p>now: {{ now() }}</p>
  </div>

  <script>
      var vm = new Vue({
          el: '#app',
          data: {
              firstName: '',
              lastName: ''
          },
          methods: {
              fullName: function () {
                  return this.firstName + ' ' + this.lastName
              },
              now: function () {
                  return Date.now()
              }
          }
      })
  </script>
</body>

watchオプション

  • 監視プロパティ
  • 既にセットされているプロパティを監視する
  • 監視するプロパティの名前と、そのプロパティに変化(トリガー)があった場合に実行する関数(ハンドラ)を対にして指定する
  • 関数は、更新後・更新前のプロパティの値を引数に取ることができる
  • 処理は実行するが、データは返さない
  • computedでは行えないコストの高い処理を実行できる

以下、公式サイトより。

この場合では、watch オプションを利用することで、非同期処理( API のアクセス)の実行や、処理をどのくらいの頻度で実行するかを制御したり、最終的な answer が取得できるまでは中間の状態にしておく、といったことが可能になっています。これらはいずれも算出プロパティでは実現できません。

ソースコード

fullNameプロパティの表示はwatchオプションで描画することもできますが、逆に冗長なコードになってしまいます。

vue_test01.html
<!DOCTYPE>
<head>
  <meta charset="UTF-8">
  <title>Vue.js_test01</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <input v-model="firstName" placeholder="firstName">
    <input v-model="lastName" placeholder="lastName">
    <p>firstName: {{ firstName }}</p>
    <p>lastName: {{ lastName }}</p>
    <p>fullName: {{ fullName }}</p>
    <p>now: {{ now }}</p>
  </div>

  <script>
      var vm = new Vue({
          el: '#app',
          data: {
              firstName: '',
              lastName: '',
              // プロパティはあらかじめセットしておく
              fullName: '',
              now: '',
          },
          watch: {
              // firstName/lastName/now: 監視対象のプロパティ名
              // newValue: 更新後のプロパティの値
              // oldValue: 更新前のプロパティの値
              firstName: function (newValue, oldValue) {
                  console.log(newValue, oldValue);
                  this.fullName = newValue + ' ' + this.lastName
              },
              lastName: function (newValue, oldValue) {
                  console.log(newValue, oldValue);
                  this.fullName = this.firstName + ' ' + newValue
              },
              // 監視しているが、値に変化がないので変更されることはない
              now: function () {
                  return Date.now()
              }
          }
      })
  </script>
</body>

参考サイト

リアクティブの探求
【Vue.js】v-modelを使ってた時になんか動なかったお話
算出プロパティとウォッチャ

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

webpack事始め

webpackとは

webpackとは各種ファイルを指定のファイルの中に取り込んで、関連の機能を一つのファイルにバンドルする(=統合する)機能を提供しています。

ファイルのフォーマットはJS以外にもStyleSheetや画像データ、jsonもバンドルできます。

用語の整理

モジュール

機能ごとに分割されたファイル。

バンドル(バンドルファイル)

統合されたファイルの事。
・バンドルを生成する = まとめられたファイルを生成する。
・モジュールをバンドルする = モジュールを統合する。

webpackを導入する事のメリット

ブラウザ上でデータを取得するにはhtmlを取得した後に、再度、JSや画像をサーバーに要求しなければいけません。(=取得するデータ数が多いほど、画面の描画に時間がかかる事になります)

その為、個々のファイル群を1つのJSファイルにバンドルすると、ファイルのリクエスト数を減らす事が出来る=ページの読み込み速度の問題も改善し、UXの向上にも良い影響をもたらす事ができます。

webpackはモジュールをかき集めて、一つのJSファイルとして、出力する事ができます。
モジュールの価値は他のファイルに必要なモジュール機能を取り入れて、利用できる点にあります。

導入してみる

まず、webpackを管理するための準備として、package.jsonを生成する。
(package.jsonはインストールしたパッケージを管理するjson形式で記録されたファイル)

$npm init -y

次にwebpackをインストールする

$npm info webpack //最新バージョンを確認してみる
$npm install --save-dev webpack@4.40.2 //現時点での最新バージョンをインストール

さらにwebpack-cliをインストールする

$npm info webpack-cli //最新バージョンを確認してみる
$npm install --save-dev webpack-cli@3.3.8 //現時点での最新バージョンをインストール

package.jsonを確認すると、上記インストールされた事が確認できる。

package.json
{
  "name": "webpackStudy",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/10mi8o/webpack.git"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/10mi8o/webpack/issues"
  },
  "homepage": "https://github.com/10mi8o/webpack#readme",
  "devDependencies": {
    "webpack": "^4.40.2",
    "webpack-cli": "^3.3.8"
  }
}

簡単なファイル構成でバンドルを試してみる

まず、バンドルしたファイルの出力先になるdistディレクトリを作成。

$mkdir dist

バンドルするjsファイルを作成

$mkdir src
$touch src/index.js
$touch src/utilities.js

検証用のhtmlファイルを作成。
この時点では、main.jsは生成されていません

$touch dist/index.html 
index.html
<!doctype html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    WebPackを勉強しましょう!!
    <script src="main.js"></script> //バンドルされたjsファイルを読み込む
</body>
</html>

ブラウザ上で変更をリアルタイムに確認したい場合に便利なlive-serverをインストールしておきます。

$npm install --save-dev live-server //インストール
$npx live-server //サーバー起動 npxコマンドでは、ローカルにインストールしたnpmのバイナリを相対パスを指定する事なく使えるようになります

次にモジュールを作成しましょう。先ほど作成したutilities.jsには与えた引数の2乗を返させましょう。

utilities.js
// ある数字を引数として与えるとその数字の2乗の数を返す
function double(num) {
    return num**2;
}

モジュールの価値は他のファイルにそのモジュールの機能を取り入れて利用できる事にあります。

他のファイルで利用できるようにする為には、exportを実装します。
関数だけでなく、定数も定義できます。

utilities.js
// ある数字を引数として与えるとその数字の2乗の数を返す
export function double(num) {
    return num**2;
}

//定数もexportできる
export const NAME = '10mi8o';

続いて、src/index.jsで利用できるようにします。
(importする関数名ピンポイントで指定したい場合は{}で囲って指定)
(jsの場合は.jsは省略可能)

index.js
import { NAME, double } from './utilities';

console.log(double(2));
console.log(NAME);

asを利用して名前をつけたい場合は以下のように書きます。

index.js
import * as utilities from './utilities';

console.log(utilities.double(2));
console.log(utilities.NAME);

asは名前の衝突を防ぐ際にも使えます。

index.js
import { NAME as NAME_OF_10mi8o } from './utilities';

console.log(NAME_OF_10mi8o);

webpackに関する設定を定義できるファイルを作成します。

$touch webpack.config.js
webpack.config.js
// 出力は絶対pathで指定しなければいけない為、node.jsのpathモジュールを使用する
const path = require('path');
const outputPath = path.resolve(__dirname, 'dist');

module.exports = {
    // バンドルするファイルを指定
    entry: './src/index.js',
    output: {
        // バンドルしてmain.jsとして出力
        filename: 'main.js',
        path: outputPath
    }
}

最後にmain.jsを生成します。

$npx webpack

以上ざっくりですが、簡単にwebpackに入門しました。

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

vue/dist/vue.esm.js って何~【とりあえず動くからいいや】からの卒業~

本記事執筆の経緯

  • こちらの素晴らしいチュートリアル記事の中で筆者さんがわからないって言っていたモノに対して調べようと思ったことがきっかけ
  • Vue.jsとRailsでTODOアプリのチュートリアルみたいなものを作ってみた
  • ※本記事のタイトルは決して上記の筆者さんを否定するものではないですし、むしろ自分で調べなかったら絶対気にすることなかったので感謝です!!

何についての記事か

vueを読み込む際に使っているコレ

import Vue from 'vue/dist/vue.esm.js'

対象読者

  • vueの入門者(わたし)

どうやって使っているか

app/javascript/packs/todo.js
import Vue from 'vue/dist/vue.esm.js'
import Header from './components/header.vue'

 var app = new Vue({
   el: '#app',
   components: {
    'navbar': Header,
  }
 });

こんな感じで意味もわからずなんとなく使っている

でも時々import Vue from 'vue'って書いてるやん?

何が違うか

  • import Vue from 'vue/dist/vue.esm.js': 完全ビルド(vue.esm.js)
  • import Vue from 'vue': ランタイム限定ビルド(vue.runtime.esm.js)

って呼ぶらしい。

何が違うかPart2

公式より抜粋

ランタイム + コンパイラとランタイム限定の違い

もしクライアントでテンプレートをコンパイルする必要がある (例えば、 template オプションに文字列を渡す、もしくは DOM 内の HTML をテンプレートとして利用し要素にマウントする) 場合は、コンパイラすなわち完全ビルドが必要です。

hoge.js
// これはコンパイラが必要です
new Vue({
  template: '<div>{{ hi }}</div>'
})

// これはコンパイラは必要ありません
new Vue({
  render (h) {
    return h('div', this.hi)
  }
})

どうすればいいのか1

  • html側にテンプレートの参照を書かずに単一ファイルコンポーネント側にまとめ、html側ではマウントするDOMの情報だけを書くようにする

  • 例)クリックした時に何かするやつ(修正前

click.html
<div id=#app>
  <button v-on:click="hoge">Click me!</button>
</div>
click.js
import Vue from 'vue/dist/vue.esm.js'

const app = new Vue({
  el: "#app",
  methods: {
    myClick() {
      alert("click")
    }
  }
})
  • 例)クリックした時に何かするやつ(修正後
click.html
<div id=#app></div>
click.js
import App from '../app.vue'

document.addEventListener('DOMContentLoaded', () => {
  const app = new App
  app.$mount('#app')
})
click.vue
<template>
  <button v-on:click="hoge">Click me!</button>
</template>

<script>
import Vue from 'vue'

export default Vue.extend({
  methods: {
    myClick() {
      alert("click")
    }
  }
})
</script>

疎結合になってメンテもしやすそう!!

どうすればいいのか2

  • 一応エイリアスを作成すれば完全ビルドの書き方のままでも動くんだけど

ランタイム限定ビルドは完全ビルドに比べおよそ 30% 軽量

と公式にあるため基本は1がいいと思うが 30%くらい別にいいわっていうときはエイリアスを作成すればimport Vue from 'vue'としてimportできるようになります。(実態は完全ビルドなんですけどね

エイリアスを作成する(パフォーマンス30%減)

  • webpackの場合
webpack.config.js
module.exports = {
  // ...
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.esm.js' // 'vue/dist/vue.common.js' webpack 1 用
    }
  }
}

2年目も後半になったのでとりあえず動くからいいやは卒業しようと思います。

なんかあったらコメントくださいませ
以上。

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

jQueryのcssをJavaScriptで書き換えたらこうなる一例

自分自身の基礎力向上のために、jQueryで作ったコードがJavaScriptで書いたらどのようになるか書いてみています。もし、他にも方法がありましたら、ご教示いただけると嬉しいです。

結果

jQueryのhover

jQuery

 $('button').hover(function () {
 //マウスカーソルが重なった時の処理
 $('button').css('background-color', '#f00');
 },
 function () {
 //マウスカーソルが離れた時の処理
   $('button').css('background-color', 'yellowgreen');
 })

JavaScriptのみ

 const target = document.getElementById('target');
 target.addEventListener('mouseenter', () => {
   target.classList.add('background-color', '#f00');
 }, false);

 target.addEventListener('mouseleave', () => {
   target.classList.remove('background-color', 'yellowgreen');
 }, false);

classListは読み取り専用ですが、addやremoveでオブジェクトを変更することが可能
Element.classList

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

Node.jsとは?

目的

  • Slackでアプリを作成したときにNode.jsというものに触れたが不明なことが多かったため調べたことをまとめる。

特徴

  • サーバで動くJavaScript
  • 小さい計算が得意で速い
  • マイクロソフトやYahooが推奨している
  • リアルタイムWebの分野に強い
  • 小規模で機動性の高いWebアプリで、リアルタイム性が求められるアプリに最適

ちょっと深堀した特徴

  • 非同期処理で動く(動作が高速な理由)
    データベースからデータを取得するプログラムで同期処理の場合はデータベースからの反応を待つが、非同期処理だとデータベースからの反応を待っている間に別の処理を実行することができる。前述の方法をとっているため大量のアクセスを高速に処理することが可能。

  • シングルスレットでメモリの消費効率化(小さい計算が得意な理由)
    本来サーバに複数のアクセスがあった時にそれぞれにメモリを割り当てて実行する(マルチスレッド)が、1万人規模のアクセスがあるとメモリが上限にあたり効率が悪くなる。Node.jsでは一つのメモリでひとつずつアクセスを処理する方法をとることでメモリ効率を向上させている。

    一見一つのメモリで一つの処理しかできないため処理が遅そうに感じるが前述した非同期処理のおかげで、メモリでの処理を待たずして処理を実行できるため、少ないメモリ消費量を抑えて大量のアクセスを高速に処理できる。

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

PureJSでExcelライクなセル編集UIを実装

※本記事の実装コードはこちらhttps://jsfiddle.net/jnado3g5/1/

はじめに

個人的な興味がきっかけですが、Excelライクなセル編集UIをJavaScriptで作成する際の課題として、「キー押下と同時にセル編集を開始する」という仕様をどう実現するか、少々難しい問題があります。
厄介なのが日本語入力で、実際、(おそらく)有名な商用ライブラリであるWijimo FlexGridでも、日本語入力を含むキー押下とセル編集開始は同時に行えないみたいです。
しかし、Googleスプレッドシートではそれが実現されているため、技術的に不可能ではなさそうです。

そこで、「キー押下と同時にセル編集を開始する方法」を調べて、実際に検証を行った結果を記します。

仕様

メインは「キー押下と同時にセル編集を行う方法」ですが、せっかくなので編集に関わるUIを一通り検証実装してみました。
なお、編集機能に焦点を当てているため、それ以外(行列追加削除等)は一切考えていません。

セル選択イメージ:
入力イメージ:
  • セル対してmousedownするとセルが選択されます
  • セル選択された状態で次の動作をします
    • F2キーで既存の値の編集開始します
    • 通常の入力(Backspace、数値、アルファベット、日本語等)で新規入力開始します
    • セルをダブルクリックすると既存の値の編集開始します
    • Delキーで既存の値をクリアします(編集開始しません)
    • セルからフォーカスが外れる(セル部分以外をmousedownする)と、上記のようなセル編集開始が行われなくなります(編集開始には再度フォーカスが必要)
  • セル編集中、次の動作をします
    • Enterキーを押されると入力終了して、入力された値をセルに反映します
    • Escapeキーを押されると入力終了して、入力された値を破棄します
    • フォーカスが外れると入力終了して、入力された値をセルに反映します

動作確認環境

次の環境で動作確認を行いました。なおOSはWindows10です。

  • Chrome
  • FireFox
  • Edge
  • IE11
  • Opera

キー押下と同時にセル編集を開始する方法

わかりやすく、実際は隠れている(y座標位置が-10000px)input要素を目に見えるようにすると、セル選択時は次のような状態になっています。
input要素にフォーカスが当たっているので、要素に対するinputイベントが拾えます。
初回inputイベント発生時、input要素をセルの位置に移動させれば、あたかも「キー押下と同時にセル編集を開始する」ようなUIが出来上がります。

ただし、Backspaceキーではinputイベントが拾えないので、別途keydownイベントを拾って制御します。

その他工夫

上記の方法で実装を行う場合、input要素に対して「編集前の待機状態 → 編集中 → 編集完了」と状態が遷移し、各状態で必要なイベントが異なります。
そのため、「次の状態に移ったら前の状態で登録していたイベントは破棄する」といった実装を組み込んでみました。
結果、ソースコードの見通しが良くなったかなという個人的な感想を持っています。

おわりに

目指せ!個人開発でWijimo FlexGrid越え!(厳しい)

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

Reactでreact-router-domを使おうとするとページのロードが終わらない問題を解決する

※この記事は、プログラミング歴五ヶ月程度の初心者によって書かれています。ベテランの方は大目に見てもらえると助かります。同じように初心者の人は、私の言っていることを鵜呑みにしすぎず「?」と感じた点はすぐに検索するようにしてください。

今回抜け出せなくなったこと

バックエンドはRails、フロントエンドはReactにTypeScriptを使って簡単なアプリを作ろうとしていました。

その時に、フロントエンド側でルーティング設定をしようと思い

$ npm install --save @types/react-router-dom

をターミナルで実行しました(セーブフラッグは無くても大丈夫だと思います。@typesというのはTypescript向けに型が付いたものなので、普通のJSでReact開発をしている人は必要ありません)。

そして、普通にコードで必要なものをインポートして使っていたのですが

Module not found: Can't resolve 'react-router-dom' in

というエラーに出くわします。

エラー文をそのまま検索したところ、TypeScript用だけじゃ無くて普通のreact-router-domもインストールした方が良いとのことだったので下のコマンドを実行しました。

$ npm install --save react-router-dom

そして、普通にコードを書き進め、npm startをしてlocalhostにアクセスすると画面のロードが終わらなくなりました。真っ白い画面で、コンソールにエラー表示もないまま、ずっとこのままです。

この問題を解決するのに、10時間以上のリサーチが必要になりました。

環境

内容的にバージョンは関係ないので、JSONファイルを貼っておきます。

{
  "name": "react_front_end",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@types/jest": "24.0.18",
    "@types/node": "12.7.5",
    "@types/react-router-dom": "^4.3.1",
    "axios": "^0.19.0",
    "react": "^16.9.0",
    "react-dom": "^16.9.0",
    "react-router-dom": "^4.3.1",
    "react-scripts": "3.1.1",
    "typescript": "^3.5.3"
  },
  "scripts": {
    "start": "PORT=8000 react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  },
  "devDependencies": {
    "@types/react": "^16.9.2",
    "@types/react-dom": "^16.9.0"
  }
}

解決策

原因は、Routeを使って、Routeを使っているコンポーネント自身を無限ループさせていたことでした。言葉だとうまく説明できないので、コードを表示します。

これが、ブラウザで表示されなかった時のコードです(長いので簡易化しています)。

import * as React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

class App extends React.Component<{}, {}> {
  render() {
    return(
      <div>
          <Router>
              <Switch>
                <Route path="/" component={App} />
                <Route path="/count" render={() => <Count name="fantastic!" />} />
                <Route path="/history" component={History} />
              </Switch>
          </Router>

          <h1>サインイン</h1>
          <form onSubmit={e=>this.handleSubmit(e)} >
             <input onChange={e=>this.handleEmailChange(e)} />
             <input onChange={e=>this.handlePassChange(e)} />
             <button type="submit">ログイン</button>
          </form>

      </div>
    )
  };
}

このコードで問題となっていたのは、Appコンポーネントの中でAppコンポーネント用のルートを設定している下のコードでした。

<Route path="/" component={App} />

解決策は単純でこの一行を削除してあげるだけです。

仕組みとしては最初にAppコンポーネントを読み込むのに、その中でそれ自身であるAppコンポーネントが読み込まれていて、それを読み込もうとするとまたAppコンポーネントが出てきて読み込んで…エラーになるということだと思います。

そもそも、元からAppコンポーネントは表示されているのでわざわざ、そこから設定しなくても大丈夫ですね。

解決までの道のり

久しぶりに思いっきりハマってしまい、しかも一切エラーメッセージのないエラーだったので原因を特定するのにかなり時間がかかってしまいました。

最初は、アプリを閉じたり色々アップデートしてみたり、再起動したり、タブ閉じたりキャッシュ削除したりと色々していました。が、もちろんダメでした。

最終的に出てきた「page unresponsive」とReactを組み合わせて検索すると、ようやく解決策にたどり着けました、

エラーにハマった時は、とにかく違うアプローチを色々試してみること、そしてChromeのエラーであったとしても一緒に検索してみることが大事だなと思いました。

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

window.mediaMatchで3デバイス判定

  • mediaMatchで3デバイス判定と切替時のイベントをまとめる
  • メディアクエリはサイトによって書き換え

サンプルコード

// メディアチェック
let medias = {
    winobj:[],
    query:[
        'screen and (max-width: 559px)', //SP
        'screen and (max-width: 959px) and (min-width: 560px)', //TABLET
        'screen and (min-width: 960px)', //PC
    ],
    removeSpace: function(s) {
        // スペースを除去するやつ
        return s.replace(/\s/g,'');
    }
};

// 切り替えイベント
function checkBreak(query){
    if(medias.removeSpace(query.media) === medias.removeSpace(medias.query[0]) && query.matches === true){
        // spの処理
        console.log( 'sp' );
    }
    if(medias.removeSpace(query.media) === medias.removeSpace(medias.query[1]) && query.matches === true){
        // tabの処理
        console.log( 'tab' );
    }
    if(medias.removeSpace(query.media) === medias.removeSpace(medias.query[2]) && query.matches === true){
        // pcの処理
        console.log( 'pc' );
    }
}

// ロード時実行
document.addEventListener('DOMContentLoaded',function(){
    for(let i=0;i<medias.query.length;i++){
        medias.winobj[i] = window.matchMedia(medias.query[i]);
        medias.winobj[i].addListener(checkBreak);
    }    
});

ちょっと課題点

スペースを含む以下のif部分。
もっと簡易なmatchを生成してよりメディアクエリ部分に特化したifのがブラウザの変更とかにはつよそーだなーと思ったり。

medias.removeSpace(query.media) === medias.removeSpace(medias.query[0]) && ~

ブラウザ確認

Chrome 76.0
Firefox 69.0
Safari 12.1

偉大なる参考

window.matchMedia をそろそろ活用してもいい頃
【続々】window.matchMedia を用いたブレイクポイントイベント
Matching multiple CSS media queries using window.matchMedia()

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

JavaScript の Array を特定の数に分割する関数(chunk)を生やしてみる

TL;DR

Array.prototype.chunk = function(size) {
  const new_array = [];
  for(let i = 0; i < this.length; i += size) {
    new_array.push(this.slice(i, i + size));
  };
  return new_array;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScript の Array に zip 関数を生やしてみる

TL;DR

Array.prototype.zip = function(...args) {
  const new_array = [];
  for(let i = 0; i < this.length; i ++) {
    new_array.push([this[i], ...args.map(arg => arg[i])]);
  }
  return new_array;
}

詳細

やっていることは単純で同じインデックスの値同士の組み合わせを作って新しい配列に push しているだけですね。もともとは Ruby の Array#zip のようなことを JavaScript でもできないかなと思って探していたんですが、どうやらネイティブでは実装されていないようなので今回作ってみた次第です。

実際に使ってみるとこんな感じになります。

const array0 = [0, 1, 2];
const array1 = [3, 4, 5];
const array2 = [6, 7, 8];

array0.zip(array1, array2) // => [[ 0, 3, 6 ], [ 1, 4, 7 ], [ 2, 5, 8 ]]

参考文献

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

イケてるマウスカーソルをPure JavaScriptで実装する

結果

See the Pen Cool UI - Mouse Chaser 01 by Loki (@loki__codepen) on CodePen.

追従するマウスカーソルについて

ここ最近、マウスカーソルに装飾をするサイトが増えてきている気がします。
自作するのは面倒なので、多くの制作者は以下のサイトのコードをコピーすることも多いでしょう。

イケてるマウスカーソルを簡単に実装する | 株式会社 エヴォワークス -EVOWORX-
https://www.evoworx.co.jp/blog/mouse-stoker-gsap/

jQueryを利用して書かれています。
Webサイトの制作であればjQueryを利用しない機会はあまりないと思いますが、記述をPure JavaScriptに調整したものを作成しましたので、共有いたします。

ソースコード

const 
  cursor = document.getElementById('js-cursor'),
  chaser = document.getElementById('js-chaser'),
  target = document.querySelector('a');
let
    delay = 10,
    cursorPosX = 0,
    cursorPosY = 0,
    chaserPosX = 0,
    chaserPosY = 0;

// カーソルの遅延アニメーション
TweenMax.to({}, .001, {
    repeat: -1,
    onRepeat: function() {
        chaserPosX += (cursorPosX - chaserPosX) / delay;
        chaserPosY += (cursorPosY - chaserPosY) / delay;

        TweenMax.set(cursor, {
            css: {
                left: cursorPosX - (cursor.clientWidth / 2),
                top: cursorPosY - (cursor.clientWidth / 2)
            }
        });

        TweenMax.set(chaser, {
            css: {
                left: chaserPosX - (chaser.clientWidth / 2),
                top: chaserPosY - (chaser.clientWidth / 2)
            }
        });
    }
});

// マウス座標を取得
document.onmousemove = function(event) {
    cursorPosX = event.pageX;
    cursorPosY = event.pageY;
};

// マウスオーバー時の処理
target.onmouseover = function() {
    cursor.classList.add('is-active');
    chaser.classList.add('is-active');
};

// マウスアウト時の処理
target.onmouseout = function() {
    cursor.classList.remove('is-active');
    chaser.classList.remove('is-active');
};

その他に調整した点

以下の点を修正しております。
- セレクタ名を.cursorから#js-cursorに変更
- セレクタ名を.followerから#js-chaserに変更
- cWidthfWidthなど、JavaScript側でもサイズを入力する必要があった値を、CSSのみで認識するように変更

さいごに

マウスカーソルの視認性が良くなるので、個人的には好きな実装です!
十数年前の流行りが回帰していますが、アニメーション系のライブラリと合わさって現代バージョンとして流行っていることがなんだか面白いですね。
やりすぎには注意(笑)


Twitterのフォロワーを募集しております!
すでにWeb業界にいらっしゃる方や、業界未経験の方、どんな方でも募集しておりますので、どうぞよろしくお願いいたします(*^^*)

Twitter - https://twitter.com/loki__tweet

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

便利ページ:Javascriptでカラーピッカー

便利ページ:Javascriptでちょっとした便利な機能を作ってみた のシリーズものです。

今回は、色の選択とRGB値の表示です。
HTMLにカラーピッカーがあるので、それを使います。また、プリセットカラーで選択できるようにします。

こんな感じのページです。

毎度の通り、デモページとGitHubです。

GitHub
 https://github.com/poruruba/utilities

デモページ
 https://poruruba.github.io/utilities/

(2019/9/15 追記)
一番近い色の名前を検索できるようにしました。色差を計算するのに以下を使わせていただきました。
 https://gka.github.io/chroma.js/
一口に色と言っても奥が深いですね。

カラーピッカーを表示する

HTMLは以下の通りです。

index.html
<div class="form-inline">
    <input type="color" v-on:change="color_change" v-model="color_value">&nbsp;&nbsp;
    <label>RGB:</label> <input type="text" size="7" class="form-control" v-model="color_value">
</div>
<br>
<table class="table table-borderless">
    <td v-bind:bgcolor="color_value">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</td>
    <td><label>R:</label><input type="range" min="0" max="255" v-model.number="color_r" v-on:input="color_range"><input type="number" class="form-control" v-model.number="color_r" v-on:change="color_range"></td>
    <td><label>G:</label><input type="range" min="0" max="255" v-model.number="color_g" v-on:input="color_range"><input type="number" class="form-control" v-model.number="color_g" v-on:change="color_range"></td>
    <td><label>B:</label><input type="range" min="0" max="255" v-model.number="color_b" v-on:input="color_range"><input type="number" class="form-control" v-model.number="color_b" v-on:change="color_range"></td>
</table>

カラーピッカーは、type="color"のinputです。
あとは、color_valueに格納されたRGB値をテーブルのセルの背景色として表示しています。
type="range"のinputのゲージでもRGBの各値を変更できるようにしました。
ここらへんは、Vueのデータバインディング機能が大活躍です。

基本16色などのテーブルがありますが、プリセットとしてcolor.jsに配列として格納しておいたものを、テーブル表示しているだけです。

単純ですね。。。

以上

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

フリーランスになるために

初めまして

まず「フリーランスになりたい」なんて記事をQiitaで書いていいの?なんて気もしますが、
自分が学んだ技術を書き残す場所が欲しいなってことでQiitaブログを初めてみます。

何をゴールとするか

小目標

私は現在本業で8時間フルタイム働いているので、副業として動くことを前提として、
2019年のうちにWebコーダーとして月10万売り上げることを目標とします。

中目標

小目標が達成できたら、本業をやめる、つまり退職してフリーランス一本にします。
数字としては、月30万売り上げたい。

大目標

今の年収と同額をフリーランスで稼ぐこと。
どのタイミングで知り合いに公開するか不明なので、現段階では具体的な金額は伏せます。
2020年までにできるといいなあ・・・

現状のスキル

つい最近までECサイト構築の会社にいてエンジニアとして働いていたので、
HTML、CSS、JavaScriptの知識は多少あります。
あとは、.Net(VB、C#)がかけるのと、JavaとかRubyは調べながらなら読めるくらい。

これから

実際に1から10まで作れるかというと微妙なので、実務に必要な流れを一通り学びたい。
そして流行りや効率的な書き方を全然知らないので、学びたい。

具体的に

  • サイトの模写コーディング
  • ポートフォリオ作成

がんばるぞい!

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

JavaScriptで綺麗にリンク一覧を取得する

スクレイピングするまでもないけど、サイトのリンク一覧をさっと取得したいシーンがあったのでスクリプトを書きました。
実用的なスクレイピングやもっと高度なことを求めている方はChrome拡張のScraper等の利用を検討して、どうぞ。

仕様

「綺麗に」の内訳。

  • リンクのテキストとURLを並べて表示、クリップボードにコピー
  • URLの重複は除去する
  • テキストの無駄な改行は除去する
  • 特定のドメインに絞れるようにする
  • For文は使わない

コード

copy()コマンドを利用しているので、Chrome前提です。
ブラウザのConsoleに貼って実行してください。

// 検索ワードは適宜変更してください。
const targetLinkWords = ['www.bbc.com'];

const createLinkList = (el) => {
  let existsList = [];
  let res = '';
  Array.prototype.filter.call(el, (node) => {
    // hrefの値重複とtargetLinkWordsに登録されたワードを含まない場合、除外
    if (existsList.indexOf(node.href) === -1 && 
      targetLinkWords.find((val) => {return node.href.indexOf(val) !== -1;})) {
      existsList.push(node.href);
      res = `${res}\r\n` + (node.text.trim() === '' ? 
        'テキストなし':node.text.replace(/\r?\n/g, '')) + `||${node.href}`;
    }
  });
  return res;
};

const result = createLinkList(document.querySelectorAll('a'));
console.log(result);
copy(result);

結果

試しにBBC NEWS Techのページで実行してみました。
Homepage||https://www.bbc.com/
Skip to content||https://www.bbc.com/news/technology#skip-to-content
Accessibility Help||https://www.bbc.com/accessibility/
Sign in||https://session.bbc.com/session?ptrt=https%3A%2F%2Fwww.bbc.com%2Fnews%2Ftechnology&context=news_gnl&userOrigin=news_gnl
Notifications||https://www.bbc.com/news/technology#
News||https://www.bbc.com/news
Sport||https://www.bbc.com/sport
Reel||https://www.bbc.com/reel
Worklife||https://www.bbc.com/worklife
Travel||https://www.bbc.com/travel
Future||https://www.bbc.com/future
Culture||https://www.bbc.com/culture
Music||https://www.bbc.com/culture/music
Weather||https://www.bbc.com/weather
More||https://www.bbc.com/news/technology#orb-footer
Video||https://www.bbc.com/news/video_and_audio/headlines
World||https://www.bbc.com/news/world
Asia||https://www.bbc.com/news/world/asia
UK||https://www.bbc.com/news/uk
Business||https://www.bbc.com/news/business
TechTech selected||https://www.bbc.com/news/technology
Science||https://www.bbc.com/news/science_and_environment
Stories||https://www.bbc.com/news/stories
Entertainment & Arts||https://www.bbc.com/news/entertainment_and_arts
Health||https://www.bbc.com/news/health
World News TV||https://www.bbc.com/news/world_radio_and_tv
In Pictures||https://www.bbc.com/news/in_pictures
Reality Check||https://www.bbc.com/news/reality_check
Newsbeat||https://www.bbc.com/news/newsbeat
Special Reports||https://www.bbc.com/news/special_reports
Explainers||https://www.bbc.com/news/explainers
Long Reads||https://www.bbc.com/news/the_reporters
Have Your Say||https://www.bbc.com/news/have_your_say
Africa||https://www.bbc.com/news/world/africa
Australia||https://www.bbc.com/news/world/australia
Europe||https://www.bbc.com/news/world/europe
Latin America||https://www.bbc.com/news/world/latin_america
・・・

ちゃんとコピーされました。

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

JavaScript の Array で undefined などの値を除外する方法

TL;DR

const array = [0, 1, 2, "", undefined, null, NaN, "hoge"]
array.filter(Boolean) // => [1, 2, "hoge"]

詳細

Array.prototype.filter を使用しています。引数に入るコールバックの処理に対して true を返す値のみに絞る関数です。

例えば、以下のように偶数である値のみを抽出したいときなんかにも使えます。

[...Array(10).keys()].filter(n => n % 2 == 0) // => [0, 2, 4, 6, 8]

上記の例の場合は偶数かどうかの処理をはさみましたが、ここに Boolean を入れると Boolean() を実行してくれます。関数呼び出しですね。Boolean() を実行すると値の真偽を判定してくれます。Boolean の仕様によると、以下の値はすべて false と判定されるようです。

false と判定される値

[0, -0, null, false, NaN, undefined, ""]

If the value is omitted or is 0, -0, null, false, NaN, undefined, or the empty string (""), the object has an initial value of false.

備考

蛇足ですが、new Boolean() でオブジェクトを作るときは false と判定された値を持つ Boolean オブジェクトも生成されるようです。しかも if 文とかで判定するときも常に true が返ってきます。不思議。

const bool = new Boolean(false)
if(bool) console.log("false is true!") // => "false is true!"

参考文献

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

[GAS] GoogleSpreadSheet のGASで日本企業上位1000社の時価総額を取得

Google SpreadSheet はとても便利にデータをまとめることができます
今回、はじめてGASを利用してデータの取得をしたので、ソースコードとともに使い方の解説を載せておきます

スプレッドシートの準備

GASの利用にあたり、スプレッドシートを用意します

ツールを押し、スクリプトエディタを起動します

起動させたら、このような画面になっているはずです

ここからGASを利用してデータの取得をしていきます

GASの利用

今回用意したスプレッドシートとGASのプログラムを先に載せておきます
簡単な作業の流れの説明の後に、ソースコードの説明をしていきます

スプレッドシートの中身

順位、業界、コード、市場、名称、時価総額

の順に並んでいます

時価総額以外のところはすでに用意していました

今回やりたいこと

毎日時価総額を自動で更新してくれるスプレッドシートの作成

早速コードの解説と作成の仕方へ

基本的な解説はコメントの中に書いているので、GASでスクレイピングをする際に使用したライブラリの使い方を説明していきます

今回使用したのはスクレイピングを簡単にするライブラリのParserというものです
※詳しい説明はこちらをご覧ください
https://www.kotanin0.work/entry/2019/01/06/200000

まずは リソース > ライブラリ の順に開きます

開いたら、下に書いてあるキーを入力します

M1lugvAXKKtUxn_vdAG9JZleS6DrsjUUV

キーの入力をし、ライブラリを追加したら、バージョンを選んで完了です

前準備が完了したので、コードの解説に入ります

基本的にコード内にコメントとして解説を書いていきます

スクレイピングをするにあたり重要なのは、どこのwebサイトから情報を引っ張ってくるか、です

今回はみんかぶさんから情報を引っ張ってきています
理由として、Yahoo!ファイナンスはスクレイピングを禁止しているため、こちらのサイトを選びました

スクレイピングをやりすぎたら罪に問われることもあるので注意!!

では、コードと一緒に解説をしていきます

//今回は二つの関数を使用しています
//まずはJikaを実行します

function Jika() {
  // 取得した株価を出力するBookを定義
 const book = SpreadsheetApp.getActiveSpreadsheet()

  // 出力先のBookのシートを定義
  const sheet = book.getSheetByName("シート1");  
    //シート名を間違えるとエラーが出るので注意

  //証券コードの読み込み
  var bango = sheet.getRange('c2:c1001').getValues();
  //今回はc列に証券コードを集めているので、証券コードが書かれている1行目から範囲指定しています
  var array = [];


  //1000社分のループを回す
  for(var i = 0; i < 1000; i++) {
    //下のurlと繋げるため、文字列にする
    var num = bango[i].toString();
   //stokPrice関数を呼び出す
    var marketValue = [stockPrice(num)];

    //二次元配列として格納していく
    array.push(marketValue);<img width="1255" alt="スクリーンショット 2019-09-14 23.47.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/362795/1350d33f-d410-a42f-31cc-d96d18fccd24.png">

  }

  //最後にf列に時価総額を反映させる
  sheet.getRange('f2:f1001').setValues(array);
}


function stockPrice(code) {
  //証券コードごとにurlを作る
  var url = 'https://minkabu.jp/stock/' + code

  //ページ全てを取得する
  var response = UrlFetchApp.fetch(url);
  var html = response.getContentText('UTF-8');

  //fromとtoにタグを書き、その間にあるデータを取得する
  //今回はテーブル内のデータだったのでfromとtoの中身に日本語が入っていますが、これで大丈夫です
  var data = Parser.data(html).from('時価総額</th><td class="tar wsnw">').to('百万円</td>').build();
  return data;
}

完成したシートがこちらになります

これで毎日更新される!!!

(...手作業ならね)

これを自動化したい!!!
どうにか簡単にいかないものか!!

そんな悩める子羊たちもGASは救ってくれます

自動化の流れ

GASエディタに戻り、
左上のタイマーボタンを押します
すると、このような画面が出てくるはずです

トリガーを作成しましょう!!

トリガーの作成を押して出てきた画面で次のように選択します

これでようやく、
「Jikaを毎日、朝の3~4時に自動で実行して時価総額を引っ張ってくる」
ようになりました!!!!

ぱちぱちぱち

最後に

こちらのサイトには大変お世話になりました
1日でGASを使えるようになったのもこのサイトのおかげです
ありがとうございます!
https://tonari-it.com/gas-script-editor/

注意

ソースコードは、1000社分の証券コードを抜いて実行するぞ!!
という形になっていますが、GASさんは6分たつと自動でプログラムを終了してしまいます

僕のコードだと、100社で1分ほどかかります
なので、僕はスクリプトエディタを二つ作り、1~500と501~1000に分けて使っています

もし、1000社分一気にいっても余裕!!という案があればぜひ教えていただきたいです

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