抖音话题发布实现
//jsonp
(function () {
const isPlainObject = function isPlainObject(obj) {
let proto, Ctor;
if (!obj || Object.prototype.toString.call(obj) !== "[object Object]") return false;
proto = Object.getPrototypeOf(obj);
if (!proto) return true;
Ctor = proto.hasOwnProperty('constructor') && proto.constructor;
return typeof Ctor === "function" && Ctor === Object;
};
const stringify = function stringify(obj) {
let keys = Reflect.ownKeys(obj),
str = ``;
keys.forEach(key => {
str += `&${key}=${obj[key]}`;
});
return str.substring(1);
};
const jsonp = function jsonp(url, options) {
if (typeof url !== "string") throw new TypeError('url is not a string~');
if (!isPlainObject(options)) options = {};
let { params, jsonpName } = Object.assign(
{
params: null,
jsonpName: 'callback'
},
options
);
return new Promise((resolve, reject) => {
let name = `jsonp${+new Date()}`,
script;
window[name] = function (data) {
resolve(data);
if (script) document.body.removeChild(script);
Reflect.deleteProperty(window, name);
};
if (params && isPlainObject(params)) {
params = stringify(params);
url += `${url.includes('?') ? '&' : '?'}${params}`;
}
url += `${url.includes('?') ? '&' : '?'}${jsonpName}=${name}`;
script = document.createElement('script');
script.src = url;
script.async = true;
script.onerror = () => reject();
document.body.appendChild(script);
});
};
/* 暴露API */
if (typeof module === 'object' && typeof module.exports === 'object') module.exports = jsonp;
if (typeof window !== 'undefined') window.jsonp = jsonp;
})();
//utils.js
(function () {
const clearTimer = function clearTimer(timer) {
if (timer) clearTimeout(timer);
return null;
};
// 函数防抖
const debounce = function debounce(func, wait, immediate) {
if (typeof func !== 'function') throw new TypeError('func is not a function~');
if (typeof wait === 'boolean') immediate = wait;
if (typeof wait !== 'number') wait = 300;
if (typeof immediate !== "boolean") immediate = false;
let timer = null;
return function operate(...params) {
let now = !timer && immediate,
result;
timer = clearTimer(timer);
timer = setTimeout(() => {
if (!immediate) func.call(this, ...params);
timer = clearTimer(timer);
}, wait);
if (now) result = func.call(this, ...params);
return result;
};
};
// 函数节流
const throttle = function throttle(func, wait) {
if (typeof func !== 'function') throw new TypeError('func is not a function~');
if (typeof wait !== 'number') wait = 300;
let timer = null,
previous = 0;
return function operate(...params) {
let now = +new Date(),
remaining = wait - (now - previous),
result;
if (remaining <= 0) {
result = func.call(this, ...params);
previous = +new Date();
timer = clearTimer(timer);
} else if (!timer) {
timer = setTimeout(() => {
func.call(this, ...params);
previous = +new Date();
timer = clearTimer(timer);
}, remaining);
}
return result;
};
};
let utils = {
debounce,
throttle
};
/* 导出模块 */
if (typeof module === "object" && typeof module.exports === "object") module.exports = utils;
if (typeof window !== 'undefined') window.utils = utils;
})();
//index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>面试</title>
<!-- IMPORT CSS -->
<link rel="stylesheet" href="reset.min.css">
<style>
html,
body {
height: 100%;
font-size: 14px;
}
/* 文本框 */
.box {
position: relative;
box-sizing: border-box;
margin: 0 auto;
width: 300px;
}
.inpText {
display: block;
box-sizing: border-box;
padding: 5px;
width: 100%;
height: 60px;
}
/* 模糊匹配 */
.search {
display: none;
position: absolute;
top: 60px;
box-sizing: border-box;
width: 100%;
background: rgb(252, 238, 240);
}
.search li {
padding: 0 5px;
line-height: 30px;
cursor: pointer;
}
.search li:hover {
background: rgb(221, 239, 250);
}
/* 按钮 */
.handle {
padding: 5px 0;
}
.handle button {
padding: 5px 10px;
border: none;
background: lightskyblue;
cursor: pointer;
}
.handle button.upload {
background: lightpink;
}
</style>
</head>
<body>
<div class="box">
<textarea class="inpText"></textarea>
<ul class="search"></ul>
<div class="handle">
<button class="talk">#话题</button>
<button class="upload">上传</button>
</div>
</div>
<!-- IMPORT JS -->
<script src="utils.js"></script>
<script src="jsonp.js"></script>
<script>
(function () {
const inpText = document.querySelector('.inpText'),
search = document.querySelector('.search'),
talk = document.querySelector('.talk'),
upload = document.querySelector('.upload');
const { debounce, throttle } = utils,
reg = /#([^#\s]*)/;
let prev = '';
/* 从百度服务器,根据话题关键词获取匹配结果 */
const queryData = async function queryData(match) {
// 从百度获取模糊匹配的数据
try {
let { g: arr } = await jsonp('https://www.baidu.com/sugrec', {
params: {
prod: 'pc',
wd: match
},
jsonpName: 'cb'
});
if (arr && arr.length > 0) {
// 有匹配的结果
search.style.display = 'block';
let str = ``;
arr.forEach((item, index) => {
if (index < 5 && item.q) {
str += `<li>${item.q}</li>`;
}
});
search.innerHTML = str;
return;
}
} catch (_) { }
// 请求失败或者没有匹配结果
search.innerHTML = '';
search.style.display = 'none';
};
/* 基于事件委托实现匹配结果的输入 */
document.addEventListener('click', function (ev) {
let target = ev.target,
targetTag = target.tagName,
parent = target.parentNode;
if (targetTag === "LI" && parent === search) {
// 点击的是搜索出来的LI:把选中的内容插入到文本域中话题位置
inpText.value = inpText.value.replace(reg, `#${target.innerHTML} `);
prev = target.innerHTML;
search.innerHTML = '';
search.style.display = 'none';
return;
}
if (targetTag === 'TEXTAREA' && target === inpText) {
// 点击的是文本域:啥都不处理
return;
}
// 剩下情况都是让搜索区域消失
search.innerHTML = '';
search.style.display = 'none';
});
/* 文本框输入中 */
inpText.addEventListener('input', throttle(function () {
let val = inpText.value,
match = reg.exec(val);
match = match ? match[1] : '';
if (match !== '' && match !== prev) {
// 用户输入了话题
search.style.display = 'block';
prev = match;
queryData(match);
return;
}
// 用户没有输入话题
search.style.display = 'none';
}));
/* 插入话题 */
talk.addEventListener('click', debounce(function () {
let val = inpText.value,
n,
m;
if (reg.test(val)) {
alert('当前已经插入过相关话题了~');
return;
}
n = inpText.selectionStart;
m = inpText.selectionEnd;
inpText.value = `${val.slice(0, n)}# ${val.slice(m)}`;
inpText.focus();
inpText.setSelectionRange(n + 1, m + 1);
}));
/* 话题解析 */
upload.addEventListener('click', debounce(function () {
let val = inpText.value.trim(),
match = reg.exec(val),
res = [];
match = match ? match[0] : '';
if (val.length === 0) {
alert('请先输入内容~');
return;
}
if (match === '') {
// 没有发布话题
res.push({
type: 'text',
value: val.trim()
});
} else {
// 有发布话题
let [$left, $right] = val.split(match);
$left = $left.trim();
$right = $right.trim();
if ($left) {
res.push({
type: 'text',
value: $left
});
}
res.push({
type: 'talk',
value: match
});
if ($right) {
res.push({
type: 'text',
value: $right
});
}
}
alert(JSON.stringify(res));
}));
})();
</script>
</body>
</html>