不失逼格地使用原生 JS

估计平时大伙儿使用 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 原生基础知识,多多益善。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章