FULL STUCK DIARY

だいたい行き詰まっている備忘録

Vue-cli3 でグローバルなCSSを読み込む

引き続き、あまり需要がなさそうというか本来の意義から逸れていそうな目的のために Vue CLI と格闘しています。

ゴール

最終的に出力された index.html でSCSSからコンパイルした共通CSSファイルを読み込み、Vueコンポーネント外の要素にCSSを適用できるようにする。

方法は一応2つあります。グローバルにVueコンポーネントで読み込んでしまう方法と、JSで呼び出す方法です。ただ実際には後者が優先になると思います。参考にしたのはこちら

forum.vuejs.org

グローバルにVueコンポーネントで読み込む

vue.config.jsmodule.exports 以下に次の設定を追加します。

module.exports = {
    css: {
        loaderOptions: {
            sass: {
                data: `@import "<読み込ませたいscssファイル>";`
            }
        },
    },
    ...
}

すると、指定したファイルのスタイルを挿入した上でコンポーネントコンパイルするようになります。コンポーネントのSCSSを scoped にしていなければグローバルなスタイル適用の完成です。はい。

これはこの目的のために使っていい方法ではない

問題点

じゃあなんのためにこの機能があるかというと、mixinfunctionvariablesなどプリプロセッサ固有でありコンパイル後はCSSのコードとして出力されない設定要素を、いちいち全部のコンポーネントで import しなくてすむようにするためです。

公式でも、このオプションに指定するファイルには、CSSとして出力されてしまうスタイルを含まないよう勧告していました。*1

JSで呼び出す

main.jsに1行付け加えるだけ。

require('@/path/to/scss/entry.scss')

SCSSで書いていればモジュールを分割してエントリーポイントのようなファイルで一括読み込みしていると思うので、そのエントリーポイントを読み込みます。こうするとWebpackが勝手にコンパイルしていい感じにしてくれます。簡単。副作用なし。

まとめ

  • CSSに変換されないプリプロセッサ固有の変数やmixinを各コンポーネントで読み込みたい時は vue.config.jsCSSパラメータの loaderOptionsを使う。
  • グローバルなCSSファイルをテンプレートで読み込みたい時はmain.jsrequireする。

すっきり解決してついでにスマートなモジュール読み込み方法も知れたので、充実した話題でした。

(そもそもせっかくVueを使うならグローバルなCSSなんてものからは手を引けという声が聞こえる)

*1:ソースをメモしておくのを忘れたので間違ってるかもしれない

Google Spreadsheet を JSONファイルとしてダウンロードしたい(2)

前回

ashesrl.hatenablog.com

続きです。

ゴール(再掲)

  • JavaScript から API を叩いて、スプレッドシートjson ファイルとしてローカルにダウンロードできるようにしたい。
  • ただし業務効率化の一貫なので、作成するWebアプリのアクセスは社用のドメイン制限内に限る。

スプレッドシートjsonファイルとして取得するAPIをGASで作成できたので、今回は認証込みでローカルからそれにアクセスします。

APIcurlで叩く

結論から言うと認証付きでは簡単にはできない。

ブラウザでjson取得は前回の通りできたので、ローカルのスクリプトから使う前にまずはcurlコマンドでAPIを叩いてみよう、と思ったらここでつまずきました。

$ curl -L *******(URL)
<!DOCTYPE html>
<html lang="en">
  <head>
  <meta charset="utf-8">
  <meta name="robots" content="noindex">
  <title>Sign in - Google Accounts</title>
  <meta http-equiv="refresh" content="1; url=******"></meta>
  </head>
  <body >
  <form id="hiddenget" action="https://www.google.com/accounts/AccountChooser?hd=*******" method="get">
  <noscript>
  You should turn on Javascript support.
  <input type="submit" id="nojssubmit" value="Continue">
  </noscript>
</form>
  <script nonce="******">
