GAミント至上主義

Web Monomaniacal Developer.

ブログのRSSをCloud Functionsを使ってJSONに変換してajaxで使う

yagish履歴書で今後のアップデートのお知らせ用にはてなブログProを利用しようと考えている。

rirekisho.yagish.jp
(関係ないけどはてブ800超えてうれしい)

もちろんRSSがあるので、これをVue.jsアプリ内で読み込んでもいい。
でも、RSSとかXMLとかを扱うとなると、クライアント側にそれだけのためにライブラリがいくつも必要になる。
せっかくLighthouse対策でjsを減らすのがんばったのに嫌。

uyamazak.hatenablog.com

そこでCloud Functionsを使って、jsonに変換して返せないか考えた。
そうすれば、もともと使っているaxiosだけで済むし、コードも減るし、使いやすい。

まず、RSSJSONに変換するのはそのまんまのライブラリがあった。
すごいシンプルで言うことない。

www.npmjs.com

次にajaxで読み込むのでCORS対策。
Firebase HostingでCloud Functionsにリライトして接続すれば同一ドメインになるのでいらない。

Cloud Functions による動的コンテンツの配信  |  Firebase

でも今回はローカルでもテストしやすいのでCORSで許可することにした。

検索したらまさにのstackoverflowがあった。
stackoverflow.com

複数ドメインを動的に処理するのはcorsのドキュメントにあったのを使う。
github.com

これを組み合わせたのが下記。
これをCloud Functionsに作ってHTTPトリガーでデプロイする。
RSSの量によるかもだけどメモリは128MBでも良さげ。

Cloud FunctionsはCDNを通してるのでCache-Controlをつけておけば爆速で返せるので成功時だけつけておく。
期限は必要に応じてもっと短くてもいいかも。

res.set('Cache-Control', 'public, max-age=300, s-maxage=600');

index.js

const Feed = require('rss-to-json');
const RssURL = 'https://blog.yagish.jp/rss';
const whitelist = ['http://192.168.0.1', 'https://rirekisho.yagish.jp/']
const corsOptions = {
  origin: function (origin, callback) {
    if (whitelist.indexOf(origin) !== -1) {
      callback(null, true)
    } else {
      callback(new Error('Not allowed by CORS'))
    }
  }
}
const cors = require('cors')(corsOptions)

/**
 * Responds to any HTTP request.
 *
 * @param {!Object} req HTTP request context.
 * @param {!Object} res HTTP response context.
 */
exports.RssToJson = (req, res) => {
  Feed.load(RssURL, function(err, rss){
    cors(req, res, () => {});
    if(err){
      res.status(503).send(err);
    }else{
      res.set('Cache-Control', 'public, max-age=300, s-maxage=600');
      res.status(200).send(rss);
    }
  });
};

package.json

{
  "name": "sample-http",
  "version": "0.0.1",
  "dependencies": {
   "rss-to-json": "^1.0.4",
   "cors": "^2.8.4"
 }
}

whitelistを変えてみてエラーになったり、ならなかったりを確認した。

元のRSS
https://blog.yagish.jp/rss

<?xml version="1.0"?>
<rss version="2.0">
  <channel>
    <title>yagishのブログ</title>
    <link>https://blog.yagish.jp/</link>
    <description></description>
    <lastBuildDate>Wed, 18 Jul 2018 18:36:13 +0900</lastBuildDate>
    <docs>http://blogs.law.harvard.edu/tech/rss</docs>
    <generator>Hatena::Blog</generator>
    
      
      
        <item>
          <title>バックアップ機能アップデートのお知らせ</title>
          <link>https://blog.yagish.jp/entry/2018/07/18/183613?utm_source=feed</link>          <description>&lt;p&gt;バックアップ機能アップデートのお知らせ&lt;/p&gt;
</description>          <pubDate>Wed, 18 Jul 2018 18:36:13 +0900</pubDate>
          <guid isPermalink="false">hatenablog://entry/10257846132602357979</guid>
          
          <enclosure url="https://cdn.blog.st-hatena.com/images/theme/og-image-1500.png" type="image/png" length="0" />
        </item>
      
    
  </channel>
