UUUM攻殻機動隊(エンジニアブログ)

UUUMのエンジニアによる技術ブログです

Webpackerについてちゃんと調べてみた

カイシャで唯一 Emacs を使ってる✨✨✨✨キラキラエンジニア✨✨✨のtakeokunnです。

ギョームで webpacker を使っているのですが、中のコードを読む機会があったのでせっかくだからまとめてみました✨✨✨✨✨


コードについて書く前に、そもそも Webpacker がどんな機能を提供しているか書く。

READMEによると:

Features
* webpack 4.x.x
* ES6 with babel
* Automatic code splitting using multiple entry points
* Stylesheets - Sass and CSS
* Images and fonts
* PostCSS - Auto-Prefixer
* Asset compression, source-maps, and minification
* CDN support
* React, Angular, Elm and Vue support out-of-the-box
* Rails view helpers
* Extensible and configurable

これ入れとけば最新のwebpack使えて諸々の設定省いて最新 javascript をいい感じにrailsに組み込める!って書いてある。すっごーい✨✨✨✨✨


Webpacker のコードを追ってみる。$ ./bin/webpack が実行されてから webpackbundle.js が生成されるまでを見ていく。

./bin/webpack:

#!/usr/bin/env ruby

ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
ENV["NODE_ENV"]  ||= "development"

require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", Pathname.new(__FILE__).realpath)

require "rubygems"
require "bundler/setup"

require "webpacker"
require "webpacker/webpack_runner"
Webpacker::WebpackRunner.run(ARGV)

諸々の設定をしたあと、Webpacker::WebpackRunner.run(ARGV) が実行されている。

lib/webpacker/webpack_runner.rb抜粋:

def run
  env = Webpacker::Compiler.env

  cmd = if node_modules_bin_exist?
    ["#{@node_modules_bin_path}/webpack"]
  else
    ["yarn", "webpack"]
  end

  if ARGV.include?("--debug")
    cmd = [ "node", "--inspect-brk"] + cmd
    ARGV.delete("--debug")
  end

  cmd += ["--config", @webpack_config] + @argv

  Dir.chdir(@app_path) do
    Kernel.exec env, *cmd
  end
end

ここではもともと引数で渡していたものを元に webpack のcommandを生成して、 Kernel.exec env, *cmd で実行をしている。

@webpack_config の定義元について調べてみる。

lib/webpacker/runner.rb抜粋:

def initialize(argv)
  @argv = argv

  @app_path              = File.expand_path(".", Dir.pwd)
  @node_modules_bin_path = ENV["WEBPACKER_NODE_MODULES_BIN_PATH"] || `yarn bin`.chomp
  @webpack_config        = File.join(@app_path, "config/webpack/#{ENV["NODE_ENV"]}.js")

  unless File.exist?(@webpack_config)
    $stderr.puts "webpack config #{@webpack_config} not found, please run 'bundle exec rails webpacker:install' to install Webpacker with default configs or add the missing config file for your custom environment."
    exit!
  end
end

Railsconfig/webpack/#{ENV["NODE_ENV"]}.js から取得している。

config/webpack/development.js:

process.env.NODE_ENV = process.env.NODE_ENV || 'development'

const environment = require('./environment')

module.exports = environment.toWebpackConfig()

toWebpackConfig の定義元はここ: https://github.com/rails/webpacker/blob/b041a1d3e53c55ad801654892c99f5ec4ff099f2/package/environments/base.js#L150-L165

entry, plugins, module, resolve などをまとめて webpack.config.js を吐き出している。

config/webpack/environment.js:

const webpack = require('webpack')
const { environment } = require('@rails/webpacker')

environment.loaders.get('sass').use.splice(-1, 0, { loader: 'resolve-url-loader' });

environment.plugins.prepend(
  'Provide',
  new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery',
    jquery: 'jquery',
    Popper: ['popper.js', 'default']
  })
)

module.exports = environment

ここで webpack の設定を書く。

記述の仕方は公式ドキュメントにある: https://github.com/rails/webpacker/blob/master/docs/webpack.md

以上が Webpacker$ webpack --config webpack.config.js の一連の処理だ。


次にRailsとのつなぎ込みの部分を見ていく。

<%= javascript_pack_tag 'application' %>
<%= stylesheet_pack_tag 'application' %>

上記のようにlayout部分に記述するといい感じに bundle.js を読み込んでくれる。

javascript_pack_tag の定義元を読む。

lib/webpacker/helper.rb抜粋:

def javascript_packs_with_chunks_tag(*names, **options)
    javascript_include_tag(*sources_from_manifest_entrypoints(names, type: :javascript), **options)
end

def sources_from_manifest_entries(names, type:)
    names.map { |name| current_webpacker_instance.manifest.lookup!(name, type: type) }.flatten
end

current_webpacker_instanceWebpacker::Instance のインスタンスだ。

lib/webpacker/instance.rb抜粋:

class Webpacker::Instance
    def initialize(root_path: Rails.root, config_path: Rails.root.join("config/webpacker.yml"))
        @root_path, @config_path = root_path, config_path
    end
end

config/webpacker.yml を読み込んでいる。

current_webpacker_instance.manifest.lookup!(name, type: type) の処理を追ってみる。

lib/webpacker/manifest.rb抜粋:

class Webpacker::Manifest
    # Computes the relative path for a given Webpacker asset using manifest.json.
    # If no asset is found, returns nil.
    #
    # Example:
    #
    #   Webpacker.manifest.lookup('calendar.js') # => "/packs/calendar-1016838bab065ae1e122.js"
    def lookup(name, pack_type = {})
compile if compiling?

find(full_pack_name(name, pack_type[:type]))
    end

    # Like lookup, except that if no asset is found, raises a Webpacker::Manifest::MissingEntryError.
    def lookup!(name, pack_type = {})
lookup(name, pack_type) || handle_missing_entry(name)
    end

    def full_pack_name(name, pack_type)
        return name unless File.extname(name.to_s).empty?
        "#{name}.#{manifest_type(pack_type)}"
    end

    def find(name)
        data[name.to_s].presence
    end
end

この lookup! 部分でpath解決をしている。 webpack をしてない状態でrenderingした時に compile が走るのはこの部分が原因みたいだ。

webpack 側の output はどうなっているだろうか。

package/config.js抜粋:

const configPath = resolve('config', 'webpacker.yml')

const config = deepMerge(defaults, app)
config.outputPath = resolve(config.public_root_path, config.public_output_path)

config/webpacker.yml を元に output 先を指定している。

webpack 側も rails 側もお互い config/webpacker.yml を元にしてるためpathを解決することができる。


webpack のconfigを拡張していくとなると結構大変だけれども、特に弄らないなら導入も運用も楽で良いですね!

create-react-app のように eject 出来るように( webpack のみの運用)なってくれるともっと運用が楽になると思います。

へーしゃでは emacs を使うのが好きなエンジニアを切に募集しています✨✨✨✨✨✨✨

一緒にインスタ映えする emacs を作りましょう✨✨✨✨✨✨✨

View this post on Instagram

インスタ映えするemacs

@ takeokunnがシェアした投稿 -

詳しくはこちら

www.wantedly.com www.wantedly.com

今年も合宿にいきました!

こんにちは。2月入社でシステムユニット所属の井上です。

今日のテーマは合宿です!!

システムユニットでは年に2回、開発旅行合宿があります。

合宿ではテーマが設定されており、テーマに沿って自分の好きなプロジェクトの開発だったり、新技術の勉強などそれぞれが自由に研鑽します。

今回はその一環として2/22(金)~2/23(土)に、UUUM攻殻機動隊のエンジニア達で開発合宿に行ってきました。

今回の合宿場は、神奈川県の箱根にある「COLONY 箱根」でした。

施設紹介

企業の研修用の施設らしく広々としていました。また箱根名物の温泉もありました。

そしてラッキーなことに利用者は他におらず(!!)貸切りのような状態で利用することができました。

簡単に施設紹介をします。

お昼ご飯

f:id:iammyeye1:20190225192101j:plain
お昼ご飯

広間 

f:id:iammyeye1:20190225194458j:plain:h551:w681
広間

