Blog address

Recently there was a requirement to write an SDK for applets to monitor API calls and page errors (similar to fundebug).

The SDK is just a JS file, similar to the third-party library we introduced during development:

const moment = require('moment');
moment().format();
Copy the code

The modularization of small program adopts Commonjs specification. In other words, I need to provide a monitor.js file that supports Commonjs and can be imported in the applets entry file app.js:

/ / import the SDK
const monitor = require('./lib/monitor.js');
monitor.init('API-KEY');

// Normal business logic
App({
    ...
})
Copy the code

So the question is, how do I develop this SDK? (Note: this article does not specifically discuss how to implement monitor applets.)

There are several options: for example, simply write all the logic in a monitor.js file and export it

module.exports = {
    // All kinds of logic
}
Copy the code

However, considering the amount of code, in order to reduce coupling, I prefer to split the code into different modules and finally package all JS files into a monitor.js. Those of you who have used Vue and React development can appreciate the benefits of modular development.

Demo code download

The following directory structure is defined:

The SRC directory holds the source code, and the dist directory packs the final monitor.js

SRC /main.js SDK entry file

import { Engine } from './module/Engine';

let monitor = null;

export default {
    init: function (appid) {
        if(! appid || monitor) {return;
        }
        monitor = newEngine(appid); }}Copy the code

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('Start listening for applets ~~~'); }}Copy the code

src/util/index.js

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

So, how do I package all this JS into a final monitor.js file that will execute correctly?

webpack

The first thing THAT came to my mind was webpack. After all, work is often developed with React, and it was the last thing I used to package projects.

Based on webPack 4.x

npm i webpack webpack-cli --save-dev
Copy the code

Relying on my shallow knowledge of webpack metaphysics, I tearfully wrote down a few lines of configuration: 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',}};Copy the code

When you run Webpack, the package is packaged, but try introducing it into a small application

Applets entry file app.js

var monitor = require('./dist/monitor.js');
Copy the code

The console reported an error…

monitor.js
eval

All we need to do is change the Devtool for the WebPack configuration

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'
};
Copy the code

Source-map mode does not use the eval keyword for debugging, and instead generates an additional monitor.js.map file for debugging

Webpack again, import applets, and the problem is again:

var monitor = require('./dist/monitor.js');
console.log(monitor); / / {}
Copy the code

It prints out an empty object!

src/main.js

import { Engine } from './module/Engine';

let monitor = null;

export default {
    init: function (appid) {
        if(! appid || monitor) {return;
        }
        monitor = newEngine(appid); }}Copy the code

Monitor.js does not export an object with an init method!

We wanted monitor.js to conform to the CommonJS specification, but we didn’t specify that in the configuration, so nothing was exported from the webPack package.

In our normal development, we do not need to export a variable when packaging, as long as the packaged file can be executed immediately on the browser. Look at any Vue or React project and see what the entry file looks like.

main.js

import Vue from 'vue'
import App from './App'

new Vue({
  el: '#app'.components: { App },
  template: '<App/>'
})
Copy the code
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.js';

ReactDOM.render(
    <App />,
  document.getElementById('root')
);
Copy the code

Are there any similar routines that end up executing a method immediately without exporting a variable?

libraryTarget

The libraryTarget is the key, and by setting this property we let WebPack know which specification to use to export a variable

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'
    
};
Copy the code

Commonjs2 is what we want the CommonJS specification to be

Repackage it, and it’ll be right this time

var monitor = require('./dist/monitor.js');
console.log(monitor);
Copy the code

The object we exported is mounted on the default property because when we exported it:

export default {
    init: function (appid) {
        if(! appid || monitor) {return;
        }
        monitor = newEngine(appid); }}Copy the code

Now we are happy to import the SDK

var monitor = require('./dist/monitor.js').default;
monitor.init('45454');
Copy the code

