20190425のvue.jsに関する記事は7件です。

人気のWEBサイトの誕生日をまとめてみた。

世界的に人気のWEBサイトやアプリーケーションがいつできたのか知っていますか?

個人的にものすごく気になったのでVue.jsの勉強がてらまとめてみました。

The Birth Of websites

スクリーンショット 0031-04-25 午後10.33.25.png

アレクサランキングTOP50を元にまとめました。

もちろん日本しか利用しない人気のサービスはたくさんありますが、そこらへんも含めるとキリがないので世界的に有名でトラフィックが多いサイトを元に作成しました。このサイトがないのはおかしいっていうのがあったら教えてください。

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

平成に生まれた人気のWEBサイトの誕生日をまとめてみた。

世界的に人気のWEBサイトやアプリーケーションがいつできたのか知っていますか?

個人的にものすごく気になったのでVue.jsの勉強がてらまとめてみました。

The Birth Of websites

スクリーンショット 0031-04-25 午後10.33.25.png

アレクサランキングTOP50を元にまとめました。

もちろん日本しか利用しない人気のサービスはたくさんありますが、そこらへんも含めるとキリがないので世界的に有名でトラフィックが多いサイトを元に作成しました。このサイトがないのはおかしいっていうのがあったら教えてください。

平成に誕生したWEBサイトの誕生日まとめ

DATE NAME
1994-07-05 Amazon.com
1995-03-01 Yahoo
1995-08-24 Msn
1995-09-03 eBay
1996 Alexa
1996 Ask.com
1996-01-31 Yahoo!Japan
1997 XVIDEOS
1997-02-07 楽天
1997-08-29 NETFLIX
1998 IMDb
1998-09 Amazon.co.jp
1998-09-04 Google
1998-11-12 Tencent
1998-12 PayPal
1999-03 Alibaba
1999-03-08 Salesforce.com
1999-06 NAVER
1999-07-20 FC2
1999-08-23 Blogger
2000-01-01 Baidu
2000-05 Stack Overflow
2001-01-15 Wikipedia
2001-11-12 LIVEJASMIN
2002-12-18 Linkedin
2003-05-27 WordPress
2004-02 flickr
2004-02-04 Facebook
2005 Reddit
2005-02-14 YouTube
2006-03-21 Twitter
2006-08 YouPorn
2006-12-12 ニコニコ
2007 DropBox
2007 xHamster
2007-02-19 tumblr
2007-05-25 Pornhub
2008 Bitly
2008-04 GitHub
2009 WhatsApp
2009-02-23 imgur
2009-06-01 Bing
2010-03 Pinterest
2010-10-06 Instagram
2011-06 Twitch
2011-06-23 LINE
2011-09 SnapChat

おれとXVIDEOSの年齢が同じだということがわかってよかったです。

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

Vue+electron製ツールでSlackに�メッセージを送る

Vue + electron製のツールでSlackにメッセージを送る

事の発端

新しくアルバイトとして入社した企業の配属チームの勤怠ルール

出社、休憩開始、休憩終わり、退社時に特定のチャンネルにそれぞれに対応する絵文字を送信する。

割とめんどくさい・・・ ちなみにこれ以外にも勤怠システムとタイムカードの3重門。

やったこと

タイトルの通り、SlackのAPIをVue + electron のツールでたたき、ワンクリックで出社退社などの絵文字を送信できるようにっした。

コード

vue initでelectron-vueのミニマムテンプレートから作っています。

App.vue
<template>
  <div id="app">
    <div class="cont">
      <div class="button" style="border-color: rgb(129,188,189); color: rgb(129,188,189);" @click="send(1)">
        <h2>出社</h2>
      </div>
      <div class="button" style="border-color: rgb(235,115,62); color: rgb(235,115,62);" @click="send(2)">
        <h2>休憩</h2>
      </div>
      <div class="button" style="border-color: rgb(235,115,62); color: rgb(235,115,62); " @click="send(3)">
        <h2>再開</h2>
      </div>
      <div class="button" style="border-color: rgb(58,61,145); color: rgb(58,61,145);" @click="send(4)">
        <h2>退社</h2>
      </div>
    </div>
  </div>
</template>

<script>
import { SlackOAuthClient } from 'messaging-api-slack';

