basket.js 源碼分析

来源:http://www.cnblogs.com/oadaM92/archive/2016/04/03/5348793.html
-Advertisement-
Play Games

basket.js 源碼分析 一、前言 basket.js 可以用來載入js腳本並且保存到 LocalStorage 上,使我們可以更加精準地控制緩存,即使是在 http 緩存過期之後也可以使用。因此可以使我們防止不必要的重新請求 js 腳本,提升網站載入速度。 可以到 basket.js 的 "G ...


basket.js 源碼分析

一、前言

basket.js 可以用來載入js腳本並且保存到 LocalStorage 上,使我們可以更加精準地控制緩存,即使是在 http 緩存過期之後也可以使用。因此可以使我們防止不必要的重新請求 js 腳本,提升網站載入速度。

可以到 basket.js 的 Github 上查看更多的相關信息。

由於之前在工作中使用過 basket.js ,好奇它的實現原理,因此就有了這篇分析 basket.js 源碼的文章。

二、簡單的使用說明

basket.js 的使用非常簡單,只要引入相應的js腳本,然後使用 basket 的 require 方法載入就可以了,例子:

<!DOCTYPE html>
<html>
<head>
    <title>basket.js demo</title>
    <script src="basket.full.min.js"></script>
</head>
<body>
    <script>
        basket.require({url: 'helloworld.js'});
    </script>
</body>
</html>

第一次載入,由於helloworld.js 只有一行代碼alert('hello world');, 所以運行該demo時就會彈出 "hello world"。並且對應的 js 會被保存到 LocalStorage:

此時對應的資源載入情況:

刷新一次頁面,再查看一次資源的載入情況:

可以看到已經沒有再發送 helloworld.js 相關的請求,因為 LocalStorage 上已經有對應的緩存了,直接從本地獲取即可。

三、實現流程

流程圖

細節說明

處理參數

參數處理就是根據已有的參數初始化未指定的參數。例如 require 方法支持 once 參數用來表示是否只執行一次對應 JS,execute 參數標示是否載入完該 JS 之後立刻執行。所以參數處理這一步驟就會根據是否執行過該 JS 和 once 參數是否為 ture 來設置execute參數。

獲取緩存

調用 localStorage.getItem方法獲取緩存。存入的 key 值為 basket- 加上 JS 文件的 URL。以上面載入 helloworld.js 為例,key 值為:basket-helloworld.js獲取的緩存為一個緩存對象,裡面包含 JS 代碼和相關的一些信息,例如:

{
"url": "helloworld.js?basket-unique=123",
"unique": "123",
"execute": true,
"key": "helloworld.js",
"data": "alert('hello world');",
"originalType": "application/javascript",
"type": "application/javascript",
"skipCache": false,
"stamp": 1459606005108,
"expire": 1477606005108
}

其中 data 屬性對應的值就是 JS 代碼。

判斷緩存是否有效

判斷比較簡單,根據緩存對象裡面的版本號 unique 和過期時間 expire 等來判斷。這和瀏覽器使用 Expire 和 Etag 頭部來判斷 HTTP 緩存是否有效相似。最大的不同就是緩存完全由 JS 控制!這也就是 basket.js 最大的作用。讓我們更好的控制緩存。預設的過期時間為5000小時,也就是208.33天。

判斷代碼:

/**
 * 判斷ls上的緩存對象是否過期
 * @param   {object}   source 從ls里取出的緩存對象
 * @param   {object}   obj    傳入的參數對象
 * @returns {Boolean}         過期返回true,否則返回false
 */
var isCacheValid = function(source, obj) {
    return !source || // 沒有緩存數據返回true
        source.expire - +new Date() < 0  || // 超過過期時間返回true
        obj.unique !== source.unique || // 版本號不同的返回true
        (basket.isValidItem && !basket.isValidItem(source, obj)); // 自定義驗證函數不成功的返回true
};

Ajax獲取JS

普通的利用 XMLHttpRequest 請求。

緩存到LocalStorage

調用localStorage.setItem方法保存緩存對象。一般來說,只要這一行代碼就能完成本步驟。但是LocalStorage保存的數據是有大小限制的!我利用 chrome 做了一個小測試,保存500KB左右的東西就會令到 Resources 面板變卡,2M 幾乎可以令到 Resources 基本卡死,到了 5M 就會超出限制,瀏覽器拋出異常:

DOMException: Failed to execute 'setItem' on 'Storage': Setting the value of 'basket-http://file.com/ykq/wap/v3Templates/timeout/timeout/large.js' exceeded the quota

因此需要使用 try catch 對localStorage.setItem方法進行異常捕獲。當沒容量不足時就需要根據保存時間逐一刪除 LocalStorage 的緩存對象。

相關代碼:

/**
 * 把緩存對象保存到localStorage中
 * @param   {string}    key         ls的key值
 * @param   {object}    storeObj    ls的value值,緩存對象,記錄著對應script的對象、有url、execute、key、data等屬性
 * @returns {boolean}               成功返回true
 */
var addLocalStorage = function( key, storeObj ) {
    // localStorage對大小是有限制的,所以要進行try catch
    // 500KB左右的東西保存起來就會令到Resources變卡
    // 2M左右就可以令到Resources卡死,操作不了
    // 5M就到了Chrome的極限
    // 超過之後會拋出如下異常:
    // DOMException: Failed to execute 'setItem' on 'Storage': Setting the value of 'basket-http://file.com/ykq/wap/v3Templates/timeout/timeout/large.js' exceeded the quota
    try {
        localStorage.setItem( storagePrefix + key, JSON.stringify( storeObj ) );
        return true;
    } catch( e ) {
        // localstorage容量不夠,根據保存的時間刪除已緩存到ls里的js代碼
        if ( e.name.toUpperCase().indexOf('QUOTA') >= 0 ) {
            var item;
            var tempScripts = [];

            // 先把所有的緩存對象來出來,放到 tempScripts里
            for ( item in localStorage ) {
                if ( item.indexOf( storagePrefix ) === 0 ) {
                    tempScripts.push( JSON.parse( localStorage[ item ] ) );
                }
            }

            // 如果有緩存對象
            if ( tempScripts.length ) {
                // 按緩存時間升序排列數組
                tempScripts.sort(function( a, b ) {
                    return a.stamp - b.stamp;
                });

                // 刪除緩存時間最早的js
                basket.remove( tempScripts[ 0 ].key );

                // 刪除後在再添加,利用遞歸完成
                return addLocalStorage( key, storeObj );

            } else {
                // no files to remove. Larger than available quota
                // 已經沒有可以刪除的緩存對象了,證明這個將要緩存的目標太大了。返回undefined。
                return;
            }

        } else {
            // some other error
            // 其他的錯誤,例如JSON的解析錯誤
            return;
        }
    }

};

生成script標簽註入到頁面

生成 script 標簽,append 到 document.head:

var injectScript = function( obj ) {
    var script = document.createElement('script');

    script.defer = true;
    // Have to use .text, since we support IE8,
    // which won't allow appending to a script
    script.text = obj.data;
    head.appendChild( script );
};

四、非同步編程

basket.js 是一個典型的需要大量非同步編程的庫,所以稍有不慎,代碼將會高度耦合,臃腫難看。。。

所以 basket.js 引入遵從 Promises/A+ 標準的非同步編程庫 RSVP.js 來這個問題。

(遵從 Promises/A+ 標準的還有 ES6 原生的 Promise 對象,jQuery 的$.Deferred 方法等)

所以 basket.js 中涉及非同步編程的方法都會返回一個 Promise 對象。很好地解決了非同步編程問題。例如 basket.require 方法就是返回一個promise 對象,因此需要按順序載入 JS 的時候可以這樣子寫:

basket.require({
    url: 'helloworld.js'
}).then(function() {
    basket.require({
        url: 'helloworld2.js'
    })
});

