kqito's 技術ブログ

技術やプログラミングについて思ったことを呟くブログ

@loadable/componentでSSRする

はじめに

私はよくReactをSSRする際にはNext.jsを用いて開発することが殆どです。その弊害?なのか、SSRの仕組みを自分で実装したことがないため必要最低限の機能を持つサーバーを実装してみました。

作成したサンプルについて

サンプルはこちらになります

github.com

目指した部分

SSRサーバを作成するにあたって以下の機能を実装しました

  • @loadable/componentを利用してパフォーマンスに注力したい
  • 開発時にはHMRでリアルタイムでブラウザに反映されるようにしたい

上記を実装するにあたってこちらを参考にしました。

loadable-components.com

github.com

Webpack

バンドラーはWebpackを用いて、clientとserver側でconfigファイルを分けました。

その上で、開発する際に工夫が必要だった部分を抜粋してメモします。

CSS周り

clientとserver側で共通のwebpack.config.jsでは、以下のようにmofule.rulescssのloaderを記述しました(一部抜粋

{
  test: /.css$/,
  use: [
    isProduction && {
      loader: MiniCssExtractPlugin.loader,
    },
    isDevelopment && {
      loader: 'style-loader',
    },
    {
      loader: 'css-loader',
      options: {
        sourceMap: isDevelopment,
      },
    },
  ].filter(Boolean),
},

production環境ではMiniCssExtractPluginを用いることは多いと思います。

しかしdevelop環境におけるHMRを機能させる為にstyleタグ(linkタグも含め)としてcssを適用させて必要がある為上記のようにしました。

style-loaderSSRには対応していない為必要に応じてisomorphic-style-loaderなども導入する必要がありそうです。(今回はパス

LoadablePlugin

loadable-components.com

上記のリンクでも説明されているように@loadable-componentを利用する際にはwebpack.pluginsにプラグイン を追加する必要があります。

以下がその該当部分です。

plugins: [
  isDevelopment && new webpack.HotModuleReplacementPlugin(),
  isProduction &&
    new workboxPlugin.GenerateSW({
      swDest: 'sw.js',
      clientsClaim: true,
      skipWaiting: false,
    }),
  new LoadablePlugin(),
].filter(Boolean)

Loadableはdevelopmentとproductionの両方とも利用する為、普通に配列の要素として代入していますが、HMRやworkboxなどは基本的に限られた時にしか利用したいためprocess.env.NODE_ENVをみてpluginを利用するかを分けています。

Client

エントリーになっているsrc/client/index.tsxは以下のようになっています

export const render = () => {
  ReactDom.hydrate(
    <BrowserRouter>
      <App />
    </BrowserRouter>,
    document.getElementById("app")
  );
};

if (module.hot) {
  module.hot.accept("./app", () => {
    render();
  });
}

loadableReady(() => {
  render();
});

module.hotがtrueのときはHMRが有効(development)と判断しmodule.hot.acceptでリレンダリングを登録しています。

webpack.js.org

また、loadableReadyの部分ですが、@loadable/componentを利用すると、SSR後のクライアントにてasync属性が有効になってているscriptファイルを並列でダウンロードするように自動でよしなにしてくれるのですが、その並列のダウンロードが終わり次第、レンダリング(hydrate)を行うと言う処理になります。

hydarteが遅いほどscriptに依存している要素がInteractiveな状態になりませんので、このような並列 + asyncな処理を行っています。

Server

SSRに該当するファイルは以下です。

import React from "react";
import { Request, Response } from "express";
import { renderToString } from "react-dom/server";
import { StaticRouter } from "react-router-dom";
import { ChunkExtractor } from "@loadable/server";

import { App } from "../../../client/app";
import { appResolve } from "../../../utils/path";
import { renderHtml } from "./renderHtml";

const statsFile = appResolve("./dist/client/loadable-stats.json");

export function renderer(req: Request, res: Response) {
  const context = {};

  const Root = () => (
    <div id="app">
      <StaticRouter location={req.url} context={context}>
        <App />
      </StaticRouter>
    </div>
  );

  try {
    const extractor = new ChunkExtractor({ statsFile });
    const jsx = extractor.collectChunks(<Root />);
    const body = renderToString(jsx);
    const script = extractor.getScriptTags();
    const style = extractor.getStyleTags();

    const html = renderHtml({ body, script, style });

    res.send(html);
  } catch (e) {
    console.error(e);
    res.status(500).send(e.message);
  }
}

まず、以下の行にてwebpackのバンドルによって生成されたloadable-stats.jsonのパスを取得します。

const statsFile = appResolve("./dist/client/loadable-stats.json");

これを元にjsxやscriptなどの取得を行います。

const extractor = new ChunkExtractor({ statsFile });
const jsx = extractor.collectChunks(<Root />);
const body = renderToString(jsx);
const script = extractor.getScriptTags();
const style = extractor.getStyleTags();

取得できたこれらをrenderHTMLという関数にてhtmlを生成します。

renderHTMLはstringを返す純粋な関数です。

type Renderer = {
  body: string;
  script: string;
  style: string;
};

export const renderHtml = ({ body, script, style }: Renderer) => {
  return `
  <!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="UTF-8"/>
      <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
      <title>SSR example</title>
      ${style}
    </head>
    <body>
      ${body}
      ${script}
    </body>
  </html>
  `;
};

そしてその結果をexpressサーバがresponseとして返却することで最初のHTMLをブラウザが取得できるようになります。

まとめ

本当に今更だったのですが、SSRサーバーを作成したことがなかったので新しい知見が増えました。

これをもとにfastyなどのCDNでキャッシュまでを自分で実装する予定です。

以上です。

webpackのバンドルで利用されてないライブラリファイルを除外する

はじめに

Webpackのパフォーマンスでは様々な面で取り組むことができますが、ライブラリに利用されないはずのファイルがバンドルに含まれ、結果的にバンドルサイズが大きくなってしまっている場面にあいました。

そこで行ったことをmoment.jsを例として残します

事前調査

定義

moment.jsを利用して日本語のみの利用を前提とします。

まず、特に意識しないで利用してみましょう。

import moment from "moment";

moment.lang("jp");

console.log(moment().format("M月D日(dd)"));

これらをwebpack-bundle-analyzerでみると以下のような感じになります。

f:id:kqito:20200717222932p:plain
webpack-bundle-pluginの結果

moment.jsではlocaleファイルにそれぞれの地域に対するファイルがそれぞれ用意されており、importすれば全てがバンドルに含まれるようになります。

今回、jpファイルしか利用しないので他にあるファイルは余分になっています。

これによってバンドルサイズが大きくなってしまっているので、これをどうにかしましょうという話です。

どうするか

webpack.IgnorePluginを使う

対策として様々な手段で省くことができますが、今回はwebpack.IgnorePluginを利用してみます。

まずwebpack設定ファイルに以下を追記します。

plugins: [
  new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
],

これでmoment.jsのlocaleは全てビルドに含まれてなくなりました。

そして/src/utils/moment.jsのようなファイルを作成し、以下を追記します

import moment from "moment";
import "moment/locale/ja";

moment.lang("jp");

export default moment;

これでlocale/jaのみがバンドルに含まれるようになります。

結果は以下のとおりです。

f:id:kqito:20200718000851p:plain
変更後の結果

さいごに

以上のようにしてバンドルサイズを減らすことができました。webpack.IgnorePluginは使い方次第ではライブラリが正常に動作しなくなる原因でもあるので気をつけて利用した方が良さそうです。

以上です

Webpackのcss系のloader周りを調べてみた

はじめに

Webpackを設定する中でcssファイルに対するloader(css-loaderとかstyle-loaderraw-loaderなど)がよく利用されています。

しかし、それぞれの役割、挙動が自分の中であやふやだったのでそれぞれ調べた時の結果などをブログにまとめました。

loader

css-loader

一言で言えばurlやimport/require()の解決を行いオブジェクトとして生成するloader。

まず最初に以下のようなwebpack設定ファイルで*.cssファイルがどのように解決されるかを調べます。

(module部分のみ記述) (ts-loaderを用いてトランスパイルしていますが、省略)

module: {
  rules: [
    {
      test: /\.css$/,
      use: [
        {
          loader: "css-loader",
          options: {
            modules: true,
          },
        },
      ],
    },
  ],
}

そしてこのようなファイルを用意してwebpackでバンドルさせます。

import style from "./index.module.css";

console.log(style);
.test {
  color: blue;
}

これらの出力は以下のようになります。

0: (4) ["./src/index.module.css", "._34TPoweJRFabqRaBcU6yIt {↵  color: blue;↵}↵", "", {…}]
i: ƒ (modules, mediaQuery, dedupe)
locals: {test: "_34TPoweJRFabqRaBcU6yIt"}
toString: ƒ toString()

css-loaderによって生成されたこのオブジェクトは様々な用途で利用されます。

例えば

webpack.js.org

公式のGetting Startedでも例があるようにcss-loaderstyle-loaderなどといった様々なloaderと併用されることが多そうです。

style-loader

一言で言えば、cssをdomに注入する役割を持つloader。

上記で述べたcss-loaderと併用した以下のようなwebpack設定ファイルを用意します。

module: {
  rules: [
    {
      test: /\.css$/,
      use: [
        {
          loader: "style-loader"
        },
        {
          loader: "css-loader",
          options: {
            modules: true,
          },
        },
      ],
    },
  ],
}

加えて適当にreact + typescriptでクリックするとalert()が実行されるindex.tsxファイルとcssファイルを用意します

import React from "react";
import ReactDom from "react-dom";

import style from "./index.module.css";

type AlertButtonProps = {
  message: string;
};

const AlertButton: React.FC<AlertButtonProps> = ({ message }) => (
  <button className={style.test} type="button" onClick={() => alert(message)}>
    Let's alert
  </button>
);

ReactDom.render(<AlertButton message="hi" />, document.getElementById("app"));
.test {
  color: blue;
}

これを実行すると表示されたAlertButtonにハッシュされた_34TPoweJRFabqRaBcU6yItのようなクラスが付与&styleタグの生成が行われ、CSS-Moduleが実現できます。

CSS-Module意外にも普通にcssを生成する方法もあったり、

style-loaderのoptions.injectTypeを'linkTag'に設定し、file-loaderと併用することでlinkタグによるスタイルの適用を行なったりすることもできます。

raw-loader

一言で言うと、ファイルを文字列としてimportできるようにするloaderです。

webpack.js.org

公式ではtxtファイルを文字列として読み取る例が用意されています。

raw-loadercssファイルを文字列として読み込み & style-loaderでDOMに反映と言う流れでも上記と同じことができるようです。

ただ、文字列として読み取っているだけなのでCSS-Moduleなどは当然無理です。

{
  test: /\.css$/,
  use: [
    {
      loader: "style-loader",
    },
    {
      loader: "raw-loader",
    },
  ],
}

mini-css-extract-plugin

一言で言えば、cssをそれぞれのファイルに生成するプラグイン

下記のようなwebpack設定フィアルでビルドすると今までの処理結果とは異なりcssが別で生成され、linkタグにて読み込まれるようになります。

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
{
  test: /\.css$/,
  use: [
    {
      loader: MiniCssExtractPlugin.loader,
    },
    {
      loader: "css-loader"
    },
  ],
}

react-scriptsのwebpack.config.jsでは以下のような*.cssファイルに対する設定が行われています。(一部抜粋)

    const loaders = [
      isEnvDevelopment && require.resolve('style-loader'),
      isEnvProduction && {
        loader: MiniCssExtractPlugin.loader,
        // css is located in `static/css`, use '../../' to locate index.html folder
        // in production `paths.publicUrlOrPath` can be a relative path
        options: paths.publicUrlOrPath.startsWith('.')
          ? { publicPath: '../../' }
          : {},
      },
      {
        loader: require.resolve('css-loader'),
        options: cssOptions,
      }

develop環境だとstyle-loaderを利用、production環境だとmini-css-extract-pluginを利用しています。

ここはおそらくパフォーマンスを考慮して切り替えていると思われるのですが、今後自分の方で試してみたいと思います。

まとめ

webpackのcss周りについての設定の理解を多少深めることができたと思います。

これはそもそもCSS-Moduleにおけるモジュールの構築順が書き換わってリファクタリングしたときに意図せぬスタイルが適用されたりするので純粋なBEMの方がよさそう...」のような記事をみたのがきっかけでした。

CSS-in-JSなのかCSS-Moduleなのか、手段は多くあるのでそれぞれ理解を深めていきたいです。

以上です。

workboxで簡単にprecacheする

はじめに

今回はworkboxを利用したprecacheをフレームワークworkbox-webpack-pluginなどを利用せずに行いたいと思います。

www.npmjs.com

目標は「シンプルに」実装していきます。

環境

Server

今回はgo言語のwebフレームワークであるginを利用します。

github.com

goには標準ライブラリとしてnet/httpなどありますが、ginによる恩恵が大きいのでこちらを利用。

DBなども用いないので単純なhtmlなどを返すサーバーになります。

Client

こちらは純粋なhtml + jsのみで作成します。precacheする内容は

  • /index.html
  • *.js

とします。

ディレクトリ構成

.
├── build
│   └── main
├── go.mod
├── go.sum
├── main.go
├── refresh.yml
├── static
│   ├── image
│   │   └── react.png
│   └── js
│       ├── service-worker.js
│       └── workbox.js
└── view
    └── index.html

コード

Server

サーバーサイドはmain.goだけになります。

package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
    "path/filepath"

    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()

    router.GET("/", func(ctx *gin.Context) {
        ctx.File("./view/index.html")
    })

    router.GET("/:file", func(ctx *gin.Context) {
        file := ctx.Param("file")
        fp, err := filepath.Glob("./static/**/" + file)
        if err != nil || len(fp) == 0 {
            ctx.Status(http.StatusNotFound)
            return
        }

        ctx.File(fp[0])
    })

    host := os.Getenv("HOST")
    if host == "" {
        host = "localhost"
    }

    port := os.Getenv("PORT")
    if port == "" {
        port = "8081"
    }

    baseUrl := fmt.Sprintf("%s:%s", host, port)
    log.Println("Listening on", baseUrl)
    log.Fatal(router.Run(baseUrl))
}

service-workerにはスコープの概念があり、/service-worker.jsとリクエストできるように/:fileでは`filepath.Globを用いてstaticファイルを返すようにしています。

が、ここはもっと綺麗に書ると思います。直してないけど

Client

まずはindex.htmlから。

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Service-worker</title>
    <meta charset="UTF-8">
    <script type="module">
      import { unregister, register } from "/service-worker.js"
      register()
    </script>
  </head>
  <body>
    <img src="/react.png" alt="react icon" style="width: 400px; height: 300px;"/>
  </body>
<html>

こちらは

  • /react.pngを表示
  • /service-worker.jsのregister関数を実行する

上記2点を行っているシンプルなhtmlです。

それではその/service-worker.jsが以下です。

const swKey = "serviceWorker";

function registerServiceWorker(sw) {
  navigator.serviceWorker
    .register(sw)
    .then(() => {
      console.log("Register service worker");
    })
    .catch((err) => {
      console.error(err);
    });
}
export function unregister() {
  if (!swKey in navigator) {
    return;
  }

  navigator.serviceWorker.ready
    .then((registration) => {
      registration.unregister();
    })
    .catch((err) => {
      console.log(err);
    });
}

export function register() {
  if (!swKey in navigator) {
    return;
  }

  window.addEventListener("load", () => {
    const swUrl = `/workbox.js`;

    registerServiceWorker(swUrl);
  });
}

こちらはservice-workerを登録/登録解除するjsを記述しています。/service-worker.jsが存在しているかどうかなどの確認は行わず、簡潔に記述しています。

そして重要なworkboxを利用したprecacheを行っている/workbox.jsはこちらです。

importScripts(
  "https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js"
);

const cacheTargets = [
  { url: "/", revision: null },
  { url: "/react.png", revision: null },
];

workbox.core.clientsClaim();

workbox.precaching.precacheAndRoute(cacheTargets, {});

const handler = workbox.precaching.createHandlerBoundToURL("/");
const navigationRoute = new workbox.routing.NavigationRoute(handler);
workbox.routing.registerRoute(navigationRoute);

上記で行っていることは

  • workboxをcdnからimport
  • clientsClaimしてすぐにコントロールできる状態に
  • cacheするファイルを定義
  • navigation requestの設定

です。

これでサーバーを立ち上げ、http://localhost:8081/にhttpリクエストすることでservice-workerがprecacheを行ってくれます。

precache後はオフラインでもコンテンツを表示することができるようになりました。

まとめ

今回は完結にworkboxを用いてprecacheしてみました。

service-workerはまだ理解できていない部分があるのでこまめに学習したいです。

以上です。

create-react-appで生成されるservice-worker周りについてのメモ

はじめに

こんにちは。Webフロントエンドを専攻しているkqitoです。 今回はcreate-react-appで自動で生成されるservice-worker周りについてメモします。

create-react-appする

create-react-appのバージョンは以下の通りです。

$ create-react-app -V
> 3.3.0

このような形で生成します

create-react-app test-service-worker --template typescript

今回はtypescriptでservice-workerのregister, unregisterの部分を読んでいきます。

そういえば

create-react-app repo --typescript

みたいなオプションはdeprecatedされていました。

service-worker.tsをみてみる

exportされている関数

exportされているのはunregisterregisterの2つだけです。順を追ってコードを見てみます。

unregister

export function unregister() {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.ready
      .then(registration => {
        registration.unregister();
      })
      .catch(error => {
        console.error(error.message);
      });
  }
}

早期returnなどはせず、navigatorグローバル変数にserviceWorkerがあるなら(ブラウザ対応してるなら)unregisterしてる単純な関数ですね。

こちらはcreate-react-app/src/index/tsxにてデフォルトで呼び出されている関数です。

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

デフォルトはunregisterしてるけど、registerするときには「隠れた罠(pitfalls)」があるので注意してね的な内容ですね。

基本的に開発時にprecacheする必要はないですし、productionデプロイ時に利用するだけで良さそう。

ということは何も編集していない限りは上記のunregisterが呼び出されていると...

register

以下がregister関数の全体です。

export function register(config?: Config) {
  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
    // The URL constructor is available in all browsers that support SW.
    const publicUrl = new URL(
      process.env.PUBLIC_URL,
      window.location.href
    );
    if (publicUrl.origin !== window.location.origin) {
      // Our service worker won't work if PUBLIC_URL is on a different origin
      // from what our page is served on. This might happen if a CDN is used to
      // serve assets; see https://github.com/facebook/create-react-app/issues/2374
      return;
    }

    window.addEventListener('load', () => {
      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;

      if (isLocalhost) {
        // This is running on localhost. Let's check if a service worker still exists or not.
        checkValidServiceWorker(swUrl, config);

        // Add some additional logging to localhost, pointing developers to the
        // service worker/PWA documentation.
        navigator.serviceWorker.ready.then(() => {
          console.log(
            'This web app is being served cache-first by a service ' +
              'worker. To learn more, visit https://bit.ly/CRA-PWA'
          );
        });
      } else {
        // Is not localhost. Just register service worker
        registerValidSW(swUrl, config);
      }
    });
  }
}

ifがいくつかネストしていますが、やっていることはproductionかつserviceWorkerがブラウザ対応しているときにloadイベントを登録している感じですね。

const isLocalhost = Boolean(
  window.location.hostname === 'localhost' ||
    // [::1] is the IPv6 localhost address.
    window.location.hostname === '[::1]' ||
    // 127.0.0.0/8 are considered localhost for IPv4.
    window.location.hostname.match(
      /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
    )
);

上記を満たすホストだった場合にはservice-workerが読み込めるかを確認 + service-workerを登録するcheckValidServiceWorker関数を実行したりしています。

service-workerでやっていること

yarn buildなどで生成されるservice-worker.jsは以下のようなファイルです。このファイルが上記のservice-worker.tsで登録されています。

importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");

importScripts(
  "/precache-manifest.1e65759576907aec8ccc3e22bff345f4.js"
);

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') {
    self.skipWaiting();
  }
});

workbox.core.clientsClaim();

/**
 * The workboxSW.precacheAndRoute() method efficiently caches and responds to
 * requests for URLs in the manifest.
 * See https://goo.gl/S9QRab
 */
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});

workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("/index.html"), {
  
  blacklist: [/^\/_/,/\/[^\/?]+\.[^\/]+$/],
});

これらはworkboxによって提供されているservice-workerです。

workboxについてはこちらの記事が理解しやすかったです

qiita.com

yarn ejectしてwebpackの設定を見ればわかるのですが、workbox-webpack-pluginというライブラリを利用してworkboxに関する設定ファイルを生成しているため生成されるわけですね。

      new ManifestPlugin({
        fileName: 'asset-manifest.json',
        publicPath: paths.publicUrlOrPath,
        generate: (seed, files, entrypoints) => {
          const manifestFiles = files.reduce((manifest, file) => {
            manifest[file.name] = file.path;
            return manifest;
          }, seed);
          const entrypointFiles = entrypoints.main.filter(
            fileName => !fileName.endsWith('.map')
          );

          return {
            files: manifestFiles,
            entrypoints: entrypointFiles,
          };
        },
      }),

ざっくりまとめるとこんなことをしています。

  • Precacheするファイルの指定(runtime cacheとかはしてない)
  • Navigation requestsされるprecacheされているファイルの指定

こんな感じでしょうか。

ちなみに最新バージョンのworkboxではregisterNavigationRouteはdeprecatedされている感じでした

github.com

まとめ

ざっくりと生成されるファイル等を読んでみました。

Cache Storageはパフォーマンスをかなり向上する物ですが、キャッシュ削除のタイミングなどちゃんと図らないと意図せぬ内容が表示されたりすると思うので気をつけながら利用したいところです。

以上です。

CyberAgent主催のハッカソンインターンで特別賞(2位)を受賞しました!

はじめに

こんにちは。Webフロントエンドを専攻しているkqitoです。
今回は初めて参加したCyberAgent主催のCA Tech Challenge AbemaTV Hack -クライアントサイド編-(ハッカソン形式インターン)に参加し、特別賞(2位)を獲得することができたので、その感想や勉強になったことを共有したいと思います。(許可を得てブログを書いています。)

f:id:kqito:20190925223642j:plain
Abemaのトートバッグも頂きました

インターンの大まかな説明

日程

ハッカソンは3日間で開催され、初日に公開されたお題に沿って最終発表の時まで開発をする感じでした。
初日は台風の影響で2時間ぐらい遅れてしまいました...満員電車辛い。

環境

個人つきそれぞれメンターが1人ついて、わからないところ・悩んでいるところをそのメンターに対して相談できる環境でした。

例えば、

  • コードの意味がわかりません。
  • この機能を実装しようとしているんですけど、どうしたらいいか悩んでいます。

と言った質問も受け付けてくれるような環境でした。

私はコーディング自体に躓くことはあまりなかったので、主にUI/UX的なデザインや実装する機能のアドバイス等を相談をさせて頂きました。適切な回答を納得する形で頂けるので悩んで手が止まるということはありませんでした。

部門

インターンでは

  • Webフロント
  • ios
  • andriod

の三部門があり、私はWebフロント部門で参加しました。
また最終発表で表彰されるのは各部門の1人のみなので、全体で3人が表彰されることになっています。

課題について

課題の内容

ハッカソン初日にgithubを通じてとあるWebアプリを提供されました。
それは十字キーなどで再生する動画を切り替えることができるWebアプリでした。(コード自体は控えさせて頂きます。)

そして本命のハッカソンのお題はそのWebアプリに対してに何か機能を付け加えてくださいといった自由なお題でした。

具体的にいうとなんでも評価して頂ける課題で、例えば

  • 既存のAbemaTVに対する改善案
  • 新機能の実装
  • PWA化

など様々なベクトルで開発を行っても良いというものでした。

課題の難易度について

課題自体 - 少し難易度高め

この課題について私は自由がすぎるために難易度が少し高いと感じました。
というのも、3日間という短期間において(実際に開発できるのは1.5日程?)完成度の高い物を作るのは難しいと感じました。(ハッカソンあるある)

提供されたコード - 普通

提供されたコードはCQRSの概念が取り組まれているコードで非常に洗練されているアーキテクチャだと感じました。正直な話、読んでいるだけで勉強になりましたw
また、可読性も良く理解しやすい良質なコードでした。

しかし実力不足で一部のコードを理解するのに時間がかかりました。それはRxjsの部分です。というのもRxjsを利用したことがないのでオペレーター等の使い方がわからず最初はとっつきにくかった感じでした。

個人的な考えですが、オペレーターは関数型言語的な思想があるので慣れないと理解のスピードが遅くなってしまうと思っています。

Rxjsの理解をした後は順調に開発を進めることができたと思います。

(追記) 最近Rxjsを利用してゴリゴリリアクティブプログラミングしていますが、正直すごいライブラリだと感じました。

  • ジェスチャーなどからストリーム生成
  • 複雑な処理をLINQのようなオペレーターを利用しながらストリームとして操作できる

Rxjs最強。

開発について

なにを開発したのか

私は テレビと番組表を同一ページで視聴することができる機能 を開発しました!

なぜこれを実装しようとしたのか

その機能によってユーザの利便性の向上・UI/UXの新しい表現が期待できると思ったためです。
ちなみにこれは実際のAbemaTVのサイトを見て「この機能があったら便利なのでは」と感じたので、それを実装しました。

また、工数的にも自分の実力に見合った物だと感じたという事もあります。

・実際に実装できた事

最終的には以下の事を実装することができました。

  • 実際のAbemaTVの番組表の再現
  • テレビをPicture in Picture化し、番組表も同時に視聴できるように変更
  • テレビを隠すことで番組表を見やすくする機能の実装
  • スマホ対応

以上を実装したことで閲覧している番組を番組表上でハイライトしたり、各番組をクリックすることでページ遷移することなくテレビを切り替えることができました。
短期間で自分が実装したい内容を達成することができたので完成度としてはなかなか高いのではないかと思います。

・実装しきれなかった部分

逆に実装できなかった事として以下のことが挙げられます。

  • 番組詳細ページ・コンポーネントの実装
  • 番組表やテレビ以外のコンテンツの再現

これらは時間が足りなく実装できなかった部分でした。もうすこし時間を効率よく使っていたら実装できていたかももしれないので、次のハッカソンでは時間の使い方に気を配る事を意識したいです。

終結

冒頭で説明したように、本インターンは各部門で優秀賞(1位)のみの表彰です。
表彰が始まるとそれぞれの部門の素晴らしい作品を作成した方々が優秀賞を受賞していきます。しかし、私がWebフロントの優秀賞を獲得することは叶いませんでした...

受賞ならず...残念!!

と思いきや.....

特別賞を受賞!!!!!!

すごくビックリしたのを覚えています。 特例で特別賞を頂くことができました!! (タイトルに2位と表記したのは理解しやすくするためです)

3日間の努力が受賞という形になったのが非常に嬉しかったです!! なによりも、メンターの方や同じテーブルの方の協力の元で受賞することができたので感謝の気持ちで一杯でした。

その後

懇談会

表彰のあとは一緒に頑張ったインターンの参加者の方々や人事の方、メンターの方とお食事をご馳走になりました。 楽しく会話をしたり、実際の職場のお話等を聞く事ができCyberAgentの雰囲気などを知る機会にもなりました。

三日間の疲れも吹き飛ぶぐらい楽しく会話させて頂きました

勉強になったこと

洗練されたアーキテクチャ

今回提供されたCQRSの思想を取り入れたコードは非常に洗練されていたと感じます。 テスト性非同期通信における副作用を考慮していたり非常に勉強になりました。ただ、初期学習コストは他のアーキテクチャと比較しても高めと感じました。

CyberAgentの雰囲気

社内環境は素晴らしいの一言に尽きる位充実していました。
特に社員の方々同士がいつも楽そうにしており、私も気分良く会話・開発する事ができました。
また、上記で述べたようにコミュニケーションは成果物やプロジェクトに直結すると私は考えているため、エンジニア視点からしても働きやすい環境だと感じました。

ハッカソンにおける立ち回り方

はじめてのハッカソンに参加する事で、短期間で完成度の高い成果物を出す立ち回り方を学習する事ができました。

実際に優秀賞をとった方はWebアプリケーションの完成度が極めて高く、ライブラリの利用方法やUI/UXが洗練されており素晴らしい成果物を作成していました。

次回は今回の反省を生かしながらイベントに参加したいです。

反省点

汚いコードを量産

時間が限れている中で急いだ結果、コードを汚く書いてしまった部分があると自覚しています。

  • ステートを持つコンポーネントを作成
  • 可読性を考慮しない(モジュール分割しないなど)

などをやってしまいました。 今後はなるべく余裕を持って美しいコードを書いていきたいと思います。

まとめ

今回のインターンで様々な事を勉強する事ができました。そして自分に対する課題も見つかったのでそれに向けて努力していきたいと思います。

最後になりますが、関わって頂いた社員の方々や一緒に頑張った皆様、本当にありがとうございました!