vue3.0初窥

276 阅读7分钟

vite构建demo

优点

1.基于浏览器原生ES imports 的开发服务器(利用浏览器去解析imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用)

2.同时不仅有Vue文件支持,还支持热更新,而且热更新的速度不会随着模块增多而变慢

构建指令

npm init vite-app name
cd name
npm run dev/build

与cli的不同点

1.html页面没放在public下了,挪到了根目录下

2.vite构建是将所有的代码都放在页面模板里面的一个type=module的script标签中

<script type="module" src="/src/main.js"></script>

3.在这个script标签中间,可以直接通过import的方式引用,而不需要像webpack那样先进行打包编译了

Composition Api (响应式系统Api)

官方文档

setup

概念

setup函数是一个新的的组件选项,作为在组件内使用Composition Api的入口点

特性

1.在初始化props和beforeCreated之前调用 2.接收props和context两个参数

  • props 父组件传进来的基于Proxy代理的响应式数据,和vue2.0的也差不多
  • context 可以拿到一些属性像attrs、slots、emit 3.this在setup里面不可用 4.只会在初次加载是执行一次,数据变更不会再次执行 5.如果setup返回一个对象,则对象的属性可以在模板中进行渲染

组合式api

使用之前需要从vue导出来

import {  watchEffect, computed, reactive, readonly, ref, watch } from 'vue'
ref

将单个简单值构建为响应式对象,在setup里使用时,要名字.value的方式操作,在标签中使用可以直接用名称

<template>
  <div>
    <p>支持人数:{{ supNum }}</p>
    <p>反对人数:{{ oppNum }}</p>
    <p>支持率:{{ ratio }}</p>
    <button @click="change(0)">支持</button>
    <button @click="change(1)">反对</button>
  </div>
</template>

<script>
import { ref } from 'vue'
export default {
  setup (props, context) {
    let supNum = ref(0) // ref的括号里面包裹初始值value
    let oppNum = ref(0)
    let ratio = ref(0)
    function change (type) {
      type ? oppNum.value++ : supNum.value++
      let total = oppNum.value + supNum.value
      ratio.value = (((supNum.value / total) * 100).toFixed(2)) + '%'
    }
    return {
      supNum, oppNum, ratio, change
    }
  }
}
</script>
reactive

1.将复杂值构建为响应式对象,有点像vue2.0的data,里面的值都是响应式的

2.在setup里使用要以对象的形式使用,在模板使用可以在导出时,先用toRefs把响应式对象转换为普通对象,让每个属性都是一个ref,再解耦导出

<template>
  <div>
    <p>支持人数:{{ supNum }}</p>
    <p>反对人数:{{ oppNum }}</p>
    <p>支持率:{{ ratio }}</p>
    <button @click="change(0)">支持</button>
    <button @click="change(1)">反对</button>
  </div>
</template>

<script>
import { reactive, ref, toRefs } from 'vue'
export default {
  setup (props, context) {
    let state = reactive({
      supNum: 0,
      oppNum: 0,
      ratio: 0,
    })
    function change (type) {
      type ? state.oppNum++ : state.supNum++
      let total = state.oppNum + state.supNum
      state.ratio = (((state.supNum / total) * 100).toFixed(2)) + '%'
    }
    return {
      // 为了可以直接在模板渲染时直接用,先解耦
      // 导出的时候,要把响应式对象转换成普通对象,但每个属性都是一个ref
      ...toRefs(state),
      change
    }
  }
}
</script>
readonly

传入一个对象(响应式或普通)或 ref,返回一个原始对象的只读代理(所有层级所有属性都是只读),在代码中再修改这个对象或ref,都会报提示,可以用来设置一些不允许修改,但很多组件都要用到的公共数据

<template>
  <div>
    <p>支持人数:{{ supNum }}</p>
    <p>反对人数:{{ oppNum }}</p>
    <p>支持率:{{ ratio }}</p>
    <button @click="change(0)">支持</button>
    <button @click="change(1)">反对</button>
  </div>
</template>

