Markup 和 CMarkup 對象

Windows 為了實現瀏覽器功能代碼的復用,將瀏覽器內部 DOM 接口\DHTML接口使用 COM 方式實現,這樣HTML頁面的內容就可以方便的被其他各個模塊所調用,如 瀏覽器的javascript、操作瀏覽器組件的C++等。其主要的實現均存在于 mshtml.dll 中。
其中 Markup 是一系列接口和對象的集合,主要為用戶提供訪問和修改HTML頁面內容的功能。Markup 在IE 瀏覽器中被封裝成一個類,叫做 CMarkup。

Markup

理解 Markup 首先需要理清如下的幾個概念

Tags vs Elements

一個html 標簽在瀏覽器內部的表示形式被稱為 Element,理解 tag 和 element 的概念尤其重要。HTML 頁面內容包含有 tag,如<B></B>、<A></A>等,在使用瀏覽器訪問HTML 頁面時,瀏覽器的 parser 讀到 <B></B>等標簽,并且根據 tag 的不同創建不同的對象。這些可以操作的對象稱為 Element。Markup 所能夠操作的也正是這些對象

舉例來說,有如下頁面

<P>First<P>Second

瀏覽器頁面 parser 解析這些語句時則會變成下面的樣子

<HTML><HEAD><TITLE></TITLE></HEAD><BODY>
<P>First</P><P>Second</P></BODY></HTML>

換而言之,paser 將 HTML 頁面中的內容轉變成了 element,并且添加了一些元素以保證頁面結構的完整性。

另一個需要理解的概念是 stream 和 tree。
以如下頁面舉例

My <B>dog</B> has fleas.

上述頁面會被解析成為如下的樹結構

              ROOT
                |
          +-----+------+
          |     |      |
         "My"   B  "has fleas."
                |
              "dog"

而對于上述文檔的操作看起來就像是在對樹進行操作,比如添加或者刪除葉節點。
然而隨著功能的加強,頁面的內容從 ie4.0 開始變得不再是上圖中那樣簡單的樹結構了。
如下一個例子

Where do <B>you <I>want to</B> go</I> today?

在這個頁面中 <B> 標簽和 <I> 標簽相互嵌套,這樣一來頁面便無法被簡單的表示成為樹結構,此時 markup 便應運而生了。
Markup 將頁面看作是一個 stream。頁面中的內容均由markup pointer 進行索引,對于頁面內容的操作也是按照markup pointer 指定的范圍進行。以上面的頁面為例,操作重疊的tag 時使用兩個markup pointer ,一個指向tag 的開頭另一個指向tag 的結尾,這種方式當然也可以表示之前的樹結構,換句話說 stream 是 tree 的超集

合法的和不合法的頁面

一般的瀏覽器都具有容錯性,就像上面舉過的例子一樣,瀏覽器的 parser 會在解析過程中為頁面添加必要的結構以努力構成一個合法頁面。一個合法頁面至少要包含一個 html、一個head、一個 TITLE 和一個 body。

markup 為用戶提供接口,使用戶可以在頁面解析完成、或者尚未完成時修改頁面內容。

IMarkupServices

MarkupContainer

Container 顧名思義即頁面Element 的容器,也是Markup 操作的容器,MarkupContainer 用于把創建的Element 對象和頁面中的 text 內容聯系起來。在頁面解析完成之后,系統會默認創建一個主 Container,其后每一次頁面內容的操作都需要指定一個 Container,具體的流操作均在這個 Container 上進行。
舉例來說,下面的代碼想要向一個頁面中插入一個元素

