估计平时大伙儿使用 JS 框架写大型代码
比较多,效率是高;不过作为前端从业者,多掌握原生 JS 的能力对专业技能的培养是非常有益的。
本文收集了 6 个用简短的原生 JS 来实现一些常见的功能,既简单又高效,有些还意外地酷炫。
以下的代码片段都可以 “拿走即用”~,也可以根据自己的需要稍微修改。
1、下载功能
在网站上我们想要下载网站的内容,基本是靠浏览器自带的下载功能。那能否我们自定义下载的内容和行为呢?!
当然可以,而且也不难,不到 10 行代码就能搞定。
比如你想让用户一串 “Hello World” 的字符串(每当开始表演的时候,总少不了经典的 “Hello World”…),就可以这么写:
/** 将 Hello World 塞给 Blob 对象,同时设置 MIME 类型为 文本文件 **/
const blob = new Blob(["Hello World"], { type: 'text/plain' });
/** 根据 blob 内容创建对象 URL(不了解该知识点,就可以理解成类似音视频这样的 url ) **/
const url = window.URL.createObjectURL(blob);
/** 创建 a 表情 **/
const link = document.createElement('a');
/** 下载时显示的文件名 **/
link.download = '下载文件名';
/** 将对象 URL 赋值链接给 href 属性 **/
link.href = url;
/** 加载到文档末尾 **/
document.body.appendChild(link);
/** 模拟点击刚动态创建是 a 标签,触发浏览器下载动作 **/
link.click();
/** 此时 a 标签失去利用价值,从 body 中移除 **/
document.body.removeChild(link);
/** 同样 URL 对象也失去价值,从内存中释放~ **/
window.URL.revokeObjectURL(url);
这几行代码完成自定义下载功能了,注意下载的时候设置 Blob 的 MIME 类型,这样会给你下载的文件自动加上后缀,常用的有 ‘text/plain’(.txt 后缀)、‘application/json’(.json 后缀)、‘image/jpeg’(.jpg 后缀)等等
完整类型列表请前往:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
2、用 emoji 做为网站图标
一般我们网站图标都是通过 <link rel="icon" src='xxx'/>
来指定我们的网站图标。
使用 JS 后,我们可以做一点儿有意思的事儿,比如可以用 emoji 表情来做你网站的图标,先看一下效果:
首先,我们定义一个 setFavicon
函数,可以用指定的 url 来做当前网站的 favicon :
const setFavicon = function(url) {
/** 找到 favicon 元素,有些网站的图标的 rel 值是 'shortcut icon' **/
const favicon = document.querySelector('link[rel="shortcut icon"]') || document.querySelector('link[rel="icon"]');
if (favicon) {
/** 如果能找到元素,将其值更新入参 url **/
favicon.href = url;
} else {
/** 如果找不到元素,自己创建 favicon 的 link 元素 **/
const link = document.createElement('link');
link.rel = 'icon';
link.href = url;
/** 将创建 link 元素加到文档中 **/
document.head.appendChild(link);
}
};
现在如果你想动态更新你的网站图标,就能直接调用该方法,传入图片 url 去更新即可。
有了这个方法,我们就可以用 emoji 表情作为你网站的图标:
- 将 emoji 表情转换成 URL (使用 canvas 画布中转)
- 调用 setFavicon 方法即可
具体代码如下:
const emojiFavicon = function(emoji) {
/** 将创建 canvas 元素,尺寸 64x64 **/
const canvas = document.createElement('canvas');
canvas.height = 64;
canvas.width = 64;
/** 获取 canvas 元素的 context 对象 **/
const context = canvas.getContext('2d');
context.font = '64px serif';
/** 将 emoji 元素放到画布中 **/
context.fillText(emoji, 0, 64);
/** 调用 canvas.toDataURL 获取 base64 格式的 URL **/
const url = canvas.toDataURL();
/** 更新网站 favicon **/
setFavicon(url);
};
最终你前往网站,打开控制台 -> 粘贴上述代码 -> 调用 emojiFavicon(‘😂’) 就能达到本节开头 gif 所展示的效果了
3、只允许输入指定字符
很多场景下,我们只允许用户输入指定的字符,比如以下的输入框,用来保存手机号码只能要求输入数字和空格。
基础版
<input type="text" id="input" />
借助 input 事件,写个两三行代码就能实现:
/** 保留当前值 **/
const ele = document.getElementById('input');
let currentValue = ele.value || '';
ele.addEventListener('input', function(e) {
const target = e.target;
/** 如果用户输入可选字符(字符或者空格) **/
/^[0-9\s]*$/.test(target.value)
/** 就将值存储起来 **/
? currentValue = target.value
/** 否则还是保留原来的值 **/
: target.value = currentValue;
});
别小看这几行代码,除了用户常规的键盘输入,对于用户通过复制(Ctrl+V)、右键菜单或者将字符拖入输入框 都会进入上面的事件监听,阻止了一部分心思灵活用户的骚操作~
细节版
如果你不追求极致体验,到上面为止就行了。想要追求细节的读者,会发现上述代码会有一点瑕疵:当上述调用 target.value = currentValue
时,鼠标位置会总是放在输入框的末尾。
如果要追求完美,对光标位置得处理一下,两步走搞定~
第一步:保存用户当前光标位置
/** 该变量保存用户的当前光标位置 **/
const selection = {};
/** 监听 keydown 事件 **/
ele.addEventListener('keydown', function(e) {
const target = e.target;
selection = {
selectionStart: target.selectionStart,
selectionEnd: target.selectionEnd,
};
});
第二步:当恢复用户内容时,也同时恢复其光标位置
let currentValue = '';
ele.addEventListener('input', function(e) {
const target = e.target;
if (/^[0-9s]*$/.test(target.value)) {
currentValue = target.value;
} else {
/** 恢复原 input 内容 **/
target.value = currentValue;
/** 恢复光标位置 **/
target.setSelectionRange(
selection.selectionStart,
selection.selectionEnd
);
}
});
偷懒版
如果你觉得上述 JS 写起来比较烦,想怎么简单怎么来,那 HTML5 也帮你考虑到了,用特定的 type 的 input 输入框就行:
还有很多类型 type,就不一一列举,可以去 MDN 文档查阅:https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#%3Cinput%3E_types
4、粘贴预览图片
如果你想制作一个 图片上传 功能,为了提高用户体验,希望能从粘贴板中获取用户已经复制的图片,这样用户直接在你的界面上进行 Ctrl + V 进行预览操作:
核心代码如下(大概 10 行左右,不算注释啊…):
/** 关键是监听 'paste' 事件 **/
document.addEventListener('paste', function(evt) {
/** 获取粘贴板上的数据 **/
const clipboardItems = evt.clipboardData.items;
/** 过滤筛选获得图片列表 **/
const items = [].slice
.call(clipboardItems)
.filter(function(item) {
/** 过滤条件是 type 为 image **/
return item.type.indexOf('image') !== -1;
});
if (items.length === 0) {
return;
}
/** “弱水三千,只取一瓢”,咱们只用第一个 **/
const item = items[0];
/** 将图片数据转换成 blob 对象 **/
const blob = item.getAsFile();
/** 动态创建 image 标签(假设 id 为 preivew) **/
const imageEle = document.getElementById('preview');
/** 将图片 blob 对象转换成 URL 对象,赋值 **/
/** 打完!收工! **/
imageEle.src = URL.createObjectURL(blob);
});
几行代码代码就能获得良好的用户体验,一定会获得交互师、业务方的称赞,晚饭加个鸡腿犒劳一下自己~~(估计残酷的现实是设计师让你按设计稿左移几 px 像素…泪奔…)
5、按顺序动态加载 JS 文件
这其实是一个常问的面试题,“给你一个 js 路径数组,如何按顺序动态加载这些脚本”?
平时面试官还会基于这个命题分散出更多的面试题,比如 如何按优先级加载 js 文件?、如何同时并行 & 串行加载 js 文件?
等等,可自行发散思维
首先定义加载单个 js 文件的方法:
/** 根据给定的 url ,加载 js 文件 **/
const loadScript = function(url) {
/** 返回 promise 对象,当加载完毕后才会 resolve **/
return new Promise(function(resolve, reject) {
/** 动态创建 script 标签 **/
const script = document.createElement('script');
/** 给标签 src 属性赋值 **/
script.src = url;
/** 监听 load 事件 **/
script.addEventListener('load', function() {
/** 完毕后调用 resolve 方法 **/
resolve(true);
});
document.head.appendChild(script);
});
};
然后定义按顺序处理 promise 数组的方法:
/** 根据给定的 promise 数组,按顺序执行这些 promise **/
const waterfall = function(promises) {
/** 调用数组的 reduce 方法 **/
return promises.reduce(
function(p, c) {
/** 等前一个 promise 执行完 **/
return p.then(function() {
/** 然后执行当前 promise **/
return c().then(function(result) {
return true;
});
});
},
/** promise 初始值,立即执行 **/
Promise.resolve([])
);
};
上述的 loadScript 和 waterfall 方法,平时都可以单独拿来使用,也非常方便。
我们现在结合这两个方法,就能实现 按顺序动态加载 js 文件 的功能:
/** 按顺序动态加载 js 文件 **/
const loadScriptsInOrder = function(arrayOfJs) {
/** 将 string 数组转换成 promise 数组 **/
const promises = arrayOfJs.map(function(url) {
return loadScript(url);
});
/** 按顺序串接 promise 数组内元素 **/
return waterfall(promises);
};
举个调用例子:
loadScriptsInOrder([
'/path/to/file.js',
'/path/to/another-file.js',
'/yet/another/file.js',
]).then(function() {
/** 等上述 3 个 js 都加载完,再做一些操作 **/
})
代码看上去比较多,不过条理清晰,记忆起来不算费劲~
6、判断容器是否可滚动
这个需求比较少见,不过既然收集到了就罗列在这里,只用 4 行代码就能完成这项判断:
const isScrollable = function(ele) {
/** 对比元素的 scrollHeight 和 clientHeight 数值(如果容器不可滚动,这两个值相等) **/
const hasScrollableContent = ele.scrollHeight > ele.clientHeight;
/** 以上操作还不充分,还得判断 `overflow-y` 样式没有被用户设置成 `hidden` 或 `hidden !important` **/
const overflowYStyle = window.getComputedStyle(ele).overflowY;
const isOverflowHidden = overflowYStyle.indexOf('hidden') !== -1;
return hasScrollableContent && !isOverflowHidden;
};
这个方法我们可以做一下简单的扩展,就能实现 获取当前元素的首个可滚动父容器 能力,用递归方法:
const getFirstScrollableParent = function(ele) {
return (!ele || ele === document.body)
? document.body
: (isScrollable(ele) ? ele : getScrollableParent(ele.parentNode));
};
7、小结
掌握原生 JS 的优势在于,让你具备从 JS 框架的使用者转换成 JS 框架的制造者的基础,因此掌握牢固的 JS 原生基础知识,多多益善。