Hành trình của mình đi tìm chiếc structure hoàn hảo cho React (Phần 2)
Sau khi mình published chiếc blog đi tìm structure hoàn hảo cho ứng dụng React, mình rất vui vì các bạn đã đón nhận nó ❤️
Mình nhận được một số ý kiến, phản hồi của các bạn với những cách chia và sử dụng folder khác nhau:
Lúc đầu mình viết một phần thôi, nhưng sau khi tiếp thu ý kiến của các bạn thì thấy cũng có nhiều hướng hay ho để mình tổng hợp, nên mình viết tiếp phần 2.
Vì không thể cover hết được các usecase, ví dụ như có bạn đề cập đến grpc, bla bla... nên mình sẽ tạo một repository, tạm gọi là react-clean-structure để các bạn có thể contribute vào để cùng học hỏi nhé.
Phần 2 mình sẽ cài đặt redux, redux-saga trên structure hiện tại, implement alias để clean code, giúp các bạn tự tin hơn trong việc quyết định kiến trúc của mình.
Lưu ý:
Ở đây mình không đưa ra một chuẩn boilerplate, hay khẳng định đây là một structure hoàn hảo, mình chỉ kể lại hành trình, cách mình sử dụng thế mạnh của React, sử dụng thư viện xung quanh hệ sinh thái React, sử dụng công nghệ sẵn có để giải quyết vấn đề. Mình tin chắc đến với React các bạn cũng sẽ đối mặt với những vấn đề như vậy, chỉ có là cách giải quyết vấn đề, quyết định của các bạn tại thời điểm đó khác với mình.
Nếu vấn đề mình nêu ra bạn chưa gặp phải, hãy thử suy nghĩ cách bạn sẽ giải quyết như thế nào trong tình huống đề trước khi đọc tiếp solution của mềnh nha .
Cùng xem lại sản phẩm của phần 1
Tuỳ thuộc vào mục đích của ứng dụng, nhưng thành phần có trong hệ thống để chúng ta quyết định structure code như thế nào.
Thực sự thì nếu mà đưa ra một kiến trúc để cover được tất cả các usecase thì không biết nhét bao nhiêu vào cho đủ, nên mình đã suy nghĩ và tự đặt cho mình rule như sau:
Yes, một khi mình đã define ra một rules rồi thì trong quá trình phát triển, khi muốn thêm file hoặc thêm folder thì mình cứ base trên cái rule ban đầu thôi. Việc áp dụng rule cũng tương đối nha, nếu cứng nhắc quá cũng sẽ tự làm khó mình
Yah, như trường hợp ở trên, mình sẽ structure cùng cấp với src/pages vì tất cả code ở dưới pages sẽ đều sử dụng graphql, http request, helpers....
Sử dụng Alias
Giả sử mình có components src/pages/Feed/components/FeedItems.jsx
import function getLikes
tại src/api/index
bằng relative path sẽ như thế này:
Nhìn có vẻ đơn giản ha và gọn gàng hak. Nhưng tình chỉ còn đẹp khi tình dang dở, project chỉ còn đẹp khi ít code. Khi code một thời gian thì nó sẽ như này.
Haizz... Hãy nghĩ cái cái cảnh đến một ngày refactor file, hay có một tính năng tương tự muốn sử dụng file code này tại một đường dẫn file khác, sửa lại đống relative path kia chắc......
Vấn đề ở đây là bạn có nhìn thấy sự bất cập hay không, có bạn cảm như vậy là hết sức bình thường, nếu như bạn nghĩ nó bất cập, bạn sẽ tìm cách để resolve nó.
Yah, đó là việc mình sắp phải làm luôn. Thay vì sử dụng đường dẫn tương đối thì mình sẽ dùng đường dẫn tuyệt đối, như vậy khi bạn di chuyển file, hoặc copy code tại file này sử dụng ở những nơi khác thì nó vẫn work.
Mình sẽ sử dụng webpack alias để resolve problem này. Webpack cung cấp một option để mình có thể rút gọn đường dẫn và gán vào một giá trị. Ví dụ:
Đoạn code trên mình sẽ gán đường dẫn tuyệt đối của folder api vào biến #api. Viết tường minh ra thì sẽ như này:
{
"#api":"/Users/mylap/Desktop/my-app/src/api"
}
Sử dụng trong code:
// implement theo cách cũ:
import { getLikes } from "../../../api";
import { viewMore } from "../../../helpers";
import { Header, Button, Text } from "../../../components";
import firebase from "../../../firebase";
//Sử dụng alias:
import { getLikes } from "#api";
import { viewMore } from "#helpers";
import { Header, Button, Text } from "#components";
import firebase from "#services/firebase";
Trong quá trình resolve module, khi gặp syntax import có prefix math với alias, webpack sẽ thay thế alias đó thằng giá trị khởi tạo trong config của webpack.
// nếu có một alias được implement trong webpack config:
{
resolve:{
alias:{
"#api": path.join(rootPath, "api/"),
}
}
}
// thì:
import { getLikes } from "#api";
// tương đương với
import { getLikes } from "/Users/mylap/Desktop/my-app/src/api";
Mọi dòng code nằm trong scope build của webpack đều được apply alias này. Giả sử như càng ngày mình di chuyển cái file này, hoặc mình copy code để sử dụng ở một chỗ khác thì nó vẫn work như bình thường.
Sử dụng Create-react-app các bạn chạy command yarn eject để bảo CRA chìa ra config để custom, hoặc có một giải pháp an toàn hơn để custom lại config của CRA là sử dụng react-app-rewired.
Dùng alias sẽ giúp chúng ta tự tin hơn trong việc structure project, giảm thời gian implement, refactor code.
Implement redux,redux-saga
Với những ứng dụng lớn, state phức tạp, cần lưu state khi navigate, share state giữa các page thì cần một global state management có lẽ là hiển nhiên. Trong trường hợp này, mình sẽ chọn redux.
Không phải data nào mình cũng đẩy lên redux, tuỳ thuộc vào data của các bạn nữa, mình dùng redux để
Khi data share giữa các pages
Global state (authentication, user,config ...)
data tốn nhiều thời gian để fetch, và ít thay đổi theo thời gian
Cache để tối ưu user experience
Những trường hợp component của một page cần sử dụng state của components cha, thì mình sẽ implement React Context API.
Khi implement redux mình giữ trong đầu 2 rule, global state mình sẽ để tại folder setup redux, còn những state share giữa các page mình sẽ để nó đi theo page luôn.
Với những reducer thuộc về từng pages, các nhân mình thích đưa về tại pages đó để xử lý hơn, lý do:
Flow làm việc của bạn sẽ trôi chạy hơn và nhanh hơn vì mọi thứ đều nằm tại page.
Project structure vừa có thể scale theo chiều ngang hoặc chiều dọc.
Lazy load reducer
Để làm được như vậy, mình cũng phải ra một rule để dể quản lý và tránh việc components nào cũng có reducer đó là: Chỉ duy nhất components cha mới có reducer. Có nghĩa là reducer chỉ có tại src/store/reducers và src/pages/{PageName}/reducer.js
.
Implement tại cấp reducer và combine lại như sau:
Dùng redux-dev-tool để debug sẽ thấy reducer belike:
Nãy mình có nói là chỉ tại cấp pages mới có reducer thôi, như vậy nếu những pages lớn muốn nest reducer thì sao?
Giả sử mình implement lại reducer Feed/reducer.js như sau:
Lúc này cây reducer sẽ như này:
Bugs phần 1:
Phần trước mình có đề cập đên việc viết scrip để auto load routes:
let routes=[]
const context = require.context(".",true,/route.js$/);
context.keys().forEach((path)=>{
- routes.push(require(`${path}`).default);
+ routes.push(context(`${path}`).default);
})
export default routes
Nay mình có debugs thì thấy không lazy load được components theo route, mình nghĩ nguyên nhân do hàm require()
là một hàm sync, nên nó sẽ import tất cả component trong lúc build time luôn, nên hàm lazy(()=>import())
ở đây không có nghĩa nữa.
Nên mình sẽ import lại manual như sau:
How about?
Thay vì apply tất cả reducer tại root, thì mình chỉ apply global reducer,saga . Sau đó những reducer,saga đi theo page sẽ được bundle và gọi khi page đó được render ra, việc này giúp thời gian tải trang ban đầu sẽ nhanh hơn.
Inject saga function:
function createSagaInjector(runSaga, rootSaga) {
const injectedSagas = new Map();
const isInjected = (key)=> injectedSagas.has(key);
const injectSaga = (key, saga) => {
if (isInjected(key)) return;
console.log("saga", key, saga);
const task = runSaga(saga);
injectedSagas.set(key, task);
};
injectSaga("root", rootSaga);
return injectSaga;
}
Inject Reducer:
const store = createStore(
createReducer(),
{},
compose(
applyMiddleware(sagaMiddleware),
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
)
);
store.asyncReducers = {};
store.injectReducer = (key, reducer) => {
store.asyncReducers[key] = reducer;
store.replaceReducer(createReducer(store.asyncReducers));
return store;
};
Lúc này mình sẽ implement lại file pages/routes.js
Trước khi tải code logic của page và compile, mình sẽ tải script để init reducer, saga trước. Đảm bảo rằng khi script của component được tải xuống execute thì reducer, saga đã có sẵn để handle.
Lúc này khi vào pages sẽ có 3 script được tải xuống:
2.chunk.js ( Feed/reducer.js
)
3.chunk.js (Feed/saga.js
)
1.chunk.js (Feed/index.js
)
Kết bài
Đây chưa phải là điểm dừng, với những dự án phức tạp hơn, share code giữa các ứng dụng, giữa các platform (React-Native, React-Native-Web) thì kiến trúc hiện tại đối với mình là không dùng được. Cần phải apply công nghệ khác như lerna, yarn workspace (mono repo), hay kết hợp webpack + rollup để vừa build app + build lib.
Điều quan trọng ở đây là bạn phải nắm rõ cái mình đang làm, hiểu rõ được công nghệ, hệ sinh thái từ đó suy nghĩ và build ra cho mình một kiến trúc ngon lành ??