JowayYoung

JowayYoung 查看完整档案

广州编辑广东医学院  |  医学信息 心理学 编辑网易  |  全栈前端 编辑 juejin.im/user/584ec3a661ff4b006cd6383e/posts 编辑
编辑

谢谢曾经努力的自己,欢迎关注公众号『 IQ前端 』,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔

个人动态

JowayYoung 发布了文章 · 8月10日

嗯,手搓一个TinyPng压缩图片的WebpackPlugin也SoEasy啦

前言

曾经发表过一篇性能优化的文章《前端性能优化指南》,笔者总结了一些在项目开发过程中使用过的性能优化经验。说句真话,性能优化可能在面试过程中会有用,实际在项目开发过程中可能没几个同学会注意这些性能优化的细节。

若经常关注性能优化的话题,可能会发现无论怎样对代码做最好的优化也不及对一张图片做一次压缩好。所以压缩图片成了性能优化里最常见的操作,不管是手动压缩图片还是自动压缩图片,在项目开发过程中必须得有。

自动压缩图片通常在webpack构建项目时接入一些第三方Loader&Plugin来处理。打开Github,搜素webpack image等关键字,Star最多还是image-webpack-loaderimagemin-webpack-plugin这两个Loader&Plugin。很多同学可能都会选择它们,方便快捷,简单易用,无脑接入。

可是,这两个Loader&Plugin存在一些特别问题,它们都是基于imagemin开发的。imagemin的某些依赖托管在国外服务器,在npm i xxx安装它们时会默认走GitHub Releases的托管地址,若不是规范上网,你们是不可能安装得上的,即使规范上网也不一定安装得上。所以笔者又刨根到底发表了一篇关于NPM镜像处理的文章《聊聊NPM镜像那些险象环生的坑》,专门解决这些因为网络环境而导致安装失败的问题。除了这个安装问题,imagemin还存在另一个大问题,就是压缩质感损失得比较严重,图片体积越大越明显,压缩出来的图片总有几张是失真的,而且总体压缩率不是很高。这样在交付项目时有可能被细心的QA小姐姐抓个正着,怎么和设计图对比起来不清晰啊!

工具

图片压缩工具

此时可能有些同学已转战到手动压缩图片了。比较好用的图片压缩工具无非就是以下几个,若有更好用的工具麻烦在评论里补充喔!同时笔者也整理出它们的区别,供各位同学参考。

工具集合

工具开源收费API免费体验
QuickPicture✖️✔️✖️可压缩类型较多,压缩质感较好,有体积限制,有数量限制
ShrinkMe✖️✖️✖️可压缩类型较多,压缩质感一般,无数量限制,有体积限制
Squoosh✔️✖️✔️可压缩类型较少,压缩质感一般,无数量限制,有体积限制
TinyJpg✖️✔️✔️可压缩类型较少,压缩质感很好,有数量限制,有体积限制
TinyPng✖️✔️✔️可压缩类型较少,压缩质感很好,有数量限制,有体积限制
Zhitu✖️✖️✖️可压缩类型一般,压缩质感一般,有数量限制,有体积限制

从上述表格对比可看出,免费体验都会存在体积限制,这个可理解,即使收费也一样,毕竟每个人都上传单张10多M的图片,哪个服务器能受得了。再来就是数量限制,一次只能上传20张,好像有个规律,压缩质感好就限制数量,否则就不限制数量,当然收费后就没有限制了。再来就是可压缩类型,图片类型一般是jpgpnggifsvgwebpgif压缩后一般都会失真,svg通常用在矢量图标上很少用在场景图片上,webp由于兼容性问题很少被使用,故能压缩jpgpng就足够了。当然压缩质感是最优考虑,综上所述,大部分同学都会选择TinyJpgTinyPng,其实它俩就是兄弟,出自同一厂商。

在笔者公众号的微信讨论群里发起了一个简单的投票,最终还是TinyJpgTinyPng胜出。

工具投票

TinyJpg/TinyPng存在问题
  • 上传下载全靠手动
  • 只能压缩jpgpng
  • 每次只能压缩20
  • 每张体积最大不能超过5M
  • 可视化处理信息不是特别齐全
TinyJpg/TinyPng压缩原理

TinyJpg/TinyPng使用智能有损压缩技术将图片体积降低,选择性地减少图片中相似颜色,只需很少字节就能保存数据。对视觉影响几乎不可见,但是在文件体积上就有很大的差别。而使用到智能有损压缩技术被称为量化

TinyJpg/TinyPng在压缩png文件时效果更显著。扫描图片中相似颜色并将其合并,通过减少颜色数量将24位png文件转换成体积更小的8位png文件,丢弃所有不必要的元数据。

大部分png文件都有50%~70%的压缩率,即使视力再好也很难区分出来。使用优化过的图片可减少带宽流量和加载时间,整个网站使用到的图片经TinyJpg/TinyPng压缩一遍,其成效是再多的代码优化也无法追赶得上的。

熊猫

TinyJpg/TinyPng开发API

查阅相关资料,发现TinyJpg/TinyPng暂时还未开源其压缩算法,不过提供了适合开发者使用的API。有兴趣的同学可到其开发API文档瞧瞧。

Node方面,TinyJpg/TinyPng官方提供了tinify作为压缩图片的核心JS库,使用很简单,看文档吧。可是换成开发API还是逃不过收费,你是想包月呢还是免费呢,想免费的话就继续往下看,土豪随意!

图片压缩工具

实现

笔者也是经常使用TinyJpg/TinyPng的程序猿,收费,那是不可能的😂。寻找突破口,解决问题,是作为一位程序猿最基本的素养。我们需明确什么问题,需解决什么问题

分析

从上述得知,只需对TinyJpg/TinyPng原有功能改造成以下功能。

  • 上传下载全自动
  • 可压缩jpgpng
  • 没有数量限制
  • 存在体积限制,最大体积不能超过5M
  • 压缩成功与否输出详细信息
自动处理

对于前端开发者来说,这种无脑的上传下载操作必须得自动化,省事省心省力。但是这个操作得结合webpack来处理,到底是开发成Loader还是Plugin,后面再分析。不过细心的同学看标题就知道用什么方式处理了。

压缩类型

gif压缩后一般都会失真,svg通常用在矢量图标上很少用在场景图片上,webp由于兼容性问题很少被使用,故能压缩jpgpng就足够了。在过滤图片时,使用path模块判断文件类型是否为jpgpng,是则继续处理,否则不处理。

数量限制

数量限制当然是不能存在的,万一项目里超过20张图片,那不是得分批处理,这个不能有。对于这种无需登录状态就能处理一些用户文件的网站,通常都会通过IP来限制用户的操作次数。有些同学可能会说,刷新页面不就行了吗,每次压缩20张图片,再刷新再压缩,万一有500张图片呢,你就刷新25次吗,这样很好玩是吧!

由于大多数Web架构很少会将应用服务器直接对外提供服务,一般都会设置一层Nginx作为代理和负载均衡,有的甚至可能有多层代理。鉴于大多数Web架构都是使用Nginx作为反向代理,用户请求不是直接请求应用服务器的,而是通过Nginx设置的统一接入层将用户请求转发到服务器的,所以可通过设置HTTP请求头字段X-Forwarded-For来伪造IP。

X-Forwarded-For指用来识别通过代理负载均衡的方式连接到Web服务器的客户端最原始的IP地址的HTTP请求头字段。当然,这个IP也不是一成不变的,每次请求都需随机更换IP,骗过应用服务器。若应用服务器增加了伪造IP识别,那可能就无法继续使用随机IP了。

体积限制

体积限制这个能理解,也没必要搞一张那么大的图片,多浪费带宽流量和加载时间啊。在上传图片时,使用fs模块判断文件体积是否超过5M,是则不上传,否则继续上传。当然,交给TinyJpg/TinyPng接口判断也行。

输出信息

压缩成功与否得让别人知道,输出原始大小、压缩大小、压缩率和错误提示等,让别人清楚这些处理信息。

编码

通过上述抽丝剥茧的分析,那么就开始着手编码了。

随机生成HTTP请求头

既然可通过X-Forwarded-For来伪造IP,那么得有一个随机生成HTTP请求头字段的函数,每次请求接口时都随机生成相关的请求头字段。打开tinyjpg.comtinypng.com上传一张图片,通过Chrome DevTools分析Network发现其请求接口是web/shrink。另外每次请求也不要集中在单一的hostname上,随机派发到tinyjpg.comtinypng.com上会更好。通过封装RandomHeader函数随机生成请求头信息,后续使用https模块RandomHeader()生成的配置作为入参进行请求。

trample是笔者开发的一个Web/Node通用函数工具库,包含常规的工具函数,助你少写更多通用代码。详情请查看文档,顺便给一个Star以作鼓励。

工具接口

const { RandomNum } = require("trample/node");

const TINYIMG_URL = [
    "tinyjpg.com",
    "tinypng.com"
];

function RandomHeader() {
    const ip = new Array(4).fill(0).map(() => parseInt(Math.random() * 255)).join(".");
    const index = RandomNum(0, 1);
    return {
        headers: {
            "Cache-Control": "no-cache",
            "Content-Type": "application/x-www-form-urlencoded",
            "Postman-Token": Date.now(),
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
            "X-Forwarded-For": ip
        },
        hostname: TINYIMG_URL[index],
        method: "POST",
        path: "/web/shrink",
        rejectUnauthorized: false
    };
}
上传图片与下载图片

使用Promise封装上传图片下载图片的函数,方便后续使用Async/Await同步化异步代码。以下函数的具体断点调试就不说了,有兴趣的同学自行调试函数的入参和出参哈!

const Https = require("https");
const Url = require("url");

function UploadImg(file) {
    const opts = RandomHeader();
    return new Promise((resolve, reject) => {
        const req = Https.request(opts, res => res.on("data", data => {
            const obj = JSON.parse(data.toString());
            obj.error ? reject(obj.message) : resolve(obj);
        }));
        req.write(file, "binary");
        req.on("error", e => reject(e));
        req.end();
    });
}

function DownloadImg(url) {
    const opts = new Url.URL(url);
    return new Promise((resolve, reject) => {
        const req = Https.request(opts, res => {
            let file = "";
            res.setEncoding("binary");
            res.on("data", chunk => file += chunk);
            res.on("end", () => resolve(file));
        });
        req.on("error", e => reject(e));
        req.end();
    });
}
压缩图片

通过上传图片函数获取压缩后的图片信息,再依据图片信息通过下载图片函数生成本地文件。

const Fs = require("fs");
const Path = require("path");
const Chalk = require("chalk");
const Figures = require("figures");
const { ByteSize, RoundNum } = require("trample/node");

async function CompressImg(path) {
    try {
        const file = Fs.readFileSync(path, "binary");
        const obj = await UploadImg(file);
        const data = await DownloadImg(obj.output.url);
        const oldSize = Chalk.redBright(ByteSize(obj.input.size));
        const newSize = Chalk.greenBright(ByteSize(obj.output.size));
        const ratio = Chalk.blueBright(RoundNum(1 - obj.output.ratio, 2, true));
        const dpath = Path.join("img", Path.basename(path));
        const msg = `${Figures.tick} Compressed [${Chalk.yellowBright(path)}] completed: Old Size ${oldSize}, New Size ${newSize}, Optimization Ratio ${ratio}`;
        Fs.writeFileSync(dpath, data, "binary");
        return Promise.resolve(msg);
    } catch (err) {
        const msg = `${Figures.cross} Compressed [${Chalk.yellowBright(path)}] failed: ${Chalk.redBright(err)}`;
        return Promise.resolve(msg);
    }
}
压缩目标图片

完成上述步骤对应的函数后,就能自由压缩图片了,以下使用一张图片作为演示。

const Ora = require("ora");

(async() => {
    const spinner = Ora("Image is compressing......").start();
    const res = await CompressImg("src/pig.png");
    spinner.stop();
    console.log(res);
})();

你看,压缩完后笨猪都变帅猪了,能电眼的猪都是好猪。源码请查看compress-img

电眼猪

若压缩指定文件夹里符合条件的所有图片,可通过fs模块获取图片并使用map()将单个图片路径映射为CompressImg(path),再通过Promise.all()操作即可。在这里就不贴代码了,当作思考题,自行完成。

将上述压缩图片的功能封装成Loader还是Plugin呢?接下来会一步一步分析。

Loader&Plugin

webpack是一个前端资源打包工具,它根据模块依赖关系进行静态分析,然后将这些模块按照指定规则生成对应的静态资源。

网上一大堆webpack教程,笔者就不再花大篇幅啰嗦了,相信各位同学都是一位标准的Webpack配置工程师。以下简单回顾一次webpack的组成、构建机制和构建流程,相信也能从这些知识点中定位出LoaderPluginWebpack构建流程中是处于一个什么样的角色地位。

本文所说的webpack都是基于webpack v4
组成
  • Entry:入口
  • Output:输出
  • Loader:转换器
  • Plugin:扩展器
  • Mode:模式
  • Module:模块
  • Target:目标
构建机制
  • 通过Babel转换代码并生成单个文件依赖
  • 从入口文件开始递归分析并生成依赖图谱
  • 将各个引用模块打包成一个立即执行函数
  • 将最终bundle文件写入bundle.js
构建流程
  • 初始

    • 初始参数:合并命令行和配置文件的参数
  • 编译

    • 执行编译:依据参数初始Compiler对象,加载所有Plugin,执行run()
    • 确定入口:依据配置文件找出所有入口文件
    • 编译模块:依据入口文件找出所有依赖模块关系,调用所有Loader进行转换
    • 生成图谱:得到每个模块转换后的内容及其之间的依赖关系
  • 输出

    • 输出资源:依据依赖关系将模块组装成块再组装成包(module → chunk → bundle)
    • 生成文件:依据配置文件将确认输出的内容写入文件系统
Loader

Loader用于转换模块源码,笔者将其翻译为转换器Loader可将所有类型文件转换为webpack能够处理的有效模块,然后利用webpack的打包能力对它们进行二次处理。

Loader具有以下特点:

  • 单一职责原则(只完成一种转换)
  • 转换接收内容
  • 返回转换结果
  • 支持链式调用

Loader将所有类型文件转换为应用程序的依赖图谱可直接引用的模块,所以Loader可用于编译一些文件,例如pug → htmlsass → cssless → csses5 → es6ts → js等。

处理一个文件可使用多个LoaderLoader的执行顺序和配置顺序是相反的,即末尾Loader最先执行,开头Loader最后执行。最先执行的Loader接收源文件内容作为参数,其它Loader接收前一个执行的Loader的返回值作为参数,最后执行的Loader会返回该文件的转换结果。一句话概括:富土康流水线厂工

Loader开发思路总结如下:

  • 通过module.exports导出一个函数
  • 函数第一默认参数为source(源文件内容)
  • 在函数体中处理资源(可引入第三方模块扩展功能)
  • 通过return返回最终转换结果(字符串形式)
编写Loader时要遵循单一职责原则,每个Loader只做一种转换工作
Plugin

Plugin用于扩展执行范围更广的任务,笔者将其翻译为扩展器Plugin的范围很广,在Webpack构建流程里从开始到结束都能找到时机作为插入点,只要你想不到没有你做不到。所以笔者认为Plugin的功能比Loader更加强大。

Plugin具有以下特点:

  • 监听webpack运行生命周期中广播的事件
  • 在合适时机通过webpack提供的API改变输出结果
  • webpack的Tapable事件流机制保证Plugin的有序性

webpack运行生命周期中会广播出许多事件,Plugin可监听这些事件并在合适时机通过webpack提供的API改变输出结果。在webpack启动后,在读取配置过程中执行new MyPlugin(opts)初始化自定义Plugin获取其实例,在初始化Compiler对象后,通过compiler.hooks.event.tap(PLUGIN_NAME, callback)监听webpack广播事件,当捕抓到指定事件后,会通过Compilation对象操作相关业务逻辑。一句话概括:自己看着办

Plugin开发思路总结如下:

  • 通过module.exports导出一个函数或类
  • 函数原型或类上绑定apply()访问Compiler对象
  • apply()中指定一个绑定到webpack自身的事件钩子
  • 在事件钩子中通过webpack提供的API处理资源(可引入第三方模块扩展功能)
  • 通过webpack提供的方法返回该资源
传给每个Plugin的Compiler和Compilation都是同一个引用,若修改它们身上的属性会影响后面的Plugin,所以需谨慎操作
Loader/Plugin区别
  • 本质

    • Loader本质是一个函数,转换接收内容,返回转换结果
    • Plugin本质是一个类,监听webpack运行生命周期中广播的事件,在合适时机通过webpack提供的API改变输出结果
  • 配置

    • Loadermodule.rule中配置,类型是数组,每一项对应一个模块解析规则
    • Pluginplugin中配置,类型是数组,每一项对应一个扩展器实例,参数通过构造函数传入

封装

分析

从上述可知LoaderPlugin在角色定位和执行机制上有很多不一样,到底如何选择呢?各有各好,当然还是需分析后进行选择。

Loaderwebpack中扮演着转换器的角色,用于转换模块源码,简单理解就是将文件转换成另外形式的文件,而本文主题是压缩图片jpg压缩后还是jpgpng压缩后还是png,在文件类型上来说还是没有变化。Loader的转换过程是附属在整个Webpack构建流程中的,意味着打包时间包含了压缩图片的时间成本,对于追求webpack性能优化来说实属有点违背原则。而Plugin恰好是监听webpack运行生命周期中广播的事件,在合适时机通过webpack提供的API改变输出结果,所以可在整个Webpack构建流程完成后(全部打包文件输出完成后)插入压缩图片的操作。换句话说,打包时间不再包含压缩图片的时间成本,打包完成后该干嘛就干嘛,还能干嘛,压缩图片啊。

所以依据需求情况,Plugin作为首选。

编码

依据上述Plugin开发思路,那么就开始着手编码了。

笔者把这个压缩图片的Plugin命名为tinyimg-webpack-plugintinyimg意味着TinyJpgTinyPng合体。

新建项目,目录结构如下。

tinyimg-webpack-plugin
├─ src
│  ├─ index.js
│  ├─ schema.json
├─ util
│  ├─ getting.js
│  ├─ setting.js
├─ .gitignore
├─ .npmignore
├─ license
├─ package.json
├─ readme.md

主要文件如下。

  • src

    • index.js:入口函数
    • schema.json:参数校验
  • util

    • getting.js:常量集合
    • setting.js:函数集合

安装项目所需模块,和上述compress-img的依赖一致,额外安装schema-utils用于校验Plugin参数是否符合规定。

npm i chalk figures ora schema-utils trample
封装常量集合和函数集合

将上述compress-imgTINYIMG_URLRandomHeader()封装到工具集合中,其中常量集合增加IMG_REGEXPPLUGIN_NAME两个常量。

// getting.js
const IMG_REGEXP = /\.(jpe?g|png)$/;

const PLUGIN_NAME = "tinyimg-webpack-plugin";

const TINYIMG_URL = [
    "tinyjpg.com",
    "tinypng.com"
];

module.exports = {
    IMG_REGEXP,
    PLUGIN_NAME,
    TINYIMG_URL
};
// setting.js
const { RandomNum } = require("trample/node");

const { TINYIMG_URL } = require("./getting");

function RandomHeader() {
    const ip = new Array(4).fill(0).map(() => parseInt(Math.random() * 255)).join(".");
    const index = RandomNum(0, 1);
    return {
        headers: {
            "Cache-Control": "no-cache",
            "Content-Type": "application/x-www-form-urlencoded",
            "Postman-Token": Date.now(),
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36",
            "X-Forwarded-For": ip
        },
        hostname: TINYIMG_URL[index],
        method: "POST",
        path: "/web/shrink",
        rejectUnauthorized: false
    };
}

module.exports = {
    RandomHeader
};
通过module.exports导出一个函数或类
// index.js
module.exports = class TinyimgWebpackPlugin {};
函数原型或类上绑定apply()访问Compiler对象
// index.js
module.exports = class TinyimgWebpackPlugin {
    apply(compiler) {
        // Do Something
    }
};
apply()中指定一个绑定到webpack自身的事件钩子

从上述分析中可知,在全部打包文件输出完成后插入压缩图片的操作,所以应该选择该时机对应的事件钩子。从Webpack Compiler Hooks API文档中可发现,emit正是这个Plugin所需的事件钩子。emit生成资源到输出目录前执行,此刻可获取所有图片文件的数据和输出路径。

为了方便在特定条件下启用功能打印日志,所以设置相关配置。

  • enabled:是否启用功能
  • logged:是否打印日志

apply()中处理相关业务逻辑,可能使用到Plugin的入参,那么就得对参数进行校验。定义一个PluginSchema,通过schema-utils来校验Plugin的入参。

// schema.json
{
    "type": "object",
    "properties": {
        "enabled": {
            "description": "start plugin",
            "type": "boolean"
        },
        "logged": {
            "description": "print log",
            "type": "boolean"
        }
    },
    "additionalProperties": false
}
// index.js
const SchemaUtils = require("schema-utils");

const { PLUGIN_NAME } = require("../util/getting");
const Schema = require("./schema");

module.exports = class TinyimgWebpackPlugin {
    constructor(opts) {
        this.opts = opts;
    }
    apply(compiler) {
        const { enabled } = this.opts;
        SchemaUtils(Schema, this.opts, { name: PLUGIN_NAME });
        enabled && compiler.hooks.emit.tap(PLUGIN_NAME, compilation => {
            // Do Something
        });
    }
};
整合compress-imgPlugin

在整合过程中会有一些小修改,各位同学可对比看看哪些细节发生了变化。

// index.js
const Fs = require("fs");
const Https = require("https");
const Url = require("url");
const Chalk = require("chalk");
const Figures = require("figures");
const { ByteSize, RoundNum } = require("trample/node");

const { RandomHeader } = require("../util/setting");

module.exports = class TinyimgWebpackPlugin {
    constructor(opts) { ... }
    apply(compiler) { ... }
    async compressImg(assets, path) {
        try {
            const file = assets[path].source();
            const obj = await this.uploadImg(file);
            const data = await this.downloadImg(obj.output.url);
            const oldSize = Chalk.redBright(ByteSize(obj.input.size));
            const newSize = Chalk.greenBright(ByteSize(obj.output.size));
            const ratio = Chalk.blueBright(RoundNum(1 - obj.output.ratio, 2, true));
            const dpath = assets[path].existsAt;
            const msg = `${Figures.tick} Compressed [${Chalk.yellowBright(path)}] completed: Old Size ${oldSize}, New Size ${newSize}, Optimization Ratio ${ratio}`;
            Fs.writeFileSync(dpath, data, "binary");
            return Promise.resolve(msg);
        } catch (err) {
            const msg = `${Figures.cross} Compressed [${Chalk.yellowBright(path)}] failed: ${Chalk.redBright(err)}`;
            return Promise.resolve(msg);
        }
    }
    downloadImg(url) {
        const opts = new Url.URL(url);
        return new Promise((resolve, reject) => {
            const req = Https.request(opts, res => {
                let file = "";
                res.setEncoding("binary");
                res.on("data", chunk => file += chunk);
                res.on("end", () => resolve(file));
            });
            req.on("error", e => reject(e));
            req.end();
        });
    }
    uploadImg(file) {
        const opts = RandomHeader();
        return new Promise((resolve, reject) => {
            const req = Https.request(opts, res => res.on("data", data => {
                const obj = JSON.parse(data.toString());
                obj.error ? reject(obj.message) : resolve(obj);
            }));
            req.write(file, "binary");
            req.on("error", e => reject(e));
            req.end();
        });
    }
};
在事件钩子中通过webpack提供的API处理资源

通过compilation.assets获取全部打包文件的对象,筛选出jpgpng,使用map()将单个图片数据映射为this.compressImg(file),再通过Promise.all()操作即可。

整个业务逻辑结合了PromiseAsync/Await两个ES6常用特性,它俩组合起来玩异步编程极其有趣,关于它俩更多细节可查看笔者这篇4000点赞量14万阅读量的文章《1.5万字概括ES6全部特性》

// index.js
const Ora = require("ora");
const SchemaUtils = require("schema-utils");

const { IMG_REGEXP, PLUGIN_NAME } = require("../util/getting");
const Schema = require("./schema");

module.exports = class TinyimgWebpackPlugin {
    constructor(opts) { ... }
    apply(compiler) {
        const { enabled, logged } = this.opts;
        SchemaUtils(Schema, this.opts, { name: PLUGIN_NAME });
        enabled && compiler.hooks.emit.tap(PLUGIN_NAME, compilation => {
            const imgs = Object.keys(compilation.assets).filter(v => IMG_REGEXP.test(v));
            if (!imgs.length) return Promise.resolve();
            const promises = imgs.map(v => this.compressImg(compilation.assets, v));
            const spinner = Ora("Image is compressing......").start();
            return Promise.all(promises).then(res => {
                spinner.stop();
                logged && res.forEach(v => console.log(v));
            });
        });
    }
    async compressImg(assets, path) { ... }
    downloadImg(url) { ... }
    uploadImg(file) { ... }
};
通过webpack提供的方法返回该资源

由于压缩图片的操作是在整个Webpack构建流程完成后,所以没有什么可返回了,故不作处理。

控制webpack依赖版本

由于tinyimg-webpack-plugin基于webpack v4,所以需在package.json中添加peerDependencies,用来告知安装该Plugin的模块必须存在peerDependencies里的依赖。

{
    "peerDependencies": {
        "webpack": ">= 4.0.0",
        "webpack-cli": ">= 3.0.0"
    }
}
总结

按照上述总结的开发思路一步一步来完成编码,其实是挺简单的。若需开发一些跟自己项目相关的Plugin,还是需多多熟悉Webpack Compiler Hooks API文档,相信各位同学都能手戳一个完美的Plugin出来。

tinyimg-webpack-plugin源码请戳这里查看,Star一个如何,嘻嘻。

电眼猪

测试

整个Plugin开发完成,接下来需走一遍测试流程,看能不能把这个压缩图片的扩展器跑通。相信各位同学都是一位标准的Webpack配置工程师,可自行编写测试Demo验证你们的Plugin

在根目录下创建test文件夹,并按照以下目录结构加入文件。

tinyimg-webpack-plugin
├─ test
│  ├─ src
│  │  ├─ img
│  │  │  ├─ favicon.ico
│  │  │  ├─ gz.jpg
│  │  │  ├─ pig-1.jpg
│  │  │  ├─ pig-2.jpg
│  │  │  ├─ pig-3.jpg
│  │  ├─ index.html
│  │  ├─ index.js
│  │  ├─ index.scss
│  │  ├─ reset.css
│  └─ webpack.config.js

安装测试Demo所需的webpack相关配置模块。

npm i -D @babel/core @babel/preset-env babel-loader clean-webpack-plugin css-loader file-loader html-webpack-plugin mini-css-extract-plugin node-sass sass sass-loader style-loader url-loader webpack webpack-cli webpackbar

安装完成后,着手完善webpack.config.js代码,代码量有点多,直接贴链接好了,请戳这里

最后在package.json中的scripts插入以下npm scripts,然后执行npm run test调试测试Demo。

{
    "scripts": {
        "test": "webpack --config test/webpack.config.js"
    }
}

发布

发布到NPM仓库上非常简单,仅需几行命令。若还没注册,赶紧去NPM上注册一个账号。若当前镜像为淘宝镜像,需执行npm config set registry https://registry.npmjs.org/切换回源镜像。

接下来一波操作就可完成发布了。

  • 进入目录:cd my-plugin
  • 登录账号:npm login
  • 校验状态:npm whoami
  • 发布模块:npm publish
  • 退出账号:npm logout

若不想牢记这么多命令,可用笔者开发的pkg-master一键发布,若存在某些错误会立马中断发布并提示错误信息,是一个非常好用的集成创建和发布的NPM模块管理工具。详情请查看文档,顺便给一个Star以作鼓励。

安装

npm i -g pkg-master

使用
命令缩写功能描述
pkg-master createpkg-master c创建模块生成模块的基础文件
pkg-master publishpkg-master p发布模块检测NPM的运行环境账号状态,通过则自动发布模块

发布模块

接入

安装

npm i tinyimg-webpack-plugin

使用
配置功能格式描述
enabled是否启用功能true/false建议只在生产环境下开启
logged是否打印日志true/false打印处理信息

webpack.config.jswebpack配置插入以下代码。

在CommonJS中使用
const TinyimgPlugin = require("tinyimg-webpack-plugin");

module.exports = {
    plugins: [
        new TinyimgPlugin({
            enabled: process.env.NODE_ENV === "production",
            logged: true
        })
    ]
};
在ESM中使用

必须在babel加持下的Node环境中使用

import TinyimgPlugin from "tinyimg-webpack-plugin";

export default {
    plugins: [
        new TinyimgPlugin({
            enabled: process.env.NODE_ENV === "production",
            logged: true
        })
    ]
};
推荐一个零配置开箱即用的React/Vue应用自动化构建脚手架

bruce-cli是一个React/Vue应用自动化构建脚手架,其零配置开箱即用的优点非常适合入门级、初中级、快速开发项目的前端同学使用,还可通过创建brucerc.js文件来覆盖其默认配置,只需专注业务代码的编写无需关注构建代码的编写,让项目结构更简洁。使用时记得查看文档哟,喜欢的话给个Star。

当然,笔者已将tinyimg-webpack-plugin集成到bruce-cli中,零配置开箱即用走起。

总结

总体来说开发一个Webpack Plugin不难,只需好好分析需求,了解webpack运行生命周期中广播的事件,编写自定义Plugin在合适时机通过webpack提供的API改变输出结果。

若觉得tinyimg-webpack-plugin对你有帮助,可在Issue提出你的宝贵建议,笔者会认真阅读并整合你的建议。喜欢tinyimg-webpack-plugin的请给一个Star,或Fork本项目到自己的Github上,根据自身需求定制功能。

查看原文

赞 19 收藏 12 评论 1

JowayYoung 发布了文章 · 6月9日

聊聊NPM镜像那些险象环生的坑

作者:JowayYoung
仓库:GithubCodePen
博客:掘金思否知乎简书头条CSDN
公众号:IQ前端
联系我:关注公众号后有我的微信
特别声明:原创不易,未经授权不得对此文章进行转载或抄袭,否则按侵权处理,如需转载或开通公众号白名单可联系我,希望各位尊重原创的知识产权

前言

由于国内网络环境的原因,在执行npm i安装项目依赖过程中,肯定会遇上安装过慢安装失败的情况。有经验的同学通常会在安装完Node时顺便把NPM镜像设置成国内的淘宝镜像。

npm config set registry https://registry.npm.taobao.org/

这样就能爽歪歪应付大部分npm i的安装情况了。当然,这只是解决了大部分的安装过慢安装失败的情况,随着项目的深入开发,肯定还会遇上一些比较奇葩的情况,这也是笔者为什么要写本文的原因。

管理镜像

你还可能会遇上这种情况,开发项目时使用淘宝镜像,但是发布NPM第三方模块时就必须使用原镜像了。在着手解决那些奇葩情况前,先推荐大家使用一个NPM镜像管理工具

  • 原镜像https://registry.npmjs.org/
  • 淘宝镜像https://registry.npm.taobao.org/

主角就是nrm,它是一个可随时随地自由切换NPM镜像的管理工具。有了它,上面所说的何时使用什么镜像的问题就迎刃而解了。下面对其进行安装并简单讲解如何使用。

安装
npm i -g nrm
查看镜像
nrm ls
增加镜像
nrm add <name> <url>
移除镜像
nrm del <name>
测试镜像
nrm test <name>
使用镜像
nrm use <name>
查看当前镜像
nrm current

熟悉命令后一波操作如下,原镜像淘宝镜像之间随意切换。当然,如果你记性好也不需要用这个工具了,哈哈。

nrm操作

遇坑填坑

有了nrm切换到淘宝镜像上,安装速度会明显加快,但是遇上安装的模块依赖了C++模块那就坑爹了。在安装过程中会隐式安装node-gypnode-gyp可编译这些依赖C++模块的模块。

那么问题来了,node-gyp在首次编译时会依赖Node源码,所以又悄悄去下载Node。虽然在前面已设置了淘宝镜像,但是在这里一点卵用都没有。这样又因为国内网络环境的原因,再次遇上安装过慢安装失败的情况。

还好npm config提供了一个参数disturl,它可设置Node镜像地址,当然还是将其指向国内的淘宝镜像。这样又能爽歪歪安装这些依赖C++模块的模块了。

npm config set disturl https://npm.taobao.org/mirrors/node/

问题一步一步解决,接下来又出现另一个问题。平常大家都会使用node-sass作为项目开发依赖,但是node-sass的安装一直都是一个令人头疼的问题。

安装node-sass时,在install阶段会从Github上下载一个叫binding.node的文件,而GitHub Releases里的文件都托管在s3.amazonaws.com上,这个网址被Q了,所以又安装不了。

然而办法总比困难多,从node-sass的官方文档中可找到一个叫sass_binary_site的参数,它可设置Sass镜像地址,毫无疑问还是将其指向国内的淘宝镜像。这样又能爽歪歪安装node-sass了。

npm config set sass_binary_site https://npm.taobao.org/mirrors/node-sass/

其实还有好几个类似的模块,为了方便,笔者还是把它们源码里的镜像参数和淘宝镜像里对应的镜像地址扒出来,统一设置方便安装。以下是笔者常用的几个模块镜像地址配置,请收下!

分别是:SassSharpElectronPuppeteerPhantomSentrySqlitePython

镜像地址配置

npm config set <name> <url>,赶紧一键复制,永久使用。特别注意,别漏了最后面的/

npm config set sass_binary_site https://npm.taobao.org/mirrors/node-sass/
npm config set sharp_dist_base_url https://npm.taobao.org/mirrors/sharp-libvips/
npm config set electron_mirror https://npm.taobao.org/mirrors/electron/
npm config set puppeteer_download_host https://npm.taobao.org/mirrors/
npm config set phantomjs_cdnurl https://npm.taobao.org/mirrors/phantomjs/
npm config set sentrycli_cdnurl https://npm.taobao.org/mirrors/sentry-cli/
npm config set sqlite3_binary_site https://npm.taobao.org/mirrors/sqlite3/
npm config set python_mirror https://npm.taobao.org/mirrors/python/

有了这波操作,再执行npm i安装以上模块时就能享受国内的速度了。如果有条件,建议把这些镜像文件搬到自己或公司的服务器上,将镜像地址指向自己的服务器即可。在公司内网搭建一个这样的镜像服务器,一直安装一直爽,目前笔者所在的团队就是如此处理。

