尤雨溪(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 应该都会用这套方案,简单又优雅。