无可奈何的页面增强方案

2 mins to read

如果有一个大型的 Web 程序,需要对其进行一些使用上的改进,但有时候又需要跟踪上游的更新;需要保持其稳定性,不能频繁地部署;更重要的是,没有足够的时间、人员资源去进行二次开发。 如果说只是更改页面的样式或者体验性增强,可以通过浏览器插件来做到类似客户端的效果,而且有更大的权限去做更多的事情,但是可能没有办法去令到每个人安装这样的插件,特别是Chrome收紧了安全策略之后。 该怎么做呢?

我是这样做的:找到切入点,使这个程序加载一个外部的 JavaScript 脚本,通过脚本去修改页面的 DOM,数据来源是页面上的 DOM 结构以及藏在 window 对象下的的信息。当然了,单纯地改变页面并不足以满足需求,还要通过各式各样的API(包括这个 Web 程序的,或者我们自己开发的后端)去获取数据、去触发钩子操作。

仔细分析,这样的操作就是 userscript,greasyfork.org 上有大量这样的脚本,唯一不同的只是脚本的位置,我将这样的操作叫做:运行在服务端的UserScript。

首先第一个问题来了,为了部署方便,最后我们只会利用 Webpack 打包出一个脚本,整站都会加载,那么不同的页面自然有不同的功能需求,且不同功能之间不能够互相影响。我希望有一个功能(规则)表,能够根据规则来开启不同的功能。

import FeatureA from 'components/feature-a';
import FeatureB from 'components/feature-b';
import FeatureC from 'components/feature-c';
rules = {
    "controller1": {
        "page1": FeatureA
    },
    "controller2": {
        "page2": [FeatureB, FeatureC],
        "controller5": {
            components: FeatureB,
            children: [{"page4": FeatureC}]
        }
    },
    "controller3": {
        "controller4": {
            "page3": [FeatureA]
        }
    }
}

第二个问题,如何编写功能组件。首先设计一个抽象基类,所有功能组件都继承自它,并且它保持了一些基本的元信息,并且可以进行一些初始化操作和开启关闭某些操作。

abstract class Base {
    feature={}
    constructor(parent: Base | undefined) {
        this.parent = perent;
    }
    abstract init(): void;
}
class FeatureA extends Base {
    feature={action1: true, action2: false}
    init() {
        // do something
    }
    action1() {}
    action2() {}
}

第三个问题,就是解决根据规则表加载功能组件。

class App {
    constructor () {
        go(rules, path);
    }
    load (component, args) {
        let c = new component(..args);
        c.init();
        if (c.hasOwnProperty('feature')) {
            Object.keys(c.feature).map((action)=> {
                if (!c.feature.action) return;
                if (c.hasOwnProperty(action)) c.action();
            })
        }
    }
    go (rules: IRules, path, parent) {
        // if is component
        // or if is component list
        // then load it directly
        // if has components key, load rules[components] = parent
        // then go rules[children], and set parent to parent
    }
}
new App()

通过脚本来更改 DOM 免不了需要编写 HTML 片段,个人认为 JSX 可能是比较好的解决方案,我会选择轻量的 Preact 去渲染 HTML 片段,处理内部事件,但其生命周期由上面所述功能组件决定。例如,我需要在页面上增加一个按钮,这个按钮会触发一些事件,进行某些操作,按钮的状态与外部交互通过事件通信,或者说由外部重新渲染(依靠内部 diff )。

class ReactButtonDemo extends PreactComponent {}
class Feature extends Base {
    init () {
        render(<ReactButtonDemo {...args} click={()=>this.click()} />)
    }
}