Giao tiếp giữa các thành phần tách rời bằng cách sử dụng các sự kiện


8

Chúng tôi đã có một Ứng dụng web nơi chúng tôi có rất nhiều (> 50) ít WebComponents tương tác với nhau.

Để giữ mọi thứ tách rời, chúng tôi có một quy tắc là không thành phần nào có thể trực tiếp tham chiếu đến cái khác. Thay vào đó, các thành phần kích hoạt các sự kiện mà sau đó (trong ứng dụng "chính") có dây để gọi các phương thức của thành phần khác.

Khi thời gian trôi qua, ngày càng có nhiều thành phần được thêm vào và tệp ứng dụng "chính" trở nên ngổn ngang với các đoạn mã trông như thế này:

buttonsToolbar.addEventListener('request-toggle-contact-form-modal', () => {
  contactForm.toggle()
})

buttonsToolbar.addEventListener('request-toggle-bug-reporter-modal', () => {
  bugReporter.toggle()
})

// ... etc

Để cải thiện điều này, chúng tôi đã nhóm chức năng tương tự lại với nhau, trong một Class, đặt tên cho nó một cái gì đó có liên quan, vượt qua các yếu tố tham gia khi khởi tạo và xử lý "hệ thống dây điện" trong Class, như vậy:

class Contact {
  constructor(contactForm, bugReporter, buttonsToolbar) {
    this.contactForm = contactForm
    this.bugReporterForm = bugReporterForm
    this.buttonsToolbar = buttonsToolbar

    this.buttonsToolbar
      .addEventListener('request-toggle-contact-form-modal', () => {
        this.toggleContactForm()
      })

    this.buttonsToolbar
      .addEventListener('request-toggle-bug-reporter-modal', () => {
        this.toggleBugReporterForm()
      })
  }

  toggleContactForm() {
    this.contactForm.toggle()
  }

  toggleBugReporterForm() {
    this.bugReporterForm.toggle()
  }
}

và chúng tôi khởi tạo như vậy:

<html>
  <contact-form></contact-form>
  <bug-reporter></bug-reporter>

  <script>
    const contact = new Contact(
      document.querySelector('contact-form'),
      document.querySelector('bug-form')
    )
  </script>
</html>

Tôi thực sự không thích giới thiệu các mẫu của riêng mình, đặc biệt là các mẫu không thực sự OOP-y vì tôi chỉ sử dụng Classesnhư các thùng chứa khởi tạo, vì không có từ nào tốt hơn.

Có một mẫu được xác định tốt hơn / được biết đến nhiều hơn để xử lý loại nhiệm vụ này mà tôi đang thiếu không?


2
Điều này thực sự trông nhẹ nhàng tuyệt vời.
Robert Harvey

Khi tôi nhớ chính xác, trong câu hỏi trước đây của bạn, bạn đã gọi nó là hòa giải viên, cũng là tên mẫu từ sách GoF, vì vậy đây chắc chắn là mẫu OOP.
Doc Brown

@RobertHarvey Vâng, từ của bạn có rất nhiều trọng lượng đối với tôi; Bạn sẽ làm điều đó khác đi? Tôi không chắc là tôi đang xem xét lại điều này.
Nik Kyriakides

2
Đừng lật đổ điều này. Lớp "nối dây" của bạn có vẻ RẮN với tôi, nếu nó hoạt động và bạn hài lòng với cái tên đó, thì nó không quan trọng như thế nào.
Doc Brown

1
@NicholasKyriakides: tốt hơn hãy hỏi một trong những đồng nghiệp của bạn (người chắc chắn hiểu rõ hệ thống hơn tôi) để biết tên tốt chứ không phải người lạ như tôi từ internet.
Doc Brown

Câu trả lời:


6

Mã bạn có là khá tốt. Điều có vẻ hơi khó chịu là mã khởi tạo không phải là một phần của chính đối tượng. Đó là, bạn có thể khởi tạo một đối tượng, nhưng nếu bạn quên gọi lớp dây của nó, nó vô dụng.

Hãy xem xét một Trung tâm thông báo (còn gọi là Event Bus) được định nghĩa như thế này:

class NotificationCenter(){
    constructor(){
        this.dictionary = {}
    }
    register(message, callback){
        if not this.dictionary.contains(message){
            this.dictionary[message] = []
        }
        this.dictionary[message].append(callback)
    }
    notify(message, payload){
        if this.dictionary.contains(message){
            for each callback in this.dictionary[message]{
                callback(payload)
            }
        }
    }
}

