Qomolangma實現篇(一):核心載入模組system.js的實現

來源:互聯網
上載者:User

================================================================================
Qomolangma OpenProject v1.0

類別    :Rich Web Client
關鍵詞  :JS OOP,JS Framwork, Rich Web Client,RIA,Web Component,
          DOM,DTHML,CSS,JavaScript,JScript

項目發起:aimingoo (aim@263.net)
項目團隊:aimingoo, leon(pfzhou@gmail.com)
有貢獻者:JingYu(zjy@cnpack.org)
================================================================================

一、system.js 模組概要
~~~~~~~~~~~~~~~~~~

system.js是Qomo的第一個載入模組。這個模組主要實現三個功能:
  - 基本的$debug()函數
  - 基本的$import()函數
  - 核心子系統的裝載

system.js是firefox相容的。

二、核心子系統的構成與載入
~~~~~~~~~~~~~~~~~~

在Qomo中,所謂核心是指由直接在system.js中載入的模組構成的系統功能層。system.js
實現了$import()函數,並通過它裝載以下模組:
----------
  $import('Names/NamedSystem.js'); //命名空間管理
  $import('RTL/JSEnhance.js');     //基於標準JS的增強特性

  $import('JSUnit/debug.js');      //增強調試輸出
  // more ...                      //調試分析相關功能(profiler/unit test等)

  $import('RTL/error.js');         //錯誤和異常處理
  $import('RTL/object.js');        //實現物件導向語言特性和基類TObject
  $import('RTL/ajax.js');          //tiny ajax sub system
  // more ...                      //其它核心層級的特性(SOA、Interface等)
----------

Qomo的核心是可裁減的:如果開發人員不喜歡使用命名空間,那麼可以不載入NamedSystem.js;
或者根本就不使用增強OOP特性,那麼也可以不載入object.js。——換而言之,Qomo的
結構可以讓開發人員重新組織自己的核心子系統,並在這個基礎上發展自己的語言和架構。

Qomo實現這個特性的方法,就是使用“函數重載”和“功能重述”的技術。system.js中的
$debug使用了“函數重載”的技術,而$import()中的部分實現特性就利用了“功能重述”
的技術。

簡單的說:“函數重載”是指在延後的代碼中重寫當前函數;而“功能重述”則是指在延後
的代碼中對函數中的一個、多個特性重新描述(並代碼實現)。——基本上來說,如果你有良
好的代碼習慣,那麼可以用“更完美的物件導向的設計”+“OOP的多態特性”來實現這兩種
技術。然而,system.js是在一個很底層的、核心層級的實現,我不希望它變得龐大而低效,
所以使用了技巧來替代設計。

三、$debug()的分析
~~~~~~~~~~~~~~~~~~

在JScript系統中,提供一個調試期對象Debug,這個對象用於調試器控制台輸入資訊。如果
你使用C以及其它的進階語言編程式,你應該知道OutputDebugString()函數。而這個Debug
對象的作用就與它相同。這個對象提供了兩個方法:Debug.write()和Debug.writeln()。

然而Qomo試圖實現一個更有價值的調試輸出子系統。例如向一個輸出控制台發送對象,或者
用於記錄效率分析資訊等等。因此Qomo提供了$debug()函數。

然而作為基礎模組system.js中的$debug並不提供上述的這些(完整的)特性。system.js中的
$debug()僅僅只是向document輸出字串資訊。這與Debug.writeln()是一樣的,只是輸出信
息的目標不一樣:Debug面向偵錯主控台,而$debug()面向window.document對象。

在system.js中的$debug()實現起來可以非常簡單。例如這樣:
----------
$debug = document.writeln;
----------

然而這樣的代碼在firefox中會出錯。firefox對一些對象的方法做了保護,使得它不能被賦
值給JavaScript對象/變數,也不能反過來試圖通過賦值來修改這些行為。因此在system.js
採用的最終代碼是這樣:
----------
$debug = function() {
  document.writeln(Array.prototype.join.call(arguments, ''))
};
----------

這樣就將$debug傳入的參數arguments視作一個數組,並通過Array對象原型中的join()方法
串連成一個字串,最後使用document.writeln()輸出。

我們前面說到過$debug()使用了“函數重載”的技術。這是因為這裡的$debug()事實上只被
隨後載入的NamedSystem.js和JSEnhance.js使用。——如果在它們“載入的過程中”出了錯
誤、異常(或者出於調試的需要),就可以通過$debug()來輸出資訊。然而接下來:
  - 第一步:在JSEnhance.js的未尾,會有一行代碼將$debug置為空白函數(NullFunction);
  - 第二步:在debug.js的頭部,有一段代碼重寫$debug()函數,實現自己的輸出控制台。

這樣就保證了在任何的代碼中都可以使用$debug()函數來輸出資訊。而這個輸出的表現會是
這樣:
  - 如果是在核心載入中,則向document輸出錯誤資訊;否則,
  - 如果載入了debug.js模組,則會有一個輸出控制台來顯示資訊或者對象(資料);否則,
  - $debug()資訊被屏弊,或由使用者載入的第三方模組來承接調試資訊或錯誤資訊的輸出。

開發人員可以自由地、安全地使用$debug(),而無需關心它怎麼實現,或者如何輸出。如
果不希望WEB瀏覽者看到它,只需要去掉debug.js(或者第三方的模組),而無需移除原始碼
中的函數調用。

四、$import()的實現
~~~~~~~~~~~~~~~~~~

為了使得system.js等核心層級的模組不影響全域變數的定義,因此在核心的很多地方(當
然也包括$import()函數),使用了以下技巧來聲明函數:
----------
$import = function() {       // <--- 匿名函數1
  var data= ...
  function foo() { ... }

  return function() {        // <--- 匿名函數2
    ...
  };
}();
----------

這樣一來,foo()和data都聲明在一個“匿名函數1”的內部,因此不會對今後代碼中對全
局變數的命名造成影響(命名重複)。而$import()實際上是“匿名函數1”執行後返回的結
果:匿名函數2。——JavaScript中,“函數”(對象)可作為其它函數的執行結果返回。

重要的是,由於“匿名函數2”與data、foo()在同一個“上下文環境”中,因此它可以自
由地存取這些變數和方法。而外部、全域的其它代碼就看不到$import()的實現細節了。

利用這種技巧,$import()實現了許多內部功能和資訊的隱藏。其實現如下:
----------
$import = function () {
  // for firefox only
  var _SYS_TAG =
  var _MOZ_TAG =
  var _CHARSET =

  // 使遠程擷取的指令碼
  var toCurrentCharset = ...

  // 通過檢測當前的網頁字元集,來確定.js檔案使用的編碼
  var _uu = ...

  // 在firefox以及IE的不同版本下取HTTP連線物件(HTTPRequest)
  var getHttpConnect = ...

  // 在$import()中使用的一個唯一的http connect
  // (指令碼執行是有先後的,所以沒有必要使用非同步HTTP串連)
  var _http = ...

  // 是否使用XMLHTTP來取指令碼代碼。如果為false,則使用<script>標籤載入
  var _xml = ...

  // 通過_http串連取代碼,並轉換編碼的函數
  function httpGet ...

  // 取當前正在啟動並執行指令碼URL
  // (例如system.js的URL,實現使用檔案相對路徑來$import()的特性)
  function activeJS ...

  // 重要的、在後期可能使用或“重述”的系統資訊
  var _sys = ...

  // 一個activeJS的棧,用於實現“在$import()的代碼中再調用$import()”的特性
  var _stack = ...

  // 遠程讀取、裝載指定src的指令碼並執行
  var _load_and_execute = ...

  // 向外返回的函數
  function _import(src) {
    /* Qomo Core System.. */
    _load_and_execute( src );
  }

  // 其它代碼(參見後文中“_sys對象的價值”)
  // ...

  return _import;
}
----------

下面我們逐一講述其中的主要功能:

 1. 網頁字元集、unicode及其解碼
 ~~~~~~
 在ajax系統中,一個很重要的問題就是編解碼的問題。因為不管是Microsoft的XMLHTTP控制項,
