vue-组件


1.1 第一个组件

1.1.1 官方 demo

<div id="components-demo">
  <button-counter></button-counter>
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>

<script>
  // 定义一个名为 button-counter 的新组件
  Vue.component("button-counter", {
    data: function () {
      return {
        count: 0,
      };
    },
    template:
    '<button v-on:click="count++">You clicked me {{ count }} times.</button>',
  });
</script>

<script>
  new Vue({ el: "#components-demo" });
</script>

说明:

  1. 组件定义是由 component 完成,第一个参数为组件的名称,第二个参数为组件相关的数据、方法等;
  2. 组件中只能包含一个根元素,如果有多个元素,则可使用一个元素包裹起来;
  3. 组件的使用方式和 html 标签相同,尖括号包裹组件名即可;
  4. 根实例中 el 指定 DOM 实例挂载位置,关于挂载和实例数据见后续笔记;
  5. 组件可以复用,但需要注意组件内的 data 必须是一个函数,这样每个实例的数据都是 data 返回对象的独立拷贝,否则实例数据会在组件内共享。

1.1.2 vue-cli 创建工程

下面笔记的代码使用 vue-cli 创建工程,不再使用单个 html,起步:

npm install -g @vue/cli  # 安装 vue-cli 新版
vue ui  # 启动 vue 项目 web 管理工具,ui 方式选择插件、并 install 到 node_modules 内。

npm run serve  # 启动项目,实际脚本定义在 package.json 的 scripts 内。

下面使用的版本是:

  • vue: 2.6.11 (package.json 中)
  • vue-cli: 4.5.0 (命令 vue -V)

注意:对于这个空白项目,启动后会提示 Access to XMLHttpRequest at 'http://10.20.19.9:8080/sockjs-node/info?t=1598950194338' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.,解决方案是在项目跟目录下新建 vue.config.js 写入:

module.exports = {
  devServer: {
    host: 'localhost',
    port: 8080
  }
}

关于 vue.config.js 的官方文档: 配置参考 - 全局 CLI 配置#devserverwebpack - DevServer#devserverhost

为了不每个这样的简单项目都创建一次,可以创建多个 App.vue,如: App-1.vue, App-2.vue,然后在 main.js 中 import App from './App-3.vue' 改为自己的需要运行的 App vue 文件即可。

在 vue-cli 创建的工程中改写上面官网的 demo:

  1. 在 components 下创建文件 ButtonClickCounter.vue:
    <template>
     <button v-on:click="count++">You clicked me {{ count }} times.</button>
    </template>
    
    <script>
    export default {
     name: "button-click-cnt",  // 有 name 选项,错误日志会更易读,且 vue-devtool 调试有此语义信息
     data: function () {
       return {
         count: 0,
       };
     },
    };
    </script>
    
    
  2. App-2.vue 内通过 components 方式注册组件(局部注册):
    <template>
     <div id="app">
       <!-- <ButtonClickCounter/> -->
       <button-click-counter/>
     </div>
    </template>
    
    <script>
    import ButtonClickCounter from "./components/ButtonClickCounter.vue";
    
    export default {
     name: "App",
     components: {
       // ButtonClickCounter,  // 不设置组件名,默认使用组件文件名的 PascalCase 以及 kebab-case
       "button-click-counter": ButtonClickCounter,  // 设置组件名,设置后默认的不再有效
     },
    };
    </script>
    
    
  3. npm run serve 启动项目,可看到效果。

1.2 组件的组织与注册

实际中通常会有多个组件、以及组件之间相互依赖,形成组件树形式。

1.2.1 组件名

组件名相关规则,例子见上面 vue-cli 创建的工程:

  1. 组件文件名建议使用 PascalCase(单词首字母大写命名),import 后为文件名,from 为文件路径;
  2. components 中可以指定组件名,官方推荐使用 PascalCase 或 kebab-case。
  3. 组件名如果在 components 中未指定,则会使用组件文件名的 PascalCase 和 kebab-case(短横线分隔命名),二者均可;

1.2.2 组件注册

组件的注册有全局和局部两种方式:

  • 通过 Vue.component 创建的组件是全局组件,实际中不推荐全局注册;
  • export default { components: {}} 为局部注册:
  • 对于子组件中引入的组件,如果是局部注册,则还需要给子组件引入。

例,子组件中引入组件:

  1. ChildChildComponent.vue:
    <template>
     <div>
       {{ fullName }}
     </div>
    </template>
    
    <script>
    export default {
     name: "child-child-component",
     props: ["fullName"]
    }
    </script>
    
  2. ChildComponent.vue:
    <template>
     <child-child-component :fullName="fullName"></child-child-component>
    </template>
    
    <script>
    import ChildChildComponent from "./ChildChildComponent";
    
    export default {
     props: {
       firstName: {
         type: String,
         default: "yourFirstName",
       },
       lastName: String,
     },
     components: {
       "child-child-component": ChildChildComponent,
     },
     computed: {
       fullName: function() {
         return this.firstName + " " + this.lastName;
       },
     },
    };
    </script>
    
  3. App-3.vue:
    <template>
     <div id="app">
       <child-component :lastName="lastName"></child-component>
     </div>
    </template>
    
    <script>
    import ChildComponent from "./components/ChildComponent";
    
    export default {
     name: "App",
     components: {
       "child-component": ChildComponent,
     },
     data: function() {
       return {
         lastName: "Rick555"
       };
     },
    };
    </script>
    

1.3 向子组件传数据 - prop

1.3.1 基本使用

由于 html 中 attri 大小写不明感,浏览器会将大写转为小写,所以在使用 prop 时推荐使用等价的 kebab-case,在字符串模板中(template 标签的内容写在字符串内)不存在此限制。

  1. 新建组件,SendDataToComponent1.vue:
    <template>
     <div>
       <span>一个字符串:</span>
       {{ aString }}
     </div>
    </template>
    
    <script>
    export default {
     name: "send-data-to-component-1",
     props: ["aString"],
    };
    </script>
    
    
  2. App-4.vue:
    <template>
     <div id="app">
            <span>aString</span>
       <send-data-to-component-1 :aString="stringFromApp" />
       <span>astring</span>
       <send-data-to-component-1 :astring="stringFromApp" />
       <span>a-string: 推荐</span>
       <send-data-to-component-1 :a-string="stringFromApp" />
     </div>
    </template>
    
    <script>
    import SendDataToComponent1 from "./components/SendDataToComponent1.vue";
    
    export default {
     name: "App",
     components: {
       "send-data-to-component-1": SendDataToComponent1,
     },
     data: function() {
       return {
         stringFromApp: "stringFromApp",
       };
     },
    };
    </script>
    
    

1.3.2 prop 类型

prop 即可以使用字符串数组也可以使用对象形式:

  • 字符串数组: props: ['a', 'b', 'c'], 如上面例子使用的 props: ["aString"];

  • 对象形式(推荐): 方便为每个属性指定数值类型。

    • 如:
    props: {
      aString: String,
      aNumber: Number,
      aArray: Array,
      aObject: Object,
      aFunction: Function,
    },
    
    • 支持定制参数是否必须、默认值、自定义验证函数。更多见官方文档: prop#Prop-验证

例:

  1. 组件:SendDataToComponent2.vue:
    <template>
     <div>
       <div>
         <span>一个字符串:</span>
         {{ aString }}
       </div>
       <div>
         <span>一个数值:</span>
         {{ aNumber }}
       </div>
       <div>
         <span>一个数组:</span>
         {{ aArray }}
       </div>
       <div>
         <span>一个对象:</span>
         {{ aObject }}
       </div>
       <div>
         <span>一个函数:</span>
         {{ aFunction() }}
       </div>
     </div>
    </template>
    
    <script>
    export default {
     name: "send-data-to-component-1",
     props: {
       aString: {
         type: String,
         default: "aString-o_o"
       },
       aNumber: {
         type: Number,
         required: true
       },
       aArray: Array,
       aObject: {
         type: Object,
         default: function () {
           return {message: "object default property"}
         }
       },
       aFunction: Function,
     },
    };
    </script>
    
    
  2. App-5.vue:
    <template>
     <div id="app">
       <send-data-to-component-2
         :aNumber="55"
         :aArray="[111, 222, 33]"
         :aObject="objectFromApp"
         :aFunction="functionFromApp"
       />
     </div>
    </template>
    
    <script>
    import SendDataToComponent2 from "./components/SendDataToComponent2.vue";
    
    export default {
     name: "App",
     components: {
       "send-data-to-component-2": SendDataToComponent2,
     },
     data: function() {
       return {
         stringFromApp: "stringFromApp",
         objectFromApp: {
           name: "Rick",
           habits: "pee",
         },
       };
     },
     methods: {
       functionFromApp() {
         return "functionFromApp: called by component";
       },
     },
    };
    </script>
    
    

注意:prop 会在一个组件实例创建之前进行验证,所以实例的 property (如 datacomputed 等) 在 defaultvalidator 函数中不可用。

1.3.3 prop 数据流动

prop 数据是单向下行流动,即父级向子级,在使用需要注意:

  1. 如果 prop 传递的是一个初始值,推荐在组件内 data 中定义此 property;
  2. 如果 prop 传递的原始数据需要计算,应该在 computed 或者 watch 中进行;
  3. 如果 prop 传递的是数组或对象,由于这俩类型在 js 中是引用传递,即传递的是地址,所以在子组件中更改,也会影响父级,见下例;

例:

  1. 组件 propTransmit.vue:
    <template>
     <div>
       <span>子: a string value: {{ money }}</span>
       <br />
       <span>子: object value: {{ creator }}</span>
       <br />
       <button @click="changeMoneyInComponent">changeMoneyInComponent</button>
       <button @click="changeCreatorInComponent">changeCreatorInComponent</button>
     </div>
    </template>
    <script>
    export default {
     name: "prop-transmit",
     props: {
       money: Number,
       creator: Object,
     },
     methods: {
       changeMoneyInComponent() {
         this.money += 100;
       },
       changeCreatorInComponent() {
         this.creator.year += 5;
       },
     },
    };
    </script>
    
  2. App-6.vue:
    <template>
     <div id="app">
       <hr />
       <div>
         <p>测试 prop 传递</p>
         <span>父: a string value: {{ money }}</span>
         <br />
         <span>父: object value: {{ creator }}</span>
         <br />
         <button @click="changeMoneyInApp">changeMoneyInApp</button>
         <button @click="changeCreatorInApp">changeMoneyInApp</button>
         <prop-transmit :money="money" :creator="creator" />
       </div>
     </div>
    </template>
    
    <script>
    import propTransmit from "./components/propTransmit.vue";
    
    export default {
     name: "App",
     components: {
       "prop-transmit": propTransmit,
     },
     data: function() {
       return {
         money: 100,
         creator: {
           name: "Rick",
           year: 1999,
         },
       };
     },
     methods: {
       changeMoneyInApp() {
         this.money -= 100;
       },
       changeCreatorInApp() {
         this.creator.year -= 5;
       },
     },
    };
    </script>
    

这段代码现象及反映出的规则:

  1. 改变父组件的数据,子组件数据会重新被初始,如:在父中改变 money 或者 creator 后,子组件 prop 的值都会被重新传入;
  2. 改变子组件中 creator 中的数据,父组件中也会改变;
  3. 改变子组件 creator 的数据后,money 数据会被重置成父组件传入的值,因为子组件修改 creator 相当于改变父级的 creator,触发了规则 1;

故实际中不要随意在子组件改变 prop 的值,无论是数值、字符串还是对象、数值。推荐在子组件 data 中定义一个变量,将 prop 赋值过去,如:

props: ["initMoney"],
data: function () {
    return {
        money: this.initMoney
    }
}

1.4 插槽

1.4.1 插槽基础

插槽使用的指令在 2.6.0 之后有更改,下面笔记均使用 2.6.0 的新指令。

在上面代码中,如果父级在子组件标签写任何内容,都会被直接删除(因为实例挂载就是用 vue 生成的 DOM 替换到挂载点上),如上面的:

<prop-transmit :money="money" :creator="creator">页面上是否显示子组件标签的内容?</prop-transmit>

标签内的文字是不会显示在页面的。

而对于实际中经常需要向组件传递内容的,此时就需要使用插槽来实现。

slot 标签提供一个插槽,组件起始标签和结束标签之间的任何内容都会被正确地渲染,需要注意的是作用域问题:

  • 组件标签内插入的变量或对象等必须是当前实例中的,即在 App 中使用组件 CompA,并向 CompA 插入数据时,那这些数据也必须是 App 实例的。
  1. 组件 SlotContent.vue:
    <template>
     <div>
       <h4>{{title}}</h4>
       <slot></slot>
     </div>
    </template>
    
    <script>
    export default {
     name: "slot-content",
     props: {
       title: String,
     },
    }
    </script>
    
  2. App-7.vue
    <template>
     <slot-content :title="'标题'">
       插槽插入内容<br>
       <strong>{{ creator }}</strong>
       <!-- 这个 creator 必须是当前实例的 -->
     </slot-content>
    </template>
    
    <script>
    import SlotContent from "./components/SlotContent.vue";
    
    export default {
     name: "App",
     components: {
       "slot-content": SlotContent,
     },
     data: function () {
       return {
         creator: "Rick"
       }
     },
    };
    </script>
    

1.4.2 具名插槽

  • 作用:父级插入内容时插入到组件内的指定位置。

  • 用法:在组件内 slot 标签的 name 属性设置插槽名,父级使用时在 template 标签的 v-slot 传递对应的 name,语法格式为:

    <!-- 子组件内设置插槽名 -->
    <slot name="slotName"></slot>
    
    <!-- 父级插入到指定的插槽 -->
    <子组件>
    <template v-slot:slotName>
      <h4>header</h4>
    </template>
    </子组件>
    
    • 如果组件 slot 插槽未指定名,则会分配默认名 default,在父级插入时如果未指定 name,则会插入到 default 插槽。

例:

  1. 组件 NamedSlot.vue:
    <template>
     <div>
       <header>
         <slot name="header"></slot>
       </header>
       <main>
         <!-- slot 未指定 name,则默认名为 default  -->
         <slot></slot>
       </main>
       <footer>
         <slot name="footer"></slot>
       </footer>
     </div>
    </template>
    
    <script>
    export default {
     name: "named-slot",
     data: function() {
       return {};
     },
     methods: {},
    };
    </script>
    
  2. App-8.vue:
    <template>
     <div>
       <named-slot>
         <template v-slot:header>
           <h4>header</h4>
         </template>
    
         <!-- 未指定 v-slot:name,则默认插入到 slot 名为 default -->
         <h4>main</h4>
    
         <template v-slot:footer>
           <h4>footer</h4>
         </template>
       </named-slot>
     </div>
    </template>
    
    <script>
    import NamedSlot from "./components/NamedSlot.vue";
    
    export default {
     name: "App",
     components: {
       "named-slot": NamedSlot,
     },
    };
    </script>
    

1.4.3 作用域插槽

  • 作用:使父级能够访问子组件的数据;

  • 用法:在组件内 slot 标签的上绑定属性,父级使用时在 template 标签的 v-slot 取绑定的属性,语法格式为:

    <!-- 子组件内绑定数据到属性,name 具名可以不写,默认为 default -->
    <slot name="slotName" :attriName="data"></slot>
    
    <!-- 父级取子组件绑定的数据 -->
    <子组件>
    <!-- 可具名不写,则会默认指定 default 插槽 -->
    <template v-slot:slotName="scoped">
      sloted data: {{scoped.attriName}}
    </template>
    </子组件>
    

例:

  1. 组件 ScopedSlot.vue:
    <template>
     <div>
       creator: {{ creator }}<br />
       <!-- 子组件内绑定数据到属性,name 具名可以不写,默认为 default -->
       <slot :creator1="creator"></slot>
     </div>
    </template>
    
    <script>
    export default {
     name: "scoped-slot",
     data: function() {
       return {
         creator: {
           firstName: "Rick",
           lastName: "Sanchez",
         },
       };
     },
    };
    </script>
    
  2. App.vue:
    <template>
     <scoped-slot>
       <!-- 具名不写,则默认指定 default 插槽 -->
       <template v-slot="scoped">
         sloted data: {{ scoped.creator1 }}
        </template>
     </scoped-slot>
    </template>
    
    <script>
    import ScopedSlot from "./components/ScopedSlot.vue";
    
    export default {
     name: "App",
     components: {
       "scoped-slot": ScopedSlot,
     },
    };
    </script>
    
使用场景 - 取表格组件内数据

例如,在 element-ui 的表格中,对每行进行某项操作,如按钮或者弹窗显示。

下面示例弹窗显示表格当前行 echarts 折线图:

<template>
  <div>
    <el-table :data="customerInfo">
      <el-table-column label="customerName" prop="name"/>
      <el-table-column label="consumeRate">
        <template v-slot="scope">
          <el-progress
              :percentage="parseFloat(scope.row.consumeRate)"
              :stroke-width="15"
              :text-inside="true"
              :color="customBarColor"
          ></el-progress>
        </template>
      </el-table-column>
      <el-table-column label="consumeHistory">
        <template v-slot="scope">
          <el-popover
              placement="left-end"
              title="消费趋势"
              width="600"
              trigger="click"
              @after-enter="getConsumeHistory(scope.row.id)"
          >
            <el-button slot="reference">
              <span>历史</span>
              <img src="@/assets/image/history.png" alt="history"/>
            </el-button>
            <div class="chart-container">
              <div class="chart" :ref="scope.row.id"></div>
            </div>
          </el-popover>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script>
import Echarts from "echarts";

export default {
  name: "ScopedTableRowData",
  data: function () {
    return {
      customerInfo: [
        {name: "c1", id: "001", consumeRate: "31"},
        {name: "c2", id: "002", consumeRate: "80"},
        {name: "c3", id: "003", consumeRate: "60"},
      ],
      chart: null,
    }
  },
  methods: {
    customBarColor(percentage) {
      if (percentage < 70) {
        return '#85ea50';
      } else {
        return '#ff0000'
      }
    },
    getConsumeHistory(customerId) {
      let consumeHistory = [{
        date: "2020-01-01",
        consume: 50,
      }, {
        date: "2020-01-05",
        consume: 19,
      }];

      this.renderChart(customerId, consumeHistory);
    },
    renderChart(customerId, consumeHistory) {
      this.chart = Echarts.init(this.$refs[customerId]);

      let datasource = {};

      Array.prototype.forEach.call(consumeHistory, (ele) => {
        if (datasource.time) {
          datasource.time.push(ele.date)
        } else {
          datasource.time = [ele.date]
        }
        if (datasource.history) {
          datasource.history.push(ele.consume)
        } else {
          datasource.history = [ele.consume]
        }
      });

      this.chart.setOption({})
    }
  },
};
</script>

<style lang="scss" scoped>
.chart-container {
  margin-left: 40px;
  margin-top: 20px;

  .chart {
    height: 200px;
  }
}
</style>

需要引入 依赖,这里使用 cnpm:

# scss 编译依赖
npm i sass-loader -D  
npm i node-sass -D

# element-ui 依赖
npm i element-ui -S
# echarts 依赖
npm i element-ui -S

-S: --save,将包信息添加到 package.json 的 dependencies(生产阶段的依赖)

-D: --save-dev,将包信息添加到 package.json 的 devDependencies(开发阶段的依赖)

引入依赖后,还需要引入 element-ui 及其样式,通常在 main.js:

import ElementUI from 'element-ui';
import "element-ui/lib/theme-chalk/index.css"

Vue.use(ElementUI)

1.4.4 插槽新旧属性

上面例子使用的都是 2.6.0 的新属性 v-slot,而被遗弃的旧属性为:slot 与 slot-scope,旧属性的 slot 来指定具名插槽,slot-scope 表示为作用域插槽。可以看出新属性 v-slot 其实就是这俩旧属性的综合,例:

<!-- 新属性 v-slot -->
<template v-slot:default="scoped">
    sloted data: {{ scoped.creator }}
</template>

<!-- 旧属性 slot, slot-scoped -->
<template slot="default" slot-scope="scoped">
    sloted data: {{ scoped.creator }}
</template>

1.5 总结-命名规范

常见的四种:

  • PascalCase:单词首字母大写,如 LastName
  • camelCase: 首个单词之外的单词首字母大写,如 lastName
  • kebab-case: 单词之间使用中划线间隔,如 last-name
  • snake_case: 单词之间使用下划线间隔,如 last_name

Vue 中推荐的命名方式:

  1. 组件文件名:PascalCase;
  2. 组件名: PascalCase 或者 kebab-case;
  3. prop 数据: 定义时 camelCase,html 标签内使用时 kebab-case;
  4. 自定义事件名: kebab-case。

三和四是因为 DOM 中大小写不明感,会被全部替换为小写,导致数据或者自定义的事件无法被访问。

1.6 总结-父级与子组件的数据传递

父向子:prop,具名插槽

子向父:自定义事件(未学习),作用域插槽

评论
还没有评论
    发表评论 说点什么