2025/03/14

程式碼轉換不再難:AI 助你精通 FastReport FastScript

 作者:吳祐賓

 

 


 

 

 

FastScript 是 FastReport VCL / FMX 超棒的產品之一,FastScript 功能非常豐富,七成需求都可以依賴它來完成,不一定要使用肥大的開發工具。

 

FastScript 限制

 

FastScript 也有其限制,例如它不支援以下兩種東西:

  • Class 建立。無法使用 record, class 等 object 來建立自己的物件
  • Set 集合。無法使用 [fsBold, ...]來設定集合

 

使用 AI,在 FastScript 上寫出一手好程式


還好現在有 AI 工具,把提示語給 AI 後,就可以快速寫出正確又有效率的工具出來,真是太棒了!

 

我使用提示語如下,提供給各位讀者參考:


使用 FastReport FastScript 的 JScript 語法建立一個腳本:[
  將 "Hello, World!" 顯示在 Memo1 元件中。(可以代換為你的需求)
]
Refer to the following FastScript JScript syntax rules:[
===
Classes
You cannot define a class inside the script, but you can use the external classes defined in add-in modules or your application.
===
Functions
There is a rich set of standard functions which can be used in a
script. To get an access to these functions, pass the fsGlobalUnit
reference to the TfsScript.Parent property.
===
About FastReport's FastScript:JScript syntax:
Program -> Statements
Statements -> Statement...
Block -> '{' Statements '}'
ImportStmt -> IMPORT (String/,)...
VarStmt -> VAR (VarDecl/',')...
VarDecl -> Ident [Array] [InitValue]Array -> '[' (ArrayDim/',')... ']'
ArrayDim -> Expression
InitValue -> '=' Expression
Expression -> SimpleExpression [RelOp SimpleExpression]...
SimpleExpression -> ['-'] Term [AddOp Term]...
Term -> Factor [MulOp Factor]...
Factor -> Designator
-> UnsignedNumber
-> String
-> '(' Expression ')'
-> '!' Factor
-> '[' SetConstructor ']'
-> NewOperator
-> '<' FRString '>'
SetConstructor -> SetNode/','...
SetNode -> Expression ['..' Expression]
NewOperator -> NEW Designator
RelOp -> '>'
-> '<'
-> '<='
-> '>='
-> '!='
-> '=='
-> IN
-> IS
AddOp -> '+'
-> '-'
-> '||'
-> '^'
MulOp -> '*'
-> '/'
-> '%'
-> '&&'
-> '<<'
-> '>>'
Designator -> ['&'] Ident ['.' Ident | '[' ExprList ']' | '(' [ExprList] ')']...
ExprList -> Expression/','...
Statement -> (AssignStmt | CallStmt | BreakStmt | ContinueStmt |
DeleteStmt | DoWhileStmt | ForStmt | FunctionStmt |
IfStmt | ImportStmt | ReturnStmt | SwitchStmt |
VarStmt | WhileStmt | WithStmt | Block) [';']
BreakStmt -> BREAK
ContinueStmt -> CONTINUE
DeleteStmt -> DELETE Designator
AssignStmt -> Designator ['+'|'-'|'*'|'/']'=' Expression
CallStmt -> Designator ['+''+'|'-''-']
ReturnStmt -> RETURN [Expression]
IfStmt -> IF '(' Expression ')' Statement [ELSE Statement]
SwitchStmt -> SWITCH '(' Expression ')' '{' (CaseSelector)... [DEFAULT ':' Statement] '}'
CaseSelector -> CASE SetConstructor ':' Statement
DoWhileStmt -> DO Statement [';'] WHILE '(' Expression ')' ';'
WhileStmt -> WHILE '(' Expression ')' Statement
ForStmt -> FOR '(' ForStmtItem ';' Expression ';' ForStmtItem ')' Statement
ForStmtItem -> AssignStmt-> CallStmt-> VarStmt-> Empty
TryStmt -> TRY CompoundStmt (FINALLY | EXCEPT) CompoundStmt
FunctionStmt -> FunctionHeading Block
FunctionHeading -> FUNCTION Ident FormalParameters
FormalParameters -> '(' [FormalParam/','...] ')'
FormalParam -> ['&'] Ident
WithStmt -> WITH '(' Designator ')' Statement
]
Specific usage of FastScript JScript: [
#language JScript // this is optional
import "unit1.js", "unit2.js"
Report.Memo1.Text = "HelloWorld"; // Sets the text of Memo1 through the Report object.
Memo1.Text = "HelloWorld";// Equivalent if Memo1 is in the main report scope (not within a sub-component like a Group or Band).
var mbSet = mbYes + mbNo;  // FastScript does not support sets directly; use addition for combining flags.
if (MessageDlg("Welcome to my JScript application. Exit now?", mtConfirmation, mbSet, 0) == mrYes)
{ ShowMessage("OK");}
// import section - must be before any other sections
var i, j = 0; // var section
function p1() // procedures and function
{//
}
// main procedure that will be executed.
p1();
// Dynamic array create. (var myarray = [] not support. Need use the FR TfrxArray Component)
var myarray = new TfrxArray();
myarray[0] = 1;
myarray[1] = 2;
for (i = 0; i < 1; i++) j = i + 1; myarray[i] = myarray[i] + j;
ShowMessage(IntToStr(myarray.Count)); // TfrxArray is not JScript array type.
// You can use the TStrings/TStringList. Same the Delphi TStrings/TStringList.
var myList = new TStringList();
myList.Add("message=HelloWorld");
ShowMessage(myList.Values("message"));
]
The following FastScript built-in functions are available:
function IntToStr(i: Integer): Stringfunction FloatToStr(e: Extended): Stringfunction DateToStr(e: Extended): Stringfunction TimeToStr(e: Extended): Stringfunction DateTimeToStr(e: Extended): Stringfunction VarToStr(v: Variant): Stringfunction StrToInt(s: String): Integerfunction StrToFloat(s: String): Extendedfunction StrToDate(s: String): Extendedfunction StrToTime(s: String): Extendedfunction StrToDateTime(s: String): Extendedfunction Format(Fmt: String; Args: array): Stringfunction FormatFloat(Fmt: String; Value: Extended): Stringfunction FormatDateTime(Fmt: String; DateTime: TDateTime): Stringfunction FormatMaskText(EditMask: string; Value: string): stringfunction EncodeDate(Year, Month, Day: Word): TDateTimeprocedure DecodeDate(Date: TDateTime; var Year, Month, Day: Word)function EncodeTime(Hour, Min, Sec, MSec: Word): TDateTimeprocedure DecodeTime(Time: TDateTime; var Hour, Min, Sec, MSec: Word)function Date: TDateTimefunction Time: TDateTimefunction Now: TDateTimefunction DayOfWeek(aDate: DateTime): Integerfunction IsLeapYear(Year: Word): Booleanfunction DaysInMonth(nYear, nMonth: Integer): Integerfunction Length(s: String): Integerfunction Copy(s: String; from, count: Integer): Stringfunction Pos(substr, s: String): Integerprocedure Delete(var s: String; from, count: Integer): Stringprocedure Insert(s: String; var s2: String; pos: Integer): Stringfunction Uppercase(s: String): Stringfunction Lowercase(s: String): Stringfunction Trim(s: String): Stringfunction NameCase(s: String): Stringfunction CompareText(s, s1: String): Integerfunction Chr(i: Integer): Charfunction Ord(ch: Char): Integerprocedure SetLength(var S: String; L: Integer)function Round(e: Extended): Integerfunction Trunc(e: Extended): Integerfunction Int(e: Extended): Integerfunction Frac(X: Extended): Extendedfunction Sqrt(e: Extended): Extendedfunction Abs(e: Extended): Extendedfunction Sin(e: Extended): Extendedfunction Cos(e: Extended): Extendedfunction ArcTan(X: Extended): Extendedfunction Tan(X: Extended): Extendedfunction Exp(X: Extended): Extendedfunction Ln(X: Extended): Extendedfunction Pi: Extendedprocedure Inc(var i: Integer; incr: Integer = 1)procedure Dec(var i: Integer; decr: Integer = 1)procedure RaiseException(Param: String)procedure ShowMessage(Msg: Variant)procedure Randomizefunction Random: Extendedfunction ValidInt(cInt: String): Booleanfunction ValidFloat(cFlt: String): Booleanfunction ValidDate(cDate: String): Booleanfunction CreateOleObject(ClassName: String): Variantfunction VarArrayCreate(Bounds: Array; Typ: Integer): Variant


前面的提示詞也可以使用:

 

將以下 C# 程式碼片段轉換為使用 JScript 語法的 FastReport FastScript。 該腳本應在 FastReport 環境中實現等效的功能。 請密切注意所提供的 FastScript JScript 語法規則和可用的內建函數。


總結

有了AI後,程式轉換上變得十分容易,從以前要求 Google 關鍵字力,現在要漸漸改為 AI 提示詞力。

 

如果有什麼想回饋的內容,歡迎在底下留言讓我知道。

2025/03/07

C++ Builder記憶體管理:使用unique_ptr實現物件自動釋放

作者:吳祐賓

 

 

據說,未來是個沒有 Delete 的世界(大誤)



 

C++ Builder開發者:硬核技術與直覺UI的雙重追求

 

在說明物件釋放方法之前,還得先聊聊 C++ Builder 開發者的習慣

 

就目前所接觸到會使用 C++ Builder 的開發者所整理的經驗,他們使用 C++ Builder 的理由大致上是以下兩點:


  1. 本身具有極高的 C 語言造詣,通常具有硬體開發經驗
  2. 和 Visual Studio C++ 相比,C++ Builder 的 UI 更是直覺的建立


 

由 1 可知,會使用 C 語言的開發者,通常有很強烈的語言潔癖,以及有自己一套對記憶體控制的要求

 

由 2 可知,C++ Builder 對從 C 過來的開發者來說,是很棒的 UI 建模工具



C++ Builder (CB) 中的物件釋放:常見問題與解決方案探討

 
只是,由於 CB 的 WinForm 是建構在 VCL framework 之上,所以學習 C++ 的物件自然是必須要的。在擴充C++的基本知識後,底下便是常見的寫法:

2025/03/06

node.js 的字串拼接函式比較

 作者:吳祐賓

 

 

 


 

 

最近在寫 Express.js API 時,被 AI 提示說直接字串拼接較不安全,建議使用指令處理。

 

但採用後原來的程式反而無法執行。檢查後才發現原來組合出來的路徑和我想的不一樣。

 

於是就做了一點測試比較:

 

 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
// node.js 的字串拼接函式比較:

// 1. 字串直接拼接
console.log('/A/B/C' + '/fetch_emp');
// 輸出 (所有系統, 但 Windows 上可能不正確): /A/B/C/fetch_emp
// 解釋:  直接使用 + 運算子。在 *nix (Linux/macOS) 上沒問題,但在 Windows 上可能因路徑分隔符 (\) 而出錯。不推薦。

// 2. path.join() - 跨平台安全 (推薦!)
console.log(path.join('/A/B/C', '/fetch_emp'));
// 輸出 (Windows): \A\B\C\fetch_emp
// 輸出 (Linux/macOS): /A/B/C/fetch_emp  (與上面相同,但更可靠)
// 解釋:  path.join() 根據 *當前作業系統* 自動選擇正確的分隔符。這是最安全、可靠的方法,確保跨平台兼容。

