Làm mới cấu trúc Công cụ cho nhà phát triển: di chuyển sang các mô-đun JavaScript

Tim van der Lippe
Tim van der Lippe

Như bạn có thể đã biết, Công cụ của Chrome cho nhà phát triển là một ứng dụng web được viết bằng HTML, CSS và JavaScript. Trong những năm qua, Công cụ cho nhà phát triển ngày càng có nhiều tính năng phong phú hơn, thông minh hơn và am hiểu hơn về nền tảng web rộng hơn. Mặc dù Công cụ cho nhà phát triển đã mở rộng trong những năm qua, nhưng kiến trúc của Công cụ cho nhà phát triển phần lớn vẫn giống với kiến trúc ban đầu khi vẫn còn là một phần của WebKit.

Bài đăng này nằm trong loạt bài đăng trên blog mô tả những thay đổi mà chúng tôi đang thực hiện đối với cấu trúc Công cụ cho nhà phát triển và cách xây dựng công cụ này. Chúng tôi sẽ giải thích cách Công cụ cho nhà phát triển hoạt động trước đây, lợi ích và hạn chế của Công cụ cho nhà phát triển cũng như những việc chúng tôi đã làm để khắc phục những hạn chế này. Do đó, hãy cùng tìm hiểu sâu hơn về các hệ thống mô-đun, cách tải mã và cách chúng ta sử dụng các mô-đun JavaScript.

Ban đầu, chẳng có gì

Mặc dù bối cảnh giao diện người dùng hiện tại có nhiều hệ thống mô-đun với các công cụ được xây dựng xung quanh chúng và định dạng mô-đun JavaScript hiện được chuẩn hoá, nhưng không có hệ thống nào trong số này tồn tại khi Công cụ cho nhà phát triển được xây dựng lần đầu tiên. Công cụ cho nhà phát triển được xây dựng dựa trên mã được phát hành lần đầu trong WebKit vào hơn 12 năm trước.

Lần đầu tiên nhắc đến hệ thống mô-đun trong Công cụ cho nhà phát triển bắt nguồn từ năm 2012: việc giới thiệu danh sách các mô-đun có danh sách các nguồn được liên kết. Đây là một phần của cơ sở hạ tầng Python được sử dụng vào thời điểm đó để biên dịch và xây dựng Công cụ cho nhà phát triển. Thay đổi tiếp theo đã trích xuất tất cả mô-đun vào một tệp frontend_modules.json riêng biệt (cam kết) vào năm 2013, rồi thành các tệp module.json riêng biệt (cam kết) trong năm 2014.

Ví dụ về tệp module.json:

{
  "dependencies": [
    "common"
  ],
  "scripts": [
    "StylePane.js",
    "ElementsPanel.js"
  ]
}

Kể từ năm 2014, mẫu module.json đã được sử dụng trong Công cụ cho nhà phát triển để chỉ định các mô-đun và tệp nguồn của công cụ. Trong khi đó, hệ sinh thái web phát triển nhanh chóng và nhiều định dạng mô-đun đã được tạo, bao gồm UMD, CommonJS và các mô-đun JavaScript được chuẩn hoá sau cùng. Tuy nhiên, Công cụ cho nhà phát triển bị mắc kẹt với định dạng module.json.

Trong khi Công cụ cho nhà phát triển vẫn hoạt động, việc sử dụng một hệ thống mô-đun không được chuẩn hoá và độc đáo đã có một vài nhược điểm:

  1. Định dạng module.json yêu cầu công cụ tạo bản dựng tuỳ chỉnh, gần giống với các trình đóng gói hiện đại.
  2. Không tích hợp IDE nào, nên yêu cầu sử dụng công cụ tuỳ chỉnh để tạo tệp mà các IDE hiện đại có thể hiểu được (tập lệnh gốc để tạo tệp jsconfig.json cho Mã VS).
  3. Các hàm, lớp và đối tượng đều được đưa vào phạm vi toàn cục để có thể chia sẻ giữa các mô-đun.
  4. Các tệp phụ thuộc vào thứ tự, nghĩa là thứ tự liệt kê của sources rất quan trọng. Không có gì đảm bảo rằng mã mà bạn dựa vào sẽ được tải ngoài việc một người đã xác minh mã.

Nhìn chung, khi đánh giá trạng thái hiện tại của hệ thống mô-đun trong Công cụ cho nhà phát triển và các định dạng mô-đun khác (được sử dụng rộng rãi hơn), chúng tôi kết luận rằng mẫu module.json đang tạo ra nhiều vấn đề hơn là giải quyết được và đã đến lúc chúng ta ngừng sử dụng mẫu này.

Lợi ích của tiêu chuẩn

Trong số các hệ thống mô-đun hiện có, chúng tôi đã chọn các mô-đun JavaScript làm mô-đun để di chuyển sang. Tại thời điểm đó, các mô-đun JavaScript vẫn đang được vận chuyển sau một cờ trong Node.js và một lượng lớn gói có sẵn trên ALIAS không có gói mô-đun JavaScript mà chúng tôi có thể sử dụng. Mặc dù vậy, chúng tôi kết luận rằng các mô-đun JavaScript là lựa chọn tốt nhất.

Lợi ích chính của các mô-đun JavaScript là định dạng mô-đun chuẩn cho JavaScript. Khi liệt kê các nhược điểm của module.json (xem ở trên), chúng tôi nhận thấy hầu hết các nhược điểm này đều liên quan đến việc sử dụng định dạng mô-đun không được chuẩn hoá và duy nhất.

Nếu chọn một định dạng mô-đun không được chuẩn hoá, chúng tôi phải đầu tư thời gian vào việc xây dựng các công cụ tích hợp bằng các công cụ xây dựng và công cụ mà các nhà bảo trì của chúng tôi sử dụng.

Những công cụ tích hợp này thường dễ gặp vấn đề và không được hỗ trợ các tính năng, nên yêu cầu thời gian bảo trì lâu hơn, đôi khi dẫn đến những lỗi nhỏ mà cuối cùng sẽ xuất hiện cho người dùng.

Vì các mô-đun JavaScript là tiêu chuẩn, nên các IDE như VS Code, các trình kiểm tra kiểu như wrap Compiler/TypeScript và các công cụ xây dựng như Rollup/minififiers sẽ có thể hiểu được mã nguồn mà chúng ta đã viết. Hơn nữa, khi một nhà bảo trì mới tham gia nhóm Công cụ cho nhà phát triển, họ sẽ không phải dành thời gian tìm hiểu định dạng module.json độc quyền, trong khi họ (có thể) đã quen thuộc với các mô-đun JavaScript.

Tất nhiên, khi Công cụ cho nhà phát triển được xây dựng vào lúc đầu, không có lợi ích nào nêu trên. Chúng tôi đã mất nhiều năm làm việc trong các nhóm tiêu chuẩn, triển khai thời gian chạy và các nhà phát triển sử dụng các mô-đun JavaScript để đưa ra ý kiến phản hồi để đạt được kết quả như bây giờ. Nhưng khi có các mô-đun JavaScript, chúng tôi phải đưa ra lựa chọn: tiếp tục duy trì định dạng riêng hoặc đầu tư vào việc chuyển sang định dạng mới.

Chi phí của một chiếc điện thoại mới

Mặc dù các mô-đun JavaScript có rất nhiều lợi ích mà chúng tôi muốn sử dụng, nhưng chúng tôi vẫn ở trong thế giới module.json không theo chuẩn. Nhờ nhận được lợi ích từ các mô-đun JavaScript, chúng tôi đã phải đầu tư đáng kể vào việc giải quyết các khoản nợ kỹ thuật, thực hiện quá trình di chuyển có thể phá vỡ các tính năng và tạo ra lỗi hồi quy.

Tại thời điểm này, câu hỏi không phải là "Chúng tôi có muốn sử dụng các mô-đun JavaScript không?", mà là câu hỏi "Chi phí để có thể sử dụng các mô-đun JavaScript là bao nhiêu?". Ở đây, chúng tôi phải cân bằng giữa rủi ro khiến người dùng ngừng sử dụng bằng cách hồi quy, chi phí các kỹ sư đã bỏ ra (một lượng lớn) thời gian để di chuyển và tình trạng tệ hơn tạm thời mà chúng tôi sẽ phải đối mặt.

Điểm cuối cùng đó hoá ra lại rất quan trọng. Mặc dù về mặt lý thuyết, chúng ta có thể sử dụng các mô-đun JavaScript, nhưng trong quá trình di chuyển, chúng ta sẽ chọn các mã cần phải tính đến cả module.json và các mô-đun JavaScript. Việc này không chỉ khó đạt được về mặt kỹ thuật mà còn có nghĩa là tất cả các kỹ sư làm việc về Công cụ cho nhà phát triển cần phải biết cách làm việc trong môi trường này. Họ sẽ phải liên tục tự hỏi "Đối với phần này của cơ sở mã, là module.json hay các mô-đun JavaScript và làm cách nào để thực hiện thay đổi?".

Xem trước: Chi phí ẩn khi hướng dẫn các đồng nghiệp bảo trì di chuyển lớn hơn chúng tôi dự kiến.

Sau khi phân tích chi phí, chúng tôi kết luận rằng vẫn nên di chuyển sang các mô-đun JavaScript. Do đó, mục tiêu chính của chúng tôi là:

  1. Hãy đảm bảo rằng việc sử dụng các mô-đun JavaScript sẽ mang lại lợi ích tối đa.
  2. Đảm bảo rằng việc tích hợp với hệ thống hiện có dựa trên module.json là an toàn và không gây ra tác động tiêu cực cho người dùng (lỗi hồi quy, sự thất vọng của người dùng).
  3. Hướng dẫn tất cả nhà bảo trì Công cụ cho nhà phát triển trong quá trình di chuyển, chủ yếu bằng cách kiểm tra và cân bằng các công cụ tích hợp sẵn để ngăn chặn sai sót ngoài ý muốn.

Bảng tính, chuyển đổi và nợ kỹ thuật

Mặc dù mục tiêu đã rõ ràng, nhưng những hạn chế do định dạng module.json áp đặt lại rất khó để giải quyết. Chúng tôi đã trải qua nhiều lần lặp lại, nguyên mẫu và thay đổi cấu trúc trước khi phát triển được giải pháp mà chúng tôi cảm thấy hài lòng. Chúng tôi đã viết một tài liệu thiết kế về chiến lược di chuyển mà chúng tôi đưa ra. Tài liệu thiết kế cũng liệt kê thời gian ước tính ban đầu của chúng tôi là: 2-4 tuần.

cảnh báo: phần tốn nhiều thời gian nhất của quá trình di chuyển mất 4 tháng và từ đầu đến cuối mất 7 tháng!

Tuy nhiên, kế hoạch ban đầu đã vượt qua thử thách thời gian: chúng tôi sẽ hướng dẫn môi trường thời gian chạy của Công cụ cho nhà phát triển tải tất cả các tệp được liệt kê trong mảng scripts trong tệp module.json theo cách cũ, trong khi tất cả các tệp được liệt kê trong mảng modules có tính năng nhập động mô-đun JavaScript. Mọi tệp nằm trong mảng modules đều có thể sử dụng tính năng nhập/xuất ES.

Ngoài ra, chúng tôi sẽ di chuyển theo 2 giai đoạn (cuối cùng, chúng tôi cũng chia giai đoạn cuối cùng thành 2 giai đoạn phụ, xem bên dưới): giai đoạn exportimport. Trạng thái của mô-đun sẽ là giai đoạn được theo dõi trong một bảng tính lớn:

Bảng tính di chuyển mô-đun JavaScript

Bạn có thể xem một đoạn trích của bảng tiến trình tại đây.

export pha

Giai đoạn đầu tiên là thêm câu lệnh export cho mọi ký hiệu mà lẽ ra sẽ được chia sẻ giữa các mô-đun/tệp. Quá trình chuyển đổi này sẽ được thực hiện tự động bằng cách chạy một tập lệnh cho mỗi thư mục. Với biểu tượng sau đây sẽ tồn tại trong thế giới module.json:

