Vue3入门指南:从createApp到单文件组件,彻底搞懂项目结构
发布时间:2026/9/24 23:11:44 作者:尧图编辑部 阅读量:1,286

很多刚接触 Vue3 的人第一反应往往不是“它好用”而是“它到底是个啥”。尤其是你从一个 Vue2 老项目切过来打开新项目一看入口不再是new Vue({ el: #app })而是createApp(App).mount(#app)页面文件变成了.vue单文件组件目录结构里多了一堆看起来相似又不一样的文件夹。这套组合拳打下来很容易让人懵。这篇文章就围绕createApp、单文件组件、项目结构这三件事带你从零开始把一个 Vue3 项目跑通搞清楚每一行代码和每个目录背后的逻辑。无论你是刚学前端、准备面试还是从 Vue2 迁移过来的老手这篇文章都能给你一个清晰的参考。1. Vue3 到底是什么先把它从神坛上拉下来1.1 它不是一门新语言而是一次全方位的升级很多初学者以为 Vue3 是一套完全陌生的东西其实不是。Vue3 仍然是 Vue.js 这个渐进式 JavaScript 框架的第三个大版本理念没有变数据驱动视图、组件化开发、响应式系统。变的更多是底层的实现方式和上层的使用体验。最核心的变化是响应式系统。Vue2 用的是Object.defineProperty来劫持对象属性的读写这带来两个很让人头疼的问题数组的索引变化监听不了对象新增属性也不是响应式的。Vue3 换成了Proxy可以对整个对象进行代理拦截更灵活性能也更好。数组问题、动态添加属性问题统统从根上解决了。另一个大变化是组合式 API。Vue2 里我们习惯用data、computed、methods、watch这些选项去组织逻辑当一个组件功能复杂时同一块业务逻辑会被拆散到各个选项里代码可读性很差。Vue3 的组合式 API 让你可以把同一个功能的变量、计算属性、方法、监听器写到一起逻辑内聚度一下子就上来了。这不是让你必须抛弃选项式 APIVue3 完全兼容 Vue2 的写法但你写新项目时强烈推荐用组合式 API后续维护会舒服很多。1.2 学 Vue3 之前需要先具备哪些基础很多人问“我能不能直接学 Vue3”我的建议是你得先有基础的 HTML、CSS、JavaScript 功底尤其是 ES6 里那几样东西——解构赋值、箭头函数、模板字符串、import/export。这些语法你在 Vue3 的几乎所有代码里都会看到不熟悉的话连读代码都读不顺。还有一点Vue3 对 TypeScript 的支持是天然友好的官方生态也全面转向 TS。不过这不是说你必须会 TS 才能用 Vue3纯 JavaScript 也完全可以开发。如果你有精力建议直接学一点 TS 基础写大型项目时类型系统能帮你省下大量排查低级错误的时间。环境要求上Vue3 项目通常是搭配 Vite 构建工具运行的所以你需要装一个相对较新的 Node.js 版本。建议 Node 18 以上Vite 5 及以上版本对 Node 版本有硬性要求版本太老会直接报错。装好后用node -v和npm -v检查一下环境确认没问题再继续。2. createApp一切从这行代码开始2.1 为什么不再是 new Vue()而是 createApp()Vue2 里创建一个应用实例是这样的new Vue({ el: #app, data() { return { message: Hello Vue2 } } })这种写法有点像一个“总构造函数”所有配置都堆在一个对象里如果想要多个 Vue 实例就得反复new。问题是这样创建的实例之间共享的是同一个全局 Vue 构造函数插件注册、全局组件定义都是挂在 Vue 本身上的多个实例之间的隔离性很差。Vue3 里换成了createApp它和 Vue2 最大的区别在于每个应用实例都是独立的。你通过createApp(App)创建了一个新的应用实例插件、全局组件、指令、混入都是注册到这个实例上的不会和别的应用实例串台。这有点像一个工厂从生产一种产品变成了生产多个独立的产品线一条产线的配置不会影响另一条。你可以用下面的表格对比一下两者的差异对比项Vue2 的 new VueVue3 的 createApp创建方式全局构造函数 new Vue实例工厂函数 createApp挂载方式el 配置项或 $mountmount(‘#app’)全局配置Vue.use、Vue.component 挂在类上app.use、app.component 挂在实例上多实例隔离共享全局配置实例间相互独立响应式原理Object.definePropertyProxy这个转变不只是技术细节的变化它代表了一种设计思路的演进应用实例从“类”变成更纯粹的“实例”依赖关系更清晰也更好测试。2.2 createApp 返回的对象到底干了什么我们通常会在main.js里看到这样的代码import { createApp } from vue import App from ./App.vue const app createApp(App) app.use(router) app.use(pinia) app.component(MyButton, MyButton) app.mount(#app)createApp(App)会返回一个应用实例app。这个实例上有几个非常常用的方法mount(rootContainer)把应用渲染到指定的 DOM 节点上这是启动应用的最后一步。use(plugin)注册插件比如 Vue Router、Pinia、Element Plus都是通过use安装的。component(name, component)注册全局组件注册后任意组件都可以直接用不需要再 import。directive(name, directive)注册全局自定义指令。config配置应用级选项比如app.config.globalProperties可以挂载全局属性。值得注意的一个细节是mount的调用必须在use之后因为插件往往会在应用实例上做一些配置比如注入路由实例、全局状态。如果你先挂了载再注册插件应用都渲染完了插件里的内容自然就不会生效。2.3 一个最简 createApp 示例的运行逻辑为了把流程说清楚我写一个最简版的例子!-- index.html -- div idapp/div// main.js import { createApp } from vue const app createApp({ template: h1{{ message }}/h1, data() { return { message: Hello Vue3 } } }) app.mount(#app)实际上我们在真实项目里很少这样写因为真实项目用的是.vue单文件组件createApp(App)里的App是从App.vue导入的组件对象。但理解了上面这个最简写法你就知道 createApp 接收的本质是一个根组件这个组件内部可以继续嵌套其他组件最终形成一个组件树挂载到#app这个容器上。整个 Vue3 应用就是从这一行createApp开始的。3. 单文件组件把页面拆成一个个盒子3.1 单文件组件是什么为什么它让 Vue 开发如此舒服单文件组件简称 SFC就是把一个组件相关的模板、逻辑、样式写在一个.vue文件里。典型的结构是这样的template div classcard h2{{ title }}/h2 p{{ content }}/p /div /template script setup import { ref } from vue const title ref(标题) const content ref(内容) /script style scoped .card { border: 1px solid #ddd; padding: 16px; } /style这种写法最大的好处是“高内聚”。一个组件本身就是一块完整的 UI 和业务逻辑它的模板、数据、方法、样式都放在同一个文件里改的时候不用来回跳文件。你可以把组件理解成一个积木块页面就是由这些积木块拼接而成。如果某个积木块要复用直接把这个.vue文件复制或者封装成公共组件就行。但要注意单文件组件不能直接在浏览器里运行浏览器只认识 HTML、CSS、JavaScript。所以.vue文件需要经过构建工具处理打包成浏览器能识别的文件。这就是为什么 Vue3 项目要使用 Vite 或 Webpack 这类构建工具的原因之一。3.2 template、script、style 三部分的分工和配合一个.vue文件里有三个顶层块分别负责三件事template 部分写的是 HTML 结构直接决定了组件长什么样。Vue 的模板语法非常丰富{{ }}插值、v-if、v-for、v-model、v-bind、v-on这些指令都是在这里使用。模板不是简单地把数据塞进 HTML 里Vue 会在编译阶段做优化比如静态节点提升、动态节点追踪这样渲染性能更好。script 部分写的是组件的逻辑包括数据、计算属性、方法、生命周期钩子。在 Vue3 里比较推荐的是script setup语法。这种语法是组合式 API 的语法糖它能让你在script里直接使用顶层变量不需要写export default和setup()的返回。简单来说你写的变量和方法模板里可以直接用代码更精简。style 部分写样式。如果加上scoped特性Vue 会在编译时给当前组件的所有元素添加一个唯一的数据属性比如>npm create vitelatest my-vue3-demo -- --template vue项目创建好以后默认目录结构大致如下my-vue3-demo ├── index.html ├── package.json ├── vite.config.js ├── public │ └── favicon.ico └── src ├── main.js ├── App.vue ├── components │ └── HelloWorld.vue └── assets └── logo.png先把这些文件和目录的功能逐一说清楚。index.html是整个项目的 HTML 入口Vite 会把这个文件当作运行时的首页模板。你会看到里面有div idapp/div这就是 Vue 应用的挂载点。Vite 开发模式下实际上所有模块都是由这个页面入口动态加载的。package.json是项目的配置文件记录了项目依赖的第三方包、脚本命令和项目元信息。你通过npm install安装的依赖都会写进package.json和package-lock.json里团队成员拿到项目后只需要执行npm install就能还原环境。vite.config.js是 Vite 的配置文件。这里可以配置开发服务器端口、代理、路径别名、构建选项等。几乎所有 Vue3 Vite 项目的工程化配置都在这里搞定。public目录放静态资源比如 favicon、一些不需要构建的公共图片这些文件会原样拷贝到打包产物里。src目录是源代码的根目录。main.js是源码入口App.vue是根组件components目录放公共组件assets放需要参与构建的静态资源比如图片、全局样式文件。4.2 从入口文件到页面的完整链路整个项目启动后文件之间的调用关系是这样的用户访问index.html浏览器加载 Vite 注入的 JavaScript 模块也就是src/main.js。main.js里createApp(App)创建 Vue 应用实例并将App.vue作为根组件传入。应用实例挂载到index.html中的#app容器上Vue 开始渲染组件树。App.vue内部又引入了其他子组件比如HelloWorld子组件里还可以继续嵌套子组件一层层向下延伸最终形成完整的页面。我建议你在学习阶段把这条链路写在一张纸上跟着调试工具一步一步走一遍。当你在浏览器里看到页面渲染出来时你要能说出“这个页面是从哪个组件渲染来的、经过了几层嵌套、挂载到了哪个容器上”。想清楚这条链路Vue3 项目对你来说就不再是黑盒了。4.3 组件目录和业务模块怎么规划更合理项目规模变大后components目录不可能只放几个公共组件。这里分享一下我目前比较常用的组织方式供你参考src ├── api # 接口请求封装 ├── assets # 静态资源 ├── components # 公共组件 ├── composables # 组合式函数 ├── router # 路由配置 ├── stores # 状态管理Pinia ├── styles # 全局样式 ├── utils # 工具函数 ├── views # 页面级组件 ├── App.vue └── main.jsviews放的是页面级组件一般和路由一一对应比如首页就是views/Home.vue用户中心就是views/User.vue。components放的是跨页面复用的公共组件比如一个自定义弹窗、一个分页器。composables放的是组合式函数也就是把一组可以复用的逻辑提取出来的文件。比如usePagination.js专门封装分页逻辑。api统一管理接口请求避免在业务组件里到处写axios.get。router和stores分别管理路由和全局状态。小项目可以把目录精简一些但大项目尤其要重视职责拆分。否则所有组件堆在一起、所有接口散落在页面里后期想改一处逻辑你可能要在文件海里翻半天。5. 从第一个项目开始手写一个 TodoList 应用5.1 项目初始化与开发环境准备理论讲再多都不如动手跑一遍。我们用 Vite 从零开始创建一个 Vue3 项目然后动手写一个简单的 TodoList。先确认 Node 版本建议 18 以上node -v然后创建项目npm create vitelatest todo-demo -- --template vue cd todo-demo npm install npm run dev打开终端输出的本地地址比如http://localhost:5173你应该能看到 Vite 默认的欢迎页面。这说明项目已经跑起来了。这里补充一个可能踩到的坑执行npm create vite时如果 npm 版本过旧可能会卡在交互式提示里出不来或者报一些奇怪的错误。建议把 npm 升级到最新版或者用npm create vitelatest指定版本。5.2 在 App.vue 里写一个 TodoList 组件为了减少目录层级干扰我们先把 TodoList 逻辑直接写在App.vue里跑通以后再拆出去。打开src/App.vue删掉原有模板内容改成下面的代码template div classtodo-app h1我的待办事项/h1 input v-modelnewTodo classtodo-input placeholder输入事项按回车添加 keyup.enteraddTodo / ul classtodo-list li v-fortodo in filteredTodos :keytodo.id classtodo-item input typecheckbox v-modeltodo.done / span :class{ done: todo.done }{{ todo.text }}/span button clickremoveTodo(todo.id)删除/button /li /ul div classtodo-filters button clickfilter all全部/button button clickfilter active未完成/button button clickfilter done已完成/button /div /div /template script setup import { ref, computed } from vue const newTodo ref() const todos ref([ { id: 1, text: 学习 createApp, done: false }, { id: 2, text: 理解单文件组件, done: true } ]) const filter ref(all) const filteredTodos computed(() { if (filter.value active) { return todos.value.filter(todo !todo.done) } if (filter.value done) { return todos.value.filter(todo todo.done) } return todos.value }) function addTodo() { const text newTodo.value.trim() if (!text) return todos.value.push({ id: Date.now(), text, done: false }) newTodo.value } function removeTodo(id) { todos.value todos.value.filter(todo todo.id ! id) } /script style scoped .todo-app { max-width: 480px; margin: 40px auto; padding: 24px; border: 1px solid #e5e7eb; border-radius: 12px; } .todo-input { width: 100%; padding: 8px 12px; margin-bottom: 16px; box-sizing: border-box; } .todo-list { list-style: none; padding: 0; } .todo-item { display: flex; align-items: center; gap: 8px; padding: 8px 0; border-bottom: 1px solid #f0f0f0; } .done { text-decoration: line-through; color: #999; } .todo-filters { display: flex; gap: 8px; margin-top: 16px; } /style这段代码里用到了ref、computed、v-model、v-for、keyup.enter几个高频知识点正好对应 Vue3 核心开发技能ref用来定义响应式数据页面上的数据变更会自动同步到视图。computed根据已有数据计算新的值并且会被缓存依赖数据变化时才会重新计算。v-model做表单双向绑定输入框内容变化会直接更新newTodo。v-for遍历数组渲染列表注意一定要加上:keyVue 需要靠它做列表的精确更新。keyup.enter是事件绑定按回车触发addTodo。页面保存后浏览器热更新会立即生效。你可以试着输入一条待办事项、回车添加、勾选、筛选、删除整个过程就是标准的前端交互闭环。5.3 把代码拆成父子组件理解组件化开发刚才代码全写在App.vue里功能虽然能用但不够“组件化”。真实项目里我们会把可复用的部分拆成独立组件。现在拆一个TodoItem.vue出来!-- src/components/TodoItem.vue -- template li classtodo-item input typecheckbox :checkedtodo.done changetoggle / span :class{ done: todo.done }{{ todo.text }}/span button clickonRemove删除/button /li /template script setup const props defineProps({ todo: { type: Object, required: true } }) const emit defineEmits([toggle, remove]) function toggle() { emit(toggle, props.todo.id) } function onRemove() { emit(remove, props.todo.id) } /script然后在App.vue里引入并使用script setup import { ref, computed } from vue import TodoItem from ./components/TodoItem.vue const todos ref([ { id: 1, text: 学习 createApp, done: false }, { id: 2, text: 理解单文件组件, done: true } ]) function toggleTodo(id) { const item todos.value.find(todo todo.id id) if (item) item.done !item.done } function removeTodo(id) { todos.value todos.value.filter(todo todo.id ! id) } /script template ul classtodo-list TodoItem v-fortodo in todos :keytodo.id :todotodo toggletoggleTodo removeremoveTodo / /ul /template这个拆分过程很有代表性它演示了组件通信的两个方向父传子用propsApp.vue把todo数据传给TodoItem。子传父用emitTodoItem里触发toggle、remove事件父组件监听并处理数据变更。记住一条原则数据流永远是单向的子组件不能直接修改父组件传下来的 props只能通过事件通知父组件去改。这个设计能让你在项目变复杂后依然能追踪数据的来源和变更路径。6. 常见问题与排查技巧实录6.1 项目跑不起来先从这三个方向排查很多初学者第一次执行npm run dev就报错常见原因无非三种一是依赖没装全。克隆别人的项目后直接运行没执行npm install会提示找不到模块。解决方案很明确先装依赖再启动。二是Node 版本过低。Vite 高版本对 Node 版本有要求建议升级 Node 到 18 或更高版本然后用node -v确认。三是端口被占用。默认端口是 5173如果被占用Vite 会自动往后找端口比如 5174。如果还是不行可以手动指定端口在vite.config.js里加server: { port: 3000 }或者直接在命令行用npm run dev -- --port 3000。6.2 页面白屏控制台也没报错页面空白但控制台没有明显报错这种问题最常见的原因就是挂载点没找到。比如index.html里的div写成了idapp但main.js里挂载的是app.mount(#app)一旦 ID 对不上Vue 找不到容器页面自然空白。还有可能是组件引入路径拼错比如import App from ./App.vue写成了../App.vue运行时模块加载失败页面也会白屏。排查这类问题建议先在main.js里加一行console.log(main.js loaded)确认入口是否正常再逐层往上排查。6.3 部署上线后报 uncaught syntaxerror: unexpected token这是一个非常经典的问题本地npm run dev一切正常但部署到 Nginx 后浏览器报Uncaught SyntaxError: Unexpected token。这往往不是语法写错了而是构建产物的引用路径出了问题。默认情况下Vite 构建出来的资源路径是相对路径还是绝对路径取决于base配置。如果项目部署在域名子路径下比如https://example.com/my-app/而你构建时没有设置base: /my-app/那么引用 JS/CSS 的地址就会指向错误的位置浏览器尝试去解析一个 HTML 文件当 JS 用自然报语法错误。解决方法是修改vite.config.jsexport default { base: /my-app/ // 正式环境部署的子路径 }或者部署在域名根路径下设置base: ./让构建后的资源使用相对路径。改完配置后重新npm run build再部署一次问题基本就解决了。6.4 Vue3 登录不跳转、前端调后端接口不通登录成功后不跳转或者接口请求失败这类问题在前后端分离项目里非常常见原因主要集中在跨域和代理配置上。本地开发时前端跑在5173后端接口跑在8080端口不同就会产生跨域。解决方式是在vite.config.js里配置代理export default { server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }配置完成后前端请求/api/login就会转发到http://localhost:8080/api/login跨域问题在开发环境就规避掉了。部署到生产环境后跨域问题则通常由 Nginx 反向代理或者后端开启 CORS 来解决而不是继续依赖 Vite 的代理。至于登录后不跳转很多时候是路由跳转时机不对或者跳转代码写错位置。建议先确认登录接口是否真的返回成功、token 是否存到了本地再确认跳转使用的是router.push(/home)而不是window.location.href。前者是应用内跳转不会刷新页面也更符合 SPA 的交互方式。6.5 几个高频细节问题速查根据社区里经常被问到的问题我再整理几个高频细节供你参考v-model 在组件上的用法Vue3 中v-model在组件上等价于modelValue属性和update:modelValue事件。写自定义表单组件时要自己接收modelValue并在输入时触发更新事件。sortable 库拖拽不生效很多人在 Vue3 里用 vuedraggable 或 sortablejs 时发现拖拽没反应先检查是否引入了对应样式再确认列表传递的是响应式数组还要注意动态渲染完成后才能初始化拖拽。使用 JSX 和打印模板组件Vue3 里使用 JSX 需要在vite.config.js里安装并配置官方插件vitejs/plugin-vue-jsx。打印场景常用vue-print-nb在 Vue3 里更推荐看看vue3-print-nb两者版本差异性比较大。若依 Vue3 版本报 TS 类型错误这类问题通常和版本依赖不一致、某个插件声明文件缺失有关。先检查按需自动导入插件是否需要额外安装unplugin-auto-import的类型声明引用再确认 tsconfig 里是否包含对应的类型文件。问题千千万但排查的思路是通用的先复现问题、看控制台报错、定位出错文件、检查数据流、再修改验证。只要这个闭环走熟练了遇到新问题也不会慌。我个人在实际操作中的体会是Vue3 的学习路径没必要搞得太复杂。先把这个最小的“createApp 单文件组件 项目结构”闭环跑通然后把经典案例亲手写一遍比如 TodoList、表单校验、列表筛选再逐步引入路由、Pinia、UI 组件库。代码量不用大但每个知识点都要自己动手敲一遍踩过几个坑之后你对 Vue3 的理解就会有一个质的飞跃。希望这篇文章能帮你迈出第一步接下来的路多写几行代码自然就顺了。