Mrli
别装作很努力,
因为结局不会陪你演戏。
Contacts:
QQ博客园

Vue3初体验

2022/03/29 前端 Vue
Word count: 8,757 | Reading time: 39min

Vue3于2020年09月18日正式发布,映入我视野的新东西除了Vue3以外,还有yarn和vite。他们两个本身的功能并不是新东西,yarn对标的是npm包管理工具,vite对标的webpack包打包工具。

与vue2不同的是,vue3工程中没有 build 文件, 相应的build/index.js的配置改由了项目根目录下vue.config.js承担

Vue3新特性

  1. template标签下面可以有多个节点了,终于不用先写一个DIV了

  2. setup函数可以代替之前的data,methods,computed,watch,Mounted等对象,但是props声明还是在外面。

相比于Vue采用的OPTIONS API,Vue3使用的为Composition-API

  1. vue3.0允许一个组件有多个根节点
    创建一个组件:通常,我们通过在最外层包裹一层 div 来创建一个组件,但这个div元素一般没有任何使用价值,就只是让模板符合单根需求。

    1
    2
    3
    4
    5
    6
    <template>
    <div> <!--只是来包装一下, 如果不加这一行会报错-->
    <div>Node 1</div>
    <div>Node 2</div>
    </div>
    </template>

    但在Vue3中支持多根即多个<div>、 甚至不写div也没问题

    1
    2
    3
    4
    5
    6
    7
    8
    <template>
    <div>
    {{ msg1 }}
    </div>
    <div>
    {{ msg2 }}
    </div>
    </template>

    原因是Vue.js 3.0 支持了 Fragments 的语法,具体原理没分析。

Composition-Api

setup() 函数

一组低侵入式的、函数式的 API,使得我们能够更灵活地「组合」组件的逻辑。

在学习 Composition-Api 之前,我们需要先了解一下 setup() 函数。 setup() 是 Vue3 中的新增内容。它为基于 Composition API 的新特性提供了统一的入口。

在Vue3中,定义 methods、watch、computed、data数据 等都放在了 setup() 函数中

  1. 执行时机
    setup()函数会在created()生命周期之前执行。

  2. 参数

  • setup() 函数的第一个参数是 props ,组件接收的 props 数据可以在 setup() 函数内访问到。
  • context 是 setup() 的第二个参数,它是一个上下文对象,可以通过 context 来访问Vue的实例 this 。
    注意:在 setup() 函数中访问不到Vue的 this 实例, 需要通过context来取

具体函数

数据

在Vue2.x的版本中,我们只需要在 data() 中定义一个数据就能将它变为响应式数据,在 Vue3.0 中,需要用 reactive 函数或者 ref 来创建响应式数据。

reactive()
reactive() 函数接收一个普通的对象,返回出一个响应式对象。

1
2
3
4
5
6
7
8
9
10
11
12
// 在组件库中引入 reactive
import { reactive } from '@vue/ composition-api'

setup() {
// 创建响应式对象
const state = reactive({
count:0
});

// 将响应式对象return出去,暴露给模板使用
return state;
}

ref()
ref() 函数可以根据给定的值来创建一个响应式的数据对象,返回值是一个对象,且只包含一个 .value 属性。

1
2
3
4
5
6
7
8
9
10
11
// 引入 ref
import { ref } from '@vue/composition-api'

setup() {
// 创建响应式对象
const count = ref(0);

return {
count
}
}

ref 的注意事项

  • 在 setup() 函数内,由 ref() 创建的响应式数据返回的是对象,所以需要用 .value 来访问;
  • 而在 setup() 函数外部则不需要 .value ,直接访问即可。 ==> <template>中直接使用响应式数据: 响应式对象里面如果有ref包装的值类型。则Vue会实现自动拆箱
  • 可以在 reactive 对象中访问 ref() 函数创建的响应式数据。
  • 新的 ref() 会覆盖旧的 ref() 。
  • 选择 reactive 还是 ref
    • 如, new Date().getMonth()返回的是Number, 通过const m = ref(new Date().getMonth())可以直接拆包获得值,而不是使用reactive。但需要
    • 注意的是,使用m的时候,应该使用m.value
  • reactive无需.value来获得值,而是直接通过xx.prop获取即可

Vue3 —— 创建响应式数据使用 reactive 还是 ref ?(本周更新)

computed()

computed() 用来创建计算属性,返回值是一个 ref() 实例。按照惯例,使用前需要先引入。

  • computed创建只读计算属性
1
2
3
4
5
6
7
const count = ref(1)

// 创建一个计算属性,使其值比 count 大 1
const bigCount = computed(() => count.value + 1) // 即如果只有一个函数,且没有声明是get还是set默认为get只读

console.log(bigCount.value) // 输出 2
bigCount.value++ // error 不可写
  • computed创建可读可写计算属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

const count = ref(1)

// 创建一个 computed 计算属性,传入一个对象
const bigCount = computed({
// 取值函数
get: () => (count.value + 1),
// 赋值函数
set: val => {
count.value = val - 1
}
})

// 给计算属性赋值的操作,会触发 set 函数
bigCount.value = 9
// 触发 set 函数后,count 的值会被更新
console.log(count.value) // 8

readonly()

传入一个响应式对象、普通对象或 ref ,返回一个只读的对象代理。这个代理是深层次的,对象内部的数据也是只读的。

1
2
3
const state = reactive({ count: 0 })

const copy = readonly(state)

函数

以前是写在methods对象中,现在是声明函数后放回

侦听

watch()

composition-api 中的 watch 和 Vue2.x 中是一样的,watch 需要侦听数据,并执行它的侦听回调。默认情况下初次渲染不执行。

应用场景: 监听路由变化(路由变化的监听在Vue生命周期函数中实现都不合适,放watch中比较合适)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)

// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})

watch 与 watchEffect 的不同

  • watch 初次渲染不执行
  • watch 侦听的更具体
  • watch 可以访问侦听数据变化前后的值

from: Vue3 的新特性(二) —— Composition-Api

总结:

  • Composition API 的入口在 setup() 函数中
    • reactive 响应式对象
    • ref 接收一个参数并返回响应式对象
  • 原先在 Vue2 中的 methods,watch,component、data 均写在 setup() 函数,使用之前需要自行导入
  • 回归了 function xxx 定义函数

注:

  • OnMounted()、watch()、computed()中间传的都是匿名函数,而不是对象
  • 在setup中取值不能使用this, this在composition中是undefine.
  • 所以vue2代码用到vue3中时得把this删了(比如function), data的话还得加.value
  • 双向绑定的变量在setup中定义时要加ref,修改变量的值时要加value

Vue2->Vue3废弃的API

  • filters过滤器:由于

    1
    <p>{{ accountBalance | currencyUSD }}</p>

    打破了大括号内的表达式“只是 JavaScript”的假设,这不仅有学习成本,而且有实现成本。==>在 3.x 中,过滤器已移除,且不再支持。取而代之的是,我们建议用方法调用或计算属性来替换它们。

Vue optionalAPI可以使用

  • this.$store.state
1
2
3
4
5
6
7
8
9
10
computed: {
isLoginOpen() {
return this.$store.state.isLoginOpen;
},
},
methods:{
close() {
this.$store.commit("setLoginModal", false);
},
}
  • this.$refs
1
this.$refs.emailRef.focus
  • this.$data
  • this.$axios

附录

导入vuex和vue-router

vue-router使用

yarn add vue-router@next --dev, 参数说明packageName@tagorpackageName@version
创建router/index.js

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
import { createRouter, createWebHistory } from "vue-router";
import ShowList from "../components/ShowList.vue"
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: "/show",
component: ShowList
},
{
path: "/login",
name: "Login",
newB: "yes", // 还可以自定义对象的属性值, 方便之后获取import routes from "@/router/routes"; ==> 如naive的菜单组件中是要获得key的, 这边就可以绑定个key
meta: {
title: '登录'
},
// 懒加载
component: () => import ( /* webpackChunkName: "login" */ "../views/Login.vue")
},{
path: '/user/:id', // 如/user/111, router-link to="/user/111"也行。注id为params参数, User.vue中通过 {{ $route.params.id }} 获取
components: { default: User, sidebar: Sidebar },
/**在组件中使用 $route 会使之与其对应路由形成高度耦合,从而使组件只能在某些特定的 URL 上使用,限制了其灵活性。可以使用 props 将组件和路由解耦:这样你便可以在任何地方使用该组件,使得该组件更易于重用和测试。 */
props: { default: true, sidebar: false }
}


]
})

router.beforeEach((to, from, next) => {
document.title = `${to.meta.title} | vue-manage-system`;
const role = localStorage.getItem('ms_username');
if (!role && to.path !== '/login') {
next('/login');
} else if (to.meta.permission) {
// 如果是管理员权限则可进入,这里只是简单的模拟管理员权限而已
role === 'admin'
? next()
: next('/403');
} else {
next();
}
});
export default router
  • app下进行router跳转
1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<div id="app">
<div>
// 下面的叫声明式
<router-link to="/">Home</router-link> |
<router-link to="/show">show</router-link>|
<router-link to="/hello">hello</router-link>|
<router-link :to="{name: 'antd', params: { id: 2 }}">antd</router-link>
</div>
<router-view/>

</div>
</template>

router-link 实现路由之间的跳转,如点击show超链接则会跳转到http://localhost:8080/show

