| name | userscript |
| description | 编写User.JS用户脚本的技能 |
用户脚本(UserScript)开发技能
本技能文档基于作者的实际脚本编写经验,包含Tampermonkey脚本开发的核心知识、编码规范和最佳实践。
目录
- 脚本结构规范
- 元数据配置
- 编码风格指南
- 常用工具函数
- GM_* API 使用
- DOM 操作技巧
- 库文件使用
- 调试技巧
- 最佳实践
脚本结构规范
基本结构模板
(function(unsafeWindow, document) {
'use strict';
const wait = t => new Promise(r => setTimeout(r, t));
const get = q => document.querySelector(q);
const getAll = q => [...document.querySelectorAll(q)];
const log = (...args) => console.log('[脚本名]', ...args);
async function init() {
}
init();
})(unsafeWindow, document);
结构说明
- IIFE 包装:使用立即执行函数表达式,接收
unsafeWindow 和 document 作为参数
- 严格模式:始终使用
'use strict'; 启用严格模式
- 代码分区:使用
#region 和 #endregion 注释组织代码块
- 工具函数优先:在文件顶部定义常用工具函数
- 异步处理:优先使用 async/await 而非回调
元数据配置
必需的元数据标签
推荐的可选标签
依赖和资源
@match 和 @include 的使用
@run-at 运行时机
@grant 权限声明
编码风格指南
变量声明
const CONFIG = { retries: 3 };
let counter = 0;
var oldStyle = 'bad';
const { name, version } = GM_info.script;
const [first, ...rest] = array;
函数定义
const wait = t => new Promise(r => setTimeout(r, t));
const add = (a, b) => a + b;
const fetchData = async (url) => {
const response = await fetch(url);
return response.json();
};
function MyClass() {
this.value = 1;
}
async function loadPage() {
await wait(1000);
return 'loaded';
}
命名规范
const MAX_RETRY_COUNT = 3;
const API_BASE_URL = 'https://api.example.com';
const userName = 'Alice';
const getUserInfo = () => {};
class DataManager {}
const MyComponent = () => {};
const _privateVar = 'internal';
const btnSubmit = document.querySelector('#submit');
const divContainer = document.querySelector('.container');
字符串处理
const message = `Hello, ${userName}!`;
const html = `
<div class="item">
<span>${title}</span>
</div>
`;
const sql = `
SELECT * FROM users
WHERE active = 1
ORDER BY created_at DESC
`;
对象和数组
const name = 'Alice';
const age = 25;
const user = { name, age };
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5];
const obj1 = { a: 1 };
const obj2 = { ...obj1, b: 2 };
const result = items
.filter(item => item.active)
.map(item => item.name)
.sort();
条件判断
const isValid = value && value.length > 0;
const result = condition ? valueA : valueB;
const userName = user?.profile?.name;
const displayName = userName ?? 'Anonymous';
function process(data) {
if (!data) return null;
if (!data.isValid) return null;
return processData(data);
}
错误处理
async function safeFetch(url) {
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
log('Fetch error:', error);
return null;
}
}
try {
riskyOperation();
} catch (e) {
console.error('[ScriptName] Error:', e);
}
try {
sound.play();
} catch (e) {
}
常用工具函数
时间延迟
const wait = t => new Promise(r => setTimeout(r, t));
await wait(1000);
const waitWithTimeout = (ms, timeoutMs) => {
return Promise.race([
wait(ms),
wait(timeoutMs).then(() => { throw new Error('Timeout'); })
]);
};
DOM 查询
const get = q => document.querySelector(q);
const $ = get;
const getAll = q => [...document.querySelectorAll(q)];
const $$ = getAll;
const $child = (parent, selector) => {
if (typeof parent === 'string') {
return document.querySelector(`${parent} ${selector}`);
}
return parent.querySelector(selector);
};
const $childAll = (parent, selector) => {
if (typeof parent === 'string') {
return [...document.querySelectorAll(`${parent} ${selector}`)];
}
return [...parent.querySelectorAll(selector)];
};
const waitFor = async (selector, timeout = 10000, interval = 100) => {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const element = get(selector);
if (element) return element;
await wait(interval);
}
return null;
};
const button = await waitFor('#submit-button');
if (button) {
button.click();
}
日志输出
const log = (...args) => console.log('[ScriptName]', ...args);
const error = (...args) => console.error('[ScriptName]', ...args);
const warn = (...args) => console.warn('[ScriptName]', ...args);
const logger = {
log(...args) {
console.log('%c[ScriptName]', 'color: #4CAF50', ...args);
},
error(...args) {
console.error('%c[ScriptName]', 'color: #F44336', ...args);
},
warn(...args) {
console.warn('%c[ScriptName]', 'color: #FF9800', ...args);
},
debug(...args) {
if (DEBUG_MODE) {
console.log('%c[ScriptName Debug]', 'color: #2196F3', ...args);
}
}
};
URL 处理
const fixUrlProtocol = (url) => {
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
} else if (url.startsWith('//')) {
return window.location.protocol + url;
} else if (url.startsWith('/')) {
return window.location.origin + url;
}
return url;
};
const removeTailingSlash = (str) => {
return str.replace(/\/+$/, '');
};
const getUrlParam = (name) => {
const params = new URLSearchParams(window.location.search);
return params.get(name);
};
const parseUrl = (url) => {
const parser = document.createElement('a');
parser.href = url;
return {
protocol: parser.protocol,
hostname: parser.hostname,
port: parser.port,
pathname: parser.pathname,
search: parser.search,
hash: parser.hash
};
};
Cookie 操作
const getCookie = (name) => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
return parts.pop().split(';').shift();
}
return null;
};
const getCSRFToken = () => getCookie('bili_jct');
页面判断
const pages = {
isVideoPage() {
return /\/video\//.test(location.pathname);
},
isProfilePage() {
return location.hostname.startsWith('space.bilibili.com');
},
isPlayPage() {
return /\/(video|list)\//.test(location.pathname);
}
};
if (pages.isVideoPage()) {
}
重试机制
const retry = async (fn, retries = 3, delay = 1000) => {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
if (i === retries - 1) throw error;
await wait(delay);
delay *= 2;
}
}
};
const data = await retry(() => fetchData(url), 3, 500);
GM_* API 使用
数据存储
GM_setValue('config', { theme: 'dark', fontSize: 14 });
GM_setValue('counter', 42);
GM_setValue('enabled', true);
const config = GM_getValue('config', { theme: 'light', fontSize: 12 });
const counter = GM_getValue('counter', 0);
const enabled = GM_getValue('enabled', false);
GM_deleteValue('oldKey');
const keys = GM_listValues();
keys.forEach(key => {
console.log(key, GM_getValue(key));
});
const config = await GM.getValue('config', {});
await GM.setValue('config', newConfig);
await GM.deleteValue('oldKey');
存储最佳实践
const storage = {
get(key, defaultValue) {
const val = GM_getValue(key);
if (typeof val === 'undefined' || val === null) {
return defaultValue;
}
return val;
},
set(key, val) {
GM_setValue(key, val);
},
del(key) {
if (typeof GM_removeValue === 'function') {
GM_removeValue(key);
} else {
GM_setValue(key, undefined);
}
}
};
const settings = {
get autoExtendInfo() {
return storage.get('autoExtendInfo', true);
},
set autoExtendInfo(val) {
storage.set('autoExtendInfo', val);
},
get batchDelay() {
return storage.get('batchDelay', 0.5);
},
set batchDelay(val) {
storage.set('batchDelay', val);
}
};
菜单命令
const menuIds = [];
const registerMenu = (text, callback) => {
const id = GM_registerMenuCommand(text, callback);
menuIds.push(id);
return id;
};
const clearMenus = () => {
menuIds.forEach(id => GM_unregisterMenuCommand(id));
menuIds.length = 0;
};
function updateMenu() {
clearMenus();
registerMenu(`✓ 功能${enabled ? '已启用' : '已禁用'}`, toggleFeature);
registerMenu('⚙ 打开设置', openSettings);
}
let enabled = GM_getValue('enabled', true);
function toggleFeature() {
enabled = !enabled;
GM_setValue('enabled', enabled);
updateMenu();
}
updateMenu();
跨域请求
GM_xmlhttpRequest({
method: 'GET',
url: 'https://api.example.com/data',
onload: (response) => {
if (response.status === 200) {
const data = JSON.parse(response.responseText);
console.log(data);
}
},
onerror: (error) => {
console.error('Request failed:', error);
}
});
GM_xmlhttpRequest({
method: 'POST',
url: 'https://api.example.com/submit',
headers: {
'Content-Type': 'application/json'
},
data: JSON.stringify({ key: 'value' }),
onload: (response) => {
console.log('Response:', response.responseText);
}
});
const request = (options) => {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
...options,
onload: resolve,
onerror: reject,
ontimeout: reject
});
});
};
try {
const response = await request({
method: 'GET',
url: 'https://api.example.com/data'
});
const data = JSON.parse(response.responseText);
console.log(data);
} catch (error) {
console.error('Request failed:', error);
}
添加样式
GM_addStyle(`
.my-custom-class {
color: red;
font-size: 16px;
}
.my-button {
background: #4CAF50;
border: none;
padding: 10px 20px;
cursor: pointer;
}
.my-button:hover {
background: #45a049;
}
`);
const addStyleDynamic = (selector, rules) => {
GM_addStyle(`${selector} { ${rules} }`);
};
addStyleDynamic('.target', 'display: none !important;');
通知
GM_notification(
'操作成功',
'提示',
'https://example.com/icon.png'
);
GM_notification({
text: '这是通知内容',
title: '通知标题',
image: 'https://example.com/icon.png',
timeout: 5000,
onclick: () => {
console.log('通知被点击');
},
ondone: () => {
console.log('通知已关闭');
}
});
剪贴板
GM_setClipboard('要复制的文本');
const copyToClipboard = async (text) => {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (e) {
GM_setClipboard(text);
return true;
}
};
打开新标签页
GM_openInTab('https://example.com', { active: false });
GM_openInTab('https://example.com', { active: true });
GM_openInTab('https://example.com', { insert: true });
DOM 操作技巧
创建元素
const button = document.createElement('button');
button.textContent = '点击我';
button.className = 'my-button';
button.addEventListener('click', handleClick);
document.body.appendChild(button);
const button = CKTools.domHelper({
tag: 'button',
text: '点击我',
classList: ['my-button', 'primary'],
on: {
click: handleClick
},
append: document.body
});
const container = document.createElement('div');
container.innerHTML = `
<div class="card">
<h3 class="title">${title}</h3>
<p class="content">${content}</p>
<button class="btn">操作</button>
</div>
`;
container.querySelector('.btn').addEventListener('click', handleClick);
document.body.appendChild(container);
样式操作
const element = get('.target');
element.style.color = 'red';
element.style.fontSize = '16px';
element.style.cssText = 'color: red; font-size: 16px; margin: 10px;';
Object.assign(element.style, {
color: 'red',
fontSize: '16px',
margin: '10px'
});
element.classList.add('active');
element.classList.remove('disabled');
element.classList.toggle('selected');
element.classList.contains('active');
const bgColor = getComputedStyle(element).backgroundColor;
事件监听
const button = get('#button');
button.addEventListener('click', (event) => {
console.log('clicked', event.target);
});
const handleClick = (event) => {
console.log('clicked');
};
button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick);
button.addEventListener('click', handleClick, { once: true });
document.body.addEventListener('click', (event) => {
if (event.target.matches('.dynamic-button')) {
console.log('动态按钮被点击');
}
});
link.addEventListener('click', (event) => {
event.preventDefault();
});
元素操作
parent.appendChild(child);
parent.insertBefore(child, referenceNode);
parent.prepend(child);
parent.append(child1, child2);
parent.replaceChild(newChild, oldChild);
oldElement.replaceWith(newElement);
element.remove();
parent.removeChild(child);
const clone = element.cloneNode(true);
属性操作
element.setAttribute('data-id', '123');
const id = element.getAttribute('data-id');
element.removeAttribute('data-id');
element.hasAttribute('data-id');
element.dataset.userId = '123';
const userId = element.dataset.userId;
element.id = 'my-id';
element.className = 'my-class';
element.href = 'https://example.com';
页面滚动
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
window.scrollTo({ top: 0, behavior: 'smooth' });
window.scrollTo({ top: 500, left: 0, behavior: 'smooth' });
window.addEventListener('scroll', () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
console.log('当前滚动位置:', scrollTop);
});
MutationObserver 监听DOM变化
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) {
console.log('新增元素:', node);
}
});
} else if (mutation.type === 'attributes') {
console.log('属性变化:', mutation.attributeName);
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributeOldValue: true,
characterData: true
});
observer.disconnect();
const waitForElement = (selector, callback) => {
const element = get(selector);
if (element) {
callback(element);
return;
}
const observer = new MutationObserver(() => {
const element = get(selector);
if (element) {
callback(element);
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
};
waitForElement('.dynamic-content', (element) => {
console.log('元素加载完成:', element);
});
库文件使用
本项目提供了多个实用的库文件,位于 libs/ 目录下。
CKUI - UI 组件库
CKUI 是一个现代化、无依赖的 UI 组件库,适用于创建美观的用户界面。
const modal = ckui.modal({
title: '设置',
content: '这里是模态框内容',
buttons: [
{
text: '确定',
primary: true,
onClick: () => {
console.log('确定被点击');
modal.close();
}
},
{
text: '取消',
onClick: () => modal.close()
}
]
});
modal.show();
const floatWindow = ckui.floatWindow({
title: '工具面板',
content: '<div>面板内容</div>',
width: 400,
height: 300,
draggable: true
});
floatWindow.show();
const form = ckui.form({
fields: [
{
type: 'text',
name: 'username',
label: '用户名',
placeholder: '请输入用户名'
},
{
type: 'checkbox',
name: 'remember',
label: '记住我'
}
],
onSubmit: (data) => {
console.log('表单数据:', data);
}
});
CKTools - 工具函数库
CKTools 提供了丰富的DOM操作和工具函数。
const element = CKTools.get('#my-element');
const elements = CKTools.getAll('.my-class');
const button = CKTools.domHelper({
tag: 'button',
text: '点击我',
classList: ['btn', 'btn-primary'],
style: {
backgroundColor: '#4CAF50',
color: 'white',
padding: '10px 20px'
},
on: {
click: () => console.log('clicked')
},
append: document.body
});
const div = CKTools.domHelper({
tag: 'div',
from: '<span>Hello</span><span>World</span>'
});
CKLogger - 日志系统
提供结构化的日志记录功能。
const logger = new Logger('MyScript');
logger.log('普通日志');
logger.info('信息日志');
logger.warn('警告日志');
logger.error('错误日志');
logger.debug('调试日志');
const subLogger = logger.getSubLogger('API');
subLogger.log('API调用');
logger.setVisible(false);
logger.setVisible(true);
logger.subscribe({
levels: ['error', 'warn'],
onlog: (level, namespace, ...args) => {
sendToServer({ level, namespace, message: args });
}
});
CKEventEmitter - 事件发射器
实现发布-订阅模式。
const emitter = new EventEmitter();
emitter.on('dataLoaded', (data) => {
console.log('数据已加载:', data);
});
emitter.emit('dataLoaded', { items: [] });
const handler = (data) => console.log(data);
emitter.on('event', handler);
emitter.off('event', handler);
emitter.clean('event');
emitter.clean();
CKSimpleFilter - 数据过滤器
提供复杂的数据过滤功能。
const filter = new SimpleFilter({
$and: [
(item) => item.age > 18,
(item) => item.active === true,
{
$or: [
(item) => item.role === 'admin',
(item) => item.role === 'moderator'
]
}
]
});
const users = [
{ age: 20, active: true, role: 'admin' },
{ age: 17, active: true, role: 'user' },
{ age: 25, active: false, role: 'admin' }
];
const results = await filter.applyFilterToAll(users);
console.log(results);
const filtered = users.filter((user, i) => results[i]);
CKAutoLoader - B站页面加载检测
专门用于检测 B站播放器加载完成。
CKAutoLoader.reg('myScript', () => {
console.log('播放器加载完成');
});
window.addEventListener('ckBilibiliCommentLoaded', () => {
console.log('评论区加载完成');
});
window.addEventListener('ckBilibiliPlayerLoaded', () => {
console.log('播放器加载完成');
});
调试技巧
开启调试模式
unsafeWindow.DEBUG_MODE = true;
const DEBUG = GM_getValue('debug', false);
const DEBUG = new URLSearchParams(location.search).has('debug');
const debug = (...args) => {
if (DEBUG) console.log('[Debug]', ...args);
};
使用 debugger 语句
function criticalFunction() {
debugger;
}
if (someCondition) {
debugger;
}
性能测试
console.time('操作名称');
console.timeEnd('操作名称');
performance.mark('start');
performance.mark('end');
performance.measure('操作', 'start', 'end');
const measure = performance.getEntriesByName('操作')[0];
console.log(`耗时: ${measure.duration}ms`);
错误追踪
window.addEventListener('error', (event) => {
log('全局错误:', event.error);
});
window.addEventListener('unhandledrejection', (event) => {
log('未处理的Promise错误:', event.reason);
});
function trackError() {
const stack = new Error().stack;
console.log('调用栈:', stack);
}
监控 GM 存储变化
const listenerId = GM_addValueChangeListener('config', (key, oldValue, newValue, remote) => {
console.log('存储变化:', {
key,
oldValue,
newValue,
remote
});
});
GM_removeValueChangeListener(listenerId);
最佳实践
1. 脚本初始化
(async function() {
'use strict';
if (!unsafeWindow) {
console.error('unsafeWindow 不可用');
return;
}
if (typeof CKTools === 'undefined') {
console.error('CKTools 未加载');
return;
}
if (!location.pathname.includes('/video/')) {
return;
}
const player = await waitFor('.bilibili-player');
if (!player) {
console.error('播放器未找到');
return;
}
await init();
})();
2. 配置管理
const CONFIG = {
VERSION: '1.0.0',
DEBUG: GM_getValue('debug', false),
RETRY_COUNT: 3,
TIMEOUT: 5000,
SELECTORS: {
player: '.bilibili-player',
video: 'video',
danmaku: '.bilibili-player-danmaku'
},
API: {
base: 'https://api.bilibili.com',
userInfo: '/x/space/acc/info',
videoInfo: '/x/web-interface/view'
}
};
const player = await waitFor(CONFIG.SELECTORS.player, CONFIG.TIMEOUT);
3. 模块化组织
const modules = {
storage: {
get(key, defaultValue) {
return GM_getValue(key, defaultValue);
},
set(key, value) {
GM_setValue(key, value);
}
},
ui: {
showNotification(message) {
GM_notification({
text: message,
timeout: 3000
});
},
createButton(text, onClick) {
return CKTools.domHelper({
tag: 'button',
text,
on: { click: onClick }
});
}
},
api: {
async getUserInfo(uid) {
const response = await request({
method: 'GET',
url: `${CONFIG.API.base}${CONFIG.API.userInfo}?mid=${uid}`
});
return JSON.parse(response.responseText);
}
}
};
4. 错误恢复
async function initWithFallback() {
try {
await primaryMethod();
} catch (error) {
log('主方法失败,尝试备用方案:', error);
try {
await fallbackMethod();
} catch (fallbackError) {
error('所有方法均失败:', fallbackError);
showErrorNotification('功能初始化失败,请刷新页面重试');
}
}
}
async function autoRetry(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
await wait(1000 * Math.pow(2, i));
}
}
}
5. 内存管理
const cleanupFunctions = [];
function addCleanup(fn) {
cleanupFunctions.push(fn);
}
function cleanup() {
cleanupFunctions.forEach(fn => {
try {
fn();
} catch (e) {
console.error('清理失败:', e);
}
});
cleanupFunctions.length = 0;
}
window.addEventListener('beforeunload', cleanup);
const button = get('#button');
const handler = () => console.log('clicked');
button.addEventListener('click', handler);
addCleanup(() => button.removeEventListener('click', handler));
6. 性能优化
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
function throttle(func, wait) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= wait) {
lastTime = now;
func.apply(this, args);
}
};
}
const handleScroll = throttle(() => {
console.log('滚动事件');
}, 200);
window.addEventListener('scroll', handleScroll);
const lazyLoad = (items, render) => {
const batchSize = 20;
let index = 0;
function loadNext() {
const batch = items.slice(index, index + batchSize);
batch.forEach(render);
index += batchSize;
if (index < items.length) {
requestAnimationFrame(loadNext);
}
}
loadNext();
};
7. 兼容性处理
const isGM4 = typeof GM !== 'undefined' && typeof GM.getValue === 'function';
const getValue = isGM4
? (key, defaultValue) => GM.getValue(key, defaultValue)
: GM_getValue;
const setValue = isGM4
? (key, value) => GM.setValue(key, value)
: GM_setValue;
const features = {
hasIntersectionObserver: 'IntersectionObserver' in window,
hasResizeObserver: 'ResizeObserver' in window,
hasFetch: 'fetch' in window
};
if (!features.hasFetch) {
}
8. 用户体验
const STORAGE_KEY_FIRST_RUN = 'firstRun';
if (GM_getValue(STORAGE_KEY_FIRST_RUN, true)) {
GM_notification({
title: '欢迎使用脚本',
text: '首次运行,请在菜单中配置选项',
timeout: 5000
});
GM_setValue(STORAGE_KEY_FIRST_RUN, false);
}
const CURRENT_VERSION = '1.2.0';
const lastVersion = GM_getValue('version', '0.0.0');
if (lastVersion !== CURRENT_VERSION) {
showUpdateLog(lastVersion, CURRENT_VERSION);
GM_setValue('version', CURRENT_VERSION);
}
function showLoading(message = '加载中...') {
const loading = document.createElement('div');
loading.id = 'script-loading';
loading.textContent = message;
loading.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: rgba(0,0,0,0.8);
color: white;
padding: 10px 20px;
border-radius: 4px;
z-index: 999999;
`;
document.body.appendChild(loading);
return {
update: (msg) => loading.textContent = msg,
remove: () => loading.remove()
};
}
const loading = showLoading('正在初始化...');
await init();
loading.update('初始化完成');
setTimeout(() => loading.remove(), 2000);
9. 安全性考虑
function sanitizeInput(input) {
return String(input)
.replace(/[<>"']/g, (char) => {
const entities = {
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return entities[char];
});
}
function safeSetHTML(element, html) {
element.textContent = '';
const temp = document.createElement('div');
temp.innerHTML = html;
while (temp.firstChild) {
element.appendChild(temp.firstChild);
}
}
function isValidURL(url) {
try {
const parsed = new URL(url);
return ['http:', 'https:'].includes(parsed.protocol);
} catch {
return false;
}
}
const userInput = sanitizeInput(untrustedData);
const element = get('#output');
element.textContent = userInput;
10. 文档和维护
async function getUserInfo(uid, includeStats = false) {
if (!uid || typeof uid !== 'number') {
throw new Error('Invalid user ID');
}
const url = `${API_BASE}/user/${uid}${includeStats ? '?stats=true' : ''}`;
try {
const response = await request({ method: 'GET', url });
return JSON.parse(response.responseText);
} catch (error) {
log('获取用户信息失败:', error);
throw error;
}
}
完整示例脚本
以下是一个综合应用上述所有最佳实践的完整示例:
(async function(unsafeWindow, document) {
'use strict';
const CONFIG = {
VERSION: '1.0.0',
DEBUG: false,
STORAGE_PREFIX: 'bili_enhance_',
SELECTORS: {
player: '.bilibili-player',
video: 'video',
videoTitle: 'h1.video-title'
}
};
const wait = t => new Promise(r => setTimeout(r, t));
const get = q => document.querySelector(q);
const getAll = q => [...document.querySelectorAll(q)];
const log = (...args) => console.log('[VideoEnhance]', ...args);
const error = (...args) => console.error('[VideoEnhance]', ...args);
const waitFor = async (selector, timeout = 10000) => {
const start = Date.now();
while (Date.now() - start < timeout) {
const el = get(selector);
if (el) return el;
await wait(100);
}
return null;
};
const storage = {
get(key, defaultValue) {
return GM_getValue(CONFIG.STORAGE_PREFIX + key, defaultValue);
},
set(key, value) {
GM_setValue(CONFIG.STORAGE_PREFIX + key, value);
},
del(key) {
GM_deleteValue(CONFIG.STORAGE_PREFIX + key);
}
};
const settings = {
get autoPlay() {
return storage.get('autoPlay', true);
},
set autoPlay(val) {
storage.set('autoPlay', val);
},
get playerVolume() {
return storage.get('playerVolume', 0.5);
},
set playerVolume(val) {
storage.set('playerVolume', Math.max(0, Math.min(1, val)));
}
};
GM_addStyle(`
.video-enhance-btn {
padding: 8px 16px;
background: #00a1d6;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
.video-enhance-btn:hover {
background: #0092c4;
}
.video-enhance-panel {
position: fixed;
top: 20px;
right: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
padding: 20px;
z-index: 9999;
min-width: 300px;
}
`);
function createButton(text, onClick) {
return CKTools.domHelper({
tag: 'button',
text,
classList: ['video-enhance-btn'],
on: { click: onClick }
});
}
class VideoEnhancer {
constructor() {
this.player = null;
this.video = null;
this.initialized = false;
}
async init() {
if (this.initialized) return;
try {
log('初始化中...');
this.player = await waitFor(CONFIG.SELECTORS.player);
if (!this.player) {
throw new Error('播放器未找到');
}
this.video = await waitFor(CONFIG.SELECTORS.video);
if (!this.video) {
throw new Error('视频元素未找到');
}
this.applySettings();
this.setupUI();
this.setupMenu();
this.initialized = true;
log('初始化完成');
GM_notification({
title: '视频增强',
text: '功能已启用',
timeout: 2000
});
} catch (err) {
error('初始化失败:', err);
}
}
applySettings() {
if (!settings.autoPlay) {
this.video.pause();
}
this.video.volume = settings.playerVolume;
log('设置已应用');
}
setupUI() {
const videoTitle = get(CONFIG.SELECTORS.videoTitle);
if (!videoTitle) return;
const buttonContainer = CKTools.domHelper({
tag: 'div',
style: { marginTop: '10px' }
});
const downloadBtn = createButton('下载视频', () => {
this.downloadVideo();
});
const speedBtn = createButton('倍速播放', () => {
this.changeSpeed();
});
buttonContainer.appendChild(downloadBtn);
buttonContainer.appendChild(speedBtn);
videoTitle.parentNode.insertBefore(
buttonContainer,
videoTitle.nextSibling
);
}
setupMenu() {
const menuIds = [];
menuIds.push(GM_registerMenuCommand(
`${settings.autoPlay ? '✓' : '✗'} 自动播放`,
() => {
settings.autoPlay = !settings.autoPlay;
this.setupMenu();
GM_notification({
text: `自动播放已${settings.autoPlay ? '启用' : '禁用'}`,
timeout: 2000
});
}
));
menuIds.push(GM_registerMenuCommand(
'⚙ 重置设置',
() => {
if (confirm('确定要重置所有设置吗?')) {
storage.del('autoPlay');
storage.del('playerVolume');
location.reload();
}
}
));
this._menuIds = menuIds;
}
downloadVideo() {
log('下载功能待实现');
GM_notification({
text: '下载功能开发中...',
timeout: 2000
});
}
changeSpeed() {
const speeds = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0];
const currentSpeed = this.video.playbackRate;
const currentIndex = speeds.indexOf(currentSpeed);
const nextIndex = (currentIndex + 1) % speeds.length;
this.video.playbackRate = speeds[nextIndex];
GM_notification({
text: `播放速度: ${speeds[nextIndex]}x`,
timeout: 1500
});
}
destroy() {
if (this._menuIds) {
this._menuIds.forEach(id => {
GM_unregisterMenuCommand(id);
});
}
log('资源已清理');
}
}
if (!/\/video\//.test(location.pathname)) {
return;
}
const enhancer = new VideoEnhancer();
await enhancer.init();
window.addEventListener('beforeunload', () => {
enhancer.destroy();
});
log(`Version ${CONFIG.VERSION} loaded`);
})(unsafeWindow, document);
总结
本技能文档覆盖了用户脚本开发的核心知识点:
- 结构规范:IIFE包装、严格模式、代码分区
- 元数据配置:完整的@标签说明和最佳实践
- 编码风格:现代JavaScript语法、命名规范
- 工具函数:常用的辅助函数封装
- GM API:Tampermonkey提供的强大API
- DOM操作:高效的DOM查询和操作技巧
- 库文件:项目自带库的使用说明
- 调试技巧:开发和调试方法
- 最佳实践:生产级代码的编写规范
- 完整示例:综合应用的实战案例
遵循这些指南可以编写出高质量、可维护的用户脚本。