Fork me on GitHub

模块的发展演变及包的构建与发布

 求同存异,站在巨人的肩膀上。

  上周我做了一篇从roadhogwebpack项目编译打包迁移的share。算是理清了webpack的脉络。但是同时也引出了我早期学习前端的另一个问题:工程中的模块化在我们前端项目里具体是如何体现的。

模块化的具体体现是什么

  这个问题对于现在的前端开发工程师而言,大概都能给出一个比较统一的广义上的答案,即把不同的实现写在不同的组件(文件)内,通过使用ES6的exportimport来导出和引入。可以说在我们的开发过程中,无时无刻不在和模块打交道。

  但是在早期缺少工程化的前端开发中是没有模块化这一概念的。

  2009年的时候,Ryan Dahl整出了NodeJS,并用于JavaScript的服务端编程,同时标志着JS模块化编程的诞生。也就是说JS的模块化实装是先从服务端开始的。之后在这种模块化之上,社区不断推出了各种工程化的方案,如gruntgulpwebpack等等。

  所以模块化的体现在我看来是要区分服务端体现和浏览器端体现的。在服务端,NodeJS本身的模块系统是按着CommonJS的规范来的。而过去流行的浏览器端模块化开发规范又有AMDCMD两种。这里可能会有朋友问为什么CommonJS的规范不能也用于浏览器端,我们其实可以通过下面简单的使用例子来讨论:

1
2
const sum = require('sum');
sum(1,2,3,4,5); // 15

  当我们在本地使用一个sum库时,其实这个依赖库已经下载到本地了,我们的require动作消耗的时间其实就是我们硬盘的处理时间,这个过程是非常短的,我们几乎可以忽略该过程。但如果是在浏览器端,我们需要去下载这个资源,那就要看网速的脸色和实际包的体积了,这可能是一个漫长的等待时间,并且由于浏览器跑JS的主线程是单线程的,这个下载期间后续操作会被阻塞,导致页面被锁死。

  在这个背景下,处理思路也很简单,既然同步不行,我们就用异步。通过回调的方式,在依赖库下载完后再触发,规避阻塞问题。大概实现如下:

1
2
3
  require(['sum'], function (sum) {
    sum(1,2,3,4,5);
  });

  有了思路,社区也先后给出了模块化的方案。先是国外有了AMD这种异步模块定义的规范,再是国内发展出来的CMD通用模块定义规范。CMD的主流实践应用是当时阿里玉伯推的SeaJS

  根据玉伯所言,AMDRequireJS在推广过程中对模块定义的规范化产出。而CMD则是SeaJS在推广过程中的规范化产出。两者都致力于浏览器端的模块化开发。两者的对比差异网络上也总结的很多:

  模块的处理AMD推崇的是预加载,而CMD则是懒加载。
  依赖的处理AMD推崇依赖前置,而CMD倾向依赖就近。
  API的职责划分AMD的API一个可以支持很多种情况,而CMD的API是根据指责划分的。

  以上算是对整个模块化发展的一个回顾,毕竟前端的发展一直非常迅速,即便现在只有一些老项目还在使用SeaJSRequireJS,但是对于“历史的演变”,作为开发者我认为还是很有必要了解的。

  现在我们的开发日常都是使用webpack进行工程化构建了,可以说在webpack的构建流程中,万事万物都能视作模块,尽管在我们的开发项目中有各种组件、引入库、样式、静态资源、模板,但最后部署到我们的服务器上(本地开发的webpack-dev-server、云服务器、机房的服务器等等)就只剩几个类型的文件了:html,也就是我们访问的web;整个工程编译打包后的js文件,它会在script中引入;所有CSS处理压缩后的一个通过link引入的样式文件,部分放在服务器静态资源目录的文件。从某种角度上而言,说最后输出的就是一个HTML文件也无妨,只不过在HTML内部引入了所需的jscss,资源文件(图片、音频等)。

  而关于gulpgrunt,其实它们都是前端构建工具,负责代码合并、压缩之类的流程,然而这些现在在webpack中通过插件都已经集成进来了,包括前面所讨论的模块化在webpack中也不再局限在JS上了,所以这也是为什么近些年越来越多的项目采用webpack构建,因为它太全面了,同时整个生态圈的完善也更进一步捍卫了它大哥的地位。

如何发布一个我们自己的包

  前文,我们讨论了前端模块化的发展,可以简单做一个总结。过去的前端在JS方向上通过RequireJSSeaJS进行浏览器端的JavaScript模块化,然后结合gruntgulp这些流程化构建工具,最终将资源压缩整合出部署在服务端的文件;现在则是结合npm成熟的包生态和webpack成熟的预编译以及资源整合处理压缩的能力来进行项目的工程化开发。

  相信开发人员在开发过程中,会做很多重复的工作,比如一些工具类方法,同样的目的,在不同人、不同场景都会出现结果一样,实现手法不一样的代码,给后续维护带来了很大的困扰,并且从代码量的角度上来说也是严重冗余的。为了处理这些痛点,我们会考虑将一些公共的东西提取抽离到一个文件中,我们可以称它为公共方法模块。一般放在项目树的utils中。如果我们只维护一个项目,其实这样已经足够了,但是当启动的项目多了,这个模块难道要一直copy么,这肯定是不优雅的,所以一旦依赖的多了,我们可以将这个模块处理成包,发布到npm上去。

  既然我们打算将模块作为包发布了,有个最基本的考虑就是包引入时的兼容性。那如何兼容AMDCMDCommonJS、浏览器script引入方式呢?

  先说一个最基本的思路,为了规避模块中的函数声明污染全局变量,我们会将函数绑定和声明放到自执行函数内。一个简单的结构如下:

1
2
3
4
5
6
(function() {
// 包 命名空间声明块
// 函数声明块
// 函数绑定块
// 导出方式判定块
})()

  内容就是根据你的实际场景填充了,填充完毕后,就是发布流程:

  1. 注册npm账号(npm adduser或官网注册)。
  2. npm login登陆。
  3. npm init生成package.json配置信息。
  4. npm publish发布。
  5. 更新版本,修改package.json内的version,再npm publish
  6. 删除指定版本包,npm unpublish 包名@版本号
  7. 删除整个包(所有关联版本),npm unpublish 包名 --force