一、背景
技术 Leader 发现最近一段时间,大家的工作热情着实不高,眉头微皱,长叹一口气。心想,是时候来提高大家的工作热情了…
根据调研,同学们工作热情不高的主要原因是:每次使用浏览器控制台调试页面,看到满屏幕的输出,就特别烦躁,导致无心工作。
于是,Leader 找到了你,希望开发一个能提高大家工作热情的 SDK…
开发 SDK 要顾及的东西实在太多了!于是我使用了自研的 「SDK 脚手架工具」,生成了最标准的 SDK 模板。并且高效的完成了开发!
咳咳,还是先弄清楚开发 SDK 到底需要哪些步骤再说工具的事吧
二、SDK 初成
1. project-x
SDK 是给项目用的。所以先观察我们的项目「project-x」
project-x
├── README.md
├── index.html
├── package.json
├── src
| └── index.js
└── webpack.config.js
// src/index.js
console.log("Hello World!");
一个基于 webpack 的普通项目,主要逻辑是输出 "Hello World!"
定义:对于不再有调用方的项目。我们称之为「应用」
2. hummer-master
怎么才能看到满屏输出还不心烦呢?
心烦是因为输出的内容都是 debug。所以我们加入一些其他内容,比如幽默的小故事。这样开发者每次调试,都会先看到段子,笑过之后再排查问题,效率立马就提高了!
我们给 SDK 取名叫 hummer-master,并创建对应的文件
hummer-master
├── package.json
└── src
├── constant.js
└── index.js
// hummer-master/package.json
{
"name": "@baidu/hummer-master",
"version": "1.0.0",
// 保证 project-x 能正确找到项目入口
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"license": "ISC"
}
// hummer-master/src/index.js
import {HUMMER_STORY} from './constant'
function HummerMaster() {
this.storys = HUMMER_STORY
}
HummerMaster.prototype.getStory = function () {
const story = this.storys[
Math.floor(
Math.random() * this.storys.length
)
]
return story
}
export { HummerMaster }
定义:对于会被「应用」或者其他任意代码库引入的项目。我们称之为「类库」或者「SDK」
调用 SDK 的一方称为「调用方」
3. 使用
- 在 hummer-master 中执行
npm link
生成一个软链接 - 在 project-x 中使用
npm link hummer-master
引入写好的 SDK - 在代码中使用,并执行
npm run build
编译。查看效果
// project-x/src/index.js
+ import {HummerMaster} from '@baidu/hummer-master'
+ const hummerMaster = new HummerMaster()
+ console.log(hummerMaster.getStory());
console.log("Hello World!");
功能完全正常。因为 webpack 在编译过程中,会处理所有的导入、导出。因此 SDK 不用做任何编译也可以被正常调用
完美达成了 Leader 的需求!
三、满足不同模块化规范
使用了 hummer-master 之后,前端同学们都笑着 debug,开发效率提高了 200%
- 服务端的同学表示「我们主要基于 Node.js 开发,能不能也引入这个 SDK」
- 其他组的前端同学表示「我们的项目太老了,压根没用 webpack,能不能通过 script 直接使用」
这两种声音代表着模块化的不同规范。我们先大致了解下当前的主流规范
1. 规范简介
典型的模块规范有如下几种:
- CommonJS
- ES6 Modules
- UMD
1.1 CommonJS
CommonJS 是一套以在浏览器环境之外(服务器、桌面),构建 JavaScript 生态系统为目标而产生的规范
服务器端的 Node.js 是 CommonJS 规范的一个实现
cjs 只有一种导入、导出方式
// 导出方式
module.exports = {
a: 1,
b: 2
}
// 和上面等价,算一种
exports.a = 1;
exports.b = 2;
// 导入方式
const lib = require('./lib');
console.log('a:',lib.a);
console.log('b:',lib.b);
对 Node.js 来说,模块存放在本地硬盘,同步加载,等待时间就是硬盘的读取时间,这个时间非常短
1.2 ES6 Modules
ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言
ES6 在语言标准的层面上,实现了模块功能
esm 支持三种导入方式和两种导出方式,如下所示
// 导出方式
export default 'hello world'; // default export
export const name = 'hello world'; // named export
export {name:'hello world'} // 上一种的简便写法
// 导入方式
import lib from './lib'; // default import
import * as lib from './lib';
import { method1, method2 } from './lib';
1.3 UMD
希望提供一个前后端跨平台的解决方案(支持 AMD 与 CommonJS 模块方式)
- 先判断是否支持Node.js模块格式(exports 是否存在),存在则使用 Node.js 模块格式
- 再判断是否支持AMD(define 是否存在),存在则使用 AMD 方式加载模块
- 前两个都不存在,则将模块公开到全局(window 或 global)
目前输出 UMD 最主要的作用,是让 SDK 支持类似 <script src="xxx/sdk.js">
形式的引用。(也就是挂载到 window 或 global 上)
2. 让 SDK 满足规范
显然,我们不可能在源码里把每个规范都实现一遍。所以引入 rollup 来编译 SDK 源码, 输出满足三种规范的代码
Rollup 是基于 ES6 实现的代码模块化工具
安装 rollup
npm install rollup -D
新增配置文件
hummer-master
├── package.json
+ ├── build
+ | └── rollup.config.js
└── src
├── constant.js
└── index.js
// src/rollup.config.js
import { resolve } from 'path'
import { name } from '../package.json'
const FORMAT = {
'ES': 'es',
'CJS': 'cjs',
'UMD': 'umd'
}
const base = {
input: resolve(__dirname, '../src/index.js'),
};
const output = function (format) {
return {
name,
dir: resolve(__dirname, `../dist/${format}`),
// format 参数,决定输出需要满足哪一种模块化规范
format,
}
}
export default [
{
...base,
output: output(FORMAT.ES),
},
{
...base,
output: output(FORMAT.CJS),
},
{
...base,
output: output(FORMAT.UMD),
}
]
修改 package.json
{
"name": "@baidu/hummer-master",
"version": "1.0.0",
- "main": "src/index.js",
+ "main": "dist/cjs/index.js",
+ "module": "dist/es/index.js",
+ "files": ["dist"],
"scripts": {
+ "build":"rm -rf ./dist && rollup -c build/rollup.config.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"devDependencies": {
"rollup": "^2.55.1"
}
}
3. 测试输出
3.1 es
使用方式不变
3.2 cjs
mock 一个 nodejs 项目来测试 cjs 规范
// index.js
const {HummerMaster} = require('@baidu/hummer-master')
const hummerMaster = new HummerMaster()
console.log(hummerMaster.getStory());
console.log("Hello World!");
3.3 umd
创建一个 .html 文件直接访问,验证 umd
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
</head>
<body>
<script src="./node_modules/@baidu/hummer-master/dist/umd/index.js"></script>
<script> const { HummerMaster } = window['@baidu/hummer-master'] const hummerMaster = new HummerMaster() console.log(hummerMaster.getStory()); console.log("Hello World!"); </script>
</body>
</html>
四、SDK 中的 SDK
有同学表示,hummer-master 输出的信息不太明显。希望能和上下内容分隔开
假设我们刚好有两个现成的,设置分割线的库
- divider-top
- divider-bottom
目录结构都一样:
divider-top, divider-bottom
├── index.js
└── package.json
唯一区别在于遵守的模块化规范不同。一个是 esm、一个是 cjs
// divider-top/index.js
export const dividerTop = '↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓\n'
// divider-bottom/index.js
module.exports = {
dividerBottom:'\n↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑'
}
1. 引入依赖
生成软链接,并在 hummer-master 中安装这两个依赖
npm link divider-top divider-bottom
// package.json
{
"name": "@baidu/hummer-master",
"version": "1.0.0",
...
"license": "ISC",
+ "dependencies": {
+ "@baidu/divider-bottom": "^1.0.0",
+ "@baidu/divider-top": "^1.0.0"
+ }
}
修改源码,增加分割线
// src/index.js
import {HUMMER_STORY} from './constant'
+ import {dividerTop} from '@baidu/divider-top'
+ import {dividerBottom} from '@baidu/divider-bottom'
function HummerMaster() {
this.storys = HUMMER_STORY
}
HummerMaster.prototype.getStory = function () {
const story = this.storys[
Math.floor(
Math.random() * this.storys.length
)
]
+ return dividerTop + story + dividerBottom
}
export { HummerMaster }
执行 build 命令,编译代码。此时控制台会提示 warn
(!) Unresolved dependencies
https://rollupjs.org/guide/en/#warning-treating-module-as-external-dependency
@baidu/divider-top (imported by src/index.js)
@baidu/divider-Bottom (imported by src/index.js)
需要修改 rollup 的配置,告知其不用处理 dependencies 相关的 import
// src/rollup.config.js
import { resolve } from 'path'
+ import { name, dependencies } from '../package.json'
const FORMAT = {
'ES': 'es',
'CJS': 'cjs',
'UMD': 'umd'
}
const base = {
input: resolve(__dirname, '../src/index.js'),
+ external: Object.keys(dependencies)
};
// ... 省略
2. 依赖处理
观察 dist 里输出的最终代码。rollup 在处理依赖时,并没有跟处理源码一样,把代码引入。
输出的代码和源码完全没区别,在 cjs 规范下,唯一变动也只是把 import 处理成了 require
// hummer-master/dist/es/index.js
import { dividerTop } from '@baidu/divider-top';
import { dividerBottom } from '@baidu/divider-bottom';
// ... 中间省略
export { HummerMaster };
// hummer-master/dist/cjs/index.js
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var dividerTop = require('@baidu/divider-top');
var dividerBottom = require('@baidu/divider-bottom');
// ... 中间省略
exports.HummerMaster = HummerMaster;
这是因为对于「调用方」来说,在安装 hummer-master 的同时。也会安装其 dependencies 中的所有 SDK
- 如果没有同名且冲突的依赖,会直接安装到 projext-x 的
node_modules
下,和其他依赖同级 - 如果有同名且冲突的依赖,会直接安装到
hummer-master/node_modules
下
更多情况详见文章:「完全理解」各种 dependencies
project-x
├── README.md
├── index.html
├── package.json
├── src
| └── index.js
|
├── node_modules
| ├── @baidu
| | ├── divider-top
| | ├── divider-bottom
| | └── hummer-master
| ... 省略
|
└── webpack.config.js
es
因此在 projext-x 使用 webpack 编译时, 处理依赖中的依赖,和处理源码中的依赖并没什么区别
build 项目。项目运行正常,输出符合预期 这主要是得益于 webpack 的强大(可以同时处理 cjs、esm 规范的源码和依赖)
cjs
而在 node 环境中,由于无法识别 dividerTop 中的 esm 规范,调用时则会报错(这也提醒我们,node 项目虽然本身支持 cjs,也还是配合 webpack 开发比较靠谱,毕竟你无从得知依赖里是否存在一个 esm 模块会导致项目报错)
umd
script 调用的情境就更不用说了。压根不认识 import 和 require(不考虑最新的 type="module"
特性)
所以我们需要想办法,把 divider-top 和 divider-bottom 的代码也打包到输出中去。好满足 node 和 script 情况下的使用
3. 引入插件处理依赖
rollup 官方已经考虑到了这种情况,所以引入两个插件即可解决问题
- @rollup/plugin-node-resolve – 把依赖当成普通文件一样处理
- @rollup/plugin-commonjs – rollup 默认只支持 esm 规范,这个插件引入后可以同时支持 cjs 规范
npm install @rollup/plugin-node-resolve @rollup/plugin-commonjs -D
修改 rollup
// src/rollup.config.js
import { resolve } from 'path'
import { name, dependencies } from '../package.json'
+ import nodeResolve from '@rollup/plugin-node-resolve';
+ import commonjs from '@rollup/plugin-commonjs';
const FORMAT = {
'ES': 'es',
'CJS': 'cjs',
'UMD': 'umd'
}
const base = {
input: resolve(__dirname, '../src/index.js'),
external: Object.keys(dependencies),
+ plugins: [
+ nodeResolve(),
+ commonjs()
+ ]
};
// ... 省略
export default [
{
...base,
output: output(FORMAT.ES),
},
{
...base,
output: output(FORMAT.CJS),
},
{
...base,
output: output(FORMAT.UMD),
+ external: null,
}
]
这里只修改了 umd 中的 external 为 null。这是因为对于 esm 和 cjs,可以设想如下情境:
假设 hummer-master 依赖 A,project-x 或者及其依赖同样依赖 A。那么 project-x 经过编译,会同时存在两份 A 的代码。导致代码 size 增大
所以只让 umd 规范的输出把所有依赖都编译到最终产物
这样不依赖编译工具的 node 项目,以及 script 引入都能得到满足
// index.js
- const {HummerMaster} = require('@baidu/hummer-master')
+ const {HummerMaster} = require('@baidu/hummer-master/dist/umd')
const hummerMaster = new HummerMaster()
console.log(hummerMaster.getStory());
console.log("Hello World!");
当然你也可以让 es、cjs 的 external 为 null。这样的话,可以把所有的依赖都作为 devDependencies
五、 引入 babel
经过长时间的迭代,hummer-master 增加了新功能。可以用 promise 的形式,在等待指定时间后返回段子
// src/index.js
import {HUMMER_STORY} from './constant'
import {dividerTop} from '@baidu/divider-top'
import {dividerBottom} from '@baidu/divider-bottom'
function HummerMaster() {
this.storys = HUMMER_STORY
}
HummerMaster.prototype.getStory = function () {
const story = this.storys[
Math.floor(
Math.random() * this.storys.length
)
]
return dividerTop + story + dividerBottom
}
+ HummerMaster.prototype.proGetStory = function (time) {
+ return new Promise(res=>{
+ setTimeout(() => {
+ res(this.getStory())
+ }, time);
+ })
+ }
export { HummerMaster }
调用方式改变
// src/index.js
import {HummerMaster} from '@baidu/hummer-master'
const hummerMaster = new HummerMaster()
- console.log(hummerMaster.getStory());
+ hummerMaster.proGetStory(1000).then(res=>{
+ console.log(hummerMaster.getStory(res));
+ })
console.log("Hello World!");
那么像箭头函数、Promise 等新特性。低版本的浏览器显然无法支持,我们需要想办法处理成能兼容的代码
本文重点不是 babel,对于相关配置只做简单的介绍。babel 相关可以阅读我的另一篇文章:「完全理解」如何配置项目中的 Babel
1. api 和 syntax
js 兼容问题一般分为两部分:api(接口)和 syntax(句法)。 有个最直观的区分方法
不修改源码,只添加 polyfill(垫片)就可以兼容的是 api
// 挂载一个全局 Promise 类即可兼容
const pro = new Promise()
需要修改源码才能兼容的是 syntax
// 需要改写源码
const fn = ()=>{}
// 编译后
var fn = function fn() {};
2. syntax
先解决 syntax
安装 babel 相关的依赖
npm install @rollup/plugin-babel @babel/preset-env @babel/core -D
根目录新增 babal 配置文件
// hummer-master/babel.config.js
module.exports = {
'presets': [
[
'@babel/preset-env', {
useBuiltIns: false, // 不处理 api,只处理 syntax
},
]
],
};
在 rollup.config.js 中引入对应插件
// src/rollup.config.js
import { resolve } from 'path'
import { name, dependencies } from '../package.json'
import nodeResolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
+ import { babel } from '@rollup/plugin-babel';
const FORMAT = {
'ES': 'es',
'CJS': 'cjs',
'UMD': 'umd'
}
const base = {
input: resolve(__dirname, '../src/index.js'),
external: Object.keys(dependencies),
plugins: [
nodeResolve(),
commonjs(),
+ babel({ babelHelpers: 'bundled' })
]
};
// ... 省略
babelHelpers:github.com/rollup/plug…
执行 npm run build 之后,我们可以看到编译完的代码。已经处理好了箭头函数和 const
// hummer-master/dist/es/index.js
//... 省略
function HummerMaster() {
this.storys = HUMMER_STORY;
}
HummerMaster.prototype.getStory = function () {
var story = this.storys[Math.floor(Math.random() * this.storys.length)];
return dividerTop + story + dividerBottom;
};
HummerMaster.prototype.proGetStory = function (time) {
var _this = this; // const -> var
return new Promise(function (res) {
setTimeout(function () { // ()=> -> function(){}
res(_this.getStory());
}, time);
});
};
export { HummerMaster };
3. api
对于 api 的处理,常规方法有三种
3.1 引入所有 polyfill(替换、修改原生方法)
只推荐 project-x 这种最上级项目用,可以保证自身源码,和依赖里所有的 api 都能被覆盖到
缺点是代码量大,而且会造成 api 全局污染
// hummer-master/dist/es/index.js
import 'core-js/modules/es.symbol.js';
import 'core-js/modules/es.symbol.description.js';
import 'core-js/modules/es.symbol.async-iterator.js';
import 'core-js/modules/es.symbol.has-instance.js';
// ...
// ... 省略上百行
// ...
HummerMaster.prototype.proGetStory = function (time) {
var _this = this;
return new Promise(function (res) {
_setTimeout(function () {
res(_this.getStory());
}, time);
});
};
export { HummerMaster };
3.2 只引入当前源码既有 api 的 polyfill
通过 babel 配置 useBuiltIns: 'usage'
实现
// hummer-master/dist/es/index.js
import 'core-js/modules/es.object.to-string.js';
import 'core-js/modules/es.promise.js';
import 'core-js/modules/web.timers.js';
// ... 省略
HummerMaster.prototype.proGetStory = function (time) {
var _this = this;
return new Promise(function (res) {
_setTimeout(function () {
res(_this.getStory());
}, time);
});
};
export { HummerMaster };
3.3 只引入当前源码既有 api 的 runtime(不影响原生方法)
通过 babel 插件, @babel/plugin-transform-runtim 实现
// hummer-master/dist/es/index.js
import _Promise from '@babel/runtime-corejs3/core-js-stable/promise';
import _setTimeout from '@babel/runtime-corejs3/core-js-stable/set-timeout';
// ... 省略
HummerMaster.prototype.proGetStory = function (time) {
var _this = this;
return new _Promise(function (res) {
_setTimeout(function () {
res(_this.getStory());
}, time);
});
};
export { HummerMaster };
处理了 api 的同时,还不污染全局环境。
可以通过把 @babel/runtime-corejs3 设置为 peerDependencies,尝试减小调用方的引用成本
3.4 不处理 api
其实还有第四种方案。在 3.1 的方案我们可以知道, project-x 这种最上级项目可能会按浏览器版本配置的 polyfill。也就意味着会覆盖到所以需要兼容的 api
如果能确认 SDK 的所有调用方都使用的该方法,完全可以不处理 api,减小最终代码的体积
六、其他
经过上面一系列的配置,我们的 SDK 终于有了一个最小粒度、功能完备的框架。可以放心的提供给任何调用方了
而对于开发环境来说,其实还有很多需要优化的地方:
- 引入 typescript
- 引入 jest
- 引入 eslint
- 引入 husky
- …
SDK 脚手架
针对这一系列乱七八糟麻烦的配置,我开发了一个 SDK 脚手架工具。用于生成一个包含以上所有特性的 SDK 基础模板,方便后续的开发!
欢迎有 SDK 开发需求的同学使用 @draftbook/cli
draftbookJs/cli: Draftbook cli,For quickly creating a customized sdk (github.com)
今天的文章「完全理解」SDK 实现解析分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/18816.html