2016年1月30日 星期六

在Raspberry 上面安裝 node.js 來寫javascript


安裝請參考Ref 1

Adding the Package Repository
curl -sLS https://apt.adafruit.com/add | sudo bash

install the latest version of node.js using apt-get
sudo apt-get install node

check the installed version of node.js by running node -v
node -v

  1. npm install onoff

onoff 套件可以讓我們用js來驅動GPIO...

請看下面的例子

GPIO18 -> LED
GPIO17 -> switch button

vi  test.js

======================
var GPIO = require('onoff').Gpio,
    led = new GPIO(18,'out'),
    button = new GPIO(17,'in','both');

function light(err, state) {

   if(state==1){
   // turn LED on
   led.writeSync(1);
   } else {
   // turn LED off
   led.writeSync(0);
   }
}

button.watch(light);

================================

執行檔案 ....

node test.js

按一下GPIO17 所接的switch button....每按一下...LED就會反轉


當然你也可以把  test.js 改寫成下面這樣

  1. // button is attaced to pin 17, LED to 18
  2. var GPIO = require('onoff').Gpio,
  3. led = new GPIO(18, 'out'),
  4. button = new GPIO(17, 'in', 'both');
  5.  
  6. // pass the callback function to the
  7. // as the first argument to watch() and define
  8. // it all in one step
  9. button.watch(function(err, state) {
  10. // check the state of the button
  11. // 1 == pressed, 0 == not pressed
  12. if(state == 1) {
  13. // turn LED on
  14. led.writeSync(1);
  15. } else {
  16. // turn LED off
  17. led.writeSync(0);
  18. }
  19. });
=============================

這邊他使用了一個名為匿名函式的技巧....

直接把function 的body 寫在  button.watch(function(err,state)){

}...

這樣的好處是可以省下function 的名字....


接下來介紹如何用node.js 來寫server端的網頁

詳情請看Ref 2


vi   helloworld.js

=====================================
console.log("Hello world");
=====================================

然後執行他

node helloworld.js

就可以得到Hello world的訊息



這只是小暖身而已....接下來就是一個完整的基於node.js 的web 應用

目標:
     1. 用戶可以透過瀏覽器使用我們的應用
     2. 當用戶請求http://domain/start時,可以看到一個歡迎頁面,頁面上有一個檔案上傳的表單
     3. 用戶可以選擇一個圖片並送出表單,隨後檔案將被上傳到http://domain/upload,該頁面完成上傳後會把圖片顯示在頁面上

  需要實現的地方
       
  • 我們需要提供Web頁面,因此需要一個HTTP伺服器
  • 對於不同的請求,根據請求的URL,我們的伺服器需要給予不同的回應,因此我們需要一個路由(Router),用於把請求對應到請求處理程序(request handler)
  • 路由還應該能處理POST資料,並且把資料封裝成更友好的格式傳遞給請求處理入程序,因此需要請求資料處理功能
  • 我們不僅僅要處理URL對應的請求,還要把內容顯示出來,這意味著我們需要一些視圖邏輯供請求處理程序使用,以便將內容發送給用戶的瀏覽器
  • 最後,用戶需要上傳圖片,所以我們需要上傳處理功能來處理這方面的細節
  先建立一個server.js 的檔案
  
   vi  server.js

 ===============================

var http = require("http");

http.createServer(function(request,response){
  response.writeHead(200,{"Content-Type": "text/plain"});
  response.write("Hello World");
  response.end();
  }).listen(8888);

================================

執行他....如果你是在本機上執行就用web browser 輸入


但如果你是像我一樣在raspberry 上面執行node.js.... 但是你卻是用NB遠端到RPI...

那妳可以改成輸入RPI 的ip address....

以我的例子就是 192.168.1.2......(不知道自己的ip address...請打 ifconfig)

就可以得到 Hello World的訊息


接下來講一下甚麼是參數傳遞....


function say(word) {
  console.log(word);
}
function execute(someFunction, value) {
  someFunction(value);
}

execute(say, "Hello");
execute(say, "Hello") ->  say("Hello") -> console.log("Hello")

懂了這個方法後...也可以把server.js 的source code改寫

var http = require("http");
function onRequest(request, response) {
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("Hello World");
  response.end();
}

http.createServer(onRequest).listen(8888);
一樣可以達到同樣的目的....

Node.js是事件驅動的


我們建立了伺服器,並且向建立它的方法傳遞了一個函數。無論何時我們的伺服器收到一個請求,這個函數就會被執行。
我們不知道這件事情什麼時候會發生,但是我們現在有了一個處理請求的地方:它就是我們傳遞過去的那個函數。至於它是被預先定義的函數還是匿名函數,就無關緊要了。
這個就是傳說中的 回呼(callback) 
接下來把code改成這樣, 加了兩行 console.log("Request received."); 和  console.log("Server has started.");
var http = require("http");
function onRequest(request, response) {
  console.log("Request received.");
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("Hello World");
  response.end();
}

http.createServer(onRequest).listen(8888);

console.log("Server has started.");
執行結果:

在server端印出  "Server has started" 的訊息....
然後打開web browser...  ...然後多出現了兩行message...  "Request receieved"

這就是事件驅動的非同步伺服器端JavaScript和它的回呼(callback)啦!

回呼(callback)函數 onRequest() ..當回呼(callback)啟動,我們的 onRequest() 函數被觸發的時候,有兩個參數被傳入: request 和 response 。


模組化你的程式



Node.js中自帶了一個叫做 "http" 的模組,我們在我們的程式碼中請求它並把回傳值賦給一個區域變數。
這把我們的區域變數變成了一個擁有所有 http 模組所提供的公共方法的物件。
給這種區域變數起一個和模組名稱一樣的名字是一種慣例
把 server.js改成下面的樣子
var http = require("http");
function start() {
  function onRequest(request, response) {
    console.log("Request received.");
    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;
其實就是用一個function start 把剛剛的內容包起來 ...然後把這個function 輸出到外面
建立 index.js 檔案並寫入以下內容:
var server = require("./server");

server.start();
這邊就是把剛剛的server 引用進來...並且呼叫server.start();

接下來執行他

node index.js
就會得到跟剛剛一樣的結果....


對於不同的URL請求  -> Router

我們要為路由提供請求的URL和其他需要的GET及POST參數,隨後路由需要根據這些資料來執行相應的程式碼(這裡 "程式碼" 對應整個應用的第三部分:一系列在接收到請求時真正工作的處理程序)。
因此,我們需要查看HTTP請求,從中提取出請求的URL以及GET/POST參數。這一功能應當屬於路由還是伺服器(甚至作為一個模組自身的功能)確實值得探討,但這裡暫定其為我們的HTTP伺服器的功能
我們需要的所有資料都會包含在request物件中,該物件作為onRequest()回呼(callback)函數的第一個參數傳遞。但是為了解析這些資料,我們需要額外的Node.JS模組,它們分別是urlquerystring模組。


                               url.parse(string).query
                                           |
           url.parse(string).pathname      |
                       |                   |
                       |                   |
                     ------ -------------------
http://localhost:8888/start?foo=bar&hello=world
                                ---       -----
                                 |          |
                                 |          |
              querystring(string)["foo"]    |
                                            |
                         querystring(string)["hello"]
現在我們來給onRequest()函數加上一些邏輯,用來找出瀏覽器請求的URL路徑:
modify server.ps

var http = require("http");
var url = require("url");
function start() {
  function onRequest(request, response) {
    var pathname = url.parse(request.url).pathname;
    console.log("Request for " + pathname + " received.");
    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;
用瀏覽器得到下面的結果...得到 /favicon.ico 


建立一個名為router.js的檔案

function route(pathname) {
  console.log("About to route a request for " + pathname);
}

exports.route = route;
修改  server.js  加入  route(pathname)

var http = require("http");
var url = require("url");
function start(route) {
  function onRequest(request, response) {
    var pathname = url.parse(request.url).pathname;
    console.log("Request for " + pathname + " received.");

    route(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;
同時也修改index.js

var server = require("./server");
var router = require("./router");

server.start(router.route);
執行的結果如下:

 web browser 輸入 : http://192.168.1.2:8888/foo




路由給真正的請求處理程序


是指我們要針對不同的URL有不同的處理方式
在現在的實現下,路由過程會在路由模組中 "結束" ,並且路由模組並不是真正針對請求 "採取行動" 的模組,否則當我們的應用程式變得更為複雜時,將無法很好地擴充。
我們暫時把作為路由目標的函數稱為請求處理程序。現在我們不要急著來開發路由模組,因為如果請求處理程序沒有就緒的話,再怎麼完善路由模組也沒有多大意義。
應用程式需要新的部件,因此加入新的模組 -- 已經無需為此感到新奇了。我們來建立一個叫做requestHandlers的模組,並對於每一個請求處理程序,增加一個占位用函數,隨後將這些函數作為模組的方法匯出:
vi requestHandlers.js

function start() {
  console.log("Request handler 'start' was called.");
}
function upload() {
  console.log("Request handler 'upload' was called.");
}

exports.start = start;
exports.upload = upload;
這樣我們就可以把請求處理程序和路由模組連接起來,讓路由 "有路可尋"。

在C++或C#中,當我們談到物件,指的是類別(Class)或者結構體(Struct)的實體。物件根據他們實體化的範本(就是所謂的類別),會擁有不同的屬性和方法。但在JavaScript裡物件不是這個概念。在JavaScript中,物件就是一個鍵/值對的集合 -- 你可以把JavaScript的物件想象成一個鍵為字串類型的字典。

但如果JavaScript的物件僅僅是鍵/值對的集合,它又怎麼會擁有方法呢?好吧,這裡的值可以是字串、數字或者……函數!
我們先將這個物件引入到主檔案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);
正如所見,將不同的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;
然後我們相應地在route.js檔案中修改route()函數:

function route(handle, pathname) {
  console.log("About to route a request for " + pathname);
  if (typeof handle[pathname] === 'function') {
    handle[pathname]();
  } else {
    console.log("No request handler found for " + pathname);
  }
}

exports.route = route;
透過以上程式碼,我們首先檢查給定的路徑對應的請求處理程序是否存在,如果存在的話直接執行相應的函數。我們可以用從關聯陣列中取得元素一樣的方式從傳遞的物件中取得請求處理函數,因此就有了簡潔流暢的形如handle[pathname]();的表達式,這個感覺就像在前方中提到的那樣: "嗨,請幫我處理了這個路徑" 。

打開你的web browser  ...輸入: http://192.168.1.2:8888/start



讓請求處理程序作出回應

其實 "處理請求" 說白了就是 "對請求作出回應" ,因此,我們需要讓請求處理程序能夠像onRequest函數那樣可以和瀏覽器進行 "對話" 。

不好的實現方式

   讓我們從讓請求處理程序回傳需要在瀏覽器中顯示的訊息開始。我們需要將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]();
  } else {
    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" 。
好,那麼問題在哪裡呢?簡單的說就是: 當未來有請求處理程序需要進行Non-Blocking的操作的時候,我們的應用就 "掛" 了。

Blocking與Non-Blocking

我們先來看看什麼是Blocking操作。

讓我們將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;
首先,打開兩個瀏覽器窗口或者標簽頁。在第一個瀏覽器窗口的地址欄中輸入http://your_ip:8888/start, 但是先不要打開它!
在第二個瀏覽器窗口的地址欄中輸入http://your_ip:8888/upload, 同樣的,先不要打開它!接下來,做如下操作:在第一個窗口中( "/start" )按下 Enter,然後快速切換到第二個窗口中( "/upload" )按下 Enter。注意,發生了什麼: /start URL加載花了10秒,這和我們預期的一樣。但是,/upload URL居然花了10秒,而它在對應的請求處理程序中並沒有類似於sleep()這樣的操作!
這顯然是個問題,因為Node一向是這樣來標榜自己的: "在node中除了程式碼,所有一切都是並行執行的" 

接下來,我們會介紹一種錯誤的使用Non-Blocking操作的方式。
再次修改 requestHandler.js
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。之所以用它,是為了實現一個既簡單又實用的Non-Blocking操作:exec()
exec()做了什麼呢?它從Node.js來執行一個shell命令。在上述例子中,我們用它來取得目前目錄下所有的檔案( "ls -lah" ),然後,當/startURL請求的時候將檔案訊息輸出到瀏覽器中。
和往常一樣,我們啟動伺服器,然後存取 "http://your_ip:8888/start" 。

其內容為 "empty" 。怎麼回事?
這個時候,你可能大致已經猜到了,exec()在Non-Blocking這塊發揮了神奇的功效。它其實是個很好的東西,有了它,我們可以執行非常耗時的shell操作而無需迫使我們的應用停下來等待該操作。
問題就在於,為了進行Non-Blocking工作,exec()使用了回呼(callback)函數。在我們的例子中,該回呼(callback)函數就是作為第二個參數傳遞給exec()的匿名函數:
function (error, stdout, stderr) {
  content = stdout;
}
現在就到了問題根源所在了:我們的程式碼是同步執行的,這就意味著在執行exec()之後,Node.js會立即執行 return content ;在這個時候,content仍然是 "empty" ,因為傳遞給exec()的回呼(callback)函數還未執行到——因為exec()的操作是非同步的。
那究竟我們要如何才能實現將目前目錄下的檔案列表顯示給用戶呢?

以Non-Blocking操作進行請求回應

用Node.js就有這樣一種實現方案: 函數傳遞

到目前為止,我們的應用已經可以透過應用各層之間傳遞值的方式(請求處理程序 -> 請求路由 -> 伺服器)將請求處理程序回傳的內容(請求處理程序最終要顯示給用戶的內容)傳遞給HTTP伺服器。
現在我們採用如下這種新的實現方式:相對採用將內容傳遞給伺服器的方式,我們這次採用將伺服器 "傳遞" 給內容的方式。 從實踐角度來說,就是將response物件(從伺服器的回呼(callback)函數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);
  } else {
    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()的匿名回呼(callback)函數中做請求回應的操作,而upload處理程序仍然是簡單的回復 "Hello World" ,只是這次是使用response物件而已。
這時再次我們啟動應用(node index.js),一切都會工作的很好。

更有用的場景

要實現該功能,分為如下兩步: 首先,讓我們來看看如何處理POST請求(非檔案上傳),之後,我們使用Node.js的一個用於檔案上傳的外部模組。之所以採用這種實現方式有兩個理由。
第一,盡管在Node.js中處理基礎的POST請求相對比較簡單,但在這過程中還是能學到很多。
第二,用Node.js來處理檔案上傳(multipart POST請求)是比較複雜的,它在本書的范疇,但,如何使用外部模組卻是在本書涉獵內容之內。

處理POST請求



Reference 1 : Node入門

Reference 2 : https://learn.adafruit.com/node-embedded-development/installing-node-dot-js

Reference 3 : http://debuggable.com/posts/understanding-node-js:4bd98440-45e4-4a9a-8ef7-0f7ecbdd56cb

沒有留言:

張貼留言