為了使代碼更好看,basket.js 添加了一個方法 basket.thenRequire,現在代碼就可以寫成這樣:

basket.require({
    url: 'helloworld.js'
}).thenRequire({
    url: 'helloworld2.js'
});

五、吐槽

其實 basket.js 算是一種黑科技,使用起來有比較多的東西要註意。例如我們無法正常使用 chrome 的 Sources 面板斷點調試,解決方法為手動在代碼裡面添加debugger設置斷點。還有就是由於強制刷新頁面也不能清除 localStorage 上的緩存,所以每次修改代碼時我們都需要手動清除 localStorage,比較麻煩。當然調試時可以在 JS 文件的頭部添加localStorage.clear()解決這個問題。

還有就是 basket.js 已經好久沒有更新了,畢竟黑科技,總會被時代淘汰。而且 api 文檔也不齊全,例如上面的 thenRequire 方法是我查看源代碼時才發現的,官方文檔裡面根本沒有。

最後,雖然 basket.js 應該不會在維護了,但是閱讀其源碼還是能有很多收穫,推薦大家花點時間閱讀一下。

六、源碼完整註釋

/*!
* basket.js
* v0.5.2 - 2015-02-07
* http://addyosmani.github.com/basket.js
* (c) Addy Osmani;  License
* Created by: Addy Osmani, Sindre Sorhus, Andrée Hansson, Mat Scales
* Contributors: Ironsjp, Mathias Bynens, Rick Waldron, Felipe Morais
* Uses rsvp.js, https://github.com/tildeio/rsvp.js
*/(function( window, document ) {
    'use strict';

    var head = document.head || document.getElementsByTagName('head')[0];
    var storagePrefix = 'basket-'; // 保存localStorage時的首碼
    var defaultExpiration = 5000; // 預設過期時間為5000小時
    var inBasket = []; // 保存已經執行過的js的url。輔助設置參數的execute選項。

    /**
     * 把緩存對象保存到localStorage中
     * @param   {string}    key         ls的key值
     * @param   {object}    storeObj    ls的value值,緩存對象,記錄著對應script的對象、有url、execute、key、data等屬性
     * @returns {boolean}               成功返回true
     */
    var addLocalStorage = function( key, storeObj ) {
        // localStorage對大小是有限制的,所以要進行try catch
        // 500KB左右的東西保存起來就會令到Resources變卡
        // 2M左右就可以令到Resources卡死,操作不了
        // 5M就到了Chrome的極限
        // 超過之後會拋出如下異常:
        // DOMException: Failed to execute 'setItem' on 'Storage': Setting the value of 'basket-http://file.com/ykq/wap/v3Templates/timeout/timeout/large.js' exceeded the quota
        try {
            localStorage.setItem( storagePrefix + key, JSON.stringify( storeObj ) );
            return true;
        } catch( e ) {
            // localstorage容量不夠,根據保存的時間刪除已緩存到ls里的js代碼
            if ( e.name.toUpperCase().indexOf('QUOTA') >= 0 ) {
                var item;
                var tempScripts = [];

                // 先把所有的緩存對象來出來,放到 tempScripts里
                for ( item in localStorage ) {
                    if ( item.indexOf( storagePrefix ) === 0 ) {
                        tempScripts.push( JSON.parse( localStorage[ item ] ) );
                    }
                }

                // 如果有緩存對象
                if ( tempScripts.length ) {
                    // 按緩存時間升序排列數組
                    tempScripts.sort(function( a, b ) {
                        return a.stamp - b.stamp;
                    });

                    // 刪除緩存時間最早的js
                    basket.remove( tempScripts[ 0 ].key );

                    // 刪除後在再添加,利用遞歸完成
                    return addLocalStorage( key, storeObj );

                } else {
                    // no files to remove. Larger than available quota
                    // 已經沒有可以刪除的緩存對象了,證明這個將要緩存的目標太大了。返回undefined。
                    return;
                }

            } else {
                // some other error
                // 其他的錯誤,例如JSON的解析錯誤
                return;
            }
        }

    };

    /**
     * 利用ajax獲取相應url的內容
     * @param   {string}    url 請求地址
     * @returns {object}        返回promise對象,解決時的參數為對象:{content:'', type: ''}
     */
    var getUrl = function( url ) {
        var promise = new RSVP.Promise( function( resolve, reject ){

            var xhr = new XMLHttpRequest();
            xhr.open( 'GET', url );

            xhr.onreadystatechange = function() {
                if ( xhr.readyState === 4 ) {
                    if ( ( xhr.status === 200 ) ||
                            ( ( xhr.status === 0 ) && xhr.responseText ) ) {
                        resolve( {
                            content: xhr.responseText,
                            type: xhr.getResponseHeader('content-type')
                        } );
                    } else {
                        reject( new Error( xhr.statusText ) );
                    }
                }
            };

            // By default XHRs never timeout, and even Chrome doesn't implement the
            // spec for xhr.timeout. So we do it ourselves.
            // 自定義超時設置
            setTimeout( function () {
                if( xhr.readyState < 4 ) {
                    xhr.abort();
                }
            }, basket.timeout );

            xhr.send();
        });

        return promise;
    };

    /**
     * 獲取js,保存緩存對象到ls
     * @param   {object}   obj basket.require的參數對象(之前的處理過程中添加相應的屬性)
     * @returns {object}       promise對象
     */
    var saveUrl = function( obj ) {
        return getUrl( obj.url ).then( function( result ) {
            var storeObj = wrapStoreData( obj, result );

            if (!obj.skipCache) {
                addLocalStorage( obj.key , storeObj );
            }

            return storeObj;
        });
    };

    /**
     * 進一步添加對象obj屬性
     * @param   {object}   obj  basket.require的參數(之前的處理過程中添加相應的屬性)
     * @param   {object}   data 包含content和type屬性的對象,content就是js的內容
     * @returns {object}        經過包裝後的obj
     */
    var wrapStoreData = function( obj, data ) {
        var now = +new Date();
        obj.data = data.content;
        obj.originalType = data.type;
        obj.type = obj.type || data.type;
        obj.skipCache = obj.skipCache || false;
        obj.stamp = now;
        obj.expire = now + ( ( obj.expire || defaultExpiration ) * 60 * 60 * 1000 );

        return obj;
    };

    /**
     * 判斷ls上的緩存對象是否過期
     * @param   {object}   source 從ls里取出的緩存對象
     * @param   {object}   obj    傳入的參數對象
     * @returns {Boolean}         過期返回true,否則返回false
     */
    var isCacheValid = function(source, obj) {
        return !source || // 沒有緩存數據返回true
            source.expire - +new Date() < 0  || // 超過過期時間返回true
            obj.unique !== source.unique || // 版本號不同的返回true
            (basket.isValidItem && !basket.isValidItem(source, obj)); // 自定義驗證函數不成功的返回true
    };

    /**
     * 判斷緩存是否還生效,獲取js,保存到ls
     * @param   {object}   obj basket.require參數對象
     * @returns {object}       返回promise對象
     */
    var handleStackObject = function( obj ) {
        var source, promise, shouldFetch;

        if ( !obj.url ) {
            return;
        }

        obj.key =  ( obj.key || obj.url );

        source = basket.get( obj.key );

        obj.execute = obj.execute !== false;

        shouldFetch = isCacheValid(source, obj); // 判斷緩存是否還有效

        // 如果shouldFetch為true,請求數據,保存到ls(live選項意義不明,文檔也沒有說,這裡當它一隻是undefined)
        if( obj.live || shouldFetch ) {
            if ( obj.unique ) {
                // set parameter to prevent browser cache
                obj.url += ( ( obj.url.indexOf('?') > 0 ) ? '&' : '?' ) + 'basket-unique=' + obj.unique;
            }
            promise = saveUrl( obj ); // 請求對應js,緩存到ls里

            if( obj.live && !shouldFetch ) {
                promise = promise
                    .then( function( result ) {
                        // If we succeed, just return the value
                        // RSVP doesn't have a .fail convenience method
                        return result;
                    }, function() {
                        return source;
                    });
            }
        } else {
        // 緩存可用。
            source.type = obj.type || source.originalType;
            source.execute = obj.execute;
            promise = new RSVP.Promise( function( resolve ){
                // 下麵的setTimeout用來解決結合requirejs使用時的載入問題。
                // setTimeout(function(){
                    debugger;
                    resolve( source );
                // },0);
            });
        }

        return promise;
    };

    /**
     * 把script插入到head中
     * @param {object} obj 緩存對象
     */
    var injectScript = function( obj ) {
        var script = document.createElement('script');

        script.defer = true;
        // Have to use .text, since we support IE8,
        // which won't allow appending to a script
        script.text = obj.data;
        head.appendChild( script );
    };

    // 保存著特定類型的執行函數,預設行為是把script註入到頁面
    var handlers = {
        'default': injectScript
    };

    /**
     * 執行緩存對象對應回調函數,把script插入到head中
     * @param   {object}   obj 緩存對象
     * @returns {undefined}    不需要返回結果
     */
    var execute = function( obj ) {
        // 執行類型特定的回調函數
        if( obj.type && handlers[ obj.type ] ) {
            return handlers[ obj.type ]( obj );
        }

        // 否則執行預設的註入script行為
        return handlers['default']( obj ); // 'default' is a reserved word
    };

    /**
     * 批量執行緩存對象動作
     * @param   {Array} resources  緩存對象數組
     * @returns {Array}            返回參數resources
     */
    var performActions = function( resources ) {
        return resources.map( function( obj ) {
            if( obj.execute ) {
                execute( obj );
            }

            return obj;
        } );
    };

    /**
     * 處理請求對象,不包括執行對應的動作
     * @param   {object}   會把basket.require的參數傳過來,也就是多個對象
     * @returns {object}   promise對象
     */
    var fetch = function() {
        var i, l, promises = [];

        for ( i = 0, l = arguments.length; i < l; i++ ) {
            promises.push( handleStackObject( arguments[ i ] ) );
        }
        return RSVP.all( promises );
    };

    /**
     * 包裝promise的then方法實現鏈式調用
     * @returns {Object} 添加了thenRequire方法的promise實例
     */
    var thenRequire = function() {
        var resources = fetch.apply( null, arguments );
        var promise = this.then( function() {
            return resources;
        }).then( performActions );
        promise.thenRequire = thenRequire;
        return promise;
    };

    window.basket = {
        require: function() { // 參數為多個請求相關的對象,對象的屬性:url、key、expire、execute、unique、once和skipCache等
            // 處理execute參數
            for ( var a = 0, l = arguments.length; a < l; a++ ) {
                arguments[a].execute = arguments[a].execute !== false; // execute 預設選項為ture
                
                // 如果有隻執行一次的選項once,並之前已經載入過這個js,那麼設置execute選項為false
                if ( arguments[a].once && inBasket.indexOf(arguments[a].url) >= 0 ) {
                    arguments[a].execute = false;
                // 需要執行的請求的url保存到inBasket,
                } else if ( arguments[a].execute !== false && inBasket.indexOf(arguments[a].url) < 0 ) {  
                    inBasket.push(arguments[a].url);
                }
            }

            var promise = fetch.apply( null, arguments ).then( performActions );

            promise.thenRequire = thenRequire;
            return promise;
        },

        remove: function( key ) {
            localStorage.removeItem( storagePrefix + key );
            return this;
        },

        // 根據key值獲取對應ls的value
        get: function( key ) {

            var item = localStorage.getItem( storagePrefix + key );
            try {
                return JSON.parse( item || 'false' );
            } catch( e ) {
                return false;
            }
        },

        // 批量清除緩存對象,傳入true只清除過期對象
        clear: function( expired ) {
            var item, key;
            var now = +new Date();

            for ( item in localStorage ) {
                key = item.split( storagePrefix )[ 1 ];
                if ( key && ( !expired || this.get( key ).expire <= now ) ) {
                    this.remove( key );
                }
            }

            return this;
        },

        isValidItem: null, // 可以自己擴展一個isValidItem函數,來自定義判斷緩存是否過期。

        timeout: 5000, // ajax 預設的請求timeout為5s

        // 添加特定類型的執行函數
        addHandler: function( types, handler ) {
            if( !Array.isArray( types ) ) {
                types = [ types ];
            }
            types.forEach( function( type ) {
                handlers[ type ] = handler;
            });
        },

        removeHandler: function( types ) {
            basket.addHandler( types, undefined );
        }
    };

    // delete expired keys
    // basket.js 載入時會刪除過期的緩存
    basket.clear( true );

})( this, document );

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、變數和常量 定義 let 定義常量,一經賦值不允許再修改 var 定義變數,賦值之後仍然可以修改 自動推導 Swift能夠根據右邊的代碼,推導出變數的準確類型 通常在開發時,不需要指定變數的類型 如果要指定變數,可以在變數名後使用:,然後跟上變數的類型 重要技巧:Option + Click 可 ...
  • 這個問題讓我糾結了幾個小時,剛開始還以為是JS有問題,一直排查排查,,最後發現,是thinkphp3.2.3把JS正則的反斜杠\過濾掉了,導致JS正則失效,比如\d{4},解析出來變成了d{4} 解決辦法是可以用兩個反斜杠\\,比如\d{4}寫成 \\d{4}即可 ...
  • 效果: 需求和分析: 1.點擊TreeView中的節點,Listview中顯示相應的電視節目,其中節目的信息都儲存在xml文件中(IO的應用) 2.在“所有電臺”中選中節點右擊可進行添加到“我的電臺”,同時在”我的電臺“也可進行刪除,在你退出後,系統會把你選擇的”我的電臺“中的記錄記錄,在你再次打開 ...
  • 不談抽象類可以有實現等語法糖的問題,本文主要講在語義層面抽象類和介面的本質區別、以及使用以及選擇。 一、介紹 抽象類,首先是個類,類是對現實世界中對象的建模模型,抽象類是對類整體的抽象描述,包含方法,以及屬性。介面是對類某特性行為的抽象。 對抽象類的繼承才是Is-A的關係,對介面的實現,則是“有沒有 ...
  • 繼承是Is-A的關係。曾經看到有人為了復用而使用了繼承:有一個Base類,裡面寫了很多Common的方法,很多類繼承自這個類,這種做飯就是把Has-A用Is-A來實現的。 繼承的概念是說一個類是另一個類的特化。 Liskov替換原則:派生類必須能夠通過基類的介面而被使用,且使用者無需瞭解兩者之間的差 ...
  • × 目錄 [1]定義 [2]過渡屬性 [3]持續時間[4]延遲時間[5]時間函數[6]多值[7]階段[8]觸發[9]API 前面的話 通過過渡transition,可以讓web前端開發人員不需要javascript就可以實現簡單的動畫交互效果。過渡屬性看似簡單,但實際上它有很多需要註意的細節和容易混 ...
  • 一:純css+html的手風琴效果 這種用css寫的手風琴比較簡單,主要是應用到css中的,transition屬性。 代碼如下: 二:純js+html製作手風琴 這個手風琴出現一個問題,就是單獨移動每個li時,沒問題,但是當移動很快時,最右邊的li出現空隙。我感覺是定時器的問 題,就是當每個li還 ...
  • 一、六中數據類型: 二、運算符和表達式 七種運算符 三、程式控制語句 註:do-while迴圈至少執行一次迴圈體;break語句可以跳出迴圈語句;continue語句可以跳過迴圈內剩餘的語句進入下一次迴圈;label語句用於為語句添加標號 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...