Wowgineer Notes

WowTechエンジニアの徒然開発日記

Wowgineer Notes

〜新たな"Wow"を生み出す〜

大きなActivityは小さく分割したほうがいいお話

こんにちは。ワウテックAndroidアプリ担当の岡野です。

今回は珍しくAndroidに関する投稿です。

WowTalkには様々な機能がありますが、私が着任当時に実装した機能がタスク管理機能でした。当時はiOS担当だったため、Androidの実装は当時の担当の方がしてくれた(※1)のですが、最近不定期に機能改善をする際にAndroidのタスク管理機能のメンテナンスに苦心することが多かったので思い切ってアーキテクチャの変更に取り組みました。

1 タスク管理機能の概要

WowTalkでは作業内容をタスクとして管理する機能があり、テキスト情報に加えて画像やドキュメントファイルを添付し、タスクの担当者・関係者間で共有できる仕組みです。タスクには期限が設定でき、期限切れ当日には通知が届くようにもなっています。

f:id:okano4413:20191204135117p:plain

自分はもっぱら個人のタスク管理に使うことが多い

このタスク管理機能は大きく3種類のActivityから構成されています。

リスト画面 タスクの一覧を表示する画面
作成画面 タスクを作成する画面
詳細/編集画面 作成したタスクの詳細を確認し、編集する画面

 

f:id:okano4413:20191203153350p:plain

タスク管理機能。詳細画面と編集画面は1つのActivityで実装されていた

それぞれ独立したActivityで実装されており、サーバーからのデータ取得リクエスト呼び出しや描画に伴うデータ加工などもそれぞれのActivityで実装されていました。

2. 実装の問題点

上述したとおり複数の役割を1つのActivityで実装したため、多いものではソースコードの行数が2800行近くにまでなりました。多いと見るかは個人差があるかもしれませんが、個人的にはかなり苦労するレベルです。またメンテナンスも忘れた頃にするので、毎回作業時に「これなにしてるんだっけ...」と処理を見返すことが何度もあり、作業効率があまりに悪いということもありました。

3. 改善方針

今回はタスクの詳細・編集画面を改修することにし、下記の2つを実施することにしました。

画面ごとにActivityを切り分ける

タスクの詳細画面と編集画面は類似したインターフェース及びデータ構造のため、1つのクラスで実装されていましたが、それぞれ表示の仕方が変わるため、モードの切り替え時に逐次ViewのVisibilityを変更していました。

これらを個別のActivityに分割することで状態の管理などを気にしなくて済むような実装にしていきます。 

データとViewの操作をActivityから切り分ける(MVVMアーキテクチャ)

合わせてデータ操作とViewの状態管理も切り分けて、それぞれが依存しないようにしていきます。今回はGoogleがAndroidアプリ実装時の推奨アーキテクチャとしているMVVM(※2)を採用することにしました。

簡単にMVVMについて書くと、データの取得/送信部分をModel(M)クラスに任せ、ViewModel(VM)はModelからデータを受け取り、View(V)が使いやすい形でデータを提供します(※3)。

今回2つの画面のデータ取得ロジックがほぼ同じため、下記の様にModelを共通化した形で変更を加えることにしました。

f:id:okano4413:20191204141623p:plain

1つの大きなActivityを画面と処理の種類で分割する

4. 改善の効果

画面ごとにActivityを切り分けた上、MVVMにすることで1クラスあたりのソースコード量が減り、かなり見やすくなりました。合わせて以下の様な効果がありました。

・現在の状態を意識する必要がなくなった

・クラス内部の変数が減り、各変数への依存関係も減ったためロジックの構築がシンプルになった

・表示のための"ビューロジック"と"実際の表示処理"が個々のクラス内で完結しているため、実装時に頭の中で同時に考える負担が減り実装が早くなった

1つ目と2つ目は書いている通りで、今の画面が詳細画面なのか編集画面なのかを意識する必要もなく、またそれぞれの状態で利用していた変数を個別のClassで管理する様になったので、条件分岐などの記述が減り1つの関数の行数もだいぶ削減されました。

 

3つ目に関しては実際に経験があるとイメージしやすいのですが、1つのActivity内でデータの取得から表示までの一連の流れを書く場合

