整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          聽說過CSS in JS,那你聽說過JS in CSS嗎


          SS in JS

          CSS in JS是一種解決css問題想法的集合,而不是一個指定的庫。從CSS in JS的字面意思可以看出,它是將css樣式寫在JavaScript文件中,而不需要獨立出.css.less之類的文件。將css放在js中使我們更方便的使用js的變量模塊化tree-shaking。還解決了css中的一些問題,譬如:更方便解決基于狀態的樣式更容易追溯依賴關系生成唯一的選擇器來鎖定作用域。盡管CSS in JS不是一個很新的技術,但國內的普及程度并不高。由于Vue和Angular都有屬于他們自己的一套定義樣式的方案,React本身也沒有管用戶怎樣定義組件的樣式[1],所以CSS in JS在React社區的熱度比較高。

          目前為止實現CSS in JS的第三方庫有很多:(http://michelebertoli.github.io/css-in-js/)。像JSS[2]styled-components[3]等。在這里我們就不展開贅述了(相關鏈接已放在下方),這篇文章的重點是JS in CSS

          JS in CSS又是什么

          在上面我們提到CSS in JS就是把CSS寫在JavaScript中,那么JS in CSS我們可以推斷出就是可以在CSS中使用JavaScript腳本,如下所示。可以在CSS中編寫Paint API的功能。還可以訪問:ctx,geom。甚至我們還可以編寫自己的css自定義屬性等。這些功能的實現都基于CSS Houdini[4]

          .el {
            --color: cyan;
            --multiplier: 0.24;
            --pad: 30;
            --slant: 20;
            --background-canvas: (ctx, geom) => {
              let multiplier = var(--multiplier);
              let c = `var(--color)`;
              let pad = var(--pad);
              let slant = var(--slant);
          
              ctx.moveTo(0, 0);
              ctx.lineTo(pad + (geom.width - slant - pad) * multiplier, 0);
              ctx.lineTo(pad + (geom.width - slant - pad) * multiplier + slant, geom.height);
              ctx.lineTo(0, geom.height);
              ctx.fillStyle = c;
              ctx.fill();
            };
            background: paint(background-canvas);
            transition: --multiplier .4s;
          }
          .el:hover {
            --multiplier: 1;
          }
          

          Houdini 解決了什么問題

          CSS 與 JS的標準制定流程對比

          在如今的Web開發中,JavaScript幾乎占據了項目代碼的大部分。我們可以在項目開發中使用ES 2020、ES2021、甚至提案中的新特性(如:Decorator[5]),即使瀏覽器尚未支持,也可以編寫Polyfill或使用Babel之類的工具進行轉譯,讓我們可以將最新的特性應用到生產環境中(如下圖所示)。

          JavaScript標準制定流程.png

          而CSS就不同了,除了制定CSS標準規范所需的時間外,各家瀏覽器的版本、實戰進度差異更是曠日持久(如下圖所示),最多利用PostCSS、Sass等工具來幫我們轉譯出瀏覽器能接受的CSS。開發者們能操作的就是通過JS去控制DOMCSSOM來影響頁面的變化,但是對于接下來的LayoutPaintComposite就幾乎沒有控制權了。為了解決上述問題,為了讓CSS的魔力不在受到瀏覽器的限制,Houdini就此誕生。

          CSS 標準制定流程.png

          CSS Polyfill

          我們上文中提到JavaScript中進入提案中的特性我們可以編寫Polyfill,只需要很短的時間就可以講新特性投入到生產環境中。這時,腦海中閃現出的第一個想法就是CSS Polyfill,只要CSS的Polyfill 足夠強大,CSS或許也能有JavaScript一樣的發展速度,令人可悲的是編寫CSS Polyfill異常的困難,并且大多數情況下無法在不破壞性能的情況下進行。這是因為JavaScript是一門動態腳本語言[6]。它帶來了極強的擴展性,正是因為這樣,我們可以很輕松使用JavaScript做出JavaScript的Polyfill。但是CSS不是動態的,在某些場景下,我們可以在編譯時將一種形式的CSS的轉換成另一種(如PostCSS[7])。如果你的Polyfill依賴于DOM結構或者某一個元素的布局、定位等,那么我們的Polyfill就無法編譯時執行,而需要在瀏覽器中運行了。不幸的是,在瀏覽器中實現這種方案非常不容易。

          頁面渲染流程.png

          如上圖所示,是從瀏覽器獲取到HTML到渲染在屏幕上的全過程,我們可以看到只有帶顏色(粉色、藍色)的部分是JavaScript可以控制的環節。首先我們根本無法控制瀏覽器解析HTML與CSS并將其轉化為DOMCSSOM的過程,以及Cascade,Layout,Paint,Composite我們也無能為力。整個過程中我們唯一完全可控制的就是DOM,另外CSSOM部分可控。

          CSS Houdini草案中提到,這種程度的暴露是不確定的、兼容性不穩定的以及缺乏對關鍵特性的支持的。比如,在瀏覽器中的 CSSOM 是不會告訴我們它是如何處理跨域的樣式表,而且對于瀏覽器無法解析的 CSS 語句它的處理方式就是不解析了,也就是說——如果我們要用 CSS polyfill讓瀏覽器去支持它尚且不支持的屬性,那就不能在 CSSOM 這個環節做,我們只能遍歷一遍DOM,找到 <style><link rel="stylesheet"> 標簽,獲取其中的 CSS 樣式、解析、重寫,最后再加回 DOM 樹中。令人尷尬的是,這樣DOM樹全部刷新了,會導致頁面的重新渲染(如下如所示)。

          即便如此,有的人可能會說:“除了這種方法,我們也別無選擇,更何況對網站的性能也不會造成很大的影響”。那么對于部分網站是這樣的。但如果我們的Polyfill是需要對可交互的頁面呢?例如scrollresizemousemovekeyup等等,這些事件隨時會被觸發,那么意味著隨時都會導致頁面的重新渲染,交互不會像原本那樣絲滑,甚至導致頁面崩潰,對用戶的體驗也極其不好。

          綜上所述,如果我們想讓瀏覽器解析它不認識的樣式(低版本瀏覽器使用grid布局),然而渲染流程我們無法介入,我們也只能通過手動更新DOM的方式,這樣會帶來很多問題,Houdini的出現正是致力于解決他們。

          Houdini API

          Houdini是一組底層API,它公開了CSS引擎的各個部分,如下圖所示展示了每個環節對應的新API(灰色部分各大瀏覽器還未實現),從而使開發人員能夠通過加入瀏覽器渲染引擎的樣式和布局過程來擴展CSS。Houdini是一群來自Mozilla,Apple,Opera,Microsoft,HP,Intel和Google的工程師組成的工作小組設計而成的。它們使開發者可以直接訪問CSS對象模型(CSSOM),使開發人員可以編寫瀏覽器可以解析為CSS的代碼,從而創建新的CSS功能,而無需等待它們在瀏覽器中本地實現。

          CSS Houdini-API

          Properties & Values API

          盡管當前已經有了CSS變量,可以讓開發者控制屬性值,但是無法約束類型或者更嚴格的定義,CSS Houdini新的API,我們可以擴展css的變量,我們可以定義CSS變量的類型,初始值,繼承。它是css變量更強大靈活。

          CSS變量現狀:

          .dom {
            --my-color: green;
            --my-color: url('not-a-color'); // 它并不知道當前的變量類型
            color: var(--my-color);
          }
          
          

          Houdini提供了兩種自定義屬性的注冊方式,分別是在js和css中。

          CSS.registerProperty({
            name: '--my-prop', // String 自定義屬性名
            syntax: '<color>', // String 如何去解析當前的屬性,即屬性類型,默認 *
            inherits: false, // Boolean 如果是true,子節點將會繼承
            initialValue: '#c0ffee', // String 屬性點初始值
          });
          

          我們還可以在css中注冊,也可以達到上面的效果

          @property --my-prop {
            syntax: '<color>';
            inherits: false;
            initial-value: #c0ffee;
          }
          

          這個API中最令人振奮人心的功能是自定義屬性上添加動畫,像這樣:transition: --multiplier 0.4s;,這個功能我們在前面介紹什么是js in css那個demo[8]用使用過。我們還可以使用+使syntax屬性支持一個或多個類型,也可以使用|來分割。更多syntax屬性值:

          屬性值描述<length>長度值<number>數字<percentage>百分比<length-percentage>長度或百分比,calc將長度和百分比組成的表達式<color>顏色<image>圖像<url>網址<integer>整數<angle>角度<time>時間<resolution>分辨率<transform-list>轉換函數<custom-ident>ident

          Worklets

          Worklets是渲染引擎的擴展,從概念上來講它類似于Web Workers[9],但有幾個重要的區別:

          1. 設計為并行,每個Worklets必須始終有兩個或更多的實例,它們中的任何一個都可以在被調用時運行
          2. 作用域較小,限制不能訪問全局作用域的API(Worklet的函數除外)
          3. 渲染引擎會在需要的時候調用他們,而不是我們手動調用

          Worklet是一個JavaScript模塊,通過調用worklet的addModule方法(它是個Promise)來添加。比如registerLayout,registerPaint, registerAnimator 我們都需要放在Worklet中

          //加載單個
          await demoWorklet.addModule('path/to/script.js');
          
          // 一次性加載多個worklet
          Promise.all([
            demoWorklet1.addModule('script1.js'),
            demoWorklet2.addModule('script2.js'),
          ]).then(results => {});
          
          registerDemoWorklet('name', class {
          
            // 每個Worklet可以定義要使用的不同函數
            // 他們將由渲染引擎在需要時調用
            process(arg) {
              return !arg;
            }
          });
          
          

          Worklets的生命周期

          Worklets lifecycle

          1. Worklet的生命周期從渲染引擎內開始
          2. 對于JavaScript,渲染引擎啟動JavaScript主線程
          3. 然后他將啟動多個worklet進程,并且可以運行。這些進程理想情況下是獨立于主線程的線程,這樣就不會阻塞主線程(但它們也不需要阻塞)
          4. 然后在主線程中加載我們瀏覽器的JavaScript
          5. 該JavaScript調用 worklet.addModule 并異步加載一個worklet
          6. 加載后,將worklet加載到兩個或多個可用的worklet流程中
          7. 當需要時,渲染引擎將通過從加載的Worklet中調用適當的處理函數來執行Worklet。該調用可以針對任何并行的Worklet實例。

          Typed OM

          Typed OM是對現有的CSSOM的擴展,并實現 Parsing APIProperties & Values API相關的特性。它將css值轉化為有意義類型的JavaScript的對象,而不是像現在的字符串。如果我們嘗試將字符串類型的值轉化為有意義的類型并返回可能會有很大的性能開銷,因此這個API可以讓我們更高效的使用CSS的值。

          現在讀取CSS值增加了新的基類CSSStyleValue,他有許多的子類可以更加精準的描述css值的類型:

          子類描述CSSKeywordValueCSS關鍵字和其他標識符(如inherit或grid)CSSPositionValue位置信息 (x,y)CSSImageValue表示圖像的值屬性的對象CSSUnitValue表示為具有單個單位的單個值(例如50px),也可以表示為沒有單位的單個值或百分比CSSMathValue比較復雜的數值,比如有calc,min和max。這包括子類 CSSMathSum, CSSMathProduct, CSSMathMin,CSSMathMax, CSSMathNegate 和 CSSMathInvertCSSTransformValue由CSS transforms組成的CSSTransformComponent列表,其中包括CSSTranslate, CSSRotate, CSSScale, CSSSkew, CSSSkewX, CSSSkewY, CSSPerspective 和 CSSMatrixComponent

          使用Typed OM主要有兩種方法:

          1. 通過attributeStyleMap設置和獲取有類型的行間樣式
          2. 通過computedStyleMap獲取元素完整的Typed OM樣式

          使用attributeStyleMap設置并獲取

          myElement.attributeStyleMap.set('font-size', CSS.em(2));
          myElement.attributeStyleMap.get('font-size'); // CSSUnitValue { value: 2, unit: 'em' }
          
          myElement.attributeStyleMap.set('opacity', CSS.number(.5));
          myElement.attributeStyleMap.get('opacity'); // CSSUnitValue { value: 0.5, unit: 'number' };
          

          在線demo[10]

          使用computedStyleMap

          .foo {
            transform: translateX(1em) rotate(50deg) skewX(10deg);
            vertical-align: baseline;
            width: calc(100% - 3em);
          }
          
          const cs = document.querySelector('.foo').computedStyleMap();
          
          cs.get('vertical-align');
          // CSSKeywordValue {
          //  value: 'baseline',
          // }
          
          cs.get('width');
          // CSSMathSum {
          //   operator: 'sum',
          //   length: 2,
          //   values: CSSNumericArray {
          //     0: CSSUnitValue { value: -90, unit: 'px' },
          //     1: CSSUnitValue { value: 100, unit: 'percent' },
          //   },
          // }
          
          cs.get('transform');
          // CSSTransformValue {
          //   is2d: true,
          //   length: 3,
          //   0: CSSTranslate {
          //     is2d: true,
          //     x: CSSUnitValue { value: 20, unit: 'px' },
          //     y: CSSUnitValue { value: 0, unit: 'px' },
          //     z: CSSUnitValue { value: 0, unit: 'px' },
          //   },
          //   1: CSSRotate {...},
          //   2: CSSSkewX {...},
          // }
          

          Layout API

          開發者可以通過這個API實現自己的布局算法,我們可以像原生css一樣使用我們自定義的布局(像display:flex, display:table)。在Masonry layout library[11] 上我們可以看到開發者們是有多想實現各種各樣的復雜布局,其中一些布局光靠 CSS 是不行的。雖然這些布局會讓人耳目一新印象深刻,但是它們的頁面性能往往都很差,在一些低端設備上性能問題猶為明顯。

          CSS Layout API 暴露了一個registerLayout方法給開發者,接收一個布局名(layout name)作為后面在 CSS中使用的屬性值,還有一個包含有這個布局邏輯的JavaScript類。

          my-div {
            display: layout(my-layout);
          }
          
          // layout-worklet.js
          registerLayout('my-layout', class {
            static get inputProperties() { return ['--foo']; }
            
            static get childrenInputProperties() { return ['--bar']; }
            
            async intrinsicSizes(children, edges, styleMap) {}
          
            async layout(children, edges, constraints, styleMap) {}
          });
          
          await CSS.layoutWorklet.addModule('layout-worklet.js');
          

          目前瀏覽器大部分還不支持

          Painting API

          我們可以在CSS background-image中使用它,我們可以使用Canvas 2d上下文,根據元素的大小控制圖像,還可以使用自定義屬性。

          await CSS.paintWorklet.addModule('paint-worklet.js');
          
          registerPaint('sample-paint', class {
            static get inputProperties() { return ['--foo']; }
          
            static get inputArguments() { return ['<color>']; }
          
            static get contextOptions() { return {alpha: true}; }
          
            paint(ctx, size, props, args) { }
          });
          

          Animation API

          這個API讓我們可以控制基于用戶輸入的關鍵幀動畫,并且以非阻塞的方式。還能更改一個 DOM 元素的屬性,不過是不會引起渲染引擎重新計算布局或者樣式的屬性,比如 transform、opacity 或者滾動條位置(scroll offset)。Animation API的使用方式與 Paint APILayout API略有不同我們還需要通過new一個WorkletAnimation來注冊worklet。

          // animation-worklet.js
          registerAnimator('sample-animator', class {
            constructor(options) {
            }
            animate(currentTime, effect) {
              effect.localTime = currentTime;
            }
          });
          
          await CSS.animationWorklet.addModule('animation-worklet.js');
          
          // 需要添加動畫的元素
          const elem = document.querySelector('#my-elem');
          const scrollSource = document.scrollingElement;
          const timeRange = 1000;
          const scrollTimeline = new ScrollTimeline({
            scrollSource,
            timeRange,
          });
          
          const effectKeyframes = new KeyframeEffect(
            elem,
            // 動畫需要綁定的關鍵幀
            [
              {transform: 'scale(1)'},
              {transform: 'scale(.25)'},
              {transform: 'scale(1)'}
            ],
            {
              duration: timeRange,
            },
          );
          new WorkletAnimation(
            'sample-animator',
            effectKeyframes,
            scrollTimeline,
            {},
          ).play();
          

          關于此API的更多內容:(https://github.com/w3c/css-houdini-drafts/tree/main/css-animation-worklet-1)

          Parser API

          允許開發者自由擴展 CSS 詞法分析器。

          解析規則:

          const background = window.cssParse.rule("background: green");
          console.log(background.styleMap.get("background").value) // "green"
          
          const styles = window.cssParse.ruleSet(".foo { background: green; margin: 5px; }");
          console.log(styles.length) // 5
          console.log(styles[0].styleMap.get("margin-top").value) // 5
          console.log(styles[0].styleMap.get("margin-top").type) // "px"
          

          解析CSS:

          const style = fetch("style.css")
                  .then(response => CSS.parseStylesheet(response.body));
          style.then(console.log);
          

          Font Metrics API

          它將提供一些方法來測量在屏幕上呈現的文本元素的尺寸,將允許開發者控制文本元素在屏幕上呈現的方式。使用當前功能很難或無法測量這些值,因此該API將使開發者可以更輕松地創建與文本和字體相關的CSS特性。例如:

          • flex布局: align-items baseline特性。需要知道每一個flex盒子中第一個元素的基線位置。
          • 首字母: 需要知道每個字母的基線高度和字母最大的高度,以及換行內容的基線長度。
          • 單個字形的前進和后退。
          • 換行: 需要訪問字體數據,文本的所有樣式輸入以及布局信息(可用的段落長度等)。
          • 元素中的每一個line boxes都需要一個基線。(line boxes代表包含眾多inline boxes的這行)

          Houdini 目前進展

          Is Houdini ready yet

          (https://ishoudinireadyyet.com/)

          Houdini 的藍圖

          了解到這里,部分開發者可能會說:“我不需要這些花里胡哨的技術,并不能帶收益。我只想簡簡單單的寫幾個頁面,做做普通的Web App,并不想試圖干預瀏覽器的渲染過程從而實現一些實驗性或炫酷的功能。”如果這樣想的話,我們不妨退一步再去思考。回憶下最近做過的項目,用于實現頁面效果所使用到的技術,grid布局方式在考慮兼容老版本瀏覽器時也不得不放棄。我們想控制瀏覽器渲染頁面的過程并不是僅僅為了炫技,更多的是為了幫助開發者們解決以下兩個問題:

          1. 統一各大瀏覽器的行為
          2. JavaScript一樣,在推出新的特性時,我們可以通過Polyfill的形式快速的投入生產環境中。

          幾年過后再回眸,當主流瀏覽器完全支持Houdini的時候。我們可以在瀏覽器上隨心所欲的使用任何CSS屬性,并且他們都能完美支持。像今天的grid布局在舊版本瀏覽器支持的并不友好的這類問題,那時我們只需要安裝對應的Polyfill就能解決類似的問題。

          檔對象模型(DOM)

          JS 有很多地方讓咱們吐槽,但沒那么糟糕。作為一種在瀏覽器中運行的腳本語言,它對于處理web頁面非常有用。在本中,我們將看到我們有哪些方法來交互和修改HTML文檔及其元素。但首先讓我們來揭開文檔對象模型的神秘面紗。

          文檔對象模型是一個基本概念,它是咱們在瀏覽器中所做的一切工作的基礎。但那到底是什么? 當咱們訪問一個 web 頁面時,瀏覽器會指出如何解釋每個 HTML 元素。這樣它就創建了 HTML 文檔的虛擬表示,并保存在內存中。HTML 頁面被轉換成樹狀結構,每個 HTML 元素都變成一個葉子,連接到父分支。考慮這個簡單的HTML 頁面:

          <!DOCTYPE html>
          <html lang="en">
          <head>
           <title>A super simple title!</title>
          </head>
          <body>
          <h1>A super simple web page!</h1>
          </body>
          </html
          

          當瀏覽器掃描上面的 HTML 時,它創建了一個文檔對象模型,它是HTML結構的鏡像。在這個結構的頂部有一個 document 也稱為根元素,它包含另一個元素:html。html 元素包含一個 head,head 又有一個 title。然后是含有 h1的 body。每個 HTML 元素由特定類型(也稱為接口)表示,并且可能包含文本或其他嵌套元素

          document (HTMLDocument)
           |
           | --> html (HTMLHtmlElement)
           | 
           | --> head (HtmlHeadElement)
           | |
           | | --> title (HtmlTitleElement)
           | | --> text: "A super simple title!"
           |
           | --> body (HtmlBodyElement)
           | |
           | | --> h1 (HTMLHeadingElement)
           | | --> text: "A super simple web page!"
          

          每個 HTML 元素都是從 Element 派生而來的,但是它們中的很大一部分是進一步專門化的。咱們可以檢查原型,以查明元素屬于什么“種類”。例如,h1 元素是 HTMLHeadingElement

          document.quertSelector('h1').__proto__
          // 輸出:HTMLHeadingElement
          

          HTMLHeadingElement 又是 HTMLElement 的“后代”

          document.querySelector('h1').__proto__.__proto__
          // Output: HTMLElement
          
          Element 是一個通用性非常強的基類,所有 Document 對象下的對象都繼承自它。這個接口描述了所有相同種類的元素所普遍具有的方法和屬性。一些接口繼承自 Element 并且增加了一些額外功能的接口描述了具體的行為。例如, HTMLElement 接口是所有 HTML 元素的基本接口,而 SVGElement 接口是所有 SVG 元素的基礎。大多數功能是在這個類的更深層級(hierarchy)的接口中被進一步制定的。

          在這一點上(特別是對于初學者),document 和 window 之間可能有些混淆。window 指的是瀏覽器,而 document 指的是當前的 HTML 頁面。window 是一個全局對象,可以從瀏覽器中運行的任何 JS 代碼直接訪問它。它不是 JS 的“原生”對象,而是由瀏覽器本身公開的。window 有很多屬性和方法,如下所示:

          window.alert('Hello world'); // Shows an alert
          window.setTimeout(callback, 3000); // Delays execution
          window.fetch(someUrl); // makes XHR requests
          window.open(); // Opens a new tab
          window.location; // Browser location
          window.history; // Browser history
          window.navigator; // The actual device
          window.document; // The current page
          

          由于這些屬性是全局屬性,因此也可以省略 window:

          alert('Hello world'); // Shows an alert
          setTimeout(callback, 3000); // Delays execution
          fetch(someUrl); // makes XHR requests
          open(); // Opens a new tab
          location; // Browser location
          history; // Browser history
          navigator; // The actual device
          document; // The current page
          

          你應該已經熟悉其中的一些方法,例如 setTimeout()或 window.navigator,它可以獲取當前瀏覽器使用的語言:

          if (window.navigator) {
           var lang = window.navigator.language;
           if (lang === "en-US") {
           // show something
           }
           if (lang === "it-IT") {
           // show something else
           }
          }
          

          要了解更多 window 上的方法,請查看MDN文檔。在下一節中,咱們深入地研究一下 DOM

          節點、元素 和DOM 操作

          document 接口有許多實用的方法,比如 querySelector(),它是用于選擇當前 HTML 頁面內的任何 HTML 元素:

          document.querySelector('h1');
          


          window 表示當前窗口的瀏覽器,下面的指令與上面的相同:

          window.document.querySelector('h1');
          


          不管怎樣,下面的語法更常見,在下一節中咱們將大量使用這種形式:

          document.methodName();
          


          除了 querySelector() 用于選擇 HTML 元素之外,還有很多更有用的方法

          // 返回單個元素
          document.getElementById('testimonials'); 
          // 返回一個 HTMLCollection
          document.getElementsByTagName('p'); 
          // 返回一個節點列表
          document.querySelectorAll('p');
          

          咱們不僅可以選 擇HTML 元素,還可以交互和修改它們的內部狀態。例如,希望讀取或更改給定元素的內部內容:

          // Read or write
          document.querySelector('h1').innerHtml; // Read
          document.querySelector('h1').innerHtml = ''; // Write! Ouch!
          

          DOM 中的每個 HTML 元素也是一個“節點”,實際上咱們可以像這樣檢查節點類型:

          document.querySelector('h1').nodeType;
          


          上述結果返回 1,表示是 Element 類型的節點的標識符。咱們還可以檢查節點名:

          document.querySelector('h1').nodeName;
          "H1"
          

          這里,節點名以大寫形式返回。通常我們處理 DOM 中的四種類型的節點

          • document: 根節點(nodeType 9)
          • 類型為Element的節點:實際的HTML標簽(nodeType 1),例如 <p> 和 <div>
          • 類型屬性的節點:每個HTML元素的屬性(屬性)
          • Text 類型的節點:元素的實際文本內容(nodeType 3)

          由于元素是節點,節點可以有屬性(properties )(也稱為attributes),咱們可以檢查和操作這些屬性:

          // 返回 true 或者 false
          document.querySelector('a').hasAttribute('href');
          // 返回屬性文本內容,或 null
          document.querySelector('a').getAttribute('href');
          // 設置給定的屬性
          document.querySelector('a').setAttribute('href', 'someLink');
          

          前面我們說過 DOM 是一個類似于樹的結構。這種特性也反映在 HTML 元素上。每個元素都可能有父元素和子元素,我們可以通過檢查元素的某些屬性來查看它們:

          // 返回一個 HTMLCollection
          document.children;
          // 返回一個節點列表
          document.childNodes;
          // 返回一個節點
          document.querySelector('a').parentNode;
          // 返回HTML元素
          document.querySelector('a').parentElement;
          

          了解了如何選擇和查詢 HTML 元素。那創建元素又是怎么樣?為了創建 Element 類型的新節點,原生 DOM API 提供了 createElement 方法:

          var heading = document.createElement('h1');
          


          使用 createTextNode 創建文本節點:

          var text = document.createTextNode('Hello world');
          


          通過將 text 附加到新的 HTML 元素中,可以將這兩個節點組合在一起。最后,還可以將heading元素附加到根文檔中:

          var heading = document.createElement('h1');
          var text = document.createTextNode('Hello world');
          heading.appendChild(text);
          document.body.appendChild(heading);
          

          還可以使用 remove() 方法從 DOM 中刪除節點。在元素上調用方法,該節點將從頁面中消失:

          document.querySelector('h1').remove();
          


          這些是咱們開始在瀏覽器中使用 JS 操作 DOM 所需要知道的全部內容。在下一節中,咱們將靈活地使用 DOM,但首先要繞個彎,因為咱們還需要討論“DOM事件”

          DOM 和事件

          DOM 元素是很智能的。它們不僅可以包含文本和其他 HTML 元素,還可以“發出”和響應“事件”。瀏覽任何網站并打開瀏覽器控制臺。使用以下命令選擇一個元素:

          document.querySelector('p')
          


          看看這個屬性

          document.querySelector('p').onclick
          


          它是什么類型:

          typeof document.querySelector('p').onclick // "object"
          


          "object"! 為什么它被稱為“onclick”? 憑一點直覺我們可以想象它是元素上的某種神奇屬性,能夠對點擊做出反應? 完全正確。

          如果你感興趣,可以查看任何 HTML 元素的原型鏈。會發現每個元素也是一個 Element,而元素又是一個節點,而節點又是一個EventTarget。可以使用 instanceof 來驗證這一點。

          document.querySelector('p') instanceof EventTarget // true
          


          我很樂意稱 EventTarget 為所有 HTML 元素之父,但在JS中沒有真正的繼承,它更像是任何 HTML 元素都可以看到另一個連接對象的屬性。因此,任何 HTML 元素都具有與 EventTarget相同的特性:發布事件的能力

          但事件到底是什么呢?以 HTML 按鈕為例。如果你點擊它,那就是一個事件。有了這個.onclick對象,咱們可以注冊事件,只要元素被點擊,它就會運行。傳遞給事件的函數稱為“事件監聽器”“事件句柄”

          事件和監聽

          在 DOM 中注冊事件監聽器有三種方法。第一種形式比較陳舊,應該避免,因為它耦合了邏輯操作和標簽

          <!-- 不好的方式 -->
          <button onclick="console.log('clicked')">喜歡,就點點我</button>
          

          第二個選項依賴于以事件命名的對象。例如,咱們可以通過在對象.onclick上注冊一個函數來監聽click事件:

          document.querySelector("button").onclick = handleClick;
          function handleClick() {
           console.log("Clicked!");
          }
          

          此語法更加簡潔,是內聯處理程序的不錯替代方案。還有另一種基于addEventListener的現代形式:

          document.querySelector("button").addEventListener("click", handleClick);
          function handleClick() {
           console.log("Clicked!");
          }
          

          就我個人而言,我更喜歡這種形式,但如果想爭取最大限度的瀏覽器兼容性,請使用 .on 方式。現在咱們已經有了一 個 HTML 元素和一個事件監聽器,接著進一步研究一下 DOM 事件。

          事件對象、事件默認值和事件冒泡

          作為事件處理程序傳遞的每個函數默認接收一個名為“event”的對象

          var button = document.querySelector("button");
          button.addEventListener("click", handleClick);
          function handleClick() {
           console.log(event);
          }
          

          它可以直接在函數體中使用,但是在我的代碼中,我更喜歡將它顯式地聲明為參數:

          function handleClick(event) {
           console.log(event);
          }
          

          事件對象是“必須要有的”,因為咱們可以通過調用事件上的一些方法來控制事件的行為。事件實際上有特定的特征,尤其是“默認”“冒泡”。考慮一 個HTML 鏈接。使用以下標簽創建一個名為click-event.html的新HTML文件:

          <!DOCTYPE html>
          <html lang="en">
          <head>
           <meta charset="UTF-8">
           <title>Click event</title>
          </head>
          <body>
          <div>
           <a href="/404.html">click me!</a>
          </div>
          </body>
          <script src="click-event.js"></script>
          </html>
          

          在瀏覽器中運行該文件并嘗試單擊鏈接。它將跳轉到一個404的界面。鏈接上單擊事件的默認行為是轉到href屬性中指定的實際頁面。但如果我告訴你有辦法阻止默認值呢?輸入preventDefault(),該方法可用于事件對象。使用以下代碼創建一個名為click-event.js的新文件:

          var button = document.querySelector("a");
          button.addEventListener("click", handleClick);
          function handleClick(event) {
           event.preventDefault();
          }
          

          在瀏覽器中刷新頁面并嘗試現在單擊鏈接:它不會跳轉了。因為咱們阻止了瀏覽器的“事件默認” 鏈接不是默認操作的惟一HTML 元素,表單具有相同的特性。

          當 HTML 元素嵌套在另一個元素中時,還會出現另一個有趣的特性。考慮以下 HTML

          <!DOCTYPE html>
          <html lang="en">
          <head>
           <meta charset="UTF-8">
           <title>Nested events</title>
          </head>
          <body>
          <div id="outer">
           I am the outer div
           <div id="inner">
           I am the inner div
           </div>
          </div>
          </body>
          <script src="nested-events.js"></script>
          </html>
          

          和下面的 JS 代碼:

          // nested-events.js
          var outer = document.getElementById('inner');
          var inner = document.getElementById('outer');
          function handleClick(event){
           console.log(event);
          }
          inner.addEventListener('click', handleClick);
          outer.addEventListener('click', handleClick);
          

          有兩個事件監聽器,一個用于外部 div,一個用于內部 div。準確地點擊內部div,你會看到:

          兩個事件對象被打印。這就是事件冒泡在起作用。它看起來像是瀏覽器行為中的一個 bug,使用 stopPropagation() 方法可以禁用,這也是在事件對象上調用的

          //
          function handleClick(event) {
           event.stopPropagation();
           console.log(event);
          }
          ///
          

          盡管看起來實現效果很差,但在注冊過多事件監聽器確實對性能不利的情況下,冒泡還是會讓人眼前一亮。考慮以下示例:

          <!DOCTYPE html>
          <html lang="en">
          <head>
           <meta charset="UTF-8">
           <title>Event bubbling</title>
          </head>
          <body>
          <ul>
           <li>one</li>
           <li>two</li>
           <li>three</li>
           <li>four</li>
           <li>five</li>
          </ul>
          </body>
          <script src="event-bubbling.js"></script>
          </html>
          

          如果要兼聽列表的點擊事件,需要在列表中注冊多少事件監聽器?答案是:一個。只需要一個在ul上注冊的偵聽器就可以截獲任何li上的所有單擊:

          // event-bubbling.js
          var ul = document.getElementsByTagName("ul")[0];
          function handleClick(event) {
           console.log(event);
          }
          ul.addEventListener("click", handleClick);
          

          可以看到,事件冒泡是提高性能的一種實用方法。實際上,對瀏覽器來說,注冊事件監聽器是一項昂貴的操作,而且在出現大量元素列表的情況下,可能會導致性能損失。

          用 JS 生成表格

          現在咱們開始編碼。給定一個對象數組,希望動態生成一個HTML 表格。HTML 表格由 <table> 元素表示。每個表也可以有一個頭部,由 <thead> 元素表示。頭部可以有一個或多個行 <tr>,每個行都有一個單元格,由一個 <th>元 素表示。如下所示:

          <table>
           <thead>
           <tr>
           <th>name</th>
           <th>height</th>
           <th>place</th>
           </tr>
           </thead>
           <!-- more stuff here! -->
          </table>
          

          不止這樣,大多數情況下,每個表都有一個主體,由 <tbody> 定義,而 <tbody> 又包含一組行<tr>。每一行都可以有包含實際數據的單元格。表單元格由<td>定義。完整如下所示:

          <table>
           <thead>
           <tr>
           <th>name</th>
           <th>height</th>
           <th>place</th>
           </tr>
           </thead>
           <tbody>
           <tr>
           <td>Monte Falco</td>
           <td>1658</td>
           <td>Parco Foreste Casentinesi</td>
           </tr>
           <tr>
           <td>Monte Falterona</td>
           <td>1654</td>
           <td>Parco Foreste Casentinesi</td>
           </tr>
           </tbody>
          </table>
          

          現在的任務是從 JS 對象數組開始生成表格。首先,創建一個名為build-table.html的新文件,內容如下:

          <!DOCTYPE html>
          <html lang="en">
          <head>
           <meta charset="UTF-8">
           <title>Build a table</title>
          </head>
          <body>
          <table>
          <!-- here goes our data! -->
          </table>
          </body>
          <script src="build-table.js"></script>
          </html>
          

          在相同的文件夾中創建另一個名為build-table.js的文件,并使用以下數組開始:

          "use strict";
          var mountains = [
           { name: "Monte Falco", height: 1658, place: "Parco Foreste Casentinesi" },
           { name: "Monte Falterona", height: 1654, place: "Parco Foreste Casentinesi" },
           { name: "Poggio Scali", height: 1520, place: "Parco Foreste Casentinesi" },
           { name: "Pratomagno", height: 1592, place: "Parco Foreste Casentinesi" },
           { name: "Monte Amiata", height: 1738, place: "Siena" }
          ];
          

          考慮這個表格。首先,咱們需要一個 <thead>:

          document.createElement('thead')
          


          這沒有錯,但是仔細查看MDN的表格文檔會發現一個有趣的細節。<table> 是一個 HTMLTableElement,它還包含有趣方法。其中最有用的是HTMLTableElement.createTHead(),它可以幫助創建咱們需要的 <thead>。

          首先,編寫一個生成 thead 標簽的函數 generateTableHead

          function generateTableHead(table) {
           var thead = table.createTHead();
          }
          

          該函數接受一個選擇器并在給定的表上創建一個 <thead>:

          function generateTableHead(table) {
           var thead = table.createTHead();
          }
          var table = document.querySelector("table");
          generateTableHead(table);
          

          在瀏覽器中打開 build-table.html:什么都沒有.但是,如果打開瀏覽器控制臺,可以看到一個新的 <thead> 附加到表。

          接著填充 header 內容。首先要在里面創建一行。還有另一個方法可以提供幫助:HTMLTableElement.insertRow()。有了這個,咱們就可以擴展方法了:

          function generateTableHead (table) {
           var thead = table,createThead();
           var row = thead.insertRow();
          }
          

          此時,我們可以生成我們的行。通過查看源數組,可以看到其中的任何對象都有咱們需要信息:

          var mountains = [
           { name: "Monte Falco", height: 1658, place: "Parco Foreste Casentinesi" },
           { name: "Monte Falterona", height: 1654, place: "Parco Foreste Casentinesi" },
           { name: "Poggio Scali", height: 1520, place: "Parco Foreste Casentinesi" },
           { name: "Pratomagno", height: 1592, place: "Parco Foreste Casentinesi" },
           { name: "Monte Amiata", height: 1738, place: "Siena" }
          ];
          

          這意味著咱們可以將另一個參數傳遞給我們的函數:一個遍歷以生成標題單元格的數組:

          function generateTableHead(table, data) {
           var thead = table.createTHead();
           var row = thead.insertRow();
           for (var i = 0; i < data.length; i++) {
           var th = document.createElement("th");
           var text = document.createTextNode(data[i]);
           th.appendChild(text);
           row.appendChild(th);
           }
          }
          

          不幸的是,沒有創建單元格的原生方法,因此求助于document.createElement("th")。同樣值得注意的是,document.createTextNode(data[i])用于創建文本節點,appendChild()用于向每個標記添加新元素。

          當以這種方式創建和操作元素時,我們稱之為“命令式” DOM 操作。現代前端庫通過支持“聲明式”方法來解決這個問題。我們可以聲明需要哪些 HTML 元素,而不是一步一步地命令瀏覽器,其余的由庫處理。

          回到我們的代碼,可以像下面這樣使用第一個函數

          var table = document.querySelector("table");
          var data = Object.keys(mountains[0]);
          generateTableHead(table, data);
          

          現在我們可以進一步生成實際表的數據。下一個函數將實現一個類似于generateTableHead的邏輯,但這一次咱們需要兩個嵌套的for循環。在最內層的循環中,使用另一種原生方法來創建一系列td。方法是HTMLTableRowElement.insertCell()。在前面創建的文件中添加另一個名為generateTable的函數

          function generateTable(table, data) {
           for (var i = 0; i < data.length; i++) {
           var row = table.insertRow();
           for (var key in data[i]) {
           var cell = row.insertCell();
           var text = document.createTextNode(data[i][key]);
           cell.appendChild(text);
           }
           }
          }
          

          調用上面的函數,將 HTML表 和對象數組作為參數傳遞:

          generateTable(table, mountains);
          


          咱們深入研究一下 generateTable 的邏輯。參數 data 是一個與 mountains 相對應的數組。最外層的 for 循環遍歷數組并為每個元素創建一行:

          function generateTable(table, data) {
           for (var i = 0; i < data.length; i++) {
           var row = table.insertRow();
           // omitted for brevity
           }
          }
          

          最內層的循環遍歷任何給定對象的每個鍵,并為每個對象創建一個包含鍵值的文本節點

          function generateTable(table, data) {
           for (var i = 0; i < data.length; i++) {
           var row = table.insertRow();
           for (var key in data[i]) {
           // inner loop
           var cell = row.insertCell();
           var text = document.createTextNode(data[i][key]);
           cell.appendChild(text);
           }
           }
          }
          

          最終代碼:

          var mountains = [
           { name: "Monte Falco", height: 1658, place: "Parco Foreste Casentinesi" },
           { name: "Monte Falterona", height: 1654, place: "Parco Foreste Casentinesi" },
           { name: "Poggio Scali", height: 1520, place: "Parco Foreste Casentinesi" },
           { name: "Pratomagno", height: 1592, place: "Parco Foreste Casentinesi" },
           { name: "Monte Amiata", height: 1738, place: "Siena" }
          ];
          function generateTableHead(table, data) {
           var thead = table.createTHead();
           var row = thead.insertRow();
           for (var i = 0; i < data.length; i++) {
           var th = document.createElement("th");
           var text = document.createTextNode(data[i]);
           th.appendChild(text);
           row.appendChild(th);
           }
          }
          function generateTable(table, data) {
           for (var i = 0; i < data.length; i++) {
           var row = table.insertRow();
           for (var key in data[i]) {
           var cell = row.insertCell();
           var text = document.createTextNode(data[i][key]);
           cell.appendChild(text);
           }
           }
          }
          

          其中調用:

          var table = document.querySelector("table");
          var data = Object.keys(mountains[0]);
          generateTable(table, mountains);
          generateTableHead(table, data);
          

          執行結果:


          當然,咱們的方法還可以該進,下個章節將介紹。

          總結

          DOM 是 web 瀏覽器保存在內存中的 web 頁面的虛擬副本。DOM 操作是指從 DOM 中創建、修改和刪除 HTML 元素的操作。在過去,咱們常常依賴 jQuery 來完成更簡單的任務,但現在原生 API 已經足夠成熟,可以讓 jQuery 過時了。另一方面,jQuery 不會很快消失,但是每個 JS 開發人員都必須知道如何使用原生 API 操作 DOM。

          這樣做的理由有很多,額外的庫增加了加載時間和 JS 應用程序的大小。更不用說 DOM 操作在面試中經常出現。

          DOM 中每個可用的 HTML 元素都有一個接口,該接口公開一定數量的屬性和方法。當你對使用何種方法有疑問時,參考MDN文檔。操作 DOM 最常用的方法是 document.createElement()用于創建新的 HTML 元素,document.createTextNode() 用于在 DOM 中創建文本節點。最后但同樣重要的是 .appendchild(),用于將新的 HTML 元素或文本節點附加到現有元素。

          HTML 元素還能夠發出事件,也稱為DOM事件。值得注意的事件為“click”、“submit”、“drag”、“drop”等等。DOM 事件有一些特殊的行為,比如“默認”和冒泡。

          JS 開發人員可以利用這些屬性,特別是對于事件冒泡,這些屬性對于加速 DOM 中的事件處理非常有用。雖然對原生 API 有很好的了解是件好事,但是現代前端庫提供了不容置疑的好處。用 Angular、React 和 Vue 來構建一個大型的JS應用程序確實是可行的。

          代碼部署后可能存在的BUG沒法實時知道,事后為了解決這些BUG,花了大量的時間進行log 調試,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug。

          原文:https://github.com/valentinogagliardi/Little-JavaScript-Book/blob/v1.0.0/manuscript/chapter8.md

          php js jquery功能片段#

          唯一需求:表格id。

          注意:因提交數據不能包含\n,所以要替換。呈現時可以替換回換行符。

          <script type="text/javascript">

          /**

          * 遍歷表格內容后返回數組

          * @param string id 表格id

          * @return Array 或者JSON

          */

          function getTableContent(id) {

          var mytable = document.getElementById(id);

          var data = [];

          var tmpTxt;

          for(var i = 0, rows = mytable.rows.length; i < rows; i++) {

          for(var j = 0, cells = mytable.rows[i].cells.length; j < cells; j++) {

          if(!data[i]) {

          data[i] = new Array();

          }

          tmpTxt = mytable.rows[i].cells[j].innerHTML;

          //tmpTxt = tmpTxt.replace(/<[^>]+>/gi,'');//過濾全部的html標簽,不包括內容

          //tmpTxt =tmpTxt.replace(/\s/gi,'');

          tmpTxt =tmpTxt.replace(/\n/gi,'<br>');

          //data[i][j] = tmpTxt;

          data[i][j] = tmpTxt.replace(/ /g, ""); //替換全角空格

          }

          }

          //var JSONdata=data;

          var JSONdata = JSON.stringify(data); //序列化數組JSON.stringify(data) 反序列化數組JSON.parse(data)

          return JSONdata;

          //return data;//返回數組

          }

          </script>


          主站蜘蛛池模板: 无码国产精品一区二区免费3p | 无码国产精品久久一区免费| 亚洲AV成人一区二区三区AV| 国产经典一区二区三区蜜芽| 肥臀熟女一区二区三区| 成人免费视频一区二区三区| 国产精品无码一区二区三区电影| 国产萌白酱在线一区二区| 亚洲一区免费在线观看| 99久久国产精品免费一区二区| 国产精品免费综合一区视频| 亚洲欧美日韩国产精品一区| 日韩人妻精品无码一区二区三区| 三上悠亚日韩精品一区在线| 国产成人精品无人区一区| 波多野结衣一区在线| 国产精品亚洲专区一区| 国模无码一区二区三区| 精品人妻少妇一区二区三区不卡| 极品尤物一区二区三区| 中文字幕一区在线观看视频| 久久毛片一区二区| 无码精品尤物一区二区三区| 亚洲av日韩综合一区在线观看| 大伊香蕉精品一区视频在线 | 91福利国产在线观看一区二区| 色欲AV无码一区二区三区| 国产无人区一区二区三区| 日本一区二区三区日本免费| 理论亚洲区美一区二区三区| 国产日韩精品一区二区在线观看| 国产成人一区二区三区免费视频| 国模大胆一区二区三区| 国产一区二区三区日韩精品| 日本精品一区二区三区在线视频一 | 精品无码一区二区三区爱欲| 亚洲综合一区二区国产精品| 国产手机精品一区二区| 婷婷亚洲综合一区二区| 精品一区二区无码AV| 国产激情з∠视频一区二区|