export default {
  name: "kintary",
  methods: {
    send(type) {
      let mes = ''
      switch(type) {
        case 1:
          mes = ':sagyo-kaishi-作業開始_green:'
          break;
        case 2:
          mes = ':sagyo-ohiru-kyukei-お昼休憩_orange:'
          break;
        case 3:
          mes = ':sagyo-saikai-作業再開_orange:'
          break;
        case 4:
          mes = ':sagyo-shuryo-作業終了_navy:'
          break;
      }
      const client = SlackOAuthClient.connect(
        'xoxp-slackから取得したアクセストークン(管理者じゃなくても自分のものは取得できた)'
      );
      client.postMessage('frontteam_kintai', mes, { as_user: true });
    }

  }
};
</script>

<style>
#app {
  height: 100vh;
  width: 100vw;
  display: flex;
  justify-content: center;
  align-items: center;
}

.cont {
  width: 480px;
  height: 120px;
  display: flex;
  justify-content: center;
  align-items: center;
}

.button {
  height: 100px;
  width: 100px;
  border-radius: 50px;
  border: 3px solid;
  display: flex;
  justify-content: center;
  align-items: center;
  margin: 10px;
}
.button:hover {
  opacity: 0.6;
}
.button:active {
  transform: scale(0.95);
}
</style>

悩んだところ

SlackのAPIを叩く際、Axios.postで叩いたところCORS関連のエラーが出た。

超便利なnpmモジュールがあったのでおとなしくそれを利用、importして設定して.postMessageだけで利用できた。

※as_userオプションをtrueにすることで、tokenを取得したUserとして投稿ができる。

できたもの

test.gif

今後

ちゃんとビルドしたい。

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

k8sでPWAをびるどしてはいしんする。

Angularやvue.jsなどの静的ファイルをホストするときの流れだよ。

js等のビルドされたファイルをnginxのDocumentRootにコピーして、k8sにapplyするだけなので大した話ではないよ。
ALB経由で配信するよ。

1. package.json

今回はvuejsで作ったもの

{
  "name": "dapps",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "test:unit": "vue-cli-service test:unit"
  },
  "dependencies": {
    "axios": "^0.18.0",
    "vue": "^2.6.6",
    "vue-class-component": "^6.0.0",
    "vue-property-decorator": "^7.0.0",
    "vue-router": "^3.0.1"
  },
  "devDependencies": {
    "@types/jest": "^23.1.4",
    "@vue/cli-plugin-babel": "^3.4.0",
    "@vue/cli-plugin-typescript": "^3.4.0",
    "@vue/cli-plugin-unit-jest": "^3.4.0",
    "@vue/cli-service": "^3.4.0",
    "@vue/test-utils": "^1.0.0-beta.20",
    "babel-core": "7.0.0-bridge.0",
    "ts-jest": "^23.0.0",
    "typescript": "^3.0.0",
    "vue-template-compiler": "^2.5.21"
  },
  "postcss": {
    "plugins": {
      "autoprefixer": {}
    }
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8"
  ],
  "jest": {
    "moduleFileExtensions": [
      "js",
      "jsx",
      "json",
      "vue",
      "ts",
      "tsx"
    ],
    "transform": {
      "^.+\\.vue$": "vue-jest",
      ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": "jest-transform-stub",
      "^.+\\.tsx?$": "ts-jest"
    },
    "moduleNameMapper": {
      "^@/(.*)$": "<rootDir>/src/$1"
    },
    "snapshotSerializers": [
      "jest-serializer-vue"
    ],
    "testMatch": [
      "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)"
    ],
    "testURL": "http://localhost/",
    "globals": {
      "ts-jest": {
        "babelConfig": true
      }
    }
  }
}

2. Dockerfile

ビルドするコンテナとhttpdを分けるとイメージ節約になる。
URLのルーティングにhistory apiを利用している場合には、別途 try_files の設定をしないといけない。
Docker+nginxでtry_filesを追加したい

# Stage 0, "build-stage", based on Node.js, to build and compile the frontend
FROM node:11.3.0 as build-stage
WORKDIR /app
COPY package*.json /app/
RUN npm install --global npm@6.8.0 npx@10.2.0 && npm install
COPY ./ /app/
RUN npm run build

# Stage 1, based on Nginx, to have only the compiled app, ready for production with Nginx
FROM nginx:1.15
COPY --from=build-stage /app/dist /usr/share/nginx/html

できたDockerfileはECRにpushしておいた。

3. k8s.yaml

kubectl applay -f k8s.yam でデプロイする。
シンプルだね!

アプリとそれのServiceのPod

kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: gascenter-web-frontend-dapps
spec:
  replicas: 2
  selector:
    matchLabels:
      app: gascenter-web-frontend-dapps
  template:
    metadata:
      labels:
        app: gascenter-web-frontend-dapps
    spec:
      containers:
      - args:
        image: 026695289470.dkr.ecr.ap-northeast-1.amazonaws.com/gascenter-web-frontend-dapps:latest
        imagePullPolicy: Always
        name: gascenter-web-frontend-dapps
        ports:
        - containerPort: 80
          protocol: TCP
      imagePullSecrets:
        - name: awsecs