</rss>

こんなのが返ってきた。まだ記事数も文章もないので速度は速い。

{
    "items": [
        {
            "title": "バックアップ機能アップデートのお知らせ",
            "description": "<p>バックアップ機能アップデートのお知らせ</p>",
            "link": "https://blog.yagish.jp/entry/2018/07/18/183613?utm_source=feed",
            "url": "https://blog.yagish.jp/entry/2018/07/18/183613?utm_source=feed",
            "created": 1531906573000,
            "enclosures": [
                {
                    "url": "https://cdn.blog.st-hatena.com/images/theme/og-image-1500.png",
                    "type": "image/png",
                    "length": "0"
                }
            ]
        }
    ],
    "title": "yagishのブログ",
    "description": "",
    "url": "https://blog.yagish.jp/",
    "image": ""
}

pubDateはタイムスタンプに変換されcreatedという名前になってる。

こういうクライアント側でもできなくはないけど、サーバーでやった方がユーザー&クライアント側に優しいものにCloud Functionsは最適なので今後も使い所を探していこうと思う。

2018/07/23追記

Python3.7が使えるようになったので作ってみた
uyamazak.hatenablog.com



yagish履歴書がはてブ(IT)1位でうれしい

rirekisho.yagish.jp

リリース出したのは7/11だけど、なぜか今朝になっていつも見てるはてブ上位にいた
f:id:uyamazak:20180712101522p:plain

うれしい

ファビコンがルートに置いてないせいか出てないから直そう

2018/7/12 12:43追記

テクノロジー1位やったー
この記事のタイトルも変えた

f:id:uyamazak:20180712124513p:plain

なんでこんなにブクマつくんだろう

Vue.jsのPWAでLighthouse100点を目指したがPerformance77点で挫折した

WEB上だと空のプロジェクトとかでLighthouseの高得点を目指す記事はあるけど、実用的なPWAアプリでの情報は少ないので試したことをまとめてみる。

履歴書をスマホで入力できてPDFで出せる新サービスyagish履歴書。
まだまだ改良の余地はあるけど使える状態。

rirekisho.yagish.jp

まず結果。
5日ぐらいがんばった後のレポートはこれ。
Chrome拡張でLihghtHouseのバージョンは3.0.1。
ユーザーの第一印象となるトップページだけがんばった。

f:id:uyamazak:20180704102245p:plain
実行のたびに結果は変わる(Peformance 50~77点ぐらい)

Performance以外の100点は意外と簡単だった。
ただ足りないのを追加したり、よくないのを変えていく感じ。
Performanceはコード量とか処理を減らせというので難しい。

でも45点だった最初と比べると、スマホでも初回5秒以上かかっていた読み込みが体感できるほど早くなり、やってよかったと思っている。
緑(75点)以上で妥協するのも全然いいと思う。

Vueは下記PWAテンプレートをもとに開発している。
GitHub - vuejs-templates/pwa: PWA template for vue-cli based on the webpack template

これは現在すでにメンテモードになってしまい、下記プラグイン形式に移るので今からはじめるならこっち。
大きいところだとWebpackが3だったのが4になったりして、そのままは移せない。
vue-cli/packages/@vue/cli-plugin-pwa at dev · vuejs/vue-cli · GitHub

いろいろ試行錯誤したけど、効果が大きかったのは下記3つ。

1.画像は可能な限り小さくする

最初は履歴書のサムネがjpgで60KBあったけど、PhotoshopでPDFを開き、幅480pxに縮小、PNGで保存し、さらに下記サービスで圧縮したところ、10KB前後まで削減でき、文句言われなくなった。
色が少なく、はっきりしている画像はやっぱりPNGが強い。

PNGイメージをオンラインで圧縮する

ロゴやヤギなど他の画像はVGを使っているため、この手法は使えなかった。

2. vue-routerで遅延評価

js側だとこれが一番効いた気がする。
公式ドキュメント通り、ページで使うVueコンポーネントをPromise返しにするだけでおk。
動作もほとんど影響しないが、これまでトップページでも読み込まれていた他のページのファイルが分割されて20点ぐらいあがった。

