博客地址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