JavaScript(JS)总览与最佳实践
快速上手(5 分钟)
1. 选择载入方式
先选方式,再写代码——不同方式约束不同:
| 载入方式 | 用途 | 语法限制 | 适用场景 | 缓存说明 |
|---|---|---|---|---|
| MediaWiki:Common.js | 全站通用行为 | ES5 静态检查 | 小而稳的全站增强 | 约半小时缓存 |
| Gadget(小工具) | 可开关/按需启用 | ES5 静态检查 | 功能模块化、分人群启用 | 约半小时缓存 |
| Widget 内嵌脚本 | 页面/模板局部功能 | 无 ES5 检查 | 单页组件、实验 | 页面缓存 |
| 用户脚本(油猴) | 个人自用 | 现代语法 | 开发调试、个性化 | 浏览器控制 |
注意:Bwiki 已关闭内置用户脚本功能,但可通过油猴脚本(Tampermonkey)实现类似效果
2. 核心执行流程
- 等 DOM 就绪(DOMContentLoaded)
- 如需 jQuery/API 等 MediaWiki 组件,用 ResourceLoader 等其加载
- 处理后续注入内容(mw.hook)
- 确保只绑定一次(避免重复执行)
参考手册
1. ResourceLoader(RL)使用
何时需要 ResourceLoader:
- 需要 jQuery 选择器操作
- 需要使用 MediaWiki API
- 需要其他 MediaWiki 扩展模块
基础用法:
// 等待 ResourceLoader 和 jQuery
window.RLQ = window.RLQ || [];
window.RLQ.push(function () {
mw.loader.using(['jquery']).then(function () {
// 这里可以安全使用 jQuery
jQuery(function() {
// DOM 就绪后的逻辑
});
});
});
// 简单脚本(不需要 MediaWiki 组件)
document.addEventListener('DOMContentLoaded', function() {
// 直接操作 DOM
});2. DOM 就绪检测
// 原生方式(推荐)
document.addEventListener('DOMContentLoaded', function() {
// DOM 已就绪
}, { once: true });
// jQuery 方式(需要先加载 jQuery)
jQuery(function() {
// DOM 已就绪
});3. ES5 静态检查约束
适用于:Common.js & Gadget
禁止语法:
let/const、箭头函数()=>{}、类class- 模板字符串
`x${y}` - 顶层
await、模块化import/export - 新 API(需自备 Polyfill)
替代方案:
var代替let/constfunction(){}代替箭头函数- 字符串拼接
str + y代替模板字符串 - IIFE +
'use strict'做作用域控制
Widget/动态脚本无此限制,但需自行评估浏览器支持
4. 内容注入处理
// 处理后续动态加载的内容
mw.hook('wikipage.content').add(function($content) {
// 只处理新注入的元素
$content.find('.btn').not('[data-bound]').attr('data-bound', '1')
.on('click', handler);
});5. 避免重复绑定
简单方案(数据标记):
function bindOnce(selector, event, handler) {
document.querySelectorAll(selector + ':not([data-bound])')
.forEach(function(el) {
el.setAttribute('data-bound', '1');
el.addEventListener(event, handler);
});
}6. 性能优化
- 事件委托:减少监听器数量
- 节流/防抖:高频事件优化
- 避免布局抖动:合并 DOM 操作
- 分模块:功能分离,便于维护
7. 安全规范
- 禁止使用
eval、注入第三方不受控脚本 - 跨域请求遵守同源策略
- 输出到 DOM 前转义或构造安全节点
操作指南
A. 载入方式选择决策
- 只改某个页面或模板? → 用 Widget(局部、可迭代)
- 面向特定人群/可开关? → 用 Gadget(模块化、可治理)
- 确实是全站通用、非常稳定的增强? → 少量放 Common.js
- 还在试验? → 用 油猴脚本,验证后再上 Gadget/Widget
B. 基础代码模板
ES5 安全模板(Common.js/Gadget):
/* eslint-env es5 */
(function () {
'use strict';
function init() {
// 确保只执行一次
document.querySelectorAll('.btn:not([data-bound])')
.forEach(function(btn) {
btn.setAttribute('data-bound', '1');
btn.addEventListener('click', function() {
alert('clicked');
});
});
}
// 等 DOM 就绪
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
// 处理后续注入内容(如果需要)
if (typeof mw !== 'undefined' && mw.hook) {
mw.hook('wikipage.content').add(function($content) {
$content.find('.btn:not([data-bound])')
.attr('data-bound', '1')
.on('click', function() { alert('clicked'); });
});
}
})();现代语法模板(Widget/油猴):
<includeonly><script>
(() => {
'use strict';
const init = () => {
document.querySelectorAll('.btn:not([data-bound])')
.forEach(btn => {
btn.dataset.bound = '1';
btn.addEventListener('click', () => alert('clicked'));
});
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();
</script></includeonly>C. API 读取示例
// 需要 ResourceLoader
window.RLQ = window.RLQ || [];
window.RLQ.push(function () {
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
api.get({
action: 'query',
prop: 'extracts',
titles: mw.config.get('wgPageName'),
explaintext: 1
}).done(function (data) {
console.log(data);
}).fail(function (err) {
console.warn('API error:', err);
});
});
});D. 事件委托优化
document.addEventListener('click', function (e) {
// 使用事件委托,避免重复绑定
if (e.target.matches('.btn') || e.target.closest('.btn')) {
// 处理点击
}
});常见问题
Q1:为什么箭头函数在 Common.js 报错?
A:Common.js/Gadget 受 ES5 静态检查,请改用 function(){} 和 var
Q2:ResourceLoader 是必须的吗? A:不是必须。只有需要 jQuery、API 等 MediaWiki 组件时才需要
Q3:代码修改后为什么不立即生效? A:Common.js 和 Gadget 有约半小时缓存,Widget 依赖页面缓存
Q4:用户脚本怎么用? A:Bwiki 已关闭内置用户脚本,但可通过油猴脚本实现类似功能
Q5:同一元素被多次绑定事件? A:确保代码幂等,使用数据标记或事件委托
实用代码片段
ES5 节流函数:
function throttle(fn, wait) {
var last = 0, timer = null;
return function () {
var now = Date.now(), args = arguments, ctx = this;
if (now - last >= wait) {
last = now; fn.apply(ctx, args);
} else {
clearTimeout(timer);
timer = setTimeout(function(){ last = Date.now(); fn.apply(ctx, args); }, wait - (now - last));
}
};
}安全创建元素:
function createElement(tag, attrs, text) {
var el = document.createElement(tag);
for (var key in attrs) {
if (attrs.hasOwnProperty(key)) {
el.setAttribute(key, attrs[key]);
}
}
if (text) el.textContent = text;
return el;
}下一步行动
- 只改”某页” → 新建 Widget(
<includeonly><script>…</script></includeonly>) - 可复用功能 → 建 Gadget(在
MediaWiki:Gadgets-definition登记) - 全站极小增强 → 慎重放到 Common.js
- 个人试验 → 使用油猴脚本
核心原则:
- 先判断是否需要 ResourceLoader
- 确保 DOM 就绪后再操作
- 代码可安全重复执行
- 遵守语法约束(ES5 或浏览器兼容性)