《前端那些事》从0到1开发工具库

简介: 在日常开发中,特别是中后台管理页面,会经常使用到一些常用的函数比如:防抖节流、本地存储相关、时间格式化等,但是随着项目不断增加,复用性和通用性就成为一个很至关重要的问题,如何减少复制张贴的操作,那就是封装成为,适用与多项目统一的工具包,并用npm进行管理,“U盘式安装”的方式可以提高团队的效率,那今天就讲讲开发一个简易的工具库需要涉及哪些环节

微信截图_20220512122706.png


在日常开发中,特别是中后台管理页面,会经常使用到一些常用的函数比如:防抖节流、本地存储相关、时间格式化等,但是随着项目不断增加,复用性和通用性就成为一个很至关重要的问题,如何减少复制张贴的操作,那就是封装成为,适用与多项目统一的工具包,并用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 其他打包工具



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地址github.com/littleTreem…,如果感到对你有帮助,给个star ✨,非常感谢



相关文章
|
5月前
|
存储 前端开发 测试技术
前端代码托管:存储库管理综合指南
前端代码托管:存储库管理综合指南
94 1
|
5月前
|
前端开发 程序员 API
前端反卷计划-组件库-05-Menu组件开发
前端反卷计划-组件库-05-Menu组件开发
|
5月前
|
前端开发 JavaScript 测试技术
前端反卷计划-组件库-04-Button组件开发
前端反卷计划-组件库-04-Button组件开发
|
5月前
|
前端开发 程序员
前端反卷计划-组件库-06-Icon组件开发
前端,组件库开发,Icon组件,前端反卷计划
|
6月前
|
设计模式 JavaScript 前端开发
|
7月前
|
前端开发 程序员 开发工具
前端反卷计划-组件库-01-环境搭建
前端组件库系列:环境搭建
前端反卷计划-组件库-01-环境搭建
|
8月前
|
前端开发 JavaScript API
vue3-ts-storybook:理解storybook、实践 / 前端组件库
vue3-ts-storybook:理解storybook、实践 / 前端组件库
149 0
|
2天前
|
移动开发 前端开发 JavaScript
前端高效开发JavaScript库!
前端高效开发JavaScript库!
|
4天前
|
JSON 前端开发 JavaScript
【2024-04-22 源码】最新PDF批注注释插件库,pdf.js插件库,纯前端离线JavaScript库(PDF高亮、下划线、橡皮擦、文本框、画笔、历史记录)
一款基于 pdf.js 开发的PDF批注插件库,支持纯离线内网部署,功能完善、强大且在不断升级,极易上手,欢迎关注!
37 4
【2024-04-22 源码】最新PDF批注注释插件库,pdf.js插件库,纯前端离线JavaScript库(PDF高亮、下划线、橡皮擦、文本框、画笔、历史记录)
|
4天前
|
前端开发 JavaScript API
前端框架与库
前端框架与库
44 2