從封裝一個[日期處理]工具到發佈爲 npm 公共包的全過程

背景:項目比較多,包含一些公共的代碼,如 utils 輔助工具,爲避免複製粘貼、版本同步,要將其抽離出來單獨做爲一個模塊來維護 爲了方便咱們開發,代碼老是分分合合,比人純粹些,分久必合,合久必分。javascript

在這裏呢,我將講解,一步一步的從封裝一個工具(日期處理),到發佈到 npm 倉庫(公共包免費,私有包收費),帶你瞭解整個過程,少踩坑。css

準備

環境

平臺:Mac oshtml

node:v10.15.3java

git:v2.22.1node

代碼管理:GitHubgit

編輯器:VS Codees6

EditorConfig for VS Code:跨編輯器統一項目文件/文本格式github

ESLint:eslint 規範插件npm

Prettier - Code formatter:代碼美化json

項目依賴

  • @babel/cli
  • @babel/core
  • @babel/preset-env
  • @babel/register
  • mocha
  • eslint
  • eslint-config-prettier
  • eslint-plugin-prettier
  • prettier
  • husky
  • lint-staged

@babelxxx 將 es6+語法編譯成 es5,eslintxxxprettier 代碼書寫規範及美化,huskylint-staged 提交鉤子,在提交代碼到倉庫以前作點事情

項目骨架

.
├── .github               --- github 相關代碼
├── es                    --- es6+ 源碼
├── lib                   --- es5 源碼(由babel編譯而來)
├── test                  --- 測試代碼
├── .babelrc              --- babel 配置
├── .editorconfig         --- editor 配置
├── .eslintignore         --- 忽略eslint檢查配置
├── .eslintrc             --- eslint 配置
├── .gitignore            --- 忽略git提交配置
├── .prettierignore       --- 忽略代碼美化配置
├── .prettierrc           --- 代碼美化配置
├── LICENSE               --- 許可證
├── README.md             --- 項目介紹(推薦用[readme-md-generator](https://github.com/kefranabg/readme-md-generator)生成)
├── package-lock.json     --- pkg lock文件
└── package.json          --- pkg 文件
複製代碼

如今開始一步步建立出上面的目錄結構(通用模版

  • 在 github 上建立一個organization(筆者起了個名 jsany 😎)

  • 在上面建立的組織裏建立(new)一個 public 倉庫(date)

  • npm上也建立一個organization(筆者起了個名 jsany 😎)這樣能夠避免 publish 時包名重複的問題

  • 在本地登錄 npm npm login

    npm_login

  • 在本地工做目錄建立一個文件夾 date 並進入 mkdir date && cd date

  • 初始化 git git init

  • 初始化 npm 工程 npm init --scope=jsany

    npm_init

    {
      "name": "@jsany/date",
      "version": "1.0.0",
      "description": "javascript date small",
      "main": "lib/index.js",
      "scripts": {
        "test": "mocha --require @babel/register"
      },
      "repository": {
        "type": "git",
        "url": "git+https://github.com/jsany/date.git"
      },
      "keywords": [
        "data",
        "js",
        "format",
        "transform"
      ],
      "author": "jiangzhiguo2010",  // 注意這裏要和npm帳號的用戶名一致
      "license": "MIT", // 開源許可證
      "bugs": {
        "url": "https://github.com/jsany/date/issues"
      },
      "homepage": "https://github.com/jsany/date#readme"
    }
    複製代碼
  • 用 vscode 打開項目(方便操做),在終端執行 code . (這個須要另外配置,想學的能夠私聊我)

  • 新建目錄 .github,用來存放 github 相關的東西,這裏我用來放一個提交規則校驗的腳本,詳情請看

  • 新建目錄 es,用來存放 es6+語法的源碼

  • 新建目錄 lib,用來存放 es5 語法並支持 commonjs 的源碼,不須要編寫,由 babel 編譯生成

  • 新建目錄 test,用來存放測試代碼

  • 安裝依賴 npm i --save-dev @babel/cli @babel/core @babel/preset-env @babel/register mocha eslint eslint-config-prettier eslint-plugin-prettier husky lint-staged prettier

  • 新建文件 .babelrcbabel 配置

    {
      "presets": [
        [
          "@babel/preset-env",
          {
            "modules": "auto", // 若想經過script標籤引入,這裏可使用 umd
            "loose": true,
            "targets": {
              "esmodules": true,
              "node": true
            }
          }
        ]
      ]
    }
    複製代碼
  • 新建文件 .editorconfig(安裝 EditorConfig for VS Code 插件後,也可經過 ⌘+⇧+p 而後輸入 Generate .editorconfig 生成),editorconfig 配置,跨編輯器統一項目文件/文本格式

    root = true
    
    [*]
    indent_style = space
    indent_size = 2
    end_of_line = lf
    charset = utf-8
    trim_trailing_whitespace = false
    insert_final_newline = false
    複製代碼
  • 新建文件 .eslintignoreeslint 配置,忽略檢查

    node_modules/
    複製代碼
  • 新建文件 .eslintrceslint 配置

    {
      "env": {
        "browser": true,
        "es6": true,
        "node": true
      },
      "plugins": ["prettier"],
      "extends": ["eslint:recommended", "plugin:prettier/recommended"],
      "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly"
      },
      "parserOptions": {
        "ecmaVersion": 2018,
        "sourceType": "module"
      },
      "rules": {
        "no-console": "off",
        "prettier/prettier": "error"
      }
    }
    複製代碼
  • 新建文件 .gitignore忽略 git 提交配置

    # dependencies
    /node_modules
    /npm-debug.log*
    /yarn-error.log
    /yarn.lock
    /package-lock.json
    
    .DS_Store
    .idea/
    .vscode
    
    複製代碼
  • 新建文件 .prettierignore忽略代碼美化配置

    **/*.svg
    **/*.ejs
    **/*.html
    複製代碼
  • 新建文件 .prettierrc代碼美化配置

    {
      "singleQuote": true,
      "trailingComma": "es5",
      "printWidth": 100
    }
    複製代碼

通過上面的步驟,package.json大致上是這樣子:

{
  "name": "@jsany/date",
  "version": "1.0.5", // 包版本,每發佈一次,需更新
  "description": "javascript date small es5 es6+",
  "main": "lib/index.js", // commonjs 入口文件,使用 require 語法引入
  "module": "es/index.js", // esmodules 入口文件,使用 import/require 語法引入,支持tree shaking 優化
  "files": ["lib", "es"], // 這個files用來指定須要發佈的文件(將無用的文件剔除掉,減小體積,下載快,也能夠在`.npmignore`文件中指定須要剔除的文件)
  "scripts": {
    "test": "mocha --require @babel/register", // 測試命令
    "compile": "babel es --out-dir lib" // babel編譯命令
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/jsany/date.git"
  },
  "keywords": ["data", "js", "format", "transform"],
  "author": "jiangzhiguo2010",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/jsany/date/issues"
  },
  "homepage": "https://github.com/jsany/date",
  "devDependencies": {
    "@babel/cli": "^7.5.5",
    "@babel/core": "^7.5.5",
    "@babel/preset-env": "^7.5.5",
    "@babel/register": "^7.5.5",
    "mocha": "^6.2.0",
    "eslint": "^5.16.0",
    "eslint-config-prettier": "^4.3.0",
    "eslint-plugin-prettier": "^3.1.0",
    "husky": "^2.4.1",
    "lint-staged": "^8.2.0",
    "prettier": "^1.18.2"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "commit-msg": "node .github/verifyCommitMsg"
    }
  },
  "lint-staged": {
    "*.{js,css,json,md}": ["prettier --write", "git add"]
  },
  "directories": {
    "test": "test"
  },
  "dependencies": {}
}
複製代碼

開始編寫 date 源碼

date 提供格式化,時區轉換,獲取時間戳等功能

格式化功能

思路:(日期,格式)=>符合預期格式的日期,格式能夠經過正則匹配來返回固定的格式,按照這個來實現一下

新建 dateFormat.js

/** * @description 格式化日期 * @param {(object|string)} date - 日期對象/字符串 * @param {string} mask - 日期格式,默認:mask='yyyy-MM-dd HH:mm:ss' * @returns {string} 返回格式化後的日期 */
const dateFormat = (date, mask = 'yyyy-MM-dd HH:mm:ss') => {
  const d = typeof date !== 'object' ? new Date(date) : date;
  if (!d.getTime()) {
    throw new MyError({ code: '000', msg: ErrorCode['000'] });
  }
  const zeroize = (value, length = 2) => {
    value = String(value);
    let zeros = '';
    for (let i = 0, len = length - value.length; i < len; i++) {
      zeros += '0';
    }
    return zeros + value;
  };
  return mask.replace(
    /"[^"]*"|'[^']*'|\b(?:d{1,4}|m{1,4}|yy(?:yy)?|([hHMstT])\1?|[lLZ])\b/gi,
    function($0) {
      switch ($0) {
        case 'd':
          return d.getDate();
        case 'dd':
          return zeroize(d.getDate());
        case 'ddd':
          return ['Sun', 'Mon', 'Tue', 'Wed', 'Thr', 'Fri', 'Sat'][d.getDay()];
        case 'dddd':
          return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][
            d.getDay()
          ];
        case 'M':
          return d.getMonth() + 1;
        case 'MM':
          return zeroize(d.getMonth() + 1);
        case 'MMM':
          return [
            'Jan',
            'Feb',
            'Mar',
            'Apr',
            'May',
            'Jun',
            'Jul',
            'Aug',
            'Sep',
            'Oct',
            'Nov',
            'Dec',
          ][d.getMonth()];
        case 'MMMM':
          return [
            'January',
            'February',
            'March',
            'April',
            'May',
            'June',
            'July',
            'August',
            'September',
            'October',
            'November',
            'December',
          ][d.getMonth()];
        case 'yy':
          return String(d.getFullYear()).substr(2);
        case 'yyyy':
          return d.getFullYear();
        case 'h':
          return d.getHours() % 12 || 12;
        case 'hh':
          return zeroize(d.getHours() % 12 || 12);
        case 'H':
          return d.getHours();
        case 'HH':
          return zeroize(d.getHours());
        case 'm':
          return d.getMinutes();
        case 'mm':
          return zeroize(d.getMinutes());
        case 's':
          return d.getSeconds();
        case 'ss':
          return zeroize(d.getSeconds());
        case 'l':
          return zeroize(d.getMilliseconds(), 3);
        case 'L':
          var m = d.getMilliseconds();
          if (m > 99) m = Math.round(m / 10);
          return zeroize(m);
        case 'tt':
          return d.getHours() < 12 ? 'am' : 'pm';
        case 'TT':
          return d.getHours() < 12 ? 'AM' : 'PM';
        case 'Z':
          return d.toUTCString().match(/[A-Z]+$/);
        // Return quoted strings with the surrounding quotes removed
        default:
          return $0.substr(1, $0.length - 2);
      }
    }
  );
};
export default dateFormat;
複製代碼

獲取 utc 時間戳

思路:(utc 日期)=> utc 時間戳,經過 getTime 獲得當前時區的時間戳,getTimezoneOffset 獲得當前時區偏移量,兩者差值即 utc 時間戳

新建 utcTimestamp.js

/** * @description 獲取utc時間戳 * @param {string} date - utc日期對象/字符串,默認:當前時間 * @returns {number} 返回utc時間戳 */
const UTCTimestamp = (date = new Date()) => {
  return new Date(date).getTime() - new Date().getTimezoneOffset() * 60 * 1000;
};
export default UTCTimestamp;
複製代碼

utc 時間轉任意時區時間

思路:(utc 日期,時區偏移量,格式)=>任意時區時間,經過 getTime 獲得時間戳,減去輸入的偏移量,即任意時區時間,按照這個來實現一下

新建 utc2target.js

/** * @description utc時間轉目標時區的時間,默認爲utc時間轉本地時間 * @param {object|string} date - utc時間,日期對象/字符串 * @param {number} timezone - 目標時區,默認:本地時區timezone=-480(中國時區+0800) * @param {*} mask - 日期格式,默認:mask='yyyy-MM-dd HH:mm:ss' * @returns {string} 返回目標時區的時間 */
const UTC2Target = (
  date,
  timezone = new Date().getTimezoneOffset(),
  mask = 'yyyy-MM-dd HH:mm:ss'
) => {
  const utcTimestamp = new Date(date).getTime();
  if (!utcTimestamp) {
    throw new MyError({ code: '000', msg: ErrorCode['000'] });
  }
  date = dateFormat(new Date(utcTimestamp - timezone * 60 * 1000), mask);
  return date;
};

export default UTC2Target;
複製代碼

任意時區時間轉 utc 時間

思路:(任意時區日期,時區偏移量,格式)=>utc 時間,經過 getTime 獲得當前時區時間戳,加上輸入的偏移量,即 utc 時間,按照這個來實現一下

新建 target2utc.js

/** * @description 目標時區的時間轉utc時間,默認爲本地時間轉utc時間 * @param {object|string} date - 目標時區時間,日期對象/字符串 * @param {number} timezone - 目標時區,默認:本地時區timezone=-480(中國時區+0800) * @param {*} mask - 日期格式,默認:mask='yyyy-MM-dd HH:mm:ss' * @returns {string} 返回目標時區的utc時間 */
const Target2UTC = (
  date,
  timezone = new Date().getTimezoneOffset(),
  mask = 'yyyy-MM-dd HH:mm:ss'
) => {
  let targetTimestamp = new Date(date).getTime();
  if (!targetTimestamp) {
    throw new MyError({ code: '000', msg: ErrorCode['000'] });
  }
  date = dateFormat(new Date(targetTimestamp + timezone * 60 * 1000), mask);
  return date;
};

export default Target2UTC;
複製代碼

補充一下上面用到的工具函數/模塊:

  • ./helper/errCode.js

    export default {
      '000': 'Invalid Date',
    };
    複製代碼
  • ./helper/index.js

    /** * @description 獲取數據的具體類型 * @param {any} o - 要判斷的數據 * @returns {string} - 返回該數據的具體類型 */
    export const getDataType = o => {
      // 映射數據類型
      const map2DataType = {
        '[object String]': 'String',
        '[object Number]': 'Number',
        '[object Undefined]': 'Undefined',
        '[object Boolean]': 'Boolean',
        '[object Array]': 'Array',
        '[object Function]': 'Function',
        '[object Object]': 'Object',
        '[object Symbol]': 'Symbol',
        '[object Set]': 'Set',
        '[object Map]': 'Map',
        '[object WeakSet]': 'WeakSet',
        '[object WeakMap]': 'WeakMap',
        '[object Null]': 'Null',
        '[object Promise]': 'Promise',
        '[object NodeList]': 'NodeList',
        '[object Date]': 'Date',
        '[object FormData]': 'FormData',
      };
      o = Object.prototype.toString.call(o);
      if (map2DataType[o]) {
        return map2DataType[o];
      } else {
        return o.replace(/^\[object\s(.*)\]$/, '$1');
      }
    };
    
    /** * @description 擴展Error */
    export class MyError extends Error {
      constructor(props) {
        super(props);
        this.code = props.code || 0;
        this.msg = props.msg || 'default msg';
        this.name = 'MyError';
        this.message = JSON.stringify(props);
      }
    }
    複製代碼

新建文件 index.js 將方法集中導出

export { default as dateFormat } from './dateFormat';
export { default as UTCTimestamp } from './utcTimestamp';
export { default as UTC2Target } from './utc2target';
export { default as Target2UTC } from './target2utc';
複製代碼

測試 date 功能(mocha

mocha 不支持 esmodules,所以要用 babel 進行編譯,在 cli 增長參數 --require @babel/register 便可

es6+ 測試(unit)

在 test 文件夾下新建文件 index.es.test.js

import { UTCTimestamp, UTC2Target, Target2UTC } from '../es/index';
const assert = require('assert');

const bj = '2019-01-01 08:00:00';
const ist = '2019-01-01 05:30:00';
const utc = '2019-01-01 00:00:00';
const utc_unix = 1546300800000;

describe('#@jsany/date(es)', () => {
  describe('#UTCTimestamp', () => {
    it('UTCTimestamp() should return true', () => {
      return assert.strictEqual(UTCTimestamp(utc, -480), utc_unix);
    });
  });
  describe('#UTC2Target', () => {
    it('UTC2Target() should return true', () => {
      return assert.strictEqual(UTC2Target(utc, -480), bj);
    });
    it('UTC2Target() should return true', () => {
      return assert.strictEqual(UTC2Target(utc, -330), ist);
    });
  });
  describe('#Target2UTC', () => {
    it('Target2UTC() should return true', () => {
      return assert.strictEqual(Target2UTC(bj, -480), utc);
    });
    it('Target2UTC() should return true', () => {
      return assert.strictEqual(Target2UTC(ist, -330), utc);
    });
  });
});
複製代碼

es5 測試(unit)

首先運行編譯命令 npm run compile

而後在 test 文件夾下新建文件 index.lib.test.js

const { UTCTimestamp, UTC2Target, Target2UTC } = require('../lib/index');
const assert = require('assert');

const bj = '2019-01-01 08:00:00';
const ist = '2019-01-01 05:30:00';
const utc = '2019-01-01 00:00:00';
const utc_unix = 1546300800000;

describe('#@jsany/date(lib)', () => {
  describe('#UTCTimestamp', () => {
    it('UTCTimestamp() should return true', () => {
      return assert.strictEqual(UTCTimestamp(utc, -480), utc_unix);
    });
  });
  describe('#UTC2Target', () => {
    it('UTC2Target() should return true', () => {
      return assert.strictEqual(UTC2Target(utc, -480), bj);
    });
    it('UTC2Target() should return true', () => {
      return assert.strictEqual(UTC2Target(utc, -330), ist);
    });
  });
  describe('#Target2UTC', () => {
    it('Target2UTC() should return true', () => {
      return assert.strictEqual(Target2UTC(bj, -480), utc);
    });
    it('Target2UTC() should return true', () => {
      return assert.strictEqual(Target2UTC(ist, -330), utc);
    });
  });
});
複製代碼

運行測試:npm run test

npm_test

本地npm包測試(npm link)

首先,新建一個文件夾,做爲測試npm包的新工程,能夠與npm包工程(date)同級目錄

cd .. && mkdir dateTest && cd dateTest
複製代碼

而後創建與date的npm軟鏈

npm link ../date
複製代碼

此時,在dateTest文件夾下就有了date的npm依賴,能夠查看下node_modules

npm link

如今就能夠新建js文件進行導入測試了

test

編寫 README

運行 npx readme-md-generator,建立 README.md 模版文件,而後補全

tips:這種

npm_version
小圖標能夠在 shields.io生成

提交代碼至 github

  • git remote add origin git@github.com:jsany/date.git
  • git push -u origin master

發佈

首先確認本身已登錄

npm_whoami

檢查 package.json 文件,記住每次更改發佈,都應該是一個新的版本,沒問題後開始發佈(公開包 --access=public), 因爲我們是在一個組織(organization)下發包,因此不用擔憂 包名會重複的問題了,無需用 npm view 作檢查了

npm publish --access=public

npm_publish

查看源碼

【參考】:

  1. www.npmjs.cn
  2. babeljs.io/docs/en
  3. eslint.org
  4. git-scm.com
  5. prettier.io
  6. mochajs.org
  7. shields.io

===🧐🧐 文中不足,歡迎指正 🤪🤪===

相關文章
相關標籤/搜索