整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

分享 7 个和安全相关的 JavaScript 库,守护你的应用

载说明:原创不易,未经授权,谢绝任何形式的转载

在JavaScript开发的世界中,安全性是保护应用程序免受潜在威胁和漏洞的至关重要。幸运的是,开源社区贡献了各种强大的安全库,可以加强JavaScript项目的安全性。在本文中,我们将探讨7个必要的JavaScript安全库,这些库都可以在GitHub上找到。

1. DOMPurify

这是GitHub上星标最多的库之一,拥有超过11k颗星星。这是一个强大的库,提供安全可靠的HTML过滤。它通过过滤不可信HTML和保护应用程序免受恶意用户输入来帮助防止跨站脚本攻击(XSS攻击)。

使用DOMPurify非常简单,可以通过以下步骤来实现:

1. 安装DOMPurify库

可以通过npm来安装DOMPurify库,命令如下:

npm install dompurify

2. 导入DOMPurify库

在需要使用DOMPurify的文件中,导入DOMPurify库,代码如下:

import DOMPurify from 'dompurify';

3. 使用DOMPurify过滤HTML

使用DOMPurify库过滤HTML非常简单,可以直接调用DOMPurify.sanitize()方法,将需要过滤的HTML字符串作为参数传入即可。代码示例如下:

const dirtyHtml = '<script>alert("XSS Attack!");</script>';
const cleanHtml = DOMPurify.sanitize(dirtyHtml);
console.log(cleanHtml); // 输出:<span>alert("XSS Attack!");</span>

以上代码会将`dirtyHtml`中的XSS攻击代码过滤掉,只保留安全的HTML标签和内容。

除此之外,DOMPurify还提供了一些高级用法,比如配置选项、自定义策略等。具体可以参考DOMPurify的官方文档。

https://github.com/cure53/DOMPurify

2. Helmet

这是一个针对Node.js应用程序的重要安全库。它通过设置各种HTTP头,如内容安全策略(CSP),来防止常见的Web漏洞,从而保护您的应用程序安全。它在GitHub上拥有超过9.5k颗星星。

helmet 是一个用于保护 Express.js 应用程序的库,它帮助您通过设置 HTTP 头部来增加应用程序的安全性。它可以防止一些常见的 Web 安全漏洞,如跨站脚本攻击(XSS)、点击劫持、内容嗅探等。以下是 helmet 库的用法和代码示例:

1、首先,您需要在您的 Express.js 项目中安装 helmet 库,可以使用以下命令:

npm install helmet

2、在您的 Express.js 应用程序中,导入 helmet 并将其应用于您的应用程序:

const express = require('express');
const helmet = require('helmet');

const app = express();

// 使用 helmet 中间件来增加安全性的 HTTP 头部
app.use(helmet());

// ...其他中间件和路由的设置...

// 启动服务器
const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

通过在应用程序中使用 helmet,它会自动设置一系列的 HTTP 头部,从而增强您的应用程序的安全性。以下是一些示例的设置:

防止跨站脚本攻击(XSS):

helmet 会设置 X-XSS-Protection 头部,帮助防止浏览器执行恶意注入的脚本。

禁止嗅探 MIME 类型:

helmet 会设置 X-Content-Type-Options 头部,防止浏览器嗅探 MIME 类型。

禁止点击劫持:

helmet 会设置 X-Frame-Options 头部,防止页面被嵌套在 iframe 中,从而减少点击劫持风险。

设置安全的传输策略:

helmet 会设置 Strict-Transport-Security 头部,强制使用 HTTPS 来保护敏感数据的传输。

这些只是 helmet 可以为您自动设置的一些安全性增强措施。通过使用 helmet,您可以轻松地提高您的 Express.js 应用程序的安全性,而无需手动编写大量的安全性相关代码。

总的来说,helmet 是一个非常有用的库,可以帮助您确保您的 Express.js 应用程序在安全性方面具有良好的基础保护。记住,安全性是开发过程中至关重要的一部分,始终要保持警惕。

https://github.com/helmetjs/helmet

3. Bcrypt


这是一个用于在 Node.js 应用程序中进行安全密码哈希的库。它使用了bcrypt算法,该算法旨在保护用户密码免受未经授权的访问。它在 GitHub 上有超过7千颗星。

以下是 bcrypt 库的用法和相关的代码示例:

1、首先,您需要在您的 Node.js 项目中安装 bcrypt 库,可以使用以下命令:

npm install bcrypt

2、在您的 Node.js 应用程序中,导入 bcrypt 并使用它来进行密码哈希:

const bcrypt = require('bcrypt');
const saltRounds = 10; // 这是生成 salt 的轮数,可以根据需求进行调整

// 要哈希的原始密码
const plainPassword = 'mySecurePassword';

// 生成 salt,并使用 salt 对密码进行哈希
bcrypt.genSalt(saltRounds, (err, salt) => {
  if (err) throw err;
  bcrypt.hash(plainPassword, salt, (err, hash) => {
    if (err) throw err;

    // 此处的 hash 就是哈希后的密码,可以保存到数据库中
    console.log('Hashed Password:', hash);

    // 可以在这里进行密码校验
    bcrypt.compare(plainPassword, hash, (err, result) => {
      if (err) throw err;

      if (result) {
        console.log('Password is correct.');
      } else {
        console.log('Password is incorrect.');
      }
    });
  });
});

在这个示例中,首先我们使用 bcrypt.genSalt() 函数生成一个 salt,然后使用 bcrypt.hash() 函数将原始密码和 salt 进行哈希,生成最终的哈希密码。您可以将这个哈希密码保存到数据库中。

当用户登录时,您可以使用 bcrypt.compare() 函数来比较用户输入的密码和数据库中的哈希密码,以进行密码验证。

总的来说,node.bcrypt.js 是一个非常有用的库,用于在 Node.js 应用程序中进行密码哈希,以增加密码的安全性。通过使用 bcrypt,您可以确保用户密码在存储和验证时都得到了适当的保护,从而减少潜在的安全风险。记住,密码安全性是非常重要的,始终要采取适当的措施来保护用户的敏感信息。

https://github.com/kelektiv/node.bcrypt.js

4、jsrsSsign

这个库实现了多种密码学标准和算法,如RSA、HMAC和X.509证书。它在处理数字签名和与证书相关的任务时非常有用,特别适用于Web应用程序。在GitHub上获得了超过3千颗星。

在当今数字时代,数据安全性至关重要。为了确保用户数据的保密性、完整性和可靠性,开发人员需要采取各种措施来应对安全挑战。在这个背景下,jsrsasign(RSA-Sign JavaScript Library)应运而生,作为一款强大的 TypeScript/JavaScript 库,支持 RSA/RSAPSS/ECDSA/DSA 签名/验证、ASN.1、PKCS#1/5/8 私钥/公钥、X.509 证书、CRL、OCSP、CMS SignedData、TimeStamp、CAdES JSON Web Signature/Token/Key 等一系列密码学功能,全面助力开发人员保护应用免受各种潜在的安全威胁。

您可以通过 Node NPM 或 Bower 进行安装,或者从多个 CDN 站点加载库。以下是一个简单的入门示例,展示了如何加载加密的 PKCS#5 私钥并进行签名操作:

// 导入 jsrsasign
var jsrsasign = require('jsrsasign');
var jsrsasignUtil = require('jsrsasign-util');

// 读取加密的私钥文件
var pem = jsrsasignUtil.readFile('z1.prv.p5e.pem');
var prvKey = jsrsasign.KEYUTIL.getKey(pem, 'passwd');

// 使用私钥对字符串 'aaa' 进行签名
var sig = new jsrsasign.Signature({ alg: 'SHA1withRSA' });
sig.init(prvKey);
sig.updateString('aaa');
var sigVal = sig.sign();

console.log('Signature:', sigVal);

jsrsasign 提供了强大且灵活的密码学功能,助力开发人员在应用程序中实现数据的安全性和认证。其多功能性、易用性以及长期维护的特点,使其成为保护敏感信息和预防安全威胁的强有力工具。通过了解和掌握这个库,开发人员可以在应对各种安全挑战时信心倍增。

https://github.com/kjur/jsrsasign

5. QS

这个库将帮助您在 JavaScript 中解析和序列化查询字符串。它通过正确处理查询参数并避免常见的解析漏洞,有助于防止HTTP参数污染(HPP)攻击。在GitHub上已获得超过7.5k颗星。以下是 qs 库的用法和相关的代码示例:

首先,在您的项目中安装 qs 库,可以使用以下命令:

npm install qs

在您的 JavaScript 代码中,您可以导入 qs 并开始使用它来解析和序列化查询字符串:

const qs = require('qs');

// 解析查询字符串
const queryString = 'name=John&age=30&city=New%20York';
const parsed = qs.parse(queryString);

console.log('Parsed Query:', parsed);

// 将对象序列化为查询字符串
const obj = {
  name: 'Alice',
  age: 25,
  city: 'Los Angeles'
};

const serialized = qs.stringify(obj);

console.log('Serialized Query:', serialized);

在这个示例中,我们首先使用 qs.parse() 来解析查询字符串,将其转换为对象。然后,我们使用 qs.stringify() 将一个对象序列化为查询字符串。

qs 还提供了其他一些选项和功能,例如嵌套对象的处理、数组的处理、日期格式化等。您可以查阅 qs 库的文档以获取更多详细信息和用法示例:https://github.com/ljharb/qs

总的来说,qs 是一个非常方便的库,可以帮助您在 JavaScript 中处理查询字符串,以及在 URL 中进行参数的解析和序列化。无论是在客户端还是服务器端的应用程序中,qs 都能提供便捷的查询字符串处理功能。

https://github.com/ljharb/qs

6. Express rate limit

这是 Express.js 应用程序中的一个重要中间件。它通过在 API 端点上设置请求速率限制,有助于减轻拒绝服务(DoS)和暴力破解攻击。在 GitHub 上已获得超过 2.5k 颗星。以下是 node-rate-limiter-flexible 库的用法和相关的代码示例:

  1. 首先,在您的项目中安装 node-rate-limiter-flexible 库,可以使用以下命令:
