导读:你还在手动操控 DOM 吗?

如果你是从 jQuery、Zepto 那个年代过来的前端开发者,一定有过这样的经历:数据变了,你要手动去找元素、改文本、加 class、绑事件——改一处牵全身,项目越大越像一团乱麻。

Vue3 的核心理念就一句话:你只管写数据,页面会自动更新。 这叫"响应式编程"——当你的 JavaScript 变量发生变化时,Vue 自动帮你把页面对应的部分刷新掉。你不需要再操心"哪个 DOM 需要改"。

此外,Vue3 引入了组合式 API(Composition API),核心语法是 <script setup>。它让你按"功能"组织代码(而不是按"生命周期钩子"拆散),代码更紧凑、更容易复用。

这篇教程会带着你从头搭建一个 Vue3 项目,一步步了解关键概念,最后做一个完整的 TodoList 小应用收尾。全程无废话,只讲你需要用的。

前置知识:基本的 HTML、CSS、JavaScript(ES6 箭头函数和解构赋值就够了)。如果你会用 npm 装过一个工具包,那就足够了。


一、Vue3 对比 jQuery:一张表说清楚

维度jQuery 时代(传统写法)Vue3 组合式 API
更新页面$('#box').text(newData) 手动操作 DOM变量变了,{{ data }} 自动更新
事件绑定.on('click', handler) 到处散布@click="handleClick" 写在模板里
组件化靠约定或第三方库(如 React 风格封装)内置 .vue 单文件组件,开箱即用
状态管理全局变量 / $bus 事件总线ref / reactive 响应式系统 + Pinia
构建方式CDN 直接引入即可推荐 Vite 脚手架(开发快、打包小)

jQuery 不是不好——它是那个时代的最佳实践。但在今天的项目复杂度面前,手动操作 DOM 成了最大的维护负担。Vue3 的思路是:声明式地描述"数据长什么样",让框架负责"DOM 怎么变"。


二、快速创建项目

Vue3 官方推荐的开发方式是使用 Vite 作为构建工具。Vite 非常快——冷启动通常不到 1 秒,因为浏览器原生支持 ES Module,Vite 利用了这个特性省去了打包这一步。

前提条件

首先需要安装两个东西:

# 检查 Node.js 版本(Vue3 + Vite 要求 Node 18+,以官网最新要求为准)
node -v        # 期望输出 v18.x 或更高(比如 v20.10.0)

# 检查 npm 版本
npm -v         # 期望输出 9.x 或更高

如果你的电脑还没装 Node.js,去 nodejs.org 下载 LTS 版本(长期支持版)。国内用户如果下载慢,可以用淘宝镜像

用 Vite 初始化项目

在终端中运行以下命令:

# -- 第一步:用 Vite 创建一个 Vue 项目(会自动下载 create-vue 脚手架工具)--
npm create vue@latest my-vue-app
# 然后按提示选择功能:
#   ❯ Project name: → 直接回车,默认 my-vue-app
#   ❯ Add TypeScript?            → 选 No(本文用纯 JavaScript,更简单)
#   ❯ Add JSX Support?           → 选 No
#   ❯ Add Vue Router?            → 选 No(先学基础,以后再加)
#   ❯ Add Pinia?                 → 选 No
#   ❯ Add Vitest?                → 选 No
#   ❯ Add ESLint?                → 选 No(后续有需要再开)
#   ❯ Add Playwright?            → 选 No
#
# 选完后等它自动安装依赖。

cd my-vue-app       # 进入项目目录

# -- 第二步:安装所有依赖(上面选了 No 的话基本没有额外依赖)--
npm install

# -- 第三步:启动开发服务器 --
npm run dev

# 启动成功后,终端会显示类似如下信息:
#   ➜  Local:   http://localhost:5173/
#   ➜  Network: use --host to expose
# 打开浏览器访问 http://localhost:5173/ 就能看到 "Hello Vue" 页面了!

国内加速提示:如果 npm install 速度很慢,可以切换镜像源(见文末一键脚本)。或者临时用这条命令:

npm config set registry https://registry.npmmirror.com

认识项目结构

创建好之后,项目的核心目录长这样:

my-vue-app/
├── node_modules/          # 依赖包(别手动改这里)
├── public/                # 静态资源(favicon.ico 等)
├── src/                   # 👈 你的代码全部放在这里
│   ├── assets/            #   图片、样式等资源
│   ├── components/        #   可复用的组件放这里
│   ├── App.vue            #   根组件(整个应用的入口)
│   └── main.js            #   应用启动文件(程序从这里开始跑)
├── index.html             #   HTML 模板
├── package.json           #   项目配置和依赖列表
├── vite.config.js         #   Vite 配置(一般不用动)
└── ...

