在日常开发中,特地是中后盾治理页面,会常常应用到一些罕用的函数比方:防抖节流、本地存储相干、工夫格式化等,然而随着我的项目一直减少,复用性和通用性就成为一个很至关重要的问题,如何缩小复制张贴的操作,那就是封装成为,实用与多我的项目对立的工具包,并用 npm 进行治理,“U 盘式装置”的形式能够进步团队的效率,那明天就讲讲开发一个繁难的工具库须要波及哪些环节,看下图👇
1. 我的项目构造
开发一个工具库,到底须要哪些配置,上面是我写的一个简易版工具库(kdutil)的案例👇
波及到的有:
- build:用来寄存打包配置文件
- dist:用来寄存编译完生成的文件
- src:寄存源代码(蕴含各个模块的入口及常量的定义)
- test:寄存测试用例
- babel.config.js : 配置将 ES2015 版本的代码转换为兼容的 JavaScript 语法
- package.json : 定义包的配置及依赖信息
- README.md:介绍了整个工具包的应用及蕴含的性能
2. 打包形式
为什么须要打包?工具库波及到多模块化开发,须要保留单个模块的可维护性,其次是为了解决局部低版本浏览器不反对 es6 语法,须要转换为 es5 语法,为浏览器应用,该我的项目采纳 webpack 作为前端打包工具
2.1 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()
# 启用作用域晋升,作用是让代码文件更小、运行的更快
]
};
配置解析:
- entry:打包的入口文件定义
- plugins:通过插件引入来解决,用于转换某种类型的模块,能够解决:打包、压缩、从新定义变量等
- loader – 解决浏览器不能间接运行的语言, 能够将所有类型的文件转换为 webpack 可能解决的无效模块 (如上图 babel-loader 用于转换浏览器因不兼容 es6 写法的转换
常见 loader 还有 TypeScript、Sass、Less、Stylus 等) - output:输出文件配置,path 指的是输入门路,file 是指最终输入的文件名称,最要害的是 libraryTarget 和 library,请看下一章
2.1 webpack 对于开发类库中 libraryTarget 和 library 属性
因为在个别 SPA 我的项目中,应用 webpack 无需关注这两个属性,然而如果是开发类库,那么这两个属性就是必须理解的。
libraryTarget 有次要几种常见的模式👇:
- libraryTarget:“var”(default): library 会将值作为变量申明导出(当应用 script 标签时,其执行后将在全局作用域可用)
- libraryTarget:“window”: 当 library 加载实现,返回值将调配给 window 对象。
- libraryTarget:“commonjs”: 当 library 加载实现,返回值将调配给 exports 对象,这个名称也意味着模块用于 CommonJS 环境(node 环境)
- libraryTarget:“umd”:这是一种能够将你的 library 可能在所有的模块定义下都可运行的形式。它将在 CommonJS, AMD 环境下运行 (目前该工具库应用🚀)
而 library 指定的是你 require 或者 import 时候的模块名
2.3 其余打包工具
- Rollup:传送门🚪
3. 模块化开发
该工具库蕴含多个功能模块,如 localstorage、date、http 等等,就须要将不同功能模块离开治理,最初应用 webpack 解析 require.context(),通过 require.context() 函数来创立本人的上下文,导出所有的模块,上面是 kdutil 工具库蕴含的所有模块👇
3.1 localstorage 本地存储模块
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);
}
};
3.2 date 工夫格式化模块
日常开发中常常须要格式化工夫,比方将工夫设置为 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;
}
};
3.3 tools 罕用的函数治理模块
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);
}
3.4 http 模块
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 监控模块
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;
3.6 require.context() 主动引入源文件
当所有模块开发实现之后,咱们须要将各模块导出,这里用到了 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()可传入三个参数别离是:
- directory:读取文件的门路
- useSubdirectories:是否遍历文件的子目录
- regExp:匹配文件的正则
4. 单元测试
实现工具库模块化开发之后,为了保障代码的品质,验证各模块性能完整性,咱们须要对各模块进行测试后,确保性能失常应用,再进行公布
我在工具库开发应用 jest 作为单元测试框架,Jest 是 Facebook 开源的一款 JS 单元测试框架,Jest 除了根本的断言和 Mock 性能外,还有快照测试、覆盖度报告等实用功能
,对于更多单元测试的学习返回《前端单元测试那些事》传送门🚪
上面我那 date 模块来作为一个案例,是如何对该模块进行测试的
4.1 jest 配置文件
// 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/'],
};
4.2 测试用例
// 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
5. 脚本命令
实现下面一系列开发后,接下来就是如何将所有模块打包成工具库了,这个时候就轮到“脚本命令”
这个配角退场了
通过在 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
执行实现,dist 目录将会呈现生成的 kdutil.min.js , 这也是工具库最终上传到 npm 的“入口文件“
6.npm 公布
实现上述脚本命令的设置,当初轮到最初的一步就是“发包”,应用 npm 来进行包治理
6.1 通过 packjson 配置你的包相干信息
//package.json
{
"name": "kdutil",
"version": "0.0.2", # 包的版本号,每次公布不能反复
"main": "dist/kdutil.min.js", # 打包完的指标文件
"author": "tree <shuxin_liu@kingdee.com>",
"keywords": [
"utils",
"tool",
"kdutil"
],
...
}
6.2 编写开发文档 readme.me
6.3 公布
首先须要先登录你的 npm 账号,而后执行发布命令
npm login # 登录你下面注册的 npm 账号
npm publish # 登录胜利后,执行发布命令
+ kdutil@0.0.2 # 公布胜利显示 npm 报名及包的版本号
7. 结尾
通过上文所述,咱们就从 0 到 1 实现来一个简易版的工具库 kdutil,这是 github 地址 https://github.com/littleTreeme/kdutil🚀,如果感到对你有帮忙,给个 star ✨,非常感谢