Development tool library from 0 to 1

Time:2021-10-21

Development tool library from 0 to 1

In daily development, especially in the middle and background management pages, some commonly used functions are often used, such as anti shake throttling, local storage correlation, time formatting, etc. However, with the continuous increase of projects, reusability and versatility have become a very important problem. How to reduce the operation of copying and posting is encapsulation, It is applicable to the toolkit unified with multiple projects and managed by NPM. The “U-disk installation” method can improve the efficiency of the team. Today, let’s talk about the links involved in developing a simple tool library. See the figure below

Development tool library from 0 to 1

1. Project structure

What configurations are needed to develop a tool library? The following is a case of a simple tool library (kdutil) I wrote

Development tool library from 0 to 1

Involved are:

  • Build: used to store packaged 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 to compatible JavaScript syntax
  • Package.json: defines package configuration and dependency information
  • Readme.md: introduces the use and functions of the whole toolkit

2. Packing method

Why pack? The tool library involves multi modular development and needs to retain the maintainability of a single module. Secondly, in order to solve that some lower version browsers do not support ES6 syntax and need to be converted to Es5 syntax for use by browsers, the 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()  
    #Enable scope elevation to make code files smaller and run faster
  ]
};

Configuration resolution:

  • Entry: packaged entry file definition
  • Plugins: processed through plug-in introduction. It is used to convert certain types of modules. It can handle: packaging, compressing, redefining variables, etc
  • Loader – handles languages that the browser cannot run directly. It can convert all types of files into effective modules that the webpack can handle (as shown in the figure above). Babel loader is used to convert the browser because it is incompatible with ES6 writing
    Common loaders include typescript, sass, less, stylus, etc.)
  • Output: input the file configuration. Path refers to the output path, file refers to the file name of the final output, and the most important are 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, you don’t need to pay attention to these two attributes when using webpack, but if you are developing a class library, you must understand these two attributes.

There are several common forms of librarytarget:

  • Librarytarget: “var” (default): library will export the value as a variable declaration (when script tag is used, 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 to make your library run under all module definitions. It will run under the environment of commonjs and AMD (currently used by the tool library)

The library specifies the module name when you require or import

2.3 other packaging tools

  • Rollup: Portal

3. Modular development

The tool library contains multiple functional modules, such as localstorage, date, HTTP, etc. different functional modules need to be managed separately. Finally, use webpack to parse require. Context(), create your own context through the require. Context() function, and export all modules. The following are all modules contained in kdutil tool library

Development tool library from 0 to 1

Development tool library from 0 to 1

3.1 localstorage local storage module

Localstorage is a new feature of HTML5. It is used as local storage to solve the problem of insufficient cookie storage space. The general browser in localstorage supports 5m size

/*
  @File: localstorage local storage
  @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 format module

It is often necessary to format the time in daily development. For example, set the time to 2019-04-03 23:32:32

/*
 *@ file date format
 * @author:tree
 * @createBy:@2020.04.07
 */
module.exports =  {
  /**
   *Format the current elapsed time
   * @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 + "years ago";
    If (month) return month + "months ago";
    If (day) return day + "days ago";
    If (hour) return hour + "hours ago";
    If (min) return min + "minutes ago";
    Else return 'just now';
  },
  /**
   *Format timestamp
   *@ param time {number} timestamp
   *@ param FMT {string} format
   * @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(), // month
      "D +": date. Getdate(). Tostring(), // day
      "H +": date. Gethours(). Tostring(), // hours
      "M +": date. Getminutes(). Tostring(), // minutes
      "S +": date. Getseconds(). Tostring(), // seconds
    };
    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 common function management modules of tools

The tools module contains some common tool functions, including anti shake throttling function, deep copy, regular type judgment, etc. in the later stage, more general tool functions will be added to slowly remove lodash (a consistent, modular and high-performance JavaScript practical tool library) that the project originally relied on

/*
  @File: tool functions commonly used by tools
  @Author:tree
 */

module.exports =  {
  /**
   *Recursive deep copy
   *@ param data: copied 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 deep copy
   *@ param data: copied data
   *@ return data object the object generated after copying
   */
  deepCopy: function (data) {
    return JSON.parse(JSON.stringify(data));
  },

  /**
   *Returns regular by type
   *@ param str {string}: detected content
   *@ param type {string}: detection type
   */
  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), // phone number
      'number': / ^ [0-9] + $/. Test (STR), // are all numbers,
      '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': /[[email protected]:%._\+~#=]{2,256}\.[a-z]{2,6}\b([[email protected]:%_\+.~#?&//=]*)/i.test(str)
    };
    return regexp[type];
  },


  /**
   *Replace the middle part of the phone number with an asterisk
   *@ param phone {string}: mobile number
   */
  formatPhone: function (phone) {
    return phone.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2");
  },


  /**
   *Anti shake
   *@ param func {*} execute function
   *@ param wait {*} throttling time, MS
   */
  debounce: (func, wait) => {
    let timeout;
    return function () {
      let context = this;
      let args = arguments;

      if (timeout) clearTimeout(timeout);

      timeout = setTimeout(() => {
        func.apply(context, args)
      }, wait);
    }
  },

  /**
   *Throttling
   *@ param func {*} execute function
   *@ param wait {*} throttling time, MS
   */
  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;
      }
    }
  },

};

