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 開始學習,事實上

包含 React 官方網站,很多學習 React 的資料顯示,JavaScript ES6 是必須先學會的基礎,我認為這是造成 React 陡峭的學習曲線的罪魁禍首。許多社團新手遇到的問題大多是編譯 React 時產生的異常狀態,NPM 會回傳非常完整的【長篇報告】,也因為內容非常的多,若直接複製貼上,只會造成有心要幫忙的人在理解前還必須先把整篇報告看過一輪,除錯到心累。

React 三大核心

React 是水很深的函式庫之一,技術中心思想在【狀態的傳遞】,基於中心所延伸出來的有三大核心:

  • React : 屬性、狀態、事件、元件和元件狀態互動等。
  • Router : 頁面到頁面間狀態的互動
  • Redux : 全域狀態管理

React 的問題大多就是狀態問題,Router 和 Redux 都是 React 官方沒做但是推薦的解決方案,都是學習 React 過程中必須學習的技能。

我歸納學習 React 學習資源後,發現從 create-react-app 幾乎就是給新人從入門到放棄的好專案,而我認為學習 React 路線有二:

拿掉 NPM 編譯,純粹的 React

我發現 React 的能手中,多數也是 CSS 的能手;許多外觀功能面,能使用 CSS 完成的,全都自己處理,對於 CSS 已經熟悉的能手,走此路線能讓你以最短的時間走遍所有 React 特色。

甚至不需編譯,直接使用 HTML Include CDN 的方式都能游刃有餘。

REACT 三大核心 CDN:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 <head>

    <meta charset='UTF-8'>

    <script src='https://unpkg.com/react@17/umd/react.production.min.js'></script>
    <script src='https://unpkg.com/react-dom@17/umd/react-dom.production.min.js'>    </script>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/8.0.0-alpha.0/react-redux.min.js" integrity="sha512-3f0oep7JUJU3ocfNA2e1dM9qxlz7mQpbM0HkmTg43/K/6YL/YnRNIAkzyqpE6OySBU6Y6jTL4y7BHSJr+WJhMA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

    <script src='https://unpkg.com/react-router-dom@5/umd/react-router-dom.min.js'></script>

    <script src='https://unpkg.com/babel-standalone@6/babel.js'></script>

  </head>


Babel 屬於線上編譯器,處理 JSX 和 ES6 和瀏覧器相容性問題,不使用 Babel,直接使用 React.createElement 也是可以的,網上 JSX 範例可以透過 Babel 的 JSX 線上編譯器玩玩看怎麼編譯的。


擁抱編譯,從 Webpack 開始

新手學習 React 的第一步應是 Webpack 而非從 React 開始,依賴 React 三方元件的開發者更應優先學習 Webpack,學習如何把現行網站透過 Webpack 封裝,從方便維護現行網站開始熟悉 Webpack,之後再一步一步導入 React,而非從 create-react-app 開始。

Webpack 學習好資源:【前端速成】Webpack5 快速入門|Tiktok工程師帶你入門前端|布魯斯前端


結論時間

就以使用 React,是否要走編譯來說,我認為【入門學習】或【使用上都以自刻 CSS 為主】的場合,使用 HTML include 的方式是可以的。但若會用到 React 三方廠商提供的元件,因為絕大多數都沒有推出 Bundle 版本,所以這時會考慮以編譯為主,不論那一種方式,Webpack 學習都是穩賺不賠的好技能,推薦大家學習它。


和你分享。😉


2021/09/17

逐步使用 React 時,元件如何和外部 DOM 連結?

已知 React 會渲染指定的 div,學習資料中似乎沒有和外部 DOM 連結的方法?

React 元件渲染的條件是 props 或 state 變更時觸發,所以把 DOM 元素放入到 React 元件的 Props,就能夠滿足此條件,範例程式碼如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const e = React.createElement;

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.props.textBox.onchange = () => this.calcCallback();
    this.state = { text: "" };
  }

  calcCallback() {
    this.setState((state, props) => ({
      text: this.props.textBox.value
    }));
  }

  render() {
    return e(React.Fragment, null, e("div", null, this.state.text));
  }
}

ReactDOM.render(
  e(MyComponent, { textBox: document.querySelector("#textBox1") }, null),
  document.querySelector("#app2")
);

