10 najczęstszych błędów popełnianych przez programistów Node.js

Opublikowany: 2022-03-11

Od momentu, gdy Node.js został zaprezentowany światu, spotkał się z licznymi pochwałami i krytyką. Debata nadal trwa i może się nie skończyć w najbliższym czasie. To, co często pomijamy w tych debatach, to fakt, że każdy język programowania i platforma jest krytykowana w oparciu o pewne problemy, które są spowodowane tym, jak korzystamy z platformy. Niezależnie od tego, jak trudne Node.js sprawia, że ​​pisanie bezpiecznego kodu jest łatwe i jak łatwe sprawia pisanie wysoce współbieżnego kodu, platforma istnieje już od dłuższego czasu i była używana do tworzenia ogromnej liczby niezawodnych i zaawansowanych usług internetowych. Te usługi sieciowe dobrze się skalują i udowodniły swoją stabilność dzięki swojej trwałości w Internecie.

Jednak, jak każda inna platforma, Node.js jest podatny na problemy i problemy programistów. Niektóre z tych błędów obniżają wydajność, podczas gdy inne sprawiają, że Node.js wydaje się być bezużyteczny do tego, co próbujesz osiągnąć. W tym artykule przyjrzymy się dziesięciu częstym błędom, które często popełniają programiści nowi w Node.js, oraz tym, jak można ich uniknąć, aby zostać profesjonalistą w Node.js.

Błędy programisty node.js

Błąd nr 1: Blokowanie pętli zdarzeń

JavaScript w Node.js (podobnie jak w przeglądarce) zapewnia jednowątkowe środowisko. Oznacza to, że żadne dwie części aplikacji nie działają równolegle; zamiast tego współbieżność jest osiągana poprzez asynchroniczną obsługę operacji związanych z operacjami we/wy. Na przykład żądanie od Node.js do silnika bazy danych, aby pobrać jakiś dokument, pozwala Node.js skupić się na innej części aplikacji:

 // Trying to fetch an user object from the database. Node.js is free to run other parts of the code from the moment this function is invoked.. db.User.get(userId, function(err, user) { // .. until the moment the user object has been retrieved here }) 

Środowisko jednowątkowe node.js

Jednak fragment kodu związanego z procesorem w instancji Node.js z tysiącami podłączonych klientów wystarczy, aby zablokować pętlę zdarzeń, powodując, że wszyscy klienci czekają. Kody związane z procesorem obejmują próby sortowania dużej tablicy, uruchamianie bardzo długiej pętli i tak dalej. Na przykład:

 function sortUsersByAge(users) { users.sort(function(a, b) { return a.age < b.age ? -1 : 1 }) }

Wywołanie tej funkcji „sortUsersByAge” może być w porządku, jeśli zostanie uruchomione na małej tablicy „użytkowników”, ale przy dużej tablicy będzie miało straszny wpływ na ogólną wydajność. Jeśli jest to coś, co absolutnie musi zostać zrobione i masz pewność, że nic więcej nie będzie czekać na pętlę zdarzeń (na przykład, jeśli była to część narzędzia wiersza poleceń, które tworzysz za pomocą Node.js, i to nie miałoby znaczenia, gdyby całość działała synchronicznie), to może to nie stanowić problemu. Jednak w instancji serwera Node.js, która próbuje obsługiwać jednocześnie tysiące użytkowników, taki wzorzec może okazać się fatalny.

Gdyby ta tablica użytkowników była pobierana z bazy danych, idealnym rozwiązaniem byłoby pobranie jej już posortowanej bezpośrednio z bazy danych. Jeśli pętla zdarzeń była blokowana przez pętlę napisaną w celu obliczenia sumy danych z długiej historii transakcji finansowych, może to zostać odroczone do jakiegoś zewnętrznego pracownika/konfiguracji kolejki, aby uniknąć przeciążenia pętli zdarzeń.

Jak widać, nie ma srebrnej kuli rozwiązania tego rodzaju problemu Node.js, raczej każdy przypadek należy rozpatrywać indywidualnie. Podstawową ideą jest to, aby nie wykonywać intensywnej pracy procesora w instancjach Node.js skierowanych do przodu — tych, z którymi klienci łączą się jednocześnie.

