Skip to content

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,按需转换源码;第三方依赖仍会预构建,生产环境仍会打包:

核心区别:

对比项WebpackVite
启动方式先打包,再提供服务先启动服务,按需编译
冷启动受依赖图、缓存和插件影响减少启动时需要转换的源码
热更新 (HMR)重建受影响模块并发送更新沿依赖链更新到最近的 HMR 边界
开发工具链Webpack + loaders / pluginsVite 开发服务器 + esbuild 依赖预构建
生产打包器WebpackRollup(Vite 6)

1.3 Vite 为什么能这么快 ​

两个关键技术:

  1. 预构建依赖(Pre-bundling):用 esbuild 把 node_modules 里的库(如 vue、lodash)转换为适合浏览器加载的 ESM 并合并部分模块;结果会缓存,依赖或相关配置变化时重新生成
  2. 按需编译源码:你的 .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 示例一致,固定脚手架版本:

bash
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:

bash
cd vue-todo
npm install vue@~3.5.13
npm run dev

按终端打印的地址打开浏览器(默认是 http://localhost:5173,端口占用时可能变化),看到 Vue 欢迎页就成功了。此版本会在项目中集成 vite-plugin-vue-devtools 调试插件,不会替你安装浏览器扩展。

2.2 初始化 Git ​

bash
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 config

3.1 关键文件逐行解读 ​

index.html — 浏览器入口 ​

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 应用入口 ​

typescript
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 单文件组件) ​

vue
<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 + style

5. <script setup> 语法糖 ​

5.1 普通写法 vs <script setup> ​

普通 setup() 写法(Vue 3 仍支持):

vue
<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> 语法糖(推荐):

vue
<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 ​

typescript
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 清理默认文件 ​

删除不需要的默认内容:

bash
# 删除默认组件和资源
rm src/components/HelloWorld.vue
rm src/components/TheWelcome.vue
rm src/components/WelcomeItem.vue
rm -rf src/components/icons/
rm src/assets/logo.svg

7.2 替换 src/App.vue ​

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 ​

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 确认效果 ​

bash
npm run dev

浏览器应该显示一个简洁的页面:标题 "📝 Vue Todo" + 一个占位框。


8. 理解热更新(HMR) ​

现在试一下:不关闭浏览器,修改 App.vue 里的标题文字。

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。下面只列出相关选项作说明,不要用它覆盖整个生成文件:

jsonc
{
  "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.pngimport 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 文件不能带这些注释):

jsonc
{
  "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 提交 ​

bash
git add .
git commit -m "L01: 清理默认文件,搭建 Todo App 骨架"

🔗 钩子连接 ​

→ 下一节:L02 · 第一个组件:TodoItem ​

L02 将在当前项目基础上:

  1. 在 src/components/ 创建 TodoItem.vue 组件
  2. 学习 defineProps() 定义组件接口
  3. 理解 Props 单向数据流 原则
  4. 在 App.vue 中引入并渲染 TodoItem

L02 会用到这节课的:

  • 项目结构(知道在哪创建组件)
  • <script setup>(知道 import 即自动注册)
  • SFC 三段式(知道如何组织组件代码)