正课:
- 组件
- 组件化开发
- SPA
- 脚手架
- 脚手架文件夹结构
一. 组件:
- 什么是: 拥有专属的HTML+CSS+JS+数据的独立的可重用的页面功能区域
- 为什么: 重用
- 何时: 今后只要一个功能可能会被反复使用时,都用组件
- 如何:
(1). 创建组件:
组件
Vue.component("组件名",{
//模板/界面
template:`HTML片段`, //必须用唯一父元素包裹
data(){
return { //模型对象
数据/变量: 值,
... : ...
}
},
/******以下内容就和new Vue()完全一样了*******/
methods:{ 函数 },
watch:{ 监视函数 },
computed:{ 计算属性 },
其它生命周期函数...
})
(2). 在页面上反复使用组件:
VUE中一个组件,其实就是一个可重用的自定义标签而已!
<组件名></组件名>
-
组件的加载过程:
(1). Vue.component()创建一个组件对象,保存在内存中vue大家庭里备用
(2). new Vue()在扫描页面过程中,发现不是标准的HTML元素的标签,就会自动去内存中vue大家庭中找是否有同名的组件对象
(3). 一旦找到同名的组件对象,做3件事:
a. 用组件对象的template代替页面中<组件名>标签的位置b. 自动调用组件对象的data()函数,在data函数内部先创建一个新的模型对象,包含新的模型变量。然后将新创建的模型对象返回给当前组件所在的位置
c. 将methods中的事件处理函数,分发给每个组件的副本
(4). 结果: 在页面中就会形成多个麻雀虽小五脏俱全的小组件副本。每个组件副本内部的原型原理和过程与new Vue()的原理是完全一样的!
-
示例: 定义计数器组件,并反复使用
1_component.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="js/vue.js"></script>
</head>
<body>
<div id="app">
<ul>
<li>
<my-counter></my-counter>
</li>
<li>
<my-counter></my-counter>
</li>
<li>
<my-counter></my-counter>
</li>
</ul>
</div>
<script>
//因为界面中修改数量部分经常被重用,所以应该下封装为一个组件,再在界面中反复使用组件
//创建一个组件:
Vue.component("my-counter",{
//组件其实就是一个缩微的new Vue()
//麻雀虽小五脏俱全!
//1 做界面:和new Vue()做界面过程一样
//模板: 一次定义,反复生成一模一样的副本!
//1.1 也找可能发生变化的位置
//本例中: span的内容可能发生变化
//1.2 也找触发事件的元素
//本例中: 两个button可能触发事件
template:`<div>
<button @click="minus">-</button>
<span>{{n}}</span>
<button @click="add">+</button>
</div>`,
//2. 创建模型: 大致和new Vue()相同
data(){//变为一个函数
return {//相当于之前的data:{}
//因为界面中需要一个变量
n:0
}
},
methods:{ //和以前没有差别
//因为界面中需要2个函数
add(){
this.n++;
},
minus(){
if(this.n>0){
this.n--
}
}
}
})
//必须加new Vue()
new Vue({//扫描页面,识别vue的各种功能!
el:"#app"
})
</script>
</body>
</html>
二. 组件化开发:
- 什么是: 拿到一个页面后,先划分组件。然后再把每个组件分给不同的人去开发!最后运行时,再将所有组件合并为一个页面运行!
- 为什么: 2个原因:
(1). 极其便于多人协作开发!极大提高开发的效率
(2). 松耦合: 即使一个组件出错,不影响整个页面中其它组件的功能! - 何时: 今后所有的前端框架项目都是用组件化开发实现的。
- 如何:
(1). 拿到功能/页面时,先划分组件:
a. 位置
b. 是否重用
(2). 为每个组件,在独立的js文件中创建组件对象
(3). 在父组件中,希望加载子组件的位置,用<子组件名>占位
(4). 在唯一完整的html页面中,先引入vue.js,再引入所有组件的独立js文件
(5). 创建new Vue()对象,在<div id="app">
内,用<父组件>占位 - 问题: Vue.component()创建的组件,称为全局组件。可放在任何位置!没有限制!但是,有些子组件,离开父组件单独使用,是没有意义的!所以不应该随意在父组件外部使用子组件!
- 原理: 其实vue中共有三大类组件:
(1). new Vue() 根组件: 整个项目只有一个根组件,监控整个界面范围。
(2). Vue.component()全局组件: 可在界面中任何位置使用,没有限制
(3). 子组件: 被限制只能在一个指定的父组件内使用的组件。一旦超出指定的父组件范围就报错! - 解决: 今后只要限制在指定父组件内才能使用的组件都要创建为子组件,3步:
(1). 不要用Vue.component()创建,而应该将子组件创建为一个普通的js对象。但是对象的内容,要符合vue组件的格式要求:
//子组件对象名,应该是将来组件标签名的驼峰命名形式
//比如:<todo-add>
,则子组件对象名应该为: todoAdd
//<todo-list>
,则子组件对象名应该为: todoList
var 对象名={
template:`HTML片段`,
data(){ return { ... } },
methods:{ ... },
... ...
}
(2). 在父组件对象中添加一个新的成员:
父组件对象:{
template:xxx,
data(){ return { ... }},
... ...,
components:{ 子组件对象名, ... }
} todoAdd 或 todoList
(3). 在父组件的template中,使用组件标签引入子组件:
子组件的标签名,必须恢复成-分割
vue会自动将驼峰命名翻译为-分割!
比如: <todo-add> <todo-list>
-
坑: 一旦使用子组件,则引入时,子组件必须在父组件之前先引入!
-
示例: 使用组件化开发,实现todo案例的界面部分:
todo1/js/todoAdd.js
//创建todo-add组件
var todoAdd={
template:`<div>
<input><button>+</button>
</div>`
}
todo1/js/todoItem.js
//创建todo-item组件
var todoItem={
template:`<div>
1. 吃饭 <a href="javascript:;">×</a>
</div>`
}
todo1/js/todoList.js
//创建todo-list组件
var todoList={
template:`<ul>
<li><todo-item></todo-item></li>
<li><todo-item></todo-item></li>
<li><todo-item></todo-item></li>
</ul>`,
//todoItem是todoList的子组件
components:{ todoItem }
}
todo1/js/todo.js
//创建todo组件
Vue.component("todo",{
template:`<div>
<h3>待办事项列表</h3>
<todo-add></todo-add>
<todo-list></todo-list>
</div>`,
//todoAdd和todoList是todo的子组件
components:{ todoAdd, todoList }
})
todo1/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="js/vue.js"></script>
<script src="js/todoAdd.js">
//是todo的子
</script>
<script src="js/todoItem.js">
//是todoList的子
</script>
<script src="js/todoList.js">
//是todo的子
</script>
<script src="js/todo.js"></script>
</head>
<body>
<div id="app">
<todo></todo>
</div>
<script>
new Vue({
el:"#app"
});
</script>
</body>
</html>
-
组件间传参:
(1). 问题: Vue中父组件的成员,子组件也无权使用!
(2). 原理: Vue中每个组件的数据是专属的!
(3). 解决: 其实组件之间可以传递数据的!
(4). 父给子: 2步:
a. 父组件在子组件身上放一个数据:
父组件的template中:
<子组件 :自定义属性名=“父组件变量”></子组件>
b. 子组件从父组件放变量的自定义属性中取出父组件传来的值
子组件对象中:
var 子组件={
template:xxx,
data(){ return { … } }, //自己定义的
//属性,从自定义属性中获取变量值
props: [ “自定义属性名” , … ] //父组件给的
//结果: props中的自定义属性名,等效于当前子组件中的data中的变量。用法和data中的变量完全一样!
this.自定义属性名
或
{{自定义属性名}}
} -
示例: 实现todo案例中父组件给子组件传值
todo2/js/todoAdd.js
//创建todo-add组件
var todoAdd={
template:`<div>
<input><button>+</button>
</div>`
}
todo2/js/todoItem.js
//创建todo-item组件
var todoItem={
template:`<div>
{{i+1}}. {{t}} <a href="javascript:;">×</a>
</div>`,
props:["t","i"]
}
todo2/js/todoList.js
//创建todo-list组件
var todoList={
template:`<ul>
<li v-for="(t,i) of tasks" :key="i">
<todo-item :t="t" :i="i"></todo-item>
</li>
</ul>`,
props:["tasks"],
//todoItem是todoList的子组件
components:{ todoItem }
}
todo2/js/todo.js
//创建todo组件
Vue.component("todo",{
template:`<div>
<h3>待办事项列表</h3>
<todo-add></todo-add>
<todo-list :tasks="tasks"></todo-list>
</div>`,
data(){
return {
tasks:[ "吃饭", "睡觉", "打亮亮"]
}
},
//todoAdd和todoList是todo的子组件
components:{ todoAdd, todoList }
})
todo2/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="js/vue.js"></script>
<script src="js/todoAdd.js">
//是todo的子
</script>
<script src="js/todoItem.js">
//是todoList的子
</script>
<script src="js/todoList.js">
//是todo的子
</script>
<script src="js/todo.js"></script>
</head>
<body>
<div id="app">
<todo></todo>
</div>
<script>
new Vue({
el:"#app"
});
</script>
</body>
</html>
三. SPA: Single Page Application
单 页面 应用
-
什么是单页面应用: 整个应用程序只有一个唯一完整的HTML页面。其它所谓的"页面",其实都是组件而已。所谓的"页面跳转",其实只是在一个HTML中切换不同的组件显示而已。
-
为什么:
多页面 单页面
请求次数 每切换一次页面,都要向服务器发送请求,请求次数多 首次请求时,就把唯一完整的HTML页面和其他所有组件都下载到客户端本地。今后,无论怎样切换页面,都是在客户端切换不同的组件显示而已,不会像服务器端发送任何请求!请求次数少
公共资源的使用 每切换一次页面,都要重新请求公共的资源(bootstrap.css, jquery.js等)。请求次数多 单页面应用只在首次请求页面时,下载一次公共资源。之后切换页面时,除了组件部分之外,其余部分是保持把不变的!所以不会反复请求公共的资源!请求次数少
页面加载效率 每次切换页面都要废弃旧的DOM树,重建整棵DOM树,所以效率低。 只在加载首页时,建立完整DOM树。之后,切换页面时,因为唯一完整的HTML页面是不变的,只更新局部的组件内容。所以,DOM树也不会重建,而是更新部分节点。效率高!
页面切换动画 因为每次切换页面都是先清空前一个页面,再重新请求请求后一个页面。前后两个页面不可能并存在客户端。所以,几乎不可能实现页面切换动画。 因为所有的组件都在客户端了,所以完全有可能同时显示前后两个组件的部分内容,形成动画效果。 -
何时: 今后,只要使用框架开发项目,用的都是单页面应用!
-
缺点: 首屏加载极慢。
(1). 原因: 首屏就要把所有页面组件内容都下载下来!下载内容比较多!
(2). 解决: 懒加载!(明天讲) -
如何: 3个主要的步骤:
(1). 创建一个唯一完整的HTML文件
a. 标准的支持vue的页面结构
b. 必须引入vue-router.js核心组件
c. 在唯一完整的页面内部添加<router-view>
标签,为将来的页面组件占位。
(2). 创建多个页面组件
a. 为每个页面组件创建独立的js文件
b. 每个页面组件都要创建为子组件的格式(不要用Vue.component())
(3). 在独立的router.js文件中创建路由器对象和路由字典列表:
a. 什么是路由字典: 专门保存地址栏中相对路径与组件对象之间对应关系的一个数组。
var routes=[
{ path:"/", component:Index },
{ path:"/details", component: Details}
... ...
]
b. 什么是路由器对象: 专门负责监控地址栏变化,并根据地址栏变化查找对应组件,替换页面中router-view的 核心对象
var router=new VueRouter({ routes })
c. 后续操作:
(1). 将所页面组件文件和router.js文件都引入到唯一完整的HTML页面中。
强调: 所有页面组件必须先于router.js引入
(2). 将router对象加入到new Vue()中!
new Vue({
el:"#app",
router
})
-
结果: 只要地址栏相对路径一变,router对象会自动获得新的相对地址。自动去routes中查找对应的组件对象。最后用找到的组件对象代替HTML文件中
<router-view>
的位置 -
强调: Vue的路由器采用锚点地址,作为客户端导航的相对地址:
http://127.0.0.1:5500/index.html#/相对路径 -
示例: 实现包含两个页面的基本的SPA应用
SPA/js/index.js
var Index={
template:`<div>
<h3 style="color:red">这里是首页</h3>
</div>`
}
SPA/details.js
var Details={
template:`<div>
<h3 style="color:green">这里是详情页</h3>
</div>`
}
SPA/js/router.js
//路由字典: 包含多对儿相对路径与组件对象间对应关系
var routes=[
{path:"/", component:Index},
{path:"/details", component:Details}
//强调: component后的组件对象名必须和组件.js文件中的组件对象名一致!
];
//路由器对象
var router=new VueRouter({
routes
})
SPA/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="js/vue.js"></script>
<script src="js/vue-router.js"></script>
<script src="js/index.js">
//var Index={}
</script>
<script src="js/details.js">
//var Details={}
</script>
<script src="js/router.js">
//routes=[
//{ ..., component:Index},
//{ ..., component:Details}
//]
</script>
</head>
<body>
<div id="app">
<router-view></router-view>
</div>
<script>
new Vue({ //扫描
el:"#app",
router
})
</script>
</body>
</html>
- 404页面:
(1). 除了所有正确的路径之外,其余所有任意不正确的路径,都要跳转到404。
(2). 如何:
a. 在独立的js文件中创建一个NotFound组件
b. 在router.js中添加路由字典项: (列表的最后)
{ path:"*", component:NotFound }
c. 在唯一完整的html页面中提前引入notFound.js文件。
- 页头:
(1). 因为页头可以用在任何一个页面中,不受限制,所以应该是一个全局组件。
(2). 如何: 4步:
a. 在独立的js文件中,先创建一个普通的子组件,包含页头的部分内容
b. 在唯一完整的HTML页面中引入页头.js文件,同时就引入了页头组件对象——暂时还不是全局组件!
强调: 页头组件不用加入router.js中。因为页头组件绝对不会单独展示!总是和别的页面配合一起展示。所以页头没有专门的路径与之对应!
c. 在new Vue()之前,外部,使用Vue.component()将页头子组件,转化为全局组件。
d. 使用页头组件: 2种:
1). 所有页面都需要页头,无一例外:
应该将页头组件写在<router-view>
外部的上方
2). 有的页面需要页头,有的页面不需要页头
i. 不能放在<router-view>
外部的上方
ii. 哪个页面需要页头,就在哪个页面组件的内部添加页头组件。那些不需要页头的组件,就不写。 - 路由跳转: 2种:
(1). html中: 不要用<a>
<router-link to="/相对地址">文本</router-link>
(2). js中: `this.$router.push("/相对路径")`
- 路由传参: 3步:
(1). 修改router.js中路由字典中的路由地址,允许传参
{path:"/details/:变量名", component:Details, props:true},
(2). 跳转时,路径: /details/变量值
(3). 下个页面中如何获得地址栏中的参数值:
a. router.js中的props:true,意为让地址栏中的参数值自动变成当前页面组件的props中的一个属性值
b. 下一个页面中就可以用: props:[ "变量名" ]
(4). 坑: 一旦一个路径配置/:变量名,则必须携带参数才能进入该路径。如果不带参数,将被禁止进入!
- 问题: 那么多js文件都放在同一个js文件夹下,非常乱
解决: 建立专门的文件夹,分别保存不同的组件:
views/文件夹,专门保存所有页面组件
components/文件夹,专门保存所有全局组件
router/文件夹,专门保存router.js文件。
route, routes, router
- router: 用new VueRouter()创建出的路由器对象
a. 监视地址栏变化
b. 还可以执行跳转动作! - routes: 一个路由字典数组,包含当前网站中所有路径与组件的对应关系列表。routes会被装入new
VueRouter()
中,和router对象一起发挥作用! - route: 一个路由地址,代表当前地址栏中的url信息,像BOM中的location。
地址栏:http://127.0.0.1:5500/index.html#/details
总结:
3. 只要希望在页面加载时自动对元素执行一些初始化操作时就用自定义指令:
(1). 添加自定义指令:
Vue.directive("自定义指令名",{
inserted(domElem){
对domElem执行DOM操作
}
})
(2). 使用自定义指令:
<元素 v-自定义指令名>
4. 今后只要根据其他变量的值动态计算出一个属性值就用计算属性:
<元素>{{计算属性}}</元素>
new Vue({
el:"#app",
data:{...},
methods:{...},
computed:{
计算属性名(){
计算过程
return 计算结果
}
}
})
- 希望将变量的原始值先加工后再显示给用户看时就用过滤器:
Vue.filter("过滤器名",function(oldVal, 自定义形参,...){
return 加工后的新值
})
<元素>{{ 变量 | 过滤器(实参值, …) | … }}</元素>
6. 只要在vue中发送ajax请求,就用axios
axios.defaults.baseURL="服务器端接口的公共基础地址部分"
axios.get(
"服务器端接口地址的相对路径",
{
params:{ 参数名: 参数值, ... }
}
).then(result=>{
... result.data...
})
或
axios.post(
"服务器端接口地址的相对路径",
"参数名1=参数值1&参数名2=参数值2&..."
).then(result=>{
... result.data...
})
强调: 在vue内使用axios,then中必须用箭头函数,保持then内this与外部this一致,都指向当前new Vue()对象
7. vue生命周期4个阶段 8个钩子函数
beforeCreate(){ ... }
(1). 创建(create)
created(){ ... }
beforeMount(){ ... }
(2). 挂载(mount)
mounted(){ ... 经常在这里发送ajax请求 ... }
beforeUpdate(){ ... }
(3). 更新(update)
updated(){ ... }
beforeDestroy(){ ... }
(4). 销毁(destroy)
destroyed(){ ... }
- 只要希望重用一块独立的功能区域就用组件:
(1). 定义组件
Vue.component(`组件标签名`,{
template:`HTML内容片段`,
data(){ return { 变量 } },
//其余和new Vue()完全相同
})
(2). 在HTML中使用自定义组件
<组件标签名/>
或双标记也行
(3). 原理: new Vue()扫描到自定义组件标签,
a.组件的template中的HTML内容代替页面中<组件标签>位置。
b. 并为这个小区域专门创建一个缩微版的vue类型对象。
1). 调用组件的data()函数为当前组件副本创建一个专属数据对象副本。
2). 引入组件对象中的methods等其他内容到当前组件对象副本中
9. 组件化开发:
(1). 步骤:
a. 拿到页面先划分功能区域
1). 从上到下,按功能不同划分区域
2). 按是否重用划分
b. 为每个组件创建独立的.js文件,其中包含一个组件对象及其内容
c. 将所有组件引入唯一完整的html页面中,并在<div id=”app”></div>
中添加父组件标签。
(2). 运行时:
a. new Vue()扫描<div id=”app”>
,发现父组件标签,创建并替换父组件
b. 父组件扫描自己内部的template内容,创建并替换子组件
(3). 三种组件:
a. 根组件: new Vue()
b. 全局组件: Vue.component(...)
c. 子组件: 3步
1). var 子组件对象名={
内容必须符合组件的要求
}
子组件对象名必须是驼峰命名
2). 父组件对象中:{
… …
components: { 子组件对象名, … ,… }
}
子组件对象名必须是驼峰命名
3). 父组件template中用<子组件标签名/>引入子组件内容
components会将子组件对象名的驼峰命名自动翻译为-分隔
所以, 使用子组件标签时,要用-分隔多个单词
(4). 组件间传参: 父给子
a. 父组件给:
<子组件 :自定义属性名=“父组件变量”>
b. 子组件取:
props:[“自定义属性名”]
结果: 在子组件内,props中的"自定义属性名"与子组件自己data中的变量用法完全相同!
10. SPA
(1). 3步:
a. 先创建唯一完整的HTML页面
1). 包含vue基本的页面结构
<div id="app"> new Vue({el:"#app"})
2). 引入所有必要的文件和组件
vue-router.js, 其它页面或组件文件, router.js路由器对象所在文件
3). <div id="app">中用<router-view/>
为今后页面组件预留空位
b. 再为每个页面组件创建独立的文件。每个页面组件其实都是一个子组件
c. 创建router.js文件,创建路由器对象
1). 创建路由字典对象:
var routes=[
{path:"/", component:首页组件对象名},
{path:"/相对路径" , component: 其它页面组件对象名},
{path:"*", component: 404页面组件对象 }
]
2). 创建路由器对象,并将路由字典对象转入路由器对象中
var router=new VueRouter({ routes })
3). 将router对象加入到new Vue()中
回到唯一完整的HTML页面中: new Vue({ el:"#app", router })
(2). 页头等全局组件:
a. 创建独立的文件保存页头组件的内容
b. 使用Vue.component("my-header",{ ... })
将页头创建为全局组件
c. 在唯一完整的HTML页面中引入页头组件文件
d. 使用页头组件标签<my-header/>
: 2种:
1). 如果所有页面都有统一的页头:
就放在唯一完整的html页面中<router-view>
外部上方
2). 如果有的页面有页头,有的页面没有页头:
就只放在需要页头的组件中的template中
(3). 路由跳转: 2种:
a. html中: `<router-link to="/相对路径">文本<router-link>`
b. js中: this.$router.push("/相对路径")
(4). 路由传参:
a. 修改路由字典:
{path:"/相对路径/:自定义参数名", component:页面组件对象, props:true}
b. 跳转时:
<router-link to="/相对路径/参数值"
或
this.$router.push("/相对路径/参数值")
c. 下一个页面接:
1). props:[ ‘自定义参数名’ ]
2). 可将"自定义参数名"用作绑定或程序中都行
11. 脚手架文件夹结构:
(1). 唯一完整的HTML页面: 一分为三:
a. public文件夹
1). 图片img文件夹放在public文件夹下
2). 第三方css的压缩版和第三方js的压缩版都放在public文件夹下
3). 唯一完整的HTML文件index.html中,head中引入第三方的css和js
b. src/App.vue
1). <template>下只包含公共的页头组件和<router-view>
2). <style>
下包含所有网页都要用到的公共css样式,比如css重置代码
c. src/main.js
1). import引入App.vue,router,axios,以及其他全局组件
2). 将全局组件对象转为真正的全局组件: Vue.component( "组件标签名", 全局组件对象 )
3). 配置axios并放入原型对象中:
axios.defaults.baseURL=“服务器端基础路径”
Vue.prototype.axios=axios;
(2). 为每个页面创建.vue组件文件,都放在src/views文件夹下。每个.vue文件中:
a. <template>
标签内,包含这个页面的HTML内容
b. <script>export default{ ... }</script>
中包含组件对象的js内容。
c. <style>
标签内包含仅这个页面组件内才使用的css
d. <template>
中的HTML内容以及<script>export default{...}</script>
中的js内容,和前四天将的是完全一样的写法,绑定,指令,函数,生命周期,axios请求等都一样。前四天怎么用,这里就怎么用。
(3). 路由字典和路由器对象,在src/router/index.js文件中
a. 仅import首页组件对象,不要过早引入其它页面组件
b. 路由字典中首页组件: { path:"/", component:首页组件对象}
c. 其余页面组件都做成懒加载:
{
path: '/相对路径',
component: () => import(/* webpackChunkName: "组件名" */ '../views/其它页面组件.vue')
}
(4). 全局组件都放在src/components文件夹下,每个全局组件.vue文件中。但是,全局组件必须在main.js引入,并用Vue.component()转化为真正的全局组件,才能在其它组件的HTML中使用。
(5). 运行时: 路由器router对象监视浏览器地址栏路径变化,并查找对应的页面组件内容,先代替App.vue中的<router-view>
,然后main.js再将包含要加载的页面内容的App.vue组件内容,替换到唯一完整的index.html中空<div id="app">
位置。