普通视图

发现新文章,点击刷新页面。
昨天 — 2025年12月13日掘金 前端

从一个“不能输负号”的数字输入框说起:Web Component 数字输入组件重构实录

作者 莫石
2025年12月13日 17:56

背景

拿到需求时,因为工期还比较宽松,官网开发,我又只做其中一个组件,框架又没有定。

我决定使用原生开发,并封装为Web Component以适配任何框架(如果不能适配,说明框架有问题)。其中就有一个数字输入框带拉杆的,数字输入框和拉杆这两个东西,原生组件都有。

于是在我的要求下,ai很快给我封装了一个还可以的东西,不过后面ui又去掉拉杆了。

临近发布,要合代码了,同事才发现这个输入框有点儿问题!


起点:一段“差不多能用”的代码

这里就不赘述Web Component的开发了,因为确实很简单,看代码就行了。

这是我最初写的 NumInput 组件(为简洁省略部分 CSS):

export class NumInput extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // ... 模板里用了 <input type="number" steop="0.1">
  }
  _onNumberInput(event) {
    const newValue = this.clamp(event.target.value, this.min, this.max);
    this.setAttribute('value', newValue);
    this._dispatchEvent();
  }
}

功能上:

  • 支持 labelunitminmaxstep
  • 聚焦有高亮效果
  • 值变化会派发 value-changed 事件
  • 还有后缀单位

看起来没问题?直到女同事问我:“为什么我输 - 没反应?” 我压根儿没想过手输,因为这个组件最开始的时候,还是带拉杆的,纯鼠标操作一点儿问题没有。

当然,现在UI变了。


问题一:type="number" 不让你输 “-” 和 “.”

首先,这不是一个bug,嗯。 我仔细研究了一下原生的数字框的机制。

1 实时校验,你每输入一个字符都会校验。 2 只能输入数字相关的,0-9 . -

那么问题来了,为什么她无法输入“-” 和 “.”呢?

实际上,并不是无法输入,只是时机和位置不对。 如果输入框中已经有一个数字1了,这个时候,你就可以在这个数字前面输入一个 “-”,在它后面输入一个“.”,这两种情况(-1和1.)都是合法的。

其余情况都是不合法的,所以无法输入。

结论:type="number"校验过于严苛,鼠标操作足矣,不适合手动输入。


重构第一步:放弃 type="number",拥抱 type="text"

使用text输入框,意味着之前数字框有的功能,我现在也都要也有,这是这个手动输入的校验规则要自定义。

我改成了:

<input type="text" inputmode="decimal" />

inputmode="decimal" 能让移动端弹出带小数点的数字键盘,体验不降反升。

但光改类型不够,得自己控制输入内容。

宽松过滤,只拦非法字符

input 事件中,我只做一件事:

_onTextInput(e) {
  let val = e.target.value;
  val = val.replace(/[^0-9.\-+]/g, ''); // 只留数字、点、正负号
  // 再处理符号位置、小数点数量...
  e.target.value = val;
}

关键原则

输入过程中,只过滤,不校验
允许用户输 -.5-12.,这些“中间状态”必须保留。


重构第二步:什么时候才该“认真”校验?

要保留用户输入的字符,又要在结束后校验,一般可能会想到节流,我觉得太麻烦了,不是指实现节流麻烦,而是节流这个逻辑本身,会一直后延,让js很麻烦。

所以,怎么判断:输入结束了

我定义了两个“结束信号”:

  1. 失焦(blur
  2. 按下回车(Enter) 刚开始没想到这个,直到我输了数字没有反应,习惯性地回车了一下。

在这两个时机,调用同一个函数 _finalizeInput()

_finalizeInput() {
  const raw = this.numberEl.value.trim();
  // 如果是中间状态(如 '-'),不处理
  if (raw === '' || raw === '-' || raw === '.') return;

  let num = parseFloat(raw);
  if (isNaN(num)) {
    // 无效?回退到上次合法值
    this.numberEl.value = this.getAttribute('value') || '';
    return;
  }

  // clamp 到 [min, max]
  num = Math.min(Math.max(num, this.min), this.max);

  // 修正浮点精度(关键!)
  num = this._roundToStepPrecision(num, this.step);

  this.numberEl.value = String(num);
  this.setAttribute('value', num);
  this._dispatchEvent(); // 派发的是数字,不是字符串!
}

问题二:0.1 + 0.2 ≠ 0.3?

  • 0.1 + 0.2 → 显示 0.30000000000000004 JavaScript 的浮点精度问题是老朋友了。
    但用户不关心这些,他们只看到“我 step=0.1,怎么变出一串小数?

解法:按 step 的小数位数四舍五入

_roundToStepPrecision(value, step) {
  if (Number.isInteger(step)) return Math.round(value);
  const decimalPlaces = step.toString().split('.')[1]?.length || 0;
  const factor = 10 ** decimalPlaces;
  return Math.round(value * factor) / factor;
}
  • step=0.1 → 保留 1 位 → 0.300000000000000040.3
  • step=0.01 → 保留 2 位 → 0.13
  • step=1 → 整数 → 4

所有赋值路径(手动输入、上下键、外部设置)都走这个修正,彻底告别脏数字。


监听一下回车作为“确认”。

这里直接不仅走了失焦的逻辑,还主动失焦,避免二次“失焦”。

this.numberEl.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    this._finalizeInput();
    this.numberEl.blur(); // 自动失焦,统一交互
  }
});

其他细节打磨

  • 修复 typosteop="0.1"step="0.1"(别笑,真有人写错)
  • 移除无用代码:原始代码里声明了 rangeEl 但没用,删掉
  • 事件传数字value-changeddetail.valuenumber 类型,不是字符串
  • 外部设置值也修正setAttribute('value', '0.30000000000000004') 会自动转成 0.3

最终效果

✅ 自由输入 -.-12.
✅ 按 ↑/↓ 按 step 精确增减
✅ 按 Enter 或失焦自动校验+修正
✅ 支持 min/max 限制
✅ 移动端弹出数字键盘
✅ 事件传出干净的数字
✅ 完全 Web Component,零依赖


