第二章簡單的Node.js框架——2.2 預定義的PHP變量

字號+ 編輯: Snake 修訂: 种花家 來源: 《写给PHP开发者的Node.js学习指南》 2023-09-11 我要說兩句(0)

當一個支持 PHP 的 Web 服務器執行一個 PHP 頁面時,它並不是僅提供一個未處理的對某個頁面的 HTTP request,然後執行這個頁面。如果它這樣做的話,那麽每一個 PHP 頁面都需要大量額外的代碼來解析原始的 HTTP request 並且把這些值用更方便的方式存儲起來。相反,PHP 引擎解碼原始的 HTTP 請求,並將數據填充到一堆衆所周知的 PHP 全局變量中。這些全局變量被正確填充才能保证 PHP 頁面正常工作。

由於我們採用的基本方法是將 PHP 頁面拷貝到本地模塊中並將其轉換成Node.js 代碼,那麽我們需要自己在 Node.js 中實現這些全局變量以保证轉換過的頁面能正常工作。通過分析 PHP 頁面,我們可以決定它依賴於哪些變量。並不是每一個 PHP 引擎提供的全局變量都需要實現。相反,我們僅實現那些被使用的到的變量。

有五個PHP 預定義的全局變量是最常用的:$_GET、$_POST、$_COOKIE、$_REQUEST和$_SESSION。

一個 HTTP request 被發送時總是有一個 HTTP 操作,它被叫作方法或者動詞。一個HTTP GET 操作是非常簡單的:客戶耑向服務器請求獲取一個頁面。當用戶在瀏覽器的_地址欄裡鍵入URL時,他就是在輸入一個HTTP GET request。

HTTP GET request。可能有一些以名稱/值對形式的參數。這些參數通常叫做查詢參數或者查詢字符串。用戶可以手動向瀏覽器的地址欄中的 URL 最後添加添加一個問號(?)並將名稱/值對以&符號分割添加到後面。鍵值對本身之間以等號分割。這有一個例子:

http://localhost:1337/index.php?theme=green&tab=user&fastload=true

在這個例子中的鍵值對有:theme=green、tab=users 和 fastload=true。當一個PHP頁面獲取到一個像這個例子中一樣的 GET request 時,PHP 引擎從原始 HTTP GET request 中提取出這些鍵值對並把它們放到預定義的PHP$_GET數組。名字作爲$_GET 數組中的鍵值或索引,值就是值。對於之前的URL 例子,$_GET數組看起來就像這樣:

$_GET['theme'] = 'green';
$_GET['tab'] = 'users';
$_GET['fastload'] = 'true';

當 PHP 頁面被轉換成 Node.js 代碼後,Node.js 仍然需要這些預定義的數組存在並被正確填充。接下來的代碼展示了一個 Node.js 函數 initGET(),它可以被用在任何轉換過的 PHP 頁面的本地模塊中用來填充一個 Node.js_GET 變量,這個變量就像PHP 中的$_GET 變量一樣工作:

function initGET(req,pre,cb){
  pre._GET = {};
  var urlparts = req.url.split('?');
  if(urlparts.length >= 2){
    var query = urlparts[urlparts.length-1].split('&');
    for (var p=0; p < query.length; ++p){
      var pair = query[p].split('=');
      pre._GET[pair[0]] = pair[1];
    }
  }
  cb();
   }

Node.js 函數 initGET()需要三個參數:req、pre 和 cb。req 參數包含一個原始的 HTTP request。pre 參數是一個包含了所有預定義的全局變量的 Node.js 對象。所有預定義的變量都存儲在 pre 變量中,而不是一堆不同的變量,這樣可以方便地傳遞它。而cb 包含了一個在 initGET()函數結束時會被調用的回調函數。由於 initGET()函數僅進行了一些簡單的内存操作並且無需進行有回調函數的操作,因此在技術上並不需要回調函數。但是,由於稍後將要實現的 initPOST()函數將會需要一個回調函數 cb() 作爲參數,所以最好讓 initGET()和 initPOST()函數保持一致。

initGET()函數的第一行代碼在參數 pre 中創建了一個名爲_GET 的數組。pre._GET就是相當於 PHP 中$_GET 數組的 Node.js 對象。接下來再從 main URL 中提取出查詢參數,而這個 main URL 則是從 req.url 屬性獲取的。通過使用 split()函數來將每一個 URL 查詢參數分離開來進而區分它們的名字/值對使得填充 pre._GET 變量也非常簡單。最後,調用 cb 參數讓回調函數知道 pre._GET 變量已經可用。

