- 搞定J2EE核心技術(shù)與企業(yè)應(yīng)用
- 常建功 王向華編著
- 4643字
- 2018-12-29 13:50:00
4.5 XMLHttpRequest概述
XMLHttpRequest是Ajax開發(fā)必備的對象,雖然Iframe也能實現(xiàn)它的部分功能,但它提供了客戶端和服務(wù)器端異步通信的能力。相對于HttpRequest來說,XMLHttpRequest不僅能實現(xiàn)HttpRequest的功能,而且還不需要重新加載頁面。
4.5.1 XMLHttpRequest的生命周期
XMLHttpRequest的生命周期主要包括:初始化、發(fā)送請求、設(shè)置回調(diào)函數(shù)、處理返回值4個部分,下面一一進行講解。
XMLHttpRequest的初始化即XMLHttpRequest的創(chuàng)建工作,目前沒有統(tǒng)一的標準,在不同的瀏覽器中實現(xiàn)存在著不同的方法。
在IE 5.0和IE 6.0的相關(guān)版本中,XMLHttpRequest對象的實現(xiàn)方式如下:
var xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
而在其他的瀏覽器中,XMLHttpRequest對象的實現(xiàn)方式如下:
var xmlhttp=new XMLHttpRequest();
為了使其實現(xiàn)方式一致,在IE 7.0版本以后,XMLHttpRequest對象的實現(xiàn)方式也改為如下方式:
var xmlhttp = new XMLHttpRequest();
為了提高兼容性,所以在創(chuàng)建一個XMLHttpRequest對象時,一般采用如下寫法:
var xmlhttp; if (window.XMLHttpRequest) { //其他瀏覽器 xmlhttp = new XMLHttpRequest(); } else if (window.ActiveXObject) { try { //IE瀏覽器 xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) { xmlhttp = new ActiveXObject('Msxml2.XMLHTTP'); } }
這種寫法類似于JSON-RPC的寫法,JSON-RPC中創(chuàng)建XMLHttpRequest對象的示例代碼如下:
/*將各種可能的XMLHttpRequest對象放在數(shù)組中*/ JSONRpcClient.msxmlNames = [ "MSXML2.XMLHTTP.5.0", "MSXML2.XMLHTTP.4.0", "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP", "Microsoft.XMLHTTP" ]; JSONRpcClient.getHTTPRequest = function JSONRpcClient_getHTTPRequest() { /*Mozilla瀏覽器的創(chuàng)建 */ try { JSONRpcClient.httpObjectName = "XMLHttpRequest"; return new XMLHttpRequest(); } catch(e) {} /*微軟IE瀏覽器的創(chuàng)建*/ for (var i=0;i < JSONRpcClient.msxmlNames.length; i++) { try { //一個一個循環(huán)創(chuàng)建,直到有一個成功 JSONRpcClient.httpObjectName = JSONRpcClient.msxmlNames[i]; return new ActiveXObject(JSONRpcClient.msxmlNames[i]); } catch (e) {} } /* 如果沒有創(chuàng)建成功*/ JSONRpcClient.httpObjectName = null; throw new JSONRpcClient.Exception(0, "Can't create XMLHttpRequest object"); };
DWR也提供了類似的寫法,DWR中創(chuàng)建XMLHttpRequest對象的示例代碼如下:
/*將各種可能的XMLHttpRequest對象放在數(shù)組中*/ dwr.engine._XMLHTTP = ["Msxml2.XMLHTTP.6.0", "Msxml2.XMLHTTP.5.0", "Msxml2.XMLHTTP.4.0", "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP", "Microsoft.XMLHTTP"]; /*假如為非IE瀏覽器*/ if (window.XMLHttpRequest) { batch.req = new XMLHttpRequest(); } /*假如為IE瀏覽器*/ else if (window.ActiveXObject && !(navigator.userAgent.indexOf("Mac") >= 0 && navigator. userAgent.indexOf("MSIE") >= 0)) { batch.req = dwr.engine._newActiveXObject(dwr.engine._XMLHTTP); } dwr.engine._newActiveXObject = function(axarray) { var returnValue; /*一個一個循環(huán)創(chuàng)建,直到有一個成功*/ for (var i = 0; i < axarray.length; i++) { try { returnValue = new ActiveXObject(axarray[i]); break; } catch (ex) { /* ignore */ } } return returnValue; };
針對上述問題,Prototype給出了一個很好的解決方案,在Prototype中定義了一個Try.these()函數(shù),示例代碼如下:
var Try = { these: function() { var returnValue; //遍歷參數(shù),分別創(chuàng)建 for (var i = 0, length = arguments.length; i < length; i++) { var lambda = arguments[i]; try { //如果創(chuàng)建成功,則中止 returnValue = lambda(); break; } catch (e) {} } return returnValue; } }
通過這個函數(shù),前面創(chuàng)建XMLHttpRequest對象的方式就可以改為如下所示:
var Ajax.xmlhttp = Try.these( //分別創(chuàng)建,直到有一個成功 function() {return new XMLHttpRequest()}, function() {return new ActiveXObject('Msxml2.XMLHTTP')}, function() {return new ActiveXObject('Microsoft.XMLHTTP')} ) || false;//如果都不成功,則返回false
由此可以看出Prototype設(shè)計的巧妙,讀者可以根據(jù)Prototype的設(shè)計思路進行類推,從而為自己以后的JavaScript設(shè)計提供幫助。
通過上面給出的JSON-RPC、DWR和Prototype創(chuàng)建XMLHttpRequest對象的不同方式可以比較出它們之間的特點,也可以看出它們支持瀏覽器的類型和版本。
XMLHttpRequest對象創(chuàng)建完畢后,即可開始請求連接服務(wù)器端,XMLHttpRequest可以采用同步或異步方式與服務(wù)器端通信。同步方式適用于數(shù)據(jù)量非常少的場合;一般情況下,在Ajax中都使用異步方式來與服務(wù)器端通信。通過XMLHttpRequest對象的open方法的第3個參數(shù),可以用來設(shè)定是采用同步還是異步方式,參數(shù)為true代表異步方式,為false代表同步方式,示例代碼如下:
xmlhttp.open("GET", "http://localhost:8080/index.jsp", true);
XMLHttpRequest對象的open方法共有以下5個參數(shù)。
● request-type:發(fā)送請求的類型,如GET、POST或HEAD。
● url:要連接的URL。
● asynch:如果希望使用異步方式連接則為true,否則為false。該參數(shù)是可選的,默認值為true。
● username:如果需要身份驗證,則可以在此指定用戶名。該可選參數(shù)沒有默認值。
● password:如果需要身份驗證,則可以在此指定口令。該可選參數(shù)沒有默認值。
設(shè)定通信的相關(guān)參數(shù),即調(diào)用open方法,并不代表已經(jīng)把信息發(fā)送給服務(wù)器端了,只代表這個請求已經(jīng)建立,要想把信息發(fā)送給服務(wù)器端,還要調(diào)用send方法。調(diào)用send方法的示例代碼如下:
xmlhttp.send(body);
send方法有一個參數(shù)body,表示要發(fā)送的信息,其格式和Web方式下發(fā)送信息的格式一樣,如下所示:
var body = ''username=gd&password=gf'';
如果采用GET的通信方式,則服務(wù)器端通信可以采用request.querystring()的方法獲取值;如果采用POST的通信方式,則服務(wù)器端通信可以采用request.form()的方法來獲取值,當然也可以不傳值,即body=null。另外,如果采用POST的通信方式傳送信息給服務(wù)器端,則必須先調(diào)用setRequestHeader方法,修改MIME類型。
在Prototype中的實現(xiàn)代碼如下:
setRequestHeaders: function() { var headers = {//設(shè)置各種不同的頭參數(shù) 'X-Requested-With': 'XMLHttpRequest', 'X-Prototype-Version': Prototype.Version, 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' }; //判斷通信方式是否設(shè)定為POST if (this.method == 'post') { headers['Content-type'] = this.options.contentType + (this.options.encoding ? '; charset=' + this.options.encoding : ''); /*單獨處理比較老版本的Mozilla瀏覽器,把'Connection'關(guān)閉*/ if (this.transport.overrideMimeType && (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) headers['Connection'] = 'close'; } //如果用戶自己定義了頭參數(shù) if (typeof this.options.requestHeaders == 'object') { var extras = this.options.requestHeaders; if (typeof extras.push == 'function') for (var i = 0, length = extras.length; i < length; i += 2) headers[extras[i]] = extras[i+1]; else $H(extras).each(function(pair) { headers[pair.key] = pair.value }); } //如果用戶定義了多個頭參數(shù) for (var name in headers) this.transport.setRequestHeader(name, headers[name]); }
如果前面設(shè)定為采用異步方式來進行通信,則執(zhí)行完send方法后,會立即執(zhí)行接下來的語句,因為采用異步方式要設(shè)定回調(diào)函數(shù),等狀態(tài)改變時可以調(diào)用回調(diào)函數(shù);但是如果設(shè)定為采用同步方式來進行通信,則執(zhí)行完send方法后,會一直等待直到服務(wù)器端處理完畢,并返回數(shù)值后或者請求超時后才會接著執(zhí)行下面的語句。
如果是采用同步方式來和服務(wù)器端通信,那么在服務(wù)器端返回數(shù)據(jù)后,再調(diào)用函數(shù)進行處理就可以了;但是如果是采用異步方式來和服務(wù)器端通信,客戶端不知道服務(wù)器端會在何時返回數(shù)據(jù),也就沒有辦法知道該在何時調(diào)用函數(shù)進行操作了。為了解決這個問題,XMLHttpRequest對象實現(xiàn)了一個onreadystatechange事件,它根據(jù)XMLHttpRequest對象的狀態(tài)屬性readyState的改變來觸發(fā)函數(shù),onreadystatechange事件在每一次readyState的狀態(tài)值改變時都會被觸發(fā)。
設(shè)置回調(diào)函數(shù)的示例代碼如下:
//設(shè)定回調(diào)函數(shù) xmlhttp.onreadystatechange=function(){ //獲取返回值 var msgWeclome = xmlhttp.responseText; var msg = document.getElementById("msg"); msg.innerHTML = msgWeclome; }
一般情況下,只有readyState的狀態(tài)值為4時才進行實際的函數(shù)處理,這是因為狀態(tài)值為4代表請求完畢,數(shù)據(jù)已接收成功,此時進行處理才有意義。readyState的狀態(tài)值共有5個,如表4.23所示。
表4.23 readyState的狀態(tài)值

就算readyState的狀態(tài)值是4,也只是表示請求的狀態(tài),代表了服務(wù)器端數(shù)據(jù)已接收成功,并不表示請求的結(jié)果是否成功,即請求是否已經(jīng)成功返回客戶端。為解決這個問題, XMLHttpRequest對象又實現(xiàn)了一個屬性status,用來表示請求響應(yīng)代碼。status的狀態(tài)值有很多,這里只列出幾個常用的,如表4.24所示。
表4.24 status的常用狀態(tài)值

有了這兩個屬性,在設(shè)置回調(diào)函數(shù)時,一般都要增加對這兩個屬性狀態(tài)的判斷,示例代碼如下:
//設(shè)定回調(diào)函數(shù) xmlhttp.onreadystatechange=function(){ if (4 == xmlhttp.readystate) { if (200 == xmlhttp.status) { //獲取返回值 var msgWeclome = xmlhttp.responseText; var msg = document.getElementById("msg"); msg.innerHTML = msgWeclome; } } }
XMLHttpRequest對象一般常用的處理返回值的屬性有兩個,分別是responseXML和responseText。
(1)responseXML,顧名思義,采用這個屬性來處理返回值,返回的肯定是XML格式的文檔對象,而且是已經(jīng)解析好的文檔對象,可以直接操作。這里要說明的一點是,如果請求的直接是XML對象,則在服務(wù)器端不需要設(shè)定Response的ContentType值,服務(wù)器會自動設(shè)定;如果請求的結(jié)果是動態(tài)生成的XML對象,則在服務(wù)器端要設(shè)定Response的ContentType值,設(shè)定方法如下:
response.setContentType("text/xml;charset=UTF-8");
注意:加上charset=UTF-8的意思是防止中文亂碼。
(2)responseText,采用這個屬性來處理返回值,返回的是純文本代碼,未經(jīng)過加工處理。
4.5.2 XMLHttpRequest的方法和屬性
在4.5.1節(jié)的“XMLHttpRequest的生命周期”中,已經(jīng)講解了它的一部分方法和屬性, XMLHttpRequest還提供了其他的一些方法和屬性,下面一一進行講解。
XMLHttpRequest的常用方法如表4.25所示。
表4.25 XMLHttpRequest的常用方法

XMLHttpRequest的常用屬性如表4.26所示。
表4.26 XMLHttpRequest的常用屬性

4.5.3 建立XMLHttpRequest對象池
因為使用XMLHttpRequest對象具有異步通信的功能,所以在一個頁面中原來頁面刷新一次只能進行一次和服務(wù)器交互的功能,現(xiàn)在改為用戶可以隨時點擊多次和服務(wù)器進行交互,這樣一來就會出現(xiàn)問題,就是如果整個頁面通用一個XMLHttpRequest對象,則只有最后一次調(diào)用的請求能有結(jié)果返回,因為新的請求會覆蓋舊的請求。如果采用和服務(wù)器每交互一次就新建一個XMLHttpRequest對象的方式,則會很耗費客戶端的內(nèi)存,造成資源的浪費。要解決這個問題,可以采用和數(shù)據(jù)庫連接池類似的方法,建立一個XMLHttpRequest對象池。
實現(xiàn)XMLHttpRequest對象池的大體思路是:建立一個緩存數(shù)組用來存放已經(jīng)創(chuàng)建好的XMLHttpRequest對象,遇到用戶請求需要創(chuàng)建XMLHttpRequest對象時,則先查看緩存數(shù)組中是否已經(jīng)有XMLHttpRequest對象空閑,如果有則直接從緩存中取一個使用;如果沒有則重新創(chuàng)建一個。
實現(xiàn)XMLHttpRequest對象池的示例代碼如下:
var Try = { these: function() { var returnValue; for (var i = 0, length = arguments.length; i < length; i++) { var lambda = arguments[i]; try { //如果創(chuàng)建成功,則中止 returnValue = lambda(); break; } catch (e) {} } return returnValue; } }; var Ajax = { getTransport: function() { //創(chuàng)建一個XMLHttp,直到成功為止 return Try.these( function() {return new XMLHttpRequest()}, function() {return new ActiveXObject('Msxml2.XMLHTTP')}, function() {return new ActiveXObject('Microsoft.XMLHTTP')} ) || false; } }; var XMLHttp = { _xmlhttpCache: [], _getXmlhttp: function () { //判斷是否有空閑的XMLHttp for (var i = 0; i < this._xmlhttpCache.length; i ++) { if (this._xmlhttpCache[i].readyState == 0 || this._xmlhttpCache[i].readyState == 4 { return this._xmlhttpCache[i]; } } //創(chuàng)建一個新的XMLHttp this._xmlhttpCache[this._xmlhttpCache.length] = Ajax.getTransport(); //返回緩存中的一個XMLHttp return this._xmlhttpCache[this._xmlhttpCache.length - 1]; }, send: function(method, url, data, callback){ var xmlhttp = this._getXmlhttp(); with(xmlhttp) { try { if (url.indexOf("?")!=-1) {//防止緩存 url+="&requestTime="+(new Date()).getTime(); } else { url+="?requestTime="+(new Date()).getTime(); } //采用異步方式調(diào)用 open(method, url, true); //設(shè)定請求編碼方式 setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset= UTF-8'); send(data); onreadystatechange = function () { if (xmlhttp.readyState == 4 && (xmlhttp.status == 200 || xmlhttp.status == 304)) { //調(diào)用回調(diào)函數(shù) callback(xmlhttp); } } } } } };
將上述代碼單獨保存為gd.js文件,然后需要用到它的頁面將其引入即可,引入代碼示例如下:
<script type='text/javascript' src='gd.js'></script>
只在網(wǎng)頁中保留回調(diào)函數(shù)的代碼即可實現(xiàn)和第8章中示例代碼同樣的功能,從而實現(xiàn)了代碼的重用;當然也可以將回調(diào)函數(shù)單獨保存成JS文件,從而實現(xiàn)HTML和JavaScript代碼的分離。修改后的myHelloWorld.jsp的代碼如下所示:
//******* myHelloWorld.jsp ************** <%@ page contentType="text/html; charset=UTF-8" language="java" import="java.sql.*" errorPage="" %> <html> <head> <title>Ajax</title> <script type='text/javascript' src='gd.js'></script> <script type="text/javascript"> function ok() { XMLHttp.send("get", "http://localhost:8080/myHelloWorld/myHelloWorld.do",null,gf); //設(shè)定回調(diào)函數(shù) function gf(xmlhttp){ var msgWeclome = xmlhttp.responseText; var msg = document.getElementById("msg"); msg.innerHTML = msgWeclome; } } </script> </head> <body> <span id='msg'></span><br> <input type="button" onclick="ok()" value="單擊此按鈕"/> </body> </html>
4.5.4 使用Iframe代替XMLHttpRequest
在Ajax興起之前,其實也可以實現(xiàn)頁面無刷新即可和服務(wù)器交互的功能,那就是利用Iframe。本節(jié)講解如何使用Iframe代替XMLHttpRequest實現(xiàn)Ajax的功能。
實現(xiàn)的大體思路是:在提交頁面中嵌套一個Iframe,然后在提交時,將form的target設(shè)定為該Iframe。另外,在Servlet返回時,設(shè)定返回到一個臨時頁面,然后在該臨時頁面調(diào)用提交頁面的方法來獲取服務(wù)器端返回的信息,具體步驟如下:
01 用鼠標右鍵單擊JSP文件夾,在彈出的快捷菜單中選擇“New>JSP”命令,彈出“Create a new JSP page”對話框,如圖4.16所示。

圖4.16 “Create a new JSP page”對話框
02 輸入文件名為“myHelloWorldIframe.jsp”,然后單擊“Finish”按鈕,即可新建JSP文件myHelloWorldIframe.jsp。
03 輸入myHelloWorldIframe.jsp的代碼如下所示:
//*******myHelloWorldIframe.jsp************** <%@ page contentType="text/html; charset=UTF-8" language="java" import="java.sql.*" errorPage="" %> <%@ page import="java.sql.*,java.util.*,javax.servlet.*, javax.servlet.http.*,java.text.*,java.math.*" %> <% //獲取服務(wù)器端傳來的數(shù)據(jù) String msg = (String)((request.getAttribute("msg") == null) ? "" : (String)request.getAttribute ("msg")); %> <html> <head> <title>使用Iframe代替XMLHttpRequest</title> <script type="text/javascript"> function ok() { form1.target ="sendMess"; form1.submit(); } //得到從子頁面(myHelloWorldIframeSend.jsp)返回的信息顯示 function getMsg(str) { var msg = document.getElementById("msg"); msg.innerHTML = str; } </script> </head> <body> <form name="form1" action="/myHelloWorld/myHelloWorld.do" method="get"> <span id='msg'><%=msg%></span><br> <input type="button" onclick="ok();" value="單擊此按鈕"/> <iframe name="sendMess" style="display:none" ></iframe> </form> </body> </html>
04 使用同樣的方法新建myHelloWorldIframeSend.jsp文件,輸入myHelloWorldIframeSend.jsp的代碼如下所示:
//******* myHelloWorldIframeSend.jsp ************** <%@ page contentType="text/html; charset=UTF-8" language="java" import="java.sql.*" errorPage="" %> <%@ page import="java.sql.*,java.util.*,javax.servlet.*, javax.servlet.http.*,java.text.*,java.math.*" %> <% //獲取服務(wù)器端傳來的數(shù)據(jù) String msg = (String)((request.getAttribute("msg") == null) ? "" : (String)request.getAttribute ("msg")); %> <html> <head> <title>使用Iframe代替XMLHttpRequest</title> <script type="text/javascript"> //返回信息到父頁面(myHelloWorldIframe.jsp) function setReturnMsg(msg) { window.parent.getMsg(msg); } setReturnMsg('<%=msg%>'); </script> </head> <body> <form name="form1" method="post"> </form> </body> </html>
05 編寫Servlet程序HelloWorldIframe.java,主要用來獲取服務(wù)器端的時間并組成問候語。用鼠標右鍵單擊包com.myHelloWorld.web,在彈出的快捷菜單中選擇“New>Servlet”命令,彈出“Create a new Servlet”對話框,如圖4.17所示。在該對話框中輸入相應(yīng)內(nèi)容,然后單擊“Next”按鈕就會進入關(guān)于Servlet程序配置的對話框,具體配置如圖4.18所示。

圖4.17 “Create a new Servlet”對話框

圖4.18 配置Servlet
06 單擊“Finish”按鈕,即可新建類HelloWorldIframe.java。
07 輸入HelloWorldIframe.java的代碼如下所示:
//******* HelloWorldIframe.java************** package com.myHelloWorld.web; import java.io.IOException; import java.io.PrintWriter; import java.text.SimpleDateFormat; import java.util.Date; //引入Servlet import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; //該類繼承Servlet類 public class HelloWorldIframe extends HttpServlet { public void doGet(HttpServletRequest request,HttpServletResponse response)throws IOException, ServletException { response.setContentType("text/plain;charset=UTF-8"); request.setAttribute("msg","現(xiàn)在時間是:"+getCurrentDateAndTime()+" 歡迎您(HelloWorld)"); RequestDispatcher rd = request.getRequestDispatcher("/jsp/myHelloWorldIframeSend.jsp"); rd.forward(request, response); } /** * 得到當前系統(tǒng)日期,格式:yyyy-MM-dd HH:mm:ss * @return String */ public String getCurrentDateAndTime() { String currentDate = ""; //設(shè)定日期格式 SimpleDateFormat format1 = new SimpleDateFormat("yyyy'-'MM'-'dd HH:mm:ss"); format1.setLenient(false); currentDate = format1.format(new Date()); //返回當前日期 return currentDate; } }
05 查看web.xml文件,這是Web程序開發(fā)所必需的,web.xml文件的示例代碼如下:
//******* web.xml ************** <?xml version="1.0" encoding="UTF-8"?> <web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> <!--設(shè)定Servlet--> <servlet> <servlet-name>Servlet</servlet-name> <servlet-class>com.myHelloWorld.web.HelloWorldIframe</servlet-class> </servlet> <!--設(shè)定Servlet的對應(yīng)關(guān)系--> <servlet-mapping> <servlet-name>Servlet</servlet-name> <url-pattern>*.do</url-pattern> </servlet-mapping> </web-app>
09 上述代碼完成后,在MyEclipse上啟動Tomcat 7,然后在IE地址欄中輸入http://localhost:8080/myHelloWorld/jsp/myHelloWorldIframe.jsp,即可看到有“單擊此按鈕”按鈕的畫面,如圖4.19所示。

圖4.19 有“單擊此按鈕”按鈕的畫面
10 單擊“單擊此按鈕”按鈕,即可顯示服務(wù)器端的時間和問候語,如圖4.20所示。

圖4.20 顯示出服務(wù)器端的時間和問候語
- Microsoft Dynamics CRM Customization Essentials
- 大數(shù)據(jù)導(dǎo)論:思維、技術(shù)與應(yīng)用
- Seven NoSQL Databases in a Week
- 微型計算機控制技術(shù)
- 機器學(xué)習(xí)與大數(shù)據(jù)技術(shù)
- 數(shù)據(jù)庫原理與應(yīng)用技術(shù)
- B2B2C網(wǎng)上商城開發(fā)指南
- Android游戲開發(fā)案例與關(guān)鍵技術(shù)
- 精通數(shù)據(jù)科學(xué)算法
- Lightning Fast Animation in Element 3D
- 愛犯錯的智能體
- 工業(yè)機器人維護與保養(yǎng)
- 零起點學(xué)西門子S7-200 PLC
- Web璀璨:Silverlight應(yīng)用技術(shù)完全指南
- 大數(shù)據(jù)素質(zhì)讀本