HTML

1
2
<input id="textBox1" value="Hello Vanilla"></input>
<div id="app2"></div>

 

成果圖



 

 

 

 

2021/09/16

愛用 jQuery 又想擁抱 React 的逐步採用練習,以 DevExtreme 為例

React 逐步採用

不知道你是否和我一樣,斷斷續續學幾年的 React,總是無法順利把 React 加入到現有的 jQuery 網站中,學 jQuery 時很快樂,需要什麼功能時只需要在 script 標籤裡信手捻來 js 檔案就能快速使用,怎麼到了 React 就完全走鐘?非得使用 NodeJS 編譯才可以?React 起手式就是 create-react-app?這其中一定有什麼誤會,帶你來看看官方說法:

React 在剛推出的時候就容許被逐步採用,你可以按自己所需可多可少的採用 React。 或許你只想在現存的網頁上增加少量的互動性。採用 React component 會是個非常好的選擇。 -- React 官方手冊

按照官網裡的步驟練習結束,就如上面所說一樣,可以很順利的把 React 加入不需 NodeJS 編譯的傳統網站裡。

造成網頁變慢的元兇

會學習到可選的 JSX,JSX 很有意思,可以在 JS 裡安插類 HTML 標籤,從【逐步採用】學習到【可選:嘗試 React 與 JSX】的最後一段結論:

這種方式最適合用來學習和建立簡單的示範。然而,它會令你的網頁變慢,而且並不適合發佈到線上環境。 -- React 官方手冊:快速嘗試 JSX 章節

之所以會令你的網頁變慢,是因為它是載入 babel.js 後在網頁執行時才編譯 JS 裡的 JSX 標籤內容,每次都要花時間編譯,網頁自然快不了。

原來網頁變慢是因為動態編譯 JSX,而不是 React 慢。

JSX 最佳用法就是預先編譯,但

官方的教學在此之後就是利用 NodeJS 把 babel 安裝到現有的 WebPack 專案中,再編譯 JSX 內容成為標準 JS 內容,最後才把編譯後的成品上傳到網站執行。

等等,這樣就不是逐步採用 React 了,要加入 WebPack 就等於是大改現有網站啦!

小結:要逐步採用 React 就不能使用 JSX

原有網站在沒有 WebPack 加持下,要逐步採用 React 勢必不能使用 JSX,因使用 Babel 動態轉換 JSX 的效率不佳。

結論就是:逐步採用 React 下又不要使用 NodeJS 編譯網站,React.createElement 你只能用它了。 😁

逐步採用 React 下的難題:外掛元件

逐步採用 React 雖然可以限縮自己要學習的內容,比如我只要學 ES6 + React.State + React.Props 等,只要會 CSS,要刻什麼都很簡單。

但外掛元件呢?

我的現有網站使用了大量的 EasyUI 和 DevExtreme 元件,這些元件僅提供 NodeJS 下載安裝,並沒有發佈編譯後可使用的 JS 檔案。

解決方案就只好在 React class 裡使用 jQuery 操作外掛元件?

 

這叫 React 入門到放棄。


所幸的是,在 DevExpress 官網裡有個叫【DevExtreme Tickets】的技術小本本,它記錄許多過往使用者所提出的技術問題和解決方案,其中一個主題是:Have demo about devextreme-react without npm?

DevExtreme 官方提出【提取+橋接】的概念。撇開現有網站,另開【Webpack-React-DevExtreme】專案,使用 WebPack 提取所需的元件,如 dxButton 等,再利用 NodeJS 編譯,把提取並編譯過的 JS 檔放入現有網站,如此就能在逐步採用 React 的場合下又能享受外掛元件的開發便利性。

而外掛元件則是存放到 DOM 的 window 裡,範例程式碼如下:

DevExtreme 提取 JS

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import * as React from "react";
import { Button } from "devextreme-react/button";

export class ButtonExample extends React.Component {
    // <Button icon="plus" text="Click me"/>
  constructor(props) {
    super(props);
  }
    render() {
        return (
          <Button {...this.props} />
        );
    }
}

const MyLib = {
    ButtonExample
}

export default MyLib

逐步採用 React 專案呼叫編譯後的 DevExtreme 元件

 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
