你是否是也在爲能夠使用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
咱們將要討論的是書寫客戶端(client-side)ES6 libraries,而不是整個網站或者 app 。(不管是在你的開源項目裏或者是在你工做中的軟件項目,這是能夠在不一樣的項目中可複用的代碼。)」等一下!「,你可能會想:」這個難道不是在瀏覽器支持ES6以後才能實現的嗎?「node
你是對的!然而,咱們利用上面提到的Babel能夠把ES6代碼轉化爲ES5代碼,在大多數狀況下如今就能夠實現咱們的目標。webpack
咱們目標的第二部分是寫一個不管在什麼模塊規範下均可以使用的JS模塊。AMD死忠飯?你會獲得一個可用的模塊。CommonJS 加 browserify 纔是你的最愛?沒問題!你會獲得一個可用的模塊。或者你對AMD和CommonJS不感冒,你只是想要在你的頁面上加一個<script>
引用而且成功運行?你也會獲得一個可用的模塊。Webpack會把咱們的代碼打包成UMD( universal module definition)模塊規範,使咱們的代碼在任何代碼規範中均可用。ios
在接下來的幾分鐘,咱們將要完成這些代碼。我常常用src/,spec/ 和 lib/文件夾來構建項目。在src/目錄裏,你會看到一個有趣的示例模塊,這個模塊是提供樂高電影裏的樂高角色的隨機語錄。這個示例會用到ES6的classes,modules,const,destructuring,generator等--這些能夠被安全轉化爲ES5代碼的新特性。git
這篇文章的主要目的是討論如何利用 Babel 和 Webpack 來編譯和打包 ES6 library。然而我仍是想簡要的介紹咱們的示例代碼以證實咱們切實在用 ES6。es6
Note: 你若是是 ES6 新手,沒必要擔憂。這個示例足夠簡單到大家會看懂。github
在 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/目錄。看一下這些文件能夠告訴咱們兩個事情:
我打賭你已經據說過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是一個簡單而高效的選擇。
###SETTING UP A BUILD TASK
首先,咱們要安裝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