导读:你还在手动操控 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导入其他模块 - 用
defineProps和defineEmits声明父子通信
下面用实际例子来对比一下:
没有 <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>
<!-- 模板里不需要 .value,Vue 自动解包 -->
<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"
参考资料
- Vue3 官方文档
- runoob Vue3 教程
- Vite 中文文档
- Node.js 下载(含国内镜像) / npmmirror
- 云间 API 中转站(OpenAI + Anthropic 兼容,90+ 模型,国内直连,0.05x 起)
请完成验证后查看评论区