npm config set electron_mirror https://xyz/mirrors/electron/

源码分析

以经常卡住的node-sass为例,下面是坑爹货node-sass/lib/extensions.js源码部分,可看出它会默认走GitHub Releases的托管地址,上面也分析过原因,在这里就不重复了。

function getBinaryUrl() {
  const site = getArgument("--sass-binary-site")
    || process.env.SASS_BINARY_SITE
    || process.env.npm_config_sass_binary_site
    || (pkg.nodeSassConfig && pkg.nodeSassConfig.binarySite)
    || "https://github.com/sass/node-sass/releases/download";
  const result = [site, "v" + pkg.version, getBinaryName()].join("/");
  return result;
}

而其他模块也有类似的代码,例如puppeteer这个安装Chronium源码部分,有兴趣的同学都去扒一下源码,如出一辙。

async function download() {
  await compileTypeScriptIfRequired();
  const downloadHost =
    process.env.PUPPETEER_DOWNLOAD_HOST
    || process.env.npm_config_puppeteer_download_host
    || process.env.npm_package_config_puppeteer_download_host;
  const puppeteer = require("./index");
  const product =
    process.env.PUPPETEER_PRODUCT
    || process.env.npm_config_puppeteer_product
    || process.env.npm_package_config_puppeteer_product
    || "chrome";
  const browserFetcher = puppeteer.createBrowserFetcher({
    product,
    host: downloadHost,
  });
  const revision = await getRevision();
  await fetchBinary(revision);
  // 还有很多
}

坑货小结

由于node-sass是大家经常使用的项目开发依赖,也是安装时间较长和最常见到报错的模块,在这里笔者就花点篇章分析和解决下可能会遇到的问题。

node-sass安装失败的原因其实并不止上面提到的情况,我们可从安装过程中分析并获取突破口来解决问题。根据npm i node-sass的输出信息来分析,可得到下面的过程。

  • 检测项目node_modulesnode-sass是否存在且当前安装版本是否一致

    • Yes:跳过,完成安装过程
    • No:进入下一步
  • NPM上下载node-sass
  • 检测全局缓存项目缓存中是否存在binding.node

    • Yes:跳过,完成安装过程
    • No:进入下一步
  • Github Releases上下载binding.node并将其缓存到全局

    • Success:将版本信息写入package-lock.json
    • Error:进入下一步
  • 尝试本地编译出binding.node

    • Success:将版本信息写入package-lock.json
    • Error:输出错误信息

不难看出,node-sass依赖了一个二进制文件binding.node,不仅需要从NPM上下载本体还需要从Github Releases上下载binding.node


从实际情况来看,node-sass出现安装过慢安装失败的情况可能有以下几种:

NPM镜像托管在国外服务器

上面有提到,在这里不再叙述,解决办法如下。

nrm use taobao
安装过程中悄悄下载node-gyp

上面有提到,在这里不再叙述,解决办法如下。

npm config set disturl https://npm.taobao.org/mirrors/node/
binding.node文件托管在国外服务器

上面有提到,在这里不再叙述,解决办法如下。

npm config set sass_binary_site https://npm.taobao.org/mirrors/node-sass/
Node版本与node-sass版本不兼容

node-sass版本兼容性好差,必须与Node版本对应使用才行,详情请参考node-sass-version-association,复用官方文档的版本对照表,如下。

NodeJSMinimum node-sass versionNode Module
Node 144.14+83
Node 134.13+79
Node 124.12+72
Node 114.10+67
Node 104.9+64
Node 84.5.3+57

执行npm i安装依赖前请确保当前的Node版本和node-sass版本已兼容。

全局缓存中的binding.node版本与Node版本不兼容

假如本地使用nvmn进行Node版本管理,并且已切换了Node版本,在安装过程中可能会出现Windows/OS X/Linux 64-bit with Node.js 12.x这样的提示,这种情况也是笔者经常遇上的情况(笔者电脑里安装了30多个Node版本并且经常来回切换😂)。

这是因为node-sass版本和Node版本是关联的(看上面的表格),修改Node版本后在全局缓存中匹配不到对应的binding.node文件而导致安装失败。根据错误提示,清理NPM缓存且重新安装即可,解决办法如下。

npm cache clean -f

npm rebuild node-sass

所以没什么事就别来回切换Node版本了,像笔者装这么多Node版本也是逼不得已,老项目太多了😂。

安装失败后重新安装

有可能无权限删除已安装的内容,导致重新安装时可能会产生某些问题,建议将node_modules全部删除并重新安装。

在Mac系统和Linux系统上删除node_modules比较快,但是在Windows系统上删除node_modules就比较慢了,推荐大家使用rimraf删除node_modules,一个Node版的rm -rf工具。

npm i -g rimraf

在项目的package.json中加入npm scriptsrimraf常驻。三大操作系统通用,非常推荐使用。

{
  "scripts": {
    "reinstall": "rimraf node_modules && npm i"
  }
}

一有什么安装失败重新安装之类的操作,先执行npm run remove删除node_modulesnpm i

npm run reinstall

终极总结

如果看得有点乱,那下面直接贴代码操作顺序,建议前端小白在安装完Node后立马处理这些NPM镜像问题,防止后续产生不必要的麻烦(解决这些问题是需要花费时间的😂)。

# 查看Node版本和NPM版本确认已安装Node环境
node -v
npm -v

# 安装nrm并设置NPM的淘宝镜像
npm i -g nrm
nrm use taobao

# 设置依赖安装过程中内部模块下载Node的淘宝镜像
npm config set disturl https://npm.taobao.org/mirrors/node/

# 设置常用模块的淘宝镜像
npm config set sass_binary_site https://npm.taobao.org/mirrors/node-sass/
npm config set sharp_dist_base_url https://npm.taobao.org/mirrors/sharp-libvips/
npm config set electron_mirror https://npm.taobao.org/mirrors/electron/
npm config set puppeteer_download_host https://npm.taobao.org/mirrors/
npm config set phantomjs_cdnurl https://npm.taobao.org/mirrors/phantomjs/
npm config set sentrycli_cdnurl https://npm.taobao.org/mirrors/sentry-cli/
npm config set sqlite3_binary_site https://npm.taobao.org/mirrors/sqlite3/
npm config set python_mirror https://npm.taobao.org/mirrors/python/

针对node-sass的情况:

# 安装rimraf并设置package.json
npm i -g rimraf

# 安装前请确保当前的Node版本和node-sass版本已兼容

# 安装失败
npm cache clean -f
npm rebuild node-sass 或 npm run reinstall

package.json中加入npm scripts

{
  "scripts": {
    "reinstall": "rimraf node_modules && npm i"
  }
}

总结

NPM镜像问题的坑确实很多,归根到底还是网络环境导致的。当然这些问题也阻碍不了乐于探索的我们,办法总比困难多,坚持下去始终能找到解决方式。

笔者总结出一个解决这种NPM镜像问题的好方法,遇到一些上面没有提到的模块,可尝试通过以下步骤去解决问题。

  • 执行npm i前设置淘宝镜像,保证安装项目依赖时都走国内网络
  • 安装不成功时,肯定是在安装过程中该模块内部又去下载了其他国外服务器的文件
  • 在Github上克隆一份该模块的源码进行分析,搜索包含base、binary、cdn、config、dist、download、host、mirror、npm、site、url等这样的关键词(自行探索,通常mirror的匹配度最高)
  • 在搜查结果里查找形态像镜像地址的代码块,再分析该代码块的功能并提取最终的镜像地址,例如node-sasssass_binary_site
  • 去淘宝镜像官网、百度、谷歌等网站查找你需要的镜像地址,如果实在找不到就规范上网把国外服务器的镜像文件拉下来搬到自己或公司的服务器上
  • 设置模块依赖的镜像地址:npm config set <registry name> <taobao url / yourself url>
  • 重新执行npm i安装项目依赖,大功告成

如果以上内容帮不了你或在解决NPM镜像问题上还遇到其他坑,欢迎添加笔者微信一起交流。如有错误地方也欢迎指出,如有更好的解决方法也可提上建议。

另外笔者花了一些时间用Xmind整理了本文内容并生成一张知识点分布图,浓缩就是精华。由于图片太大无法上传就保存到公众号里,如有需要可关注IQ前端,扫描文章底部二维码,后台回复NPM镜像获取该图片,希望能帮助到你。

结语

❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更好的文章

关注公众号IQ前端,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔

  • 关注后回复关键词免费领取视频教程
  • 关注后添加我微信拉你进技术交流群
  • 欢迎关注IQ前端,更多CSS/JS开发技巧只在公众号推送

查看原文

赞 10 收藏 9 评论 1

JowayYoung 发布了文章 · 4月8日

一杯喜茶的时间手搓Promise

作者:JowayYoung
仓库:GithubCodePen
博客:掘金思否知乎简书头条CSDN
公众号:IQ前端
联系我:关注公众号后有我的微信
特别声明:原创不易,未经授权不得对此文章进行转载或抄袭,否则按侵权处理,如需转载或开通公众号白名单可联系我,希望各位尊重原创的知识产权

本文由笔者师妹LazyCurry创作,收录于笔者技术文章专栏下

前言

我们都知道,JS是单线程的,只有前一个任务结束,才能执行下一个任务。显然在浏览器上,这样执行会堵塞浏览器对DOM的渲染。所以,JS中会有很多异步操作,那JS是如何实现异步操作呢?这就要想到Promise对象了,文本先来认识Promise,再手写代码实现Promise。

认识Promise

Promise是JS解决异步编程的方法之一,其英文意思是承诺。在程序中可理解为等一段时间就会执行,等一段时间就是JS中的异步。异步是指需要比较长的时间才能执行完成的任务,例如网络请求,读取文件等。Promise是一个实例对象,可从中获取异步处理的结果。

Promise有3种状态,分别是pending(进行中)、fulfilled(已成功)、rejected(已失败)。只有异步操作可改变Promise的状态,其他操作都无法改变。并且状态改变后就不会再变,只能是从pendingfulfiledpendingrejected,这也是Promise一个比较鲜明的特点。

使用Promise

上述已说到,Promise是一个对象,那么它肯定是由其构造函数来创建。其构造函数接受一个函数作为参数,其函数的参数有2个,分别是resolverejectresolve将状态从pending变为fulfiled,成功时调用。reject将状态从pending变为rejected,失败时调用。

function RunPromise(num, time) {
    return new Promise((resolve, reject) => {
        console.log("开始执行");
        if (num % 2 === 0) {
            setTimeout(() => {
                resolve(`偶数时调用resolve,此时num为${num}`);
            }, time);
        } else {
            setTimeout(() => {
                reject(new Error(`奇数时调用rejected,此时num为${num}`));
            }, time);
        }
    });
}

Promise对象上有then()catch()方法。then()接收2个参数,第一个对应resolve的回调,第二个对应reject的回调。catch()then()的第二个参数一样,用来接受reject的回调,但是还有一个作用,如果在then()中执行resolve回调时抛出异常,这个异常可能是代码定义抛出,也可能是代码错误,而这个异常会在catch()被捕获到。

RunPromise(22, 2000)
    .then(res => {
        console.log("then的第一个参数执行");
        console.log(res);
        console.log(newres);
    }, error => {
        console.log("then的第二个参数执行");
        console.log(error);
    })
    .catch(error => {
        console.log("error");
        console.log(error);
    });

// 输出结果如下:
// 开始执行
// then的第一个参数执行
// 偶数时调用resolve,此时num为22
// error
// ReferenceError: newres is not defined

上面例子中,RunPromise()调用resolvethen()的第一个参数对应回调,状态从pending改成fulfilled,且状态不会再改变。在then()中,newres这个变量尚未定义,因此程序出错,其异常在catch()被捕获。一般来说,then()使用第一个参数即可,因为catch()then()的第二个参数一样,还能捕获到异常。

实现Promise

Promise大致已了解清楚,也知道如何使用。为了了解Promise是如何实现的,我们手写实现一个简单的Promise方法,简单地实现then()异步处理链式调用。用最简单的思考方法,函数是为了实现什么功能,给对应函数赋予相应的实现代码即可。以下代码均使用ES6进行书写。

定义Promise构造函数

创建Promise对象使用new Promise((resolve, reject) => {}),可知道Promise构造函数的参数是一个函数,我们将其定义为implement,函数带有2个参数:resolvereject,而这2个参数又可执行,所以也是一个函数。

声明完成后,需要解决状态。上述已说过,Promise有3种状态,这里不再细说,直接上代码。

// ES6声明构造函数
class MyPromise {
    constructor(implement) {
        this.status = "pending"; // 初始化状态为pending
        this.res = null; // 成功时的值
        this.error = null; // 失败时的值
        const resolve = res => {
            // resolve的作用只是将状态从pending转为fulfilled,并将成功时的值存在this.res
            if (this.status === "pending") {
                this.status = "fulfilled";
                this.res = res;
            }
        };
        const reject = error => {
            // reject的作用只是将状态从pending转为rejected,并将失败时的值存在this.error
            if (this.status === "pending") {
                this.status = "rejected";
                this.error = error;
            }
        };
        // 程序报错时会执行reject,所以在这里加上错误捕获,直接执行reject
        try {
            implement(resolve, reject);
        } catch (err) {
            reject(err);
        }
    }
}
then函数

我们在使用Promise时,都知道then()有2个参数,分别是状态为fulfilledrejected时的回调函数,我们在这里将2个函数定义为onFulfilledonRejected

class MyPromise {
    constructor(implement) { ... }
    then(onFulfilled, onRejected) {
        // 当状态为fulfilled时,调用onFulfilled并传入成功时的值
        if (this.status === "fulfilled") {
            onFulfilled(this.res);
        }
        // 当状态为rejected时,调用onRejected并传入失败时的值
        if (this.status === "rejected") {
            onRejected(this.error);
        }
    }
}
异步处理

到这里已实现了基本的代码,但是异步时会出现问题。例如,本文一开始举例使用Promise时,resolvesetTimeout()中使用,这时候在then()里,状态还是pending,那就没办法调用到onFulfilled。所以我们先将处理函数(onFulfilledonRejected)保存起来,等到then()被调用时再使用这些处理函数。

因为Promise可定义多个then(),所以这些处理函数用数组进行存储。实现思路:

  • then()增加状态为pending的判断,在此时存储处理函数
  • resolvereject时循环调用处理函数
class MyPromise {
    constructor(implement) {
        this.status = "pending";
        this.res = null;
        this.error = null;
        this.resolveCallbacks = []; // 成功时回调的处理函数
        this.rejectCallbacks = []; // 失败时回调的处理函数
        const resolve = res => {
            if (this.status === "pending") {
                this.status = "fulfilled";
                this.res = res;
                this.resolveCallbacks.forEach(fn => fn()); // 循环执行成功处理函数
            }
        };
        const reject = error => {
            if (this.status === "pending") {
                this.status = "rejected";
                this.error = error;
                this.rejectCallbacks.forEach(fn => fn()); // 循环执行失败处理函数
            }
        };
        try {
            implement(resolve, reject);
        } catch (err) {
            reject(err);
        }
    }
    then(onFulfilled, onRejected) {
        if (this.status === "fulfilled") {
            onFulfilled(this.res);
        }
        if (this.status === "rejected") {
            onRejected(this.error);
        }
        // 当状态为pending时,说明这时还没有调用到resolve或reject
        // 在这里把成功函数和失败函数存至相应的数组中,不做执行操作只做存储操作
        if (this.status === "pending") {
            this.resolveCallbacks.push(() => onFulfilled(this.res));
            this.rejectCallbacks.push(() => onRejected(this.error));
        }
    }
}

测试一下异步功能,打印结果中,'执行resolve'是等待了2秒后打印出来的

new MyPromise((resolve, reject) => {
    console.log("开始执行");
    setTimeout(() => {
        resolve("执行resolve");
    }, 2000);
}).then(res => console.log(res));

// 输出结果如下:
// 开始执行
// 执行resolve
链式调用

到这里就已实现异步操作啦!吼吼~但是,我们都知道,Promise能定义多个then,就例如new Promise().then().then(),这种就是链式调用。当然我们也要实现这个功能。

链式调用是指Promise在状态是fulfilled后,又开始执行下一个Promise。要实现这个功能,我们只需要在then()里返回Promise就好了,说起来好像是挺简单的。

then()的实现思路:

  • then()中需要返回Promise对象,我们将其命名为nextPromise
  • 仍然需要判断状态,执行相应处理
  • onFulfilledonRejected是异步调用,用setTimeout(0)解决
  • 需要对onFulfilledonRejected类型做判断,并做相应返回
class MyPromise {
    constructor(implement) { ... }
    then(onFulfilled, onRejected) {
        // 如果onRejected不是函数,就直接抛出错误
        onFulfilled = typeof onFulfilled === "function" ? onFulfilled : res => res;
        onRejected = typeof onRejected === "function" ? onRejected : err => { throw err; };
        const nextPromise = new MyPromise((resolve, reject) => {
            if (this.status === "fulfilled") {
                // 解决异步问题
                setTimeout(() => {
                    const x = onFulfilled(this.res);
                    RecursionPromise(nextPromise, x, resolve, reject);
                }, 0);
            }
            if (this.status === "rejected") {
                setTimeout(() => {
                    const x = onRejected(this.error);
                    RecursionPromise(nextPromise, x, resolve, reject);
                }, 0);
            }
            if (this.status === "pending") {
                this.resolveCallbacks.push(() => {
                    setTimeout(() => {
                        const x = onFulfilled(this.res);
                        RecursionPromise(nextPromise, x, resolve, reject);
                    }, 0);
                });
                this.rejectCallbacks.push(() => {
                    setTimeout(() => {
                        const x = onRejected(this.error);
                        RecursionPromise(nextPromise, x, resolve, reject);
                    }, 0);
                });
            }
        });
        return nextPromise;
    }
}

RecursionPromise()用来判断then()的返回值,以决定then()向下传递的状态走resolve还是reject,实现思路:

  • nextPromisex不能相等,否则会一直调用自己
  • 判断x的类型,如果不是函数或对象,直接resolve(x)
  • 判断x是否拥有then(),并且如果then()是一个函数,那么就可执行xthen(),并且带有成功与失败的回调
  • flag的作用是执行xthen()时成功与失败只能调用一次
  • 执行xthen(),成功时继续递归解析
  • 如果then()不是一个函数,直接resolve(x)
function RecursionPromise(nextPromise, x, resolve, reject) {
    if (nextPromise === x) return false;
    let flag;
    if (x !== null && (typeof x === "object" || typeof x === "function")) {
        try {
            let then = x.then;
            if (typeof then === "function") {
                then.call(x, y => {
                    if (flag) return false;
                    flag = true;
                    // 这里说明Promise对象resolve之后的结果仍然是Promise,那么继续递归解析
                    RecursionPromise(nextPromise, y, resolve, reject);
                }, error => {
                    if (flag) return false;
                    flag = true;
                    reject(error);
                });
            } else {
                resolve(x);
            }
        } catch (e) {
            if (flag) return false;
            flag = true;
            reject(e);
        }
    } else {
        resolve(x);
    }
}

总结

具有异步处理链式调用的Promise已实现啦!还有一些方法在这里就不一一实现了。毕竟实现一个完整的Promise不是一篇文章就能讲完的,有兴趣的同学可自行参照Promise的功能进行解构重写,若有写得不正确的地方请各位大佬指出。公众号后台回复promise可获取本文的源码,如果是转载的文章,可关注IQ前端再回复promise即可。

写这篇文章的目的是为了给各位同学提供一个函数解构的思路,学会去分析一个函数的功能,从而解构出每一个步骤是如何执行和实现的,祝大家学习愉快,下次再见~

结语

❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更好的文章

关注公众号IQ前端,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔

  • 关注后回复关键词免费领取视频教程
  • 关注后添加我微信拉你进技术交流群
  • 欢迎关注IQ前端,更多CSS/JS开发技巧只在公众号推送

查看原文

赞 18 收藏 13 评论 0

JowayYoung 发布了文章 · 4月1日

接近完美地判断JS数据类型,可行吗

作者:JowayYoung
仓库:GithubCodePen
博客:掘金思否知乎简书头条CSDN
公众号:IQ前端
联系我:关注公众号后有我的微信
特别声明:原创不易,未经授权不得对此文章进行转载或抄袭,否则按侵权处理,如需转载或开通公众号白名单可联系我,希望各位尊重原创的知识产权

本文由笔者师妹LazyCurry创作,收录于笔者技术文章专栏下

前言

JS的变量与其他语言的变量有很大区别,因为其变量松散的本质,决定了变量只是在特定时间内用于保存特定值的一个名字而已,变量的值及其数据类型可在声明周期内改变。

JS的数据类型可分为基本类型引用类型,先简单介绍两种数据类型,再来分析判断数据类型的几种方法。当然,这个也是大厂常考的面试题,同学们可按照文章的思路进行回答和扩展,让面试官耳目一新。

数据类型

基本类型

基本类型包括Undefined、Null、String、Number、Boolean、Symbol。基本类型按值访问,所以我们可操作保存在变量中实际的值。

基本类型的值在内存中占据固定大小的空间,是被保存在栈内存中。从一个变量向另一个变量复制基本类型的值,会创建这个值的一个副本,这两个值完全独立地存放在栈内存中。

引用类型

引用类型是对象类型,包括Object、Array、Function、Data、Regexp、Error。引用类型的值是保存在堆内存中的对象,JS不允许直接访问内存中的位置,也就是说不能直接访问操作对象的内存空间。

操作对象时,实际上是在操作对象的引用,所以说引用类型的值是按引用访问的。从而有[1, 2] === [1, 2]false

判断数据类型

简单的讲完JS的两种数据类型,接下来介绍一下JS判断数据类型的4种方法。

typeof

typeof是确定一个变量是stringnumberbooleansymbol(ES6新增类型)还是undefined的最佳工具。注意,这里并没有提及null以及引用型数据。

typeof可能返回下面某个结果,结果的对应值如下:

  • undefined:未定义的值
  • string:字符串
  • number:数值
  • boolean:布尔
  • symbol:唯一值
  • object:对象或空值(null)
  • function:函数
typeof undefined; // undefined
typeof null; // object
typeof "这是一段字符串"; // string
typeof 1; // number
typeof true; // boolean
typeof new Symbol(); // symbol
typeof new Object(); // object
typeof new Function(); // function
typeof new Date(); // object

上面的例子中,对于基本类型来说,除开null都可返回正确的结果。调用typeof null会返回object,是因为null被认为是一个空的对象引用,因此返回了object,当然这个也是JS设计语言早期遗留的Bug。

而在其他引用类型,除开function均返回object类型,因此用typeof来判断引用类型数据的类型并不可取,typeof适合用来判断基础类型值。

instanceof

instanceof可用来判断一个实例对象是否属于一个构造函数,其表达式A instanceof B,如果A是B的实例,则返回true,否则返回false

实现原理其实就是在A的原型链上寻找是否有原型等于B.prototype,如果一直找到A原型链的顶端null,仍然找不到原型等于B.prototype,那么就可返回false。原型链的知识可戳往期文章《来自原形与原型链的拷问》回顾下哦,这里就不再讲原型链啦~

new Date() instanceof Date; // true
new Date() instanceof Object; // true

[] instanceof Array; // true
[] instanceof Object; // true

function Person() {};
const person = new Person();
person instanceof Person; // true
person instanceof Object; // true

从上面的例子可看到,instanceof可判断出[]Array的实例,Date对象是Date的实例,personPerson构造函数的实例,到这里并没什么问题,但是instanceof认为这些也都是Object的实例,这就有点令人疑惑。

其实可根据instanceof的实现原理来分析一下,上面已经讲过实现原理,在这里我们套用一下instanceof用于数组判断的过程。

[] instanceof Array,因为能找到[].__proto__指向Array.prototype,因此返回true。[] instanceof Object,在这里就是也是要沿着[]的原型链找,有[].__proto__指向Array.prototype,又因为Array.prototype默认是Object的实例,所以有Array.prototype.__proto__指向了Object.prototype,因此这就是为什么instanceof认为[]也是Object的实例。

instanceof只能用来判断两个对象是否属于实例关系,并不能判断一个对象属于什么类型。简单说,就是判断两个类是否从属关系。

avatar

instanceof的问题在于,假如只有一个全局执行环境,如果网页中有两个框架,实际上就存在两个不用的全局执行环境,从而存在两个不同版本的Array构造函数。如果从一个框架向另一个框架传入一个数组,那么传入的数组与第二个框架中原生创建的数组分别是不同的构造函数。

const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
const IArray = window.frames[0].Array;
const iarr = new IArray();
iarr instanceof Array; // false
Array.isArray(iarr); // true

为了解决这个问题,ES5新增了Array.isArray(),这个方法能确定某个值是不是数组或类数组。

constructor

上面提到的原型链,原型对象的constructor属性指向了构造函数,又因为实例对象的__proto__属性指向原型对象,因此可有:每一个实例对象都可通过constructor来访问它的构造函数。而JS内置对象在内部构建时也是这么做的,因此可用来判断数据类型。

"".__proto__.constructor === String; // true
// 下面将属性__proto__去掉,效果相同
"".constructor === String; // true
new Number(1).constructor === Number; // true
true.constructor === Boolean; // true
[].constructor === Array; // true
new Date().constructor === Date; // true
new Function().constructor === Function; // true

可看出,大部分类型都能通过这个属性来判断。但是由于undefinednull是无效的对象,因此是没有constructor属性的,这两个值不能用这种方法判断。另外,当重写原型时,原型原有的constructor会丢失,这时判断也就不生效了。

function Person() {};
Person.prototype = {
    name: "XX"
};
const person = new Person();
person.constructor === Person; // false

这时打印person.constructor,可看到是一个Object。为什么会变成Object呢?这是因为在重新定义原型时,传入的是一个对象{}{}new Object()的字面量,因此会将Object原型上的constructor传递给{},所以person.constructor也就打印出了Object。

因此,在重写原型对象时,都需要给constructor重新赋值,来保证对象实例的类型不改变。这个点在开发时记得记得注意!

toString

Object.prototype.toString方法返回对象的类型字符串,因此可用来判断一个值的类型。因为实例对象有可能会自定义toString方法,会覆盖Object.prototype.toString,所以在使用时,最好加上call。会有以下返回值:

  • [object Undefined]:未定义的值
  • [object Null]:空值
  • [object String]:字符串
  • [object Number]:数值
  • [object Boolean]:布尔
  • [object Symbol]:唯一值
  • [object Object]:对象
  • [object Array]:数组
  • [object Function]:函数
  • [object Date]:日期
  • [object RegExp]:正则
  • [object Error]:错误
Object.prototype.toString.call(undefined); // [object Undefined]
Object.prototype.toString.call(null); // [object Null]
Object.prototype.toString.call("这是字符串"); // [object String]
Object.prototype.toString.call(1); // [object Number]
Object.prototype.toString.call(true); // [object Boolean]
Object.prototype.toString.call({}); // [object Object]
Object.prototype.toString.call([]); // [object Array]
Object.prototype.toString.call(new Function()); // [object Function]
Object.prototype.toString.call(new Date()); // [object Date]
Object.prototype.toString.call(new RegExp()); // [object RegExp]
Object.prototype.toString.call(new Error()); // [object Error]
总结与对比
  • typeof使用简单,但是只适用于判断基础类型数据
  • instanceof能判断引用类型,不能检测出基本类型,且不能跨iframe使用
  • constructor基本能判断所有类型,除了nullundefined,但是constructor容易被修改,也不能跨iframe使用
  • toString能判断所有类型,因此可将其封装成一个全能的DataType()判断所有数据类型
function DataType(tgt, type) {
    const dataType = Object.prototype.toString.call(tgt).replace(/\[object (\w+)\]/, "$1").toLowerCase();
    return type ? dataType === type : dataType;
}

DataType("young"); // "string"
DataType(20190214); // "number"
DataType(true); // "boolean"
DataType([], "array"); // true
DataType({}, "array"); // false

总结

JS的四种判断方法都有各自的优点跟缺点,要根据具体情况采取合适的判断方式。那么就到这里啦,有什么写得不对还麻烦各位大佬指出。有你们的支持我还会继续写出更好的文章~

结语

❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更好的文章

关注公众号IQ前端,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔

  • 关注后回复关键词免费领取视频教程
  • 关注后添加我微信拉你进技术交流群
  • 欢迎关注IQ前端,更多CSS/JS开发技巧只在公众号推送

查看原文

赞 23 收藏 15 评论 3

JowayYoung 发布了文章 · 3月31日

来自原形与原型链的拷问

作者:JowayYoung
仓库:GithubCodePen
博客:掘金思否知乎简书头条CSDN
公众号:IQ前端
联系我:关注公众号后有我的微信
特别声明:原创不易,未经授权不得对此文章进行转载或抄袭,否则按侵权处理,如需转载或开通公众号白名单可联系我,希望各位尊重原创的知识产权

本文由笔者师妹LazyCurry创作,收录于笔者技术文章专栏下

前言

在JS中,我们经常会遇到原型。字面上的意思会让我们认为,是某个对象的原型,可用来继承。但是其实这样的理解是片面的,下面通过本文来了解原型与原型链的细节,再顺便谈谈继承的几种方式。

原型

在讲到原型之前,我们先来回顾一下JS中的对象。在JS中,万物皆对象,就像字符串、数值、布尔、数组等。ECMA-262把对象定义为:无序属性的集合,其属性可包含基本值、对象或函数。对象是拥有属性和方法的数据,为了描述这些事物,便有了原型的概念。

无论何时,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向该函数的原型对象。所有原型对象都会获得一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针。

这段话摘自《JS高级程序设计》,很好理解,以创建实例的代码为例。

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayName = function() {
        alert(this.name);
    };
}

const person1 = new Person("gali", 18);
const person2 = new Person("pig", 20);

avatar

上面例子中的person1跟person2都是构造函数Person()的实例,Person.prototype指向了Person函数的原型对象,而Person.prototype.constructor又指向Person。Person的每一个实例,都含有一个内部属性__proto__,指向Person.prototype,就像上图所示,因此就有下面的关系。

console.log(Person.prototype.constructor === Person); // true
console.log(person1.__proto__ === Person.prototype); // true
console.log(person2.__proto__ === Person.prototype); // true

继承

JS是基于原型的语言,跟基于类的面向对象语言有所不同,JS中并没有类这个概念,有的是原型对象这个概念,原型对象作为一个模板,新对象可从原型对象中获得属性。那么JS具体是怎样继承的呢?

在讲到继承这个话题之前,我们先来理解原型链这个概念。

原型链

构造函数,原型和实例的关系已经很清楚了。每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例对象都包含一个指向与原型对象的指针。这样的关系非常好理解,但是如果我们想让原型对象等于另一个类型的实例对象呢?那么就会衍生出相同的关系,此时的原型对象就会含有一个指向另一个原型对象的指针,而另一个原型对象会含有一个指向另一个构造函数的指针。如果另一个原型对象又是另一个类型的实例对象呢?这样就构成了原型链。文字可能有点难理解,下面用代码举例。

function SuperType() {
    this.name = "张三";
}
SuperType.prototype.getSuperName = function() {
    return this.name;
};

function SubType() {
    this.subname = "李四";
}
SubType.prototype = new SuperType();
SubType.prototype.getSubName = function() {
    return this.subname;
};

const instance = new SubType();
console.log(instance.getSuperName()); // 张三

上述例子中,SubType的原型对象作为SuperType构造函数的实例对象,此时,SubType的原型对象就会有一个__proto__属性指向SuperType的原型对象,instance作为SubType的实例对象,必然能共享SubType的原型对象的属性,又因为SubType的原型对象又指向SuperType原型对象的属性,因此可得,instance继承了SuperType原型的所有属性。

我们都知道,所有函数的默认原型都是Object的实例,所以也能得出,SuperType的默认原型必然有一个__proto__指向Object.prototype。

图中由__proto__属性组成的链子,就是原型链,原型链的终点就是null

avatar

上图可很清晰的看出原型链的结构,这不禁让我想到JS的一个运算符instanceof,instanceof可用来判断一个实例对象是否属于一个构造函数。

A instanceof B; // true

实现原理其实就是在A的原型链上寻找是否有原型等于B.prototype,如果一直找到A原型链的顶端null,仍然找不到原型等于B.prototype,那么就可返回false。下面手写一个instanceof,这个也是很多大厂常用的手写面试题。

function Instance(left, right) {
    left = left.__proto__;
    right = right.prototype;
    while (true) {
        if (left === null) return false;
        if (left === right) return true;
        // 继续在left的原型链向上找
        left = left.__propo__;
    }
}
原型链继承

上面例子中,instance继承了SuperType原型的属性,其继承的原理其实就是通过原型链实现的。原型链很强大,可用来实现继承。可是单纯的原型链继承也是有问题存在的。

  • 实例属性变成原型属性,影响其他实例
  • 创建子类型的实例时,不能向超类型的构造函数传递参数
function SuperType() {
    this.colorArr = ["red", "blue", "green"];
}
function SubType() {}
SubType.prototype = new SuperType();

const instance1 = new SubType();
instance1.colorArr.push("black");
console.log(instance1.colorArr); // ["red", "blue", "green", "black"]

const instance2 = new SubType();
console.log(instance2.colorArr); // ["red", "blue", "green", "black"]

当SubType的原型作为SuperType的实例时,此时SubType的实例对象通过原型链继承到colorArr属性,当修改了其中一个实例对象从原型链中继承到的原型属性时,便会影响到其他实例。对instance1.colorArr的修改,在instance2.colorArr便能体现出来。

组合继承

组合继承指的是组合原型链和构造函数的技术,通过原型链实现对原型属性和方法的继承,而通过借用构造函数实现对实例属性的继承。

function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
    console.log(this.name);
};

function SubType(name, age) {
    // 继承属性,借用构造函数实现对实例属性的继承
    SuperType.call(this, name);
    this.age = age;
}

// 继承原型属性及方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
    console.log(this.age);
};

const instance1 = new SubType("gali", 18);
instance1.colors.push("black");
console.log(instance1.colors); // ["red", "blue", "green", "black"]
instance1.sayName(); // gali
instance1.sayAge(); // 18

const instance2 = new SubType("pig", 20);
console.log(instance2.colors); // ["red", "blue", "green"]
instance2.sayName(); // pig
instance2.sayAge(); // 20

上述例子中,借用构造函数继承实例属性,通过原型继承原型属性与方法。这样就可让不同的实例分别拥有自己的属性,又可共享相同的方法。而不会像原型继承那样,对实例属性的修改影响到了其他实例。组合继承是JS最常用的继承方式。

