登录和瀑布流是两个非常常用的功能,本文将讨论这两个功能的具体的设计思路
登录
表单展示(单例模式)
验证登录,前端验证(策略模式)
登录成功后,各模块的显示(发布订阅模式封装,父组件向其他组件父组件或孙组件等传值)
-
最简单
用户输入用户名和密码等,浏览器将其发给服务器,服务器验证成功,通知浏览器更新保存session的cookie,并返回登录成功后的页面给用户
-
端点登录sso
权限管理
-
常规权限模型
RBAC(Role-Based Access Control)基于角色的权限访问控制
- 常规管理系统中的权限
-
根据登录用户返回菜单列表— — 可能是一颗树
-
根据登录用户控制接口权限
- 前台系统中的权限管理
- 更专注于入口的控制
- 越权的提示也不可少
- 多级权限可能会涉及到计算
根据数组构建树
菜单列表通常是一个树的结构,后端一般喜欢给一个列表(数组),该如何去构建?
给定原始数据(originData),期待生成一个树形结构的对象(treeData)
export type Item = {
id: number;
parentId: number;
name: String;
};
export type Node = Item & { children: Node[] };
export const originData: Item[] = [
{
id: 0,
parentId: null,
name: "生物"
},
{
id: 1,
parentId: 0,
name: "动物"
},
{
id: 2,
parentId: 0,
name: "植物"
},
{
id: 3,
parentId: 0,
name: "微生物"
},
{
id: 4,
parentId: 1,
name: "哺乳动物"
},
{
id: 5,
parentId: 1,
name: "卵生动物"
},
{
id: 6,
parentId: 2,
name: "种子植物"
},
{
id: 7,
parentId: 2,
name: "蕨类植物"
},
{
id: 8,
parentId: 4,
name: "大象"
},
{
id: 9,
parentId: 4,
name: "海豚"
},
{
id: 10,
parentId: 4,
name: "猩猩"
},
{
id: 11,
parentId: 5,
name: "蟒蛇"
},
{
id: 12,
parentId: 5,
name: "麻雀"
}
];
export const treeData: Node = {
children: [
{
children: [
{
children: [
{ children: [], id: 8, name: "大象", parentId: 4 },
{ children: [], id: 9, name: "海豚", parentId: 4 },
{ children: [], id: 10, name: "猩猩", parentId: 4 }
],
id: 4,
name: "哺乳动物",
parentId: 1
},
{
children: [
{ children: [], id: 11, name: "蟒蛇", parentId: 5 },
{ children: [], id: 12, name: "麻雀", parentId: 5 }
],
id: 5,
name: "卵生动物",
parentId: 1
}
],
id: 1,
name: "动物",
parentId: 0
},
{
children: [
{ children: [], id: 6, name: "种子植物", parentId: 2 },
{ children: [], id: 7, name: "蕨类植物", parentId: 2 }
],
id: 2,
name: "植物",
parentId: 0
},
{ children: [], id: 3, name: "微生物", parentId: 0 }
],
id: 0,
name: "生物",
parentId: null
};
- 单元测试
import arrayToTree from "./arrayToTree";
import { originData, treeData } from "./data";
test("arrayToTee", () => {
expect(arrayToTree(originData)).toEqual(treeData);
});
-
实现步骤
-
实现函数签名
-
实现函数体
-
处理 root
-
实现 addChildren 方法
-
执行 addChildren
-
-
【解题步骤】
import { Item, Node } from "./data";
// + 实现函数签名
// + 实现函数体
// + 处理 root
// + 实现 addChildren 方法
// + 执行 addChildren
const arrayToTree = (arr) => {
if (!Array.isArray(arr) || arr.length < 1) return null;
const [root] = arr.filter(item => item.parentId === null);
const addChildren = (node, dataList) => {
const children = dataList
.filter(item => item.parentId === node.id)
.map(item => addChildren(item, dataList));
return { ...node, children };
};
return addChildren(root, arr);
};
export default arrayToTree;
树的遍历
【题目分析】
把上一节树的数据转成数组?
【答题思路】
- 单元测试
import treeToArray from "./treeToArray";
import { originData, treeData } from "./data";
test("arrayToTee", () => {
expect(
treeToArray(treeData).sort((a, b) => (a.name > b.name ? 1 : -1))
).toEqual(originData.sort((a, b) => (a.name > b.name ? 1 : -1)));
});
-
大概步骤
- 编写函数框架
- 实现
const nodeToArray = (node: Node, result: Item[]) => {}
- 调用 nodeToArray
【解题步骤】
import { Node, Item } from "./data";
const treeToArray = (node: Node) => {
const nodeToArray = (node: Node, arr: Item[]) => {
const { children, ...item } = node;
arr.push(item);
children.forEach(child => nodeToArray(child, arr));
return arr;
};
return nodeToArray(node, []);
};
export default treeToArray;
【问题延伸】
- 二叉树遍历 https://leetcode.com/problems/binary-tree-preorder-traversal/
- 二叉树层序遍历 https://leetcode-cn.com/problems/binary-tree-level-order-traversal/
- bfs
瀑布流
图片排列
同宽同高:inline-block,flex,float,自动按内容排列
同宽不空高:position(top:列的高[ ]arrCol/index,left:index)
遍历items
-
第一行
-
其他行:找出最小高的列显示图片:遍历arrColj
<style>
.item{
position: absolute;
}
</style>
<body>
<div id="box">
<div class="item"><img src="270*270.jpg" alt=""></div>
<div class="item"><img src="188.png" alt=""></div>
</div>
<script>
var box = document.getElementById('box')
var items = box.children
var gap = 10
window.onload = function(){
waterFall()
function waterFall(){
// 视口的宽度
var pageWidth = getClient().width
// 图片item的宽
var itemWidth = items[0].offsetWidth
// 列数取整
var columns = parseInt(pageWidth/(itemWidth+gap))
// 列的高数组
var arr = []
for (var i=0; i<items.length;i++){
// 第一行
if(i<columns){
items[i].style.top = 0
items[i].style.left = (itemWidth+gap)*i + 'px'
arr.push(items[i].offsetHeight)
}else{
// 其他行
// 1. 找出arr列的最小高度和最小索引
// 假设
var minHeight = arr[0]
var index = 0
// for循环找出
for (var j=0; j<arr.length; j++){
if (minHeight > arr[j]){
minHeight = arr[j]
index = j
}
}
// 2. 第二行的第一张图片出现的位置
items[i].style.top = arr[index] + gap + 'px'
items[i].style.left = items[index].offsetLeft + 'px'
// 最小高度的图片的offsetLeft也是第二行的第一张图片的left
// 3. 显示出第一张图片后,改变最小列的高度
arr[index] = arr[index] + items[i].offsetHeight + gap
}
}
}
// 获取 视口的宽度或宽度 的兼容处理
function getClient(){
return {
width: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth,
height: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
}
}
// 获取 滚动条的高度 的兼容处理
function getScrollTop(){
return window.pageYOffset || document.documentElement.scrollTop
}
}
</script>
懒加载
-
一页里的图片不多可以都写在html里,可再结合,分页请求
视口高+滚动条高> 图片items[i] 的顶部位置top,将data-src赋值给data
if (items[i].offsetTop < _clientHeight + _srollTop)
见js节流(滚动限制)防抖(屏幕高度计算)函数的懒加载应用
-
一页里的图片太多,后端一段一段请求
视口高+滚动条高>最后一张图片的顶部位置top时,去请求新的图片地址
将请求到的data加入dom(createDocumentFragment一次性dom操作)
接下来就和1一样了
<div id="box"> <div class="item"><img src="270*270.jpg" alt=""></div> <div class="item"><img src="188.png" alt=""></div> </div> <script> var box = document.getElementById('box') var items = box.children var gap = 10 window.onload = function(){ waterFall() function waterFall(){ // 视口的宽度 var pageWidth = getClient().width // 图片item的宽 var itemWidth = items[0].offsetWidth // 列数取整 var columns = parseInt(pageWidth/(itemWidth+gap)) // 列的高数组 var arr = [] for (var i=0; i<items.length;i++){ // 第一行 if(i<columns){ items[i].style.top = 0 items[i].style.left = (itemWidth+gap)*i + 'px' arr.push(items[i].offsetHeight) }else{ // 其他行 // 1. 找出最小高度和最小索引 // 假设 var minHeight = arr[0] var index = 0 // for循环找出 for (var j=0; j<arr.length; j++){ if (minHeight > arr[j]){ minHeight = arr[j] index = j } } // 第二行的第一张图片出现的位置 items[i].style.top = arr[index] + gap + 'px' items[i].style.left = items[index].offsetLeft + 'px' // 最小高度的图片的offsetLeft也是第二行的第一张图片的left // 显示出第一张图片后,改变最小列的高度 arr[index] = arr[index] + items[i].offsetHeight + gap } } } var _clientHeight = getClient().height // 懒加载(后端请求图片地址) var lazyload = function(){ // 视口高+滚动条高>最后一张图片的顶部位置top时,去请求新的图片地址 if (_clientHeight +getScrollTop() >= items[items.length-1].offsetTop){ // 请求后端,加载图片地址 var data = ["188.png", "270*270.jpg" ] // 将data里的图片地址插入dom,此处如果从优化的角度,可以用上createDocumentFragment一次性dom操作 // 100条数据以内,就不用分段渲染了,分段渲染一般用于动画(loop,requestAnimationFrame) const fragment = document.createDocumentFragment(); for (var n = 0; n<data.length;n++){ var div = document.createElement('div') div.className = 'item' div.innerHTML = '<img src="' + data[n] + '"alt="">' fragment.appendChild(div); } box.appendChild(fragment); waterFall() } } // 获取视口的 宽度或宽度 的兼容处理 function getClient(){ return { width: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth, height: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight } } // 获取滚动条的高度 function getScrollTop(){ return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop } // 节流防抖的优化 // 1)改getClient().height function computedClientHeight(){ _clientHeight = getClient().height } // 节流函数 function throttle(callback,delay){ let timeout return function(){ if (!timeout){ timeout = setTimeout(function(){ callback() timeout = null // 完成功能前,别的操作都不管,等到该定时器销毁,新操作才有效 },delay) } } } // 防抖函数 function debounce(callback,delay){ let timer return function(){ clearTimeout(timer) timer = setTimeout(function(){ callback() },delay) } } // 加载图片需要一定的时间,滚动 屏幕限制,节流函数 window.addEventListener('scroll',throttle(lazyload,100)) // 监听可视区域高度变化,防止频繁滚动屏幕多次 计算 ,防抖函数 window.addEventListener('resize',debounce(computedClientHeight,100)) } </script>