我使用的是 Google的 Gemini Pro [付費版],因為他會分析他的思路,還會幫程式碼做結論(重點摘要)
日前做了一些前端的工作(非UI、非美工)
但我對於前端實在苦手,不太行啊!
[工作規格與需求]:搜尋關鍵字,到GitLab找到相關的專案、倉庫(Repo)與超連結,以JSON格式傳回,並呈現在HTML畫面上
運氣很好,GitLab有提供現成的API讓我呼叫,問題算是簡單
只要你把傳回的JSON格式給AI工具,他就能幫你完成。
…..後續要整合多台GitLab傳回的JSON,合併在一起,也不難( 對我來說 :-) 不難,因為我只是請AI幫我做好 )
後來需要連上另一套Source Code平台,名為 Gerrit Server。這套就麻煩了,
他傳回的JSON格式不標準,是一個物件。格式跟上面的也不一樣。…….這裡我就頭大了
=== 原本的程式碼 ===
可以看見功能幾乎一模一樣,但是細節稍微有點改變
這樣的程式碼,你交出去一定會被幹攪…….這時候,呼叫AI大神~~~
免費版的AI工具 - Copilot 就糟糕,因為他有字數限制。簡單的說,「免費的,最貴(服務不好)」
換成 "付費版" 就沒問題了!
我使用Google的 Gemini Pro。每次回答時,會先告訴你「他的思路」(如何解題?),最後還會跟我上課(教課)一樣,做重點整理與複習
這真的很厲害喔!這真的很厲害喔!
<input type="text" id="keywordInput" placeholder=" ex: 'wifi'..." width="280px">
<button type="button" id="searchButton">Search</button>
<div id="resultsContainer" class="col-md-12 themed-grid-col">
</div>
<div id="resultsContainerGerrit" class="col-md-12 themed-grid-col">
</div>
<script>
const searchButton = document.getElementById('searchButton');
const keywordInput = document.getElementById('keywordInput');
const resultsContainer = document.getElementById('resultsContainer');
searchButton.addEventListener('click', fetchProjects);
keywordInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault(); // 防止表單提交
fetchProjects();
}
});
async function fetchProjects() {
const keyword = keywordInput.value.trim();
resultsContainer.innerHTML = `<p class="loading"width="450px">....... Loading, Please wait ...... .</p >`;
try { // *** 避免CORS,透過HttpHandler (.ashx檔) 連過去 *****************
const proxyUrl = `/GitLabHandler.ashx?keyword=${encodeURIComponent(keyword)}`;
const response = await fetch(proxyUrl, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
const responseText = await response.text();
if (!response.ok) {
let errorDetails = `HTTP 錯誤: ${response.status} - ${response.statusText}`;
try {
const errorJson = JSON.parse(responseText);
errorDetails += `. 後端訊息: ${errorJson.error || JSON.stringify(errorJson)}`;
} catch (e) {
errorDetails += `. 後端原始響應: ${responseText}`;
}
throw new Error(errorDetails);
}
let projects;
try {
projects = JSON.parse(responseText);
} catch (e) {
throw new Error(`無法解析後端響應為 JSON: ${e.message}. 原始響應: ${responseText}`);
}
if (!Array.isArray(projects)) {
throw new Error("接收到的數據不是一個有效的 JSON 數組。");
}
// GitLab有API,傳回的JSON格式很標準
displayResults(projects); // 將 projects (JSON)傳遞給 displayResults
} catch (error) {
console.error('搜尋失敗:', error);
}
}
function displayResults(projects) {
const resultsContainer = document.getElementById('resultsContainer'); // 確保這裡能正確獲取到容器
resultsContainer.innerHTML = ''; /* 清空 舊結果 */
if (projects.length === 0) {
resultsContainer.innerHTML = '<p class="no-results">Not Found in GitLab Projects</p>'; /* 未找到符合關鍵字的專案。 */
} else {
const table = document.createElement('table');
table.classList.add('table', 'table-striped', 'table-hover', 'table-bordered');
table.id = "table1";
const thead = document.createElement('thead');
const tbody = document.createElement('tbody');
thead.innerHTML = ` <tr>
<th align="left"><img src="GitLab_Logo_S1.jpg""></th>
<th>Project/Repo</th>
<th>Path</th>
<th>URL</th>
<th>Descr.</th>
<th>Last time</th>
</tr>`;
table.appendChild(thead);
projects.forEach(project => {
const row = document.createElement('tr');
// 確保這裡的屬性名稱與後端 JSON 完全匹配
row.innerHTML = `
<td>${project.SourceServer || '未知'}</td> <td>${project.name || 'N/A'}</td>
<td>${project.path_with_namespace || 'N/A'}</td>
<td class="project-url"><a href="${project.web_url}" target="_blank"> ...Hyperlink... </a></td>
<td><div class="project-description">${project.description || 'N/A'}</div></td>
<td>${project.last_activity_at ? new Date(project.last_activity_at).toLocaleString() : 'N/A'}</td>
`;
tbody.appendChild(row);
});
table.appendChild(tbody);
resultsContainer.appendChild(table);
}
/* ***完成後,繼續尋找 Gerrit並呈現結果 *** */
fetchProjectsGerrit();
}
/* -- Gerrit搜尋結果 -------- */
async function fetchProjectsGerrit() {
const keyword = keywordInput.value.trim();
const resultsContainerGerrit = document.getElementById('resultsContainerGerrit');
try {
const proxyUrl = `/GerrithHandler.ashx?keyword=${encodeURIComponent(keyword)}`;
const response = await fetch(proxyUrl, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
if (!response.ok) {
// 同上
}
// Gerrit傳回的JSON格式,是一種物件。處理起來就不同。
const data = await response.json(); // 這次接收的數據是包含 success, message 和 servers 的物件
if (data.success) {
displayResultsGerrit(data.servers); // 將 servers 物件傳遞給 displayResults
} else {
resultsContainerGerrit.innerHTML = `<p class="error">[Gerrit] Search Fail : ${data.message}</p>`;
}
} catch (error) {
console.error('Gerrit搜尋失敗:', error);
}
}
function displayResultsGerrit(servers) {
resultsContainerGerrit.innerHTML = ''; // 清空舊結果
const projects = []; // ********
for (const serverName in servers) {
if (servers.hasOwnProperty(serverName)) {
const server = servers[serverName];
server.Projects.forEach(project => {
project.SourceServer = server.Name; // 添加來源伺服器信息
projects.push(project);
});
}
}
if (projects.length === 0) {
resultsContainerGerrit.innerHTML = '<p class="no-results">Not Found in Gerrit Projects</p>'; // 未找到符合關鍵字的專案
} else {
const table = document.createElement('table');
table.classList.add('table', 'table-striped', 'table-hover', 'table-bordered');
table.id = "table2";
const thead = document.createElement('thead');
const tbody = document.createElement('tbody');
thead.innerHTML = `
<tr>
<th align="left""><img src="Gerrit_Logo_S1.jpg""></th>
<th>......同上,不贅述.... </th>
</tr>`;
table.appendChild(thead);
projects.forEach(project => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${project.SourceServer || '未知'}</td>
<td>${project.name || 'N/A'}</td>
<td>......同上,不贅述.... </td>
`;
tbody.appendChild(row);
});
table.appendChild(tbody);
resultsContainerGerrit.appendChild(table);
}
}
</script>
<!-- 註解:如果你有多台 Gitlab,或是多台 Gerrit Server,密碼(或token)、網址.....可以寫在 "設定檔" 裡面,統一管理與呼叫 -->
<!-- .NET Framework 4.x版,不分Web Form或MVC "設定檔" ,名為 Web.Config。 -->
<!-- .NET Core 6.0起,設定檔名為 Program.cs。早期版本(.NET 5.0(含)以前)則是 Startup.cs -->
=== AI 重構以後 的 程式碼 === ……註:以下都是AI (Gemini Pro) 提供的內容,我只有加上一些註解
好的,這段程式碼確實有許多可以重構和簡化的空間。主要的重複點在於:
- 資料擷取邏輯 (
fetch
):兩個fetch
函式 (fetchProjects
和fetchProjectsGerrit
) 的結構非常相似,都包含了設定 URL、發送請求、處理錯誤等步驟。 - 結果顯示邏輯 (
display
):兩個display
函式 (displayResults
和displayResultsGerrit
) 在建立表格、標頭、以及遍歷資料產生表格內容的部份幾乎一模一樣。 - 資料結構的差異:這是兩組函式最主要的差別。GitLab 直接回傳一個專案陣列
[]
,而 Gerrit 回傳一個包含servers
物件的結構{ "success": true, "servers": { ... } }
。
我們的目標是將相同的部分抽離出來,並將不同的部分作為參數傳遞。
重構思路
- 建立一個主控函式 (
handleSearch
):取代原本的fetchProjects
,作為搜尋按鈕點擊後的唯一入口。 - 平行處理請求:使用
Promise.all
來同時對 GitLab 和 Gerrit 發送請求,而不是一個接一個地等待,這樣可以提升效率。 - 建立一個通用的資料擷取與顯示函式 (
fetchAndDisplay
):這個函式會接收一個設定物件,物件中包含了特定 Server 的所有資訊(如 API 路徑、顯示容器 ID、Logo 圖片、以及最重要的資料轉換函式)。 - 建立一個通用的結果顯示函式 (
renderResults
):這個函式只負責將標準化後的專案陣列渲染成 HTML 表格,不再關心資料是從哪裡來的。 - 使用設定檔管理差異:將每個 Server 的不同之處(API URL、Logo、資料處理方式等)都放在一個設定物件中,讓程式碼主體保持通用性。
Gemini Pro 重構後的程式碼
<div class="search-area">
同上 ....不贅述....
</div>
<!-- 這裡有修正 -->
<div id="resultsGitLab" class="col-md-12 themed-grid-col"></div>
<div id="resultsGerrit" class="col-md-12 themed-grid-col"></div>
<script>
// ------------------- DOM 元素宣告 (集中管理) -------------------
const searchButton = document.getElementById('searchButton');
const keywordInput = document.getElementById('keywordInput');
// ------------------- 設定檔 (將差異點集中管理) -------------------
// 為每個資料來源定義一個設定物件
const sourceConfigs = [
{
name: 'GitLab',
containerId: 'resultsGitLab',
apiUrl: 'GitLabHandler.ashx',
noResultsMessage: 'Not Found in GitLab Projects',
// 資料轉換函式:將 API 回應轉換成標準的專案陣列
transformData: (data) => {
if (!Array.isArray(data)) {
throw new Error("GitLab response is not a valid JSON array.");
}
return data; // GitLab 的回應直接就是專案陣列,直接返回
}
},
{
name: 'Gerrit',
containerId: 'resultsGerrit',
apiUrl: '/GerritHandler.ashx',
noResultsMessage: 'Not Found in Gerrit Projects',
// 資料轉換函式:處理 Gerrit 特殊的 JSON 結構
transformData: (data) => {
if (!data.success) {
throw new Error(data.message || 'Gerrit search failed.');
}
// ***************************************
// 因為Gerrit Server傳回的JSON格式不標準,是一個物件,必須重新整理
const projects = [];
for (const serverName in data.servers) {
if (Object.hasOwnProperty.call(data.servers, serverName)) {
const server = data.servers[serverName];
server.Projects.forEach(project => {
project.SourceServer = server.Name; // 注入來源伺服器名稱
projects.push(project);
});
}
}
return projects;
}
}
];
// ------------------- 事件監聽 -------------------
searchButton.addEventListener('click', handleSearch);
keywordInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSearch();
}
});
// ------------------- 主要邏輯函式 -------------------
/**
* 主控函式:當使用者觸發搜尋時執行
*/
async function handleSearch() {
const keyword = keywordInput.value.trim();
if (!keyword) {
// 清空所有結果並提示使用者輸入
sourceConfigs.forEach(config => {
const container = document.getElementById(config.containerId);
container.innerHTML = `<p class="no-results">Please enter a keyword and click Search.</p>`;
});
return;
}
// ***************************************
// 使用 Promise.all 同時觸發所有來源的搜尋,提升效能
await Promise.all(
sourceConfigs.map(config => fetchAndDisplay(config, keyword))
);
}
/**
* 通用的資料擷取與顯示函式
* @param {object} config - 來源的設定檔 (sourceConfigs 陣列中的一個元素)
* @param {string} keyword - 搜尋關鍵字
*/
async function fetchAndDisplay(config, keyword) {
const container = document.getElementById(config.containerId);
container.innerHTML = `<p class="loading">Searching ${config.name}... Please wait...</p>`;
try {
const proxyUrl = `${config.apiUrl}?keyword=${encodeURIComponent(keyword)}`;
const response = await fetch(proxyUrl, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP Error: ${response.status} - ${response.statusText}. Details: ${errorText}`);
}
// ***************************************
const rawData = await response.json();
// 使用設定檔中定義的 transformData 函式來處理特定來源的資料結構
const projects = config.transformData(rawData);
// 將標準化後的 projects 陣列交給通用的渲染函式
renderResults(projects, config);
// ***************************************
} catch (error) {
console.error(`[${config.name}] Search failed:`, error);
container.innerHTML = `<p class="error">[${config.name}] Search Failed: ${error.message}</p>`;
}
}
/**
* 通用的結果渲染函式
* @param {Array} projects - 標準化後的專案物件陣列
* @param {object} config - 來源的設定檔,用於獲取 Logo、容器 ID 等資訊
*/
function renderResults(projects, config) {
const container = document.getElementById(config.containerId);
container.innerHTML = ''; // 清空舊結果或 "Loading..." 訊息
if (projects.length === 0) {
container.innerHTML = `<p class="no-results">${config.noResultsMessage}</p>`;
return;
}
const table = document.createElement('table');
table.classList.add('table', 'table-striped', 'table-hover', 'table-bordered');
table.id = `table-${config.name.toLowerCase()}`; // e.g., table-gitlab
// 表格標頭 (thead) - 使用設定檔中的 logo
const thead = document.createElement('thead');
thead.innerHTML = `
<tr>
<th align="left"><img src="${config.logo}" alt="${config.name} Logo"></th>
<th>....同上,省略(不贅述).......
</tr>`;
// 表格內容 (tbody) - 這部分邏輯完全通用
const tbody = document.createElement('tbody');
projects.forEach(project => {
const row = document.createElement('tr');
// 欄位名稱 (name, path_with_namespace 等) 必須在轉換後保持一致
row.innerHTML = `
<td>${project.SourceServer || config.name}</td>
<td>....同上,省略(不贅述).......
`;
tbody.appendChild(row);
});
table.appendChild(thead);
table.appendChild(tbody);
container.appendChild(table);
}
</script>
Gemini Pro 重構後的優點
高內聚,低耦合 (High Cohesion, Low Coupling):
fetchAndDisplay
專注於網路請求和錯誤處理。renderResults
專注於將資料渲染成畫面。transformData
專注於處理不同來源的 JSON 資料格式。- 每個函式的職責都非常清晰。
可擴充性 (Scalability):
- 如果未來要增加第三個來源(例如 Bitbucket),你幾乎不需要修改現有函式。
- 你只需要在
sourceConfigs
陣列中新增一個 Bitbucket 的設定物件,定義它的apiUrl
、containerId
、logo
,並實作它專屬的transformData
函式即可。
可維護性 (Maintainability):
- 所有重複的程式碼都被消除。如果你需要修改表格的 CSS class 或欄位順序,只需要在
renderResults
函式中修改一次。 - 每個 Server 的 API 端點或資料結構若有變更,也只需要去
sourceConfigs
中對應的物件裡修改,不會影響到其他程式碼。
效能提升 (Performance): ……註:學到一個新技巧…..前端我不熟悉,想不到有這東西!讚!
- 使用
Promise.all
讓兩個網路請求併發執行,而不是串行等待。使用者可以更快地看到兩個來源的結果,尤其是在網路延遲較高時,這個差異會更明顯。
程式碼更簡潔易讀 (Readability):
- 透過將差異點抽象化為設定檔,主體邏輯變得非常通用和乾淨,更容易理解整個搜尋流程。
……………………………………………………………………………………………………………………………………………………..
我也是一個有情有義的人,每次AI幫我忙,我都會謝謝他 :-)
AI 的 人性化程度?說真的,你是不是我兄弟? https://dotblogs.com.tw/mis2000lab/2025/04/01/AI_Funny_Answer_20250401

