この記事では、数ヶ月前のReactベースのWebアプリをリファクタリングして完全に書き換える方法と理由を共有します。
本ブログは英語版からの翻訳です。オリジナルはこちらからご確認いただけます。一部機械翻訳を使用しております。翻訳の間違いがありましたら、ご指摘いただけると幸いです。
私たちはミスをしがちです。そのため、作業の見直し、再編成、見直しが必要になることがよくあります。ソフトウェアエンジニアリングでは、このフェーズをリファクタリングと呼びます。
リファクタリングには価値があります。ソフトウェアを健全な状態に保つことができるからです。これはAoneに現れるような特別なタスクではありません。上手くやれば、プログラミング活動の定期的な一部になります。
#リファクタが完全なリライトになる場合
新しいプロジェクトや新しい開発スプリントを開始するときはいつも、最新かつ最高の技術やソフトウェアのデザインスキームを使用します。しかし、しばらくすると、新しい技術や新しい設計手法が出てきます。コアとなるコードベースの設計は、変更のしやすさに大きな違いをもたらします。拡張機能は多くの場合、作成時には意味のある方法で重ねて適用されますが、後になって、さらなる変更や機能、改善を開発するのがますます難しくなります。
私たちのチームの最初のReactプロジェクトは、2018年後半にアルファ版として開発されましたが、現在(2019年初頭)は、高い要求に応えるために、新しい機能、より多くのメンテナンス、より良いパフォーマンスを必要としています。
まず私たちのコアコードベースを改善する必要がありました。
"Embrace change" === "Re-write a five months old app"
- 依存関係の自由 - 依存関係をよりコントロールできるようにするために、Next.js から独自の configure 環境に移行しました。
- 懸念事項の分離 - 単一の責任という観点から、重要な機能(ルーティングなど)をReactから分離しました。
- 誇大広告の背後を見る - みんなが使っているものを自動的に使うのをやめて、自分たちに合ったものを検討し始めました。
- npmインストール時間 - いくつかの簡単な質問に基づいて、プロジェクトに注入する依存関係を選択します。最初に自問したのは、「自分たちで実装するにはどれくらいの時間がかかるのか」ということでした。
- 変化を受け入れる - コードと依存関係を定期的に更新し、リファクタリングします。
- パフォーマンスのコツ - このリファクタリングの段階では、アプリのパフォーマンスとユーザーエクスペリエンスも大幅に改善されました。
#プロローグ
今回関係するReactプロジェクトはHyperMLと呼ばれるものです。
HyperMLは、研究者や開発者を念頭に置いて設計された継続的な研究プラットフォームです。これは、研究者や開発者が(ディープラーニングのトレーニングのような)計算量の多いタスクをクラウドGPU上で最小限のオーバーヘッドで合理化された方法でWebインターフェースを介して実行できるようにするためのものです。
HyperMLは、チームの最初のReactアプリとして2018年に構築された、それを持っています。最良の方法は、最も早く、最適な設定のジャンプスタートを見つけること、そして、展開のためのマインドセットを持つことが重要であることに変わりはありませんでした。
"開発者の経験とエンドユーザーのパフォーマンスの両方に恍惚としているので、コミュニティで共有することにしました。" - Next.js
それは完璧に聞こえましたが、その後、私たちはそのSSRを使用していませんでした。私たちはより多くのコントロール、より多くの柔軟性を必要としていました、そして正直に言うと、私たちはこの "ブラックボックス "に快適さを感じませんでした。Creat-React-App は良い代替手段ですが、それでも私たちの問題は解決されませんでした。
偶然にも、JetpackはGithubのトレンドにちょうどヒットしました(JetpackはWebpackの周りの薄いラッパーであり、Webpackの設定のいずれかで拡張することができます) - それは私たちに完全にフィットします。基本的な最良の構成と、それを好きなように拡張する方法を提供してくれました。
Next.js関連のコードとその依存関係をすべて削除したので、ルーティングを処理する別の方法を見つけなければなりませんでした - 今回はRouter5 JSを選択しました。react-routerはReactアプリのためのコミュニティで好まれているルーティングパッケージですが、Router5はより成熟しており、安定しており、私たちの「懸念の分離」という設計原則に適合しているように思えました。
Router5 (React プラグイン付き) は、React からルーティングを分離し、遅延ロードのようなものを処理するためにコアの React 関数を使用することを可能にしてくれました。その方法は以下の通りです。
[Root page]
import React, { Suspense } from 'react';
import Loading from 'components/loading';
import TopBar from 'components/top_bar';
// Common
const NotFound = React.lazy(() => import('pages/404'));
//...
// HyperML
const HomePage = React.lazy(() => import('pages/hyperml_pages/home_page'));
const JobView = React.lazy(() => import('pages/hyperml_pages/job_view'));
//...
// AutoML
//...
class Root extends React.Component {
//...
getContent = () => {
switch (route) {
case 'home_page':
return <HomePage />;
case 'job':
return <JobView />;
//...
default:
return <NotFound />;
}
};
render() {
return (
<Layout className="layout">
<Layout.Header>
<TopBar />
</Layout.Header>
<Layout.Content>
<Suspense fallback={<Loading />}>
<div>{this.getContent()}</div>
</Suspense>
</Layout.Content>
<Layout.Footer>
Alibaba ©2019 Created by IMVL
</Layout.Footer>
</Layout>
);
}
}
//...
#誇大広告の先を見る
自分に合ったものを選ぶ
HyperMLはチーム初のReactアプリでしたが、それだけではありませんでした。
アプリの状態を管理するために他の可能性のある方法を探っていくうちに、正しい方法は一つではないことがわかりました。
しかし、1つだけはっきりしていたことは、サードパーティのパッケージを使用する必要がない限り、サードパーティのパッケージを使用しないことです。Reactのデフォルトの状態管理は、正しく使えば素晴らしいものですが、私たちはReduxを状態管理に使っている "古い "プロジェクトを書き換えているので、できるだけデフォルトに近い状態を維持しつつ、"Reduxのやり方 "を維持する方法を探し始めました。
このMediumの記事(新しいコンテキストAPIの上に構築された状態管理を実装する新しいライブラリ)を見つけ、そのスマートでわかりやすいソースコードを調べた後、"npm install react-waterfall -save "を選択しました。
最終的には、より管理しやすく、より明確なコードになり、おまけとして、Redux で使用しなければならなかったすべてのパッケージを手放すことができました。
[Jobs Store]
import { setData, mergeData } from './utils';
const initialState = {
jobs: {},
lastActivity: {},
};
function updateJob(state, callback, newJob) {
const { lastActivity } = state;
if (lastActivity[newJob.ID]) lastActivity[newJob.ID] = newJob;
return {
jobs: {
...state.jobs,
[newJob.ID]: newJob,
},
lastActivity,
};
}
function deleteJob(state, callback, jobID) {
const jobs = Object.assign({}, state.jobs);
const lastActivity = Object.assign({}, state.lastActivity);
delete jobs[jobID];
delete lastActivity[jobID];
return { jobs, lastActivity };
}
const actionsCreators = {
setLastActivity: setData('lastActivity'),
setJobs: setData('jobs'),
mergeJobs: mergeData('jobs'),
updateJob,
deleteJob,
};
export default { initialState, actionsCreators };
[Projects Store]
import { setData, mergeData, addData } from './utils';
const initialState = {
projects: {},
};
const actionsCreators = {
setProjects: setData('projects'),
mergeProjects: mergeData('projects'),
updateProject: addData('projects'),
};
export default { initialState, actionsCreators };
[Index Store]
import createStore from 'react-waterfall';
import mergeStores from './utils';
import jobs from './jobs';
import projects from './projects';
export const { Provider, connect, actions } = createStore(mergeStores([
jobs,
projects,
]));
[Connect to react]
//...
import { Provider as StoreProvider } from 'store';
import Root from 'pages';
const App = () => (
<StoreProvider>
<Root />
</StoreProvider>
);
[Use "action" anywhere]
//...
import { actions } from 'store';
actions.deleteJob(jobID);
[Connect to component]
//...
import { connect } from 'store';
const Activity = ({ lastActivity }) => (
<Card>
<JobsList jobs={lastActivity} />
</Card>
);
Activity.propTypes = {
lastActivity: PropTypes.arrayOf(PropTypes.shape()),
};
export default connect(({ lastActivity }) => ({ lastActivity }))(Activity);
#npm インストール時間
プロジェクトの依存関係をコントロールできるようになってからは、何を追加するかをより賢く選択できるようになりました。私たちは特定の目標を達成するためにこのリファクタリングを計画しました。
プロジェクトの依存関係を大幅に減らしたことで、開発の進捗の中で最も時間のかかる部分であるデザインに役立つ重要なライブラリを追加することができました。
CSSコードの記述とメンテナンスには驚くほどの時間がかかっていましたが、Ant-Designを使用することでHyperMLのUIが改善され、ユーザーの体験が向上しただけでなく、新機能の開発にかかる時間が大幅に短縮され、より良いコードを書くための時間を確保することができました。
#変化を受け入れる
React v16.8:Hooksを導入したもの
2019年2月にReact v16.8が "Hooks "を導入して登場しました。
"一晩でHooksを使うために既存のアプリケーションを書き換えることはお勧めしません。"と彼らはその日のうちに書いていました。
しかし、変化を受け入れることは私たちの文化の中にあります。現在のコードを改善していく中で、クラスのコンポーネントを関数に置き換えたり、新しいコンポーネントをHooksで書いたり、独自のHooksを構築したりするようになりました。
振り返ってみると、それは私たちがHooksをよく理解するのに役立ち、それをよく理解することで、私たちはコーディング時間を大幅に改善することができました。50個以上のコンポーネントを書き換えなければ、それは達成できませんでした。
#フロントエンドパフォーマンスソリューション
アプリのパフォーマンスを向上させるには、サーバー側のコードを修正する必要があるかもしれませんが、サーバーのバックログにタスクが多すぎる場合は、フロントエンドで解決策を考える必要があります。ここでは、サーバー側で変更を加えることなくアプリのパフォーマンスを向上させた2つの方法を紹介します。
####フロントエンドページング
アプリのページの一部にはジョブのリストが含まれており、これらのリストは大きくなることがあります。これらのリストは大きくなる可能性があります。広範なリストをレンダリングすることは、ほとんどのブラウザにとって複雑な作業であり、アプリのパフォーマンスだけでなく、ユーザーエクスペリエンスにも影響を与えます(ブラウザがリストのレンダリングを終えるまで真っ黒な白いページに直面する)。
この問題に対処するために、ページング機構を開発する必要があります。通常、これはサーバー側で行うものですが、主な問題はDOMにデータをレンダリングすることであり、サーバーからデータを取得することではないので、フロントエンドで開発することができます。
そのために、このパッケージでは react-infinite-scroller を使うことにしました。
[Jobs list]
//...
import InfiniteScroll from 'react-infinite-scroller';
import { connect } from 'store';
import JobItem from './item';
const JobsList = ({ jobs }) => {
const [numOfJobsToShow, setNumOfJobsToShow] = useState(10);
const jobsInView = useMemo(() => jobs.slice(0, numOfJobsToShow), [jobs, numOfJobsToShow]);
return (
<InfiniteScroll
pageStart={0}
loadMore={() => setNumOfJobsToShow(showingJobs => showingJobs + 10)}
hasMore={numOfJobsToShow < jobs.length}
loader={<Spin />}
>
{jobsInView.map(job => <JobItem job={job} />)}
</InfiniteScroll>
);
};
JobsList.propTypes = {
jobs: PropTypes.arrayOf(PropTypes.shape()),
};
####ローカルストレージに状態を保存
パフォーマンスとユーザーエクスペリエンスを向上させるもう一つの方法は、特に遅いインターネット接続がある場合に、ブラウザがデータをフェッチするのを「助ける」ことです。
私たちは、アプリのデータストアをブラウザのローカルストレージに保存することでこれを実現しました。この方法では、ユーザーは次回アプリにアクセスしたときに(サーバーからの)データを待つ必要がありません。
この方法には1つの欠点があります。それは、変更があるたびにローカルストレージにデータを書き込むことによるパフォーマンスへの影響です。これを克服するために、このタスクにはWeb Workerを使用して、バックグラウンドスレッドで実行するようにしています。
[Index Store]
import createStore from 'react-waterfall';
import mergeStores from './utils';
import jobs from './jobs';
import projects from './projects';
import { importLocalStore } from './localStore';
const localStorage = require('workerize-loader!./localStore')(); // ** Web Worker **
export const { Provider, connect, actions, subscribe } = createStore(mergeStores([
jobs,
projects,
]));
importLocalStore(actions);
subscribe((action, state) => localStorage.update(state));
[localStore.js]
// Runs on Web Worker
export function update(state) {
Object.keys(state).forEach(key => localStorage.setItem(key, state[key]));
}
// Runs on main thread
export function importLocalStore(actions) {
localStorage.getItem('jobs').then(jobs => actions.mergeJobs(jobs || {}));
localStorage.getItem('projects').then(projects => actions.mergeProjects(projects || {}));
}
####ボーナスのヒント
HyperMLに画像を追加することで、アプリがより使いやすくなり、作業ツールを楽しくするちょっとしたおまけを与えてくれることがわかりました。Undrawは定期的に更新される美しいSVG画像のオープンソースのコレクションで、完全に無料で帰属表示なしで使用することができます。
####ボーナスヒントII
ESlintは、より良いコードを書くことを強制することで、チームのdevスキルを向上させます。プロジェクトでの使用を検討してみてください。
#概要
数ヶ月前のアプリを書き換えるのは無駄に思えるかもしれませんが、コードのメンテナンス、新機能の追加、新しいプロジェクトの開発にかかる時間を考えれば、このリアクターが時間の適切な使い方であるだけでなく、必要な作業であることは明らかです。
私たちは、最初にいくつかの部分を書き換え、他の部分はマイナーチェンジを加えたままにして、少しずつ始めました。約2週間後には、完全に刷新された作業版のウェブアプリが完成しました。今では、このリファクタリングのおかげで、それ以降に開発した新機能ごとに何時間もの作業時間を節約できたと推定しています。
私たちの経験を共有し、時々リファクタリングをしてコードを更新することを奨励することは、私たちにとって不可欠でした。
例えば、コードベースが膨大で、長い間改訂が行われていない場合など、私たちの推奨を行うことが難しい場合があることを想定しています。しかし、その場合はもっと重要なことがあるかもしれません。
アリババクラウドは日本に2つのデータセンターを有し、世界で60を超えるアベラビリティーゾーンを有するアジア太平洋地域No.1(2019ガートナー)のクラウドインフラ事業者です。
アリババクラウドの詳細は、こちらからご覧ください。
アリババクラウドジャパン公式ページ