<dfn id="hx5t3"><strike id="hx5t3"><em id="hx5t3"></em></strike></dfn>

    <thead id="hx5t3"></thead><nobr id="hx5t3"><font id="hx5t3"><rp id="hx5t3"></rp></font></nobr>

    <listing id="hx5t3"></listing>

    <var id="hx5t3"></var>
    <big id="hx5t3"></big>

      
      

      <output id="hx5t3"><ruby id="hx5t3"></ruby></output>
      <menuitem id="hx5t3"><dfn id="hx5t3"></dfn></menuitem>

      <big id="hx5t3"></big>

        luckness

        luckness 查看完整檔案

        填寫現居城市  |  填寫畢業院校  |  填寫所在公司/組織填寫個人主網站
        編輯
        _ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 個人簡介什么都沒有

        個人動態

        luckness 發布了文章 · 2月20日

        cookie知識總結

        最近,清除cookie的時候,使用了document.cookie = '',然后就發現沒有作用。于是,帶著上述疑問,找個時間研究了下cookie。本文主要是基于我遇到問題,然后解決問題的思路寫的,并不是以從0到1講述cookie的思路寫的。

        為了解決上述問題,以及在解決問題中的發散思考,主要通過以下問題:

        1. document.cookie = ''能否用來清除cookie;
        2. 同時設置了expires和max-age,哪個有效;
        3. 假設已經有一個名為hello的cookie,再次設置名為hello的cookie是新增還是修改;
        4. 如何把字符串形式的cookie改成json的形式;
        5. 如果設置了多個同名cookie,獲取cookie時以哪個為準;

        學習了以下知識點:

        1. cookie賦值語法;
        2. cookie賦值算法;
        3. cookie取值算法。

        預備知識

        本文參考了英文文檔,所以提前說明以下英文對應的中文翻譯:

        1. cookie store: cookie表,瀏覽器內部用于存儲cookie信息的數據結構;
        2. cookie name: 一個cookie的名稱;
        3. cookie value: 一個cookie的值;
        4. cookie attribute: 一個cookie的屬性,cookie除了基本的名稱和值之外,還有用來進一步描述cookie的屬性,比如Expires,Max-Age,Domain,Path,Secure HttpOnly等。這些屬性是提供給用戶設置cookie的時候使用的,并不是cookie內部實際存儲的屬性。

        其他預備知識:

        每個cookie在實際存儲的時候,會有下述字段:name, value, expiry-time, domain, path, creation-time, last-access-time, persistent-flag, host-only-flag, secure-only-flag, and http-only-flag??梢钥闯鲞@些字段和設置cookie時的屬性并不是一對一關系,其中,

        1. expiry-time是通過Expires和Max-Age得出來的;
        2. persistent-flag和是否設置了過期時間有關;
        3. creation-time(創建時間)和last-access-time(最后一次訪問時間)是瀏覽器自動存儲的屬性。

        document.cookie賦值語法

        這部分內容主要是基于以下問題:

        1. document.cookie = ''能否用來清除cookie?

        首先看下document.cookie的定義:

        The Document property cookie lets you read and write cookies associated with the document. It serves as a getter and setter for the actual values of the cookies.

        從上面可以看到,document.cookie只是提供了一個getter和setter方法來訪問和設置cookie,并不是直接修改cookie的值。什么意思呢?

        我們知道,JS提供了兩種類型的屬性,data propertyaccessor property。分別舉個例子對比看下:

        // data property
        var obj1 = {
            hello: 'name'
        }
        Object.getOwnPropertyDescriptor(obj1, 'hello')

        image.png

        // accessor property
        var realHello = '' // 實際存儲obj2.hello的變量
        var obj2 = {
            get hello () {
                return 'custom get ' + realHello
            },
            set hello (val) {
                realHello = 'custom set ' + val
            }
        }
        obj2.hello = 'name'
        
        console.log(obj2.hello)
        Object.getOwnPropertyDescriptor(obj2, 'hello')

        image.png

        可以看到用于描述obj1hello屬性的是valuewritable,用于描述obj2hello屬性的是getset。簡單的說,獲取和修改data property是直接的,獲取和修改accessor property是間接的,可以通過該屬性的getset做自定義處理。我們熟悉的Vue 2.0響應式的data的屬性就是accessor property。

        document的cookie屬性也是accessor property。驗證的時候發現一個有意思的問題,最終是在document的原型上找到的屬性描述符,以后有時間了研究下:

        Object.getOwnPropertyDescriptor(document, 'cookie') // undefined
        Object.getOwnPropertyDescriptor(document.__proto__.__proto__, 'cookie')

        image.png

        我們看下document.cookie的語法:

        document.cookie = newCookie;

        修改的時候,newCookie的格式需要滿足如下形式,且一次只能設置/更新單個cookie:

        In the code above, newCookie is a string of form key=value. Note that you can only set/update a single cookie at a time using this method.

        舉個例子,打開MDN document.cookie頁面。添加一個cookie,每個cookie后面可以添加和該cookie相關的一系列屬性,通過分號;分割:

        // cookie默認過期時間是session,也就是瀏覽器關閉的時候,該cookie就失效了
        document.cookie = 'hello1=world1;'
        
        // 使用max-age設置過期時間:max-age以秒為單位,設置1個小時之后過期:3600 = 60 * 60
        document.cookie = 'hello2=world2;max-age=3600'
        
        // 使用expires設置過期時間:expires是GMT形式的日期格式,設置1小時之后過期
        var current = new Date()
        current.setTime(current.getTime() + 3600000) // setTime單位是ms,3600000 = 60 * 60 * 1000
        document.cookie = `hello3=world3;expires=${current.toUTCString()}`

        Chrome Application Cookies內容截圖如下:
        image.png

        既然document.cookie只能設置/更新單個cookie,當賦值為空字符串的時候,那就是什么都沒有做。也就是document.cookie = ''不會產生任何作用,不能用于清除cookie。

        那么,
        問:如何使用document.cookie清除一個cookie呢?
        答:可以在設置cookie的時候,設置一個已經過去的過期時間:

        // 使用expires設置一個過去一小時之前的時間
        document.cookie = 'hello2=world2;max-age=-3600'
        
        // 使用expires設置一個過去一小時之前的時間
        var current = new Date()
        current.setTime(current.getTime() - 3600000)
        document.cookie = `hello3=world3;expires=${current.toUTCString()}`

        現在,只剩下了一個hello1:
        image.png

        注:瀏覽器開發者工具是提供了清除cookie的操作的,上述主要討論的是如何通過js清除cookie。

        cookie賦值算法

        這部分內容主要是基于以下兩個問題:

        1. 同時設置了expires和max-age,哪個有效?
        2. 假設已經有一個名為hello的cookie,再次設置名為hello的cookie的時候是新增還是修改?

        問題1: 同時設置了expires和max-age,哪個有效?

        前面的例子中,我使用了expires和max-age,我的疑問是感覺這兩個屬性是干了一樣的事情,如果我同時設置了這兩個屬性的話,哪個生效呢?

        帶著這個疑問,找到了RFC 6265 - HTTP State Management Mechanism。這個規范里面明確定義了如何設置/更新cookie。

        就下面的例子,我們按照文檔找一下答案:

        var current = new Date()
        console.log('now: ', current.toUTCString()) // now:  Sat, 20 Feb 2021 11:41:02 GMT
        current.setTime(current.getTime() + 3600000)
        // 使用expires設置為一小時之后過期:60 * 60 * 1000 = 3600000ms
        // 使用max-age設置為兩小時之后過期:2 * 60 * 60 = 7200s
        document.cookie = `hello=world;expires=${current.toUTCString()};max-age=7200` // hello=world;expires=Sat, 20 Feb 2021 12:41:02 GMT;max-age=7200

        這部分的內容主要是在第五部分的5.2。首先,它會把設置的這個值通過逗號分割成多個部分,然后通過等號把每個部分分成key-value的形式:

        hello=world;expires=Sat, 20 Feb 2021 12:41:02 GMT;max-age=7200
        // 變為
        hello=world // 第一個分號前面的是這個cookie的name和value,后面的是這個cookie的屬性
        expires=Sat, 20 Feb 2021 12:41:02 GMT // 屬性
        max-age=7200 // 屬性
        // 變為
        hello: world
        expires: expires=Sat, 20 Feb 2021 12:41:02 GMT
        max-age: 7200

        然后看每個屬性,文檔中5.2.*部分,算法會解析每個key-value,并放在一個cookie屬性列表cookie-attribute-list里面,分析屬性的時候不區分大小寫,expires和max-age轉化為:

        Expires: expires=Sat, 20 Feb 2021 12:41:02 GMT
        Max-Age: expires=Sat, 20 Feb 2021 13:41:02 GMT

        屬性都分析完之后,我們可以得到和每條cookie相關的三個部分:cookie-name(cookie名),cookie-value(cookie值),cookie-attribute-list(cookie屬性列表)。然后進入設置階段,關鍵在5.3部分的第三步:
        image.png
        首先看上面生成的cookie屬性列表里面是否包含Max-Age,如果包含,把cookie的過期時間設置成最后一個Max-Age的值;如果沒有Max-Age,才會查看是否包含Expires,如果包含,把cookie的過期時間設置成最后一個Expires的值。這里last attribute的意思是我們在給cookie賦值的時候,是可以寫多個max-age的,這個時候取最后一個語法有效的max-age。

        所以,簡單的說:當expires和max-age同時出現的時候,max-age的優先級更高。

        Chrome截圖也證明了max-age優先級更高:
        image.png

        假設已經有一個名為hello的cookie,再次設置名為hello的cookie是新增還是修改?

        我們接著往下看5.3部分的第11、12步:
        image.png
        在11步中,首先判斷已有cookie中是否有和本cookie同時具有相同的name(名稱),domain(域),path(路徑)的cookie。
        11.1 如果有,標記為old-cookie;
        11.2 如果新cookie不是通過http的方式設置的,并且old-cookie設置了只能用于http,忽略新cookie;
        11.3 把新cookie的creation-time(創建時間)字段更新為old-cookie的創建時間;
        11.4 從cookie表中移除old-cookie;
        12 把新cookie插入到cookie表里面。

        所以,可以得出:判斷是新增還是修改的標準是cookie表中是否存在和新cookie的name,domain和path屬性都一樣的cookie。

        在設置cookie的時候,如果設置結果和你想要的不符,可以按照上面例子的思路,查看RFC 6265 - HTTP State Management Mechanism文檔的5.2和5.3部分。

        cookie取值算法

        這部分內容主要是基于以下兩個問題:

        1. 如何把字符串形式的cookie改成json的形式?
        2. 如果設置了多個同名cookie,獲取cookie時以哪個為準?

        如何把字符串形式的cookie改成json的形式?

        之所以會遇到這個問題,主要是解決下面這個場景:
        如果通過WebSocket發送請求,在連接建立的時候,是可以獲取cookie信息的。但是,一旦連接建立成功,WebSocket一直連接的時候,發送的數據是不會攜帶cookie信息的。如果在連接建立之后,用戶做了登出操作,這個時候其實不應該再響應用戶請求,并且斷開連接。

        這時,問題來了,如何判斷當用戶登出的時候不再響應用戶請求呢?
        其實和http一樣,每次請求帶上cookie信息就可以了。只不過發送http請求的時候,瀏覽器幫我們做了這部分工作。WebSocket的話,就得我們手動添加上了。

        我們可以通過document.cookie獲取cookie值,但是該值是一個表示所有cookie的字符串。為了獲取某個cookie的值,我們就得自己解析。要想解析,就得知道cookie字符串的取值規則。這部分內容主要是RFC 6265 - HTTP State Management Mechanism文檔的5.4部分。

        image.png

        1. 通過步驟4的第2步,可以知道不同的cookie是通過分號和空格; 拼接起來的;
        2. 通過步驟4的第1步,可以知道最終結果中只會出現cookie的name和value,cookie的屬性并不會出現。name和value是通過等號=拼接起來的;

        所以,解析cookie的為代碼可以表示如下:

        var cookieStr = document.cookie
        var cookieArray = cookieStr.split('; ') // 獲得每個cookie的字符串形式組成的數組
        var cookieObj = {}
        cookieArray.forEach(item => {
            var index = item.indexOf('=') // 獲取每個cookie字符串的key和value
            if (index !== -1) { // 沒有使用split的原因是value里面也是可以包含=號的
                cookieObj[item.slice(0, index)] = item.slice(index + 1)
            }
        })
        console.log(cookieObj)

        如果設置了多個同名cookie,獲取cookie時以哪個為準?

        例如,有下面幾個cookie:

        document.cookie = 'hello4=world4;'
        document.cookie = 'hello5=world;'
        document.cookie = 'hello5=worldShortPath;Path=/en-US'
        document.cookie = 'hello6=world6;'

        Chrome截圖如下:
        image.png

        這部分內容還是在RFC 6265 - HTTP State Management Mechanism文檔的[5.4部分],只不過是在第2步。首先第1步是從cookie store(cookie列表)中找到和請求地址相關的所有cookie。然后執行第2步:
        image.png

        1. 通過步驟2的第1步,可以知道具有更長path屬性的cookie是放在前面的;
        2. 通過步驟2的第2步,可以知道path屬性長度相同的時候,creation-times創建時間越早的放在前面。

        綜上,所有同名的cookie都會出現在最終的cookie字符串中。并且,cookie是先按照path屬性就行排序,然后按照creation-times創建時間屬性進行排序。但是,步驟二下面有注釋,這種排序方式是實踐得出的比較好的排序方式,但是瀏覽器不一定按照這種排序方式實現。

        就上面的例子,我們在設置完cookie之后,在控制臺中查看document.cookie:

        console.log(document.cookie) // hello4=world4; hello5=world; hello6=world6; hello5=worldShortPath

        可以看到hello5的路徑最短,所以放在最后,其他的按照設置順序,也就是創建時間排序。

        總結

        在cookie賦值語法部分,我們知道document.cookie = ''不能用來清除cookie;
        在cookie賦值算法部分,我們知道:

        1. 同時設置了expires和max-age,max-age有效;
        2. 假設已經有一個名為hello的cookie,再次設置名為hello的cookie的時候,如果name,domain和path屬性完全相同就是修改,否則是新增;

        在cookie取值算法部分,我們知道:

        1. 如何把字符串形式的cookie改成json的形式的時候,可以先通過; 分割一次,在通過=分割一次;
        2. 如果設置了多個同名cookie,所有同名的cookie都會出現在最終的cookie字符串中,按照path屬性的長度和creation-times排序。

        如果你遇到和cookie賦值語法、賦值算法和取值算法相關的問題,可以參照上述部分遇到的問題的解決思路去看看能否解決問題。

        如有錯誤,歡迎留言討論。

        參考鏈接

        1. RFC 6265 - HTTP State Management Mechanism
        2. MDN document.cookie
        查看原文

        贊 9 收藏 7 評論 0

        luckness 發布了文章 · 1月28日

        THREE.js morphTargets介紹

        本文介紹下THREE.js里面和geometry相關的morphTargets。

        THREE.js有兩種基本的geometry:Geometry和BufferGeometry。這兩種類型創建morphTargets的方式不一樣,所以會分別進行講述。因此,本文包括以下三個部分:

        1. morphTargets是啥;
        2. 給Geometry添加morphTargets;
        3. 給BufferGeomtry添加morphAttributes;

        本文示例基于THREE.js的124版本,可以通過THREE.REVISION屬性獲取THREE.js的版本。

        morphTargets是啥

        morph圖像變換、變形的意思。那么,一個物體的幾何形態是如何表示的呢?
        頂點位置。

        THREE.js中用于表示頂點位置的數據包括Geometry的vertices屬性,以及,BufferGeometry的attributes屬性的position屬性。那么,如何表示變形后的物體呢?

        THREE.js采用的是通過變形的頂點來定義變形物體。這里,原物體的頂點和變形后物體的頂點是一一對應關系,包括以下特點:

        1. 數量相同;
        2. 順序一致;

        前面提到Geometry和BufferGeometry是通過不同的屬性來存儲頂點信息的,所以導致它們存儲變形頂點的屬性也是不一樣的。所以,下文會針對這兩種類型分別進行介紹。

        給Geometry添加morphTargets

        首先,創建一個長寬高都是2,材質是線框的紅色立方體:

        const boxGeometry = new THREE.BoxGeometry(2, 2, 2)
        const material = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true })
        const mesh = new THREE.Mesh(boxGeometry, material)

        設置Geometry變形后的形態。因為變形后的頂點信息是和Geometry的vertices屬性相對應的,所以我們先看下原vertices屬性是咋樣的:

        console.log(boxGeometry.vertices)

        image.png
        因為是要把圖形縮小一半,所以頂點信息的單位長度變為一半就行,我們通過一個循環實現:

        const morphVertices = boxGeometry.vertices.map(vector => {
            return vector.clone().multiplyScalar(0.5)
        })
        console.log(morphVertices)

        image.png
        給Geometry添加morphTargets屬性:

        boxGeometry.morphTargets.push({
            target: 'halfBox', // 名字隨便設置,目前還沒有發現有啥用
            vertices: morphVertices
        })

        添加之后,發現并沒有生效,經過查找資料,發現除了設置Geometry之外,還需要Material和Mesh的配合:

        1. 初始化Material的時候,設置morphTargets屬性為true;
        2. 給Mesh添加morphTargetInfluences屬性,屬性值可以是0-1之間,表示應用變形的程度;
        const material = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true, morphTargets: true })
        mesh.morphTargetInfluences = [1]

        截圖如下,藍色線框是原始長寬高為2的立方體,此處用來對比:
        morphTargetInfluence: 1
        image.png
        morphTargetInfluence: 0.5
        image.png

        此時,遇到了一些疑問:

        1. 如果influence的值介于0到1之間,計算實際頂點位置的算法是啥;
        2. 查看Geometry的morphTargets屬性會發現該屬性是一個數組,同樣Mesh的morphTargetInfluences屬性也是一個數組,所以這是不是表示我們可以添加多個morphTargets;
        3. 如果第2項的答案是可以添加多個,那么,多個morphTargets如何同時作用呢;

        我們可以在上面的基礎上再添加一個morphTargets,這個morphTargets把原立方體放大2倍:

        const morphVertices2 = boxGeometry.vertices.map(vector => {
            return vector.clone().multiplyScalar(2)
        })
        boxGeometry.morphTargets.push({
            target: 'doubleBox',
            vertices: morphVertices2
        })

        然后重新設置morphTargetInfluences:

        mesh.morphTargetInfluences = [1, 0.8]

        那么,最后的效果圖立方體的尺寸是多少呢?

        通過查看源碼,發現文件src/renderers/shaders/ShaderChunk/morphtarget_vertex.glsl.js中有這樣的注釋:

        // morphTargetBaseInfluence is set based on BufferGeometry.morphTargetsRelative value:
        // When morphTargetsRelative is false, this is set to 1 - sum(influences); this results in position = sum((target - base) * influence)
        // When morphTargetsRelative is true, this is set to 1; as a result, all morph targets are simply added to the base after weighting
        transformed *= morphTargetBaseInfluence;
        transformed += morphTarget0 * morphTargetInfluences[ 0 ];
        transformed += morphTarget1 * morphTargetInfluences[ 1 ];
        transformed += morphTarget2 * morphTargetInfluences[ 2 ];
        transformed += morphTarget3 * morphTargetInfluences[ 3 ];
        // ...

        也就是

        1. 當BufferGeometry.morphTargetsRelative是false的時候,計算方式為:base + sum((target - base) * influence),或者按照上述部分的代碼邏輯:base * (1 - sum(influences)) + sum(target * influence),這兩個計算方式是等價的。
        2. 當BufferGeometry.morphTargetsRelative是true的時候,計算方式是:base + sum(target * influence)。

        上述中,base指原始頂點的值,target指每個變形定義的頂點的值,influence是每個target對應的影響值。

        還存在一個問題,計算方式是和BufferGeometry.morphTargetsRelative的值相關的,但是我們用的是Geometry,并沒有morphTargetsRelative屬性。又經過一番查找,發現Geometry是有一個對應的BufferGeometry的,掛在_bufferGeometry屬性下面。

        需要注意的是,我們創建完Geometry,在首次渲染之前,THREE.js并不會給Geometry創建_bufferGeometry,那么如何捕捉這個設置morphTargetsRelative屬性的時機呢?我使用的是Mesh.onBeforeRender回調:

        mesh.onBeforeRender = function () {
            boxGeometry._bufferGeometry.morphTargetsRelative = true // 默認是false
        }

        當morphTargetsRelative是false的時候,立方體的長寬高是2 + (2 * 0.5 - 2) * 1 + (2 * 2 - 2) * 0.8 = 2.6,我是通過設置上面那個藍色的線框立方體為2.6,進行比對來驗證的。

        當morphTargetsRelative是true的時候,立方體的長寬高是2 + 2 * 0.5 * 1 + 2 * 2 * 0.8 = 6.2。

        更多morphTargets同時作用的細節可以參見src/renderers/webgl/WebGLMorphtargets.js文件,比如當morphTargets的個數超過8個的時候。

        給BufferGeomtry添加morphAttributes

        與Geometry的morphTargets對應的是BufferGeometry的morphAttributes。

        同樣,首先創建一個長寬高都是2,材質是線框的紅色立方體:

        const boxGeometry = new THREE.BoxBufferGeometry(2, 2, 2)
        const material = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true, morphTargets: true })
        const mesh = new THREE.Mesh(boxGeometry, material)
        scene.add(mesh)

        此時,頂點數據存儲在BufferGeometry.attributes.position里面,所以,遍歷這個數據,生成一個新的morphAttribute,然后添加在morphAttributes屬性上:

        const morphPositions = []
        const positions = boxGeometry.attributes.position.array
        for (let i = 0; i < positions.length; i++) {
          morphPositions.push(positions[i] * 0.5)
        }
        const morphAttribute = new THREE.BufferAttribute(Float32Array.from(morphPositions), 3)
        morphAttribute.name = 'halfBox'
        boxGeometry.morphAttributes.position = [ // 注意,我們這里修改的是position屬性,對應attributes.position
          morphAttribute
        ]

        Material和Mesh的修改與前面一樣。
        同樣,我們可以添加多個morphAttribute,對應上面的例子:

        const morphPositions2 = []
        const positions2 = boxGeometry.attributes.position.array
        for (let i = 0; i < positions.length; i++) {
          morphPositions2.push(positions2[i] * 2)
        }
        const morphAttribute2 = new THREE.BufferAttribute(Float32Array.from(morphPositions2), 3)
        morphAttribute2.name = 'doubleBox'
        boxGeometry.morphAttributes.position.push(morphAttribute2)
        
        boxGeometry.morphTargetsRelative = true // 比geometry設置morphTargetsRelative的方式要簡單

        總結

        本文通過Geometry和BufferGeometry介紹了morphTargets是啥,以及多個morphTargets同時存在的時候,最終頂點信息的計算方法,希望大家有所收獲。

        如有錯誤,歡迎留言評論。

        查看原文

        贊 2 收藏 1 評論 0

        luckness 收藏了文章 · 1月25日

        程序員練級攻略(2018):前端基礎和底層原理

        圖片描述

        想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你!

        這個是我訂閱 陳皓老師在極客上的專欄《左耳聽風》,我整理出來是為了自己方便學習,同時也分享給你們一起學習,當然如果有興趣,可以去訂閱,為了避免廣告嫌疑,我這就不多說了!以下第一人稱是指陳皓老師。

        對于前端的學習和提高,我的基本思路是這樣的。首先,前端的三個最基本的東西 HTML5、CSS3 和 JavaScript(ES6)是必須要學好的。這其中有很多很多的技術,比如,CSS3 引申出來的 Canvas(位圖)、SVG(矢量圖) 和 WebGL(3D 圖),以及 CSS 的各種圖形變換可以讓你做出非常豐富的渲染效果和動畫效果。

        ES6 簡直就是把 JavaScript 帶到了一個新的臺階,JavaScript 語言的強大,大大釋放了前端開發人員的生產力,讓前端得以開發更為復雜的代碼和程序,于是像 React 和 Vue 這樣的框架開始成為前端編程的不二之選。

        我一直認為學習任何知識都要從基礎出發,所以我會有很大的篇幅在講各種技術的基礎知識和基本原理,尤其是如下的這些知識,都是前端程序員需要一塊一塊啃掉的硬骨頭。

        • JavaScript 的核心原理。這里我會給出好些網上很不錯的講 JavaScript 的原理的文章或圖書,你一定要學好語言的特性和其中的各種坑。
        • 瀏覽器的工作原理。這也是一塊硬骨頭,我覺得這是前端程序員需要了解和明白的東西,不然,你將無法深入下去。
        • 網絡協議 HTTP。也是要著重了解的,尤其是 HTTP/2,還有 HTTP 的幾種請求方式:短連接、長連接、Stream 連接、WebSocket 連接。
        • 前端性能調優。有了以上的這些基礎后,你就可以進入前端性能調優的主題了,我相信你可以很容易上手各種性能調優技術的。
        • 框架學習。我只給了 React 和 Vue 兩個框架。就這兩個框架來說,Virtual DOM 技術是其底層技術,組件化是其思想,管理組件的狀態是其重點。而對于 React 來說,函數式編程又是其編程思想,所以,這些基礎技術都是你需要好好研究和學習的。
        • UI 設計。設計也是前端需要做的一個事,比如像 Google 的 Material UI,或是比較流行的 Atomic Design 等應該是前端工程師需要學習的。

        而對于工具類的東西,這里我基本沒怎么涉及,因為本文主要還是從原理和基礎入手。那些工具我覺得都很簡單,就像學習 Java 我沒有讓你去學習 Maven 一樣,因為只要你去動手了,這種知識你自然就會獲得,我們還是把精力重點放在更重要的地方。

        下面我們從前端基礎和底層原理開始講起。先來講講 HTML5 相關的內容。

        HTML5

        • HTML5 權威指南 ,本書面向初學者和中等水平 Web 開發人員,是牢固掌握 HTML5、CSS3 和 JavaScript 的必讀之作。書看起來比較厚,是因為里面的代碼很多。
        • HTML5 Canvas 核心技術 ,如果你要做 HTML5 游戲的話,這本書必讀。

        對于 SVG、Canvas 和 WebGL 這三個對應于矢量圖、位圖和 3D 圖的渲染來說,給前端開發帶來了重武器,很多 HTML5 小游戲也因此蓬勃發展。所以,你可以學習一下。

        學習這三個技術,我個人覺得最好的地方是 MDN。

        最后是幾個資源列表。

        CSS

        在《程序員練級攻略(2018)》系列文章最開始,我們就推薦過 CSS 的在線學習文檔,這里再推薦一下

        MDN Web Doc - CSS 。我個人覺得只要你仔細讀一下文檔,CSS 并不難學。絕大多數覺得難的,一方面是文檔沒讀透,另一方面是瀏覽器支持的標準不一致。所以,學好 CSS 最關鍵的還是要仔細地讀文檔。

        之后,在寫 CSS 的時候,你會發現,你的 CSS 中有很多看起來相似的東西。你的 DRY - Don’t Repeat Yourself 潔癖告訴你,這是不對的。所以,你需要學會使用 LESSSaSS
        這兩個 CSS 預處理工具,其可以幫你提高很多效率。

        然后,你需要學習一下 CSS 的書寫規范,前面的《程序員修養》一文中提到過一些,這里再補充幾個。

        如果你需要更有效率,那么你還需要使用一些 CSS Framework,其中最著名的就是 Twitter 公司的 Bootstrap,其有很多不錯的 UI 組件,頁面布局方案,可以讓你非常方便也非??焖俚亻_發頁面。除此之外,還有,主打清新 UI 的 Semantic UI 、主打響應式界面的 Foundation 和基于 Flexbox 的 Bulma。

        當然,在使用 CSS 之前,你需要把你瀏覽器中的一些 HTML 標簽給標準化掉。所以,推薦幾個 Reset 或標準化的 CSS 庫:Normalize 、MiniRest.css、 sanitize.cssunstyle.css。

        關于更多的 CSS 框架,你可以參看 Awesome CSS Frameworks

        接下來,是幾個公司的 CSS 相關實踐,供你參考。

        CodePen’s CSS

        Github 的 CSS

        Medium’s CSS is actually pretty f*ing good

        CSS at BBC Sport

        Refining The Way We Structure Our CSS At Trello

        最后是一個可以寫出可擴展的 CSS 的閱讀列表 A Scalable CSS Reading List

        JavaScript

        下面是學習 JavaScript 的一些圖書和文章。

        瀏覽器原理

        你需要了解一下瀏覽器是怎么工作的,所以,你必需要看《How browsers work》。這篇文章受眾之大,后來被人重新整理并發布為《How Browsers Work: Behind the scenes of modern web browsers》,其中還包括中文版。這篇文章非常非常長,所以,你要有耐心看完。如果你想看個精簡版的,可以看我在 Coolshell 上發的《瀏覽器的渲染原理簡介》或是看一下這個幻燈片。

        然后,是對 Virtual DOM 的學習。Virtual DOM 是 React 的一個非常核心的技術細節,它也是前端渲染和性能的關鍵技術。所以,你有必要要好好學習一下這個技術的實現原理和算法。當然,前提條件是你需要學習過前面我所推薦過的瀏覽器的工作原理。下面是一些不錯的文章可以幫你學習這一技術。

        網絡協議

        小結

        總結一下今天的內容。我一直認為學習任何知識都要從基礎出發,所以今天我主要講述了 HTML5、CSS3 和 JavaScript(ES6)這三大基礎核心,給出了大量的圖書、文章以及其他一些相關的學習資源。之后,我建議你學習瀏覽器的工作原理和網絡協議相關的內容。我認為,掌握這些原理也是學好前端知識的前提和基礎。值得花時間,好好學習消化。

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

        你們的點贊是我持續分享好東西的動力,歡迎點贊!

        交流

        干貨系列文章匯總如下,覺得不錯點個Star,歡迎 加群 互相學習。

        https://github.com/qq44924588...

        我是小智,公眾號「大遷世界」作者,對前端技術保持學習愛好者。我會經常分享自己所學所看的干貨,在進階的路上,共勉!

        關注公眾號,后臺回復福利,即可看到福利,你懂的。

        clipboard.png

        查看原文

        luckness 關注了用戶 · 1月23日

        dreamapplehappy @dreamapplehappy

        微信公眾號「關山不難越」
        除了代碼,生活還有很多值得經歷和體味的事情;去旅游,去看不一樣的風景;活,該快樂!

        無欲速,無見小利。欲速則不達,見小利則大事不成

        個人博客:https://github.com/dreamapple...

        關注 511

        luckness 贊了文章 · 1月23日

        2020年,我第一次很正式地寫年終總結

        好像我一直沒有寫年終總結的習慣,之前也一直都是看別人寫年終總結。有時看別人的年終總結,會感嘆別人這一年過的好充實??戳撕芏鄷?,去了很多地方;或者結識了很多新的朋友,能力得到了很多的提高等等吧。也常常會感覺自己這一年好像沒干什么事情,會有那么一點沮喪。

        當我寫這篇文章的時候,我突然意識到自己好像已經忘記了2020年定下的什么目標了。不難想象自己當時定下目標的時候肯定是躊躇滿志的,沒想到已經連定下的什么目標都忘記了。

        所以寫一下年終總結對我來說還是很有必要的,一方面可以記錄一下自己這一年做了哪些事情。另一方面也可以記錄自己對來年的一個目標,等到下一年再次寫年終總結的時候就有了一個對比??梢灾雷约菏沁M步了還是倒退了,可以看看自己之前定下的目標都實現了多少。這樣我覺得也挺好的,今年就作為一個開始吧。

        工作和技術分享,輸出了14篇文章

        2020年的后半年加入了一家中型的互聯網公司,再次開啟了自己的職場生涯。我也重拾了自己寫博客的習慣。作為一個開發者,寫博客在目前來說依然是一個不錯的方式讓大家認識和了解你。對于自己來說,寫博客可以記錄和沉淀自己學習過的知識或者解決的問題。如果自己下次再遇到類似的問題,就知道如何去解決,如果忘記了就查找一下之前的博客。

        寫博客也是一個強化自己知識的過程。很多時候你以為你掌握了,但是真的到把這些知識寫成博客,然后想要分享給別人的時候,你會發現自己好像并沒有掌握的很牢固,也會有一些不熟練的地方。而寫博客這個過程恰恰就是幫助你把這些不熟悉的地方打通的一個很好的方法。

        關于寫博客我自己也有一些心得,想在這里跟大家分享一下。我覺得寫博客最重要的其實是堅持,然后是自己對博客質量的要求,最后是平臺的選擇。

        先說一下堅持吧,我感覺每一個前端工程師或多或少都倒騰過自己的博客。我最早寫博客是在 CSDN 和博客園,后來又轉移到思否,掘金和知乎上,也使用過 Hexo,以及 Ghost 搭建過自己的博客。反正前前后后換了很多平臺,但是文章卻沒留下來幾篇。所以后來我就決定不再糾結在那個平臺寫博客了。而是把精力放在堅持寫博客上,我們可以看看,基本上那些技術很厲害的大牛也都是專注于自己博客的內容,而不是糾結博客的形式。所以我后來就把我自己的博客放在了Github上,簡簡單單的,反而讓我有了堅持寫下去的動力??粗恼乱黄黄黾?,也是會有一點成就感的。

        然后就是博客的質量了,當我們能夠堅持寫博客之后,接下來就需要專注于我們博客的質量。我們寫博客大多是為了分享給別人或者用做記錄以便自己查看。如果是分享給別人看的話,如何讓大家從你的博客中學習到東西,或者能夠吸引大家來看就是一個需要考慮的問題了。一般來說,只要我們能夠把要分享的內容講清楚,內容邏輯條理清晰。這樣下來,文章的質量一般不會太差。如果你的文筆還不錯那就又是一個加分項了。

        持續高質量的文章輸出很快就會給你帶來很多的關注者,也會有很多厲害的人主動跟你聯系。這樣下來,你通過寫博客不但能夠提升自己的技術,提高自己的知名度,還可以跟優秀的人建立聯系。這些都是你的寶貴財富,也都是可以轉換為你自己實實在在的利益的。更重要的是,你寫下的文章隨著時間的推移,看到的人會越來越多,會持續給你帶來更多收獲的。

        最后就是平臺的選擇了,不同平臺用戶感興趣的不一樣,用戶活躍度也不一樣。這個要根據你自己的文章在各個平臺的數據來判斷,可以選擇一兩個作為主要的分享平臺,其它的作為輔助。選擇好一個合適的平臺后,可以多跟這個平臺的用戶保持積極的溝通交流,你會收到來自大家的肯定,這會讓你有繼續堅持寫下去的動力。

        2020年寫的一些文章

        在2020年我一共寫了14篇博客,不算很多,但是每一篇博客我都寫得很用心。我個人在數量和質量上進行權衡的時候,我還是比較看重質量的。當然在接下來的2021年,我希望自己能夠盡量半個月寫一篇文章。在質量不下降的情況下,提升寫博客的數量。

        之前的博客主要圍繞兩個主題來寫的,一個是關于設計模式的。我把這個系列稱作設計模式大冒險。另一個系列是關于正則表達式的,這個系列最近的更新比較少了。主要是如果要深入的講解好正則表達式,需要學習的知識很多;我也不希望自己一知半解的就去寫一篇文章。這樣是對自己文章和讀者的不負責。在新的一年,如果精力允許的話我還會繼續更新關于正則表達式的一些文章,希望大家可以保持關注。

        細心的你也許會發現,我分享的知識都是不限于某一種語言的。我個人認為,學習知識應該學習通用的,適用范圍更廣的。比如上面我分享的設計模式正則表達式,以及數據結構算法,還有編程的思想等等。這些知識一旦掌握了,在每一種編程語言上面都可以使用。用世俗的評判標準就是“性價比”比較高。

        當然我不是說你就不需要對某一種語言,某一個框架,或者某一個方向深入研究了。這些也是很有必要的。只不過有些類似的東西,你掌握好一個就可以了。你的時間和精力是有限的,要把時間放在更有用的地方。當然,如果是因為工作的需要的話,那就是另外一回事了。工作的事情還是要做好的,要首先把自己工作需要掌握的知識掌握熟練之后,再考慮去學習其它的提升自己的知識。

        上面是關于寫博客,學習技術和技術分享的事情。接下來分享一些關于開發過程中的心得。我們在平時的開發中,如果遇到一些不是很好開發的需求,一定要問一下產品做這個功能的目的是為了什么。不要上來就去開始寫代碼了,很多情況下想要實現一個功能有很多種解決辦法,產品告訴你的方式也許是一個不好實現的方式。這個時候你就可以跟產品溝通換一種方式是否可行,很多情況下經過我們溝通之后,發現可以用一種簡單的方式實現同樣的效果。

        多跟產品溝通,多思考為什么要這樣做。這樣,在以后的開發過程中,你會減少很多不必要的編碼勞動,也能夠工作的比較舒服一點。

        主線程小程序的持續維護

        關注我的一些朋友知道我跟我的女朋友在2019年做了一個創業項目,是一個學習打卡類小程序,小程序的名字是主線程,如果你想了解更多可以看一下之前思否的一個小采訪,如果全身心投入1年,但是收入是0,你還愿意做獨立開發者嗎?。

        主線程小程序的一些截圖

        在2020年的4月份我們決定暫停主線程項目的繼續開發,只做日常的系統維護工作。因為當時的經濟狀況已經不允許我們繼續投入到這個項目中去了。這個決定在目前的我們看來還算是一個正確的決定。因為如果繼續下去,我們可能會面臨一系列的問題和壓力。這些問題和壓力對于當時的我們來說大概率是解決不了的。

        那么,主線程這個項目成功了嗎?這個問題要看你是從哪個角度去看了。如果單純從世俗的金錢回報上說,我們肯定是失敗了,而且失敗得很徹底。不僅沒有賺到一分錢(除去一些贊賞),還倒貼了一筆錢。但是如果站在另外一個角度看的話,我覺得對于我們兩個來說還是很成功的。首先我們從0到1把這個產品做了出來,并且這個產品也得到了不少用戶的好評,一些關于產品的體驗評價可以在這里瀏覽哦。到目前為止,在沒有花錢推廣的情況下已經有15000+的用戶了。也算是一個小小的成就吧。

        每當收到用戶對主線程的夸獎的時候,我們那種開心和成就感是用金錢換不來的。其實最重要的收獲是在于我們對創業,對產品和看待事情的角度都發生了變化。以前的角色是作為公司的員工,現在是以一個創業者的身份來看待和處理這些事情,這中間我們學習到了很多。

        總之我們不后悔花費了一年的時間開發了一個沒有盈利的產品,接下來我們還會繼續維護這個項目。如果你有什么關于創業和開發產品的想法想跟我交流的,也歡迎添加我的個人微信(請備注來意),我們一起溝通交流一下吧。

        身體健康和讀書

        去年前半年因為沒有上班,所以有比較多的時間鍛煉身體。跑步基本每周有兩三次,那段時間經歷也還算充沛。后半年上班之后,時間比較少,再加上冬天也比較冷。跑步的次數就很少了,從Kepp上的記錄來看,我后半年關于做俯臥撐的鍛煉就只做了13次,因為鍛煉比較少??梢悦黠@感覺到每天工作到快晚上下班的時候會比較疲憊。

        也因為工作的原因,久坐在辦公室,頸椎和腰有時也會感覺到有點不舒服。關于吃飯,好的一點是去年點外賣的次數變少了。去外面小餐館吃飯的次數變多了。我個人會覺得出去走動一下挺好的,呼吸一下外面的新鮮空氣,給自己換個心情。走路去外面吃個飯也算是一個小小的鍛煉吧。

        去年讀書沒有讀很多,《增長黑客》 這本書基本讀完了,感覺收獲很多。對于想創業的朋友應該很有幫助的,推薦感興趣的可以讀一讀。還有一本書是 《豐子愷:無寵不驚過一生》 ,還沒讀完,不過每次讀都會給我一些啟發,給我帶來一些內心的平靜。感興趣的也可以讀一下。

        因為短視頻的興起,感覺自己身邊很多的人都沒有了讀書的習慣了。很多人可能都沒有耐心讀完一篇很長的文章了。我個人感覺這不是一個很好的習慣,我之前也有一段時間對抖音比較沉迷,每周在上面花費的時間有4,5個小時。后來我意識到,看這些短視頻并不能給我帶來什么能力和技術的提升,大部分情況下都只是消耗了我的時間而已。所以我慢慢就不怎么看了,現在手機上的抖音雖然沒有被我卸載,但是一直處于待更新的狀態。

        我覺得,知識的獲取需要一個比較完整的體系。看書或者看一個系列的文章和視頻在目前依然還是一個挺好的方式去獲取知識。碎片化的知識會讓你感覺自己掌握了很多知識,但是這些知識沒有成為一個體系,沒有融合進你自己的知識系統的話,這些知識是很難被你再次吸收和利用的。我自己平時比較喜歡在 《珍新聞》 上面獲取一些資訊,個人感覺里面的文章都還是不錯的,大部分能夠把一個事情講明白,講解完整。很多文章也會帶給你一些思考。如果感興趣的話,可以體驗一下。

        新年的計劃

        迎接新的一年

        關于去年的一些總結已經寫得差不多了,接下來就是對2021的的一些展望了。下面是我列的一些想在2021年完成的一些目標。

        • 閱讀12本書,希望自己可以一個月讀一本書,書的種類不做限制,先培養自己讀書的習慣。
        • 博客繼續更新,希望半個月可以寫一篇。會繼續更新設計模式大冒險系列,和正則表達式系列。應該還會加上數據結構和算法。之前寫文章會比較糾結一些細節,導致寫一篇文章用的時間比較久,新的一年我要把寫文章的流程好好優化一下,爭取在原來的基礎上再多寫幾篇高質量的文章。
        • 爭取每周可以鍛煉一次,不管是跑步,俯臥撐鍛煉還是爬山或者其他的運動。把身體搞好是很重要的事情。
        • 多認識一些新的朋友,多跟不同行業的人溝通交流。提升一下自己的人脈和社交水平。

        上面的這些就是我打算在2021年完成的一些事情了,當然還有一些我現在正在做但是不知道自己能不能堅持下來的事情。如果我堅持下來了,會告訴大家的。希望大家保持關注。

        如果你有什么想跟我溝通的,可以添加我的微信或者關注我的公眾號關山不難越,新的一年我們一起加油吧。新的一年祝大家都身體健康,然后定下的目標都能夠實現。

        本文參與了 SegmentFault 思否征文「2020 總結」,歡迎正在閱讀的你也加入
        查看原文

        贊 10 收藏 1 評論 4

        luckness 發布了文章 · 1月22日

        THREE.js如何擴展已有材質

        最近,想要給一個立方體不同的面賦不同的材質,可以是純色也可以是貼圖。然后,我就覺得一個簡單的shaderMaterial就可以解決了。但是放到實際應用場景中發現,別的物體有光照效果,我寫的沒有光照效果。所以還得給自定義著色器添加光照效果,于是就有了這篇文章。

        本文主要從以下幾個方面進行講述:

        1. 創建沒有光照效果的立方體;
        2. 擴展lambert材質,創建有光照效果的立方體;

        適用人群:對THREE.js和glsl有基本了解的人。

        創建沒有光照效果的立方體

        本示例會創建一個前后左右面是純色,上下面是貼圖的立方體。該部分的內容主要包括以下部分:

        1. 創建bufferGeometry;
        2. 自定義shaderMaterial,在shaderMaterial里面判斷是用純色還是貼圖;
        3. 創建mesh。

        創建bufferGeometry

        因為想更深入的了解THREE.js的實現原理,所以這塊沒有直接使用BoxBufferGeometry,而是自己定義頂點信息:

        const geometry = new THREE.BufferGeometry()
        const position = [ // 每個面兩個三角形,每個三角形三個頂點,每個頂點三個坐標值,所以一個三角形是3*3=9個值,一個面是3*3*2=18個值
          -1, -1, 1, 1, -1, 1, 1, 1, 1, // front face
          1, 1, 1, -1, 1, 1, -1, -1, 1,
          1, -1, 1, 1, -1, -1, 1, 1, -1, // right face
          1, 1, -1, 1, 1, 1, 1, -1, 1,
          1, -1, -1, -1, -1, -1, -1, 1, -1, // back face
          -1, 1, -1, 1, 1, -1, 1, -1, -1,
          -1, -1, -1, -1, -1, 1, -1, 1, 1, // left face
          -1, 1, 1, -1, 1, -1, -1, -1, -1,
          -1, 1, 1, 1, 1, 1, 1, 1, -1, // top face
          1, 1, -1, -1, 1, -1, -1, 1, 1,
          1, -1, 1, -1, -1, 1, -1, -1, -1, // bottom face
          -1, -1, -1, 1, -1, -1, 1, -1, 1
        ]
        // 定義了一個長寬高都是2的立方體,所以上面xyz的坐標要么是1,要么是-1
        geometry.setAttribute('position', new THREE.BufferAttribute(Float32Array.from(position), 3))

        然后,給每個頂點添加顏色信息,每個頂點既可以是純色也可以是貼圖,純色需要rgb三個分量,貼圖需要uv兩個分量,所以每個頂點至少需要三個分量來表示。

        那么,如何判斷這個頂點是純色還是貼圖呢?
        我們當然可以再使用一個數組來表示。但是注意到上面貼圖只需要兩個分量,那么我們就可以利用第三個分量來判斷。glsl語言里面rgb色值的范圍是0-1,所以我們可以使用這個范圍之外的值表示這是一個貼圖。

        那取什么值呢?我們這個立方體定義了上下面是貼圖,也就是貼圖不只一個,那么這個值還要能推導出是第幾個貼圖。我這里設置了一個textureBaseIndex2的變量。

        const colors = []
        const textureBaseIndex = 2
        for (let i = 0; i < 12; i++) {
          switch (i) {
            case 0: // front color
            case 1:
              colors.push(1, 0, 0, 1, 0, 0, 1, 0, 0) // 紅
              break
            case 2: // right color
            case 3:
              colors.push(0, 1, 0, 0, 1, 0, 0, 1, 0) // 綠
              break
            case 4: // back color
            case 5:
              colors.push(0, 0, 1, 0, 0, 1, 0, 0, 1) // 藍
              break;
            case 6: // left color
            case 7:
              colors.push(1, 1, 0, 1, 1, 0, 1, 1, 0) // 黃
              break
            case 8: // top texture uv,前兩個分量表示uv,第三個分量表示取第幾個紋理,在紋理實際索引值的基礎上加上textureBaseIndex
              colors.push(0, 0, textureBaseIndex + 0, 1, 0, textureBaseIndex + 0, 1, 1, textureBaseIndex + 0)
              break
            case 9:
              colors.push(1, 1, textureBaseIndex + 0, 0, 1, textureBaseIndex + 0, 0, 0, textureBaseIndex + 0)
              break
            case 10: // bottom texture uv,前兩個分量表示uv,第三個分量表示取第幾個紋理,在紋理實際索引值的基礎上加上textureBaseIndex
              colors.push(1, 1, textureBaseIndex + 1, 0, 1, textureBaseIndex + 1, 0, 0, textureBaseIndex + 1)
              break
            case 11:
              colors.push(0, 0, textureBaseIndex + 1, 1, 0, textureBaseIndex + 1, 1, 1, textureBaseIndex + 1)
              break
          }
        }
        geometry.setAttribute('color', new THREE.BufferAttribute(Float32Array.from(colors), 3))

        自定義shanderMaterial

        頂點著色器的代碼比較簡單,把color屬性通過varying變量vColor傳給片元著色器:

        function getVertexShader () {
          return `
            attribute vec3 color;
        
            varying vec3 vColor;
        
            void main () {
              vColor = color;
              gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
            }
          `
        }

        接下來是片元著色器,主要有以下幾點:

        1. 通過vColor.z判斷是純色還是貼圖;
        2. 把貼圖信息通過sampler2D數組傳入,然后在根據vColor.z獲取數組下標的時候,前面在生成下標的時候加了一個textureBaseIndex,所以用的時候得先減去;
        3. 通過下標獲取sampler2D數組中的某一項的時候,不能直接使用textures[index],glsl要求[]里面的內容必須是Integral constant expression,所以使用了一個generateSwitch函數動態生成一系列if代碼;

        完整代碼如下:

        function getFragmentShader (textureLength, textureBaseIndex) {
          function generateSwitch () {
            let str = ''
            for (let i = 0; i < textureLength; i++) {
              str += `${str.length ? 'else' : ''} if (index == ${i}) {
                gl_FragColor = texture2D(textures[${i}], vec2(vColor.x, vColor.y));
              }
              `
            }
        
            return str
          }
        
          return `
            ${textureLength ? `
              uniform sampler2D textures[${textureLength}];
            ` : ''}
        
            varying vec3 vColor;
        
            void main () {
              ${textureLength ? `
                if (vColor.z <= 1.0) {
                  gl_FragColor = vec4(vColor, 1.0);
                } else {
                  int index = int(vColor.z) - ${textureBaseIndex};
                  ${generateSwitch()}
                }` : `
                gl_FragColor = vec4(vColor, 1.0);
                `
              }
            }
          `
        }

        生成自定義材質:

        const textures = [
          new THREE.TextureLoader().load('./textures/colors.png'), // 頂面貼圖
          new THREE.TextureLoader().load('./textures/colors.png') // 底面貼圖
        ]
        const material = new THREE.ShaderMaterial({
          uniforms: {
            textures: { value: textures } // 片元著色器中會使用
          },
          vertexShader: getVertexShader(),
          fragmentShader: getFragmentShader(textures.length, textureBaseIndex)
        })

        創建mesh

        這步就比較簡單了,創建一個mesh,并添加到場景中:

        const mesh = new THREE.Mesh(geometry, material)
        scene.add(mesh)

        這樣,立方體就創建好了。本例使用了基本的WebGLRenderer,Scene,PerspectiveCamera,沒有特殊處理,這里就不再寫了。實現效果截圖如下:
        front/right/top面效果截圖
        image.png
        back/left/bottom面效果截圖
        image.png

        擴展lambert材質,創建有光照效果的立方體

        我的實際應用場景中的物體是lambert材質,也就是MeshLambertMaterial。所以,下面的實例代碼以擴展lamert材質的光照效果為例。要想使用該實現方案,最好研究下THREE.js的源碼。

        THREE.js里面預先定義了一系列材質,MeshLambertMaterial材質就是其中之一。這部分代碼在src/renderers/shaders文件夾下面,ShaderLib.js里面是材質的入口,比如MeshLambertMaterial:

        const ShaderLib = {
            lambert: {
                uniforms: mergeUniforms( [ // uniform變量
                    UniformsLib.common,
                    UniformsLib.specularmap,
                    UniformsLib.envmap,
                    UniformsLib.aomap,
                    UniformsLib.lightmap,
                    UniformsLib.emissivemap,
                    UniformsLib.fog,
                    UniformsLib.lights,
                    {
                        emissive: { value: new Color( 0x000000 ) }
                    }
                ] ),
        
                vertexShader: ShaderChunk.meshlambert_vert, // 頂點著色器代碼
                fragmentShader: ShaderChunk.meshlambert_frag // 片元著色器代碼
            },
        }

        ShaderChunk和ShaderLib文件夾下面就是實際的著色器代碼,區別是ShaderLib是THREE.js給我們直接使用的,ShaderChunk是更細粒度的代碼。ShderLib里面的不同材質有很多共有的代碼,所以這個共有的代碼就提取成一個個ShaderChunk,達到復用的目的。一個材質是由多個ShaderChunk生成的。我們可以打開ShaderLib/meshlambert_vert.glsl.js文件,會發現里面有很多#include語句,這些語句最后會被替換為實際的ShaderChunk里面的片段。

        我們看到shaders文件夾下面只是定義了材質的結構以及glsl代碼片段,那么,完整效果的代碼是在哪生成的呢?
        src/renderers/webgl/WebGLProgram.js文件。

        列一下這個文件我了解的一些知識點:

        1. 首先根據我們創建材質時的參數,定義一些#define變量,添加在著色器代碼的前面;
        2. 解析ShaderLib里面的代碼,把#include語句替換為實際代碼,參見resolveIncludes函數;

        更重要的是,ShaderLib里面預定義的一些材質,掛在了THREE變量上,這樣我們就可以獲得原始代碼,并通過修改部分glsl代碼達到擴展材質的目的。

        比如,上面的那個例子,首先改造一下頂點著色器:

        1. 在默認的lambert頂點著色器代碼前面添加屬性變量和varying變量;
        2. 在main函數里面給varying變量賦值;
        3. 具體插在原始main函數的哪一行看你的需求;
        function getVertexShader () {
          let shader = `
            attribute vec3 color;
            varying vec3 vColor;
          ` + THREE.ShaderLib.lambert.vertexShader
        
          const index = shader.indexOf('#include <uv_vertex>')
          shader = shader.slice(0, index) + `
            vColor = color;
          ` + shader.slice(index)
        
          return shader
        }

        片元著色器的改造如下:

        1. 在默認的lambert片元著色器代碼前面添加uniform變量和varying變量;
        2. 在main函數里面插入我們的代碼,插入位置我選在了#include <color_fragment>后面,因為這個代碼片段和我現在的修改做了類似的事情,所以插在這個位置是可以的。注意,此時就不是直接給gl_FragColor賦值了,而是把效果加在diffuseColor變量上。實際開發的時候,具體修改哪個值就得參考THREE.js源碼了。
        function getFragmentShader (textureLength, textureBaseIndex) {
          function generateSwitch () {
            let str = ''
            for (let i = 0; i < textureLength; i++) {
              str += `${str.length ? 'else' : ''} if (index == ${i}) {
                diffuseColor *= texture2D(textures[${i}], vec2(vColor.x, vColor.y));
              }
              `
            }
        
            return str
          }
        
          let shader = `
            uniform sampler2D textures[${textureLength}];
            varying vec3 vColor;
          ` + THREE.ShaderLib.lambert.fragmentShader
        
          const index = shader.indexOf('#include <color_fragment>')
          shader = shader.slice(0, index) + `
            ${textureLength ? `
              if (vColor.z <= 1.0) {
                diffuseColor.rgb *= vColor;
              } else {
                int index = int(vColor.z) - ${textureBaseIndex};
                ${generateSwitch()}
              }` : `
              diffuseColor.rgb *= vColor;
              `
            }
          ` + shader.slice(index)
        
          return shader
        }

        然后,創建著色器:

        1. 修改一下uniform變量,把lambert默認的uniform變量也添加進去;
        2. 添加lights參數為true,否則代碼報錯;
        3. THREE源碼默認diffuse是0xeeeeee,覆蓋一下,修改為0xffffff;
        const material = new THREE.ShaderMaterial({
          uniforms: THREE.UniformsUtils.merge([
            THREE.ShaderLib.lambert.uniforms,
            {
              textures: { value: textures }
            },
            {
              diffuse: {
                value: new THREE.Color(0xffffff)
              }
            }
          ]),
          vertexShader: getVertexShader(),
          fragmentShader: getFragmentShader(textures.length, textureBaseIndex),
          lights: true
        })

        這個時候刷新頁面,會發現是一個黑色的立方體,這是因為我們還沒有添加光源:

        const light = new THREE.DirectionalLight( 0xffffff ); // 平行光
        light.position.set( 1, 1, 1 );
        scene.add( light );
        
        const ambient = new THREE.AmbientLight(0xffffff, 0.7); // 環境光
        scene.add(ambient)

        之所以添加兩個光源是因為發現:

        1. 環境光不受幾何物體法線影響;
        2. 平行光受幾何物體法線影響;

        添加上述代碼后,如果把環境光注釋掉,會發現材質還是黑色的,這是因為上面創建的geometry沒有法線信息,所以需要使用下面的方法添加一下法線信息:

        geometry.computeVertexNormals()

        最終效果截圖如下:
        front/right/top面效果截圖,同時受平行光和環境光影響
        image.png
        back/left/bottom面效果截圖,不在平行光照射范圍內,只受環境光影響
        image.png

        總結

        本文例子只是為了講解如何擴展已有材質,可能并沒有任何使用意義。

        上述觀點是基于目前對THREE.js的研究結果,可能會有認知錯誤。如有,歡迎留言評論。

        參考資料:

        1. Extending the Built-in Phong Material Shader in Three.js
        2. Integral constant expression
        查看原文

        贊 3 收藏 2 評論 0

        luckness 發布了文章 · 1月5日

        前端WebSocket知識點總結

        最近研究了下WebSocket,總結下目前對WebSocket的認知。本文不是基于WebSocket展開的一個從0到1的詳細介紹。如果你從來沒有了解過WebScoket,建議可以先搜一些介紹WebSocket的文章,這類文章還是挺多的,我就不再贅述了。

        下面的內容是基于你對WebSocket有基本了解后展開的幾個小的知識點:

        1. ping/pong協議;
        2. 如何使ERROR_INTERNET_DISCONNECTED錯誤信息不顯示在控制臺;

        ping/pong協議

        背景:連接WebSocket的時候,發現WebSocket剛連接上沒過多久就斷開了,為了保持長時間的連接,就想到了ping/pong協議。

        問題:

        1. ping/pong是一種特殊的幀類型嗎,還是說只是一種設計思想?
        2. JS有原生方法支持發送ping/pong消息嗎

        通過WebSocket協議,發現ping/pong確實是一種特殊的幀類型:

        The Ping frame contains an opcode of 0x9.
        The Pong frame contains an opcode of 0xA.

        那么,上面所說的opcode又是什么東西呢?講opcode就得說到幀數據格式
        image.png
        通過上圖可以發現,除了最后面的Payload Data,也就是我們要發送的數據之外,還會有一些其他信息。我覺得可以類比http請求的請求頭部分。上圖中第5-8位表示的就是opcode的內容。其余字段的含義可以參考上述WebSocket規范,或者搜WebSocket協議數據幀格式,這類博客還是挺多的。

        拿nodeJS舉個例子:
        在瀏覽器端發起WebSocket的時候,會發送一個http請求,注意請求頭里面的Upgrade字段,意思就是我要升級到websocket連接:

        GET /chat HTTP/1.1
        Host: example.com:8000
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
        Sec-WebSocket-Version: 13
        

        此時,nodeJS就可以監聽upgrade事件,去做拒絕或者升級操作,注意下這個事件里面有個參數socket:

        socket: <stream.Duplex> Network socket between the server and client

        socket有一個write方法,該方法是可以用來寫幀數據的,也就是上面幀格式里面的全部數據,而不僅僅是Payload Data。

        ws倉庫就是使用了socket的write方法發送了根據WebSocket協議定義的ping/pong,部分關鍵代碼如下:

        doPing(data, mask, readOnly, cb) {
          this.sendFrame(
            Sender.frame(data, {
              fin: true,
              rsv1: false,
              opcode: 0x09, // ping opcode
              mask,
              readOnly
            }),
            cb
          );
        }
        doPong(data, mask, readOnly, cb) {
          this.sendFrame(
            Sender.frame(data, {
              fin: true,
              rsv1: false,
              opcode: 0x0a, // pong opcode
              mask,
              readOnly
            }),
            cb
          );
        }
        sendFrame(list, cb) {
          if (list.length === 2) {
            this._socket.cork();
            this._socket.write(list[0]);
            this._socket.write(list[1], cb);
            this._socket.uncork();
          } else {
            this._socket.write(list[0], cb);
          }
        }

        所以,nodeJS是可以實現WebSocket協議定義的ping/pong幀的。原因是我們可以拿到socket對象,并且該對象提供了可以發送完整幀數據的方法。那么瀏覽器端呢?

        瀏覽器提供了原生的WebSocket構造函數用來創建一個WebSocket實例,該實例只提供了一個send方法,并且該send方法只能用來發送上述協議中Payload Data的內容,瀏覽器會根據send的參數自動生成一個完整的幀數據。所以,在瀏覽器端是沒法控制除了Payload Data之外的幀內容的,也就是無法自定義opcode。所以,也就實現不了WebSocket規范定義的ping/pong協議。

        此時,我們就可以把ping/pong當成一種用來解決特定問題的設計模式。既然我們只能自定義Payload Data的內容,那么我們可以簡單的在Payload Data里面添加一個字段用于區分是ping/pong幀,還是普通的數據幀,比如type。當type字段是ping/pong的時候表明是ping/pong幀,如果是其他字段才是普通的數據幀。

        如何使ERROR_INTERNET_DISCONNECTED錯誤信息不顯示在控制臺

        當斷網的時候,連接WebSocket會發現瀏覽器控制臺會log一個錯誤信息:

        WebSocket connection to 'ws://...' failed: Error in connection establishment: net::ERR_INTERNET_DISCONNECTED

        原先的開發經驗是,控制臺如果有報錯的話,肯定是代碼某個地方有錯誤,并且沒有被我們的代碼捕獲到,所以就會在控制臺拋出,如果使用了try catch 或者全局的window.onerror捕獲到了錯誤信息,就不會在控制臺打印了。所以,我就嘗試了上述方法,發現捕捉不到,還是會在控制臺log。

        另外,WebSocket提供了兩個事件,onerror和onclose。當發生上述錯誤信息的時候,onerror和onclose是會被調用的。但是,此時控制臺還是會有上述報錯信息。

        經過一番查找,發現無法阻止上述錯誤信息顯示在控制臺。

        那么,為什么瀏覽器會設計這樣的行為呢?猜測原因如下:
        上面說到通過onerror和onclose事件是可以捕捉到WebSocket創建失敗的,但是,查看這兩個事件的參數,我們只能從中找到一個code是1006的屬性,輸出在控制臺的錯誤信息ERR_INTERNET_DISCONNECTED在參數里面找不到。接著,看一下code1006相關的東西:

        User agents must not convey any failure information to scripts in a way that would allow a script to distinguish the following situations:
        
        *   A server whose host name could not be resolved.
        *   A server to which packets could not successfully be routed.
        *   A server that refused the connection on the specified port.
        *   A server that failed to correctly perform a TLS handshake (e.g., the server certificate can't be verified).
        *   A server that did not complete the opening handshake (e.g. because it was not a WebSocket server).
        *   A WebSocket server that sent a correct opening handshake, but that specified options that caused the client to drop the connection (e.g. the server specified a subprotocol that the client did not offer).
        *   A WebSocket server that abruptly closed the connection after successfully completing the opening handshake.
        
        In all of these cases, the the WebSocket connection close code would be 1006, as required by WebSocket Protocol. 
        
        Allowing a script to distinguish these cases would allow a script to probe the user's local network in preparation for an attack.
        

        從上述規范可以看到,規范是禁止瀏覽器向腳本傳遞下述造成WebSocket連接失敗的具體原因的,只允許向腳本傳遞一個1006的code碼,否則,用戶就可以探測到局部網的信息,進而發起攻擊。舉個例子,上面那種斷網的情況,腳本中只能得到1006的狀態碼,比如下面這種報錯

        Error in connection establishment: net::ERR_CONNECTION_REFUSED

        也只能從onerror中獲得一個1006的code碼。

        所以,作為開發人員,瀏覽器要怎么在告訴我們具體的錯誤信息的同時又阻止有可能發生的攻擊呢?答案就是在控制臺把具體的錯誤信息log出來。

        總結

        基于目前了解的知識總結的一篇博客,如有錯誤,歡迎留言討論。

        查看原文

        贊 4 收藏 2 評論 0

        luckness 贊了文章 · 2020-12-18

        一文搞懂Babel配置

        最近在做一次Babel6升級Babel7的操作,把升級的過程和關于babel的配置進行一次總結。

        1 為什么講Babel配置

        Babel 是一個工具鏈,主要用于將 ECMAScript 2015+ 版本的代碼轉換為向后兼容的 JavaScript 語法,以便能夠運行在當前和舊版本的瀏覽器或其他環境中。

        其實目前前端開發,各種項目模版,你也不需要關心babel的配置,隨便拉下來一個就能運行,但是要做定制化的處理還是要把babel搞懂。

        @babel/cli是Babel的命令行工具,我們一般用不到,因為我們通常都是用babel-loader,里邊使用的是@babel/core的api形式,我們只需要關心Babel的配置,如果有需要在編譯階段對代碼進行處理 也可以寫自己的插件,但是大部分場景是需要我們把Babel的配置搞清楚。

        2 Babel的配置文件

        Babel6的階段 最常用的是.babelrc,但是現在Babel7支持了更多格式:

        const RELATIVE_CONFIG_FILENAMES = [".babelrc", ".babelrc.js", ".babelrc.cjs", ".babelrc.mjs", ".babelrc.json"];
        package.json files with a "babel" key。

        配置文件的格式如下:

        {
            "presets": [
              [
                "@babel/preset-env",
                {
                  "modules": "commonjs"
                }
              ]
            ],
            "plugins": [
              [
                "@babel/plugin-transform-runtime",
                {
                  "corejs": 3
                }
              ],
              "@babel/plugin-syntax-dynamic-import",
            ]
          }
        }

        更詳細介紹參見Babel Config。

        2.1 pluginspreset

        配置文件中主要有兩個配置pluginspreset,@babel/core本身對代碼不做任何的轉化,但是提供了對代碼的解析,各種插件在解析過程中可以進行代碼的轉換,比如處理箭頭函數的插件@babel/plugin-transform-arrow-functions等等,所以比如針對ES6語法的解析就需要很多插件,preset預設就是配置的子集,預設的一套配置,可以根據參數動態的返回配置。

        2.2 執行順序

        順序問題很重要,比如某一個插件是添加'use strict', 一個插件是刪除'use strict',如果想要刪除成功,就要保證執行順序。
        在一個配置里面

        • 插件在 presets 前運行。
        • 插件順序從前往后排列。
        • preset 順序是顛倒的(從后往前)。

        所以在preset中的插件,肯定比外層的插件要后執行。

        2.3 傳參數

        pluginspreset的配置是數組的形式,如果不需要傳參數,最基本的就是字符串名稱,如果需要傳參數,把它寫成數組的形式,數組第一項是字符串名稱,第二項是要傳的參數對象。

        3 Babel的升級

        3.1 廢棄的preset

        @babel/preset-env已經完全可以替換

        • babel-preset-es2015
        • babel-preset-es2016
        • babel-preset-es2017
        • babel-preset-latest

        所有stage的preset在Babel v7.0.0-beta.55版本都已經被廢棄了,
        stage-x:指處于某一階段的js語言提案

        • Stage 0?- 設想(Strawman):只是一個想法,可能有 Babel插件。
        • Stage 1?- 建議(Proposal):這是值得跟進的。
        • Stage 2?- 草案(Draft):初始規范。
        • Stage 3?- 候選(Candidate):完成規范并在瀏覽器上初步實現。
        • Stage 4 - 完成(Finished):將添加到下一個年度版本發布中。

        最開始stage的出現是為了方便開發人員,每個階段的插件與TC39和社區相互作用,同步更新,用戶可以直接引用對應stage支持的語法特性。關于廢棄的原因 總結下來是:

        • 1 對用戶太黑盒了,當提案發生重大變化和廢棄時,stage內部的插件就會變化,用戶可能會出現未編譯的語法。
        • 2 當用戶想要支持某種語法時,不知道在某一個stage里,所以最好是讓用戶自己去添加插件,或者你只需要指定瀏覽器的兼容性,preset中動態的添加對應插件。
        • 3 第三點舉了個例子,很多人都把裝飾器特性叫做ES7,其實這只是階段0的實驗性建議,可能永遠不會成為JS的一部分。不要將其稱為“ES7”,我們要時刻提醒開發者babel是怎么工作的。

        3.1 廢棄的polyfill

        先說下已經有了Babel為什么還要polyfill,Babel默認只轉換新的JavaScript句法(syntax),而不轉換新的API,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局對象,以及一些定義在全局對象上的方法(比如Object.assign)都不會轉碼。舉個栗子,ES6在Array對象上新增了Array.from方法。babel就不會轉碼這個方法。所以之前我們都需要引入polyfill。

        但是從Babel 7.4.0開始,不推薦使用此軟件包,而直接包括core-js/stable(包括regenerator-runtime/runtimepolyfill ECMAScript功能)和(需要使用轉譯的生成器函數)。

        import "core-js/stable";
        import "regenerator-runtime/runtime";

        但是最優的方式也不是直接這樣引入,后面講@babel/preset-env的使用時會有更好的方式。

        3.3 babel-upgrade

        關于升級,官方提供了工具 babel-upgrade 總結關鍵點如下:

        • 1 node版本8以上 這個應該都不是問題了。
        • 2 npx babel-upgrade --write --install,兩個參數,--write會把更新的配置寫入babel的配置文件中,package.json中也會更新依賴,但是發現沒有的依賴沒有新增,所以我在更新的時候把配置中依賴的npm包,在package.json都check了一遍。--install是會進行一次安裝操作。

        4 @babel/preset-env

        @babel/preset-env是Babel推薦的最智能的預設,在使用了 babel-upgrade 升級之后你就可以看到配置中會有這個預設,因為設個預設集成了常用插件和polyfill能力,可以根據用戶指定的環境尋找對應的插件。

        下面對它的關鍵配置項做說明。

        4.1 targets

        string | Array<string> | { [string]: string },默認為{}。

        描述您為項目支持/目標的環境。

        這可以是與瀏覽器列表兼容的查詢:

        `{
          "targets": "> 0.25%, not dead"
        }` 
        

        或支持最低環境版本的對象:

        `{
          "targets": {
            "chrome": "58",
            "ie": "11"
          }
        }` 
        

        實施例的環境中:chrome,opera,edge,firefox,safari,ie,ios,android,node,electron。

        如果未指定目標,則旁注@babel/preset-env將默認轉換所有ECMAScript 2015+代碼,所以不建議。

        4.2 useBuiltIns

        "usage"| "entry"| false,默認為false。

        此選項決定@babel/preset-env如何處理polyfill的引入。

        前面將廢棄polyfill時 講到了polyfill現在分為兩個npm包,是這樣引入

        import "core-js/stable";
        import "regenerator-runtime/runtime";

        但是問題是全量引入,增加包體積,所以useBuiltIns選項就是對其進行優化。

        當取值"entry"時,@babel/preset-env 會把全量引入替換為目標環境特定需要的模塊。

        當目標瀏覽器是 chrome 72 時,上面的內容將被 @babel/preset-env 轉換為

        require("core-js/modules/es.array.reduce");
        require("core-js/modules/es.array.reduce-right");
        require("core-js/modules/es.array.unscopables.flat");
        require("core-js/modules/es.array.unscopables.flat-map");
        require("core-js/modules/es.math.hypot");
        require("core-js/modules/es.object.from-entries");
        require("core-js/modules/web.immediate");

        當取值"usage"時,我們無需手動引入polyfill文件,@babel/preset-env 在每個文件的開頭引入目標環境不支持、僅在當前文件中使用的 polyfills。

        例如,

        const set = new Set([1, 2, 3]);
        [1, 2, 3].includes(2);

        當目標環境是老的瀏覽器例如 ie 11,將轉換為

        import "core-js/modules/es.array.includes";
        import "core-js/modules/es.array.iterator";
        import "core-js/modules/es.object.to-string";
        import "core-js/modules/es.set";
        
        const set = new Set([1, 2, 3]);
        [1, 2, 3].includes(2);

        當目標是 chrome 72 時不需要導入,因為這個環境不需要 polyfills:

        const set = new Set([1, 2, 3]);
        [1, 2, 3].includes(2);

        4.3 core-js

        core-js就是Javascript標準庫的polyfill,@babel/preset-env的polyfill就依賴于它,所以我們需要指定使用的core-js的版本,目前最新版本是3。
        默認情況下,僅注入穩定ECMAScript功能的polyfill,如果想使用一些提案的語法,可以有三種選擇:

        • 使用useBuiltIns: "entry"時,可以直接導入建議填充工具import "core-js/proposals/string-replace-all"。
        • 使用useBuiltIns: "usage"時,您有兩種不同的選擇:

          • shippedProposals選項設置為true。這將啟用已經在瀏覽器中發布一段時間的投標的polyfill和transforms。
          • 使用corejs: { version: 3, proposals: true }。這樣可以對所支持的每個提案進行填充core-js。

        4.4 exclude

        我覺得這個選擇有用,因為@babel/preset-env中內置的插件,我們無法在其后執行,比如里面內置的"@babel/plugin-transform-modules-commonjs"插件會默認的在所有的模塊上都添加use strict 嚴格模式, 雖然有babel-plugin-remove-use-strict用于移除use strict 但是由于執行順序的問題,還是無法移除。
        第二個問題就是內置插件無法傳參數的問題。
        所以我想到的方法是先exclude排除掉這個插件,然后在外層再添加 這樣就可以改變執行順序同時也可以自定義傳參數。

        5 @babel/plugin-transform-runtime

        已經有了polyfill,這個包的作用是什么?主要分兩類:

        • 1 減少代碼體積,Babel的編譯會在每一個模塊都添加一些行內的代碼墊片,例如await_asyncToGeneratorasyncGeneratorStep,使用了它之后會把這些方法通過@babel/runtime/helpers中的模塊進行替換。

        例如代碼

        async function a () {
          await new Promise(function(resolve, reject) {
            resolve(1)
          })
        } 

        沒使用之前,編譯結果

        require("regenerator-runtime/runtime");
        
        function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
        
        function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
        
        
        function _a() {
          _a = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee() {
            return regeneratorRuntime.wrap(function _callee$(_context) {
              while (1) {
                switch (_context.prev = _context.next) {
                  case 0:
                    _context.next = 2;
                    return new Promise(function (resolve, reject) {
                      resolve(1);
                    });
        
                  case 2:
                  case "end":
                    return _context.stop();
                }
              }
            }, _callee);
          }));
          return _a.apply(this, arguments);
        }

        使用之后

        
        var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs3/regenerator"));
        
        var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
        
        require("regenerator-runtime/runtime");
        
        var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/asyncToGenerator"));
        
        • 2 局部引入 不影響全局變量

        @babel/preset-env中引入的polyfill都是直接引入的core-js下的模塊,它的問題會污染全局變量,比如

        "foobar".includes("foo");

        編譯后的polyfill是給String.prototype添加了includes方法,所以會影響全局的String對象。

        require("core-js/modules/es.string.includes");

        而使用了@babel/plugin-transform-runtime后的編譯結果

        var _includes = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/includes"));
        
        (0, _includes.default)(_context = "foobar").call(_context, "foo");

        會把代碼中用的到的方法進行包裝,而不會對全局變量產生影響。

        最后是 @babel/plugin-transform-runtime的配置項,關鍵的是指定 core-js的版本。

        corejs: 2僅支持全局變量(例如Promise)和靜態屬性(例如Array.from),corejs: 3還支持實例屬性(例如[].includes)。

        默認情況下,@babel/plugin-transform-runtime不填充提案。如果您使用corejs: 3,則可以通過使用proposals: true選項啟用此功能。

        需要安裝對應的運行時依賴:
        npm install --save @babel/runtime-corejs3

        最后 你可以基于以上知識已經創建了符合自己團隊開發的preset。

        如果覺得有收獲請關注微信公眾號 前端良文 每周都會分享前端開發中的干貨知識點。

        查看原文

        贊 16 收藏 11 評論 1

        luckness 發布了文章 · 2020-11-07

        ECMAScript7規范中的instanceof操作符

        本文主要講解ECMAScript7規范中的instanceof操作符。

        預備知識

        有名的Symbols

        “有名”的Symbols指的是內置的符號,它們定義在Symbol對象上。ECMAScript7中使用了@@name的形式引用這些內置的符號,比如下面會提到的@@hasInstance,其實就是Symbol.hasInstance。

        InstanceofOperator(O, C)

        O instanceof C在內部會調用InstanceofOperator(O, C)抽象操作,該抽象操作的步驟如下:

        1. 如果C的數據類型不是對象,拋出一個類型錯誤的異常;
        2. instOfHandler等于GetMethod(C, @@hasInstance),大概語義就是獲取對象C@@hasInstance屬性的值;
        3. 如果instOfHandler的值不是undefined,那么:

          1. 返回ToBoolean(? Call(instOfHandler, C, ? O ?))的結果,大概語義就是執行instOfHandler(O),然后把調用結果強制轉化為布爾類型返回。
        4. 如果C不能被調用,拋出一個類型錯誤的異常;
        5. 返回OrdinaryHasInstance(C, O)的結果。

        OrdinaryHasInstance(C, O)

        OrdinaryHasInstance(C, O)抽象操作的步驟如下:

        1. 如果C不能被調用,返回false;
        2. 如果C有內部插槽[[BoundTargetFunction]],那么:

          1. BC等于C的內部插槽[[BoundTargetFunction]]的值;
          2. 返回InstanceofOperator(O, BC)的結果;
        3. 如果O的類型不是對象,返回false;
        4. P等于Get(C, "prototype"),大概語義是獲取C.prototype的值;
        5. 如果P的數據類型不是對象,拋出一個類型錯誤的異常;
        6. 重復執行下述步驟:

          1. O等于O.[[GetPrototypeOf]]()的結果,大概語義就是獲取O的原型對象;
          2. 如果O等于null,返回false;
          3. 如果SameValue(P, O)的結果是true,返回true。

        SameValue抽象操作參見JavaScript中的==,===和Object.js()中的Object.is(),Object.is()使用的就是這個抽象操作的結果。

        由上述步驟2可知,如果C是一個bind函數,那么會重新在C綁定的目標函數上執行InstanceofOperator(O, BC)操作。

        由上述步驟6可知,會重復地獲取對象O的原型對象,然后比較該原型對象和Cprototype屬性是否相等,直到相等返回true,或者O變為null,也就是遍歷完整個原型鏈,返回false。

        Function.prototype[@@hasInstance] (V)

        由上面的InstanceofOperator(O, C)抽象操作的步驟23可以知道,如果C上面定義或繼承了@@ hasInstance屬性的話,會調用該屬性的值,而不會走到步驟45。步驟45的目的是為了兼容沒有實現@@hasInstance方法的瀏覽器。如果一個函數沒有定義或繼承@@hasInstance屬性,那么就會使用默認的instanceof的語義,也就是OrdinaryHasInstance(C, O)抽象操作描述的步驟。

        ECMAScript7規范中,在Functionprototype屬性上定義了@@hasInstance屬性。Function.prototype[@@hasInstance](V)的步驟如下:

        1. F等于this值;
        2. 返回OrdinaryHasInstance(F, V)的結果。

        所以,你可以看到在默認情況下,instanceof的語義是一樣的,都是返回OrdinaryHasInstance(F, V)的結果。為什么說默認情況下?因為你可以覆蓋Function.prototype[@@hasInstance]方法,去自定義instanceof的行為。

        例子

        function A () {}
        function B () {}
        
        var a = new A
        a.__proto__ === A.prototype // true
        a.__proto__.__proto__ === Object.prototype // true
        a.__proto__.__proto__.__proto__ === null // true
        
        a instanceof A // true
        a instanceof B // false

        OrdinaryHasInstance(C, O)的第6步可知:

        • 對于a instanceof A,PA.prototype,在第一次循環的時候,a的原型對象a._proto__A.prototype,也就是步驟中的OA.prototype,所以返回了true;
        • 對于a instanceof B,PB.prototype,在第一次循環的時候,a的原型對象a._proto__A.prototype,不等于P;執行第二次循環,此時Oa.__proto__.__proto__,也就是Object.prototype,不等于P;執行第三次循環,此時Oa.__proto__.__proto__.__proto__,也就是null,也就是原型鏈都遍歷完了,所以返回了false。

        接著上面的例子:

        A.prototype.__proto__ = B.prototype
        
        a.__proto__ === A.prototype // true
        a.__proto__.__proto__ === B.prototype // true
        a.__proto__.__proto__.__proto__ === Object.prototype // true
        a.__proto__.__proto__.__proto__.__proto__ === null // true
        
        a instanceof B // true

        在上面的例子中,我們把B.prototype設置成了a的原型鏈中的一環,這樣a instanceof BOrdinaryHasInstance(C, O)的第6步的第2次循環的時候,返回了true。

        OrdinaryHasInstance(C, O)的第2步,我們知道bind函數的行為和普通函數的行為是不一樣的:

        function A () {}
        var B = A.bind()
        
        B.prototype === undefined // true
        
        var b = new B
        b instanceof B // true
        b instanceof A // true

        由上面的例子可知,B.prototypeundefined。所以,instanceof作用于bind函數的返回結果其實是作用于綁定的目標函數的返回值,和bind函數基本上沒有什么關系。

        InstanceofOperator(O, C)步驟2和步驟3可知,我們可以通過@@hasInstance屬性來自定義instanceof的行為:

        function A () {}
        var a = new A
        a instanceof A // true
        
        A[Symbol.hasInstance] = function () { return false }
        a instanceof A // ?

        chrome瀏覽器測試了一下,發現還是輸出true。然后看了一下ECMAScript6的文檔,ECMAScript6文檔里面還沒有規定可以通過@@hasInstance改變instanceof的行為,所以應該是目前chrome瀏覽器還沒有實現ECMAScript7中的instanceof操作符的行為。

        直到有一天看了MDNSymbol.hasInstance的兼容性部分,發現chrome51版本就開始支持Symbol.hasInstance了:

        class MyArray {  
          static [Symbol.hasInstance](instance) {
            return Array.isArray(instance)
          }
        }
        console.log([] instanceof MyArray) // true

        那么為什么我那樣寫不行呢?直到我發現:

        function A () {}
        var fun = function () {return false}
        A[Symbol.hasInstance] = fun
        A[Symbol.hasInstance] === fun // false
        A[Symbol.hasInstance] === Function.prototype[Symbol.hasInstance] // true
        A[Symbol.hasInstance] === A.__proto__[Symbol.hasInstance] // true

        由上面的代碼可知,A[Symbol.hasInstance]并沒有賦值成功,而且始終等于Function.prototype[Symbol.hasInstance],也就是始終等于A的原型上的Symbol.hasInstance方法。那是不是因為原型上的同名方法?

        Object.getOwnPropertyDescriptor(Function.prototype, Symbol.hasInstance)
        // Object {writable: false, enumerable: false, configurable: false, value: function}

        由上面的代碼可知,Function.prototype上的Symbol.hasInstance的屬性描述符的writablefalse,也就是這個屬性是只讀的,所以在A上面添加Symbol.hasInstance屬性失敗了。但是為啥沒有失敗的提示呢?

        'use strict'
        function A () {}
        var fun = function () {return false}
        A[Symbol.hasInstance] = fun
        // Uncaught TypeError: Cannot assign to read only property 'Symbol(Symbol.hasInstance)' of function 'function A() {}'

        錯誤提示出來了,所以以后還是盡量使用嚴格模式。非嚴格模式下有些操作會靜默失敗,也就是即使操作失敗了也不會有任何提示,導致開發人員認為操作成功了。

        var a = {}
        a[Symbol.hasInstance] = function () {return true}
        new Number(3) instanceof a // true

        因為可以通過自定義Symbol.hasInstance方法來覆蓋默認行為,所以用instanceof操作符判斷數據類型并不一定是可靠的。

        還有一個問題:為什么上面MDN文檔的例子可以成功,我最初的例子就不行呢,目的不都是寫一個構造函數,然后在構造函數上添加一個屬性嗎?
        個人分析的結果是:雖然大家都說Class是寫構造函數的一個語法糖,但是其實還是和使用function的方式有差別的,就比如上面的例子。使用Class的時候,會直接在構造函數上添加一個靜態屬性,不會先檢查原型鏈上是否存在同名屬性。而使用function的方式的時候,給構造函數添加一個靜態方法,相當于給對象賦值,賦值操作會先檢查原型鏈上是否存在同名屬性,所以就會有賦值失敗的風險。所以,就給構造函數添加Symbol.hasInstance屬性來說,Class能做到,使用Function的方式就做不到。

        更新于2018/11/20
        上面總結到

        所以,就給構造函數添加Symbol.hasInstance屬性來說,Class能做到,使用Function的方式就做不到。

        但是,后來發現給對象添加屬性的方法不只是賦值這一種方式,還有一個Object.defineProperty方法:

        function A () {}
        var a = new A
        a instanceof A // true
        
        Object.defineProperty(A, Symbol.hasInstance, {
            value: function () { return false }
        })
        a instanceof A // false

        總結

        本文主要講解ECMAScript7規范中的instanceof操作符,希望大家能有所收獲。如果本文有什么錯誤或者不嚴謹的地方,歡迎在評論區留言。

        查看原文

        贊 1 收藏 1 評論 0

        luckness 發布了文章 · 2020-10-31

        JavaScript實現網頁截屏方法總結

        最近研究了下如何利用JavaScript實現網頁截屏,包括在瀏覽器運行的JS,以及在后臺運行的nodeJs的方法。主要看了以下幾個:

        1. PhantomJS
        2. Puppeteer(chrome headless)
        3. SlimerJS
        4. dom-to-image
        5. html2canvas

        測試的網頁使用了WebGL技術,所以下面的總結會和WebGL相關。

        名詞定義

        headless browser

        無界面瀏覽器,多用于網頁自動化測試、網頁截屏、網頁的網絡監控等。

        PhantomJS

        PhantomJS是可以通過JS進行編程的headless瀏覽器,使用的是QtWebKit內核。

        實現截屏的代碼,假設文件名為github.js:

        // 創建一個網頁實例
        var page = require('webpage').create();
        // 加載頁面
        page.open('http://github.com/', function () {
          // 給網頁截屏,保存到github.png文件中
          page.render('github.png');
          phantom.exit();
        })

        運行:

        phantomjs github.js

        普通的頁面沒有問題,但是如果運行包含WebGL的頁面,發現截屏不對。經過一些調查,發現不支持WebGL,github issue。

        總結:

        1. PhantomJs已經停止維護了,所以不太建議繼續使用。停止維護的一個原因是chrome發布的headless版本對它造成了一定沖擊。
        2. 不支持WebGL。但是,還是有開發者說可以自己給PhantomJS添加WebGL支持,不過,這個方案目前超出我的知識范圍了,就沒有繼續研究。

        Puppeteer(chrome headless)

        Puppeteer是一個Node庫,提供了控制chrome和chromium的API。默認運行headless模式,也支持界面運行。

        實現截屏的代碼example.js:

        const puppeteer = require('puppeteer');
        
        (async () => {
          const browser = await puppeteer.launch();
          const page = await browser.newPage();
          await page.setViewport({ // 設置視窗大小
            width: 600,
            height: 800
          });
          await page.goto('https://example.com'); // 打開頁面
          await page.screenshot({path: 'example.png'}); // path: 截屏文件保存路徑
        
          await browser.close();
        })();

        運行:

        node example.js

        接下來看下screenshot方法的實現原理:

        screenshot的源碼位于lib/cjs/puppeteer/common/Page.js文件中,是一個異步方法:

        async screenshot(options = {}) {
          // ...
          return this._screenshotTaskQueue.postTask(() => this._screenshotTask(screenshotType, options));
        }
        async _screenshotTask(format, options) {
          // ...
          const result = await this._client.send('Page.captureScreenshot', {
            format,
            quality: options.quality,
            clip,
          });
          // ...
        }

        這個this._client.send又是個什么東西?別急,我們重新看下Puppeteer的定義:

        Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol.

        看到最后面那個DevTools Protocol了嗎?這是個什么東西:

        The Chrome DevTools Protocol allows for tools to instrument, inspect, debug and profile Chromium, Chrome and other Blink-based browsers.

        詳細的解釋可以看這篇博客。

        簡單來說,Puppeteer就是通過WebSocket給瀏覽器發送遵循Chrome Devtools Protocol的數據,命令瀏覽器去執行一些操作。然后,瀏覽器再通過WebSocket把結果返回給Puppeteer。這個過程是異步的,所以看源代碼會發現好多async/await。

        所以screenshot方法是調用了Chrome Devtools Protocol的captureScreenshot。

        總結:

        1. 支持WebGL。
        2. 網頁比較復雜的話,截屏時間也挺長的,我測試的頁面是幾百毫秒。
        3. Puppeteer是對(CDP)Chrome Devtools Protocol功能的封裝。大部分功能都是通過WebSocket傳輸給CDP處理的。

        SlimerJS

        SlimerJS和PhantomJS類似。不同點是SlimerJS是基于火狐的瀏覽器引擎Gecko,而不是Webkit。

        SlimerJS可以通過npm安裝,最新版本是1.x。不過兼容的火狐版本是53.0到59.0。我看現在火狐最新版本都82了。因為我本機是安裝了火狐最新版本的,所以我還得安裝一個老版本的火狐,比如59.0??梢詤⒖歼@篇安裝舊版本的火狐瀏覽器。我是mac系統,感覺安裝還是挺容易的。

        實現截屏的代碼screenshot.js:

        var page = require('webpage').create();
        page.open("http://slimerjs.org", function (status) {
          page.viewportSize = { width:1024, height:768 };
          page.render('screenshot.png');
        });

        運行

        // mac操作系統設置火狐路徑
        export SLIMERJSLAUNCHER=/Applications/Firefox.app/Contents/MacOS/firefox
        ./node_modules/.bin/slimerjs screenshot.js // 我是局部安裝的slimer包

        需要注意的是SLIMERJSLAUNCHER=/Applications/Firefox.app/Contents/MacOS/firefox啟動的是火狐默認的安裝路徑,因為我一開始就有火狐瀏覽器,所以啟動的是最新版本的瀏覽器,然后就報錯了,說不兼容。在前面我安裝過一個59版本的火狐,那么這個火狐瀏覽器的路徑是什么?

        在應用程序里面我把這個舊版本的火狐命名為Firefox59,然后這個路徑就是/Applications/Firefox59.app/Contents/MacOS/firefox。重新設置SLIMERJSLAUNCHER為59版本的火狐瀏覽器之后,發現就能成功了。

        不過,Puppeteer默認會打開瀏覽器界面,也就是non-headless模式。如果要使用headless模式,可以

        ./node_modules/.bin/slimerjs --headless screenshot.js

        不過,headless模式下,不支持WebGL。

        我在寫例子的時候,發現的一個明顯的不同就是Puppeteer截屏是異步函數,而SlimerJS截屏是同步函數?好奇心驅使下,看了下源碼(src/modules/slimer-sdk/webpage.js):

        render: function(filename, options) {
          // ...
          let canvas = webpageUtils.getScreenshotCanvas(
            browser.contentWindow,
            finalOptions.ratio,
            finalOptions.onlyViewport, this);
          }
          canvas.toBlob(function(blob) {
            let reader = new browser.contentWindow.FileReader();
            reader.onloadend = function() {
              content = reader.result;
            }
            reader.readAsBinaryString(blob);
          }, finalOptions.contentType, finalOptions.quality);
          // ...
        }

        webpageUtils.getScreenshotCanvas(src/modules/webpageUtils.jsm):

        getScreenshotCanvas : function(window, ratio, onlyViewport, webpage) {
          // ...
          // create the canvas
          let canvas = window.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
          canvas.width = canvasWidth;
          canvas.height = canvasHeight;
        
          let ctx = canvas.getContext("2d");
          ctx.scale(ratio, ratio);
          ctx.drawWindow(window, clip.left, clip.top, clip.width, clip.height, "rgba(0,0,0,0)");
          ctx.restore();
        
          return canvas;
        }

        關鍵代碼就是那行ctx.drawWindow。what?JS原生API還支持直接截屏?
        CanvasRenderingContext2D.drawWindow():只有火狐支持,已經被廢棄掉的非規范定義的標準API。

        總結

        1. 1.0版本支持的火狐版本是53.0到59.0。不保證最新版本火狐可用。
        2. headless模式下,不支持WebGL。

        dom-to-image

        dom-to-image:前端截屏的開源庫。工作原理是:
        SVG的foreignObject標簽可以包裹任意的html內容。那么,為了渲染一個節點,主要進行了以下步驟:

        1. 遞歸地拷貝原始dom節點和后代節點;
        2. 把原始節點以及后代節點的樣式遞歸的應用到對應的拷貝后的節點和后代節點上;
        3. 字體處理;
        4. 圖片處理;
        5. 序列化拷貝后的節點,把它插入到foreignObject里面,然后組成一個svg,然后生成一個data URL;
        6. 如果想得到PNG內容或原始像素值,可以先使用data URL創建一個圖片,使用一個離屏canvas渲染這張圖片,然后從canvas中獲取想要的數據。

        測試的時候,發現外部資源不能加載,所以簡單的了解了后就放棄了。

        html2canvas

        html2canvas。網上查了下感覺有一篇文章寫的挺好的:淺析 js 實現網頁截圖的兩種方式。感興趣的可以看下。

        未驗證的猜想

        雖然后面這兩種是前端的實現方式,但是結合前面講的headless庫,也是可以實現后端截屏的。以Puppeteer的API為例,可以首先使用page.addScriptTag(options)往網頁中添加前端截屏的庫,然后在page.evaluate(pageFunction[, ...args])中的pageFunction函數里面寫相應的截屏代碼就可以了,因為pageFunction的執行上下文是網頁上下文,所以可以獲取到document等對象。

        總結

        對截屏的一個簡單研究,寫篇博客整理下思路。如果文中有敘述錯誤的地方,歡迎評論留言。

        查看原文

        贊 7 收藏 4 評論 9

        認證與成就

        • 獲得 430 次點贊
        • 獲得 8 枚徽章 獲得 1 枚金徽章, 獲得 1 枚銀徽章, 獲得 6 枚銅徽章

        擅長技能
        編輯

        (??? )
        暫時沒有

        開源項目 & 著作
        編輯

        (??? )
        暫時沒有

        注冊于 2016-05-19
        個人主頁被 5.9k 人瀏覽

        一本到在线是免费观看_亚洲2020天天堂在线观看_国产欧美亚洲精品第一页_最好看的2018中文字幕