从封装一个日期处理工具到发布为 npm 公共包的全过程
背景:项目比较多,包含一些公共的代码,如 utils 辅助工具,为避免复制粘贴、版本同步,要将其抽离出来单独作为一个模块来维护 为了方便我们开发,代码总是分分合合,比人纯粹些,分久必合,合久必分。
在这里呢,我将讲解,一步一步的从封装一个工具(日期处理),到发布到 npm 仓库(公共包免费,私有包收费),带你了解整个过程。
平台:Mac os
node:v10.15.3
git:v2.22.1
代码管理:GitHub
编辑器:VS Code
EditorConfig for VS Code:跨编辑器统一项目文件/文本格式
ESLint:eslint 规范插件
Prettier - Code formatter:代码美化
@babel/cli
@babel/core
@babel/preset-env
@babel/register
mocha
eslint
eslint-config-prettier
eslint-plugin-prettier
prettier
husky
lint-staged
@babelxxx
将 es6+语法编译成 es5,eslintxxx
、prettier
代码书写规范及美化,husky
、lint-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 时包名重复的问题
-
在本地工作目录创建一个文件夹 date 并进入
mkdir date && cd date
-
初始化 git
git init
-
初始化 npm 工程
npm init --scope=jsany
{ "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
-
新建文件
.babelrc
,babel 配置{ "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
-
新建文件
.eslintignore
,eslint 配置,忽略检查node_modules/
-
新建文件
.eslintrc
,eslint 配置{ "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 提供格式化,时区转换,获取时间戳等功能
思路:(日期,格式)=>符合预期格式的日期,格式可以通过正则匹配来返回固定的格式,按照这个来实现一下
新建 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 时间戳,通过 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 日期,时区偏移量,格式)=>任意时区时间,通过 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 时间,通过 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
即可
在 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);
});
});
});
首先运行编译命令 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 包测试(npm link)
首先,新建一个文件夹,作为测试 npm 包的新工程,可以与 npm 包工程(date
)同级目录
cd .. && mkdir dateTest && cd dateTest
然后建立与date
的 npm 软链
npm link ../date
此时,在 dateTest 文件夹下就有了 date 的 npm 依赖,可以查看下 node_modules
现在就可以新建 js 文件进行导入测试了
运行 npx readme-md-generator
,创建 README.md
模版文件,然后补全
tips:这种小图标可以在https://shields.io生成
git remote add origin [email protected]:jsany/date.git
git push -u origin master
首先确认自己已登陆
检查 package.json
文件,记住每次更改发布,都应该是一个新的版本,没问题后开始发布(公开包 --access=public),
由于咱们是在一个组织(organization)下发包,所以不用担心
包名会重复的问题了,无需用 npm view
做检查了
npm publish --access=public
【参考】:
- https://www.npmjs.cn
- https://babeljs.io/docs/en
- https://eslint.org
- https://git-scm.com
- https://prettier.io
- https://mochajs.org
- https://shields.io
===🧐🧐 文中不足,欢迎指正 🤪🤪===