---
kind: Service
apiVersion: v1
metadata:
  name: gascenter-web-frontend-dapps-svc
spec:
  # externalTrafficPolicy: Local
  type: NodePort
  ports:
  - name: "http-port"
    protocol: TCP
    port: 80
    targetPort: 80
  selector:
    app: gascenter-web-frontend-dapps

ここからはIngress(ALB)の設定
今回はEKSを使ったので、ALBの設定もk8sから行った。こいつを食わせるとALBをよしなにいじってくれます。

alb.ingress.kubernetes.ioのアノテーションについてはこちら
https://kubernetes-sigs.github.io/aws-alb-ingress-controller/guide/ingress/annotation/

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: gascenter-web-frontend-app
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/scheme: internet-facing
    # alb.ingress.kubernetes.io/subnets: subnet-000000,subnet-111111,subnet-22222
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}, {"HTTPS": 443}]'
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-northeast-1:026695289470:certificate/65a878de-ed52-4415-a69e-c0fcf9aeca7a
    alb.ingress.kubernetes.io/actions.ssl-redirect: '{"Type": "redirect", "RedirectConfig": { "Protocol": "HTTPS", "Port": "443", "StatusCode": "HTTP_301"}}'
spec:
  rules:
    - host: xxxx.takamattekita.cc
      http:
        paths:
          - path: /*
            backend:
              serviceName: ssl-redirect
              servicePort: use-annotation
          - path: /*
            backend:
              serviceName: gascenter-web-frontend-dapps-svc
              servicePort: 80
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

pͪoͣnͬpͣoͥnͭpͣa͡inͥを支える技術

pͪoͣnͬpͣoͥnͭpͣa͡inͥの作り方

ぽんぽんぺいんを簡単に作るサイトを作ったのでよかったら使ってみてください。

https://ykhirao.github.io/qiita/ponponpain/dist/

タイトルなし.gif

ponponpain(haraita-i)とは

画像でいうとこんなやつのこと。

スクリーンショット 2019-04-25 2.28.11.png

今回参考にさせていただきましたが、以下のサイトによくまとまっていると思います。

まあ要するに、不思議な上付き文字を組み合わせて、文字に副題(ルビ)をふろうって感じの遊びのことで、昔流行ったみたいです。

今回はクリックだけで上付き文字を加えられるサイトを作ったのでぜひみんなに遊んでほしいなと思っています。

投稿について

UbuntuのChromeだと綺麗に表示されないのですが、Twitterだといい感じになるみたいです。

傑作ができたら、 #pͪoͣnͬpͣoͥnͭpͣa͡inͥを支える技術 とかハッシュタグつけてツイートしてくれるか、Qiitaにコメントいただけると全部見ませう!!!

私が作ったのは ponponpain(moudameda)です。電車の中で、この文字を見ていただけると幸いです。

スクリーンショット 2019-04-25 2.27.05.png

使い方

そんなに難しくないので、こちらの画像を見ていただけるとなんとなくわかるかと思います。もしくは既出のGIF動画を見てからお使いください。

スクリーンショット 2019-04-25 1.09.26.png

おわり

たくさん、遊んでいただけると幸いです!!!


以下は、ちょっとだけ、真面目な話するよ、読む人は優しい心を持ってみてね!!


真面目な話

この上付きの文字は ダイアクリティカルマーク というみたいで、日本語の パ̊ とかそういうもんだと考えればなんとなく文字コードが別にあっても不思議ではないのではないでしょうか。

// Chromeのコンソールで以下のコードを入力してください
` ${String.fromCharCode(867)} ` // 見やすい様に半角スペースいれている
//   ͣ がでてきます。

ダイアクリティカルマークは番号でいうと 768 ~ 879 HEXでいうと 0x300 ~ 0x36F が該当するみたいです。

逆向きの変換は 'A'.codePointAt(0); みたいなStringのAPIを使えば、 A の文字コード。

pͪoͣnͬpͣoͥnͭpͣa͡inͥ の文字コードはというと

"pͪoͣnͬpͣoͥnͭpͣa͡inͥ".codePointAt(0) // 112 普通のpが一文字目
"pͪoͣnͬpͣoͥnͭpͣa͡inͥ".codePointAt(1) // 874 上付きのhが二文字目にきます
// 気になる人はChromeのコンソールに右の値を入れましょう ` ${String.fromCharCode(874)} `
//  ͪ がでてきます

てな感じになります。

javascriptでいい感じに扱いやすくしたのは ぎとはぶ に乗せているので良かったら使ってください。↓以下のようなコードになります。

const codes = [
  { "id": 768, "hex": "0x300", "val": "̀" },
  { "id": 769, "hex": "0x301", "val": "́" },
  { "id": 770, "hex": "0x302", "val": "̂" },
  // ryaku
]

またフロントエンドの実装はVue.jsでサクッとやったので、参考にする奇特な方がいたらこちらをどうぞ!!
https://github.com/ykhirao/qiita/blob/master/ponponpain/src/components/DraggableText.vue このあたり。

右から左に文字列を読むアラビア語などのために U+202Eという制御コードがあったり、UnicodeがーーとかSJISがーーーとか、正直難しい世界ですよね。

なんで今回文字コードを追ってみようと思ったかというと、弊社でよく文字コードを見る機会があって、具体的には日本郵便とかに出す送り状を印刷している部分がWindows上のC#で書かれたアプリケーションでやっていて、WinのMS系のフォントにない文字コードのものは文字化けして大変なことになるんです。

なのでそのあたりの制御とかいろいろやっていて文字コードマスター(?)とかが社内にいて、あ、もしよかったら文字コード好きな人いたら Qiita Jobs からオープンロジという会社にチャット送っていただいて面接とか来てくれると嬉しいなーーと思います。。宣伝すみません。(ここまで早口)

文字コードの深淵を覗く時、文字コードの深淵もまたあなたを覗いているとニーチェが言ったとか(言ってない)ありますけど、今回いろいろ調べたので少しだけ文字コードと仲良くなれた気がします。

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

.

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

HTML の input type="number" には e を入力できる

HTML で input 要素の type 属性に number を指定したとき、数値以外で入力できる値として

  • -
  • +
  • .

以外に、実は e も入力できます

これは(おそらく)べき乗の表現にも対応しようとした処置です(例:1e+2100を表す)。

JavaScript でも 1000000000000000000000 以降は 1e+21 といった形で表現されたりしますしね。

ただ、困ったことにこれだと場合によってはサーバーサイドでの値のバリデーション等でコケる可能性があるので、フロントエンドで e を入力できないようにしたほうが親切かもしれません。

html5 - Why does the html input with type "number" allow the letter 'e' to be entered in the field? - Stack Overflow

自分は普段 Vue を使っているので、以下のように書くことで e を入力できないようにできたので便利だな〜と思ったりました。

<input type="number" @keydown.69.prevent>

以上です。

参考

input type="number" で e が書ける

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

Vue + Vuex + TypeScript でテトリスを作った話

TL;DR

ここで遊べます.

リポジトリはこちら

操作方法

  • カーソルキー (左下右) または A, S, D : 移動
  • 上キーまたは W : ハードドロップ
  • J : 左回転
  • K : 右回転
  • R または U : ホールド

一応おまけでスマホでも操作できるようにしてありますが PC 推奨です.
ARE も現代的な T-Spin も無いストイックなやつを目指して作りました. (しかしホールド機能は付けたかったので付けました)

きっかけ

普段フロントエンドの開発をすることは少ないのですが, 遅ればせながら最近になってぼちぼち情報に触れるようになり, 自分も勉強してなんか書いてみようかなという気持ちになったのが昨年末あたり.

ちょうどその頃なぜかたまたま Classic Tetris World Championship (CTWC) の動画1をよく見る日々が続いており, テトリスでも作って自分で遊ぶか〜と思ったのがきっかけです.

とりあえず React の前に Vue から入ろうと思い, Vue と TypeScript の公式ドキュメントを一通り読んでから取り掛かり, 寝る前の隙間時間にちまちま作っていきました.

せっかくなので自分用の備忘録として残しておこうと思って書いたのがこの記事というわけですが, これもちまちま書いていたらこんな時間が経ってしまった.

Vue + TypeScript

Vue の公式ガイドを上から順に読んでいくと,

  • 基本的にはコンポーネントと呼ばれるものを定義し,

    • 親から子コンポーネントへは v-bind を使って props としてデータを受け渡す
    • 子は親の状態を直接変化させない
    • 子から親に何かを伝えたいときはカスタムイベントを発火して親が v-on の形で受け取る
    • props は子の中では immutable なものとして扱い, props を使って宣言的に記述された算出プロパティが親からの props の変更に対してリアクティブに変化する
      • それがテンプレートにも反映される
  • 結局のところ単一ファイルコンポーネントを基本の形としてコードを書いていく

ということがわかります.

続いて TypeScript の公式ドキュメントを読んでいくわけですが, そこで素朴な疑問がひとつ浮かびます. つまり TypeScript では const answer: number = 42 のように変数を定義するのですが, Vue コンポーネントの datacomputed などのオプションはオブジェクトリテラルの形で定義されます. すると TypeScript の variableName: type という表記とオブジェクトリテラルの key: value という表記が衝突してしまうのではないか?という疑問です.

この疑問は TypeScript の Quick Start から辿れる TypeScript-Vue-Starter の中で解決されます.
README を読んでいくと, 一番最後のほうに「デコレータを使ってコンポーネントを定義する (Using decorators to define a component)」という章があります. それによれば, vue-property-decorator という便利なパッケージをインストールすると

HelloDecorator.vue
import { Vue, Component, Prop } from "vue-property-decorator";

@Component
export default class HelloDecorator extends Vue {
    @Prop() name!: string;
    @Prop() initialEnthusiasm!: number;

    enthusiasm = this.initialEnthusiasm;

    increment() {
        this.enthusiasm++;
    }
    decrement() {
        if (this.enthusiasm > 1) {
            this.enthusiasm--;
        }
    }

    get exclamationMarks(): string {
        return Array(this.enthusiasm + 1).join('!');
    }
}

のように, @Prop() というデコレータをつけることでその変数が props として扱われるようになったり, get アクセサをつけることでそのメソッドが算出プロパティとして扱われるようになったりするという話です.
( 上記 README 内での書き方が若干まぎらわしいのですが, vue-property-decorator は vue-class-component を依存先として持つので, インストールするのは vue-property-decorator だけで十分です )

デコレータの詳細については公式ドキュメントやほかの方の記事に解説を譲るとして, 利用者視点で言えば vue-property-decorator によって型を付けながらコンポーネントを定義することができるようになるというわけです.

vue-property-decorator を使ったさらなる書き方についてはこちらの記事も大変参考になりました:

tsconfig.json

この時点で tsconfig.json は基本的に以下のような内容になっていると思います:

tsconfig.json
{
    "compilerOptions": {
        "outDir": "./built/",
        "sourceMap": true,
        "strict": true,
        "noImplicitReturns": true,
        "experimentalDecorators": true,
        "module": "es2015",
        "moduleResolution": "node",
        "target": "es5"
    },
    "include": [
        "./src/**/*"
    ]
}

この記事を書いている 2019 年 4 月時点では "experimentalDecorators": true を明記する必要があります.

webpack.config.js

今回は私は勉強のため vue-cli を使わず手動で (?) 必要なパッケージをインストールしたり webpack.config.js を書いたりすることにしました.

基本的には上記の TypeScript-Vue-Starter 内の指示に従えば良いと思いますが, いくつか注意点があったので記録しておきます.

VueLoaderPlugin

webpack で Vue の単一ファイルコンポーネントを扱うために vue-loader が必要ですが, vue-loader v15 からは pluginsVueLoaderPlugin を指定することが必須であるとドキュメントに記載があります:

webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
  module: {
    rules: [
      // ... other rules
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      }
    ]
  },
  plugins: [
    // make sure to include the plugin!
    new VueLoaderPlugin()
  ]
}

ランタイム限定ビルドの利用

Vue 公式ドキュメントのランタイム + コンパイラとランタイム限定の違いという項を読むと, vue-loader を使用していれば, (単一ファイルコンポーネントのテンプレートはプリコンパイルされるので) 「完全ビルドに比べおよそ 30% 軽量」なランタイム限定ビルドを利用することができる, と書かれています.

つまり基本的に単一ファイルコンポーネントの形でコードを書き, エントリポイントとなる index.tsrender 関数を使ってルート要素に描画していれば, vue.esm.js ではなく vue.runtime.esm.js を利用することができます:

