<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          談一談組件化

          共 49507字,需瀏覽 100分鐘

           ·

          2022-03-04 01:33

          作者:記得要微笑

          來源:SegmentFault  思否社區(qū) 


          前言



          今天前端生態(tài)里面,React、AngularVue三分天下。雖然這三個框架的定位各有不同,但是它們有一個核心的共同點(diǎn),那就是提供了組件化的能力。W3C也有Web Component的相關(guān)草案,也是為了提供組件化能力。今天我們就來聊聊組件化是什么,以及它為什么這么重要。


          正文



          其實(shí)組件化思想是一種前端技術(shù)非常自然的延伸,如果你使用過HTML,相信你一定有過“我要是能定義一個標(biāo)簽就好了”這樣的想法。HTML雖然提供了一百多個標(biāo)簽,但是它們都只能實(shí)現(xiàn)一些非常初級的功能。


          HTML本身的目標(biāo),是標(biāo)準(zhǔn)化的語義,既然是標(biāo)準(zhǔn)化,跟自定義標(biāo)簽名就有一定的沖突。所以從前端最早出現(xiàn)的2005年,到現(xiàn)在2022年,我們一直沒有等到自定義標(biāo)簽這個功能,至今仍然是Draft狀態(tài)。


          但是,前端組件化的需求一直都存在,歷史長流中工程師們提出過很多組件化的解決方案。


          ExtJS


          Ext JS是一個流行的JavaScript框架,它為使用跨瀏覽器功能構(gòu)建Web應(yīng)用程序提供了豐富的UI。我們來看看它的組件定義:


          MainPanel = function() {
            this.preview = new Ext.Panel({
              id: "preview",
              region: "south"
              // ...
            });
            MainPanel.superclass.constructor.call(this, {
              id: "main-tabs",
              activeTab: 0,
              region: "center"
              // ...
            });

            this.gsm = this.grid.getSelectionModel();

            this.gsm.on(
              "rowselect"function(sm, index, record) {
                // ...
              }, this, { buffer: 250 }
            );

            this.grid.store.on("beforeload", this.preview.clear, this.preview);
            this.grid.store.on("load", this.gsm.selectFirstRow, this.gsm);

            this.grid.on("rowdbclick", this.openTab, this);
          };

          Ext.extend(MainPanel, Ext.TabPanel, {
            loadFeed: function(feed) {
              // ...
            },
            // ...
            movePreview: function(m, pressed) {
              // ...
            }
          });


          你可以看到ExtJS將組件設(shè)計成一個函數(shù)容器,接受組件配置參數(shù)optionsappend到指定DOM上。這是一個完全使用JS來實(shí)現(xiàn)組件的體系,它定義了嚴(yán)格的繼承關(guān)系,以及初始化、渲染、銷毀的生命周期,這樣的方案很好地支撐了ExtJS的前端架構(gòu)。


          https://www.w3cschool.cn/extjs/extjs_overview.html


          HTML Component


          搞前端時間比較長的同學(xué)都會知道一個東西,那就是HTC(HTML Components),這個東西名字很現(xiàn)在流行的Web Components很像,但卻是不同的兩個東西,它們的思路有很多相似點(diǎn),但是前者已是昨日黃花,后者方興未艾,是什么造成了它們的這種差距呢?


          因?yàn)橹髁鳛g覽器里面只有IE支持過HTC,所以很多人潛意識都認(rèn)為它不標(biāo)準(zhǔn),但其實(shí)它也是有標(biāo)準(zhǔn)文檔的,而且到現(xiàn)在還有鏈接,注意它的時間!


          https://www.w3.org/TR/NOTE-HTMLComponents


          MSDN onlineHTC的定義僅如下幾句:


          HTML Components (HTCs) provide a mechanism to implement components in script as Dynamic HTML (DHTML) behaviors. Saved with an .htc extension, an HTC is an HTML file that contains script and a set of HTC-specific elements that define the component.
          (
          HTC是由HTML標(biāo)記、特殊標(biāo)記和腳本組成的定義了DHTML特性的組件.)


          作為組件,它也有屬性、方法、事件,下面簡要說明其定義方式:


          • <PUBLIC:COMPONENT></PUBLIC:COMPONENT>:定義HTC,這個標(biāo)簽是其他定義的父元素。

          • <PUBLIC:PROPERTY NAME=”pName” GET=”getMethod” PUT=”putMethod” />:定義HTC的屬性,里面三個定義分別代表屬性名、讀取屬性、設(shè)置屬性時HTC所調(diào)用的方法。

          • <PUBLIC:METHOD NAME=”mName” />:定義HTC的方法,NAME定義了方法名。

          • <PUBLIC:EVENT NAME=”eName” ID=”eId” />:定義了HTC的事件,NAME定義了事件名,ID是個可選屬性,在HTC中唯一標(biāo)識這個事件。

          • <PUBLID:ATTACH EVENT=”sEvent” ONEVENT=”doEvent” />:定義了瀏覽器傳給HTC事件的相應(yīng)方法,其中EVENT是瀏覽器傳入的事件,ONEVENT是處理事件的方法。


          我們來看看它主要能做什么呢?

          它可以以兩種方式被引入到HTML頁面中,一種是作為“行為”被附加到元素,使用CSS引入,一種是作為“組件”,擴(kuò)展HTML的標(biāo)簽體系。

          行為為腳本封裝和代碼重用提供了一種手段


          通過行為,可以輕松地將交互效果添加為可跨多個頁面重用的封裝組件。例如,考慮在 Internet Explorer 4.0 中實(shí)現(xiàn)onmouseover highlight的效果,通過使用 CSS 規(guī)則,以及動態(tài)更改樣式的能力,很容易在頁面上實(shí)現(xiàn)這種效果。

          在 Internet Explorer 4.0 中,實(shí)現(xiàn)在列表元素li上實(shí)現(xiàn) onmouseover 高亮可以使用onmouseoveronmouseout事件動態(tài)更改li元素樣式:

          <HEAD>
          <STYLE>
          .HILITE
          { color:red;letter-spacing:2; }
          </STYLE>
          </HEAD>

          <BODY>
          <UL>
          <LI onmouseover="this.className='HILITE'"
              onmouseout ="this.className=''">HTML Authoring</LI>
          </UL>
          </BODY>

          從 Internet Explorer 5 開始,可以通過 DHTML 行為來實(shí)現(xiàn)此效果。當(dāng)將DHTML行為應(yīng)用于li元素時,此行為擴(kuò)展了列表項(xiàng)的默認(rèn)行為,在用戶將鼠標(biāo)移到其上時更改其顏色。

          下面的示例以 HTML 組件 (HTC) 文件的形式實(shí)現(xiàn)一個行為,該文件包含在hilite.htc文件中,以實(shí)現(xiàn)鼠標(biāo)懸停高亮效果。使用 CSS 行為屬性將行為應(yīng)用到元素li 上。上述代碼在 Internet Explorer 5 及更高版本中可能如下所示:

          // hilite.htc
          <HTML xmlns:PUBLIC="urn:HTMLComponent">
          // <ATTACH> 元素定義了瀏覽器傳給HTC事件的相應(yīng)方法,其中EVENT是瀏覽器傳入的事件,ONEVENT是處理事件的方法
          <PUBLIC:ATTACH EVENT="onmouseover" ONEVENT="Hilite()" />
          <PUBLIC:ATTACH EVENT="onmouseout"  ONEVENT="Restore()" />
          <SCRIPT LANGUAGE="JScript">
          var normalColor;

          function Hilite()
          {
             if (event.srcElement == element)
             {
               normalColor = style.color;
               runtimeStyle.color  = "red";
               runtimeStyle.cursor = "hand";
             }
          }

          function Restore()
          {
             if (event.srcElement == element)
             {
                runtimeStyle.color  = normalColor;
                runtimeStyle.cursor = "";
             }
          }
          </SCRIPT>

          通過CSS behavior屬性將DHTML行為附加到頁面上的元素

          <HEAD>
          <STYLE>
             LI {behavior:url(hilite.htc)}
          </STYLE>
          </HEAD>

          <BODY>
          <UL>
            <LI>HTML Authoring</LI>
          </UL>
          </BODY>

          HTC自定義標(biāo)記


          我們經(jīng)常看到某些網(wǎng)頁上有這樣的效果:用戶點(diǎn)擊一個按鈕,文本顯示,再次點(diǎn)擊這個按鈕,文本消失,但瀏覽器并不刷新。下面我就用HTC來實(shí)現(xiàn)這個簡單效果。編程思路是這樣的:用HTC模擬一個開關(guān),它有”on”和”off”兩種狀態(tài)(可讀/寫屬性status);用戶可以設(shè)置這兩種狀態(tài)下開關(guān)所顯示的文本(設(shè)置屬性 turnOffTextturnOnText);用戶點(diǎn)擊開關(guān)時,開關(guān)狀態(tài)被反置,并觸發(fā)一個事件(onStatusChanged)通知用戶,用戶可以自己寫代碼來響應(yīng)這個事件;該HTC還定義了一個方法(reverseStatus),用來反置開關(guān)的狀態(tài)。下面是這個HTC的代碼:

          <!—switch.htc定義 -->  
          <PUBLIC:COMPONENT TAGNAME="Switch">  
              <!--屬性定義-->  
              <PUBLIC:PROPERTY NAME="turnOnText" PUT="setTurnOnText" VALUE="Turn on" />  
              <PUBLIC:PROPERTY NAME="turnOffText" PUT="setTurnOffText" VALUE="Turn off" />  
              <PUBLIC:PROPERTY NAME="status" GET="getStatus" PUT="setStatus" VALUE="on" />  
            
              <!--定義事件-->  
              <PUBLIC:EVENT NAME="onStatusChanged" ID="changedEvent" />  
            
              <!--定義方法-->  
              <PUBLIC:METHOD NAME="reverseStatus" />  
            
              <!--關(guān)聯(lián)客戶端事件-->  
              <PUBLIC:ATTACH EVENT="oncontentready" ONEVENT="initialize()"/>  
              <PUBLIC:ATTACH EVENT="onclick" ONEVENT="expandCollapse()"/>  
            
          </PUBLIC:COMPONENT>  
            
          <!-- htc腳本 -->  
          <script language="javascript">  
              var sTurnOnText;    //關(guān)閉狀態(tài)所顯示的文本  
              var sTurnOffText;   //開啟狀態(tài)所顯示的文本  
              var sStatus;    //開關(guān)狀態(tài)  
              var innerHTML   //使用開關(guān)時包含在開關(guān)中的HTML     
            
              //設(shè)置開關(guān)關(guān)閉狀態(tài)所顯示的文本  
              function setTurnOnText(value)  
              {  
                  sTurnOnText = value;  
              }   
            
              //設(shè)置開關(guān)開啟狀態(tài)所顯示的文本  
              function setTurnOffText(value)  
              {  
                  sTurnOffText = value;  
              }     
            
              //設(shè)置開關(guān)狀態(tài)  
              function setStatus(value)  
              {  
                  sStatus = value;  
              }   
            
               //讀取開關(guān)狀態(tài)  
              function getStatus()  
              {  
                  return sStatus;  
              }     
            
              //反向開關(guān)的狀態(tài)  
              function reverseStatus()  
              {  
                  sStatus = (sStatus == "on") ? "off" : "on";  
              }  
            
              //獲取htc控制界面html文本  
              function getTitle()  
              {  
                  var text = (sStatus == "on") ? sTurnOffText : sTurnOnText;  
                  text = "<div id='innerDiv'>" + text + "</div>";  
                  return text;  
            
              }  
            
              //htc初始化代碼  
              function initialize()  
              {  
                  //back up innerHTML  
                  innerHTML = element.innerHTML;  
                  element.innerHTML = (sStatus == "on") ? getTitle() + innerHTML : getTitle();  
              }   
            
              //響應(yīng)用戶鼠標(biāo)事件的方法  
              function expandCollapse()  
              {  
                   reverseStatus();  
                   //觸發(fā)事件  
                   var oEvent = createEventObject();  
                   changedEvent.fire(oEvent);    
                   var srcElem = element.document.parentWindow.event.srcElement;  
                   if(srcElem.id == "innerDiv")  
                   {  
                        element.innerHTML = (sStatus == "on") ? getTitle() + innerHTML : getTitle();  
                   }  
              }  
          </script>  

          html頁面引入自定義標(biāo)記

          <!--learnhtc.html-->  
          <html xmlns:frogone><!--定義一個新的命名空間-->  
          <head>  
              <!--告訴瀏覽器命名空間是由哪個HTC實(shí)現(xiàn)的-->  
              <?IMPORT namespace="frogone" implementation="switch.htc">  
          </head>  
          <body>  
             <!--設(shè)置開關(guān)的各個屬性及內(nèi)部包含的內(nèi)容-->  
             <frogone:Switch id="mySwitch"  
                              TurnOffText="off"  
                              TurnOnText="on"  
                              status="off"  
                              onStatusChanged="confirmChange()">  
                  <div id="dBody">文本內(nèi)容...... </div>  
              </frogone:Switch>  
          </body>  
          <script language="javascript">  
              //相應(yīng)開關(guān)事件  
              function confirmChange()  
              {  
                  if(!confirm("是否改變開關(guān)狀態(tài)?"))  
                      mySwitch.reverseStatus();
              }  
          </script>  
          </html>

          這項(xiàng)技術(shù)提供了事件綁定和屬性、方法定義,以及一些生命周期相關(guān)的事件,應(yīng)該說已經(jīng)是一個比較完整的組件化方案了。但是我們可以看到后來的結(jié)果,它沒有能夠進(jìn)入標(biāo)準(zhǔn),默默地消失了。用我們今天的角度來看,它可以說是生不逢時。

          如何定義一個組件


          ExtJS基于面向?qū)ο蟮乃枷耄瑢⒔M件設(shè)計成函數(shù)容器,擁有嚴(yán)格的繼承關(guān)系和組件生命周期鉤子。HTC利用IE瀏覽器內(nèi)置的一種腳本封裝機(jī)制,將行為從文檔結(jié)構(gòu)中分離,通過類似樣式或者自定義標(biāo)識的方式為HTML頁面引入高級的自定義行為(behavior)。從歷史上組件化的嘗試來看,我們應(yīng)該如何來定義一個組件呢?

          首先應(yīng)該清楚組件化的設(shè)想為了解決什么問題?不言而喻,組件化最直接的目的就是復(fù)用,提高開發(fā)效率,作為一個組件應(yīng)該滿足下面幾個條件:

          • 封裝:組件屏蔽了內(nèi)部的細(xì)節(jié),組件的使用者可以只關(guān)心組件的屬性、事件和方法。

          • 解耦:組件本身隔離了變化,組件開發(fā)者和業(yè)務(wù)開發(fā)者可以根據(jù)組件的約定各自獨(dú)立開發(fā)和測試。

          • 復(fù)用:組件將會作為一種復(fù)用單元,被用在多處。

          • 抽象:組件通過屬性和事件、方法等基礎(chǔ)設(shè)施,提供了一種描述UI的統(tǒng)一模式,降低了使用者學(xué)習(xí)的心智成本。


          接下來我們深入具體的技術(shù)細(xì)節(jié),看看組件化的基本思路。首先,最基礎(chǔ)的語義化標(biāo)簽就能看作成一個個組件,通過DOM API可以直接掛載到對應(yīng)的元素上:

          var element = document.createElement('div')
          document.getElementById('container').appendChild(element)

          但是實(shí)際上我們的組件不可能這么簡單,涵蓋場景比較多的組件都比較復(fù)雜,工程師就想到將組件定義為原生JS中的Function或者Class容器(其實(shí)Object也是一種思路,比如Vue),因?yàn)樵?/span>JavaScript語法中它們天生就是提供了一個閉關(guān)的空間,形如:

          function MyComponent(){
              this.prop1;
              this.method1;
              ……
          }

          不過,要想掛載又成了難題,普通的JS對象沒法被用于appendChild,所以前端工程師就有了兩種思路,第一種是反過來,設(shè)計一個appendTo方法,讓組件把自己掛到DOM樹上去。

          function MyComponent(){
              this.root = document.createElement("div");
              this.appendTo = function(node){
                  node.appendChild(this._root)
              }
          }

          第二種比較有意思,是讓組件直接返回一個DOM元素,把方法和自定義屬性掛到這個元素上:

          function MyComponent(){
              var _root = document.createElement("div");
              root.prop1 // = ...
              root.method1 = function(){
              /*....*/
              }
              return root;
          }

          document.getElementById("container").appendChild(new MyComponent());

          下面我們根據(jù)上面思想來設(shè)計一個輪播組件,能夠自動播放圖片


          <!DOCTYPE html>
          <html lang="en">
          <head>
            <meta charset="UTF-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Document</title>
            <style>
              .carousel, .carousel > img {
                width: 500px;
                height: 300px;
              }

              .carousel {
                display: flex;
                overflow: hidden;
              }

              .carousel > img {
                transition: transform ease 0.5s;
              }
            </style>
          </head>
          <body>
            <script>
              let d = [
                {
                    img: "https://static001.geekbang.org/resource/image/bb/21/bb38fb7c1073eaee1755f81131f11d21.jpg",
                    url: "https://time.geekbang.org",
                    title: "藍(lán)貓"
                },
                {
                    img: "https://static001.geekbang.org/resource/image/1b/21/1b809d9a2bdf3ecc481322d7c9223c21.jpg",
                    url: "https://time.geekbang.org",
                    title: "橘貓"
                },
                {
                    img: "https://static001.geekbang.org/resource/image/b6/4f/b6d65b2f12646a9fd6b8cb2b020d754f.jpg",
                    url: "https://time.geekbang.org",
                    title: "橘貓加白"
                },
                {
                    img: "https://static001.geekbang.org/resource/image/73/e4/730ea9c393def7975deceb48b3eb6fe4.jpg",
                    url: "https://time.geekbang.org",
                    title: "貓"
                }
              ];

              class Carousel {
                constructor(data) {
                  this._root = document.createElement('div');
                  this._root.classList = ['carousel']
                  this.children = [];
                  for (const d of data) {
                    const img = document.createElement('img');
                    img.src = d.img;
                    this._root.appendChild(img);
                    this.children.push(img);
                  }

                  let i = 0;
                  let current = i
                  setInterval(() => {
                    for (const child of this.children) {
                      child.style.zIndex = '0';
                    }
                    // 計算下一張圖片的下標(biāo)
                    let next = (i + 1) % this.children.length;

                    const currentElement = this.children[current];
                    const nextElement = this.children[next];

                    // 下一張圖片的zIndex應(yīng)該大于當(dāng)前圖片的zIndex
                    currentElement.style.zIndex = '1';
                    nextElement.style.zIndex = '2';
                    // 禁止添加的動畫過渡樣式
                    currentElement.style.transition = 'none';
                    nextElement.style.transition = 'none';
                    console.log('current', current, next)
                    // 每次初始化當(dāng)前圖片和下一張圖片的位置
                    currentElement.style.transform = `translate3d(${-100 * current}%, 0 , 0)`;
                    nextElement.style.transform = `translate3d(${100 - 100 * next}%, 0 , 0)`;

                    // 瀏覽器刷新頻率是每秒60幀,所以這里需要延遲到瀏覽器下次重繪更新下一幀動畫
                    setTimeout(() => {
                      // 啟動添加的動畫過渡樣式
                      currentElement.style.transition = '';
                      nextElement.style.transition = '';
                      // 當(dāng)前圖片退出,下一張圖片進(jìn)來
                      currentElement.style.transform = `translate3d(${-100 -100 * current}% 0 , 0)`;
                      nextElement.style.transform = `translate3d(${-100 * next}%, 0 , 0)`;
                    }, 1000 / 60);
                      
                    // 或者使用window.requestAnimationFrame,當(dāng)然這個比較難理解,容易出錯,使用setTimeout也是可以的
                    // window.requestAnimationFrame(() => {
                    //   window.requestAnimationFrame(() => {
                    //   // 啟動添加的動畫過渡樣式
                    //   currentElement.style.transition = '';
                    //   nextElement.style.transition = '';
                    //   // 當(dāng)前圖片退出,下一張圖片進(jìn)來
                    //   currentElement.style.transform = `translate3d(${-100 -100 * current}% 0 , 0)`;
                    //   nextElement.style.transform = `translate3d(${-100 * next}%, 0 , 0)`;
                    //   })
                    // })

                    current = next;
                    i++;
                  }, 3000);

                  // 追加 
                  this.appendTo = function(node){
                    node.appendChild(this._root)
                  }
                }
              }

              new Carousel(d).appendTo(document.body);
            </script>
          </body>
          </html>

          效果:


          上面我們已經(jīng)實(shí)現(xiàn)了一個簡單的輪播圖,接下來我們嘗試將其應(yīng)用到JSX中

          // index.js
          const ele = <div id="root" name="container">
            <Carousel data=go7utgvlrp></Carousel>
            <span>a</span>
            <span>b</span>
            <span>c</span>
          </div>

          document.body.appendChild(ele);

          <!DOCTYPE html>
          <html lang="en">
          <head>
            <meta charset="UTF-8">
            <meta http-equiv="X-UA-Compatible" content="IE=edge">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Document</title>
            <style>
              .carousel, .carousel > img {
                width: 500px;
                height: 300px;
              }

              .carousel {
                display: flex;
                overflow: hidden;
              }

              .carousel > img {
                transition: transform ease 0.5s;
              }
            </style>
          </head>
          <body>
            <script src="./index.js"></script>
          </body>
          </html>

          但是直接照上面這樣往html中引入含有JSX語法的js腳本運(yùn)行會報錯,因?yàn)闉g覽器并不支持JSX,那我們怎么辦呢?

          我們需要先將JSX編譯成js,然后再引入到html中,此時Babel就派上用場了,@babel/plugin-transform-react-jsx插件可以幫助我們將JSX編譯js

          // 編譯前
          const ele = <div id="root" name="container">
            <Carousel data=go7utgvlrp></Carousel>
            <span>a</span>
            <span>b</span>
            <span>c</span>
          </div>

          // 編譯后
          var ele = React.createElement("div", {id: "root", name: "container"}, 
              React.createElement(Carousel, {data: d}), 
              React.createElement("span", null, "a"), 
              React.createElement("span", null, "b"), 
              React.createElement("span", null, "c")
          );

          編譯后的元素將默認(rèn)采用React.createElement創(chuàng)建,createElement方法除了支持基本的html標(biāo)簽外,還支持自定義的函數(shù)組件和類組件,但問題是我們的Carousel組件并不是React中的函數(shù)組件和類組件,正好@babel/plugin-transform-react-jsx默認(rèn)配置參數(shù)pragma使用React.createElement替換編譯JSX表達(dá)式時使用的函數(shù),也允許我們自定義函數(shù)去做React.createElement函數(shù)類似的事情,下面我們來實(shí)現(xiàn)一下:

          function createElement<P extends {}>(
              type: FunctionComponent<P>,
              props?: Attributes & P | null,
              ...children: ReactNode[]): FunctionComponentElement<P>;
          function createElement<P extends {}>(
              type: ClassType<P, ClassicComponent<P, ComponentState>, ClassicComponentClass<P>>,
              props?: ClassAttributes<ClassicComponent<P, ComponentState>> & P | null,
              ...children: ReactNode[]): CElement<P, ClassicComponent<P, ComponentState>>;
          function createElement<P extends {}, T extends Component<P, ComponentState>, C extends ComponentClass<P>>(
              type: ClassType<P, T, C>,
              props?: ClassAttributes<T> & P | null,
              ...children: ReactNode[]): CElement<P, T>;
          function createElement<P extends {}>(
              type: FunctionComponent<P> | ComponentClass<P> | string,
              props?: Attributes & P | null,
              ...children: ReactNode[]): ReactElement<P>;

          首先,我們先來改造一下Carousel組件,使其可以接收注入的data屬性,這里我們采用setAttribute和屬性描述符set存值函數(shù)來實(shí)現(xiàn)

          // index.js
          class Carousel {
            constructor(data) {
              this._root = document.createElement('div');
              this._root.classList = ['carousel'];
              this.children = [];
            }

            set data(data) {
              this._root.innerHTML = '';

              for (const d of data) {
                const img = document.createElement('img');
                img.src = d.img;
                this._root.appendChild(img);
                this.children.push(img);
              }

              let i = 0;
              let current = i
              setInterval(() => {
                for (const child of this.children) {
                  child.style.zIndex = '0';
                }
                let next = (i + 1) % this.children.length;

                const currentElement = this.children[current];
                const nextElement = this.children[next];

                currentElement.style.zIndex = '1';
                nextElement.style.zIndex = '2';
                currentElement.style.transition = 'none';
                nextElement.style.transition = 'none';
                currentElement.style.transform = `translate3d(${-100 * current}%, 0 , 0)`;
                nextElement.style.transform = `translate3d(${100 - 100 * next}%, 0 , 0)`;

                setTimeout(() => {
                  currentElement.style.transition = '';
                  nextElement.style.transition = '';
                  currentElement.style.transform = `translate3d(${-100 -100 * current}% 0 , 0)`;
                  nextElement.style.transform = `translate3d(${-100 * next}%, 0 , 0)`;
                }, 1000 / 60);

                current = next;
                i++;

              }, 3000);
            }

            setAttribute(name, value) {
              this[name] = value; // 這里統(tǒng)一attribute和properties,vue使用的是attribute
            }

            // 追加 
            appendTo = function(node){
              node.appendChild(this._root);
            }
          }

          當(dāng)往Carousel組件注入data時,我們觸發(fā)組件的setAttribute方法將data掛載到組件實(shí)例上,并且觸發(fā)set存值函數(shù),初始化輪播圖組件。那么如何在注入data時觸發(fā)組件的setAttribute方法呢?這就是我們自定義轉(zhuǎn)化函數(shù)要做的事了

          // index.js
          const create = (Class, properity, ...children) => {
            let element;
            if (typeof Class === 'string') {
              // 基本標(biāo)簽直接創(chuàng)建
              element = document.createElement(Class);
            } else {
              // 自定義組件實(shí)例化
              element = new Class;
            }

            // 注入到基本標(biāo)簽上的屬性直接追加到元素的Attribute屬性中,而注入到自定義組件的屬性調(diào)用組件的setAttribute方法
            for (const p in properity) {
              element.setAttribute(p, properity[p]);  
            }

            // 處理子節(jié)點(diǎn)
            for(let child of children) {
              if (typeof child === 'string') {
                // 如果子節(jié)點(diǎn)是字符串,那就創(chuàng)建文本節(jié)點(diǎn)
                child = document.createTextNode(child);
              }
              // 如果子節(jié)點(diǎn)含有appendTo方法,則是我們自定義的Carousel組件,將子節(jié)點(diǎn)追加上當(dāng)前節(jié)點(diǎn)上
              if (child.appendTo) {
                child.appendTo(element);
              } else {
                // html標(biāo)簽,也追加到當(dāng)前節(jié)點(diǎn)上
                element.appendChild(child);
              }
            }
            return element;
          }

          最后我們將index.js構(gòu)建后在引入到html中,附上webpack配置:

          // webpack.config.js
          const path = require('path')

          module.exports = {
            entry: "./index.js",
            output: {
              path: path.resolve(__dirname, 'dist')
            },
            module: {
                rules: [
                    {
                        test:/\.js$/,
                        use:{
                            loader: "babel-loader",
                            options: {
                                presets:["@babel/preset-env"],
                                plugins: [["@babel/plugin-transform-react-jsx", {pragma: "create"}]]
                            }
                        }
                    }
                ]
            },
            mode: "development"
          }

          我們來看下構(gòu)建后的腳本

          /*
           * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
           * This devtool is neither made for production nor for readable output files.
           * It uses "eval()" calls to create a separate source file in the browser devtools.
           * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
           * or disable the default devtool with "devtool: false".
           * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
           */
          /******/ (() => { // webpackBootstrap
          /******/     var __webpack_modules__ = ({

          /***/ "./index.js":
          /*!******************!*\
            !*** ./index.js ***!
            \******************/
          /***/ (() => {

          eval("function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== \"undefined\" && o[Symbol.iterator] || o[\"@@iterator\"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === \"number\") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError(\"Invalid attempt to iterate non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.\"); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it[\"return\"] != null) it[\"return\"](); } finally { if (didErr) throw err; } } }; }\n\nfunction _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === \"string\") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === \"Object\" && o.constructor) n = o.constructor.name; if (n === \"Map\" || n === \"Set\") return Array.from(o); if (n === \"Arguments\" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }\n\nfunction _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }\n\nvar create = function create(Class, properity) {\n  var element;\n\n  if (typeof Class === 'string') {\n    element = document.createElement(Class);\n  } else {\n    element = new Class();\n  }\n\n  for (var p in properity) {\n    element.setAttribute(p, properity[p]);\n  }\n\n  for (var _len = arguments.length, children = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {\n    children[_key - 2] = arguments[_key];\n  }\n\n  for (var _i = 0, _children = children; _i < _children.length; _i++) {\n    var child = _children[_i];\n\n    if (typeof child === 'string') {\n      // 文本節(jié)點(diǎn)\n      child = document.createTextNode(child);\n    }\n\n    if (child.appendTo) {\n      // Carousel組件\n      child.appendTo(element);\n    } else {\n      // html標(biāo)簽\n      element.appendChild(child);\n    }\n  }\n\n  return element;\n};\n\nvar d = [{\n  img: \"https://static001.geekbang.org/resource/image/bb/21/bb38fb7c1073eaee1755f81131f11d21.jpg\",\n  url: \"https://time.geekbang.org\",\n  title: \"藍(lán)貓\"\n}, {\n  img: \"https://static001.geekbang.org/resource/image/1b/21/1b809d9a2bdf3ecc481322d7c9223c21.jpg\",\n  url: \"https://time.geekbang.org\",\n  title: \"橘貓\"\n}, {\n  img: \"https://static001.geekbang.org/resource/image/b6/4f/b6d65b2f12646a9fd6b8cb2b020d754f.jpg\",\n  url: \"https://time.geekbang.org\",\n  title: \"橘貓加白\"\n}, {\n  img: \"https://static001.geekbang.org/resource/image/73/e4/730ea9c393def7975deceb48b3eb6fe4.jpg\",\n  url: \"https://time.geekbang.org\",\n  title: \"貓\"\n}];\n\nvar Carousel = /*#__PURE__*/function () {\n  function Carousel(data) {\n    _classCallCheck(this, Carousel);\n\n    _defineProperty(this, \"appendTo\", function (node) {\n      node.appendChild(this._root);\n    });\n\n    this._root = document.createElement('div');\n    this._root.classList = ['carousel'];\n    this.children = [];\n  }\n\n  _createClass(Carousel, [{\n    key: \"data\",\n    set: function set(data) {\n      var _this = this;\n\n      this._root.innerHTML = '';\n\n      var _iterator = _createForOfIteratorHelper(data),\n          _step;\n\n      try {\n        for (_iterator.s(); !(_step = _iterator.n()).done;) {\n          var _d = _step.value;\n          var img = document.createElement('img');\n          img.src = _d.img;\n\n          this._root.appendChild(img);\n\n          this.children.push(img);\n        }\n      } catch (err) {\n        _iterator.e(err);\n      } finally {\n        _iterator.f();\n      }\n\n      var i = 0;\n      var current = i;\n      setInterval(function () {\n        var _iterator2 = _createForOfIteratorHelper(_this.children),\n            _step2;\n\n        try {\n          for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {\n            var child = _step2.value;\n            child.style.zIndex = '0';\n          }\n        } catch (err) {\n          _iterator2.e(err);\n        } finally {\n          _iterator2.f();\n        }\n\n        var next = (i + 1) % _this.children.length;\n        var currentElement = _this.children[current];\n        var nextElement = _this.children[next];\n        currentElement.style.zIndex = '1';\n        nextElement.style.zIndex = '2';\n        currentElement.style.transition = 'none';\n        nextElement.style.transition = 'none';\n        currentElement.style.transform = \"translate3d(\".concat(-100 * current, \"%, 0 , 0)\");\n        nextElement.style.transform = \"translate3d(\".concat(100 - 100 * next, \"%, 0 , 0)\");\n        setTimeout(function () {\n          currentElement.style.transition = '';\n          nextElement.style.transition = '';\n          currentElement.style.transform = \"translate3d(\".concat(-100 - 100 * current, \"% 0 , 0)\");\n          nextElement.style.transform = \"translate3d(\".concat(-100 * next, \"%, 0 , 0)\");\n        }, 1000 / 60);\n        current = next;\n        i++;\n      }, 3000);\n    }\n  }, {\n    key: \"setAttribute\",\n    value: function setAttribute(name, value) {\n      this[name] = value; // 這里統(tǒng)一attribute和properties,vue使用的是attribute\n    } // 追加 \n\n  }]);\n\n  return Carousel;\n}();\n\nvar ele = create(\"div\", {\n  id: \"root\",\n  name: \"container\"\n}, create(Carousel, {\n  data: d\n}), create(\"span\", null, \"a\"), create(\"span\", null, \"b\"), create(\"span\", null, \"c\"));\ndocument.body.appendChild(ele);\n\n//# sourceURL=webpack://webpack-jsx/./index.js?");

          /***/ })

          /******/     });
          /************************************************************************/
          /******/     
          /******/     // startup
          /******/     // Load entry module and return exports
          /******/     // This entry module can't be inlined because the eval devtool is used.
          /******/     var __webpack_exports__ = {};
          /******/     __webpack_modules__["./index.js"]();
          /******/     
          /******/ })()
          ;

          最后,實(shí)際的運(yùn)行效果之前的一樣

          未完待續(xù)!!!



          點(diǎn)擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開更多互動和交流,掃描下方”二維碼“或在“公眾號后臺回復(fù)“ 入群 ”即可加入我們的技術(shù)交流群,收獲更多的技術(shù)文章~

          - END -


          瀏覽 60
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美成人国产精品高潮 | 日日撸日日操 | 国产黄色视频免费网址 | 91香蕉在线观看 | 欧美在线观看网站 |