寄生组合式继承

虽然说组合继承是最常用的继承方式,但是有没有发现,就上面的例子中,组合继承中调用了2次SuperType函数。回忆一下,在第一次调用SubType时。

SubType.prototype = new SuperType();

这里调用完之后,SubType.prototype会从SuperType继承到2个属性:name和colors。这2个属性存在SubType的原型中。而在第二次调用时,就是在创造实例对象时,调用了SubType构造函数,也就会再调用一次SuperType构造函数。

SuperType.call(this, name);

第二次调用之后,便会在新的实例对象上创建了实例属性:name和colors。也就是说,这个时候,实例对象跟原型对象拥有2个同名属性。这样实在是浪费,效率又低。

为了解决这个问题,引入了寄生组合继承方式。重点就在于,不需要为了定义SubType的原型而去调用SuperType构造函数,此时只需要SuperType原型的一个副本,并将其赋值给SubType的原型即可。

function InheritPrototype(subType, superType) {
    // 创建超类型原型的一个副本
    const prototype = Object(superType.prototype);
    // 添加constructor属性,因为重写原型会失去constructor属性
    prototype.constructor = subType;
    subType.prototype = prototype;
}

将组合继承中的:

SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;

替换成:

InheritPrototype(SubType, SuperType);

寄生组合继承的优点在于,只需要调用一次SuperType构造函数。避免了在SubType的原型上创建多余的不必要的属性。

总结

温故而知新,再次看回《JS高级程序设计》这本书的原型与原型链部分,发现很多以前忽略掉的知识点。而这次回看这个知识点,并输出了一篇文章,对我来说受益匪浅。写文章往往不是为了写出怎样的文章,其实中间学习的过程才是最享受的。

结语

❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更好的文章

关注公众号IQ前端,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔

  • 关注后回复关键词免费领取视频教程
  • 关注后添加我微信拉你进技术交流群
  • 欢迎关注IQ前端,更多CSS/JS开发技巧只在公众号推送

查看原文

赞 44 收藏 28 评论 7

JowayYoung 发布了文章 · 3月9日

妙用CSS变量,让你的CSS变得更心动

作者:JowayYoung
仓库:GithubCodePen
博客:掘金思否知乎简书头条CSDN
公众号:IQ前端
联系我:关注公众号后有我的微信
特别声明:原创不易,未经授权不得对此文章进行转载或抄袭,否则按侵权处理,如需转载或开通公众号白名单可联系我,希望各位尊重原创的知识产权

前言

CSS变量又叫CSS自定义属性,为什么会突然提起这个很少人用到的东西呢?因为最近在重构个人官网,不知道为什么突然喜欢用上CSS变量,可能其自身隐藏的魅力,让笔者对它刮目相看。

谈到为什么会在CSS中使用变量,下面举个栗子,估计大家一看就会明白。

/* 不使用CSS变量 */
.title {
    background-color: red;
}
.desc {
    background-color: red;
}

/* 使用CSS变量 */
:root {
    --bg-color: red;
}
.title {
    background-color: var(--bg-color);
}
.desc {
    background-color: var(--bg-color);
}

看完可能会觉得使用CSS变量的代码量多了一点,但是有没有想到突然某天万恶的策划小哥哥和设计小姐姐说要做一个换肤功能。按照平常的思路,估计有些同学就会按照默认颜色主题增加一份对照的新颜色主题CSS文件。这样每次新增需求都同时维护几套主题颜色多麻烦啊。

此时CSS变量就派上用场了,提前跟设计小姐姐规范好各种需要变换的颜色并通过CSS变量进行定义,通过JS批量操作这些定义好的CSS变量即可。这也是变换主题颜色的一种解决方案之一,好处在于只需写一套CSS代码。

["red", "blue", "green"].forEach(v => {
    const btn = document.getElementById(`${v}-theme-btn`);
    btn.addEventListener("click", () => document.body.style.setProperty("--bg-color", v));
});

在此总结下CSS使用变量的好处:

  • 减少样式代码的重复性
  • 增加样式代码的扩展性
  • 提高样式代码的灵活性
  • 增多一种CSS与JS的通讯方式
  • 不用深层遍历DOM改变某个样式

可能有些同学会问,Sass和Less早就实现了变量这个特性,何必再多此一举呢。可是细想一下,CSS变量对比Sass和Less的变量,又有它的过人之处。

  • 浏览器原生特性,无需经过任何转译就可直接运行
  • DOM对象一员,极大便利了CSS与JS之间的联系

认识

本来打算用一半篇幅讲述CSS变量的规范和用法,但是网上一搜一大把就感觉没必要了,贴上阮一峰老师写的教程《CSS变量教程》。同时笔者也对CSS变量的细节地方进行一个整理,方便大家记忆。

  • 声明:--变量名
  • 读取:var(--变量名, 默认值)
  • 类型

    • 普通:只能用作属性值不能用作属性名
    • 字符:与字符串拼接 "Hello, "var(--name)
    • 数值:使用calc()与数值单位连用 var(--width) * 10px
  • 作用域

    • 范围:在当前元素块作用域及其子元素块作用域下有效
    • 优先级别:内联样式 > ID选择器 > 类选择器 = 属性选择器 = 伪类选择器 > 标签选择器 = 伪元素选择器

接下来使用几个特别的场景展示CSS变量的魅力。还是那句话,一样东西有使用的场景,那自然就会有它的价值,那么用的人也会越来越多。

使用场景

其实CSS变量有一个特别好用的场景,那就是结合List元素集合使用。如果不明白这是什么,请继续往下看。

以下所有演示代码基于Vue文件,但HTML、CSS和JS分开书写,为了简化CSS的书写而使用Sass进行预处理,方便代码演示
条形加载条

一个条形加载条通常由几条线条组成,并且每条线条对应一个存在不同时延的相同动画,通过时间差运行相同的动画,从而产生加载效果。估计大部分的同学可能会把CSS代码写成以下这样。

strip-loading

<ul class="strip-loading flex-ct-x">
    <li v-for="v in 6" :key="v"></li>
</ul>
.loading {
    width: 200px;
    height: 200px;
    li {
        border-radius: 3px;
        width: 6px;
        height: 30px;
        background-color: #f66;
        animation: beat 1s ease-in-out infinite;
        & + li {
            margin-left: 5px;
        }
        &:nth-child(2) {
            animation-delay: 200ms;
        }
        &:nth-child(3) {
            animation-delay: 400ms;
        }
        &:nth-child(4) {
            animation-delay: 600ms;
        }
        &:nth-child(5) {
            animation-delay: 800ms;
        }
        &:nth-child(6) {
            animation-delay: 1s;
        }
    }
}

分析代码发现,每个<li>只是存在animation-delay不同,而其余代码则完全相同,换成其他类似的List元素集合场景,那岂不是有10个<li>就写10个:nth-child

显然这种方法不灵活也不容易封装成组件,如果能像JS那样封装成一个函数,并根据参数输出不同的样式效果,那就更棒了。说到这里,很明显就是为了铺垫CSS变量的开发技巧了。

对于HTML部分的修改,让每个<li>拥有一个自己作用域下的CSS变量。对于CSS部分的修改,就需要分析哪些属性是随着index递增而发生规律变化的,对规律变化的部分使用CSS变量表达式代替即可。

<ul class="strip-loading flex-ct-x">
    <li v-for="v in 6" :key="v" :style="`--line-index: ${v}`"></li>
</ul>
.strip-loading {
    width: 200px;
    height: 200px;
    li {
        --time: calc((var(--line-index) - 1) * 200ms);
        border-radius: 3px;
        width: 6px;
        height: 30px;
        background-color: #f66;
        animation: beat 1.5s ease-in-out var(--time) infinite;
        & + li {
            margin-left: 5px;
        }
    }
}
源码链接可在文章结尾处获取

代码中的变量--line-index--time使每个<li>拥有一个属于自己的作用域。例如第2个<li>--line-index的值为2,--time的计算值为200ms,换成第3个<li>后这两个值又会不同了。

这就是CSS变量的作用范围所致(在当前元素块作用域及其子元素块作用域下有效),因此在.strip-loading的块作用域下调用--line-index是无效的。

/* flex属性无效 */
.loading {
    display: flex;
    align-items: center;
    flex: var(--line-index);
}

通过妙用CSS变量,也把CSS代码从29行缩减到15行,对于那些含有List元素集合越多的场景,效果就更明显。而且这样写也更加美观更加容易维护,某天说加载效果的时间差不明显,直接将calc((var(--line-index) - 1) * 200ms)里的200ms调整成400ms即可。就无需对每个:nth-child(n)进行修改了。

心形加载条

前段时间刷掘金看到陈大鱼头兄的心形加载条,觉得挺漂亮的,很带感觉。

h-loading

通过动图分析,发现每条线条的背景色和动画时延不一致,另外动画运行时的高度也不一致。细心的你可能还会发现,第1条和第9条的高度一致,第2条和第8条的高度一致,依次类推,得到高度变换相同类的公式:对称index = 总数 + 1 - index

背景色使用了滤镜的色相旋转hue-rotate函数,目的是为了使颜色过渡得更加自然;动画时延的设置和上面条形加载条的设置一致。下面就用CSS变量根据看到的动图实现一番。

<div class="heart-loading flex-ct-x">
    <ul style="--line-count: 9">
        <li v-for="v in 9" :key="v" :class="`line-${v}`" :style="`--line-index: ${v}`"></li>
    </ul>
</div>
.heart-loading {
    width: 200px;
    height: 200px;
    ul {
        display: flex;
        justify-content: space-between;
        width: 150px;
        height: 10px;
    }
    li {
        --Θ: calc(var(--line-index) / var(--line-count) * .5turn);
        --time: calc((var(--line-index) - 1) * 40ms);
        border-radius: 5px;
        width: 10px;
        height: 10px;
        background-color: #3c9;
        filter: hue-rotate(var(--Θ));
        animation-duration: 1s;
        animation-delay: var(--time);
        animation-iteration-count: infinite;
    }
    .line-1,
    .line-9 {
        animation-name: line-move-1;
    }
    .line-2,
    .line-8 {
        animation-name: line-move-2;
    }
    .line-3,
    .line-7 {
        animation-name: line-move-3;
    }
    .line-4,
    .line-6 {
        animation-name: line-move-4;
    }
    .line-5 {
        animation-name: line-move-5;
    }
}
源码链接可在文章结尾处获取

一波操作后就有了下面的效果。和陈大鱼头兄的心形加载条对比一下,颜色、波动曲线和跳动频率有点不一样,在暖色调的蔓延和肾上腺素的飙升下,这是一种心动的感觉。想起自己曾经写的一首诗:我见犹怜,爱不释手,雅俗共赏,君子好逑

heart-loading

标签导航栏

上面通过两个加载条演示了CSS变量在CSS中的运用以及一些妙用技巧,现在通过标签导航栏演示CSS变量在JS中的运用。

JS中主要有3个操作CSS变量的API,看上去简单易记,分别如下:

  • 读取变量:elem.style.getPropertyValue()
  • 设置变量:elem.style.setProperty()
  • 删除变量:elem.style.removeProperty()

先上效果图,效果中主要是使用CSS变量标记每个Tab的背景色和切换Tab的显示状态。

tab-navbar

<div class="tab-navbar">
    <nav>
        <a v-for="(v, i) in list" :key="v" :class="{ active: index === i }" @click="select(i)">标题{{i + 1}}</a>
    </nav>
    <div>
        <ul ref="tabs" :style="`--tab-count: ${list.length}`">
            <li v-for="(v, i) in list" :key="v" :style="`--bg-color: ${v}`">内容{{i + 1}}</li>
        </ul>
    </div>
</div>
.tab-navbar {
    display: flex;
    overflow: hidden;
    flex-direction: column-reverse;
    border-radius: 10px;
    width: 300px;
    height: 400px;
    nav {
        display: flex;
        height: 40px;
        background-color: #f0f0f0;
        line-height: 40px;
        text-align: center;
        a {
            flex: 1;
            cursor: pointer;
            transition: all 300ms;
            &.active {
                background-color: #66f;
                font-weight: bold;
                color: #fff;
            }
        }
    }
    div {
        flex: 1;
        ul {
            --tab-index: 0;
            --tab-width: calc(var(--tab-count) * 100%);
            --tab-move: calc(var(--tab-index) / var(--tab-count) * -100%);
            display: flex;
            flex-wrap: nowrap;
            width: var(--tab-width);
            height: 100%;
            transform: translate3d(var(--tab-move), 0, 0);
            transition: all 300ms;
        }
        li {
            display: flex;
            justify-content: center;
            align-items: center;
            flex: 1;
            background-color: var(--bg-color);
            font-weight: bold;
            font-size: 20px;
            color: #fff;
        }
    }
}
export default {
    data() {
        return {
            index: 0,
            list: ["#f66", "#09f", "#3c9"]
        };
    },
    methods: {
        select(i) {
            this.index = i;
            this.$refs.tabs.style.setProperty("--tab-index", i);
        }
    }
};
源码链接可在文章结尾处获取

<ul>上定义--tab-index表示Tab当前的索引,当点击按钮时重置--tab-index的值,就可实现不操作DOM来移动<ul>的位置显示指定的Tab。不操作DOM而可移动<ul>是因为定义了--tab-move,通过calc()计算--tab-index--tab-move的关系,从而操控transform: translate3d()来移动<ul>

另外在<li>上定义--bg-color表示Tab的背景色,也是一种比较简洁的模板赋值方式,总比写<li :style="backgroundColor: ${color}">要好看。如果多个CSS属性依赖一个变量赋值,那么使用CSS变量赋值到style上就更方便了,那些CSS属性可在CSS文件里进行计算与赋值,这样可帮助JS分担一些属性计算工作。

当然,这个标签导航栏也可通过纯CSS实现,有兴趣的同学可看看笔者之前一篇文章里的纯CSS标签导航栏

悬浮跟踪按钮

通过几个栗子实践了CSS变量在CSS和JS上的运用,相信大家已经掌握了其用法和技巧。之前在某个网站看过一个比较酷炫的鼠标悬浮特效,好像也是使用CSS变量实现的。笔者凭着记忆也使用CSS变量实现一番。

其实思路也比较简单,先对按钮进行布局和着色,然后使用伪元素标记鼠标的位置,定义--x--y表示伪元素在按钮里的坐标,通过JS获取鼠标在按钮上的offsetLeftoffsetLeft分别赋值给--x--y,再对伪元素添加径向渐变的背景色,大功告成,一个酷炫的鼠标悬浮跟踪特效就这样诞生了。

track-btn

<a class="track-btn pr tac" @mousemove="move">
    <span>妙用CSS变量,让你的CSS变得更心动</span>
