Vue3 体验

淮城一只猫 编程技术
阅读量 0 评论量 0

前言

经过几个月后,Vue3 慢慢成长逐渐成熟,今天进入 RC 阶段,意味着核心 Api 稳定下来了。查看周边的生态也慢慢丰富了,所以抽出点时间查阅相关资料。当我阅读 V3 文档 的时候发现很多写法和 V2 差不多,过渡没有多大问题,当然在脚手架开发中不好说了。所以简单弄个脚手架项目。

最近整合 Vue3 项目可以参考:https://github.com/JaxsonWang/Vue3-Ant-Design-Admin-Pro

新建项目

新建 vue3 目前有俩种,分别是 vite@vue/clivite 下面有描述,这边依然采用 @vue/cli 脚手架来新建项目。首先需要安装最新的 @vue/cli 来新建一个 vue3 项目:

npm install @vue/cli
vue create vue3-demo

# 输出如下
Vue CLI v4.5.6
? Please pick a preset: (Use arrow keys)
  Default ([Vue 2] babel, eslint)
> Default (Vue 3 Preview) ([Vue 3] babel, eslint)
  Manually select features

选择第二个进行创建 Vue3 项目,里面只包含最基础的 BabelVue3 的项目,如果你需要支持路由状态管理器和样式处理器需要自己额外安装。下面将踩坑体验下其中的过程。

Router、Vuex 和 SASS 支持

查看下项目发现和 vue2 改动不是很大,除了一些细节上的要点,剩下和v2没什么区别了,下面就直接按照 RouterVuexSASS 来支持:

yarn add vue-router@next vuex@4.0.0-beta.4
yarn add sass sass-loader -D

按照好了再进行相关配置就可以基本能正常开发一个 Vue3 项目了。

首先配置路由,在 src/router 文件夹下新建 index.js 写入:

import { createRouter, createWebHashHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import(/* webpackChunkName: "home" */ '../views/Home.vue')
  },
  {
    path: '/about-vue',
    name: 'About-Vue',
    component: () => import(/* webpackChunkName: "about-vue" */ '../views/About.vue')
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes: routes
})

export default router

这边新建路由实例和以前版本不一样,其他没啥区别了,细节上的差别可以查阅下文档。

至于 Vuex 和老版本完全一样,没有不一样的地方,这边就不详细解说了:

import { createStore } from 'vuex'
import getters from './getters'
import app from './modules/app'
import user from './modules/user'

const store = createStore({
  getters,
  modules: {
    app,
    user
  }
})

export default store

学习源码:vue3-router-vuex

JSX 的支持

对于复杂的 UI 界面使用 template 去编写不合适了,例如类似递归渲染,比如菜单,使用 JSX 更合适,在这个项目里可以一部分用 template 编写也可以一部分使用 JSX 编写,可是相当舒服,Vue3 在社区采纳 JSX 很多方案,最终确定由 ant-design-vue 团队定制的 JSX 方案,大致看了下还不错,安装也相当简单:

yarn install @vue/babel-plugin-jsx -D

babel.config.js 添加规则:

plugins: [
  '@vue/babel-plugin-jsx'
]

然后新建个 jsx 文件可以进行编写了,我这边编写一个 jsx 渲染的页面,定义一个路由:

const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import(/* webpackChunkName: "home" */ '../views/Home.vue')
  },
  {
    path: '/about-vue',
    name: 'About-Vue',
    component: () => import(/* webpackChunkName: "about-vue" */ '../views/About.vue')
  },
  {
    path: '/about-jsx',
    name: 'About-JSX',
    component: () => import(/* webpackChunkName: "about-jsx" */ '../views/About.jsx')
  }
]

About.jsx 文件如下:

import { mapGetters } from 'vuex'

const styles = {
  about: {
    border: '1px solid #ffaaee'
  }
}

const About = {
  name: 'About',
  computed: {
    ...mapGetters([
      'systemName'
    ])
  },
  methods: {
    onInputChange(event) {
      this.$store.dispatch('app/setSystemName', event.target.value)
    }
  },
  render() {
    return <>
      <div>
        <h3>这是 JSX 页面</h3>
        <label>
          <input type="text" placeholder="系统名称" style={styles.about} value={this.systemName} onInput={this.onInputChange}/>
        </label>
      </div>
    </>
  }
}

