原生JS SDK开发浅见

2,214 阅读1分钟

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

前言

在开发JS SDK时,『原生 or 框架』不同人不同场景都有不同的选择。本文源于实际项目场景,阐述一些在原生环境下『不顺手』问题的处理。

1. 模块化开发

// index.js
// 加入一个入口按钮
// 按钮点击增加侧边栏
// 点击空白隐藏侧边栏
// 侧边栏点击出现弹窗
// ...

如此写下去想想都头秃,像一篇流水账作文。

  • 为了让读者快速定位到寻找的代码段,可以使用模块化(利用 webpack , gulp 等构建工具)。
  • 为了弱化组织逻辑的时间顺序,可以使用面向对象,比如拆分 Class (拆分的粒度可以借鉴对框架中组件的使用)。
//index.js
Class Entry {} // 入口按钮

// dropdown.js
Class Dropdown { // 为指定dom绑定侧边栏
    show() {} // 显示侧边栏
    hidden() {} // 隐藏侧边栏
    select() {} // 选中打开弹框
}

这里将组件开发的经验活用在了原生环境,代码结构更清晰了。

2. 维护 innerHTML 的秩序

<ul class="list">
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ul>

用 JS 动态生成上面一段 dom 可能是这样的。

const ul = document.createElement('ul');
ul.className = 'list';
ul.innerHTML = [1, 2, 3].map(i => `<li>${i}</li>`).join('');

innerHTML 接受 HTML 序列化片段,因此我们可以使用模板字符串动态生成 dom 。不过这种方法存在局限性:

  1. 需要一个已有节点作为容器,无法在插入容器前操作 dom
  2. 容器节点的属性设置仍然需要手动添加(比如class); 通过封装innerHTML可以克服上述局限性脱离容器生成一段 dom。
const wrap = document.createElement('div');
export const template2dom = <T extends HTMLElement>(template: string): T => {
    wrap.innerHTML = template;
    return wrap.children[0] as T;
};

const ul = template2dom(`
<ul class="list">${
    [1, 2, 3].map(i => `<li>${i}</li>`).join('')
}</ul>`);

结合模块拆分可以将 dom 操作分为初始化渲染动态修改。使用封装的 template2dom 可以得到模块的根节点,在根节点下 querySelector 可获取动态修改的节点,使用原生 dom API操作。

3. 清除事件

使用框架时,我们可以不必关心自己监听的事件是不是该移除了,通常框架会替我们解决。现在需要我们来解决:

  • 当 dom 离开文档时,移除在其中绑定的事件;
  • 当 dom 内有频繁更且需要绑定事件的节点,使用事件委托; 知易行难,移除事件的位置通常与注册事件不在一起,为了获取监听函数和 dom 增加了很多在 class 中共享的属性;由于疏忽很可能遗漏某个 removeEventlistener 。封装一个事件管理类作为一个基类可以很好解决。
export abstract class EventCleaner {
    private readonly eventMap = new Map <HTMLElement, Set<{
        name: keyof HTMLElementEventMap;
        cb(e: HTMLElementEventMap[keyof HTMLElementEventMap]): unknown;
    }>>();
    addEventListener<T extends keyof HTMLElementEventMap>(
        el: HTMLElement,
        name: T,
        cb: (e: HTMLElementEventMap[T]) => unknown
    ): void {
        el.addEventListener(name, cb);
        const events = this.eventMap.get(el);
        if (events) {
            events.add({name, cb});
        }
        else {
            this.eventMap.set(el, new Set([{name, cb}]));
        }
    }
    clearEvent(): void {
        for (const [dom, events] of Array.from(this.eventMap)) {
            for (const {name, cb} of events) {
                dom.removeEventListener(name, cb);
            }
        }
        this.eventMap.clear();
    }
}

这样只需要在每个派生类写入一个卸载方法,调用 clearEvent ,即可清除该实例内监听的所有事件。

4. 样式隔离

样式隔离是 SDK 绕不开的问题。CSS Module 可在构建时修改 id 和 class 的形式,使得样式不会造成意外污染。

  • hash:CSS Module 默认class,丢失语义性随样式修改 (不利于下游覆盖样式)
  • local + name[md5]:兼顾语义性、唯一性且不会随样式修改。 local + name\[md5\] 并不是 css-loader 支持的形式,需要在构建时提供 hash
const md5 = require('md5');
const affix = md5(path.parse(packageName)).slice(0, 5); // 将包名md5作为CssModule的后缀

// webpack 配置
{
    test: /\.css$/i,
    use: [
        'style-loader',
        {
            loader: 'css-loader',
            options: {
                modules: {
                    localIdentName: `[local]_${affix}`,
                }
            }
        }
    ]
}

以上是本人使用原生 JS 开发 SDK 的一些思考。其实主要围绕着梳理代码的秩序,原生API已足够强大不需要我扩展什么功能。前端框架给开发者更低的成本来养成逻辑清晰的习惯,也带来了一些思维惯性。希望能够取其精华弃其糟粕,无畏向前!

源码参考:github.com/anyblue/blu…