<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      [譯文]如何實現一個單文件組件

      前端開發人員只要了解過vue.js框架可能都知道單文件組件。vue.js中的單文件組件允許在一個文件中定義一個組件的所有內容。這是一個非常有用的解決方案,在瀏覽器網頁中已經開始提倡這種機制。但是不幸的是,這個概念自從2017年8月被提出以來,到現在沒有任何進展,像是已經要消亡了一樣。然而,深入研究這個主題并試著使用現有的技術來實現單文件組件是很有趣的,值得嘗試。

      單文件組件

      知道“漸進增強”這個概念的前端開發人員想必也聽說過“分層”這個概念。在組件中,同樣有這樣的概念。事實上,每個組件至少有3層,甚至多余3層:內容/模板,表現和行為。又或者保守的說,每個組件會被分成至少3個文件,比如:一個按鈕組件的文件結構可能是下面這樣的:

      Button/
      | -- Button.html
      | -- Button.css
      | -- Button.js

      采用這種方式分層相當于技術的分離(內容/模板:使用html,表現:使用css,行為:使用JavaScript)。如果沒有采用任何構建工具打包,這意味著瀏覽器需要獲取這3個文件。因此,一個想法是:迫切需要一種分離組件代碼而不分離技術(文件)的技術來解決這個問題。這就是這篇文章要討論的主題—單文件組件。

      總的來說,我對“技術分層”持懷疑態度。它來自一個事實,就是組件分層常常因為繞不開“技術分層”而被放棄,而這兩者是完全分離的。

      回到主題,用單文件組件實現按鈕可能是這樣的:

      <template>
        <!-- Button.html contents go here. -->
      </template>
      
      <style>
        /* Button.css contents go here. */
      </style>
      
      <script>
        // Button.js contents go here.
      </script>

      可以看到這個單文件組件很像最初前端開發中的html文檔,它有自己的style標簽和script標簽,只是表現層使用一個template標簽。由于使用了簡單的方式,得到一個強大的分層組件(內容/模板:<template>,表現:<style>,行為:<script>),而不需要使用3個分離的文件。

      基本概念

      首先,我們創建一個全局函數loadComponent()來加載組件。

      window.loadComponent = (function() {
        function loadComponent( URL ) {}
        return loadComponent;
      }());

      這里使用了JavaScript模塊模式。它允許定義所有必要的輔助函數,但是只向外公開loadComponent()函數。當然,現在這個函數還是空的。

      后面,我們要創建一個<hello-world>組件,顯式下面的內容。

      Hello, world! My name is <given name>.

      另外,點擊這個組件,彈出一個信息:

      Don’t touch me!

      組件代碼保存為一個文件HelloWorld.wc(這里.wc代表Web Component)。初始代碼如下:

      <template>
        <div class="hello">
          <p>Hello, world! My name is <slot></slot>.</p>
        </div>
      </template>
      <style>
        div {
          background: red;
          border-radius: 30px;
          padding: 20px;
          font-size: 20px;
          text-align: center;
          width: 300px;
          margin: 0 auto;
        }
      </style>
      <script></script>

      目前,沒有給組件添加任何行為,只是定義了模板和樣式。模板中,可以使用常見的html標簽,比如<div>,另外,template中出現了<slot>元素表明組件將實現影子DOM。并且默認情況下這個DOM自身所有樣式和模板都隱藏在這個DOM中。

      組件在網頁中使用的方式非常簡單。

      <hello-world>Comandeer</hello-world>
      <script src="loader.js"></script>
      <script>
        loadComponent( 'HelloWorld.wc' );
      </script>

      可以像標準的自定義元素一樣使用組件。唯一的區別是需要在使用loadComponent()方法前先加載它(這個方法放在loader.js中)。loadComponent()方法完成所有繁重的工作,比如獲取組件,并通過customElements.define()注冊它。

      在了解了所有概念之后,是時候動手實踐了。

      簡單的loader

      如果想從外部文件中加載文件,需要使用萬能的ajax。但是現在已經是2020年了,在大部分瀏覽器中,你可以大膽的使用Fetch API

      function loadComponent( URL ) {
        return fetch( URL );
      }

      但是,這只是獲取到文件,沒有對文件做任何處理,接下來要做的是把ajax返回內容轉換成text文本,如下:

      function loadComponent( URL ) {
        return fetch( URL ).then( ( response ) => {
          return response.text();
        } );
      }

      由于loadComponent()函數返回的是fetch函數的執行結果,所以它是一個Promise對象。可以在then方法中檢查文件(HelloWorld.wc)是不是真的被加載,還有,是不是被轉成文本了:

      loadComponent('HelloWorld.wc').then((component) => {
          console.log(component);
      });

      運行結果如下:

       

      chrome瀏覽器下,使用console()方法,我們看到HelloWorld.wc的內容被轉成了text并輸出,所以貌似行得通!

      解析組件內容

      然而,僅僅把文本輸出并沒有達到我們的目的。最終要把它轉換成DOM用于顯示,并能和用戶真正交互起來。

      在瀏覽器環境中有一個非常實用的類DOMParser,可以實用它創建一個DOM解析器。實例化一個DOMParser類得到一個對象,實用這個對象可以將組件文本轉換成DOM:

      window.loadComponent = (function () {
          function loadComponent(URL) {
              return fetch(URL).then((response) => {
                  return response.text();
              }).then((html) => {
                  const parser = new DOMParser(); // 1
                  return parser.parseFromString(html, 'text/html'); // 2
              });
          }
          return loadComponent;
      }());

      首先,創建一個DOMParser實例parser(1),然后用這個實例將組件內容轉化成DOM(2)。值得注意的是,這里實用的是HTML模式(‘text/html’)。如果希望代碼更好的符合JSX標準或者原始的Vue.js組件,可以實用XML模式(‘text/XML’)。但是,在這種情況下,需要更改組件本身的結構(例如,添加可以容納其他元素的主元素)。

      這是再輸出loadComponent()函數的返回結果,就是一個DOM樹了。

       

      在chrome瀏覽器下,console.log()輸出解析后的HelloWorld.wc文件,是一個DOM樹。

      注意,parser.parseFromString方法自動給組件添加上<html>,<head>,<body>標簽元素。這是由HTML解析器的工作原理造成的。在HTML LS規范中詳細描述了構建DOM樹的算法。這篇文章很長,要花點時間閱讀,可以簡單地理解為解析器會默認把所有內容放在<head>元素中,直至遇到一個只能放在<body>標簽內的DOM元素。所以,組件代碼中所有的元素(<element>,<style>,<script>)都允許放在<head>中。如果在<template>外層包一個<p>元素,那么解析器將把它放在<body>中。

      還有一個問題,組件被解析之后并沒有<!DOCTYPE html>聲明,所以這得到的是一個非正常的html文檔,因此瀏覽器會使用一種被成為怪異模式的方式來渲染這種html文檔。所幸的是,在這里它不會帶來任何負面作用,因為這里只使用DOM解析器將組件分割成適當的部分。

      有了DOM樹之后,可以只截取我們需要的部分。

      return fetch(URL).then((response) => {
          return response.text();
      }).then((html) => {
          const parser = new DOMParser();
          const document = parser.parseFromString(html, 'text/html');
          const head = document.head;
          const template = head.querySelector('template');
          const style = head.querySelector('style');
          const script = head.querySelector('script');
      
          return {
              template,
              style,
              script
          };
      }); 

      最后整理一下代碼,loadComponent方法如下。

      window.loadComponent = (function () {
          function fetchAndParse(URL) {
              return fetch(URL).then((response) => {
                  return response.text();
              }).then((html) => {
                  const parser = new DOMParser();
                  const document = parser.parseFromString(html, 'text/html');
                  const head = document.head;
                  const template = head.querySelector('template');
                  const style = head.querySelector('style');
                  const script = head.querySelector('script');
      
                  return {
                      template,
                      style,
                      script
                  };
              });
          }
      
          function loadComponent(URL) {
              return fetchAndParse(URL);
          }
      
          return loadComponent;
      }()); 

      從外部文件中獲取組件代碼的方式不止Fetch API這一種,XMLHttpRequest有一個專用的文檔模式,允許您省略整個解析步驟。但是XMLHttpRequest返回的不是一個Promise,這個需要自己包裝。

      注冊組件

      現在有了組件的層,可以創建registerComponent()方法來注冊新的自定義組件了。

      window.loadComponent = (function () {
          function fetchAndParse(URL) {
              […]
          }
          function registerComponent() {
          }
          function loadComponent(URL) {
              return fetchAndParse(URL).then(registerComponent);
          }
          return loadComponent;
      }()); 

      要注意的是,自定義組件必須是一個繼承自HTMLElement的類。此外,每個組件都將使用用于存儲樣式和模板內容的影子DOM。所以每個引用這個組件的場合下,這個組件都有相同的樣式。方法如下:

      function registerComponent({template, style, script}) {
          class UnityComponent extends HTMLElement {
              connectedCallback() {
                  this._upcast();
              }
      
              _upcast() {
                  const shadow = this.attachShadow({mode: 'open'});
                  shadow.appendChild(style.cloneNode(true));
                  shadow.appendChild(document.importNode(template.content, true));
              }
          }
      } 

      應該在registerComponent()方法內創建UnityComponent類,因為這個類要使用傳入registerComponent()的參數。這個類會使用一種稍加修改的機制來實現影子DOM,在這篇關于影子DOM的文章(波蘭文)中我有詳細介紹。

      關于注冊組件,現在只剩下一件事,給單文件組件一個名字,并把它加到當前頁面的DOM中。

      function registerComponent( { template, style, script } ) {
        class UnityComponent extends HTMLElement {
          [...]
        }
      
        return customElements.define( 'hello-world', UnityComponent );
      } 

      現在可以打開看一下了,如下:

       

      在chrome中,這個按鈕組件中,有一個紅色矩形,文字內容:Hello, world! My name is Comandeer。

      獲取腳本內容

      現在一個簡單的按鈕組件已經實現。現在要實現最困難的部分,添加行為層,并自定義按鈕內內容。在上面的步驟中,我們應該使用到按鈕的地方傳入內容,而不是在組件代碼中硬編碼按鈕內的文字內容。同理還要處理組件內綁定的事件監聽,這里我們使用類似Vue.js的約定,如下:

      <template>
        […]
      </template>
      
      <style>
        […]
      </style>
      
      <script>
        export default { // 1
          name: 'hello-world', // 2
          onClick() { // 3
            alert( `Don't touch me!` );
          }
        }
      </script> 

      可以假設組件內<script>標簽中的內容是一個JavaScript模塊,它導出內容(1)。模塊導出的對象包含組件的名稱(2)和一個已“on..”開頭的事件監聽方法(3)。

      這看上去很工整,沒有內容暴露在模塊外部(因為JavaScript中modules并不是在全局作用域中)。這里有一個問題:沒有一個標準可以處理從內部模塊導出的對象(這些代碼直接定義在HTML文檔中)。import語句會假設獲取到一個模塊標識,根據這個標識導入。最常見的是從一個包含代碼的文件的URL路徑。組件不是一個js文件,沒有這樣一個標識,內部的模塊是沒有這樣的標識的。

      在繳械投降之前,可以使用跟一個超級臟的hack。最少有2中方式讓瀏覽器像處理一個文件一樣處理一段文本:Data URIObject URI。也有一些建議是使用Service Worker。但是在這里顯得有點大材小用。

      Data URI和Object URI

      Data URI是一個古老,原始的方法。它的基礎是將文件內容轉換成URL,去掉不必要的空格,然后使用Base64對所有內容進行編碼。假設有一個JavaScript文件,內容如下:

      export default true

      轉換成Data URI如下:

      data:application/javascript;base64,ZXhwb3J0IGRlZmF1bHQgdHJ1ZTs= 

      然后,可以像引入一個文件一樣引入這個URI:

      import test from 'data:application/javascript;base64,ZXhwb3J0IGRlZmF1bHQgdHJ1ZTs=';
      console.log( test ); 

      Data URI這種方式的一個明顯的缺點是隨著JavaScript文件內容增多,這個URL的長度會隨之變得很長。還有把二進制數據放在Data URI非常困難。

      所以,現在有一種新的Object URI。它是從幾種標準中衍生出來的,包括File API和HTML5中的<video>和<audio>標簽。Object URI的目的很簡單,從給定的二進制數據創建一個“偽文件”,在當前上下文中給出一個唯一URI。簡單點說,就是在內存中創建一個有唯一名稱的文件。Object URI有Data URI所有的優點(一種創建"文件"的方法),而沒它的缺點(即使文件有100M也沒關系)。

      對象uri通常是從多媒體流(例如在<video>或<audio>上下文中)或通過輸入[type=file]和拖放機制發送的文件創建的。還可以使用FileBlob這兩個類手動創建。在本例中,我們使用Bolb,先把內容放在模塊中,然后轉換成Object URI:

      const myJSFile = new Blob( [ 'export default true;' ], { type: 'application/javascript' } );
      const myJSURL = URL.createObjectURL( myJSFile );
      
      console.log( myJSURL ); // blob:https://blog.comandeer.pl/8e8fbd73-5505-470d-a797-dfb06ca71333 

      動態導入

      不過,還有一個問題:import語句不接受變量作為模塊標識符。這意味著除了使用該方法將模塊轉換成“文件”之外,還是無法導入它。還是無解的嗎?

      也不盡然。這個問題在很久之前就被提出了,使用動態導入機制可以解決。它是ES2020標準的一部分,并且在Firefox,Safari和Node.js13.x中已經被實現。使用一個變量作為要動態導入的模塊的標示符已經不再是一個難題了:

      const myJSFile = new Blob( [ 'export default true;' ], { type: 'application/javascript' } );
      const myJSURL = URL.createObjectURL( myJSFile );
      
      import( myJSURL ).then( ( module ) => {
        console.log( module.default ); // true
      }); 

      從上面代碼中可以看到,import()命令可以像方法一樣使用,它返回一個Promise對象,then方法中得到模塊對象。在它的default屬性中包含了模塊中定義的所有導出對象。 

      實現

      現在我們已經知道思路了,現在可以著手實現它。在添加一個工具方法,getSetting()。在registerComponents()方法之前調用它,進而從腳本代碼中獲取所有信息。

      function getSettings( { template, style, script } ) {
        return {
          template,
          style,
          script
        };
      }
      [...]
      function loadComponent( URL ) {
        return fetchAndParse( URL ).then( getSettings ).then( registerComponent );
      } 

      現在,這個方法返回了所有傳入的參數。按照上面介紹的邏輯,將腳本代碼轉換成Object URI:

      const jsFile = new Blob( [ script.textContent ], { type: 'application/javascript' } );
      const jsURL = URL.createObjectURL( jsFile ); 

      下一步,使用import加載模塊,返回模板,樣式和組件的名稱:

      return import( jsURL ).then( ( module ) => {
        return {
          name: module.default.name,
          template,
          style
        }
      } ); 

      由于這個原因,registerComponent()仍然獲得3個參數,但是現在它獲取的是name,而不是腳本。正確的代碼如下:

      function registerComponent( { template, style, name } ) {
        class UnityComponent extends HTMLElement {
          [...]
        }
      
        return customElements.define( name, UnityComponent );
      } 

      行為層

      組件還剩下最后一層:行為層,用來處理事件。現在我們只是在getSettings()方法中獲取到了組件的名字,還要獲取事件監聽。可以使用Object.entrie()方法獲取。 在getSettings()方法中添加合適的代碼:

      function getSettings( { template, style, script } ) {
        [...]
      
        function getListeners( settings ) { // 1
          const listeners = {};
      
          Object.entries( settings ).forEach( ( [ setting, value ] ) => { // 3
            if ( setting.startsWith( 'on' ) ) { // 4
              listeners[ setting[ 2 ].toLowerCase() + setting.substr( 3 ) ] = value; // 5
            }
          } );
      
          return listeners;
        }
      
        return import( jsURL ).then( ( module ) => {
          const listeners = getListeners( module.default ); // 2
      
          return {
            name: module.default.name,
            listeners, // 6
            template,
            style
          }
        } );
      } 

      現在方法變得有點復雜了。添加了一個新的函數getListeners()(1) ,將模塊的輸出傳入這個參數中。

      然后使用Object.entries()(3)方法遍歷導出的模塊。如果當前屬性以“on”(4)開頭,說明是一個監聽函數,將這個節點的值(監聽函數)添加到listeners對象中去,使用setting[2].toLowerCase()+setting.substr(3)(5)得到鍵值。

      鍵值是通過去掉開頭的“on”,并將后面的“Click”首字母轉換成小寫組成的(就是從onClick得到click作為建值)。然后傳入isteners對象(6)。

      可以使用[].reduce()方法代替[].forEach()方法,這樣可以省略掉listeners這個變量,如下:

      function getListeners( settings ) {
        return Object.entries( settings ).reduce( ( listeners, [ setting, value ] ) => {
          if ( setting.startsWith( 'on' ) ) {
            listeners[ setting[ 2 ].toLowerCase() + setting.substr( 3 ) ] = value;
          }
      
          return listeners;
        }, {} );
      } 

      現在,可以將監聽綁定在組件內部的類中:

      function registerComponent( { template, style, name, listeners } ) { // 1
        class UnityComponent extends HTMLElement {
          connectedCallback() {
            this._upcast();
            this._attachListeners(); // 2
          }
          [...]
          _attachListeners() {
            Object.entries( listeners ).forEach( ( [ event, listener ] ) => { // 3
              this.addEventListener( event, listener, false ); // 4
            } );
          }
        }
        return customElements.define( name, UnityComponent );
      } 

      在listeners方法(1)上增加了一個參數,并且在class中添加了一個新方法_attachListeners()(2)。在這里可以再次使用Object.entries()來遍歷listeners(3),并把他們綁定到element(4)。

      最后,點擊組件可以彈出“Don't touch me!”,如下:

       

      兼容性問題及其他

      可以看到,為了實現這個單文件組件,大部分工作圍繞如何支持基本的Form。很多部分使用了臟hacks(使用Object URI來加載ES中的模塊,沒有瀏覽器的支持,這種技術沒有什么意義)。還好,所有的技術在主流瀏覽器下運行正常,包括:Chrome,Firefox和Safari。

      盡管如此,創建一個這樣的項目會接觸到很多瀏覽器技術和最新的web標準,也是一件很有趣的事情。

      最后,可以在網上獲取這個項目的到代碼

       

      參考連接:https://ckeditor.com/blog/implementing-single-file-web-components/

      posted @ 2020-05-29 14:52  nd  閱讀(713)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 色哟哟www网站入口成人学校| 久久久精品94久久精品| 国内熟妇人妻色在线视频| 国产尤物精品自在拍视频首页 | 亚洲熟女乱色一区二区三区| 成人一区二区三区激情视频 | av中文字幕一区二区| 国产中文字幕日韩精品| 亚洲av乱码久久亚洲精品| 一区二区中文字幕久久| 欧美成人VA免费大片视频| 国产偷国产偷亚洲高清午夜 | 四虎成人精品国产永久免费| 国产精品亚洲av三区色| 亚洲av午夜福利精品一区二区| 2021亚洲爆乳无码专区| 女同亚洲精品一区二区三| 国产一区二区四区不卡| 人妻系列无码专区69影院| 网友偷拍视频一区二区三区| 国产又色又爽又黄的网站免费| 一区二区三区AV波多野结衣| 亚洲国产成人精品无码一区二区| 亚洲综合小综合中文字幕| 日韩精品中文字幕人妻| 三级三级三级A级全黄| 亚洲高清国产拍精品熟女| 精品少妇后入一区二区三区 | 成人国产一区二区三区精品| 日韩一区在线中文字幕| 人妻激情乱人伦视频| 蜜臀av一区二区国产精品| 欧洲无码一区二区三区在线观看| av中文无码乱人伦在线观看| 亚洲AV无码久久精品成人| 国产AV福利第一精品| 国产麻豆md传媒视频| 91高清免费国产自产拍| 亚洲 制服 丝袜 无码| 偷拍精品一区二区三区| 国产精品亚洲аv无码播放|