使用模塊化工具打包自己開發的JS庫

使用模塊化工具打包自己開發的JS庫

博客原文

最近有個需求,需要為小程序寫一個SDK,監控小程序的後台介面調用和頁面報錯(類似fundebug)

聽起來高大上的SDK,其實就是一個JS文件,類似平時開發中我們引入的第三方庫:

const moment = require(moment);moment().format();

小程序的模塊化採用了Commonjs規範。也就是說,我需要提供一個monitor.js文件,並且該文件需要支持Commonjs,從而可以在小程序的入口文件app.js中導入:

// 導入sdkconst monitor = require(./lib/monitor.js);monitor.init(API-KEY);// 正常業務邏輯App({ ...})

所以問題來了,我應該怎麼開發這個SDK? (注意:本文並不具體討論怎麼實現監控小程序)

方案有很多種:比如直接把所有的邏輯寫在一個monitor.js文件里,然後導出

module.exports = { // 各種邏輯}

但是考慮到代碼量,為了降低耦合度,我還是傾向於把代碼拆分成不同模塊,最後把所有JS文件打包成一個monitor.js。平時有使用過Vue和React開發的同學,應該能體會到模塊化開發的好處。

演示代碼下載

如下是定義的目錄結構:

src目錄下存放源代碼,dist目錄打包最後的monitor.js

src/main.js SDK入口文件

import { Engine } from ./module/Engine;let monitor = null;export default { init: function (appid) { if (!appid || monitor) { return; } monitor = new Engine(appid); }}

src/module/Engine.js

import { util } from ../util/;export class Engine { constructor(appid) { this.id = util.generateId(); this.appid = appid; this.init(); } init() { console.log(開始監聽小程序啦~~~); }}

src/util/index.js

export const util = { generateId() { return Math.random().toString(36).substr(2); }}

所以,怎麼把這堆js打包成最後的monitor.js文件,並且程序可以正確執行?

webpack

我第一個想到的就是用webpack打包,畢竟工作經常用React開發,最後打包項目用的就是它。

基於webpack4.x版本

npm i webpack webpack-cli --save-dev

靠著我對於webpack玄學的微薄知識,含淚寫下了幾行配置:

webpack.config.js

var path = require(path);var webpack = require(webpack);module.exports = { mode: development, entry: ./src/main.js, output: { path: path.resolve(__dirname, ./dist), publicPath: /dist/, filename: monitor.js, }};

運行webpack,打包倒是打包出來了,但是引入到小程序里試試

小程序入口文件app.js

var monitor = require(./dist/monitor.js);

控制台直接報錯。。。

原因很簡單:打包出來的monitor.js使用了eval關鍵字,而小程序內部並支持eval。

我們只需要更改webpack配置的devtool即可

var path = require(path);var webpack = require(webpack);module.exports = { mode: development, entry: ./src/main.js, output: { path: path.resolve(__dirname, ./dist), publicPath: /dist/, filename: monitor.js, }, devtool: source-map};

source-map模式就不會使用eval關鍵字來方便debug,它會多生成一個monitor.js.map文件來方便debug

再次webpack打包,然後導入小程序,問題又來了:

var monitor = require(./dist/monitor.js);console.log(monitor); // {}

列印出來的是一個空對象!

src/main.js

import { Engine } from ./module/Engine;let monitor = null;export default { init: function (appid) { if (!appid || monitor) { return; } monitor = new Engine(appid); }}

monitor.js並沒有導出一個含有init方法的對象!

我們期望的是monitor.js符合commonjs規範,但是我們在配置中並沒有指出,所以webpack打包出來的文件,什麼也沒導出。

我們平時開發中,打包時也不需要導出一個變數,只要打包的文件能在瀏覽器上立即執行即可。你隨便翻一個Vue或React的項目,看看入口文件是咋寫的?

main.js

import Vue from vueimport App from ./Appnew Vue({ el: #app, components: { App }, template: <App/>})

import React from react;import ReactDOM from react-dom;import App from ./App.js;ReactDOM.render( <App />, document.getElementById(root));

是不是都類似這樣的套路,最後只是立即執行一個方法而已,並沒有導出一個變數。

libraryTarget

libraryTarget就是問題的關鍵,通過設置該屬性,我們可以讓webpack知道使用何種規範導出一個變數

var path = require(path);var webpack = require(webpack);module.exports = { mode: development, entry: ./src/main.js, output: { path: path.resolve(__dirname, ./dist), publicPath: /dist/, filename: monitor.js, libraryTarget: commonjs2 }, devtool: source-map };

commonjs2就是我們希望的commonjs規範

重新打包,這次就正確了

var monitor = require(./dist/monitor.js);console.log(monitor);

我們導出的對象掛載到了default屬性上,因為我們當初導出時:

export default { init: function (appid) { if (!appid || monitor) { return; } monitor = new Engine(appid); }}

現在,我們可以愉快的導入SDK

var monitor = require(./dist/monitor.js).default;monitor.init(45454);

你可能注意到,我打包時並沒有使用babel,因為小程序是支持es6語法的,所以開發該sdk時無需再轉一遍,如果你開發的類庫需要兼容瀏覽器,則可以加一個babel-loader

module: { rules: [ { test: /.js$/, loader: babel-loader, exclude: /node_modules/ } ] }

注意點:

  1. 平時開發調試sdk時可以直接webpack -w
  2. 最後打包時,使用webpack -p進行壓縮

完整的webpack.config.js

var path = require(path);var webpack = require(webpack);module.exports = { mode: development, // production entry: ./src/main.js, output: { path: path.resolve(__dirname, ./dist), publicPath: /dist/, filename: monitor.js, libraryTarget: commonjs2 }, module: { rules: [ { test: /.js$/, loader: babel-loader, exclude: /node_modules/ } ] }, devtool: source-map // 小程序不支持eval-source-map};

其實,使用webpack打包純JS類庫是很簡單的,比我們平時開發一個應用,配置少了很多,畢竟不需要打包css,html,圖片,字體這些靜態資源,也不用按需載入。

rollup

文章寫到這裡本來可以結束了,但是在前期調研如何打包模塊的時候,我特意看了下Vue和React是怎麼打包代碼的,結果發現,這倆都沒使用webpack,而是使用了rollup。

Rollup 是一個 JavaScript 模塊打包器,可以將小塊代碼編譯成大塊複雜的代碼,例如 library 或應用程序。

Rollup官網的這段介紹,正說明了rollup就是用來打包library的。

如果你有興趣,可以看一下webpack打包後的monitor.js,絕對會吐槽,這一坨代碼是啥東西?

module.exports =/******/ (function(modules) { // webpackBootstrap/******/ // The module cache/******/ var installedModules = {};/******//******/ // The require function/******/ function __webpack_require__(moduleId) {/******//******/ // Check if module is in cache/******/ if(installedModules[moduleId]) {/******/ return installedModules[moduleId].exports;/******/ }/******/ // Create a new module (and put it into the cache)/******/ var module = installedModules[moduleId] = {/******/ i: moduleId,/******/ l: false,/******/ exports: {}// 以下省略1萬行代碼

webpack自己實現了一套__webpack_exports__ , __webpack_require__ , module機制

/***/ "./src/util/index.js":/*!***************************!* !*** ./src/util/index.js ***! ***************************//*! exports provided: util *//***/ (function(module, __webpack_exports__, __webpack_require__) {"use strict";__webpack_require__.r(__webpack_exports__);/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "util", function() { return util; });const util = { generateId() { return Math.random().toString(36).substr(2); }}/***/ })

它把每個js文件包裹在一個函數里,實現模塊間的引用和導出。

如果使用rollup打包,你就會驚訝的發現,打包後的代碼可讀性簡直和webpack不是一個級別!

npm install --global rollup

新建一個rollup.config.js

export default { input: ./src/main.js, output: { file: ./dist/monitor.js, format: cjs }};

format: cjs指定打包後的文件符合commonjs規範

運行rollup -c

這時會報錯,說[!] Error: Could not resolve ../util from srcmoduleEngine.js

這是因為,rollup識別../util/時,並不會自動去查找util目錄下的index.js文件(webpack默認會去查找),所以我們需要改成../util/index

打包後的文件:

use strict;const util = { generateId() { return Math.random().toString(36).substr(2); }};class Engine { constructor(appid) { this.id = util.generateId(); this.appid = appid; this.init(); } init() { console.log(開始監聽小程序啦~~~); }}let monitor = null;var main = { init: function (appid) { if (!appid || monitor) { return; } monitor = new Engine(appid); }}module.exports = main;

是不是超簡潔!

而且導入的時候,無需再寫個default屬性

webpack打包

var monitor = require(./dist/monitor.js).default;monitor.init(45454);

rollup打包

var monitor = require(./dist/monitor.js);monitor.init(45454);

同樣,平時開發時我們可以直接rollup -c -w,最後打包時,也要進行壓縮

npm i rollup-plugin-uglify -D

import { uglify } from rollup-plugin-uglify;export default { input: ./src/main.js, output: { file: ./dist/monitor.js, format: cjs }, plugins: [ uglify() ]};

然而這樣運行會報錯,因為uglify插件只支持es5的壓縮,而我這次開發的sdk並不需要轉成es5,所以換一個插件

npm i rollup-plugin-terser -D

import { terser } from rollup-plugin-terser;export default { input: ./src/main.js, output: { file: ./dist/monitor.js, format: cjs }, plugins: [ terser() ]};

當然,你也可以使用babel轉碼

npm i rollup-plugin-terser babel-core babel-preset-latest babel-plugin-external-helpers -D

.babelrc

{ "presets": [ ["latest", { "es2015": { "modules": false } }] ], "plugins": ["external-helpers"]}

rollup.config.js

import { terser } from rollup-plugin-terser;import babel from rollup-plugin-babel;export default { input: ./src/main.js, output: { file: ./dist/monitor.js, format: cjs }, plugins: [ babel({ exclude: node_modules/** }), terser() ]};

UMD

我們剛剛打包的SDK,並沒有用到特定環境的API,也就是說,這段代碼,其實完全可以運行在node端和瀏覽器端。

如果我們希望打包的代碼可以兼容各個平台,就需要符合UMD規範(兼容AMD,CMD, Commonjs, iife)

import { terser } from rollup-plugin-terser;import babel from rollup-plugin-babel;export default { input: ./src/main.js, output: { file: ./dist/monitor.js, format: umd, name: monitor }, plugins: [ babel({ exclude: node_modules/** }), terser() ]};

通過設置formatname,這樣我們打包出來的monitor.js就可以兼容各種運行環境了

在node端

var monitor = require(monitor.js);monitor.init(6666);

在瀏覽器端

<script src="./monitor.js"></srcipt><script> monitor.init(6666);</srcipt>

原理其實也很簡單,你可以看下打包後的源碼,或者看我之前寫過的一篇文章

總結

rollup通常適用於打包JS類庫,通過rollup打包後的代碼,體積較小,而且沒有冗餘的代碼。rollup默認只支持ES6的模塊化,如果需要支持Commonjs,還需下載相應的插件rollup-plugin-commonjs

webpack通常適用於打包一個應用,如果你需要代碼拆分(Code Splitting)或者你有很多靜態資源需要處理,那麼可以考慮使用webpack

推薦閱讀:

如何評價0配置的web打包器parcel?

TAG:前端開發 | webpack | rollup |