1
2
3
4
5
6
7
8
9
10
11
12
> <!-- 字符串 -->
> <router-link to="home">Home</router-link>
> <!-- 渲染结果 -->
> <a href="home">Home</a>
>
> <!-- 同上 -->
> <router-link :to="{ path: 'home' }">Home</router-link>
> <!-- 命名的路由 -->
> <router-link :to="{ name: 'user', params: { userId: 123 }}">User</router-link>
> <!-- 带查询参数,下面的结果为 /register?plan=private -->
> <router-link :to="{ path: 'register', query: { plan: 'private' }}">Register</router-link>
>

视图:router-view路由的path 与访问的地址相符时,会将指定的组件内容替换该 router-view,如果不点击而是直接将URL换成http://localhost:8080/show也会显示ShowList.vue的内容(为什么是ShowList.vue?是因为router/index.js中将path=''/show'与ShowList.vue绑定了)

命名视图

  • 子vue中点击按钮后进行router跳转
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
    // vue2写法
function submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.$router.push({name: "Mainwithout", params: {name: this.form.name}}) // 如果要传参,则为router-name;如果不传参直接push(path)
sessionStorage.setItem("isLogin", true)
this.$store.dispatch("asyncUpdateUser", {name: this.form.name})
console.log("enter", this.$store.state)
} else {
this.$message.error('错了哦,这是一条错误消息');
return false;
}
});
}
// vue3写法:Vue-router 4.x为此提供了useRoute():
import {useRouter} from 'vue-router'
setup(props,ctx){
//router是全局路由对象,route= userRoute()是当前路由对象
let router = useRouter();
let start = () => {
// 这个叫编程式
router.push({
//传递参数使用query的话,指定path或者name都行,但使用params的话,只能使用name指定
path:'/home',
query:{
num:1
}
})
}

onMounted(() => {
const id = router.params.id
})
}

编程式调用方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 字符串
router.push('home')
// 对象
router.push({ path: 'home' })
// 命名的路由
router.push({ name: 'user', params: { userId: '123' }})
// 带查询参数,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})

// ▲注:同样的规则也适用于 router-link 组件的 to 属性。

router.replace(location, onComplete?, onAbort?)
// 跟 router.push 很像,唯一的不同就是,它不会向 history 添加新记录,而是跟它的方法名一样 —— 替换掉当前的 history 记录。
router.go(n)
// 这个方法的参数是一个整数,意思是在 history 记录中向前或者后退多少步,类似 window.history.go(n)。

更多

  • $router : 是路由操作对象,只写对象
  • $route : 路由信息对象,只读对象
1
2
3
4
5
6
7
8
9
10
11
12
//操作 路由跳转
this.$router.push({
name:'hello',
params:{
name:'word',
age:'11'
}
})

//读取 路由参数接收
this.name = this.$route.params.name;
this.age = this.$route.params.age;

query传参要用path来引入,params传参要用name来引入

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
// path+query==> /second?queryId=2222&queryName=query
//query传参,使用name跳转
this.$router.push({
name:'second',
query: {
queryId:'20180822',
queryName: 'query'
}
})

//query传参,使用path跳转
this.$router.push({
path:'second',
query: {
queryId:'20180822',
queryName: 'query'
}
})

//query传参接收
this.queryName = this.$route.query.queryName;
this.queryId = this.$route.query.queryId;

/** ---------------------- **/
// name+params==> /second/20180822/query, 更多见:https://router.vuejs.org/zh/guide/essentials/named-routes.html
this.$router.push({
name:'second',
params: {
id:'20180822',
name: 'query'
}
})
//params接收参数, 是$route
this.id = this.$route.params.id ;
this.name = this.$route.params.name ;
//路由
{
path: '/second/:id/:name',
name: 'second',
component: () => import('@/view/second')
}
  1. params是路由的一部分, 必须要在路由后面添加参数名。query是拼接在url后面的参数,没有也没关系。

  2. params一旦设置在路由,params就是路由的一部分,如果这个路由有params传参,但是在跳转的时候没有传这个参数,会导致跳转失败或者页面会没有内容。(最重要的一点,params刷新会消失。query则不会)

    1
    2
    parmas:https://blog.csdn.net/xxx
    query: https://blog.csdn.net?xxx

    from:https://router.vuejs.org/zh/guide/essentials/dynamic-matching.html

    采坑:

    1
    2
    :to="{path:'invest/scattered',query: {productId: list.id}}"
    to="invest/scattered?productId=1" // 如果不加冒号访问动态参数即to="{path:'invest/scattered',query: {productId: list.id}}",则会乱码

    冒号可以传动态的参数(也可以传静态的);不加冒号是静态的路径;动态路由也可以以静态路由的形式写,参数固定如:to="{name: 'antd', params: { id: 2 } }"==>to="/antd/2";参数为变量:

    1
    2
    <router-link :to="{name: 'antd', params: { id: 2 }}">antd</router-link>
    <router-link :to="`/antd/${uid}`">antd</router-link>

    更推荐指定url的写法,因为就不用考虑name还是path了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // App.vue
    <router-link :to="`/antd?name=mrli`">antd</router-link>
    // 要跳转的 Antd.vue
    {{ $route.query.name }}

    // App.vue
    <router-link :to="`/antd/3`">antd</router-link>
    // 要跳转的 Antd.vue
    {{ $route.params.id }}
    // 还需要router/index.js中定义, 不然$route.params.id无法将那个值对应到id上
    {
    path: "/antd/:id",
    name: "antd",
    component: AntdCom
    },

from : Vue3实战系列:结合 Ant-Design-of-Vue 实践 Composition API

踩坑:

  • 嵌套子路由无法直接通过URL访问:报错原因是GET http://localhost:3000/dashboard/src/main.js net::ERR_ABORTED 404 (Not Found)

    • 后经过测试查明,createWebHashHistory是可以直接跳转的,但createWebHistory是不能直接跳到嵌套子路由的

vuex

  1. 安装yarn add vuex@next --dev

  2. 编写store/index.js文件

    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
    import {createStore} from 'vuex'

    export default createStore({
    state: {
    tagsList: [],
    collapse: false
    },
    mutations: {
    closeCurrentTag(state, data) {
    for (let i = 0, len = state.tagsList.length; i < len; i++) {
    const item = state.tagsList[i];
    if (item.path === data.$route.fullPath) {
    if (i < len - 1) {
    data
    .$router
    .push(state.tagsList[i + 1].path);
    } else if (i > 0) {
    data
    .$router
    .push(state.tagsList[i - 1].path);
    } else {
    data
    .$router
    .push("/");
    }
    state
    .tagsList
    .splice(i, 1);
    break;
    }
    }
    },
    // 侧边栏折叠
    handleCollapse(state, data) {
    state.collapse = data;
    }
    },
    actions: {},
    modules: {}
    })
  3. 在组件中使用

    1
    2
    3
    const collapseChage = () => {
    store.commit("handleCollapse", !collapse.value);
    };

分析Vue3项目

Vue Color Avatar

index.js 中的 v-cloak 指令

首先yarn dev本地运行后进入页面,会有一个Loading, coming soon的页面加载过程,之后就会取消掉。然后在项目入口的index.html中看到了实现——v-cloak

功能:使用 v-cloak 指令设置样式,这些样式会在 Vue 实例编译结束时,从绑定的 HTML 元素上被移除

场景:当网络较慢,网页还在加载 Vue.js ,而导致 Vue 来不及渲染,这时页面就会显示出 Vue 源代码(如下图)。我们可以使用 v-cloak 指令来解决这一问题。

我们使用 v-cloak 指令来解决屏幕闪动的问题吧O(∩_∩)O~,js 不变,在 div 中加入 v-cloak 指令。

1
2
3
4
// html
<div id="app" v-cloak>
{{context}}
</div>
1
2
3
4
# css
[v-cloak]{
display: none;
}

使用 v-cloak 指令之后的效果(demo):

img

在简单项目中,使用 v-cloak 指令是解决屏幕闪动的好方法。但在大型、工程化的项目中(webpack、vue-router)只有一个空的 div 元素,元素中的内容是通过路由挂载来实现的,这时我们就不需要用到 v-cloak 指令咯。

from: https://www.jianshu.com/p/f56cde007210

注:index.html中定义的元素,在Vue实例加载完成后,将都不会显示,因此比较适合用来做Loading页面

1
2
3
4
5
6
<div id="app" v-cloak>
<div class="placeholder">
<div class="logo"></div>
<div class="text">Coming soon...</div>
</div>
</div>

toRefs+解包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
setup(){
const data = reactive({
name: '小皮',
age: 18,
year: computed({
// 设置 getter 和 setter
get: () => {
return 2020 - data.age
},
set: val => {
data.age = 2020 - val
}
})
})
return {...toRefs(data), changeAge, changeYear}
}

vue 3.0 给input 获取焦点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 给table 循环的input 点击的时候获取焦点
// 1、给input 设置ref 属性
<el-input
v-else
ref="refInput"
@blur="(scope.row.code = !scope.row.code), changeBlur()"
v-model="scope.row[item.prop]"
:placeholder="'请输入内容' + item.label"
/>
// 1、给input的ref属性在setup中使用? 叫什么我不清楚
const refInput =ref() //记得return 出去

//事件触发 记得引入 nextTick from vue
const aaaa ()=>{
nextTick(()=>{
refInput.value.focus()
})
}

vue2中的写法,通过this.$ref.xxx.focus()来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 <div>
//获取焦点主要就是定义一个ref
<input v-model="inputvalue" ref="inputVal"/>
<button @click="addItem">提交</button>
</div>


methods:{
//事件调用
addItem:function(){
//然后调用focus方法
this.$nextTick(()=>{
this.$refs.inputVal.focus()
})
}
}