<!doctype html>
<html>

<head>
  <meta charset="utf-8">
    <link rel='stylesheet' type='text/css' href='https://cdn3.devexpress.com/jslib/18.1.3/css/dx.common.css' />
    <link rel='stylesheet' type='text/css' href='https://cdn3.devexpress.com/jslib/18.1.3/css/dx.material.blue.light.css' />  
  <title>DevExtreme with webpack and React example</title>
</head>

<body>
  <div class="container">
    <h1>DevExtreme with webpack and React example</h1>
    <div id="app"></div>
    <div id="app2"></div>
  </div>
    <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
    <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
  <script src="js/app/bundle.js"></script>
  <script>
    const e = React.createElement
    ReactDOM.render(e(window.MyLib.ButtonExample, { icon:"plus",text:"Click me"}, null), document.getElementById("app2"))
  </script>
</body>

</html>

至於 Webpack-React-DevExtreme 原始碼,你可以在上述的票證裡找到。

結論時間

愛用 jQuery 是我個人的選擇,你也可以使用單純的 Vanilla JS,端看你的場合和上手程度來使用。

初學 React 最怕的就是在環境上的摸索上花很多時間,在不使用 npm、webpack 甚至是 Babel,現在我可以專注在學習 React 的設計上,並且和現有 HTML / JavaScript 專案進行互動,做中學所得到的回饋是最直接的。

官方不建議採用【逐步使用 React】?官方是建議使用 NPM Bundler,官方有說明建議原因,和相關資源也比較多,但有關於官方不建議【逐步使用 React】?我官方網站翻了翻,沒找到,有找到的期待你分享這類的相關資料。

React 裡要不要用 jQuery 是另一個問題,React 元件裡我是盡量用相容性比較高的 ES5/6 標準,至於 IE,我的環境不會遇到它,忽略也無仿。

在逐步採用 React 又要使用外掛元件,不再是魚和熊掌的問題,使用一個簡單 WebPack 專案 + 一次編譯,就能把所需的外掛元件放到 window 中,完全滿足「我全都要」的需求,實在是太棒啦!

如果有其它的外掛元件,我想也可以參照此方法把外掛元件產出。

底下是使用 Vanilla JS + React 使用 DevExtreme 小部件的範例。

和你分享 😉

SEE ALSO

2021/09/13

你需要處理 Unicode 編碼?在複製之前邀請你思考這個問題


底下的程式內容到了 XE 後已經被 TEncoding 物件所取代,但這問題時常被問到,之前主流是 Base64,當時還可以用 EncdDecd 單元處理 Unicdoe 編碼,現在已逐漸改以 JSON 格式為主流,EncdDecd 單元已無法滿足眼下的需求了。

WideString 是 Delphi 7 相容 Unicode 的字串類型,然而 WideString 並不適合作為資料傳輸,要達到良好的相容性,使用 UTF16 是比較好的選擇,可以確定的是 Delphi 7 沒有 WideString 對 JSON 的 Encode 和 Decode 的函式內容,不想自己寫?不囉嗦,直接上程式碼:

 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
function DecodeUTF16(sStr: string): WideString;
var
  i: Integer;
  StrPos: Integer;
  temp, top, last: string;
  ResultStr: WideString;
begin
  ResultStr := '';
  repeat
    StrPos := Pos('\u', sStr) - 1;
    if StrPos < 0 then begin            // 無 unicode 編碼不需轉換
      last := sStr;
      ResultStr := ResultStr + last;
      Break;
    end;
    top := Copy(sStr, 1, StrPos);       // 取出編碼字符前非 unicode 編碼的文字,如數字
    temp := Copy(sStr, StrPos + 1, 6);  // 取出編碼,包括 \u,如\u53f0
    Delete(temp, 1, 2);
    Delete(sStr, 1, StrPos + 6);
    ResultStr := ResultStr + top + WideChar(StrToInt('$' + temp));
  until (StrPos >= 0);
  Result := ResultStr;
end;

function EdenUTF16(const sStr: WideString): string;
var
  w: Word;
  StrPos: Integer;
  UTF16Str, TmpStr: string;
