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

你是不是是也在为能够运用ES6的新特征而愉快,却不太肯定应当从哪最先,或许怎样最先?不止你一个人如许!我已花了一年半的时刻去处理这个幸运的困难。在这段时刻里 JavaScript 东西链中有几个令人愉快的打破。

这些打破让我们能够用ES6誊写完整的JS模块,而不会为了一些基础的前提而让步,比方testing,linting 和(最重要的)其他人能够随意马虎明白我们所写的代码。

《[译] 用 ES6 构建新一代可复用 JS 模块》

在这篇文章中,我们集合精力在怎样用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 模块中,我们能够看到以下代码(检察解释相识更多):

// 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.jsemmet.js 都是离开的零丁的文件–这是我们示例代码中的典范例子。跟你之前写的 JavaScript 代码比拟,我们的示例代码能够比较生疏。然则,在我们完成我们一系列的事变以后,我们将会获得一个 将这些代码打包到一同的‘built’版本。

The index.js

我们项目中的另一个文件– 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代码后依旧会有一样的作用。

OK. Now What?

我们已运用了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 )范例打包,而且能够在浏览器环境中运用。

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)):

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
            }
         }
      ]
   }
};

让我们来看一些症结的设置信息。

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我们的代码,而且在敕令行里输出以下的内容:

» 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。

What Happened, Exactly?

当我们在敕令行里运转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;
})();

It’s Usable!

这时候我们就能够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.

Moving To Gulp

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
            }
         }
      ]
   }
};

WHAT ABOUT MINIFYING?

我很愉快你问了这个题目!我们用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.jslegoQuotes.min.jslegoQuotes.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使我们唯一运用的插件:

// 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/" ) );
} );

What’s Next?

我们已为我们的旅途开了个好头!!到现在为止我们已用Babel 和 webpack敕令行东西构建了我们的项目,然后我们用gulp(和相干插件)自动化构建打包我们的项目。这篇文章的代码包含了example/文件夹,在其中有浏览器端和node端的示例。鄙人一篇文章中,我们将用 ESLint 和 JSCS 来搜检我们的代码,用 mocha 和 chai 来誊写测试,用 Karma 来跑这些测试,用 istanbul 来计量测试的掩盖面。同时,你能够看另一篇异常棒的文章–Designing Better JavaScript APIs,它能够协助你写出更好的模块代码。

译自 Writing Next Generation Reusable JavaScript Modules in ECMAScript 6

    原文作者:MrDream
    原文地址: https://segmentfault.com/a/1190000004419412
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