// 3. path.posix.join() - 單純傳入virtualDirPath
console.log(path.posix.join('/A/B/C'));
// 輸出 (所有系統): /A/B/C
// 解釋: 只傳入一個參數時,作用等同於 path.normalize(),用來規範化路徑。

// 4. path.posix.join() + ./  (當前目錄)
console.log(path.posix.join('/A/B/C', './'));
// 輸出 (所有系統): /A/B/C/
// 解釋:  path.posix.join() 強制使用 POSIX 分隔符 (/)。'./' 代表當前目錄,被絕對路徑 /A/B/C 吸收,最後加上/。

// 5. path.posix.join() + / (根目錄)
console.log(path.posix.join('/A/B/C', '/'));
// 輸出 (所有系統): /A/B/C/
// 解釋:  path.posix 使用 / 分隔符。'/' 代表根目錄。因 /A/B/C 已是完整路徑, 最終結果為/A/B/C加上/,並處理掉多餘的 /

// 6. path.posix.join() + /fetch_emp (強制 POSIX)
console.log(path.posix.join('/A/B/C', '/fetch_emp'));
// 輸出 (所有系統): /A/B/C/fetch_emp
// 解釋:  path.posix.join() 強制 / 分隔符。即使 '/fetch_emp' 開頭是 /,也會正確處理,避免雙斜線。

2025/02/21

React 19 use API 避坑指南:常見錯誤與解決方案 (含 RCC 說明)

作者:吳祐賓 

 

 


 

 

 

React 應用程式開發過程中,很常需要去 Server 端拉資料回來,通常我們會使用 useState, useEffect 來完成工作。

 

底下是一個簡單的範例,從 JSONPlaceholder (著名的公開偽資料 API 服務)提供的 API 取得使用者資料。

 

 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
import React, { useState, useEffect } from 'https://esm.sh/react@18'; // Changed to React 18 for compatibility with useEffect/useState
import ReactDOMClient from 'https://esm.sh/react-dom@18/client'; // Changed to React 18

const Users = () => {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null); // Added error handling

  useEffect(() => {
    const fetchUsers = async () => {
      const res = await fetch('https://jsonplaceholder.typicode.com/users');
      const data = await res.json();
      setUsers(data);
      setLoading(false);
    };

    fetchUsers();
  }, []); // Empty dependency array ensures this runs only once on mount

  if (loading) {
    return <h1>Loading...</h1>;
  }

  if (error) {
    return <h1>Error: {error.message}</h1>; // Display the error message
  }

  return (
    <ul>
      {users.map((user) => (
        <div key={user.id}>
          <h2>{user.name}</h2>
        </div>
      ))}
    </ul>
  );
};


function App() {
  return (
    // Suspense is not needed with useEffect + useState for initial data loading
    <Users />
  );
}

const root = ReactDOMClient.createRoot(document.getElementById('root'));
root.render(<App />);

 

 

useEffect + fetch 坊間有很多教學,因為很多人會遇到 "Warning"

 

這個設計法已經行之有年,還是有很多人搞不清楚 useEffect + fetch 為什麼會出現類似 "Warning: useEffect function must return a cleanup function or nothing. Promises..." 的訊息。

 

解決方式很多,React 19 也提出了一個解決方式。

 

 

 use API

 

use API 是 React 19 提出的新概念,是搭配 React Server Components 框架,如 Next.js 等設計使用。

 

use 的設計目標是:

 

  • 簡化 Server Components 中的資料取得設計
  • 更簡潔的程式碼和更高的可讀性
  • 錯誤處理更方便 (Error Boundaries 整合)

 

 

直接將 React 18 程式碼升級到 React 19,參考 How to fetch an API with REACT 19 | use - suspense 文章,程式碼如下:

 

 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
import React, { Suspense, use } from 'https://esm.sh/react@19';
import ReactDOMClient from 'https://esm.sh/react-dom@19/client';
const fetchUsers = async () => {
  const res = await 
  fetch('https://jsonplaceholder.typicode.com/users');
  return res.json();
};


const Users = () => {
  const users = use(fetchUsers());

  return (
    <ul>
      {users.map((user) => (
        <div key={user.id}>
          <h2>{user.name}</h2>
        </div>
      ))}
    </ul>
  );
};


function App() {
  return (
    <Suspense fallback={<h1>Loading...</h1>}>
      <Users />
    </Suspense>
  );
}

const root = ReactDOMClient.createRoot(document.getElementById('root'));
root.render(<App />);

 

 

程式碼就是如此簡單,但 React Client Components 時會無限 Promise

 

React Stand-alone 就是 RCC 模式,在這個模式下使用 use API,就會出現如下圖般無限 Promise 的情形。

 

因為 use 是搭配 RSC 的設計,所以和 render 會有關係,RSC 框架會管理好 use 裡 Promise 的狀態,但 RCC 沒有框架處理 Promise 狀態,流程上是:

 

User render -> use(new Promise) <in use> -> do Promise -> retrun New Promise <in use> -> "use" call User render... repeat 

 

 


 

 

 RCC 的 Promise 管理快取機制自己寫,就為了 use API

 

這部份和 useCallback 有點像,只有 callback 相依的變數修改了才會更新。Promise 也可以如法泡製。參考 New React 19 use hook–deep dive 的教學,寫了 useQuery 來模擬 Server 端對 Promise 快取機制的處理。

 

程式碼修改如下。

 

 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
