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

2024/12/04

SharpDevelop 技巧:讓你的 dot NET 程式 "單檔" 行天下

 作者:吳祐賓

 


 

 

SharpDevelop 可以編譯小巧可愛的執行檔,但遇到需要三方的函式庫時,就必須連同其 DLL 檔一同複製過去,例如我之前一篇「SharpDevelop 使用 NuGet 套件管理器,以 NPOI 為例」中就需要 NPOI 的相關 DLL。有時這類的小工具我僅需要一個單檔傳遞,可以省下許多部署的時間。

 

這個好用的工具就是「iLmerge」,專門用來整合所有資源檔為單一執行檔的好工具。底下是 SharpDevelop 的設定方式。



iLmerge 所需環境

專案必須導入相關 DLL,在 iLmerge 執行時會使用到它們,要注意的是,這些檔案放在 iLmerge.exe 所在目錄是行不通的。


  • BouncyCastle.Crypto.dll
  • ICSharpCode.SharpZipLib.dll



加入相依參考 (Add Reference)

上面提到 iLmerge 所需套件,我使用的版本是:

 

  • portable.bouncycastle.1.9.0
  • sharpziplib.1.3.3


修改專案檔內容 (csproj)


SharpDevelop 也使用到 Bouncycastle 和 Sharpziplib,所以,加入上述相依參考後,編譯時會出現錯誤訊息:


同一個相依組件的不同版本之間發生衝突。請將專案檔中的 "AutoGenerateBindingRedirects" 屬性設為 true。

解決方式,使用記事本開啟專案檔 csproj,在 <PropertyGroup> 區塊裡加入以下內容:


<PropertyGroup>

...

<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>

</PropertyGroup>



修改專案檔選項 (Project Options)


  • 在 SharpDevelop 中開啟您的專案。
  • 在「Project」選單中,選擇「Project Options...」。
  • 選擇「Build Events」頁籤。
  • 在「Build Events」中,添加「Post-build event command line」來執行 ILMerge。


 

設定建置事件


    以下是一個範例建置後事件命令列,您可以根據您的需求修改:

"$(SolutionDir)\packages\MSBuild.ILMerge.Task.1.1.3\tools\ILMerge.exe" /target:winexe /out:"$(TargetDir)$(TargetName)-merged.exe" "$(TargetPath)"  "$(TargetDir)*.dll" /wildcards





說明


  •     $(SolutionDir): 方案目錄的路徑。
  •     $(TargetDir): 輸出目錄的路徑。
  •     $(TargetName): 目標檔案的名稱(不含副檔名)。
  •     $(TargetPath): 目標檔案的完整路徑。
  •     /wildcards: 使用萬用字元合併所有 DLL 文件。

 

 

總結

 

SharpDevelop 編譯出的執行檔若需使用第三方函式庫,則需一併複製 DLL 檔。為解決此問題,可使用 iLmerge 工具將所有資源檔整合為單一執行檔。

設定步驟:

    導入 DLL:將 BouncyCastle.Crypto.dll 和 ICSharpCode.SharpZipLib.dll 導入專案。
    修改專案檔:在 SharpDevelop 的專案選項中,設定建置後事件命令列,使用 ILMerge 合併 DLL 檔案。

透過以上設定,即可輕鬆產生單一執行檔,方便部署和傳遞。


和你分享

 

 See also

 


2024/12/02

SharpDevelop 使用 NuGet 套件管理器,以 NPOI 為例

 作者:吳祐賓

 

 

 


 

 

操作 Excel 時原以為使用 ADO 吃工作表就可以走天下,想不到踢到鐵板的日子來的這麼快。


C# 操作 Excel 方案有很多,清單和簡易心得如下:



  • Microsoft.Office.Interop.Excel:依賴 Office,加上 SharpDevelop 太老舊,不建議使用
  • OpenXml:微軟官方提供的SDK,只能使用 xlsx,所以放棄使用
  • NPOI:Github 上的開源框架,用過的都說好
  • EPPlus:專業讀寫 Excel 套件,$499 起跳
  • Spire.Office for .NET:$2,999 起跳
  • Aspose.Cells  .NET:極致奢華,貴族使用



NPOI 是什麼?


NPOI 是一個免費開源的 .NET 函式庫,讓你不用安裝 Microsoft Office 也能讀寫 Excel 檔案(xls, xlsx)。

 

NPOI 源自於 APOI,Wiki 是這麼寫的:

Apache POI是Apache軟體基金會的開放原始碼函式庫,POI提供API給Java程式對Microsoft Office格式檔案讀和寫的功能。 .NET的開發人員則可以利用NPOI(POI for .NET)來存取POI的功能。 



優點:


    **免費開源:**基於 Apache 2.0 授權,可自由使用於商業和非商業專案。
    **跨平台:**可在 Windows, Linux, macOS 等平台上使用。
    **功能豐富:**支援大部分 Excel 功能,包含格式設定、公式計算、圖表操作等。
    **效能良好:**讀寫速度快,記憶體佔用低。
    **社群活躍:**擁有龐大的使用者群體和完善的技術文件。



應用場景:


    程式化產生 Excel 報表
    從 Excel 檔案匯入資料到資料庫
    將資料從資料庫匯出到 Excel 檔案
    自動化處理 Excel 文件



SharpDevelop 如何安裝 NPOI?

SharpDevelop 5.1 把 NuGet 移除,所以必須另外下載 NuGet 進行安裝,我在「SharpDevelop 使用 NuGet 套件管理器,以 Newtonsoft.Json 為例 」有詳細講述使用 NuGet 進行指令操作步驟,有興趣的人可以透過前面的連結仔細研究。

 

後來我發現,可以直接到 NuGet 官網直接下載套件及適用的版本,不需要使用 NuGet 程式就能完成。

 

 

NuGet 直接下載所需套件 - 以 NPOI 為例


在搜尋引擎以關鍵字 "nuget NPOI" 查找,可以找到 NPOI 2.7.2,按下右邊的「Download package」即可下載。




且慢,請注意圖片下方的 ".NET Framework",顯示的是支援的版本,從紅框處可以知道 NPOI 2.7.2 最低支援 .net461 以上。SharpDevelop 5 最高也只到 .NET Framework 4.5.2,所以要下載舊一點的版本。我是下載 NOPI 2.5.6,提供給各位評估使用。




下載的檔案是「npoi.2.5.6.nupkg」,副檔名為 nupkg,實際格式為 zip,可以使用任意解壓縮程式解開它。



SharpDevelop 導入 NPOI 方法



使用 SharpDevelop 開啟你的專案,在 Project 管理視窗點擊滑鼠右鍵,路徑 Add Reference > .NET Assembly Browser > Browse... > npoi.2.5.6 目錄 >  lib > net45 選取以下檔案:

 

  • NPOI.dll
  • NPOI.OOXML.dll
  • NPOI.OpenXml4Net.dll
  • NPOI.OpenXmlFormats.dll

 

加入後就可以在你的專案使用 NPOI 了。






因 NPOI 2.5.6 導入後造成 SharpDevelop 出錯時

導入 NPOI 2.5.6 的 NPOI.OOXML.dll 時會出例外,這是 SharpDevelop 內部的錯誤,不影響編譯。如果要完全避免錯誤,可以使用 NPOI 2.5.1 的NPOI.OOXML.dll。







範例程式碼:建立簡單 Excel 報表


以下範例示範如何使用 NPOI 建立一個簡單的 Excel 報表,包含建立工作簿、工作表、儲存格,以及設定簡單的格式。



using NPOI.HSSF.UserModel; // 引用 NPOI 函式庫
using NPOI.SS.UserModel;
using System.IO;

public class NPOIExample
{
    public static void CreateExcelReport()
    {
        // 建立新的工作簿
        IWorkbook workbook = new HSSFWorkbook(); 

        // 建立新的工作表
        ISheet sheet = workbook.CreateSheet("銷售報表");

        // 建立標題列
        IRow headerRow = sheet.CreateRow(0);
        headerRow.CreateCell(0).SetCellValue("產品名稱");
        headerRow.CreateCell(1).SetCellValue("數量");
        headerRow.CreateCell(2).SetCellValue("單價");
        headerRow.CreateCell(3).SetCellValue("總價");

        // 建立資料列
        IRow dataRow1 = sheet.CreateRow(1);
        dataRow1.CreateCell(0).SetCellValue("產品 A");
        dataRow1.CreateCell(1).SetCellValue(10);
        dataRow1.CreateCell(2).SetCellValue(100);
        dataRow1.CreateCell(3).SetCellValue(1000);

        IRow dataRow2 = sheet.CreateRow(2);
        dataRow2.CreateCell(0).SetCellValue("產品 B");
        dataRow2.CreateCell(1).SetCellValue(20);
        dataRow2.CreateCell(2).SetCellValue(50);
        dataRow2.CreateCell(3).SetCellValue(1000);

        // 設定格式
        ICellStyle currencyStyle = workbook.CreateCellStyle();
        currencyStyle.DataFormat = workbook.CreateDataFormat().GetFormat("#,##0.00");
        dataRow1.GetCell(2).CellStyle = currencyStyle;
        dataRow1.GetCell(3).CellStyle = currencyStyle;
        dataRow2.GetCell(2).CellStyle = currencyStyle;
        dataRow2.GetCell(3).CellStyle = currencyStyle;

        // 將工作簿儲存到檔案
        using (FileStream stream = new FileStream("銷售報表.xls", FileMode.Create, FileAccess.Write))
        {
            workbook.Write(stream);
        }
    }
}


程式碼說明:

  1.  引用 NPOI 函式庫: using NPOI.HSSF.UserModel; 引用 NPOI 函式庫,使用 HSSFWorkbook 建立 .xls 格式的 Excel 檔案。
  2. 建立工作簿和工作表: IWorkbook 代表整個 Excel 檔案, ISheet 代表一個工作表。
  3. 建立儲存格: IRow 代表一行, ICell 代表一個儲存格。使用 CreateRow 和 CreateCell 方法建立儲存格。
  4. 設定儲存格值: 使用 SetCellValue 方法設定儲存格的值。
  5. 設定格式: ICellStyle 代表儲存格的樣式,例如數字格式、字型、顏色等。
  6. 儲存檔案: 使用 FileStream 將工作簿儲存到檔案。


注意事項:

    以上程式碼建立的是 .xls 格式的 Excel 檔案。如果要建立 .xlsx 格式的檔案,需要使用 XSSFWorkbook 類別。
    NPOI 支援更複雜的操作,例如設定公式、合併儲存格、插入圖片等,可以參考 NPOI 的官方文件了解更多資訊。



NPOI 範例程式碼:讀取上述 Excel 報表

 

以下範例示範如何使用 NPOI 讀取前面建立的 "銷售報表.xls" 檔案,並將資料顯示在主控台。



using NPOI.HSSF.UserModel;
using NPOI.SS.UserModel;
using System;
using System.IO;

public class NPOIExample
{
    public static void ReadExcelReport()
    {
        // 開啟 Excel 檔案
        using (FileStream stream = new FileStream("銷售報表.xls", FileMode.Open, FileAccess.Read))
        {
            IWorkbook workbook = new HSSFWorkbook(stream);
            ISheet sheet = workbook.GetSheetAt(0); // 取得第一個工作表

            // 讀取標題列
            IRow headerRow = sheet.GetRow(0);
            for (int i = 0; i < headerRow.LastCellNum; i++)
            {
                Console.Write(headerRow.GetCell(i).StringCellValue + "\t");
            }
            Console.WriteLine();

            // 讀取資料列
            for (int i = 1; i <= sheet.LastRowNum; i++)
            {
                IRow dataRow = sheet.GetRow(i);
                for (int j = 0; j < dataRow.LastCellNum; j++)
                {
                    ICell cell = dataRow.GetCell(j);

                    // 根據儲存格類型取得值
                    switch (cell.CellType)
                    {
                        case CellType.String:
                            Console.Write(cell.StringCellValue + "\t");
                            break;
                        case CellType.Numeric:
                            Console.Write(cell.NumericCellValue + "\t");
                            b 1 reak;
                        default:
                            Console.Write(" \t");
                            break;
                    }
                }
                Console.WriteLine();
            }
        }
    }
}



程式碼說明:

    開啟 Excel 檔案: 使用 FileStream 開啟 "銷售報表.xls" 檔案。
    讀取工作表: 使用 workbook.GetSheetAt(0) 取得第一個工作表。
    讀取標題列: 使用 sheet.GetRow(0) 取得第一行 (標題列)。
    讀取資料列: 使用 sheet.LastRowNum 取得最後一列的索引,迴圈讀取每一列資料。
    取得儲存格值: 使用 dataRow.GetCell(j) 取得儲存格,並根據 cell.CellType 判斷儲存格類型,取得對應的值。
    顯示資料: 將讀取到的資料顯示在主控台。

注意事項:

    以上程式碼讀取的是 .xls 格式的 Excel 檔案。如果要讀取 .xlsx 格式的檔案,需要使用 XSSFWorkbook 類別。
    程式碼中使用 switch 判斷儲存格類型,讀取不同類型的儲存格值。實際應用中,可能需要處理更多不同的儲存格類型和格式。


希望以上範例程式碼和說明能幫助你使用 NPOI 讀取 Excel 檔案!



總結


NPOI 是免費開源的 .NET 函式庫,讓你輕鬆讀寫 Excel 檔案。它功能豐富、效能良好,且跨平台支援 Windows、Linux 和 macOS。不用依賴 Office,就能處理格式設定、公式計算、圖表操作等。無論產生報表、匯入匯出資料或自動化處理,NPOI 都是 SharpDevelop 使用者的絕佳選擇!還在煩惱 Excel 操作嗎?快試試 NPOI 吧!



和你分享



See also



2024/10/16

推播服務 DIY - 使用 node.js 實作

作者:吳祐賓




 

 

 

在製作 Prototype 產品時,例如電商網站的促銷活動、新聞網站的即時通知等,推播是一項必要且方便的技術,以往只能使用 email 作為推播的手段,現在有了 Web Push 功能,推播就更方便了。


Web Push 支援程度

 

Web Push 技術已行之有年,但各家瀏覽器支援的時間卻不一定,Safari on iOS 在 2018 開始支援後,推播技術也算是達到了真正的跨平台技術。以下是最早支援 Web Push 的瀏覽器版本。

 


Minimal version Supported year
Safari on iOS 11.3 iPhone XR (2018)
Chrome Android 40 2012
Firefox Android 44 2011
Chrome / Edge 40 / 17 2015
Firefox 44 2016

 

雖然知道現在多數的瀏覽器都支援,在實作上,仍然需要先進行檢查,因為很難保證不會有使用者仍然使用 IE 瀏覽器。

 

先以建立 Express.js 服務作為開始吧!這裡要謝謝 Whien 寫的這篇「建立 Service Worker Web Push Notification — (Web Notification 實作紀錄)」 文章,本篇文章的程式碼大多參考自該篇文章。檔案目錄結構如下所示:

 

├── server
│   └── server.js
└── js
    ├── main.js
    └── sw.js

 

server.js 的程式碼如下:

 

const path = require('path');
const express = require('express');
const app = express();
app.use('/js', express.static(path.resolve(__dirname, '../js')));
app.get('/', (req, res) => {
  res.send(`
    <html>
      <head></head>
      <body>
        <h1>Push Notification</h1>
        <script src='js/main.js'></script>
      </body>
    </html>
  `);
});app.listen(8998);

 

 

檢查 Web Push 機能

 

在實作 Web Push 前,要先確認瀏覽器是否支援這項技術。瀏覽器的 Web Push 標準是以 "PushManager" 和 "ServiceWorker" 兩個 API 架構而成。 

 

  • PushManager:瀏覽器提供的推播管理 API,負責處理推播訊息的訂閱和接收
  • ServiceWorker:瀏覽器提供的 Service Worker API,允許在背景執行 JavaScript 程式碼,即使網頁關閉也能接收推播訊息

 

ServiceWorker 具體實作內容交由開發者自行決定。由於 serviceWorker 比 PushManager 還要晚推出,所以要各別檢查 (又或是檢查 serviceWorker 即可)。


使用以下程式碼作為 main.js 程式碼的起始:


if ('serviceWorker' in navigator && 'PushManager' in window) {
  console.log('Service Worker and Push is supported')
} else {
  console.log('Push is not supported')
}

 

 

註冊 ServiceWorker

 

Service Worker 是在背景執行的 JavaScript 檔案,負責接收和處理推播訊息。必須先向瀏覽器註冊 Service Worker 才能使用 Web Push。以下程式碼示範如何註冊 Service Worker:

 

if ('serviceWorker' in navigator && 'PushManager' in window) {
//...
  navigator.serviceWorker
    .register('/js/sw.js')
    .then(swReg => {
      console.log('Service Worker is registered', swReg);
    })
    .catch(err => console.log('Service Worker register error.', err));
//...

 

 

  • navigator.serviceWorker.register('/js/sw.js') 會向瀏覽器註冊位於 /js/sw.js 的 Service Worker 檔案。
  • .then(swReg => { ... }) 會在 Service Worker 註冊成功後執行,swReg 參數包含 Service Worker 的註冊資訊。 
  • .catch(err => console.log('Service Worker register error.', err)) 會在 Service Worker 註冊失敗時執行,err 參數包含錯誤資訊。

 

 

建立 Web Push 金鑰

 

Web Push 規範中對資料的加密有嚴格的要求,這部份可以透過 node.js 裡的 web-push 函式庫產生 VAPID 金鑰:

 


npx web-push generate-vapid-keys

=======================================

Public Key:
BNuUKzbcqU288QpgHA4PMSAfw8I________________XYLHBZCuPH27DMlLJMhXTlr84fpHtKs3g

Private Key:
ywMS_d68vai________________lrNSOz3IjxU22ks-IFhGc

=======================================

 

 

接著把 Public Key 在 main.js 最上層放入變數:


const applicationServerPublicKey = "BNuUKzbcqU288QpgHA4PMSAfw8I________________XYLHBZCuPH27DMlLJMhXTlr84fpHtKs3g"

 

 

 

檢查使用者是否已訂閱

 

接下來在訂閱推播訊息之前,我們需要先檢查使用者是否已經訂閱,避免重複訂閱。程式碼變化後如下所示:

 


navigator.serviceWorker
  .register('/js/sw.js')
  .then(swReg => {
    console.log('Service Worker is registered', swReg);
    let swRegistration = swReg;
    return Promise.resolve(swRegistration)
  })
  .then(swRegistration => {
    inititalUI(swRegistration)
  })


return Promise.resolve(swRegistration)swRegistration 包裝成一個 Promise 物件,並返回。 這樣可以將 Service Worker 註冊的結果傳遞給後續的 .then() 方法,以便進行訂閱等操作。如此使用 Promise 可以把註冊、訂閱程式碼邏輯拆開,方便日後程式碼的維護。


initialUI 可以實作顯示訂閱的按鈕,範例程式碼如下所示:


function inititalUI(swRegistration) {
  const pushButton = document.createElement('button');
  pushButton.textContent = '啟用推播';
  document.body.appendChild(pushButton);

  swRegistration.pushManager.getSubscription().then(subscription => {
    updatePushButton(subscription, pushButton);

    pushButton.addEventListener('click', () => {
      if (subscription) {
        unsubscribeUser(subscription, pushButton, swRegistration);
      } else {
        // 檢查通知權限狀態
        if (Notification.permission === 'denied') {
          // 如果已封鎖,引導使用者前往瀏覽器設定頁面
          alert('您已封鎖通知權限,請前往瀏覽器設定頁面開啟通知。');
          // (Optional)  您可以根據瀏覽器類型,提供更具體的引導方式
          // window.open('chrome://settings/content/notifications'); // Chrome 
          // window.open('about:preferences#privacy'); // Firefox
        } else {
          subscribeUser(swRegistration, pushButton);
        }
      }
    });
  });
}

function updatePushButton(subscription, pushButton) {
  if (subscription) {
    console.log('使用者已訂閱.');
    pushButton.textContent = '取消推播';
  } else {
    console.log('使用者未訂閱.');
    pushButton.textContent = '啟用推播';
  }
}

 

  • initialUI 所建立的「啟用推播」按鈕按下後,實際上只會詢問使用者一次 (瀏覽器預設為「詢問」), 按下允許 / 封鎖後,就不會再從瀏覽器跳出,要使用者自行進瀏覽器變更設定。
  • swRegistration.pushManager.getSubscription() 方法會返回一個 Promise,其中包含使用者的訂閱資訊。如果使用者尚未訂閱,則 Promise 的值為 null。


啟用推播和取消推播的程式碼如下:

 


function urlB64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/\-/g, '+')
    .replace(/_/g, '/');const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

