傳統 Response.Write("") 會在 </html> **之後**輸出腳本,導致:
正解:使用 Page.ClientScript.RegisterStartupScript / ScriptManager.RegisterStartupScript (AJAX),在 </body> 前正確註冊腳本。
using System;
using System.Text;
using System.Web;
using System.Web.UI;
///
/// JavaScript 輸出助手 - 基於 ClientScriptManager,符合 HTML 標準
///
public static class JScript
{
#region 核心私有方法
private static void Register(Page page, string key, string script)
{
if (page == null) throw new ArgumentNullException(nameof(page));
if (page.ClientScript.IsStartupScriptRegistered(page.GetType(), key)) return;
page.ClientScript.RegisterStartupScript(page.GetType(), key, script);
}
private static void RegisterAjax(Control control, string key, string script)
{
if (control == null) throw new ArgumentNullException(nameof(control));
if (ScriptManager.GetCurrent(control.Page)?.IsInAsyncPostBack == true)
ScriptManager.RegisterStartupScript(control, control.GetType(), key, script, true);
else
Register(control.Page, key, script);
}
#endregion
#region 警告與導向
public static void Alert(Page page, string message) =>
Register(page, "alert", $@"alert({JsonEncode(message)});");
public static void Alert(Control control, string message) =>
RegisterAjax(control, "alert", $@"alert({JsonEncode(message)});");
public static void AlertAndRedirect(Page page, string message, string url) =>
Register(page, "alertRedirect", $@"alert({JsonEncode(message)}); window.location.replace({JsonEncode(url)});");
public static void AlertAndRedirect(Control control, string message, string url) =>
RegisterAjax(control, "alertRedirect", $@"alert({JsonEncode(message)}); window.location.replace({JsonEncode(url)});");
public static void Redirect(Page page, string url) =>
Register(page, "redirect", $@"window.location.replace({JsonEncode(url)});");
public static void Redirect(Control control, string url) =>
RegisterAjax(control, "redirect", $@"window.location.replace({JsonEncode(url)});");
#endregion
#region 視窗操作
public static void CloseWindow(Page page) =>
Register(page, "closeWin", @"window.close();");
public static void RefreshParent(Page page, string url) =>
Register(page, "refreshParent", $@"window.opener.location.href = {JsonEncode(url)}; window.close();");
public static void RefreshOpener(Page page) =>
Register(page, "refreshOpener", @"window.opener.location.reload(); window.close();");
public static void OpenWindow(Page page, string url, int width, int height, int left = 0, int top = 0, string name = "") =>
Register(page, "openWin", $@"window.open({JsonEncode(url)}, {JsonEncode(name)}, 'width={width},height={height},left={left},top={top},resizable=yes,scrollbars=yes,status=yes');");
#endregion
#region 歷史導航
public static void GoBack(Page page, int steps = -1) =>
Register(page, "goback", $@"history.go({steps});");
#endregion
#region 模態對話框 (IE 專屬,現代建議用 Modal Dialog Polyfill)
public static void ShowModalDialog(Page page, string url, int width, int height, int left, int top) =>
Register(page, "modalDlg", $@"showModalDialog({JsonEncode(url)}, '', 'dialogWidth:{width}px;dialogHeight:{height}px;dialogLeft:{left}px;dialogTop:{top}px;center:yes;help:no;resizable:no;status:no;scroll:yes');");
#endregion
#region JSON 編碼 (防 XSS、語法錯誤)
private static string JsonEncode(string s)
{
if (string.IsNullOrEmpty(s)) return "\"\"";
var sb = new StringBuilder(s.Length + 4);
sb.Append('"');
foreach (char c in s)
{
switch (c)
{
case '"': sb.Append("\\\""); break;
case '\\': sb.Append("\\\\"); break;
case '\b': sb.Append("\\b"); break;
case '\f': sb.Append("\\f"); break;
case '\n': sb.Append("\\n"); break;
case '\r': sb.Append("\\r"); break;
case '\t': sb.Append("\\t"); break;
default:
if (char.IsControl(c)) sb.Append($"\\u{(int)c:x4}");
else sb.Append(c);
break;
}
}
sb.Append('"');
return sb.ToString();
}
#endregion
}
public partial class UserEdit : Page
{
protected void btnSave_Click(object sender, EventArgs e)
{
if (SaveUser())
{
JScript.AlertAndRedirect(this, "儲存成功!", "UserList.aspx");
}
else
{
JScript.Alert(this, "儲存失敗,請重試。");
}
}
}
public partial class AjaxPage : Page
{
protected void btnAjax_Click(object sender, EventArgs e)
{
// 此時必須用 Control 多載,自動偵測 AJAX 並用 ScriptManager 註冊
JScript.Alert(btnAjax, "AJAX 操作完成!");
JScript.RefreshOpener(btnAjax); // 關閉彈出視窗並重新整理父頁
}
}
public static class ScriptHelper
{
// 註冊外部 JS (自動去重、依賴順序)
public static void RegisterScriptFile(Page page, string key, string url, string[] dependencies = null)
{
if (page.ClientScript.IsClientScriptIncludeRegistered(key)) return;
page.ClientScript.RegisterClientScriptInclude(key, ResolveUrl(url));
}
// 註冊模組化啟動腳本 (ES6 模組)
public static void RegisterModuleScript(Page page, string key, string moduleScript)
{
var script = $@"";
page.ClientScript.RegisterStartupScript(page.GetType(), key, script, false); // false = 不加 script 標籤
}
}
若網站啟用 Content-Security-Policy: script-src 'self',**內聯腳本會被阻擋**。解決方案:
nonce-{value}。.js 檔案,透過 RegisterClientScriptInclude 引用。'sha256-...'。// Nonce 範例 (需在 MasterPage/Layout 產生)
public static void RegisterWithNonce(Page page, string key, string script, string nonce)
{
var wrapped = $@"";
page.ClientScript.RegisterStartupScript(page.GetType(), key, wrapped, false);
}
| 錯誤 | 原因 | 修正 |
|---|---|---|
| 腳本不執行 | Key 重複導致略過註冊 | 每個呼叫點使用唯一 Key (如 alert_{Guid} 或方法名) |
| AJAX 下腳本不執行 | 用 Page.ClientScript 而非 ScriptManager | 統一用 ScriptManager.RegisterStartupScript 或工具類自動偵測 |
| 中文亂碼 / 語法錯誤 | 字串未 JSON 編碼 | 務必透過 JsonEncode 處理所有動態字串 |
| CSP 阻擋 | 內聯腳本無 nonce/hash | 改用外部 .js 或加入 nonce |
| 巢狀 MasterPage 找不到 ClientScript | this.Page 非最上層 Page | 傳入 Page 參數而非 this |
對於新專案,建議**後端僅輸出 JSON 資料**,前端由 Vue/React/Alpine.js 接管互動:
// Controller
public JsonResult GetUser(int id) => Json(userService.GetDto(id));
// View (Alpine.js)
載入中...
此架構**徹底分離關注點**,後端無需輸出任何 JavaScript,安全性與可維護性大幅提升。