简介:这是一份面向前端初学者的Vue3入门示例资源,以经典HelloWorld形式演示Vue3相对Vue2的核心改进,帮助学习者快速认识Composition API、setup函数以及基于Proxy的响应式系统。压缩包内共7个文件,以2个js脚本和2个xml工程配置为主,辅以1个html入口文件、1个gitignore及1个iml工程标识,整体仅257KB,轻量精简可直接复用。目前已有784人下载学习,适合作为第一份Vue3实践素材。资源包含可直接在浏览器中运行测试的HTML页面与Vue3运行库,无需事先搭建复杂脚手架即可观察响应式效果;同时保留了项目级IDE配置,便于导入WebStorm等工具继续扩展。通过这个示例,读者可以掌握setup函数的基本写法、模板中的常用指令,以及组件化的组织方式,并为理解Teleport、Suspense等进阶特性打下基础。
1. Vue3 HelloWorld 工程模板:从脚手架到组件通信一次讲清
很多人第一次接触 Vue3 HelloWorld 项目时,最容易掉进两个坑:一个是还在用 Vue2 的 Options API 写法,另一个是跟着老教程配 Vue CLI,结果启动一次要等十几秒。实际上 Vue3 生态早就把重心移到了 Vite 和<script setup>语法糖上,一个 HelloWorld 跑通之后,组件通信、响应式数据、构建部署这些真实项目里天天用的东西,你基本都能无缝迁移过去。这篇文章要拆的这份 Vue3 HelloWorld 资源,就是一套基于 Vite 的完整最小工程,适合刚转 Vue3 的前端、要做课程设计的学生,以及想快速搭一个干净模板的开发者。下面直接从脚手架创建开始,把每一步的坑和参数都讲清楚。
2. 从零到跑通:Vite 脚手架与三个必调参数
2.1 为什么选 Vite 而不是 Vue CLI:启动速度和依赖预构建
在 Vue3 刚发布那阵,官方推荐的是 Vue CLI,底层走的是 Webpack。Webpack 在开发服务器启动时要把整个项目从头编译一遍,项目一超过几十个组件,冷启动就是五到十秒起步,改一行代码触发热更新也要等一两秒。Vite 的思路完全不同:开发模式下它不对整个项目做打包编译,而是利用浏览器原生的 ES Module 按需加载,启动时只做依赖预构建,所以一个 HelloWorld 工程基本是一秒内启动。
这里有个容易被忽略的概念叫依赖预构建。Vite 启动时会把 node_modules 里那些 CommonJS 或 UMD 格式的依赖,用 esbuild 预打包成 ESM 格式,并缓存在node_modules/.vite目录。这意味着你第一次跑npm install后启动项目,Vite 会做一次比较耗时的预构建,之后再启动就快很多。如果你改了某个依赖的版本,或者手动改了vite.config.js里的optimizeDeps.include,你会发现启动时会重新预构建,这是正常的,不是报错。
对于 Vue3 HelloWorld 这种规模的工程,Vite 和 Vue CLI 的最终产物差别不大,但开发体验差距巨大。我一般给新手推荐 Vite,就是因为热更新反馈快,能立刻看到自己改的代码在页面上变成什么样子,这对理解响应式原理有直观帮助。
2.2 创建 HelloWorld 工程:命令与关键参数逐条拆解
假设你已经装好了 Node.js 环境,版本最好在 16 以上,下一步就是创建工程。最常见的方式是直接使用 Vite 官方脚手架,命令如下:
# 用 Vite 官方脚手架创建 Vue3 工程 npm create vite@latest vue3-helloworld -- --template vue # 进入工程目录 cd vue3-helloworld # 安装依赖 npm install # 启动开发服务器 npm run dev逐条拆解一下命令里的关键点。第一行的npm create vite@latest是 npm 创建脚手架的统一语法,vue3-helloworld是你的项目目录名,--template vue指定使用 Vue 模板,注意这里有两个--,第一个--是 npm 的参数分隔符,第二个是让后面的参数原样传给 create-vite。如果漏掉-- --template vue,脚手架会进入交互式问答模式,问你选择原生 JS 还是 Vue、要不要 TS 等,虽然也不麻烦,但脚本化创建时不够干脆。
第四行的npm install会生成 package-lock.json 文件,这一步在网络不稳定时容易卡住,常见做法是用npm install --registry=https://registry.npmmirror.com切换镜像源,或者换用pnpm install,但混用包管理器容易把 node_modules 结构搞乱,这点后面避坑章会展开。
启动npm run dev之后,终端会输出一个本地访问地址,通常是http://localhost:5173/。这里有一个新手常见误区:Vite 默认端口是 5173,不是 Vue CLI 时代的 8080,也不是老教程里常写的 3000。如果你的 5173 端口被占用,Vite 会自动往后找 5174、5175,终端打印的地址会跟着变,以终端输出为准。
2.3 工程目录速读:HelloWorld 里每个文件在干什么
脚手架创建完成后,项目里会生成一批文件,很多人一看到十几个文件就懵了。实际上核心文件就那几个,我用下面的表格把职责列清楚:
| 文件路径 | 作用 | 在 HelloWorld 里你该关注什么 |
|---|---|---|
index.html | Vite 的入口 HTML | <script type="module" src="/src/main.js">是挂载点 |
src/main.js | 创建应用实例 | 引入 App.vue,调用createApp().mount('#app') |
src/App.vue | 根组件 | 所有视图的顶层容器 |
src/components/HelloWorld.vue | 示例子组件 | 展示 props、响应式数据、事件绑定 |
vite.config.js | Vite 配置文件 | 端口、代理、打包路径都在这里改 |
package.json | 工程元信息 | scripts 里的 dev/build/preview 是最常用三个命令 |
打开index.html你可能会奇怪,为什么 Vue 单页应用的入口居然不是src/main.js而是index.html?这正是 Vite 与传统 Webpack 工程的核心差异之一。Vite 在开发环境下直接把index.html当作页面入口,浏览器解析到<script type="module">标签时,才去请求对应模块,所以启动速度才快。
src/main.js是 Vue3 应用的诞生点,代码非常短:
// main.js 是 Vue3 应用的最小编排入口 import { createApp } from 'vue' import App from './App.vue' createApp(App).mount('#app')这段代码的逻辑是:从 vue 包中导入createApp函数,传入根组件 App,最后挂载到index.html里 id 为 app 的 DOM 节点上。对比 Vue2 的new Vue({ render: h => h(App) }).$mount('#app'),Vue3 用函数式创建应用的方式,好处是多个应用实例互不干扰,而且方便后续按需注册插件,比如app.use(router)。注意,main.js里没有默认全局引入 Vue,因为 Vue3 的 SFC 转译会自动注入必要的运行时导入,这也是为什么你在组件里写模板却不需要import Vue from 'vue'的原因。
3. 拆开 HelloWorld:setup 语法糖、ref/reactive 与模板渲染链路
3.1 Options API 与 setup 语法糖:同样的 HelloWorld 两种写法
Vue3 初期兼容 Vue2 的 Options API,也就是data、computed、methods、watch这些选项对象放在组件默认导出的对象里。但新项目里几乎看不到这种写法了,因为组合式 API 配合<script setup>语法糖,代码组织更紧凑。
用一个最经典的计数器对比两种写法:
<!-- 传统 Options API 写法:Vue2 时代延续下来的风格 --> <script> export default { data() { return { count: 0 } }, methods: { increment() { this.count++ } } } </script> <template> <button @click="increment">点击了 {{ count }} 次</button> </template><!-- 现在推荐的 <script setup> 写法:Vue3 HelloWorld 的标准姿势 --> <script setup> import { ref } from 'vue' // ref 把基本类型包装成响应式对象 const count = ref(0) const increment = () => { count.value++ } </script> <template> <button @click="increment">点击了 {{ count }} 次</button> </template>第二种写法里的逻辑区别值得细讲。<script setup>是单文件组件编译时的语法糖,它把顶层变量和函数直接暴露给模板,不需要return。这意味着你写在 setup 里的每一个ref、computed、普通函数,模板里都能直接用。ref(0)返回的是一个 Proxy 对象,访问它的值要用count.value,但在模板里 Vue 会自动解包,所以模板里直接写count而不是count.value。
很多新手在这块会翻车:在<script setup>里导入的组件,不需要写components: { HelloWorld }注册,直接<HelloWorld />就能用。因为编译阶段 Vite 会把导入的变量自动视为组件引用。顺手提一句,文件名大小写敏感,import HelloWorld from './components/HelloWorld.vue'里的路径如果写错大小写,本地 Windows 上可能能跑,但 Linux 服务器上直接报错。
3.2 计数器 Demo:ref.value 为什么模板里不用写点
继续用计数器深入理解渲染链路。Vue3 的响应式核心是 Proxy,ref的功能是把内部值包裹成带有 getter 和 setter 的对象。修改count.value时,dep 依赖收集器会通知所有依赖它的 effect 函数重新执行,模板编译出来的渲染函数自然也被重新执行,于是页面更新。
有一个细节特别容易让新手困惑:count.value++和++count.value看起来没啥区别,但如果把计数逻辑拆到普通函数里,比如写一个const logAndIncrement = () => { console.log(count.value); count.value++ },你会发现console.log打印的是自增前的值。这不是 bug,而是 JS 本身的执行顺序问题,跟 Vue 无关。排查这类问题时先回到原生 JS 语义,别急着怀疑框架。
对于对象和数组这类复杂结构,ref内部其实会自动调用reactive来包裹深层数据。所以ref({ name: 'Vue3' })之后,obj.name也是响应式的。但规范用法是对象用reactive、基本类型用ref,这样代码语义更清晰。
3.3 列表与条件渲染:HelloWorld 里最常用的流程控制
计数器只是起步,HelloWorld 工程里通常还包含列表渲染和条件渲染,这两个几乎每个页面都会用。下面这段是一个带状态切换的待办示例:
<script setup> import { ref, computed } from 'vue' const todos = ref([ { id: 1, text: '学习 Vite 脚手架', done: true }, { id: 2, text: '理解 ref 和 reactive', done: false }, { id: 3, text: '跑通第一个 HelloWorld', done: false } ]) // computed 会根据 done 状态自动衍生数据 const unfinished = computed(() => todos.value.filter(item => !item.done)) const toggleDone = (id) => { const target = todos.value.find(item => item.id === id) if (target) { target.done = !target.done } } </script> <template> <ul> <li v-for="item in unfinished" :key="item.id"> {{ item.text }} <button @click="toggleDone(item.id)">标为完成</button> </li> </ul> <p v-if="unfinished.length === 0">所有任务都完成了</p> <p v-else>还有 {{ unfinished.length }} 个任务未完成</p> </template>这里有三个重点参数。第一个是v-for的:key,必须绑定稳定且唯一的标识,用item.id而不是数组下标。如果拿下标做 key,当列表头部插入一项时,Vue 会认为每个位置的元素没变,只是内容变了,导致复用错误的 DOM,出现复选框状态串位之类的诡异问题。第二个是v-if与v-else必须紧挨着,中间不能插入其他元素,否则 Vue 无法正确匹配分支。第三个是computed的缓存特性:它只在todos.value变化时重新计算,而不是每次渲染都重新 filter 一遍,这也是它比普通函数调用高效的原因。
3.4 样式绑定与事件处理:模板里的两个顺手技巧
HelloWorld 工程里还经常演示动态样式和事件对象,这两个技巧在真实项目中几乎天天要用。
先看动态样式绑定:
<script setup> import { ref } from 'vue' const isActive = ref(true) const activeColor = ref('#42b983') const toggleActive = () => { isActive.value = !isActive.value } </script> <template> <div class="demo-box" :class="{ active: isActive }" :style="{ color: isActive ? activeColor : '#999' }" @click="toggleActive($event)" > 点击切换状态,当前颜色 {{ isActive ? activeColor : '灰色' }} </div> </template>:class绑定对象时,对象键决定是否添加这个 class,值决定条件,这里isActive为 true 时 div 会同时拥有demo-box和active两个类。:style绑定的对象键对应 CSS 属性名,注意驼峰命名,background-color要写成backgroundColor。@click事件处理方法里手动传入$event,是为了拿到原生事件对象,比如event.clientX做弹窗定位。如果是纯标记性点击,不传$event也行,Vue 默认会传入事件对象作为第一个参数。
事件处理还有一个容易被忽略的细节:内联语句里直接调用toggleActive()不传参时,事件对象不会自动传进去,收到的是 undefined。所以需要在模板里手动传$event,示例里就是这么处理的。
4. 组件通信:props、emit 与 defineExpose 的配合
4.1 props 单向数据流:父组件给子组件传值的标准写法
HelloWorld 工程一般会带一个子组件,用来演示父组件向子组件传值。Vue3 中定义 props 的推荐写法是直接用defineProps宏:
<!-- ChildComp.vue --> <script setup> // 用 defineProps 声明组件接收哪些属性 const props = defineProps({ title: { type: String, default: '默认标题' }, count: { type: Number, required: true } }) // props 是只读的,不能直接修改 console.log(props.title) </script> <template> <div class="child-box"> <h3>{{ title }}</h3> <p>收到的计数是:{{ count }}</p> </div> </template>defineProps是编译宏,不需要从 vue 里 import,直接在<script setup>里调用就行。传 props 的父组件写法:
<!-- App.vue --> <script setup> import { ref } from 'vue' import ChildComp from './components/ChildComp.vue' const appCount = ref(3) </script> <template> <ChildComp title="父组件传来的标题" :count="appCount" /> </template>这里有个单向数据流的概念要反复强调:子组件不能直接修改props.count,如果写了props.count = 5,Vue 会在控制台告警。正确做法是子组件把 props 的值拷贝到自己的本地 ref,或者通过 emit 事件通知父组件去修改。实际上 Vue3 的底层并没有真正冻结 props 对象,但直接修改会破坏单向数据流,导致数据来源混乱,这是设计约定不是技术限制。
4.2 emit 自定义事件:子组件把操作结果交回父组件
子组件要改变父组件的数据,标准姿势是触发自定义事件。还是用计数器场景:
<!-- CounterChild.vue --> <script setup> const props = defineProps({ start: { type: Number, default: 0 } }) // 声明组件能触发哪些自定义事件 const emit = defineEmits(['update:count']) const localCount = props.start const plusOne = () => { // 第一个参数是事件名,第二个是要传给父组件的值 emit('update:count', localCount + 1) } </script> <template> <button @click="plusOne">子组件按钮:当前 {{ localCount }}</button> </template>父组件监听方式:
<!-- App.vue --> <script setup> import { ref } from 'vue' import CounterChild from './components/CounterChild.vue' const total = ref(10) const handleUpdate = (newCount) => { total.value = newCount } </script> <template> <p>父组件维护的数值:{{ total }}</p> <CounterChild :start="total" @update:count="handleUpdate" /> </template>defineEmits的作用不只是声明,还有一个容易被忽略的好处:如果子组件发了一个未声明的事件名,比如emit('foo')而defineEmits里没有列出foo,在开发模式下 Vue 会给出警告。这在多人协作时能快速发现事件拼写错误。另外注意update:count这种带冒号的事件名是 Vue 提供的一种约定,配合v-model:count="total"可以实现类似v-model的双向绑定效果,本质上是:count="total"加@update:count="total = $event"的语法糖。
4.3 defineExpose 与 ref:拿到子组件内部方法的另一种路径
props 和 emit 覆盖了大多数场景,但有一种情况它们不好使:父组件想在某个时机主动调用子组件内部的方法,比如点一个按钮让子组件的弹窗打开。这时候可以用ref获取子组件实例,并通过defineExpose控制暴露哪些内容给父组件。
子组件侧代码:
<!-- ModalChild.vue --> <script setup> import { ref } from 'vue' const visible = ref(false) const open = () => { visible.value = true } const close = () => { visible.value = false } // vue3 中 setup 里的变量默认不暴露给父组件,需要显式声明 defineExpose({ open, close }) </script> <template> <div v-if="visible" class="modal-mask"> <div class="modal-content"> <p>子组件的弹窗内容</p> <button @click="close">关闭</button> </div> </div> </template>父组件侧获取实例:
<!-- App.vue --> <script setup> import { ref } from 'vue' import ModalChild from './components/ModalChild.vue' // 模板里 ref="modalRef" 的名字需要和这个变量一致 const modalRef = ref(null) const callOpen = () => { if (modalRef.value) { modalRef.value.open() } } </script> <template> <button @click="callOpen">打开子组件弹窗</button> <ModalChild ref="modalRef" /> </template>关键点在于ref="modalRef"和const modalRef = ref(null)的名字必须保持一致,否则拿不到组件实例。也不是每个组件实例的所有内部属性都能访问的,因为<script setup>默认是关闭的,父组件只能拿到defineExpose显式暴露的成员。如果你发现modalRef.value是 null,最常见的原因是子组件还没挂载完成,或者你在onMounted之前就去访问了。
5. 避坑指南:从 HelloWorld 到真实项目最常见的七个坑
5.1 端口被占用后刷新失效
现象:启动npm run dev后终端提示 5173 被占用,自动换到 5174,但浏览器访问 5174 一直白屏,控制台报Failed to fetch dynamically imported module。
原因:Vite 重启后换了端口,但内存里的模块映射还引用旧端口,或者浏览器缓存了旧的模块请求。
解决:先彻底停掉终端里跑着的 Vite 进程,然后清一遍浏览器缓存再重新npm run dev。更稳的做法是在vite.config.js里固定端口:
// vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 5173, strictPort: true } })strictPort设为 true 后,端口被占用时会直接报错而不是自动切换到下一个端口,避免端口漂移带来的各种缓存不一致问题。
5.2 安装了依赖但 import 就报错
现象:npm install正常完成,但npm run dev后页面报Cannot find module 'vue'或'@vitejs/plugin-vue' is not defined。
原因:大概率是 package.json 里依赖版本不对,或者 npm 用了旧缓存安装了一半的依赖。
解决:第一步rm -rf node_modules package-lock.json,注意 Windows 下用rmdir /s node_modules。第二步重新安装,并确认 package.json 里 Vue 版本是^3.x而不是^2.7,因为 Vue2 的包名也叫 vue,装上后代码里createApp不存在直接崩。我一般会用npm view vue version先确认最新的 3.x 主版本号再写进依赖。
5.3 热更新失效:改代码页面毫无反应
现象:改了模板或样式,浏览器不自动刷新,必须手动刷新才能看到变化。
原因:Vite 的文件监听机制在某些环境下失效,比如 IDE 自动保存时写入的是软链接路径、WSL 跨文件系统访问、或者项目目录里有符号链接。
解决:如果项目在 WSL 或 Docker 挂载目录里运行,把项目挪到本地文件系统再试。如果实在挪不了,改 Vite 配置里的监听选项:
server: { watch: { usePolling: true, interval: 100 } }usePolling会强制 Vite 用轮询方式检测文件变化,代价是 CPU 占用上升,只作为开发阶段的临时方案。
5.4 生产构建后打开是白屏
现象:npm run build成功,npm run preview本地预览正常,但把dist目录部署到服务器子目录后,访问页面白屏,控制台报资源 404。
原因:Vite 默认base是/,即产物里的资源引用路径都是从域名根目录开始。部署在https://example.com/myapp/这种二级目录下时,资源请求会跑到https://example.com/assets/...去。
解决:在vite.config.js里把base改为相对路径:
export default defineConfig({ base: './' })base: './'会让产物里的路径变成相对路径,这样部署在任何目录下都能正常加载。注意改了base之后,preview本地预览也要用相对路径环境,否则带入旧配置。
5.5 依赖预构建缓存不更新
现象:改了 npm 某依赖的版本,重新npm install后启动 Vite,还是加载旧版本的依赖,页面表现与依赖版本不符。
原因:Vite 的依赖预构建缓存存在node_modules/.vite,它基于 package-lock.json 内容和依赖版本生成 hash,但某些场景下 hash 计算不准,比如用npm install升级了 patch 版本后锁文件缺失。
解决:强制清除缓存再启动:
# 删除预构建缓存 rm -rf node_modules/.vite # 重启开发服务器 npm run dev某些特殊依赖如在运行时动态 import 的模块,还需要在optimizeDeps.include里显式声明,Vite 才会在预构建时把它们打包进去。
5.6 混用包管理器导致依赖版本不一致
现象:同事用pnpm install后项目能跑,你用npm install后报错,或者反过来。尤其在公司协作项目中,这种问题很致命。
原因:pnpm和npm的依赖解析策略不同,pnpm基于内容寻址的链接结构,npm是平铺的 node_modules,两者生成的 lock 文件互不兼容。混用后 node_modules 里会残留大量不属于当前包管理器策略的重复依赖。
解决:项目里统一只用一个包管理器。如果当前项目已经混用了,彻底清场重装:
# 清掉所有依赖和锁文件,windows 请用对应命令 rm -rf node_modules package-lock.json pnpm-lock.yaml yarn.lock # 选定一个包管理器重装,推荐 pnpm 或 npm 二选一 npm install从那以后我每次接手新项目,第一件事就是看仓库根目录有没有pnpm-lock.yaml或yarn.lock,确认锁文件到底属于哪个包管理器,再决定自己用哪个命令。
5.7 子组件不渲染也没有报错
现象:页面没有报错,模板也写了<HelloWorld />,但子组件内容不显示。
原因:最常见的是子组件导入路径写错了后缀,./components/HelloWorld.vue写成了./components/HelloWorld,Vite 默认允许省略.vue后缀,但省略后它按 ESM 去解析时可能匹配不到文件;另一种可能是子组件根元素只有一个注释节点,Vue 渲染结果为空。
解决:统一带.vue后缀导入,并确认子组件模板根节点是真实 DOM 元素。如果用了<Fragment>或多个根节点,确认没有把<template>的注释当渲染结果。检查技巧是临时在子组件里加一行静态文本,如果还是不显示,再往导入路径上排查。
6. 进阶技巧:从 HelloWorld 到多页面与生产构建的三步提升
HelloWorld 跑通之后,最自然的进阶路径是往里面加路由,让页面从单组件变成多视图。
# 装 vue-router,注意当前安装的是 vue-router 4.x npm install vue-router@4然后在src下建router/index.js:
// src/router/index.js import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' const routes = [ { path: '/', name: 'home', component: HomeView }, { path: '/about', name: 'about', component: () => import('../views/AboutView.vue') } ] const router = createRouter({ history: createWebHistory(), routes }) export default routercreateWebHistory()基于 HTML5 History API,URL 里没有#,好看但部署到服务器时需要配置前端路由回退到index.html,否则刷新页面会 404。不想配服务器回退就改createWebHashHistory(),url 变成/#/about,兼容性更好,代价是略丑。
然后在main.js里挂载路由:
// main.js import { createApp } from 'vue' import App from './App.vue' import router from './router/index.js' createApp(App).use(router).mount('#app')第二步是区分环境变量。HelloWorld 阶段往往写死接口地址,真实项目里要把开发环境和生产环境的 api 地址拆开。在项目根目录建.env.development和.env.production:
# .env.development 开发环境变量 VITE_API_BASE=/api # .env.production 生产环境变量 VITE_API_BASE=https://api.example.com组件里通过import.meta.env.VITE_API_BASE读取,注意变量必须以VITE_开头,Vite 才会把它暴露给客户端代码。改完环境变量要重启开发服务器才生效,这是环境变量加载时机决定的,不是 Vite 的 bug。
第三步是生产构建的验证流程。npm run build之后,除了看构建结果,我固定会做三个自查动作:第一,npm run preview本地起一个生产环境预览服务器,因为开发环境表现和构建产物不完全一致;第二,打开浏览器开发者工具的网络面板,确认静态资源路径是相对路径还是绝对路径,对应判断base配置是否有问题;第三,如果页面有路由,把preview服务的 URL 手动改成/about刷新一次,验证服务器是否配置了路由回退,如果 404 就说明部署环境还缺一条 try_files 规则。
关于环境变量有个常见误区:.env.production在本地npm run build时生效,但部署到服务器后又发现接口地址不对,这种情况往往是构建时没把生产环境变量打包进去,或者服务器上的部署目录和base配置不匹配。处理办法是回本地重新npm run build,用vite preview验证后再上传,别在服务器上直接改 dist 文件。
还有个小技巧值得提醒:构建产物里的assets文件夹如果文件名带了很长的 hash,比如index.8f3d2c.js,这是 Vite 基于内容生成的指纹,作用是强制浏览器加载新版本而不是命中旧缓存。如果你发现线上改了代码但用户还是看到旧页面,优先检查服务器是否设置了index.html的no-cache响应头,而不是怀疑构建有问题。
这套流程跑通之后,Vue3 的工程化底座就算立住了。从那以后我每次新建 Vue3 工程,都强制走一遍创建脚手架、确认端口、跑通组件通信、构建预览的完整流程,哪怕只是写个测试页也绝不跳过构建这一步。踩过太多次开发环境好好的、生产环境白屏的坑,这习惯帮我提前拦掉了至少一半的部署事故。希望这篇 Vue3 HelloWorld 的拆解能帮你在起步阶段少绕几个弯,把基础工程搭得干净一点,后面加路由、加状态管理、接后端接口的时候,你会感谢自己没在起步阶段引入一堆看不懂的配置。
本文还有配套的精品资源,点击获取