function subscribeUser(swRegistration) {
  const applicationServerKey = urlB64ToUint8Array(applicationServerPublicKey);
  swRegistration.pushManager
    .subscribe({
      userVisibleOnly: true,
      applicationServerKey
    })
    .then(subscription => {
      console.log('User is subscribe.');
      updatePushButton(subscription, pushButton);
    })
    .catch(err => console.log('Failed to subscribe the user: ', err));
}

function unsubscribeUser(subscription, pushButton, swRegistration) {
  subscription.unsubscribe()
    .then(() => {
      console.log('使用者已取消訂閱.');
      updatePushButton(null, pushButton);

      // (Optional) 向伺服器發送取消訂閱請求
      // ...
    })
    .catch(err => console.log('取消訂閱失敗: ', err));
}

 

  • urlB64ToUint8Array(base64String) 函式:將 Base64 編碼的字串轉換為 Uint8Array 格式。這是 Web Push 訂閱所需的格式。
  • subscribeUser(swRegistration) 函式:訂閱使用者的推播通知。
  • userVisibleOnly: true :確保所有推播通知都顯示給使用者。
  • unsubscribeUser(subscription, pushButton, swRegistration) 函式:取消訂閱使用者的推播通知。



實作 Service Worker


Chrome / Edge 的 DevTools 的「應用程式」頁籤有提供 Service Worker 測試推播功能,不用送 request 到後端,直接觸發 API 呼叫 push 事件,以下程式碼示範如何在 Service Worker 中處理 push 事件,並顯示推播通知:


const SW = '[Service Worker]';

self.addEventListener('push', event => {  // 監聽 push 事件
  const eventData = event.data.text();  // 提取推播訊息
  console.log(`${SW} Push Received.`);
  console.log(`${SW} Push had this data: ${eventData}`);

  const title = '推送好消息';  // 設定通知標題
  const options = {  // 設定通知選項
    body: eventData,  // 設定通知內容
    // icon: '',  // 設定通知圖示
    // badge: ''  // 設定通知徽章
  };

  // 使用 showNotification() 方法顯示推播通知
  event.waitUntil(
    new Promise(resolve => { 
      setTimeout(() => {
        self.registration.showNotification(title, options).then(resolve); 
      }, 5000);  // 延遲 5 秒顯示通知
    })
  );
});


這段程式碼會監聽 push 事件,並從事件資料中提取推播訊息。 然後,它會使用 showNotification() 方法顯示推播通知,並且通知的標題和內容中顯示提取到的訊息。 


  • 監聽 push 事件:self.addEventListener('push', event => { ... });
  • 提取推播訊息:const eventData = event.data.text();
  • 設定通知標題和選項:const title = '推送好消息'; 和 const options = { ... };
  • 顯示推播通知:self.registration.showNotification(title, options);
  • 延遲顯示通知:setTimeout(() => { ... }, 5000);
  • 使用 waitUntil() 方法確保通知成功顯示:event.waitUntil( ... );


 

