美食广场-项目总结
项目是程序员最好的锻炼神器,只有经过项目,才能真正检验知识的完整性。
本次项目是我在某内学习,跟着老师一步步写出来的,这种跟着老师思路走的项目,做完始终有种不太真实的感觉,虽然都是自己敲下的代码,但是整体的项目思想是不连贯的,因此,写一篇项目总结,将项目中的一些难点,还有整体的脉络梳理一下。在梳理过程中,我会按照我在gitee的提交顺序进行。
一、项目准备阶段
1.1,项目文件目录
在项目开发中,一个良好的文件目录,尤为重要,能帮助我们节省许多时间。例如,我们在使用express时,就会使用express创建出一个完整的项目目录结构,后续只需要在固定的结构下写入代码,就能快速构建好项目的服务器。
下面就是本次项目的文件目录结构,
-
common:公共样式css,公共js文件
-
components:公共组件(在项目开发中,对于像头部栏,侧边栏,底部这样频繁使用的地方,通常将代码抽离出去,形成一个单独的组件部分,在页面需要使用时,加载进来,一般称为组件化开发)
-
page:页面文件,每个页面文件包含完整的html,css,js文件
-
node_modules:项目中使用到的依赖(库)
-
index.html 项目的主体文件(项目采用SPA)
就是只有一张Web页面的应用。单页应用程序 (SPA) 是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序。 浏览器一开始会加载必需的HTML、CSS和JavaScript,所有的操作都在这张页面上完成,都由JavaScript来控制。因此,对单页应用来说模块化的开发和设计显得相当重要。
1.2,基础样式、js库
在common文件中,css文件下写好样式重置文件,配置基础样式文件(例如版心样式等);
“*CSS 重置样式主要就是为了让各个浏览器的css样式有一个统一的基准。*我们可以通过重置样式,把浏览器的默认样式全部去掉,然后设置一个统一的标准。这样我们的网页在不同的浏览器下显示的效果就是一个的了
在js文件夹下,放置jQuery文件,方便后续项目中引用。
1.3,查看后端提供的API文档
后端工程师给到的项目开发文档,提供了请求的url,以及请求需要传递的参数,知道这些参数名,我们在构建页面的时候在关键位置使用同样的参数命名,对于后续的js操作会更加便捷。
二、项目开发阶段
2.1,html代码引入
在本项目中,采用SPA(单页面应用程序)的思想,因此如何将业务代码重新引入index页面,是我们需要解决第一项问题。
在本项目采用的方法使用的是监听hash变化的方法,来引入(切换)不同的功能页面
2.1.1 监听hash变化切换页面
demo
<body>
<div id="header">
<a href="#p=01.html">页面1</a>
<a href="#p=02.html">页面2</a>
<a href="#p=03.html">页面3</a>
<a href="#p=04.html">页面4</a>
</div>
<div id="main"></div>
<script src="../jquery/jquery.js"></script>
<script>
//addEventListener 监听事件,
//在hash变化时自动触发
addEventListener('hashchange',function(){
console.log('新的hash值:',location.hash);
console.log('截取字符串',location.hash.substr(1));
//如下获取p=xxx的字符串
let h = location.hash.substr(1)
//如下将h转为URLSearchParams对象,此对象专门用于处理url网址信息中的查询字符串
let params = new URLSearchParams(h)
//如下利用对象获取参数p的值
let p = params.get('p')
console.log(p);
//如下,利用load加载页面
$('#main').load(p)
})
</script>
</body>
需要说明的点:
-
addEventListener:是一个侦听事件并处理相应的函数,此处监听hash值得变化
-
location.hash:location对象的hash属性;hash 属性是一个可读可写的字符串,该字符串是 URL 的锚部分(从 # 号开始的部分)
-
URLSearchParams:URLSearchParams 对象提供了一些方法来操作查询参数,如获取参数值、添加新参数、删除参数等
-
load:加载数据,并把数据放置到指定的元素中
2.1.2 加载页面依赖文件
在加载完页面的html文件以后,需要将它的依赖文件一并引入,不然无法实现响应的效果。对于css文件直接拼接地址,使用模板用法就能引入。
//页面加载完毕以后,同步加载css文件
$(this).append(`<link rel="stylesheet" href="./pages/${p}/${p}.css">`)
但对于js文件则不能使用模板语法的方法引入,因为在脚本中再次写
const s = document.createElement('script')
s.src = `pages/${p}/${p}.js`
$(this).append(s)
2.2,GET请求,页面渲染
在本项目当中,除了页面基本的框架布局,其他内容都依靠通过后端获取数据后,利用js渲染动态生成的。在此项功能中,一是如何从后端获取数据,二就是如何使用css控制样式,是渲染页面满足设计要求。
在每一个功能页面的js中都会使用到get请求,并渲染页面。
发送请求模块
//url的值从api文档中复制
let url = 'https://serverms.xin88.top/mall?page=' + p
//jquer封装的get请求,$.get()直接调用
$.get(url, data => {
$('#mall-items').html(
data.data.map(value => {
//在服务器返回的数据中,解构需要的值
let { pic, name, price, sale_count } = value
//模板语法,拼接以后返回,渲染
return `
<li>
<img src="assets/img/mall/${pic}" alt="">
<div>
<p>${name}</p>
<div>
<span class="price">¥${price}</span>
<span class="sale">月售${sale_count}</span>
</div>
</div>
</li>
`
})
)
2.3,分页功能
页面的分页功能在后端是分页查询路由模块获取到本页查询的开始值start,每页返回的数据量size;在前端效果上,则是一些页码,点击之后进行切换,每页固定多少个页码,超过之后页码需要变化,另外,需要上下页的翻页按钮,这就是最基础的分页功能;后端的分页功能在前后端分离的今天,不需要考虑,我们只需要根据服务器返回当前页码值和总的页码数量,在前端实现分页就行。
前端js文件,分页功能模块:
根据服务器数据,解构当前页码和总的页码数
let { page, pageCount } = data
本项目中,固定显示5页
//最多显示五页
let start = page - 2
let end = page + 2
if (start < 1) {
start = 1
end = start + 4
}
if (end > pageCount) {
end = pageCount
start = end - 4
}
获得数据之后,在页面渲染出来,渲染之前的清空操作很有必要,因为这里循环添加必须使用append()这个追加,而不是html()的覆盖式渲染
//清空
$('.pages>ul').empty()
for (let i = start; i <= end; i++) {
$('.pages>ul').append(`<li class="${page == i ? 'active' : ''}">${i}</li>`)
}
因分页功能需要使用到请求服务器返回的数据,因此在项目中将页码渲染和页面渲染都封装成了函数getData()
因此,需要初始化就要调用一下函数
getData(1)//1是传入的页码,初始化时显示第一页的数据
为页码添加函数,使得点击时显示的数据能够发生变化
//利用事件委托机制
$('.pages>ul').on('click', '>li', function () {
let pno = $(this).html()
getData(pno)
})
实现了页码切换以后,我们还需要,上下页的按钮,也为其添加事件,使其点击时也能发生页面的改变
//翻页按钮
//下页
$('.pages>button').eq(1).click(function () {
// console.log('翻页点击');
$('.pages li.active').next().click()
})
//上页
$('.pages>button').eq(0).click(function () {
$('.pages li.active').prev().click()
})
})
在一些特殊时候,如第一页或者最后一页时,我们需要禁用按钮
//禁用按钮
let $btn_prev = $('.pages>button').eq(0)
let $btn_next = $('.pages>button').eq(1)
page==1?$btn_prev.hide():$btn_prev.show()
page==pageCount?$btn_next.hide():$btn_next.show()
2.4,滚动加载
滚动加载其实和分页功能使用的都是同一套路由,不过在前台的交互上选择不一样事件。分页功能选择的是通过监听对按钮和页码的点击事件,发送请求完成的页面更新;滚动加载则是通过监听页面的滚动事件,通过判断滚动的距离来触发相应的请求事件,将新的数据添加到页面上。
监听滚动事件
//监听滚动触底
$(window).scroll(function(){
let top = $(window).scrollTop()
let win_h = $(window).height()
let dom_h = $(document).height()
//提前触底
if(top > dom_h-win_h-300){
// getData(2)
// nowPage++
getData(nowPage+1)
}
})
锁定请求
通过判断距离去触发请求数据的事件,存在一个问题,一旦距离超过我们限定的距离则事件就会一直触发,发送过多没有必要的请求,导致服务器承受过多压力,因此我们需要对请求事件进行锁定,当请求完成以后再解除锁定。
//设置一个锁,初始状态为开放
let lock = false
function getData(p) {
//如果查询到的数据已经为空了,则停止发送请求,用#nomore的状态作为参数来判断
console.log($('.nomore:visible').length);
//在请求事件内
//查验锁的状态
//1,锁定,如果锁定则放弃本次请求事件
if(lock)return
//2,未锁定,加锁,如果未锁定则加锁,抢占请求事件,保证请求事件不被打断
lock = true
$.get(url, data => {
//请求完成以后,解锁,释放
lock=false
})
}
在完成监听事件以及加锁以后,只需要通过正常请求数据,然后渲染页面,便能是实现页面的滚动加载啦。
2.5,JS实现瀑布流布局
瀑布流对于图片的展现,是高效而具有吸引力的,用户一眼扫过的快速阅读模式可以在短时间内获得更多的信息量,而瀑布流里懒加载模式又避免了用户鼠标点击的翻页操作,瀑布流的主要特性便是错落有致,定宽而不定高的设计让页面区别于传统的矩阵式图片布局模式,巧妙的利用视觉层级,视线的任意流动又缓解了视觉疲劳
利用js实现瀑布流布局是一种较为简单和常规的布局,它的理论步骤其实很简单:
-
等宽不等高的布局,需要获取到元素的宽度,为了计算每行能够排列的元素数量
-
元素定位,设置第一行的top=0,得到第一行的元素,作为一个数组
-
遍历第一行元素的高度,查询最小值
-
将新元素添加到高度最小的元素下面
-
在数组中删除高度最小的元素,并将新元素添加进来
-
重复上述操作
// 获取某个元素的底部偏移量
function getBottom(el) {
const top = $(el).css('top') // '0px'
const height = $(el).height()
// 把 top 转数字 再相加
return parseInt(top) + height
}
$.get(url, data => {
// 全局设定:
const li_w = 242.5 //宽度
const space = 10 //间距
$('#note-items').append(
data.data.map(value => {
// 关于网络图: 挨个需要异步加载, 只有当加载完毕后才能知道其宽高
// 为了能够初始化时就计算元素的高度, 则服务器必须在返回值中提供图片的宽高
const {
head_icon, name, title, favorite, cover,
width, height //图片的宽高
} = value
// 根据图片原有比例, 计算出图片的显示高度
const img_h = li_w / width * height
})
})
// 遍历已有的所有li 计算其位置
// each: 类似与 数组的forEach方法, 可以对元素进行遍历
// 数组: 存放已经完成布局的元素中, 每一列最下方的那个
const arr = []
$('#note-items>li').each((i, el) => {
// el: 元素; i: 序号
// console.log(i, el)
// 对前4个元素进行布局
if (i < 4) {
// css方法: 用于设置元素的 style 内联样式
$(el).css({
top: 0, // 第一排, 所以top是0
left: i * (li_w + space) // 左侧偏移量=序号*(宽度+间距)
})
arr.push(el)
} else {
// 第五个元素摆放的位置: 已经在前4个元素里, 底部最小的那个元素下方
// 思路: 假设数组中第一个元素最小, 然后遍历后续的看有没有更小的
let min_el = arr[0]
for (let i = 1; i < arr.length; i++) {
if (getBottom(arr[i]) < getBottom(min_el)) {
min_el = arr[i]
}
}
// 新的元素放在 最小元素的正下方: 左侧对齐
$(el).css({
left: $(min_el).css('left'),
top: getBottom(min_el) + space
})
// 从 arr 中删除当前找到的最小元素, 把新增的元素存储数组里
// 找到之前最小元素在 arr 中的序号
const index_min = arr.indexOf(min_el)
// 删除最小元素, 把新增元素加入
arr.splice(index_min, 1, el)
// 解决高度坍塌问题:
// 由于所有的li都是定位方式, 导致父元素高度丢失
// 解决: 给父元素设置固定的高度 = 最下方的li的底部
// 找最大: 假设第一个值最大, 然后遍历看是否有更大的
let max_el = arr[0]
for (const el of arr) {
if (getBottom(el) > getBottom(max_el)) {
max_el = el
}
}
2.6,swiper
swiper是一个免费,强大的滑动插件,可以快速帮助我们构建一个好看的滑动模块
官方网站:https://www.swiper.com.cn/
这里使用swiper构建一个三个一组的滑动
// 设置swiper
let mySwiper = new Swiper('.swiper', {
//每页显示三个
slidesPerView: 3,
//间隔10像素
spaceBetween: 10,
//三个一组
slidesPerGroup: 3,
on: {
slideChange: function () {
// console.log('变更',this.activeIndex);;
$('.menu li').eq(this.activeIndex / 3).click()
},
}
})
2.7,animate
Animate.css是一个现成的跨浏览器动画库,可以在你的web项目中使用。非常适合强调、主页、滑块和注意力引导提示。
在项目中按照animate库
npm install animate.css
在所需要的元素中,添加animate设置好的类名,便可以实现动画效果
2.8,注册页面
利用正则表达式验证用户输入的手机号码是否正确
/^1[3-9]\d{9}$/.test(phone)
保证注册页面所有信息都填写才能发送注册请求
visible
//注册操作
$('#registerBtn').click(function(){
//判断输入框的值都是对的
if($('.msg>p.ok:visible').length == 3){
let phone = $('input#phone').val()
let pwd = $('input#pwd').val()
let url = '测试url'
let params = {phone,pwd}
$.post(url,params,data=>{
if(data.code == 200){
alert(`恭喜你成为本网站第${data.id}位吃货!即将跳转登录页面`)
location.assign('#p=login')
}else{
alert('注册失败,请稍后重试')
}
})
}else{
alert('请确保所有信息都填写正确')
}
})