【重要更新】XIUNOX jQuery 依赖移除与原生 JS 迁移指南

管理

jQuery 依赖移除与原生 JS 迁移指南

自 2026-07-24 起,XIUNOX 已系统性移除所有 jQuery 依赖。本文档记录迁移原则、关键修复页面改造规范、以及 jQuery → 原生 JS API 对照表,供插件开发者参考。


一、背景

一、安全漏洞风险(XIUNOX 重构首要解决痛点)

  1. 大范围 XSS 跨站脚本漏洞

    旧版 jQuery(3.5.0 以下,Xiuno 原版长期捆绑老旧 jQuery)的.html().append()$()动态 HTML 解析存在缺陷,攻击者可构造带onerror/onclick恶意事件标签注入脚本,窃取用户 Cookie、劫持会话,对应 CVE-2020-11022/11023 高危漏洞CSDN博...

    论坛大量弹窗、AJAX 加载帖子、评论渲染均依赖该 API,原生前端过滤极易被绕过。

  2. 原型污染漏洞

    3.4.0 前版本$.extend对象合并逻辑缺陷,传入含__proto__恶意参数可污染全局 JS 原型,篡改站点全局逻辑,存在后台越权风险LTD枢纽...

  3. AJAX 接口缺少标准化安全防护

    原版$.ajax调用无统一 CSRF 校验封装,大量插件、前台提交接口遗漏令牌校验,容易触发 CSRF 跨站请求伪造;XIUNOX 重构强制用htmx/fetch替代$.ajax统一携带 CSRF 令牌,消除该隐患稀土掘金

  4. 第三方依赖攻击面扩大

    jQuery 是全站强制引入的第三方 JS 库,只要存在未修复版本,全站所有页面共享漏洞;各类第三方插件还会重复打包不同版本 jQuery,版本混乱更难统一修复。

二、性能与加载损耗问题

  1. 冗余体积拖慢页面加载

    jQuery 压缩后仍有约 30KB,每个页面强制加载,增加首屏请求体积、延长关键渲染路径;移动端弱网场景下延迟明显,现代浏览器原生querySelector/fetch/classList可完全替代,无需额外库开销highfivest...

  2. DOM 操作双层抽象损耗

    jQuery 选择器、DOM 方法会先内部解析语法、封装一层逻辑再调用原生 API,频繁渲染帖子列表、动态加载分页时,比原生 JS 执行效率更低;论坛海量帖子、评论动态渲染场景性能短板突出。

  3. 事件绑定内存泄漏隐患

    动态刷新内容时,若未手动.off()解绑 jQuery 事件,会重复堆积事件监听,长期浏览页面造成内存持续上涨,出现弹窗关闭后滚动异常、按钮点击失效等 BUG(Xiuno 原版已知 UI 缺陷)Programmin...