①バックグランドでデータの取得

②受け取ったデータをメインスレッド(Handler)にポストする

③メインスレッド内で変数への保存を行いデータの表示処理を行う

みたいなことをするわけですが、一連の流れを描く際に1つで今回取り扱うデータとは関係ないデータを取り扱う処理(例えば描画や別の処理のエラーハンドリング関数)をかき分けながらHandlerを探すなんてことがよくあります。

これが無くなるだけでもかなり実装時の負担は低減されました。 

5. 気づいたこと

今回実装していて思ったのがViewModelが思ったよりも肥大化したことでした。実装にあたりLiveData(※4)を利用した値の自動更新を採用したのですが、これとビューロジックを一度に実装するとそこそこな分量になりました(1000行程)

また通常ModelはSingleton(※5)で実装してDBやサーバーから取得したデータをキャッシュすることを推奨(※6)されているのですが、別のタスクを開いたはずが直前のタスクの情報が表示されてるなんてこともありました。ActivityとModelは依存関係を排除しているとはいえ、用途は想定して実装しないといけませんね。

 

6. 終わりに

今回はタスク管理機能のFatActivityをビューロジックの切り分けと整理によって2800行あったソースコードを200~1000行のクラスに分割し、保守性を改善しました。今後はこの画面以外でも同様の問題を抱えている箇所を1つずつ改善していければと思います。

 

当たり障りのない内容でしたが、基本的なことの大切さを実感します。社内ではこういった既存実装が抱える問題を「負の遺産」と呼んでいますが、まだまだ負の遺産はありますので1つずつ返済していきたいと思います。

 

7. 注釈

※1 : Androidの実装は当時の担当の方がしてくれた

決して前任者のソースコードがわかりにくいというわけではないです。

※2 : アプリのアーキテクチャガイド

https://developer.android.com/jetpack/docs/guide?hl=ja#cache-data

※3 : MVCと何が違うのか

いわゆる昔のModel-View-Controller(MVC)と同じに聞こえるかもしれませんが、個人的に思うControllerとViewModelの大きな違いはまさにViewModelである点です。

画面で表示する際、仕様によって表現を変える必要が出てくる場合があります。例えばサーバー側ではLong型の UnixTimeで保存し、アプリ側ではこれを「2019-11-29」と表示する場合もあれば「明日」「今日」の様な表現にする場合もあります。

これらの表示の上での計算(=ビューロジック)は、MVCにおいてControllerで記述することはせず、Viewで加工することになります。一方MVVMではこのビューロジックをViewではなくViewModelに担当させることでViewは複雑な処理を記述する必要がなく、表示だけに集中できます。

 

※4 : LiveDataの概要 

 https://developer.android.com/topic/libraries/architecture/livedata?hl=ja

※5 : デザインパターン シングルトン

https://qiita.com/shoheiyokoyama/items/c16fd547a77773c0ccc1

※6 : Androidアプリ設計パターン入門

https://peaks.cc/books/architecture_patterns

 

我々ワウテック技術開発部はこのような環境で開発をしています。
興味を持った方がいらっしゃればいつでもご連絡下さい。

f:id:wowtech-dev:20190313163146j:plain

1週間で作って社内リリースしたヘルプボットを性能改善した話

お久しぶりです。WowTalkのAndroid開発担当している岡野です。

前回1週間でヘルプボット作って社内リリースした話を書きました。

その際にいくつか課題が出ましたので、今回は前回ほど専門的な内容ではありませんが、課題解決までのアプローチを書いていきたいと思います。

1.前回の課題点

前回作成したボットですが、欠点として単一のキーワードを送った時した回答できないという課題がありました。通常ユーザーがボットに質問する時には適当な長さの文章を送ってくると考えると使い物にならないですね。

この課題を解決するため下記の流れでアプローチしていきました。

  1. 文章の形態素解析によるキーワード抽出
  2. 形態素解析サービス/ツールの選定
  3. 記事の検索方法の変更 

1つずつ見ていきましょう。

2.文章の形態素解析によるキーワード抽出

形態素解析とはなんぞやというと方のためにお話しすると、要は文章を意味のある単語単位(形態素)で分割し、それぞれの品詞などを解析する作業です。

例文 Androidでログインができない
形態素で分割 Android/で/ログイン/が/でき/ない
品詞の抽出  名詞 / 助詞 / 名詞 /助詞 /動詞 / 助動詞

この中で意味のある単語=キーワードは「Android」「ログイン」= 名詞となることはわかると思います。あとはこのキーワードをうまくユーザガイドの検索ワードとして投げてあげるだけ。

3.形態素解析サービス/ツールの選定

SaaSサービス信者になりつつある自分はAWSにも形態素解析できるサービスがあるのではと探してみるとAmazon Comprehendというサービスがありました。

やったぜ(*´-`)...と思ったけどこれ、日本語に対応していなかった...orz

他にないかなと探してGoogle Cloud Platform(GCP)にもCloud Natural Language APIという似たようなサービスがあるそうです。

Googleさんなら日本語にも対応しているし安心かな。

f:id:okano4413:20191003114754p:plain

Cloud Natural Language Apiでの解析

ちゃんと日本語も解析できてるようです。

あとはコスト面が気になるので確認することにしました。料金のページを見ると構文解析を含むNatural Language Apiはユニット単位の課金らしいです。(以下抜粋)

 

Natural Language API の使用量は「ユニット」の数に基づいて計算されます。分析のためにこの API に送信される各ドキュメントのユニット数は、少なくとも 1 ユニットとなります。ドキュメントに 1,000 文字を超える Unicode 文字が含まれている場合(空白文字や、HTML / XML タグなどのマークアップ文字も含まれます)、1,000 文字 = 1 ユニットの複数ユニットとしてカウントされます。

たとえば、Natural Language API に送信する 3 件のリクエストそれぞれに、800 文字、1,500 文字、600 文字が含まれている場合、1 番目のリクエスト(800)が 1 ユニット、2 番目のリクエスト(1,500)が 2 ユニット、3 番目のリクエスト(600)が 1 ユニット、合計 4 ユニットとして課金されます。

Natural Language API の使用料は、使用された API の機能とその機能を使用して評価されたユニットの数に基づいて、月単位で計算されます。次の表に、請求月に分析されたユニットの総数に基づく 1,000 ユニットあたりの料金を示します。

つまり1000文字ごとに1ユニットが消費され、最低でも1回のリクエストで1ユニットが消費されるそうです。

通常チャットで質問する時には短文であることが多いため、これでは費用対効果が悪いと判断しました。無念。。。

このAPI、そこそこの長さの分を構文解析する時には有用だけど、今回みたいな短文向きではなさそうです(料金的な意味で)。

最後に以前Androidの開発の時にふと見つけたKuromojiというライブラリを思い出してnodeモジュールがないか探してみることに。ありました

最近はなんでもありますね。ほんと助かります。

f:id:okano4413:20191003121327p:plain

Kuromojiによる解析

 これなら日本語にも対応してくれてるし、料金も利用したLambdaのリソース分しかかからないので、(Lambdaの利用料金は実行時間と消費したメモリの量に依存するため)残念なコードを書かない限り料金も抑えれそうです。

 以下今回の調査のまとめです。

目的 文(sentence)を単語(word)に分割できる    
  amazon comprehend cloud natural language api Kuromoji
機能 構文解析
キーフレーズ抽出
感情分析
エンティティ分析
言語検出など
構文解析
エンティティ分析
感情分析
コンテンツ分類など
構文解析
長所 ・ドキュメントからそのドキュメントが何を指すものなのかを学習させることでユーザの真に知りたい内容や行いたい行動を抽出できる ・精度自体はAWSよりいいと思う。
・5000ユニットまで無料利用枠がある
・node moduleを
入れるだけですぐ利用できる
・Lambdaの無料利用枠を利用した計算ができる
短所 ・日本語で利用できる機能が少ない。
・文章の解析がメインとなるため、ボットのように短文で質問を行う場合は費用対効果が悪い
・1ユニットが1000文字で設定されているためAWSよりも費用対効果が悪い ・名詞を分割して解析するので自分でどこまでがまとまった単語なのか判定するロジックを追加する必要がある。
料金体系 10億文字まで
$0.0001/100文字
10億文字まで
$0.001/1000文字
Lambdaの利用枠内

4.記事の検索方法の変更

前回WP REST APIで検索を行う時、通常の質問形式や複数キーワードを投げても適切な回答が得られないという話をしました。これは

{domain}/wp-json/wp/v2/posts/?search="キーワード"

のクエリで検索した時に、完全一致で検索しようとするため、投稿のタイトルや本文内に質問文と同じものがなければ検索結果に出てこないことが原因でした。

またユーザの質問文に含まれるキーワードが必ずしもユーザガイドに含まれないこともあります。

例えば「トークの一覧」という言い方をする人と「トークのリスト」という人がいる場合にユーザガイド内の言い方が「トークの一覧」だったとすると後者の検索で「リスト」というキーワードに一致しなくなります。

このような揺らぎを少しでもなくすために、ある程度予測される表現については別途「タグ」として管理することにし、「タグ」による検索を行えるようにしました。

{domain}/wp-json/wp/v2/posts/?filter[tag]="キーワード1,キーワード2..."

こうすることでかなりの期待したものに近い結果が得られるようになりました。

f:id:okano4413:20191003143458p:plain

あとは細かいロジックの調整(連続する名詞を1つの単語として検索する、一致するキーワードが多い記事を優先して表示するなど)をして完成(?)と相成りました。

5.これからのお話

今回ユーザーガイドURLを取得するボットを作っていて気づいたのが、

「1つの記事の中で複数の機能を説明している」

記事があることです。

もちろん、全く関係のない機能を説明しているわけではないのですが、本来ユーザーが調べたい機能以外のことが多く存在していると、欲しい回答を探す手間が発生します。

せっかく回答の書かれたURLを教えてもらってもその回答がすぐにはわからないとストレスになってしまいますね。これは当社のユーザーガイド作成の至らない点です。。。

 

今後はユーザーガイドの改善もしながらボットの改善を目指して行けるようにも動いていきたいと思います。そしていつの日か、このボットが皆さんの前に現れてくれることを願います。

6.おわりに

今回の記事では、以前作成した「キーワードに一致する記事のURLを返却するボット」へのリクエストフォーマットを改善してより意味のあるURLを返すようにするための取り組みについて記載しました。内容としてはあまり専門的なものではなかったですが、結局はこういう地道なことの積み重ねでUXがよくなるのかなとも思っています。

 

次回はいい加減Androidについて記事を書きたいなと思いますので、その時は改めてよろしくお願いします。^^; 

参考URL

 

我々ワウテック技術開発部はこのような環境で開発をしています。
興味を持った方がいらっしゃればいつでもご連絡下さい。

f:id:wowtech-dev:20190313163146j:plain

1週間でヘルプボット作って社内リリースしたお話

こんにちは。WowTalkのAndroid開発担当している岡野です。

今回はAndroidと全く関係ないのですが、AWSのサービスを使ってWowTalkのユーザサポート用のボットを作って1週間で社内リリースした話を書いていこうと思います。

1. なぜ作ろうと思ったのか

WowTalkには電話やメールでのサポート窓口というものがあるですが、ここに来る問い合わせ件数を減らせないかというのがそもそもの始まりでした。

WowTalkには操作説明が載っているユーザーサイトもあるのですが、せっかくWowTalkには自分でカスタマイズできるボット機能と、WowTalkにメッセージを送信できるOpenAPIもあるのでそれをうまく利用したいと考えました。

 

f:id:okano4413:20190723154630p:plain

今回考えたアーキテクチャの概要

今回は検証も兼ねて、社内からの問い合わせに対応できるよう社内公開しましたが、最終的にはユーザの皆様に公開し、利用していただけるようなボットを提供できればと考えています。

2.開発環境

Lambda : node.js 8.10 

ServerlessFramework : 1.40 

3. サーバー立てずに実装したい

WowTalkは裏側でAWSを使っているのですが、今回は機能上EC2でインスタンスを立てる必要もないと判断したためAPIGateWayとLambdaを使うことにしました。

ただしインスタンスを立てないことに伴いLambdaの設定を工夫する必要がありました。

4. IPアドレスがない問題

WowTalkのOpenAPIの利用にはセキュアアクセスのためアクセス元にIPアドレスが必要ですが、LambdaはサーバーレスなFaaSサービスのためIPアドレスがありません。

ではどうするかというと、OpenAPIと同じサブネットにLambdaを設置することで対応しました。OpenAPIは社内で検証を行うことがしばしばあるので、同じサービスネットワーク内からのアクセスは許可されています。なのでLambda関数のVPC設定から、WowTalkのインスタンス群と同じサブネットに配置することでOpenAPIの呼び出しを行えるようにしました。

 

f:id:okano4413:20190723144448p:plain

Lambda関数のコンソールからVPCの設定をする

5.Lamdaから外部へアクセスできない問題

ただこれだけでは不十分で、今度はLambdaからユーザーサイトにアクセスできなくなりました。上の画像の注意書きにもありますが、NAT GateWayを設定していないため外部ネットワークとのアクセスができず、ドメイン解決ができなくなるので当然と言えば当然なのですが(汗) 

これはRoute53のプライベートホストゾーン設定にユーザーサイトへアクセスするためのレコードセットを設定することで解決しました。OpenAPIのサーバーへのアクセスも同様に通常のドメイン設定ではアクセスできなくなったので合わせて設定することで対応しました。

6.回答しよう

ここまでできたらあとは裏側の処理をnode.jsでガリガリ書くだけです。まずはシンプルにキーワードに関連する記事のURLをボットのそれっぽく返却するだけにしました。

'use strict';
const crypto = require('crypto');
const URLSafeBase64 = require('urlsafe-base64'); 
const Buffertrim = require('buffertrim'); 
const https = require("request");
const Config = require('./config');
var util = require('util');

var configs;

module.exports.handleMessage = async (event, context, callback) => {
    const body = event['body'];
    const alias = event['stageVariables']['alias'];//APIGateWayから渡される環境変数
    configs = new Config(alias).getConfigs();//環境ごとに設定を変更
    if (body != null) {
        var json = getJsonFromBody(body);
        const userId = json['source']['userId'];
        const content = json['message']['content'];
        const companyId = json['source']['companyId']
        getURLFromKeyWord(content, function(answer){
            sendMessageBuddy(configs.BOT_ID, configs.BOT_PASSWORD, userId, companyId, answer);
        }); 
    }
    callback();
};

function getJsonFromBody(body) {
       /**   暗号化解析処理 (省略)  **/
}

// ユーザーサイトからキーワードが該当する記事を取得する
function getURLFromKeyWord(keyword, callback) {
    var encodedKeyWord = encodeURI(keyword);
    var opt = {
        url: configs.SUPPORT_URL,
        headers: {
            "Accept" : "application/json"
        },
        qs : {
            search : encodedKeyWord,
            per_page : 100
        },
        qsStringifyOptions : {
            encode : false
        },
        rejectUnauthorized : false
    }
    https.get(opt, function(error, response, body) {
        if (body != null) {
            var JSONBody = JSON.parse(body);
            var answers = []; //回答メッセージの配列
            var answer;
            if (body.length == 0 || body == "[]") {
                answer = configs.HINT_TEXT_FOR_NO_RESULT;
            } else {
                //ここで回答本文作成処理。
                answer = util.format(configs.HINT_TEXT_FOR_RESULT, keyword);
                for (var i=0; i<JSONBody.length; i++) {
                    var val = JSONBody[i];
                    var title = val["title"]["rendered"];
                    var link = val["link"];

                    /** WowTalkに送れる文字数は800文字の文字制限があるため、
                     *  回答が制限を超える場合は別のメッセージとして送信するようにする。
                     **/

                    //判定用文字列
                    var textAfterJoin = answer + title + "\n" + link + "\n";
                    if (answer.length < configs.MAX_LENGTH 
                        && textAfterJoin.length >= configs.MAX_LENGTH) {
                        answers.push(answer);
                        answer = util.format(configs.HINT_TEXT_FOR_RESULT, keyword);
                    }
                    if (link != null) {
                        answer += title + "\n" + link + "\n";
                    }
                }
            }
            answers.push(answer);
            for (var j=0; j< answers.length ; j++) {
                var ans = answers[j];
                callback(ans);
            }
        } else if (error != null) {
            callback(configs.HINT_TEXT_FOR_REQUEST_ERROR);
        }
    });
}

//OpenAPIにリクエスト
function sendMessageBuddy(fromAccount, fromPassword, toAccount, companyId, content) {
    const message = {
        from_account : fromAccount,
        from_account_password: fromPassword,
        to_account : toAccount,
        message_content: content
    };
    
    const messageJsonString = JSON.stringify(message);
    
    /**   回答の暗号化処理 (省略)  **/
    
    var opt = {
        uri: configs.OPENAPI_URL,
        method: 'POST',
        headers:{
            "Content-type": "application/x-www-form-urlencoded",
        },
        form: {
            "company": companyId,
            "request_id": "req123",
            "language":"jp",
            "request_data":"暗号化された回答"
        }
    }
    https.post(opt,function(error,response,body){
        /** Open APIにリクエストを投げた後の処理 **/
    });
    
}

 

こうして記事のURLを返却するボットを無事(?)社内リリースしました。

f:id:okano4413:20190723145627p:plain

7. これからのお話

ただこれでは通常の質問形式や複数キーワードで検索をかけようとした時に、WP REST APIでは投稿が引っかからないので正直ほとんど使い物になりませんでした orz

f:id:okano4413:20190723150643p:plain

というわけで「投げられたメッセージをキーワード解析しながらWP REST APIへの検索方法も改善しないと使い物にならないな〜」となりました。そちらについてはまた今後追記していきたいと思います。 

8. おわりに

今回の記事では(ボットのクオリティはさておき)お手製のサポートボットをリリースするまでを簡単に追いました。

タイトルにもある通り、AWSのサーバーレスアーキテクチャを利用すると、アーキテクチャを考えてから社内リリースするまで1週間という短期間で実装ができました。EC2でインスタンスを立てると利用していないリソースにまで料金が発生したり負荷が高くなった時のスケーリングとかリソースの配分なども考えないといけませんが、そこを考慮しなくて済むのは魅力的です。

Androidアプリ開発する傍ら、サービスがもっと使いやすくなるような機能の模索もしていきたいと思います。

 

参考URL

AWS関連
Serverless関連

 

我々ワウテック技術開発部はこのような環境で開発をしています。
興味を持った方がいらっしゃればいつでもご連絡下さい。

f:id:wowtech-dev:20190313163146j:plain

React+PHP (CodeIgniter) でWebアプリケーションを作る ~Storybookを添えて~

こんにちは。Web開発チームの島根です。
今回のテーマはReactとCodeIgniterでのWebアプリケーション開発です。
Laravel使った方が楽なのでしょうが、現在動いている他のシステムでCodeIgniterが使われているので今回やったことを他のシステムに流用できるようCodeIgniterを選択しました。
やったことはと言えば、要はReactをビルドして作られたindex.htmlと同じことをCodeIginterのviewで行うということだけです。
ただし、2点ほどつまづいてしまいました。

1.create-react-appでreactプロジェクトを作ったのでビルドする度にファイル名が変わる

 これは、create-react-appで作ったreacctプロジェクトでは、webpackの設定でビルド時にファイル名にハッシュ値を含むようにしてあります。
ejectすればconfigファイルを設定できるようですが戻せなくなるため、今回はbuildした後にできるasset-manifest.jsonからファイル名を読み込むという方法をとりました。
読み込みたいのは
・main.<ハッシュ値>.chunk.js
・<ハッシュ値>.chunk.js
・main.<ハッシュ値>.chunk.css
・<ハッシュ値>.chunk.css
の4つのファイルです。

f:id:wt-shimane:20190329121311p:plainf:id:wt-shimane:20190329120737p:plain
割と力技です。笑
・controller

public function index()
{
        $assetJson  = file_get_contents('./build/asset-manifest.json');
        $asset = json_decode($assetJson, true);
        $base = '<reactをビルドしてできたbuildディレクトリのパス>';
        $filePaths = array();
        foreach($asset as $key => $filename){
            if($key === 'main.css'){
                $filePaths['MAIN_CSS'] = $base . $filename;
            }
            if($key === 'main.js'){
                $filePaths['MAIN_JS'] = $base . $filename;
            }
            if($key !== 'main.css' && preg_match("/chunk\.css$/", $key)){
                $filePaths['CHUNK_CSS'] = $base . $filename;
            }
            if($key !== 'main.js' && preg_match("/chunk\.js$/", $key)){
                $filePaths['CHUNK_JS'] = $base . $filename;
            }
        }

		$this->load->view('welcome_message', $filePaths);
	}
}

file_get_contentsでasset-manifest.jsonからファイル名のマッピング情報を取って正規表現で各ファイルを判断してます。main.jsとmain.cssはキーが固定なのでキーで判断し、残り2つは正規表現で判断。ただしmain.jsとmain.cssもchunk.jsやchunk.cssで終わるのでキーの名前ではじいています。(力技)
こうしておけばビルドしたらCodeIgniter側から読み取れるフォルダにasset-manifest.jsonとjsファイルとcssファイルを移動すれば読み取れるようになります。
ファイル移動の部分はshell書いてもいいですし、デプロイ時のフローに入れてもいいですし簡単に自動化できると思います。
・view

<?php
defined('BASEPATH') OR exit('No direct script access allowed');
?><!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8">
	<title></title>
    <link href="<?=$CHUNK_CSS?>" rel="stylesheet">
    <link href="<?=$MAIN_CSS?>" rel="stylesheet">
</head>
<body>

<div id="root">
</div>
        <script>!function(l){function e(e){for(var r,t,n=e[0],o=e[1],u=e[2],f=0,i=[];f<n.length;f++)t=n[f],p[t]&&i.push(p[t][0]),p[t]=0;for(r in o)Object.prototype.hasOwnProperty.call(o,r)&&(l[r]=o[r]);for(s&&s(e);i.length;)i.shift()();return c.push.apply(c,u||[]),a()}function a(){for(var e,r=0;r<c.length;r++){for(var t=c[r],n=!0,o=1;o<t.length;o++){var u=t[o];0!==p[u]&&(n=!1)}n&&(c.splice(r--,1),e=f(f.s=t[0]))}return e}var t={},p={1:0},c=[];function f(e){if(t[e])return t[e].exports;var r=t[e]={i:e,l:!1,exports:{}};return l[e].call(r.exports,r,r.exports,f),r.l=!0,r.exports}f.m=l,f.c=t,f.d=function(e,r,t){f.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},f.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(r,e){if(1&e&&(r=f(r)),8&e)return r;if(4&e&&"object"==typeof r&&r&&r.__esModule)return r;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:r}),2&e&&"string"!=typeof r)for(var n in r)f.d(t,n,function(e){return r[e]}.bind(null,n));return t},f.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return f.d(r,"a",r),r},f.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},f.p="/";var r=window.webpackJsonp=window.webpackJsonp||[],n=r.push.bind(r);r.push=e,r=r.slice();for(var o=0;o<r.length;o++)e(r[o]);var s=n;a()}([])</script>
<script src="<?=$MAIN_JS?>"></script>
<script src="<?=$CHUNK_JS?>"></script>
</body>
</html>

2.BrowserRouteを使うとページを読み込めない。

これに関してはエラーが出なかったため困りました。
jsやcssのファイルが読み込めていないわけでもなく、コンソールやサーバー側のログファイルにも気になるエラーは何もなし。
ブラウザプラグインのReact Developer Toolで調べたところ、Component自体が読み込めていない様子でした。
f:id:wt-shimane:20190329112201p:plain

rootの中が空なので一番トップのBrowserRouterがきちんとルーティングできていなかった。HashRouteに変えればちゃんと表示されました。

ReactDOM.render(
  <HashRouter>
    <div>
      <Route path='/' exact component={App}/>
      <Route path='/page1'  component={Page1}/>
      <Route path='/page2'  component={Page2}/>
    </div>
  </HashRouter>


, document.getElementById('root'));
 

構築手順

構築手順を簡単に書いておきます。
・create-react-app

create-react-app ci-sample

・codeigniter install

composer create-project kenjis/codeigniter-composer-installer codeigniter

・reactやCodeIgniterでコードを書く。ReactのRouterにはHashRouterを使う。

・buildしてCodeIgniter側にソースコードを移動。

・CodeIgniterのトップページでbuildしたreactのソースコードを読み込めるようにする。index.html内に直接書かれているscriptをコピペする。

おまけ

最後におまけですが、Storybookというコンポーネントをカタログにして共有できるツールも入れました。

f:id:wt-shimane:20190329115346p:plainf:id:wt-shimane:20190329115350p:plain

コンポーネントごとにPropsやコンポーネント自体の説明を書けたり渡す値を変えたりできるaddonがあったりしてすごくいいです。
Storybookはまだまだ活用しきれていないので熟した頃に投稿させてもらいます。

ワウテック技術開発部の開発環境について

こんにちは(^o^)!web開発チームの島根です。
本日からワウテックの技術開発部もブログを運営していくこととなりました!
技術的なトピックはもちろんのこと開発メンバーのことについても配信していく予定ですので、
このブログを通じてワウテックの技術開発部の雰囲気も伝えられたらなぁと考えています。

さて、記念すべき1本目のテーマは
「ワウテック技術開発部の開発環境について」です!!

開発環境といっても開発用のサーバーの設定や使っているツール等についてではなく、
今回はPC・デスク・チェア・オフィス内の様子など、どういった環境で開発しているのかをご紹介します。

 

PC

PCはWindowsかMacかを選ぶことはできます。
テレワーク制度もあるので基本的にノートPCが配布されています。
僕はMacbook Proを使っています。
僕はiOSの開発はしないのですが、Macだとコマンドラインからbashのコマンドうてるのでほとんどの作業をコマンドライン内で済ませることができるので僕はWindowsよりMacのほうが好きです。
Windowsも10になってからbashが使えるようになったらしいですが試していません笑。

f:id:wowtech-dev:20190313145640j:plain

PCとディスプレイ ( 画面に線が入っていますが反射です。割れているわけではないです笑 )

27型のディスプレイも配布されます。ソースコードを映すだけですがディスプレイが大きいと目が疲れにくいような気がします。
僕はもうこの大きさに慣れてしまって、家でテレワークをするときは別でディスプレイを持っていないので物足りなく感じてしまいます(-_-)

 

デスク


隣や前の席とのパーティションはありません。
必要ないものはロッカーに入れておくようにしているので、1人あたりの広さも十分かなーと思っています。

 

椅子

 椅子はErgohuman Fit(エルゴヒューマン フィット)のヘッドレスト有りのものを使わせてもらっています。
椅子に関しては事あるごとに、いいオフィスチェアを買って下さい!とねだっていたのですが、昨年12月にオフィス移転した際についに買ってもらえました( ゚Д゚ノノ"☆パチパチパチパチ
これ、ホント嬉しかったです。

f:id:wowtech-dev:20190313145711j:plain

調整箇所が多く自分の体に合わせて自在に調整することができて、まさにFit!

さらに、開発メンバー全員でオフィスチェア専門店に行き、実際に試してこれに選定したという思い出エピソードもあります笑

 

オフィスについて 

現在入居しているオフィスのビルは1フロアごとの貸出となっているので6階にはWowTechの社員しかいません。1フロアには2つ執務室があって、技術開発部は管理部とマーケティング部と同じ執務室となっています。
これらの部署はあまり外部と電話をするような部署ではないので執務室内はとても静かで開発に集中しやすい環境になっています。

f:id:wowtech-dev:20190313150045j:plain

オフィス内の風景

また、リフレッシュルームにコーヒーメーカーが置いてあり、いつでも豆から淹れた美味しいコーヒーを飲むことができます。コーヒー豆は近くのコーヒー屋さんから総務の方が定期的に買ってきてくれています。いつもありがとうございます(*´∀`*)

f:id:wowtech-dev:20190313150211j:plain

コーヒーメーカーとコーヒー豆

僕はコーヒーを毎日飲むので美味しいものがオフィスでできるってのは非常にありがたいです。

 

我々ワウテック技術開発部はこのような環境で開発をしています。
興味を持った方がいらっしゃればいつでもご連絡下さい。

f:id:wowtech-dev:20190313163146j:plain