5 câu hỏi lập trình viên .NET/Java đang học JavaScript và Node.js thường gặp

11884

Bài viết này của Harry Cummings, tác giả của cuốn sách Learning Node.js for .NET Developers dành cho những người có kinh nghiệm phát triển web trên .NET hay Java, những người đã từng viết JavaScript dựa trên các trình duyệt. Không rõ vì lý do gì mọi người lại muốn đưa JavaScript ra khỏi phạm vị trình duyệt và coi nó như một ngôn ngữ lập trình đa năng. Tuy nhiên, đây chính xác là những gì Node.js có thể làm.

Node.js đã xuất hiện đủ lâu để trưởng thành như một platform, và duy trì được sử phổ biến ấn tượng trong giới lập trình hơn bất kỳ thời điểm nào về một công nghệ mới.

Trong bài giới thiệu này, chúng ta sẽ tìm hiểu tại sao Node.js là một ngôn ngữ hấp dẫn đáng học, giải quyết một số rào cản phổ biến hiện nay và những nhầm lẫn mà các dev gặp phải khi học Node.js và JavaScript.

Tại sao sử dụng Node.js?

Mô hình thực thi của Node.js tương ứng với code JavaScript trong trình duyệt. Điều này không phải là một sự lựa chọn rõ ràng hoàn hảo cho việc phát triển server-side. Trên thực tế, hai trường hợp sử dụng này có một điểm chung rất quan trọng. Code user interface là event-driven tự nhiên (ví dụ: binding event handlers cho các sự kiện click của button). Node.js thực hiện việc này bằng cách áp dụng event-driven để lập trình server-side.

Node.js được công bố chính thức có mô hình thực thi theo single-threaded, non-blocking, event-driven. Chúng ta sẽ định nghĩa từng cụm từ này.

Non-blocking

Nói một cách đơn giản, Node.js nhận ra rằng nhiều chương trình dành phần lớn thời gian để chờ đợi. Ví dụ, một hoạt động I/O khá chậm như truy cập đĩa và yêu cầu network. Node.js giải quyết vấn đề này bằng cách làm cho các thao tác này không bị chặn (non-blocking). Điều này có nghĩa là việc thực hiện chương trình có thể tiếp tục diễn ra trong khi nó đang làm việc.

