Làm lại giao diện một sản phẩm khi người dùng vẫn đang dùng
PRIMUS, một nền tảng tuyển dụng, có phần giao diện chia ra nhiều ứng dụng riêng. Gom chúng về một mối mất khoảng 18 tháng trong khi tính năng mới vẫn ra đều, và phần khó là duy trì cả phiên bản cũ lẫn mới cùng chạy song song.
Tóm tắt
Là lập trình viên frontend duy nhất của PRIMUS, nền tảng tuyển dụng tại Navigos Group, mình mất khoảng 18 tháng để gom các ứng dụng React rời rạc về một ứng dụng Angular trong khi tính năng mới vẫn ra đều, và thời gian tải ban đầu giảm từ khoảng bảy giây xuống dưới một giây. Bài viết kể cách mình giữ bản cũ và bản mới cùng chạy song song, cùng những điều mình sẽ làm khác ở lần sau, chẳng hạn một đợt viết lại tập trung trong sáu tháng.

Sửa sang một căn phòng sẽ dễ hơn nhiều nếu có thể dọn hết đồ ra ngoài một thời gian. Nhưng một sản phẩm vẫn có người dùng và vẫn cần thêm tính năng mới thì không cho mình khoảng trống ấy, và PRIMUS đang ở đúng tình cảnh đó khi mình tham gia.
PRIMUS là nền tảng tuyển dụng cao cấp tại Navigos Group, và mình vào làm lập trình viên giao diện (frontend) duy nhất của sản phẩm. Lúc đó, phần giao diện nằm rải rác trong vài ứng dụng React viết bằng JavaScript thuần, còn trạng thái Redux mà các ứng dụng này dùng chung đã trở nên rối rắm. Với người dùng, mở sản phẩm mất khoảng bảy giây, còn chuyển giữa các tính năng mất khoảng ba giây. Phần frontend cần được dời sang một cấu trúc tốt hơn, nhưng trong suốt thời gian đó sản phẩm vẫn phải chạy và vẫn phải có tính năng mới.
Chọn cấu trúc trước khi dời đồ
Trước khi chuyển bất cứ thứ gì, mình cần biết sẽ chuyển sang đâu. Các ứng dụng cũ không dùng TypeScript, thiếu ranh giới rõ ràng giữa các phân hệ (module), và phần trạng thái dùng chung thì khó theo dõi. Đó là những lựa chọn trong cách viết code, nên chỉ đổi bộ khung phát triển (framework) thì không tự sửa được chúng.
Để thay thế, mình chọn Angular, một framework làm ứng dụng web có sẵn khung cấu trúc rõ ràng. Module chia ứng dụng thành từng phần có ranh giới riêng, service là nơi đặt phần xử lý mà nhiều phần cùng dùng, còn dependency injection tự đưa đúng service tới phần nào cần nó, nên mình có được ranh giới và một chỗ cố định cho phần xử lý dùng chung, đúng những thứ mà các ứng dụng cũ với đống trạng thái dùng chung rối rắm còn thiếu. Là người duy nhất lo frontend, mình cũng muốn bớt những lần phải tự chọn từng thư viện riêng lẻ, và Angular có sẵn công cụ điều hướng (routing), HTTP, biểu mẫu và RxJS. Đó là lựa chọn thực tế cho công việc trước mắt: khi có một cấu trúc nhất quán, mỗi phần mới đều có chỗ phù hợp để đặt vào, trong lúc các ứng dụng cũ vẫn đang được dùng.

Dời từng phòng một
Viết lại toàn bộ nghĩa là nhiều tháng liền không phát hành được gì, nên mình chuyển sản phẩm sang từng phần trong khi việc làm tính năng vẫn tiếp tục. Sau khoảng 18 tháng, những ứng dụng React riêng lẻ đã về chung một ứng dụng Angular.

