内容大部分文字描述来自尚硅谷课件,实例操作截图和注释内容为亲测。
第二章:vue 组件化编码
2.1. 使用vue-cli 创建模板项目
2.1.1. 说明
- vue-cli 是vue 官方提供的脚手架工具
- github: https://github.com/vuejs/vue-cli
- 作用: 从 https://github.com/vuejs-templates 下载模板项目
2.1.2. 创建vue 项目
npm install -g vue-cli
vue init webpack vue_demo
cd vue_demo
npm install
npm run dev
访问: http://localhost:8080/
注释:
"vue init webpack vue_demo"中的"webpack"为固定的,即使使用webpack作为模板,提供六套模板。"vue_demo"为自起的项目名称,不可以包含大写字母。
官方文档:https://github.com/vuejs/vue-cli/tree/master
确认即可
听说得下载很久,但是我还是点下载了。
2.1.3. 模板项目的结构
|-- build : webpack 相关的配置文件夹(基本不需要修改)
|-- dev-server.js : 通过express 启动后台服务器
|-- config: webpack 相关的配置文件夹(基本不需要修改)
|-- index.js: 指定的后台服务的端口号和静态资源文件夹
|-- node_modules
|-- src : 源码文件夹
|-- components: vue 组件及其相关资源文件夹
|-- App.vue: 应用根主组件
|-- main.js: 应用入口js
|-- static: 静态资源文件夹
|-- .babelrc: babel 的配置文件
|-- .eslintignore: eslint 检查忽略的配置
|-- .eslintrc.js: eslint 检查的配置
|-- .gitignore: git 版本管制忽略的配置
|-- index.html: 主页面文件
|-- package.json: 应用包配置文件
|-- README.md: 应用描述说明的readme 文件
什么是组件化
组件是包含一个功能界面的所有元素的集合。
参考文档:https://www.kancloud.cn/zep-tsang/components/341917
初始化界面的三个文件
App.vue
<template>
<div>
<img class="logo" src="./assets/logo.png" alt="Logo">
<!-- 3.使用组件标签 -->
<HelloWorld/>
</div>
</template>
<script>
//1.引入组件
import HelloWorld from './components/HelloWorld.vue'
export default {
//映射组件标签
components : {
HelloWorld
}
}
</script>
<style>
.logo {
width: 200px;
height: 200px;
}
</style>
main.js
/*
入口JS:创建Vue实例
*/
import Vue from 'vue'
import App from './App.vue'
new Vue({
el : '#app',
components: {
App
},
template: '<App/>'
})
HelloWorld.vue
<template>
<div>
<p class="msg">{{msg}}</p>
</div>
</template>
<script>
export default { //配置对象(与Vue一致)
data () { //必须写函数
return {
msg : 'Hello Vue Component'
}
}
}
</script>
<style>
.msg {
color: red;
font-size: 30px;
}
</style>
2.2. 项目的打包与发布
2.2.1. 打包:
npm run build
2.2.2. 发布1: 使用静态服务器工具包
npm install -g serve
serve dist
访问: http://localhost:5000
2.2.3. 发布2: 使用动态web 服务器(tomcat)
修改配置: webpack.prod.conf.js
output: {
publicPath: ‘/xxx/’ //打包文件夹的名称
}
重新打包:
npm run build
修改dist 文件夹为项目名称: xxx
将xxx 拷贝到运行的tomcat 的webapps 目录下
访问: http://localhost:8080/xxx
2.3. eslint
2.3.1. 说明
- ESLint 是一个代码规范检查工具
- 它定义了很多特定的规则, 一旦你的代码违背了某一规则, eslint 会作出非常有用的提示
- 官网: http://eslint.org/
- 基本已替代以前的JSLint
2.3.2. ESLint 提供以下支持
- ES
- JSX
- style 检查
- 自定义错误和提示
2.3.3. ESLint 提供以下几种校验
- 语法错误校验
- 不重要或丢失的标点符号,如分号
- 没法运行到的代码块(使用过WebStorm 的童鞋应该了解)
- 未被使用的参数提醒
- 确保样式的统一规则,如sass 或者less
- 检查变量的命名
2.3.4. 规则的错误等级有三种
- 0:关闭规则。
- 1:打开规则,并且作为一个警告(信息打印黄色字体)
- 2:打开规则,并且作为一个错误(信息打印红色字体)
2.3.5. 相关配置文件
- .eslintrc.js : 全局规则配置文件
‘rules’: {
‘no-new’: 1
} - 在js/vue 文件中修改局部规则
/* eslint-disable no-new */
new Vue({
el: ‘body’,
components: { App }
}) - .eslintignore: 指令检查忽略的文件
*.js
*.vue
注释:
规则不符合会报错,但是不影响代码执行。
使用格式化文档会减少规则报错
配置:
修改.eslintrc.js文件
把认为不需要报错的设置为off或者0,可以0,1,2,根据自身要求。
2.4. 组件定义与使用
2.4.1. vue 文件的组成(3 个部分)
- 模板页面
<template>
页面模板
</template>
- JS 模块对象
<script>
export default {
data() {return {}},
methods: {},
computed: {},
components: {}
}
</script>
- 样式
<style>
样式定义
</style>
2.4.2. 基本使用
- 引入组件
- 映射成标签
- 使用组件标签
<template>
<HelloWorld></HelloWorld>
<hello-world></hello-world>
</template>
<script>
import HelloWorld from './components/HelloWorld'
export default {
components: {
HelloWorld
}
}
</script>
2.4.3. 关于标签名与标签属性名书写问题
- 写法一: 一模一样
- 写法二: 大写变小写, 并用-连接
2.5. 组件间通信
2.5.1. 组件间通信基本原则
- 不要在子组件中直接修改父组件的状态数据
- 数据在哪, 更新数据的行为(函数)就应该定义在哪
2.5.2. vue 组件间通信方式
- props
- vue 的自定义事件
- 消息订阅与发布(如: pubsub 库)
- slot
- vuex(后面单独讲)
2.6. 组件间通信1: props
2.6.1. 使用组件标签时
2.6.2. 定义MyComponent 时
- 在组件内声明所有的props
- 方式一: 只指定名称
props: [‘name’, ‘age’, ‘setName’] - 方式二: 指定名称和类型
props: {
name: String,
age: Number,
setNmae: Function
} - 方式三: 指定名称/类型/必要性/默认值
props: {
name: {type: String, required: true, default:xxx},
}
2.6.3. 注意
- 此方式用于父组件向子组件传递数据
- 所有标签属性都会成为组件对象的属性, 模板页面可以直接引用
- 问题:
a. 如果需要向非子后代传递数据必须多层逐层传递
b. 兄弟组件间也不能直接props 通信, 必须借助父组件才可以
2.7. 组件间通信2: vue 自定义事件
2.7.1. 绑定事件监听
// 方式一: 通过v-on 绑定
@delete_todo="deleteTodo"
// 方式二: 通过$on()
this.$refs.xxx.$on('delete_todo', function (todo) {
this.deleteTodo(todo)
})
2.7.2. 触发事件
// 触发事件(只能在父组件中接收)
this.$emit(eventName, data)
2.7.3. 注意:
- 此方式只用于子组件向父组件发送消息(数据)
- 问题: 隔代组件或兄弟组件间通信此种方式不合适
2.8. 组件间通信3: 消息订阅与发布(PubSubJS 库)
2.8.1. 订阅消息
PubSub.subscribe(‘msg’, function(msg, data){})
2.8.2. 发布消息
PubSub.publish(‘msg’, data)
2.8.3. 注意
- 优点: 此方式可实现任意关系组件间通信(数据)
2.8.4. 事件的2 个重要操作(总结)
- 绑定事件监听(订阅消息)
目标: 标签元素
事件名(类型): click/focus
回调函数: function(event){} - 触发事件(发布消息)
DOM 事件: 用户在浏览器上对应的界面上做对应的操作
自定义: 编码手动触发
2.9. 组件间通信4: slot
2.9.1. 理解
此方式用于父组件向子组件传递标签数据
2.9.2. 子组件: Child.vue
<template>
<div>
<slot name="xxx">不确定的标签结构1</slot>
<div>组件确定的标签结构</div>
<slot name="yyy">不确定的标签结构2</slot>
</div>
</template>
2.9.3. 父组件: Parent.vue
<child>
<div slot="xxx">xxx 对应的标签结构</div>
<div slot="yyy">yyyy 对应的标签结构</div>
</child>
2.10. demo1: comment manage
在static下创建comment_page文件夹
comment_page文件夹下三个文件构成静态页面
bootstrap.css,利用到bootstrap技术
index.css
.reply {
margin-top: 0px;
}
li {
transition: .5s;
overflow: hidden;
}
.handle {
width: 40px;
border: 1px solid #ccc;
background: #fff;
position: absolute;
right: 10px;
top: 1px;
text-align: center;
}
.handle a {
display: block;
text-decoration: none;
}
.list-group-item .centence {
padding: 0px 50px;
}
.user {
font-size: 22px;
}
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="./bootstrap.css">
<link rel="stylesheet" href="index.css">
</head>
<body>
<div id="app">
<div>
<header class="site-header jumbotron">
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>请发表对React的评论</h1>
</div>
</div>
</div>
</header>
<div class="container">
<div class="col-md-4">
<form class="form-horizontal">
<div class="form-group">
<label>用户名</label>
<input type="text" class="form-control" placeholder="用户名">
</div>
<div class="form-group">
<label>评论内容</label>
<textarea class="form-control" rows="6" placeholder="评论内容"></textarea>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="button" class="btn btn-default pull-right">提交</button>
</div>
</div>
</form>
</div>
<div class="col-md-8">
<h3 class="reply">评论回复:</h3>
<h2 style='display: none'>暂无评论,点击左侧添加评论!!!</h2>
<ul class="list-group">
<li class="list-group-item">
<div class="handle">
<a href="javascript:;">删除</a>
</div>
<p class="user"><span>xxx</span><span>说:</span></p>
<p class="centence">React不错!</p>
</li>
<li class="list-group-item">
<div class="handle">
<a href="javascript:;">删除</a>
</div>
<p class="user"><span>yyy</span><span>说:</span></p>
<p class="centence">React有点难!</p>
</li>
</ul>
</div>
</div>
</div>
</div>
</body>
</html>
三步:
1.拆,把页面拆成不同部分。
2.静,哪些是静态页面,直接渲染生成。
3.动,动态页面,初始化和动态加载。
App.vue
<template>
<div>
<header class="site-header jumbotron">
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>请发表对Vue的评论</h1>
</div>
</div>
</div>
</header>
<div class="container">
<Add :addComment="addComment"/>
<List :comments="comments" :deleteComment="deleteComment"/>
</div>
</div>
</template>
<script>
import Add from './components/Add.vue'
import List from './components/List.vue'
export default {
data () {
return {
comments: [ //数据在哪个组件,更新数据的行为(方法)就应该定义在哪个组件
{
name : 'BOB',
content: 'Vue 还不错'
},
{
name: 'Cat',
content: 'Vue So Easy'
},
{
name: 'BZ',
content: 'Vue So So'
}
]
}
},
methods: {
//添加评论
addComment(comment) {
this.comments.unshift(comment)
},
//删除指定下标的评论
deleteComment (index) {
this.comments.splice(index, 1)
}
},
components: {
Add,
List
}
};
</script>
<style>
</style>
main.js
import Vue from 'vue'
import App from './App.vue'
new Vue({
el: '#app',
components: {App},
template: '<App/>'
})
components下三个vue文件,本应该是三个文件夹,因为此例没有图片,简化为三个vue文件。
Add.vue
<template>
<div class="col-md-4">
<form class="form-horizontal">
<div class="form-group">
<label>用户名</label>
<input type="text" class="form-control" placeholder="用户名" v-model="name" />
</div>
<div class="form-group">
<label>评论内容</label>
<textarea
class="form-control"
rows="6"
placeholder="评论内容"
v-model="content"
></textarea>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="button" class="btn btn-default pull-right" @click="add">
提交
</button>
</div>
</div>
</form>
</div>
</template>
<script>
export default {
props: {
addComment: {
type: Function,
required: true
}
},
data () {
return {
name: '',
content: ''
}
},
methods: {
add () {
//1.检查输入的合法性
const name = this.name.trim()
const content = this.content.trim()
if(!name || !content) {
alert('姓名或内容不能空')
return
}
//2.根据输入的数据,封装成一个comment对象
const comment = {
name,
content
}
//3.添加到comment中
this.addComment(comment)
//4.清除输入
this.name = ''
this.content = ''
}
}
};
</script>
<style>
</style>
List.vue
<template>
<div class="col-md-8">
<h3 class="reply">评论回复:</h3>
<h2 v-show="comments.length == 0" >暂无评论,点击左侧添加评论!!!</h2>
<ul class="list-group">
<Item v-for="(comment, index) in comments" :index="index" :key="index" :comment="comment" :deleteComment="deleteComment"/>
</ul>
</div>
</template>
<script>
import Item from './Item.vue'
export default {
//声明接收属性: 这个属性就会成为组件对象的属性
props: ['comments','deleteComment'], //只指定属性名
components : {
Item
}
};
</script>
<style>
.reply {
margin-top: 0px;
}
</style>
Item.vue
<template>
<li class="list-group-item">
<div class="handle">
<a href="javascript:;" @click="deleteItem" >删除</a>
</div>
<p class="user"><span>{{comment.name}}</span><span>说:</span></p>
<p class="centence">{{comment.content}}</p>
</li>
</template>
<script>
export default {
props: { //指定属性名和属性值的类型
comment: Object,
deleteComment: Function,
index: Number
},
methods: {
deleteItem () {
const {comment, index, deleteComment} = this
if(window.confirm(`确定删除${comment.name}的评论吗?`)){
this.deleteComment(index)
}
}
}
};
</script>
<style>
li {
transition: 0.5s;
overflow: hidden;
}
.handle {
width: 40px;
border: 1px solid #ccc;
background: #fff;
position: absolute;
right: 10px;
top: 1px;
text-align: center;
}
.handle a {
display: block;
text-decoration: none;
}
.list-group-item .centence {
padding: 0px 50px;
}
.user {
font-size: 22px;
}
</style>
2.11. demo2: todo list
main.js
import Vue from 'vue'
import App from './App.vue'
import './base.css'
new Vue({
el: '#app',
components: {App},
template: '<App/>'
})
base.css
/*base*/
body {
background: #fff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
App.vue
<template>
<div class="todo-container">
<div class="todo-wrap">
<!-- 给TodoHeader标签对象绑定addTodo事件监听 -->
<!-- <TodoHeader @addTodo="addTodo" /> -->
<TodoHeader ref="header" />
<TodoList :todos="todos"/>
<!-- <TodoFooter
:todos="todos"
:deleteCompleteTodos="deleteCompleteTodos"
:selectAllTodos="selectAllTodos"
/> -->
<todo-footer>
<input type="checkbox" v-model="isAllCheck" slot="checkAll" />
<span slot="count">已完成{{ completeSize }} / 全部{{ todos.length }}</span>
<button
class="btn btn-danger"
v-show="completeSize"
@click="deleteAllCompleted"
slot="delete"
>
清除已完成任务
</button>
</todo-footer>
</div>
</div>
<!-- 绑定事件监听
触发事件
订阅消息
发布消息 -->
</template>
<script>
import PubSub from 'pubsub-js'
import TodoHeader from "./components/TodoHeader.vue";
import TodoList from "./components/TodoList.vue";
import TodoFooter from "./components/TodoFooter.vue";
import storageUtil from './util/storageUtil.js'
export default {
mounted() {
//执行异步代码
//给<TodoHeader />绑定addTodo事件监听
this.$refs.header.$on("addTodo", this.addTodo);
//订阅消息
PubSub.subscribe('deleteTodo', (msg, index) => {
this.deleteTodo(index)
})
},
data() {
return {
//从localStorage读取todos,深度监视
// todos: JSON.parse(window.localStorage.getItem("todos_key") || "[]")
todos: storageUtil.readTodos()
};
},
computed: {
completeSize() {
return this.todos.reduce(
(preTotal, todo) => preTotal + (todo.complete ? 1 : 0),
0
);
},
isAllCheck: {
get() {
return this.completeSize === this.todos.length && this.completeSize > 0;
},
set(value) {
//value 是当前checkbox最新的值
this.selectAllTodos(value);
}
}
},
watch: {
//监视
todos: {
deep: true, //深度监视
// handler: function (newValue, oldValue) { }
// handler: function(value) {
// //将todos最新的值的json数据,保存到localStorage
// // window.localStorage.setItem("todos_key", JSON.stringify(value));
// storageUtil.saveTodos(value)
// }
handler: storageUtil.saveTodos
}
},
components: {
TodoHeader,
TodoList,
TodoFooter
},
methods: {
addTodo(todo) {
this.todos.unshift(todo);
},
deleteTodo(index) {
this.todos.splice(index, 1);
},
//删除所有选中的todo
deleteCompleteTodos() {
this.todos = this.todos.filter(todo => !todo.complete);
},
//全选/全不选
selectAllTodos(check) {
this.todos.forEach(todo => {
todo.complete = check;
});
},
deleteAllCompleted() {
if (window.confirm("确定清除已完成的吗?")) {
this.deleteCompleteTodos();
}
}
}
};
</script>
<style>
/*app*/
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
storageUtil.js
// 使用localStorage存储数据的工具模块
// 1.函数 :1个功能
// 2.对象 :2个功能
// 需要向外暴露1个功能还是多个功能
const TODOS_KEY = "todos_key"
export default {
saveTodos (todos) {
window.localStorage.setItem(TODOS_KEY, JSON.stringify(todos));
},
readTodos () {
return JSON.parse(window.localStorage.getItem(TODOS_KEY) || "[]")
}
}
TodoHeader.vue
<template>
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="addItem" />
</div>
</template>
<script>
export default {
props: {
},
data () {
return {
title: ''
}
},
methods: {
addItem () {
//1.检查输入的合法性
const title = this.title.trim()
if(!title) {
alert('必须输入')
return
}
//2.根据输入生成一个todo对象
const todo = {
title,
complete: false
}
//3.添加到todos
// this.addTodo(todo)
//触发自定义事件: addTodo
this.$emit('addTodo', todo)
//4.清除输入
this.title = ''
}
}
};
</script>
<style>
/*header*/
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 7px;
}
.todo-header input:focus {
outline: none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}
</style>
TodoList.vue
<template>
<ul class="todo-main">
<TodoItem v-for="(todo, index) in todos" :key="index" :todo="todo" :index="index"/>
</ul>
</template>
<script>
import TodoItem from './TodoItem.vue'
export default {
props: {
todos: Array,
},
components: {
TodoItem
}
};
</script>
<style>
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>
TodoItem.vue
<template>
<li @mouseenter="handleEnter(true)" @mouseleave="handleEnter(false)" :style="{background: bgColor}">
<label>
<input type="checkbox" v-model="todo.complete" />
<span>{{todo.title}}</span>
</label>
<button class="btn btn-danger" v-show="isShow" @click="deleteItem">删除</button>
</li>
</template>
<script>
import PubSub from 'pubsub-js'
export default {
props: {
todo: Object,
index: Number
},
data () {
return {
bgColor: 'white', //默认的背景颜色
isShow: false //按钮默认是否显示
}
},
methods: {
handleEnter(isEnter) {
if(isEnter) {
this.bgColor = '#aaaaaa'
this.isShow = true
} else {
this.bgColor = 'white'
this.isShow = false
}
},
deleteItem () {
const {todo, index, deleteTodo} = this
if(window.confirm(`确认删除${todo.title}吗?`)) {
// deleteTodo(index)
//发布消息
PubSub.publish('deleteTodo', index)
}
}
},
};
</script>
<style>
/*item*/
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
display: none;
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
</style>
TodoFooter.vue
<template>
<div class="todo-footer">
<label>
<!-- <input type="checkbox" v-model="isAllCheck" /> -->
<slot name="checkAll"></slot>
</label>
<span>
<!-- <span>已完成{{ completeSize }} / 全部{{ todos.length }}</span> -->
<slot name="count"></slot>
</span>
<!-- <button
class="btn btn-danger"
v-show="completeSize"
@click="deleteAllCompleted"
>
清除已完成任务
</button> -->
<slot name="delete"></slot>
</div>
</template>
<script>
export default {
// props: {
// todos: Array,
// deleteCompleteTodos: Function,
// selectAllTodos: Function
// },
// computed: {
// completeSize() {
// return this.todos.reduce(
// (preTotal, todo) => preTotal + (todo.complete ? 1 : 0),
// 0
// );
// },
// isAllCheck: {
// get() {
// return this.completeSize === this.todos.length && this.completeSize > 0;
// },
// set(value) {
// //value 是当前checkbox最新的值
// this.selectAllTodos(value);
// }
// }
// },
// methods: {
// deleteAllCompleted() {
// if (window.confirm("确定清除已完成的吗?")) {
// this.deleteCompleteTodos();
// }
// }
// }
};
</script>
<style>
/*footer*/
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}
.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}
.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}
.todo-footer button {
float: right;
margin-top: 5px;
}
</style>