最关键的三个文件:

  • main.js:启动入口,相当于建筑的基石
  • App.vue:根组件,相当于房子本身
  • index.html:HTML 外壳,相当于地基的蓝图

三、.vue 单文件组件

Vue3 的核心是 .vue 文件,也叫单文件组件(SFC,Single File Component)。一个 .vue 文件包含三部分:

<!-- MyButton.vue -->
<template>
  <!-- 这里是 HTML 模板就像你在写普通网页一样 -->
  <button @click="handleClick">{{ label }}</button>
</template>

<script setup>
  // 这里是 JavaScript 逻辑 —— 组合式 API 的代码写在这里
  import { ref } from 'vue'

  // 定义 props(父组件传给我的数据)
  const props = defineProps({
    label: String      // 按钮上显示的文字
  })

  // 定义 emit(我要向父组件发送的事件)
  const emit = defineEmits(['click'])

  function handleClick() {
    emit('click')       // 触发 click 事件告诉父组件
  }
</script>

<style scoped>
  /* 这里是 CSS 样式,scoped 表示只影响当前组件 */
  button {
    padding: 8px 16px;
    border-radius: 4px;
    cursor: pointer;
  }
</style>

每一部分的作用一目了然:

  • <template> = 页面长什么样(HTML)
  • <script setup> = 数据怎么管理(JS 逻辑)
  • <style scoped> = 样式怎么定义(CSS,scoped 保证不串台)

这就是 Vue3 的"结构化"——把 HTML、JS、CSS 写在一起,而不是分散在三个地方。


四、<script setup> 语法

<script setup> 是 Vue3 的组合式 API 入口,比普通 <script> 写法简洁得多。在 <script setup> 中:

  • 顶层变量和函数会自动暴露给模板——不需要 return!
  • 可以直接使用 import 导入其他模块
  • definePropsdefineEmits 声明父子通信

下面用实际例子来对比一下:

没有 <script setup>(旧式写法)

// 需要 return,否则模板看不到这些变量
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}

用了 <script setup>(新式写法)

<script setup>
import { ref } from 'vue'

const count = ref(0)          // ← 顶层声明,模板直接用
function increment() {         // ← 顶层函数,模板直接用
  count.value++              //   注意:ref 的值要写 .value
}
</script>

<template>
  <button @click="increment">点了我 {{ count }} </button>
</template>

是不是简洁多了?没有 data()、没有 methods、没有 return,也没有 this


五、ref 和 reactive:响应式两大武器

Vue3 的响应式系统有两个核心 API,用来让变量"变得智能"——变量变化时自动触发页面更新。

ref:包装单个值

ref 适合包装单个值(数字、字符串、布尔值等基础类型),当然也可以包装数组或对象。

<script setup>
import { ref } from 'vue'

// 创建一个值为 0 的响应式引用
const count = ref(0)

// 在 JavaScript 中修改 ref 的值,必须加上 .value
function add() {
  count.value++        // 每次点击 +1
}
function reset() {
  count.value = 0
}
</script>

<template>
  <div>
    <p>计数器{{ count }}</p>
    <!-- 模板里不需要 .valueVue 自动解包 -->
    <button @click="add">+1</button>
    <button @click="reset">重置</button>
  </div>
</template>

为什么需要 .value ref 内部是一个对象 { value: xxx }。在模板中 Vue 自动帮你取了 .value,所以你写 {{ count }} 就行;但在 JavaScript 代码里你需要显式写 count.value。这个设计是为了区分"ref 引用"和"普通变量"。

reactive:包装对象或数组

如果你需要管理多个相关的值,reactive 更方便——它接收一个对象并返回其响应式代理,你直接点语法访问,不需要 .value

<script setup>
import { ref, reactive } from 'vue'

// reactive 适合包装对象或数组
const todoForm = reactive({
  text: '',
  priority: 'medium'   // low / medium / high
})

function handleSubmit() {
  if (todoForm.text.trim()) {
    console.log('提交:', todoForm.text, todoForm.priority)
    todoForm.text = ''         // 清空输入框
  }
}
</script>

<template>
  <form @submit.prevent="handleSubmit">
    <input v-model="todoForm.text" placeholder="输入内容..." />
    <select v-model="todoForm.priority">
      <option value="low"></option>
      <option value="medium"></option>
      <option value="high"></option>
    </select>
    <button type="submit">添加</button>
  </form>
</template>

选 ref 还是 reactive? 经验法则:如果只是一个值(比如计数器),用 ref;如果是一组相关的属性(比如表单、配置),用 reactive。两者可以混搭使用。


六、组件通信:props 向下,emit 向上

Vue3 的组件是树状结构的——父组件包裹子组件。数据传递遵循单向流动原则

  • props(属性):父 → 子(父组件传数据给子组件,只读不可改)
  • emit(事件):子 → 父(子组件通知父组件"发生了某事",由父组件决定怎么做)

这就像公司里的汇报关系:老板下达指令(props),员工上报进度(emit),但员工不能直接替老板做决定。

用 props 传数据

父组件 App.vue

<script setup>
import Header from './components/Header.vue'
</script>

<template>
  <!-- title 就是 prop我们给它传了一个字符串 -->
  <Header title="我的待办清单" />
  <!-- 也可以传动态数据 -->
  <Header :title="'待办 - 今日' + new Date().toLocaleDateString()" />
</template>

子组件 Header.vue

<script setup>
import { defineProps } from 'vue'

// 声明这个组件接收什么 props
const props = defineProps({
  title: {
    type: String,
    required: true
  }
})
</script>

<template>
  <header>
    <h1>{{ props.title }}</h1>
  </header>
</template>

用 emit 向上传递事件

子组件 AddTodo.vue

<script setup>
import { reactive } from 'vue'

const props = defineProps(['list'])     // 拿到父组件传来的 list
const emit = defineEmits(['add'])        // 声明要触发的事件名

const newTodo = reactive({ text: '' })

function submit() {
  if (!newTodo.text.trim()) return
  emit('add', newTodo.text)              // 把文字传给父组件
  newTodo.text = ''                      // 清空输入
}
</script>

<template>
  <form @submit.prevent="submit">
    <input v-model="newTodo.text" placeholder="新增待办..." />
    <button type="submit"></button>
  </form>
</template>

父组件 App.vue

<script setup>
import { ref } from 'vue'
import AddTodo from './components/AddTodo.vue'

const todos = ref([
  { id: 1, text: '学习 Vue3', done: false }
])

// 监听子组件触发的 'add' 事件
function handleAdd(text) {
  todos.value.push({ id: Date.now(), text, done: false })
}
</script>

<template>
  <AddTodo :list="todos" @add="handleAdd" />
  <!--  :list  props   @add 监听 emit 事件 -->
</template>

记住这个对应关系:

  • 父用 : 前缀传 prop → <Child :msg="hello" />
  • 子用 emit('event') 发信号
  • 父用 @ 前缀接事件 → <Child @event="handler" />

七、渲染控制:v-if 和 v-for

前端最常用的两种循环和条件渲染,Vue 提供了专属指令(以 v- 开头的特殊 attribute)。

v-if / v-show:条件渲染

<template>
  <!-- v-if条件为假时元素根本不会出现在 DOM  -->
  <p v-if="isLoggedIn">欢迎回来管理员</p>
  <p v-else>请先登录</p>

  <!-- v-show始终存在只是 CSS display: none 隐藏 -->
  <span v-show="showExtraInfo">这是附加信息</span>
</template>

<script setup>
import { ref } from 'vue'

const isLoggedIn = ref(false)
const showExtraInfo = ref(true)
</script>

v-if vs v-show 怎么选?

  • 很少切换 → 用 v-if(节省渲染开销)
  • 频繁切换(比如开关面板)→ 用 v-show(只需改 CSS,不涉及创建/销毁 DOM)

v-for:列表循环

<template>
  <ul>
    <!--  index 是可选的第二参数代表当前索引 -->
    <!--  :key 很重要帮助 Vue 高效追踪每个列表项必须唯一 -->
    <li v-for="(item, index) in items" :key="item.id">
      {{ item.name }}
    </li>
  </ul>
</template>

<script setup>
import { ref } from 'vue'

const items = ref([
  { id: 1, name: 'Vue3 组合式 API 实战' },
  { id: 2, name: 'React Hooks 入门指南' },
  { id: 3, name: 'Node.js 后端开发手册' }
])
</script>

:key 为什么重要? 当列表增删时,Vue 需要知道"哪个 DOM 对应哪条数据"。key 就是这个标识。如果不设 key 或设成 index,在列表顺序变化时可能出现渲染 bug。


八、动手实践:写一个 TodoList 应用

