使用模塊化工具打包本身開發的JS庫(webpack/rollup)對比總結

打包JS庫demo項目地址:github.com/BothEyes199…javascript

背景

最近有個需求,須要爲小程序寫一個SDK,監控小程序的後臺接口調用和頁面報錯(相似fundebug)css

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

const moment = require('moment');
moment().format();
複製代碼

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

// 導入sdk
const monitor = require('./lib/monitor.js');
monitor.init('API-KEY');

// 正常業務邏輯
App({
    ...
})
複製代碼

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

方案有不少種:好比直接把全部的邏輯寫在一個monitor.js文件裏,而後導出node

module.exports = {
    // 各類邏輯
}
複製代碼

可是考慮到代碼量,爲了下降耦合度,我仍是傾向於把代碼拆分紅不一樣模塊,最後把全部JS文件打包成一個monitor.js。平時有使用過Vue和React開發的同窗,應該能體會到模塊化開發的好處。react

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

src/main.js SDK入口文件git

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文件,而且程序能夠正確執行?es6


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 'vue'
import App from './App'

new 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的。 www.rollupjs.com/guide/zh#-u…

若是你有興趣,能夠看一下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 src\module\Engine.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()
  ]
};
複製代碼

固然,你也可使用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()
    ]
};
複製代碼

經過設置format和name,這樣咱們打包出來的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

原文地址:segmentfault.com/a/119000001…


文章二 使用Rollup打包JavaScript

rollup是一款小巧的javascript模塊打包工具,更適合於庫應用的構建工具;能夠將小塊代碼編譯成大塊複雜的代碼,基於ES6 modules,它可讓你的 bundle 最小化,有效減小文件請求大小,vue在開發的時候用的是webpack,可是最後將文件打包在一塊兒的時候用的是 rollup.js

全局安裝

npm install --global rollup
複製代碼

開始使用rollup

建立第一個bundle 建立main.js

console.log(111);
複製代碼

執行 rollup --input main.js --output bundle.js --format cjs, 該命令編譯 main.js 生成 bundle.js, --format cjs 意味着打包爲 node.js 環境代碼, 請觀察 bundle.js 文件內容

'use strict'
console.log(111);
複製代碼

命令行參數簡介:

輸入(input -i/--input) String 這個包的入口點 (例如:你的 main.js 或者 app.js 或者 index.js) 文件(file -o/--output.file) String 要寫入的文件。也可用於生成 sourcemaps,若是適用 格式(format -f/--output.format) 關於format選項 rollup提供了五種選項:

1,amd – 異步模塊定義,用於像RequireJS這樣的模塊加載器

2,cjs – CommonJS,適用於 Node 和 Browserify/Webpack

3,es – 將軟件包保存爲ES模塊文件

4,iife – 一個自動執行的功能,適合做爲<script>標籤。(若是要爲應用程序建立一個捆綁包,您可能想要使用它,由於它會使文件大小變小。)

5,umd – 通用模塊定義,以amdcjsiife 爲一體

使用配置文件

rollup.config.js

export default {
    input: 'src/main.js',
    output: {
        file: 'bundle.js',
        format: 'cjs'
    }
};
複製代碼

執行 rollup -c rollup.config.js啓動配置項;

rollup 提供了 --watch / -w 參數來監聽文件改動並自動從新打包

使用rollup插件
npm install --save-dev rollup-plugin-json
複製代碼

咱們用的是 --save-dev 而不是 --save,由於代碼實際執行時不依賴這個插件——只是在打包時使用。

在配置文件中啓用插件

import json from 'rollup-plugin-json';
export default {
    input: './main.js',
    output: {
        file: 'bundle.js',
        format: 'umd'
    },
    plugins: [
        json(),
    ],
}
複製代碼

新建文件 data.json

{
    "name": "xiaoming",
    "age": 12
}
複製代碼

main.js 引入 data.json

import { name } from './data.json';
console.log(name);
複製代碼

執行 rollup -c rollup.config.js,並查看 bundle.js

(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? factory() :
    typeof define === 'function' && define.amd ? define(factory) :
    (factory());
}(this, (function () { 'use strict';

var name = "xiaoming";

console.log(name);

})));
複製代碼

看到bundle中僅引用了data.json中的name字段,這是由於rollup會自動進行 Tree-shaking,main.js中僅引入了name,age並無沒引用,因此age並不會被打包

rollup基礎插件

rollup-plugin-alias: 提供modules名稱的 alias 和reslove 功能