关于import

默认情况下,JavaScript中在模块内的所有声明都是本地的,外部无法访问。如果需要公开模块中部分声明的内容,并让其它模块加以使用,这个时候就需要导出功能,最简单的方式是添加export关键字导出模块。

  • 默认导出导入: 每个模块仅有一个default的导出,导出内容可以是一个function、class,object等。因为这种方式被当做主要的导出内容,导入方式最为简单。
    • ▲注意:由于一个模块仅仅只允许导出一个default对象,实际导出的是一个default命名的变量。
    • 因此导入的时候import后的名字, 实际上是给这个default命名的导出暴露的内容进行重命名,所以import后可以是任意变量名称,且不需要{}。
  • named导出导入export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
    • import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(即暴露接口模块)对外接口的名称相同。
    • 如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。

总结:

  • 如果是默认导出export default则可以省略花括号,且import的模块名是随意的
  • 如果是非export default导出的接口内容,在引用时须加花括号,且引用的模块名与导出时的命名必须相同
    本质是因为:一个模块中只允许一个默认导出export default,但允许多个命名导出export;

from 格式:

Js import的具体查找规则为:
① 如果X是内置模块,则直接返回该模块。如require(‘http’)。
② 如果X以.//../的相对路径开头,则
1. 根据X所在的父模块,确定X的绝对路径。
2. 将X当做文件,依次查找下面的文件,如果找到,则直接返回。
1. X
2. X.js
3. X.json
4. X.node
3. 将X当做目录,依次查找下面的文件,如果找到,则直接返回。
1. X/package.json(查找main字段中的文件,规则同上)
2. X/index.js
3. X/index.json
4. X/index.node
③ 如果X不带路径:
1. 根据X所在的父模块,确定X可能的安装目录。
2. 依次在每个目录中,将X当成文件名或目录名加载。
④ “not found”

因此,以下三种效果都是一样的,都可以成功

1
2
3
import router from './router/index.js'          // 符合 `②21`
import router from './router/index' // 符合 `②22`
import router from './router' // 符合 `②32`

既然如此,那么肯定有先后顺序,因此,我们做个实验。main.js为根目录下有个vu.js(目标为②22), 还有个vu的文件夹,其中包含个index.js(目标为②32)。

1
2
3
4
5
6
7
8
mrli@VM-4-7-ubuntu:~/import_test$ tree
.
├── main.js
├── vu
│   └── index.js
└── vu.js

1 directory, 3 files

注意:本地测试时是node运行,而Node和浏览器端所支持的模块规范不同。

条目 Node 浏览器
模块规范 CommonJS ES6
导出 modules.exports export, export default;modules.exports
引入 require import;require

在windows下用node编写的测试文件如下

1
2
3
4
5
6
7
8
9
10
11
12

// main.js
a = require("./vu")
console.log(a)

// vu.js
exports.a = 'root';
// module.exports.a = 'root';

// vu/index.js
exports.a = 'vu folder';
// module.exports.a = 'vu folder';

输出的结果为:显示为 同级目录xx.js优先级高于xx/index.js

1
2
mrli@VM-4-7-ubuntu:~/import_test$ node main.js 
{ a: 'root' }

参考:

编写注意点:

如果是在函数体内写的语句是不需要分号分隔的,如

1
2
3
4
5
6
7
8
9
10
setup(){
onMounted(()=>{

})

functipon xxx(){

}
return {}
}

与之对应的是在对象体内,如

1
2
3
4
5
6
7
8
9
10
11
var app = {
data() { // 等价于 data: ()=>{}

},
methods:{

},
props:{

},
}

里面会出现键值对必须要逗号分隔开

axios请求

  • 从浏览器中创建 XMLHttpRequest
  • 从 node.js 发出 http 请求
  • 支持promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF
  1. 直接每次从axios库中调用axios函数
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>
<div>
<div>mmmm</div>
</div>
</template>

<script>
import axios from 'axios'

export default {
name: "post",
created() {
/*
post常用的请求数据(data)格式有两种:
1)applicition/json
2)form-data 表单提交(图片上传,文件上传)
*/
//第一种写法叫做post别名请求方法
// http://localhost:8080/static/data.json?id=1
// applicition/json 请求
let data = {
id: 1
}
axios.post('../../static/data.json', data)
.then((res) => {
console.log('数据:', res);
})
//第二种写法
axios({
method: 'post',
url: '../../static/data.json',
data: data,
}).then((res) => {
console.log('数据:', res)
})
// form-data 请求
let formData = new FormData()
for (let key in data) {
formData.append(key, data[key])
}
axios.post('../../static/data.json', formData)
.then((res) => {
console.log('数据:', res);
})
}
}
</script>

<style scoped>
</style>

结合 vue使用vue-axios

npm install --save axios vue-axios
vue-axios是按照vue插件的方式去写的。那么结合vue-axios,就可以去使用vue.use方法了

