整合營銷服務商

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

          免費咨詢熱線:

          宇宙廠:Vue3.0 為何用 Proxy 替代 de

          宇宙廠:Vue3.0 為何用 Proxy 替代 defineProperty?

          家好,很高興又見面了,我是"高級前端?進階?",由我帶著大家一起關注前端前沿、深入前端底層技術,大家一起進步,也歡迎大家關注、點贊、收藏、轉發!

          1. 什么是 Object.defineProperty

          1.1 Object.defineProperty 基本用法

          Object.defineProperty() 允許精確添加或修改對象屬性。通過賦值添加的普通屬性會在枚舉屬性時(例如 for...in、Object.keys() 等)出現,值可以被更改,也可以被刪除。

          defineProperty() 方法允許更改額外細節,以使其不同于默認值。默認情況下,使用 Object.defineProperty() 添加的屬性是不可寫、不可枚舉和不可配置的。此外,Object.defineProperty() 使用 [[DefineOwnProperty]] 內部方法,而不是 [[Set]],因此即使屬性已經存在也不會調用 setter。


          Object.defineProperty(obj, prop, descriptor)

          方法每一個參數定義如下:

          • obj:要定義屬性的對象。
          • prop:一個字符串或 Symbol,指定了要定義或修改的屬性鍵。
          • descriptor:要定義或修改的屬性的描述符,包括:configurable(如是否可刪除)、enumerable、writable、get、set 等等。

          下面示例使用 Object.defineProperty 進行對象屬性定義:

          const obj={};
          // 1. 使用 null 原型:沒有繼承的屬性
          const descriptor=Object.create(null);
          descriptor.value="static";
          
          // 默認情況下,不可枚舉、不可配置、不可寫
          // obj.key="static modified" 賦值后依然是 "static"
          Object.defineProperty(obj, "key", descriptor);
          
          // 2. 使用一個包含所有屬性的臨時對象字面量來明確其屬性
          Object.defineProperty(obj, "key2", {
            enumerable: false,
            configurable: false,
            writable: false,
            value: "static",
          });
          
          // 3. 重復利用同一對象
          function withValue(value) {
            const d=withValue.d ||
              (withValue.d={
                enumerable: false,
                writable: false,
                configurable: false,
                value,
              });
          
            // 避免重復賦值
            if (d.value !==value) d.value=value;
            return d;
          }
          // 然后
          Object.defineProperty(obj, "key", withValue("static"));
          
          // 如果 freeze 可用,防止添加或刪除對象原型屬性
          // (value、get、set、enumerable、writable、configurable)
          (Object.freeze || Object)(Object.prototype);

          1.2 Object.defineProperty 優缺點

          Object.defineProperty 的主要優點包括:

          • Object.defineProperty() 方法可以對屬性的行為方式進行細粒度的控制
          • 允許設置只讀屬性,防止意外修改
          • 開發者可以決定某個屬性是否在枚舉期間出現,從而實現特定的功能
          • 允許開發者使屬性不可刪除,從而保證核心屬性的安全。

          當然,Object.defineProperty 也有不足之處,主要體現在:

          • Object.defineProperty() 不能很好地處理數組,因為無法捕獲修改索引值或長度屬性以及動態屬性(動態 getter 是一種沒有為 property 顯式定義 getter,而是在訪問屬性時動態創建的),詳情可以看這篇文章(https://vue3js.cn/interview/vue3/proxy.html#一、object-defineproperty)。同時,也不支持嵌套對象,這意味著不會觀察到嵌套對象的任何更改

          下面是對象示例:

          const obj={
              foo: "foo",
              bar: "bar"
          }
          observe(obj)
          delete obj.foo // no ok
          obj.jar='xxx' // no ok

          下面是數組示例:

          const arrData=[1,2,3,4,5];
          arrData.forEach((val,index)=>{
              defineProperty(arrData,index,val)
          })
          arrData.push() // no ok
          arrData.pop()  // no ok
          arrDate[0]=99 // ok

          基于對象和數組的以上局限性,Vue2 增加了 set、delete API,并且對數組 api 方法進行一個重寫。

          • Object.defineProperty() 的語法很冗長,可能會增加可讀性,影響代碼的模塊化和可重用性。

          而掌握 Object.defineProperty 的關鍵在于透徹理解屬性描述符的屬性。 例如,正確地將 writable 屬性設置為 false 可以確保屬性值在整個程序中保持不變,從而減少出現錯誤的機會。

          比如下面的示例將 π 置為常量后將無法修改:

          let constantObj={};
          Object.defineProperty(constantObj, 'pi', {
            value: 3.14159,
            writable: false
          });
          
          console.log(constantObj.pi);
          // Outputs 3.14159
          constantObj.pi=3;
          // Attempting to change the value
          console.log(constantObj.pi);
          // Still outputs 3.14159

          注意:Vue 3 改用了 Proxy 。Proxy 可以攔截對象屬性讀取、賦值和刪除操作,從而能夠在屬性發生變化時觸發相應的更新。對于數組,Proxy 可以攔截數組的修改操作,比如: push、pop、splice 等,從而能夠在數組發生變化時觸發相應的更新。


          此外,Proxy 還可以攔截對象的原型方法和構造函數調用,從而可以對對象的所有操作進行攔截和處理。

          2. 什么是 Proxy

          JavaScript 的 Proxy 對象是一項強大的功能,使開發者能夠攔截和自定義對對象執行的操作,例如:屬性查找、賦值、枚舉和函數調用。 這種多功能工具允許開發人員創建更高效、更靈活的代碼,同時還提高代碼的可維護性。

          Proxy 遵循以下語法規范:

          const p=new Proxy(target, handler)
          • target:要使用 Proxy 包裝的目標對象,可以是任何類型的對象,包括原生數組,函數,甚至另一個代理。
          • handler:一個通常以函數作為屬性的對象,各屬性中的函數分別定義了在執行各種操作時代理 p 的行為。

          值得一提的是,handler 對象是一個容納一批特定屬性的占位符對象,包含有 Proxy 的各個捕獲器(trap)。而且所有的捕捉器是可選的。如果沒有定義某個捕捉器,那么就會保留源對象的默認行為。

          常見的捕獲器包括:

          • getPrototypeOf()
          • setPrototypeOf()
          • isExtensible()
          • preventExtensions()
          • getOwnPropertyDescriptor()
          • defineProperty()
          • has()
          • get()
          • set()
          • deleteProperty()
          • ownKeys()
          • apply()
          • construct()

          在以下例子中,使用了一個原生 JavaScript 對象,Proxy 會將所有應用到它的操作轉發到這個對象上。

          let target={};
          let p=new Proxy(target, {});
          
          p.a=37;
          // 操作轉發到目標
          
          console.log(target.a);
          // 37. 操作已經被正確地轉發

          3.Proxy 與 Object.defineProperty 主要區別

          Proxy 與 Object.defineProperty 一個主要區別在于抽象級別。 Proxy 在對象周圍創建一個新層,可以 Hook 任何屬性,而無需預先顯式定義, 而 Object.defineProperty 直接修改對象并要求相關屬性在定義時就存在。

          此外,Proxies 涵蓋了廣泛的 property 操作,而 Object.defineProperty 則專注于 attribute 屬性操作。

          關于 Proxy 和 Object.defineProperty 還需要弄清楚一個常見錯誤,即將 Object.defineProperty 用于復雜的動態對象,期望新屬性的反應行為可能會導致意外結果,因為 Object.defineProperty 只影響現有屬性。

          比如下面的示例:

          let object={};
          Object.defineProperty(object, 'property', {
              value: 42,
              writable: false
          });
          object.newProperty=100;
          console.log(object.newProperty);
          // 輸出: 100
          // newProperty 的行為不受 Object.defineProperty 控制

          實現新屬性反應性(Reactivity)的正確方法是使用 Proxy,或者為每個新屬性動態實現 Object.defineProperty。比如下面的 Proxy 示例:

          let targetObject={message: 'Hello, world'};
          let handler={
              set: function(target, prop, value) {
                  if (prop==='newProperty') {
                      target[prop]=value * 2;
                  } else {
                      target[prop]=value;
                  }
              }
          };
          
          let proxy=new Proxy(targetObject, handler);
          proxy.newProperty=100;
          console.log(proxy.newProperty);
          // 輸入: 200

          4. 使用 Proxy 的場景

          4.1 驗證對象屬性

          考慮創建一個需要具有某些有條件所需屬性的嚴格架構的對象,可以通過使用代理包裝對象并在 set 中實施驗證檢查來管理,從而確保只有有效數據進入對象。

          比如下面的代碼示例使用 Proxy 實現 set 方法,驗證對象的屬性是否在指定的 schema 中:

          let schema={
              id: {
                  type: 'number',
                  required: true
              },
              comment: {
                  type: 'string',
                  required: false
              }
          };
          let handler={
              set: function (target, key, value) {
                  if (schema[key] && typeof value !==schema[key].type) {
                      throw new Error(`Type ${typeof value} is not assignable to type ${schema[key].type}`);
                  } else if (schema[key] && schema[key].required && value===undefined) {
                      throw new Error(`${key} is required.`);
                  }
                  target[key]=value;
                  return true;
              }
          };
          
          let movie=new Proxy({}, handler);

          4.2 對象級訪問控制

          Proxy 對象可以有效控制對象屬性的訪問,通常可用于提供對象的只讀視圖或限制可訪問的對象屬性的范圍。

          比如下面的代碼示例表示訪問 password 屬性后則會拋出錯誤:

          let personDetails={
              firstName: 'John',
              lastName: 'Doe',
              password: '12345!'
          };
          
          let handler={
              get: function (target, prop) {
                  if (prop==='password') {
                      throw new Error('Access to password is denied');
                  }
                  return target[prop];
              }
          };
          let proxy=new Proxy(personDetails, handler);
          console.log(proxy.password);
          // 拋出錯誤
          console.log(proxy.firstName);
          // 輸出: 'John'

          又或者下面的代碼示例在修改元素屬性之前做精確的控制,從而屬性相互覆蓋:

          function assignIfNotExists(target, source){
              for (let prop in source) {
                  if (!target.hasOwnProperty(prop)) {
                      target[prop]=source[prop];
                  }
              }
          }
          let data={username: 'Zach'};
          let userInput={username: 'JohnDoe', password: 'secret'};
          // Avoid overwriting 'username' in data object
          assignIfNotExist(data, userInput);

          4.3 數據綁定和觀察者

          Proxy 可以幫助構建數據綁定解決方案,比如:當應用程序的狀態發生變化時,開發者可能希望跟蹤變化并做出響應,比如: Vue.js 就是一個很好的示例。

          let state={
              count: 0
          };
          let handler={
              set: function (target, property, value) {
                  target[property]=value;
                  console.log(`State has changed. New ${property}: ${value}`);
                  return true;
              }
          };
          let proxy=new Proxy(state, handler);
          proxy.count=2;
          // 輸出: State has changed. New count: 2

          以上代碼示例,JavaScript Proxy 提供了對對象交互的精確控制,從而實現復雜行為、驗證、訪問控制等等。 然而,由于 Proxy 的復雜性,考慮使用 Proxy 的開銷也同樣重要。 因此,Proxy 的使用應該針對特定的挑戰,其獨特的功能可以顯著提高系統操作和可讀性。

          5.Proxy 與 Object.defineProperty 深入比較

          5.1 性能

          JavaScript Proxy 比 Object.defineProperty 消耗的時間稍多, Proxy 本質上應用了一個額外的抽象層(處理程序),從而可能會使操作比 Object.defineProperty 更慢。

          比如下面的代碼示例:

          let object={};
          Object.defineProperty(object, 'property', {
              value: 42,
              writable: false
          });

          object 中的 property 屬性值必須是常量,直接訪問 property 非常簡單快捷。 而對于 Proxy 來說,在獲取對象值之前有一個額外的檢查和驗證過程:

          let targetObject={property: 42};
          let handler={
              get: function(target, prop) {
                  return target[prop];
              }
          };
          let proxy=new Proxy(targetObject, handler);

          5.2 代碼復雜性和可讀性

          Object.defineProperty 重點關注屬性級別, 當需要控制屬性是否可以修改、配置甚至枚舉時則是理想選擇,同時 Object.defineProperty 的用法直接且有針對性的,使代碼更容易閱讀和理解。

          然而,Proxy 在提供更高級別的抽象方面表現出色。 Proxy 對象可以針對整個對象,而不僅僅是單個屬性,從而允許開發人員以更高級的方式攔截和重新定義對象的默認行為。

          然而,Proxy 中的處理程序可能會造成復雜性,因為總是需要通過一個額外的中間層。 其他開發者也需要對 Proxy 概念有更多的了解才能輕松閱讀 Proxy 代碼。

          5.3 模塊化和可重用性

          在模塊化和可重用性方面,當想要為更大范圍甚至整個應用程序定義全局處理程序行為時,Proxy 通常會發揮作用。 Proxy 通常提供一種極好的方法來將特定的控制行為封裝在單獨的處理程序中。 這樣,同一個處理程序可以與多個目標對象重復使用。

          相反,Object.defineProperty 允許模塊化和保護單個對象屬性,對于以模塊化方式定義、保護或控制對象的屬性非常重要。

          Proxy 提供了更多的可能性,捕獲更多的動作,并提供對對象的更多控制。 然而,它們也會帶來性能成本,需要了解它們的用法,并且可能會使調試變得復雜。

          另一方面,Object.defineProperty 雖然不如代理那么強大和靈活,但提供了一種簡單、直接且易于調試的方法。

          6.Proxy 常見方法

          6.1 Proxy 轉為普通對象

          const proxy={"name":"高級前端進階"}
          
          const proxyObj=new Proxy(proxy, {
            get: (target, prop)=> prop in target ? target[prop] : 37
          });
          
          console.log(proxyObj.a)
          // 輸出 37
          console.log(proxyObj.name)
          // 輸出 ` 高級前端進階 `
          console.log(JSON.stringify(proxyObj))
          // 輸出 {"name":"晴天"}

          值得注意的是,使用 JSON.parse(JSON.stringify(proxyObj)) 方法會刪除任何不能字符串化的內容,比如:類、函數、回調等。

          如果確實需要,可以考慮使用 Lodash 的 cloneDeep 函數,該方法在將 Proxy 對象轉換為 POJO(The Plain Old JavaScript Object) 的同時保持對象結構方面確實做得很好。

           convertProxyObjectToPojo(proxyObj) {
            return _.cloneDeep(proxyObj);
          }

          6.2 Proxy 監聽數組元素變化

          以下示例表示 Proxy 確實能監聽到數組元素的變更,這與 defineProperty 是有差別的,至于監聽嵌套對象屬性變化可以自行驗證。

          function get(target, prop, receiver) {
            console.log('target:' + target);
            console.log('property:' + prop);
            return Reflect.get(target, prop, receiver);
          }
          
          var handler={
            'get': get
          };
          
          // 為數組添加 Proxy
          var proxy=new Proxy([1,2,3,4,5], handler );
          
          console.log('Result=> beep:' + proxy.beep );
          // target: 1,2,3,4,5
          // property: beep
          // Result=> beep: undefined
          console.log('Result=> -123:' + proxy[ -123] );
          // target: 1,2,3,4,5
          // property: -123
          // proxy:16 Result=> -123: undefined
          console.log(proxy.fill( 1) );
          // target: 1,2,3,4,5
          // property: fill
          // target: 1,2,3,4,5
          // property: length
          // Proxy(Array) {0: 1, 1: 1, 2: 1, 3: 1, 4: 1}
          console.log('Result=> 0:' + proxy[ 0 ] );
          // target: 1,1,1,1,1
          // property: 0
          // Result=> 0: 1
          var arr1=[10, 20, 30, 40, 50];
          Object.setPrototypeOf(arr1, proxy);
          console.log('Result=> beep:' + arr1.beep );
          console.log('Result=> -123:' + arr1[ -123 ] );
          console.log(arr1.fill( 100) );
          // 輸出 (5) [100, 100, 100, 100, 100]
          console.log('Result=> 0:' + arr1[ 0 ] );

          6.3 Proxy 監聽嵌套對象

          var validator={
            get(target, key) {
              if (typeof target[key]==='object' && target[key] !==null) {
                  // 如果是對象則繼續創建 Proxy
                return new Proxy(target[key], validator)
              } else {
                return target[key];
              }
            },
            set (target, key, value) {
              console.log(target);
              // 輸出 {salary: 8250, Proffesion: '.NET Developer'}
              console.log(key);
              // 輸出 salary
              console.log(value);
              // 輸出 foo
              return true
            }
          }
          var person={
                firstName: "alfred",
                lastName: "john",
                inner: {
                  salary: 8250,
                  Proffesion: ".NET Developer"
                }
          }
          var proxy=new Proxy(person, validator)
          proxy.inner.salary='foo'
          // 這一句代碼會先訪問 proxy.inner 屬性,發現是 Object
          // 然后會繼續訪問 salary 屬性

          參考資料

          https://borstch.com/blog/objects-in-javascript-properties-methods-and-prototypes

          https://borstch.com/blog/proxies-vs-objectdefineproperty-when-to-use-which

          https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy

          https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

          https://blog.javascripttoday.com/blog/deep-dive-proxies-in-javascript/

          https://vue3js.cn/interview/vue3/proxy.html#二、proxy

          https://vue3js.cn/interview/vue3/proxy.html#一、object-defineproperty

          https://juejin.cn/post/7306783965532717108

          https://www.youtube.com/watch?app=desktop&v=_k3WiANNB4U

          https://segmentfault.com/q/1010000043053833

          https://www.30secondsofcode.org/js/s/dynamic-getter-setter-proxy/

          https://gist.github.com/kgryte/713ab40f36c128bc1d52

          https://stackoverflow.com/questions/41299642/how-to-use-javascript-proxy-for-nested-objects

          he Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc). - MDN

          前言

          前面兩篇文章我們介紹了在JS中對引用類型做非破壞性操作的一些方式,對前端程序員來說,一個非常重要的好處就是可以讓我們寫出正確的redux reducer。在調研reducer更新方式的過程中,我注意到了一個庫,叫做ImmerJS,它允許我們用破壞性的語法來得到非破壞性的結果。由于它是基于Proxy的機制來實現的,這讓我對Proxy這個ES6的新特性產生了很大的興趣。另外Vue3的reactivity system也是基于Proxy的,如果你使用Vue那么了解Proxy的機制也是很有必要的。

          所以這兩天我就抽時間去學習了一下,這篇文章就給大家簡單介紹一下Proxy。

          什么是Proxy?

          Proxy,也就是”代理“,在程序中的意思通常是在用戶和真正訪問的對象之間加了一個中間層,用戶不再直接訪問原始對象,而是通過Proxy來做一個中轉。比如我們最常見的關于代理的概念就是網絡代理,分為"正向代理"和"反向代理"。"反向代理"拿nginx來講,它就是在用戶(瀏覽器)和目標(web server)之間的一個中間層,瀏覽器通過nginx(代理)才能訪問到web server。通過代理可以實現訪問控制、負載均衡等目標。

          我們這里講的JavaScript的Proxy也是類似的概念,只不過它是在對象對象的訪問之間的一層代理,應用了Proxy之后,我們將通過Proxy來訪問目標對象。

          而Proxy會改變JavaScript中一些基礎操作的執行路徑,如讀取屬性、寫入屬性、對象遍歷、函數調用等。這是一種強大的元編程(meta programming)機制,可以實現很多通過之前無法實現的功能特性。雖然Proxy并沒有引入新的語法,但由于它會修改JavaScript的一些底層代碼的執行方式,所以它是無法被完全polyfill的。

          Proxy的幾個概念

          在你讀Proxy的相關文檔時,有幾個概念會頻繁出現,我們先對這些概念做一個簡單介紹。

          • target - target指的就是我們的目標對象,也就是被代理的對象
          • trap - trap可以理解為是一些預定義的觸發點以及定義在這些觸發點上的函數,比如上面提到的讀取屬性、寫入屬性、函數調用等,如果我們在這些觸發點上定義了函數,那么在對Proxy執行對應操作的時候,我們定義的函數將會被調用
          • handler - handler是一個對象,它包裝了所有Proxy提供的trap函數,可以理解為是trap的集合

          Proxy的創建

          Proxy的創建是比較簡單的,參考以下代碼:

          // 我們的原始對象
          var target={
           x: 1,
          }
          // handler對象,里面定義了各種trap函數
          var handler={
           get: function(obj, prop) {
           return obj[prop]
           }
          }
          // 創建proxy,參數分別為target和handler,含義見上方解釋
          var proxy=new Proxy(target, handler)
          // 通過proxy訪問target對象
          console.log(proxy.x) // 輸出:1
          

          在上面代碼中,我們通過new Proxy來創建proxy對象,新創建的proxy對象將代理我們對target對象的訪問。當我們訪問proxy.x的時候,我們定義的handler中的get函數(trap)將被調用。get接受到的參數分別為target對象和我們要訪問的屬性名稱。而因為我們直接返回指定屬性的值,所以返回proxy.x的返回值就是target中對應屬性的值。

          當然這里我們可以寫任意的代碼,也可以返回任意的值。

          有哪些traps?

          上面我們介紹了Proxy的創建方式,實現了一個簡單的trap:get。所有的trap都是可選的,如果我們定義了trap,當我們執行特定操作的時候就會執行我們的trap,如果沒有定義trap,默認操作就是執行將操作直接傳遞到目標對象。

          接下來我們就來看一下JavaScript提供的traps都有哪些:

          • get - 當我們獲取目標對象屬性值的時候被調用,返回屬性的值
          • set - 和get對應,當我們寫入目標對象的屬性值時被調用,改寫屬性的值
          • deleteProperty - 當我們刪除目標對象的屬性值時被調用
          • has - 當我們對目標對象使用in操作符時被調用,返回指定的屬性是否存在
          • apply - 當我們對目標對象(這里是一個函數)執行函數調用時這個trap會被調用
          • construct - 當我們對目標對象(要求是一個函數)執行new操作符的時候被調用
          • ownKeys - 當我們調用Object.keys的時候這個trap將會被執行
          • getPrototypeOf - 當我們對目標對象調用Object.getPrototypeOf時這個trap將被執行
          • setPrototypeOf - 當我們對目標對象調用Object.setPrototypeOf時這個trap將被執行
          • isExtensible - 當我們對目標對象執行Object.isExtensible時這個trap將被執行
          • preventExtensions - 當我們對目標對象執行Object.preventExtensions時這個trap將被執行
          • getOwnPropertyDescriptor - 當我們對目標對象執行Object.getOwnPropertyDescriptor時這個trap將被調用
          • defineProperty - 當我們對目標對象執行Object.defineProperty時這個trap將被調用

          Reflect

          Reflect是一個對象,針對上面提到的每一個trap,它都定義了一個static方法。它主要用于方便我們寫trap函數的時候將操作傳遞到目標對象。比如:

          var target={
           x: 1,
          }
          var handler={
           get: function(...args) {
           	// 我們使用Reflect將操作直接傳遞到目標對象
           return Reflect.get(...args)
           },
           set: function(...args) {
           return Reflect.set(...args)
           }
          }
          var proxy=new Proxy(target, handler)
          console.log(proxy.x) // 輸出:1
          

          由于每個trap都有一個對應的Reflect方法,而且他們的參數都是一致的,所以當我們需要在trap中將操作直接傳遞到目標對象的時候使用Reflect中的方法是非常方便的。Reflect中的很多方法和Object中的一些方法都比較相似,但還是有一些小的差別,比如返回值的不同等。可以參考這個網址。

          和getter、setter的區別

          通過以上的描述,我們可以看到Proxy和getter、setter有點像。它們都可以做到當我們訪問或改寫一個對象的屬性值時調用一個特定的函數,但有以下區別:

          1. Proxy除了對屬性值的獲取和改寫還可以改變其他JavaScript基礎操作的執行方式,也就是它的功能比getter、setter多很多
          2. getter、setter是定義在對象本身上的,而Proxy是定義在另一個對象(proxy)上的。這表示Proxy允許我們比較方便的修改一些我們不方便直接改變其定義方式的對象的執行方式,比如一些第三方庫中定義的對象等。

          總結

          ES6的Proxy對很多人可能比較陌生,但其實它真的非常簡單,以上介紹的基本就是它的全部內容了。雖然實現起來比較簡單,但是它可以實現很多非常強大的功能,比如ImmerJS這個庫,它提供了用破壞性的語法來實現非破壞性操作的方式,我們打算下篇文章來介紹它

          升Web應用的性能從未像今天這樣刻不容緩。

          在線經濟活動的比例日益提高,就連發展中國家和地區的經濟活動都已經有5%以上在線進行了(相關數據請參考本文后面的資源)。在這個超級鏈接、隨時在線的現代世界,用戶的期望也遠非昔日可比。如果你的網站不能馬上響應,你的應用不能立即運行,用戶轉身就會投奔你的競爭對手。

          亞馬遜大約10年前的一項研究表明,頁面加載時間減少1/10秒,能夠使其營收增長1%。另一項近期的調查也顯示,一多半受訪站點所有者提到因為自己應用的性能不佳導致了收入減少或者用戶流失。

          一個網站到底多快才行?頁面加載每花1秒鐘,就有大約4%的用戶走掉。排名最靠前的電商站點的首次交互時間為1至3秒,這個區間的轉換率最高。顯而易見,Web應用性能的重要性與日俱增。

          提升性能其實不難,難的是怎么看到結果。本文給出能夠提升大約10倍網站性能的10個建議供大家參考。如此全面地涵蓋各種性能優化技術,這還是頭一回,但這些建議可能需要NGINX的一點支持。除了性能,這些建議也會涉及提升安全性。

          1 建議一:使用反向代理服務器讓應用更快更安全

          果你的Web應用只跑在一臺機器上,那要提升其性能非常簡單:換一臺更快的,多配幾個處理器,多加幾條內存,磁盤陣列也要高速的。換了以后,這臺機器上跑的WordPress服務器、Node.js或Java應用速度都會加快。(要是應用還會訪問另一臺數據庫服務器,那也簡單:找兩臺更快的機器,用更快的網絡連起來就行了。)

          麻煩在于,機器速度并不是問題。很多時候Web應用慢,是因為要在各種任務之間切換,一會兒要處理數千個連接上的用戶請求,一會兒要向磁盤讀寫文件,一會兒又要運行應用的代碼,一會兒又要去干別的。應用服務器因此可能出現各種狀況,耗盡內存、交換文件,或者讓很多請求等待一個硬盤I/O之類的任務。

          除了升級硬件,其實你還可以選擇另外一種完全不同的方法:加一臺反向代理服務器,分擔上述一些任務。反向代理服務器位于運行應用的機器之前,負責處理來自外網的請求。反向代理服務器直接連到互聯網,它與應用服務器通信使用的是快速的內部網絡。

          反向代理服務器可以讓應用服務器專注于構建頁面,然后交給反向代理向外網發送,而不必理會用戶與應用的交互。由于不必等待客戶端的響應,應用服務器的運行速度能達到接近最優的水平。

          增加反向代理服務器同時也可以為Web服務器增添靈活性。比如,假設執行某種任務的服務器過載了,那隨時可以再增加一臺同類服務器;而如果這臺服務器掛了,替換它也很容易。

          鑒于這種靈活性,反向代理服務器往往也是其他性能優化手段的先決條件,比如:

          • 負載均衡(參見“建議二”),反向代理服務器上運行負載均衡服務,把流量平均分配給幾臺應用服務器。有了負載均衡,添加應用服務器根本不需要修改應用。
          • 緩存靜態文件(參見“建議三”),圖片或代碼之類的可以直接請求的文件,都可以保存在反向代理服務器中,以便直接發給客戶端。這樣不僅可以更快地響應請求,還能減輕應用服務器的負擔,加快其運行速度。
          • 保證站點安全,可以配置反向代理服務器提升其安全級別,通過它監控來快速識別和響應攻擊,從而保存應用服務器安全。

          NGINX專門為使用反向代理服務器做了設計,使其天然支持上述優化。由于使用事件驅動的處理機制,NGINX比傳統服務器效率更高。NGINX Plus則增加了更高端的反向代理功能,如應用體檢、特有的請求路由、高級緩存和售后支持。

          傳統服務器與NGINX Worker的比較

          建議二:增加負載均衡服務器

          加負載均衡服務器相對簡單,但卻能顯著提升站點性能和安全性。通過它把流量分配給多個服務器,就可以不必升級Web服務器了。就算應用本身寫得不太好,或者難以擴展,負載均衡都可以在不做其他改變的情況下提升用戶體驗。

          負載均衡服務器首先是一個反向代理服務器(參見“建議一”),負責把來自互聯網的請求轉發給其他服務器。這里關鍵在于負載均衡服務器可以支持兩臺以上的應用服務器,使用一種選擇算法在不同的服務器間分配請求。最簡單的負載均衡算法是循環調度,即把新請求依次轉發給可用服務器中的下一臺服務器。其他算法還有把請求發給活動連接最少的服務器。NGINX Plus支持一種功能,就是把用戶會話保持在同一臺服務器上,叫做會話保持。

          負載均衡服務器可以避免一臺服務器過載而其他服務器過閑,從而極大提升性能。同時,有了它還可以讓Web服務器擴容更簡單,因為可以選用比較便宜的服務器,同時保證物盡其用。

          可以通過負載均衡調度的協議包括HTTP、HTTPS、SPDY、HTTP/2、WebSocket、FastCGI、SCGI、uwsgi、memcached,以及其他一些應用形式,包括基于TCP的應用和其他第四層的協議。為此,首先要分析Web應用,看性能短板在哪里,然后再確定使用哪一個。

          同一臺服務器或用于負載均衡的服務器也可以承擔其他任務,比如SSL終止、視客戶端不同支持HTTP/1/x或HTTP/2、緩存靜態文件。

          NGINX經常被用來做負載均衡,更多信息請參考我們以前發的介紹性文章、有關配置的文章、電子書和相關的在線視頻,當然還有文檔。我們的商業版本NGINX Plus支持更多的負載均衡功能,如基于服務器響應時間路由負載和支持微軟NTLM協議的負載均衡。

          建議三:緩存靜態及動態內容

          存能提升Web應用性能,因為可以更快地把內容交付給客戶端。緩存的策略包括預處理內容、在較快的設備上存儲內容、把內容保存在靠近客戶端的地方,以及同時運用這些策略。

          緩存有兩種。

          • 靜態內容緩存,不常變化的文件,如圖片(JPEG、PNG)和代碼(CSS、JavaScript),可以保存在邊緣服務器中,以便快速從內容或磁盤中獲取。
          • 動態內容緩存,很多Web應用會為每個頁面請求生成全新的HTML,把生成的每個HTML都緩存一小段時間,可能顯著減少需要生成的頁面總數,同時又可以保證交付的內容足夠新鮮。

          假設一個頁面每秒被查看10次,而你緩存它1秒,那么90%針對這個頁面的請求都將來自在緩存。如果你單獨緩存靜態內容,那么即使全新生成的頁面,很可能大部分都來自緩存的內容。

          緩存Web應用生成內容的技術主要分三種。

          • 把內容放到離用戶近的地方。離用戶近,傳輸時間少。
          • 把內容放到較快的機器上。機器快,檢索速度快。
          • 把內容從過度使用的機器中拿走。有時候機器會比在專注執行特定任務時慢很多,那是因為太多任務讓它們分心。這時候把內容拿到其他機器上,不僅對緩存的內容有好處,對非緩存的內容同樣有利,因為托管它們的主機的負擔減輕了。

          Web應用的緩存可以在Web應用服務器內部或外部實現。首先,考慮緩存動態內容,以減輕應用服務器的負載。其次,緩存用于靜態內容(包括那些動態生成內容的臨時副本),進一步減輕應用服務器的負擔。然后,考慮把緩存轉移到其他更快或更靠近用戶的機器,給應用服務器減負,縮短傳輸時間。

          用好緩存能顯著加快應用的響應速度。對很多網頁來說,大圖片之類的靜態數據,往往占據一半以上的內容。不用緩存,查詢和傳輸這類數據可能會花好幾秒鐘,而用緩存,則可能只要花幾分之一秒。

          可以舉一個例子來說明怎么使用緩存,NGINX和NGINX Plus通過兩個指令來設置緩存:proxy_cache_path和proxy_cache指定緩存的位置和大小、最長緩存時間以及其他參數。使用第三個(也是很受歡迎的)指令proxy_cache_use_stale,甚至可以告訴緩存在本來應該提供新鮮內容的服務器太忙或宕機時,提供原來的舊文件,對客戶端來說,拿到內容總比拿不到強。從用戶角度看,這樣也可以樹立你的站點或應用非常穩定的形象。

          NGINX Plus支持高級緩存功能,包括緩存凈化(caching purging)和通過控制板以可視化的形式展示緩存狀態,實現實時監控。

          要了解NGINX中關于緩存的更多信息,可以看看參考文檔和NGINX Plus Admin Guide中的NGINX Content Caching。另外,關注微信公眾號:Java技術棧,也可以獲取我整理的 NGINX 教程,都是干貨。

          注意: 緩存涉及開發、決策和運維,完善的緩存策略,比如本文提到的這些,能夠體現從DevOps角度考慮的價值。也說是說,開發人員、架構師、運維人員此時攜手,共同保障一個網站的功能、響應時間、安全和業務目標。


          建議四:壓縮數據

          縮同樣能極大提升性能。圖片、視頻、音樂等文件都有非常成熟和高效的壓縮標準(JPEG和PNG、MPEG-4、MP3),任何一個標準都可以把文件大小縮小一個數量級甚至更多。

          文本文件,包括HTML(純文本和HTML標簽)、CSS和JavaScript代碼,經常在不壓縮的情況下傳輸。壓縮這些數據對提升Web應用的感知性能有時候特別明顯,尤其是移動用戶的網絡很慢又不穩定的情況下。

          因為文本數據通過對于頁面交互能夠起到必要的支援作用,而多媒體數據則更多是錦上添花的作用。聰明的內容壓縮可以把HTML、JavaScript、CSS等文本內容的縮小30%以上,因此能夠相應地減少加載時間。

          如果你使用SSL,壓縮又可以減少必須經過SSL編碼的數據量,從而補償了壓縮這些數據的CPU時間。

          壓縮數據的方法非常多。比如,建議六中關于HTTP/2的部分就描述了一個新穎的壓縮思路,特別適合首部數據壓縮。還有一個關于文本壓縮的例子,就是可以在NGINX中開啟GZIP壓縮。預壓縮文本數據之后,可以使用gzip_static指令直接發送.gz文件。

          建議五:優化SSL/TLS

          來越多的網站在使用Secure Sockets Layer(SSL)及后來的Transport Layer Security(TLS)協議。SSL/TLS通過加密從源服務器發送給用戶的數據來提升網站安全性。Google會提升使用SSL/TLS的網站的搜索引擎排名,將有力地推動這一進程。點擊這里了解SSL/TLS運行機制詳解。另外,關注微信公眾號:Java技術棧,也可以獲取我整理的更多 HTTPS 教程,都是干貨。

          盡管采用率越來越高,但SSL/TLS造成的性能損失也困擾著很多網站。SSL/TLS拖慢網站的原因有兩個。

          1、每次打開新連接的初次握手都必須創建加密密鑰,而瀏覽器使用HTTP/1.x對每個2、服務器建立多個連接的方式進一步加劇了這個問題。

          服務器端加密數據和客戶端解密數據的操作同樣也是開銷。

          為了鼓勵人們使用SSL/TLS,HTTP/2和SPDY(參見建議六)的作者將這兩個協議設計為只讓瀏覽器針對一次會話建立一個連接。這樣就把SSL導致性能降低的兩個主要原因之一消滅掉了。然而,說到優化SSL/TLS性能,還是有很多事情可做。

          優化SSL/TLS的方法因Web服務器而異。以NGINX為例,NGINX使用OpenSSL,運行于普通機器上,能夠提供接近定制機器的性能。NGINX SSL performance詳細介紹了如何將SSL/TLS加密和解密的開銷降至最低。

          此外,這里還有一篇文章,介紹了很多種提升SSL/TLS性能的方法。簡單總結一下,涉及的技術主要有如下幾種。

          • 會話緩存。使用ssl_session_cache指令開啟緩存,緩存每次SSL/STL連接時用到的參數。
          • 會話票或ID。把特定SSL/TLS會話的信息保存為一個會話票或ID,以便連接重用,而不必重新握手。
          • OCSP封套。通過緩存SSL/TLS證書信息減少握手時間。

          NGINX和NGINX Plus都可以來終止SSL/TLS,即處理客戶端信息的加密和解密,同時與其他服務器保持明文通信。在NGINX或NGINX Plus中設置處理SSL/TLS終止可以采取這幾個步驟。而對于在接受TCP連接的服務器上使用NGINX Plus而言,可以參考這里的設置步驟。

          建議六:實現HTTP/2或SPDY

          經使用SSL/TLS的站點,如果再使用HTTP/2或SPDY則很可能提升性能,因為一個連接只要一次握手。尚未使用SSL/TLS、HTTP/2和SPDY的站點切換到SSL/TLS(通常會降低性能),從響應速度方面看,可能是一次倒退。點擊這里了解HTTP/2詳解。

          谷歌2012年開始SPDY項目,致力于在HTTP/1.x之上實現更快的速度。HTTP/2則是IETF最近批準的基于SPDY的標準。SPDY得到了廣泛支持,但很快就將被HTTP/2取代。

          SPDY和HTTP/2的關鍵在于只用一個連接,而非多個連接。這一個連接是多路復用的,因此可以同時承載多個請求和響應。

          只維持一個連接,可以省掉多個連接所需的設置和管理消耗。而且一個連接對SSL特別重要,因為可以將SSL/TLS建立安全連接所需的握手時間降至最少。

          SPDY協議要求使用SSL/TLS,HTTP/2并沒有正式要求,但目前所有支持HTTP/2的瀏覽器都只會在啟用SSL/TLS的情況下才會使用它。換句話說,支持HTTP/2的瀏覽器只有在網站使用SSL且服務器接受HTTP/2流量的情況下才會使用HTTP/2。否則,瀏覽器會基于HTTP/1.x通信。

          實現了SPDY或HTTP/2之后,域名分片、資源合并、圖片精靈等之前針對HTTP的性能優化措施就用不著了。因此也可以簡化代碼和部署。關于HTTP/2會帶來哪些變化,可以參考我們的這個白皮書。

          NGINX很早就開始支持SPDY,而且今天使用SPDY的大多數站點都在運行NGIN

          X。NGINX同樣率先支持了HTTP/2,2015年9月,NGINX開源和NGINX Plus開始支持 HTTP/2。

          隨著時間推移,NGINX希望大多數站點啟用SSL并遷移到HTTP/2。這樣不僅可以讓網站更安全,而且隨著新的優化技術不斷涌現,也可以通過簡單的代碼實現更高的性能。

          建議七:升級軟件

          升應用性能的一個簡單的方法,就是根據可靠性及性能選擇軟件。此外,高質量組件的開發者更可能不斷提升性能和修復問題,因此使用最新的穩定版本是劃算。新發布的版本會得到開發者和用戶更多的關注,同時也會利用新的編譯器優化技術,包括針對新硬件的調優。

          相對舊版本,新發布的穩定版本明顯性能更高。堅持升級,也可以保證在調優、問題修復和安全警報方面與時俱進。

          不升級軟件也會妨礙利用新能力。比如,HTTP/2目前要求OpenSSL 1.0.1。從2016年下半年開始,HTTP/2會要求OpenSSL 1.0.2,該版本發布于2015年1月。

          NGINX用戶可以從NGINX開源軟件的最新版本或NGINX Plus開始,它們支持套接字共享、線程池(參見下文),而且都會持續優化性能。因此,檢查一下自己的軟件,盡量把它們升級到最新的版本。

          建議八:調優Linux

          Linux是今天大多數Web服務器的底層操作系統,作為一切基礎設施的基礎,Linux對提升性能至關重要。默認情況下,很多Linux系統都比較保守,僅以桌面辦公為需求,以占用少量資源為調優目標。對于Web應用而言,為達到性能最佳,肯定需要重新調優。

          Linux優化因Web服務器而異。以NGINX為例,可以從以下幾方面考慮。另外,關注微信公眾號:Java技術棧,也可以獲取我整理的 NGINX 教程,都是干貨。

          存量隊列。如果發現有一些連接得不到處理,可以增大net.core.somaxconn,即等待NGINX處理的最大連接數。如果這個連接數限制過小,應該可以看到錯誤消息,可以逐步提高這個值,直到錯誤消息不再出現。

          • 文件描述符。NGINX對每個連接最多使用兩個文件描述符。如果系統服務于很多連接,可能需要增大sys.fs.file_max這個對描述符的系統級限制,以及nofile這個用戶文件描述符限制,以支持增大后的負載。
          • 臨時端口。在作為代理使用時,NGINX會為每個上游服務器創建臨時端口。可以設置net.ipv4.ip_local_port_range,增大端口值的范圍,以增加可用的端口量。此外,還可以減小net.ipv4.tcp_fin_timeout的值,它控制非活動端口釋放重用的等待時間,加快周轉。
          • 對NGINX而言,請參考NGINX性能調優指南,了解如何不費吹灰之力將你的Linux系統優化為能夠支持更大的吞吐量。

          建議九:調優Web服務器

          論使用什么Web服務器,都需要針對應用對其調優。以下建議適用于任何Web服務器,但會給出只有NGINX的設置說明。

          • 訪問日志。不要每個請求的日志都馬上寫到磁盤,可以在內存里做個緩存,然后批量定入。對NGINX而言,將buffer=_size_參數添加到access_log指令,等內存緩沖區寫滿后再把日志寫到磁盤。如果你添加了**flush=_time_**參數,那么緩沖區的內容也會按照指定時間寫入磁盤。
          • 緩沖。緩沖用于在內存里保存部分響應,直到緩沖區被填滿,可以實現對客戶端更有效的響應。無法寫入內存的響應會被寫到磁盤,從而降低性能。在NGINX的緩沖啟用時,可以使用proxy_buffer_size和proxy_buffers指令來管理它。
          • 客戶端活動連接。活動連接可以減少時間消耗,特別是在使用SSL/TLS的情下。對NGINX而言,可以針對客戶端提高keepalive_requests的數值,默認值為100;也可以增大keepalive_timeout的值,讓活動連接持續時間更長,從而讓后續請求得到更快響應。
          • 上游活動連接。上游連接,即連接到應用服務器、數據庫服務器的連接,同樣可以從活動連接的設置中獲得好處。對上游連接來說,可以增加活動連接,也就是每個工作進程可用的空閑活動連接的數量。這樣可以增進連接重用,減少重開連接。關于活動連接的更多信息,請參考這篇博客。
          • 限制。限制客戶端使用的資源可以提升性能和安全性。對NGINX而言,limit_conn和limit_conn_zone指令限制指定源的連接數,而limit_rate限制帶寬。這些設置可以防止合法用戶“侵吞”資源,同時也有助于防止攻擊。limit_req和limit_req_zone指令限制客戶端請求。對于到上游服務器的連接,可以在上游配置區的服務器指令中使用max_conns參數,它限制對上游服務器的連接,防止過載。相關的隊列指令會創建一個隊列,在max_conns限制到達后將指定的請求數保存指定的時間。
          • 工作進程。工作進程負責處理請求。NGINX采用基于事件的模型和OS相關的機制有效地在工作進程間分配請求。建議將worker_processes的值設置為每個CPU一個工作進程。如果需要,大多數系統都支持提高worker_connections的值(默認為512)。可以通過試驗找到最適合你系統的這個值。
          • 套接字分片。通常,一個套接字監聽器向所有工作進程分發新連接。套按字分片則為每個工作進程都創建一個套接字監聽器,由內核在套接字監聽器可用時為其指定連接。這樣可以減少鎖爭用,提升多核系統上的性能。要啟用套接字分片,在listen指令中包含reuseport參數。
          • 線程池。一個費時的操作會阻塞任何計算機進程。對Web服務器軟件來說,磁盤訪問可能阻礙很多較快的操作,比如內存中的計算和復制。在使用線程池的情況下,慢操作會被指定給一組獨立的任務,而主處理循環會繼續運行較快的操作。磁盤操作完成后,結果會返回到主處理循環。在NGINX中,read()系統調用和sendfile()被轉載到了線程池。

          提示,修改任何操作系統及周邊設備的設置時,每次只修改一項,然后測試性能。如果該項修改導致了問題,或者并未提升性能,再改回去。

          建議十:監控實時動態以發現問題和瓶頸

          存應用高性能的關鍵是實時監控應用性能。必須實時監控特定設備及相應Web基礎設施中應用的動態。

          監控站點活動多數情況下是被動的,它只告訴你發生了什么,至于如何發現和解決問題,則是你自己的事情。

          監控可以捕獲以下幾種問題:

          1、服務器停機

          2、服務器不穩,漏處理連接

          3、服務器出現大面積緩存失效

          4、服務器發送的內容不對

          New Relic或Dynatrace等全局性的性能監控工具,可以幫我們監控遠程加載頁面的時間,而NGINX則可以幫你監控應用交付這一端。應用的性能數據可以告訴你優化手段什么時候真正給用戶帶去了不同的體驗,以及什么時候需要擴容以滿足越來越多的流量。

          為了幫助用戶盡快發現問題,NGINX Plus增加了應用程序體檢功能,會報告經常重復出現的問題。NGINX Plus還具備session draining特性,會在已有任務完成前阻止新連接,以及慢啟動容量,從而讓恢復的服務器在負載均衡集群中達到應有的速度。使用得當的情況下,健康體檢會在問題顯著影響用戶體驗之前幫你定位問題,而session draining和慢啟動則讓你替換服務器時不影響感知的性能和在線時間。這張圖展示了NGINX Plus內置的實時活動監控的控制板,涵蓋了服務器、TCP連接和緩存。

          結論:10倍性能提升

          能提升因Web應用不同會有巨大差異。實際的提升取決于預算、時間,以及現有實現的與理想性能的差距。那么怎么讓你的應用獲得10倍的性能提升呢?

          為了幫大家理解每項優化建議的潛能,下面再針對之前的建議給出一些實施方針,希望大家各取所需。

          • 反向代理服務器及負載均衡。沒有負載均衡或池負載均衡,可能導致極低的性能。添加一個反向代理服務器,比如NGINX,可以減少Web應用在內存和磁盤之間的往返。負載均衡可以把任務從過載的服務器轉移到空閑的服務器,也便于擴展。這些改變能極大地提升性能,與原有的部署方式最差的時候相比,10倍性能提升是很輕松的事,即使不到10倍那也在總體上有了質的飛躍。
          • 緩存動態和靜態內容。如果你的Web服務器同時又充當了應用服務器,那么通過緩存動態內容就可以達到高峰期10倍的性能提升。緩存靜態內容也可以有幾倍的性能提升。
          • 壓縮數據。使用JPEG、PNG、MPEG-4以及MP3等壓縮格式能顯著提升性能。如果這些手段都用上了,那么壓縮的文本數據(代碼及HTML)可以將初始頁面加載時間提升兩倍。
          • 優化SSL/TLS。安全握手對性能有很大影響,因此對其進行優化可以讓初次響應加快兩倍,對于文本內容較多的網站尤其如此。優化SSL/TLS下的媒體文件帶來的性能提升很小。
          • 實施HTTP/2和SPDY。在使用SSL/TLS的情況下,這兩個協議有可能提升網站的整體性能。
          • 調優Linux和Web服務器。使用優化的緩沖策略、使用活動連接,將耗時的任務轉載至獨立的線程池,可以顯著提升性能。比如線程池可以將磁盤操作密集性任務的性能提升至少一個數量級。

          希望大家自己多嘗試以上技術,也希望大家分享自己在性能改進方面的心得。

          譯者:為之漫筆

          來源:http://www.zcfy.cc/article/10-tips-for-10x-application-performance-nginx-22.html

          原文:https://www.nginx.com/blog/10-tips-for-10x-application-performance/


          主站蜘蛛池模板: 无码毛片一区二区三区中文字幕| 在线电影一区二区| 亚洲AV无码一区二区二三区软件 | 国产一区二区三区亚洲综合| 国产一区二区三区播放| 久久久无码一区二区三区| 久久中文字幕无码一区二区| 在线观看精品一区| 日本中文字幕一区二区有码在线| 日产精品久久久一区二区| 国产在线精品一区二区中文| 欧洲精品码一区二区三区| 精品一区二区无码AV| 国产一区二区三区免费看| 国产成人久久一区二区不卡三区| 国精品无码一区二区三区左线 | 亚洲成a人一区二区三区| 麻豆一区二区三区蜜桃免费| 精品理论片一区二区三区| 日本精品视频一区二区| 国产精品香蕉在线一区| 亚洲国产日韩一区高清在线| 亚洲一区在线视频| 一区二区三区视频观看| 天堂Aⅴ无码一区二区三区| 一区二区三区免费视频播放器| 国产精品一区二区三区高清在线| 中文字幕乱码亚洲精品一区| 日韩高清一区二区三区不卡 | 91福利一区二区| 精品国产AⅤ一区二区三区4区| 久久久国产精品亚洲一区| 人妻夜夜爽天天爽爽一区| 亚洲综合在线成人一区| 国产精品亚洲一区二区在线观看| 日本韩国一区二区三区| 国产综合精品一区二区三区| 国产一区二区三区不卡AV| 色一情一乱一区二区三区啪啪高| 一区二区三区四区视频在线| 无码精品视频一区二区三区|