就在今天凌晨 4 点左右,vue-next v3.0.0-beta.1 版本发布,这意味着 Vue 3.0 全家桶正式登场,发布内容包括:

  • vue: Beta
  • vue-router: Alpha
  • vuex: Alpha
  • vue-class-component: Alpha
  • vue-cli: Experimental support via vue-cli-plugin-vue-next
  • eslint-plugin-vue: Alpha
  • vue-test-utils: Alpha
  • vue-devtools: WIP
  • jsx: WIP

可以看到 Vue 3.0 beta 版本是一个项目系列,包含了我们在开发过程中需要的套件、webpack 插件等等,本文将带大家快速搭建基于 Vue 3.0 的项目框架,这和之前很多 Vue 3.0 的 Demo 不同,是具备商业化项目能力的框架,本文将包括以下内容:

  • 基于 vue-cli 快速搭建 Vue 3.0 项目
  • Vue 3.0 基本特性体验
  • 集成 vue-router 和 vuex 4.0
说个题外话,今天中午我搭建 Vue 3.0 项目时,发现了 vue-router-next 一个 block 级别的 BUG,想在 vue-router-next 项目 issue 中反馈时,发现已经有人提交了相似问题,随后晚上测试时,bug 已经被 fixed,为 Vue 团队的高效点赞,issue 地址,所以当大家发现使用中问题时,可以及时到项目 issue 下进行反馈,这是一支可以信赖的团队!

Vue 3.0 项目初始化

Vue 3.0 项目初始化过程和 Vue 2.0 类似,具体步骤如下:

Vue 项目初始化

第一步,安装 vue-cli:

npm install -g @vue/cli
<span class="copy-code-btn">复制代码</span>

注意以下命令是错误的!

npm install -g vue
npm install -g vue-cli
<span class="copy-code-btn">复制代码</span>

安装成功后,我们即可使用 vue 命令,测试方法:

$ vue -V
@vue/cli 4.3.1
<span class="copy-code-btn">复制代码</span>

第二步,初始化 vue 项目:

vue create vue-next-test
<span class="copy-code-btn">复制代码</span>

输入命令后,会出现命令行交互窗口,这里我们选择 Manually select features:

Vue CLI v4.3.1
? Please pick a preset: 
  default (babel, eslint) 
❯ Manually select features 
<span class="copy-code-btn">复制代码</span>

随后我们勾选:Router、Vuex、CSS Pre-processors 和 Linter / Formatter,这些都是开发商业级项目必须的:

Vue CLI v4.3.1
? Please pick a preset: Manually select features
? Check the features needed <span class="hljs-keyword">for</span> your project: 
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◉ Router
 ◉ Vuex
 ◉ CSS Pre-processors
❯◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing
<span class="copy-code-btn">复制代码</span>
注意:Vue 3.0 项目目前需要从 Vue 2.0 项目升级而来,所以为了直接升级到 Vue 3.0 全家桶,我们需要在 Vue 项目创建过程中勾选 Router 和 Vuex,所以避免手动写初始化代码

回车后会自动安装依赖,为了加速安装速度,我们可以使用淘宝源来加快初始化速度:

vue create -r https://registry.npm.taobao.org vue-next-test
<span class="copy-code-btn">复制代码</span>

项目创建完毕后,目录结构如下:

.
├── README.md
├── babel.config.js
├── package-lock.json
├── package.json
├── public
│   ├── favicon.ico
│   └── index.html
└── src
    ├── App.vue
    ├── assets
    │   └── logo.png
    ├── components
    │   └── HelloWorld.vue
    ├── main.js
    ├── router
    │   └── index.js
    ├── store
    │   └── index.js
    └── views
        ├── About.vue
        └── Home.vue
<span class="copy-code-btn">复制代码</span>

升级 Vue 3.0 项目

目前创建 Vue 3.0 项目需要通过插件升级的方式来实现,vue-cli 还没有直接支持,我们进入项目目录,并输入以下指令:

<span class="hljs-built_in">cd</span> vue-next-test
vue add vue-next
<span class="copy-code-btn">复制代码</span>

执行上述指令后,会自动安装 vue-cli-plugin-vue-next 插件(查看项目代码),该插件会完成以下操作:

  • 安装 Vue 3.0 依赖
  • 更新 Vue 3.0 webpack loader 配置,使其能够支持 .vue 文件构建(这点非常重要)
  • 创建 Vue 3.0 的模板代码
  • 自动将代码中的 Vue Router 和 Vuex 升级到 4.0 版本,如果未安装则不会升级
  • 自动生成 Vue Router 和 Vuex 模板代码

完成上述操作后,项目正式升级到 Vue 3.0,注意该插件还能支持 typescript,用 typescript 的同学还得再等等。

Vue 3.0 基本特性体验

下面我们从项目开发的角度逐步体验 Vue 3.0 的开发流程

创建路由

项目开发中,我们通常需要创建新页面,然后添加路由配置,我们在 /src/views 目录下创建 Test.vue:

<span class="hljs-tag"><<span class="hljs-name">template</span>></span>
  <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"test"</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">h1</span>></span>test page<span class="hljs-tag"></<span class="hljs-name">h1</span>></span>
  <span class="hljs-tag"></<span class="hljs-name">div</span>></span>
<span class="hljs-tag"></<span class="hljs-name">template</span>></span>

<span class="hljs-tag"><<span class="hljs-name">script</span>></span><span class="javascript">
 <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
 }
</span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>

<span class="hljs-tag"><<span class="hljs-name">style</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"less"</span> <span class="hljs-attr">scoped</span>></span><span class="css">
<span class="hljs-selector-class">.test</span> {
  <span class="hljs-attribute">color</span>: red;
}
</span><span class="hljs-tag"></<span class="hljs-name">style</span>></span>
<span class="copy-code-btn">复制代码</span>

之后在 /src/router/index.js 中创建路由配置:

<span class="hljs-keyword">import</span> { createRouter, createWebHashHistory } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue-router'</span>
<span class="hljs-keyword">import</span> Home <span class="hljs-keyword">from</span> <span class="hljs-string">'../views/Home.vue'</span>

<span class="hljs-keyword">const</span> routes = [
  {
    <span class="hljs-attr">path</span>: <span class="hljs-string">'/'</span>,
    <span class="hljs-attr">name</span>: <span class="hljs-string">'Home'</span>,
    <span class="hljs-attr">component</span>: Home
  },
  {
    <span class="hljs-attr">path</span>: <span class="hljs-string">'/about'</span>,
    <span class="hljs-attr">name</span>: <span class="hljs-string">'About'</span>,
    <span class="hljs-attr">component</span>: <span class="hljs-function"><span class="hljs-params">()</span> =></span> <span class="hljs-keyword">import</span>(<span class="hljs-comment">/* webpackChunkName: "about" */</span> <span class="hljs-string">'../views/About.vue'</span>)
  },
  {
    <span class="hljs-attr">path</span>: <span class="hljs-string">'/test'</span>,
    <span class="hljs-attr">name</span>: <span class="hljs-string">'Test'</span>,
    <span class="hljs-attr">component</span>: <span class="hljs-function"><span class="hljs-params">()</span> =></span> <span class="hljs-keyword">import</span>(<span class="hljs-comment">/* webpackChunkName: "test" */</span> <span class="hljs-string">'../views/Test.vue'</span>)
  }
]

<span class="hljs-keyword">const</span> router = createRouter({
  <span class="hljs-attr">history</span>: createWebHashHistory(),
  routes
})

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> router
<span class="copy-code-btn">复制代码</span>

初始化 Vue Router 的过程与 3.0 版本变化不大,只是之前采用构造函数的方式,这里改为使用 createRouter 来创建 Vue Router 实例,配置的方法基本一致,配置完成后我们还需要在 App.vue 中增加链接到 Test.vue 的路由:

<span class="hljs-tag"><<span class="hljs-name">template</span>></span>
  <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"app"</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"nav"</span>></span>
      <span class="hljs-tag"><<span class="hljs-name">router-link</span> <span class="hljs-attr">to</span>=<span class="hljs-string">"/"</span>></span>Home<span class="hljs-tag"></<span class="hljs-name">router-link</span>></span> |
      <span class="hljs-tag"><<span class="hljs-name">router-link</span> <span class="hljs-attr">to</span>=<span class="hljs-string">"/about"</span>></span>About<span class="hljs-tag"></<span class="hljs-name">router-link</span>></span> |
      <span class="hljs-tag"><<span class="hljs-name">router-link</span> <span class="hljs-attr">to</span>=<span class="hljs-string">"/test"</span>></span>Test<span class="hljs-tag"></<span class="hljs-name">router-link</span>></span>
    <span class="hljs-tag"></<span class="hljs-name">div</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">router-view</span>/></span>
  <span class="hljs-tag"></<span class="hljs-name">div</span>></span>
<span class="hljs-tag"></<span class="hljs-name">template</span>></span>
<span class="copy-code-btn">复制代码</span>

启动项目:

npm run serve
<span class="copy-code-btn">复制代码</span>

在浏览器中访问项目地址,此时已经可以跳转到 Test 页面:

图片描述

状态和事件绑定

Vue 3.0 中定义状态的方法改为类似 React Hooks 的方法,下面我们在 Test.vue 中定义一个状态 count:

<span class="hljs-tag"><<span class="hljs-name">template</span>></span>
  <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"test"</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">h1</span>></span>test count: {{count}}<span class="hljs-tag"></<span class="hljs-name">h1</span>></span>
  <span class="hljs-tag"></<span class="hljs-name">div</span>></span>
<span class="hljs-tag"></<span class="hljs-name">template</span>></span>

<span class="hljs-tag"><<span class="hljs-name">script</span>></span><span class="javascript">
  <span class="hljs-keyword">import</span> { ref } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>

  <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
    setup () {
      <span class="hljs-keyword">const</span> count = ref(<span class="hljs-number">0</span>)
      <span class="hljs-keyword">return</span> {
        count
      }
    }
  }
</span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>
<span class="copy-code-btn">复制代码</span>

Vue 3.0 中初始化状态通过 setup 方法,定义状态需要调用 ref 方法。接下来我们定义一个事件,用来更新 count 状态:

<span class="hljs-tag"><<span class="hljs-name">template</span>></span>
  <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"test"</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">h1</span>></span>test count: {{count}}<span class="hljs-tag"></<span class="hljs-name">h1</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"add"</span>></span>add<span class="hljs-tag"></<span class="hljs-name">button</span>></span>
  <span class="hljs-tag"></<span class="hljs-name">div</span>></span>
<span class="hljs-tag"></<span class="hljs-name">template</span>></span>

<span class="hljs-tag"><<span class="hljs-name">script</span>></span><span class="javascript">
  <span class="hljs-keyword">import</span> { ref } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>

  <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
    setup () {
      <span class="hljs-keyword">const</span> count = ref(<span class="hljs-number">0</span>)
      <span class="hljs-keyword">const</span> add = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
        count.value++
      }
      <span class="hljs-keyword">return</span> {
        count,
        add
      }
    }
  }
</span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>
<span class="copy-code-btn">复制代码</span>

这里的 add 方法不再需要定义在 methods 中,但注意更新 count 值的时候不能直接使用 count++,而应使用 count.value++,更新代码后,点击按钮,count 的值就会更新了:

图片描述

计算属性和监听器

Vue 3.0 中计算属性和监听器的实现依赖 computed 和 watch 方法:

<span class="hljs-tag"><<span class="hljs-name">template</span>></span>
  <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"test"</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">h1</span>></span>test count: {{count}}<span class="hljs-tag"></<span class="hljs-name">h1</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">div</span>></span>count * 2 = {{doubleCount}}<span class="hljs-tag"></<span class="hljs-name">div</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"add"</span>></span>add<span class="hljs-tag"></<span class="hljs-name">button</span>></span>
  <span class="hljs-tag"></<span class="hljs-name">div</span>></span>
<span class="hljs-tag"></<span class="hljs-name">template</span>></span>

<span class="hljs-tag"><<span class="hljs-name">script</span>></span><span class="javascript">
  <span class="hljs-keyword">import</span> { ref, computed, watch } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>

  <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
    setup () {
      <span class="hljs-keyword">const</span> count = ref(<span class="hljs-number">0</span>)
      <span class="hljs-keyword">const</span> add = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
        count.value++
      }
      watch(<span class="hljs-function"><span class="hljs-params">()</span> =></span> count.value, val => {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`count is <span class="hljs-subst">${val}</span>`</span>)
      })
      <span class="hljs-keyword">const</span> doubleCount = computed(<span class="hljs-function"><span class="hljs-params">()</span> =></span> count.value * <span class="hljs-number">2</span>)
      <span class="hljs-keyword">return</span> {
        count,
        doubleCount,
        add
      }
    }
  }
</span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>
<span class="copy-code-btn">复制代码</span>

计算属性 computed 是一个方法,里面需要包含一个回调函数,当我们访问计算属性返回结果时,会自动获取回调函数的值:

<span class="hljs-keyword">const</span> doubleCount = computed(<span class="hljs-function"><span class="hljs-params">()</span> =></span> count.value * <span class="hljs-number">2</span>)
<span class="copy-code-btn">复制代码</span>

监听器 watch 同样是一个方法,它包含 2 个参数,2 个参数都是 function:

watch(<span class="hljs-function"><span class="hljs-params">()</span> =></span> count.value, 
  val => {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`count is <span class="hljs-subst">${val}</span>`</span>)
  })
<span class="copy-code-btn">复制代码</span>

第一个参数是监听的值,count.value 表示当 count.value 发生变化就会触发监听器的回调函数,即第二个参数,第二个参数可以执行监听时候的回调

获取路由

Vue 3.0 中通过 getCurrentInstance 方法获取当前组件的实例,然后通过 ctx 属性获得当前上下文,ctx.$router 是 Vue Router 实例,里面包含了 currentRoute 可以获取到当前的路由信息

<script>
  import { getCurrentInstance } from 'vue'

  export default {
    setup () {
      const { ctx } = getCurrentInstance()
      console.log(ctx.$router.currentRoute.value)
    }
  }
</script>
<span class="copy-code-btn">复制代码</span>

Vuex 集成

Vuex 的集成方法如下:

定义 Vuex 状态

第一步,修改 src/store/index.js 文件:

<span class="hljs-keyword">import</span> Vuex <span class="hljs-keyword">from</span> <span class="hljs-string">'vuex'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> Vuex.createStore({
  <span class="hljs-attr">state</span>: {
    <span class="hljs-attr">test</span>: {
      <span class="hljs-attr">a</span>: <span class="hljs-number">1</span>
    }
  },
  <span class="hljs-attr">mutations</span>: {
    setTestA(state, value) {
      state.test.a = value
    }
  },
  <span class="hljs-attr">actions</span>: {
  },
  <span class="hljs-attr">modules</span>: {
  }
})
<span class="copy-code-btn">复制代码</span>

Vuex 的语法和 API 基本没有改变,我们在 state 中创建了一个 test.a 状态,在 mutations 中添加了修改 state.test.a 状态的方法: setTestA

引用 Vuex 状态

第二步,在 Test.vue 中,通过计算属性使用 Vuex 状态:

<span class="hljs-tag"><<span class="hljs-name">template</span>></span>
  <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"test"</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">h1</span>></span>test count: {{count}}<span class="hljs-tag"></<span class="hljs-name">h1</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">div</span>></span>count * 2 = {{doubleCount}}<span class="hljs-tag"></<span class="hljs-name">div</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">div</span>></span>state from vuex {{a}}<span class="hljs-tag"></<span class="hljs-name">div</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"add"</span>></span>add<span class="hljs-tag"></<span class="hljs-name">button</span>></span>
  <span class="hljs-tag"></<span class="hljs-name">div</span>></span>
<span class="hljs-tag"></<span class="hljs-name">template</span>></span>

<span class="hljs-tag"><<span class="hljs-name">script</span>></span><span class="javascript">
  <span class="hljs-keyword">import</span> { ref, computed, watch, getCurrentInstance } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>

  <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
    setup () {
      <span class="hljs-keyword">const</span> count = ref(<span class="hljs-number">0</span>)
      <span class="hljs-keyword">const</span> add = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
        count.value++
      }
      watch(<span class="hljs-function"><span class="hljs-params">()</span> =></span> count.value, val => {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`count is <span class="hljs-subst">${val}</span>`</span>)
      })
      <span class="hljs-keyword">const</span> doubleCount = computed(<span class="hljs-function"><span class="hljs-params">()</span> =></span> count.value * <span class="hljs-number">2</span>)
      <span class="hljs-keyword">const</span> { ctx } = getCurrentInstance()
      <span class="hljs-built_in">console</span>.log(ctx.$router.currentRoute.value)
      <span class="hljs-keyword">const</span> a = computed(<span class="hljs-function"><span class="hljs-params">()</span> =></span> ctx.$store.state.test.a)
      <span class="hljs-keyword">return</span> {
        count,
        doubleCount,
        add,
        a
      }
    }
  }
</span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>
<span class="copy-code-btn">复制代码</span>

这里我们通过计算属性来引用 Vuex 中的状态:

<span class="hljs-keyword">const</span> a = computed(<span class="hljs-function"><span class="hljs-params">()</span> =></span> ctx.$store.state.test.a)
<span class="copy-code-btn">复制代码</span>

ctx 是上节中我们提到的当前组件实例

更新 Vuex 状态

更新 Vuex 状态仍然使用 commit 方法,这点和 Vuex 3.0 版本一致:

<span class="hljs-tag"><<span class="hljs-name">template</span>></span>
  <span class="hljs-tag"><<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"test"</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">h1</span>></span>test count: {{count}}<span class="hljs-tag"></<span class="hljs-name">h1</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">div</span>></span>count * 2 = {{doubleCount}}<span class="hljs-tag"></<span class="hljs-name">div</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">div</span>></span>state from vuex {{a}}<span class="hljs-tag"></<span class="hljs-name">div</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"add"</span>></span>add<span class="hljs-tag"></<span class="hljs-name">button</span>></span>
    <span class="hljs-tag"><<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"update"</span>></span>update a<span class="hljs-tag"></<span class="hljs-name">button</span>></span>
  <span class="hljs-tag"></<span class="hljs-name">div</span>></span>
<span class="hljs-tag"></<span class="hljs-name">template</span>></span>

<span class="hljs-tag"><<span class="hljs-name">script</span>></span><span class="javascript">
  <span class="hljs-keyword">import</span> { ref, computed, watch, getCurrentInstance } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>

  <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
    setup () {
      <span class="hljs-keyword">const</span> count = ref(<span class="hljs-number">0</span>)
      <span class="hljs-keyword">const</span> add = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
        count.value++
      }
      watch(<span class="hljs-function"><span class="hljs-params">()</span> =></span> count.value, val => {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`count is <span class="hljs-subst">${val}</span>`</span>)
      })
      <span class="hljs-keyword">const</span> doubleCount = computed(<span class="hljs-function"><span class="hljs-params">()</span> =></span> count.value * <span class="hljs-number">2</span>)
      <span class="hljs-keyword">const</span> { ctx } = getCurrentInstance()
      <span class="hljs-built_in">console</span>.log(ctx.$router.currentRoute.value)
      <span class="hljs-keyword">const</span> a = computed(<span class="hljs-function"><span class="hljs-params">()</span> =></span> ctx.$store.state.test.a)
      <span class="hljs-keyword">const</span> update = <span class="hljs-function"><span class="hljs-params">()</span> =></span> {
        ctx.$store.commit(<span class="hljs-string">'setTestA'</span>, count)
      }
      <span class="hljs-keyword">return</span> {
        count,
        doubleCount,
        add,
        a,
        update
      }
    }
  }
</span><span class="hljs-tag"></<span class="hljs-name">script</span>></span>
<span class="copy-code-btn">复制代码</span>

这里我们点击 update a 按钮后,会触发 update 方法,此时会通过 ctx.$store.commit 调用 setTestA 方法,将 count 的值覆盖 state.test.a 的值

总结

通过我第一时间体验 Vue 3.0-beta 版本后,感觉 Vue 3.0 已经具备了商业项目开发的必备条件,语法精炼,不管是代码可读性还是运行效率都非常赞。但由于未深入使用,目前还无法提出更多问题,需要在项目实战中进一步发现和总结问题,再和大家分享交流。