1
2
3
4
5
6
// 首先在主入口文件main.js中引用

import axios from 'axios'
import VueAxios from 'vue-axios'

Vue.use(VueAxios,axios);

之后就可以使用了,在组件文件中的methods里去使用了

1
2
3
4
5
6
7
8
9
10
11


getNewsList(){
this.axios.get('api/getNewsList').then((response)=>{
this.newsList=response.data.data;
}).catch((response)=>{
console.log(response);
})


},

axios 改写为 Vue 的原型属性

首先在主入口文件main.js中引用,之后挂在vue的原型链上

1
2
import axios from 'axios'
Vue.prototype.$axios= axios

在组件中使用

1
2
3
4
5
this.$axios.get('api/getNewsList').then((response)=>{
this.newsList=response.data.data;
}).catch((response)=>{
console.log(response);
})

创建axios实例,并封装成工具类

1
2
3
4
5
6
7
8
// utils/requests.js
import axios from 'axios'

// 创建axios实例
const service = axios.create({
baseURL: process.env.BASE_API, // api的base_url
timeout: 5000 // 请求超时时间
})

组件中直接import

1
2
3
4
5
6
7
8
9
<script>
import { axios } import "./utils/requests.js"
axios.get('api/getNewsList').then((response)=>{
this.newsList=response.data.data;
}).catch((response)=>{
console.log(response);
})

</script>

axios进阶用法: 结合vuex再封装一层

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
import axios from 'axios'
import { Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'

// 创建axios实例
const service = axios.create({
baseURL: process.env.BASE_API, // api的base_url
timeout: 5000 // 请求超时时间
})

// request拦截器
service.interceptors.request.use(config => {
// Do something before request is sent
if (store.getters.token) {
config.headers['X-Token'] = getToken() // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
}
return config
}, error => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
})

// respone拦截器
service.interceptors.response.use(
response => response,
/**
* 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页
* 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
*/
// const res = response.data;
// if (res.code !== 20000) {
// Message({
// message: res.message,
// type: 'error',
// duration: 5 * 1000
// });
// // 50008:非法的token; 50012:其他客户端登录了; 50014:Token 过期了;
// if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// MessageBox.confirm('你已被登出,可以取消继续留在该页面,或者重新登录', '确定登出', {
// confirmButtonText: '重新登录',
// cancelButtonText: '取消',
// type: 'warning'
// }).then(() => {
// store.dispatch('FedLogOut').then(() => {
// location.reload();// 为了重新实例化vue-router对象 避免bug
// });
// })
// }
// return Promise.reject('error');
// } else {
// return response.data;
// }
error => {
console.log('err' + error)// for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
})

export default service

父子组件间通信

props, 父向子传递数据

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
// 父组件
<template>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/> // 这边传给子组件 :msg
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
name: 'App',
components: {
HelloWorld
}
}
</script>


// 子组件
<template>
<div class="hello">
<h1>{{ msg }}</h1>

</div>
</template>

<script>
import {onMounted} from "vue"
export default {
name: 'HelloWorld',
props: { // 需要指明接收的props
msg: String // 变量类型
},
setup(props){ // 然后这边传入
onMounted(()=>{
console.log(props)
})
}
}
</script>

<style scoped lang="scss">
h3 {
margin: 40px 0 0;
}
a {
color: $bg;
}
</style>

provide和inject, 祖向孙传递数据

通常,当我们需要从父组件向子组件传递数据时,我们使用 props。想象一下这样的结构:有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。
官方文档

  • 父组件通过 provide 选项来提供数据,
1
2
3
4
5
6
7
8
9
10
// optional api写法
provide: {
todoLength: this.todos.length // 将会导致错误 `Cannot read property 'length' of undefined`
},
// 要访问组件实例 property,我们需要将 provide 转换为**返回对象的函数**:
provide() {
return {
todoLength: this.todos.length
}
},

▲.处理响应性:
Q: 默认情况下,provide/inject 绑定并不是响应式的
A: 我们可以通过传递一个 ref property 或 reactive 对象给 provide 来改变这种行为。在我们的例子中,如果我们想对祖先组件中的更改做出响应,我们需要为 provide 的 todoLength 分配一个组合式 API computed property:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.component('todo-list', {
// ...
provide() {
return {
todoLength: Vue.computed(() => this.todos.length)
}
}
})

app.component('todo-list-statistics', {
inject: ['todoLength'],
created() {
console.log(`Injected property: ${this.todoLength.value}`) // > 注入的 property: 5
}
})
  • 子组件通过 inject 选项来开始使用这些数据。

完整案例:

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
// 父组件
<script>
import HelloWorld from './components/HelloWorld.vue'
import { ref, provide } from "vue"
export default {
name: 'App',
components: {
HelloWorld
},
setup(){
let a = ref("")
provide('a', a);

function show(){
console.log(a.value)
}
return {
a,
show
}
}
}
</script>
// 子组件
<template>
<div class="hello">
<h1>{{ msg }}</h1>
{{a}}
</div>
</template>

