项目准备
启动项目步骤:
-
安装依赖
yarn install ## 或者 npm i
npm可能会警告一些东西,但无所谓。
推荐yarn
-
运行项目
node admin.js
-
前端开发
参考接口文档,进行前端进行开发。
首页开发
首页需要展示较多数据,但一次性加载完全部数据则会及其消耗性能,因此需要进行分页处理。
如果有一万条数据,想让其绑定到页面中,怎么做好一些?
一种方案
文档碎片,遍历数据,把对应的数据和结构都添加到文档碎片中(或基于字符串拼串),再把文档碎片扎入到页面中(优势:减少了DOM的回流)
另一种方案
虚拟DOM,类似于REACT架构,基于虚拟DOM以及DIFF算法
第三种方案
进行分页异步加载数据
分页加载思路
如果想实现分页,那么后端需要支持。如果后端不支持,那么无法完成。
步骤 | 说明 | 客户端 | 服务端 |
---|---|---|---|
第一步 | 请求数据携带每页展示的条数和页数 | 像服务端发送请求数据 | 服务端接收响应 |
第二步 | 接收服务端传回来的数据 | 根据前端传来的需求,进行数据查询 | |
第三步 | 将数据渲染到页面 |
快速开始
进入首页,首先我们应该显示出待投票的人,如下图所示:
根据真实情况,我们请求到后端接口后,后端会将数据返回给我们,我们只需要将这些数据渲染成HTML元素即可。
但在这之前,我们需要发送出请求,才能进行数据的”可视化“操作。
在首页的JS逻辑中,采用单例模式进行编写JS代码。在编写逻辑之前,我们要先配置一下axios的默认配置,以便后期更方便的发送请求。
if (typeof axios !== "undefined") {
// 请求基于地址
axios.defaults.baseURL = "http://localhost:8000";
// 允许跨域请求
// axios.defaults.withCredentials = true;
// 将请求数据转换成URLENCODED格式
axios.defaults.transformRequest = (data) => {
let str = ``;
if (data && typeof data === "object") {
for (let attr in data) {
if (data.hasOwnProperty(attr)) {
str += `${attr}=${data[attr]}&`;
}
}
}
return str.substring(0, str.length - 1);
};
// 默认请求头
axios.defaults.headers["Content-Type"] = "x-www-form-urlencoded";
// 拦截器,只返回服务器返回的结果
axios.interceptors.response.use((result) => result.data);
}
编写首页的逻辑框架。通过一个init函数进行初始化,在初始化的函数中进行数据的获取(初始化)。初始化主要涉及到两个逻辑:获取数据、数据”可视化“。
// 创建单例模式
let matchRender = (function ($) {
// 获取显示用户的容器
let $userList = $(".userList"),
// 找到无序列表容器
$wrapper = $userList.find("ul"),
// 如果没有用户时提示的文字容器
$tip = $userList.find(".tip");
// 每页显示条数
let limit = 10,
// 页数
page = 1,
// 搜索内容
search = "";
// 获取数据
let queryDate = function queryDate() {
axios
.get("/getMatchList", {
params: {
limit,
page,
search,
},
})
.then(bindHTML);
};
return {
init: function init() {
queryDate();
},
};
})(Zepto);
matchRender.init();
-
获取数据
获取数据很简单,无非是一个请求,发送后将结果传入
then
方法中,在此方法中进行数据的"可视化"。 -
”数据可视化“
这里即将数据转换成HTML元素,并添加到页面,让用户看到。由于数据”可视化“代码量较为复杂,因此为了后期维护以及美观新增一个方法用于数据绑定即
bindHTML
方法。// 数据绑定 let bindHTML = function bindHTML(result) { let { code, list = [] } = result; if (parseFloat(code) !== 0) { // 获取的数据并不是想要的 $wrapper.css("display", "none"); $tip.css("display", "block"); return; } // 获取的数据是想要的 $wrapper.css("display", "block"); $tip.css("display", "none"); let $frg = $(document.createDocumentFragment()); list.forEach((item, index) => { console.log(item); let { id, name, picture, sex, matchId, slogan, voteNum, isVote } = item; $frg.append(`<li> <a href="detail.html?userId=${id}"> <img src="${picture}" alt="${name}" class="picture"> <p class="title"> <span>${name}</span> | <span>编号 #${matchId}</span> </p> <p class="slogan">${slogan}</p> </a> <div class="vote"> <span class="voteNum">${voteNum}</span> ${ // 根据是否投过票,判断是否显示 "投他一票" 按钮 parseFloat(isVote) === 0 ? `<a href="javascript:;" class="voteBtn">投他一票</a>` : "" } </div> </li>`); }); $wrapper.append($frg); $frg = null; };
处理完成后上方的逻辑,在HTML页面中引入我们相关的JS代码并启动后台程序即可看到效果。
<!--IMPORT JS-->
<script src="js/zepto.min.js"></script>
<script src="js/axios.min.js"></script>
<!-- 用于设置默认配置项的文件 -->
<script src="js/axios_default.js"></script>
<!-- <script src="js/component/nav.js"></script> -->
<script src="js/index.js"></script>
滑动相关事件
下拉加载更多
下拉刷新的规则即(一屏幕的高度-卷去的高度>=页面的真实高度)那么则将触发刷新的逻辑。
下拉刷新即分页加载,也就是当下拉时,会请求下一页的数据并添加到页面。
为了控制当前页数及总量,应在全局在添加两个变量,用于控制当前页数等。
let limit = 10,
// 总页数
pageNum = 1,
// 总数
total = 0,
// 页数
page = 1,
// 搜索内容
search = "",
// 是否执行的标志
isRun = false;
接下来便是在进行数据绑定之前获取到服务端返回的结果,并提取其总数量与总页数。
// 获取数据
let queryDate = function queryDate() {
axios
.get("/getMatchList", {
params: {
limit,
page,
search,
},
})
.then((result) => {
// 处理总页数条数等
pageNum = parseFloat(result.pageNum);
total = parseFloat(result.total);
return result;
})
.then(bindHTML);
};
最后只需要在滚动事件中进行数据处理即可。
$(window).on("scroll", () => {
let {
clientHeight,
scrollTop,
scrollHeight,
} = document.documentElement;
if (clientHeight + scrollTop + 100 >= scrollHeight) {
// 即将到达页面底部
// 如果正在加载中,那么不加载数据
if (isRun) return;
// 如果所有数据都加载完成了,那么不在加载
if (page >= pageNum) {
$(".none").css("display", "block");
return;
}
isRun = true;
page++;
queryDate();
}
});
搜索
因为queryDate
方法已经拼接了search参数,因此只需要为按钮绑定事件后,获取其搜索的值,然后重新执行此方法即可。
$searchBtn.tap(() => {
if (isRun) return;
isRun = true;
// 获取按钮上方的input框的值
search = $searchBtn.prev("input").val().trim();
page = 1;
// 清空之前的内容
$wrapper.html("");
queryDate();
});
导航插件
对于登陆回转(从哪里跳转到登录页,那么登陆成功后应该跳回哪个页面),为了解决这个问题可以在进入登录页时传递一个参数,这个参数的值代表来自哪个页面。
同时由于很多页面都包含导航栏,因此将其写为一个插件。通过在需要的页面引入JS文件即可使用导航栏。
进入页面后,首先应该检查是否已经登陆,如果已经登陆,那么显示的是个人信息,否则显示登陆与注册的信息。
axios
.get("/checkLogin")
.then((result) => {
//=>控制导航的显示
let code = parseFloat(result.code);
// 根据是否登陆 添加不同的内容。
$mainBox.prepend(`<nav class="navBox">
<a href="index.html">首页</a>
${
code === 0
? `<a href="javascript:;">登录</a><a href="javascript:;">注册</a>`
: `<a href="detail.html"></a><a href="javascript:;">退出</a>`
}
</nav>`);
$navBox = $mainBox.find(".navBox");
$navList = $navBox.find("a");
// 把结果返回交给下一个then
return code;
})
将code进行return操作,在下一个then中进行判断,如果已经登陆了,那么获取其信息。否则也就不用再继续执行了。
.then((code) => {
//=>如果已经登录:获取登录用户的用户信息
if (code === 0) return;
// 获取已经登陆的用户信息
return axios.get("/getUser");
})
关于事件绑定,由于元素是动态插入的,因此需要采用事件委托的方式进行数据绑定。为了记录是在哪一个页面进入的登陆页,需要在请求地址拼接一个参数,用于表示请求地址。
.then(() => {
//=>基于事件委托给NAV-BOX中的A绑定点击事件
$navBox.tap((ev) => {
let target = ev.target,
tarTAG = target.tagName,
tarINN = target.innerHTML;
if (tarTAG !== "A") return;
if (tarINN === "登录") {
//=>window.location.href既可以实现页面跳转也可以基于这个属性获取当前页面的URL地址(一定要编码,否则特殊字符会和跳转的地址冲突)
window.location.href = `login.html?fromURL=${encodeURIComponent(
window.location.href
)}`;
return;
}
if (tarINN === "注册") {
window.location.href = `register.html?fromURL=${encodeURIComponent(
window.location.href
)}`;
return;
}
if (tarINN === "退出") {
axios.get("/exitLogin");
window.location.href = window.location.href; //=>页面刷新
return;
}
});
});
登陆页面
登陆页面同样采用单例模式
let loginRender = (function ($) {
return {
init: function init() {
}
}
})(Zepto);
loginRender.init();
同时为了简化操作,采用工具库的方式,将地址解析的方法写入到这个文件。
let utils = (function anonymous() {
//=>URL地址解析
let queryURLParams = function (url = window.location.href) {
let obj = {},
reg = /([^?=&#]+)=([^?=&#]+)/g;
url.replace(reg, (...arg) => {
let [, key, value] = arg;
obj[key] = value;
});
return obj;
};
return {
queryURLParams
}
})();
let loginRender = (function ($) {
let $userName = $("#userName"),
$userPass = $("#userPass"),
$submit = $("#submit");
// 获取参数传递的URL
let fromURL = utils.queryURLParams()["fromURL"];
fromURL ? (fromURL = decodeURIComponent(fromURL)) : (fromURL = "index.html");
let submitFn = function submitFn() {
axios
.post("/login", {
// 用户名
name: $userName.val().trim(),
//=>HEX_MD5把一个字符串进行MD5加密处理
password: hex_md5($userPass.val().trim()),
})
.then((result) => {
let code = parseFloat(result.code);
if (code === 0) {
//=>登录成功
window.location.href = fromURL;
return;
}
alert("请检查用户名密码,登录失败了!");
});
};
return {
init: function init() {
$submit.tap(submitFn);
},
};
})(Zepto);
loginRender.init();
关于跨域问题
当使用VSCODE的Live service插件进行打开服务器时无法携带Cookie进行发送请求,因此使用web storm进行替换。
携带Cookie发送需要将Axios的withCredentials
配置开启。
axios.defaults.withCredentials = true;
本项目代码参考:https://github.com/changeclass/Tzk/tree/master/2020-09/09/code