当 Vue3 遇上 TypeScript 和 TSX – 淮城一只猫
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 慢慢成长逐渐成熟,今天进入 RC
阶段,意味着核心 Api
稳定下来了。查看周边的生态也慢慢丰富了,所以抽出点时间查阅相关资料。当我阅读 V3 文档 的时候发现很多写法和 V2
差不多,过渡没有多大问题,当然在脚手架开发中不好说了。所以简单弄个脚手架项目。
最近整合 Vue3
项目可以参考:https://github.com/JaxsonWang/Vue3-Ant-Design-Admin-Pro
新建 vue3
目前有俩种,分别是 vite
和 @vue/cli
,vite
下面有描述,这边依然采用 @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
项目,里面只包含最基础的 Babel
和 Vue3
的项目,如果你需要支持路由状态管理器和样式处理器需要自己额外安装。下面将踩坑体验下其中的过程。
Router、Vuex 和 SASS 支持
查看下项目发现和 vue2
改动不是很大,除了一些细节上的要点,剩下和v2
没什么区别了,下面就直接按照 Router
、Vuex
和 SASS
来支持:
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
这边新建路由实例和以前版本不一样,其他没啥区别了,细节上的差别可以查阅下文档。
至于 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
以下皆为历史记录
在 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
写法需要暴露 props
在 render
使用,然后在父组件只需要 <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>
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK