이 책의 상태
당신이 읽고있는 것은 이미 책의 최종 버전입니다. 따라서 오류 수정 및 해당 수정이 새 버전의 Node.js에 이루어질 때만 업데이트됩니다.
이 책의 코드 케이스는 Node.js 버전 0.6.11에서 테스트되었으며 올바르게 작동 할 수 있습니다.
독자 대상
이 책은 나와 비슷한 기술적 배경을 가진 독자에게 가장 좋습니다. 적어도 Ruby, Python, PHP 또는 Java와 같은 객체 지향 언어에 대한 경험이 있습니다. JS.
이것은 다른 프로그래밍 언어에 대한 경험이있는 개발자를 말합니다.이 책은 데이터 유형, 변수, 제어 구조 등과 같은 매우 기본적인 개념을 소개하지 않습니다. 이 책을 이해하기 위해, 나는 당신이 이미 이러한 기본 개념을 알고 있다고 가정합니다.
그러나이 책은 다른 유사한 프로그래밍 언어의 함수 및 객체와 매우 다르기 때문에 JavaScript의 기능과 객체를 자세히 소개합니다.
이 책의 구조
이 책을 읽은 후에는 사용자가 페이지를 탐색하고 파일을 업로드 할 수있는 완전한 웹 응용 프로그램을 완료하게됩니다.
물론 응용 프로그램 자체는이 기능을 구현하기 위해 작성된 코드 자체와 비교할 수 없습니다. 매우 신비하지 않습니까? 나중에 이해할 것입니다.
이 책은 Node.js 환경의 JavaScript 개발과 브라우저 환경의 JavaScript 개발의 차이점을 도입하는 것으로 시작합니다.
그 후 즉시, 우리는 모든 사람들이 가장 기본적인 "Hello World"응용 프로그램을 완료하도록 이끌고 가장 기본적인 Node.js 응용 프로그램이기도합니다.
마지막으로, "실제"완전한 응용 프로그램을 설계하는 방법, 응용 프로그램을 완료하기 위해 구현 해야하는 다양한 모듈을 분석하고 이러한 모듈을 단계별로 구현하는 방법을 소개하는 방법에 대해 설명합니다.
보장되는 것은이 프로세스에서 JavaScript의 고급 개념, 사용 방법 및 왜 이러한 개념을 구현할 수 있는가 다른 프로그래밍 언어의 유사한 개념을 구현할 수 없다는 것입니다.
이 응용 프로그램의 모든 소스 코드는이 책의 github 코드 저장소를 통해 액세스 할 수 있습니다 : https://github.com/manuelkiessling/nodebeginnerbook/tree/master/code/application.
JavaScript 및 node.js
JavaScript와 당신
기술을 제쳐두고 당신과 JavaScript와의 관계에 대해 이야기합시다. 이 장의 주요 목적은 후속 장의 내용을 계속 읽어야하는지 확인하는 것입니다.
당신이 나와 같다면, 당신은 매우 일찍 HTML로 "개발"하기 시작했습니다.
당신이 정말로 원하는 것은 "Dry stuff"입니다. 복잡한 웹 사이트를 구축하는 방법을 알고 싶어서 PHP, Ruby, Java와 같은 프로그래밍 언어를 배우고 "백엔드"코드를 작성하기 시작합니다.
동시에, 당신은 항상 jquery 및 프로토 타입과 같은 기술에 대한 소개를 통해 항상 JavaScript에주의를 기울이고 있습니다. Open () 너무 간단합니다. .
그러나 이들은 결국 프론트 엔드 기술입니다. jquery를 사용하면 페이지를 향상시키고 싶을 때 항상 기분이 좋지만 JavaScript 개발자가 아닙니다.
그런 다음 서버의 Node.js, JavaScript, 이것이 얼마나 멋진가요?
따라서 친숙하고 익숙하지 않은 JavaScript를 다시 선택할 때가됩니다. 그러나 걱정하지 마십시오. Node.js 응용 프로그램을 작성하는 것은 그들이 작성 해야하는 이유를 이해하는 것이 JavaScript를 이해해야한다는 것을 의미합니다. 이번에는 그것을 진짜 연주했습니다.
여기에 문제가 있습니다 : JavaScript는 실제로 두 가지 또는 세 가지 형태로 존재하기 때문에 (1990 년대 DHTML을 향상시키는 작은 장난감에서 jQuery와 같은 엄격한 의미에서 프론트 엔드 기술에 이르기까지) 따라서 찾기가 어렵습니다. JavaScript를 배우는 "올바른"방법으로 Node.js 응용 프로그램을 작성할 때 사용하는 것보다 실제로 개발하는 것처럼 느낄 수 있습니다.
그것이 핵심이기 때문에 : 당신은 이미 경험이 풍부한 개발자이며 어디에서나 솔루션을 찾아 새로운 기술을 배우고 싶지 않습니다 (잘못된 기술이있을 수 있음).이 기술을 올바른 방식으로 배워야합니다.
물론 외부에는 훌륭한 자바 스크립트 학습 기사가 많이 있습니다. 그러나 때로는 기사만으로 의존하기에 충분하지 않습니다. 필요한 것은지도입니다.
이 책의 목표는 안내를 제공하는 것입니다.
짧은 진술
업계에는 매우 좋은 JavaScript 프로그래머가 있습니다. 그리고 나는 그들 중 하나가 아닙니다.
나는 이전 섹션에 설명 된 것입니다. 백엔드 웹 애플리케이션을 개발하는 방법에 익숙하지만 "실제"JavaScript 및 Node.js를 가진 초보자 일뿐입니다. 나는 최근에 고급 JavaScript 개념을 배웠으며 실용적인 경험이 없습니다.
그러므로이 책은 "입문에서 숙달로"책이 아니라 "소개에서 고급까지"라는 책과 비슷합니다.
성공하면이 책은 Node.js를 배우기 시작했을 때 가장 기대했던 튜토리얼입니다.
서버 JavaScript
JavaScript는 먼저 브라우저에서 실행되었지만 브라우저는 JavaScript로 수행 할 수있는 작업을 정의하지만 JavaScript 언어 자체가 수행 할 수있는 일에 대해 ""말하지 않는 컨텍스트를 제공합니다. 실제로 JavaScript는 "완전한"언어입니다. 다른 상황에서는 다른 유사한 언어보다 훨씬 큰 컨텍스트에서 사용할 수 있습니다.
node.js는 실제로 또 다른 컨텍스트로, 브라우저 환경에서 백엔드에서 JavaScript 코드를 실행할 수 있습니다.
백그라운드에서 실행되는 JavaScript 코드를 구현하려면 코드를 먼저 해석 한 다음 올바르게 실행해야합니다. 이것은 Google의 V8 가상 머신 (Google의 Chrome 브라우저에서 사용하는 JavaScript 실행 환경)을 사용하여 JavaScript 코드를 해석하고 실행하는 Node.js의 원칙입니다.
또한 Node.js와 함께 제공되는 많은 유용한 모듈이 있습니다.
따라서 Node.js는 실제로 런타임 환경과 라이브러리입니다.
Node.js를 사용하려면 먼저 설치해야합니다. 여기에서 Node.js를 설치하는 방법에 대한 자세한 내용은 공식 설치 안내서를 직접 참조 할 수 있습니다. 설치가 완료되면 계속 돌아와서이 책 아래의 내용을 읽으십시오.
"안녕하세요 세계"
자,“말도 안되는”말을 많이하지 말자. 첫 번째 node.js 응용 프로그램 인“Hello World”를 즉시 시작합시다.
좋아하는 편집기를 열고 HelloWorld.js 파일을 만듭니다. 다음과 같이 "Hello World"를 stdout에 출력하기 위해해야합니다.
코드 사본은 다음과 같습니다. console.log ( "Hello World");
파일을 저장하고 node.js를 통해 실행하십시오.
코드를 다음과 같이 복사하십시오. Node HelloWorld.js
정상적인 경우, Hello World는 터미널에서 출력됩니다.
좋아, 나는이 응용 프로그램이 약간 지루하다는 것을 인정하므로 약간의 "건조"를 가져 가자.
node.js를 기반으로 한 완전한 웹 응용 프로그램
사용 사례
목표를 단순하게 설정하겠습니다. 그러나 실용적이어야합니다.
1. 사용자는 브라우저를 통해 앱을 사용할 수 있습니다.
2. 사용자가 http : // domain/start를 요청하면 페이지에 파일 업로드 양식이 포함 된 환영 페이지를 볼 수 있습니다.
3. 사용자는 이미지를 선택하고 양식을 제출할 수 있습니다.
거의 완료되었습니다. 지금 Google에 가서 기능을 완료하기 위해 엉망으로 찾을 수 있습니다. 그러나 우리는 지금 이것을하지 않을 것입니다.
더 나아가이 목표를 달성하는 과정에서 코드가 우아한 지 여부에 관계없이 기본 코드 이상의 것이 필요합니다. 또한보다 복잡한 Node.js 응용 프로그램을 구축하는 방법을 찾으려면이를 추상화해야합니다.
다른 모듈의 적용
이 응용 프로그램을 분해하겠습니다. 위의 사용 사례를 구현하려면 어떤 부분을 구현해야합니까?
1. 웹 페이지를 제공해야하므로 HTTP 서버가 필요합니다.
2. 다른 요청의 경우, 우리 서버는 요청 URL에 따라 다른 응답을 제공해야하므로 요청 핸들러에 해당하는 경로가 필요합니다.
3. 서버에서 요청을 수신하고 경로를 통과하면 처리해야하므로 최종 요청 핸들러가 필요합니다.
4. 라우팅은 또한 사후 데이터를 처리하고 데이터를보다 친숙한 형식으로 캡슐화하여 요청 처리를 프로그램으로 전달할 수 있어야하므로 요청 데이터 처리 기능이 필요합니다.
5. URL에 해당하는 요청을 처리 할 필요가있을뿐만 아니라 컨텐츠를 표시하므로 컨텐츠를 사용자의 브라우저로 전송하려면 요청 핸들러에 대한 뷰 로직이 필요합니다.
6. 마지막으로 사용자는 이미지를 업로드해야 하므로이 측면의 세부 사항을 처리하려면 처리 기능을 업로드해야합니다.
먼저 PHP를 사용하는 경우이 구조를 어떻게 구축 할 것인지 생각해 봅시다. 일반적으로 Apache HTTP 서버를 사용하여 mod_php5 모듈과 일치합니다.
이러한 관점에서 "HTTP 요청 수신 및 웹 페이지 제공"의 전체 요구 사항은 PHP가 전혀 처리 할 필요가 없습니다.
그러나 Node.js의 경우 개념이 완전히 다릅니다. Node.js를 사용할 때는 하나의 애플리케이션을 구현할뿐만 아니라 전체 HTTP 서버를 구현하고 있습니다. 실제로 웹 응용 프로그램과 해당 웹 서버는 기본적으로 동일합니다.
해야 할 일이 많이있는 것처럼 들리지만, 우리는 이것이 Node.js의 번거 로움이 아니라는 것을 점차 깨닫게 될 것입니다.
이제 첫 번째 부분 인 HTTP 서버부터 구현 경로를 시작하겠습니다.
응용 프로그램의 모듈을 작성하십시오
기본 HTTP 서버
첫 "Real"Node.js 응용 프로그램을 작성하기 시작했을 때 Node.js 코드를 작성하는 방법뿐만 아니라 구성 방법도 몰랐습니다.
모든 것을 하나의 파일에 넣어야합니까? 온라인으로 모든 논리를 Node.js에 작성된 기본 HTTP 서버에 넣는 것을 가르치는 많은 튜토리얼이 있습니다. 그러나 코드를 읽을 수있는 상태에서 더 많은 콘텐츠를 추가하려면 어떻게해야합니까?
실제로 다른 기능의 코드를 다른 모듈에 넣는 한 코드를 분리하는 것은 매우 간단합니다.
이 메소드를 사용하면 Node.js로 실행할 수있는 깨끗한 기본 파일을 가질 수 있습니다.
이제 응용 프로그램을 시작하기위한 기본 파일과 HTTP 서버 코드를 보유하는 모듈을 만들어 봅시다.
내 인상에서, 기본 파일 index.js를 호출하는 것은 표준 형식입니다. 서버 모듈을 Server.js라는 파일에 넣는 것은 이해하기 쉽습니다.
서버 모듈부터 시작하겠습니다. 프로젝트의 루트 디렉토리에서 Server.js라는 파일을 작성하고 다음 코드를 작성하십시오.
코드 사본은 다음과 같습니다.
var http = 요구 사항 ( "http");
http.createserver (함수 (요청, 응답) {
response.writehead (200, { "content-type": "text/plain"});
Response.write ( "Hello World");
응답 ();
}). 듣기 (8888);
끝내십시오! 방금 작동하는 HTTP 서버를 완료했습니다. 이를 증명하기 위해이 코드를 실행하고 테스트합시다. 먼저 Node.js로 스크립트를 실행하십시오.
Node Server.js
다음으로 브라우저를 열고 http : // localhost : 8888/을 방문하면 "Hello World"가 작성된 웹 페이지가 표시됩니다.
이것은 흥미 롭지 않습니까? 먼저 HTTP 서버 문제에 대해 이야기하고 프로젝트를 구성하는 방법을 제쳐두고 어떻게 생각하십니까? 나중에 그 문제를 해결할 것이라고 약속합니다.
HTTP 서버를 분석하십시오
그런 다음이 HTTP 서버의 구성을 분석하겠습니다.
첫 번째 줄은 node.js와 함께 제공되는 HTTP 모듈을 요청하고 HTTP 변수에 할당합니다.
다음으로 HTTP 모듈에서 제공 한 함수 : CreateServer를 호출합니다. 이 함수는 Listen이라는 메소드가있는 객체를 반환합니다.
http.createserver의 괄호 안의 함수 정의를 당분간 무시합시다.
우리는 이와 같은 코드를 사용하여 서버를 시작하고 포트 8888을들을 수있었습니다.
코드 사본은 다음과 같습니다.
var http = 요구 사항 ( "http");
var server = http.createserver ();
Server.Listen (8888);
이 코드는 포트 8888을 듣는 서버 만 시작하며 다른 일을하지 않으며 요청에 응답하지 않습니다.
가장 흥미로운 (그리고 PHP와 같은 보수적 인 언어를 사용하는 데 익숙하다면 이상하다)는 기능 정의 인 CreateSever ()에게 첫 번째 논쟁이다.
실제로이 함수 정의는 CreateServer ()의 첫 번째이자 유일한 매개 변수입니다. JavaScript에서는 기능이 다른 변수와 마찬가지로 전달 될 수 있습니다.
기능 패스를 수행하십시오
예를 들어 다음을 수행 할 수 있습니다.
코드 사본은 다음과 같습니다.
함수는 (단어) {
Console.log (Word);
}
함수 실행 (일부 기능, 값) {
약간의 기능 (값);
}
실행 ( "Hello");
이 코드를주의 깊게 읽으십시오! 여기서 우리는 Say 함수를 실행 함수의 첫 번째 변수로 전달합니다. 여기서 반환되는 것은 Say의 반환 가치가 아니라 말 자체입니다!
이런 식으로, 실행중인 로컬 변수 약간의 기능은 say 기능을 사용할 수 있습니다 (괄호 안에서).
물론, Say는 변수가 있기 때문에 Execute는 약간의 기능을 호출 할 때 그러한 변수를 전달할 수 있습니다.
우리는 지금했던 것처럼 이름이있는 변수로 함수를 전달할 수 있습니다. 그러나 우리는이 "먼저 정의 한 다음 전달"을 주위로 돌릴 필요가 없습니다.
코드 사본은 다음과 같습니다.
함수 실행 (일부 기능, 값) {
약간의 기능 (값);
}
execute (function (word) {console.log (word)}, "hello");
우리는 Execute에 의해 첫 번째 매개 변수가 허용되는 곳에서 실행하기 위해 전달하려는 함수를 직접 정의합니다.
이런 식으로, 우리는이 기능의 이름을 지정할 필요조차 없으므로 익명 기능이라고합니다.
이것은 내가 "고급"JavaScript라고 생각하는 것과 처음으로 밀접하게 접촉하지만, 우리는 여전히 단계적으로 가야합니다. 이제 이것을 먼저 받아들이자 : JavaScript에서 함수는 다른 함수로 매개 변수를 수신 할 수 있습니다. 먼저 함수를 정의한 다음 전달하거나 매개 변수가 전달되는 함수를 직접 정의 할 수 있습니다.
기능 패스 스루가 어떻게 HTTP 서버가 작동하게합니까?
이 지식으로 간단하지만 간단하지 않은 HTTP 서버를 살펴 보겠습니다.
코드 사본은 다음과 같습니다.
var http = 요구 사항 ( "http");
http.createserver (함수 (요청, 응답) {
response.writehead (200, { "content-type": "text/plain"});
Response.write ( "Hello World");
응답 ();
}). 듣기 (8888);
이제 훨씬 더 명확 해 보일 것입니다 : 우리는 익명의 기능을 CreateServer 함수에 전달했습니다.
그러한 코드를 사용하여 동일한 목적을 달성 할 수 있습니다.
코드 사본은 다음과 같습니다.
var http = 요구 사항 ( "http");
onrequest (요청, 응답) {
response.writehead (200, { "content-type": "text/plain"});
Response.write ( "Hello World");
응답 ();
}
http.createserver (onrequest) .listen (8888);
어쩌면 지금 우리는이 질문을해야 할 것입니다. 왜 우리는이 방법을 사용합니까?
이벤트 중심 콜백
이 질문은 대답하기 어렵지만 (적어도 나에게는) 이것이 Node.js가 기본적으로 작동하는 방식입니다. 이벤트 중심이므로 너무 빠릅니다.
Felix Geisendörfer의 걸작 이해 Node.js를 읽는 데 시간이 걸릴 수도 있습니다.
그것은 모두 "node.js가 이벤트 구동"이라는 사실에 달려 있습니다. 글쎄, 나는이 문장의 의미를 정말로 이해하지 못한다. 그러나 Node.js를 사용하여 웹 응용 프로그램을 작성하는 것이 왜 의미가 있는지 설명하려고 노력할 것입니다.
http.createserver 메소드를 사용하면 특정 포트를 듣는 서버를 원할뿐만 아니라 서버가 HTTP 요청을받을 때 무언가를하기를 원합니다.
문제는 이것이 비동기식이라는 것입니다. 요청은 언제든지 도착할 수 있지만 서버는 단일 프로세스에서 실행됩니다.
PHP 애플리케이션을 작성할 때는 요청이 들어올 때마다 웹 서버 (일반적으로 APACHE)가 요청에 대한 새로운 프로세스를 생성하고 해당 PHP 스크립트를 처음부터 끝까지 실행하기 시작합니다.
따라서 Node.js 프로그램에서 새로운 요청이 Port 8888에 도달하면 프로세스를 어떻게 제어합니까?
글쎄, 그것은 Node.js/JavaScript의 이벤트 중심 디자인이 실제로 도움이 될 수있는 곳입니다. 그러나 우리는 여전히 그것을 마스터하기 위해 새로운 개념을 배워야합니다. 이러한 개념이 서버 코드에 어떻게 적용되는지 살펴 보겠습니다.
우리는 서버를 생성하고이를 생성 한 메소드에 함수를 전달했습니다. 서버가 요청을받을 때 마다이 기능이 호출됩니다.
우리는 이것이 언제 일어날 지 알지 못하지만 이제 우리는 요청을 처리 할 장소가 있습니다. 그것은 우리가 과거에 통과 한 기능입니다. 그것이 사전 정의 된 함수인지 익명 함수인지는 중요하지 않습니다.
이것은 전설적인 콜백입니다. 해당 이벤트가 발생할 때이 기능을 호출하는 메소드에 함수를 전달합니다.
적어도 나를 위해서는 그것을 이해하려면 약간의 노력이 필요합니다. 아직 확실하지 않은 경우 Felix의 블로그 게시물을 읽으십시오.
이 새로운 개념에 대해 다시 생각합시다. 서버를 작성한 후 HTTP 요청이 없어도 콜백 기능이 호출되지 않더라도 코드가 계속 유효하다는 것을 어떻게 증명합니까? 이것을 시도해 봅시다 :
코드 사본은 다음과 같습니다.
var http = 요구 사항 ( "http");
onrequest (요청, 응답) {
Console.log ( "요청 수신");
response.writehead (200, { "content-type": "text/plain"});
Response.write ( "Hello World");
응답 ();
}
http.createserver (onrequest) .listen (8888);
Console.log ( "서버가 시작되었습니다.");
참고 : onrequest (콜백 함수)가 트리거되는 경우 Console.log를 사용하여 텍스트를 출력합니다. HTTP 서버가 작동하기 시작하면 텍스트가 출력됩니다.
평소와 같이 Node Server.js를 실행하면 "서버가 시작되었습니다. 서버에 요청할 때 (http : // localhost : 8888/브라우저에서) "수신 된 요청이 표시됩니다."
이것은 이벤트 중심의 비동기 서버 측 JavaScript 및 해당 콜백입니다!
(서버에서 웹 페이지에 액세스 할 때 서버는 "요청 수신"을 출력 할 수 있습니다. 두 번. 이는 대부분의 서버가 http : // localhost : 8888/: // localhost : 8888을 방문 할 때 http를 읽으려고하기 때문입니다. /favicon.ico)
서버가 요청을 어떻게 처리합니까?
자, 서버 코드의 나머지 부분, 즉 콜백 함수 onrequest ()의 주요 부분을 간단히 분석 해 봅시다.
콜백이 시작되고 OnRequest () 함수가 트리거되면 요청 및 응답의 두 매개 변수가 전달됩니다.
이들은 객체이며 방법을 사용하여 HTTP 요청의 세부 사항을 처리하고 요청에 응답 할 수 있습니다 (요청을하는 브라우저로 다시 보내는 것).
따라서 우리의 코드는 다음과 같습니다. 요청을 수신 할 때 response.writehead () 함수를 사용하여 HTTP 상태 200 및 컨텐츠 유형을 HTTP 헤더의 컨텐츠 유형을 보내고 응답 () 함수를 사용하여 해당 HTTP 본문에서 텍스트를 보냅니다. "안녕하세요 세계".
마지막으로 응답을 완료하기 위해 Response.end ()를 호출합니다.
현재 우리는 요청의 세부 사항을 신경 쓰지 않으므로 요청 객체를 사용하지 않습니다.
서버 모듈을 넣을 위치
좋아, 내가 약속했듯이, 우리는 이제 우리가 응용 프로그램을 구성하는 방법으로 돌아갈 수 있습니다. 이제 Server.js 파일에 매우 기본적인 HTTP 서버 코드가 있으며 일반적으로 응용 프로그램의 다른 모듈 (예 : Server.js의 HTTP 서버 모듈)을 호출하는 index.js라는 파일이 있다고 언급했습니다. 응용 프로그램을 시작하십시오.
Server.js를 실제 node.js 모듈로 바꾸는 방법에 대해 이야기 해 봅시다.
어쩌면 우리는 코드에서 모듈을 사용했다는 것을 알았을 것입니다. 이와 같이:
코드 사본은 다음과 같습니다.
var http = 요구 사항 ( "http");
...
http.createserver (...);
Node.js에는 "http"라는 모듈이 포함되어 있습니다.
이것은 우리의 로컬 변수를 HTTP 모듈에서 제공하는 모든 공개 방법을 갖춘 객체로 바꿉니다.
이 로컬 변수를 모듈 이름과 동일한 이름으로 제공하는 것은 컨벤션이지만 선호도를 따를 수도 있습니다.
코드 사본은 다음과 같습니다.
var foo = 요구 사항 ( "http");
...
foo.createserver (...);
매우 좋습니다. Node.js 내부 모듈을 사용하는 방법은 분명합니다. 우리는 어떻게 우리 자신의 모듈을 만들고 어떻게 사용합니까?
Server.js를 실제 모듈로 전환하면 이해할 것입니다.
실제로, 우리는 너무 많은 수정을 할 필요가 없습니다. 코드를 모듈로 전환한다는 것은 모듈을 요청하는 스크립트에 기능을 제공하려는 부분을 내보내야한다는 것을 의미합니다.
현재 HTTP 서버가 내보내는 기능은 매우 간단합니다. 서버 모듈을 요청하는 스크립트는 서버를 시작하면됩니다.
서버 스크립트를 START라는 함수에 넣고이 기능을 내보낼 것입니다.
코드 사본은 다음과 같습니다.
var http = 요구 사항 ( "http");
함수 start () {
onrequest (요청, 응답) {
Console.log ( "요청 수신");
response.writehead (200, { "content-type": "text/plain"});
Response.write ( "Hello World");
응답 ();
}
http.createserver (onrequest) .listen (8888);
Console.log ( "서버가 시작되었습니다.");
}
Exports.start = 시작;
이렇게하면 서버의 코드가 여전히 Server.js에 있지만 메인 파일 index.js를 만들고 HTTP를 시작할 수 있습니다.
index.js 파일을 만들고 다음을 작성하십시오.
코드 사본은 다음과 같습니다.
var server = require ( "./ server");
server.start ();
보시다시피, 우리는 다른 내장 모듈과 마찬가지로 서버 모듈을 사용할 수 있습니다.
괜찮은. 이제 메인 스크립트에서 응용 프로그램을 시작할 수 있으며 여전히 동일합니다.
코드 사본은 다음과 같습니다.
노드 index.js
매우 좋습니다. 이제 응용 프로그램의 다른 부분을 다른 파일에 넣고 모듈을 생성하여 함께 연결할 수 있습니다.
우리는 여전히 전체 응용 프로그램의 초기 부분 만 가지고 있습니다. HTTP 요청을받을 수 있습니다. 그러나 우리는 무언가를해야합니다. 서버는 다른 URL 요청에 대해 다른 반응을 가져야합니다.
매우 간단한 응용 프로그램의 경우 콜백 함수 onrequest ()에서 직접 수행 할 수 있습니다. 그러나 내가 말했듯이, 우리는 예제를 좀 더 흥미롭게 만들기 위해 추상적 요소를 추가해야합니다.
다른 HTTP 요청을 처리하는 것은 코드에서 "라우팅"이라는 다른 부분입니다. 따라서 라우팅이라는 모듈을 만들어 봅시다.
"라우팅"을 요청하는 방법
경로에 요청 된 URL 및 기타 필요한 GET 및 게시 매개 변수를 제공하고 경로는이 데이터를 기반으로 해당 코드를 실행해야합니다 (여기서 "코드"는 전체 응용 프로그램의 세 번째 부분에 해당합니다. 요청 핸들러를받을 때 실제 작업).
따라서 HTTP 요청을보고 요청 된 URL을 추출하고 Get/Post 매개 변수를 추출해야합니다. 이 기능이 라우팅 또는 서버 (모듈의 자체 기능으로도)에 속하면 HTTP 서버의 기능입니다.
필요한 모든 데이터는 요청 객체에 포함되어 있으며 onrequest () 콜백 함수의 첫 번째 매개 변수로 전달됩니다. 그러나이 데이터를 구문 분석하려면 각각 URL 및 QueryString 모듈 인 추가 Node.js 모듈이 필요합니다.
코드 사본은 다음과 같습니다.
url.parse (String) .Query
url.parse (string) .pathname |
|. | |
--------------------------------------------------------- --------------------------------------------------------- ----------------------------
http : // localhost : 8888/start? foo = bar & hello = world
--- ------
|. | |
QueryString (String) [ "foo"] |
QueryString (String) [ "Hello"]
물론 QueryString 모듈을 사용하여 게시물 요청 본문의 매개 변수를 구문 분석 할 수 있으며 나중에 데모가 있습니다.
이제 OnRequest () 함수에 논리를 추가하여 브라우저에서 요청한 URL 경로를 찾으십시오.
var http = 요구 사항 ( "http");
var url = require ( "url");
함수 start () {
onrequest (요청, 응답) {
var pathname = url.parse (request.url) .pathname;
console.log ( "" + pathname + "에 대한 요청");
response.writehead (200, { "content-type": "text/plain"});
Response.write ( "Hello World");
응답 ();
}
http.createserver (onrequest) .listen (8888);
Console.log ( "서버가 시작되었습니다.");
}
Exports.start = 시작;
글쎄, 당사의 앱은 이제 요청 된 URL 경로별로 다른 요청을 구별 할 수 있습니다. 따라서 우리는 핸들러의 URL 경로를 기반으로 라우팅 (아직 완료되지 않은)을 사용하여 핸들러에 요청을 매핑 할 수 있습니다.
우리가 구축하는 응용 프로그램에서, 이것은 /시작 및 /업로드의 요청이 다른 코드로 처리 될 수 있음을 의미합니다. 이 콘텐츠가 나중에 어떻게 정리되는지 살펴 보겠습니다.
이제 경로를 작성하고 router.js라는 파일을 만들고 다음 내용을 추가 할 수 있습니다.
기능 경로 (PathName) {
Console.log ( "" + pathname에 대한 요청을 라우팅하려면);
}
Exports.route = 경로;
보시다시피,이 코드는 아무것도하지 않지만 지금은 그것이되어야합니다. 더 많은 논리를 추가하기 전에 먼저 라우팅 및 서버를 통합하는 방법을 살펴 보겠습니다.
당사의 서버는 경로의 존재를 알고 효과적으로 사용해야합니다. 물론 우리는 하드 코딩으로 서버 에이 종속성을 바인딩 할 수 있지만 다른 언어의 프로그래밍 경험은 이것이 매우 고통스러운 일이 될 것이므로 종속성 주입을 사용하여 더 느슨하게 모듈을 추가 할 것입니다 (읽을 수 있습니다. 배경 지식으로서 의존성 주입에 대한 Martin Fowlers의 걸작).
먼저, 라우팅 함수가 매개 변수로 전달되도록 서버의 start () 함수를 확장하겠습니다.
코드 사본은 다음과 같습니다.
var http = 요구 사항 ( "http");
var url = require ( "url");
함수 시작 (경로) {
onrequest (요청, 응답) {
var pathname = url.parse (request.url) .pathname;
console.log ( "" + pathname + "에 대한 요청");
경로 (PathName);
response.writehead (200, { "content-type": "text/plain"});
Response.write ( "Hello World");
응답 ();
}
http.createserver (onrequest) .listen (8888);
Console.log ( "서버가 시작되었습니다.");
}
Exports.start = 시작;
동시에, 우리는 라우팅 함수가 서버에 주입 될 수 있도록 index.js를 확장합니다.
코드 사본은 다음과 같습니다.
var server = require ( "./ server");
var router = require ( "./ router");
server.start (router.route);
여기서 우리가 전달하는 기능은 아무것도하지 않습니다.
지금 응용 프로그램을 시작한 후 (Node Index.js, 항상이 명령 줄을 기억하십시오) URL을 요청하면 응용 프로그램이 해당 정보를 출력하여 HTTP 서버가 이미 라우팅 모듈을 사용하고 있음을 나타내는 것을 나타냅니다. 그것을 요청하십시오. 경로로가는 길 :
코드 사본은 다음과 같습니다.
bash $ node index.js
수신 /foo 요청.
/foo에 대한 요청을 라우팅하려고합니다
(위의 출력은 더 성가신 /favicon.ico 요청 관련 부품을 제거했습니다).
행동 중심의 실행
주제에서 다시 벗어나 기능 프로그래밍에 대해 이야기 할 수 있습니다.
매개 변수로서 기능을 전달하는 것은 기술적 인 고려 사항만을위한 것이 아닙니다. 소프트웨어 디자인의 경우 이것은 실제로 철학적 질문입니다. 이 시나리오를 생각해보십시오. 인덱스 파일에서 라우터 객체를 전달할 수 있으며 서버는이 객체의 경로 함수를 호출 할 수 있습니다.
이와 같이, 우리는 무언가를 전달하고 서버는 이것을 사용하여 무언가를 성취합니다. 안녕하세요, 라우팅이라는 점, 이걸로 도와 줄 수 있습니까?
그러나 서버는 실제로 그런 것들이 필요하지 않습니다. 실제로 일을 마치면됩니다. 사물을 끝내려면 전혀 필요하지 않습니다. 즉, 명사가 필요하지 않으며 동사가 필요합니다.
이 개념에서 가장 핵심적이고 기본적인 아이디어를 이해 한 후에는 기능 프로그래밍을 자연스럽게 이해했습니다.
나는 Steve Yegge의 걸작을 읽은 후 명사 왕국의 사형을 읽은 후 기능 프로그래밍을 이해했습니다. 당신은 정말로이 책을 읽었습니다. 이것은 저에게 독서의 기쁨을 준 소프트웨어에 관한 책 중 하나입니다.
실제 요청 처리기로 라우팅
주제로 돌아가서, 우리의 HTTP 서버와 요청 라우팅 모듈은 이제 우리가 예상 한대로, 가까운 형제처럼 서로 통신 할 수 있습니다.
물론, 이것은 이름에서 알 수 있듯이 충분하지 않습니다. 예를 들어, 처리/시작의 "비즈니스 로직"은 처리/업로드와는 달라야합니다.
현재 구현을 사용하면 라우팅 프로세스가 라우팅 모듈에서 "종료"되며 라우팅 모듈은 요청에 대해 실제로 "조치를 취하는"모듈이 아닙니다. 그렇지 않으면 응용 프로그램이 더 복잡하게 확장 될 때는 그리 좋지 않습니다.
우리는 라우팅 대상이 요청 핸들러 인 함수를 일시적으로 호출합니다. 이제 요청 핸들러가 준비되지 않은 경우 라우팅 모듈을 개선하는 것은 의미가 없기 때문에 라우팅 모듈을 개발하기 위해 서두르지 않아야합니다.
애플리케이션에는 새로운 구성 요소가 필요하므로 새 모듈을 추가 할 수 있습니다. 더 이상 소설이 필요하지 않습니다. requestHandlers라는 모듈을 만들고 각 요청 핸들러에 대해 자리 표시 자 기능을 추가 한 다음 이러한 기능을 모듈 메소드로 내보내겠습니다.
코드 사본은 다음과 같습니다.
함수 start () {
Console.log ( "요청 핸들러 '시작'이 호출되었습니다.");
}
함수 upload () {
Console.log ( "요청 처리기 '업로드'가 호출되었습니다.");
}
Exports.start = 시작;
Exports.upload = 업로드;
이러한 방식으로 요청 핸들러와 라우팅 모듈을 연결하여 경로를 "찾을 수있는 방법"을 연결할 수 있습니다.
여기서 우리는 결정을 내려야합니다. 요청 핸들러 모듈을 사용하기 위해 경로로 하드 코딩해야합니까, 아니면 조금 더 종속성 주입을 추가해야합니까? 다른 모드와 마찬가지로 의존성 주입을 사용하는 데만 사용해서는 안됩니다.이 경우 종속성 주입을 사용하면 경로와 요청 핸들러 간의 커플 링을 풀어 경로를 더 재사용 할 수 있습니다.
즉, 요청 핸들러를 서버에서 경로로 전달해야하지만이를 수행하는 것이 더 터무니없는 느낌이 듭니다. 경로로.
그렇다면이 요청 처리기를 어떻게 통과합니까? 실제 응용 프로그램에서는 2 개의 핸들러가 있지만 요청 처리기 수는 계속 증가 할 것입니다 핸들러에게 반복적으로. 또한 요청 == X 인 If request == x와 경로에서 핸들러 y를 호출하여 시스템을 추악하게 만듭니다.
그것에 대해주의 깊게 생각해보십시오. 많은 것들이 있으며, 각각은 문자열에 매핑되어야합니까 (즉, 요청 된 URL)? 연관 배열은 완벽하게 유능 할 수 있습니다.
그러나 그 결과는 약간 실망 스럽습니다. JavaScript는 연관 배열을 제공하지 않습니다. 또한 제공한다고 말할 수 있습니까? 실제로 JavaScript에서 이러한 종류의 기능을 실제로 제공하는 것은 객체입니다.
이와 관련하여 http://msdn.microsoft.com/en-us/magazine/cc163419.aspx는 좋은 소개를 가지고 있으며 여기에서 발췌 할 것입니다.
C ++ 또는 C#에서 객체에 대해 이야기 할 때 클래스 또는 구조 인스턴스를 참조합니다. 객체는 인스턴스팅 된 템플릿 (즉, 소위 클래스)을 기반으로 다른 속성과 방법을 갖습니다. 그러나 JavaScript 객체에서는이 개념이 아닙니다. JavaScript에서 객체는 키/값 쌍의 컬렉션입니다. JavaScript 객체를 문자열 유형으로 키를 가진 사전으로 생각할 수 있습니다.
그러나 JavaScript 객체가 단지 키/값 쌍의 컬렉션이라면 어떻게 방법을 가질 수 있습니까? 글쎄, 여기의 값은 문자열, 숫자 또는 ... 함수 일 수 있습니다!
좋아, 마지막에 코드로 돌아가 봅시다. 이제 우리는 일련의 요청 핸들러를 객체를 통해 전달하기로 결정 했으며이 개체를 느슨하게 결합 된 방식으로 Route () 함수에 주입해야합니다.
먼저이 개체를 기본 파일 index.js에 소개하겠습니다.
코드 사본은 다음과 같습니다.
var server = require ( "./ server");
var router = require("./router");
var requestHandlers = require("./requestHandlers");
var handle = {}
handle["/"] = requestHandlers.start;
handle["/start"] = requestHandlers.start;
handle["/upload"] = requestHandlers.upload;
server.start(router.route, handle);
虽然handle并不仅仅是一个“东西”(一些请求处理程序的集合),我还是建议以一个动词作为其命名,这样做可以让我们在路由中使用更流畅的表达式,稍后会有说明。
正如所见,将不同的URL映射到相同的请求处理程序上是很容易的:只要在对象中添加一个键为"/"的属性,对应requestHandlers.start即可,这样我们就可以干净简洁地配置/start和/的请求都交由start这一处理程序处理。
在完成了对象的定义后,我们把它作为额外的参数传递给服务器,为此将server.js修改如下:
코드 사본은 다음과 같습니다.
var http = require("http");
var url = require("url");
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
route(handle, pathname);
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello World");
response.end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
这样我们就在start()函数里添加了handle参数,并且把handle对象作为第一个参数传递给了route()回调函数。
然后我们相应地在route.js文件中修改route()函数:
코드 사본은 다음과 같습니다.
function route(handle, pathname) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname]();
} 또 다른 {
console.log("No request handler found for " + pathname);
}
}
exports.route = route;
通过以上代码,我们首先检查给定的路径对应的请求处理程序是否存在,如果存在的话直接调用相应的函数。我们可以用从关联数组中获取元素一样的方式从传递的对象中获取请求处理函数,因此就有了简洁流畅的形如handle[pathname]();的表达式,这个感觉就像在前方中提到的那样:“嗨,请帮我处理了这个路径”。
有了这些,我们就把服务器、路由和请求处理程序在一起了。现在我们启动应用程序并在浏览器中访问http://localhost:8888/start,以下日志可以说明系统调用了正确的请求处理程序:
코드 사본은 다음과 같습니다.
Server has started.
Request for /start received.
About to route a request for /start
Request handler 'start' was called.
并且在浏览器中打开http://localhost:8888/可以看到这个请求同样被start请求处理程序处理了:
코드 사본은 다음과 같습니다.
Request for / received.
About to route a request for /
Request handler 'start' was called.
让请求处理程序作出响应
매우 좋은.不过现在要是请求处理程序能够向浏览器返回一些有意义的信息而并非全是“Hello World”,那就更好了。
这里要记住的是,浏览器发出请求后获得并显示的“Hello World”信息仍是来自于我们server.js文件中的onRequest函数。
其实“处理请求”说白了就是“对请求作出响应”,因此,我们需要让请求处理程序能够像onRequest函数那样可以和浏览器进行“对话”。
不好的实现方式
对于我们这样拥有PHP或者Ruby技术背景的开发者来说,最直截了当的实现方式事实上并不是非常靠谱: 看似有效,实则未必如此。
这里我指的“直截了当的实现方式”意思是:让请求处理程序通过onRequest函数直接返回(return())他们要展示给用户的信息。
我们先就这样去实现,然后再来看为什么这不是一种很好的实现方式。
让我们从让请求处理程序返回需要在浏览器中显示的信息开始。我们需要将requestHandler.js修改为如下形式:
코드 사본은 다음과 같습니다.
function start() {
console.log("Request handler 'start' was called.");
return "Hello Start";
}
function upload() {
console.log("Request handler 'upload' was called.");
return "Hello Upload";
}
exports.start = start;
exports.upload = upload;
좋아요同样的,请求路由需要将请求处理程序返回给它的信息返回给服务器。因此,我们需要将router.js修改为如下形式:
코드 사본은 다음과 같습니다.
function route(handle, pathname) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
return handle[pathname]();
} 또 다른 {
console.log("No request handler found for " + pathname);
return "404 Not found";
}
}
exports.route = route;
正如上述代码所示,当请求无法路由的时候,我们也返回了一些相关的错误信息。
最后,我们需要对我们的server.js进行重构以使得它能够将请求处理程序通过请求路由返回的内容响应给浏览器,如下所示:
코드 사본은 다음과 같습니다.
var http = require("http");
var url = require("url");
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
response.writeHead(200, {"Content-Type": "text/plain"});
var content = route(handle, pathname)
response.write(content);
response.end();
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
如果我们运行重构后的应用,一切都会工作的很好:请求http://localhost:8888/start,浏览器会输出“Hello Start”,请求http://localhost:8888/upload会输出“Hello Upload”,而请求http://localhost:8888/foo 会输出“404 Not found”。
好,那么问题在哪里呢?简单的说就是: 当未来有请求处理程序需要进行非阻塞的操作的时候,我们的应用就“挂”了。
没理解?没关系,下面就来详细解释下。
阻塞与非阻塞
正如此前所提到的,当在请求处理程序中包括非阻塞操作时就会出问题。但是,在说这之前,我们先来看看什么是阻塞操作。
我不想去解释“阻塞”和“非阻塞”的具体含义,我们直接来看,当在请求处理程序中加入阻塞操作时会发生什么。
这里,我们来修改下start请求处理程序,我们让它等待10秒以后再返回“Hello Start”。因为,JavaScript中没有类似sleep()这样的操作,所以这里只能够来点小Hack来模拟实现。
让我们将requestHandlers.js修改成如下形式:
코드 사본은 다음과 같습니다.
function start() {
console.log("Request handler 'start' was called.");
function sleep(milliSeconds) {
var startTime = new Date().getTime();
while (new Date().getTime() < startTime + milliSeconds);
}
sleep(10000);
return "Hello Start";
}
function upload() {
console.log("Request handler 'upload' was called.");
return "Hello Upload";
}
exports.start = start;
exports.upload = upload;
上述代码中,当函数start()被调用的时候,Node.js会先等待10秒,之后才会返回“Hello Start”。当调用upload()的时候,会和此前一样立即返回。
(当然了,这里只是模拟休眠10秒,实际场景中,这样的阻塞操作有很多,比方说一些长时间的计算操作等。)
接下来就让我们来看看,我们的改动带来了哪些变化。
如往常一样,我们先要重启下服务器。为了看到效果,我们要进行一些相对复杂的操作(跟着我一起做): 首先,打开两个浏览器窗口或者标签页。在第一个浏览器窗口的地址栏中输入http://localhost:8888/start, 但是先不要打开它!
在第二个浏览器窗口的地址栏中输入http://localhost:8888/upload, 同样的,先不要打开它!
接下来,做如下操作:在第一个窗口中(“/start”)按下回车,然后快速切换到第二个窗口中(“/upload”)按下回车。
注意,发生了什么: /start URL加载花了10秒,这和我们预期的一样。但是,/upload URL居然也花了10秒,而它在对应的请求处理程序中并没有类似于sleep()这样的操作!
이게 왜?原因就是start()包含了阻塞操作。形象的说就是“它阻塞了所有其他的处理工作”。
这显然是个问题,因为Node一向是这样来标榜自己的:“在node中除了代码,所有一切都是并行执行的”。
这句话的意思是说,Node.js可以在不新增额外线程的情况下,依然可以对任务进行并行处理―― Node.js是单线程的。它通过事件轮询(event loop)来实现并行操作,对此,我们应该要充分利用这一点―― 尽可能的避免阻塞操作,取而代之,多使用非阻塞操作。
然而,要用非阻塞操作,我们需要使用回调,通过将函数作为参数传递给其他需要花时间做处理的函数(比方说,休眠10秒,或者查询数据库,又或者是进行大量的计算)。
对于Node.js来说,它是这样处理的:“嘿,probablyExpensiveFunction()(译者注:这里指的就是需要花时间处理的函数),你继续处理你的事情,我(Node.js线程)先不等你了,我继续去处理你后面的代码,请你提供一个callbackFunction(),等你处理完之后我会去调用该回调函数的,谢谢!”
(如果想要了解更多关于事件轮询细节,可以阅读Mixu的博文――理解node.js的事件轮询。)
接下来,我们会介绍一种错误的使用非阻塞操作的方式。
和上次一样,我们通过修改我们的应用来暴露问题。
这次我们还是拿start请求处理程序来“开刀”。将其修改成如下形式:
코드 사본은 다음과 같습니다.
var exec = require("child_process").exec;
function start() {
console.log("Request handler 'start' was called.");
var content = "empty";
exec("ls -lah", function (error, stdout, stderr) {
content = stdout;
});
return content;
}
function upload() {
console.log("Request handler 'upload' was called.");
return "Hello Upload";
}
exports.start = start;
exports.upload = upload;
上述代码中,我们引入了一个新的Node.js模块,child_process。之所以用它,是为了实现一个既简单又实用的非阻塞操作:exec()。
exec()做了什么呢?它从Node.js来执行一个shell命令。在上述例子中,我们用它来获取当前目录下所有的文件(“ls -lah”),然后,当/startURL请求的时候将文件信息输出到浏览器中。
上述代码是非常直观的: 创建了一个新的变量content(初始值为“empty”),执行“ls -lah”命令,将结果赋值给content,最后将content返回。
和往常一样,我们启动服务器,然后访问“http://localhost:8888/start” 。
之后会载入一个漂亮的web页面,其内容为“empty”。 무슨 일이야?
这个时候,你可能大致已经猜到了,exec()在非阻塞这块发挥了神奇的功效。它其实是个很好的东西,有了它,我们可以执行非常耗时的shell操作而无需迫使我们的应用停下来等待该操作。
(如果想要证明这一点,可以将“ls -lah”换成比如“find /”这样更耗时的操作来效果)。
然而,针对浏览器显示的结果来看,我们并不满意我们的非阻塞操作,对吧?
好,接下来,我们来修正这个问题。在这过程中,让我们先来看看为什么当前的这种方式不起作用。
问题就在于,为了进行非阻塞工作,exec()使用了回调函数。
在我们的例子中,该回调函数就是作为第二个参数传递给exec()的匿名函数:
코드 사본은 다음과 같습니다.
function (error, stdout, stderr) {
content = stdout;
}
现在就到了问题根源所在了:我们的代码是同步执行的,这就意味着在调用exec()之后,Node.js会立即执行return content ;在这个时候,content仍然是“empty”,因为传递给exec()的回调函数还未执行到――因为exec()的操作是异步的。
我们这里“ls -lah”的操作其实是非常快的(除非当前目录下有上百万个文件)。这也是为什么回调函数也会很快的执行到―― 不过,不管怎么说它还是异步的。
为了让效果更加明显,我们想象一个更耗时的命令: “find /”,它在我机器上需要执行1分钟左右的时间,然而,尽管在请求处理程序中,我把“ls -lah”换成“find /”,当打开/start URL的时候,依然能够立即获得HTTP响应―― 很明显,当exec()在后台执行的时候,Node.js自身会继续执行后面的代码。并且我们这里假设传递给exec()的回调函数,只会在“find /”命令执行完成之后才会被调用。
那究竟我们要如何才能实现将当前目录下的文件列表显示给用户呢?
好,了解了这种不好的实现方式之后,我们接下来来介绍如何以正确的方式让请求处理程序对浏览器请求作出响应。
以非阻塞操作进行请求响应
我刚刚提到了这样一个短语―― “正确的方式”。而事实上通常“正确的方式”一般都不简单。
不过,用Node.js就有这样一种实现方案: 函数传递。下面就让我们来具体看看如何实现。
So far, our application can pass values between the application layers (request handler->request routing->server) content returned by the request handler (the content that the request handler will eventually display to the user) Pass to the HTTP server .
现在我们采用如下这种新的实现方式:相对采用将内容传递给服务器的方式,我们这次采用将服务器“传递”给内容的方式。 从实践角度来说,就是将response对象(从服务器的回调函数onRequest()获取)通过请求路由传递给请求处理程序。 随后,处理程序就可以采用该对象上的函数来对请求作出响应。
原理就是如此,接下来让我们来一步步实现这种方案。
先从server.js开始:
코드 사본은 다음과 같습니다.
var http = require("http");
var url = require("url");
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
route(handle, pathname, response);
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
相对此前从route()函数获取返回值的做法,这次我们将response对象作为第三个参数传递给route()函数,并且,我们将onRequest()处理程序中所有有关response的函数调都移除,因为我们希望这部分工作让route()函数来完成。
下面就来看看我们的router.js:
코드 사본은 다음과 같습니다.
function route(handle, pathname, response) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname](response);
} 또 다른 {
console.log("No request handler found for " + pathname);
response.writeHead(404, {"Content-Type": "text/plain"});
response.write("404 Not found");
response.end();
}
}
exports.route = route;
同样的模式:相对此前从请求处理程序中获取返回值,这次取而代之的是直接传递response对象。
如果没有对应的请求处理器处理,我们就直接返回“404”错误。
最后,我们将requestHandler.js修改为如下形式:
코드 사본은 다음과 같습니다.
var exec = require("child_process").exec;
function start(response) {
console.log("Request handler 'start' was called.");
exec("ls -lah", function (error, stdout, stderr) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write(stdout);
response.end();
});
}
function upload(response) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello Upload");
response.end();
}
exports.start = start;
exports.upload = upload;
我们的处理程序函数需要接收response参数,为了对请求作出直接的响应。
start处理程序在exec()的匿名回调函数中做请求响应的操作,而upload处理程序仍然是简单的回复“Hello World”,只是这次是使用response对象而已。
这时再次我们启动应用(node index.js),一切都会工作的很好。
如果想要证明/start处理程序中耗时的操作不会阻塞对/upload请求作出立即响应的话,可以将requestHandlers.js修改为如下形式:
코드 사본은 다음과 같습니다.
var exec = require("child_process").exec;
function start(response) {
console.log("Request handler 'start' was called.");
exec("find /",
{ timeout: 10000, maxBuffer: 20000*1024 },
function (error, stdout, stderr) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.write(stdout);
response.end();
});
}
function upload(response) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello Upload");
response.end();
}
exports.start = start;
exports.upload = upload;
这样一来,当请求http://localhost:8888/start的时候,会花10秒钟的时间才载入,而当请求http://localhost:8888/upload的时候,会立即响应,纵然这个时候/start响应还在处理中。
更有用的场景
到目前为止,我们做的已经很好了,但是,我们的应用没有实际用途。
服务器,请求路由以及请求处理程序都已经完成了,下面让我们按照此前的用例给网站添加交互:用户选择一个文件,上传该文件,然后在浏览器中看到上传的文件。 为了保持简单,我们假设用户只会上传图片,然后我们应用将该图片显示到浏览器中。
好,下面就一步步来实现,鉴于此前已经对JavaScript原理性技术性的内容做过大量介绍了,这次我们加快点速度。
要实现该功能,分为如下两步: 首先,让我们来看看如何处理POST请求(非文件上传),之后,我们使用Node.js的一个用于文件上传的外部模块。之所以采用这种实现方式有两个理由。
第一,尽管在Node.js中处理基础的POST请求相对比较简单,但在这过程中还是能学到很多。
第二,用Node.js来处理文件上传(multipart POST请求)是比较复杂的,它不在本书的范畴,但,如何使用外部模块却是在本书涉猎内容之内。
处理POST请求
考虑这样一个简单的例子:我们显示一个文本区(textarea)供用户输入内容,然后通过POST请求提交给服务器。最后,服务器接受到请求,通过处理程序将输入的内容展示到浏览器中。
/start请求处理程序用于生成带文本区的表单,因此,我们将requestHandlers.js修改为如下形式:
function start(response) {
console.log("Request handler 'start' was called.");
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" content="text/html; '+
'charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" method="post">'+
'<textarea name="text" rows="20" cols="60"></textarea>'+
'<input type="submit" value="Submit text" />'+
'</form>'+
'</body>'+
'</html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
}
function upload(response) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("Hello Upload");
response.end();
}
exports.start = start;
exports.upload = upload;
好了,现在我们的应用已经很完善了,都可以获得威比奖(Webby Awards)了,哈哈。(译者注:威比奖是由国际数字艺术与科学学院主办的评选全球最佳网站的奖项,具体参见详细说明)通过在浏览器中访问http://localhost:8888/start就可以看到简单的表单了,要记得重启服务器哦!
你可能会说:这种直接将视觉元素放在请求处理程序中的方式太丑陋了。说的没错,但是,我并不想在本书中介绍诸如MVC之类的模式,因为这对于你了解JavaScript或者Node.js环境来说没多大关系。
余下的篇幅,我们来探讨一个更有趣的问题: 当用户提交表单时,触发/upload请求处理程序处理POST请求的问题。
现在,我们已经是新手中的专家了,很自然会想到采用异步回调来实现非阻塞地处理POST请求的数据。
这里采用非阻塞方式处理是明智的,因为POST请求一般都比较“重” ―― 用户可能会输入大量的内容。用阻塞的方式处理大数据量的请求必然会导致用户操作的阻塞。
为了使整个过程非阻塞,Node.js会将POST数据拆分成很多小的数据块,然后通过触发特定的事件,将这些小数据块传递给回调函数。这里的特定的事件有data事件(表示新的小数据块到达了)以及end事件(表示所有的数据都已经接收完毕)。
我们需要告诉Node.js当这些事件触发的时候,回调哪些函数。怎么告诉呢? 我们通过在request对象上注册监听器实现。这里的request对象是每次接收到HTTP请求时候,都会把该对象传递给onRequest回调函数。
如下所示:
코드 사본은 다음과 같습니다.
request.addListener("data", function(chunk) {
// called when a new chunk of data was received
});
request.addListener("end", function() {
// called when all chunks of data have been received
});
问题来了,这部分逻辑写在哪里呢? 我们现在只是在服务器中获取到了request对象―― 我们并没有像之前response对象那样,把request 对象传递给请求路由和请求处理程序。
在我看来,获取所有来自请求的数据,然后将这些数据给应用层处理,应该是HTTP服务器要做的事情。因此,我建议,我们直接在服务器中处理POST数据,然后将最终的数据传递给请求路由和请求处理器,让他们来进行进一步的处理。
因此,实现思路就是: 将data和end事件的回调函数直接放在服务器中,在data事件回调中收集所有的POST数据,当接收到所有数据,触发end事件后,其回调函数调用请求路由,并将数据传递给它,然后,请求路由再将该数据传递给请求处理程序。
还等什么,马上来实现。先从server.js开始:
코드 사본은 다음과 같습니다.
var http = require("http");
var url = require("url");
function start(route, handle) {
function onRequest(request, response) {
var postData = "";
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
request.setEncoding("utf8");
request.addListener("data", function(postDataChunk) {
postData += postDataChunk;
console.log("Received POST data chunk '"+
postDataChunk + "'.");
});
request.addListener("end", function() {
route(handle, pathname, response, postData);
});
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
上述代码做了三件事情: 首先,我们设置了接收数据的编码格式为UTF-8,然后注册了“data”事件的监听器,用于收集每次接收到的新数据块,并将其赋值给postData 变量,最后,我们将请求路由的调用移到end事件处理程序中,以确保它只会当所有数据接收完毕后才触发,并且只触发一次。我们同时还把POST数据传递给请求路由,因为这些数据,请求处理程序会用到。
上述代码在每个数据块到达的时候输出了日志,这对于最终生产环境来说,是很不好的(数据量可能会很大,还记得吧?),但是,在开发阶段是很有用的,有助于让我们看到发生了什么。
我建议可以尝试下,尝试着去输入一小段文本,以及大段内容,当大段内容的时候,就会发现data事件会触发多次。
再来点酷的。我们接下来在/upload页面,展示用户输入的内容。要实现该功能,我们需要将postData传递给请求处理程序,修改router.js为如下形式:
코드 사본은 다음과 같습니다.
function route(handle, pathname, response, postData) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname](response, postData);
} 또 다른 {
console.log("No request handler found for " + pathname);
response.writeHead(404, {"Content-Type": "text/plain"});
response.write("404 Not found");
response.end();
}
}
exports.route = route;
然后,在requestHandlers.js中,我们将数据包含在对upload请求的响应中:
코드 사본은 다음과 같습니다.
function start(response, postData) {
console.log("Request handler 'start' was called.");
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" content="text/html; '+
'charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" method="post">'+
'<textarea name="text" rows="20" cols="60"></textarea>'+
'<input type="submit" value="Submit text" />'+
'</form>'+
'</body>'+
'</html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
}
function upload(response, postData) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("You've sent: " + postData);
response.end();
}
exports.start = start;
exports.upload = upload;
好了,我们现在可以接收POST数据并在请求处理程序中处理该数据了。
我们最后要做的是: 当前我们是把请求的整个消息体传递给了请求路由和请求处理程序。我们应该只把POST数据中,我们感兴趣的部分传递给请求路由和请求处理程序。在我们这个例子中,我们感兴趣的其实只是text字段。
我们可以使用此前介绍过的querystring模块来实现:
코드 사본은 다음과 같습니다.
var querystring = require("querystring");
function start(response, postData) {
console.log("Request handler 'start' was called.");
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" content="text/html; '+
'charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" method="post">'+
'<textarea name="text" rows="20" cols="60"></textarea>'+
'<input type="submit" value="Submit text" />'+
'</form>'+
'</body>'+
'</html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
}
function upload(response, postData) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("You've sent the text: "+
querystring.parse(postData).text);
response.end();
}
exports.start = start;
exports.upload = upload;
好了,以上就是关于处理POST数据的全部内容。
处理文件上传
最后,我们来实现我们最终的用例:允许用户上传图片,并将该图片在浏览器中显示出来。
回到90年代,这个用例完全可以满足用于IPO的商业模型了,如今,我们通过它能学到这样两件事情: 如何安装外部Node.js模块,以及如何将它们应用到我们的应用中。
这里我们要用到的外部模块是Felix Geisendörfer开发的node-formidable模块。它对解析上传的文件数据做了很好的抽象。 其实说白了,处理文件上传“就是”处理POST数据―― 但是,麻烦的是在具体的处理细节,所以,这里采用现成的方案更合适点。
使用该模块,首先需要安装该模块。Node.js有它自己的包管理器,叫NPM。它可以让安装Node.js的外部模块变得非常方便。通过如下一条命令就可以完成该模块的安装:
코드 사본은 다음과 같습니다.
npm install formidable
如果终端输出如下内容:
코드 사본은 다음과 같습니다.
npm info build Success: [email protected]
npm ok
就说明模块已经安装成功了。
现在我们就可以用formidable模块了――使用外部模块与内部模块类似,用require语句将其引入即可:
코드 사본은 다음과 같습니다.
var formidable = require("formidable");
这里该模块做的就是将通过HTTP POST请求提交的表单,在Node.js中可以被解析。我们要做的就是创建一个新的IncomingForm,它是对提交表单的抽象表示,之后,就可以用它解析request对象,获取表单中需要的数据字段。
node-formidable官方的例子展示了这两部分是如何融合在一起工作的:
코드 사본은 다음과 같습니다.
var formidable = require('formidable'),
http = require('http'),
sys = require('sys');
http.createServer(function(req, res) {
if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
// parse a file upload
var form = new formidable.IncomingForm();
form.parse(req, function(err, fields, files) {
res.writeHead(200, {'content-type': 'text/plain'});
res.write('received upload:/n/n');
res.end(sys.inspect({fields: fields, files: files}));
});
반품;
}
// show a file upload form
res.writeHead(200, {'content-type': 'text/html'});
res.end(
'<form action="/upload" enctype="multipart/form-data" '+
'method="post">'+
'<input type="text" name="title"><br>'+
'<input type="file" name="upload" multiple="multiple"><br>'+
'<input type="submit" value="Upload">'+
'</form>'
);
}).listen(8888);
如果我们将上述代码,保存到一个文件中,并通过node来执行,就可以进行简单的表单提交了,包括文件上传。然后,可以看到通过调用form.parse传递给回调函数的files对象的内容,如下所示:
코드 사본은 다음과 같습니다.
received upload:
{ fields: { title: 'Hello World' },
파일 :
{ upload:
{ size: 1558,
path: '/tmp/1c747974a27a6292743669e91f29350b',
name: 'us-flag.png',
type: 'image/png',
lastModifiedDate: Tue, 21 Jun 2011 07:02:41 GMT,
_writeStream: [Object],
length: [Getter],
filename: [Getter],
mime: [Getter] } } }
为了实现我们的功能,我们需要将上述代码应用到我们的应用中,另外,我们还要考虑如何将上传文件的内容(保存在/tmp目录中)显示到浏览器中。
我们先来解决后面那个问题: 对于保存在本地硬盘中的文件,如何才能在浏览器中看到呢?
显然,我们需要将该文件读取到我们的服务器中,使用一个叫fs的模块。
我们来添加/showURL的请求处理程序,该处理程序直接硬编码将文件/tmp/test.png内容展示到浏览器中。当然了,首先需要将该图片保存到这个位置才行。
将requestHandlers.js修改为如下形式:
코드 사본은 다음과 같습니다.
var querystring = require("querystring"),
fs = require("fs");
function start(response, postData) {
console.log("Request handler 'start' was called.");
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" '+
'content="text/html; charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" method="post">'+
'<textarea name="text" rows="20" cols="60"></textarea>'+
'<input type="submit" value="Submit text" />'+
'</form>'+
'</body>'+
'</html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
}
function upload(response, postData) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("You've sent the text: "+
querystring.parse(postData).text);
response.end();
}
function show(response, postData) {
console.log("Request handler 'show' was called.");
fs.readFile("/tmp/test.png", "binary", function(error, file) {
if(error) {
response.writeHead(500, {"Content-Type": "text/plain"});
response.write(error + "/n");
response.end();
} 또 다른 {
response.writeHead(200, {"Content-Type": "image/png"});
response.write(file, "binary");
response.end();
}
});
}
exports.start = start;
exports.upload = upload;
exports.show = show;
我们还需要将这新的请求处理程序,添加到index.js中的路由映射表中:
코드 사본은 다음과 같습니다.
var server = require("./server");
var router = require("./router");
var requestHandlers = require("./requestHandlers");
var handle = {}
handle["/"] = requestHandlers.start;
handle["/start"] = requestHandlers.start;
handle["/upload"] = requestHandlers.upload;
handle["/show"] = requestHandlers.show;
server.start(router.route, handle);
重启服务器之后,通过访问http://localhost:8888/show,就可以看到保存在/tmp/test.png的图片了。
好,最后我们要的就是:
在/start表单中添加一个文件上传元素
将node-formidable整合到我们的upload请求处理程序中,用于将上传的图片保存到/tmp/test.png
将上传的图片内嵌到/uploadURL输出的HTML中
第一项很简单。只需要在HTML表单中,添加一个multipart/form-data的编码类型,移除此前的文本区,添加一个文件上传组件,并将提交按钮的文案改为“Upload file”即可。 如下requestHandler.js所示:
코드 사본은 다음과 같습니다.
var querystring = require("querystring"),
fs = require("fs");
function start(response, postData) {
console.log("Request handler 'start' was called.");
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" '+
'content="text/html; charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" enctype="multipart/form-data" '+
'method="post">'+
'<input type="file" name="upload">'+
'<input type="submit" value="Upload file" />'+
'</form>'+
'</body>'+
'</html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
}
function upload(response, postData) {
console.log("Request handler 'upload' was called.");
response.writeHead(200, {"Content-Type": "text/plain"});
response.write("You've sent the text: "+
querystring.parse(postData).text);
response.end();
}
function show(response, postData) {
console.log("Request handler 'show' was called.");
fs.readFile("/tmp/test.png", "binary", function(error, file) {
if(error) {
response.writeHead(500, {"Content-Type": "text/plain"});
response.write(error + "/n");
response.end();
} 또 다른 {
response.writeHead(200, {"Content-Type": "image/png"});
response.write(file, "binary");
response.end();
}
});
}
exports.start = start;
exports.upload = upload;
exports.show = show;
매우 좋은.下一步相对比较复杂。这里有这样一个问题: 我们需要在upload处理程序中对上传的文件进行处理,这样的话,我们就需要将request对象传递给node-formidable的form.parse函数。
但是,我们有的只是response对象和postData数组。看样子,我们只能不得不将request对象从服务器开始一路通过请求路由,再传递给请求处理程序。 或许还有更好的方案,但是,不管怎么说,目前这样做可以满足我们的需求。
到这里,我们可以将postData从服务器以及请求处理程序中移除了―― 一方面,对于我们处理文件上传来说已经不需要了,另外一方面,它甚至可能会引发这样一个问题: 我们已经“消耗”了request对象中的数据,这意味着,对于form.parse来说,当它想要获取数据的时候就什么也获取不到了。(因为Node.js不会对数据做缓存)
我们从server.js开始―― 移除对postData的处理以及request.setEncoding (这部分node-formidable自身会处理),转而采用将request对象传递给请求路由的方式:
코드 사본은 다음과 같습니다.
var http = require("http");
var url = require("url");
function start(route, handle) {
function onRequest(request, response) {
var pathname = url.parse(request.url).pathname;
console.log("Request for " + pathname + " received.");
route(handle, pathname, response, request);
}
http.createServer(onRequest).listen(8888);
console.log("Server has started.");
}
exports.start = start;
接下来是router.js ―― 我们不再需要传递postData了,这次要传递request对象:
function route(handle, pathname, response, request) {
console.log("About to route a request for " + pathname);
if (typeof handle[pathname] === 'function') {
handle[pathname](response, request);
} 또 다른 {
console.log("No request handler found for " + pathname);
response.writeHead(404, {"Content-Type": "text/html"});
response.write("404 Not found");
response.end();
}
}
exports.route = route;
现在,request对象就可以在我们的upload请求处理程序中使用了。node-formidable会处理将上传的文件保存到本地/tmp目录中,而我们需要做的是确保该文件保存成/tmp/test.png。 没错,我们保持简单,并假设只允许上传PNG图片。
这里采用fs.renameSync(path1,path2)来实现。要注意的是,正如其名,该方法是同步执行的, 也就是说,如果该重命名的操作很耗时的话会阻塞。 这块我们先不考虑。
接下来,我们把处理文件上传以及重命名的操作放到一起,如下requestHandlers.js所示:
코드 사본은 다음과 같습니다.
var querystring = require("querystring"),
fs = require("fs"),
formidable = require("formidable");
function start(response) {
console.log("Request handler 'start' was called.");
var body = '<html>'+
'<head>'+
'<meta http-equiv="Content-Type" content="text/html; '+
'charset=UTF-8" />'+
'</head>'+
'<body>'+
'<form action="/upload" enctype="multipart/form-data" '+
'method="post">'+
'<input type="file" name="upload" multiple="multiple">'+
'<input type="submit" value="Upload file" />'+
'</form>'+
'</body>'+
'</html>';
response.writeHead(200, {"Content-Type": "text/html"});
response.write(body);
response.end();
}
function upload(response, request) {
console.log("Request handler 'upload' was called.");
var form = new formidable.IncomingForm();
console.log("about to parse");
form.parse(request, function(error, fields, files) {
console.log("parsing done");
fs.renameSync(files.upload.path, "/tmp/test.png");
response.writeHead(200, {"Content-Type": "text/html"});
response.write("received image:<br/>");
response.write("<img src='/show' />");
response.end();
});
}
function show(response) {
console.log("Request handler 'show' was called.");
fs.readFile("/tmp/test.png", "binary", function(error, file) {
if(error) {
response.writeHead(500, {"Content-Type": "text/plain"});
response.write(error + "/n");
response.end();
} 또 다른 {
response.writeHead(200, {"Content-Type": "image/png"});
response.write(file, "binary");
response.end();
}
});
}
exports.start = start;
exports.upload = upload;
exports.show = show;
好了,重启服务器,我们应用所有的功能就可以用了。选择一张本地图片,将其上传到服务器,然后浏览器就会显示该图片。
总结与展望
恭喜,我们的任务已经完成了!我们开发完了一个Node.js的web应用,应用虽小,但却“五脏俱全”。 期间,我们介绍了很多技术点:服务端JavaScript、函数式编程、阻塞与非阻塞、回调、事件、内部和外部模块等等。
当然了,还有许多本书没有介绍到的: 如何操作数据库、如何进行单元测试、如何开发Node.js的外部模块以及一些简单的诸如如何获取GET请求之类的方法。
但本书毕竟只是一本给初学者的教程―― 不可能覆盖到所有的内容。
幸运的是,Node.js社区非常活跃(作个不恰当的比喻就是犹如一群有多动症小孩子在一起,能不活跃吗?), 这意味着,有许多关于Node.js的资源,有什么问题都可以向社区寻求解答。 其中Node.js社区的wiki以及NodeCloud就是最好的资源。