rollup-plugin-babel: 提供babel能力

rollup-plugin-eslint: 提供eslint能力

rollup-plugin-node-resolve: 解析 node_modules 中的模塊

rollup-plugin-commonjs: 轉換 CJS -> ESM, 一般配合上面一個插件使用

rollup-plugin-serve: 類比 webpack-dev-server, 提供靜態服務器能力

rollup-plugin-filesize: 顯示 bundle 文件大小

rollup-plugin-uglify: 壓縮 bundle 文件

rollup-plugin-replace: 類比 Webpack 的 DefinePlugin , 可在源碼中經過 process.env.NODE_ENV 用於構建區分 Development 與 Production 環境.

rollup於其餘工具集成

打包npm 模塊

webpackBrowserify不一樣, rollup 不會去尋找從npm安裝到你的node_modules文件夾中的軟件包; rollup-plugin-node-resolve 插件能夠告訴 Rollup 如何查找外部模塊

npm install --save-dev rollup-plugin-node-resolve
複製代碼

打包 commonjs模塊

npm中的大多數包都是以CommonJS模塊的形式出現的。 在它們更改以前,咱們須要將CommonJS模塊轉換爲 ES2015 供 Rollup 處理。 rollup-plugin-commonjs 插件就是用來將 CommonJS 轉換成 ES2015 模塊的。 請注意,rollup-plugin-commonjs應該用在其餘插件轉換你的模塊以前 - 這是爲了防止其餘插件的改變破壞CommonJS的檢測

npm install --save-dev rollup-plugin-commonjs
複製代碼

使用babel

使用 Babel 和 Rollup 的最簡單方法是使用 rollup-plugin-babel

npm install --save-dev rollup-plugin-babel
複製代碼

新建.babelrc

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

1,首先,咱們設置"modules": false,不然 Babel 會在 Rollup 有機會作處理以前,將咱們的模塊轉成 CommonJS,致使 Rollup 的一些處理失敗

2,咱們使用external-helpers插件,它容許 Rollup 在包的頂部只引用一次 「helpers」,而不是每一個使用它們的模塊中都引用一遍(這是默認行爲) 運行 rollup以前, 須要安裝latest preset 和external-helpers插件

npm i -D babel-preset-latest babel-plugin-external-helpers
複製代碼

一個簡單的配置項

import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import babel from 'rollup-plugin-babel';
import json from 'rollup-plugin-json';
export default {
    input: './main.js',
    output: {
        file: 'bundle.js',
        format: 'umd'
    },
    watch: {
        exclude: 'node_modules/**'
    },
    plugins: [
        resolve(),
        commonjs(),
        json(),
        babel({
            exclude: 'node_modules/**',
            plugins: ['external-helpers'],
        }),
    ],
}
複製代碼

原文地址:www.jianshu.com/p/6a7413481…


附一份react-redux開源項目的rollup配置文件

import nodeResolve from 'rollup-plugin-node-resolve'     // 幫助尋找node_modules裏的包
import babel from 'rollup-plugin-babel'                             // rollup 的 babel 插件,ES6轉ES5
import replace from 'rollup-plugin-replace'                       // 替換待打包文件裏的一些變量,如 process在瀏覽器端是不存在的,須要被替換
import commonjs from 'rollup-plugin-commonjs'              // 將非ES6語法的包轉爲ES6可用
import uglify from 'rollup-plugin-uglify'                              // 壓縮包

const env = process.env.NODE_ENV

const config = {
  input: 'src/index.js',
  external: ['react', 'redux'],                           // 告訴rollup,不打包react,redux;將其視爲外部依賴
  output: { 
    format: 'umd',       &emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp; // 輸出 UMD格式,各類模塊規範通用
    name: 'ReactRedux',&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;// 打包後的全局變量,如瀏覽器端 window.ReactRedux&emsp;
    globals: {
      react: 'React',                                         // 這跟external 是配套使用的,指明global.React便是外部依賴react
      redux: 'Redux'
    }
  },
  plugins: [
    nodeResolve(),
    babel({
      exclude: '**/node_modules/**'
    }),
    replace({
      'process.env.NODE_ENV': JSON.stringify(env)
    }),
    commonjs()
  ]
}

if (env === 'production') {
  config.plugins.push(
    uglify({
      compress: {
        pure_getters: true,
        unsafe: true,
        unsafe_comps: true,
        warnings: false
      }
    })
  )
}

export default config
複製代碼
相關文章
相關標籤/搜索