</a>
.track-btn {
    display: block;
    overflow: hidden;
    border-radius: 100px;
    width: 400px;
    height: 50px;
    background-color: #66f;
    line-height: 50px;
    cursor: pointer;
    font-weight: bold;
    font-size: 18px;
    color: #fff;
    span {
        position: relative;
    }
    &::before {
        --size: 0;
        position: absolute;
        left: var(--x);
        top: var(--y);
        width: var(--size);
        height: var(--size);
        background-image: radial-gradient(circle closest-side, #09f, transparent);
        content: "";
        transform: translate3d(-50%, -50%, 0);
        transition: all 200ms ease;
    }
    &:hover::before {
        --size: 400px;
    }
}
export default {
    name: "track-btn",
    methods: {
        move(e) {
            const x = e.pageX - e.target.offsetLeft;
            const y = e.pageY - e.target.offsetTop;
            e.target.style.setProperty("--x", `${x}px`);
            e.target.style.setProperty("--y", `${y}px`);
        }
    }
};
源码链接可在文章结尾处获取

其实可结合鼠标事件来完成更多的酷炫效果,例如动画关联事件响应等操作。没有做不到,只有想不到,尽情发挥你的想象力啦。

之前在CodePen上还看到一个挺不错的栗子,一个悬浮视差按钮,具体代码涉及到一些3D变换的知识。看完源码后,按照其思路自己也实现一番,顺便对代码稍加改良并封装成Vue组件,存放到本课件示例代码中。感觉录制的GIF有点别扭,显示效果不太好,有兴趣的同学可下载本课件示例代码,自己运行看看效果。

parallax-btn

兼容

对于现代浏览器来说,CSS变量的兼容性其实还是蛮好的,所以大家可放心使用。毕竟现在都是各大浏览器厂商快速迭代的时刻,产品对于用户体验来说是占了很大比重,因此在条件允许的情况下还是大胆尝新,不要被一些过去的所谓的规范所约束着。

caniuse-css-var

试问现在还有多少人愿意去维护IE6~IE9的兼容性,如果一个产品的用户体验受限于远古浏览器的压制(可能政务Web应用和金融Web应用除外吧),相信这个产品也不会走得很远。

我们在完成一个产品的过程中,不仅仅是为了完成工作任务,如果在保证进度的同时能花点心思点缀一下,可能会有意外的收获。用心写好每一段代码,才是享受写代码的真谛

总结

本文通过循序渐进的方式探讨了CSS变量的运用和技巧,对于一个这么好用的特性,当然是不能放过啦。其实多多思考,就能把CSS变量用在很多场景上。笔者把本文提到的示例统一组成一个Demo,也方便有兴趣的同学通过课件示例代码进行学习,思考一些可能在阅读本文时没有注意到的细节。

  • Demo示例:条形加载条心形加载条标签导航栏悬浮跟踪按钮悬浮视差按钮
  • Demo地址:关注IQ前端,扫描文章底部二维码,后台回复变量,获取整套课件示例代码
  • Demo运行:里面的readme.html有详细说明,记得看喔

写到最后,送给大家一个大大的彩蛋,一个暖心彩虹色调🌈搭配的爱心点赞按钮。如果你觉得本文写得棒棒哒,请给笔者一个赞喔,就像下面那样。当然,彩蛋源码也在课件示例代码里啦。想了解更多的CSS开发技巧,可移步到笔者19年写的一篇9.2万阅读量的爆款文章《灵活运用CSS开发技巧(66个骚操作案例)》,保证满足你的眼球。

like-btn

结语

❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更好的文章

关注公众号IQ前端,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔

  • 关注后回复关键词免费领取视频教程
  • 关注后添加我微信拉你进技术交流群
  • 欢迎关注IQ前端,更多CSS/JS开发技巧只在公众号推送

查看原文

赞 39 收藏 29 评论 0

JowayYoung 发布了文章 · 2月13日

25个你不得不知道的数组reduce高级用法

作者:JowayYoung
仓库:GithubCodePen
博客:掘金思否知乎简书头条CSDN
公众号:IQ前端
联系我:关注公众号后有我的微信
特别声明:原创不易,未经授权不得对此文章进行转载或抄袭,否则按侵权处理,如需转载或开通公众号白名单可联系我,希望各位尊重原创的知识产权

背景

距离上一篇技术文章《1.5万字概括ES6全部特性》发布到现在,已经有整整4个月没有输出过一篇技术文章了。哈哈,不是不想写,而是实在太忙,这段时间每天不是上班就是加班,完全没有自己的时间。这篇文章也是抽空之余完成,希望大家喜欢,谢谢大家继续支持我。
本文首发于『搜狐技术产品』公众号,首发内容与博客内容略有不同,博客内容今早发布时额外有所增加

reduce作为ES5新增的常规数组方法之一,对比forEachfiltermap,在实际使用上好像有些被忽略,发现身边的人极少使用它,导致这个如此强大的方法被逐渐埋没。

如果经常使用reduce,怎么可能放过如此好用的它呢!我还是得把他从尘土中取出来擦干净,奉上它的高级用法给大家。一个如此好用的方法不应该被大众埋没。

下面对reduce的语法进行简单说明,详情可查看MDNreduce()的相关说明。

  • 定义:对数组中的每个元素执行一个自定义的累计器,将其结果汇总为单个返回值
  • 形式:array.reduce((t, v, i, a) => {}, initValue)
  • 参数

    • callback:回调函数(必选)
    • initValue:初始值(可选)
  • 回调函数的参数

    • total(t):累计器完成计算的返回值(必选)
    • value(v):当前元素(必选)
    • index(i):当前元素的索引(可选)
    • array(a):当前元素所属的数组对象(可选)
  • 过程

    • t作为累计结果的初始值,不设置t则以数组第一个元素为初始值
    • 开始遍历,使用累计器处理v,将v的映射结果累计到t上,结束此次循环,返回t
    • 进入下一次循环,重复上述操作,直至数组最后一个元素
    • 结束遍历,返回最终的t

reduce的精华所在是将累计器逐个作用于数组成员上,把上一次输出的值作为下一次输入的值。下面举个简单的栗子,看看reduce的计算结果。

const arr = [3, 5, 1, 4, 2];
const a = arr.reduce((t, v) => t + v);
// 等同于
const b = arr.reduce((t, v) => t + v, 0);

代码不太明白没关系,贴一个reduce的作用动图应该就会明白了。

reduce

reduce实质上是一个累计器函数,通过用户自定义的累计器对数组成员进行自定义累计,得出一个由累计器生成的值。另外reduce还有一个胞弟reduceRight,两个方法的功能其实是一样的,只不过reduce是升序执行,reduceRight是降序执行。

对空数组调用reduce()和reduceRight()是不会执行其回调函数的,可认为reduce()对空数组无效

高级用法

单凭以上一个简单栗子不足以说明reduce是个什么。为了展示reduce的魅力,我为大家提供25种场景来应用reduce的高级用法。有部分高级用法可能需要结合其他方法来实现,这样为reduce的多元化提供了更多的可能性。

部分示例代码的写法可能有些骚,看得不习惯可自行整理成自己的习惯写法
累加累乘
function Accumulation(...vals) {
    return vals.reduce((t, v) => t + v, 0);
}

function Multiplication(...vals) {
    return vals.reduce((t, v) => t * v, 1);
}
Accumulation(1, 2, 3, 4, 5); // 15
Multiplication(1, 2, 3, 4, 5); // 120
权重求和
const scores = [
    { score: 90, subject: "chinese", weight: 0.5 },
    { score: 95, subject: "math", weight: 0.3 },
    { score: 85, subject: "english", weight: 0.2 }
];
const result = scores.reduce((t, v) => t + v.score * v.weight, 0); // 90.5
代替reverse
function Reverse(arr = []) {
    return arr.reduceRight((t, v) => (t.push(v), t), []);
}
Reverse([1, 2, 3, 4, 5]); // [5, 4, 3, 2, 1]
代替map和filter
const arr = [0, 1, 2, 3];

// 代替map:[0, 2, 4, 6]
const a = arr.map(v => v * 2);
const b = arr.reduce((t, v) => [...t, v * 2], []);

// 代替filter:[2, 3]
const c = arr.filter(v => v > 1);
const d = arr.reduce((t, v) => v > 1 ? [...t, v] : t, []);

// 代替map和filter:[4, 6]
const e = arr.map(v => v * 2).filter(v => v > 2);
const f = arr.reduce((t, v) => v * 2 > 2 ? [...t, v * 2] : t, []);
代替some和every
const scores = [
    { score: 45, subject: "chinese" },
    { score: 90, subject: "math" },
    { score: 60, subject: "english" }
];

// 代替some:至少一门合格
const isAtLeastOneQualified = scores.reduce((t, v) => t || v.score >= 60, false); // true

// 代替every:全部合格
const isAllQualified = scores.reduce((t, v) => t && v.score >= 60, true); // false
数组分割
function Chunk(arr = [], size = 1) {
    return arr.length ? arr.reduce((t, v) => (t[t.length - 1].length === size ? t.push([v]) : t[t.length - 1].push(v), t), [[]]) : [];
}
const arr = [1, 2, 3, 4, 5];
Chunk(arr, 2); // [[1, 2], [3, 4], [5]]
数组过滤
function Difference(arr = [], oarr = []) {
    return arr.reduce((t, v) => (!oarr.includes(v) && t.push(v), t), []);
}
const arr1 = [1, 2, 3, 4, 5];
const arr2 = [2, 3, 6]
Difference(arr1, arr2); // [1, 4, 5]
数组填充
function Fill(arr = [], val = "", start = 0, end = arr.length) {
    if (start < 0 || start >= end || end > arr.length) return arr;
    return [
        ...arr.slice(0, start),
        ...arr.slice(start, end).reduce((t, v) => (t.push(val || v), t), []),
        ...arr.slice(end, arr.length)
    ];
}
const arr = [0, 1, 2, 3, 4, 5, 6];
Fill(arr, "aaa", 2, 5); // [0, 1, "aaa", "aaa", "aaa", 5, 6]
数组扁平
function Flat(arr = []) {
    return arr.reduce((t, v) => t.concat(Array.isArray(v) ? Flat(v) : v), [])
}
const arr = [0, 1, [2, 3], [4, 5, [6, 7]], [8, [9, 10, [11, 12]]]];
Flat(arr); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
数组去重
function Uniq(arr = []) {
    return arr.reduce((t, v) => t.includes(v) ? t : [...t, v], []);
}
const arr = [2, 1, 0, 3, 2, 1, 2];
Uniq(arr); // [2, 1, 0, 3]
数组最大最小值
function Max(arr = []) {
    return arr.reduce((t, v) => t > v ? t : v);
}

function Min(arr = []) {
    return arr.reduce((t, v) => t < v ? t : v);
}
const arr = [12, 45, 21, 65, 38, 76, 108, 43];
Max(arr); // 108
Min(arr); // 12
数组成员独立拆解
function Unzip(arr = []) {
    return arr.reduce(
        (t, v) => (v.forEach((w, i) => t[i].push(w)), t),
        Array.from({ length: Math.max(...arr.map(v => v.length)) }).map(v => [])
    );
}
const arr = [["a", 1, true], ["b", 2, false]];
Unzip(arr); // [["a", "b"], [1, 2], [true, false]]
数组成员个数统计
function Count(arr = []) {
    return arr.reduce((t, v) => (t[v] = (t[v] || 0) + 1, t), {});
}
const arr = [0, 1, 1, 2, 2, 2];
Count(arr); // { 0: 1, 1: 2, 2: 3 }
此方法是字符统计和单词统计的原理,入参时把字符串处理成数组即可
数组成员位置记录
function Position(arr = [], val) {
    return arr.reduce((t, v, i) => (v === val && t.push(i), t), []);
}
const arr = [2, 1, 5, 4, 2, 1, 6, 6, 7];
Position(arr, 2); // [0, 4]
数组成员特性分组
function Group(arr = [], key) {
    return key ? arr.reduce((t, v) => (!t[v[key]] && (t[v[key]] = []), t[v[key]].push(v), t), {}) : {};
}
const arr = [
    { area: "GZ", name: "YZW", age: 27 },
    { area: "GZ", name: "TYJ", age: 25 },
    { area: "SZ", name: "AAA", age: 23 },
    { area: "FS", name: "BBB", age: 21 },
    { area: "SZ", name: "CCC", age: 19 }
]; // 以地区area作为分组依据
Group(arr, "area"); // { GZ: Array(2), SZ: Array(2), FS: Array(1) }
数组成员所含关键字统计
function Keyword(arr = [], keys = []) {
    return keys.reduce((t, v) => (arr.some(w => w.includes(v)) && t.push(v), t), []);
}
const text = [
    "今天天气真好,我想出去钓鱼",
    "我一边看电视,一边写作业",
    "小明喜欢同桌的小红,又喜欢后桌的小君,真TM花心",
    "最近上班喜欢摸鱼的人实在太多了,代码不好好写,在想入非非"
];
const keyword = ["偷懒", "喜欢", "睡觉", "摸鱼", "真好", "一边", "明天"];
Keyword(text, keyword); // ["喜欢", "摸鱼", "真好", "一边"]
字符串翻转
function ReverseStr(str = "") {
    return str.split("").reduceRight((t, v) => t + v);
}
const str = "reduce最牛逼";
ReverseStr(str); // "逼牛最ecuder"
数字千分化
function ThousandNum(num = 0) {
    const str = (+num).toString().split(".");
    const int = nums => nums.split("").reverse().reduceRight((t, v, i) => t + (i % 3 ? v : `${v},`), "").replace(/^,|,$/g, "");
    const dec = nums => nums.split("").reduce((t, v, i) => t + ((i + 1) % 3 ? v : `${v},`), "").replace(/^,|,$/g, "");
    return str.length > 1 ? `${int(str[0])}.${dec(str[1])}` : int(str[0]);
}
ThousandNum(1234); // "1,234"
ThousandNum(1234.00); // "1,234"
ThousandNum(0.1234); // "0.123,4"
ThousandNum(1234.5678); // "1,234.567,8"
异步累计
async function AsyncTotal(arr = []) {
    return arr.reduce(async(t, v) => {
        const at = await t;
        const todo = await Todo(v);
        at[v] = todo;
        return at;
    }, Promise.resolve({}));
}
const result = await AsyncTotal(); // 需要在async包围下使用
斐波那契数列
function Fibonacci(len = 2) {
    const arr = [...new Array(len).keys()];
    return arr.reduce((t, v, i) => (i > 1 && t.push(t[i - 1] + t[i - 2]), t), [0, 1]);
}
Fibonacci(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
URL参数反序列化
function ParseUrlSearch() {
    return location.search.replace(/(^\?)|(&$)/g, "").split("&").reduce((t, v) => {
        const [key, val] = v.split("=");
        t[key] = decodeURIComponent(val);
        return t;
    }, {});
}
// 假设URL为:https://www.baidu.com?age=25&name=TYJ
ParseUrlSearch(); // { age: "25", name: "TYJ" }
URL参数序列化
function StringifyUrlSearch(search = {}) {
    return Object.entries(search).reduce(
        (t, v) => `${t}${v[0]}=${encodeURIComponent(v[1])}&`,
        Object.keys(search).length ? "?" : ""
    ).replace(/&$/, "");
}
StringifyUrlSearch({ age: 27, name: "YZW" }); // "?age=27&name=YZW"
返回对象指定键值
function GetKeys(obj = {}, keys = []) {
    return Object.keys(obj).reduce((t, v) => (keys.includes(v) && (t[v] = obj[v]), t), {});
}
const target = { a: 1, b: 2, c: 3, d: 4 };
const keyword = ["a", "d"];
GetKeys(target, keyword); // { a: 1, d: 4 }
数组转对象
const people = [
    { area: "GZ", name: "YZW", age: 27 },
    { area: "SZ", name: "TYJ", age: 25 }
];
const map = people.reduce((t, v) => {
    const { name, ...rest } = v;
    t[name] = rest;
    return t;
}, {}); // { YZW: {…}, TYJ: {…} }
Redux Compose函数原理
function Compose(...funs) {
    if (funs.length === 0) {
        return arg => arg;
    }
    if (funs.length === 1) {
        return funs[0];
    }
    return funs.reduce((t, v) => (...arg) => t(v(...arg)));
}

兼容和性能

好用是挺好用的,但是兼容性如何呢?在Caniuse上搜索一番,兼容性绝对的好,可大胆在任何项目上使用。不要吝啬你的想象力,尽情发挥reducecompose技能啦。对于时常做一些累计的功能,reduce绝对是首选方法。

caniuse-reduce

caniuse-reduceRight

另外,有些同学可能会问,reduce的性能又如何呢?下面我们通过对forforEachmapreduce四个方法同时做1~100000的累加操作,看看四个方法各自的执行时间。

// 创建一个长度为100000的数组
const list = [...new Array(100000).keys()];

// for
console.time("for");
let result1 = 0;
for (let i = 0; i < list.length; i++) {
    result1 += i + 1;
}
console.log(result1);
console.timeEnd("for");

// forEach
console.time("forEach");
let result2 = 0;
list.forEach(v => (result2 += v + 1));
console.log(result2);
console.timeEnd("forEach");

// map
console.time("map");
let result3 = 0;
list.map(v => (result3 += v + 1, v));
console.log(result3);
console.timeEnd("map");

// reduce
console.time("reduce");
const result4 = list.reduce((t, v) => t + v + 1, 0);
console.log(result4);
console.timeEnd("reduce");
累加操作执行时间
for6.719970703125ms
forEach3.696044921875ms
map3.554931640625ms
reduce2.806884765625ms

以上代码在MacBook Pro 2019 15寸 16G内存 512G闪存Chrome 79下执行,不同的机器不同的环境下执行以上代码都有可能存在差异。

我已同时测试过多台机器和多个浏览器,连续做了10次以上操作,发现reduce总体的平均执行时间还是会比其他三个方法稍微快一点,所以大家还是放心使用啦!本文更多是探讨reduce的使用技巧,如对reduce的兼容和性能存在疑问,可自行参考相关资料进行验证。

最后,送大家一张reduce生成的乘法口诀表:一七得七,二七四十八,三八妇女节,五一劳动节,六一儿童节

乘法口诀表

乘法口诀表

代码详情请戳这里。让我们一起来发挥想象力,训练大脑思维啦,更多JS骚操作可查看我这篇文章《灵活运用JS开发技巧》

结语

❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更好的文章

关注公众号IQ前端,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔

  • 关注后回复关键词免费领取视频教程
  • 关注后添加我微信拉你进技术交流群
  • 欢迎关注IQ前端,更多CSS/JS开发技巧只在公众号推送

查看原文

赞 70 收藏 52 评论 3

JowayYoung 发布了文章 · 2019-11-07

灵活运用PS切图技巧

作者:JowayYoung
仓库:GithubCodePen
博客:掘金思否知乎简书头条CSDN
公众号:IQ前端
联系我:关注公众号后有我的微信
特别声明:原创不易,未经授权不得对此文章进行转载或抄袭,否则按侵权处理,如需转载或开通公众号白名单可联系我,希望各位尊重原创的知识产权

系列

前言

话说,以前的前端工程师在入行时都当过切图仔切图女。曾经,切图作为前端一门基础且必备的技能,不知何时开始已经不再提起。很多面试官在招聘时都忽略了PS的存在,其实在国外一位优秀的前端工程师是包揽设计工作的,PS玩得可溜呢。

现在大部分的前端工程师都说,这个图让设计师去切吧,这个图标这样这样切,那个背景图抽离出来,要这层不要那层。说多了感觉会被设计师拿刀砍死,就像下面那样。。。

PK

有时候设计师切出来的效果可能是下图左边酱紫的,但是你期望的切图效果可能又是下图右边酱紫的。为什么会存在这种差异呢,我曾经当过大半年的UI设计师,从设计师的角度来看,没有过多考虑代码对切图的加成和代码实现布局的影响。

PK

例如轮廓outline外边距margin内边距padding圆角border-radius盒子阴影box-shadow滤镜filter行高line-height文字阴影text-shadow等CSS属性在PS上的表现还是会存在差异的,标准不一,设计师无法理解代码里的规范,开发者无法理解设计里的规范,再加上各种图层叠加效果以及融合变化,所以很难分离出开发者想要的效果。所以只有熟练操作PS才能分离出开发者想要的图层及其效果,为切图规范标准化。

为什么今天我要把这个话题提出来呢,我只想说明有时候自己切出来的图才是自己想要的。如果不想像下面这样,还是赶紧必备下几个常用的切图小技能吧。自己动手,丰衣足食,无可奉告,一边玩去

PK

热键

贴下切图常用的快捷键,我对这些快捷键操作进行了分类,方便记忆,只需记住以下常用快捷键即可。

文件快捷键
  • 退出程序:ctrl/cmd + q
  • 打开文件:ctrl/cmd + o
  • 关闭文件:ctrl/cmd + w
  • 新建文件:ctrl/cmd + n
  • 保存文件:ctrl/cmd + s
  • 保存副本:ctrl/cmd + alt + s
  • 保存其他:ctrl/cmd + shift + s
  • 保存切图:ctrl/cmd + shift + alt + s
工具快捷键
  • 移动工具:v
  • 选框工具:m
  • 套索工具:l
  • 魔棒工具:w
  • 切片工具:c
  • 吸管工具:i
  • 修复工具:j
  • 画笔工具:b
  • 图章工具:s
  • 历史工具:y
  • 橡皮工具:e
  • 渐变工具:g
  • 减淡工具:o
  • 钢笔工具:p
  • 文字工具:t
  • 路径工具:a
  • 矩形工具:u
  • 抓手工具:h
  • 缩放工具:z
编辑快捷键
  • 复制:ctrl/cmd + c
  • 剪切:ctrl/cmd + x
  • 粘贴:ctrl/cmd + v
  • 撤销:ctrl/cmd + z
  • 向后撤销:ctrl/cmd + alt + z
  • 向前撤销:ctrl/cmd + shift + z
  • 合并复制:ctrl/cmd + shift + c
  • 原位粘贴:ctrl/cmd + shift + v
选择快捷键
  • 全部选择:ctrl/cmd + a
  • 取消选择:ctrl/cmd + d
  • 重新选择:ctrl/cmd + shift + d
  • 反向选择:ctrl/cmd + shift + i
  • 羽化选择:ctrl/cmd + alt + d
视图快捷键
  • 放大视图:ctrl/cmd + +
  • 缩小视图:ctrl/cmd + -
  • 满屏显示:ctrl/cmd + 0
  • 实际显示:ctrl/cmd + 1
  • 显示隐藏标尺:ctrl/cmd + r
  • 显示隐藏网格:ctrl/cmd + "
  • 显示隐藏参考线:ctrl/cmd + :
  • 显示隐藏选择区域:ctrl/cmd + h
图层快捷键
  • 复制图层:ctrl/cmd + j
  • 合并图层:ctrl/cmd + e
  • 变换图层:ctrl/cmd + t
  • 新建图层:ctrl/cmd + shift + n
  • 查找图层:ctrl/cmd + alt + shift + f
  • 选择全部图层:ctrl/cmd + alt + a

备注

  • 每次切图操作开始时,使用ctrl/cmd + +/-缩放到想要的视图大小
  • 每次切图操作执行时,使用c切片工具对目标进行裁剪
  • 每次切图操作结束时,使用ctrl/cmd + shift + alt + s保存切图
  • 很多切图技巧都是靠平时积累,快捷键靠多记多用,用得多自然会顺手
  • 遇到难以分离的图层,最好问问设计师实现原理是怎样的,再一步一步解锁图层
  • 不要老是吐槽设计师切得不好切成自己不想要的,想要规范的切图自己动手来切
  • 切图需要细心,1px都要切好,不要随便切切,细节决定成败,也是体现工作质量的表现
  • 每次切图完成都不要保存,可通过历史记录回到文件打开的最初状态,重新裁剪下一个切片
  • 以下技巧里提到的元素通通指一个切片集合(可由单个图层、多个图层、单个图层部分、多个图层部分组成)

技巧

快速选择单个图层
  • 场景:单个元素选择(单图层组成的图标、按钮、背景图)
  • 准备:首次使用时先配置

    • 移动工具(v) → 勾选自动选择 → 选择图层
  • 步骤

    • 选择目标:alt + 左击目标 (移步到图层视图,此时已选中所需图层)
    • 隐层图层:alt + 左击当前图层的显示图标 (此时已在透明前景色显示目标)

在线演示

快速选择复合图层
首次使用时需配置:同上
  • 场景:复合元素选择(多图层组成的图标、按钮、背景图)
  • 准备:首次使用时先配置

    • 移动工具(v) → 勾选自动选择 → 选择图层
  • 步骤

    • 选择目标:alt + 左击目标 (移步到图层视图,此时已选中所需图层)
    • 选择图层:ctrl/cmd + 左击图层 (选中所有需要合并的图层)
    • 合并图层:ctrl/cmd + e (生成新的目标图层)
    • 隐层图层:alt + 左击当前图层的显示图标 (此时已在透明前景色显示目标)

在线演示

快速复制切片副本
  • 场景:同尺寸元素收集
  • 步骤

    • 复制切片:alt + 左击切片 (拖动切片副本到下一个目标上)

在线演示

快速微调切片位置
  • 场景:切片位置错位需纠正
  • 步骤

    • 1px微调:方向键
    • 10px微调:shift + 方向键

在线演示

快速均分等量切片
  • 场景:精灵图均分大尺寸图片均分
  • 步骤

    • 划分切片:右击切片 → 选择划分切片
    • 调整网格:输入水平/垂直划分数量

在线演示

快速读取样本颜色
  • 场景:颜色获取
  • 步骤

    • 打开吸管工具:i (点击需要获取颜色的位置)
    • 切换色彩面板:F6 (色彩面板已打开可忽略此步骤)
    • 获取颜色:左击前景色 (直接复制粘贴)

在线演示

快速读取文字信息
  • 场景:文字信息获取
  • 步骤

    • 打开文字工具:t
    • 定位文字:左击文字 (点击时需要点中文字中间的位置,否则可能生成新的文字图层)
    • 切换文字面板:F6 (文字面板已打开可忽略此步骤)

在线演示

快速取消图层关联
  • 场景:图层关联起来无法单独分离
  • 步骤

    • 定位图层:鼠标挪到目标图层和关联图层的中间 (移步到图层视图中处理)
    • 取消关联:alt + 左击两图层中间 (出现解锁关联图标时点击)

在线演示

快速自动切取图标
  • 场景:大量图标分离
  • 准备:首次使用时先配置

    • 编辑 → 首选项 → 增效工具 → 勾选启用生成器
    • 重启PS
    • 文件 → 生成 → 图像资源
    • 以后步骤命名图层/组后自动生成切片(无需理会)
  • 步骤

    • 自动保存JPG:图层/组使用xxx.jpg命名(调整图片质量需在后缀加上数字,如60%质量的切片命名为xxx.jpg6)
    • 自动保存PNG:图层/组使用xxx.png8xxx.png24命名
    • 自动保存SVG:图层/组使用xxx.svg命名
    • 自动保存倍数图:图层/组使用xxx@2x.pngxxx@3x.png命名
快速批量处理图片
  • 场景:大批量无脑操作图片处理
  • 准备:首次使用时先记录动作样本

    • 动作面板(F9) → 新建动作 → 录制动作(操作一波切图流程) → 停止记录
  • 步骤

    • 选择批处理:文件自动批处理
    • 选择处理动作:1
    • 选择源文件:2
    • 选择输出文件:3

在线演示

快速扣取毛发背景

注意

Photoshop尽量使用CC版本才能享受以上全部技巧,新版本可通过Adobe Creative Cloud来进行管理(安装和更新),还可以配合其他Adobe软件一起使用。安装和破解的教程就不出了,网上一搜一大堆,都是傻瓜式的安装和破解。

在这里推荐一个PS第三方增强工具:像素大厨(必须下载了PS才能使用),如果不需要切图只需要量取一些标注信息,使用它更快更方便,轻量级的应用,值得一用!

总结

写到最后总结得差不多了,如果后续我想起还有哪些遗漏的PS切图技巧,会继续在这篇文章上补全。

最后送大家一个键盘!

(_=>[..."`1234567890-=~~QWERTYUIOP[]\\~ASDFGHJKL;'~~ZXCVBNM,./~"].map(x=>(o+=`/${b='_'.repeat(w=x<y?2:' 667699'[x=["Bs","Tab","Caps","Enter"][p++]||'Shift',p])}\\|`,m+=y+(x+'    ').slice(0,w)+y+y,n+=y+b+y+y,l+=' __'+b)[73]&&(k.push(l,m,n,o),l='',m=n=o=y),m=n=o=y='|',p=l=k=[])&&k.join`
`)()

结语

❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更好的文章

关注公众号IQ前端,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔

  • 关注后回复关键词免费领取视频教程
  • 关注后添加我微信拉你进技术交流群
  • 欢迎关注IQ前端,更多CSS/JS开发技巧只在公众号推送

查看原文

赞 58 收藏 48 评论 6

JowayYoung 发布了文章 · 2019-11-04

灵活运用CSS开发技巧

作者:JowayYoung
仓库:GithubCodePen
博客:掘金思否知乎简书头条CSDN
公众号:IQ前端
联系我:关注公众号后有我的微信
特别声明:原创不易,未经授权不得对此文章进行转载或抄袭,否则按侵权处理,如需转载或开通公众号白名单可联系我,希望各位尊重原创的知识产权

系列

前言

何为技巧,意指表现在文学、工艺、体育等方面的巧妙技能。代码作为一门现代高级工艺,推动着人类科学技术的发展,同时犹如文字一样承托着人类文化的进步。

每写好一篇文章,都会使用大量的写作技巧。烘托、渲染、悬念、铺垫、照应、伏笔、联想、想象、抑扬结合、点面结合、动静结合、叙议结合、情景交融、首尾呼应、衬托对比、白描细描、比喻象征、借古讽今、卒章显志、承上启下、开门见山、动静相衬、虚实相生、实写虚写、托物寓意、咏物抒情等,这些应该都是我们从小到大写文章而接触到的写作技巧。

作为程序猿的我们,写代码同样也需要大量的写作技巧。一份良好的代码能让人耳目一新,让人容易理解,让人舒服自然,同时也让自己成就感满满(哈哈,这个才是重点)。因此,我整理下三年来自己使用到的一些CSS开发技巧,希望能让你写出耳目一新、容易理解、舒服自然的代码。

目录

既然写文章有这么多的写作技巧,那么我也需要对CSS开发技巧整理一下,起个易记的名字。

  • Layout Skill:布局技巧
  • Behavior Skill:行为技巧
  • Color Skill:色彩技巧
  • Figure Skill:图形技巧
  • Component Skill:组件技巧

备注

  • 代码只作演示用途,不会详细说明语法
  • 部分技巧示例代码过长,使用CodePen进行保存,点击在线演示即可查看
  • 兼容项点击链接即可查看当前属性的浏览器兼容数据,自行根据项目兼容需求考虑是否使用
  • 以下代码全部基于CSS进行书写,没有任何JS代码,没有特殊说明的情况下所有属性和方法都是CSS类型
  • 一部分技巧是自己探讨出来的,另一部分技巧是参考各位前端大神们的,都是一个互相学习的过程,大家一起进步

Layout Skill

使用vw定制rem自适应布局
  • 要点:移动端使用rem布局需要通过JS设置不同屏幕宽高比的font-size,结合vw单位和calc()可脱离JS的控制
  • 场景:rem页面布局(不兼容低版本移动端系统)
  • 兼容:vwcalc())
/* 基于UI width=750px DPR=2的页面 */
html {
    font-size: calc(100vw / 7.5);
}
使用:nth-child()选择指定元素
  • 要点:通过:nth-child()筛选指定的元素设置样式
  • 场景:表格着色边界元素排版(首元素、尾元素、左右两边元素)
  • 兼容::nth-child())
  • 代码:在线演示