import React, { Suspense, use, useState } from 'https://esm.sh/react@19';
import ReactDOMClient from 'https://esm.sh/react-dom@19/client';

/** useQuery Hook */
const promiseCache = new Map();

const useQuery = ({ fn, key }) => {
  if (!promiseCache.has(key)) {
    promiseCache.set(key, fn());
  }

  const promise = promiseCache.get(key);

  const result = use(promise);

  return result;
};
/* end useQuery Hook */

const fetchUsers = () => {
  const res = fetch('https://jsonplaceholder.typicode.com/users')
    .then( response => response.json());
  return res;
};

const Users = () => {
  const users = useQuery({fn: ()=> fetchUsers(), key: "fetchUsers"});

  return (
    <ul>
      {users.map((user) => (
        <div key={user.id}>
          <h2>{user.name}</h2>
        </div>
      ))}
    </ul>
  );
};


function App() {
  return (
    <Suspense fallback={<h1>Loading...</h1>}>
      <Users />
    </Suspense>
  );
}

const root = ReactDOMClient.createRoot(document.getElementById('root'));
root.render(<App />);

 

 

 

重點總結:


傳統方式 (React 18 及之前): 使用 useState 和 useEffect 組合來處理資料獲取、載入狀態和錯誤處理。  這是常見且有效的方法,但程式碼相對較多。



React 19 的 use API:


  • 設計目標: 簡化資料獲取,特別是在 Server Components (RSC) 環境下,提供更簡潔的程式碼和內建的錯誤處理。
  • RSC 環境: use hook 旨在與 React Server Components 框架 (如 Next.js) 搭配使用。 框架會負責管理 Promise 的狀態,避免無限迴圈。
  • RCC (React Client Components) 環境限制: 在獨立的 React Client Components (沒有 RSC 框架) 中直接使用 use 會導致無限迴圈,因為沒有機制來管理 Promise 的狀態。


模擬 RSC 的 Promise 快取 (RCC 環境):

 

  • 為了在 RCC 環境下也能體驗 use 的簡潔性,可以自行建立一個 Promise 快取機制 (例如範例中的 useQuery hook)。
  • useQuery hook 使用 Map 來儲存 Promise,確保相同的請求只會發送一次,避免重複的非同步操作。


 

總之,use API 是 React 19 中一個強大的新特性,但目前主要適用於 Server Components 環境。對於傳統的 Client Components,就繼續使用 useEffect 和 useState,對使用新技術有興趣的人,可以自行實作 Promise 快取機制來模擬 use 的行為。 

 

如果你正在使用 Next.js 這樣的 RSC 框架,use API 可以大幅簡化你的資料 request 的程式碼。

 

和你分享。

 

See Also

 

2025/02/19

React 19 + esm.sh CDN 新手入門:快速掌握 React 19 最新功能

作者:吳祐賓

 

 


 

 

前言


React 19 帶來了許多令人興奮的新功能,例如 Actions, Server Components, Asset Loading 等,可以幫助我們提升開發效率和應用程式效能。

 

本教學將以新手友善的方式,帶領大家快速入門 React 19,並搭配 esm.sh CDN 快速搭建開發環境,讓你立即體驗 React 最新功能!


附帶說明,本教學只會提及 Client-Side Rendering (CSR) 功能,如果這篇文章迴響不錯,會再另外推出 Server-Side Rendering (SSR) 的教學內容,敬請期待!


準備工作


要開始 React 19 開發,最快速的方式就是使用 esm.sh CDN。它讓我們無需安裝 Node.js 和 npm,就能在瀏覽器中直接使用 React 19。細節可以參閱我之前寫的:我在 React 19 新手入門:CDN + esm.sh 快速上手

 

首先,建立一個 HTML 檔案 (例如 index.html),並加入以下程式碼:

 

<!doctype html> <html> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <title>React App with JSX React 19</title> </head> <body class="dx-viewport"> <div id="root"></div> <script type="text/babel" data-type="module"> import React from "https://esm.sh/react@19" import ReactDOMClient from "https://esm.sh/react-dom@19/client" function App(){ return <> <h1>Hello, React 19 with ESM.sh</h1> </> } const root = ReactDOMClient.createRoot(document.getElementById('root')) root.render(<App />) </script> </body> </html>

 

這個簡單的 HTML 檔案就建立了一個 React 19 開發環境。使用 Babel stand-alone,用來啟動 JSX 即時編譯的能力。

 

使用 babel 優點是開發習慣移轉到 Build Tool 環境 (Vite, Webpack, Next.js 等) 十分方便,缺點則是所有 class 都必須寫在同一個 html 檔裡。

 

我還是習慣使用 CDN 做教學範例。現在,你可以直接用瀏覽器打開 index.html,看到 "Hello, React 19 with ESM.sh!" 的訊息,就代表成功了!

 

 

React 19 新功能巡禮

 

接下來,我們將逐一介紹 React 19 的重點新功能,讓你快速掌握 React 最新技術。

Actions:簡化資料流程與狀態管理


在 React 18 之前,處理表單提交或資料更新時,我們需要手動管理 loading 狀態和錯誤處理。

 

React 19 引入了 Actions (useTranstition),可以更簡潔地處理表單的非同步操作。

 

簡單的說,React 19 Actions 比較接近資料庫的交易概念,我們拿官方的 18/19 的程式碼來比較,並附上我修改後的程式碼。

 


 

 

<!doctype html> <html> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> <title>React App with JSX React 19</title> </head> <body class="dx-viewport"> <div id="root"></div> <script type="text/babel" data-type="module"> import React, {useState, useTransition} from "https://esm.sh/react@19" import ReactDOMClient from "https://esm.sh/react-dom@19/client" // Using pending state from Actions function UpdateName({}) { const [name, setName] = useState(""); const [error, setError] = useState(null); const [isPending, startTransition] = useTransition(); async function updateName(name) { // 模擬異步操作,例如 API 呼叫 return new Promise((resolve) => { setTimeout(() => { if (!name) { resolve("Name cannot be empty."); // 模擬錯誤情況 } else if (name.length > 20) { resolve("Name is too long."); // 模擬另一個錯誤情況 } else { // 在此處添加更新名稱的實際邏輯,例如 API 呼叫 console.log("Name updated to:", name); // 模擬成功情況 resolve(null); // 成功時返回 null 表示沒有錯誤 } }, 1000); // 模擬 1 秒的延遲 }); } const handleSubmit = () => { startTransition(async () => { const error = await updateName(name); if (error) { setError(error); return; } setError("Ok!"); }) }; return ( <div> <input value={name} onChange={(event) => setName(event.target.value)} /> <button onClick={handleSubmit} disabled={isPending}> Update </button> {error && <p>{error}</p>} </div> ); } function App(){ return <> <h1>Hello, React 19 with ESM.sh</h1> <UpdateName /> </> } const root = ReactDOMClient.createRoot(document.getElementById('root')) root.render(<App />) </script> </body> </html>

 

 

使用 form 和 useActionState,使程式更簡單

 

前面的例子是使用 Button 直接送出 request 進行狀態切換。React 19 對 form 也加入了一些 Action。修改後程式碼如下。可以看到 useActionState 內可以自動切換狀態,並且和錯誤機制配合的冾到好處。

 

<!doctype html>
<html>
<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
    <title>React App with JSX React 19</title>
</head>
<body class="dx-viewport">
<div id="root"></div>

<script type="text/babel" data-type="module">
  import React, {useState, useActionState} from "https://esm.sh/react@19"
  import ReactDOMClient from "https://esm.sh/react-dom@19/client" 


  async function updateName(name) {
      // 模擬異步操作,例如 API 呼叫
      return new Promise((resolve) => {
        setTimeout(() => {
          if (!name) {
            resolve("Name cannot be empty."); // 模擬錯誤情況
          } else if (name.length > 20) {
            resolve("Name is too long."); // 模擬另一個錯誤情況
          }
          else {
            // 在此處添加更新名稱的實際邏輯,例如 API 呼叫
            console.log("Name updated to:", name); // 模擬成功情況
            resolve(null); // 成功時返回 null 表示沒有錯誤
          }
        }, 1000); // 模擬 1 秒的延遲
      });
    }


    // Using <form> Actions and useActionState
    function ChangeName({ name, setName }) {
      const [error, submitAction, isPending] = useActionState(
      async (previousState, formData) => {
        const error = await updateName(formData.get("name"));
        if (error) {
          return error;
        }
        return null;
      },
      null,
    );

    return (
      <form action={submitAction}>
        <input type="text" name="name" />
        <button type="submit" disabled={isPending}>Update</button>
        {error && <p>{error}</p>}
      </form>
    );
  }

  function App(){
    return <>
      <h1>Hello, React 19 with ESM.sh</h1>
      <ChangeName />
    </>    
  }

  const root = ReactDOMClient.createRoot(document.getElementById('root'))
  root.render(<App />)
</script>
</body>
</html>

 

 

New API:  use - 讓 Async Function 回傳 Promise 更融入 React 世界


在 React 19 之前,我們在 Component 裡處理 Async Function 的回傳值時,通常需要 useState, useEffect 搭配 async/await 才能比較好的處理 loading, error, data 狀態。

 

這在 fetch 資料庫經常會希望有但都要自己來刻的功能。


React 19 引入了 use 這個新的 Hook,讓你在 React Component 裡可以直接 "await" Promise,讓 Async Function 的回傳值可以更自然的融入 React 的世界。

我們來看一個簡單的例子。

 

// New API: use function fetchData() { console.log('Fetching data...'); return new Promise(resolve => { setTimeout(() => { console.log('Data fetched!'); resolve({ message: "Data from Async Function!" }); }, 1500); // 模擬 1.5 秒的 API 延遲 }); } function DataDisplay() { // 直接 use(Promise) const data = use(fetchData()); return ( <div> <p>Data Display:</p> {data ? <p>{data.message}</p> : <p>Loading data...</p>} </div> ); }

 

在 DataDisplay 這個 Component 裡,我們定義了一個 fetchData 的 Async Function,這個 Function 模擬了一個 API 呼叫,會在 1.5 秒後 resolve 一個包含 message 的 Object。

 

重點在 DataDisplay Component 裡,我們可以直接使用 use(fetchData()),use Hook 會處理 fetchData() 回傳的 Promise

 

Pending 狀態: 在 Promise resolve 之前,Component 會進入 Pending 狀態 (Suspense),你可以看到畫面上顯示 "Loading data..."。
    成功狀態: 當 Promise resolve 後,use(fetchData()) 會回傳 Promise 的 resolve 值,也就是 { message: "Data from Async Function!" },Component 重新 render,畫面就會顯示 "Data from Async Function!"。

 

use 的優點:


  • 更簡潔的 Async Function 處理: 不需要再手動管理 loading 狀態,程式碼更簡潔易讀。
  • 提升開發體驗: 讓 Async Function 更自然的融入 React Component 的開發流程。
  • 搭配 Suspense: 可以和 Suspense Component 搭配使用,讓 Loading 體驗更流暢。


 

Ref as a prop - Function Component 也可以直接接收 Ref 了!


在 React 19 之前,Function Component  如果需要接收 Ref,必須要透過 forwardRef 這個 API 包裝才能使用。


