import/export:前端模块化实战 | JS 基础语法与数据操作
介绍前端 ES Modules 中 import 和 export 的基础用法及最佳实践。通过对比非模块化代码的痛点,阐述了模块化在隔离作用域、明确依赖关系和提升可维护性方面的优势。提供了基于 Vite + Vue 的最小可运行项目示例,包含 utils、api、composables 和 constants 的标准目录结构。最后总结了常见坑点,如循环依赖、魔法数字等,旨在帮助开发者建立清晰的前端工程化思维。

介绍前端 ES Modules 中 import 和 export 的基础用法及最佳实践。通过对比非模块化代码的痛点,阐述了模块化在隔离作用域、明确依赖关系和提升可维护性方面的优势。提供了基于 Vite + Vue 的最小可运行项目示例,包含 utils、api、composables 和 constants 的标准目录结构。最后总结了常见坑点,如循环依赖、魔法数字等,旨在帮助开发者建立清晰的前端工程化思维。

假设你要做一个「用户列表」页面,需要:
如果全写在一个文件里,大概是这样的:
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>用户列表</title>
</head>
<body>
<div id="app"></div>
<script src="./app.js"></script>
</body>
</html>
// app.js - 所有逻辑全塞在一起,有 100 多行
const API_BASE = '/api';
const list = [];
let loading = false;
function formatDate(date) {
return new Date(date).toLocaleDateString('zh-CN');
}
function getUserList() {
loading = true;
fetch(API_BASE + '/user/list')
.then(res => res.json())
.then(data => {
// 注意:这里 list 是 const,会报错!变量多了容易搞混
list = data;
loading = false;
});
}
// 假设还有 10 个其他函数...
// 三个月后你回来改 bug,根本找不到 formatDate 在哪...
问题很快就出来了:
formatDate 如果别的页面也要用,只能复制粘贴模块化要解决的,就是这三个问题。
import 明确「谁用了谁」后面我们就用 ES Modules(import / export)来做这件事。
两种常见用法:
方式一:命名导出(一个文件可以导出多个)
// utils/format.js
export function formatDate(date) {
return new Date(date).toLocaleDateString('zh-CN');
}
export function formatMoney(num) {
return `¥${num.toFixed(2)}`;
}
方式二:默认导出(一个文件只能有一个 default)
// config.js
export default {
apiBase: '/api',
timeout: 5000,
};
import 时要用同名import 时可以随便起名字// 引入命名导出:名字必须和 export 的一致
import { formatDate, formatMoney } from './utils/format.js';
// 引入默认导出:名字可以随便起
import config from './config.js';
console.log(config.apiBase); // '/api'
// 把整个模块当作对象引入
import * as formatUtils from './utils/format.js';
formatUtils.formatDate(new Date());
记住一个区别:
| 写法 | 含义 |
|---|---|
import { foo } | 引入命名导出,{} 是语法,不是解构 |
import foo | 引入默认导出 |
下面是一个最小可运行项目,涵盖:utils、api、constants、composables,可以直接照抄跑起来。
my-project/
├── index.html
├── package.json
├── vite.config.js
└── src/
├── main.js # 入口
├── constants/
│ └── index.js # 常量
├── utils/
│ ├── format.js # 格式化
│ └── index.js # 统一导出
├── api/
│ ├── request.js # 请求封装
│ ├── user.js # 用户接口
│ └── index.js # 统一导出
├── composables/ # Vue 组合式函数(可复用逻辑)
│ └── useUserList.js
└── App.vue # 根组件
1. package.json
{
"name": "module-demo",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite"
},
"dependencies": {
"vue": "^3.4.0",
"axios": "^1.6.0"
},
"devDependencies": {
"vite": "^5.0.0",
"@vitejs/plugin-vue": "^5.0.0"
}
}
2. index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>用户列表 - 模块化示例</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
3. src/constants/index.js
// 常量:放那些「写死不变」的值,以后改起来只改这一处
export const API_BASE = '/api';
export const TIMEOUT = 10000;
// 订单状态:用常量代替魔法数字 0、1、2、3
export const ORDER_STATUS = {
PENDING: 0,
PAID: 1,
SHIPPED: 2,
COMPLETED: 3,
};
export const ORDER_STATUS_TEXT = {
0: '待支付',
1: '已支付',
2: '已发货',
3: '已完成',
};
4. src/utils/format.js
// 纯函数:同样的输入,一定得到同样的输出,不依赖外部状态
export function formatDate(date, options = {}) {
if (!date) return '-';
return new Date(date).toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
...options,
});
}
export function formatMoney(num) {
if (num == null || isNaN(num)) return '¥0.00';
return `¥${Number(num).toFixed(2)}`;
}
export function formatPhone(phone) {
if (!phone || phone.length !== 11) return phone;
return `${phone.slice(0, 3)}****${phone.slice(7)}`;
}
5. src/utils/index.js
// 统一导出:别人只需要 import from '@/utils' 就能拿到所有工具
export { formatDate, formatMoney, formatPhone } from './format.js';
6. src/api/request.js
import axios from 'axios';
import { API_BASE, TIMEOUT } from '../constants/index.js';
// 创建一个配置好的 axios 实例,所有请求都走它
const request = axios.create({
baseURL: API_BASE,
timeout: TIMEOUT,
});
// 响应拦截器:统一处理错误和数据结构
request.interceptors.response.use(
(res) => res.data, // 直接返回 data,调用方少写一层
(err) => {
if (err.response?.status === 401) {
console.warn('未登录,请先登录');
// 实际项目:跳转登录页
}
return Promise.reject(err);
}
);
export default request;
7. src/api/user.js
import request from './request.js';
// 每个函数对应一个接口,参数和返回值一目了然
export function getUserList(params = {}) {
return request.get('/user/list', { params });
}
export function getUserDetail(id) {
return request.get(`/user/${id}`);
}
8. src/api/index.js
export * from './user.js';
// 以后有 order.js,再加一行:export * from './order.js';
9. src/composables/useUserList.js
import { ref, watch } from 'vue';
import { getUserList } from '../api/index.js';
// 把「拉列表 + loading + error」抽成组合式函数,任何组件都能复用
export function useUserList(params = {}) {
const list = ref([]);
const loading = ref(false);
const error = ref(null);
async function fetchList() {
loading.value = true;
error.value = null;
try {
const res = await getUserList(params);
list.value = res.data || res;
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
}
// 当 params 变化时重新请求
watch(() => [params.page, params.pageSize], () => fetchList(), {
immediate: true,
});
return { list, loading, error, fetchList };
}
10. src/App.vue
<template>
<div v-if="loading">加载中...</div>
<div v-else-if="error">出错了:{{ error }}</div>
<div v-else style="padding: 20px">
<h1>用户列表</h1>
<ul>
<li v-for="user in list" :key="user.id">
{{ user.name }} - 注册时间:{{ formatDate(user.createdAt) }}
</li>
</ul>
</div>
</template>
<script setup>
import { useUserList } from './composables/useUserList.js';
import { formatDate } from './utils/index.js';
const { list, loading, error } = useUserList({ page: 1, pageSize: 10 });
</script>
11. src/main.js
import { createApp } from 'vue';
import App from './App.vue';
createApp(App).mount('#app');
12. vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
});
cd my-project
npm install
npm run dev
浏览器打开 http://localhost:5173 即可(接口需自行 mock 或接真实后端)。
formatDate、formatMoney、isValidPhonegetUserList、getUserDetailuseUserList、useDebounceORDER_STATUS、API_BASE| 坑 | 建议 |
|---|---|
| utils 里写业务逻辑 | 工具函数只做通用处理,不依赖具体业务 |
| api 层写业务判断 | api 只管请求和错误,业务逻辑在组件/composable 里 |
| 魔法数字 | 状态码、业务码用常量,如 ORDER_STATUS.PENDING |
| watch 依赖写错 | watch 的监听源要写对,否则会多请求或漏更新 |
| ref 忘记 .value | 在 script 里访问 ref 要加 .value,模板中自动解包 |
| 循环依赖 | 保持单向依赖:constants → api → composables → 组件 |
模块化的目的就三点:职责清晰、依赖明确、好维护。
先按 utils、api、composables、constants 这几类把代码分好,再按项目规模慢慢细化,不必一上来就设计得很复杂。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online