温泉

f:id:iammyeye1:20190225193734j:plain:h551:w681
温泉

訪問した施設はこちらです。

colony-hakone.com

開発合宿では何をしたのか?

ここからが本題ですが、今回の開発合宿のテーマは「自由開発」でした。

今回の合宿も前回に引き続き、それぞれが一つのプロダクトを開発しました。

お昼ご飯をいただいて2時ごろから雑談混じりで開発は始まり、次第にそれぞれの開発に集中していきました。

f:id:iammyeye1:20190225193857j:plain:h551:w681
開発風景

みんな自由に開発しています!

夕飯・温泉後

18時半の夕ご飯で一旦休憩。

f:id:iammyeye1:20190225192051j:plain
豪華な夕御飯

温泉に入るなど各自で休憩を挟みつつ開発を続けます。 温泉とマッサージ機は凶器ということも知りました。

f:id:iammyeye1:20190225193907j:plain
開発デスマーチ開発を楽しんでる様子

開発の合間の一枚です!

楽しそうな笑顔〜。

しかし開発が進まず5時ごろまで開発をしていた人も、、、

2日目

朝から豪華な食事をいただき、開発の最終調整と発表資料を作成して10時半から発表開始です。

僕は遅くまで起きていたので寝不足で食欲がなく、ご飯は2杯しか食べれませんでした、、、(美味しかったです)

f:id:iammyeye1:20190225192332j:plain
朝食

発表風景

最後にそれぞれが発表資料をスライドで作り、プロダクトのデモンストレーションなども交えながら発表しました!

f:id:iammyeye1:20190225193929j:plain
個人的に一番好きだったプロダクトを開発されたメルさんの発表

こんな感じで発表会が行われました。

発表では、技術的にはAWS, Rails, Lambda, bulma, dockerなど様々な分野に渡り、開発物も自分が欲しいプロダクト、業務効率改善用プロダクト、日頃の業務で関わることの少ないライブラリを試してみた開発など十人十色でした。

結果として幅広い分野の発表となり、非常に知的好奇心を刺激しあう発表でした。

まとめ

まず2日間に渡り開発に集中できる素晴らしい環境を整えてくださった施設の皆様ありがとうございました。

合宿全体に関してですが、まとまった時間を設けて勉強することのできる非常に貴重な機会でした。

発表を通じてそれぞれの成果や知識の共有をすることで、チーム全体で大きく成長できたのかなと思います。

また2日間一緒に過ごしたことでコミュニケーションが活発になったのも良かったと感じています。

個人的に非常に楽しかった合宿でした。

最後は集合写真でもって締めさせていただきます。

f:id:iammyeye1:20190225192239j:plain

また参加したい〜。

すごいTerminal 楽しく学ぼう!

最近Terminalが楽しい takeokunn です。

社内勉強会の順番が回ってきたので、せっかくだから最近ハマってるterminalについて語っちゃおうかなーと思いスライドを作りました!

インスタ映えしちゃうterminalを創ってスタバでドヤ顔したいですね!

スタバといえば、木下ゆうかさんの動画!

↑チャンネル登録しよう

Vue.jsコミッターのkazuponさんをお招きして社内勉強会をしました

こんにちは!エンジニア(仮)のめる(@c5meru)です。
最近、弊社のフロントエンドエンジニアごーさんが、社内勉強会に立て続けにスーパーゲストを呼んでくださっています(第1回第2回 )。
上記に続いて今回はなんと、なんと、Vue.jsコミッターのkazuponさんこと、川口和也さんをお招きしました!🎉✨

続きを読む

Webの技術でYoutube Viewer作った

はじめに

この記事は、UUUM Advent Calendar 2018 20日目です。

12月から新入社員の takeokunn です。普段は LMND の開発をやっております。

UUUMに入った初日(9/18)、 クリエイター(youtuber)のことを全く知らず他の人と会話が合わずに詰みました。 そこでこの会社でこの先生きのこるには効率よくキャッチアップする方法が必要だと思ってUUUM Creatorの動画viewerを作りました。

木下ゆうか さん、ごはん美味しそうに食べるから好き

