单文件组件
在很多Vue项目中,我们使用 Vue.component 来定义全局组件,紧接着用 new Vue({ el: '#app '}) 在每个页面内指定一个容器元素。
这种方式在很多中小规模的项目中运作的很好,在这些项目里 JavaScript 只被用来加强特定的视图。但当在更复杂的项目中,或者你的前端完全由 JavaScript 驱动的时候,下面这些缺点将变得非常明显:
- 全局定义强制要求每个 component 中的命名不得重复
- 字符串模板 缺乏语法高亮,在 HTML 有多行的时候,需要用到丑陋的 ``
- 不支持 CSS 意味着当 HTML 和 JavaScript 组件化时,CSS 明显被遗漏
- 没有构建步骤 限制只能使用 HTML 和 ES5 JavaScript, 而不能使用预处理器,如 Pug (formerly Jade) 和 Babel
文件扩展名为 .vue 的 single-file components(单文件组件) 为以上所有问题提供了解决方法,并且还可以使用 webpack 或 Browserify 等构建工具。
这是一个文件名为 Hello.vue 的简单实例:
现在我们获得
- 完整语法高亮
- CommonJS 模块
- 组件作用域的 CSS
在看完上文之后,建议使用官方提供的 Vue CLI 3脚手架来开发工具,只要遵循提示,就能很快地运行一个带有.vue组件,ES2015,webpack和热重载的Vue项目
Vue CLI3
基本配置
- 安装Nodejs
- 保证Node.js8.9或更高版本
- 终端中输入node -v,保证已安装成功
- 安装淘宝镜像源
- npm install -g cnpm --registry=https://registry.npm.taobao.org
- 以后的npm可以用cnpm代替
- 安装Vue Cli3脚手架
- cnpm install -g @vue/cli
- 检查其版本是否正确
- vue --version
快速原型开发
使用 vue serve 和 vue build 命令对单个 *.vue 文件进行快速原型开发,不过这需要先额外安装一个全局的扩展:
npm install -g @vue/cli-service-global
vue serve 的缺点就是它需要安装全局依赖,这使得它在不同机器上的一致性不能得到保证。因此这只适用于快速原型开发。
需要的仅仅是一个 App.vue 文件:
<template>
<div>
<h2>hello world 单页面组件</h2>
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>
然后在这个 App.vue 文件所在的目录下运行:
vue serve
启动效果:
网页效果:
但这种方式仅限于快速原型开发,终归揭底还是使用vue cli3来启动项目
创建项目
vue create mysite
详细的看官网介绍
购物车
App.vue
<ul>
<li v-for="(item, index) in cartList" :key="index">
<h3>{{item.title}}</h3>
<p>¥{{item.price}}</p>
<button @click='addCart(index)'>加购物车</button>
</li>
</ul>
cartList: [
{
id:1,
title:'web全栈开发',
price:1999
},
{
id: 2,
title: 'python全栈开发',
price: 2999
}
],
新建Cart.vue购物车组件
<template>
<div>
<table border='1'>
<tr>
<th>#</th>
<th>课程</th>
<th>单价</th>
<th>数量</th>
<th>价格</th>
</tr>
<tr v-for="(c, index) in cart" :key="c.id" :class='{active:c.active}'>
<td>
<input type="checkbox" v-model='c.active'>
</td>
<td>{{c.title}}</td>
<td>{{c.price}}</td>
<td>
<button @click='subtract(index)'>-</button>
{{c.count}}
<button @click='add(index)'> </button>
</td>
<td>¥{{c.price*c.count}}</td>
</tr>
<tr>
<td></td>
<td colspan="2">{{activeCount}}/{{count}}</td>
<td colspan="2">{{total}}</td>
</tr>
</table>
</div>
</template>
<script>
export default {
name: "Cart",
props: ['name', 'cart'],
methods: {
subtract(i) {
let count = this.cart[i].count;
// if(count > 1){
// this.cart[i].count-=1
// }else{
// this.remove(i)
// }
count > 1 ? this.cart[i].count -= 1 : this.remove(i);
},
add(i) {
this.cart[i].count ;
},
remove(i) {
if (window.confirm('确定是否要删除')) {
this.cart.splice(i, 1);
}
}
},
data() {
return {}
},
created() {},
computed: {
activeCount() {
return this.cart.filter(v => v.active).length;
},
count() {
return this.cart.length;
},
total() {
// let num = 0;
// this.cart.forEach(c => {
// if (c.active) {
// num = c.price * c.count
// }
// });
// return num;
return this.cart.reduce((sum, c) => {
if (c.active) {
sum = c.price * c.count
}
return sum;
}, 0)
}
},
}
</script>
<style scoped>
.active {
color: red;
}
</style>
mock数据
简单的mock,使用自带的webpack-dev-server即可,新建vue.config.js扩展webpack设置
webpack官网介绍
module.exports = {
configureWebpack:{
devServer:{
// mock数据模拟
before(app,server){
app.get('/api/cartList',(req,res)=>{
res.json([
{
id:1,
title:'web全栈开发',
price:1999
},
{
id: 2,
title: 'web全栈开发',
price: 2999
}
])
})
}
}
}
}
访问http://localhost:8080/api/cartList 查看mock数据
使用axios获取接口数据npm install axios -S
created() {
axios.get('/api/cartList').then(res=>{
this.cartList = res.data
})
}
使用ES7的async await语法
async created() {
// try-catch解决async-awiat错误处理
try {
const { data } = await axios.get('/cartList')
this.cartList = data;
} catch (error) {
console.log(error);
}
},
数据持久化
localstorage vue监听器
如果组件没有明显的父子关系,使用中央事件总线进行传递
Vue每个实例都有订阅/发布模式的额实现,使用$on和$emit
main.js
Vue.prototype.$bus = new Vue();
App.vue
methods: {
addCart(index) {
const good = this.cartList[index];
this.$bus.$emit('addGood',good);
}
}
Cart.vue
data() {
return {
cart:JSON.parse(localStorage.getItem('cart')) || []
}
},
//数组和对象要深度监听
watch: {
cart: {
handler(n, o) {
const total = n.reduce((total, c) => {
total = c.count
return total;
}, 0)
localStorage.setItem('total', total);
localStorage.setItem('cart', JSON.stringify(n));
this.$bus.$emit('add', total);
},
deep: true
}
},
created() {
this.$bus.$on('addGood', good => {
const ret = this.cart.find(v => v.id === good.id);
if (ret) { //购物车已有数据
ret.count = 1;
} else {
//购物车无数据
this.cart.push({
...good,
count: 1,
active: true
})
}
})
},
更复杂的数据传递,可以使用vuex,后面课程会详细介绍
组件深入
组件分类
- 通用组件
- 基础组件,大部分UI都是这种组件,比如表单 布局 弹窗等
- 业务组件
- 与需求挂钩,会被复用,比如抽奖,摇一摇等
- 页面组件
- 每个页面都是一个组件v
使用第三方组件
比如vue最流行的element,就是典型的通用组件,执行npm install element-ui安装
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';
Vue.use(ElementUI);
new Vue({
el: '#app',
render: h => h(App)
});
在vue-cli中可以使用vue add element 安装
安装之前注意提前提交当前工作内容,脚手架会覆盖若干文件
发现项目发生了变化,打开App.vue,ctrl z撤回
此时可以在任意组件中使用
官网element-ui的通用组件,基本上都是复制粘贴使用,在这里就不一一赘述,后面项目中用到该库,咱们再一一去使用
关于组件设计,最重要的还是自己去设计组件,现在我们模仿element-ui提供的表单组件,手写实现表单组件m-form
先看一下element-ui的表单
新建FormElement.vue
<template>
<div>
<h3>element表单</h3>
<el-form
:model="ruleForm"
status-icon
:rules="rules"
ref="ruleForm"
label-width="100px"
class="demo-ruleForm"
>
<el-form-item label="用户名" prop="name">
<el-input type="text" v-model="ruleForm.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="pwd">
<el-input type="password" v-model="ruleForm.pwd" autocomplete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')">提交</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: "FormElement",
data() {
return {
ruleForm: {
name:'',
pwd:''
},
rules:{
name:[
{required:true,message:'请输入名称'},
{min:6,max:10,message:'请输入6~10位用户名'}
],
pwd:[{require:true,message:'请输入密码'}],
}
}
},
methods: {
submitForm(name) {
this.$refs[name].validate(valid=>{
console.log(valid);
if(valid){
alert('验证成功,可以提交')
}else{
alert('error 提交');
return false;
}
})
}
},
};
</script>
在App.vue组件中导入该组件,挂载,使用
组件设计
表单组件,组件分层
- Form负责定义校验规则
- FormtItem负责显示错误信息
- Input负责数据双向绑定
- 使用provide和inject内部共享数据
表单控件实现双向的数据绑定
Input.vue
<template>
<div>
<input :type="type" @input="handleInput" :value="inputVal">
</div>
</template>
<script>
export default {
props: {
value: {
type: String,
default: ""
},
type: {
type: String,
default: "text"
}
},
data() {
return {
//单向数据流的原则:组件内不能修改props
inputVal: this.value
};
},
methods: {
handleInput(e) {
this.inputVal = e.target.value;
// 通知父组件值的更新
this.$emit("input", this.inputVal);
}
}
};
</script>
<style scoped>
</style>
FormElement.vue
如果不传type表示默认值,在Input.vue的props中有说明
<m-input v-model="ruleForm.name"></m-input>
<m-input v-model="ruleForm.name" type='password'></m-input>
//数据
data() {
return {
ruleForm: {
name: "",
pwd: ""
},
rules: {
name: [
{ required: true, message: "请输入名称" },
{ min: 6, max: 10, message: "请输入6~10位用户名" }
],
pwd: [{ require: true, message: "请输入密码" }]
}
};
},
FormItem
- 获取当前输入框的规则
- 如果输入框和rule不匹配 显示错误信息
- Input组件中用户输入内容时,通知FormItem做校验
- 使用async-validator做出校验
FormItem.vue
<template>
<div>
<label v-if="label">{{label}}</label>
<slot></slot>
<!-- 校验的错误信息 -->
<p v-if="validateStatus=='error'" class="error">{{errorMessage}}</p>
</div>
</template>
<script>
import schema from "async-validator";
export default {
name: "FormItem",
data() {
return {
validateStatus: "",
errorMessage: ""
};
},
props: {
label: {
type: String,
default: ""
},
prop: {
type: String
}
}
};
</script>
<style scoped>
.error {
color: red;
}
</style>
FormElement.vue
<m-form-item label="用户名" prop="name">
<m-input v-model="ruleForm.name"></m-input>
</m-form-item>
<m-form-item label="密码" prop="pwd">
<m-input v-model="ruleForm.pwd" type="password"></m-input>
</m-form-item>
此时网页正常显示,但没有校验规则,添加校验规则
思路:比如对用户名进行校验,用户输入的用户名必须是6~10位
npm i asycn-validator -S
Input.vue
methods: {
handleInput(e) {
this.inputVal = e.target.value;
//....
//通知父组件校验,将输入框的值实时传进去
this.$parent.$emit("validate", this.inputVal);
}
}
FormItem.vue
import schema from "async-validator";
export default {
name: "FormItem",
data() {
return {
validateStatus: "",
errorMessage: ""
};
},
methods: {
validate(value) {//value为当前输入框的值
// 校验当前项:依赖async-validate
let descriptor = {};
descriptor[this.prop] = this.form.rules[this.prop];
// const descriptor = { [this.prop]: this.form.rules[this.prop] };
const validator = new schema(descriptor);
let obj = {};
obj[this.prop] = value;
// let obj = {[this.prop]:this.form.model[this.prop]};
validator.validate(obj, errors => {
if (errors) {
this.validateStatus = "error";
this.errorMessage = errors[0].message;
} else {
this.validateStatus = "";
this.errorMessage = "";
}
});
}
},
created() {
//监听子组件Input的派发的validate事件
this.$on("validate", this.validate);
},
//注入名字 获取父组件Form 此时Form我们还没创建
inject: ["form"],
props: {
label: {
type: String,
default: ""
},
prop: {
type: String
}
}
};
Form
- 声明props中获取数据模型(model)和检验规则(rules)
- 当FormItem组件挂载完成时,通知Form组件开始缓存需要校验的表单项
- 将缓存的表单项进行统一处理,如果有一个是错误,则返回false.(思路:使用promise.all()进行处理)
- 声明校验方法,供父级组件方法调用validate()方法
Form.vue
声明props中获取数据模型(model)和检验规则(rules)
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
name:'Form',
//依赖
provide(){
return {
// 将表单的实例传递给后代,在子组件中我们就可以获取this.form.rules和this.form.rules
form: this
}
},
props:{
model:{
type:Object,
required:true
},
rules:{
type:Object
}
},
}
</script>
当FormItem组件挂载完成时,通知Form组件开始缓存需要校验的表单项
FormItem.vue
mounted() {
//挂载到form上时,派发一个添加事件
//必须做判断,因为Form组件的子组件可能不是FormItem
if (this.prop) {
//通知将表单项缓存
this.$parent.$emit("formItemAdd", this);
}
}
Form.vue
created () {
// 缓存需要校验的表单项
this.fileds = []
this.$on('formItemAdd',(item)=>{
this.fileds.push(item);
})
},
将缓存的表单项进行统一处理,如果有一个是错误,则返回false.(思路:使用Promise.all()进行处理).
注意:因为Promise.all方法的第一个参数是数组对象,该数组对象保存多个promise对象,所以要对FormItem的validate方法进行改造
FormItem.vue
validate() {
// 校验当前项:依赖async-validate
return new Promise(resolve => {
const descriptor = { [this.prop]: this.form.rules[this.prop] };
const validator = new schema(descriptor);
validator.validate({[this.prop]:this.form.model[this.prop]}, errors => {
if (errors) {
this.validateStatus = "error";
this.errorMessage = errors[0].message;
resolve(false);
} else {
this.validateStatus = "";
this.errorMessage = "";
resolve(true);
}
});
});
}
Form.vue
methods: {
validate(callback) {
// 获取所有的验证结果统一处理 只要有一个失败就失败,
// 将formItem的validate方法 验证修改为promise对象,并且保存验证之后的布尔值
// tasks保存着验证之后的多个promise对象
const tasks = this.fileds.map(item=>item.validate());
let ret = true;
// 统一处理多个promise对象来验证,只要有一个错误,就返回false,
Promise.all(tasks).then(results=>{
results.forEach(valid=>{
if(!valid){
ret = false;
}
})
callback(ret);
})
}
},
测试:
<m-form :model="ruleForm" :rules="rules" ref="ruleForm2">
<m-form-item label="用户名" prop="name">
<m-input v-model="ruleForm.name"></m-input>
</m-form-item>
<m-form-item label="密码" prop="pwd">
<m-input v-model="ruleForm.pwd" type="password"></m-input>
</m-form-item>
<m-form-item>
<m-button type="danger" @click="submitForm2('ruleForm2')">提交</m-button>
</m-form-item>
</m-form>
methods:{
submitForm2(name) {
this.$refs[name].validate(valid=>{
console.log(valid);
if(valid){
alert('验证成功');
}else{
alert('验证失败')
}
});
}
}