int Insert(
    MSHTML::IHTMLDocument2Ptr pDoc2,
    ....)
{
    HRESULT              hr = S_OK;
    //IHTMLDocument2 *   pDoc2;
    IMarkupServices  *   pMS;
    IMarkupContainer *   pMpContainer;
    IMarkupPointer   *   pPtr1, * pPtr2;
    
    pDoc2->QueryInterface( IID_IMarkupContainer, (void **) & pMpContainer);
    pDoc2->QueryInterface( IID_IMarkupServices, (void **) & pMS );

    // need two pointers for marking
    pMS->CreateMarkupPointer( & pPtr1 );
    // beginning and ending position.
    pMS->CreateMarkupPointer( & pPtr2 ); 

    //
    // Set gravity of this pointer so that when the replacement text
    // is inserted it will float to be after it.
    //
    pPtr1->SetGravity( POINTER_GRAVITY_Right ); // Right gravity set
    pPtr2->SetGravity( POINTER_GRAVITY_Left );


    pPtr1->MoveToContainer( pMpContainer, TRUE );
    pPtr2->MoveToContainer( pMpContainer, TRUE );
    
    ......
    Insert()
}

對頁面的插入操作首先需要通過頁面對象獲取對應的 Container 接口,接著使用 markup pointer 遍歷到 Container 中的指定位置,這樣才能執行操作。

MarkupPointer

MarkupPointer 并不是 MarkupContainer 的一部分。MarkupPointer 的主要功能是用來指示tag節點在文檔中的位置。因此 pointer 可以看作是用于在 Container 中進行索引的迭代器。
舉例來說

My <B>d[p1]og</B> has fleas.

在這個頁面中,MarkupPointer 出現在[p1]所示的位置上,但它并不會在頁面內容中添加任何東西,或者對頁面內容進行任何修改。
MarkupPointer 可以被至于頁面的這些位置:element的開始、element的結束、或者text之中。由于MarkupPointer本身不包含內容,因此如果兩個 MarkupPointer 指向了同一個位置便會難以區分。

通過 Markup ,用戶便可以操作頁面中的內容,其主要提供了以下一些功能

放置Markup Pointers

markup pointer 被創建后處于 unpositioned 狀態,表示它還沒有被放置到頁面中的任何位置。微軟提供了三個函數用來為markup pointer 指定位置

  • MoveAdjacentToElement
  • MoveToContainer
  • MoveToPointer

MoveAdjacentToElement函數有兩個參數,Element和一個枚舉類型常量,他們協同指定markup pointer的位置。函數原型如下

HRESULT MoveAdjacentToElement(
    IHTMLElement *elementTarget,
    ELEMENT_ADJACENCY
);

    enum ELEMENT_ADJACENCY {
         ELEMENT_ADJ_BeforeBegin
         ELEMENT_ADJ_AfterBegin
         ELEMENT_ADJ_BeforeEnd
         ELEMENT_ADJ_AfterEnd
    };

MoveToContainer函數也有兩個參數,MarkupContainer 和一個Bool 類型用以指定 markup pointer 應該放在 container 的開始還是結尾。函數原型如下

HRESULT MoveToContainer(
    IMarkupContainer *containerTarget,
    BOOL fAtStart
);

MoveToPointer函數只有一個參數,另一個markup pointer。函數功能即把當前 pointer 指定到參數 pointer 的位置。函數原型如下

HRESULT MoveToPointer(
    IMarkupPointer *pointerTarget
);

這個函數一般用于在markup pointer執行功能的時候,保存當前的位置

比較pointer 的位置

兩個 markup pointer 的位置關系可以使用下面的函數進行比較


HRESULT IsEqualTo(
    IMarkupPointer *compareTo,
    BOOL *fResult
);

HRESULT IsLeftOf(
    IMarkupPointer *compareTo,
    BOOL *fResult
);

HRESULT IsLeftOfOrEqualTo(
    IMarkupPointer *compareTo,
    BOOL *fResult
);

HRESULT IsRightOf(
    IMarkupPointer *compareTo,
    BOOL *fResult
);

HRESULT IsRightOfOrEqualTo(
    IMarkupPointer *compareTo,
    BOOL *fResult
);
Navigating the Pointer

一旦一個 markup pointer 被放置在一個 markup containter 中。用戶便可以使用這個 pointer 來檢查周圍的頁面內容,或者遍歷這塊內容。用戶只能使用windows 提供的兩個函數完成這些功能,Left檢查pointer 的左邊是什么,Right 檢查pointer 的右邊是什么

HRESULT Left(
    BOOL fMove,
    MARKUP_CONTEXT_TYPE pContextType,
    IHTMLElement **ppElement,
    long *plCch,
    OLE_CHAR *pch
);

HRESULT Right(
    BOOL fMove,
    MARKUP_CONTEXT_TYPE pContextType,
    IHTMLElement **ppElement,
    long *plCch,
    OLE_CHAR *pch
);
  • 第一個參數指定指針是否可移動,若不可移動,則函數僅僅會返回指針周圍內容的描述;否則,函數在返回周圍內容描述的同時還會移動過去。
  • 第二個參數為返回值,返回pointer周圍的內容類型。
Value Are Example
CONTEXT_TYPE_None pointer左邊或者右邊沒有東西 [p1]<HTML></HTML>[p2]
CONTEXT_TYPE_Text pointer左邊或者右邊是一個text tex[p]t
CONTEXT_TYPE_EnterScope 如果是Left,則point左邊是一個End tag;如果是Right,pointer的右邊是一個Begin tag 。 </B>[p]<B>
CONTEXT_TYPE_ExitScope 如果是Left,則point左邊是一個Begin tag;如果是Right,pointer的右邊是一個End tag 。 <B>[p]</B>
CONTEXT_TYPE_NoScope pointer的左邊或者右邊不是一個可以成對的標簽 <BR>[p]<BR>
  • 第三個參數返回 pointer 左邊或者右邊的element
  • 第四個參數用來限定讀取的text范圍,同時也用來返回獲取的text 的大小
  • 第五個參數返回pointer 左邊或者右邊的 text

下面以具體的頁面舉例說明

[p1]Where [p2]<I>do </I>[p3]<B>you <BR>[p4]want</B> to go today[p5]?

對于頁面上的五個pointer 分別調用left,right結果如下表

Ptr Derection Type Element cch in cch out Text
p1 left None - - - -
p1 right Text - 2 2 Wh
p1 right Text - -1 6 -
p1 right Text - 345 6 Where
p2 left Text - NULL - -
p2 right EnterScope I - - -
p3 left ExitScope I - - -
p4 left NoScope BR - - -
p5 left Text I 100 12 NULL

CurrentScope函數可以得到Pointer 當前指向的Element。函數原型如下

HRESULT CurrentScope(
    IHTMLElement **ppElementCurrent
);

上述例子中,p1返回值是 NULL;p4返回值是B,因為BR不是一個可以成對的標簽

Pointer Gravity

通常情況下,一個 document 被修改之后,document 中的markup Pointer還會保留在之前未修改時的位置。
舉例來說

abc[p1]defg[p2]hij
abc[p1]deXYZfg[p2]hij

當第一個頁面被修改為第二個頁面之后,雖然頁面的內容發生了改變,但是pointer 的相對位置仍然保持不變。
但如果頁面的修改發生在 point 指向的位置,如上例中,向c、d之間插入一個Z,p 的位置就會出現二義性。

abcZ[p1]de  or  abc[p1]Zde

這時就需要引用另一個重要的概念gravity,每一個pointer都有一個 gravity 值標識著其左偏或右偏。仍以上述頁面為例

abc[p1,right]defg[p2,left]hij 

分別在p1,p2的位置插入一對<B>標簽。這時由于gravity的存在,頁面會變成如下

abc<B>[p1,right]defg[p2,left]</B>hij 

默認情況下 pointer 的gravity 值是 left。用戶可以通過 windows 提供的函數來查看或者修改 pointer 的 gravity 值

enum POINTER_GRAVITY {
    POINTER_GRAVITY_Left,
    POINTER_GRAVITY_Right
};

HRESULT Gravity(
    POINTER_GRAVITY *pGravityOut
);

HRESULT SetGravity(
    POINTER_GRAVITY newGravity
);
Pointer Cling

有如下例子

[p2]ab[p1]cdxy

當bc 段被移動到 xy之間時p1的位置也出現了二義性,是應該隨著bc移動,還是應該繼續保持在原位呢

[p2]a[p1]dxbcy or [p2]adxb[p1]cy

這就需要 cling 的存在,如果p1指定了cling 屬性,那么頁面操作之后就會成為右邊所示的情況,否則就會出現左邊所示的情況

cling 和 gravity 可以協同作用,比如下面的例子

a[p1]bcxy

b移動到x、y之間,如果p1指定了 cling屬性,并且gravity 值為 right,那么p1便會跟隨b一起到xy之間。這種情況下如果b被刪除,那么p1也會跟著從content 中移除,但并不會銷毀,因為p1還有可能重新被使用
cling相關的函數,函數原型如下

HRESULT Cling(
    BOOL *pClingOut
);

HRESULT SetCling(
    BOOL NewCling
);
創建新Element

動態創建新節點的操作也是通過 markup 來完成的,CreateElement 函數原型如下

enum ELEMENT_TAG_ID {
    TAGTADID_A,
    TAGTADID_ACRONYM,
        ..
    TAGTADID_WBR,
    TAGTADID_XMP
};

HRESULT CreateElement(
    TAG_ID tagID,
    OLECHAR *pchAttrs,
    IHTMLElement **ppNewElement
);

第二個參數是屬性串,可以在 Element創建時就加入屬性。
用戶也可以通過從一個已有 element 克隆,來得到新的 element

插入新 Element

新 element 成功創建之后,如果想加入document 中,還需要通過markup 將element插入。 函數原型如下

HRESULT InsertElement(
    IHTMLElement *pElementInsertThis,
    IMarkupPointer *pPointerStart,
    IMarkupPointer *pPointerFinish
);

第二參數指示這個element 的begin tag 插入到哪里;第三個參數指示這個 element 的end tag應該插入到哪里;這兩個位置必須在同一個 markup Container 中。
舉例來說,調用函數將 <B> 標簽插入下面的頁面中

My [pstart]dog[pend] has fleas.

默認情況下結果將如下面所示,如果 pointer 的 gravity 改變,情況也會改變

My [pstart]<B>dog[pend]</B> has fleas.
移除Element

移除 element 并不需要markup pointer ,只需要傳遞給函數要刪除的 element 就可以。函數原型如下

HRESULT RemoveElement(
    IHTMLElement *pElementRemoveThis
);

element 被從 document 中移除之后并不會被刪除,他隨時可以被重新插入

插入 Text

在 document 中插入 text ,函數原型如下

HRESULT InsertText(
    OLECHAR *pch,
    long cch,
    IMarkupPointer *pPointerTarget
);

注意到,插入text 只需要一個 markup pointer 來指定位置

移除內容

用戶可以移除在同一個container 中一段連續的內容,函數原型如下

HRESULT Remove(
    IMarkupPointer *pPointerSourceStart,
    IMarkupPointer *pPointerSourceFinish
);

兩個參數用來指定remove操作的范圍,所有在這兩個點之間的內容都會被移除。但是有一點例外,即兩個 pointer 沒有完全包含的 element 不會被移除。舉例來說

     <--------- i -----------> <---------- u ----------->
    a<I>b<B>c[pstart]d<S>e</I>f<U>g</S>h[pend]hi</B>j</U>kl
                      <----- s ------->         

remove 操作傳入 pstart、pend 兩個參數,結果頁面被修改為下面的情況

 <------- i --------><------- u -------->
a<I>b<B>c[pstart]</I><U>[pend]hi</B>j</U>kl

<U> 和 <I> 并未被移除。

替換內容

插入和移除操作何以合成 Replace 操作

int MarkupSvc::RemoveNReplace(
    MSHTML::IHTMLDocument2Ptr pDoc2,
    _bstr_t bstrinputfrom, _bstr_t bstrinputto)
{
    HRESULT              hr = S_OK;
    //IHTMLDocument2 *   pDoc2;
    IMarkupServices  *   pMS;
    IMarkupContainer *   pMarkup;
    IMarkupPointer   *   pPtr1, * pPtr2;
    TCHAR            *   pstrFrom = _T( bstrinputfrom );
    TCHAR            *   pstrTo = _T( bstrinputto );
    
    pDoc2->QueryInterface( IID_IMarkupContainer, (void **) & pMarkup );
    pDoc2->QueryInterface( IID_IMarkupServices, (void **) & pMS );

    // need two pointers for marking
    pMS->CreateMarkupPointer( & pPtr1 );
    // beginning and ending position of text.
    pMS->CreateMarkupPointer( & pPtr2 ); 

    //
    // Set gravity of this pointer so that when the replacement text
    // is inserted it will float to be after it.
    //
    pPtr1->SetGravity( POINTER_GRAVITY_Right ); // Right gravity set

    //
    // Start the search at the beginning of the primary container
    //

    pPtr1->MoveToContainer( pMarkup, TRUE );

    for ( ; ; )
    {
        hr = pPtr1->FindText( (unsigned short *) pstrFrom, 0, pPtr2, NULL );

        if (hr == S_FALSE) // did not find the text
            break;

        // found it, removing..
        pMS->Remove( pPtr1, pPtr2 );
        
        //inserting new text
        pMS->InsertText( (unsigned short *) pstrTo, -1, pPtr1 );
    }
    if (hr == S_FALSE) return FALSE;
    else return(TRUE);
}
移動內容

用戶可以使用 Move 移動一段頁面內容,函數原型如下

HRESULT Move(
    IMarkupPointer *pPointerSourceStart,
    IMarkupPointer *pPointerSourceFinish,
    IMarkupPointer *pPointerTarget
);

函數前兩個參數和 remove 類似,函數會將這一整段內容移動到目的 pointer 中。那些與pointer 范圍有重疊的 element,即并不完全包含在 pointers 之間的 element 會在目的處創建一個拷貝。
舉例來說


X[pdest]Y

 <--------- i -----------> <---------- u ----------->
a<I>b<B>c[pstart]d<S>e</I>f<U>g</S>h[pend]hi</B>j</U>kl
                  <----- s ------->      

操作之后頁面變成

X[pdest]<I'>d<S>e</I'>f<U'>g</S>h</U'>Y

 <------- i --------><------- u -------->
a<I>b<B>c[pstart]</I><U>[pend]hi</B>j</U>kl

可以看到完全包含在pointers 中的<S>標簽被移動到dest 位置,而與 pointers 區域重疊的 <U>、<I>標簽在目標位置創建一個備份。

以上內容翻譯自微軟提供的官方 Markup Serivce 文檔。

CMarkup

CMarkup 其本質上是對Markup Service 的封裝,在 IE/EDGE 中方便 js 引擎在操作頁面時調用。簡單來說 CMarkup 可以看作是 Markup Service 中的 MarkupContainer。以下是 IE8 中 CMarkup 的部分結構,可以看出其關聯了與頁面相關的許多重要的元素。不僅如此所有的頁面元素都保存一個指向 CMarkup 的指針,在對頁面元素進行訪問時,均需要通過 CMarkup 來進行。

Class CMarkup{
   +0xA0    WindowedMarkupContext
   +0x40    CDocument
   +0x108  COmWindowProxy
   +0x50   CHtmlCtx
   +0x54   CProgSink
   +0x5C  CSecurityContext
   +0x8c   CAPStatr
   +0xc0   CSecurityContext
   +0xc8   CStyleSheetArray
   +0xcc   TagArray
   +0xd0   ComWindowProxy
   +0xdc  obj_name_space
   +0xf4   CHtmlElemeCtxStream
   +0x124  uri
   +0x158  CTimeManager
   +0x16c  CMSPerformanceData
   +0x140  CTreePos
}

以 DOM 節點固有屬性 nextSibling 舉例,該屬性用于返回其父節點的 childNodes 列表中緊跟在其后的節點。通過 js 訪問節點的該屬性,IE 8 內部使用 CElement::get_nextSibling 函數來實現,對該函數進行逆向后部分代碼如下。

HRESULT CElement::GetNextSiblingHelper(CElement *this, CElement **nextSibling)
{
  CMarkupPointer * markupPointer;
  CDoc* cDoc;
  HRESULT result;

  cDoc = CElement::Doc(this);
  CMarkupPointer::CMarkupPointer(markupPointer, cDoc);   // 創建 MarkupPointer
  
  result = markupPointer->MoveAdjacentToElement( this, ELEMENT_ADJ_AfterEnd);    // 放置 MarkupPointer
    if ( result == S_OK )
    {
      cDoc = CElement::Doc(this);
      result = sub_74D4A0B3(cDoc , markupPointer, &nextSibling);       // 通過 MarkupPoint 獲取 Element
    }

  result = CBase::SetErrorInfo(markupPointer, result);
  CMarkupPointer::~CMarkupPointer(markupPointer);
  return result;
}

函數的主要邏輯即,首先新建一個 MarkupPointer 對象,接著將該 MarkupPointer 放置于目標節點的 ELEMENT_ADJ_AfterEnd 位置,而后通過該 MarkupPointer 來檢查周圍的內容,這里使用的函數其實是 CMarkupPointer::There ,其函數為 Left() 和 Right() 的合并。

同樣的 previousSiblingfirstChildlastChild 的內部實現流程也類似,通過 CMarkupPointer::MoveAdjacentToElement 將 CMarkupPointer 放置在節點對象的不同位置,再通過 CMarkupPointer::There 取出對應的節點信息即可。

childNodes 節點屬性則是通過 CMarkupPointer 遍歷對應 Element 節點而實現,在 IE 8 中其主要的功能函數為 CElement::DOMEnumerateChildren ,該函數逆向后主要功能代碼如下

CElement::DOMEnumerateChildren(CElement children[])
{
     cDoc = CElement::Doc(this);
     CMarkupPointer::CMarkupPointer(markupPtrBegin, cDoc);   
     CMarkupPointer::CMarkupPointer(markupPtrEnd, cDoc);    
     ......
     result = markupPtrBegin->MoveAdjacentToElement( this, ELEMENT_ADJ_AfterBegin);    // 放置 MarkupPointer
     result = markupPtrEnd->MoveAdjacentToElement( this, ELEMENT_ADJ_AfterEnd);    // 放置 MarkupPointer
     do{
        ......
        child = markupPointer->There()
        children[i++] = child;
        result = markupPtrBegin->MoveAdjacentToElement( child, ELEMENT_ADJ_AfterBegin);    // 放置 MarkupPointer
        ......
        }while( !markupPtrBegin->isLeftOf(markupPtrEnd) )
    ......
}

通過兩個 CMarkupPointer 指針分別指向 Element 的開始和結尾,從 Element 的開始位置依次遍歷 ,其間所有的節點均為 Element 的子節點。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容

  • [TOC] 一、DOM 流簡介 DOM (Document Object Model)作為現代瀏覽器的基礎,其設計...
    o_0xF2B8F2B8閱讀 1,054評論 0 0
  • 課程介紹 先修課:概率統計,程序設計實習,集合論與圖論 后續課:算法分析與設計,編譯原理,操作系統,數據庫概論,人...
    ShellyWhen閱讀 2,329評論 0 3
  • 問答題47 /72 常見瀏覽器兼容性問題與解決方案? 參考答案 (1)瀏覽器兼容問題一:不同瀏覽器的標簽默認的外補...
    _Yfling閱讀 13,776評論 1 92
  • 1.幾種基本數據類型?復雜數據類型?值類型和引用數據類型?堆棧數據結構? 基本數據類型:Undefined、Nul...
    極樂君閱讀 5,555評論 0 106
  • 是什么時候發現原來自己是個沒有秘密的人? 小學的時候喜歡ck先生,好像很多人知道吧?有一次去檢查眼保...
    張笑憂閱讀 332評論 0 1