研究小程序的npm没多久,也就是稍微花了点时间研究了下,并记录一下我的理解和心得,有疏漏的地方,望各位指教。
首先,这里介绍的只囊括了如何使用npm,以及小程序npm基本的模块加载原理(没有太深入),并且我只测试了工具类的js的使用比如underscore,而如何发布npm包,如何使用npm中的组件,我并没有了解,望见谅。
微信小程序npm构建方法
第一步在你项目的根目录,打开命令行,使用npm init初始化一个项目,然后使用npm安装一些工具包,比如underscore和lodash等(注意官网上的使用--production参数下载,以免下载不必要的包),我会拿这两个介绍使用小程序的npm包的一些问题。
安装好了之后,进入微信开发者工具,按如下操作
1.首先需要勾选:使用npm模块 这个选项,然后,找到开发者工具左上角工具 => 构建npm 这一项,点击进行构建。
ps:如果你找不到这些东西,估计你的开发者工具需要更新了。
2.然后弹出如下弹窗,并且没有报错,则算是构建成功了。
我们来看一下构建后的目录,看一下有什么变化:
多出来一个miniprogram_npm文件夹,这是小程序构建打包npm包后的包目录,也正如官网所说的,小程序的打包npm不会更改node_modules中的内容,而是把node_modules中的所有包进行重新打包一次,以小程序的规则方式,专门适用小程序开发。而require方法在导入模块时,也会从这个miniprogram_npm文件夹中查找模块(import语法也会被编译成require的)。
然后每个包只有两个文件,一个index.js的主模块,一个source map文件,方便进行逆向调试。不管你之前的那个npm包有多大,都给你整个在一个文件中。
那么我们分析一波,小程序npm构建生成这个index到底是什么,究竟小程序他的npm构建到底干了什么。(不会太深入,仅仅是模块依赖方面的分析,怎么打包的,我也不知道)。
微信小程序npm模块加载原理
ps:首先你需要会基本的node和npm知识以及前端模块加载原理(CommonJS模块化规范)
为了更加好的解释其中的代码,我在Underscore模块的underscore.js同级目录新建了一个test.js模块,他的代码如下:
// test.js
const testFn = function() {
console.log('test');
}
module.exports = {
testFn,
};
然后在underscore.js源码的开头使用require导入这个test.js:
// underscore源码
// Baseline setup,underscore源码开始
// --------------
// 新增测试代码
const test = require('./test.js');
test.testFn();
// Establish the root object, `window` (`self`) in the browser, `global`
// on the server, or `this` in some virtual machines. We use `self`
// instead of `window` for `WebWorker` support.
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
this || {};
native code......
然后我们看一下Underscore打包后的index.js结构大致如下:
module.exports = (function() {
// 代码第一部分开始
var __MODS__ = {};
var __DEFINE__ = function(modId, func, req) {
var m = {
exports: {}
};
__MODS__[modId] = {
status: 0,
func: func,
req: req,
m: m
};
};
var __REQUIRE__ = function(modId, source) {
if (!__MODS__[modId]) return require(source);
if (!__MODS__[modId].status) {
var m = {
exports: {}
};
__MODS__[modId].status = 1;
__MODS__[modId].func(__MODS__[modId].req, m, m.exports);
if (typeof m.exports === "object") {
Object.keys(m.exports).forEach(function(k) {
__MODS__[modId].m.exports[k] = m.exports[k];
});
if (m.exports.__esModule) Object.defineProperty(__MODS__[modId].m.exports, "__esModule", {
value: true
});
} else {
__MODS__[modId].m.exports = m.exports;
}
}
return __MODS__[modId].m.exports;
};
var __REQUIRE_WILDCARD__ = function(obj) {
if (obj && obj.__esModule) {
return obj;
} else {
var newObj = {};
if (obj != null) {
for (var k in obj) {
if (Object.prototype.hasOwnProperty.call(obj, k)) newObj[k] = obj[k];
}
}
newObj.default = obj;
return newObj;
}
};
var __REQUIRE_DEFAULT__ = function(obj) {
return obj && obj.__esModule ? obj.default : obj;
};
// 代码第一部分结束
// 代码第二部分开始(其实就是定义underscore模块)
__DEFINE__(1545375993448, function(require, module, exports) {
// Baseline setup
// --------------
// 新增测试代码
const test = require('./test.js');
test.testFn();
// Underscore模块本身的所有源代码...
}, function (modId) { var map = { "./test.js": 1545375993449 }; return __REQUIRE__(map[modId], modId); });
// 代码第二部分结束
// 代码第三部分开始(其实就是定义test.js模块)
__DEFINE__(1545375993449, function (require, module, exports) {
const testFn = function () {
console.log('test');
}
module.exports = {
testFn,
};
}, function (modId) { var map = {}; return __REQUIRE__(map[modId], modId); })
// 代码第三部分结束
// 代码最后部分
return __REQUIRE__(1545294162674);
})()
然后看结构,其实就这么几块东西:
1.index本身也是一个模块,则通过module.exports暴露接口,通过一个自执行函数,这个自执行函数的返回值就是这个模块暴露的接口。
2.代码第一部分是定义了一个对象和四个函数:
__MODS__对象,__DEFINE__函数,__REQUIRE__函数,__REQUIRE_WILDCARD__函数以及__REQUIRE_DEFAULT__函数。
一些名词解释:模块id,即modId,他是一个时间戳,以这个为模块id,工厂函数factory:属性CommonJS模块化规范的各位都知道,factory函数其实就是我们前端使用define函数定义模块时,写模块逻辑的函数。
然后下面依次介绍他们的作用:
__MODS__对象:存储模块对象,通过__DEFINE__这个函数定义模块时存储的
__DEFINE__函数:通过modId(模块id), func函数(工厂函数factory,即CommonJS用来执行逻辑代码的函数,他接受require, module, exports这三个参数),req函数(用它来加载依赖这个模块的依赖模块,接受模块id为参数)
__REQUIRE__函数:根据模块id来加载一个模块(后面会详细介绍)
__REQUIRE_DEFAULT__函数:不太清楚,估计是为了配合es6的Module语法的通配符的
__REQUIRE_DEFAULT__函数,估计是为了获取es6的Module语法的default模块。
微信小程序通过模块依赖分析,把这个npm的包的主入口模块,以及他所依赖的所有模块通过__DEFINE__函数创建一个模块对象,存储于__MODS__对象中,此模块的id为一个时间戳,每个模块的id都不同,第二个factory没什么好说的,遵循CommonJS,第三个参数就是这个模块的require方法,也就是说这个模块内部通过require函数依赖模块时就是调用的这个函数,比如:
function (modId) {
var map = { "./test.js": 1545375993449 };
return __REQUIRE__(map[modId], modId);
}
这个是定义Underscore这个模块时(还有一个test模块)的第三个参数,他等同于第二个参数factory函数中的require参数,他们是一样的,可以查看__REQUIRE__函数的实现。然后他的参数modId就是一个路径,比如Underscore中在引入test模块时,是这样调用的:
const test = require('./test.js');
test.testFn();
也就是说require的参数是模块路径,而require函数的里面的map对象就存在一个./test.js属性,值为模块的id,通过他拿到模块id,用它来调用__REQUIRE__函数,获取模块暴露的接口。也就是,小程序在构建npm时他会分析你这个模块的代码,把这个模块所有调用了require方法的模块全部存储在require函数的map对象中(如果依赖的是其他的npm包,则不会放在map中)。
而__REQUIRE__函数就是根据modId通过__MODS__对象获取到该模块对象,然后执行factory函数,获取到这个模块暴露的接口对象。
那么这里有一点需要注意,如果模这个模块require依赖的模块不是我这个模块本身提供的,而是其他npm模块提供的,也就是依赖的是其他的npm模块,那怎么办呢?比如,我在Underscore中引入了lodash,会是什么情况呢?我们测试一下,然后检查一下重新构建后underscore依赖中的代码:
var map = {"./test.js":1545375993454};
function(modId) {
var map = {"./test.js":1545375993454};
return __REQUIRE__(map[modId], modId);
}
我们发现,underscore模块在定义时,map对象中只依赖了test模块,而lodash没有通过这个模块进行构建打包,那么,这时候,__REQUIRE__函数如果发现__MODS__不存在这个模块,那么会使用微信小程序本身的require函数进行依赖,这也是很符合我们的预期的,因为只让这个模块打包本模块的东西,而其他的npm模块通过全局的require来引用,所以只使用__DEFINE__定义本模块的东西,用__REQUIRE__函数依赖本模块的东西,避免重复打包,看__REQUIRE__的第一行代码:
// source参数就是模块名称(不是模块的时间戳id)看上一个例句的代码。
if (!__MODS__[modId]) return require(source);
以上就是微信小程序的npm构建,我们知道其实他是通过类似webpack的模块打包的方式,将一个原始的npm模块,通过模块依赖打包成一个index.js文件,然后我们在使用require时,小程序会从miniprogram_npm中查找模块,直接加载模块中的index.js文件。当然这里只介绍打包后的样子,至于怎么打包的,就不清楚了。
使用小程序npm构建后的模块
那到了这里,你就可以在页面中直接使用require()方法传入一个模块名,来导入包,那岂不是说现在就可以直接使用了...我们来测试一下:
// 或者使用: const _ = require('underscore');
import _ from 'underscore';
const result = _.map([1, 2, 3], (value => {
return value + 1;
}))
console.log(result);
// 输出 [2, 3, 4]
就结果而言,完美。然后我们来测试lodash:
import _ from 'lodash';
const result = _.map([1, 2, 3], (value => {
return value + 1;
}))
console.log(result);
// 输出
TypeError: Cannot read property 'prototype' of undefined
恩?怎么报错了?这时就要提到,微信小程序的开发和web浏览器端和node端的开发不一样的问题了,我们来看一下lodash的报错的地方。
// lodash部分源码
var runInContext = (function runInContext(context) {
context = context == null ? root : _.defaults(root.Object(), context, _.pick(root, contextProps));
/** Built-in constructor references. */
var Array = context.Array,
Date = context.Date,
Error = context.Error,
Function = context.Function,
Math = context.Math,
Object = context.Object,
RegExp = context.RegExp,
String = context.String,
TypeError = context.TypeError;
/** Used for built-in method references. */
var arrayProto = Array.prototype,
funcProto = Function.prototype,
objectProto = Object.prototype;
......
// 省略
})
报错在这一行:var arrayProto = Array.prototype,也就是这个Array是一个undefined,而这个Array是来自于context.Array。而context又是什么?context是runInContext函数在执行时传入的参数,我们在注释中看到runInContext函数的注释:
Create a new pristine `lodash` function using the `context` object
也就是runInContext是使用context创建一个初始的 lodash 函数用的(好了,不管那么多,我们不是研究lodash源码的,只是看怎么会报错的)那么在哪执行的runInContext函数呢?源码中有一句:
// Export lodash.
var _ = runInContext();
我们可以看到,在创建lodash也就是 _ 对象时,调用runInContext传入的是一个空对象,那么根据runInContext函数的代码,context就是root对象。那root对象是什么呢?看源码的这一段代码:
/** Detect free variable `global` from Node.js. */
var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;
/** Detect free variable `self`. */
var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
/** Used as a reference to the global object. */
var root = freeGlobal || freeSelf || Function('return this')();
从上面看出,判断是否node环境的global,以及判断是否是浏览器环境self,如果都不是,则返回this。也就是说root就是找到这个环境的全局对象。那么小程序的全局对象是什么呢???我们来测试一下:
我们在一个page页面中打印window,发现竟然是undefined?然后在页面中打印global和this,self,发现global是一个对象,但不是我们想要的,因为他没有global.Object对象,而且只有寥寥的几个属性。而this和self也是一个undefined,我们在小程序构建后的lodash中打印上述对象,发现和page中的值一致。那么,就很明显了,lodash中的root是一个{},因为Function('return this')()他返回的是一个空对象。那么很显然,一个空对象的Array属性肯定是undefined了。
那么小程序中为啥会没有window对象,而页面中的js就没有呢?这就要提一下小程序的模块化了,首先小程序遵循CommonJS的模块化规范,那么小程序在执行时,会帮我们编译每一个js文件,帮我们把js代码使用一个define函数包裹起来,这样就避免我们手动使用define函数来包裹每一个js文件了。
如下:
眼尖的各位应该已经注意到了,define函数的factory参数函数除了传入了传统Commonjs规范中的require, module, exports这三个参数之外,还传入了 window,document,frames,self这些,熟悉浏览器端开发的可能知道,这些都是浏览器端提供的js对象,而小程序在factory函数中传入这些参数是为啥?我们知道因为js作用域链的原因,函数内部里面的变量优先被访问,那么小程序的define函数的factory函数中的这些参数,估计是为了覆盖这些浏览器拥有的属性,从而防止我们访问这些浏览器属性吧,咳,扯远了。
那么如何解决lodash的问题呢?其实到了这里已经比较麻烦了,因为root不存在,并且,不太好大范围改动源码,因为root使用的地方不确定有多少,怎么获取到这个全局对象root呢?
我在lodash中通过调用一个普通函数,输出里面的this,发现竟然可以获取到window对象,这让我开心了一阵,然后打算在不同模块中多测试几次,不过经过我多次测试发现一个很奇怪的现象,那就是同时在lodash和underscore中调用一个普通函数,然后打印里面的this,发现,里面的this竟然不同:
var test = function () {
return this;
}
// underscore:undefined
// lodash:Window对象
咦,为什么在lodash中就可以打印出Window对象?这很让我疑惑,然后我打算在真机上测试一下,这时候他突然给了一个让人容易疏忽的提示:
lodash文件过大(大于500k,我用的不是生产环境的版本),跳过es6转es5以及代码压缩。
开始也没在意,但因为我找了半天,实在找不到lodash到底做了什么语法处理,导致可以他可以访问小程序的window对象,其实也就是微信小程序的全局对象。抱着一试的态度,我关闭了微信小程序的es6转es5,然后重新测试上诉代码,我惊奇的发现,关闭es6转es5的功能后,underscore和lodash的this指向的都是全局对象。这让我很惊讶。那么竟然可以得到全局对象,那么我尝试对lodash的如下改动:
// 改动前
var root = freeGlobal || freeSelf || Function('return this')();
// 改动后
var root = freeGlobal || freeSelf || (function(){return this}());
ps:以上代码要在lodash源码中改,不要在构建后的小程序npm的源码中改,不然,下一次构建还是需要重新添加。
最后,重新构建,然后重新运行下面代码:
import _ from 'lodash';
const result = _.map([1, 2, 3], (value => {
return value + 1;
}))
console.log(result);
输出:[2, 3, 4]
成功了,这就很奇怪,为什么关闭es6转es5的功能后,调用函数时,那个函数的指向就是全局对象,而进行了es6转es5后,调用函数时那个函数的this就为undefined了呢?这个,我暂时没有找到合理的解释,我百思不得其解,他到底是怎么做到将this指向undefined的呢,正当我一度快要放弃的时候,我偶然找了几篇关于js的this指向的文章,突然,我撇到了一眼 "use strict"; 哎呦,我去,我一拍脑门,骂了声自己是SB,这么简单的细节竟然被我忽略了,因为babel编译时,会为编译的js添加严格模块,
所以,函数调用的this指向为undefined。很难受。自我检讨,自我检讨...
使用微信小程序构建npm模块中的问题:
虽然找到了原因,但总不能真的不用es6转es5吧,这可无法接受啊,所以,还是无法使用。而且上面的例子仅仅是为了述说一下在构建小程序的npm时,有很多npm上的包通过小程序构建后是无法使用的,需要进行修改,我们也通过上面的例子分析,知道了小程序构建npm时的一些注意事项,以及如果你确定要使用这个包时,遇到了问题如何解决,以下是注意的点:
1.小程序屏蔽掉了window对象(self,global等)
2.一般不依赖于root,即不依赖于全局对象的模块倒是可以使用的,比如underscore
3.小程序通过babel编译,this也无法使用,等于如果一个npm包如果依赖root,但window,self,global和
this都无法使用,那就凉了,比如lodash
所以你真的还想要使用这个包,那么只有以下几个办法了:
1.不使用微信小程序的babel进行编译,因为微信小程序的babel编译目前我没有找到办法控制,所以严格模块更改不了,所以比较麻烦,只能舍弃了
2.自定义babel配置,使用微信小程序的自定义处理命令,自己构建babel编译,我的初始想法是:使用编译前的自定义预处理命令,调用babel或者配合node命令,编译js文件,如同我在小程序中自己编译sass那样(关于这篇文章,请看我这篇博客:https://blog.csdn.net/qq_33024515/article/details/85100597)。但是,我毕竟没试验过啊,可能根本行不通,大家就不要
踩坑了啊(也许配合Gulp或者webpack可以)。
3.去微信开发者论坛去反馈(我已经反馈了,不过好像沉了),因为怎么说呢,按理来说,一般我们在配置babel进行编译时时,都会忽略掉node_module文件夹中的模块,因为一般这里面的模块都是不需要进行编译的,不知道为什么小程序的babel还会对miniprogram_npm文件夹中的模块进行babel编译,所以,希望官方出个功能可以在编译时可以选择忽略miniprogram_npm文件夹的模块的选项就好了。
4.不要用这个模块了,或者自己改造,出了问题,然后找到问题,看可不可以解决嘛,解决不了再说吧。
5.虽然我个人没有使用过,但在这里可以推荐一波小程序开发框架来开发小程序,比如:wepy和mpvue这些他们自带webpack,以及babel这些,这样你想用啥都可以。
最后附上我想到的使用lodash的解决方案的另一个骚代码,解决root的问题(这里可以使用babel进行编译):
源码中有这么一个数组:
/** Used to assign default `context` object properties. */
var contextProps = [
'Array', 'Buffer', 'DataView', 'Date', 'Error', 'Float32Array', 'Float64Array',
'Function', 'Int8Array', 'Int16Array', 'Int32Array', 'Map', 'Math', 'Object',
'Promise', 'RegExp', 'Set', 'String', 'Symbol', 'TypeError', 'Uint8Array',
'Uint8ClampedArray', 'Uint16Array', 'Uint32Array', 'WeakMap',
'_', 'clearTimeout', 'isFinite', 'parseInt', 'setTimeout'
];
看意思,好像是context对象的一些默认属性,我翻了一下,发现好像也是lodash在使用root对象时,使用过的属性也都是这些。那么我们何不直接把这些属性方法给root扩展一个,看是不是可以解决:
// 在定义root的后面加上这一句:
// 扩展root
var wxExtendsRoot = function () {
root = typeof root === 'object' ? root : {};
root['Array'] = typeof Array === 'undefined' ? undefined : Array;root['Buffer'] = typeof Buffer === 'undefined' ? undefined : Buffer;root['DataView'] = typeof DataView === 'undefined' ? undefined : DataView;root['Date'] = typeof Date === 'undefined' ? undefined : Date;root['Error'] = typeof Error === 'undefined' ? undefined : Error;root['Float32Array'] = typeof Float32Array === 'undefined' ? undefined : Float32Array;root['Float64Array'] = typeof Float64Array === 'undefined' ? undefined : Float64Array;root['Function'] = typeof Function === 'undefined' ? undefined : Function;root['Int8Array'] = typeof Int8Array === 'undefined' ? undefined : Int8Array;root['Int16Array'] = typeof Int16Array === 'undefined' ? undefined : Int16Array;root['Int32Array'] = typeof Int32Array === 'undefined' ? undefined : Int32Array;root['Map'] = typeof Map === 'undefined' ? undefined : Map;root['Math'] = typeof Math === 'undefined' ? undefined : Math;root['Object'] = typeof Object === 'undefined' ? undefined : Object;root['Promise'] = typeof Promise === 'undefined' ? undefined : Promise;root['RegExp'] = typeof RegExp === 'undefined' ? undefined : RegExp;root['Set'] = typeof Set === 'undefined' ? undefined : Set;root['String'] = typeof String === 'undefined' ? undefined : String;root['Symbol'] = typeof Symbol === 'undefined' ? undefined : Symbol;root['TypeError'] = typeof TypeError === 'undefined' ? undefined : TypeError;root['Uint8Array'] = typeof Uint8Array === 'undefined' ? undefined : Uint8Array;root['Uint8ClampedArray'] = typeof Uint8ClampedArray === 'undefined' ? undefined : Uint8ClampedArray;root['Uint16Array'] = typeof Uint16Array === 'undefined' ? undefined : Uint16Array;root['Uint32Array'] = typeof Uint32Array === 'undefined' ? undefined : Uint32Array;root['WeakMap'] = typeof WeakMap === 'undefined' ? undefined : WeakMap;root['_'] = typeof _ === 'undefined' ? undefined : _;root['clearTimeout'] = typeof clearTimeout === 'undefined' ? undefined : clearTimeout;root['isFinite'] = typeof isFinite === 'undefined' ? undefined : isFinite;root['parseInt'] = typeof parseInt === 'undefined' ? undefined : parseInt;root['setTimeout'] = typeof setTimeout === 'undefined' ? undefined : setTimeout;
}
wxExtendsRoot();
这样root上面也会拥有诸如:Array Buffer setTimeout这些属性方法。然后重新构建一下,验证一下:
import _ from 'lodash';
const result = _.map([1, 2, 3], (value => {
return value + 1;
}))
console.log(result);
输出:[2, 3, 4]
我们发现结果也是ok的,但是会不会出其他问题就不知道了,而且,如果你下载的是生产环境,压缩过的代码那估计谁都找不到那个地方,所以,比较鸡肋吧。
总结:
如何使用微信小程序进行npm构建大致就到了这里,其实各位可以进行更多的尝试,然后一起交流,其实这个npm构建还是有点用的,比如你想要使用async/await语法,那你可以使用npm直接下载regenerator-runtime包,然后直接构建,直接在页面中导入就可以使用了,这样就不需要每次项目都要复制一份这个文件,还是不错的。虽然小程序在使用的npm包时,还有其他问题,不过喜欢新鲜事物的,可以尝试一下,并且期待一下他的更新,做的越来越会。