Cách tiếp cận non-blocking còn được gọi là lập trình không đồng bộ. Tất nhiên, các nền tảng khác đều hỗ trợ điều này (ví dụ, async của C # / từ khóa await và thư viện Task Parallel). Tuy nhiên, trong Node.js nó được xử lí theo cách làm đơn giản và tự nhiên nhất cho việc sử dụng. Các phương thức API không đồng bộ được gọi theo cùng một cách: Tất cả chúng đều lấy một hàm callback để gọi (“called back“) khi quá trình thực hiện hoàn tất. Hàm này được gọi với một tham số lỗi tùy chọn và kết quả của tác vụ này.

Tính nhất quán khi gọi các phương thức API non-blocking (không đồng bộ) trong Node.js truyền tới các thư viện của bên thứ ba. Tính nhất quán này giúp bạn dễ dàng tạo các ứng dụng không đồng bộ.

Các thư viện JavaScript khác, chẳng hạn như bluebird ( http://bluebirdjs.com/docs/getting-started.html ), cho phép callback-based APIs thích nghi với các patterns không đồng bộ khác. Là một sự thay thế cho callbacks, bạn có thể chọn Promises (tương tự như Tasks trong .NET hoặc Futures trong Java) hoặc coroutines (tương tự như các phương thức async trong C #) trong codebase của chính bạn. Điều này cho phép bạn sắp xếp code trong khi vẫn giữ được lợi ích của các API không đồng bộ nhất quán trong Node.js và các thư viện ngoài.

Event-driven

Bản chất event-driven của Node.js mô tả các hoạt động được sắp xếp như thế nào. Trong các môi trường lập trình thủ tục điển hình, mỗi chương trình có một số điểm thực hiện một tập lệnh cho đến khi hoàn thành hoặc đi vào một vòng lặp để thực hiện công việc trên mỗi lần lặp.

Node.js có tích hợp event loop, không trực tiếp tiếp xúc với dev. Đó là công việc của event loop để quyết định đoạn code được thực hiện tiếp theo. Thông thường, đây sẽ là một số hàm callback đã sẵn sàng chạy để đáp ứng với một số event khác. Ví dụ, một hoạt động filesystem có thể đã hoàn thành, thời gian chờ có thể đã hết hạn, hoặc một yêu cầu network mới có thể đã đến.

Tích hợp event loop này đơn giản hoá việc lập trình không đồng bộ bằng cách cung cấp cách tiếp cận nhất quán và tránh sự không cần thiết cho các ứng dụng để quản lý việc sắp xếp.

Single-threaded

Bản chất single-threaded của Node.js là chỉ có một luồng thực hiện trong mỗi quy trình. Ngoài ra, mỗi đoạn code được đảm bảo chạy để hoàn thành mà không bị gián đoạn bởi các hoạt động khác. Điều này làm đơn giản hoá quá trình phát triển và làm cho các chương trình trở nên dễ hiểu hơn. Nó loại bỏ khả năng cho một loạt các vấn đề về đồng bộ. Ví dụ, không cần đồng bộ hóa/khóa truy cập vào trạng thái chia sẻ quá trình như trong Java hay .NET. Một quá trình không thể deadlock hoặc tạo race condition trong code của nó. Lập trình Single-threaded chỉ khả thi nếu thread không bao giờ bị chặn phải chờ để một tiến trình có thời gian làm việc lâu hoàn thành. Do đó, mô hình lập trình này được thực hiện bởi bản chất non-blocking của Node.js.

Lập trình ứng dụng web

Trường hợp được sử dụng hàng đầu với Node.js là xây dựng các trang web và API web. Đây là các event-driven có sẵn vì hầu hết hoặc tất cả các quá trình xử lý đều diễn ra để đáp ứng yêu cầu HTTP. Ngoài ra, nhiều trang web giảm việc tính toán lại hơn. Nó có xu hướng thực hiện rất nhiều tác vụ I / O, ví dụ:

  • Yêu cầu phát trực tuyến từ client
  • Nói chuyện với một database cục bộ hoặc network
  • Lấy dữ liệu từ các API từ xa qua network
  • Đọc các tập tin từ đĩa để gửi lại cho client

Những yếu tố này làm cho tác vụ I/O như một “nút cổ chai” cho các ứng dụng web. Mô hình lập trình non-blocking của Node.js cho phép các ứng dụng web làm được nhiều nhất một single thread. Ngay khi có bất kỳ hoạt động I/O nào bắt đầu, thread sẽ ngay lập tức tiếp nhận và bắt đầu xử lý một yêu cầu khác. Xử lý mỗi yêu cầu tiếp theo thông qua callbacks không đồng bộ khi các hoạt động I/O hoàn thành. Các thread đang hoạt động ngắt rời và kết nối các hoạt động này với nhau, không bao giờ đợi nó hoàn thành. Điều này cho phép Node.js xử lý một tỉ lệ lớn yêu cầu trên một thread hơn nhiều so với các môi trường khác.

Node.js mở rộng như thế nào?

Vì vậy, Node.js có thể xử lý nhiều yêu cầu cho mỗi thread, nhưng điều gì xảy ra khi chúng ta đạt đến giới hạn của một thread có thể xử lý được? Câu trả lời là tất nhiên sử dụng thread khác!

Bạn có thể làm được điều này bằng cách bắt đầu nhiều tiến trình Node.js, thông thường, một cho mỗi lõi CPU máy chủ web. Lưu ý điều này vẫn còn khá khác nhau đối với hầu hết các ứng dụng web Java hoặc .NET. Chúng thường sử dụng một nhóm các threads lớn hơn nhiều so với số lỗi, bởi vì thread được dự đoán sẽ mất nhiều thời gian hơn khi bị chặn.

Module được tích hợp trong Node.js làm cho nó trở nên đơn giản để tạo ra nhiều tiến trình Node.js. Các công cụ như PM2 (http://pm2.keymetrics.io/) và các thư viện như throng (https://github.com/hunterloftis/throng) làm cho nó dễ dàng hơn.

Cách tiếp cận này mang lại cho chúng những hiệu năng tốt nhất:

  • Sử dụng nhiều luồng để tối ưu năng lượng CPU
  • Bằng cách có một luồng duy nhất cho mỗi lõi, chúng ta tiết kiệm chi phí chung từ việc chuyển ngữ cảnh giữa các luồng hệ điều hành
  • Vì các quy trình là độc lập và không chia sẻ trạng thái trực tiếp, chúng ta vẫn giữ lại các lợi ích của mô hình lập trình đơn luồng đã thảo luận ở trên
  • Bằng cách sử dụng các quá trình long-running (như với .NET hay Java), chúng ta tránh được tổng phí của một quá trình cho mỗi yêu cầu (như trong PHP)

Tôi có thực sự cần phải sử dụng JavaScript?

Rất nhiều nhà phát triển web Node.js mới đã có sẵn một số trải nghiệm về client-side JavaScript. Kinh nghiệm này có thể không tốt và khiến bạn không muốn sử dụng JavaScript.

Bạn không phải sử dụng JavaScript để làm việc với Node.js. TypeScript (http://www.typescriptlang.org/) và các ngôn ngữ biên dịch sang JavaScript khác tồn tại dưới dạng các lựa chọn thay thế. Tuy nhiên, tôi khuyên bạn nên học Node.js với JavaScript đầu tiên. Nó sẽ cho bạn một sự hiểu biết rõ ràng hơn về Node.js và đơn giản hóa chuỗi công cụ. Một khi có một hoặc hai dự án dưới sự cố gắng của mình, bạn sẽ hiểu được ưu và khuyết điểm của các ngôn ngữ khác. Bạn có thể bị ngạc nhiên về sự phát triển JavaScript trong Node.js.

Có ba giai đoạn lớn trong kinh nghiệm phát triển JavaScript trước đó có thể khiến mọi người có ấn tượng tiêu cực với nó. Đó là:

  • Trải nghiệm từ cuối những năm 90 và đầu những năm 20, trước các MV * Framework như Angular / Knockout / Backbone / Ember, thậm chí trước cả jQuery. Đây là giai đoạn tiên phong của phát triển web phía client-side.
  • Trải nghiệm gần đây trong hệ sinh thái JavaScript, như một lập trình viên full-stack lập trình được trên cả server-side và client-side. Sự phức tạp của một số frameworks  (chẳng hạn như các frameworks  MV * được liệt kê trước đó), hoặc số lượng của các framework tốt hơn áp đảo.
  • Hạn chế trải nghiệm với chính JavaScript, nhưng lại tiếp xúc với một số tính năng không tốt của nó. Điều này có thể dẫn đến một cảm giác khó chịu do kết quả của việc gặp phải trong một ngôn ngữ bằng những cách bất ngờ hoặc vô thức.

Những người tiên phong trong lập trình Web

Trình duyệt đôi khi được miêu tả như một môi trường khó khăn để code thực thi. Trong một vài trường hợp đặc biệt, bạn gặp phải một số các lệnh đặc biệt khó chịu. Ví dụ: code từ thư viện ngoài trên cùng một trang có thể tạo và sửa đổi global objects.

Node.js giải quyết một số vấn đề này ở mức cơ bản và giảm tải ở những vị trí không thể thực hiện được. Đó là JavaScript, do đó, nó vẫn là trường hợp mà mọi thứ đều có thể thay đổi. Tuy nhiên, hệ thống module Node.js làm giảm phạm vi global, do đó các thư viện ít có khả năng xung đột lẫn nhau. Các quy ước mà Node.js thiết lập cũng làm cho các thư viện ngoài nhất quán hơn. Điều này làm cho môi trường ít trở nên xung khắc và dễ đoán trước hơn.

Những người tiên phong về web cũng sẽ phải đương đầu với các API có sẵn cho JavaScript trong trình duyệt. Mặc dù những điều này đã được cải thiện theo thời gian khi trình duyệt và các chuẩn đã dần được hoàn thiện, những ngày đầu phát triển web giống như Wild West. Quirks và sự không nhất quán trong các API gây ra rất nhiều khó khăn và thất vọng. Sự gia tăng của jQuery là một minh chứng cho sự khó khăn khi làm việc với Document Object Model cũ. Sự phổ biến không ngừng của jQuery cho thấy mọi người vẫn thích làm việc trực tiếp với các API này.

Node.js giải quyết các vấn đề này khá triệt để:

  • Trước hết, bằng cách sử dụng JavaScript ngoài trình duyệt, DOM và các API khác chỉ đơn giản là biến mất vì chúng không còn phù hợp nữa.
  • Các API mới mà Node.js giới thiệu là đơn giản, tập trung và nhất quán.
  • Bạn không còn phải đối mặt với sự không tương thích giữa các trình duyệt. Mọi thứ bạn viết sẽ thực hiện trong cùng một công cụ JavaScript (V8).

Quá tải cho lập trình viên full-stack

Nhiều frameworks front-end của JavaScript cung cấp rất nhiều tính năng, nhưng rất phức tạp. Ví dụ: AngularJS có đường cong học tập dốc, khá khó chỉnh sửa về cấu trúc ứng dụng, và có vài câu lệnh hoặc những điều bạn cần biết.

Bản thân JavaScript thực sự là một ngôn ngữ nhỏ gọn. Điều này cung cấp một canvas trống cho Node.js để cung cấp một số nhỏ các API tương đồng (như được mô tả trong phần trước). Mặc dù vẫn còn rất nhiều thứ để học, nhưng bạn chỉ có thể tập trung vào những thứ mình cần mà không bị “vấp” bởi những phạm vị chưa quen.

Nó vẫn đúng rằng có rất nhiều sự lựa chọn và điều này có thể gây nhầm lẫn. Ví dụ: có nhiều frameworks JavaScript thử nghiệm khác đang cạnh tranh nhau. Xu hướng các gói nhỏ hơn và có khả năng kết hợp hơn trong hệ sinh thái Node.js – mặc dù nói chung là một điều tốt – có thể có nhiều nghiên cứu hơn, nhiều quyết định hơn, và ít năng lượng hơn bao gồm các framework làm mọi thứ sáng tạo hơn. Mặc dù vậy, điều này giúp bạn dễ dàng bắt kịp với tốc độ làm việc của mình và hiểu mọi thứ bạn đang đưa vào ứng dụng .

Học vặt JavaScript

Rất dễ bị ấn tượng xấu với JavaScript nếu bạn tiếp xúc với nó ít và không bao giờ xem nó như một ngôn ngữ chính (hoặc thậm chí là thứ yếu ) trong một dự án.

JavaSript không tự làm bất kì điều gì trong một vài câu lệnh mà hầu hết mọi người hay dùng. Ví dụ, về cơ bản kí hiệu so sánh == và những kí tự khác kiểu khuông khổ. Mặc dù những điều này gây ấn tượng đầu không tốt, nhưng chúng sẽ không ảnh hưởng nhiều cho những trải nghiệm khi làm việc với JavaScript thường xuyên hơn.

Như đã đề cập trong phần trước, bản thân JavaScript thực sự là một ngôn ngữ nhỏ. Tính đơn giản của nó giới hạn số lượng gotcha có thể có.Trong khi có một vài điều bạn ” chỉ cần biết”, nó là một danh sách ngắn. Điều này tốt hơn so với các ngôn ngữ cung cấp một luồng không mong muốn liên tục (Ví dụ, PHP nổi tiếng không được tích hợp chức năng)

Hơn nữa, các tiêu chuẩn ECMAScript tiếp theo đã hoàn thành rất nhiều để làm sạch ngôn ngữ JavaScript. Với Node.js, bạn sẽ tận dụng được điều này, vì tất cả code sẽ chạy trên engine V8 , nó sẽ thực hiện chuẩn ES2015 mới nhất.

Một lý do lớn khác mà JavaScript có thể không hòa hợp là vấn đề context hơn là những thiếu sót vốn có. Bề ngoài tương tự như các ngôn ngữ khác với cú pháp giống như C, Java và C #. Sự tương đồng với Java là cố ý khi JavaScript được tạo ra, nhưng nó không thích hợp. Mô hình lập trình JavaScript khá khác với các ngôn ngữ hướng đối tượng khác như Java hay C #. Điều này có thể gây nhầm lẫn hoặc khó chịu, khi cú pháp của nó cho thấy nó có thể hoạt động gần như cùng một cách. Điều này đặc biệt đúng đối với lập trình hướng đối tượng trong JavaScript. Một khi bạn đã hiểu các nguyên tắc cơ bản của JavaScript, nó rất dễ dàng để áp dụng một cách hiệu quả.

Lập trình với JavaScript

Tôi sẽ không khẳng định JavaScript là ngôn ngữ hoàn hảo. Nhưng tôi nghĩ rằng nhiều yếu tố dẫn đến những người có ấn tượng xấu về JavaScript không phải là bản thân ngôn ngữ. Điều quan trọng là “nhiều yếu tố đơn giản khác không áp dụng” khi bạn dùng JavaScript ngoài môi trường trình duyệt.

Hơn nữa, JavaScript có một số thuộc tính thực sự tốt. Đây là những thứ không hiển thị trong code, nhưng có ảnh hưởng đến việc nó tương thích với ngôn ngữ như thế nào. Ví dụ: JavaScript’s interpreted nature giúp bạn dễ dàng thiết lập các bài kiểm tra tự động để chạy liên tục và cung cấp phản hồi gần như tức thì về các thay đổi code .

Làm thế nào để thừa kế trong JavaScript?

Khi giới thiệu về lập trình hướng đối tượng, chúng ta thường nói về các lớp và sự thừa kế. Java, C # và nhiều ngôn ngữ khác có cách tiếp cận rất giống với các khái niệm này. Đối với JavaScript thì khá bất thường. Nó hỗ trợ lập trình hướng đối tượng mà không có các lớp. Nó thực hiện điều này bằng cách áp dụng khái niệm kế thừa trực tiếp cho các đối tượng.

Bất cứ thứ gì không phải là một trong các nguyên tố được xây dựng sẵn của JavaScript (chuỗi, số, null, vv) là một đối tượng. Các hàm chỉ là một loại đối tượng đặc biệt có thể được gọi với các đối số. Mảng là một loại đối tượng đặc biệt với hành vi giống như danh sách. Tất cả các đối tượng (bao gồm cả hai loại đặc biệt) có thể có thuộc tính, mà chỉ là tên với một giá trị. Bạn có thể nghĩ các đối tượng JavaScript như một từ điển với các string keys và đối tượng values.

Lập trình không có classes

Giả sử bạn có một biểu đồ với một số lượng rất lớn các điểm dữ liệu. Những điểm này được đại diện bởi các đối tượng có một số hành vi thông thường. Trong C # hoặc Java, bạn có thể tạo ra một lớp Point. Trong JavaScript, bạn có thể implement các points như sau:

Hàm createPoint trả về một đối tượng mới mỗi khi nó được gọi (đối tượng được định nghĩa bằng cách sử dụng ký hiệu object-literal của JavaScript, đây là cơ sở cho JSON). Một vấn đề với cách tiếp cận này là chức năng được gán cho thuộc tính isAboveDiagonal được định nghĩa lại cho mỗi điểm trên đồ thị, do đó chiếm nhiều không gian hơn trong bộ nhớ.

Bạn có truy cập nó bằng cách sử dụng prototypal inheritance. Mặc dù JavaScript không có các lớp, các đối tượng có thể kế thừa từ các đối tượng khác. Mỗi đối tượng có một prototype. Nếu trình thông dịch cố gắng truy cập vào một thuộc tính trên một đối tượng và thuộc tính đó không tồn tại, nó sẽ tìm một property có cùng tên trên prototype của đối tượng thay thế. Nếu property không tồn tại ở đó, nó sẽ kiểm tra prototype của nguyên mẫu, và như vậy. Chuỗi prototype sẽ kết thúc bằng Object.prototype được tích hợp.

Bạn có thể thực hiện các đối tượng điểm sử dụng một prototype như sau:

Phương thức Object.create tạo ra một đối tượng mới với một prototype xác định. Phương thức isAboveDiagonal bây giờ chỉ tồn tại một lần trong bộ nhớ trên đối tượng pointPrototype. Khi code cố gắng gọi isAboveDiagonal trên một đối tượng điểm riêng lẻ, nó không có mặt, nhưng nó được tìm thấy trên prototype thay thế.

Lưu ý rằng ví dụ trước cho chúng ta biết điều gì đó quan trọng về hành vi của từ khoá this trong JavaScript. Nó thực sự đề cập đến đối tượng mà chức năng hiện tại đã được gọi vào, chứ không phải là đối tượng nó được định nghĩa trên.

Tạo đối tượng với từ khóa ‘new’

Bạn có thể viết lại ví dụ code trước đó dưới dạng nhỏ gọn hơn sử dụng toán tử new :

Theo quy ước, các hàm có một thuộc tính có tên là prototype, mặc định là một đối tượng rỗng. Sử dụng toán tử new với chức năng Point tạo ra một đối tượng kế thừa từ Point.prototype và áp dụng hàm Point vào đối tượng mới tạo ra.

Lập trình với các classes

Mặc dù JavaScript không có các classes cơ bản, nhưng ES2015 giới thiệu một từ khoá class mới. Điều này làm cho nó có thể thực hiện hành vi chia sẻ và kế thừa theo một cách quen thuộc hơn so với các ngôn ngữ hướng đối tượng khác.

Tương đương với ví dụ code trước đó:

Lưu ý ví dụ này thực sự tương đương với ví dụ trước. Từ khoá class chỉ là cú pháp để thiết lập sự thừa kế theo nguyên mẫu đã được nói ở trước.

Một khi bạn đã biết làm thế nào để xác định các đối tượng và các lớp, bạn có thể bắt đầu cấu trúc phần còn lại của ứng dụng.

Làm cách nào để cấu trúc các ứng dụng Node.js?

Trong C # và Java, cấu trúc tĩnh của một ứng dụng được định nghĩa bởi các namespaces hoặc packages (tương ứng) và các kiểu static. Cấu trúc run-time của ứng dụng (tức là tập hợp các đối tượng được tạo ra trong bộ nhớ) thường được khởi động bằng cách sử dụng một container dependency injection (DI). Ví dụ về các containers chứa DI bao gồm NInject, Autofac và Unity in .NET, hoặc Spring, Guice và Dagger trong Java. Các frameworks này cung cấp các tính năng như cấu hình khai báo và autowiring của dependencies.

Vì JavaScript là một ngôn ngữ động, nên nó không có cấu trúc ứng dụng tĩnh cố hữu. Thật vậy, trong trình duyệt, tất cả các tập lệnh được tải vào một trang sau cùng trong cùng một bối cảnh global. Hệ thống mô-đun Node.js cho phép bạn cấu trúc ứng dụng của bạn vào các tệp tin và thư mục và cung cấp một cơ chế để nhập các chức năng từ một tệp này sang tệp khác.

Có các bộ chứa DI dành cho JavaScript nhưng chúng ít được sử dụng hơn. Nó phổ biến hơn để vượt trội dependencies một cách rõ ràng. Hệ thống mô-đun Node.js và cách viết tự động của JavaScript làm cho cách tiếp cận này trở nên tự nhiên hơn. Bạn không cần phải thêm nhiều fields và constructors/properties để thiết lập dependencies. Bạn chỉ có thể gói mô-đun trong một chức năng khởi tạo mà lấy dependencies như là các tham số.

Ví dụ đơn giản sau minh hoạ hệ thống mô-đun Node.js và cho thấy cách inject dependencies thông qua chức năng của factory :

Chúng tôi thêm đoạn code sau vào /src/greeter.js:

Trong hệ thống module Node.js, mỗi file sẽ thiết lập một module mới với phạm vi của riêng nó. Trong phạm vi này, Node.js cung cấp các module đối tượng cho module hiện tại để xuất các chức năng, và yêu cầu các chức năng để nhập các module khác.

Nếu bạn chạy ví dụ trước đó (sử dụng nút main.js), Node.js runtime sẽ tải module greeter như là kết quả của việc gọi module chính đến chức năng yêu cầu. Module greeter gán một giá trị cho thuộc tính exports của đối tượng module. Nó trở thành giá trị trả về yêu cầu gọi lại trong module chính. Trong trường hợp này, module greeter exports một đối tượng single, đó là một chức năng factory mà phải lấy một dependency..

Tóm tắt

Trong bài này, đề cập đến những vấn đề:

Hy vọng bài viết này đã cung cấp cho bạn một số hiểu biết sâu sắc về lý do tại sao Node.js là một ngôn ngữ hấp dẫn và giúp bạn chuẩn bị tốt hơn để tìm hiểu thêm về cách viết các ứng dụng server-side bằng JavaScript và Node.js.

Techtalk via packtpub.com