要注意的地方是,某些瀏覽器到現在 2024 年,Service Worker 都還不支援 ECMAScript modules 設計,所以 Servcie Worker 實作上建議盡量使用簡單的方式設計。



總結

 

呼~終於完成了!這篇文章帶大家用瀏覽器的內建功能,完整體驗了一次 Web Push 的流程,也實際看到了推播通知的效果。相當的簡單方便!

從檢查瀏覽器支援性、註冊 Service Worker、產生 Web Push 金鑰,再到訂閱和取消訂閱推播訊息,最後實作 Service Worker API 進行推播通知的顯示。希望這些步驟都能幫助大家理解 Web Push 的運作方式。

當然,這只是 Web Push 的單機入門介紹,之後如何與後端伺服器整合,發送客製化的推播訊息等相關資訊,我會在之後的文章中陸續跟大家分享,敬請期待!

最後,要再次感謝 Whien 的文章「建立 Service Worker Web Push Notification — (Web Notification 實作紀錄)」提供的程式碼範例和靈感。 也歡迎大家參考 Whien 的文章,了解更多 Web Push 的實作細節。

希望這篇文章對大家有所幫助,也歡迎大家留言分享你的學習心得和問題喔!

 

 

See also

2024/03/11

突破限制:在64位元Windows上使用JScript的兩種方法

 

作者:吳祐賓

 


 

最近我的程式莫名出現了:EOleSysError: 類別未登錄, ProgID: "MSScriptControl.ScriptControl" 的錯誤訊息。一查才知道原來是 Windows 沒有提供 Microsoft Script Control 64 位元版本,但許多程式會用到 JScript。遇到這難題該怎麼處理呢? 

 


 

 

使用外掛 Tablacus Script Control 64

 

外國網友 Gaku 提供了好用的 OCX 套件:Tablacus Script Control 64。現有的程式幾乎不用修改就可以使用。缺點是必須要另外安裝。截取官方範例如下:

 

var SC = new ActiveXObject("ScriptControl");
SC.Language = "JScript";
SC.AddObject("WScript", WScript);
SC.AddCode('function fnx(a,b,c){ WScript.Echo(a + ":" + b + ":" + c) }');
SC.Run("fnx", 1, "data2", "data3");

 

使用直譯式工具 FastScript


FastScript 是 FastReport 公司旗下的直譯式跨語言開發套件,可在不開啟程式編譯器的場合下做簡易的程式開發。使用起來也相當簡單。缺點就是 JScript 語言支援度比微軟老舊的 JScript 版本更為受限。

 

procedure TForm2.Button1Click(Sender: TObject);
var
  LfsResult: Variant;
begin
  fsScript1.Clear;
  fsScript1.Parent := fsGlobalUnit;
  fsScript1.SyntaxType := 'JScript';
  fsScript1.Lines.Text := 'function jMethod(a, b){return a+b}';
  if fsScript1.Compile then
  begin
    LfsResult := fsScript1.CallFunction('jMethod',VarArrayOf([1, 1]));
    ShowMessage(LfsResult);
  end;
end;

 

總結

 

兩種解決方案各有優缺點,使用者可根據自身需求進行選擇:

 

若需要使用現有的程式,且對相容性要求較高,採用 Tablacus Script Control 64 外掛可以很快進入狀況

 

若需要快速開發簡易的 JScript 程式,或對相容性要求不高,則可以使用 FastScript 直譯式工具

 

其他注意事項

    使用 Tablacus Script Control 64 外掛時,需注意其版本與 Windows 系統的版本是否相符。
    使用 FastScript 直譯式工具時,需注意其對 JScript 語言的支持程度。



本文介紹了兩種解決 Windows 64 位元系統下使用 JScript 的方案,希望能對相關使用者有所幫助。

 

 和你分享

 

See also

 


2024/03/07

React + DevExtreme DataGrid:打造你的第一個 CRUD 應用

 

作者:吳祐賓

 


 DataGrid,DevExtreme 最重要的元件之一,也是 WinForm 界夢幻逸品 cxGrid 的 Web 化元件。若能精通它,所有元件都能百分百理解!

 

對於初學者而言,DataGrid 可能有些陌生,但它的用途非常廣泛。這篇文章將帶你一窺 DataGrid 的神奇之處,從最基礎的 Array 顯示開始,一路探索至增刪改的高階操作!

 

要進入增刪改階段,可能會遇到一些困難,但不用擔心,我們會一步步解決這些難題。

 

首先,讓我們從新增階段開始。在這篇文章中,將重點放在 DataGrid 屬性 dataSource 和 store 上。dataSource 是一個大容器,可以根據需求的大小進行調整。你可以使用平舖的 Array,也可以使用更複雜的 DataSource 類別實作。不管你遇到什麼樣的需求,DataSource 都能應對自如!

 

如果你覺得講得太難,別擔心!這裡也提供了 DevExtreme 官方文件的 Data Layer Overview,讓你更容易理解。留言下方告訴我,你覺得更容易理解的是哪一種方法!

 

 

本章目標:快速建立可以 CRUD 的 DataGrid 頁面

 

使用簡單的 array 就可以成為 DataGrid 的資料來源,官方很貼心的提供展示用的人事資料(連結在此)。將上一章 "WinForm 設計師的 Web 開發秘訣:DevExtreme 教學" 建立的HelloWorld 頁面,顯示內容的地方置換為 DataGrid,並在 import 區域導入上述的人事資料及 DataGrid 依賴的套件檔,完成後的程式碼應如下內容。


import React from 'react';
import './hello.scss';

import { employees } from './emp';
import {
  DataGrid
} from 'devextreme-react/data-grid';

export default () => (
  <React.Fragment>
    <h2 className={'content-block'}>Hello</h2>
    <DataGrid
      className={'dx-card wide-card'}
      dataSource={employees}
      keyExpr="ID">
    </DataGrid>
  </React.Fragment>
);


 

 

調整顯示欄位


由於欄位數量太多,畫面顯得相當擁擠。所以接下來調整要顯示的欄位內容。


使用 Column 建立自定欄位 


這裡限制僅顯示 FirstName, Position, BirthDate, HireDate 四個欄位。簡單指定欄位時只要填入 <Column dataField={FieldName}></Column> 即可,需要指定型別時再加入 dataType 等屬性,如下表程式碼粗體文字內容。DataGrid allowColumnReordering 屬性則是允許使用者自行調整欄位順序。


...
import { DataGrid, Column } from 'devextreme-react/data-grid';
...
<DataGrid
  className={'dx-card wide-card'}
  dataSource={employees}
  keyExpr="ID"
  allowColumnReordering={true}>
    <Column dataField="FirstName"></Column>
    <Column dataField="Position"></Column>
    <Column
        dataField="BirthDate"
        dataType="date">
    </Column>
    <Column
        dataField="HireDate"
        dataType="date">
    </Column>

</DataGrid>


開啟編輯功能


加入 Editing 標籤,並指定顯示方式為 popup 氣泡顯示模式,並加入 allowUpdating, allowDeleting, allowAdding 及設定為 true。如此就完成第一部份「帶有 CRUD 的 DataGrid」的內容。


import { DataGrid, Column, Editing } from 'devextreme-react/data-grid';
...
<DataGrid>
  <Column>...</Column>
  ...
  <Editing
      mode="popup"
      allowUpdating={true}
      allowDeleting={true}
      allowAdding={true}
  />

</DataGrid>





總結


這篇文章說明 DevExtreme DataGrid 是目前商業套件中最強大的表格工具,面對新手或簡單功能的場合下,在設計上依然容易學習及使用


無論你是新手還是有經驗的開發者,這篇文章都會帶給你全新的視角和啟發。DataGrid 這個元件越研究越有意思,讓我們一起持續消化中!

 

喜歡這篇文章的話,記得分享給更多人哦!

 

 

See also