
尤雨溪(Evan You)主创的前端框架 Vue.js 首发于 2014 年,距今已经 7 年过去了。我作为一个曾经的前端工程师,虽然代码写得不怎样,但经常还会折腾点小工具什么的。去年我用上 TypeScript 之后感觉给自己的小工具找到了“可维护”之路,于是写了《TypeScript + ExpressJS 快速搭建小工具服务》作为记录,今年我又学了下 Vue.js,从 Vue 2.x 开始试着上手,因为 2.x 流行多年,相关生态和第三方库最全。
但使用 Vue 2.x 的我配合的是臃肿的 webpack,虽然工具链成熟但开发过程总觉得笨重且慢。近来我改用 Vue 3 配合官方的 Vite.js,感受到了轻量级自动化工具带来的闪电般的开发体验,于是把我的播客枫言枫语官网的前端搬到 Vue 3 上来,效果还不错,遂写此文以记之。
一、选择 Vue 3 和 Vite.js
枫言枫语官网非常简单,只有一个 Episodes 列表和一个 About 页面,很适合用来练手新技术。早期我用的是简单的 Express.js 做后端,Pug(Jade)做 HTML 前端引擎,用户访问官网时直接吐一个后端渲染好的 HTML 静态页面。
我练习 Vue 2 的项目还用上了 BootstrapVue 这个库,基本上把所有的 Bootstrap 官方控件都包装成了 Vue Component,引入后直接用就行,非常方便。用上 Vue 3 之后我就自己 import bootstrap 自己写控件逻辑了,好在页面简单这点不成问题。对于这个项目来说,Vue 3 和 2 的区别并未造成大的影响,如果你的项目规模较大可以参考官方迁移文档。
在 Vue 2 的项目中,我需要用 webpack 实现:
- ts → js
- scss → css
- html 自动打包
- dev server 实现 Hot reload
因为 webpack 的设计目标具有普适性,所以为了实现我这种比较特别的开发环境,我的 webpack 配置文件会比较庞大。如果 dev server 的逻辑涉及在前端用到的数据结构,我还得把 webpack.config.js 也用 ts 来写,才能引入相关的数据类型。
Vite.js 的出现直接解决所有问题,使用 npm init vite@latest 直接构建脚手架:
➜ npm init vite@latest
Need to install the following packages:
create-vite@latest
Ok to proceed? (y)
✔ Project name: … vite-project
✔ Select a framework: › vue
✔ Select a variant: › vue-ts
如此就完成了使用 ts 开发 Vue 3 项目的脚手架,assets 目录里也支持 scss,非常方便。
Vite 项目的开发预览直接跑 vite 命令即可,在去年的文章中我还是比较多用 Makefile,但现在我更喜欢用 package.json 里的 scripts 了,因为可以直接用 local node modules,无需 npx 前缀。
npm run dev
Dev server 就跑起来了,Vue 文件的任何修改几乎都是秒刷新,非常赞。目前我唯一觉得慢的就是修改 scss 文件,要等十多秒的编译时间,不过 CSS 的修改我可以直接在浏览器里做,然后再复制过来,所以影响不大。
二、引入 Bootstrap 和 Fontwaesome
Bootstrap 和 Fontawesome 都可以通过 npm 安装。
Bootstrap 的 scss 文件在 style.scss 里通过 @import 引入,Fontawesome 在 main.ts 通过 import 引入。
三、Vue Router History Mode
引入 Vue Router 如果使用 History Mode 需要 Server 端配合。比如我们配置两个路由:
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
}
]
当我们使用 <router-link>直接跳进 /about 页面时, Vue Router 会使用History API 实现页面跳转和浏览器 URL 的修改,并不会真的发起一次 GET /about 的请求。
但是如果用户直接访问了 /about 这个 URL,我们希望他能直接见到 about 页面的 UI,所以我们需要在服务端做一次 fallback,当一个请求不 match 我们 server 端的路由时直接返回 index.html,这样 Vue Router 会自行处理页面渲染。
服务端我用的还是 express.js 所以直接在 Router 配置完之后,末尾加了一个 app.all("*", () => {}) 的处理,简单粗暴。
app.use('/episode', episodeRouter)
app.all("*", (_req, res) => {
try {
res.sendFile(path.join(g_publicFolder, 'index.html'));
} catch (error) {
res.json({ success: false, message: "Something went wrong" });
}
});
四、Vite Dev Server 转发请求到 API 后端
Vite 默认用 `http://127.0.0.1:3000` 来 serve Vue 的前端页面,我们对前端文件的任何修改都会自动触发 Vite 的自动编译然后 reload。但是如果我们在前端需要使用 JS 发起 API 请求到后端呢?
一般如果用 axios 来发起一个 GET 请求我们这样写:
const response = await axios.get(`/episode/all`)
如果在浏览器中跑起来这个请求就会发到 Vite server 而不是真正的后端。所以我们还需要在 vite.config.ts 中配置 Dev Server Proxy:
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
input: resolve(__dirname, 'index.html')
}
},
server: {
host: '0.0.0.0', // 把服务暴露给内网其他设备,比如同个WiFi下的手机
proxy: {
'^/episode/.*': 'http://127.0.0.1:3001', // 所有 episode 的请求都会被转发到 3001 端口,我们在这个端口启动一个后端的 server 就可以实现前后端联调了
}
}
})
五、解决 ts 的 this.property does not exist 编译错误
在 Vue 2 里使用 js 的单独的 vue 文件大约是这样的:

但是 Vue 3 里使用 ts 的话需要加上 defineComponent() 不然 this 的类型推断就会出错,比如:
export default defineComponent({
data() {
return {
foo: 0,
}
},
methods: {
bar() {
console.log(this.foo); // 如果没有 `defineComponent()` 这里会报错
}
}
})
六、总结
目前用着感觉 Vue 3 + Vite.js 的组合非常地轻便好用,适合我这种不做大型项目但又需要方便可维护的特性的场景。
从一个 iOS 程序员的角度来看,单文件的 .vue 可以类比为 iOS 的 ViewController 或者 View。当我写一个 About.vue 这样的页面时,他就是一个 View Controller, <script> 是他的 View Model,<template> 包起来的部分就是他的 UI 布局代码。
Vue framework 的整体设计使得复杂的逻辑只能放在 <script> 部分编写,所有的用户行为操作通过事件或函数回调进 methods 里面,再修改自己的 this.data。通过 this.data 与 template 中元素的绑定关系更新 UI。
在 iOS 开发中,如果要实现这样比较纯粹的单项数据流动的写法,我们可以使用类似 RxSwift 的库实现 UI 绑定,然后从 View Controller 中分离 View Model 来达成。但约束就没有 Vue 来得单一和严格。
ts 带来了使用 JS 实现大型项目的可能,比如 Angular.js 和 VS Code,Vue 带来相比 Angular, React 更加轻量的响应式编程体验,Vite.js 也以轻量迅捷的优势用起来比 webpack 舒服得多,以后我的小工具如果需要 Web UI 应该都会用这套方案,简单又优雅。