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。
上面说了,笔者一直在寻找最好用的构建工具,其实笔者的要求真的很简单:
Typescriptvue、react等)HMR(热模块替换)tree shakingCSS 工具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,是他们三个中最小的。
我们先来回顾一下,刚刚我们提出的要求:
Typescriptvue、react等)HMR(热模块替换)tree shakingCSS 工具SVG,PNG,JSON 和其他我们想要导入的东西现在看来,我们知道,这些要求,vite都满足了。事实上,vite带给我们的,还不止这些,它还支持SSR等功能。
此时此刻,是大年三十的最后一刻,希望小伙伴们新年快乐!
刚刚我们说了还有一个目的,我们来对比一下vue3、react、svelte。
从构建体积来看,svelete 优于 vue3 优于 react。
从状态管理来看,svelte 优于 vue3 优于 react。
从路由管理来看,svelte 等于 vue3 等于 react。
从对于typescript的支持来看, react 优于 vue3 优于 svelete。
那么回到标题的问题,"Vite 会成为2021年最受欢迎的前端工具吗?",相信大家心中已经有了答案。