<script>
import { reactive, readonly, ref, toRefs } from 'vue'
export default {
  setup (props, context) {
    // 把state设置为只读
    let state = readonly(
      // 创建一个响应式对象
      reactive({
        supNum: 0,
        oppNum: 0,
        ratio: 0,
      })
    )
    function change (type) {
      type ? state.oppNum++ : state.supNum++
      let total = state.oppNum + state.supNum
      state.ratio = (((state.supNum / total) * 100).toFixed(2)) + '%'
    }
    return {
      // 为了可以直接在模板渲染时直接用,先解耦
      // 导出的时候,要把响应式对象转换成普通对象,但每个属性都是一个ref
      ...toRefs(state),
      change
    }
  }
}
</script>

点击按钮的话,会报这是只读属性

computed

1.传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象,直接name.value++就会报错,感觉vue2.0也差不多

<template>
  <div>
    <p>支持人数:{{ supNum }}</p>
    <p>反对人数:{{ oppNum }}</p>
    <p>支持率:{{ ratio }}</p>
    <button @click="change(0)">支持</button>
    <button @click="change(1)">反对</button>
  </div>
</template>

<script>
import { computed, reactive, readonly, ref, toRefs } from 'vue'
export default {
  setup (props, context) {
    // 创建一个响应式对象
    let state = reactive({
      supNum: 0,
      oppNum: 0,
    })
    // 传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象。
    let ratio = computed(() => {
      let total = state.oppNum + state.supNum
      return total > 0 ? ((state.supNum / total) * 100).toFixed(2) + '%' : '--'
    })
    ratio.value++ // 这样会报错
    function change (type) {
      type ? state.oppNum++ : state.supNum++
    }
    return {
      // 为了可以直接在模板渲染时直接用,先解耦
      // 导出的时候,要把响应式对象转换成普通对象,但每个属性都是一个ref
      ...toRefs(state),
      ratio,
      change
    }
  }
}
</script>

2.或者传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态,这种用的少

// 传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态。
let ratio = computed({
  get: () => {
    let total = state.oppNum + state.supNum
    return total > 0 ? ((state.supNum / total) * 100).toFixed(2) + '%' : '--'
  },
  set: (val) => {
    console.log(val);
  }
})
ratio.value++ //  这样会执行computed的set函数
watchEffect

1.立即执行传入的一个函数,就是一进来就会执行一次

2.并响应式追踪其依赖,就是在它里面用到了的变量修改了都会执行这个方法,它里面打印了的变量修改了也会执行这个方法

<template>
  <div>
    <p>支持人数:{{ supNum }}</p>
    <p>反对人数:{{ oppNum }}</p>
    <p>支持率:{{ ratio }}</p>
    <button @click="change(0)">支持</button>
    <button @click="change(1)">反对</button>
  </div>
</template>

<script>
import { watchEffect, computed, reactive, readonly, ref, toRefs } from 'vue'
export default {
  setup (props, context) {
    // 创建一个响应式对象
    let state = reactive({
      supNum: 0,
      oppNum: 0,
      ratio: 0
    })
    // 立即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变更时重新运行该函数
    watchEffect(() => {
      console.log(state.supNum, 'state.supNum', state.oppNum) //打印了的变量修改了也会执行 
      let total = state.oppNum + state.supNum //用到了的变量修改了都会执行这个方法
      state.ratio = total > 0 ? ((state.supNum / total) * 100).toFixed(2) + '%' : '--'
    })
    function change (type) {
      type ? state.oppNum++ : state.supNum++
    }
    return {
      // 为了可以直接在模板渲染时直接用,先解耦
      // 导出的时候,要把响应式对象转换成普通对象,但每个属性都是一个ref
      ...toRefs(state),
      change
    }
  }
}
</script>
watch

1.watch API 完全等效于vue2.x的watch。侦听指定的变量,变量修改执行回调

2.它可以监听响应式对象和ref,第一个参数放监听对象,但如果是监听响应式对象里面具体的一个属性,需要写成函数的返回该属性的形式,第二个参数是回调函数,第三个参数是配置immediate立即执行和deep深度监听都有

