根据之前做的项目总结概括一些关于函数封装、模块化、闭包、 MVC 相关的知识点。
一、 MVC 模型
以上, M 表示 model( 模型 ),V 表示 view( 视图 ),C 表示 Controller( 控制器 )。暂时不解释,先看看如何使用。
1、 模块化
以往在做项目的过程中,为了方便查找,经常会将本页面的相关 js 代码写到页面中,而不是以 js 文件引入的方式加载到页面中,页面中的代码类似于下图:
上图所示代码,或许在开发中比较方便,但是时间久了再回去看这些代码,难免会有些晦涩难懂。为了便于理解,我们尝试着将实现不同功能的代码放置到不同 js 文件中,并引入到当前页面中,对于 js 文件的命名,最好能够描述实现的功能。
根据代码中的例子,我们可以进行如下修改:
- 将 swiper 相关的代码移到
./js/init-swiper.js
文件中 - getCloset() 方法命名不准确,此方法是实现滚动位置与导航相对应
- window.scroll 方法中实现了两个功能,使用
window.addEventListener('scroll',function(){})
将两个功能实现拆开来写 - 将所有跟自动滚动的 js 移动到
./js/auto-slide-up.js
文件中,并引入到当前页面 - 将所有与滚动到某一高度后固定导航位置的 js 存储到文件
./js/sticky-topbar.js
中,并引入到当前页面 - 将鼠标悬停到菜单栏时出现下拉菜单的 js 存储到文件
./js/menu-hover.js
- 将页面再入过程中 loading 相关的 js 存储到文件
./js/loading.js
以上就是模块化的实现过程。
2、MVC 中的 View
此部分最好看完 二、立即执行函数
和 三、闭包的使用
再进行阅读。以 ./js/init-swiper.js
为例。自动轮播的部分对应的视图部分为 index.html
中的 <div class="swiper-container"></div>
中,现在我们给这个 div 外加上一层 div , id 为 mySlides 。对应到 init-swiper.js 中的代码为:1
2
3
4
5...
let view = document.querySelector('#mySlides');
//轮播
var mySwiper = new Swiper (view.querySelector('.swiper-container'), {
...
这么做的目的是,当页面中出现多个轮播时,可以使用 div.swiper-container 的父级元素作为选择器进行实例化。另一方面,在我们看项目源码时能够很清楚的看到我们引入的这部分代码是操作的哪一部分页面元素。
3、MVC 中的 Controller
控制器这部分我们以 ./js/sticky-topbar.js
为例,代码修改成如下结构:1
2
3
4
5
6
7
8
9
10
11
12
13
14!function(){
//固定topBar
let view = document.querySelector('#topBar');
let controller = function(view){
window.addEventListener('scroll',function(){
if (window.scrollY > 0) {
view.classList.add('active');
} else {
view.classList.remove('active');
}
})
}
controller.call(null,view);
}.call()
这种写法直接将视图层和控制器层很清晰的展示给使用者,继续上述代码,我们在进行一次优化,将 cotroller 改成一个对象,将其实现的代码放在 controller 下的 init 方法中,具体代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23!function(){
//固定topBar
let view = document.querySelector('#topBar');
let controller = {
view : null,
init : function(view){
this.view = view;
this.addEvents();
},
addEvents : function(){
let view = this.view;
window.addEventListener('scroll',function(){
if (window.scrollY > 0) {
view.classList.add('active');
} else {
view.classList.remove('active');
}
})
}
}
controller.init(view);
//controller.init.call(controller,view),所以以上代码中的 this 为controller
}.call()
以上代码我们可以继续进行拆分,将实现不同功能的代码进行分开可管理:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18...
addEvents : function(){
let view = this.view;
window.addEventListener('scroll',function(){
if (window.scrollY > 0) {
this.active()
} else {
this.deactive()
}
})
},
active : function(){
this.view.classList.add('active');
},
deactive :function(){
this.view.classList.remove('active');
}
...
以上代码执行时发现报错,问题出在在调用 this.active() 和 this.deactive() 时, this 已经指向 window ,而 window 没有这两个方法,在此提供两个解决方法:
- window.addEventListener(){}.bind(this); 这种方法直接绑定对应的 this
- 使用箭头函数,箭头函数没有 this ,即改成
window.addEventListener('scroll', ()= {...})
,由于箭头函数中无 this , 在方法中使用 this 时会顺着代码向上查找,找到 controller 。
根据以上操作修改响应的其他代码。
二、立即执行函数
继续优化上述代码。根据标题一中的代码可知,我们已经将不同的模块放进到不同的 JS 文件中并分别引入到页面中。看似对代码完成解耦,但是仍旧存在一个问题:每个 JS 文件中的变量大部分为全局变量,如使用 var 声明的变量,如果后期维护引入相同变量名的变量,将会引起报错。那么全局变量存在风险,我们就使用局部变量进行代替。
在 ES6 中,以下写法为局部变量写法:1
2
3{
let a = 1;
}
但是需要注意,如果使用 var 进行变量声明,由于 var 存在变量提升, a 的声明会被提到 {}
之外而称为全局变量。我们知道,在一个函数内部声明的变量,无论是使用 var 还是 let 进行声明,都是局部变量,以 ./js/init-swiper.js
为例,我们将轮播初始化的代码封装到一个函数中,并执行这个函数。代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14function xxx{
//轮播
var mySwiper = new Swiper ('.swiper-container', {
loop: true,
pagination: {
el: '.swiper-pagination',
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
}
})
}
xxx.call();
以上,已经规避了 mySwiper 这个全局变量,但是同时又产生了新的全局变量 xxx ,为了避免这个问题,我们需要定义一个匿名函数,然后直接调用此函数: (function(){}).call()
。
总结以上:
- 不使用全局变量,使用局部变量
- ES5 里面只有函数内有局部变量
- 声明一个 function xxx(),然后调用此方法: xxx.call()
- 但是此时 xxx 为全局变量(全局函数)
- 所以不能给函数命名,只能使用匿名函数:
function(){}.call()
- 以上语法有报错,可以改写为:
!function(){}.call()
或(function(){}).call()
,其中!
可替换为+
或-
三、闭包的使用
以上代码实现了全局变量到局部变量的修改,保证了各个模块之间的变量就算是重命名也不会出现问题。那新的问题又出现了,如果我们就是希望两个模块之间能够完成数据通讯怎么办呢?闭包是一个不错的选择。
看如下用例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22...
//==========index.html======
<script src="module1.js"></script>
<script src="module2.js"></script>
//============module1.js============
!function(){
let person = {
name:'summer',
age:18
}
window.growUp = function(){
person.age += 1;
return person.age;
}
}.call()
//============module2.js============
!function(){
let age = window.growUp();
console.log(age);
}.call()
...
以上, module1.js 中 window.growUp
和 person
构成闭包。闭包主要用来隐藏数据细节,其中 person 的 age 不能修改,只能通过 window.growUp 来不断增加。
综上:
- 立即执行函数使得 person 无法被外部访问
- 闭包使得匿名函数可以操作 person
- window.growUp 保存匿名函数的地址
- 任何地方都可以使用 window.growUp
=》 任何地方都可以使用 window.growUp 操作 person ,但是不能直接访问 person
四、数据操作
现在我们做的页面基本上为静态页面,为了便于开发,实现对数据库数据增删改查操作,我们使用 LeanCloud 作为服务端数据库,来完成前后端的交互。具体如何注册、登录、入门等不再赘述,官网上文档写的非常清楚。
LeanCloud 具有以下功能:
- 登录注册、手机验证码功能(收费)
- 存储任意信息
- 读取任意信息
- 搜索任意信息
- 删除任意信息
- 更新任意信息
- …等等
具体操作如下:
- 1、新增 resume-message 应用,并在库中创建表 Message
- 2、通过 CDN 加速将引入
<script src="//cdn1.lncld.net/static/js/3.6.8/av-min.js"></script>
,引入完成后,在浏览器中打开首页 -> 控制台 -> 输入 AV 即可打印出相关对象信息 - 3、初始化,在页面中增加 js ,内容为:
var APP_ID = '0r7UeXa1LVbwKLzTKIKvxueQ-gzGzoHsz'; var APP_KEY = 'ELk4KGSA1TR3oAd091pwWi9m'; AV.init({ appId: APP_ID, appKey: APP_KEY });
- 4、实例化:
var Message = AV.Object.extend('Message'); var message = new Message(); message.save({ words: 'Hello World!' }).then(function(object) { alert('LeanCloud Rocks!'); })
- 5、刷新页面后,到 LeanCloud 中刷新表 Message ,可见表 Message 有一条新增数据
1、使用 LeanCloud
根据以上操作,我们对简历文件中新增 ./js/message.js
,并引入到 index.html 中,其代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19!function(){
var APP_ID = '0r7UeXa1LVbwKLzTKIKvxueQ-gzGzoHsz';
var APP_KEY = 'ELk4KGSA1TR3oAd091pwWi9m';
AV.init({
appId: APP_ID,
appKey: APP_KEY
});
//创建表
var Message = AV.Object.extend('Message');
//创建表中的一条数据
var message = new Message();
//存储一条数据到表中
message.save({
words: 'Hello World!'
}).then(function(object) {//保存成功的回调
alert('LeanCloud Rocks!');
})
}.call()
2、增加页面结构
在 index.html 文件中,增加留言部分,器结构为:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27<section class="message section">
<h2>留言</h2>
<div class="message-list">
<ul>
<li>
<img src="./img/avatar/0.png" alt="">
<span class="name">abc</span>:
<span class="text">哈哈哈</span>
</li>
</ul>
</div>
<div class="post-message">
<form id="postMessageForm">
<div class="item">
<label for="name">姓名:</label>
<input type="text" name="name">
</div>
<div class="item">
<label for="content">留言:</label>
<input type="text" name="content">
</div>
<div class="item">
<button type="submit">提交</button>
</div>
</form>
</div>
</section>
至于样式,就不再过多说明,只要排版不至于特别丑就可以。接下来我们来监听用户提交表单的事件。一般来说,我们最好监听 from 的表单提交事件,即 message.addEventListener('submit',function(){...})
,message.js 代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20...
let myForm = document.querySelector('#postMessageForm');
myForm.addEventListener('submit',function(e){
e.preventDefault();
let name = myForm.querySelector('input[name="name"]').value;
let content = myForm.querySelector('input[name="content"]').value;
//创建表
let Message = AV.Object.extend('Message');
//创建表中的一条数据
let message = new Message();
//存储一条数据到表中
message.save({
'name': name,
'msg':content
}).then(function(object) {//保存成功的回调
console.log(object);
window.location.reload();
})
})
...
以上关于表单监听 form 的提交事件而不是监听 button 的点击事件,原因是这样,因为有的用户习惯在输入完成后直接回车,回车事件并不会触发 button 的点击事件,要想完美解决这个问题,js 就既要监听 button 的点击事件还要监听键盘的回车键操纵,写起来会更麻烦。综上才选择的 form 表单提交。
接口请求成功后,直接刷新页面即可。
3、查询展示历史留言
以上已经完成留言的发送,那么如何查询历史留言信息呢?
查看 LeanCloud 官网文档 -> 对象 -> 批量操作中查看获取数据的语法:1
2
3
4
5
6
7
8
9
10
11var query = new AV.Query('Todo');
query.find().then(function (todos) {
todos.forEach(function(todo) {
todo.set('status', 1);
});
return AV.Object.saveAll(todos);
}).then(function(todos) {
// 更新成功
}, function (error) {
// 异常处理
});
根据以上代码,在 message.js中增加获取数据的代码:1
2
3
4var query = new AV.Query('Message');
query.find().then(function (messages) {
console.log(messages)
});
其中, Message
为表的名称,请求成功后,我们可以打印出返回数据来查看下。我们提交的数据保存在返回数据的 attributes 中,我们可以通过循环输出将数据库中的姓名、留言信息展示在页面中。
如果请求成功后,我们可以将以下代码添加到请求成功后的回调中进行处理:1
2
3
4
5
6
7
8
9
10
11let array = messages.map((item) => item.attributes);
array.forEach((item) => {
let obj = document.querySelector('#messageList');
let li = document.createElement('li');
li.innerHTML = `
<img src="./img/avatar/`+ imgs[Math.floor(Math.random() * imgs.length)] +`" alt="">
<span class="name">${item.name}</span>:
<span class="text">${item.msg}</span>
`;
obj.append(li);
})
其中,页面中的头像是在静态资源中存储的 7 张图片,随机读取的。可以看到目前我们添加完成留言后,页面自动刷新并且能够看到新增加的留言信息。但是对于提交完数据后页面刷新这一体验是不特别好,我们可以在表单提交成功后,将数据直接添加到页面并重置表单数据,这样带来的体验会比较好些。代码就不再展示了,这部分比较简单。
4、将新增 message.js 改写 MVC 结构
在此文件的立即执行函数中添加视图、控制器、模型。一时间很难解释清出,还是直接上代码吧。最终的代码可以查看我的简历。
五、MVC 究竟是什么
MVC 其实是将代码分为三部分,第一部分 V 为 view(视图) ,负责告诉程序员你的代码长什么样子、在页面的哪一个部分。第二部分 M 为 model(数据) ,主要能够说明数据有哪些操作,以这篇讲的代码为例子,数据部分共分为三个操作:数据初始化、获取数据、保存数据。第三部分 C 为 controller(控制器) ,这部分主要负责对 DOM 的数据操作、事件绑定、逻辑操作等。本质上来说,MVC 只是一种代码组织形式,在使用 controller 时需要将 view 和 model 传递给 controller。
以上操作用到的代码请参考: https://github.com/HappyJeannie/resume
推荐几个网站:
- 图片素材网: https://unsplash.com/