// React 18 寫法
const MyInput = React.forwardRef((props, ref) => {
  return <input placeholder={props.placeholder} ref={ref} />;
})

 

// React 19 寫法 - Function Component 直接接收 ref prop
function MyInput({placeholder, ref}) {
  return <input placeholder={placeholder} ref={ref} />
}

 

React 18 寫法對於 Function Component 來說,多了一層 forwardRef 的包裝,語法上比較囉嗦一點,也讓 Function Component 的程式碼看起來比較複雜。

 

React 19  簡化了 Function Component  Ref 的使用方式。現在,Function Component  可以像 Class Component 一樣,直接接收 ref 這個 Prop 了!

 

未來 forwardRef 也將棄用,請盡早學會 ref as a prop。

 

實際案例會像這樣:

 

// Ref as a prop
  function MyInput({placeholder, ref}) {
    return <input placeholder={placeholder} ref={ref} />
  }

  function RefInputComponent() {
    const inputRef = useRef(null);

    useEffect(() => {
      if (inputRef.current) {
        console.log('Input Ref:', inputRef.current);
      }
    }, []);

    return (
      <div>
        <p>Ref as a prop:</p>
        <MyInput placeholder="Enter text" ref={inputRef} />
      </div>
    );
  }

 

總結

 

好的,React 19 這次更新真的太棒啦!一口氣帶來了 Actions, use Hook, ref as a prop 這些超實用的新功能,每一個都打中開發者的痛點,讓開發 React 應用程式變得更輕鬆、更高效。

 

總結一下 React 19 這些必學新功能:

  • Actions:  告別複雜的表單狀態管理!useTransition 和 useActionState 就像是神隊友,幫你優雅地處理非同步操作, loading 狀態、錯誤處理都變得超簡單,程式碼也更簡潔易讀,真的就像資料庫交易一樣方便!


  • use Hook:  Async Function 的救星!以前在 Component 裡處理 Promise,總是要 useState, useEffect 寫一堆,現在有了 use,直接 await 就搞定,程式碼瞬間清爽!資料載入 loading 狀態也自動處理,開發體驗直接起飛!


  • ref as a prop:  Function Component 的 Ref 也太方便了吧!再也不用 forwardRef 包裝了,直接像 Class Component 一樣接收 ref prop 就行,語法更直覺,程式碼也更簡潔,而且聽說 forwardRef 以後要被棄用,這個一定要學起來!


 

React 19 這些新功能,不只是錦上添花,更是實實在在地提升了開發效率和體驗。無論你是 React 新手還是老手,都非常建議趕快升級到 React 19,體驗這些新功能帶來的魅力!

 

看完這篇入門教學,是不是覺得 React 19 其實也沒那麼難?趕快動手試試看,用 esm.sh CDN 快速搭建你的 React 19 開發環境

 

一起擁抱 React 新時代吧!  相信你會愛上 React 19 的!
 

 

 

See also

 


2025/02/18

React 19 新手入門:CDN + esm.sh 快速上手

作者:吳祐賓

 

 


 

 

 

自學習 React 以來,一直很嚮往使用 CDN 來寫 React,因為習慣 jQuery 那種不需要建構環境就能開發應用程式的模式 (No-build development),總認為網頁就該輕鬆寫才是。(當然,複雜邏輯怎樣都不會輕鬆寫就是)

 

轉眼間 React 已經釋出 19 版,多了許多很新奇的東西,最有印象的是:

 

  • 整合 Form 表單的優化 (Action, useActionState 等) 
  • "use" API,可以進行非同步的渲染處理

 

 

 而在 React 19 Upgrade Guide 裡,有段內容吸引到我的注意:

 

UMD builds removed

UMD was widely used in the past as a convenient way to load React without a build step. Now, there are modern alternatives for loading modules as scripts in HTML documents. Starting with React 19, React will no longer produce UMD builds to reduce the complexity of its testing and release process.

To load React 19 with a script tag, we recommend using an ESM-based CDN such as esm.sh.

 


 

這要追朔到 React 誕生年份:2013。當時 JavaScript 模型化規則還沒有定案,所以使用 UMD (Universal Module Definition) ,來讓程式碼可以在各種不同的環境下都能順利執行。

 

隨著 IE6 走入歷史,ESM (ECMAScript Modules) 已成為現代 JavaScript 的官方模組標準。瀏覽器和 Node.js 都已經原生支援 ESM。React 團隊認為使用 ESM 除了能減少維護 UMD 的能量外,也可以提高程式的開發效率。

 

 

回顧使用 CDN 建構 React 18 的 Hello World App

 

 

<!doctype html>
<html>
<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    <title>React App without JSX in React 18</title>
</head>
<body>
<div id="root"></div>
<!-- script 寫在後面才會在 root 後面執行,或是使用 window.onload 就可以將此段放在 head 標籤內 -->
<script>
  const e = React.createElement;

  const root = ReactDOM.createRoot(document.getElementById('root'));
  root.render(e('div', null, 'Hello React App without JSX in React 18'));
</script>
</body>
</html>

 

 

使用 CDN 建構 React 19 的 Hello World App

 

因為 React19 不再開發 UMD,取而代之的是 ESM,esm.sh CDN 站點有支援 esm 即時載入及編譯的功能,又可以支援 modules 寫法,使用上更加接近本機環境建構內容。

 

<!doctype html>
<html>
<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>React App without JSX React 19</title>
</head>
<body>
<div id="root"></div>
<script type="module">
  import React from "https://esm.sh/react@19/?dev"
  import ReactDOMClient from "https://esm.sh/react-dom@19/client/?dev"
  
  const e = React.createElement;

  const root = ReactDOMClient.createRoot(document.getElementById('root'));
  root.render(e('div', null, 'Hello React App without JSX in React 19'));