现在把我们学到的所有内容串起来,写一个功能完整的 TodoList 应用。它包含:

  • 添加待办事项
  • 标记完成/未完成
  • 删除已完成
  • 统计剩余数量
  • 按状态筛选(全部 / 未开始 / 已完成)

直接把下面的代码粘贴到你新建的 Vue3 项目中试试。

步骤 1:替换 App.vue

<script setup>
import { ref, computed } from 'vue'

// ====== 数据层 ======
const todos = ref([
  { id: 1, text: '学习 Vue3 组合式 API', done: false },
  { id: 2, text: '做一个 TodoList 练手', done: false },
  { id: 3, text: '部署上线分享给朋友', done: true }
])

const newTodoText = ref('')
const filterOption = ref('all')  // all / active / completed

// ====== 计算属性(数据变了自动重新计算)======
const filteredTodos = computed(() => {
  if (filterOption.value === 'active') {
    return todos.value.filter(t => !t.done)
  }
  if (filterOption.value === 'completed') {
    return todos.value.filter(t => t.done)
  }
  return todos.value
})

const remainingCount = computed(() => {
  return todos.value.filter(t => !t.done).length
})

// ====== 方法层 ======
function addTodo() {
  const text = newTodoText.value.trim()
  if (!text) return               // 空内容不添加
  todos.value.push({
    id: Date.now(),              // 用时间戳做唯一 ID(简单够用)
    text: text,
    done: false
  })
  newTodoText.value = ''          // 清空输入框
}

function toggleTodo(todo) {
  todo.done = !todo.done
}

function removeCompleted() {
  todos.value = todos.value.filter(t => !t.done)
}
</script>

<template>
  <div class="todo-app">
    <h1>📝 我的待办清单</h1>

    <!-- 输入区域 -->
    <div class="input-row">
      <input
        v-model="newTodoText"
        @keyup.enter="addTodo"
        placeholder="输入待办内容,回车添加..."
      />
      <button @click="addTodo">添加</button>
    </div>

    <!-- 筛选按钮 -->
    <div class="filters">
      <button :class="{ active: filterOption === 'all' }"
              @click="filterOption = 'all'">全部 ({{ todos.length }})</button>
      <button :class="{ active: filterOption === 'active' }"
              @click="filterOption = 'active'">未开始 ({{ remainingCount }})</button>
      <button :class="{ active: filterOption === 'completed' }"
              @click="filterOption = 'completed'">已完成 ({{ todos.length - remainingCount }})</button>
      <button class="danger" @click="removeCompleted" v-if="todos.length - remainingCount > 0">
        清除已完成
      </button>
    </div>

    <!-- 列表区域 -->
    <ul>
      <li v-for="todo in filteredTodos" :key="todo.id"
          :class="{ done: todo.done }">
        <label>
          <input type="checkbox" :checked="todo.done"
                 @change="toggleTodo(todo)" />
          <span>{{ todo.text }}</span>
        </label>
      </li>
    </ul>

    <p v-if="filteredTodos.length === 0" class="empty">
      🎉 没有待办事项休息一下吧
    </p>
  </div>
</template>

<style>
/* ---------- 基础主题变量(亮色 / 暗色双模式)---------- */
:root {
  --bg: #f9fafb;
  --card-bg: #ffffff;
  --text: #1e293b;
  --muted: #64748b;
  --border: #e2e8f0;
  --primary: #3b82f6;
  --success: #22c55e;
  --danger: #ef4444;
  --hover: #f1f5f9;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f172a;
    --card-bg: #1e293b;
    --text: #e2e8f0;
    --muted: #94a3b8;
    --border: #334155;
    --primary: #60a5fa;
    --success: #4ade80;
    --danger: #f87171;
    --hover: #2d3a4d;
  }
}

/* ---------- TodoList 样式 ---------- */
.todo-app {
  max-width: 560px;
  margin: 2rem auto;
  padding: 2rem;
  background: var(--card-bg);
  border-radius: 12px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.08);
  font-family: system-ui, -apple-system, sans-serif;
  color: var(--text);
}
.todo-app h1 {
  text-align: center;
  margin-bottom: 1.5rem;
  font-size: 1.5rem;
}
.input-row {
  display: flex;
  gap: 8px;
  margin-bottom: 1rem;
}
.input-row input {
  flex: 1;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 14px;
  background: var(--bg);
  color: var(--text);
}
.input-row button, .filters button {
  padding: 8px 14px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 13px;
  background: var(--primary);
  color: white;
}
.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 1rem;
}
.filters button.active {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
}
.filters button.danger {
  background: var(--danger);
  margin-left: auto;
}
ul {
  list-style: none;
  padding: 0;
}
li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border-radius: 6px;
  margin-bottom: 4px;
  cursor: pointer;
}
li:hover {
  background: var(--hover);
}
li.done span {
  text-decoration: line-through;
  color: var(--muted);
}
.empty {
  text-align: center;
  color: var(--muted);
  padding: 2rem;
}
</style>

