BWiki JS 编写实践

JavaScript(JS)总览与最佳实践


快速上手(5 分钟)

1. 选择载入方式

先选方式,再写代码——不同方式约束不同:

载入方式用途语法限制适用场景缓存说明
MediaWiki:Common.js全站通用行为ES5 静态检查小而稳的全站增强约半小时缓存
Gadget(小工具)可开关/按需启用ES5 静态检查功能模块化、分人群启用约半小时缓存
Widget 内嵌脚本页面/模板局部功能无 ES5 检查单页组件、实验页面缓存
用户脚本(油猴)个人自用现代语法开发调试、个性化浏览器控制

注意:Bwiki 已关闭内置用户脚本功能,但可通过油猴脚本(Tampermonkey)实现类似效果

2. 核心执行流程

  1. 等 DOM 就绪(DOMContentLoaded)
  2. 如需 jQuery/API 等 MediaWiki 组件,用 ResourceLoader 等其加载
  3. 处理后续注入内容(mw.hook)
  4. 确保只绑定一次(避免重复执行)

参考手册

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/const
  • function(){} 代替箭头函数
  • 字符串拼接 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. 载入方式选择决策

  1. 只改某个页面或模板? → 用 Widget(局部、可迭代)
  2. 面向特定人群/可开关? → 用 Gadget(模块化、可治理)
  3. 确实是全站通用、非常稳定的增强? → 少量放 Common.js
  4. 还在试验? → 用 油猴脚本,验证后再上 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
  • 个人试验 → 使用油猴脚本

核心原则

  1. 先判断是否需要 ResourceLoader
  2. 确保 DOM 就绪后再操作
  3. 代码可安全重复执行
  4. 遵守语法约束(ES5 或浏览器兼容性)