从0搭建Vue3.0+TypeScript+antd-Vue+axios+JSX前端框架

背景

Vue3.0出来了很长一段时间了,Vue3.0对于TypeScript的支持也有了质的提升,因为自己现在用React稍微多一些,所以也想在Vue中加入JSX,试试利用一下Vue3.0的新特性搭建一个基础框架。
所需主要库包括:

  • UI antd-Vue 2.0
  • TypeScript
  • Axios
  • 状态管理采取的Provide、Inject的方案
  • JSX(TSX)
  • Vue-router

package.json的依赖如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
  "name": "vue-cli",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "ant-design-vue": "^2.0.0-beta.9",
    "axios": "^0.20.0",
    "normalize.css": "^8.0.1",
    "core-js": "^3.6.5",
    "style-resources-loader": "^1.3.3",
    "vue": "^3.0.0-0",
    "vue-router": "^4.0.0-0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-router": "~4.5.0",
    "@vue/cli-plugin-typescript": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0-0",
    "postcss-import": "^12.0.1",
    "postcss-px-to-viewport": "^1.1.1",
    "postcss-url": "^8.0.0",
    "node-sass": "^4.12.0",
    "sass-loader": "^8.0.2",
    "postcss-write-svg": "^3.0.1",
    "typescript": "~3.9.3"
  }
}

安装

  • yarn安装Vue + TypeScript
1
2
3
npm i -g @vue/cli
OR
yarn global add @vue/cli

创建项目

1
vue create vue-cli

选择配置

1
yarn serve

  • yarn安装antd-design-vue
1
yarn add ant-design-vue@next -S

引入antd-Vue

1
2
3
4
5
6
7
8
9
10
11
import { createApp } from 'vue'
import { Button, message } from 'ant-design-vue';
import { App } from './App'
import router from './router'
import 'ant-design-vue/dist/antd.css';
import 'normalize.css'
const app = createApp(App)
app.use(Button)
app.use(router)
app.config.globalProperties.$message = message;
app.mount('#app')

目录结构

image.png

创建组件

这里稍微和React中的JSX不一样的地方就是,不是用children去取中间子元素,而是用slots获取

1
2
3
4
5
6
7
8
9
10
11
12
13
// compoents/Button.tsx
import { defineComponent } from 'vue';
import { Button } from 'ant-design-vue';
const ButtonCom = defineComponent({
    setup(props: {}, { slots }) {
        return () => (
            <Button type="primary">
                {slots.default && slots.default()}
            </Button>
        )
    }
})
export default ButtonCom;

引入组件

引入组件的方式和React一致,顶部import导入,页面直接调用

1
 <Button type="danger">这是一个按钮</Button>

引入按钮组件

改造原有HOME页面

原有Home组建的改造也和React写法一致,这里必须要先申明图片declare,不然要报错的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// views/Home/index.tsx
import { defineComponent } from 'vue';
import HelloWorld from "@/components/HelloWorld/index";
import logo from '@/assets/logo.png'
interface HomeProps { }
const Home = defineComponent({
    setup(props: HomeProps) {
        return () => (
            <div class="home">
                <img alt="Vue logo" src={logo} />
                <HelloWorld />
            </div>
        )
    }
})
export default Home;

在src新建 images.d.ts文件

1
2
3
4
5
6
7
8
// images.d.ts
declare module '*.svg'
declare module '*.png'
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.gif'
declare module '*.bmp'
declare module '*.tiff'

重新打包,图片加载正常

这里有一个问题,比如这样的组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//  components/Content/index.tsx
import { defineComponent, reactive, onMounted } from 'vue';

interface LabelProps {
    content: any;
}
const Label = defineComponent({
    setup(props: LabelProps) {
        onMounted(() => { console.log('mounted!'); });
        return () => {
            const { content } = props;
            return <span>{content}</span>;
        }
    }
})
export default Label
//调用
<Content content={props.msg}></Content>

按道理来说content会展示出来,而实际上查看却没有content内容,打开控制台会发现,content内容变成一个而属性跑到节点上去了,节点内部却没有任何内容。这个问题 尤大大也出来解释过https://github.com/vuejs/rfcs/pull/154,解决的方法我这里有两种。

image.png

  • attrs
    既然它成为了属性,那么就把他用属性的方式取出来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//  components/Content/index.tsx
import { defineComponent, reactive, onMounted } from 'vue';

interface LabelProps {
    content: any;
}
const Label = defineComponent({
    setup(props: LabelProps, { attrs }: any) {
        console.log(attrs)
        onMounted(() => { console.log('mounted!'); });
        return () => {
            const { content } = attrs;
            return <span>{content}</span>;
        }
    }
})
export default Label

image.png

  • props
    有人会觉得attrs会不优雅,就想用props,怎么办,就可以使用props方法,只是写法和之前略有区别:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//  components/Content/index.tsx
import { defineComponent, reactive, onMounted } from 'vue';

const Label = defineComponent({
    props: {
        content: String,
    },
    setup: (props) => {
        return () => (
            <p>{props.content}</p>
        )
    }
})
export default Label

image.png

现在就能正确拿到props内容,同时也不会有一个属性叫content;

状态管理

本来采取的是Vuex的方式,后来想着既然都使用了Vue3.0了何不用 provide和inject;于是这篇文章就改了;
在context文件夹下创建button.ts 主要用于button组件的状态管理:
provide:是一个对象,或者是一个返回对象的函数。里面呢就包含要给子孙后代的东西,也就是属性和属性值。
ref:用于类似于Hooks的 useRef用于存储变量
Ref:是 ref的types
inject:一个字符串数组,或者是一个对象。
computed:计算属性

1
2
3
//  src/context/button.ts
import { provide, ref, Ref, inject, computed } from 'vue'
import { getTestApi } from '@/api/testApi'

定义interface

1
2
3
4
5
6
//  src/context/button.ts
interface ListContext {
    count: Ref<number>,
    count2: Ref<number>,
    changeCount: (data: number) => void
}

provide方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//  src/context/button.ts
// provide名称,推荐用Symbol
const listymbol = Symbol()
// 提供provide的函数
export const buttonProvide = () => {
    const count = ref<number>(0);

    // 计算属性
    const count2 = computed(() => {
        return count.value * 2
    })
   //在这里可以引入axios api做异步请求
    const changeCount = async function (data: number) {
         try {
            let res: any = await getTestApi("async")
         } catch (error) {
            console.log(error)
            count.value = count.value + data
            console.log(count.value)
         }
    }

    provide(listymbol, {
        count,
        count2,
        changeCount
    })
}

inject方法:

1
2
3
4
5
6
7
8
//  src/context/button.ts
export const buttonInject = () => {
    const listContext = inject<ListContext>(listymbol);
    if (!listContext) {
        throw new Error(`buttonInject must be used after buttonProvide`);
    }
    return listContext
};

这就是一个button组建的状态provide、inject方法就写好了,接下来需要另外写个index.ts 统一将他们暴露出去。

1
2
3
4
5
6
7
8
//  src/context/index.ts
import { buttonProvide, buttonInject } from './button'
console.log("buttonInject", buttonInject)

export { buttonInject }
export const useProvider = () => {
    buttonProvide()
}

现在就可以使用了,这里必须先将provide挂载到某个你需要共用的地方,可以是某几个组件的父页面,也可以是APP.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// App.tsx
import { defineComponent } from 'vue';
import { useProvider } from '@/context/index'
import '@/assets/stylus/index.scss'
export const App = defineComponent({
  name: 'App',
  props: {
    content: String,
  },
  setup: (props) => {
    useProvider()
    return () => (
      <div>
        <div id="nav">
          <router-link to="/">Home</router-link> |
          <router-link to="/about">About</router-link>
        </div>
        <router-view />
      </div>
    )
  }
})

至于调用就下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { defineComponent } from 'vue';
import { Button } from 'ant-design-vue';
import { buttonInject } from '@/context/index'     //引入入口index.ts
interface ButtonProps {
    type: any
}
const ButtonCom = defineComponent({
    setup(props: ButtonProps, { slots }) {
        const { changeCount, count, count2 } = buttonInject()        //获取到方法属性 就可以使用了
        const handleClick = () => {
            changeCount(1)
        };
        return () => (
            <Button type={props.type} onClick={handleClick}>
                {slots.default && slots.default()}count:{count.value}count2:{count2.value}  
            </Button>
        )
    }
})

export default ButtonCom;

chrome-capture (10).gif

项目gitHub地址:https://github.com/Benzic/vue3.0-typescript-antdVue-tsx
欢迎star 谢谢