從0到1開發工具庫

在平常開發中,特別是中後臺管理頁面,會常用到一些經常使用的函數好比:防抖節流、本地存儲相關、時間格式化等,可是隨着項目不斷增長,複用性和通用性就成爲一個很相當重要的問題,如何減小複製張貼的操做,那就是封裝成爲,適用與多項目統一的工具包,並用npm進行管理,「U盤式安裝」的方式能夠提升團隊的效率,那今天就講講開發一個簡易的工具庫須要涉及哪些環節,看下圖👇

1.項目結構

開發一個工具庫,到底須要哪些配置,下面是我寫的一個簡易版工具庫(kdutil)的案例👇

涉及到的有:javascript

  • 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 有主要幾種常見的形式👇:vue

  • libraryTarget: 「var」(default): library會將值做爲變量聲明導出(當使用 script 標籤時,其執行後將在全局做用域可用)
  • libraryTarget: 「window」 : 當 library 加載完成,返回值將分配給 window 對象。
  • libraryTarget: 「commonjs」 : 當 library 加載完成,返回值將分配給 exports 對象,這個名稱也意味着模塊用於 CommonJS 環境(node環境)
  • libraryTarget: 「umd」 :這是一種能夠將你的 library 可以在全部的模塊定義下均可運行的方式。它將在 CommonJS, AMD 環境下運行 (目前該工具庫使用🚀)

而library指定的是你require或者import時候的模塊名java

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 監控模塊node

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()可傳入三個參數分別是:webpack

  • directory :讀取文件的路徑
  • useSubdirectories :是否遍歷文件的子目錄
  • regExp: 匹配文件的正則

4.單元測試

完成工具庫模塊化開發以後,爲了保證代碼的質量,驗證各模塊功能完整性,咱們須要對各模塊進行測試後,確保功能正常使用,再進行發佈

我在工具庫開發使用jest做爲單元測試框架,Jest 是 Facebook 開源的一款 JS 單元測試框架,Jest 除了基本的斷言和 Mock 功能外,還有快照測試、覆蓋度報告等實用功能
,關於更多單元測試的學習前往《前端單元測試那些事》 傳送門🚪ios

下面我那date模塊來做爲一個案例,是如何對該模塊進行測試的git

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 es6

5.腳本命令

完成上面一系列開發後,接下來就是如何將全部模塊打包成工具庫了,這個時候就輪到「腳本命令」
這個主角登場了

經過在packjson中定義腳本命令以下👇github

{
  "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 ✨,很是感謝
相關文章
相關標籤/搜索