打包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)对比总结的更多相关文章

  1. Zepto——简化版jQuery,移动端首选js库

    转载请注明原文地址:https://www.cnblogs.com/ygj0930/p/10826054.html 一:Zepto是什么 Zepto最初是为移动端开发的js库,是jQuery的轻量级替 ...

  2. JS 模块化和打包方案收集

    1.这里想讨论的是拆分规则,不是在问哪个工具怎么使用.2.这里没有在想找正确答案,因为感觉这个问题要结合具体业务场景. 随着项目开发越来越大,一开始代码全打包到一个文件的方式,让文件越来越大,接下来要 ...

  3. Javascript模块化工具require.js教程

    转自:http://www.w3cschool.cc/w3cnote/requirejs-tutorial-1.html, http://www.w3cschool.cc/w3cnote/requir ...

  4. 【转】JS模块化工具requirejs教程(一):初识requirejs

    随着网站功能逐渐丰富,网页中的js也变得越来越复杂和臃肿,原有通过script标签来导入一个个的js文件这种方式已经不能满足现在互联网开发模式,我们需要团队协作.模块复用.单元测试等等一系列复杂的需求 ...

  5. 报表开发工具Finereport移动端app js接口列表【全】

    应用报表工具Finereport的开发人员会发现其移动端app 同样也推出了很多js接口,那这些接口到底有多少,其移动端又有哪些地方支持调用js,这些接口具体又该如何调用呢.根据我平时的开发经验,给大 ...

  6. 模块化开发--sea.js

    当你的网站开发越来越复杂的时候,会经常遇到一下问题吗?1.冲突2.性能3.依赖如果在多人开发或者是复杂的开发过程中会经常遇到这些问 题,就可以用模块化开发来解决.以上问题是如何产生的?1.冲突:如果你 ...

  7. JS模块化开发----require.js

    前言 前端开发中,起初只要在script标签中嵌入几十上百行代码就能实现一些基本的交互效果,后来js得到重视,应用也广泛起来了,jQuery,Ajax,Node.Js,MVC,MVVM等的助力也使得前 ...

  8. JS模块化工具require.js教程(二):基本知识

    前一篇:JS模块化工具我们以非常简单的方式引入了requirejs,这一篇将讲述一下requirejs中的一些基本知识,包括API使用方式等 基本API require会定义三个变量:define,r ...

  9. 【转】JS模块化工具requirejs教程(二):基本知识

    前一篇:JS模块化工具requirejs教程(一):初识requirejs 我们以非常简单的方式引入了requirejs,这一篇将讲述一下requirejs中的一些基本知识,包括API使用方式等. 基 ...

随机推荐

  1. Maven依赖的JAR包下载慢?赶紧看过来

    相信许多JAVA开发者在日常工作中时常会碰到这种情况,那就是编译Maven工程时,工程所依赖的jar包文件下载非常慢,甚至经常出现下载不成功的问题,今天,小编就给大家讲讲如何提升Maven依赖包的下载 ...

  2. redis安全(加入密码)

    一.前言 在使用云服务器时,安装的redis3.0+版本都关闭了protected-mode,因而都遭遇了挖矿病毒的攻击,使得服务器99%的占用率!! 因此我们在使用redis时候,最好更改默认端口, ...

  3. python函数超时情况应对总结

    最近处理一个线程中的函数超时问题. 函数里面有一个地方可能会卡死,我们需要去判断这个是不是卡死了,并做出相应的应对方案. 最开始想的是在函数上增加一个装饰器,使其在超时时抛出异常,然后在其他地方捕获这 ...

  4. Windbg双机调试环境配置(Windows7/Windows XP+VirtualBox/VMware+WDK7600)

    简介:Windbg双机调试内核.驱动 下载软件: 下载Windbg(GRMWDK_EN_7600_1.ISO) 下载VirtualBox 5.2/VMware 12 一.安装WDK,这里要提一点的是D ...

  5. FreeRTOS-06任务运行时间信息统计

    根据正点原子FreeRTOS视频整理 单片机:STM32F207VC FreeRTOS源码版本:v10.0.1 * 1. 要使用vTaskGetRunTimeStats()函数,需满足以下条件: * ...

  6. 面经问题总结——django相关

    1.让你从头设计一个web框架,第一步你会做什么? 2.django的orm是怎么实现的? 3.django的URL路径映射是怎么实现的? 4.平常你怎么运用django提供的钩子函数? 5.三级分销 ...

  7. openstf安装手记

    一款基于nodejs+jade模板开发的手机群控框架,你可以在此基础上拿来测试设备,或者群操作等等. 主要核心技术在于如何在不root的情况下实时流畅录屏,以及远程操作手机. 官方git https: ...

  8. springboot和quartz整合分布式多节点

    虽然单个Quartz实例能给予我们很好的任务job调度能力,但它不能满足典型的企业需求,如可伸缩性.高可靠性满足.假如你需要故障转移的能力并能运行日益增多的 Job,Quartz集群势必成为你应用的一 ...

  9. centos 7安装phpMyadmin

    1.phpmyadmin官网下载多语言包 2.找到libraries目录下的config.default.php1)查找$cfg['PmaAbsoluteUri'] 设置默认访问网址 2)查找$cfg ...

  10. ubuntu16上安装openJDK.md

    ubuntu16上安装openJDK.md 环境 操作系统:ubuntu 16.04.2 LTS 安装 当你不需要安装oracle的JDK时,使用openJDK,安装就比较方便. sudo apt-g ...