</script>
</body>
</html>

 

 

esm.sh CDN 提供即時模組匯入,但模組來源在哪裡?

 

esm.sh 的原理是即時編譯模組,而模組的來源便是來自 npmjs 倉庫,以 React 來說,我們可以到 npmjs 網站查詢 React。網址:

 

https://www.npmjs.com/package/react

 

 

如下圖所示,目前的版本為 19.0.0,使用 React 19 Upgrade Guide 裡的範例即可使用。

 


 

 


總結:React 19 CDN 方式在新手入門學習上更加方便與平滑

 

從上述的程式碼可以得知 React 19 CDS 開發上很貼近本地建構環境 (例如 React + Webpack) 。

 

以往 UMD 只能使用 React 三本柱 (React, Router, Redux),要使用 npm 套件難度極高,例如 DevExtreme 這類套件就不能使用。

 

而 ESM 開發方式不僅是現代開發主流,更讓這些三方套件在 CDN 下使用成為可能。

 

我認為也能讓 React 新手在適應 ESM CDN 後能更平滑過渡到本地建構環境。

 

實際玩過 React 19 後覺得能夠在 CDN 環境下享受 NPM 豐富的套件,讓 CDN 模式延展性更好,更適合學習、小規模測試和原型(Prototype)開發。

 

下一篇會加入 JSX,來看看 React 19 新規則能否和 Babel 結合,來體驗 React 19 更多的可能。

 

和你分享

 

 

See also

 

 

 

2025/02/13

告別 AI 訂閱費?我的 Intel 迷你電腦 AI 本機實戰(LM Studio / Ollama / IPEX-LLM)

作者:吳祐賓

 


 

 

最近 Deepseek R1 火紅,據說硬體需求不高,我在想是否有可能在本機下運作,省下每個月繳給 AI 商的費用。

 

先說結論:租 AI 最實在!

 

因為算力這種事真的還是需要高檔 GPU 才辦得到,如果自家的硬體能力不夠,還是租 AI 服務來得划算,畢竟不需要承擔 GPU 建置、汰換及電費等費用。

 

DeepSeek-R1是深度求索(DeepSeek)於2025年1月20日發布的人工智慧大型語言模型,專門適用於數學、編碼和邏輯等任務。

 

據聞 DeepSeek 硬體需求較低,有審查機制等資安問題是很多人卻步的關鍵。

 

然而,在封閉的本機環境在資訊安全上還是有一定程度的保護,能在自己的 PC 上跑 AI 也很有趣,怎麼樣也要來玩看看。

 

本篇內容將說明本機 AI 建置步驟、體感心得。


 

環境

 

OS: Windows 11

CPU: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.8GHz

GPU:  Intel(R) Iris(R) Xe Graphics (performance close to NVIDIA GeForce MX330)

RAM: 32 GB RAM

 

 

使用虛擬機器執行 LM Studio

 

LM Studio 是一款視覺介面非常好的本機建置、測試 AI 的應用程式,新手友善的整合介面,很適合 AI 起步使用。

 

由於沒有獨立顯示卡,只能使用 CPU 運算,LM Studio 建置上又相當簡單,索性就用虛擬機器來跑 AI。

 

安裝、搜尋功能太直覺,Deepseek R1 (DeepSeek-R1-Distill-Qwen-7B) 一下子就安裝好了。

 

殊.不.知.

 

實際跑出的體感效果實在緩慢,思考時間往往超過 5 分鐘以上,加上每 2 秒 1 個字的輸出時間,太過燃燒生命,於是就果斷刪除。

 

 


 

 

在 Intel 硬體平臺上執行 Ollama

 

目前 LM Studio 和 Ollama 語言模型框架都支援 Nvidia 和 AMD GPU 運算,不用額外設定就可以使用。

 

而 Intel GPU 不論是內建顯示卡或獨立顯示卡,都不會被語言模型框架使用。幸好,Intel 有提供名叫 "ipex-llm" 加速器來讓框架順利使用 GPU 來進行運算。

 

什麼是 IPEX-LLM?

 

閱讀了 Intel 出的 "使用IPEX和IPEX-LLM優化Intel® CPU上的大語言模型推斷 - 技術檔",有關 IPEX-LLM 的說明,整理後我是這樣理解的:

 

Intel® LLM Library for PyTorch (IPEX-LLM) 是專門設計用於在 Intel 處理器和 GPU 上運行大型語言模型 (LLM) 的 PyTorch 函式庫。  它的進階功能,讓 Intel GPU 支援模型轉換的即時優化,顯著提高開發者的體驗。這個強大的功能增加 LLM 的運行潛力,使其超越了使用傳統獨立顯示卡,並且可以支援 Intel 的整合式 GPU (內建顯示卡)。 

 

 

安裝 Miniforge

 

Miniforge 是一套用於計算科學(資料科學、機器學習、巨量資料處理和預測分析)的Python和R語言的應用平台,有點像沙盒(Sandbox)的概念,核心在簡化軟體套件管理系統和部署。

 

Miniforge 下載點 


https://github.com/conda-forge/miniforge

 

點擊 Windows installer 連結下載 Miniforge3-Windows-x86_64.exe。再使用命令提示字元下指令安裝:

 

start /wait "" Miniforge3-Windows-x86_64.exe /InstallationType=JustMe /RegisterPython=0 /S /D=%UserProfile%\Miniforge3

 

Windows 下要在環境變數 (系統 > 系統資訊 > 進階系統設定 > 進階頁面 > 環境變數)中加入 Miniforge 路徑:

 

%UserProfile%\Miniforge3
%UserProfile%\Miniforge3\Scripts
 