export default About

后面被人请教 jsx 的组件事件调用,使用方法也相当简单,先编写个组件传递事件:

import { defineComponent } from 'vue'

const HelloVue = defineComponent({
  props: {
    msg: {
      require: false,
      type: String,
      default: ''
    }
  },
  methods: {
    emitClick(event) {
      this.$emit('hello-vue', event)
    }
  },
  render() {
    return <>
      <h3 onClick={this.emitClick}>{ this.msg }</h3>
    </>
  }
})

export default HelloVue

在父类接受事件回调:

import { mapGetters } from 'vuex'
import HelloVue from '@/components/HelloVue.jsx'

const styles = {
  about: {
    border: '1px solid #ffaaee'
  }
}

const About = {
  name: 'About',
  components: {
    HelloVue
  },
  computed: {
    ...mapGetters([
      'systemName'
    ])
  },
  methods: {
    onInputChange(event) {
      this.$store.dispatch('app/setSystemName', event.target.value)
    },
    helloVueAction(event) {
      alert('我被点击了!')
      console.log(event)
    }
  },
  render() {
    return <>
      <div>
        <h3>这是 JSX 页面</h3>
        <hello-vue msg="这是一个被调用的 JSX 的组件,你可以点击试试看!" onHello-vue={this.helloVueAction}/>
        <label>
          <input type="text" placeholder="系统名称" style={styles.about} value={this.systemName} onInput={this.onInputChange}/>
        </label>
      </div>
    </>
  }
}

export default About

大致就像这样一个效果,更多用法请参考文档:Babel Plugin JSX for Vue 3.0

学习源码:vue3-router-vuex-jsx

TypeScript支持

既然 JSX 都用上了,那么对 TypeScript 支持不能少,虽然所在的公司几乎用不上,不过多掌握一门技术对自己没有坏处。

vue create vue3-demo 选项选择最后一个然后添加 typescript 支持就行了,我是后面才发觉,不然上面那些安装方法直接省略...ts 的写法无法就是加上数据类型支持,不过我接触的比较少,所以用起来不是太熟练,typescript 愣生生被我用成 anyscript 了,当然一套下来感觉还是没 React 爽快。

学习源码:Vue3 + Vue Router + Vuex + TSX

参考文档


以下皆为历史记录

Vite

Vue-Cli 使用中,发现热更新和编译页面非常慢,所以作者放弃基于 Webpack 开发的脚手架,全新开发新的脚手架:Vite ,诸多新特征查阅相关文档,这边不做详述,但对于老版本的脚手架来比,上手几乎没有任何难点,参考的 Api 和老版本一致,新建新的 Vue3 项目很简单:

npm init vite-app hello-vue3

就会生成比较简单的基础 Vue3 项目,运行 npm run dev 即可看到项目内容。

安装 TypeScript

下面把项目转换成 TSX 的项目,首先安装相关依赖:

yarn add typescript @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-plugin-vue -D

配置 tsconfig.json 文件:

{
  "include": [
    "./**/*.ts"
  ],
  "compilerOptions": {
    "jsx": "react",
    "target": "es2020",
    "module": "commonjs",
    "sourceMap": true,
    "strict": true,
    "noUnusedLocals": true,
    "noImplicitReturns": true,
    "moduleResolution": "node",
    "esModuleInterop": true
  }
}

配置语法校验规则 .eslintrc.js 文件:

module.exports = {
  parser: 'vue-eslint-parser',
  parserOptions: {
    parser: '@typescript-eslint/parser',
    ecmaVersion: 2020,
    sourceType: 'module',
    ecmaFeatures: {
      tsx: true,
      jsx: true,
    },
  },
  extends: [
    'plugin:vue/vue3-recommended',
    'plugin:@typescript-eslint/recommended'
  ],
  rules: {
  }
}

src/ 目录下新建 typescript 类型推断优化配置:

source.d.ts 静态资源优化:

declare const React: string
declare module '*.json'
declare module '*.png'
declare module '*.jpg'

shim.d.ts

declare module '*.vue' {
  import Vue from 'vue';
  export default Vue;
}

修改 index.html 文件入口:

<script type="module" src="/src/main.ts"></script>

转换 TSX 项目

修改项目入口文件 main.ts :

import { createApp } from 'vue'
import App from './App'
import './index.css'

createApp(App).mount('#app')

然后这边报错,是因为 App 类型推断错误导致,所以继续修改 App.vue 文件,将文件转换成 App.tsx

import { defineComponent } from 'vue'

export default defineComponent({
  name: 'App',
  setup() {
    return () =>
      <>
        <div class="container">
          Hello World
        </div>
      </>
  }
})

然后重新启动服务即可看到全新的 TSX 项目。

编写组件

本来在 components 文件夹下新建组件,无奈引入的时候一直报错,搜查资料也没有搜到相关资料,所以这边就在当前目录下随意折腾。编写 tsx 组件没有想象中那么复杂,比如新建个 Title.tsx 组件:

import { defineComponent } from 'vue'

export default defineComponent({
  name: 'Title',
  setup() {
    return () =>
      <>
        <h1 class="title">
           This is title.
        </h1>
      </>
  }
})

然后在 App.tsx 引入调用即可:

import { defineComponent } from 'vue'
import Title from './Title'

export default defineComponent({
  name: 'App',
  setup() {
    return () =>
      <>
        <div class="container">
          <Title/>
          Hello World
        </div>
      </>
  }
})

如果组件开放 props 属性给父组件传入参数也很简单,只需要把 Title.tsx 修改成:

import { defineComponent } from 'vue';
export default defineComponent({
  name: 'Title',
  props: {
    title: {
      type: String,
      require: false,
      default: 'This is title.'
    }
  },
  setup(props) {
    return () =>
      <>
        <h1 class="title">
          { props.title }
        </h1>
      </>
  }
})

组件的 props 声明和之前没有什么区别,在 tsx 写法需要暴露 propsrender 使用,然后在父组件只需要 <Title title="Hey!This my title!" /> 使用即可。

子组件事件传递

如果需要将子组件的事件传递到父组件也简单,只需要用到 setup 的第二个参数对象中的 emit 使用方式和 vue2 一样:

import { defineComponent } from 'vue';

export default defineComponent({
  name: 'Title',
  props: {
    title: {
      type: String,
      require: false,
      default: 'This is title.'
    }
  },
  setup(props, context) {
    return () =>
      <>
        <h1 class="title" onClick={() => context.emit('data')}>
          { props.title }
        </h1>
      </>
  }
})

但目前不知道在父组件如何调用...这边暂时就到这里吧。


在插件的支持下,传递事件如下:

import { defineComponent, PropType } from 'vue'

export default defineComponent({
  name: 'Title',
  props: {
    title: {
      type: String,
      require: false,
      default: 'This is title.'
    },
    onTitleClick: Function as PropType<(event: MouseEvent) => void>
  },
  setup (props, context) {
    return () =>
      <>
        <h1 class="title" onClick={(event: MouseEvent) => context.emit('title-click', event)}>
          { props.title }
        </h1>
      </>
  }
})

注意 props 要声明事件, 然后外部调用:

import { defineComponent } from 'vue'
import Title from './components/HelloWorld'

export default defineComponent({
  name: 'App',
  setup () {
    const onTitleClick = (event: any) => {
      console.log(event)
    }
    return () =>
      <>
        <div class="container">
          <Title title="这是一个例子" onTitleClick={onTitleClick} />
          Hello World
        </div>
      </>
  }
})

下面是历史文章,可以无视~

前言

从去年开始就有 Vue3 各种消息,一直比较期待 V3 的版本,因为 V2 针对 TypeScript 不是太完善,支持不是太好,一直没用上。其次针对 React 来比, V2 又显然太死板,并且在大项项目构架上来看,复用性很差。所以在之前的 Vue3 PPT 讲到诸多特性,知道前几天的 Beta 版本出现,虽然 Alpha 版本尝鲜过,无奈问题太多。所以趁此机会折腾一波,前置学习下,趁机弯道超过各位大佬(#滑稽脸)。

建立

最近的 Beta 版本不需要建立 Webpack 项目,所以直接基于 Vue-CLI 脚手架生成的项目,直接注入相关插件就支持 Vue3 项目了。

使用 Vue Cli 建立项目:

vue create vue3-demo

Vue CLI v4.3.1
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, TS, CSS Pre-processors, Lin
ter
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfi
lls, transpiling JSX)? No
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported 
by default): Sass/SCSS (with dart-sass)
? Pick a linter / formatter config: Standard
? Pick additional lint features: Lint on save, Lint and fix on commit
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated confi
g files
? Save this as a preset for future projects? No


vue add vue-next # 安装插件

安装上去发现 package.json 添加几个依赖以及 App.vue 发生改变,但还需要进行进一步配置:

  • 删除 shims-tsx.d.ts 文件,控制台一直显示错误,新版本存在兼容性问题,等官方说明,删掉不影响使用;
  • 修改 App.vue 内容:
<template>
  <div class="home">
    <img src="./assets/logo.png" alt="logo">
    <h1>Hello Vue 3!</h1>
    <button @click="inc">Clicked {{ count }} times.</button>
    <div class="todo-area">
      <todo-list/>
    </div>
  </div>
</template>a

<script lang="ts">
import { ref } from 'vue'
import TodoList from '@/components/TodoList.vue'

export default {
  setup () {
    const count = ref(0)
    const inc = () => {
      count.value++
    }
    return {
      count,
      inc
    }
  },
  components: {
    TodoList
  }
}
</script>

<style lang="scss" scoped>
.home {
  img {
    width: 200px;
  }

  h1 {
    color: #41b983;
    font-family: Arial, Helvetica, sans-serif;
  }
}
</style>

@/components/TodoList.vue 随便填充点内容,启动服务 yarn dev 就可以看到新版本的页面了。

Router / Vuex 整合

未完待续

实战

打算利用新的 Api 做个简单的 Todo List 例子:

<template>
  <div class="todo-list">
    <h3>Todo List</h3>
    <div class="add-todo-area">
      <label>
        <input v-model="addTodoName"/>
      </label>
      <label>
        <button @click="addTodoAction">新增清单</button>
      </label>
    </div>
    <div class="todo-area">
      <h3>任务清单</h3>
      <ul class="todo-list">
        <li
           v-for="item in undoneTodoList"
           :key="item.id"
           class="todo-item"
        >
          {{ item.name }}
          <button @click="doneTodo(item)">已完成</button>
          <button @click="delTodoAction(item, true)">删除</button>
        </li>
      </ul>
    </div>
    <div class="done-todo-area">
      <h3>已完成的任务清单</h3>
      <ul class="todo-list">
        <li
           v-for="item in completedTodoList"
           :key="item.id"
           class="todo-item"
        >
          {{ item.name }}
          <button @click="delTodoAction(item, false)">删除</button>
        </li>
      </ul>
    </div>
  </div>
</template>

<script lang="ts">
import { ref, reactive } from 'vue'

import { TodoList } from './types/TodoList'

export default {
  setup () {
    const addTodoName = ref<string>('')
    const undoneTodoList: TodoList[] = reactive([]) // 清单列表
    const completedTodoList: TodoList[] = reactive([]) // 已完成的清单列表

    const addTodoAction = () => {
      const obj = {
        id: new Date().valueOf(),
        name: addTodoName
      }
      undoneTodoList.push(JSON.parse(JSON.stringify(obj)))
      addTodoName.value = ''
    }

    const delTodoAction = (item: TodoList, todo: boolean) => {
      if (todo) {
        undoneTodoList.splice(undoneTodoList.findIndex(i => i.id === item.id), 1)
      } else {
        completedTodoList.splice(completedTodoList.findIndex(i => i.id === item.id), 1)
      }
    }

    const doneTodo = (item: TodoList) => {
      undoneTodoList.splice(undoneTodoList.findIndex(i => i.id === item.id), 1)
      completedTodoList.push(item)
    }

    return {
      addTodoName,
      addTodoAction,
      delTodoAction,
      doneTodo,
      undoneTodoList,
      completedTodoList
    }
  }
}
</script>

<style lang="scss" scoped>
</style>

参考

Vue Composition API RFC

Github vue-cli-plugin-vue-next

Github vue-next

Github vue-next-webpack-preview

喵~