- 投稿日:2020-04-02T23:53:29+09:00
Laravel7.xにVue-Routerを実装してSPAを構築してみる (3)
概要
Laravel側で設定してないURLでリロードすると404エラーページが出てしまう。
リロードすると…
リロードすることでVue-Routerで設定したラウトのページへ飛ぶようにしてみる
詳細
Laravel側のラウト設定
- ラウトしていないURLもIndexページへ遷移するようにコードを追加
- URL情報をパラメータとしてVue-Routerに渡せるように設定
routes/web.php// Route::get('/', function () { // return view('welcome'); // }); Route::get('/{extraUrl?}', function ($extraUrl = null) { return view('welcome', [ 'extraUrl' => $extraUrl ]); });上の処理でprefix(サブディレクトリ)がないURLはIndexページへ遷移することが可能
例1)localhost:8000/report
例2)localhost:8000/report/report
→ prefixが設定されているので404エラーが出力される… ここは後で対応してみようVue-Routerでデフォルトラウトを設定
- Vue-routerに設定されてないラウトが来た場合はIndexページへ遷移されるように設定
resources/js/app.jsconst router = new VueRouter({ mode: 'history', routes: [ // Indexページ { path: '/report', name: 'report-page', component: ReportPage }, // フォームページ { path: '/form', name: 'form-page', component: FormPage }, // デフォルトページ;Indexページ { path: '*', redirect: '/report' }, ] });結果確認
→ 404エラーページが出力されず、設定したデフォうとページに遷移されたのでOK
- 投稿日:2020-04-02T23:49:19+09:00
Vue Internals①astを中心に 1-3compile(generate)
はじめに
前回はAST作成までを見てきました。
今回はASTがどのように
{render: "with(this){return _c('div',{attrs:{"id":"vue_example"}},[_v("\n "+_s(message)+"\n")])}"
みたいな形になっていくか見ていきます。generateから
var createCompiler = createCompilerCreator(function baseCompile ( template, options ) { var ast = parse(template.trim(), options); if (options.optimize !== false) { optimize(ast, options); } //今回はここから var code = generate(ast, options); return { ast: ast, render: code.render, staticRenderFns: code.staticRenderFns } });generateの本体はgetElement
function generate ( ast, options ) { var state = new CodegenState(options); var code = ast ? genElement(ast, state) : '_c("div")'; return { render: ("with(this){return " + code + "}"), staticRenderFns: state.staticRenderFns } }function genElement (el, state) { if (el.parent) { el.pre = el.pre || el.parent.pre; } if (el.staticRoot && !el.staticProcessed) { return genStatic(el, state) } else if (el.once && !el.onceProcessed) { return genOnce(el, state) } else if (el.for && !el.forProcessed) { return genFor(el, state) } else if (el.if && !el.ifProcessed) { return genIf(el, state) } else if (el.tag === 'template' && !el.slotTarget && !state.pre) { return genChildren(el, state) || 'void 0' } else if (el.tag === 'slot') { return genSlot(el, state) } else { // component or element var code; if (el.component) { code = genComponent(el.component, el, state); } else { var data; if (!el.plain || (el.pre && state.maybeComponent(el))) { data = genData$2(el, state); } var children = el.inlineTemplate ? null : genChildren(el, state, true); code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")"; } // module transforms for (var i = 0; i < state.transforms.length; i++) { code = state.transforms[i](el, code); } return code } }getElementはstaticだったりforとか特殊な奴はgenStaticとかで処理、今回は特殊な処理もなくgetData$2とgetChildrenのみ
getData$2
function genData$2 (el, state) { var data = '{'; // directives first. // directives may mutate the el's other properties before they are generated. var dirs = genDirectives(el, state); if (dirs) { data += dirs + ','; } // key if (el.key) { data += "key:" + (el.key) + ","; } // ref if (el.ref) { data += "ref:" + (el.ref) + ","; } if (el.refInFor) { data += "refInFor:true,"; } // pre if (el.pre) { data += "pre:true,"; } // record original tag name for components using "is" attribute if (el.component) { data += "tag:\"" + (el.tag) + "\","; } // module data generation functions for (var i = 0; i < state.dataGenFns.length; i++) { data += state.dataGenFns[i](el); } // attributes if (el.attrs) { data += "attrs:" + (genProps(el.attrs)) + ","; } // DOM props if (el.props) { data += "domProps:" + (genProps(el.props)) + ","; } // event handlers if (el.events) { data += (genHandlers(el.events, false)) + ","; } if (el.nativeEvents) { data += (genHandlers(el.nativeEvents, true)) + ","; } // slot target // only for non-scoped slots if (el.slotTarget && !el.slotScope) { data += "slot:" + (el.slotTarget) + ","; } // scoped slots if (el.scopedSlots) { data += (genScopedSlots(el, el.scopedSlots, state)) + ","; } // component v-model if (el.model) { data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},"; } // inline-template if (el.inlineTemplate) { var inlineTemplate = genInlineTemplate(el, state); if (inlineTemplate) { data += inlineTemplate + ","; } } data = data.replace(/,$/, '') + '}'; // v-bind dynamic argument wrap // v-bind with dynamic arguments must be applied using the same v-bind object // merge helper so that class/style/mustUseProp attrs are handled correctly. if (el.dynamicAttrs) { data = "_b(" + data + ",\"" + (el.tag) + "\"," + (genProps(el.dynamicAttrs)) + ")"; } // v-bind data wrap if (el.wrapData) { data = el.wrapData(data); } // v-on data wrap if (el.wrapListeners) { data = el.wrapListeners(data); } return data }state.dataGenFnsでstyleとclassが追加される、ほかにもいろいろdataに追加しているんだけど、今回はattrだけ
staticProps.slice(0,-1)で最後の,をとって{"id":"vue_example"}として、最終的に
data="{attrs:{"id":"vue_example"},"となる
data.replace(/,$/, '') + '}'
で,をとって}を追加し、getData$2からは
"{attrs:{"id":"vue_example"}}"が返るgetChildren
genChildrenはchildrenからchildを一つ一つとりだして、childにgen=genNodeを適応させて、その返り値を,で繋げていって[gen(child1),gen(child2)]みたいにしていくgenNodeからの返り値がv("\n "+_s(message)+"\n")で今回はchildが一つなのでgenChildrenの返り値は
"[v("\n "+_s(message)+"\n")]"最終的に生成されるcodeは
"_c('div',{attrs:{"id":"vue_example"}},[_v("\n "+_s(message)+"\n")])"となる
これがgenerateのcode=getElement()のcodeでgenerateからの返り値は
render: "with(this){return _c('div',{attrs:{"id":"vue_example"}},[_v("\n "+_s(message)+"\n")])}" staticRenderFns: []ここまででcompileが終わり、baseCompie→compile→compileToFunctionまで戻る
compileToFunctionast: {type: 1, tag: "div", attrsList: Array(1), attrsMap: {…}, rawAttrsMap: {…}, …} render: "with(this){return _c('div',{attrs:{"id":"vue_example"}},[_v("\n "+_s(message)+"\n")])}" staticRenderFns: [] errors: [] tips: [] __proto__: Objectres.renderにcreateFunctionでつくったやつが入る
res.render(function anonymous( ) { with(this){return _c('div',{attrs:{"id":"vue_example"}},[_v("\n "+_s(message)+"\n")])} })最後にcacheを作ってcompileToFunction終わり
return (cache[key] = res) key="<div id="vue_example"> {{message}} </div>" res=render: ƒ anonymous( ) length: 0 name: "anonymous" arguments: null caller: null prototype: {constructor: ƒ} __proto__: ƒ () [[FunctionLocation]]: VM46968:1 [[Scopes]]: Scopes[1] staticRenderFns: [] __proto__: Objectそして、Vue.$mountまで戻って、this.$optionsにrenderとstaticRenderFnsを追加して、this.$optionは
components: {} directives: {} filters: {} _base: ƒ Vue(options) el: "#vue_example" data: ƒ mergedInstanceDataFn() render: ƒ anonymous( ) staticRenderFns: [] __proto__: Objectとなります。
ここまででcompile部分終了です、次回はmountComponentでやっと本当のdomを作成していくます、ここで大目標の一つ{{message}}の処理方法がわかります!
- 投稿日:2020-04-02T23:30:40+09:00
【カンタン】Vueで縦に自動伸縮するtextareaの作り方 | How to make auto resizing <textarea> by Vue.js
インターネッツで調べてみても中途半端なtextareaしか出てこなかったので、備忘も兼ねた記事です。
伸びるけど縮まないとか、縮むけど1文字いれるごとに2pxずつしか縮まないとか、意味なくない!?!?!?!?!?!?
んおお!?!?!?!?
まずは結果
うおおおおおおおお
うおおおおおおおお
うおおおおおおおお
うおおおおおおおお
うおおおおおおおおうおおおおおおおお
うおおおおおおおお
うおおおおおおおおうおおおおおおおおうおおおおおおおお
うおおおおおおおお
うおおおおおおおお
うおおおおおおおおコード
今気づいたけどマークダウンの候補にvuejsってある。。
便利すぎない?業務で書いたコードからいらないやつ消した感じなので、このまま動かなかったらごめんなさい。
編集リクエスト送っていただければーーーMyTextarea.vue<template> <textarea class="textarea bg-white" :style="textareaStyle" :placeholder="placeholder" :value="value" @input="handleInput($event)" /> </template> <script lang="ts"> import Vue from "vue" export default Vue.extend({ props: { placeholder: { type: String, default: "" }, value: { type: String, default: '' } }, data() { return { textareaHeight: 100 // デフォルト値いれとく。minHeightといっしょでよい。borderあるのでちょっとずれる } }, computed: { textareaStyle(): object { // 動的にtextareaのheightをいじれるようにしている return { height: `${this.textareaHeight}px` } } }, methods: { async handleInput(event: any) { // 入力されるたびによばれる。anyなのはゆるして。。。 this.$emit('input', event.target.value) // これは親に伝えるためだけ。 this.textareaHeight = 0 // ミソ。一旦textareaのheightを0にしちゃう await this.$nextTick() // さらにミソ。ここで待たないとDOMの更新前に下のコードが走って変な挙動になる // heightが0になった瞬間textareaはminHeight: 100になる // 入力済み文字列の高さが100を超えたら、scrollHeightが必要な分だけ大きくなる = それをheightにしちゃえばOK! this.textareaHeight = event.target.scrollHeight } } }) </script> <style lang="stylus" scoped> .textarea-container { width: 100%; } .textarea { width: 100%; min-height: 100px; // ここはお好み。変えるならdata()の値も変えるとよいよ border: 1px solid #D9D9D9 border-radius: 4px; padding: 5px 12px; &::placeholder { color: #D9D9D9 } } </style>TL;DR
this.$nextTickがミソなんじゃぞ。
- 投稿日:2020-04-02T22:44:16+09:00
Vue.js でインクリメンタルサーチ
インクリメンタルサーチとは
インクリメンタルサーチ(英語: incremental search)は、アプリケーションにおける検索方法のひとつ。検索したい単語をすべて入力した上で検索するのではなく、入力のたびごとに即座に候補を表示させる
完成品
HTML
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>インクリメンタルサーチ</title> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <link rel="stylesheet" href="style.css"> </head> <body> <div id="app"> <!-- v-model は任意の form 要素にある value、checked、 または selected 属性の初期値を無視します。input または textarea は常に、信頼できる情報源として Vue インスタンスを扱います。 コンポーネントの data オプションの中で JavaScript 側で初期値を宣言する必要があります --> <input type="text" placeholder="検索" v-model="search"> <select v-model="sort"> <option value="">ソート無し</option> <option value="asc">昇順</option> <option value="desc">降順</option> </select> <transition-group tag="ul"> <li v-for="item in sortedList" v-bind:key="item.id"> {{ item.text }} </li> </transition-group> </div> <script src="main.js"></script> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </body> </html>JS
new Vue({ el: "#app", data:{ search: '', sort: '', list: [ { id: 1, text: 'Python' }, { id:2, text: 'Ruby'}, { id:3, text: 'PHP'}, { id:4, text: 'JavaScript'}, { id:5, text: 'Java'}, { id:6, text: 'Go'}, { id:7, text: 'C'}, { id:8, text: 'C#'}, { id: 9, text: 'Rails' }, { id:10, text: 'Django'}, { id:11, text: 'MySQL'}, { id:12, text: 'Vue.jst'}, { id:13, text: 'react'}, { id:14, text: 'Docker'}, { id:15, text: 'unity'}, { id:16, text: 'jQuery'} ] }, computed: { filteredList: function(){ return this.list.filter(function(item){ return item.text.indexOf(this.search) > -1 }, this) }, sortedList: function(){ var copy = this.filteredList.slice() if(this.sort === 'asc' ){ return copy.sort(this.comparatorAsc) } else if(this.sort === 'desc') { return copy.sort(this.comparatorDesc) } else{ return copy } } }, methods:{ comparatorAsc: function(itemA, itemB){ if(itemA.text < itemB.text){ return -1 } else if(itemA > itemB.text){ return 1 } else{ return 0 } }, comparatorDesc: function(){ if(itemA.text > itemB.text){ return -1 } else if(itemA < itemB.text){ return 1 } else{ return 0 } }, } });CSS
body{ font-family: sans-serif; } input, select{ padding: 2px 8px; font-size: inherit; vertical-align: middle; } ul { position: relative; margin-top: 6px; padding: 0; width: 300px; list-style: none; } li{ margin: 0; padding: 10px; border-bottom: 1px solid #ddd; } .v-move{ transition: transform 300ms ease-out; } .v-enter-active { transition: 300ms; } .v-enter{ opacity: 0; } .v-enter-to{ opacity: 1; } .v-leave-active{ transition: 300ms; } .v-leave{ opacity: 1; } .v-leave-to{ opacity: 0; }
- 投稿日:2020-04-02T20:22:59+09:00
vueの勉強し直し
Javascriptのフレームワーク「vue.js」について書いていきます。
まずは Hello World
<div id="app">{{ message }}</div>const app = new Vue({ el: '#app', data: { message: 'Hello World' } })変数を
{{}}で囲むことでHTMLとして表示することができます。
双方向データバインディング
vueと言ったらこれが最初に思い浮かびました。
初めて実装したときは感動しました。
<div id="app"> <p>{{ inputText }}</p> <input v-model="inputText"> </div>const app = new Vue ({ el: '#app', data: { inputText: '' } })テキストフィールドで入力した文字がリアルタイムでテキストとして反映されます。
メソッドも追加できます
<div id="app"> <p>{{ count }} 回クリックされました!</p> <button v-on:click="counter">押してね</button> </div>const app = new Vue ({ el: '#app', data: { count: 0 }, methods: { counter: function(){ this.count++ } } })vanillaや、jQueryのようにclickなどのイベントのメソッドも設定することがでいます。
<button @click="counter">押してね</button>と書き換えることができます。
- 投稿日:2020-04-02T20:13:01+09:00
初めてのFirebaseを触ってみる(導入編)
はじめに
おはようございます。こんにちは。こんばんは。
今回は初めてFirebaseを触ってみるということで、Firebaseとはなんぞや?
とかFirebaseってどうやって使うの?と行ったことをまとめていきたいと思います。Firebaseとは?
Firebaseとは、Googleが提供している、すばやく高品質のモバイルアプリやWebアプリケーションを開発することができるプラットフォームです。
Firebaseを使うことで、開発者はアプリケーションの開発に専念でき、バックエンドで動くサービスを作成する必要も管理する必要もありません。
ここでいうバックエンドとは、サービスの内、見えないところでデータの処理や保存などを行う要素のことです。Firebaseは、BaaS(Backend as a service)の1つです。
サービスの早期リリースという要件が求められたときに、サーバーレスアーキテクチャが注目され、 BaaSというクラウドサービスが登場しました。
モバイル向けBaaSということで、MBaaSとよばれることもあります。BaaSを使うことにより、アプリ単体がモバイル端末上で動作するだけでなく、外部からの働きかけを行うことで、アプリがそれに応じた動作を行うことができるようになります。
こう行った方々にオススメ
- 「制作したポートフォリオをできるだけ簡単に公開したい」
- 「Vue.jsやReactを使ってWebサービスを作りたいけれど、バックエンドは苦手なので避けたい」
- 「フロントエンドの勉強で何か作りたいけれど、バックエンドの実装に時間を掛けたくない」
Firebaseを導入する
今回は例としてNuxt.jsを使用しての解説です。
※Nuxt.jsのプロジェクトは作成済みとする。①Firebaseにログイン
https://firebase.google.com/にアクセスする。
②コンソールへ移動
③プロジェクト追加をクリックする
④プロジェクト名などを入力し、最終「プロジェクト作成」ボタンをクリック
しばらく待機で終了。
⑤アプリに Firebase を追加して利用を開始しましょう
すると必要条件が出てくるので
plagins/firebase.jsを作成し、準備完了。plagins.firebase.jsimport firebase from 'firebase' if (!firebase.apps.length) { firebase.initializeApp({ ///この間を自分のアプリ情報に変える apiKey: "**************************", authDomain: "*********************", databaseURL: "******************", projectId: "*************", storageBucket: "********************", messagingSenderId: "*****************", appId: "*******************", measurementId: "*************************" //------------------------------- }) } export default firebase以上。
次回からはfirebaseが提供している機能の紹介を5回ぐらいに分け解説していきます。最後まで読んでいただきありがとうございました。
- 投稿日:2020-04-02T20:12:30+09:00
Vue Internals①astを中心に 1-2compile(AST)
はじめに
前回はcompileまでを見てきました。
今回はcompileのast部分を見ていきたいと思います。ASTとは?
compilerだったりデータベースだったりにも出てくるんですけど 例えばint a=5みたいなやつをを処理しやすいデータの形にすることをparseといい、それの役割を担うやつがparserといいます。
そして、そのわかりやすいデータのことをASTといいます。
1-1でも書いた通り"<div id="vue_example"> {{message}} </div>"が
parserでObject type: 1 tag: "div" attrsList: Array(1) 0: {name: "id", value: "vue_example", start: 5, end: 21} length: 1 __proto__: Array(0) attrsMap: {id: "vue_example"} rawAttrsMap: id: {name: "id", value: "vue_example", start: 5, end: 21} __proto__: Object parent: undefined children: Array(1) 0: {type: 2, expression: ""\n "+_s(message)+"\n"", tokens: Array(3), text: "↵ {{message}}↵", start: 22, …} length: 1 __proto__: Array(0) start: 0 end: 43 plain: false attrs: Array(1) 0: {name: "id", value: ""vue_example"", dynamic: undefined, start: 5, end: 21} length: 1 __proto__: Array(0) __proto__: Objectとなります。
ごちゃごちゃしていて分かりづらいですが、これからどうやってこのようなASTになるか見ていきましょう。compileToFunctionから
ここのキャッシュは二回目以降のもので、compileToFunctionの最後にcacheに生成物を入れています。
compile
return function createCompiler (baseOptions: CompilerOptions) { function compile ( template: string, options?: CompilerOptions ): CompiledResult { const finalOptions = Object.create(baseOptions) const errors = [] const tips = [] for (const key in options) { if (key !== 'modules' && key !== 'directives') { finalOptions[key] = options[key] } } finalOptions.warn = warn const compiled = baseCompile(template.trim(), finalOptions) if (process.env.NODE_ENV !== 'production') { detectErrors(compiled.ast, warn) } compiled.errors = errors compiled.tips = tips return compiled } return { compile, compileToFunctions: createCompileToFunctionFn(compile) } }baseOptionsは
expectHTML: true modules: (3) [{…}, {…}, {…}] directives: {model: ƒ, text: ƒ, html: ƒ} isPreTag: ƒ (tag) isUnaryTag: ƒ (val) mustUseProp: ƒ (tag, type, attr) canBeLeftOpenTag: ƒ (val) isReservedTag: ƒ (tag) getTagNamespace: ƒ getTagNamespace(tag) staticKeys: "staticClass,staticStyle" __proto__: ObjectとなっていてこれをObject.createでfinalOptionとする
そして、引数のoptionをfinalOptionとmerge
してbaseCompileへvar createCompiler = createCompilerCreator(function baseCompile ( template, options ) { var ast = parse(template.trim(), options); if (options.optimize !== false) { optimize(ast, options); } var code = generate(ast, options); return { ast: ast, render: code.render, staticRenderFns: code.staticRenderFns } });parse
function parse ( template, options ) { warn$2 = options.warn || baseWarn; platformIsPreTag = options.isPreTag || no; platformMustUseProp = options.mustUseProp || no; platformGetTagNamespace = options.getTagNamespace || no; var isReservedTag = options.isReservedTag || no; maybeComponent = function (el) { return !!el.component || !isReservedTag(el.tag); }; transforms = pluckModuleFunction(options.modules, 'transformNode'); preTransforms = pluckModuleFunction(options.modules, 'preTransformNode'); postTransforms = pluckModuleFunction(options.modules, 'postTransformNode'); delimiters = options.delimiters; var stack = []; var preserveWhitespace = options.preserveWhitespace !== false; var whitespaceOption = options.whitespace; var root; var currentParent; var inVPre = false; var inPre = false; var warned = false; function warnOnce (msg, range) { if (!warned) { warned = true; warn$2(msg, range); } } function closeElement (element) { trimEndingWhitespace(element); if (!inVPre && !element.processed) { element = processElement(element, options); } // tree management if (!stack.length && element !== root) { // allow root elements with v-if, v-else-if and v-else if (root.if && (element.elseif || element.else)) { { checkRootConstraints(element); } addIfCondition(root, { exp: element.elseif, block: element }); } else { warnOnce( "Component template should contain exactly one root element. " + "If you are using v-if on multiple elements, " + "use v-else-if to chain them instead.", { start: element.start } ); } } if (currentParent && !element.forbidden) { if (element.elseif || element.else) { processIfConditions(element, currentParent); } else { if (element.slotScope) { // scoped slot // keep it in the children list so that v-else(-if) conditions can // find it as the prev node. var name = element.slotTarget || '"default"' ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element; } currentParent.children.push(element); element.parent = currentParent; } } // final children cleanup // filter out scoped slots element.children = element.children.filter(function (c) { return !(c).slotScope; }); // remove trailing whitespace node again trimEndingWhitespace(element); // check pre state if (element.pre) { inVPre = false; } if (platformIsPreTag(element.tag)) { inPre = false; } // apply post-transforms for (var i = 0; i < postTransforms.length; i++) { postTransforms[i](element, options); } } function trimEndingWhitespace (el) { // remove trailing whitespace node if (!inPre) { var lastNode; while ( (lastNode = el.children[el.children.length - 1]) && lastNode.type === 3 && lastNode.text === ' ' ) { el.children.pop(); } } } function checkRootConstraints (el) { if (el.tag === 'slot' || el.tag === 'template') { warnOnce( "Cannot use <" + (el.tag) + "> as component root element because it may " + 'contain multiple nodes.', { start: el.start } ); } if (el.attrsMap.hasOwnProperty('v-for')) { warnOnce( 'Cannot use v-for on stateful component root element because ' + 'it renders multiple elements.', el.rawAttrsMap['v-for'] ); } } //optionがすごい長い parseHTML(template, { warn: warn$2, expectHTML: options.expectHTML, isUnaryTag: options.isUnaryTag, canBeLeftOpenTag: options.canBeLeftOpenTag, shouldDecodeNewlines: options.shouldDecodeNewlines, shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref, shouldKeepComment: options.comments, outputSourceRange: options.outputSourceRange, start: function start (tag, attrs, unary, start$1, end) { // check namespace. // inherit parent ns if there is one var ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag); // handle IE svg bug /* istanbul ignore if */ if (isIE && ns === 'svg') { attrs = guardIESVGBug(attrs); } var element = createASTElement(tag, attrs, currentParent); if (ns) { element.ns = ns; } { if (options.outputSourceRange) { element.start = start$1; element.end = end; element.rawAttrsMap = element.attrsList.reduce(function (cumulated, attr) { cumulated[attr.name] = attr; return cumulated }, {}); } attrs.forEach(function (attr) { if (invalidAttributeRE.test(attr.name)) { warn$2( "Invalid dynamic argument expression: attribute names cannot contain " + "spaces, quotes, <, >, / or =.", { start: attr.start + attr.name.indexOf("["), end: attr.start + attr.name.length } ); } }); } if (isForbiddenTag(element) && !isServerRendering()) { element.forbidden = true; warn$2( 'Templates should only be responsible for mapping the state to the ' + 'UI. Avoid placing tags with side-effects in your templates, such as ' + "<" + tag + ">" + ', as they will not be parsed.', { start: element.start } ); } // apply pre-transforms for (var i = 0; i < preTransforms.length; i++) { element = preTransforms[i](element, options) || element; } if (!inVPre) { processPre(element); if (element.pre) { inVPre = true; } } if (platformIsPreTag(element.tag)) { inPre = true; } if (inVPre) { processRawAttrs(element); } else if (!element.processed) { // structural directives processFor(element); processIf(element); processOnce(element); } if (!root) { root = element; { checkRootConstraints(root); } } if (!unary) { currentParent = element; stack.push(element); } else { closeElement(element); } }, end: function end (tag, start, end$1) { var element = stack[stack.length - 1]; // pop stack stack.length -= 1; currentParent = stack[stack.length - 1]; if (options.outputSourceRange) { element.end = end$1; } closeElement(element); }, chars: function chars (text, start, end) { if (!currentParent) { { if (text === template) { warnOnce( 'Component template requires a root element, rather than just text.', { start: start } ); } else if ((text = text.trim())) { warnOnce( ("text \"" + text + "\" outside root element will be ignored."), { start: start } ); } } return } // IE textarea placeholder bug /* istanbul ignore if */ if (isIE && currentParent.tag === 'textarea' && currentParent.attrsMap.placeholder === text ) { return } var children = currentParent.children; if (inPre || text.trim()) { text = isTextTag(currentParent) ? text : decodeHTMLCached(text); } else if (!children.length) { // remove the whitespace-only node right after an opening tag text = ''; } else if (whitespaceOption) { if (whitespaceOption === 'condense') { // in condense mode, remove the whitespace node if it contains // line break, otherwise condense to a single space text = lineBreakRE.test(text) ? '' : ' '; } else { text = ' '; } } else { text = preserveWhitespace ? ' ' : ''; } if (text) { if (!inPre && whitespaceOption === 'condense') { // condense consecutive whitespaces into single space text = text.replace(whitespaceRE$1, ' '); } var res; var child; if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) { child = { type: 2, expression: res.expression, tokens: res.tokens, text: text }; } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') { child = { type: 3, text: text }; } if (child) { if (options.outputSourceRange) { child.start = start; child.end = end; } children.push(child); } } }, comment: function comment (text, start, end) { // adding anyting as a sibling to the root node is forbidden // comments should still be allowed, but ignored if (currentParent) { var child = { type: 3, text: text, isComment: true }; if (options.outputSourceRange) { child.start = start; child.end = end; } currentParent.children.push(child); } } }); //parseのreturn return root }長いけど実際重要なのはparseHTML
parseHTML
ここでastを作っていきます、まずどのような処理が行われるかを説明します。
このように与えられたhtmlを切り取っていって徐々にastを生成していく感じです。
今回は三巡目までなので順番に見ていきましょう。parseHTML一巡目
function parseHTML (html, options) { var stack = []; var expectHTML = options.expectHTML; var isUnaryTag$$1 = options.isUnaryTag || no; var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no; var index = 0; var last, lastTag; while (html) { last = html; // Make sure we're not in a plaintext content element like script/style if (!lastTag || !isPlainTextElement(lastTag)) { var textEnd = html.indexOf('<'); if (textEnd === 0) { // Comment: if (comment.test(html)) { var commentEnd = html.indexOf('-->'); if (commentEnd >= 0) { if (options.shouldKeepComment) { options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3); } advance(commentEnd + 3); continue } } // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment if (conditionalComment.test(html)) { var conditionalEnd = html.indexOf(']>'); if (conditionalEnd >= 0) { advance(conditionalEnd + 2); continue } } // Doctype: var doctypeMatch = html.match(doctype); if (doctypeMatch) { advance(doctypeMatch[0].length); continue } // End tag: var endTagMatch = html.match(endTag); if (endTagMatch) { var curIndex = index; advance(endTagMatch[0].length); parseEndTag(endTagMatch[1], curIndex, index); continue } // Start tag: var startTagMatch = parseStartTag(); if (startTagMatch) { handleStartTag(startTagMatch); if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) { advance(1); } continue//ここで二巡目へ } }一巡目はtextEnd=0となるのでif (textEnd === 0)へ、html.matchは正規表現であてはまるものを探しています。当てはまらなかったのでStartTagの処理がされます。
html.match(startTagOpen)の結果
0: "<div" 1: "div" index: 0 input: "<div id="vue_example">↵ {{message}}↵</div>"でこれをつかってadvanceでhtml.substringでmatch↓分を切り取るとhtmlは
" id="vue_example"> {{message}} </div>"となる
whileでhtml.match(attribute)がmatchしてattrが
0: " id="vue_example"" 1: "id" 2: "=" 3: "vue_example" 4: undefined 5: undefined index: 0 input: " id="vue_example">↵ {{message}}↵</div>" groups: undefined start: 4 length: 6となる、attr.startはindexで<divの分advanceしたのでindexは4
" id="vue_example""分advanceで進めて、
htmlは"> {{message}} </div>"indexは21となる
whileループの最後でmatchにattrを入れる。
whileループでendにマッチしたらwhileを抜けて、endでもadvanceをする。
最終的にmatchはtagName: "div" attrs: Array(1) 0: (6) [" id="vue_example"", "id", "=", "vue_example", undefined, undefined, index: 0, input: " id="vue_example">↵ {{message}}↵</div>", groups: undefined, start: 4, end: 21] length: 1 __proto__: Array(0) start: 0 unarySlash: "" end: 22 __proto__: Objectとなり、これがstartTagMatchに入る
今度はhandleStartTagでhandleStartTagは返ってきたmatchを見やすい形に成形してあげることと、あとはstackにpushしてあげること、このpushしたやつはparseEndtagで使う
matchからattrに成形していて、option.startへ
option.startはparseHTMLのoptionで渡した奴createASTElement
function createASTElement ( tag, attrs, parent ) { return { type: 1, tag: tag, attrsList: attrs, attrsMap: makeAttrsMap(attrs), rawAttrsMap: {}, parent: parent, children: [] } }このmakeAttrsMapではattrsのname:id、value:"vue_example"をmapにして
id:vue_exampleとしている
startに戻ってelementにはcreateASTElementの返り値が入り、さらにstart,end,rawAttrsMapが追加される。
process~系はまたv-系列のところでやります。
最後にrootとcurrentElementが空なのでelementをその二つに入れて,startは終わり一巡目終了時
index=22
html" {{message}} </div>"これでhandleStartTagも終わり、ここまでで一巡目が終了
parseHTML二巡目
// Start tag: var startTagMatch = parseStartTag(); if (startTagMatch) { handleStartTag(startTagMatch); if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) { advance(1); } continue } } //ここから二巡目で使うところ var text = (void 0), rest = (void 0), next = (void 0); if (textEnd >= 0) { rest = html.slice(textEnd); while ( !endTag.test(rest) && !startTagOpen.test(rest) && !comment.test(rest) && !conditionalComment.test(rest) ) { // < in plain text, be forgiving and treat it as text next = rest.indexOf('<', 1); if (next < 0) { break } textEnd += next; rest = html.slice(textEnd); } text = html.substring(0, textEnd); } if (textEnd < 0) { text = html; } if (text) { advance(text.length); } if (options.chars && text) { options.chars(text, index - text.length, index); } }handleStartTagでlastTagはdivになっている。
textEndは{{message}}の<にヒットして15となり0ではないので
var text = (void 0), rest = (void 0), next = (void 0);のところから始める。
(void 0)というのはundefinedと同じだと思っていい、undefinedが上書きできるから(void 0)を使っていたらしいが、今はもうundefinedで大丈夫まずrestにhtml.substringでが入り、textには"
{{message}}
"が入るtextの分だけadvanceで進めて
index=37
html="</div>"
option.chars
charsは前巡startのcreateASTElementで作ったcurrentParent=elementにchildrenを作ることと、
parseTextでmountComponentようにtextを_s()みたいな形に成形してあげること
謎の_sとか_cは関数で、mountComponentで使われるので後でやりましょう。parseTextは"↵ {{message}}↵"を成形していく、まずmatch=tagRE.exec(text)で("↵ ")をtokenに入れる
matchは0: "{{message}}" 1: "message" index: 3 input: "↵ {{message}}↵" groups: undefined length: 2expression: ""\n "+_s(message)+"\n"" tokens: Array(3) 0: "↵ " 1: {@binding: "message"} 2: "↵" length: 3 __proto__: Array(0) __proto__: Objectとなる
parseFilterでは括弧とかブラケットとか処理しているんだけど今回は関係ないので飛ばす
charsに戻って、作ったchildをchildrenにpushしてcharsは終わり
ここまでで二巡目終了
二巡目終了時
index=37
html="</div>"
parseHTML三巡目
再びtextendが0なので一巡目と同じところへ
ただ、endTagにマッチする//End tag: var endTagMatch = html.match(endTag); if (endTagMatch) { var curIndex = index; advance(endTagMatch[0].length); parseEndTag(endTagMatch[1], curIndex, index); continue }advanceでindex=43、html=""となる
parseEndTag
parseEndTagではhandleStartTagでstackにpushしたやつを使って対応するtagをoption.endで処理していく、終わったらstackから取り除く
今回はdiv一組しかないので一回やって終わりend→closeElement→processElementとやっていく、processElementでattrをelementに追加することだけ今回は押さえておく
function parseEndTag (tagName, start, end) { var pos, lowerCasedTagName; if (start == null) { start = index; } if (end == null) { end = index; } // Find the closest opened tag of the same type if (tagName) { lowerCasedTagName = tagName.toLowerCase(); for (pos = stack.length - 1; pos >= 0; pos--) { if (stack[pos].lowerCasedTag === lowerCasedTagName) { break } } } else { // If no tag name is provided, clean shop pos = 0; } if (pos >= 0) { // Close all the open elements, up the stack for (var i = stack.length - 1; i >= pos; i--) { if (i > pos || !tagName && options.warn ) { options.warn( ("tag <" + (stack[i].tag) + "> has no matching end tag."), { start: stack[i].start, end: stack[i].end } ); } if (options.end) { options.end(stack[i].tag, start, end); } } // Remove the open elements from the stack stack.length = pos; lastTag = pos && stack[pos - 1].tag; } else if (lowerCasedTagName === 'br') { if (options.start) { options.start(tagName, [], true, start, end); } } else if (lowerCasedTagName === 'p') { if (options.start) { options.start(tagName, [], false, start, end); } if (options.end) { options.end(tagName, start, end); } } } }end: function end (tag, start, end$1) { var element = stack[stack.length - 1]; // pop stack stack.length -= 1; currentParent = stack[stack.length - 1]; if (options.outputSourceRange) { element.end = end$1; } closeElement(element); }function closeElement (element) { trimEndingWhitespace(element); if (!inVPre && !element.processed) { element = processElement(element, options); } // final children cleanup // filter out scoped slots element.children = element.children.filter(function (c) { return !(c).slotScope; }); // remove trailing whitespace node again trimEndingWhitespace(element); }processAttrはdirectiveがattrに含まれるか含まれないかで処理が変わる、今回は含まれないのでparseText
ただ今回はparseTextは何もしないので、単にaddAttr
elはtype: 1 tag: "div" attrsList: Array(1) 0: {name: "id", value: "vue_example", start: 5, end: 21} length: 1 __proto__: Array(0) attrsMap: {id: "vue_example"} rawAttrsMap: {id: {…}} parent: undefined children: [{…}] start: 0 end: 43 plain: false __proto__: Objectfunction processAttrs (el) { var list = el.attrsList; var i, l, name, rawName, value, modifiers, syncGen, isDynamic; for (i = 0, l = list.length; i < l; i++) { name = rawName = list[i].name; value = list[i].value; if (dirRE.test(name)) { } else { // literal attribute { var res = parseText(value, delimiters); if (res) { warn$2( name + "=\"" + value + "\": " + 'Interpolation inside attributes has been removed. ' + 'Use v-bind or the colon shorthand instead. For example, ' + 'instead of <div id="{{ val }}">, use <div :id="val">.', list[i] ); } } addAttr(el, name, JSON.stringify(value), list[i]); // #6887 firefox doesn't update muted state if set via attribute // even immediately after element creation if (!el.component && name === 'muted' && platformMustUseProp(el.tag, el.attrsMap.type, name)) { addProp(el, name, 'true', list[i]); } } } }これで三巡目も終わって、parseHTMLは終わります。
ここまでやって特に三巡目の操作は部分的なelementをいろいろいじっていただけで、本元のASTにどう影響していたか分かりづらかったんですが、一巡目のoption.startでroot=elementで、しかもそのelementをstackにpush、そのelementを三巡目で使っていたので結果としてrootをいじっていたということみたいです。。
そしてそのroot=elementがparseから返り,
baseCompileにもどってast=parse()でast=rootとなります。最終的なast=root
Object type: 1 tag: "div" attrsList: Array(1) 0: {name: "id", value: "vue_example", start: 5, end: 21} length: 1 __proto__: Array(0) attrsMap: {id: "vue_example"} rawAttrsMap: id: {name: "id", value: "vue_example", start: 5, end: 21} __proto__: Object parent: undefined children: Array(1) 0: {type: 2, expression: ""\n "+_s(message)+"\n"", tokens: Array(3), text: "↵ {{message}}↵", start: 22, …} length: 1 __proto__: Array(0) start: 0 end: 43 plain: false attrs: Array(1) 0: {name: "id", value: ""vue_example"", dynamic: undefined, start: 5, end: 21} length: 1 __proto__: Array(0) __proto__: ObjectここまででAST部分は終了です。次回はoptimazeを飛ばして、generateに入っていきたいと思います。
- 投稿日:2020-04-02T19:39:32+09:00
Vue.js+FirebaseでプリコネRの防御力計算機を作って公開する
概要
- Vue.jsとFirebaseを使って簡単なサービスを作ってみました。
- プリコネRの攻撃力と与ダメージから防御力を計算するツール。
- アプリ:https://r-tools-69dd3.web.app
ソースコード
index.html︙ <div class="calc"> <p>攻撃力:<input type="text" v-model="attack"></p> <p>ダメージ:<input type="text" v-model="damage"></p> <p>防御力:<input type="text" v-model="defense"></p> </div> ︙js/main.js(function(){ 'use strict'; let vm = new Vue({ el: '#app', data: { attack: 5000, damage: 5000, }, computed: { defense: { get: function () { return Math.round(100 * (this.attack / this.damage - 1)) }, set: function (defense) { this.damage = Math.round(this.attack / (1 + defense / 100)) } }, } }) }())アプリの公開
Firebase(無料枠)で公開しています。こちらの記事を参考にさせていただきました。
Vue.js + FirebaseでTodoアプリを作る参考文献
- 投稿日:2020-04-02T19:39:32+09:00
プリコネR 防御力計算機 (Vue.js + Firebase)
概要
- Vue.jsとFirebaseを使って簡単なサービスを作ってみました。
- プリコネRの攻撃力と与ダメージから防御力を計算するツール。
- リンク:https://r-tools-69dd3.web.app/
ソースコード
index.html︙ <div class="calc"> <p>攻撃力:<input type="text" v-model="attack"></p> <p>ダメージ:<input type="text" v-model="damage"></p> <p>防御力:<input type="text" v-model="defense"></p> </div> ︙js/main.js(function(){ 'use strict'; let vm = new Vue({ el: '#app', data: { attack: 5000, damage: 5000, }, computed: { defense: { get: function () { return Math.round(100 * (this.attack / this.damage - 1)) }, set: function (defense) { this.damage = Math.round(this.attack / (1 + defense / 100)) } }, } }) }())アプリの公開
Firebase(無料枠)で公開しています。こちらの記事を参考にさせていただきました。
Vue.js + FirebaseでTodoアプリを作る参考文献
- 投稿日:2020-04-02T19:39:32+09:00
プリコネR 防御力計算機 (Vue.js+Firebase)
概要
- Vue.jsとFirebaseを使って簡単なサービスを作ってみました。
- プリコネRの攻撃力と与ダメージから防御力を計算するツール。
- アプリ:https://r-tools-69dd3.web.app
ソースコード
index.html︙ <div class="calc"> <p>攻撃力:<input type="text" v-model="attack"></p> <p>ダメージ:<input type="text" v-model="damage"></p> <p>防御力:<input type="text" v-model="defense"></p> </div> ︙js/main.js(function(){ 'use strict'; let vm = new Vue({ el: '#app', data: { attack: 5000, damage: 5000, }, computed: { defense: { get: function () { return Math.round(100 * (this.attack / this.damage - 1)) }, set: function (defense) { this.damage = Math.round(this.attack / (1 + defense / 100)) } }, } }) }())アプリの公開
Firebase(無料枠)で公開しています。こちらの記事を参考にさせていただきました。
Vue.js + FirebaseでTodoアプリを作る参考文献
- 投稿日:2020-04-02T19:39:32+09:00
Vue.js+Firebaseで簡単なWebサービスを作って公開する
概要
- Vue.jsとFirebaseを使って簡単なサービスを作ってみました。
- プリコネRの攻撃力と与ダメージから防御力を計算するツール。
- アプリ:https://r-tools-69dd3.web.app
ソースコード
index.html︙ <div class="calc"> <p>攻撃力:<input type="text" v-model="attack"></p> <p>ダメージ:<input type="text" v-model="damage"></p> <p>防御力:<input type="text" v-model="defense"></p> </div> ︙js/main.js(function(){ 'use strict'; let vm = new Vue({ el: '#app', data: { attack: 5000, damage: 5000, }, computed: { defense: { get: function () { return Math.round(100 * (this.attack / this.damage - 1)) }, set: function (defense) { this.damage = Math.round(this.attack / (1 + defense / 100)) } }, } }) }())アプリの公開
Firebase(無料枠)で公開しています。こちらの記事を参考にさせていただきました。
Vue.js + FirebaseでTodoアプリを作る参考文献
- 投稿日:2020-04-02T18:44:41+09:00
[備忘録]Vue.jsのテスト
vueで任意の名前とランダムな背番号を表示
See the Pen vueで任意の名前とランダムな背番号を表示 by koji hirai (@koji_5) on CodePen.
- 投稿日:2020-04-02T17:40:41+09:00
Jest で Nuxt.js の Vue コンポーネントをテストする
環境
- Nuxt v2.9.2
- Node v10.15.2
- yarn 1.21.1
- TypeScript
- vue-property-decorator
テストに必要なもの
- 単体テストライブラリ
- テストランナー
セットアップ
- TODO: あとでかく。
input のテスト
コンポーネント
<label> <input type="text" :value="name" @input="$emit('input', $event.target.value)"> </label>import { Component, Prop, Vue } from 'vue-property-decorator' @Component() export default class MyInput extends Vue { @Prop({ default: '' }) name: string }テストファイル
今回は以下の内容をテストします。
- props の確認
- emit の確認
import MyInput from '@/components/MyInput.vue' import { shallowMount } from '@vue/test-utils' describe('MyInput.vue', () => { it('props の確認', () => { const wrapper = shallowMount(MyInput, { propsData: { name: 'shts' }, }) expect(wrapper.props().name).toMatch('shts') }) it('emit の確認', () => { const w = shallowMount(MyInput) w.find('input').setValue('shts') expect(w.emitted().input).toBeTruthy() expect(w.emitted().input[0]).toEqual(['shts']) }) })
- 投稿日:2020-04-02T17:05:59+09:00
【Vue.js】算出プロパティによる四則演算のサンプル
はじめに
- Vue.jsにおいては通常プロパティだけでなく、算出プロパティ(computed)も使用できます。
- 大した内容ではありませんが、四則演算のサンプルを作成しましたので、ソースコードを公開します。
算出プロパティ―
- 通常プロパティはmessageをjsで定義すると html中で{{message}}と参照できます。
var app= new Vue({ el: '#app', data: { message: 'Hello' }, })
- 算出プロパティは、computed内で関数で計算した結果をプロパティで定義できます。以下の場合、html中で{{add1}}と参照できます。
var app = new Vue({ el: '#app', data: { input1_1:0, input1_2:0, }, computed:{ add1: function() {return this.input1_1 + this.input1_2}, }, })四則演算のサンプル
- 上記を踏まえて、四則演算の算出プログラムを作成しましたので、ソースを提示します。
はまった点
- 一点はまった点としては、入力フォームを以下定義していたのですが、入力した数値を文字列と判定してしまいました。
- 例えば 「10 + 20」の計算結果が「1020」 となってしまいました
<!-- うまくいかない例(文字列結合となってしまった) --> <input type="number" v-model="input1_1">
- 公式サイトにも記載が有りますが、以下のようにv-model.numberと定義しなくてはいけませんでした。
<!-- うまくいった例 --> <input type="number" v-model.number="input1_1">サンプル
- 見栄えの為、bootstrap を入れています。
<html lang='ja'> <head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"> </head> <body> <div id="app"> <div class="col-sm12"> <label>足し算:</label><input type="number" v-model.number="input1_1"> + <input type="number" v-model.number="input1_2"> = <span>{{ add1 }}</span> </div> <div class="col-sm12"> <label>引き算:</label><input type="number" v-model.number="input2_1"> - <input type="number" v-model.number="input2_2"> = <span>{{ min1 }}</span> </div> <div class="col-sm12"> <label>掛け算:</label><input type="number" v-model.number="input3_1"> × <input type="number" v-model.number="input3_2"> = <span>{{ multi1 }}</span> </div> <div class="col-sm12"> <label>割り算:</label><input type="number" v-model.number="input4_1"> × <input type="number" v-model.number="input4_2"> = <span>{{ divis1 }}</span> </div> </div> <script> var app = new Vue({ el: '#app', data: { input1_1:0, input1_2:0, input2_1:0, input2_2:0, input3_1:0, input3_2:0, input4_1:0, input4_2:0, }, computed:{ add1: function() {return this.input1_1 + this.input1_2}, min1: function() {return this.input2_1 - this.input2_2}, multi1: function() {return this.input3_1 * this.input3_2}, divis1: function() {return this.input4_1 / this.input4_2}, }, }) </script> </body> </html>出力結果
- 投稿日:2020-04-02T16:37:48+09:00
Cloud Firestoreの時系列データをChart.jsでグラフ化するには?
はじめに
Firestoreに貯まっていく時系列データをWeb上で視覚化させるために、
Chart.jsを使用してグラフを作成させてみました。
本記事は、それを実現する上で自分にとってポイントとなった点をピックアップし、
備忘録としてまとめております。FirestoreとChart.js、いずれも使ったことないよという方が、
「あ、こんなこと出来るんだね」と知見や興味を増やすことが出来るきっかけとなれば幸いです。環境
- Ubuntu 18.04.3
- Node.js : v12.16
- vue@2.6.11
- firebase@7.13.1
- chart.js@2.9.3
- chartjs-plugin-streaming@1.8.0
- vue-chartjs@3.5.0
- moment@2.24.0
関連リンク
* Node.js
* Firebase CLI
* Chart.js
* Moment.js結論:どんなグラフが出来たのか?
グラフのデータは説明用のサンプルです。
Firestoreに入っている温度と日付のデータをもとに、ブラウザ上でグラフが視覚化されている状態です。Firestoreにデータを入れてみる
FirestoreはNoSQLですので、柔軟なデータ構造が可能です。
ここでは「試しに手動で入れるとどんな感じなのか?」という確認と、
「ソースコードからデータを入れる場合は?」という2つのやり方を見てみましょう。コンソールから手動で追加する場合
上の画像は、コンソールから手動でデータを入力している様子です。
Firestoreでは、以下のような構成でデータが保管されます。
*コレクション
- データを入れるフォルダのようなもの
*ドキュメント
- フォルダに入った書類のようなもの
*データ
- ドキュメントに載せる内容の数々図では、事前にコレクションを作成しています。
はじめに何かしらデータを入れることを求められ、上図のような画面が出てきます。今回は「室温の変化」を例に、温度と時間を入れるフィールドを用意しました。
具体的には次のとおりです。
*Temp
: 温度。タイプはnumber
で数値。
*DateTime
: 日時。タイプはtimestamp
で日時。タイプの詳細については、以下の公式のページを参照して下さい。
サポートされているデータ型ソースコードからデータを追加する場合
今回、私はNuxt.jsの環境で作成しました。
公式ページでは様々な言語でのデータ追加方法について記載されていますので、
もし参考にされる場合はお使いの言語に置き換えていただければと思います。準備:Firebaseプロジェクトでアプリを作成しておく
まずはアプリをFirebaseに追加しておく必要があります。
アプリはプロジェクト概要の歯車アイコンから、「プロジェクトを設定」を選択します。
アプリを追加し、今回はWebを選択します。
アプリの名前は、適当な名前をつけます。
ここでの名前はコンソール上で見られるだけの見出しにすぎないので、何でもOKです。これらを使用するので、ソースコードの中に放り込んでおいて下さい。
ちなみに今すぐこの画面でメモしなくても、いつでも確認は可能です。データ追加までの準備
まずは
index.vue
から。
ここにごちゃごちゃ書くのを避けておきます。index.vue<template> <chart/> </template> <script> import chart from '~/components/chart.vue' export default { components: { chart } } </script>続いて
chart.vue
というグラフを描画するコードを用意。
<canvas>
内にグラフを描画します。chart.vue<template> <div> <canvas id="myChart" width="800px" height="600px"></canvas> </div> </template>必要なライブラリをインポートし、先程のスクリプトをそのまま貼り付けます。
chart.vue<script> import { Line } from "vue-chartjs"; import "chartjs-plugin-streaming"; const firebase = require("firebase/app"); import "firebase/firestore"; import { firestore } from 'firebase'; import moment from "moment"; var firebaseConfig = { apiKey: "***************************************", authDomain: "********************.firebaseapp.com", databaseURL: "https://********************.firebaseio.com", projectId: "********************", storageBucket: "********************.appspot.com", messagingSenderId: "**************", appId: "*****************************************" };Firebaseアプリインスタンスを作成し、初期化します。
実はFirebase App named '[DEFAULT]' already exists
というエラーにかなりはまってしまいました。
どうやらfirebase.apps.length
が初回は0
なのですが、修正して更新かけると1
になり、もうinitializeAppする必要がないようです。chart.vueif (!firebase.apps.length) { firebase.initializeApp(firebaseConfig); } else { firebase.app(); } let db = firebase.firestore();データを追加する
いよいよデータをFirestoreに追加してみましょう。
ソースは先程の続きからです。ここでは
setData()
というデータを追加する関数を作ってみましょう。chart.vueexport default { extends: Line, mounted() { let fireStoreDB = db.collection("【コレクション名】"); function setData() { let temp = 20.2; let stringDate = '2020/04/01'; let stringTime = '13:00:00'; let date = stringDate + " " + stringTime; fireStoreDB.doc().set({ Temp: temp, DateTime: firestore.Timestamp.fromDate(new Date(date)) }); }上記のように、
{ "フィールド名": "値", "フィールド名": "値" }という形式でデータを追加できることが分かりますね。
ソース内ではまだsetData()
を呼び出していませんが、
実行されると以下のようにFirestoreの指定したコレクション内に格納されます。データを読み込む
続いてはFirestoreのデータを読み込む関数を作ってみましょう。
chart.vuefunction getData() { let queryInfo = fireStoreDB.orderBy('DateTime'); queryInfo.get().then(snapshot => { snapshot.docs.forEach(doc => { let item = doc.data(); let time = item.DateTime; let temp = item.Temp; let datetime; if (time !== undefined && temp !== undefined) { datetime = new Date(time * 1000); dataArr = { time: datetime, temp: temp }; addData(myChart, dataArr.time, dataArr.temp); } }); }); }ポイントは、
orderBy('DateTime')
とつけることで、DateTimeでソートしているという点です。
これがないと、ドキュメントID順で読むため、以下のようなヘンテコな時系列のグラフになってしまうので注意です。
addData()
関数で、Firestoreから取得したデータをグラフに反映させていきます。
この関数については後述します。グラフの設定を決める
続いて
Chart.js
に関する部分です。chart.vuelet ctx = document.getElementById("myChart").getContext("2d"); let config = { type: "line", // 折れ線グラフ data: { datasets: [ { label: "Temp", // ラベル名 data: dataArr.temp, // グラフにしたいデータ backgroundColor: "rgba(50,50,255,0.1)", // グラフの背景色 borderColor: "rgba(50,50,255,1)", // グラフの線の色 fill: true, // グラフの背景色を塗りつぶし方("top"にすると反転したり) lineTension: 0.4, // ベジェ曲線の度合い // (0.5超えてくると不自然なラインに...) } ] }, options: { title: { display: true, // グラフのタイトルを表示する text: "室温データ", // グラフのタイトル fontSize: 30 // タイトルのフォントサイズ }, scales: { yAxes: [ // Y軸の設定 { ticks: { min: 17, // 値の表示範囲(下限) max: 22 // 値の表示範囲(上限) }, // ticksは省略するとデータ内容に応じて自動で範囲を決めてくれます scaleLabel: { display: true, // 凡例の表示 labelString: "Temp", // 凡例の名前 fontSize: 15 // 凡例のフォントサイズ } } ] }, } };この辺のカスタマイズは公式ドキュメントを読んだほうが詳しく幸せになれますのでご覧ください。
グラフにデータを追加する
最後は先述した
addData()
の中身です。chart.vuelet myChart = new Chart(ctx, config); function addData(chart, time, temp) { console.log(time, temp); moment_time = moment(time, "x").format("MM/DD HH:mm:ss"); chart.data.labels.push(moment_time); chart.data.datasets.forEach(dataset => { dataset.data.push({ x: time, y: temp }); }); chart.update(); } getData(); setData(); // Firestoreにデータを追加する場合のみ呼び出しましょう } }; </script>ポイントは、X軸のラベル名を作成する所です。
Moment.jsを使用すると、時間の表記方法を簡単に設定できます。
あくまでもグラフのラベルのために処理している点に注意して下さい。クエリの方法を変える
上記で紹介した方法ですと、
DateTime
の全データを見に行きますので、
データが多ければ多いほどFirestoreの読み取り量が増加します。
Firestoreの無料枠は、2020/4/1現在で1日50,000までとなっております。ここでは自分が試したほんの数例を紹介するまでですが、さらに色々知りたい方は公式ドキュメントを読んでみましょう。
1時間以内のデータのみを取得したい
今から1時間以内のデータのみ取得したい場合、私は以下のようにしてみました。
/* 今の日時を用意して */ let now = new Date(); /* Moment.jsを使って1時間前を計算して */ now = moment(now).subtract(1, 'h'); now = new Date(now); /* .whereで条件を追加する */ let queryInfo = fireStoreDB.where('DateTime', '>', now).orderBy('DateTime');
moment(指定した時間).subtract(引きたい時間, 単位)
というように、
Moment.jsにお世話になると時間計算も簡単ですね。ちなみに引き算ではなく足し算の場合は、
moment(指定した時間).add(足したい時間, 単位)
となります。
お約束の流れですが詳細は公式ドキュメントをどうぞ。指定した件数のみを取得したい
取得するデータの個数を制限するのは非常に簡単です。
/* .limit()を追加して件数を制限 */ let queryInfo = fireStoreDB.orderBy('DateTime').limit(7);ちなみにこれでグラフはどのようになったかというと、以下のとおりです。
.orderBy('DateTime')
で時系列順になったデータの頭から7件を拾っていることが分かりますね。
ただ、このままですと最新の7件にはなっておりません。ではどうすれば良いか?
以下のように変更すれば可能になります。
/* .limitToLast()を追加して件数を制限 */ let queryInfo = fireStoreDB.orderBy('DateTime').limitToLast(7);これで無事最新の7件が時系列順に取得できました。
おわりに
Firestoreは柔軟にデータが格納でき、それを本記事のように視覚化させてデプロイするだけで、
簡単にWeb上でそれらを共有できることが分かりました。
(いずれも触ったことなく形になるまで時間は要しましたが)Chart.jsの部分だけでも、簡単に美しいグラフが描けることが伝わったのではないかと思います。
どうか皆さんのデータの視覚化に、少しでもお役に立てることを心より願っております。
- 投稿日:2020-04-02T16:33:34+09:00
Vue.js初心者がVuesax+Netlify使ってポートフォリオ作ってみた全記録
はじめに
Vue.jsを用いたポートフォリオサイト制作に挑戦してみました。
また今回は、デザイン疎いなりに『かっこいいポートフォリオ』作るぞ!!ってことでVuesaxも使用してみました。
この記事は、制作過程の記録になります。とても長文かつ制作しながら書いているのでまとまりもあまりありません。(すみません)
ですのでとりあえず完成品だけチェックしたいって方は以下のリンクから見て見て下さい。
https://masaru-portfolio.netlify.com/
スマホからですとCSSアニメーションに伴い型崩れが起きますが(要改修部分)、Vuesaxで比較的簡単にかっこいいデザインを実装することができました。
同じくプログラミング初心者の方々やVuesaxなりなんらかのUIフレームワークを使ってみたいといった方々の参考になれば幸いです。※以下一部抜粋。
ライブラリの選定
どのライブラリを使う??
Bootstrap
すぐ取り入れられそう。Vue.js + Bootstrap4でポートフォリオサイトの雛形を作ろう!
MDBootStrap
ちょっとめんどくさそう??Material Design for Bootstrap 4 (Vue version)を導入してみる
Vuetify
とりあえずかっこいい。
Vueでもっと幸せになりたいあなたへ。VueのUIコンポーネントライブラリVuetifyのススメ
【vue.js】Vuefityをマスターする(1)
Vuesax
かっこいいけど手軽に始められそう??VuetifyかVuesaxを利用してみたいが、良し悪しがいまいちわからないのでとりあえずフォルダをコピーして(予備)、、、
手軽でかっこ良さそうなVuesaxを選定。
Vuesaxを使ってみた
インストール
$ npm install vuesax
マテリアルアイコンもインストール
$ npm install material-icons --save
main.jsへ追記。これでもう使えるらしい。
import Vue from 'vue' import App from './App' import router from './router.js' + import Vuesax from 'vuesax' + import 'vuesax/dist/vuesax.css' + import 'material-icons/iconfont/material-icons.css'
- Vue.use(Vuesax)
Vue.config.productionTip = false
とりあえず公式ドキュメント見ながら見よう見まねで使ってみる。(英文が嫌で今まで公式ドキュメント系避けてきた。ちゃんと読むのは今回が初。褒め称えたい。(遅すぎ))https://lusaxweb.github.io/vuesax/components/alert.html#default
Home.vueを編集
<template> <div id="top"> <p>WELCOME TO MY PAGE</p> + <vs-alert active="true"> <h1>Sato Masaru's Portfolio Site</h1> + </vs-alert> <img src="@/image/profile.jpg" width=350px> <p>HTML/CSS(Bootstrap)/JavaScript(jQuery,Vue.js)/PHP(Laravel)</p> <p>Check out some of my works.</p> </div> </template>
反映された!!うれしい◎アイコンを付けてみた
こんな感じでボタンタグにicon属性を付けてあげるだけでOK。
<vs-button :color="colorx" :gradient-color-secondary="colorx2" type="gradient" icon="home">home</vs-button>
表示された。とても簡単。(ボタンもついでにグラデーション付きで実装。)以下がアイコンリスト。沢山ありすぎて沼です。
https://material.io/resources/icons/?icon=face&style=baseline
CSSデザイン(フルスクリーン/アニメーション/レスポンシブ対応)
フルスクリーンレイアウト
レスポンシブにも対応・CSSでフルスクリーンレイアウト上記とりあえず見よう見まねで実装。
アニメーション関連
画像を徐々に表示する CSS アニメーション画面遷移時のアニメーションは上記のサイトを参考に実装。スクロールでの表示にはJSが必要。
今回は実装しなかったけど別の機会に試してみたい。
Vueでimgタグのsrcを書き換えて、transitionアニメーションさせる
アニメーションが気持ちいい!コピペで実装できる最新HTML/CSSスニペットまとめ
JavaScript不要!CSSだけでアニメーションを作る方法
router-link
では、アクティブなリンクに対してrouter-link-active
とrouter-link-exact-active
クラスを付与するらしい。以下でもいけるらしい。が、いまいちわからず。
<transition> <router-view></router-view> </transition>
意外と知られていないCSSの色々な回転アニメーションの作成方法上記参考にアイコン画像を回転させてみた◎
CSSのみで実装できる、画像と相性が良さそうなホバーエフェクト 15
cssだけ!画像をhoverしたときの簡単おしゃれエフェクト4つ
上記参考にプロフィール写真にホバーエフェクト付ける。
現状こんな感じ。前回よりだいぶ良くなりそうな予感。
レスポンシブ対応
Googleもメディアクエリを使わない?この記事が興味深かったけどとりあえずメディアクエリを導入。
てかBootstrapをVuesaxに加えて使うのもアリなのかな??と思ったり。。。
App.vueの<style>タグに追記
@media (max-width: 768px) { p { font-size: 8px; } }
反映された。(画像下の文字が小さくなった)ヘッダー部分が崩れているので改修。
サイドバーつけるのもいいかなと思ったけど、これでもいいかな??なんだろこれ的な好奇心で押してもらうことを狙った。
(後付け)
コンテンツ制作
Home
ここ辺りで、さすがにコンテンツのしょぼさが目立ち始めたので作り出す。というより何を書くか決めていなかったので、vuesaxでどれを使うか選べなくなってしまった。
とりあえず『かっこいいポートフォリオサイト』というテーマだけは決めていたので、トップに英語の文章を加える。
英語は苦手なのでGoogle翻訳で。ダサすぎる。。。笑笑
微調整しつつこんな感じで。(PC版がだいぶ素朴だけどまあ良しとしよう。)
About
最初入れようとしていたけど、Homeで自分語りはしてしまったので要らない説。ってことでなしにしました。
Work
ここにポートフォリオを入れることにしました。
Skill
aboutなしになってちょっと寂しかったので作ることにしました。
Contact
Googleフォームの埋め込みとかも検討したがとりあえずTwitterだけ貼っておくっことにする。
Vuesaxを使ってみた パート2
構成はなんとなく練れたのでさっそくWork.vueでポートフォリオの紹介ページの製作へ。
Work
制作物の紹介に合いそうなコンポーネントを探して。。。https://lusaxweb.github.io/vuesax/components/card.html#default
制作物の紹介にはCardコンポーネントがちょうどよさそうだったので使ってみた。
はみ出しました。。。笑 が一応反映。
その後、以下を参考に。
https://lusaxweb.github.io/vuesax/layout/
上記のグリッドを参考に、vs-w="6"をvs-w="3"にしてみるといい感じになりました。
Bootstrapと似た仕組みですね。
コンテンツ追加して少し微調整でこんな感じになりました。
余白が気になるけどまあいいでしょう。笑
がしかし、レスポンシブで思いっきり型崩れ。。。(当たり前)
とりあえずよくある縦にそろえる感じで調整。
。。。
ここで気づく。全体を囲っているボーダーラインをApp.vue側で作っているので枠を消せない。。。omg
(たまに見かけるボーダーラインが消えるようなアニメーションはどうやっているんだろう??)
今回はVuesaxで気になっていたTagコンポーネントを使用して解決しました。
https://lusaxweb.github.io/vuesax/components/tabs.html#alignments
アイコンも使えて、Flexで幅一杯にタグを広げることもできました。我ながら良い感じ。
Skills
最初はTableコンポーネントを使おうとしたけど少々オーバースペック&幅の調整の仕方がわからなかったのでやめる。https://lusaxweb.github.io/vuesax/components/table.html#state
ついでにCollapseコンポーネントを使ってみたけど書く内容が少ないので見え隠れするのが逆に煩わしいと感じてやめる。
https://lusaxweb.github.io/vuesax/components/collapse.html#default
最終的にListコンポーネントに落ち着きました。
https://lusaxweb.github.io/vuesax/components/list.html
シンプルイズベスト。
Contact
よしラスト!!(若干の息切れ感。。。笑)残念ながらVuesaxにはTwitterアイコンはなかったので以下を参考に、Font awesomeを使うことにする。
公式サイトと照らし合わせて必要なものだけインストールしれば使えるみたい。
$ npm install --save @fortawesome/fontawesome-svg-core $ npm install --save @fortawesome/free-solid-svg-icons $ npm install --save @fortawesome/vue-fontawesome $ npm install --save @fortawesome/free-regular-svg-icons $ npm install --save @fortawesome/free-brands-svg-icons #Twitterアイコン使いたいならこれっぽい
main.jsに以下を追記
import { library } from '@fortawesome/fontawesome-svg-core' import { fas } from '@fortawesome/free-solid-svg-icons' //fasで全アイコンを追加できる。 import { fab } from '@fortawesome/free-brands-svg-icons' import { far } from '@fortawesome/free-regular-svg-icons' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' library.add(fas, far, fab) Vue.component('font-awesome-icon', FontAwesomeIcon)
あとは以下の公式サイトから使いたいアイコンを探せばOKhttps://fontawesome.com/icons?d=gallery
ちなみにTwitterはfasではないので書き方が違うらしい。
Vue CLIの中でfont awesomeを使いたいのですが、fabが反映されません・・・
コチラを参考に以下のように記入。
<font-awesome-icon :icon="['fab', 'twitter']" />
無事表示されました◎さらにここでPopupコンポーネントを使ってみました。
https://lusaxweb.github.io/vuesax/components/popup.html
以下でTwitterの埋め込みを作成して。。。<script>タグはindex.htmlに記載しました。おそらく邪道。。。
どん。いい感じ!!
と思いきやここでエラー。残念ながら/Contactでリロードすれば読み込むけど、localhostで読み込んだ際は以下の表示。
<script>タグをindex.htmlに埋め込んでいることが原因なのは容易に想像つく。
ただ、コンポーネントで以下のスクリプトを書き換える方法がわからず。。。
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
以下の記事も試してみたが解決には至らなかった。。。User Timeline - ユーザータイムラインをウェブサイトに埋め込む
一旦これはステイ。asyncなりちゃんと別途学習しよう。
デプロイ
Xserverへのデプロイ
もはやコンテンツ制作はモチベーションの問題で、半分力尽きたのでとりあえずデプロイを試みる。この日のためにいろいろとテストしていたので準備万全。
https://chobimusic.com/vue-cli_deploy/
いざ表示。router.js管理下のviewが一切表示されていない。。。
とりあえずhomeボタンを押してみると、目を覆いたくなる現実。アイコンとイメージも表示されていない。。。号泣
その後、Xserverに上げたdistフォルダの名前をルーティングに合わせてHomeにしたところ、画像が表示されました。
が、ページ遷移すると画像は再度表示されなくなる現象。
念のため恐る恐るスマホからも確認。
こちらは想像を更に超えるひどさでもはや笑うしかありませんでした。。。笑
仮想環境で見ていたフォントと全然違う。。。笑笑
そのあと少しもがくもこれといった改善策が出ず断念。。。
おまけにページ遷移後にリロードすると404エラーになることが発覚。。。泣
Netifyを使ってみた
さすがにここまで作ったのでデプロイを諦めるわけにはいかず、他のデプロイサーバーを探す。Vue.jsで作ったWebアプリをGitHubで管理してS3に自動デプロイする
ちょっと手間がかかりそうだ。GitHubとS3と使ったことがあまりないのもあり他探す。。。
Herokuは大いにあり!!ただ気になるサーバーが元からあったのでそちらの記事を探す。
(初めからそれ調べてやれ)
vue-cliでwebアプリケーションを作って、Netlifyを使って無料で爆速でリリースした話
はい。Netify。とても簡単そうなのでとりあえず試してみる。
以下からGitHub認証を使ってサインイン。
distファイルをZip形式にしてドラッグ&ドロップするだけ。3分程待ち、表示されたリンクをクリックしたらすぐ表示されました◎めちゃ簡単!!そして超早い!!
画像が表示&ページ遷移しても表示されるので画像問題はクリア◎(Xserverではなぜだめだったのだろうか。。。)
ただ残念なことにVuesaxのアイコンが表示されず。。。
改修作業へ
①Vuesaxのアイコンが表示されない
https://lusaxweb.github.io/vuesax/theme/icon.html公式ページを確認。
(お決まりのGoogle翻訳。)これ見てFont AwesomeのTwitterアイコンは問題なく動作してるしFont Awesomeでよくない??となっちゃいました。
なので全てFont Awesomeへ置き換え。
以下の感じ
ちなみにサイズの変更もできました。以下に載っています。
https://github.com/FortAwesome/vue-fontawesome
困ったこと。以下のようにVuesaxのコンポーネントタグ内にiconを書いていたケースでは、うまくアイコンを文字に続けて連結させることができませんでした。
<vs-tab label="Blog" icon="public"> <font-awesome-icon icon="plane" size="lg"/> #これ表示させたいアイコン
VuesaxはHTMLを自動生成してくれるので、Font Awesomeで同じ仕様にしたいならVuesax使わないで1から作ることになるのでやめました。残念。
②Twitterの埋め込みができない問題
.jsファイルの読み込み、async/awakeの理解がまだ疎いため、今後勉強することを胸に誓い以下のように変えました。Popupコンポーネントに画像を挿入できることがわかっただけ収穫ありということにしておきます。
②リロードした際に404エラーになる
これは以下の記事がとても参考になりそう。SPAにありがちな更新処理を行うと404になっちゃう問題(VueとHeroku版)
今回はNetifyに上げる予定なのでまた次回に回します。
(すみません、力尽きました。。。)
③フォント修正
少しだけ調べました。2020年流行?font-familyのオススメ設定はこれ!【CSS】
すみません、とりあえず上記で最後に記載されているおすすめフォントをコピペしました。。。
Vuesaxでデザインを簡単に作りたかったのに、<style>タグ内を拘り始めるのは本末転倒な気がしてしまいまして。。。
(言い訳&力尽きてる)ってことで以下の表示になりました。そこそこ良い感じ??
問題は、本番環境に上げたときの表示。Safariから見たらどうなるかです。。。
お、おもい。。。。画像がなかなか表示されない。
そしてなんか左上にずれる。
なぜかタップすると直る。笑 でも仮想環境に比べて下にずれてはみ出てる。
(原因が無知なのでわからない。もはやだるい)フォントはわりとおしゃれなので及第点です。
ここから再修正
重いのでトップ画のホバーエフェクトはなくす。レスポンシブ側のみh1のサイズを縮小。
スマホでのタブバーが表示される分を考慮できていないことに気付き、ハゲ(私)のプロフィール画像の主張を少し控えめにして、かつ囲いのボーダーラインの位置をずらしました。いろいろ試しましたが上の余白は作んなくてもそんなに気にならない。それ以上に下のタブバーの干渉が強いように感じました。
再表示。囲いの幅は及第点◎
変わらず以下のような型崩れは起きている。。。
が、どこか画面をタッチすれば直るので今回は一旦見過ごすことにする。
アニメーションを外したところ直ったのでこれが干渉しているっぽいが、ないと『かっこいいポートフォリオ』という根底のテーマから覆されてしまうので残すことにした。
navバーからの高さも微調整してデプロイ。とりあえずこれで完成にします。パチパチ。
以下、完成品。
https://vigorous-brahmagupta-3ae1cb.netlify.com/
おわりに
Vuesaxを使うことで、かなり簡単に動きのあるデザイン、かっこいいデザインを実装することができました。次はVuetifyを使ってみるのもアリかなと思いながら、そもそものCSSやJavaScript(Vue.js)の基礎が全然身についていないなと実感したので、基本に立ち返り、再度書籍なり公式ドキュメントを利用しながら学習を進めていこうと思います。
また、今回あまりリファレンスのない中、英文の公式ドキュメントを見ながら実装する経験ができたのは大きかったかなと思います。やれば多少はできるんだと実感しました。
次はLaravel+Vue.jsのポートフォリオを作りたいなと思う次第です。
おそらくこんな長文を最後まで見る方はいないかと思いますが、個人的備忘録として残しておきます。(じぶんも修正のための読み返しすらしたくなかった。。。笑笑)
最後にデザイン面で参考にさせていただきましたサイトを別の記事で紹介させていただきます。読んでいただきありがとうございました。
https://chobimusic.com/portfolio_summary/
引き続き頑張るぞ!!!
その他備忘録
画像が重かった件
こりゃ重くなるわってくらい1つだけ(私のでこ光ハゲ画像)容量がでかかったのでサイズダウンさせて解決しました。
config/index.htmlに関して
config/index.htmlの以下の修正箇所。
- assetsPublicPath: '/', + assetsPublicPath: '',
デプロイ時推奨の部分ですがこの状態でVue Routerによるページ遷移先(URLが変わっている状態)で更新ボタン押すとページ表示できなくなりました。。。cannot get / vue と表示されてしまう。(テスト環境において)
さらにここに関しては、再度npm run serveしないと変更が反映されないみたい。。
本番環境においては両方試しにデプロイしたけど、今回は特に変化は見られなかったのでデフォルトに戻して作業しました。
またデプロイして気付いたけどVue Routerの遷移先のURLは頭文字が必ず大文字になるっぽい。デプロイ時はディレクトリ名に気を付けないと。
Xserverでのデプロイ対策
Netifyにアップロードした後に気付いて試したことをここに書きます。以下のように、WordPressドメイン直下(public_html)に更にディレクトリを作って表示してました。
chobimusic/public_html/dist/index.html
例えばこれをWordPressのない純粋なドメイン直下にアップデートしたらどうなるかも試してみました。(以下の構造)
ドメイン/public_html/index.html
なんと遷移後も画像が表示されました!!(遷移後のリロードはNG)
また、NetifyとXserverへのFTP送信ですが、Netifyの方が若干表示するまでの時間が短縮できました。こればっかりはNetify凄すぎ!!と思いました。
XSERVER(エックスサーバー)にNode.jsをインストールするときはnodebrew使うと楽
Herokuでのデプロイを確認してて思ったけどXserverにNode.jsインストールすればいけるのかな??とか思ったけどSSH接続だしちょっと大変そう。。。メモ。
バグ
最初にページを読み込む際に<nav>の型崩れ(高さが2倍??)が起きてしまう。
ページ遷移すると治るので何が原因かと思いきや、リロードの際に一瞬ボタンが2段になって読み込まれていることが判明。(なぜかはわからない)
色々弄っていたら解決できたけどなぜ解決できたかもよくわからず。。。笑
CSS関連(躓いたところ)
【HTML】aタグで作ったリンクのクリック範囲を親要素のサイズに広げる方法【CSS】<router-link>タグの高さを親要素<nav>にそろえる際に参考にした。
Google Fonts
【2019年版】Google Fontsの使い方:初心者向けに解説!
今後実装したいこと
トップ画像のエフェクト
canvas⇒SVG
CSS3とHTML5 Canvasで作るモーショングラフィックCanvasを使った動きのあるWebサイト・ホームページ制作をご希望の方へ (Canvasを使ったサイトがまとめられている)
Vue.js with canvas Vueコンポーネントへの実装に難易度の高さを感じる。
グリグリ動くUIをVueとSVGでサクッと書く canvasよりSVGで実装してみようかな??
編集可能な SVG アイコンシステム(公式)CSS・SVGとVue.jsでのアニメーション作成入門 SVGもなんだかんだ難しそう。。。
- 投稿日:2020-04-02T16:00:35+09:00
Vue Internals①astを中心に 1-1compile前まで
はじめに
Vueを使ってはいるけど実際どんな操作が行われているかわからない・・・
なので、実際にソースを読んでみることにしました。
静的にソースを読むのではなく、今回はchromeのデバック機能を使って動的に解析していきたいと思います。
動的に見ていく利点としては実際の変数が入った状態で見れるので何をしているかわかりやすい、誤った道筋に飛ばないといった感じです。
今回使っていくcodeは上記の画像にある通りのシンプルな奴です、ただシンプルとはいっても処理はとても長いのでやはりソースコードを読むうえでは気になる処理に絞って想像がつく処理はある程度雑に読んでいった方がいい気がします。
ただ、記事にするうえで雑にやってしまうと分かりづらい気がするのでこの記事では割と丁寧にやっていきたいと思います。VueInternalシリーズの大目標としては
①{{message}}はどうやって処理されてる?
②v-系がどう処理されているか
③watcherとかcomputedとか
④diffアルゴリズムについて
の4つを理解することとします。1.全体の流れでは{{message}}はどうやって処理されてる?の謎が解けます。
2.v-系の処理を見るではv-系がどう処理されているか、
3.watcherやらdepやらについてではwatcher周りが、
4.diffアルゴリズムではdiffアルゴリズムについてを目標としてみていきます。全体の流れ
compile部分がとても長いです。この図の中の大部分がここのast生成部です。
ast部分で"<div id="vue_example"> {{message}} </div>"を
Object type: 1 tag: "div" attrsList: Array(1) 0: {name: "id", value: "vue_example", start: 5, end: 21} length: 1 __proto__: Array(0) attrsMap: {id: "vue_example"} rawAttrsMap: id: {name: "id", value: "vue_example", start: 5, end: 21} __proto__: Object parent: undefined children: Array(1) 0: {type: 2, expression: ""\n "+_s(message)+"\n"", tokens: Array(3), text: "↵ {{message}}↵", start: 22, …} length: 1 __proto__: Array(0) start: 0 end: 43 plain: false attrs: Array(1) 0: {name: "id", value: ""vue_example"", dynamic: undefined, start: 5, end: 21} length: 1 __proto__: Array(0) __proto__: Objectみたいにobjectにして、generateで
{render: "with(this){return _c('div',{attrs:{"id":"vue_example"}},[_v("\n "+_s(message)+"\n")])}"renderFunctionを作り、最後にこの関数を使ってmountComponentでrealDomを作ります。
抑えるところとしてはast生成、generate、最後のmountComponentが重要になっていきます。
new Vue~compile到達まで
function Vue (options) { //optionsの中身は el: "#vue_example" data: message: "Hello Vue.js!"// if (!(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword'); } this._init(options); }function initMixin (Vue) { Vue.prototype._init = function (options) { var vm = this; // a uid vm._uid = uid$3++; var startTag, endTag; /* istanbul ignore if */ if (config.performance && mark) { startTag = "vue-perf-start:" + (vm._uid); endTag = "vue-perf-end:" + (vm._uid); mark(startTag); } // a flag to avoid this being observed vm._isVue = true; // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options); } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ); } /* istanbul ignore else */ { initProxy(vm); } // expose real self vm._self = vm; initLifecycle(vm); initEvents(vm); initRender(vm); callHook(vm, 'beforeCreate'); initInjections(vm); // resolve injections before data/props initState(vm); initProvide(vm); // resolve provide after data/props callHook(vm, 'created'); /* istanbul ignore if */ if (config.performance && mark) { vm._name = formatComponentName(vm, false); mark(endTag); measure(("vue " + (vm._name) + " init"), startTag, endTag); } if (vm.$options.el) { vm.$mount(vm.$options.el); } }; }initでやることはvm=thisにmountで必要な情報を追加してあげることです。
initに入った直後
まずはmergeOptionから見ていきます。
resolveConstructorOptionsはnew VueにsuperClassがある場合なので今回は何もしません。
mergeOptionの役割はparentとchildがそれぞれobjectなのでそのobjectのkeyを使ってあらかじめ用意したmapであるstratsから関数を取り出して,実行しその返り値をoptionに加えることです。/** * Merge two option objects into a new one. * Core utility used in both instantiation and inheritance. */ function mergeOptions ( parent, child, vm ) { var options = {}; var key; for (key in parent) { mergeField(key); } for (key in child) { if (!hasOwn(parent, key)) { mergeField(key); } } function mergeField (key) { var strat = strats[key] || defaultStrat; options[key] = strat(parent[key], child[key], vm, key); } return options }例えばparentのcomponentを例としてみるとmergeFieldのstrats[component]はmergeAssetsなので、
option[component]にはmergeAssetsの返り値が入ります。childのelとdataをkeyとしてstratsから手に入れる関数は特殊なので注意、
starts.elによってoptionにel: "#vue_example"、
strats.dataによってdata: ƒ mergedInstanceDataFn()が追加されますmergedOptionの結果としてel,dataがvmに追加されます。
次にinitProxyを見ます。
initProxy = function initProxy (vm) { if (hasProxy) { // determine which proxy handler to use const options = vm.$options const handlers = options.render && options.render._withStripped ? getHandler : hasHandler //ここではhasHandler vm._renderProxy = new Proxy(vm, handlers) } else { vm._renderProxy = vm } }const hasHandler = { has (target, key) { const has = key in target const isAllowed = allowedGlobals(key) || (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)) if (!has && !isAllowed) { if (key in target.$data) warnReservedPrefix(target, key) else warnNonPresent(target, key) } return has || !isAllowed } }補足 proxyについて
proxyはobjectのgetとかsetの振る舞いをhandlerを渡してそのhandlerの通りに動かしたい時に使います。
例えば例としてlet obj = { name:"stonelike",value:1}; const handler = { has(target,key){// key in target const has = key in target; return has; } } let proxy = new Proxy(obj,handler);//objをwrapしたproxyができる //今回はhandlerにhasを指定しているので ? in proxyみたいにinを使ったときにhandlerが起動します。 //example console.log(name in proxy);//true,objにnameがあるので1 console.log(adadad in proxy)l//falseなので、vmに追加される_renderProxyは ? in vm._renderProxyとなったときにhasが起動してチェックが入ります、これはmountCOmponentで使われるのでまだまだ先ですが・・・
補足終わりinitに戻って、initLifeCycleとinitEventは特に大したことはやっていないので飛ばしてinitRenderを見ていきます。
function initRender (vm) { vm._vnode = null; // the root of the child tree vm._staticTrees = null; // v-once cached trees var options = vm.$options; vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); }; vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); }; /* istanbul ignore else */ { defineReactive$$1(vm, '$attrs', parentData && parentData.attrs || emptyObject, function () { !isUpdatingChildComponent && warn("$attrs is readonly.", vm); }, true); defineReactive$$1(vm, '$listeners', options._parentListeners || emptyObject, function () { !isUpdatingChildComponent && warn("$listeners is readonly.", vm); }, true); } }ここのvm._cはmountComponentで使います。
そして重要なのはdefineReactive$$1です。やりたいことはproxyと同じくgetとかsetのcustomなんですが、それとともにwatcherと連携させるようにしたいので、例えば$attrにsetするとき、customSetの中でdep.notify()が発火,そしてwatchが起動してre-renderするようにしてあげます。こうして名前の通り、Reactiveとしています。watcherとdepの関係など詳しくはwatcherの章でやります。
definePropertyですが、objectにpropertyを追加し、そのpropertyに対してproxyでやった通りにget,setなどをcustomするときに使います、$attrをObservableにしている感じですね。
ここではvmに$attrなどを追加しています。definePropertyの参考
https://qiita.com/saka_pon/items/42f30cf4983822240398callHookはbeforeCreateとかを呼ぶためのものです。今回は何も書いていないので飛ばします。
initInjectionも飛ばして、initStateへ、ここでvmにdataがsetされるのでここから先で初めてdataにアクセス可能となります。function initState (vm) { vm._watchers = []; var opts = vm.$options; if (opts.props) { initProps(vm, opts.props); } if (opts.methods) { initMethods(vm, opts.methods); } if (opts.data) { initData(vm); } else { observe(vm._data = {}, true /* asRootData */); } if (opts.computed) { initComputed(vm, opts.computed); } if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch); } }ここでvar data = vm.$options.data;はstrats.dataで手に入れたmergedInstanceDataFnで、getDataではこの関数が実行されてdata,vm_dataに返り値が入ります。
data = vm._data = {message: "Hello Vue.js!"}getData以降は
一つはdataのkey、ここではmessageのみに対してvmからproxyを使ってショートカットを作ります。
vm._data.messageではなく、vm.messageでも値がとれる、といった感じです。二つ目は$attrと同じようにdataをobservableにします。
これはmessageが"aaaa"みたいになったら{{message}}の部分も"aaaa"になってほしいからです。function initData (vm) { var data = vm.$options.data; data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}; if (!isPlainObject(data)) { data = {}; process.env.NODE_ENV !== 'production' && warn( 'data functions should return an object:\n' + 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm ); } // proxy data on instance var keys = Object.keys(data); var props = vm.$options.props; var methods = vm.$options.methods; var i = keys.length; while (i--) { var key = keys[i]; if (process.env.NODE_ENV !== 'production') { if (methods && hasOwn(methods, key)) { warn( ("Method \"" + key + "\" has already been defined as a data property."), vm ); } } if (props && hasOwn(props, key)) { process.env.NODE_ENV !== 'production' && warn( "The data property \"" + key + "\" is already declared as a prop. " + "Use prop default value instead.", vm ); } else if (!isReserved(key)) { proxy(vm, "_data", key); } } // observe data observe(data, true /* asRootData */); }new Observer
defによってvalueにobを作り、walkでvalueのkey一つ一つをObservableにしていきます。function def (obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }); }Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { defineReactive$$1(obj, keys[i]); } };複雑だったのでinitStateでやったことをまとめると、結論から言えば今回の例でいえばmessageをObservable化している。
まず、data = vm.data = {message: "Hello Vue.js!"}とした。
次にvm.messageからvm._data.messageにアクセスできるようになり、
observeでdata、つまりvm._dataのkey一つ一つをwalkでObservableにした。
ここで、vm.messageを変更しようとすると、vm.data.messageにアクセスすることになり、これはObservableになっているので、Reactiveになっている。initStateにもどると、残りにinitComputedとinitWatchがあるが、今回は使わないので飛ばして
initに戻り、initProvideも大したことはやっていないのでmountへ進むmount
ここでelは#vue_example
query(el)でelに本当のdomが入る,そしてgetOuterHTML(el)で
template="<div id="vue_example">
{{message}}
</div>"
となるVue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) const options = this.$options template = getOuterHTML(el) const { render, staticRenderFns } = compileToFunctions(template, { outputSourceRange: process.env.NODE_ENV !== 'production', shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns } } return mount.call(this, el, hydrating) }export function query (el: string | Element): Element { if (typeof el === 'string') { const selected = document.querySelector(el) if (!selected) { process.env.NODE_ENV !== 'production' && warn( 'Cannot find element: ' + el ) return document.createElement('div') } return selected } else { return el } }やっとcompile前にたどり着いたので次からcompileを見ていきたいと思います。
- 投稿日:2020-04-02T14:56:30+09:00
Rails_MySQL_Docker_Vue.jsの環境をシェル一発で始められるようしてみた
■ 何これ?
$ rails new
する機会が久々にあって、諸々込みでもっと簡単に環境構築出来たら良いなと思ったので、@azul915さんの記事のshellを参考にちょっと改変して使えるようにしてみました。Githubにもありますので好きなだけいじってください。
ご指摘などありましたらお気軽に。
■ 最終的なディレクトリ構成
【app_name】 ∟ Dockerfile ∟ docker-compose.yml ∟ src ∟ [rails new で生成されたファイル群]■ Special Thanks
- 丁寧すぎるDocker-composeによるrails5 + MySQL on Dockerの環境構築(Docker for Mac)
- https://qiita.com/azul915/items/5b7063cbc80192343fc0
- このサイトのshellからスタートしてます。大感謝。
■ 使い方
1. docker / docker-composeが入っている前提
2. 任意のディレクトリを作成
ex. $ mkdir sample_app $ cd sample_app3. shellを配置して実行
$ bash docker-rails-vue.sh
4. 起動確認して接続
$ docker-compose ps Name Command State Ports ------------------------------------------------------------------------------------------ sample_app_db_1 docker-entrypoint.sh mysqld Up 0.0.0.0:3306->3306/tcp, 33060/tcp sample_app_web_1 rails s -p 3000 -b 0.0.0.0 Up 0.0.0.0:3000->3000/tcp5. http://localhost:3000/に接続
6. 生成されたファイル群をGitにあげて開発スタートすれば良かろうもん
■docker-rails-vue.sh
#!/bin/bash echo "----- 1. docker pull ruby:2.7.0 -----" docker pull ruby:2.7.0 echo "-----2. docker pull mysql:5.7 -----" docker pull mysql:5.7 docker images echo "----- 3. create Dockerfile -----" APP_ROOT="/`pwd | xargs basename`" cat <<EOF > Dockerfile FROM ruby:2.7.0 ENV LANG C.UTF-8 RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \ apt-get update && \ apt-get install -y build-essential \ libpq-dev \ nodejs \ yarn \ && rm -rf /var/lib/apt/lists/* \ && yarn install --check-files RUN mkdir $APP_ROOT WORKDIR $APP_ROOT ADD ./src/Gemfile $APP_ROOT/Gemfile ADD ./src/Gemfile.lock $APP_ROOT/Gemfile.lock RUN bundle install ADD ./src/ $APP_ROOT EOF echo "----- 4. create Gemfile -----" mkdir src && cd src cat <<'EOF' > Gemfile source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '2.7.0' gem 'rails', '~> 6.0.2', '>= 6.0.2.1' gem 'mysql2', '>= 0.4.4', '< 0.6.0' gem 'puma', '~> 4.1' gem 'sass-rails', '>= 6' gem 'webpacker', '~> 4.0' gem 'turbolinks', '~> 5' gem 'jbuilder', '~> 2.7' gem 'bootsnap', '>= 1.4.2', require: false gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] group :development, :test do gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] end group :development do gem 'web-console', '>= 3.3.0' gem 'listen', '>= 3.0.5', '< 3.2' gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' end group :test do gem 'capybara', '>= 2.15' gem 'selenium-webdriver' gem 'webdrivers' end EOF touch Gemfile.lock cd ../ echo "----- 5. create docker-compose.yml -----" cat <<EOF > docker-compose.yml version: '3' services: db: image: mysql:5.7 volumes: - ./src/db/mysql_data:/var/lib/mysql environment: - MYSQL_ROOT_PASSWORD=password ports: - "3306:3306" web: build: . command: rails s -p 3000 -b '0.0.0.0' volumes: - ./src:$APP_ROOT environment: RAILS_ENV: development MYSQL_DATABASE: db_dev MYSQL_USERNAME: root MYSQL_PASSWORD: password MYSQL_HOST: db ports: - "3000:3000" links: - db EOF echo "----- 6. create Rails new app -----" docker-compose build docker-compose run web rails new . --force --database=mysql --webpack=vue --skip-bundle --skip-turbolinks echo "----- 7. fix config/database.yml -----" cd src echo "fix config/database.yml" cd config rm database.yml cat <<'EOF' > database.yml default: &default adapter: mysql2 encoding: utf8 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> socket: /var/run/mysqld/mysqld.sock database: <%= ENV.fetch("MYSQL_DATABASE") %> username: <%= ENV.fetch("MYSQL_USERNAME") %> password: <%= ENV.fetch("MYSQL_PASSWORD") %> host: <%= ENV.fetch("MYSQL_HOST") %> development: <<: *default production: <<: *default EOF cd ../ echo "----- 8. create database -----" docker-compose run web rake db:create echo "----- 9. docker-compose up -d -----" docker-compose up -d■ エラーとか
WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
https://wp.tekapo.com/2019/07/15/difference-between-apt-and-apt-get/
aptじゃなくてapt-get使いましょう。
■ 他参考サイト様
■ この後やること
- vue入ってるのにまだ使ってない件
- この辺りとか参考に↓
- Ruby on Rails, Vue.js で始めるモダン WEB アプリケーション入門
- webpackerじゃなくてwebpackで良い感じにしたい件
- docker image 最適化(マルチステージビルド)したい件
- 投稿日:2020-04-02T12:59:15+09:00
Vue.jsで画像ファイルを選択する方法、拡大縮小する方法
概要
Vue.jsで、
<input type="file">
タグを用いてローカルの画像ファイル(※1)を選択して読み込む方法を記載する。合わせて、拡大縮小する方法も記載する。※1:スマホならカメラでの撮影を選択できることも多い(スマホのブラウザ実装次第)。
※2:Vue CLIを前提とするので、CDN版で了する際は、html部分とjs部分を適宜読み替えること。前提
Vue CLIで生成される
App.vue
を次のように変更し、components
フォルダ配下にMyClient.vue
ファイルを配置するものとする。以降では、この構成を前提としてMyClient.vue
ファイルの実装方法をについて述べる。App.vue<template> <div id="app"> <MyClient></MyClient> </div> </template> <script> import MyClient from './components/MyClient.vue' export default { name: 'App', components: { MyClient } } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: #2c3e50; } </style>ローカルの画像ファイルを読み込んで表示する方法
ファイル選択ダイアログの入力受けて画像ファイルを表示するには次のようにする。
<input type="file">
タグにv-on:change="メソッド名"
を追加して、ファイル選択ダイアログの実行結果を受け取れるようにするv-on:change="メソッド名"
で指定したメソッドにイベントオブジェクトが渡されるので、そこからファイルパスを取得する- 続いて、FileReaderオブジェクトを利用して、Base64エンコードしたデータとして受け取る
※file属性では、セキュリティの都合で「外部(ダイアログ以外)からローカルファイルパスを指定できない」ので、
v-model
による双方向バインドは指定不可。※読み込んだファイルを、REST APIでどこかのバックエンドにアップロードすることが多いと思われるんので、その際によく使われる形式であるBase64で読み込んでおく。
img
タグはBase64形式を画像として表示できるので問題ない。他の形式での読み込み方は、こちらを参照のこと。→ https://developer.mozilla.org/ja/docs/Web/API/FileReader
MyClient.vue
ファイルの実装例は次のようになる。MyClient.vue<template> <div> <div id="id_title"> ファイル選択ダイアログからの画像ファイルのアップロード </div> <br> <div id="id_face_imaeg"> <img :src="targetImage" alt="選択された画像" class="image"> </div> <br> <br> <div id="id_register_image"> <input v-on:change="selectedFile" type="file" name="file" accept="image/jpeg, image/png"><br> <br> <br> </div> </div> </template> <script> // javascriptファイルをココへ。 export default { name : "MyClient", components : { }, data : function () { return { targetImage : null } }, methods : { getFileAsBase64 : function(filePath) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = e => resolve(e.target.result); reader.onerror = error => reject(error); reader.readAsDataURL(filePath); // ここまでで「resolve(e.target.result)」でbase64化された画像ファイルデータが返却される。 // https://fujiten3.hatenablog.com/entry/2019/07/10/133132 }) }, selectedFile : function (e) { let files = e.target.files; e.preventDefault(); // 標準のInputタグの動作をキャンセル // http://tech.aainc.co.jp/archives/10714 // https://developer.mozilla.org/ja/docs/Web/API/File/Using_files_from_web_applications if(files && files.length > 0){ // 有効なFileオブジェクトが渡された時は、画像ファイルとして読み込みを実施 this.getFileAsBase64(files[0]) .then((imgDataBase64)=>{ this.targetImage = imgDataBase64; }); // ToDo: エラー処理 } } } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> /* Cssファイルはここへ配置する。 */ </style>読み込んだ画像をリサイズ(拡大縮小)する方法
読み込んだ画像を拡大縮小してからアップロードを行いたい場合があります。
その際に、JavaScriptのみでリサイズ後の画像データを生成するには、次のようにします。※表示時にリサイズしたいだけ(データは変更しない)なら、imgタグの縦横設定でOKです。
- 読み込んだ画像データをImageオブジェクトへ変換
- srcタグに相当
- 変換は非同期に行われるので、onloadメソッドが呼ばれたらresolve()する
- HTML5の
canvas
タグへ描画する際の「縮尺指定」を利用してリサイズを実行canvas
タグの描画データを「その縦横サイズの画像」として新規に取得する
MyClient.vue
ファイルの実装例は次のようになる。なお、このサンプルではcanvasエリアを非表示の設定としている。MyClient.vue<template> <div> <div id="id_title"> ファイル選択ダイアログからの画像ファイルのアップロード </div> <br> <div id="id_face_imaeg"> <img :src="targetImage" alt="選択された画像" class="image"> </div> <br> <br> <div id="id_register_image"> <input v-on:change="selectedFile" type="file" name="file" accept="image/jpeg, image/png"><br> <br> <br> </div> <div id="id_canvas_for_resize" v-show="false"> 拡大縮小用のCanvasエリア<br> <canvas ref="canvas"/> </div> </div> </template> <script> // javascriptファイルをココへ。 export default { name : "MyClient", components : { }, data : function () { return { resizeUpperLimitPixel : 320, targetImage : null } }, methods : { getFileAsBase64 : function(filePath) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = e => resolve(e.target.result); reader.onerror = error => reject(error); reader.readAsDataURL(filePath); // ここまでで「resolve(e.target.result)」でbase64化された画像ファイルデータが返却される。 // https://fujiten3.hatenablog.com/entry/2019/07/10/133132 }) }, resizeImage64withCanvase : function (loadedFile, canvas) { // 以下、縮小を掛けるにはimage経由でのcanvasへの貼り付けを利用する。 // https://qiita.com/busroutemap/items/b563dfe8b08bb3338eb5 // https://qiita.com/su_mi1228/items/492c89db7f96823a26c0 // https://www.mahirokazuko.com/entry/2019/08/20/133713 return new Promise((resolve)=>{ const image = new Image(); image.onload = () => resolve(image); // (e)は利用されないので省略。 image.src = loadedFile; }).then((image)=>{ const ctx = canvas.getContext('2d'); // 2Dコンテキスト const MAX_SIZE = this.resizeUpperLimitPixel; // 縦横で長い方の最大値を指定する(例:800)とする if (image.width < MAX_SIZE && image.height < MAX_SIZE) { // MAX_SIZEよりも小さかったらそのまま貼り付ける [canvas.width, canvas.height] = [image.width, image.height]; ctx.drawImage(image, 0, 0); }else{ let dstWidth; let dstHeight; // 縦横比の計算 if (image.width > image.height) { dstWidth = MAX_SIZE; dstHeight = (image.height * MAX_SIZE) / image.width; } else { dstHeight = MAX_SIZE; dstWidth = (image.width * MAX_SIZE) / image.height; } canvas.width = dstWidth; canvas.height = dstHeight; // canvasに既に描画されている画像があればそれを消す ctx.clearRect(0,0,dstWidth,dstHeight); // リサイズして貼り付ける ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, dstWidth, dstHeight); } // canvasから画像をbase64として取得する let base64 = canvas.toDataURL('image/jpeg'); return Promise.resolve(base64); }); }, selectedFile : function (e) { let files = e.target.files; e.preventDefault(); // 標準のInputタグの動作をキャンセル // http://tech.aainc.co.jp/archives/10714 // https://developer.mozilla.org/ja/docs/Web/API/File/Using_files_from_web_applications if(files && files.length > 0){ // 有効なFileオブジェクトが渡された時は、画像ファイルとして読み込みを実施 this.getFileAsBase64(files[0]) .then((imgDataBase64)=>{ return this.resizeImage64withCanvase(imgDataBase64, this.$refs.canvas); }).then((imgDataBase64)=>{ this.targetImage = imgDataBase64; }); // ToDo: エラー処理 } } } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> /* Cssファイルはここへ配置する。 */ </style>画像選択とリサイズまでをComponent化する
画像ファイル選択とリサイズの部分をComponent化(部品可)して、「
<SelectImage v-on:selectedImage="finishSelectingImage">
と記載したら、読み込みとリサイズ完了後にfinishSelectingImage()が呼ばれる(※呼ばれる関数名は任意)」ようにすることを考える。この場合、呼び出し側の
MyClient.veu
は次のようにする。
なお、リサイズ値とCanvasの表示非表示を設定できるようにしてある。MyClient.vue<template> <div> <div id="id_title"> ファイル選択ダイアログからの画像ファイルのアップロード </div> <div id="id_face_imaeg"> <div id="id_face_imaeg_uploaded" v-show="targetImage"> <img :src="targetImage" alt="サムネイル" class="image"> </div> <div id="id_face_imaeg_notfound" v-show="!targetImage"> <img alt="画像は未選択" src="../assets/no_image.png"> </div> </div> <br> <br> <SelectImage v-on:selectedImage="finishSelectingImage" v-bind:isCanvasShow="false" v-bind:resizeUpperLimitPixel="360"> <!-- 画像の読み込み --> </SelectImage> </div> </template> <script> // javascriptファイルをココへ。 import SelectImage from './SelectImage.vue'; export default { name : "MyClient", components : { SelectImage, }, data : function () { return { targetImage : null } }, methods : { clickReset : function () { this.targetImage = null; }, finishSelectingImage : function (selectedImage) { if(selectedImage){ this.targetImage = selectedImage; } } } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> /* Cssファイルはここへ配置する。 */ </style>呼び出し先のcomponentは、この例では「SelectImage」として次のように実装する。
SelectImage.vue<template> <div> <div id="id_register_image"> <input v-on:change="selectedFile" type="file" name="file" accept="image/jpeg, image/png"><br> ※画像は{{resizeUpperLimitPixel}}x{{resizeUpperLimitPixel}}以下へリサイズされます。<br> <br> <br> <div class="selecting-image-footer"> <slot name="footer"><!-- default footer --> <!-- 戻るボタンなどが必要な場合は、呼び出しもとで設定すること --> </slot> </div> </div> <div id="id_canvas_for_resize" v-show="isCanvasShow"> <canvas ref="canvas"/> </div> </div> </template> <script> // javascriptファイルをココへ。 export default { name : "SelectImage", props : { isCanvasShow : { type: Boolean, default: true, required: false }, resizeUpperLimitPixel : { type: Number, default: 800, required: false } }, data : function () { return { uploadedImage : null } }, created : function () { }, methods : { getFileAsBase64 : function(filePath) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = e => resolve(e.target.result); reader.onerror = error => reject(error); reader.readAsDataURL(filePath); // ここまでで「resolve(e.target.result)」でbase64化された画像ファイルデータが返却される。 // https://fujiten3.hatenablog.com/entry/2019/07/10/133132 }) }, resizeImage64withCanvase : function (loadedFile, canvas) { // 以下、縮小を掛けるにはimage経由でのcanvasへの貼り付けを利用する。 // https://qiita.com/busroutemap/items/b563dfe8b08bb3338eb5 // https://qiita.com/su_mi1228/items/492c89db7f96823a26c0 // https://www.mahirokazuko.com/entry/2019/08/20/133713 return new Promise((resolve)=>{ const image = new Image(); image.onload = () => resolve(image); // (e)は利用されないので省略。 image.src = loadedFile; }).then((image)=>{ const ctx = canvas.getContext('2d'); // 2Dコンテキスト const MAX_SIZE = this.resizeUpperLimitPixel; // 縦横で長い方の最大値を指定する(例:800)とする if (image.width < MAX_SIZE && image.height < MAX_SIZE) { // MAX_SIZEよりも小さかったらそのまま貼り付ける [canvas.width, canvas.height] = [image.width, image.height]; ctx.drawImage(image, 0, 0); }else{ let dstWidth; let dstHeight; // 縦横比の計算 if (image.width > image.height) { dstWidth = MAX_SIZE; dstHeight = (image.height * MAX_SIZE) / image.width; } else { dstHeight = MAX_SIZE; dstWidth = (image.width * MAX_SIZE) / image.height; } canvas.width = dstWidth; canvas.height = dstHeight; // canvasに既に描画されている画像があればそれを消す ctx.clearRect(0,0,dstWidth,dstHeight); // リサイズして貼り付ける ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, dstWidth, dstHeight); } // canvasから画像をbase64として取得する let base64 = canvas.toDataURL('image/jpeg'); return Promise.resolve(base64); }); }, selectedFile : function (e) { let files = e.target.files; e.preventDefault(); // 標準のInputタグの動作をキャンセル // http://tech.aainc.co.jp/archives/10714 // https://developer.mozilla.org/ja/docs/Web/API/File/Using_files_from_web_applications if(files && files.length > 0){ // 有効なFileオブジェクトが渡された時は、画像ファイルとして読み込みを実施 this.getFileAsBase64(files[0]) .then((imgDataBase64)=>{ return this.resizeImage64withCanvase(imgDataBase64, this.$refs.canvas); }).then((imgDataBase64)=>{ this.uploadedImage = imgDataBase64; this.$emit('selectedImage', this.uploadedImage); }); // ToDo: エラー処理 } } } } </script> <style scoped> </style>以上ー。
参考サイト
- Vueで画像アップロード + プレビュー機能付きフォームを作りました。(Base64エンコード利用)
- canvasとvuejsの連携を初心者なりに調べた
- 画像をリサイズしてblobでプレビュー表示する方法【Vue/Canvas】
- ブラウザで画像を縮小してサーバにアップロードするJavaScript
- Vue.js でファイルをポストしたいとき
- ウェブアプリケーションからのファイルの使用
- 投稿日:2020-04-02T12:41:01+09:00
Vue.js 算出プロパティとメソッドの違い
はじめに
Vue.js テンプレート制御ディレクティブ まとめの続きです。
今回は算出プロパティとメソッドの違いを学習していきます。
算出プロパティcomputed
算出プロパティ
computed
は、関数によって算出したデータを返す事ができるプロパティ。例えば、以下のようにマスタッシュ構文内に複雑なロジックを書くと可読性が悪くなる。
<!-- 文字列を反転する式 --> {{ message.split('').reverse().join('') }}
こういった複雑なロジックを実行する時に、算出プロパティを利用することが推奨される。
また、ロジックの再利用性を高めたい時などにも利用できる。
computed
を利用してコードを書いてみる。<div id="app"> <p> {{ reverseMessage }} <!-- !sj.euV olleH --> </p> </div> <script> var app = new Vue({ el: '#app', data: { message: 'Hello Vue.js', }, // 算出プロパティの定義 computed: { // メソッドの定義 reverseMessage() { return this.message.split('').reverse().join('') } } }) </script>あれ?これ
methods
と何が違うの・・・
computed
とmethods
それぞれの違いを深掘りする。算出プロパティとメソッドの3つの違い
①プロパティとメソッド
computed
はプロパティなので()
が不要。
methods
はメソッドなので()
が必要。<!-- computed --> {{ reverseMessage }} <!-- methods --> {{ reverseMessage() }}②getterとsetter
computed
はgetter
とsetter
を定義する事ができる。
methods
はgetter
しか定義できない。③キャッシュ
computed
はキャッシュされる。
methods
はキャッシュされない。
methods
は呼び出されるたびに関数の処理を行うまとめ
実行したい処理の内容によって
computed
かmethods
か使い分けるのが良さそう。しかし、現状どういったケースで使い分けるかイメージが沸かない。
この辺りは、実際にアプリケーションを開発しながら身に付けるしかなさそう。
知見が深まり次第、追記していきます
更新履歴
Vue.jsの基本的な使い方まとめ
Vue.jsでTO DOアプリを作る
Vue.js テンプレート制御ディレクティブ まとめ
Vue.js 算出プロパティとメソッドの違い 今ココ
- 投稿日:2020-04-02T11:58:08+09:00
【Vue.js】概要理解からHello World出力まで
Vue.jsとは
- オープンソースのJavaScriptフレームワークです。
- ライセンスはMIT Licenseです。
- プログレッシブフレームワークと公式サイトで称しています。
- プログレッシブとは、「進歩的」「進行形の」「進行性の」「漸進的な」「累進的な」「連続的な」などを意味する英語ですが、ここでは、「少しずつ適用していけるように」という意味を込めたフレームワークという意味で使用しているようです。
- 中核となるライブラリは、View層だけに焦点を当てている為、他のライブラリに比べて導入が簡単。
- 軽量のフレームワークの為、新規に導入するのも、既存プロジェクトに導入するのもSPA(シングルページアプリケーション)を構築するのも簡単
- 開発者はEvan You氏。「Angularの本当に好きだった部分を抽出して、余分な概念なしに本当に軽いものを作ることができたらどうだろうか?」と考えてVue.jsを開発したとのこと。
1. インストールするには
1.1. ダウンロードして使用
- GitHubからダウンロードします。
- 2020/4/2時点では、最新の安定バージョンは 2.6.11です。
- ダウンロード後、scriptタグで読み込みます。
1.2. CDNで使用
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
- 上記の記載だと、最新バージョンが適用されますので、本番環境では注意が必要です。最新バージョン適用に伴い、予期せぬ挙動となる可能性が有るので、バージョン番号、ビルド番号を明記する事を推奨
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
- ネイティブのES Modulesを使用している場合、ES Modules互換ビルドを利用できます。ES Modulesについては、こちらのサイトを参考にさせて頂きました。
<script type="module"> import Vue from 'https://unpkg.com/vue@2.6.0/dist/vue.esm.browser.min.js'; new Vue({ ... }); </script>
- Vue は unpkg または cdnjs 上でも利用可能。
1.3. npmによるインストール
- 大規模アプリケーションを構築する際は、npmを利用したインストールが推奨されています。npmは、Node.jsのパッケージ管理ツールです。
$ npm install vue
- パッケージ管理ツールを使用する事で、Webpack、Browserify といったモジュールバンドラを組み合わせて使用するメリットも生まれます。WebpackとVue.jsを組み合わせたチュートリアルとして、こちらの記事を参考にさせて頂きました。
1.3.1. npmでインストールした場合のビルドファイルについて
- dist/ ディレクトリ下のビルドファイル (公式サイトより)
- 用語については、公式サイトを参照
UMD CommonJS ES Module(バンドラ用) ES Module (ブラウザ用) 完全 vue.js vue.common.js vue.esm.js vue.esm.browser.js ランタイム限定 vue.runtime.js vue.runtime.common.js vue.runtime.esm.js - 完全 (本番用) vue.min.js - - vue.esm.browser.min.js ランタイム限定 (本番用) vue.runtime.min.js - - - 1.4. CLIによる環境構築
- Vue.jsの開発環境をCLIを使用してスピーディーに構築する事が出来るようです。大規模アプリケーション環境の場合に重宝できそうです。
- 公式サイトでは、ビルドツールに関する事前知識を前提としています。
CLI は Node.js および関連するビルドツールに関する事前知識を前提としています。Vue またはフロントエンドビルドツールを初めて使用している場合、CLI を使用する前に、ビルドツールなしでガイドを参照することを強くお勧めします。
デバッグツール
- Vue Devtoolsが用意されています。使い方は、こちらの記事を参考にさせて頂きました。
Hello Worldの作成
2.1. 前提条件
- Vue.jsのscriptタグはCDNで読み込みしています。Vue.jsをダウンロードして読み込む場合は、1.1. ダウンロードして使用を参照してください。
2.2. 手順
- index.htmlを作成
headにscriptタグを定義する
- 開発バージョンの場合
<!-- 開発バージョン、便利なコンソールの警告が含まれています --> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
- 本番バージョンの場合
<!-- 本番バージョン、サイズと速度のために最適化されています --> <script src="https://cdn.jsdelivr.net/npm/vue"></script>bodyに”Hello World”の文言を出力するdivタグを定義する
<div id="app"> {{ message }} </div>index.htmlファイル中にJSを定義する
var app = new Vue({ el: '#app', data: { message: 'Hello World!' } })2.3. 完成系のindex.html
<html lang='ja'> <head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> </head> <body> <div id="app"> {{ message }} </div> <script> var app = new Vue({ el: '#app', data: { message: 'Hello World!' } }) </script> </body> </html>2.4. 出力結果
参考サイト
公式サイト
vue.js developers New in Vue: ES Module Browser Build
Wikipedia-Vue.js
Webpackで始めるVue.js
- 投稿日:2020-04-02T11:42:24+09:00
マンガを1話だけ完成させてみた
漫画を1話だけ完成させてみました。ページ数的には、1話くらいの分量ですね。物語的にも一区切り付きました。絵を書くのって大変で、セリフとか背景とか時間がかかります。週間連載の漫画家は、バケモンじゃないかと思いました。
漫画のページはvue.jsで構築しています。次のページボタンと画像URLのリンクを生成。
ここから技術的な話
昔は、gitbookを使ってみたことがあったのですが、シンプルじゃないと思ったので、vueで作りました。
vue 2.x + webpack 4.x 系でenvを使うのにハマったのでメモ。
結論としてdotenv-webpackを使えばいけました。
$ echo "page=4" >> .env $ yarn add dotenv-webpackwebpack.config.jsconst Dotenv = require('dotenv-webpack'); module.exports = { plugins: [ new Dotenv('./.env') ] }src/main.jsvar app = new Vue({ data: { products: [...Array(Number(process.env.page)).keys()] } })package.json"scripts": { "build": "cross-env NODE_ENV=production webpack --progress --hide-modules" }以上、漫画の宣伝をqiitaでもやってみたという話でした。
- 投稿日:2020-04-02T00:00:37+09:00
Nuxt.js(Vue.js) + Netlifyでポートフォリオ作成@2020年度版
キッカケは色々ググって見つけた記事 Nuxt.js + Netlifyで爆速構築するサーバーレス開発入門 から。
やりたいこと(ざっくり)
- 1日で出来るぐらいの粒度でパパッとアウトプットしたい
- ひとまず小難しいことは考えずほぼHTML/CSSで完結させたい
- SCSSでglobal変数を扱いたい
- Nuxt.js選別理由としては主に2点
- header / footerなど共通パーツはコンポーネント化して共通で呼び出したい
- 制作物は量が多いので、JSONにデータ集約させてforループで表示させたい
- (Nuxt以外にもNextなどいくらでも候補はありそうだが、個人的な使用経験により)
- ホスティングサービス候補は、Netlify※使用経験なし or GitHub Pages※昔作ったことがある
- Netlifyが簡単とよく見かけるので、お試し感覚で。
→ ちなみに作成したサイトはこちら
Nuxtインストール
※前提として、npm vs yarnどっち使うかの話を踏まえてyarnで進めていきます。
※yarnのインストールまでは個々の環境差があるため省略しますmmNuxt公式ドキュメントより、今回はcreate-nuxt-appを使用していきます。
$ yarn create nuxt-app <project-name>今回project-nameはs2-web-creationとしています。
ステップが進み、各種設定を入力するフェーズでは一例として以下としました。
※最小限のインストールであればすべてデフォルトで進めて問題ないそうです。
※後々の拡張を考えて、Typescript関連とTailwind CSS、あとLinterを導入した感じです。✨ Generating Nuxt.js project in s2-web-creation ? Project name s2-web-creation ? Project description S2 Web Creation's portfolio and so on. ? Author name Seven Sound ? Choose programming language TypeScript ? Choose the package manager Yarn ? Choose UI framework Tailwind CSS ? Choose custom server framework None (Recommended) ? Choose the runtime for TypeScript @nuxt/typescript-runtime ? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection) ? Choose linting tools ESLint, Prettier, StyleLint ? Choose test framework Jest ? Choose rendering mode Universal (SSR) ? Choose development tools jsconfig.json (Recommended for VS Code)インストール完了後、
$ cd <project-name> $ yarn devと打って
localhost:3000
にアクセスしてNuxtデフォルトページが表示されればOK。
確認できたら、このファイル/pages/index.vue
の中身を一旦クリアして作りたい内容に書き換えていきます。SCSS準備
SCSSでglobal変数を扱いたいを実現するために、モジュール追加・設定追加しておきます。
参考:nuxt.js で scss のグローバルな変数を使用する
@nuxtjs/style-resources公式README ではより多くの例も。$ yarn add sass-loader node-sass $ yarn add -D @nuxtjs/style-resourcesでmodule追加して、SCSSファイル追加+nuxt.config.jsに追記しておく。
nuxt.config.jsexport default { modules: [ '@nuxtjs/style-resources' ], styleResources: { scss: [ './assets/scss/vars/*.scss' // <- scssファイルの置き場所によって調整 ] } }準備ができたので実装開始
ここで説明することは脱線気味にもなるので、ピックアップで。
- font-familyについて面白い記事があり、
パクら参考にさせてもらいました。 font-familyについて本気で考えてみた- レスポンシブ対応するときのおすすめ Sassの変数とmixinで変更に強いメディアクエリをつくる
- worksのforループはこんな感じ
- ついでに(?)h1要素のタイトルロゴも少し表現こだわったのでコンポーネント化したり
works.vue<template> <div> <titleLogo titleText="Works"></titleLogo> <ul class="workList mt-6"> <li class="card" v-for="works in works" :key="works.name"> <img class="cardImage" :src='"~/assets/images/works/" + works.name + ".jpg"' :alt="works.title" /> <div class="cardText"> <h2 class="workTitle">{{ works.title }}</h2> <p class="workDetail" v-html="works.detail"></p> <p class="date">date:<span class="dateTag">{{ works.date }}</span></p> <p class="link"><a :href="works.url" target="_blank">visit site ></a></p> </div> </li> </ul> </div> </template> <script lang="ts"> import Vue from 'vue' import TitleLogo from '~/components/TitleLogo.vue' export default Vue.extend({ components: { TitleLogo }, asyncData () { return { works: require(`~/assets/json/works.json`) } } }) </script>なお実装中の確認はすべて
yaml dev
からのlocalhost確認のみで進めました。
確認できたらgit pushしておきます。(ブランチ等は任意。 master以外でも可)Netlifyでホスティング
さて、実装も無事終わったのでgit pushした後、Netlifyのページでポチポチやっていきます。
詳しい流れは冒頭で紹介した記事がわかりやすかったので、該当の項目を参照していただければ。
今回、私はブランチをmasterではなく試しにdeploy/netlify
というブランチで運用してみたため、
デプロイ直前の設定画面では以下のようになりました。あとは「Deploy site」をポチーでサイトが公開できるなんて楽な時代だ… なんて思っていたら不測の事態が。
failedしとる~~~~~!!?
なんでやなんでや…と手探りで原因を探っていたところ行き当たったのがディレクトリ構成。
※ちなみに普通の構成ならfailedしないと思います。今回、先を見据えてこのリポジトリをサンドボックス的な位置づけにしようと思ったので、とりあえず実装内容を
/nuxt
ディレクトリに移していたんですよね。
なのでデフォルトのままだと直下でgenerateしようとしてコケてたわけで。というわけで引き続き以下の設定を追加して、無事に公開に至りました!
まず、failed画面の「Deploy settings」を選択すると、以下のように設定画面へ遷移します。このBuild settings内にあるBase directory に、今回構成変更した
nuxt
を設定すればOKです
(設定時に、Publish directoryも追従して書き換えてくれます)
Overviewに戻り、1 Deploy your site !Site nameを変えておく
デプロイ完了後はサイトURLがデフォルトのランダムに割り当てられた文字列になっているため、
SettingsからSite nameを好きなものに変更しておきましょう。結局、1日で無事に公開に至った…!
実装中や、最後に書いたNetlifyの設定やらちょくちょく詰まったものの、ベース構築~デプロイ自体は本当に爆速で組めたので、
サーバーレスなものなら気軽にアウトプットできるなと改めて体感することができた。
(そもそもコンテンツ自体やレイアウトどうしよっかなーと考えていた時間の方がほとんどなので尚更)
- 投稿日:2020-04-02T00:00:37+09:00
Nuxt.js(Vue.js) + Netlifyでポートフォリオ作成 (2020年度版)
キッカケは色々ググって見つけた記事 Nuxt.js + Netlifyで爆速構築するサーバーレス開発入門 から。
やりたいこと(ざっくり)
- 1日で出来るぐらいの粒度でパパッとアウトプットしたい
- ひとまず小難しいことは考えずほぼHTML/CSSで完結させたい
- SCSSでglobal変数を扱いたい
- Nuxt.js選別理由としては主に2点
- header / footerなど共通パーツはコンポーネント化して共通で呼び出したい
- 制作物は量が多いので、JSONにデータ集約させてforループで表示させたい
- (Nuxt以外にもNextなどいくらでも候補はありそうだが、個人的な使用経験により)
- ホスティングサービス候補は、Netlify※使用経験なし or GitHub Pages※昔作ったことがある
- Netlifyが簡単とよく見かけるので、お試し感覚で。
→ ちなみに作成したサイトはこちら
Nuxtインストール
※前提として、npm vs yarnどっち使うかの話を踏まえてyarnで進めていきます。
※yarnのインストールまでは個々の環境差があるため省略しますmmNuxt公式ドキュメントより、今回はcreate-nuxt-appを使用していきます。
$ yarn create nuxt-app <project-name>今回project-nameはs2-web-creationとしています。
ステップが進み、各種設定を入力するフェーズでは一例として以下としました。
※最小限のインストールであればすべてデフォルトで進めて問題ないそうです。
※後々の拡張を考えて、Typescript関連とTailwind CSS、あとLinterを導入した感じです。✨ Generating Nuxt.js project in s2-web-creation ? Project name s2-web-creation ? Project description S2 Web Creation's portfolio and so on. ? Author name Seven Sound ? Choose programming language TypeScript ? Choose the package manager Yarn ? Choose UI framework Tailwind CSS ? Choose custom server framework None (Recommended) ? Choose the runtime for TypeScript @nuxt/typescript-runtime ? Choose Nuxt.js modules (Press <space> to select, <a> to toggle all, <i> to invert selection) ? Choose linting tools ESLint, Prettier, StyleLint ? Choose test framework Jest ? Choose rendering mode Universal (SSR) ? Choose development tools jsconfig.json (Recommended for VS Code)インストール完了後、
$ cd <project-name> $ yarn devと打って
localhost:3000
にアクセスしてNuxtデフォルトページが表示されればOK。
確認できたら、このファイル/pages/index.vue
の中身を一旦クリアして作りたい内容に書き換えていきます。SCSS準備
SCSSでglobal変数を扱いたいを実現するために、モジュール追加・設定追加しておきます。
参考:nuxt.js で scss のグローバルな変数を使用する
@nuxtjs/style-resources公式README ではより多くの例も。$ yarn add sass-loader node-sass $ yarn add -D @nuxtjs/style-resourcesでmodule追加して、SCSSファイル追加+nuxt.config.jsに追記しておく。
nuxt.config.jsexport default { modules: [ '@nuxtjs/style-resources' ], styleResources: { scss: [ './assets/scss/vars/*.scss' // <- scssファイルの置き場所によって調整 ] } }準備ができたので実装開始
ここで説明することは脱線気味にもなるので、ピックアップで。
- font-familyについて面白い記事があり、
パクら参考にさせてもらいました。 font-familyについて本気で考えてみた- レスポンシブ対応するときのおすすめ Sassの変数とmixinで変更に強いメディアクエリをつくる
- worksのforループはこんな感じ
- ついでに(?)h1要素のタイトルロゴも少し表現こだわったのでコンポーネント化したり
works.vue<template> <div> <titleLogo titleText="Works"></titleLogo> <ul class="workList mt-6"> <li class="card" v-for="works in works" :key="works.name"> <img class="cardImage" :src='"~/assets/images/works/" + works.name + ".jpg"' :alt="works.title" /> <div class="cardText"> <h2 class="workTitle">{{ works.title }}</h2> <p class="workDetail" v-html="works.detail"></p> <p class="date">date:<span class="dateTag">{{ works.date }}</span></p> <p class="link"><a :href="works.url" target="_blank">visit site ></a></p> </div> </li> </ul> </div> </template> <script lang="ts"> import Vue from 'vue' import TitleLogo from '~/components/TitleLogo.vue' export default Vue.extend({ components: { TitleLogo }, asyncData () { return { works: require(`~/assets/json/works.json`) } } }) </script>なお実装中の確認はすべて
yaml dev
からのlocalhost確認のみで進めました。
確認できたらgit pushしておきます。(ブランチ等は任意。 master以外でも可)Netlifyでホスティング
さて、実装も無事終わったのでgit pushした後、Netlifyのページでポチポチやっていきます。
詳しい流れは冒頭で紹介した記事がわかりやすかったので、該当の項目を参照していただければ。
今回、私はブランチをmasterではなく試しにdeploy/netlify
というブランチで運用してみたため、
デプロイ直前の設定画面では以下のようになりました。あとは「Deploy site」をポチーでサイトが公開できるなんて楽な時代だ… なんて思っていたら不測の事態が。
failedしとる~~~~~!!?
なんでやなんでや…と手探りで原因を探っていたところ行き当たったのがディレクトリ構成。
※ちなみに普通の構成ならfailedしないと思います。今回、先を見据えてこのリポジトリをサンドボックス的な位置づけにしようと思ったので、とりあえず実装内容を
nuxt/
ディレクトリに移していたんですよね。
なのでデフォルトのままだと直下でgenerateしようとしてコケてたわけで。というわけで引き続き以下の設定を追加して、無事に公開に至りました!
まず、failed画面の「Deploy settings」を選択すると、以下のように設定画面へ遷移します。このBuild settings内にあるBase directory に、今回構成変更した
nuxt
を設定すればOKです
(設定時に、Publish directoryも追従して書き換えてくれます)
Overviewに戻り、1 Deploy your site !Site nameを変えておく
デプロイ完了後はサイトURLがデフォルトのランダムに割り当てられた文字列になっているため、
SettingsからSite nameを好きなものに変更しておきましょう。結局、1日で無事に公開に至った…!
実装中や、最後に書いたNetlifyの設定やらちょくちょく詰まったものの、ベース構築~デプロイ自体は本当に爆速で組めたので、
サーバーレスなものなら気軽にアウトプットできるなと改めて体感することができた。
(そもそもコンテンツ自体やレイアウトどうしよっかなーと考えていた時間の方がほとんどなので尚更)