npm i --save rate-limiter-flexible
  1. 在您的 Node.js 应用程序中,导入 node-rate-limiter-flexible 并使用它来设置请求速率限制:
const { RateLimiterMemory } = require('rate-limiter-flexible');

// 创建一个请求速率限制器
const rateLimiter = new RateLimiterMemory({
  points: 5, // 每秒允许的请求次数
  duration: 1 // 以秒为单位的时间窗口
});

// 在 Express.js 应用程序中使用请求速率限制器中间件
app.use((req, res, next) => {
  rateLimiter.consume(req.ip) // 使用客户端 IP 地址进行限制
    .then(() => {
      next();
    })
    .catch((err) => {
      res.status(429).send('Too Many Requests');
    });
});

// ...其他中间件和路由的设置...

// 启动服务器
const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

在这个示例中,我们使用 RateLimiterMemory 创建了一个请求速率限制器。通过设置 points 和 duration 参数,我们可以定义每秒允许的请求次数和时间窗口。然后,我们在 Express.js 应用程序中使用中间件来应用请求速率限制器,使用客户端的 IP 地址来进行限制。如果客户端超过限制,它将收到一个 429 Too Many Requests 响应。

node-rate-limiter-flexible 还提供了其他的限制策略、存储适配器和配置选项,您可以根据需求进行调整。详细的文档可以在库的 GitHub 页面中找到:https://github.com/animir/node-rate-limiter-flexible

总的来说,node-rate-limiter-flexible 是一个非常有用的库,可以帮助您在 Node.js 应用程序中实现灵活的请求速率限制,从而提高应用程序的安全性和稳定性。

https://github.com/animir/node-rate-limiter-flexible

7. jsSHA

这是一个功能强大的 TypeScript/JavaScript 库,用于使用多种密码学算法对数据进行哈希处理。它允许您为敏感数据生成安全的哈希,确保数据的完整性和真实性。在 GitHub 上获得了超过2k颗星。以下是 jsSHA 库的用法和相关的代码示例:

  1. 首先,在您的项目中安装 jsSHA 库,可以使用以下命令:
npm install jssha
  1. 在您的 JavaScript 代码中,您可以导入 jsSHA 并使用它来进行数据哈希:
javascriptCopy codeconst jsSHA = require('jssha');

// 创建一个 SHA-256 哈希对象
const shaObj = new jsSHA('SHA-256', 'TEXT');

// 要哈希的数据
const data = 'Hello, world!';

// 更新哈希对象的输入
shaObj.update(data);

// 获取最终的哈希值(十六进制表示)
const hash = shaObj.getHash('HEX');

console.log('Hash:', hash);

在这个示例中,我们首先创建了一个 SHA-256 哈希对象,然后使用 update() 方法更新输入数据,最后使用 getHash() 方法获取最终的哈希值。

jsSHA 支持多种加密算法,您可以在创建哈希对象时指定所需的算法,例如 'SHA-1'、'SHA-256'、'SHA-512' 等。

请注意,jsSHA 还提供了许多其他选项和功能,如 HMAC 计算、处理二进制数据等。您可以查阅 jsSHA 库的文档以获取更多详细信息和用法示例:https://caligatio.github.io/jsSHA/

总的来说,jsSHA 是一个方便的库,可用于在浏览器和 Node.js 中生成安全的数据哈希,以确保数据的完整性和认证。

https://github.com/Caligatio/jsSHA

结束

当今数字时代,安全性在应用程序开发中变得愈发重要。为了确保用户数据的保密性、完整性和可靠性,开发人员需要采取各种措施来应对安全挑战。在本文中,我们介绍了七个与安全相关的 JavaScript 库,它们为开发人员提供了强大的工具来保护应用程序免受各种潜在的安全威胁。

在您的开发旅程中,这些库将充当强大的盾牌,保护您的应用免受恶意行为的侵害。同时,随着技术的不断进步,我们也鼓励您保持警惕,时刻关注最新的安全标准和最佳实践,以确保您的应用程序始终处于安全的状态。如果您还有其他优秀的 JavaScript 库推荐,欢迎在评论中与我们分享。

由于文章内容篇幅有限,今天的内容就分享到这里,文章结尾,我想提醒您,文章的创作不易,如果您喜欢我的分享,请别忘了点赞和转发,让更多有需要的人看到。同时,如果您想获取更多前端技术的知识,欢迎关注我,您的支持将是我分享最大的动力。我会持续输出更多内容,敬请期待。

heck酱

Check酱是一个通用网页内容监控工具,可以监测网页内容变化,并发送异动到微信

Check酱是方糖气球出品的网页内容监测工具,它包含一个Edge/Chrome浏览器插件和可以自行架设的云端

基于浏览器插件,它通过可视化选择器理论上可以监控网页上的任意内容(文本)、除了浏览器通知,还可以配合Server酱将异动推送到微信或手机。

Check酱的原理是,通过浏览器插件后台打开网页进行监测,从而完全模拟用户的真实行为,可以监控绝大部分复杂的动态网页,需要登录的各种后台页面,并(在绝大多数情况下)自动延续登录态。

除了支持网页内容(Dom)的监测,还支持HTTP状态(通过GET监测)、JSON和RSS方式。



配合可以自行架设的云端,可以将监测任务同步到服务器,这样当浏览器和电脑关掉以后,监测任务依然可以定时运行。



插件的安装和使用

插件可以独立使用,只是关掉后定时监测任务不执行。

插件的安装和使用

插件可以独立使用,只是关掉后定时监测任务不执行。

安装

目前Check酱正在内测,尚未上架Edge商店,只能通过手工方式载入

下载插件ZIP包(Github下载地址,无需注册),解压为目录(后文称其为A)。

打开Edge的插件页面,打开「开发者模式」,点击「Load Unpacked」,选择上边解压得到的目录A。

成功载入的话,就可以看到Check酱界面了。如果失败,通常是因为解压时多了一层目录导致的,可以试试重新选择A目录的下一级目录

使用

添加网页监控点

安装插件后,打开要监控的网页,在网页上点击右键,可以看到「定位监测对象」一项。

点击后,开始初始化可视化选择器。

移动鼠标可以看到高亮区域,放到要监控的文字上点击鼠标左键。

注意,选择区域必须包含文本,否则会返回空。有很多文本是印在图片上的,这种也会返回空。

将转向到添加页面。

可以修改名称、设置监控间隔时间、延迟、最大重试次数。在保存之前,最好点击CSS选择器路径一栏后的测试按钮进行测试。

如果提示「检测内容为空」,说明存在问题。再次点击进行观察:

如果发现页面打开后favicon没有出来就关了,可以增加「延迟读取」的秒数;如果打开后还是返回空,那么刚才自动生成的选择器路径可能不正确。

可以更换为浏览器自动生成的,方法如下:

① 在要检测的文本上点右键,选择「inspect/审查元素」

② 这时候会自动打开开发者工具,并自动选中源码中元素的对应行。在高亮的行上点击右键,选择「复制/Copy」→ 「复制选择器/Copy selector」

③ 将复制到的剪贴板的路径填入到「CSS选择器路径」一行后,再次点击「测试」按钮进行测试。

测试通过后,点击「提交」保存监测点。

通过Server酱推送到微信和其他设备

在添加和修改监测点时,填入Sendkey即可将消息推送到Server酱。

如何获得 SendKey

登录Server酱官网,进入「Key&API」,点击「复制」按钮即可。

如何推送到其他通道

登录Server酱官网,进入「通道配置」,选择要推送的通道,并按页面上的说明进行配置。可以将消息推送到「PushDeer」和各种群机器人。

如果以上通道不能满足你的需要,可以选择「自定义」通道,发送自定义的http请求。此方式可以兼容绝大部分通知接口。

导入和导出全部监控点

点击监控点列表右上方的向上和向下箭头可以导入和导出全部监控点。

分享和导入监控点

点击监控点列表中的「剪贴板」,可以将当前监控点的设置导出到剪贴板。

导出数据类似这样:

checkchan://title=Server%E9%85%B1%E5%AE%98%E6%96%B9%E7%BD%91%E7%AB%99%E7%8A%B6%E6%80%81&url=https%3A%2F%2Fsct.ftqq.com&type=get&code=200&rss_field=title&delay=3&retry=10

复制以上字符后,在Check酱浏览器插件界面通过Ctrl+V粘贴,会自动识别并跳转到「添加监测点」界面。

监测周期限制

有些任务只需要在特定的时间段执行,为了节省资源,我们添加了「监测周期限制」功能。比如某动画每周五上午十点更新,那么我们可以将「监测周期限制」设置如下:

这样其他时间段就不再启动监测。对于无法预知事件段的任务,使用默认的「每分钟」即可。

注意在「监测周期限制」之上,还有「监控间隔时间」。

如果 「监测周期限制」 为每分钟,而「监控间隔时间」为60分钟,那么每分钟都会尝试监测,而一旦监测成功一次,那么下次监测将是60分钟后。

同时,因为执行监测任务本身也耗费时间,所以「监控间隔时间」为1分钟时,往往每隔一分钟(即每两分钟)才会运行一次任务。

Selector 扩展语法

通过@指定数组元素

最新的版本支持了一个Selector扩展语法:由于底层通过 document.querySelectorAll 实现,因此会返回匹配的全部元素于一个数组。当这些元素属于同一个父节点我们可以用 :nth-of-type(1) 或者 :nth-child(1)来指定数组中的某一项。

但如果这些元素不属于同一个父节点,那么以上方法会失效。这里添加了一个 selector@n 的语法,让我们可以从 Selector 返回的数组中指定某一个元素。如 .booklist .item@0 将返回 document.querySelectorAll(.booklist .item)[0]

@语法除了使用在DOM selector上,还可以用来指定 RSS 监测结果。默认情况下,RSS 将返回整个Feed的第一篇文章,你可以用过在 Feed url 后边添加 @1 ,将其指定为监测第二篇文章(依然是从0开始计数)。

通过%获得元素属性

Selector的最下一级返回一个元素,但有时候我们需要监测这个元素的某个属性值。比如监测一个链接的href。为了解决这个问题,我们扩展了 selector 语法,让用户可以通过 element%attribute 的方式来获取元素的某个属性值。如:article#post-180 > div > div > p > a%href。当 %@ 同时使用时, % 应紧跟元素后,如:article#post-180 > div > div > p > a%href@0

日志查看和错误定位

为了更清楚的了解定时任务的执行情况,你可以打开「开发者工具」(F12)在 Console 标签页中可以看到任务产生的日志。

错误信息也会在这里以红色高亮的行显示,遇到Bug时提供日志错误截图可以帮助我们更快的定位到问题。

更新浏览器插件

上架商店后,可以自动升级,在此之前需要手动升级。升级方式为下载zip包解压后覆盖原有文件,再在浏览器的插件管理面板中「reload」一下。

镜像的安装和使用

新版镜像已经将云端和远程桌面版本集成,只需一次安装都可以使用。

  • 云端:配合自行架设的服务器,可以将任务同步到云端执行,即使关掉浏览器和电脑后监测任务也会一直运行。
  • 远程桌面:在Docker中封装了Chrome浏览器,可以通过VNC和Web界面像在电脑上一样使用。

⚠️ 特别说明:因为云端的网络、环境都和本机不同,所以并不保证本机能运行的任务都能在云端运行成功,一些复杂网页和有较多动态效果的网页可能失败。

安装

架设自架版云端需要技术基础,非技术用户建议购买我们的官方版云端(将在内测完成后发布)

需要docker环境。如果你没有云服务器,可以看看腾讯云30~50元首单的特价服务器。

一键安装命令

点此进入工具界面

通过 Docker-compose 启动

登录服务器(假设其IP为IPB),在要安装的目录下新建目录 data,并使其可写:

mkdir data && chmod 0755 data

新建一个 docker-compose.yml 文件,将下边的内容按提示调整后粘贴保存:

version: '3'
services:
  chrome:
    image: easychen/checkchan:latest
    volumes:
      - "./data:/checkchan/data"
    environment:
      - "CKC_PASSWD=<这里是远程桌面的密码,写一个你自己想的>"
      - "VDEBUG=OFF"
      - "VNC=ON"
      #- "WIN_WIDTH=414"
      #- "WIN_HEIGHT=896"
      #- "XVFB_WHD=500x896x16"
      - "API_KEY=<这里是云端的API KEY,写一个你自己想的>"
      - "ERROR_IMAGE=NORMAL" # NONE,NORMAL,FULL
      #- "SNAP_URL_BASE=<开启截图在这里写服务器地址(结尾不用加/),不开留空>..."
      #- "SNAP_FULL=1"
      - "TZ=Asia/Chongqing"
      # - "WEBHOOK_URL=http://..." # 云端 Webhook地址,不需要则不用设置
      # - "WEBHOOK_FORMAT=json" # 云端 Webhook POST 编码,默认是 Form
    ports:
      - "5900:5900" 
      - "8080:8080" 
      - "8088:80"

将其中<这里是远程桌面的密码,写一个你自己想的><这里是云端的API KEY,写一个你自己想的> 换成别人不知道的密码(下文称密码C和D)。注意不要包含$字符,替换完后也不再有两边的尖括号<>

如果不希望启动远程桌面,请将 VNC=ON 改为 VNC=OFF

保证Docker用户对此目录有写权限,并在同一目录下运行以下命令:

docker-compose up -d

如提示docker服务未安装/找不到/未启动,可在 docker-compose 前加 sudo 再试

等待初始化完成后,访问 http://$BBB:8080( 将$BBB替换为IP B),看到 NoVNC Web界面说明容器已经启动。

服务所在的端口为:

  • 云端:8088
  • 远程桌面(VNC): 5900
  • 远程桌面的Web界面(NoVNC): 8080

通过 Docker 启动

你也可以将 docker-compose 中的参数传给 docker 来启动:

docker run -d -p 8088:80 -p 8080:8080 -p 5900:5900 -v ${PWD}/data:/checkchan/data -e API_KEY=123  -e VDEBUG=OFF -e VNC=ON -e SNAP_URL_BASE=http://localhost:8088  -e CKC_PASSWD=123 -e TZ=Asia/Chongqing easychen/checkchan:latest

请将上述命令中的123替换为你想要设定的密码、将SNAP_URL_BASE换成服务器的外网IP(如果想通过手机查看截图)。

群晖安装

只有支持Docker的群晖型号才能安装Check酱,除了可以直接通过命令行安装,也可以参考GUI安装教程。

Volume 和环境变量可以参考以上的docker/compose设定。

更新镜像

Check酱云端镜像更新后,你可以将正在运行的云端服务升级到最新版。方式如下:

首先停现有的容器:

通过 docker-compose 启动的运行:

docker-compose down

通过 docker 直接启动的运行 docker ps 查询到容器id,通过 docker stop 容器id 停止。

然后运行 docker pull 拉取最新版:

docker pull easychen/checkchan:latest

完成后再启动服务即可。

云端的使用

将浏览器插件对接云端

点击插件右上方菜单中的云端服务

服务器地址一栏输入 http://$BBB:8088(将$BBB替换为IP B,这里的URL不用加key参数);在API_KEY一栏输入密码C。

点击保存,连接成功后,配置完成。

同步本地任务到云端

配置好云端以后回到列表页,每行最右边会多出来一个「电脑」图标,点击后会变成「云」图标,该任务将改为在云端执行。

点击右上角 「云+箭头」的按钮,可以主动同步任务到云端。

Check酱也会每十分钟自动同步一次。

云端截图

Check酱自架云端支持对网页(dom)类型任务进行截图,可以通过给镜像传递环境变量来开启:

  • SNAP_URL_BASE=<开启截图在这里写服务器地址,不开留空> #如 http://ip.com/
  • SNAP_FULL=1 #完整网页长图

可参考上文的docker-compser.yml。添加环境变量后重启服务即可。

注意

  • 截图功能需要较大的内存,部分服务器可能会报错
  • 云端网络和本地不同,可能会超时失败,请适当增加延时,并将取消完整截图

RSS上行接口

Check酱自架云端内置了动态页面RSS上行用的接口:

  • RSS上行地址为: http://$ip:$port/rss/upload?key=$api_key
  • RSS Feed地址为:http://$ip:$port/image/rss.xml?key=$api_key

多浏览器Cookie同步接口

Check酱自架云端内置了Cookie同步用的接口:

  • Cookie同步URL为: http://$ip:$port/cookie/sync?key=$api_key
  • 配对密码: 自行设定,同步Cookie的两个浏览器中必须一致
  • 同步方向: 一个发送一个接收,一般电脑上的发送、服务器上远程桌面里的接收
  • 同步频率:发送或者接收间隔时间

云端任务的安全性

Check酱云端任务的原理是将cookie同步到云端,然后用浏览器查看,本质和用户操作一样。但因为出口IP可能是机房和数据中心,频次太高也有被风控的可能。如果将云端部署在家里,则和在家用电脑访问效果一样。

云端错误排查

通常来讲,出现本地任务可以执行,云端不能执行的问题,是因为两者网络环境、浏览器软件存在差异,比如:

  1. 页面结构每次都会变动:比如一些网站的首页,建议进入分类列表页面选择监控点
  2. 电脑网络和云端网络不同:在浏览器中可以访问的内容,在数据中心可能访问不到
  3. CDN更新延迟:电脑和云端CDN节点刷新未完成,会造成一边可用一边不可用,等待更新完成后再监控
  4. 浏览器插件改变了网页结构:比如本地通过 AdBlock 过滤了广告,但云端没有,造成结构不同,监测失败

由于服务器内存通常没大家电脑大,所以很多在本地执行OK的任务同步到云端后会因为「延迟读取」秒数太小中途停止而失败。如果遇到类似情况,请尝试增加「延迟读取」。

如果这样也不行,往往是因为云端无头浏览器显示网页和本地存在差异导致,我们为这种情况生成了最近一次失败的任务的截图,可以在「云端服务」菜单下看到。

点击「失败截图」按钮即可看到。注意:需要只用最新的镜像,并传递ERROR_IMAGE=NORMAL 环境变量。如果希望截取完整网页的图片,可以传递ERROR_IMAGE=FULL

如果任务失败又没有截图,说明该任务不是因为CSS选择器未命中而失败,尝试增加「延迟读取」可能解决。

这个页面也能看到云端任务日志,这里的日志不包含手动点击「监测」按钮触发的任务。如果没有可以执行的任务(任务是定时触发的),那么日志亦可能为空。

远程桌面版的使用

除了自架云端,我们还在镜像中集成了远程桌面模式。它让你可以通过VNC连接服务器,像使用本地浏览器一样使用。

远程桌面版本之前为一个独立镜像,现在已经整合到 easychen/checkchan 中,因此你可以直接使用。

通过 Web 界面使用

  • Web界面: http:///$BBB:8080
  • 密码: 123 (可自行修改命令调整)

通过 VNC 连接使用

服务启动后,可以通过 VNC 客户端软件进行连接使用。

  • 连接地址: 架设服务的IP:5900
  • 密码: 123 (可自行修改命令调整)

连接云端

在远程桌面中,可以直接连接同一个容器内的云端,服务器地址填 http://localhost,API KEY按上边 YML 中设置的输入即可。

移动版

可以添加环境变量,修改屏幕宽高限制,使其在手机上更好用:

- WIN_WIDTH=414 
- WIN_HEIGHT=896 
- XVFB_WHD=500x896x16

一个加到 docker 命令中的例子:

docker run -d -p 5900:5900 -v ${PWD}/data:/checkchan/data -e CKC_PASSWD=123 -e WIN_WIDTH=414 -e WIN_HEIGHT=896 -e XVFB_WHD=500x896x16 easychen/checkchan:latest

特别说明

容器日常消耗在 300M~500M。内存较大的运行环境会比较稳定,如果遇到问题可尝试加大内存。

可视化调试

使用同一个镜像中集成的云端可以对云端任务进行可视化调试,将 YML 文件中的 VDEBUG 设置为 ON,再重新启动容器即可看到云端监测网页的详细过程。

为工作中经常用到这些方法,所有便把这些方法进行了总结。

JavaScript

1. type 类型判断

isString (o) { //是否字符串
    return Object.prototype.toString.call(o).slice(8, -1) === 'String'
}

