- 投稿日:2020-02-15T19:17:49+09:00
CircleCIでVue.jsアプリをFirebaseにデプロイする
以前作ったVue.jsアプリ、これはFirebaseでHositingさせてもらってます。
当時の記事はこれです。
https://qiita.com/qoAop/items/9d8375de98b74dd55b60
Vue.jsアプリをFirebaseHostingにデプロイするには、
yarn build yarn deployで、デプロイしてました。
ただ、厳密(?)には、これだけじゃなくて、
firebase login
、してfirebase use [プロジェクト]
をしておかなければならないわけです。で、本題ですがこれをCircleCIで自動化したれということです。
始めたばかりなので設定ファイルに過不足があるかと思いますがこんな感じで設定しておけば、
git push origin master
すれば、CircleCIのほうでビルドしてFirebaseにHostingまでしてくれるようになります。CircleCIの設定
一点注意することがあります。Macのコマンドラインでfirebase loginをするとブラウザが立ち上がりますがそういうことは無理なので事前にDeploy用のアクセストークンを取得して完全にコマンドで操作できるようにしなければなりません。
それで、Firebaseのアクセストークンを取得して、ついでにデプロイ用のプロジェクトIDも設定します。画面はこんな感じです。
こうすると、CircleCIのconfig.ymlの中で変数として利用できるようになります。
ディレクトリ構成はこんな感じ
Vue.jsとFirebaseでプロジェクトつくるとこんな感じになってると思います。
プロジェクトのトップレベルに.circleci/config.ymlを作るのがポイントだそうです。
. ├── .circleci │ └── config.yml ├── .env ├── .env.development ├── .env.production ├── .eslintrc.js ├── .firebase ├── .firebaserc ├── .git ├── .gitignore ├── .node-version ├── README.md ├── babel.config.js ├── dist <-------- build したもの ├── firebase.json ├── firestore.indexes.json ├── firestore.rules ├── functions ├── node_modules ├── package.json ├── public ├── src ├── storage.rules ├── vue.config.js └── yarn.lock.circleci/config.yml
中身はこんな感じです。テストなしw、ビルドしていきなりデプロイしちゃいます。
version: 2 jobs: build: working_directory: ~/app docker: - image: circleci/node:10 environment: LANG: ja_JP.UTF-8 TZ: Asia/Tokyo steps: - checkout # ソースコードを作業ディレクトリにチェックアウト - run: name: install & build command: | yarn install yarn build - persist_to_workspace: root: . # workspaceのrootパスを永続化する、要は共有 paths: - . # rootからの相対パス deploy: working_directory: ~/app docker: - image: circleci/node:10 environment: LANG: ja_JP.UTF-8 TZ: Asia/Tokyo steps: - attach_workspace: # persist_to_workspaceで設定したディレクトリを利用する at: . - run: name: deploy firebase hosting command: | yarn global add firebase-tools `yarn global bin`/firebase deploy --only hosting --project "${PROJECT_ID}" --token "${FIREBASE_TOKEN}" workflows: version: 2 build_and_deploy: jobs: - build - deploy: requires: # buildが成功したら実行 - build filters: branches: only: master # master branchのみdeployこれでいい感じにデプロイするようになりました〜
- 投稿日:2020-02-15T16:53:22+09:00
『これからはじめるVue.js実践入門』の正誤表など
jQueryからのステップアップとして『基礎から学ぶVue.js』で挫折。『これからはじめるVue.js実践入門』が分かりやすそうだったので、再チャレンジ中。ただ、おかしな点がいくつか出てくるので、その度に追記していく。
対象書籍
『これからはじめるVue.js実践入門』初版第1刷
正誤表など
- 45~47ページ
- _.debounceメソッドは、「遅延」関数ではなく、「抑制」関数、または「間引き」関数と呼ぶほうが正しい。遅延では実行のタイミングを遅らせるだけで必ず実行されてしまう。
- 49ページ
- 上部のソースコード内の、thisをthatに退避するコードは、蛇足。おかげでthisについて深く知ることができたけれど。
- 77ページ
- サンプルコード誤り。thisをthatなどに退避させていないため、テンプレートに反映されず、図3-20のようにはならない。正しいサンプルコード「model_file.js」は下記の通り。
new Vue({ el: '#app', data: { message: '' }, methods: { onchange: function() { let fl = this.$refs.upfile.files[0]; let data = new FormData(); let that = this; data.append('upfile', fl, fl.name); fetch('upload.php', { method: 'POST', body: data, }) .then(function (response) { return response.text(); }) .then(function (text) { that.message = text; }) .catch(function (error) { alert('Error: ' + error.message); }); } } });
- 投稿日:2020-02-15T14:41:11+09:00
[Form作成から学ぶVue(TypeScript)その4] 選択肢を動的に変更するプルダウン
親となるプルダウンを選択することで、子のプルダウンの選択肢を動的に変更するプルダウンの作りかた。
参考としたページ
https://qiita.com/niisan-tokyo/items/7d15716304c492f244c2ここへ以下の変更を加えた。
- まず記述をTypeScriptへ。
- 親プルダウンを変更すると子プルダウンは自動的に未選択にリセットされる。
- 子プルダウンの選択を親コンポーネントへEmitする。
- 選択肢を後から柔軟に変更したい。 →改良版へ
- 子プルダウンの数は任意の数だけ無限に設置したい。 →改良版へ
今見ると、何の都道府県なのか謎すぎるがやりたいことは連動したプルダウン作成なのでこの際気にしない
とりあえず真似てみた
PullDown.vue(子コンポーネント)<template> <div> <p>都道府県区分 <select v-model="selectedArea"> <option v-for="area in areas" :key="area.value">{{ area }}</option> </select> </p> <p>都道府県 <select id="prefecture" @change="changeSelectedPrefecture" v-model="selectedPrefecture"> <option v-for="prifecture in prifectures" :key="prifecture.value">{{ prifecture }}</option> </select> </p> </div> </template> <script lang="ts"> import { Component, Vue, Emit } from "vue-property-decorator"; @Component export default class Form extends Vue { private selectedArea: string = ""; private selectedPrefecture: string = ""; private prifecture: object = { "北海道" : ["北海道"], "東北" : ["青森", "秋田", "岩手", "山形", "宮城", "福島"], "関東" : ["茨城", "栃木", "群馬", "埼玉", "千葉", "東京", "神奈川"], "中部" : ["新潟", "富山", "石川", "福井", "山梨", "長野", "岐阜", "静岡", "愛知"], "近畿" : ["三重", "滋賀", "京都", "大阪", "兵庫", "奈良", "和歌山"], "中国" : ["鳥取", "島根", "岡山", "広島", "山口"], "四国" : ["徳島", "香川", "愛媛", "高知"], "九州・沖縄" : ["福岡", "佐賀", "長崎", "熊本", "大分", "宮崎", "鹿児島", "沖縄"] }; private areas: object = Object.keys(this.prifecture); private get prifectures(): object { return this.prifecture[this.selectedArea]; } private changeSelectedPrefecture(): void { console.log(this.selectedPrefecture) this.updateSelected(this.selectedPrefecture) } @Emit('updateSelected') private updateSelected(selected: string): void{ } } </script>なお、
changeSelectedPrefecture
はRadioButtonコンポーネントでやったように以下のようにしてもよい。selectタグから取得する値はHTMLSelectElement
なので注意。private changeSelectedPrefecture(): void { if (event!.target instanceof HTMLSelectElement) { console.log(event!.target.value) this.updateSelected(event!.target.value) } }プルダウンから選択されると
@change="changeSelectedPrefecture"
をトリガとして親コンポーネントで定義したupdateSelected
をEmitする。ここの記述は前回までのRadioButtonなどと同じなので省略。ただ、このままでは2連結のプルダウンしか動的に生成できないので、子プルダウンを3つ目、4つ目が欲しいと思っても、都度作成する必要が出てしまう。そこで、子プルダウンは親から与えられた選択肢を表示するに止めたより小さな機能のコンポーネントとして作り直す。
改良版
NewPullDown.vue(子コンポーネント)<template> <p> <span>{{ title }}</span> <span> <select @change="changeSelected" v-model="selected"> <option v-for="alt in alts" :key="alt.value">{{ alt }}</option> </select> </span> </p> </template> <script lang="ts"> import { Component, Vue, Prop, Emit } from "vue-property-decorator"; @Component export default class Form extends Vue { @Prop() private title: string; private selected: string = ""; @Prop() private alternatives: object; private get alts(): Array<string> { if(this.alternatives == undefined) return []; return Object.keys(this.alternatives); } mounted(){ console.log(this.alternatives); } private changeSelected(): void { console.log(this.selected) this.updateSelected(this.selected) } @Emit('updateSelected') private updateSelected(selected: string): void{ } } </script>使い方は以下
<PullDown :title="表示したいタイトル" :alternatives="選択肢を並べたjson" @updateSelected="Emitされる関数" />実際にこれを利用して先ほどの都道府県区分+都道府県の親コンポーネントを書くと以下。
Form.vue(親コンンポーネント)<template> <div> <div> <PullDown :title="areaTitle" :alternatives="alternatives" @updateSelected="updateSelectedArea" /> <PullDown :title="prefectureTitle" :alternatives="prefectures" @updateSelected="updateSelectedPrefecture" /> </div> </div> </template> <script lang="ts"> import { Component, Vue } from 'vue-property-decorator'; import PullDown from '@/components/PullDown.vue' import { alternativesOfPrefectures } from '../components/prefectures' @Component({ components:{ PullDown } }) export default class Form extends Vue { private selectedArea: string = "" private selectedPrefecture: string = "" private areaTitle = "都道府県区分" private prefectureTitle = "都道府県" private alternatives: object= alternativesOfPrefectures private updateSelectedArea(selected: string){ console.log("selectedArea:" + selected) this.selectedArea = selected } private updateSelectedPrefecture(selected: string){ console.log("selectedPrefecture:" + selected) this.selectedPrefecture = selected } private get prefectures(): object { console.log(this.alternatives[this.selectedArea]) return this.alternatives[this.selectedArea]; } } </script>なお、選択肢のJSONは別ファイルに外出しして参照している。
components/predfectures.ts(選択肢のJSONを並べたファイル)export const alternativesOfPrefectures: Object = { "北海道" : { "北海道": null }, "東北" : { "青森" : null, "秋田" : null, : (略) : "鹿児島" : null, "沖縄" : null } }jsonにリストを混ぜるのをやめてnullとしているのは、のちに3つ目のプルダウンを増やしたいと思った時の拡張性を考慮している。
- 投稿日:2020-02-15T12:42:44+09:00
VuetifyとNetlify Formsを使ったときに躓いたところ
Nuxt, Vuetifyで簡単なページを作り、Netlifyでページを公開しようとした際に、躓いたときのメモです。
問合せフォームをつけていたので、Netlify Formsを使いました。
Code an HTML form into any page on your site, add a
netlify
attribute ordata-netlify="true"
to the<form>
tagNetlifyでフォームを使うには、
form
タグにnetlify
属性をつけ、<form netlify>
とするだけです。ただ、Vuetifyでは、単に
<v-form netlify>
としてもうまく動作しませんでした。Netlify automatically adds a hidden field to your published form called
form-name
with a value to match thename
attribute of the form when you submit an HTML formNetlifyは、自動的に
name="form-name"
を持ったテキストフィールドをつけているようです。そこで、以下のようにフォームを書きました。
contact.vue<template> <v-container tag="section"> <v-row> <v-col v-text="title.toUpperCase()" cols="12" tag="h1" /> </v-row> <v-form method="post" netlify> <v-text-field v-show="false" v-model="title" name="form-name" /> <v-row> <v-col cols="12"> <v-text-field v-model="name" label="name" name="name" autofocus /> </v-col> </v-row> <v-row> <v-col cols="12"> <v-text-field v-model="email" label="E-mail" name="email" /> </v-col> </v-row> <v-row> <v-col cols="12"> <v-textarea v-model="message" label="message" name="message" /> </v-col> </v-row> <v-row> <v-col> <v-btn :disabled="isEmpty" type="submit" > submit </v-btn> </v-col> </v-row> </v-form> </v-container> </template> <script> export default { data () { return { title: 'contact', name: '', email: '', message: '' } }, computed: { isEmpty () { if ( this.name !== '' && this.email !== '' && this.message !=='' ) { return false } return true } } } </script>このように、
<v-form method="post" netlify> <v-text-field v-show="false" v-model="title" name="form-name" />
v-form
にnetlify
属性をつけて、
v-show="false" v-model="title"
をつけたv-text-field
をv-form
下に置くと、正常に動作します。参考
- 投稿日:2020-02-15T05:57:15+09:00
Vue.jsとAWS Amplify でプロジェクト管理サービスを作った話
概要
プロジェクト管理で苦労した体験から、プロジェクトを俯瞰的に可視化・管理するサービスをVue.jsとAWS Amplifyでつくりました。
プロジェクトの可視化はプロジェクト譜という記法で描いた図で表現します。作ったサービス
- サービス名: Pufu (読み方:プフ)
- 現在β版として公開中
- 対象ブラウザ: Chrome
- ここからサービスサイトに飛べます。
- アカウント登録は『アカウントの新規作成』から行えます。
- サイトイメージ
- サービスサイトと合わせて紹介用のサイトも作りました。
サービスを作った動機
プロジェクトを管理する傍ら炎上するプロジェクトと遭遇することもあり、
それはなぜかを自分なりに考えていくと、「手を打つべき時に手を打てず、対応が後手に回る」という至極普通な考えに至りました。
ただ、あのとき「ああしていれば。。」というのは、振り返って気付くのであって、その時点では気づきにくいのです。
どうすれば事前に必要な対応を気付けるのか考えているときに、「予定通り進まないプロジェクトの進め方」という本に出会い、
プロジェクト譜という手法を知りました。プロジェクト譜はプロジェクトを俯瞰的に図示し、編集を重ねていきます。
なので、プロジェクト全体で見たときに落とし穴に気付きやすくなります。
プロジェクト譜の詳しい説明は割愛しますが、目標や状況変化に対する
記述のしやすさに興味が惹かれました。プロジェクト譜に興味のある方は下記の書籍をご参照ください。
予定通り進まないプロジェクトの進め方 (Amazon)
著者: 前田考歩、 後藤洋平
出版社: 宣伝会議いざプロジェクト譜をパワポなどを使って実践していくと、
汎用ツールでは操作が多くなったり、欲しい機能がないなど不便さがありました。
draw.ioのようにWebでつくれたらいいんじゃないかと調べても、
プロジェクト譜を対象としたサービスもなかったので、
自分で作ってしまえ!と作ったのがPufuです。サービスの背景はここまでにして、Qiitaなので技術的な話を書きます。
※サービスデザイン的な話についてはnoteにかに書こうと思います。
→書きましたnoteシステム構成
フレームワーク類
フロントエンドはVue.jsで実装、UIはBootstrapVueを用いて、
バックエンドと一部フロントエンド(認証周り)でAWS Amplifyを利用しています。
Amplifyの裏側でAppSyncとDynamoDB、Cognitoを利用しています。
- Vue.js (ver. 3.11.0)
- BootstrapVue: UIフレームワーク
- AWS Amplify: mBaasサービス(認証・API類&Webサイトホスティング)
- AppSync: データアクセス(GraphQL)
- DynamoDB: データ管理
- Cognito: ユーザー管理
全体構成イメージ
開発環境
AWSのWebIDE Cloud9を利用しています。去年、Cloud9が日本リージョンに対応して重宝しています。
- IDE: AWS Cloud9
- バージョン管理: AWS CodeCommit
- CodeCommitにプッシュするとAmplifyのホスティングサービスでビルドとデプロイが走ります
- AmplifyはCodeCommit以外にもGitHubとの連携も対応しています
デプロイのイメージ
- Amplify push: バックエンド側の環境構築
amplifyコマンドで実行し、CloudformationでAPI(GraphQL)やAuth(Cognito)のリソースを作成します。- Git Push: Cloud9で編集したコードをCodeCommitにプッシュ
- Notify: プッシュをAmplify側が検知
- Build & Deploy: CodeCommit側のソースを取得してビルド、デプロイ
$ amplify push Scanning for plugins... Plugin scan successful Current Environment: dev | Category | Resource name | Operation | Provider plugin | | -------- | ------------- | --------- | ----------------- | | Auth | abcdefghijkl | Update | awscloudformation | | Api | abcdefghijkl | Update | awscloudformation |実装
データアクセス
- AppSyncのGraphQLを利用
- スキーマを定義ファイルを編集します
- amplify pushするとクエリの定義ファイルが作成
- @authや@keyなどのディレクティブで認証や検索時のインデックスを制御
schema.graphqltype Blog @model @auth(rules: [{ allow: owner }]) @key(name: "ByOwner", fields: ["owner"], queryField: "listBlogByOwner") { id: ID! name: String! owner: String! }https://aws-amplify.github.io/docs/cli-toolchain/graphql?sdk=js
ユーザー管理
- ログイン、サインアップはamplifyのコンポーネントを利用
- サインアップ時のダイアログ表示は、signUpのイベントをキャッチしてダイアログを表示
https://aws-amplify.github.io/docs/js/vue
プロジェクト譜エディタ
- SVGで実装
- 各アイコンをコンポーネントで実装
- コンポーネント管理できるVue.jsのメリットを活かせた感じ
- 要素の移動など、開始イベントは子要素が発生し、終了イベントは親要素で発生する場合がある
- 子要素で処理を実装すると終了イベントを子要素自身でキャッチできない
- 親要素で終了イベントの処理を実装すると複雑化しすぎる
- 要素移動などはDelegateパータンで実装
テスト環境
ユニットテスト
- ユニットテストではjestを利用
- amplifyやstoreの挙動はモックを作成しました
jest.spyOn(API, 'graphql').mockReturnValue( Promise.resolve({ data: { getProject: {id: 'todo1', name: 'todo_name_1'}, createProject: {id: '123'} } }) )e2eテスト
- e2eテストはjest + puppeteerを利用
nightwatchやcypressを試しましたが、cloud9上で動かさず、最終的にpupetterで構築しました。pupetterで画面キャプチャを取れるので、cloud9上でもテスト時の画面結果を確認できます。感想
Amplifyは使い方が分かると実装しやすい。凝ったことをしようとすると、カスタマイズが多くなるので、開発スピードをとるなら、素のまま使う割り切りが必要。
VueとSVGの組み合わせは相性がいい。jQueryで実装するとかなり大変だったはず。Reactと相性はよさそう。(Reactは全然詳しくないですが。。。)
SVGのエディタの処理はかなり複雑化になるので、どう実装するといいのか悩みます。
今回はできてないですが、Atomic Designで初めから細分化しておくとすっきりしそう。サービスを作るのはやはり大変。Pufuは仕事の合間、勉強しながらで作ったので6か月かかりました。最初はやる気があっても中だるみするのでモチベーションの維持は難しいですね。
VueのCompositionAPIやTypeScriptも興味あるので、余裕があるときに導入検討します。あとテストはもっと充実させたい。
あと、Pufuが本当によりよいプロジェクト運営に役に立てるのか検証していければと思います。
- 投稿日:2020-02-15T05:57:15+09:00
Vue.jsとAWS Amplifyでプロジェクト譜のWebサービスを作った話
概要
プロジェクト管理で苦労した体験から、プロジェクトを俯瞰的に可視化・管理するサービスをVue.jsとAWS Amplifyでつくりました。
プロジェクトの可視化はプロジェクト譜という記法で描いた図で表現します。作ったサービス
- サービス名: Pufu (読み方:プフ)
- 現在β版として公開中
- 対象ブラウザ: Chrome
- ここからサービスサイトに飛べます。
- アカウント登録は『アカウントの新規作成』から行えます。
- サンプルエリアがあるのでアカウントを作らなくても試せます。
- サイトイメージ
- サービスサイトと合わせて紹介用のサイトも作りました。
サービスを作った動機
プロジェクトを管理する傍ら炎上するプロジェクトと遭遇することもあり、
それはなぜかを自分なりに考えていくと、「手を打つべき時に手を打てず、対応が後手に回る」という至極普通な考えに至りました。
ただ、あのとき「ああしていれば。。」というのは、振り返って気付くのであって、その時点では気づきにくいのです。
どうすれば事前に必要な対応を気付けるのか考えているときに、「予定通り進まないプロジェクトの進め方」という本に出会い、
プロジェクト譜という手法を知りました。プロジェクト譜はプロジェクトを俯瞰的に図示し、編集を重ねていきます。
なので、プロジェクト全体で見たときに落とし穴に気付きやすくなります。
プロジェクト譜の詳しい説明は割愛しますが、目標や状況変化に対する
記述のしやすさに興味が惹かれました。プロジェクト譜に興味のある方は下記の書籍をご参照ください。
予定通り進まないプロジェクトの進め方 (Amazon)
著者: 前田考歩、 後藤洋平
出版社: 宣伝会議いざプロジェクト譜をパワポなどを使って実践していくと、
汎用ツールでは操作が多くなったり、欲しい機能がないなど不便さがありました。
draw.ioのようにWebでつくれたらいいんじゃないかと調べても、
プロジェクト譜を対象としたサービスもなかったので、
自分で作ってしまえ!と作ったのがPufuです。サービスの背景はここまでにして、Qiitaなので技術的な話を書きます。
※サービスデザイン的な話についてはnoteにかに書こうと思います。
→書きましたnoteシステム構成
フレームワーク類
フロントエンドはVue.jsで実装、UIはBootstrapVueを用いて、
バックエンドと一部フロントエンド(認証周り)でAWS Amplifyを利用しています。
Amplifyの裏側でAppSyncとDynamoDB、Cognitoを利用しています。
- Vue.js (ver. 3.11.0)
- BootstrapVue: UIフレームワーク
- AWS Amplify: mBaasサービス(認証・API類&Webサイトホスティング)
- AppSync: データアクセス(GraphQL)
- DynamoDB: データ管理
- Cognito: ユーザー管理
全体構成イメージ
開発環境
AWSのWebIDE Cloud9を利用しています。去年、Cloud9が日本リージョンに対応して重宝しています。
- IDE: AWS Cloud9
- バージョン管理: AWS CodeCommit
- CodeCommitにプッシュするとAmplifyのホスティングサービスでビルドとデプロイが走ります
- AmplifyはCodeCommit以外にもGitHubとの連携も対応しています
デプロイのイメージ
- Amplify push: バックエンド側の環境構築
amplifyコマンドで実行し、CloudformationでAPI(GraphQL)やAuth(Cognito)のリソースを作成します。- Git Push: Cloud9で編集したコードをCodeCommitにプッシュ
- Notify: プッシュをAmplify側が検知
- Build & Deploy: CodeCommit側のソースを取得してビルド、デプロイ
$ amplify push Scanning for plugins... Plugin scan successful Current Environment: dev | Category | Resource name | Operation | Provider plugin | | -------- | ------------- | --------- | ----------------- | | Auth | abcdefghijkl | Update | awscloudformation | | Api | abcdefghijkl | Update | awscloudformation |実装
データアクセス
- AppSyncのGraphQLを利用
- スキーマを定義ファイルを編集します
- amplify pushするとクエリの定義ファイルが作成
- @authや@keyなどのディレクティブで認証や検索時のインデックスを制御
schema.graphqltype Blog @model @auth(rules: [{ allow: owner }]) @key(name: "ByOwner", fields: ["owner"], queryField: "listBlogByOwner") { id: ID! name: String! owner: String! }https://aws-amplify.github.io/docs/cli-toolchain/graphql?sdk=js
ユーザー管理
- ログイン、サインアップはamplifyのコンポーネントを利用
- サインアップ時のダイアログ表示は、signUpのイベントをキャッチしてダイアログを表示
https://aws-amplify.github.io/docs/js/vue
プロジェクト譜エディタ
- SVGで実装
- 各アイコンをコンポーネントで実装
- コンポーネント管理できるVue.jsのメリットを活かせた感じ
- 要素の移動など、開始イベントは子要素が発生し、終了イベントは親要素で発生する場合がある
- 子要素で処理を実装すると終了イベントを子要素自身でキャッチできない
- 親要素で終了イベントの処理を実装すると複雑化しすぎる
- 要素移動などはDelegateパータンで実装
テスト環境
ユニットテスト
- ユニットテストではjestを利用
- amplifyやstoreの挙動はモックを作成しました
jest.spyOn(API, 'graphql').mockReturnValue( Promise.resolve({ data: { getProject: {id: 'todo1', name: 'todo_name_1'}, createProject: {id: '123'} } }) )e2eテスト
- e2eテストはjest + puppeteerを利用
nightwatchやcypressを試しましたが、cloud9上で動かさず、最終的にpupetterで構築しました。pupetterで画面キャプチャを取れるので、cloud9上でもテスト時の画面結果を確認できます。感想
Amplifyは使い方が分かると実装しやすい。凝ったことをしようとすると、カスタマイズが多くなるので、開発スピードをとるなら、素のまま使う割り切りが必要。
VueとSVGの組み合わせは相性がいい。jQueryで実装するとかなり大変だったはず。Reactと相性はよさそう。(Reactは全然詳しくないですが。。。)
SVGのエディタの処理はかなり複雑化になるので、どう実装するといいのか悩みます。
今回はできてないですが、Atomic Designで初めから細分化しておくとすっきりしそう。サービスを作るのはやはり大変。Pufuは仕事の合間、勉強しながらで作ったので6か月かかりました。最初はやる気があっても中だるみするのでモチベーションの維持は難しいですね。
VueのCompositionAPIやTypeScriptも興味あるので、余裕があるときに導入検討します。あとテストはもっと充実させたい。
あと、Pufuが本当によりよいプロジェクト運営に役に立てるのか検証していければと思います。
- 投稿日:2020-02-15T05:57:15+09:00
Vue.jsとAWS Amplifyでプロジェクト譜のサービスを作った話
概要
プロジェクト管理で苦労した体験から、プロジェクトを俯瞰的に可視化・管理するサービスをVue.jsとAWS Amplifyでつくりました。
プロジェクトの可視化はプロジェクト譜という記法で描いた図で表現します。作ったサービス
- サービス名: Pufu (読み方:プフ)
- 現在β版として公開中
- 対象ブラウザ: Chrome ※スマホは閲覧のみ
- ここからサービスサイトに飛べます。
- アカウント登録は『アカウントの新規作成』から行えます。
- サンプルエリアがあるのでアカウントを作らなくても試せます。
- サイトイメージ
- サービスサイトと合わせて紹介用のサイトも作りました。
サービスを作った動機
プロジェクトを管理する傍ら炎上するプロジェクトと遭遇することもあり、
それはなぜかを自分なりに考えていくと、「手を打つべき時に手を打てず、対応が後手に回る」という至極普通な考えに至りました。
ただ、あのとき「ああしていれば。。」というのは、振り返って気付くのであって、その時点では気づきにくいのです。
どうすれば事前に必要な対応を気付けるのか考えているときに、「予定通り進まないプロジェクトの進め方」という本に出会い、
プロジェクト譜という手法を知りました。プロジェクト譜はプロジェクトを俯瞰的に図示し、編集を重ねていきます。
なので、プロジェクト全体で見たときに落とし穴に気付きやすくなります。
プロジェクト譜の詳しい説明は割愛しますが、目標や状況変化に対する
記述のしやすさに興味が惹かれました。プロジェクト譜に興味のある方は下記の書籍をご参照ください。
予定通り進まないプロジェクトの進め方 (Amazon)
著者: 前田考歩、 後藤洋平
出版社: 宣伝会議いざプロジェクト譜をパワポなどを使って実践していくと、
汎用ツールでは操作が多くなったり、欲しい機能がないなど不便さがありました。
draw.ioのようにWebでつくれたらいいんじゃないかと調べても、
プロジェクト譜を対象としたサービスもなかったので、
自分で作ってしまえ!と作ったのがPufuです。サービスの背景はここまでにして、Qiitaなので技術的な話を書きます。
※サービスデザイン的な話についてはnoteにかに書こうと思います。
→書きましたnoteシステム構成
フレームワーク類
フロントエンドはVue.jsで実装、UIはBootstrapVueを用いて、
バックエンドと一部フロントエンド(認証周り)でAWS Amplifyを利用しています。
Amplifyの裏側でAppSyncとDynamoDB、Cognitoを利用しています。
- Vue.js (ver. 3.11.0)
- BootstrapVue: UIフレームワーク
- AWS Amplify: mBaasサービス(認証・API類&Webサイトホスティング)
- AppSync: データアクセス(GraphQL)
- DynamoDB: データ管理
- Cognito: ユーザー管理
全体構成イメージ
開発環境
AWSのWebIDE Cloud9を利用しています。去年、Cloud9が日本リージョンに対応して重宝しています。
- IDE: AWS Cloud9
- バージョン管理: AWS CodeCommit
- CodeCommitにプッシュするとAmplifyのホスティングサービスでビルドとデプロイが走ります
- AmplifyはCodeCommit以外にもGitHubとの連携も対応しています
デプロイのイメージ
- Amplify push: バックエンド側の環境構築
amplifyコマンドで実行し、CloudformationでAPI(GraphQL)やAuth(Cognito)のリソースを作成します。- Git Push: Cloud9で編集したコードをCodeCommitにプッシュ
- Notify: プッシュをAmplify側が検知
- Build & Deploy: CodeCommit側のソースを取得してビルド、デプロイ
$ amplify push Scanning for plugins... Plugin scan successful Current Environment: dev | Category | Resource name | Operation | Provider plugin | | -------- | ------------- | --------- | ----------------- | | Auth | abcdefghijkl | Update | awscloudformation | | Api | abcdefghijkl | Update | awscloudformation |実装
データアクセス
- AppSyncのGraphQLを利用
- スキーマを定義ファイルを編集します
- amplify pushするとクエリの定義ファイルが作成
- @authや@keyなどのディレクティブで認証や検索時のインデックスを制御
schema.graphqltype Blog @model @auth(rules: [{ allow: owner }]) @key(name: "ByOwner", fields: ["owner"], queryField: "listBlogByOwner") { id: ID! name: String! owner: String! }https://aws-amplify.github.io/docs/cli-toolchain/graphql?sdk=js
ユーザー管理
- ログイン、サインアップはamplifyのコンポーネントを利用
- サインアップ時のダイアログ表示は、signUpのイベントをキャッチしてダイアログを表示
https://aws-amplify.github.io/docs/js/vue
プロジェクト譜エディタ
- SVGで実装
- 各アイコンをコンポーネントで実装
- コンポーネント管理できるVue.jsのメリットを活かせた感じ
- 要素の移動など、開始イベントは子要素が発生し、終了イベントは親要素で発生する場合がある
- 子要素で処理を実装すると終了イベントを子要素自身でキャッチできない
- 親要素で終了イベントの処理を実装すると複雑化しすぎる
- 要素移動などはDelegateパータンで実装
テスト環境
ユニットテスト
- ユニットテストではjestを利用
- amplifyやstoreの挙動はモックを作成しました
jest.spyOn(API, 'graphql').mockReturnValue( Promise.resolve({ data: { getProject: {id: 'todo1', name: 'todo_name_1'}, createProject: {id: '123'} } }) )e2eテスト
- e2eテストはjest + puppeteerを利用
nightwatchやcypressを試しましたが、cloud9上で動かさず、最終的にpupetterで構築しました。pupetterで画面キャプチャを取れるので、cloud9上でもテスト時の画面結果を確認できます。感想
Amplifyは使い方が分かると実装しやすい。凝ったことをしようとすると、カスタマイズが多くなるので、開発スピードをとるなら、素のまま使う割り切りが必要。
VueとSVGの組み合わせは相性がいい。jQueryで実装するとかなり大変だったはず。Reactと相性はよさそう。(Reactは全然詳しくないですが。。。)
SVGのエディタの処理はかなり複雑化になるので、どう実装するといいのか悩みます。
今回はできてないですが、Atomic Designで初めから細分化しておくとすっきりしそう。サービスを作るのはやはり大変。Pufuは仕事の合間、勉強しながらで作ったので6か月かかりました。最初はやる気があっても中だるみするのでモチベーションの維持は難しいですね。
VueのCompositionAPIやTypeScriptも興味あるので、余裕があるときに導入検討します。あとテストはもっと充実させたい。
あと、Pufuが本当によりよいプロジェクト運営に役に立てるのか検証していければと思います。
- 投稿日:2020-02-15T05:50:39+09:00
vuelidateを使ってバリデーションを実装する。
インストール
npm install vuelidate --saveグローバルで使用するときは、下記のように記述
import Vue from "vue"; import Vuelidate from "vuelidate"; Vue.use(Vuelidate);コンポーネント内で直接使用するときは、
import { validationMixin } from 'vuelidate' var Component = Vue.extend({ mixins: [validationMixin], validations: { ... } })のようにコンポーネントファイルに追加してください。
例えば、age と message という 2 つの属性にそれぞれ必須のバリデーションを、
加えて、名前のほうに 4 文字以上で入力させたい場合は、下記のように validations
の中でそれぞれrequired
、name に minLength を書いていきます。import { required, minLength, between } from "vuelidate/lib/validators"; export default { data() { form { name: "", message: "" }; }, validations: { name: { required, minLength: minLength(4) }, message: { required } } };最後に、下記のようにフォームに使用する場合は、下記のように
v-if="!$v.form.属性.バリデーション"
でエラーメッセージを
表示させることができます。(下記はVueの例です。)<form @submit.prevent="validateForm"> <div class="sender-info"> <md-field :class="getValidationClass('name')"> <label for="name" class="label">名前</label> <md-input name="name" id="name" autocomplete="given-name" v-model="form.name" class="md-accent" /> <span class="md-error md-primary" v-if="!$v.form.name.required" >名前を入力してください</span > <span class="md-error md-primary" v-else-if="!$v.form.name.minlength" >名前は4文字以上で入力して下さい</span > </md-field> </div> </form>methods: { getValidationClass(fieldName) { const field = this.$v.form[fieldName]; if (field) { return { "md-invalid": field.$invalid && field.$dirty }; } } }, validateForm() { this.$v.$touch(); if (!this.$v.$invalid) { this.submit(); } }, submit () { // Instead of this timeout, here you can call your API window.setTimeout(() => { this.lastUser = `${this.form.firstName} ${this.form.lastName}` this.userSaved = true this.sending = false this.clearForm() }, 1500) }
- 投稿日:2020-02-15T01:52:05+09:00
便利ページ:アイコンファイルを生成する
「便利ページ:Javascriptでちょっとした便利な機能を作ってみた」のシリーズものです。
今回は、1つの画像ファイルから、AndroidやiPhoneアプリで使う様々なサイズのアイコンを生成します。
いつもの通りGitHubにも上げてあります。
https://github.com/poruruba/utilities参考までに、以下にデモとしてアクセスできるようにしてあります。「画像ファイル」のタブを選択してみてください。
https://poruruba.github.io/utilities/(2020/2/16 修正)
・回転も加えました。操作方法
まずは、ドラッグアンドドロップで、アイコン化したい画像ファイルをドロップします。
アプリで使われるアイコンは、正方形ですが、ドロップするは画像は必ずしも正方形でなくてもよいです。
次に、これから作成したいアプリのOSを選択します。各OSによって、必要とされるアイコンのサイズが異なるためです。そして、scaleのところで、画像の拡大・縮小方法を選択します。
- cover:縦横比を維持せずに、画像を正方形に拡大あるいは縮小します。
- contain:縦横比を維持しつつ、画像全体が正方形に収まるようにします。余った領域は、透過色とします。
- crop:縦横比は維持しますが、画像の真ん中を中心に画像を切り出します。縦横のうち長い方は両端が削られます。
最後に、「ファイルに保存」ボタンを押下することで、複数のサイズのアイコンファイルをまとめたZIPファイルの保存ダイアログが表示されます。
例えば、Androidの場合には、以下のアイコンファイルがZIPファイルになっています。
36x26.png、48x48.png、72x72.png、96x96.png、144x144.png、192x192.png<出力されるアイコンサイズ>
OS アイコンサイズ Android 192x192, 144x144, 96x96, 72x72, 48x48, 36x36 iPhone 180x180, 167x167, 152x152, 120x120, 87x87, 76x76, 60x60, 58x58, 40x40, 29x29, 20x20 Windows 48x48, 32x32, 16x16 ソースコード
まずは該当部分のソースコードを示します。
start.js/* 画像ファイル */ image_open: function(e){ this.image_open_file(e.target.files[0]); }, image_drag: function(e){ e.stopPropagation(); e.preventDefault(); }, image_drop: function(e){ e.stopPropagation(); e.preventDefault(); $('#image_file')[0].files = e.dataTransfer.files; this.image_open_file(e.dataTransfer.files[0]); }, image_open_file: function(file){ if( !file.type.startsWith('image/') ){ alert('画像ファイルではありません。'); return; } var reader = new FileReader(); reader.onload = (theFile) =>{ this.image_type = file.type; this.image_src = reader.result; this.image_image = new Image(); this.image_image.onload = () =>{ this.image_size = { width: this.image_image.width, height: this.image_image.height }; this.image_scale_change(); }; this.image_image.src = this.image_src; }; reader.readAsDataURL(file); }, image_scale_change: function(){ if(!this.image_src) return; var image = this.image_image; var size, sx, sy, sw, sh, dx, dy, dw, dh; if( this.image_scale == 'cover'){ size = (image.width > image.height) ? image.width : image.height; sx = sy = 0; sw = image.width; sh = image.height; dx = dy = 0; dw = dh = size; }else if( this.image_scale == 'contain'){ size = (image.width > image.height) ? image.width : image.height; var x = Math.floor((size - image.width) / 2); var y = Math.floor((size - image.height) / 2); sx = 0; sy = 0; sw = image.width; sh = image.height; dx = x; dy = y; dw = image.width; dh = image.height; }else if( this.image_scale == 'crop'){ size = (image.width < image.height) ? image.width : image.height; var x = Math.floor((image.width - size) / 2); var y = Math.floor((image.height - size) / 2); sx = x; sy = y; sw = sh = size; dx = dy = 0; dw = dh = size; } var canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; var context = canvas.getContext('2d'); var angle = this.image_rotate; var trans = Math.floor(size / 2); context.translate(trans, trans); context.rotate(angle * Math.PI / 180); context.translate(-trans, -trans); context.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh); this.image_image_scaled = canvas; var canvas2 = $('#image_icon')[0]; canvas2.width = canvas.width; canvas2.height = canvas.height; var context2 = canvas2.getContext('2d'); context2.drawImage(canvas, 0, 0); }, image_save: async function(){ if(!this.image_src) return; var canvas = document.createElement('canvas'); var context = canvas.getContext('2d'); var zip = new JSZip(); var list = this.image_icon_list[this.image_icon]; for( var i = 0 ; i < list.length ; i++ ){ canvas.width = list[i]; canvas.height = list[i]; context.drawImage(this.image_image_scaled, 0, 0, this.image_image_scaled.width, this.image_image_scaled.height, 0, 0, canvas.width, canvas.height); var data_url = canvas.toDataURL('image/png'); var byteStr = atob( data_url.split( "," )[1] ) ; var content = new Uint8Array(byteStr.length); for( var j = 0; j < byteStr.length; j++ ) content[j] = byteStr.charCodeAt( j ) ; var blob = new Blob( [ content ], { type: this.image_type, }); var fname = list[i] + "x" + list[i] + '.png'; zip.file(fname, blob); } var zip_blob = await zip.generateAsync({type: "blob"}) var url = window.URL.createObjectURL(zip_blob); var a = document.createElement("a"); a.href = url; a.target = '_blank'; a.download = "icon_list.zip"; a.click(); window.URL.revokeObjectURL(url); }, image_click: function(e){ this.image_type = ''; this.image_src = null; e.target.value = ''; },その他のソースはGitHubを参照してください。
解説
各関数について、少し解説しておきます。
image_open
HTMLのinput type=”file” でファイルが選択されたときに呼び出されます。image_drag
マウスでファイルをドラッグ中に呼び出されます。まだドロップしていないので何もしていません。
以下は、ブラウザがドラッグ中のファイルを横取りされるのを防ぐためのものです。
e.stopPropagation();
e.preventDefault();image_drop
マウスでファイルをドロップされたときに呼び出されます。image_open_file
実際の画像ファイルの読み込み処理です。image_openによるファイルの選択時と、image_dropによるファイルのドロップ時に、共通で呼び出されます。
HTML5のFileAPIを使ってファイルを読み込み、いったんデータURL形式にして、後の画像処理用に保持しておくとともに、HTMLのimgに画像を当てています。image_scale_change
読み込んだ画像を、指定されたscaleに合わせて、整形します。Canvasを使います。
また、HTMLのcanvasに整形後の画像を当てています。image_save
アイコンサイズに合わせて画像をリサイズしファイルに出力します。ここでも、Canvasを活用します。
ファイルに出力する際に、複数のアイコンファイルを1つのファイルにするために、ZIP化しています。以下のライブラリを利用させていただきました。
JSZip
https://stuk.github.io/jszip/image_click
image_openでファイルを選択した後、再度同じファイル名を指定したときに、ファイル変更していても再読み込みしてくれないので、いったん選択ファイルを解除しています。以上
- 投稿日:2020-02-15T01:52:05+09:00
便利ページ:Javascriptでアイコンファイルを生成する
「便利ページ:Javascriptでちょっとした便利な機能を作ってみた」のシリーズものです。
今回は、1つの画像ファイルから、AndroidやiPhoneアプリで使う様々なサイズのアイコンを生成します。
いつもの通りGitHubにも上げてあります。
https://github.com/poruruba/utilities参考までに、以下にデモとしてアクセスできるようにしてあります。「画像ファイル」のタブを選択してみてください。
https://poruruba.github.io/utilities/(2020/2/16 修正)
・回転も加えました。操作方法
まずは、ドラッグアンドドロップで、アイコン化したい画像ファイルをドロップします。
アプリで使われるアイコンは、正方形ですが、ドロップするは画像は必ずしも正方形でなくてもよいです。
次に、これから作成したいアプリのOSを選択します。各OSによって、必要とされるアイコンのサイズが異なるためです。そして、scaleのところで、画像の拡大・縮小方法を選択します。
- cover:縦横比を維持せずに、画像を正方形に拡大あるいは縮小します。
- contain:縦横比を維持しつつ、画像全体が正方形に収まるようにします。余った領域は、透過色とします。
- crop:縦横比は維持しますが、画像の真ん中を中心に画像を切り出します。縦横のうち長い方は両端が削られます。
最後に、「ファイルに保存」ボタンを押下することで、複数のサイズのアイコンファイルをまとめたZIPファイルの保存ダイアログが表示されます。
例えば、Androidの場合には、以下のアイコンファイルがZIPファイルになっています。
36x26.png、48x48.png、72x72.png、96x96.png、144x144.png、192x192.png<出力されるアイコンサイズ>
OS アイコンサイズ Android 192x192, 144x144, 96x96, 72x72, 48x48, 36x36 iPhone 180x180, 167x167, 152x152, 120x120, 87x87, 76x76, 60x60, 58x58, 40x40, 29x29, 20x20 Windows 48x48, 32x32, 16x16 ソースコード
まずは該当部分のソースコードを示します。
start.jsfile_drag: function(e){ e.stopPropagation(); e.preventDefault(); }, /* 画像ファイル */ image_open: function(e){ this.image_open_file(e.target.files[0]); }, image_drop: function(e){ e.stopPropagation(); e.preventDefault(); $('#image_file')[0].files = e.dataTransfer.files; this.image_open_file(e.dataTransfer.files[0]); }, image_open_file: function(file){ if( !file.type.startsWith('image/') ){ alert('画像ファイルではありません。'); return; } var reader = new FileReader(); reader.onload = (theFile) =>{ this.image_type = file.type; this.image_src = reader.result; this.image_image = new Image(); this.image_image.onload = () =>{ this.image_size = { width: this.image_image.width, height: this.image_image.height }; this.image_scale_change(); }; this.image_image.src = this.image_src; }; reader.readAsDataURL(file); }, image_scale_change: function(){ if(!this.image_src) return; var image = this.image_image; var size, sx, sy, sw, sh, dx, dy, dw, dh; if( this.image_scale == 'cover'){ size = (image.width > image.height) ? image.width : image.height; sx = sy = 0; sw = image.width; sh = image.height; dx = dy = 0; dw = dh = size; }else if( this.image_scale == 'contain'){ size = (image.width > image.height) ? image.width : image.height; var x = Math.floor((size - image.width) / 2); var y = Math.floor((size - image.height) / 2); sx = sy = 0; sw = image.width; sh = image.height; dx = x; dy = y; dw = image.width; dh = image.height; }else if( this.image_scale == 'crop'){ size = (image.width < image.height) ? image.width : image.height; var x = Math.floor((image.width - size) / 2); var y = Math.floor((image.height - size) / 2); sx = x; sy = y; sw = sh = size; dx = dy = 0; dw = dh = size; } var canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; var context = canvas.getContext('2d'); var angle = this.image_rotate; var trans = Math.floor(size / 2); context.translate(trans, trans); context.rotate(angle * Math.PI / 180); context.translate(-trans, -trans); context.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh); this.image_image_scaled = canvas; var canvas2 = $('#image_icon')[0]; canvas2.width = canvas.width; canvas2.height = canvas.height; var context2 = canvas2.getContext('2d'); context2.drawImage(canvas, 0, 0); }, image_save: async function(){ if(!this.image_src) return; var canvas = document.createElement('canvas'); var context = canvas.getContext('2d'); var zip = new JSZip(); var list = this.image_icon_list[this.image_icon]; for( var i = 0 ; i < list.length ; i++ ){ canvas.width = list[i]; canvas.height = list[i]; context.drawImage(this.image_image_scaled, 0, 0, this.image_image_scaled.width, this.image_image_scaled.height, 0, 0, canvas.width, canvas.height); var data_url = canvas.toDataURL('image/png'); var byteStr = atob( data_url.split( "," )[1] ) ; var content = new Uint8Array(byteStr.length); for( var j = 0; j < byteStr.length; j++ ) content[j] = byteStr.charCodeAt( j ) ; var blob = new Blob( [ content ], { type: this.image_type, }); var fname = list[i] + "x" + list[i] + '.png'; zip.file(fname, blob); } var zip_blob = await zip.generateAsync({type: "blob"}) var url = window.URL.createObjectURL(zip_blob); var a = document.createElement("a"); a.href = url; a.target = '_blank'; a.download = "icon_list.zip"; a.click(); window.URL.revokeObjectURL(url); }, image_click: function(e){ this.image_type = ''; this.image_src = null; e.target.value = ''; },その他のソースはGitHubを参照してください。
解説
各関数について、少し解説しておきます。
image_open
HTMLのinput type=”file” でファイルが選択されたときに呼び出されます。file_drag
マウスでファイルをドラッグされたときに呼び出されます。まだドロップされていないので何もしません。image_drop
マウスでファイルをドロップされたときに呼び出されます。
以下は、ブラウザがドラッグ中のファイルを横取りされるのを防ぐためのものです。
e.stopPropagation();
e.preventDefault();image_open_file
実際の画像ファイルの読み込み処理です。image_openによるファイルの選択時と、image_dropによるファイルのドロップ時に、共通で呼び出されます。
HTML5のFileAPIを使ってファイルを読み込み、いったんデータURL形式にして、後の画像処理用に保持しておくとともに、HTMLのimgに画像を当てています。image_scale_change
読み込んだ画像を、指定されたscaleに合わせて、整形します。Canvasを使います。
また、HTMLのcanvasに整形後の画像を当てています。image_save
アイコンサイズに合わせて画像をリサイズしファイルに出力します。ここでも、Canvasを活用します。
ファイルに出力する際に、複数のアイコンファイルを1つのファイルにするために、ZIP化しています。以下のライブラリを利用させていただきました。
JSZip
https://stuk.github.io/jszip/image_click
image_openでファイルを選択した後、再度同じファイル名を指定したときに、ファイル変更していても再読み込みしてくれないので、いったん選択ファイルを解除しています。以上