「✍ 发布自己的NPM工具库」

656 阅读3分钟

背景

最近在写公司业务时,发现在各个项目中都有一些可复用的模块,比如校验、环境判断等等,每次开一个新项目都要复制一份代码,如果某些方法需要更新就更麻烦了,把这些模块发布到NPM上,就可以集中管理了

搭建项目

选型

选型定为webpack + ts, 当然你可以选择其他的打包工具,如glupGruntRollup等等

目录结构

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"
  }
}

注:要区分开devDependenciesdependencies,当别人安装你的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()],
  },
]

可以参考 juejin.cn/post/684490…

持续集成

这篇讲的很全了:www.cnblogs.com/gaobw/p/115…

参考:github.com/liujiapeng/…


👉 github.com/liujiapeng/…