Một thư viện thiếu tính kiến trúc tổng thể

Một thư viện thiếu tính kiến trúc tổng thể

nohu52 đăng nhập,s6666 đăng nhập

BackboneJS

Ngày cập nhật: 2020-04-27 | Số lần xem: 21589 | Số từ: 2930 | Phân loại: BackboneJS
Tìm kiếm

Nếu bạn muốn sử dụng Backbone.js như một framework front-end chính, thì tốt nhất nên cân nhắc lại. Nếu ví AngularJS là một khẩu súng lục đầy sức mạnh và hiệu quả, thì Backbone.js chỉ có thể so sánh như một đoạn cành cây – không đủ sắc bén để chiến đấu hiệu quả. Bạn sẽ cần kết hợp nó với các framework xây dựng trên nền tảng BackboneJS (như Marionette) mới có thể nâng cao năng suất phát triển.

Models

Những thao tác nào nên đặt trong Model?

  • Thêm, xóa, sửa, tìm kiếm dữ liệu
  • Chuyển đổi kiểu dữ liệu
  • Kiểm soát quyền truy cập
  • Xác thực dữ liệu, ví dụ kiểm tra định dạng email
  • Tạo ra dữ liệu dùng để hiển thị, ví dụ ghép tên từ họ và tên thành tên đầy đủ

Các hàm cốt lõi của Backbone.js Model

  • extend: kế thừa từ Backbone.Model và mở rộng thêm các chức năng
  • set: thiết lập thuộc tính và đồng thời kích hoạt sự kiện change. Lưu ý sai lầm thường gặp là cố gắng lấy giá trị bằng cách gõ model.property, nhưng đúng cách phải là model.get('property')
  • validate: xác thực dữ liệu khi gọi save() hoặc set({ validate: true })

Một chi tiết ẩn giấu là sau khi this.model được xác thực thành công, phương thức sẽ trả về đối tượng đã được thiết lập; nếu thất bại, sẽ trả về false

1
2
3
4
var model = this.model.set(attrs, { validate: true });
if (!model) {
  console.log('attrs không hợp lệ');
}

Xử lý kết quả của model.save(), tham khảo “Làm thế nào để kích hoạt callback success khi gọi model.save()?”

1
2
3
4
5
6
7
8
this.model.save(null, {
  success: function(model, response) {
    console.log("Thành công");
  },
  error: function(model, response) {
    console.log("Lỗi");
  }
});
  • Bước đầu tiếp cận Backbone.js: Xác thực Model

Views

Views đóng vai trò như cầu nối giữa template và collection, là chất keo dính giữa dữ liệu backend và giao diện frontend. Có thể hình dung nó như người điều khiển trong vở kịch rối – giữ vai trò dẫn dắt mọi hành động diễn ra.
!](ảnh minh họa về nghệ thuật rối - Naruto)

Những thao tác nào nên đặt trong View?

  • Khi collection.add() thường được liên kết với render() của View
  • Xử lý các sự kiện người dùng như click chuột, nhấn phím
  • Tạo mới DOM element hoặc thao tác trên các phần tử hiện có trên trang

Sự khác biệt giữa View.elView.$el

  • el tương ứng với phần HTML thuần túy
  • $el là một đối tượng jQuery

Tránh việc render View bị trùng lặp

Trước khi render, hãy nhớ:

1
this.$el.empty();

Tham khảo bài viết: [Vấn đề về render duplicate item trong Backbone.js Collection View

Memory leak do Subview gây ra

Dưới đây là một giải pháp được chia sẻ trên Google Group:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var AppView = Backbone.View.extend({
  el: $("#core-element"),
  showView: function(view){
   if (this.currentView) {
    this.currentView.unbind();
    this.currentView.remove();
   }
   this.currentView = view;
   this.currentView.render();
   this.el.append(this.currentView.el);
  },
 });

 var ItemRouter = Backbone.Router.extend({
  formAddEditEl: $("#add-edit-form"),
  routes: {
   "": "index",
   "/new": "add",
  },
  initialize: function(options){
   this.collection = options.collection;
   this.appView = options.appView;
  },
  index: function(){
   var listView = new ListView({collection: this.collection});
   this.appView.showView(listView);
  },
  add: function(){
   var addEditView = new AddEditView({model: new Item()});
   this.appView.showView(addEditView);
  }
 });

Khi nào nên dùng tagName hay el để tạo View, chúng khác nhau ở điểm nào?

Tối ưu nhất là sử dụng tagName, vì như vậy bạn không cần quan tâm đến việc gắn ID cụ thể. Chỉ cần append vào View cha và khi Model có sự kiện change, bạn render lại View đó. Cách này giúp bạn phân tách View một cách rõ ràng hơn.

Collections

Là tập hợp các Models

  • Thiết lập URL RESTful cho backend
  • Thực hiện các thao tác CRUD (thêm, xóa, sửa, tìm kiếm)

Nên đặt sự kiện thay đổi của Collection ở đâu?

Sử dụng phương pháp loại trừ để phân tích:

  • Đặt trong initialize của Collection
    Nếu bạn gán View trực tiếp trong Collection, thì Collection đó sẽ khó tái sử dụng. Truyền View qua tham số cũng không phải là cách tối ưu, đặc biệt khi có nhiều View cùng cấp cần đồng bộ. → Không khả thi.

  • Đặt trong Collection View
    Khi khởi tạo Collection View, bạn truyền vào một instance Collection. Instance này có thể được định nghĩa trong toàn cục (app) hoặc trong AppView. → Khả thi.

Liệu có cần thiết phải có AppView?

Nếu bạn không sử dụng AppView, thì toàn bộ logic sẽ được quản lý bởi một object toàn cục, nơi chứa Collection, Collection View, và fetch dữ liệu từ backend. Việc này hoàn toàn khả thi mà không cần đến AppView.

Sự khác biệt giữa addcreate

Phương thức create sẽ kích hoạt add. Quy trình thực tế của create như sau:

  1. Gọi add (nếu có {wait: true} thì add sẽ được gọi sau khi server lưu trữ thành công)
  2. Gửi yêu cầu lên server

Ngược lại, add không gửi bất kỳ yêu cầu nào đến server.

Nhóm dữ liệu trong Collection

Việc phân nhóm theo category là phổ biến. Ví dụ: nhóm các Model trong Collection theo category, sau đó sắp xếp từng nhóm theo rank.

Làm thế nào để điền dữ liệu giả vào Collection trong giai đoạn đầu phát triển?

Với ứng dụng Todos, bạn có thể khởi tạo một Collection với dữ liệu mẫu trước khi fetch từ backend.

1
2
3
4
5
6
7
8
9
var Todos = Backbone.Collection.extend({
  model: Todo,
  url: '/api/todos',
});
var todos = new Todos([
  {title: 'coding'},
  {title: 'running'}
]);
new TodosView(todos);

Sau khi API backend sẵn sàng, bạn chuyển sang:

1
2
3
var todos = new Todos();
todos.fetch();
new TodosView(todos);

Các sự kiện thường gặp

Trong View, lắng nghe sự kiện từ Model

1
2
this.listenTo(model, 'change:name', this.changeName); // Khi thuộc tính name thay đổi
this.listenTo(model, 'change', this.change);         // Khi bất kỳ thuộc tính nào thay đổi

Trong View, lắng nghe sự kiện từ Collection

1
2
3
4
this.listenTo(collection, 'change', this.addOne);      // Khi thuộc tính của model trong collection thay đổi
this.listenTo(collection, 'add', this.addOne);         // Khi model được thêm vào collection
this.listenTo(collection, 'remove', this.removeOne);   // Khi model bị xóa khỏi collection
this.listenTo(collection, 'reset', this.render);       // Khi gọi `collection.fetch({ reset: true })`

Sự kiện liên quan đến HTTP request (chưa test thành công)

1
2
3
request // Khi bắt đầu request
sync    // Khi request thành công
error   // Khi request thất bại

Lắng nghe sự kiện trên các phần tử trong template

1
2
3
4
5
events: {
 "click #commitBtn": 'commitInfo',  
 'mouseover .title': 'mouseoverTitle', 
 'keypress #commitForm': 'commitInfoOnEnter',  // function(e) { if (e.which === 13) { this.commitInfo(); }}
}

Tham khảo: Danh mục sự kiện - Tài liệu chính thức Backbone

Sự khác biệt giữa onlistenTo

Về mặt cú pháp:

  • on dùng để lắng nghe sự kiện trên chính object đó

object.on(event, callback, [context])

  • listenTo dùng để lắng nghe sự kiện từ object khác

object.listenTo(other, event, callback)

Ứng dụng:

  • listenTo phù hợp khi View cần lắng nghe sự kiện từ Model/Collection
  • on phù hợp khi Model cần tự lắng nghe sự kiện thay đổi của chính nó

Ví dụ trong TodosView, ta có thể thấy:

1
2
3
4
5
this.listenTo(app.todos, 'add', this.addOne);
this.listenTo(app.todos, 'reset', this.addAll);
this.listenTo(app.todos, 'change:completed', this.filterOne);
this.listenTo(app.todos, 'filter', this.filterAll);
this.listenTo(app.todos, 'all', this.render);

Nếu lắng nghe sự kiện bên trong Collection, thì:

1
this.on('change:completed', this.filterOne);

Tham khảo: Lắng nghe sự kiện Collection add trong Backbone

Phân tách View

Không có hướng dẫn cụ thể nào nói đến cách tách View trong Backbone. Theo kinh nghiệm từ AngularJS, việc tách nhỏ View càng nhiều càng tốt để dễ viết unit test.

Dù là AngularJS hay BackboneJS, yếu tố then chốt để tăng năng suất và giảm chi phí bảo trì là việc chia nhỏ View/controller. Mỗi khu vực trên thiết kế giao diện nên được tách riêng thành một View/controller độc lập.

So với AngularJS, BackboneJS đòi hỏi bạn phải thủ công thêm các ràng buộc DOM, làm tăng chi phí đặt ID, class cho các phần tử – khá phiền toái. Hãy thử tìm cách đơn giản hơn nếu có thể.

Tất cả các View đều chịu trách nhiệm cho một lượng logic nhất định. Làm sao tổ chức những thứ này một cách hợp lý?

Để tổ chức tốt logic này, ta có thể áp dụng mô hình Element Controller Pattern. Mô hình này gồm hai View: một View điều khiển tập hợp các phần tử, và một View xử lý từng phần tử riêng lẻ.

Tôi gọi mô hình này là “mô hình nhà thầu tổng”, tức là một nhà thầu lớn điều phối các nhà thầu nhỏ, mỗi nhà thầu nhỏ lại điều phối các kỹ thuật viên chuyên môn. Trước đây tôi vẫn gọi nó là “mô hình tách View”, nhưng cảm thấy cái tên này chưa đủ sinh động để phản ánh đúng bản chất phân cấp, nên đổi thành “mô hình nhà thầu tổng”.

Ví dụ thực tế: AppView là View ngoài cùng, gắn vào một phần tử trong file index thông qua el: {id}, và bên trong AppView lại chứa các View con với chức năng cụ thể.

  • BACKBONE.VIEW PATTERNS – CÁCH VÀ LÝ DO SỬ DỤNG SUBVIEW
  • Cách render và append subview trong Backbone.js

Một View muốn kích hoạt sự kiện render của View khác, có nên nghe sự kiện trực tiếp không?

Các giải pháp có thể:

  • Nên nghe sự kiện từ Collection (xem phần Events)
  • Sử dụng cơ chế publish-subscribe toàn cục

Unit Testing

Làm thế nào để tránh dùng biến global?

Sự khác biệt lớn giữa BackboneJS và AngularJS nằm ở chỗ tài liệu chính thức của BackboneJS không nhấn mạnh tầm quan trọng của unit testing, và không có cơ chế kiến trúc ép buộc bạn viết code dễ test.

  • Kiểm thử ứng dụng Backbone với Jasmine và Sinon – Phần 1

Sử dụng SeaJS / RequireJS để modular hóa mã Frontend MVC

Khái niệm modular là gì?
Bạn có thể hiểu như các module tiêu chuẩn trong Python như datetime, time, os, tức là mỗi chức năng được tách riêng thành một file JS.

Ưu điểm của modular so với cách viết JS truyền thống:

  • Rõ ràng về mối phụ thuộc giữa các file JS. Ví dụ: một trang web có 2 file JS là jQuery.jsforum.js, tuy nhiên không thể nhìn vào forum.js để biết nó phụ thuộc vào jQuery.js.
  • Tải về theo nhu cầu, không tải tất cả như cách truyền thống.
  • Không cần lo lắng về thứ tự load các file JS, vì khi số lượng file tăng lên, việc quản lý thứ tự trở nên phức tạp.

Vì SeaJS do người Việt phát triển, tài liệu tiếng Anh còn hạn chế, nên tạm thời sử dụng RequireJS (gần như tất cả tutorial BackboneJS đều giới thiệu).

Vậy frontend MVC sử dụng RequireJS như thế nào?
Giống như trong Django, views.py sẽ import models.py, thì trong BackboneJS, View cũng sẽ import Model, và đương nhiên cũng dùng tới jQuery, backbone, underscore.

Vấn đề khi dùng AJAX trong Backbone

Khi sử dụng this.<method_name> trong success, done, hoặc error của AJAX luôn báo lỗi, rằng phương thức đó không tồn tại. Lý do là this trong scope nội tại của AJAX không còn là Backbone Object nữa. Giải pháp là khai báo một biến that bên ngoài:

1
2
3
4
5
6
7
8
var that = this;

$.ajax({
  ...
  success: function(data) {
    that.<method_name>();
  }
});

Trong các callback của $.ajax, this sẽ là object được truyền vào option context. Nếu không có, this sẽ là settings của AJAX.

  • Vấn đề callback trong Backbone.Model và THIS
  • Cách sử dụng this trong JavaScript
  • 5 cách gọi hàm trong JavaScript
  • Hiểu về _.each trong Backbone Collection

Tại sao Render trong View không được gọi chủ động?

TODO

Router và AppView ngoài cùng hoạt động như thế nào?

AppView là View cấp cao nhất, là container chính của toàn bộ ứng dụng. Để hiểu rõ cách Router và AppView hoạt động song song, cần nắm rõ vai trò của từng thành phần.

Backbone là một framework rất linh hoạt, cùng một chức năng có thể có nhiều cách triển khai khác nhau. Với tôi, AppView là nơi chứa namespace, các biến toàn cục dùng chung, và là gốc rễ của toàn bộ ứng dụng.

Giả sử ứng dụng có 2 View là HomeViewAboutView, với đường dẫn tương ứng là #home#about. Mặc định sẽ vào HomeView. Cả hai View này đều là con của AppView.

AppView cần lắng nghe sự kiện click để quyết định chuyển đến View nào, sau đó gọi router.navigate() để điều hướng. Do đó, việc đặt Router bên trong AppView là hợp lý.

Vấn đề nảy sinh là: Trong handler của Router, mình cần làm gì?

Nếu đang khởi tạo HomeView hoặc AboutView, điều kiện tiên quyết là AppView đã render xong. Điều này không khó, bạn có thể render AppView trước rồi mới khởi tạo Router. Còn việc render HomeViewAboutView sẽ dựa vào một vị trí cụ thể trong AppView.

Nếu HomeView chứa 3 subview ngang hàng, thì việc điều hướng trong handler của Router sẽ phức tạp hơn. Tuy nhiên, không cần dùng sub-router để xử lý điều này. Thay vào đó, mỗi subview có thể tự kiểm tra xem HomeView đã được khởi tạo chưa. Nếu chưa, thì khởi tạo. Quá trình này có thể thực hiện trong handler của Router. Đồng thời, HomeView cần hỗ trợ truyền tham số để xác định subview mặc định, nhằm tránh hiện tượng “giật”.

Cách điều hướng từ subview sang subview khác:

1
2
3
// trigger: có kích hoạt handler trong router không?
// replace: có ghi đè lịch sử trình duyệt không?
appView.router.navigate("pay", {trigger: true, replace: true});
  • Sử dụng Backbone Router để điều hướng giữa các View được modular bằng RequireJS
  • Subroute theo module trong Backbone

Sử dụng Mustache hay Underscore Template của Backbone?

Hiện tại mình chưa gặp vấn đề gì khi sử dụng Underscore Template tích hợp sẵn trong Backbone.

  • Template trong Backbone View

Nếu sử dụng:

1
<a href="#">...</a>

sẽ ảnh hưởng đến routing của Backbone. Thay vào đó, nên dùng:

1
<a href="javascript:void(0)">...</a>

Tài liệu

  • Developing Backbone.js Applications
  • Trang chủ Backbone.js
  • Backbone Views Using Mustache Templates
  • Gợi ý sự kiện giữa các View trong Backbone.js
  • Tips và Patterns trong Backbone.js
  • Web Framework Backbone.Marionette
comments powered by Disqus
Built with Hugo
Theme Stack thiết kế bởi Jimmy