三、技术栈兼容与升级阻碍

  1. Bootstrap 5 彻底抛弃 jQuery 依赖

    XIUNOX 升级 Bootstrap5.3 后,原有基于 jQuery 的弹窗、下拉、分页组件全部失效,两套 DOM 操作 API 并存(jQuery $ + 原生 JS),模板、插件代码大量冲突,必须逐行清理 jQuery 调用代码稀土掘金

  2. 插件生态分裂,兼容成本极高

    论坛存量插件分两类:老插件重度依赖$$.ajax;新现代化插件使用 HTMX / 原生 JS。官方开发插件兼容性扫描器专门检测$.ajax(、jQuery 组件调用,批量标记不兼容代码,重构时需要逐个改造所有插件,维护成本巨大稀土掘金

  3. 新旧浏览器兼容逻辑累赘

    旧 jQuery 内置大量 IE 兼容代码,而 XIUNOX 目标环境为现代浏览器 + PHP8,冗余兼容代码无使用价值,徒增代码体积;新版 jQuery4.0 大幅删减旧浏览器兼容,但升级又会引发大量 API 破坏性变更。

  4. 前后端现代化架构不匹配

    XIUNOX 采用 HTMX 无刷新、RESTful API 现代化架构,HTMX 原生基于fetch,和$.ajax回调式写法割裂,同时 jQuery 回调地狱不利于异步逻辑维护,无法统一异步请求规范。

四、项目长期维护与代码架构缺陷

  1. 命令式 DOM 代码难以维护

    jQuery 是命令式操作,代码散落在各处直接操作 DOM,无组件化、数据驱动逻辑;论坛弹窗、分页、评论加载代码碎片化,形成 “面条代码”,新增功能、修复 BUG 时极易产生联动问题稀土掘金

  2. 版本升级破坏性变更多

    jQuery 大版本(1.x→3.x→4.x)存在大量废弃 API、行为修改:自动补全 px 单位、选择器语法、Deferred 异步逻辑全部改动;Xiuno 原版基于老旧 1.x/2.x,升级需要全站模板、JS 脚本逐行适配,工作量极大。

  3. ** 全局

    \(变量污染** jQuery挂载全\) 变量,插件、自定义脚本容易出现命名冲突,引发随机脚本失效;模块化 ES6 环境下和原生变量、其他库冲突概率更高。

历史路径

  1. 1.1.3及之前版本兼容:项目内置 jQuery 兼容 (view/js/xiuno-modern.js 暴露 window.jQuery = $),让 20 个存量插件 JS 和 .htm 模板内联 $ 代码继续工作。新插件代码强制用 XN.* API。

  2. 1.1.4版本移除:系统性移除所有 jQuery 依赖。68 个文件中的 $/XN.confirm 调用全部改造为原生 JS。关键修复页面(在线升级/数据库升级/后台登录等)已完全不依赖 xiuno-modern.js,使用原生 fetch + confirm + Web API 实现,避免「网站坏 → 修复页面也坏 → 无法修复」的死循环。

设计原则

  • 零外部依赖:关键修复页面(在线升级、数据库升级、后台登录、系统工具)禁止依赖 xiuno-modern.js$/XN shim,使用原生 fetch + confirm + querySelectorAll 等 Web API

  • 原生 JS 优先:新代码强制使用原生 JS + htmx 4 属性,禁止使用 $/jQuery/$.fn.*

  • XN API 保留XN.toast() / XN.ajax() / XN.confirm() / XN.alert() 等高层 API 仍在 xiuno-modern.js 中提供,非关键页面可继续使用(前后台 footer.inc.htm 已全局加载 xiuno-modern.js

  • 安全兜底:调用 XN.confirm / XN.confirmCreditsDeduct 等异步 API 时必须加 typeof 守卫 + try-catch,避免 shim 加载失败导致按钮永久禁用


二、关键修复页面规范

定义:用户修复网站问题的「最后手段」页面。旧版本站点可能没有 xiuno-modern.js 或该文件加载失败时,这些页面必须仍然能工作。

关键修复页面清单

文件

用途

admin/view/htm/online_upgrade.htm

在线升级(检查更新、下载、应用)

admin/view/htm/upgrade.htm

数据库结构升级

admin/view/htm/index_login.htm

后台登录

admin/view/htm/plugin_list.htm

插件管理(启用/禁用/卸载)

admin/view/htm/other_cache_setting.htm

系统工具(清缓存)

关键页面改造检查表

  • 内联 JS 使用原生 fetch + querySelectorAll + confirm,不依赖 $/XN

    • AJAX 请求用自实现的 ajax() 函数(基于 fetch),不调用 $.ajax/XN.ajax

    • CSRF token 通过 PHP json_encode 注入 JS 变量,请求中显式传递(header + body 双保险)

    • 动态内容插入 DOM 前用 escapeHtml() 转义,防止 XSS

    • Toast 提示独立实现,不依赖 XN.toast(用 #toast-containeralert 兜底)

    • 按钮状态管理用原生 disabled + innerHTML,不依赖 $.fn.button('loading')

    • 事件监听用 addEventListener,不依赖 $.fn.on

范式:原生 ajax 函数

JavaScript

// ponytail: 关键修复页面必须独立于 xiuno-modern.js
function buildQuery(data) {
    var parts = [];
    for (var k in data) {
        if (Object.prototype.hasOwnProperty.call(data, k)) {
            parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(data[k]));
        }
    }
    return parts.join('&');
}

function ajax(opts) {
    var url = opts.url;
    var type = (opts.type || 'GET').toUpperCase();
    var data = opts.data || null;
    var dataType = opts.dataType || 'json';

    var fetchOpts = {
        method: type,
        headers: {
            'X-Requested-With': 'XMLHttpRequest',
            'X-CSRF-Token': csrfToken
        },
        credentials: 'same-origin'
    };

    if (type === 'POST' && data) {
        if (data instanceof FormData) {
            fetchOpts.body = data;
        } else {
            fetchOpts.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
            fetchOpts.body = typeof data === 'object' ? buildQuery(data) : data;
        }
    } else if (data && !(data instanceof FormData)) {
        var sep = url.indexOf('?') === -1 ? '?' : '&';
        url += sep + buildQuery(data);
    }

    fetch(url, fetchOpts).then(function(response) {
        return response.text().then(function(text) {
            var xhrLike = { status: response.status, responseText: text };
            if (!response.ok) throw xhrLike;
            if (dataType === 'json') {
                try { return JSON.parse(text); }
                catch (e) { throw xhrLike; }
            }
            return text;
        });
    }).then(function(res) {
        if (opts.success) opts.success(res);
    }).catch(function(err) {
        var xhr = err && err.responseText !== undefined ? err : { responseText: (err && err.message) ? err.message : String(err || '') };
        if (opts.error) opts.error(xhr);
    });
}

范式:独立 Toast 函数

JavaScript

// ponytail: 不依赖 xiuno-modern.js 的 showToast/XN.toast
// 复用 footer.inc.htm 已渲染的 #toast-container(若无则 fallback 到 alert)
function showToast(message, type) {
    var container = document.getElementById('toast-container');
    if (!container) { alert(message); return; }
    var toastEl = document.createElement('div');
    toastEl.className = 'toast show align-items-center text-white bg-' + (type === 'success' ? 'success' : (type === 'error' ? 'danger' : 'secondary'));
    toastEl.setAttribute('role', 'alert');
    var body = document.createElement('div');
    body.className = 'd-flex';
    var inner = document.createElement('div');
    inner.className = 'toast-body';
    inner.textContent = message;
    body.appendChild(inner);
    var btn = document.createElement('button');
    btn.type = 'button';
    btn.className = 'btn-close btn-close-white me-2 m-auto';
    btn.setAttribute('data-bs-dismiss', 'toast');
    btn.onclick = function() { toastEl.remove(); };
    body.appendChild(btn);
    toastEl.appendChild(body);
    container.appendChild(toastEl);
    setTimeout(function() { if (toastEl.parentNode) toastEl.remove(); }, 4000);
}

三、jQuery → 原生 JS API 对照表

选择器

jQuery

原生 JS

备注

$('#id')

document.getElementById('id')

返回单个元素

$('.class')

document.querySelectorAll('.class')

返回 NodeList

$('input[name="x"]')

document.querySelector('input[name="x"]')

单个

$(els).each(fn)

els.forEach(fn)

NodeList 支持 forEach

$(els).on('click', fn)

els.forEach(el => el.addEventListener('click', fn))

直接绑定

$(document).on('click', '.btn', fn)

document.addEventListener('click', e => { if (e.target.closest('.btn')) fn(e); })

事件委托

AJAX

jQuery

原生 JS

备注

$.ajax({url, type, data, success, error})

自实现 ajax() 函数(见上范式)

基于 fetch

$.post(url, data, cb)

fetch(url, {method:'POST', body:new URLSearchParams(data)})

简单场景

$.get(url, cb)

fetch(url).then(r => r.json()).then(cb)

简单场景

$.param({a:[1,2]})

自实现 buildQuery()(支持 ids[]=1&ids[]=2

PHP 风格数组

jform.serialize()

new FormData(jform)new URLSearchParams(new FormData(jform)).toString()

表单序列化

DOM 操作

jQuery

原生 JS

备注

$(el).html(s)

el.innerHTML = s

注意 XSS,需先 escapeHtml

$(el).text(s)

el.textContent = s

安全(自动转义)

$(el).val()

el.value

读取

$(el).val(s)

el.value = s

设置

$(el).attr('name')

el.getAttribute('name')

$(el).attr('name', v)

el.setAttribute('name', v)

$(el).data('key')

el.dataset.key

HTML5 data-*

$(el).addClass('c')

el.classList.add('c')

$(el).removeClass('c')

el.classList.remove('c')

$(el).hasClass('c')

el.classList.contains('c')

$(el).toggleClass('c')

el.classList.toggle('c')

$(el).show()

el.style.display = ''

$(el).hide()

el.style.display = 'none'

$(el).append(child)

el.appendChild(child)

$(el).remove()

el.parentNode.removeChild(el)

$(el).empty()

el.innerHTML = ''

$(el).find(sel)

el.querySelectorAll(sel)

$(el).closest(sel)

el.closest(sel)

原生支持

事件

jQuery

原生 JS

备注

$(el).on('click', fn)

el.addEventListener('click', fn)

$(el).off('click', fn)

el.removeEventListener('click', fn)

$(el).trigger('click')

el.dispatchEvent(new Event('click'))

e.preventDefault()

e.preventDefault()

一致

e.stopPropagation()

e.stopPropagation()

一致

return false in handler

e.preventDefault(); e.stopPropagation();

原生 return false 不阻止默认行为

工具函数

jQuery

原生 JS

备注

$.each(arr, fn)

arr.forEach(fn)

$.extend({}, a, b)

Object.assign({}, a, b)

$.inArray(v, arr)

arr.indexOf(v)

$.isArray(v)

Array.isArray(v)

$.type(v)

typeof vArray.isArray

$.trim(s)

s.trim()

$.now()

Date.now()

Bootstrap 组件桥接

jQuery

原生 JS

备注

$('#modal').modal('show')

new bootstrap.Modal('#modal').show()

$('#modal').modal('hide')

bootstrap.Modal.getInstance(document.getElementById('modal')).hide()

$('#dropdown').dropdown('toggle')

new bootstrap.Dropdown('#dropdown').toggle()

$('#tab').tab('show')

new bootstrap.Tab('#tab').show()

$('#tooltip').tooltip()

new bootstrap.Tooltip('#tooltip')


四、异步确认弹窗的兜底保护

XN.confirm / XN.confirmCreditsDeduct 等异步 API 在 xiuno-modern.js 加载失败时会抛 TypeError。若在 htmx:confirm 事件中调用且未加保护,会导致 window._htmxConfirmAsync 标志位卡在 true,提交按钮永久禁用。

范式:带兜底的异步确认

JavaScript

form.addEventListener('htmx:confirm', function(e) {
    e.preventDefault();
    // ponytail: 兜底保护——若 XN 不可用,直接放行请求
    if (typeof XN === 'undefined' || typeof XN.confirmCreditsDeduct !== 'function') {
        e.detail.issueRequest();
        return;
    }
    window._htmxConfirmAsync = true;
    try {
        XN.confirmCreditsDeduct(creditsEvent, fid, function() {
            window._htmxConfirmAsync = false;
            e.detail.issueRequest();
        }, { onCancel: function() {
            window._htmxConfirmAsync = false;
            e.detail.dropRequest();
            if (window.resetPostSubmit) window.resetPostSubmit();
        } });
    } catch(err) {
        // ponytail: 抛异常时必须重置标志并恢复按钮
        window._htmxConfirmAsync = false;
        if (window.resetPostSubmit) window.resetPostSubmit();
        if (typeof console !== 'undefined' && console.error) console.error('confirm failed:', err);
    }
});

五、XSS 防护:escapeHtml 函数

动态内容插入 innerHTML 前必须转义。关键修复页面不依赖 XN.escapeHtml,自实现:

JavaScript

function escapeHtml(s) {
    if (s === null || s === undefined) return '';
    return String(s).replace(/[&<>"']/g, function(c) {
        return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];
    });
}

// 使用示例
resultEl.innerHTML = escapeHtml(res.message);
detailEl.innerHTML = '<div>' + icon + ' ' + escapeHtml(r.name) + '</div>';

非关键页面可继续使用 XN.escapeHtml(untrustedString)xiuno-modern.js 提供)。


六、迁移检查清单

插件 JS 文件迁移

逐项核对:

  • Grep 旧文件所有 $ 调用:$.ajax/$.xpost/$.get/$.post/$.each/$.param/$.fn.*/$('...')

    • 每个调用替换为原生 JS 等价实现(参见对照表)

    • XN.confirm / XN.alert 改为原生 confirm / alert(或保留 XN.confirm 但加 typeof 守卫)

    • 表单序列化用 new FormData(form)new URLSearchParams

    • 事件委托改为 document.addEventListener('click', e => { if (e.target.closest(sel)) ... })

    • 确认无 <script src> 引用旧 jQuery 文件

    • 修改后清 tmp/ 缓存,提示用户硬刷新

关键修复页面额外检查

  • $.ajax / $() / $.each / XN.confirm 等依赖

    • CSRF token 通过 PHP json_encode 注入 JS 变量

    • AJAX 请求显式传递 CSRF token(header + body 双保险)

    • 动态内容插入 DOM 前用 escapeHtml() 转义

    • Toast 提示独立实现,有 alert 兜底

    • 按钮状态管理用原生 disabled + innerHTML


七、常见迁移陷阱

1. return false 在原生事件中不阻止默认行为

JavaScript

// ❌ jQuery 写法:return false 等价于 preventDefault + stopPropagation
$(el).on('submit', function() { ... return false; });

// ✅ 原生写法:必须显式调用
el.addEventListener('submit', function(e) {
    e.preventDefault();
    e.stopPropagation();
    // ...
});

项目的 $ shim 已在 $.fn.on 内部包装 handler,检测 ret === false 时调 e.preventDefault() + e.stopPropagation()。但新代码仍建议显式接收 e 参数并调用 e.preventDefault()(双保险)。

2. $.param 数组序列化格式

JavaScript

// ❌ jQuery $.param({ids: [1,2,3]}) → "ids[]=1&ids[]=2&ids[]=3"
// 原生 URLSearchParams 会把数组隐式 toString 为 "1,2,3"

// ✅ 自实现 buildQuery 支持 PHP 风格数组
function buildQuery(data) {
    var parts = [];
    function build(prefix, val) {
        if (Array.isArray(val)) {
            for (var i = 0; i < val.length; i++) build(prefix + '[]', val[i]);
        } else if (typeof val === 'object') {
            for (var k in val) {
                if (Object.prototype.hasOwnProperty.call(val, k)) {
                    build(prefix ? prefix + '[' + k + ']' : k, val[k]);
                }
            }
        } else {
            parts.push(encodeURIComponent(prefix) + '=' + encodeURIComponent(val === null || val === undefined ? '' : val));
        }
    }
    build('', data);
    return parts.join('&').replace(/%20/g, '+');
}

3. FormData 不能预设 Content-Type

JavaScript

// ❌ 错误:手动设 Content-Type 会丢失 boundary
fetchOpts.headers['Content-Type'] = 'multipart/form-data';
fetchOpts.body = formData;

// ✅ 正确:不设 Content-Type,浏览器自动加 boundary
if (data instanceof FormData) {
    fetchOpts.body = data;
    // 不要设 Content-Type
}

4. NodeList.forEach 兼容性

JavaScript

// ✅ 现代浏览器支持
document.querySelectorAll('.btn').forEach(function(el) { ... });

// ✅ 兼容旧浏览器
var els = document.querySelectorAll('.btn');
Array.prototype.forEach.call(els, function(el) { ... });

5. 异步确认弹窗的按钮状态管理

htmx:confirm 事件中调用异步 API(如 XN.confirm)时,必须设置 window._htmxConfirmAsync = true 阻止 setTimeout(0) 提前恢复按钮。回调/取消/异常时必须重置为 false。详见第四节范式。


对 Xiuno 这类传统论坛系统而言,jQuery 核心矛盾是:遗留安全漏洞无法彻底根除、拖累页面性能、阻碍 Bootstrap5/PHP8/HTMX 现代化技术栈升级、插件生态维护成本极高、代码架构落后难以长期迭代,因此 XIUNOX 重构方案选择完全移除 jQuery,改用原生现代前端方案。

最新回复

请先登录后再回复 登录

uid:1 管理
关注
随遇而安,随缘而行
发帖 126
评论 447
粉丝 13
关注 1
发新帖
目录
【重要更新】XIUNOX jQuery 依赖移除与原生 JS 迁移指南
此广告位招租
此广告位招租
此广告位招租
广告招租
广告招租
广告招租
广告招租
广告招租
广告招租