AJAX z Vanilla Node.js / JavaScript - POST JSON bez formularza
Jestem nowy w node.js, ajax i asynchroniczny kod. Opracowałem działający sposób wymiany plików json między przeglądarką a serwerem i zastanawiam się, czy ktoś z większym doświadczeniem uzna to podejście za rozsądne dla moich potrzeb.
Kontekst wymagań: Ta technika będzie używana do aktualizacji danych w zapleczu. Początkowo na tym samym komputerze, na którym znajduje się baza danych, będzie jeden użytkownik. Wolałbym nie wykluczać, że pozwolę kilku innym użytkownikom na zdalne aktualizowanie zaplecza w przyszłości, ale jeśli tak się stanie, używaliby prostszych ekranów zaprojektowanych z myślą o zdalnej lokalizacji. Ponieważ jest to zaplecze biurowe, mogę nalegać na najnowszą przeglądarkę Chrome.
Moje ograniczenia: Jestem jednoosobowym sklepem rozwijającym / wspierającym stronę internetową na częściowej emeryturze. Pracuję w niepełnym wymiarze godzin i moje obowiązki wykraczają poza kodowanie, więc w najlepszym razie mogę być mistrzem niektórych zawodów, ale żadnego mistrza.
Możesz od razu przejść do sekcji „Wymagania” poniżej lub, jeśli potrzebujesz szerszego kontekstu, przeczytaj:
Dlaczego nie skorzystać z biblioteki, aby to zrobić ?: Jestem otwarty na to, że powinienem, ale wolę unikać bibliotek, chyba że istnieje ważny powód, aby z nich korzystać. Jestem jednoosobowym sklepem, który musi wymyślić, jak zrobić kilka rzeczy, a następnie zautomatyzować je raz po raz. To zupełnie inna sytuacja niż duża firma zajmująca się wieloma rzeczami. Na przykład front-end tej aplikacji ma pół miliona statycznych stron internetowych napisanych przez zaplecze. Jest tylko pół tuzina typów stron, a każdy ładuje się jednym trafieniem na serwer. Są napisane zwykłym waniliowym html, css i javascript. Menu wyglądałyby ładniej, gdyby korzystano z biblioteki (lub gdy spędzam nad nimi więcej czasu), ale w prostocie jest wartość. Biblioteki komplikują kontrolę wersji, zazwyczaj zapewniają olbrzymie możliwości, których nigdy nie będę używać, i ciągną za sobą starsze wsparcie, którego nie potrzebuję. Moje osobiste doświadczenie jest takie, że w porównaniu z wieloma innymi jestem lepszy w zagłębianiu się w jeden produkt, niż w zapamiętywaniu interfejsu wielu produktów.
Uzasadnienie narzędzia: Nie ma planów, aby witryna generowała przychody, więc jednym z powodów, dla których wybrałem MySQL i node.js jest brak kosztów licencji na hobby, które mam nadzieję kontynuować przez dwadzieścia lat. Narzędzia wydają się spełniać moje wymagania i są na tyle popularne, że mogę znaleźć sposoby na robienie rzeczy w Internecie. Ponadto, jako sklep jednoosobowy na pół etatu, możliwość używania tego samego języka na serwerze i przeglądarce jest ogromną zaletą.
Wymaganie: w przypadku zwykłego pliku node.js można przenosić duże pliki json między przeglądarką a serwerem, aby obsługiwać obsługę danych zaplecza witryny internetowej. Przetestowałem obecne rozwiązanie i przenosi ono więcej danych niż potrzebuję, aby przenieść je szybciej, niż muszę je przenieść. (Przetestowałem 100 000 obiektów wymagających plików json 8Meg w obie strony.) Ale nie mam doświadczenia w Ajax i obawiam się, że mogą wystąpić problemy, których nie przewiduję, lub łatwe sposoby na zrobienie tego rodzaju AJAX w lepszy sposób.
Zagadnienia, które rozważałem:
- CORS wymaga, aby serwer, który odpowiada na żądanie AJAX, musiał najpierw obsłużyć plik html wysyłający żądanie.
- Ajax POST nie są idempotentne
- Przeglądarka automatycznie zażąda ikony ulubionych
Na serwerze instrukcja switch będzie zbyt długa, jeśli będę nadal dodawać sprawy dla każdej strony konserwacji. Oczyszczę to i z zadowoleniem przyjmuję każdą ogólną radę, którą chcesz zaoferować, ale moje pytanie tutaj brzmi, czy technika Ajax jest rozsądna dla moich potrzeb?
Dołączam kolorowe obrazy kodu VSCode, aby ułatwić jego czytanie, a także rzeczywisty kod, który można skopiować na komputer i uruchomić. Przykładowy kod można przetestować bez zmian, umieszczając pliki .js i .html w tym samym katalogu i nadając im nazwę „test_ajax_post_json_sans_form”. Aby przetestować, należy wykonać polecenie „nazwa pliku węzła” w konsoli poleceń, a następnie załadować http: // localhost : 8000 / filename w twojej przeglądarce. Użyłem Chrome.
html z obrazem kodu klienta ORYGINALNY (zobacz fragment do aktualizacji):
obraz js:
Umieściłem kod we fragmentach, ale wymaga on działania node.js, więc musiałbyś skopiować go na komputer z node.js. (ZAKTUALIZOWANY HTML według sugestii 11.10):
'use strict';
const host = 'localhost';
const http = require('http'); // VSCode shows 3 dots under "require" and says something about NodeRequire???
const fs = require('fs').promises;
const port = 8000;
const requestListener = function (req, res) {
switch (req.url) {
case "/test_ajax_post_json_sans_form.html": // serve an intial html file
fs.readFile(__dirname + '/test_ajax_post_json_sans_form.html')
.then(contents => {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(contents);
})
.catch(err => {
res.writeHead(500);
res.end(err);
return;
});
break
case '/test_ajax_post_json_sans_form.html/ajaxTest1': // receive json, process, then return other json
let body = '';
req.on('data', (data) => {
body += data;
});
req.on('end', () => {
console.log(body); // to show all data has arrived
// here we will check or errors, create a complex return-object, stringify it, and send it back
const objToReturn = {data1: 'Test message from server', data2: 'could be a complex json object'};
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(JSON.stringify(objToReturn));
});
break
case '/favicon.ico':
// browser will call favicon automatically. This satisfies the request (though its failing
// won't keep the ajax from working).
fs.readFile(__dirname + '/favicon.ico')
.then(contents => {
res.writeHead(200, {'Content-Type': 'image/x-icon'});
res.end(contents);
})
.catch(err => { // lacking favicon will not impact test
res.writeHead(200, {'Content-Type': 'image/x-icon'});
res.end();
});
break
default:
res.writeHead(404);
res.end(JSON.stringify({error:'Resource not found'})); // in production, might load a not-found page here
}
}
const server = http.createServer(requestListener);
server.listen(port,host, () => { // binds the server object to a newtwork address
console.log(`Server is running on http://${host}:${port}. (^c to cancel)`);
});
<!DOCTYPE html><body>
<button type="button" id="submitButton">Send and then Receive JSON</button>
<br>After clicking the button, the JSON string sent from the browser to the server will show in the console.
<br>Then the JSON response string from the server will replace what is below.
<br><br>
<div id='messageArea'>
Message to replace via ajax.
</div>
<script>
'use strict';
const submitButton = document.getElementById('submitButton');
const messageArea = document.getElementById('messageArea');
async function exchangeJSON() {
try {
submitButton.disabled = true;
// here can build a complex object to send
const objToSend = {message1: 'Test message from browser', message2: 'could be a complex JSON string'};
const response = await fetch('test_ajax_post_json_sans_form.html/ajaxTest1', {
method: 'POST',
body: JSON.stringify(objToSend)
});
if (response.ok) {
const jsonResponse = await response.json();
// here can parse, update screen, etc.
messageArea.textContent = JSON.stringify(jsonResponse);
}
//throw new Error('Test error in ExchangeJSON'); // uncomment for testing
} catch (error) {
messageArea.textContent = error;
} finally {
submitButton.disabled = false;
}
}
submitButton.addEventListener('click',exchangeJSON);
</script>
</body></html>
Odpowiedzi
Rozważfetch W nowoczesnych przeglądarkach, w których Twój kod na pewno działa, fetchjest zwykle lepszym wyborem niż XMLHttpRequest- fetchjest oparty na Promise (Obietnice są zwykle nieco przyjemniejsze w obsłudze niż wywołania zwrotne), jego API jest nieco czystsze do odczytu i zapisu i jest trochę bardziej zwięzły.
Obsługa błędów Front-end nie obsługuje błędów. Jeśli żądanie nie powiedzie się z jakiegokolwiek powodu, użytkownik nie będzie o tym wiedział - po naciśnięciu przycisku będzie się wydawać, że jest przetwarzane w nieskończoność, bez ponownego odblokowania. Rozważać
- Wyświetlanie komunikatu o błędzie, jeśli wystąpi błąd, i
- Ponowne włączenie przycisku, jeśli wystąpi błąd
Odpowiedź Jeśli chcesz pokazać użytkownikowi JSON odpowiedzi:
- Nie ustawiaj odpowiedzi jako
innerHTMLelementu. Może to spowodować wykonanie dowolnego kodu, nieoczekiwanych dodatkowych elementów HTML i dziwnych rzeczy związanych z encjami HTML. Użyj.textContentzamiast tego. - Ponieważ odpowiedzią jest JSON, może użyj elementu bardziej przypominającego kod, na przykład
<pre>?
Wywołania zwrotne Kiedy dodajesz odbiornik, jeśli chcesz, aby wywołanie zwrotne działało, jeśli nie przyjmuje żadnych argumentów, możesz przekazać wywołanie zwrotne bezpośrednio do, addEventListenerzamiast zawijać je w innej funkcji.
Średniki W niektórych wierszach brakuje średników. Aby zachować spójność stylistyczną, użyj ich lub nie - a jeśli zdecydujesz się tego nie robić, miejmy nadzieję, że jesteś ekspertem, w przeciwnym razie możesz napotkać problemy z automatycznym wstawianiem średnika . Wybierz styl, a następnie wzmocnij go linerem .
idSubmitButton? Nazwa zmiennej wybranego przycisku powinna prawdopodobnie być podobna do submitButton- identyfikator nie ma znaczenia po wybraniu. Posiadanie idjako przedrostka w atrybucie id też jest dziwne, może po prostu użyj submitButton.
<button type="button" id="submitButton">Send and then Receive JSON</button>
<br>After clicking the button, the JSON string sent from the browser to the server will show in the console.
<br>Then the JSON response string from the server will replace what is below.
<br><br>
<div class='error' style='color: red; display: none;'></div>
<pre>Response gets inserted here</div>
<script>
'use strict';
const submitButton = document.getElementById('submitButton');
function exchangeJSON() {
submitButton.disabled = true; // assure post isn't sent again prior to a response
const testObjToSend = { message1: 'Test message from browser', message2: 'could be a complex JSON string' };
const errorDiv = document.querySelector('.error');
errorDiv.style.display = 'none'; // Hide previous error
// If you want to cause an error if the transaction is taking way longer than expected,
// see https://stackoverflow.com/q/46946380
fetch(
'ajaxTest1',
{
method: 'POST',
body: JSON.stringify(testObjToSend),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
},
)
.then(res => res.text()) // if you wanted to expand error handling, could check if response is OK first
.then((result) => {
try {
// If it's not JSON, this will throw
JSON.parse(result);
} catch(e) {
// Send text to catch handler below
throw new Error(result);
}
document.querySelector('pre').textContent = result;
})
.catch((error) => {
errorDiv.style.display = 'block';
errorDiv.textContent = JSON.stringify(error.message);
})
.finally(() => {
submitButton.disabled = false; // post completed, so enable posting again
});
}
submitButton.addEventListener('click', exchangeJSON);
</script>
Na zapleczu:
Udostępniaj pliki statyczne z kodem DRY Program obsługi żądań i odpowiedzi zarówno dla pliku HTML, jak i ikony favicon, jest obecnie na stałe zakodowany na serwerze HTTP. Chociaż może to działać , wymaga irytującej ilości kodu standardowego i jest trochę brzydkie. Zastanów się, czy zamiast tego masz 4 czy 5 plików statycznych; Twoja obecna metoda nie jest skalowalna.
Wolę unikać bibliotek, chyba że istnieje ważny powód, aby z nich korzystać
To bardzo ważny powód, aby z nich korzystać. Chociaż prawdą jest, że często zawierają wiele funkcji, na których nie zależy, warto to zrobić ze względu na jedną lub dwie lub trzy nietrywialne funkcje, które w innym przypadku musiałbyś żmudnie wdrożyć samodzielnie.
Który? Poleciłbym Express, jego użycie jest bardzo rozpowszechnione i jest dobrze udokumentowane na ich stronie, w Stack Overflow i w wielu innych miejscach w Internecie.
Oddzielne trasy do różnych plików Chociaż zrefaktoryzowałem wszystkie z wyjątkiem jednego poniżej, w ogólnym przypadku, gdy masz wiele niestatycznych punktów końcowych na serwerze, które muszą obsługiwać inną logikę, rozważ rozdzielenie różnych tras na różne pliki. Na przykład, możesz mieć jeden plik, który eksportuje funkcję, która obsługuje ajaxTest1żądania, a inny plik, który eksportuje funkcję, która obsługuje loginżądania (na przykład). W miarę rozwoju aplikacji jest to znacznie łatwiejsze w utrzymaniu niż umieszczanie wszystkiego w jednym pliku.
'use strict';
const port = 8000;
const express = require('express');
// Recommended to use compression if you're transferring large files:
// https://github.com/expressjs/compression
const compression = require('compression');
const app = express();
app
.use(compression())
// Put static files into the "public" directory:
.use(express.static(__dirname + '/public'))
// Parse JSON request bodies:
.use(express.json())
.post('/ajaxTest1', (req, res) => {
console.log(req.body);
res.status(200).json({ data1: 'Test message from server', data2: 'could be a complex json object' });
})
.listen(port);
console.log(`Server is running on http://localhost:${port}. (^c to cancel)`);