基于 Webpack 和 ES6 打造 JavaScript 類庫

jopen 8年前發布 | 58K 次閱讀 JavaScript開發 ECMAScript webpack

Two months ago I published a starter pack for React based on  webpack . Today I found out that I need almost the same thing but without the React bit. This simplifies the setup but there are still some tricky parts. So, I made a brand new repository  webpack-library-starter and placed all the stuff that we need for creating a JavaScript library.

兩個月前,我曾發布了一篇基于 webpack 的 React 起步教程。你眼前的這篇文章跟那一篇差不多,只不過不包含 React 那一塊。這篇教程稍微簡單一些,但仍然會有一些棘手的部分。因此,我特意建了一個全新的代碼倉庫  webpack-library-starter ,把創建一個 JavaScript 類庫所需的所有素材都放了進去。

First of all, what I meant by saying “library”

首先,我們說的 “類庫” 是指什么

My definition for library in the context of JavaScript is a piece of code that provides specific functionality. It does one thing and it is doing it well. In the ideal case should not depend on another library or framework. A good example for library is jQuery. React and  Vue.js could be also considered a library.

在 JavaScript 語境中,我對類庫的定義是 “提供了特定功能的一段代段”。一個類庫只做一件事,并且把這件事做好。在理想情況下,它不依賴其它類庫或框架。jQuery 就是一個很好的例子。 React 或者  Vue.js 也可以認為是一個類庫。

The library should:

一個類庫應該:

  • Be available for in-browser use. Understand including the library via  <script>  tag.
  • Be accessible through  npm
  • Be compatible with ES6(ES2015) module system,  commonjs  and  amd  specifications.
  • 可以在瀏覽器環境下使用。也就是說,可以通過  <script>  標簽來引入這個類庫。
  • 可以通過  npm  來安裝。
  • 兼容 ES6(ES2015) 的模塊系統、 CommonJS  和  AMD  模塊規范。

It doesn’t matter what is used for developing the library. What is important is the file that is distributed. It should match the above requirements. I prefer to see libraries written in vanilla JavaScript though. It simply makes the contribution easier.

用什么來開發這個類庫并不重要,重要的是我們最終產出的文件。它只要滿足上述要求就行。盡管如此,我還是比較喜歡用原生 JavaScript 寫成的類庫,因為這樣更方便其它人貢獻代碼。

Directory structure

目錄結構

I choose the following directory structure:

我一般選擇如下的目錄結構:

+-- lib
|   +-- library.js
|   +-- library.min.js
+-- src
|   +-- index.js
+-- test
+-- lib
|  +-- library.js
|  +-- library.min.js
+-- src
|  +-- index.js
+-- test

Where src contains the source files and  lib the final compiled version. This means that the entry point of the library is the file under  lib and not  src .

其中 src 目錄用于存放源碼文件,而  lib 目錄用于存放最終編譯的結果。這意味著類庫的入口文件應該放在  lib 目錄下,而不是  src 目錄下。

The starter

起步動作

I really enjoy the new ES6 specification. The bad thing is that there is some significant tooling around it. Some day we’ll probably write such JavaScript without the need of transpiler but today that’s not the case. Usually we need some sort of Babel integration. Babel can convert our ES6 files to ES5 format but it is not meant to create bundles. Or in other words, if we have the following files:

我確實很喜歡最新的 ES6 規范。但壞消息是它身上綁了一堆的附加工序。也許將來某一天我們可以擺脫轉譯過程,所寫即所得;但現在還不行。通常我們需要用到 Babel 來完成轉譯這件事。Babel 可以把我們的 ES6 文件轉換為 ES5 格式,但它并不打算處理打包事宜。或者換句話說,如果我們有以下文件:

+-- lib
+-- src
    +-- index.js (es6)
    +-- helpers.js (es6)
+-- lib
+-- src
    +-- index.js (es6)
    +-- helpers.js (es6)

And we apply Babel we’ll get:

然后我們用上 Babel,那我們將會得到:

+-- lib
|   +-- index.js (es5)
|   +-- helpers.js (es5)
+-- src
    +-- index.js (es6)
    +-- helpers.js (es6)
+-- lib
|  +-- index.js (es5)
|  +-- helpers.js (es5)
+-- src
    +-- index.js (es6)
    +-- helpers.js (es6)

Or in other words Babel do not resolve the imports/requires. So we need a bundler and as you may guess my choice for that is webpack . What I want to achieve at the end is:

或者再換句話說,Babel 并不解析代碼中的 import 或  require 指令。因此,我們需要一個打包工具,而你應該已經猜到了,我的選擇正是  webpack 。最終我想達到的效果是這樣的:

+-- lib
|   +-- library.js (es5)
|   +-- library.min.js (es5)
+-- src
    +-- index.js (es6)
    +-- helpers.js (es6)
+-- lib
|  +-- library.js (es5)
|  +-- library.min.js (es5)
+-- src
    +-- index.js (es6)
    +-- helpers.js (es6)

npm commands

npm 命令

npm provides a nice mechanism for running tasks - scripts . There should be at least three of those registered:

在運行任務方面,npm 提供了一套不錯的機制—— scripts (腳本)。我們至少需要注冊以下三個腳本:

"scripts": {
  "build": "...",
  "dev": "...",
  "test": "..."
}
"scripts": {
  "build": "...",
  "dev": "...",
  "test": "..."
}
  • npm run build  - this should produce a final minified version of our library
  • npm run dev  - the same as  build  but do not minify the result and keeps working in a watching mode
  • npm run test  - runs the tests
  • npm run build  - 這個腳本用來生成這個類庫的最終壓縮版文件。
  • npm run dev  - 跟  build  類似,但它并不壓縮代碼;此外還需要啟動一個監視進程。
  • npm run test  - 用來運行測試。

Building the development version

構建開發版本

npm run dev should fire webpack and should produce  lib/library.js file. We start from the webpack’s configuration file:

npm run dev 需要調用 webpack 并生成  lib/library.js 文件。我們從 webpack 的配置文件開始著手:

JavaScript

// webpack.config.js 
var webpack = require('webpack');
var path = require('path');
var libraryName = 'library';
var outputFile = libraryName + '.js';

var config = {
  entry: __dirname + '/src/index.js',
  devtool: 'source-map',
  output: {
    path: __dirname + '/lib',
    filename: outputFile,
    library: libraryName,
    libraryTarget: 'umd',
    umdNamedDefine: true
  },
  module: {
    loaders: [
      {
        test: /(\.jsx|\.js)$/,
        loader: 'babel',
        exclude: /(node_modules|bower_components)/
      },
      {
        test: /(\.jsx|\.js)$/,
        loader: "eslint-loader",
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    root: path.resolve('./src'),
    extensions: ['', '.js']
  }
};

module.exports = config;
// webpack.config.js
var webpack = require('webpack');
var path = require('path');
var libraryName = 'library';
var outputFile = libraryName + '.js';
 
var config = {
  entry: __dirname + '/src/index.js',
  devtool: 'source-map',
  output: {
    path: __dirname + '/lib',
    filename: outputFile,
    library: libraryName,
    libraryTarget: 'umd',
    umdNamedDefine: true
  },
  module: {
    loaders: [
      {
        test: /(\.jsx|\.js)$/,
        loader: 'babel',
        exclude: /(node_modules|bower_components)/
      },
      {
        test: /(\.jsx|\.js)$/,
        loader: "eslint-loader",
        exclude: /node_modules/
      }
    ]
  },
  resolve: {
    root: path.resolve('./src'),
    extensions: ['', '.js']
  }
};
 
module.exports = config;

Even if you don’t have experience with webpack you may say what is this config file doing. We define the input ( entry ) and the output ( output ) of the compilation. The  module property says what should be applied against every file during processing. In our case this is babel and  ESLint where ESLint is a used for checking the syntax and correctness of our code.

即使你還沒有使用 webpack 的經驗,你或許也可以看明白這個配置文件做了些什么。我們定義了這個編譯過程的輸入( entry )和輸出( output )。那個  module 屬性指定了每個文件在處理過程中將被哪些模塊處理。在我們的這個例子中,需要用到 Babel 和  ESLint ,其中 ESLint 用來校驗代碼的語法和正確性。

There is one tricky part where I spent couple of ours. It’s related to library ,  libraryTarget and umdNamedDefine properties. First I tried without using them and the output of the library was something like this:

這里有一個坑,花了我不少的時間。這個坑是關于 library 、 libraryTarget 和  umdNamedDefine 屬性的。最開始我沒有把它們寫到配置中,結果編譯結果就成了下面這個樣子:

JavaScript

(function(modules) {
  var installedModules = {};

  function __webpack_require__(moduleId) {
    if(installedModules[moduleId]) return installedModules[moduleId].exports;

    var module = installedModules[moduleId] = {
      exports: {},
      id: moduleId,
      loaded: false
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    module.loaded = true;
    return module.exports;
  }

  __webpack_require__.m = modules;
  __webpack_require__.c = installedModules;
  __webpack_require__.p = "";

  return __webpack_require__(0);
})([
  function(module, exports) {
    // ... my code here
  }
]);
(function(modules) {
  var installedModules = {};
 
  function __webpack_require__(moduleId) {
    if(installedModules[moduleId]) return installedModules[moduleId].exports;
 
    var module = installedModules[moduleId] = {
      exports: {},
      id: moduleId,
      loaded: false
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    module.loaded = true;
    return module.exports;
  }
 
  __webpack_require__.m = modules;
  __webpack_require__.c = installedModules;
  __webpack_require__.p = "";
 
  return __webpack_require__(0);
})([
  function(module, exports) {
    // ... my code here
  }
]);

This is how every webpack compiled code looks like. It uses similar approach like browserify . There is a self-invoking function which receives all the modules used in our application. Every of them stays behind an index of the  modules array. In the code above we have only one and  __webpack_require__(0) effectively runs the code in our  src/index.js file.

經過 webpack 編譯之后的文件差不多都是這個樣子。它采用的方式跟 Browserify 很類似。編譯結果是一個自調用的函數,它會接收應用程序中所用到的所有模塊。每個模塊都被存放到到  modules 數組中。上面這段代碼只包含了一個模塊,而  __webpack_require__(0) 實際上相當于運行  src/index.js 文件中的代碼。

Having a bundle like this one do not fulfill all the requirements mentioned in the beginning of this article because we do not export anything. The file is meant to be dropped in a web page. However, adding library ,  libraryTarget and  umdNamedDefine makes webpack injecting a really nice snippet at the top:

光是得到這樣一個打包文件,并沒有滿足我們在文章開頭所提到的所有需求,因為我們還沒有導出任何東西。這個文件的運行結果在網頁中必定會被丟棄。不過,如果我們加上 library 、 libraryTarget 和 umdNamedDefine ,就可以讓 webpack 在文件頂部注入一小段非常漂亮的代碼片斷:

JavaScript

(function webpackUniversalModuleDefinition(root, factory) {
  if(typeof exports === 'object' && typeof module === 'object')
    module.exports = factory();
  else if(typeof define === 'function' && define.amd)
    define("library", [], factory);
  else if(typeof exports === 'object')
    exports["library"] = factory();
  else
    root["library"] = factory();
})(this, function() {
return (function(modules) {
 ...
 ...
(function webpackUniversalModuleDefinition(root, factory) {
  if(typeof exports === 'object' && typeof module === 'object')
    module.exports = factory();
  else if(typeof define === 'function' && define.amd)
    define("library", [], factory);
  else if(typeof exports === 'object')
    exports["library"] = factory();
  else
    root["library"] = factory();
})(this, function() {
return (function(modules) {
 ...
 ...

Setting libraryTarget to  umd means using  universal module definition for the final result. And indeed, this piece of code recognizes the environment and provides a proper bootstrapping mechanism for our library.

把 libraryTarget 設定為  umd 表示采用  通用模塊定義 來生成最終結果。而且這段代碼確實可以識別不同的運行環境,并為我們的類庫提供一個妥當的初始化機制。

Building production version

構建生產環境所需的版本

The only one difference between development and production mode for webpack is the minification. Running npm run build should produce a minified version -  library.min.js . webpack has a nice build-in plugin for that:

對 webpack 來說,開發階段與生產階段之間唯一的區別在于壓縮。運行 npm run build 應該生成一個壓縮版—— library.min.js 。webpack 有一個不錯的內置插件可以做到這一點:

JavaScript

// webpack.config.js 
...
var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin;
var env = process.env.WEBPACK_ENV;

var libraryName = 'library';
var plugins = [], outputFile;

if (env === 'build') {
  plugins.push(new UglifyJsPlugin({ minimize: true }));
  outputFile = libraryName + '.min.js';
} else {
  outputFile = libraryName + '.js';
}

var config = {
  entry: __dirname + '/src/index.js',
  devtool: 'source-map',
  output: { ... },
  module: { ... },
  resolve: { ... },
  plugins: plugins
};

module.exports = config;
// webpack.config.js
...
var UglifyJsPlugin = webpack.optimize.UglifyJsPlugin;
var env = process.env.WEBPACK_ENV;
 
var libraryName = 'library';
var plugins = [], outputFile;
 
if (env === 'build') {
  plugins.push(new UglifyJsPlugin({ minimize: true }));
  outputFile = libraryName + '.min.js';
} else {
  outputFile = libraryName + '.js';
}
 
var config = {
  entry: __dirname + '/src/index.js',
  devtool: 'source-map',
  output: { ... },
  module: { ... },
  resolve: { ... },
  plugins: plugins
};
 
module.exports = config;

UglifyJsPlugin does the job if we add it to the  plugins array. There is something else that we have to clarify. We need some conditional logic where we instruct webpack what kind of bundle to produce (production or development). One of the popular approaches is to define an environment variable and pass it from the command line. For example:

只要我們把 UglifyJsPlugin 加入到  plugins 數組中,它就可以完成這個任務。此外,還一些事情有待明確。我們還需要某種條件判斷邏輯,來告訴 webpack 需要生成哪一種類型(“開發階段” 還是 “生產階段”)的打包文件。一個常見的做法是定義一個環境變量,并將它通過命令行傳進去。比如這樣:

JavaScript

// package.json 
"scripts": {
  "build": "WEBPACK_ENV=build webpack",
  "dev": "WEBPACK_ENV=dev webpack --progress --colors --watch"
}
// package.json
"scripts": {
  "build": "WEBPACK_ENV=build webpack",
  "dev": "WEBPACK_ENV=dev webpack --progress --colors --watch"
}

(Notice the --watch option. It makes webpack continuously running and watching for changes)

(請留意 --watch 選項。它會讓 webpack 監視文件變化并持續運行構建任務。)

Testing

測試

I’m usually using Mocha and  Chai for testing and that’s what I added in the starter. Again there was a tricky part making Mocha understands ES6 files but thankfully to Babel the problem was resolved.

我通常采用 Mocha 和  Chai 來運行測試——測試環節是這篇起步教程特有的內容。這里同樣存在一個棘手的問題,就是如何讓 Mocha 正確識別用 ES6 寫的測試文件。不過謝天謝地,Babel 再次解決了這個問題。

JavaScript

// package.json
"scripts": {
  ...
  "test": "mocha --compilers js:babel-core/register --colors -w ./test/*.spec.js"
}
// package.json
"scripts": {
  ...
  "test": "mocha --compilers js:babel-core/register --colors -w ./test/*.spec.js"
}

The important bit is the --compilers option. It allows us to process the incoming file before running it.

這里最關鍵的部分在于 --compilers 這個選項。它允許我們在運行測試文件之前預先處理這個文件。

A few other configuration files

其它配置文件

Babel received some major changes in the newest version 6. We now have something called presets where we describe what kind of transformation we want. One of the easiest ways to configure that is with a  .babelrc file:

在最新的 6.x 版本中,Babel 發生了一些重大的變化。現在,在指定哪些代碼轉換器將被啟用時,我們需要面對一種叫作 presets 的東西。最簡單配置的方法就是寫一個  .babelrc 文件:

JavaScript

// .babelrc {
  "presets": ["es2015"],
  "plugins": ["babel-plugin-add-module-exports"]
}
// .babelrc {
  "presets": ["es2015"],
  "plugins": ["babel-plugin-add-module-exports"]
}

ESLint provides the same thing and we have .eslintrc :

ESLint 也需要一個類似的配置文件,叫作 .eslintrc :

// .eslintrc {
  "ecmaFeatures": {
    "globalReturn": true,
    "jsx": true,
    "modules": true
  },
  "env": {
    "browser": true,
    "es6": true,
    "node": true
  },
  "globals": {
    "document": false,
    "escape": false,
    "navigator": false,
    "unescape": false,
    "window": false,
    "describe": true,
    "before": true,
    "it": true,
    "expect": true,
    "sinon": true
  },
  "parser": "babel-eslint",
  "plugins": [],
  "rules": {
    // ... lots of lots of rules here
  }
}
// .eslintrc {
  "ecmaFeatures": {
    "globalReturn": true,
    "jsx": true,
    "modules": true
  },
  "env": {
    "browser": true,
    "es6": true,
    "node": true
  },
  "globals": {
    "document": false,
    "escape": false,
    "navigator": false,
    "unescape": false,
    "window": false,
    "describe": true,
    "before": true,
    "it": true,
    "expect": true,
    "sinon": true
  },
  "parser": "babel-eslint",
  "plugins": [],
  "rules": {
    // ... lots of lots of rules here
  }
}

Links

相關鏈接

The starter is available in GitHub here github.com/krasimir/webpack-library-starter .

這篇起步教程還可以在 GitHub 上找到: github.com/krasimir/webpack-library-starter

Used tools:

用到的項目如下:

Dependencies:

具體依賴如下:

// package.json
"devDependencies": {
  "babel": "6.3.13",
  "babel-core": "6.1.18",
  "babel-eslint": "4.1.3",
  "babel-loader": "6.1.0",
  "babel-plugin-add-module-exports": "0.1.2",
  "babel-preset-es2015": "6.3.13",
  "chai": "3.4.1",
  "eslint": "1.7.2",
  "eslint-loader": "1.1.0",
  "mocha": "2.3.4",
  "webpack": "1.12.9"
}
// package.json
"devDependencies": {
  "babel": "6.3.13",
  "babel-core": "6.1.18",
  "babel-eslint": "4.1.3",
  "babel-loader": "6.1.0",
  "babel-plugin-add-module-exports": "0.1.2",
  "babel-preset-es2015": "6.3.13",
  "chai": "3.4.1",
  "eslint": "1.7.2",
  "eslint-loader": "1.1.0",
  "mocha": "2.3.4",
  "webpack": "1.12.9"
}

譯注

是不是意猶未盡?其實準確來說,這篇文章是作者對 webpack-library-starter 項目的一個簡要解說,講解了代碼之外的背景知識。

因此,作為學習者,光讀文章是遠遠不夠的,我們真正需要的是研讀這個項目提供的源碼,并且動手實際操作和演練,如此方能掌握要領。加油!

來自: http://web.jobbole.com/84858/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!