{
  "version": 1,
  "locale": "vi",
  "updated": "2021-09-08",
  "page": {
    "type": "article",
    "title": "Frontend Du Ký S1E2 | EGANY Apps (phần 2): Nói dễ hơn làm",
    "description": "Như đã nói ở phần một, Frontend Du Ký S1E2 | EGANY Apps sẽ kể về những lỗ hổng trong quy trình làm việc cũng như những vướng mắc về kỹ thuật mà team gặp phải...",
    "url": "https://egany.com/blogs/eganyers/frontend-du-ky-s1e2-egany-apps-phan-2-noi-de-hon-lam/",
    "markdown_url": "https://egany.com/blogs/eganyers/frontend-du-ky-s1e2-egany-apps-phan-2-noi-de-hon-lam.md",
    "json_url": "https://egany.com/blogs/eganyers/frontend-du-ky-s1e2-egany-apps-phan-2-noi-de-hon-lam.json",
    "group": "eganyers",
    "author": "Thành Nguyễn",
    "published": "2021-07-29T08:48:32.000Z",
    "modified": "2021-09-08T08:19:37.000Z",
    "categories": [
      "Culture",
      "Engineering"
    ]
  },
  "organization": {
    "name": "EGANY",
    "legal_name": "CÔNG TY CỔ PHẦN CÔNG NGHỆ EGANY",
    "alternate_name": "EGANY Technology JSC",
    "url": "https://egany.com/",
    "address": "77/6 Hồ Thị Kỷ, Phường Vườn Lài, TP. Hồ Chí Minh",
    "telephone": "+84934269700",
    "email": "support@egany.com",
    "tax_id": "0313000791",
    "opening_hours": "Thứ 2 - Thứ 6, 9:00 - 17:00",
    "same_as": [
      "https://www.facebook.com/egany.tech/",
      "https://www.youtube.com/user/eganydotcom",
      "https://zaloapp.com/qr/p/15np7bg4k8s47?src=qr"
    ]
  },
  "headings": [
    {
      "level": 1,
      "text": "Frontend Du Ký S1E2 | EGANY Apps (phần 2): Nói dễ hơn làm",
      "id": "frontend-du-ky-s1e2-egany-apps-phan-2-noi-de-hon-lam"
    },
    {
      "level": 2,
      "text": "Giới thiệu",
      "id": "gioi-thieu"
    },
    {
      "level": 2,
      "text": "Kỹ thuật",
      "id": "ky-thuat"
    },
    {
      "level": 3,
      "text": "1\\. Xác thực người dùng",
      "id": "1-xac-thuc-nguoi-dung"
    },
    {
      "level": 3,
      "text": "2\\. Global state management",
      "id": "2-global-state-management"
    },
    {
      "level": 3,
      "text": "3\\. Dynamic route (url) với Next.js",
      "id": "3-dynamic-route-url-voi-nextjs"
    },
    {
      "level": 3,
      "text": "4\\. Custom component",
      "id": "4-custom-component"
    },
    {
      "level": 3,
      "text": "5\\. Docker",
      "id": "5-docker"
    },
    {
      "level": 2,
      "text": "Quy trình",
      "id": "quy-trinh"
    },
    {
      "level": 3,
      "text": "1\\. Bỏ bước review code",
      "id": "1-bo-buoc-review-code"
    },
    {
      "level": 3,
      "text": "2\\. Single point of failure",
      "id": "2-single-point-of-failure"
    },
    {
      "level": 3,
      "text": "3\\. Giao tiếp trong team",
      "id": "3-giao-tiep-trong-team"
    },
    {
      "level": 2,
      "text": "Tạm kết",
      "id": "tam-ket"
    }
  ],
  "links": [
    {
      "text": "https://dribbble.com/shots/7133803-Programming",
      "url": "https://dribbble.com/shots/7133803-Programming"
    },
    {
      "text": "Redux",
      "url": "https://react-redux.js.org/"
    },
    {
      "text": "MobX",
      "url": "https://mobx.js.org/getting-started.html"
    },
    {
      "text": "https://dribbble.com/shots/9707966-Web-Site-Developing-Process",
      "url": "https://dribbble.com/shots/9707966-Web-Site-Developing-Process"
    }
  ],
  "sections": [
    {
      "id": "hero",
      "heading": "Frontend Du Ký S1E2 | EGANY Apps (phần 2): Nói dễ hơn làm",
      "content_markdown": "Như đã nói ở phần một, **Frontend Du Ký S1E2 | EGANY Apps** sẽ kể về những lỗ hổng trong quy trình làm việc cũng như những vướng mắc về kỹ thuật mà team gặp phải trong quá trình phát triển EGANY Apps.\n\nNếu bạn chưa đọc phần một, bạn có thể đọc trước tại đây.",
      "links": []
    },
    {
      "id": "gioi-thieu",
      "heading": "Giới thiệu",
      "content_markdown": "![egany-apps-sign-in-screen](https://cdn.egany.com/images/blogs/2021/07/egany-apps-sign-in-screen-700x338.png)\n\nSau khi thống nhất được quy trình làm việc cũng như công nghệ để thực hiện dự án, team bắt đầu quá trình phát triển.\n\nKế hoạch lúc bấy giờ bao gồm:\n\n- Tập trung phát triển sườn dự án: authentication (sign-in/sign-out), layout, …\n- Phát triển component để sử dụng chung\n- Chuyển đổi các dashboard ở hệ thống cũ\n\nĐây là lúc các vấn đề bắt đầu xuất hiện. Để dễ dàng hơn cho các bạn thì mình sẽ chia các vấn đề thành hai nhóm chính, cụ thể:\n\n- Kỹ thuật\n- Quy trình\n\nCùng bắt đầu tìm hiểu nha!\n\n> Trong bài viết mình sẽ gọi bên đối tác e-commerce (bao gồm Haravan và Sapo) là **platform** để tránh việc lặp lại tên của các nền tảng đó.",
      "links": []
    },
    {
      "id": "ky-thuat",
      "heading": "Kỹ thuật",
      "content_markdown": "![van-de-ki-thuat](https://cdn.egany.com/images/blogs/2021/07/van-de-ki-thuat-600x450.png)\n\n> Nguồn ảnh: [https://dribbble.com/shots/7133803-Programming](https://dribbble.com/shots/7133803-Programming)\n\n### 1\\. Xác thực người dùng\n\nEGANY Apps không có một hệ thống xác thực hoàn chỉnh. EGANY chỉ đóng vai trò là trung gian cho phía platform. Flow xác thực sẽ như hình sau:\n\n![sign-in-flow](https://cdn.egany.com/images/blogs/2021/07/sign-in-flow.jpg)\n\n1. EGANY Apps gửi thông tin người dùng cũng như thông tin ứng dụng về phía backend\n2. Backend nhận thông tin và truy xuất thêm một vài thuộc tính khác và gửi cho phía platform\n3. Phía platform tiến hành xác thực và trả API token lại cho phía backend. Quá trình này thường chỉ làm một lần khi cài đặt hoặc khi người dùng revoke (thu hồi) token\n4. Backend tạo một mã OTP để xác thực với EGANY Apps\n5. EGANY Apps nhận mã xác thực và gửi ngược lại phía backend\n6. Backend tạo token xác thực và gửi lại cho EGANY Apps\n\nMột số bạn có thể thắc mắc là vì sao phía backend không gửi trực tiếp token về phía EGANY Apps luôn mà phải tạo ra một token trung gian. Lý do như sau:\n\n- **Hạn chế rủi ro lộ token.** Do API phía platform đều là public API, nếu để lộ token của khách thì thiệt hại sẽ tương đối lớn. Hơn nữa, nếu token do EGANY quản lý thì khi gặp sự cố sẽ xử lý dễ dàng và nhanh chóng hơn, không bị phụ thuộc vào phía platform\n- **Phục vụ business.** Đồng ý là EGANY Apps có thể tự làm được nhiều thứ với API token trả về trực tiếp. Tuy nhiên, các tác vụ liên quan đến business (nghiệp vụ) buộc phải xử lý ở backend. Các nghiệp vụ bao gồm quản lý hạn mức, quản lý hạn sử dụng cũng như giới hạn các tính năng của ứng dụng tùy vào gói cước (pricing plan) mà khách đang sử dụng\n\nXong vấn đề về flow thì team gặp tiếp vấn đề về lưu trữ token. Mỗi ứng dụng sẽ có một token riêng và khách có thể đăng nhập một hoặc nhiều ứng dụng cùng một lúc. Hơn nữa, do EGANY Apps hỗ trợ nhiều platform nên khách có thể đăng nhập một ứng dụng trên nhiều platform khác nhau. Phức tạp phải không nào? Sau khi thảo luận và bàn bạc thì team chốt đơn như sau:\n\n- Cho phép đăng nhập nhiều ứng dụng\n- Một ứng dụng chỉ được đăng nhập trên một platform. Nếu khách hàng đăng nhập một ứng dụng trên nhiều platform khác nhau thì chỉ platform cuối cùng mới được tính\n\nTeam lựa chọn lưu trữ token dưới dạng `Array`. Đây không phải là một sự lựa chọn hợp lý vì token thường được truy cập dưới dạng `key` nên hiệu năng sẽ không tốt. Ví dụ:\n\n```javascript\n// Array\nconst token = tokenArr.find(isCurrentAuthSession)\n// => Về lý thuyết là O(n)\n\n// Map\nconst tokenKey = getCurrentAuthSessionKey()\nconst token = tokenMap.get(tokenKey)\n// => Về lý thuyết là O(1)\n```\n\nTất nhiên, về lý thuyết là như vậy nhưng trên thực tế thì sẽ không nhận ra được ảnh hưởng rõ rệt do số lượng phần tử (ở đây là phiên đăng nhập của người dùng) khá là thấp. Dù vậy thì ở những phiên bản sau này, team vẫn quyết định thay thế `Array` bằng `Map` để tiện cho việc truy xuất.\n\n### 2\\. Global state management\n\nSau khi xác thực người dùng thành công, hầu hết các ứng dụng đều lưu trữ lại thông tin xác thực để sử dụng trong suốt thời gian ứng dụng hoạt động. EGANY Apps cũng không phải là ngoại lệ.\n\nĐể giải quyết bài toán này thì team có hai lựa chọn lúc bấy giờ:\n\n- [Redux](https://react-redux.js.org/)\n- [MobX](https://mobx.js.org/getting-started.html)\n\nSau khi tham khảo một số nguồn trên mạng và trải nghiệm thử thì team quyết định chọn MobX. Lý do:\n\n- Nhu cầu của team không quá phức tạp. Cả 2 đều đáp ứng đủ\n- Syntax đơn giản và đỡ rườm rà hơn Redux (tính theo thời điểm triển khai, sau khi React hooks ra đời thì Redux đã gọn gàng hơn rất nhiều)\n- Dễ hiểu, dễ sử dụng\n\nDù vậy MobX vẫn còn một số hạn chế sau:\n\n- Buộc phải sử dụng Class component\n- Muốn debug ở console buộc team phải sử dụng hàm `toJS()` để in dữ liệu do dữ liệu được lưu ở dạng `Observable`\n- Cần cài đặt và thiết lập dự án một chút thì mới dùng được các syntax của MobX\n\nNgoài những khó khăn trên thì team khá hài lòng với giải pháp ở thời điểm hiện tại.\n\n### 3\\. Dynamic route (url) với Next.js\n\nSau khi có khung sườn xác thực người dùng cũng như móc nối thông tin vào *global state* thì team bắt tay vào phát triển giao diện dashboard.\n\nNếu các bạn chưa biết thì Next.js sử dụng *file-based routing*, nghĩa là đường dẫn file sẽ tương ứng với đường dẫn trang. Ví dụ:\n\n- **Đường dẫn file:** `pages/products/index.jsx`\n- **Đường dẫn trang** `/products`\n\nhoặc\n\n- **Đường dẫn file:** `pages/products/productId.jsx`\n- **Đường dẫn trang** `/products/productId`\n\nCó lẽ các bạn cũng nhận ra vấn đề ở đây rồi phải không? Trong thực tế sẽ xuất hiện vô số `productId` và nếu phải tạo từng file cho mỗi trang chi tiết sản phẩm theo `id` thì.. gần như không thể, đúng không nào?  \nĐó là vấn đề mà dynamic route (hay còn gọi là dynamic url) giải quyết. Nó sẽ tự động truyền `productId` vào một trang (template) phù hợp và trả về HTML tương ứng.\n\nTiếc thay, ở thời điểm này, **Next.js v7** chưa hỗ trợ sẵn mà buộc lòng team phải custom để xử lý. Cụ thể là sử dụng `express` để quản lý Server-side Rendering cho Next.js. Một đoạn code mẫu cho dynamic route sẽ như sau:\n\n```javascript\nserver.get('/products/:id', (req, res) => {\n  const pageTemplate = '/products/id'\n  const productId = req.params.id\n\n  app.render(req, res, pageTemplate, { id: productId })\n})\n```\n\nTuy nhiên, khi chạy với custom server như trên thì sẽ mất đi tính năng `hot-reload` (một dạng auto reload nhưng giữ lại `state` hiện tại của ứng dụng), khiến cho trải nghiệm khi code giảm đi đáng kể, đặc biệt là khi làm việc với form hoặc modal.\n\n### 4\\. Custom component\n\nNhư đã đề cập ở phần 1 thì team đã quyết định dựng bộ component riêng dựa trên nền tảng CSS đã có từ các sản phẩm trước đó.\n\nDo chưa có kinh nghiệm nhiều nên việc phân tách component cũng gặp tương đối khó khăn. Đáng lưu ý nhất có lẽ là việc tích hợp các thư viện bên ngoài, đặc biệt là thư viện rich-text editor.\n\nCác thư viện rich-text editor đa số đều sử dụng object `window` trong source code của mình. Ngặt nỗi khi render lần đầu tiên thì trang lại được chạy ở server, làm gì có object `window` mà dùng. Ấy vậy là team lại phải làm đủ trò để page có thể được render thành công trên server.\n\nChưa hết, ngoài rich-text editor thì team còn phải optimize color-picker. Khi người dùng chọn màu, event `onChange` sẽ được gọi rất nhiều lần. Nếu quản lý không khéo sẽ tạo ra hiện tượng giật/khựng UI rất khó chịu. Team chủ động áp dụng các thủ thuật debounce (một dạng delay function) để cải thiện hiệu năng cho component này\n\n### 5\\. Docker\n\nKhó khăn cuối cùng có lẽ là tạo dockerfile để triển khai ứng dụng.\n\nDo cũng chưa có nhiều kinh nghiệm về mảng này nên lỗi cũng tương đối nhiều. Nổi bật nhất có lẽ là lệnh `npm install --production`\n\nNếu bạn chưa biết, `--production` flag (cờ) sẽ loại bỏ toàn bộ package nằm trong `devDependencies` và chỉ cài đặt các package có trong `dependencies`. Về lý thuyết thì nó sẽ giảm thời gian deploy vì số lượng package sẽ giảm đi đáng kể. Trong thực tế, một số package ở `devDependencies` lại cần thiết để build được project, đơn cử như `node-sass` để compile file `.scss`\n\nNếu nhớ không nhầm thì team mất khoảng một buổi chiều chỉ để tạo và triển khai thành công ứng dụng bằng docker.",
      "links": [
        {
          "text": "https://dribbble.com/shots/7133803-Programming",
          "url": "https://dribbble.com/shots/7133803-Programming"
        },
        {
          "text": "Redux",
          "url": "https://react-redux.js.org/"
        },
        {
          "text": "MobX",
          "url": "https://mobx.js.org/getting-started.html"
        }
      ]
    },
    {
      "id": "quy-trinh",
      "heading": "Quy trình",
      "content_markdown": "![van-de-quy-trinh](https://cdn.egany.com/images/blogs/2021/07/van-de-quy-trinh-600x450.png)\n\n> Nguồn ảnh: [https://dribbble.com/shots/9707966-Web-Site-Developing-Process](https://dribbble.com/shots/9707966-Web-Site-Developing-Process)\n\n### 1\\. Bỏ bước review code\n\nTuy trong quy trình đã chốt có bước tạo pull request cũng như review code nhưng khi bắt đầu phát triển thì khối lượng công việc phát sinh khá nhiều, không đủ thời gian để thực hiện review. Team quyết định bỏ bước review code để tiết kiệm thời gian.\n\nĐây là quyết định không tốt và dẫn tới nhiều hậu quả xấu như:\n\n- Chất lượng code không cao. Thường xuyên xuất hiện các đoạn code không được format hoặc xử lý theo hướng không hợp lý, gây giảm hiệu năng hoặc thậm chí gây lỗi cho ứng dụng\n- Không nhất quán khi cùng xử lý một vấn đề\n- Các thành viên không có cơ hội học hỏi\n\nHiện tại team đã có một số giải pháp tạm thời để khắc phục tình trạng trên. Chắc để bài sau mình chia sẻ nhé!\n\n### 2\\. Single point of failure\n\nThường ở những bước quan trọng như merge code hoặc deploy thì chỉ có một người đảm nhận. Điều này là hợp lý vì họ là những người nắm rõ dự án nhất và khi làm việc với các team khác thì một người đại diện sẽ dễ trao đổi hơn rất nhiều.\n\nThế nhưng, nếu không có người backup thì sẽ là một tai họa. Đã xảy ra trường hợp EGANY Apps gặp lỗi lớn trên `production` và không may hôm đấy bạn phụ trách deploy lại có việc cá nhân không thể lên công ty. Ấy vậy là anh em phải xin lỗi và hẹn khách lại vào ngày mai.\n\nĐây là một thiếu sót lớn trong team và hiện đang dần được khắc phục bằng các buổi workshop training cũng như tài liệu đi kèm dự án.\n\n### 3\\. Giao tiếp trong team\n\nGiao tiếp trong giai đoạn này có lẽ là nguyên nhân lớn nhất dẫn tới các khó khăn bên trên.\n\nTuy các thành viên chủ động liên hệ lẫn nhau khi cần merge code hoặc deploy nhưng ngoài những việc đó thì lại ít trao đổi những việc khác với nhau. Điều đó dẫn tới các vấn đề như không đồng nhất trong xử lý hoặc đôi khi là việc lặp code không đáng có.\n\nRất may là giao tiếp giữa các team với nhau khá ổn, làm việc rất chủ động và ăn ý nên dự án vẫn phát triển đều, không bị khựng lại nhiều.",
      "links": [
        {
          "text": "https://dribbble.com/shots/9707966-Web-Site-Developing-Process",
          "url": "https://dribbble.com/shots/9707966-Web-Site-Developing-Process"
        }
      ]
    },
    {
      "id": "tam-ket",
      "heading": "Tạm kết",
      "content_markdown": "Có công mài sắt, có ngày nên kim. Dù còn nhiều vấn đề trong quy trình cũng như các vấn đề liên quan tới kỹ thuật nhưng team vẫn hoàn thành được mục tiêu ra mắt phiên bản đầu tiên vào tháng 4 năm 2019 với thông số:\n\n- 5 ứng dụng\n- 1 platform\n\nChưa hài lòng với kết quả trên, team tiếp tục quá trình phát triển EGANY Apps với các mục tiêu sau:\n\n- Hỗ trợ thêm ít nhất một platform\n- Phát triển các ứng dụng dạng subscription thay vì one-time payment\n- Cải thiện quy trình cũng như source code để có nền tảng vững chắc hơn\n\nChặng đường còn dài, chắc hẹn các bạn ở bài viết tiếp theo nha!\n\nCám ơn các bạn đã đọc. Happy hacking!",
      "links": []
    }
  ]
}