//Type detection
function getType(obj) {
  return Object.prototype.toString.call(obj).slice(8, -1);
}

3.4 HTTP module

The essence of HTTP module is a secondary encapsulation based on Axios. Interceptors are added to process all HTTP requests and responses. Configure the HTTP request interceptor, uniformly configure the request header, such as token, and then configure the HTTP response interceptor to return the user to the login page when the interface returns the status code 401 unauthorized.

/*
  @File: http request Library
  @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);
};

//Processing verification caused by network request
function checkStatus(response) {
  //If the HTTP status code is normal, the data will be returned directly
  if (response && (response.status === 200 || response.status === 304 || response.status === 400)) {
    return response.data || httpCode.NET_ERROR
  }
  return httpCode.NET_ERROR
}

//Verify the data returned by the server
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. By integrating it into the project, you can help you collect and record problems in different environments (testing, production, etc.) and locate the code where the problem lies. Kutil also supports sentry in the project

/*
 *@ file: sentry exception reporting log monitoring
 * @Author:tree,
 *Common configuration options: https://docs.sentry.io/clients/javascript/config/
 *1. Automatically capture exceptions in Vue components
 *2. Automatically catch exceptions in promise
 *3. Automatically catch running exceptions that are not caught
 */

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 has built-in Vue plug-in. It will catch errors in Vue components through vue.config.errorhandler and report them to sentry service

      //Record user information
      Raven.setUserContext({user: this.options.user || ''});

      //Set global tag label
      Raven.setTagsContext({environment: this.options.env || ''});
    }
  }

  /**
   *Active reporting
   * type: 'info','warning','error'
   */
  log(data = null, type = 'error', options = {}) {
    //Add bread crumbs
    Raven.captureBreadcrumb({
      message: data,
      category: 'manual message',
    });
    //Abnormal reporting
    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

After all modules are developed, we need to export each module. Here, we use require.context to traverse the specified files in the folder and then import them automatically, 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;

About the use of require. Context, require. Context () allows you to pass in a directory for search. A flag indicates whether you should also search subdirectories, and a regular expression to match files. When you build a project, webpack will handle the contents of require. Context

Require. Context() can pass in three parameters:

  • Directory: the path to read the file
  • Usesubdirectories: whether to traverse subdirectories of files
  • Regexp: the regexp that matches the file

4. Unit test

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 publishing

I developed and used jest as a unit testing framework in the tool library. Jest is an open-source JS unit testing framework for Facebook. In addition to the basic assertion and mock functions, jest also has practical functions such as snapshot testing and coverage reporting
To learn more about unit testing, go to the portal of front-end unit testing

Let me use the date module as a case 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: [// files matching test cases
    '<rootDir>/test/unit/specs/*.test.js',
  ],
  transformIgnorePatterns: ['/node_modules/'],
};

4.2 test cases

// date.test.js
const date = require('../../../src/modules/date');

Describe ('date module ', () = >{
  Test ('formattime() default format, return whether the time format is normal ', () = >{
    expect(date.formatTime(1586934316925)).toBe('2020-04-15 15:05:16');
  })
  Test ('formattime() pass parameter, return whether the time format is normal ', () = >{
    expect(date.formatTime(1586934316925,'yyyy.MM.dd')).toBe('2020.04.15');
  })
});

implement npm run test

Development tool library from 0 to 1

5. Script commands

After completing the above series of development, the next step is how to package all modules into a tool library. At this time, it’s the “script command”
The protagonist appeared

By defining the script in packjson, the command is 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, executenpm run build

Development tool library from 0 to 1
When the execution is completed, the generated kdutil.min.js will appear in the dist directory, which is also the “entry file” finally uploaded by the tool library to NPM“

6. NPM release

After setting the above script commands, the last step is to “contract” and use NPM for package management

6.1 configure your package related information through packjson

//package.json
{
  "name": "kdutil",
  "Version": "0.0.2", # package version number, which cannot be repeated for each release
  "Main": "dist / kdutil. Min.js", # packaged target files
  "author": "tree <[email protected]>",
  "keywords": [
    "utils",
    "tool",
    "kdutil"
  ],
  ... 
}

6.2 writing the development document readme.me

Development tool library from 0 to 1

6.3 release

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

NPM login # log in to the NPM account you registered above

After NPM publish # login succeeds, execute the publish command

+  [email protected] #Successfully published, display the version number of NPM registration and package

7. Conclusion

As mentioned above, we will complete a simple version of the tool library kdutil from 0 to 1, which is the GitHub address https://github.com/littleTreeme/kdutil , if you feel helpful, give a star ✨, Thank you.