Effective Youtube Viewer

f:id:bararararatty:20181217152621p:plain

Electronは配布してないので自前でbuildしてください

使ってる技術

  • electron
  • webpack/babel
  • react
  • redux/redux-saga
  • service worker
  • CSS Grid
  • csscomb/eslint
  • CircleCI

よくあるweb frontendの構成です。

redux-saga 楽しい

サービスの質を決めるのはエラーハンドリングだと思っています。 web frontendにおいて、非同期処理、特にAjax周りのハンドリングは非常に面倒ですが、redux-sagaを使えば体感すっきり書くことができます。

この辺がわかりやすいです。 redux-sagaで非同期処理と戦う f:id:bararararatty:20181217160209p:plain

以下はyoutube channelを取得する action/reducer/saga の例です。

抜粋: src/actions/youtube.action.js

export const fetchChannelVideo = {
    request: channel_id => action(YOUTUBE.FETCH_CHANNEL_VIDEO_REQUEST, { channel_id: channel_id }),
    success: data => action(YOUTUBE.FETCH_CHANNEL_VIDEO_SUCCESS, { data: data }),
    failure: () => action(YOUTUBE.FETCH_CHANNEL_VIDEO_FAILURE)
};

抜粋: src/reducers/youtube.reducer.js

const channel_video = (state, action) => {
    switch (action.type) {
    case YOUTUBE.FETCH_CHANNEL_VIDEO_REQUEST:
        return { ...state, is_fetching: true };
    case YOUTUBE.FETCH_CHANNEL_VIDEO_SUCCESS:
        return {
            ...state,
            is_fetching: false,
            search_videos: action.payload.data.items.map(item => ({
                title: item.snippet.title,
                description: item.snippet.description,
                thumbnail_url: item.snippet.thumbnails.medium.url,
                video_id: item.id.videoId,
                comments: [],
                comment_count: null,
                dislike_count: null,
                favorite_count: null,
                like_count: null,
                view_count: null,
            }))
        };
    case YOUTUBE.FETCH_CHANNEL_VIDEO_FAILURE:
        return { ...state, is_fetching: false };
    }
};

抜粋: src/sagas/youtube.saga.js

function* handleFetchChannelVideo() {
    for(;;) {
        const action = yield take(YOUTUBE.FETCH_CHANNEL_VIDEO_REQUEST);
        const response = yield call(ajax.searchChannelId, action.payload.channel_id);
        switch (response.status) {
        case 200:
            yield put(youtube.fetchChannelVideo.success(response.data));
            break;
        default:
            yield toastr.error('失敗', '通信失敗');
            yield put(youtube.fetchChannelVideo.failure());
        }
    }
}

今後の展望

弊社ではクリエイター分析ツールの開発などもやっているのでそれと連携して拡張できたら良いなぁと思います。

また、新着のsubscribe(webpush)やそのクリエイターの詳細情報など載せられるともっともっと良くなるだろうなぁと

今はクリエイターの管理がjsonで手書きで書いていて雑なのでなんとかしたいのと、あまりテストを書けてないので充実させたいなぁと src/options/youtuber.json

最後に

木下ゆうか さんの動画をデバッグに使い始めてから体感開発効率は上がるわ、体感javascript力も上がるわ、体感リロード時間も早くなるわで良いことづくめでした。本当にありがとうございました。 ← チャンネル登録しよう

UUUMでは (任意の文字列)エンジニアを募集しているそうです。 www.wantedly.com www.wantedly.com

CSS3のkeyframesで はじめしゃちょーを走らせてみました

こんにちは!エンジニア(仮)のめる(@c5meru)です。
こちらの記事は、UUUM Advent Calendar 2018 19日目の記事です!

5日目の前回は、CSSでHIKAKINをかきました
HIKAKINときたので、今回もUUUMのクリエイターである、はじめしゃちょーを題材にしたいと思います!

続きを読む

日頃あまり使っていないAWSのサービスを使う!〜AWS IoT編〜

はじめに

こちらは UUUM Advent Calendar 2018 18日目の記事です。