You may have noticed that I did not use Babel when I packaged it, because the applet supports ES6 syntax, so there is no need to go through the process of developing this SDK. If you want to develop a library that is browser-compatible, you can add a Babel-loader

module: {
        rules: [{test: /\.js$/.loader: 'babel-loader'.exclude: /node_modules/}}]Copy the code

Note:

  1. Usually development debugging SDK can be directlywebpack -w
  2. When finally packing, usewebpack -pBe compressed

The full 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' // Applets do not support eval-source-map
};
Copy the code

In fact, using WebPack pure JS class library is very simple, than we usually develop an application, configuration is much less, after all, there is no need to pack CSS, HTML, images, fonts these static resources, also do not need to load on demand.

rollup

The article could have ended here, but when I was investigating how to package modules, I took a look at how Vue and React package code, and it turned out that they both used rollup instead of Webpack.

Rollup is a JavaScript module wrapper that compiles small pieces of code into large, complex pieces of code, such as libraries or applications.

Rollup is used to package libraries.

If you’re interested, take a look at the webpack package of Monitor.js and ask, “What is this mess of code?”

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: {}


// Omit 10,000 lines of code below
Copy the code

Webpack implements its own __webpack_exports__ __webpack_require__ module mechanism

/ * * * / "./src/util/index.js":
/ *! * * * * * * * * * * * * * * * * * * * * * * * * * * *! * \! *** ./src/util/index.js ***! A \ * * * * * * * * * * * * * * * * * * * * * * * * * * * /
/ *! 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); }}/ * * * / })
Copy the code

It wraps each JS file in a function, implementing reference and export between modules.

If you use rollup packaging, you’ll be surprised to find that the readability of packaged code is not in the same league as webPack!

npm install --global rollup
Copy the code

Create a new rollup.config.js file

export default {
  input: './src/main.js'.output: {
    file: './dist/monitor.js'.format: 'cjs'}};Copy the code

Format: CJS specifies that the packaged file complies with the CommonJS specification

Run a rollup – c

An error is reported, saying [!] Error: Could not resolve ‘.. /util’ from src\module\Engine.js

This is because rollup recognizes.. /util/ does not automatically look for the index.js file in util (webpack does that by default), so we need to change it to.. /util/index

Packaged files:

'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('Start listening for applets ~~~'); }}let monitor = null;

var main = {
    init: function (appid) {
        if(! appid || monitor) {return;
        }
        monitor = newEngine(appid); }}module.exports = main;
Copy the code

Super neat!

And when importing, there is no need to write a default attribute webpack package

var monitor = require('./dist/monitor.js').default;
monitor.init('45454');
Copy the code

A rollup packaging

var monitor = require('./dist/monitor.js');
monitor.init('45454');
Copy the code

Similarly, we can roll up-c-W directly during normal development and compress it when we pack it

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

The uglify plugin only supports ES5 compression, and the SDK I developed this time doesn’t need to be converted to ES5, so I need a new plugin

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

Copy the code

Of course, you can also use Babel transcoding

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

.babelrc

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

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()
    ]
};
Copy the code

UMD

The SDK we just packaged doesn’t use any environment-specific apis, which means that this code can actually run on both node and browser sides.

If we want packaged code to be compatible with all platforms, we need to comply with the UMD specification (compatible with 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()
    ]
};
Copy the code

By setting format and name, we can package monitor.js to be compatible with a variety of runtime environments

On the node

var monitor = require('monitor.js');
monitor.init('6666');
Copy the code

On the browser side

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

The principle is actually very simple, you can take a look at the packaged source code, or read an article I wrote before

conclusion

Rollup is usually used to package JS libraries, with smaller code and no redundant code. Rollup supports ES6 modularity only by default. If you want to support Commonjs, you need to download the plugin rollup-plugin-commonjs

Webpack is usually great for packaging an app, so if you need Code Splitting or you have a lot of static resources to work with, consider using WebPack