我写过一些开源项目,在开源方面有一些经验,最近开到了阮老师的微博,深有感触,如今一个开源项目涉及的东西确实挺多的,特别是对于新手来讲很是不友好javascript
最近我写了一个jslib-base,旨在从多方面快速帮你们搭建一个标准的js库,本文将已jslib-base为例,介绍写一个开源库的知识css
jslib-base 最好用的js第三方库脚手架,赋能js第三方库开源,让开发一个js库更简单,更专业html
所谓代码未动,文档先行,文档对于一个项目很是重要,一个项目的文档包括前端
README是一个项目的门面,应该简单明了的呈现用户最关心的问题,一个开源库的用户包括使用者和贡献者,因此一个文档应该包括项目简介,使用者指南,贡献者指南三部分java
项目简介用该简单介绍项目功能,使用场景,兼容性的相关知识,这里重点介绍下徽章,相信你们都见过别人项目中的徽章,以下所示node
徽章经过更直观的方式,将更多的信息呈现出来,还可以提升颜值,有一个网站专门制做各类徽章,能够看这里webpack
这里有一个README的完整的例子git
TODO应该记录项目的将来计划,这对于贡献者和使用者都有很重要的意义,下面是TODO的例子es6
- [X] 已完成
- [ ] 未完成
复制代码
CHANGELOG记录项目的变动日志,对项目使用者很是重要,特别是在升级使用版本时,CHANGELOG须要记录项目的版本,发版时间和版本变动记录github
## 0.1.0 / 2018-10-6
- 新增xxx功能
- 删除xxx功能
- 更改xxx功能
复制代码
开源项目必需要选择一个协议,由于没有协议的项目是没有人敢使用的,关于不一样协议的区别能够看下面这张图(出自阮老师博客),个人建议是选择MIT或者BSD协议
开源项目还应该提供详细的使用文档,一份详细文档的每一个函数介绍都应该包括以下信息:
函数简单介绍
函数详细介绍
函数参数和返回值(要遵照下面的例子的规则)
- param {string} name1 name1描述
- return {string} 返回值描述
举个例子(要包含代码用例)
// 代码
特殊说明,好比特殊状况下会报错等
复制代码
理想的状况以下:
理想很丰满,现实很。。。,如何才可以让开发者和使用者都可以开心呢,jslib-base经过babel+rollup提供了解决方案
经过babel能够把ES6+的代码编译成ES5的代码,babel经理了5到6的进化,下面一张图总结了babel使用方式的变迁
本文不讨论babel的进化史(后面会单独开一片博文介绍),而是选择最现代化的babel-preset-env
方案,babel-preset-env能够经过提供提供兼容环境,而决定要编译那些ES特性
其原理大概以下,首先经过ES的特性和特性的兼容列表计算出每一个特性的兼容性信息,再经过给定兼容性要求,计算出要使用的babel插件
首先须要安装babel-preset-env
$ npm i --save-dev babel-preset-env
复制代码
而后新增一个.babelrc文件,添加下面的内容
{
"presets": [
["env",
{
"targets": {
"browsers": "last 2 versions, > 1%, ie >= 6, Android >= 4, iOS >= 6, and_uc > 9",
"node": "0.10"
},
"modules": false,
"loose": false
}]
]
}
复制代码
targets
中配置须要兼容的环境,关于浏览器配置对应的浏览器列表,能够从browserl.ist上查看
modules
表示编出输出的模块类型,支持"amd","umd","systemjs","commonjs",false这些选项,false表示不输出任何模块类型
loose
表明松散模式,将loose设置为true,可以更好地兼容ie8如下环境,下面是一个例子(ie8不支持Object.defineProperty
)
// 源代码
const aaa = 1;
export default aaa;
// loose false
Object.defineProperty(exports, '__esModule', {
value: true
});
var aaa = 1;
exports.default = 1;
// loose true
exports.__esModule = true;
var aaa = 1;
exports.default = 1;
复制代码
babel-preset-env
解决了语法新特性的兼容问题,若是想使用api新特性,在babel中通常经过babel-polyfill来解决,babel-polyfill经过引入一个polyfill文件来解决问题,这对于普通项目很实用,但对于库来讲就不太友好了
babel给库开发者提供的方案是babel-transform-runtime
,runtime提供相似程序运行时,能够将全局的polyfill沙盒化
首先须要安装babel-transform-runtime
$ npm i --save-dev babel-plugin-transform-runtime
复制代码
在.babelrc增长下面的配置
"plugins": [
["transform-runtime", {
"helpers": false,
"polyfill": false,
"regenerator": false,
"moduleName": "babel-runtime"
}]
]
复制代码
transform-runtime,支持三种运行时,下面是polyfill的例子
// 源代码
var a = Promise.resolve(1);
// 编译后的代码
var _promise = require('babel-runtime/core-js/promise');
var a = _promise.resolve(1); // Promise被替换为_promise
复制代码
虽然虽然能够优雅的解决问题,可是引入的文件很是之大,好比只用了ES6中数组的find功能,可能就会引入一个几千行的代码,个人建议对于库来讲能不用最好不用
编译解决了ES6到ES5的问题,打包能够把多个文件合并成一个文件,对外提供统一的文件入口,打包解决的是依赖引入的问题
我选择的rollup做为打包工具,rollup号称下一代打包方案,其有以下功能
webpack做为最流行的打包方案,rollup做为下一代打包方案,其实一句话就能够总结两者的区别:库使用rollup,其余场景使用webpack
为何我会这么说呢?下面经过例子对比下webpack和rollup的区别
假设咱们有两个文件,index.js和bar.js,其代码以下
bar.js对外暴漏一个函数bar
export default function bar() {
console.log('bar')
}
复制代码
index.js引用bar.js
import bar from './bar';
bar()
复制代码
下面是webpack的配置文件webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
}
};
复制代码
下面来看一下webpack打包输出的内容,o(╯□╰)o,别着急,咱们的代码在最下面的几行,上面这一大片代码实际上是webpack生成的简易模块系统,webpack的方案问题在于会生成不少冗余代码,这对于业务代码来讲没什么问题,但对于库来讲就不太友好了
注意:下面的代码基于webpack3,webpack4增长了scope hoisting,已经把多个模块合并到一个匿名函数中
/******/
(function(modules) { // webpackBootstrap
/******/ // The module cache
/******/
var installedModules = {};
/******/
/******/ // The require function
/******/
function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/
if (installedModules[moduleId]) {
/******/
return installedModules[moduleId].exports;
/******/
}
/******/ // Create a new module (and put it into the cache)
/******/
var module = installedModules[moduleId] = {
/******/
i: moduleId,
/******/
l: false,
/******/
exports: {}
/******/
};
/******/
/******/ // Execute the module function
/******/
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/
module.l = true;
/******/
/******/ // Return the exports of the module
/******/
return module.exports;
/******/
}
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/
__webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/
__webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/
__webpack_require__.d = function(exports, name, getter) {
/******/
if (!__webpack_require__.o(exports, name)) {
/******/
Object.defineProperty(exports, name, {
/******/
configurable: false,
/******/
enumerable: true,
/******/
get: getter
/******/
});
/******/
}
/******/
};
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/
__webpack_require__.n = function(module) {
/******/
var getter = module && module.__esModule ?
/******/
function getDefault() { return module['default']; } :
/******/
function getModuleExports() { return module; };
/******/
__webpack_require__.d(getter, 'a', getter);
/******/
return getter;
/******/
};
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/
__webpack_require__.p = "";
/******/
/******/ // Load entry module and return exports
/******/
return __webpack_require__(__webpack_require__.s = 0);
/******/
})
/************************************************************************/
/******/
([
/* 0 */
/***/
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */
var __WEBPACK_IMPORTED_MODULE_0__bar__ = __webpack_require__(1);
Object(__WEBPACK_IMPORTED_MODULE_0__bar__["a" /* default */ ])()
/***/
}),
/* 1 */
/***/
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* harmony export (immutable) */
__webpack_exports__["a"] = bar;
function bar() {
//
console.log('bar')
}
/***/
})
/******/
]);
复制代码
下面来看看rollup的结果,rollup的配置和webpack相似
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle2.js',
format: 'cjs'
}
};
复制代码
下面看看rollup的产出,简直完美有没有,模块彻底消失了,rollup经过顺序引入到同一个文件来解决模块依赖问题,rollup的方案若是要作拆包的话就会有问题,由于模块彻底透明了,但这对于库开发者来讲简直就是最完美的方案
'use strict';
function bar() {
//
console.log('bar');
}
bar();
复制代码
在ES6模块化以前,JS社区探索出了一些模块系统,好比node中的commonjs,浏览器中的AMD,还有能够同时兼容不一样模块系统的UMD,若是对这部份内容感兴趣,能够看我以前的一篇文章《JavaScript模块的前世此生》
对于浏览器原生,预编译工具和node,不一样环境中的模块化方案也不一样;因为浏览器环境不可以解析第三方依赖,因此浏览器环境须要把依赖也进行打包处理;不一样环境下引用的文件也不相同,下面经过一个表格对比下
浏览器(script,AMD,CMD) | 预编译工具(webpack,rollup,fis) | Node | |
---|---|---|---|
引用文件 | index.aio.js | index.esm.js | index.js |
模块化方案 | UMD | ES Module | commonjs |
自身依赖 | 打包 | 打包 | 打包 |
第三方依赖 | 打包 | 不打包 | 不打包 |
注意: legacy模式下的模块系统能够兼容ie6-8,但因为rollup的一个bug(这个bug是我发现的,但rollup并不打算修复,╮(╯▽╰)╭哎),legacy模式下,不可同时使用 export 与 export default
rollup是自然支持tree shaking,tree shaking能够提出依赖模块中没有被使用的部分,这对于第三方依赖很是有帮助,能够极大的下降包的体积
举个例子,假设index.js只是用了第三方包is.js中的一个函数isString
,没有treeshaking会将is.js所有引用进来
而使用了treeshaking的话则能够将is.js中的其余函数剔除,仅保留isString
函数
无规矩不成方圆,特别是对于开源项目,因为会有多人参与,因此你们遵照一份规范会事半功倍
首先能够经过.editorconfig
来保证缩进、换行的一致性,目前绝大部分浏览器都已经支持,能够看这里
下面的配置设置在js,css和html中都用空格代替tab,tab为4个空格,使用unix换行符,使用utf8字符集,每一个文件结尾添加一个空行
root = true
[{*.js,*.css,*.html}]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
insert_final_newline = true
[{package.json,.*rc,*.yml}]
indent_style = space
indent_size = 2
复制代码
其次能够经过eslint来保证代码风格一致,关于eslint的安装和配置这里再也不展开解释了,在jslib-base中只须要运行下面的命令就能够进行代码校验了,eslint的配置文件位于config/.eslintrc.js
$ npm run lint
复制代码
eslint只可以保证代码规范,却不能保证提供优秀的接口设计,关于函数接口设计有一些指导规则
参数数量
可选参数
参数校验与类型转换
参数类型
函数返回值
返回值可返回操做结果(获取接口),操做是否成功(保存接口)
返回值的类型要保持一致
返回值尽可能使用值类型(简单类型)
返回值尽可能不要使用复杂类型(避免反作用)
版本应该遵照开源社区通用的语义化版本
版本号格式:x.y.z
代码的提交应该遵照规范,这里推荐一个个人规范
没有单元测试的库都是耍流氓,单元测试可以保证每次交付都是有质量保证的,业务代码因为一次性和时间成本能够不作单元测试,但开源库因为须要反复迭代,对质量要求又极高,因此单元测试是必不可少的
关于单元测试有不少技术方案,其中一种选择是mocha+chai,mocha是一个单元测试框架,用来组织、运行单元测试,并输出测试报告;chai是一个断言库,用来作单元测试的断言功能
因为chai不可以兼容ie6-8,因此选择了另外一个断言库——expect.js,expect是一个BDD断言库,兼容性很是好,因此我选择的是mocha+expect.js
关于BDD与TDD的区别这里再也不赘述,感兴趣的同窗能够自行查阅相关资料
有了测试的框架,还须要写单元测试的代码,下面是一个例子
var expect = require('expect.js');
var base = require('../dist/index.js');
describe('单元测试', function() {
describe('功能1', function() {
it('相等', function() {
expect(1).to.equal(1);
});
});
});
复制代码
而后只需运行下面的命令,mocha会自动运行test目录下面的js文件
$ mocha
复制代码
mocha支持在node和浏览器中测试,但上面的框架在浏览器下有一个问题,浏览器无法支持require('expect.js')
,我用了一个比较hack的方法解决问题,早浏览器中从新定义了require的含义
<script src="../../node_modules/mocha/mocha.js"></script>
<script src="../../node_modules/expect.js/index.js"></script>
<script> var libs = { 'expect.js': expect, '../dist/index.js': jslib_base }; var require = function(path) { return libs[path]; } </script>
复制代码
下面是用mocha生成测试报告的例子,左边是在node中,右边是在浏览器中
没有可持续集成的库都是原始人,若是每次push都可以自动运行单元测试就行了,这样就省去了手动运行的繁琐,好在travis-ci已经为咱们提供了这个功能
用GitHub登陆travis-ci,就能够看到本身在GitHub上的项目了,而后须要打开下项目的开关,才可以打开自动集成功能
第二步,还须要在项目中添加一个文件.travis.yml
,内容以下,这样就能够在每次push时自动在node 4 6 8版本下运行npm test
命令,从而实现自动测试的目的
language: node_js
node_js:
- "8"
- "6"
- "4"
复制代码
开源库但愿获得用户的反馈,若是对用户提的issue有要求,能够设置一个模版,用来规范github上用户反馈的issue须要制定一些信息
经过提供.github/ISSUE_TEMPLATE
文件能够给issue提供模版,下面是一个例子,用户提issue时会自动带上以下的提示信息
### 问题是什么
问题的具体描述,尽可能详细
### 环境
- 手机: 小米6
- 系统:安卓7.1.1
- 浏览器:chrome 61
- jslib-base版本:0.2.0
- 其余版本信息
### 在线例子
若是有请提供在线例子
### 其余
其余信息
复制代码
jsmini是基于jslib-base的一系列库,jsmini的理念是小而美,而且无第三方依赖,开源了不少能力,可以 助力库开发者
五年弹指一挥间,本文总结了本身作开源项目的一些经验,但愿可以帮助你们,全部介绍的内容均可以在jslib-base里面找到
jslib-base是一个拿来即用脚手架,赋能js第三方库开源,快速开源一个标准的js库
最后再送给你们一句话,开源一个项目,重在开始,贵在坚持
最后推荐下个人新书《React状态管理与同构实战》,深刻解读前沿同构技术,感谢你们支持
当当:product.dangdang.com/25308679.ht…
最后最后招聘前端,后端,客户端啦!地点:北京+上海+成都,感兴趣的同窗,能够把简历发到个人邮箱: yanhaijing@yeah.net