Gần đây, React boilerplate ver 3 vừa mới được phát hành bởi Max Stoiber, đây là một trong những React starter kit nổi tiếng nhất. Sau đây là một số kinh nghiệm hay ho được tác giả truyền lại cho các dev khi dùng react.
Trước đây, quy mô là điều mà hệ thống ở back-end quan tâm nhiều hơn cả. Vì nếu số lượng người dùng càng ngày càng tăng, bạn phải thêm server, rồi phải đảm bảo cơ sở dữ liệu có thể được kết nối từ nhiều server, ...
Ngày nay, do webapp càng ngày càng nhiều và phức tạp, việc mở rộng quy mô trở nên quan trọng đối với cả front-end nữa. Front-end của một app phức tạp giờ phải cân thêm lưu lượng người dùng, nhiều developer và các thành phần của nó nữa.
Container và Component
Đầu tiên, muốn cải tiến một app quy mô lớn thì phải phân biệt giữa stateful ("container") component và stateless ("component") component. Container quản lí data hoặc được kết nối đến state và thường ko có style đi kèm. Còn component thì thường có style đi kèm và không quản lí data hoặc state. Về cơ bản, container chịu trách nhiệm "mọi thứ làm việc như thế nào", và component quản lí "mọi thứ sẽ trông ra sao".
Chia component sẽ tách biệt giữa các component và các lớp trong quản lí dữ liệu. Nhờ vậy, bạn có thể thoải mái chỉnh sửa component mà không lo cấu trúc dữ liệu bị thay đổi và cũng có thể chỉnh sửa container mà không sợ style thay đổi. Phân định rõ ràng giữa container và component khiến mọi việc trở nên rõ ràng hơn.
Cấu trúc
Trước đây, developers cấu trúc React theo type, có nghĩa là chia theo folders như là actions/
, components/
, containers/
, ...
Giả sử thanh navigation được đặt là NavBar
cộng một vài state đi kèm và một toggleNav
để đóng và mở. Cấu trúc file sẽ như sau:
react-app-by-type
├── css
├── actions
│ └── NavBarActions.js
├── containers
│ └── NavBar.jsx
├── constants
│ └── NavBarConstants.js
├── components
│ └── App.jsx
└── reducers
└── NavBarReducer.js
Ví dụ như trên thì không vấn đề gì, nhưng nếu bạn có hàng trăm hoặc có thể hàng nghìn component, việc phát triển sẽ rất phức tạp. Để thêm một tính năng, bạn sẽ phải tìm đúng file trong hàng đống thư mục với hàng trăm file khác.
Giải pháp tốt nhất hiện nay là thay vì nhóm file theo type, hãy nhóm chúng theo tính năng(feature)! Tức là nhóm tất cả file liên quan đến một chức năng (ví dụ navigation bar) vào cùng một thư mục.
Chúng ta xem lại cấu trúc file sau khi nhóm vào thư mục NavBar
:
react-app-by-feature
├── css
├── containers
│ └── NavBar
│ ├── NavBar.jsx
│ ├── actions.js
│ ├── constants.js
│ └── reducer.js
└── components
└── App.jsx
Developer chỉ việc vào một thư mục khi muốn làm việc với tính năng nào đó, và cũng chỉ cần tạo một thư mục khi thêm tính năng mới. Đổi tên thì còn dễ dàng hơn, chỉ cần tìm thư mục và thay tên mới; nhiều developers có thể làm việc cùng lúc trong một app mà không gây lỗi conflict.
Cần chú ý là điều đó không có nghĩa là action và reducer của redux chỉ có thể sử dụng trong một component. Chúng có thể được import và được sử dụng từ những component khác.
Hai câu hỏi có lẽ bạn sẽ đặt ra trong quá trình làm việc này là "Xử lí style như thế nào?" và "Lấy data (data-fetching) ra sao?".
Xử lý Style
Ngoài quyết định về cấu trúc, làm việc với CSS trong cấu trúc xây dựng theo component (component-based architecture) không phải đơn giản do hai đặc tính cơ bản của CSS: tên (name global) và tính kế thừa (inheritance).
Tên class không lặp lại
Thử tưởng tượng trong một app rất lớn có css như sau:
.header { /* … */ }
.title {
background-color: yellow;
}
Bạn sẽ thấy ngay là title
rất chung chung. Một dev khác (hoặc chính bản thân dev đó vào một thời điểm khác) sẽ viết code như sau chẳng hạn:
.footer { /* … */ }
.title {
border-color: blue;
}
Thế là sinh ra conflict tên, và dĩ nhiên title của bạn có border xanh và background vàng khắp nơi, và bạn sẽ phải tìm trong hàng nghìn file xem chỗ nào đã khởi tạo style kia.
Rất may là một trong những giải pháp đơn giản là sử dụng CSS Modules. Cách xử lí là thêm một file style.css vào folder của mỗi component
react-app-with-css-modules
├── containers
└── components
└── Button
├── Button.jsx
└── styles.css
Các file CSS trông sẽ như nhau, và giờ bạn không phải quan tâm là đặt tên như thế nào nữa mà có thể đặt tên chung chung như thế này:
.button {
/* … */
}
Sau đó require
(hoặc import
) các file CSS cùng component và đặt className
của styles.button
vào thẻ JSX:
/* Button.jsx */
var styles = require('./styles.css');
<div className={styles.button}></div>
Nếu bạn nhìn vào DOM của browser, bạn sẽ thấy <div class="MyApp__button__1co1k"></div>
! CSS Modules xử lý việc đặt tên class cho bạn bằng việc gắn thêm một đoạn hash ngắn chứa nội dung của class. Điều này có nghĩa là tỉ lệ lặp lại tên class hầu như là bằng 0, nếu nó lặp lại có nghĩa nội dung của các class đó chắc chắn là như nhau (vì hash lấy từ nội dung như nhau mà).
Reset thuộc tính cho mỗi component
Trong CSS. một số thuộc tính nhất định sẽ thừa hưởng xuyên suốt các node. Ví dụ, nếu node cha (parent node) có line-height
và node con không đặt lại thuộc tính đó thì sẽ được mặc định thừa hưởng line-height
từ node cha.
Tưởng tượng Header
component và Footer
component có style :
.header {
line-height: 1.5em;
/* … */
}
.footer {
line-height: 1;
/* … */
}
Nếu chúng ta render Button
bên trong hai component này, và đột nhiên button ở header sẽ khác button ở footer. Khoảng một tá thuộc tính CSS khác cũng được kế thừa như trên, và việc tìm được và chỉnh lại style sẽ rất khó.
Trong thế giới front-end, sử dụng reset style để đặt lại style về mặc định xuyên suốt browser là chuyện cơm bữa. Lựa chọn phổ biến là Reset CSS, Normalize.css và Sanitize.css! Thế nếu áp dụng ý tưởng đó vào component ?
Có một plugin cho việc auto-reset gọi là PostCSS! Nếu bạn thêm PostCSS Auto Reset cho PostCSS plugin, nó sẽ làm việc: bọc quanh local và reset style cho mỗi component, cài đặt tất cả các thuộc tính bên ngoài về mặc định và bỏ tính kế thừa.
Data-Fetching
Vấn đề thứ hai cần giải quyết là data-fetching. Việc đặt chung các action với các component liên quan vào nhau nghe có vẻ hợp lí, nhưng data-fetching là một global action và không thể gắn vào một component duy nhất.
Hầu hết các developers hiện nay sử dụng Redux Thunk để xử lý data-fetching với redux. Một action khi được "thunk" sẽ trông như sau:
/* actions.js */
function fetchData() {
return function thunk(dispatch) {
// Load something asynchronously.
fetch('https://someurl.com/somendpoint', function callback(data) {
// Add the data to the store.
dispatch(dataLoaded(data));
});
}
}
Đây là một ý tưởng tuyệt vời cho phép data-fetching từ actions, nhưng nó có hai điểm yếu: việc test function sẽ rất khó, và về mặt ý tưởng, data-fetching trong actions có vẻ không hợp lý.
Lợi ích lớn nhất của Redux là một cái máy tạo action thuần, rất dễ test. Khi trả lại một thunk từ một action, bạn sẽ phải gọi đúp action đó, thử dispatch
function, ...
Gần đây, một cách tiếp cận mới làm khuấy động thế giới React: react-saga. React-saga tận dụng Esnext generator functions để làm asynchronous code trông giống synchronous code, và làm asynchronous code dễ test hơn. Ý tưởng đằng sau là chia thread trong ứng dụng của bạn để xử lý asynchronous code mà không động đến phần còn lại của code.
Ví dụ như:
/* sagas.js */
import { call, take, put } from 'redux-saga/effects';
// The asterisk behind the function keyword tells us that this is a generator.
function* fetchData() {
// The yield keyword means that we'll wait until the (asynchronous) function
// after it completes.
// In this case, we wait until the FETCH_DATA action happens.
yield take(FETCH_DATA);
// We then fetch the data from the server, again waiting for it with yield
// before continuing.
var data = yield call(fetch, 'https://someurl.com/someendpoint');
// When the data has finished loading, we dispatch the dataLoaded action.
put(dataLoaded(data));
}
Source code trên trông đẹp hơn hẳn, tránh hoàn toàn callback hell, và trên hết là ưu điểm cực kì dễ test. Giờ bạn sẽ hỏi sao lại dễ test? Lý do sẽ liên quan đến khả năng test "những ảnh hưởng" redux-saga exports mà không cần chúng hoàn chỉnh.
Những ảnh hưởng này chúng ta import trên cùng của file là công cụ để xử lý giúp chúng ta tiếp cận dễ dàng với redux code:
put()
dispatch một action từ saga.take()
tạm dừng saga cho đến khi một action xảy ra.select()
nhận được một phần của redux state (ví dụ nhưmapStateToProps
).call()
gọi function truyền vào là tham số đầu tiên với những tham số còn lại.
Tại sao những ảnh hưởng này lại có ich? Chúng ta hãy xem khi test ví dụ trên sẽ trông như thế này:
/* sagas.test.js */
var sagaGenerator = fetchData();
describe('fetchData saga', function() {
// Test that our saga starts when an action is dispatched,
// without having to simulate that the dispatch actually happened!
it('should wait for the FETCH_DATA action', function() {
expect(sagaGenerator.next()).to.equal(take(FETCH_DATA));
});
// Test that our saga calls fetch with a specific URL,
// without having to mock fetch or use the API or be connected to a network!
it('should fetch the data from the server', function() {
expect(sagaGenerator.next()).to.equal(call(fetch, 'https://someurl.com/someendpoint'));
});
// Test that our saga dispatches an action,
// without having to have the main application running!
it('should dispatch the dataLoaded action when the data has loaded', function() {
expect(sagaGenerator.next()).to.equal(put(dataLoaded()));
});
});
Esnext generator (từ phải qua trái) sẽ không chạy quá từ khóa yield
cho đến khi generator.next()
được gọi, sau đó nó chạy function còn lại, cho đến khi nó lại gặp yield
tiếp theo ! Bằng việc sử dụng hiệu ứng redux-saga, chúng ta có thể test asynchronous dễ dàng mà không cần phụ thuộc vào test network.
Chúng ta cho các test file và các file cần test vào cùng một thư mục để đảm bảo tất cả các file liên quan đến một component sẽ ở trong cùng một folder, kể cả khi test.
Nếu bạn nghĩ chỉ được lợi có từng đó khi sử dụng redux-saga thì bạn đã nhầm! Thực chất, làm data-fetching dễ dàng hơn và dễ test hơn chỉ là lợi ích nhỏ nhất từ việc sử dụng redux-saga.
Sử dụng Redux-Saga như một "liên kết"
Các components giờ được tách biệt. Chúng không liên quan đến style hoặc logic khác.
Giả sử một Clock
và một Timer
component, khi một button trên Clock được ấn, chúng ta muốn chạy Timer, và khi button stop trên timer được ấn, chúng ta muốn hiển thị thời gian trên Clock.
Thông thường, bạn sẽ làm như thế này:
/* Clock.jsx */
import { startTimer } from '../Timer/actions';
class Clock extends React.Component {
render() {
return (
/* … */
<button onClick={this.props.dispatch(startTimer())} />
/* … */
);
}
}
/* Timer.jsx */
import { showTime } from '../Clock/actions';
class Timer extends React.Component {
render() {
return (
/* … */
<button onClick={this.props.dispatch(showTime(currentTime))} />
/* … */
);
}
}
Giờ thì bạn không thể dùng hai component này độc lập với nhau và việc tái sử dụng chúng gần như là không thể!
Thay vào đó, chúng ta có thể sử dụng redux-saga như "liên kết" giữa hai component này. Bằng việc chờ action cụ thể, chúng ta có thể xử lý theo nhiều cách khác nhau, (tuỳ app) và những component trên giờ có thể tái sử dụng được.
Thử sửa lại code như sau:
/* Clock.jsx */
import { startButtonClicked } from '../Clock/actions';
class Clock extends React.Component {
/* … */
<button onClick={this.props.dispatch(startButtonClicked())} />
/* … */
}
/* Timer.jsx */
import { stopButtonClicked } from '../Timer/actions';
class Timer extends React.Component {
/* … */
<button onClick={this.props.dispatch(stopButtonClicked(currentTime))} />
/* … */
}
Bạn có nhận thấy mỗi component chỉ chạy riêng rẽ và import action của riêng nó thôi không?
Giờ thì sử dụng saga để nối chúng vào nhau.
/* sagas.js */
import { call, take, put, select } from 'redux-saga/effects';
import { showTime } from '../Clock/actions';
import { START_BUTTON_CLICKED } from '../Clock/constants';
import { startTimer } from '../Timer/actions';
import { STOP_BUTTON_CLICKED } from '../Timer/constants';
function* clockAndTimer() {
// Wait for the startButtonClicked action of the Clock
// to be dispatched.
yield take(START_BUTTON_CLICKED);
// When that happens, start the timer.
put(startTimer());
// Then, wait for the stopButtonClick action of the Timer
// to be dispatched.
yield take(STOP_BUTTON_CLICKED);
// Get the current time of the timer from the global state.
var currentTime = select(function (state) { return state.timer.currentTime });
// And show the time on the clock.
put(showTime(currentTime));
}
Đẹp hơn đúng không?
Tóm tắt
Bạn nên nhớ những key chính sau:
- Phân biệt giữa container và component
- Cấu trúc file theo tính năng
- Dùng CSS module và PostCSS Auto Reset
- Dùng redux-saga để
- luồng xử lý async dễ đọc và dễ test hơn.
- gắn các component độc lập lại với nhau.
Bài viết được dịch lại từ nguồn : https://www.smashingmagazine.com/2016/09/how-to-scale-react-applications?utm_source=mybridge&utm_medium=email&utm_campaign=read_more
Bình luận