整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          分享 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}/p5vph55 {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}-3r5hj5h {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. 公共樣式提取


          主站蜘蛛池模板: 国产精品无码一区二区三区毛片| 亚洲av综合av一区| 亚洲国产精品无码久久一区二区 | 天天综合色一区二区三区| 无码人妻精品一区二区蜜桃AV| 国产乱码精品一区二区三区中文 | 日韩精品中文字幕无码一区| 国产精品盗摄一区二区在线| 中文字幕精品无码一区二区三区 | 国产一区在线电影| 亚洲一区二区三区久久久久| 伊人色综合一区二区三区 | 亚洲免费视频一区二区三区| 中文字幕一区二区精品区| 亚洲综合色自拍一区| 日韩在线视频不卡一区二区三区 | 精品成人av一区二区三区| 无码一区二区三区免费视频| 精品人妻码一区二区三区| 亚洲无线码一区二区三区| 区三区激情福利综合中文字幕在线一区亚洲视频1 | 亚洲精品无码一区二区| 无码av免费一区二区三区| 精品视频一区二区三区四区五区| 国产精品无码一区二区三区免费| 国产精品综合AV一区二区国产馆| 久久综合九九亚洲一区| 国产成人一区二区三区在线| 精品亚洲一区二区三区在线观看| 国产一区二区三区播放| 成人免费一区二区三区在线观看| 中文字幕日韩精品一区二区三区| 奇米精品视频一区二区三区| 精品久久一区二区三区| 中文字幕精品一区二区三区视频| 狠狠做深爱婷婷综合一区 | 国产精品伦一区二区三级视频| 大伊香蕉精品一区视频在线| 一区二区三区福利视频| 国产精品无码不卡一区二区三区 | 岛国无码av不卡一区二区|