你是否是也在为能够使用ES6的新特性而兴奋,却不太肯定应该从哪开始,或者如何开始?不止你一我的这样!我已经花了一年半的时间去解决这个幸福的难题。在这段时间里 JavaScript 工具链中有几个使人兴奋的突破。javascript
这些突破让咱们能够用ES6书写彻底的JS模块,而不会为了一些基本的条件而妥协,好比testing,linting 和(最重要的)其余人能够轻易理解咱们所写的代码。css
在这篇文章中,咱们集中精力在如何用ES6构建JS模块,而且不管你在你的网站或者app中使用CommonJS,AMD(asynchronous module definition)或者普通的网页script引入,这个模块均可以轻易被引用。html
在这个系列文章的第一部分和第二部分,咱们来看一下这些卓越的工具们。在这篇文章中,咱们详细说明如何编写,编译,打包代码;而在第二篇文章会集中在linting,formatting 和 testing(利用 JSCS,ESLint,mocha,Chai,Karma 和 Istanbul)。让咱们来看看在这篇文章中涉及到的工具:java
Babel(刚刚度过了它的第一个生日)能够把ES6代码转化为ES5代码,不只简单,并且优雅。node
Webpack,webpack平寂了咱们组里的“模块战争”,咱们每一个人都镇定得使用着webpack来应付_一切_(CommonJS,AMD 和 ES6)。它也在打包独立的ES6库方面作得很是棒——这是咱们在过去一直渴望看到的。webpack
Gulp一个强大的自动化构建工具。ios
咱们将要讨论的是书写客户端(client-side)ES6 _libraries_,而不是整个网站或者 app 。(不管是在你的开源项目里或者是在你工做中的软件项目,这是能够在不一样的项目中可复用的代码。)”等一下!“,你可能会想:”这个难道不是在浏览器支持ES6以后才能实现的吗?“git
你是对的!然而,咱们利用上面提到的Babel能够把ES6代码转化为ES5代码,在大多数状况下如今就能够实现咱们的目标。es6
咱们目标的第二部分是写一个不管在什么模块规范下均可以使用的JS模块。AMD死忠饭?你会获得一个可用的模块。CommonJS 加 browserify 才是你的最爱?没问题!你会获得一个可用的模块。或者你对AMD和CommonJS不感冒,你只是想要在你的页面上加一个<script>
引用而且成功运行?你也会获得一个可用的模块。Webpack会把咱们的代码打包成UMD( universal module definition)模块规范,使咱们的代码在任何代码规范中均可用。github
在接下来的几分钟,咱们将要完成这些代码。我常常用src/,spec/ 和 lib/文件夹来构建项目。在src/目录里,你会看到一个有趣的示例模块,这个模块是提供乐高电影里的乐高角色的随机语录。这个示例会用到ES6的classes,modules,const,destructuring,generator等--这些能够被安全转化为ES5代码的新特性。
这篇文章的主要目的是讨论如何利用 Babel 和 Webpack 来编译和打包 ES6 library。然而我仍是想简要的介绍咱们的示例代码以证实咱们切实在用 ES6。
Note: 你若是是 ES6 新手,没必要担忧。这个示例足够简单到大家会看懂。
在 LegoCharacter.js 模块中,咱们能够看到以下代码(查看注释了解更多):
// LegoCharacter.js // Let's import only the getRandom method from utils.js import { getRandom } from "./utils"; // the LegoCharacter class is the default export of the module, similar // in concept to how many node module authors would export a single value export default class LegoCharacter { // We use destructuring to match properties on the object // passed into separate variables for character and actor constructor( { character, actor } ) { this.actor = actor; this.name = character; this.sayings = [ "I haven't been given any funny quotes yet." ]; } // shorthand method syntax, FOR THE WIN // I've been making this typo for years, it's finally valid syntax :) saySomething() { return this.sayings[ getRandom( 0, this.sayings.length - 1 ) ]; } }
这些代码自己很无聊--class意味着能够被继承,就像咱们在 Emmet.js 模块里作的:
// emmet.js import LegoCharacter from "./LegoCharacter"; // Here we use the extends keyword to make // Emmet inherit from LegoCharacter export default class Emmet extends LegoCharacter { constructor() { // super lets us call the LegoCharacter's constructor super( { actor: "Chris Pratt", character: "Emmet" } ); this.sayings = [ "Introducing the double-decker couch!", "So everyone can watch TV together and be buddies!", "We're going to crash into the sun!", "Hey, Abraham Lincoln, you bring your space chair right back!", "Overpriced coffee! Yes!" ]; } }
在咱们的项目中,LegoCharacter.js 和 emmet.js 都是分开的单独的文件--这是咱们示例代码中的典型例子。跟你以前写的 JavaScript 代码相比,咱们的示例代码可能比较陌生。然而,在咱们完成咱们一系列的工做以后,咱们将会获得一个 将这些代码打包到一块儿的‘built’版本。
咱们项目中的另外一个文件-- index.js --是咱们项目的主入口。在这个文件中 import 了一些 Lego 角色的类,生成他们的实例,而且提供了一个生成器函数(generator function),这个生成器函数来 yield
一个随机的语录:
// index.js // Notice that lodash isn't being imported via a relative path // but all the other modules are. More on that in a bit :) import _ from "lodash"; import Emmet from "./emmet"; import Wyldstyle from "./wyldstyle"; import Benny from "./benny"; import { getRandom } from "./utils"; // Taking advantage of new scope controls in ES6 // once a const is assigned, the reference cannot change. // Of course, transpiling to ES5, this becomes a var, but // a linter that understands ES6 can warn you if you // attempt to re-assign a const value, which is useful. const emmet = new Emmet(); const wyldstyle = new Wyldstyle(); const benny = new Benny(); const characters = { emmet, wyldstyle, benny }; // Pointless generator function that picks a random character // and asks for a random quote and then yields it to the caller function* randomQuote() { const chars = _.values( characters ); const character = chars[ getRandom( 0, chars.length - 1 ) ]; yield `${character.name}: ${character.saySomething()}`; } // Using object literal shorthand syntax, FTW export default { characters, getRandomQuote() { return randomQuote().next().value; } };
在这个代码块中,index.js 引入了lodash,咱们的三个Lego角色的类,和一个实用函数(utility function)。而后生成三个类的实例,导出(exports)这三个实例和getRandomQuote
方法。一切都很完美,当代码被转化为ES5代码后依然会有同样的做用。
咱们已经运用了ES6的一些闪亮的新特性,那么如何才能转化为ES5的代码呢?首先,咱们须要经过 npm来安装Babel:
npm install -g babel
在全局安装Babel会提供咱们一个babel
命令行工具(command line interface (CLI) option)。若是在项目的根目录写下以下命令,咱们能够编译咱们的模块代码为ES5代码,而且把他们放到lib/目录:
babel ./src -d ./lib/
如今看一下lib/目录,咱们将看到以下文件列表:
LegoCharacter.js benny.js emmet.js index.js utils.js wyldstyle.js
还记得上面咱们提到的吗?Babel把每个模块代码转化为ES5代码,而且以一样的目录结构放入lib/目录。看一下这些文件能够告诉咱们两个事情:
首先,在node环境中只要依赖 babel/register运行时,这些文件就能够立刻使用。在这篇文章结束以前,你会看到一个在node中运行的例子。
第二,咱们还有不少工做要作,以使这些文件打包进一个文件中,而且以UMD(universal module definition )规范打包,而且能够在浏览器环境中使用。
我打赌你已经据说过Webpack,它被描述为“一个JavaScript和其余静态资源打包工具”。Webpack的典型应用场景就是做为你的网站应用的加载器和打包器,能够打包你的JavaScript代码和其余静态资源,好比CSS文件和模板文件,将它们打包为一个(或者更多)文件。webpack有一个很是棒的生态系统,叫作“loaders”,它可使webpack对你的代码进行一些变换。打包一个UMD规范的文件并非webpack最用途普遍的应用,咱们还能够用webpack loader将ES6代码转化为ES5代码,而且把咱们的示例代码打包为一个输出文件。
在webpack中,loaders能够作不少事情,好比转化ES6代码为ES5,把LESS编译为CSS,加载JSON文件,加载模板文件,等等。Loaders为将要转化的文件一个test
模式。不少loaders也有本身额外的配置信息。(好奇有多少loaders存在?看这个列表)
咱们首先在全局环境安装webpack(它将给咱们一个webpack命令行工具(CLI)):
npm install -g webpack
接下来为咱们本地项目安装babel-loader。这个loader能够加载咱们的ES6模块而且把它们转化为ES5。咱们能够在开发模式安装它,它将出如今package.json文件的devDependencies
中:
npm install --save-dev babel-loader
在咱们开始使用webpack以前,咱们须要生成一个webpack的配置文件,以告诉webpack咱们但愿它对咱们的文件作些什么工做。这个文件常常被命名为webpack.config.js,它是一个node模块格式的文件,输出一系列咱们须要webpack怎么作的配置信息。
下面是初始化的webpack.config.js,我已经作了不少注释,咱们也会讨论一些重要的细节:
module.exports = { // entry is the "main" source file we want to include/import entry: "./src/index.js", // output tells webpack where to put the bundle it creates output: { // in the case of a "plain global browser library", this // will be used as the reference to our module that is // hung off of the window object. library: "legoQuotes", // We want webpack to build a UMD wrapper for our module libraryTarget: "umd", // the destination file name filename: "lib/legoQuotes.js" }, // externals let you tell webpack about external dependencies // that shouldn't be resolved by webpack. externals: [ { // We're not only webpack that lodash should be an // external dependency, but we're also specifying how // lodash should be loaded in different scenarios // (more on that below) lodash: { root: "_", commonjs: "lodash", commonjs2: "lodash", amd: "lodash" } } ], module: { loaders: [ // babel loader, testing for files that have a .js extension // (except for files in our node_modules folder!). { test: /\.js$/, exclude: /node_modules/, loader: "babel", query: { compact: false // because I want readable output } } ] } };
让咱们来看一些关键的配置信息。
一个wenpack的配置文件应该有一个output
对象,来描述webpack如何build 和 package咱们的代码。在上面的例子中,咱们须要打包一个UMD规范的文件到lib/目录中。
你应该注意到咱们的示例中使用了lodash。咱们从外部引入依赖lodash用来更好的构建咱们的项目,而不是直接在output中include进来lodash自己。externals
选项让咱们具体声明一个外部依赖。在lodash的例子中,它的global property key(_
)跟它的名字(”lodash“)是不同的,因此咱们上面的配置告诉webpack如何在不一样的规范中依赖lodash(CommonJS, AMD and browser root)。
你可能注意到咱们把 babel-loader 直接写成了“babel”。这是webpack的命名规范:若是插件命名为“myLoaderName-loader”格式,那么咱们在用的时候就能够直接写作”myLoaderName“。
除了在node_modules/目录下的.js文件,loader会做用到任何其余.js文件。compact
选项中的配置表示咱们不须要压缩编译过的文件,由于我想要个人代码具备可读性(一会咱们会压缩咱们的代码)。
若是咱们在项目根目录中运行webpack
命令,它将根据webpack.config.js文件来build咱们的代码,而且在命令行里输出以下的内容:
» webpack Hash: f33a1067ef2c63b81060 Version: webpack 1.12.1 Time: 758ms Asset Size Chunks Chunk Names lib/legoQuotes.js 12.5 kB 0 [emitted] main + 7 hidden modules
如今若是咱们查看lib/目录,咱们会发现一个崭新的legoQuotes.js文件,而且它是符合webpack的UMD规范的代码,就像下面的代码片断:
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(require("lodash")); else if(typeof define === 'function' && define.amd) define(["lodash"], factory); else if(typeof exports === 'object') exports["legoQuotes"] = factory(require("lodash")); else root["legoQuotes"] = factory(root["_"]); })(this, function(__WEBPACK_EXTERNAL_MODULE_1__) { // MODULE CODE HERE });
UMD规范首先检查是不是CommonJS规范,而后再检查是不是AMD规范,而后再检查另外一种CommonJS规范,最后回落到纯浏览器引用。你能够发现首先在CommonJS或者AMD环境中检查是否以“lodash”加载lodash,而后在浏览器中是否以_
表明lodash。
当咱们在命令行里运行webpack
命令,它首先去寻找配置文件的默认名字(webpack.config.js),而后阅读这些配置信息。它会发现src/index.js是主入口文件,而后开始加载这个文件和这个文件的依赖项(除了lodash,咱们已经告诉webpack这是外部依赖)。每个依赖文件都是.js文件,因此babel loader会做用在每个文件,把他们从ES6代码转化为ES5。而后全部的文件打包成为一个输出文件,legoQuotes.js,而后把它放到lib目录中。
观察代码会发现ES6代码确实已经被转化为ES5.好比,LegoCharacter
类中有一个ES5构造函数:
// around line 179 var LegoCharacter = (function () { function LegoCharacter(_ref) { var character = _ref.character; var actor = _ref.actor; _classCallCheck(this, LegoCharacter); this.actor = actor; this.name = character; this.sayings = ["I haven't been given any funny quotes yet."]; } _createClass(LegoCharacter, [{ key: "saySomething", value: function saySomething() { return this.sayings[(0, _utils.getRandom)(0, this.sayings.length - 1)]; } }]); return LegoCharacter; })();
这时咱们就能够include这个打包好的文件到全部的浏览器(IE9+,固然~)中,也能够在node中运行完美,只要babel运行时依赖完美。
若是咱们想在浏览器使用,它看起来会像下面的样子:
<!-- index.html --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Lego Quote Module Example</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="container"> <blockquote id="quote"></blockquote> <button id="btnMore">Get Another Quote</button> </div> <script src="../node_modules/lodash/index.js"></script> <script src="../node_modules/babel-core/browser-polyfill.js"></script> <script src="../lib/legoQuotes.js"></script> <script src="./main.js"></script> </body> </html>
你会看到咱们已经依赖legoQuotes.js(就在babel的browser-polyfill.js下面),就像其余依赖同样使用<script>
标签。咱们的main.js使用了legoQuotes库,看起来是这个样子:
// main.js ( function( legoQuotes ) { var btn = document.getElementById( "btnMore" ); var quote = document.getElementById( "quote" ); function writeQuoteToDom() { quote.innerHTML = legoQuotes.getRandomQuote(); } btn.addEventListener( "click", writeQuoteToDom ); writeQuoteToDom(); } )( legoQuotes );
在node环境中使用,是这个样子:
require("babel/polyfill"); var lego = require("./lib/legoQuotes.js"); console.log(lego.getRandomQuote()); // > Wyldstyle: Come with me if you want to not die.
Babel和webpack的命令行工具都很是有用和高效,可是我更倾向于用相似于Gulp的自动化构建工具来执行其余相似的任务。若是你有不少项目,那么你会体会到构建命令一致性所带来的好处,咱们只须要记住相似gulp someTaskName
的命令,而不须要记不少其余命令。在大多数状况下,这无所谓对与错,若是你喜欢其余的命令行工具,就去使用它。在我看来使用Gulp是一个简单而高效的选择。
首先,咱们要安装Gulp:
npm install -g gulp
接下来咱们建立一个gulpfile配置文件。而后咱们运行npm install --save-dev webpack-stream
命令,来安装和使用webpack-streamgulp 插件。这个插件可让webpack在gulp任务中完美运行。
// gulpfile.js var gulp = require( "gulp" ); var webpack = require( "webpack-stream" ); gulp.task( "build", function() { return gulp.src( "src/index.js" ) .pipe( webpack( require( "./webpack.config.js" ) ) ) .pipe( gulp.dest( "./lib" ) ) } );
如今我已经把index.js放到了gulp的src中而且写入了output目录,那么我须要修改webpack.config.js文件,我删除了entry
而且更新了filename
。我还添加了devtool配置,它的值为#inline-source-map
(这将会在一个文件末尾写入一个source map):
// webpack.config.js module.exports = { output: { library: "legoQuotes", libraryTarget: "umd", filename: "legoQuotes.js" }, devtool: "#inline-source-map", externals: [ { lodash: { root: "_", commonjs: "lodash", commonjs2: "lodash", amd: "lodash" } } ], module: { loaders: [ { test: /\.js$/, exclude: /node_modules/, loader: "babel", query: { compact: false } } ] } };
我很高兴你问了这个问题!咱们用gulp-uglify,配合使用gulp-sourcemaps(给咱们的min文件生成source map),gulp-rename(咱们给压缩文件重命名,这样就不会覆盖未压缩的原始文件),来完成代码压缩工做。咱们添加它们到咱们的项目中:
npm install --save-dev gulp-uglify gulp-sourcemaps gulp-rename
咱们的未压缩文件依然有行内的source map,可是gulp-sourcemaps的做用是为压缩文件生成一个单独的source map文件:
// gulpfile.js var gulp = require( "gulp" ); var webpack = require( "webpack-stream" ); var sourcemaps = require( "gulp-sourcemaps" ); var rename = require( "gulp-rename" ); var uglify = require( "gulp-uglify" ); gulp.task( "build", function() { return gulp.src( "src/index.js" ) .pipe( webpack( require( "./webpack.config.js" ) ) ) .pipe( gulp.dest( "./lib" ) ) .pipe( sourcemaps.init( { loadMaps: true } ) ) .pipe( uglify() ) .pipe( rename( "legoQuotes.min.js" ) ) .pipe( sourcemaps.write( "./" ) ) .pipe( gulp.dest( "lib/" ) ); } );
如今在命令行里运行gulp build
,咱们会看到以下输出:
» gulp build [19:08:25] Using gulpfile ~/git/oss/next-gen-js/gulpfile.js [19:08:25] Starting 'build'... [19:08:26] Version: webpack 1.12.1 Asset Size Chunks Chunk Names legoQuotes.js 23.3 kB 0 [emitted] main [19:08:26] Finished 'build' after 1.28 s
如今在lib/目录里有三个文件:legoQuotes.js,legoQuotes.min.js 和 legoQuotes.min.js.map。
若是你须要在你打包好的文件头部添加licence等注释信息,webpack能够简单实现。我更新了webpack.config.js文件,添加了BannerPlugin。我不喜欢亲自去编辑这些注释信息,因此我引入了package.json文件来获取这些关于库的信息。我还把webpack.config.js写成了ES6的格式,可使用新特性template string来书写这些信息。在webpack.config.js文件底部能够看到咱们添加了plugins
属性,目前BannerPlugin
使咱们惟一使用的插件:
// webpack.config.js import webpack from "webpack"; import pkg from "./package.json"; var banner = ` ${pkg.name} - ${pkg.description} Author: ${pkg.author} Version: v${pkg.version} Url: ${pkg.homepage} License(s): ${pkg.license} `; export default { output: { library: pkg.name, libraryTarget: "umd", filename: `${pkg.name}.js` }, devtool: "#inline-source-map", externals: [ { lodash: { root: "_", commonjs: "lodash", commonjs2: "lodash", amd: "lodash" } } ], module: { loaders: [ { test: /\.js$/, exclude: /node_modules/, loader: "babel", query: { compact: false } } ] }, plugins: [ new webpack.BannerPlugin( banner ) ] };
(Note: 值得注意的是当我把webpack.config.js写成ES6,就不能再使用webpack命令行工具来运行它了。)
咱们的gulpfile.js也作了两个更新:在第一行添加了babel register hook;咱们传入了gulp-uglify 的配置信息:
// gulpfile.js require("babel/register"); var gulp = require( "gulp" ); var webpack = require( "webpack-stream" ); var sourcemaps = require( "gulp-sourcemaps" ); var rename = require( "gulp-rename" ); var uglify = require( "gulp-uglify" ); gulp.task( "build", function() { return gulp.src( "src/index.js" ) .pipe( webpack( require( "./webpack.config.js" ) ) ) .pipe( gulp.dest( "./lib" ) ) .pipe( sourcemaps.init( { loadMaps: true } ) ) .pipe( uglify( { // This keeps the banner in the minified output preserveComments: "license", compress: { // just a personal preference of mine negate_iife: false } } ) ) .pipe( rename( "legoQuotes.min.js" ) ) .pipe( sourcemaps.write( "./" ) ) .pipe( gulp.dest( "lib/" ) ); } );
咱们已经为咱们的旅途开了个好头!!到目前为止咱们已经用Babel 和 webpack命令行工具构建了咱们的项目,而后咱们用gulp(和相关插件)自动化构建打包咱们的项目。这篇文章的代码包含了example/文件夹,在其中有浏览器端和node端的示例。在下一篇文章中,咱们将用 ESLint 和 JSCS 来检查咱们的代码,用 mocha 和 chai 来书写测试,用 Karma 来跑这些测试,用 istanbul 来计量测试的覆盖面。同时,你能够看另外一篇很是棒的文章--Designing Better JavaScript APIs,它能够帮助你写出更好的模块代码。
译自 Writing Next Generation Reusable JavaScript Modules in ECMAScript 6