- 投稿日:2019-12-03T23:21:11+09:00
1年間サーバーレスで運用したNuxt.js製サイトをTypescript移行した話とそこで学んだこと
概要・前置き
どうも、都内でフロントエンドエンジニアをしてます、かめぽんです。以前、Nuxt.js + Netlifyで爆速構築するサーバーレス開発入門という記事を書きまして、その記事と同じNuxt.js + Netlifyのシンプルな構成で作った、以下のサイトを1年間と3ヶ月ほど運用してきました。撮影からデザイン、インタラクションの実装からデプロイまで一気通貫でやってみました。
https://www.brightanddizain.com/
Lighthouseの評価もよくサイトがきっかけでお仕事もちょくちょくいただいたりと、おかげさまでここまでくることができました。(PWA対応もしてます)
しかしながら、フロントエンド技術の流れが非常に早く陳腐化してきたことやサステナビリティを高めたいとは思いつつも課題が多かったのでこの機にnuxtのtypescript移行とそれに伴う環境基盤の構築をしました。そこで学んだことや大事なことなどをご紹介し、少しでも技術の移行やフロントエンド基盤構築や設計のお役に立てればと思います。
少々長いですが、最後まで読んでいただけると嬉しいです!サイトの概要
以下、サイトの技術スタックです。動的な部分は少なく頻繁にメンテもするわけではないので、静的なコンテンツで管理しています。しかし、DXの向上を目指しつつメンテしやすいことが条件ではあり、かつコンポーネント指向開発は必須なためnuxt.jsを使用しています。
- Nuxt.js(nuxt generate)
- Netlify
- Atomic Design
- slack api
移行の目的
現状、SFCのVueコンポーネントでサイトを構成していますが、コンポーネントの管理があまりできておらず将来的にさらに陳腐化が進むことが懸念されます。
また、これからくるであろう技術を出来るだけ実践的に試せるプレイグラウンド的な立ち位置にしたく整理をしていきます。さらに、Typescriptがこれからデファクトスタンダートのような立ち位置になるのではないかと予想をふみjavascriptから脱却し、型安全による品質とサステナビリティ向上を目指します。
- コンポーネント資産の管理と品質の向上
- プレイグラウンド的立ち位置
- 陳腐化による技術スタックの刷新
実際にやったこと・構成のポイント
ここからは実施したものやリファクタをした内容などを説明していきますが、実際に導入した施策はかなり多かったので、厳選して nuxt typescript移行をする際の重要な部分を記載します。また、アーキテクチャを考えるときに重要視しているStoreとService層について少し説明します。
- typescript導入
- Vuex Storeの型定義
- StoreとSeriviceについて
typescript導入
まず、nuxtのtypescript移行についてですがこちらは 公式でも出ているtypescript.nuxtjs.orgに従って移行します。ここでやることとしては以下の通りです。
- 必要なモジュールの導入
- config系の編集
必要なモジュールの導入
まずはじめにnuxt tsに移行するために、
@nuxt/types,
@nuxt/typescript-build
@nuxt/typescript-runtime
を導入します。 加えて、ts-loader
もインストールします。@nuxt/typescript-runtime
は任意ではありますがnuxt.configなどでtsを使う場合には必要なのでインストールします。なお@nuxt/typesは@nuxt/typescript-buildに含まれているので個別にインストールする必要はありません。npm i -D @nuxt/typescript-build ts-loader
npm i @nuxt/typescript-runtime
はプロダクション環境でも必要になるためdependenciesでインストールします。npm i @nuxt/typescript-runtimeconfig系の編集
最初にtsconfig.jsonの準備をしましょう。設定はお好みで良いですが以下に例を載せておきます。typesの欄に
@nuxt/types
を追加しておきましょう。package.json{ "compilerOptions": { "target": "es5", "module": "esnext", "moduleResolution": "node", "lib": [ "esnext", "esnext.asynciterable", "dom" ], "typeRoots" : ["./type"], "allowSyntheticDefaultImports": true, "noImplicitAny": false, "esModuleInterop": true, "allowJs": true, "sourceMap": true, "strict": true, "noEmit": true, "rootDir": "./src/", "baseUrl": "./src/", "paths": { "@*": [ "./*" ], "*": [ "*" ] }, "types": [ "@types/node", "@nuxt/types" ] }, "include": [ "src/**/*.ts", "src/**/*.vue", "src/**/*.spec.ts", "src/**/*.spec.tsx", "src/**/*.test.ts", "src/**/*.test.tsx" ], "exclude": [ "node_modules" ] }次に nuxt.configの設定ですが、先ほど
@nuxt/typescript-runtime
を導入したので早速 nuxt.config.jsの拡張子を.ts
に変えます。また、nuxtConfigに型をつけるので@nuxt/typesでtypeをつけます。import { Configuration as NuxtConfiguration } from '@nuxt/types' ... const nuxtConfig: NuxtConfiguration = { mode: 'universal', ... build: { extend(config: any, { isDev, isClient }) { const tsLoader = { loader: 'ts-loader', options: { appendTsSuffixTo: [/\.vue$/], context: __dirname, configFile: 'tsconfig.json' } } for (let rule of config.module.rules) { if (rule.loader === 'vue-loader') { rule.options.loaders = { ...rule.options.loaders, ts: tsLoader } } } } }, buildModules: [ [ '@nuxt/typescript-build', // buildMudulesに@nuxt/typescript-buildを追加します。 { typeCheck: true, ignoreNotFoundWarnings: true } ] ] } export default nuxtConfig次に、npmコマンドからtsを動かせるようにするためにpackage.jsonのscriptsを編集します。今までは
nuxt build
などで動きましたがts環境ではnuxt-ts
を使用するのでnpm scripts内のコマンドを書き換えましょう。package.json{ ... "scripts": { "dev": "nuxt-ts --spa", "build": "nuxt-ts build", "start": "nuxt-ts start", "generate": "nuxt-ts generate" ... } ... }ビルド周りの設定は以上なので、
npm run dev
のコマンドを実行しhttp://localhost:3000/
にアクセスしてサイトが表示されたら成功です。Vuex Storeの型定義
次にStoreのtypescript対応になりますが、Storeの型定義に関しては@takepepeさんの ts-nuxtjs-expressを参考にさせていただいてます。圧倒的に感謝です!
NuxtのVuex Storeでやることとしては以下の通りになります。
- typesの準備
- Vuexの型の拡張
- storeの型付け
最初にVuexにおけるtypesの準備をします。Storeはモジュールモードで以下のようなディレクトリ構成にします。
├─store/ ├─ contact/ ├─ index.ts // contactのstoreモジュール本体 └─ type.ts // Storeの型定義ファイルここで、nuxtが型定義のファイルまでstore配下のファイルを自動的にstoreに認識してしまうため
.nuxtignore
ファイルを作り自動的にstoreに認識されないようにします。.nuxtignorestore/**/type.ts次にcontact storeの型定義ファイルを作ります。(記事に収めるるため実際のパラメータよりも少なくしています)
interfaceの名前が略称になってますが、それぞれ S(State)、 G(Getters)、 RG(RootGetters)、 M(Mutations)、 RM(RootMutations)、 A(Actions)、 RA(RootActions)の意味になります。store/contact/type.tsexport interface S { name: string tel: string message: string } export interface G { name: string tel: string message: string isErrName: boolean isErrTel: boolean isErrMessage: boolean } export interface RG { 'contact/name': G['name'] 'contact/tel': G['tel'] 'contact/message': G['message'] 'contact/isErrName': G['isErrName'] 'contact/isEreTel': G['isErrTel'] 'contact/isErrMeassage': G['isErrMessage'] } export interface M { SET_NAME: string SET_TEL: string SET_MESSAGE: string } export interface RM { 'contact/SET_NAME': M['SET_NAME'] 'contact/SET_TEL': M['SET_TEL'] 'contact/SET_MESSAGE': M['SET_MESSAGE'] } export interface A { setName: string setTel: string setMessage: string resetContacts: void sendContacts: void } export interface RA { 'contact/setName': A['setName'] 'contact/setTel': A['setTel'] 'contact/setMessage': A['setMessage'] 'contact/resetContacts': A['resetContacts'] 'contact/sendContacts': A['sendContacts'] }次にVuexの型の拡張に入ります。以下のようにプロジェクトのルートに
types
ディレクトリを作成しVuexの型を拡張していきます。├─types/ ├─ vuex/ ├─ index.d.ts // プロジェクト全体に影響する実際に呼び出されるVuexの型定義 ├─ root.ts // プロジェクト固有のstoreで定義したtypesをVuexにつなぎこむ場所 └─ type.ts // Storeの型定義ファイルindex.d.tsでは主に共通的に使えるtype.tsとプロジェクト固有のルールを含んだroot.tsをインポートします。このプロジェクトでVuexを使う場合このファイルが読み込まれます。
types/vuex/index.d.tsimport './root' import './type'こちらはプロジェクト固有のルールを含んだVuexの型拡張になります。
types/vuex/root.tsimport 'vuex' import * as Contact from '../../store/contact/type' import * as View from '../../store/view/type' declare module 'vuex' { type RootState = { contact: Contact.S viwe: View.S } type RootGetters = Contact.RG type RootMutations = Contact.RM type RootActions = Contact.RA }以下はVuexで共通的に使われる型拡張です。
types/vuex/type.tsimport 'vuex' declare module 'vuex' { type Getters<S, G> = { [K in keyof G]: ( state: S, getters: G, rootState: RootState, rootGetters: RootGetters ) => G[K] } type Mutations<S, M> = { [K in keyof M]: (state: S, payload: M[K]) => void } type ExCommit<M> = <T extends keyof M>(type: T, payload?: M[T]) => void type ExDispatch<A> = <T extends keyof A>(type: T, payload?: A[T]) => any type ExActionContext<S, A, G, M> = { commit: ExCommit<M> dispatch: ExDispatch<A> state: S getters: G rootState: RootState rootGetters: RootGetters } type Actions<S, A, G = {}, M = {}> = { [K in keyof A]: (ctx: ExActionContext<S, A, G, M>, payload: A[K]) => any } interface ExStore extends Store<RootState> { getters: RootGetters commit: ExCommit<RootMutations> dispatch: ExDispatch<RootActions> } type StoreContext = ExActionContext< RootState, RootActions, RootGetters, RootMutations > }最後にtsconfig.jsonのfilesにtypes/vuex/index.d.tsをfilesに記述します。基本的に独自で拡張した型ファイルがあれば、随時tsconfigに追加すようにしましょう。
tsconfig.json"files": [ "src/types/vuex/index.d.ts" ],ここまで、Storeの型定義をしてきましたが、ここからは実際のStoreに型を付けていきます。先ほど配置した
store/contact/index.ts
に、定義した型を当てていきます。1行目のvuexのimportで、先ほどtypes/vuex/index.d.tsで拡張したGtters、Mutations、Actionsの型を取り込みます。2行目ではstore/contact/type.ts
のinterface宣言した型を取り込んでいます。GettersやActions内にあるserviceディレクトリから取り込んでいるのは、ビジネス要件を含んだ純関数になっています。
store/contact/index.tsimport { Getters, Mutations, Actions } from 'vuex' import { S, G, M, A } from './type' import { validName, validTel, validMessage } from '../../service/validation' import submitContact from '../../service/Contact' export const state = (): S => ({ name: '', tel: '', message: '' }) export const getters: Getters<S, G> = { name: state => state.name, tel: state => state.tel, message: state => state.message, isErrName({ name }) { return validName(name) }, isErrTel({ tel }) { return validTel(tel) }, isErrMessage({ message }) { return validMessage(message) } } export const mutations: Mutations<S, M> = { SET_NAME(state, payload) { state.name = payload }, SET_TEL(state, payload) { state.tel = payload }, SET_MESSAGE(state, payload) { state.message = payload } } export const actions: Actions<S, A, G, M> = { setName({ commit }, payload) { commit('SET_NAME', payload) }, setTel({ commit }, payload) { commit('SET_TEL', payload) }, setMessage({ commit }, payload) { commit('SET_MESSAGE', payload) }, resetContacts({ commit }) { commit('SET_NAME', '') commit('SET_TEL', '') commit('SET_EMAIL', '') commit('SET_COMPANY', '') commit('SET_MESSAGE', '') }, sendContacts({ state }) { const { company, name, email, tel, message } = state return submitContact({ name, tel, message }) } }Storeの型定義と型付けに関しては以上で、あとはいつも通りVueコンポーネントでmapGettersやmapActionsでStoreの内容を取り込むだけで型安全なStoreを扱うことができます。モジュールモードになっているので、別のStoreを導入し型安全にしていきたい場合は同様の手順を踏んでいくと良いと思います。
ここで書いてあるtypescriptのコードはあくまで一例なのでプロジェクトにあった型定義を模索していければと思います。StoreとService層ついて
NuxtでもVueのプロジェクトでもビジネスルールを含んだロジックの記述場所は悩みのタネの一つだと思います。コンポーネント側に寄せるのか、それともStoreに集約されるのか、両方使うならどれくらStoreに寄せるべきかなどなどあると思います。よく、
Storeの肥大化
なんて言われるかと思います。とはいえ、コンポーネントにロジックが並んでいるのもViewとロジックが同居してしまい混乱の原因になり得ます。では、Storeの肥大化を防ぎながらどうやってビジネスルールを含んだロジックをVuexで管理するか、というところになります。僕の意見としては、「ロジックの網羅性」と「ビジネス固有の複雑さ」を役割分担することが大事だとおもってます。
ここでいうと、Storeがロジック(ユースケース)の網羅性を担保し、Service層でビジネス固有の複雑性を含むことです。例えば先ほどのStoreの型をつけているところでいうと、Gettersを宣言しているisErrNameやisErrTelなどのreturn部分で、validNameやvalidTelなどがあります。これがServiece層の関数になっています。ここでは、Storeはこのロジックの中身を知りません。しかしながら、Storeは画面側で必要なプロパティやActionsをもつべきなので、このようにしています。
export const getters: Getters<S, G> = { name: state => state.name, tel: state => state.tel, message: state => state.message, isErrName({ name }) { return validName(name) // ValidNameがService層 }, isErrTel({ tel }) { return validTel(tel) }, isErrMessage({ message }) { return validMessage(message) } }こちらがそのService層の中身です。ここではあくまでとてもシンプルな例を出してますが、アプリケーション固有のロジックを以下のように値を受け取って出力するだけの純関数にすることで、簡単に使い回せますしVuexのコードを汚すことなく済みます。
type ValidType = (value: string) => boolean export const validName: ValidType = value => { const isErr: boolean = value.length < 4 // 名前が4文字以内だと弾かれるというルール return isErr }StoreとService層とで分割することで、Vuexのことを気にせずこの関数の中身自体をスケールさせることも分割することも用意です。
また、純関数なので非常にテストがしやすく、Vuex以外の技術に移行しやすくその時のFWなどに依存しずらいというメリットもあります。(そもそもビジネスがFWの流行り廃りに左右されるのは本望ではないかと思います)Service層が割とutils層みたいな部分と区別がつかないみたいな話もありますが、僕個人の意見としてはutils層は
アプリケーション固有の情報を含まず共通して使えるもの
と認識しております。
例えば、axiosをラップした外部リソース取得用のモジュールなどです。それ自体は、ビジネスルールを含む訳ではないのでutils層におきます。ただそこから、特定のAPIを叩くための関数だったりプロジェクト固有の独自キーを用いたストレージへのアクセスとなるとそれを取り込んだ上でService層で定義します。学んだこと
サーバーレス&generateサイトを1年間運用してみて
サーバーレス&静的サイトジェネレートということでNuxt + Netlifyで1年間サイトを運用してみて、非常に楽だったなという思いです。動的コンテンツが少なくちょっとしたお問い合わせフォームだけで複雑な実装の画面などはなかったので静的コンテンツの管理だけで済みました。
また、料金面でもドメイン取得料だけだったので総額の出費も3000円程度で済みました。Netlifyに限った話ではなく、Amazon S3にアップするだけでも良いですし、若干レガシー環境だとレンタルサーバーに書き出したファイルを設定するだけでサイト作成がほぼ完了したりします。またNuxtなので、静的ファイルも書き出せつつコンポーネント志向開発で出来るという強みは非常に大きかったです。見えてきた課題とアーキテクチャを考える上で大事なこと
という主語がデカめのタイトルになってしまって申し訳ないですが、「配置すること」って結構重要だと思っていてディレクトリ構成やなんの技術をどう使うか、という話は後の運用に大きく影響し導入時に大部分が決まる、みたいなとこはあると思います。
実際に、typescript移行は影響範囲が全体でコンポーネントやStore全てに影響が出たので、最初からTSにすればよかったんや!!って思うことがありました。また、それによって必要なモジュールも増えるので決めるべき時にしっかり決めるべきだなと感じました。
その上で設計や技術選定で気をつけたいなと思ったことは「壊しやすいか」と「組織あるいはチームの構造はどんなか」です。壊しやすさについて
壊しやすさは「依存度を下げる」みたいな意味合いも入っていて、ある特定の技術がなければ作れないあるいは作り直したほうが早いみたいな状況です。
いかに既存の資産を活かせる形をとれるかが重要になってくるんですが、先ほどのStoreとService層の話はまさにそれです。
Service層はビジネス固有のルールを含んでいますが、Vuexに対して依存しないようにしています。そうすることによって、最悪VueやVuexが使えない状態になったとしても、そのServiceの純関数群はそのまま他のFWでも扱うことが出来ます。(あくまで「壊しやすい形」のほんの一例です)おそらく、フレームワークのライフサイクルよりも事業のライフサイクルの方がおそらく長い(事業による)かと思います。また、プロダクトやサービスに与える影響の大きさは、フレームワークではなくマーケットの動きだったりユーザーフィードバックの方が大きいはずなので、それに合わせていかに変化(壊しやすく)出来るかが重要、という感じです。
組織あるいはチームの構造はどんなか
こちらは、コンウェイの法則でもあるんですが
The structure of any system designed by an organization is isomorphic to the structure of the organization.とあるので、出来上がるシステムの構造は設計する組織の構造に依存する、と解釈できます。
設計する時の組織構造は何か、どんなメンバーがいるのか、どんなチーム構成なのかという部分がシステムやアプリケーションに影響します。加えて、そのサービスやプロダクトが将来的にどういった方向に行きたいのか、どういった形態になりうるのか、未来の組織の様子を踏まえて設計することが必要です。つまり、未来のチームの姿と現在との差分を定義した上でアーキテクチャを考えると良さそうだなと、思ってます。まとめ
- Nuxt typescrptおすすめ!
- 静的サイトジェネレート+ホスティングで運用コストを減らそう
- 設計は「壊しやすいか」と「組織あるいはチームの構造はどんなか」を考慮する
ここまで読んでいただき誠に感謝です。
前章で組織構造大事やぞ、と言いつつ今回のNuxtのサイトは僕一人で作ったので、今回に関しては組織構造とかないですが技術に依存し過ぎずでもそれの旨味は味わいつつ、という風にやっていけそうな気はするのでこれからも運用していきます。以下、本記事のまとめになります。今回実際に刷新したサイトのリポジトリをのせていますのでご参考にしていただければと思います。
https://github.com/isihigameKoudai/bright-and-dizain参考
以下、参考リポジトリや資料になります。ありがとうございます!
- https://typescript.nuxtjs.org/
- https://github.com/takefumi-yoshii/ts-nuxtjs-express
- https://ja.wikipedia.org/wiki/%E3%83%A1%E3%83%AB%E3%83%B4%E3%82%A3%E3%83%B3%E3%83%BB%E3%82%B3%E3%83%B3%E3%82%A6%E3%82%A7%E3%82%A4
- 投稿日:2019-12-03T23:02:28+09:00
正規表現を理解してみる
はじめに
これは、静岡 Advent Calendar 2019の3日目の記事となります。
今年は勉強会には2019/1/26(土)に「DevLOVE静岡」しか参加していないので勉強会のことは書きにくい。
一応、Qiitaは技術系ブログなので静岡にまつわる(名産品や地名など)なら何でもいいかなと、ネタとして2012年に静岡Developers勉強会で「JavaScriptの正規表現」のセッションを引っ張り出して、これをQiita用に書き直してみました。正規表現
正規表現ってなに?
正規表現とは、簡単に言えば 検索や置換をより便利にするものです。
例えば文章中に 「りんご」 と 「みかん」 を検索したいとき、別々に検索するのは手間だし面倒です。
正規表現で検索に使うキーワードを以下のように書くとりんご|みかん「りんご」と「みかん」を同時に検索する事が出来ます。
javascript 正規表現 チートシート
http://visibone.com/products/bbk14-15_850.jpgオンライン上の正規表現の確認サイト
正規表現で使用する特殊文字
「|」を使うと複数キーワードで検索できるように、下記の特殊文字には役割がある。
¥ / [ ] - ( ) ? + * | . ^ $
正規表現では「メタ文字」と呼んでいる。
メタ文字自身を検索対象とする場合、¥を付ける例 ¥¥ ,¥[ , ¥+任意の1文字と位置指定
「.」…とにかくなんでもいい1文字(改行以外)
正規表現 み.ん
対象文字 みかん みしん みりん「^」…先頭にマッチ 呼び名:ハット
正規表現 ^みかん
対象文字 みかん みかん みかん「$」…末尾にマッチ
正規表現 みかん$
対象文字 みかん みかん みかん同じ文字の繰り返し
「*」…直前の文字を 0 回以上にマッチ
正規表現 おー*い ※数量詞表現: おー{0,}い
対象文字 おい おーい おーーい「+」…直前の文字を 1 回以上にマッチ
正規表現 おー+い ※数量詞表現: おー{1,}い
対象文字 おい おーい おーーい「?」…直前の文字を 0 回又は 1 回にマッチ
正規表現 おー?い ※数量詞表現: おー{0,1}い
対象文字 おい おーい おーーい「.*」 …なんでもいい文字の連続(最長)
正規表現 JR.*駅
対象文字 JR静岡駅から浜松駅まで
※可能な限り合致するまでヒットする「.*?」…なんでもいい文字の連続(最短)
正規表現 JR.*?駅
対象文字 JR静岡駅から浜松駅まで
※可能な限り少ない回数でヒットするパターン論理和とパターングループ
「|」…いずれかの文字列(論理和)
正規表現 みかん|りんご
対象文字 ばなな みかん ぶどう りんご「()」…グループ化(先優先)
()を付けた場合の違い
正規表現 私はみかん|りんごが好きです
対象文字 私はみかんが好きです
対象文字 私はりんごが好きです正規表現 私は(みかん|りんご)が好きです
対象文字 私はみかんが好きです
対象文字 私はりんごが好きです数詞とパターングループ
「{}」…数量詞
正規表現 w{3} 直前の文字を n 回にマッチ
対象文字 www.ora.com/goodparts/ww正規表現 w{2,} 直前の文字を n 回以上にマッチ
対象文字 www.ora.com/goodparts/ww正規表現 w{0,3} 直前の文字を n 回以上、m 回以下にマッチ
対象文字 www.ora.com/goodparts/ww「()」…グループ化
正規表現 (じゃ)+ーん
正規表現 (じゃ){1,}ーん ※数量詞に置き換えた場合
対象文字 じゃーん じゃじゃーん じゃじゃじゃーん数量詞の補足
繰り返し回数
正規表現 (みかん){2,4}
対象文字 みかんみかんみかんみかんみかん
※指定した回数より多く繰り返している場合には 繰り返し回数の上限までマッチする。正規表現 (みかん){2,4}
対象文字 みかんみかんみかん
※可能な限り多い回数でマッチする。正規表現 (みかん){2,4}
対象文字 みかんみかんみかんみかんみかんみかん
※5 個以上は、パターンの繰り返しでマッチする。文字クラス
「[]」…指定内の任意表現 呼び名:ブラケット
正規表現 今日の天気は[晴曇雨]です
対象文字 今日の天気は晴です
対象文字 今日の天気は曇です
対象文字 今日の天気は雨です
対象文字 今日の天気は雪です「[^]」…指定内の任意以外表現
正規表現 今日の天気は[^晴曇雨]です (^が先頭)
対象文字 今日の天気は雪です※[]の中で ^ が使用された場合は、行の先頭を表す ^(ハット)とは意味が異なります。
[] と (|) の使い方を間違えた場合
例 [りんご|みかん]
「り」「ん」「ご」「|」「み」「か」「ん」のどれか一文字という意味になる。
文字クラス内の記述
- 「[A-Z]」…英大文字 A から Z までのどれか
- 「[a-z]」…英小文字 a から z までのどれか
- 「[0-9]」…数字 0 から 9 までのどれか
- 「[A-Za-z0-9]」…上記を連続して書ける
- 「[あ-お]」…あぃいぅうぇえぉお のどれか
- 「[か-こ]」…かがきぎくぐけげこ のどれか
- 「[.*]」…. か * のいずれかの文字 ※[]の中ではメタ文字は普通の文字として認識される文字もメタ文字ではない。 (但し、先頭の^や ] や範囲指定の‐は例外)
主な特殊文字
- 「¥w」…英字、数字、アンダースコア。 [a-zA-Z0-9_] と同じ
- 「¥W」…英字、数字、アンダースコア以外の文字。 [^a-zA-Z0-9_]と同じ
- 「¥d」…数字。[0-9] と同じ
- 「¥D」…数字以外の文字。[^0-9]と同じ。
- 「¥n」…改行
- 「¥s」…スペース(全角スペースも対象)
- 「¥S」…スペース(全角スペースも対象)以外の文字
主な正規表現の使用例
日付を検索する
正規表現 \d{4}\/\d{1,2}\/\d{1,2}
対象文字 2019/12/20 2019/12/2郵便番号を検索する(厳密ではない)
正規表現 \d{3}-\d{4}
対象文字 012-1234メールアドレスを検索する(厳密ではない)
正規表現 ^[\w_-]+@[\w¥.-]+.\w{2,}$
対象文字 microsoft@e-mail.microsoft.com正規表現の修飾子
「/g」…Global (複数回マッチする)
正規表現 /みかん/g
対象文字 みかん りんご みかん「/i」…Insensitive (大文字小文字を区別しない)
正規表現 /ABC/i
対象文字 abcdef「/m」… Multiline (^ と $ が行末記号にマッチ)
正規表現 /456/m
対象文字 123¥n456¥n789
※¥n:改行正規表現グループ
多くの正規表現エンジンでは「( )」を使ったキャプチャ(マッチした文字列の捕捉)をサポートしています。「$(番号)」でマッチした文字列を順番に取得できます。
キャプチャ
xx = "12:34:56".match(/(\d+):(\d+):(\d+)/); document.write(RegExp.$1 + "<br>"); // → 12 document.write(RegExp.$2 + "<br>"); // → 34 document.write(RegExp.$3 + "<br>"); // → 56非キャプチャ
「(?:)」を使うことでキャプチャを外します。
xx = "12:34:56".match(/(?:\d+):(\d+):(\d+)/); document.write(RegExp.$1 + "<br>"); // → 34 document.write(RegExp.$2 + "<br>"); // → 56肯定先読み
リンカーンの「人民の人民による人民のための政治」というフレーズ
正規表現 人民
対象文字 人民の人民による人民のための政治
人民が3つ抽出されました。肯定先読みを使用します。
正規表現 人民(?=による)
対象文字 人民の人民による人民のための政治人民が3つありますが、肯定先読み(?=による)をつけることで「による」が含まれる「人民」だけがマッチしたということです。ここでポイントなのは、マッチした「による」が結果に含まれていないことです。
肯定先読みは後に特定の言葉を含んでいる条件を満たす文字列のみを抽出したいという時に使えます。参照:肯定先読み~否定後読み
否定先読み
肯定先読みの逆になります。「による」が含まれる「人民」以外が抽出されます。
正規表現 人民(?!による)
対象文字 人民の人民による人民のための政治文字列の否定
[^ABC]*? … 文字1つ1つを除外
正規表現 JR[^静岡]*?駅
対象文字 JR静岡駅 JR浜松駅 JR三島駅 JR岡山駅
これだとJR岡山駅があると対象外となる((?!ABC).)? … 文字列を除外 (?!ABC).?に変更
正規表現 JR((?!静岡).)*?駅
対象文字 JR静岡駅 JR浜松駅 JR三島駅 JR岡山駅
これだとJR東静岡駅を含めた場合、JR東静岡駅も対象外となってしまう最後に
当初は静岡のIT勉強会について何か書こうと思ったのですが、あまり過去を振り返っても仕方ないし未来志向にしようと。
ここ数年は低活動状態なので勉強会を検索するとかもしてなかったので、いつの間にか静岡でも結構勉強会やってたんだなというのを記事を書く上で検索して今頃知るくらいです。行こうかなと思ったら出勤日でした残念。
"11月か12月を目処に「人工知能ハンズオン リベンジ」として勉強会を開催しよう"と思ってたのですが、まだ知識が足りていないことと、仕事でサーバーリプレース作業をしていまして、これが来年6月までに月2,3の土曜出勤(日曜日は予備日)となり実施日も2週間前くらいでないと確定しないので、延び延びになっています。
自分的は絶対やりたいので来年のいつぞやまでお待ちください。
- 投稿日:2019-12-03T22:56:22+09:00
Figmaプラグイン開発のデバッグ方法
はじめに
こちらはFigmaプラグインAdvent Calendar 2019の3日目の記事です。
Figmaプラグイン開発を始める上で、使えると便利なデバッグの方法をご紹介します。
基本的には以下に載っている方法の紹介です。
https://www.figma.com/plugin-docs/debugging/デバッグ方法
早速デバッグ方法を紹介していきます。
1. コンソール出力
実際の開発でとりあえずコンソールに値を出力してみる、ということがあると思います。Figmaプラグインでも同様のことを行うことができます。
今回は1日目のfigmaプラグイン開発の準備の内容を使ってコンソール出力を利用してみましょう。
step1. console.logをコードに書き込む
出力させたい箇所でconsole.logを書きましょう。
console.log(nodes);
step2. figma.closePlugin()をコメントアウトする
これはプラグインの処理が終了してしまうと、オブジェクトに(...)という部分があるのですが、そこがErrorで読み取ってくれません。プラグインを終了させないように、figma.closePlugin()をコメントアウトしましょう。
※figma.closePlugin()をコメントアウトしていないので、Errorになってしまっている図
↓step1,2を書いたコード
code.ts// This plugin will open a modal to prompt the user to enter a number, and // it will then create that many rectangles on the screen. // This file holds the main code for the plugins. It has access to the *document*. // You can access browser APIs in the <script> tag inside "ui.html" which has a // full browser enviroment (see documentation). // This shows the HTML page in "ui.html". figma.showUI(__html__); // Calls to "parent.postMessage" from within the HTML page will trigger this // callback. The callback will be passed the "pluginMessage" property of the // posted message. figma.ui.onmessage = msg => { // One way of distinguishing between different types of messages sent from // your HTML page is to use an object with a "type" property like this. if (msg.type === 'create-rectangles') { const nodes: SceneNode[] = []; for (let i = 0; i < msg.count; i++) { const rect = figma.createRectangle(); rect.x = i * 150; rect.fills = [{type: 'SOLID', color: {r: 1, g: 0.5, b: 0}}]; figma.currentPage.appendChild(rect); nodes.push(rect); } console.log(nodes) //nodesを出力してみる figma.currentPage.selection = nodes; figma.viewport.scrollAndZoomIntoView(nodes); } // Make sure to close the plugin when you're done. Otherwise the plugin will // keep running, which shows the cancel button at the bottom of the screen. // figma.closePlugin(); // コメントアウトする };step3. OpenConsoleを開く
次にFigmaのOpenConsoleを開きましょう。
Figmaのメニュー → Plugins → Development → Open Consoleをクリック
step4. 実行
では実行してみましょう。
Figmaのメニュー → Plugins → Development → プラグイン名
実行するとConsoleタブに出力結果が表示されます。step5. プラグインの終了
figma.closePlugin()をコメントアウトしているので、処理が終了されません。コンソール出力を確認し終えたら、figma.closePlugin()をConsoleで実行しましょう。
処理が終了します。
2. DeveloperVM
こちらを使うとGoogleChromeのディベロッパーツールと同じようにステップ実行などができます。複雑な開発をする上では欠かせない方法ですね!
step1. Debuggerの埋め込み
ソースコードの止めたい箇所にdebuggerを埋め込みましょう。
code.ts// This plugin will open a modal to prompt the user to enter a number, and // it will then create that many rectangles on the screen. // This file holds the main code for the plugins. It has access to the *document*. // You can access browser APIs in the <script> tag inside "ui.html" which has a // full browser enviroment (see documentation). // This shows the HTML page in "ui.html". figma.showUI(__html__); // Calls to "parent.postMessage" from within the HTML page will trigger this // callback. The callback will be passed the "pluginMessage" property of the // posted message. figma.ui.onmessage = msg => { // One way of distinguishing between different types of messages sent from // your HTML page is to use an object with a "type" property like this. if (msg.type === 'create-rectangles') { const nodes: SceneNode[] = []; debugger; // 止めたいところでdebuggerを書く for (let i = 0; i < msg.count; i++) { const rect = figma.createRectangle(); rect.x = i * 150; rect.fills = [{type: 'SOLID', color: {r: 1, g: 0.5, b: 0}}]; figma.currentPage.appendChild(rect); nodes.push(rect); } figma.currentPage.selection = nodes; figma.viewport.scrollAndZoomIntoView(nodes); } // Make sure to close the plugin when you're done. Otherwise the plugin will // keep running, which shows the cancel button at the bottom of the screen. figma.closePlugin(); };step2. DeveloperVMをオンにする
DeveloperVMはデフォルトではオフになっています。
Figmaのメニュー → Plugins → Development → Use Developer VMをクリックしてオンにします。※「Use Developer VM」にチェックがつけばOK
step3. 実行
では実行してみましょう。
Figmaのメニュー → Plugins → Development → プラグイン名あとはディベロッパーツールと同じようにいろいろ確認してみましょう!
最後に
以上2つのデバッグ方法を紹介しました。
デバッグを駆使して、Figmaプラグイン開発をしていきましょう!どなたかの助けになれば嬉しいです。ありがとうございました!
- 投稿日:2019-12-03T22:48:14+09:00
【自分用メモ】supertestとpassport-stubをmochaテストに組み合わせる
supertestとは
supertestはmochaと組み合わせて使うのですが、ExpressのRouterモジュールのテストを行うことができます。
例えば以下の例では、/
にアクセスしたらindexRouterが処理されるかテストしてくれます。
もちろん、/login
も/logout
もテストしてくれます。app.jsapp.use('/', indexRouter); app.use('/login', loginRouter); app.use('/logout', logoutRouter);passport-stubとは
passport-stubは、passportモジュールを利用した認証システムを、テストする際に役に立ちます。
例えば、
「facebook認証などのテストをしたいけど、facebookアカウントを持っていない!」
といった時に役に立ちます。テストの例
test.js//supertestの読み込み const request = require('supertest'); //supertestで使う、app.jsの読み込み const app = require('app'); //passport-stubの読み込み const passportStub = require('passport-stub'); //ログイン(/login)のテストであることを明示 describe('/login', () => { //before、afterはmochaの機能 before(() => { //テストの前にpassportstubモジュールでログイン passportStub.install(app); //'testuser'としてログイン passportStub.login({ username: 'testuser' }); }); after(() => { //テストの後にpassportstubモジュールでログアウト passportStub.logout(); passportStub.uninstall(app); }); //以下の記法は、supertestの記法 //テストの内容を指定 it('ログインのためのリンクが含まれる', (done) => { //request(app).get('/login') で、 /login への GETリクエストを作成 request(app) .get('/login') //文字列を2つ引数として渡すとヘッダのテスト .expect('Content-Type', 'text/html; charset=utf-8') //正規表現を1つ渡すとHTMLのテスト .expect(/<a href="\/auth\/facebook"/) //期待されるステータスコードの整数と、テスト自体の引数に渡されるdone 関数を渡すと、レスポンスヘッダのテスト .expect(200, done); }); it('ログイン時はユーザー名が表示される', (done) => { request(app) .get('/login') .expect(/testuser/) .expect(200, done); }); }); //ログアウト(/logout)のテストであることを明示 describe('/logout', () => { //テスト内容を明示 it('ログアウト後に / にリダイレクトされる', (done) => { ////request(app).get('/logout')で、/logoutへのGETリクエストを作成 request(app) .get('/logout') // `/`へリダイレクトされるかのテスト .expect('Location', '/') // ステータスコードがリクエストであるかのテスト .expect(302, done); }); });
describe
、it
、before
、after
はmochaの書き方。
request(app).get
、.expect
はsupertestの書き方
passportStub.install(app)
、passportStub.login
はpassport-stubの書き方テスト結果
- 投稿日:2019-12-03T22:24:42+09:00
JavaScriptの基本箇所についての復習:寄り道(変数var)
はじめに
JSの変数宣言には3つの方法があります。
var
,let
,const
です。
これから3回にわたって、この3つの違いについて解説したいと思います。
最初はvar
です。実行
var
は昔からある書き方のようで、宣言した関数の中でしか使えない変数です。
次の関数を実行すると、意味がわかると思います。var name = "suzuki"; function Say() { var name = "satou"; console.log(name); } Say(); console.log(name);まず
name
にsuzuki
を代入し、その次にSay
関数を定義します。
Say
関数では、最初にname
にsatou
を代入し、console.log
で出力する、という処理を行います。
関数を定義した後は、Say
を実行し、最後にname
の中身をconsole.log
で出力します。この一連の処理を行うと、どういう風に出力されるでしょうか?
まず一番最初に出力を行うのは、Say();
の部分です。関数Say
では、前述の通りnameにsatouを代入して、それを出力するという処理を行うので、ここではsatou
と出力されそうです。
その後のconsole.log(name);
との間で、name
に新しい値を代入してないので、ここでもsatou
と出力されそうな気がします。つまり、satou satou
という出力結果になりそうです。
しかし実際は予想と違って、satou suzuki
と出力されます。
Say();
を実行した時に、一度var name = "satou"
とし、実際にその通りに出力されているのに、Say()*
の処理が終わった瞬間、name
が最初に代入されたsuzuki
に戻っています。これこそが、varで宣言した変数は、関数の中でしか使えない変数の具体例になります。
var name = "satou"
と宣言したのは関数Say
の中なので、nameにsatouが代入されている事は、関数Sayの処理を行っている時だけであり、関数Sayの処理が終わればnameは元に戻るという事になります。
今回の関数でいえばSay関数の処理が終われば、nameは最初に代入したsuzukiに戻るという事になります。基本的な部分ですが、この事を知らないと「ちゃんと変数を上書きしたのに!?」と慌てることになるので、しっかり覚えておきましょう。
- 投稿日:2019-12-03T22:01:55+09:00
メモ
ストロングパラメータ
指定したキーのみを受け取るので、データをDBに入れるかどうかを判別させるために使う。
ex params.permit(:キー名,キーの値)java script
list.push(***); 配列に要素を追加する。
list.pop(); 配列の最後の要素削除
⇅ 逆の関係
list.shift(); 配列の最初の要素削除オブジェクトの作成
let object = {};関数を使うとき
returnを記述する。
returnは関数内で処理をした結果戻り値として返す。関数とは
入力された 値に対して 出力して 返すこと引数 → 関数 → 戻り値
function calc(num1,num2){
return num1*num2;
}let num1 = 5;
let num2 = 7;
console.log(calc(num1,num2));DOM (document object model)
htmlを階層上に表現したデータでjava script
body ---------header
|
|
------content
|
|
-------footerbody header content footer をノードと言う
- 投稿日:2019-12-03T21:43:24+09:00
Three.jsで遊んでみよう
「GLSLの記事書いておいて、今さらWebGLのライブラリかよ!」というツッコミは置いておいて
実はオライリーのThree.jsの参考書を読んでいる最中なのです。
(第2版の内容はr78ですが、ここではr107で進めていきます。)これからThree.jsを触ってみようという方向けに
流動食のように咀嚼しやすく飲み込みやすく書いていこうと思います。Three.jsとは?
WebGLをイチから書くのは、とんでもなく大変なのです………。
「じゃあ、WebGLを簡単に書けるようにしようじゃなイカ!」
そうして生まれたJavaScriptのライブラリです。WebGLのライブラリは他にもありますが、代表的なものだと
2DはPixiJS、3DはThree.js
といったところでしょうか?そもそもWebGLとは?
すっごく簡単に言うと
インターネットブラウザをプレイステーションにしてしまう魔法の技術
(引用元:http://wise9.jp/archives/6060)もうちょっと詳しくいうと…
ブラウザ上で3DCGプログラミングを実現できる、JavaScriptから利用できるAPI。
ブラウザを通してデバイスのGPUにアクセスすることができます。
(GPU:PCの中で画像処理を専門とした頭脳)(上記ですでに書いてはいますが)
WebGLと聞くと3Dがすぐ思い浮かぶかと思いますが、実はWebGLは3Dのためだけの機能ではなく、2Dでの表現も可能です。Three.jsで3D空間をつくってみる
まず、3D空間をつくる流れを説明します。
とりあえず何が必要でしょう…?
シーン
いわゆるステージ。テレビの撮影スタジオみたいなもの。すべてを保持し監視するコンテナ。カメラ
そのままカメラ。シーンに何を描画するかを決定する。レンダラ
すべてを踏まえ、つくったシーンがどう見えるか計算してくれる。イラストにするとこんな感じでしょうか??
コードはこんな感じ↓↓↓
<head> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/107/three.min.js"></script> </head> <body> <script> var scene; var camera; var renderer; function init() { // まず、シーンをつくる。 scene = new THREE.Scene(); // そして、カメラをつくる。PerspectiveCamera は遠近感があり自然な見た目になります。 // PerspectiveCamera の引数は (視野角, アスペクト比, 近くはどこまで映すか, 遠くはどこまで映すか) camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000); // カメラの位置を指定する。 camera.position.set(-20, -15, -30); // カメラの視点を指定する。この場合はシーンの位置。 camera.lookAt(scene.position); // 最後に、レンダラをつくる。antialias を true にしてあげると輪郭のギザギザが滑らかになりますが、重たくなるのでそこは様子をみて。 renderer = new THREE.WebGLRenderer({ antialias: true }); // デバイスの解像度を指定する。 renderer.setPixelRatio(window.devicePixelRatio); // レンダラのサイズを指定する。 renderer.setSize(window.innerWidth, window.innerHeight); // ボディにレンダラ(canvas)を追加する。 document.body.appendChild(renderer.domElement); // レンダー用関数呼び出し render(); } // レンダー用関数 function render() { // レンダラをシーンに追加する。 renderer.render(scene, camera); // 1フレームごとにループする。 requestAnimationFrame(render); } // リサイズイベント function onWindowResize() { // カメラのアスペクト比を更新する。 camera.aspect = window.innerWidth / window.innerHeight; // カメラのパラメータを変更したら必ず呼び出す。(カメラ投影行列を更新) camera.updateProjectionMatrix(); // レンダラのサイズを更新する。 renderer.setSize(window.innerWidth, window.innerHeight); } window.addEventListener('DOMContentLoaded', init); window.addEventListener('resize', onWindowResize); </script> </body>さて、見てましょう………!とはいかず、見てもエラーにはなりませんが
あくまで空間をつくっただけですので何も映りません。ものを映すために必要なのは
ここからは撮影風景を思い浮かべるとわかりやすいかもです。
ライティング
撮影にはライティングが大事。オブジェクトの配置
頑張って3D空間をつくっても、撮影小物や役者がいないと何も映らないですよね。ここでもイメージイラストを載せておきます…笑
さてさて、以下はオブジェクトをつくるのに必要なもの…それは3つあります。
ジオメトリ
オブジェクトの形状を表す情報。ポリゴン。(頂点や線分、面など)マテリアル
オブジェクトの質感の情報。オブジェクトの皮のようなもの。(色や、金属っぽいのか木っぽいのか…など)メッシュ
ジオメトリとマテリアルの集合体。ここでいうと、オブジェクト=メッシュになります。流れとしては、
① ジオメトリで形状をつくり、マテリアルで質感をつくる。
② 最後にメッシュにこの2つをいれてあげることでオブジェクトが完成します。先ほどのサンプルに、オブジェクトを配置します。
(今回はライトの影響を受けないマテリアルを使用するので、ライトは省きます。)<head> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/107/three.min.js"></script> </head> <body> <script> var scene; var camera; var renderer; function init() { scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(-20, -15, -30); camera.lookAt(scene.position); renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); // 円球のジオメトリ(ポリゴン)をつくる。SphereBufferGeometry の引数は (半径, 横方向を何面にするか, 縦方向を何面にするか) var sphereGeometry = new THREE.SphereBufferGeometry(6, 36, 36); // 円球のマテリアル(質感)をつくる。 var sphereMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 }); // 円球のジオメトリとマテリアルをメッシュにまとめる。 var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial); // シーンに円球を追加する。 scene.add(sphere); document.body.appendChild(renderer.domElement); render(); } function render() { renderer.render(scene, camera); requestAnimationFrame(render); } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } window.addEventListener('DOMContentLoaded', init); window.addEventListener('resize', onWindowResize); </script> </body>確かに円球らしきものは見えましたが、これだけだと面白くはないですよね〜…
…ということで、もう少しゴニョゴニョしたいと思います。<head> <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/107/three.min.js"></script> </head> <body> <script> var scene; var camera; var renderer; // キューブマップ var cubeMap = { path: './assets/cubemap/', urls: [ 'posx.jpg', 'negx.jpg','posy.jpg', 'negy.jpg','posz.jpg', 'negz.jpg' ] } function init() { scene = new THREE.Scene(); // シーンの背景にキューブマップを設定する。 scene.background = new THREE.CubeTextureLoader().setPath(cubeMap.path).load(cubeMap.urls); camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(-20, -15, -30); camera.lookAt(scene.position); renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); var sphereGeometry = new THREE.SphereBufferGeometry(6, 36, 36); // マテリアルに環境マッピングを設定する。(シーンの背景) var sphereMaterial = new THREE.MeshBasicMaterial({ envMap: scene.background }); var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial); scene.add(sphere); document.body.appendChild(renderer.domElement); render(); } function render() { renderer.render(scene, camera); requestAnimationFrame(render); } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } window.addEventListener('DOMContentLoaded', init); window.addEventListener('resize', onWindowResize); </script> </body>これはいわゆる環境マッピングというやつです。
(スカイボックスを作らないでシーンに背景をそのまま貼り付けられるようになったんだ…とか、こっそり感動していました。)ここでは紹介していませんが、OrbitControlsやdat.guiを使うと
マウスでぐりぐり動かせたり、メーターで色や形を変更できるようになります。最後に
Three.jsはどうしても3DCGの知識が伴うので、あまり知らない人にとっては敷居が高いかもしれません。
…が、ちょろっと遊ぶくらいなら簡単にできるので、ぜひとも遊んでいただきたいです。また、誤字や間違いなどがあれば、ご指摘いただけると幸いです。
- 投稿日:2019-12-03T21:33:36+09:00
Expoで作成したアプリをGoogle Playに申請するまで(2019/11時点)
App Store版の記事と同様に、Expoで作成した最低限のアプリをGoogle Playに申請する流れに関してまとめます。
今回私が作成し申請したアプリはこちらのFirebaseとカメラを使用したアプリです。
Google Play Developper登録
Google Playにアプリを公開するには、GoogleアカウントとGoogle Play Developerとしての登録が必要です。
Google Play Developer
https://developer.android.com/distribute/consoleこちらの「ログイン」ボタンから新規登録して支払い情報を入力してください。
App Storeと違う点は年額ではなく最初に$25かかるだけであるというのと、デベロッパー登録のみに関しては審査がないというところです。Google Play Consoleで新規アプリを作成
デベロッパー登録が完了したら、Google Play Consoleを開き、「アプリの作成」ボタン→言語・アプリ名を入力してアプリ情報を作成します。
app.jsonを編集
まずはドキュメントを参考にしながら、app.jsonに必要な情報を追加します。
icon
やsplash
はiOSと共通app.json(部分)"icon": "./assets/icon.png", "splash": { "image": "./assets/splash.png", "resizeMode": "contain", "backgroundColor": "#00c2ad" },Android向けにアイコンなどを追加します。
app.json(部分)"android": { "package": "[パッケージ名]", "adaptiveIcon": { "backgroundColor": "#00c2ad", "foregroundImage": "./assets/adaptiveicon.png" }, "permissions": [ "CAMERA", "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE" ] }, "notification": { "icon": "./assets/notificationicon.png", "color": "#00c2ad" },パッケージ名はiOSのbundleIdentifierと同じように、ドメインを使って世界的に固有のものを使用します。
アダプティブアイコンはAndroid独自のアイコン規格で、ランチャー側でマスク形状を変えたりアニメーションなどをつけることができるものです。
アダプティブアイコンがサポートされている端末においてはこれを設定していないアプリはアイコンの周りに白い余白が出てしまいます(参考)。
前面の画像(foregroundImage)と後景の画像(backgroundColor(デフォルトは白) あるいは backgroundImage)で構成され、
Expoの設定ではforegroundImageに1024x1024pxの透明pngファイル、背景色(backgroundColor)あるいはbackgroundImageに1024x1024pxのpngファイルを指定すればOKです。私が作成したforegroundImageは以下の画像ですが、このように中央に要素を配置するようなデザインの場合、様々な形状に合わせるためにある程度余白を取っておく必要があります。Expoではデフォルトでは全ての権限への許可が設定されていて、そのままだとインストールする際に必要以上の権限をユーザーに求めてしまうことになりますので、
permissions
に必要な分だけ記載しましょう。
今回私が申請したアプリはカメラとFirebase(内部的にAsyncStorageを使用)を使用するので、CAMERA
、READ_EXTERNAL_STORAGE
およびWRITE_EXTERNAL_STORAGE
を許可しています。通知アイコンについてはこちらの記事を参考にしてください。
ビルドする
app.jsonに必要な設定を追加したら、Android Studioをインストールした上で、Android向けにビルドします。
$ expo build:android
最初にビルドする際は、このように証明書(keystore)を新しく生成するか既存のものを使うかどうかの選択肢が出てきます。
[exp] No currently active or previous builds for this project. Would you like to upload a keystore or have us generate one for you? If you don't know what this means, let us handle it! :) 1) Let Expo handle the process! 2) I want to upload my own keystore!通常は1)でそのままExpoに任せてしまって大丈夫です。ただし、後々アップデートする際には同じkeystoreが必要なので、(これも同じ環境で開発する場合は通常意識しなくても大丈夫ですが)keystoreを安全な場所にバックアップして置くことが推奨されています。
$ expo fetch:android:keystore上記のコマンドを使うと、keystoreの情報が表示され、ディレクトリに.jksファイルが生成されます。
これをpublicなリポジトリにアップしたりしないよう注意して保存しましょう。ビルドが完了し、apkファイルへのリンクが表示されたらダウンロードし完了です。
アプリ情報を入力する
Google Play Consoleを開き、アプリ情報を入力します。
右側メニューのチェックマークが付いている部分を順に入力していきます。
まずは「ストアの掲載情報」で基本的な情報を埋めていきましょう。
タイトルや説明文などのほか、最低限以下のものが必要です。
- アイコン→ 512x512pxのPNG画像(Google Playで表示されます)
- スクリーンショット→ 1辺の長さ320px~3840pxのJPGまたはPNG画像 少なくとも2種類
- フィーチャーグラフィック(ストアで大きめに表示されるバナー)→ 横1024 x 縦500のJPGまたはPNG画像(↓スクショを使って適当に作っておきました)
![]()
- プライバシーポリシー→ 今回はURLを送信しないというチェックがありますが、Expoでは必須(広告IDを送信しているため)
プライバシーポリシーは気になりますが、App Store版の記事と同じくそれほど厳しい感じではない気がします(もちろんそれなりのサービスを運用するのであれば別ですが)。
「価格と配布」で価格、配布する国、同意事項などを入力します。ここで広告を含むかどうかを選択したら、「アプリのコンテンツ」でターゲット年齢層を入力することができます。
残るは「コンテンツのレーティング」と「アプリのリリース」ですが、「コンテンツのレーティング」でアンケートに回答するにはapkファイルをアップロードする必要があります。
「アプリのリリース」で任意のトラック(そのまま公開する場合は製品版)から「管理」→「リリースを作成」し、ドロップエリアにapkファイルをドラッグ&ドロップしてアップロードします。アップロードが完了し、「コンテンツのレーティング」で全ての質問に答えたら、全ての必要な情報が入力できたことになります。
その後先ほど作成したリリース画面から確認→公開へ進めばアプリの申請が完了します。審査
申請が完了すると、ダッシュボードには公開中と表示されますが、しばらくすると以下のように審査中と表示されるようになります。
今までにExpoで作成したアプリをアップデートを含め3回申請しましたが、問題ない場合でも審査の完了には数時間から数日と幅があるような気がします。
そして無事に審査が完了したら、このようにIARCのレーティングを知らせるメールでもって伝えられます。
公開されたアプリはGoogle Play Consoleのダッシュボード→「GOOGLE PLAYで表示」で確認できます。
以上、Google PlayでのExpoアプリ申請の大まかな流れでした。
App Storeでの申請のようにアップロード作業などが縛られていないのと、最低限用意するスクリーンショットが少ないため比較的簡単に感じるのではないでしょうか。
- 投稿日:2019-12-03T21:33:36+09:00
Expoで作成したアプリをGoogle Playに申請するまで(2019/11~12時点)
App Store版の記事と同様に、Expoで作成した最低限のアプリをGoogle Playに申請する流れに関してまとめます。
今回私が作成し申請したアプリはこちらのFirebaseとカメラを使用したアプリです。
Google Play Developper登録
Google Playにアプリを公開するには、GoogleアカウントとGoogle Play Developerとしての登録が必要です。
Google Play Developer
https://developer.android.com/distribute/consoleこちらの「ログイン」ボタンから新規登録して支払い情報を入力してください。
App Storeと違う点は年額ではなく最初に$25かかるだけであるというのと、デベロッパー登録のみに関しては審査がないというところです。Google Play Consoleで新規アプリを作成
デベロッパー登録が完了したら、Google Play Consoleを開き、「アプリの作成」ボタン→言語・アプリ名を入力してアプリ情報を作成します。
app.jsonを編集
まずはドキュメントを参考にしながら、app.jsonに必要な情報を追加します。
icon
やsplash
はiOSと共通app.json(部分)"icon": "./assets/icon.png", "splash": { "image": "./assets/splash.png", "resizeMode": "contain", "backgroundColor": "#00c2ad" },Android向けにアイコンなどを追加します。
app.json(部分)"android": { "package": "[パッケージ名]", "adaptiveIcon": { "backgroundColor": "#00c2ad", "foregroundImage": "./assets/adaptiveicon.png" }, "permissions": [ "CAMERA", "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE" ] }, "notification": { "icon": "./assets/notificationicon.png", "color": "#00c2ad" },パッケージ名はiOSのbundleIdentifierと同じように、ドメインを使って世界的に固有のものを使用します。
アダプティブアイコンはAndroid独自のアイコン規格で、ランチャー側でマスク形状を変えたりアニメーションなどをつけることができるものです。
アダプティブアイコンがサポートされている端末においてはこれを設定していないアプリはアイコンの周りに白い余白が出てしまいます(参考)。
前面の画像(foregroundImage)と後景の画像(backgroundColor(デフォルトは白) あるいは backgroundImage)で構成され、
Expoの設定ではforegroundImageに1024x1024pxの透明pngファイル、背景色(backgroundColor)あるいはbackgroundImageに1024x1024pxのpngファイルを指定すればOKです。私が作成したforegroundImageは以下の画像ですが、このように中央に要素を配置するようなデザインの場合、様々な形状に合わせるためにある程度余白を取っておく必要があります。Expoではデフォルトでは全ての権限への許可が設定されていて、そのままだとインストールする際に必要以上の権限をユーザーに求めてしまうことになりますので、
permissions
に必要な分だけ記載しましょう。
今回私が申請したアプリはカメラとFirebase(内部的にAsyncStorageを使用)を使用するので、CAMERA
、READ_EXTERNAL_STORAGE
およびWRITE_EXTERNAL_STORAGE
を許可しています。通知アイコンについてはこちらの記事を参考にしてください。
ビルドする
app.jsonに必要な設定を追加したら、Android Studioをインストールした上で、Android向けにビルドします。
$ expo build:android
最初にビルドする際は、このように証明書(keystore)を新しく生成するか既存のものを使うかどうかの選択肢が出てきます。
[exp] No currently active or previous builds for this project. Would you like to upload a keystore or have us generate one for you? If you don't know what this means, let us handle it! :) 1) Let Expo handle the process! 2) I want to upload my own keystore!通常は1)でそのままExpoに任せてしまって大丈夫です。ただし、後々アップデートする際には同じkeystoreが必要なので、(これも同じ環境で開発する場合は通常意識しなくても大丈夫ですが)keystoreを安全な場所にバックアップして置くことが推奨されています。
$ expo fetch:android:keystore上記のコマンドを使うと、keystoreの情報が表示され、ディレクトリに.jksファイルが生成されます。
これをpublicなリポジトリにアップしたりしないよう注意して保存しましょう。ビルドが完了し、apkファイルへのリンクが表示されたらダウンロードし完了です。
アプリ情報を入力する
Google Play Consoleを開き、アプリ情報を入力します。
右側メニューのチェックマークが付いている部分を順に入力していきます。
まずは「ストアの掲載情報」で基本的な情報を埋めていきましょう。
タイトルや説明文などのほか、最低限以下のものが必要です。
- アイコン→ 512x512pxのPNG画像(Google Playで表示されます)
- スクリーンショット→ 1辺の長さ320px~3840pxのJPGまたはPNG画像 少なくとも2種類
- フィーチャーグラフィック(ストアで大きめに表示されるバナー)→ 横1024 x 縦500のJPGまたはPNG画像(↓スクショを使って適当に作っておきました)
![]()
- プライバシーポリシー→ 今回はURLを送信しないというチェックがありますが、Expoでは必須(広告IDを送信しているため)
プライバシーポリシーは気になりますが、App Store版の記事と同じくそれほど厳しい感じではない気がします(もちろんそれなりのサービスを運用するのであれば別ですが)。
「価格と配布」で価格、配布する国、同意事項などを入力します。ここで広告を含むかどうかを選択したら、「アプリのコンテンツ」でターゲット年齢層を入力することができます。
残るは「コンテンツのレーティング」と「アプリのリリース」ですが、「コンテンツのレーティング」でアンケートに回答するにはapkファイルをアップロードする必要があります。
「アプリのリリース」で任意のトラック(そのまま公開する場合は製品版)から「管理」→「リリースを作成」し、ドロップエリアにapkファイルをドラッグ&ドロップしてアップロードします。アップロードが完了し、「コンテンツのレーティング」で全ての質問に答えたら、全ての必要な情報が入力できたことになります。
その後先ほど作成したリリース画面から確認→公開へ進めばアプリの申請が完了します。審査
申請が完了すると、ダッシュボードには公開中と表示されますが、しばらくすると以下のように審査中と表示されるようになります。
今までにExpoで作成したアプリをアップデートを含め3回申請しましたが、問題ない場合でも審査の完了には数時間から数日と幅があるような気がします。
そして無事に審査が完了したら、このようにIARCのレーティングを知らせるメールでもって伝えられます。
公開されたアプリはGoogle Play Consoleのダッシュボード→「GOOGLE PLAYで表示」で確認できます。
以上、Google PlayでのExpoアプリ申請の大まかな流れでした。
App Storeでの申請のようにアップロード作業などが縛られていないのと、最低限用意するスクリーンショットが少ないため比較的簡単に感じるのではないでしょうか。
- 投稿日:2019-12-03T21:29:20+09:00
JavaScriptでカレンダーを作ってみた
JavaScriptの基本を勉強したのでなにか作ってみようと思い、
カレンダーの制作を行いました。どこで何をしているかをコメントで書いておかないとわからなくなるので
コメントを書くことを意識しようと思いました。参考にしたサイト
http://cly7796.net/wp/javascript/create-a-calendar-with-javascript/
- 投稿日:2019-12-03T21:25:31+09:00
インターンが勝手にTypeScript導入して学んだこと
初めに
こんにちは、株式会社LITALICOにて19卒エンジニアの@M108Kineticsです。
同社でインターンだった頃に、ほぼ独断と偏見でプロダクションコードをTypeScript化した話をご紹介します。
細かい導入お作法ではなく、チームへのインパクトなど比較的エモめな内容になっております。
TL;DR
TypeScriptは不可欠ではないが楽しいしためになる。
対象
世の中には「全人類TypeScript化するべき」みたいなマウント風潮がなくもない気がしますが、過激派はそっとしておきましょう。
実際のところメリデメってどんな感じだったのか、弊チームでの体験をお伝えします。
TypeScriptが最高であることにノーダウトな方にはあまり参考にならないかもしれませんが、
- ちょっと気になってるけど触れられてない
- 既存のプロジェクトに導入してみたいが実際どれぐらいコストがかかるのかよくわからない
- やたらと最近聞くけど本当に必要?
等々感じている皆様に参考になれば幸いです。チーム
この記事での「チーム」は、リードとなる1~2名の先輩エンジニアの他、4~5名の18卒・19卒の新卒エンジニアで構成されています。
若い衆はいわゆる「文系エンジニア」で、経験も浅いですが日々頑張ってプロダクト開発しています。
動機
導入背景はいたってシンプル、純粋に興味でした。
学生はやたらと新しい技術に触りたがって「〇〇やったことあります」と言いたがってるぐらいがいいんです。(のはず)
フロントエンドに長けた先輩ともちょこちょこ「楽しそうだよね」と話していて、それをゴーサインと勝手に認識してとりあえずイシューだけ立てたのがファーストステップでした。
ふざけたイシューですが、個人的に新しい技術・ライブラリの導入意欲発信のハードルはできるだけ低くていいと考えています。
移行作業
もともとWebpackとBabelを使ってES6を書いていました。
それをJS・TSどちらもをトランスパイルできるようにしたので、移行作業は時間を見つけながらゆっくり行いました。
プルリク数約30、ファイル数でいうと200以上(たぶん)。
今考えるとよく一人でやるなとも思いますが、インターンだからこそできたのかもしれません。
実際終わったのは正社員になってから、一番はじめのプルリクを出してから約半年後でした。
おこがましいことに説明らしい説明も勉強会らしい勉強会もせず勝手に導入し、今となっては6名いる開発チーム全体がTypeScriptを日常的に書いています。
やってよかったと思える理由
仕様・内装を知ろうとする癖がつく
僕はTypeScriptを使うメリットはコードの安全性より何より、「今まで理解しづらかったコードへの窓口を用意してくれる」ことにあると思っています。
完成されているコードって難しい
あるライブラリを利用する場合、ドキュメントをさらっと読んで言われた通りのものを渡せばそれっぽい返り値が返ってきます。
ある別の開発メンバーが用意したモジュールなどを使うときは、直接聞けばなんとなくやるべきことはわかります。
しかしそれだけの理解だとバグ発生の際やモンキーパッチを当てたい時に頭を抱えることになります。
一方で、中身を覗きに行こうとして「どこから始めればいいんだ...」と途方に暮れたことのある方も多いのではないかと思います。
糸口としての「型」
全てのコードを「インプットを受け取って何かしらの処理をし、その結果をアウトプットとして返す」関数として捉えるとします。
インプットである引数とアウトプットである返り値が「どういった形式でどんな情報であるべきなのか」がハッキリとコードの中で定義されているだけで、理解へのとっつきやすさはものすごく変わります。
渡しているどのデータがどのように処理・利用され、最終的なアウトプットとどう関係するのか。これを追うスタートに立てるので、気がついたら大まかな流れを理解できていたりします。
実際の規模感はわからない、巨大に思えるひとつのブラックボックスが、より小さな、案外大したことのないブラックボックスに細分化されていく感じでしょうか。
潜在バグを発見することができた・しやすくなった
JSからTSへの移行は基本的に寝ながらでもできるような作業が大半でした。
が、中には「ほんとにこれで今まで動いてたの!?」と思わせるような型エラーもありました。
そのままだったらまず気付いていなかったでしょうし、何か変更の副作用でバグとして顕在化したとしても原因特定までかなりの時間を要したと思います。
また、導入後のチェック・レビューもしやすくなりました。コードレビューも初心者にとっては基準がよくわからないことが多いかと思いますが、先ほどと同じように「とりあえず引数と返り値の型がちゃんとしているか見よう」と思って始めると、案外指摘点が見つかったりするものです。
デメリット
入れたことによって起きた・起きているダウンサイドはデメリットは、正直特にありません。
なんらかの理由でTypeScriptがオワコンになってJavaScriptに戻すときのコストぐらいでしょうか。
(チームメンバーに文句を言われたら追記します。)
反省
ちゃんとした導入背景・技術説明をチームにしなかった
「なぜ何の為に導入するのか」「導入するなら手順でやるのか」「TypeScriptってなんなのか」といったような説明を、チームに一切せずに進めてしまいました。
TypeScriptは必要になるものではなく、あくまであったらいいものでしかありません。
導入することによっての移行コストや学習コストは払わなければなりません。
そしてさらには、「どこまで徹底するのか」も自由に決めることができるので、チームでのポリシーを決める必要があります。(だからこその過激派)結果的にペアプロやレビューを通してチーム全体の方針が固まっていきましたが、
きちっと方針を決めてアウトプットするべきでした。課題
弊チームでのTypeScript運用は@uhyoさんがおっしゃっている「敗北者のTypeScript」です。
any
は移行時にバラまいたものがまだまだありますし、そのほかの「ループホール」を自主規制するポリシーを決め切れてもいません。ただそれでも導入するだけの価値はあったと個人的には思っています。
これらの課題はこれから、時間をかけて練っていこうと思います。まとめ
TypeScript導入にはプログラムの理解への手順の取り方を教わりました。
これからはそもそもそれの存在意義、コードの安全性・保守性を意識しながら開発できるように精進したいと思います。
そして新米ながらにも新技術導入を経験させてくれたチームに感謝します。成果報告のようになってしまいましたが、ここまで読んでいただきありがとうございました。
次回は@YudaiTsukamotoさんの「parcelの時代が中々はじまらないので使いみちを見出してみた」です!お楽しみに!
- 投稿日:2019-12-03T21:16:29+09:00
Web3.jsでイベント購読 前編
Web3.jsでイベントハンドリング 前編
お久しぶりです、アメリカ8泊10日の後にぶっ続けで3泊4日北海道に行ったり気温0度の広島の県北で野宿して死にかけたりしてる旅行ガイジです。
(建前上は)ブロックチェーンエンジニアとしてプログラムを書く日々も早1年半、せっかくのアドベントカレンダーということでまだqiitaに無さそうだったWeb3.js@1.xのイベント購読についてまとめようかなと思った次第です。
全部書くと間に合いそうにない割とボリュームが大きくなりそうだったので前後編に分けます。
前編は基本的なEthereumのスマートコントラクト(以下、コントラクト)のイベントハンドリングになります。前提
EthereumとかSolidityの初歩的なとこは分かっている前提です。
とりあえずコンソールでもなんでもいいからコントラクトをデプロイしてみたことがあれば多分分かります。各種モジュールのバージョン
node@12.13.1
web3@1.2.1今回は裏でganache-cliというモジュールを使ってEthereumチェーン(をエミュレートしたもの)を用意しています。
gagache-cliを使えばコマンド一つでHTTPとWebソケットがご用意されます。便利。
そして、その環境に後述のコントラクトがデプロイされているものとします。コントラクト
今回使用するコントラクトは以下になります。よくチュートリアルとして使われるSimpleStorageコントラクトのデータ型をmappingにしてイベント処理を突っ込んだだけです。
MappingStorage.solpragma solidity ^0.5.11; contract MappingStorage { mapping(uint => uint) private storedValue; // mappingにセットしたkey, valueを通知するイベント event SetValue(uint key, uint value); constructor() public { storedValue[0] = 1; emit SetValue(0, 1); // イベントの発火 } function set(uint key, uint value) public { storedValue[key] = value; emit SetValue(key, value); // イベントの発火 } function get(uint key) public view returns(uint retVal) { return storedValue[key]; } }イベントとはなんぞや
コントラクトにおいて変数などのデータはブロックチェーンのストレージ領域に保存されますが、イベントはその中でもトランザクションレシートのログ領域に書き込まれるものになります。
コントラクトというものは外部から隔離された環境で動作するため、コントラクトから何か外部の機能を発火させるということはできません。
しかし、例えば特定の関数が呼ばれたのをトリガーに処理を行う、といった際にはそれでは不都合な訳です。ひたすら繰り返し値を取得し続けて差分を記録するなんて前時代的なやり方はこのご時世にはちょっといただけません。そこで登場するのがイベントです。
イベントを利用することで外部からコントラクトの動作を動的に知ることができるようになります。前述のコントラクトではコンストラクタとset関数が呼ばれた際にイベントが発火します。
イベントの購読
ここからが本題になります。コントラクトのイベント購読を実例で見ていきましょう。
まず、イベントを購読するスクリプトが以下になります。eventHandle.jsconst Web3 = require('web3'); // HttpProviderでなくWebsocketProviderなので注意 const web3 = new Web3(new Web3.providers.WebsocketProvider("http://localhost:8545")); (async () => { const contractAddress = "コントラクトアドレス"; const abi = ["コントラクトのABI"]; const MappingStorage = await new web3.eth.Contract(abi, contractAddress); // {}内に色々入れることで購読するイベントをフィルタリングできます、詳しくは後述のドキュメント参照 MappingStorage.events.SetValue({}, (err, event) => { console.log(`event called: ${event.event}`); console.log(JSON.stringify(event, null, " ")); }); })();
MappingStorage.events.SetValue(...)
でイベントの購読を行います。購読するイベントのフィルタリングなんかもできますがここでは省きます。コントラクト内でイベントが発火される(set関数が呼ばれる)と、第2引数にセットしたコールバック関数が呼ばれるようになります。
なお、MappingStorage.events.allEvents(...)
とすることでコントラクト内の全イベントが購読対象となります。(今回はSetValueイベントしかありませんが)上記スクリプトを実行するとイベントの購読待ちとなります。この状態でset関数が呼ばれると、以下のような出力がされます。
出力event called: SetValue { "logIndex": 0, "transactionIndex": 0, "transactionHash": "0xa79f032fb7faf9384da2249cc24e0c4fad6ee582fd5715a39db8f665446866f2", "blockHash": "0xdabdbac45b7493db64315cde20a1e2635eac54aaa61426d75ec0d0a8fc5d9eee", "blockNumber": 8, "address": "0xFcac91F1b345bC6E955fDc317fF827950C014142", "returnValues": { "0": "10", "1": "1", "key": "10", "value": "1" }, "event": "SetValue", "signature": "0xf2247e57590b0cc543ff61d1604a3fc00440b206ff3ee22e9a2ff5e5bbdf83da", "raw": { "data": "0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000001", "topics": [ "0xf2247e57590b0cc543ff61d1604a3fc00440b206ff3ee22e9a2ff5e5bbdf83da" ] } }
logIndex
:ブロック内のイベントインデックス
transactionIndex
:ブロック内のトランザクションインデックス
transactionHash
:トランザクションハッシュ
blockHash
:ブロックハッシュ
blockNumber
:ブロック番号
address
:イベントが発火されたコントラクトアドレス
returnValues
:イベントにセットされたパラメータ、数字キーが自動追加され、event.returnValues[0]
のようにアクセスできます
event
:イベント名
signature
:イベント定義のkeccak256ハッシュです。今回の場合はkeccak256("SetValue(uint,uint)")
です
raw.data
:イベントのパラメータが格納されたHEX文字列
raw.topics
:最大4×32ByteのHEX文字列配列。インデックス0にはシグネチャと同内容が入り、1-3にはインデックス化されたイベントパラメータの内容が格納されます(詳細は省略)おわりに
色々複雑なブロックチェーンですが、最近は補助モジュールやIDEも登場してずいぶん扱いやすいものになっています。
Web3.jsを使えばEthereumの細かな動きが分からずとも動かせてしまうので、JSの環境構築の容易さも相まってなかなかにお手軽だと思います。後編では、イベントを利用した効率的なデータ管理について書こうと思います。
- 投稿日:2019-12-03T19:48:53+09:00
ReactくんさぁHooksとか言ってるけど、で?どうやってAPIから値持ってくんの?
はじめに
いなたつアドカレの三日目の記事です。
今回はReactでRestAPIなどから値を取ってくるのに、関数型コンポーネントでどーやってAPIから値取ってくんの?
componentDidMountなんてないよ?
componentDidUpdateなんてないよ?
ってなりますよね、僕はなります。
結論から言ってクレメンス
useEffectを使うンゴ
React Hooks にある、useEffectを使用します。
useEffectってなんぞや
関数コンポーネントで副作用を実行するためのフックで、classコンポーネント時のcomponentDidMount,componentDidUpdate,componentWillUpdateの3つにあたります。
じっそー
useEffect(() => { // ここでAPIをたたく },[])useEffectの第一引数が、実行する関数で、第二引数が何が変化したときにuseEffectを実行するかを決めるものとなっています。
ここでは空配列を指定しています、空配列を指定することでコンポーネントが配置される前(componentDidMount)でのみuseEffectを実行するといったことができます。useEffect(() => { // ここでAPIをたたく },[hoge])これでhogeが変更された時に反応してuseEffectが実行され、APIを再度叩くことができます。
このようにしてきっちりバックエンドとフロントエンドを分離した際に再描画のタイミングを指定してAPIを再び指定することができます。
- 投稿日:2019-12-03T19:27:32+09:00
React x Firebase を用いた Web アプリケーション開発時のハマりどころと解決策
概要
これから React x Firebase の構成で Web アプリケーションを開発する方が同じところでハマらないようにハマりどころと解決策をまとめました。
この資料は Firebase Meetup #15 Cloud Functions Day の LT 資料です。
スライドは以下です。
React x Firebase を用いた Web アプリケーション開発時のハマりどころと解決策 - Speaker Deck1. Hosting と同じドメインで Cloud Functions の関数を呼び出す
Cloud Functions のデフォルトで生成される URL は以下ですが、
https://[region]-[project id].cloudfunctions.net/[function name]
以下の URL で呼び出したいことがあります。
https://[project id].web.app/[function name]
https://[project id].firebaseapp.com/[function name]
https://[独自ドメイン]/[function name]
Hosting の設定ファイルである firebase.json の rewrites の設定をすれば OK です。以下の設定で /notifications にアクセスされた時に Cloud Functions の notifications 関数を実行し、その他のパスにアクセスされた場合は index.html が参照されます。
firebase.json"hosting": { "rewrites": [ { "source": "/notifications", "function": "notifications" }, { "source": "**", "destination": "/index.html" } ]Functions は以下のように、通常通り実装すれば良いです。
functions/index.jsexports.notifications = functions .https .onRequest((req, res) => { // ... };2. ローカル環境の React から Emulator の Functions を呼び出す
やりたかったことは以下です。
- Functions をいちいちデプロイして検証したくない
- React のホットリロードはそのまま使いたい
そのため、 React は yarn start して Functions は emulator を使って接続します。 Functions をローカルで動作させるために Emulator Suite を使います。
Command$ firebase emulators:start --only functionssrc/index.jsconst app = firebase.initializeApp(config) if (process.env.NODE_ENV === 'development') { app.functions().useFunctionsEmulator('http://localhost:5001') }3. React と Functions で Firestore の参照先を合わせる
2 の設定を行った場合、 Firestore を利用していると以下のような状態になります。
- React: Cloud の Firestore を参照
- Functions: Emulator の Firestore 参照
そのため、Cloud か Emulator か参照先を統一する必要があります。
3-1. Cloud の Firestore を参照する
以下のような構成です。
- React: yarn start
- Functions: Emulator
- Firestore: Cloud
この場合のメリデメは以下のようになります。
- メリット: 開発時に Firebase console からデータを確認できる
- デメリット: Cloud のデータが更新されるため、複数人開発には不向き。リリース後は Firebase を複数プロジェクト構成 にするなどの工夫が必要
設定は以下のように行います。Functions のディレクトリで @google-cloud/firestore パッケージをインストールして、 functions/index.js からそのパッケージを利用します。 引数には Firebase の project id を指定します。
Command$ npm install --save @google-cloud/firestorefunctions/index.jsconst Firestore = require('@google-cloud/firestore') const admin = require('firebase-admin') const db = (() => { if (process.env.FUNCTIONS_EMULATOR) { return new Firestore({ projectId: 'your project id' }) } else { return admin.firestore() } })()公式ドキュメントの 認証の開始 ページからサービスアカウントキーを作成してローカルに配置し、 .bashrc や .zshrc などに追記しておきます。
Command$ export GOOGLE_APPLICATION_CREDENTIALS="[PATH]"3-2. ローカルの Firestore を参照する
以下のような構成です。
- React: yarn start
- Functions: Emulator
- Firestore: Emulator
この場合のメリデメは以下のようになります。
- メリット: ローカル環境に閉じてデータを変更できるので複数人開発やリリース後の運用にも向いている
- デメリット: データの確認が難しい
デメリットとしてデータ確認が難しいとしましたが、これは Firestore CLI などがない(?)ためで、もし良いアクセス方法ご存じの方いたらコメントください。
設定は以下のように行います。公式ドキュメント でもこの方法が紹介されています。
Command$ firebase emulators:start --only functions,firestoresrc/index.jsconst app = firebase.initializeApp(config) if (process.env.NODE_ENV === 'development') { app.functions().useFunctionsEmulator('http://localhost:5001') db.settings({ host: "localhost:8081", ssl: false }); }4. Hosting から asia-northeast1 region の Functions を呼び出せない
以下のように region 指定をすると Hosting から呼び出せなくなる
functions/index.jsexports.getEvents = functions .region(‘asia-northeast1’) .https() .onCall((data) => { // ... });公式ドキュメント を見ると
Firebase Hosting は、us-central1 でのみ Cloud Functions をサポートします。
との記述が。。というわけで、Hosting とつなぐ Function は、現状 region 指定しないで運用するしかなさそうです。
以上!
- 投稿日:2019-12-03T19:05:48+09:00
nodejs v12(LTS)におけるasync, awaitを用いたstream処理
nodejs v12(LTS)におけるasync, awaitを用いたstream処理
QualiArts Advent Calendar 2019、3日目の記事になります。
はじめに
2019年10月21日にnodejs v12のLTS版が公開されました。
nodeは奇数バージョンが開発版、偶数バージョンが安定版となるため、v11以降の今まで実プロジェクトだと利用しにくかった機能がこれによりいくつか使えるようになりました。
そのなかでもasync-generatorsやfor-await-of loops構文が大手を振って使えるようになったことにより、stream関連の処理が大きく変わると感じたため、すでにいくつか紹介されている記事も有りますが、この機会に改めて紹介したいと思います。
また、最後に簡単なサンプル処理も記述しているので、ご参考いただければ幸いです。for-await-of loops
今までstreamはeventEmitterを利用し、発火したeventをトリガーに処理を記述していました。
for-await-of loopsを用いると下記のようにわかりやすくかけるようになります。
for-await-of自体は単純にfor-ofのasyncも利用可能になったものとなります。for-await-of_loops1.jsconst fs = require('fs'); const reader = async (rs) => { for await (const chunk of rs) { console.log(chunk); } }; (async function () { const rs = fs.createReadStream('./input.txt', { encoding: 'utf8' }); await reader(rs); })();input.txtabcde fghij klmno pqrxy z実行結果
terminalabcde fghij klmno pqrxy z
従来だと、下記のように終了イベントをpromise化するなどで、全体のpromise化などは簡単にできますが、イテレータ内部のchunk単位でpromise化を行う場合非常に可読性が悪くなってしまっていました。
(v10以降であれば下記のようにstream.finishedをpromisifyすることで全体のpromise化は簡略可能です)これが上記のように簡単に記述できるようになったのは非常にやりやすくなったと感じます。
for-await-of_loops2.jsconst fs = require('fs'); const stream = require('stream'); const util = require('util'); const finished = util.promisify(stream.finished); const msleep = util.promisify(setTimeout); // streamを用いた場合の処理(全体終了部分のみのpromise化) const reader1 = async (rs) => { rs.on('data', (chunk) => { console.log(chunk); }); await finished(rs); // stream.finishedを使わない場合下記のようなpromiseを生成する // await new Promise((resolve, reject) => { // rs.once('finished', (chunk) => { // return resolve(data); // }); // rs.once('error', (err) => { // return reject(err); // }); // }); }; // streamを用いた場合の処理(chunk単位でのpromise化) const reader2 = async (rs, iterator) => { let buffer = ''; rs.on('data', (chunk) => { buffer = chunk; rs.pause(); }); let isEnd = false; rs.once('end', () => { isEnd = true; }); let error; rs.once('error', (err) => { error = err; }); while (true) { if (error) { throw error; } if (buffer) { await iterator(buffer); buffer = ''; } else if (isEnd) { return; } else if (rs.isPaused()) { rs.resume(); } // 非同期メソッドがないと無限ループしてしまうため await msleep(0); } } (async function () { const rs1 = fs.createReadStream('./input.txt', { encoding: 'utf8' }); await reader1(rs1); const rs2 = fs.createReadStream('./input.txt', { encoding: 'utf8' }); await reader2(rs2, async (chunk) => { console.log(chunk); }); })();実行結果
terminalabcde fghij klmno pqrxy z abcde fghij klmno pqrxy z
async-generators
今までは同期メソッドでしか使えなかったyieldがasyncにも対応しました。
async function*
でasyncIteratorのジェネレータメソッドを生成でき、await対応したnextメソッドを呼び出すことができます。
(nextで呼び出した場合返り値はObjectになります)async-generators1.jsconst util = require('util'); const msleep = util.promisify(setTimeout); async function* generate() { for (let i = 1; i <= 3; i++) { await msleep(1000); yield i; } } const asyncIterator = generate(); (async () => { console.log(await asyncIterator.next()); console.log(await asyncIterator.next()); console.log(await asyncIterator.next()); console.log(await asyncIterator.next()); })();実行結果
{ value: 1, done: false } { value: 2, done: false } { value: 3, done: false } { value: undefined, done: true }
こちらはfor-await-of loopsも利用可能です。
こちらを利用すると簡単にラグのあるstreamデータの生成が可能になります。async-generators2.jsconst util = require('util'); const msleep = util.promisify(setTimeout); async function* generate() { for (let i = 1; i <= 3; i++) { await msleep(1000); yield i; } } const asyncIterator = generate(); (async () => { for await (const v of asyncIterator) { console.log(v); } })();実行結果
1 2 3
行ごとにawait処理を行うサンプル
上記の機能が実装されたことで、行ごとのように一定windowずつstreamで非同期メソッドを実行する処理が非常に簡単にかけるようになりました。
下記は得られたstreamを、行ごとに非同期メソッドを実行する場合のサンプルになります。
可読性重視&行単位でeventループが回るため、パフォーマンスがシビアな場合は別途実装することをおすすめします。readlineモジュールを利用した場合
line-reader1.jsconst fs = require('fs'); const readline = require('readline'); const util = require('util'); const msleep = util.promisify(setTimeout); const asyncLineReader = async (iterater) => { const rl = readline.createInterface({ input: fs.createReadStream('input.txt', { encoding: 'utf8' }), crlfDelay: Infinity }); for await (const line of rl) { await iterater(line); } } (async () => { await asyncLineReader(async (line) => { await msleep(100); console.log(line); }); })();実行結果
terminalabcde fghij klmno pqrxy z
解説
こちらはreadlineモジュールを利用したものになります。
以前も行ごとに処理を行えたのですが、非同期メソッドの実行はできませんでした。
v1.12からasyncIteratorに対応したことで、上記のように簡単に非同期メソッドが実行できるようになりました。stream(for-await-of利用)
line-reader2.jsconst fs = require('fs'); const util = require('util'); const msleep = util.promisify(setTimeout); // streamを用いた場合の処理(for-await-of使用) const asyncLineReader = async (iterater) => { const rs = fs.createReadStream('./input.txt', { encoding: 'utf8' }); let buffer = ''; for await (const chunk of rs) { buffer += chunk; const list = buffer.split('\n'); // 最後の要素は改行が含まれているわけではないため、bufferに戻す buffer = list.pop(); for (let i = 0; i < list.length; i++) { await iterater(list[i]); } } if (buffer) { // 終了時にbufferに残っている文字列もiteratorにわたす await iterater(buffer); } } (async () => { await asyncLineReader(async (line) => { await msleep(100); console.log(line); }); })();実行結果
terminalabcde fghij klmno pqrxy z
解説
こちらはstreamのfor-await-ofを利用したものになります。
イベントループの回数が他と比べて半分以下なので、このなかでは一番パフォーマンスが良いです。
実装を合わせるために1行ずつ処理していますが、こちらで複数行制御してlistをiteratorに渡すような実装が実利用だと良いかもしれません。
比較的簡単に可読性良く記述できるようになっているかと思います。stream(for-await-of未使用)
line-reader2.jsconst fs = require('fs'); const util = require('util'); const msleep = util.promisify(setTimeout); // streamを用いた場合の処理(for-await-of未使用) const asyncLineReader = async (iterator) => { const rs = fs.createReadStream('./input.txt', { encoding: 'utf8' }); let buffer = ''; let rows = []; rs.on('data', (chunk) => { buffer += chunk; const list = buffer.split('\n'); buffer = list.pop(); if (list.length) { rows.push(...list); rs.pause(); } }); let isEnd = false; rs.once('end', () => { isEnd = true; }); let error; rs.once('error', (err) => { error = err; }); while (true) { if (error) { // errorがあれば終了 throw error; } if (rows.length) { for (let i = 0; i < rows.length; i++) { await iterator(rows[i]); } rows = []; } else if (isEnd) { if (buffer) { // 終了時にbufferに残っている文字列もiteratorにわたす await iterator(buffer); } return; } else if (rs.isPaused()) { rs.resume(); } // 非同期メソッドがないと無限ループしてしまうため、setImmediate代わりに実行 await msleep(0); } } (async () => { await asyncLineReader(async (line) => { await msleep(100); console.log(line); }); })();実行結果
terminalabcde fghij klmno pqrxy z
解説
こちらはstreamのfor-await-ofの未使用版になります。
かなり複雑になり可読性も落ちている事がわかります。
ただ、async, awaitが対応しているv8以降であれば利用可能なため、nodeのバージョン次第では利用できるかもしれません。まとめ
これらの機能の追加により、async, awaitを用いたstream処理が非常に簡潔に記述できるようになりました。
v1.12LTSに上げることで非常にstream周りが記述しやすくなっているため、この機会にぜひ試してみてはどうでしょうか?
- 投稿日:2019-12-03T18:32:37+09:00
reduceを使った集計データへの変換
概要
配列の形で持っているデータをグループごとに集計したデータの形に変換したときの話。
こんなデータを
グループ 目標 実績 A 100 200 A 100 50 B 50 100 こんな形に集計する
グループ 目標 実績 A 200 250 B 50 100 詳細
まずはreduceで呼び出す、集計処理の関数
hasOwnProperty
でマップに対象キーのデータが登録済みかどうかで判断している。const convertDataMap = function(dataMap, d) { const key = d.group1; //const key = `${d.group1}-${d.group2}`; // keyの作り方次第で複合キーでの集計も可能 if (dataMap.hasOwnProperty(key)) { const tmp = Object.assign({}, dataMap[key]); dataMap[key] = { target: tmp.target + d.target, result: tmp.result + d.result } } else { dataMap[key] = { target: d.target, result: d.result } } return dataMap; }上記の関数を呼び出す部分
reduce
内でさっきの関数を呼びだす。const data = [ { "group1":"A","group2":"C", "target":100, "result":50 }, { "group1":"A","group2":"D", "target":50, "result":50 }, { "group1":"A","group2":"D", "target":10, "result":100 }, { "group1":"B","group2":"D", "target":100, "result":50 }, { "group1":"B","group2":"C", "target":50, "result":50 }, { "group1":"B","group2":"C", "target":10, "result":100 }, ] const convertMap = data.reduce((dataMap, d) => { return convertDataMap(dataMap, d); }, {});作成後のデータの形はこんな感じ
変換後データ{ "A": { "target": 160, "result": 200 }, "B": { "target": 160, "result": 200 } }データを配列の形にしたい場合は、以下のような処理をしてあげると
const _data = []; _data.push( ...Object.entries(convertMap).map(([key, value]) =>({key, value})) );配列になる(value の部分もフラットにしたい場合は、上記の関数のmap内の処理を書き換えてください)
変換後データ[ { "key": "A", "value": { "target": 160, "result": 200 } }, { "key": "B", "value": { "target": 160, "result": 200 } } ]まとめ
サーバー側で集計まで処理して返すことは結構あったが、JavaScriptでやるのは何気に初めてだった。
画面側で集計処理できれば、集計キーが複数あった場合の階層を変更してもリクエスト投げずに処理できてしまうので、今後も使いどころはあるかも。。。
- 投稿日:2019-12-03T17:10:34+09:00
git add -pで分割コミットしようとするとlint-stagedでエラーになる
症状
git add -pなどして「partially staged」した状態からgit commit経由でlint-stagedをすると、エラーになった。lint-stagedのバージョンは9.2.3で確認
$ git commit husky > pre-commit (node v11.13.0) Stashing changes... [started] Stashing changes... [completed] Running tasks... [started] Running tasks for *.js [started] eslint --fix [started] eslint --fix [completed] git add [started] git add [completed] Running tasks for *.js [completed] Running tasks... [completed] Updating stash... [started] Updating stash... [completed] Restoring local changes... [started] Restoring local changes... [failed] → Command failed with exit code 128 (Unknown system error -128): git checkout-index -a Command failed with exit code 128 (Unknown system error -128): git checkout-index -af husky > pre-commit hook failed (add --no-verify to bypass)これが起こると、lintが異常終了していて以下の状態になる。
- add -pして分け分けしたdiffを持つファイルが全部add状態になる
- 一部のファイルがworkdirからなくなる (たまたま作業中のファイルに当たると悲惨?)
原因と解決策
別端末で自動ファイル更新検知と再ビルドが走っていると、それと競合することが原因の模様。
npm run serve
経由で開発用httpサーバーを動かしていた。解決策はシンプルに開発用httpサーバーを都度止めてgit操作をする。ちょっと面倒だがとりあえずデータロストの心配はなくなる。
参考 & 解決策ではなかったもの
- そもそもpartially stagedなdiffをlint-stagedが扱えるようになったのはver.8以降。その基本アリゴリズムはこちらの通り。
- partially stagedなdiffとlint-stagedの組み合わせに関するバグが
lint-staged@9.5.0
で修正されているそうなので、それにアップデートしてみたが、本投稿の件は解決しなかった。よく見るとエラーメッセージが違う。ちなみにこのバグは本投稿の事象のエラーメッセージ中のgit checkout-index -af
で検索をかけて発見した。- エラーメッセージ中には
--no-verify
しろと書いてあるが、それをやると当然lintが通らないので回避は可能だが解決策にはならない。
- 投稿日:2019-12-03T16:44:37+09:00
p5.js - SVG文字列を読み込んで表示する
動機
p5.jsライブラリを使い、SVG文字列を配置できるようにしたい
実行環境
実行結果
サンプルコード
let svgString = `<svg> .. 虎のSVG文字列..</svg>` let img; function convertBase64EncodedSvg(svgString) { return "data:image/svg+xml;base64," + btoa(svgString) } function preload() { img = loadImage(convertBase64EncodedSvg(svgString)) } function setup() { createCanvas(512, 512) } function draw() { background(220) image(img, 0, 0, 512, 512) }コード解説
1.SVG文字列を用意する
let svgString = `<svg> .. 虎のSVG文字列..</svg>`ネット上によくある虎の絵をサンプルとして準備。(参考: ファイル:Ghostscript Tiger.svg)
そのまま使用すると画質が荒くなった。
svgタグにwidth,height属性を追記した結果綺麗になった。
(<svg .. width="900" height="900"...>)
2.SVGのBase64変換処理を定義
function convertBase64EncodedSvg(svgString) { return "data:image/svg+xml;base64," + btoa(svgString) }サンプルのため、SVGのみで定義。
PNGやJPGなどの分岐をもたせて汎用性を高くしておくといいと思う。Base64画像変換も簡略化。
encodeURIComponentなどで各種文字に対応するなどの検討余地はあり。3. p5.jsで読み込んで実行
let img;画像の変数を定義。
function preload() { img = loadImage(convertBase64EncodedSvg(svgString)) }事前読み込み処理でBase64変換したSVG文字列をp5.jsのloadImage関数で読み込む。
こうすることでSVG画像をimageとして扱うことが可能になる。function setup() { createCanvas(512, 512) } function draw() { background(220) image(img, 0, 0, 512, 512) }あとはp5.jsのサンプルコードによくある処理。
imageを配置。(便宜的に512pxに設定)参考文献
- 投稿日:2019-12-03T16:39:34+09:00
【CakePHP2】formタグのスコープ外からformの内容をcontrollerに送信
やりたいこと
formタグの外にあるボタンやリンクでもcontrollerに値を渡したい
やったこと
test.ctp<?= $this->Form->create('HogeList', ['type' => 'GET']); ?> <div class="searchInputArea" style="margin-bottom: 0px;margin-top: 0px;"> <table class="em9"> <tr> <th> ID </th> <td> <?php echo $this->Form->input('id', [ 'type' => 'text', 'id' => 'item_id', 'size' => 30, 'label' => false, 'div' => false, 'empty' => true, 'required' => false, ]); ?> </td> </tr> <tr> <th> 名前 </th> <td> <?php echo $this->Form->input('name', [ 'id' => 'name', 'type' => 'text', 'size' => 30, 'label' => false, 'div' => false, 'empty' => true, 'required' => false, ]); ?> </td> </tr> <tr> <th> ステータス有効のみ </th> <td> <?php echo $this->Form->input('status', [ 'type' => 'checkbox', 'div' => false, 'value' => TestAppModel::STATUS_ACTIVE, 'label' => '', ]); ?> </td> </tr> </table> </div> <div class="submitArea hasSubAction"> <ul class="submitButtons"> <li> <label class="submitBase primary" for="submit"> <?php echo $this->Form->hidden('mode', ['value'=> '']); ?> <?php echo $this->Form->submit('検索', [ 'type' => 'submit', 'id' => 'submit', 'div' => false, 'label' => false ]); ?>検索 </label> </li> </ul> </div> <?php echo $this->Form->end(); ?> </div>test.ctp<!--formの外に追加--> <?php echo $this->Html->link('formの値を送信した使いたいリンク', 'javascript:clickOutOfFormLink();void(0)', ['class' => 'actionBtnBase hogeListOutput']); ?> <!--jQuery部分--> <script> // スコープ外から検索ボタンを押下 function clickOutOfFormLink() { // 検索機能をform外の処理に切り替えるためhiddenを設定 $('#HogeListMode').val('hogehoge'); document.getElementById('submit').click(); // 検索機能に戻すためhiddenをクリア $('#HogeListMode').val(''); } </script>結果
TestController.php/** * 一覧 * * @return void */ public function index(): void { if (Hash::get($this->request->query, 'mode') === 'hoge') { // modeがhogeである場合はform外のリンクが押下されている echo 'hoge'; // 入力値も使用可能? var_dump($this->request->query); } }
- 投稿日:2019-12-03T16:36:06+09:00
Node.jsのCLIツールをカスタマイズ可能にする
Node.jsで何らかのCLIツールを作っているとします。
おそらくどのツールでも、オプショナルな機能を使うかどうかは、エンドユーザーに決めてもらいたいでしょう。その場合、
-i
とか-a
のようにコマンドの引数をいくつか提供するのもいいですが、複雑な設定が必要になりそうだったら、設定ファイルでのカスタマイズを可能にした方がいいです。設定ファイル
ファイル名
設定ファイルの名前は、ツールの開発者が決めないといけないのですが、ほとんどのプロジェクトでは、
{tool}.config.js
という風に決まっています。たとえば、webpack
だったら、webpack.config.js
というファイルで設定できます。設定ファイルの検出
最近の多くのプロジェクトでは、設定を全てオプショナルにするトレンドがあります。もし設定ファイルが存在しなければ、デフォルトの値で動きます。設定ファイルが存在するかどうか、Node.jsの
fs
モジュールにあるexistsSync
という関数を使えばわかります。
existsSync
はダイレクトリーかファイルのパスを取るが、CLIツールのソースコードと、ユーザーのプロジェクトのルートはそれぞれ違う場所にあるので、ユーザーがCLIを呼んだダイレクトリーを検出しないといけないです。それはprocess.cwd()
でできます。const path = require('path'); const fs = require('fs'); const cwd = process.cwd(); const configPath = path.join(cwd, 'tool.config.json'); if (fs.existsSync(configPath)) { // 設定を読み込む }読み込み
これは設定ファイルの形式によりますが、多くのプロジェクトではJSONを使っています。JSONを読み込むには、
fs
にあるreadFileSync
でファイルを読み込み、その中身をJSON.parse
します。if (fs.existsSync(configPath)) { const rawConfig = fs.readFileSync(configPath); const config = JSON.parse(rawConfig); }
fs
の拡張であるfs-extra
というライブラリには、fs.readJSONSync
という関数があるので、それを使うと一気にJSONのファイルが読み込めます。もちろん、
require
だけで読み込むこともできますし、json
にもjs
にも使えます。if (fs.existsSync(configPath)) { const config = require(configPath); }ただし、一回
require
されたものは、Node.jsプログラムのライフタイムに渡ってキャッシュされるので、CLIにはwatch
のような機能を入れたい場合は、注意が必要です。設定ファイルが変更されても、たとえば2回目のビルドでキャッシュされた古いものがrequire
されてしまいます。そういう問題を避けるためにfs-extra
などを使った方がいいです。
- 投稿日:2019-12-03T16:05:48+09:00
HandsonTableのドキュメントを見るときの注意点
HandsonTableとは
HandsonTableは、JavaScriptのEXCELのようなグリッドコンポーネントです。
https://handsontable.com/ドキュメントを見るときの注意点
Handsontableを使う際に、公式のドキュメントを見ると思います。
React、Angular、Vue.jsのサンプルも書いてあります。
こちらを見る際に、知っておかないと困ることがあります。サンプルページまでは、簡単に行けます。
このサンプルページを見てソースコードを読んでいると、
「あれ、vueコンポーネントを呼び出しているところはどこだ?」となると思います。上記のEditボタンを押してください!!!
そうするとすべてのソースコードが見えます!!!
当たり前のようですが、気づくのにしばらくかかり困りました。
- 投稿日:2019-12-03T15:43:43+09:00
Gmailの添付ファイルを自動でGoogle Driveに保存 をGAS(Google Apps Script)で実装
初投稿です。
やりたいこと
Gmailで条件検索したメールの添付ファイルを、Google Driveに自動で保存
他担当者が、取引先からの請求書を都度印刷→経理ソフトで入力していましたが、印刷を省いてPDFファイルを表示しながら入力する環境を目指すため、Gmailのメールを添付ファイル(請求書はPDFですが今回は添付ファイルななんでも保存)をGoogle Driveに保存するスクリプトを実装しました。
新規性に乏しい無い内容ですが、初投稿ということと、多少つまづいた点を含め、記載しておきます。実装の流れ
- コーディング:Gmailを条件検索(今回は from,日付,添付あり を条件)し、検索結果を取得
- コーディング:検索結果から添付をGoogle Driveへ順次保存
- 実行トリガー設定:時間をトリガーとして1日に1回実行する
コード以下を参照させていただきました。ほぼ丸々コピー。
テレビ局で働く独学プログラマー Gmailの添付ファイルをgoogle driveに保存する1. Gmailを条件で検索(今回は from,日付,添付あり を条件)し、検索結果を取得
SaveGmailAttachToDrive.gs//メール検索用の日付計算 var now = new Date(); var today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); //こうすることで0時0分0秒となる var yesterday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 1); var strYesterday = Utilities.formatDate(yesterday, "JST", "yyyy/MM/dd"); //メール検索キーワードの作成 var condition = ''; // condition += ' is:unread' //未読 // condition += ' subject:添付やで'; //件名 condition += ' has:attachment'; //添付あり condition += ' after:' + strYesterday; //今日(昨日より後) condition += ' from:' + 'メールアドレス'; //差出人 //Gmailで検索 var searchMail = GmailApp.search(condition); var messeges = GmailApp.getMessagesForThreads(searchMail); //検索結果を配列で取得2. Gmailを条件で検索(今回は from,日付,添付あり を条件)し、検索結果を取得
メールの検索結果の配列は2次元の為、2重ループで取り出します
SaveGmailAttachToDrive.gsvar hozon_folder = DriveApp.getFolderById('GoogleDriveのFolderID'); for(var i = 0; i < messeges.length; i++) { //検索結果を一つずつ取り出す for(var j = 0; j < messeges[i].length; j++) { //スレッドの場合ここを複数回実行 var attach = messeges[i][j].getAttachments(); var day = messeges[i][j].getDate(); //取り出したメールの日付を取得 var strDay = Utilities.formatDate(day , "JST", "yyyy_MM_dd"); if (day > today){ //★★メールが今日以降か再度チェック for(var k = 0; k < attach.length; k++){ var filename = strDay + '_' + attach[k].getName(); hozon_folder.createFile(attach[k]); var file = hozon_folder.getFilesByName(attach[k].getName()) file.next().setName(filename); } } } messeges[i][0].markRead(); }はまったポイント
メールの検索結果はメール単位でなく、スレッド単位でした。
つまり、1.の処理で今日のメールのみ取得していると思いきや、以下画像ように、Gmailの検索結果で数字が表示される場合は、スレッド内に複数のメールが含まれており、今日より以前のメールも含まれている可能性が高い。
ということで、スレッドからメッセージを取り出した後も日付を判定する処理を加えてます。
3. 実行トリガー設定:時間をトリガーとして1日に1回実行する
実行日の添付ファイルをチェックするので、23~24時で毎日実行でセットしました。
トリガーの設定は以下の参照ください
GAS ビギナーが GAS を使いこなすために知るべきこと 10 選おわり
Google Driveに保存したファイルは、Google Drive↔Synology NAS Cloud Sync↔DropBoxでDropbBox同期させ他ユーザも閲覧可能にしています。
Gmail→DropBox直接保存も考えましたが、以下消去法により今回の実装をしました。
- IFTTT Gmailをトリガーとした連携が終了していた
- zaiper 有料範囲があるから面倒、手を出したくない
- MS Flow:実装可能が、サービスの継続が不安
- DropBox APIの利用:DropBoxでtokenを取得する際、DropBox内の既存のフォルダを対象としたAPIを利用する場合、無駄にAPI使われないかDropBox側でチェック入るっぽい(参照)。 今回は既存のフォルダを使うつもりだったので無し。
初投稿でした。他にもGASで色々実装しているので、その周り寄稿できればと思います。
- 投稿日:2019-12-03T15:37:03+09:00
SweetAlert2(swal2)のcss適応方法〜ゴリ押しでできてしまった?〜
はじめに
こんにちは。
記事を書くのは何ヶ月ぶりだろうか…。
最近、SweetAlert2使ってるんだけど、毎回調べてる気がするから備忘録兼記事として。やりたいこと
・Swalのコンテナにボーダーをつけたい!
・タイトルの文字色などを変えたい!
etc...1.そもそもSweetAlertってなんやねんって話
SweetAlertというのは、とある方(多分Limon Monte氏)によって開発されたJSのライブラリである。
主な機能としては、JSで出すアラートを綺麗にカッコよく可愛く装飾、表示してくれるライブラリである。
売りはそれだけじゃなくて、綺麗以外にもレスポンシブに対応していたり、使いやすかったり、カスタマイズが簡単だったり。See the Pen mdydaBJ by 至極色のひかる (@sigoku14) on CodePen.
とこんな感じで、簡単にアラートをデコレーションすることができます!
2.どうやってcss弄るの?(基本編)
これはライブラリ側でほとんどやってくれているようなもの。
ドキュメントを読めば大体のことは書いてくれている。その上、下みたいにクラスを追加してアニメーションや微細なところまでいじれるようにしてくれているのだ!
customClass: 'customizable'とか、
customClass: { container: 'container-class', popup: 'popup-class', header: 'header-class', title: 'title-class', closeButton: 'close-button-class', icon: 'icon-class', image: 'image-class', content: 'content-class', input: 'input-class', actions: 'actions-class', confirmButton: 'confirm-button-class', cancelButton: 'cancel-button-class', footer: 'footer-class' }みたいな感じで、アラート自体にクラスを付与することができ、cssを記入してあげれば動かすことができます。
そもそもこの
customClass
というのが強いのですが、なぜか二つ目の書き方をすると、classが効かないという。
多分、僕の問題なんだろうけど、調べて解決するつもりが、何となくでやったらできてしまったので、それを綴ります。3.どうやってcss弄るの?(class分解編)
まず、Swal2のclassがどうなっているか調べましょう。
version 9.4.1
時点での構成を見てみましょう。まぁ、これ見れば大体のことはわかるんだけど、正直読むのめんどくさいわ!!
っていう人が大半でしょう。
- swal2-container
- swal2-title
- swal2-header
- swal2-icon とか swal2-progress-steps
- swal2-content
- swal2-html-container とか swal2-range とか swal2-check
- swal2-actions
- swal2-footer
- swal2-timer-progress-bar
と、分解したらこんな感じ(かなり端折ってはいますが)。
なら、これに書き込めば良いじゃん!
初心者の私はそう思いましたが、もちろん動くはずもなく...。
では、どうしたか。4.どうやってcssを弄るの?(ゴリ押し編)
多分、様々なところに応用できるとは思うので、今回は自分が解決したかったことを中心に書いていく。
1.Swalのコンテナにボーダーをつけたい!
→解決法
まず、customClass: 'customizable'のような感じで、アラート自体にclassを付与します。
で、次はcssにdiv.customizable{ border: 3px solid black !important; }と記述します。
すると、class名がかかっている要素にcssをかけることができます。ただ、それだけでは動かずcssに
!important
を追加してあげる必要があります。
これにより、優先度を上げることができる!2.タイトルの文字色などを変えたい!
→解決法
先ほどと、解決法にさほど変わりはないが、
付与したclassの階層下の要素がどうなっているかをよく見ましょう。すると、
h2
とか、label
とかが存在しています。
それらにcssを直に付与すれば良いわけです。.customizable h2{ color: red !important; }と今回も同様に
!important
は必要です。
5.〆
わかったことは、cssは割とゴリ押しでもどうにかなる。
そして、このやり方はだいぶキモイ。正規のやり方があるなら、正規のやり方でやりたい。
でも、英語しかなくてわからない。つまり、英語を翻訳した方が良さそうということがわかった。
最近、英語の翻訳系ばっかやってる気がするw長々としょうもない備忘録にお付き合いいただきありがとうございました。
- 投稿日:2019-12-03T15:05:11+09:00
俺の正規表現は長い
この記事は株式会社ACCESS Advent Calendar 2019の7日目の記事です。が、内容は個人的なものです。
まえがき
皆さん正規表現は好きですか?僕は嫌いです。
そんなある日、「7の倍数」を表す正規表現 という記事を見つけました。
ちょっと分かりづらかったですが、「7の倍数」という言語を受理するDFAを作り、そこから機械的に正規表現を作っていったわけですね。DFA -> 正規表現 の変換は StackExchange のこの質問ページの図で分かりやすく表現されています。
しかし、なんとこの正規表現、期待通りに動いてくれません。Chrome DevTool で
(問題の正規表現).test('7')
を評価するとfalse
になってしまいました。正規表現自体は正しいはずなのに。
僕がいつも使ってる Elixir くんならうまくやってくれるはず…
$ iex Erlang/OTP 20 [erts-9.3.3.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false] Interactive Elixir (1.9.4) - press Ctrl+C to exit (type h() ENTER for help) iex(1)> ~R/\A((...中略...))+\z/ \ ...(1)> |> Regex.match?("7") ** (Regex.CompileError) regular expression is too large at position 19647 (elixir) lib/regex.ex:209: Regex.compile!/2 (elixir) expanding macro: Kernel.sigil_R/2 iex:1: (file) (elixir) expanding macro: Kernel.|>/2 iex:1: (file)
regular expression is too large
と悲鳴を上げて死んでしまいました。JavaScriptでダメだったのも実は正規表現が長すぎたためかもしれません。
こうなると悪戯心に火が付きます。
正規表現の長さの限界は?
僕の検索力が足りず、明確にいくつみたいなのが書いてあるドキュメントが見つかりませんでした。
(Elixir -> Erlang のre
-> PCRE という依存までは見えましたがPCREでの制限がどうなってるのかよく分からず…)なのでテキトーに二分探索してみます。結果は環境に依存する可能性があります。
JavaScript
const testN = (n) => { const source = Array(n).fill('a').join``; try { return new RegExp(source).test(source); } catch(_) { return false; } }; testN(50000); // false const tryAndContinue = (previousN, currentN) => { const diff = Math.abs(currentN - previousN); if (diff <= 1) { return [previousN, currentN]; } const nextN = currentN + (testN(currentN) ? 1 : -1) * (diff >> 1); return () => tryAndContinue(currentN, nextN); } let continuation = tryAndContinue(50000, 25000); while(typeof continuation === 'function') { continuation = continuation(); } continuation // (2) [32765, 32766] testN(32766); // true testN(32767); // true testN(32768); // false
a
を 32768個並べるだけでアウト、ということが分かりました。Elixir
test_n = fn n -> source = String.duplicate("a", n) case Regex.compile(source) do {:ok, regex} -> Regex.match?(regex, source) _ -> false end end Stream.unfold({50000, 25000}, fn {previous_n, current_n} -> diff = abs(current_n - previous_n) cond do diff <= 1 -> nil test_n.(current_n) -> next_n = current_n + div(diff, 2) {next_n, {current_n, next_n}} :else -> next_n = current_n - div(diff, 2) {next_n, {current_n, next_n}} end end) \ |> Enum.take(-3) # => [32768, 32765, 32764] test_n.(32764) # => trueこちらは
a
を 32765個並べるとアウト。JavaScriptに近いですがちょっとだけ違いがありますね。もっと短く壊す
((((((((((a))))))))))
みたいな感じで()
を多用したりして複雑にすると上限がもっと短くなります。
最初の正規表現は()
だけでなく|
や[
、*
、+
も使っているのでダメだった感じですね。みなさんもぜひお気に入りの言語で試してみてください。
QAの人に前後を挟まれたためか、奇しくも「コーナーケースのテスト」みたいな話になりました。明日の記事は @illypong さんが数多のバグをワンパンしてくれそうな雰囲気です。お楽しみに!
- 投稿日:2019-12-03T14:40:30+09:00
【GAS】すでに同じフォルダ内に同一ファイル名が存在した時、連番をふって命名する
Driveは最近、ファイル自体をIDで管理するようになったので、ファイル名自体の重複を気にしなくなった。ひとえに不便なので機能を @acro5piano と協力して作成した。
function fileExists(files) { return files.hasNext() } function getNextFileNameFromOutputFolder(outputFolder, fileName, index) { index = index || 0 var maybeFileName = index === 0 ? fileName : fileName + "(" + index + ")" if (fileExists(outputFolder.getFilesByName(maybeFileName))) { return getNextFileNameFromOutputFolder(outputFolder, fileName, index + 1) } return maybeFileName } function incrementFileNumber(OutputFileName, folderId) { var OutputFolder = DriveApp.getFolderById(folderId); return getNextFileNameFromOutputFolder(OutputFolder, OutputFileName) }
- 投稿日:2019-12-03T14:27:40+09:00
SPAってページ遷移するの?
概要
- SPAにおけるルーティングについて簡単に紹介します
- SPAを作ったことがない人が対象読者です
- 知ってる人からすると、そんな当たり前なことを・・と思うかもしれません
SPAとは?
- Single Page Applicationの略です
- 対義語はMultiple Page Applicationです
- つまり単一ページで構成されるのがSPAで、複数ページで構成されるのがMPAです
- SPAは最初に画面(HTML)を取得した後は、JavaScriptによって画面の書き換えを行うことで単一ページでありながらアプリケーションを構築することができます
- これは言い換えると
SPAはページ遷移しない
ということ?本当にそうでしょうか?SPAはページ遷移しない?
- 2つのデモページを用意しました
- それぞれのページのGIFです
SPA
MPA
- どちらのページもリンクをクリックするとURLが切り替わってページ遷移しています
- どういうことでしょう???
SPAにおけるページ遷移
URLの書き換え
- SPAはJavaScriptで画面を操作することになりますが、実はアドレスバーのURLはJavaScriptで操作できるんです
- ブラウザにはHistoryAPIというJavaScriptのAPIが実装されており、それらを使うことでブラウザの履歴を操作できます
- アドレスバーのURLは
history.pushState()
で書き換えられます- 試しに開発者ツールで実行してみた例
- 実行するとURLが書き換わるのが分かると思います
- ブラウザのHistoryを操作しているためブラウザバックにも対応します
- この関数はあくまでURLを書き換えるだけなので画面に表示されている内容に変化はありません
画面の書き換え
- SPAにおける画面の書き換えは、「ボタンをクリックしたこと」や「通信が完了したこと」などのイベントを検知して処理を実行します
- 「URLが書き換わったこと」もイベントの1つに過ぎず、それを検知して画面全体を書き換えているというわけです
- 実際の開発ではReact RouterやVue Routerといったライブラリを用いることが多いです
- どのURLに変更されたらどのコンポーネントを表示するかマッピングしておくことで、MPAの画面遷移のように扱うことができます
まとめ
- SPAってページ遷移するの?
- ユーザ視点で見た場合、MPAと同じようにページ遷移することができます
- SPAってどうやってページ遷移してるの?
- ブラウザのHistoryをJavaScriptで操作することでURLを書き換えることができます
- URLの更新を検知して画面を書き換えることでページ遷移を表現しています
余談
- 途中紹介したSPAとMPAのサンプルは全く同じ挙動をするわけではありません
- SPAは
/home
などでリロードすると404(Not Found)となってしまいます
- 仕組みを考えるとあたりまえで、サーバ上には初回アクセス時に取得する
/index.html
しかなく/home.html
などは存在しないためです- 404を出さないためには、SPAを配信するホスティング側の設定で404が出た場合に
/index.html
にforwardしてあげればOKです- 今回サンプルで使っているNetlifyでは
_redirects
というファイルを作成し以下のように設定すればOKです_redirects/* /index.html 200
- この辺はホスティングサービスごとに異なるのでそれぞれ調べてみてください
- URLダイレクトアクセスにも対応したサンプル
- ここまでできるとユーザ視点ではSPAもMPAもほとんど変わらないてすね!
- 投稿日:2019-12-03T14:03:54+09:00
ざっくりwebpack ~その壱~
概要
開発メンバーの偉い人が環境作ってくれた上にREADMEに事細かくビルド方法とか環境構築のやり方が書いてあって
npm
かyarn
叩けばすべてオッケーみたいな状態になってる事ない?
そんで他の開発環境作ることになっても「偉い人が書いたコードだし」みたいな感覚でそのままコピペして使って...そしてぬるま湯に浸かったまま自分が環境作っていかなきゃならなくなった時にきっとこう思う
「webpackってどうやって使うんだよ...」このまま行けば間違いなく訪れるであろう悲劇を回避するためにも、少しずつでもざっくりでも知っておくことが必要だと感じたので書き記す
こんな感じからスタート
まずはバンドルツールとして基本的な流れを追ってみるのだ
scripts
配下のjsどもをwebpack
を使ってバンドるpublic/ ├ index.html └ app.bundle.js src/ ├ scripts/ │ ├ hoge1.js │ ├ hoge2.js │ └ hoge3.js │ └ app.js └ webpack.config.js package.jsonまずはじめに
使う前に必要なパッケージはいれないとな!
ターミナル$ yarn add -D webpack webpack-cli babel-loader @babel/core @babel/preset-env
webpack
-> webpackの事やるんだからそりゃ入るwebpack-cli
-> 4以前は一緒になってたけど、分割されたらしいのでこれもいれるbabel-loader
-> ES6で書くから必要@babel/core
-> babelの本体@babel/preset-env
-> 変換設定するためのやつQ.まずローダーってなんだろ? A.webpack自体はjavascriptしか分かんないから、他の言語で書かれた(cssとか)ファイルをモジュールに変換してくれる機能...らしい Q.でもES6ってjavascriptじゃん A.ES6対応してないブラウザ多いからbable-loader通してbabelで古い書き方に対応してくれる感じ? Q.@babel/preset-envってなんすか? A.上でES6対応してないブラウザ多いって言ったけど、ブラウザもバージョン上がったら順次対応はしていくわけで。 ブラウザのバージョンとか指定するとその環境に合わせてよしなにやってくださるようです。modeの設定
ではwebpack.config.jsを書いていく
まずはじめにmode
の設定weback.config.js// outputのパスを絶対パスで書く必要があるのでパスモジュール読込 const path = require('path') module.exports = { // webpack4からmodeを設定しないと警告が出るようになったので設定 mode: 'development' }コメントに書いたけど
mode
の設定をしないと文句言われるターミナルWARNING in configuration The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
mode:
production
development
none
の3つある
productionモードでビルドすると圧縮される。
あとnone
ていつ使うの?
development
production
どうです?圧縮されてるでしょう?
enrtyの設定
mode
の設定ができたので、enrty
を書いていくwebpack.config.jsmodule.exports = { // webpack4からmodeを設定しないと警告が出るようになったので設定 mode: 'development', // エントリーポイントの設定 entry: './src/app.js' }
entry:
ここで指定された
.js
を親分として、子分どもをまとめてくれる
複数の親分(エントリーポイント)を指定する事も可能outputの設定
続いて
output
を書く
その名の通り出力ファイルや出力先の設定module.exports = { // webpack4からmodeを設定しないと警告が出るようになったので設定 mode: 'development', // エントリーポイントの設定 entry: './src/app.js', // 出力先を設定 output: { // 出力する時の名前 filename: 'app.bundle.js', // 出力先のファイルパス(絶対パスじゃないとダメ) path: path.join(__dirname, 'public') } }
filename:
その名の通り出力する時のファイル名
[name]とか[hash]とかあるけど、また後日書いてみようと思う
path:
出力先のディレクトリパスを指定するが、
絶対パス
で指定する必要があるターミナルInvalid configuration object. Webpack has been initialised using a configuration object that does not match the API schema. - configuration.output.path: The provided value "./public" is not an absolute path! -> The output directory as **absolute path** (required).ここもコメントにあるけど、
'./public'
みたいに相対パス
で書くと文句言われるんじゃビルド
ファイルの入出力に関する設定が終わったので、ビルドしてみるぞ
package.json
にこれを追加するpackage.json"scripts": { "build": "webpack" },そしたらコマンド叩く
ターミナル$ yarn build
or
ターミナル$ npm run build
public/
が生成されて、app.bundle.js
が出力されたな?されたよな??
されたら続きは其の弐で!
- 投稿日:2019-12-03T13:52:21+09:00
年末まで毎日webサイトを作り続ける大学生 〜46日目 Cookieを使う、その他〜
はじめに
こんにちは!@70days_jsです。
Cookieを使ってみました。
その他色々JavaScriptの機能を使ってみました。今日は46日目。(2019/12/3)
よろしくお願いします。サイトURL
https://sin2cos21.github.io/day46.html
やったこと
- documentオブジェクトを利用する
- onselectionchangeイベントと、getSelectionを利用する
- cookieを利用する
では1からいきます。
1. documentオブジェクトを利用する
ざっとこんな感じです。↓
<div id="test"></div>// 最終更新日 lastModified test.innerHTML += '最終更新日: ' + document.lastModified + '<br>'; // ドメイン名 domain test.innerHTML += 'ドメイン名: ' + document.domain + '<br>'; // タイトル title test.innerHTML += 'タイトル名: ' + document.title + '<br>'; // プラグイン数 plugins for (var i = 0; document.plugins.length > i; i++) { plugins += document.plugins[i] + ','; } test.innerHTML += 'プラグイン数: ' + plugins + '<br>';documentオブジェクトは色々な情報が取得できて便利ですね。
当たり前ですが、どれもdocumentの後にプロパティ名をつけるだけで呼び出せます。2. onselectionchangeイベントと、getSelectionを利用する
名前だけじゃ分かりづらいですね。
onselectionchangeイベントは選択範囲の変更時に起きるイベントです。
getSelectionは選択範囲の文字列を調べるメソッドです。
gif↓
あらかじめ用意しておいた文字列を選択すると、その文字列をinnnerHTMLに加えていくように書いています。
<div id="field2"> <div id="charSelect">選択した文字列: </div> 何だろう 。 どうしてか 、 僕は 今日 </div>// 選択範囲に変化があったときに起こるイベント onselectionchange document.onselectionchange = function () { // 選択された文字列を調べる getSelection() let select = document.getSelection(); charSelect.innerHTML += select; }onselectionchangeイベントもdocumentから取っています。
3. cookieを利用する
太郎と名前を入力すれば、それをnameに入れてcookieで記録するようにしています。
もし、nameに値があれば、h2タグに値をいれて名前を表示します。<h2 id="myName"></h2> ... <div id="field3"> <button id="test2">cookieを作るボタン</button> <div id="cookieField"></div> </div>test2.addEventListener('click', test3); function test3() { let name = prompt('名前を入力してください。'); document.cookie = 'name=' + name; document.cookie = 'your_type = shy'; cookieField.innerHTML += 'クッキー: ' + document.cookie + '<br>'; takeCookie(); myName2(); } function takeCookie() { if (document.cookie) { let cookie = document.cookie.split(';'); cookie.forEach(function (value) { let content = value.split('='); cookieField.innerHTML += 'クッキーの値だけ: ' + content[1] + '<br>'; if (content[0] == 'name') { cookieName = content[1]; } }) } }; let myName = document.getElementById('myName'); function myName2() { if (cookieName) { myName.innerHTML = 'Hey, ' + cookieName + 'さん!'; } };どうでもいいことを色々書いているので長くなっていますが、要はdocument.cookieで値を代入していて、cookie.forEachで値を取り出しているだけです。
cookieは一連の文字列なので、;で区切ってやります。
その後に=で区切って名前だけを取り出します。感想
最近眠れなくて脳がまともに働きません。プログラムミスかな?
冗談です。最後までお読みいただきありがとうございます。明日も投稿しますのでよろしくお願い致します。
- 投稿日:2019-12-03T13:41:44+09:00
Javascript初心者の 非同期処理 Promise async await
はじめに
私はJavascript初心者です。
今までバックエンドエンジニアをやってきたので、Javascriptの概念に追いつくのに時間がかかりました。その中で私を苦しめたのは、並列処理と非同期処理でした。
また、それを解決してくれたPromise async awaitについてのお話をしようと思います。
並列処理に関しては、またの機会に・・・。そもそも非同期処理ってなに?
非同期処理とは・・・色々な尊敬すべき先輩たちが語っているので、私から説明するのもおこがましいのですが、簡単に説明します。
一言で言うと、I/O待ちをしないってことです。
別な言い方をすると、実行できる時に実行するってことですね。具体的な例を出してみましょう。
この記事を読んでいるJavascript初心者の皆さん。ボタンのクリックイベントって違和感ありませんか?
私は同期処理ばかり書いてきたので、違和感がありました。
だってプログラムって上から順番に実行されるものでしょ?それが鉄則。そう思っていた時期が私にもありました。Javascriptのボタンクリックイベントは、対象のボタンイベントを覚えていて、ボタンが押された時に実行される。
これは違和感ありありでした。ざっくりですが、これが非同期処理で言う、実行できる時に実行をする。と言う考え方です。
理解しにくい”時間がかかる処理”
ボタンクリックイベントなんかは、なんとなく理解できました。
JS読み込み時にクリックイベントが存在することを認識させておけば、クリックされた時点で、クリックイベントを発火できる!なんだよ!楽勝じゃん!
とは問屋がおろしませんでした。本質的には同じことなのですが、少し理解しにくい"時間がかかる処理"について。
例えば、今回仕事で利用したFirebaseです。
これ、Node.jsで作成して、サーバ側もJSでかけまっせ!ってやつです。
Node.jsは某有名な騎空団なゲームも利用しています。
「C10K問題」(クライアント1万台問題)を解消してくれるすごいやつです。
「C10K問題」や「Firebase」についてはまた今度機会があったら記事にしたいです。Firebaseには、FirestoreっていうDBがあるのですが、クライアント側からFirestoreにアクセスすると時間がかかる。
PHPなんかで言うと、DBConnectionですね。先ほどもお話したように、時間がかかる処理はI/O待ちしませんので、例えば以下のようなコードを書くと問題が起こります。
const snapshot = firebase.firestore().collection("aaa").get(); var snapDatas = snapshot.data(); for(const data of snapDatas){ ........処理...... }さて、どんな問題が起こるでしょうか?
Javascriptさんの気持ちを代弁しましょう。
Javascriptさんは基本的にせっかちです。
const snapshot = firebase.firestore().collection("aaa").get();Javascriptさん「Firestoreに接続しに行ってるよー」
Javascriptさん「.............まだー?」
Javascriptさん「応答ないから先行くねー!」と結果が返ってくる前に先に進んでしまいます。
その結果var snapDatas = snapshot.data();Javascriptさん「"undefined"って言われたー」
と言ってエラーを吐きます。
そしてプログラムが動き終わって暫くたった頃、Javascriptさん「snapshotさん帰ってきたー」
と反応があります。
おせぇぇぇぇぇぇぇぇぇぇぇぇ!
Javascriptさん的には、今処理が終わったようです。
実行できる時に実行する。。。。。なるほどそう言うことね。。。。
ともあれ、ほしい時にデータが返ってこない!これ・・・どうすんの?
これを解決してくれたのが、Promise async awaitでした。Promiseってなに?
一言で言うと、非同期のJSを同期っぽく動作させてくれる関数です。
callbackでも同様の処理は一応作れますが、めちゃくちゃ複雑化するので、使わない方針で。
その辺はググってもらえればエンジニアの悲喜交々が聞こえてきますよきっと。
(IEの昔のバージョンはPromiseに対応していないため、悲鳴をあげているのはIEが要件に入ってるエンジニアなのではないかなー?)Promiseの基本的な使い方は以下の通りです。
//Promise関数を利用して、本日の日付を取得する。 var result = new Promise(function(resolve){ resolve(new Date); }); //resultを実行した後に、年を取得する var res = result.then(function(data){ console.log(data.getFullYear()); });ポイントはresolve()と、then()です。
resolve(new Date);
とやると、戻り値に「new Date」が設定されます。
thenは、Promise関数実行後に、実行してほしい処理を記載します。then(function(data))
のdataにnew Dateが入っているわけですね。読みにくい問題
Promiseで記載してあげると、順番を指定できることがわかりました。
しかし、そうすると結構読みにくくなる印象です。
なぜなら、var res = result.then(function(dara){ var testData = ""; //ここに後続処理 . . . . . . . . . . . . });thenの中に記載する内容が長くなれば長くなるほど、下に読んでいって、上に戻らないといけないわけです。
これは読みにくい。async await
そこで登場するのがasync awaitです。
firebaseのソースを元に考えてみましょう。
まずはthenを利用した書き方。
ちなみに、firebase.firestore().collectiom("aa").get()が既にPromise関数で返ってくるため、
新規でPromise関数として宣言する必要はありません。var getFirebaseData = function(){ firebase.firestore.collection("aa").get().then(function(snapshot){ var snapDatas = snapshot.data(); for(const data of snapDatas){ ........処理...... } . . . . . . . . . . }); }となります。
続いて、async awaitを利用した記載方法
var getFirebaseData = async function(){ const snapshot = await firebase.firestore.collection("aa").get(); var snapDatas = snapshot.data(); for(const data of snapDatas){ ........処理...... } . . . . . . . . . . }こっちのが読みやすくないですか?
thenの中で書くと、スコープの管理もしなくちゃいけないですし、面倒が増えます。
何より、ごちゃつく!そのため、Promiseの同期処理をしたい場合は、async awaitを利用するのがおすすめです!
まとめ
・時間がかかる処理を実行したいときはPromiseを思い出してみて。
・Promiseを使ったらasync awaitがとっても便利!
- 投稿日:2019-12-03T13:37:27+09:00
AngularDart開発の最低限の基本
AngularDart、まだ多少しかやってないけど、最低限これだけあれば開発できるってのをまとめてみた。細かいとこは端折ってるので、他サイト見てね。
Dart
Flutter/Dartの入門サイトが多数あるので、初期設定する。
IDEはVSCodeがおすすめ。
Inteli Jがいいってサイトも多数あり。
なので、若干キモいけどAndroidStudioでも開発できる。とりあえず雛形生成して、IDEで開く。
stagehand web-angular pub getwebdev serveするとローカルで動く。auto restartはセッションとかがダメで、そのうちできなくなるのであてにしない。
コンポーネントはhtml用コードで一対一が基本
xx_component.html/xx_component.dartで1画面を構成する。雛形を参考に。
複数画面あるときはコンポーネントを複数作る。複数にしたら、appコンポーネントに追加する。
app_component.dartdirectives: [ routerDirectives, ProductsComponent, ListComponent, ],html側 {{インスタンス変数}}/ngIf/ngFor/(click)
<img src="{{header}}"> <p>{{item['price']}}</p>xx_component.dartのインスタンス変数がhtml側から参照できるので、htmlに{{インスタンス変数名}}とかで埋め込む。
itemがMapのときは「item['price']」とか書ける。つーかDartコードを埋め込める。<p class="price {{(downloaded[item['key']]!=null)?'downloaded':''}}">{{item['price']}}</p>dartそのまんま。
<img *ngIf="!headerLink" class="thumbnail" src="{{header}}">タグ内にifを書ける。falseのときはimgタグが無くなる。使うにはコンポーネントのdirectivesに追加する。NgForも同様。NgSwitchとかもある。
directives: [NgIf, NgFor],当然Listでループできる。
<li *ngFor="let item of citem['items']; let idx=index"> </li>コンポーネントのonClickMore(int idx,var citem)でクリックを受け取れる。
<a class="categoryTitle" (click)="onClickMore(idx,citem)">固定値クラスとかもhtmlから参照できる。コンポーネントのexportsに追加する。
exports: [MyString,MyStrId,Constant],てやると
<a *ngIf="downloaded[product['key']]==Constant.ITEM_DOWNLOADED" class="getButton downloaded">{{MyString.get(MyStrId.downloaded)}}</a>とかね。素敵。
パイプ
<div innerHtml="{{product|lang:'description'|mynl2br}}"></div>パイプで整形できる。nl2brがなぜか動かなかったので、そのまんまコピーで自作で作ってみた。langも自作でMapから設定言語にしたがったアイテム持ってくるやつ。
@Pipe('mynl2br') class MyNl2brPipe implements PipeTransform { String transform(String value, [bool isXhtml = false]) { // ignore: avoid_positional_boolean_parameters if (value == null) return ""; return value.replaceAll(RegExp(r'\r?\n'), isXhtml ? '<br/>' : '<br>'); } }コンポーネント側。commonPipesに色々入ってる。
pipes: [commonPipes, MyNl2brPipe],画面遷移(Router)
複数画面切り替える時は、Routerを導入する。呪文がたくさん必要。
angular_routerを追加。alphaだよ。
pubspec.yaml dependencies: angular: ^5.3.0 angular_components: ^0.13.0 angular_router: ^2.0.0-alpha+22main.dartfinal InjectorFactory injector = self.injector$Injector; void main() { //アプリ起動 runApp(ng.AppComponentNgFactory, createInjector: injector); }routes.dart追加して、URLを定義
routes.dartclass RoutePaths { ///URL セット static final setp = RoutePath(path: 'set'); ///URL カード表 static final cfp = RoutePath(path: 'cf'); ///その他全部 static final allp = RoutePath(path: '.+'); } class Routes { static final all = <RouteDefinition>[ RouteDefinition( routePath: RoutePaths.setp, component: product_temp.ProductsComponentNgFactory, additionalData: { "type": RoutePaths.setp.path, "index": 0, "css": "setList" }), RouteDefinition( routePath: RoutePaths.cfp, component: product_temp.ProductsComponentNgFactory, additionalData: { "type": RoutePaths.cfp.path, "index": 1, "css": "cFrontList" }), ]; }appコンポーネントのhtmlはこうする。
app_component.html<router-outlet [routes]="Routes.all"></router-outlet>index.htmlは全画面共通な感じになる。cssとかも。
index.html<body> <my-app>Loading...</my-app> </body>コンポーネントでこんなんで遷移できる。
Future<NavigationResult> onClickMore(int idx, Map item) { return router.navigate( RoutePaths.setp.path, NavigationParams(queryParameters: { "type": item["type"], "index": "$idx", "class": cssClass })); }routerは注入してくれる。
XxComponent(this.productsService, this.router);こういう風にも書ける。
<a [routerLink]="item['url']" [routerLinkActive]="'active-route'">{{item|lang:"name"}}</a>http通信
データはサービスで持つらしいぞ。
class ProductsService { //シングルトンのはずがなぜかだめなので、とりあえずstatic static List<Map<String, String>> _types; final Client _http;コンストラクタに引数追加すると、入れてくれる。
ProductsService(this._http) { }なんか呪文が必要だったけど、後ほど・・・
final response = await _http.get(ここにURL);リリースビルド
webdev buildすると、build以下にビルドされる。それをサーバーに持ってくだけ。
packagesはリリース時は要らないので、削除する。ビルドエラー
最近のバージョンはwebdevが頻繁にビルドエラー出すバグがある。
.dart_tool/build以下を消すと、概ね、直る。最後に
調べるの、ちょっと時間かかったけど、概ねこれだけ。Dartできれば数日で結構慣れる。あまり深く考えないで、雛形に従う。
慣れるとめっちゃサクサク開発できる。幸せが訪れる。たまにハマると非常に困る
呪文はちょっと端折ってるので、そのうち追加予定。