20210606のvue.jsに関する記事は11件です。

Vue.jsでSPAを実現するためのVue Router

まず、SPAとは シングルページアプリケーションの略 必要な部分のみ置き換えて画面遷移させるアプリケーション設計 画面遷移の際にページ全体ではなく、必要な部分だけを遷移させるところがポイント 通常のページ遷移 SPAのページ遷移 (Vuexで作った素材 示している意味は違うけど、なんか通じそうだったから使ってみた) Vue Routerのインストール 既存プロジェクト配下に移動した上で下記を実行 vue add router ページ遷移時URLの部分にハッシュを使用しないヒストリーモードを使うかどうかの選択をする (ファイルに少し変更を加えるため、ファイル編集後にGitコミットしていない場合、インストールしても良いか警告が表示される) Use history mode for router? (Requires proper server setup for index fallback in production) こちらを yesか Noを選択後、インストールされる インストール完了後、Vue Routerを使用することができる 参考文献 基礎から学ぶVuejs
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

vuetifyのstorybookをchromaticにホスティングする

はじめに vueなどで開発したWeb画面の部品を描画してデザイナーとの齟齬をなくしたり、見た目のテストにstorybookが便利だと言われています。 vueの部品をきれいにするvuetifyライブラリにsotrybookを適用する方法がほとんど書かれていなかったのでまとめました。 さらにstorybookはchromaticにホスティングすることを推奨しているため、chromaticにホスティングする方法もまとめます。 環境 node:v12.18.2 npm:6.14.5 yarn:1.22.4 vue/cli:4.5.4 npx:6.14.5 前提 Node.jsとVue CLIのインストールは多くのサイトで説明されているので ここではインストール済みなことを前提とします。 Vuetifyプロジェクトの作成 Vueプロジェクトの作成 Vueプロジェクトをvue-cliを使用して作成します。 コマンドを打つと設定値の入力になります。設定値はstorybookには関係ないため自由で良いですが、例では以下のようにしています。 選択肢があるときはキーボードの上下で対象を選択してスペースで有効無効の切り替えエンターで決定です。 preset:Manually select freature 設定:TypeScrit、Router、Vuex、Unit Testing Vueバージョン:2.x クラススタイル:Yes BabelとTypeScriptの使用:Yes vue create vuetify-book-ts コンソール画面 Vueプロジェクトの確認 コマンドが正常終了するとVueプロジェクトが実行できるようになっているため、試しに実行してみます。 実行が完了するとコマンドを実行したプロンプトにアドレスが出るためブラウザからアクセスします。 cd vuetify-book-ts yarn serve ブラウザ画面 vuetifyのインストール 作成したプロジェクトにvuetifyのインストールをします。 設定値はデフォルトで問題ないです。 vue add vuetify vuetifyプロジェクトの確認 Vueプロジェクトの作成と同様にvuetifyのプロジェクトのインストールが完了するとvuetifyが適用されてVueプロジェクトが実行できるようになっています。 試しに実行します。実行が完了するとコマンドを実行したプロンプトにアドレスが出るためブラウザからアクセスします。 yarn serve ブラウザ画面 コンポーネントの作成 今はデフォルトのコンポーネントしかないため、自作のコンポーネントを作成します。 例なので単純なボタンです。 <!-- src/components/MyButton.vue --> <template> <v-btn depressed rounded :outlined="outlined" :color="color" :dark="dark">{{text}}</v-btn> </template> <script lang="ts"> import { Component, Prop, Vue } from 'vue-property-decorator' @Component export default class SimpleButton extends Vue { @Prop({ type: String, default: ""}) text!: string @Prop({ type: String, default: "primary"}) color!: string @Prop({ type: String, default: "primary"}) type!: string outlined = false dark = false created() { switch(this.type){ case "primary": this.outlined = false this.dark = true break case "inversion": this.outlined = true this.dark = false break } } } </script> ページの作成 コンポーネントだけでもstorybookに反映できるので特に必要はありませんが、せっかくなのでページも作成しておきます。 <!-- src/view/MyPage.vue --> <template> <v-container> {{ info }} <div> <my-button text="テスト"/> </div> <div> <my-button type="inversion" color="error" text="エラーテスト"/> </div> </v-container> </template> <script lang="ts"> import Vue from "vue"; import MyButton from '../components/MyButton.vue' import axiosInstance from '../utils/axios' export default Vue.extend({ name: "HelloWorld", mounted () { axiosInstance .get('http://localhost/v1/apt/test') .then(response => (this.info = response.data)) }, data: () => ({ info: null }), components: { MyButton, }, }); </script> axiosのクライアント作成 ページでaxiosを使用しているため、axiosのクライアントを作成するファイルを作成します。 // src/utils/axios.ts import axios from 'axios'; export default axios.create(); リンクの追加 ページまで作成できたので、このページにアクセスするためのリンクを追加します。 // src/router/index.ts import Vue from "vue"; ~~ 略 ~~ // 追加部分 import MyPage from "../views/MyPage.vue"; Vue.use(VueRouter); const routes: Array<RouteConfig> = [ { path: "/", name: "Home", component: Home, }, ~~ 略 ~~ // 追加部分 { path: "/myPage", name: "MyPage", component: MyPage, } ]; const router = new VueRouter({ routes, }); export default router; MyPageのチェック これでページがリンクに追加されたので、再度Vueを起動してブラウザからlocalhost:8080/myPageにアクセスして ボタンが2つあることを確かめます。 ブラウザ画面 storybookの作成 storybookのインストール storybookをインストールします。インストール対象は先ほど作成したプロジェクトですので上で使用していたプロンプトをそのまま使用します。 npx sb init storybookの実行 上のコマンドが終わるとstorybookのサンプルが実行できるため、確かめてみます。 storybookが起動できると自動的にブラウザが開かれてstorybookの画面が表示されます。 ここでは上で作成したコンポーネントとページは適用していないので、事前にあるサンプル以外は表示されていません。 yarn storybook ブラウザ画面 コンポーネントのstoryの作成 storybookは描画したい部品やページごとにstoryという設定を記載する必要があります。 まずは、MyButtonのstoryを作成します。export defaultには描画するコンポーネントと画面に表示する内容の設定をします。 Templateに設定するのはprops, コンポーネント, html部分を設定します。export const XXXXは1画面に表示するコンポーネントのpropsの設定をします。 // src/stories/MyButton.stories.js // 描画対象のインポート import MyButton from '../components/MyButton'; export default { // 描画対象の設定 component: MyButton, // 描画項目の設定 title: 'Components/MyButton', }; const Template = (args, { argTypes }) => ({ // propsに下で与えた値を与える props: Object.keys(argTypes), // 描画対象の設定 components: { MyButton }, // 描画用のhtml template: `<my-button v-bind="$props" v-on="$props"/> ` }) // storybookに表示する画面1 export const Primary = Template.bind({}) // propsの設定 Primary.args = { type: "primary", text: 'プライマリーボタン' } // storybookに表示する画面2 export const Inversion = Template.bind({}) Inversion.args = { type: "inversion", text: 'インバーションボタン' } // storybookに表示する画面3 export const ErrorInversion = Template.bind({}) ErrorInversion.args = { type: "inversion", color: "error", text: '強調ボタン' } storybookにvuetifyを適用する storybookに対してvuetifyを使用するためには、.storybook/preview.jsにvuetifyのインストールを記載します。 さらに、必要であればここでcssやpropertyの追加等を行います。 // .storybook/preview.js import Vue from 'vue' import Vuetify from 'vuetify' import 'vuetify/dist/vuetify.css' import colors from 'vuetify/es5/util/colors' const vuetifyOptions = {} // Vueに必要なものをインストールする Vue.use(Vuetify, {}) export const parameters = { actions: { argTypesRegex: "^on[A-Z].*" }, controls: { matchers: { color: /(background|color)$/i, date: /Date$/, }, }, } // storybookが使用するパラメータと大本のhtmlテンプレート export const decorators = [ () => { return ( { vuetify: new Vuetify(vuetifyOptions), template: '<v-app><story/></v-app>' } )} ] MyPageのstoryを作成する ページを作成したのでページのstoryも作成します。今回、MyPageにはaxiosを使用した通信を行っているため、モック化しています。 モック化のためにはaxiosのインスタンスをインポートしてMockAdapterに食わせます。その後、urlとレスポンスを設定することでモック化できます。 // src/stories/MyPage.stories.js import MockAdapter from 'axios-mock-adapter'; import MyPage from '../views/MyPage.vue'; // axiosのインスタンスをインポートします。 import axiosInstance from '../utils/axios' // axiosのモックアダプター作成 const mock = new MockAdapter(axiosInstance); // axiosのモックにURLとレスポンスを設定する mock.onGet('http://localhost/v1/apt/test').reply(200, {"sample": "OK"}) export default { component: MyPage, title: 'Page/MyPage', }; const Template = (args, { argTypes }) => ({ props: Object.keys(argTypes), components: { MyPage }, template: `<my-page v-bind="$props"/> ` }) export const Page = Template.bind({}) Page.args = {} storybookの実行 ここまで設定が終わるとstorybookの画面にコンポーネントとページが追加されています。 yarn storybook ブラウザ画面 ブラウザ画面を見るとCOMPONENTSにMyButton、PAGEにMyPageが追加されています。さらに、MyButtonには設定した3つの設定時の画面が描画できるようになっています。 Chromaticへのアップロード GitHubリポジトリの作成 GitHubのリポジトリからChromaticへ連携されるため上で作成したプロジェクトをリポジトリに追加します。 Chromaticはコミットが2つ以上ないとダメなようなので適当なものをコミットしておきます。 ChromaticとGitHubリポジトリの連携 Chromaticのホームページを開いて会員登録を行います。会員登録後にChromaticのホームページに表示されるGitHubを選択してGitHubのリポジトリなど必要なことを入力します。 プロジェクトのアップロード 必要なことを入力するとChromaticのページにChromaticのインストールコマンドとアップロードコマンドの2つが表示されるため作成したプロジェクトに対してコマンドを実行します。 コマンド実行後はChromaticのページの次ボタンが有効になっているはずなので次ボタンを押すとホスティングされたstorybookが表示されます。 ※この後、Chromaticからチュートリアルのメールが届きました。 終わりに storybookはWeb画面上でPropsを動的に変更できたり、いろいろ便利な機能があり触ってみると予想より良いものだと感じました。 さらに、ビジュアルテスト等に使用できるらしいのでより活用できればさらに便利になるのかなと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オリンピック調査サイトを作った話

はじめに(作った理由) 1.オリンピックチケットを持っているのですが開催されるかされないかまだわからない状態で他のみんなはどんな気持ちなのかを知りたかった。 2.Netflixでドハマリしていたブレイキング・バッドを観終わって暇でした。 の理由で作ってみました。 おすすめ→ブレイキング・バッド ※ハマり過ぎに注意です。 作ったもの 作ったのも この記事を書いている時点では皆さん真剣なコメントを書いてくれています(最初の数コメントは私が書きましたが) 機能 ・オリンピック開催に「賛成」「反対」「どちらでもいい」を投票できる。 ・集計結果をひと目で確認できる。 ・コメントを読める書き込める のシンプルな3つです 技術スタック フロントエンド Vue.js Vuetify Chart.js バックエンド FastAPI Boto3 - AWSのPython用SDK DB DynamoDB インフラ CloudFront - フロントのCDN S3 - フロントの配置 ALB - バックエンドのルーティング ECS - バックエンドの配置 Fargate - コンテナの実行環境 システム構成 おおまかに下記のような構成になっています。 SPAでサーバレス。 VueからのAPIをFastAPIが返すというような流れです。 フロントは、Vueをビルドして静的ファイルが生成できるので、それをそのままS3のWEBサイトホスティング機能を利用して公開しています。 ログイン機能を実装しないで複数回回答されないようにするにはどうしたらいいのかを考え、いろいろと案が浮かんだのですが一番手っ取り早いローカルストレージに回答ステータスを保存する方法で実装しました。(クリアされたらそこまでですが。。) バックエンドは、FastAPI(Python3.8)を初めて利用してみました。 今まではFlaskは触った事はあるのですが、最近のPythonのAPI系はDjangoやFlaskよりもFastAPIのほうがイケている、ということでなんとなく利用してみましたが、良かったです。 ドキュメントも日本語が整っていました。 標準でSwaggerUIが使えて おー! ってなりました。 あと本番用のコンテナイメージが用意されているのもポイントかもしれないですね! インフラは、始めてコンテナをデプロイしてみました。 ECSが思っていたよりも便利でよかったです。 マシンに「EC2」か「Fargate」を選べるのですが、せっかくなのでAWSのマネージドでサーバレスなFargateを使ってみました。(EC2よりも20%高いですが?。 今のAWSアカウントは無料期間なので贅沢しました) ローカルの開発用コンテナをイメージ化してほとんどそのままECRにプッシュして利用しています。 ECRへのプッシュの方法も丁寧に説明されているのでとても安心です。 UI/UX 私は元々インフラをやっており最近WEB業界に転職してプログラムをやるようになりました。 しかし、デザインに関してはまだあまりやったことがありませんでしたので誰でも簡単にいい感じのデザインをすることができるVuetifyに頼ることにしました。 Vuetifyとてもいいです。 グリッドシステムがあるのでモバイルへの対応も簡単です。 チャートにはChat.jsのvue用にラッパーされたvue-chartjsを利用しています。 少しだけ本家のChart.jsと仕様が違うので注意が必要です。 苦戦したところ あえて言うなら「DynamoDB」です!!!!!!!!!!!!! やっぱり検索(SQLでいうWHERE句)に弱いです。というよりまだなれていないので設計が甘いというのもあるかもしれませんが。。 今回でいうとコメントを表示する件数を絞るときなど、 DynamoDBではプライマリーキーとソートキーというクエリに利用するカラムのようなのを設定できるのですが、、 というのが問題でこの2つしか設定できません!! GSIという機能を利用すると他のカラムでクエリ条件を絞ることもできますがこちらも5個までという制約があります。 例えば下記のようなテーブルからageが何歳以上何歳以下というような条件で絞り込みたいと思ってもできません。 一つのカラムには一つの条件しかクエリできません。 そのようなことをしたいと思ったらscanコマンドを利用して一旦テーブルからすべてのデータを取得して自前のコードで値を絞り込むなどしないといけません。 そのような要件が最初から決まっているのであればDynamoDBの選定は再度検討してもいいかもしれません。 userId age name email 1 24 Olivia olivia@example.com 2 32 Emma emma@example.com 3 18 Isabella isabella@example.com 感想 今回はとてもシンプルなサイトでしたがUI/UXを考えるのが純粋にとても楽しかったです。 基本的にFargateやDynamoDBなどAWSのマネージドサービスを利用して、ほんとに簡単に構築できるし保守運用のことをあまり考えずにすみサービス開発に専念できると感じました。 あとはAWSを久しぶりに触ったのですが、コンソールが新しくなっていて「あれはどこいった?」みたいなこと多かったです。。 AWSのアップデート恐るべしです。こまめに触ったりするか、IaCで構築できるようになってということですかね。。 最後までご覧いただきありがとうございました。 今後もこのようなちょっとしたものを作って皆さんに共有しいきたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsとFastAPIとDynamoDBでオリンピック調査サイトを作った話