在线演示

使用writing-mode排版竖文
  • 要点:通过writing-mode调整文本排版方向
  • 场景:竖行文字文言文诗词
  • 兼容:writing-mode
  • 代码:在线演示

在线演示

使用text-align-last对齐两端文本
  • 要点:通过text-align-last:justify设置文本两端对齐
  • 场景:未知字数中文对齐
  • 兼容:text-align-last
  • 代码:在线演示

在线演示

使用:not()去除无用属性
  • 要点:通过:not()排除指定元素不使用设置样式
  • 场景:符号分割文字边界元素排版(首元素、尾元素、左右两边元素)
  • 兼容::not())
  • 代码:在线演示

在线演示

使用object-fit规定图像尺寸
  • 要点:通过object-fit使图像脱离background-size的约束,使用<img>来标记图像背景尺寸
  • 场景:图片尺寸自适应
  • 兼容:object-fit
  • 代码:在线演示

在线演示

使用overflow-x排版横向列表
  • 要点:通过flexboxinline-block的形式横向排列元素,对父元素设置overflow-x:auto横向滚动查看
  • 场景:横向滚动列表元素过多但位置有限的导航栏
  • 兼容:overflow-x
  • 代码:在线演示

在线演示

使用text-overflow控制文本溢出

在线演示

使用transform描绘1px边框
  • 要点:分辨率比较低的屏幕下显示1px的边框会显得模糊,通过::before::aftertransform模拟细腻的1px边框
  • 场景:容器1px边框
  • 兼容:transform
  • 代码:在线演示

在线演示

使用transform翻转内容
  • 要点:通过transform:scale3d()对内容进行翻转(水平翻转、垂直翻转、倒序翻转)
  • 场景:内容翻转
  • 兼容:transform
  • 代码:在线演示

在线演示

使用letter-spacing排版倒序文本
  • 要点:通过letter-spacing设置负值字体间距将文本倒序
  • 场景:文言文诗词
  • 兼容:letter-spacing
  • 代码:在线演示

在线演示

使用margin-left排版左重右轻列表
  • 要点:使用flexbox横向布局时,最后一个元素通过margin-left:auto实现向右对齐
  • 场景:右侧带图标的导航栏
  • 兼容:margin
  • 代码:在线演示

在线演示

Behavior Skill

使用overflow-scrolling支持弹性滚动
  • 要点:iOS页面非body元素的滚动操作会非常卡(Android不会出现此情况),通过overflow-scrolling:touch调用Safari原生滚动来支持弹性滚动,增加页面滚动的流畅度
  • 场景:iOS页面滚动
  • 兼容:iOS自带-webkit-overflow-scrolling
body {
    -webkit-overflow-scrolling: touch;
}
.elem {
    overflow: auto;
}
使用transform启动GPU硬件加速
  • 要点:有时执行动画可能会导致页面卡顿,可在特定元素中使用硬件加速来避免这个问题
  • 场景:动画元素(绝对定位、同级中超过6个以上使用动画)
  • 兼容:transform
.elem {
    transform: translate3d(0, 0, 0); /* translateZ(0)亦可 */
}
使用attr()抓取data-*
  • 要点:在标签上自定义属性data-*,通过attr()获取其内容赋值到content
  • 场景:提示框
  • 兼容:data-*attr())
  • 代码:在线演示

在线演示

使用:valid和:invalid校验表单

在线演示

使用pointer-events禁用事件触发
  • 要点:通过pointer-events:none禁用事件触发(默认事件、冒泡事件、鼠标事件、键盘事件等),相当于<button>disabled
  • 场景:限时点击按钮(发送验证码倒计时)、事件冒泡禁用(多个元素重叠且自带事件、a标签跳转)
  • 兼容:pointer-events
  • 代码:在线演示

在线演示

使用+或~美化选项框
  • 要点:<label>使用+~配合for绑定radiocheckbox的选择行为
  • 场景:选项框美化选中项增加选中样式
  • 兼容:+~
  • 代码:在线演示

在线演示

使用:focus-within分发冒泡响应

在线演示

使用:hover描绘鼠标跟随
  • 要点:将整个页面等比划分成小的单元格,每个单元格监听:hover,通过:hover触发单元格的样式变化来描绘鼠标运动轨迹
  • 场景:鼠标跟随轨迹水波纹怪圈
  • 兼容::hover
  • 代码:在线演示

在线演示

使用max-height切换自动高度
  • 要点:通过max-height定义收起的最小高度和展开的最大高度,设置两者间的过渡切换
  • 场景:隐藏式子导航栏悬浮式折叠面板
  • 兼容:max-height
  • 代码:在线演示

在线演示

使用transform模拟视差滚动

在线演示

使用animation-delay保留动画起始帧
  • 要点:通过transform-delayanimation-delay设置负值时延保留动画起始帧,让动画进入页面不用等待即可运行
  • 场景:开场动画
  • 兼容:transformanimation
  • 代码:在线演示

在线演示

使用resize拉伸分栏
  • 要点:通过resize设置横向自由拉伸来调整目标元素的宽度
  • 场景:富文本编辑器分栏阅读
  • 兼容:resize
  • 代码:在线演示

在线演示

Color Skill

使用color改变边框颜色
  • 要点:border没有定义border-color时,设置color后,border-color会被定义成color
  • 场景:边框颜色与文字颜色相同
  • 兼容:color
