摘要:平时有使用过和开发的同学,应该能体会到模块化开发的好处。原因很简单打包出来的使用了关键字,而小程序内部并支持。是一个模块打包器,可以将小块代码编译成大块复杂的代码,例如或应用程序。官网的这段介绍,正说明了就是用来打包的。
博客地址
最近有个需求,需要为小程序写一个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");
在浏览器端