【前端工具函数规范】+【中后台前端开发】:从抽离逻辑到目录规范,彻底搞懂utils工具函数的落地写法,避开全局污染、命名混乱等高频坑!
📑 文章目录
- 一、开篇:为什么要单独讲「工具函数」?
- 二、核心概念:什么是「工具函数」?
- [三、怎么拆分:按领域划分 utils](#三、怎么拆分:按领域划分 utils)
- [3.1 推荐目录结构(以 Vue 为例)](#3.1 推荐目录结构(以 Vue 为例))
- [3.2 拆分原则(三个问题)](#3.2 拆分原则(三个问题))
- [3.3 完整示例:format.js](#3.3 完整示例:format.js)
- 四、命名规范:一眼能看懂用途
- [4.1 基本规则](#4.1 基本规则)
- [4.2 常用动词前缀](#4.2 常用动词前缀)
- [4.3 命名示例](#4.3 命名示例)
- 五、怎么复用:入口统一、按需引入
- [5.1 统一入口 index.js](#5.1 统一入口 index.js)
- [5.2 使用方式(两种)](#5.2 使用方式(两种))
- [5.3 避免的用法](#5.3 避免的用法)
- 六、避免全局污染:作用域与模块化
- [6.1 什么是全局污染?](#6.1 什么是全局污染?)
- [6.2 正确做法:ES Module](#6.2 正确做法:ES Module)
- [6.3 旧项目怎么办?](#6.3 旧项目怎么办?)
- [七、完整实战:从零搭一套 utils](#七、完整实战:从零搭一套 utils)
- [7.1 format.js](#7.1 format.js)
- [7.2 validate.js](#7.2 validate.js)
- [7.3 storage.js](#7.3 storage.js)
- [7.4 index.js(统一导出)](#7.4 index.js(统一导出))
- [7.5 在 Vue 组件中使用](#7.5 在 Vue 组件中使用)
- 八、常见踩坑总结
- 九、小结
- [🔍 系列模块导航](#🔍 系列模块导航)
同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。
(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)
很多前端开发者都会遇到一个瓶颈:
代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。
想写出干净、优雅、可维护 的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验。
这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。
帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。
一、开篇:为什么要单独讲「工具函数」?
很多人学 JavaScript 会用 Array.map、String.split,但一提到「写自己的工具函数」就懵了------什么时候该写?写在哪?怎么命名、怎么拆分、怎么复用?
这篇文章的目的很简单:
让你在日常开发中,清楚地知道:什么时候抽工具函数、抽出来后放哪、叫什么名字、怎么用,顺带帮你避开「全局污染」「命名冲突」这些坑。
适用读者:
- 会写 JS,但对「函数组织」概念不太清晰的
- 从零开始学前端,想建立良好习惯的
- 有经验但想统一自己团队/项目规范的
下面按:为什么要拆 → 怎么拆 → 怎么命名 → 怎么复用 → 怎么避免全局污染 来写。
[⬆ 返回目录](#⬆ 返回目录)
二、核心概念:什么是「工具函数」?
工具函数(utility function) 指的是:只做一件事、与业务逻辑解耦、可在多处复用的纯函数。
常见特征:
| 特征 | 说明 |
|---|---|
| 单一职责 | 每个函数只负责一件事 |
| 无副作用 | 不直接改 DOM、不发请求,输入相同则输出相同 |
| 可复用 | 多个页面、组件都能用 |
| 可测试 | 输入输出明确,容易写单测 |
示例:符合 vs 不符合
js
// ❌ 不是好的工具函数:掺杂业务逻辑、改全局状态
function handleUserSubmit(userId, formData) {
document.getElementById('submitBtn').disabled = true; // 副作用
const res = await fetch(`/api/user/${userId}`); // 业务耦合
return res.json();
}
// ✅ 符合工具函数:纯逻辑、无副作用
function formatDate(timestamp, format = 'YYYY-MM-DD') {
const date = new Date(timestamp);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return format.replace('YYYY', year).replace('MM', month).replace('DD', day);
}
[⬆ 返回目录](#⬆ 返回目录)
三、怎么拆分:按领域划分 utils
不要把所有工具函数丢进一个巨大的 utils.js,而是按功能领域拆成多个模块。
3.1 推荐目录结构(以 Vue 为例)
src/
├── utils/
│ ├── index.js # 统一导出入口
│ ├── format.js # 格式化:日期、金额、手机号等
│ ├── validate.js # 校验:手机、邮箱、身份证等
│ ├── storage.js # 本地存储封装
│ ├── request.js # 请求封装(可单独成 api 目录)
│ └── string.js # 字符串:截断、脱敏等
思路:
- 每个文件只处理一类问题
- 业务扩展时,优先在对应文件里加函数,不够再新建文件
[⬆ 返回目录](#⬆ 返回目录)
3.2 拆分原则(三个问题)
拆之前可以问自己:
- 这个逻辑会不会在多个地方用到?会 → 抽成工具函数。
- 它属于哪一类?(格式化 / 校验 / 存储 / 请求 / 字符串等)按类别放进对应文件。
- 它是否依赖具体业务?依赖业务 → 放业务模块;不依赖 → 放 utils。
[⬆ 返回目录](#⬆ 返回目录)
3.3 完整示例:format.js
js
/**
* 格式化相关工具函数
* @file format.js
*/
/**
* 格式化日期
* @param {number|string|Date} timestamp - 时间戳或日期对象
* @param {string} format - 输出格式,如 'YYYY-MM-DD'、'YYYY/MM/DD HH:mm'
* @returns {string}
*/
export function formatDate(timestamp, format = 'YYYY-MM-DD') {
const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
if (isNaN(date.getTime())) return '';
const pad = (n) => String(n).padStart(2, '0');
const map = {
YYYY: date.getFullYear(),
MM: pad(date.getMonth() + 1),
DD: pad(date.getDate()),
HH: pad(date.getHours()),
mm: pad(date.getMinutes()),
ss: pad(date.getSeconds()),
};
return format.replace(/YYYY|MM|DD|HH|mm|ss/g, (key) => map[key] || key);
}
/**
* 格式化金额(千分位)
* @param {number} num - 数字
* @param {number} decimals - 小数位数
* @returns {string}
*/
export function formatMoney(num, decimals = 2) {
const fixed = Number(num).toFixed(decimals);
const [int, dec] = fixed.split('.');
const intWithComma = int.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return dec ? `${intWithComma}.${dec}` : intWithComma;
}
[⬆ 返回目录](#⬆ 返回目录)
四、命名规范:一眼能看懂用途
4.1 基本规则
| 规则 | 说明 | 示例 |
|---|---|---|
| 动词开头 | 表示「做什么」 | formatDate、validateEmail |
| 见名知意 | 名字要能反映功能 | 用 formatMoney 而不是 fm |
| 避免否定 | 少用 isNotXxx,多用肯定 |
用 isEmpty 而不是 isNotEmpty(或 hasContent) |
[⬆ 返回目录](#⬆ 返回目录)
4.2 常用动词前缀
format - 格式化:formatDate, formatPhone
validate/check - 校验:validateEmail, checkIdCard
get - 获取:getStorage, getQuery
set - 设置:setStorage
parse - 解析:parseQuery, parseJSON
debounce/throttle - 节流防抖
[⬆ 返回目录](#⬆ 返回目录)
4.3 命名示例
js
// ✅ 清晰
function formatPhone(phone) { /* 格式化手机号 */ }
function validateEmail(email) { /* 校验邮箱 */ }
function getStorage(key) { /* 获取本地存储 */ }
// ❌ 模糊
function phone(p) { /* 做什么不清楚 */ }
function check(e) { /* 检查什么不清楚 */ }
function util1(x) { /* 完全无意义 */ }
[⬆ 返回目录](#⬆ 返回目录)
五、怎么复用:入口统一、按需引入
5.1 统一入口 index.js
把所有 utils 在一个入口里导出,方便统一管理和按需引入:
js
/**
* utils 统一导出
* 使用方式:import { formatDate, validateEmail } from '@/utils'
*/
// 格式化
export { formatDate, formatMoney } from './format';
// 校验
export { validateEmail, validatePhone } from './validate';
// 存储
export { getStorage, setStorage, removeStorage } from './storage';
[⬆ 返回目录](#⬆ 返回目录)
5.2 使用方式(两种)
js
// 方式一:按需引入(推荐,利于 tree-shaking)
import { formatDate, validateEmail } from '@/utils';
// 方式二:引入整个模块(适合一次性用很多)
import * as formatUtils from '@/utils/format';
formatUtils.formatDate(1234567890);
[⬆ 返回目录](#⬆ 返回目录)
5.3 避免的用法
js
// ❌ 全局挂在 window 上,容易污染和冲突
window.formatDate = formatDate;
// ❌ 在 main.js 里全量挂到 Vue 原型
Vue.prototype.$formatDate = formatDate; // 现代项目不推荐
[⬆ 返回目录](#⬆ 返回目录)
六、避免全局污染:作用域与模块化
6.1 什么是全局污染?
把函数挂在 window 上,会让所有脚本都能访问,容易:
- 命名冲突(多个库都用
format) - 难以追踪来源
- 不利于打包和按需加载
js
// ❌ 全局污染
window.myFormat = function() { /* ... */ };
[⬆ 返回目录](#⬆ 返回目录)
6.2 正确做法:ES Module
用 export 明确导出,用 import 明确引入,所有函数都在模块作用域内:
js
// format.js
export function formatDate(timestamp) { /* ... */ }
// 组件中
import { formatDate } from '@/utils/format';
[⬆ 返回目录](#⬆ 返回目录)
6.3 旧项目怎么办?
老项目可能是 IIFE + 全局变量,可以逐步改成模块:
js
// 旧写法
(function(global) {
global.myUtils = {
formatDate: function(timestamp) { /* ... */ }
};
})(window);
// 新写法:用模块
// format.js
export function formatDate(timestamp) { /* ... */ }
[⬆ 返回目录](#⬆ 返回目录)
七、完整实战:从零搭一套 utils
下面是一套可直接复用的工具函数示例,覆盖格式化、校验、存储三个常见场景。
7.1 format.js
js
/**
* 格式化工具
*/
export function formatDate(timestamp, format = 'YYYY-MM-DD') {
const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
if (isNaN(date.getTime())) return '';
const pad = (n) => String(n).padStart(2, '0');
const map = {
YYYY: date.getFullYear(),
MM: pad(date.getMonth() + 1),
DD: pad(date.getDate()),
HH: pad(date.getHours()),
mm: pad(date.getMinutes()),
ss: pad(date.getSeconds()),
};
return format.replace(/YYYY|MM|DD|HH|mm|ss/g, (key) => map[key] || key);
}
export function formatPhone(phone) {
const str = String(phone).replace(/\D/g, '');
if (str.length !== 11) return phone;
return str.replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3');
}
[⬆ 返回目录](#⬆ 返回目录)
7.2 validate.js
js
/**
* 校验工具
*/
export function validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email || '');
}
export function validatePhone(phone) {
return /^1[3-9]\d{9}$/.test(String(phone).replace(/\s/g, ''));
}
[⬆ 返回目录](#⬆ 返回目录)
7.3 storage.js
js
/**
* 本地存储封装(自动 JSON 序列化)
*/
const PREFIX = 'myapp_';
export function getStorage(key, defaultValue = null) {
try {
const val = localStorage.getItem(PREFIX + key);
return val ? JSON.parse(val) : defaultValue;
} catch {
return defaultValue;
}
}
export function setStorage(key, value) {
try {
localStorage.setItem(PREFIX + key, JSON.stringify(value));
} catch (e) {
console.warn('setStorage error:', e);
}
}
[⬆ 返回目录](#⬆ 返回目录)
7.4 index.js(统一导出)
js
export { formatDate, formatPhone } from './format';
export { validateEmail, validatePhone } from './validate';
export { getStorage, setStorage } from './storage';
[⬆ 返回目录](#⬆ 返回目录)
7.5 在 Vue 组件中使用
html
<template>
<div>
<p>注册时间:{{ displayDate }}</p>
<p>手机号:{{ displayPhone }}</p>
</div>
</template>
<script>
import { formatDate, formatPhone, validateEmail } from '@/utils';
export default {
data() {
return {
user: {
registerTime: 1700000000000,
phone: '13800138000',
email: 'test@example.com',
},
};
},
computed: {
displayDate() {
return formatDate(this.user.registerTime, 'YYYY年MM月DD日');
},
displayPhone() {
return formatPhone(this.user.phone);
},
},
methods: {
submit() {
if (!validateEmail(this.user.email)) {
this.$message.error('邮箱格式不正确');
return;
}
// 提交逻辑...
},
},
};
</script>
[⬆ 返回目录](#⬆ 返回目录)
八、常见踩坑总结
| 坑 | 原因 | 建议 |
|---|---|---|
| 一个 utils.js 几千行 | 没按领域拆分 | 按 format、validate、storage 等拆分 |
| 命名看不懂 | 缩写过度、语义不清 | 用 formatDate 这类动词+名词 |
到处 window.xxx |
不了解模块化 | 使用 ES Module 导出/引入 |
| 组件里写死逻辑 | 不会抽函数 | 抽成纯函数,再放到 utils |
| 工具函数里发请求 | 业务和工具混在一起 | 工具只做纯逻辑,请求放 api 层 |
[⬆ 返回目录](#⬆ 返回目录)
九、小结
- 为什么拆:逻辑复用、易测试、易维护。
- 怎么拆:按 format、validate、storage 等领域分文件。
- 怎么命名:动词开头、见名知意,少用缩写。
- 怎么复用 :统一在
index.js导出,按需import。 - 怎么避免污染 :用 ES Module,不挂
window。
养成「能复用就抽、抽了就放对位置」的习惯,对团队协作和长期维护都很重要。
[⬆ 返回目录](#⬆ 返回目录)
🔍 系列模块导航
📝 编码语法规范
这是前端规范实战系列中第二个模块,当编码语法规范模块更新完成之后会附上此模块的跳转链接,方便同学们阅读学习。
更新中,敬请期待~
👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~
📚 系列总览
前端规范实战系列目前正在持续更新中,当该系列完结之后我会整理出一篇《前端规范实战系列全系列目录导航》,届时会附上文章简介以及跳转链接,方便同学们按顺序体系化的学习~
更新中,敬请期待~
[⬆ 返回目录](#⬆ 返回目录)
技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护。
哪怕每次只吃透一条规范,长期下来,差距会非常明显。
后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。
觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。
我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~