Đây là một xử lý sự kiện nhiều công văn DIY. Sau đó, bạn có thể thực hiện nối dây của riêng mình bằng cách chỉ cần yêu cầu Thông báo hóa làm đối số của hàm tạo. Gửi tin nhắn vào đó và chờ nó chuyển tải trọng của bạn là liên hệ duy nhất bạn có với hệ thống, vì vậy nó rất RẮN.

class Toolbar{
    constructor(notificationCenter){
        this.NC = notificationCenter
        this.NC.register('request-toggle-contact-form-modal', (payload) => {
            this.toggleContactForm(payload)
          }
    }
    toolbarButtonClicked(e){
        this.NC.notify('toolbar-button-click-event', e)
    }
}

Lưu ý: Tôi đã sử dụng chuỗi ký tự chuỗi tại chỗ cho các khóa để phù hợp với kiểu được sử dụng trong câu hỏi và để đơn giản. Điều này là không nên do rủi ro lỗi chính tả. Thay vào đó, hãy xem xét sử dụng một phép liệt kê hoặc hằng chuỗi.

Trong đoạn mã trên, Thanh công cụ có trách nhiệm cho NotificationCenter biết loại sự kiện nào nó quan tâm và xuất bản tất cả các tương tác bên ngoài của nó thông qua phương thức thông báo. Bất kỳ lớp nào khác quan tâm đến toolbar-button-click-eventsẽ chỉ cần đăng ký nó trong hàm tạo của nó.

Các biến thể thú vị trên mẫu này bao gồm:

  • Sử dụng nhiều NC để xử lý các phần khác nhau của hệ thống
  • Có phương thức Notify sinh ra một luồng cho mỗi thông báo, thay vì chặn ser seri
  • Sử dụng danh sách ưu tiên thay vì danh sách thông thường bên trong NC để đảm bảo đặt hàng từng phần mà thành phần nào được thông báo trước
  • Đăng ký trả lại ID có thể được sử dụng cho Unregister sau
  • Bỏ qua đối số tin nhắn và chỉ gửi đi dựa trên lớp / loại tin nhắn

Các tính năng thú vị bao gồm:

  • Thiết bị NC dễ dàng như đăng ký logger để in tải trọng
  • Kiểm tra một hoặc nhiều thành phần tương tác chỉ đơn giản là vấn đề khởi tạo chúng, thêm người nghe cho kết quả mong đợi và gửi tin nhắn
  • Thêm các thành phần mới nghe tin nhắn cũ là chuyện nhỏ
  • Thêm các thành phần mới gửi tin nhắn đến những cái cũ là chuyện nhỏ

Gotchas thú vị và biện pháp khắc phục có thể bao gồm:

  • Các sự kiện kích hoạt các sự kiện khác có thể gây nhầm lẫn.
    • Bao gồm ID người gửi trong sự kiện để xác định nguồn gốc của sự kiện không mong muốn.
  • Mỗi thành phần không biết liệu có bất kỳ phần nhất định nào của hệ thống hoạt động hay không trước khi nhận được một sự kiện, vì vậy các tin nhắn sớm có thể bị loại bỏ.
    • Điều này có thể được xử lý bằng mã tạo ra các thành phần gửi thông điệp 'hệ thống sẵn sàng', tất nhiên các thành phần quan tâm sẽ cần phải đăng ký.
  • Bus sự kiện tạo ra một giao diện ngụ ý giữa các thành phần, có nghĩa là không có cách nào để trình biên dịch chắc chắn rằng bạn đã triển khai mọi thứ bạn nên.
    • Các đối số tiêu chuẩn giữa tĩnh và động áp dụng ở đây.
  • Cách tiếp cận này nhóm các thành phần với nhau, không nhất thiết phải hành vi. Theo dõi các sự kiện thông qua hệ thống có thể yêu cầu nhiều công việc ở đây hơn phương pháp của OP. Ví dụ, OP có thể có tất cả các trình nghe liên quan đến tiết kiệm được thiết lập cùng nhau và các trình nghe liên quan đến xóa được thiết lập cùng nhau ở nơi khác.
    • Điều này có thể được giảm thiểu bằng cách đặt tên và tài liệu sự kiện tốt như biểu đồ dòng chảy. (Có, tài liệu nổi tiếng không phù hợp với mã). Bạn cũng có thể thêm các danh sách xử lý trước và sau bắt tất cả các tin nhắn và in ra ai đã gửi những gì theo thứ tự.

Cách tiếp cận này có vẻ gợi nhớ đến kiến ​​trúc Event Bus. Sự khác biệt duy nhất là đăng ký của bạn là một chuỗi chủ đề chứ không phải là một loại tin nhắn. Điểm yếu chính của việc sử dụng các chuỗi là chúng có thể bị nhầm lẫn - có nghĩa là thông báo hoặc người nghe có thể bị viết sai và rất khó để gỡ lỗi.
Berin Loritsch

Theo như tôi có thể nói đây là một ví dụ kinh điển về Người hòa giải . Một vấn đề với cách tiếp cận này là nó kết hợp một thành phần với Tổ chức sự kiện / Người hòa giải. Điều gì xảy ra nếu tôi muốn di chuyển một thành phần, ví dụ: buttonsToolbardự án khác không sử dụng Event Bus?
Nik Kyriakides

+1 Lợi ích của hòa giải viên là cho phép bạn đăng ký chống lại chuỗi / enum và có khớp nối lỏng lẻo trong lớp. Nếu bạn di chuyển hệ thống dây điện bên ngoài đối tượng đến lớp chính / thiết lập thì nó sẽ biết về tất cả các đối tượng và có thể kết nối chúng trực tiếp với các sự kiện / chức năng mà không phải lo lắng về việc ghép nối. @NicholasKyriakides Chọn một hoặc khác thay vì cố gắng sử dụng cả hai
Ewan

Với kiến ​​trúc bus sự kiện cổ điển, khớp nối duy nhất là chính thông điệp. Thông điệp thường là một đối tượng bất biến. Đối tượng gửi tin nhắn chỉ cần giao diện nhà xuất bản để gửi tin nhắn. Nếu bạn sử dụng loại đối tượng tin nhắn thì bạn chỉ cần xuất bản đối tượng tin nhắn. Nếu bạn sử dụng một chuỗi, bạn phải cung cấp cả chuỗi chủ đề và tải trọng thông báo (trừ khi chuỗi là tải trọng). Sử dụng chuỗi có nghĩa là bạn chỉ cần tỉ mỉ về các giá trị ở cả hai bên.
Berin Loritsch

@NicholasKyriakides Điều gì xảy ra nếu bạn di chuyển mã gốc của mình sang một giải pháp mới? Bạn phải mang theo lớp thiết lập của bạn và thay đổi nó cho bối cảnh mới của nó. Điều tương tự áp dụng cho mô hình này.
Joel Harmon

3

Tôi đã từng giới thiệu một "xe buýt sự kiện" nào đó và trong những năm sau đó, tôi đã bắt đầu phụ thuộc ngày càng nhiều vào chính Mô hình Đối tượng Tài liệu để truyền đạt các sự kiện cho mã UI.

Trong trình duyệt, DOM là một phụ thuộc luôn có mặt - ngay cả trong khi tải trang. Điều quan trọng là sử dụng các sự kiện tùy chỉnh trong JavaScript và dựa vào sự kiện sôi nổi để truyền đạt các sự kiện đó.

Trước khi mọi người bắt đầu hét lên về việc "chờ tài liệu sẵn sàng" trước khi đính kèm người đăng ký, thuộc document.documentElementtính tham chiếu <html>phần tử từ thời điểm JavaScript bắt đầu thực thi, bất kể tập lệnh được nhập ở đâu hoặc thứ tự xuất hiện trong đánh dấu của bạn.

Đây là nơi bạn có thể bắt đầu nghe các sự kiện.

Rất phổ biến khi có một thành phần JavaScript (hoặc widget) nằm trong một thẻ HTML nhất định trên trang. Phần tử "gốc" của thành phần là nơi bạn có thể kích hoạt các sự kiện sủi bọt của mình. Người đăng ký vào thành <html>phần sẽ nhận được các thông báo này giống như bất kỳ sự kiện nào khác do người dùng tạo.

Chỉ cần một số ví dụ mã tấm nồi hơi:

(function (window, document, html) {
    html.addEventListener("custom-event-1", function (event) {
        // ...
    });
    html.addEventListener("custom-event-2", function (event) {
        // ...
    });

    function someOperation() {
        var customData = { ... };
        var event = new CustomEvent("custom-event-3", { detail : customData });

        event.dispatchEvent(componentRootElement);
    }
})(this, this.document, this.document.documentElement);

Vì vậy, mô hình trở thành:

  1. Sử dụng sự kiện tùy chỉnh
  2. Theo dõi các sự kiện này trên document.documentElementtài sản (không cần phải đợi tài liệu sẵn sàng)
  3. Xuất bản các sự kiện trên một thành phần gốc cho thành phần của bạn hoặc document.documentElement.

Điều này sẽ làm việc cho cả cơ sở mã hướng chức năng và đối tượng.


1

Tôi sử dụng phong cách tương tự với phát triển trò chơi video của mình với Unity 3D. Tôi tạo các thành phần như Sức khỏe, Đầu vào, Chỉ số, Âm thanh, v.v. và thêm chúng vào Đối tượng Trò chơi để xây dựng đối tượng trò chơi đó là gì. Unity đã có cơ chế để thêm các thành phần vào các đối tượng trò chơi. Tuy nhiên, những gì tôi tìm thấy là hầu hết mọi người đều truy vấn các thành phần hoặc tham chiếu trực tiếp các thành phần bên trong các thành phần khác (ngay cả khi chúng sử dụng giao diện, nó vẫn được kết nối nhiều hơn cảm ơn tôi thích). Tôi muốn các thành phần có thể được tạo ra một cách cô lập với sự phụ thuộc bằng không của bất kỳ thành phần nào khác. Vì vậy, tôi đã có các sự kiện cháy thành phần khi dữ liệu thay đổi (cụ thể cho thành phần) và các phương thức khai báo để thay đổi dữ liệu về cơ bản. Sau đó, đối tượng trò chơi tôi đã tạo một lớp cho và dán tất cả các sự kiện thành phần vào các phương thức thành phần khác.

Điều tôi thích ở đây là để xem tất cả các tương tác của các thành phần cho một đối tượng trò chơi, tôi chỉ có thể nhìn vào lớp 1 này. Nghe có vẻ như lớp Liên hệ của bạn rất giống với các lớp Game Object của tôi (tôi đặt tên các đối tượng trò chơi cho đối tượng mà chúng phải giống như MainPlayer, Orc, v.v.).

Các lớp này là loại lớp quản lý. Bản thân chúng không thực sự có bất cứ thứ gì ngoại trừ các thành phần và mã để nối chúng. Tôi không chắc tại sao bạn tạo các phương thức ở đây mà chỉ gọi các phương thức thành phần khác khi bạn có thể nối chúng trực tiếp. Điểm của lớp này thực sự chỉ là tổ chức sự kiện kết nối.

Là một lưu ý phụ cho đăng ký sự kiện của tôi, tôi đã thêm một cuộc gọi lại bộ lọc và gọi lại cuộc gọi. Khi sự kiện được kích hoạt (tôi đã tạo lớp sự kiện tùy chỉnh của riêng mình), nó sẽ gọi hàm gọi lại bộ lọc nếu nó tồn tại và nếu nó trả về đúng thì nó sẽ chuyển sang gọi lại args. Điểm của cuộc gọi lại bộ lọc là để linh hoạt. Một sự kiện có thể kích hoạt vì nhiều lý do nhưng tôi chỉ muốn gọi sự kiện kết nối của mình nếu kiểm tra là đúng. Một ví dụ có thể là thành phần Input có sự kiện OnKeyHit. Nếu tôi có thành phần Chuyển động có các phương thức như MoveForward () MoveBackward (), v.v. Tôi có thể kết nối OnKeyHit + = MoveForward nhưng rõ ràng tôi sẽ không muốn tiến lên với bất kỳ lần nhấn phím nào. Tôi chỉ muốn làm điều đó nếu khóa là khóa 'w'. Vì OnKeyHit đang điền vào các đối số để vượt qua và một trong số đó là khóa được nhấn,

Đối với tôi, đăng ký cho một lớp trình quản lý đối tượng trò chơi cụ thể trông giống như:

input.OnKeyHit.Subscribe(movement.MoveForward, (args) => { return args.Key == 'w' });

Bởi vì các thành phần có thể được phát triển một cách cô lập, nhiều lập trình viên có thể đã mã hóa chúng. Với ví dụ trên, bộ mã hóa đầu vào đã cho đối tượng đối số một biến có tên là Key. Tuy nhiên, nhà phát triển thành phần Movement có thể đã không sử dụng Key (nếu cần xem xét các đối số, trong trường hợp này có thể không nhưng trong các trường hợp khác, họ sử dụng các giá trị đối số được truyền). Để loại bỏ yêu cầu giao tiếp này, cuộc gọi lại args hoạt động như một ánh xạ cho các đối số giữa các thành phần. Vì vậy, người tạo ra lớp quản lý đối tượng trò chơi này là người chỉ cần biết tên biến arg giữa 2 máy khách khi họ đi dây chúng lên và thực hiện ánh xạ tại điểm đó. Phương pháp này được gọi sau hàm lọc.

input.OnKeyHit.Subscribe(movement.MoveForward, (args) => { return args.Key == 'w' }, (args) => { args.keyPressed = args.Key });

Vì vậy, trong tình huống trên, Người đầu vào đã đặt tên một biến bên trong đối tượng args 'Khóa' nhưng Phong trào đặt tên là 'keyPress'. Điều này giúp tiếp tục cách ly giữa các thành phần khi chúng đang được phát triển và đưa nó vào trình triển khai của lớp trình quản lý để kết nối chính xác.


0

Để biết giá trị của nó, tôi đang làm một cái gì đó như là một phần của dự án phụ trợ và đã thực hiện một cách tiếp cận tương tự:

  • Hệ thống của tôi không liên quan đến các vật dụng (các thành phần web) mà là các "bộ điều hợp" trừu tượng, trong đó xử lý các giao thức khác nhau.
  • Một giao thức được mô hình hóa như một tập hợp các 'cuộc hội thoại' có thể. Bộ điều hợp giao thức kích hoạt các cuộc hội thoại này tùy thuộc vào một sự kiện đến.
  • Có một Bus sự kiện về cơ bản là một Chủ đề Rx.
  • Bus sự kiện đăng ký vào đầu ra của tất cả các bộ điều hợp và tất cả các bộ điều hợp đều đăng ký vào đầu ra của Bus sự kiện.
  • 'Bộ điều hợp' được mô hình hóa thành luồng tổng hợp của tất cả các 'cuộc hội thoại' của nó. Cuộc hội thoại là một luồng được đăng ký vào đầu ra của xe buýt sự kiện, tạo thông báo đến xe buýt sự kiện, được điều khiển bởi Máy trạng thái.

Làm thế nào tôi xử lý các thách thức xây dựng / hệ thống dây điện của bạn:

  • Một giao thức (được thực hiện bởi bộ điều hợp) xác định tiêu chí bắt đầu cuộc hội thoại là các bộ lọc qua luồng đầu vào mà nó được đăng ký. Trong C # đây là các truy vấn LINQ qua các luồng. Trong ReactJS, đây sẽ là các toán tử .Where hoặc .ilter.
  • Một cuộc hội thoại quyết định một thông điệp có liên quan bằng cách sử dụng các bộ lọc của chính nó.
  • Nói chung, bất cứ điều gì được đăng ký vào xe buýt là một luồng và xe buýt được đăng ký vào các luồng đó.

Sự tương tự với thanh công cụ của bạn:

  • Lớp thanh công cụ là .Map của một đầu vào có thể quan sát được (bus), có thể quan sát được các sự kiện thanh công cụ, mà bus được đăng ký
  • Một thanh công cụ có thể quan sát được (nếu bạn có nhiều thanh công cụ phụ) có nghĩa là bạn có thể có nhiều vật quan sát, vì vậy thanh công cụ của bạn có thể quan sát được. Đây sẽ là RxJs .Merge'd thành một đầu ra duy nhất cho xe buýt.

Các vấn đề bạn có thể gặp phải:

  • Đảm bảo rằng các sự kiện không theo chu kỳ và treo quá trình.
  • Đồng thời (không biết điều này có phù hợp với WebComponents không): đối với các hoạt động không đồng bộ hoặc hoạt động có thể chạy lâu, trình xử lý sự kiện của bạn có thể chặn luồng có thể quan sát được nếu không chạy dưới dạng tác vụ nền. Bộ lập lịch RxJS có thể giải quyết vấn đề này (theo mặc định bạn có thể .ObserveTrên một bộ lập lịch mặc định cho tất cả các đăng ký xe buýt chẳng hạn)
  • Các kịch bản phức tạp hơn không thể được mô hình hóa mà không có một số khái niệm về cuộc hội thoại (ví dụ: xử lý một sự kiện bằng cách gửi tin nhắn và chờ phản hồi, bản thân nó là một sự kiện). Trong trường hợp này, một máy trạng thái rất hữu ích để tự động chỉ định các sự kiện bạn muốn xử lý (cuộc hội thoại trong mô hình của tôi). Tôi làm điều này bằng cách có luồng hội thoại .filtertheo trạng thái (thực ra, việc triển khai có nhiều chức năng hơn - cuộc hội thoại là một bản đồ phẳng của các vật quan sát từ một sự kiện thay đổi trạng thái có thể quan sát được).

Vì vậy, tóm lại, bạn có thể xem toàn bộ miền vấn đề của mình dưới dạng có thể quan sát được, hoặc 'theo chức năng' / 'khai báo' và coi các thành phần web của bạn như các luồng sự kiện, như các quan sát, xuất phát từ xe buýt (có thể quan sát), mà xe buýt ( người quan sát) cũng được đăng ký. Khởi tạo các vật thể quan sát (ví dụ: một thanh công cụ mới) là khai báo, trong đó toàn bộ quá trình có thể được xem như là một vật thể quan sát được .maptừ luồng đầu vào.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.