搭建一个typescript函数库

1,879 阅读4分钟

为什么我们需要一个函数库:

假设我们在项目中,有一些比较通用的函数,比如:解析url, 复制对象, 我们用一个js文件保存这些通用的函数,用导出的方法来使用,他就是一个函数库了。

痛点是:如果多个项目使用这个函数库,我们就要考虑使用和维护优化的地方了,不然会导致添加或者修改函数库, 多个项目需要手动同步的问题

所以我们的函数库需要用到npm来做版本管理, 这样这个函数库就能更好的发挥他的能力


一个函数库应该具备

  • npm版本管理

  • 支持按需引入

  • 提供多种模块导出方式

  • 通过单元测试

  • 文档

    然后我们需要ts语法,加上了

  • 支持ts语法

    为了团队维护方便,加上了

  • 自动化构建模板


选择打包工具构建

在开始的时候我们可以选一个打包工具:rollupwebpack

使用rollup构建

rollup中文文档

  • 全局安装rollup
npm install rollup --global
  • 构建项目目录

    目录大概长这样:

    构建的步骤:

  1. 新建src/main.js, 这里只将index的模块全部暴露出去

     export * from './components/index'
    
  2. 新建package.json, 添加需要的依赖,包括rollup,typescript,还有rollup的两个插件/plugin-babel,plugin-typescript。

    {
      "name": "ts-util-rollup",
      "version": "1.0.0",
      "scripts": {
        "build": "rollup -c"
      },
      "main": "dist/index.js", // npm库入口
      "dependencies": {},
      "devDependencies": {
        "@rollup/plugin-babel": "^5.2.1",
        "@rollup/plugin-typescript": "^6.0.0",
        "rollup": "^2.32.0",
        "rollup-plugin-json": "^4.0.0",
        "tslib": "^2.0.3",
        "typescript": "^4.0.3"
      }
    }
    
    
  3. 新增rollup.config.js配置文件

      // rollup.config.js
      import json from 'rollup-plugin-json'
      import typescript from '@rollup/plugin-typescript'
      import pkg from './package.json'
      export default {
        input: 'src/main.ts',
        output: [
          {
            name: pkg.name,
            file: pkg.main,
            format: 'umd'
          }
        ],
        plugins: [
          json(),
          typescript({lib: ["es5", "es6", "dom"], target: "es5"})
        ]
      };
    
  4. 编辑函数, 这里以一个很简单的检验手机号的函数为例,在src/components里添加一个模块函数isMobile.ts,里面很简单就用正则判断了下手机号 ,我们这里直接用ts文件后缀

    export default function isMobile(v: any): boolean {
      return /^1[0-9][0-9]\d{8}$/.test(v)
    }
    

    然后引入到index.ts里再统一暴露出去

    import isMobile from './isMobile'
    export {
        isMobile
    }
    
  5. 运行打包命令 rollup -c 或者我们定义的npm run build, 在dist里面会出现打包好的文件。

    dist/index.js:

    (function (global, factory) {
        typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
        typeof define === 'function' && define.amd ? define(['exports'], factory) :
        (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global['ts-util-rollup'] = {}));
    }(this, (function (exports) { 'use strict';
    
        function isMobile(v) {
            return /^1[0-9][0-9]\d{8}$/.test(v);
        }
    
        exports.isMobile = isMobile;
    
        Object.defineProperty(exports, '__esModule', { value: true });
    
    })));
    
    
  6. 验证

    把打包好的dist/index.js文件放到在一个项目里, 导入isMobile函数验证

    import {isMobile} from './index.js'
    ...
    console.log('手机号是否正确', isMobile('13680988189'))
    


使用webpack构建

  • 安装webpack & webpack-cli
npm install --global webpack // 全局安装webpack
npm install webpack webpack-cli --save-dev // 此工具用于在命令行中运行 webpack
  • 构建项目目录
  1. src里的文件保持不变

  2. 新建packjson.js

    {
      "name": "ts-util-webpack",
      "version": "1.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "build": "webpack"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "ts-loader": "^8.0.11",
        "typescript": "^4.1.2",
        "webpack": "^5.8.0",
        "webpack-cli": "^4.2.0"
      }
    }
    
    
  3. 新建webpack.config.js

    const path = require('path');
    module.exports = {
        entry: './src/main.ts', // 入口
        output: {
            filename: 'index.umd.js',
            path: path.resolve(__dirname, 'dist'),
            library: 'tsUtil',
            libraryTarget: 'umd' // 导出模块为umd
        }
    }
    
  • 添加支持typescript

    安装tsloader

    npm install --save-dev typescript ts-loader
    

    新建ts.config.js文件

    {
      "compilerOptions": {
        "outDir": "./dist/",
        "noImplicitAny": true,
        "module": "es6",
        "target": "es5",
        "jsx": "react",
        "allowJs": true
      }
    }
    

    改造webapck.config.js添加ts支持

    const path = require('path');
    module.exports = {
        entry: './src/main.ts',
        output: {
            filename: 'index.umd.js',
            path: path.resolve(__dirname, 'dist'),
            library: 'tsUtil',
            libraryTarget: 'umd'
        },
        // 模块, 使用tsloader解析ts文件
        module: {
          rules: [
            {
              test: /\.tsx?$/,
              use: 'ts-loader',
              exclude: /node_modules/
            }
          ]
        },
        resolve: {
          extensions: [ '.tsx', '.ts', '.js' ]
        },
    }
    
    
  • 打包验证

    运行npx webapck即可打包, 我们可以看到打包出了我们想要的文件:dist/index.umd.js, 同样的验证方法

    import {isMobile} from './index.umd.js'
    ...
    console.log('手机号是否正确', isMobile('13680988189'))
    

两种打包工具的取舍

webpack的诞生始于构建复杂的单页应用程序(SPA)的难题。

  • 代码拆分解决了单页应用加载整个程序缓慢的问题, 将应用分解成可管理的块,按需加载,在我们需要快速响应的交互式网页中提升非常大。
  • 静态资源管理如图像和 CSS 可以导入到你的应用程序中,而且还能够被作为依赖图中的另一个节点

rollup则是用ES2015的模块设计,尽可能的高效构建能直接被其他js库应用的模块,这无疑是符合函数库的需求的,

  • 他把所有代码放到一个地方,然后一次执行,生成更简洁的代码,启动更快
  • 提供功能单一,相对webpack更轻便

在社区里也有很多用webpack打包库的,也有很多应用用rollup来打包,目前在我们公司生产环境里用的是rollup来打包函数库,webpack则用于应用打包


发布到npm

  • 运行npm init 初始化,会让你填内容,用默认的就好了

  • 运行npm login登录账号,还没有的话去npm官网注册一个

  • 运行npm publish发布 注意这里要用npm官方源,不能用淘宝源

    会输出一堆信息,我这里提示因为版本被拒绝了,因为我已经提交过1.0.0的版本,也就是说不能publish相同版本,改一下packjson.js里的version,重新发布就行了。

  • 在项目里安装验证:


提供更多模块规范

javascript内置是没有支持模块化,但是社区里给出了解决方案,就是模块规范, 对于你输出的dist文件,库的使用者可能和你用的不是同一个模块规范,如果要想库的使用者可以灵活的选择自己需要的模块方式,那就要对输出的格式做优化

配置几种主流的模块规范,使用者可以自己选择

output: [
   {
     name: pkg.name,
     file: pkg.main,
     format: 'umd'
   },
   {
     file: pkg.common,
     format: 'cjs'
   },
   {
     file: pkg.module,
     format: 'es'
   }
],

我们这里配置三种模块, 而且加上后缀不一样,这样我们就能在dist目录里看到三个js文件,里面对应三种模块规范打包的代码

  • commonjs模块

    • 使用require和exports(module.exports)引用和导出的交互方式
    • 同步加载
    • 一个文件就是一个模块

    配置后打包出来的代码index.common.js

     Object.defineProperty(exports, '__esModule', { value: true });
     
     function isMobile(v) {
         return /^1[0-9][0-9]\d{8}$/.test(v);
     }
    
     exports.isMobile = isMobile;
    

    以及他的调用规范

    // ts-util 是库名称
    const util = require('ts-util)
    // ...
    util.isMobile('123');
    

    commonJs属于node默认的模块规范,可以通过webpack加载


  • es模块

    • es6语言层面实现的模块机制, 未来模块标准
    • import和export (export default)的模块交互
    • 浏览器支持有限 配置后打包出来的代码 index.module.js
    function isMobile(v) {
      return /^1[0-9][0-9]\d{8}$/.test(v);
    }
    
    export { isMobile };
    
    

    以及他的调用规范

    // ts-util 是库名称
    import * as util from 'ts-util';
    // ...
    util.isMobile('123');
    

    es模块webpackrollup都可加载


  • umd模块
    • 兼容的通用模式
    • 判断是否支持再使用commonjsamd挂载到全局, 配置后打包出来的代码 index.umd.js
      (function (global, factory) {
        typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : // 是否支持commonjs
        typeof define === 'function' && define.amd ? define(['exports'], factory) : // amd
        // 挂载全局
        (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global['ts-util-rollup'] =  {}));
    }(this, (function (exports) { 'use strict';
    
        function isMobile(v) {
            return /^1[0-9][0-9]\d{8}$/.test(v);
        }
        exports.isMobile = isMobile;
    
        Object.defineProperty(exports, '__esModule', { value: true });
    
      })));
    

添加单元测试

函数库必定要经过单元测试,这才是比较严谨的,而且一定要覆盖全

推荐用测试库jest,很多开源项目都在用

  • 安装jestts-jest
npm i jest --save-dev
npm i ts-jest --save-dev
  • 新建一个jest.config.js
module.exports = {
  transform: {
    '^.+\\.tsx?$': 'ts-jest' // ts文件使用ts-jest
  }
}

  • 编写测试文件:isMobile.test.ts
import isMobile from './isMobile'

test('15919316514 正确返回true', () => {
    expect(isMobile('15919316514')).toBe(true)
})
test('159193 少于11位返回false', () => {
    expect(isMobile('159193')).toBe(false)
})
test('15919312312312312 超过11位返回false', () => {
    expect(isMobile('15919312312312312')).toBe(false)
})
test('15919312312312312 返回false', () => {
    expect(isMobile('15919312312312312')).toBe(false)
})

  • 运行jest

    可以看到我们通过了测试


自动化构建

如果我们现在要加一个新函数 我们会怎么做呢?

需要先去新建一个功能函数的ts文件,再新建一个jest的测试文件等等,如果文件层级比较深,随着函数库变丰富,层级深是必然的, 这个过程就会变得繁琐,还要再去文档里说明怎么新建,很麻烦的

我们可以用node文件操作自动化这一繁琐的过程 ,主要是利用node文件系统

  • 我们先调整下文件结构使其更合理, 每个函数都有一个文件夹包裹, 测试文件放在里面

  • 我们在build文件夹里新建一个add.js的node运行文件

    add.js:

    const fs = require('fs')
    const path = require('path')
    const readline = require('readline')
    
    // 终止
    const exit = function (log) {
      console.error(log)
      process.exit(1)
    }
    // 模块路径
    const MODULES_PATH = path.join(__dirname, '../src/components')
    
    // 模块名称
    const MODULES = process.argv[2]
    
    if (!MODULES) {
      exit('请填入模块名称')
    }
    
    // 输出路径
    const OUT_PATH = path.join(MODULES_PATH, MODULES) // 文件路径
    const TEST_OUT_PATH = path.join(OUT_PATH, '__tests__') // 测试用例文件路径
    
    const isExists = fs.existsSync(OUT_PATH)
    
    if (isExists) {
      exit(`${MODULES}目录已经存在`)
    }
    
    fs.mkdirSync(OUT_PATH)
    console.log('创建模块目录', OUT_PATH)
    
    fs.mkdirSync(TEST_OUT_PATH)
    console.log('创建测试目录', TEST_OUT_PATH)
    
    // 函数模版
    const addFileTemp =
      `
      export default function ${MODULES} (val:any) {
        return val
      }
      `
    // 测试用例模版
    const testTemp =
      `
        import ${MODULES} from '../index'
        describe('${MODULES}', () => {
          it('结果通过啦', () => {
            expect(${MODULES}('a')).toBe('a')
          })
        })
        `
    
    // 文件路径
    const FILE_PATH = path.join(OUT_PATH, `/index.ts`)
    const TEST_PATH = path.join(TEST_OUT_PATH, `${MODULES}.test.ts`)
    // 写入文件
    fs.writeFileSync(FILE_PATH, addFileTemp)
    fs.writeFileSync(TEST_PATH, testTemp)
    console.log('创建新模块成功', `路径: src/components/${MODULES}`)
    
    

    可以看出这个文件其实就是,通过我们输入的函数名称,在文件夹/src/components/下帮我们创建了同函数名称的文件夹,里面包含了函数js文件,测试文件,并且都写入了基本的模板

    函数模板:

    `
      export default function ${MODULES} (val:any) {
        return val
      }
      `
    

    测试文件模板:

    `
        import ${MODULES} from '../index'
        describe('${MODULES}', () => {
          it('结果通过啦', () => {
            expect(${MODULES}('a')).toBe('a')
          })
        })
        `
    
  • 然后我们在package.json里添加script命令, 当我们使用npm run add 的时候就是执行这个add.js文件

    "add": "node ./build/add.js"
    
  • 运行npm run add cloneDeep 新增一个深拷贝的函数

    控制台会输出

  • 我们再去components文件夹下面看 cloneDeep已经生成好了

    结构也符合我们的要求,我们就可以专注写函数的代码了,这对于多人协作来说非常方便


文档生成

如果是公司团队维护,那文档就很有必要,这里我懒得手写,就用了一个自动生成的apidoc

  • 在代码中写上注释

    /**
     * @api {function} is系列 isMobile
     * @apiName isMobile
     * @apiVersion 0.1.0
     * @apiDescription 手机号码是否正确
     * @apiGroup is系列
     * @apiParam {any} v 手机号
     * @apiSuccess (返回值) {boolean} val 结果
     *
     */
    export default function isMobile(v: any): boolean {
        return /^1[0-9][0-9]\d{8}$/.test(v)
    }
    
    
  • 新建命令

    "doc": "apidoc -i src/modules/ -o doc/ -t apidoc/template/",
    

    -i src/modules/为需要apidoc去查找生成的源文件

    -o doc/ 为output即生成后的目录

    -t apidoc/template/ 为模板源文件,可以省略(方便部署的时候才可能会用到)

参考:

webpack和rollup

commonJs规范