src/idnex.ts
import Vue from 'vue';
import App from './components/App.vue';

new Vue({
  el: '#app',
  render: h => h(App)
})
webpack.config.js
module.exports = {
  // ...
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.runtime.esm.js'
    }
  }
}

ESLint + Prettier

私が知る限り, フロントエンド開発においてはリンターに ESLint を, コードフォーマッターに Prettier を用いるという構成が現在のスタンダードのようです. 私も長いものに巻かれてエディタに甘えようと思い, この構成を取ることにしました.

TypeScript の場合 TSLint というリンターもあるようですが, TSLint チームは今後 ESLint に寄っていくというような記事が今年の 1 月に出たり:

していたので ESLint を使おうと判断しました. また折しも今年の 2 月後半には 2019 年中に TSLint が deprecated となる旨がアナウンスされました:

さて, ESLint と Prettier を組み合わせるために Prettier の公式ドキュメントを見ていきましょう. するとまず ESLint から Prettier を実行してくれるプラグインであるところの eslint-plugin-prettier が必要と書かれています. リポジトリの README を見ると .eslintrc.json

.eslintrc.json
{
  "plugins": ["prettier"],
  "rules": {
    "prettier/prettier": "error"
  }
}

と書けば良いようです.

さらに ESLint 側のコードフォーマットに関わるルールを無効にしてくれる eslint-config-prettier なるものもあります. こちらの README にもいろいろと書いてありますが, eslint-plugin-prettier 側の README とも合わせて読むと, 結局これら 2 つをインストールした上で .eslintrc.json"extends" 配列の最後に "plugin:prettier/recommended" を指定すれば良いことがわかります.

