# vue2

组件化是 vue 的核心思想,主要目的是为了代码重用

# 1. 组件通信

# 1.1. 父组件 --> 子组件

属性(props):

<!-- child -->
<script>
  export default {
    props: {
      msg: String
    }
  }
</script>

<!-- parent -->
<template>
  <Child msg="hello" />
</template>

引用($refs):

<!-- child -->
<script>
  export default {
    data() {
      return {
        userInfo: null,
      }
    }
  }
</script>

<!-- parent -->
<template>
  <Child ref="childRef" />
</template>
<script>
  export default {
    mounted() {
      const childVm = this.$refs.childRef;

      childVm.userInfo = { name: '张三', age: 18 };
    }
  }
</script>

子元素($children):

// parent
const childVm = this.$children[0];

// 只针对唯一子组件有效,$children 不保证顺序

# 1.2. 子组件 --> 父组件

自定义事件:

<!-- child -->
<script>
  export default {
    methods: {
      submit() {
        this.$emit('submit', { name: '张三', age: 18 });
      }
    }
  }
</script>

<!-- parent -->
<template>
  <Child @submit="handleSubmit" />
</template>

# 1.3. 兄弟组件

说明:

  • 通过共同的祖先组件作为中间人, $parent$root
// brother1
this.$parent.$on('add', handleAdd);

// brother2
this.$parent.$emit('add', { msg: 'hello' });

# 1.4. 祖先 --> 后代

说明:

  • 多用于组件库
  • provide / inject: 祖先给后代传值(属性传值)
<!-- 祖先 -->
<script>
  export default {
    provide() {
      return {
        msg: 'hello',
        parent: this, // 把自己的实例也传过去
      }
    }
  }
</script>

<!-- 后代 -->
<script>
  export default {
    inject: [
      'msg',
      'parent'
    ]
  }
</script>

# 1.5. 任意两个组件

说明:

  • 事件总线 或 vuex

事件总线:

// main.js
new Vue({
  beforeCreated() {
    this.$bus = new Vue();
  }
});

// 1.vue
this.$bus.$on('add', ({ msg }) => {
  console.log(msg);
})


// 2.vue
this.$bus.$emit('add', { msg: 'hello' });

vuex:

  • 创建唯一的全局数据管理者 store,通过它管理数据并通知组件状态变更。

# 2. 插槽

插槽语法是 Vue 实现的内容分发 API,用于复合组件开发。

该技术在通用组件库开发中有大量应用。

# 2.1. 匿名插槽

<!-- child -->
<template>
  <div class="panel">
      <slot></slot>
  </div>
</template>

<!-- parent -->
<template>
  <Panel>
    内容
  </Panel>
</template>

# 2.2. 具名插槽

将内容分发到指定的位置

<!-- child -->
<template>
  <div class="panel">
    <slot name="body">内容</slot>
  </div>
</template>

<!-- parent -->
<template>
  <Panel>
    <template v-slot:body>
      内容xxx
    </template>
  </Panel>
</template>

# 2.3. 作用域插槽

父组件使用子组件传递的数据

<!-- child -->
<template>
  <div class="panel">
    <slot name="test" :list="list"></slot>
  </div>
</template>
<script>
export default {
  data() {
    return {
      list: ['a', 'b', 'c']
    }
  }
}
</script>

<!-- parent -->
<template>
  <Child>
    <template v-slot:test="{ list }">
      <div v-for="item in list" :key="item">{{ item }}</div>
    </template>
  </Child>
</template>

# 3. v-model 和 .sync

说明:

  • 一般情况下,父组件传递的属性 子组件是无法修改的
  • v-model.sync 可以。

区别:

  • v-model 常用于表单元素或与表单元素相关的组件
  • .sync 常用于非表单元素的组件

应用场景:

  • 对话框

    • 父组件引入对话框(子组件)
    • 父组件通过属性绑定(visible)控制子组件的显示和隐藏
    • 子组件可以主动关闭自己,并通知父组件改变 visible 的值,避免父组件无法再次打开子组件的情况
    • 比如: Dialog (visible.sync)—— element-ui (opens new window)
  • 其他(待补充)

# 3.1. v-model

表单组件:

  • 可直接使用,默认绑定 value 属性,监听 input 事件

    <template>
      <!-- 不使用 v-model -->
      <input :value="username" @input="username = $event.target.value">
    
      <!-- 使用 v-model -->
      <input v-model="username">
    </template>
    <script>
    export default {
      data() {
        return {
          username: '',
        }
      }
    }
    </script>
    

自定义组件:

  • 需要指定 model 属性,默认值: { prop: 'value', event: 'input' }

    <template>
      <input :value="message" @input="$emit('update-message', $event.target.value)" />
    </template>
    <script>
    export default {
      props: [
        'message',
      ],
      model: {
        prop: 'message',  // 绑定的属性名
        event: 'updateMessage', // 侦听的事件名
      }
    }
    </script>
    

# 3.2. .sync 修饰符

说明:

  • .sync 也是语法糖,省略了监听 @update:属性名
<!-- parent -->
<template>
  <Child :message.sync="content" />
</template>
<script>
export default {
  data() {
    return {
      content: '张三',
    }
  }
}
</script>

<!-- child -->
<script>
export default {
  props: [
    'message'
  ],
  mounted() {
    setTimeout(() => {
      this.$emit('update:message', '娃哈哈');
    }, 2000);
  }
}
</script>

# 4. 弹出层(弹窗)

说明:

  • alert 、 confirm 等弹窗组件
  • 它们在当前 Vue 实例之外独立存在,通常挂载到 body
  • 它们通过 JS 动态创建的,不需要在任何组件中声明

使用方式:

const dialog = this.$dialog({ title: '提示', content: '确认删除?' });

dialog.show();
// dialog.hide();

示例:

  • App.vue

    <!--04-src-dialog/App.vue-->
    <template>
      <div id="app">
        <button @click="handleClickOpenDialogButton">open a dialog</button>
        <button @click="handleClickCloseDialogButton">close the dialog</button>
      </div>
    </template>
    <script>
    import create from './create';
    import Dialog from './Dialog';
    
    export default {
      methods: {
        handleClickOpenDialogButton() {
          const dialog = create(Dialog, { title: '提示', content: '中奖了!!!'});
    
          dialog.show();
    
          this.dialog = dialog;
        },
        handleClickCloseDialogButton() {
          this.dialog.hide();
        },
      }
    }
    </script>
    
  • Dialog.vue

    <!--04-src-dialog/Dialog.vue-->
    <template>
      <div
        v-show="visible"
        class="dialog"
      >
        <div class="dialog__head">{{ title }}</div>
        <div class="dialog__body">{{ content }}</div>
      </div>
    </template>
    <script>
    export default {
      props: {
        title: String,
        content: String,
      },
    
      data() {
        return {
          visible: false,
        }
      },
    
      methods: {
        show() {
          this.visible = true;
        },
    
        hide() {
          this.visible = false;
    
          if (typeof this.$$destroy === 'function') {
            this.$$destroy();
          }
        },
      }
    }
    </script>
    
  • create.js

    /*04-src-dialog/create.js*/
    import Vue from 'vue';
    
    /**
     * 从头创建指定组件实例,并挂载到 document.body
    * 
    * @param component
    * @param props
    * @return {Vue|VNode}
    */
    export default function create(component, props) {
      const vm = new Vue({
        render(createElement) {
          return createElement(component, { props });
        }
      }).$mount(); // 挂载到“内存 DOM”(生成 $el)
    
      // 将 “内存 DOM” 添加到 DOM 树
      document.body.appendChild(vm.$el);
    
      const componentVm = vm.$children[0];
    
      componentVm.$$destroy = () => {
        document.body.removeChild(vm.$el);
        vm.$destroy();
      };
    
      return componentVm;
    }
    

# 5. 递归组件

说明:

  • 递归组件是可以在它们自己模板中调用自身的组件

条件:

  • 组件有 name 属性
  • 数据有 children 属性
  • 收敛条件 node.children && node.children.length > 0

示例:

  • App.vue

    <!--05-src-tree/App.vue-->
    <template>
      <div id="app">
        <Tree :data="tree"></Tree>
      </div>
    </template>
    <script>
    import Tree from './Tree.vue';
    
    export default {
      components: {
        Tree
      },
      data() {
        return {
          tree: [
            {
              id: '1',
              label: '一级节点-1',
              children: [
                {id: '1-1', label: '二级节点-1'},
                {id: '1-2', label: '二级节点-2'},
              ]
            },
            {
              id: '2',
              label: '一级节点-2',
              children: [
                {id: '2-1', label: '二级节点-1'},
                {
                  id: '2-2', label: '二级节点-2',
                  children: [
                    {id: '2-2-1', label: '三级节点-1'},
                    {id: '2-2-2', label: '三级节点-2'},
                  ]
                },
              ]
            },
          ]
        };
      },
    }
    </script>
    
  • Tree.vue

    <!--05-src-tree/Tree.vue-->
    <template>
      <div class="tree">
        <TreeNode v-for="node in data" :node="node" :key="node.id" />
      </div>
    </template>
    <script>
    import TreeNode from './TreeNode.vue';
    
    export default {
      name: 'Tree',
    
      components: {
        TreeNode,
      },
    
      props: {
        data: {
          type: Array,
          required: true,
        }
      },
    }
    </script>
    
  • TreeNode.vue

    <!--05-src-tree/TreeNode.vue-->
    <template>
      <div class="tree-node">
        <div class="tree-node__label">{{ node.label }}</div>
        <div
          v-if="node.children && node.children.length > 0"
          class="tree-node__sub-tree"
        >
          <TreeNode v-for="subNode in node.children" :node="subNode" :key="subNode.id" />
        </div>
      </div>
    </template>
    <script>
    export default {
      name: 'TreeNode',
      props: {
        node: {
          type: Object,
        }
      }
    }
    </script>
    <style>
    .tree-node {
      font-size: 14px;
      padding: 8px 16px;
    }
    .tree-node__label {
    
    }
    .tree-node__sub-tree {
      padding-left: 24px;
    }
    </style>
    

# 6. 路由

# 6.1. 基础

vue-cli 添加路由:

vue add router

指定路由器:

new Vue({
  router,
  render: (h) => h(App),
}).$mount('#app');

路由视图:

<router-view />

路由链接:

<router-link to="/">home</router-link>
<router-link to="/about">about</router-link>

# 6.2. 路由传参

说明:

  • 将路由参数直接设置到路由对应组件的 props

示例:

  • router.js

    /*06-src-router/router.js*/
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import Detail from './Detail.vue'
    
    Vue.use(VueRouter)
    
    function setQueryToPropsOfComponent(route) {
      return route.query;
    }
    
    const routes = [
      // 动态路由
      { path: '/dynamic-detail/:id', component: Detail, props: true },
    
      { path: '/detail', component: Detail, props: setQueryToPropsOfComponent },
    ]
    
    const router = new VueRouter({
      mode: 'hash',
      base: '06-src-router.html',
      routes
    })
    
    export default router
    
  • App.vue

    <!--06-src-router/App.vue-->
    <template>
      <div id="app">
        <ul>
          <li><router-link to="/detail?id=123">/detail?id=123</router-link></li>
          <li><router-link to="/dynamic-detail/456">/dynamic-detail/456</router-link></li>
        </ul>
    
        <router-view/>
      </div>
    </template>
    
  • Detail.vue

    <!--06-src-router/Detail.vue-->
    <template>
      <div class="detail">
        <p>id: {{ id }}</p>
    
        <p>$route.query: {{ $route.query }}</p>
    
        <p>$route.params: {{ $route.params }}</p>
      </div>
    </template>
    <script>
    export default {
      props: {
        id: {
          type: String,
        }
      },
    }
    </script>
    

# 6.3. 路由守卫

说明:

  • 路由导航过程中有若干生命周期钩子,可以在这里实现逻辑控制。

全局前置守卫:

const route = {
  path: '/foo',
  component: Foo,
  meta: { auth: true }, // auth 需要认证 
};

router.beforeEach((to, from ,next) => {
  if (to.meta.auth && !store.state.isLogin) {
    next({ name: 'Login' });
    return;
  }
  
  next();
});

单个路由独享的守卫:

const route = {
  path: '/foo',
  component: Foo,
  beforeEnter: (to, from, next) => {
    // ...
  }
};

组件内的守卫:(hooks)

const Foo = {
  template: `...`,
  beforeRouteEnter(to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
  },
  beforeRouteUpdate(to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
  },
  beforeRouteLeave(to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
  }
}

# 6.4. 动态路由

利用 $router.addRoutes() 可以实现动态路由添加,常用于用户权限控制。

const routersOfServer = [
  { 
    path: '/oa',
    component: 'oa', 
    children: [
      { path: '/oa/user-manage', component: 'oa.UserManage' }  
    ],
  },
  { 
    path: '/crm',
    component: 'crm', 
    children: [
      { path: '/crm/user-manage', component: 'crm.UserManage' }  
    ],
  },
];

// 异步获取路由
routerService.getRouters().then((routesOfServer) => {
  const routes = routesOfServer.map((route) => mapComponent(route));
  router.addRoutes(routes);
});

// 隐射关系
const componentsMap = {
  'oa': Oa,
  'oa.UserManager': Oa.UserMamager,
  'crm': Crm,
  'crm.UserManager': Crm.UserMamager,
};

// 递归替换
function mapComonent(route) {
  route.component = componentsMap[route.component];
  
  if (route.children) {
    route.children = route.children.map((childRoute) => mapComonent(childRoute));
  }
  
  return route;
}

# 6.5. 面包屑导航

利用 $route.matched 可以得到路由匹配数组,按顺序解析可以得到路由层次关系

<!-- Breadcrumb.vue -->
<template>
  <div>
    <ul><li v-for="item in pageNameList" :key="item">{ item }</li></ul>
  </div>
</template>
<script>
export default {
  data() {
    return {
      pageNameList: [],
    }
  },
  watch: {
    $route() {
      /*
      [ 
        { path: '/oa', meta: { name: '首页' } }, 
        { path: '/oa/user-manage', meta: { name: '用户管理' } } 
      ]
      */
      console.log(this.$route.matched); 
      
      this.pageNameList = this.$route.matched.map((item) => item.meta.name);
    }
  }
}
</script>

1:13:17

# 7. 生命周期

父组件的 created 早于子组件

父组件的 mounted 晚于子组件

本章目录