摘要:平時有使用過和開發的同學,應該能體會到模塊化開發的好處。原因很簡單打包出來的使用了關鍵字,而小程序內部并支持。是一個模塊打包器,可以將小塊代碼編譯成大塊復雜的代碼,例如或應用程序。官網的這段介紹,正說明了就是用來打包的。
博客地址
最近有個需求,需要為小程序寫一個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: "" })
import React from "react"; import ReactDOM from "react-dom"; import App from "./App.js"; ReactDOM.render(, document.getElementById("root") );
是不是都類似這樣的套路,最后只是立即執行一個方法而已,并沒有導出一個變量。
libraryTargetlibraryTarget就是問題的關鍵,通過設置該屬性,我們可以讓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/ } ] }
注意點:
平時開發調試sdk時可以直接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,圖片,字體這些靜態資源,也不用按需加載。
rollup文章寫到這里本來可以結束了,但是在前期調研如何打包模塊的時候,我特意看了下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 srcmoduleEngine.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() ] };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");
在瀏覽器端