L01 · 项目脚手架 + 开发环境
🎯 本节目标:用 Vite 创建 Vue 3 + TypeScript 项目,理解每个文件的作用
📦 本节产出:可运行的 Hello World 项目 + 配置好的开发环境 + 第一次 Git 提交
🔗 前置钩子:无(起点)
🔗 后续钩子:L02 将在此项目基础上创建第一个 TodoItem 组件TIP
本节较长(12 个章节),推荐学习路径:
- 必学: §2 创建项目、§3 项目结构解析、§4 SFC 三段式、§5 script setup、§7 动手改造
- 建议了解: §1 Vite 原理、§8 HMR
- 可跳过(后续需要时回查): §6 vite.config、§9 TypeScript 配置、§10 public vs assets、§11 npm scripts
1. 为什么选 Vite 而不是 Webpack
在写第一行代码之前,先理解我们为什么选 Vite 作为构建工具。
1.1 Webpack 的痛点
Webpack 的常见开发配置会先构建入口及其依赖图,再向浏览器提供构建结果;缓存和具体配置也会影响启动时间:
项目规模、插件和缓存都会影响冷启动与热更新耗时,不能仅凭模块数量给出固定时间。
1.2 Vite 的解法:原生 ESM
本教程采用 Vite 6。它在开发时利用浏览器原生 ES Modules,按需转换源码;第三方依赖仍会预构建,生产环境仍会打包:
核心区别:
| 对比项 | Webpack | Vite |
|---|---|---|
| 启动方式 | 先打包,再提供服务 | 先启动服务,按需编译 |
| 冷启动 | 受依赖图、缓存和插件影响 | 减少启动时需要转换的源码 |
| 热更新 (HMR) | 重建受影响模块并发送更新 | 沿依赖链更新到最近的 HMR 边界 |
| 开发工具链 | Webpack + loaders / plugins | Vite 开发服务器 + esbuild 依赖预构建 |
| 生产打包器 | Webpack | Rollup(Vite 6) |
1.3 Vite 为什么能这么快
两个关键技术:
- 预构建依赖(Pre-bundling):用 esbuild 把
node_modules里的库(如vue、lodash)转换为适合浏览器加载的 ESM 并合并部分模块;结果会缓存,依赖或相关配置变化时重新生成 - 按需编译源码:你的
.vue、.ts文件在浏览器请求时才编译,未被请求的源码通常不需要转换;依赖扫描和插件也可能提前读取文件
2. 创建项目
2.1 初始化 Vue 3 项目
先安装 Node.js 22.12 或更新的 22.x 版本,并用 node -v、npm -v 确认命令可用。编辑器可使用 VS Code,安装 Vue - Official 扩展。
为与课程的 Vue 3.5、Vite 6、TypeScript 5 示例一致,固定脚手架版本:
npm create vue@3.14.2
create-vue是 Vue 官方的脚手架工具,底层基于 Vite。v3.14.2 的模板 使用 Vue 3.5 和 Vite 6。npm create vue@latest会跟随当前脚手架升级,选项、Node.js 要求和构建工具版本可能与本课不同。提交生成的package-lock.json,以后用npm ci复现已锁定的依赖。
交互式选项推荐:
✔ Project name: … vue-todo
✔ Add TypeScript? … Yes
✔ Add JSX Support? … No
✔ Add Vue Router? … No ← Phase 1 不需要路由
✔ Add Pinia? … No ← Phase 1 不需要状态管理
✔ Add Vitest? … No ← Phase 2 再加
✔ Add an End-to-End Testing Solution? … No
✔ Add ESLint for code quality? … Yes
✔ Add Prettier for code formatting? … Yes为什么现在不加 Router 和 Pinia? Phase 1 是纯基础阶段。让你先理解组件、响应式和模板语法,不被额外概念干扰。Phase 2 用到时再手动安装,这样你会知道每个依赖解决什么问题。
安装依赖并启动;这里把模板默认的 Vue ^3.5.13 收窄到 ~3.5.13,让课程项目保持在 3.5.x:
cd vue-todo
npm install vue@~3.5.13
npm run dev按终端打印的地址打开浏览器(默认是 http://localhost:5173,端口占用时可能变化),看到 Vue 欢迎页就成功了。此版本会在项目中集成 vite-plugin-vue-devtools 调试插件,不会替你安装浏览器扩展。
2.2 初始化 Git
git init
git add .
git commit -m "L01: 项目初始化 - Vite + Vue 3 + TypeScript"每节课结束都提交一次,这样你可以随时回溯到任何阶段。
3. 项目结构解析
执行 tree -I node_modules 查看目录结构:
vue-todo/
├── public/ # 静态资源(不经过 Vite 处理)
│ └── favicon.ico
├── src/ # 源码目录(核心)
│ ├── assets/ # 需要 Vite 处理的资源(CSS、图片)
│ │ ├── base.css
│ │ └── main.css
│ ├── components/ # 组件目录
│ │ └── HelloWorld.vue
│ ├── App.vue # 根组件
│ └── main.ts # 应用入口
├── index.html # HTML 入口(注意:在根目录,不在 public/)
├── env.d.ts # TypeScript 环境声明
├── tsconfig.json # 引用下面两个 TypeScript 项目
├── tsconfig.app.json # src 中的应用代码配置
├── tsconfig.node.json # Vite 等 Node.js 工具配置
├── vite.config.ts # Vite 配置
├── package.json # 依赖和脚本
└── eslint.config.ts # ESLint flat config3.1 关键文件逐行解读
index.html — 浏览器入口
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<!-- ⬆ Vue 将在这个 div 内渲染整个应用 -->
<script type="module" src="/src/main.ts"></script>
<!-- ⬆ type="module" 告诉浏览器用 ES Module 方式加载 -->
<!-- ⬆ Vite 拦截这个请求,即时编译 main.ts -->
</body>
</html>注意:
index.html在项目根目录,不在public/里。这是 Vite 和 Webpack 的一个不同点。Vite 把index.html当作"入口"(等同于 Webpack 的entry),通过 HTML 里的<script>标签找到 JS 入口。
src/main.ts — Vue 应用入口
import { createApp } from 'vue' // 从 Vue 导入创建应用的工厂函数
import App from './App.vue' // 导入根组件
import './assets/main.css' // 导入全局样式
createApp(App) // 创建 Vue 应用实例,App 是根组件
.mount('#app') // 挂载到 index.html 的 <div id="app">createApp 做了什么?
src/App.vue — 根组件(SFC 单文件组件)
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<header>
<div class="wrapper">
<HelloWorld msg="You did it!" />
</div>
</header>
</template>
<style scoped>
/* scoped: 样式只作用于当前组件 */
header {
line-height: 1.5;
}
</style>4. SFC 三段式结构
Vue 的单文件组件(Single File Component)由三部分组成:
4.1 为什么把 HTML/JS/CSS 放在同一个文件?
SFC 按组件组织相关代码:
"关注点分离" ≠ "文件类型分离"
传统方式按文件类型分离(.html / .js / .css),但一个按钮的逻辑、模板和样式分散在三个文件里,改一个按钮要打开三个文件。
SFC 按功能单元分离——一个组件的所有相关代码放在一起:
❌ 按文件类型分离(改一个按钮 → 打开 3 个文件)
src/
├── templates/
│ └── button.html
├── scripts/
│ └── button.js
└── styles/
└── button.css
✅ 按功能单元分离(改一个按钮 → 打开 1 个文件)
src/
└── components/
└── Button.vue ← template + script + style5. <script setup> 语法糖
5.1 普通写法 vs <script setup>
普通 setup() 写法(Vue 3 仍支持):
<script lang="ts">
import { defineComponent, ref } from 'vue'
import HelloWorld from './components/HelloWorld.vue'
export default defineComponent({
components: { // 手动注册组件
HelloWorld
},
setup() { // setup 函数
const count = ref(0)
function increment() {
count.value++
}
return { // 必须手动 return 暴露给模板
count,
increment
}
}
})
</script><script setup> 语法糖(推荐):
<script setup lang="ts">
import { ref } from 'vue'
import HelloWorld from './components/HelloWorld.vue'
// 顶层变量自动暴露给模板,不需要 return
const count = ref(0)
function increment() {
count.value++
}
// 导入的组件自动注册,不需要 components 选项
</script>5.2 <script setup> 帮你省了什么
| 特性 | 普通 <script> | <script setup> |
|---|---|---|
| 组件注册 | 手动在 components 中注册 | import 就自动注册 |
| 暴露给模板 | 必须在 setup() 中 return | 顶层变量自动暴露 |
| TypeScript 推断 | 通常用 defineComponent() 辅助推断 | 编译器支持模板与脚本间的类型推断 |
| 样板代码 | 需要组件注册和返回绑定 | 省去这些样板代码 |
| Props 声明 | props 选项 | defineProps() 宏 |
| Emits 声明 | emits 选项 | defineEmits() 宏 |
本教程全程使用
<script setup>,这是 Vue 3 官方推荐的写法。
6. 了解 vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue(), // Vue SFC 支持:编译 .vue 文件
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
// ⬆ 路径别名:@/components/xxx 等价于 src/components/xxx
}
}
})@vitejs/plugin-vue 做了什么:
它在 Vite 构建管线中注册了一个"转换器",遇到 .vue 文件时,把三段式 SFC 拆分编译为浏览器可执行的 JS + CSS。
7. 动手:改造为 Todo 应用骨架
现在让我们把默认的 Hello World 改造为 Todo 应用的骨架。
7.1 清理默认文件
删除不需要的默认内容:
# 删除默认组件和资源
rm src/components/HelloWorld.vue
rm src/components/TheWelcome.vue
rm src/components/WelcomeItem.vue
rm -rf src/components/icons/
rm src/assets/logo.svg7.2 替换 src/App.vue
<script setup lang="ts">
// 目前为空,L02 会在这里导入第一个组件
</script>
<template>
<div class="app">
<header class="app-header">
<h1>📝 Vue Todo</h1>
<p class="subtitle">Phase 1 — 用 Vue 3 从零做一个 Todo App</p>
</header>
<main class="app-main">
<p class="placeholder">🚧 Todo 列表将在 L02-L08 中逐步实现</p>
</main>
<footer class="app-footer">
<p>Built with Vue 3 + Vite + TypeScript</p>
</footer>
</div>
</template>
<style scoped>
.app {
max-width: 640px;
margin: 0 auto;
padding: 2rem;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.app-header {
text-align: center;
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2rem;
color: #42b883;
margin-bottom: 0.25rem;
}
.subtitle {
color: #888;
font-size: 0.9rem;
}
.app-main {
flex: 1;
}
.placeholder {
text-align: center;
padding: 3rem;
background: #f6f8fa;
border-radius: 12px;
color: #666;
border: 2px dashed #e0e0e0;
}
.app-footer {
text-align: center;
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid #eee;
color: #aaa;
font-size: 0.8rem;
}
</style>7.3 替换 src/assets/main.css
/* 全局基础样式 */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #2c3e50;
background-color: #ffffff;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}你可以删除
src/assets/base.css,因为我们用自己的全局样式。
7.4 确认效果
npm run dev浏览器应该显示一个简洁的页面:标题 "📝 Vue Todo" + 一个占位框。
8. 理解热更新(HMR)
现在试一下:不关闭浏览器,修改 App.vue 里的标题文字。
<h1>📝 Vue Todo</h1>
<!-- 改为 -->
<h1>✅ My Todo App</h1>保存后,模板修改通常能直接更新页面并保留组件状态。修改 <script> 可能重建组件,某些变化会触发整页刷新,不能保证所有状态都保留。
这就是 HMR(Hot Module Replacement):
与整页刷新的区别:
- 整页刷新:重新加载 HTML → 重新执行所有 JS → 所有状态归零
- HMR:更新受影响的模块;是否保留状态取决于修改类型及模块的热更新处理
9. TypeScript 配置简析
脚手架的 tsconfig.json 主要通过 references 引用 tsconfig.app.json 和 tsconfig.node.json。应用代码的配置写在 tsconfig.app.json,并继承 @vue/tsconfig/tsconfig.dom.json。下面只列出相关选项作说明,不要用它覆盖整个生成文件:
{
"compilerOptions": {
"target": "ES2020", // 编译目标:现代浏览器
"module": "ESNext", // 模块系统:ES Modules
"moduleResolution": "bundler", // TypeScript 按打包器规则解析模块
"strict": true, // 严格模式:推荐开启
"jsx": "preserve", // 如使用 JSX,保留给相应 JSX 插件处理
"paths": { // 路径别名:与 vite.config.ts 保持一致
"@/*": ["./src/*"]
}
}
}
strict: true会开启 TypeScript 的严格类型检查选项组。 Vite 本身只转译 TypeScript,不做类型检查;用npm run type-check或本项目的npm run build执行vue-tsc。target也不是浏览器兼容性的完整保证,生产输出还受 Vite 的build.target配置影响。
10. public/ vs src/assets/ 的区别
public/ | src/assets/ | |
|---|---|---|
| 处理方式 | 原样复制到构建输出 | 导入后进入资源构建图;小资源可能内联 |
| 引用方式 | 绝对路径 /logo.png | import logo from '@/assets/logo.png' |
| 文件名 | 构建后名字不变 | 输出为文件时通常带哈希;也可能内联为 data URL |
| 适合存放 | favicon.ico、robots.txt | 图片、字体、CSS |
| 缓存控制 | 服务器配置缓存策略 | 哈希便于长期缓存,缓存响应头仍由服务器配置 |
通常把需要从代码导入的资源放在 src/assets/,需要保留固定文件名、无需导入的资源放在 public/。Vite 不会默认替你压缩图片;部署到子路径时还需处理资源 URL 的基础路径。
11. npm scripts 解读
打开 package.json 的 scripts 部分(下面用 JSONC 添加注释,实际 JSON 文件不能带这些注释):
{
"scripts": {
"dev": "vite", // 启动开发服务器
"build": "run-p type-check \"build-only {@}\" --", // 类型检查 + 构建
"preview": "vite preview", // 预览构建产物
"build-only": "vite build", // 只构建,不类型检查
"type-check": "vue-tsc --build", // TypeScript 类型检查
"lint": "eslint . --fix" // ESLint 检查 + 自动修复
}
}| 命令 | 用途 | 什么时候用 |
|---|---|---|
npm run dev | 启动开发服务器 | 日常开发 |
npm run build | 生产构建 | 部署前 |
npm run preview | 预览生产构建 | 部署前验证 |
npm run lint | 代码风格检查 | 提交前 |
本课的构建机制和资源说明依据 Vite 6 官方文档。
12. 本节总结
知识清单
🔬 深度专题
📖 D01 · Options API vs Composition API — 为什么本教程全程使用 Composition API?
检查清单
- [ ] 能用
npm create vue@3.14.2创建项目 - [ ] 能解释 Vite 比 Webpack 快的原因
- [ ] 能说出
index.html → main.ts → App.vue的加载链 - [ ] 能解释 SFC 三段式结构的每个部分
- [ ] 能解释
<script setup>相比普通<script>省了什么 - [ ] 能区分
public/和src/assets/的用途 - [ ] 项目已完成第一次
git commit
课后练习
练习 1:跟做(10 min) 完整跟做本节内容:创建项目 → 清理默认文件 → 替换 App.vue → 启动确认。
练习 2:举一反三(15 min) 给 main.css 增加一套 CSS 变量(--color-primary、--color-bg、--color-text),在 App.vue 中使用这些变量。思考:为什么用 CSS 变量比硬编码颜色值更好?
挑战题(20 min) 在 vite.config.ts 中配置第二个路径别名 @components → src/components,确认在 .vue 文件中 import from '@components/xxx' 可以正常工作。同时需要同步修改 tsconfig.app.json 的 paths。
Git 提交
git add .
git commit -m "L01: 清理默认文件,搭建 Todo App 骨架"🔗 钩子连接
→ 下一节:L02 · 第一个组件:TodoItem
L02 将在当前项目基础上:
- 在
src/components/创建TodoItem.vue组件 - 学习
defineProps()定义组件接口 - 理解 Props 单向数据流 原则
- 在
App.vue中引入并渲染 TodoItem
L02 会用到这节课的:
- 项目结构(知道在哪创建组件)
<script setup>(知道 import 即自动注册)- SFC 三段式(知道如何组织组件代码)