1.研究小程序的npm没多久,也就是稍微花了点时间研究了下,并记录一下我的理解和心得,有疏漏的地方,望各位指教。
2.首先,这里介绍的只囊括了如何使用npm,以及小程序npm基本的模块加载原理(没有太深入),并且我只测试了工具类的js的使用比如underscore,而如何发布npm包,如何使用npm中的组件,我并没有了解,望见谅。
微信小程序npm构建方法3.第一步在你项目的根目录,打开命令行,使用npminit初始化一个项目,然后使用npm安装一些工具包,比如underscore和lodash等(注意官网上的使用--production参数下载,以免下载不必要的包),我会拿这两个介绍使用小程序的npm包的一些问题。
4.安装好了之后,进入微信开发者工具,按如下操作
5.1.首先需要勾选:使用npm模块这个选项,然后,找到开发者工具左上角工具=构建npm这一项,点击进行构建。
6.
7.ps:如果你找不到这些东西,估计你的开发者工具需要更新了。
8.2.然后弹出如下弹窗,并且没有报错,则算是构建成功了。
9.
10.我们来看一下构建后的目录,看一下有什么变化:
11.
12.多出来一个miniprogram_npm文件夹,这是小程序构建打包npm包后的包目录,也正如官网所说的,小程序的打包npm不会更改node_modules中的内容,而是把node_modules中的所有包进行重新打包一次,以小程序的规则方式,专门适用小程序开发。而require方法在导入模块时,也会从这个miniprogram_npm文件夹中查找模块(import语法也会被编译成require的)。然后每个包只有两个文件,一个index.js的主模块,一个sourcemap文件,方便进行逆向调试。不管你之前的那个npm包有多大,都给你整个在一个文件中。
13.那么我们分析一波,小程序npm构建生成这个index到底是什么,究竟小程序他的npm构建到底干了什么。(不会太深入,仅仅是模块依赖方面的分析,怎么打包的,我也不知道)。
微信小程序npm模块加载原理14.ps:首先你需要会基本的node和npm知识以及前端模块加载原理(CommonJS模块化规范)
15.为了更加好的解释其中的代码,我在Underscore模块的underscore.js同级目录新建了一个test.js模块,他的代码如下:
//test.jsconsttestFn=function(){console.log('test');}module.exports={testFn,};16.然后在underscore.js源码的开头使用require导入这个test.js:
//underscore源码//Baselinesetup,underscore源码开始//--------------//新增测试代码consttest=require('./test.js');test.testFn();//Establishtherootobject,`window`(`self`)inthebrowser,`global`//ontheserver,or`this`insomevirtualmachines.Weuse`self`//insteadof`window`for`WebWorker`support.varroot=typeofself=='object'&&self.self===self&&self||typeofglobal=='object'&&global.global===global&&global||this||{};nativecode......17.然后我们看一下Underscore打包后的index.js结构大致如下:
module.exports=(function(){//代码第一部分开始var__MODS__={};var__DEFINE__=function(modId,func,req){varm={exports:{}};__MODS__[modId]={status:0,func:func,req:req,m:m};};var__REQUIRE__=function(modId,source){if(!__MODS__[modId])returnrequire(source);if(!__MODS__[modId].status){varm={exports:{}};__MODS__[modId].status=1;__MODS__[modId].func(__MODS__[modId].req,m,m.exports);if(typeofm.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){returnobj;}else{varnewObj={};if(obj!=null){for(varkinobj){if(Object.prototype.hasOwnProperty.call(obj,k))newObj[k]=obj[k];}}newObj.default=obj;returnnewObj;}};var__REQUIRE_DEFAULT__=function(obj){returnobj&&obj.__esModule?obj.default:obj;};//代码第一部分结束//代码第二部分开始(其实就是定义underscore模块)__DEFINE__(1545375993448,function(require,module,exports){//Baselinesetup//--------------//新增测试代码consttest=require('./test.js');test.testFn();//Underscore模块本身的所有源代码...},function(modId){varmap={"./test.js":1545375993449};return__REQUIRE__(map[modId],modId);});//代码第二部分结束//代码第三部分开始(其实就是定义test.js模块)__DEFINE__(1545375993449,function(require,module,exports){consttestFn=function(){console.log('test');}module.exports={testFn,};},function(modId){varmap={};return__REQUIRE__(map[modId],modId);})//代码第三部分结束//代码最后部分return__REQUIRE__(1545294162674);})()18.然后看结构,其实就这么几块东西:
19.1.index本身也是一个模块,则通过module.exports暴露接口,通过一个自执行函数,这个自执行函数的返回值就是这个模块暴露的接口。
20.2.代码第一部分是定义了一个对象和四个函数:__MODS__对象,__DEFINE__函数,__REQUIRE__函数,__REQUIRE_WILDCARD__函数以及__REQUIRE_DEFAULT__函数。
21.一些名词解释:模块id,即modId,他是一个时间戳,以这个为模块id,工厂函数factory:属性CommonJS模块化规范的各位都知道,factory函数其实就是我们前端使用define函数定义模块时,写模块逻辑的函数。
22.然后下面依次介绍他们的作用:
23.__MODS__对象:存储模块对象,通过__DEFINE__这个函数定义模块时存储的
24.__DEFINE__函数:通过modId(模块id),func函数(工厂函数factory,即CommonJS用来执行逻辑代码的函数,他接受require,module,exports这三个参数),req函数(用它来加载依赖这个模块的依赖模块,接受模块id为参数)__REQUIRE__函数:根据模块id来加载一个模块(后面会详细介绍)__REQUIRE_DEFAULT__函数:不太清楚,估计是为了配合es6的Module语法的通配符的__REQUIRE_DEFAULT__函数,估计是为了获取es6的Module语法的default模块。
25.微信小程序通过模块依赖分析,把这个npm的包的主入口模块,以及他所依赖的所有模块通过__DEFINE__函数创建一个模块对象,存储于__MODS__对象中,此模块的id为一个时间戳,每个模块的id都不同,第二个factory没什么好说的,遵循CommonJS,第三个参数就是这个模块的require方法,也就是说这个模块内部通过require函数依赖模块时就是调用的这个函数,比如:
function(modId){varmap={"./test.js":1545375993449};return__REQUIRE__(map[modId],modId);}26.这个是定义Underscore这个模块时(还有一个test模块)的第三个参数,他等同于第二个参数factory函数中的require参数,他们是一样的,可以查看__REQUIRE__函数的实现。然后他的参数modId就是一个路径,比如Underscore中在引入test模块时,是这样调用的:
consttest=require('./test.js');test.testFn();27.也就是说require的参数是模块路径,而require函数的里面的map对象就存在一个./test.js属性,值为模块的id,通过他拿到模块id,用它来调用__REQUIRE__函数,获取模块暴露的接口。也就是,小程序在构建npm时他会分析你这个模块的代码,把这个模块所有调用了require方法的模块全部存储在require函数的map对象中(如果依赖的是其他的npm包,则不会放在map中)。
28.而__REQUIRE__函数就是根据modId通过__MODS__对象获取到该模块对象,然后执行factory函数,获取到这个模块暴露的接口对象。
29.那么这里有一点需要注意,如果模这个模块require依赖的模块不是我这个模块本身提供的,而是其他npm模块提供的,也就是依赖的是其他的npm模块,那怎么办呢?比如,我在Underscore中引入了lodash,会是什么情况呢?我们测试一下,然后检查一下重新构建后underscore依赖中的代码:
varmap={"./test.js":1545375993454};function(modId){varmap={"./test.js":1545375993454};return__REQUIRE__(map[modId],modId);}30.我们发现,underscore模块在定义时,map对象中只依赖了test模块,而lodash没有通过这个模块进行构建打包,那么,这时候,__REQUIRE__函数如果发现__MODS__不存在这个模块,那么会使用微信小程序本身的require函数进行依赖,这也是很符合我们的预期的,因为只让这个模块打包本模块的东西,而其他的npm模块通过全局的require来引用,所以只使用__DEFINE__定义本模块的东西,用__REQUIRE__函数依赖本模块的东西,避免重复打包,看__REQUIRE__的第一行代码:
//source参数就是模块名称(不是模块的时间戳id)看上一个例句的代码。if(!__MODS__[modId])returnrequire(source);31.以上就是微信小程序的npm构建,我们知道其实他是通过类似webpack的模块打包的方式,将一个原始的npm模块,通过模块依赖打包成一个index.js文件,然后我们在使用require时,小程序会从miniprogram_npm中查找模块,直接加载模块中的index.js文件。当然这里只介绍打包后的样子,至于怎么打包的,就不清楚了。
使用小程序npm构建后的模块32.那到了这里,你就可以在页面中直接使用require()方法传入一个模块名,来导入包,那岂不是说现在就可以直接使用了...我们来测试一下:
//或者使用:const_=require('underscore');import_from'underscore';constresult=_.map([1,2,3],(value={returnvalue+1;}))console.log(result);//输出[2,3,4]33.就结果而言,完美。然后我们来测试lodash:
import_from'lodash';constresult=_.map([1,2,3],(value={returnvalue+1;}))console.log(result);//输出TypeError:Cannotreadproperty'prototype'ofundefined34.恩?怎么报错了?这时就要提到,微信小程序的开发和web浏览器端和node端的开发不一样的问题了,我们来看一下lodash的报错的地方。
//lodash部分源码varrunInContext=(functionrunInContext(context){context=context==null?root:_.defaults(root.Object(),context,_.pick(root,contextProps));/**Built-inconstructorreferences.*/varArray=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;/**Usedforbuilt-inmethodreferences.*/vararrayProto=Array.prototype,funcProto=Function.prototype,objectProto=Object.prototype;......//省略})35.报错在这一行:vararrayProto=Array.prototype,也就是这个Array是一个undefined,而这个Array是来自于context.Array。而context又是什么?context是runInContext函数在执行时传入的参数,我们在注释中看到runInContext函数的注释:
36.Createanewpristine`lodash`functionusingthe`context`object
37.也就是runInContext是使用context创建一个初始的lodash函数用的(好了,不管那么多,我们不是研究lodash源码的,只是看怎么会报错的)那么在哪执行的runInContext函数呢?源码中有一句:
//Exportlodash.var_=runInContext();38.我们可以看到,在创建lodash也就是_对象时,调用runInContext传入的是一个空对象,那么根据runInContext函数的代码,context就是root对象。那root对象是什么呢?看源码的这一段代码:
/**Detectfreevariable`global`fromNode.js.*/varfreeGlobal=typeofglobal=='object'&&global&&global.Object===Object&&global;/**Detectfreevariable`self`.*/varfreeSelf=typeofself=='object'&&self&&self.Object===Object&&self;/**Usedasareferencetotheglobalobject.*/varroot=freeGlobal||freeSelf||Function('returnthis')();39.从上面看出,判断是否node环境的global,以及判断是否是浏览器环境self,如果都不是,则返回this。也就是说root就是找到这个环境的全局对象。那么小程序的全局对象是什么呢???我们来测试一下:我们在一个page页面中打印window,发现竟然是undefined?然后在页面中打印global和this,self,发现global是一个对象,但不是我们想要的,因为他没有global.Object对象,而且只有寥寥的几个属性。而this和self也是一个undefined,我们在小程序构建后的lodash中打印上述对象,发现和page中的值一致。那么,就很明显了,lodash中的root是一个{},因为Function('returnthis')()他返回的是一个空对象。那么很显然,一个空对象的Array属性肯定是undefined了。
40.那么小程序中为啥会没有window对象,而页面中的js就没有呢?这就要提一下小程序的模块化了,首先小程序遵循CommonJS的模块化规范,那么小程序在执行时,会帮我们编译每一个js文件,帮我们把js代码使用一个define函数包裹起来,这样就避免我们手动使用define函数来包裹每一个js文件了。如下:眼尖的各位应该已经注意到了,define函数的factory参数函数除了传入了传统Commonjs规范中的require,module,exports这三个参数之外,还传入了window,document,frames,self这些,熟悉浏览器端开发的可能知道,这些都是浏览器端提供的js对象,而小程序在factory函数中传入这些参数是为啥?我们知道因为js作用域链的原因,函数内部里面的变量优先被访问,那么小程序的define函数的factory函数中的这些参数,估计是为了覆盖这些浏览器拥有的属性,从而防止我们访问这些浏览器属性吧,咳,扯远了。
41.那么如何解决lodash的问题呢?其实到了这里已经比较麻烦了,因为root不存在,并且,不太好大范围改动源码,因为root使用的地方不确定有多少,怎么获取到这个全局对象root呢?
42.我在lodash中通过调用一个普通函数,输出里面的this,发现竟然可以获取到window对象,这让我开心了一阵,然后打算在不同模块中多测试几次,不过经过我多次测试发现一个很奇怪的现象,那就是同时在lodash和underscore中调用一个普通函数,然后打印里面的this,发现,里面的this竟然不同:
vartest=function(){returnthis;}//underscore:undefined//lodash:Window对象43.咦,为什么在lodash中就可以打印出Window对象?这很让我疑惑,然后我打算在真机上测试一下,这时候他突然给了一个让人容易疏忽的提示:
44.lodash文件过大(大于500k,我用的不是生产环境的版本),跳过es6转es5以及代码压缩。
45.开始也没在意,但因为我找了半天,实在找不到lodash到底做了什么语法处理,导致可以他可以访问小程序的window对象,其实也就是微信小程序的全局对象。抱着一试的态度,我关闭了微信小程序的es6转es5,然后重新测试上诉代码,我惊奇的发现,关闭es6转es5的功能后,underscore和lodash的this指向的都是全局对象。这让我很惊讶。那么竟然可以得到全局对象,那么我尝试对lodash的如下改动:
//改动前varroot=freeGlobal||freeSelf||Function('returnthis')();//改动后varroot=freeGlobal||freeSelf||(function(){returnthis}());46.ps:以上代码要在lodash源码中改,不要在构建后的小程序npm的源码中改,不然,下一次构建还是需要重新添加。
47.最后,重新构建,然后重新运行下面代码:
import_from'lodash';constresult=_.map([1,2,3],(value={returnvalue+1;}))console.log(result);输出:[2,3,4]48.成功了,这就很奇怪,为什么关闭es6转es5的功能后,调用函数时,那个函数的指向就是全局对象,而进行了es6转es5后,调用函数时那个函数的this就为undefined了呢?这个,我暂时没有找到合理的解释,我百思不得其解,他到底是怎么做到将this指向undefined的呢,正当我一度快要放弃的时候,我偶然找了几篇关于js的this指向的文章,突然,我撇到了一眼"usestrict";哎呦,我去,我一拍脑门,骂了声自己是SB,这么简单的细节竟然被我忽略了,因为babel编译时,会为编译的js添加严格模块,所以,函数调用的this指向为undefined。很难受。自我检讨,自我检讨...
使用微信小程序构建npm模块中的问题:49.虽然找到了原因,但总不能真的不用es6转es5吧,这可无法接受啊,所以,还是无法使用。而且上面的例子仅仅是为了述说一下在构建小程序的npm时,有很多npm上的包通过小程序构建后是无法使用的,需要进行修改,我们也通过上面的例子分析,知道了小程序构建npm时的一些注意事项,以及如果你确定要使用这个包时,遇到了问题如何解决,以下是注意的点:
50.1.小程序屏蔽掉了window对象(self,global等)2.一般不依赖于root,即不依赖于全局对象的模块倒是可以使用的,比如underscore3.小程序通过babel编译,this也无法使用,等于如果一个npm包如果依赖root,但window,self,global和this都无法使用,那就凉了,比如lodash
51.所以你真的还想要使用这个包,那么只有以下几个办法了:
52.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这些,这样你想用啥都可以。
53.最后附上我想到的使用lodash的解决方案的另一个骚代码,解决root的问题(这里可以使用babel进行编译):
54.源码中有这么一个数组:
/**Usedtoassigndefault`context`objectproperties.*/varcontextProps=['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'];55.看意思,好像是context对象的一些默认属性,我翻了一下,发现好像也是lodash在使用root对象时,使用过的属性也都是这些。那么我们何不直接把这些属性方法给root扩展一个,看是不是可以解决:
//在定义root的后面加上这一句://扩展rootvarwxExtendsRoot=function(){root=typeofroot==='object'?root:{};root['Array']=typeofArray==='undefined'?undefined:Array;root['Buffer']=typeofBuffer==='undefined'?undefined:Buffer;root['DataView']=typeofDataView==='undefined'?undefined:DataView;root['Date']=typeofDate==='undefined'?undefined:Date;root['Error']=typeofError==='undefined'?undefined:Error;root['Float32Array']=typeofFloat32Array==='undefined'?undefined:Float32Array;root['Float64Array']=typeofFloat64Array==='undefined'?undefined:Float64Array;root['Function']=typeofFunction==='undefined'?undefined:Function;root['Int8Array']=typeofInt8Array==='undefined'?undefined:Int8Array;root['Int16Array']=typeofInt16Array==='undefined'?undefined:Int16Array;root['Int32Array']=typeofInt32Array==='undefined'?undefined:Int32Array;root['Map']=typeofMap==='undefined'?undefined:Map;root['Math']=typeofMath==='undefined'?undefined:Math;root['Object']=typeofObject==='undefined'?undefined:Object;root['Promise']=typeofPromise==='undefined'?undefined:Promise;root['RegExp']=typeofRegExp==='undefined'?undefined:RegExp;root['Set']=typeofSet==='undefined'?undefined:Set;root['String']=typeofString==='undefined'?undefined:String;root['Symbol']=typeofSymbol==='undefined'?undefined:Symbol;root['TypeError']=typeofTypeError==='undefined'?undefined:TypeError;root['Uint8Array']=typeofUint8Array==='undefined'?undefined:Uint8Array;root['Uint8ClampedArray']=typeofUint8ClampedArray==='undefined'?undefined:Uint8ClampedArray;root['Uint16Array']=typeofUint16Array==='undefined'?undefined:Uint16Array;root['Uint32Array']=typeofUint32Array==='undefined'?undefined:Uint32Array;root['WeakMap']=typeofWeakMap==='undefined'?undefined:WeakMap;root['_']=typeof_==='undefined'?undefined:_;root['clearTimeout']=typeofclearTimeout==='undefined'?undefined:clearTimeout;root['isFinite']=typeofisFinite==='undefined'?undefined:isFinite;root['parseInt']=typeofparseInt==='undefined'?undefined:parseInt;root['setTimeout']=typeofsetTimeout==='undefined'?undefined:setTimeout;}wxExtendsRoot();56.这样root上面也会拥有诸如:ArrayBuffersetTimeout这些属性方法。然后重新构建一下,验证一下:
import_from'lodash';constresult=_.map([1,2,3],(value={returnvalue+1;}))console.log(result);输出:[2,3,4]57.我们发现结果也是ok的,但是会不会出其他问题就不知道了,而且,如果你下载的是生产环境,压缩过的代码那估计谁都找不到那个地方,所以,比较鸡肋吧。
总结:58.如何使用微信小程序进行npm构建大致就到了这里,其实各位可以进行更多的尝试,然后一起交流,其实这个npm构建还是有点用的,比如你想要使用async/await语法,那你可以使用npm直接下载regenerator-runtime包,然后直接构建,直接在页面中导入就可以使用了,这样就不需要每次项目都要复制一份这个文件,还是不错的。虽然小程序在使用的npm包时,还有其他问题,不过喜欢新鲜事物的,可以尝试一下,并且期待一下他的更新,做的越来越会。
微信使用小程序推荐-微信小程序的npm使用心得-微信有哪些小程序
浏览量:1660
时间:
来源:农村的师傅
版权声明
即速应用倡导尊重与保护知识产权。如发现本站文章存在版权问题,烦请提供版权疑问、身份证明、版权证明、联系方式等发邮件至197452366@qq.com ,我们将及时处理。本站文章仅作分享交流用途,作者观点不等同于即速应用观点。用户与作者的任何交易与本站无关,请知悉。
最新资讯
-

即速应用,赋能企业玩转微信小程序智慧经营
作为国内领军的智慧商业经营服务商,即速应用始终秉承“让每个企业都拥有自己的智慧店铺”的愿景,持续赋能更多企业玩转智慧经营。即速应用旗下拥有“小程序搭建工具-即速应用”、“私域流量专家-即客云”等产品,帮助商家打通互联网全生态营销闭环。 -

即客云2.0重磅更新,让微信小程序运营更简单!
即客云作为一款基于企业微信的第三方工具,现从多维度提供超过30种功能,自上线以来,已服务多家企业,受到一致好评。近期,我们根据客户反馈和市场调研正式推出升级版 即客云2.0!更新了私域运营SOP,群日历功能,批量拓客,客户雷达,消息推送,个人欢迎语,帮助企业更好运用企业微信;同时提升了社群运营工作标准化,提升运营效率,帮助企业实现客户增长,玩转私域流量。 -

零代码 + AI 双轮驱动|即速应用解锁人工智能小程序开发新范式
无需代码、无需 AI 算法功底,普通人也能快速搭建智能小程序。即速应用将人工智能与零代码开发深度融合,推出 AI 智能生成能力,用户通过自然语言描述需求,AI 自动生成小程序页面、功能模块与后台配置,覆盖商城、预约、同城、社区团购等全场景。平台内置 AI 智能推荐、智能客服、用户画像分析等能力,一键对接微信生态,打通视频号、企业微信、短信跳转,帮企业快速落地 AI 应用,抢占智慧经营先机,让每家企业都拥有 AI 驱动的智慧店铺。












