一、基础结构
vue组件基础结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <template> </template>
<script> </script>
<style scoped> </style> ``` ## 使用typescript ```shell npm install -D typescript ts-node ``` ```json "scripts": { "dev:ts": "ts-node src/ts/index.ts", "build": "tsc src/ts/index.ts --outDir dist --target es6" },
|
dev:ts 代表用了 ts-node 来代替原来在用的 node ,因为使用 node 无法识别 TypeScript 语言。
typescript 这个包是用 TypeScript 编程的语言依赖包
ts-node 是让 Node 可以运行 TypeScript 的执行环境
tsc src/ts/index.ts –outDir dist –target es6
代表打包成js(比如html中使用),输出到src的同级目录dist中, –target es6可以省略,代表js规范。
执行npm run build,就可以在dist中看到相应的js文件。
使用npm run dev:ts
,可以测试typescript的可运行性。
执行npm run build
,会将代码打包到dist目录,之后怎么修改,dist里的代码都不变,直到再次build。
在 Webpack ,可以使用 process.env.NODE_ENV 来区分开发环境( development )还是生产环境( production )。
在 Vite ,还可以通过判断 import.meta.env.DEV 为 true 时是开发环境,判断 import.meta.env.PROD 为 true 时是生产环境。
注意事项:
- 在.gitignore 文件里添加 node_modules 忽略。
文件
- vite.config.ts脚手架配置文件
- .editorconfig文件,代码风格,比如缩进和空格。vsCode需要安装EditorConfig扩展。
- .prettierrc格式化代码。”semi”:true代表结尾需要分号。vsCode需要安装Prettier扩展。
- .eslintrc.js ESLint代码检查工具。
需要在开发依赖中安装相关依赖。
二、创建项目
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| npm init vue@3
//或者用Awesome Starter的脚手架创建模版项目 npm create preset ```
<br> <br> <br> <br> <br> # 三、setup setup在props解析之后,创建组件之前执行。 是组合式API的入口,业务代码可以直接放在里面。
**使用setup的时候,无法使用this来获取Vue实例。** ```typescript <template> <p class="msg">{{ msg }}</p> </template>
<script lang="ts"> import { defineComponent } from 'vue' export default defineComponent({ const msg = 'Hello World!' //context暴露三个属性,attrs、slots、emit setup(props, context) { // 业务代码写这里... onBeforeMount(() => { //一些声明周期触发 })
return { // 需要给 template 用的数据、函数放这里 return // 只在函数中调用,不需要rerurn msg, } }, }) </script>
<style scoped> .msg { font-size: 14px; } </style>
|
四、生命周期
- setup 组件创建前执行
- setup 组件创建后执行
- onBeforeMount 组件挂载到节点上之前执行
- onMounted 组件挂载完成后执行
- onBeforeUpdate 组件更新之前执行
- onUpdated 组件更新完成之后执行
- onBeforeUnmount 组件卸载之前执行
- onUnmounted 组件卸载完成后执行
- onErrorCaptured 当捕获一个来自子孙组件的异常时激活钩子函数
被包含在<keep-alive>
中的组件,会多出2个生命周期钩子:
- onActivated 被激活时执行
- onDeactivated 切换组件后,原组件消失前执行
五、ref与reactive的使用
读取任何 ref 对象的值都必须通过 xxx.value。
ref是一个对象,所以const定义后,依旧可以修改value。
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
| const msg = ref<string>('Hello World!'); const uids = ref<number[]>([ 1, 2, 3 ]);
interface Member { id: number, name: string };
const userInfo = ref<Member>({ id: 1, name: 'Tom' });
<template> <!-- 挂载DOM元素 --> <p ref="msg"> 留意该节点,有一个ref属性 </p> <!-- 挂载DOM元素 -->
<!-- 挂载子组件 --> <Child ref="child" /> <!-- 挂载子组件 --> </template> ``` <br>
**reactive只用于对象、数组。** ```typescript
const userInfo: Member = reactive({ id: 1, name: 'Tom' });
const userList: Member[] = reactive([ { id: 1, name: 'Tom' }, { id: 2, name: 'Petter' }, { id: 3, name: 'Andy' } ]); ```
例子: ```typescript import { defineComponent, reactive, toRefs } from 'vue'
interface Member { id: number, name: string, age: number, gender: string };
export default defineComponent({ setup () { const userInfo = reactive({ id: 1, name: 'Petter', age: 18, gender: 'male' })
const userInfoRefs = toRefs(userInfo);
setTimeout( () => { userInfo.id = 2; userInfo.name = 'Tom'; userInfo.age = 20; }, 2000);
return { ...userInfoRefs } } }) ```
<br> <br> <br> <br> <br> # 六、watch ```typescript import { defineComponent, reactive, watch } from 'vue'
export default defineComponent({ setup() { const userInfo = reactive({ name: 'Petter', age: 18, })
setTimeout(() => { userInfo.name = 'Tom' }, 2000)
watch(userInfo, () => { console.log('监听整个 userInfo ', userInfo.name) })
watch( () => userInfo.name, (newValue, oldValue) => { console.log('只监听 name 的变化 ', userInfo.name) console.log('打印变化前后的值', { oldValue, newValue }) } )
const handleWatch = ( newValue: string | number, oldValue: string | number ): void => { console.log({ newValue, oldValue }) }
watch(userInfo, handleWatch)
}, }) ```
watchEffect: ```typescript export default defineComponent({ setup() { const foo = ref<string>('')
setTimeout(() => { foo.value = 'Hello World!' }, 2000)
function bar() { console.log(foo.value) }
watchEffect(bar) }, })
|
七、computed
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 34 35 36 37 38 39 40 41 42 43 44 45 46
| import { defineComponent, ref, computed } from 'vue'
export default defineComponent({ setup() { const firstName = ref<string>('Bill') const lastName = ref<string>('Gates')
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
const foo = computed({ get() { }, set(newValue) { }, })
setTimeout(() => { firstName.value = 'Petter' }, 2000)
return { fullName, } }, }) ``` **computed的优势是会缓存数据,不用每次都执行一遍操作。** **computed的变量获取依旧要通过.value,并且是只读。** <br> <br> <br> <br> <br>
# 八、动态修改style ## 使用:class
|
Hello World!
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
| ## 使用 :style 动态修改内联样式 ```html <template> <p :style="[style1, style2]" > Hello World! </p> </template>
<script lang="ts"> import { defineComponent } from 'vue'
export default defineComponent({ setup () { const style1 = { fontSize: '13px', 'line-height': 2, } const style2 = { color: '#ff0000', textAlign: 'center', }
return { style1, style2, } } }) </script>
|
使用v-bind
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| <template> <p class="msg">Hello World!</p> </template>
<script lang="ts"> import { defineComponent, ref } from 'vue'
export default defineComponent({ setup () { const fontColor = ref<string>('#ff0000')
return { fontColor, } } }) </script>
<style scoped> .msg { color: v-bind(fontColor); } </style> ```
<br> <br> <br> <br> <br> # 九、路由
```html <template> <Login v-if="route.name === 'login'" /> <Register v-else-if="route.name === 'register'" />
<div v-else> <Sidebar /> <router-view /> </div> </template>
|
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 34 35 36 37 38 39
| import { useRouter } from 'vue-router' const router = useRouter();
router.push({ name: 'home', params: { id: 123 } })
router.back(); ``` 配置404页面 ```typescript const routes: Array<RouteRecordRaw> = [ { path: '/:pathMatch(.*)*', name: '404', component: () => import( '@views/404.vue') } ] ``` 路由守卫
|
import { createRouter } from ‘vue-router’
// 创建路由
const router = createRouter({ … })
// 在路由跳转前触发
router.beforeEach((to, from) => {
// …
})
// 在导航被确认前,同时在组件内守卫和异步路由组件被解析后
router.beforeResolve(async to => {
// 如果路由配置了必须调用相机权限
if ( to.meta.requiresCamera ) {
// 正常流程,咨询是否允许使用照相机
try {
await askForCameraPermission()
}
// 容错
catch (error) {
if ( error instanceof NotAllowedError ) {
// … 处理错误,然后取消导航
return false
} else {
// 如果出现意外,则取消导航并抛出错误
throw error
}
}
}
})
//跳转完成后
router.afterEach( (to, from) => {
// 上报流量的操作
// …
})
// 暴露出去
export default router
{
path: ‘/home’,
name: ‘home’,
component: () => import(/* webpackChunkName: “home” */ ‘@views/home.vue’),
// 在这里添加单独的路由守卫
beforeEnter: (to, from) => {
document.title = ‘程沛权 - 养了三只猫’;
}
}
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
| <br> <br> <br> <br> <br>
# 十、组件通信 ## 1、通过Props ```html <template> <Child title="用户信息" :index="1" :uid="userInfo.id" :user-name="userInfo.name" /> </template> ``` 接收 ```typescript export default defineComponent({ props: { title: { type: String, required: true, default: '默认标题' }, index: Number, userName: String, //两种类型之一 uid: 【Number,String] } }) ``` ## 2、通过emits ```html <template> <Child @update-age="updateAge" /> </template> <script> import { defineComponent, reactive } from 'vue' import Child from '@cp/Child.vue'
interface Member { id: number, name: string, age: number };
export default defineComponent({ components: { Child }, setup () { const userInfo: Member = reactive({ id: 1, name: 'Petter', age: 0 })
// 定义一个更新年龄的方法 const updateAge = (age: number): void => { userInfo.age = age; }
return { userInfo,
// return给template用 updateAge } } }) </script> ``` 子元素调用 ```typescript export default defineComponent({ emits: [ 'update-age' ], setup (props, { emit }) { // 2s 后更新年龄 setTimeout( () => { emit('update-age', 22); }, 2000);
} }) ``` ## 3、eventbus ```shell npm install --save mitt ``` 在libs文件夹下建立bus.ts ```typescript import mitt from 'mitt'; export default mitt(); ``` 启用接收eventbus ```typescript import { defineComponent, onBeforeUnmount } from 'vue' import bus from '@libs/bus'
export default defineComponent({ setup () { // 定义一个打招呼的方法 const sayHi = (msg: string = 'Hello World!'): void => { console.log(msg); }
// 启用监听 bus.on('sayHi', sayHi);
// 在组件卸载之前移除监听 onBeforeUnmount( () => { bus.off('sayHi', sayHi); }) } }) ``` 发送eventbus ```typescript import { defineComponent } from 'vue' import bus from '@libs/bus'
export default defineComponent({ setup () { // 调用打招呼事件,传入消息内容 bus.emit('sayHi', '哈哈哈哈哈哈哈哈哈哈哈哈哈哈'); } }) ``` <br> <br> <br> <br> <br> # 十一、Vuex src/store/index
```typescript import { createStore } from 'vuex'
export default createStore({ state: { }, mutations: { }, actions: { }, modules: { } }) ``` <br> <br> <br> <br> <br> # 十一、Pinia ```shell npm install pinia ``` **scr/main.ts**中 ```typescript import { createApp } from 'vue' import { createPinia } from 'pinia' // 导入 Pinia import App from '@/App.vue' createApp(App) .use(createPinia()) // 启用 Pinia .mount('#app') ``` **Pinia的核心也是store** 基本定义: ```typescript // src/stores/index.ts import { defineStore } from 'pinia' //如果有多个store,可以分模块管理。useMessageStore、useUserStore等 export const useStore = defineStore('main', { state: () => { return { message: 'Hello World', //通过as指定类型。等同<string[]>[] randomMessages: [] as string[], } },
actions: {
// 异步更新 message //调用store.updateMessage('New message by async.').then((res){}) async updateMessage(newMessage: string): Promise<string> { return new Promise((resolve) => { setTimeout(() => { // 这里的 this 是当前的 Store 实例 this.message = newMessage resolve('Async done.') }, 3000) }) }, // 同步更新 message updateMessageSync(newMessage: string): string { // 这里的 this 是当前的 Store 实例 this.message = newMessage return 'Sync done.' }, },
// 定义一个 fullMessage 的计算数据 getters: { fullMessage: (state) => `The message is "${state.message}".`, // 这个 getter 返回了另外一个 getter 的结果 emojiMessage(): string { return `🎉🎉🎉 ${this.fullMessage}` }, // 定义一个接收入参的函数作为返回值。这种get没有相应性 //调用const signedMessage = store.signedMessage('Petter') signedMessage: (state) => { return (name: string) => `${name} say: "The message is ${state.message}".` }, }, }) ``` 基本使用: ```typescript import { defineComponent } from 'vue' import { useStore } from '@/stores'
export default defineComponent({ setup() { // 像 useRouter 那样定义一个变量拿到实例 const store = useStore()
// 通过计算拿到里面的数据 // 直接通过 message.value = 'New Message.';就可以更新 const message = computed({ // getter 还是返回数据的值 get: () => store.message, // 配置 setter 来定义赋值后的行为 set(newVal) { store.message = newVal }, })
// 传给 template 使用 return { message, } }, }) ``` <br> <br> 也提供```storeToRefs```把state 的数据转换为 ref 变量。 它会忽略掉 Store 上面的方法和非响应性的数据,只返回 state 上的响应性数据。 ```typescript import { defineComponent } from 'vue' import { useStore } from '@/stores'
// 记得导入这个 API import { storeToRefs } from 'pinia'
export default defineComponent({ setup() { const store = useStore()
// 通过 storeToRefs 来拿到响应性的 message const { message } = storeToRefs(store) //const { message } = toRefs(store) //跟 storeToRefs 操作都一样,只不过用 Vue 的这个 API 来处理
console.log('message', message.value)
//要用时直接赋值即可 //message.value = 'New Message.'
return { message, } }, })
|
批量修改
patch为增量修改。修改的内容会补充到state中
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| store.$patch({ message: 'New Message', randomMessages: ['msg1', 'msg2', 'msg3'], })
store.$patch((state) => { state.message = 'New Message' for (let i = 0; i < 3; i++) { state.randomMessages.push(`msg${i + 1}`) } })
store.$state = { message: 'New Message', randomMessages: ['msg1', 'msg2', 'msg3'], }
store.$reset()
store.$subscribe((mutation, state) => { })
const unsubscribe = store.$subscribe((mutation, state) => { }, { detached: true })
unsubscribe() ``` <br> <br> <br>
## 多store ### 1、在scr/stores目录下建立多个文件,每个store一个。
|
src
└─stores
│ # 入口文件
├─index.ts
│ # 多个 store
├─user.ts
├─game.ts
└─news.ts
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| ### 2、每个store文件中,导出use方法。 ```typescript //每个store的id必须唯一 export const useUserStore = defineStore('user', { }) ``` ### 3、在index.ts里统一输出。 ```typescript export * from './user' export * from './game' export * from './news' ``` ### 4、使用时只需要从stores导入。 ```typescript import { useUserStore, useGameStore } from '@/stores' export default defineComponent({ setup() { // 先从 userStore 获取用户信息(已经登录过,所以可以直接拿到) const userStore = useUserStore() const { userId, userName } = storeToRefs(userStore)
// 使用 gameStore 里的方法,传入用户 ID 去查询用户的游戏列表 const gameStore = useGameStore() const gameList = ref<GameItem[]>([]) onMounted(async () => { gameList.value = await gameStore.queryGameList(userId.value) })
return { userId, userName, gameList, } }, }) ``` <br> <br> <br>
## store之间相互调用 ```typescript import { defineStore } from 'pinia'
// 导入用户信息的 Store 并启用它 import { useUserStore } from './user' const userStore = useUserStore()
export const useMessageStore = defineStore('message', { state: () => ({ message: 'Hello World', }), getters: { // 这里我们就可以直接引用 userStore 上面的数据了 greeting: () => `Welcome, ${userStore.userName}!`, }, }) ```
<br> <br> <br> <br> <br> # 十二、补遗 使用了script-setup ```typescript <!-- 使用 script-setup 格式 --> <template> <Child /> </template>
<script setup lang="ts"> //无需再defineComponent和return, import Child from '@cp/Child.vue'
defineProps({ name: { type: String, required: false, default: 'Petter' }, userInfo: Object, tags: Array });
const msg: string = 'Hello World!';
// 获取 emit const emit = defineEmits(['chang-name']);
// 调用 emit emit('chang-name', 'Tom');
const post = await fetch(`/api/post/1`).then((r) => r.json()) </script>
|