還是firefox中的XMLHttpRequest對象,都將遠程擷取的內容預設識別為Unicode編碼。XMLHTTP
控制項預設通過遠程內容中的前置字元來識別Uniocde的編碼方式。因此在不特別指明的情況下,
XMLHTTP可以正確的解析以下編碼方式的遠程內容:
----------
  var _uu = _CHARSET in {
   'utf-8': null,
   'unicode': null,
   'utf-16': null,
   'UnicodeFFFE': null,
   'utf-32': null,
   'utf-32BE': null
  };
----------
如果XMLHTTP不能通過前置字元來解析編碼,那麼它就預設遠程內容使用了UTF-8的編碼格式。

然而這隻是說“遠程內容”的格式(例如.js檔案使用的編碼儲存格式)。大多數情況下,我們會
在網頁中用如下標籤來描述“當前網頁”的編碼:
----------
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
----------
在沒有這個HTML標籤描述的情況下,IE會使用當前的預設設定來給網頁解碼並顯示。這種情況
下,document.charset將會置為“_autodetect_all”,或者你在IE菜單“查看->編碼”中選
擇的字元集。在charset=="_autodetect_all"時,可以通過存取document.defaultCharset來
得到解碼時選擇的字元集。

我們看到一個問題:“當前網頁”解碼與“遠程內容”解碼所依賴的字元集設定並不一致。

事實上,麻煩不僅於此。在JScript引擎中理解的字串等內容,使用的也將會是unicode字
符集。這一點,無論.js檔案編碼格式是什麼,或者網頁編碼格式是什麼,都不會被改變。
也就是說,在一個charsett=gb2312,且.js檔案使用gb2312編碼的系統中,你使用escape()
或unescape()都將會在一個unicode環境中進行字串編解碼。更有甚者,你即使強行指定了
一個字串的解碼方式,它最終顯示在網頁上的時候,也不會如你所願。例如:
----------
// 字串"這是一個測試"的gb2312位元組碼
var s1 = '%D5%E2%CA%C7%D2%BB%B8%F6%B2%E2%CA%D4';

// 解碼
var s2 = unescape(s1);

// 顯示
document.writeln(s2);
----------
這段代碼在utf-8或gb2312字元集的網頁上顯示都不正常。

在Qomo中$import()函數的解碼基於一個假設:“.js檔案的‘遠程內容’與‘當前網頁’
必然使用相同的字元集”。——必須說明的是,這是在一個封閉環境中的理想情況。如果
你試圖用$import()讀取RSS的內容,你可能會必須面臨“在gb2313網頁中去處理utf-8編
碼的RSS資料”這樣的問題。因而你應該清楚:核心一級的$import()主要用於處理Qomo系
統(及擴充功能)的模組載入,其它的“遠程內容”應該交由更複雜的ajax系統去做。

因此Qomo認為:遠程跟當前網頁採用相同編碼,因此在網頁字元集為unicode的情況下,
遠程內容不需要解碼,否則應當從XMLHTTP所(錯誤)理解的unicode轉換為當前字元集。這
個轉換依賴於當前網頁字串的設定,也就是$import()內部的_CHARSET變數的值。

Qomo在$import()中實現瞭解碼函數:toCurrentCharset()。解碼函數只實現了對gb2312
字元集的處理,如果需要其它(非unicode)的解碼,則需要修改toCurrentCharset()中的
部分代碼。

Qomo的解碼函數最初實現gb2312位元組碼的處理時,借鑒網上流傳很廣泛的一個bytes2BSTR()
函數,實現了改良版本的vbs_JoinBytes()。在vbs_JoinBytes()函數中減少了字串串連
和長度識別的次數,使效率大為提高。但在最終實現這個功能時,借鑒Hutia、bjhaoyun
在“經典論壇”中公布的、使用unescape()/escape()函數來處理編碼字串的技巧。由於
大量最佳化了代碼,新的toCurrentCharset()比bjhaoyun提供的代碼有30%左右的效能提升。

這幾種解碼方案中,toCurrentCharset()與bjhaoyun的reCode()採用相同的方案,但整體
效能提升30%。在通常情況下,toCurrentCharset()比vbs_JoinBytes()快3倍以上;在以
英文內容為主的情況下,可以快近10倍;但在中文字元量非常多(例如全中文文本)的情
況下,vbs_JoinBytes()的效能表現會極佳,甚至會比toCurrentCharset()快50%。

由於$import()主要處理的主體內容是英文代碼.js指令碼,因此選用了toCurrentCharset()
作為內建的解碼函數。關於其它幾個解碼函數,可以參見測試網頁T_DecodeUnicode.html。

(目前,)Qomo沒有為firefox中使用XMLHttpRequest對象載入的內容提供解碼函數。

 2. XMLHTTP載入與<script>標籤載入的區別
 ~~~~~~
 如果XMLHTTP對象不能建立,或者無法正常處理編碼。Qomo中提供了後備方案,也就是使
用<script>標籤來載入模組及其它遠程內容。

然而這兩者原本是不能完全替代的,因此有一些差異之處必須補充說明。

首先XMLHTTP載入的內容存放在XMLHTTP對象(例如Qomo中的_http)的responseBody屬性中,
這是一個以Byte為基礎類型的SafeArray數組,而JScript只能處理以Variant為基礎類型
的SafeArray。所以Qomo中調用VBScript的CStr()來使它變成字串,然後進一步地交由
toCurrentCharset()處理。

——然而如果使用<script>標籤來載入,那麼這整個的解碼過程就不需要了。因為<script>
可以指定charset屬性,也可以直接使用“與當前網頁相同”字元集的指令檔。

如果僅這樣看,<script>會比XMLHTTP好。但事實上XMLHTTP具備的另一項優勢讓<script>
望塵莫及。

使用非同步方式,XMLHTTP載入的內容可以被立即執行。因此在這個例子中:
----------
<script>
$import('1.js');
$import('2.js');

foo_in_js1();
</script>
----------
前兩行的1.js和2.js被立即載入並執行了,因此在1.js檔案中的foo_in_js1()可以得到
執行。而在下面的例子中:
----------
<script>
document.writeln('<script src="1.js"><', '/script>');
document.writeln('<script src="2.js"><', '/script>');

foo_in_js1();
</script>
----------
document.writeln()向網頁寫入的內容會出現在</script>標籤之後。因此,1.js和
2.js會在當前的指令碼塊被全部執行完之後,才被載入、解析並執行。——這也意味著
函數foo_in_js1()調用不會成功。

很顯然,我們在一個大的架構系統中,會利用下面這樣的代碼來說明當前模組(或單
元)的依賴性:
----------
$import('/OS/Win32/FileSystem/*');
$import('/OS/Win32/UI/*');

// some code ...
----------
這種情況下在“some code”執行前FileSystem和UI模組就應該是被載入、執行過的。
而我們已經看到<script>並不支援這種特性。

因此Qomo核心中使用<script>來替代$import()僅僅是權益之計,它不能完成$import()
的全部工作。——但是在一些簡化的、小型的、經過定製Qomo系統中,他仍舊是可用
的。只不過要注意XMLHTTP與<script>之間的這種差異,以及這種差異帶來的負面影響。

 3. execScript()與eval()的不同表現
 ~~~~~~
 JScript中有window.execScript()方法,但JavaScript規範中卻沒有它。因此firefox
並沒有實現一個execScript。另一個與之相近的是Global.eval()方法。

在IE的JScript中,eval()執行一個字串並返回結果。在執行時,使用的是調用函數的
上下文環境。因此如果函數A中調用了eval(Str),那麼字串Str中的指令碼代碼可以使用、
修改和影響函數A中的局部變數。而window.execScript()將直接使用全域上下文環境,
因此,execScript(Str)中的字串Str可以影響全域變數。——也包括聲明全域變數、
函數以及物件建構器。

因此我們在用XMLHTTP來遠程地取得.js檔案的內容之後,我們就可以利用execScript()
來執行它。這種執行與在<script>標籤中的執行效果是一致的。

從JavaScript的約定來說,Global.eval()不具有在全域的上下文環境中執行的能力。在
做一個偶然的代碼測試時,我發現firefox中的eval()存在一個奇怪的特性:
  - 如果在函數中使用window.eval()來執行,則使用全域上下文環境;
  - 如果使用eval()來執行,則使用當前函數的上下文環境。

我不確知這是FireFox為ajax而提供的語言特性呢,還是它一個JavaScript實現上的BUG。
但我測試過的幾個版本都呈現這種效果。因此$import()中我使用了window.eval來替代
window.execScript(),以實現firefox版本的Qomo核心。

關於這個特性請參見測試網頁T_eval.html。

 4. 載入路徑與activeJS的關係
 ~~~~~~
在Qomo系統中載入一個.js時,採用比較靈活的路徑定位策略:
  - 如果src以"xxxx://"的形式開始,則使用“完整路徑”定位;
  - 如果src以"/"開始,則使用以當前document所在網站的根路徑開始的絕對路徑;
  - 否則使用相對路徑定位。

但接下來的問題就比較麻煩了。如果網頁的URL是“http://site/sub/a.html”,而
Qomo系統被部署在"http://site/Qomo/"路徑上,則可用如下兩種方式之一在a.html
中載入system.js:
----------
<script src="../Framework/system.js"></script>      <!-- 之一 -->
<script src="/Qomo/Framework/system.js"></script>   <!-- 之二 -->
----------

因為XMLHTTP也使用當前網頁來做相對定位,因此在這方面他與<script>對象相同。
那麼顯然我們在system.js中匯入NameSystem.js應該使用下面這樣的代碼:
----------
// $import()使用XMLHTTP來實現
$import('../Framework/Names/NameSystem.js');     // 方法一
$import('/Qomo/Framework/Names/NameSystem.js');  // 方法二
----------

各個.js中都要使用當前網頁來做相對定位,這導致了系統中的指令碼很不靈活,編寫
代碼時要留意其它.js所在位置,目錄轉移時也不方便。——當然你也可以使用"/"開
始的絕對位置,但如果這樣,Qomo在應用系統中的可部署性就很差了。

因此Qomo對載入路徑的理解是基於“當前.js檔案”的。這樣一來,在system.js中
就可以這樣來匯入NameSystem.js:
----------
// system.js位於/Qomo/Framework/路徑上
$import('Names/NameSystem.js');
----------
而在NameSystem.js中如果要匯入同樣位於Names目錄中的A.js檔案,則只需要:
----------
$import('A.js');
----------

為此,Qomo在$import()實現了一個_stack陣列變數。它被當做一個後入先出隊列,
以保證curScript中總是存在當前正在執行的.js檔案的URL。這樣$import()就可以
據此來計算“正在執行的.js中新匯入的.js的相對路徑”。

麻煩並沒有解除。因為Qomo對系統的理解是“可拆解”的,因此它會允許下面這樣
的代碼:
----------
<script src="../Qomo/Framework/system.js"></script>
<script src="../Qomo/Controls/Components.js"></script>
<script src="../Qomo/DB/DB.js"></script>

<script>
  $import('../Qomo/Common/Tools.js');
</script>
----------
在這樣的一個系統中,system.js、Components.js和DB.js中所理解的“當前路徑”
都不一樣。因此我們必須有辦法來知道$import()當前正在哪一個.js檔案中執行,
並取得它的src。

函數activeJS()用於取得由<script>匯入的.js檔案的URL,然後在.js中就可以使
用相對路徑來載入其它.js檔案了。

在IE中有機會取到“當前.js的路徑”。在使用中我發現<script>對象的readyState
屬性可以協助我們來實現這個需求。簡單的說,.js檔案執行時,script.readyState
的值將會是"interactive"。如果我們列舉所有的<script>標籤,則可以找到這個
script對象,從而得到src的值。

activeJS()利用這個特性來實現。但firefox中DOM的Script對象不具有readyState
屬性,因此firefox部分的代碼採用了識別檔案名稱"system.js"的方法來實現。——
但需要注意的是,我沒有辦法為Qomo在firefox中提供與IE一樣的特性。它們之間的
差異表現在:
  - (除非在system.js中,)$import()在firefox中不能使用相對路徑的特性
  - 如果修改system.js的檔案名稱,則firefox在system.js也不能使用相對路徑

最後,在一個普通的<script>指令碼塊中使用$import(),它將會以當前的document的
路徑作為計算相對路徑的基礎。這時,activeJS()返回空串。——正好script.src
也是空串。

 4. 為什麼不是命名空間
 ~~~~~~
 Qomo支援但不強制使用命名空間。這是Qomo如此複雜地實現$import()的原因。因
為在一個支援“命名空間註冊”的系統中,可以這樣來做:
----------
// 1. in system.js
Names.registerNamespace('/Framework', 'currentPath');

// 2. in my_script.js
$import('/Framework/Debug/debug.js');
----------

這樣,由於命名空間的存在,系統的確可以快速地反映模組的位置和相互關係。然
而這種通過registerNamespace()手工註冊的形式,導致使用者必須強制使用命名空間。
——儘管這沒有什麼不好,也的確是firefox上可以採用的最佳方式。然而我還是在
Qomo中實現了自註冊的命名空間管理子系統,這使得在大多數情況下,命名空間都可
以通過$import()調用時的路徑關係來自動擷取和計算。

即使不載入命名空間模組,Qomo系統也能正常的運行。這是Qomo的一個特點。儘管這
個特性在firefox中不能被實現,然而我還是為“喜歡快捷輕巧的核心”的開發人員們提
供了一種可能的選擇。

不過關於NameSystem.js的具體實現我們以後再講。今次我們討論的,只是system.js。

 5. _sys對象的價值
 ~~~~~~
 在Qomo的核心中,一部分代碼是可被外部使用的。例如解碼用的toCurrentCharset()
以及用於匯入.js檔案的、非同步XMLHTTP對象。

然而$import()封裝了這些細節。在Qomo對代碼的理解裡面,是“沒有必要,就不要
公布”,這樣儘可能地少佔用一些全域的變數名。

那麼有價值的資源能如何被使用呢?Qomo在$import()函式宣告了對象_sys,:
----------
  var _sys = {
    // 讀取這些內容可以瞭解$import()的運行情況
    'scripts': {},
    'curScript': '',

    // ajax kernal
    'httpGet': httpGet,
    'httpConn': getHttpConnect,

    // decode for XMLHTTP.responseBody
    'bodyDecode': toCurrentCharset,

    // 取當前正在執行中的指令碼的src
    'activeJS' : activeJS

    // ...
  }
----------

然後為$import實現了一些用於存取這個對象的方法:
----------
  _import.get = function(n) {
    return eval('_sys[n]');
  }

  _import.set = function(n, v) {
    return eval('_sys[n] = v');
  }

  _import.OnSysInitialized = function() { ... }
----------

這樣,在Qomo中就可以寫下面這樣的代碼來使用_sys對象了:
----------
var httpGet = $import.get('httpGet');
var str = httpGet('http://www.sina.com.cn/');

alert(str);
----------

而$import.set()的提供,就與我們在最前說到的“功能重述”技術有關了。
因為$import.set()修改的是_sys對象所存放的“內部功能入口”,因此可以
通過調用set()方法來“重新描述”這些功能入口的實現方法。這些能被重述
的內容取決於_sys對象公開了哪些內容。在Qomo中,這些是可以被重述的:
----------
  var _sys = {
    'transitionUrl': function(url){ ... }
    'srcBase': function() { ... }

    // ...
  }
----------

一些人應該已經注意到$import()並沒有實現“匯入包或命名空間”這樣的功能。
而這裡公開這些功能入口,就是使得它們可以被“重述”:如果在NameSystem.js
中重述transitionUrl()和srcBase()的實現,那麼就可以在不修改$import()的
情況下,支援命名空間和包。

然而這些“可重述”的特性仍然是與核心直接相關的。因此$import()還公開了
一個事件OnSysInitialized(),並在system.js的最未尾啟用了這個事件。——
這個事件的響應代碼所做的工作,就是為$import()清除set()/set()等這些方法。

通過$import.get()/set(),我們可以在system.js所匯入的其它.js中為$import()
進行重述,也可以利用$import()已經實現過的特性和代碼。而在system.js載入並
執行完成之後,這些預留給系統核心的功能就隨著OnSysInitialized()的觸發而被
清除了。

system.js模組實現了一個具有張力和包容性的載入架構,為後面實現可裁剪的Qomo
系統提供了充分的基礎。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.