在平常開發中,特別是中後臺管理頁面,會常用到一些經常使用的函數好比:防抖節流、本地存儲相關、時間格式化等,可是隨着項目不斷增長,複用性和通用性就成爲一個很相當重要的問題,如何減小複製張貼的操做,那就是封裝成爲,適用與多項目統一的工具包,並用npm進行管理,「U盤式安裝」的方式能夠提升團隊的效率,那今天就講講開發一個簡易的工具庫須要涉及哪些環節,看下圖👇
開發一個工具庫,到底須要哪些配置,下面是我寫的一個簡易版工具庫(kdutil)的案例👇
涉及到的有:javascript
爲何須要打包?工具庫涉及到多模塊化開發,須要保留單個模塊的可維護性,其次是爲了解決部分低版本瀏覽器不支持es6語法,須要轉換爲es5語法,爲瀏覽器使用,該項目採用webpack做爲前端打包工具
// webpack.pro.config.js const webpack = require('webpack'); const path = require('path'); const {name} = require('../package.json'); const rootPath = path.resolve(__dirname, '../'); module.exports = { mode: 'production', entry: { kdutil: path.resolve(rootPath, 'src/index.js'), }, output: { filename: `[name].min.js`, path: path.resolve(rootPath, 'dist'), library: `${name}`, libraryTarget: "umd" }, module: { rules: [ { test: /\.js$/, loader: "babel-loader", exclude: /node_modules/ }, ] }, plugins: [ new webpack.optimize.ModuleConcatenationPlugin() # 啓用做用域提高,做用是讓代碼文件更小、運行的更快 ] };
配置解析:前端
由於在通常SPA項目中,使用webpack無需關注這兩個屬性,可是若是是開發類庫,那麼這兩個屬性就是必須瞭解的。
libraryTarget 有主要幾種常見的形式👇:vue
而library指定的是你require或者import時候的模塊名java
該工具庫包含多個功能模塊,如localstorage、date、http等等,就須要將不一樣功能模塊分開管理,最後使用webpack解析require.context(), 經過require.context() 函數來建立本身的上下文,導出全部的模塊,下面是kdutil工具庫包含的全部模塊👇
localStorage是Html5的新特徵,用來做爲本地存儲來使用的,解決了cookie存儲空間不足的問題,localStorage中通常瀏覽器支持的是5M大小
/* @file: localStorage 本地存儲 @Author: tree */ module.exports = { get: function (name) { if (!name) return; return window.localStorage.getItem(name); }, set: function (name, content) { if (!name) return; if (typeof content !== 'string') { content = JSON.stringify(content); } window.localStorage.setItem(name, content); }, delete: function (name) { if (!name) return; window.localStorage.removeItem(name); } };
平常開發中常常須要格式化時間,好比將時間設置爲 2019-04-03 23:32:32
/* * @file date 格式化 * @author:tree * @createBy:@2020.04.07 */ module.exports = { /** * 格式化如今的已過期間 * @param startTime {Date} * @return {String} */ formatPassTime: function (startTime) { let currentTime = Date.parse(new Date()), time = currentTime - startTime, day = parseInt(time / (1000 * 60 * 60 * 24)), hour = parseInt(time / (1000 * 60 * 60)), min = parseInt(time / (1000 * 60)), month = parseInt(day / 30), year = parseInt(month / 12); if (year) return year + "年前"; if (month) return month + "個月前"; if (day) return day + "天前"; if (hour) return hour + "小時前"; if (min) return min + "分鐘前"; else return '剛剛'; }, /** * 格式化時間戳 * @param time {number} 時間戳 * @param fmt {string} 格式 * @return {String} */ formatTime: function (time, fmt = 'yyyy-mm-dd hh:mm:ss') { let ret; let date = new Date(time); let opt = { "y+": date.getFullYear().toString(), "M+": (date.getMonth() + 1).toString(), //月份 "d+": date.getDate().toString(), //日 "h+": date.getHours().toString(), //小時 "m+": date.getMinutes().toString(), //分 "s+": date.getSeconds().toString(), //秒 }; for (let k in opt) { ret = new RegExp("(" + k + ")").exec(fmt); if (ret) { fmt = fmt.replace(ret[1], (ret[1].length === 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0"))) } } return fmt; } };
tools 模塊包含一些經常使用的工具函數,包括防抖節流函數、深拷貝、正則類型判斷等等,後期還會添加更多通用的工具函數,慢慢地把項目原先依賴的lodash一個一致性、模塊化、高性能的 JavaScript 實用工具庫)去掉
/* @file: tools 經常使用的工具函數 @Author:tree */ module.exports = { /** * 遞歸 深拷貝 * @param data: 拷貝的數據 */ deepCopyBy: function (data) { const t = getType(data); let o; if (t === 'array') { o = []; } else if (t === 'object') { o = {}; } else { return data; } if (t === 'array') { for (let i = 0; i < data.length; i++) { o.push(deepCopy(data[i])); } } else if (t === 'object') { for (let i in data) { o[i] = deepCopy(data[i]); } } return o; }, /** * JSON 深拷貝 * @param data: 拷貝的數據 * @return data Object 複製後生成的對象 */ deepCopy: function (data) { return JSON.parse(JSON.stringify(data)); }, /** * 根據類型返回正則 * @param str{string}: 檢測的內容 * @param type{string}: 檢測類型 */ checkType: function (str, type) { const regexp = { 'ip': /((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}/.test(str), 'port': /^(\d|[1-5]\d{4}|6[1-4]\d{3}|65[1-4]\d{2}|655[1-2]\d|6553[1-5])$/.test(str), 'phone': /^1[3|4|5|6|7|8][0-9]{9}$/.test(str), //手機號 'number': /^[0-9]+$/.test(str), //是否全數字, 'email': /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/.test(str), 'IDCard': /^(^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$)|(^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])((\d{4})|\d{3}[Xx])$)$/.test(str), 'url': /[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/i.test(str) }; return regexp[type]; }, /** * 將手機號中間部分替換爲星號 * @param phone{string}: 手機號碼 */ formatPhone: function (phone) { return phone.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2"); }, /** * 防抖 * @param func {*} 執行函數 * @param wait {*} 節流時間,毫秒 */ debounce: (func, wait) => { let timeout; return function () { let context = this; let args = arguments; if (timeout) clearTimeout(timeout); timeout = setTimeout(() => { func.apply(context, args) }, wait); } }, /** * 節流 * @param func {*} 執行函數 * @param wait {*} 節流時間,毫秒 */ throttle: (func, wait) => { let previous = 0; return function () { let now = Date.now(); let context = this; if (now - previous > wait) { func.apply(context, arguments); previous = now; } } }, }; // 類型檢測 function getType(obj) { return Object.prototype.toString.call(obj).slice(8, -1); }
http 模塊本質是基於axios作的二次封裝,添加攔截器,經過攔截器統一處理全部http請求和響應。配置http request inteceptor,統一配置請求頭,好比token,再經過配置http response inteceptor,當接口返回狀態碼401 Unauthorized(未受權),讓用戶回到登陸頁面。
/* @file: http 請求庫 @Author: tree */ import axios from 'axios'; import httpCode from '../../consts/httpCode'; import localStorage from '../localStorage' const _axios = axios.create({}); _axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; _axios.interceptors.request.use( (config) => { if (localStorage.get('token')) { config.headers.token = localStorage.get('token'); } return config; }, (err) => Promise.reject(err), ); _axios.interceptors.response.use( (response) => { return response; }, (error) => { if (error && error.response) { if (error.response.status === 401) { //todo } } return Promise.reject(error.response && error.response.data); }, ); const request = function (url, params, config, method) { return _axios[method](url, params, Object.assign({}, config)) .then(checkStatus).then(checkCode); }; // 處理網絡請求帶來的校驗 function checkStatus(response) { // 若是 http 狀態碼正常, 則直接返回數據 if (response && (response.status === 200 || response.status === 304 || response.status === 400)) { return response.data || httpCode.NET_ERROR } return httpCode.NET_ERROR } // 校驗服務器返回數據 function checkCode(res) { return res; } export default { init: function (option = {withCredentials: true}) { _axios.defaults.baseURL = option.url; _axios.defaults.timeout = option.timeout || 20000; _axios.defaults.withCredentials = option.withCredentials; }, get: (url, params, config = {}) => request(url, params, config, 'get'), post: (url, params, config = {}) => request(url, params, config, 'post'), }
#### 3.5 sentry 監控模塊node
sentry是開源的前端異常監控上報工具,經過集成到項目中,你能夠在不一樣環境(測試,生產等)中,幫你收集記錄問題,並定位到問題所在代碼,kutil 也在項目作了sentry的支持
/* * @file: sentry 異常上報日誌監控 * @Author:tree, * 經常使用配置 option:https://docs.sentry.io/clients/javascript/config/ * 1.自動捕獲vue組件內異常 * 2.自動捕獲promise內的異常 * 3.自動捕獲沒有被catch的運行異常 */ import Raven from 'raven-js'; import RavenVue from 'raven-js/plugins/vue'; class Report { constructor(Vue, options = {}) { this.vue = Vue; this.options = options; } static getInstance(Vue, Option) { if (!(this.instance instanceof this)) { this.instance = new this(Vue, Option); this.instance.install(); } return this.instance; } install() { if (process.env.NODE_ENV !== 'development') { Raven.config(this.options.dsn, { environment: process.env.NODE_ENV, }).addPlugin(RavenVue, this.Vue).install(); // raven內置了vue插件,會經過vue.config.errorHandler來捕獲vue組件內錯誤並上報sentry服務 // 記錄用戶信息 Raven.setUserContext({user: this.options.user || ''}); // 設置全局tag標籤 Raven.setTagsContext({environment: this.options.env || ''}); } } /** * 主動上報 * type: 'info','warning','error' */ log(data = null, type = 'error', options = {}) { // 添加麪包屑 Raven.captureBreadcrumb({ message: data, category: 'manual message', }); // 異常上報 if (data instanceof Error) { Raven.captureException(data, { level: type, logger: 'manual exception', tags: {options}, }); } else { Raven.captureException('error', { level: type, logger: 'manual data', extra: { data, options: this.options, date: new Date(), }, }); } } } export default Report;
當全部模塊開發完成以後,咱們須要將各模塊導出,這裏用到了require.context遍歷文件夾中的指定文件,而後自動導入,而不用每一個模塊單獨去導入
// src/index.js /* * @author:tree */ let utils = {}; let haveDefault = ['http','sentry']; const modules = require.context('./modules/', true, /.js$/); modules.keys().forEach(modulesKey => { let attr = modulesKey.replace('./', '').replace('.js', '').replace('/index', ''); if (haveDefault.includes(attr)) { utils[attr] = modules(modulesKey).default; }else { utils[attr] = modules(modulesKey); } }); module.exports = utils;
關於 require.context的使用,require.context() 它容許傳入一個目錄進行搜索,一個標誌表示是否也應該搜索子目錄,以及一個正則表達式來匹配文件,當你構建項目時,webpack會處理require.context的內容
require.context()可傳入三個參數分別是:webpack
完成工具庫模塊化開發以後,爲了保證代碼的質量,驗證各模塊功能完整性,咱們須要對各模塊進行測試後,確保功能正常使用,再進行發佈
我在工具庫開發使用jest做爲單元測試框架,Jest 是 Facebook 開源的一款 JS 單元測試框架,Jest 除了基本的斷言和 Mock 功能外,還有快照測試、覆蓋度報告等實用功能
,關於更多單元測試的學習前往《前端單元測試那些事》 傳送門🚪ios
下面我那date模塊來做爲一個案例,是如何對該模塊進行測試的git
// jest.config.js const path = require('path'); module.exports = { verbose: true, rootDir: path.resolve(__dirname, '../../'), moduleFileExtensions: [ 'js', 'json', ], testMatch: [ // 匹配測試用例的文件 '<rootDir>/test/unit/specs/*.test.js', ], transformIgnorePatterns: ['/node_modules/'], };
// date.test.js const date = require('../../../src/modules/date'); describe('date 模塊', () => { test('formatTime()默認格式,返回時間格式是否正常', () => { expect(date.formatTime(1586934316925)).toBe('2020-04-15 15:05:16'); }) test('formatTime()傳參數,返回時間格式是否正常', () => { expect(date.formatTime(1586934316925,'yyyy.MM.dd')).toBe('2020.04.15'); }) });
執行 npm run test
es6
完成上面一系列開發後,接下來就是如何將全部模塊打包成工具庫了,這個時候就輪到「腳本命令」
這個主角登場了
經過在packjson中定義腳本命令以下👇github
{ "scripts": { "build_rollup": "rollup -c", "build": "webpack --config ./build/webpack.pro.config.js" "test": "jest --config src/test/unit/jest.conf.js", }, ... }
配置完後,執行 npm run build
執行完成,dist目錄將會出現生成的 kdutil.min.js , 這也是工具庫最終上傳到npm的「入口文件「
完成上述腳本命令的設置,如今輪到最後的一步就是「發包」,使用npm來進行包管理
//package.json { "name": "kdutil", "version": "0.0.2", # 包的版本號,每次發佈不能重複 "main": "dist/kdutil.min.js", # 打包完的目標文件 "author": "tree <shuxin_liu@kingdee.com>", "keywords": [ "utils", "tool", "kdutil" ], ... }
首先須要先登陸你的npm帳號,而後執行發佈命令
npm login # 登陸你上面註冊的npm帳號 npm publish # 登陸成功後,執行發佈命令 + kdutil@0.0.2 # 發佈成功顯示npm報名及包的版本號
經過上文所述,咱們就從0到1完成來一個簡易版的工具庫kdutil,這是github地址 https://github.com/littleTreeme/kdutil🚀,若是感到對你有幫助,給個star ✨,很是感謝