Module.File1.exported = function() {
  console.log('exported');
  Module.File1.localFunctionInFile();
};
Module.File1.localFunctionInFile = function() {
  console.log('Local');
};

(Ở đây, Module là tên của mô-đun và File1 là tên của tệp. Trong cây nguồn của chúng ta, giá trị đó sẽ là front_end/module/file1.js.)

Mục này sẽ được chuyển đổi thành như sau:

export function exported() {
  console.log('exported');
  Module.File1.localFunctionInFile();
}
export function localFunctionInFile() {
  console.log('Local');
}

/** Legacy export object */
Module.File1 = {
  exported,
  localFunctionInFile,
};

Ban đầu, chúng tôi dự định viết lại các lệnh nhập cùng tệp trong giai đoạn này. Ví dụ: trong ví dụ trên, chúng ta sẽ viết lại Module.File1.localFunctionInFile thành localFunctionInFile. Tuy nhiên, chúng tôi nhận ra rằng việc áp dụng tự động hoá sẽ dễ dàng và an toàn hơn nếu tách riêng hai quy tắc chuyển đổi này. Do đó, "di chuyển tất cả biểu tượng trong cùng một tệp" sẽ trở thành giai đoạn phụ thứ hai của giai đoạn import.

Vì việc thêm từ khoá export vào tệp sẽ biến đổi tệp từ "tập lệnh" thành "mô-đun" nên rất nhiều cơ sở hạ tầng Công cụ cho nhà phát triển phải được cập nhật tương ứng. Điều này bao gồm thời gian chạy (có tính năng nhập động), nhưng cũng có các công cụ như ESLint để chạy ở chế độ mô-đun.

Một phát hiện mà chúng tôi đã thực hiện trong khi xử lý những vấn đề này là các thử nghiệm của chúng tôi đã chạy ở chế độ "trơ trọi". Vì các mô-đun JavaScript ngụ ý rằng tệp chạy ở chế độ "use strict", điều này cũng sẽ ảnh hưởng đến các chương trình kiểm thử của chúng tôi. Kết quả là có một lượng kiểm thử không hề nhỏ dựa vào độ chậm này, bao gồm cả một bài kiểm thử sử dụng câu lệnh with 😂.

Cuối cùng, việc cập nhật thư mục đầu tiên để bao gồm các câu lệnh export mất khoảng một tuầnnhiều lần thử lại khi gửi lại.

import pha

Sau khi xuất tất cả biểu tượng bằng câu lệnh export và vẫn nằm trong phạm vi chung (cũ), chúng tôi phải cập nhật tất cả tệp tham chiếu đến biểu tượng của nhiều tệp để sử dụng tính năng nhập ES. Mục tiêu cuối cùng là xoá tất cả "đối tượng xuất cũ", đồng thời dọn dẹp phạm vi trên toàn cầu. Quá trình chuyển đổi này sẽ được thực hiện tự động bằng cách chạy một tập lệnh cho mỗi thư mục.

Ví dụ: đối với các ký hiệu sau đây tồn tại trong thế giới module.json:

Module.File1.exported();
AnotherModule.AnotherFile.alsoExported();
SameModule.AnotherFile.moduleScoped();

Chúng sẽ được chuyển đổi thành:

import * as Module from '../module/Module.js';
import * as AnotherModule from '../another_module/AnotherModule.js';

import {moduleScoped} from './AnotherFile.js';

Module.File1.exported();
AnotherModule.AnotherFile.alsoExported();
moduleScoped();

