在學習 React 時,最快樂的莫過遇到淺白易吸收的好文章。
Joe Morgan 所寫的【How To Add Login Authentication to React Applications】就是一例。
參考它的流程,讓我想到可以利用它來設計一款問卷調整系統,App 架構和路由經整理後如下圖所示:
在學習 React 時,最快樂的莫過遇到淺白易吸收的好文章。
Joe Morgan 所寫的【How To Add Login Authentication to React Applications】就是一例。
參考它的流程,讓我想到可以利用它來設計一款問卷調整系統,App 架構和路由經整理後如下圖所示:
Router v6 的改版所變動之處非常大,許多舊語法、屬性被【移除】,可以說是新產品也不為過,雖然作者有提出未來可能會提供向下相容的語法,但目前為止仍舊沒有出現,我想再回去的機率很低,還是接受現實改為新版語法吧!
withReouter 已移除,改用 Router Hooks 取代,這意味著以前使用 class 所開發的元件將無法繼續使用,改為函式元件似乎是條不得不為的道路。
<Route /> path 移除多路徑陣列寫法
<Route path=["/list", "/list/:id"] /> 寫法在 v6 已不可用,作者說 Router v6 引擎有針對多路由做最佳化,使用多次宣告 Route 很直覺,效能更好。
exact 屬性移除,前面提到作者有對 Router v6 引擎最佳化,故不再需要 exact 絕對路徑屬性。
Route 的 Component、Render 屬性移除,改以 element 取代,傳參數的方式改為 useParams 等 Hooks 後就不再需要以前的 Component、Render 屬性,寫法可以一致化。
activeClassName、activeStyle 被移除,改以 Style 屬性取代,這裡官方手冊有個地方範例程式碼寫錯:
而在說明整合的文件中有寫出正確的內容:
範例使用 className 說明,這裡也提到 style 寫法相同,但就僅止於此,一不小心會花很多時間在找問題上。
還是可以寫得出範列所要傳達的效果,請放心。
Router v6 改動的地方非常多,現在書和網路上許多資料還停留在 v4、v5 上,在使用 v6 時常會用到已移除的功能,寫起來倍感艱辛。
改版的過程中會發現其實 v6 改動方向是把路由工作劃分得很清楚,Route 歸 Route;Link 歸 Link,Element 歸 Element,寫作上比較不會有串來串去的情形。
整體來說 v6 的改動是利大於弊,只要你能熬過那升級陣痛期的話。
和你分享 😉
【Webpack 5 初探筆記】這篇記錄了 Webpack 三項重點:
你以為已經很多了?不,還有線上除錯服務器 (Webpack devServer) 還沒用到,該篇文章僅就編譯功能處理,編譯完成後再到實機裡除錯。
所以本篇就來研究如何讓 Webpack devServer 和後端 WebAPI 結合,本篇文章會講到兩個重點:
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 所產生的問題,有興趣的開發者邀請你購買起來看,保證物超所值!
來看看 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! 😉
我在【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//babel.config.json
npm install --save @babel/runtime
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 區,有興趣的朋友歡迎來看看。
😉
在寫完【還在 React 從入門到放棄?告訴你選擇比努力還重要的事】這篇文章之後,原本要繼續我的 React 裸奔之旅,但看到【【前端速成】Webpack5 快速入門|Tiktok工程師帶你入門前端|布魯斯前端】的直播內容,發現 Webpack 和 Delphi 的專案檔實在太像了,手癢忍不住動手把自己的 jQuery 網站利用 Webpack 重新打包順道重構,過程很有趣,而且整理後的專案內容就像是看網站地圖一樣,看了心情大好。
趁還有記憶時把 Webpack 的參數檔和 NPM package 參數檔透過這偏文章寫下來。
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 安裝的外掛工具及版本,功能有:
scripts:在命令提示字元視窗下執行 webpack 的指令,語法為【npm run [scripts]】,SET 是設定 Windows 的環境變數,可以依照【設計】或【產品】語法來改變 webpack 封裝設定。[&&] 是 Windows 執行多指令的節點,某些靜態檔案還可以透過批次檔進行複製。
npm 在對專案處理時都會以 package.json 作為進入點,比較要記的就是這兩個屬性。
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 外掛設定區。
entry 裡,要寫上所有需要編譯的 js 檔案,格式為【暱稱:路徑】,暱稱在之後的【plugins 》 HtmlWebpackPlugin】會用到。
output 裡,path 說明 entry 裡的檔案在編譯後所放置的路徑,以 package.json 所在路徑為 root,可以自由指定,filename 為了確保客戶端每次都會重新讀取,可以把檔案加入【hash】參數,編譯會將 hash 碼帶入檔名,如:maindesk.f22745d9520340e7af08.bundle.js。
plugin 裡面主要放 webpack 外掛模組,有:
CleanWebpackPlugin 是每次編譯當下都會將目的目錄內的檔案全數清空。
webpack.DefinePlugin 是定義編譯環境的全域變數,此例可以在網路中的任何 JS 寫上類似以下的內容:
可以在編譯時決定要執行哪部份的程式內容,好用!HtmlWebpackPlugin 決定讀取的樣板和 HTML 檔案路徑,chunks 是決定要寫入 entry 的哪些檔案,以暱稱設定,設為空字串時則不匯入任何 js 檔;也可以使用 excludeChunks 決定不加入的 js 清單。
學習 Webpack 的過程中不斷帶入以前寫 Delphi 專案檔的時光,所以還蠻能理解 Webpack 的各項設定的內容,都已經用編譯了,那就順便把 Delphi 的設定概念套入,Webpack 還有很多細節的設定,只要大方向有了,再往細節裡找的難度就會下降很多!
今日成就:把 jQuery 網站套入 Webpack 專案編譯成功!
和你分享 😉
NPM 編譯經常無預期出錯,學習 React 免不了會經過一連串試錯的過程,但往往會不經意的出現許多編譯錯誤,大多是 Webpack 出現問題,在無法排除錯誤的情形下,最常做的就是重建 node_modules 內容,次數一多,是在學習 React 還是背誦 NPM i 指令,我已經搞不清楚了。
只要不是靜態網頁,在前端的開發過程一定會和後端 API 傳遞訊息,尤其是開發網頁應用程式更是必須。在設計時期,只要前端和後端的 Web 服務網域不同,就一定會遇上【跨來源資源共用(Cross-Origin Resource Sharing (CORS))】這個大魔王,後端就必須進行 CORS 設定,但實際上線又會是在同一網域,只有開發時期才會遇到的 CORS 顯得是非常詭異的歷程。
已知 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> |
成果圖
不知道你是否和我一樣,斷斷續續學幾年的 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 慢。
官方的教學在此之後就是利用 NodeJS 把 babel 安裝到現有的 WebPack 專案中,再編譯 JSX 內容成為標準 JS 內容,最後才把編譯後的成品上傳到網站執行。
等等,這樣就不是逐步採用 React 了,要加入 WebPack 就等於是大改現有網站啦!
原有網站在沒有 WebPack 加持下,要逐步採用 React 勢必不能使用 JSX,因使用 Babel 動態轉換 JSX 的效率不佳。
結論就是:逐步採用 React 下又不要使用 NodeJS 編譯網站,React.createElement 你只能用它了。 😁
逐步採用 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 裡,範例程式碼如下:
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 小部件的範例。
和你分享 😉
作者:吳祐賓
底下的程式內容到了 XE 後已經被 TEncoding 物件所取代,但這問題時常被問到,之前主流是 Base64,當時還可以用 EncdDecd 單元處理 Unicdoe 編碼,現在已逐漸改以 JSON 格式為主流,EncdDecd 單元已無法滿足眼下的需求了。
WideString 是 Delphi 7 相容 Unicode 的字串類型,然而 WideString 的存在是為了當時的 Win32 API 而存在的,它並不適合作為現代化的資料傳輸格式。
要達到良好的相容性,使用 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 社群版來進行專案提升,省下你開發的時間,專注地在商業邏輯的開發會為你帶來更好的效益,畢竟公司開來目的是為了賺錢,而非程式碼研究,研究的事我來就行。😁
和你分享 😉
Delphi in Depth DataSnap 網站應用程式全端開發 出版
最近同事詢問到 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
物件可以取得以下時間
以「檔案建立時間」為例,簡單程式碼如下:
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
內建的快取資料集有兩個:
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.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 上的改變,框架的整合應用也是重要且必學的技術,本文提供了硬體存取和資料庫存取的綜合應用,期待你的發揮 ❤
和你分享 😉
以往以 Web 平台為基礎的開發框架,到最後都必須使用 NodeJS 進行編譯,看著 NodeJS 的編譯效率和 WebPack 的龐大,這還不包含配置各家平台的開發工具,你花了多少心力在「部署」你的環境? 你遲早都要用編譯器來編譯跨平台 APP 專案,何不現在就用最好的!
2021 年的「桌面第一高峰會」研討會內容十分豐富,各家贊助商也來參與這難得齊聚的盛事。
其中 Marco Cantu 提到 RAD Studio (Delphi, C++ Builder) 11 預覽版本的內容,他認為有三個值得一提的亮點:
當然還有很多,例如 FMX 上的 TWebBrowser 在 Windows 平台上支援 WebView2 (edge) 版本等。
感受到 EMBT 是下定決心要擺脫【老派】應用程式的惡名,未來提供教育版時,新人或學生的接受度也會提高,才不會有「現在都 202x 年了你還讓我開發 WinXP APP」的反抗心態。
VCL 重回主角身份,依舊是地表上最強的桌面開發框架無誤,FMX 持續發展,相較於 VCL 少了三方廠商的支援,FMX 不再訴求「取代 VCL」,反而比較適合作為 VCL 的延伸。
在應用程式開發會是以 Windows 為核心,Mac 和行動裝置為延伸。對我們已有的產品來說,原有的產品提高了全功能的重要性,跨平台會以重新檢視和篩選出必要的功能實現為主軸,旨在提供使用者便利性和即時性,加強提供全方位的服務為其核心價值。
小孩才做選擇,我全都要!
VCL 你要學,FMX 也是必學的功課,你的薪資也莫名地變薄了呢 (誤),是說能給服務的對象更有價值的內容,才是我們作產品最大的樂趣,你說是吧!
覺得操作 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 似乎沒有人這樣做,【找不到成員】這問題難道就無解了嗎?
微軟連自家軟體的工程師手冊都寫得如此破碎和簡短,也難怪這方面的開發資訊幾近沒有。
原本還要多寫些關於 SmartArt 賦值的內容,無奈再往下追盡是 Access Violation,使用 Excel_TLB 時必定會發生,Stack Overflow 這篇【Delphi - How to create Excel PivotChart】也遇到一模一樣的問題,雖無法找到原因,但改以 OLE 重新刻一次後卻可以解決問題。
Delphi 只佔了其中 1%,換算下來也就 300 多人,就計畫遷移數為 0 的狀況下,我猜這 1% 全部都是超過 30 歲的開發者。
而這 1% 裡面中還可以細分:
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 的開發者,至少我就沒被代表到 😆,但也可能是全球開發者的縮影,可以作為之後移轉到其它平台的參考。
和你分享 ❤
嚴格來說是 Delphi 的 Web 外掛工具,屬於三層架構裡的客戶端,採用兩種模式:
近似 Delphi TDataSet 架構,但有自己的 TConnection (TWebClientConnection)、TClientDataSet (TWebClientDataSet) 和 TDataSource (TWebDataSource)。
透過 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,頓時有讓我歪腰到 😆
對依賴 Delphi 的開發者來說,投資 JS 的目的只是為了方便開發 Web 前期的磨刀工作,在進入開發期時,則可以完全發揮 Delphi 的優勢,至於 PascalToJS 的繁瑣工作? TWC 早就為你搞定這一切!
和你分享 😉
試用 Devart dbExpress driver 時,每當程式一執行便會跳出【此為試用版】的警告訊息,導致在測試上還必須多一個按按鈕的動作。
為此,Devart 提供了一個簡單的作法,只要註冊機碼【HKEY_LOCAL_MACHINE\SOFTWARE\Devart\dbExpress\SQL Server】裡,增加【ShowTrialMessage】的 Dword,內容設為 0,便可以在執行時期暫時關閉顯示試用版訊息視窗,其它產品也可如法泡製。
當然,試用 30 天到期時一樣會跳出【已過試用期】的錯誤。😘
和你分享 ❤
最近在翻一些古早實作書籍,看到幾個別開生面的 HTML 屬性:BorderColorLight, BorderColorDark,使用 Chrome、FireFox 等支援 HTML5 的瀏覧器來開時會是這樣:
當時只覺得為什麼都是黑白線條,現在該是回來解謎的時候!😉
圖片中的上半部使用到 BorderColorLight, BorderColorDark 屬性,經查才知道是 IE 限定,之後在 IE 裡開啟效果如下:
效果還蠻好看的,原來表格也可以上色彩。
IE 真的好,用 CSS 才驚覺這 BorderColorLight 和 BorderColorDark 是神馬黑技術!
圖片來源:linuxtopia |
Linuxtopia 的圖片真的太到位,連光源都送你了,Light 是指被太陽照到的地方,圖片以淺藍色表示;Dark 則是太陽照不到的地方,圖片以深藍色表示。
理解意思後,就要找 CSS 有沒有可以解的解方。
答案是有的,只是和 IE 相比在 CSS 上並不容易呈現,必須以上下左右獨立設置,回頭看看 IE,簡單明瞭!
<table border="2" cellspacing="2" cellpadding="2" width="99%" bordercolordark="#99cc66" bordercolorlight="#ffcccc"> <caption>Eden IE Table Test</caption> <tr> <td>IE Column 1</td> <td>IE Column 2</td> <td>IE Column 3</td> </tr> </table>
在修改前還要說明下,cellspacing 和 cellpadding 這兩個 HTML 屬性又是 IE 限定,這下又多了一道關卡:
圖片來源:維基百科 |
簡單的說就是填上一堆空白或留白,好在 CSS 這裡相對容易理解,就是多了一道標籤手工活。
<style> .cssIETable { border-top: 2px solid #ffcccc; border-left: 2px solid #ffcccc; border-right: 2px solid #99cc66; border-bottom: 2px solid #99cc66; border-collapse: separate; /*cellspacing */ border-spacing: 2px; } .cssIETable TD { padding: 2; /*cellpadding*/ } </style> <body> <table class="cssIETable" border="2" width="99%"> <caption>Eden CSS Table Test</caption> <tr> <td>CSS Column 1</td> <td>CSS Column 2</td> <td>CSS Column 3</td> </tr> </table> </body>
嗯,那復古風味感終於又回來了!😎