您好,登錄后才能下訂單哦!
HTML5中如何進行多線程編程應用,相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。
HTML5 中工作線程(Web Worker)簡介
至 2008 年 W3C 制定出第一個 HTML5 草案開始,HTML5 承載了越來越多嶄新的特性和功能。它不但強化了 Web 系統或網頁的表現性能,而且還增加了對本地數據庫等 Web 應用功能的支持。其中,最重要的一個便是對多線程的支持。在 HTML5 中提出了工作線程(Web Worker)的概念,并且規范出 Web Worker 的三大主要特征:能夠長時間運行(響應),理想的啟動性能以及理想的內存消耗。Web Worker 允許開發人員編寫能夠長時間運行而不被用戶所中斷的后臺程序,去執行事務或者邏輯,并同時保證頁面對用戶的及時響應。本文深入 HTML5 多線程規范,講述多線程實現原理、方法,同時以實例的形式講解 HTML5 中多線程編程以及應用。
W3C 中的工作線程規范到目前為止已經定義了出了一系列公共接口,它允許 Web 程序開發人員去創建后臺線程在他們的主頁面中并發的運行腳本。這將使得線程級別的消息通信成為現實。
詳解 HTML5 工作線程原理
傳統上的線程可以解釋為輕量級進程,它和進程一樣擁有獨立的執行控制,一般情況下由操作系統負責調度。而在 HTML5 中的多線程是這樣一種機制,它允許在 Web 程序中并發執行多個 JavaScript 腳本,每個腳本執行流都稱為一個線程,彼此間互相獨立,并且有瀏覽器中的 JavaScript 引擎負責管理。下面我們將詳細講解 HTML5 的工作線程原理。
工作線程與多線程編程
在 HTML5 中,工作線程的出現使得在 Web 頁面中進行多線程編程成為可能。眾所周知,傳統頁面中(HTML5 之前)的 JavaScript 的運行都是以單線程的方式工作的,雖然有多種方式實現了對多線程的模擬(例如:JavaScript 中的 setinterval 方法,setTimeout 方法等),但是在本質上程序的運行仍然是由 JavaScript 引擎以單線程調度的方式進行的。在 HTML5 中引入的工作線程使得瀏覽器端的 JavaScript 引擎可以并發地執行 JavaScript 代碼,從而實現了對瀏覽器端多線程編程的良好支持。
HTML5 中的 Web Worker 可以分為兩種不同線程類型,一個是專用線程 Dedicated Worker,一個是共享線程 Shared Worker。兩種類型的線程各有不同的用途。下面對這兩種工作線程作了詳細的說明和描述。
專用線程:Dedicated Worker
1. 專用線程(dedicated worker)的創建方式:
在創建專用線程的時候,需要給 Worker 的構造函數提供一個指向 JavaScript 文件資源的 URL,這也是創建專用線程時 Worker 構造函數所需要的唯一參數。當這個構造函數被調用之后,一個工作線程的實例便會被創建出來。下面是創建專用線程代碼示例:
清單 1. 創建專用線程示例代碼
var worker = new Worker('dedicated.js');
2. 與一個專用線程通信:
專用線程在運行的過程中會在后臺使用 MessagePort 對象,而 MessagePort 對象支持 HTML5 中多線程提供的所有功能,例如:可以發送和接受結構化數據(JSON 等),傳輸二進制數據,并且支持在不同端口中傳輸數據等。
為了在頁面主程序接收從專用線程傳遞過來的消息,我們需要使用工作線程的 onmessage 事件處理器,定義 onmessage 的實例代碼如下:
清單 2. 接收來至工作線程示例代碼
worker.onmessage = function (event) { ... };
3.另外,開發人員也可以選擇使用 addEventListener 方法,它最終的實現方式和作用和 onmessage 相同。
就像前面講述的,專用線程會使用隱式的 MessagePort 實例,當專用線程被創建的時候,MessagePort 的端口消息隊列便被主動啟用。因此,這也和工作線程接口中定義的 start 方法作用一 致。
如果要想一個專用線程發送數據,那么我們需要使用線程中的 postMessage 方法。專用線程不僅僅支持傳輸二進制數據,也支持結構化的 JavaScript 數據格式。在這里有一點需要注意,為了高效地傳輸 ArrayBuffer 對象數據,需要在 postMessage 方法中的第二個參數中指定它。實例代碼如下:
清單 3. 高效的發送 ArrayBuffer 數據代碼
worker.postMessage({ operation: 'list_all_users', //ArrayBuffer object input: buffer, threshold: 0.8, }, [buffer]);
共享線程 Shared Worker
1.共享線程
共享線程可以由兩種方式來定義:一是通過指向 JavaScript 腳本資源的 URL 來創建,而是通過顯式的名稱。當由顯式的名稱來定義的時候,由創建這個共享線程的第一個頁面中使用 URL 會被用來作為這個共享線程的 JavaScript 腳本資源 URL。通過這樣一種方式,它允許同域中的多個應用程序使用同一個提供公共服務的共享線程,從而不需要所有的應用程序都去與這個提供公共服務的 URL 保持聯系。
無論在什么情況下,共享線程的作用域或者是生效范圍都是由創建它的域來定義的。因此,兩個不同的站點(即域)使用相同的共享線程名稱也不會沖突。
2.共享線程的創建
創建共享線程可以通過使用 SharedWorker() 構造函數來實現,這個構造函數使用 URL 作為第一個參數,即是指向 JavaScript 資源文件的 URL,同時,如果開發人員提供了第二個構造參數,那么這個參數將被用于作為這個共享線程的名稱。創建共享線程的代碼示例如下:
/ 從端口接收數據 , 包括文本數據以及結構化數據 1. worker.port.onmessage = function (event) { define your logic here... }; // 向端口發送普通文本數據
worker.port.postMessage('put your message here … '); // 向端口發送結構化數據
worker.port.postMessage({ username: 'usertext'; live_city: ['data-one', 'data-two', 'data-three','data-four']});
3.與共享線程通信
共享線程的通信也是跟專用線程一樣,是通過使用隱式的 MessagePort 對象實例來完成的。當使用 SharedWorker() 構造函數的時候,這個對象將通過一種引用的方式被返回回來。我們可以通過這個引用的 port 端口屬性來與它進行通信。發送消息與接收消息的代碼示例如下:
清單 4. 發送消息與接收消息代碼
上面示例代碼中,第一個我們使用 onmessage 事件處理器來接收消息,第二個使用 postMessage 來發送普通文本數據,第三個使用 postMessage 來發送結構化的數據,這里我們使用了 JSON 數據格式。
工作線程事件處理模型
當工作線程被一個具有 URL 參數的構造函數創建的時候,它需要有一系列的處理流程來處理和記錄它本身的數據和狀態。下面我們給出了工作線程的處理模型如下(注:由于 W3C 中工作線程的規范依然在更新,您讀到這篇文章的時候可能看到已不是最新的處理模型,建議參考 W3C 中的最新規范):
1. 創建一個獨立的并行處理環境,并且在這個環境里面異步的運行下面的步驟。
2. 如果它的全局作用域是 SharedWorkerGlobalScope 對象,那么把最合適的應用程序緩存和它聯系在一起。
3. 嘗試從它提供的 URL 里面使用 synchronous 標志和 force same-origin 標志獲取腳本資源。
4. 新腳本創建的時候會按照下面的步驟:
創建這個腳本的執行環境。
使用腳本的執行環境解析腳本資源。
設置腳本的全局變量為工作線程全局變量。
設置腳本編碼為 UTF-8 編碼。
5. 啟動線程監視器,關閉孤兒線程。
6. 對于掛起線程,啟動線程監視器監視掛起線程的狀態,即時在并行環境中更改它們的狀態。
7. 跳入腳本初始點,并且啟動運行。
8. 如果其全局變量為 DedicatedWorkerGlobalScope 對象,然后在線程的隱式端口中啟用端口消息隊列。
9. 對于事件循環,等待一直到事件循環列表中出現新的任務。
10. 首先運行事件循環列表中的最先進入的任務,但是用戶代理可以選擇運行任何一個任務。
11. 如果事件循環列表擁有存儲 mutex 互斥信號量,那么釋放它。
12. 當運行完一個任務后,從事件循環列表中刪除它。
13. 如果事件循環列表中還有任務,那么繼續前面的步驟執行這些任務。
14. 如果活動超時后,清空工作線程的全局作用域列表。
15. 釋放工作線程的端口列表中的所有端口。
工作線程應用范圍和作用域
工作線程的全局作用域僅僅限于工作線程本身,即在線程的生命周期內有效。規范中 WorkerGlobalScope 接口代表了它的全局作用域,下面我們來看下這個接口的具體實施細節(WorkerGlobalScope 抽象接口)。
清單 5. WorkerGlobalScope 抽象接口代碼
interface WorkerGlobalScope { readonly attribute WorkerGlobalScope self; readonly attribute WorkerLocation location; void close(); attribute Function onerror; }; WorkerGlobalScope implements WorkerUtils; WorkerGlobalScope implements EventTarget;
我們可以使用 WorkerGlobalScope 的 self 屬性來或者這個對象本身的引用。location 屬性返回當線程被創建出來的時候與之關聯的 WorkerLocation 對象,它表示用于初始化這個工作線程的腳步資源的絕對 URL,即使頁面被多次重定向后,這個 URL 資源位置也不會改變。
當腳本調用 WorkerGlobalScope 上的 close()方法后,會自動的執行下面的兩個步驟:
1. 刪除這個工作線程事件隊列中的所有任務。
2. 設置 WorkerGlobalScope 對象的 closing 狀態為 true (這將阻止以后任何新的任務繼續添加到事件隊列中來)。
工作線程生命周期
工作線程之間的通信必須依賴于瀏覽器的上下文環境,并且通過它們的 MessagePort 對象實例傳遞消息。每個工作線程的全局作用域都擁有這些線程的端口列表,這些列表包括了所有線程使用到的 MessagePort 對象。在專用線程的情況下,這個列表還會包含隱式的 MessagePort 對象。
每個工作線程的全局作用域對象 WorkerGlobalScope 還會有一個工作線程的線程列表,在初始化時這個列表為空。當工作線程被創建的時候或者擁有父工作線程的時候,它們就會被填充進來。
最后,每個工作線程的全局作用域對象 WorkerGlobalScope 還擁有這個線程的文檔模型,在初始化時這個列表為空。當工作線程被創建的時候,文檔對象就會被填充進來。無論何時當一個文檔對象被丟棄的時候,它就要從這個文檔對象列舉里面刪除出來。
在工作線程的生命周期中,定義了下面四種不同類型的線程名稱,用以標識它們在線程的整個生命周期中的不同狀態:
當一個工作線程的文檔對象列舉不為空的時候,這個工作線程會被稱之為許可線程。(A worker is said to be a permissible worker if its list of the worker's Documents is not empty.)
當一個工作線程是許可線程并且或者擁有數據庫事務或者擁有網絡連接或者它的工作線程列表不為空的時候,這個工作線程會被稱之為受保護的線程。(A worker is said to be a protected worker if it is a permissible worker and either it has outstanding timers, database transactions, or network connections, or its list of the worker's ports is not empty)
當一個工作線程的文檔對象列表中的任何一個對象都是處于完全活動狀態的時候,這個工作線程會被稱之為需要激活線程。(A worker is said to be an active needed worker if any of the Document objects in the worker's Documents are fully active.)
當一個工作線程是一個非需要激活線程同時又是一個許可線程的時候,這個工作線程會被稱之為掛起線程。(A worker is said to be a suspendable worker if it is not an active needed worker but it is a permissible worker.)
由于 W3C 的 Web Worker 規范目前還是處于完善階段,沒有形成最終的規范,本文也將上面線程的四種不同狀態的原文定義附在了后面。
工作線程(Web Worker)API 接口
類庫和腳本的訪問和引入
對于類庫和腳本的訪問和引入,規范中規定可以使用 WorkerGlobalScope 對象的 importScripts(urls) 方法來引入網絡中的腳本資源。當用戶調用這個方法引入資源的時候會執行下面的步驟來完成這個操作:
如果沒有給 importScripts 方法任何參數,那么立即返回,終止下面的步驟。
解析 importScripts 方法的每一個參數。
如果有任何失敗或者錯誤,拋出 SYNTAX_ERR 異常。
嘗試從用戶提供的 URL 資源位置處獲取腳本資源。
對于 importScripts 方法的每一個參數,按照用戶的提供順序,獲取腳本資源后繼續進行其它操作。
清單 6. 外部資源腳本引入和訪問示例代碼
/** * 使用 importScripts 方法引入外部資源腳本,在這里我們使用了數學公式計算工具庫 math_utilities.js * 當 JavaScript 引擎對這個資源文件加載完畢后,繼續執行下面的代碼。同時,下面的的代碼可以訪問和調用 * 在資源文件中定義的變量和方法。 **/ importScripts('math_utilities.js'); /** * This worker is used to calculate * the least common multiple * and the greatest common divisor */ onmessage = function (event) { var first=event.data.first; var second=event.data.second; calculate(first,second); }; /* * calculate the least common multiple * and the greatest common divisor */ function calculate(first,second) { //do the calculation work var common_divisor=divisor(first,second); var common_multiple=multiple(first,second); postMessage("Work done! " + The least common multiple is "+common_divisor +" and the greatest common divisor is "+common_multiple); }
工作導航器對象(WorkerNavigator)
在 HTML5 中, WorkerUtils 接口的 navigator 屬性會返回一個工作導航器對象(WorkerNavigator),這個對象定義并且代表了用戶代理(即 Web 客戶端)的標識和狀態。因此,用戶和 Web 腳本開發人員可以在多線程開發過程中通過這個對象來取得或者確定用戶的狀態。
工作導航器對象(WorkerNavigator)
WorkerUtils 抽象接口的 navigator 屬性會返回一個 WorkerNavigator 用戶接口,用于用戶代理的識別的狀態標識。我們來看下 WorkerNavigator 接口的定義。
WorkerNavigator 接口定義
清單 7. WorkerNavigator 接口定義代碼
interface WorkerNavigator {};
WorkerNavigator implements NavigatorID;
WorkerNavigator implements NavigatorOnLine;
其中,有一點需要注意:如果接口的相對命名空間對象為 Window 對象的時候,WorkerNavigator 對象一定不可以存在,即無法再使用這個對象。
創建與終止線程
在講解創建新的工作線程之前,我們先看下 W3C 規范對工作線程的定義。工作線程規范中定義了線程的抽象接口類 AbstractWorker ,專用線程以及共享線程都繼承自該抽象接口。專用線程以及共享線程的創建方法讀者可以參考第一小節中的示例代碼。下面是此抽象接口的定義。
1.AbstractWorker 抽象接口
清單 8. AbstractWorker 抽象接口代碼
此外,該接口還定義了錯誤處理的事件處理器 onerror,當工作線程在通信過程中遇到錯誤時便會觸發這個事件處理器。
2.專用線程及其定義
清單 9. 專用線程定義代碼
[Constructor(in DOMString scriptURL)] interface Worker : AbstractWorker { void terminate(); void postMessage(in any message, in optional MessagePortArray ports); attribute Function onmessage; };
當創建完線程以后,我們可以調用 terminate() 方法去終止一個線程。每個專用線程都擁有一個隱式的 MessagePort 對象與之相關聯。這個端口隨著線程的創建而被創建出來,但并沒有暴露給用戶。所有的基于這個端口的消息接收都以線程本身為目標。
3.共享線程及其定義
清單 10. 共享線程定義代碼
[Constructor(DOMString scriptURL, optional DOMString name)] interface SharedWorker : AbstractWorker { readonly attribute MessagePort port; };
共享線程同專用線程一樣,當創建完線程以后,我們可以調用 terminate() 方法去終止一個共享線程。
工作線程位置屬性
工作線程被創建出來以后,需要記錄它的狀態以及位置信息,在工作線程規范中定義了 WorkerLocation 來表示它們的位置。接口定義如下:
清單 11. 共享線程定義代碼
interface WorkerLocation { // URL decomposition IDL attributes stringifier readonly attribute DOMString href; readonly attribute DOMString protocol; readonly attribute DOMString host; readonly attribute DOMString hostname; readonly attribute DOMString port; readonly attribute DOMString pathname; readonly attribute DOMString search; readonly attribute DOMString hash; };
WorkerLocation 對象表示了工作線程腳本資源的絕對 URL 信息。我們可以使用它的 href 屬性取得這個對象的絕對 URL。WorkerLocation 接口還定義了與位置信息有關的其它屬性,例如:用于信息傳輸的協議(protocol),主機名稱(hostname),端口(port),路徑名稱(pathname)等。
工作線程(Web Worker)應用與實踐
我們可以寫出很多的例子來說明后臺工作線程的合適的用法,下面我們以幾種典型的應用場景為例,用代碼實例的形式講解在各種需求背景下正確的使用它們。
應用場景一:使用工作線程做后臺數值(算法)計算
工作線程最簡單的應用就是用來做后臺計算,而這種計算并不會中斷前臺用戶的操作。下面我們提供了一個工作線程的代碼片段,用來執行一個相對來說比較復雜的任務:計算兩個非常大的數字的最小公倍數和最大公約數。
在這個例子中,我們在主頁面中創建一個后臺工作線程,并且向這個工作線程分配任務(即傳遞兩個特別大的數字),當工作線程執行完這個任務時,便向主頁面程序返回計算結果,而在這個過程中,主頁面不需要等待這個耗時的操作,可以繼續進行其它的行為或任務。
我們把這個應用場景分為兩個主要部分,一個是主頁面,可以包含主 JavaScript 應用入口,用戶其它操作 UI 等。另外一個是后臺工作線程腳本,即用來執行計算任務。代碼片段如下:
清單 12. 主程序頁面代碼
<!DOCTYPE HTML> <html> <head> <title> Background Worker Application Example 1: Complicated Number Computation </title> </head> <body> <div> The least common multiple and greatest common divisor is: <p id="computation_results">please wait, computing … </p> </div> <script> var worker = new Worker('numberworker.js'); worker.postMessage("{first:347734080,second:3423744400}"); worker.onmessage = function (event) { document.getElementById(' computation_result').textContent = event.data; }; </script> </body> </html>
清單 13. 后臺工作線程代碼
/** * This worker is used to calculate * the least common multiple * and the greatest common divisor */ onmessage = function (event) { var first=event.data.first; var second=event.data.second; calculate(first,second); }; /* * calculate the least common multiple * and the greatest common divisor */ function calculate(first,second) { //do the calculation work var common_divisor=divisor(first,second); var common_multiple=multiple(first,second); postMessage("Work done! " + "The least common multiple is "+common_divisor +" and the greatest common divisor is "+common_multiple); } /** * calculate the greatest common divisor * @param number * @param number * @return */ function divisor(a, b) { if (a % b == 0) { return b; } else { return divisor(b, a % b); } } /** * calculate the least common multiple * @param number * @param number * @return */ function multiple( a, b) { var multiple = 0; multiple = a * b / divisor(a, b); return multiple; }
在主程序頁面中,我們使用 Worker()構造函數創建一個新的工作線程,它會返回一個代表此線程本身的線程對象。接下來我們使用這個線程對象與后臺腳本進行通信。線程對象有兩個主要事件處理器:postMessage 和 onmessage 。postMessage 用來向后臺腳本發送消息,onmessage 用以接收從后臺腳本中傳遞過來的消息。
在后臺工作線程代碼片段中,我們定一個兩個 JavaScript 函數,一個是 function divisor:用以計算最大公約數,一個是 function multiple:用以計算最小公倍數。同時工作線程的 onmessage 事件處理器用以接收從主頁面中傳遞過來的數值,然后把這兩個數值傳遞到 function calculate 用以計算。當計算完成后,調用事件處理器 postMessage,把計算結果發送到主頁面。
應用場景二:使用共享線程處理多用戶并發連接
由于線程的構建以及銷毀都要消耗很多的系統性能,例如 CPU 的處理器調度,內存的占用回收等,在一般的編程語言中都會有線程池的概念,線程池是一種對多線程并發處理的形式,在處理過程中系統將所有的任務添加到一個任務隊列,然后在構建好線程池以后自動啟動這些任務。處理完任務后再把線程收回到線程池中,用于下一次任務調用。線程池也是共享線程的一種應用。
在 HTML5 中也引入了共享線程技術,但是由于每個共享線程可以有多個連接,HTML5 對共享線程提供了和普通工作線程稍微有些區別的 API 接口。下面我們提供幾個例子來講述對共享線程的用法。
下面我們給出一個例子:創建一個共享線程用于接收從不同連接發送過來的指令,然后實現自己的指令處理邏輯,指令處理完成后將結果返回到各個不同的連接用戶。
清單 14. 共享線程用戶連接頁面代碼
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Shared worker example: how to use shared worker in HTML5</title> <script> var worker = new SharedWorker('sharedworker.js'); var log = document.getElementById('response_from_worker'); worker.port.addEventListener('message', function(e) { //log the response data in web page log.textContent =e.data; }, false); worker.port.start(); worker.port.postMessage('ping from user web page..'); //following method will send user input to sharedworker function postMessageToSharedWorker(input) { //define a json object to construct the request var instructions={instruction:input.value}; worker.port.postMessage(instructions); } </script> </head> <body onload=''> <output id='response_from_worker'> Shared worker example: how to use shared worker in HTML5 </output> send instructions to shared worker: <input type="text" autofocus oninput="postMessageToSharedWorker(this);return false;"> </input> </body> </html>
清單 15. 用于處理用戶指令的共享線程代碼
// 創建一個共享線程用于接收從不同連接發送過來的指令,指令處理完成后將結果返回到各個不同的連接用戶。 /* * define a connect count to trace connecting * this variable will be shared within all connections */ var connect_number = 0; onconnect = function(e) { connect_numberconnect_numberconnect_number =connect_number+ 1; //get the first port here var port = e.ports[0]; port.postMessage('A new connection! The current connection number is ' + connect_number); port.onmessage = function(e) { //get instructions from requester var instruction=e.data.instruction; var results=execute_instruction(instruction); port.postMessage('Request: '+instruction+' Response '+results +' from shared worker...'); }; }; /* * this function will be used to execute the instructions send from requester * @param instruction * @return */ function execute_instruction(instruction) { var result_value; //implement your logic here //execute the instruction... return result_value }
清單 16. 主頁面(僅僅是用來顯示計算結果)代碼
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Shared worker example: how to use delegation worker in HTML5</title> <script> var worker = new SharedWorker('delegationworker.js'); var log = document.getElementById('response_from_worker'); worker.onmessage = function (event) { //resolve the population from delegation worker var resultdata=event.data; var population=resultdata.total_population; var showtext='The total population of the word is '+population; document.getElementById('response_from_worker').textContent = showtext; }; </script> </head> <body onload=''> <output id='response_from_worker'> Shared worker example: how to use delegation worker in HTML5 </output> </body> </html>
清單 17. 主工作線程代碼
/* * define the country list in the whole word * take following Array as an example */ var country_list = ['Albania','Algeria','American','Andorra','Angola','Antigua','....']; // define the variable to record the population of the word var total_population=0; var country_size=country_list.length; var processing_size=country_list.length; for (var i = 0; i < country_size; i++) { var worker = new Worker('subworker.js'); //wrap the command, send to delegations var command={command:'start',country:country_list[i]}; worker.postMessage(command); worker.onmessage = update_results; } /* * this function will be used to update the result * @param event * @return */ function storeResult(event) { total_population += event.data; processing_size -= 1; if (processing_size <= 0) { //complete the whole work, post results to web page postMessage(total_population); } }
清單 18. 代理線程代碼
//define the onmessage hander for the delegation onmessage = start_calculate; /* * resolve the command and kick off the calculation */ function start_calculate(event) { var command=event.data.command; if(command!=null&&command=='start') { var coutry=event.data.country; do_calculate(country); } onmessage = null; } /* * the complex calculation method defined here * return the population of the country */ function do_calculate(country) { var population = 0; var cities=//get all the cities for this country for (var i = 0; i < cities.length; i++){ var city_popu=0; // perform the calculation for this city //update the city_popu population += city_popu; } postMessage(population); close(); }
HTML5 Web Worker 的多線程特性為基于 Web 系統開發的程序人員提供了強大的并發程序設計功能,它允許開發人員設計開發出性能和交互更好的富客戶端應用程序。本文不僅僅詳細講述 HTML5 中的多線程規范。同時,也以幾種典型的應用場景為例,以實例的形式講解 HTML5 中多線程編程以及應用,為用戶提供了詳細而全面的參考價值,并且指導開發人員設計和構建更為高效和穩定的 Web 多線程應用。
看完上述內容,你們掌握HTML5中如何進行多線程編程應用的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。