2022/02/16

Memory Table Showdown! FdMemTable vs VirtualTable vs ClientDataSet, Part 1: Increase Data

Content

English Version

Recently, I saw people debating in a technical group about which method is better for writing data to a database, and it surprised me that the way a program controls writing to a database can affect the writing results. This piqued my curiosity, so I decided to conduct a test myself to see how much of an impact poorly written code could have on rapid memory table operations.

In Delphi's TDataSet, there are two well-known methods for adding new records:
  1. INSERT
  2. APPEND

What is "INSERT"?

Inserts a new, empty record in the dataset. 

What is "APPEND"?

Adds a new, empty record to the end of the dataset.

Some people say that INSERT is more efficient than APPEND, which is quite surprising. My previous impression was that APPEND was faster, but I don't remember how much faster it was. Now, I will write down the test results to see how much difference there really is between the two methods in terms of performance.

Testing method:

  • Test time: 100,000 records
  • Table structure: 3 fields (Integer, String, String)
  • Record unit: milliseconds

    Example using ClientDataSet

    From the above figure, it can be seen that Insert takes much more time than Append.

    Next is the time required for each ten thousand records:

    As the data amount increases, the time required for Insert also increases. When it reaches 90,000 to 100,000 records, it takes 797 ms.

    The result is obvious, Append is better.

    But this result is really boring!

    ClientDataSet is a type of Memory Table. If we compare it with other Memory Tables...

    Speaking of Memory Tables, there are many options to choose from. Here are the competitors for this test:

    • FireDAC -- TFdMemTable
    • Devart -- TVirtualTable
    • DataSnap -- TClientDataSet

    FireDAC -- TFdMemTable


    Similar to ClientDataSet, FdMemTable also spends more time on Insert than on Append.

    Devart -- VirtualTable





    In VirtualTable, we can also see that Insert takes longer than Append. But please note that the total difference of 32 ms is not very noticeable in terms of user experience. This means that users of VirtualTable do not need to pay too much attention to program details 😋.

    Three-way showdown

    The contestants have all completed their individual events, and now it's time for the showdown:




    Summary

    Although memory operations are very fast, careless handling can still lead to performance tragedies.

    In cases of massive data insertion, using the Append method is undoubtedly the best practice.

    The above conclusion is an exception for VirtualTable. 😁

    The performance difference between Insert and Append is the biggest in ClientDataSet. Developers who use it should pay attention to optimizing their code.

    FdMemTable, as the successor of ClientDataSet, is not good enough for performance-obsessed developers yet, but it still has room for improvement.

    "DBNavigator.Insert" and "TDataSetInsert" in ActionList are both "Insert" actions and neither of them has "Append". If needed, manual modification is required. Fortunately, in the case of "end-users", it will not have too much impact.


    Next time someone tells me that Insert has better performance than Append, I'll kick them out! 😊


    中文版本 (2017/08/14)

    最近在技術群組裡看到有人在爭論寫入資料庫的方式孰優孰劣,看到程式控制寫入資料庫的方法,居然也會影響寫入結果;這引發了我的好奇心,就自己來測試看看在飛快的記憶體表格操作,如果程式寫得不好,會有多大的影響。

    Delphi 的 TDataSet 在新增上有兩個著名的操作方法:

    • INSERT
    • APPEND

    What is "INSERT"?

    Inserts a new, empty record in the dataset. 

    What is "APPEND"?

    Adds a new, empty record to the end of the dataset.

    這兩者的比較,居然有人說 INSERT 比 APPEND 的效能要來得好,真是意外,我以前的印象中,記得是 APPEND 要來得快,但已經不記得快多少,現在就來把測試結果寫下來,看這兩者到底的效能差別到底有多大。

    測試方式:

    • Test time:100000 Record
    • Table Structure: Integer, String, String; 3 Fields
    • Record unit: ms

    以 ClientDataSet 為例:

    由上圖可知, Insert 遠比 Append 要花更多的時間。

    接著是每累加一萬筆時所需的時間:


    隨著資料量越多,Insert 所需的時間就越長,當九萬至十萬筆時,就需要花上 797 ms 的時間。

    結果很明顯,Append 勝出。






































    但這樣結果實在是太無趣了!

    ClientDataSet是一種 Memory Table,如果把其它的 Memory Table 一起抓來比較的話……

    說到 Memory Table 的選擇非常多,以下是本次參戰的選手:

    • FireDAC -- TFdMemTable
    • Devart -- TVirtualTable
    • DataSnap -- TClientDataSet

    FireDAC -- TFdMemTable



    就像 ClientDataSet 一樣,FdMemTable 在 Insert 明顯比 Append 花更多的時間。

    Devart -- VirtualTable






    VirtualTable 也可以看到 Insert 也要比 Append 要久。

    但請注意!32 ms 的總差距在體感上其實是差不多的!

    *意謂著使用 VirtualTable 的人可以不用太在意程式細節 (笑)


    大亂鬥

    選手們都已經完成個人賽事,接下來就要登台對決:




    總結

    僅管記憶體非常快,但操作不慎仍有可能造成效能悲劇。

    在大量新增的場合,使用 Append 的方式絕對是最佳作法。

    以上總結在 VirtualTable 是個例外。(笑)

    ClientDataSet 在 Insert / Append 效能落差最大,有在使用它的人要注意手上的程式碼優化工作。

    FdMemTable 作為未來 ClientDataSet 的接班人,對效能偏執狂的開發者來說還不夠好,請再繼續加油。

    「DBNavigator.Insert」和 ActionList 裡的「TDataSetInsert」兩者都是【Insert】行為,而且它們都沒有【Append】,需要的話則須人工修改。所幸在【一般使用者】的場合下,不會有太大的影響。






































    下次誰再跟我說 Insert 效能比 Append 好就踹飛他!



    See also:



    2022/01/10

    學會這招 FireMonkey 視覺佈局技巧讓你內力上升一甲子

    A little bit about FireMonkey and Delphi XE2 | Andreano Lanusse |  Technology and Software Development

    FireMonkey (FMX) 就 EMBT 的設計概念中是要解決跨平台的問題,那跨平台是要解決什麼問題?首當其衝的就是外觀設計了。

    跨平台的外觀設計 = 跨螢幕解析度的處理

    跨平台的視覺佈局簡單的說,就是要 APP 能在多款解析度的螢幕上都能正確顯示,不會有跑位、消失、需要手指八方滑動才能找到資料的額外行為。

    跨平台問題核心 = VCL 和 FMX 外觀設計原理的不同

    Delphi 最強大的 VCL 框架在外觀設計上完美的使用 Win32 API,造就了無數桌面應用程式,然而在其它系統上,VCL 外觀設計方法已經無法滿足在 MAC、iOS、Android 等多平台開發的需求,取而代之的就是 FMX,FMX 可以滿足多平台外觀設計的原因是它採用了外觀抽象化設計,FMX 獨有的外觀引擎作為讓外觀層和系統層橋接的媒介,以達到跨平台的目標。

    基於上述兩大關鍵因素,使用 FMX 設計前,Eden 要先來梳理 FMX 的外觀容器,才不會在之後的設計上到處採雷,

     

    TForm

     

    FMX 應用程式唯一標準容器,一個應用程式應當就只有一個 TForm,應用程式裡所有可視元件都會在這唯一容器上顯示。

    TPanel

    可視的元件容器,當有多數視覺元件需要群組時可用此容器群組化元件;由繼承關係來看,它本身會受到視覺樣式的影響,主要是延襲 VCL 框架裡的 TPanel 元件,使原來的開發者能快速使用。

    TLayout

    「不」可視元件容器,當有多數視覺元件需要群組時可用此容器群組化元件,和 TPanel 不同,它本身是不可視的;由繼承關係來看,它比 TPanel 更為輕巧,且能控制其子元件顯示功能,值得一提的是它帶有【自動適應能力】,在跨裝置上能自動調整可視元件的顯示排列,所以  TLayout 比 TPanel 更具競爭力,TLayout 容器可說是 FMX 的靈魂也不為過,有興趣學習 FMX 應用程式開發的朋友請務必善用此元件。

    TFrame

    TFrame 容器由繼承關係來看,它與 TLayout 容器處在同一個位階上,兩者不同之處在:TLayout 負責群組化可視元件,TFrame 則是負責群組化 TLayout 和其程式邏輯,也可以說透過 TFrame 可以創作出重覆利用的視覺模組,可以讓更多應用程式使用它。

    從熟悉 VCL 框架移轉到 FMX 時很是興奮,當時覺得行動平台的視覺開發上不能無腦延用 VCL 概念,如果相同就不用推新框架,而且在看過 Android、iOS 視覺設計的文件後,更加深對 FMX 理解的信念。

    除了視覺元件差異外,FMX 還有值得一試的設計技巧:

    自動適應能力

    自動適應能力在 Web 上是一個非常成熟的技術,Web 上使用 CSS 來完成,在 FMX 設計上則善用 TLayout 元件,可以在每個不同解析度的螢幕上試試,你會發現另一片天空。

    排版 (Layout)

    和自動適應力能一樣,Web 使用 CSS 來完成,FMX 上僅需熟悉 [akLeft,akTop,akRight, akBottom] 即可,推薦你來玩看看。

    元件綁定 (Data-Binding)

    專案在原型階段時就可以把不可視物件和可視元件進行綁定,不用煩腦更新資料如何替換到可視介面,使我們更能專注在開發上,更重要的是當進入產品開發期則僅需替換資料庫元件就能立馬上線,想不炫技都很困難!

    原生支援

    FMX 為了跨平台,把各家平台許多原生 API 都封裝成好用的物件,目的就是讓你可以專注在 Delphi 程式開發,而不用在各家平台 API 手冊上奔走學習,不止節省開發時間,也更好地發揮應用程式效率,可以讓你的使用者滑得不亦樂乎!

     

    總結

    當然還有更多設計上神技巧,日後有學習到更多的技巧會再整理起來和各位分享。

    還在觀望 Delphi  的發展性?現在 Delphi 推出了 Delphi Community Edition 社群版,現在免費提供給想加入 Delphi 開發行列的朋友,我也寫了幾篇關於 Delphi CE 的介紹,邀請你現在就搜尋看看。😉

     

    See also

    2021/11/24

    建立一款問卷調查系統的 React 應用程式


    在學習 React 時,最快樂的莫過遇到淺白易吸收的好文章。

    Joe Morgan 所寫的【How To Add Login Authentication to React Applications】就是一例。


    參考它的流程,讓我想到可以利用它來設計一款問卷調整系統,App 架構和路由經整理後如下圖所示:

    2021/11/10

    React Router 從 v4, v5 升級 v6 的異動


    Router v6 的改版所變動之處非常大,許多舊語法、屬性被【移除】,可以說是新產品也不為過,雖然作者有提出未來可能會提供向下相容的語法,但目前為止仍舊沒有出現,我想再回去的機率很低,還是接受現實改為新版語法吧!

    withReouter 已移除

    withReouter 已移除,改用 Router Hooks 取代,這意味著以前使用 class 所開發的元件將無法繼續使用,改為函式元件似乎是條不得不為的道路。

    <Route /> path 移除多路徑陣列寫法

     <Route path=["/list", "/list/:id"] /> 寫法在 v6 已不可用,作者說 Router v6 引擎有針對多路由做最佳化,使用多次宣告 Route 很直覺,效能更好。

    <Route /> exact、componet、render 屬性移除

    exact 屬性移除,前面提到作者有對 Router v6 引擎最佳化,故不再需要 exact 絕對路徑屬性。

    Route 的 Component、Render 屬性移除,改以 element 取代,傳參數的方式改為 useParams 等 Hooks 後就不再需要以前的 Component、Render 屬性,寫法可以一致化。

    Link, NavLink 移除部份屬性

    activeClassName、activeStyle 被移除,改以 Style 屬性取代,這裡官方手冊有個地方範例程式碼寫錯:

    而在說明整合的文件中有寫出正確的內容:


    範例使用 className 說明,這裡也提到 style 寫法相同,但就僅止於此,一不小心會花很多時間在找問題上。


    還是可以寫得出範列所要傳達的效果,請放心。

    總結

    Router v6 改動的地方非常多,現在書和網路上許多資料還停留在 v4、v5 上,在使用 v6 時常會用到已移除的功能,寫起來倍感艱辛。

    改版的過程中會發現其實 v6 改動方向是把路由工作劃分得很清楚,Route 歸 Route;Link 歸 Link,Element 歸 Element,寫作上比較不會有串來串去的情形。

    整體來說 v6 的改動是利大於弊,只要你能熬過那升級陣痛期的話。

    和你分享 😉

    See also

    2021/10/15

    JS DataSnap framework in Webpack

     


    Webpack 5 初探筆記】這篇記錄了 Webpack 三項重點:

    • JS 入口
    • HTML Page 建立及嵌入 JS 設定。
    • 除錯模式

    你以為已經很多了?不,還有線上除錯服務器 (Webpack devServer) 還沒用到,該篇文章僅就編譯功能處理,編譯完成後再到實機裡除錯。

    所以本篇就來研究如何讓 Webpack devServer 和後端 WebAPI 結合,本篇文章會講到兩個重點:

    • 使用 devServer Proxy 來解決【跨來源資源共用(CORS)】問題。
    • JS DataSnap framework 修改呼叫 DataSnap API 的方式。

    使用 devServer Proxy 來解決【跨來源資源共用(CORS)】問題

    devServer 的基本功能是提供網頁服務,熱編譯重讀 (hot reload) 功能則是在前端網頁開發時省時好幫手,檔案一儲存畫面就立即更新,這能省下許多重新編譯的時間,所以這次就把使用它為目標。

    webpack 設定 devServer 非常簡單,內容如下:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    module.exports = {
      ...
        devServer: {
            static: {
                directory: path.join(__dirname, 'dist'),
            },
            hot: true,
            port: 9000,
        }
      ...
    }
    

    接著在 index.js 裡寫上範例程式碼:

    1
    2
    3
    setConnection("localhost", "8080", "")
    var oldExecutor = new ServerFunctionExecutor("TServerMethods1", connectionInfo);
    console.log("EchoString : ", oldExecutor.executeMethod("EchoString", "GET", ["A B C"]))
    

    網頁在啟動後會出現錯誤:


    已封鎖跨來源請求: 同源政策不允許讀取】這句話的翻譯就是:後端網頁伺服器未對 CORS 做出對應的處理,在這裡指的便是 DataSnap 伺服器。

    Eden 的【Delphi in Depth DataSnap 網站應用程式全端開發】一書中所提到的 CORS 解決方案其實就是要解決前端伺服器呼叫 DataSnap 伺服器上的 API 所產生的問題,有興趣的開發者邀請你購買起來看,保證物超所值!

    devServer 對 CORS 的處理方式:Proxy

    來看看 Webpack 官網對 Proxy 是怎麼寫的:

    当拥有单独的 API 后端开发服务器并且希望在同一域上发送 API 请求时,代理某些 URL 可能会很有用。

    「擁有單獨的 API 後端開發服務器」以本篇指的就是 DataSnap 伺服器,所以按照官網的範例調整後,webpack.config.js 內容為:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
        devServer: {
            static: {
                directory: path.join(__dirname, 'dist'),
            },
            hot: true,
            port: 9000,
            proxy:{
                "/datasnap/rest":"http://localhost:8080"
            }
        },
    

    現在,http://localhost:9000/datasnap/rest 的網址就會對應到 http://localhost:8080/datasnap/rest,完成後就來試一下:


    設定成功,現在可以順利在開發時期輕鬆呼叫 DataSnap API! 😉


    JS DataSnap framework 修改呼叫 DataSnap API 的方式

    我在【JavaScript ES6 call DataSnap API with Promise Fetch】寫到使用 fetch 來取代 ServerFunctionExecutor.executeMethodUrl 裡的 XMLHttpRequest,但為了避免回呼地獄 (callback hell) 而放棄 ServerFunctionExecutor 整個類別,必須要說,我真的很喜歡 EMBT 寫的 ServerFunctionExecutor 類別,它不僅是我學習 JavaScript 的啟蒙,而且對瀏覽器相容性也達到 100%,連 IE6 都相容,我真的愛死 ServerFunctionExecutor 類別了!

    修改 ServerFunctionExecutor.js 原始碼是一種方法,但每一版的 Delphi 對 ServerFunctionExecutor 類別都有或多或少的修改,我認為最好的方案就是【繼承】,既然要用 fetch,也表示要放棄對 IE 的相容性,那採用 JavaScript ES6/7 新標準也是可以的,程式碼如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
     10
     11
     12
     13
     14
     15
     16
     17
     18
     19
     20
     21
     22
     23
     24
     25
     26
     27
     28
     29
     30
     31
     32
     33
     34
     35
     36
     37
     38
     39
     40
     41
     42
     43
     44
     45
     46
     47
     48
     49
     50
     51
     52
     53
     54
     55
     56
     57
     58
     59
     60
     61
     62
     63
     64
     65
     66
     67
     68
     69
     70
     71
     72
     73
     74
     75
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    import "./dsjs/connection"
    import "./dsjs/ServerFunctionExecutor"
    
    class DSFunctionExecutor extends ServerFunctionExecutor {
        constructor(className, connectionInfo, owner) {
            super(className, connectionInfo, owner)
        }
    
        /**
         * This function executes the given method with the specified parameters and then
         * notifies the Promise when a response is received.
         * @param url the url to invoke
         * @param contentParam the parameter to pass through the content of the request (or null)
         * @param requestType must be one of: GET, POST, PUT, DELETE
         * @param hasResult true if a result from the server call is expected, false to ignore any result returned.
         *                  This is an optional parameter and defaults to 'true'
         * @param accept The string value to set for the Accept header of the HTTP request, or null to set as application/json
         * @return This function will return the result that would have otherwise been passed to the Promise.
         */
        async #fetchMethodURL(url, contentParam, requestType, hasResult, accept) {
            if (hasResult == null) hasResult = true;
    
            requestType = validateRequestType(requestType);
    
            const fetchHeaders = new Headers();
            fetchHeaders.append("Accept", (accept == null ? "application/json" : accept));
            fetchHeaders.append("Content-Type", "text/plain;charset=UTF-8");
            fetchHeaders.append("If-Modified-Since", "Mon, 1 Oct 1990 05:00:00 GMT");
            const sessId = getSessionID();
            if (sessId != null)
                fetchHeaders.append("Pragma", "dssession=" + sessId);
            if (this.authentication != null)
                fetchHeaders.append("Authorization", "Basic " + this.authentication);
    
            const fetchParams = {
                method: requestType,
                body: contentParam,
                headers: fetchHeaders,
            }
            try {
                const response = await fetch(url, fetchParams)
                this.#parseFetchSessionID(response);
                const responseText = await response.text();
                let JSONResultWrapper = null;
                try {
                    JSONResultWrapper = JSON.parse(responseText);
                }
                catch (e) {
                    JSONResultWrapper = responseText;
                }
                if (response.status == 403) {
                    if (JSONResultWrapper != null && JSONResultWrapper.SessionExpired != null) {
                        //the session is no longer valid, so clear the stored session ID
                        //a new session will be creates the next time the user invokes a server function
                        setSessionData(null, null);
                    }
                }
                //all other results (including other errors)
                //return JSONResultWrapper;
                const returnObject = JSONResultWrapper;
                if (returnObject != null && returnObject.result != null && Array.isArray(returnObject.result)) {
                    return returnObject.result[0];
                }
                return returnObject;
            }
            catch (err) {
                console.error('Error:', err)
                return err
            }
        };
    
        /**
         * This function executes the given method with the specified parameters and then
         * notifies the callback when a response is received.
         * @param methodName the name of the method in the class to invoke
         * @param requestType must be one of: GET, POST, PUT, DELETE
         * @param params an array of parameter values to pass into the method, or a single parameter value
         * @param hasResult true if a result from the server call is expected, false to ignore any result returned.
         *                  This is an optional parameter and defaults to 'true'
         * @param requestFilters JSON Object containing pairs of key/value filters to add to the request (filters such as ss.r, for example.)
         * @param accept The string value to set for the Accept header of the HTTP request, or null to set application/json
         * @return This function will return the result that would have otherwise been passed to the Promise.
         */
        async fetchMethod(methodName, requestType, params, hasResult, requestFilters, accept) {
            const url = this.getMethodURL(methodName, requestType, params, requestFilters);
            return await this.#fetchMethodURL(url[0], url[1], requestType, hasResult, accept);
        };
    
        /**
         * Tries to get the session ID from the Pragma header field of the given request/response object
         * If successful, will set the value of the $$SessionID$$ and $$SessionExpires$$ variables accordingly.
         * @param response the response from the http request
         */
        #parseFetchSessionID(response) {
            if (response != null) {
                //pragma may store the Session ID value to use in future calls
                var pragmaStr = response.headers.get("Pragma");
    
                if (pragmaStr != null) {
                    //Header looks like this, if set: Pragma: dssession=$$SessionID$$,dssessionexpires=$$SessionExpires$$
                    var sessKey = "dssession=";
                    var expireKey = "dssessionexpires=";
                    var sessIndx = pragmaStr.indexOf("dssession=");
    
                    if (sessIndx > -1) {
                        var commaIndx = pragmaStr.indexOf(",", sessIndx);
                        commaIndx = commaIndx < 0 ? pragmaStr.length : commaIndx;
                        sessIndx = sessIndx + sessKey.length;
                        var sessionId = pragmaStr.substr(sessIndx, (commaIndx - sessIndx));
    
                        var sessionExpires = null;
                        var expiresIndx = pragmaStr.indexOf(expireKey);
                        if (expiresIndx > -1) {
                            commaIndx = pragmaStr.indexOf(",", expiresIndx);
                            commaIndx = commaIndx < 0 ? pragmaStr.length : commaIndx;
                            expiresIndx = expiresIndx + expireKey.length;
                            var expiresMillis = parseInt(pragmaStr.substr(expiresIndx, (commaIndx - expiresIndx)));
                            if (expiresMillis != 0 && expiresMillis != NaN) {
                                sessionExpires = new Date();
                                sessionExpires.setMilliseconds(sessionExpires.getMilliseconds() + expiresMillis);
                            }
                        }
    
                        setSessionData(sessionId, sessionExpires);
                    }
                }
            }
        }
    }
    

    程式碼說明

    因 fetch 回傳的 response 和 XMLHttpRequest 的不同,所以抄了原來的 executeMethodURL、 parseSessionID 為 #fetchMethodURL 和 #parseFetchSessionID,加了「#」是為私有屬性,以避免免被其它開發者誤用。

    應用程式大多需要同步處理以得到較好的操作性,故使用 async / await 等待 response 回來。

    修改和執行結果如下:

    這裡要留意 Webpack Babel 外掛在編譯這類別的時候可能會出現【regeneratorRuntime is not defined】錯誤訊息,原因很複雜,【Webpack 前端打包工具 - 使用 babel-loader 編譯並轉換 ES6+ 代碼 】提供了說明,而解決方式很簡單:

    安裝 Babel transform-runtime 外掛:

    npm install --save-dev @babel/plugin-transform-runtime
    npm install --save @babel/runtime
    //babel.config.json


    {
    "presets": ["@babel/preset-env"], "plugins": ["@babel/transform-runtime"]
    }


    本次 npm 用到的指令

    npm init
    npm i webpack -D
    npm install --save-dev css-loader
    npm install --save-dev style-loader
    npm install --save-dev html-webpack-plugin
    npm install --save-dev mini-css-extract-plugin
    npm install -D babel-loader @babel/core @babel/preset-env webpack
    npm install --save-dev clean-webpack-plugin
    npm install copy-webpack-plugin --save-dev
    // 排除 ES6 太新語法造成 "Babel 7 - ReferenceError: regeneratorRuntime is not defined"
    // https://stackoverflow.com/questions/53558916/babel-7-referenceerror-regeneratorruntime-is-not-defined
    npm install --save-dev @babel/plugin-transform-runtime
    npm install --save @babel/runtime

    結論時間

    透過這次 Webpack 的練習,發現只要 JS 該引用的單元都正確引用,Visual Studio Code 都能夠正確追蹤到程式碼來源或 JS DOC,尤其是在追蹤 JS DataSnap framework,VS Code 真的有效加強學習的效果,向你推薦!

    本次練習專案我放在 Github 上,連結在 See also 區,有興趣的朋友歡迎來看看。 😉


    See also

    2021/10/08

    Webpack 5 初探筆記

     


    在寫完【還在 React 從入門到放棄?告訴你選擇比努力還重要的事】這篇文章之後,原本要繼續我的 React 裸奔之旅,但看到【【前端速成】Webpack5 快速入門|Tiktok工程師帶你入門前端|布魯斯前端】的直播內容,發現 Webpack 和 Delphi 的專案檔實在太像了,手癢忍不住動手把自己的 jQuery 網站利用 Webpack 重新打包順道重構,過程很有趣,而且整理後的專案內容就像是看網站地圖一樣,看了心情大好。

    趁還有記憶時把 Webpack 的參數檔和 NPM package 參數檔透過這偏文章寫下來。

    Package.json

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // package.json
    {
      "name": "eden_first_project",
      "version": "1.0.0",
      "description": "",
      "main": "maindesk.js",
      "scripts": {
        "build": "SET NODE_ENV=production && webpack",
        "design_build": "SET NODE_ENV=development && webpack && copyfile_with_debug.bat",
        "dev": "webpack serve"
      },
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "@babel/core": "^7.15.5",
        "@babel/preset-env": "^7.15.6",
        "babel-loader": "^8.2.2",
        "clean-webpack-plugin": "^4.0.0",
        "html-webpack-plugin": "^5.3.2",
        "webpack": "^5.56.1",
        "webpack-cli": "^4.8.0",
        "webpack-dev-server": "^4.3.1"
      }
    }
    

    程式說明

    devDependencies:內容是 npm 安裝的外掛工具及版本,功能有:

    • webpack 本體
    • webpack 外掛 -- 清理編譯後檔案
    • webpack 外掛 -- 頁面樣板設定
    • babel -- 轉譯 ES6 程式碼為其它瀏覽器可讀的程式碼內容

     

    scripts:在命令提示字元視窗下執行 webpack 的指令,語法為【npm run [scripts]】,SET 是設定 Windows 的環境變數,可以依照【設計】或【產品】語法來改變 webpack 封裝設定。[&&] 是 Windows 執行多指令的節點,某些靜態檔案還可以透過批次檔進行複製。

    npm 在對專案處理時都會以 package.json 作為進入點,比較要記的就是這兩個屬性。


    webpack.config.js - part 1

    程式說明

    package.json 的 SET NODE_ENV,它會在 webpack.config.js 的 process.env 被 NPM 寫入環境變數。

    先快速導覽大分類,之後再一個個進去看。

    module.export 這是 webpack 專案檔的核心。

    mode 設定【設計】或【產品】模式;entry 要被 webpack 編譯的 js 檔案。

    output 指定原檔案編譯後存放的路徑和命名規則。

    devServer 執行 webpack server 時 Web 伺服器的參數。

    module webpack 在讀取 js 檔以外的檔案時,所需要的 loader

    devtool 編譯時做對照檔 (map),使瀏覽器能夠讀出編譯和原始程式碼的對照檔

    plugin webpack 外掛設定區。 

    webpack.config.js - part 2


    程式說明

    entry 裡,要寫上所有需要編譯的 js 檔案,格式為【暱稱:路徑】,暱稱在之後的【plugins 》 HtmlWebpackPlugin】會用到。

    output 裡,path 說明 entry 裡的檔案在編譯後所放置的路徑,以 package.json 所在路徑為 root,可以自由指定,filename 為了確保客戶端每次都會重新讀取,可以把檔案加入【hash】參數,編譯會將 hash 碼帶入檔名,如:maindesk.f22745d9520340e7af08.bundle.js。


    webpack.config.js - part3


    程式說明

    plugin 裡面主要放 webpack 外掛模組,有:

    CleanWebpackPlugin 是每次編譯當下都會將目的目錄內的檔案全數清空。

    webpack.DefinePlugin 是定義編譯環境的全域變數,此例可以在網路中的任何 JS 寫上類似以下的內容:

    可以在編譯時決定要執行哪部份的程式內容,好用!

    HtmlWebpackPlugin 決定讀取的樣板和 HTML 檔案路徑,chunks 是決定要寫入 entry 的哪些檔案,以暱稱設定,設為空字串時則不匯入任何 js 檔;也可以使用 excludeChunks 決定不加入的 js 清單。


    結論時間

    學習 Webpack 的過程中不斷帶入以前寫 Delphi 專案檔的時光,所以還蠻能理解 Webpack 的各項設定的內容,都已經用編譯了,那就順便把 Delphi 的設定概念套入,Webpack 還有很多細節的設定,只要大方向有了,再往細節裡找的難度就會下降很多!

    今日成就:把 jQuery 網站套入 Webpack 專案編譯成功!


    和你分享 😉

    2021/10/04

    還在 React 從入門到放棄?告訴你選擇比努力還重要的事


    看了許多 React 的入門資料,大多會是從 create-react-app 專案起始,用久了覺得很不舒服,原因如下:

    NPM 編譯經常無預期出錯

    NPM 編譯經常無預期出錯,學習 React 免不了會經過一連串試錯的過程,但往往會不經意的出現許多編譯錯誤,大多是 Webpack 出現問題,在無法排除錯誤的情形下,最常做的就是重建 node_modules 內容,次數一多,是在學習 React 還是背誦 NPM i 指令,我已經搞不清楚了。

    在設計時期,前端和後端 API 網域位置不一致時

    只要不是靜態網頁,在前端的開發過程一定會和後端 API 傳遞訊息,尤其是開發網頁應用程式更是必須。在設計時期,只要前端和後端的 Web 服務網域不同,就一定會遇上【跨來源資源共用(Cross-Origin Resource Sharing (CORS))】這個大魔王,後端就必須進行 CORS 設定,但實際上線又會是在同一網域,只有開發時期才會遇到的 CORS 顯得是非常詭異的歷程。

    你以為要從 JavaScript ES6 開始學習,事實上