前端开发从入门到精通:JavaScript → TypeScript → Vue 3 实战指南
- 约 24 分钟读完 - 全文 9306 字前端开发从入门到精通:JavaScript → TypeScript → Vue 3 实战指南
目标读者:会一点 HTML/CSS,想系统进入前端世界;或者写过大半年 Vue,但总觉得「代码能跑,心里没底」的同学。
这篇文章是我这些年写前端、带新人、修线上事故的一份复盘。它不讲完所有 API(MDN 和官方文档做得比我好),而是讲三件事:
- 主干知识:每个阶段真正重要的 20% 是什么。
- 踩坑故事:我自己和身边人真实踩过的坑,比知识点本身更值钱。
- 思考与感悟:为什么这条路线是 JavaScript → TypeScript → Vue 3,而不是反过来。
先给一张全文的路线图:
flowchart LR
A["HTML / CSS<br/>页面骨架与样式"] --> B["JavaScript<br/>让页面动起来"]
B -->|"语言内功"| C["执行模型<br/>作用域 闭包 原型<br/>事件循环 异步"]
C --> D["TypeScript<br/>类型系统<br/>编译期排错"]
D --> E["Vue 3<br/>响应式 + 组件化"]
E --> F["工程化<br/>Vite / Pinia / Router"]
F --> G["真实项目<br/>能写 能改 能扛事"]
很多新人想跳过 B 和 D 直接学 E,结果写 setup 里的每一行代码都像在背课文。框架是皮,语言是骨。这篇文章的顺序,就是我认为唯一正确的顺序。
第一篇 JavaScript:先把语言的骨头长出来
1.1 JavaScript 到底是什么
一句话版本:一门单线程、基于事件循环、动态类型的脚本语言。
flowchart TB
subgraph 浏览器
A["调用栈 Call Stack<br/>同步代码在这里执行"]
B["宏任务队列<br/>setTimeout / setInterval / IO"]
C["微任务队列<br/>Promise.then / queueMicrotask"]
D["Event Loop"]
end
A -->|"栈空了就问 D"| D
D -->|"先清空"| C
C -->|"清空了再取"| B
B --> A
「单线程」是理解 JavaScript 一切怪异行为的总钥匙:因为它只有一个调用栈,所以耗时操作绝对不能阻塞它,于是有了回调、Promise、async/await 这一整条异步进化链。
1.2 事件循环:一道面试题背后的真功夫
先看这段代码,你猜输出是什么?
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');答案是 1 4 3 2。规则只有两条:
- 同步代码在调用栈里,先跑完。
- 栈空之后,先清空所有微任务,再取一个宏任务。
踩坑故事 1:一个 0ms 的 setTimeout 引发的「灵异事件」
早年我维护一个老后台系统,有个 bug 时好时坏:弹窗打开后表单偶尔是空的。翻代码发现数据加载和弹窗渲染是两拨人写的,中间靠一个
setTimeout(..., 0)「协调顺序」。它的潜台词是「让数据先加载完」——但 0ms 宏任务只是排到下一轮,数据请求是网络 IO,什么时候回来全看运气。上线第一周没事,用户网络一差就炸。修复方法不是把 0 改成 100(那是赌得更大),而是用 Promise 把依赖关系显式写出来:
fetchData().then(openDialog)。从那以后我立了个规矩:代码里出现魔法数字延时,一律当 bug 处理。
1.3 作用域与闭包:var 的原罪
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:3 3 3
因为 var 没有块级作用域,循环结束后 i 只有一个,三个回调共享同一个 i。改成 let 就好了,因为 let 每次迭代都创建一个新的绑定。
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:0 1 2
闭包就是「函数 + 它出生时看到的词法环境」。下面这个计数器是闭包的标准用法:
function createCounter() {
let count = 0; // 外部拿不到,只能通过返回的函数碰它
return {
inc() { return ++count; },
get() { return count; },
};
}
const counter = createCounter();
counter.inc();
counter.inc();
console.log(counter.get()); // 2
思考:新人常把闭包当面试八股,其实它是 JavaScript 最重要的封装手段。ES Module 普及之前,整个前端世界的模块化都建立在闭包之上(IIFE)。理解了闭包,你就理解了「私有状态」这件事在没有 class 的时代是怎么做到的。
1.4 this:JavaScript 最大的心理阴影
this 的判定规则其实只有一句话:看它怎么被调用,而不是在哪里定义。
flowchart TD
A["函数被调用时 this 是什么?"] --> B{"new 调用?"}
B -->|是| C["新创建的对象"]
B -->|否| D{"call / apply / bind?"}
D -->|是| E["显式传入的对象"]
D -->|否| F{"作为对象方法调用 obj.fn()?"}
F -->|是| G["点号前面的 obj"]
F -->|否| H["undefined(严格模式)<br/>或 window(非严格)"]
I["箭头函数:没有自己的 this<br/>永远用定义处的 this"] -.-> A
踩坑故事 2:回调里的 this 丢了
团队新人写过这样的代码:
class UserService { constructor() { this.name = '方糖猫'; } greet() { console.log(`你好,${this.name}`); } } const svc = new UserService(); document.querySelector('#btn').addEventListener('click', svc.greet); // 点击后输出:你好,undefined
svc.greet被取出来当回调传递时,调用者变成了 DOM 元素,this就丢了。排查时新人百思不得其解:「我明明在 class 里定义的!」——这正是 1.4 开头那句话的意思。修法两个:addEventListener('click', () => svc.greet()),或者构造器里this.greet = this.greet.bind(this)。这件事对后面学 Vue 很重要:Vue 的
setup和 Composition API 大量使用普通函数和箭头函数,从根上绕开了 this 的坑,这是它相对 Options API 被推崇的深层原因之一。
1.5 异步进化史:从回调地狱到 async/await
flowchart LR
A["回调函数<br/>callback"] -->|"嵌套过深<br/>回调地狱"| B["Promise<br/>链式调用"]
B -->|"then 链仍啰嗦"| C["async / await<br/>同步写法"]
C -.->|"语法糖"| B
Promise 三种状态的流转是面试和实战的双重重点:
stateDiagram-v2
[*] --> Pending: new Promise(executor)
Pending --> Fulfilled: resolve(value)
Pending --> Rejected: reject(reason) 或抛异常
Fulfilled --> [*]: .then(onFulfilled)
Rejected --> [*]: .catch(onRejected)
note right of Pending
状态只能流转一次
且不可逆
end note
同一个逻辑,三种写法对比:
// 回调时代
getUser(id, function (err, user) {
if (err) return handle(err);
getOrders(user.id, function (err, orders) {
if (err) return handle(err);
render(orders);
});
});
// Promise 时代
getUser(id)
.then(user => getOrders(user.id))
.then(orders => render(orders))
.catch(handle);
// async/await 时代
async function showOrders(id) {
try {
const user = await getUser(id);
const orders = await getOrders(user.id);
render(orders);
} catch (err) {
handle(err);
}
}踩坑故事 3:await 放进 forEach 里「不生效」
const ids = [1, 2, 3]; ids.forEach(async id => { await saveRecord(id); }); console.log('全部保存完成'); // 这行会先执行!
forEach不会等回调里的 Promise,它只是把三个 async 函数同时踢出去就完事了。需要串行用for...of,需要并行用await Promise.all(ids.map(saveRecord))。这个坑我在 Code Review 里至少见过五次,堪称 async 时代第一坑。
1.6 引用类型与拷贝:看不见的共享
const a = { name: '猫', tags: ['前端'] };
const b = a; // 不是拷贝,是同一个对象的另一张门牌
b.name = '狗';
console.log(a.name); // '狗' —— a 也变了
- 浅拷贝(
Object.assign、{...obj}):只拷第一层,嵌套对象还是共享。 - 深拷贝:现代浏览器直接用
structuredClone(obj),别再手写递归了。
踩坑故事 4:改了这个表单,那个弹窗也跟着变
一个配置页有两处编辑入口,打开弹窗时
this.form = rowData。用户在弹窗里改着改着,背后的表格行数据实时跟着变——因为两个变量指向同一个对象。测试没测出来(他们只点了保存),用户取消编辑时发现数据「没保存也生效了」,提了 P1 故障。修法一行:this.form = structuredClone(rowData)。感悟:JavaScript 里 90% 的「数据莫名变化」,最后都能追到共享引用上。这也是后来 Vue 3 的响应式、Pinia 的 state 管理反复强调「数据流向要清晰」的原因——可变的共享状态是万恶之源。
1.7 Demo:手写防抖与节流
这两个函数是真实项目里用得最多的工具函数,也是检验你闭包和 this 是否真懂了的试金石:
// 防抖:停止触发 n 毫秒后才执行(搜索框联想)
function debounce(fn, delay = 300) {
let timer = null;
return function (...args) {
clearTimeout(timer); // 闭包记住了上一次的 timer
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 节流:n 毫秒内最多执行一次(滚动监听)
function throttle(fn, interval = 200) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last >= interval) {
last = now;
fn.apply(this, args);
}
};
}
// 用法
document.querySelector('#search')
.addEventListener('input', debounce(e => searchApi(e.target.value), 300));
window.addEventListener('scroll', throttle(onScroll, 200));记忆法:防抖像电梯门——有人进就重新计时;节流像技能 CD——冷却时间内按了也白按。
1.8 阶段小结
JavaScript 学到什么程度算「入门通关」?我的标准:
- 能徒手画出事件循环图,并解释任意一段代码的输出顺序。
- 能写对一个不借助库的 debounce,并且说得清为什么
this要透传。 - 知道
let/const与var的全部区别,代码里不再出现var。 - 看到共享引用引起的 bug,能在一分钟内定位到「是同一个对象」。
第二篇 TypeScript:给代码买一份编译期保险
2.1 为什么需要 TypeScript
先看一段纯 JavaScript 的日常事故:
function formatPrice(price) {
return '¥' + price.toFixed(2);
}
formatPrice('19.9'); // 运行时才炸:price.toFixed is not a function
这个错误要到用户点了那个按钮才暴露。TypeScript 做的事,就是把这类错误从「运行时炸给用户看」提前到「写代码时编辑器就红线警告」:
flowchart LR
A["你写的 TS 代码"] -->|"tsc 编译<br/>类型检查"| B{"有类型错误?"}
B -->|"是:当场拦住"| C["编辑器红线<br/>CI 直接失败"]
B -->|"否"| D["擦掉类型<br/>生成纯 JS"]
D --> E["浏览器运行"]
感悟:很多人对 TS 的第一印象是「麻烦」——要写类型、要应付编译器报错。但我的体会是:TS 不是让你多写代码,而是让你把脑子里本来就该想清楚的东西写下来。一个函数的参数到底是什么形状,本来就该想明白;TS 只是逼你别含糊。项目越大、接手的人越多,这份「写下来」的回报越惊人。
2.2 核心类型速览
// 基础类型
let count: number = 0;
let title: string = '前端之路';
let done: boolean = false;
// 数组与元组
let tags: string[] = ['vue', 'ts'];
let pair: [string, number] = ['vue', 3];
// 联合类型:要么是这个,要么是那个
type Status = 'pending' | 'success' | 'failed';
// interface:描述对象的形状
interface User {
id: number;
name: string;
email?: string; // 可选
readonly createdAt: Date; // 只读
}
// type:更灵活,可以做联合、交叉、映射
type Admin = User & { permissions: string[] };interface 和 type 的区别不用背,记一句经验法则:描述对象结构优先 interface(可被继承扩展、报错信息更友好),需要联合/交叉/工具类型时用 type。
2.3 泛型:类型的「函数参数」
不写泛型时,你只能二选一:要么 any 放弃检查,要么为每种类型复制一份代码。泛型让你把「类型」当参数传:
// 一个通用的 API 响应包装
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
interface User {
id: number;
name: string;
}
async function fetchUser(id: number): Promise<ApiResponse<User>> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
// 使用时,data 自动推断为 User,点 name 有提示,点 naem 会报错
const resp = await fetchUser(1);
console.log(resp.data.name);2.4 类型守卫:收窄的艺术
function handle(value: string | number) {
if (typeof value === 'string') {
value.toUpperCase(); // 这里 TS 知道 value 是 string
} else {
value.toFixed(2); // 这里 TS 知道 value 是 number
}
}自定义守卫在解析后端数据时特别有用:
function isUser(data: unknown): data is User {
return typeof data === 'object'
&& data !== null
&& 'id' in data
&& typeof (data as User).id === 'number';
}
const raw: unknown = await (await fetch('/api/user/1')).json();
if (isUser(raw)) {
console.log(raw.name); // 安全
}2.5 踩坑故事:那些年我们踩过的 TS 坑
踩坑故事 5:any 是会传染的
接手过一个项目,前人为了赶进度,在一个核心工具函数上写了
(data: any)。这个函数的返回值被到处使用,于是any像病毒一样扩散——凡是碰过它的变量全部失去类型检查。半年后统计,全项目 40% 的「类型报错被绕过」都能追到这一个 any。最后的修复花了两个人周,从它开始一层层把类型补回来。教训:
any不是「暂时不管」,是「把错误埋给未来」。实在搞不定第三方数据的类型,用unknown——它强迫你使用前先做守卫,安全得多。
踩坑故事 6:as 断言骗过了编译器,没骗过运行时
const user = JSON.parse(localStorage.getItem('user')!) as User; user.name.toUpperCase(); // 某天 localStorage 里是旧版数据,没有 name 字段,线上白屏
as的意思是「我比编译器更懂」,运行时零校验。localStorage、接口返回、URL 参数——凡是程序边界进来的数据,都不能信。该写守卫写守卫,别偷懒。
2.6 阶段小结
TS 入门标准:
- 项目里
any的出现次数趋近于 0,且每次出现都能说出理由。 - 能给项目的所有 API 请求和响应写出 interface。
- 会写
Partial<T>、Pick<T, K>、Record<K, V>这三个最常用的工具类型。 - 看到
unknown不慌,知道用守卫收窄。
第三篇 Vue 3:响应式与组件化的艺术
3.1 Vue 3 解决了什么问题
框架存在的意义,一句话:让视图自动跟随数据变化,你只管改数据。
flowchart LR
A["你修改数据<br/>state.count++"] --> B["Vue 响应式系统<br/>Proxy 拦截到变化"]
B --> C["找到依赖该数据的<br/>组件渲染函数"]
C --> D["重新渲染<br/>虚拟 DOM diff"]
D --> E["最小化更新真实 DOM"]
没有框架时你要写 document.querySelector('#count').textContent = count;有了 Vue,你写 count++,剩下的它管。心智负担从「操作 DOM」变成「操作数据」,这是前端开发史上最重要的一次抽象升级。
3.2 响应式原理:Proxy 魔法
Vue 2 用 Object.defineProperty,Vue 3 换成了 Proxy。这个改变解决了 Vue 2 的两个著名痛点:
flowchart TB
subgraph Vue2 的痛点
A1["新增属性不响应<br/>要 this.$set"]
A2["数组下标修改不响应<br/>要 splice"]
end
subgraph Vue3 Proxy
B1["拦截整个对象<br/>新增属性也能捕获"]
B2["数组天然支持"]
end
Vue2 的痛点 -->|"升级"| Vue3
简化版原理,三十行代码让你理解响应式:
// 极简响应式:不是生产实现,只为理解原理
let activeEffect = null;
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key); // 收集依赖:谁在用我
return Reflect.get(target, key);
},
set(target, key, value) {
const result = Reflect.set(target, key, value);
trigger(target, key); // 派发更新:通知用我的人
return result;
},
});
}
const state = reactive({ count: 0 });
effect(() => console.log(state.count)); // 建立依赖
state.count++; // 自动打印新值
理解了「get 时收集依赖、set 时派发更新」,Vue 3 的响应式在你眼里就没有魔法了。
3.3 第一个 Vue 3 应用:环境搭建
Vue 3 时代的标配工具链是 Vite(冷启动毫秒级,谁用谁知道):
npm create vite@latest my-app -- --template vue-ts
cd my-app
npm install
npm run dev项目结构速览:
flowchart TB
A["src/"] --> B["main.ts<br/>应用入口"]
A --> C["App.vue<br/>根组件"]
A --> D["components/<br/>业务组件"]
A --> E["stores/<br/>Pinia 状态"]
A --> F["router/<br/>路由"]
B -->|"createApp(App)<br/>.use(router)<br/>.use(pinia)<br/>.mount('#app')"| C
3.4 Composition API:setup 语法糖
这是 Vue 3 写法的核心。一个计数器组件:
<script setup lang="ts">
import { ref, computed } from 'vue';
const count = ref(0);
// computed:依赖 count,自动缓存,自动更新
const double = computed(() => count.value * 2);
function increment() {
count.value++; // 注意:script 里要 .value,template 里不用
}
</script>
<template>
<p>当前:{{ count }},翻倍:{{ double }}</p>
<button @click="increment">+1</button>
</template>ref vs reactive,新人必问的问题,我的答案:
- 统一用
ref,除非你明确知道自己在做什么。 ref包基本类型和对象都行,行为一致;reactive只能包对象,且解构会失去响应式(见下面的坑)。
3.5 踩坑故事:Vue 3 的那些坑
踩坑故事 7:解构 reactive,响应式当场消失
<script setup lang="ts"> import { reactive } from 'vue'; const state = reactive({ count: 0, name: '猫' }); const { count } = state; // 完蛋:count 只是个普通数字了 setTimeout(() => { state.count = 99; }, 1000); // 模板里的 {{ count }} 永远停在 0 </script>解法:要么不解构(
state.count),要么用toRefs(state)。这个坑的原理很直白:解构出来的是那一刻的值,跟 Proxy 再没有关系了。想通这一点,你对响应式的理解就及格了。
踩坑故事 8:watch 监听对象,怎么不触发?
const user = ref({ name: '猫', age: 3 }); watch(user, (val) => console.log('变了')); // user.value.age = 4 时不触发!
watch默认只盯引用的变化(整个对象被替换),user.value.age = 4改的是内部属性,引用没变。要么watch(user, cb, { deep: true }),要么更推荐——监听 getter,精确指定你关心什么:watch(() => user.value.age, (val) => console.log('年龄变了', val));
deep: true会递归遍历整个对象,大对象上性能开销不小。我见过一个列表页卡成 PPT,就是有人给万行数据加了 deep watch。
踩坑故事 9:v-for 用 index 当 key,列表一排序就乱
<li v-for="(item, index) in list" :key="index">{{ item.name }}</li>用 index 当 key,列表中间插入或删除一项时,Vue 的 diff 会复用错误的 DOM 和组件状态。真实的症状是:删除第二行,结果第三行的勾选状态「跑」到了第二行上。key 必须用数据里唯一且稳定的标识,通常是
item.id。没有 id?那就造一个,别用 index。
3.6 组件通信:一张图说清所有方式
flowchart TB
P["父组件"] -->|"props 单向传下去"| C["子组件"]
C -->|"emit 事件冒上去"| P
S["兄弟组件 A"] -->|"共享状态"| Store["Pinia Store"]
Store --> S2["兄弟组件 B"]
G["祖组件"] -->|"provide"| GG["隔代孙组件"]
铁律只有一条:props 是单向的,子组件不许直接改。
<!-- 子组件:定义 props 和 emits -->
<script setup lang="ts">
const props = defineProps<{ title: string; count: number }>();
const emit = defineEmits<{ (e: 'update', value: number): void }>();
function onClick() {
// props.count++; // 错误!单向数据流被破坏
emit('update', props.count + 1); // 正确:通知父组件去改
}
</script>踩坑故事 10:子组件直接改 props,两个人的状态打架
一个弹窗子组件收到父组件传来的
visible,关闭按钮直接写props.visible = false。表面上能用(基本类型甚至会报错,对象类型能偷偷改成功),但父组件的状态没跟着变,于是出现「弹窗关了,父组件还以为它开着,下次点不开」的灵异 bug。Vue 官方把这叫做单向数据流,我更愿意叫它「状态的唯一权威来源」:每个状态只能有一个主人,其他人想要变化,只能向主人申请。这条原则在 Pinia、React、Redux 里是通用的,值得刻进肌肉记忆。
3.7 生命周期:什么时候干什么事
flowchart LR
A["setup()<br/>最早的时机"] --> B["onBeforeMount"]
B --> C["onMounted<br/>DOM 已就绪<br/>发请求 操作 DOM"]
C --> D["onBeforeUpdate"]
D --> E["onUpdated"]
E -.->|"数据再变"| D
C --> F["onBeforeUnmount"]
F --> G["onUnmounted<br/>清定时器 解绑事件"]
最常见的需求对应关系:
- 发接口请求:
onMounted(或 setup 里直接发,视 SSR 需求而定)。 - 注册定时器/事件监听:
onMounted里注册,onUnmounted里必须清理,否则组件反复挂载会内存泄漏。
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
let timer: number;
onMounted(() => {
timer = window.setInterval(fetchData, 5000);
});
onUnmounted(() => {
clearInterval(timer); // 不清这个,路由来回切几次就有 N 个定时器在跑
});
</script>3.8 Demo:一个完整的 TodoList
把上面的知识全部串起来,这是一个可以直接跑的真实组件:
<script setup lang="ts">
import { ref, computed } from 'vue';
interface Todo {
id: number;
text: string;
done: boolean;
}
const todos = ref<Todo[]>([]);
const newText = ref('');
let nextId = 1;
const remaining = computed(() => todos.value.filter(t => !t.done).length);
function addTodo() {
const text = newText.value.trim();
if (!text) return;
todos.value.push({ id: nextId++, text, done: false });
newText.value = '';
}
function removeTodo(id: number) {
todos.value = todos.value.filter(t => t.id !== id);
}
</script>
<template>
<div class="todo">
<h2>待办(还剩 {{ remaining }} 件)</h2>
<input
v-model="newText"
placeholder="回车添加"
@keyup.enter="addTodo"
/>
<ul>
<!-- 注意:key 用 item.id,不是 index -->
<li v-for="item in todos" :key="item.id">
<label :class="{ done: item.done }">
<input type="checkbox" v-model="item.done" />
{{ item.text }}
</label>
<button @click="removeTodo(item.id)">删除</button>
</li>
</ul>
</div>
</template>
<style scoped>
.done { text-decoration: line-through; color: #999; }
</style>这个不到 50 行的组件里,藏着这些考点:ref + 泛型、计算属性、v-model 双向绑定、事件修饰符(@keyup.enter)、列表渲染的 key、scoped 样式。面试时让你手写一个,能 10 分钟写出来且 key 用对,Vue 基础就算过关。
3.9 Pinia:跨组件状态的正确姿势
组件多了以后,「A 改了状态 B 也要知道」的需求越来越多,props 一层层传会疯掉。Pinia 是 Vue 3 官方推荐的状态管理:
// stores/user.ts
import { defineStore } from 'pinia';
interface UserState {
name: string;
token: string | null;
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
name: '',
token: null,
}),
getters: {
isLoggedIn: (state) => state.token !== null,
},
actions: {
async login(name: string, password: string) {
const res = await loginApi(name, password);
this.name = res.name;
this.token = res.token;
},
logout() {
this.name = '';
this.token = null;
},
},
});组件里使用:
<script setup lang="ts">
import { useUserStore } from '@/stores/user';
import { storeToRefs } from 'pinia';
const userStore = useUserStore();
const { name, isLoggedIn } = storeToRefs(userStore); // 保持响应式
// const { name } = userStore; // 又踩坑 7:解构丢响应式!
</script>注意 storeToRefs——它和 toRefs 解决的是同一个问题,所以我说坑 7 是 Vue 3 第一大坑,它会换着花样反复出现。
3.10 Vue Router:单页应用的路由
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: () => import('@/views/Home.vue') },
{ path: '/user/:id', component: () => import('@/views/User.vue') },
{ path: '/admin', component: () => import('@/views/Admin.vue'),
meta: { requiresAuth: true } },
],
});
// 路由守卫:登录校验的标准位置
router.beforeEach((to) => {
const userStore = useUserStore();
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
return { path: '/login', query: { redirect: to.fullPath } };
}
});
export default router;注意路由组件用 () => import(...) 的懒加载写法——首屏只加载首页代码,其余按需加载,这是性能优化的免费午餐。
第四篇 工程化与心智成长
4.1 一个项目从代码到上线的完整链路
flowchart LR
A["本地开发<br/>vite dev 热更新"] --> B["提交代码<br/>ESLint + 类型检查"]
B --> C["构建<br/>vite build<br/>压缩 分包 指纹"]
C --> D["部署<br/>静态资源上 CDN / Nginx"]
D --> E["线上监控<br/>错误上报 性能指标"]
E -.->|"发现 bug"| A
4.2 从入门到精通的真实分水岭
我带过不少新人,观察下来,「入门」到「精通」之间隔着的从来不是更多 API,而是这几件事:
- 能从现象推回原理。弹窗数据不同步,能想到共享引用;列表状态错乱,能想到 key;数据改了视图没变,能想到响应式丢失。原理是那把万能钥匙。
- 对异步有敬畏心。接口时序、竞态、取消请求,这些才是真实项目里最贵的 bug 来源。
- 状态有主人。任何一份数据,都能一句话说清「谁拥有它、谁能改它」。
- 敢看源码。从
structuredClone到 Vue 的track/trigger,你会发现那些「魔法」拆开看都朴素得很。
4.3 一些真心话
关于学习顺序:直接学 Vue 能不能干活?能。但你会在第一次遇到 this 丢失、响应式消失、异步时序问题时卡住,因为你缺的是底下的地基。JavaScript → TypeScript → Vue 3 这条路看着绕,其实是最短的。
关于框架之争:React、Vue、Angular 吵了很多年,但响应式、组件化、单向数据流这些核心思想是相通的。把一门学透,换另一门是周级别的事;门门都浅尝,换哪门都是从头开始。
关于 AI 时代:现在让 AI 写一个 TodoList 只要十秒钟。但上面那些坑——共享引用、解构丢响应式、await forEach——AI 生成的代码照样会犯,而能看出来它错了的人,才能驾驭它。越是工具强大,基本功越不是可有可无,而是你最硬的议价能力。
结语
这条路线走下来,你收获的不仅是「会写 Vue」,而是一套完整的思维方式:用 JavaScript 理解运行,用 TypeScript 管理复杂度,用 Vue 组织界面,用工程化把这一切交付给真实用户。
入门的标志是你能写出功能;精通的标志,是你能在深夜的故障群里,三分钟说出一句:「我知道问题出在哪。」
祝你也早日成为那样的人。