首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> ASP.NET 基礎 >> ASP.NET 基礎

ASP.NET 輸出 JavaScript 腳本最佳實踐:ClientScriptManager 取代 Response.Write 完整重構

     編輯:ASP.NET 基礎

核心問題:Response.Write 破壞 HTML 結構

傳統 Response.Write("") 會在 </html> **之後**輸出腳本,導致:

  • HTML 文件不合法 (W3C 驗證失敗)。
  • CSP (Content-Security-Policy) 可能阻擋內聯腳本。
  • 頁面載入時序不可控,易發生「找不到控制項」錯誤。

正解:使用 Page.ClientScript.RegisterStartupScript / ScriptManager.RegisterStartupScript (AJAX),在 </body> 前正確註冊腳本。

一、JScript 工具類完整重構版

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
}

二、使用範例

一般 PostBack 頁面

public partial class UserEdit : Page
{
    protected void btnSave_Click(object sender, EventArgs e)
    {
        if (SaveUser())
        {
            JScript.AlertAndRedirect(this, "儲存成功!", "UserList.aspx");
        }
        else
        {
            JScript.Alert(this, "儲存失敗,請重試。");
        }
    }
}

UpdatePanel 非同步 PostBack

public partial class AjaxPage : Page
{
    protected void btnAjax_Click(object sender, EventArgs e)
    {
        // 此時必須用 Control 多載,自動偵測 AJAX 並用 ScriptManager 註冊
        JScript.Alert(btnAjax, "AJAX 操作完成!");
        JScript.RefreshOpener(btnAjax); // 關閉彈出視窗並重新整理父頁
    }
}

三、進階:註冊外部 .js 檔案與模組化腳本

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 標籤
    }
}

四、CSP (Content-Security-Policy) 相容性

若網站啟用 Content-Security-Policy: script-src 'self',**內聯腳本會被阻擋**。解決方案:

  1. Nonce 策略:每請求產生隨機 Nonce,註冊腳本時附加 nonce-{value}。
  2. 外部化腳本:將常用邏輯打包成 .js 檔案,透過 RegisterClientScriptInclude 引用。
  3. Hash 策略:固定腳本內容計算 SHA256,加入 CSP '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 找不到 ClientScriptthis.Page 非最上層 Page傳入 Page 參數而非 this

六、現代化替代方案:前端框架整合

對於新專案,建議**後端僅輸出 JSON 資料**,前端由 Vue/React/Alpine.js 接管互動:

// Controller
public JsonResult GetUser(int id) => Json(userService.GetDto(id));

// View (Alpine.js)

此架構**徹底分離關注點**,後端無需輸出任何 JavaScript,安全性與可維護性大幅提升。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved