tacamy--blog

JavaScriptを勉強中の人のブログです。

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_IDprocess.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の値を取得したくて、その方法について調べた。

developer.chrome.com

といっても、👆の公式ドキュメント書かれてるとおり、 chrome.cookies っていうAPIChromeに用意されているので、それを使うだけなんだけど。

まず、マニフェストパーミッション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エンコードする

ユーザーが入力した文字列を元にして、そのキーワードの検索結果ページへリンクしたいけど、そのキーワードをJavaScriptShift_JISエンコードしないといけないという仕様を実装した。JavaScript文字コードを変換するというのをしたことがなくて調べたので、忘れないうちにメモ。

文字コードUnicodeからShift_JISに変換

JavaScriptの世界の文字コードUnicodeなので、まずは、入力されたキーワードをShift_JISに変換する必要がある。

npmにencoding-japaneseというライブラリがあったので、利用することに。

www.npmjs.com

ちなみに、文字コードの変換だけだと iconv-lite - npm の方が有名っぽい雰囲気だったけど、encoding-japaneseはURLエンコードもできるからそっちにした。

JavaScriptでの文字コード変換のことをまったく理解していなくて、最初は文字列をそのまま渡せばいいのかと思ってたけど、文字列を1文字ずつ分解して、文字コードの数値の配列に変換しないといけないっぽい。ぐぐってると「コードユニット値」とか「コードセットの値」とかいろんな言葉の定義が出てきてよくわからなかったけど、とりあえず数値の配列にする。

JavaScript標準の.charCodeAt(i)メソッドで文字コードの数値を取れるから、文字列の長さ分ループしてindexを指定したらできた。

developer.mozilla.org

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を渡すとか書いてあるけど、なんの配列なんだろって思って、文字列を一文字ずつ分解した配列とか渡してた😅

あと、UnicodeUTF-16の違いもよくわかってないままだ。

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を実行すると、デフォルトでActionsLinksが最初から入ってたけど、それ以外にも、よさげなアドオンをいくつか入れてみることに。

でも、紹介されているアドオンすべてがVue.jsに対応しているわけじゃないので、Addon / Framework Support Tableで確認してどれを入れるか検討する。

アドオンを追加する場合、だいたい以下のような手順で入れられる。

  1. npm i --save-dev {ADDON}でインストールする
  2. /.storybook/addons.jsでインポートする
  3. 全体の設定は/.storybook/config.jsに記述
  4. 個別のストーリーの設定が必要なら/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: covertransformを使って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は出ない。 aftera っていう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要素のクリック時にonClickAonClickBという2つのイベントハンドラを実行したいというケースで。

本来は、👇みたいにちゃんとメソッドにまとめてから指定してあげるべきなんだろうけど、

<button @click="onClick"></button>
methods: {
  onClick() {
    this.onClickA()
    this.onClickB()
  }
}

👇こんな感じにしたいときもあるけど、これだと動かなかったので、

<button @click="onClickA, onClickB"></button>

👇()をつけてみたら動いた。

<button @click="onClickA(), onClickB()"></button>

でもこんな書き方していいのかどうかはわからない。