// 监听对象
watch(state,
  (val, oldVal) => {
    console.log(val, oldVal, 'val,oldVal')
  },
  {
    immediate: true,
    deep: true
  })
// 监听对象中的属性
watch(()=>state.supNum, (val, oldVal) => {
  console.log(val, oldVal, 'val,oldVal')
})

3.可以将多个变量用一个watch进行监控,只要它监控的变量里面任意一个发生改变都会执行回调用,具体就是将原来的单个参数改为数组,放多个参数

// 将多个变量用一个watch进行监控,只要它监控的变量里面任意一个发生改变都会执行回调用
watch([() => state.supNum, () => state.oppNum], ([val, val2], [oldVal, oldVal2]) => {
  console.log([val, val2], [oldVal, oldVal2], '[val, val2], [oldVal, oldVal2]')
})

响应式系统工具集

总共七个,基本上从字面上看就可以判断出功能,都是用于类型判断和类型转换

import { isProxy, isReactive, isReadonly, isRef, toRef, toRefs, unref } from 'vue'
  1. unref 如果参数是一个 ref 则返回它的 value,否则返回参数本身

  2. toRef、toRefs,一个是将reactive对象的属性创建一个ref,一个是统一处理,将reactive对象的每一个属性创建为个ref

3.isProxy、isReactive、isReadonly、isRef判断是否是各自名称对应的类型,返回布尔值

生命周期钩子函数

1.基本上与之前vue2.x的类似,只是需要从vue导入,并且在setup中以on钩子名的方式调用

import { onMounted, onUpdated, onUnmounted } from 'vue'
const MyComponent = {
  setup() {
    onMounted(() => {
      console.log('mounted!')
    })
    onUpdated(() => {
      console.log('updated!')
    })
    onUnmounted(() => {
      console.log('unmounted!')
    })
  },
}
  1. vue2.x和vue3.0名称对应关系

依赖注入

provide 和 inject 提供依赖注入,功能类似 2.x 的 provide/inject。两者都只能在当前活动组件实例的 setup() 中调用。

import { provide, inject } from 'vue'

const ThemeSymbol = Symbol()

const Ancestor = {
  setup() {
    provide(ThemeSymbol, 'dark')
  },
}

const Descendent = {
  setup() {
    const theme = inject(ThemeSymbol, 'light' /* optional default value */)
    return {
      theme,
    }
  },
}

模板Refs

当使用组合式 API 时,reactive refs 和 template refs 的概念已经是统一的。为了获得对模板内元素或组件实例的引用,我们可以像往常一样在 setup() 中声明一个 ref 并返回它:

<template>
  <div ref="root"></div>
</template>

<script>
  import { ref, onMounted } from 'vue'

  export default {
    setup() {
      const root = ref(null)

      onMounted(() => {
        // 在渲染完成后, 这个 div DOM 会被赋值给 root ref 对象
        console.log(root.value) // <div/>
      })

      return {
        root,
      }
    },
  }
</script>

使用vue脚手架

1.cli版本需要大于4.0

2.选择配置

3.选择vue版本

4.安装axios和ui框架Ant Design of Vue2.x版本

cnpm install axios ant-design-vue --save

5.main.js入口文件

// 入口文件
// 和2.0不一样的地方,导出了createApp
import { createApp } from 'vue'
import App from './App.vue'
import './registerServiceWorker'
import router from './router'
import store from './store'

// vue3都是函数,通过函数的方式去创建和挂载
createApp(App).use(store).use(router).mount('#app')

创建和挂载这里可以改成和vue2.0像一点的方式

const vue  =  createApp(App)
vue.use(store)
vue.use(router)
vue.mount('#app')

6.app.vue

<template>
  <a-layout id="components-layout-demo-fixed">
    <a-layout-header :style="{ position: 'fixed', zIndex: 1, width: '100%' }">
      <div class="logo" />
      <!-- v-model:selectedKeys="selectedKeys"表示传了两个属性,
      一个叫value,一个叫selecteKeys -->
      <a-menu
        theme="dark"
        mode="horizontal"
        v-model:selectedKeys="selectedKeys"
        :style="{ lineHeight: '64px' }"
      >
        <a-menu-item key="/">
          <router-link to="/">首页</router-link>
        </a-menu-item>
        <a-menu-item key="/plan">
          <router-link to="/plan">时间计划</router-link>
        </a-menu-item>
      </a-menu>
    </a-layout-header>
    <a-layout-content :style="{ padding: '0 50px', marginTop: '64px' }">
      <a-breadcrumb :style="{ margin: '16px 0' }"> </a-breadcrumb>
      <div :style="{ background: '#fff', padding: '24px', minHeight: '380px' }">
        <router-view></router-view>
      </div>
    </a-layout-content>
    <a-layout-footer :style="{ textAlign: 'center' }">
      Ant Design ©2018 Created by Ant UED
    </a-layout-footer>
  </a-layout>
</template>
<script>
// 需要在开始把要用到的方法引入进来
//       挂载钩子            监控   双向绑定用的reactive方法
import { onMounted, toRefs, ref, watch, reactive, computed } from 'vue'
import { useRoute } from 'vue-router'
import { useStore } from 'vuex'
// vue3使用的是函数式api,做混入更方便
export default {
  // 入口函数,默认只执行一次,可以在入参里拿到属性和上下文,我们可以把所有的逻辑都放到setup里,比较好管理
  setup (props, context) { // context里面可以拿到一些属性像attrs、slots、emit
    // 执行vuex方法
    const store = useStore()
    // 执行路由方法
    const route = useRoute()
    //使用reactive方法创建响应式数据
    const state = reactive({
      // 计算属性 
      selectedKeys: computed(() => {
        return [route.path]
      }),
      // 单独将某个属性变成响应式的
      allTime: ref(store.getters.allTime)
    })
    /**
     * @description: 监控某个属性
     * @param {type} 一个拥有返回值的 getter 函数,也可以是 ref
     * @param {type} 回调,参数和vue2一样
     * @param {type} 对象,参数配置,和vue2一样
     * @author: dlb
     */
    // watch(() => route.path, (newValue, oldValue) => {
    //   console.log(newValue, 'newValue')
    //   state.selectedKeys = [newValue]
    // }, {
    //   immediate: true,
    //   deep: true
    // })



    // 新的写法所有的钩子都是函数
    onMounted(() => {
      console.log('mounted!')
    })


    // 如果 setup 返回一个对象,则对象的属性将会被合并到组件模板的渲染上下文
    return {
      // 将响应式数据state返回
      // toRef 可以用来为一个 reactive 对象的属性创建一个 ref。这个 ref 可以被传递并且能够保持响应性。
      ...toRefs(state),
    }
    // 这里如果返回的时函数,return的内容会覆盖掉template的内容
    // return ()=>{
    //   return <h1>998</h1>
    // }
  },
};
</script>
<style>
#components-layout-demo-fixed .logo {
  width: 120px;
  height: 31px;
  background: rgba(255, 255, 255, 0.2);
  margin: 16px 24px 16px 0;
  float: left;
}
</style>

vue-router

vue3.0对应的vue-router版本是4.0.0-alpha.xx版本,它的使用和3.0版本略微有些差别

// router文件夹下的index.js
import {
  createRouter,
  createWebHashHistory
} from 'vue-router';
import Home from '../views/Home.vue';
import Detail from '../views/Detail.vue';

const router = createRouter({
  history: createWebHashHistory(),
  routes: [{
    path: '/',
    component: Home
  }, {
    path: '/detail/:id',
    component: Detail
  }]
});
export default router;

axios

axios的封装基本上还和vue2.0时一样

import axios from 'axios';
import qs from 'qs';

