@Rico
2016-02-17T06:13:25.000000Z
字数 13640
阅读 3299
javascript
你是不是也在为可以使用ES6的新特性而兴奋,却不太确定应该从哪开始,或者如何开始?不止你一个人这样!我已经花了一年半的时间去解决这个幸福的难题。在这段时间里 JavaScript 工具链中有几个令人兴奋的突破。
这些突破让我们可以用ES6书写完全的JS模块,而不会为了一些基本的条件而妥协,比如testing,linting 和(最重要的)其他人可以轻易理解我们所写的代码。
在这篇文章中,我们集中精力在如何用ES6构建JS模块,并且无论你在你的网站或者app中使用CommonJS,AMD(asynchronous module definition)或者普通的网页script引入,这个模块都可以轻易被引用。
在这个系列文章的第一部分和第二部分,我们来看一下这些卓越的工具们。在这篇文章中,我们详细说明如何编写,编译,打包代码;而在第二篇文章会集中在linting,formatting 和 testing(利用 JSCS,ESLint,mocha,Chai,Karma 和 Istanbul)。让我们来看看在这篇文章中涉及到的工具:
WRITE IN ES6, USE IN ES5
我们将要讨论的是书写客户端 ES6 ,而不是整个网站或者 app 。(无论是在你的开源项目里或者是在你工作中的软件项目,这是可以在不同的项目中可复用的代码。)”等一下!“,你可能会想:”这个难道不是在浏览器支持ES6之后才能实现的吗?“
你是对的!然而,我们利用上面提到的Babel可以把ES6代码转化为ES5代码,在大多数情况下现在就可以实现我们的目标。
我们目标的第二部分是写一个无论在什么模块规范下都可以使用的JS模块。AMD死忠饭?你会得到一个可用的模块。CommonJS 加 browserify 才是你的最爱?没问题!你会得到一个可用的模块。或者你对AMD和CommonJS不感冒,你只是想要在你的页面上加一个引用并且成功运行?你也会得到一个可用的模块。Webpack会把我们的代码打包成UMD( universal module definition)模块规范,使我们的代码在任何代码规范中都可用。
在接下来的几分钟,我们将要完成这些 代码。我经常用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.jsimport { 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 valueexport default class LegoCharacter {// We use destructuring to match properties on the object// passed into separate variables for character and actorconstructor( { 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.jsimport LegoCharacter from "./LegoCharacter";// Here we use the extends keyword to make// Emmet inherit from LegoCharacterexport default class Emmet extends LegoCharacter {constructor() {// super lets us call the LegoCharacter's constructorsuper( { 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 callerfunction* randomQuote() {const chars = _.values( characters );const character = chars[ getRandom( 0, chars.length - 1 ) ];yield `${character.name}: ${character.saySomething()}`;}// Using object literal shorthand syntax, FTWexport 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.jsbenny.jsemmet.jsindex.jsutils.jswyldstyle.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/importentry: "./src/index.js",// output tells webpack where to put the bundle it createsoutput: {// 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 modulelibraryTarget: "umd",// the destination file namefilename: "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我们的代码,并且在命令行里输出如下的内容:
» webpackHash: f33a1067ef2c63b81060Version: webpack 1.12.1Time: 758msAsset Size Chunks Chunk Nameslib/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"));elseroot["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 179var 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.jsvar 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.jsmodule.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.jsvar 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.1Asset Size Chunks Chunk NameslegoQuotes.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.jsimport 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.jsrequire("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 outputpreserveComments: "license",compress: {// just a personal preference of minenegate_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
原文 https://segmentfault.com/a/1190000004419412#articleHeader11