20210412のvue.jsに関する記事は9件です。

①Vue.js & Laravel(環境構築)

こちらは10本の記事で構成されています ①Vue.js & Laravel(環境構築) ②Vue.js & Laravel(CRUDシステムの準備) ③Vue.js & Laravel(Read...データベースからデータを取得) ④Vue.js & Laravel(Create...データベースにデータを保存) ⑤Vue.js & Laravel(Update…データベースのデータを更新) ⑥Vue.js & Laravel(Delete…データベースのデータを削除) ⑦Vue.js & Laravel(リアルなダミーデータを大量に挿入) ⑧Vue.js & Laravel(検索機能の実装) ⑨Vue.js & Laravel(ページャーの実装) ⑩Vue.js & Laravel(認証機能の実装) 【目標】LaravelにVue.jsを埋め込んだフォルダを作成し、SPAの土台を作る 前提①パソコンにnodeがインストールされている こちらのコマンドで確認しましょう $ node -v v14.16.1 もしインストールされていない場合はこちらからインストール 前提②パソコンにcomposerがインストールされている こちらのコマンドで確認しましょう $ composer --version Composer version 2.0.12 もしインストールされていない場合は(MAC) $ curl -sS https://getcomposer.org/installer | php $ sudo mv composer.phar /usr/local/bin/composer これでインストール完了です。 【手順】①フォルダの作成②データベースへの接続③Vue.jsとLaravelの接続 ①フォルダの作成 $ cd [フォルダを作りたい階層] (フォルダを作りたい階層に移動) $ composer create-project "laravel/laravel=7.*" hoge (「hoge」という名前でフォルダの作成) $ cd hoge (作成したフォルダの中に移動) $ composer require laravel/ui:2 (UIの様々な機能導入) $ php artisan ui vue (vue導入) $ php artisan ui vue --auth (ログイン機能導入) $ npm install --save vue-router (vueルーター導入) $ npm install (npm導入) それではサーバーを立ち上げて確認してみましょう! $ npm run watch (フォルダに変更がある度にビルド) $ php artisan serve (サーバーを立ち上げる) ターミナルを2枚開き上記のコマンドを実行してください。 上記の2つのコマンドは開発中常に実行させます。 表示されたURLにアクセスしウェルカム画面が表示されれば成功! これで「①フォルダの作成」は完了です! ②データベースへの接続 .env DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=8889 DB_DATABASE=laravel DB_USERNAME=root DB_PASSWORD=root 上記の部分を自身のデータベースの環境に合わせ書き換えましょう $ php artisan migrate:refresh --seed こちらを実行しテーブルが作成されれば「②データベースへの接続」が完了です ③ルーティングを設定しvueのexampleコンポーネントを表示させる 不要ファイルの削除、必要ファイルの作成 下記のコマンドでフォルダを整理します $ rm resources/views/home.blade.php $ rm resources/views/welcome.blade.php $ rm app/Http/Controllers/HomeController.php $ rm resources/js/components/ExampleComponent.vue $ touch resources/views/app.blade.php $ touch resources/js/components/AppComponent.vue $ touch resources/js/components/Header.vue $ mkdir resources/js/components/task $ touch resources/js/components/task/Task.vue $ mkdir resources/js/components/search $ touch resources/js/components/search/Search.vue $ mkdir resources/js/components/user $ touch resources/js/components/user/User.vue resources/routes/web.php <?php use Illuminate\Support\Facades\Route; - /* - |-------------------------------------------------------------------------- - | Web Routes - |-------------------------------------------------------------------------- - | - | Here is where you can register web routes for your application. These - | routes are loaded by the RouteServiceProvider within a group which - | contains the "web" middleware group. Now create something great! - | - */ Route::get('/', function () { - return view('welcome'); + return view('app'); }); Auth::routes(); - Route::get('/home', 'HomeController@index')->name('home'); resouces/views/app.blade.php <!doctype html> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="csrf-token" content="{{ csrf_token() }}"> <title>{{ config('app.name', 'Laravue') }}</title> <link href="{{ mix('/css/app.css') }}" rel="stylesheet"> </head> <body> <div id="app"> <app-component></app-component> </div> <script src="{{ mix('/js/app.js') }}" defer></script> </body> </html> resouces/js/components/App.vue <template> <div class="appComponent"> <Header /> <div class="container"> <router-view /> </div> </div> </template> <script> import Header from "./Header.vue"; export default { components: { Header, }, }; </script> <style> button, input{ border: 1px solid black; padding: 0 5px; } </style> resouces/js/components/Header.vue <template> <div class="header"> <div class="header_nav"> <router-link to="/task">task</router-link> <router-link to="/search">search</router-link> <router-link to="/user">user</router-link> </div> </div> </template> <style scoped> .header { text-align: center; background-color: #000066; margin-bottom: 50px; } .header_nav { padding: 30px; } .header_nav a { color: white; text-decoration: none; font-size: 25px; margin-right: 25px; } .header_nav a:last-child { margin-right: 0; } .header_nav a.router-link-active { color: #f3920b !important; } </style> resouces/js/components/task/Task.vue <template> <div class="taskComponent"> タスク画面 </div> </template> <script> export default { data() { return { }; }, methods: { }, mounted() { }, }; </script> <style lang="scss" scoped> </style> resouces/js/components/search/Search.vue <template> <div class="searchComponent"> 検索画面 </div> </template> <script> export default { data() { return { }; }, methods: { }, mounted() { }, }; </script> <style lang="scss" scoped> </style> resouces/js/components/user/User.vue <template> <div class="taskComponent"> ユーザー画面 </div> </template> <script> export default { data() { return { }; }, methods: { }, mounted() { }, }; </script> <style lang="scss" scoped> </style> resouces/js/app.js - /** - * First we will load all of this project's JavaScript dependencies which - * includes Vue and other libraries. It is a great starting point when - * building robust, powerful web applications using Vue and Laravel. - */ + import VueRouter from 'vue-router'; require('./bootstrap'); window.Vue = require('vue'); - /** - * The following block of code may be used to automatically register your - * Vue components. It will recursively scan this directory for the Vue - * components and automatically register them with their "basename". - * - * Eg. ./components/ExampleComponent.vue -> <example-component></example-component> - */ - // const files = require.context('./', true, /\.vue$/i) - // files.keys().map(key => Vue.component(key.split('/').pop().split('.')[0], files(key).default)) - Vue.component('example-component', require('./components/ExampleComponent.vue').default); + Vue.component('app-component', require('./components/AppComponent.vue').default); + Vue.use(VueRouter); + const router = new VueRouter({ + routes: [ + { + path: '/task', + name: 'task', + component: () => import('./components/task/Task.vue'), + }, + { + path: '/search', + name: 'search', + component: () => import('./components/search/Search.vue'), + }, + { + path: '/user', + name: 'user', + component: () => import('./components/user/User.vue'), + }, + ] + }); - /** - * Next, we will create a fresh Vue application instance and attach it to - * the page. Then, you may begin adding components to this application - * or customize the JavaScript scaffolding to fit your unique needs. - */ const app = new Vue({ el: '#app', + router }); こちらの画面が表示されれば成功です 【達成】今回の目標「LaravelにVue.jsを埋め込んだフォルダを作成し、SPAの土台を作る」が達成されました。 次回↓↓↓↓②Vue.js & Laravel(CRUDシステムの準備)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VueTyperを実装

