【Vue3.0】还学得动吗?赶紧和我过一遍用法吧!

写在前面

2020.4.20 更新:目前Vue3.0 已经出于 Beta 测试阶段,即可以对开发者开放使用了,官方工具链如vue-routervuex 等仍处于 Alpha 阶段,距离正式发布还有一段时间。
本篇文章发布于2020.01.14,如果后续更新/发布的话,会考虑更新这篇文章的内容。

阅读依赖

本篇文章默认读者已经了解以下知识,没有掌握的请去补课~

  • Vue2.x的用法
  • npm的安装与构建,npx局部安装替代全局安装方法(了解最好,非必须)
  • git相关操作
  • react hooks相关知识(了解最好,非必须)

Vue3.0介绍

其他文章已经说得差不多了,几个核心点就是Proxy/函数式API/TS支持以及模板编译优化,不再赘述,想看源码的去github拉代码即可:
https://github.com/vuejs/vue-next

快速开始

使用 @vue/cli 构建

需要 vue-cli 版本大于 3.x ,如果是2.x版本,得先升级依赖。
的这里只介绍全局安装方法:

npm i -g @vue/cli
vue create my-vue3-app

注意:目前不建议选择 Typescript 预设,因为 vuex vue-router 等工具尚未完全支持(当然精力旺盛的可以自己实现)。
项目初始化完毕,进入 my-vue3-app 目录,执行 vue-next 安装命令:

vue add vue-next

这个命令可以自动运行安装脚本,将你当前 vue 项目升级为 vue 3.0 项目:

📦  Installing vue-cli-plugin-vue-next...

yarn add v1.22.4
[1/4] Resolving packages...
[2/4] Fetching packages...

success Saved 1 new dependency.
info Direct dependencies
└─ vue-cli-plugin-vue-next@0.1.2
info All dependencies
└─ vue-cli-plugin-vue-next@0.1.2
Done in 24.71s.
✔  Successfully installed plugin: vue-cli-plugin-vue-next


🚀  Invoking generator for vue-cli-plugin-vue-next...
⚓  Running completion hooks...

✔  Successfully invoked generator for plugin: vue-cli-plugin-vue-next
 vue-next  Installed vuex 4.0.
 vue-next  Documentation available at https://github.com/vuejs/vuex/tree/4.0
 vue-next  Installed vue-router 4.0.
 vue-next  Documentation available at https://github.com/vuejs/vue-router-next
 vue-next  Installed @vue/test-utils 2.0.
 vue-next  Documentation available at https://github.com/vuejs/vue-test-utils-next

happy coding.

从源码构建

首先把代码拉到本地(默认使用master分支即可),在根目录下执行npm install && npm run build, 就能在/packages/vue/dist下得到打包后的文件:

vue.cjs.js
vue.esm-bundler.j s
vue.esm.prod.js
vue.global.prod.js
vue.cjs.prod.js
vue.esm.js
vue.global.js
vue.runtime.esm-bundler.js

浏览器引入使用

如果在纯浏览器环境下,我们选用上面的vue.global.js作为依赖,因为它包含了开发提示以及template编译器。直接来一段小demo:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue3.0 sample</title>
  <!--  在浏览器下template可以这么写了  -->
  <script type="text/x-template" id="greeting">
    <h3>{{ message }}</h3>
  </script>
  <script src="vue.global.js"></script>
</head>
<body>
  <div id="app"></div>
  <script type="text/javascript">
    const { createApp } = Vue;
    const myApp = {
      template: '#greeting',
      data() {
        return {
          message: 'Hello Vue3.0'
        }
      }
    }
    createApp(myApp).mount('#app')
  </script>
</body>
</html>

浏览器中打开,你将看到如下文字:

Hello Vue3.0

可以看到:new Vue变成了createApp,不再接受option参数,而是搬到了mount方法中。我们的template字符串,现在可以使用text/x-template的脚本格式引入,至于其他的用法,基本和2.0一模一样
另外,源码仓库中 /packages/vue/examples目录下,提供了几个官方示例,有兴趣的可以去参阅

使用webpack构建3.0的sfc

仔细读读仓库中的readme.md,我们发现尤大已经很贴心地为我们做了一个webpack构建项目的最小实践: https://github.com/vuejs/vue-next-webpack-preview
同样的操作:拉代码->构建->运行后,看到的是一个点击计数器的基本用法:

<template>
  <img src="./logo.png">
  <h1>Hello Vue 3!</h1>
  <button @click="inc">Clicked {{ count }} times.</button>
</template>

<script type="text/javascript">
import { ref } from 'vue'
export default {
  setup() {
    const count = ref(0); // 响应式数据
    // 事件回调不需要任何处理,直接作为对象方法返回即可;
    const inc = () => {
      count.value++
    }
    return {
      count,
      inc
    }
  }
}
</script>

与2.0不同了,这个setup方法就是3.0的新API,定义了一个响应式属性count和方法inc,将他们作为对象返回,就能在template中使用。其中,ref是对一个原始值添加响应式(装箱),通过.value获取和修改(拆箱),对应2.0中的data,如果需要对一个对象/数组添加响应式,则需要调用reactive()方法

import { reactive } from 'vue'
export default {
  setup() {
    return {
      foo: reactive({ a: 1, b: 2 })
    }
  }
}

拓展实践

为了更进一步理解setup,我们改造一下点击计数器——键盘计数器。
要实现的目标和思路为:

  • 将计数器变成一个组件,由外部控制开启/关闭: componentrefv-if的使用
  • 计数器监听某个键盘按键,按键名称由父组件作为props传入(如Enter,Space等): setup(props)获取
  • 组件渲染(onMounted)后开始监听,组件拆卸(onUnmounted)后取消监听:生命周期钩子在3.0中的用法
  • 添加is-odd文本,表示按键次数是否为奇数:computedvue3.0中的用法
  • 按键次数为5的倍数(0除外)时,弹出alert窗口:watch在vue3.0中的用法

Talk is cheap, show me the code

首先是改造App.vue父组件导入key-press-enter子组件,注意看template有何变化

<template>
    <!--  设置checkbox控制组件开关  -->
    <input id="key-counter" type="checkbox" v-model="enableKeyPressCounter">
    <label for="key-counter">check to enable keypress counter</label>
    <key-press-counter v-if="enableKeyPressCounter" key-name="Enter" />
</template>

<script type="text/javascript">
  import { ref } from 'vue'
  import KeyPressCounter from './KeyPressCounter.vue';

  export default {
    components: {
      KeyPressCounter // 组件用法和原来一致
    },
    setup() {
      return {
        enableKeyPressCounter: ref(false), // 是否开启组件
      }
    }
  }
</script>

可以发现:template现在可以像jsx一样作为碎片引入,不需要添加根元素了(当然#app根容器还是需要的)
接着是子组件KeyPressCounter.vue

<script>
 <template>
  <h3>Listening keypress: {{ keyName }}</h3>
  <p>Press {{ pressCount }} times!</p>
  <p>Is odd times: {{ isOdd }}</p>
</template>

<script type="text/javascript">
  import { onMounted, onUnmounted, ref, effect, computed } from 'vue';

  /**
   * 创建一个键盘按键监听钩子
   * @param keyName 按键名称
   * @param callback 监听回调
   */
  const useKeyboardListener = (keyName, callback) => {
    const listener = (e) =>{
      console.log(`你按下了${e.key}键`) // 用来验证监听时间是否开启/关闭
      if (e.key !== keyName) {
        return;
      }
      callback()
    }
    // 当组件渲染时监听
    onMounted(()=>{
      document.addEventListener('keydown', listener)
    })
    // 当组件拆解时监听
    onUnmounted(()=>{
      document.removeEventListener('keydown', listener)
    })
  }

  export default {
    name: "EnterCounter",
    /**
     * @param props 父组件传入的props
     * @return { Object } 返回的对象可以在template中引用
     */
    setup(props) {
      const { keyName } = props
      const pressCount = ref(0)
      // hooks调用
      useKeyboardListener(keyName, ()=>{
        pressCount.value += 1;
      })
      // watch的用法,可以看到,现在无需声明字段或者source,vue自动追踪依赖
      effect(()=>{
        if (pressCount.value && pressCount.value % 5 === 0) {
          alert(`you have press ${pressCount.value} times!`)
        }
      })
      // computed的用法,基本是原来的配方
      const isOdd = computed(()=> pressCount.value % 2 === 1)

      return {
        keyName,
        pressCount,
        isOdd
      }
    }
  }
</script>

以后编写组件就是setup一把梭了!是不是越来越像react hooks了?
对比一下传统写法:

<template>
  <div>
    <h3>Listening keypress: {{ keyName }}</h3>
    <p>Press {{ pressCount }} times!</p>
    <p>Is odd times: {{ isOdd }}</p>
  </div>
</template>

<script type="text/javascript">
  let listener

  export default {
    name: "EnterCounter",
    props: {
      keyName: String
    },
    computed: {
      isOdd() {
        return this.pressCount % 2 === 1;
      }
    },
    data() {
      return {
        pressCount: 0
      }
    },
    mounted() {
      listener = (e) =>{
        if (e.key !== this.keyName) {
          return;
        }
        this.callback()
      }
      document.addEventListener('keydown', listener)
    },
    beforeUnmount() {
      document.removeEventListener('keydown', listener)
    },
    watch: {
      pressCount(newVal) {
        if (newVal && newVal % 5 === 0) {
          alert(`you have press ${newVal} times!`)
        }
      }
    },
    methods: {
      callback() {
        this.pressCount += 1;
      }
    }
  }
</script>

当然,声明式vs函数式,不能说哪个一定比另外一个好。尤大依然为我们保持了传统api,这也意味着从2.0迁移到3.0,付出的成本是非常平滑的。

使用 Vuex hooks

初始化

首先定义全局的 Store :

import { createStore } from 'vuex';

export default createStore({
  state: {
    username: 'Xiaoming',
  },
  modules: {
    foo: {
      namespaced: true,
      state: {
        bar: 'baz',
      },
      modules: {
        nested: {
          namespaced: true,
          state: {
            final: 'you\'ve done',
          },
        },
      },
    },
  },
});

接着在组件根实例中注入

import { createAPP } from 'vue'
import Store from './store'
import APP from './views/App.vue'

createApp(APP)
  .use(Store) // 以插件安装形式注入
  .mount('#app')

如何在setup中引用

目前 vuex 的辅助函数 mapStatemapGettermapMutationmapAction,需要绑定组件实例上下文,而 setup 函数中无法访问组件实例,所以这些辅助函数在 setup 中应该用不上了,取而代之的应该是 useStateuseGetteruseMutationuseAction ,目前官方尚未实现。这里根据 vuex 源码写一个兼容3.0的 useState ,抛砖引玉(代码有点长,可以跳着看):

import { computed } from 'vue';
import { useStore } from 'vuex';

const normalizeNamespace = (fn) => (namespace, map) => {
  let appliedMap = map;
  let appliedNamespace = namespace;
  if (typeof appliedNamespace !== 'string') {
    appliedMap = namespace;
    appliedNamespace = '';
  } else if (namespace.charAt(appliedNamespace.length - 1) !== '/') {
    appliedNamespace += '/';
  }
  return fn(appliedNamespace, appliedMap);
};

const normalizeMap = (map) => (Array.isArray(map)
  ? map.map((item) => ({ key: item, val: item }))
  : Object.entries(map).map(([key, val]) => ({ key, val })));

export const useState = normalizeNamespace((namespace, states) => {
  const res = {};
  // 将源码中引用的 this.$store 替换成全局store
  const store = useStore();
  normalizeMap(states).forEach(({ key, val }) => {
    res[key] = (computed(() => {
      let { state, getters } = store;
      if (namespace) {
        // eslint-disable-next-line no-underscore-dangle
        const module = store._modulesNamespaceMap[namespace];
        if (!module) {
          return;
        }
        state = module.context.state;
        getters = module.context.getters;
      }
      // eslint-disable-next-line consistent-return
      return typeof val === 'function'
        ? val.call(store, state, getters)
        : state[val];
    }));
    // mark vuex getter for devtools
    res[key].vuex = true;
  });
  return res;
}, true);

编写完毕,在实际组件中使用:

<template>
  <header>
    <h5>
      Hello {{ username }} | {{ bar }} | {{ final }}
    </h5>
  </header>
</template>

<script>
import { useState } from '../store/hooks';

export default {
  name: 'Header',
  setup() {
    const { username } = useState(['username']);
    const { bar } = useState('foo', ['bar']);
    const { final } = useState('foo/nested', ['final']);
    return {
      username,
      bar,
      final,
    };
  },
};
</script>

输出结果:

Hello Xiaoming | baz | you've done

总结

  • template支持碎片,即除根组件外,子组件无需声明根元素。
  • 传统的组件option api,现在可以用setup来实现,不仅比以前变得更加灵活,在类型分析上(typescript)将会支持得更好
  • 大部分api如ref/reactive/onMounted等方法,现在支持按需导入,对于tree-shaking优化有利
  • setup使开发者不必再关心令人头疼的this问题
  • setup是一把双刃剑,如果你的思路足够清晰,那么它将会是你抽象逻辑的利器。反之使用不当同样也会让你的代码变成意大利面条🍝
  • vuex 的辅助函数将在未来以 useXXXX 的形式 兼容 setup 函数。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335