Vue-cli3 でグローバルなCSSを読み込む
引き続き、あまり需要がなさそうというか本来の意義から逸れていそうな目的のために Vue CLI と格闘しています。
ゴール
最終的に出力された index.html でSCSSからコンパイルした共通CSSファイルを読み込み、Vueコンポーネント外の要素にCSSを適用できるようにする。
方法は一応2つあります。グローバルにVueコンポーネントで読み込んでしまう方法と、JSで呼び出す方法です。ただ実際には後者が優先になると思います。参考にしたのはこちら
グローバルにVueコンポーネントで読み込む
vue.config.js
の module.exports
以下に次の設定を追加します。
module.exports = { css: { loaderOptions: { sass: { data: `@import "<読み込ませたいscssファイル>";` } }, }, ... }
すると、指定したファイルのスタイルを挿入した上でコンポーネントをコンパイルするようになります。コンポーネントのSCSSを scoped
にしていなければグローバルなスタイル適用の完成です。はい。
これはこの目的のために使っていい方法ではない
問題点
- 子要素から親要素にスコープを漏らすような書き方になっている。(全てのコンポーネントで
scoped
を指定すれば普通にコンポーネント外にはスタイルが適用されなくなります) - 全コンポーネントの
style
部に挿入されているだけであり、コンポーネントが複数あればその数だけ同じスタイルが増殖してしまう。
じゃあなんのためにこの機能があるかというと、mixin
やfunction
やvariables
などプリプロセッサ固有でありコンパイル後はCSSのコードとして出力されない設定要素を、いちいち全部のコンポーネントで import しなくてすむようにするためです。
公式でも、このオプションに指定するファイルには、CSSとして出力されてしまうスタイルを含まないよう勧告していました。*1
JSで呼び出す
main.js
に1行付け加えるだけ。
require('@/path/to/scss/entry.scss')
SCSSで書いていればモジュールを分割してエントリーポイントのようなファイルで一括読み込みしていると思うので、そのエントリーポイントを読み込みます。こうするとWebpackが勝手にコンパイルしていい感じにしてくれます。簡単。副作用なし。
まとめ
- 生CSSに変換されないプリプロセッサ固有の変数やmixinを各コンポーネントで読み込みたい時は
vue.config.js
内CSSパラメータのloaderOptions
を使う。 - グローバルなCSSファイルをテンプレートで読み込みたい時は
main.js
でrequire
する。
すっきり解決してついでにスマートなモジュール読み込み方法も知れたので、充実した話題でした。
(そもそもせっかくVueを使うならグローバルなCSSなんてものからは手を引けという声が聞こえる)
*1:ソースをメモしておくのを忘れたので間違ってるかもしれない
Google Spreadsheet を JSONファイルとしてダウンロードしたい(2)
前回
続きです。
ゴール(再掲)
- JavaScript から API を叩いて、スプレッドシートを json ファイルとしてローカルにダウンロードできるようにしたい。
- ただし業務効率化の一貫なので、作成するWebアプリのアクセスは社用のドメイン制限内に限る。
スプレッドシートをjsonファイルとして取得するAPIをGASで作成できたので、今回は認証込みでローカルからそれにアクセスします。
APIをcurlで叩く
結論から言うと認証付きでは簡単にはできない。
ブラウザで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。
とりあえず有効化してドキュメントに飛んでみます。
英文読解能力が怪しいですが、The Google Apps Script API replaces and extends the Apps Script Execution API.
って書いてあるし、認証情報の作成できるし、いけるのでは?とりあえず認証情報を追加しました。APIを呼び出す場所は ウェブサーバー
を選択。
認証情報(JSON形式)をダウンロードしましたが、どうやって使うのか皆目わからない。ギブアップして調べてみると、どうやらそもそもGASの方からプロジェクトに紐づける方法がありそう。
調べてみると、なんとすでに紐づいていました。
ええ?でもコンソールに最初にログインした時はこの名前のプロジェクトなかったのに……
リンクを開いてみると若干レイアウトとURLが違うコンソールが。
こちらにはGASに紐づけられたプロジェクトは存在しますが、これまでにつくったプロジェクトが存在しません。アカウントを切り替えたわけでももちろんありません。どうなってるんだ。そもそもなんでダッシュボードが2種類あるんだ。
どうやらGASからプロジェクトに飛んだ直後だけそのプロジェクト(だけ)が表示されるようです。全くわからないけど一時リンクのようなものなんだろうか。得体が知れないのでこれまでつくってきたプロジェクトにアプリを紐付けようとしたんですが、それっぽいプロジェクトIDが不正としか言われないので、仕方なく自動で紐づいていたプロジェクトを使うことにしました。同じようにApps Script APIを有効にして認証情報を獲得します。
APIの名前が変わっていること以外は変更がなさそうなので、前述のQiita記事にお世話になりアクセストークンを取得……と思ったらできない。
ウェブ アプリケーション のクライアント ID
以下の情報を編集しようとしたらエラーになりました。詰んだ。
Headless Chromeを使う
認証が必要なら、認証できるブラウザでアクセスすればいいじゃない
はい。心が折れました。こうなったら力技です。コマンドラインで操作できるブラウザを使います。
ちなみに最新のChromeであれば --disable-gpu
は不要です。自分は不要でした。*2
ただしこのヘッドレスChrome、ユーザプロファイルを保持してくれないので認証が必要なページにアクセスするには毎回ゼロからログインしなくてはいけません。そのログイン処理を puppeteer
というGoogle おすすめのライブラリで書いていきます。参考にしたのはこちら。
ユーザが操作するのと同じことを非常に直感的にスクリプトに起こせます。これぞ力技。
// 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段階認証を設定しているといちいちスマホで認証しなくてはいけません。実用性〜〜〜。どうあがいてもブラウザGUIにURIブクマして都度ダウンロードした方が早い。ただ2段階認証解除できるアカウントだったらこれ便利だと思います。*3そもそも認証を外せない状況なら2段階認証必須だと思いますが……
まとめ
諦めました。
多分トークンをどうにか取得する方法があると思うのですが……とりあえず手動でダウンロードできるようにはなったのでひとまずよしとします。個人的にGoogleさんのドキュメントは難易度が高いので今後の課題です。
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
として指定すればそのままのパスで出力されます。ファイルが多い時は処理用のパッケージがあるらしい。
極めて言葉だけでは伝わりづらいところ特に図解もしないのはこちらにアクセスすればすべてわかるからです。では次。
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()
とか見られます。
全ページ共通のデータを渡す
pug-plain-loader
の data
オプションを使います。
まずは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() }
上書きしつつオプションを渡します*1。vue 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渡っているのがわかります。serve
もbuild
も成功しデータが渡っているのを確認できました。
個別のデータを渡す
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やコードをの情報を駆使して問題解決する経験が図らずもできたのでよしとします。