vite-vue3-react-svelte-todo-app Svelte Themes

Vite Vue3 React Svelte Todo App

one vite + vue3 + react + svelte todo App.

preview

vue3: https://vite-vue3-todo.netlify.app

react: https://vite-react-todo.netlify.app

svelte: https://vite-svelte-todo.netlify.app

Vite 会成为2021年最受欢迎的前端工具吗?

说在前面

今天是大年初一,首先祝大家新年快乐,牛气冲天🎉🎉🎉
这篇文章是带给大家的新年礼物!

测试不同的前端构建工具一直以来是笔者的一个奇怪的嗜好,因为说实话,webpack 真的太难用了。上手成本高、插件鱼龙混杂、最难受的就是启动dev太慢,这些都是它的缺点。直到vite出现,笔者才原来前端开发可以如此丝滑。

Vite是什么?

来自流行的 Vue.js 框架的祖师爷 Evan You 的一个新的前端构建工具。它由两个主要部分组成:

  • 一个通过native ES modules提供源文件的开发服务器,具有丰富的内置特性和快得惊人的热模块替换(HMR)。
  • 将代码与 Rollup 捆绑在一起,输出用于生产的高度优化的构建包。

最近在在各种平台(TwitterGayHub掘金知乎等平台)都看见了vite,特别是 Evan You 本人,其更新速度,简直令人咋舌。同时他几乎每天都在Twitter上面推广vite,基于这些原因,真的很难让笔者不去尝试vite

目的

上面说了,笔者一直在寻找最好用的构建工具,其实笔者的要求真的很简单:

  • 它一定要够快(不会随着项目增大而变慢)
  • 它必须支持 Typescript
  • 它必须支持现在主流的前端框架(包括vuereact等)
  • 它必须支持HMR(热模块替换)
  • 它必须支持 tree shaking
  • 它必须支持各种 CSS 工具
  • 它支持导入 SVGPNGJSON 和其他我们想要导入的东西

讲道理,其实这些要求不算过分吧。

有了这些需求清单,我们继续往下面看,看看vite是否能满足我们这些要求。

测试Vite

为了测试vite,笔者创建了一个简单的Todo App。它的功能很简单,我们可以在待办页面输入我们的待办事项,点击该项待办事项,即可以标注它已经完成,同时我们可以在已完成页面查看我们已经完成了哪些待办事项。

刚刚上面说了,我们想要知道vite对于现在各种前端框架的支持程度,所以我决定分别使用vue3reactsvelte来实现我们的Todo App。同时值得一提的是,虽然这篇文章是我为了测试vite而专门写的,但是通过这篇文章,你能够从头到尾的学会,如何将vitevue3reactsvelte结合起来使用。

但是你可能会好奇,为什么我这里同时也要尝试将vitesvelte结合起来?因为svelte 这个前端框架新秀成为了2020最受欢迎的前端框架,所以我想一并尝试一下。

svelte的优点这里笔者不作过多介绍。至于svelte为什么会火,可以看看这篇文章:都快2020年,你还没听说过SvelteJS?

说了这么多,让我们一起开始吧!

哦对了,在开始之前,还得说明一下。既然我们分别测试了vue3reactsvelte,那我们也同时对他们做一个比较吧。我会从以下两个维度来进行比较:

  • 开发体验
  • 构建包的体积

其中开发体验包括对于typescript的支持、状态管理路由管理等。

基于这个目的,我们得保持一定的公平性,意思是我们在进行功能实现时,尽可能少的借助框架本身之外的工具。比如我们在实现状态管理时,我们尽量使用框架本身自带的功能来实现。

好了,带着这两个目的,我们一起操作起来吧!

Vue3 + Vite

为了将三个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里面,新建routerstoreviewscomponents。这些太常见不过了,笔者就不作过多介绍了。我们先来看看项目结构:

.
├── 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,并将其作为viteplugins:

// 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里面,当我们点击itemcheck 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命令,我们就可以打出vue3Todo App

嗯,285k,貌似不是特别大,如果想查看线上效果,直接点击 Vue3-Todo

接下来我们一起再来看看vite + react 的配合吧!

React + Vite

这里还是和vue3一样,vite为我们提供了基于reacttemplate,我们只需执行一条脚本命令即可:

$ 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,并将其作为viteplugins:

// 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的官方路由。

第二个就是状态管理,这里我们采用了contextuseReducer的方式。

首先,我们还是需要创建一个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
}

接着我们需要一些能够改变statereducer:

// 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
            }
    }
}

然后我们通过useReducerContenxtstatereducer结合起来并暴露出去:

// 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)
...

这样我们就可以拿到statedispatch了。 通过contextuseReducer的方式,我们完美了替代了redux。和vue3一样,我们没有使用第三方状态管理,至于对于typescript的支持嘛,那肯定不用我说,大家都知道react对于typescript的支持非常的棒了。

接下来我们来build一下,通过运行vite为我们提供的vite build命令,我们就可以打出reactTodo App

363k,好家伙,有点大啊,如果想查看线上效果,直接点击 React-Todo

接下来我们一起再来看看vite + svelte 的配合吧!

Svelte + Vite

对于sveltevite 没有提供官方 template ,所以我们得自己动手了。 虽然没有官方 template,但是我们可以依葫芦画瓢。首先我们在packages目录下面新建一个目录:svelte-todo,接着新建publicsrc目录,index.htmltsconfig.jsonvite.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,那我们就需要安装vitesvelte:

"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/sveltesvelte-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
        })
    ]
})

至此,我们就完美的将sveltevite结合在一起了。

接下来我们来介绍一下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包裹起来,这样就实现了响应式。

接下来我们来创建一些变更stateaction:

// 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包裹的值,是具有响应式的,这一点很棒!

然后我们将stateaction结合起来:

// 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组件里面如何拿到stateaction:

// pages/Todo.svelte
...
const store = useStore()
    const { state, action } = store
    let newItemContent = ''
    let todoList: Array<TodoItemType> = []

    state.subscribe((state) => {
        todoList = state.todoList
    })
...

这样,我们就完美的拿到了stateaction。 另外还有一点,值得提一下。在变更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命令,我们就可以打出svelteTodo App

嗯,262k,是他们三个中最小的。

总结

我们先来回顾一下,刚刚我们提出的要求:

  • 它一定要够快(不会随着项目增大而变慢)
  • 它必须支持 Typescript
  • 它必须支持现在主流的前端框架(包括vuereact等)
  • 它必须支持HMR(热模块替换)
  • 它必须支持 tree shaking
  • 它必须支持各种 CSS 工具
  • 它支持导入 SVGPNGJSON 和其他我们想要导入的东西

现在看来,我们知道,这些要求,vite都满足了。事实上,vite带给我们的,还不止这些,它还支持SSR等功能。

此时此刻,是大年三十的最后一刻,希望小伙伴们新年快乐!

刚刚我们说了还有一个目的,我们来对比一下vue3reactsvelte

从构建体积来看,svelete 优于 vue3 优于 react
从状态管理来看,svelte 优于 vue3 优于 react
从路由管理来看,svelte 等于 vue3 等于 react
从对于typescript的支持来看, react 优于 vue3 优于 svelete

那么回到标题的问题,"Vite 会成为2021年最受欢迎的前端工具吗?",相信大家心中已经有了答案。

Top categories

Loading Svelte Themes