java web app libries_使用模块化工具打包自己开发的JS库

最近有个需求,需要为小程序写一个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开发的同学,应该能体会到模块化开发的好处。

如下是定义的目录结构:

c7b8af02c7ec3727e5054fa62860eeeb.png

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');

控制台直接报错。。。

e523029004b9bb0b8ce4531e6b505c73.png

1c0978e208122e95affc58918477db72.png

原因很简单:打包出来的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')

);

是不是都类似这样的套路,最后只是立即执行一个方法而已,并没有导出一个变量。

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);

55fb2c459229fdd3eb45102ce31c98ac.png

我们导出的对象挂载到了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');

7b46f77304030c0cd0e25f68e66919d4.png

你可能注意到,我打包时并没有使用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 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()

]

};

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');

在浏览器端

monitor.init('6666');

原理其实也很简单,你可以看下打包后的源码,或者看我之前写过的一篇文章

总结

rollup通常适用于打包JS类库,通过rollup打包后的代码,体积较小,而且没有冗余的代码。rollup默认只支持ES6的模块化,如果需要支持Commonjs,还需下载相应的插件rollup-plugin-commonjs

webpack通常适用于打包一个应用,如果你需要代码拆分(Code Splitting)或者你有很多静态资源需要处理,那么可以考虑使用webpack

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值