はじめに(作った理由) 1.オリンピックチケットを持っているのですが開催されるかされないかまだわからない状態で他のみんなはどんな気持ちなのかを知りたかった。 2.Netflixでドハマリしていたブレイキング・バッドを観終わって暇でした。 の理由で作ってみました。 おすすめ→ブレイキング・バッド ※ハマり過ぎに注意です。 作ったもの 作ったのも この記事を書いている時点では皆さん真剣なコメントを書いてくれています(最初の数コメントは私が書きましたが) 機能 ・オリンピック開催に「賛成」「反対」「どちらでもいい」を投票できる。 ・集計結果をひと目で確認できる。 ・コメントを読める書き込める のシンプルな3つです 技術スタック フロントエンド Vue.js Vuetify Chart.js バックエンド FastAPI Boto3 - AWSのPython用SDK DB DynamoDB インフラ CloudFront - フロントのCDN S3 - フロントの配置 ALB - バックエンドのルーティング ECS - バックエンドの配置 Fargate - コンテナの実行環境 システム構成 おおまかに下記のような構成になっています。 SPAでサーバレス。 VueからのAPIをFastAPIが返すというような流れです。 フロントは、Vueをビルドして静的ファイルが生成できるので、それをそのままS3のWEBサイトホスティング機能を利用して公開しています。 ログイン機能を実装しないで複数回回答されないようにするにはどうしたらいいのかを考え、いろいろと案が浮かんだのですが一番手っ取り早いローカルストレージに回答ステータスを保存する方法で実装しました。(クリアされたらそこまでですが。。) バックエンドは、FastAPI(Python3.8)を初めて利用してみました。 今まではFlaskは触った事はあるのですが、最近のPythonのAPI系はDjangoやFlaskよりもFastAPIのほうがイケている、ということでなんとなく利用してみましたが、良かったです。 ドキュメントも日本語が整っていました。 標準でSwaggerUIが使えて おー! ってなりました。 あと本番用のコンテナイメージが用意されているのもポイントかもしれないですね! インフラは、始めてコンテナをデプロイしてみました。 ECSが思っていたよりも便利でよかったです。 マシンに「EC2」か「Fargate」を選べるのですが、せっかくなのでAWSのマネージドでサーバレスなFargateを使ってみました。(EC2よりも20%高いですが?。 今のAWSアカウントは無料期間なので贅沢しました) ローカルの開発用コンテナをイメージ化してほとんどそのままECRにプッシュして利用しています。 ECRへのプッシュの方法も丁寧に説明されているのでとても安心です。 UI/UX 私は元々インフラをやっており最近WEB業界に転職してプログラムをやるようになりました。 しかし、デザインに関してはまだあまりやったことがありませんでしたので誰でも簡単にいい感じのデザインをすることができるVuetifyに頼ることにしました。 Vuetifyとてもいいです。 グリッドシステムがあるのでモバイルへの対応も簡単です。 チャートにはChat.jsのvue用にラッパーされたvue-chartjsを利用しています。 少しだけ本家のChart.jsと仕様が違うので注意が必要です。 苦戦したところ あえて言うなら「DynamoDB」です!!!!!!!!!!!!! やっぱり検索(SQLでいうWHERE句)に弱いです。というよりまだなれていないので設計が甘いというのもあるかもしれませんが。。 今回でいうとコメントを表示する件数を絞るときなど、 DynamoDBではプライマリーキーとソートキーというクエリに利用するカラムのようなのを設定できるのですが、、 というのが問題でこの2つしか設定できません!! GSIという機能を利用すると他のカラムでクエリ条件を絞ることもできますがこちらも5個までという制約があります。 例えば下記のようなテーブルからageが何歳以上何歳以下というような条件で絞り込みたいと思ってもできません。 一つのカラムには一つの条件しかクエリできません。 そのようなことをしたいと思ったらscanコマンドを利用して一旦テーブルからすべてのデータを取得して自前のコードで値を絞り込むなどしないといけません。 そのような要件が最初から決まっているのであればDynamoDBの選定は再度検討してもいいかもしれません。 userId age name email 1 24 Olivia olivia@example.com 2 32 Emma emma@example.com 3 18 Isabella isabella@example.com 感想 今回はとてもシンプルなサイトでしたがUI/UXを考えるのが純粋にとても楽しかったです。 基本的にFargateやDynamoDBなどAWSのマネージドサービスを利用して、ほんとに簡単に構築できるし保守運用のことをあまり考えずにすみサービス開発に専念できると感じました。 あとはAWSを久しぶりに触ったのですが、コンソールが新しくなっていて「あれはどこいった?」みたいなこと多かったです。。 AWSのアップデート恐るべしです。こまめに触ったりするか、IaCで構築できるようになってということですかね。。 最後までご覧いただきありがとうございました。 今後もこのようなちょっとしたものを作って皆さんに共有しいきたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPとVue.jsを使って「本のレンタルアプリ」を作成した話

はじめに PHPとVue.jsを使った「本のレンタルアプリケーション」を作成した時の話や、構成をまとめてみました。 Web系のプログラミングを大方学んでみて、実際に何か作ってみようと考えている人や、複数のプログラミングや技術を合わせて使ったことのない人に参考にしていただければと思います。 PHPとVue.jsでなくても、RubyやjQueryなどでも代用はできますし、どちらかといえば環境構築やデータベースの構成のほうをメインに書いています。 図書レンタルアプリを作った理由 今回この図書レンタルアプリを作ろうと思ったのは、一から一人だけで、サービスを作る経験を積んでおきたかったからです。 業務では基本的に分業が多く、フロントエンドとバックエンドで別れてサービスを構築するのが基本で、新しくサービスを一から作る機会自体少ないです。 基本は現状あるサービスの改修や、修正がメインのため、一からサービスをコンスタントに作って、場数を踏みたいと思ったのがきっかけです。 そして何を作ろうかと考えたときに、会社の本棚の本の管理ができていなかったので、誰が何をいつまで借りているのかや、本のレンタル履歴を管理できるアプリケーションを作ってみようと思ったのがきっかけです。 使用した技術 使用した技術 PHP Vue.js MySQL Docker アプリを作り始めた時の自分のスキルレベル javascript javascriptは実務でもメインで使っているため、問題なくかけるレベルです。 ライブラリーやフレームワークにつては、jQueryとVue.jsが使えました。 jQueryは結構知識はありましたが、Vue.jsの経験は少なめでした。 Vue.js Vue-CLI、vue-router、VueXの基礎知識をudemyの動画を通して学んだくらいです。 仕事ではVueファイル形式で書いたことはなく、今回Vueファイル形式で、ビルドする書き方ははじめてでした。 PHP PHPも実務での経験は少なく、PHPで一からサービスを作ったことはありませんでした。 実務で、今あるサービスの改修や修正、機能追加などでPHPを触るくらいの経験しか有りませんでした。 Docker Docker関しては、完全に初めてで、素人でした。 それまではVirtualBoxでlinuxの環境を作って、開発を行っていました。 なのでlinuxコマンドなどの知識はある前提です。 スキルレベルのまとめ 一応現役のエンジニアではありますが、基本的に一からのサービス構築の経験は少なく、あってもメインで携わって来たのはフロント側のみで、バックエンドやインフラはすでに構築されているものを少し触るくらいの経験しか積んできませんでした。 キャリア的にももっと経験を積まないとまずいと思い、ここらで一気に経験を積みたいと思い、とりあえず「PHP」「Vue.js」「MySQL」「Docker」この辺の技術を使って、一人で一から簡単なサービスを作るところから始めようと思いました。 図書レンタルアプリの機能 バックエンドとユーザー登録、ログインのページはPHP それ以外のフロントエンドはVue.jsで作成しました。 実装する機能のリスト ・ユーザー登録機能 ・ログイン機能 ・ログアウト機能 ・レンタル機能 ・レンタル状況確認機能 ・返却機能 ・返却期限編集機能 ・ユーザー情報編集機能 ・ランキング機能 ・本の登録機能 アプリのデザインについて 普段の業務で自分であまり使わないのですが、デザインを作成するのにXDを使い、簡単にデザインを作成しました。 今回はXDのスキルではなく、Web開発のエンジニアスキルを高めたかったので、デザインはなるべくシンプルにしました。 もともとミニマムなデザインが好きなので、色もモノトーンベースにしています。 PHPで作成した「ユーザー登録ページ」と「ログインページ」     Vue.jsで作成したシングルページアプリケーション(SPA)      データベースについて データベースはMySQLを使用しました。 MySQLにlibraryというデータベースを作成し、その中に「members」「book」というテーブルを作成しました。 データベースの構成と役割 データベース:library   テーブル   └book   └members book bookには本の情報を保存します。 book_id:本を識別するID title:本のタイトル status:本がレンタル中or在庫あるか rental_id:レンタルされている場合はmemberのid、レンタルされていない場合はNULL date_rimit:レンタルされている場合はレンタル期限、レンタルされていない場合はNULL category:本のカテゴリー tag:本の検索に使うtag count:レンタルされた回数 created_at:本が登録された日付 members membersにはアプリを使用する人のログイン情報や、レンタルしている本のidのリストを保存します。 id:ユーザーを識別するID name:ユーザーの名前 mail:ユーザーのメールアドレス password:ログインするためのパスワード created:ユーザー登録した日付 rental_list:現在レンタルしている本のbook_idのリスト 各ファイルの役割 PHPについて PHPでCRUD機能を実装しました。 ユーザーの登録機能(Create) ユーザのログイン機能(Read) ユーザー、本の管理機能(Read) ユーザー、本の情報書き換え(Update) ※Dleteに当たる機能は今回有りませんでした。 Vue.jsで作成したSPAから、本を検索したり、レンタルしたりするときにPHPを動かす必要がありますが、axiosを使い通信を行いました。 Vue.jsについて ※この話はVue.jsを学んでいる人ならわかると思いますが、Vue.jsに興味がない場合は飛ばしてください Vuejsで作成したSPAのページはタブ構成になっており、タブの切替はVue-routerを使って、urlを切り替えてタブ機能を実装しています。 各タブの中のページの切り替え(例えば、レンタルタブの中で、本を検索したり、本をレンタルする際にページの切り替え)は、動的コンポーネントで表示を切り替えています。 レンタルタブ      状況タブ(レンタル状況と返却) Dockerでの環境構築 今回は開発環境としてはDockerでLAMP環境を構築することにしました。 Docker-composeを使って、複数のコンテナをたててLAMPを構築しました。 参考までにymlファイルと、dockerファイルを公開しておきます。 yml version: "3.7" services: mysql: image: mysql:5.7 volumes: - db_data:/var/lib/mysql restart: always environment: MYSQL_ROOT_PASSWORD: "password" phpmyadmin: depends_on: - mysql image: phpmyadmin/phpmyadmin environment: PMA_HOST: mysql restart: always ports: - "8080:80" php-apache: build: ./php volumes: - ./htdocs:/var/www/html restart: always ports: - "80:80" depends_on: - mysql volumes: db_data: {} ./phpの直下に書きを保存 dockerfile FROM php:7.3-apache COPY ./php.ini /usr/local/etc/php/ COPY ./apache2.conf /etc/apache2/ COPY ./sites/*.conf /etc/apache2/sites-available/ RUN apt-get update \ && apt-get install -y libfreetype6-dev libjpeg62-turbo-dev libpng-dev \ && docker-php-ext-install pdo_mysql mysqli mbstring gd iconv 上記のファイルで下記のコマンドを実行すれば、LAMP環境が立ち上がります。 terminal docker-compose up -d Dockerでの環境構築で困ったこと 今回一番困ったのがDockerで作ったapacheの設定です。 私も初めて知ったのですが、公式で公開されている「php:7.3-apache」などのApacheは、あくまでphpが動くだけのapacheの環境しか用意していないため、普通のapacheの環境でデフォルトで設定されているようなものは、デフォルトで入っていないことがあります。 今回ハマったのが、Rewriteのモジュールです。 Vue-routerを使った時に、historyモードを使用していると、リロードした時に404エラーが出てしまいます。これはVueの公式にも記載されています。 それを回避するために、apacheの設定ファイルか、.htaccessでRewriteの設定をする必要があるのですが、そのためのモジュールがapacheにはありませんでした。 それに気づかずに、設定ファイルにRewriteの記述を書いても問題が解決せずにいました。 今回の場合は、それをphp:7.3-apacheに入り、インストールして解決しましたが、本来であれば、Dockerfileにそれを記載しておく必要があります。 そうしないと次回コンテナを新しく立ち上げた時に、また、同じことをしないといけないからです。 かなり勉強になりました。 アプリケーションを作ってみた感想 今回一番新しい試みだったのが、Vue.js(Vuex、Vue-router)とDockerでの開発でしたが、初めてだったので、ディレクトリ構造などが少し納得できるくらい整理ができず、本当は記事にして、詳細まで説明しようと思っていたのですが、できませんでした。 ただ、一度作ってみることで、ディレクトリ構造以外にも、設計やデータベースの構成など、ワンストップで開発しないと経験できないことを多く学ぶことができました。 やはり部分的に開発しているだけでは、見えていないことや、自分のスキルとして足りていないことが多くあることを発見でき、やってみてよかったと思いました。 AWSの使用について(余談) ちょっと今回の話に関係ないののですが、少し話をさせてください。 このアプリを作る際に、本番のアップロード先について最初はAWSで、VPCとEC2を使ってネットワークとサーバーを立てて、そこで公開しようと思っていました。 ですが、この手のアプリケーションを作る時に、流行っているからというだけでAWSを使うのはちょっと違うと考えました。 特に初学者の人は手を出しやすい場所だと思うのですが、AWSは基本的に従量課金のため、サーバーが動いている分だけ料金が課金されていきます。 lamdaなどを使って、アプリケーションが動くときだけ稼働させるなどの工夫をすればいいのですが、割と初学者にとってはハードルが高いため、最初はおすすめしません。 もちろんAWSの技術をみにつける目的があるのであれば、試すのもありですが、今回はインフラ周りというよりも動くものを作るのが目的だったので、さくらサーバーなどのレンタルサーバーを使用しました。 というのも会社で使うアプリケーションのため、サーバーやAWSは会社のものを使うので、コストは最小限に抑える必要があったからです。 プログラミングを学ぶときにはこういったコストのことも考えながら作ると、より実践向きのエンジニアに成長できると思うので、今回関係ないですが、この話をはさみました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue × Firebase】子コンポーネントから親コンポーネントへ$emitで渡す場合

子コンポーネントから親コンポーネントへ$emitで渡す場合 子コンポーネント(post.vue) <input placeholder="例)アクション 恋愛 ミステリー SF ホラー ミュージカル etc.." class="search-main-item" type="search" @input="searchData($event.target.value)"/> <!--searchDataというカスタムイベント(今回は@input)を設定して、valueを渡す。valueを渡す際は「$event.target」が必要。--> method:{ searchData(value) { this.$emit("searchData", value); } //第一引数にメソッド名(searchData)、第二引数にデータを指定して$emitで親コンポーネントへ値を渡す。 } }, 親コンポーネント(board.vue) <Post @searchData="search" /> <!--子コンポーネントから$emitで受けたデータを関数とする。※searchに()を付けるとvalueの引数が取れないので注意。--> search(value){ ~~ 構文 ~~ console.log(value) } コンソールでvalueを確認すると子コンポーネントから受けた値が受けれていることを確認出来ます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【エラー解決】@vue/cli導入で「vue: command not found」と表示された場合の対処方法

参考にした記事  vue/cli導入 $ npm install -g @vue/cli $ npm bin -g /Users/#{myname}/.npm-global/bin (not in PATH env variable) $ vue create my-project -bash: vue: command not found  エラー解決方法 パスを通す export PATH=$PATH:`npm bin -g` $ vue -V @vue/cli 4.5.13 参考記事 Why wont my npm package commands work?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js+Firebaseでメールフォームを作成する

はじめに Vue.jsで制作したSPAにFirebaseFunctionsを用いて簡単にメールフォームを設置できました。 制作にあたって、こちらの記事を参考にさせていただきました。 適用したVue.jsのアプリはこちら↓ 1. FirebaseFunctionsの追加とメール通知処理の実装 こちらの記事の  1. プロジェクトの追加  2. Firebase functionでのメール通知処理の実装 を参考にさせていただきました。 Functionsを使用するにあたって有料枠にする必要がありますが、従量課金制度なので大量にメールが送られてこない限り、無料で使うことができます。 また、2.のconfig内の値はFirebaseプロジェクトコンソールのcdnチェックボックス内に記載されています。 2. ContactFormコンポーネントの作成 component配下でContact.vueファイルを作成します。 Contact.vue <template> <div class="contact"> <h1>Contact</h1> <v-container> <v-form ref="form" v-model="contactFormValidation.valid" lazy-validation > <v-text-field v-model="contactForm.name" :rules="contactFormValidation.nameRules" label="name" required ></v-text-field> <v-text-field v-model="contactForm.email" :rules="contactFormValidation.emailRules" label="email" required ></v-text-field> <v-textarea v-model="contactForm.contents" :rules="contactFormValidation.contentsRules" label="contents" required ></v-textarea> <v-btn :loading="contactForm.loading" :disabled="!contactFormValidation.valid" @click="sendMail()" color=primary block large >submit </v-btn> </v-form> </v-container> <v-snackbar v-model="snackBar.show" :color="snackBar.color" bottom right :timeout="6000" > {{snackBar.message}} </v-snackbar> </div> </template> <script> import { functions } from '@/plugins/firebase.js' export default { data: () => ({ contactForm: { name: '', email: '', contents: '', loading: false }, contactFormValidation: { valid: false, nameRules: [v => !!v || '必須項目です'], emailRules: [ v => !!v || '必須項目です', v => /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test(v) || 'メールアドレスが正しくありません' ], contentsRules: [v => !!v || '必須項目です'] }, snackBar: { show: false, color: '', message: '' } }), methods: { sendMail: function () { if (this.$refs.form.validate()) { this.contactForm.loading = true const mailer = functions.httpsCallable('sendMail') mailer(this.contactForm) .then(() => { this.formReset() this.showSnackBar( 'success', 'お問い合わせありがとうございます。送信が完了しました。' ) }) .catch(err => { this.showSnackBar( 'error', '送信に失敗しました。時間をおいて再度お試しください。' ) console.log(err) }) .finally(() => { this.contactForm.loading = false }) } }, showSnackBar: function (color, message) { this.snackBar.message = message this.snackBar.color = color this.snackBar.show = true }, formReset: function () { this.$refs.form.reset() } } } </script> 空欄だったり、メールアドレスが正しくないと送信できないようにvalidationを設けました。 特にメールアドレスのvalidationは下記の記事で詳しく解説しています。 また、App.vueにContact.vueを読み込ませることも忘れずに行いましょう。 3. メールサーバの設定・デプロイ 先ほどと同様にこちらの記事の  4. メールサーバーの設定・デプロイ を参考にさせていただきました。 さいごに Firebaseを使うとサーバサイドの処理をほとんど記述することなく、メールフォームを実装できるので驚きました。 v-modelでの双方向バインディングで入力情報を常にチェックできるのでVue.jsを用いてよかったと思います。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】ローディング画面を実装する

はじめに vue-loading-templateモジュールを用いて、Vue.jsのアプリにローディング画面を実装しました Vue.jsのアプリにはVuetifyを使用しています 適用したVue.jsのアプリはこちら↓ . vue-loading-templateのインストール $ npm add vue-loading-template src/App.vue App.vue <template> <v-app id="inspire" :style="{background: $vuetify.theme.dark}" >   <!-- ローディング --> <Loading v-show="isLoading"></Loading> <v-main :style="{width:'90%', margin: '3vh auto'}" v-show="!isLoading" > <!-- 省略 --> </v-main> <v-footer color="grey darken-2" padless v-show="!isLoading" > <v-col class="text-center white--text" cols="12" > {{ new Date().getFullYear() }} — <strong>Komekami</strong> </v-col> </v-footer> </v-app> </template> <script> // Loadingコンポーネントの読み込み import Loading from '@/components/Loading' export default { data: () => ({ isLoading: true, drawer: null, items: [ { title: 'Top', icon: 'mdi-view-dashboard', link: '/' }, { title: 'Profile', icon: 'mdi-account', link: '/profile' }, { title: 'Gallery', icon: 'mdi-image', link: '/gallery' }, { title: 'Contact', icon: 'mdi-forum', link: '/contact' } ], right: null }), // Loading切り替え処理 mounted () { setTimeout(() => { this.isLoading = false }, 1000) }, components: { Loading } } </script> Point ローディング画面コンポーネントをLoadingとして読み込んでいます 変数isLoadingを用いてtrueかfalseかでローディング画面の表示/非表示を行っています。 mountedを使用し切り替えを行っています。 mountedはDOMの作成完了時にisLoading=falseとなり、ローディング画面が非表示になります。 メイン部(v-main)とフッター部(v-footer)は変数isLoadingがfalseで描画(v-show)されます。 src/components/Loading.vue ローディング画面のコンポーネントです。 Loading.vue <template> <div v-show="isLoading"> <div class="fullview"> <div class="loading-spacer"></div> <vue-loading type="beat" color="#304686" :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:black; position: fixed; top: 0; left: 0; } .loading-spacer { height: 40%; } </style> Point VueLoadingをインポート <vue-loading>内でLoadingの各種設定を行っています 今回はローディングの動き方をtype="beat"にしました 他にも色々typeがあるのでvue-loading-templateをチェックしてみてください さいごに モジュールをインストールすることで簡単にローディング画面を実装することができました。 SPAは初回読み込み時に時間がかかりがちなので、ローディング画面を実装するとサイト訪問者のストレスも軽減されるのではないかと思います。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

webpack + Babel を使ってIE対応したつもりが、アロー関数が残っていてIEで実行できなかった件

あるプロジェクトで、webpackでVue.jsの環境構築をしました。 Babelを導入しトランスパイルの設定もしたので、Internet Explorer (IE11) でも動くだろうと思っていました。 しかし、実際にIE11で確認してみると、Vue.jsで作ったアプリケーションが全く動かない。。 コンソールを見てみると、JavaScriptのエラーが出ていました。 なんでだろうと思い、コンパイル後のファイルを覗いてみると、、 app.js (()=>{ ... })(); いや、アロー関数残ってるやないかーい!! IE11はいまだにアロー関数に対応してないので、これだとエラーになってしまいます。 うまくいかなかったときのwebpackの設定 当初、webpackの設定ファイルの中身はこんな感じでした。 webpack.config.js const VueLoaderPlugin = require('vue-loader/lib/plugin') module.exports = { entry: { app: './src/app.js', }, output: { filename: '[name].js', path: __dirname + '/dist' }, module: { rules: [ { test: /\.vue$/, loader: 'vue-loader' }, { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } ] }, plugins: [ new VueLoaderPlugin() ], resolve: { alias: { 'vue$': 'vue/dist/vue.esm.js' }, } } ちゃんとBabelインストールして設定したのに、何がいけないの? って感じでした。 Babelの設定の問題だと思い、いろいろ試し、何時間も格闘しました。 が、原因はBabelではありませんでした。 改良後のwebpackの設定 改良後のwebpackの設定ファイルはこんな感じです! webpack.config.js const VueLoaderPlugin = require('vue-loader/lib/plugin') module.exports = { entry: { app: './src/app.js', }, output: { filename: '[name].js', path: __dirname + '/dist', environment: { arrowFunction: false } }, module: { rules: [ { test: /\.vue$/, loader: 'vue-loader' }, { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } ] }, plugins: [ new VueLoaderPlugin() ], resolve: { alias: { 'vue$': 'vue/dist/vue.esm.js' }, } } さて、みなさん、何が変わったかお気づきでしょうか? なんと、たった3行追加しただけです! environment: { arrowFunction: false } この3行をoutputの中に入れただけです! よく見ると、webpackの公式ドキュメントに、さらっと書いてあります。さらっと。。 https://webpack.js.org/configuration/output/#outputenvironment こんなのわかるわけないじゃないか!! 上記の設定をすることで、webpackがアロー関数を出力しないよう制御できるみたいです。 結果、コンパイル後のファイルを見てみると… app.js !function(){ ... }(); やった! やっと関数がfunctionになったぞ! IEで確認してみると、、 無事に動きました!! めでたしめでたし みなさんも、Babel導入してちゃんと設定したのにアロー関数が残っているなーと思ったら、上記設定を試してみてください。 僕みたいに無駄に格闘して消耗する被害者が減ることを願っています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerでDjango+Vue+NginxのSPAを作る

Docker composeの復習がてらSPAサイトの雛形コンテナ構成を最速ビルドしてみる 可能な限り初期ファイル構成はシンプルかつ最小限で。 ディレクトリ&コンテナ構成 docker_spa/ ┣━ back/ ┃ ┣━ scripts/ ┃ ┃ ┣━ django-appstart.sh # Django新規アプリ作成用スクリプト ┃ ┃ ┣━ django-settings.sh # Djangoセットアップ用スクリプト ┃ ┃ ┗━ start.sh # コンテナ起動関連スクリプト ┃ ┣━ Dockerfile # バックエンド用Dockerfile ┃ ┗━ requirements.txt # pythonパッケージ一覧 ┣━ front/ ┃ ┣━ scripts/ ┃ ┃ ┣━ start.sh # 「settings.py」修正用スクリプト ┃ ┃ ┗━ vue-settings.sh # vue用スクリプト ┃ ┗━ Dockerfile # フロントエンド用Dockerfile ┣━ web/ ┃ ┣━ conf/ ┃ ┃ ┗━ app_nginx.conf # アプリ用Nginx設定ファイル ┃ ┣━ logs/nginx ┃ ┃ ┗━ access.log # アクセスログ ┃ ┃ ┗━ error.log # エラーログ ┃ ┣━ Dockerfile # webサーバー用Dockerfile ┃ ┗━ uwsgi_params # アプリサーバー設定ファイル ┃ ┗━ docker-compose.yml docker-compose.yml docker-compose.yml version: '3.7' services: web: container_name: web-container build: context: ./ dockerfile: ./web/Dockerfile restart: unless-stopped # コンテナが異常停止した場合は再起動する ports: - "8000:8000" environment: TZ: "Asia/Tokyo" volumes: - ./web/conf:/etc/nginx/conf.d - ./web/logs/nginx/:/var/log/nginx/ - ./web/uwsgi_params:/etc/nginx/uwsgi_params - ./web/static:/static - django_statics:/var/www/vhosts/localhost/static:ro networks: - django_net depends_on: - back back: container_name: back-container build: context: ./back dockerfile: ./Dockerfile command: 'sh /code/scripts/start.sh' restart: unless-stopped volumes: - ./back:/code - ./back/app/static:/static expose: - "8001" networks: - django_net front: container_name: front-container build: context: ./front dockerfile: ./Dockerfile # ./front/Dockerfileを参照 ports: - 8080:8080 expose: - "3000" command: 'sh /code/scripts/start.sh' volumes: - ./front:/code - ./static:/static networks: - django_net depends_on: - back networks: django_net: driver: bridge volumes: django_statics: driver: local 詳細な導線は異なるところはあると思いますが、上記がComposeのコンテナ構成イメージです。 ページアクセスのリクエストをwebサーバーコンテナ内のNginxがプロキシサーバーとして中継 バックエンドコンテナ内のアプリサーバーのuWSGIでバックエンドとNginxとの疎通 POSTリクエストをAPIサーバーとしてDjango(REST Framework)で処理 Vueのaxiosのプロキシサーバーと連結してレスポンスオブジェクトをVueの非同期で受け取る こんな流れで最小限のSPA環境をビルドします。 今回はwebアプリとしてVueからしか繋いでないのに無駄に複雑な構成になってますが、専用コンテナをcomposeに追加してそこからのリクエストをAPIサーバーに処理してもらってNginx経由でアクセスを振り分けてもらえば将来的にネイティブアプリやデスクトップアプリなどクロスプラットフォーム対応した際にもコンテナ同士の構造を意識しなくても良くなる・・・ハズ・・・。 バックエンド側 APIサーバー処理用のバックエンド側のコンテナ構成 FROM python:3.8 ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 EXPOSE 8000 # 開発ディレクトリ設定 ENV WORKDIR /code WORKDIR ${WORKDIR} COPY ./scripts/ ${WORKDIR}/ # コンテナ内でパッケージを一括インストール COPY requirements.txt ${WORKDIR} RUN pip install --upgrade pip && pip install -r requirements.txt && \ pip install coreapi django-cors-headers python-jose COPY . ${WORKDIR}/ コンテナ起動関連スクリプト 初回ビルド時にappフォルダ&新規プロジェクトを作成 同時に各種セットアップのスクリプトを実行 migrationとuWSGIサーバーを起動(※2回目以降はここのみ実行) back/scripts/start.sh #!/bin/bash set 'echo -e -o pipefail' unix_today=$(date +'%s') unix_today=$((unix_today+32400)) jst_ymd_today=$(date '+%Y/%m/%d %H:%M:%S' --date "@$unix_today") # メインプロジェクトフォルダが無ければセットアップ実行 if [ ! -d /code/app ]; then mkdir app && cd app django-admin startproject config . bash /code/scripts/django-settings.sh bash /code/scripts/django-appstart.sh main fi cd app python manage.py makemigrations python manage.py migrate # python manage.py collectstatic # 静的ファイルディレクトリをstatic指定ディレクトリに集約コピー(--noinputで対話プロンプトを無視) python manage.py collectstatic --noinput # configアプリuWSGIに接続。「--py-autoreload 1」でファイル等に変更があった際は自動リロード uwsgi --socket :8001 --module config.wsgi --py-autoreload 1 --logto /tmp/uwsgi.log Djangoセットアップ用スクリプト 「config/settings.py」の各種セットアップ タイムゾーンと言語を日本語対応 REST Framework設定を追記 staticディレクトリ設定 「config/url.py」のセットアップ 静的ファイル用「template」ディレクトリ作成 back/scripts/django-appstart.sh #!/bin/sh # シェル実行が失敗したら終了してエラー出力 set 'echo -e -o pipefail' # ルートディレクトリ取得 ROOT_DIR=$(cd $(dirname $0)/..;pwd) echo ${ROOT_DIR} # 各種モジュールインポートを追記 if ! grep -q "import os" "${ROOT_DIR}/app/config/settings.py" ;then sed -i -e "s/from pathlib import Path/from pathlib import Path \nimport os\nimport json\nfrom six.moves.urllib import request\nfrom cryptography.x509 import load_pem_x509_certificate\nfrom cryptography.hazmat.backends import default_backend/" ${ROOT_DIR}/app/config/settings.py fi # BASE_DIR変更 sed -i -e "s/BASE_DIR = Path(__file__).resolve().parent.parent/BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))/" ${ROOT_DIR}/app/config/settings.py # INSTALLED_APPSにrest_frameworkとrest_framework_jwtとcorsheadersを追加 if ! grep -q "'rest_framework'," "${ROOT_DIR}/app/config/settings.py" ;then sed -i -e "s/'django.contrib.staticfiles',/'django.contrib.staticfiles',\n 'rest_framework', \n 'rest_framework_jwt', \n 'corsheaders',/" ${ROOT_DIR}/app/config/settings.py fi # MIDDLEWAREにcorsheadersミドルウェアを追記 if ! grep -q "'corsheaders.middleware.CorsMiddleware'," "${ROOT_DIR}/app/config/settings.py" ;then sed -i -e "s/'django.middleware.common.CommonMiddleware',/'corsheaders.middleware.CorsMiddleware', \n 'django.middleware.common.CommonMiddleware', \n/" ${ROOT_DIR}/app/config/settings.py fi # テンプレートディレクトリ設定 sed -i -e "s/'DIRS': \[\],/'DIRS': \[os.path.join(BASE_DIR, \"app\/templates\")\],/" ${ROOT_DIR}/app/config/settings.py # 言語をjaに変更 sed -i -e "s/LANGUAGE_CODE = 'en-us'/LANGUAGE_CODE = 'ja'/" ${ROOT_DIR}/app/config/settings.py # タイムゾーンをAsia/Tokyoに変更 sed -i -e "s/TIME_ZONE = 'UTC'/TIME_ZONE = 'Asia\/Tokyo'/" ${ROOT_DIR}/app/config/settings.py # DATEBASEパス変更 sed -i -e "s/'NAME': BASE_DIR \/ 'db.sqlite3',/'NAME': BASE_DIR+'\/app\/db.sqlite3',/" ${ROOT_DIR}/app/config/settings.py # ALLOWED_HOSTSにlocalhostと127.0.0.1追加 sed -i -e "s/ALLOWED_HOSTS = \[\]/ALLOWED_HOSTS = \[\n '127.0.0.1', \n 'localhost', \]/" ${ROOT_DIR}/app/config/settings.py # STATIC_ROOT追記 sed -i -e "s/STATIC_URL = '\/static\/'/STATIC_URL = '\/static\/'\nSTATIC_ROOT = os.path.join(BASE_DIR, 'static')/" ${ROOT_DIR}/app/config/settings.py # REST_FRAMEWORK設定追記 if ! grep -q "REST_FRAMEWORK = {" "${ROOT_DIR}/app/config/settings.py" ;then echo "# REST_FRAMEWORK設定 REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', ), 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', ), } # クロスオリジン許可 CORS_ORIGIN_WHITELIST = ( 'http://localhost:8080', ) " >> ${ROOT_DIR}/app/config/settings.py fi # importにincludeモジュールを追加&napp_name設定 sed -i -e "s/from django.urls import path/from django.urls import path, include \nfrom rest_framework.documentation import include_docs_urls\n/" ${ROOT_DIR}/app/config/urls.py # APIドキュメントルート追加 sed -i -e "s/path('admin\/', admin.site.urls),/path('admin\/', admin.site.urls),\n path('docs\/', include_docs_urls(title='API Document')),\n/" ${ROOT_DIR}/app/urls.py # templates&staticフォルダ階層作成 mkdir ${ROOT_DIR}/app/templates mkdir ${ROOT_DIR}/app/static && mkdir ${ROOT_DIR}/app/static/css ${ROOT_DIR}/app/static/js ${ROOT_DIR}/app/static/images # base.html作成 cat > ${ROOT_DIR}/app/templates/base.html << "EOF" <!DOCTYPE html> {% load tz %} {% load static %} <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}{% endblock %}</title> {% block style %}{% endblock %} </head> <body> {% if messages %} <ul class="pl-0 ml-3"> {% for message in messages %} <li class="alert alert-{{ message.tags }}">{{ message }}<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button></li> {% endfor %} </ul> {% endif %} {% block content %} {% endblock %} {% block script %}{% endblock %} </body> </html> EOF # index.html作成 cat > ${ROOT_DIR}/app/templates/index.html << "EOF" {% extends "base.html" %} {% load static %} {% block title %}ページタイトル{% endblock %} {% block style %} {% endblock %} {% block breadcrumb %} {% endblock %} {% block content %} インデックスページのコンテンツを表示中 {% endblock %} {% block script %} {% endblock %} EOF # template.html作成 cat > ${ROOT_DIR}/app/templates/template.html << EOF {% extends "base.html" %} {% load static %} {% block title %}ページタイトル{% endblock %} {% block style %} {% endblock %} {% block breadcrumb %} {% endblock %} {% block content %} コンテンツテンプレートです {% endblock %} {% block script %} {% endblock %} EOF pythonパッケージ一覧 back/requirements.txt Django==3.2 psycopg2-binary>=2.8 scrapy-djangoitem==1.1.1 scrapy==2.4 scrapyd==1.2.0 djangorestframework==3.12.4 djangorestframework-jwt==1.11.0 django-cors-headers==3.7.0 django-webpack-loader==1.0.0 uwsgi==2.0.19.1 markdown==3.3.4 django-filter==2.4.0 Pygments==2.9.0 新規アプリ作成用スクリプト back/scripts/django-appstart.sh #!/bin/sh # シェル実行が失敗したら終了してエラー出力 set -e -o pipefail # 実行ディレクトリ取得 ROOT_DIR=$(cd $(dirname $0)/..;pwd) echo $ROOT_DIR # "新規アプリ作成" # 新規ディレクトリ&アプリ作成 mkdir $ROOT_DIR/app/${1} django-admin startapp ${1} $ROOT_DIR/app/${1} # INSTALLED_APPSに新規アプリを追加 sed -i -e "s/'rest_framework',/'rest_framework', \n '${1}', /" ${ROOT_DIR}/app/config/settings.py # websiteルーティング追加 sed -i -e "s/\]/ path('${1}\/', include('${1}.urls')),\n\]/" ${ROOT_DIR}/app/config/urls.py # APP_NAME/urls.py作成 cat > $ROOT_DIR/app/${1}/urls.py << EOF from django.urls import include, path # from . import views from .views import * app_name = '${1}' urlpatterns = [ path('api/jp', JPTestAPI.as_view()), path('api/us', USTestAPI.as_view()), path('api/br', BRTestAPI.as_view()), path('', index, name='index'), ] EOF # APP_NAME/views.py修正 cat > $ROOT_DIR/app/${1}/views.py << "EOF" from uuid import uuid4 from urllib.parse import urlparse from django.core.validators import URLValidator from django.core.exceptions import ValidationError from django.views.decorators.http import require_POST, require_http_methods from django.shortcuts import render from django.utils import timezone from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from django.views.generic import ListView, TemplateView, DetailView from django.shortcuts import render from rest_framework.views import APIView from rest_framework import authentication, permissions from rest_framework import status, viewsets, filters from rest_framework import permissions from rest_framework.response import Response # from django.db.models import Q import pytz from datetime import datetime # REST API 通信テスト用 class JPTestAPI(APIView): permission_classes = (permissions.AllowAny,) def get(self, request, format=None): # return Response(data={'status': dt_now}, status=status.HTTP_200_OK) jp = pytz.timezone('Asia/Tokyo') return JsonResponse(data={'status': datetime.now(tz=jp)}, status=status.HTTP_200_OK) class USTestAPI(APIView): permission_classes = (permissions.AllowAny,) def get(self, request, format=None): us = pytz.timezone('US/Eastern') return JsonResponse(data={'status': datetime.now(tz=us)}, status=status.HTTP_200_OK) class BRTestAPI(APIView): permission_classes = (permissions.AllowAny,) def get(self, request, format=None): br = pytz.timezone('Brazil/East') return JsonResponse(data={'status': datetime.now(tz=br)}, status=status.HTTP_200_OK) def index(request): return render(request, 'index.html' EOF # "テンプレート作成" # templates&staticフォルダ階層作成 mkdir $ROOT_DIR/app/templates/${1} # "「config/urls.py」にルーティングを追加" sed -i -e "s/]/ path('${1}', include('${1}.urls')),\n]/" $ROOT_DIR/config/urls.py 初回実行時に「main」ディレクトリを作成するスクリプトですが、他にも新規でアプリディレクトリを追加したい場合は以下のコマンドで APPNAME を好きなアプリ名で実行するとスクリプトが実行され、新規ディレクトリが作成されます $ docker-compose run back bash /code/scripts/django-appstart.sh APPNAME 新規ディレクトリにアプリファイル一覧を作成 「app/config/settings.py」にアプリ名設定追記 「app/config/urls.py」にアプリルーティング追記 「app/templates/」に新規アプリ名ディレクトリを作成 「views.py」と「urls.py」に設定追記 フロントエンド側 webアプリとしてのフロントエンド側のコンテナ構成 FROM node:16.2.0 # 開発ディレクトリ設定 ENV WORKDIR /code WORKDIR ${WORKDIR} COPY ./scripts/ ${WORKDIR}/ # コンテナ内でパッケージを一括インストール RUN npm update npm && npm install -g @vue/cli && \ npm install -g @vue/cli-init && npm install -S axios COPY . ${WORKDIR}/ コンテナ起動関連スクリプト 初回ビルド時は新規Vueプロジェクト作成コマンドを催促 初回以降にappディレクトリ作成済の場合は各種スクリプト実行 front/scripts/start.sh #!/bin/bash set 'echo -e -o pipefail' unix_today=$(date +'%s') unix_today=$((unix_today+32400)) jst_ymd_today=$(date '+%Y/%m/%d %H:%M:%S' --date "@$unix_today") app_name='app' # プロジェクトディレクトリが存在していればサーバー起動 if [ -d /code/${app_name} ]; then # vueプロジェクト設定済みではなかったら if [ ! -f /code/${app_name}/vue.config.js ]; then echo "${jst_ymd_today} | Vue directory set up" # vueテンプレート作成 bash /code/scripts/vue-settings.sh else echo "${jst_ymd_today} | Start the project server" # プロジェクトディレクトリに移動してサーバー起動 cd ${app_name} npm run serve fi else echo "「${jst_ymd_today}」|Create a new project" #*********************************************************************** echo "Vue axios設定ファイル作成" #*********************************************************************** mkdir /code/${app_name} if [ ! -f /code/${app_name}/vue.config.js ]; then cat > /core/${app_name}/vue.config.js << "EOF" module.exports = { devServer: { proxy: { '^/api/': { target: 'http://localhost:8000', logLevel: 'debug', pathRewrite: { "^/api/": "/api/" } } } } } EOF fi echo "-----------------------------------------------------------" echo "Run the command 'docker-compose run front vue create ${app_name} .' on the host machine" echo "------------------------------------------------------------" fi Vue.js設定用スクリプト 「HelloWorld.vue」にテスト用コード追記 「main.js」にaxiosとrourer設定追記 「router.js」作成 「components」内にルーティングテンプレートファイル作成 front/scripts/vue-settings.sh #!/bin/bash set 'echo -e -o pipefail' # ルートディレクトリ取得 ROOT_DIR=$(cd $(dirname $0)/..;pwd) app_name='app' #************************************************************************************************ echo "「HelloWorld.vue」修正" #************************************************************************************************ sed -i -e "s/<h1>{{ msg }}<\/h1>/<h1>{{ msg }}<\/h1>\n<h2>現在の時間:{{ result }}<\/h2>\n<button @click=\"getAPI()\">クリック!<\/button>/" ${ROOT_DIR}/${app_name}/src/components/HelloWorld.vue sed -i -e "N;s/}\n<\/script>/, data () {\n return {\n result: 'No Result',\n url: 'http:\/\/localhost:8000\/api\/'\n }\n },\n methods: {\n getAPI () {\n this.\$axios.get(this.url).then(response => {\n this.result = response.data.status\n })\n }\n }\n}\n<\/script>/" ${ROOT_DIR}/${app_name}/src/components/HelloWorld.vue #************************************************************************************************ echo "main.js修正" #************************************************************************************************ cat > ${ROOT_DIR}/${app_name}/src/main.js << "EOF" import Vue from 'vue' import App from './App.vue' import axios from 'axios' import router from './router.js' Vue.config.productionTip = false Vue.prototype.$axios = axios new Vue({ router, render: h => h(App), }).$mount('#app') EOF #*********************************************************** echo "コンテンツテンプレート作成" #********************************************************** cat > ${ROOT_DIR}/${app_name}/src/router.js << "EOF" import Vue from 'vue' import Router from 'vue-router' import top from '@/components/top' import page2 from '@/components/page2' import page3 from '@/components/page3' Vue.use(Router) export default new Router({ mode: 'history', routes: [ { path: '/', component: top }, { path: '/page2', component: page2 }, { path: '/page3', component: page3 } ] }) EOF cat > ${ROOT_DIR}/${app_name}/src/components/top.vue << "EOF" <template> <div> <p>ここはトップページです。</p> <h2>現在の日本時間:{{ result }}</h2> <button @click="getAPI()">クリック!</button> </div> </template> <script> export default { name: 'jp-time', props: { msg: String }, data () { return { result: '不明', url: 'http://localhost:8000/api/jp' } }, methods: { getAPI () { this.$axios.get(this.url).then(response => { this.result = response.data.status }) } } } </script> <style> </style> EOF cat > ${ROOT_DIR}/${app_name}/src/components/page2.vue << "EOF" <template> <div> <p>ここは2ページ目です。</p> <h2>現在のアメリカ時間:{{ result }}</h2> <button @click="getAPI()">クリック!</button> </div> </template> <script> export default { name: 'us-time', props: { msg: String }, data () { return { result: '不明', url: 'http://localhost:8000/api/us' } }, methods: { getAPI () { this.$axios.get(this.url).then(response => { this.result = response.data.status }) } } } </script> EOF cat > ${ROOT_DIR}/${app_name}/src/components/page3.vue << "EOF" <template> <div> <p>ここは3ページ目です。</p> <h2>現在のブラジル時間:{{ result }}</h2> <button @click="getAPI()">クリック!</button> </div> </template> <script> export default { name: 'br-time', props: { msg: String }, data () { return { result: '不明', url: 'http://localhost:8000/api/br' } }, methods: { getAPI () { this.$axios.get(this.url).then(response => { this.result = response.data.status }) } } } </script> EOF #********************************************************* # "「App.vue」修正" #********************************************************* cat <<EOF > ${ROOT_DIR}/${app_name}/src/App.vue <template> <div id="app"> <div id="header"> <!--リンクタグを生成します。--> <router-link to="/">top</router-link>41 <router-link to="/page2">2</router-link> <router-link to="/page3">3</router-link> </div> <router-view></router-view> </div> </template> <!--コンポーネントの名前を定義します。--> <script> export default { name: 'App' } </script> <!--スタイルの指定をします--> <style> body { margin: 0; } #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; } #header { height: 40px; background: white; box-shadow: 0px 3px 3px rgba(0,0,0,0.1); display: flex; justify-content: center; align-items: center; } #header a { text-decoration: none; color: #2c3e50; margin: 0 10px; padding: 3px 10px; background: #5ccebf; } </style> EOF Webサーバー すべてのアクセスをさばくプロキシサーバー役のコンテナ構成 ROM nginx:1.11.7 RUN mkdir /var/www RUN mkdir /var/www/app RUN mkdir /var/www/static アプリ用Nginxファイル web/conf/app_nginx.conf upstream back { ip_hash; server back:8001; # uWSGIでDjangoとnginxとが通信するためのポート server front:3000; # Vueとnginxとが通信するためのポート } server { listen 8000; # 待ち受けポート server_name 127.0.0.1; charset utf-8; client_header_buffer_size 1k; large_client_header_buffers 8 32k; add_header Strict-Transport-Security 'max-age=31536000'; add_header X-Frame-Options DENY; add_header X-XSS-Protection "1; mode=block"; error_page 500 502 503 504 /50x.html; location /static { alias /var/www/static; } # バックエンドサーバー location / { include /etc/nginx/uwsgi_params; uwsgi_pass back; # バックエンド proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $server_name; proxy_read_timeout 86400s; proxy_send_timeout 86400s; } # バックエンド adminサーバー location /admin/ { include /etc/nginx/uwsgi_params; uwsgi_pass back; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $server_name; } location = /50x.html { root /usr/share/nginx/html; } client_max_body_size 75M; } server_tokens off; uWSGI設定ファイル uwsgi_param QUERY_STRING $query_string; uwsgi_param REQUEST_METHOD $request_method; uwsgi_param CONTENT_TYPE $content_type; uwsgi_param CONTENT_LENGTH $content_length; uwsgi_param REQUEST_URI $request_uri; uwsgi_param PATH_INFO $document_uri; uwsgi_param DOCUMENT_ROOT $document_root; uwsgi_param SERVER_PROTOCOL $server_protocol; uwsgi_param REQUEST_SCHEME $scheme; uwsgi_param HTTPS $https if_not_empty; uwsgi_param REMOTE_ADDR $remote_addr; uwsgi_param REMOTE_PORT $remote_port; uwsgi_param SERVER_PORT $server_port; uwsgi_param SERVER_NAME $server_name; コンテナ起動 まずコンテナ全体を起動。 $ cd docker_spa $ docker-compose -d バックエンドにappディレクトリが作成&Djangoアプリ全体の設定ディレクトリ「config」とメインアプリディレクトリの「main」が作成されます。 webサーバー側のNginxも立ち上がるので http:localhost:8000 にアクセスで「インデックスページのコンテンツを表示中」のテキストが表示されていればDjangoとNginxの疎通はOK $ docker-compose run front vue create app . ? Please pick a preset: Default (Vue 2) ([Vue 2] babel, eslint) ? Pick the package manager to use when installing dependencies: NPM 続いて上記コマンドで新規Vueプロジェクトは初期設定の対話コマンドがあるので「 Vue2 」と「 npm 」を選択してインストールします。 結構時間かかりますがインストールが完了したら再度 docker-compose -d でビルドするとフロント側のセットアップスクリプトが実行されます docker_spa/ ┣━ back/ ┃ ┣━ app/ ┃ ┃ ┣━ config/ ┃ ┃ ┣━ main/ ┃ ┃ ┣━ static/ ┃ ┃ ┣━ templates/ ┃ ┃ ┣━ db.sqlite3 ┃ ┃ ┗━ manage.py ┃ ┣━ scripts/ ┃ ┣━ Dockerfile ┃ ┗━ requirements.txt ┣━ front/ ┃ ┣━ app/ ┃ ┃ ┣━ node_modules ┃ ┃ ┣━ public/ ┃ ┃ ┣━ src/ ┃ ┃ ┣━ babel.config.js ┃ ┃ ┣━ package-lock.json ┃ ┃ ┣━ package.json ┃ ┃ ┗━ vue.config.js # proxy設定ファイル ┃ ┣━ scripts/ ┃ ┣━ Dockerfile ┃ ┣━ package-lock.json ┃ ┗━ package.json ┣━ web/ ┃ ┣━ conf/ ┃ ┃ ┗━ app_nginx.conf ┃ ┣━ logs/nginx ┃ ┃ ┗━ access.log ┃ ┃ ┗━ error.log ┃ ┣━ Dockerfile ┃ ┗━ uwsgi_params ┃ ┗━ docker-compose.yml 上手くいけば上記の構成が出来上がってると思います。 http:localhost:8080 にアクセスするとVueのトップページが表示されます。 「top」「2」「3」でページが切り替わり、「クリック!」ボタンをクリックするとバックエンドとの通信を行い、現在の時刻を表示します。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む