Błąd nr 2: wywoływanie oddzwaniania więcej niż raz

JavaScript od zawsze polega na wywołaniach zwrotnych. W przeglądarkach internetowych zdarzenia są obsługiwane przez przekazywanie referencji do (często anonimowych) funkcji, które działają jak wywołania zwrotne. W Node.js wywołania zwrotne były jedynym sposobem, w jaki asynchroniczne elementy kodu komunikowały się ze sobą - aż do wprowadzenia obietnic. Wywołania zwrotne są nadal w użyciu, a twórcy pakietów nadal projektują swoje interfejsy API wokół wywołań zwrotnych. Jednym z powszechnych problemów Node.js związanych z używaniem wywołań zwrotnych jest wywoływanie ich więcej niż raz. Zazwyczaj funkcja dostarczona przez pakiet w celu wykonania czegoś asynchronicznego jest zaprojektowana tak, aby oczekiwała funkcji jako ostatniego argumentu, który jest wywoływany po zakończeniu zadania asynchronicznego:

 module.exports.verifyPassword = function(user, password, done) { if(typeof password !== 'string') { done(new Error('password should be a string')) return } computeHash(password, user.passwordHashOpts, function(err, hash) { if(err) { done(err) return } done(null, hash === user.passwordHash) }) }

Zwróć uwagę, że za każdym razem, gdy wywoływane jest „done”, aż do ostatniego razu, pojawia się oświadczenie zwrotne. Dzieje się tak, ponieważ wywołanie funkcji zwrotnej nie kończy automatycznie wykonywania bieżącej funkcji. Jeśli pierwszy „powrót” został wykomentowany, przekazanie do tej funkcji hasła bez ciągu znaków nadal spowoduje wywołanie „computeHash”. W zależności od tego, jak „computeHash” radzi sobie z takim scenariuszem, „gotowe” może być wielokrotnie nazywane. Każdy, kto korzysta z tej funkcji z innego miejsca, może zostać całkowicie zaskoczony, gdy przekazywane przez niego wywołanie zwrotne jest wywoływane wielokrotnie.

Ostrożność wystarczy, aby uniknąć tego błędu Node.js. Niektórzy programiści Node.js przyjmują zwyczaj dodawania słowa kluczowego return przed każdym wywołaniem funkcji zwrotnej:

 if(err) { return done(err) }

W wielu funkcjach asynchronicznych zwracana wartość nie ma prawie żadnego znaczenia, więc takie podejście często ułatwia uniknięcie takiego problemu.

Błąd nr 3: głęboko zagnieżdżone wywołania zwrotne

Głęboko zagnieżdżone wywołania zwrotne, często określane jako „piekło wywołań zwrotnych”, nie są same w sobie problemem Node.js. Może to jednak powodować problemy, przez co kod szybko wymyka się spod kontroli:

 function handleLogin(..., done) { db.User.get(..., function(..., user) { if(!user) { return done(null, 'failed to log in') } utils.verifyPassword(..., function(..., okay) { if(okay) { return done(null, 'failed to log in') } session.login(..., function() { done(null, 'logged in') }) }) }) } 

Oddzwoń piekło