isNumber (o) { //是否数字
    return Object.prototype.toString.call(o).slice(8, -1) === 'Number'
}

isBoolean (o) { //是否boolean
    return Object.prototype.toString.call(o).slice(8, -1) === 'Boolean'
}

isFunction (o) { //是否函数
    return Object.prototype.toString.call(o).slice(8, -1) === 'Function'
}

isNull (o) { //是否为null
    return Object.prototype.toString.call(o).slice(8, -1) === 'Null'
}

isUndefined (o) { //是否undefined
    return Object.prototype.toString.call(o).slice(8, -1) === 'Undefined'
}

isObj (o) { //是否对象
    return Object.prototype.toString.call(o).slice(8, -1) === 'Object'
}

isArray (o) { //是否数组
    return Object.prototype.toString.call(o).slice(8, -1) === 'Array'
}

isDate (o) { //是否时间
    return Object.prototype.toString.call(o).slice(8, -1) === 'Date'
}

isRegExp (o) { //是否正则
    return Object.prototype.toString.call(o).slice(8, -1) === 'RegExp'
}

isError (o) { //是否错误对象
    return Object.prototype.toString.call(o).slice(8, -1) === 'Error'
}

isSymbol (o) { //是否Symbol函数
    return Object.prototype.toString.call(o).slice(8, -1) === 'Symbol'
}

isPromise (o) { //是否Promise对象
    return Object.prototype.toString.call(o).slice(8, -1) === 'Promise'
}

isSet (o) { //是否Set对象
    return Object.prototype.toString.call(o).slice(8, -1) === 'Set'
}

isFalse (o) {
    if (!o || o === 'null' || o === 'undefined' || o === 'false' || o === 'NaN') return true
        return false
}

isTrue (o) {
    return !this.isFalse(o)
}

isIos () {
    var u = navigator.userAgent;
    if (u.indexOf('Android') > -1 || u.indexOf('Linux') > -1) {//安卓手机
        // return "Android";
        return false
    } else if (u.indexOf('iPhone') > -1) {//苹果手机
        // return "iPhone";
        return true
    } else if (u.indexOf('iPad') > -1) {//iPad
        // return "iPad";
        return false
    } else if (u.indexOf('Windows Phone') > -1) {//winphone手机
        // return "Windows Phone";
        return false
    }else{
        return false
    }
}

isPC () { //是否为PC端
    var userAgentInfo = navigator.userAgent;
    var Agents = ["Android", "iPhone",
                "SymbianOS", "Windows Phone",
                "iPad", "iPod"];
    var flag = true;
    for (var v = 0; v < Agents.length; v++) {
        if (userAgentInfo.indexOf(Agents[v]) > 0) {
            flag = false;
            break;
        }
    }
    return flag;
}

browserType(){
    var userAgent = navigator.userAgent; //取得浏览器的userAgent字符串
    var isOpera = userAgent.indexOf("Opera") > -1; //判断是否Opera浏览器
    var isIE = userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1 && !isOpera; //判断是否IE浏览器
    var isIE11 = userAgent.indexOf('Trident') > -1 && userAgent.indexOf("rv:11.0") > -1;
    var isEdge = userAgent.indexOf("Edge") > -1 && !isIE; //判断是否IE的Edge浏览器  
    var isFF = userAgent.indexOf("Firefox") > -1; //判断是否Firefox浏览器
    var isSafari = userAgent.indexOf("Safari") > -1 && userAgent.indexOf("Chrome") == -1; //判断是否Safari浏览器
    var isChrome = userAgent.indexOf("Chrome") > -1 && userAgent.indexOf("Safari") > -1; //判断Chrome浏览器

    if (isIE) {
        var reIE = new RegExp("MSIE (\\d+\\.\\d+);");
        reIE.test(userAgent);
        var fIEVersion = parseFloat(RegExp["$1"]);
        if(fIEVersion == 7) return "IE7"
        else if(fIEVersion == 8) return "IE8";
        else if(fIEVersion == 9) return "IE9";
        else if(fIEVersion == 10) return "IE10";
        else return "IE7以下"//IE版本过低
    }
    if (isIE11) return 'IE11';
    if (isEdge) return "Edge";
    if (isFF) return "FF";
    if (isOpera) return "Opera";
    if (isSafari) return "Safari";
    if (isChrome) return "Chrome";
}

checkStr (str, type) {
    switch (type) {
        case 'phone':   //手机号码
            return /^1[3|4|5|6|7|8|9][0-9]{9}$/.test(str);
        case 'tel':     //座机
            return /^(0\d{2,3}-\d{7,8})(-\d{1,4})?$/.test(str);
        case 'card':    //身份证
            return /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(str);
        case 'pwd':     //密码以字母开头,长度在6~18之间,只能包含字母、数字和下划线
            return /^[a-zA-Z]\w{5,17}$/.test(str)
        case 'postal':  //邮政编码
            return /[1-9]\d{5}(?!\d)/.test(str);
        case 'QQ':      //QQ号
            return /^[1-9][0-9]{4,9}$/.test(str);
        case 'email':   //邮箱
            return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(str);
        case 'money':   //金额(小数点2位)
            return /^\d*(?:\.\d{0,2})?$/.test(str);
        case 'URL':     //网址
            return /(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?/.test(str)
        case 'IP':      //IP
            return /((?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d))/.test(str);
        case 'date':    //日期时间
            return /^(\d{4})\-(\d{2})\-(\d{2}) (\d{2})(?:\:\d{2}|:(\d{2}):(\d{2}))$/.test(str) || /^(\d{4})\-(\d{2})\-(\d{2})$/.test(str)
        case 'number':  //数字
            return /^[0-9]$/.test(str);
        case 'english': //英文
            return /^[a-zA-Z]+$/.test(str);
        case 'chinese': //中文
            return /^[\u4E00-\u9FA5]+$/.test(str);
        case 'lower':   //小写
            return /^[a-z]+$/.test(str);
        case 'upper':   //大写
            return /^[A-Z]+$/.test(str);
        case 'HTML':    //HTML标记
            return /<("[^"]*"|'[^']*'|[^'">])*>/.test(str);
        default:
            return true;
    }

    // 严格的身份证校验
    isCardID(sId) {
        if (!/(^\d{15}$)|(^\d{17}(\d|X|x)$)/.test(sId)) {
            alert('你输入的身份证长度或格式错误')
            return false
        }
        //身份证城市
        var aCity={11:"北京",12:"天津",13:"河北",14:"山西",15:"内蒙古",21:"辽宁",22:"吉林",23:"黑龙江",31:"上海",32:"江苏",33:"浙江",34:"安徽",35:"福建",36:"江西",37:"山东",41:"河南",42:"湖北",43:"湖南",44:"广东",45:"广西",46:"海南",50:"重庆",51:"四川",52:"贵州",53:"云南",54:"西藏",61:"陕西",62:"甘肃",63:"青海",64:"宁夏",65:"新疆",71:"台湾",81:"香港",82:"澳门",91:"国外"};
        if(!aCity[parseInt(sId.substr(0,2))]) { 
            alert('你的身份证地区非法')
            return false
        }

        // 出生日期验证
        var sBirthday=(sId.substr(6,4)+"-"+Number(sId.substr(10,2))+"-"+Number(sId.substr(12,2))).replace(/-/g,"/"),
            d = new Date(sBirthday)
        if(sBirthday != (d.getFullYear()+"/"+ (d.getMonth()+1) + "/" + d.getDate())) {
            alert('身份证上的出生日期非法')
            return false
        }

        // 身份证号码校验
        var sum = 0,
            weights =  [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2],
            codes = "10X98765432"
        for (var i = 0; i < sId.length - 1; i++) {
            sum += sId[i] * weights[i];
        }
        var last = codes[sum % 11]; //计算出来的最后一位身份证号码
        if (sId[sId.length-1] != last) { 
            alert('你输入的身份证号非法')
            return false
        }

        return true
    }
}

2. Date

/**
 * 格式化时间
 * 
 * @param  {time} 时间
 * @param  {cFormat} 格式
 * @return {String} 字符串
 *
 * @example formatTime('2018-1-29', '{y}/{m}/{d} {h}:{i}:{s}') // -> 2018/01/29 00:00:00
 */
formatTime(time, cFormat) {
    if (arguments.length === 0) return null
    if ((time + '').length === 10) {
        time = +time * 1000
    }

    var format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}', date
    if (typeof time === 'object') {
        date = time
    } else {
        date = new Date(time)
    }

    var formatObj = {
        y: date.getFullYear(),
        m: date.getMonth() + 1,
        d: date.getDate(),
        h: date.getHours(),
        i: date.getMinutes(),
        s: date.getSeconds(),
        a: date.getDay()
    }
    var time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
        var value = formatObj[key]
        if (key === 'a') return ['一', '二', '三', '四', '五', '六', '日'][value - 1]
        if (result.length > 0 && value < 10) {
            value = '0' + value
        }
        return value || 0
    })
    return time_str
}

/**
 * 返回指定长度的月份集合
 * 
 * @param  {time} 时间
 * @param  {len} 长度
 * @param  {direction} 方向:  1: 前几个月;  2: 后几个月;  3:前后几个月  默认 3
 * @return {Array} 数组
 * 
 * @example   getMonths('2018-1-29', 6, 1)  // ->  ["2018-1", "2017-12", "2017-11", "2017-10", "2017-9", "2017-8", "2017-7"]
 */
getMonths(time, len, direction) {
    var mm = new Date(time).getMonth(),
        yy = new Date(time).getFullYear(),
        direction = isNaN(direction) ? 3 : direction,
        index = mm;
    var cutMonth = function(index) {
        if ( index <= len && index >= -len) {
            return direction === 1 ? formatPre(index).concat(cutMonth(++index)):
                direction === 2 ? formatNext(index).concat(cutMonth(++index)):formatCurr(index).concat(cutMonth(++index))
        }
        return []
    }
    var formatNext = function(i) {
        var y = Math.floor(i/12),
            m = i%12
        return [yy+y + '-' + (m+1)]
    }
    var formatPre = function(i) {
        var y = Math.ceil(i/12),
            m = i%12
        m = m===0 ? 12 : m
        return [yy-y + '-' + (13 - m)]
    }
    var formatCurr = function(i) {
        var y = Math.floor(i/12),
            yNext = Math.ceil(i/12),
            m = i%12,
            mNext = m===0 ? 12 : m
        return [yy-yNext + '-' + (13 - mNext),yy+y + '-' + (m+1)]
    }
    // 数组去重
    var unique = function(arr) {
        if ( Array.hasOwnProperty('from') ) {
            return Array.from(new Set(arr));
        }else{
            var n = {},r=[]; 
            for(var i = 0; i < arr.length; i++){
                if (!n[arr[i]]){
                    n[arr[i]] = true; 
                    r.push(arr[i]);
                }
            }
            return r;
        }
    }
    return direction !== 3 ? cutMonth(index) : unique(cutMonth(index).sort(function(t1, t2){
        return new Date(t1).getTime() - new Date(t2).getTime()
    }))
}

/**
 * 返回指定长度的天数集合
 * 
 * @param  {time} 时间
 * @param  {len} 长度
 * @param  {direction} 方向: 1: 前几天;  2: 后几天;  3:前后几天  默认 3
 * @return {Array} 数组
 *
 * @example date.getDays('2018-1-29', 6) // -> ["2018-1-26", "2018-1-27", "2018-1-28", "2018-1-29", "2018-1-30", "2018-1-31", "2018-2-1"]
 */
getDays(time, len, diretion) {
    var tt = new Date(time)
    var getDay = function(day) {
        var t = new Date(time)
        t.setDate(t.getDate() + day)
        var m = t.getMonth()+1
        return t.getFullYear()+'-'+m+'-'+t.getDate()
    }
    var arr = []
    if (diretion === 1) {
        for (var i = 1; i <= len; i++) {
            arr.unshift(getDay(-i))
        }
    }else if(diretion === 2) {
        for (var i = 1; i <= len; i++) {
            arr.push(getDay(i))
        }
    }else {
        for (var i = 1; i <= len; i++) {
            arr.unshift(getDay(-i))
        }
        arr.push(tt.getFullYear()+'-'+(tt.getMonth()+1)+'-'+tt.getDate())
        for (var i = 1; i <= len; i++) {
            arr.push(getDay(i))
        }
    }
    return diretion === 1 ? arr.concat([tt.getFullYear()+'-'+(tt.getMonth()+1)+'-'+tt.getDate()]) : 
        diretion === 2 ? [tt.getFullYear()+'-'+(tt.getMonth()+1)+'-'+tt.getDate()].concat(arr) : arr
}

/**
 * @param  {s} 秒数
 * @return {String} 字符串 
 *
 * @example formatHMS(3610) // -> 1h0m10s
 */
formatHMS (s) {
    var str = ''
    if (s > 3600) {
        str = Math.floor(s/3600)+'h'+Math.floor(s%3600/60)+'m'+s%60+'s'
    }else if(s > 60) {
        str = Math.floor(s/60)+'m'+s%60+'s'
    }else{
        str = s%60+'s'
    }
    return str
}

/*获取某月有多少天*/
getMonthOfDay (time) {
    var date = new Date(time)
    var year = date.getFullYear()
    var mouth = date.getMonth() + 1
    var days

    //当月份为二月时,根据闰年还是非闰年判断天数
    if (mouth == 2) {
        days = (year%4==0 && year%100==0 && year%400==0) || (year%4==0 && year%100!=0) ? 28 : 29
    } else if (mouth == 1 || mouth == 3 || mouth == 5 || mouth == 7 || mouth == 8 || mouth == 10 || mouth == 12) {
        //月份为:1,3,5,7,8,10,12 时,为大月.则天数为31;
        days = 31
    } else {
        //其他月份,天数为:30.
        days = 30
    }
    return days
}

/*获取某年有多少天*/
getYearOfDay (time) {
    var firstDayYear = this.getFirstDayOfYear(time);
    var lastDayYear = this.getLastDayOfYear(time);
    var numSecond = (new Date(lastDayYear).getTime() - new Date(firstDayYear).getTime())/1000;
    return Math.ceil(numSecond/(24*3600));
}

/*获取某年的第一天*/
getFirstDayOfYear (time) {
    var year = new Date(time).getFullYear();
    return year + "-01-01 00:00:00";
}

/*获取某年最后一天*/
getLastDayOfYear (time) {
    var year = new Date(time).getFullYear();
    var dateString = year + "-12-01 00:00:00";
    var endDay = this.getMonthOfDay(dateString);
    return year + "-12-" + endDay + " 23:59:59";
}

/*获取某个日期是当年中的第几天*/
getDayOfYear (time) {
    var firstDayYear = this.getFirstDayOfYear(time);
    var numSecond = (new Date(time).getTime() - new Date(firstDayYear).getTime())/1000;
    return Math.ceil(numSecond/(24*3600));
}

/*获取某个日期在这一年的第几周*/
getDayOfYearWeek (time) {
    var numdays = this.getDayOfYear(time);
    return Math.ceil(numdays / 7);
}

3. Array

/*判断一个元素是否在数组中*/
contains (arr, val) {
    return arr.indexOf(val) != -1 ? true : false;
}

/**
 * @param  {arr} 数组
 * @param  {fn} 回调函数
 * @return {undefined}
 */
each (arr, fn) {
    fn = fn || Function;
    var a = [];
    var args = Array.prototype.slice.call(arguments, 1);
    for(var i = 0; i < arr.length; i++) {
        var res = fn.apply(arr, [arr[i], i].concat(args));
        if(res != null) a.push(res);
    }
}

/**
 * @param  {arr} 数组
 * @param  {fn} 回调函数
 * @param  {thisObj} this指向
 * @return {Array} 
 */
map (arr, fn, thisObj) {
    var scope = thisObj || window;
    var a = [];
    for(var i = 0, j = arr.length; i < j; ++i) {
        var res = fn.call(scope, arr[i], i, this);
        if(res != null) a.push(res);
    }
    return a;
}

/**
 * @param  {arr} 数组
 * @param  {type} 1:从小到大   2:从大到小   3:随机
 * @return {Array}
 */
sort (arr, type = 1) {
    return arr.sort( (a, b) => {
        switch(type) {
            case 1:
                return a - b;
            case 2:
                return b - a;
            case 3:
                return Math.random() - 0.5;
            default:
                return arr;
        }
    })
}

/*去重*/
unique (arr) {
    if ( Array.hasOwnProperty('from') ) {
        return Array.from(new Set(arr));
    }else{
        var n = {},r=[]; 
        for(var i = 0; i < arr.length; i++){
            if (!n[arr[i]]){
                n[arr[i]] = true; 
                r.push(arr[i]);
            }
        }
        return r;
    }
    // 注:上面 else 里面的排重并不能区分 2 和 '2',但能减少用indexOf带来的性能,暂时没找到替代的方法。。。
    /* 正确排重
    if ( Array.hasOwnProperty('from') ) {
        return Array.from(new Set(arr))
    }else{
        var r = [], NaNBol = true
        for(var i=0; i < arr.length; i++) {
            if (arr[i] !== arr[i]) {
                if (NaNBol && r.indexOf(arr[i]) === -1) {
                    r.push(arr[i])
                    NaNBol = false
                }
            }else{
                if(r.indexOf(arr[i]) === -1) r.push(arr[i])
            }
        }
        return r
    }

     */
}

/*求两个集合的并集*/
union (a, b) {
    var newArr = a.concat(b);
    return this.unique(newArr);
}

/*求两个集合的交集*/
intersect (a, b) {
    var _this = this;
    a = this.unique(a);
    return this.map(a, function(o) {
        return _this.contains(b, o) ? o : null;
    });
}

/*删除其中一个元素*/
remove (arr, ele) {
    var index = arr.indexOf(ele);
    if(index > -1) {
        arr.splice(index, 1);
    }
    return arr;
}

/*将类数组转换为数组的方法*/
formArray (ary) {
    var arr = [];
    if(Array.isArray(ary)) {
        arr = ary;
    } else {
        arr = Array.prototype.slice.call(ary);
    };
    return arr;
}

/*最大值*/
max (arr) {
    return Math.max.apply(null, arr);
}

/*最小值*/
min (arr) {
    return Math.min.apply(null, arr);
}

/*求和*/
sum (arr) {
    return arr.reduce( (pre, cur) => {
        return pre + cur
    })
}

/*平均值*/
average (arr) {
    return this.sum(arr)/arr.length
}

4. String 字符串操作

/**
 * 去除空格
 * @param  {str}
 * @param  {type} 
 *       type:  1-所有空格  2-前后空格  3-前空格 4-后空格
 * @return {String}
 */
trim (str, type) {
    type = type || 1
    switch (type) {
        case 1:
            return str.replace(/\s+/g, "");
        case 2:
            return str.replace(/(^\s*)|(\s*$)/g, "");
        case 3:
            return str.replace(/(^\s*)/g, "");
        case 4:
            return str.replace(/(\s*$)/g, "");
        default:
            return str;
    }
}

/**
 * @param  {str} 
 * @param  {type}
 *       type:  1:首字母大写  2:首页母小写  3:大小写转换  4:全部大写  5:全部小写
 * @return {String}
 */
changeCase (str, type) {
    type = type || 4
    switch (type) {
        case 1:
            return str.replace(/\b\w+\b/g, function (word) {
                return word.substring(0, 1).toUpperCase() + word.substring(1).toLowerCase();

            });
        case 2:
            return str.replace(/\b\w+\b/g, function (word) {
                return word.substring(0, 1).toLowerCase() + word.substring(1).toUpperCase();
            });
        case 3:
            return str.split('').map( function(word){
                if (/[a-z]/.test(word)) {
                    return word.toUpperCase();
                }else{
                    return word.toLowerCase()
                }
            }).join('')
        case 4:
            return str.toUpperCase();
        case 5:
            return str.toLowerCase();
        default:
            return str;
    }
}

/*
    检测密码强度
*/
checkPwd (str) {
    var Lv = 0;
    if (str.length < 6) {
        return Lv
    }
    if (/[0-9]/.test(str)) {
        Lv++
    }
    if (/[a-z]/.test(str)) {
        Lv++
    }
    if (/[A-Z]/.test(str)) {
        Lv++
    }
    if (/[\.|-|_]/.test(str)) {
        Lv++
    }
    return Lv;
}

/*过滤html代码(把<>转换)*/
filterTag (str) {
    str = str.replace(/&/ig, "&");
    str = str.replace(/</ig, "<");
    str = str.replace(/>/ig, ">");
    str = str.replace(" ", " ");
    return str;
}

5. Number

/*随机数范围*/
random (min, max) {
    if (arguments.length === 2) {
        return Math.floor(min + Math.random() * ( (max+1) - min ))
    }else{
        return null;
    }

}

/*将阿拉伯数字翻译成中文的大写数字*/
numberToChinese (num) {
    var AA = new Array("零", "一", "二", "三", "四", "五", "六", "七", "八", "九", "十");
    var BB = new Array("", "十", "百", "仟", "萬", "億", "点", "");
    var a = ("" + num).replace(/(^0*)/g, "").split("."),
        k = 0,
        re = "";
    for(var i = a[0].length - 1; i >= 0; i--) {
        switch(k) {
            case 0:
                re = BB[7] + re;
                break;
            case 4:
                if(!new RegExp("0{4}//d{" + (a[0].length - i - 1) + "}$")
                    .test(a[0]))
                    re = BB[4] + re;
                break;
            case 8:
                re = BB[5] + re;
                BB[7] = BB[5];
                k = 0;
                break;
        }
        if(k % 4 == 2 && a[0].charAt(i + 2) != 0 && a[0].charAt(i + 1) == 0)
            re = AA[0] + re;
        if(a[0].charAt(i) != 0)
            re = AA[a[0].charAt(i)] + BB[k % 4] + re;
        k++;
    }

    if(a.length > 1) // 加上小数部分(如果有小数部分)
    {
        re += BB[6];
        for(var i = 0; i < a[1].length; i++)
            re += AA[a[1].charAt(i)];
    }
    if(re == '一十')
        re = "十";
    if(re.match(/^一/) && re.length == 3)
        re = re.replace("一", "");
    return re;
}

/*将数字转换为大写金额*/
changeToChinese (Num) {
        //判断如果传递进来的不是字符的话转换为字符
        if(typeof Num == "number") {
            Num = new String(Num);
        };
        Num = Num.replace(/,/g, "") //替换tomoney()中的“,”
        Num = Num.replace(/ /g, "") //替换tomoney()中的空格
        Num = Num.replace(/¥/g, "") //替换掉可能出现的¥字符
        if(isNaN(Num)) { //验证输入的字符是否为数字
            //alert("请检查小写金额是否正确");
            return "";
        };
        //字符处理完毕后开始转换,采用前后两部分分别转换
        var part = String(Num).split(".");
        var newchar = "";
        //小数点前进行转化
        for(var i = part[0].length - 1; i >= 0; i--) {
            if(part[0].length > 10) {
                return "";
                //若数量超过拾亿单位,提示
            }
            var tmpnewchar = ""
            var perchar = part[0].charAt(i);
            switch(perchar) {
                case "0":
                    tmpnewchar = "零" + tmpnewchar;
                    break;
                case "1":
                    tmpnewchar = "壹" + tmpnewchar;
                    break;
                case "2":
                    tmpnewchar = "贰" + tmpnewchar;
                    break;
                case "3":
                    tmpnewchar = "叁" + tmpnewchar;
                    break;
                case "4":
                    tmpnewchar = "肆" + tmpnewchar;
                    break;
                case "5":
                    tmpnewchar = "伍" + tmpnewchar;
                    break;
                case "6":
                    tmpnewchar = "陆" + tmpnewchar;
                    break;
                case "7":
                    tmpnewchar = "柒" + tmpnewchar;
                    break;
                case "8":
                    tmpnewchar = "捌" + tmpnewchar;
                    break;
                case "9":
                    tmpnewchar = "玖" + tmpnewchar;
                    break;
            }
            switch(part[0].length - i - 1) {
                case 0:
                    tmpnewchar = tmpnewchar + "元";
                    break;
                case 1:
                    if(perchar != 0) tmpnewchar = tmpnewchar + "拾";
                    break;
                case 2:
                    if(perchar != 0) tmpnewchar = tmpnewchar + "佰";
                    break;
                case 3:
                    if(perchar != 0) tmpnewchar = tmpnewchar + "仟";
                    break;
                case 4:
                    tmpnewchar = tmpnewchar + "万";
                    break;
                case 5:
                    if(perchar != 0) tmpnewchar = tmpnewchar + "拾";
                    break;
                case 6:
                    if(perchar != 0) tmpnewchar = tmpnewchar + "佰";
                    break;
                case 7:
                    if(perchar != 0) tmpnewchar = tmpnewchar + "仟";
                    break;
                case 8:
                    tmpnewchar = tmpnewchar + "亿";
                    break;
                case 9:
                    tmpnewchar = tmpnewchar + "拾";
                    break;
            }
            var newchar = tmpnewchar + newchar;
        }
        //小数点之后进行转化
        if(Num.indexOf(".") != -1) {
            if(part[1].length > 2) {
                // alert("小数点之后只能保留两位,系统将自动截断");
                part[1] = part[1].substr(0, 2)
            }
            for(i = 0; i < part[1].length; i++) {
                tmpnewchar = ""
                perchar = part[1].charAt(i)
                switch(perchar) {
                    case "0":
                        tmpnewchar = "零" + tmpnewchar;
                        break;
                    case "1":
                        tmpnewchar = "壹" + tmpnewchar;
                        break;
                    case "2":
                        tmpnewchar = "贰" + tmpnewchar;
                        break;
                    case "3":
                        tmpnewchar = "叁" + tmpnewchar;
                        break;
                    case "4":
                        tmpnewchar = "肆" + tmpnewchar;
                        break;
                    case "5":
                        tmpnewchar = "伍" + tmpnewchar;
                        break;
                    case "6":
                        tmpnewchar = "陆" + tmpnewchar;
                        break;
                    case "7":
                        tmpnewchar = "柒" + tmpnewchar;
                        break;
                    case "8":
                        tmpnewchar = "捌" + tmpnewchar;
                        break;
                    case "9":
                        tmpnewchar = "玖" + tmpnewchar;
                        break;
                }
                if(i == 0) tmpnewchar = tmpnewchar + "角";
                if(i == 1) tmpnewchar = tmpnewchar + "分";
                newchar = newchar + tmpnewchar;
            }
        }
        //替换所有无用汉字
        while(newchar.search("零零") != -1)
            newchar = newchar.replace("零零", "零");
        newchar = newchar.replace("零亿", "亿");
        newchar = newchar.replace("亿万", "亿");
        newchar = newchar.replace("零万", "万");
        newchar = newchar.replace("零元", "元");
        newchar = newchar.replace("零角", "");
        newchar = newchar.replace("零分", "");
        if(newchar.charAt(newchar.length - 1) == "元") {
            newchar = newchar + "整"
        }
        return newchar;
    }

6. Http

/**
 * @param  {setting}
 */
ajax(setting){
    //设置参数的初始值
    var opts={
        method: (setting.method || "GET").toUpperCase(), //请求方式
        url: setting.url || "", // 请求地址
        async: setting.async || true, // 是否异步
        dataType: setting.dataType || "json", // 解析方式
        data: setting.data || "", // 参数
        success: setting.success || function(){}, // 请求成功回调
        error: setting.error || function(){} // 请求失败回调
    }

    // 参数格式化
    function params_format (obj) {
        var str = ''
        for (var i in obj) {
            str += i + '=' + obj[i] + '&'
        }
        return str.split('').slice(0, -1).join('')
    }

    // 创建ajax对象
    var xhr=new XMLHttpRequest();

    // 连接服务器open(方法GET/POST,请求地址, 异步传输)
    if(opts.method == 'GET'){
        xhr.open(opts.method, opts.url + "?" + params_format(opts.data), opts.async);
        xhr.send();
    }else{
        xhr.open(opts.method, opts.url, opts.async);
        xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
        xhr.send(opts.data);
    }

    /*
    ** 每当readyState改变时,就会触发onreadystatechange事件
    ** readyState属性存储有XMLHttpRequest的状态信息
    ** 0 :请求未初始化
    ** 1 :服务器连接已建立
    ** 2 :请求已接受
    ** 3 : 请求处理中
    ** 4 :请求已完成,且相应就绪
    */
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4 && (xhr.status === 200 || xhr.status === 304)) {
            switch(opts.dataType){
                case "json":
                    var json = JSON.parse(xhr.responseText);
                    opts.success(json);
                    break;
                case "xml":
                    opts.success(xhr.responseXML);
                    break;
                default:
                    opts.success(xhr.responseText);
                    break;
            }
        }
    }

    xhr.onerror = function(err) {
        opts.error(err);
    }
}

/**
 * @param  {url}
 * @param  {setting}
 * @return {Promise}
 */
fetch(url, setting) {
    //设置参数的初始值
    let opts={
        method: (setting.method || 'GET').toUpperCase(), //请求方式
        headers : setting.headers  || {}, // 请求头设置
        credentials : setting.credentials  || true, // 设置cookie是否一起发送
        body: setting.body || {},
        mode : setting.mode  || 'no-cors', // 可以设置 cors, no-cors, same-origin
        redirect : setting.redirect  || 'follow', // follow, error, manual
        cache : setting.cache  || 'default' // 设置 cache 模式 (default, reload, no-cache)
    }
    let dataType = setting.dataType || "json", // 解析方式  
        data = setting.data || "" // 参数

    // 参数格式化
    function params_format (obj) {
        var str = ''
        for (var i in obj) {
            str += `${i}=${obj[i]}&`
        }
        return str.split('').slice(0, -1).join('')
    }

    if (opts.method === 'GET') {
        url = url + (data?`?${params_format(data)}`:'')
    }else{
        setting.body = data || {}
    }

    return new Promise( (resolve, reject) => {
        fetch(url, opts).then( async res => {
            let data = dataType === 'text' ? await res.text() :
                dataType === 'blob' ? await res.blob() : await res.json() 
            resolve(data)
        }).catch( e => {
            reject(e)
        })
    })

}

7. DOM

$ (selector){ 
    var type = selector.substring(0, 1);
    if (type === '#') {
        if (document.querySelecotor) return document.querySelector(selector)
            return document.getElementById(selector.substring(1))

    }else if (type === '.') {
        if (document.querySelecotorAll) return document.querySelectorAll(selector)
            return document.getElementsByClassName(selector.substring(1))
    }else{
        return document['querySelectorAll' ? 'querySelectorAll':'getElementsByTagName'](selector)
    }
} 

/*检测类名*/
hasClass (ele, name) {
    return ele.className.match(new RegExp('(\\s|^)' + name + '(\\s|$)'));
}

/*添加类名*/
addClass (ele, name) {
    if (!this.hasClass(ele, name)) ele.className += " " + name;
}

/*删除类名*/
removeClass (ele, name) {
    if (this.hasClass(ele, name)) {
        var reg = new RegExp('(\\s|^)' + name + '(\\s|$)');
        ele.className = ele.className.replace(reg, '');
    }
}

/*替换类名*/
replaceClass (ele, newName, oldName) {
    this.removeClass(ele, oldName);
    this.addClass(ele, newName);
}

/*获取兄弟节点*/
siblings (ele) {
    console.log(ele.parentNode)
    var chid = ele.parentNode.children,eleMatch = []; 
    for(var i = 0, len = chid.length; i < len; i ++){ 
        if(chid[i] != ele){ 
            eleMatch.push(chid[i]); 
        } 
    } 
    return eleMatch;
}

/*获取行间样式属性*/
getByStyle (obj,name){
    if(obj.currentStyle){
        return  obj.currentStyle[name];
    }else{
        return  getComputedStyle(obj,false)[name];
    }
}

8. Storage 储存操作

class StorageFn {
    constructor () {
        this.ls = window.localStorage;
        this.ss = window.sessionStorage;
    }

    /*-----------------cookie---------------------*/
    /*设置cookie*/
    setCookie (name, value, day) {
        var setting = arguments[0];
        if (Object.prototype.toString.call(setting).slice(8, -1) === 'Object'){
            for (var i in setting) {
                var oDate = new Date();
                oDate.setDate(oDate.getDate() + day);
                document.cookie = i + '=' + setting[i] + ';expires=' + oDate;
            }
        }else{
            var oDate = new Date();
            oDate.setDate(oDate.getDate() + day);
            document.cookie = name + '=' + value + ';expires=' + oDate;
        }

    }

    /*获取cookie*/
    getCookie (name) {
        var arr = document.cookie.split('; ');
        for (var i = 0; i < arr.length; i++) {
            var arr2 = arr[i].split('=');
            if (arr2[0] == name) {
                return arr2[1];
            }
        }
        return '';
    }

    /*删除cookie*/
    removeCookie (name) {
        this.setCookie(name, 1, -1);
    }

    /*-----------------localStorage---------------------*/
    /*设置localStorage*/
    setLocal(key, val) {
        var setting = arguments[0];
        if (Object.prototype.toString.call(setting).slice(8, -1) === 'Object'){
            for(var i in setting){
                this.ls.setItem(i, JSON.stringify(setting[i]))
            }
        }else{
            this.ls.setItem(key, JSON.stringify(val))
        }

    }

    /*获取localStorage*/
    getLocal(key) {
        if (key) return JSON.parse(this.ls.getItem(key))
        return null;

    }

    /*移除localStorage*/
    removeLocal(key) {
        this.ls.removeItem(key)
    }

    /*移除所有localStorage*/
    clearLocal() {
        this.ls.clear()
    }

    /*-----------------sessionStorage---------------------*/
    /*设置sessionStorage*/
    setSession(key, val) {
        var setting = arguments[0];
        if (Object.prototype.toString.call(setting).slice(8, -1) === 'Object'){
            for(var i in setting){
                this.ss.setItem(i, JSON.stringify(setting[i]))
            }
        }else{
            this.ss.setItem(key, JSON.stringify(val))
        }

    }

    /*获取sessionStorage*/
    getSession(key) {
        if (key) return JSON.parse(this.ss.getItem(key))
        return null;

    }

    /*移除sessionStorage*/
    removeSession(key) {
        this.ss.removeItem(key)
    }

    /*移除所有sessionStorage*/
    clearSession() {
        this.ss.clear()
    }

}

9. Other 其它操作

/*获取网址参数*/
getURL(name){
    var reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)");
    var r = decodeURI(window.location.search).substr(1).match(reg);
    if(r!=null) return  r[2]; return null;
}

/*获取全部url参数,并转换成json对象*/
getUrlAllParams (url) {
    var url = url ? url : window.location.href;
    var _pa = url.substring(url.indexOf('?') + 1),
        _arrS = _pa.split('&'),
        _rs = {};
    for (var i = 0, _len = _arrS.length; i < _len; i++) {
        var pos = _arrS[i].indexOf('=');
        if (pos == -1) {
            continue;
        }
        var name = _arrS[i].substring(0, pos),
            value = window.decodeURIComponent(_arrS[i].substring(pos + 1));
        _rs[name] = value;
    }
    return _rs;
}

/*删除url指定参数,返回url*/
delParamsUrl(url, name){
    var baseUrl = url.split('?')[0] + '?';
    var query = url.split('?')[1];
    if (query.indexOf(name)>-1) {
        var obj = {}
        var arr = query.split("&");
        for (var i = 0; i < arr.length; i++) {
            arr[i] = arr[i].split("=");
            obj[arr[i][0]] = arr[i][1];
        };
        delete obj[name];
        var url = baseUrl + JSON.stringify(obj).replace(/[\"\{\}]/g,"").replace(/\:/g,"=").replace(/\,/g,"&");
        return url
    }else{
        return url;
    }
}

/*获取十六进制随机颜色*/
getRandomColor () {
    return '#' + (function(h) {
        return new Array(7 - h.length).join("0") + h;
    })((Math.random() * 0x1000000 << 0).toString(16));
}

/*图片加载*/
imgLoadAll(arr,callback){
    var arrImg = []; 
    for (var i = 0; i < arr.length; i++) {
        var img = new Image();
        img.src = arr[i];
        img.onload = function(){
            arrImg.push(this);
            if (arrImg.length == arr.length) {
                callback && callback();
            }
        }
    }
}

/*音频加载*/
loadAudio(src, callback) {
    var audio = new Audio(src);
    audio.onloadedmetadata = callback;
    audio.src = src;
}

/*DOM转字符串*/
domToStirng(htmlDOM){
    var div= document.createElement("div");
    div.appendChild(htmlDOM);
    return div.innerHTML
}

/*字符串转DOM*/
stringToDom(htmlString){
    var div= document.createElement("div");
    div.innerHTML=htmlString;
    return div.children[0];
}

/**
 * 光标所在位置插入字符,并设置光标位置
 * 
 * @param {dom} 输入框
 * @param {val} 插入的值
 * @param {posLen} 光标位置处在 插入的值的哪个位置
 */
setCursorPosition (dom,val,posLen) {
    var cursorPosition = 0;
    if(dom.selectionStart){
        cursorPosition = dom.selectionStart;
    }
    this.insertAtCursor(dom,val);
    dom.focus();
    console.log(posLen)
    dom.setSelectionRange(dom.value.length,cursorPosition + (posLen || val.length));
}

/*光标所在位置插入字符*/
insertAtCursor(dom, val) {
    if (document.selection){
        dom.focus();
        sel = document.selection.createRange();
        sel.text = val;
        sel.select();
    }else if (dom.selectionStart || dom.selectionStart == '0'){
        let startPos = dom.selectionStart;
        let endPos = dom.selectionEnd;
        let restoreTop = dom.scrollTop;
        dom.value = dom.value.substring(0, startPos) + val + dom.value.substring(endPos, dom.value.length);
        if (restoreTop > 0){
            dom.scrollTop = restoreTop;
        }
        dom.focus();
        dom.selectionStart = startPos + val.length;
        dom.selectionEnd = startPos + val.length;
    } else {
        dom.value += val;
        dom.focus();
    }
}

CSS

1. pc-reset PC样式初始化

/* normalize.css */

html {
  line-height: 1.15;
  /* 1 */
  -ms-text-size-adjust: 100%;
  /* 2 */
  -webkit-text-size-adjust: 100%;
  /* 2 */
}

body {
  margin: 0;
}

article,
aside,
footer,
header,
nav,
section {
  display: block;
}

h1 {
  font-size: 2em;
  margin: 0.67em 0;
}

figcaption,
figure,
main {
  /* 1 */
  display: block;
}

figure {
  margin: 1em 40px;
}

hr {
  box-sizing: content-box;
  /* 1 */
  height: 0;
  /* 1 */
  overflow: visible;
  /* 2 */
}

pre {
  font-family: monospace, monospace;
  /* 1 */
  font-size: 1em;
  /* 2 */
}

a {
  background-color: transparent;
  /* 1 */
  -webkit-text-decoration-skip: objects;
  /* 2 */
}

abbr[title] {
  border-bottom: none;
  /* 1 */
  text-decoration: underline;
  /* 2 */
  text-decoration: underline dotted;
  /* 2 */
}

b,
strong {
  font-weight: inherit;
}

b,
strong {
  font-weight: bolder;
}

code,
kbd,
samp {
  font-family: monospace, monospace;
  /* 1 */
  font-size: 1em;
  /* 2 */
}

dfn {
  font-style: italic;
}

mark {
  background-color: #ff0;
  color: #000;
}

small {
  font-size: 80%;
}

sub,
sup {
  font-size: 75%;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}

sub {
  bottom: -0.25em;
}

sup {
  top: -0.5em;
}

audio,
video {
  display: inline-block;
}

audio:not([controls]) {
  display: none;
  height: 0;
}

img {
  border-style: none;
}

svg:not(:root) {
  overflow: hidden;
}

button,
input,
optgroup,
select,
textarea {
  font-family: sans-serif;
  /* 1 */
  font-size: 100%;
  /* 1 */
  line-height: 1.15;
  /* 1 */
  margin: 0;
  /* 2 */
}

button,
input {
  /* 1 */
  overflow: visible;
}

button,
select {
  /* 1 */
  text-transform: none;
}

button,
html [type="button"],

/* 1 */

[type="reset"],
[type="submit"] {
  -webkit-appearance: button;
  /* 2 */
}

button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
  border-style: none;
  padding: 0;
}

button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
  outline: 1px dotted ButtonText;
}

fieldset {
  padding: 0.35em 0.75em 0.625em;
}

legend {
  box-sizing: border-box;
  /* 1 */
  color: inherit;
  /* 2 */
  display: table;
  /* 1 */
  max-width: 100%;
  /* 1 */
  padding: 0;
  /* 3 */
  white-space: normal;
  /* 1 */
}

progress {
  display: inline-block;
  /* 1 */
  vertical-align: baseline;
  /* 2 */
}

textarea {
  overflow: auto;
}

[type="checkbox"],
[type="radio"] {
  box-sizing: border-box;
  /* 1 */
  padding: 0;
  /* 2 */
}

[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
  height: auto;
}

[type="search"] {
  -webkit-appearance: textfield;
  /* 1 */
  outline-offset: -2px;
  /* 2 */
}

[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
}

 ::-webkit-file-upload-button {
  -webkit-appearance: button;
  /* 1 */
  font: inherit;
  /* 2 */
}

details,

/* 1 */

menu {
  display: block;
}

summary {
  display: list-item;
}

canvas {
  display: inline-block;
}

template {
  display: none;
}

[hidden] {
  display: none;
}

/* reset */

html,
body,
h1,
h2,
h3,
h4,
h5,
h6,
div,
dl,
dt,
dd,
ul,
ol,
li,
p,
blockquote,
pre,
hr,
figure,
table,
caption,
th,
td,
form,
fieldset,
legend,
input,
button,
textarea,
menu {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

2. Phone-reset

/* normalize.css */

html {
  line-height: 1.15;
  /* 1 */
  -ms-text-size-adjust: 100%;
  /* 2 */
  -webkit-text-size-adjust: 100%;
  /* 2 */
}

body {
  margin: 0;
}

article,
aside,
footer,
header,
nav,
section {
  display: block;
}

h1 {
  font-size: 2em;
  margin: 0.67em 0;
}

figcaption,
figure,
main {
  /* 1 */
  display: block;
}

figure {
  margin: 1em 40px;
}

hr {
  box-sizing: content-box;
  /* 1 */
  height: 0;
  /* 1 */
  overflow: visible;
  /* 2 */
}

pre {
  font-family: monospace, monospace;
  /* 1 */
  font-size: 1em;
  /* 2 */
}

a {
  background-color: transparent;
  /* 1 */
  -webkit-text-decoration-skip: objects;
  /* 2 */
}

abbr[title] {
  border-bottom: none;
  /* 1 */
  text-decoration: underline;
  /* 2 */
  text-decoration: underline dotted;
  /* 2 */
}

b,
strong {
  font-weight: inherit;
}

b,
strong {
  font-weight: bolder;
}

code,
kbd,
samp {
  font-family: monospace, monospace;
  /* 1 */
  font-size: 1em;
  /* 2 */
}

dfn {
  font-style: italic;
}

mark {
  background-color: #ff0;
  color: #000;
}

small {
  font-size: 80%;
}

sub,
sup {
  font-size: 75%;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}

sub {
  bottom: -0.25em;
}

sup {
  top: -0.5em;
}

audio,
video {
  display: inline-block;
}

audio:not([controls]) {
  display: none;
  height: 0;
}

img {
  border-style: none;
}

svg:not(:root) {
  overflow: hidden;
}

button,
input,
optgroup,
select,
textarea {
  font-family: sans-serif;
  /* 1 */
  font-size: 100%;
  /* 1 */
  line-height: 1.15;
  /* 1 */
  margin: 0;
  /* 2 */
}

button,
input {
  /* 1 */
  overflow: visible;
}

button,
select {
  /* 1 */
  text-transform: none;
}

button,
html [type="button"],

/* 1 */

[type="reset"],
[type="submit"] {
  -webkit-appearance: button;
  /* 2 */
}

button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
  border-style: none;
  padding: 0;
}

button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
  outline: 1px dotted ButtonText;
}

fieldset {
  padding: 0.35em 0.75em 0.625em;
}

legend {
  box-sizing: border-box;
  /* 1 */
  color: inherit;
  /* 2 */
  display: table;
  /* 1 */
  max-width: 100%;
  /* 1 */
  padding: 0;
  /* 3 */
  white-space: normal;
  /* 1 */
}

progress {
  display: inline-block;
  /* 1 */
  vertical-align: baseline;
  /* 2 */
}

textarea {
  overflow: auto;
}

[type="checkbox"],
[type="radio"] {
  box-sizing: border-box;
  /* 1 */
  padding: 0;
  /* 2 */
}

[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
  height: auto;
}

[type="search"] {
  -webkit-appearance: textfield;
  /* 1 */
  outline-offset: -2px;
  /* 2 */
}

[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
}

 ::-webkit-file-upload-button {
  -webkit-appearance: button;
  /* 1 */
  font: inherit;
  /* 2 */
}

details,

/* 1 */

menu {
  display: block;
}

summary {
  display: list-item;
}

canvas {
  display: inline-block;
}

template {
  display: none;
}

[hidden] {
  display: none;
}

/* reset */

html,
body,
h1,
h2,
h3,
h4,
h5,
h6,
div,
dl,
dt,
dd,
ul,
ol,
li,
p,
blockquote,
pre,
hr,
figure,
table,
caption,
th,
td,
form,
fieldset,
legend,
input,
button,
textarea,
menu {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html,
body {
  /* 禁止选中文本 */
  -webkit-user-select: none;
  user-select: none;
  font: Oswald, 'Open Sans', Helvetica, Arial, sans-serif
}

/* 禁止长按链接与图片弹出菜单 */

a,
img {
  -webkit-touch-callout: none;
}

/*ios android去除自带阴影的样式*/

a,
input {
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

input[type="text"] {
  -webkit-appearance: none;
}

2. Phone-reset

/* normalize.css */

html {
  line-height: 1.15;
  /* 1 */
  -ms-text-size-adjust: 100%;
  /* 2 */
  -webkit-text-size-adjust: 100%;
  /* 2 */
}

body {
  margin: 0;
}

article,
aside,
footer,
header,
nav,
section {
  display: block;
}

h1 {
  font-size: 2em;
  margin: 0.67em 0;
}

figcaption,
figure,
main {
  /* 1 */
  display: block;
}

figure {
  margin: 1em 40px;
}

hr {
  box-sizing: content-box;
  /* 1 */
  height: 0;
  /* 1 */
  overflow: visible;
  /* 2 */
}

pre {
  font-family: monospace, monospace;
  /* 1 */
  font-size: 1em;
  /* 2 */
}

a {
  background-color: transparent;
  /* 1 */
  -webkit-text-decoration-skip: objects;
  /* 2 */
}

abbr[title] {
  border-bottom: none;
  /* 1 */
  text-decoration: underline;
  /* 2 */
  text-decoration: underline dotted;
  /* 2 */
}

b,
strong {
  font-weight: inherit;
}

b,
strong {
  font-weight: bolder;
}

code,
kbd,
samp {
  font-family: monospace, monospace;
  /* 1 */
  font-size: 1em;
  /* 2 */
}

dfn {
  font-style: italic;
}

mark {
  background-color: #ff0;
  color: #000;
}

small {
  font-size: 80%;
}

sub,
sup {
  font-size: 75%;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}

sub {
  bottom: -0.25em;
}

sup {
  top: -0.5em;
}

audio,
video {
  display: inline-block;
}

audio:not([controls]) {
  display: none;
  height: 0;
}

img {
  border-style: none;
}

svg:not(:root) {
  overflow: hidden;
}

button,
input,
optgroup,
select,
textarea {
  font-family: sans-serif;
  /* 1 */
  font-size: 100%;
  /* 1 */
  line-height: 1.15;
  /* 1 */
  margin: 0;
  /* 2 */
}

button,
input {
  /* 1 */
  overflow: visible;
}

button,
select {
  /* 1 */
  text-transform: none;
}

button,
html [type="button"],

/* 1 */

[type="reset"],
[type="submit"] {
  -webkit-appearance: button;
  /* 2 */
}

button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
  border-style: none;
  padding: 0;
}

button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
  outline: 1px dotted ButtonText;
}

fieldset {
  padding: 0.35em 0.75em 0.625em;
}

legend {
  box-sizing: border-box;
  /* 1 */
  color: inherit;
  /* 2 */
  display: table;
  /* 1 */
  max-width: 100%;
  /* 1 */
  padding: 0;
  /* 3 */
  white-space: normal;
  /* 1 */
}

progress {
  display: inline-block;
  /* 1 */
  vertical-align: baseline;
  /* 2 */
}

textarea {
  overflow: auto;
}

[type="checkbox"],
[type="radio"] {
  box-sizing: border-box;
  /* 1 */
  padding: 0;
  /* 2 */
}

[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
  height: auto;
}

[type="search"] {
  -webkit-appearance: textfield;
  /* 1 */
  outline-offset: -2px;
  /* 2 */
}

[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
  -webkit-appearance: none;
}

 ::-webkit-file-upload-button {
  -webkit-appearance: button;
  /* 1 */
  font: inherit;
  /* 2 */
}

details,

/* 1 */

menu {
  display: block;
}

summary {
  display: list-item;
}

canvas {
  display: inline-block;
}

template {
  display: none;
}

[hidden] {
  display: none;
}

/* reset */

html,
body,
h1,
h2,
h3,
h4,
h5,
h6,
div,
dl,
dt,
dd,
ul,
ol,
li,
p,
blockquote,
pre,
hr,
figure,
table,
caption,
th,
td,
form,
fieldset,
legend,
input,
button,
textarea,
menu {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html,
body {
  /* 禁止选中文本 */
  -webkit-user-select: none;
  user-select: none;
  font: Oswald, 'Open Sans', Helvetica, Arial, sans-serif
}

/* 禁止长按链接与图片弹出菜单 */

a,
img {
  -webkit-touch-callout: none;
}

/*ios android去除自带阴影的样式*/

a,
input {
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

input[type="text"] {
  -webkit-appearance: none;
}

3. 公共样式提取