美文网首页前端工程化vue
使用模块化工具打包自己开发的JS库(webpack/rollup

使用模块化工具打包自己开发的JS库(webpack/rollup

作者: _双眸 | 来源:发表于2019-04-30 14:01 被阅读0次

    打包JS库demo项目地址:https://github.com/BothEyes1993/bes-jstools

    背景

    最近有个需求,需要为小程序写一个SDK,监控小程序的后台接口调用和页面报错(类似fundebug)

    听起来高大上的SDK,其实就是一个JS文件,类似平时开发中我们引入的第三方库:

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

    小程序的模块化采用了Commonjs规范。也就是说,我需要提供一个monitor.js文件,并且该文件需要支持Commonjs,从而可以在小程序的入口文件app.js中导入:

    // 导入sdk
    const 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 '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的。
    https://www.rollupjs.com/guide/zh#-using-plugins-

    如果你有兴趣,可以看一下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

    原文地址:https://segmentfault.com/a/1190000015221988#articleHeader0


    文章二 使用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'],
            }),
        ],
    }
    

    原文地址:https://www.jianshu.com/p/6a7413481bd2


    附一份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',                  // 输出 UMD格式,各种模块规范通用
        name: 'ReactRedux',         // 打包后的全局变量,如浏览器端 window.ReactRedux 
        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
    

    相关文章

      网友评论

        本文标题:使用模块化工具打包自己开发的JS库(webpack/rollup

        本文链接:https://www.haomeiwen.com/subject/clxvnqtx.html