Madeira 这个名字,很多前端同学可能是在翻 Vue 源码或听尤雨溪分享时偶然撞见的。我第一次注意到它,是在一份很古早的 Vue 原型资料里。简单说,Vue 在正式对外公开之前,有过一段早期的实验原型阶段,那个阶段的项目代号就是 Madeira,后来它迭代演化,才成了我们今天熟知的 Vue 框架。
很多人第一反应是:这跟马德拉葡萄酒有什么关系?其实名字的来源并没有特别确切的官方解释,我更愿意把它看成一种“陈酿”的隐喻。原型阶段就是框架的陈酿期,很多设计思路在里面慢慢发酵。这篇文章想跟你聊的,就是这瓶“Madeira”里到底装了什么:它为什么会出现、核心设计是怎么长出来的、如何用 Vue 0.10 复现一个那个年代的页面、以及我在复现过程中踩过的坑。适合对框架演进感兴趣、想深入理解 Vue 原理的前端开发者。
1. “Madeira”到底是什么:Vue 出生前的那个原型
1.1 我第一次在 Vue 仓库里看到 Madeira
Vue.js 最早的公开版本是 0.10.0,发布时间是 2014 年 2 月,当时的版本代号叫 Asmodeus。但在那之前,尤雨溪还在 Google 工作的时候,就已经利用业余时间写了一个实验性的前端项目,后来的社区资料里经常把这段原型期称为 Madeira。它不像 Vue 2、Vue 3 那样有完整文档和技术委员会,它就只是一个“用着顺手”的私人玩具,慢慢长成了后来的 Vue。
我最早看到 Madeira 这个称呼,是在看尤雨溪早年演讲和仓库 commit 记录时顺藤摸瓜找到的。说句实话,当时我以为是某个新框架,搜了半天才发现它是 Vue 的前身。那会儿前端正在经历从“操作 DOM”到“数据驱动视图”的切换期,看到一个原型能孵化出这么成熟的框架,还是挺震撼的。
如果你现在去翻 npm 上 vue 的版本列表,你会发现一大堆像 0.10.0、0.11.0、0.12.0 这样的老版本。它们对应的就是 Vue 从原型过渡到稳定框架那段时间。这段历史的价值在于:它把一个“个人实验项目”如何变成“行业级框架”的完整路径摆在了我们面前。对任何一个想自己做开源项目的人来说,这都是很好的参考样本。
1.2 为什么那时候需要一个新的视图层框架
2013 年前后的前端生态,跟今天差别很大。jQuery 还是主流,AngularJS 刚火起来,Backbone 和 Knockout 也各有拥趸。AngularJS 的双向绑定和指令系统让人眼前一亮,但它也有很明显的沉重感:作用域继承、依赖注入、模块系统,这些概念要理解透就得花不少时间。Backbone 虽然轻,但它把 View 和 Model 的同步工作交给你手动处理,代码写多了很容易变成一堆零散的监听事件。Knockout 使用起来直观,模板写起来却不太像 HTML,对很多后端转前端的开发者来说接受成本并不低。
尤雨溪在公开分享里多次提到,他很喜欢 Angular 里“数据变了,视图自动更新”的思路,但又不想要那些重型架构约束。他想要一个“中等重量级”的选择:既能保持声明式模板的直觉,又不必让开发者一开始就被框架套牢。所以 Madeira 原型从一开始就很刻意地做减法,核心只做“数据绑定”和“视图更新”,至于路由、状态管理、构建工具,都不在早期原型范围内。
这个决策影响非常深远。今天你拿起 Vue 3,看到的核心依然是一套“响应式系统”,至于 Vue Router、Pinia,全都是你可以自由选择安装的扩展。这种“必要部分做深,非必要部分放开”的思路,早在 Madeira 阶段就已经定下来了。
2. 从 Madeira 到 Vue:核心设计是怎样一步步长出来的
2.1 数据响应式:数据一改,页面自动跟着变
Vue 最让新手感到“魔法”的部分,是数据响应式。你在 data 里声明一个message,在模板里写{{ message }},之后只要修改this.message,页面上的文字就会自动变。这个效果不是浏览器原生能力,而是 Vue 自己实现的。
Vue 2.x 的实现方式,是把 data 对象里的每个属性都用Object.defineProperty改写成 getter/setter。组件实例初始化的时候,遍历 data 的 key,给每个 key 挂上依赖收集器,叫 Dep。模板里用到哪些数据,就会触发哪些 getter,数据对应的 Watcher 会被收集进 Dep。等到 setter 被触发,也就是代码里确实改了数据,Dep 就会通知相关 Watcher 重新求值,再更新 DOM。
你可以用 Excel 来理解这个过程。假设 A 列和 B 列是输入值,C 列写了一个公式=A1+B1,那么 A1 或 B1 一变,C1 自动重算。Vue 的响应式逻辑类似,只是它收集的不是公式,而是“依赖”——模板里哪些地方读了这个数据,就把这些地方统统订阅起来。数据一变,与其相关的视图片段一起更新,其他没依赖的数据则碰都不碰。
Madeira 原型阶段很早就采用了这套基于 getter/setter 的思路。后来 Vue 3 换成 Proxy,解决了两大痛点:一是新增和删除属性也能被拦截,二是省去了对嵌套对象逐层递归 defineProperty 的麻烦。但底层“依赖收集 + 通知更新”这套核心模型,从 Madeira 时代一直延续到今天。
2.2 模板与指令:为什么大家可以开箱即用
Vue 另一个非常聪明的设计,是直接用 HTML 写模板。模板里可以写{{ message }}做插值,可以用v-if、v-for做条件渲染和列表渲染,也可以写v-on:click绑定事件。对绝大多数开发者来说,模板就是 HTML 加一点“魔法标签”,几乎不需要学习成本。这套思路在 Madeira 时期就已经成型了,它不像后来 React 的 JSX 那样把模板能力直接揉进 JavaScript 里,而是保持了“模板就是模板”的直观。
v-model是另一个很有代表性的设计。它本质上是“给输入框绑定 value + 监听 input 事件”的语法糖,但使用者不需要关心底层细节,只需要写一个v-model="newItem"就能实现输入框和数据的双向同步。在 Vue 0.10 的时代,指令系统还比较朴素,但核心方向已经很明确了:把最常用的交互模式封装成语义清晰的指令,让开发者把精力放在业务逻辑上。
模板编译的过程也不复杂。Vue 会把模板字符串解析成 AST,再根据 AST 生成 render 函数,之后每次数据更新只需要执行一次 render 函数来重新生成视图描述。Vue 0.10 时代还没有虚拟 DOM,数据更新是直接操作真实 DOM 的。虽然性能上不如后来引入虚拟 DOM 的 Vue 2,但在当时的页面复杂度下已经够用。我在复现老版本 demo 时发现,这个“接近手写 HTML”的开发体验依然很舒服。
2.3 组件化:把页面拆成可以拼装的积木
单文件组件 SFC 是 Vue 2 时代才广泛普及的,但组件化思想在 Vue 1.0 之前就已经非常成熟了。Vue 0.10 里已经支持通过Vue.extend和Vue.component注册全局组件,也支持父组件给子组件传 props、子组件给父组件发事件。用今天的话说,这就是一个非常标准的“单向数据流 + 事件反馈”模型。
组件化的价值在于:页面复杂度一旦上来,把所有 HTML、CSS、JavaScript 混在一起写,几乎没法维护。拆分组件之后,每个组件是一个独立的“零件”,有自己的模板、数据和行为。团队协作时,不同成员可以负责不同组件,互相之间只需要约定好 props 和事件接口,就能并行开发。
从 Madeira 原型到今天,组件化的发展轨迹其实很清晰:最早是一个组件对应一个模板片段,后来是把模板、样式、脚本封装进同一个.vue文件,再后来是组合式 API 把组件内的逻辑也做了模块化拆分。你会发现,每代版本都在解决同一个问题的某个侧面:如何让组件的封装更彻底、组合更灵活、心智模型更简单。这也是为什么 Vue 被称为“渐进式框架”的原因之一,它不会逼你一次学完全部概念,你可以只用模板插值,也可以逐步深入组件、路由、状态管理。
3. 手脚并用来复现:在本地跑一个 Madeira 时代的 Vue
3.1 工具准备:直接引入 CDN 里的 Vue 0.10
要“穿越”回 Madeira 时代的开发体验,最省事的方式不是用 npm 搭工程,而是直接写一个 HTML 文件,再用 script 标签引入旧版本 Vue。之所以不推荐一上来就上构建工具,是因为 Vue 0.10 时代没有对应的脚手架,后端依赖链也很老旧,硬上 webpack 很容易报各种版本兼容错误。我试过在较新的 Node 环境里跑 vue@0.10.0 的旧构建流程,光是处理过时的依赖就花了大半天,最后还是用 CDN 方案最快。
你可以在浏览器打开任意在线 CDN 页面,搜索vue@0.10.0的 dist 文件地址,然后把脚本地址直接写在 HTML 里。我习惯用一个本地静态目录,配一个轻量 HTTP 服务,比如在项目目录下运行python3 -m http.server 8000,然后访问localhost:8000。如果你不喜欢命令行,直接用浏览器双击打开 index.html 通常也可以,只要页面里没有跨域请求。
使用固定版本的 CDN 引用还有一个好处:它不会跟着主版本更新而改变行为。这样你复现的结果和文章里提到的表现是一致的,不会被最新的 Vue 版本“偷偷”纠正掉。官方文档里老版本信息已经不怎么维护了,查资料时要注意甄别。对只想体验老版本,而不是做生产项目的人来说,CDN 固定版本是最省心的方案。
3.2 完整可复现的 demo:老版本也能写留言板
下面这个 demo 是我实际跑通过的,一个非常简单的留言板。它用到了文本插值、v-model双向绑定、v-for列表渲染、v-on事件绑定,几乎涵盖了 Vue 0.10 最核心的模板能力。我把完整代码放在这里,你可以直接复制成一个 HTML 文件,在浏览器里打开就能看到效果。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>Madeira 时代复现</title> <script src="https://cdn.jsdelivr.net/npm/vue@0.10.0/dist/vue.js"></script> </head> <body> <div id="app"> <h3>{{ title }}</h3> <input v-model="newItem" placeholder="输入一条留言"> <button v-on:click="addItem">添加</button> <ul> <li v-for="item in list"> {{ item }} <a v-on:click="removeItem($index)">删除</a> </li> </ul> <p>当前共有 <strong>{{ list.length }}</strong> 条留言</p> </div> <script> new Vue({ el: '#app', data: { title: '欢迎回到 Madeira 时代', newItem: '', list: ['第一条留言', '第二条留言'] }, methods: { addItem: function () { if (!this.newItem.trim()) return; this.list.push(this.newItem); this.newItem = ''; }, removeItem: function (index) { this.list.splice(index, 1); } } }); </script> </body> </html>在列表渲染时,老版本里取索引要写$index,这一点跟 Vue 2 差异很大。Vue 2 里你更常写的是(item, index) in list,但 Vue 0.10 中$index是现成的内置变量。如果你的代码里写成了 Vue 2 的语法,会直接报编译错误,这个需要在复现时特别注意。
我在浏览器里跑这段代码的时候,最直观的感受是“它真的很像 Vue 1.x”。模板语法、数据操作方式、甚至报错风格,都能看出与老 Angular 的血缘关系。虽然这套代码在今天的生产项目里不够健壮,但作为一条理解 Vue 演进的时间线,价值非常高。
看完了官方 Vue 的用法,我们下一步直接拆到底层:自己动手写一个迷你响应式系统。
3.3 四十几行代码造一个迷你响应式系统
你自己动手写一遍,会比看十篇原理文章都管用。下面这个极简实现,核心就是“遍历数据对象,给每个属性设置 getter/setter;读取属性时收集回调,赋值时触发回调”。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>迷你响应式系统</title> </head> <body> <p id="app">加载中...</p> <input id="input" placeholder="输入内容试试"> <script> function observe(data, onChange) { if (!data || typeof data !== 'object') return; Object.keys(data).forEach(function (key) { var value = data[key]; observe(value, onChange); Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { return value; }, set: function (newValue) { if (newValue === value) return; value = newValue; onChange(); } }); }); } var vm = { data: { text: 'hello madeira' } }; var view = document.querySelector('#app'); function render() { view.textContent = vm.data.text; } observe(vm.data, render); render(); var input = document.querySelector('#input'); input.addEventListener('input', function (e) { vm.data.text = e.target.value; }); </script> </body> </html>这个迷你系统的问题很明显:它只会“全量重渲染”,不会精确区分哪些视图片段依赖哪个数据。真正的 Vue 里有 Dep 类做依赖收集,有 Watcher 实例做订阅者,有调度器做批量更新,Vue 2 之后还有虚拟 DOM 来减少真实 DOM 操作的次数。但核心骨架就藏在这四十几行代码里。
我比较建议你把这个 demo 实际跑一遍,然后把onChange回调换成 Vue 自己的 Watcher 思维:回调不再是无脑更新整个页面,而是把“更新的具体位置”记录成精确指令。理解了这一点,你再看 Vue 源码里那些复杂术语,就不会觉得一头雾水了。
4. 复古接线现场:版本差异、构建方式、排错经验
4.1 Vue 0.10 / 1.0 / 2.x 差异对照:查文档别被坑
老版本时代,没有今天这么丰富的文档生态,很多 API 也是半成品。我整理了一份对照表,方便你查旧文档时快速定位差异。
| 维度 | Vue 0.10(Madeira 后期) | Vue 1.0 | Vue 2.x |
|---|---|---|---|
| 渲染层 | 直接操作 DOM | 直接操作 DOM | 虚拟 DOM |
| 列表索引 | $index | $index | (item, index) in list |
| 事件简写 | v-on:click | v-on/@ | v-on/@ |
| 生命周期 | init、ready等 | created、ready等 | created、mounted等 |
| 内置过滤器 | filterBy、orderBy等常用 | 内置过滤器 | 基本移除,改为方法 |
| 模块化 | 基本是全局脚本 | 支持 CommonJS | 完整 ESM 生态 |
最坑的是过滤器。filterBy和orderBy在 Vue 1.0 时期还是官方内置能力,到了 Vue 2.0 就被删掉了。如果你翻老教程,看到v-for="item in list | filterBy query"这种写法,千万别以为 Vue 2 里还能用。现在如果要实现类似效果,要么用 computed 属性提前过滤,要么在组件里封装一个过滤方法。这不是小事,我在复现时踩过这个坑,报错信息还不直观,排查了半天才发现是过滤器语法的问题。
生命周期也是一个容易混淆的点。ready在 0.10 和 1.0 里都表示“DOM 挂载完成后”,到 Vue 2 里就改成了mounted。如果你写惯了新版本,回到老版本第一件事就是把这些生命周期改回来,否则页面会静默失败,连报错都没有。
4.2 别急着上构建工具:老项目用 script 标签最省心
现在开发前端,默认就是 npm install、webpack/vite、ESM。但在复现 Vue 0.10 这种老古董时,这套流程反而是负担。老包依赖的构建工具、Node 版本和今天差异很大,硬跑很容易出错。我自己曾经试图把 Vue 0.10 的源码 clone 下来,在本地执行npm install,结果光是旧版 Babel 和 UglifyJS 的生态依赖就互相怼了半天,最终放弃了。
对老版本项目,直接用 script 标签引入是最稳妥的。把 vue.js 下载到本地,或者在 HTML 里引用固定版本的 CDN 地址,然后像写原生页面一样写业务代码。这样不依赖构建工具,也不需要额外配置,报错链路短,排查起来非常方便。
当然,这只是在“复现历史”场景下的建议。如果你要在新项目里用 Vue,该上构建工具还是得上。我的经验是:先分清目标是“学习历史”还是“生产开发”,两种场景的技术选型完全不同。对着老框架硬套新工具链,等于用最新的挖掘机去挖一个手铲就能搞定的小坑,纯属自己给自己找麻烦。
4.3 调试技巧:报错信息是判断版本最快的线索
老版本 Vue 的报错风格跟 Vue 2、Vue 3 差别很大。Vue 2 常见的是 “TypeError: Cannot read property 'xxx' of undefined”,Vue 3 则可能在 warn 里给出详细的组件链路。而 Vue 0.10 时代,遇到模板错误多半是直接抛出编译异常或者干脆没有任何输出的“静默失败”。这也是为什么我建议用最简 HTML 文件开始:你把问题范围缩小到最小环境,基本上分分钟就能定位。
调试的时候,别急着开 DevTools 的 Vue 插件。新版 Vue DevTools 很可能识别不了 Vue 0.10 的实例。我试过安装多版本插件,最后发现控制台手动操作反而更直观。你可以在 Vue 实例创建时给它一个全局引用,比如window.app = new Vue(...),然后直接在控制台里读app.$data、调用app.addItem(),观察数据是否变化。这比依赖插件可视化更像是“在用框架”。
如果你需要跟踪某个属性的变更,也可以在defineProperty的 setter 里临时加一行console.log(newValue),再刷新页面。这种“埋点调试法”虽然不够优雅,但在复古项目里非常管用,能很快确认某个绑定到底有没有被触发。
4.4 容易掉进去的思维误区:别带着 Vue 2 心智看老代码
很多 Vue 2 时代入行、后来用 Vue 3 的开发者,再回头去看 Vue 0.10 时,脑子里会默认“它应该也是虚拟 DOM 加组件化”。但实际上 0.10 时代连虚拟 DOM 的影子都没有。它确实有组件化,但组件之间的通信、事件修饰符、动态组件等等,都比 Vue 2 要简陋得多。你要是用 Vue 2 的写法硬套,大概率是改完一个错又来一个错。
另一个容易被忽略的误区,是scoped style。今天你在 SFC 里写<style scoped>已经习以为常,但那个的时代根本不存在这个能力。Vue 0.10 里样式是全局的,所谓“组件内样式隔离”,是后来 Vue 2 + vue-loader 才引入的。就拿上面留言板 demo 来说,如果我用新项目的思维去给它加 scoped 样式,首先就会发现旧版本根本没有单文件组件的解析逻辑。
所以我的建议是:复现老项目时,先把 Vue 2 时代的 API 从脑子里清空,只保留“数据驱动视图”这一个核心概念,其他的语法细节按着对应版本文档来写。遇到差异,先去查旧版本 release notes,别用新版本的思维硬猜。这样才能真正体会到框架演进的意义,而不是困在“怎么到处都不兼容”的烦躁里。
5. Madeira 的遗产:它如何影响了一整代前端开发
5.1 MVVM 的分层思想:ViewModel 从哪儿来
Vue 早期被归入 MVVM 架构,虽然现在这个说法不那么流行了,但 MVVM 的骨架依然贯穿 Vue 的设计。MVVM 把界面分成三层:Model 负责数据,View 负责可视化,ViewModel 是连接两者的桥梁。ViewModel 做的事情,就是把 Model 的数据转成 View 能直接消费的状态,同时把 View 上的用户操作转回 Model 的变更。
对应到 Vue 里,Vue 实例本身就是一个很大的 ViewModel。它替你管理数据监听、模板渲染、事件绑定,你写的代码更多是告诉它“数据长什么样”“模板长什么样”,而不用关心怎么操作 DOM。这个分层的好处是:业务逻辑可以独立于 UI 被测试,UI 的改动也不会污染数据模型。
Madeira 原型的价值在于,它是这套 MVVM 思想在前端落地的一次“极简实验”。尤雨溪在那个原型里验证了一个关键问题:不借助重型框架和复杂架构,能不能用足够轻量的代码,把 ViewModel 这层做得既好用又好懂。答案是肯定的,这也解释了为什么后来的 Vue 能始终保持较陡的学习曲线,让新手几天就能上手。
5.2 渐进式思想在原型里就已经有了雏形
很多框架都追求“全能”:我帮你处理路由、帮你管理状态、帮你做依赖注入。Vue 走的是“渐进式增强”的路线。你可以在一个老旧的 jQuery 页面里引入 Vue,只用来渲染一个区块;也可以全套使用 Vue 生态,构建一个大型单页应用。这种自由度,在早期 Vue 里就显得非常明显。
在 Madeira 原型阶段,尤雨溪就只聚焦于“视图渲染”这一件事。没有路由,没有状态管理,没有复杂的模块系统,你把它拿过来,它也不会抢占你现有的架构决策权。它像一个很轻的“增强插件”,你想用就用,不想用也不影响。这个基因词就是“渐进式”,后来 Vue 的官方口号 “The Progressive JavaScript Framework”,从这个原型时期就已经有了雏形。
这个思想在今天依旧值得借鉴。技术选型时,我们常面临“这个框架到底要绑定多少东西”的纠结。如果一个框架从第一天开始就要求你把整套方案都装上,那它在某些存量项目里很难被采用。反而是在边界上放得足够轻、你想加组件化就加组件化、想加路由就加路由,这种渐进式路线能更好地融入真实业务。
5.3 对今天选型与个人成长的几点启发
从 Madeira 到 Vue 3,我可以提炼出三条非常受益的经验。
第一是“小实验也能撬动大行业”。尤雨溪最初做 Vue,只是出于个人对 Angular 的不满和对轻量化方案的追求,他大概没想过它会成为全球数百万开发者使用的框架。但正因为保持了小体量、低耦合,它才有机会被不同项目反复验证,最终反超很多重框架。对个人开发者来说,这提醒我们不用非得等到“完美方案”才能开始。先把一个痛点解决到底,哪怕代码丑一点,只要方向对,后续可以慢慢迭代。
第二是“技术选型要看长远”。Vue 能走到今天,一个重要原因是它的核心 API 变化相对平滑。虽然 Vue 3 引入了组合式 API,但模板语法、响应式原理、组件通信模型仍然一脉相承。相比之下,那些频繁推翻自己的框架,对用户来说迁移成本太高,很难培养起生态。选型时,除了看它现在多流行,更要看它的核心设计是不是经得住时间检验。
第三是“敢于理解底层”。在这个框架满天飞的时代,会配置 webpack 的人很多,真正理解响应式原理的人却没有想象中那么多。我自己写迷你响应式系统时,最有价值的收获不是这份代码本身,而是以后再遇到 Vue 的诡异 bug,我不会再一头雾水,而是能顺着“依赖收集、更新策略、触发时机”这条线去推敲。你不需要把每个源码细节背下来,但核心知识链条一定要亲手走一遍。
最后再分享一个小技巧:你可以在本地保留几个不同版本(0.10、1.0、2.x、3.x)的 Vue 文件,随时用一个几分钟就能跑起来的页面做对比实验。这种“并排考古”的方式,能非常直观地感受到框架在不同阶段的取舍。我也是在对比中才真正明白,为什么 Vue 2 要引入虚拟 DOM,为什么 Vue 3 要把 defineProperty 换成 Proxy——因为老方案的边界问题清晰可见,设计的新方案也就能更有说服力。
我个人在实际操作中的体会是:复现老框架,最大的意义不是“怀旧”,而是从那段略显粗糙的代码里看到框架设计师当时的思考轨迹。你会明白,任何被今天视为理所当然的设计,背后都有一连串现实的约束和权衡。理解这些权衡,比背下一个 API 名字要重要得多。