樹醬但願將前端的樂趣帶給你們 本文已收錄 github.com/littleTreem… 喜歡就star✨javascript
在平常開發中,特別是中後臺管理頁面,會常用到一些經常使用的函數好比:防抖節流、本地存儲相關、時間格式化等,可是隨着項目不斷增長,複用性和通用性就成爲一個很相當重要的問題,如何減小複製張貼的操做,那就是封裝成爲,適用與多項目統一的工具包,並用npm進行管理,「U盤式安裝」的方式能夠提升團隊的效率,那今天就講講開發一個簡易的工具庫須要涉及哪些環節,看下圖👇前端
開發一個工具庫,到底須要哪些配置,下面是我寫的一個簡易版工具庫(kdutil)的案例👇vue
涉及到的有:java
爲何須要打包?工具庫涉及到多模塊化開發,須要保留單個模塊的可維護性,其次是爲了解決部分低版本瀏覽器不支持es6語法,須要轉換爲es5語法,爲瀏覽器使用,該項目採用webpack做爲前端打包工具node
// 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()
# 啓用做用域提高,做用是讓代碼文件更小、運行的更快
]
};
複製代碼
配置解析:webpack
由於在通常SPA項目中,使用webpack無需關注這兩個屬性,可是若是是開發類庫,那麼這兩個屬性就是必須瞭解的。ios
libraryTarget 有主要幾種常見的形式👇:git
libraryTarget: 「var」(default): library會將值做爲變量聲明導出(當使用 script 標籤時,其執行後將在全局做用域可用)es6
libraryTarget: 「window」 : 當 library 加載完成,返回值將分配給 window 對象。github
libraryTarget: 「commonjs」 : 當 library 加載完成,返回值將分配給 exports 對象,這個名稱也意味着模塊用於 CommonJS 環境(node環境)
libraryTarget: 「umd」 :這是一種能夠將你的 library 可以在全部的模塊定義下均可運行的方式。它將在 CommonJS, AMD 環境下運行 (目前該工具庫使用🚀)
而library指定的是你require或者import時候的模塊名
該工具庫包含多個功能模塊,如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'),
}
複製代碼
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()可傳入三個參數分別是:
完成工具庫模塊化開發以後,爲了保證代碼的質量,驗證各模塊功能完整性,咱們須要對各模塊進行測試後,確保功能正常使用,再進行發佈
我在工具庫開發使用jest做爲單元測試框架,Jest 是 Facebook 開源的一款 JS 單元測試框架,Jest 除了基本的斷言和 Mock 功能外,還有快照測試、覆蓋度報告等實用功能 ,關於更多單元測試的學習前往《前端單元測試那些事》 傳送門🚪
下面我那date模塊來做爲一個案例,是如何對該模塊進行測試的
// 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
完成上面一系列開發後,接下來就是如何將全部模塊打包成工具庫了,這個時候就輪到「腳本命令」 這個主角登場了
經過在packjson中定義腳本命令以下👇
{
"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
完成上述腳本命令的設置,如今輪到最後的一步就是「發包」,使用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地址github.com/littleTreem…,若是感到對你有幫助,給個star ✨,很是感謝
往期文章