Tuy nhiên, có một số lưu ý với phương pháp này:

  1. Không phải biểu tượng nào cũng được đặt tên là Module.File.symbolName. Một số biểu tượng chỉ được đặt tên là Module.File hoặc thậm chí là Module.CompletelyDifferentName. Sự không nhất quán này có nghĩa là chúng tôi phải tạo ánh xạ nội bộ từ đối tượng toàn cục cũ đến đối tượng được nhập mới.
  2. Đôi khi sẽ có xung đột giữa các tên moduleScoped. Nổi bật nhất, chúng tôi đã sử dụng một mẫu khai báo một số loại Events nhất định, trong đó mỗi ký hiệu chỉ được đặt tên là Events. Điều này có nghĩa là nếu bạn đang theo dõi nhiều loại sự kiện được khai báo trong các tệp khác nhau, thì xung đột tên sẽ xảy ra trên câu lệnh import cho Events đó.
  3. Hoá ra, có những phần phụ thuộc vòng tròn giữa các tệp. Điều này bình thường trong ngữ cảnh phạm vi toàn cầu, vì việc sử dụng biểu tượng được sử dụng sau khi tất cả mã được tải. Tuy nhiên, nếu bạn yêu cầu import, phần phụ thuộc vòng sẽ được làm rõ. Đây không phải là vấn đề ngay lập tức, trừ phi bạn có lệnh gọi hàm hiệu ứng phụ trong mã phạm vi toàn cầu mà Công cụ cho nhà phát triển cũng có. Tóm lại, cần phải phẫu thuật và tái cấu trúc một chút để đảm bảo quá trình chuyển đổi diễn ra an toàn.

Một thế giới hoàn toàn mới với các mô-đun JavaScript

Vào tháng 2 năm 2020, 6 tháng sau khi bắt đầu tháng 9 năm 2019, lần dọn dẹp gần đây nhất được thực hiện trong thư mục ui/. Việc này đánh dấu việc kết thúc không chính thức của quá trình di chuyển này. Sau khi để bụi lắng xuống, chúng tôi chính thức đánh dấu quá trình di chuyển là hoàn tất vào ngày 5 tháng 3 năm 2020. 🎉

Giờ đây, tất cả các mô-đun trong Công cụ cho nhà phát triển đều sử dụng mô-đun JavaScript để chia sẻ mã. Chúng tôi vẫn đặt một số ký hiệu trên phạm vi toàn cầu (trong tệp module-legacy.js) cho các kiểm thử cũ hoặc để tích hợp với các phần khác của kiến trúc Công cụ cho nhà phát triển. Chúng sẽ bị xoá theo thời gian, nhưng chúng tôi không xem đó là yếu tố cản trở quá trình phát triển trong tương lai. Chúng tôi cũng có hướng dẫn quy tắc sử dụng mô-đun JavaScript.

Số liệu thống kê

Số liệu ước tính thận trọng cho số lượng CL (viết tắt của danh sách thay đổi – thuật ngữ dùng trong Gerrit để thể hiện sự thay đổi – tương tự như yêu cầu lấy dữ liệu từ GitHub) liên quan đến quá trình di chuyển này là khoảng 250 CL, phần lớn do 2 kỹ sư thực hiện. Chúng tôi không có số liệu thống kê chính xác về quy mô của các thay đổi được thực hiện nhưng số liệu ước tính thận trọng về các dòng đã thay đổi (tính bằng tổng chênh lệch tuyệt đối giữa các lượt chèn và xoá cho mỗi CL) là khoảng 30.000 (~20% toàn bộ mã giao diện người dùng cho Công cụ cho nhà phát triển).

Tệp đầu tiên sử dụng export được phát hành trong Chrome 79, được phát hành dưới dạng phiên bản ổn định vào tháng 12 năm 2019. Thay đổi gần đây nhất để di chuyển sang import được phát hành trong Chrome 83 (phát hành phiên bản chính thức vào tháng 5 năm 2020).

Chúng tôi đã biết về một phiên hồi quy được chuyển cho Chrome phiên bản ổn định và phiên bản đó đã được đưa vào trong quá trình di chuyển này. Tính năng tự động hoàn tất các đoạn mã trong trình đơn lệnh bị gián đoạn do xuất default không liên quan. Chúng tôi đã có một số lần hồi quy khác, nhưng bộ kiểm tra tự động và người dùng Chrome Canary của chúng tôi đã báo cáo và chúng tôi đã khắc phục chúng trước khi chúng có thể tiếp cận được người dùng Chrome ổn định.

Bạn có thể xem toàn bộ hành trình (không phải tất cả CL đều đính kèm với lỗi này, nhưng hầu hết trong số đó) được ghi lại trên crbug.com/1006759.