步骤 2:启动开发服务器

npm run dev

然后在浏览器打开显示的地址(通常是 http://localhost:5173),你就能看到一个功能齐全的 TodoList 应用了!

试着添加几条待办、标记完成、切换筛选——所有操作都是响应式的,数据变页面就跟着变。


九、进阶:抽离组件,让代码更清晰

当项目变大时,把所有逻辑塞进 App.vue 会很难维护。我们来把上面的 TodoList 拆成几个小组件:

App.vue                  — 主容器
├── TodoInput.vue        — 输入框组件(负责收集新待办的文字)
├── TodoFilterBar.vue    — 筛选栏组件(负责筛选和清除)
└── TodoList.vue         — 列表组件(负责渲染每一条待办)

TodoInput.vue

<script setup>
import { reactive } from 'vue'

const emit = defineEmits(['add'])
const form = reactive({ text: '' })

function submit() {
  if (!form.text.trim()) return
  emit('add', form.text)
  form.text = ''
}
</script>

<template>
  <form class="input-row" @submit.prevent="submit">
    <input v-model="form.text" placeholder="输入待办内容,回车添加..." />
    <button type="submit">添加</button>
  </form>
</template>

TodoFilterBar.vue

<script setup>
const props = defineProps(['total', 'active', 'completed', 'hasCompleted'])
const emit = defineEmits(['filter', 'clear'])

const filter = defineModel()    // Vue 3.4+ 语法糖,双向绑定
</script>

<template>
  <div class="filters">
    <button :class="{ active: filter === 'all' }"
            @click="emit('filter', 'all')">全部 ({{ total }})</button>
    <button :class="{ active: filter === 'active' }"
            @click="emit('filter', 'active')">未开始 ({{ active }}</button>
    <button :class="{ active: filter === 'completed' }"
            @click="emit('filter', 'completed')">
      已完成 ({{ completed }})</button>
    <button v-if="hasCompleted" class="danger"
            @click="emit('clear')">清除已完成</button>
  </div>
</template>

TodoList.vue

<script setup>
defineProps(['items'])
const emit = defineEmits(['toggle'])
</script>

<template>
  <ul>
    <li v-for="todo in items" :key="todo.id"
        :class="{ done: todo.done }" @click="emit('toggle', todo)">
      <label>
        <input type="checkbox" :checked="todo.done" />
        <span>{{ todo.text }}</span>
      </label>
    </li>
  </ul>
</template>

App.vue(瘦身后的主容器)

<script setup>
import { ref, computed } from 'vue'
import TodoInput from './components/TodoInput.vue'
import TodoFilterBar from './components/TodoFilterBar.vue'
import TodoList from './components/TodoList.vue'

const todos = ref([
  { id: 1, text: '拆分组件', done: false },
  { id: 2, text: '测试联动', done: false }
])
const filter = ref('all')

const filteredTodos = computed(() => {
  if (filter.value === 'active') return todos.value.filter(t => !t.done)
  if (filter.value === 'completed') return todos.value.filter(t => t.done)
  return todos.value
})

const counts = computed(() => ({
  total: todos.value.length,
  active: todos.value.filter(t => !t.done).length,
  completed: todos.value.filter(t => t.done).length
}))

function handleAdd(text) {
  todos.value.push({ id: Date.now(), text, done: false })
}
function handleToggle(todo) {
  todo.done = !todo.done
}
function handleClear() {
  todos.value = todos.value.filter(t => !t.done)
}
</script>

<template>
  <div class="todo-app">
    <h1>📝 我的待办清单</h1>
    <TodoInput @add="handleAdd" />
    <TodoFilterBar
      v-model="filter"
      :total="counts.total"
      :active="counts.active"
      :completed="counts.completed"
      :has-completed="counts.completed > 0"
      @filter="filter = $event"
      @clear="handleClear"
    />
    <TodoList :items="filteredTodos" @toggle="handleToggle" />
  </div>
</template>

可以看到,拆分后 App.vue 的逻辑清爽了很多:它只负责数据管理和调度,具体 UI 交给了子组件。这就是组件化的威力。


十、小结与延伸

到这里你已经掌握了 Vue3 组合式 API 的核心概念:

概念一句话总结
<script setup>简洁的组件逻辑入口,顶层变量直接暴露给模板
ref()包装单个值的响应式引用,JS 中需 .value
reactive()包装对象的响应式代理,点语法直接访问
props父 → 子传递数据的单向通道
emit子 → 父报告事件的单向通道
v-if / v-show条件渲染(隐藏或不渲染)
v-for列表循环渲染(别忘了 :key
computed自动追踪依赖的计算属性

下一步该学什么?

  • Vue Router:多页面路由管理(SPA 的灵魂)
  • Pinia:跨组件的状态管理(替代 Vuex)
  • Tailwind CSS:原子化 CSS 框架,配合 Vue 开发效率翻倍
  • 后端接入:前端写好了,怎么跟后端 API 交互?这时候你可以考虑用云间 API 中转站(cloudzone-api.cyou),它的 OpenAI 兼容接口可以直接对接 AI 能力——代码补全、文案生成、智能摘要等等。对于不想直连海外服务的开发者来说,90+ 模型、国内直连、0.05x 起的性价比非常香,而且香港节点延迟低,体验丝滑。

💡 一个小提醒:Vue 生态中很多工具和库(包括一些 AI 辅助编程工具)的后端需要调用外部 API。如果在国内直连不稳定,可以考虑用 OpenAI 兼容的中转服务,云间就是个不错的选择——价格不到官方的十分之一,且支持 Anthropic 系列模型。

祝你前端之旅愉快!有任何问题欢迎在评论区讨论。


附录:一键安装脚本(双版本)

以下是本文的一键脚本源码,公开透明,欢迎复制给任何 AI 审查。

脚本功能:检测网络环境 → 确认 Node.js 版本 → 配置 npm 镜像 → 创建 Vite + Vue 项目 → 安装依赖 → 启动开发服务器。

不方便下载的同学可以直接复制下方完整源码,新建文本文档粘贴后改后缀为 .sh.ps1 运行。

也可从 https://cleanresolver.com/scripts/install-vue3-guide.sh.ps1)下载。

Linux / macOS / WSL 版

#!/usr/bin/env bash
set -u
# ============================================================
#  Vue3 组合式 API 实战 — 一键脚本(macOS / Linux / WSL)
#  公开源码,欢迎审查 —— 不放心可先复制给 AI 判断
#  功能:检测 Node.js → 配镜像 → 用 Vite 创建 Vue 项目 → 启动
# ============================================================

GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; CYAN='\033[0;36m'; NC='\033[0m'
info()  { echo -e "${GREEN}[INFO]${NC} $1"; }
warn()  { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
step()  { echo -e "${CYAN}[STEP]${NC} $1"; }

# ---- 第 1 步:检测 Node.js ----
check_node() {
  step "检查 Node.js 环境..."
  if ! command -v node >/dev/null 2>&1; then
    error "未检测到 Node.js。请先安装:"
    echo "  macOS:   brew install node"
    echo "  Ubuntu:  sudo apt install -y nodejs npm"
    echo "  Windows: 去 https://nodejs.org 下载 LTS 版安装包(国内可用 npmmirror.com 镜像)"
    exit 1
  fi
  local NODE_VERSION
  NODE_VERSION=$(node -v | sed 's/v//')
  info "当前 Node.js 版本:v${NODE_VERSION}"

  # 比较版本号是否 >= 18(用 awk 做浮点比较)
  local MAJOR
  MAJOR=$(echo "$NODE_VERSION" | cut -d. -f1)
  if [ "$MAJOR" -lt 18 ]; then
    warn "当前 Node.js v${NODE_VERSION} 低于推荐版本 18。Vite 可能需要 Node 18+,以官网最新要求为准。"
    echo "  建议升级:https://nodejs.org(或 npmmirror.com/mirrors/node/)"
    read -r -p "是否继续?[Y/N]: " yn
    case "$yn" in
      [Yy]* ) ;;
      * ) error "已中止。请先升级 Node.js。"; exit 1;;
    esac
  else
    info "Node.js 版本满足要求 ✅"
  fi
}

# ---- 第 2 步:网络检测 + npm 镜像配置 ----
configure_npm() {
  step "配置 npm 镜像源..."
  local current_registry
  current_registry=$(npm config get registry 2>/dev/null || echo "")

  # 判断是否为国内镜像
  if echo "$current_registry" | grep -qi "npmmirror\|taobao\|tencent"; then
    info "npm 已配置为国内镜像:$current_registry"
  else
    info "将 npm 镜像切换至 npmmirror(国内加速)..."
    npm config set registry https://registry.npmmirror.com
    info "已切换。原 registry:${current_registry:-(默认官方源)}"
  fi
  info "当前 registry:$(npm config get registry)"
}

# ---- 第 3 步:创建 Vue 项目 ----
create_project() {
  step "创建 Vue 项目..."
  local PROJECT_DIR="${1:-my-vue-app}"

  if [ -d "$PROJECT_DIR" ]; then
    error "目录 '$PROJECT_DIR' 已存在。请换一个名字或删除已有目录。"
    exit 1
  fi

  info "正在运行:npm create vue@latest $PROJECT_DIR"
  info "(按回车选用默认选项即可,推荐全部选 No 保持简洁)"

  npm create vue@latest "$PROJECT_DIR" <<EOF

y


n
n
n
n
n
n
y
EOF

  if [ ! -d "$PROJECT_DIR" ]; then
    error "项目创建失败。请检查网络后重试。"
    exit 1
  fi

  info "项目创建成功:$PROJECT_DIR"
  cd "$PROJECT_DIR" || exit 1
}

# ---- 第 4 步:安装依赖 ----
install_deps() {
  step "安装项目依赖..."
  local NET_TIMEOUT="--timeout 120"

  if npm install $NET_TIMEOUT; then
    info "依赖安装完成 ✅"
    return 0
  fi

  warn "npm install 失败,尝试回退官方源..."
  npm config set registry https://registry.npmjs.org
  if npm install $NET_TIMEOUT; then
    info "依赖安装完成(官方源)✅"
    return 0
  fi

  error "所有源安装均失败。可能的原因:"
  echo "  1. 网络不通:检查 DNS 和防火墙"
  echo "  2. npm 版本过低:执行 npm install -g npm@latest 升级"
  echo "  3. 磁盘空间不足:df -h 查看可用空间"
  return 1
}

# ---- 第 5 步:启动开发服务器 ----
start_dev_server() {
  step "启动开发服务器..."
  info ""
  info "============================================"
  info "  项目已就绪!"
  info ""
  info "  cd $PWD"
  info "  npm run dev"
  info ""
  info "  启动后浏览器访问:http://localhost:5173/"
  info ""
  info "  生产构建:npm run build"
  info "  预览构建产物:npm run preview"
  info "============================================"
  echo ""
  info "如需立即启动开发服务器,请输入 Y;稍后再跑请输入 N。"
  read -r -p "是否现在启动?[Y/N]: " start_now
  if [ "${start_now:-N}" = "Y" ] || [ "${start_now:-N}" = "y" ]; then
    step "正在启动 dev server..."
    npm run dev
  else
    info "好的,随时运行 npm run dev 即可启动。"
    info "提示:想后台运行可以加 &,或用 nohup/npm start 持久化。"
  fi
}

# ---- 主流程 ----
main() {
  echo "============================================"
  echo "  Vue3 组合式 API 实战 — 一键脚本"
  echo "  适用于 macOS / Linux / WSL"
  echo "============================================"
  echo ""

  check_node

  PROJECT_DIR="${1:-my-vue-app}"
  configure_npm

  create_project "$PROJECT_DIR"

  if ! install_deps; then
    error "环境准备失败,脚本终止。请按上方提示处理。"
    exit 1
  fi

  start_dev_server
}

main "$@"

Windows PowerShell 版

# ============================================================
#  Vue3 组合式 API 实战 — 一键脚本(Windows PowerShell)
#  公开源码,欢迎审查 -- 不放心可先复制给 AI 判断
#  功能:检测 Node.js -> 配镜像 -> 用 Vite 创建 Vue 项目 -> 启动
# ============================================================

$ErrorActionPreference = "Stop"

function Write-Info  { Write-Host "[INFO] $args" -ForegroundColor Green }
function Write-Warn  { Write-Host "[WARN] $args" -ForegroundColor Yellow }
function Write-Err   { Write-Host "[ERROR] $args" -ForegroundColor Red }
function Write-Step  { Write-Host "[STEP] $args" -ForegroundColor Cyan }

# ---- 第 1 步:检测 Node.js ----
function Check-Node {
    Write-Step "检查 Node.js 环境..."

    if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
        Write-Err "未检测到 Node.js。请先安装:"
        Write-Host "  去 https://nodejs.org 下载 LTS 版安装包"
        Write-Host "  (国内用户可用 npmmirror.com/mirrors/node/ 镜像加速下载)"
        exit 1
    }

    $nodeVersion = & node -v
    Write-Info "当前 Node.js 版本:$nodeVersion"

    # 提取主版本号
    $major = [int]($nodeVersion -replace 'v(\d+)\..*', '$1')
    if ($major -lt 18) {
        Write-Warn "当前 Node.js $nodeVersion 低于推荐版本 18。Vite 可能需要 Node 18+,以官网最新要求为准。"
        Write-Host "  建议升级:https://nodejs.org 或 npmmirror.com/mirrors/node/"
        $yn = Read-Host "是否继续?[Y/N]"
        if ($yn -ne "Y" -and $yn -ne "y") {
            Write-Err "已中止。请先升级 Node.js。"
            exit 1
        }
    } else {
        Write-Info "Node.js 版本满足要求 ✅"
    }
}

# ---- 第 2 步:npm 镜像配置 ----
function Configure-Npm {
    Write-Step "配置 npm 镜像源..."

    $currentRegistry = npm config get registry 2>$null

    if ($currentRegistry -match 'npmmirror|taobao|tencent') {
        Write-Info "npm 已配置为国内镜像:$currentRegistry"
    } else {
        Write-Info "将 npm 镜像切换至 npmmirror(国内加速)..."
        npm config set registry https://registry.npmmirror.com
        Write-Info "已切换。当前 registry:$(npm config get registry)"
    }
}

# ---- 第 3 步:创建 Vue 项目 ----
function Create-Project {
    param([string]$ProjectDir = "my-vue-app")

    if (Test-Path $ProjectDir) {
        Write-Err "目录 '$ProjectDir' 已存在。请换一个名字或删除已有目录。"
        exit 1
    }

    Write-Step "创建 Vue 项目..."
    Write-Info "正在运行:npm create vue@latest $ProjectDir"
    Write-Host "(脚本将自动选择默认选项:TypeScript=No, Router=No, Pinia=No...)"
    Write-Host ""

    try {
        # 通过 stdin 自动回答 yes(允许覆盖同名包)
        $proc = Start-Process -FilePath "npx" -ArgumentList "create-vue@latest", $ProjectDir -NoNewWindow -PassThru -RedirectStandardInput "$null"
        $proc.WaitForExit()
        $exitCode = $proc.ExitCode
    } catch {
        Write-Err "项目创建失败:$_"
        Write-Host "请手动在终端运行:npx create-vue@latest $ProjectDir"
        Write-Host "按提示选择:All features = No (y/N),回车即采用默认。"
        exit 1
    }

    if (-not (Test-Path $ProjectDir)) {
        Write-Err "项目目录未创建,请检查上述日志。"
        exit 1
    }

    Write-Info "项目创建成功:$ProjectDir"
    Set-Location $ProjectDir
}

# ---- 第 4 步:安装依赖 ----
function Install-Deps {
    Write-Step "安装项目依赖..."

    try {
        # 先用 npmmirror 安装
        npm install --timeout 120000
        Write-Info "依赖安装完成 ✅"
        return $true
    } catch {
        Write-Warn "npm install 失败,回退官方源..."
        npm config set registry https://registry.npmjs.org
        try {
            npm install --timeout 120000
            Write-Info "依赖安装完成(官方源)✅"
            return $true
        } catch {
            Write-Err "所有源安装均失败。可能的原因:"
            Write-Host "  1. 网络不通:检查网络和防火墙"
            Write-Host "  2. npm 版本过低:运行 npm install -g npm@latest 升级"
            Write-Host "  3. 磁盘空间不足:运行 Get-PSDrive C 查看"
            return $false
        }
    }
}

# ---- 主流程 ----
Write-Host "============================================"
Write-Host "  Vue3 组合式 API 实战 — 一键脚本"
Write-Host "  适用于 Windows"
Write-Host "============================================"
Write-Host ""

Check-Node
Create-Project
Configure-Npm

if (-not (Install-Deps)) {
    Write-Err "环境准备失败,脚本终止。请按上方提示处理。"
    exit 1
}

Write-Host ""
Write-Host "============================================"
Write-Host "  项目已就绪!"
Write-Host ""
Write-Host "  在项目目录下运行:npm run dev"
Write-Host "  启动后浏览器访问:http://localhost:5173/"
Write-Host ""
Write-Host "  生产构建:npm run build"
Write-Host "============================================"
Write-Host ""
Write-Host "脚本公开源码,欢迎复制给任何 AI 审查。"
Write-Host "也可靠此链接下载:https://cleanresolver.com/scripts/install-vue3-guide.ps1"

参考资料