Nhảy tới nội dung

· Một phút để đọc
ManhPT

Vấn đề là...

Trong quá trình code chắc hẳn bạn thường xuyên tạo ra những dumb commit để tránh mất code, hoặc nhiều commit trùng tên nhau. Bạn có bao giờ nghĩ đến việc gộp các commit (squash commit) đó lại thành một commit có ý nghĩa hơn chưa?

Squash commit (gộp commit) là một việc nên làm nhưng không phải ai cũng nghĩ đến. Thậm chí có nghĩ đến thì đa số cũng không muốn làm bởi sợ rằng có thể gây ra mất code. Xin được khẳng định luôn là code một khi đã được commit thì không mất đi đâu được, vô tư đi.

Để squash commit, chúng ta sẽ sử dụng câu lệnh git rebase -i hoặc git rebase --interactive. Trong nội dung bài viết này tôi sẽ không giải thích về rebase. Các bạn có thể tham khảo thêm các bài viết sau để hiểu rõ ràng hơn:

  1. https://www.atlassian.com/git/tutorials/rewriting-history/git-rebase
  2. https://www.atlassian.com/git/tutorials/merging-vs-rebasing

Bài viết này nằm trong series về Git Workflow. Các bạn có thể tham khảo bài viết giới thiệu tại đây.

Trước khi squash

  • Bạn nên kiểm tra lại lịch sử của mình trước để xác nhận lại lần cuối những commit mà mình cần squash.
  • Copy hash id của commit nằm ngay trước những commit cần squash

Ví dụ trong ảnh trên, chúng ta có tới 4 commit giống nhau, cùng là update readme.md. 4 commit này làm chung một nhiệm vụ là update readme nên có thể gộp lại thành 1 commit, tránh tạo rác trong git history. Để squash được bạn cần có hash id của commit nằm trước tất cả các commit cần squash, VD trong ảnh sẽ là dbd01be2.

Bắt đầu squash commit

Tổng hợp các commit

  • $ git rebase -i dbd01be2 vào terminal tại root của project.
  • Màn hình VIM editor sẽ hiện lên như hình sau, đôi khi vim không phải editor mặc định của git mà có thể là nano hay một editor nào đó khác do bạn thiết lập sẵn hoặc mặc định của hệ điều hành mà bạn dùng. Không cần quá lo lắng, mục đích chỉ là edit nội dung và cũng chỉ toàn là text mà thôi.

Trong màn hình này, bạn sẽ nhìn thấy 4 commit mà bạn chọn để squash sẽ nằm ở trên cùng, mỗi commit nằm trên 1 dòng với nội dung gồm 3 phần: pick 7424f7c update readme.md

  1. pick là command, mặc định pick là giữ lại commit đó sau quá trình rebase và không thay đổi gì.
  2. 7424f7c là hash id của commit
  3. update readme.md là commit message

Để ý một chút bạn sẽ thấy các hướng dẫn rebase ở phía bên dưới (nằm trong vùng comment) nói chi tiết hơn về các action mà bạn có thể sử dụng khi rebase, hiện tại chúng ta chỉ quan tâm đến action squash mà thôi. Nếu bạn rebase lần đầu thì hãy đọc kỹ để hiểu rõ hơn về rebase.

Đổi action của các commit cần squash

Tiếp tục squash:

  • Nhấn i để có thể edit content
  • Trừ commit trên cùng, sửa command của 3 commit sau từ pick thành squash

Chú ý: Ở màn hình này bạn cần đặc biệt cẩn thận với việc edit thông tin. Squash luôn là gộp với commit trước đó nên commit đầu tiên không thể là squash. VD như màn hình trên thì 3 commit sau sẽ được gộp vào commit đầu tiên và tạo ra 1 commit mới. Đừng xóa bất cứ dòng nào không phải commit #. Nếu bạn xóa 1 dòng thì commit tại dòng đó sẽ bị mất.

  • Nhấn Esc để thoải khỏi INSERT mode
  • :wq để write nội dung và quit ra khỏi VIM editor

Cập nhật commit message của commit mới (commit sau khi gộp)

