我寫過一些開源項目,在開源方面有一些經驗,最近開到了阮老師的微博,深有感觸,如今一個開源項目涉及的東西確實挺多的,特別是對於新手來講很是不友好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