我想我老闆正在我後面,他非常火……. :-)
我將思想傳授他人, 他人之所得,亦無損於我之所有;
猶如一人以我的燭火點燭,光亮與他同在,我卻不因此身處黑暗。----Thomas Jefferson
線上課程教學,遠距教學 (Web Form 約 51hr) https://dotblogs.com.tw/mis2000lab/2016/02/01/aspnet_online_learning_distance_education_VS2015
線上課程教學,遠距教學 (ASP.NET MVC 約 140hr) https://dotblogs.com.tw/mis2000lab/2018/08/14/ASPnet_MVC_Online_Learning_MIS2000Lab
寫信給我,不要私訊 -- mis2000lab (at) yahoo.com.tw 或 school (at) mis2000lab.net
(1) 第一天 ASP.NET MVC5 完整影片(5.5小時 / .NET 4.x版)免費試聽。影片 https://youtu.be/9spaHik87-A
(2) 第一天 ASP.NET Core MVC 完整影片(3小時 / .NET Core 6.0~8.0)免費試聽。影片 https://youtu.be/TSmwpT-Bx4I
[學員感言] mis2000lab課程評價 - ASP.NET MVC , WebForm 。 https://mis2000lab.medium.com/%E5%AD%B8%E5%93%A1%E6%84%9F%E8%A8%80-mis2000lab%E8%AA%B2%E7%A8%8B%E8%A9%95%E5%83%B9-asp-net-mvc-webform-77903ce9680b
ASP.NET遠距教學、線上課程(Web Form + MVC)。 第一天課程, "完整" 試聽。
......... facebook社團 https://www.facebook.com/mis2000lab ......................
......... YouTube (ASP.NET) 線上教學影片 https://www.youtube.com/channel/UC6IPPf6tvsNG8zX3u1LddvA/
Blog文章 "附的範例" 無法下載,請看 https://dotblogs.com.tw/mis2000lab/2016/03/14/2008_2015_mis2000lab_sample_download
請看我們的「售後服務」範圍(嚴格認定)。
......................................................................................................................................................
ASP.NET MVC => .NET Core MVC 線上教學 ...... 第一天課程 完整內容 "免費"讓您評估 / 試聽

[遠距教學、教學影片] ASP.NET (Web Form) 課程 上線了!MIS2000Lab.主講 事先錄好的影片,並非上課側錄! 觀看時,有如「一對一」面對面講課。