window.onload = function() {
  var redirectUrl = 'https:\x2F\x2Fwww.google.com\x2Faccounts\x2FAccountChooser?hd=*******';
  var domain = '***';
  var hash = window.location.hash;
  if (hash) {
  var match = hash.match(/[#&]Email=([^&]+)/);
  if (match) {
  redirectUrl += "&Email=" + match[1] + "@" + domain;
  }
  }
  window.location.replace(redirectUrl);
};
</script>
  </body>
</html>

****** で置き換えている部分は作成したWebアプリの実行URLやドメインなどです。

どうやらアカウントの認証をしなきゃいけないらしい。そういえばブラウザでアクセスした時も初回はログインのし直しを求められました。たぶんWebアプリとして作成する際に アプリケーションにアクセスできるユーザー全員(匿名ユーザーを含む) に設定していたら発生しないと思います。今回はどうしてもアクセスできるユーザをドメイン制限内にしたいので対策を探します。

ブラウザ以外から認証が必要なGASにアクセス

5年も前の情報ですがやりたいことが合致したフォーラム記事をみつけました。

認証が必要なGASウェブアプリケーションにRESTfulなアクセスを行いたい。
https://groups.google.com/forum/#!topic/google-apps-api-japan/J7V7rJ8Boj0

半分くらいよくわかりませんがとりあえずOAuth認証はできないらしい。 さらに調べてできそうな記事をみつけるも、こちらも情報が古いこともありよくわからない。

https://www.ka-net.org/blog/?p=4350

しかし Google APIs のコンソールページにアクセスし、新しいプロジェクト を作成して ライブラリ をみてみると、ありました、Apps Script API の文字が*1

console.developers.google.com

とりあえず有効化してドキュメントに飛んでみます。

developers.google.com

英文読解能力が怪しいですが、The Google Apps Script API replaces and extends the Apps Script Execution API. って書いてあるし、認証情報の作成できるし、いけるのでは?とりあえず認証情報を追加しました。APIを呼び出す場所は ウェブサーバー を選択。

Google APIs 認証情報の追加画面スクリーンショット
認証情報の追加画面

認証情報(JSON形式)をダウンロードしましたが、どうやって使うのか皆目わからない。ギブアップして調べてみると、どうやらそもそもGASの方からプロジェクトに紐づける方法がありそう。

qiita.com

調べてみると、なんとすでに紐づいていました。

GASの設定

ええ?でもコンソールに最初にログインした時はこの名前のプロジェクトなかったのに……

リンクを開いてみると若干レイアウトとURLが違うコンソールが。

CloudPlatformコンソール

こちらにはGASに紐づけられたプロジェクトは存在しますが、これまでにつくったプロジェクトが存在しません。アカウントを切り替えたわけでももちろんありません。どうなってるんだ。そもそもなんでダッシュボードが2種類あるんだ。

console.cloud.google.com

console.developers.google.com

どうやらGASからプロジェクトに飛んだ直後だけそのプロジェクト(だけ)が表示されるようです。全くわからないけど一時リンクのようなものなんだろうか。得体が知れないのでこれまでつくってきたプロジェクトにアプリを紐付けようとしたんですが、それっぽいプロジェクトIDが不正としか言われないので、仕方なく自動で紐づいていたプロジェクトを使うことにしました。同じようにApps Script APIを有効にして認証情報を獲得します。

APIの名前が変わっていること以外は変更がなさそうなので、前述のQiita記事にお世話になりアクセストークンを取得……と思ったらできない。

f:id:ashesrl:20181009175723p:plain

ウェブ アプリケーション のクライアント ID 以下の情報を編集しようとしたらエラーになりました。詰んだ。

photo-tea.com

Headless Chromeを使う

認証が必要なら、認証できるブラウザでアクセスすればいいじゃない

はい。心が折れました。こうなったら力技です。コマンドラインで操作できるブラウザを使います。

developers.google.com

ちなみに最新のChromeであれば --disable-gpu は不要です。自分は不要でした。*2

ただしこのヘッドレスChrome、ユーザプロファイルを保持してくれないので認証が必要なページにアクセスするには毎回ゼロからログインしなくてはいけません。そのログイン処理を puppeteer というGoogle おすすめのライブラリで書いていきます。参考にしたのはこちら。

honeybe.hatenablog.jp

ユーザが操作するのと同じことを非常に直感的にスクリプトに起こせます。これぞ力技。

// https://script.google.com/a/kayac.com/macros/s/AKfycbwm3FP7RyBb_Sw2ERGWzKo4aOwB0WrfDoW3V0tOqcNFJ_b1js8/exec
const puppeteer = require('puppeteer');
const pc = {
  'name': 'Chrome Mac',
  'userAgent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36',
  'viewport': {
    'width': 1024,
    'height': 820,
    'deviceScaleFactor': 1,
    'isMobile': false,
    'hasTouch': false,
    'isLandscape': false
  }
};

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  
  await page.emulate(pc);

  // ページを開く
  await page.goto('<JSON生成WebAppのURI>');

  // 読み込みを待つ
  await page.waitFor(5000);
  // 確認
  await page.screenshot({
    path: 'test/index.png',
    fullPage: true
  });

  // ログイン情報入力
  await page.type("input[type='email']", '<GoogleアカウントID>');
  // ログインボタン押下
  const nextButton = await page.$('div#identifierNext');
  await nextButton.click();

  // 読み込みを待つ
  await page.waitFor(5000);
  // 確認
  await page.screenshot({
    path: 'test/second.png',
    fullPage: true
  });

  await page.type("input[name='password']", '<パスワード>');
  // ログインボタン押下
  const loginButton = await page.$('div#passwordNext');
  await loginButton.click();

  // 読み込みを待つ
  await page.waitFor(10000);
  // 確認
  await page.screenshot({
    path: 'test/dashboard.png',
    fullPage: true
  });

  // ブラウザを閉じる。
  await browser.close();
})();

アクセスできました。ただアカウントが2段階認証を設定しているといちいちスマホで認証しなくてはいけません。実用性〜〜〜。どうあがいてもブラウザGUIURIブクマして都度ダウンロードした方が早い。ただ2段階認証解除できるアカウントだったらこれ便利だと思います。*3そもそも認証を外せない状況なら2段階認証必須だと思いますが……

まとめ

諦めました。

多分トークンをどうにか取得する方法があると思うのですが……とりあえず手動でダウンロードできるようにはなったのでひとまずよしとします。個人的にGoogleさんのドキュメントは難易度が高いので今後の課題です。

*1:場合によってはまず Google Cloud Pratform の登録・認証が必要かもしれません。

*2:version 69

*3:と言ってもJSONJSONとして保存するためにもうひと山ありそう。

Webpack(Vue-CLI3)で静的pugファイルのhtml生成と格闘した

環境構築の大敵便利ツール、Webpack。自分は本格的に使い始めて約半年、設定ファイルがいまだにほとんど理解できてないので固有のことをしようとすると毎回手こずります。さらにVue CLI3ではWebpackは包含されてしまっているのでよりややこしい部分があります。今回も例に漏れず苦難の連続だったのでメモ。

環境設定はこれの続きです。

ゴール

一言でいうとテンプレート的に作ったpugファイルにjsonからいくつかのデータを渡して、複数のディレクトリに内容違いのhtmlファイルを生成したい。整理すると以下です。

入力

  • index.pug (本文は変数になっている)
  • data.json (複数のオブジェクトの配列になっている)

出力

  • a/index.html
  • b/index.html
  • c/index.html
  • ...(全てレイアウト及び機能は同じだが文言が違う)

どうあがいてもわかりづらい。難所はいくつかあるのですが1つずつ解決していきます。

複数のentryから複数のディレクトリにoutputを出力

これは実は非常に簡単です。pagesに設定する時点で出力後の生成ファイルに期待する相対パスhushとして指定すればそのままのパスで出力されます。ファイルが多い時は処理用のパッケージがあるらしい。

qiita.com

極めて言葉だけでは伝わりづらいところ特に図解もしないのはこちらにアクセスすればすべてわかるからです。では次。

1つのentryから複数のhtmlを出力

上の項の応用ですね。これもまあ簡単です。シンプルに同じ entry および template を持っているが output が違うパラメータオブジェクトを量産してpagesに渡すだけです。

const meta = require('data.json');
(() => {
    meta.forEach(d => {
        index[`page-${d.id}`] = {
            entry: ENTRY,
            template: MAIN_TEMPLATE,
            filename: `${d.id}/index.html`,
        };
    });
})();

こんな感じ。index['page-${d.id}'] この部分のキー page-${d.id} はのちにパラメータを追加するときの識別子になります。

pugにオブジェクトを渡す

最大と言っていい難所。jsonからpugにデータを渡していきます。

ちなみにここで知りましたが、ターミナルで

$ vue inspect > result.js

とやると最終的にどんなプラグインがどのパラメータをもってどのように適用されているのか見ることができます。vue.config.js では省略されている new HtmlWebpackPlugin() とか見られます。

qiita.com

全ページ共通のデータを渡す

pug-plain-loaderdata オプションを使います。

まずはvue add pug でお手軽にセットアップしたので、実際何が入ってるのか調査します。vue-cli-plugin-pugのソースを見ると

    webpackConfig.module
      .rule('pug')
        .test(/\.pug$/)

        // this applies to <template lang="pug"> in Vue components
        .oneOf('vue-loader')
          .resourceQuery(/^\?vue/)
          .use('pug-plain')
            .loader('pug-plain-loader')
            .end()
        .end()

        // this applies to pug imports inside JavaScript, i.e. .pug files
        .oneOf('raw-pug-files')
          .use('pug-raw')
            .loader('raw-loader')
            .end()
          .use('pug-plain')
            .loader('pug-plain-loader')
            .end()
        .end()

