- 投稿日:2020-03-23T20:59:43+09:00
JSフレームワークでのdelimiter変更覚え書き
Smartyを使っているとJSフレームワークを用いた場合に{}の記述がSmarty側とバッティングしてそのまま使えないことがある。
ここではその変更方法を記載します。Angular
app = angular.module("app", []); app.config(function($interpolateProvider) { $interpolateProvider.startSymbol('[['); $interpolateProvider.endSymbol(']]'); });Vue
var vm = new Vue({ el: '#app', data: { ~~~ }, delimiters: ['[[', ']]'] });
- 投稿日:2020-03-23T20:35:54+09:00
WEBアプリケーションのJWT認証まとめ
はじめに
FlaskとVue.jsを使ったWebアプリケーションの実装する際、ログイン機能とJWTを使ったAPIの認可の仕組みを実装しました。その機能のまとめです。
JWTをどこに保存するか問題など、明確な正解がない中で実装をしましたので、同じようにどうしようか迷っている方の参考になればと考えています。
脆弱性やもっと堅牢にするにはこうした方がよいなどあればコメントください。皆さんでより良い認証の仕組みを考えることができればいいと考えています。今回はFlask-JWT-Extendedというライブラリを使用して、JWTの生成や検証を行っています。
また、生成されたJWTはcookieに保存されます。サーバーサイド
- Flask
- Flask-JWT-Extended
クライアントサイド
- Vue.js
用語
今回の記事で使用する用語と意味合いです。認識の齟齬を生まないために一応記載しておきます。
アクセストークンJWT
APIリクエストの際に使用するトークンの役割を果たすJWTリフレッシュトークンJWT
アクセストークンJWTの有効期限が切れた際の更新処理に利用するリフレッシュトークンの役割を果たすJWTアクセストークンCSRF対策トークン
CSRFの対策のためにAPIリクエストの際にヘッダーにセットする文字列。アクセストークンJWTと1:1リフレッシュトークンCSRF対策トークン
CSRFの対策のためにトークンリフレッシュの際にヘッダーにセットする文字列。リフレッシュトークンJWTと1:1認証・認可フロー
以下のフローでログイン認証、JWT生成、JWT検証と行います。
(1) ログイン画面からID・PWを入力し、ログインボタンを押下(クライアント)
(2) ログインボタンが押下されたら、入力されたIDとPWをリクエストボディにのせログイン用エンドポイント(/loginなど)にリクエストを投げる(クライアント)
(3) サーバ側はリクエストからID・PWを取り出し、ユーザの検証(DBとの比較など)を行う(サーバ)
(4) ID・PWの組み合わせが不正な場合はステータスコード「401」を返す。(サーバ)
(5) ID・PWの組み合わせが正しい場合は、JWT等を生成しCookieにセットし、ステータスコード「200」を返す。この際、Cookieにセットされる情報は以下の4つ(サーバ)
- 「アクセストークンJWT」
- 「リフレッシュトークンJWT」
- 「アクセストークンCSRF対策トークン」
- 「リフレッシュトークンCSRF対策トークン」
(6) エラーコードを受け取った場合は再度認証を促す(クライアント)
(7) ステータスコード「200」を受け取った場合は、ログイン後画面に遷移する(クライアント)
(8) APIリクエストを行う際は、Cookieの「アクセストークンJWT」をサーバ側に送る。また、CSRF対策として、「アクセストークンCSRF対策トークン」をヘッダーにセットしてサーバに送る。(クライアント)
(9) APIリクエストを受けったサーバはCookieから「アクセストークンJWT」を取り出し、検証を行う。検証内容は「JWTの改ざんが行われていないか」「JWTの有効期限が切れていないか」。合わせて「アクセストークンCSRF対策トークン」の検証も行う(サーバ)(10) JWTの改ざんが行われている場合はエラーを返し、ログイン画面に強制的にリダイレクト返す。検証に成功した場合は正しいAPIレスポンスを返す(サーバ)
(12) JWTの有効期限が切れていた場合は、ステータスコード「401」を返す(サーバ)
(13) 401エラーを受け取ったクライアント側はトークンリフレッシュ用のエンドポイント(/refreshなど)にリクエストを投げる(クライアント)
(14) サーバは「リフレッシュトークンJWT」の検証(項番9と同等)と「リフレッシュトークンCSRF対策トークン」の検証を行い、問題なければ、新しい「アクセストークンJWT」「リフレッシュトークンJWT」「アクセストークンCSRF対策トークン」「リフレッシュトークンCSRF対策トークン」をCookieにセットし成功レスポンスを返す(サーバ)
(15) リフレッシュトークンの検証に失敗した場合はエラーを返し、ログイン画面に強制的にリダイレクトする(サーバ)JWTの有効期限問題
アクセストークンJWTの有効期限はなるべく短くすることを推奨します。
これはシンプルにJWTが漏洩した際の情報漏洩リスクを下げるためです。
しかし短く設定してしまうと、APIリクエストの度にリフレッシュ処理が行われるなどパフォーマンスにも影響が出るため作成するアプリケーションの仕様などによって調整する必要があります。また、リフレッシュトークンJWTの有効期限に関しては、アクセストークンJWTより長くなると思いますが、
仮にリフレッシュトークンJWTが漏洩してしまった際、アクセストークンJWTの漏洩より被害が大きくなる可能性があります。そのため、特定のリフレッシュトークンJWTを有効期限内であっても無効にする処理などを実装する必要があるかもしれません。JWTどこに保管するか問題
JWTに限らずトークン情報をどこに保存するか議論がずっと続いています。
選択肢として上げられるのが以下の3つ
- Local storage
- Cookie
- Session storage
それぞれのメリデメは以下の記事でまとめられています。
JWT・Cookieそれぞれの認証方式のメリデメ比較XSS問題
今回の場合はJWTをCookieに保存しているためXSSの脆弱性が残ります。
XSSの対策はCookieのHttpOnly属性をtrueにすることです。
HttpOnlyをtrueにすることで、JavaScriptから対象のCookieへアクセスすることができなくなります。今回の場合は「アクセストークンJWT」「リフレッシュトークンJWT」にHttpOnlyを設定します。「アクセストークンCSRF対策トークン」と「リフレッシュトークンCSRF対策トークン」に関してはHttpOnly属性は設定しません。これらの使い方は以下のCSRF問題で説明します。CSRF問題
CookieにJWTを保管している場合、CSRFの脆弱性が出てきます。
上記の「認可・認証フロー」に記載していませんが、「アクセストークンJWT」と「リフレッシュトークンJWT」に対応する「CSRF対策用トークン」をサーバ側で発行しています。JWTには任意の文字列を含めることも可能なので、それぞれのJWTにCSRF対策用トークンを含めておくことで簡単に検証を行うことが出来ます。APIリクエストやトークンのリフレッシュを行う場合は、このCSRF対策用トークンをhttpヘッダーに付与してリクエストをし、サーバー側でこの値を検証します。これによりCSRFに対応することができます。Flask-JWT-Extendedの場合は「X-CSRF-TOKEN」ヘッダーにCSRF対策用トークンをセットします。
例えばaxiosを使う場合は以下のようにheadersにセットします。Cookie情報の取得はVue-cookiesを使用しています。// 一部抜粋 const request = { hoge : fuga } Axios .post("/XXXXX", request, { headers: { "X-CSRF-TOKEN": this.$cookies.get("csrf_access_token") } }) .then(res => {}) .catch(error => {});トークンのリフレッシュ方法
アクセストークンの有効期限切れの際、リフレッシュ後再度本来のAPIリクエストを行うinterceptorを作成し、APIリクエストの際は必ずこのAxios定義を使用するようにします。
以下のaxios.jsを各コンポーネント(.vue)でインポートして使用します。
以前の記事で同じような処理を書いていますが、アクセストークンの有効期限が切れた状態でほぼ同時に複数のAPIリクエストを実施すると、無駄に複数回リフレッシュ処理が実施されてしまう問題があったため以下のようになりました。海外版stackoverflowで教えていただきました。有能。axios.jsimport Axios from 'axios' import Vue from 'vue' const http = Axios.create({ baseURL: 'https://XXXXXXXX/api/v1', withCredentials: true, headers: { "Content-Type": "application/json; charset=UTF-8", "X-Requested-With": "XMLHttpRequest" } }) let refreshTokenPromise; const getRefreshToken = () => http.post('/refresh', {}, { withCredentials: true, headers: { 'X-CSRF-TOKEN': Vue.$cookies.get('csrf_refresh_token') } }).then(() => Vue.$cookies.get('csrf_access_token')) http.interceptors.response.use(r => r, error => { if (error.config && error.response && error.response.status === 401 && !error.config._retry) { if (!refreshTokenPromise) { error.config._retry = true; refreshTokenPromise = getRefreshToken().then(token => { refreshTokenPromise = null return token }) } return refreshTokenPromise.then(token => { error.config.headers['X-CSRF-TOKEN'] = token return http.request(error.config) }) } return Promise.reject(error) }) export default httpURL直叩き問題
正しく認証が行われていない状態でURLを直叩きされた際、機密性の高い情報(APIリクエストにより取得する情報)は閲覧されませんが、画面が見えてしまいます。問題はないと言えばないですが、嫌なので以下のように実装してそれを防ぎます。
main.jsimport Vue from 'vue' import App from './App.vue' import router from './router' import Axios from './axios' Vue.prototype.$http = Axios // routerによる画面遷移前に共通で行う処理 router.beforeEach((to, from, next) => { // ログイン成功の情報をブラウザ情報どこかに保持しそこから値を取得する。 // 今回はtrueをべた書きしていますが、実装に合わせて変更します。 const loggingIn = true; if (to.matched.some(page => page.meta.isPublic) || loggingIn) { // ログイン済みの場合はそのまま次のページへ next(); } else { // ログイン済みでない場合はログイン画面にリダイレクト next('/login'); } }); new Vue({ router, render: h => h(App) }).$mount('#app')router.jsimport Vue from 'vue' import Router from 'vue-router' import Index from "./components/pages/index"; import About from "./components/pages/about"; import Login from "./components/pages/login"; import Public from "./components/pages/public"; Vue.use(Router) export default new Router({ mode: 'history', routes: [ { path: '/', name: 'Index', component: Index }, { path: "/about", component: About }, { path: "/login", component: Login, meta: { isPublic: true } }, { path: "/public", component: Public, meta: { isPublic: true } } ] })上記のようにすることで、meta情報にisPublicが設定されていないpathにログインしない状態でアクセスした場合は強制的にログイン画面にリダイレクトすることが可能です。
おわりに
セキュリティ難しい。
- 投稿日:2020-03-23T18:46:11+09:00
社内利用ツールを2020年らしいプロセスで作ってみる(Vue.js + Electron + SQLite3) Day1
はじめに
社内システムが異常に使いにくく、フィードバックは社内で大量に出ているものの作ってすぐだし情シスもリソースないしということなので、アンチパターンながら、自分の部署だけで使う手元ツールを休暇を利用して作ってみることにしました。
ざっくり何が欲しいのかというと、業務システムを開発する組織で、要員管理、案件管理、収益管理などの目線で部署やチームの計画を立てるためのツールです。
表計算ソフトに近い表中心のシンプルなUIで、何を見て何を決めるかのノウハウが少しだけ詰まったような軽量なツールを目指して作りますので、特にSIerのみなさん生暖かく見守ってください。開発のためのお供(参考書籍)
開発の進め方
【日経BP】.NETのエンタープライズアプリケーションアーキテクチャ
ドメイン駆動設計をはじめとして、アーキテクトなら読んでおけ的な本だと聞きました。
アーキテクトを目指している訳ではないものの、せっかくゼロから作るならイマドキな知識は入れておきたいなと。ざーっとだけ一通り読んで、あとは使いたいときに辞書的に使います。
エリック本は過去に読もうとして早々に諦めたので、開発中心で書かれていて助かりました。Webデザイン
【BNN】超明快 Webユーザビリティ ユーザーに「考えさせない」デザインの法則
Webデザインやユーザビリティ関連に興味が出たときに買って読んでいた本です。
本屋でWebデザイン関係の本をみていたのですが、パラパラめくった感じが読みやすそうで買いました。
「考えさせない」という論旨がはっきりしていてブレていないので、好きな本です。大まかな開発の流れ
- ツール作成の目的と動作環境を検討する -> Day1
- アーキテクチャをいったん検討する -> Day1
- ユースケースを洗い出す -> Day2
- 画面イメージのスケッチを作成する -> Day2
- ドメインモデルを検討する(用語の定義、振る舞いの定義)
- ユースケース、画面イメージのワイヤーフレーム、ドメインモデルを何周か見直す
- 当初決めた目的が果たせるか利用者目線を検証する(コンセプトの検証)
- 単純なCRUDアプリでアーキテクチャを検証する(実現方式の検証)
- コアになるユースケース1本だけプロトタイプ作成(MVP)
- 本格的に準備して開発を始める(テスト駆動のイテレーション開始)
お供に選んだ書籍の、特に以下の部分を参考にしました。
第5章 ドメインアーキテクチャの発見
第6章 プレゼンテーション層
第7章 伝説のビジネス層
第8章 ドメインモデルの紹介個人的に弱かったデザイン分野の用語の抜粋、理解
- スケッチ: アイディアを書き留めるために描かれる手書きの図
- ワイヤーフレーム: 機能、レイアウト、ナビゲーション、情報に焦点を合わせた、よりハイレベルなスケッチ
- モックアップ: 実際のルック&フィールに配慮し、サンプルのUIを貼り付けたワイヤーフレーム
- プロトタイプ: バックエンドなしorテスト用のダミーのバックエンドでビューと既存データで実装したフロントエンド
今回はクライアントPCでスタンドアロンで動くものが完成形なので、プロトタイプはDBがモックなくらいということですかね。
開発日記(Day0)
お供に選んだ開発の進め方の書籍にざっと目を通しながら、ちゃんと勉強していなかったDDDについてふんふんなるほどなるほどとなる時間を取りました。
前から作りたくて少しずつ練っているサービス開発と、今回のツール作成と、実利が近い方がいいと思い後者を選択しました。
開発日記(Day1)
休暇のはずながら午前は社用(事務処理の類)もあって、お昼過ぎくらいから記事を書きながら本格開始しました。
ツール作成の目的と動作環境を検討する
目的
現行システム(今回は社内システム)では何が出来ないか。何がしたいか。何はしなくていいか。
現行システムへの不満としては、一番は何を登録/参照するのがどの画面なのか分かりにくいのと遅いという使い勝手の面なのですが、機能面で改めて考えると、登録と参照が切り離されていて、計画立案のような登録するべきデータを計数を見ながら考えるというユースケースで使える機能がない点でした。
要員、案件、執務スペースなどの前提条件を準備して、各案件への要員アサインをフレキシブルに変更しながら、収益や必要座席数などの計数を可視化して、計画を立てたい。また、今後も計画は見直すことになるので、計画内容は保存したい。やらなくていいのは、リッチなアニメーションやきっちりとした入力制御、マーケティングやタスク管理の観点も排除します。
将来的には社内システムと連携できるようなCSVファイルexportも出来たらいいでしょうが、かなり先ですね。
動作環境
制約や条件として、どんなものがあるか。
いわゆる古い体質の大企業での社内システムを補うツールとしたいため、以下の条件になりました。
- 配置先はサーバーではなくWindowsのクライアントPCで完結する
- インストール不要でスタンドアロンで動作する(GitHubからzip取得や自宅PCからのメール持ち込み)
- 同じツールを複数人でデータ共有しながら利用可能
- データ保存先はツール利用者が選択可能(共有フォルダへの配置も想定)
- 初回起動含めて動作時のインターネット接続は不可
アーキテクチャをいったん検討する
アプリ開発言語
インストール不要のスタンドアロンアプリということで、.NETアプリか、Electronか、React Nativeあたりが選択肢のようです。
C#で.NETで作るのが本に沿っていけそうでいいのですが、友人から格安で譲り受けた2011年くらいのMacBookAirで開発しておりSSD容量的にVisualStudioがしんどくて、昔Xamarinを触ってみるのにインストールしたもののアンインストール済だったりするので外します。
Reactはちょっと独特なところあるという情報も見かけていて、最初はAngularやVueがいいのかなということでVue.jsベースのElectronでやってみます。データ永続化方法
またデータの保存先については、ElectronについてGoogle先生に教えてもらったところ、画面サイズなどの設定はユーザーディレクトリのJSONで保存して、トランザクションを意識した処理をしたい業務データはスタンドアロンで使えるDBMSであるSQLite3がよさそうと判断しました。
これでDBも含めて実行ファイルの入ったフォルダを配置したらスタンドアロンで動きそうですね。
アプリケーション構成
構成としては、ヘキサゴナルアーキテクチャを採用して、当然ドメイン駆動設計を意識して進めます。
(横道)開発ツール事情を下調べ
これから開発を進めるに当たって、せっかくの自由な環境なので2020年らしい開発ツールを使った開発を目指します。
いろいろ試して感想も書けると自分だけでなく誰かのためになりますし。UML(ユースケース図、クラス図)
draw.io、PlantUML、Astah professionalあたりがメジャーな選択肢のようです。
今回の開発環境はインターネット接続可能なので、以下の記事を参考にしてdraw.ioをGitHubと連携させる方針にします。Diffの見え方が美しい。。
【企業技術ブログ】図を継続的に管理するためのベストプラクティス
【個人運営サイト】超便利フリーUMLツール「draw.io」でユースケース図を作成する方法
PlantUMLをVSCodeでリアルタイムプレビューする環境は構築済なのですが、マシンスペックのせいかプレビューに時差があってサクサクと書けなかったので、新しい試みをします。
インターネット接続なし環境ではPlantUMLは本当に便利で、業務システム開発の案件ではよくお世話になっています。
また、今の現場はAstah professionalを購入予定で、こちらもcommunityの頃からいい製品ですよね。画面スケッチ
これは紙とペンですね。書きやすいボールペン買います。
画面ワイヤーフレーム〜プロトタイプ
この分野は、ツールによって守備範囲がいろいろあるようですね。
主要そうだと思ったのは、Figma、Adobe XD、Sketchあたりです。今回は一番勢いがありそうなFigmaを試してみます。
バージョン管理を備えていて、GitHubとは連携しないのかな?触りながら見てみることにします。
以下のブログをみる限り、Figma単体でGitの知識がなくてもバージョン管理出来るのがメリットとして書かれていそうに見えます。ちゃんと読んで触ったら情報アップデートします。【企業ブログ】Using Figma designs to build the Octicons icon library
コードエディタ、IDE
以前も使っていて世間に情報も多いVisualStudioCodeを使います。
今はエディタとIDEの垣根ってかなり低いですよね。
他で主要そうだと思ったのは、VisualStudio、Atomあたりでしょうか。
業務システム開発の分野ではまだまだEclipseをよく聞きますが、インターネットなし前提だといいのかもしれません。(横道)GitHubでリポジトリ作成時のライセンスについて知る
【GitHub公式】適切なライセンスを選択する
なんとなく雰囲気は掴めたものの自信がなく、ここも見てみました。
【Qiita】githubでライセンスを設定する
今回はどちらの説明でもしっくりくるGPLにしよう!
ユースケースを洗い出す
このあたりから本格的に本で学んだことを実践していきます。Day2に続く予定。
- 投稿日:2020-03-23T17:50:20+09:00
Vue.jsで子コンポーネントから親コンポーネントへデータを渡す方法
VueやNuxtを使っていて、子コンポーネントから親コンポーネントへ値を渡したい時の方法について記述します。
親→子はpropsを使えばいいのですが、子→親は少しトリッキーなやり方になりますので。結論
$emit(eventName,[...args])
を使います
$emitについてはこちらサンプル
サンプル画面
サンプルコード
parent.vue<template> <div> <h1>親</h1> コメント:{{ comment }} <hr> <h2>子</h2> //@childの「child」は子コンポーネントの第一引数で指定されたもの <child :comment="comment" @child="changeComment"></child> </div> </template> <script> import child from '~/components/child.vue' export default { components: { child }, data() { return { comment: "テスト" } }, methods: { //$emitの第二引数の値はここで受け取っている changeComment(childComment) { this.comment = childComment } } } </script>components/child.vue<template> <div> <input v-model="childComment" type="text"> <input type="button" value="親コメントを変更する" @click="change"> </div> </template> <script> export default { data() { return{ childComment:'' } }, methods: { change() { //$emitの第一引数でイベントの名前を指定、第二引数で値を渡す this.$emit('child', this.childComment) } } } </script>解説
子コンポーネントの$emitでイベント名(child)を指定して、第二引数に親コンポーネントに渡したい値を指定します。
親コンポーネント側では指定されたイベント(child)で動くmethodであるchangeCommentで第二引数で指定された値を受け取ります。最後に
子コンポーネントから親に値を渡して親の値を変更したいといった場面も意外にあるかと思います。そんな時にこの記事がお役に立てれば幸いです。
- 投稿日:2020-03-23T17:18:39+09:00
【Nuxt.js】Vue Router復習編:params, queryを使おう
前置き
前回の続きです!
基礎編の復習と思ってもらえれば⭕️https://note.com/aliz/n/ndf76ebe9853b
値を見てみよう!
consoleの値と
実際画面上でどうなるか
確認していきましょう??_idでどんな文字列がきても
良い状態にしておきます。【ディレクトリ 】
filepages/ --| _id/ -----| index.vueindex.vue<template> <div class="page"> <p>params: {{ $route.params.id }}</p> <p>query: {{ $route.query.id }}</p> </div> </template> <script> export default { fetch ({ params, query }) { console.log(params, query) } } </script>【URL】
localhost:59037/hoge
【解説】
consoleを見ていきましょう?
・paramsが{id: "hoge"}
urlのpath部分がhogeのため
templateの参照も
route.params.idで一致し
うまく表示されています?
これが$route.params.userにしてみると
一致せず何も表示されません。。。
・queryが{}
urlに?がないためqueryは空✅$router.pushを追加してみます!
https://router.vuejs.org/ja/guide/essentials/dynamic-matching.htmlindex.vue<template> <div class="page"> <p>params: {{ $route.params.id }}</p> <p>query: {{ $route.query.user }}</p> <button type="button" @click="$router.push({ path: 'hogehoge', query: { user: 'private' } })" > 移動! </button> </div> </template> <script> export default { fetch ({ params, query }) { console.log(params, query) } } </script>【URL】
ボタンを押す前localhost:59037/hoge
ボタンを押した後
localhost:59037/hogehoge?user=private
queryをuser=privateにしているので
{{ $router.query.user }}と変更したことで
privateが表示されていますね?pagination
ページネーションも
考え方はこれと同じです!
paramsは同じまま、
queryだけを変えていきます。
これにより同じページ内でソートを書け
1ページ目だけを表示、ということができます?
liの1を押せば1ページ目にいきます?【基礎構文】
変数を使う時は${変数}にします。
テンプレートリテラルについては
ここが分かりやすいです!
https://qiita.com/kura07/items/c9fa858870ad56dfec12userIdをpropsとして渡し
親でuserId=123とすれば
/user/123へ飛びます。
https://router.vuejs.org/ja/guide/essentials/navigation.html基礎構文router.push({ path: `/user/${userId}` }) // -> /user/123【飛びたいURL】
localhost:3000/home?members=1
【Pagination.vue】
queryをpropsとして渡します。
queryは?から始まるので?から始まり
その後ろに変数のqueryを入れます。
変数を使う時は${変数}にします。Pagination.vue<template> <div class="page"> <ul class="list"> <li @click="$router.push(`?${query}=1`)" > <span class="text"> 1 </span> </li> </div> </template> <script> export default { props: { query: { type: String, required: true, }, }, } </script>home.vue<template> <div class="page"> <Pagination query="members" /> </div> </template>queryを親で指定して
members一覧部分の
1ページ目でソートすることができます??router.pushの文頭に/を追加し
$router.push(/?${query}=1
)
にしてしまうと【飛びたいURL】
pages/home.vue内で
メンバーの一覧部分をソートlocalhost:3000/home?members=1
【実際のURL】
pages/index.vue内で
メンバーの一覧部分をソートlocalhost:3000/?members=1
となってしまいます!?
理解度チェック
✅あまり実用的ではないですが
理解度チェックのためのクイズです?飛びたいURLから
足りない部分を書き足しましょう✍️【飛びたいURL】
localhost:3000/user/hoge
【ディレクトリ 】
filecomponents/ --| RouterPush.vue pages/ --| user/ -----| _id.vue --| index.vueRouterPush.vue<template> <button type="button" @click="$router.push(`${query}`)" > home </button> </template>index.vue<template> <div class="page"> <RouterPush /> </div> </template> <script> import RouterPush from '~/components/RouterPush.vue' export default { components: { RouterPush }, } </script>…
…
分かりましたか??
書き足す部分はこちらです!index.vue<template> <div class="page"> <RouterPush query="user/hoge" /> </div> </template> <script> import RouterPush from '~/components/RouterPush.vue' export default { components: { RouterPush }, } </script>【解説】
RouterPush.vueで
${変数}を使っているため
親で変数queryを行きたいURLに指定します??
行きたいURLはuser/_id.vueで
_idはhogeとしているのでquery="user/hoge"
文字列をそのまま渡しているので
:queryにする必要はありません。
:query="変数"の場合は使用します?
これで復習もバッチリですね!!記事が公開したときにわかる様に、
note・Twitterフォローをお願いします?
https://twitter.com/aLizlab
- 投稿日:2020-03-23T16:01:57+09:00
yarn deployだけで更新できるブログを構築した!
はじめに
Vuepress, Google Domain, Github Pages, Github Actionsを使ってる、ブログ!
全くvuepressとgithub pages無知機の状態で、ここまでできたのは10時間ぐらい、しかもその中6割りはthemeに対するカスタマイズ。元々の状態でもよろしければ、更に早くデプロイできそうだ。
まずは、成果物?
まとめると
- Vuepressとthemeをダウンロード
- themeのカスタマイズ
- 以前書いたものの移行
- github pages設定、domain設定
- github actions設定
-package.json
script 追加Vuepress
元々LaravelとVueで書かれたブログは肥大化すぎで、色々調べて、たどり付いたのはVuepress!
Vuepressの特徴
- Vueで動く静的ページジェネレーター
- markdownで書かれたファイルをhtmlに変換
VuejsのEcosystemの一環として、今Vuejsに関するドキュメントは全部Vuepressで作られてるみたい。
awesome-vuepressはたくさんのplugin
やtheme
載せている。
その中に、気にいったのは、人気一位のvuepress-theme-reco。Google DomainとGithub Pages
Google DomainとGithub pagesのcustom domainについてこちら参考した。
Github Actions
Github Actionsに関するものたくさんあるし、ここはGithub Pagesにデプロイだけ説明する。
a
➡️an
間違ってる、気づいたもう遅いわ?Githubのsettingから、
ACCESS_TOKEN
を取得
⬇️
上の図のように、ACCESS_TOKEN
などgithubサーバ上に使いたいものをSecrets
に保存
⬇️
workflowを書く
Vussue
というgithub issue連携のコメントサービスも使うことに、Vussue
のkeyも保存。main.ymlname: Deploy GitHub Pages # once pushed to master branch on: push: branches: - master # jobs to build and deploy jobs: build-and-deploy-blog-to-github-page: # server env: latest Ubuntu runs-on: ubuntu-latest steps: # pull project - name: Checkout uses: actions/checkout@v2 with: persist-credentials: false # Pass Variables - name: Pass Variables env: EXAMPLE: ${{ secrets.THIS_IS_A_EXAMPLE }} NOTEXIST: ${{ secrets.NOTEXIST }} run: echo 'try to show secret ?' && echo $EXAMPLE && echo $NOTEXIST # build project - name: Build env: VSSUEID: ${{ secrets.VSSUEID }} VSSUESECRET: ${{ secrets.VSSUESECRET }} run: npm install && npm run build # deploy to Github Pages - name: Deploy uses: JamesIves/github-pages-deploy-action@releases/v3 with: ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} BRANCH: gh-pages FOLDER: docs/.vuepress/dist
Secrets
に保存したものは***になってる!vssueConfig: { platform: 'github', owner: 'xyyolab', repo: 'blog', clientId: process.env.VSSUEID, clientSecret: process.env.VSSUESECRET }そして、nodeの
process.env
で使えるようになる。コメントも使えるようになった。
package.json
script 追加#!/usr/bin/env sh git add . git commit -m 'deploy' git push echo 'https://blog.xyyolab.com' echo 'https://github.com/xyyolab/blog/actions'最後にgitの操作も
package.json
にscripts
化して、最終的にyarn deploy
だけで、新しいもの書いたら、デプロイできるようになっている。
残念なところ
VuepressとQiitaのmarkdown文法は微秒な違いがある。今後もしQittaも自動デプロイの一部としてやりたいなら、手動で編集しかないかな。
- 投稿日:2020-03-23T14:11:51+09:00
Vue.js グローバルな定数管理
VueのSPA開発をしていると、定数で管理しておきたいもの(描画する初期値や、何かの一覧など)が多々出てくると思います。
mixinは問題を抱えている、ViewModelにあたるのでできればVuexにも格納したくないと思い、別の方法で考えてみました。
別jsファイルで管理し、require/import(モジュールシステム)で活用
前提
例えば、APIからの返却が下記として、
response[ { id: 1, name: 'Qiita', status: { value: 'active' : label: 'アクティブ' } }, { id: 2, name: 'Qiitb', status: { value: 'active' : label: 'アクティブ' } }, { id: 3, name: 'Qiitc', status: { value: 'active' : label: 'アクティブ' } }, ]定数を別jsファイルで管理
まずは、テーブルカラムを定義
consts/hoge/table配下HogeTableColumnName.jsexport default { ID: 'id', NAME: 'name', STATUS: 'status' }次に、描画用の情報を定義
HogeTableColumnList.jsimport HogeTableColumnName from 'HogeTableColumnName' export default [ { property: 'HogeTableColumnName.ID', label: 'ID' }, { property: 'HogeTableColumnName.NAME', label: '名前' }, { property: 'HogeTableColumnName.STATUS', label: 'ステータス' }, ]オブジェクトに対してObject.kesを使用すると順番が確約されないので、配列で管理
Vueコンポーネント内にて、require/import(モジュールシステム)で活用
<template> <table> <thead> <tr> <!-- columnの数だけカラム生成 --> <th v-for="column in HogeTableColumnList"> {{ column.label }} </th> </tr> </thead> <tbody> <!-- responseの数だけレコード生成 --> <tr v-for="res in response"> <!-- columnの数だけカラム生成 --> <td v-for="column in HogeTableColumnList"> <template v-if="column.propery === HogeTableColumnName.STATUS"> {{ res[column.property].label }} </template> <template v-else> {{ res[column.property] }} </template> </td> </tr> </tbody> </table> </template> <script> import HogeTableColumnList from 'path/const/hoge/table/HogeTableColumnList' import HogeTableColumnName from 'path/const/hoge/table/HogeTableColumnName' export default { computed: { // methods内のみであれば不要だが、template内で使用するために必要 // dataプロパティへの定義も可 HogeTableColumnList() { return HogeTableColumnList }, HogeTableColumnName() { return HogeTableColumnName } } } </script>どこまで定数として管理するのか悩みますが、templateがシンプルになりやすく、コンポーネント管理がしやすくなるのではと思っています。
他の定数管理方法は、下記に紹介されていますが、②で
アプローチは、他の開発者と働いたり、大規模なアプリケーションを開発する際に、最も保守性が高いです。
と記載されているので良さそうです。
①Vue.jsでグローバルな定数をコンポーネントで使いまわせるようにしたい
②Vue.js公式: インスタンスプロパティの追加 / モジュールシステムを使用する場合(また、テストする際にも使用できそう)
応用
templateの例外処理をmethodsに移行
<!-- columnの数だけカラム生成 --> <template> <td v-for="column in HogeTableColumnList"> <template v-if="column.propery === HogeTableColumnName.STATUS"> {{ res[column.property].label }} </template> <template v-else> {{ res[column.property] }} </template> </td> </template><!-- columnの数だけカラム生成 --> <template> <td v-for="column in HogeTableColumnList"> {{ getColumnValue(res, column.property) }} </td> </template> <script> export default { methods: { getColumnValue(res columnName) { if (columnName === this.HogeTableColumnName.STATUS) { return res[columnName].label } else { return res[columnName] } } } } </script>classの出し分けプロパティを定数管理
HogeTableColumnList.jsimport HogeTableColumnName from 'HogeTableColumnName' export default [ { property: 'HogeTableColumnName.ID', label: 'ID', isRight: true }, { property: 'HogeTableColumnName.NAME', label: '名前', isRight: false }, { property: 'HogeTableColumnName.STATUS', label: 'ステータス', isRight: false }, ]<!-- columnの数だけカラム生成 --> <template> <td v-for="column in HogeTableColumnList" :class="{ 'is-right': column.isRight }"><!-- idは数字なので右寄せに --> <template v-if="column.propery === HogeTableColumnName.STATUS"> {{ res[column.property].label }} </template> <template v-else> {{ res[column.property] }} </template> </td> </template> <style scoped> .is-right { text-align: right; padding-right: 2px; } </style>随時更新予定
感想
enum管理をしてあげるついでに、ViewModelにあたる情報も定数として管理しておいても良さそうだなという話です。
他にも良さそうな方法あればコメントお願い致します。
- 投稿日:2020-03-23T11:33:23+09:00
【意外と簡単】D3.jsで地図を自在に描画できたらわくわくした話。実装方法を丁寧に解説。
地図を自在に描画できるとしたら、色々可能性の幅が広がりそうでワクワクしませんか?
この記事では 新型コロナウィルスの感染情報を可視化したサイト を作るにあたって地図を自在に描画・制御する術を手に入れたので、その知見を共有できればと思います。以下の様に D3.js を使用すると描画した地図に対してインタラクティブに制御できます。
1. D3.js とは
D3.js はデータに基づいてドキュメント(SVG や DOM など)を操作するための JavaScript ライブラリです。
今回は D3.js を使い地図情報(GeoJson データ)から SVG を生成してブラウザに描画します。2. 地図データの準備
D3.js を使ってブラウザで地図を描画するためには GeoJson 形式のファイルを用意する必要があります。
ネット上を探してみると直ぐに利用できる状態の GeoJson 形式ファイルもありますが、ここでは信頼性のありそうでライセンス的に扱いが簡単な Natural Earth からデータを取得して加工する方法を解説します。
(手っ取り早く加工済みの GeoJson データが欲しい方は こちら からどうぞ)
(1) Natural Earth / 世界地図のデータを取得
Natural Earth から世界地図をダウンロードします(次の工程で日本地図のみになるように加工します)。
サイト上の
Downloads
=>Large scale data, 1:10m
=>Cultural
=>Admin 1 – States, Provinces
=>Download states and provinces
から取得しておけば日本の都道府県別情報を含むデータを取得できます。
なお、地図データのライセンスはかなり緩いようです。以下の場所から原文を確認できます。
Natural Earth の利用規約
https://www.naturalearthdata.com/about/terms-of-use/(2) QGIS v3.12 / 地図を加工・ファイル形式変換ソフト
- Windows, macOS, Linux などで動作するフリーソフトです
- 地形のデータとなる点を移動したり不要な地形を削除したり加工ができます
- Natural Earth から取得した
.shp
形式のファイル から GeoJson 形式に変換できますa)
レイヤ
=>レイヤの追加
=>ベクタレイヤの追加
を選択b) Natural Earth で取得した shp ファイルを
ベクタデータセット
に指定して追加
ボタンをクリックc)
① 編集モードに切り替え
=>② 地物の選択に切り替え
=>③ 不要な地形を削除
d) 編集メニューから地物ポリゴンを移動させたり、新たにポリゴンを追加も可能
e) GeoJson データ形式としてエクスポート
(3) mapshaper / 地図データの軽量化サービス
- 地形のデータとなる点情報をイイ感じに削減してデータを軽量化できます
- (QGIS からでも軽量化できますが使いやすいのでこのサービスを利用しました)
Simplify
ボタンから表示されるスライダーで軽量化、Export
ボタンで出力できます。
3. 地図データを D3.js で描画
TypeScript のコードになりますが、JavaScript が読めれば問題ないと思います。
解説はコード内のコメントをご確認ください。なお、最後の方に動作確認デモへのリンクも載せてあります。
d3パッケージをインストール> npm install d3@5.15.0地図データをD3.jsで描画するコードimport * as d3 from "d3"; // GeoJsonファイルを読み込み import geoJson from "~/assets/japan.geo.json"; async function main() { const width = 400; // 描画サイズ: 幅 const height = 400; // 描画サイズ: 高さ const centerPos = [137.0, 38.2]; // 地図のセンター位置 const scale = 1000; // 地図のスケール // 地図の投影設定 const projection = d3 .geoMercator() .center(centerPos) .translate([width / 2, height / 2]) .scale(scale); // 地図をpathに投影(変換) const path = d3.geoPath().projection(projection); // SVG要素を追加 const svg = d3 .select(`#map-container`) .append(`svg`) .attr(`viewBox`, `0 0 ${width} ${height}`) .attr(`width`, `100%`) .attr(`height`, `100%`); // // [ メモ ] // 動的にGeoJsonファイルを読み込む場合は以下のコードを使用 // const geoJson = await d3.json(`/japan.geo.json`); // // 都道府県の領域データをpathで描画 svg .selectAll(`path`) .data(geoJson.features) .enter() .append(`path`) .attr(`d`, path) .attr(`stroke`, `#666`) .attr(`stroke-width`, 0.25) .attr(`fill`, `#2566CC`) .attr(`fill-opacity`, (item: any) => { // メモ // item.properties.name_ja に都道府県名が入っている // 透明度をランダムに指定する (0.0 - 1.0) return Math.random(); }) /** * 都道府県領域の MouseOver イベントハンドラ */ .on(`mouseover`, function(item: any) { // ラベル用のグループ const group = svg.append(`g`).attr(`id`, `label-group`); // 地図データから都道府県名を取得する const label = item.properties.name_ja; // 矩形を追加: テキストの枠 const rectElement = group .append(`rect`) .attr(`id`, `label-rect`) .attr(`stroke`, `#666`) .attr(`stroke-width`, 0.5) .attr(`fill`, `#fff`); // テキストを追加 const textElement = group .append(`text`) .attr(`id`, `label-text`) .text(label); // テキストのサイズから矩形のサイズを調整 const padding = { x: 5, y: 0 }; const textSize = textElement.node().getBBox(); rectElement .attr(`x`, textSize.x - padding.x) .attr(`y`, textSize.y - padding.y) .attr(`width`, textSize.width + padding.x * 2) .attr(`height`, textSize.height + padding.y * 2); // マウス位置の都道府県領域を赤色に変更 d3.select(this).attr(`fill`, `#CC4C39`); d3.select(this).attr(`stroke-width`, `1`); }) /** * 都道府県領域の MouseMove イベントハンドラ */ .on("mousemove", function(item: any) { // テキストのサイズ情報を取得 const textSize = svg .select("#label-text") .node() .getBBox(); // マウス位置からラベルの位置を指定 const labelPos = { x: d3.event.offsetX - textSize.width, y: d3.event.offsetY - textSize.height }; // ラベルの位置を移動 svg .select("#label-group") .attr(`transform`, `translate(${labelPos.x}, ${labelPos.y})`); }) /** * 都道府県領域の MouseOut イベントハンドラ */ .on(`mouseout`, function(item: any) { // ラベルグループを削除 svg.select("#label-group").remove(); // マウス位置の都道府県領域を青色に戻す d3.select(this).attr(`fill`, `#2566CC`); d3.select(this).attr(`stroke-width`, `0.25`); }); } main();以下のリンクに D3.js を使用して地図を描画するデモを用意しました。
4. 最後に
新型コロナウィルスの感染情報を可視化したサイト
https://hazard.westa.io/今回、この記事を作成するきっかけとなったプロジェクトです。
Nuxt.js + TypeScript の構成に D3.js と Chart.js でグラフ関係を描画して、Firebase-Hosting で SPA サイトとして運用しています。もしよかったらご覧いただけると幸いです。
勢いで書いてしまったので変な部分も多分にありそうですが、皆さんも
D3.js + 地図描画
いかがでしょうか。
記事の内容が『役に立った』『面白かった』と思ったら、ぜひ LGTM(いいね) を頂けると嬉しい限りでございます!
- 投稿日:2020-03-23T10:55:15+09:00
Vue.js入門7(私的メモ)
単一ファイルコンポーネントによる開発
特殊なツールを使わずとも、コンポーネントを組み合わせてWebアプリケーションを作ることはできるが、中規模以上のプロジェクトでは以下のような問題が生じる。
- JavaScriptのグローバルなスコープにおけるコンポーネントの管理
- エディタにおいてシンタックスハイライトが効かないJavaScriptによる文字列テンプレート(補完できないの辛い)
- コンポーネントに適用するCSSの名前空間管理
- コンポーネントのビルド処理
これらの問題を解決するため、単一ファイルコンポーネントを利用する。
ツールのインストール
Vue.jsで提供されるツールは、Node.jsで開発、npmによって配信されている。
そのため、ツールのインストールにはNode.jsとnpmのインストールが必要である。
これは以前、dockerで環境構築した際に導入した。
Vue CLI
Vue CLIは、Vue.js向けのアプリケーション開発環境をセットアップするなどの機能を提供する公式コマンドラインツールである。
これらを使用しない場合、個別にモジュール化、バンドルツール/プリプロセッサによるビルド、JavaScriptの政敵構文チェック(リント)、単体テストやE2Eテストをする必要があるため、かなりの手間となる。
Node.js、npmをインストールしてから
$ npm install -g @vue/cli@3.0.1(バージョン指定) @vue/cli-service-global@3.0.1(バージョン指定)とすれば、グローバル環境にVue CLIと依存モジュールがインストールされる。
//vueコマンドが使用できているか確認 $vue --version 3.0.1単一ファイルコンポーネントとは
単一ファイルコンポーネントとは、Vue.jsのコンポーネントを単独のファイルとして作成する機能。
.vue拡張子のファイル内に定義したコンポーネントで、template(HTML)、script(javascript)、style(css)のブロックで構成されたHTMLベースの構文で定義する。
Single File Componentsの頭文字からSFC(sfc)やVueコンポーネントと呼ぶことがある。
SFCの仕様
SFCを構成するそれぞれのブロックを確認する。
templateブロック
テンプレートを記述するブロック。
templateオプションと同じく、Mustache記法、v-ifなどの文法が使用できる。
scriptブロック
UIの振る舞いをスクリプトで制御するブロック。
SFC内に最大一つこのブロックを含むことができる。
ライブラリや他のコンポーネントのインポートはこの中で行う。
Vue.jsアプリケーションでSFCを利用するためには、エクスポートしなければならない。
その際は、ES Modulesのexport構文を利用する。
ES Modulesについてはこのサイトを確認。
styleブロック
UIの見た目を制御するブロック。
SFC内には複数のstyleブロックを含むことができる。
styleブロックは、SFCごとにスタイルをカプセル化することができる。
●カプセル化とは
従来のCSSはグローバルスコープのため、他のすらいると被らないようにBEMなどの記法に基づき利用されてきた。
BEMについてはこのサイトを確認。
これに対してSFCでは、CSS/CSSモジュールにスコープをつけることでスタイル定義を被らせないように記述できる(カプセル化)。
<style scoped> /* このSFC内でのみ有効 */ .xxx { ... ... ... } </style> <style> /* 全体のスタイルに有効 */ #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>ただし、公式サイトにも記述されているように、親スコープのCSSは子コンポーネントのルート要素に影響を与えるため、子コンポーネントのルート要素は親スコープのCSSと子スコープのCSSの両方から影響を受ける。
これに関しては、このサイトを確認した。
SFCのビルド
SFCは読み書きしやすいが、それはVue.js独自の仕組みである。
そのため、Webブラウザで動作可能な状態に変換しないといけない。
この変換を担ってくれるのが、バンドルツールと解析用のミドルウェアライブラリである。
変換・バンドルについては以下を確認。
この参考書ではwebpack(バンドラ)+Vue Loader(SFCの解析・まとめる処理担当)を利用しているが、そのほかにもrollup-plugin-vue+rollupやbrowserify+vueifyなどの組み合わせが存在する。
SFCの動作の確認
vue serveというコマンドは、webpackとVue Loaderを内部で利用しており、webpackの設定なしでビルドできる。
学習などに利用する分には最適。
Hello.vue<template lang="html"> <p class="message">メッセージ:{{ message }}</p> </template> <script> export default { name: "Hello", data () { return { message: "Hello Vue.js!!!", }; }, } </script> <style lang="css" scoped> .message { margin: 0; padding: 10px; color: #27ae60; font-weight: bold; background-color: #ecf0f1; } </style>vue serve Hello.vue --open
vue serve により、指定ファイルのビルド、そしてVue.jsの本体とその他の依存JavaScriptライブラリを一つのJavaSciprファイルにバンドルできる。
自分の場合は、
Command vue serve requires a global addon to be installed. Please run yarn global add @vue/cli-service-global and try again.という命令を受けてしまったため、vue createで作成された雛形を改良して表示させた。
App.vue//vue createコマンドで作成されたプロジェクト内にあるSFC <template> <div id="app"> <Hello></Hello> </div> </template> <script> import Hello from './components/Hello.vue' export default { name: 'App', components: { Hello, } } </script> <style> /* 全体のスタイルに有効 */ * { margin: 0; padding: 0; } #app { } </style>コードの確認
確認するポイントは
- JavaScriptファイルへのバンドル化
- HTMLとして描画されたテンプレート
- 挿入されたスタイル
である。
以下が全体のコード。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="/favicon.ico"> <title>test_project</title> <link href="/js/app.js" rel="preload" as="script"><link href="/js/chunk-vendors.js" rel="preload" as="script"> <style type="text/css"> .message[data-v-361a4bd2] { margin: 0; padding: 10px; color: #27ae60; font-weight: bold; background-color: #ecf0f1; } </style> <style type="text/css"> /* 全体のスタイルに有効 */ * { margin: 0; padding: 0; } #app { } </style> </head> <body> <noscript> <strong>We're sorry but test_project doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"> <p data-v-361a4bd2="" class="message">メッセージ:Hello Vue.js!!!</p> </div> <!-- built files will be auto injected --> <script type="text/javascript" src="/js/chunk-vendors.js"></script> <script type="text/javascript" src="/js/app.js"></script> </body> </html>●JavaScriptファイルへのバンドル化
SFCとそれが依存するライブラリ(Vue.js本体)がJavaScriptファイルにバンドル化されていることがわかる。
<script type="text/javascript" src="/js/app.js"></script>webpackなどのバンドルツールにより、JavaScirptやHTML、CSSといったWebブラウザ上でWebページといて表示させるために必要なリソースは全てJavaScriptファイルに束ねられる。
●HTMLとして描画されたテンプレート
templateに記述した内容がbody要素下に描画されていることがわかる。
<div id="app"> <p data-v-361a4bd2="" class="message">メッセージ:Hello Vue.js!!!</p> </div>このように描画されているのは、Vue LoaderがSFCを解析してJavaScriptモジュール(templateやらdataやらcomponentsやら)に変換してくれているからである。
●挿入されたスタイル
styleブロックで記述したCSSがhead要素下に挿入されていることがわかる。
<style type="text/css"> .message[data-v-361a4bd2] { margin: 0; padding: 10px; color: #27ae60; font-weight: bold; background-color: #ecf0f1; } </style> <style type="text/css"> /* 全体のスタイルに有効 */ * { margin: 0; padding: 0; } #app { } </style>styleブロックでスコープを使用した場合、
<p data-v-361a4bd2="" class="message">メッセージ:Hello Vue.js!!!</p>となる。
このように、カスタムデータ属性を用いてスコープごとにグループを作っている。
スコープ付きCSSの記述を増やすとこんな感じ。
Hello.vue<template lang="html"> <div class="square-box"> <p class="plain-text message">メッセージ:{{ message }}</p> <p class="plain-text username">{{ username }} さん</p> <p class="plain-text message">やあ</p> </div> </template> <script> export default { name: "Hello", data () { return { message: "Hello Vue.js!!!", username: "あはあは", }; }, } </script> <style lang="css" scoped> .square-box { margin: 30px; } .square-box > *:first-child { border-radius: 5px 5px 0 0; } .square-box > *:last-child { border-radius: 0 0 5px 5px; } .plain-text { margin: 0; padding: 10px; font-weight: bold; } .message { color: #27ae60; background-color: #2c3e50; } .username { color: ##2c3e50; background-color: #27ae60; } </style><html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="/favicon.ico"> <title>test_project</title> <link href="/js/app.js" rel="preload" as="script"><link href="/js/chunk-vendors.js" rel="preload" as="script"> <style type="text/css"> .square-box[data-v-361a4bd2] { margin: 30px; } .square-box > *[data-v-361a4bd2]:first-child { border-radius: 5px 5px 0 0; } .square-box > *[data-v-361a4bd2]:last-child { border-radius: 0 0 5px 5px; } .plain-text[data-v-361a4bd2] { margin: 0; padding: 10px; font-weight: bold; } .message[data-v-361a4bd2] { color: #27ae60; background-color: #2c3e50; } .username[data-v-361a4bd2] { color: ##2c3e50; background-color: #27ae60; } </style> <style type="text/css"> /* 全体のスタイルに有効 */ * { margin: 0; padding: 0; } #app { } </style> </head> <body> <noscript> <strong>We're sorry but test_project doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> </noscript> <div id="app"><div data-v-361a4bd2="" class="square-box"><p data-v-361a4bd2="" class="plain-text message">メッセージ:Hello Vue.js!!!</p><p data-v-361a4bd2="" class="plain-text username">あはあは さん</p><p data-v-361a4bd2="" class="plain-text message">やあ</p></div></div> <!-- built files will be auto injected --> <script type="text/javascript" src="/js/chunk-vendors.js"></script> <script type="text/javascript" src="/js/app.js"></script> </body> </html>data-v-361a4bd2というデータ属性を付与してHello.vue内でのみ使用できるCSSをグルーピングしている。
SFCの機能
SFCには色々と便利な機能があるらしい。
外部ファイルのインポート
SFCの各ブロックにおいて、src属性で外部ファイルを指定することで内容をインポートすることができる。
<template src="./xxxx.html"></template> <script src="./xxxx.js"></script> <style src="./xxxx.css"></style>これにより、既存のアプリケーション資産を流用することができる。
パスには、SFCからの相対パスを指定する。
スコープ付きCSS
先ほど説明したので省略するが、スコープ付きCSSを使用する場合の子コンポーネントのルート要素におけるスタイルには注意が必要なため、メモしておく。
以下のコードで確認してみる。
Hello.vue・・・子コンポーネント(InputBox.vue)を使用する親コンポーネント
InputBox・・・親コンポーネントのスコープ付きCSS(今回は.box-name)と同名のスコープ付きCSSが付与されたルート要素を持つ子コンポーネント
Hello.vue<template lang="html"> <div class="message-box"> <div class="square-box box-name"> <p class="plain-text message">メッセージ:{{ message }}</p> <p class="plain-text username">{{ username }} さん</p> <p class="plain-text message">やあ</p> </div> <input-box></input-box> </div> </template> <script> import InputBox from "./InputBox.vue"; export default { name: "Hello", data () { return { message: "Hello Vue.js!!!", username: "あはあは", }; }, components: { InputBox, } } </script> <style lang="css" scoped> .square-box { position: relative; margin: 30px; } .square-box > *:first-child { border-radius: 5px 5px 0 0; } .square-box > *:last-child { border-radius: 0 0 5px 5px; } .plain-text { margin: 0; padding: 10px; font-weight: bold; } .square-box > *:nth-child(2n+1) { color: #27ae60; background-color: #2c3e50; } .square-box > *:nth-child(2n) { color: ##2c3e50; background-color: #27ae60; } .box-name::before { content: "SQUARE BOX"; position: absolute; top: 0; right: 0; padding: 8px; border-radius: 0 5px 0 0; background-color: #212121; color: #ecf0f1; z-index: 2; } </style>InputBox.vue<template lang="html"> <div class="input-box box-name"> <label><span>名前:</span><input type="text"></label> <label><span>メールアドレス:</span><input type="email"></label> <label><span>内容:</span><textarea></textarea></label> </div> </template> <script> export default { components: { } } </script> <style lang="css" scoped> .input-box { position: relative; margin: 30px; padding: 10px; border: 1px solid gray; border-radius: 10px; } .input-box label { display: flex; align-items: center; margin-bottom: 20px; font-size: 1.2rem; } .input-box label:last-child { margin-bottom: 0; } .input-box label span { display: inline-block; width: 30%; } input, textarea { outline: none; border: 1px solid gray; border-radius: 5px; padding: 8px; font-size: 1.2rem; } textarea { width: 70%; height: 200px; } .box-name::before { content: "INPUT BOX"; position: absolute; top: 0; right: 0; padding: 8px; border-radius: 0 5px 0 0; background-color: #212121; color: #ecf0f1; z-index: 2; } </style>Hello.vueに記述されたスコープ付きCSSが他に影響を及ぼさないならば、
Hello.vue.box-name::before { content: "SQUARE BOX"; position: absolute; top: 0; right: 0; padding: 8px; border-radius: 0 5px 0 0; background-color: #212121; color: #ecf0f1; z-index: 2; }と、異なるスコープを持つInputBox.vueのCSS
InputBox.vue.box-name::before { content: "INPUT BOX"; position: absolute; top: 0; right: 0; padding: 8px; border-radius: 0 5px 0 0; background-color: #212121; color: #ecf0f1; z-index: 2; }はcontentの内容が違うため、「INPUT BOX」と表示されるはずである。
しかし、画面を確認してみると
どちらも「SQUARE BOX」と表示されている。
これが前述した
ただし、公式サイトにも記述されているように、親スコープのCSSは子コンポーネントのルート要素に影響を与えるため、子コンポーネントのルート要素は親スコープのCSSと子スコープのCSSの両方から影響を受ける。
これに関しては、このサイトを確認した。
という部分の例である。
要するに具体的な影響とは、スコープ付きCSSを持つ親から子コンポーネントを使用するときに、親のデータ属性が子コンポーネントのルート要素(今回の場合は.input-boxと.box-nameがついたdiv要素)へ付与されてしまい、子コンポーネントのデータ属性がバッティングしてしまうということである。
バッティングしている様子がこちら。
ルート要素に二つのデータ属性が付与されているのがわかる。
<!-- data-v-361a4bd2・・・親が持つデータ属性 data-v-66ea8951・・・子が持つデータ属性 --> <div data-v-361a4bd2="" class="message-box"> <div data-v-361a4bd2="" class="square-box box-name"> <p data-v-361a4bd2="" class="plain-text message">メッセージ:Hello Vue.js!!!</p> <p data-v-361a4bd2="" class="plain-text username">あはあは さん</p> <p data-v-361a4bd2="" class="plain-text message">やあ</p> </div> <div data-v-66ea8951="" data-v-361a4bd2="" class="input-box box-name"> <label data-v-66ea8951=""><span data-v-66ea8951="">名前:</span><input data-v-66ea8951="" type="text"></label> <label data-v-66ea8951=""><span data-v-66ea8951="">メールアドレス:</span><input data-v-66ea8951="" type="email"></label> <label data-v-66ea8951=""><span data-v-66ea8951="">内容:</span><textarea data-v-66ea8951=""></textarea></label> </div> </div>親と子でスコープ付きCSSがバッティングした場合は必ず親スコープのCSSが採用される?
最後、他に試した結果(同名でバッティングさせる)を2つだけ(合ってるかわからないけど)。
lose win グローバルスコープのCSS スコープ付きCSS 当たり前か ルートで定義したグローバルスコープのCSS 子コンポーネントで定義したグローバルスコープのCSS これは何か理由が?子で上書きされる? CSSモジュール
CSSのモジュール化に関して、Vue Loaderが実現させているScoped CSSの他に、Vue Loaderが依存しているcss-loaderによって実現されるCSS Modulesがある。
Scoped CSSではデータ属性を付与することで、コンポーネントごとにスコープを作成していたが、CSS Modulesでは一意なスタイル識別子を付与することで名前衝突を回避している。
詳しくは以下を参考にする。
カスタムブロック
SFCでは、templateやscript、styleのようなブロック要素をカスタムブロックとして独自に定義することができる。
定義だけであれば、SFCに独自のブロックを書くだけで完了する。
しかし、これをどのように機能させるかを指定しなければ動作はしない。
そのためにカスタムローダーを用意し、カスタムブロックを取り扱えるようにして、さらにそのカスタムブロックに記述された値を処理する必要がある。
カスタムローダーとは、webpackがバンドル時にファイルをどう処理するか指定するための仕組みである。
このカスタムローダーは、webpackとVue LoaderによるSFCの解析が完了したのちに呼び出される。
詳しくは以下を参考にする。
使い慣れてきたら、こういうのに手出してみたいけど、沼りそう。。。
参考文献
- 投稿日:2020-03-23T09:00:06+09:00
【Vue】モーダルコンポーネント(コピペ用)
概要
- 何の変哲もないモーダルコンポーネントです。
- 業務でもよく使うのでコピペ用に残しておきます。
- 動作確認はお手軽なcodesandboxで!!
/components/Modal.vue<template> <div> <div class="el-modal" :aria-hidden="isOpen ? 'false' : 'true'"> <div class="el-modal__holder"> <button @click="close()">閉じる</button> </div> <div class="el-modal__overlay"></div> </div> <button @click="open()">開く</button> </div> </template> <script> export default { name: "Modal", data: function() { return { isOpen: false }; }, methods: { open: function() { this.isOpen = true; }, close: function() { this.isOpen = false; } } }; </script> <style lang="sass"> .el-modal visibility: hidden &__holder position: fixed z-index: 1100 background-color: blue width: 50% height: 50% &__overlay position: fixed z-index: 1000 background-color: red width: 100% height: 100% &[aria-hidden=false] visibility: visible </style>App.vue<template> <div id="app"> <modal/> </div> </template> <script> import Modal from "./components/Modal"; export default { name: "App", components: { Modal } }; </script>
- 投稿日:2020-03-23T01:52:03+09:00
できるだけ楽にlaravel-mixから素のwebpackに移行する
laravel-mixからwebpackに移行する
webpackのラッパーであるlaravel-mixは使い始めは死ぬほど便利なのですが、さまざまな事情によりやめたくなる瞬間がやって来ることがあります。
- 特定のnpm packageだけ別チャンクにしたい(例:正規表現等で別チャンクにするパッケージを指定する)
- プロジェクトが依存しているnpm packageの読み込みをもっとカスタマイズしたい(例:moment.jsから不要なロケールを消す、特定のパッケージは自力でビルドする)
- 複雑なsassビルドがしたい(例:共通変数の読み込み)
- mix.webpackConfigでカスタマイズし続けてたら、ある日突然「これ素のWebpackで書いたほうが楽なのでは?」と感じ始めた
とはいえ、laravel-mixをやめたくなる一方で、今後も使い続けたい非常に便利な機能もあります。
ひとつめは、分割されたファイルをいい感じに読み込んでくれる点、もうひとつは、バージョニングされたファイルも1行で読み込んでくれる点です。app.blade.php<script src="{{ mix('js/manifest.js') }}"></script> <script src="{{ mix('js/chunks/vendors.js') }}"></script> <script src="{{ mix('js/app.js') }}"></script>mix-manifest.json{ "/app.js": "/app.js?id=1b4046accec02c510461", "/app.css": "/app.css?id=ed9d614f178e884779da", "/manifest.js": "/manifest.js?id=186d4d6bdb3251d7b7f2", "/vendor.js": "/vendor.js?id=331a3da9d77df744d791" }そこで本記事では、laravel-mixをやめつつ上記2点の便利ポイントをwebpackで実現する方法について書きます。
※webpack+vueで最低限のビルドを通す方法は以下の記事に譲ります。
https://ics.media/entry/16028/#webpack-babel-vue
(スーパー分かりやすい記事でおすすめです!)1. webpackのCode Splittingを使って、特定のパッケージを別ファイルにする
ここでは、moment.jsをvendors.jsに分割してみます。
webpack.config.jsentry: { app: path.join(__dirname, '/resources/assets/js/app.js'), }, // optimizationのところが、ファイル分割をしている部分 optimization: { splitChunks: { cacheGroups: { default: false, // このvendorsの部分は、好きな名前にしてOK vendors: { test: /node_modules(?!\/moment)/, name: 'vendors', chunks: 'all', }, } }, }laravel-mixでは
.extract
を記述していましたが、webpackではsplitChunks
と表現します。
上記の設定でビルドすると、app.jsに加えて、vendors.jsというファイルが出力できたはずです。その他オプションなどの公式ドキュメントはこちら
https://webpack.js.org/plugins/split-chunks-plugin/2. バージョニングしたファイルをいい感じに読み込む
まずはバージョニングする
laravel-mixでは出力したファイルにバージョンごとの値を付与することを「versioning」と言い、
.version
で実行していましたが、webpackではcaching
と表現します。地味に表現の仕方が違ってややこしい!cachingするためには、outputオプションで出力ファイル名と[chunkhash]を指定します。こうすると、ファイル名の末尾に
/app.js?id=1b4046accec02c510461
といった感じでhashがつくようになります。webpack.confing.jsentry: { app: path.join(__dirname, '/resources/assets/js/app.js'), }, output: { filename: 'js/[name].js?id=[chunkhash]', chunkFilename: 'js/chunks/[name].js?id=[chunkhash]', publicPath: '/', path: path.join(__dirname, '/public'), }, optimization: { splitChunks: { cacheGroups: { default: false, vendors: { test: /node_modules(?!\/moment)/, name: 'vendors', chunks: 'all', }, } }, }詳細はこちら
https://webpack.js.org/configuration/output/#outputchunkfilenamemix-manifest.jsonを、laravel-mixを使わずに自作する
app.blade.php側で
<script src="{{ mix('js/app.js') }}"></script>
のように簡単に読み込むためには、mix-manifest.jsonが必要です。ここでは、webpack-stats-pluginを使って自作してみます。本来はビルドした際の統計情報を書き出したりするプラグインなのですが、ビルド後の統計情報にはハッシュ付きファイル名も含まれているので、これを使ってしまおう!という作戦です。
https://github.com/FormidableLabs/webpack-stats-pluginインストール
$ npm install --save-dev webpack-stats-plugin または $ yarn add --dev webpack-stats-pluginwebpackの設定
webpack.config.js// プラグインの読み込み const { StatsWriterPlugin } = require("webpack-stats-plugin") // mix-manifest.jsonをどのように書き出すかを指定 const MixManifest = data => { return JSON.stringify({ "/js/app.js": '/' + data.assetsByChunkName.app, "/js/chunks/vendors.js": '/' + data.assetsByChunkName.vendors, }) } module.exports = { // (関連なさそうなオプションは省略) entry: { app: path.join(__dirname, '/resources/assets/js/app.js'), }, output: { filename: 'js/[name].js?id=[chunkhash]', chunkFilename: 'js/chunks/[name].js?id=[chunkhash]', publicPath: '/', path: path.join(__dirname, '/public'), }, optimization: { splitChunks: { cacheGroups: { default: false, vendors: { test: /node_modules(?!\/moment)/, name: 'vendors', chunks: 'all', }, } }, }, // ここで、書き出すファイル名を指定 plugins: [ new StatsWriterPlugin({ filename: "mix-manifest.json", transform: MixManifest }) ] }上記でビルドすると、laravel-mixが出力していたmix-manifest.jsonと同じ形式のJSONファイルが出力されます。
それぞれのビルド環境によってapp.jsやvendors.jsの他にもcssファイルを出力したりとさまざまなケースがあるかと思いますが、ビルド後のファイル名は
data.assetsByChunkName
オブジェクトの中に全て入っているので、そこから抽出できます。例えばこんな感じ。webpack.config.jsconst MixManifest = data => { return JSON.stringify({ "/css/app.css": '/' + data.assetsByChunkName.app[1], "/js/app.js": '/' + data.assetsByChunkName.app[0], "/js/chunks/vendors.js": '/' + data.assetsByChunkName.vendors, "/css/commons.css": '/' + data.assetsByChunkName.commons[1], }) }これで、無事mix-manifest.jsonが出来ました!あとはbladeファイルに配置するだけです。
app.blade.php// manifestファイルは必要ありません <script src="{{ mix('js/chunks/vendors.js') }}"></script> <script src="{{ mix('js/app.js') }}"></script>それでは、webpackでカスタマイズし放題のとっても楽しい(苦しい)ライフをお過ごしください?
- 投稿日:2020-03-23T00:33:05+09:00
【初心者が「基礎から学ぶVue.js」でVueを学ぶ:1日目】ライフサイクルフック/リアクティブ
誰が、何を、どのようにして、どこまでやるのか
WEBデザイナー/コーダーの私が、Vue.jsの勉強を、「基礎から学ぶVue.js」を参照しながら、SPAを一人で作れるようになるまでやります。変なところがあったら突っ込んでもらえると嬉しいですが、反面、自分用のまとめなので、初学者がこれを見て一緒に学ぶような感じにはなってません。同書の購入を検討している人とかは参考になるかもしれません。
備考
- 私のコーディングレベル
- HTML、CSS(Sass)の経験年数が1年半程度。
- gulpを何となく使える
- webpackはあんまりわかってない
- JSはES6であれば多少は書ける(アコーディオンだとかモーダルウィンドウだとか)
- 属している組織内に「フロントエンド」をやっている人はいない。ゴリゴリにバックエンドの人と、デザイナーだけがいる状態。必然、あんまり聞ける人はいない。ググり力が試される状況。
- 参考書(「基礎から学ぶVue.js」)選定の理由
- 本当はWEB学習サイトでヌルっとそこまで勉強したかったが、SPA絡みの話(Vue Router周りの話とか)までカバーしている日本語サイトが見つからなかった。
- (逆に言うと、本当に基本的なところは、ドットインストールやらで勉強しているので省きながらメモをつけていく形になると思う)
- 実は入門用書籍でもSPA作ろうってところまで行ってるのは「基礎から学ぶ〜」以外はあんま見つからなかったのでほぼこれ一度となった(2020年3月時点)。でもよく見たら他の本にもVue CLIの話とかはあるっぽいので、そこが該当するのかもしれない。SPAだとかVue Routerとかいうのが索引に出てくるのがあくまでこれしか私が見つけられなかった、というだけの話かもしれない。
chapter1 Vue.jsとフレームワークの基礎知識
ライフサイクルフック
Vueではインスタンスが作られてから、削除されるまでの間で、ライフサイクルフックを実行することができます。
Vueのライフサイクルを完全に理解した特定の処理を、インスタンスの揺り籠から墓場までのどっかで実行するための記述みたいです。そんなライフサイクルフックの一つ、createdに関する記述で気になる箇所が一つ。
createdは、このメソッドを登録したVueインスタンスが作成され、データの監視などリアクティブまわりの初期化が終わった後に呼び出されます。
基礎から学ぶVue.js p.45リアクティブって何ぞ...? これも読み進めていくと、少しページ飛んで次の章の冒頭に解説が出てきました。
chapter2 データの登録と更新
リアクティブデータとは、Vue.jsによって取得したとき(get)と代入したとき(set)のフック処理が登録された、反応できるデータのことを言います。
基礎から学ぶVue.js p.52「反応できる」ならReactableじゃねえのかとか思うのですが(こういうややこしい事を言い出す人以外にはわかりやすい説明なんだと思う)、実際には、リアクティブっつうのは入力に応じて「反応的に」変化する部分のことを言うのでしょう。だとすれば、
DOMの更新を自動化する「データバインディング」を行うには、テンプレートで仕様するすべてのデータは「リアクティブデータ」として定義する必要があります。
基礎から学ぶVue.js p.52これは〈良い感じに変わってほしい部分があるんやったら、まずHTML側のテンプレート内で「ここがその変わる部分でっせ!」、ほんでjs側に「そこはこんな風に変わりまっせ!」と明示せないかんよ!〉ってことでしょう。多分。
dataオプション直下のプロパティは後から追加できないため、内容が決まっていない場合でも初期値や空データとして定義しておく必要があります。(中略)なるべく、後から代入されるデータと同じ型で定義しておくようにしましょう。
基礎から学ぶVue.js p.53//初期データの例 data:{ newTodoText: '', visitCount: 0, hideCompletedTodos: false, todos: [], error: null }後からdataのプロパティを増やすことは出来ないので、予め網羅しておいて、かつどんな値(テキストなのか数値なのか真偽値なのか...etc)が入るかも決めておいたほうがいいですよ、ということみたいです。
今日はここまで。次回はchapter2の続き、section08「テキスト属性のデータバインディング」(p.54〜)。
- 投稿日:2020-03-23T00:25:52+09:00
ScaffoldHubってすごいのでは?
はじめに
最近のWebアプリを作るために様々な技術が登場しています。
結局どれで作るのが良いのだろうとふと疑問に思った時、思わず目に入ってきたサイトを見つけました。それがScaffoldHubでした。
より多くの人にScaffoldHubを知ってもらいたいと思い
(実は既に知っている人大多数説あり)、恥ずかしながら記事の初投稿を決意しました。この記事はScaffoldHubの公式サイトに書いてあることを記載しているだけなので、詳しくは以下のURLの公式サイトや公式ブログを参照してください。
https://scaffoldhub.io/ScaffoldHubとは
ScaffoldHubとは、JavaScriptで書かれたWebアプリのコード自動生成サービスです。選べるものとして、フロントエンドには御三家(React, Vue, Angular)、バックエンドはNodeJSのみ、DBにはSQL, MongoDB, Firebase Firestoreが選べるようです。Webアプリ生成した後は、生成されたソースを基に機能拡張ができるので、自分好みに改良できます。一からWebアプリを作らなくて良いのは魅力的です。
ScaffoldHubの価格
サンプルとしてWebアプリを動かすのは無料ですが、ソースコードのダウンロードはもちろん有料です。気になる価格ですが、なんと39ドル!!
これは安いのでは!?と思いました。公式サイトを見る限り、これ以上かかる費用はなさそうですが、購入を検討されている方は購入前に必ず詳細を確認してください。ScaffoldHubの特徴
いくつか抜粋して、ScaffoldHubの特徴を記載します。
認証機能
ユーザ名、パスワード認証やfacebook, Twitter, Google認証が実装されているようで、これらの認証機能を一から実装する必要がないようです。ユーザ・ロール、パーミッション管理
それぞれを管理でき、権限制御ができるようです。監査ログ
ユーザのアクション(ログイン、ページ遷移等)がログに記録されるようです。モバイルとの親和性
ScaffoldHubから生成したWebアプリはモバイル用のデザインも用意されているようです。まだまだ、ありますが公式サイトを見ると全容がわかるので、詳細はそちらを参照しください、
終わりに
ScaffoldHubは、2019年5月からサービスを開始していたようですが、
ScaffoldHubに関する記事が見当たらなかったので、記事投稿をしてみました。(今回の記事がQiitaデビュー)
まだ、次回の記事はScaffoldHubをしっかり見れていないのでこれからデモを動かしたときの使用感を試していきたいと思います。ScaffoldHubについて、知っている方や気になる方がいらっしゃればこの記事を機会にぜひ公式サイトをみて欲しいです。また、気軽なコメントもぜひお待ちしております。
https://scaffoldhub.io/