用 ES6 构建新一代可复用 JS 模块

  • A+
所属分类:JavaScript

你是不是也在为可以使用ES6的新特性而兴奋,却不太确定应该从哪开始,或者如何开始?不止你一个人这样!我已经花了一年半的时间去解决这个幸福的难题。在这段时间里 JavaScript 工具链中有几个令人兴奋的突破。

这些突破让我们可以用ES6书写完全的JS模块,而不会为了一些基本的条件而妥协,比如testing,linting 和(最重要的)其他人可以轻易理解我们所写的代码。

在这篇文章中,我们集中精力在如何用ES6构建JS模块,并且无论你在你的网站或者app中使用CommonJS,AMD(asynchronous module definition)或者普通的网页script引入,这个模块都可以轻易被引用。

The Tools

在这个系列文章的第一部分和第二部分,我们来看一下这些卓越的工具们。在这篇文章中,我们详细说明如何编写,编译,打包代码;而在第二篇文章会集中在linting,formatting 和 testing(利用 JSCS,ESLint,mocha,Chai,Karma 和 Istanbul)。让我们来看看在这篇文章中涉及到的工具:

  • Babel(刚刚度过了它的第一个生日)可以把ES6代码转化为ES5代码,不仅简单,而且优雅。
  • Webpack,webpack平寂了我们组里的“模块战争”,我们每个人都镇定得使用着webpack来应付_一切_(CommonJS,AMD 和 ES6)。它也在打包独立的ES6库方面做得非常棒——这是我们在过去一直渴望看到的。
  • Gulp一个强大的自动化构建工具。

The Goal

WRITE IN ES6, USE IN ES5

我们将要讨论的是书写客户端(client-side)ES6 _libraries_,而不是整个网站或者 app 。(无论是在你的开源项目里或者是在你工作中的软件项目,这是可以在不同的项目中可复用的代码。)”等一下!“,你可能会想:”这个难道不是在浏览器支持ES6之后才能实现的吗?“

你是对的!然而,我们利用上面提到的Babel可以把ES6代码转化为ES5代码,在大多数情况下现在就可以实现我们的目标。

MAKE IT EASY FOR ANYONE TO CONSUME

我们目标的第二部分是写一个无论在什么模块规范下都可以使用的JS模块。AMD死忠饭?你会得到一个可用的模块。CommonJS 加 browserify 才是你的最爱?没问题!你会得到一个可用的模块。或者你对AMD和CommonJS不感冒,你只是想要在你的页面上加一个<script>引用并且成功运行?你也会得到一个可用的模块。Webpack会把我们的代码打包成UMD( universal module definition)模块规范,使我们的代码在任何代码规范中都可用。

Setting Up Our Project

在接下来的几分钟,我们将要完成这些代码。我经常用src/spec/lib/文件夹来构建项目。在src/目录里,你会看到一个有趣的示例模块,这个模块是提供乐高电影里的乐高角色的随机语录。这个示例会用到ES6的classesmodulesconstdestructuringgenerator等--这些可以被安全转化为ES5代码的新特性。

这篇文章的主要目的是讨论如何利用 Babel 和 Webpack 来编译和打包 ES6 library。然而我还是想简要的介绍我们的示例代码以证明我们切实在用 ES6。

Note: 你如果是 ES6 新手,不必担心。这个示例足够简单到你们会看懂。

The LegoCharacter Class

在 LegoCharacter.js 模块中,我们可以看到如下代码(查看注释了解更多):

这些代码本身很无聊--class意味着可以被继承,就像我们在 Emmet.js 模块里做的:

在我们的项目中,LegoCharacter.js 和 emmet.js 都是分开的单独的文件--这是我们示例代码中的典型例子。跟你之前写的 JavaScript 代码相比,我们的示例代码可能比较陌生。然而,在我们完成我们一系列的工作之后,我们将会得到一个 将这些代码打包到一起的‘built’版本。

The index.js

我们项目中的另一个文件-- index.js --是我们项目的主入口。在这个文件中 import 了一些 Lego 角色的类,生成他们的实例,并且提供了一个生成器函数(generator function),这个生成器函数来 yield 一个随机的语录:

在这个代码块中,index.js 引入了lodash,我们的三个Lego角色的类,和一个实用函数(utility function)。然后生成三个类的实例,导出(exports)这三个实例和getRandomQuote方法。一切都很完美,当代码被转化为ES5代码后依然会有一样的作用。

OK. Now What?

我们已经运用了ES6的一些闪亮的新特性,那么如何才能转化为ES5的代码呢?首先,我们需要通过 npm来安装Babel:

在全局安装Babel会提供我们一个babel 命令行工具(command line interface (CLI) option)。如果在项目的根目录写下如下命令,我们可以编译我们的模块代码为ES5代码,并且把他们放到lib/目录:

现在看一下lib/目录,我们将看到如下文件列表:

还记得上面我们提到的吗?Babel把每一个模块代码转化为ES5代码,并且以同样的目录结构放入lib/目录。看一下这些文件可以告诉我们两个事情:

  • 首先,在node环境中只要依赖 babel/register运行时,这些文件就可以马上使用。在这篇文章结束之前,你会看到一个在node中运行的例子。
  • 第二,我们还有很多工作要做,以使这些文件打包进一个文件中,并且以UMD(universal module definition )规范打包,并且可以在浏览器环境中使用。

Enter webpack

我打赌你已经听说过Webpack,它被描述为“一个JavaScript和其他静态资源打包工具”。Webpack的典型应用场景就是作为你的网站应用的加载器和打包器,可以打包你的JavaScript代码和其他静态资源,比如CSS文件和模板文件,将它们打包为一个(或者更多)文件。webpack有一个非常棒的生态系统,叫做“loaders”,它可以使webpack对你的代码进行一些变换。打包一个UMD规范的文件并不是webpack最用途广泛的应用,我们还可以用webpack loader将ES6代码转化为ES5代码,并且把我们的示例代码打包为一个输出文件。

LOADERS

在webpack中,loaders可以做很多事情,比如转化ES6代码为ES5,把LESS编译为CSS,加载JSON文件,加载模板文件,等等。Loaders为将要转化的文件一个test模式。很多loaders也有自己额外的配置信息。(好奇有多少loaders存在?看这个列表

我们首先在全局环境安装webpack(它将给我们一个webpack命令行工具(CLI)):

接下来为我们本地项目安装babel-loader。这个loader可以加载我们的ES6模块并且把它们转化为ES5。我们可以在开发模式安装它,它将出现在package.json文件的devDependencies中:

在我们开始使用webpack之前,我们需要生成一个webpack的配置文件,以告诉webpack我们希望它对我们的文件做些什么工作。这个文件经常被命名为webpack.config.js,它是一个node模块格式的文件,输出一系列我们需要webpack怎么做的配置信息。

下面是初始化的webpack.config.js,我已经做了很多注释,我们也会讨论一些重要的细节:

让我们来看一些关键的配置信息。

Output

一个wenpack的配置文件应该有一个output对象,来描述webpack如何build 和 package我们的代码。在上面的例子中,我们需要打包一个UMD规范的文件到lib/目录中。

Externals

你应该注意到我们的示例中使用了lodash。我们从外部引入依赖lodash用来更好的构建我们的项目,而不是直接在output中include进来lodash本身。externals选项让我们具体声明一个外部依赖。在lodash的例子中,它的global property key(_)跟它的名字(”lodash“)是不一样的,所以我们上面的配置告诉webpack如何在不同的规范中依赖lodash(CommonJS, AMD and browser root)。

The Babel Loader

你可能注意到我们把 babel-loader 直接写成了“babel”。这是webpack的命名规范:如果插件命名为“myLoaderName-loader”格式,那么我们在用的时候就可以直接写做”myLoaderName“。

除了在node_modules/目录下的.js文件,loader会作用到任何其他.js文件。compact选项中的配置表示我们不需要压缩编译过的文件,因为我想要我的代码具有可读性(一会我们会压缩我们的代码)。

如果我们在项目根目录中运行webpack命令,它将根据webpack.config.js文件来build我们的代码,并且在命令行里输出如下的内容:

现在如果我们查看lib/目录,我们会发现一个崭新的legoQuotes.js文件,并且它是符合webpack的UMD规范的代码,就像下面的代码片段:

UMD规范首先检查是否是CommonJS规范,然后再检查是否是AMD规范,然后再检查另一种CommonJS规范,最后回落到纯浏览器引用。你可以发现首先在CommonJS或者AMD环境中检查是否以“lodash”加载lodash,然后在浏览器中是否以_代表lodash。

What Happened, Exactly?

当我们在命令行里运行webpack命令,它首先去寻找配置文件的默认名字(webpack.config.js),然后阅读这些配置信息。它会发现src/index.js是主入口文件,然后开始加载这个文件和这个文件的依赖项(除了lodash,我们已经告诉webpack这是外部依赖)。每一个依赖文件都是.js文件,所以babel loader会作用在每一个文件,把他们从ES6代码转化为ES5。然后所有的文件打包成为一个输出文件,legoQuotes.js,然后把它放到lib目录中。

观察代码会发现ES6代码确实已经被转化为ES5.比如,LegoCharacter类中有一个ES5构造函数:

It’s Usable!

这时我们就可以include这个打包好的文件到所有的浏览器(IE9+,当然~)中,也可以在node中运行完美,只要babel运行时依赖完美。

如果我们想在浏览器使用,它看起来会像下面的样子:

你会看到我们已经依赖legoQuotes.js(就在babel的browser-polyfill.js下面),就像其他依赖一样使用<script>标签。我们的main.js使用了legoQuotes库,看起来是这个样子:

在node环境中使用,是这个样子:

Moving To Gulp

Babel和webpack的命令行工具都非常有用和高效,但是我更倾向于用类似于Gulp的自动化构建工具来执行其他类似的任务。如果你有很多项目,那么你会体会到构建命令一致性所带来的好处,我们只需要记住类似gulp someTaskName的命令,而不需要记很多其他命令。在大多数情况下,这无所谓对与错,如果你喜欢其他的命令行工具,就去使用它。在我看来使用Gulp是一个简单而高效的选择。

SETTING UP A BUILD TASK

首先,我们要安装Gulp:

接下来我们创建一个gulpfile配置文件。然后我们运行npm install --save-dev webpack-stream命令,来安装和使用webpack-streamgulp 插件。这个插件可以让webpack在gulp任务中完美运行。

现在我已经把index.js放到了gulp的src中并且写入了output目录,那么我需要修改webpack.config.js文件,我删除了entry并且更新了filename。我还添加了devtool配置,它的值为#inline-source-map(这将会在一个文件末尾写入一个source map):

WHAT ABOUT MINIFYING?

我很高兴你问了这个问题!我们用gulp-uglify,配合使用gulp-sourcemaps(给我们的min文件生成source map),gulp-rename(我们给压缩文件重命名,这样就不会覆盖未压缩的原始文件),来完成代码压缩工作。我们添加它们到我们的项目中:

我们的未压缩文件依然有行内的source map,但是gulp-sourcemaps的作用是为压缩文件生成一个单独的source map文件:

现在在命令行里运行gulp build,我们会看到如下输出:

现在在lib/目录里有三个文件:legoQuotes.js,legoQuotes.min.js 和 legoQuotes.min.js.map。

Webpack Banner Plugin

如果你需要在你打包好的文件头部添加licence等注释信息,webpack可以简单实现。我更新了webpack.config.js文件,添加了BannerPlugin。我不喜欢亲自去编辑这些注释信息,所以我引入了package.json文件来获取这些关于库的信息。我还把webpack.config.js写成了ES6的格式,可以使用新特性template string来书写这些信息。在webpack.config.js文件底部可以看到我们添加了plugins属性,目前BannerPlugin使我们唯一使用的插件:

(Note: 值得注意的是当我把webpack.config.js写成ES6,就不能再使用webpack命令行工具来运行它了。)

我们的gulpfile.js也做了两个更新:在第一行添加了babel register hook;我们传入了gulp-uglify 的配置信息:

What’s Next?

我们已经为我们的旅途开了个好头!!到目前为止我们已经用Babel 和 webpack命令行工具构建了我们的项目,然后我们用gulp(和相关插件)自动化构建打包我们的项目。这篇文章的代码包含了example/文件夹,在其中有浏览器端和node端的示例。在下一篇文章中,我们将用 ESLint 和 JSCS 来检查我们的代码,用 mocha 和 chai 来书写测试,用 Karma 来跑这些测试,用 istanbul 来计量测试的覆盖面。同时,你可以看另一篇非常棒的文章--Designing Better JavaScript APIs,它可以帮助你写出更好的模块代码。

weinxin
我的微信公众号
我的微信公众号扫一扫

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: