- 你所不知道的JS模块
大家应该都JS模块化有一定的了解。主流规范有两种: CommonJS ES6 Module 其中,CommonJS是Node.js官方推荐的规范。常见关键字就是[代码]require[代码]和[代码]module.exports[代码] 而ES6 Module则是ESMAScript官方标准,常见关键字为[代码]import[代码]和[代码]export[代码] 日常开发中,这两种规范都可能用上,因此大家有可能会忽略这两种规范之间的细微差异。本文将讲解这两种规范的一个关键差异:值的引用 相同方式 即采用相同的CommonJS或ES6 Module的方式导出和引入模块。 CommonJS导出 [代码]// 输出out.js var a = 1; function add(){ a++; } module.exports = {a, add} //ES5输出 [代码] CommonJS引入 [代码]var out = require('./out.js'); //ES5输入 console.log('before:', out.a); // before:1 out.add(); console.log('after:', out.a); // after:1 [代码] 从上面的例子可以看出,通过CommonJS方式导出模块,再以同种方式引入模块,是不会改变原有模块变量的值。 我们再看看通过ES6 Module的方式导出和引入: ES6 Module导出 [代码]// 输出out.js var a = 1; function add(){ a++; } export {a, add} // ES6输出 [代码] ES6 Module引入 [代码]import {a, add} from './out.js'; //ES6输入 console.log('before:', a); // before:1 add(); console.log('after:', a); // after:2 [代码] 此时,ES6 Module的方式,就改变了原有模块的值。 不同方式 即模块的导出和引入采取不同的方式。 CommonJS导出 [代码]// 输出out.js var a = 1; function add(){ a++; } module.exports = {a, add} //ES5输出 [代码] ES6 Module引入 [代码]import {a, add} from './out.js'; //ES6输入 console.log('before:', a); // before:1 add(); console.log('after:', a); // after:1 [代码] 结果可见,模块的值不会被改变。 ES6 Module导出 [代码]// 输出out.js var a = 1; function add(){ a++; } export {a, add} // ES6输出 [代码] CommonJS引入 [代码]var out = require('./out.js'); //ES5输入 console.log('before:', out.a); // before:1 out.add(); console.log('after:', out.a); // after:2 [代码] 此时,模块的值又再次被改变了。综上四种情况,我们可以得出以下结论: CommonJS导出的a值都是不受原有的模块的代码影响的,永远都是1 而ES6 Module导出的b值则是随着原有模块的代码影响,执行add()函数之后,变成了2 总结 CommonJS导出的a值是模块里a的拷贝;而ES6 Module导出的a值就是模块里a的引用
2020-03-19 - 浅谈小程序路由的封装设计
微信官方提供了基础的路由能力,在日常的开发中虽已够用,但随着开发的深入,会遇到许多值得思考提炼的问题。本文将探讨作者在微信小程序(以下简称小程序)开发当中遇到的问题,以及解决方案设计。 参考“WHY-HOW-WHAT"黄金圈思维法则,首先讲述为什么小程序的路由需要封装设计,也就是存在哪些问题,需要封装处理? 存在的问题 路由跳转的路径与文件路径耦合 小程序的路由跳转使用的是真实文件路径,因此若文件的结构发生变化,必会影响到所有的页面的跳转路径。 笔者在实际开发中就遇到这个问题,以小程序分包举例。 小程序的分包是以文件夹为单位的。如果要将一系列的页面拆分成分包,则需要将这些文件移至同个目录之下,因此必然导致路由的跳转路径发生变更。如果此时路由跳转均是直接通过文件路径跳转的话,则需要全局改动,导致的工作量不少。 另外,当开发团队比较庞大时,不同的业务之间总会存在互相跳转的情况。当其中一个页面地址发生变更时,其他业务跳转到该页面的路径都需要手动变更。若此时通知不及时,或者遗漏了一些地方,导致跳转失败,终会酿成大错。 路由传参 目前小程序支持的传参方式,即通过跳转路径上的query查询参数。 通过query传参的问题,与在Web上URL传参是一致的,比如: query的参数长度有限 query只能传递可序列化的数据 导航前需手动序列化,到达目标页面后需反序列化 条件导航 在日常业务中,会存在一些页面需要一定条件才允许进入的。 举个例子,会员服务是一种很常见的能力,而会员中心的进入条件是: 该用户已经完成登录 该用户是本产品的会员 一般情况下,这有特定准入资格的页面的导航逻辑是这样的: [图片] 这种方式简单明了,但存在一个问题:需要每次跳转前主动判断,逻辑冗余以外,还可能被遗漏。 思路 由于小程序本身已提供了基础的路由导航能力,不像react、vue.js那样需要从底层进行封装,从而提供路由能力。但是,本质上小程序可以理解成类vue.js这样的框架,因此可以从vue.js的路由库vue-router上找到灵感,从而解决以上问题。 命名路由 使用 命名路由 的方式可以解决前文提及的跳转路径和文件真实路径耦合问题。 通过[代码]Map[代码]来映射 页面ID 和 页面地址,路由跳转时,仅能使用 页面ID 进行路由跳转。 下面以导航至首页举例: [代码]// before wx.switchTab('pages/home/index') // after router.go('home') [代码] 由于小程序有tab页面和普通页面之分,因此导航至tab页时需使用switchTab 细心的读者可能会发现上文使用了[代码]go[代码]方法,而不是[代码]switchTab[代码]。其实,具体哪些页面属于tab页面,在[代码]app.json[代码]已经明确配置。对于使用者来说,不需要关心跳转的页面是属于哪种类型,这些细节都应该统一在底层封装好。下面罗列[代码]Router[代码]与官方API的对应关系: [图片] Router API的设计原则是保持简单,以及尽量保持与web规范一致 传递参数 微信官方提供的query方式传参,若参数是普通数据类型(如[代码]Number[代码]、[代码]String[代码])时可以直接使用;但若是涉及到复杂数据类型(如[代码]Array[代码]、[代码]Object[代码])时,需要先做序列化处理,当数据较为庞大时,性能的损耗还是比较明显的。 因此,在内存上传递参数是比较便利且容易想到的办法。 利用数据字典,将[代码]页面ID[代码]作为[代码]key[代码]、传递的参数作为[代码]value[代码],写入[代码]Router[代码]的[代码]state[代码]: [代码]router.go = function(pageID, params) { // do something... router.state[pageID] = params } [代码] 在目标页面上,可以通过[代码]router.getParams()[代码]方法,获取传递的参数。 由于采用了命名路由的方式,可以使用[代码]页面ID[代码]作为[代码]key[代码],避免了使用跳转路径做[代码]key[代码]时,涉及到的绝对与相对路径问题。 条件导航 条件导航可以使用类似vue-router的导航守卫来解决问题。 由于路由的能力是微信官方提供的,因此无法像 vue-router 那样提供多类型的导航守卫,但仅有全局导航守卫也足够使用。 以下仍以“会员中心”的进入逻辑举例,并简要介绍实现思路: [图片] 其中,to和from目前是pageID,其实可以封装更多信息,以保证导航守卫可以尽可能拥有更多的信息。因此to可以理解成是即将进入的页面路由对象,而from则是当前正要离开的路由对象。 路由对象可以包含以下信息: [代码]pageID[代码]:页面ID [代码]path[代码]:页面ID对应的path [代码]params[代码]:传递的参数 [代码]query[代码]: URL的查询参数 配置信息 由前文提到的 命名路由 做法需要一个配置文件来关联[代码]页面ID[代码]与[代码]页面路径[代码]的关系。 页面的配置信息,则是使用[代码]router.config.js[代码]设置,然后通过构建工具编译转成[代码]app.json[代码]。 以下是[代码]route.config.js[代码]: [图片] 其中,跳转首页则是[代码]router.go('home')[代码];而跳转分包[代码]health[代码]的首页则是使用[代码]router.go('health.home')[代码] 通过以上的配置文件,使用构建工具转换成微信官方可识别的[代码]app.json[代码]配置: [图片] 辅助函数 在日常开发当中,经常会用到一些和路由相关的通用辅助函数,如获取当前页面,获取上个页面等。这些辅助函数都应该统一抽象封装,避免代码冗余。 [代码]router.utils = { getCurPage() { // 获取当前页面信息 let pages = getCurrentPages() let len = pages.length return pages[len - 1] }, getPrePage() {}, // 获取上个页面信息 getParams() {}, // 获取传递的参数 getPageID(path) {} // 通过path找到pageID } [代码] navigator组件 微信官方除了提供[代码]API[代码]用于导航以外,还提供了[代码]navigator[代码]组件。 另外还有[代码]functional-page-navigator[代码]是用于插件当中,不能在小程序包使用,因此本文暂且将其忽略。 由于[代码]navigator[代码]的跳转参数仍是使用[代码]path[代码],因此笔者将其进行二次封装,改造成可以通过[代码]pageID[代码]跳转: [图片] 总结 由于小程序相对比较封闭,因此在路由上能做的东西比较有限。 但路由又与许多概念有千丝万缕的关系。比如路由与文件结构关联,而文件结构又影响到分包的设计,环环相扣,影响到的地方则会越来越多。 因此,能提前看到本文提到的可能出现的问题,也许对后续的小程序开发有一定的参考意义。 另外,前文提到的很多问题,在早期开发,或者没有深入开发之前,都不会遇到。但是当你开始经历前文提到的那些问题时,往往此时的改造成本已经很大了。因此希望本文能给你带了一些启发,在早期规避这些问题,那本文的使命就达到了。
2020-06-15 - 论函数复用的几大姿势
开发过小程序的朋友们应该都遇到这样的情况,可能很多个页面有相同的函数,例如[代码]onShareAppMessage[代码],有什么最佳实践吗,应该如何处理呢? 本次开发技巧,我从以下几种解决办法剖析: 将它复制粘贴到每个地方(最烂的做法) 抽象成一个公共函数,每个[代码]Page[代码]都手动引用 提取一个behavior,每个页面手动注入 通过[代码]Page[代码]封装一个新的[代码]newPage[代码],以后每个页面都通过[代码]newPage[代码]注册 劫持Page函数,注入预设方法,页面仍可使用[代码]Page[代码]注册 复制粘贴大法 这是最直观,也是初学者最常用到的办法。也是作为工程师最不应该采取的办法。这有一个致命的问题,如果某一天,需要改动这个函数,岂不是要将所有的地方都翻出来改,所以这个办法直接否决。 抽象公共函数 这种方式,解决了复制粘贴大法的致命问题,不需要改动很多地方,只需要改动这个抽象出来的函数即可。但是其实,这个方式不便捷,每次新增页面都需要手动引入这个函数。 以下都通过[代码]onShareAppMessage[代码]方法举例。 假设在[代码]app.js[代码]通过[代码]global[代码]注册了[代码]onShareAppMessage[代码]方法: [代码]// app.js global.onShareAppMessage = function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } } [代码] 那么此时每次新增的Page都需要这样引入: [代码]// page.js Page({ ...global.onShareAppMessage, data: {} }) [代码] 这样的缺点也是非常明显的: 创建新页面时,容易遗忘 如果多个相同的函数,则需要每个独立引入,不方便 提取Behavior 将多个函数集成到一个对象中,每个页面只需要引入这个对象即可注入多个相同的函数。这种方式可以解决 抽象公共函数 提到的 缺点2。 大致的实现方式如下: 同样在[代码]app.js[代码]通过[代码]global[代码]注册一个[代码]behavior[代码]对象: [代码]// app.js global.commonPage = { onShareAppMessage: function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } }, onHide: function() { // do something } } [代码] 在新增的页面注入: [代码]// page.js Page({ data: {}, ...global.commonPage, }}) [代码] 缺点仍然是,新增页面时容易遗忘 封装新Page 封装新的[代码]Page[代码],然后每个页面都通过这个新的[代码]Page[代码]注册,而不是采用原有的[代码]Page[代码]。 同理,在[代码]app.js[代码]先封装一个新的[代码]Page[代码]到全局变量[代码]global[代码]: [代码]// app.js global.newPage = function(obj) { let defaultSet = { onShareAppMessage: function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } }, onShow() { // do something } } return Page({...defaultSet, ...obj}) } [代码] 往后在每个页面都使用新的[代码]newPage[代码]注册: [代码]// page.js global.newPage({ data: {} }) [代码] 好处即是全新封装了[代码]Page[代码],后续只需关注是否使用了新的[代码]Page[代码]即可;此外大家也很清晰知道这个是采用了新的封装,避免了覆盖原有的[代码]Page[代码]方法。 我倒是觉得没什么明显缺点,要是非要鸡蛋里挑骨头的话,就是要显式调用新的函数注册页面。 劫持Page 劫持函数其实是挺危险的做法,因为开发人员可能会在定位问题时,忽略了这个被劫持的地方。 劫持[代码]Page[代码]的做法,简单的说就是,覆盖[代码]Page[代码]这个函数,重新实现[代码]Page[代码],但这个新的[代码]Page[代码]内部仍会调用原有的[代码]Page[代码]。说起来可能有点拗口,通过代码看就一目了然: [代码]// app.js let originalPage = Page Page = function(obj) { let defaultSet = { onShareAppMessage: function() { return { title: '我在这里发现了很多好看的壁纸', path: 'pages/index/index', imageUrl: '' } }, onShow() { // do something } } return originalPage({ ...defaultSet, ...obj}) } [代码] 通过这种方式,不改变页面的注册方式,但可能会让不了解底层封装的开发者感到困惑:明明没注册的方法,怎么就自动注入了呢? 这种方式的缺点已经说了,优点也很明显,不改变任何原有的页面注册方式。 其实这个是一个挺好的思路,在一些特定的场景下,会有事半功倍的效果。
2020-03-23 - 小程序中使用 lodash
1、获取 lodash: 在 miniprogram 目录下 npm install -s lodash ** node_modules/ 不会被打包到目标包 2、复制 lodash 源码到 lib 目录 $ mkdir -p lib $ cp node_modules/lodash/lodash.min.js lib 3、编辑 app.js,添加以下内容 Object.assign(global, { Array : Array, Date : Date, Error : Error, Function : Function, Math : Math, Object : Object, RegExp : RegExp, String : String, TypeError : TypeError, setTimeout : setTimeout, clearTimeout : clearTimeout, setInterval : setInterval, clearInterval : clearInterval }); 4、引用 lodash 库 const _ = require(’./lib/lodash.min’);
2019-07-18