その上でさらにいくつか追加のプラグインも紹介されており, それらにも eslint-config-prettier が対応していることが示されています. 今回の構成だと私は

を使ったほうが良さそうです.

@typescript-eslint/eslint-plugin の README を読むと, @typescript-eslint/parser をインストールすることが求められています.

ただし eslint-plugin-vue のユーザーガイドには, eslint-plugin-vue はパーサとして vue-eslint-parser を用いるので, カスタムパーサを使いたいときは parser オプションではなく parserOptions.parser オプションで指定せよとの記述があります.

長々と見てきましたがこれで必要なものが揃いそうなので, すべてインストールして

npm install --save-dev eslint prettier eslint-plugin-prettier eslint-config-prettier @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-vue

すべての設定を融合させましょう:

.eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:vue/recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:prettier/recommended",
    "prettier/@typescript-eslint",
    "prettier/vue"
  ],
  "plugins": [
    "@typescript-eslint",
    "prettier",
    "vue"
  ],
  "parser": "vue-eslint-parser",
  "parserOptions": {
    "parser": "@typescript-eslint/parser",
    "project": "./tsconfig.json",
    "extraFileExtensions": [".vue"]
  },
  "rules": {
    "prettier/prettier": "error"
  }
}

VSCode の拡張機能

上記の eslint-plugin-vue のユーザーガイドには Editor integrations という項があり, VSCode で ESLint 拡張機能を .vue ファイルに対しても適用するための設定が書かれています.
また Vetur プラグインを使っている場合は "vetur.validation.template": false を設定せよとあります.

私はそれに加えてファイル保存時に ESLint の auto fix をかけるよう設定したので, .vscode/settings.json は以下のようになりました:

.vscode/settings.json
{
  "eslint.validate": [
      "javascript",
      "javascriptreact",
      { "language": "vue", "autoFix": true },
      { "language": "typescript", "autoFix": true }
  ],
  "eslint.autoFixOnSave": true,
  "vetur.validation.template": false
}

ファイル保存時に自動整形がかかる体験は最高で, コードフォーマッティングに関する些事を人間が気にしなくてよくなり最高.
デフォルトの printWidth: 80 だけは少し窮屈すぎたのでそこだけ広げましたが, その上で prettier が改行すると判断したのならそれは正しいのです.

(とはいえテトリミノの回転を配列で定義して, 見やすいように手で整形したのに prettier が押しつぶそうとしてくる箇所だけは disabled にしましたが...)

実装フェーズ

ようやく開発環境が整ったので, テトリスの実装に入ることができます.

「『小さく、自己完結的で、(多くの場合)再利用可能なコンポーネント』を組み合わせることで、大規模アプリケーションを構築する」2のが Vue のスタイルなので, 必要なコンポーネントについて考えると

  • メインとなる盤面 (ミノが落ちたり回転したり消えたりするところ)

  • NEXT ミノをプレビューできる部分

  • ホールド中のミノを表示する部分

  • 現在のレベルと得点を表示する部分

ぐらいがあれば良さそうです.

するとおおまかな構成は以下のようになるでしょう:

dist/index.html
<!doctype html>
<html>
  <head>
  </head>
  <body>
    <div id="app"></div>
    <script src="./build.js"></script>
  </body>
</html>
src/index.ts
import Vue from "vue"
import TetrisComponent from "components/Tetris.vue"

new Vue({
  el: "#app",
  render: h => h(TetrisComponent)
})
src/components/Tetris.vue
<template>
  <div>
    <div id="tetris-component">
      <hold class="inline-block" />
      <play-field class="inline-block" />
      <div class="inline-block">
        <next-preview />
        <level-score />
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { Vue, Component } from "vue-property-decorator"
import Hold from "components/Hold.vue"
import PlayField from "components/PlayField.vue"
import NextPreview from "components/NextPreview.vue"
import LevelScore from "components/LevelScore.vue"

