AICODER大前端培训

AICODER-IT培训专家,前端培训基地,前端培训,全栈培训,大学生IT培训基地,线上IT课程免费自学,线下培训项目实战,实习,html5培训实习-AICODE程序员

在线咨询

Vue快速入门教程-(7)Vue 入门之组件化开发(下)

老马 发布于: 2018年12月14日 10时40分10秒 阅读1920次

组件的 slot

使用组件的时候,经常需要在父组件中为子组件中插入一些标签等。当然其实可以通过属性等操作,但是比较麻烦,直接写标签还是方便很多。 那么 Vue 提供了 slot 协助子组件对父容器写入的标签进行管理。

当父容器写了额外的内容时, 如果子组件恰好有一个 slot 标签,那边子容器的 slot 标签会被父容器写入的内容替换掉。

比如下面的例子:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue入门之extend全局方法</title>
  <script src="https://unpkg.com/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <!--父容器输入标签-->
    <my-slot>
      <h3>这里是父容器写入的</h3>
    </my-slot>

    <!--父容器绑定数据到子容器的slot,这里的作用域是父容器的啊。-->
    <my-slot>{{ email }}</my-slot>

    <!--父容器什么都不传内容-->
    <my-slot></my-slot>
  </div>
  <script>
    // 反引号:可以定义多行字符串。
    var temp = `
      <div>
        <h1>这里是子组件</h1>
        <hr>
        <slot>slot标签会被父容器写的额外的内容替换掉,如果父容器没有写入任何东西,此标签将保留!</slot>
      </div>
    `;
    Vue.component('MySlot', {          // 如果定义的组件为MySlot,那么用组件的时候:<my-slot></my-slot>
      template: temp,
    });
    // 初始化一个Vue实例
    var app = new Vue({
      el: '#app',
      data: {
       email: 'flydragon@gmail.com'
      }
    });
  </script>
</body>
</html><button>Click to copy</button>

最终结果:

<div id="app">
  <div>
    <h1>这里是子组件</h1>
    <hr>
    <h3>这里是父容器写入的</h3>
  </div>

  <div>
    <h1>这里是子组件</h1>
    <hr> flydragon@gmail.com
  </div>

  <div>
    <h1>这里是子组件</h1>
    <hr> slot标签会被父容器写的额外的内容替换掉,如果父容器没有写入任何东西,此标签将删除!
  </div>
</div><button>Click to copy</button>

Vue 实现了一套内容分发的 API,这套 API 基于当前的 Web Components 规范草案,将 <slot> 元素作为承载分发内容的出口。

它允许你像这样合成组件:

<navigation-link url="/profile">
  Your Profile
</navigation-link><button>Click to copy</button>

然后你在 <navigation-link> 的模板中可能会写为:

<a
  v-bind:href="url"
 
>
  <slot></slot>
</a><button>Click to copy</button>

当组件渲染的时候,这个 <slot> 元素将会被替换为“Your Profile”。插槽内可以包含任何模板代码,包括 HTML:

<navigation-link url="/profile">
  <!-- 添加一个 Font Awesome 图标 -->
  <span></span>
  Your Profile
</navigation-link><button>Click to copy</button>

甚至其它的组件:

<navigation-link url="/profile">
  <!-- 添加一个图标的组件 -->
  <font-awesome-icon name="user"></font-awesome-icon>
  Your Profile
</navigation-link><button>Click to copy</button>

如果 <navigation-link> 没有包含一个 <slot> 元素,则任何传入它的内容都会被抛弃。

具名插槽

有些时候我们需要多个插槽。例如,一个假设的 <base-layout> 组件多模板如下:

<div>
  <header>
    <!-- 我们希望把页头放这里 -->
  </header>
  <main>
    <!-- 我们希望把主要内容放这里 -->
  </main>
  <footer>
    <!-- 我们希望把页脚放这里 -->
  </footer>
</div><button>Click to copy</button>

对于这样的情况,<slot> 元素有一个特殊的特性:name。这个特性可以用来定义额外的插槽:

<div>
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div><button>Click to copy</button>

在向具名插槽提供内容的时候,我们可以在一个父组件的 <template> 元素上使用 slot 特性:

<base-layout>
  <template slot="header">
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template slot="footer">
    <p>Here's some contact info</p>
  </template>
</base-layout><button>Click to copy</button>

另一种 slot 特性的用法是直接用在一个普通的元素上:

<base-layout>
  <h1 slot="header">Here might be a page title</h1>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <p slot="footer">Here's some contact info</p>
</base-layout><button>Click to copy</button>

我们还是可以保留一个未命名插槽,这个插槽是默认插槽,也就是说它会作为所有未匹配到插槽的内容的统一出口。上述两个示例渲染出来的 HTML 都将会是:

<div>
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div><button>Click to copy</button>

插槽的默认内容

有的时候为插槽提供默认的内容是很有用的。例如,一个 <submit-button> 组件可能希望这个按钮的默认内容是“Submit”,但是同时允许用户覆写为“Save”、“Upload”或别的内容。

你可以在 <slot> 标签内部指定默认的内容来做到这一点。

<button type="submit">
  <slot>Submit</slot>
</button><button>Click to copy</button>

如果父组件为这个插槽提供了内容,则默认的内容会被替换掉。

编译作用域

当你想在插槽内使用数据时,例如:

<navigation-link url="/profile">
  Logged in as {{ user.name }}
</navigation-link><button>Click to copy</button>

该插槽可以访问跟这个模板的其它地方相同的实例属性 (也就是说“作用域”是相同的)。但这个插槽不能访问 <navigation-link> 的作用域。例如尝试访问 url 是不会工作的。牢记一条准则:

父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。

作用域插槽

2.1.0+ 新增

有的时候你希望提供的组件带有一个可从子组件获取数据的可复用的插槽。例如一个简单的 <todo-list> 组件的模板可能包含了如下代码:

<ul>
  <li
    v-for="todo in todos"
    v-bind:key="todo.id"
  >
    {{ todo.text }}
  </li>
</ul><button>Click to copy</button>

但是在我们应用的某些部分,我们希望每个独立的待办项渲染出和 todo.text 不太一样的东西。这也是作用域插槽的用武之地。

为了让这个特性成为可能,你需要做的全部事情就是将待办项内容包裹在一个 <slot> 元素上,然后将所有和其上下文相关的数据传递给这个插槽:在这个例子中,这个数据是 todo 对象:

<ul>
  <li
    v-for="todo in todos"
    v-bind:key="todo.id"
  >
    <!-- 我们为每个 todo 准备了一个插槽,-->
    <!-- 将 `todo` 对象作为一个插槽的 prop 传入。-->
    <slot v-bind:todo="todo">
      <!-- 回退的内容 -->
      {{ todo.text }}
    </slot>
  </li>
</ul><button>Click to copy</button>

现在当我们使用 <todo-list> 组件的时候,我们可以选择为待办项定义一个不一样的 <template> 作为替代方案,并且可以通过 slot-scope 特性从子组件获取数据:

<todo-list v-bind:todos="todos">
  <!-- 将 `slotProps` 定义为插槽作用域的名字 -->
  <template slot-scope="slotProps">
    <!-- 为待办项自定义一个模板,-->
    <!-- 通过 `slotProps` 定制每个待办项。-->
    <span v-if="slotProps.todo.isComplete">✓</span>
    {{ slotProps.todo.text }}
  </template>
</todo-list><button>Click to copy</button>

在 2.5.0+,slot-scope 不再限制在 <template> 元素上使用,而可以用在插槽内的任何元素或组件上。

解构 slot-scope

如果一个 JavaScript 表达式在一个函数定义的参数位置有效,那么这个表达式实际上就可以被 slot-scope 接受。也就是说你可以在支持的环境下 (单文件组件现代浏览器),在这些表达式中使用 ES2015 解构语法。例如:

<todo-list v-bind:todos="todos">
  <template slot-scope="{ todo }">
    <span v-if="todo.isComplete">✓</span>
    {{ todo.text }}
  </template>
</todo-list><button>Click to copy</button>

这会使作用域插槽变得更干净一些。

单文件组件的使用方式介绍

通过上面我们定义组件的方式,就已经感觉很不爽了,尤其是模板的定义,而且样式怎么处理也没有很好的进行规整。 Vue 可以通过 Webpack 等第三方工具实现单文件的开发的方式。当然这里会牵扯到很多 es6 的语法、第三方工具实现前端模块化等很多知识, 我们大概看一眼指导 Vue 的组件可以直接写一个文件中,其他地方就可以直接导入这个模块了。后面做项目的时候我还会再讲一下怎么用。

<template>
  <div>
    <nav>
    </nav>
     <div>
      <ul>
        <li v-for="item in list" >
          <router-link :to="{ path: item.url }">{{ item.name }}</router-link>
        </li>
      </ul>
    </div>
    <div>
      <router-view></router-view>
    </div>
    </div>
  </div>
</template>

<script>
// 这里怎么回事
import Axios from 'axios'
export default {
  name: 'app',
  components: {
  },
  data: function () {
    return {
      list: []
    }
  },
  mounted: function () {          // 挂在完成后
    this.$nextTick(function () {
      Axios.get('/api/menulist', {
        params: {
        }
      }).then(function (res) {
        this.list = res.data
      }.bind(this))
    })
  }
}
</script>

<style>
ul, li {
  list-style: none;
}
.router-link-active {
  background-color: #f6f6f6;
}

.navbar {
  height: 50px;
  background-color: #303030;
}
.content {
  margin-top: 50px;
  padding-left: 210px;
}

.sidebar {
  background-color: #f5f5f5;
  border-right: 1px solid #eee;
  width: 200px;
}

@media (min-width: 768px) {
  .sidebar {
    position: fixed;
    top: 51px;
    bottom: 0;
    left: 0;
    z-index: 1000;
    display: block;
    padding: 20px;
    overflow-x: hidden;
    overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
    background-color: #f5f5f5;
    border-right: 1px solid #eee;
  }
}
</style><button>Click to copy</button>

单文件书写组件的方式必须要配合 webpack 之类的工具才行,所以这里暂时不讲解如何做,后面到项目阶段的时候再详细讲解。 不过你可以参考:Vue 官网单文件组件

组件的事件

事件名

跟组件和 prop 不同,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。举个例子,如果触发一个 camelCase 名字的事件:

this.$emit('myEvent')<button>Click to copy</button>

则监听这个名字的 kebab-case 版本是不会有任何效果的:

<my-component v-on:my-event="doSomething"></my-component><button>Click to copy</button>

跟组件和 prop 不同,事件名不会被用作一个 JavaScript 变量名或属性名,所以就没有理由使用 camelCase 或 PascalCase 了。并且 v-on 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以 v-on:myEvent 将会变成 v-on:myevent——导致 myEvent 不可能被监听到。

因此,我们推荐你始终使用 kebab-case 的事件名

通过事件实现子组件向父组件传递数据

 <div id="app">
    <p>{{childData}}</p>
    <ls @gg="childData=$event"></ls>
  </div>
  <script>
    Vue.component('ls', {
      template: `<div><p @click="emitGGEvent">点击子组件传递数据</p></div>`,
      methods: {
        emitGGEvent() {
          this.$emit('gg', 'aicoder.com')
        }
      }
    })
    let vm = new Vue({
      el: '#app',
      data: {
        childData: ''
      }
    });
  </script><button>Click to copy</button>

在动态组件上使用 keep-alive

is 特性来切换不同的动态的组件:

<component v-bind:is="currentTabComponent"></component><button>Click to copy</button>

当在这些组件之间切换的时候,你有时会想保持这些组件的状态,以避免反复重渲染导致的性能问题。例如我们来展开说一说这个多标签界面:

你会注意到,如果你选择了一篇文章,切换到 Archive 标签,然后再切换回 Posts,是不会继续展示你之前选择的文章的。这是因为你每次切换新标签的时候,Vue 都创建了一个新的 currentTabComponent 实例。

重新创建动态组件的行为通常是非常有用的,但是我们更希望那些标签的组件实例能够被在它们第一次被创建的时候缓存下来。为了解决这个问题,我们可以用一个 <keep-alive> 元素将其动态组件包裹起来。

<!-- 失活的组件将会被缓存!-->
<keep-alive>
  <component v-bind:is="currentTabComponent"></component>
</keep-alive><button>Click to copy</button>

异步组件

在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。例如:

Vue.component('async-example', function (resolve, reject) {
  setTimeout(function () {
    // 向 `resolve` 回调传递组件定义
    resolve({
      template: '<div>I am async!</div>'
    })
  }, 1000)
})<button>Click to copy</button>

如你所见,这个工厂函数会收到一个 resolve 回调,这个回调函数会在你从服务器得到组件定义的时候被调用。你也可以调用 reject(reason) 来表示加载失败。这里的 setTimeout 是为了演示用的,如何获取组件取决于你自己。一个推荐的做法是将异步组件和 webpack 的 code-splitting 功能一起配合使用:

Vue.component('async-webpack-example', function (resolve) {
  // 这个特殊的 `require` 语法将会告诉 webpack
  // 自动将你的构建代码切割成多个包,这些包
  // 会通过 Ajax 请求加载
  require(['./my-async-component'], resolve)
})<button>Click to copy</button>

你也可以在工厂函数中返回一个 Promise,所以把 webpack 2 和 ES2015 语法加在一起,我们可以写成这样:

Vue.component(
  'async-webpack-example',
  // 这个 `import` 函数会返回一个 `Promise` 对象。
  () => import('./my-async-component')
)<button>Click to copy</button>

当使用局部注册的时候,你也可以直接提供一个返回 Promise 的函数:

new Vue({
  // ...
  components: {
    'my-component': () => import('./my-async-component')
  }
})<button>Click to copy</button>

如果你是一个 Browserify 用户同时喜欢使用异步组件,很不幸这个工具的作者明确表示异步加载“并不会被 Browserify 支持”,至少官方不会。Browserify 社区已经找到了一些变通方案,这些方案可能会对已存在的复杂应用有帮助。对于其它的场景,我们推荐直接使用 webpack,以拥有内建的被作为第一公民的异步支持。

处理加载状态

2.3.0+ 新增

这里的异步组件工厂函数也可以返回一个如下格式的对象:

const AsyncComponent = () => ({
  // 需要加载的组件 (应该是一个 `Promise` 对象)
  component: import('./MyComponent.vue'),
  // 异步组件加载时使用的组件
  loading: LoadingComponent,
  // 加载失败时使用的组件
  error: ErrorComponent,
  // 展示加载时组件的延时时间。默认值是 200 (毫秒)
  delay: 200,
  // 如果提供了超时时间且组件加载也超时了,
  // 则使用加载失败时使用的组件。默认值是:`Infinity`
  timeout: 3000
})<button>Click to copy</button>

注意如果你希望在 Vue Router 的路由组件中使用上述语法的话,你必须使用 Vue Router 2.4.0+ 版本。

访问元素 & 组件

在绝大多数情况下,我们最好不要触达另一个组件实例内部或手动操作 DOM 元素。不过也确实在一些情况下做这些事情是合适的。

访问根实例

在每个 new Vue 实例的子组件中,其根实例可以通过 $root 属性进行访问。例如,在这个根实例中:

// Vue 根实例
new Vue({
  data: {
    foo: 1
  },
  computed: {
    bar: function () { /* ... */ }
  },
  methods: {
    baz: function () { /* ... */ }
  }
})<button>Click to copy</button>

所有的子组件都可以将这个实例作为一个全局 store 来访问或使用。

// 获取根组件的数据
this.$root.foo

// 写入根组件的数据
this.$root.foo = 2

// 访问根组件的计算属性
this.$root.bar

// 调用根组件的方法
this.$root.baz()<button>Click to copy</button>

对于 demo 或非常小型的有少量组件的应用来说这是很方便的。不过这个模式扩展到中大型应用来说就不然了。因此在绝大多数情况下,我们强烈推荐使用 Vuex 来管理应用的状态。

访问父级组件实例

和 $root 类似,$parent 属性可以用来从一个子组件访问父组件的实例。它提供了一种机会,可以在后期随时触达父级组件,以替代将数据以 prop 的方式传入子组件的方式。

在绝大多数情况下,触达父级组件会使得你的应用更难调试和理解,尤其是当你变更了父级组件的数据的时候。当我们稍后回看那个组件的时候,很难找出那个变更是从哪里发起的。

另外在一些可能适当的时候,你需要特别地共享一些组件库。举个例子,在和 JavaScript API 进行交互而不渲染 HTML 的抽象组件内,诸如这些假设性的 Google 地图组件一样:

<google-map>
  <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map><button>Click to copy</button>

这个 <google-map> 组件可以定义一个 map 属性,所有的子组件都需要访问它。在这种情况下 <google-map-markers> 可能想要通过类似 this.$parent.getMap 的方式访问那个地图,以便为其添加一组标记。你可以在这里查阅这种模式。

请留意,尽管如此,通过这种模式构建出来的那个组件的内部仍然是容易出现问题的。比如,设想一下我们添加一个新的 <google-map-region> 组件,当 <google-map-markers> 在其内部出现的时候,只会渲染那个区域内的标记:

<google-map>
  <google-map-region v-bind:shape="cityBoundaries">
    <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
  </google-map-region>
</google-map><button>Click to copy</button>

那么在 <google-map-markers> 内部你可能发现自己需要一些类似这样的 hack:

var map = this.$parent.map || this.$parent.$parent.map<button>Click to copy</button>

很快它就会失控。这也是我们针对需要向任意更深层级的组件提供上下文信息时推荐依赖注入的原因。

访问子组件实例或子元素

尽管存在 prop 和事件,有的时候你仍可能需要在 JavaScript 里直接访问一个子组件。为了达到这个目的,你可以通过 ref 特性为这个子组件赋予一个 ID 引用。例如:

<base-input ref="usernameInput"></base-input><button>Click to copy</button>

现在在你已经定义了这个 ref 的组件里,你可以使用:

this.$refs.usernameInput<button>Click to copy</button>

来访问这个 <base-input> 实例,以便不时之需。比如程序化地从一个父级组件聚焦这个输入框。在刚才那个例子中,该 <base-input> 组件也可以使用一个类似的 ref 提供对内部这个指定元素的访问,例如:

<input ref="input"><button>Click to copy</button>

甚至可以通过其父级组件定义方法:

methods: {
  // 用来从父级组件聚焦输入框
  focus: function () {
    this.$refs.input.focus()
  }
}<button>Click to copy</button>

这样就允许父级组件通过下面的代码聚焦 <base-input> 里的输入框:

this.$refs.usernameInput.focus()<button>Click to copy</button>

当 ref 和 v-for 一起使用的时候,你得到的引用将会是一个包含了对应数据源的这些子组件的数组。

$refs 只会在组件渲染完成之后生效,并且它们不是响应式的。这只意味着一个直接的子组件封装的“逃生舱”——你应该避免在模板或计算属性中访问 $refs

组件的生命周期

根组件和子组件的生命周期执行的过程如下:

  • 根组件 (beforeCreate、created、beforeMount)
  • 组件 (beforeCreate、created、beforeMount)
  • 组件 mounted
  • 根组件 mounted
  • nextTick

组件总结

Vue 的组件化还是做的比较彻底的。不像 Angular1.0 中的模块那么鸡肋。组件化确实让前端模块化开发更加容易实现, Vue 的单文件开发组件的方式也是 Vue 的一大创新,也发非常好用。

回到vue.js知识列表首页


AICODER编辑推荐:



报名就业班送基础班

最近分享