Điều chúng tôi học được

  1. Các quyết định được đưa ra trước đây có thể có tác động lâu dài đối với dự án của bạn. Mặc dù các mô-đun JavaScript (và các định dạng mô-đun khác) đã có sẵn trong một thời gian khá lâu, nhưng Công cụ cho nhà phát triển vẫn chưa có dấu hiệu phù hợp cho việc di chuyển. Việc quyết định thời điểm nên di chuyển và không nên di chuyển là điều khó khăn và dựa trên những phỏng đoán đã được rút ra.
  2. Ước tính thời gian ban đầu của chúng tôi là theo tuần thay vì tháng. Điều này phần lớn bắt nguồn từ việc chúng tôi phát hiện thấy nhiều vấn đề bất ngờ hơn dự kiến trong bản phân tích chi phí ban đầu. Mặc dù kế hoạch di chuyển này rất vững chắc, nhưng các món nợ kỹ thuật (thường xuyên hơn chúng tôi mong muốn) là yếu tố cản trở.
  3. Quá trình di chuyển các mô-đun JavaScript bao gồm một lượng lớn hoạt động dọn dẹp các khoản nợ kỹ thuật (dường như không liên quan). Việc chuyển sang định dạng mô-đun được chuẩn hoá hiện đại cho phép chúng tôi điều chỉnh các phương pháp lập trình hay nhất của mình với phương pháp phát triển web hiện đại. Ví dụ: chúng tôi có thể thay thế trình đóng gói Python tuỳ chỉnh bằng một cấu hình Rollup tối thiểu.
  4. Mặc dù có tác động lớn đến cơ sở mã của chúng tôi (khoảng 20% mã đã thay đổi), nhưng rất ít lượt hồi quy được báo cáo. Mặc dù chúng tôi gặp nhiều vấn đề khi di chuyển một số tệp đầu tiên, nhưng sau một thời gian, chúng tôi đã có quy trình làm việc vững chắc, một phần tự động. Điều này có nghĩa là quá trình di chuyển này không ảnh hưởng tiêu cực đến người dùng ổn định của chúng tôi.
  5. Việc giảng dạy những chi tiết phức tạp của một quá trình di chuyển cụ thể cho những nhà bảo trì đồng nghiệp là việc khó và đôi khi không thể thực hiện được. Quá trình di chuyển ở quy mô này rất khó thực hiện và đòi hỏi nhiều kiến thức về miền. Việc chuyển kiến thức miền đó cho những người khác làm việc trong cùng cơ sở mã là không mong muốn đối với công việc mà họ đang thực hiện. Biết được điều gì nên chia sẻ và những chi tiết nào không được chia sẻ là một nghệ thuật, nhưng là điều cần thiết. Do đó, điều quan trọng là phải giảm số lượng di chuyển lớn hoặc ít nhất không thực hiện chúng cùng một lúc.

Tải kênh xem trước xuống

Hãy cân nhắc sử dụng Chrome Canary, Dev hoặc Beta làm trình duyệt phát triển mặc định. Các kênh xem trước này cung cấp cho bạn quyền truy cập vào các tính năng mới nhất của Công cụ cho nhà phát triển, thử nghiệm API nền tảng web tiên tiến và tìm ra sự cố trên trang web của bạn trước khi người dùng của bạn làm điều đó!

Liên hệ với nhóm Công cụ của Chrome cho nhà phát triển

Sử dụng các lựa chọn sau đây để thảo luận về các tính năng mới và thay đổi trong bài đăng hoặc bất cứ vấn đề nào khác liên quan đến Công cụ cho nhà phát triển.

  • Hãy gửi đề xuất hoặc phản hồi cho chúng tôi qua crbug.com.
  • Báo cáo sự cố của Công cụ cho nhà phát triển bằng cách sử dụng phần Tuỳ chọn khác   Thêm   > Trợ giúp > Báo cáo sự cố về Công cụ cho nhà phát triển trong Công cụ cho nhà phát triển.
  • Tweet tại @ChromeDevTools.
  • Hãy để lại bình luận về tính năng mới trong video trên YouTube của Công cụ cho nhà phát triển hoặc video trên YouTube.