Im bardziej złożone zadanie, tym gorzej może się to pogorszyć. Zagnieżdżając w ten sposób wywołania zwrotne, łatwo otrzymujemy kod podatny na błędy, trudny do odczytania i trudny do utrzymania. Jednym z obejść jest zadeklarowanie tych zadań jako małych funkcji, a następnie ich połączenie. Chociaż jednym z (prawdopodobnie) najczystszych rozwiązań jest użycie pakietu narzędziowego Node.js, który zajmuje się asynchronicznymi wzorcami JavaScript, takimi jak Async.js:

 function handleLogin(done) { async.waterfall([ function(done) { db.User.get(..., done) }, function(user, done) { if(!user) { return done(null, 'failed to log in') } utils.verifyPassword(..., function(..., okay) { done(null, user, okay) }) }, function(user, okay, done) { if(okay) { return done(null, 'failed to log in') } session.login(..., function() { done(null, 'logged in') }) } ], function() { // ... }) }

Podobnie jak „async.waterfall”, istnieje wiele innych funkcji, które Async.js udostępnia do obsługi różnych wzorców asynchronicznych. Dla zwięzłości użyliśmy tutaj prostszych przykładów, ale rzeczywistość jest często gorsza.

Błąd 4: Oczekiwanie, że wywołania zwrotne będą działały synchronicznie

Programowanie asynchroniczne z wywołaniami zwrotnymi może nie jest czymś unikalnym dla JavaScript i Node.js, ale to one odpowiadają za jego popularność. W przypadku innych języków programowania jesteśmy przyzwyczajeni do przewidywalnej kolejności wykonywania, w której dwie instrukcje będą wykonywane jedna po drugiej, chyba że istnieje konkretna instrukcja do przeskakiwania między instrukcjami. Nawet wtedy są one często ograniczone do instrukcji warunkowych, instrukcji pętli i wywołań funkcji.

Jednak w JavaScript z wywołaniami zwrotnymi dana funkcja może nie działać dobrze, dopóki zadanie, na które czeka, nie zostanie zakończone. Wykonanie aktualnej funkcji będzie trwało do końca bez żadnego zatrzymania:

 function testTimeout() { console.log(“Begin”) setTimeout(function() { console.log(“Done!”) }, duration * 1000) console.log(“Waiting..”) }

Jak zauważysz, wywołanie funkcji „testTimeout” spowoduje najpierw wydrukowanie „Rozpocznij”, a następnie „Oczekiwanie...”, a następnie komunikat „Gotowe!” po około sekundzie.

Wszystko, co musi się wydarzyć po uruchomieniu wywołania zwrotnego, musi zostać wywołane z jego wnętrza.

Błąd nr 5: Przypisywanie do „eksportów”, zamiast „module.exports”

Node.js traktuje każdy plik jako mały izolowany moduł. Jeśli twój pakiet zawiera dwa pliki, na przykład „a.js” i „b.js”, to aby „b.js” mógł uzyskać dostęp do funkcji „a.js”, „a.js” musi go wyeksportować, dodając właściwości do obiekt eksportu:

 // a.js exports.verifyPassword = function(user, password, done) { ... }

Gdy to zrobisz, każdy, kto będzie wymagał „a.js”, otrzyma obiekt z funkcją właściwości „verifyPassword”:

 // b.js require('a.js') // { verifyPassword: function(user, password, done) { ... } }

Co jednak, jeśli chcemy wyeksportować tę funkcję bezpośrednio, a nie jako właściwość jakiegoś obiektu? Możemy w tym celu nadpisać eksporty, ale nie możemy wtedy traktować tego jako zmiennej globalnej:

 // a.js module.exports = function(user, password, done) { ... }

Zwróć uwagę, jak traktujemy „eksporty” jako właściwość obiektu modułu. Rozróżnienie między „module.exports” i „exports” jest bardzo ważne i często powoduje frustrację wśród nowych programistów Node.js.

Błąd nr 6: zgłaszanie błędów z wewnętrznych wywołań zwrotnych

JavaScript ma pojęcie wyjątków. Naśladując składnię prawie wszystkich tradycyjnych języków z obsługą wyjątków, takich jak Java i C++, JavaScript może „rzucać” i łapać wyjątki w blokach try-catch:

 function slugifyUsername(username) { if(typeof username === 'string') { throw new TypeError('expected a string username, got '+(typeof username)) } // ... } try { var usernameSlug = slugifyUsername(username) } catch(e) { console.log('Oh no!') }

Jednak try-catch nie będzie zachowywał się tak, jak można by się tego spodziewać w sytuacjach asynchronicznych. Na przykład, jeśli chcesz chronić dużą część kodu z dużą ilością asynchronicznej aktywności za pomocą jednego dużego bloku try-catch, to niekoniecznie zadziała:

 try { db.User.get(userId, function(err, user) { if(err) { throw err } // ... usernameSlug = slugifyUsername(user.username) // ... }) } catch(e) { console.log('Oh no!') }

Gdyby wywołanie zwrotne do „db.User.get” zostało uruchomione asynchronicznie, zakres zawierający blok try-catch już dawno wyszedłby z kontekstu, aby nadal mógł przechwytywać te błędy wyrzucane z wnętrza wywołania zwrotnego.

W ten sposób błędy są obsługiwane w inny sposób w Node.js, a to sprawia, że ​​konieczne jest przestrzeganie wzorca (err, …) we wszystkich argumentach funkcji zwrotnej - oczekuje się, że pierwszy argument wszystkich wywołań zwrotnych będzie błędem, jeśli taki wystąpi .

Błąd nr 7: zakładanie, że liczba jest liczbą całkowitą

Liczby w JavaScript są zmiennoprzecinkowe - nie ma typu danych całkowitych. Nie spodziewałbyś się, że będzie to problem, ponieważ liczby wystarczająco duże, aby podkreślić granice pływalności, nie są często spotykane. Właśnie wtedy zdarzają się błędy z tym związane. Ponieważ liczby zmiennoprzecinkowe mogą przechowywać reprezentacje liczb całkowitych tylko do określonej wartości, przekroczenie tej wartości w dowolnym obliczeniu natychmiast zacznie ją zepsuć. Choć może się to wydawać dziwne, następujące wartości są prawdziwe w Node.js:

 Math.pow(2, 53)+1 === Math.pow(2, 53)

Niestety dziwactwa z liczbami w JavaScript nie kończą się tutaj. Mimo że liczby są liczbami zmiennoprzecinkowymi, operatory działające na typach danych całkowitych również działają tutaj:

 5 % 2 === 1 // true 5 >> 1 === 2 // true

Jednak w przeciwieństwie do operatorów arytmetycznych, operatory bitowe i operatory przesunięcia działają tylko na końcowych 32 bitach tak dużych liczb całkowitych. Na przykład próba przesunięcia „Math.pow(2, 53)” o 1 zawsze da 0. Próba wykonania operacji bitowej lub 1 z tą samą dużą liczbą da 1.

 Math.pow(2, 53) / 2 === Math.pow(2, 52) // true Math.pow(2, 53) >> 1 === 0 // true Math.pow(2, 53) | 1 === 1 // true

Rzadko będziesz musiał mieć do czynienia z dużymi liczbami, ale jeśli to zrobisz, istnieje wiele dużych bibliotek liczb całkowitych, które implementują ważne operacje matematyczne na liczbach o dużej precyzji, takie jak węzeł-bigint.

Błąd nr 8: ignorowanie zalet interfejsów API do przesyłania strumieniowego

Załóżmy, że chcemy zbudować mały serwer WWW podobny do proxy, który obsługuje odpowiedzi na żądania, pobierając zawartość z innego serwera WWW. Jako przykład zbudujemy mały serwer WWW, który obsługuje obrazy Gravatar:

 var http = require('http') var crypto = require('crypto') http.createServer() .on('request', function(req, res) { var email = req.url.substr(req.url.lastIndexOf('/')+1) if(!email) { res.writeHead(404) return res.end() } var buf = new Buffer(1024*1024) http.get('http://www.gravatar.com/avatar/'+crypto.createHash('md5').update(email).digest('hex'), function(resp) { var size = 0 resp.on('data', function(chunk) { chunk.copy(buf, size) size += chunk.length }) .on('end', function() { res.write(buf.slice(0, size)) res.end() }) }) }) .listen(8080)

W tym konkretnym przykładzie problemu z Node.js pobieramy obraz z Gravatara, odczytujemy go do bufora, a następnie odpowiadamy na żądanie. Nie jest to takie złe, biorąc pod uwagę, że obrazy Gravatara nie są zbyt duże. Wyobraź sobie jednak, że rozmiar przesyłanej przez nas zawartości miałby tysiące megabajtów. O wiele lepszym podejściem byłoby to:

 http.createServer() .on('request', function(req, res) { var email = req.url.substr(req.url.lastIndexOf('/')+1) if(!email) { res.writeHead(404) return res.end() } http.get('http://www.gravatar.com/avatar/'+crypto.createHash('md5').update(email).digest('hex'), function(resp) { resp.pipe(res) }) }) .listen(8080)

Tutaj pobieramy obraz i po prostu przesyłamy odpowiedź do klienta. W żadnym momencie nie musimy wczytywać całej zawartości do bufora przed jej udostępnieniem.

Błąd nr 9: Używanie Console.log do celów debugowania

W Node.js „console.log” pozwala na drukowanie prawie wszystkiego na konsoli. Przekaż do niego obiekt, a wyświetli go jako literał obiektu JavaScript. Akceptuje dowolną liczbę argumentów i wypisuje je wszystkie starannie oddzielone spacjami. Istnieje wiele powodów, dla których programista może czuć pokusę, aby użyć tego do debugowania swojego kodu; jednak zdecydowanie zaleca się unikanie „console.log” w prawdziwym kodzie. Powinieneś unikać pisania „console.log” w całym kodzie, aby go debugować, a następnie komentować je, gdy nie są już potrzebne. Zamiast tego użyj jednej z niesamowitych bibliotek stworzonych specjalnie do tego celu, takich jak debug.

Pakiety takie jak te zapewniają wygodne sposoby włączania i wyłączania niektórych wierszy debugowania podczas uruchamiania aplikacji. Na przykład za pomocą debugowania można zapobiec drukowaniu wierszy debugowania na terminalu, nie ustawiając zmiennej środowiskowej DEBUG. Korzystanie z niego jest proste:

 // app.js var debug = require('debug')('app') debug('Hello, %s!', 'world')

Aby włączyć wiersze debugowania, po prostu uruchom ten kod ze zmienną środowiskową DEBUG ustawioną na „app” lub „*”:

 DEBUG=app node app.js

Błąd nr 10: nieużywanie programów nadzorczych

Niezależnie od tego, czy Twój kod Node.js działa w środowisku produkcyjnym, czy w lokalnym środowisku programistycznym, monitor programu nadzorcy, który może organizować Twój program, jest niezwykle przydatny. Jedna z praktyk często zalecana przez programistów projektujących i wdrażających nowoczesne aplikacje zaleca, aby Twój kod szybko zawodził. Jeśli wystąpi nieoczekiwany błąd, nie próbuj sobie z nim radzić, raczej pozwól swojemu programowi się zawiesić i poprosić przełożonego o ponowne uruchomienie go za kilka sekund. Korzyści płynące z programów nadzorczych nie ograniczają się tylko do ponownego uruchamiania programów, które uległy awarii. Narzędzia te umożliwiają ponowne uruchomienie programu w przypadku awarii, a także ponowne ich uruchomienie, gdy zmienią się niektóre pliki. To sprawia, że ​​tworzenie programów Node.js jest znacznie przyjemniejsze.

Istnieje mnóstwo programów nadzorczych dostępnych dla Node.js. Na przykład:

  • pm2

  • na zawsze

  • nodemon

  • kierownik

Wszystkie te narzędzia mają swoje zalety i wady. Niektóre z nich są dobre do obsługi wielu aplikacji na tym samym komputerze, podczas gdy inne są lepsze w zarządzaniu logami. Jeśli jednak chcesz zacząć z takim programem, wszystko to jest uczciwym wyborem.

Wniosek

Jak widać, niektóre z tych problemów z Node.js mogą mieć niszczący wpływ na Twój program. Niektóre mogą być przyczyną frustracji, gdy próbujesz zaimplementować najprostsze rzeczy w Node.js. Chociaż Node.js bardzo ułatwił nowicjuszom rozpoczęcie pracy, nadal ma obszary, w których równie łatwo można zepsuć. Programiści z innych języków programowania mogą być w stanie odnieść się do niektórych z tych problemów, ale błędy te są dość powszechne wśród nowych programistów Node.js. Na szczęście łatwo ich uniknąć. Mam nadzieję, że ten krótki przewodnik pomoże początkującym w pisaniu lepszego kodu w Node.js oraz w tworzeniu stabilnego i wydajnego oprogramowania dla nas wszystkich.