<script>
import {onMounted, inject} from "vue"
export default {
name: 'HelloWorld',
props: {
msg: String
},
// inject: ['a'], 也可以这么写, 这样在选项中定义了的话就不需要在setup中写了
setup(){
let a = inject("a")
return {
a
}
}
}

optional写法:

1
2
3
4
5
6
7
8
9
10
11
// 父组件, 要拿this必须写成方法, 不能写provide: {a: "45"}了
provide (){
return {
"a": this.a
}
},
data(){
return {
a: "45"
}
}

emits,子向父传递事件

  • emits参数有俩种形式对象数组,对象里面可以配置带校验emit事件,为null的时候代表不校验,校验的时候,会把emit事件的参数传到校验函数的参数里面
  • 当校验函数不通过的时候,控制台会输出一个警告,但是emit事件会继续执行
  • 记录一个坑:比如你emit事件的名称正好和原生事件的名字重复了,那么这个事件会执行俩次,那么配置了emits这个选项的话,就能很好的解决这个问题
  • 子组件Emiter.vue

使用

  • emits: 列表声明从父组件继承来的事件
  • $emit: 抛出事件, 通知父组件处理
  • 在子组件中,通过$emit()来触发事件
  • 在父组件中,通过v-on来监听子组件事件
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
// 子组件
<template>
<button @click="handleClick">点击emit-click事件</button>
<button @click="handleOpen">点击emit-open事件</button>
</template>
<script lang="ts">
import {defineComponent} from "vue";
export default defineComponent({
emits: { // 不声明这个也行, 但会有提示emits event listeners (clickx) were passed to component but could not be automatically inherited because component renders fragment or text root nodes. If the listener is intended to be a component custom event listener only, declare it using the "emits" option.
clickx: null, //click事件没有检验
open_show: (value) => {
if (typeof value === "string") {
return true;
} else {
return false;
}
},
},
setup(props, {emit}) { // 注意emit不是从vue中import的
const handleClick = function() {
emit("clickx");
};
const handleOpen = function() {
emit("open_show", 1); // 1为传递的参数
};
return {
handleClick,
handleOpen,
};
},
/**
setup(props, context){
const handleClick =function(){
context.emit("name")
}
}
**/
data() {
return {};
},
methods: {},
});
</script>
<style scoped></style>

父组件Emit.vue

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
<template>
// 3. 指定侦听的事件为子传上来的clickx和open_show, 并为其指定回调函数
<emiter @clickx="onClick" @open_show="onOpen"></emiter>
</template>
<script lang="ts">
import {defineComponent} from "vue"; // 1. 需要导入子component
import Emiter from "@/components/Emiter.vue";
export default defineComponent({
components: {
Emiter, // 2.注册子component,才能拿到emit传上来的open事件
},
data() {
return {};
},
methods: {
onClick() {
console.log("click me!");
},
onOpen() {
console.log("open me!");
},
},
});
</script>
<style scoped></style>

例子:

注意: 使用compositionAPI也是能有数据成员的比如props, name, emits

解决yarn全局安装模块后但仍提示无法找到命令的问题

如果使用yarn global add xxxx 安装了xxxx插件之后,但是在cmd窗口中仍然提示类似命令无法找到(Command not found)的错误(如@vue/cli后输入vue <create>没有提示,),一般是由于yarn的环境没有配置好。可以通过以下的方法,将yarn的环境配置环境中。

首先,先查看一下yarn的bin目录,

  • windows输入yarn global bin=>C:\Users\leon\AppData\Local\Yarn\bin然后将该路径加入到path中,对于windows中直接将该目录加入到path中。
  • 在Linux中which yarn后,将结果填入到~/.bashrc中,如下格式:export PATH=$PATH:/usr/bin/yarn

导入scss

  1. 安装scss/sass,node-sass sass-loader sass 属于重要依赖,所以需-D而不是-S;
1
2
3
npm install node-sass sass-loader sass -D
# or
yarn add node-sass sass-loader@8.0.2 sass
  1. 根路径下建立文件vue.config.js

    下面这个(不加也没事)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const path = require('path');
    module.exports = {
    pluginOptions: {
    'style-resources-loader': {
    preProcessor: 'scss',
    patterns: []
    }
    }
    }

    还有人提供的全局配置,实测可以

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    module.exports = {
    css: {
    loaderOptions: {
    sass: {
    prependData: `@import "./assets/css/test.scss";`
    // 等价于:prependData: `@import "@/assets/style.scss";`
    // 因为 配置中对import中的@进行了设置'@': resolve('src'),即@ 等价于 /src 这个目录
    }
    }
    },
    }

    如果不需要全局配置,可以直接在某个组件下的style里加@import "../assets/style.scss";

  2. 调用test

    1
    2
    3
    4
    5
    6
    <style lang="scss" scoped>
    $bg: red;
    .container {
    background-color: $bg;
    }
    </style>

from :

使用Vue3特性的工程:

  • ——较为基础的使用
  • ——略微复杂的前端工程, 采用vue3 compositionAPI
  • ——采用vue2的optionalAPI

vue.config.js模板配置文件

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
// vue.config.js 配置说明
//官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions
// 这里只列一部分,具体配置参考文档
module.exports = {
//别名设置
configureWebpack: {
resolve: {
alias: {
'assets': '@/assets',
'components': '@/components',
'views': '@/views',
}
}
},
// 部署生产环境和开发环境下的URL。
// 默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上
//例如 https://www.my-app.com/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.my-app.com/my-app/,则设置 baseUrl 为 /my-app/。
baseUrl: process.env.NODE_ENV === "production" ? "./" : "/",

// outputDir: 在npm run build 或 yarn build 时 ,生成文件的目录名称(要和baseUrl的生产环境路径一致)
outputDir: "dist",
//用于放置生成的静态资源 (js、css、img、fonts) 的;(项目打包之后,静态资源会放在这个文件夹下)
assetsDir: "assets",
//指定生成的 index.html 的输出路径 (打包之后,改变系统默认的index.html的文件名)
// indexPath: "myIndex.html",
//默认情况下,生成的静态资源在它们的文件名中包含了 hash 以便更好的控制缓存。你可以通过将这个选项设为 false 来关闭文件名哈希。(false的时候就是让原来的文件名不改变)
filenameHashing: false,

// lintOnSave:{ type:Boolean default:true } 问你是否使用eslint
lintOnSave: true,
//如果你想要在生产构建时禁用 eslint-loader,你可以用如下配置
// lintOnSave: process.env.NODE_ENV !== 'production',

//是否使用包含运行时编译器的 Vue 构建版本。设置为 true 后你就可以在 Vue 组件中使用 template 选项了,但是这会让你的应用额外增加 10kb 左右。(默认false)
// runtimeCompiler: false,

/**
* 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
* 打包之后发现map文件过大,项目文件体积很大,设置为false就可以不输出map文件
* map文件的作用在于:项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错。
* 有了map就可以像未加密的代码一样,准确的输出是哪一行哪一列有错。
* */
productionSourceMap: false,

// 它支持webPack-dev-server的所有选项
devServer: {
host: "localhost", //也可以直接写IP地址这样方便真机测试
port: 8080, // 端口号
https: false, // https:{type:Boolean}
open: true, //配置自动启动浏览器
// proxy: 'http://localhost:4000' // 配置跨域处理,只有一个代理

// 配置多个代理
proxy: {
"/api": {
target: "<url>", //写地址
ws: true, // 允许跨域
changeOrigin: true, //允许跨域
pathRewrite: {
"^/api": ""
}
},
"/foo": {
target: "<other_url>"
}
}
}
};

前端技术栈

文章:

Author: Mrli

Link: https://nymrli.top/2021/11/28/Vue3初体验/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

< PreviousPost
Vue中使用axios
NextPost >
前端-跨域
CATALOG
  1. 1. Vue3新特性
    1. 1.1. Composition-Api
      1. 1.1.1. setup() 函数
      2. 1.1.2. 具体函数
        1. 1.1.2.1. 数据
        2. 1.1.2.2. 函数
        3. 1.1.2.3. 侦听
    2. 1.2. Vue2->Vue3废弃的API
    3. 1.3. Vue optionalAPI可以使用
  2. 2. 附录
    1. 2.0.1. 导入vuex和vue-router
      1. 2.0.1.1. vue-router使用
      2. 2.0.1.2. vuex
  3. 2.1. 分析Vue3项目
    1. 2.1.1. Vue Color Avatar
      1. 2.1.1.1. index.js 中的 v-cloak 指令
  4. 2.2. 关于import
  5. 2.3. 编写注意点:
  6. 2.4. axios请求
    1. 2.4.1. 结合 vue使用vue-axios
    2. 2.4.2. axios 改写为 Vue 的原型属性
    3. 2.4.3. 创建axios实例,并封装成工具类
    4. 2.4.4. axios进阶用法: 结合vuex再封装一层
  7. 2.5. 父子组件间通信
    1. 2.5.1. props, 父向子传递数据
    2. 2.5.2. provide和inject, 祖向孙传递数据
    3. 2.5.3. emits,子向父传递事件
  8. 2.6. 解决yarn全局安装模块后但仍提示无法找到命令的问题
  9. 2.7. 导入scss
  • 3. 使用Vue3特性的工程:
  • 4. vue.config.js模板配置文件
  • 5. 前端技术栈
  • 6. 文章: