Vue 3 模块导入教程:import、export 与文件组织
在 Vue 3 项目中,一个页面通常会同时使用 Vue API、第三方 npm 包、.vue 组件、接口模块、工具函数、样式文件和配置文件。
这些内容不会自动出现在当前文件中,而是通过 ES Module 的 import 与 export 建立联系。
本文重点讲解:
import与export的基本作用;- 默认导出与具名导出;
- npm 包的导入方法;
.vue、.js和.ts文件的导入方法;@路径别名;- 静态导入、动态导入与路由懒加载;
- 副作用导入;
main.ts中的真实导入流程;- 常见导入错误与排查方法。
1、先理解 Vue 项目的模块化结构
现代 Vue 项目通常基于 ES Module,也就是 JavaScript 的模块系统。
每个文件都可以看作一个独立模块,例如:
text
src/
├─ main.ts
├─ App.vue
├─ router/
│ └─ index.ts
├─ permission.ts
├─ layout/
│ ├─ index.vue
│ └─ components/
│ └─ AppMain.vue
├─ views/
│ └─ bof/
│ └─ heat/
│ ├─ index.vue
│ └─ twin.vue
└─ utils/
└─ request.ts
这些文件不会自动认识彼此。
当一个文件需要使用另一个文件中的内容时,需要通过:
typescript
import ...
进行导入。
当一个文件希望把自己的变量、函数、类或组件提供给其他文件使用时,需要通过:
typescript
export ...
进行导出。
因此,模块化最基本的关系是:
text
一个文件 export
↓
另一个文件 import
↓
导入后才能使用
2、import 到底是什么
import 的作用是从其他模块中引入内容。
最常见的格式是:
typescript
import 导入内容 from '模块路径';
例如:
typescript
import { createApp } from 'vue';
这表示:
从名为
vue的包中,导入一个叫作createApp的成员。
再例如:
typescript
import App from './App.vue';
这表示:
从当前目录的
App.vue文件中,导入该文件的默认导出,并在当前文件中把它命名为App。
需要注意的是,下面两行虽然都使用 import,但导入目标不同:
typescript
import { createApp } from 'vue';
import App from './App.vue';
第一行导入的是安装在项目中的 npm 包。
第二行导入的是项目内部文件。
3、导入 npm 包
3.1 什么是 npm 包
npm 包是通过 npm、pnpm 或 yarn 安装到项目中的第三方模块。
例如:
bash
npm install vue
npm install vue-router
npm install three
安装后,依赖信息通常会记录在:
text
package.json
实际包文件一般存放在:
text
node_modules/
代码中导入 npm 包时,通常不写 ./ 或 ../:
typescript
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import * as THREE from 'three';
判断规则可以暂时记成:
text
路径以 ./ 或 ../ 开头
→ 项目内部相对路径
路径以 @/ 开头
→ 项目内部别名路径
路径直接写 vue、vue-router、three
→ npm 包
3.2 从 Vue 中导入函数
Vue 3 的组合式 API 通常使用具名导入:
typescript
import {
ref,
computed,
onMounted,
onBeforeUnmount,
nextTick
} from 'vue';
导入后才能在当前文件中调用:
typescript
const count = ref(0);
const doubleCount = computed(() => {
return count.value * 2;
});
onMounted(() => {
console.log('组件已经挂载');
});
没有导入就直接使用,会出现类似错误:
text
ref is not defined
onMounted is not defined
3.3 导入整个包
Three.js 经常使用下面的写法:
typescript
import * as THREE from 'three';
这里的:
typescript
* as THREE
表示把 three 包导出的内容集中放到 THREE 这个命名空间中。
使用时写成:
typescript
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera();
const renderer = new THREE.WebGLRenderer();
这样做的好处是代码来源非常清晰:
text
THREE.Scene
THREE.Mesh
THREE.Group
THREE.Vector3
一眼就能看出这些对象来自 Three.js。
Three.js 的扩展模块通常单独导入,例如:
typescript
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
然后使用:
typescript
const controls = new OrbitControls(
camera,
renderer.domElement
);
4、默认导出和具名导出
理解 import 时,最重要的知识之一就是区分:
text
默认导出
具名导出
4.1 默认导出 export default
一个模块最多只能有一个默认导出。
例如:
typescript
const router = createRouter({
history: createWebHistory(),
routes: []
});
export default router;
另一个文件导入时不需要花括号:
typescript
import router from './router';
默认导入的本地名称可以修改:
typescript
import appRouter from './router';
虽然原文件导出的变量叫 router,但当前文件可以把它命名为 appRouter。
在团队项目中,通常仍建议保持含义一致:
typescript
import router from './router';
这样更容易阅读。
4.2 具名导出 export
一个模块可以有多个具名导出。
例如创建一个工具文件:
typescript
// src/utils/format.ts
export const formatTemperature = (
value: number
): string => {
return `${value.toFixed(1)} ℃`;
};
export const formatPressure = (
value: number
): string => {
return `${value.toFixed(2)} MPa`;
};
导入时必须使用花括号:
typescript
import {
formatTemperature,
formatPressure
} from '@/utils/format';
使用时:
typescript
const temperatureText =
formatTemperature(1650.28);
const pressureText =
formatPressure(0.85);
4.3 给具名导入改名
可以通过 as 修改本地名称:
typescript
import {
formatTemperature as formatTemp
} from '@/utils/format';
之后使用:
typescript
formatTemp(1650);
这适合解决名称冲突,但不要无意义地频繁改名,否则代码会更难追踪。
4.4 同时使用默认导出和具名导出
一个文件可以同时提供默认导出和具名导出:
typescript
export const ROUTE_NAME = 'BofHeatTwin';
const twinConfig = {
fullScreen: true
};
export default twinConfig;
导入时可以写:
typescript
import twinConfig, {
ROUTE_NAME
} from '@/config/twin';
5、如何导入 .vue 文件
Vue 单文件组件使用 .vue 扩展名,通常包含:
vue
<template>
<!-- 页面结构 -->
</template>
<script setup lang="ts">
// 页面逻辑
</script>
<style scoped>
/* 页面样式 */
</style>
5.1 静态导入 Vue 组件
假设有一个组件:
text
src/components/TwinHeader.vue
在页面中可以这样导入:
vue
<script setup lang="ts">
import TwinHeader from '@/components/TwinHeader.vue';
</script>
<template>
<TwinHeader />
</template>
在 <script setup> 中,导入后的组件可以直接在模板里使用,不需要再写:
typescript
components: {
TwinHeader
}
这是 Vue 3 <script setup> 的便利之处。
5.2 相对路径导入 .vue 文件
假设目录是:
text
src/views/bof/heat/
├─ index.vue
├─ twin.vue
└─ components/
└─ StatusPanel.vue
在 twin.vue 中导入 StatusPanel.vue:
typescript
import StatusPanel from './components/StatusPanel.vue';
这里:
text
./
表示当前目录。
如果要从 components/StatusPanel.vue 返回上一级,再访问其他文件,可以使用:
text
../
例如:
typescript
import HeatCard from '../components/HeatCard.vue';
不过相对路径层级太深时会变得难以阅读:
typescript
import HeatCard from '../../../../components/HeatCard.vue';
这时更适合使用路径别名。
5.3 使用 @ 路径别名
Vue 工程通常把:
text
@
配置为:
text
src
因此:
typescript
import TwinHeader from '@/components/TwinHeader.vue';
等价于从:
text
src/components/TwinHeader.vue
导入。
路由中的:
typescript
component: () =>
import('@/views/bof/heat/twin.vue')
实际指向:
text
src/views/bof/heat/twin.vue
@ 不是 JavaScript 原生语法,而是构建工具提供的路径别名。
在 Vite 中,一般会在 vite.config.ts 中配置,例如:
typescript
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'node:path';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
});
TypeScript 项目还可能在 tsconfig.json 中配置:
json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"src/*"
]
}
}
}
如果 Vite 配置了别名,但 TypeScript 没有对应配置,编辑器可能提示找不到模块。
6、如何导入 .js 和 .ts 文件
Vue 页面中的大量业务逻辑不会全部写在 .vue 文件里,而会拆到普通 JavaScript 或 TypeScript 模块中。
例如:
text
src/
├─ api/
│ └─ heat.ts
├─ utils/
│ └─ format.ts
├─ config/
│ └─ twin.ts
└─ permission.ts
6.1 导入工具函数
typescript
import {
formatTemperature
} from '@/utils/format';
如果构建工具支持扩展名自动解析,下面两种写法通常都可以:
typescript
import {
formatTemperature
} from '@/utils/format';
import {
formatTemperature
} from '@/utils/format.ts';
在 Vue 和 Vite 项目中,一般更常见的是省略 .ts:
typescript
import {
formatTemperature
} from '@/utils/format';
但导入 .vue 文件时通常保留 .vue:
typescript
import TwinHeader from './TwinHeader.vue';
6.2 导入接口请求模块
例如:
typescript
// src/api/heat.ts
import request from '@/utils/request';
export const getHeatDetail = (
heatId: string
) => {
return request({
url: `/bof/heat/${heatId}`,
method: 'get'
});
};
在 twin.vue 中导入:
typescript
import {
getHeatDetail
} from '@/api/heat';
调用:
typescript
const loadTwinData = async () => {
const response = await getHeatDetail(
heatId.value
);
console.log(response);
};
这种拆分方式可以让职责更清晰:
text
twin.vue
→ 管理页面状态和交互
api/heat.ts
→ 管理后端接口地址和请求方式
utils/request.ts
→ 管理 Axios 实例、请求头和异常处理
6.3 只执行文件,不接收导出内容
项目入口中可能出现:
typescript
import './permission';
这叫作副作用导入。
它没有接收任何变量,作用是:
加载并执行
permission.ts文件中的顶层代码。
例如 permission.ts 中注册了全局路由守卫:
typescript
router.beforeEach((to, from, next) => {
// 权限检查
});
当入口文件执行:
typescript
import './permission';
这个守卫就会被注册。
常见的副作用导入还包括:
typescript
import './styles/index.scss';
import './permission';
import './plugins/highlight';
7、静态导入和动态导入
7.1 静态导入
静态导入写在模块顶层:
typescript
import App from './App.vue';
import router from './router';
它通常会在当前模块加载时被分析和处理。
适合:
- 应用启动必需模块;
- 通用组件;
- 核心工具;
- 全局样式;
- Vue、Pinia、Router 等基础依赖。
7.2 动态导入
动态导入使用:
typescript
import('模块路径')
它返回一个 Promise。
Vue Router 中常见的写法是:
typescript
component: () =>
import('@/views/bof/heat/twin.vue')
这叫作路由懒加载。
它表示:
text
应用启动时
→ 暂时不加载 twin.vue
用户访问对应路由时
→ 再异步加载 twin.vue
好处包括:
- 减少首页初始下载体积;
- 缩短首次加载时间;
- 大型页面按需加载;
- Three.js、图表和数字孪生页面不会阻塞普通后台首页。
对于体积较大的三维页面,懒加载尤其重要。
7.3 静态导入和动态导入的对比
| 对比项 | 静态导入 | 动态导入 |
|---|---|---|
| 语法 | import X from '...' |
import('...') |
| 执行时机 | 模块加载阶段 | 运行到该逻辑时 |
| 返回内容 | 直接获得模块成员 | 返回 Promise |
| 常见用途 | 核心依赖、普通组件 | 路由懒加载、按需功能 |
| 是否利于拆包 | 可以参与构建分析 | 更适合形成独立代码块 |
8、Vue 应用入口 main.ts
Vue 项目一般从 main.ts 或 main.js 启动。
典型代码如下:
typescript
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import './permission';
import './styles/index.scss';
const app = createApp(App);
app.use(router);
app.use(store);
app.mount('#app');
这段代码可以拆成五步。
8.1 导入 Vue 的 createApp
typescript
import {
createApp
} from 'vue';
createApp() 用来创建 Vue 应用实例。
8.2 导入根组件
typescript
import App from './App.vue';
App.vue 是整个组件树的根组件。
8.3 导入 Router 和 Store
typescript
import router from './router';
import store from './store';
这里的:
typescript
'./router'
通常会自动解析到:
text
src/router/index.ts
这是模块目录解析的常见写法。
8.4 安装插件
typescript
app.use(router);
app.use(store);
app.use() 用于把插件安装到 Vue 应用。
安装 Router 后,项目才能正常使用:
typescript
router.push();
useRouter();
useRoute();
<router-view />
安装 Store 后,组件才能使用全局状态管理。
8.5 挂载应用
typescript
app.mount('#app');
它把 Vue 应用挂载到 HTML 中的:
html
<div id="app"></div>
最终关系是:
text
index.html
└─ #app
└─ Vue 应用
└─ App.vue
9、常见 import 错误
24.1 默认导入和具名导入写反
原文件:
typescript
export default router;
正确:
typescript
import router from './router';
错误:
typescript
import {
router
} from './router';
原文件:
typescript
export const getHeatDetail = () => {};
正确:
typescript
import {
getHeatDetail
} from '@/api/heat';
错误:
typescript
import getHeatDetail from '@/api/heat';
24.2 路径层级写错
错误示例:
typescript
import StatusPanel from
'../components/StatusPanel.vue';
但真实文件可能在当前目录下:
typescript
import StatusPanel from
'./components/StatusPanel.vue';
遇到路径问题时,要从当前文件所在目录开始计算,而不是从项目根目录凭感觉计算。
24.3 @ 别名没有配置
如果使用:
typescript
import TwinHeader from
'@/components/TwinHeader.vue';
但 Vite 或 TypeScript 没有配置别名,就会出现:
text
Cannot find module
Failed to resolve import
24.4 文件名大小写不一致
Windows 文件系统有时对大小写不敏感,但 Linux 生产环境通常敏感。
例如真实文件是:
text
TwinHeader.vue
错误写成:
typescript
import TwinHeader from
'./twinHeader.vue';
本地可能暂时没有暴露问题,部署到 Linux 后可能构建失败。
24.5 循环依赖
例如:
text
router/index.ts
→ import permission.ts
permission.ts
→ import router/index.ts
如果设计不当,可能形成循环依赖。
当前更常见的结构是:
text
main.ts
├─ import router
└─ import permission
permission.ts
└─ import router
要避免让 router/index.ts 再反向导入 permission.ts。
24.6 动态导入路径无法被构建工具分析
推荐:
typescript
const pages = {
twin: () =>
import('@/views/bof/heat/twin.vue'),
heat: () =>
import('@/views/bof/heat/index.vue')
};
不推荐随意拼接完全动态路径:
typescript
const pageName = 'twin';
import(
`@/views/bof/heat/${pageName}.vue`
);
构建工具可能无法可靠确定需要打包哪些文件。
10、模块导入学习检查清单
完成本文后,应能够回答下面的问题:
import App from './App.vue'导入的是什么?import { ref } from 'vue'为什么有花括号?import './permission'为什么没有变量名?- 默认导出和具名导出有什么区别?
- 如何导入
.vue、.js和.ts文件? @/views/bof/heat/twin.vue中的@是什么?- 相对路径中的
./和../分别表示什么? - 静态导入和动态导入有什么区别?
- 为什么路由页面常使用
() => import(...)? - 为什么文件名大小写在 Linux 生产环境中特别重要?
- 什么是副作用导入?
- 什么情况下可能出现循环依赖?
11、总结
Vue 3 项目由许多彼此独立的模块组成,export 负责把模块中的能力提供出去,import 负责在其他文件中引入这些能力。
最核心的规则可以概括为:
text
默认导出
→ import 名称 from '路径'
具名导出
→ import { 名称 } from '路径'
副作用导入
→ import '路径'
npm 包
→ import ... from 'vue'
项目内部文件
→ import ... from './文件'
→ import ... from '@/文件'
路由懒加载
→ () => import('@/views/页面.vue')
在实际 Vue 项目中,应先看懂 main.ts 中导入了哪些基础模块,再沿着组件、工具文件、接口文件和路由文件逐步阅读。只要能够判断"内容从哪个模块导出、在什么位置导入、使用的是默认导出还是具名导出",就已经掌握了大型 Vue 项目代码关系的基础。