begin
  TmpStr := '';
  for StrPos:=1 to Length(sStr) do begin
    w := Ord(sStr[StrPos]);
    UTF16Str := IntToHex(w, 4);
    TmpStr := TmpStr +'\u'+ UTF16Str;
  end;
  Result := TmpStr;
end;

 

結論時間

以上的程式碼雖然能應用在實戰中,其 Delphi 7 裡的函式庫幾乎沒有對 WideString 處理的問題需要正視,在尋求古老三方元件的協助也無可避免需要手刻不足的區塊;自 Delphi XE 開始至現在的 Delphi 11 早已解決絕大部份 Unicode 的麻煩。若你仍有中文處理需求,真心邀請你使用 Delphi CE 社群版來進行專案提升,省下你開發的時間,專注地在商業邏輯的開發會為你帶來更好的效益,畢竟公司開來目的是為了賺錢,而非程式碼研究,研究的事我來就行。😁

和你分享 😉

 

 

2021/09/10

Delphi XE 存取目錄檔案清單、和排序方式


 

最近同事詢問到 Delphi 如何讀出檔案時間,而版本為 Delphi 7,網路資料很多,Google 一找一大把,但關於 XE 以上的新版本卻找不到什麼資料,究竟新版 Delphi 有沒有更好的解法呢?

這問題可以拆分為:

  • 讀取目錄檔案
  • 讀取檔案時間
  • 儲存檔案清單(含時間)
  • 排序檔案清單

有了以上步驟後,各別突破就簡單了。😉

讀取磁碟資料在 XE 後,為了跨平台特別設計了【IOUtils】單元,目的在各家平台都能使用相同程式碼讀取其儲存媒體內容,所以 IOUtils 是非常值得投資學習的單元。

讀取目錄檔案

IOUtils.TDirectory 是讀取目錄結構的物件,只要使用 GetFiles 就可以讀出目錄內所有檔案,簡單程式碼如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var
  LFileList: TStringDynArray;
  LFile: string;
begin
  LFileList := IOUtils.TDirectory.GetFiles('D:\Eden的目錄');
  for LFile in LFileList do
  begin
    ListBox1.Items.Add(LFile);
  end;
end;


讀取檔案時間

取得檔案路徑後,就可以依序取得它們的時間,在 IOUtils 中,TFile 物件可以取得以下時間

  • GetCreationTime: 檔案建立時間
  • GetLastAccessTime: 檔案最後讀取時間
  • GetLastWriteTime: 檔案最後寫入時間

以「檔案建立時間」為例,簡單程式碼如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var
  LFileList: TStringDynArray;
  LFile: string;
begin
  LFileList := IOUtils.TDirectory.GetFiles('D:\Eden的目錄');
  for LFile in LFileList do
  begin
    ListBox2.Items.Add(DateTimeToStr(IOUtils.TFile.GetCreationTime(LFile)));
  end;
end;


儲存檔案清單(含時間)

有了檔案和時間清單,就要進行兩者的結合,使用快取資料集是不錯的選擇,Delphi 內建的快取資料集有兩個:

  • TClientDataSet
  • TFdMemTable

TFdMemTable 是比 TClientDataSet 更為輕巧的資料集物件,而 TClientDataSet 則是經典物件,選擇上看個人喜好,設計上大同小異。簡單程式碼如下:

 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
var
  LFilePos: Integer;
begin
  with ClientDataSet1.FieldDefs do
  begin
    Add('File', ftWideString, 200);
    Add('CreationTime', ftDateTime);
  end;
  ClientDataSet1.CreateDataSet;
  ClientDataSet1.LogChanges := False;
  ClientDataSet1.IndexFieldNames := 'CreationTime;File';
  for LFilePos := 0 to ListBox1.Items.Count-1 do
  begin
    ClientDataSet1.Append;
    ClientDataSet1.Fields[0].Value := ListBox1.Items[LFilePos];
    ClientDataSet1.Fields[1].Value := ListBox2.Items[LFilePos];
    ClientDataSet1.Post;
  end;
  ClientDataSet1.First;
  while not ClientDataSet1.Eof do
  begin
    ListBox3.Items.Add(Format('%s, %s', [ClientDataSet1.Fields[0].AsString, ClientDataSet1.Fields[1].AsString]));
    ClientDataSet1.Next;
  end;
end;


使用 TFdMemTable 請注意