@Component({
  components: {
    Hold,
    PlayField,
    NextPreview,
    LevelScore
  }
})
export default class TetrisComponent extends Vue {}
</script>

<style>
#tetris-component {
  text-align: center;
}
.inline-block {
  display: inline-block;
  vertical-align: top;
}
</style>

テトリス実装の基本概念

テトリスのロジック自体は素朴な実装にしました.

盤面

盤面として幅 (10 + 2) * 高さ (20 + 2) の 2 次元配列を 2 つ用意し, 1 つは色情報を, もう 1 つはブロックが埋まっているか否かの情報を格納するのに使います.

幅と高さを +2 しているのは外壁, いわゆる番兵というやつです. 色と埋まりの配列を分けずにもっと統一的に扱っても良いかもしれませんが, そこは好みの問題だと思います.

ミノ

テトリスの主役であるところのミノについては, ミノの型を定義して export default interface Tetromino し, すべてのミノの名前と色と回転パターンを記述した Tetrominos: Tetromino[] を定義しておきます:

src/Tetrominos.ts
import Tetromino from "typings/Tetromino"
export const Tetrominos: Tetromino[] = [
  {
    name: "I",
    color: "red",
    blocks: [
      [ [1, 1, 1, 1] ],
      [
        [1],
        [1],
        [1],
        [1]
      ]
    ]
  },

  {
    name: "O",
    color: "gold",
    blocks: [
      [
        [1, 1],
        [1, 1]
      ]
    ]
  },
  .
  .
  .
]

この Tetrominos のインデックスでミノの種類を表現できるというわけです.

操作

ミノを移動させたり回転させたりといった操作を行うときは単純に, その操作後のミノが既に埋まっているマスとぶつからないかどうかを検査し, ぶつからなければ操作可能, ぶつかるなら操作不可能とすれば良さそうです.

ただし moveDown() だけは特別で, それ以上ミノを下に動かせないということはミノが接地したということ, すなわち現在のターンが終了したということを示します. なので揃ったラインを消すことを始めとした様々な終了処理を行う必要が出てくるでしょう.

ラインの消去

テトリスというゲームの命 (?) であるところのライン消去機能を作るには, Array.prototype.splice() がいい感じに使えるでしょう.
ミノを接地した結果すべての列が埋まったラインが出来たならば, 上から順番にそのラインの index について splice(index, 1) すれば良いのです. そして上記の 2 次元配列の先頭には空っぽの unitLine を都度 splice(1, 0, unitLine) して詰めてあげれば良いでしょう (index が 1 なのは一番上の 0 行目が壁だからです).

ホールド

ホールドとはつまり現在の tetrominoIndexholdTetrominoIndex とを交換するという操作です. ただし初回だけはホールドされているミノが無いので, NEXT のミノを盤面に呼ぶ必要があるでしょう. なので holdTetrominoIndex の初期値は -1 とか (インデックスとしてありえない値) にしておいて, それで初回の判定をします.

またホールドは 1 ターンにつき 1 回しかできないので, isHoldTetrominoUsed のような変数を用意して, ホールドしてからミノが接地するまでは isHoldTetrominoUsed = true としてロックしておく等の処理が必要です.

余談: Math.floor(Math.random() * 7) を捨てる

出現するミノをランダムにするために Math.floor(Math.random() * 7) の値を使おうと思う方がいるかもしれませんが, やめたほうが良いです.

代わりに 「7 種類のミノが 1 つずつ入った集合の中から, 空っぽになるまで 1 つずつランダムに取り出す」ことを繰り返すようにしてください.

実際にやってみるとわかるのですが, ナイーブな Math.floor(Math.random() * 7) ではとても遊べるゲームになりません. 動作確認用くらいにとどめておくべきです. これは重要なポイントなのでぜひ覚えておいてください.

今回私は Fisher–Yates アルゴリズムというシャッフルアルゴリズムを使いました:

Algorhythms.ts
// Fisher–Yates shuffle algorithm
export const shuffle = function(array: number[]): number[] {
  for (let i: number = array.length - 1; i > 0; i--) {
    const j: number = Math.floor(Math.random() * (i + 1))
    ;[array[i], array[j]] = [array[j], array[i]]
  }
  return array
}
TetrominoIndices.ts
import { Tetrominos } from "Tetrominos"
import { shuffle } from "Algorhythms"

// ...
  nextTetrominoIndicesSet: number[] = shuffle(Array.from(Array(Tetrominos.length).keys()))
// ...

Vuex

さてこのまま props$emitmethods を駆使してコードを書いていっても良いのですが (実際私も最初はそうしていました), だんだんと状態変数が増えてきて管理が辛くなっていきます.
そこで途中から Vuex による状態管理に移行することにしました (今からやり直すなら最初から Vuex を使うと思います).

Vuex や Flux 自体についてはほかに良い記事がたくさんあると思うのであえて私がここで拙い解説を書くことはしませんが, Vuex の公式ドキュメントを読んでいけば

  • 状態 (state) は Store と呼ばれるグローバルシングルトンの中に保持する

  • 状態を変更する唯一の方法は mutation をコミットすることである

  • mutation は同期的な処理だけを行う

  • 非同期処理を行ったり複数の mutation をコミットしたりするためには action を定義し, その actiondispatch することで実行する

  • 状態は getter で取り出す

ことが基本のスタイルであると読めます.

さらに Vuex で TypeScript を型安全に使うために vuex-module-decorators を利用しました.
これについては以下の記事が大変参考になりました:

コンポーネントの中で this.$store と書かなくてよくなったり mutation 名や action 名を文字列で一字一句間違えずに書かずに済むようになったりというのはこの記事のとおりです.

コーディングスタイル

Vuex の公式ドキュメントには「ミューテーション・タイプに定数を使用する」というオプションが提示されており, 上記の記事でもそのスタイルだったので, 今回は私もそれに則ることにしました.

また vuex-module-decorators を使うと VuexModule を TypeScript の class として書くことになり, 各プロパティやメソッドにアクセス指定子をつけることができます.
私は Vuex Store が外部 (Store を利用する Vue のコンポーネント) に公開すべきなのは getteraction だけであるとドキュメントからエスパーにより読み取った (?) ので, statemutationprivate にし, getteractionpublic としました.

こうなるとテトリスを実装する上での主要なロジックはほとんど Vuex の action となり, Vue のコンポーネントからは props が消え去りました. コンポーネントに残ったのは少しの内部変数と mounted フック, それに action を制御する薄いメソッド, リアクティブな描画のために Store の getter をそのまま返す getter ぐらいです.

反省点

描画

何も考えずに Canvas 上でゲーム画面を描画するものとしてコードを書き始めてしまったのですが, そうすると例えば

import { Tetrominos } from "Tetrominos"

// class PlayField extends Vue とか VuexModule とか { 
// ...

  // 適当な値です
  tetrominoIndex: number = 0
  currentX: number = 5
  currentY: number = 1
  rotation: number = 0

  unitWidth: number = 10
  unitHeight: number = 10

  drawTetromino(): void {
    const canvas: HTMLCanvasElement = document.getElementById("canvas") as HTMLCanvasElement
    const context = canvas.getContext("2d")

    context.strokeStyle = "lightgray"
    context.fillStyle = Tetrominos[this.tetrominoIndex].color

    for (const [dy, row] of Tetrominos[this.tetrominoIndex].blocks[this.rotation].entries()) {
      for (const [dx, blockElement] of row.entries()) {
        if (blockElement != 0) {
          context.fillRect(
            (this.currentX + dx) * this.unitWidth,
            (this.currentY + dy) * this.unitHeight,
            this.unitWidth,
            this.unitHeight
          )
          context.strokeRect(
            (this.currentX + dx) * this.unitWidth,
            (this.currentY + dy) * this.unitHeight,
            this.unitWidth,
            this.unitHeight
          )
        }
      }
    }
  }

// ...
// }

的なコードを書くことになり, fillRect() がいかにも手続き的です.
だいぶ後になってから気がつきましたが, せっかく属性に値を bind できるのだから SVG で描画したほうが良かったかもしれません. こんど何か図形を描画して動かすものを作るときは SVG の利用を検討しようと思います.

そういう感じです

このテトリスを作っている間に Nintendo Switch で Tetris 993 が配信開始されました. この Tetris 99 は Nintendo Switch Online に加入していれば無料で遊べるし, 現代的な要素ももりもり詰まっています.
私はなるべくクラシックなテトリスに近いものを作って遊ぼうというモチベーションがあったから良かったものの, もし現代的テトリスを目指して作っていたら, べつに Tetris 99 とかいう良くできたゲームがあるしわざわざ俺がちまちま作んなくてもいいじゃん...という気持ちになって途中で心が折れていたかもしれません. そういう意味では運良く開発できたなあと思っています.

そういうわけですので皆さんも Tetris 99 をやりましょう.

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