こんな感じでチェインされているのがわかりました。これを使って自分の vue.config.js の chainwebpack に

    chainWebpack: config => {
        config.module.rule('pug').uses.clear();
        config.module
            .rule('pug')
            .oneOf('raw-pug-files')
            .use('pug-plain')
            .loader('pug-plain-loader')
            .options({
                root: path.resolve('src/pug/'),
                data: object
            })
            .end()
    }

上書きしつつオプションを渡します*1vue inspectすると

          /* config.module.rule('pug').oneOf('raw-pug-files') */
          {
            use: [
              {
                loader: 'raw-loader'
              },
              {
                loader: 'pug-plain-loader',
                options: {
                  root: '/path/to/src/pug',
                  data: { <object> }
                }
              }
            ]
          }

ちゃんとoptionsが無駄なく*2渡っているのがわかります。servebuildも成功しデータが渡っているのを確認できました。

個別のデータを渡す

html-webpack-plugin の templateParameters を使います。

    chainWebpack: config => {
        config.module.rule('pug').uses.clear();
        meta.forEach((data, num) => {
            config
                .plugin(`html-page-${data.id}`)
                .tap(args => {
                    args[0].templateParameters = {
                        ...data,
                        num
                    };
                    return args;
                });
        });
    },

ここで plugin に渡している html-page-${data.id} はhtml出力用の pages に渡したオブジェクト群のkeyに対応しています。vue inspect するとこう

    new HtmlWebpackPlugin(
      {
        templateParameters: {
          <dataの中身>,
          num: 0
        },
        chunks: [
          'chunk-vendors',
          'chunk-common',
          'page-<id>'
        ],
        template: '/path/to/src/pug/index.pug',
        filename: '<id>/index.html',
        title: undefined
      }
    ),
   new HtmlWebpackPlugin(
      ...

pagesに渡したオブジェクトの数だけ、それぞれのデータを含んだ状態で、 HtmlWebpackPlugin インスタンスが生成されているようです。ここからが問題。

次に vue add pug でインストールした vue-cli-plugin-pug をアンインストールして pug と pug-loader と pug-plain-loader と raw-loader をインストールします。(?!)

前項の続きでいけるかと思ったんですが、パラメータの読み込み順をいじれない問題で断念しました。どういうことかというと以下。

configureWebpackをこんな感じで設定します。

    configureWebpack: {
        module: {
            rules: [{
                test: /\.pug$/,
                oneOf: [
                    // this applies to `<template lang="pug">` in Vue components
                    {
                        resourceQuery: /^\?vue/,
                        use: ['pug-plain-loader'],
                    },

                    {
                        test: /index/,
                        use: ['pug-loader'],
                    },

                    // this applies to pug imports inside JavaScript
                    {
                        use: [
                            'raw-loader',
                            {
                            loader: 'pug-plain-loader'
                            },
                        ],
                    },
                ],
            }]
      ....
        }
     }

test: /index/ のindexはテンプレートとして使いたpugファイル名を入れます。

OneOf の中に3つのオブジェクトがあるわけですが、これは vue-cli-plugin-pug の index.js にある2つのルールの間に、まんまもう1つ割り込ませています。これはこの位置(raw-loader と pug-plain-loaderの前)にないとtemplateParameterが適用されないんですよね……chainWebpackで上書きする方法では最後尾にルールを付け足すことしかできないので、パラメータが読み込まれませんでした。ちなみに vue-cli-plugin-pug の index.js に直接ルールを付け足しても動くには動きます。

    webpackConfig.module
      .rule('pug')
        .test(/\.pug$/)

        // this applies to <template lang="pug"> in Vue components
        .oneOf('vue-loader')
          .resourceQuery(/^\?vue/)
          .use('pug-plain')
            .loader('pug-plain-loader')
            .end()
        .end()

       // 真ん中に割り込み
        .oneOf('pug-template')
          .test(/index/)
          .use('pug')
            .loader('pug-loader')
            .end()
        .end()

        // this applies to pug imports inside JavaScript, i.e. .pug files
        .oneOf('raw-pug-files')
          .use('pug-raw')
            .loader('raw-loader')
            .end()
          .use('pug-plain')
            .loader('pug-plain-loader')
            .end()
        .end()

node_modules の中をいじるとあとあとどういう地獄を見るのかゾクゾクしますね。

まとめ

というわけで色々回り道をしながらpugから内容違いのhtmlを生成することに成功しました。そもそもの話ですけどこれあまりVue-CLIで想定されている使い方ではない気がする。そういう意味で生産性が若干疑問ですが、各種レポジトリのissueやコードをの情報を駆使して問題解決する経験が図らずもできたのでよしとします。

*1:絶対もっとスマートな方法がある気がする

*2:ちなみに上のルールでruleとかoneOfとかを省略すると正しく認識されずここがややこしいことになります。