打造自己的 JavaScript 武器库

作为战斗在业务一线的前端,要想少加班,就要想办法提高工作效率。这里提一个小点,我们在业务开发过程中,经常会重复用到日期格式化、url参数转对象、浏览器类型判断、节流函数等一类函数,这些工具类函数,基本上在每个项目都会用到,为避免不同项目多次复制粘贴的麻烦,我们可以统一封装,发布到npm,以提高开发效率。

这里,笔者已经封装并发布了自己的武器库 outils,如果你对本项目感兴趣,欢迎 star 本项目。当然你也可以在本项目的基础上封装自己的武器库。

常用函数汇总

这里先分类整理下,之前项目中多次用到的工具函数。

1.Array

1.1 arrayEqual

/**

*

* @desc 判断两个数组是否相等

* @param {Array} arr1

* @param {Array} arr2

* @return {Boolean}

*/

functionarrayEqual(arr1, arr2) {

if(arr1 === arr2)returntrue;

if(arr1.length != arr2.length)returnfalse;

for(vari =0; i < arr1.length; ++i) {

if(arr1[i] !== arr2[i])returnfalse;

}

returntrue;

}

2.Class

2.1 addClass

/**

*

* @desc   为元素添加class

* @param  {HTMLElement} ele

* @param  {String} cls

*/

varhasClass =require('./hasClass');

functionaddClass(ele, cls) {

if(!hasClass(ele, cls)) {

ele.className +=' '+ cls;

}

}

2.2 hasClass

/**

*

* @desc 判断元素是否有某个class

* @param {HTMLElement} ele

* @param {String} cls

* @return {Boolean}

*/

functionhasClass(ele, cls) {

return(newRegExp('(\\s|^)'+ cls +'(\\s|$)')).test(ele.className);

}

2.3 removeClass

/**

*

* @desc 为元素移除class

* @param {HTMLElement} ele

* @param {String} cls

*/

varhasClass =require('./hasClass');

functionremoveClass(ele, cls) {

if(hasClass(ele, cls)) {

varreg =newRegExp('(\\s|^)'+ cls +'(\\s|$)');

ele.className = ele.className.replace(reg,' ');

}

}

3.Cookie

3.1 getCookie

/**

*

* @desc 根据name读取cookie

* @param  {String} name

* @return {String}

*/

functiongetCookie(name) {

vararr = document.cookie.replace(/\s/g,"").split(';');

for(vari =0; i < arr.length; i++) {

vartempArr = arr[i].split('=');

if(tempArr[0] == name) {

returndecodeURIComponent(tempArr[1]);

}

}

return'';

}

3.2 removeCookie

varsetCookie =require('./setCookie');

/**

*

* @desc 根据name删除cookie

* @param  {String} name

*/

functionremoveCookie(name) {

// 设置已过期,系统会立刻删除cookie

setCookie(name,'1', -1);

}

3.3 setCookie

/**

*

* @desc  设置Cookie

* @param {String} name

* @param {String} value

* @param {Number} days

*/

functionsetCookie(name, value, days) {

vardate =newDate();

date.setDate(date.getDate() + days);

document.cookie = name +'='+ value +';expires='+ date;

}

4.Device

4.1 getExplore

/**

*

* @desc 获取浏览器类型和版本

* @return {String}

*/

functiongetExplore() {

varsys = {},

ua = navigator.userAgent.toLowerCase(),

s;

(s = ua.match(/rv:([\d.]+)\) like gecko/)) ? sys.ie = s[1]:

(s = ua.match(/msie ([\d\.]+)/)) ? sys.ie = s[1] :

(s = ua.match(/edge\/([\d\.]+)/)) ? sys.edge = s[1] :

(s = ua.match(/firefox\/([\d\.]+)/)) ? sys.firefox = s[1] :

(s = ua.match(/(?:opera|opr).([\d\.]+)/)) ? sys.opera = s[1] :

(s = ua.match(/chrome\/([\d\.]+)/)) ? sys.chrome = s[1] :

(s = ua.match(/version\/([\d\.]+).*safari/)) ? sys.safari = s[1] :0;

// 根据关系进行判断

if(sys.ie)return('IE: '+ sys.ie)

if(sys.edge)return('EDGE: '+ sys.edge)

if(sys.firefox)return('Firefox: '+ sys.firefox)

if(sys.chrome)return('Chrome: '+ sys.chrome)

if(sys.opera)return('Opera: '+ sys.opera)

if(sys.safari)return('Safari: '+ sys.safari)

return'Unkonwn'

}

4.2 getOS

/**

*

* @desc 获取操作系统类型

* @return {String}

*/

functiongetOS() {

varuserAgent ='navigator'inwindow &&'userAgent'innavigator && navigator.userAgent.toLowerCase() ||'';

varvendor ='navigator'inwindow &&'vendor'innavigator && navigator.vendor.toLowerCase() ||'';

varappVersion ='navigator'inwindow &&'appVersion'innavigator && navigator.appVersion.toLowerCase() ||'';

if(/mac/i.test(appVersion))return'MacOSX'

if(/win/i.test(appVersion))return'windows'

if(/linux/i.test(appVersion))return'linux'

if(/iphone/i.test(userAgent) ||/ipad/i.test(userAgent) ||/ipod/i.test(userAgent))'ios'

if(/android/i.test(userAgent))return'android'

if(/win/i.test(appVersion) &&/phone/i.test(userAgent))return'windowsPhone'

}

5.Dom

5.1 getScrollTop

/**

*

* @desc 获取滚动条距顶部的距离

*/

functiongetScrollTop() {

return(document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop;

}

5.2 offset

/**

*

* @desc  获取一个元素的距离文档(document)的位置,类似jQ中的offset()

* @param {HTMLElement} ele

* @returns { {left: number, top: number} }

*/

functionoffset(ele) {

varpos = {

left:0,

top:0

};

while(ele) {

pos.left += ele.offsetLeft;

pos.top += ele.offsetTop;

ele = ele.offsetParent;

};

returnpos;

}

5.3 scrollTo

vargetScrollTop =require('./getScrollTop');

varsetScrollTop =require('./setScrollTop');

varrequestAnimFrame = (function() {

returnwindow.requestAnimationFrame ||

window.webkitRequestAnimationFrame ||

window.mozRequestAnimationFrame ||

function(callback) {

window.setTimeout(callback,1000/60);

};

})();

/**

*

* @desc  在${duration}时间内,滚动条平滑滚动到${to}指定位置

* @param {Number} to

* @param {Number} duration

*/

functionscrollTo(to, duration) {

if(duration <0) {

setScrollTop(to);

return

}

vardiff = to - getScrollTop();

if(diff ===0)return

varstep = diff / duration *10;

requestAnimationFrame(

function() {

if(Math.abs(step) >Math.abs(diff)) {

setScrollTop(getScrollTop() + diff);

return;

}

setScrollTop(getScrollTop() + step);

if(diff >0&& getScrollTop() >= to || diff <0&& getScrollTop() <= to) {

return;

}

scrollTo(to, duration -16);

});

}

5.4 setScrollTop

/**

*

* @desc 设置滚动条距顶部的距离

*/

functionsetScrollTop(value) {

window.scrollTo(0, value);

returnvalue;

}

6.Keycode

6.1 getKeyName

varkeyCodeMap = {

8:'Backspace',

9:'Tab',

13:'Enter',

16:'Shift',

17:'Ctrl',

18:'Alt',

19:'Pause',

20:'Caps Lock',

27:'Escape',

32:'Space',

33:'Page Up',

34:'Page Down',

35:'End',

36:'Home',

37:'Left',

38:'Up',

39:'Right',

40:'Down',

42:'Print Screen',

45:'Insert',

46:'Delete',

48:'0',

49:'1',

50:'2',

51:'3',

52:'4',

53:'5',

54:'6',

55:'7',

56:'8',

57:'9',

65:'A',

66:'B',

67:'C',

68:'D',

69:'E',

70:'F',

71:'G',

72:'H',

73:'I',

74:'J',

75:'K',

76:'L',

77:'M',

78:'N',

79:'O',

80:'P',

81:'Q',

82:'R',

83:'S',

84:'T',

85:'U',

86:'V',

87:'W',

88:'X',

89:'Y',

90:'Z',

91:'Windows',

93:'Right Click',

96:'Numpad 0',

97:'Numpad 1',

98:'Numpad 2',

99:'Numpad 3',

100:'Numpad 4',

101:'Numpad 5',

102:'Numpad 6',

103:'Numpad 7',

104:'Numpad 8',

105:'Numpad 9',

106:'Numpad *',

107:'Numpad +',

109:'Numpad -',

110:'Numpad .',

111:'Numpad /',

112:'F1',

113:'F2',

114:'F3',

115:'F4',

116:'F5',

117:'F6',

118:'F7',

119:'F8',

120:'F9',

121:'F10',

122:'F11',

123:'F12',

144:'Num Lock',

145:'Scroll Lock',

182:'My Computer',

183:'My Calculator',

186:';',

187:'=',

188:',',

189:'-',

190:'.',

191:'/',

192:'`',

219:'[',

220:'\\',

221:']',

222:'\''

};

/**

* @desc 根据keycode获得键名

* @param  {Number} keycode

* @return {String}

*/

functiongetKeyName(keycode) {

if(keyCodeMap[keycode]) {

returnkeyCodeMap[keycode];

}else{

console.log('Unknow Key(Key Code:'+ keycode +')');

return'';

}

};

7.Object

7.1 deepClone

/**

* @desc 深拷贝,支持常见类型

* @param {Any} values

*/

functiondeepClone(values) {

varcopy;

// Handle the 3 simple types, and null or undefined

if(null== values ||"object"!=typeofvalues)returnvalues;

// Handle Date

if(valuesinstanceofDate) {

copy =newDate();

copy.setTime(values.getTime());

returncopy;

}

// Handle Array

if(valuesinstanceofArray) {

copy = [];

for(vari =0, len = values.length; i < len; i++) {

copy[i] = deepClone(values[i]);

}

returncopy;

}

// Handle Object

if(valuesinstanceofObject) {

copy = {};

for(varattrinvalues) {

if(values.hasOwnProperty(attr)) copy[attr] = deepClone(values[attr]);

}

returncopy;

}

thrownewError("Unable to copy values! Its type isn't supported.");

}

7.2 isEmptyObject

/**

*

* @desc   判断`obj`是否为空

* @param  {Object} obj

* @return {Boolean}

*/

functionisEmptyObject(obj) {

if(!obj ||typeofobj !=='object'||Array.isArray(obj))

returnfalse

return!Object.keys(obj).length

}

8.Random

8.1 randomColor

/**

*

* @desc 随机生成颜色

* @return {String}

*/

functionrandomColor() {

return'#'+ ('00000'+ (Math.random() *0x1000000<<0).toString(16)).slice(-6);

}

8.2 randomNum

/**

*

* @desc 生成指定范围随机数

* @param  {Number} min

* @param  {Number} max

* @return {Number}

*/

functionrandomNum(min, max) {

returnMath.floor(min +Math.random() * (max - min));

}

9.Regexp

9.1 isEmail

/**

*

* @desc   判断是否为邮箱地址

* @param  {String}  str

* @return {Boolean}

*/

functionisEmail(str) {

return/\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/.test(str);

}

9.2 isIdCard

/**

*

* @desc  判断是否为身份证号

* @param  {String|Number} str

* @return {Boolean}

*/

functionisIdCard(str) {

return/^(^[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)

}

9.3 isPhoneNum

/**

*

* @desc   判断是否为手机号

* @param  {String|Number} str

* @return {Boolean}

*/

functionisPhoneNum(str) {

return/^(0|86|17951)?(13[0-9]|15[012356789]|17[678]|18[0-9]|14[57])[0-9]{8}$/.test(str)

}

9.4 isUrl

/**

*

* @desc   判断是否为URL地址

* @param  {String} str

* @return {Boolean}

*/

functionisUrl(str) {

return/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/i.test(str);

}

10.String

10.1 digitUppercase

/**

*

* @desc   现金额转大写

* @param  {Number} n

* @return {String}

*/

functiondigitUppercase(n) {

varfraction = ['角','分'];

vardigit = [

'零','壹','贰','叁','肆',

'伍','陆','柒','捌','玖'

];

varunit = [

['元','万','亿'],

['','拾','佰','仟']

];

varhead = n <0?'欠':'';

n =Math.abs(n);

vars ='';

for(vari =0; i < fraction.length; i++) {

s += (digit[Math.floor(n *10*Math.pow(10, i)) %10] + fraction[i]).replace(/零./,'');

}

s = s ||'整';

n =Math.floor(n);

for(vari =0; i < unit[0].length && n >0; i++) {

varp ='';

for(varj =0; j < unit[1].length && n >0; j++) {

p = digit[n %10] + unit[1][j] + p;

n =Math.floor(n /10);

}

s = p.replace(/(零.)*零$/,'').replace(/^$/,'零') + unit[0][i] + s;

}

returnhead + s.replace(/(零.)*零元/,'元')

.replace(/(零.)+/g,'零')

.replace(/^整$/,'零元整');

};

11.Support

11.1 isSupportWebP

/**

*

* @desc 判断浏览器是否支持webP格式图片

* @return {Boolean}

*/

functionisSupportWebP() {

return!![].map && document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') ==0;

}

12.Time

12.1 formatPassTime

/**

* @desc   格式化${startTime}距现在的已过时间

* @param  {Date} startTime

* @return {String}

*/

functionformatPassTime(startTime) {

varcurrentTime =Date.parse(newDate()),

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)returnyear +"年前"

if(month)returnmonth +"个月前"

if(day)returnday +"天前"

if(hour)returnhour +"小时前"

if(min)returnmin +"分钟前"

elsereturn'刚刚'

}

12.2 formatRemainTime

/**

*

* @desc   格式化现在距${endTime}的剩余时间

* @param  {Date} endTime

* @return {String}

*/

functionformatRemainTime(endTime) {

varstartDate =newDate();//开始时间

varendDate =newDate(endTime);//结束时间

vart = endDate.getTime() - startDate.getTime();//时间差

vard =0,

h =0,

m =0,

s =0;

if(t >=0) {

d =Math.floor(t /1000/3600/24);

h =Math.floor(t /1000/60/60%24);

m =Math.floor(t /1000/60%60);

s =Math.floor(t /1000%60);

}

returnd +"天 "+ h +"小时 "+ m +"分钟 "+ s +"秒";

}

13.Url

13.1 parseQueryString

/**

*

* @desc   url参数转对象

* @param  {String} url  default: window.location.href

* @return {Object}

*/

functionparseQueryString(url) {

url = url ==null? window.location.href : url

varsearch = url.substring(url.lastIndexOf('?') +1)

if(!search) {

return{}

}

returnJSON.parse('{"'+ decodeURIComponent(search).replace(/"/g,'\\"').replace(/&/g,'","').replace(/=/g,'":"') +'"}')

}

13.2 stringfyQueryString

/**

*

* @desc   对象序列化

* @param  {Object} obj

* @return {String}

*/

functionstringfyQueryString(obj) {

if(!obj)return'';

varpairs = [];

for(varkeyinobj) {

varvalue = obj[key];

if(valueinstanceofArray) {

for(vari =0; i < value.length; ++i) {

pairs.push(encodeURIComponent(key +'['+ i +']') +'='+ encodeURIComponent(value[i]));

}

continue;

}

pairs.push(encodeURIComponent(key) +'='+ encodeURIComponent(obj[key]));

}

returnpairs.join('&');

}

14.Function

14.1 throttle

/**

* @desc   函数节流。

* 适用于限制`resize`和`scroll`等函数的调用频率

*

* @param  {Number}    delay          0 或者更大的毫秒数。 对于事件回调,大约100或250毫秒(或更高)的延迟是最有用的。

* @param  {Boolean}   noTrailing     可选,默认为false。

*                                    如果noTrailing为true,当节流函数被调用,每过`delay`毫秒`callback`也将执行一次。

*                                    如果noTrailing为false或者未传入,`callback`将在最后一次调用节流函数后再执行一次.

*                                    (延迟`delay`毫秒之后,节流函数没有被调用,内部计数器会复位)

* @param  {Function}  callback       延迟毫秒后执行的函数。`this`上下文和所有参数都是按原样传递的,

*                                    执行去节流功能时,调用`callback`。

* @param  {Boolean}   debounceMode   如果`debounceMode`为true,`clear`在`delay`ms后执行。

*                                    如果debounceMode是false,`callback`在`delay` ms之后执行。

*

* @return {Function}  新的节流函数

*/

functionthrottle(delay, noTrailing, callback, debounceMode) {

// After wrapper has stopped being called, this timeout ensures that

// `callback` is executed at the proper times in `throttle` and `end`

// debounce modes.

vartimeoutID;

// Keep track of the last time `callback` was executed.

varlastExec =0;

// `noTrailing` defaults to falsy.

if(typeofnoTrailing !=='boolean') {

debounceMode = callback;

callback = noTrailing;

noTrailing =undefined;

}

// The `wrapper` function encapsulates all of the throttling / debouncing

// functionality and when executed will limit the rate at which `callback`

// is executed.

functionwrapper() {

varself=this;

varelapsed =Number(newDate()) - lastExec;

varargs = arguments;

// Execute `callback` and update the `lastExec` timestamp.

functionexec() {

lastExec =Number(newDate());

callback.apply(self, args);

}

// If `debounceMode` is true (at begin) this is used to clear the flag

// to allow future `callback` executions.

functionclear() {

timeoutID =undefined;

}

if(debounceMode && !timeoutID) {

// Since `wrapper` is being called for the first time and

// `debounceMode` is true (at begin), execute `callback`.

exec();

}

// Clear any existing timeout.

if(timeoutID) {

clearTimeout(timeoutID);

}

if(debounceMode ===undefined&& elapsed > delay) {

// In throttle mode, if `delay` time has been exceeded, execute

// `callback`.

exec();

}elseif(noTrailing !==true) {

// In trailing throttle mode, since `delay` time has not been

// exceeded, schedule `callback` to execute `delay` ms after most

// recent execution.

//

// If `debounceMode` is true (at begin), schedule `clear` to execute

// after `delay` ms.

//

// If `debounceMode` is false (at end), schedule `callback` to

// execute after `delay` ms.

timeoutID = setTimeout(debounceMode ? clear :exec, debounceMode ===undefined? delay - elapsed : delay);

}

}

// Return the wrapper function.

returnwrapper;

};

14.2 debounce

/**

* @desc 函数防抖

* 与throttle不同的是,debounce保证一个函数在多少毫秒内不再被触发,只会执行一次,

* 要么在第一次调用return的防抖函数时执行,要么在延迟指定毫秒后调用。

* @example 适用场景:如在线编辑的自动存储防抖。

* @param  {Number}   delay         0或者更大的毫秒数。 对于事件回调,大约100或250毫秒(或更高)的延迟是最有用的。

* @param  {Boolean}  atBegin       可选,默认为false。

*                                  如果`atBegin`为false或未传入,回调函数则在第一次调用return的防抖函数后延迟指定毫秒调用。

如果`atBegin`为true,回调函数则在第一次调用return的防抖函数时直接执行

* @param  {Function} callback      延迟毫秒后执行的函数。`this`上下文和所有参数都是按原样传递的,

*                                  执行去抖动功能时,,调用`callback`。

*

* @return {Function} 新的防抖函数。

*/

varthrottle =require('./throttle');

functiondebounce(delay, atBegin, callback) {

returncallback ===undefined? throttle(delay, atBegin,false) : throttle(delay, callback, atBegin !==false);

};

封装

除了对上面这些常用函数进行封装, 最重要的是支持合理化的引入,这里我们使用webpack统一打包成UMD通用模块规范,支持webpack、RequireJS、SeaJS等模块加载器,亦或直接通过标签引入。

但这样,还是不能让人满意。因为完整引入整个库,略显浪费,我们不可能用到所有的函数。那么,就支持按需引入

1.目录结构说明

│  .babelrc

│  .gitignore

│  .travis.yml

│  LICENSE

│package.json

│  README.md

│  setCookie.js// 拷贝到根路径的函数模块,方便按需加载

│  setScrollTop.js

│  stringfyQueryString.js

│   ...

│   ...

├─min

│      outils.min.js// 所有函数统一打包生成的全量压缩包

├─script// 本项目开发脚本目录

│      build.js// 打包构建脚本

│      test.js// 测试脚本

│      webpack.conf.js// webpack打包配置文件

├─src// 源码目录

│  │  index.js// webpack入口文件

│  │

│  ├─array

│  │

│  ├─class

│  │

│  ├─cookie

│  │

│  ├─device

│  │

│  ├─dom

│  │

│  ├─keycode

│  │

│  ├─object

│  │

│  ├─random

│  │

│  ├─regexp

│  │

│  ├─string

│  │

│  ├─support

│  │

│  ├─time

│  │

│  └─url

└─test// 测试用例目录

│  array.test.js

│class.test.js

│  cookie.test.js

│  device.test.js

│  dom.test.js

│  index.html

│  keycode.test.js

│object.test.js

│  random.test.js

│  regexp.test.js

│string.test.js

│  support.test.js

│  time.test.js

│  url.test.js

└─_lib// 测试所用到的第三方库

mocha.css

mocha.js

power-assert.js

2.构建脚本

这里主要说明一下项目中 build.js 的构建过程 第一步,构建全量压缩包,先删除min目录中之前的outils.min.js,后通过webpack打包并保存新的压缩包至min目录中:

......

......

// 删除旧的全量压缩包

rm(path.resolve(rootPath,'min',`${pkg.name}.min.js`), err => {

if(err)throw(err)

webpack(config,function(err, stats) {

if(err)throw(err)

building.stop()

process.stdout.write(stats.toString({

colors:true,

modules:false,

children:false,

chunks:false,

chunkModules:false

}) +'\n\n')

resolve()

console.log(chalk.cyan('  Build complete.\n'))

})

})

......

......

第二步,拷贝函数模块至根目录,先删除根目录中之前的函数模块,后拷贝src下面一层目录的所有js文件至根目录。这么做的目的是,拷贝到根路径,在引入的时候,直接require('outils/<方法名>')即可,缩短引入的路径,也算是提高点效率。

// 替换模块文件

......

......

// 先删除根目录中之前的函数模块

rm('*.js', err => {

if(err)throw(err)

letfolderList = fs.readdirSync(path.resolve(rootPath,'src'))

folderList.forEach((item, index) => {

// 拷贝`src`下面一层目录的所有`js`文件至根目录

copy(`src/${item}/*.js`, rootPath,function(err, files) {

if(err)throwerr;

if(index === folderList.length -1) {

console.log(chalk.cyan('  Copy complete.\n'))

copying.stop()

}

})

})

})

......

......

3.书写测试用例

俗话说,不写测试用例的前端不是一个好程序员。那就不能怂,就是干。

但是因为时间关系,本项目暂时通过项目中的 test.js ,启动了一个koa静态服务器,来加载mocha网页端的测试页面,让笔者书写项目时,可以在本地对函数功能进行测试。 但是后续将使用travis-ci配合Github来做持续化构建,自动发布到npm。改用karma,mocha,power-assert做单元测试,使用Coverage测试覆盖率。这一部分,后续更新。

这里给大家推荐一个好用的断言库 power-assert ,这个库记住assert(value,[message])一个API就基本无敌,从此再也不用担心记不住断言库的API。

本项目的所有测试用例都在test目录下,大家可以作一定参考。

发布

首先放到Github托管一下,当然你也可以直接fork本项目,然后再加入你自己的函数。 以笔者项目,举个栗子:

1.添加自己的函数

在src目录下,新建分类目录或者选择一个分类,在子文件夹中添加函数模块文件(建议一个小功能保存为一个JS文件)。

/**

*

* @desc   判断是否NaN

* @param  {Any} value

* @return {Boolean}

*/

functionisNaN(value) {

returnvalue !== value;

};

modules.export= isNaN

然后记得在src/index.js文件中暴露isNaN函数

2.单元测试

在test文件新建测试用例

describe('#isNaN()',function() {

it(`outils.isNaN(NaN) should return true`,function() {

assert(outils.isNaN(NaN))

})

it(`outils.isNaN('value') should return false`,function() {

assert.notEqual(outils.isNaN(NaN))

})

})

然后记得在test/index.html中引入之前创建的测试用例脚本。

3.测试并打包

执行npm run test,看所有的测试用例是否通过。如果没有问题,执行npm run build构建,之后提交到个人的 github 仓库即可。

4.发布到npm

在 www.npmjs.com 注册账号,修改本地package.json中的name、version、author等信息,最后npm publish就大功告成了。

注意:向

npm发包,要把镜像源切到 www.npmjs.com ,使用cnpm等第三方镜像源会报错。

使用

1.浏览器

直接下载min目录下的 outils.min.js ,通过标签引入。

varOS = outils.getOS()

注意: 本仓库代码会持续更新,如果你需要不同版本的增量压缩包或源码,请到 github Release 页面下载对应版本号的代码。

2.Webpack、RequireJS、SeaJS等模块加载器

先使用npm安装outils。

$ npm install --save-dev outils

// 完整引入

constoutils =require('outils')

constOS = outils.getOS()

推荐使用方法

// 按需引入require('outils/<方法名>')

constgetOS =require('outils/getOS')

constOS = getOS()

当然,你的开发环境有babel编译ES6语法的话,也可以这样使用:

importgetOSfrom'outils/getOS'

// 或

import{ getOS }from"outils";

总结

这里只是简单封装,发布到npm上,省去下次复制粘贴的功夫,或者直接Goole的时间。如果笔者的库中,没有你常用的函数,或者你有更好的建议,欢迎来本项目的 Github Issues 交流,如果觉得不错,欢迎 star本项目。

当然,更好的建议是 fork 本项目,或者直接新建自己的项目,添加自己想要的常用的记不住的函数,甚至是可以抽象出来的功能,封装成自己顺手、熟悉的库。 这样才能打造出你自己的武器库,瞬间提高你的单兵作战(开发)能力。

工欲善其事必先利其器。有了属于自己的这把利器,希望加班也会变成奢望。O(∩_∩)O哈哈~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容