如何编写一个JS SDK

1,495 阅读4分钟

技术选型

现代前端开发,不再是打开一个文本,一行行手敲代码,自己做代码压缩的时代了。为什么一定要选择打包工具来开发SDK?

  • 不一定非要使用构建工具来开发,使用构建工具主要是为了使用它强大的生态系统。比如代码风格检测、本地服务、同时构建多种规范的产物等等,方便我们的开发。

为什么选是 rollup 而不是 webpack?

  • 随着 rollup 和 webpack 的版本更新,二者之间的差异性特性越来越小。
  • rollup 配置简单,支持同时打包输出多种规范的产物(iife、cjs、umd、esm、amd、system)。
  • webpack 功能强大社区丰富,更加适合大型应用;不支持打包输出为es module,而且产物不是很纯净。
  • 构建App应用时,webpack比较合适;如果是类库(纯js项目),rollup更加适合。
  • webpack在打包的代码中,会引入一些webpack自身的代码,增加包大小,拖慢SDK的实际启动时间。对于包大小、执行速度有要求的SDK,rollup是更好的选择。

完整的开发流程

  1. 初始化项目
  2. 创建合理的目录结构
  3. 配置 eslint 统一代码风格
  4. 配置 babel
  5. 配置 git 提交的校验钩子
  6. 开始编写代码
  7. watch 模式开发(本地服务)
  8. 完善 package.json 必要字段
  9. 配置合适的 npm script
  10. 本地测试开发的 npm 包
  11. 发布包到 npm
  12. 提交代码到 git 仓库

合理的包结构

├── README.md // 对外接入文档,会在npm展示
├── docs // 文档说明
├── lib // 产物输出目录
├── package-lock.json
├── package.json
├── test // 测试用的demo
├── src // 源码
├── .babelrc // babel配置,将es6转义为es5
├── .eslintrc.json // eslint配置,规范代码格式
├── .gitignore // 不上传git的目录配置
├── .eslintignore // eslint不处理的目录配置
└── rollup.config.js // rollup打包配置 

使用rollup开发

示例代码

dev.sankuai.com/code/repo-d…

初始化

mkdir sdk-demo
cd sdk-demo
npm init -y // 创建package.json文件

配置 rollup

  1. 根据开发环境区分不同的配置
  2. 设置对应的 npm script
  3. 输出cjs规范的产物
import { terser } from "rollup-plugin-terser"; // 压缩代码
import replace from 'rollup-plugin-replace'; // 替换全局变量
import { babel } from '@rollup/plugin-babel'; // rollup 的 babel 插件,ES6转ES5
import eslint from '@rollup/plugin-eslint';
import packageConfig from './package.json';

const NODE_ENV = process.env.NODE_ENV;
const ENV = JSON.stringify(NODE_ENV || 'development');
const VERSION = packageConfig.version;
const banner =
'/*!\n' +
` * sdk-demo v${VERSION}-${ENV}\n` +
' * Released under the MIT License.\n' +
' */'

const plugins = [
    eslint({
        throwOnError: true,
        throwOnWarning: true,
        include: 'src/**'
    }),
    replace({
        exclude: 'node_modules/**',
        ENV,
        VERSION: `'${VERSION}'`
    }),
    babel({
        include: 'src/**/*',
        exclude: 'node_modules/**' // 仅仅转译我们的源码
    })
];

// 非开发环境压缩代码
if ('development' !== NODE_ENV) {
    plugins.push(terser({
        compress: {
            pure_getters: true, // 默认是 false. 如果你传入true,UglifyJS会假设对象属性的引用(例如foo.bar 或 foo["bar"])没有函数副作用
            unsafe: true, // 默认是 false. 使用 "unsafe"转换. 参考https://github.com/LiPinghai/UglifyJSDocCN#unsafecompress%E9%85%8D%E7%BD%AE
            unsafe_comps: true,
            dead_code: true,
            drop_console: true,
            drop_debugger: true,
            global_defs: {
                DEBUG: false,
            },
            pure_funcs: ["error", "warn"]
        }
    }));
}

const config = [
    {
        input: 'src/index.js',
        output: {
            name: 'sdkdemo',
            file: 'lib/index.js',
            format: 'cjs'
        },
        plugins,
        banner
    }
];
export default config;

配置eslint

{
    "extends": [
        "eslint:recommended",
        "prettier"
    ],
    "parser": "@babel/eslint-parser",
    "env": {
        "browser": true,
        "node": true,
        "es6": true,
        "commonjs": true
    },
    "globals": { // 下面都是小程序相关设置,如果不是小程序的SDK,可以删掉
        "__DEV__": true,
        "__WECHAT__": true,
        "__ALIPAY__": true,
        "App": true,
        "Page": true,
        "Component": true,
        "Behavior": true,
        "wx": true,
        "getApp": true,
        "getCurrentPages": true
    },
    "parserOptions": {
        "ecmaVersion": 6,
        "ecmaFeatures": {
            "experimentalObjectRestSpread": true
        },
        "sourceType": "module",
        "allowSyntheticDefaultImports": true
    },
    "rules": {
        "semi": 2,
        "no-dupe-args": 2,
        "no-const-assign": 2,
        "no-duplicate-case": 2
    }
}

配置babel

{
    "presets": [
        [
            "@babel/preset-env",
            {
                "modules": false, // 设置为false,否则 Babel 会在 Rollup 有机会做处理之前,将我们的模块转成 CommonJS。
                "targets": {
                    "ie": "10"
                }
            }
        ]
    ]
}

添加忽略文件

.gitignore

node_modules
.vscode
.DS_STORE
lib

.eslintignore

node_modules/**/*.js
lib/*.js

package.json

{
  "name": "sdk-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "rollup -c ./rollup.config.js",
    "build:prod": "export NODE_ENV=production && rollup -c ./rollup.config.js",
    "build:dev": "export NODE_ENV=development && rollup -c ./rollup.config.js",
    "build:analyze": "export BUNDLE_ANALYZE=true && npm run build",
    "test": "echo success",
    "lint": "echo success"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  
  "devDependencies": {
    "@babel/core": "^7.15.5",
    "@babel/eslint-parser": "^7.15.4",
    "@babel/preset-env": "^7.15.6",
    "@rollup/plugin-babel": "^5.3.0",
    "@rollup/plugin-eslint": "^8.0.1",
    "eslint": "^7.32.0",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^4.0.0",
    "husky": "^7.0.2",
    "lint-staged": "^11.1.2",
    "prettier": "^2.4.0",
    "rollup": "^2.56.3",
    "rollup-plugin-replace": "^2.2.0",
    "rollup-plugin-terser": "^7.0.2"
  },
  "gitHooks": {
    "pre-commit": "lint-staged"
  },
  "lint-staged": {
    "*.{js}": [
      "eslint --fix src",
      "git add"
    ]
  }
}

开发逻辑代码

  • 对外暴露的API
import import DemoDelegate from './demo';

const Demo = {};

Demo.getName = function() {
    DemoDelegate.getName();
};

Demo.getAge = function() {
    DemoDelegate.getAge();
};

export default Demo;
  • 内部逻辑代码
class Demo {
    constructor() {
        this.name = 'bing dwen dwen';
        this.age = 1;
    }

    getName() {
        console.log(this.name);
    }

    _setName(name) {
        this.name = name;
    }

    getAge() {
        console.log(this.age);
    }

    _setAge(age) {
        this.age = age;
    }
}

const instance = new Demo();
export default instance; // 输出实例

打包发布

JS SDK的接入方式一般分两种形式:

  • 一种是npm,使用import/require方式引入。
import react from 'React'
  • 一种是cdn链接,使用
<script src="https://code.jquery.com/jquery-3.6.0.js"></script>

使用

以xxxsdk为例

// 安装依赖
npm install xxxsdk
// 使用SDK
import xxx from 'xxxsdk'