在日常开发中,特地是中后盾治理页面,会常常应用到一些罕用的函数比方:防抖节流、本地存储相干、工夫格式化等,然而随着我的项目一直减少,复用性和通用性就成为一个很至关重要的问题,如何缩小复制张贴的操作,那就是封装成为,实用与多我的项目对立的工具包,并用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.jsconst 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.jsconst 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.jsconst 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 ✨,非常感谢