こんにちは、エンジニアのいぐちです。 WEBアプリケーションを開発していくうえで、AWSと触れ合うことが多い今日この頃ですが、ものすごい数のサービスを提供しているAWSの一部のサービスしか利用できておらず、少し寂しい・・・ そこで、日頃仕事で使ったことのないAWSのサービスを使ってみようということで AWS IoT を使ってみることにしました。

今回はRaspberry Pi2台とAWS IoTのセキュアなメッセージブローカーの機能を利用して簡単なおうちハック的なことをしたのでそちらをつらつらと書いていきます。

AWS IoT

AWS IoTというのはその名の通りちゃんとしたIoTでの開発をおこなっていくうえで必要になってくる様々な機能をまとめたサービス群になります。

詳しくはAWSのドキュメントをご覧ください。

AWS IoT とは - AWS IoT

今回利用したメッセージブローカー機能というのは、 MQTT等のプロトコルで各IoTデバイス(今回はRaspberry Pi)同士のメッセージのやり取りをpub/subモデルで行うものです。 MQTTなどがよく分からない方は以下のリンクをご覧ください。

MQTT の基本知識

やったこと

AWS IoTを使って、 目覚まし時計 を作りました。

え?目覚まし時計にAWS IoTって何言ってるの?ん? と思われるかもしれないですがちょっと説明させてください。

今回AWS IoTを使って作った目覚まし時計は、 スピーカーとスイッチが分離された目覚まし時計 です。

スピーカー側を寝室に置いてスイッチを遠く離れた別の部屋に置くことで、うるさいアラームを止めるのに歩いてスイッチを押しにいかなければならず嫌でも起床することになるという朝が弱い我が家の 最終兵器 です。

構成

  • Raspberry Pi A
    • スピーカーのみ接続
    • 特定の時間になるとアラームが鳴る
    • AWS IoTでTopicをsubscribeしており、別のデバイスからメッセージがpublishされたらアラームが止まる
  • Raspberry Pi B
    • タクトスイッチのみ接続
    • タクトスイッチを押すとメッセージをpublishする。
  • ソースコード
    • それぞれのRaspberry PiにはPython3系で書かれた簡単なコードが配置されています。

f:id:rinjin5th:20181217121317j:plain

AWS IoTの設定

まず以下のとおりモノを作成していきます。 (2台のデバイスを使用していますがとりあえず今回は1つのモノを使いまわします。)

f:id:rinjin5th:20181217010750p:plain

f:id:rinjin5th:20181217010841p:plain

今回は必須項目のモノの名前だけ適当に設定します。 f:id:rinjin5th:20181217010927p:plain

誰でも簡単にメッセージを送受信できては困るので、証明書による認証でセキュリティを担保します。 証明書もコンソール上からすぐに作成できます。

f:id:rinjin5th:20181217011138p:plain

作成できたら、モノの証明書、プライベートキー、ルートCAの証明書をダウンロードしておきます。 また証明書を有効化しとりあえず完了をクリックします。

f:id:rinjin5th:20181217011155p:plain

次にポリシーを作成し、先ほど作成した証明書にアタッチします。 今回はAWSIoT以外操作しないのでiot:*を指定することでAWSIoTの全操作が可能なように設定しておきます。

f:id:rinjin5th:20181217011214p:plain

f:id:rinjin5th:20181217011234p:plain

f:id:rinjin5th:20181217011250p:plain

これだけで設定は完了です。

ソースコード

一部抜粋してソースコードを掲載し説明します。 フルバージョンはこちらで公開しておりますので、気になる方はご確認ください。

https://github.com/rinjin5th/remote-alarm/

ライブラリとして以下を使用しています。

  • AWSIoTPythonSDK
  • PyAudio

AWSIoTMQTTClientの初期設定

スピーカー側、スイッチ側両方共にAWSIoTMQTTClientの設定を行う必要があります。 重要な部分は以下の通りです。

  • ClientIDの設定
  • エンドポイントの設定
  • AWSのルートCA証明書、モノの証明書、プライベートキーの設定
class MQTTClient:
    def __init__(self, client_id, host, root_ca, cert, key):
        # client_idはスピーカー側とスイッチ側で別の値にする必要がある
        self._client = AWSIoTMQTTClient(client_id)
        # AWSコンソール上で確認できるエンドポイント
        # ex)xxxxx..iot.ap-northeast-1.amazonaws.com
        self._client.configureEndpoint(host, 8883)
        # ルートCA証明書、モノの証明書、プライベートキーファイルのパスを設定
        self._client.configureCredentials(root_ca, key, cert)

parser = argparse.ArgumentParser()
parser.add_argument("-e", "--endpoint", action="store", required=True, dest="host", help="Your AWS IoT custom endpoint")
parser.add_argument("-r", "--rootCA", action="store", required=True, dest="rootCAPath", help="Root CA file path")
parser.add_argument("-c", "--cert", action="store", required=True, dest="certificatePath", help="Certificate file path")
parser.add_argument("-k", "--key", action="store", required=True, dest="privateKeyPath", help="Private key file path")
parser.add_argument("-m", "--mode", action="store", dest="mode", default="alarm" ,help="Operation modes: %s"%str(ALLOWED_MODES))

args = parser.parse_args()
host = args.host
rootCAPath = args.rootCAPath
certificatePath = args.certificatePath
privateKeyPath = args.privateKeyPath

clientId = "client_" + args.mode
mqtt_client = iotclient.MQTTClient(clientId, host, rootCAPath, certificatePath, privateKeyPath)

スピーカー側

大まかな流れは以下の通りです。

  1. トピックをsubscribeする。メッセージ受信時に実行されるcallback処理(アラーム停止)を設定しておく。
  2. アラームをループして再生しつづける。
class Alarm:
    playing = False
    
    def __init__(self):
        self._audio = pyaudio.PyAudio()

    def play(self, filename):
        self.playing = True
        while True:
            if not self.playing :
                break
            with wave.open(filename, 'rb') as wf:
                stream = self._audio.open(format=self._audio.get_format_from_width(wf.getsampwidth()),
                                 channels=wf.getnchannels(),
                                 rate=wf.getframerate(),
                                 output=True)
                 
                data = wf.readframes(1024)

                while data != b'':
                    stream.write(data)
                    data = wf.readframes(1024)

                stream.stop_stream()
                stream.close()

    def stop(self, client, userdata, message):
        self.playing = False

class MQTTClient:
    def subscribe(self, topic, callback):
        self._client.connect()
        self._client.subscribe(topic, 1, callback)

alarm_obj = alarm.Alarm()
# アラームの停止処理をメッセージ受信時のcallbackメソッドとして設定する
mqtt_client.subscribe("remote/alarm", alarm_obj.stop)
alarm_obj.play(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'alarm.wav'))

今回はあまり複雑な作りにはしたくなかったため、こちらのプログラムを起床時間にcronで実行しております。

スイッチ側

大まかな流れは以下の通りです。

  1. スイッチの状態を監視する。
  2. スイッチが押されたらメッセージをpublishする。
class MQTTClient:
    def publish(self, topic, message):
        self._client.connect()
        message_d = {}
        message_d['message'] = message
        self._client.publish(topic, json.dumps(message_d), 1)
        self._client.disconnect()

def watch():
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(24, GPIO.IN) 
    
    try:
        while True:
            # タクトスイッチが押されたらループを抜けて後続の処理を実行
            if GPIO.input(24) == GPIO.HIGH:
                break 
            time.sleep(0.1)
    
    except KeyboardInterrupt:
        pass
    
    GPIO.cleanup() 

watch()
mqtt_client.publish("remote/alarm", "stop")

こちらもまた複雑な作りにはしたくなかったため、こちらのプログラムを起床時間にcronで実行しております。

まとめ

今回は目覚まし時計という軽いモノだったため、AWS IoTのほんの一部の機能しか利用しておりません。 今後はもっと色々なものを作ってAWS IoTを極めていきたいなと思います。

さいごに

UUUMではAWSのサービス(IoTは多分使わないけど・・)を使ったWEBアプリケーション開発ができる方を募集しております! とりあえずお気軽にお話だけでもどうぞ!

www.wantedly.com

www.wantedly.com