开发npm包,用JSDoc + DTS,让你的代码飞起来

1,406 阅读7分钟

本文主要面向新手同学,也欢迎大佬来评论及指导

为了让大家了解项目的每个部分的作用,我会先使用最原始的方式创建项目,从零开始创建一遍,再将其中的一些可以抽离的配置文件改为使用统一的预设配置引入

工具目录

  1. 开源仓库
    • 本文使用gitee,如果不是面向国际的项目,使用gitee,网络问题会比较少
    • github操作方式一样,网上资料也很多,想了解的可以自行谷歌,实际上无论你开发用哪个git服务,最终都可以互相同步
  2. 代码风格检查工具 - eslint
  3. git相关工具
  4. 构建工具 - gulp
  5. 单元测试工具 - jest
  6. 基于jsdoc的文档生成工具 - agds-doc
    • 这个工具是我封装的,算是夹带私货了吧

1. 新建项目(以node包为例)

  1. 创建仓库

    创建仓库的资料很多,我就不重复了,而且我觉得看文章的大家应该都会~~

  2. 创建本地项目

    仓库创建完成后,可以简单的按照这几个命令完成本地项目的创建

    mkdir [your-project]
    cd [your-project]
    git init
    npm init # 按照项目需要完成npm init的命令行交互
    git add .
    git commit -m 'feat: 首次提交'
    git remote add origin https://gitee.com/[you-gitee-name]/[your-project].git
    git push -u origin master
    

2. 创建项目初始配置

  1. 创建.eidtorconfig文件,统一项目的文本编辑配置

    此配置文件在vscode下需要真正生效需要配合EditorConfig for VS Code插件使用

    # .eidtorconfig
    root = true
    
    [*] # 配置适用的文件名
    # 你还可以用这种方式配置特定类型的编辑器配置
    # ```
    # [*.md]
    # max_line_length = off
    # trim_trailing_whitespace = false
    # tab_width = 1
    # indent_size = 2
    # ```
    charset = utf-8
    insert_final_newline = false # 是否使文件以一个空白行结尾
    trim_trailing_whitespace = true # 是否将行尾空格自动删除
    end_of_line = lf #换行符的类型。lf, cr, crlf三种
    indent_style = space # 缩进使用tab或者space
    tab_width = 2 # 缩进为tab时,缩进的宽度
    indent_size = 4 # 缩进为space时,缩进的字符数
    
  2. 创建eslint配置
    1. 初始化eslint配置
      • npx eslint --init
      ✔ How would you like to use ESLint? · style
      ✔ What type of modules does your project use? · esm
      ✔ Which framework does your project use? · none
      ✔ Does your project use TypeScript? · No / Yes
      ✔ Where does your code run? · node
      ✔ How would you like to define a style for your project? · guide
      ✔ Which style guide do you want to follow? · standard
      ✔ What format do you want your config file to be in? · JavaScript
      
    2. 配置.eslintignore和.gitignore
      lib/
      node_modules/
      
    3. 配置git生命周期检查代码
      • npm i -D yorkie lint-staged
        • 尤大的yorkie作为git生命周期脚本配置插件
        • lint-staged作为增量文件检查工具(避免lint脚本执行时间过长)
      • 配置package.json文件
        {
        +  "lint-staged": {
        +      "**/*.{js}": [
        +          "eslint  --fix"
        +      ]
        +  },
        +  "gitHooks": {
        +      "pre-commit": "lint-staged"
        +  },  
        }
        
      • 增加json、md文件的格式检查
        • npm i -D eslint-plugin-jsdoc eslint-plugin-json-format
        • 配置.eslintrc.js文件
          module.exports = {
          // 插件注册
          +    plugins: [
          +        'jsdoc',
          +        'json-format',
          +    ],
          // 一些默认的配置(降低配置成本)
              extends: [
                  'standard',
          +        'plugin:jsdoc/recommended',
          +        'plugin:markdown/recommended',
              ],
          +    settings: {
          // 一些基础的json配置
          +        'json/sort-package-json': false,
          +        'json/json-with-comments-files': [],
          +        'json/ignore-files': [],
          // 配置jsdoc(vscode仅支持typescript模式,原因懂得都懂~~)
          +        jsdoc: {
          +            mode: 'typescript',
          +        },
          +    },
               rules: {
                   // 一些简单的配置
                  indent: ['error', 4, { SwitchCase: 1 }],
                  semi: ['error', 'always'],
                  'comma-dangle': ['error', 'always-multiline'],
                  'space-before-function-paren': [
                      'error',
                      { anonymous: 'always', named: 'never', asyncArrow: 'always' },
                  ],
                   // jsdoc
                   // 由于jsdoc插件的默认配置过于严格,忽略一些不必要的规则,但是实际上还是有很多规则过于强制,大家也可以自行修改屏蔽
          +        'valid-jsdoc': 'off',
          +        'jsdoc/require-property': 0,
          +        'jsdoc/require-returns-description': 0,
          +        'jsdoc/no-undefined-types': 0,
              },
                  overrides: [
                      // 支持md文件内的js及json内容的检查,部分规则需要修改
          +            {
          +                files: ['**/*.md/*.{js,json}', 'docs/**', 'test/**'],
          +                rules: {
          +                    'no-console': 'off',
          +                    'import/no-unresolved': 'off',
          +                    'no-undef': 'off',
          +                    'no-unused-expressions': 'off',
          +                    'no-unused-vars': 'off',
          +                    'padded-blocks': 'off',
          +                    'eol-last': 'off',
                          },
                      },
                  ],
          };
          
        • package.jsonlint脚本增加md、json文件检查
          {
                "lint-staged": {
          +        "**/*.{js,json,md}": [
          -        "**/*.{js}": [
                    "eslint  --fix"
                  ]
                },
          }
          
  3. git提交信息格式检查commitlint
    • 安装依赖
      npm i -D @commitlint/cli @commitlint/config-conventional
      
    • 配置文件commitlint.config.js
      module.exports = {
          extends: ['@commitlint/config-conventional'],
      };
      
    • 将检查脚本加入package.jsongit钩子中
      {
          "gitHooks": {
              "pre-commit": "lint-staged",
      +        "commit-msg": "commitlint -e $GIT_PARAMS"
          }
      }
      
  4. 命令行交互式填写commit信息
    • 安装依赖
      npm i -D commitizen cz-conventional-changelog-zh
      
    • 配置.czrc
      // 支持汉化版的交互界面
      {
          "path": "cz-conventional-changelog-zh"
      }
      
    • 配置package.jsongitHooks.prepare-commit-msg
      {
        "gitHooks": {
            // agds-gc-has-msg命令可以判断当前commit是否传入-m参数,避免错误调用交互界面
      +    "prepare-commit-msg": "agds-gc-has-msg && exec < /dev/tty && git cz --hook || true"
        },
      }
      

3. 构建配置

  1. gulp构建配置
    • 安装依赖
      npm i -D gulp rimraf gulp-babel @babel/core  gulp-typescript typescript merge2 @agds/node-utils 
      
      • rimraf用来在每次构建前删除之前的输出
      • babel转译js文件,支持一些当前环境未支持的语法
      • typescript,生成dts文件,让包的类型声明更加好用,在编辑器中也能有更好的只能提示
      • merge2合并ts拆分的Stream流
      • @agds/node-utils 一些快捷的node工具类
    • 配置构建规则gulpfile.js
      const { series, src, dest, parallel } = require('gulp');
      const rimraf = require('rimraf');
      const babel = require('gulp-babel');
      const ts = require('gulp-typescript');
      const merge = require('merge2');
      const { FastPath, FastFs } = require('@agds/node-utils');
      const path = require('path');
      // js项目尽量不要在本地配置[tj]sconfig文件,会影响js库的智能提示和文件跳转
      let params = {
        "compilerOptions": {
          "target": "ESNext",
          "module": "commonjs",
          "allowJs": true,
          "declaration": true,
          "allowSyntheticDefaultImports": true,
          "baseUrl": ".",
          "moduleResolution": "node",
          "experimentalDecorators": true,
          "esModuleInterop": true,
          "declarationDir": "lib/types",
          "outDir": "lib"
        }
      };
      if (FastFs.getPathStatSync(FastPath.getCwdPath('tsconfig.json'))) {
          params = 'tsconfig.json';
      }
      const tsProject = ts.createProject(params);
      const _input = FastPath.getCwdPath('src');
      const _output = FastPath.getCwdPath('lib');
      /**
       * 清除构建目录
       *
       * @returns {Promise}
       */
      function clean() {
          return new Promise((resolve) => rimraf(_output, resolve));
      };
      /**
       * 构建
       *
       * @returns {merge.Merge2Stream}
       */
      function build() {
          const tsResult = src(path.join(_input, '**/*.[tj]s'))
              .pipe(tsProject());
      
          return merge([
              tsResult.dts.pipe(dest(path.join(_output, 'types'))),
              tsResult.js.pipe(babel()).pipe(dest(_output)),
          ]);
      }
      /**
       * 复制无法构建的文件
       *
       * @returns {NodeJS.ReadWriteStream}
       */
      function cp() {
          return src(path.join(_input, '**/*.!([tj]s)')).pipe(dest(_output));
      }
      exports.default = series(clean, parallel(build, cp));
      
    • package.json中添加build脚本
      {
            "scripts": {
      +            "build": "gulp",
            }
      }
      
  2. babel配置
    • 安装依赖
      npm i -D @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/plugin-transform-modules-commonjs @babel/preset-env
      
      • class-properties插件 支持class的属性声明方式
      • decorators插件支持可选链语法a?.b?.[0]?.()
      • transform-modules-commonjs 支持将esmodule语法转换为cjs语法
    • 配置文件babel.config.js
      module.exports = {
          presets: [
              [
                  '@babel/preset-env',
                  {
                      targets: {
                          node: '12',
                      },
                  },
              ],
          ],
          plugins: [
              ['@babel/plugin-proposal-decorators', { legacy: true }],
              ['@babel/plugin-proposal-class-properties', { loose: false }],
              ['@babel/plugin-transform-modules-commonjs'],
          ],
      };
      
    • eslint配置babel解析
      • 安装依赖
        npm i -D @babel/eslint-parser
        
      • 修改.eslintrc.js
        module.exports = {
        +    parser: '@babel/eslint-parser',
        +    parserOptions: {
        +        ecmaVersion: 12,
        +        babelOptions: {
        +            configFile: './babel.config.js',
        +        },
        +    },
        };
        

4. jest配置单测环境

  1. 下载依赖
    npm i -D jest
    
  2. 配置jest.config.js文件
    const { FastPath, FastFs } = require('@agds/node-utils');
    const pkgPath = FastPath.getCwdPath('package.json');
    const path = require('path');
    let pkg = {};
    if (FastFs.getPathStatSync(pkgPath)) {
        pkg = require(pkgPath);
    }
    const entry = 'src/index.js';
    module.exports = {
        collectCoverage: true,
        testEnvironment: 'node',
        roots: [
            '<rootDir>/test',
        ],
        moduleNameMapper: {
            [`^${pkg.name}$`]: path.join('<rootDir>', entry),
        },
        coverageThreshold: {
            global: {
                // 所有数据都设置100可能很严苛,大家可以根据需要自行更改
                statements: 100,
                branches: 100,
                functions: 100,
                lines: 100,
            },
        },
        testRegex: 'test/__test__/(.+)\\.(jsx?)$',
        moduleFileExtensions: ['js', 'jsx', 'json', 'node'],
        collectCoverageFrom: [
            'src/**/*.{js,jsx}',
            '!**/node_modules/**',
            '!**/test/**',
        ],
    };
    
  3. 新建测试目录
    └── test
        ├── __mock__  // 数据mock目录
        └── __test__  // 测试用例目录
    
  4. package.json新增test脚本
    {
      "scripts": {
        "build": "gulp",
    +    "test": "jest"
      },
    }
    

4. 配置文档自动生成

  1. 下载依赖
    npm i -D @agds/cli-plugin-doc
    
  2. 编写配置文件agds.doc.config.js
    /** @type {import('@agds/cli-plugin-doc').RenderOptions} */
    module.exports = {
        // 需使用jsdoc生成文档的文件路径规则
        files: ['./src/**/*.js'],
        // 代码演示文件目录路径规则
        codesDir: './test/*',
        // 代码演示文件路径规则
        codesFiles: ['*.js'],
        // 文档输出路径
        output: 'README.md',
    };
    
  3. package.json新增docs脚本
    {
      "scripts": {
        "test": "jest",
    +    "docs": "agds-doc"
      },
    }
    

5. 增加changelog配置

默认生成的diff链接格式为github格式,如何修改请自行百度或使用standard-version

  1. 安装依赖
    npm i -D conventional-changelog conventional-changelog-cli
    
  2. package.json中配置脚本
    {
        "scripts": {
    +        "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
        }
    }
    

到这里,前期项目准备工作就做完了

6. 编写功能代码

这里简单写一个示例

  1. src/index.js下添加如下内容
    /**
     * a和b相加
     *
     * @param {number} a 数字a
     * @param {number} b 数字b
     * @returns {number} a和b相加
     */
    export function add(a, b) {
        return a + b;
    }
    
  2. test/__test__/index.js下编写测试用例
    import { add } from 'agds-node-template';
    import { expect, test } from '@jest/globals';
    
    test('测试1+3', () => {
        const res = add(1, 3);
        expect(res).toBe(4);
    });
    
  3. 在命令行中执行
    npm run docs
    
  4. 你会得到这样的README.md
    • 源码 [your-package-name]是指代你的包名的变量,和version一起自动从package.json中获取

          # [your-package-name]
      
          **版本** :[your-package-version]
      
          npm库项目的简单模板
      
          ## 快速开始
      
          ### 安装
      
          ```bash
          npm i [your-package-name]
          ```
      
          ## 代码演示
      
          ```js
          import { add } from '[your-package-name]';
          import { expect, test } from '@jest/globals';
      
          test('测试1+3', () => {
              const res = add(1, 3);
              expect(res).toBe(4);
          });
          ```
      
          ## API文档
      
          <a name="add"></a>
      
          ### add(a, b) ⇒ <code>number</code>
      
          a和b相加
      
          **性质**: 函数
          **返回值**: <code>number</code> - a和b相加
      
          | 参数 | 类型 | 描述 |
          | --- | --- | --- |
          | a | <code>number</code> | 数字a |
          | b | <code>number</code> | 数字b |
      
      
    • 渲染后的效果


      [your-package-name]

      版本 :[your-package-version]

      npm库项目的简单模板

      快速开始

      安装

      npm i [your-package-name]
      

      代码演示

      import { add } from '[your-package-name]';
      import { expect, test } from '@jest/globals';
      
      test('测试1+3', () => {
          const res = add(1, 3);
          expect(res).toBe(4);
      });
      

      API文档

      add(a, b) ⇒ number

      a和b相加

      性质: 函数 返回值: number - a和b相加

      参数类型描述
      anumber数字a
      bnumber数字b

6. 基于npm hooks编写脚本代码

package.json的scripts字段中新增脚本

{
    "scripts": {
+        "docs:postbuild": "npm run docs && (git add . && git commit -m \"docs(readme): 更新文档\" || true)",
+        "commit": "git add . && git commit --no-edit",
+        "postversion": "npm run changelog",
        // npm包发布之前先执行test测试用例,确保代码运行正确,再执行build构建代码,最后执行docs:postbuild,生成文档并提交git
+        "prepublishOnly": "npm run test && npm run build && npm run docs:postbuild",
+        "postchangelog": "git add . && git commit -m \"docs(changelog): 更新CHANGELOG\" || true"
    }
}

接下来你只需要在每次修改代码后执行以下脚本即可完成发布

npm run commit
npm version patch # 非必选,如果需要修改版本的话最好使用这个方式,他会帮你在git上打上tag,npm version 有很多参数可以选择, npm version -h查看
npm publish # 如果你需要发布npm包的话
git push # 将变更推到git仓库

这个过程中,会自动调用以下功能

  • 命令行交互填写message
  • 基于git提交记录生成CHANGELOG.md文件
  • 执行单测脚本
  • 构建源码
  • 构建代码文档

7. 源码地址

  1. 本地配置版
  2. 使用预设配置版