webpackでビルドしているVue.jsから、.envファイルの値を参照する
JavaScriptでAlgoliaのAPIを叩くときに、APP IDとAPIのKEYを引数に渡す必要があるんだけど、それらを外部ファイルにして、Gitにコミットしないようにしたかったけど、webpackの設定をどう書けばいいのかよくわかんなくてつらかったのでメモ。
Vue.jsをwebpack v4でビルドしてる環境です。
dotenvのインストール
$ npm install dotenv --save
ほかにもdotenv-webpackとかいろいろあって、どれをつかえば…!ってなってた。
.envの作成
ルート直下に.env
という名前のファイルを作成して、以下のような感じで保存する。
APP_ID=***** API_KEY=*****
.envをignore
.env
をGit管理化から除外するために、.gitignore
に.env
を追記する。
webpackの設定
webpack.config.js
に以下の内容を追記する。
require('dotenv').config({ path: __dirname + '/.env' }); const config = { plugins: [ new webpack.DefinePlugin({ 'process.env': { APP_ID: JSON.stringify(process.env.APP_ID), API_KEY: JSON.stringify(process.env.API_KEY) } }); ] };
.config()
の引数の { path: __dirname + '/.env' }
がないと .env
ファイルが読み込まれなくてそこでハマってた。
process.envの値を利用
Vueファイルからprocess.env.APP_ID
とprocess.env.API_KEY
を参照できるようになっているので、algoliasearch()
の引数に指定する。
import algoliasearch from 'algoliasearch'; const client = algoliasearch(process.env.APP_ID, process.env.API_KEY);
サンプルファイルを作成
.env
の値が空の状態のサンプルファイルを.env.example
とかいう名前(なんでもいい)で作成して、Gitにコミットしておく。
APP_ID= API_KEY=
他の人がこのリポジトリを触るときは、これらの値をなんらかの形で共有して、各自で.env
ファイルを作成してもらう。
Chrome拡張機能から特定のドメインのcookieの値を取得する
Chrome拡張機能のバックグラウンドスクリプトから特定のサイトのcookieの値を取得したくて、その方法について調べた。
といっても、👆の公式ドキュメント書かれてるとおり、 chrome.cookies
っていうAPIがChromeに用意されているので、それを使うだけなんだけど。
まず、マニフェストのパーミッションにcookie利用の旨と、取得対象のホストを指定する必要がある。でも、ドキュメントの例を参考にしてそのまま書くと Permission '*://*sample.com' is unknown or URL pattern is malformed.
というエラーになってしまうので注意が必要。
{ "name": "My extension", ... "permissions": [ "cookies", "*://*.sample.com" ], ... }
正しくは、以下のように、ドメインの後ろにスラッシュが必要だった。
{ "name": "My extension", ... "permissions": [ "cookies", "*://*.sample.com/" ], ... }
マニフェストを指定したら、あとはバックグラウンドスクリプトで chrome.cookies.get()
の第一引数にパラメータ、第二引数にコールバック関数を指定して実行するだけ。
function getUserID() { return new Promise((resolve, reject) => { chrome.cookies.get( { url: 'https://www.sample.com/', name: 'USERID', }, cookie => { return cookie ? resolve(cookie.value) : reject(new Error('no cookie')); } ); }); }
値をPromiseで返すようにしとけば、この関数を使う先で、cookieの値を取得したあとに何かしらの処理をするってするとき楽かなと思って👆みたいに書いた。
Chrome拡張、いろいろできておもしろい。
JavaScriptで文字列をShift_JISに変換&URLエンコードする
ユーザーが入力した文字列を元にして、そのキーワードの検索結果ページへリンクしたいけど、そのキーワードをJavaScriptでShift_JISにエンコードしないといけないという仕様を実装した。JavaScriptで文字コードを変換するというのをしたことがなくて調べたので、忘れないうちにメモ。
文字コードをUnicodeからShift_JISに変換
JavaScriptの世界の文字コードはUnicodeなので、まずは、入力されたキーワードをShift_JISに変換する必要がある。
npmにencoding-japaneseというライブラリがあったので、利用することに。
ちなみに、文字コードの変換だけだと iconv-lite - npm の方が有名っぽい雰囲気だったけど、encoding-japaneseはURLエンコードもできるからそっちにした。
JavaScriptでの文字コード変換のことをまったく理解していなくて、最初は文字列をそのまま渡せばいいのかと思ってたけど、文字列を1文字ずつ分解して、文字コードの数値の配列に変換しないといけないっぽい。ぐぐってると「コードユニット値」とか「コードセットの値」とかいろんな言葉の定義が出てきてよくわからなかったけど、とりあえず数値の配列にする。
JavaScript標準の.charCodeAt(i)
メソッドで文字コードの数値を取れるから、文字列の長さ分ループしてindexを指定したらできた。
import encoding from 'encoding-japanese'; const keyword = 'キーワード'; // キーワードを文字コードの数値の配列に変換 const unicodeArray = []; for (let i = 0; i < keyword.length; i++) { unicodeArray.push(keyword.charCodeAt(i)); }
これで文字コード変換の準備ができたので、encoding-japaneseのconvert()
を使ってShift_JISに変換する。
// Shift_JISに変換 const sjisArray = encoding.convert(unicodeArray, { to: 'SJIS', from: 'UNICODE', });
Shift_JISの文字コードの配列をURLエンコード
encoding-japaneseのurlEncode()
でURLエンコードができるので、先ほど生成したShift_JISの文字コードの配列を引数に渡して、URLエンコードした文字列を生成する。
// SJISのキーワードをURLエンコード const encodedKeyword = encoding.urlEncode(sjisArray);
あとは、テンプレートリテラルなりなんなりで、検索結果ページのURLを組み立てて完成!
// リンク先のURLを生成 const href = `https://www.example.com/search?keyword=${encodedKeyword}`;
まとめ
全部のコードを合わせるとこうなる。
import encoding from 'encoding-japanese'; const keyword = 'キーワード'; // キーワードを文字コードの数値の配列に変換 const unicodeArray = []; for (let i = 0; i < keyword.length; i++) { unicodeArray.push(keyword.charCodeAt(i)); } // Shift_JISに変換 const sjisArray = encoding.convert(unicodeArray, { to: 'SJIS', from: 'UNICODE', }); // SJISのキーワードをURLエンコード const encodedKeyword = encoding.urlEncode(sjisArray); // リンク先のURLを生成 const href = `https://www.example.com/search?keyword=${encodedKeyword}`;
便利なライブラリがあるというのに、文字コードの概念がよくわからなくて使い方が分からず、結構悩んでしまった。
最初の方、文字列そのまま渡しても変換できず、ドキュメント読んだらArrayを渡すとか書いてあるけど、なんの配列なんだろって思って、文字列を一文字ずつ分解した配列とか渡してた😅
Nuxt.jsへのStorybookの導入と、Sassの変数や共通CSSを読めるようにする設定
かなり疲弊した。バージョンは次のとおり。
"nuxt": "2.4.0"
"@storybook/vue": "5.0.11"
Nuxt.jsにStorybookを追加
Storybookの公式ドキュメントを参考にした。手動で入れる方法もあるみたいだけど、CLIの方が簡単そうなので任せることに。
npx -p @storybook/cli sb init --type vue
Storybookを起動する
設定ファイルやデモ用のコンポーネントの生成、npm scriptへの追加などはCLIがすべて用意してくれるので、とりあえずもうStorybookの起動はできる。ここまではとても簡単。
npm run storybook
アドオンの追加
アドオンは、Storybookの拡張機能みたいなもので、これを使ってStorybookをカスタマイズできる。CLIを実行すると、デフォルトでActionsとLinksが最初から入ってたけど、それ以外にも、よさげなアドオンをいくつか入れてみることに。
でも、紹介されているアドオンすべてがVue.jsに対応しているわけじゃないので、Addon / Framework Support Tableで確認してどれを入れるか検討する。
アドオンを追加する場合、だいたい以下のような手順で入れられる。
npm i --save-dev {ADDON}
でインストールする/.storybook/addons.js
でインポートする- 全体の設定は
/.storybook/config.js
に記述 - 個別のストーリーの設定が必要なら
/stories/*.stories.js
に記述
Viewport
Viewportは、ChromeのDevToolsみたいに、Viewportをポチポチ切り替えて表示を確認できる。
インストール
npm i --save-dev @storybook/addon-viewport
/.storybook/addons.js
import '@storybook/addon-viewport/register'
/.storybook/config.js
import { addParameters } from '@storybook/vue' addParameters({ viewport: { defaultViewport: 'iphonex' } })
ここでは、デフォルトのVuewportのサイズをiPhoneXに設定した。
a11y
a11yは、アクセシビリティ的にNGな箇所をエラーで教えてくれる。
インストール
npm i --save-dev @storybook/addon-a11y --dev
/.storybook/addons.js
import '@storybook/addon-a11y/register'
/.storybook/config.js
import { withA11y } from '@storybook/addon-a11y' addDecorator(withA11y)
storybook-addon-vue-info
storybook-addon-vue-infoは、コードのプレビューやpropsの情報などを、自動で生成してくれるので、簡易的なドキュメントの代わりになりそう。Storybookの公式サイトにあるInfoはVue.jsに対応していなかったので、同じようなことができるstorybook-addon-vue-infoを入れた。
インストール
npm install --save-dev storybook-addon-vue-info
/.storybook/addons.js
import 'storybook-addon-vue-info/lib/register'
/.storybook/webpack.config.js
.storybook
以下にwebpack.config.js
を新規作成し、以下の設定を記述する。
module.exports = ({ config }) => { config.module.rules.push({ test: /\.vue$/, loader: 'storybook-addon-vue-info/loader', enforce: 'post' }) return config }
/stories/index.stories.js
import { withInfo } from 'storybook-addon-vue-info' storiesOf('MyComponent', module) .addDecorator(withInfo) .add( 'foo', () => ({ components: { MyAwesomeComponent }, template: '<my-awesome-component/>' }), { info: { summary: 'Summary for MyComponent' } } )
ストーリーごとにaddDecorator(withInfo)
で個別に指定しないと動かなかった。あと、info
もあわせて指定しないと動かなかった。
Moduleが見つからないエラーを解消
@
や~
を使ってパスを指定すると、Error: Can’t resolve ‘@/components/*’
のようなエラーとなるので、次の設定で解消する。
/.storybook/.babelrc
.storybook
以下に.babelrc
を新規作成し、以下の設定を記述する。
{ "presets": [ "@babel/preset-env", "babel-preset-vue" ] }
/.storybook/webpack.config.js
storybook-addon-vue-infoの設定で作成したwebpack.config.js
にaliasの設定を追記する。
const path = require('path') const rootPath = path.resolve(__dirname, '../') module.exports = ({ config }) => { config.resolve.alias['@'] = rootPath config.resolve.alias['~'] = rootPath config.module.rules.push({ test: /\.vue$/, loader: 'storybook-addon-vue-info/loader', enforce: 'post' }) return config }
Sassの変数とmixinをStorybook上で読み込みできるようにする
/components/*.vue
のファイルを/stories/*.stories.js
で読み込むと、変数やmixinを使っていたVueコンポーネントがエラーになってしまう。これは、StorybookはNuxt.jsのレールから外れることになるので、Nuxt.jsで共通のSCSSを読めるように設定していたとしても、Storybookで別途設定しないといけないから。
/.storybook/webpack.config.js
webpack.config.js
に以下の設定を追記する。
これまでの設定も含めた最終的なコードは以下のとおり。
const path = require('path') const rootPath = path.resolve(__dirname, '../') module.exports = ({ config }) => { config.resolve.alias['@'] = rootPath config.resolve.alias['~'] = rootPath config.module.rules.push({ test: /\.s?css$/, loaders: [ 'style-loader', 'css-loader', 'sass-loader', { loader: 'sass-resources-loader', options: { resources: ['./assets/stylesheets/_variables.scss', './assets/stylesheets/_mixins.scss'], include: path.resolve(__dirname, '../') } } ] }) config.module.rules.push({ test: /\.vue$/, loader: 'storybook-addon-vue-info/loader', enforce: 'post' }) return config }
共通のスタイルを適用できるようにする
Decoratorという機能を使うとStorybook上で共通のスタイルを読み込むことができるので、リセット用のCSSや、サイト共通で使うCSSコンポーネントのスタイルを読み込んでおくとよさそう。
/.storybook/Decorator.vue
.storybook
以下にDecorator.vue
を新規作成して、以下のコードを記載し、読み込みたい共通CSSを<style lang="scss">
内でインポートする。
<template> <div class="decoarator"> <slot name="story"></slot> </div> </template> <script> export default { name: 'Decorator' } </script> <style lang="scss"> @import "@/assets/stylesheets/_normalize.scss"; @import "@/assets/stylesheets/_base.scss"; @import "@/assets/stylesheets/_components.scss"; </style>
/.storybook/config.js
config.js
にDecoratorを追加して、Storybook全体で共通CSSが読み込まれるようにする。アドオン等の設定も含めた最終的なコードは以下のとおり。
import { configure, addParameters, addDecorator } from '@storybook/vue' import { withA11y } from '@storybook/addon-a11y' import Decorator from './Decorator.vue' addParameters({ viewport: { defaultViewport: 'iphonex' } }) addDecorator(withA11y) addDecorator(story => ({ components: { Decorator }, render(h) { return ( <decorator> <story slot="story" /> </decorator> ) } })) // automatically import all files ending in *.stories.js const req = require.context('../stories', true, /\.stories\.js$/) function loadStories() { req.keys().forEach(filename => req(filename)) } configure(loadStories, module)
まとめ
こんなにがんばったのに、まだVuexをStorybookから読めるようにはなってないけど、つかれたので今日はここまで。
長方形の画像をCSSで上下左右中央に配置して正円にトリミングする
アバター画像とかをCSSで丸くくり抜くときに、img
要素にborder-radius: 50%
を指定するか、以下のような感じにすれば丸くくり抜きできるけど、画像が正方形じゃなかった場合に縦横比が伸びておかしくなってしまう。
<span class="avatar"> <img src="https://github.com/tacamy.png" alt=""> </span>
.avatar { display: inline-block; overflow: hidden; border-radius: 50%; width: 32px; width: 32px; } .avatar > img { width: 100%; height: 100%; }
HTMLの構造は上記のままで、object-fit: cover
とtransform
を使ってCSSを以下のように指定すると、縦長でも横長でもどちらの場合でも、画像が上下左右中央にトリミングされて正円にピタッと収まる。
.avatar { display: inline-block; position: relative; overflow: hidden; border-radius: 50%; width: 32px; height: 32px; } .avatar > img { display: block; position: absolute; top: 50%; width: 100%; height: 100%; transform: translateY(-50%); object-fit: cover; }
例によってIE11は対応していない(caniuse)ので、対応するにはポリフィルが必要そうだけど、わたしは対応してないのでどのポリフィルが使えるのかは把握していないのでよいのがあったら教えてほしい。
すぐ忘れちゃうのでメモっといた。
thx to id:nakajmg
Lodashを使って2つのオブジェクトのDiffを抽出する
JavaScriptで2つのオブジェクトの差分を出したいとき、Lodashの omitBy
を使うと簡単に書けた。
const before = { a: 1, b: 2, c: 3 } const after = { a: 0, b: 1, c: 3 } const diff = _.omitBy(after, (v, k) => before[k] === v)
この場合、 diff
の結果は👇こうなる。
console.log(diff)
// { a: 0, b: 1 }
差分がない場合は空のオブジェクトが返ってくる。
ちなみに、 omitBy
の第一引数に渡すオブジェクトのkeyとvalueを基にしてもうひとつのオブジェクトの値と比較してるから、👇こんな感じだとDiffは出ない。 after
は a
っていうkeyしか持ってないから。
const before = { a: 1, b: 2 } const after = { a: 1 } const diff = _.omitBy(after, (v, k) => before[k] === v) console.log(diff) // {}
なので、オブジェクトを渡す順番を逆にしたら、どのkeyの値が変わったかというDiffなら取れるけど、
const before = { a: 1, b: 2 } const after = { a: 1 } const diff = _.omitBy(before, (v, k) => after[k] === v) console.log(diff) // { b: 2 }
基本、keyが同じなオブジェクト同士で、値だけが変わるみたいなときに使おう。
Vueのtemplateで1つのイベントに複数のハンドラを設定する
たとえば、button
要素のクリック時にonClickA
とonClickB
という2つのイベントハンドラを実行したいというケースで。
本来は、👇みたいにちゃんとメソッドにまとめてから指定してあげるべきなんだろうけど、
<button @click="onClick"></button>
methods: { onClick() { this.onClickA() this.onClickB() } }
👇こんな感じにしたいときもあるけど、これだと動かなかったので、
<button @click="onClickA, onClickB"></button>
👇()
をつけてみたら動いた。
<button @click="onClickA(), onClickB()"></button>
でもこんな書き方していいのかどうかはわからない。