8

视图

包含内容#NavigationBar#TabBar#MainContext

为什么#NavigationBar#TabBar分在Layout中,而不是components中?

代码上实际上是没有差别的,只是认为#NavigationBar#TabBar是加载一次的,而非复用,且属于页面布局内容。

App.vue

Vue实例化的根组件,我们在这里进行布局:

src/App.vue文件:

<template>
  <div id="app">
    <navigation-bar></navigation-bar>
    <router-view></router-view>
    <tab-bar></tab-bar>
  </div>
</template>
<script>
import TabBar from "@/views/layout/TabBar";
import NavigationBar from "@/views/layout/NavigationBar";
export default {
  name: 'App',
  components: {
    'navigation-bar': NavigationBar,
    'tab-bar':TabBar,
  }
}
</script>
<style>
 ...//未动原有样式;
</style>
  • 在这里,我们使用<template />标识 其内部的HTML为Vue Template。
  • <template />内部必有一个且唯一的节点(这里是div#app)包裹内容(即使只是一串字符)-->若存在同级节点,则会报错(这是因为VNode会通过createElement('div')来创建真实节点,只能是单个元素);
  • 通过components属性以键值对的形式引入组件,模板(HTML)中使用的标签名键名(自定义元素VNode),值为导入的组件模块;
  • 通过components定义组件使用的方式,限制了组件应用的范围。即:如果你在其它文件直接使用<navigation-bar></navigation-bar>,控制台会报错:组件未注册-->这就是组件的局部注册
  • 局部注册的组件要求:如果在某一文件中应用该组件,必须要使用components注册一次。
  • 导入组件import TabBar from "@/views/layout/TabBar";路径以@起,这是因为build/webpack.base.conf.js中配置的路径别名'@' === "resolve('src')"
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src'), //可以追加当前项目下,想快捷访问的文件目录
    }
  },

临时定义的组件文件:

src/views/layout/NavigationBar.vue文件:

<template>
  <header>NavigationBar</header>
</template>
<style scoped>
  header{
    border-bottom: 4px solid #821910;
  }
</style>
  • 在这里,我们通过style[scoped]定义一份样式,其作用范围仅限于当前文件(又可称模块)模板中的元素。
  • 像下边,在TabBar.vue中的header元素就没有使用到该文件中的对应样式,这就是局部作用域的样式。
  • 局部作用域的样式只对当前文件<template/>中的元素起作用,想改变body的样式,不好意思,请全局导入或不使用局部作用域。

src/views/layout/TabBar.vue文件:

<template>
  <div>
    <header>测试是否和NavigationBar一样的效果</header>
    <footer>TabBar</footer>
  </div>
</template>
<style scoped>
  footer{
    border-bottom: 4px solid #07776e;
  }
</style>

显示效果:

App

NavigationBar

#NavigationBar中分左右结构,左边按钮后退,右边按钮更新页面。

更新页面只是更新数据,而不是整个页面的刷新,每个页面更新数据的接口不同,所以,要作为组件属性传入。

src/views/layout/navigationBar.vue中:

<template>
  <header class="navigation_bar">
    <button @click="goBack" class="navigation_back">
      <i class="arrow"></i>
      <span class="back_tip">关闭</span>
    </button>
    <h2 v-if="hasTitle" class="navigation_title">{{title}}</h2>
    <button class="refresh" @click="onRefresh">刷新</button>
  </header>
</template>
  • 该部分为单文件组件#NavigationBar的Template部分。
  • @clickv-on:click的简写,用于绑定点击事件。
  • v-ifVue中的条件指令,根据返回的布尔值动态添加或移除DOM元素。
<script>
  /**
   * @title:头部标题;
   * @refresh:刷新处理函数;
   */
  export default {
    props: ['title', 'refresh'],
    computed: {
      hasTitle() {
        return this.title && this.title.trim();
      },
    },
    methods: {
      goBack() {
        this.$router.back()
      },
      onRefresh() {
        this.refresh();
      },
    },
  }
</script>
  • 该部分为单文件组件#NavigationBar的组件配置对象。
  • props为父级(调用该组件的组件)传过来的属性。

    • 传值方式<navigation-bar title="我是标题" :refresh="refresh"></navigation-bar>(需要在src/App.vue中定义refresh函数)

      • title传的值为字符串,不需要:前缀;
      • :refresh传的值为非字符串(数字、布尔值、函数、数组、对象...),:为前缀,值为Javascript表达式计算结果;
    • 在程序中,如this.title引用props的值。
    • 在模板中,作元素的innerHTML内容时,如{{title}}引用。
  • methods为该组件内,元素绑定的事件处理函数。

    • 在程序中,如this.refresh()引用。
    • 在模板中,如@click="onRefresh"调用,传入的是函数应用;若传参,如@click="onRefresh(param)"调用。
  • computed本身写法和函数定义一致,然而,其本身是一个data(数据源),字段名为函数名,值为函数的返回值。

    • 使用方式与props一致。
区别 method computed
类型 函数 数据变量
参数 可以带参 不带参(非函)
触发 交互时触发 声明内部的this属性的值变化时执行

显示效果

这里样式请大家随意设定,我使用的是flexBox布局。

点击刷新,我定义了console.log('refresh success')

Navigation

TabBar

#TabBar分以下情况:

  • 一个按钮
  • 两个按钮

每个视图中#TabBar按钮是不同的,所以,按钮的配置要当作组件属性传入(控制变化的量)。

测试数据源

const tabBars = [
  {
    label: '提交',
    eventType: 'click',
    disabled: false,
    callBack(vm) {
      console.log('单击,提交');
    }
  },
  {
    label: '取消',
    eventType: 'dblclick', //该事件在手机模式下无法响应呢,只能在PC模式下调试
    disabled: false,
    callBack(vm) {
      console.log('双击,取消');
    }
  }
]

src/views/layout/TabBar.vue的模板:

<template>
  <footer class="tab-bar" v-if="isRender">
    <div class="tab-button" v-for="tab in tabBars" :key="tab.label">
      <template>
        <tab-button :el="$parent" :disabled="tab.disabled" :event="tab.eventType" :callBack="tab.callBack">
          <span>{{tab.label}}</span>
        </tab-button>
      </template>
    </div>
  </footer>
</template>
  • v-for="tab in tabBars"Vue中的循环结构,搭配:key使用,优化Vue的渲染机制;

    • tabBars进行遍历,tab为数组中的元素。
    • 同样key值,在更新时,会复用组件,而不是销毁后,再创建一个新的组件。
  • <tab-button :el="$parent" :disabled="tab.disabled" :event="tab.eventType" :callBack="tab.callBack">这是是一个新组件的引用。

    • $parent是组件实例#TabBar的父实例(#App)。

src/views/layout/TabBar.vue组件配置对象:

<script>
  const tabButton = {
    render(createElement) {
      return createElement(
        'button',
        {
          "class": this.className,
          on: {
            [this.event]: this.tabClick,
          },
        },
        this.$slots.default, //指代<span>{{tab.label}}</span>
      )
    },
    props: ['event', 'callBack', 'disabled','el'],
    computed: {
      className() {
        return this.disabled ? 'tab-label disabled' : 'tab-label';
      }
    },
    methods: {
      tabClick() {
        if (this.disabled) return;
        this.callBack && this.callBack(this.el)
      }
    }
  }
  export default {
    components: {
      'tab-button': tabButton
    },
    props: ['tabBars'],
    computed: {
      isRender() {
        const isRender = _.isArray(this.tabBars) && this.tabBars.length !== 0;
        return isRender;
      },
    },
  }
</script>
  • 这里使用了另一种方式定义组件tabButton,其与 单文件组件 的区别仅仅在于使用render方法定义模板

    • 优势:定义出来的组件更具有灵活性,在这里on属性可以动态绑定事件类型

      • 注意:这里的事件类型[this.event]是作为参数传进来的呢!
    • 组件本质上只是一个JavaScript对象(虚拟DOM),该对象按Vue规定的成员属性构建,区别只在于Template的写作模式。
  • 这里应用了Slot,指代该组件嵌套的子节点。
  • 这里使用了underscore.js_.isArray),需要在build/webpack.base.conf.js中配置:
const webpack = require('webpack');
...
module.exports = {
  ...
  module:{
    ...
  },
  plugins:[
    new webpack.ProvidePlugin({
      _: 'underscore',
    }),
  ],
  ...

然后,underscore在全局可用。

因为这里的配置对devprod环境是一致的,所以,直接在build/webpack.base.conf.js中配置了。

显示效果

tabBars

整体Layout布局

最终,我们要做一个顶天立地的内滚动结构(使用flexBox布局即可):

内滚动结构

src/App.vue样式中:

<style lang="scss" scoped>
@import "@/styles/mixins.scss";

#app {
  @include flex($direction: column);
}

.main-context {
  flex-grow: 1;
  overflow: hidden;
  overflow-y: auto;
}
</style>

其中src/styles/mixins.scss

@mixin flex($direction:row, $alignItems: stretch, $justifyContent: space-between, $basis: auto,$wrap:nowrap) {
  display: flex;
  flex-direction: $direction;
  align-items: $alignItems;
  justify-content: $justifyContent;
  flex-basis: $basis;
  flex-wrap: $wrap;
}

章节回顾

  • 我这里面省略了将写好的#NavigationBar#TabBar替换原临时搭建的对应组件,我相信你能处理好,对吧?!
  • #App小节中,是怎样注册局部组件的,如果想要在项目所有模板中可以直接使用标签名来应用组件,该怎么处理呢?
  • #App小节中,如何定义局部样式的,如果想让app.vue中header的样式全局可用,该怎么处理呢?
  • 父组件如何传值给子组件,若想传布尔值,该如何操作?
  • render函数如何渲染组件模板,使用该方法如何定义组件?
  • slot用于什么情况下呢,有什么好处?

思考

  • 接下来要实现列表了呢,怎么做列表数据呢?

番外


米花儿团儿
1.3k 声望75 粉丝