资讯专栏INFORMATION COLUMN

2020年如何写一个现代的JavaScript库

joyqi / 2398人阅读

摘要:我写过一些开源项目,在开源方面有一些经验,最近开到了阮老师的微博,深有感触,现在一个开源项目涉及的东西确实挺多的,特别是对于新手来说非常不友好最近我写了一个,旨在从多方面快速帮大家搭建一个标准的库,本文将已为例,介绍写一个开源库的知识

我写过一些开源项目,在开源方面有一些经验,最近开到了阮老师的微博,深有感触,现在一个开源项目涉及的东西确实挺多的,特别是对于新手来说非常不友好

最近我写了一个jslib-base,旨在从多方面快速帮大家搭建一个标准的js库,本文将已jslib-base为例,介绍写一个开源库的知识

jslib-base 最好用的js第三方库脚手架,赋能js第三方库开源,让开发一个js库更简单,更专业
文档

所谓代码未动,文档先行,文档对于一个项目非常重要,一个项目的文档包括

README.md

TODO.md

CHANGELOG.md

LICENSE

doc

README.md

README是一个项目的门面,应该简单明了的呈现用户最关心的问题,一个开源库的用户包括使用者和贡献者,所以一个文档应该包括项目简介,使用者指南,贡献者指南三部分

项目简介用该简单介绍项目功能,使用场景,兼容性的相关知识,这里重点介绍下徽章,相信大家都见过别人项目中的徽章,如下所示

徽章通过更直观的方式,将更多的信息呈现出来,还能够提高颜值,有一个网站专门制作各种徽章,可以看这里

这里有一个README的完整的例子

TODO.md

TODO应该记录项目的未来计划,这对于贡献者和使用者都有很重要的意义,下面是TODO的例子

- [X] 已完成
- [ ] 未完成
CHANGELOG.md

CHANGELOG记录项目的变更日志,对项目使用者非常重要,特别是在升级使用版本时,CHANGELOG需要记录项目的版本,发版时间和版本变更记录

## 0.1.0 / 2018-10-6

- 新增xxx功能
- 删除xxx功能
- 更改xxx功能
LICENSE

开源项目必须要选择一个协议,因为没有协议的项目是没有人敢使用的,关于不同协议的区别可以看下面这张图(出自阮老师博客),我的建议是选择MIT或者BSD协议

doc

开源项目还应该提供详细的使用文档,一份详细文档的每个函数介绍都应该包括如下信息:

函数简单介绍

函数详细介绍

函数参数和返回值(要遵守下面的例子的规则)

- param {string} name1 name1描述
- return {string} 返回值描述

举个例子(要包含代码用例)

// 代码

特殊说明,比如特殊情况下会报错等
构建

理想的情况如下:

库开发者美滋滋的写ES6+的代码;

库使用者能够运行在浏览器(ie6-11)和node(0.12-10)中

库使用者能够使用AMD或CMD模块方案

库使用者能够使用webpack、rollup或fis等预编译工具

理想很丰满,现实很。。。,如何才能够让开发者和使用者都能够开心呢,jslib-base通过babel+rollup提供了解决方案

编译

通过babel可以把ES6+的代码编译成ES5的代码,babel经理了5到6的进化,下面一张图总结了babel使用方式的变迁

本文不讨论babel的进化史(后面会多带带开一片博文介绍),而是选择最现代化的babel-preset-env方案,babel-preset-env可以通过提供提供兼容环境,而决定要编译那些ES特性

其原理大概如下,首先通过ES的特性和[特性的兼容列表](http://kangax.github.io/compa...
)计算出每个特性的兼容性信息,再通过给定兼容性要求,计算出要使用的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 vs webpack

我选择的rollup作为打包工具,rollup号称下一代打包方案,其有如下功能

依赖解析,打包构建

仅支持ES6模块

Tree shaking

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](https://github.com/rollup/rol...
)(这个bug是我发现的,但rollup并不打算修复,╮(╯▽╰)╭哎),legacy模式下,不可同时使用 export 与 export default*

tree shaking

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只能够保证代码规范,却不能保证提供优秀的接口设计,关于函数接口设计有一些指导规则

参数数量

函数的参数个数最多不要超过5个

可选参数

可选参数应该放到后面

可选参数数量超过三个时,可以使用对象传入

可选参数,应该提供默认值

参数校验与类型转换

必传参数,如果不传要报错

对下列类型要做强制检验,类型不对要报错(object, array, function)

对下列类型要做自动转换(number, string, boolean)

对于复合类型的内部数据,也要做上面的两个步骤

对于number转换后如果为NaN,要做特殊处理(有默认值的赋值为默认值,无默认值的要报错)

参数类型

参数尽量使用值类型(简单类型)

参数尽量不要使用复杂类型(避免副作用)

使用复杂类型时,层级不要过深

使用复杂数据类型时,应该进行深拷贝(避免副作用)

函数返回值

返回值可返回操作结果(获取接口),操作是否成功(保存接口)

返回值的类型要保持一致

返回值尽量使用值类型(简单类型)

返回值尽量不要使用复杂类型(避免副作用)

版本规范

版本应该遵守开源社区通用的[语义化版本](https://semver.org/lang/zh-CN/
)

版本号格式:x.y.z

x 主版本号,不兼容的改动

y 次版本号,兼容的改动

z 修订版本号,bug修复

Git commit规范

代码的提交应该遵守规范,这里推荐一个我的规范

测试

没有单元测试的库都是耍流氓,单元测试能够保证每次交付都是有质量保证的,业务代码由于一次性和时间成本可以不做单元测试,但开源库由于需要反复迭代,对质量要求又极高,所以单元测试是必不可少的

关于单元测试有很多技术方案,其中一种选择是[mocha](https://mochajs.org/
)+[chai](http://www.chaijs.com/
),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的含义



下面是用mocha生成测试报告的例子,左边是在node中,右边是在浏览器中

可持续集成

没有可持续集成的库都是原始人,如果每次push都能够自动运行单元测试就好了,这样就省去了手动运行的繁琐,好在[travis-ci](https://www.travis-ci.org/
)已经为我们提供了这个功能

用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

jsmini是基于jslib-base的一系列库,jsmini的理念是小而美,并且无第三方依赖,开源了很多能力,能够
助力库开发者

总结

五年弹指一挥间,本文总结了自己做开源项目的一些经验,希望能够帮助大家,所有介绍的内容都可以在jslib-base里面找到

jslib-base是一个拿来即用脚手架,赋能js第三方库开源,快速开源一个标准的js库

最后再送给大家一句话,开源一个项目,重在开始,贵在坚持

最后推荐下我的新书《React状态管理与同构实战》,深入解读前沿同构技术,感谢大家支持

京东:https://item.jd.com/12403508.html

当当:http://product.dangdang.com/25308679.html

最后最后招聘前端,后端,客户端啦!地点:北京+上海+成都,感兴趣的同学,可以把简历发到我的邮箱: yanhaijing@yeah.net

原文网址:http://yanhaijing.com/javascr...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/98176.html

相关文章

  • CloudBest:度复盘丨盘点2020无处不在「云原生」

    摘要:华为云华为云在云原生这场游戏中,最具竞争力的玩家之一。年,金山云在云原生领域推出了三款重磅产品星曜裸金属服务器云服务器和云盘。在线上智博会上,浪潮云发布了经过全新迭代升级的浪潮云,进一步提升平台云原生服务能力。面对数字时代复杂系统的不确定性,传统的 IT 应用架构研发交付周期长、维护成本高、创新升级难,烟囱式架构,开放性差、组件复用度低,这些都成为了企业业务快速增长的瓶颈。而云原生以其敏捷、...

    Tecode 评论0 收藏0
  • 关于JavaScript, NPM官方发布了2018回顾以及2019预测

    摘要:不过,根据伯克利大学的这篇文章来看,拥有丰富的开源库,是开发者在选择一门开发语言时,最重要的因素。拥有超过个可用的开源库,是目前世界上最大的开源库集合。月份,我们发布了。这和年的情况是相反的。在的调查中,超过的受访者表示他们正在使用。 showImg(https://segmentfault.com/img/bVblvke?w=693&h=300); 原文标题:This year in...

    dadong 评论0 收藏0
  • 前端每周清单半盘点之 JavaScript

    摘要:前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点分为新闻热点开发教程工程实践深度阅读开源项目巅峰人生等栏目。背后的故事本文是对于年之间世界发生的大事件的详细介绍,阐述了从提出到角力到流产的前世今生。 前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点;分为新闻热点、开发教程、工程实践、深度阅读、开源项目、巅峰人生等栏目。欢迎...

    Vixb 评论0 收藏0
  • 持续拥抱云原生,现代化应用将把云计算带进怎样“新世界”?_云资讯

    摘要:全球云计算厂商躬身入局,开启现代化应用之旅事实上,包括亚马逊云科技华为云在内的全球云计算厂商已在这一领域进行了多年实践。过去年,亚马逊云科技一直在持续不断地突破很多现代化应用技术。年,亚马逊云科技发布第一个消息队列的服务,至今已有年历史。 2006年,是云计算滚滚浪潮的开端,这场IT技术变革始于亚马逊AWS的成立,它让公有云成为整个云行业的标杆,也形成了...

    RyanQ 评论0 收藏0
  • 前端每周清单:Node.js 微服务实践,Vue.js 与 GraphQL,Angular 组件技巧

    摘要:前端每周清单第期微服务实践,与,组件技巧,攻防作者王下邀月熊编辑徐川前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点分为新闻热点开发教程工程实践深度阅读开源项目巅峰人生等栏目。 前端每周清单第 26 期:Node.js 微服务实践,Vue.js 与 GraphQL,Angular 组件技巧,HeadlessChrome 攻防 作者:王下邀月熊 编辑:徐川...

    wall2flower 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<