Khi có tính năng mới hoặc một màn hình cần sửa lớn
Phần nào được chuyển tiếp theo phần lớn do chính công việc sản phẩm quyết định. Mình làm tính năng mới bằng Angular ngay từ đầu, nên việc phát triển tính năng cũng giúp sản phẩm chuyển dần sang ứng dụng mới. Khi gặp phần xử lý mà nhiều nơi trong sản phẩm cùng dùng, mình chuyển phần đó vào service. Một màn hình React cũ sẽ được chuyển sang khi màn hình đó cần thay đổi lớn, còn các Redux store được thay lần lượt từng cái, bắt đầu từ những store gây nhiều rắc rối nhất.
Sửa một tính năng bình thường khi hai framework cùng chạy
Cách làm này giúp việc chuyển đổi tiến lên song song với công việc sản phẩm, nhưng cũng có nghĩa là mình phải duy trì hai framework trong phần lớn giai đoạn chuyển tiếp. Mỗi ranh giới giữa phần cũ và phần mới lại thêm một điều phải nhớ, kể cả khi chỉ sửa một tính năng thông thường. Việc làm tính năng và sửa lỗi thông thường vẫn tiếp tục song song với chuyển đổi: tính trên toàn dự án, hồ sơ PRIMUS ghi nhận 89 phiếu công việc (ticket) Jira đã được phân công, ở trạng thái Done hoặc Resolved, gồm cả tính năng lẫn sửa lỗi frontend.
Khi quản lý sản phẩm chọn việc làm tiếp theo
Trong lúc việc chuyển đổi vẫn đang diễn ra, quản lý sản phẩm sắp thứ tự cho các tính năng dựa trên phàn nàn của khách hàng và nhận định của chính họ. Mình làm một hệ thống theo dõi dựa trên sự kiện (event) để họ có thêm dữ liệu sử dụng.
Ý tưởng là các phần khác nhau trên một trang có thể bổ sung bối cảnh vào một sự kiện trước khi sự kiện đó được gửi đi. Chẳng hạn với một lượt xem trang, module có thể thêm giai đoạn trong quy trình xử lý ứng viên, thành phần tìm kiếm có thể thêm truy vấn Boolean, còn một hàm xử lý khác cho biết mức phân quyền. Hệ thống theo dõi gom được những phần đóng góp ấy mà không cần biết chi tiết bên trong của từng thành phần. Nhờ vậy, lần đầu tiên quản lý sản phẩm có dữ liệu sử dụng trong tay, và đó cũng là một cách cải thiện sản phẩm trong lúc việc chuyển đổi vẫn tiếp tục.
Khi chuyển xong, và điều mình sẽ làm khác
Đến cuối, thời gian tải ban đầu giảm xuống dưới một giây, còn chuyển giữa các tính năng chỉ mất chưa tới nửa giây. Angular hợp với dự án này, nhưng nếu bắt đầu lại, mình sẽ cân nhắc SvelteKit hoặc Next.js, vì khi làm một mình, giờ mình thấy decorator, module và cấu hình dependency injection của Angular tạo thêm việc phải lo.
Mình cũng sẽ kiên trì đề xuất một đợt viết lại tập trung trong sáu tháng, đồng thời ngừng thêm tính năng vào ứng dụng cũ. Việc tiếp tục làm tính năng là thứ giúp cách chuyển từng bước khả thi, nhưng cũng chính nó kéo thời gian duy trì hai hệ thống lên khoảng 18 tháng, nên ở lần chuyển đổi tiếp theo, mình muốn kế hoạch dành nhiều thời gian hơn cho việc hoàn tất giai đoạn chuyển tiếp.
PRIMUS cuối cùng ngừng hoạt động vì lý do chiến lược kinh doanh, và khi quyết định đó được đưa ra, ứng dụng vẫn đang chạy tốt. Trải nghiệm ấy khiến mình cân nhắc kỹ hơn khi chọn cách chuyển đổi từng bước, vì giờ mình biết kế hoạch cần tính cả công sức giữ cho phần cũ và phần mới cùng dùng được với nhau.