整合營銷服務商

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

          免費咨詢熱線:

          聽說過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就能解決類似的問題。

          前一陣兒被某網站的 JS 反爬流程難住了,至今也沒明白它的反扒原理和攻破方法。最終找到了一個自動化腳本工具 autoit 3,用一個笨方法將人手動點擊瀏覽器的動作腳本化,達到網頁數據獲取目的,拿到網頁文件后,再用代碼解析,曲線完成任務。

          本文將介紹這個自動化的過程,并帶編寫一個完整的 autoit 3 爬蟲腳本,希望對各位讀者朋友有所啟發。

          自動化操作分析

          以國家信息安全漏洞共享平臺為例,它在返回數據前發起了兩次 512 響應,第三次瀏覽器帶著動態生成的 Cookie 信息才能得到數據。

          這次咱們直接從網頁入手,操作鍵盤找到“下一頁” 按鈕,按下 Enter 鍵完全請求。通過鍵盤定位到 “下頁” 按鈕的過程為:

          1. 第一步,按下 “End” 鍵,到達網頁底部;
          2. 第二步,反向 “Tab” 鍵,按 15 次就可定位到 “下頁” 按鈕。

          接著就可以編寫自動化腳本了,把剛剛的手動操作翻譯成腳本命令:

          1. 切換為英文輸入法,保證瀏覽器輸入欄信息正確;
          2. 打開 Chrome 瀏覽器;
          3. 向瀏覽器地址欄輸入目標 URL;
          4. 按下 Enter 鍵,等待 2 秒保證頁面數據加載完成;
          5. 按下 Ctrl +S 鍵,并向存儲路徑發送存儲文件名稱,等待“保存”操作完成;
          6. 按下 End 鍵盤,定位到頁面底部;
          7. 按下反向 Tab 鍵 15 次,定位到 “下頁” 按鈕;
          8. 按下 Enter 鍵,請求下一頁數據;
          9. 循環 5-8 這個過程 N 次,N=需要爬的頁數。

          這個流程,對其他高反扒的信息發布網站,也是適用的。

          編寫自動化腳本

          按照上面的流程,編寫 autoit 自動化腳本,創建一個 myspider.au3 文件:

          #include <AutoItConstants.au3>
          
          ;;切換為英文輸入法,保證瀏覽器輸入正常
          $hWnd = WinGetHandle("[ACTIVE]");$hWnd 為目標窗口句柄,這里設置的是當前活動窗口
          $ret = DllCall("user32.dll", "long", "LoadKeyboardLayout", "str", "08040804", "int", 1 + 0)
          DllCall("user32.dll", "ptr", "SendMessage", "hwnd", $hWnd, "int", 0x50, "int", 1, "int", $ret[0])
          
          $url = "https://www.cnvd.org.cn/flaw/list.htm"
          spiderData($url)
          
          Func spiderData($url)
          	;;打開 Chrome 瀏覽器窗口
          	$chromePath = "C:\Users\admin\AppData\Local\Google\Chrome\Application\chrome.exe"
          	Run($chromePath)
          
          	;;登錄窗口顯示
          	WinWaitActive("[CLASS:Chrome_WidgetWin_1]")
          	;; 休息2000毫秒
          	Sleep(2000)
          	;; 移動窗口
          	WinMove("[CLASS:Chrome_WidgetWin_1]", "打開新的標簽頁 - Google Chrome", 0, 0,1200,740,2)
          
          	;; 休息500毫秒
          	Sleep(500)
          
          	;;地址欄輸入URL 并按下 Enter 鍵
          	Send($url)
          	Sleep(500)
          	Send("{enter}")
          	Sleep(3000)
          
          	;; 循環爬取需要的頁數,測試只爬3頁
          	For $i = 1 To 3 Step 1
          		;;打開右鍵另存為按鈕: Ctrl+S
          		send("^s")
          		Sleep(2000)
          		WinWait("[CLASS:#32770]","",10)
          
          		;;將存儲路徑設置到另存為組件輸入框 Edit1 里
          		$timeNow = @YEAR & "" & @MON & "" & @MDAY & "" & @HOUR & "" & @MIN
          		$savePath = "F:\A2021Study\ListData\" &$timeNow &  "_page" & $i & ".html"
          		ControlSetText("另存為","", "Edit1", $savePath)
          
          		;;點擊確定
          		ControlClick("另存為","","Button2")
          
          		;;再次確定
          		WinWait("[CLASS:#32770]","",10)
          		ControlClick("確認另存為","","Button1")
          
          		;; 等待保存操作完成
          		Sleep(3000)
          
          		;; 定位到下一頁按鈕,并觸發點擊下一頁
          		send("{END}")
          		Send("+{TAB 15}")
          		Send("{enter}")
          
          		;;點擊確定后,等待網頁加載完成
          		Sleep(3000)
          	Next
          
          	;; 整個操作完成,則關閉瀏覽器
          	Send("^w")
          EndFunc

          腳本編寫過程中,有幾點需要注意:

          • 第一,輸入法切換很重要,否則 URL 地址欄的值很容易亂;
          • 第二, windows 的文件路徑是反斜杠 \ ,否則會導致另存為的路徑無法識別;
          • 第三,幫助文檔里面提供的關閉方法是 WinClose ,但是反復測試,確定這個方法不靠譜,要么會引起瀏覽器異常關閉導致下次打開會恢復上次的網址;要么完全不生效。迂回的解決辦法是用關閉按鍵 Ctrl+W ,完成了正常關閉的目的。

          因為爬蟲要作為定時任務運行的,為避免打開太多瀏覽器窗口,因此需要在腳本結束時關閉瀏覽器。

          啟示錄

          數據爬取一般分為列表頁和詳情頁,定位點擊每一條詳情的過程比較麻煩,所以爬取詳情頁面的和列表分開,用 Java 代碼解析所有詳情 URL 后,再由另一個 autoit 腳本去獲取詳情頁面,這個流程大家可以自己寫一下,這里就不詳細介紹了。

          最后再匯總下整個爬取的流程:

          第一步,執行爬取列表的 autoit 腳本,得到列表頁面 html;
          第二步,解析列表頁 html ,得到所有詳情頁面的 URL ,寫入到文件中;
          第三步,執行爬取詳情頁面的 autoit 腳本,它遍歷第二步的目標 URL ,得到詳情頁 html ;
          第四步,解析詳情頁 html 文件,得到詳情數據。
          

          總控流程、第二步和第四步的解析都用 Java 代碼完成,用 Runtime.getRuntime().exec("cmd /c E:\A2021Study\Autoit3\myspider.au3") 調用腳本,文件路徑是反斜杠。

          這個方法雖然有點笨,但完全是人工操作瀏覽器,能夠對抗反爬蟲策略,感興趣的朋友可以執行下本文的腳本試試。

          autoit 還是蠻有意思的,語法也很簡單,DirCreate 創建文件,iniread 讀取配置項,一行代碼頂 Java 幾十行,不得不承認 Java 操作文件才是最麻煩的哇!

          TTP 起源

          HTTP車是由蒂姆·伯納斯-李( TimBerners—Lee )于1989年在歐洲核子研究組織( CERN )所發起

          其中最著名的是 1999 年 6 月公布的 RFC 2616 ,定義了 HTTP 協議中現今廣泛使用的一個版本—— HTTP 1.1

          HTTP 是什么

          全稱:超文本傳輸協議( HyperText Transfer Protocol )

          概念: HTTP 是一種能夠獲取像 HTML 、圖片等網絡資源的通訊協議( protocol )。它是在 web 上進行數據交換的基礎,是一種 client-server 協議

          HTTP ——因特網的多媒體信使 ——《HTTP權威指南》。 HTTP 在因特網的角色:充當一個信使的角色,干的就是一個跑腿的活,在客戶端和服務端之間傳遞信息,但我們又不能缺少它。 HTTP 協議是應用層的協議,是與前端開發最息息相關的協議。平時我們遇到的 HTTP 請求、 HTTP 緩存、 Cookies 、跨域等其實都跟 HTTP 息息相關

          HTTP 的基礎特性

          • 可拓展協議。 HTTP 1.0 出現的 HTTP headers 讓協議拓展變得更加的容易。只要服務端和客戶端就 headers 達成語義一致,新功能就可以被輕松的加入進來
          • HTTP 是無狀態的、有會話的。在同一個連接中,兩個執行成功的 HTTP 請求之間是沒有關系的。這就帶來了一個問題,用戶沒有辦法在同一個網站中進行連續的交互,比如在一個電商網站里,用戶把某個商品加入到購物車,切換一個頁面后再次添加了商品,這兩次添加商品的請求之間沒有關聯,瀏覽器無法知道用戶最終選擇了哪些商品。而使用 HTTP 的頭部擴展, HTTP Cookies 就可以解決這個問題。把 Cookies 添加到頭部中,創建一個會話讓每次請求都能共享相同的上下文信息,達成相同的狀態。在同一個連接中,兩個執行成功的 HTTP 請求之間是沒有關系的。這就帶來了一個問題,用戶沒有辦法在同一個網站中進行連續的交互,比如在一個電商網站里,用戶把某個商品加入到購物車,切換一個頁面后再次添加了商品,這兩次添加商品的請求之間沒有關聯,瀏覽器無法知道用戶最終選擇了哪些商品。而使用 HTTP 的頭部擴展, HTTP Cookies 就可以解決這個問題。把 Cookies 添加到頭部中,創建一個會話讓每次請求都能共享相同的上下文信息,達成相同的狀態。
          • HTTP 與連接。通過 TCP ,或者 TLS ——加密的 TCP 連接來發送,理論上任何可靠的傳輸協議都可以使用。連接是傳輸層控制的,這從根本上來講不是 HTTP 的范疇。

          也就是說, HTTP 依賴于面向連接的 TCP 進行消息傳遞,但連接并不是必須的。只需要它是可靠的,或不丟失消息的(至少返回錯誤)。

          HTTP/1.0 默認為每一對 HTTP 請求/響應都打開一個單獨的 TCP 連接。當需要連續發起多個請求時,這種模式比多個請求共享同一個 TCP 鏈接更低效。為此, HTTP 1.1 持久連接的概念,底層 TCP 連接可以通過 connection 頭部實現。但 HTTP 1.1 在連接上也是不完美的,后面我們會提到。

          基于 HTTP 的組件系統

          HTTP 的組件系統包括客戶端、 web 服務器和代理

          客戶端:user-agent

          瀏覽器,特殊比如是工程師使用的程序,以及 Web 開發人員調試應用程序

          Web服務端

          由 Web Server 來服務并提供客戶端所請求的文檔。每一個發送到服務器的請求,都會被服務器處理并返回一個消息,也就是 response

          代理(Proxies)

          在瀏覽器和服務器之間,有很多計算機和其他設備轉發了 HTTP 消息。它們可能出現在傳輸層、網絡層和物理層上,對于 HTTP 應用層而言就是透明的

          有如下的一些作用

          • 緩存
          • 過濾(像防病毒掃描、家長控制)
          • 負載均衡
          • 認證(對不同的資源進行權限控制)
          • 日志管理

          HTTP 報文組成

          HTTP 有兩種類型的消息:

          • 請求——由客戶端發送用來觸發一個服務器上的動作
          • 響應——來自服務器端的應答

          HTTP 消息由采用 ASCII 編碼的多行文本構成的。在 HTTP/1.1 以及更早的版本中,這些消息通過連接公開的發送。在 HTTP2.0 中,消息被分到了多個 HTTP 幀中。通過配置文件(用于代理服務器或者服務器), API (用于瀏覽器)或者其他接口提供 HTTP 消息

          典型的 HTTP 會話

          • 建立連接 在客戶端-服務器協議中,連接是由客戶端發起建立的。在 HTTP 中打開連接意味著在底層傳輸層啟動連接,通常是 TCP 。使用 TCP 時, HTTP 服務器的默認端口號是 80 ,另外還有 8000 和 8080 也很常用
          • 發送客戶端請求
          • 服務器響應請求

          HTTP 請求和響應

          HTTP 請求和響應都包括起始行( start line )、請求頭( HTTP Headers )、空行( empty line )以及 body 部分,如下圖所示:

          • 起始行。 請求的起始行:請求方法、請求 Path 和 HTTP 版本號 響應的起始性: HTTP 版本號、響應狀態碼以及狀態文本描述

          下面詳細說下請求 Path ,請求路徑( Path )有以下幾種:

          1)一個絕對路徑,末尾跟上一個 ' ? ' 和查詢字符串。這是最常見的形式,稱為 原始形式 ( origin form ),被 GET , POST , HEAD 和 OPTIONS 方法所使用

          POST / HTTP/1.1
          GET /background.png HTTP/1.0
          HEAD /test.html?query=alibaba HTTP/1.1
          OPTIONS /anypage.html HTTP/1.0
          復制代碼

          2)一個完整的 URL 。主要在使用 GET 方法連接到代理的時候使用

          GET http://developer.mozilla.org/en-US/docs/Web/HTTP/Messages HTTP/1.1
          復制代碼

          3)由域名和可選端口(以':'為前綴)組成的 URL 的 authority component ,稱為 authority form 。僅在使用 CONNECT 建立 HTTP 隧道時才使用

          CONNECT developer.mozilla.org:80 HTTP/1.1
          復制代碼

          4)星號形式 ( asterisk form ),一個簡單的星號('*'),配合 OPTIONS 方法使用,代表整個服務器。

          OPTIONS * HTTP/1.1
          復制代碼
          • Headers 請求頭或者響應頭。詳見下面的首部。 不區分大小寫的字符串,緊跟著的冒號 (':') 和一個結構取決于 header 的值
          • 空行。很多人容易忽略
          • Body

          請求 Body 部分: 有些請求將數據發送到服務器以便更新數據:常見的的情況是 POST 請求(包含 HTML 表單數據)。請求報文的 Body 一般為兩類。一類是通過 Content-Type 和 Content-Length 定義的單文件 body 。另外一類是由多 Body 組成,通常是和 HTML Form 聯系在一起的。兩者的不同表現在于 Content-Type 的值。

          1) Content-Type —— application/x-www-form-urlencoded 對于 application/x-www-form-urlencoded 格式的表單內容,有以下特點:

          I.其中的數據會被編碼成以&分隔的鍵值對

          II.字符以URL編碼方式編碼。

          // 轉換過程: {a: 1, b: 2} -> a=1&b=2 -> 如下(最終形式)
          "a%3D1%26b%3D2"
          復制代碼

          2) Content-Type —— multipart/form-data

          請求頭中的 Content-Type 字段會包含 boundary ,且 boundary 的值有瀏覽器默認指定。例: Content-Type: multipart/form-data;boundary=----WebkitFormBoundaryRRJKeWfHPGrS4LKe 。

          數據會分為多個部分,每兩個部分之間通過分隔符來分隔,每部分表述均有 HTTP 頭部描述子包體,如 Content-Type ,在最后的分隔符會加上--表示結束。

          Content-Disposition: form-data;name="data1";
          Content-Type: text/plain
          data1
          ----WebkitFormBoundaryRRJKeWfHPGrS4LKe
          Content-Disposition: form-data;name="data2";
          Content-Type: text/plain
          data2
          ----WebkitFormBoundaryRRJKeWfHPGrS4LKe--
          復制代碼

          響應 Body 部分:

          1)由已知長度的單個文件組成。該類型 body 有兩個 header 定義: Content-Type 和 Content-Length

          2)由未知長度的單個文件組成,通過將 Transfer-Encoding 設置為 chunked 來使用 chunks 編碼。

          關于 Content-Length 在下面 HTTP 1.0 中會提到,這個是 HTTP 1.0 中新增的非常重要的頭部。

          方法

          安全方法: HTTP 定義了一組被稱為安全方法的方法。 GET 方法和 HEAD 方法都被認為是安全的,這意味著 GET 方法和 HEAD 方法都不會產生什么動作 —— HTTP 請求不會再服務端產生什么結果,但這并不意味著什么動作都沒發生,其實這更多的是 web 開發者決定的

          • GET :請求服務器發送某個資源
          • HEAD :跟 GET 方法類似,但服務器在響應中只返回了首部。不會返回實體的主體部分。
          • PUT :向服務器中寫入文檔。語義:用請求的主體部分來創建一個由所請求的 URL 命名的新文檔
          • POST :用來向服務器中輸入數據的。通常我們提交表單數據給服務器。【 POST 用于向服務器發送數據, PUT 方法用于向服務器上的資源(例如文件)中存儲數據】
          • TRACE :主要用于診斷。實現沿通向目標資源的路徑的消息環回( loop-back )測試 ,提供了一種實用的 debug 機制。
          • OPTIONS :請求 WEB 服務器告知其支持的各種功能。可以詢問服務器支持哪些方法。或者針對某些特殊資源支持哪些方法。
          • DELETE :請求服務器刪除請求 URL 中指定的的資源

          GET 和 POST 的區別

          首先要了解下副作用和冪等的概念,副作用指的是對服務器端資源做修改。冪等指發送 M 和 N 次請求(兩者不相同且都大于 1),服務器上資源的狀態一致。應用場景上,get是無副作用的,冪等的。post 主要是有副作用的,不冪等的情況

          技術上有以下的區分:

          • 緩存: Get 請求能緩存, Post 請求不能
          • 安全: Get 請求沒有 Post 請求那么安全,因為請求都在 URL 中。且會被瀏覽器保存歷史紀錄。 POST 放在請求體中,更加安全
          • 限制: URL 有長度限制,會干預 Get 請求,這個是瀏覽器決定的
          • 編碼: GET 請求只能進行 URL 編碼,只能接收 ASCII 字符,而 POST 沒有限制。 POST 支持更多的編碼類型,而且不對數據類型做限制
          • 從 TCP 的角度, GET 請求會把請求報文一次性發出去,而 POST 會分為兩個 TCP 數據包,首先發 header 部分,如果服務器響應 100(continue) , 然后發 body 部分。(火狐瀏覽器除外,它的 POST 請求只發一個 TCP 包)

          狀態碼

          • 100~199——信息性狀態碼101 Switching Protocols。在HTTP升級為WebSocket的時候,如果服務器同意變更,就會發送狀態碼 101。
          • 200~299——成功狀態碼200 OK,表示從客戶端發來的請求在服務器端被正確處理204 No content,表示請求成功,但響應報文不含實體的主體部分205 Reset Content,表示請求成功,但響應報文不含實體的主體部分,但是與 204 響應不同在于要求請求方重置內容206 Partial Content,進行范圍請求
          • 300~399——重定向狀態碼301 moved permanently,永久性重定向,表示資源已被分配了新的 URL302 found,臨時性重定向,表示資源臨時被分配了新的 URL303 see other,表示資源存在著另一個 URL,應使用 GET 方法獲取資源304 not modified,表示服務器允許訪問資源,但因發生請求未滿足條件的情況307 temporary redirect,臨時重定向,和302含義類似,但是期望客戶端保持請求方法不變向新的地址發出請求
          • 400~499——客戶端錯誤狀態碼400 bad request,請求報文存在語法錯誤401 unauthorized,表示發送的請求需要有通過 HTTP 認證的認證信息403 forbidden,表示對請求資源的訪問被服務器拒絕404 not found,表示在服務器上沒有找到請求的資源
          • 500~599——服務器錯誤狀態碼500 internal sever error,表示服務器端在執行請求時發生了錯誤501 Not Implemented,表示服務器不支持當前請求所需要的某個功能503 service unavailable,表明服務器暫時處于超負載或正在停機維護,無法處理請求

          首部

          HTTP Headers

          1.通用首部( General headers )同時適用于請求和響應消息,但與最終消息主體中傳輸的數據無關的消息頭。如 Date

          2.請求首部( Request headers )包含更多有關要獲取的資源或客戶端本身信息的消息頭。如 User-Agent

          3.響應首部( Response headers )包含有關響應的補充信息

          4.實體首部( Entity headers )含有關實體主體的更多信息,比如主體長( Content-Length )度或其 MIME 類型。如 Accept-Ranges

          詳細的 Header 見 HTTP Headers 集合

          HTTP 的前世今生

          HTTP(HyperText Transfer Protocol) 是萬維網( World Wide Web )的基礎協議。 Tim Berners-Lee 博士和他的團隊在 1989-1991 年間創造出它。【HTTP、網絡瀏覽器、服務器】

          在 1991 年發布了 HTTP 0.9 版,在 1996 年發布 1.0 版,1997 年是 1.1 版,1.1 版也是到今天為止傳輸最廣泛的版本。2015 年發布了 2.0 版,其極大的優化了 HTTP/1.1 的性能和安全性,而 2018 年發布的 3.0 版,繼續優化 HTTP/2 ,激進地使用 UDP 取代 TCP 協議,目前, HTTP/3 在 2019 年 9 月 26 日 被 Chrome , Firefox ,和 Cloudflare 支持

          HTTP 0.9

          單行協議,請求由單行指令構成。以唯一可用的方法 GET 開頭。后面跟的是目標資源的路徑

          GET /mypage.html
          復制代碼

          響應:只包括響應文檔本身

          <HTML>
          這是一個非常簡單的HTML頁面
          </HTML>
          復制代碼
          HTML
          

          HTTP 1.0

          RFC 1945 提出了 HTTP1.0 , 構建更好可拓展性

          • 協議版本信息會隨著每個請求發送
          • 響應狀態碼
          • 引入了 HTTP 頭的概念,無論是請求還是拓展,允許傳輸元數據。使協議變得靈活,更加具有拓展性
          • Content-Type 請求頭,具備了傳輸除純文本 HTML 文件以外其他類型文檔的能力 在響應中, Content-Type 標頭告訴客戶端實際返回的內容的內容類型

          媒體類型是一種標準。用來表示文檔、文件或者字節流的性質和格式。瀏覽器通常使用 MIME ( Multipurpose Internet Mail Extensions )類型來確定如何處理 URL ,因此 Web 服務器在響應頭中配置正確的 MIME 類型會非常的重要。如果配置不正確,可能會導致網站無法正常的工作。 MIME 的組成結構非常簡單;由類型與子類型兩個字符串中間用'/'分隔而組成。

          HTTP 從 MIME type 取了一部分來標記報文 body 部分的數據類型,這些類型體現在 Content-Type 這個字段,當然這是針對于發送端而言,接收端想要收到特定類型的數據,也可以用 Accept 字段。

          這兩個字段的取值可以分為下面幾類:

          - text: text/html, text/plain, text/css 等
          - image: image/gif, image/jpeg, image/png 等
          - audio/video: audio/mpeg, video/mp4 等
          - application: application/json, application/javascript, application/pdf, application/octet-stream
          復制代碼

          同時為了約定請求的數據和響應數據的壓縮方式、支持語言、字符集等,還提出了以下的 Header

          1.壓縮方式:發送端: Content-Encoding (服務端告知客戶端,服務器對實體的主體部分的編碼方式) 和 接收端: Accept-Encoding (用戶代理支持的編碼方式),值有 gzip: 當今最流行的壓縮格式;deflate: 另外一種著名的壓縮格式;br: 一種專門為 HTTP 發明的壓縮算法

          2.支持語言: Content-Language 和 Accept-Language (用戶代理支持的自然語言集)

          3.字符集:發送端: Content-Type 中,以 charset 屬性指定。接收端: Accept-Charset (用戶代理支持的字符集)。

          // 發送端
          Content-Encoding: gzip
          Content-Language: zh-CN, zh, en
          Content-Type: text/html; charset=utf-8
          
          // 接收端
          Accept-Encoding: gzip
          Accept-Language: zh-CN, zh, en
          Accept-Charset: charset=utf-8
          復制代碼

          雖然 HTTP1.0 在 HTTP 0.9 的基礎上改進了很多,但還是存在這不少的缺點

          HTTP/1.0 版的主要缺點是,每個 TCP 連接只能發送一個請求。發送數據完畢,連接就關閉,如果還要請求其他資源,就必須再新建一個連接。 TCP 連接的新建成本很高,因為需要客戶端和服務器三次握手,并且開始時發送速率較慢( slow start )。

          HTTP 最早期的模型,也是 HTTP/1.0 的默認模型,是短連接。每一個 HTTP 請求都由它自己獨立的連接完成;這意味著發起每一個 HTTP 請求之前都會有一次 TCP 握手,而且是連續不斷的。

          HTTP 1.1

          HTTP/1.1 在1997年1月以 RFC 2068 文件發布。

          HTTP 1.1 消除了大量歧義內容并引入了多項技術

          • 連接可以復用。長連接: connection: keep-alive 。 HTTP 1.1 支持長連接( PersistentConnection ),在一個 TCP 連接上可以傳送多個 HTTP 請求和響應,減少了建立和關閉連接的消耗和延遲,在 HTTP1.1 中默認開啟 Connection: keep-alive ,一定程度上彌補了 HTTP1.0 每次請求都要創建連接的缺點。
          • 增加了管道化技術( HTTP Pipelinling ),允許在第一個應答被完全發送完成之前就發送第二個請求,以降低通信延遲。復用同一個 TCP 連接期間,即便是通過管道同時發送了多個請求,服務端也是按請求的順序依次給出響應的;而客戶端在未收到之前所發出所有請求的響應之前,將會阻塞后面的請求(排隊等待),這稱為"隊頭堵塞"( Head-of-line blocking )。
          • 支持響應分塊,分塊編碼傳輸: Transfer-Encoding: chunked Content-length 聲明本次響應的數據長度。 keep-alive 連接可以先后傳送多個響應,因此用 Content-length 來區分數據包是屬于哪一個響應。 使用 Content-Length 字段的前提條件是,服務器發送響應之前,必須知道響應的數據長度。 對于一些很耗時的動態操作來說,這意味著,服務器要等到所有操作完成,才能發送數據,顯然這樣的效率不高。更好的處理方法是,產生一塊數據,就發送一塊,采用"流模式"( Stream )取代"緩存模式"( Buffer )。因此, HTTP 1.1 規定可以不使用 Content-Length 字段,而使用"分塊傳輸編碼"( Chunked Transfer Encoding )。只要請求或響應的頭信息有 Transfer-Encoding: chunked 字段,就表明 body 將可能由數量未定的多個數據塊組成。 每個數據塊之前會有一行包含一個 16 進制數值,表示這個塊的長度;最后一個大小為 0 的塊,就表示本次響應的數據發送完了。
          • 引入額外的緩存控制機制。在 HTTP1.0 中主要使用 header 里的 If-Modified-Since , Expires 等來做為緩存判斷的標準, HTTP1.1 則引入了更多的緩存控制策略例如 Entity tag , If-None-Match , Cache-Control 等更多可供選擇的緩存頭來控制緩存策略。
          • Host 頭。不同的域名配置同一個 IP 地址的服務器。 Host 是 HTTP 1.1 協議中新增的一個請求頭,主要用來實現虛擬主機技術。

          虛擬主機( virtual hosting )即共享主機( shared web hosting ),可以利用虛擬技術把一臺完整的服務器分成若干個主機,因此可以在單一主機上運行多個網站或服務。

          舉個例子,有一臺 ip 地址為 61.135.169.125 的服務器,在這臺服務器上部署著谷歌、百度、淘寶的網站。為什么我們訪問 https://www.google.com 時,看到的是 Google 的首頁而不是百度或者淘寶的首頁?原因就是 Host 請求頭決定著訪問哪個虛擬主機。

          HTTP 2.0

          2015年, HTTP2.0 面世。 rfc7540

          • HTTP/2 是二進制協議而不是文本協議。先來看幾個概念:幀:客戶端與服務器通過交換幀來通信,幀是基于這個新協議通信的最小單位。消息:是指邏輯上的 HTTP 消息,比如請求、響應等,由一或多個幀組成。流:流是連接中的一個虛擬信道,可以承載雙向的消息;每個流都有一個唯一的整數標識符

          HTTP 2.0 中的幀將 HTTP/1.x 消息分成幀并嵌入到流 ( stream ) 中。數據幀和報頭幀分離,這將允許報頭壓縮。將多個流組合,這是一個被稱為多路復用 ( multiplexing ) 的過程,它允許更有效的底層 TCP 連接。

          也就是說,流用來承載消息,消息又是有一個或多個幀組成。二進制傳輸的方式更加提升了傳輸性能。 每個數據流都以消息的形式發送,而消息又由一個或多個幀組成。 幀是流中的數據單位。

          HTTP 幀現在對 Web 開發人員是透明的。在 HTTP/2 中,這是一個在 HTTP/1.1 和底層傳輸協議之間附加的步驟。 Web 開發人員不需要在其使用的 API 中做任何更改來利用 HTTP 幀;當瀏覽器和服務器都可用時, HTTP/2 將被打開并使用。

          • 這是一個復用協議。并行的請求能在同一個連接中處理,移除了 HTTP/1.x 中順序和阻塞的約束。多路復用允許同時通過單一的 HTTP/2 連接發起多重的請求-響應消息

          之前我們提到,雖然 HTTP 1.1 有了長連接和管道化的技術,但是還是會存在 隊頭阻塞。而 HTTP 2.0 就解決了這個問題 HTTP/2 中新的二進制分幀層突破了這些限制,實現了完整的請求和響應復用:客戶端和服務器可以將 HTTP 消息分解為互不依賴的幀,然后交錯發送,最后再在另一端把它們重新組裝起來。

          如上圖所示,快照捕捉了同一個連接內并行的多個數據流。 客戶端正在向服務器傳輸一個 DATA 幀(數據流 5),與此同時,服務器正向客戶端交錯發送數據流 1 和數據流 3 的一系列幀。因此,一個連接上同時有三個并行數據流。

          將 HTTP 消息分解為獨立的幀,交錯發送,然后在另一端重新組裝是 HTTP 2 最重要的一項增強。事實上,這個機制會在整個網絡技術棧中引發一系列連鎖反應,從而帶來巨大的性能提升,讓我們可以: 1.并行交錯地發送多個請求,請求之間互不影響。 2.并行交錯地發送多個響應,響應之間互不干擾。 3.使用一個連接并行發送多個請求和響應。 4.消除不必要的延遲和提高現有網絡容量的利用率,從而減少頁面加載時間。 5.不必再為繞過 HTTP/1.x 限制而做很多工作(比如精靈圖) ...

          連接共享,即每一個 request 都是是用作連接共享機制的。一個 request 對應一個 id ,這樣一個連接上可以有多個 request ,每個連接的 request 可以隨機的混雜在一起,接收方可以根據 request 的 id 將 request 再歸屬到各自不同的服務端請求里面。

          HTTP 1.1 和 HTTP 2.0 的對比,可以參考這個 網站 demo 演示

          HTTP 1.1 演示如下:

          HTTP2.0 演示如下:

          • 壓縮了 headers 。 HTTP1.x 的 header 帶有大量信息,而且每次都要重復發送,就造成了性能的損耗。 為了減少此開銷和提升性能, HTTP/2 使用 HPACK 壓縮格式壓縮請求和響應標頭元數據,這種格式采用兩種簡單但是強大的技術: 這種格式支持通過靜態霍夫曼代碼對傳輸的標頭字段進行編碼,從而減小了各個傳輸的大小。 這種格式要求客戶端和服務器同時維護和更新一個包含之前見過的標頭字段的索引列表(換句話說,它可以建立一個共享的壓縮上下文),此列表隨后會用作參考,對之前傳輸的值進行有效編碼。

          • 服務端推送。其允許服務器在客戶端緩存中填充數據,通過一個叫服務器推送的機制來提前請求。服務器向客戶端推送資源無需客戶端明確地請求,服務端可以提前給客戶端推送必要的資源,這樣可以減少請求延遲時間,例如服務端可以主動把 JS 和 CSS 文件推送給客戶端,而不是等到 HTML 解析到資源時發送請求,這樣可以減少延遲時間大致過程如下圖所示:

          如何升級你的 HTTP 版本

          使用 HTTP/1.1 和 HTTP/2 對于站點和應用來說是透明的。擁有一個最新的服務器和新點的瀏覽器進行交互就足夠了。只有一小部分群體需要做出改變,而且隨著陳舊的瀏覽器和服務器的更新,而不需 Web 開發者做什么,用的人自然就增加了

          HTTPS

          HTTPS 也是通過 HTTP 協議進行傳輸信息,但是采用了 TLS 協議進行了加密

          對稱加密和非對稱加密

          對稱加密就是兩邊擁有相同的秘鑰,兩邊都知道如何將密文加密解密。但是因為傳輸數據都是走的網絡,如果將秘鑰通過網絡的方式傳遞的話,一旦秘鑰被截獲就沒有加密的意義的

          非對稱加密

          公鑰大家都知道,可以用公鑰加密數據。但解密數據必須使用私鑰,私鑰掌握在頒發公鑰的一方。首先服務端將公鑰發布出去,那么客戶端是知道公鑰的。然后客戶端創建一個秘鑰,并使用公鑰加密,發送給服務端。服務端接收到密文以后通過私鑰解密出正確的秘鑰

          TLS 握手過程

          TLS 握手的過程采用的是非對稱加密

          • Client Hello : 客戶端發送一個隨機值( Random1 )以及需要的協議和加密方式。
          • Server Hello 以及 Certificate : 服務端收到客戶端的隨機值,自己也產生一個隨機值( Random2 ),并根據客戶端需求的協議和加密方式來使用對應的方式,并且發送自己的證書(如果需要驗證客戶端證書需要說明)
          • Certificate Verify : 客戶端收到服務端的證書并驗證是否有效,驗證通過會再生成一個隨機值( Random3 ),通過服務端證書的公鑰去加密這個隨機值并發送給服務端,如果服務端需要驗證客戶端證書的話會附帶證書
          • Server 生成 secret : 服務端收到加密過的隨機值并使用私鑰解密獲得第三個隨機值( Random3 ),這時候兩端都擁有了三個隨機值,可以通過這三個隨機值按照之前約定的加密方式生成密鑰,接下來的通信就可以通過該密鑰來加密解密

          HTTP 緩存

          強緩存

          強緩存主要是由 Cache-control 和 Expires 兩個 Header 決定的

          Expires 的值和頭里面的 Date 屬性的值來判斷是否緩存還有效。 Expires 是 Web 服務器響應消息頭字段,在響應 http 請求時告訴瀏覽器在過期時間前瀏覽器可以直接從瀏覽器緩存取數據,而無需再次請求。 Expires 的一個缺點就是,返回的到期時間是服務器端的時間,這是一個絕對的時間,這樣存在一個問題,如果客戶端的時間與服務器的時間相差很大(比如時鐘不同步,或者跨時區),那么誤差就很大。

          Cache-Control 指明當前資源的有效期,控制瀏覽器是否直接從瀏覽器緩存取數據還是重新發請求到服務器取數據。但是其設置的是一個相對時間。

          指定過期時間: max-age 是距離請求發起的時間的秒數,比如下面指的是距離發起請求 31536000S 內都可以命中強緩存

          Cache-Control: max-age=31536000
          復制代碼

          表示沒有緩存

          Cache-Control: no-store
          復制代碼

          有緩存但要重新驗證

          Cache-Control: no-cache
          復制代碼

          私有和公共緩存

          public 表示響應可以被任何中間人(比如中間代理、 CDN 等緩存) 而 private 則表示該響應是專用于某單個用戶的,中間人不能緩存此響應,該響應只能應用于瀏覽器私有緩存中。

          Cache-Control: private
          Cache-Control: public
          復制代碼

          驗證方式:以下表示一旦資源過期(比如已經超過 max-age ),在成功向原始服務器驗證之前,緩存不能用該資源響應后續請求

          Cache-Control: must-revalidate
          復制代碼

          Cache-control 優先級比 Expires 優先級高

          以下是一個 Cache-Control 強緩存的過程:

          • 首次請求,直接從 server 中獲取。其中會設置 max-age=100
          • 第二次請求, age=10 ,小于 100,則命中 Cache ,直接返回
          • 第三次請求, age=110 ,大于 110。強緩存失效,就需要再次請求 Server

          協商緩存

          • If-Modified-Since——Last-Modified

          Last-Modified 表示本地文件最后修改日期,瀏覽器會在 request header 加上 If-Modified-Since (上次返回的 Last-Modified 的值),詢問服務器在該日期后資源是否有更新,有更新的話就會將新的資源發送回來

          但是如果在本地打開緩存文件,就會造成 Last-Modified 被修改,所以在 HTTP / 1.1 出現了 ETag

          • If-none-match——ETags

          Etag 就像一個指紋,資源變化都會導致 ETag 變化,跟最后修改時間沒有關系, ETag 可以保證每一個資源是唯一的。 If-None-Match 的 header 會將上次返回的 Etag 發送給服務器,詢問該資源的 Etag 是否有更新,有變動就會發送新的資源回來

          If-none-match 、 ETags 優先級高于 If-Modified-Since、Last-Modified

          第一次請求:

          第二次請求相同網頁:

          協商緩存,假如沒有改動的話,返回 304 ,改動了返回 200 資源

          • 200:強緩存 Expires/Cache-Control 失效時,返回新的資源文件
          • 200 (from cache) : 強緩 Expires/Cache-Control 兩者都存在,未過期, Cache-Control 優先 Expires 時,瀏覽器從本地獲取資源成功
          • 304 (Not Modified) :協商緩存 Last-modified/Etag 沒有過期時,服務端返回狀態碼304

          現在的200 (from cache) 已經變成了 disk cache (磁盤緩存)和 memory cache (內存緩存)兩種

          revving 技術

          上面提到 HTTP 緩存相關,但是很多有時候,我們希望上線之后需要更新線上資源。

          web 開發者發明了一種被 Steve Souders 稱之為 revving 的技術。不頻繁更新的文件會使用特定的命名方式:在 URL 后面(通常是文件名后面)會加上版本號。

          弊端:更新了版本號,所有引用這些的資源的地方的版本號都要改變

          web 開發者們通常會采用自動化構建工具在實際工作中完成這些瑣碎的工作。當低頻更新的資源( js/css )變動了,只用在高頻變動的資源文件( html )里做入口的改動。

          Cookies

          HTTP Cookie (也叫 Web Cookie 或瀏覽器 Cookie )是服務器發送到用戶瀏覽器并保存在本地的一小塊數據,它會在瀏覽器下次向同一服務器再發起請求時被攜帶并發送到服務器上。

          創建 cookie

          Set-Cookie 響應頭部和 Cookie 請求頭部

          Set-Cookie: <cookie名>=<cookie值>
          復制代碼

          會話期Cookie

          會話期Cookie是最簡單的 Cookie :瀏覽器關閉之后它會被自動刪除,也就是說它僅在會話期內有效。會話期 Cookie 不需要指定過期時間( Expires )或者有效期( Max-Age )。需要注意的是,有些瀏覽器提供了會話恢復功能,這種情況下即使關閉了瀏覽器,會話期 Cookie 也會被保留下來,就好像瀏覽器從來沒有關閉一樣

          持久性Cookie

          和關閉瀏覽器便失效的會話期 Cookie 不同,持久性 Cookie 可以指定一個特定的過期時間( Expires )或有效期( Max-Age )。

          Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
          復制代碼

          Cookie的Secure和HttpOnly 標記

          標記為 Secure 的 Cookie 只應通過被 HTTPS 協議加密過的請求發送給服務端。

          標記為 Secure 的 Cookie 只應通過被 HTTPS 協議加密過的請求發送給服務端。但即便設置了 Secure 標記,敏感信息也不應該通過 Cookie 傳輸,因為 Cookie 有其固有的不安全性, Secure 標記也無法提供確實的安全保障

          通過 JavaScript 的 Document.cookie API 是無法訪問帶有 HttpOnly 標記的 cookie 。這么做是為了避免跨域腳本攻擊( XSS )

          Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
          復制代碼

          Cookie的作用域

          Domain 和 Path 標識定義了 Cookie 的作用域:即 Cookie 應該發送給哪些 URL 。

          Domain 標識指定了哪些主機可以接受 Cookie 。如果不指定,默認為當前的主機(不包含子域名)。如果指定了 Domain ,則一般包含子域名。

          例如,如果設置 Domain=mozilla.org ,則 Cookie 也包含在子域名中(如 developer.mozilla.org )。

          Path 標識指定了主機下的哪些路徑可以接受 Cookie (該 URL 路徑必須存在于請求 URL 中)。以字符 %x2F ("/") 作為路徑分隔符,子路徑也會被匹配。

          例如,設置 Path=/docs ,則以下地址都會匹配:

          /docs
          /docs/Web/
          /docs/Web/HTTP
          復制代碼

          SameSite Cookies

          SameSite Cookie 允許服務器要求某個 cookie 在跨站請求時不會被發送,從而可以阻止跨站請求偽造攻擊

          • None 瀏覽器會在同站請求、跨站請求下繼續發送 cookies ,不區分大小寫。【舊版本 chrome 默認 Chrome 80 版本之前】
          • Strict 瀏覽器將只在訪問相同站點時發送 cookie 。
          • Lax 將會為一些跨站子請求保留,如圖片加載或者 frames 的調用,但只有當用戶從外部站點導航到 URL 時才會發送。如 link 鏈接
          Set-Cookie: key=value; SameSite=Strict
          復制代碼

          None Strict Lax

          在新版本的瀏覽器( Chrome 80 之后)中, SameSite 的默認屬性是 SameSite=Lax 。換句話說,當 Cookie 沒有設置 SameSite 屬性時,將會視作 SameSite 屬性被設置為 Lax —— 這意味著 Cookies 將不會在當前用戶使用時被自動發送。如果想要指定 Cookies 在同站、跨站請求都被發送,那么需要明確指定 SameSite 為 None 。因為這一點,我們需要好好排查舊系統是否明確指定 SameSite ,以及推薦新系統明確指定 SameSite ,以兼容新舊版本 Chrome

          更多 cookie 相關,可以查看我之前總結的一篇關于 cookie 的文章 前端須知的 Cookie 知識小結

          HTTP訪問控制(CORS)

          跨域資源共享( CORS )是一種機制,它使用額外的 HTTP 頭告訴瀏覽器,讓運行在一個 origin ( domain ) 上的 web 應用被準許訪問來自不同源服務器上的指定的資源

          跨域資源共享標準新增了一組 HTTP 首部字段,允許服務器聲明哪些源站通過瀏覽器有權限訪問哪些資源。

          簡單請求

          簡單請求(不會觸發 CORS 的預檢請求)需要同時滿足以下三點:

          • 方法是 GET/HEAD/POST 之一
          • Content-Type 的值僅限 text/plain 、 multipart/form-data 、 application/x-www-form-urlencoded 三者之一
          • HTTP 頭部不能超過以下字段: Accept 、 Accept-Language 、 Content-Language Content-Type (需要注意額外的限制) DPR 、 Downlink 、 Save-Data 、 Viewport-Width 、 Width

          以下為一個簡單請求的請求報文以及響應報文

          簡化以下:

          請求首部字段 Origin 表明該請求來源于 http://foo.example

          本例中,服務端返回的 Access-Control-Allow-Origin: * 表明,該資源可以被任意外域訪問。如果服務端僅允許來自 http://foo.example 的訪問,該首部字段的內容如下:

          Access-Control-Allow-Origin: http://foo.example
          復制代碼

          Access-Control-Allow-Origin 應當為 * 或者包含由 Origin 首部字段所指明的域名。

          預檢請求

          規范要求,對那些可能對服務器數據產生副作用的 HTTP 請求方法。瀏覽器必須首先使用 OPTIONS 方法發起一個預檢請求( preflight request ),從而獲知服務端是否允許該跨域請求。

          服務器確認允許之后,才發起實際的 HTTP 請求。在預檢請求的返回中,服務器端也可以通知客戶端,是否需要攜帶身份憑證(包括 Cookies 和 HTTP 認證相關數據)

          預檢請求中同時攜帶了下面兩個首部字段:

          Access-Control-Request-Method: POST
          Access-Control-Request-Headers: X-PINGOTHER, Content-Type
          復制代碼

          首部字段 Access-Control-Request-Method 告知服務器,實際請求將使用 POST 方法。首部字段 Access-Control-Request-Headers 告知服務器,實際請求將攜帶兩個自定義請求首部字段: X-PINGOTHER 與 Content-Type 。服務器據此決定,該實際請求是否被允許。

          預檢請求的響應中,包括了以下幾個字段

          Access-Control-Allow-Origin: http://foo.example
          // 表明服務器允許客戶端使用 POST, GET 和 OPTIONS 方法發起請求
          Access-Control-Allow-Methods: POST, GET, OPTIONS
          // 表明服務器允許請求中攜帶字段 X-PINGOTHER 與 Content-Type
          Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
          // 表明該響應的有效時間為 86400 秒,也就是 24 小時。在有效時間內,瀏覽器無須為同一請求再次發起預檢請求。
          Access-Control-Max-Age: 86400
          復制代碼

          一般而言,對于跨域 XMLHttpRequest 或 Fetch 請求,瀏覽器不會發送身份憑證信息。如果要發送憑證信息,需要設置 XMLHttpRequest 的某個特殊標志位。比如說 XMLHttpRequest 的 withCredentials 標志設置為 true ,則可以發送 cookie 到服務端。

          對于附帶身份憑證的請求,服務器不得設置 Access-Control-Allow-Origin 的值為“*”。 這是因為請求的首部中攜帶了 Cookie 信息,如果 Access-Control-Allow-Origin 的值為“*”,請求將會失敗。而將 Access-Control-Allow-Origin 的值設置為 http://foo.example ,則請求將成功執行。

          CORS 涉及到的請求和響應頭如下: HTTP 響應首部字段

          • Access-Control-Allow-Origin 允許訪問該資源的外域 URI 。對于不需要攜帶身份憑證的請求,服務器可以指定該字段的值為通配符,表示允許來自所有域的請求。
          • Access-Control-Expose-Headers 頭讓服務器把允許瀏覽器訪問的頭放入白名單
          • Access-Control-Max-Age 頭指定了 preflight 請求的結果能夠被緩存多久
          • Access-Control-Allow-Credentials 頭指定了當瀏覽器的 credentials 設置為 true 時是否允許瀏覽器讀取 response 的內容。
          • Access-Control-Allow-Methods 首部字段用于預檢請求的響應。其指明了實際請求所允許使用的 HTTP 方法。
          • Access-Control-Allow-Headers 首部字段用于預檢請求的響應。其指明了實際請求中允許攜帶的首部字段。

          HTTP 請求首部字段

          Origin
          Access-Control-Request-Method
          Access-Control-Request-Headers

          有感興趣的朋友可以關注一下我的公眾號:前端維他命,不定時更新優秀文章。


          主站蜘蛛池模板: 国产主播福利一区二区| 亚洲国产精品一区二区九九| 久久中文字幕一区二区| 久久99热狠狠色精品一区| 一区二区三区四区国产| 久久精品一区二区三区AV| 国产精品女同一区二区久久| 久久精品国产一区二区三区| 精品一区二区三区| 国产一区二区三区影院| 国产在线一区二区| 久久久99精品一区二区| 亚洲日本中文字幕一区二区三区 | 中文字幕精品一区二区2021年| 国产一区二区三区高清视频| 久久精品一区二区三区中文字幕| 色综合久久一区二区三区| 日韩在线视频一区| 日本在线视频一区二区三区| 国产午夜精品一区二区| 内射白浆一区二区在线观看 | 国产免费伦精品一区二区三区| 视频在线一区二区| 亚洲日韩国产一区二区三区| 丝袜人妻一区二区三区网站| 亚洲人成人一区二区三区 | 一区二区日韩国产精品| 久久免费区一区二区三波多野| 国产日韩精品视频一区二区三区| 亚洲一区二区三区四区在线观看| 在线观看精品一区| 激情综合丝袜美女一区二区| 一区二区三区在线播放| 中文字幕乱码一区二区免费| 久久久无码精品国产一区| 成人精品一区二区户外勾搭野战| 性盈盈影院免费视频观看在线一区| 久久毛片免费看一区二区三区| 中文字幕av无码一区二区三区电影| 福利一区二区三区视频在线观看| 亚洲丶国产丶欧美一区二区三区 |