结语

这个组件最终代码比最初长了近一倍,但用户体验提升是质的飞跃

有时候,看似简单的功能,深挖下去全是坑。
但正是这些“小细节”,决定了产品是“能用”还是“好用”。

最后,女同事看了一眼说:“这个输入框终于不抽风了。”
我笑了笑,没告诉她,我让AI改了四版。

(完)


斐波那契数列:从递归到缓存优化的极致拆解

作者 闲云ing
2025年12月13日 16:57
斐波那契数列:从递归到缓存优化的极致拆解 斐波那契数列是算法入门的经典案例,也是理解「递归」「缓存优化」「闭包」核心思想的绝佳载体。本文会从最基础的递归解法入手,逐步拆解重复计算的痛点,再通过哈希缓存

异步互斥锁

作者 NuLL
2025年12月13日 16:27
节流防抖时间太短,而接口返回太久,导致节流防抖结束后仍然可以被重复提交?尝试换个思路——异步互斥锁,以互斥锁作为基石,将整个同名称的异步操作状态锁定,彻底防止接口数据重复提交!

如何正确实现圆角渐变边框?为什么 border-radius 对 border-image 不生效?

作者 三十_
2025年12月13日 14:51

在项目中需要实现一个圆角渐变边框效果。

image-20251212075318921.png

我的第一反应是使用 border-radiusborder-image。然而实践后发现 border-radiusborder-image 不生效效果是这样的:

image-20251201193414791.png

给 div 设置了 border-radius,但边框仍然是直角。


为什么 border-radiusborder-image 失效?

两者的工作层级不同

  • border-radius 作用在 div 元素上,它控制的是整个 div 轮廓的圆角。
  • border-image 绘制边框,是独立于 div 之外的,是脱离于 div 的。

所以,看到的效果就是边框依然是直角,而 div 是圆角。

实现方案

主要通过两点来实现:

  • 创建一个稍大于主元素的伪元素,并设置渐变背景。
  • 使用CSS遮罩"挖空"中间部分,只留下边框区域。

代码如下:

.gradient-border-box {
    width: 100px;
    height: 100px;
    border-radius: 6px;
    position: relative;
}

.gradient-border-box::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    border-radius: 6px; /* 和主元素相同的圆角 */
    padding: 1px; /* 边框宽度 */
    
    /* 渐变效果 */
    background: linear-gradient(360deg, 
        rgba(96, 161, 250, 0.5), 
        rgba(96, 161, 250, 1));
    
    /* CSS遮罩 */
    mask: 
        linear-gradient(#fff 0 0) content-box, 
        linear-gradient(#fff 0 0);
    mask-composite: exclude;
}

方案解析

  • 主元素: 负责内容区域和圆角,只设置 border-radius

  • 伪元素: 负责绘制渐变边框,它的位置与大小覆盖主元素,通过:

    • background 绘制渐变
    • padding 控制边框宽度
    • mask 挖空中间区域

伪元素中的遮罩详解

mask: 
    linear-gradient(#fff 0 0) content-box, 
    linear-gradient(#fff 0 0);
mask-composite: exclude;
  • -webkit-mask: 这行代码创建了两个完全相同的白色矩形遮罩,第一个仅作用于内容区域(content-box);第二个作用域整个元素区域(border-box)。

    第一个 linear-gradient(#fff 0 0) 创建一个纯白色矩形(线性渐变,从0%到0%);

    content-box 指定遮罩的参考框,作用域元素的内容区域(不包括padding、border);

    第二个 linear-gradient(#fff 0 0) 创建了一个白色矩形,默认是 border-box(包括内容+padding+border)。

  • mask-composite:exclude: 控制多个遮罩如何组合exclude 代表异或操作

    结合遮罩解释就是: 边框区域 = border-box(整个区域) - content-box(中心区域)

🎉 React 的 JSX 语法与组件思想:开启你的前端‘搭积木’之旅(深度对比 Vue 哲学)

2025年12月13日 14:46
嘿,未来的前端大神们!👋 欢迎来到 React 的世界!如果你正在寻找一个现代、高效、充满乐趣的前端框架,那么恭喜你,你找对地方了! 在 React 中,有两个核心概念你必须掌握:JSX 语法和组件化

前端常用模式:提升代码质量的四大核心模式

作者 1024肥宅
2025年12月13日 13:00

引言

在前端开发中,随着应用复杂度不断增加,代码的组织和管理变得越来越重要。本文将深入探讨前端开发中四种极其有用的模式:中间件模式、管道模式、链式调用和惰性加载。这些模式不仅能提高代码的可读性和可维护性,还能显著优化应用性能。

一、中间件模式(Middleware Pattern)

中间件模式允许我们在请求和响应的处理流程中插入多个处理阶段, 这种模式在Node.js框架(例如Koa、Express)中广泛应用, 但在前端同样有其用武之地。

1.1 核心概念

中间件本质上是一个函数, 它可以:

  • 访问请求(request)和响应(response)对象
  • 执行任何代码
  • 修改请求和响应对象
  • 结束请求-响应周期
  • 调用下一个中间件
1.2 基础实现
// 简单中间件系统实现
class MiddlewareSystem {
  constructor() {
    this.middlewares = [];
    this.context = {};
  }

  // 添加中间件
  use(middleware) {
    if (typeof middleware !== 'function') {
      throw new Error('Middleware must be a function');
    }
    this.middlewares.push(middleware);
    return this; // 支持链式调用
  }

  // 执行中间件
  async run(input) {
    this.context = { ...input };
    
    // 创建next函数
    let index = 0;
    const next = async () => {
      if (index < this.middlewares.length) {
        const middleware = this.middlewares[index++];
        await middleware(this.context, next);
      }
    };
    
    try {
      await next();
      return this.context;
    } catch (error) {
      console.error('Middleware execution error:', error);
      throw error;
    }
  }
}

// 使用示例
const system = new MiddlewareSystem();

// 添加中间件
system
  .use(async (ctx, next) => {
    console.log('Middleware 1: Start');
    ctx.timestamp = Date.now();
    await next();
    console.log('Middleware 1: End');
  })
  .use(async (ctx, next) => {
    console.log('Middleware 2: Start');
    ctx.user = { id: 1, name: 'John' };
    // 模拟异步操作
    await new Promise(resolve => setTimeout(resolve, 100));
    await next();
    console.log('Middleware 2: End');
  })
  .use(async (ctx, next) => {
    console.log('Middleware 3: Start');
    ctx.data = { message: 'Hello World' };
    // 不再调用next(),结束执行
    console.log('Middleware 3: End (no next call)');
  });

// 运行中间件
system.run({ requestId: '123' }).then(result => {
  console.log('Final context:', result);
});
1.3 错误处理中间件
class ErrorHandlingMiddleware {
  constructor() {
    this.middlewares = [];
    this.errorMiddlewares = [];
  }

  use(middleware) {
    this.middlewares.push(middleware);
    return this;
  }

  useError(errorMiddleware) {
    this.errorMiddlewares.push(errorMiddleware);
    return this;
  }

  async run(input) {
    const context = { ...input };
    let index = 0;
    let errorIndex = 0;
    let error = null;

    const next = async (err) => {
      if (err) {
        error = err;
        errorIndex = 0;
        return await nextError();
      }

      if (index < this.middlewares.length) {
        const middleware = this.middlewares[index++];
        try {
          await middleware(context, next);
        } catch (err) {
          await next(err);
        }
      }
    };

    const nextError = async () => {
      if (errorIndex < this.errorMiddlewares.length) {
        const errorMiddleware = this.errorMiddlewares[errorIndex++];
        try {
          await errorMiddleware(error, context, nextError);
        } catch (err) {
          error = err;
          await nextError();
        }
      }
    };

    await next();
    return { context, error };
  }
}

// 使用示例
const errorSystem = new ErrorHandlingMiddleware();

errorSystem
  .use(async (ctx, next) => {
    console.log('Processing...');
    // 模拟错误
    if (!ctx.user) {
      throw new Error('User not found');
    }
    await next();
  })
  .useError(async (err, ctx, next) => {
    console.error('Error caught:', err.message);
    ctx.error = err.message;
    ctx.status = 'error';
    await next();
  });

errorSystem.run({}).then(result => {
  console.log('Result with error handling:', result);
});
1.4 前端应用场景
// 前端请求拦截中间件
class RequestInterceptor {
  constructor() {
    this.interceptors = [];
    this.defaultConfig = {
      timeout: 5000,
      headers: {}
    };
  }

  use(interceptor) {
    this.interceptors.push(interceptor);
    return this;
  }

  async request(url, config = {}) {
    const context = {
      url,
      config: { ...this.defaultConfig, ...config },
      response: null,
      error: null
    };

    let index = 0;
    const next = async () => {
      if (index < this.interceptors.length) {
        const interceptor = this.interceptors[index++];
        await interceptor(context, next);
      } else {
        // 执行实际请求
        await this.executeRequest(context);
      }
    };

    await next();
    return context;
  }

  async executeRequest(context) {
    try {
      const response = await fetch(context.url, context.config);
      context.response = await response.json();
    } catch (error) {
      context.error = error;
    }
  }
}

// 创建请求拦截器
const api = new RequestInterceptor();

api
  .use(async (ctx, next) => {
    console.log('Auth interceptor');
    ctx.config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`;
    await next();
  })
  .use(async (ctx, next) => {
    console.log('Logging interceptor');
    console.log(`Request to ${ctx.url}`, ctx.config);
    const start = Date.now();
    await next();
    const duration = Date.now() - start;
    console.log(`Request completed in ${duration}ms`);
  })
  .use(async (ctx, next) => {
    console.log('Cache interceptor');
    const cacheKey = `cache_${ctx.url}`;
    const cached = localStorage.getItem(cacheKey);
    
    if (cached && !ctx.config.noCache) {
      ctx.response = JSON.parse(cached);
      console.log('Using cached response');
    } else {
      await next();
      if (ctx.response && !ctx.error) {
        localStorage.setItem(cacheKey, JSON.stringify(ctx.response));
      }
    }
  });

// 使用拦截器
api.request('https://api.example.com/data', { method: 'GET' })
  .then(result => console.log('Response:', result.response));

二、管道模式(Pipeline Pattern)

管道模式将多个处理函数连接起来, 数据像流水一样经过这些函数进行处理和转换。

2.1 基本实现
// 同步管道
const pipeline = (...fns) => (initialValue) => {
  return fns.reduce((value, fn) => {
    if (typeof fn !== 'function') {
      throw new Error(`Pipeline expects functions, got ${typeof fn}`);
    }
    return fn(value);
  }, initialValue);
};

// 异步管道
const asyncPipeline = (...fns) => async (initialValue) => {
  let result = initialValue;
  for (const fn of fns) {
    if (typeof fn !== 'function') {
      throw new Error(`Pipeline expects functions, got ${typeof fn}`);
    }
    result = await fn(result);
  }
  return result;
};

// 可中断的管道
const breakablePipeline = (...fns) => (initialValue) => {
  let shouldBreak = false;
  let breakValue = null;
  
  const breakFn = (value) => {
    shouldBreak = true;
    breakValue = value;
  };
  
  const result = fns.reduce((value, fn) => {
    if (shouldBreak) return breakValue;
    return fn(value, breakFn);
  }, initialValue);
  
  return shouldBreak ? breakValue : result;
};
2.2 数据处理管道示例
// 数据清洗管道
const dataCleaningPipeline = pipeline(
  // 1. 移除空值
  (data) => data.filter(item => item != null),
  
  // 2. 标准化字段
  (data) => data.map(item => ({
    ...item,
    name: item.name?.trim().toLowerCase() || 'unknown',
    value: Number(item.value) || 0
  })),
  
  // 3. 去重
  (data) => {
    const seen = new Set();
    return data.filter(item => {
      const key = `${item.name}-${item.value}`;
      if (seen.has(key)) return false;
      seen.add(key);
      return true;
    });
  },
  
  // 4. 排序
  (data) => data.sort((a, b) => b.value - a.value),
  
  // 5. 限制数量
  (data) => data.slice(0, 10)
);

// 使用管道
const rawData = [
  { name: '  John  ', value: '100' },
  { name: 'Jane', value: 200 },
  null,
  { name: 'John', value: '100' }, // 重复
  { name: '', value: 'invalid' }
];

const cleanedData = dataCleaningPipeline(rawData);
console.log('Cleaned data:', cleanedData);
2.3 表单验证管道
// 验证规则
const validators = {
  required: (value) => ({
    isValid: value != null && value.toString().trim() !== '',
    message: 'This field is required'
  }),
  
  minLength: (min) => (value) => ({
    isValid: value?.toString().length >= min,
    message: `Minimum length is ${min} characters`
  }),
  
  maxLength: (max) => (value) => ({
    isValid: value?.toString().length <= max,
    message: `Maximum length is ${max} characters`
  }),
  
  email: (value) => ({
    isValid: /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
    message: 'Invalid email format'
  }),
  
  numeric: (value) => ({
    isValid: !isNaN(parseFloat(value)) && isFinite(value),
    message: 'Must be a number'
  })
};

// 表单验证管道
class FormValidator {
  constructor(rules) {
    this.rules = rules;
  }

  validate(data) {
    const errors = {};
    const results = {};

    for (const [field, fieldRules] of Object.entries(this.rules)) {
      const value = data[field];
      const fieldErrors = [];

      for (const rule of fieldRules) {
        const result = rule(value);
        if (!result.isValid) {
          fieldErrors.push(result.message);
        }
      }

      if (fieldErrors.length > 0) {
        errors[field] = fieldErrors;
      } else {
        results[field] = value;
      }
    }

    return {
      isValid: Object.keys(errors).length === 0,
      errors,
      results
    };
  }
}

// 使用示例
const registrationRules = {
  username: [
    validators.required,
    validators.minLength(3),
    validators.maxLength(20)
  ],
  email: [
    validators.required,
    validators.email
  ],
  age: [
    validators.required,
    validators.numeric,
    (value) => ({
      isValid: value >= 18 && value <= 100,
      message: 'Age must be between 18 and 100'
    })
  ]
};

const validator = new FormValidator(registrationRules);
const userData = {
  username: 'johndoe',
  email: 'john@example.com',
  age: 25
};

const validationResult = validator.validate(userData);
console.log('Validation result:', validationResult);

三、链式调用(Chaining Pattern)

链式调用通过返回对象实例本身(this), 允许连续调用多个方法, 使代码更加流畅易读。

3.1 基础链式调用
// jQuery风格的链式调用
class QueryBuilder {
  constructor() {
    this.query = {
      select: [],
      from: null,
      where: [],
      orderBy: [],
      limit: null,
      offset: null
    };
  }

  select(...fields) {
    this.query.select.push(...fields);
    return this;
  }

  from(table) {
    this.query.from = table;
    return this;
  }

  where(condition) {
    this.query.where.push(condition);
    return this;
  }

  orderBy(field, direction = 'ASC') {
    this.query.orderBy.push({ field, direction });
    return this;
  }

  limit(count) {
    this.query.limit = count;
    return this;
  }

  offset(count) {
    this.query.offset = count;
    return this;
  }

  build() {
    const { select, from, where, orderBy, limit, offset } = this.query;
    
    if (!from) {
      throw new Error('FROM clause is required');
    }

    let sql = `SELECT ${select.length > 0 ? select.join(', ') : '*'} FROM ${from}`;
    
    if (where.length > 0) {
      sql += ` WHERE ${where.join(' AND ')}`;
    }
    
    if (orderBy.length > 0) {
      const orderClauses = orderBy.map(({ field, direction }) => `${field} ${direction}`);
      sql += ` ORDER BY ${orderClauses.join(', ')}`;
    }
    
    if (limit !== null) {
      sql += ` LIMIT ${limit}`;
    }
    
    if (offset !== null) {
      sql += ` OFFSET ${offset}`;
    }
    
    return sql + ';';
  }

  reset() {
    this.query = {
      select: [],
      from: null,
      where: [],
      orderBy: [],
      limit: null,
      offset: null
    };
    return this;
  }
}

// 使用示例
const sql = new QueryBuilder()
  .select('id', 'name', 'email')
  .from('users')
  .where('active = true')
  .where('age >= 18')
  .orderBy('name', 'ASC')
  .limit(10)
  .offset(0)
  .build();

console.log('Generated SQL:', sql);
3.2 DOM操作链式调用
class DOMElement {
  constructor(selector) {
    if (typeof selector === 'string') {
      this.elements = Array.from(document.querySelectorAll(selector));
    } else if (selector instanceof Element) {
      this.elements = [selector];
    } else if (Array.isArray(selector)) {
      this.elements = selector;
    } else {
      this.elements = [];
    }
  }

  // CSS相关方法
  css(property, value) {
    if (typeof property === 'object') {
      this.elements.forEach(el => {
        Object.assign(el.style, property);
      });
    } else if (value !== undefined) {
      this.elements.forEach(el => {
        el.style[property] = value;
      });
    }
    return this;
  }

  addClass(className) {
    this.elements.forEach(el => {
      el.classList.add(className);
    });
    return this;
  }

  removeClass(className) {
    this.elements.forEach(el => {
      el.classList.remove(className);
    });
    return this;
  }

  toggleClass(className) {
    this.elements.forEach(el => {
      el.classList.toggle(className);
    });
    return this;
  }

  // 内容操作
  text(content) {
    if (content !== undefined) {
      this.elements.forEach(el => {
        el.textContent = content;
      });
      return this;
    }
    return this.elements[0]?.textContent || '';
  }

  html(content) {
    if (content !== undefined) {
      this.elements.forEach(el => {
        el.innerHTML = content;
      });
      return this;
    }
    return this.elements[0]?.innerHTML || '';
  }

  // 属性操作
  attr(name, value) {
    if (value !== undefined) {
      this.elements.forEach(el => {
        el.setAttribute(name, value);
      });
      return this;
    }
    return this.elements[0]?.getAttribute(name) || null;
  }

  // 事件处理
  on(event, handler, options = {}) {
    this.elements.forEach(el => {
      el.addEventListener(event, handler, options);
    });
    return this;
  }

  off(event, handler, options = {}) {
    this.elements.forEach(el => {
      el.removeEventListener(event, handler, options);
    });
    return this;
  }

  // 遍历
  each(callback) {
    this.elements.forEach((el, index) => {
      callback.call(el, index, el);
    });
    return this;
  }

  // 查找子元素
  find(selector) {
    const found = [];
    this.elements.forEach(el => {
      found.push(...Array.from(el.querySelectorAll(selector)));
    });
    return new DOMElement(found);
  }

  // 获取父元素
  parent() {
    const parents = this.elements.map(el => el.parentElement);
    return new DOMElement(parents.filter(Boolean));
  }

  // 显示/隐藏
  show() {
    return this.css('display', '');
  }

  hide() {
    return this.css('display', 'none');
  }

  // 动画
  animate(properties, duration = 300, easing = 'ease') {
    this.elements.forEach(el => {
      el.style.transition = `all ${duration}ms ${easing}`;
      Object.assign(el.style, properties);
      
      setTimeout(() => {
        el.style.transition = '';
      }, duration);
    });
    return this;
  }
}

// 使用示例
// 假设HTML中有: <div id="myDiv">Hello</div>
const $ = (selector) => new DOMElement(selector);

$('#myDiv')
  .css({
    color: 'white',
    backgroundColor: 'blue',
    padding: '10px'
  })
  .addClass('highlight')
  .text('Hello, World!')
  .on('click', function() {
    $(this).toggleClass('active');
  })
  .animate({
    opacity: 0.8,
    transform: 'scale(1.1)'
  }, 300);
3.3 构建器模式与链式调用
// 配置对象构建器
class ConfigurationBuilder {
  constructor() {
    this.config = {
      api: {},
      ui: {},
      features: {},
      performance: {}
    };
  }

  // API配置
  withApi(baseUrl) {
    this.config.api.baseUrl = baseUrl;
    return this;
  }

  withApiVersion(version) {
    this.config.api.version = version;
    return this;
  }

  withTimeout(ms) {
    this.config.api.timeout = ms;
    return this;
  }

  // UI配置
  withTheme(theme) {
    this.config.ui.theme = theme;
    return this;
  }

  withLanguage(lang) {
    this.config.ui.language = lang;
    return this;
  }

  withDarkMode(enabled) {
    this.config.ui.darkMode = enabled;
    return this;
  }

  // 功能配置
  enableFeature(feature) {
    this.config.features[feature] = true;
    return this;
  }

  disableFeature(feature) {
    this.config.features[feature] = false;
    return this;
  }

  // 性能配置
  withCache(enabled) {
    this.config.performance.cache = enabled;
    return this;
  }

  withLazyLoad(enabled) {
    this.config.performance.lazyLoad = enabled;
    return this;
  }

  withCompression(enabled) {
    this.config.performance.compression = enabled;
    return this;
  }

  // 构建方法
  build() {
    // 验证配置
    this.validate();
    // 返回不可变配置
    return Object.freeze(JSON.parse(JSON.stringify(this.config)));
  }

  validate() {
    const { api } = this.config;
    if (!api.baseUrl) {
      throw new Error('API base URL is required');
    }
    if (api.timeout && api.timeout < 100) {
      throw new Error('Timeout must be at least 100ms');
    }
  }
}

// 使用示例
const config = new ConfigurationBuilder()
  .withApi('https://api.example.com')
  .withApiVersion('v1')
  .withTimeout(5000)
  .withTheme('dark')
  .withLanguage('en')
  .withDarkMode(true)
  .enableFeature('analytics')
  .enableFeature('notifications')
  .disableFeature('debug')
  .withCache(true)
  .withLazyLoad(true)
  .build();

console.log('App configuration:', config);

四、惰性加载/求值(Lazy Loading/Evaluation)

惰性加载延迟计算或初始化, 直到真正需要时才执行, 可以显著提高应用性能。

4.1 惰性求值实现
// 惰性函数
function lazy(fn) {
  let result;
  let evaluated = false;
  
  return function(...args) {
    if (!evaluated) {
      result = fn.apply(this, args);
      evaluated = true;
    }
    return result;
  };
}

// 惰性属性
function lazyProperty(target, propertyName, getter) {
  let value;
  let evaluated = false;
  
  Object.defineProperty(target, propertyName, {
    get() {
      if (!evaluated) {
        value = getter.call(this);
        evaluated = true;
      }
      return value;
    },
    enumerable: true,
    configurable: true
  });
}

// 惰性类属性
class LazyClass {
  constructor() {
    this._expensiveData = null;
  }

  get expensiveData() {
    if (this._expensiveData === null) {
      console.log('Computing expensive data...');
      // 模拟耗时计算
      this._expensiveData = this.computeExpensiveData();
    }
    return this._expensiveData;
  }

  computeExpensiveData() {
    // 复杂的计算逻辑
    const start = Date.now();
    let result = 0;
    for (let i = 0; i < 1000000; i++) {
      result += Math.sqrt(i);
    }
    console.log(`Computed in ${Date.now() - start}ms`);
    return result;
  }

  // 重置惰性值
  reset() {
    this._expensiveData = null;
  }
}
4.2 图片懒加载
class LazyImageLoader {
  constructor(options = {}) {
    this.options = {
      root: null,
      rootMargin: '0px',
      threshold: 0.1,
      placeholder: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
      ...options
    };
    
    this.images = new Map();
    this.observer = null;
    this.initObserver();
  }

  initObserver() {
    if ('IntersectionObserver' in window) {
      this.observer = new IntersectionObserver(
        this.handleIntersection.bind(this),
        this.options
      );
    }
  }

  registerImage(imgElement, src) {
    if (!imgElement || !src) return;

    // 保存原始src
    const originalSrc = imgElement.getAttribute('data-src') || src;
    imgElement.setAttribute('data-src', originalSrc);
    
    // 设置占位符
    imgElement.src = this.options.placeholder;
    imgElement.classList.add('lazy-load');
    
    // 添加到观察列表
    this.images.set(imgElement, {
      src: originalSrc,
      loaded: false
    });
    
    if (this.observer) {
      this.observer.observe(imgElement);
    } else {
      // 降级方案:立即加载
      this.loadImage(imgElement);
    }
  }

  handleIntersection(entries) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        this.loadImage(img);
        this.observer?.unobserve(img);
      }
    });
  }

  async loadImage(imgElement) {
    const imageData = this.images.get(imgElement);
    if (!imageData || imageData.loaded) return;

    try {
      // 预加载图片
      await this.preloadImage(imageData.src);
      
      // 应用实际图片
      imgElement.src = imageData.src;
      imgElement.classList.remove('lazy-load');
      imgElement.classList.add('lazy-loaded');
      
      imageData.loaded = true;
      
      // 触发加载完成事件
      imgElement.dispatchEvent(new CustomEvent('lazyload', {
        detail: { src: imageData.src }
      }));
    } catch (error) {
      console.error('Failed to load image:', imageData.src, error);
      imgElement.classList.add('lazy-error');
      
      imgElement.dispatchEvent(new CustomEvent('lazyloaderror', {
        detail: { src: imageData.src, error }
      }));
    }
  }

  preloadImage(src) {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve(img);
      img.onerror = reject;
      img.src = src;
    });
  }

  // 批量注册图片
  registerAll(selector = 'img[data-src]') {
    const images = document.querySelectorAll(selector);
    images.forEach(img => {
      const src = img.getAttribute('data-src');
      if (src) {
        this.registerImage(img, src);
      }
    });
  }

  // 强制加载特定图片
  forceLoad(imgElement) {
    this.loadImage(imgElement);
  }

  // 清理
  destroy() {
    this.observer?.disconnect();
    this.images.clear();
  }
}

// 使用示例
document.addEventListener('DOMContentLoaded', () => {
  const lazyLoader = new LazyImageLoader({
    threshold: 0.5,
    placeholder: '/images/placeholder.png'
  });
  
  // 注册现有图片
  lazyLoader.registerAll();
  
  // 动态添加的图片
  document.addEventListener('newImagesAdded', (event) => {
    const newImages = event.detail.images;
    newImages.forEach(img => {
      lazyLoader.registerImage(img, img.dataset.src);
    });
  });
  
  // 页面离开时清理
  window.addEventListener('beforeunload', () => {
    lazyLoader.destroy();
  });
});
4.3 组件懒加载(Vue/React示例)
// React组件懒加载
import React, { Suspense, lazy } from 'react';

// 懒加载组件
const LazyComponent = lazy(() => import('./ExpensiveComponent'));

// 使用Suspense包裹
function App() {
  return (
    <div>
      <h1>My App</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent />
      </Suspense>
    </div>
  );
}

// Vue组件懒加载
const LazyComponent = () => import('./ExpensiveComponent.vue');

// 路由配置中使用懒加载
const routes = [
  {
    path: '/dashboard',
    component: () => import('./views/Dashboard.vue')
  },
  {
    path: '/settings',
    component: () => import('./views/Settings.vue')
  }
];

// 自定义懒加载封装
function createLazyLoader(importFn, loadingComponent, errorComponent) {
  return {
    data() {
      return {
        component: null,
        error: null,
        loading: true
      };
    },
    
    async created() {
      try {
        const module = await importFn();
        this.component = module.default || module;
      } catch (err) {
        this.error = err;
        console.error('Failed to load component:', err);
      } finally {
        this.loading = false;
      }
    },
    
    render(h) {
      if (this.loading && loadingComponent) {
        return h(loadingComponent);
      }
      
      if (this.error && errorComponent) {
        return h(errorComponent, { error: this.error });
      }
      
      if (this.component) {
        return h(this.component);
      }
      
      return null;
    }
  };
}
4.4 数据懒加载(无限滚动)
class InfiniteScroll {
  constructor(options = {}) {
    this.options = {
      container: document.documentElement,
      distance: 100,
      throttle: 200,
      onLoadMore: () => Promise.resolve(),
      ...options
    };
    
    this.loading = false;
    this.hasMore = true;
    this.throttleTimer = null;
    
    this.init();
  }

  init() {
    this.container = typeof this.options.container === 'string' 
      ? document.querySelector(this.options.container)
      : this.options.container;
    
    if (!this.container) {
      console.error('Container not found');
      return;
    }
    
    this.bindEvents();
  }

  bindEvents() {
    this.container.addEventListener('scroll', this.handleScroll.bind(this));
    window.addEventListener('resize', this.handleScroll.bind(this));
  }

  handleScroll() {
    if (this.throttleTimer) {
      clearTimeout(this.throttleTimer);
    }
    
    this.throttleTimer = setTimeout(() => {
      this.checkPosition();
    }, this.options.throttle);
  }

  checkPosition() {
    if (this.loading || !this.hasMore) return;
    
    const scrollTop = this.container.scrollTop;
    const scrollHeight = this.container.scrollHeight;
    const clientHeight = this.container.clientHeight;
    
    const distanceToBottom = scrollHeight - (scrollTop + clientHeight);
    
    if (distanceToBottom <= this.options.distance) {
      this.loadMore();
    }
  }

  async loadMore() {
    if (this.loading || !this.hasMore) return;
    
    this.loading = true;
    this.container.dispatchEvent(new CustomEvent('loadstart'));
    
    try {
      const result = await this.options.onLoadMore();
      
      if (result && typeof result.hasMore === 'boolean') {
        this.hasMore = result.hasMore;
      }
      
      this.container.dispatchEvent(new CustomEvent('load', { 
        detail: result 
      }));
    } catch (error) {
      this.container.dispatchEvent(new CustomEvent('error', { 
        detail: error 
      }));
      console.error('Failed to load more data:', error);
    } finally {
      this.loading = false;
      this.container.dispatchEvent(new CustomEvent('loadend'));
      
      // 检查是否还需要继续加载(数据可能没有填满屏幕)
      if (this.hasMore) {
        setTimeout(() => this.checkPosition(), 100);
      }
    }
  }

  // 手动触发加载
  triggerLoad() {
    this.loadMore();
  }

  // 重置状态
  reset() {
    this.loading = false;
    this.hasMore = true;
  }

  // 销毁
  destroy() {
    this.container.removeEventListener('scroll', this.handleScroll);
    window.removeEventListener('resize', this.handleScroll);
    
    if (this.throttleTimer) {
      clearTimeout(this.throttleTimer);
    }
  }
}

// 使用示例
const infiniteScroll = new InfiniteScroll({
  container: '#scrollContainer',
  distance: 200,
  throttle: 300,
  async onLoadMore() {
    // 模拟API调用
    const response = await fetch(`/api/items?page=${currentPage}`);
    const data = await response.json();
    
    // 渲染新数据
    renderItems(data.items);
    
    // 返回是否有更多数据
    return { hasMore: data.hasMore };
  }
});

// 动态更新选项
function updateInfiniteScroll(options) {
  infiniteScroll.options = { ...infiniteScroll.options, ...options };
}

五、模式对比与应用场景

模式 优点 缺点 适用场景
中间件模式 解耦、可组合、易于测试 可能增加复杂性、调试困难 请求处理、数据处理管道、插件系统
管道模式 清晰的数据流向、易于测试和复用 可能创建太多小函数、错误处理复杂 数据转换、验证、清洗流程
链式调用 代码流畅、易读、减少临时变量 可能掩盖错误来源、调试困难 构建器模式、DOM操作、配置设置
惰性加载 提高性能】减少内存使用 初始化延迟、复杂性增加 图片加载、组件加载、数据计算
5.1 如何选择模式?
  1. 需要处理请求/响应流程 → 中间件模式
  2. 需要数据转换流水线 → 管道模式
  3. 需要流畅的API接口 → 链式调用
  4. 需要优化性能,延迟初始化 → 惰性加载

六、综合应用实例

6.1 完整的API客户端
class ApiClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
    this.middlewares = [];
    this.defaultHeaders = {
      'Content-Type': 'application/json'
    };
  }

  // 添加中间件
  use(middleware) {
    this.middlewares.push(middleware);
    return this;
  }

  // 创建请求管道
  async request(endpoint, options = {}) {
    const context = {
      url: `${this.baseURL}${endpoint}`,
      options: {
        method: 'GET',
        headers: { ...this.defaultHeaders, ...options.headers },
        ...options
      },
      response: null,
      error: null,
      data: null
    };

    // 执行中间件管道
    await this.executeMiddleware(context);
    
    if (context.error) {
      throw context.error;
    }

    return context.response;
  }

  async executeMiddleware(context) {
    let index = 0;
    const middlewares = this.middlewares;
    
    const next = async () => {
      if (index < middlewares.length) {
        const middleware = middlewares[index++];
        await middleware(context, next);
      } else {
        // 执行实际请求
        await this.executeRequest(context);
      }
    };
    
    await next();
  }

  async executeRequest(context) {
    try {
      const response = await fetch(context.url, context.options);
      context.response = {
        status: response.status,
        headers: Object.fromEntries(response.headers.entries()),
        data: await response.json()
      };
    } catch (error) {
      context.error = error;
    }
  }

  // 快捷方法(链式调用)
  get(endpoint, options = {}) {
    return this.request(endpoint, { ...options, method: 'GET' });
  }

  post(endpoint, data, options = {}) {
    return this.request(endpoint, {
      ...options,
      method: 'POST',
      body: JSON.stringify(data)
    });
  }

  put(endpoint, data, options = {}) {
    return this.request(endpoint, {
      ...options,
      method: 'PUT',
      body: JSON.stringify(data)
    });
  }

  delete(endpoint, options = {}) {
    return this.request(endpoint, { ...options, method: 'DELETE' });
  }
}

// 创建API客户端并配置中间件
const api = new ApiClient('https://api.example.com')
  .use(async (ctx, next) => {
    // 认证中间件
    const token = localStorage.getItem('auth_token');
    if (token) {
      ctx.options.headers.Authorization = `Bearer ${token}`;
    }
    await next();
  })
  .use(async (ctx, next) => {
    // 日志中间件
    console.log(`[API] ${ctx.options.method} ${ctx.url}`);
    const start = Date.now();
    await next();
    const duration = Date.now() - start;
    console.log(`[API] Completed in ${duration}ms`);
  })
  .use(async (ctx, next) => {
    // 错误处理中间件
    try {
      await next();
    } catch (error) {
      console.error('[API] Request failed:', error);
      // 可以在这里实现重试逻辑
      throw error;
    }
  });

// 使用示例
async function fetchUserData() {
  try {
    const response = await api.get('/users/1');
    console.log('User data:', response.data);
    return response.data;
  } catch (error) {
    console.error('Failed to fetch user:', error);
  }
}
6.2 表单处理系统
class FormProcessor {
  constructor(formElement) {
    this.form = formElement;
    this.fields = new Map();
    this.validators = new Map();
    this.transformers = [];
    this.submitHandlers = [];
    
    this.init();
  }

  init() {
    // 收集表单字段
    Array.from(this.form.elements).forEach(element => {
      if (element.name) {
        this.fields.set(element.name, element);
      }
    });
    
    // 绑定提交事件
    this.form.addEventListener('submit', this.handleSubmit.bind(this));
  }

  // 添加验证器
  addValidator(fieldName, validator) {
    if (!this.validators.has(fieldName)) {
      this.validators.set(fieldName, []);
    }
    this.validators.get(fieldName).push(validator);
    return this;
  }

  // 添加数据转换器
  addTransformer(transformer) {
    this.transformers.push(transformer);
    return this;
  }

  // 添加提交处理器
  onSubmit(handler) {
    this.submitHandlers.push(handler);
    return this;
  }

  // 获取表单数据
  getData() {
    const data = {};
    this.fields.forEach((element, name) => {
      data[name] = this.getValue(element);
    });
    return data;
  }

  getValue(element) {
    if (element.type === 'checkbox') {
      return element.checked;
    } else if (element.type === 'radio') {
      return element.checked ? element.value : null;
    } else if (element.type === 'select-multiple') {
      return Array.from(element.selectedOptions).map(opt => opt.value);
    }
    return element.value;
  }

  // 验证表单
  validate() {
    const errors = {};
    const data = this.getData();
    
    this.validators.forEach((validators, fieldName) => {
      const value = data[fieldName];
      const fieldErrors = [];
      
      validators.forEach(validator => {
        const result = validator(value, data);
        if (result !== true) {
          fieldErrors.push(result || `Validation failed for ${fieldName}`);
        }
      });
      
      if (fieldErrors.length > 0) {
        errors[fieldName] = fieldErrors;
        this.showFieldError(fieldName, fieldErrors[0]);
      } else {
        this.clearFieldError(fieldName);
      }
    });
    
    return {
      isValid: Object.keys(errors).length === 0,
      errors,
      data
    };
  }

  showFieldError(fieldName, message) {
    const field = this.fields.get(fieldName);
    if (field) {
      field.classList.add('error');
      
      let errorElement = field.parentElement.querySelector('.error-message');
      if (!errorElement) {
        errorElement = document.createElement('div');
        errorElement.className = 'error-message';
        field.parentElement.appendChild(errorElement);
      }
      errorElement.textContent = message;
    }
  }

  clearFieldError(fieldName) {
    const field = this.fields.get(fieldName);
    if (field) {
      field.classList.remove('error');
      const errorElement = field.parentElement.querySelector('.error-message');
      if (errorElement) {
        errorElement.remove();
      }
    }
  }

  // 处理提交
  async handleSubmit(event) {
    event.preventDefault();
    
    // 验证
    const validation = this.validate();
    if (!validation.isValid) {
      console.log('Form validation failed:', validation.errors);
      return;
    }
    
    // 数据转换管道
    let processedData = validation.data;
    for (const transformer of this.transformers) {
      processedData = transformer(processedData);
    }
    
    // 执行提交处理器
    for (const handler of this.submitHandlers) {
      try {
        const result = await handler(processedData);
        if (result === false || result?.stopPropagation) {
          break;
        }
      } catch (error) {
        console.error('Submit handler error:', error);
        break;
      }
    }
  }

  // 重置表单
  reset() {
    this.form.reset();
    this.fields.forEach((field, name) => {
      this.clearFieldError(name);
    });
    return this;
  }
}

// 使用示例
const formProcessor = new FormProcessor(document.getElementById('myForm'))
  .addValidator('email', (value) => {
    if (!value) return 'Email is required';
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
      return 'Invalid email format';
    }
    return true;
  })
  .addValidator('password', (value) => {
    if (!value) return 'Password is required';
    if (value.length < 8) return 'Password must be at least 8 characters';
    return true;
  })
  .addTransformer((data) => {
    // 转换数据
    return {
      ...data,
      email: data.email.toLowerCase().trim(),
      createdAt: new Date().toISOString()
    };
  })
  .onSubmit(async (data) => {
    console.log('Submitting data:', data);
    
    // 模拟API调用
    const response = await fetch('/api/register', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    
    if (!response.ok) {
      throw new Error('Registration failed');
    }
    
    alert('Registration successful!');
    return true;
  })
  .onSubmit((data) => {
    // 第二个处理器:发送分析事件
    console.log('Analytics event sent for registration');
  });

七、最佳实践与注意事项

7.1 中间件模式最佳实践
  1. 保持中间件简洁单一: 每个中间件只做一件事
  2. 错误处理: 确保中间件有适当的错误处理机制
  3. 性能考虑: 避免在中间件中进行昂贵的同步操作
  4. 顺序很重要: 注意中间件的执行顺序对业务逻辑的影响
7.2 管道模式最佳实践
  1. 纯函数优先: 确保管道中的函数是纯函数,避免副作用
  2. 类型检查: 考虑添加运行时类型检查
  3. 错误处理: 现管道级别的错误处理机制
  4. 性能优化: 考虑使用流式处理大数据集
7.3 链式调用最佳实践
  1. 返回this: 确保每个链式方法都返回实例本身
  2. 不可变操作: 考虑实现不可变版本的链式调用
  3. 清晰的方法名: 方法名应该清晰表达其功能
  4. 文档完善: 链式调用可能隐藏复杂度,需要良好文档
7.4 惰性加载最佳实践
  1. 适度使用: 不要过度使用惰性加载,会增加复杂度
  2. 预加载策略: 对于可能很快需要的内容,考虑预加载
  3. 错误处理: 确保惰性加载失败时有降级方案
  4. 用户反馈: 加载过程中给用户适当的反馈

总结

这四种前端模式-中间件模式、管道模式、链式调用和惰性加载---都是现代前端开发中极其有用的工具。它们各自解决了不同的问题:

  • 中间件模式提供了处理复杂流程的模块化方式
  • 管道模式让数据转换变得清晰和可组合
  • 链式调用创造了流畅、易读的API
  • 惰性加载优化了性能和资源使用

掌握这些模式并知道何时使用它们,将帮助你编写更可维护、更高效的前端代码。记住,设计模式是工具,而不是银弹。根据具体场景选择最合适的模式,并始终以代码清晰性和可维护性为首要考虑。

在实际项目中,这些模式经常组合使用。例如,一个API客户端可能同时使用中间件模式处理请求、管道模式处理数据转换、链式调用提供流畅API,并在适当的地方使用惰性加载优化性能。

希望这篇文章能帮助你在前端开发中更好地应用这些强大的模式!

❌
❌