TFdMemTable 預設不會記錄資料歷程 (TFdMemTable.CachedUpdates default value is False),故 LogChanges 該行程式碼要刪除。


排序檔案清單

ClientDataSet1.IndexFieldNames 屬性設定好以時間欄位排序,即可得到理想的結果,如果要倒序,則可以參閱【【Delphi】ClientDataSet 的排序】,裡面有詳細的解說。


資料集太大?泛型物件也可以

如果認為資料集物件過於龐大,則可以考慮使用泛型物件,在這裡使用 Generics.Collections 單元裡的 TDictionary 物件,目的是它可以存放 Key=Value 字典,在這個場合可以存放兩種值,適合使用 TDictionary 物件。

搭配 TArray 處理排序功能便可以取代資料集的設計方式,完整程式碼如下:


結論時間

在 Windows 平台下找尋 Win32 API 是一件再正常不過的事情了,但進入到 64 位元或是行動裝置等平台後,以往的開發習慣也要跟著一起改變,Delphi 隨著科技的更新也一同進步,你不需要 Win64 API、Android API、MacOS API、iOS API 等全部自行實作一次,只需要在設計程式上跟著 Delphi 轉換開發思維,如前面的範例所述,不僅僅是 API 上的改變,框架的整合應用也是重要且必學的技術,本文提供了硬體存取和資料庫存取的綜合應用,期待你的發揮 ❤


和你分享 😉


See also


2021/09/07

Delphi 10.4 Community Edition (CE) 社群版初體驗

到 Youtube 上觀看 

你遲早都要用編譯器來編譯跨平台 APP,何不現在就用最好的! 

你開發 APP 所消耗的心力遠超過你的想像?

以往以 Web 平台為基礎的開發框架,到最後都必須使用 NodeJS 進行編譯,看著 NodeJS 的編譯效率和 WebPack 的龐大,這還不包含配置各家平台的開發工具,你花了多少心力在「部署」你的環境? 你遲早都要用編譯器來編譯跨平台 APP 專案,何不現在就用最好的! 

Delphi 社群版是入門最佳首選的理由?

Delphi 企業版是火力強大的全方位開發工具,而 Delphi 社群版則提供了另一條開發跨平台 APP 的道路,去無存菁地提供跨平台專屬的框架。除此之外,各家的開發工具都已在安裝程式安排妥當,僅需「下一步」就能在彈指之間完成跨平台 APP 大部份所需要的前置工具部署。 

給自己一個學習 Delphi 的理由

你花了多少時間在打基礎?你又花了多少時間「準備」進入業界?學習 Delphi,這會是你會發現這是從入門到入行最好的投資,給自己一個學習 Delphi 的理由,現在就試試 Delphi 社群版吧!

2021/09/03

Desktop First UX Summit 2021 桌面第一高峰會


2021 年的「桌面第一高峰會」研討會內容十分豐富,各家贊助商也來參與這難得齊聚的盛事。

其中 Marco Cantu 提到 RAD Studio (Delphi, C++ Builder) 11 預覽版本的內容,他認為有三個值得一提的亮點:

  1. 高解析度的工具介面

  2. 隨主題變化,所見即所得的設計介面

  3. 改良 VCL TRichEdit 元件

當然還有很多,例如 FMX 上的 TWebBrowser 在 Windows 平台上支援 WebView2 (edge) 版本等。

研討會訴求主軸從【升級】變為【推廣】。

感受到 EMBT 是下定決心要擺脫【老派】應用程式的惡名,未來提供教育版時,新人或學生的接受度也會提高,才不會有「現在都 202x 年了你還讓我開發 WinXP APP」的反抗心態。

重新雕塑 VCL 和 FMX 的選擇

VCL 重回主角身份,依舊是地表上最強的桌面開發框架無誤,FMX 持續發展,相較於 VCL 少了三方廠商的支援,FMX 不再訴求「取代 VCL」,反而比較適合作為 VCL 的延伸。

在應用程式開發會是以 Windows 為核心,Mac 和行動裝置為延伸。對我們已有的產品來說,原有的產品提高了全功能的重要性,跨平台會以重新檢視和篩選出必要的功能實現為主軸,旨在提供使用者便利性和即時性,加強提供全方位的服務為其核心價值。

總結

小孩才做選擇,我全都要!

VCL 你要學,FMX 也是必學的功課,你的薪資也莫名地變薄了呢 (誤),是說能給服務的對象更有價值的內容,才是我們作產品最大的樂趣,你說是吧!

SEE ALSO

2021/08/25

從Excel操作問題來看工程師的通靈技能


前陣子在 Delphi.KTOP 看到一篇「請問 Excel AddSmartArt 第一個參數該怎麼設定」。

覺得操作 SmartArt 這點很有意思,這兩天才有時間認真看了這篇文章,一開始以為使用 Excel 的「錄製巨集」就可以搞定,但直覺認為回覆此內容時會得到「我早就已經知道」的結果。

因此決定一試 Excel 巨集功能,取得的 VBA 內容是:

    Call ActiveSheet.Shapes.AddSmartArt(Application.SmartArtLayouts( _
        "urn:microsoft.com/office/officeart/2005/8/layout/cycle1")).Select

就這麼短短一行,轉到 Delphi 上會是以下內容:

procedure TForm2.Button3Click(Sender: TObject);
var
  ExcelApp,
  ActiveSheet,
  oSALayout: Variant;
begin
  try
    ExcelApp := CreateOleObject('Excel.Application');
  except
    ShowMessage('建立EXCEL錯誤');
  end;

  ExcelApp.Visible := True;
  ExcelApp.WorkBooks.Add;
  ActiveSheet := ExcelApp.ActiveSheet;
  oSALayout := ExcelApp.SmartArtLayouts('urn:microsoft.com/office/officeart/2005/8/layout/hierarchy2');
  ActiveSheet.Shapes.AddSmartArt(oSALayout, 50, 50, 200, 200);
end;

卻得到以下結果:

看來樓主的問題內容並沒有說明完整,就來看一下 Excel Developer Docs 怎麼描述 SmartArtLayouts:


文件也就這麼一點點,沒了。 再透過 Google 搜尋,沒有更多的資料,Delphi 似乎沒有人這樣做,【找不到成員】這問題難道就無解了嗎?

Google 沒有沒關係,Excel_TLB 單元來解答

利用 Excel 執行檔來製作最適合它的 Delphi 元件看來是最終解法,果不其然,答案在這裡:
ExcelApplication 確實有 SmartArtLayouts 成員,接下來就是直接操作它,程式碼直接公開:
VBA 對 OLE 成員非常自由,Item 到底是什麼東西?經查詢的結果是:

微軟連自家軟體的工程師手冊都寫得如此破碎和簡短,也難怪這方面的開發資訊幾近沒有。

原本還要多寫些關於 SmartArt 賦值的內容,無奈再往下追盡是 Access Violation,使用 Excel_TLB 時必定會發生,Stack Overflow 這篇【Delphi - How to create Excel PivotChart】也遇到一模一樣的問題,雖無法找到原因,但改以 OLE 重新刻一次後卻可以解決問題。

2021/08/10

Delphi 7 Web 開發實戰 Part5 - 前後端取得資料 API



除了補充書本裡對行動裝置的內容外,也是圓了當時自己的一個未盡的理想。

在登入畫面完成後,資料表格顯得超簡單 😉 這篇至少遲到 10 年的教學內容,向你呈現! 😘 【Delphi in Depth DataSnap 網站應用程式全端開發】 PUBU - https://www.pubu.com.tw/ebook/201543 Google Play - https://play.google.com/store/books/d...

2021/08/09

JetBrains 與 StackOverflow 在 2021 年針對全球三萬多名程式開發者的調查

JetBrains 與 StackOverflow 在 2021 年針對全球三萬多名程式開發者的調查。

Delphi 只佔了其中 1%,換算下來也就 300 多人,就計畫遷移數為 0 的狀況下,我猜這 1% 全部都是超過 30 歲的開發者。
而這 1% 裡面中還可以細分:

  • Desktop = 83%
  • Mobile = 33%
  • Web (Back-end) = 61%
  • Web (Front-end) = 52%

Desktop 使用數最多,表示 Delphi 主力就是在商業開發,Mobile 在這部份僅為加分項目而非主要終端。

Web 比我想像中的還要多,甚至比 Mobile 還要多。

Back-end 和 Front-end 佔比接近,這表示使用 Delphi 若參與 Web,大部份會採用 UniGUI、IntraWeb、DelphiMVCFramework 等中繼開發框架,後端偏多則可能是 DataSnap 也來參了一腳 😁

(你以為我要說 RAD Server 嗎?我偏不! 😛)

EMBT 在 2012 年開始將重心轉往 Mobile 至今,看得出來 Delphi 開發者依舊未大幅度的往行動裝置遷移,在跨平台的選擇上,Web 還是比 Mobile 多,表示應用程式並不會用到太多 Mobile 硬體周邊,也就能合理猜測 Delphi 主要用在商業應用上。然而 EMBT 已經挹注了大量的資源在 FMX 上,還有辦法再多開戰線到 Web 上嗎?

當然,這 300 人不能代表全球還在使用 Delphi 的開發者,至少我就沒被代表到 😆,但也可能是全球開發者的縮影,可以作為之後移轉到其它平台的參考。

和你分享 ❤

SEE ALSO

Delphi Data in JetBrains Developers Survey

2021/08/05

Delphi 7 Web 開發實戰 Part4 - EasyUI 切換頁面

EasyUI Mobile 在設計上屬於一頁式網頁開發,但它卻可以利用 DOM ID 切換的方式來滿足頁面切換的效果,來看看程式上要如何實作 😉

這篇至少遲到 10 年的教學內容,向你呈現! 😘 

【Delphi in Depth DataSnap 網站應用程式全端開發】
PUBU -
https://www.pubu.com.tw/ebook/201543 

Youtube - https://youtube.com/playlist?list=PLJshRqG16Wq5uC2ZJboLOYN6XhWuPXLRB

Facebook - https://www.facebook.com/DaJiang.fans

2021/08/03

Delphi 7 Web 開發實戰 Part3 - 登入API

行動裝置瀏覽器都已經支援 HTML5 標準,所以在前端設計上就能使用近代支援的標準 API,來看看如何快速地做完前後端 API 設計。 

這篇至少遲到 10 年的教學內容,向你呈現! 😘

TMS Web Core 試用心得速記


架構

嚴格來說是 Delphi 的 Web 外掛工具,屬於三層架構裡的客戶端,採用兩種模式:

TMS product bundles

近似 Delphi TDataSet 架構,但有自己的 TConnection (TWebClientConnection)、TClientDataSet (TWebClientDataSet) 和 TDataSource (TWebDataSource)。


TMS product plugin

透過 TMS 自訂的規則把三方 JS 元件,如 TWebJQXGrid 等實作為 Delphi 元件。


它們共同的特色是以客戶端的實作,而非伺服器端,有關伺服器的內容要自行製作,對後端的依賴不再是 Delphi,而是各種 REST JSON API 來源,如 ASP.NET、Python、NodeJS 等。

至於 FireDAC 等 TDataSet 元件則無法使用,TWC 完全和 Delphi 資料庫元件脫勾。

連線基礎

TMS Web Core (TWC) 元件在手冊上以存取 JSON API 為主,XML 倒是沒看到,若是使用 TWebJQXGrid 等三方 JS 元件則還需查來源手冊才會知道。

但 TWebJQXGrid 範例在資料的存取上用的是 CSV,頓時有讓我歪腰到 😆


優點

  • 純客戶端呈現,操作效能會比 IntraWeb、UniGUI 等框架還要好。
  • Delphi IDE 的可視化編輯器在編排應用程式 UI 上很好用。
  • 可以把各種三方 JS 元件以 TWC 重新實作在 Delphi 上使用,不再受限於 TWC 元件數量。(這點超讚!)


缺點

  • 習慣 Delphi 資料庫元件的開發者需要轉換開發思維;可能要把 TWC 視為 Indy 之類的網路程式開發會比較容易上手。
  • 若用到三方 JS 元件,仍需要學習相關 JS 知識,多了一道轉換工法。


總結

對依賴 Delphi 的開發者來說,投資 JS 的目的只是為了方便開發 Web 前期的磨刀工作,在進入開發期時,則可以完全發揮 Delphi 的優勢,至於 PascalToJS 的繁瑣工作? TWC 早就為你搞定這一切!


和你分享 😉


See also