formatList はトップページで使うコンポーネントのため、直接importのままにした。どっちにしても数字的にはたいして影響しなかった気がする。

router.vuejs.org

router/index.js

before

import Vue from 'vue'
import Router from 'vue-router'
import formatList from '@/components/FormatList'
import formatDetail from '@/components/FormatDetail'
import formatEdit from '@/components/FormatEdit'
import notFound from '@/components/NotFound'
import helpPage from '@/components/pages/Help'
import termsPage from '@/components/pages/Terms'
import serviceInfoPage from '@/components/pages/ServiceInfo'

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      component: formatList,
      name: 'formatList',
      meta: {
        title: SITE_TITLE
      }
    },
    {
      path: '/format/:id',
      component: formatDetail,
      props: true,
      beforeEnter: formatExistsOr404,
      name: 'formatDetail',
      meta: {
        title: '書式詳細 - ' + SITE_TITLE
      }
    },
// 省略

after

import Vue from 'vue'
import Router from 'vue-router'
import formatList from '@/components/FormatList'
import formatsData from '@/assets/formats/index'

const formatDetail = () => import('@/components/FormatDetail')
const formatEdit = () => import('@/components/FormatEdit')
const notFound = () => import('@/components/NotFound')
const termsPage = () => import('@/components/pages/Terms')
const serviceInfoPage = () => import('@/components/pages/ServiceInfo')
const helpPage = () => import('@/components/pages/Help')

// 省略

router以外でも動的コンポーネントを使っている箇所も遅延にしてみたけど、大して変わらなかった。

WEBフォントは必須にしない

今回のサービスの特質上、ユーザーとPDFサーバーでフォントをそろえる必要があった。
GoogleのWEBフォントを使っており、サイト上でもメインで使っていたけど、読み込まれるまで文字が表示されないため、大きく減点されていた。
そこでWEBフォントはどうしても必要な履歴書のプレビュー画面だけにして、他の箇所では優先順位をさげた。

あと原因が分かったけど改善が難しいもの。

vendorファイルのサイズ削減

つかってるjsのライブラリがまとまるvendorファイル。
これが3MBありビルド中もtoo Large的な警告が出ていた。
中身を確認するには下記コマンド(Webpack Bundle Analyzer)がいいらしく、今回のPWAテンプレートにも入っていたので実行してみた。

How to troubleshoot a large vendor file? · Issue #1297 · vuejs-templates/webpack · GitHub

Webpack Bundle Analyzerについて詳しくはこれ。今回ホスト名、ポート番号は設定で変えた。
www.npmjs.com

絶望の結果がこれ。
f:id:uyamazak:20180704105314p:plain

element-uiとfirebaseが2大巨頭だがどっちも深く使っているため大きく削れない。
elementは使ってないものは省けるが、commonがでかすぎて、他を削ったところで数十kb削減にしかならず、効果は薄かった。
firebaseは公式にある通り使っているものだけrequireしている。
Firebase を JavaScript プロジェクトに追加する  |  Firebase

でもPerformanceには1点も効果がなかった。

あと小さいけどlodashも使っている機能が限られているので、下記のように個別に読み込んでサイズを小さくできたが、これも結果はほぼ変わらなかった。

const MyLodash = {
  forEach: require('lodash/forEach'),
  compact: require('lodash/compact'),
  debounce: require('lodash/debounce'),
  orderBy: require('lodash/orderBy'),
  defaultsDeep: require('lodash/defaultsDeep')
}
module.exports = MyLodash

トップページの高速化ならPWA +AMP

というわけで、Vueなどを使ったSPAではライブラリの容量が大きくロード時間などで100点は難しい。
そのため、これ以上の高速化となるとAMPを組み合わせるしかない。

詳しくは公式ドキュメント
www.ampproject.org

これを使えばAMPで高速に表示して、その間にService Workerでロードしつつ、リンク先ではSPAが動くという形にでき、最初の画面表示を爆速にできるはず。