安裝完成,以系統管理員身份開啟 Miniforge Prompt,使用指令 conda -V COMMAND mamba -V COMMAND 來查看版本,出現版本號碼即是設定成功 。

 

mamba 是使用 C++ 重寫 conda 的版本,功能和 conda 完全相同,但效率更好。







為安裝 IPEX-LLM 建立 llm-cpp 環境

 

在 Miniforge Prompt 使用以下指令:

 

conda create -n llm-cpp python=3.11
conda activate llm-cpp
pip install --pre --upgrade ipex-llm[cpp]


** conda create... = 建立 llm-cpp 環境,python 版本為 3.11

** conda activate... 啟動 llm-cpp 環境


建置 llm-cpp 環境

 

用以下指令建立一個名為 llama-cpp 目錄,並進入該目錄。 


mkdir llama-cpp
cd llama-cpp



使用最高權限執行以下指令:

 

init-llama-cpp.bat



啟動 llm-cpp 環境

 

啟動前,在 Miniforge Prompt 使用以下指令:

 

set SYCL_CACHE_PERSISTENT=1
rem under most circumstances, the following environment variable may improve performance, but sometimes this may also cause performance degradation
set SYCL_PI_LEVEL_ZERO_USE_IMMEDIATE_COMMANDLISTS=1


使用 IPEX-LLM 讀取 gguf 模組


llama-cli 是執行 gguf 的工具,可以在不用 Ollama 框架下載入 gguf。指令如下:

 

llama-cli -m Breeze-7B-Instruct-64k-v0.1-Q4_K_M.gguf -n 32 --prompt "Once upon a time, there existed a little girl who liked to have adventures. She wanted to go to places and meet new people, and have fun" -c 1024 -t 8 -e -ngl 99 --color 

 

Breeze-7B 是聯發科開源的MediaTek Research Breeze-7B模型,支援英文及繁體中文,效能也相當卓越。執行時,GPU 使用率會拉到 99%。

 


 

 

執行結束時,會看到 llama 執行的效率,結果應如下圖所示。

 



使用 Conda 初始化 Ollama,讓 Ollama 使用 IPEX-LLM


在 Miniforge Prompt 執行以下命令使用 conda 進行 llm-cpp 環境啟動和初始化 Ollama。在你的當前目錄中會出現一個指向 ollama 的符號連結。我的版本是 ollama version is 0.5.4-ipexllm-20250211

 

conda activate llm-cpp
init-ollama.bat



啟動 Ollama

 

在 Miniforge Prompt 中執行以下命令(activate llm-cpp 要先做)。或是寫成一個 bat 來啟動 Ollama。

 

set OLLAMA_NUM_GPU=999
set no_proxy=localhost,127.0.0.1
set ZES_ENABLE_SYSMAN=1
set SYCL_CACHE_PERSISTENT=1
rem under most circumstances, the following environment variable may improve performance, but sometimes this may also cause performance degradation
set SYCL_PI_LEVEL_ZERO_USE_IMMEDIATE_COMMANDLISTS=1

ollama serve


啟動成功後,在結尾處會看到 runners=[ipex_llm]。就表示 Ollama 有成功使用 IPEX-LLM。

 


 

使用 Ollama 載入模型,愉快體驗吧!

 

保留上一步驟的 Miniforge Prompt,打開另一個 Miniforge Prompt 終端,然後使用 ollama.exe run <model_name>自動拉一個模型。

 

ollama run jcai/breeze-7b-instruct-v1_0:q4_K_M

 

 


 


總結:土炮煉 AI,一場硬頸的浪漫


繞了一大圈,從 LM Studio 到 Ollama,再到 IPEX-LLM,最後還搬出 Miniforge 這沙盒神器,中間還一度卡在 IPEX-LLM 起不來的關卡而想放棄,這條在本機跑 AI 的路,走得還真不輕鬆。但你問我值不值得?我會說,這就像組一台自己的鋼彈模型,過程充滿挑戰,但看到它動起來的那一刻,爽度破表!

 

雖然結論還是「租 AI 最實在」(畢竟人家是專業的嘛!),但這趟旅程讓我更了解大型語言模型(LLM)的底層運作,也見識到 Intel 在 AI 領域的努力(IPEX-LLM 真的有厲害到)。更重要的是,我證明了即使沒有高檔顯卡,只要有顆不服輸的心,還是能在自己的電腦上玩 AI!

 

那…效能呢?老實說,跟雲端服務比,當然還是有差。由於整合顯示卡的記憶體和系統記憶體共用,執行速度也不如獨顯VRAM來得好。


雖然效能不盡人意,但看著 GPU 使用率飆到 99%,聽著風扇呼呼地轉,那種「我的電腦正在努力思考!」的感覺,還真不是花錢租 AI 能體會到的。 而且跑 Breeze-7B 這種支援繁體中文的模型,真是倍感親切感!

 

總之,這是一場「土炮煉 AI」的硬頸浪漫。如果你也喜歡 DIY,喜歡挑戰非正規的道路,喜歡那種「自己的 AI 自己養」的感覺,不妨也來試試看! 不過, 還是要提醒一下,這條路有點崎嶇,要有心理準備喔!



給路人的小建議:


  •     硬體先決: 雖然不用高檔 GPU,但 CPU、RAM 也不能太差。
  •     耐心至上: 安裝、設定、除錯…都需要時間,請保持耐心。
  •     社群是你的好朋友: 遇到問題,多爬文、多請教,你會發現這條路上並不孤單。
  •     安全第一:注意來路不明的模型,以及潛在的資安問題。
  •     玩得開心最重要!

 

 

和你分享



See also