摘要:我們已經運用了的一些閃亮的新特性,那么如何才能轉化為的代碼呢首先,我們需要通過來安裝在全局安裝會提供我們一個命令行工具。
你是不是也在為可以使用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不感冒,你只是想要在你的頁面上加一個引用并且成功運行?你也會得到一個可用的模塊。Webpack會把我們的代碼打包成UMD( universal module definition)模塊規范,使我們的代碼在任何代碼規范中都可用。
Setting Up Our Project在接下來的幾分鐘,我們將要完成這些代碼。我經常用src/,spec/ 和 lib/文件夾來構建項目。在src/目錄里,你會看到一個有趣的示例模塊,這個模塊是提供樂高電影里的樂高角色的隨機語錄。這個示例會用到ES6的classes,modules,const,destructuring,generator等--這些可以被安全轉化為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.js 和 emmet.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運行時依賴完美。
如果我們想在瀏覽器使用,它看起來會像下面的樣子:
Lego Quote Module Example
你會看到我們已經依賴legoQuotes.js(就在babel的browser-polyfill.js下面),就像其他依賴一樣使用標簽。我們的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.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使我們唯一使用的插件:
// 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
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/78657.html
摘要:前端工程化的演化。前端較為流行的單元測試,等自動化測試自動化測試是軟件通過模擬瀏覽器,對頁面進行操作,判斷是否產生預想的效果。 前端工程化 ??前端工程化的概念在近些年來逐漸成為主流構建大型web應用不可或缺的一部分,在此我通過以下這三方面總結一下自己的理解。 為什么需要前端工程化。 前端工程化的演化。 怎么實現前端工程化。 為什么需要工程化 ??隨著近些年來前端技術的不斷發展,越...
摘要:也是一款優秀的響應式框架站點所使用的一套框架為微信服務量身設計的一套框架一組很小的,響應式的組件,你可以在網頁的項目上到處使用一個可定制的文件,使瀏覽器呈現的所有元素,更一致和符合現代標準。 GitHub 值得收藏的前端項目 整理與收集的一些比較優秀github項目,方便自己閱讀,順便分享出來,大家一起學習,本篇文章會持續更新,版權歸原作者所有。歡迎github star與fork 預...
摘要:轉載來源包管理器管理著庫,并提供讀取和打包它們的工具。能構建更好應用的客戶端包管理器。一個整合和的最佳思想,使開發者能快速方便地組織和編寫前端代碼的下一代包管理器。很棒的組件集合。隱秘地使用和用戶數據。 轉載來源:https://github.com/jobbole/aw... 包管理器管理著 javascript 庫,并提供讀取和打包它們的工具。?npm – npm 是 javasc...
閱讀 3702·2021-11-11 11:00
閱讀 2180·2021-10-08 10:05
閱讀 2671·2021-10-08 10:04
閱讀 3204·2021-09-30 09:48
閱讀 3763·2021-09-27 14:10
閱讀 1704·2021-09-09 09:33
閱讀 2100·2019-08-30 15:55
閱讀 1602·2019-08-30 13:53