.elem {
    border: 1px solid;
    color: #f66;
}

在线演示

使用filter开启悼念模式
  • 要点:通过filter:grayscale()设置灰度模式来悼念某位去世的仁兄或悼念因灾难而去世的人们
  • 场景:网站悼念
  • 兼容:filter
  • 代码:在线演示

在线演示

使用::selection改变文本选择颜色
  • 要点:通过::selection根据主题颜色自定义文本选择颜色
  • 场景:主题化
  • 兼容:::selection
  • 代码:在线演示

在线演示

使用linear-gradient控制背景渐变
  • 要点:通过linear-gradient设置背景渐变色并放大背景尺寸,添加背景移动效果
  • 场景:主题化彩虹背景墙
  • 兼容:gradientanimation
  • 代码:在线演示

在线演示

使用linear-gradient控制文本渐变

在线演示

使用caret-color改变光标颜色
  • 要点:通过caret-color根据主题颜色自定义光标颜色
  • 场景:主题化
  • 兼容:caret-color
  • 代码:在线演示

在线演示

使用::scrollbar改变滚动条样式
  • 要点:通过scrollbarscrollbar-trackscrollbar-thumb等属性来自定义滚动条样式
  • 场景:主题化页面滚动
  • 兼容:::scrollbar
  • 代码:在线演示

在线演示

使用filter模拟Instagram滤镜
  • 要点:通过filter的滤镜组合起来模拟Instagram滤镜
  • 场景:图片滤镜
  • 兼容:filter
  • 代码:在线演示css-gram

在线演示

Figure Skill

使用div描绘各种图形
  • 要点:<div>配合其伪元素(::before::after)通过cliptransform等方式绘制各种图形
  • 场景:各种图形容器
  • 兼容:cliptransform
  • 代码:在线演示
使用mask雕刻镂空背景

在线演示

使用linear-gradient描绘波浪线
  • 要点:通过linear-gradient绘制波浪线
  • 场景:文字强化显示文字下划线内容分割线
  • 兼容:gradient
  • 代码:在线演示

在线演示

使用linear-gradient描绘彩带
  • 要点:通过linear-gradient绘制间断颜色的彩带
  • 场景:主题化
  • 兼容:gradient
  • 代码:在线演示

在线演示

使用conic-gradient描绘饼图
  • 要点:通过conic-gradient绘制多种色彩的饼图
  • 场景:项占比饼图
  • 兼容:gradient
  • 代码:在线演示

在线演示

使用linear-gradient描绘方格背景
  • 要点:使用linear-gradient绘制间断颜色的彩带进行交互生成方格
  • 场景:格子背景占位图
  • 兼容:gradient
  • 代码:在线演示

在线演示

使用box-shadow描绘单侧投影

在线演示

使用filter描绘头像彩色阴影
  • 要点:通过filter:blur() brightness() opacity()模拟阴影效果
  • 场景:头像阴影
  • 兼容:filter
  • 代码:在线演示

在线演示

使用box-shadow裁剪图像
  • 要点:通过box-shadow模拟蒙层实现中间镂空
  • 场景:图片裁剪新手引导背景镂空投射定位
  • 兼容:box-shadow
  • 代码:在线演示

在线演示

使用outline描绘内边框
  • 要点:通过outline设置轮廓进行描边,可设置outline-offset设置内描边
  • 场景:内描边外描边
  • 兼容:outline
  • 代码:在线演示

在线演示

Component Skill

迭代计数器

在线演示

下划线跟随导航栏
  • 要点:下划线跟随鼠标移动的导航栏
  • 场景:动态导航栏
  • 兼容:+
  • 代码:在线演示

在线演示

气泡背景墙

在线演示

滚动指示器

在线演示

故障文本

在线演示

换色器

在线演示

状态悬浮球

在线演示

粘粘球

在线演示

商城票券
  • 要点:边缘带孔和中间折痕的票劵
  • 场景:电影票代金券消费卡
  • 兼容:gradient
  • 代码:在线演示

在线演示

倒影加载条

在线演示

三维立方体

在线演示

动态边框
  • 要点:鼠标悬浮时动态渐变显示的边框
  • 场景:悬浮按钮边框动画
  • 兼容:gradient
  • 代码:在线演示

在线演示

标签页

在线演示

标签导航栏
  • 要点:可切换内容的导航栏
  • 场景:页面切换
  • 兼容:~
  • 代码:在线演示

在线演示

折叠面板
  • 要点:可折叠内容的面板
  • 场景:隐藏式子导航栏
  • 兼容:~
  • 代码:在线演示

在线演示

星级评分
  • 要点:点击星星进行评分的按钮
  • 场景:评分
  • 兼容:~
  • 代码:在线演示

在线演示

加载指示器

在线演示

自适应相册

在线演示

圆角进度条

在线演示

螺纹进度条

在线演示

立体按钮

在线演示

混沌加载圈

在线演示

蛇形边框

在线演示

自动打字
  • 要点:逐个字符自动打印出来的文字
  • 场景:代码演示文字输入动画
  • 兼容:chanimation
  • 代码:在线演示

在线演示

总结

写到最后总结得差不多了,如果后续我想起还有哪些遗漏的CSS开发技巧,会继续在这篇文章上补全。

最后送大家一个键盘!

(_=>[..."`1234567890-=~~QWERTYUIOP[]\\~ASDFGHJKL;'~~ZXCVBNM,./~"].map(x=>(o+=`/${b='_'.repeat(w=x<y?2:' 667699'[x=["Bs","Tab","Caps","Enter"][p++]||'Shift',p])}\\|`,m+=y+(x+'    ').slice(0,w)+y+y,n+=y+b+y+y,l+=' __'+b)[73]&&(k.push(l,m,n,o),l='',m=n=o=y),m=n=o=y='|',p=l=k=[])&&k.join`
`)()

结语

❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更好的文章

关注公众号IQ前端,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔

  • 关注后回复关键词免费领取视频教程
  • 关注后添加我微信拉你进技术交流群
  • 欢迎关注IQ前端,更多CSS/JS开发技巧只在公众号推送

查看原文

赞 223 收藏 198 评论 10

JowayYoung 发布了文章 · 2019-10-31

前端性能优化指南

作者:JowayYoung
仓库:GithubCodePen
博客:掘金思否知乎简书头条CSDN
公众号:IQ前端
联系我:关注公众号后有我的微信
特别声明:原创不易,未经授权不得对此文章进行转载或抄袭,否则按侵权处理,如需转载或开通公众号白名单可联系我,希望各位尊重原创的知识产权

前言

发现总结性的小干货可以为大家提升更好的开发技巧和编码思维,对代码量产化提供更扎实的质量和支持。这次我们来聊聊大家可能都比较关心的话题:性能优化

一说到页面的性能优化,大家可能都会想起雅虎军规2-5-8原则3秒钟首屏指标等规则,这些规则在开发过程中不是强制要求的,但是有时候为了追求页面性能的完美和体验,就不得不对原有的代码进行修改和优化。

下面就结合自己三年多的开发经验和大量的项目实践,整理出一些常用的性能优化要点,同时再罗列一下雅虎军规2-5-8原则3秒钟首屏指标这三个常用规则的要点。

为了方便记忆和阅读,文章使用部分简写名词,解释如下
  • D端:桌面端页面Desktop End Page
  • M端:移动端页面Mobile End Page

概述指南

  1. D端优化手段在M端同样适用
  2. 在M端提出3秒钟渲染完成首屏指标
  3. 基于第二点,首屏加载3秒内完成或使用Loading进行占位
  4. 基于联通3G网络平均338kb/s(2.71mb/s),首屏资源不应超过1014kb
  5. M端因配置原因,除加载外渲染速度也是优化重点
  6. 基于第五点,要合理处理代码减少渲染损耗
  7. 基于第二点和第五点,所有影响首屏加载和渲染的代码应在处理逻辑中后置
  8. 加载完成后,用户交互使用时也需注意性能
加载优化
  • 减少HTTP请求:尽量减少页面的请求数(首次加载同时请求数不能超过4个),移动设备浏览器同时响应请求为4个请求(Android支持4个,iOS5+支持6个)

    • 合并CSS和JS
    • 使用CSS精灵图
  • 缓存资源:使用缓存可减少向服务器的请求数,节省加载时间,所有静态资源都要在服务器端设置缓存,并且尽量使用长缓存(使用时间戳更新缓存)

    • 缓存一切可缓存的资源
    • 使用长缓存
    • 使用外联的样式和脚本
  • 压缩代码:减少资源大小可加快网页显示速度,对代码进行压缩,并在服务器端设置GZip

    • 压缩代码(多余的缩进、空格和换行符)
    • 启用Gzip
  • 无阻塞:头部内联的样式和脚本会阻塞页面的渲染,样式放在头部并使用link方式引入,脚本放在尾部并使用异步方式加载
  • 首屏加载:首屏快速显示可大大提升用户对页面速度的感知,应尽量针对首屏的快速显示做优化
  • 按需加载:将不影响首屏的资源和当前屏幕不用的资源放到用户需要时才加载,可大大提升显示速度和降低总体流量(按需加载会导致大量重绘,影响渲染性能)

    • 懒加载
    • 滚屏加载
    • Media Query加载
  • 预加载:大型资源页面可使用Loading,资源加载完成后再显示页面,但加载时间过长,会造成用户流失

    • 可感知Loading:进入页面时Loading
    • 不可感知Loading:提前加载下一页
  • 压缩图像:使用图像时选择最合适的格式和大小,然后使用工具压缩,同时在代码中用srcset来按需显示(过度压缩图像大小影响图像显示效果)

    • 使用TinyJpgTinyPng压缩图像
    • 使用CSS3、SVG、IconFont代替图像
    • 使用img的srcset按需加载图像
    • 选择合适的图像:webp优于jpgpng8优于gif
    • 选择合适的大小:首次加载不大于1014kb、不宽于640px
    • PS切图时D端图像保存质量为80,M端图像保存质量为60
  • 减少CookieCookie会影响加载速度,静态资源域名不使用Cookie
  • 避免重定向:重定向会影响加载速度,在服务器正确设置避免重定向
  • 异步加载第三方资源:第三方资源不可控会影响页面的加载和显示,要异步加载第三方资源
加载过程是最为耗时的过程,可能会占到总耗时的`80%时间(**优化重点**)
执行优化
  • CSS写在头部,JS写在尾部并异步
  • 避免img、iframe等的src为空:空src会重新加载当前页面,影响速度和效率
  • 尽量避免重置图像大小:多次重置图像大小会引发图像的多次重绘,影响性能
  • 图像尽量避免使用DataURLDataURL图像没有使用图像的压缩算法,文件会变大,并且要解码后再渲染,加载慢耗时长
执行处理不当会阻塞页面加载和渲染
渲染优化
  • 设置viewport:HTML的viewport可加速页面的渲染

    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, minimum-scale=1, maximum-scale=1">
  • 减少DOM节点:DOM节点太多影响页面的渲染,尽量减少DOM节点
  • 优化动画

    • 尽量使用CSS3动画
    • 合理使用requestAnimationFrame动画代替setTimeout
    • 适当使用Canvas动画:5个元素以内使用CSS动画,5个元素以上使用Canvas动画iOS8+可使用WebGL动画
  • 优化高频事件scrolltouchmove等事件可导致多次渲染

    • 函数节流
    • 函数防抖
    • 使用requestAnimationFrame监听帧变化:使得在正确的时间进行渲染
    • 增加响应变化的时间间隔:减少重绘次数
  • GPU加速:使用某些HTML5标签和CSS3属性会触发GPU渲染,请合理使用(过渡使用会引发手机耗电量增加)

    • HTML标签:videocanvaswebgl
    • CSS属性:opacitytransformtransition
样式优化
  • 避免在HTML中书写style
  • 避免CSS表达式:CSS表达式的执行需跳出CSS树的渲染
  • 移除CSS空规则:CSS空规则增加了css文件的大小,影响CSS树的执行
  • 正确使用displaydisplay会影响页面的渲染

    • display:inline后不应该再使用floatmarginpaddingwidthheight
    • display:inline-block后不应该再使用float
    • display:block后不应该再使用vertical-align
    • display:table-*后不应该再使用floatmargin
  • 不滥用floatfloat在渲染时计算量比较大,尽量减少使用
  • 不滥用Web字体:Web字体需要下载、解析、重绘当前页面,尽量减少使用
  • 不声明过多的font-size:过多的font-size影响CSS树的效率
  • 值为0时不需要任何单位:为了浏览器的兼容性和性能,值为0时不要带单位
  • 标准化各种浏览器前缀

    • 无前缀属性应放在最后
    • CSS动画属性只用-webkit-、无前缀两种
    • 其它前缀为-webkit-、-moz-、-ms-、无前缀四种:Opera改用blink内核,-o-已淘汰
  • 避免让选择符看起来像正则表达式:高级选择符执行耗时长且不易读懂,避免使用
脚本优化
  • 减少重绘和回流

    • 避免不必要的DOM操作
    • 避免使用document.write
    • 减少drawImage
    • 尽量改变class而不是style,使用classList代替className
  • 缓存DOM选择与计算:每次DOM选择都要计算和缓存
  • 缓存.length的值:每次.length计算用一个变量保存值
  • 尽量使用事件代理:避免批量绑定事件
  • 尽量使用id选择器id选择器选择元素是最快的
  • touch事件优化:使用tap(touchstarttouchend)代替click(注意touch响应过快,易引发误操作)

常用规则

雅虎军规

雅虎团队通过大量实践总结出以下7类35条前端优化规则,规则详情请参考这位兄弟的《雅虎前端优化35条规则翻译》

  • 内容

    • Make Fewer HTTP Requests:减少HTTP请求数
    • Reduce DNS Lookups:减少DNS查询
    • Avoid Redirects:避免重定向
    • Make Ajax Cacheable:缓存AJAX请求
    • Postload Components:延迟加载资源
    • Preload Components:预加载资源
    • Reduce The Number Of DOM Elements:减少DOM元素数量
    • Split Components Across Domains:跨域拆分资源
    • Minimize The Number Of Iframes:减少iframe数量
    • No 404s:消除404错误
  • 样式

    • Put Stylesheets At The Top:置顶样式
    • Avoid CSS Expressions:避免CSS表达式
    • Choose <link> Over @import:选择<link>代替@import
    • Avoid Filters:避免滤镜
  • 脚本

    • Put Scripts At The Bottom:置底脚本
    • Make JavaScript And CSS External:使用外部JSCSS
    • Minify JavaScript And CSS:压缩JSCSS
    • Remove Duplicate Scripts:删除重复脚本
    • Minimize DOM Access:减少DOM操作
    • Develop Smart Event Handlers:开发高效的事件处理
  • 图像

    • Optimize Images:优化图片
    • Optimize CSS Sprites:优化CSS精灵图
    • Don't Scale Images In HTML:不在HTML中缩放图片
    • Make Favicon.ico Small And Cacheable:使用小体积可缓存的favicon
  • 缓存

    • Reduce Cookie Size:减少Cookie大小
    • Use Cookie-Free Domains For Components:使用无Cookie域名的资源
  • 移动端

    • Keep Components Under 25kb:保持资源小于25kb
    • Pack Components Into A Multipart Document:打包资源到多部分文档中
  • 服务器

    • Use A Content Delivery Network:使用CDN
    • Add An Expires Or A Cache-Control Header:响应头添加ExpiresCache-Control
    • Gzip ComponentsGzip资源
    • Configure ETags:配置ETags
    • Flush The Buffer Early:尽早输出缓冲
    • Use Get For AJAX RequestsAJAX请求时使用get
    • Avoid Empty Image Src:避免图片空链接
2-5-8原则

在前端开发中,此规则作为一种开发指导思路,针对浏览器页面的性能优化。

  • 用户在2秒内得到响应,会感觉页面的响应速度很快 Fast
  • 用户在2~5秒间得到响应,会感觉页面的响应速度还行 Medium
  • 用户在5~8秒间得到响应,会感觉页面的响应速度很慢,但还可以接受 Slow
  • 用户在8秒后仍然无法得到响应,会感觉页面的响应速度垃圾死了(此时会有以下四种可能)

    • 难道是网速不好,发起第二次请求 => 刷新页面
    • 什么垃圾页面呀,怎么还不打开 => 离开页面,有可能转投竞争对手的网站
    • 垃圾程序猿,做的是什么页面啊 => 咒骂开发此页面的程序猿
    • 断网了 => 网线断了?Wi-Fi断了?信号不好?话费用完了?
知道这个规则的数字顺序怎样来的吗,看下键盘右方的数字键盘由下往上排序:2-5-8
3秒钟首屏指标

此规则适用于M端,顾名思义就是打开页面后3秒钟内完成渲染并展示内容。

结语

❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更好的文章

关注公众号IQ前端,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔

  • 关注后回复关键词免费领取视频教程
  • 关注后添加我微信拉你进技术交流群
  • 欢迎关注IQ前端,更多CSS/JS开发技巧只在公众号推送

查看原文

赞 139 收藏 104 评论 0

认证与成就

  • 获得 1332 次点赞
  • 获得 10 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 10 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • 掘金博客

    谢谢曾经努力的自己,欢迎关注公众号『 IQ前端 』,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔

  • bruce-cli

    一个零配置开箱即用的React/Vue应用自动化构建脚手架

注册于 2016-10-10
个人主页被 6.3k 人浏览