10

In daily development, especially in the middle and backstage management pages, some commonly used functions such as anti-shake and throttling, local storage related, time formatting, etc. are often used, but as the number of projects continues to increase, reusability and versatility become It has become a very important issue. How to reduce the operation of copying and posting, that is, packaging becomes a toolkit that is unified with multiple projects and is managed by npm. The "U disk installation" method can improve the efficiency of the team. Today, I will talk about the links involved in developing a simple tool library, see the picture below👇

1. Project structure

What configuration is needed to develop a tool library? Below is a case of a simple version of the tool library (kdutil) I wrote👇

The ones involved are:

  • build: used to store packaging configuration files
  • dist: used to store files generated after compilation
  • src: Store the source code (including the entry of each module and the definition of constants)
  • test: store test cases
  • babel.config.js: Configure to convert ES2015 version code into compatible JavaScript syntax
  • package.json: defines the configuration and dependency information of the package
  • README.md: Introduces the use and functions of the entire toolkit

2. Packing method

Why do I need to pack? The tool library involves multi-modular development, and the maintainability of a single module needs to be preserved. Secondly, in order to solve the problem that some low-version browsers do not support es6 syntax, they need to be converted to es5 syntax for browser use. This project uses webpack as the front-end packaging tool

2.1 webpack configuration file

// 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()  
    # 启用作用域提升,作用是让代码文件更小、运行的更快
  ]
};

Configuration analysis:

  • entry: the definition of the packaged entry file
  • plugins: It is processed through the introduction of plug-ins, used to convert a certain type of module, and can be processed: packaging, compression, redefining variables, etc.
  • Loader-handles languages that the browser cannot run directly, and can convert all types of files into effective modules that webpack can handle (as shown in the figure above, babel-loader is used to convert the browser because it is not compatible with es6 conversion.
    Common loaders include TypeScript, Sass, Less, Stylus, etc.)
  • output: input file configuration, path refers to the output path, file refers to the name of the final output file, the most important thing is libraryTarget and library, please see the next chapter

2.1 webpack about the libraryTarget and library attributes in the development class library

Because in general SPA projects, using webpack does not need to pay attention to these two attributes, but if you are developing a class library, then these two attributes must be understood.

There are several common forms of libraryTarget👇:

  • libraryTarget: "var" (default): library will export the value as a variable declaration (when using the script tag, it will be available in the global scope after execution)
  • libraryTarget: “window”: When the library is loaded, the return value will be assigned to the window object.
  • libraryTarget: “commonjs”: When the library is loaded, the return value will be assigned to the exports object. This name also means that the module is used in the CommonJS environment (node environment)
  • libraryTarget: "umd": This is a way that your library can be run under all module definitions. It will run under CommonJS, AMD environment (currently the tool library uses 🚀)

The library specifies the module name when you require or import

2.3 Other packaging tools

3. Modular development

The tool library contains multiple functional modules, such as localstorage, date, http, etc., which need to be managed separately, and finally use webpack to parse require.context(), and create your own context through the require.context() function. Export all modules, below are all modules included in the kdutil tool library👇

3.1 localstorage local storage module

localStorage is a new feature of Html5. It is used as a local storage to solve the problem of insufficient cookie storage space. The general browser in localStorage supports 5M size.
/*
  @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 time formatting module

It is often necessary to format the time in daily development, such as setting the time to 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 commonly used function management module

The tools module contains some commonly used tool functions, including anti-shake throttling function, deep copy, regular type judgment, etc., and more general tool functions will be added later to slowly integrate the lodash that the project originally relied on. Oriented, high-performance JavaScript utility library) remove
/*
  @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 module

The http module is essentially based on the secondary encapsulation done by axios, adding an interceptor, and processing all http requests and responses uniformly through the interceptor. Configure the http request inteceptor, uniformly configure the request headers, such as token, and then configure the http response inteceptor, when the interface returns the status code 401 Unauthorized (unauthorized), the user is returned to the login page.
/*
  @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 monitoring module

Sentry is an open source front-end exception monitoring and reporting tool. Through integration into the project, you can help you collect and record problems in different environments (testing, production, etc.), and locate the problem code. Kutil also does sentry in the project. stand by
/*
 * @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() automatically import source files

When all the modules are developed, we need to export each module. Here, require.context is used to traverse the specified files in the folder, and then automatically import, instead of importing each module separately
// 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;

Regarding the use of require.context, require.context() allows you to pass in a directory for search, a flag indicating whether subdirectories should also be searched, and a regular expression to match files. When you build the project, webpack will handle require .context content

The three parameters that require.context() can pass in are:

  • directory: the path to read the file
  • useSubdirectories: Whether to traverse the subdirectories of the file
  • regExp: the regularity of the matching file

4. Unit testing

After completing the modular development of the tool library, in order to ensure the quality of the code and verify the functional integrity of each module, we need to test each module to ensure the normal use of the function before releasing it.

I use jest as a unit testing framework for tool library development. Jest is a JS unit testing framework open sourced by Facebook. In addition to basic assertion and mock functions, Jest also has practical functions such as snapshot testing and coverage reports.
, For more unit testing learning, go to "Those Things about Front-end Unit Testing" Portal 🚪

Below is my date module as a case, how to test the module

4.1 jest configuration file

// 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 Test case

// 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');
  })
});

Execute npm run test

5. Script commands

After completing the above series of development, the next step is how to package all the modules into a tool library. At this time, it is the turn of the "script commands"
This protagonist is on the stage

By defining the script command in packjson as follows👇

{
  "scripts": {
    "build_rollup": "rollup -c",
    "build": "webpack --config ./build/webpack.pro.config.js"
    "test": "jest --config src/test/unit/jest.conf.js",
  },
  ...
}

After configuration, execute npm run build


After the execution is complete, the generated kdutil.min.js will appear in the dist directory, which is also the "entry file" that the tool library will finally upload to npm

6.npm release

Complete the above script command settings, now the last step is to "send the package", use npm for package management

6.1 Configure your package related information through 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 Write development document readme.me

6.3 Release

First, you need to log in to your npm account first, and then execute the release command

npm login # 登录你上面注册的npm账号

npm publish # 登录成功后,执行发布命令

+ kdutil@0.0.2 # 发布成功显示npm报名及包的版本号

7. End

Through the above, we have completed a simple version of the tool library kdutil from 0 to 1. This is the github address https://github.com/littleTreeme/kdutil🚀 , if you feel helpful, give me star ✨, thank you very much

树酱
457 声望953 粉丝

95年程序猿,搞前端,爱音乐,唱跳rap工程🌲