5

当 Vue3 遇上 TypeScript 和 TSX – 淮城一只猫

 3 years ago
source link: https://iiong.com/vue3-use-notes/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
当 Vue3 遇上 TypeScript 和 TSX – 淮城一只猫
Vue3 体验
Jaxson Wang / 2020-09-17 / 编程技术 / 阅读量 3625

经过几个月后,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 [email protected]
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
javascript

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

至于 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
javascript

学习源码: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'
]
javascript

然后新建个 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')
  }
]
javascript

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


以下皆为历史记录

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'
typescript

shim.d.ts

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

修改 index.html 文件入口:

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

转换 TSX 项目

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

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

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

然后这边报错,是因为 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>
typescript

@/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>
typescript

Vue Composition API RFC

Github vue-cli-plugin-vue-next

Github vue-next

Github vue-next-webpack-preview


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK