【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"])) |
網頁在啟動後會出現錯誤:
【
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//babel.config.json
npm install --save @babel/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 區,有興趣的朋友歡迎來看看。
😉