Vue.jsで実装した機能を備忘録として記録する。 VueTyperとは タイピングをしてるかのようにテキストを表示するVue.js用のコンポーネントライブラリ。 前提条件 ・Vue-CLIをインストールしていること ※CDNでも実装可能ですが、今回はVue-CLIを使用して進めています。 インストール npm install --save vue-typer ライブラリの読み込み js import { VueTyper } from 'vue-typer' コンポーネントに取り込み js export default { components: { VueTyper } } テンプレートに記述 template <template> <div> <vue-typer :pre-type-delay='1500' :type-delay='100' :repeat='0' text="表示させたい文字を入力"> </vue-typer> </div> </template> デモページ 下記ページで動きを確認することができます。 https://cngu.github.io/vue-typer/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails6 + Vue.js】ブックマークを管理する簡単なSPAを作ってみた話(第四章:ローディング表示、ページネーション、ユーザー管理機能の実装)

前回の記事の続きになります。 前回の記事はこちら↓ 本記事の概要 今回実装する機能について 【ローディング表示機能】 vue-loading-template を導入してローディングを表示させます。 【ページネーション機能】 gem で kaminari を導入して実装します。 【ユーザー管理機能】 今回は devise で実装します。 記事構成 本アプリケーションについての記事は大きく4部構成で作成しております。 第一章:アプリケーション作成〜ダミーデータ表示 第二章:ブックマークのCRUD実装 第三章:フリーワード検索、カテゴリー別絞り込み機能の実装 第四章:本記事(ローディング表示、ページネーション、ユーザー管理機能の実装) 本記事の参考URL 本記事の作成にあたって主に以下のドキュメント・記事を参考にさせて頂きました。 【Vue.js】ローディング画面の実装方法(サンプルコード付き) vue-loading (Github) 【rails】kaminariを使ってページネーションを作る Rails + Vue.js でページネーション付きのテーブルを簡単作成 実装 それでは実装に移ります。 ローディング表示機能 vue-loading-templateを導入して実装します。 参考記事↓ % yarn add vue-loading-template app.vueを編集 app/javascript/app.vue <template> <v-app id="app"> <!-- ローディングを表示 --> <Loading v-show="isLoading"></Loading> <v-container> <!-- 中略 --> </v-container> </v-app> </template> <script> import draggable from 'vuedraggable' import Loading from './components/Loading' // Loading コンポーネントをインポート import axios from 'axios'; export default { data: function () { return { isLoading: true, bookmarkList: ['',''], allData: ['',''], ... } }, mounted () { axios.get("/api/bookmarks") .then(response => { this.allData = response.data; this.isLoading = false; // マウントしたらfalseに変更 } ); this.setBookmark(); }, components: { Loading, // Loagingコンポーネントを追加 draggable, }, methods: { ... } } </script> components フォルダ配下に Loading.vue を設置して実装 app/javascript/components/Loading.vue <template> <div v-show="isLoading"> <div class="fullview"> <div class="loading-spacer"></div> <!-- spiningDubblesというタイプのローディングを実装 --> <vue-loading type="spiningDubbles" color="#2f8af0" :size="{ width: '100px', height: '100px' }" > </vue-loading> </div> </div> </template> <script> import { VueLoading } from 'vue-loading-template' export default { name: 'isLoading', data() { return { isLoading: true, } }, components: { VueLoading, }, } </script> <style> .fullview { width: 100%; height: 100%; background: rgba(254, 254, 254, 0.8); position: fixed; top: 60px; left: 0; z-index: 5; } .loading-spacer { height: 30%; } </style> 以上でローディング表示機能の実装は終了です。 ページネーション機能 kaminariを導入して実装します。 参考記事↓ gem 'kaminari' % bundle install bookmarks_controllerの編集 app/controllers/api/bookmarks_controller.rb class Api::BookmarksController < ApplicationController def index page = params[:page] || 1 # 表示するページの指定 per = params[:per] || 30 # 1ページあたり30件のブックマークを表示 @bookmarks = Bookmark.order('created_at DESC').page(page).per(per) total_pages = @bookmarks.total_pages # 全ページ数を取得 response = { bookmarks: @bookmarks, total_pages: total_pages, } render json: response end end app.vueの編集 【 Vue.js の編集】 app/javascript/app.vue <script> export default { data: function () { return { isLoading: true, bookmarkList: ['',''], ... currentPage: 1, // 表示するページの番号 itemsPerPage: 30, // 1ページあたりの表示件数 totalPages: null, // ページネーションした時の全ページ数 } }, ... methods: { setBookmark: function () { // Axios で api にリクエスト const url = `/api/bookmarks?page=${this.currentPage}?per=${this.itemsPerPage}`; axios.get(url) .then(response => { // ブックマークの全データを取得 this.allData = response.data.bookmarks this.bookmarkList = this.allData // ページネーションの全ページ数を取得 this.totalPages = response.data.total_pages this.listCategories(); this.abstruct(); } ); }, ... } } </script> 【template の編集】 app/javascript/app.vue <template> <v-app id="app"> <Loading v-show="isLoading"></Loading> <v-container style="height: 1000px; max-width: 2400px; padding: 0 20px;"> <v-layout> <v-flex xs8> <div style="width: 100%; margin: 5px 0 20px 0; display: flex; justify-content: center;"> <h1>Bookmark 一覧</h1> </div> <v-layout> <v-flex row wrap style="justify-content: center;"> <draggable v-model="bookmarkList" style="margin: 0 25px; width: 80%; cursor: pointer;"> <v-card v-for="bookmark in bookmarkList" :key="bookmark.id" :items-per-page="itemsPerPage" style="width: 100%"> <v-card-title primary-title style="margin-bottom: 15px; width: 100%; padding-bottom: 10px;"> <div style="width: 100%;"> <div class="headline mb-0" style="display: flex; justify-content: space-between; width: 100%"> <a v-bind:href="bookmark.url" target="_blank" rel="noopener noreferrer" style="font-size: 18px;"> {{ bookmark.title }} </a> <v-tooltip right> <template v-slot:activator="{ on }"> <v-btn light v-on="on" @click="togglePutModal(bookmark.id)" style="margin-bottom: 8px"> <span class="material-icons" style="margin-right: 4px;">create</span> </v-btn> </template> <span>編集する</span> </v-tooltip> </div> <v-divider></v-divider> <div style="font-size: 16px; display: flex; justify-content: space-between; width: 100%"> <div>#{{ bookmark.category }}</div> <v-tooltip right> <template v-slot:activator="{ on }"> <v-btn dark v-on="on" @click="toggleDeleteModal(bookmark.id)" style="margin-top: 8px"> <span class="material-icons" style="margin-right: 4px;">delete</span> </v-btn> </template> <span>削除する</span> </v-tooltip> </div> </div> </v-card-title> </v-card> </draggable> </v-flex> </v-layout> <!-- ページネーション表示部分を追加 --> <div class="text-xs-center" style="margin: 20px 0 40px 0;"> <v-pagination v-model="currentPage" :length="totalPages" @input="setBookmark" ></v-pagination> </div> </v-flex> </v-layout> </v-container> </v-app> </template> 以上でページネーション機能の実装は終了です。 ユーザー管理機能の実装 deviseを導入して実装します。(新規登録ページやログインページの実装に関しては割愛します。) 本来は最初に実装するべきですが、今回はブックマークの機能(Vue.js)を主な目的としているため後から実装しています。 gem 'devise' bundle installしてdeviseをインストール % bundle install % rails g devise:install bookmarkのマイグレーションファイルをロールバックして編集 db/migrate/2021xxxxxxxxx2_create_bookmarks.rb class CreateBookmarks < ActiveRecord::Migration[6.0] def change create_table :bookmarks do |t| t.string :title, null: false t.string :url, null: false t.string :category, null: false t.references :user, foreign_key: true t.timestamps end end end userモデルの作成 % rails g devise user db/migrate/2021xxxxxxxxxx1_create_users.rb class DeviseCreateUsers < ActiveRecord::Migration[6.0] def change create_table :users do |t| ## Database authenticatable t.string :nickname, null: false, default: "" t.string :email, null: false, default: "" t.string :encrypted_password, null: false, default: "" ## Recoverable t.string :reset_password_token t.datetime :reset_password_sent_at ## Rememberable t.datetime :remember_created_at t.timestamps null: false end add_index :users, :email, unique: true add_index :users, :reset_password_token, unique: true end end % rails db:migrate application_controllerの編集 app/controllers/application_controller.rb class ApplicationController < ActionController::Base # 登録に必要なストロングパラメータを設定 before_action :configure_permitted_parameters, if: :devise_controller? # ユーザーがログインしていない場合はログインページを表示 before_action :authenticate_user! private def configure_permitted_parameters devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname]) end end それぞれのモデルを編集(アソシエーションの記述) app/models/user.rb class User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable has_many :bookmarks # 一人のユーザーは複数のブックマークのデータを扱う # バリデーション設定 validates :nickname, presence: true PASSWORD_REGEX = /\A(?=.*?[a-z])(?=.*?[\d])[a-z\d]+\z/i.freeze validates_format_of :password, with: PASSWORD_REGEX, message: 'には英字と数字の両方を含めて設定してください' end app/models/bookmark.rb class Bookmark < ApplicationRecord belongs_to :user with_options presence: true do validates :url, format: {with: /\A#{URI::regexp(%w(http https))}\z/} end end bookmarks_controllerの編集 app/controllers/api/bookmarks_controller.rb class Api::BookmarksController < ApplicationController protect_from_forgery :except => [:create, :update, :destroy] def index page = params[:page] || 1 per = params[:per] || 30 # ログインしているユーザーのブックマーク情報を取得 @bookmarks = Bookmark.where(user_id: current_user.id).order('created_at DESC').page(page).per(per) total_pages = @bookmarks.total_pages response = { bookmarks: @bookmarks, total_pages: total_pages, } render json: response end end 以上でユーザー管理機能の実装は終了です。 また、今後学習を深めてJsonWebToken(JWT)によるユーザー認証機能の実装にも挑戦したいと思います。 最後に 以上でブックマーク個人管理アプリケーションの実装は全て終了です。 いかがだったでしょうか? 未だ記事を書くことに慣れていない為分かりづらい部分や読みにくい部分があるかもしれませんが今後修正点や改善点を気付き次第修正をしていこうと思います。 読んでいただき、ありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react、vue、next.jsなどの簡易的環境構築〜codesandbox〜

