背景
最近在写公司业务时,发现在各个项目中都有一些可复用的模块,比如校验、环境判断等等,每次开一个新项目都要复制一份代码,如果某些方法需要更新就更麻烦了,把这些模块发布到NPM上,就可以集中管理了
搭建项目
选型
选型定为webpack + ts, 当然你可以选择其他的打包工具,如glup、Grunt、Rollup等等
目录结构
util
├── src
│ ├── env
│ │ └── index.ts
│ └── valid
│ └── index.ts
├── tsconfig.json
├── webpack.config.js
├── package.json
Begin
安装依赖及配置命令
"scripts": {
"build": "rimraf lib && webpack"
},
"devDependencies": {
"typescript": "^4.2.2",
// ts-loader的替代品
"awesome-typescript-loader": "^5.2.1",
// webpack中配置出入口文件线管
"fs": "^0.0.1-security",
"glob": "^7.1.6",
"path": "^0.12.7",
// 打包前用于删除旧的打包文件
"rimraf": "^3.0.2",
// webpack
"webpack": "4",
"webpack-cli": "^4.5.0"
// 压缩打包文件
"uglifyjs-webpack-plugin": "^2.2.0",
}
编写工具函数
src/valid/index.ts
/**
* @desc 校验国内手机号是否合法
* @param {String} phoneNum 手机号
* @return {Boolean}
*/
const validatePhoneNum = (phoneNum: string): boolean => {
const reg = /^1[0-9]{10}$/;
return reg.test(phoneNum);
};
export default {
validatePhoneNum,
};
webpack配置
webpack.config.js
const { CheckerPlugin } = require("awesome-typescript-loader");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const path = require("path");
const glob = require("glob");
const fs = require("fs");
const ENTRY_REG = path.join(__dirname, "./src/*/**/index.ts");
let entries = {}; //多入口
glob.sync(ENTRY_REG).forEach((entry) => {
const catalogue = path.dirname(entry); // util/valid/index.ts => util/valid
const fileName = catalogue.split("/").pop(); // util/valid => valid
entries[fileName] = entry;
});
/*
entries:{
valid: 'src/valid.index.ts'
env: 'src/env/index.ts'
}
*/
glob.sync(ENTRY_REG).forEach((item) => console.log(item));
const WEBPACK_CONFIG = {
mode: "production",
entry: {
...entries, // 多入口
},
output: {
path: path.resolve(__dirname, "./lib"), // 打包目录
filename: "[name]/index.js",
publicPath: "/",
libraryTarget: "umd", // 兼容es和node
umdNamedDefine: true, // libraryTarget使用umd, 这里必须设置
globalObject: "this", // 兼容node中没有window
},
// Currently we need to add '.ts' to the resolve.extensions array.
resolve: {
extensions: [".ts", ".tsx", ".js", ".jsx"],
},
module: {
rules: [
{
test: /\.ts$/,
loader: "awesome-typescript-loader",
},
],
},
plugins: [new CheckerPlugin()],
// 压缩
optimization: {
minimizer: [
new UglifyJsPlugin({
uglifyOptions: {
compress: false,
mangle: true,
output: {
comments: false,
},
},
sourceMap: false,
}),
],
},
devtool: "hidden-source-map",
};
module.exports = WEBPACK_CONFIG;
在webpack配置中,主要关注的是output部分
tsconfig配置
tsconfig.json
{
"compilerOptions": {
"outDir": "lib", // 输出文件名
"module": "esnext",
"target": "es6",
"declaration": true, // 自动生成声明文件,默认在ts文件同目录生成index.d.ts
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"skipLibCheck": true
},
"include": ["src"],
"exclude": []
}
执行打包
yarn build
打包生成目录如下
util
├── lib
│ ├── env
│ │ ├── index.d.ts
│ │ └── index.js
│ └── valid
│ ├── index.d.ts
│ └── index.js
├── src
│ ├── env
│ │ └── index.ts
│ └── valid
│ └── index.ts
├── tsconfig.json
├── webpack.config.js
├── package.json
测试
import valid from "../lib/valid";
valid.validatePhoneNum("13999999999") // success
valid.validatePhoneNum(1) // error => validatePhoneNum: (phoneNum: string) => boolean
单元测试
现在,编写的库已经可以正常被引用,但在发布前,需要确保库里的组件/方法是准确无误的,我们将使用jest进行单元测试
引入依赖
yarn add jest @types/jest ts-jest -D
根目录加入配置文件jest.config.js
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
};
编写测试文件__test__/valid.test.ts
import valid from "../src/valid";
test("valid 12345", () => {
expect(valid.validatePhoneNum("12345")).toBe(false);
});
test("valid 123123123", () => {
expect(valid.validatePhoneNum("123123123")).toBe(false);
});
test("valid 13429999999", () => {
expect(valid.validatePhoneNum("13429999999")).toBe(true);
});
运行测试
yarn jest
发布
构建发布
package.json
{
"name": "util",
"version": "0.1.0",
"description": "v0.1.0",
"main": "./lib/main", // 也可以不指定主入口,让用户直接引入子目录的入口
"files": [
"lib"
],
"scripts": {
"build": "rimraf lib && webpack"
},
"author": "jensonliu",
"license": "ISC",
"devDependencies": {
"@types/jest": "^26.0.20",
"awesome-typescript-loader": "^5.2.1",
"fs": "^0.0.1-security",
"glob": "^7.1.6",
"jest": "^26.6.3",
"path": "^0.12.7",
"rimraf": "^3.0.2",
"ts-jest": "^26.5.2",
"typescript": "^4.2.2",
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "4",
"webpack-cli": "^4.5.0"
}
}
注:要区分开devDependencies和dependencies,当别人安装你的npm包,会将你dependencies中的包一并下载,如果不需要这样做,请将依赖包放到devDependencies中
首先执行构建
yarn build
然后进行发布
npm login
npm publish
打包导出类型
npm包不一定只在一种环境使用,可能在node, 浏览器都要生效,因此我们需要生成多个文件供开发者引入
开发者如果使用webpack开发,webpack会兼容转换各种模块模式,直接用esmodule也没有问题
但有时候我们想在浏览器直接使用esmodule, 这时候需要提供一个esm的文件:如index.esm.js
<script type="module">
import env from 'module/index.esm.js'
</script>
同时,我们需要告诉开发者的webpack/rollup如何在node_modules中寻找入口,在package.json中注明
"main": "lib/index.js",
"module": "lib/index.esm.js",
使用webpack在引入node_modules中的npm包时,会先在该包的package.json中找module对应的入口,没有的话再找main对应的入口
注: 打包类库时,webpack中output的libraryTarget没有找到es module的设置,可以用esm-webpack-plugin 插件实现, rollup中可以直接设置esm的format
rollup.config.js配置文件参考
import resolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
import typescript from 'rollup-plugin-typescript2'
import pkg from './package.json'
export default [
// UMD for browser-friendly build
{
input: 'src/index.ts',
output: {
name: 'pkg-umd',
file: pkg.main,
format: 'umd',
},
plugins: [resolve(), commonjs(), typescript()],
},
{
input: 'src/index.ts',
output: {
name: 'pkg-esm',
file: pkg.module, // for es module like <script type="module"> import EventBus from './lib/index.esm.js'</script>
// or webpack , cause webpack will find npm package in module field
format: 'esm',
},
plugins: [typescript()],
},
]
持续集成
这篇讲的很全了:www.cnblogs.com/gaobw/p/115…