爲了初始化 Node.js pre._GET 變量,需要對 exports.serve()函數進行一些修改。這裡是最初的 exports.serve()函數:

exports.serve = function(req,res){
  res.weiteHead(200,{'Content-Type': 'text/plain'});
  res.end('admin/index.njs');
}

這裡我們並不是在  exports.serve()函數中實現真實的頁面,而是使用一個新的函數叫作 page(),exports.serve()將被保留作爲初始化和完成其他 PHP 引擎爲 PHP 頁面做的工作:

function page(req,res,pre,cb){
  res.weiteHead(200,{'Content-Type': 'text/plain'});
  res.end('admin/index.njs');
  cb();
}

page()函數需要四個參數:req、res、pre 和 cb。req 和 res 參數代表 HTTP request和 HTTP response。pre 參數是預定義的變量,包含了存儲查詢參數的 GET 屬性。cb 參數是一個回調函數,它可以讓 exports.serve()函數知道什麽時候頁面被完全處理完了。

將 pre 對象列印出來可以幫助調試。通過使用 require()函數加載内建的_ util 模塊,並在 res.end()函數調用中添加一個 util.inspect()函數調用就可以把 pre 變量中包括_GET 屬性的所有内容顯示在 HTTP response 中:

var util = require('util');
function page(req,res,pre,cb){
  res.weiteHead(200,{'Content-Type': 'text/plain'});
  res.end('admin/index.njs\n'+util.inspect(pre));
  cb();
}

現在處理頁面的操作已經移動到 page()函數中了,exports.serve()函數被修改成進行初始化工作,包括調用 initGET()函數:

exports.serve = function(req,res){
  var pre = {};
  ininGET(req,pre,function(){
    page(req,res,pre,function(){
    });
  });
};

pre 變量是最先被創建的。然後調用 initGET()函數,當它完成時,則調用 page()函數。在 page()函數之後並沒有終止化或清除操作,所以它的回調函數是空的。

當 _GET 屬性被實現之後,就可以修改 page()函數使用查詢參數。修改 page()函數,期待接收一個 x 查詢參數並返回對應的值:

function page(req,res,pre,cb){
  res.writeHead(200,{'Content-Type':'text/plain'});
  if(pre._GET['x']){
   res.end('The value of x is '+pre._GET['x']+'.');
  } else{
    res.end('There is no value for x.');
    }
}

如果 Node.js 服務器正在運行,並且瀏覽器被指向到http://localhost: 1337/index.php?x=4, 瀏覽器會顯示“The value of x is 4.”。

HTTP POST request 和 HTTP GET request 基本一樣,除了鍵值對是通過 request 正文而不是 URL 最後的查詢字符串進行發送的。一個 HTTP request 包括 HTTPheader 和一個 HTTP body。

包含查詢字符串的 URL 便是 HTTP header 中的一個。HTTP header 内容精簡並且有長度限制的。尤其是包含查詢字符串的 URL,需要限制在一定長度,不推薦使用過長的 URL。相應地,當需要在 HTTP request 中包含很多數據時,推薦把數據放到 HTTP body 中作爲 HTTP POST 的一部分。HTTP body 跟被限制長度的 HTTP header 不一樣,它可以處理非常大量的數據。對於 HTTP POST,HTTP body 通常被稱爲 POST 數據。

由於 HTTP POST 中的正文可能非常巨大,所以 POST 數據通常不是一次性發送的; 它在收到變成可用的事件時會被發送。事件是 Node.js 用來指示某件事情發生的一種方法。例如,一個data 事件表明下一個數據塊已經從 HTTP body 中被讀出。假如這裡有很多數據,Node.js 會在讀取每一塊數據時觸發若幹事件。

一個時間可以跟回調函數聯繫到一起,這也被叫作事件處理程序。事序會在一個事件發生時被執行。

on()函數可以將一個事件和事件處理函數聯繫到一起。下面的例子說明了如何使用on 函數把 data 事件跟一個數據處理函數綁定在一起,這個數據處理函數會將數據寫到控制台:

req.on('data',function(chunk){
  sonsole.log(chunk);
});

對於 initPOST()函數,pre 的 POST 屬性會被初始化。就像從 initGET()函數中重新調用後一樣,pre 參數是一個包含了所有預定義的全局變量的 Node.js 對象。一個body 變量會被創建出來保存到被讀取的 HTTP body。on()函數把一個事件處理程序和data 事件聯繫到一起,這個事件處理程序會在數據變爲可用之後將其寫入到body變量中:

pre._POST = {};
var body = '';
req.on('data',function(chunk){
  body += chunk;
  if(body.length > 1e6){
    req.connection.destroy();
}
});

在 data 事件處理程序中添加的 if 語句是用來檢測 HTTP 正文是否太長,如果是則會中斷連接。寫得不好或惡意的客戶耑可能會發送無限量的數據,在這時該 if 語句就需要放棄那個發送的大量數據的 HTTP request 來保護 Node.js 服務器。

最後,on()函數爲 end 事件綁定一個事件處理程序,它會在整個 HTTP 正文被讀取之後觸發。通過簡單的 split()函數調用,end 事件處理程序提取出數據並把它們放到 pre._POST 變量中:

req.on('end',function(){
  var pairs = body.dplit('&');
  for (var p=0; p < pairs.length; ++p){
    var pair = pairs[p].split('=');
    pre._POST[pair[0]] = pair[1];
  }
  cb();
});

cb()在最後被調用時,已經有正確的 pre._POST 變量值可以使用了。將所有代碼放到一起,initPOST()函數的全文顯示如下:

function initPOST(req,pre,cb){
  pre._POST = {};
  var body = '';
  req.on('data',function(){
    body += chunk;
  if(body.length > 1e6){
    req.connection.destroy();
}
});
req.on('end',function(){
  var pairs = body.split('&');
  for (var p=0; p < apirs.length; ++p){
    var pair = pairs[p].split('=');
    pre._POST[pair[0]] = pair[1];
  }
  cb();
});
}

對於那些期待 HTTP GET request 的頁面,必須修改 exports.serve()函數。而對於那些期待 HTTP POST request 的頁面,exports.serve()函數的代碼則是一樣的,除了調用 initGET()函數的地方替換爲 initPOST()函數調用。設計就是這樣的。盡管 initGET() 函數不需要一個回調函數,但是給 initGET()函數一個回調函數讓它和 initPOST() 函數有一樣的參數並且代碼幾乎相同。

exports.serve = function(req,res){
  var pre = {};
  ininPOST(req,pre,function(){
    page(req,res,pre,function(){
    });
  });
};

到目前爲止,HTTP GET 和 HTTP POST 是最常用的 HTTP 操作,並且在大多數的情況下,這也是一個 Web 應用程序僅需要的 HTTP 操作。還有一些其他的 HTTP 操作,如HTTP PUT、HTTP DELETE 和HTTP HEAD,但是 PHP 引擎並沒有對這些操作提供支持,所有 Node.js 移植通常也不需要提供支持。

cookie 是一個服務器發送到客戶耑(通常是瀏覽器)的鍵值對,客戶耑會將 cookie 存起來並將它作爲一個 HTTP header 添加到每一個後續的 HTTP 請求中。客戶耑通常將 cookie 存儲在一個可持久化的地方,如硬盤中,因此能在將來的 HTTP 請求中使用這個 cookie,即使客戶耑被關閉並重新啓動後。cookie 是服務器提供給客戶耑的一小段數據,它可以幫助服務器驗证客戶耑,例如讓客戶耑可以自動登錄自己的賬戶。

cookie 的 HTTP header 的名字就是“Cookie”:

Cookie: user=admin;sessid=21EC33203BEA1169A2EA08332B313090

在 Node.js 的 HTTP 請求中,cookies 是存儲在 HTTP 請求的 headers.cookie 屬性中的,在本書中的例子裡就是 req。

initCOOKIE()與initGET()函數都用於將cookie 從相應的HTTP 頭中提取出來並放到pre._COOKIE 變量中,非常相似。獲取 cookie 會更方便:cookie 不是附加 在 URL 最後作爲查詢字符串而是有自己的屬性值。pre._COOKIE 變量將會 用來替代 PHP 引擎爲 PHP 頁面提供的 PHP $_COOKIE 變量:

function initCOOKIE(req,pre,cb._COOKIE = {};
  if(req.headers.cookie){
    var cookies = req.headers.cookie.split(';');
    for (var c=0; c < cookies.length; ++c){
      var pair = cookies[c].split('=');
      pre._COOKIE[0] = pair[1];
    }
  }
  cb();
}

就像那些期待 HTTP GET 和 HTTP POST 請求的頁面,那些需要 cookie 的頁面需要修改它們的  exports.serve()函數來調用  initCOOKIE()函數。initCOOKIE()函數跟initGET()和 initPOST()有同樣的參數,因此可以使用同樣的代碼調用 initCOOKIE()函數:

exprts.serve = function(req,res){
  var pre = {};
  initCOOKIE(req,pre,function(){
    page(req,res,pre,function(){
    });
 });
};

當一個頁面同時需要處理 HTTP GET 和 HTTP POST 兩種請求並且還需要用到cookie 時,可以通過將一個函數當作另一個函數的回調函數的方法來增強exports.serve()函數初始化的操作。以下代碼用於加載 pre._GET、pre._POST 和 pre._COOKIE 屬性,它們將用於替代 PHP 預定義變量$_GET 、$_POST 和$_COOKIE:

exports.serve = function(req,res){
  var re = {};
  initGET(rwq,pre,function(){
    initPOST(req,pre,function(){
      initCOOKIE(req,pre.function(){
        page(req,res,pre,function(){
        });
      });
    });
  });
}

在 PHP 裡,$_REQUEST 預定義變量包含了在$_GET、$_POST 和$_COOKIE 中所有的鍵值對。我們需要將 pre._GET、pre_POST 和 pre._COOKIE 變量複制一份來創建pre._REQUEST:

function initREQUEST(req,pre,cb){
  pre._REQUEST = {};
  if(PRE._GET){
    for(var k in pre._GET){
      pre._RWQUEST[k] = pre._GET[k];
    }
  }
  if(pre._POST){
    for(var k in pre._POST){
      pre._REQUEST[k] = pre._POST[k];
    }
  }
  if(pre._COOKIE){
    for(var k in pre._COOKIE){
    pre._REQUEST[k] = pre._COOKIE[K];
    }
  }
  cb();
}

Node.js 中的 for…in 語句可以從一個 Node.js 對象找出所有的屬性名。下面的代碼顯示了一個對象 obj 並且把它所有的屬性名列印出來:

var obj = {'a': '1','b': '2','c': '3','d': '4','e': '5'};
for (var name in obj){
  console.log('name:'+name);
}

這裡是 Node.js 的 for…in 循環的輸出:

name:a
name:b
name:c
name:d
name:e

在 PHP 中,foreach…as 與 Node.js 中的 for…in 工作行爲差不多,除了 foreach…as 會返回 PHP 中的屬性值而不是像 for…in 在 Node.js 中返回屬性名。

就像其他預定義變量的初始化函數一樣,initREQUEST()函數必須被 exports.serve()函數調用。因爲 REQUEST 值是 GET、 POST 和 COOKIE 的一個複合體,構造REQUEST 值需要的三個值已經賦值給 pre 變量,因此 initREQUEST()函數必須要在其他函數初始化之後被調用:

exports.serve = function(req,res){
  var pre = {};
  initGET(req,pre,function(){
    initPOST(req,pre,function(){
      initCOOKIE(req,pre,function(){
        initREQUEST(req,pre,function(){
          page(req,pre,function(){
          });
        });
      });
    });
  });
}

initREQUEST()函數就像期待的那樣與其他初始化函數使用完全相同的參數並且表現也相同。

在 PHP 還有一個常用的預定義變量:$_SESSION 變量。$_SESSION 變量代表了每一個用戶對 PHP 頁面的調用。

initSESSION()函數使用 pre._COOKIE 變量維護當前用戶在sessions 變量中的會話。當會話不存在時,它會被創建出來;否_則,將從 sessions 對象中獲取:

/** All the session of all the users.*/
var session = {};
function initSESSION(req,pre,cb){
  if((typeof pre._COOKIE['NODESESSID'] == 'undefined'){
    var pool = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZzbcdefghijkklmopqrstuvwxyz';
    for (var i = 0; i < 26; ++i) {
      var r = Math.floor(Math.random()*pool.length);
      newid += pool.charAt(r);
    }
    pre._COOKIE['NODESESSID'] = newid;
    sessions[pre._COOKIE['NODESESSID']] = {};
  }
  var id = pre._COOKIE['NODESESSID'];
  if((typeof session[id]) == 'undefinnde'){
    session[id] = {};
  }
  pre._SESSION = sessions[id];
}

同樣也需要讓頁面處理會話。通過將一個函數添加到另一個函數的回調函數中來加強 exports.serve()初始化函數的能力。下面的代碼會加載 pre._GET、pre._POST、pre._COOKIE 和 pre_REQUEST 屬性,它們是用來代替 PHP 中預定義的變量$_GET、$_POST、$_COOKIE 和$_REQUEST 的。

initSESSION()函數是在 exports.serve()函數中調用的最後一個函數。它依賴於 PHP變量$_COOKIE 來維護這個會話。爲了保证 cookie 處於被激活的狀態,需要修改page()函數的回調函數將 cookie 返回給調用者:

exports.serve = function(req,res){
  var pre = {};
  initGET(req,pre,function(){
    initPOST(req,pre,function(){
      initCOOKIR(req,pre,function(){
        initREQUEST(req,pre,function(){
          initSESSION(req,pre,function(){
            page(req,res,pre,function(){
              var cookies = [];
              for(var c in pre._COOKIE){
                cookies.push(c + '=' + pre._COOKIE[c];);
              }
              res.setHeader('Set-Cookie',cookies);
              reswriteHead(200,{'Content-Type':'text/plain'});
              res.end(res.content);
            });
         });
        });
      });
    });
  });
};

就像所期待的那樣,initSESSION()函數跟其他初始化函數一樣採用完全相同的參數,並且執行方式也幾乎相同。

這裡我們創建一個本地模塊 initreq.njs 用來將 initGET()、initPOST()、initCOOKIE()、initREQUEST()和 initSESSION()函數共享給所有其他模塊。並且這些函數作爲屬性賦值給 exports 變量,從而使它們可以暴露給加載該模塊的調用者:

exports.initGET = function(req,pre,cb){
  pre._GET = {};
  var urlparts = req.url.split('?');
  if(urlparts.length >= 2){
    var query = urlparts[urlparts.length-1].split('&');
    for(var p=0; = < query.length; ++p){
      var pair = query[p].split('=');
    }
  }
  cb();
};
exports.initPOST = function(req,pre,cb){
  pre._POST = {};
  var body = '';
  req.on('data',function(chunk){
    body += chunk;
    if(body.length > 1e6){
      req.connection.destroy();
    }
});
    req.on('end',function(){
      var pairs = body.split('&');
      for(var p=0; p < pairs.length; ++p){
        var pair = pairs[p].length('=');
        pre._POST[pair[0]] = pair[1];
      }
      cb();
    });
};
export.initCOOKIE = function(req,pre,cb){
  pre._COOKIE = {};
  if(req.headers.cookie){
    var cookies = req.headers.cookie.split(';');
    for(var c=0; c < cookies.length; ++c){
      var pair = cookies[c].split('=');
    }
  }
  cb();
};
export.iitREQUEST = function(req,pre,cb){
  pre._REQUEST = {};
  if(pre._GET){
    for(var k in pre._GET){
      pre._REQUEST[k] = pre._GET[k];
    }
  }
  if(pre._POST){
    for(va k in pre._POSST){
      pre._REQUEST[k] = pre._COOKIE[k];
    }
  }
  cb();
};
/** All the sessions of all the users. */
var sessions = {};
exports.initSESSION = function(req,pre,cb){
  if((typeof pre._COOKIE['NODESESSID']) == 'undefined'){
    var pool = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZzbcdefghijkklmopqrstuvwxyz';
    var newif = '';
    for(var i = 0; i <  26; ++i){
      var r = Math.floor(Math.random()*pool.length);
      newid += pool.charAt(r);       
    }
    pre._COOKIE['NODESESSID'] = newid;
    sessiions[pre._COOKIE['NODESESSID']] = {};
  }
  var id = pre._COOKIE['NODESESSID'];
  if((typeof sessions[id] == 'undefined')){
    sessions[id] {};
  }
  pre._SESSION = sessions[id];
  cb();
}

爲了使用這個 initreq.njs 本地模塊,首先需要用 require()函數加載它。然後,需要將模塊的名字 initreq 置於 initreq.njs 文档暴露出來的每一個初始函數的引用之前來進行調用。下面的代碼顯示了這些改變:

var initreq = require('./initreq.njs');
exports.serve = function(req,res){
  var pre = {};
  initreq.initGET(req,pre,function(){
    initreq.initPOST(req,pre,function(){
      initreqCOOKIE(req,pre,function(){
        initreqREQUEST(req,pre,function(){
          initreqSESSION(req,pre,function(){
            page(req,res,pre,function(){
              var cookies = [];
              for(var c in pre._COOKIE){
                cookies.push(c + '=' + pre._COOKIE[C]);
              }
              res.setHeader('Set-Cookie',cookies);
              res.writeHead(200,{'Content-Type':'text/plain'});
              res.end(res.content);
            });
          });
        });
      });
    });
  });
};

現在你對於從 PHP 到Node.js 的轉換應該有更好的想法並且知道整個過程是如何工作的了。

httpsvr.njs 文档是一個 Node.js HTTP 服務器。在一個常見的 PHP 設置中,httpsvr.njs 文档類似於一個安裝了 PHP 模塊的 Apache Web 服務器。如果你想要調整 Node.js Web 服務器來添加一些頁面,將 URL 指定到特定的頁面或者執行其他通用的 Web 服務器的配置時,那麽就需要修改 httpsvr.njs 文档。我們把之前的 httpsvr.njs 例子再放到這裡以便於引用:

var http = require('http');
var static = require('node-static');
var file = new satic.Server();
var url = require('url');
var index = require('.index.njs');
var login = require('./login.njs');
var admin_index = require('./admin/index.njs');
var admin_login = reauire('./admin/login.njs');
http.createServer(function(req,res){
  if(url.parse(req.url).pathname =='/index.php'){
    index.serve(req,res);
  } else if (url.parse(req.url).pathname == '/login.php'){
    login.serve(req,res):
  } else if (url.parse(req.url).pathname == '/admin/index.php'){
    admin_index.serve(req,res);
  } else if (url.parse(req.url).pathname == '/admin/login.php'){
    admin_login.serve();
  } else {
    file.serve(req,res);
  }
}).listen(1337,'127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

對於每一個 PHP 頁面,都會有一個 index.njs 文档或其他模塊文档被創建出來。exports.serve()是 Node.js 中對應於 PHP 引擎用來處理某個特定頁面代碼的函數。如果需要一些額外的預定義變量,初始化代碼或者結束代碼(例如,在頁面完成之後執行的代碼),就需要修改 exports.serve()函數。exports.serve()函數並不是頁面本身, 而是“包裹”這個頁面的代碼:

var initreq = require('./initreq.njs');
exports.serve = function(req,res){
  var pre = {};
  initGET(req,pre,function(){
    initPOST(req,pre,function(){
      initCOOKIR(req,pre,function(){
        initREQUEST(req,pre,function(){
          initSESSION(req,pre,function(){
            page(req,res,pre,function(){
              var cookies = [];
              for(var c in pre._COOKIE){
                cookies.push(c + '=' + pre._COOKIE[c];);
              }
              res.setHeader('Set-Cookie',cookies);
              reswriteHead(200,{'Content-Type':'text/plain'});
              res.end(res.content);
            });
         });
        });
      });
    });
  });
};
function page(req,res,pre,cb){
  res.writeHead(200,{'Content-Type':'text/plain'});
  res.end('admin/index.njs\n'+ntil.inspect(pre));
  cb();
}
閲完此文,您的感想如何?
  • 有用

    1

  • 沒用

    1

  • 開心

    1

  • 憤怒

    1

  • 可憐

    1

1.如文章侵犯了您的版權,請發郵件通知本站,該文章將在24小時内刪除;
2.本站標注原創的文章,轉發時煩請注明來源;
3.交流群: 2702237 13835667

相關課文
  • JS如何防止父節點的事件運行

  • nodejs編寫一個簡單的http請求客戶耑代碼demo

  • 說一則爲什麽後耑開發人員不選擇node.js的原因

  • 使用Sublime Text3 開發React-Native的配置

我要說說
網上賓友點評