vue3: https://vite-vue3-todo.netlify.app
react: https://vite-react-todo.netlify.app
svelte: https://vite-svelte-todo.netlify.app
今天是大年初一,首先祝大家新年快乐,牛气冲天🎉🎉🎉
这篇文章是带给大家的新年礼物!
测试不同的前端构建工具一直以来是笔者的一个奇怪的嗜好,因为说实话,webpack
真的太难用了。上手成本高、插件鱼龙混杂、最难受的就是启动dev
太慢,这些都是它的缺点。直到vite
出现,笔者才原来前端开发可以如此丝滑。
Vite
是什么?来自流行的 Vue.js
框架的祖师爷 Evan You
的一个新的前端构建工具。它由两个主要部分组成:
native ES modules
提供源文件的开发服务器,具有丰富的内置特性和快得惊人的热模块替换(HMR
)。Rollup
捆绑在一起,输出用于生产的高度优化的构建包。最近在在各种平台(Twitter
、GayHub
、掘金
、知乎
等平台)都看见了vite
,特别是 Evan You 本人,其更新速度,简直令人咋舌。同时他几乎每天都在Twitter
上面推广vite
,基于这些原因,真的很难让笔者不去尝试vite
。
上面说了,笔者一直在寻找最好用的构建工具,其实笔者的要求真的很简单:
Typescript
vue
、react
等)HMR
(热模块替换)tree shaking
CSS
工具SVG
,PNG
,JSON
和其他我们想要导入的东西讲道理,其实这些要求不算过分吧。
有了这些需求清单,我们继续往下面看,看看vite
是否能满足我们这些要求。
Vite
为了测试vite
,笔者创建了一个简单的Todo App
。它的功能很简单,我们可以在待办页面
输入我们的待办事项,点击该项待办事项,即可以标注它已经完成,同时我们可以在已完成页面
查看我们已经完成了哪些待办事项。
刚刚上面说了,我们想要知道vite对于现在各种前端框架的支持程度,所以我决定分别使用vue3
、react
、svelte
来实现我们的Todo App
。同时值得一提的是,虽然这篇文章是我为了测试vite
而专门写的,但是通过这篇文章,你能够从头到尾的学会,如何将vite
与vue3
、react
、svelte
结合起来使用。
但是你可能会好奇,为什么我这里同时也要尝试将vite
和 svelte
结合起来?因为svelte
这个前端框架新秀成为了2020最受欢迎的前端框架,所以我想一并尝试一下。
svelte
的优点这里笔者不作过多介绍。至于svelte
为什么会火,可以看看这篇文章:都快2020年,你还没听说过SvelteJS?
说了这么多,让我们一起开始吧!
哦对了,在开始之前,还得说明一下。既然我们分别测试了vue3
、react
、svelte
,那我们也同时对他们做一个比较吧。我会从以下两个维度来进行比较:
其中开发体验包括对于
typescript
的支持、状态管理
、路由管理
等。
基于这个目的,我们得保持一定的公平性,意思是我们在进行功能实现时,尽可能少的借助框架本身之外的工具。比如我们在实现状态管理
时,我们尽量使用框架本身自带的功能来实现。
好了,带着这两个目的,我们一起操作起来吧!
为了将三个Todo App
都放在一个工程下面,我们采用了 lerna
来管理我们的三个Todo App
。
# install lerna and init project
$ npm install lerna -g
$ mkdir vite-vue3-react-svelte-todo && cd vite-vue3-react-svelte-todo
$ lerna init
接着我们进入packages
下面新建我们的vue3-todo App
。由于vite
本身是由vue
的作者实现的,所以毋庸置疑,vite
+vue3
肯定是有template
的:
$ cd packages
$ yarn create @vitejs/app vue3-todo --template vue-ts
然后进入到vue3-todo
里面,新建router
、store
、views
、components
。这些太常见不过了,笔者就不作过多介绍了。我们先来看看项目结构:
.
├── index.html
├── node_modules
├── package.json
├── public
│ └── favicon.ico
├── src
│ ├── App.vue
│ ├── assets
│ │ └── logo.png
│ ├── components
│ │ ├── FinishItem.vue
│ │ └── TodoItem.vue
│ ├── main.ts
│ ├── router
│ │ └── index.ts
│ ├── store
│ │ ├── action.ts
│ │ ├── index.ts
│ │ └── state.ts
│ ├── views
│ │ ├── Finish.vue
│ │ └── Todo.vue
│ └── vue-shim.d.ts
├── tsconfig.json
└── vite.config.ts
现在vite2
为了适应更多的前端框架,所以它不会自动支持vue3
,我们得安装一个官方提供的插件@vitejs/plugin-vue
,并将其作为vite
的 plugins
:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()]
})
这里说一下,路由采用了官方最新的路由库:vue-router 4.x
。这个目录一眼便知,我们将todo list
的状态管理,放到了store
里面来管理。这里想着重讲一下状态管理,我们为了公平公正,所以我们这里不借助于vuex
,既然现在vue3
是基于vue-composition-api
的,那我们可以利用这个特性来实现我们的状态管理。 首先我们需要创建一个state
:
// store/state.ts
import { reactive } from 'vue'
export interface TodoItemType {
id: number
done: boolean
content: string
}
export type VuexState = {
todoList: Array<TodoItemType>
}
const state: VuexState = {
todoList: [
{
id: 0,
done: false,
content: 'your first todo'
}
]
}
export const createStore = () => {
return reactive(state)
}
接着我们需要定义一些action
用来变更state
,这其中包括待办事项的增删改查:
// store/action.ts
import { VuexState, TodoItemType } from './state'
function addNewTodoItem(state: VuexState) {
return (newItem: TodoItemType) => {
state.todoList = [...state.todoList, newItem]
}
}
function delteTodoItem(state: VuexState) {
return (item: TodoItemType) => {
state.todoList = state.todoList.filter((e) => e.id !== item.id)
}
}
function changeTodoItemStatus(state: VuexState) {
return (todoItem: TodoItemType) => {
let list = [...state.todoList]
list.map((item) => {
if (item.id === todoItem.id) item.done = !item.done
return item
})
state.todoList = [...list]
}
}
export function createAction(state: VuexState) {
return {
addNewTodoItem: addNewTodoItem(state),
delteTodoItem: delteTodoItem(state),
changeTodoItemStatus: changeTodoItemStatus(state)
}
}
然后我们将其统一暴露出去:
// store/index.ts
import { readonly } from 'vue'
import { createAction } from './action'
import { createStore } from './state'
const state = createStore()
const action = createAction(state)
export const useStore = () => {
return {
state: readonly(state),
action: readonly(action)
}
}
这样,我们就完美的利用vue3
的最新特性实现状态管理,且不需要vuex
了,最棒的是,这样做我们还完美的实现了typescript
支持。
如果想查看有关利用vue3实现自身状态管理的更多内容,请查看这篇文章:vuex4都beta了,vuex5还会远吗?
好了,最重要的部分说完了,我们来看看Todo.vue
:
<template>
<div class="todo">
<div class="card">
<input
class="input"
type="text"
placeholder="your new todo"
v-model="newItemContent"
@keyup.enter="addNewTodoItem"
/>
<div class="card-content">
<TodoItem
v-for="item in todoList"
:key="item.id"
:todoItem="item"
@changeTodoItem="changeTodoItem"
@delteTodoItem="delteTodoItem"
/>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref } from 'vue'
import TodoItem from '../components/TodoItem.vue'
import { useStore } from '../store/index'
import { TodoItemType } from '../store/state'
export default defineComponent({
name: 'Todo',
components: {
TodoItem
},
setup() {
let newItemContent = ref('')
const store = useStore()
const todoList = computed(() => store.state.todoList)
function addNewTodoItem() {
store.action.addNewTodoItem({
done: false,
id: todoList.value.length,
content: newItemContent.value
})
newItemContent.value = ''
}
function changeTodoItem(todoItem: TodoItemType) {
store.action.changeTodoItemStatus(todoItem)
}
function delteTodoItem(todoItem: TodoItemType) {
store.action.delteTodoItem(todoItem)
}
return {
todoList,
newItemContent,
addNewTodoItem,
delteTodoItem,
changeTodoItem
}
}
})
</script>
....
很简单,对吧。在这个页面,我们取出state
里面的todo list
,渲染了每个todo item
。同时还提供了一个Input
输入框,利用v-model
绑定了输入框的值。当我们按下回车键时,就会触发我们提供的addNewTodoItem
方法。这个方法做了两件事情,取出Input
的值,然后通过action
dispatch
到我们的store
,从而新增一个todo item
。
同时我们还提供了更新item
和删除item
的方法,当我们勾选item
前面的check box
时,就表明我们完成了该待办事项。在TodoItem.vue
里面,当我们点击item
的check box
时,通过emit
的方式,将变更提交到父组件Todo.vue
,不过在vue3
里面我们稍微有点改变,我们得通过setup
的第二个参数拿到emit
:
// components/TodoItem.vue
...
setup(props, ctx) {
const { todoItem } = props
function statusChage() {
ctx.emit('changeTodoItem', todoItem)
}
function deleteTodoItem() {
ctx.emit('delteTodoItem', todoItem)
}
return {
todoItem,
statusChage,
deleteTodoItem
}
}
...
为了证明,我们勾选了待办事项之后,我们的state
的确变更了,所以我们也准备了一个 Finish.vue
的页面,这个页面的功能很简单,就是查看我们已经完成的待办事项:
<template>
<div class="finish">
<div class="card">
<div class="card-content">
<div class="card-content">
<FinishItem v-for="item in finishList" :key="item.id" :finishItem="item" />
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
import FinishItem from '../components/FinishItem.vue'
import { useStore } from '../store/index'
export default defineComponent({
name: 'Finish',
components: {
FinishItem
},
setup() {
const store = useStore()
const finishList = computed(() => store.state.todoList).value.filter((item) => item.done)
return {
finishList
}
}
})
</script>
....
这样的话,当我们在Todo
页面点击了某项待办事项之后,我们就可以在finish
页面查看已经完成的待办事项了。
到目前为止,我们在没有使用第三方状态管理库的情况下,实现了状态管理,而且同时获得了很完美的typescript
支持。我们 vue3
版本的Todo App
就完成了。
接下来我们来build
一下,通过运行vite
为我们提供的vite build
命令,我们就可以打出vue3
的Todo App
:
嗯,285k,貌似不是特别大,如果想查看线上效果,直接点击 Vue3-Todo。
接下来我们一起再来看看vite + react
的配合吧!
这里还是和vue3
一样,vite
为我们提供了基于react
的template
,我们只需执行一条脚本命令即可:
$ cd packages
$ yarn create @vitejs/app react-todo --template react-ts
按照惯例,先看目录:
.
|-- index.html
|-- package-lock.json
|-- package.json
|-- src
| |-- App.scss
| |-- App.tsx
| |-- components
| | |-- FinishItem
| | | |-- index.tsx
| | | |-- styles.scss
| | |-- TodoItem
| | |-- index.tsx
| | |-- styles.scss
| |-- index.css
| |-- logo.svg
| |-- main.tsx
| |-- pages
| | |-- Finish
| | | |-- index.tsx
| | | |-- styles.scss
| | |-- Todo
| | |-- index.tsx
| | |-- styles.scss
| |-- router
| | |-- index.tsx
| | |-- styles.scss
| |-- store
| |-- index.tsx
| |-- reducer.ts
| |-- state.ts
|-- tsconfig.json
`-- vite.config.ts
我们之所以这么设置目录,是想和vue3
的目录结构保持一致。
和上面👆vue3
一样,我们得安装一个官方提供的插件@vitejs/plugin-react-refresh
,并将其作为vite
的 plugins
:
// vite.config.ts
import reactRefresh from '@vitejs/plugin-react-refresh'
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [reactRefresh()]
})
另外由于功能都是一样的,所以我们只介绍一下不同的地方。
第一个就是路由,这里我们的路由使用的是react-router-dom
,这是react
的官方路由。
第二个就是状态管理,这里我们采用了context
和useReducer
的方式。
首先,我们还是需要创建一个state
:
// store/state.ts
export interface TodoItemType {
id: number
done: boolean
content: string
}
export type StateType = {
todoList: Array<TodoItemType>
}
const state: StateType = {
todoList: [
{
id: 0,
done: false,
content: 'your first todo'
}
]
}
export const createStore = () => {
return state
}
接着我们需要一些能够改变state
的reducer
:
// store/reducer.ts
import { StateType, TodoItemType } from './state'
export type ActionType =
| { type: 'NEW_TODO_ITEM'; todoItem: TodoItemType }
| { type: 'DELETE_TODO_ITEM'; todoItem: TodoItemType }
| { type: 'UPDATE_TODO_ITEM'; todoItem: TodoItemType }
export const reducer = (state: StateType, action: ActionType) => {
switch (action.type) {
case 'NEW_TODO_ITEM':
return {
...state,
todoList: [...state.todoList, action.todoItem]
}
case 'DELETE_TODO_ITEM':
return {
...state,
todoList: state.todoList.filter((e) => e.id !== action.todoItem.id)
}
case 'UPDATE_TODO_ITEM':
let list = [...state.todoList]
list = list.map((item) => {
if (item.id === action.todoItem.id) {
item.done = !item.done
}
return item
})
return {
...state,
todoList: list
}
}
}
然后我们通过useReducer
和 Contenxt
将state
、reducer
结合起来并暴露出去:
// store/index.tsx
import { createStore, StateType } from './state'
import { ActionType, reducer } from './reducer'
import React, { useReducer, createContext } from 'react'
const store = createStore()
export type TodoContextType = {
state: StateType
dispatch: React.Dispatch<ActionType>
}
export const TodoContext = createContext<any>({})
const TodoProvider: React.FC = (props) => {
const [state, dispatch] = useReducer(reducer, store)
const contextValue = { state, dispatch }
return <TodoContext.Provider value={contextValue}>{props.children}</TodoContext.Provider>
}
export default TodoProvider
我们用Provider
包裹useReducer
暴露出的值,提供给所有子组件。然后在App.tsx
包裹一下Router
组件即可。
我们在Todo/index.tsx
里面,就能通过useContext
拿到useReducer
提供的值:
// pages/Todo/index.tsx
...
const { state, dispatch } = useContext<TodoContextType>(TodoContext)
...
这样我们就可以拿到state
和dispatch
了。
通过context
和useReducer
的方式,我们完美了替代了redux
。和vue3
一样,我们没有使用第三方状态管理,至于对于typescript
的支持嘛,那肯定不用我说,大家都知道react
对于typescript
的支持非常的棒了。
接下来我们来build
一下,通过运行vite
为我们提供的vite build
命令,我们就可以打出react
的Todo App
:
363k,好家伙,有点大啊,如果想查看线上效果,直接点击 React-Todo。
接下来我们一起再来看看vite + svelte
的配合吧!
对于svelte
, vite
没有提供官方 template
,所以我们得自己动手了。
虽然没有官方 template
,但是我们可以依葫芦画瓢。首先我们在packages
目录下面新建一个目录:svelte-todo
,接着新建public
和src
目录,index.html
、tsconfig.json
、vite.config.ts
文件。之后我们在src
目录下面新建我们需要的目录和文件,文件目录就变成了这样:
.
├── index.html
├── node_modules
├── package.json
├── public
│ └── favicon.ico
├── src
│ ├── App.css
│ ├── App.svelte
│ ├── assets
│ │ └── logo.svg
│ ├── components
│ │ ├── FinishItem.svelte
│ │ └── TodoItme.svelte
│ ├── main.ts
│ ├── pages
│ │ ├── Finish.svelte
│ │ └── Todo.svelte
│ ├── router
│ │ └── index.svelte
│ ├── store
│ │ ├── action.ts
│ │ ├── index.ts
│ │ └── state.ts
│ └── types.d.ts
├── tsconfig.json
└── vite.config.ts
既然要使用 vite + svelte
,那我们就需要安装vite
和svelte
:
"devDependencies": {
"@tsconfig/svelte": "^1.0.10",
"svelte-preprocess": "^4.6.3",
"typescript": "^4.1.3",
"vite": "^2.0.0-beta.50",
"vite-plugin-svelte": "https://github.com/benmccann/vite-plugin-svelte"
},
"dependencies": {
"svelte": "^3.32.0",
"svelte-routing": "^1.5.0"
}
这里和上面两个框架一样,路由我们都采用了相应的官方路由,svelte
采用了svelte-routing
。查看了svelte
的教程,如果想要获得typescript
的支持,我们需要安装@tsconfig/svelte
和svelte-preprocess
,并在根目录创建一个svelte.config.js
:
const preprocess = require('svelte-preprocess')
module.exports = { preprocess: preprocess() }
另外,如果我们需要HMR
的功能,这里同样得安装一个plugin
,vite-plugin-svelte
:
// vite.config.ts
import { defineConfig } from 'vite'
import svelte from 'vite-plugin-svelte'
import sveltePreprocess from 'svelte-preprocess'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
svelte({
preprocess: sveltePreprocess(),
compilerOptions: {
dev: true
},
hot: true,
emitCss: false
})
]
})
至此,我们就完美的将svelte
和vite
结合在一起了。
接下来我们来介绍一下svelte
的状态管理。
先在store
目录下面新建一个state
:
// store/state.ts
import { writable } from 'svelte/store'
export interface TodoItemType {
id: number
done: boolean
content: string
}
export type StateType = {
todoList: Array<TodoItemType>
}
const state: StateType = {
todoList: [
{
id: 0,
done: false,
content: 'your first todo'
}
]
}
export const createStore = () => {
return writable(state)
}
svelte
为我们提供了writable
,将我们的state
包裹起来,这样就实现了响应式。
接下来我们来创建一些变更state
的action
:
// store/action.ts
import type { Writable } from 'svelte/store'
import type { StateType, TodoItemType } from './state'
function addNewTodoItem(state: Writable<StateType>) {
return (newItem: TodoItemType) => {
state.update((state) => {
return {
...state,
todoList: [...state.todoList, newItem]
}
})
}
}
function delteTodoItem(state: Writable<StateType>) {
return (item: TodoItemType) => {
state.update((state) => {
return {
...state,
todoList: state.todoList.filter((e) => e.id !== item.id)
}
})
}
}
// svelte do not change state by action ,beacase all of them is reactivity,it's amazing!
// function changeTodoItemStatus(state: Writable<StateType>) {
// return (todoItem: TodoItemType) => {
// state.update((state) => {
// let list = [...state.todoList]
// // list.map((item) => {
// // if (item.id === todoItem.id) item.done = !item.done
// // return item
// // })
// return {
// ...state,
// todoList: [...list]
// }
// })
// }
// }
export function createAction(state: Writable<StateType>) {
return {
addNewTodoItem: addNewTodoItem(state),
delteTodoItem: delteTodoItem(state)
// changeTodoItemStatus: changeTodoItemStatus(state)
}
}
todo item
的时候,不需要通过action,是因为被writable
包裹的值,是具有响应式的,这一点很棒!
然后我们将state
和action
结合起来:
// store/index.ts
import { createAction } from './action'
import { createStore } from './state'
const state = createStore()
const action = createAction(state)
export const useStore = () => {
return {
state,
action
}
}
接着我们来看看在svelte
组件里面如何拿到state
和action
:
// pages/Todo.svelte
...
const store = useStore()
const { state, action } = store
let newItemContent = ''
let todoList: Array<TodoItemType> = []
state.subscribe((state) => {
todoList = state.todoList
})
...
这样,我们就完美的拿到了state
和action
。
另外还有一点,值得提一下。在变更todo item
时,我们如何从TodoItem.svelte
通知父组件Todo.svelte
呢?
svelte
为我们提供了 createEventDispatcher
:
// components/TodoItem.svelte
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
通过这个dispatch
,我们可以派发一个action
到父组件:
function deleteTodoItem() {
dispatch('delteTodoItem', todoItem)
}
在父组件,通过同名action
,我们就能拿到从子组件携带的参数:
// pages/Todo.svelte
...
function delteTodoItem(e: CustomEvent) {
action.delteTodoItem(e.detail)
}
...
<div class="card-content">
{#each todoList as item}
<TodoItem todoItem={item} on:delteTodoItem={delteTodoItem} />
{/each}
</div>
...
发现没有,这种方式和vue的emit
其实一个样。
接下来我们来build
一下,通过运行vite
为我们提供的vite build
命令,我们就可以打出svelte
的Todo App
:
嗯,262k,是他们三个中最小的。
我们先来回顾一下,刚刚我们提出的要求:
Typescript
vue
、react
等)HMR
(热模块替换)tree shaking
CSS
工具SVG
,PNG
,JSON
和其他我们想要导入的东西现在看来,我们知道,这些要求,vite
都满足了。事实上,vite
带给我们的,还不止这些,它还支持SSR
等功能。
此时此刻,是大年三十的最后一刻,希望小伙伴们新年快乐!
刚刚我们说了还有一个目的,我们来对比一下vue3
、react
、svelte
。
从构建体积来看,svelete
优于 vue3
优于 react
。
从状态管理来看,svelte
优于 vue3
优于 react
。
从路由管理来看,svelte
等于 vue3
等于 react
。
从对于typescript
的支持来看, react
优于 vue3
优于 svelete
。
那么回到标题的问题,"Vite 会成为2021年最受欢迎的前端工具吗?",相信大家心中已经有了答案。