axios.defaults.baseURL = "";
axios.defaults.withCredentials = true;
axios.defaults.headers["Content-Type"] = "application/x-www-form-urlencoded";
axios.defaults.transformRequest = data => qs.stringify(data);
axios.interceptors.request.use(config => {
	return config;
});
axios.interceptors.response.use(response => {
	return response.data;
}, reason => {
	let response = reason.response;
	if (response) {
		// 不同状态码下的统一操作
		switch (parseInt(response.status)) {
			case 400:
				break;
			case 401:
				break;
			case 403:
				break;
			case 404:
				break;
		}
	} else {
		if (!window.navigator.onLine) {
			// 断网处理
		}
	}
	return Promise.reject(reason);
});
export default axios;

组件demo

编码思路和之前vue2.0时基本上一样,只是api修改而已,同样是在钩子里请求数据

<template>
  <div class="homeBox">
    <!-- HEADER -->
    <header class="headerBox">
      <div class="base">
        <span class="time">
          {{day}}
          <em>{{month}}月</em>
        </span>
        <h1 class="title">知乎日报</h1>
      </div>
      <div class="user"></div>
    </header>

    <!-- BANNER -->
    <div class="bannerBox">
      <div class="swiper-container" v-if="bannerData.length>0">
        <div class="swiper-wrapper">
          <div class="swiper-slide" v-for="item in bannerData" :key="item.id">
            <router-link :to="{path:`/detail/${item.id}`}">
              <img :src="item.image" alt />
              <div class="desc">
                <p>{{item.title}}</p>
                <p>{{item.hint}}</p>
              </div>
            </router-link>
          </div>
        </div>
        <div class="swiper-pagination"></div>
      </div>
    </div>

    <!-- LIST -->
    <div class="newsBox" v-if="newsData.length>0">
      <!-- START -->
      <div class="itemBox" v-for="(item,index) in newsData" :key="index">
        <h4 class="time" v-if="index!==0">
          <span>{{filterTime(item.time)}}</span>
          <i></i>
        </h4>

        <ul class="content">
          <li class="item" v-for="news in item.stories" :key="news.id">
            <router-link :to="{path:`/detail/${news.id}`}">
              <div class="con">
                <h4>{{news.title}}</h4>
                <span>{{news.hint}}</span>
              </div>
              <div class="img">
                <img alt :src="news.images[0]" />
              </div>
            </router-link>
          </li>
        </ul>
      </div>
      <!-- END -->
    </div>
  </div>
</template>

<script>
import API from "../api";
import utils from "../assets/utils";
import Swiper from "swiper";
import "swiper/css/swiper.min.css";
import {
  reactive,
  toRefs,
  computed,
  onBeforeMount,
  watchEffect,
  watch
} from "vue";

function init_swiper() {
  new Swiper(".swiper-container", {
    loop: true,
    pagination: {
      el: ".swiper-pagination"
    }
  });
}

export default {
  name: "Home",
  setup() {
    // 构建响应式数据
    let state = reactive({
      date: new Date(),
      bannerData: [],
      newsData: []
    });

    // 基于计算属性构建月和日
    let month = computed(() => utils.formatTime(state.date)[1]);
    let day = computed(() => utils.formatTime(state.date)[2]);

    // 在第一次渲染组件之前,从服务器获取数据
    onBeforeMount(async () => {
      let { date, stories, top_stories } = await API.zhihu.API_LATEST();
      // 日期处理
      state.date = utils.convertTime(date);
      // 新闻数据处理
      state.newsData.push({
        time: state.date,
        stories
      });
      // 轮播图数据处理
      state.bannerData = top_stories;
    });

    // 监听轮播图数据改变,在数据改变后初始化SWIPER
    watch(
      () => state.bannerData,
      () => {
        init_swiper();
      }
    );

    // 过滤日期方法
    const filterTime = time => {
      time = utils.formatTime(time);
      return `${time[1]}月${time[2]}日`;
    };

    // 暴露给模板使用
    return {
      ...toRefs(state),
      month,
      day,
      filterTime
    };
  }
};
</script>

<style lang="less" scoped>
@A: #aaa;

.headerBox {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.2rem;

  .base {
    font-size: 0;

    .time,
    .title {
      display: inline-block;
      padding: 0 0.3rem;
    }

    .time {
      text-align: center;
      font-size: 0.34rem;
      font-weight: bold;
      vertical-align: middle;

      em {
        display: block;
        font-size: 0.24rem;
        font-weight: normal;
        font-style: normal;
      }
    }

    .title {
      font-size: 0.4rem;
      vertical-align: middle;
      border-left: 0.02rem solid @A;
    }
  }

  .user {
    width: 0.6rem;
    height: 0.6rem;
    border-radius: 50%;
    background: url("../assets/images/timg.jpg") no-repeat center center;
    background-size: 100% 100%;
    overflow: hidden;

    img {
      width: 100%;
      height: 100%;
      border-radius: 50%;
    }

    img[src=""] {
      display: none;
    }
  }
}

.bannerBox {
  height: 7.5rem;
  background: lighten(@A, 20%);

  .swiper-container {
    height: 100%;
  }

  .swiper-slide {
    display: block;
    position: relative;
    z-index: 9999;
    height: 100%;
    overflow: hidden;

    a {
      display: block;
    }

    img {
      display: block;
      width: 100%;
      height: 100%;
    }

    .desc {
      box-sizing: border-box;
      position: absolute;
      bottom: 0;
      left: 0;
      z-index: 9999;

      padding: 0.3rem;
      width: 100%;
      height: 2rem;
      overflow: hidden;
      background: rgba(0, 0, 0, 0.7);
      background: -webkit-linear-gradient(
        top,
        rgba(0, 0, 0, 0),
        rgba(0, 0, 0, 0.7)
      );

      p {
        font-size: 0.4rem;
        color: rgba(255, 255, 255, 0.85);
        font-weight: bold;
        line-height: 0.8rem;
        text-overflow: ellipsis;
        white-space: nowrap;
        overflow: hidden;

        &:nth-child(2) {
          font-size: 0.32rem;
          font-weight: normal;
          line-height: 0.6rem;
        }
      }
    }
  }
}

.newsBox {
  padding: 0.2rem;

  .itemBox {
    .time {
      box-sizing: border-box;
      position: relative;
      height: 0.6rem;
      line-height: 0.6rem;
      padding-top: 0.285rem;

      i {
        display: block;
        margin-left: 1.8rem;
        height: 0.02rem;
        background: lighten(@A, 20%);
      }

      span {
        position: absolute;
        top: 0;
        left: 0;
        width: 1.6rem;
        text-align: center;
        font-size: 0.3rem;
        font-weight: normal;
        color: darken(@A, 20%);
      }
    }

    .content {
      .item {
        position: relative;
        padding: 0.2rem 0;
        min-height: 1.5rem;

        a {
          display: block;
        }

        .img {
          position: absolute;
          top: 0.2rem;
          right: 0;
          width: 1.5rem;
          height: 1.5rem;
          border-radius: 0.05rem;
          background: lighten(@A, 20%);
          overflow: hidden;

          img {
            width: 100%;
            height: 100%;
            border-radius: 0.05rem;
          }
        }

        .con {
          margin-right: 1.7rem;

          h4,
          span {
            display: block;
            color: #000;
            font-size: 0.32rem;
            font-weight: normal;
            line-height: 0.5rem;
            max-height: 1rem;
            overflow: hidden;
          }

          span {
            color: darken(@A, 20%);
            font-size: 0.28rem;
          }
        }
      }
    }
  }
}
</style>

vue3.0废弃了过滤器,可用函数替代

api请求环境控制(根据不同的环境,获取不同的路径)

1.在vue项目的根目录上创建以".env.环境 "命名的文件,例入开发:.env.development

2.在运行开发或者构建命令时,它默认会自动的去读这个文件的变量,这个变量必需以VUE_APP_开头,后面可以用自己定义的变量,例如:VUE_APP_URL

VUE_APP_URL='http://192.168.3.43:9527'

3.获取时,在任意文件里用 process.env.都可以直接去取以VUE_APP_开头的变量,例如process.env.VUE_APP_URL

3.文件的读取有优先级,默认是先读.env文件,如果没有的话,再读.env.development