创建第一个 Vue3

279 阅读5分钟

Vue3 目前已经进入到了 Beta 阶段,距离正式版也不远了(兴奋的搓手手)。相信很多开发者已经开始自己试用起来了,但是在搭建过程中可能会出现一些问题,此文仅仅作为记录一下自己创建第一个 Hello World 过程。

注:该文章中 Vue 版本为 3.0.0-beta.15

准备阶段

本次搭建是在 vue-cli3 的基础上进行搭建的(人懒没办法)。

  1. 安装 vue-cli3

npm i -g @vue/cli

  1. 正常创建项目

vue create vue3

反正只是简单的起个项目,所以我把Eslint、Router、Vuex、Typescript都关了。此处无图,都开始看 3 了,创建项目的步骤就略去了。

  1. 添加 Vue3

因为目前 Vue3 还没有发布正式版,目前名称为 vue-next,所以添加的时候名称不要写错了。

vue add vue-next

  1. 启动项目

npm run serve

到这一步,恭喜你已经启动了一个报错的 Vue3 项目!

解决报错

首先看下浏览器中的报错

h is not a function

h is not a function 这个 h 是指的 src/main.js 中 render 函数的报错

// 报错代码
createApp({
  render: function (h) { return h(App) },
}).mount('#app')

// 解决办法
createApp(App).mount('#app')

原因:此处直接接受 component 组件,不用自己在去写render函数了。

// 源码位置:packages/runtime-core/src/apiCreateApp.ts

export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
    return function createApp(rootComponent, rootProps = null) {
        // 省略大段代码
        mount(rootContainer: HostElement, isHydrate?: boolean): any {
            const vnode = createVNode(rootComponent as Component, rootProps)
            // 省略大段代码
            // 看,这儿已经帮你render了
            render(vnode, rootContainer)
        }
    }
}

此处解决后,浏览器应该是显示出了正确的页面,因为 Vue3 是同时接受 Vue2 的配置式写法的。

折腾阶段

src/App.vue 源文件

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

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

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

钩子函数(或是叫生命周期)

我们知道 Vue3 中新增了一个 setup api,那么其执行顺序是什么呢?

import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  },
  beforeCreate() {console.log('beforecreate')},
  created() {console.log('created')},
  beforeMount() {console.log('beforeMount')},
  mounted() {console.log('mounted')},
  setup() {console.log('setup')}
}

测试结果:setup 是最先执行的。其余的钩子函数执行顺讯和 Vue2 并无区别。

life cycle

data/watch/computed

Vue3 中的各个 api 都已经独立了出来,可以根据自己的需求来引入。

监测基本类型

使用 ref 进行数据的监测,一般来说,使用 ref 进行基本数据的监测。

<template>
  <div id="app">
    <button @click="changeMsg1">msg1: {{msg1}}</button>
    <div>msg2: {{msg2}}</div>
    <div>msg3: {{msg3}}</div>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'
import { ref, watch, computed } from 'vue'

export default {
    mounted() {
      console.log(this);
      this.msg3 = 'mounted changed value'
    },
    setup() {
        const msg1 = ref(1);
        const changeMsg1 = () => {
            msg1.value += 1;
        };
        watch(msg1, (val, oldVal) => {
            console.log(`new val: ${val}, old val: ${oldVal}`);
        });

        const msg2 = computed(() => msg1.value * 2);

        const msg3 = ref();

        return {
            msg1,
            changeMsg1,
            msg2
        };
    }
};
</script>

看一下该段代码中的疑问:

  1. msg1.value 是个啥?
// 源码位置: packages/reactivity/src/ref.ts
export interface Ref<T = any> {
  [isRefSymbol]: true
  value: T
}

我们打印下 msg1:

ref msg1

由上面的接口可以看出 ref 对要监测的数据变成了一个 Object,如果要取值的话,需要使用 msg1.value 来设置/获取值。在 template 中可以直接使用 msg1。

  1. 如何 watch
// 源码位置: packages/runtime-core/src/apiWatch.ts
// 注意,源码中使用了函数重载,此处只写了一个
export function watch<T, Immediate extends Readonly<boolean> = false>(
  source: WatchSource<T>,
  cb: WatchCallback<T, Immediate extends true ? (T | undefined) : T>,
  options?: WatchOptions<Immediate>
): StopHandle

使用方法:

watch(msg1, (val, oldVal) => {
  console.log(`new val: ${val}, old val: ${oldVal}`);
});

watch 实际上与 Vue2 并无区别,只不过将其拆了出来。

  1. 如何 computed

使用方法:

const msg2 = computed(() => msg1.value * 2);

computed 也是和 Vue2 一样,使用一个函数的返回值作为其值,故 computed 需要return。

  1. return 的是什么?

此处 return 的是被监测的值。简单来说就是:如果你需要在模板文件或其他hook函数中使用这个值,那么你就需要将其 return。故 watch 不需要 return;computed 需要 return,因为你要使用其返回的值。

  1. 如何在其他生命周期中修改被监测的值呢?

上面第四条已经说过如果要使用该值,那么你就需要将其 return

我们试着在其他 hook 函数中打印下 this

ref return

这时候我们可以看到 this 是个 Proxy 对象,而我们 return 的值已经被代理了,那么我们在使用或修改该值的时候,直接 this.msg3 = 'modify' 就可以了。

这时候可能就有人要问:为啥我在 setup 中修改需要使用 this.msg.value ,在 hook 函数中只需要 this.msg 就行了呢?

那是因为在 setup 中的可以确定返回的就是一个对象,而将其 return 后,他被挂载到了 proxy 对象上,在获取的时候就可以正常获取了。(这儿没去看源码,个人猜的)

监测对象

<template>
  <div id="app">
    <button @click="changeMsg1">msg1: {{msg1.val}}</button>
    <div>msg2: {{msg1.msg2}}</div>
  </div>
</template>

<script>
import { reactive, watch, computed } from 'vue'

export default {
    mounted() {
    },
    setup() {
        const msg1 = reactive({
          val: 1,
          msg2: computed(() => msg1.val * 2)
        });

        console.log(msg1);

        watch(msg1, (val, oldVal) => {
            console.log(val === oldVal); // true
            console.log(`new val: ${val.val}, old val: ${oldVal.val}`);
        });
        const changeMsg1 = () => {
            msg1.val += 1;
        };

        return {
            msg1,
            changeMsg1
        };
    }
};
</script>

解释一下该段代码:

  1. msg1 也就是我们监测的对象。由于 computed 已经可以单独使用了,所以我们把它拆出来写到 reactive 里面。
  2. 看一下 msg1 是个啥
    reactive data

可以看到 msg1 是一个 Proxy 对象,而被 computed 的 msg2 是一个 ref 对象。

  1. 如果要 watch 的话,和 ref 的使用方法一样,我们打印一下这两个对象

这儿我们可以看到一个新值和老值。桥豆麻袋,老值和新值怎么相等了……额,这是bug吧o(╥﹏╥)o,github 上问一下,等有结果了后续更新。

  1. 没了吧。

component

那么如何引入 components 呢?答:和之前没有任何区别。

<template>
  <div id="app">
      <HelloWorld />
  </div>
</template>

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

export default {
    components: {
        HelloWorld
    }
};
</script>

结束语

虽然内容很简单,还是希望能够帮助一些不知道如何建项目的同学吧。