完全に私的な備忘録として記載しています。 ご理解のほど、よろしくお願いします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Composition-apiにおける動的コンポーネント生成

templateタグ部分をJSの文字列で動的に生成したかった 私自身がこれを実現する必要に迫られて習得した内容なので復習と備忘録の意味合いでも記録を残したいと思います。 Vue.jsの *.vueファイルの構成は // テンプレート部 <template> ... </template> // コード部 <script> ... </script> // スタイル部 <style> ... </style> のようにテンプレート部、コード部、スタイル部の大きく3つの構成要素で成り立たせる事が一般的かつ標準的な利用方法かと思います。 しかしながら、状況によってはテンプレート部をJSで動的に記述したいというシチュエーションも出てくると思います。 例えばロジックは共通だが、テンプレートだけ差し替えて見た目の制御をおこないたいといった場合、 又は、バックエンドからHTMLのコードを受信してレンダリングさせたいがv-htmlを利用するとXSSのリスクがあるので、 Vueにレンダリングされるより前にVueのテンプレートとして動的にテンプレートを記述したい。 といった場合などが例であると考えます。 以下のような関数を用いる事で、動的にコンポーネントを生成する事が可能となります。 ※.以下のサンプルコードはVue2でComposition-apiを利用しているサンプルとしてimportで@vue/composition-apiを読み込んでおります。 // dynamicComponentGenerator.ts <script type="ts"> import { defineComponent, PropType, Ref, ref, computed } from '@vue/composition-api' import { Component } from 'vue' type ClickableText = { text: string clickHandler: ((event: Event) => void) | null } type SetupReturnType = { clickableText: Ref<ClickableText> } type LinkText = { text: string } const props = { linkText: { type: Object as PropType<LinkText>, required: false, default: '', }, } export const dynamicComponentGenerator = (componentName: string, template: string): Component => defineComponent({ componentName, props, emits: ['on-dynamic-text-clicked'], setup(props, { emit }): SetupReturnType { const clickableText = computed(() => { const obj: ClickableText = { text: '', clickHandler: null} if( props.linkText?.text !== '' ){ obj.text = props.linkText?.text obj.clickHandler = emit('on-dynamic-text-clicked') } return obj } return { clickableText, } } template, }) </script> このような感じで 戻り値の型をComponentとして引数にcomponentName(コンポーネント名)とtemplate(動的に生成したテンプレートの文字列)を受け取る事で 動的コンポーネントを生成する事が可能となります。 この例ではpropsで親コンポーネントからリンク用のlinkTextを設定して渡せば それがクリッカブルになり、固定のemitが親コンポーネントへ発火されます。 ここでは固定処理にしていますがpropsで渡された内容に応じて発火イベントとなるemitを新たに設定して 切り替えても良いですし、emitにパラメータを渡して処理を分岐したりしても良いかと思います。 ※). ここでpropsのlinkTextをStringではなく、Objectにしているのは深い意味はありません。Stringでも何ら問題ありません。 定義だけだと分かりにくいかと思いますので実際に関数を呼び出して、動的コンポーネントを利用してみましょう。 // index.vue <template> <div> <component :is="getLinkComponent(true)" @on-dynamic-text-clicked="linkClick" /> <component :is="getLinkComponent(false)" /> </div> </template> <script type="ts"> import { defineComponent } from '@vue/composition-api' import { dynamicComponentGenerator } from './dynamicComponentGenerator.ts' import { Component } from 'vue' const LinkComponent = dynamicComponentGenerator( 'LinkComponent', '<span><a @click="clickableText.clickHandler">{{clickableText.text}}</a></span>' ) const NoNLinkComponent = dynamicComponentGenerator( 'NoNLinkComponent', '<span>{{clickableText.text}}</span>' ) export default defineComponent({ name: 'loadDynamicComponent', setup() { return { getLinkComponent: (isActive: boolean): Component { if( isActive ) return LinkComponent return NoNLinkComponent }, linkClick: () { console.log('動的に生成されたリンクがクリックされました。') } } } }) </script> 実用レベルの例にすると複雑化して理解しにくくなると考え、出来る限りシンプルに例示したので 正直このサンプルそのものは何の役にも立たないサンプルだと思いますが このサンプルに動的生成の全ての基本が詰まっていると考えています。 ポイントはsetupの中でreturnしているgetLinkComponentというメソッドですが、このメソッドはコンポーネントを生成する関数(dynamicComponentGenerator)を実行して生成されたコンポーネントのインスタンスを戻り値として返却しているのが注視すべき点になるかと思います。 (※.コンポーネント名ではない事に注意) 更に言及すると、dynamicComponentGeneratorの実行部分の第2引数で記述されているStringの中の clickable.clickHandlerとclickHandler.textはdynamicComponentGenerator.tsで定義されている dynamicComponentGeneratorが返却するコンポーネントのsetup関数の戻り値になります。 これらのテキストが解釈されるのは、Vueによりテンプレートがレンダリングされた後になり どのタイミングでどの部分がどのように解釈されるのかが一見すると理解しづらいのですが、 このコードを理解する上で重要になるので、その点に着目してコードを読み返していただけるとよいかと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Toml & Python & PHP & vue & levelDB でSSL証明書の有効期限を毎日チェック

前置き 我々のような受託システム開発会社は、開発依頼を受ける度に、さまざまなドメインにそのシステムをロンチしていきます。 常時SSLが当たり前となった今日において、そのドメインの数だけSSL証明書が必要になります。 そしてSSL証明書にはLet's encryptを採用するケースも多いため、その更新期限管理は定期的なチェックが欠かせません。 (バッチで自動更新は当然としても) うちではもうなんだかんだと管理している客先ドメインが100超になっているので、日次バッチで全ドメインの有効期限を取得して、これが一覧画面から確認できるようにしておくことで、ある日突然サイトにアクセスできなくなって大慌てで証明書を更新しなけれけばならないような事態に未然対策しています。 本題 というのはまあいいとして、ここでの趣旨は、このようなワンオペサービスの裏側で実装される数々の周辺技術を束ねてひとつのアプリケーションとして構築する醍醐味感、のようなものになると思います。 目的はひとつでもそれを実現するための中間過程はいくつも存在し、それぞれを実現するための手段も多岐に及ぶため、適材適所な取捨選択をしていくのであれば応用技術の醍醐味がやってくるのは当然なのです。 登場人物 crontab スクリプトを定期実行されるためのスケジューラ。実体はシェル ここではドメインの有効期限を日次バッチで取得するための設定に使用 levelDB キーバリューストアな軽量ストレージ。使用頻度の高いものを優先に階層的なインデックスをおこなうのが特徴 ここでは日次バッチで取得した各ドメインの有効期限をFQDN単位で記録するのに使用 openSSL SSL証明書等の暗号プロトコルを触るためのデファクトスタンダード ここでは外部SSL証明書のパースに使用 python 昔からあるけど最近超花形言語として返り咲いた万能言語(3.7からハッシュが順番を保証してくれるようになったので私も3.7から大好き) ここではcrontabをトリガーにopenSSLで取得した有効期限をlevelDBに登録するためのバッチスクリプトとして使用 PHP 昔からあるけど今でも人気なWEB開発言語(私はずっと大好き) ここではバッチで記録した全ドメインの有効期限を後述のVueにAPIとして提供するのに使用 Vue JSフレームワークの雄。SPAや簡単なアプリケーションなら相性抜群 ここではフロントエンドの更新期限一覧画面として使用 nginx スクリプトをWEB通信規格を経由して提供するためのミドルウェア ここではPHPとVueをそれぞれインターフェースするのに使用 toml 設定ファイルのマークアップ言語。可読性がよく様々な言語にもライブラリが提供されているが、まだダークホース感有 ここでは各言語を横断してDRYな設定を管理するのに使用 醍醐味 以下、さっそくです。 前提 pwd # 適宜読み替えてください /home/you/projects/ssl-check tree -L 2 -I node_modules # 最終的な成果物 +__ api | +__ composer.json | +__ index.php | L__ vendor +__ app | +__ README.md | +__ babel.config.js | +__ dist | +__ package.json | +__ public | +__ src | L__ vue.config.js +__ batch | L__ ssl_check.py +__ configs | L__ common.toml +__ logs +__ package.json L__ results +__ 000005.ldb +__ 000006.log +__ CURRENT +__ LOCK +__ LOG +__ LOG.old L__ MANIFEST-000004 crontab 5 4 * * * cd /home/you/projects/ssl-check/batch/ && /usr/local/bin/python3.7 ssl_check.py & > /dev/null 2>&1 levelDB # for PHP sudo apt-get install libleveldb-dev cd /usr/local/src/ git clone https://github.com/reeze/php-leveldb.git cd php-leveldb/ phpize ./configure --prefix=/home/you/.phpenv/versions/7.4.0/lib/php/leveldb --with-leveldb=/home/you/.phpenv/versions/7.4.0/include/php/include/leveldb --with-php-config=/home/you/.phpenv/versions/7.4.0/bin/php-config make make install vi ~/.phpenv/versions/7.4.0/etc/php.ini extension=/home/you/.phpenv/versions/7.4.0/lib/php/extensions/no-debug-non-zts-20180731/leveldb.so sudo service php-fpm restart # for python sudo pip install plyvel pip list | grep plyvel plyvel 1.3.0 toml # for PHP cd api/ composer require yosymfony/toml # for python sudo pip install toml pip list | grep toml toml 0.10.2 configs/common.toml APP_DIR = "/home/you/projects/ssl-check/" RESULT_DB = "results" CHECK_CMD = "openssl s_client -connect {fqdn}:443 -servername {fqdn} </dev/null 2>/dev/null | openssl x509 -text | grep \"Not After\"" STATUS_OK = 1 STATUS_NOTE = 2 STATUS_NG = 3 STATUS_ERROR = 4 # ↓適宜編集 [DOMAIN_LIST] "example.com" = [ "example.com", "test.example.com", ] "google.com" = [ "google.com", ] "yahoo.co.jp" = [ "yahoo.co.jp", ] python batch/ssl_check.py import logging import os import sys import logging import subprocess import datetime import calendar import pytz import dateutil.parser import pickle import click import plyvel import json import toml configs = toml.load(open('../configs/common.toml')) logger = logging.getLogger('Batch') logger.setLevel(10) fh = logging.FileHandler(configs['APP_DIR'] + 'logs/batch.log') logger.addHandler(fh) sh = logging.StreamHandler() logger.addHandler(sh) format = logging.Formatter('%(asctime)s - [%(levelname)s] (%(lineno)d) %(message)s') fh.setFormatter(format) sh.setFormatter(format) curr_tz = pytz.timezone('Asia/Tokyo') curr_ts = int(datetime.datetime.now().timestamp()) results = {}; index = 0; for brand in configs['DOMAIN_LIST']: logger.info(brand + ' >>> start') results[brand] = {} for fqdn in configs['DOMAIN_LIST'][brand]: try: limit_at = subprocess.check_output(configs['CHECK_CMD'].format(fqdn = fqdn), shell = True) index += 1 if limit_at != '': limit_at = str(limit_at).split(' : ')[1] limit_at = limit_at.split('\\n')[0] limit_at = dateutil.parser.parse(limit_at) limit_ts = calendar.timegm(limit_at.timetuple()) if limit_ts < curr_ts: status = configs['STATUS_NG'] elif limit_ts < curr_ts + (60 * 60 * 24 * 30): status = configs['STATUS_NOTE'] else: status = configs['STATUS_OK'] limit_at = limit_at.astimezone(curr_tz).replace(tzinfo=curr_tz) limit_at = str(limit_at).split('+')[0].replace('-', '/') results[brand][fqdn] = { 'index': index, 'status': status, 'limit_at':limit_at } else: logger.warning(fqnd + ' unable to load certificate') results[brand][fqdn] = { 'index': index, 'status': configs['STATUS_ERROR'], 'limit_at':None } except Exception as e: logger.warning(e) results[brand][fqdn] = { 'index': index, 'status': configs['STATUS_ERROR'], 'limit_at':None } logger.info(brand + ' <<< end') logger.debug(results) try: plyvel.destroy_db(configs['APP_DIR'] + configs['RESULT_DB']) result_db = plyvel.DB(configs['APP_DIR'] + configs['RESULT_DB'], create_if_missing=True) for brand in results: for fqdn, result in results[brand].items(): result_db.put(str(fqdn).encode(), json.dumps(result).encode()) except Exception as e: logger.warning(e) finally: result_db.close() PHP api/index.php <?php require_once 'vendor/autoload.php'; use Yosymfony\Toml\Toml; $configs = Toml::ParseFile(dirname(__FILE__) . '/../configs/common.toml'); $db = new LevelDB($configs['APP_DIR'] . $configs['RESULT_DB'], ['create_if_missing' => true]); $results = []; foreach ($configs['DOMAIN_LIST'] as $brand => $list) { isset($results[$brand]) or $results[$brand] = []; foreach ($list as $fqdn) { $result = $db->get($fqdn); $results[$brand][$fqdn] = $result; } } $checked_at = date('Y/m/d H:i:s', filemtime($configs['APP_DIR'] . $configs['RESULT_DB'])); echo json_encode(['list' => $results, 'checked_at' => $checked_at], JSON_UNESCAPED_UNICODE); Vue app/vue.config.js module.exports = { lintOnSave: false, publicPath: 'https://ssl-check.yourdomain.com/', devServer: { host: 'localhost', port: 8030, disableHostCheck: true, public: 'https://ssl-check.yourdomain.com/', } } app/package.json { "name": "app", "version": "0.1.0", "private": true, "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }, "dependencies": { "axios": "^0.21.1", "bootstrap": "^4.6.0", "bootstrap-vue": "^2.21.2", "core-js": "^3.6.5", "register-service-worker": "^1.7.2", "vue": "^2.6.11", "vue-meta": "^2.4.0" }, "devDependencies": { "@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-eslint": "~4.5.0", "@vue/cli-service": "~4.5.0", "babel-eslint": "^10.1.0", "eslint": "^6.7.2", "eslint-plugin-vue": "^6.2.2", "vue-template-compiler": "^2.6.11" }, "eslintConfig": { "root": true, "env": { "node": true }, "extends": [ "plugin:vue/essential", "eslint:recommended" ], "parserOptions": { "parser": "babel-eslint" }, "rules": {} }, "browserslist": [ "> 1%", "last 2 versions", "not dead" ] } app/src/configs/common.js export default Object.freeze({ CNAME: 'SSL証明書 更新状況一覧', CURL: 'https://ssl-check.yourdomain.com/', }); app/src/main.js import Vue from 'vue' import { BootstrapVue, IconsPlugin } from 'bootstrap-vue' import App from './App.vue' import axios from 'axios' import './registerServiceWorker' import VueMeta from 'vue-meta' Vue.use(VueMeta) Vue.use(BootstrapVue) Vue.config.productionTip = false Vue.prototype.$axios = axios new Vue({ render: h => h(App), }).$mount('#app') app/src/App.vue <template> <div id="app"> <Ssl/> </div> </template> <script> import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' import Configs from './configs/common' import Ssl from './components/Ssl.vue' export default { metaInfo: { title: Configs.CNAME, titleTemplate: Configs.CNAME, htmlAttrs: { lang: 'ja', amp: true }, meta: [ { charset: 'utf-8' }, { name: 'application-name', content: Configs.CNAME }, { name: 'robot', content: 'noindex,nofollow' }, { name: 'author', content: 'ketoha' }, { name: 'copyright', content: '&copy;ketoha' }, { name: 'og:site_name', content: Configs.CNAME }, { name: 'og:url', content: Configs.CURL }, { name: 'og:title', content: Configs.CNAME } ] }, name: 'App', components: { Ssl } } </script> <style> @import "../public/index.css"; </style> app/src/components/Ssl.vue <template> <div> <h1>SSL証明書 更新状況一覧<span>{{ checked_at }}</span></h1> <table> <thead> <tr> <th>#</th><th>##</th><th>ドメイン</th><th>FQDN</th><th>ステータス</th><th>期限終了日時</th> </tr> </thead> <tbody> <template v-for="(data, brand, pidx) in list"> <tr v-for="(result, fqdn, sidx) in data"> <td>{{ pidx+1 }}</td> <td>{{ sidx+1 }}</td> <td>{{ brand }}</td> <td>{{ fqdn }}</td> <td v-if="result.status"> <span v-if="result.status === 1" class="ok">正常</span><span v-if="result.status === 2" class="note">間近</span><span v-if="result.status === 3" class="ng">失効</span><span v-if="result.status === 4" class="error">失敗</span> </td><td v-else>-</td> <td v-if="result.limit_at">{{ result.limit_at }}</td><td v-else>-</td> </tr> </template> </tbody> </table> </div> </template> <script> export default { name: 'Ssl', data: function(){ return { list:{}, checked_at:null, is_error:false } }, mounted:function() { window.addEventListener('DOMContentLoaded', this.getList) }, methods: { getList:function(){ document.body.classList.add('loading') this.$axios.get('https://ssl-check.ketoha.xyz/api/').then(function(response){ this.list = {} this.checked_at = response.data.checked_at let list = response.data.list //console.log(list) for (let brand in list) { this.list[brand] = {} for (let fqdn in list[brand]) { this.list[brand][fqdn] = JSON.parse(list[brand][fqdn]) } } }.bind(this)).catch(function(error){ this.is_error = true }.bind(this)).finally(function(){ document.body.classList.remove('loading') }.bind(this) )} } } </script> nginx server { listen 80 default; server_name ssl-cjeck.yourdomain.com; return 301 https://$host$request_uri; } server { listen 443 ssl http2; server_name ssl-cjeck.yourdomain.com; ssl_certificate /etc/letsencrypt/live/ssl-check.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/ssl-check.yourdomain.com/privkey.pem; root /home/you/projects/ssl-check/app/dist; index index.html; access_log /home/you/projects/ssl-check/logs/access.log; error_log /home/you/projects/ssl-check/logs/error.log; location / { root /home/you/projects/ssl-check/app/dist; index index.html; } location /api/ { root /home/you/projects/ssl-check/api; index index.php; try_files $uri $uri/ /index.php?$args; } location ~ \.php$ { root /home/you/projects/ssl-check/api; index index.php; try_files $uri = 404; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass unix:/home/you/.phpenv/versions/7.4.0/var/run/php-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } } 最後に 適材適所した結果、毎日このような画面で更新期限管理ができるようになります。 ※画面にはないですが、ここでは有効期限が切れている場合は赤で、一ヶ月を切っている場合はオレンジで表示されるようになっています。 オレンジを検知した場合はメールで通知、とかするともっといいかもしれません。 技術の応用しかしていない身としては、日々OSSでソリューションを提供してくれる先人たちには感謝しかありません。 しっかり活用していきたいものですね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsで同一コンポーネントへの遷移なんだけど、transitionアニメーションは発生させたい

動機 下記のようにコンポーネントを切り替えている場合に、同一コンポーネントに遷移してtransitionアニメーションを発生させたい。 Main.vue <transition> <component :is="currentComponent"></component> </transition> 例えば、商品詳細画面で商品Aから商品Bに切り替える場合。 例えば、ページの下の方から上の方に戻るだけなんだけど、アニメーションを発生させないといけない場合。 コンポーネントにkey()を実装してみてもkey()はリアクティブでないので、値を変更しても反応しなくて困っている。 Component.vue export default { key() { // 商品IDなど、一意な値を返す } } 解決方法 <component>に:keyを設定する。 パターン1: 商品詳細画面で商品Aから商品Bに切り替える場合 Main.vue <transition> <component :is="currentComponent" :key="currentKey"></component> </transition> Main.vue export default { computed: { currentKey() { // 商品IDなど、一意な値を返す } } } パターン2: ページの下の方から上の方に戻るだけなんだけど、アニメーションを発生させないといけない場合 前提として、<component>には複数の異なるコンポーネントが入る。 そのうちの一つに関して、タイトルに示した要求がある。 Main.vue <transition> <component :is="currentComponent" :key="currentKey"></component> </transition> Main.vue export default { computed: { ...mapState(['currentKey']) } } Component.vue export default { methods: { ...mapMutations(['incrementCurrentKey']), moveToTop() { this.incrementCurrentKey(); // ここにトップに戻るアニメーションを実装 } } } currentKeyをstoreに突っ込んで、上に戻るボタンを押されたときに更新してやればよい。 ポイントとして、incrementCurrentKeyの呼び出しは上に戻るボタンを押したときだけでよい。 <component>に:keyを設定すると異なるコンポーネントに切り替えたときもincrementCurrentKeyの呼び出しが必要な気がしてしまうが、何とそのようなことはない。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nuxt.js+Vuetify on AWS Amplify環境のWebアプリbundleサイズ削減覚書

この記事は何 自作webアプリ開発中に直面した『bundleサイズ大きすぎる問題』解決に向けた覚書 最終的な結論は下記の通り 目下開発推進中のプロジェクト(今回だとaws-amplify)を使う場合は最新版が出ていないか、その中で自分が直面している問題が解決されていないか確認することが必要。 GithubのIssueを確認して、その中で解決策が議論されていないか探すことが必要。 何が問題だったか 以下の環境でbundleサイズがgzip後で680kB程度あり、Lighthouseのモバイルスコアが50点程度になる要因の1つであった。 package.json { "dependencies": { "@aws-amplify/ui-components": "^1.1.0", "@aws-amplify/ui-vue": "^1.0.6", "@nuxtjs/pwa": "^3.3.5", "aws-amplify": "^3.3.24", "core-js": "^3.8.3", "nuxt": "^2.14.12", "nuxt-webfontloader": "^1.1.0" }, "devDependencies": { "@nuxtjs/vuetify": "^1.11.3", etc..., }, } Nuxt.js開発時のbundleサイズ削減に向けてよく見かける方法として、下記はすでに導入済み。 Vuetify.jsのTreeShakeを有効化する extractCSSオプションを有効化する またAmplifyのTreeShakeを使ってbundleサイズ削減に向けた対策も織り込み済み。この辺、ちょっと古いネット情報を見てしまうと誤るので要注意。 https://github.com/aws-amplify/amplify-js/issues/488 ざっくり書くとこう OK: import { Auth } from '@aws-amplify/auth NG: import { Auth } from 'aws-amplify' その上で、js、cssそれぞれで下記の課題あり。 js: aws-amplify/authから参照しているelliptic, bn.jsなどのサイズが無駄に大きい(gzip後で100kB超) css: 全体のサイズが280kB程度ある。中身を見てみるとTreeShakeを有効にしているのに、使用していないVuetifyコンポーネントのcssが大量に含まれている 解決手段 aws-amplify/authから参照しているモジュール GithubのIssueを漁っていると全く同じ課題が議論されており、すでに解決策もmergeされていることがわかった(see) 多くの環境にとって不要なモジュールをすべてimportしていたっぽい(雰囲気) @aws-amplify/authが参照しているamazon-cognito-identity-jsが4.6.0以降であれば上記修正を織り込み済み aws-amplify/authを3.4.27から3.4.29に上げて効果を確認した。bundleサイズ▲150kB程度。 Vuetify.jsのCSSが読み込まれてしまう問題 こちらもnuxt-vuetifyのissueを読み漁って探した どうやらTreeShakeを有効にしていてもcssは適切に取捨選択されない模様 nuxt-purgecssを使えば良いという情報にたどり着くまでが長かった…。 ただしnuxt-purgecssのデフォルト設定だと、必要なcssまで削減してしまうことも分かった。適切な設定が必要。 nuxt-purgecssという単語がわかったので検索してみると、Qiitaでも投稿されている方を発見(see)。大変参考になりました。cssサイズ激減。 おわりに 新しいものに手を出すときは、最前線の情報を探しに行くことが大事ですね…。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VueとPixi.JSでアクションゲーム『ネコメザシアタック』を開発して3年目なのでソースと解説

2/22に個人開発のブラウザゲーム『ネコメザシアタック21』をリリースしました。 特に理由はないのですが、3年前からこの時期には毎年同じテーマでゲームを作ってます。 ぶっちゃけ全然流行らないし、当然収益なんて1円もないのだけど、3年続けると見えてくることもあるので今年も記事書きます。せめて供養がわりにLGTM頂けると幸甚の極みです 作ったものの変遷 まあそんなわけで、まずはこの3年間での進歩をみて欲しい 1年目:その場でジャンプするだけの超シンプルゲーム プレイURL: https://mezashiattack.firebaseapp.com ソースコード: https://github.com/yuneco/mezashi 解説記事: VueとSVGを使ってシューティングゲーム『ネコ?メザシ?アタック?』を作ったのでソースと解説 その場でジャンプして弾(メザシ)を発射するだけの簡単仕様 Vue2(JavaScript)のオーソドックスな構成 Vueでdiv要素のCSSを操作してキャラクターを動かす方式 今コミットログ見返したら2週間(週末2回)で作ったらしい...まじか... 2年目:曲面(角丸)のステージ・ランキング・TS採用・レスポンシブ プレイURL: https://nekomzs2.web.app/ ソースコード: https://github.com/yuneco/mezashi2 解説記事: VueとCSSとTypeScriptでシューティングゲーム「ネコメザシアタック2020」を作ったのでソースと解説 時代に乗るためにTypeScriptとCompositionAPIを採用(Vue本体はまだv2) 前回位置固定だったキャラクター(たまさん)が画面内を動き回る仕様に進化 Firebaseで雑なランキングシステムを実装 3年目:複数のステージ・グラフィックの進化・マルチデバイス プレイURL: https://nekomzs21.web.app/ ソースコード: https://github.com/yuneco/mezashi21 解説記事: ここ Vue3採用 グラフィックスを超強化 画面描画をCSSからPixiJS(Canvas/WebGL)に変更 デバイス・画面サイズにかかわらず同じ画面を表示できるようになった(やっと!) 変えたこと変わらないこと 少しずつ採用技術を変えながら毎年新しく作りなおしているので基本的には別物なのですが、それでも「変わらないもの」って結構ある。「変えたもの」と「変えなかったもの」をいくつか紹介します。 3年間で変えたこと: JavaScript → TypeScriptへの変更 なんていうかTypeScriptって面倒そうじゃないですか? 型パズル大好きなつよつよエンジニアの人が使えばよくて、私みたいな弱小が趣味で作るならJSでいいじゃーん...そう思ってたのが最初の年 今だから言う。これ完全に間違ってた 別に型パズルはしなくてもいいし、それこそstrictなしのゆるゆるでもいいけど、とりあえずTypeScriptは入れとけ。きちんとした設計なしに走り出して、途中できちんとしたリファクタリングなんてしない趣味のゲーム開発なんかなら尚更。 ↓の図は1年目(js)と3年目(ts)の当たり判定ロジックの比較。 一見ほとんどおなじだけど、VSCodeでカーソル当てるとわかる。持ってる情報が段違い。 雑に作って雑に直していくためにもTypeScriptは必要。もう戻れない。戻っちゃいけない。 3年間で変えたこと:PixiJS + GSAPの採用 最初に紹介したように、1-2年目はDOMのCSSをVueで操作してキャラを動かしているんだけど、まあ正直言うとしんどいです。 1年目みたいな簡単な動きで5個10個の要素を動かすだけなら全然いいんだけど、入れ子になった何十何百の要素を管理するは結構きつい。色々最適化の工夫はあるけど、それでも動くdivの数が200〜300くらいがパフォーマンス的にも限界になることが多いので、装飾的な要素やパーティクルみたいなエフェクトもあんまり使えません。DOMでゲーム作るのは好きなんだけどね。。 そんなわけで今年は一念発起してDOMを捨て、2Dグラフィックスの王道的なライブラリであるPixiJS + GSAPを採用しました。PixiJSではWebGLを使って高速な描画ができる上、WebGLのフィルターを自作すれば表現の自由度が一気に上がるのも嬉しいポイント。 多分誰も気づいてないから自分で言うけど、上の絵の2枚目の水中面、ちゃんと画面全体に水中っぽいエフェクトかかってるのです。これもWebGLのフィルターのおかげ。 PixiJSでフィルターを自作して使う方法は●●WebGL(PIXI.js + glsl)と物理演算(matter.js)で可愛い絵本風タピオカ作ったので解説●●で解説してるので気になる方はみてみてください(今回物理演算はナシです)。 3年間変えなかったこと:Vueの利用 PixiJS + GSAPにするならVueいらなくね? みたいな意見もあると思うのですが、今年も迷わずVueを使ってます。個人的な主張ですが、ラフに個人開発をするときこそ、最初にVueなりReactなりは導入しておいた方が良いです。 だって、vue createなりcreate-react-appなりすれば、それだけで汎用的なweb開発環境にTypeScriptまでついてくるですよ?特別な思想信条がなければ使った方が早いです。 あと、最初はゲームのメイン画面だけ考えてても、リリースが近くなると「タイトル画面どうしよう?」「ランキングは?」みたいな悩みが出てきます。その手の付加的な画面をCanvasの中に作っていくのはしんどいので、さくっとVueなりReactなりで作れる準備をしておくのは有益です。 今回も、メインのゲーム画面の周辺に表示されるステータス部や上にオーバレイするランキング等はVueで作成・管理しています。Vue部分とPixi部分を連動させる方法は後ろの方でちょっと書きます。 3年間変えなかったこと:SVGの利用 PixiJSに限らず、Canvas/WebGL系のゲーム作成(特にチュートリアルや入門記事)では、画像のフォーマットとしてPNG画像をみっちり並べたスプライトシートを使うのが一般的です(多分)。たとえば『Pixi.js でゲームを作ってみる vol.1』という記事の後ろの方に実際の作例が載っているのですが、見ての通りまあ結構めんどくさいです(※この解説記事自体はとてもわかりやすくありがたい内容です。念のため)。 でもベクター画像のSVGよりもドット絵の方がまだ簡単なんじゃないの?って思うじゃん? とりあえず↓を読んで欲しい キャラや画面要素のサイズ・デザインが変わるたびに元絵を直す必要がある(=つまり主要なパーツのサイズが最初にちゃんと設計できないと辛い) Retina(高解像度ディスプレイ)対応がめんどい。あとでまとめて対応できるだろうって思うとほんと痛い目にあう ドット絵のフリー素材を集めてきてサイズやトンマナ合わせるのは至難の技。ベクターならわりとなんとでもなる(加工の可否とかの利用条件は確認してね) そもそもいい感じのドット絵を描くのはセンスも技術も超必要。あれはほんと職人芸 ...というわけで、「とりあえず適当に動けば見た目は気にしない」ならドット絵PNGでもいいけど、そうでければSVGを使うのが吉です。私は普段アナログタッチの絵を描くのでベクターイラストは得意ではないのだけど、それでもゲームの素材作るならイラレでSVG作る方が100倍楽(もちろん個人の見解ry) あと、SVGはPNGよりも圧倒的に軽いのです。今回(3年目)はそれなりに見た目にも拘ったけど、それでも実際に転送してる画像データは88ファイルで80KB! スマホの細い回線でも楽に動く上、ややこしいローディング待ちのロジックを作らなくても雑なロード処理で動いちゃうのも趣味開発にはありがたいポイント 3年間変えなかったこと:当たり判定・効果音再生等の基本ロジック これは手抜きって言えば手抜きなのですが、結果的に当たり判定とSE再生はほぼ3年間同じものを使い回しています。 当たり判定は結局3年間box-intersectという汎用の衝突検知ライブラリを使って実装しています。 - 1年目: CollisionDetector.js - 2年目: CollisionDetector.ts - 3年目: CollisionDetector.ts サウンド系はaudio-loaderとaudio-playの組み合わせです。 - 1年目: playSound.js - 2年目: playSound.ts - 3年目: playSound.ts 実装は毎年少しずつ変わってるけど、基本は全部同じなのがわかると思います。 当たり判定もサウンド再生も、よほど全部入りのゲームライブラリを使わない限り自分で組み込む必要があるので、上記に限らず一度何かを使えるようになっておくと、ものづくりのスピードが一気に上がります。ここらへんサクッとできるとつよつよ感がでて良いですね 技術的なポイント解説:VueとPixiJSとGSAPでいい感じにゲームを作る ここからはいくつか、Vue + PixiJS + GSAPでゲームを作る際のポイントや工夫を載せておきます。 ポイント:PixiJSでSVGをロードする + Retinaに対応する 上の方でSVGを超プッシュしておいてあれなのですが、PixiJSでSVGをテクスチャ画像として読み込むのはちょっと工夫が必要です。 基本的にはSVGであってもPixiのテクスチャーとして使うときにはPNGと同様のラスター画像に変換することになるのですが、そのまま読み込むとRetinaに対応できず画像が滲んで表示されます。。 対応方法はいくつかあるのですが、今回は汎用的に使える方法として、Retina環境ではSVGを一度表示サイズの2倍の<img>要素に表示して、その<img>からPixi.Textureを生成する方法をとっています。 https://github.com/yuneco/mezashi21/blob/master/src/logics/loadImgs.ts#L49 今回はかなり雑な実装しかできていないのですが、きちんと使えば画面サイズに合わせてぴったりのテクスチャーを生成することもできるはずです。 ポイント:PixiJSの世界でVueを使う 去年までVueで作ってたゲームをPixiJSベースにして最初に悩むのが状態の管理方法です。 簡単なチュートリアルだと、全部のソースが1ファイルに収まってていろんな変数がグローバルだったりするのだけど、それって現実的じゃないですよね。 Vueのprops/emitsのような仕組みもないので、ルートのPixiApplicationインスタンスからバケツリレーするのもしんどいです。 実はVueの状態管理で定番のVuexはVueの外側でも普通に使えます。 こんな感じで雑にストアを作って... /src/store/index.ts store/index.ts export default createStore<State>({ state: { system: { initialTapped: false }, stageSetting: { width: 0, height: 0, ... } ... } }) Pixiのスプライトから参照するだけ。もちろんwatchやcomputedも使えます。 /src/sprites/Tama.ts Tama.ts // ゲーム状態の監視 watch( () => store.state.game.play, (newVal, oldVal) => { if (newVal === 'over') { this.gameOverMotion() } if (oldVal === 'over') { this.stepMotion() } } ) まあこれも結局は「ちょっとおしゃれなグローバル変数」に過ぎないんだけど、Vue側と決まったルールで状態の共有ができるのは悪くないです。 ポイント:GSAPのtweenアニメーションをasync/awaitに使う GSAPといえばWebのTweenアニメーションライブラリとしてはおそらく最強で、まあ多分普通の用途でできないことはそうそうないはず。 なのでそのまま素直に使っても良いのですが、1年目にtweenクラスの実装でやったように、できることならアニメーションはasync/awaitで綺麗に書きたい・・・ということで、ちょっとだけラップしてasync/await中心でアニメーションを組み立てられるようにしています。 たまさんのジャンプモーション private async jumpMotion() { // 新しいモーションを開始する = 古いモーションはこの時点で中断 const mo = this.nextMotion('jump') // 予備動作 await all( mo.animate(cont, { scaleY: 0.75, angle: 15 }, 0.15, Sine.easeOut), // 本体 mo.animate(amFr, { angle: -40 }, 0.15), // 腕手前 mo.animate(amBk, { angle: -30 }, 0.15), // 腕奥 mo.animate(lgBk, { angle: 0 }, 0.15), // 脚奥 mo.animate(lgFr, { angle: 0 }, 0.15) // 脚手前 ) await all( // 本体ジャンプ run(async () => { await mo.animate(cont, { scaleY: 1.1, y: -1000 }, 1.6, Cubic.easeOut) await mo.animate(cont, { scaleY: 1.0, y: 0, angle: 0 }, 2.5, Bounce.easeOut) }), // 腕振り手前 run(async () => { await mo.animate(amFr, { angle: 50 }, 1.3) await mo.animate(amFr, { angle: 0 }, 1.0) }), // 腕振り奥 run(async () => { await mo.animate(amBk, { angle: 30 }, 1.3) await mo.animate(amBk, { angle: 0 }, 1.0) }), // 足振り run(async () => { await mo.animate(lgBk, { angle: -30 }, 1.2) await mo.animate(lgBk, { angle: 0 }, 0.9) }) ) store.dispatch('tamaJumpEnd') mo.alive && this.defaultMotion() } 上の例で「モーション」って呼んでるものの本体がこれ↓ /src/logics/animate.tsのAnimatorクラス Animatorクラス(抜粋) export class Animator { /** * キャンセル可能なアニメーションの管理インスタンスを作成します。 * @param canceller キャンセルすべきかどうかを返すcomputedプロパティ。一度でもtrueになるとその時点で実行中のアニメーションを中断し、以後のアニメーションを全て無視します。 */ constructor(canceller?: ComputedRef<boolean>) { ... } /** * アニメーションを実行します。すでにキャンセルされている場合には何も起こりません。 * また、実行を開始した後でキャンセルが成立した場合、アニメーションは途中で打ち切られます。 * 実行されなかった場合及び、実行が打ち切られた場合にもPromiseはresolveになります(rejectはされません)。 */ async animate(...params: Parameters<typeof animate>) { ... } } 基本的にGSAPのtweenをラップしているだけなんだけど、コンストラクターにVueのcomputedを指定することで、「所定の条件を満たさなくなったらアニメーションを打ち切る」動作ができるようにしています。Vuexストアの状態が変わったらアニメーションを切り替える...みたいなことができるわけです。便利!(自賛) こんな感じでVueの機能は結構Vueコンポーネントの外側でも便利に使えるものがあるので、活用するとPixiJSだけの開発に比べて辛さが緩和できる...はず。 おまけ:3年間でコード量はどれくらい増えた? なんか1年目より明らかにハードワークになってる気がするので確認してみた。 ↓でざっくり確認。余談だけどclocがnpxで使えることにこの間まで全然気づかなかった npx cloc mezashi/src npx cloc mezashi2/src npx cloc mezashi21/src ファイル数 Language 1年目 2年目 3年目 Vuejs Component 11 21 12 JavaScript 9 0 0 TypeScript 0 22 71 GLSL 0 0 3 合計 20 43 86 ステップ数 Language 1年目 2年目 3年目 Vuejs Component 985 2183 1127 JavaScript 234 0 0 TypeScript 0 442 3704 GLSL 0 0 110 合計 1219 2625 4941 見事に毎年2倍になってますね。。これは単に毎年前回以上のこだわりを入れようとして肥大化しただけって見方もあるにはあるのですが、それ以上に使う技術が変わったことで「ざっくり書けるコードの上限」が上がったと見ることもできそうです。 個人開発を長くやっている方ならなんとなくわかってもらえると思うのですが、同じスキルレベルの人が同じアーキテクチャで無計画にコードを書くと、大体同じくらいのボリュームで破綻が見えてきます。 私の場合、生のJS(バニラJS)だと大体1000-1500行くらい書くとヤバい空気が漂ってくるのですが、それがTSを使ったりPixiやGSAPの仕組みに乗っかることで数倍までは無理なく伸ばせている...とも言えそうです(もちろん3年でスキルアップした部分もあると信じたい...)。 まとめ そんなわけで今年もなんとか2/22にゲームをリリースすることができました。 来年は...あるかなぁ...。。来年のことはわからないけど、同じようなテーマでも何回も作っては崩しを繰り返しているとなかなか面白い結果が見えてくることがわかりました。ゆるゆる継続していけるといいですね 今年も同じこと言うけど、Vueでゲーム作るの面白いよ!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む