博客地址javascript
最近有个需求,须要为小程序写一个SDK,监控小程序的API调用和页面报错(相似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
演示代码下载webpack
以下是定义的目录结构: git
src目录下存放源代码,dist目录打包最后的monitor.js
es6
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打包,毕竟工做常常用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就是问题的关键,经过设置该属性,咱们可让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/
}
]
}
复制代码
注意点:
webpack -w
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,图片,字体这些静态资源,也不用按需加载。
文章写到这里原本能够结束了,可是在前期调研如何打包模块的时候,我特地看了下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 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()
]
};
复制代码
然而这样运行会报错,由于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()
]
};
复制代码
咱们刚刚打包的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