Bạn tiếp tục được chuyển tới một màn hình VIM editor mới. Nhiệm vụ lần này là để update commit message cho commit mới - là kết quả gộp của 4 commit trước đó. Git đơn giản tổng hợp lại 4 commit message trước đó để bạn có thể chọn hoặc sửa để thay thế mới.

  • Xóa bớt và chỉ giữ lại những gì bạn thấy cần.
  • Comment có thể bỏ qua vì sẽ không được đưa vào commit message thực tế
  • :wq để write và quit

Kết quả trên command line sẽ là:

Sau khi squash commit

Lịch sử git mới mà bạn nhận được sẽ là:

Có thể thấy commit được gộp lại là commit mới hoàn toàn vì có hash khác. Trong trường hợp bạn muốn undo việc squash, chỉ cần kiểm tra với git reflog:

Ở bên trái bạn sẽ thấy các commit hash id cũ. Để undo bạn chỉ cần copy hash id của commit cuối cùng (trong 4 commit được gộp là 71e40e0) sau đó nhập vào terminal: $ git reset --hard 71e40e0 Kiểm tra lịch sử, mọi thứ như chưa từng xảy ra. Tuyệt cmn vời!

Bonus

Sau thời điểm reset bạn có muốn thử kiểm tra git reflog xem điều gì đã xảy ra không? Magic... :))

· Một phút để đọc
ManhPT

Giới thiệu Gitlab và Gitlab Flow

Gitlab là một công cụ rất hay và có self-hosted (on-premise) plan cho phép bất cứ ai, công ty hay tổ chức nào cũng có thể cài đặt một Git Platform của riêng mình.

  • Một điểm cộng của Gitlab đó là tính năng Gitlab Board, giúp bạn tổ chức và sắp xếp các issue thành các board giống như Trello, khá tiện lợi cho việc quản lý theo quy trình (VD: Agile).
  • Gitlab còn cho phép bạn tạo các Merge Request (Pull Request, theo cách nói của Github) dựa trên các issue đã có, đồng thời tạo luôn cả source branch giúp bạn.
  • Bên cạnh đó, Gitlab cung cấp Gitlab CI cho phép bạn apply CI/CD vào bất cứ project nào. Với tôi thì đây là một tính năng không thể thiếu khi lựa chọn một công cụ devops.

Trước khi bắt đầu thực hành hoặc đọc tiếp, bạn nên tạo một repo trống để thử nghiệm.

Tại sao cần Gitlab Flow

Thực tế, gitlab flow hay git workflow không phải một khái niệm mới. Do git không hề dễ học cho người mới nên các workflow của nó thường bị bỏ qua, ngoài ra các nhà cung cấp công cụ devops cũng thường đưa ra những git workflow riêng để phù hợp với luồng devops trên công cụ của họ. Bạn có thể tham khảo thêm:

Bắt đầu tạo một issue

Vào Issues > List và chọn New issue. Hoặc bạn có thể chọn Import CSV để import list task có sẵn từ các nguồn khác như Jira, Redmine...

Click New issue như hình minh họa bên dưới.

Điền đầy đủ thông tin tùy theo yêu cầu từng team sau đó Submit issue.

Tạo merge request từ issue

Sau khi đã có issue, ta có thể tạo merge request từ issue đó rất nhanh chỉ cần click Create merge request.

Bạn có thể tạo bao nhiêu merge request tùy ý nhưng thường chỉ nên tạo 1 merge request cho mỗi 1 issue.

Branch đã được tạo sẵn

Sau khi tạo merge request thì Gitlab cũng tạo luôn branch cho bạn.

Sau đó chọn copy câu lệnh checkout mà Gitlab cung cấp sẵn, paste vào terminal/shell của bạn (sau khi đã clone repo) để bắt đầu code.

Đây là các step đơn giản đầu tiên để làm việc với Gitlab Flow. Mọi task như tạo feature mới, fix bug, refactor... đều cần được quy hoạch về step cơ bản này.

· Một phút để đọc
ManhPT

Chờ chút?

Nếu bạn chưa biết hoặc chưa thực hành redux-saga thì trước hết hãy vượt qua bài hướng dẫn cơ bản và hiểu được các concept cơ bản. Tham khảo:

  1. https://dev.to/bnorbertjs/async-react-basics-with-redux-thunk–redux-saga-4af7
  2. https://blog.logrocket.com/understanding-redux-saga-from-action-creators-to-sagas-2587298b5e71
  3. https://medium.com/@lavitr01051977/make-your-first-call-to-api-using-redux-saga-15aa995df5b6
  4. https://medium.com/@js_tut/the-saga-continues-magic-in-react-44da8d134285

Vấn đề đang gặp phải

Ví dụ kinh điển

Bạn có một button submit form và muốn button được disable hoặc hiển thị trạng thái loading/processing trong khi chờ kết quả trả về, chỉ khi nhận được kết quả thành công hoặc có lỗi mới đưa button trở lại thái ban đầu.

Cụ thể

Bạn cho phép user hủy đơn hàng khi click button như hình sau:

Hủy đơn hàng

Nhưng bạn muốn button [Hủy đơn hàng] hiển thị trạng thái đang xử lý và không nhận lệnh trong khi đang chờ kết quả trả về:

Hủy đơn hàng loading

Để làm được điều này, chúng ta chỉ có một lựa chọn đó là sử dụng redux store để chứa state isCancelingOrder và trong component sẽ phải select isCancelingOrder từ redux store với mapStateToProps() sau đó đưa state này vào button [Hủy đơn hàng] để hiển thị icon loading.

Vấn đề bắt đầu xuất hiện, để hiển thị icon loading, tốt nhất là chỉ nên sử dụng local state của component để bật tắt nó. Nhưng thực tế chúng ta lại phải đẩy trạng thái xử lý của API request vào redux store rồi từ component phải connect đến store để lấy thông tin, rồi truyền thông tin đó qua một vài cấp component để biết được API request đã xong hay chưa. Vấn đề này tạm gọi local stateglobal state.

Xử lý async action với redux-saga

Mỗi khi button được click, component sẽ dispatch() một action gửi request lên server để kiểm tra tình trạng đơn hàng và thực hiện việc hủy đơn, khi bắt đầu gửi request isCancelingOrder sẽ được set thành true và chỉ được set thành false khi có kết quả trả về hoặc có lỗi (bao gồm cả lỗi từ server trả về).

Đoạn code sau đây cho phép thực hiện yêu cầu trên trong trường hợp API hủy đơn không bao giờ trả về lỗi (chỉ trả về 2xx response).

Component
const orderId = "abc-xyz";
this.props.dispatch(cancelOrder({ orderId }));

function cancelOrder(payload) {
return {
type: "CANCEL_ORDER",
payload,
};
}
Saga
export function* takeCancelOrder({ payload: { orderId } }) {
yield put({ type: "CANCEL_ORDER_BEGIN" });
const response = yield call(request, "/huy-don", { orderId });
yield put({ type: "CANCEL_ORDER_SUCCESS", response });
}

// Individual exports for testing
export default function* rootSaga() {
yield takeLatest("CANCEL_ORDER", takeCancelOrder);
}
Reducer
const initialState = {
isCancelingOrder: false,
};

function orderReducer(state = initialState, action) {
switch (action.type) {
case "CANCEL_ORDER_BEGIN":
return Object.assign({}, state, { isCancelingOrder: true });
case "CANCEL_ORDER_SUCCESS":
return Object.assign({}, state, { isCancelingOrder: false });
default:
return state;
}
}

Nhưng đời không như mơ, đơn hàng bạn đặt đã được đóng gói và sắp được giao đến tay bạn rồi thì bạn không thể hủy được nữa, cuộc sống mà. Trong trường hợp này chúng ta sẽ phải update saga một chút để có thể xử lý trường hợp phát sinh lỗi. Cách nhanh nhất là sử dụng try catch finally.

export function* takeCancelOrder({ payload: { orderId } }) {
try {
yield put({ type: "CANCEL_ORDER_BEGIN" });
const response = yield call(request, "/huy-don", { orderId });
yield put({ type: "CANCEL_ORDER_SUCCESS", response });
} catch (error) {
yield put({ type: "CANCEL_ORDER_ERROR", error });
} finally {
yield put({ type: "CANCEL_ORDER_FINISH" });
}
}

Đồng thời cũng cần update reducer:

const initialState = {
error: null,
isCancelingOrder: false,
};

function orderReducer(state = initialState, action) {
switch (action.type) {
case "CANCEL_ORDER_BEGIN":
return Object.assign({}, state, { isCancelingOrder: true });
case "CANCEL_ORDER_ERROR":
return Object.assign({}, state, { error: action.error });
case "CANCEL_ORDER_FINISH":
return Object.assign({}, state, { isCancelingOrder: false });
default:
return state;
}
}

Có lẽ bạn nghĩ đến đây là hết, chúng ta đã trải phẳng việc xử lý async actions bằng saga và try catch. Nhưng sự thực nó không dừng lại ở đó. Trong một dự án có rất nhiều loại action để xử lý các công việc (side effect) khác nhau: localStorage, cookie, animation…

Với mỗi một action như vậy chúng lại sẽ phải lặp đi lặp lại các bước đơn giản hoặc viết đi viết lại những đoạn code như bên trên? Thay vì callback hell thì chúng ta lại tạo ra một địa ngục mới saga hell. Nếu project của bạn có sử dụng các công cụ kiểm soát chất lượng code như sonarqube hay codeclimate thì điểm chất lượng sẽ tụt xuống thậm tệ vì code lặp quá nhiều.

saga hell chưa phải là điểm dừng. Với cách xử lý state như trên react app còn bị phụ thuộc và redux store, mọi state sinh ra bởi các action đều cần được đưa vào store để có thể tác động trở lại view component. Thật sự là cồng kềnh và khó kiểm soát.

Hạn chế sự phụ thuộc vào redux store

Giải pháp với redux-saga

Cách giải quyết rất là đơn giản: làm thế nào để trả về kết quả từ saga vào component mà không cần phải đặt nó vào redux store? =)))

Ai đó: Fuck, man! Giải pháp của ông chỉ là mô tả lại vấn đề thôi à? Tôi: Bình tĩnh! Ai đó: Cho xem code đi. Tôi: Đây, bắt đầu bằng unfoldSaga nhé.

/**
* Common saga handler
* Unify handling saga into only one standard form
*
* @param payload { handler, key }
* @param callbacks { callbackOnBegin, callbackOnFailure, callbackOnFinish, callbackOnSuccess }
*/
function* unfoldSaga(
{ handler, key } = {},
{
callbackOnBegin,
callbackOnFailure,
callbackOnFinish,
callbackOnSuccess,
} = {}
) {
try {
yield put({ type: createActionTypeOnStart(key) });
yield call(callbackOnBegin);
const data = yield call(handler);
yield put({ type: createActionTypeOnSuccess(key), payload: data });
yield call(callbackOnSuccess, data);
} catch (error) {
yield put({ type: createActionTypeOnFailure(key), payload: error });
if (process.env.NODE_ENV !== "production") {
/* eslint-disable no-console */
yield call(console.log, `Error at ${key} action`);
yield call(console.log, error);
/* eslint-enable */
}
yield call(callbackOnFailure, error);
} finally {
yield put({ type: createActionTypeOnFinish(key) });
yield call(callbackOnFinish);
}
}

/**
* Action type generators
*/
export function createActionTypeOnStart(key) {
return `${key}_BEGAN`;
}

export function createActionTypeOnFailure(key) {
return `${key}_FAILED`;
}

export function createActionTypeOnFinish(key) {
return `${key}_FINISHED`;
}

export function createActionTypeOnSuccess(key) {
return `${key}_SUCCEEDED`;
}

Giải thích

Với saga trung gian mà tôi gọi là unfoldSaga cho nó hoành tráng, chúng ta có thể thống nhất việc xử lý action thông qua một helper duy nhất đồng thời cũng tạo ra một form chung cho các action type đều có chứa _BEGAN hay _FINISHED. Ở chỗ làm, tôi rất hay phàn nàn các anh em về cách đặt tên biến, tên hàm thiếu nhất quán; với các project React thì có thêm vấn action type, cũng đau đầu muốn chết.

Ai đó: Callback trong saga à? Dị đời quá nhỉ.
Tôi: Oke, cách dùng đây.

const CANCEL_ORDER = "CANCEL_ORDER";

function* takeCancelOrder({ payload: { orderId } }) {
yield unfoldSaga({
handler: async () => {
const response = await request("/huy-don", { orderId });
return response; // Or response.json();
},
key: "CANCEL_ORDER", // Once and for all
});
}

Trong unfoldSaga kết quả của handler() sẽ được trả lại cho action CANCEL_ORDER_SUCCESS. Gần như xong rồi, sửa lại reducer một chút cũng khá nhanh thôi.

Kết quả

Ai đó: Nhìn ngắn gọn hơn nhiều đấy nhỉ!
Tôi: Cứ nói là không đi. :)))
Ai đó: Sao không thấy dùng callback ở đâu nhỉ?
Tôi: Code ngắn gọn hơn rồi. Bây giờ mới đến lúc callback tỏa sáng.

function* takeCancelOrder({ callbacks, payload: { orderId } }) {
yield unfoldSaga(
{
handler: async () => {
const response = await request("/huy-don", { orderId });
return response; // Or response.json();
},
key: "CANCEL_ORDER", // Once and for all
},
callbacks
);
}

Bây giờ thì có thể update action theo phong cách mới rồi, ma thuật này:

this.props.dispatch(
cancelOrder(
{ orderId },
{
callbackOnBegin: () => {
this.setState({ isCanceling: true });
},
callbackOnFailure: (error) => {
// show error to your users or stay quiet
},
callbackOnFinish: () => {
this.setState({ isCanceling: false });
},
callbackOnSuccess: (response) => {
// whatever
},
}
)
);

function cancelOrder(payload, callbacks) {
return {
type: "CANCEL_ORDER",
payload,
callbacks,
// any other things
};
}

Ai đó: Damn!
Tôi: Khởi tạo state thì thôi, tự viết nhé! :))

Trông thật sự là hứa hẹn. Với cách gọi action ở ngay trên, chúng ta có thể sử dụng local state nhiều hơn thay vì cứ phải đẩy mọi thứ vào redux store. Chấm dứt lạm dụng redux cho những việc đơn giản như hiển thị một icon loading.

Dữ liệu chưa cần đưa vào redux store mà vẫn có thể thao tác từ component. Cái này chắc có thể gọi là half way binding chăng? :))

Tổng kết

Bài viết hơi nhiều code và dài nhưng chúng ta có thể rút ra vài điều:

  • Saga cực kỳ tối ưu cho việc xử lý API request nói riêng và app side effect nói chung
  • Saga và Callback có thể cộng tác chứ không triệt tiêu nhau
  • Callback Hell là lỗi của bạn chứ không phải javascript

Nhược điểm

Không có gì là hoàn hảo cả. Mình đã và đang áp dụng giải pháp này cho một vài dự án thì vẫn thấy nó có một số nhược điểm:

  • Làm tăng lượng code xử lý logic trong các component
  • Dễ khiến anh em quên đi việc sử dụng redux-saga để xử lý side effect

Cảm ơn anh em đã đọc đến đây. ╰(°▽°)╯

· Một phút để đọc
ManhPT

Vấn đề là...

Không ít lần project có cấu hình eslint gặp lỗi Expected linebreaks to be 'LF' but found 'CRLF'. Lỗi này thực sự dẫn đến sự bế tắc khi lần đầu gặp phải. Tại sao lập trình trên windows cứ hay gặp mấy vấn đề dễ gây bối rối như vậy? Shit… chê tí thôi chứ dùng MacOS hay Linux thì đừng mơ chơi đc PUBG Mobile giả lập.

git config --global core.autocrlf false
git config --global core.eol lf

Gõ 2 dòng lệnh trên vào bất cứ CLI tool nào bạn có (powershell, cmd, terminal…). Done!

Giải ngố 1 chút

  • core.autocrlf là để tự động sử dụng CRLF cho các file mới được tạo hoặc sau khi git add
  • core.eol là để set mặc định kiểu xuống dòng cho Git
  • --global thì bạn sẽ setup cấu hình Git trên toàn hệ thống
  • Còn nếu muốn apply setting đặc dị này cho từng project thì có thể dùng file .gitattributes

Tham khảo

  1. https://help.github.com/en/articles/dealing-with-line-endings
  2. https://stackoverflow.com/questions/37826449/expected-linebreaks-to-be-lf-but-found-crlf-linebreak-style
  3. https://stackoverflow.com/questions/1552749/difference-between-cr-lf-lf-and-cr-line-break-types