- 微信小程序对接国家网络身份认证公共服务APP,找不到wx.ncidas 相关api文档?
// 开始认证 handleAuthNetworkId() { const that = this const uuid = guid() console.log('uuid', uuid, that.platformConfig) wx.ncidas ({ orgID: that.platformConfig.orgId, //机构 ID ,业务接入申请时,分配给机构的唯一标识 businessID: that.platformConfig.appId, //应用 ID 业务接入申请时,分配给小程序的唯一标识,注意不同系统应用对应ID不同,安卓0001,IOS为0002 //由接入机构生成用于标识每笔业务的唯一性的业务序列号 bizSeq: uuid, //前端生成唯一标识UUID,在第4点,随返回的网证数据一起携带请求后端接口 type: 0, // 业务类型,0:网络身份认证凭证;1:网络身份认证凭证+口令; success (res) { console.log('wx.ncidas--success:', res);// 获取网证那些数据 }, fail(err) { console.log('wx.ncidas--fail:', err); }, }) },
08-22 - 小程序同层渲染原理剖析
众所周知,小程序当中有一类特殊的内置组件——原生组件,这类组件有别于 WebView 渲染的内置组件,他们是交由原生客户端渲染的。原生组件作为 Webview 的补充,为小程序带来了更丰富的特性和更高的性能,但同时由于脱离 Webview 渲染也给开发者带来了不小的困扰。在小程序引入「同层渲染」之前,原生组件的层级总是最高,不受 [代码]z-index[代码] 属性的控制,无法与 [代码]view[代码]、[代码]image[代码] 等内置组件相互覆盖, [代码]cover-view[代码] 和 [代码]cover-image[代码] 组件的出现一定程度上缓解了覆盖的问题,同时为了让原生组件能被嵌套在 [代码]swiper[代码]、[代码]scroll-view[代码] 等容器内,小程序在过去也推出了一些临时的解决方案。但随着小程序生态的发展,开发者对原生组件的使用场景不断扩大,原生组件的这些问题也日趋显现,为了彻底解决原生组件带来的种种限制,我们对小程序原生组件进行了一次重构,引入了「同层渲染」。 相信已经有不少开发者已经在日常的小程序开发中使用了「同层渲染」的原生组件,那么究竟什么是「同层渲染」?它背后的实现原理是怎样的?它是解决原生组件限制的银弹吗?本文将会为你一一解答这些问题。 什么是「同层渲染」? 首先我们先来了解一下小程序原生组件的渲染原理。我们知道,小程序的内容大多是渲染在 WebView 上的,如果把 WebView 看成单独的一层,那么由系统自带的这些原生组件则位于另一个更高的层级。两个层级是完全独立的,因此无法简单地通过使用 [代码]z-index[代码] 控制原生组件和非原生组件之间的相对层级。正如下图所示,非原生组件位于 WebView 层,而原生组件及 [代码]cover-view[代码] 与 [代码]cover-image[代码] 则位于另一个较高的层级: [图片] 那么「同层渲染」顾名思义则是指通过一定的技术手段把原生组件直接渲染到 WebView 层级上,此时「原生组件层」已经不存在,原生组件此时已被直接挂载到 WebView 节点上。你几乎可以像使用非原生组件一样去使用「同层渲染」的原生组件,比如使用 [代码]view[代码]、[代码]image[代码] 覆盖原生组件、使用 [代码]z-index[代码] 指定原生组件的层级、把原生组件放置在 [代码]scroll-view[代码]、[代码]swiper[代码]、[代码]movable-view[代码] 等容器内,通过 [代码]WXSS[代码] 设置原生组件的样式等等。启用「同层渲染」之后的界面层级如下图所示: [图片] 「同层渲染」原理 你一定也想知道「同层渲染」背后究竟采用了什么技术。只有真正理解了「同层渲染」背后的机制,才能更高效地使用好这项能力。实际上,小程序的同层渲染在 iOS 和 Android 平台下的实现不同,因此下面分成两部分来分别介绍两个平台的实现方案。 iOS 端 小程序在 iOS 端使用 WKWebView 进行渲染的,WKWebView 在内部采用的是分层的方式进行渲染,它会将 WebKit 内核生成的 Compositing Layer(合成层)渲染成 iOS 上的一个 WKCompositingView,这是一个客户端原生的 View,不过可惜的是,内核一般会将多个 DOM 节点渲染到一个 Compositing Layer 上,因此合成层与 DOM 节点之间不存在一对一的映射关系。不过我们发现,当把一个 DOM 节点的 CSS 属性设置为 [代码]overflow: scroll[代码] (低版本需同时设置 [代码]-webkit-overflow-scrolling: touch[代码])之后,WKWebView 会为其生成一个 [代码]WKChildScrollView[代码],与 DOM 节点存在映射关系,这是一个原生的 [代码]UIScrollView[代码] 的子类,也就是说 WebView 里的滚动实际上是由真正的原生滚动组件来承载的。WKWebView 这么做是为了可以让 iOS 上的 WebView 滚动有更流畅的体验。虽说 [代码]WKChildScrollView[代码] 也是原生组件,但 WebKit 内核已经处理了它与其他 DOM 节点之间的层级关系,因此你可以直接使用 WXSS 控制层级而不必担心遮挡的问题。 小程序 iOS 端的「同层渲染」也正是基于 [代码]WKChildScrollView[代码] 实现的,原生组件在 attached 之后会直接挂载到预先创建好的 [代码]WKChildScrollView[代码] 容器下,大致的流程如下: 创建一个 DOM 节点并设置其 CSS 属性为 [代码]overflow: scroll[代码] 且 [代码]-webkit-overflow-scrolling: touch[代码]; 通知客户端查找到该 DOM 节点对应的原生 [代码]WKChildScrollView[代码] 组件; 将原生组件挂载到该 [代码]WKChildScrollView[代码] 节点上作为其子 View。 [图片] 通过上述流程,小程序的原生组件就被插入到 [代码]WKChildScrollView[代码] 了,也即是在 [代码]步骤1[代码] 创建的那个 DOM 节点对应的原生 ScrollView 的子节点。此时,修改这个 DOM 节点的样式属性同样也会应用到原生组件上。因此,「同层渲染」的原生组件与普通的内置组件表现并无二致。 Android 端 小程序在 Android 端采用 chromium 作为 WebView 渲染层,与 iOS 不同的是,Android 端的 WebView 是单独进行渲染而不会在客户端生成类似 iOS 那样的 Compositing View (合成层),经渲染后的 WebView 是一个完整的视图,因此需要采用其他的方案来实现「同层渲染」。经过我们的调研发现,chromium 支持 WebPlugin 机制,WebPlugin 是浏览器内核的一个插件机制,主要用来解析和描述embed 标签。Android 端的同层渲染就是基于 [代码]embed[代码] 标签结合 chromium 内核扩展来实现的。 [图片] Android 端「同层渲染」的大致流程如下: WebView 侧创建一个 [代码]embed[代码] DOM 节点并指定组件类型; chromium 内核会创建一个 [代码]WebPlugin[代码] 实例,并生成一个 [代码]RenderLayer[代码]; Android 客户端初始化一个对应的原生组件; Android 客户端将原生组件的画面绘制到步骤2创建的 [代码]RenderLayer[代码] 所绑定的 [代码]SurfaceTexture[代码] 上; 通知 chromium 内核渲染该 [代码]RenderLayer[代码]; chromium 渲染该 [代码]embed[代码] 节点并上屏。 [图片] 这样就实现了把一个原生组件渲染到 WebView 上,这个流程相当于给 WebView 添加了一个外置的插件,如果你有留意 Chrome 浏览器上的 pdf 预览,会发现实际上它也是基于 [代码]<embed />[代码] 标签实现的。 这种方式可以用于 map、video、canvas、camera 等原生组件的渲染,对于 input 和 textarea,采用的方案是直接对 chromium 的组件进行扩展,来支持一些 WebView 本身不具备的能力。 对比 iOS 端的实现,Android 端的「同层渲染」真正将原生组件视图加到了 WebView 的渲染流程中且 embed 节点是真正的 DOM 节点,理论上可以将任意 WXSS 属性作用在该节点上。Android 端相对来说是更加彻底的「同层渲染」,但相应的重构成本也会更高一些。 「同层渲染」 Tips 通过上文我们已经了解了「同层渲染」在 iOS 和 Android 端的实现原理。Android 端的「同层渲染」是基于 chromium 内核开发的扩展,可以看成是 webview 的一项能力,而 iOS 端则需要在使用过程中稍加注意。以下列出了若干注意事项,可以帮助你避免踩坑: Tips 1. 不是所有情况均会启用「同层渲染」 需要注意的是,原生组件的「同层渲染」能力可能会在特定情况下失效,一方面你需要在开发时稍加注意,另一方面同层渲染失败会触发 [代码]bindrendererror[代码] 事件,可在必要时根据该回调做好 UI 的 fallback。根据我们的统计,目前同层失败率很低,也不需要太过于担心。 对 Android 端来说,如果用户的设备没有微信自研的 [代码]chromium[代码] 内核,则会无法切换至「同层渲染」,此时会在组件初始化阶段触发 [代码]bindrendererror[代码]。而 iOS 端的情况会稍复杂一些:如果在基础库创建同层节点时,节点发生了 WXSS 变化从而引起 WebKit 内核重排,此时可能会出现同层失败的现象。解决方法:应尽量避免在原生组件上频繁修改节点的 WXSS 属性,尤其要尽量避免修改节点的 [代码]position[代码] 属性。如需对原生组件进行变换,强烈推荐使用 [代码]transform[代码] 而非修改节点的 [代码]position[代码] 属性。 Tips 2. iOS 「同层渲染」与 WebView 渲染稍有区别 上文我们已经了解了 iOS 端同层渲染的原理,实际上,WebKit 内核并不感知原生组件的存在,因此并非所有的 WXSS 属性都可以在原生组件上生效。一般来说,定位 (position / margin / padding) 、尺寸 (width / height) 、transform (scale / rotate / translate) 以及层级 (z-index) 相关的属性均可生效,在原生组件外部的属性 (如 shadow、border) 一般也会生效。但如需对组件做裁剪则可能会失败,例如:[代码]border-radius[代码] 属性应用在父节点不会产生圆角效果。 Tips 3. 「同层渲染」的事件机制 启用了「同层渲染」之后的原生组件相比于之前的区别是原生组件上的事件也会冒泡,意味着,一个原生组件或原生组件的子节点上的事件也会冒泡到其父节点上并触发父节点的事件监听,通常可以使用 [代码]catch[代码] 来阻止原生组件的事件冒泡。 Tips 4. 只有子节点才会进入全屏 有别于非同层渲染的原生组件,像 [代码]video[代码] 和 [代码]live-player[代码] 这类组件进入全屏时,只有其子节点会被显示。 [图片] 总结 阅读本文之后,相信你已经对小程序原生组件的「同层渲染」有了更深入的理解。同层渲染不仅解决了原生组件的层级问题,同时也让原生组件有了更丰富的展示和交互的能力。下表列出的原生组件都已经支持了「同层渲染」,其他组件( textarea、camera、webgl 及 input)也会在近期逐步上线。现在你就可以试试用「同层渲染」来优化你的小程序了。 支持同层渲染的原生组件 最低版本 video v2.4.0 map v2.7.0 canvas 2d(新接口) v2.9.0 live-player v2.9.1 live-pusher v2.9.1
2019-11-21 - 小程序图片裁剪插件 image-cropper
之前的插件类目没有了导致搜不到了,重新发个文章。 image-cropper 一款高性能的小程序图片裁剪插件,支持旋转。 [图片] 优势 [代码]1.功能强大。[代码] [代码]2.性能超高超流畅,大图毫无卡顿感。[代码] [代码]3.组件化,使用简单。[代码] [代码]4.点击中间窗口实时查看裁剪结果。[代码] ㅤ 初始准备 1.json文件中添加image-cropper [代码] "usingComponents": { "image-cropper": "../image-cropper/image-cropper" }, "navigationBarTitleText": "裁剪图片", "disableScroll": true [代码] 2.wxml文件 [代码]<image-cropper id="image-cropper" limit_move="{{true}}" disable_rotate="{{true}}" width="{{width}}" height="{{height}}" imgSrc="{{src}}" bindload="cropperload" bindimageload="loadimage" bindtapcut="clickcut"></image-cropper> [代码] 3.简单示例 [代码] Page({ data: { src:'', width:250,//宽度 height: 250,//高度 }, onLoad: function (options) { //获取到image-cropper实例 this.cropper = this.selectComponent("#image-cropper"); //开始裁剪 this.setData({ src:"https://raw.githubusercontent.com/1977474741/image-cropper/dev/image/code.jpg", }); wx.showLoading({ title: '加载中' }) }, cropperload(e){ console.log("cropper初始化完成"); }, loadimage(e){ console.log("图片加载完成",e.detail); wx.hideLoading(); //重置图片角度、缩放、位置 this.cropper.imgReset(); }, clickcut(e) { console.log(e.detail); //点击裁剪框阅览图片 wx.previewImage({ current: e.detail.url, // 当前显示图片的http链接 urls: [e.detail.url] // 需要预览的图片http链接列表 }) }, }) [代码] 参数说明 属性 类型 缺省值 取值 描述 必填 imgSrc String 无 无限制 图片地址(如果是网络图片需配置安全域名) 否 disable_rotate Boolean false true/false 禁止用户旋转(为false时建议同时设置limit_move为false) 否 limit_move Boolean false true/false 限制图片移动范围(裁剪框始终在图片内)(为true时建议同时设置disable_rotate为true) 否 width Number 200 超过屏幕宽度自动转为屏幕宽度 裁剪框宽度 否 height Number 200 超过屏幕高度自动转为屏幕高度 裁剪框高度 否 max_width Number 300 裁剪框最大宽度 裁剪框最大宽度 否 max_height Number 300 裁剪框最大高度 裁剪框最大高度 否 min_width Number 100 裁剪框最小宽度 裁剪框最小宽度 否 min_height Number 100 裁剪框最小高度 裁剪框最小高度 否 disable_width Boolean false true/false 锁定裁剪框宽度 否 disable_height Boolean false true/false 锁定裁剪框高度 否 disable_ratio Boolean false true/false 锁定裁剪框比例 否 export_scale Number 3 无限制 输出图片的比例(相对于裁剪框尺寸) 否 quality Number 1 0-1 生成的图片质量 否 cut_top Number 居中 始终在屏幕内 裁剪框上边距 否 cut_left Number 居中 始终在屏幕内 裁剪框左边距 否 [代码]img_width[代码] Number 宽高都不设置,最小边填满裁剪框 支持%(不加单位为px)(只设置宽度,高度自适应) 图片宽度 否 [代码]img_height[代码] Number 宽高都不设置,最小边填满裁剪框 支持%(不加单位为px)(只设置高度,宽度自适应) 图片高度 否 scale Number 1 无限制 图片的缩放比 否 angle Number 0 (limit_move=true时angle=n*90) 图片的旋转角度 否 min_scale Number 0.5 无限制 图片的最小缩放比 否 max_scale Number 2 无限制 图片的最大缩放比 否 bindload Function null 函数名称 cropper初始化完成 否 bindimageload Function null 函数名称 图片加载完成,返回值Object{width,height,path,type等} 否 bindtapcut Function null 函数名称 点击中间裁剪框,返回值Object{src,width,height} 否 函数说明 函数名 参数 返回值 描述 参数必填 upload 无 无 调起wx上传图片接口并开始剪裁 否 pushImg src 无 放入图片开始裁剪 是 getImg Function(回调函数) [代码]Object{url,width,height}[代码] 裁剪并获取图片(图片尺寸 = 图片宽高 * export_scale) 是 setCutXY X、Y 无 设置裁剪框位置 是 setCutSize width、height 无 设置裁剪框大小 是 setCutCenter 无 无 设置裁剪框居中 否 setScale scale 无 设置图片缩放比例(不受min_scale、max_scale影响) 是 setAngle deg 无 设置图片旋转角度(带过渡效果) 是 setTransform {x,y,angle,scale,cutX,cutY} 无 图片在原有基础上的变化(scale受min_scale、max_scale影响) 根据需要传参 imgReset 无 无 重置图片的角度、缩放、位置(可以在onloadImage回调里使用) 否 GitHub https://github.com/wx-plugin/image-cropper/tree/master 如果有什么好的建议欢迎提issues或者提pr
2021-12-15 - html-canvas 生成小程序分享图
简介 基于 HTML 和 CSS 实现 Canvas 绘图。 项目地址 代码片段:https://developers.weixin.qq.com/s/9zFHKdmh7De2 原理 构建虚拟DOM 树,依据 CSS 规范计算样式,使用 CSS 盒模型对 DOM 进行布局,计算出所有元素的位置。最后将 DOM 树通过 Canvas Api 进行绘制。 小程序开发工具内运行 demo [代码]git clone https://github.com/alexayan/html-canvas.git npm i npm run watch [代码] 已支持的 CSS 属性 margin,margin-left,margin-top,margin-right,margin-bottom,padding,padding-left,padding-top,padding-right,padding-bottom,width,height,border,border-left,border-top,border-right,border-bottom,border-width,border-style,border-color,border-left-style,border-left-color,border-left-width,border-top-style,border-top-color,border-top-width,border-right-style,border-right-color,border-right-width,border-bottom-style,border-bottom-color,border-bottom-width,color,display,background-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-left-radius,border-bottom-right-radius,box-sizing,font,font-style,font-variant,font-weight,font-stretch,font-size,line-height,font-family,text-align,position,overflow,overflow-x,overflow-y,top,left,right,bottom,z-index demo canvas-draw.html [图片]
2020-01-08 - 微信小程序UI组件库合集
UI组件库合集,大家有遇到好的组件库,欢迎留言评论然后加入到文档里。 第一款: 官方WeUI组件库,地址 https://developers.weixin.qq.com/miniprogram/dev/extended/weui/ 预览码: [图片] 第二款: ColorUI:地址 https://github.com/weilanwl/ColorUI 预览码: [图片] 第三款: vantUI(又名:ZanUI):地址 https://youzan.github.io/vant-weapp/#/intro 预览码: [图片] 第四款: MinUI: 地址 https://meili.github.io/min/docs/minui/index.html 预览码: [图片] 第五款: iview-weapp:地址 https://weapp.iviewui.com/docs/guide/start 预览码: [图片] 第六款: WXRUI:暂无地址 预览码: [图片] 第七款: WuxUI:地址https://www.wuxui.com/#/introduce 预览码: [图片] 第八款: WussUI:地址 https://phonycode.github.io/wuss-weapp/quickstart.html 预览码: [图片] 第九款: TouchUI:地址 https://github.com/uileader/touchwx 预览码: [图片] 第十款: Hello UniApp: 地址 https://m3w.cn/uniapp 预览码: [图片] 第十一款: TaroUI:地址 https://taro-ui.jd.com/#/docs/introduction 预览码: [图片] 第十二款: Thor UI: 地址 https://thorui.cn/doc/ 预览码: [图片] 第十三款: GUI:https://github.com/Gensp/GUI 预览码: [图片] 第十四款: QyUI:暂无地址 预览码: [图片] 第十五款: WxaUI:暂无地址 预览码: [图片] 第十六款: kaiUI: github地址 https://github.com/Chaunjie/kai-ui 组件库文档:https://chaunjie.github.io/kui/dist/#/start 预览码: [图片] 第十七款: YsUI:暂无地址 预览码: [图片] 第十八款: BeeUI:git地址 http://ued.local.17173.com/gitlab/wxc/beeui.git 预览码: [图片] 第十九款: AntUI: 暂无地址 预览码: [图片] 第二十款: BleuUI:暂无地址 预览码: [图片] 第二十一款: uniydUI:暂无地址 预览码: [图片] 第二十二款: RovingUI:暂无地址 预览码: [图片] 第二十三款: DojayUI:暂无地址 预览码: [图片] 第二十四款: SkyUI:暂无地址 预览码: [图片] 第二十五款: YuUI:暂无地址 预览码: [图片] 第二十六款: wePyUI:暂无地址 预览码: [图片] 第二十七款: WXDUI:暂无地址 预览码: [图片] 第二十八款: XviewUI:暂无地址 预览码: [图片] 第二十九款: MinaUI:暂无地址 预览码: [图片] 第三十款: InyUI:暂无地址 预览码: [图片] 第三十一款: easyUI:地址 https://github.com/qq865738120/easyUI 预览码: [图片] 第三十二款 Kbone-UI: 地址 https://wechat-miniprogram.github.io/kboneui/ui/#/ 暂无预览码 第三十三款 VtuUi: 地址 https://github.com/jisida/VtuWeapp 预览码: [图片] 第三十四款 Lin-UI 地址:http://doc.mini.talelin.com/ 预览码: [图片] 第三十五款 GraceUI 地址: http://grace.hcoder.net/ 这个是收费的哦~ 预览码: [图片] 第三十六款 anna-remax-ui npm:https://www.npmjs.com/package/anna-remax-ui/v/1.0.12 anna-remax-ui 地址: https://annasearl.github.io/anna-remax-ui/components/general/button 预览码 [图片] 第三十七款 Olympus UI 地址:暂无 网易严选出品。 预览码 [图片] 第三十八款 AiYunXiaoUI 地址暂无 预览码 [图片] 第三十九款 visionUI npm:https://www.npmjs.com/package/vision-ui 预览码: [图片] 第四十款 AnimaUI(灵动UI) 地址:https://github.com/AnimaUI/wechat-miniprogram 预览码: [图片] 第四十一款 uView 地址:http://uviewui.com/components/quickstart.html 预览码: [图片] 第四十二款 firstUI 地址:https://www.firstui.cn/ 预览码: [图片]
2023-01-10 - 实现一个本地数据库
问题背景 对于一些变化性不强但可能多次使用的数据(如一些列表、文章信息等),如果每次都进行网络请求,不仅减慢了速度,也加重了服务器负担;这时往往需要通过 [代码]setStorage[代码] 和 [代码]getStorage[代码] 进行本地缓存,但也存在一些问题,一个是 [代码]storage[代码] 只能通过 [代码]key-value[代码] 的形式进行管理,无法进行更复杂的数据库操作,另外每次都从本地 [代码]storage[代码] 中读写效率不高 改进方式 改进读写方式 在 [代码]init[代码] 的时候读取本地 [代码]storage[代码],之后所有读写都在这个变量中进行操作,不必每次读取本地 [代码]storage[代码];在进行写入操作后,定期写回本地 [代码]storage[代码],也可以减少写入次数 存储结构 一个集合由一个 [代码]object[代码] 组成,里面的每一个键值对表示一个记录,通过 [代码]id[代码] 查询效率高([代码]id[代码] 可以自动生成,默认为 [代码]4[代码] 位由数字字母组成的随机值),通过 [代码]where[代码] 查询则需要遍历 实现数据库的 api 为减少上手难度,所有 [代码]api[代码] 都参照了 云数据库 的设置,几乎所有的方法都可以直接使用(但由于本地数据库存取都较快,所有方法都是同步方法,直接返回结果),通过这些 [代码]api[代码],可以大大便利数据的查询和设置 例程 [代码]const localDB = require('utils/localDB.js') const _ = localDB.command localDB.init() // 初始化 var articles = localDB.collection('articles') if(!articles) articles = localDB.createCollection('articles') // 不存在则先创建 // 按文章 id 查找 var doc = articles.doc('xxx') if(doc) { var data = doc.get() // 取得数据 } else { // 网络请求获取 data data._timeout = Date.now() + 15 * 24 * 3600000 // 设置过期时间为 15 天 articles.add(data) // 添加到本地数据库 } // 按类型查找 var data = articles.where({ type: 'xxx' }).get() // 正则查找 var data = articles.where({ title: /xxx/ // 标题中含有 xxx 的 }).get() // 分页查找 var page2 = articles.skip(10).limit(10).get() // 按时间查找 var data = articles.where({ date: _.gte('20200501').and(_.lte('20200510')) // 大于等于 20200501 小于等于 20200510 }).get() // 结果排序 var data = articles.orderBy('date', 'desc').get() // 按日期降序排序 // 清理过期数据 articles.where({ _timeout: _.lt(Date.now()) // 过期时间早于当前的 }).remove() [代码] github MpLocalDB
2020-05-09 - 如何写一个自己的脚手架 - 一键初始化项目
如何写一个自己的脚手架 - 一键初始化项目 介绍 脚手架的作用:为减少重复性工作而做的重复性工作 即为了开发中的:编译 es6,js 模块化,压缩代码,热更新等功能,我们使用[代码]webpack[代码]等打包工具,但是又带来了新的问题:初始化工程的麻烦,复杂的[代码]webpack[代码]配置,以及各种配置文件,所以就有了一键生成项目,0 配置开发的脚手架 本文项目代码地址 本文以我司的脚手架工具 简化之后为基础 本系列分 3 篇,详细介绍如何实现一个脚手架: 一键初始化项目 0 配置开发环境与打包 一键上传服务器 首先说一下个人的开发习惯 在写功能前我会先把调用方式写出了,然后一步一步的从使用者的角度写,现将基础功能写好后,慢慢完善 例如一键初始化项目功能 我期望的就是 在命令行执行输入 [代码]my-cli create text-project[代码],回车后直接创建项目并生成模板,还会把依赖都下载好 我们下面就从命令行开始入手 创建项目 [代码]my-cli[代码],执行 [代码]npm init -y[代码]快速初始化 bin [代码]my-cli[代码]: 在 [代码]package.json[代码] 中加入: [代码]{ "bin": { "my-cli": "bin.js" } } [代码] [代码]bin.js[代码]: [代码]#!/usr/bin/env node console.log(process.argv); [代码] [代码]#!/usr/bin/env node[代码],这一行是必须加的,就是让系统动态的去[代码]PATH[代码]目录中查找[代码]node[代码]来执行你的脚本文件。 命令行执行 [代码]npm link[代码] ,创建软链接至全局,这样我们就可以全局使用[代码]my-cli[代码]命令了,在开发 [代码]npm[代码] 包的前期都会使用[代码]link[代码]方式在其他项目中测试来开发,后期再发布到[代码]npm[代码]上 命令行执行 [代码]my-cli 1 2 3[代码] 输出:[代码][ '/usr/local/bin/node', '/usr/local/bin/my-cli', '1', '2', '3' ][代码] 这样我们就可以获取到用户的输入参数 例如[代码]my-cli create test-project[代码] 我们就可以通过数组第 [2] 位判断命令类型[代码]create[代码],通过第 [3] 位拿到项目名称[代码]test-project[代码] commander [代码]node[代码]的命令行解析最常用的就是[代码]commander[代码]库,来简化复杂[代码]cli[代码]参数操作 (我们现在的参数简单可以不使用[代码]commander[代码],直接用[代码]process.argv[3][代码]获取名称,但是为了之后会复杂的命令行,这里也先使用[代码]commander[代码]) [代码]#!/usr/bin/env node const program = require("commander"); const version = require("./package.json").version; program.version(version, "-v, --version"); program .command("create <app-name>") .description("使用 my-cli 创建一个新的项目") .option("-d --dir <dir>", "创建目录") .action((name, command) => { const create = require("./create/index"); create(name, command); }); program.parse(process.argv); [代码] [代码]commander[代码] 解析完成后会触发[代码]action[代码]回调方法 命令行执行:[代码]my-cli -v[代码] 输出:[代码]1.0.0[代码] 命令行执行: [代码]my-cli create test-project[代码] 输出:[代码]test-project[代码] 创建项目 拿到了用户传入的名称,就可以用这么名字创建项目 我们的代码尽量保持[代码]bin.js[代码]整洁,不将接下来的代码写在[代码]bin.js[代码]里,创建[代码]create[代码]文件夹,创建[代码]index.js[代码]文件 [代码]create/index.js[代码]中: [代码]const path = require("path"); const mkdirp = require("mkdirp"); module.exports = function(name) { mkdirp(path.join(process.cwd(), name), function(err) { if (err) console.error("创建失败"); else console.log("创建成功"); }); }; [代码] [代码]process.cwd()[代码]获取工作区目录,和用户传入项目名称拼接起来 (创建文件夹我们使用[代码]mkdirp[代码]包,可以避免我们一级一级的创建目录) 修改[代码]bin.js[代码]的[代码]action[代码]方法: [代码]// bin.js .action(name => { const create = require("./create") create(name) }); [代码] 命令行执行: [代码]my-cli create test-project[代码] 输出:[代码]创建成功[代码] 并在命令行所在目录创建了一个[代码]test-project[代码]文件夹 模板 首先需要先列出我们的模板包含哪些文件 一个最基础版的[代码]vue[代码]项目模板: [代码]|- src |- main.js |- App.vue |- components |- HelloWorld.vue |- index.html |- package.json [代码] 这些文件就不一一介绍了 我们需要的就是生成这些文件,并写入到目录中去 模板的写法后很多种,下面是我的写法: 模板目录: [代码]|- generator |- index-html.js |- package-json.js |- main.js |- App-vue.js |- HelloWorld-vue.js [代码] [代码]generator/index-html.js[代码] 模板示例: [代码]module.exports = function(name) { const template = ` { "name": "${name}", "version": "1.0.0", "description": "", "main": "index.js", "scripts": {}, "devDependencies": { }, "author": "", "license": "ISC", "dependencies": { "vue": "^2.6.10" } } `; return { template, dir: "", name: "package.json" }; }; [代码] [代码]dir[代码]就是目录,例如[代码]main.js[代码]的[代码]dir[代码]就是[代码]src[代码] [代码]create/index.js[代码]在[代码]mkdirp[代码]中新增: [代码]const path = require("path"); const mkdirp = require("mkdirp"); const fs = require("fs"); module.exports = function(name) { const projectDir = path.join(process.cwd(), name); mkdirp(projectDir, function(err) { if (err) console.error("创建失败"); else { console.log(`创建${name}文件夹成功`); const { template, dir, name: fileName } = require("../generator/package")(name); fs.writeFile(path.join(projectDir, dir, fileName), template.trim(), function(err) { if (err) console.error(`创建${fileName}文件失败`); else { console.log(`创建${fileName}文件成功`); } }); } }); }; [代码] 这里只写了一个模板的创建,我们可以用[代码]readdir[代码]来获取目录下所有文件来遍历执行 下载依赖 我们平常下载[代码]npm[代码]包都是使用命令行 [代码]npm install / yarn install[代码] 这时就需要用到 [代码]node[代码] 的 [代码]child_process.spawn[代码] api 来调用系统命令 因为考虑到跨平台兼容处理,所以使用 cross-spawn 库,来帮我们兼容的操作命令 我们创建[代码]utils[代码]文件夹,创建[代码]install.js[代码] [代码]utils/install.js[代码]: [代码]const spawn = require("cross-spawn"); module.exports = function install(options) { const cwd = options.cwd || process.cwd(); return new Promise((resolve, reject) => { const command = options.isYarn ? "yarn" : "npm"; const args = ["install", "--save", "--save-exact", "--loglevel", "error"]; const child = spawn(command, args, { cwd, stdio: ["pipe", process.stdout, process.stderr] }); child.once("close", code => { if (code !== 0) { reject({ command: `${command} ${args.join(" ")}` }); return; } resolve(); }); child.once("error", reject); }); }; [代码] 然后我们就可以在创建完模板后调用[代码]install[代码]方法下载依赖 [代码]install({ cwd: projectDir }); [代码] 要知道工作区为我们项目的目录 至此,解析 cli,创建目录,创建模板,下载依赖一套流程已经完成 基本功能都跑通之后下面就是要填充剩余代码和优化 优化 当代码写的多了之后,我们看上面[代码]create[代码]方法内的回调嵌套回调会非常难受 [代码]node 7[代码]已经支持[代码]async,await[代码],所以我们将上面代码改成[代码]Promise[代码] 在[代码]utils[代码]目录下创建,[代码]promisify.js[代码]: [代码]module.exports = function promisify(fn) { return function(...args) { return new Promise(function(resolve, reject) { fn(...args, function(err, ...res) { if (err) return reject(err); if (res.length === 1) return resolve(res[0]); resolve(res); }); }); }; }; [代码] 这个方法帮我们把回调形式的[代码]Function[代码]改成[代码]Promise[代码] 在[代码]utils[代码]目录下创建,[代码]fs.js[代码]: [代码]const fs = require(fs); const promisify = require("./promisify"); const mkdirp = require("mkdirp"); exports.writeFile = promisify(fs.writeFile); exports.readdir = promisify(fs.readdir); exports.mkdirp = promisify(mkdirp); [代码] 将[代码]fs[代码]和[代码]mkdirp[代码]方法改造成[代码]promise[代码] 改造后的[代码]create.js[代码]: [代码]const path = require("path"); const fs = require("../utils/fs-promise"); const install = require("../utils/install"); module.exports = async function(name) { const projectDir = path.join(process.cwd(), name); await fs.mkdirp(projectDir); console.log(`创建${name}文件夹成功`); const { template, dir, name: fileName } = require("../generator/package")(name); await fs.writeFile(path.join(projectDir, dir, fileName), template.trim()); console.log(`创建${fileName}文件成功`); install({ cwd: projectDir }); }; [代码] 结语 关于进一步优化: 更多功能与健壮 例如指定目录创建项目,目录不存在等情况 [代码]chalk[代码]和[代码]ora[代码]优化[代码]log[代码],给用户更好的反馈 通过[代码]inquirer[代码]问询用户得到更多的选择:模板[代码]vue-router[代码],[代码]vuex[代码]等更多初始化模板功能,[代码]eslint[代码] 更多的功能: 内置 webpack 配置 一键发布服务器 其实要学会善用第三方库,你会发现我们上面的每个模块都有第三方库的身影,我们只是将这些功能组装起来,再结合我们的想法进一步封装 虽然有[代码]vue-cli[代码],[代码]create-react-app[代码]这些已有的脚手架,但是我们还是可能在某些情况下需要自己实现脚手架部分功能,根据公司的业务来封装,减少重复性工作,或者了解一下内部原理
2019-09-26 - [有点炫]自定义navigate+分包+自定义tabbar
自定义navigate+分包+自定义tabbar,有需要的可以拿去用用,可能会存在一些问题,根据自己的业务改改吧 大家也可以多多交流 代码片段:在这里 {"version":"1.1.5","update":[{"title":"修复 [复制代码片段提示] 无法使用的问题","date":"2020-06-15 09:20","imgs":[]}]} 更新日志: 2019-11-25 自定义navigate 也可以调用wx.showNavigationBarLoading 和 wx.hideNavigationBarLoading 2019-11-25 页面滚动条显示在自定义navigate 和 自定义tabbar上面的问题(点击“体验custom Tabbar” [图片] [图片] 其他demo: 云开发之微信支付:代码片段
2020-06-15 - 深入 CommonJs 与 ES6 Module
目前主流的模块规范 UMD CommonJs es6 module umd 模块(通用模块) [代码](function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global.libName = factory()); }(this, (function () { 'use strict';}))); [代码] 如果你在[代码]js[代码]文件头部看到这样的代码,那么这个文件使用的就是 [代码]UMD[代码] 规范 实际上就是 amd + commonjs + 全局变量 这三种风格的结合 这段代码就是对当前运行环境的判断,如果是 [代码]Node[代码] 环境 就是使用 [代码]CommonJs[代码] 规范, 如果不是就判断是否为 [代码]AMD[代码] 环境, 最后导出全局变量 有了 [代码]UMD[代码] 后我们的代码和同时运行在 [代码]Node[代码] 和 [代码]浏览器上[代码] 所以现在前端大多数的库最后打包都使用的是 [代码]UMD[代码] 规范 CommonJs [代码]Nodejs[代码] 环境所使用的模块系统就是基于[代码]CommonJs[代码]规范实现的,我们现在所说的[代码]CommonJs[代码]规范也大多是指[代码]Node[代码]的模块系统 模块导出 关键字:[代码]module.exports[代码] [代码]exports[代码] [代码]// foo.js //一个一个 导出 module.exports.age = 1 module.exports.foo = function(){} exports.a = 'hello' //整体导出 module.exports = { age: 1, a: 'hello', foo:function(){} } //整体导出不能用`exports` 用exports不能在导入的时候使用 exports = { age: 1, a: 'hello', foo:function(){} } [代码] 这里需要注意 [代码]exports[代码] 不能被赋值,可以理解为在模块开始前[代码]exports = module.exports[代码], 因为赋值之后[代码]exports[代码]失去了 对[代码]module.exports[代码]的引用,成为了一个模块内的局部变量 模块导入 关键字:[代码]require[代码] [代码]const foo = require('./foo.js') console.log(foo.age) //1 [代码] 模块导入规则: 假设以下目录为 [代码]src/app/index.js[代码] 的文件 调用 [代码]require()[代码] [代码]./moduleA[代码] 相对路径开头 在没有指定后缀名的情况下 先去寻找同级目录同级目录:[代码]src/app/[代码] [代码]src/app/moduleA[代码] 无后缀名文件 按照[代码]javascript[代码]解析 [代码]src/app/moduleA.js[代码] js文件 按照[代码]javascript[代码]解析 [代码]src/app/moduleA.json[代码] json文件 按照[代码]json[代码]解析 [代码]src/app/moduleA.node[代码] node文件 按照加载的编译插件模块dlopen 同级目录没有 [代码]moduleA[代码] 文件会去找同级的 [代码]moduleA[代码]目录:[代码]src/app/moduleA[代码] [代码]src/app/moduleA/package.json[代码] 判断该目录是否有[代码]package.json[代码]文件, 如果有 找到[代码]main[代码]字段定义的文件返回, 如果 [代码]main[代码] 字段指向文件不存在 或 [代码]main[代码]字段不存在 或 [代码]package.json[代码]文件不存在向下执行 [代码]src/app/moduleA/index.js[代码] [代码]src/app/moduleA/index.json[代码] [代码]src/app/moduleA/index.node[代码] 结束 [代码]/module/moduleA[代码] 绝对路径开头 直接在[代码]/module/moduleA[代码]目录中寻找 规则同上 [代码]react[代码] 没有路径开头 没有路径开头则视为导入一个包 会先判断[代码]moduleA[代码]是否是一个核心模块 如[代码]path[代码],[代码]http[代码],优先导入核心模块 不是核心模块 会从当前文件的同级目录的[代码]node_modules[代码]寻找 [代码]/src/app/node_modules/[代码] 寻找规则同上 以导入[代码]react[代码]为例 [代码]先 node_modules 下 react 文件 -> react.js -> react.json -> react.node ->react目录 -> react package.json main -> index.js -> index.json -> index.node[代码] 如果没找到 继续向父目录的[代码]node_modules[代码]中找 [代码]/src/node_modules/[代码] [代码]/node_modules/[代码] 直到最后找不到 结束 [代码]require wrapper[代码] [代码]Node[代码]的模块 实际上可以理解为代码被包裹在一个[代码]函数包装器[代码]内 一个简单的[代码]require demo[代码]: [代码]function wrapper (script) { return '(function (exports, require, module, __filename, __dirname) {' + script + '\n})' } function require(id) { var cachedModule = Module._cache[id]; if(cachedModule){ return cachedModule.exports; } const module = { exports: {} } // 这里先将引用加入缓存 后面循环引用会说到 Module._cache[id] = module //当然不是eval这么简单 eval(wrapper('module.exports = "123"'))(module.exports, require, module, 'filename', 'dirname') return module.exports } [代码] 也可以查看:node module 源码 从以上代码我们可以知道: 模块只执行一次 之后调用获取的 [代码]module.exports[代码] 都是缓存哪怕这个 [代码]js[代码] 还没执行完毕(因为先加入缓存后执行模块) 模块导出就是[代码]return[代码]这个变量的其实跟[代码]a = b[代码]赋值一样, 基本类型导出的是值, 引用类型导出的是引用地址 [代码]exports[代码] 和 [代码]module.exports[代码] 持有相同引用,因为最后导出的是 [代码]module.exports[代码], 所以对[代码]exports[代码]进行赋值会导致[代码]exports[代码]操作的不再是[代码]module.exports[代码]的引用 循环引用 [代码]// a.js module.exports.a = 1 var b = require('./b') console.log(b) module.exports.a = 2 // b.js module.exports.b = 11 var a = require('./a') console.log(a) module.exports.b = 22 //main.js var a = require('./a') console.log(a) [代码] 运行此段代码结合上面的[代码]require demo[代码],分析每一步过程: [代码]执行 node main.js -> 第一行 require(a.js)[代码],([代码]node[代码] 执行也可以理解为调用了require方法,我们省略[代码]require(main.js)[代码]内容) [代码]进入 require(a)方法: 判断缓存(无) -> 初始化一个 module -> 将 module 加入缓存 -> 执行模块 a.js 内容[代码],(需要注意 是先加入缓存, 后执行模块内容) [代码]a.js: 第一行导出 a = 1 -> 第二行 require(b.js)[代码](a 只执行了第一行) [代码]进入 require(b) 内 同 1 -> 执行模块 b.js 内容[代码] [代码]b.js: 第一行 b = 11 -> 第二行 require(a.js)[代码] [代码]require(a) 此时 a.js 是第二次调用 require -> 判断缓存(有)-> cachedModule.exports -> 回到 b.js[代码](因为[代码]js[代码]对象引用问题 此时的 [代码]cachedModule.exports = { a: 1 }[代码]) [代码]b.js:第三行 输出 { a: 1 } -> 第四行 修改 b = 22 -> 执行完毕回到 a.js[代码] [代码]a.js:第二行 require 完毕 获取到 b -> 第三行 输出 { b: 22 } -> 第四行 导出 a = 2 -> 执行完毕回到 main.js[代码] [代码]main.js:获取 a -> 第二行 输出 { a: 2 } -> 执行完毕[代码] 以上就是[代码]node[代码]的[代码]module[代码]模块解析和运行的大致规则 es6 module [代码]ES6[代码] 之前 [代码]javascript[代码] 一直没有属于自己的模块规范,所以社区制定了 [代码]CommonJs[代码]规范, [代码]Node[代码] 从 [代码]Commonjs[代码] 规范中借鉴了思想于是有了 [代码]Node[代码] 的 [代码]module[代码],而 [代码]AMD 异步模块[代码] 也同样脱胎于 [代码]Commonjs[代码] 规范,之后有了运行在浏览器上的 [代码]require.js[代码] [代码]es6 module[代码] 基本语法: export [代码]export * from 'module'; //重定向导出 不包括 module内的default export { name1, name2, ..., nameN } from 'module'; // 重定向命名导出 export { import1 as name1, import2 as name2, ..., nameN } from 'module'; // 重定向重命名导出 export { name1, name2, …, nameN }; // 与之前声明的变量名绑定 命名导出 export { variable1 as name1, variable2 as name2, …, nameN }; // 重命名导出 export let name1 = 'name1'; // 声明命名导出 或者 var, const,function, function*, class export default expression; // 默认导出 export default function () { ... } // 或者 function*, class export default function name1() { ... } // 或者 function*, class export { name1 as default, ... }; // 重命名为默认导出 [代码] [代码]export[代码] 规则 [代码]export * from ''[代码] 或者 [代码]export {} from ''[代码],重定向导出,重定向的命名并不能在本模块使用,只是搭建一个桥梁,例如:这个[代码]a[代码]并不能在本模块内使用 [代码]export {}[代码], 与变量名绑定,命名导出 [代码]export Declaration[代码],声明的同时,命名导出, Declaration就是: [代码]var[代码], [代码]let[代码], [代码]const[代码], [代码]function[代码], [代码]function*[代码], [代码]class[代码] 这一类的声明语句 [代码]export default AssignmentExpression[代码],默认导出, AssignmentExpression的 范围很广,可以大致理解 为除了声明[代码]Declaration[代码](其实两者是有交叉的),[代码]a=2[代码],[代码]i++[代码],[代码]i/4[代码],[代码]a===b[代码],[代码]obj[name][代码],[代码]name in obj[代码],[代码]func()[代码],[代码]new P()[代码],[代码][1,2,3][代码],[代码]function(){}[代码]等等很多 import [代码]// 命名导出 module.js let a = 1,b = 2 export { a, b } export let c = 3 // 命名导入 main.js import { a, b, c } from 'module'; // a: 1 b: 2 c: 3 import { a as newA, b, c as newC } from 'module'; // newA: 1 b: 2 newC: 3 // 默认导出 module.js export default 1 // 默认导入 main.js import defaultExport from 'module'; // defaultExport: 1 // 混合导出 module.js let a = 1 export { a } const b = 2 export { b } export let c = 3 export default [1, 2, 3] // 混合导入 main.js import defaultExport, { a, b, c as newC} from 'module'; //defaultExport: [1, 2, 3] a: 1 b: 2 newC: 3 import defaultExport, * as name from 'module'; //defaultExport: [1, 2, 3] name: { a: 1, b: 2, c: 3 } import * as name from 'module'; // name: { a: 1, b: 2, c: 3, default: [1, 2, 3] } // module.js Array.prototype.remove = function(){} //副作用 只运行一个模块 import 'module'; // 执行module 不导出值 多次调用module.js只运行一次 //动态导入(异步导入) var promise = import('module'); [代码] [代码]import[代码] 规则 [代码]import { } from 'module'[代码], 导入[代码]module.js[代码]的命名导出 [代码]import defaultExport from 'module'[代码], 导入[代码]module.js[代码]的默认导出 [代码]import * as name from 'module'[代码], 将[代码]module.js的[代码]的所有导出合并为[代码]name[代码]的对象,[代码]key[代码]为导出的命名,默认导出的[代码]key[代码]为[代码]default[代码] [代码]import 'module'[代码],副作用,只是运行[代码]module[代码],不为了导出内容例如 polyfill,多次调用次语句只能执行一次 [代码]import('module')[代码],动态导入返回一个 [代码]Promise[代码],[代码]TC39[代码]的[代码]stage-3[代码]阶段被提出 tc39 import [代码]ES6 module[代码] 特点 [代码]ES6 module[代码]的语法是静态的 [代码]import[代码] 会自动提升到代码的顶层 [代码]export[代码] 和 [代码]import[代码] 只能出现在代码的顶层,下面这段语法是错误的 [代码] //if for while 等都无法使用 { export let a = 1 import defaultExport from 'module' } true || export let a = 1 [代码] [代码]import[代码] 的导入名不能为字符串或在判断语句,下面代码是错误的 [代码]import 'defaultExport' from 'module' let name = 'Export' import 'default' + name from 'module' [代码] 静态的语法意味着可以在编译时确定导入和导出,更加快速的查找依赖,可以使用[代码]lint[代码]工具对模块依赖进行检查,可以对导入导出加上类型信息进行静态的类型检查 ####[代码]ES6 module[代码]的导出是绑定的 #### 使用 [代码]import[代码] 被导入的模块运行在严格模式下 使用 [代码]import[代码] 被导入的变量是只读的,可以理解默认为 [代码]const[代码] 装饰,无法被赋值 使用 [代码]import[代码] 被导入的变量是与原变量绑定/引用的,可以理解为 [代码]import[代码] 导入的变量无论是否为基本类型都是引用传递 [代码]// js中 基础类型是值传递 let a = 1 let b = a b = 2 console.log(a,b) //1 2 // js中 引用类型是引用传递 let obj = {name:'obj'} let obj2 = obj obj2.name = 'obj2' console.log(obj.name, obj2.name) // obj2 obj2 // es6 module 中基本类型也按引用传递 // foo.js export let a = 1 export function count(){ a++ } // main.js import { a, count } from './foo' console.log(a) //1 count() console.log(a) //2 // export default 是无法 a 的动态绑定 这一点跟 CommonJs 有点相似 都是值的拷贝 let a = 1; export default a // 可以用另一种方式实现 default 的动态绑定 let a = 1; export { a as default } export function count(){ a++ } // 就跟上面 main.js 一样 [代码] 上面这段代码就是 [代码]CommonJs[代码] 导出变量 和 [代码]ES6[代码] 导出变量的区别 es module 循环引用 [代码]// bar.js import { foo } from './foo' console.log(foo); export let bar = 'bar' // foo.js import { bar } from './bar' console.log(bar); export let foo = 'foo' // main.js import { bar } from './bar' console.log(bar) [代码] [代码]执行 main.js -> 导入 bar.js[代码] [代码]bar.js -> 导入 foo.js[代码] [代码]foo.js -> 导入 bar.js -> bar.js 已经执行过直接返回 -> 输出 bar -> bar is not defined, bar 未定义报错[代码] 我们可以使用[代码]function[代码]的方式解决: [代码]// bar.js import { foo } from './foo' console.log(foo()); export function bar(){ return 'bar' } // foo.js import { bar } from './bar' console.log(bar()); export function foo(){ return 'foo' } // main.js import { bar } from './bar' console.log(bar) [代码] 因为函数声明会提示到文件顶部,所以就可以直接在 [代码]foo.js[代码] 调用还没执行完毕的[代码]bar.js[代码]的 [代码]bar[代码] 方法,不要在函数内使用外部变量,因为变量还未声明([代码]let,const[代码])和赋值,[代码]var[代码] CommonJs 和 ES6 Module 的区别 其实上面我们已经说到了一些区别 [代码]CommonJs[代码]导出的是变量的一份拷贝,[代码]ES6 Module[代码]导出的是变量的绑定([代码]export default[代码] 是特殊的) [代码]CommonJs[代码]是单个值导出,[代码]ES6 Module[代码]可以导出多个 [代码]CommonJs[代码]是动态语法可以写在判断里,[代码]ES6 Module[代码]静态语法只能写在顶层 [代码]CommonJs[代码]的 [代码]this[代码] 是当前模块,[代码]ES6 Module[代码]的 [代码]this[代码] 是 [代码]undefined[代码] 易混淆点 模块语法与解构 [代码]module语法[代码]与[代码]解构语法[代码]很容易混淆,例如: [代码]import { a } from 'module' const { a } = require('module') [代码] 尽管看上去很像,但是不是同一个东西,这是两种完全不一样的语法与作用,ps:两个人撞衫了,穿一样的衣服你不能说这俩人就是同一个人 [代码]module[代码] 的语法: 上面有写 [代码]import/export { a } / { a, b } / { a as c} FromClause[代码] [代码]解构[代码] 的语法: [代码]let { a } = { a: 1 } let { a = 2 } = { } let { a: b } = { a: 1 } let { a: b = 2, ...res } = { name:'a' } let { a: b, obj: { name } } = { a: 1, obj: { name: '1' } } function foo({a: []}) {} [代码] 他们是差别非常大的两个东西,一个是模块导入导出,一个是获取对象的语法糖 导出语法与对象属性简写 同样下面这段代码也容易混淆 [代码]let a = 1 export { a } // 导出语法 export default { a } // 属性简写 导出 { a: 1 } 对象 module.exports = { a } // 属性简写 导出 { a: 1 } 对象 [代码] [代码]export default[代码] 和 [代码]module.exports[代码] 是相似的 ES6 module 支持 CommonJs 情况 先简单说一下各个环境的 [代码]ES6 module[代码] 支持 [代码]CommonJs[代码] 情况,后面单独说如何在不同环境中使用 因为 [代码]module.exports[代码] 很像 [代码]export default[代码] 所以 [代码]ES6模块[代码] 可以很方便兼容 [代码]CommonJs[代码] 在[代码]ES6 module[代码]中使用[代码]CommonJs[代码]规范,根据各个环境,打包工具不同也是不一样的 我们现在大多使用的是 [代码]webpack[代码] 进行项目构建打包,因为现在前端开发环境都是在 [代码]Node[代码] 环境原因,而 [代码]npm[代码] 的包都是 [代码]CommonJs[代码] 规范的,所以 [代码]webpack[代码] 对[代码]ES6[代码]模块进行扩展 支持 [代码]CommonJs[代码],并支持[代码]node[代码]的导入[代码]npm[代码]包的规范 如果你使用 [代码]rollup[代码],想在[代码]ES Module[代码]中支持[代码]Commonjs[代码]规范就需要下载[代码]rollup-plugin-commonjs[代码]插件,想要导入[代码]node_modules[代码]下的包也需要[代码]rollup-plugin-node-resolve[代码]插件 如果你使用 [代码]node[代码],可以在 [代码].mjs[代码] 文件使用 [代码]ES6[代码],也支持 [代码]CommonJs[代码] 查看 nodejs es-modules.md 在浏览器环境 不支持[代码]CommonJs[代码] node 与 打包工具[代码]webpack,rollup[代码]的导入 [代码]CommonJs[代码] 差异 [代码]// module.js module.export.a = 1 // index.js webpack rollup import * as a from './module' console.log(a) // { a: 1, default: { a:1 } } // index.mjs node import * as a from './module' console.log(a) // { default: { a:1 } } [代码] [代码]node[代码] 只是把 [代码]module.exports[代码] 整体当做 [代码]export default[代码] 打包工具除了把 [代码]module.export[代码] 整体当做 [代码]export default[代码],还把 [代码]module.export[代码] 的每一项 又当做 [代码]export[代码] 输出,这样做是为了更加简洁 [代码]import defaultExport from './foo'[代码], [代码]defaultExport.foo()[代码] [代码]import { foo } from './foo'[代码], [代码]foo()[代码] 使用 ES6 Module 可以在 es6module example 仓库中获取代码在本地进行测试验证 浏览器中使用 你需要起一个[代码]Web服务器[代码]来访问,双击本地运行 [代码]index.html[代码] 并不会执行 [代码]type=module[代码] 标签 我们可以对 [代码]script[代码] 标签的 [代码]type[代码] 属性加上 [代码]module[代码] 先定义两个模块 [代码]// index.js import module from './module.js' console.log(module) // 123 // module.js export default 123 [代码] 在[代码]html[代码]中内联调用 [代码]<!-- index.html --> <script type="module"> import module from './module.js' console.log(module) // 123 </script> [代码] 在[代码]html[代码]中通过 [代码]script[代码] 的 [代码]src[代码] 引用 [代码]<!-- index.html --> <script type="module" src="index.js"></script> // 控制台 123 [代码] 浏览器导入路径规则 [代码]https://example.com/apples.mjs[代码] [代码]http://example.com/apples.js[代码] [代码]//example.com/bananas[代码] [代码]./strawberries.mjs.cgi[代码] [代码]../lychees[代码] [代码]/limes.jsx[代码] [代码]data:text/javascript,export default 'grapes';[代码] [代码]blob:https://whatwg.org/d0360e2f-caee-469f-9a2f-87d5b0456f6f[代码] 补充: 不加 后缀名 找不到具体的文件 后端可以修改接口[代码]/getjs?name=module[代码]这一类的,不过后端要返回 [代码]Content-Type: application/javascript[代码] 确保返回的是[代码]js[代码],因为浏览器是根据 [代码]MIME type[代码] 识别的 因为 [代码]ES6 Module[代码] 在浏览器中兼容并不是很好兼容性表,这里就不介绍浏览器支持情况了,我们一般不会直接在浏览器中使用 Nodejs中使用 nodejs es-modules.md 在 [代码]Node v8.5.0[代码] 以上支持 [代码]ES Module[代码],需要 [代码].mjs[代码]扩展名 NOTE: DRAFT status does not mean ESM will be implemented in Node core. Instead that this is the standard, should Node core decide to implement ESM. At which time this draft would be moved to ACCEPTED. (上面链接可以知道 [代码]ES Module[代码]的状态是 [代码]DRAFT[代码], 属于起草阶段) [代码]// module.mjs export default 123 // index.mjs import module from './module.mjs' console.log(module) // 123 [代码] 我们需要执行 [代码]node --experimental-modules index.mjs[代码] 来启动 会提示一个 [代码]ExperimentalWarning: The ESM module loader is experimental.[代码]该功能是实验性的(此提示不影响执行) [代码]ES Module[代码] 中导入 [代码]CommonJs[代码] [代码]// module.js module.exports.a = 123 // module.exports 就相当于 export default // index.mjs import module from './module.js' console.log(module) // { a: 123 } import * as module from './module.js' console.log(module) // { get default: { a: 123 } } import { default as module } from './module.js'; console.log(module) // { a: 123 } import module from 'module'; // 导入npm包 导入规则与 require 差不多 [代码] 导入路径规则与[代码]require[代码]差不多 这里要注意 [代码]module[代码] 扩展名为 [代码].js[代码],[代码].mjs[代码]专属于 [代码]es module[代码],[代码]import form[代码]导入的文件后缀名只能是[代码].mjs[代码],在 [代码].mjs[代码]中 [代码]module[代码]未定义, 所以调用 [代码]module.exports,exports[代码] 会报错 [代码]node[代码]中 [代码]CommonJs[代码] 导入 [代码]es module[代码] 只能使用 [代码]import()[代码] 动态导入/异步导入 [代码]// es.mjs let foo = {name: 'foo'}; export default foo; export let a = 1 // cjs import('./es').then((res)=>{ console.log(res) // { get default: {name: 'foo'}, a: 1 } }); [代码] webpack中使用 从 [代码]webpack2[代码] 就默认支持 [代码]es module[代码] 了,并默认支持 [代码]CommonJs[代码],支持导入 [代码]npm[代码]包, 这里 [代码]import[代码] 语法上面写太多 就不再写了 rollup中使用 [代码]rollup[代码] 专注于 [代码]es module[代码],可以将 [代码]es module[代码] 打包为主流的模块规范,注意这里与 [代码]webpack[代码] 的区别,我们可以在 [代码]webpack[代码] 的 [代码]js[代码] 中使用 [代码]Commonjs[代码] 语法, 但是 [代码]rollup[代码] 不支持,[代码]rollup[代码]需要 [代码]plugin[代码] 支持,包括加载 [代码]node_modules[代码] 下的包 [代码]form 'react'[代码] 也需要 [代码]plugin[代码] 支持 可以看到 [代码]es module[代码] 在[代码]浏览器[代码]与[代码]node[代码]中兼容性差与实验功能的 我们大多时候在 打包工具 中使用 Tree-shaking 在最后我们说一下经常跟 [代码]es module[代码] 一起出现的一个名词 [代码]Tree-shaking[代码] [代码]Tree-shaking[代码] 我们先直译一下 树木摇晃 就是 摇晃树木把上面枯死的树叶晃下来,在代码中就是把没有用到的代码删除 [代码]Tree-shaking[代码] 最早由 [代码]rollup[代码] 提出,之后 [代码]webpack 2[代码] 也开始支持 这都是基于 [代码]es module[代码] 模块特性的静态分析 rollup 下面代码使用 [代码]rollup[代码] 进行打包: [代码]// module.js export let foo = 'foo' export let bar = 'bar' // index.js import { foo } from './module' console.log(foo) // foo [代码] 在线运行 我们可以修改例子与导出多种规范 打包结果: [代码]let foo = 'foo'; console.log(foo); // foo [代码] 可以看到 [代码]rollup[代码] 打包结果非常的简洁,并去掉了没有用到的 [代码]bar[代码] 是否支持对导入 [代码]CommonJs[代码] 的规范进行 [代码]Tree-shaking[代码]: [代码]// index.js import { a } from './module' console.log(a) // 1 // module.js module.exports.a = 1 module.exports.b = 2 [代码] 打包为 [代码]es module[代码] [代码]var a_1 = 2; console.log(a_1); [代码] 可以看到去掉了未使用的 [代码]b[代码] webpack 我们下面看看 [代码]webpack[代码] 的支持情况 [代码]// src/module.js export function foo(){ return 'foo' } export function bar(){ return 'bar' } // src/index.js import { foo } from './module' console.log(foo()) [代码] 执行 [代码]npx webpack -p[代码](我们使用webpack 4,0配置,-p开启生成模式 自动压缩) 打包后我们在打包文件搜索 [代码]bar[代码] 没有搜到,[代码]bar[代码]被删除 我们将上面例子修改一下: [代码]// src/module.js module.exports.foo = function (){ return 'foo' } module.exports.bar = function (){ return 'bar' } // src/index.js import { foo } from './module' console.log(foo()) [代码] 打包后搜索 [代码]bar[代码] 发现[代码]bar[代码]存在,[代码]webpack[代码] 并不支持对[代码]CommonJs[代码] 进行 [代码]Tree-shaking[代码] pkg.module [代码]webpack[代码] 不支持 [代码]Commonjs[代码] [代码]Tree-shaking[代码],但现在[代码]npm[代码]的包都是[代码]CommonJs[代码]规范的,这该怎么办呢 ?如果我发了一个新包是 [代码]es module[代码] 规范, 但是如果代码运行在 [代码]node[代码] 环境,没有经过打包 就会报错 有一种按需加载的方案 全路径导入,导入具体的文件: [代码]// src/index.js import remove from 'lodash/remove' import add from 'lodash/add' console.log(remove(), add()) [代码] 使用一个还好,如果用多个的话会有很多 [代码]import[代码] 语句 还可以使用插件如 [代码]babel-plugin-lodash, & lodash-webpack-plugin[代码] 但我们不能发一个库就自己写插件 这时就提出了在 [代码]package.json[代码] 加一个 [代码]module[代码] 的字段来指向 [代码]es module[代码]规范的文件,[代码]main -> CommonJs[代码],那么[代码]module - es module[代码] pkg.module [代码]webpack[代码] 与 [代码]rollup[代码] 都支持 [代码]pkg.module[代码] 加了 [代码]module[代码] 字段 [代码]webpack[代码] 就可以识别我们的 [代码]es module[代码],但是还有一个问题就是 [代码]babel[代码] 我们一般使用 [代码]babel[代码] 都会排除 [代码]node_modules[代码],所以我们这个 [代码]pkg.module[代码] 只是的 [代码]es6 module[代码]必须是编译之后的 [代码]es5[代码] 代码,因为 [代码]babel[代码] 不会帮我们编译,我们的包就必须是 拥有 es6 module 规范的 es5 代码 如果你使用了 [代码]presets-env[代码] 因为会把我们的代码转为 [代码]CommonJs[代码] 所以就要设置 [代码]"presets": [["env", {"modules":false}][代码] 不将[代码]es module[代码] 转为 [代码]CommonJs[代码] [代码]webpack[代码] 与 [代码]rollup[代码] 的区别 [代码]webpack[代码] 不支持导出 [代码]es6 module[代码] 规范,[代码]rollup[代码] 支持导出 [代码]es6 module[代码] [代码]webpack[代码] 打包后代码很多冗余无法直接看,[代码]rollup[代码] 打包后的代码简洁,可读,像源码 [代码]webpack[代码] 可以进行代码分割,静态资源处理,[代码]HRM[代码],[代码]rollup[代码] 专注于 [代码]es module[代码],[代码]tree-shaking[代码]更加强大的,精简 如果是开发应用可以使用 [代码]webpack[代码],因为可以进行代码分割,静态资源,[代码]HRM[代码],插件 如果是开发类似 [代码]vue[代码],[代码]react[代码] 等类库,[代码]rollup[代码] 更好一些,因为可以使你的代码精简,无冗余代码,执行更快,导出多种模块语法 结语 本文章介绍了 [代码]Commonjs[代码] 和 [代码]ES6 Module[代码],导入导出的语法规则,路径解析规则,两者的区别,容易混淆的地方,在不同环境的区别,在不同环境的使用,[代码]Tree-shaking[代码],与 [代码]webpack[代码],[代码]rollup[代码] 的区别 希望您读完文章后,能对前端的模块化有更深的了解 参考链接 ECMAScript® 2015 Language Specification sec-imports/sec-exports MDN import github nodejs lib/module github nodejs node-eps/002-es-modules nodejs docs modules Understanding ECMAScript 6 ECMAScript 6 入门 es6-modules-final
2019-07-24 - 深入 Parcel--架构与流程篇
[图片] 本篇文章是对 [代码]Parce[代码] 的源码解析,代码基本架构与执行流程,带你了解打包工具的内部原理,在这之前你如果对 [代码]parcel[代码] 不熟悉可以先到 Parcel官网 了解 介绍 下面是偷懒从官网抄下来的介绍: 极速零配置Web应用打包工具 极速打包 [代码]Parcel[代码] 使用 [代码]worker[代码] 进程去启用多核编译。同时有文件系统缓存,即使在重启构建后也能快速再编译。 将你所有的资源打包 Parcel 具备开箱即用的对 [代码]JS[代码], [代码]CSS[代码], [代码]HTML[代码], 文件 及更多的支持,而且不需要插件。 自动转换 如若有需要,[代码]Babel[代码], [代码]PostCSS[代码], 和 [代码]PostHTML[代码] 甚至 [代码]node_modules[代码] 包会被用于自动转换代码. 零配置代码分拆 使用动态 [代码]import()[代码] 语法, [代码]Parcel[代码] 将你的输出文件束([代码]bundles[代码])分拆,因此你只需要在初次加载时加载你所需要的代码。 热模块替换 [代码]Parcel[代码] 无需配置,在开发环境的时候会自动在浏览器内随着你的代码更改而去更新模块。 友好的错误日志 当遇到错误时,Parcel 会输出 语法高亮的代码片段,帮助你定位问题。 打包工具 时间 browserify 22.98s webpack 20.71s parcel 9.98s parcel - with cache 2.64s 打包工具 我们常用的打包工具大致功能: 模块化(代码的拆分, 合并, [代码]Tree-Shaking[代码] 等) 编译([代码]es6,7,8 sass typescript[代码] 等) 压缩 ([代码]js, css, html[代码]包括图片的压缩) HMR (热替换) version [代码]parcel-bundler[代码] 版本: “version”: “1.11.0” 文件架构 [代码]|-- assets 资源目录 继承自 Asset.js |-- builtins 用于最终构建 |-- packagers 打包 |-- scope-hoisting 作用域提升 Tree-Shake |-- transforms 转换代码为 AST |-- utils 工具 |-- visitors 遍历 js AST树 收集依赖等 |-- Asset.js 资源 |-- Bundle.js 用于构建 bundle 树 |-- Bundler.js 主目录 |-- FSCache.js 缓存 |-- HMRServer.js HMR服务器提供 WebSocket |-- Parser.js 根据文件扩展名获取对应 Asset |-- Pipeline.js 多线程执行方法 |-- Resolver.js 解析模块路径 |-- Server.js 静态资源服务器 |-- SourceMap.js SourceMap |-- cli.js cli入口 解析命令行参数 |-- worker.js 多线程入口 [代码] 流程 说明 [代码]Parcel[代码]是面向资源的,[代码]JavaScript,CSS,HTML[代码] 这些都是资源,并不是 [代码]webpack[代码] 中 [代码]js[代码] 是一等公民,[代码]Parcel[代码] 会自动的从入口文件开始分析这些文件 和 模块中的依赖,然后构建一个 [代码]bundle[代码] 树,并对其进行打包输出到指定目录 一个简单的例子 我们从一个简单的例子开始了解 [代码]parcel[代码] 内部源码与流程 [代码]index.html |-- index.js |-- module1.js |-- module2.js [代码] 上面是我们例子的结构,入口为 [代码]index.html[代码], 在 [代码]index.html[代码] 中我们用 [代码]script[代码] 标签引用了 [代码]src/index.js[代码],在 [代码]index.js[代码] 中我们引入了2个子模块 执行 [代码]npx parcel index.html[代码] 或者 [代码]./node_modules/.bin/parcel index.html[代码],或者使用 [代码]npm script[代码] cli [代码]"bin": { "parcel": "bin/cli.js" } [代码] 查看 [代码]parcel-bundler[代码]的 [代码]package.json[代码] 找到 [代码]bin/cli.js[代码],在[代码]cli.js[代码]里又指向 [代码]../src/cli[代码] [代码]const program = require('commander'); program .command('serve [input...]') // watch build ... .action(bundle); program.parse(process.argv); async function bundle(main, command) { const Bundler = require('./Bundler'); const bundler = new Bundler(main, command); if (command.name() === 'serve' && command.target === 'browser') { const server = await bundler.serve(); if (server && command.open) {...启动自动打开浏览器} } else { bundler.bundle(); } } [代码] 在 [代码]cli.js[代码] 中利用 [代码]commander[代码] 解析命令行并调用 [代码]bundle[代码] 方法 有 [代码]serve, watch, build[代码] 3个命令来调用 [代码]bundle[代码] 函数,执行 [代码]pracel index.html[代码] 默认为 [代码]serve[代码],所以调用的是 [代码]bundler.serve[代码] 方法 进入 [代码]Bundler.js[代码] bundler.serve [代码]async serve(port = 1234, https = false, host) { this.server = await Server.serve(this, port, host, https); try { await this.bundle(); } catch (e) {} return this.server; } [代码] [代码]bundler.serve[代码] 方法 调用 [代码]serveStatic[代码] 起了一个静态服务指向 最终打包的文件夹 下面就是重要的 [代码]bundle[代码] 方法 bundler.bundle [代码]async bundle() { // 加载插件 设置env 启动多线程 watcher hmr await this.start(); if (isInitialBundle) { // 创建 输出目录 await fs.mkdirp(this.options.outDir); this.entryAssets = new Set(); for (let entry of this.entryFiles) { let asset = await this.resolveAsset(entry); this.buildQueue.add(asset); this.entryAssets.add(asset); } } // 打包队列中的资源 let loadedAssets = await this.buildQueue.run(); // findOrphanAssets 获取所有资源中独立的没有父Bundle的资源 let changedAssets = [...this.findOrphanAssets(), ...loadedAssets]; // 因为接下来要构建 Bundle 树,先对上一次的 Bundle树 进行 clear 操作 for (let asset of this.loadedAssets.values()) { asset.invalidateBundle(); } // 构建 Bundle 树 this.mainBundle = new Bundle(); for (let asset of this.entryAssets) { this.createBundleTree(asset, this.mainBundle); } // 获取新的最终打包文件的url this.bundleNameMap = this.mainBundle.getBundleNameMap( this.options.contentHash ); // 将代码中的旧文件url替换为新的 for (let asset of changedAssets) { asset.replaceBundleNames(this.bundleNameMap); } // 将改变的资源通过websocket发送到浏览器 if (this.hmr && !isInitialBundle) { this.hmr.emitUpdate(changedAssets); } // 对资源打包 this.bundleHashes = await this.mainBundle.package( this, this.bundleHashes ); // 将独立的资源删除 this.unloadOrphanedAssets(); return this.mainBundle; } [代码] 我们一步步先从 [代码]this.start[代码] 看 start [代码]if (this.farm) { return; } await this.loadPlugins(); if (!this.options.env) { await loadEnv(Path.join(this.options.rootDir, 'index')); this.options.env = process.env; } if (this.options.watch) { this.watcher = new Watcher(); this.watcher.on('change', this.onChange.bind(this)); } if (this.options.hmr) { this.hmr = new HMRServer(); this.options.hmrPort = await this.hmr.start(this.options); } this.farm = await WorkerFarm.getShared(this.options, { workerPath: require.resolve('./worker.js') }); [代码] [代码]start[代码]: [代码]开头的判断[代码] 防止多次执行,也就是说 [代码]this.start[代码] 只会执行一次 [代码]loadPlugins[代码] 加载插件,找到 [代码]package.json[代码] 文件 [代码]dependencies, devDependencies[代码] 中 [代码]parcel-plugin-[代码]开头的插件进行调用 [代码]loadEnv[代码] 加载环境变量,利用 [代码]dotenv, dotenv-expand[代码] 包将 [代码]env.development.local, .env.development, .env.local, .env[代码] 扩展至 [代码]process.env[代码] [代码]watch[代码] 初始化监听文件并绑定 [代码]change[代码] 回调函数,内部 [代码]child_process.fork[代码] 起一个子进程,使用 [代码]chokidar[代码] 包来监听文件改变 [代码]hmr[代码] 起一个服务,[代码]WebSocket[代码] 向浏览器发送更改的资源 [代码]farm[代码] 初始化多进程并指定 [代码]werker[代码] 工作文件,开启多个 [代码]child_process[代码] 去解析编译资源 接下来回到 [代码]bundle[代码],[代码]isInitialBundle[代码] 是一个判断是否是第一次构建 [代码]fs.mkdirp[代码] 创建输出文件夹 遍历入口文件,通过 [代码]resolveAsset[代码],内部调用 [代码]resolver[代码] 解析路径,并 [代码]getAsset[代码] 获取到对应的 [代码]asset[代码](这里我们入口是 [代码]index.html[代码],根据扩展名获取到的是 [代码]HTMLAsset[代码]) 将 [代码]asset[代码] 添加进队列 然后启动 [代码]this.buildQueue.run()[代码] 对资源从入口递归开始打包 PromiseQueue 这里 [代码]buildQueue[代码] 是一个 [代码]PromiseQueue[代码] 异步队列 [代码]PromiseQueue[代码] 在初始化的时候传入一个回调函数 [代码]callback[代码],内部维护一个参数队列 [代码]queue[代码],[代码]add[代码] 往队列里 [代码]push[代码] 一个参数,[代码]run[代码] 的时候[代码]while[代码]遍历队列 [代码]callback(...queue.shift())[代码],队列全部执行完毕 [代码]Promise[代码] 置为完成([代码]resolved[代码])(可以将其理解为 [代码]Promise.all[代码]) 这里定义的回调函数是 [代码]processAsset[代码],参数就是入口文件 [代码]index.html[代码] 的 [代码]HTMLAsset[代码] [代码]async processAsset(asset, isRebuild) { if (isRebuild) { asset.invalidate(); if (this.cache) { this.cache.invalidate(asset.name); } } await this.loadAsset(asset); } [代码] [代码]processAsset[代码] 函数内先判断是否是 [代码]Rebuild[代码] ,是第一次构建,还是 [代码]watch[代码] 监听文件改变进行的重建,如果是重建则对资源的属性[代码]重置[代码],并使其缓存失效 之后调用 [代码]loadAsset[代码] 加载资源编译资源 loadAsset [代码]async loadAsset(asset) { if (asset.processed) { return; } // Mark the asset processed so we don't load it twice asset.processed = true; // 先尝试读缓存,缓存没有在后台加载和编译 asset.startTime = Date.now(); let processed = this.cache && (await this.cache.read(asset.name)); let cacheMiss = false; if (!processed || asset.shouldInvalidate(processed.cacheData)) { processed = await this.farm.run(asset.name); cacheMiss = true; } asset.endTime = Date.now(); asset.buildTime = asset.endTime - asset.startTime; asset.id = processed.id; asset.generated = processed.generated; asset.hash = processed.hash; asset.cacheData = processed.cacheData; // 解析和加载当前资源的依赖项 let assetDeps = await Promise.all( dependencies.map(async dep => { dep.parent = asset.name; let assetDep = await this.resolveDep(asset, dep); if (assetDep) { await this.loadAsset(assetDep); } return assetDep; }) ); if (this.cache && cacheMiss) { this.cache.write(asset.name, processed); } } [代码] [代码]loadAsset[代码] 在开始有个判断防止重复编译 之后去读缓存,读取失败就调用 [代码]this.farm.run[代码] 在多进程里编译资源 编译完就去加载并编译依赖的文件 最后如果是新的资源没有用到缓存,就重新设置一下缓存 下面说一下这里吗涉及的两个东西:缓存 [代码]FSCache[代码] 和 多进程 [代码]WorkerFarm[代码] FSCache [代码]read[代码] 读取缓存,并判断最后修改时间和缓存的修改时间 [代码]write[代码] 写入缓存 [图片] 缓存目录为了加速读取,避免将所有的缓存文件放在一个文件夹里,[代码]parcel[代码] 将 [代码]16进制[代码] 两位数的 [代码]256[代码] 种可能创建为文件夹,这样存取缓存文件的时候,将目标文件路径 [代码]md5[代码] 加密转换为 [代码]16进制[代码],然后截取前两位是目录,后面几位是文件名 WorkerFarm 在上面 [代码]start[代码] 里初始化 [代码]farm[代码] 的时候,[代码]workerPath[代码] 指向了 [代码]worker.js[代码] 文件,[代码]worker.js[代码] 里有两个函数,[代码]init[代码] 和 [代码]run[代码] [代码]WorkerFarm.getShared[代码] 初始化的时候会创建一个 [代码]new WorkerFarm[代码] ,调用 [代码]worker.js[代码] 的 [代码]init[代码] 方法,根据 [代码]cpu[代码] 获取最大的 [代码]Worker[代码] 数,并启动一半的子进程 [代码]farm.run[代码] 会通知子进程执行 [代码]worker.js[代码] 的 [代码]run[代码] 方法,如果进程数没有达到最大会再次开启一个新的子进程,子进程执行完毕后将 [代码]Promise[代码]状态更改为完成 [代码]worker.run -> pipeline.process -> pipeline.processAsset -> asset.process[代码] [代码]Asset.process[代码] 处理资源: [代码]async process() { if (!this.generated) { await this.loadIfNeeded(); await this.pretransform(); await this.getDependencies(); await this.transform(); this.generated = await this.generate(); } return this.generated; } [代码] 将上面的代码内部扩展一下: [代码]async process() { // 已经有就不需要编译 if (!this.generated) { // 加载代码 if (this.contents == null) { this.contents = await this.load(); } // 可选。在收集依赖之前转换。 await this.pretransform(); // 将代码解析为 AST 树 if (!this.ast) { this.ast = await this.parse(this.contents); } // 收集依赖 await this.collectDependencies(); // 可选。在收集依赖之后转换。 await this.transform(); // 生成代码 this.generated = await this.generate(); } return this.generated; } // 最后处理代码 async postProcess(generated) { return generated } [代码] [代码]processAsset[代码] 中调用 [代码]asset.process[代码] 生成 [代码]generated[代码] 这个[代码]generated[代码] 不一定是最终代码 ,像 [代码]html[代码]里内联的 [代码]script[代码] ,[代码]vue[代码] 的 [代码]html, js, css[代码],都会进行二次或多次递归处理,最终调用 [代码]asset.postProcess[代码] 生成代码 Asset 下面说几个实现 [代码]HTMLAsset[代码]: pretransform 调用 [代码]posthtml[代码] 将 [代码]html[代码] 解析为 [代码]PostHTMLTree[代码](如果没有设置[代码]posthtmlrc[代码]之类的不会走) parse 调用 [代码]posthtml-parser[代码] 将 [代码]html[代码] 解析为 [代码]PostHTMLTree[代码] collectDependencies 用 [代码]walk[代码] 遍历 [代码]ast[代码],找到 [代码]script, img[代码] 的 [代码]src[代码],[代码]link[代码] 的 [代码]href[代码] 等的地址,将其加入到依赖 transform [代码]htmlnano[代码] 压缩代码 generate 处理内联的 [代码]script[代码] 和 [代码]css[代码] postProcess [代码]posthtml-render[代码] 生成 [代码]html[代码] 代码 [代码]JSAsset[代码]: pretransform 调用 [代码]@babel/core[代码] 将 [代码]js[代码] 解析为 [代码]AST[代码],处理 [代码]process.env[代码] parse 调用 [代码]@babel/parser[代码] 将 [代码]js[代码] 解析为 [代码]AST[代码] collectDependencies 用 [代码]babylon-walk[代码] 遍历 [代码]ast[代码], 如 [代码]ImportDeclaration[代码],[代码]import xx from 'xx'[代码] 语法,[代码]CallExpression[代码] 找到 [代码]require[代码]调用,[代码]import[代码] 被标记为 [代码]dynamic[代码] 动态导入,将这些模块加入到依赖 transform 处理 [代码]readFileSync[代码],[代码]__dirname, __filename, global[代码]等,如果没有设置[代码]scopeHoist[代码] 并存在 [代码]es6 module[代码] 就将代码转换为 [代码]commonjs[代码],[代码]terser[代码] 压缩代码 generate [代码]@babel/generator[代码] 获取 [代码]js[代码] 与 [代码]sourceMap[代码] 代码 [代码]VueAsset[代码]: parse [代码]@vue/component-compiler-utils[代码] 与 [代码]vue-template-compiler[代码] 对 [代码].vue[代码] 文件进行解析 generate 对 [代码]html, js, css[代码] 处理,就像上面说到会对其分别调用 [代码]processAsset[代码] 进行二次解析 postProcess [代码]component-compiler-utils[代码] 的 [代码]compileTemplate, compileStyle[代码]处理 [代码]html,css[代码],[代码]vue-hot-reload-api[代码] HMR处理,压缩代码 回到 [代码]bundle[代码] 方法: [代码]let loadedAssets = await this.buildQueue.run()[代码] 就是上面说到的[代码]PromiseQueue[代码] 和 [代码]WorkerFarm[代码] 结合起来:[代码]buildQueue.run —> processAsset -> loadAsset -> farm.run -> worker.run -> pipeline.process -> pipeline.processAsset -> asset.process[代码],执行之后所有资源编译完毕,并返回入口资源[代码]loadedAssets[代码]就是 [代码]index.html[代码] 对应的 [代码]HTMLAsset[代码] 资源 之后是 [代码]let changedAssets = [...this.findOrphanAssets(), ...loadedAssets][代码] 获取到改变的资源 [代码]findOrphanAssets[代码] 是从所有资源中查找没有 [代码]parentBundle[代码] 的资源,也就是独立的资源,这个 [代码]parentBundle[代码] 会在等会的构建 [代码]Bundle[代码] 树中被赋值,第一次构建都没有 [代码]parentBundle[代码],所以这里会重复入口文件,这里的 [代码]findOrphanAssets[代码] 的作用是在第一次构建之后,文件[代码]change[代码]的时候,在这个文件 [代码]import[代码]了新的一个文件,因为新文件没有被构建过 [代码]Bundle[代码] 树,所以没有 [代码]parentBundle[代码],这个新文件也被标记物 [代码]change[代码] [代码]invalidateBundle[代码] 因为接下来要构建新的树所以调用重置所有资源上一次树的属性 [代码]createBundleTree[代码] 构建 [代码]Bundle[代码] 树: 首先一个入口资源会被创建成一个 bundle,然后动态的 import() 会被创建成子 bundle ,这引发了代码的拆分。 当不同类型的文件资源被引入,兄弟 bundle 就会被创建。例如你在 JavaScript 中引入了 CSS 文件,那它会被放置在一个与 JavaScript 文件对应的兄弟 bundle 中。 如果资源被多于一个 bundle 引用,它会被提升到 bundle 树中最近的公共祖先中,这样该资源就不会被多次打包。 [代码]Bundle[代码]: [代码]type[代码]:它包含的资源类型 (例如:js, css, map, …) [代码]name[代码]:bundle 的名称 (使用 entryAsset 的 Asset.generateBundleName() 生成) [代码]parentBundle[代码]:父 bundle ,入口 bundle 的父 bundle 是 null [代码]entryAsset[代码]:bundle 的入口,用于生成名称(name)和聚拢资源(assets) [代码]assets[代码]:bundle 中所有资源的集合(Set) [代码]childBundles[代码]:所有子 bundle 的集合(Set) [代码]siblingBundles[代码]:所有兄弟 bundle 的集合(Set) [代码]siblingBundlesMap[代码]:所有兄弟 bundle 的映射 Map<String(Type: js, css, map, …), Bundle> [代码]offsets[代码]:所有 bundle 中资源位置的映射 Map<Asset, number(line number inside the bundle)> ,用于生成准确的 sourcemap 。 我们的例子会被构建成: [代码]html ( index.html ) |-- js ( index.js, module1.js, module2.js ) |-- map ( index.js, module1.js, module2.js ) [代码] [代码]module1.js[代码] 和 [代码]module2.js[代码] 被提到了与 [代码]index.js[代码] 同级,[代码]map[代码] 因为类型不同被放到了 子[代码]bundle[代码] 一个复杂点的树: [代码]// 资源树 index.html |-- index.css |-- bg.png |-- index.js |-- module.js [代码] [代码]// mainBundle html ( index.html ) |-- js ( index.js, module.js ) |-- map ( index.map, module.map ) |-- css ( index.css ) |-- js ( index.css, css-loader.js bundle-url.js ) |-- map ( css-loader.js, bundle-url.js ) |-- png ( bg.png ) [代码] 因为要对 css 热更新,所以新增了 [代码]css-loader.js, bundle-url.js[代码] 两个 js [代码]replaceBundleNames[代码]替换引用:生成树之后将代码中的文件引用替换为最终打包的文件名,如果是生产环境会替换为 [代码]contentHash[代码] 根据内容生成 [代码]hash[代码] [代码]hmr[代码]更新: 判断启用 [代码]hmr[代码] 并且不是第一次构建的情况,调用 [代码]hmr.emitUpdate[代码] 将改变的资源发送给浏览器 [代码]Bundle.package[代码] 打包 [代码]unloadOrphanedAssets[代码] 将独立的资源删除 package [代码]package[代码] 将[代码]generated[代码] 写入到文件 有6种打包: [代码]CSSPackager[代码],[代码]HTMLPackager[代码],[代码]SourceMapPackager[代码],[代码]JSPackager[代码],[代码]JSConcatPackager[代码],[代码]RawPackager[代码] 当开启 [代码]scopeHoist[代码] 时用 [代码]JSConcatPackager[代码] 否则 [代码]JSPackager[代码] 图片等资源用 [代码]RawPackager[代码] 最终我们的例子被打包成 [代码]index.html, src.[hash].js, src.[hash].map[代码] 3个文件 [代码]index.html[代码] 里的 [代码]js[代码] 路径被替换成立最终打包的地址 我们看一下打包的 js: [代码]parcelRequire = (function (modules, cache, entry, globalName) { // Save the require from previous bundle to this closure if any var previousRequire = typeof parcelRequire === 'function' && parcelRequire; var nodeRequire = typeof require === 'function' && require; function newRequire(name, jumped) { if (!cache[name]) { localRequire.resolve = resolve; localRequire.cache = {}; var module = cache[name] = new newRequire.Module(name); modules[name][0].call(module.exports, localRequire, module, module.exports, this); } return cache[name].exports; function localRequire(x){ return newRequire(localRequire.resolve(x)); } function resolve(x){ return modules[name][4][x] || x; } } for (var i = 0; i < entry.length; i++) { newRequire(entry[i]); } // Override the current require with this new one return newRequire; })({"src/module1.js":[function(require,module,exports) { "use strict"; },{}],"src/module2.js":[function(require,module,exports) { "use strict"; },{}],"src/index.js":[function(require,module,exports) { "use strict"; var _module = require("./module"); var _module2 = require("./module1"); var _module3 = require("./module2"); console.log(_module.m); },{"./module":"src/module.js","./module1":"src/module1.js","./module2":"src/module2.js","fs":"node_modules/parcel-bundler/src/builtins/_empty.js"}] ,{}]},{},["node_modules/parcel-bundler/src/builtins/hmr-runtime.js","src/index.js"], null) //# sourceMappingURL=/src.a2b27638.map [代码] 可以看到代码被拼接成了对象的形式,接收参数 [代码]module, require[代码] 用来模块导入导出,实现了 [代码]commonjs[代码] 的模块加载机制,一个更加简化版: [代码]parcelRequire = (function (modules, cache, entry, globalName) { function newRequire(id){ if(!cache[id]){ let module = cache[id] = { exports: {} } modules[id][0].call(module.exports, newRequire, module, module.exports, this); } return cache[id] } for (var i = 0; i < entry.length; i++) { newRequire(entry[i]); } return newRequire; })() [代码] 代码被拼接起来: [代码]`(function(modules){ //...newRequire })({` + asset.id + ':[function(require,module,exports) {\n' + asset.generated.js + '\n},' + '})' [代码] [代码](function(modules){ //...newRequire })({ "src/index.js":[function(require,module,exports){ // code }] }) [代码] hmr-runtime 上面打包的 [代码]js[代码] 中还有个 [代码]hmr-runtime.js[代码] 太长被我省略了 [代码]hmr-runtime.js[代码] 创建一个 [代码]WebSocket[代码] 监听服务端消息 修改文件触发 [代码]onChange[代码] 方法,[代码]onChange[代码] 将改变的资源 [代码]buildQueue.add[代码] 加入构建队列,重新调用 [代码]bundle[代码] 方法,打包资源,并调用 [代码]emitUpdate[代码] 通知浏览器更新 当浏览器接收到服务端有新资源更新消息时 新的资源就会设置或覆盖之前的模块 [代码]modules[asset.id] = new Function('require', 'module', 'exports', asset.generated.js)[代码] 对模块进行更新: [代码]function hmrAccept(id){ // dispose 回调 cached.hot._disposeCallbacks.forEach(function (cb) { cb(bundle.hotData); }); delete bundle.cache[id]; // 删除之前缓存 newRequire(id); // 重新此加载 // accept 回调 cached.hot._acceptCallbacks.forEach(function (cb) { cb(); }); // 递归父模块 进行更新 getParents(global.parcelRequire, id).some(function (id) { return hmrAccept(global.parcelRequire, id); }); } [代码] 至此整个打包流程结束 总结 [代码]parcle index.html[代码] 进入 [代码]cli[代码],启动[代码]Server[代码]调用 [代码]bundle[代码],初始化配置([代码]Plugins[代码], [代码]env[代码], [代码]HMRServer, Watcher, WorkerFarm[代码]),从入口资源开始,递归编译([代码]babel, posthtml, postcss, vue-template-compiler[代码]等),编译完设置缓存,构建 [代码]Bundle[代码] 树,进行打包 如果没有 [代码]watch[代码] 监听,结束关闭 [代码]Watcher, Worker, HMR[代码] 有 [代码]watch[代码] 监听: 文件修改,触发 [代码]onChange[代码],将修改的资源加入构建队列,递归编译,查找缓存(这一步缓存的作用就提醒出来了),编译完设置新缓存,构建 [代码]Bundle[代码] 树,进行打包,将 [代码]change[代码] 的资源发送给浏览器,浏览器接收 [代码]hmr[代码] 更新资源 最后 通过此文章希望你对 [代码]parcel[代码] 的大致流程,打包工具原理有更深的了解 了解更多请关注专栏,后续 深入Parcel 同系列文章,对 [代码]Asset[代码],[代码]Packager[代码],[代码]Worker[代码],[代码]HMR[代码],[代码]scopeHoist[代码],[代码]FSCache[代码],[代码]SourceMap[代码],[代码]import[代码] 更加 详细讲解与代码实现
2019-07-24 - Painter 一款轻量级的小程序海报生成组件
生成海报相信大家有的人都做过,但是canvas绘图的坑太多。大家可以试试这个组件。然后附上楼下大哥做的可视化拖拽生成painter代码的工具:链接地址https://developers.weixin.qq.com/community/develop/article/doc/000e222d9bcc305c5739c718d56813
2019-09-27 - 如何实现一个自定义导航栏
自定义导航栏在刚出的时候已经有很多实现方案了,但是还有大哥在问,那这里再贴下代码及原理: 首先在App.js的 onLaunch中获取当前手机机型头部状态栏的高度,单位为px,存在内存中,操作如下: [代码]onLaunch() { wx.getSystemInfo({ success: (res) => { this.globalData.statusBarHeight = res.statusBarHeight this.globalData.titleBarHeight = wx.getMenuButtonBoundingClientRect().bottom + wx.getMenuButtonBoundingClientRect().top - (res.statusBarHeight * 2) }, failure() { this.globalData.statusBarHeight = 0 this.globalData.titleBarHeight = 0 } }) } [代码] 然后需要在目录下新建个components文件夹,里面存放此次需要演示的文件 navigateTitle WXML 文件如下: [代码]<view class="navigate-container"> <view style="height:{{statusBarHeight}}px"></view> <view class="navigate-bar" style="height:{{titleBarHeight}}px"> <view class="navigate-icon"> <navigator class="navigator-back" open-type="navigateBack" wx:if="{{!isShowHome}}" /> <navigator class="navigator-home" open-type="switchTab" url="/pages/index/index" wx:else /> </view> <view class="navigate-title">{{title}}</view> <view class="navigate-icon"></view> </view> </view> <view class="navigate-line" style="height: {{statusBarHeight + titleBarHeight}}px; width: 100%;"></view> [代码] WXSS文件如下: [代码].navigate-container { position: fixed; top: 0; width: 100%; z-index: 9999; background: #FFF; } .navigate-bar { width: 100%; display: flex; justify-content: space-around; } .navigate-icon { width: 100rpx; height: 100rpx; display: flex; justify-content: space-around; } .navigate-title { width: 550rpx; text-align: center; line-height: 100rpx; font-size: 34rpx; color: #3c3c3c; font-weight: bold; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } /*箭头部分*/ .navigator-back { width: 36rpx; height: 36rpx; align-self: center; } .navigator-back:after { content: ''; display: block; width: 22rpx; height: 22rpx; border-right: 4rpx solid #000; border-top: 4rpx solid #000; transform: rotate(225deg); } .navigator-home { width: 56rpx; height: 56rpx; background: url(https://qiniu-image.qtshe.com/20190301home.png) no-repeat center center; background-size: 100% 100%; align-self: center; } [代码] JS如下: [代码]var app = getApp() Component({ data: { statusBarHeight: '', titleBarHeight: '', isShowHome: false }, properties: { //属性值可以在组件使用时指定 title: { type: String, value: '青团公益' } }, pageLifetimes: { // 组件所在页面的生命周期函数 show() { let pageContext = getCurrentPages() if (pageContext.length > 1) { this.setData({ isShowHome: false }) } else { this.setData({ isShowHome: true }) } } }, attached() { this.setData({ statusBarHeight: app.globalData.statusBarHeight, titleBarHeight: app.globalData.titleBarHeight }) }, methods: {} }) [代码] JSON如下: [代码]{ "component": true } [代码] 如何引用? 需要引用的页面JSON里配置: [代码]"navigationStyle": "custom", "usingComponents": { "navigate-title": "/pages/components/navigateTitle/index" } [代码] WXML [代码]<navigate-title title="青团社" /> [代码] 按上面步骤操作即可实现一个自定义的导航栏。 如何实现通栏的效果默认透明以及滚动更换title为白色背景,如下图所示: [图片] [图片] [图片] [图片] 最后代码片段如下: https://developers.weixin.qq.com/s/wi6Pglmv7s8P。 以下为收集到的社区老哥们的分享: @Yunior: 小程序顶部自定义导航组件实现原理及坑分享 @志军: 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能 @✨o0o有脾气的酸奶💤 [有点炫]自定义navigate+分包+自定义tabbar @安晓苏 分享一个自适应的自定义导航栏组件
2020-03-10 - 微信小程序码获取-从频繁失败到成功率100%
早期实现方案 1. 方案实现 通过微信的appSecret获取小程序accessToken并缓存 微信小程序上很多操作都需要使用accessToken,比如用户授权手机号,当然也包括获取小程序码 通过微信提供的api获取到对应的小程序码,由于http接口直接返回的是图片本身,所以考虑将图片上传七牛服务器并获取图片链接,最后使用图片的链接来展示或保存小程序码 2. 方案优点 由于上传了小程序码,对于一些跳转固定页面和参数的码可以将图片链接存到数据库,以供用户下次分享使用,无需重复获取 3. 存在的问题 稳定性很差,获取小程序码的失败率比较高,甚至会出现一个时间段内完全获取不到码的情况 接口效率不好,由于每次都会存在图片上传,而且上传本身又比较耗时,导致服务器压力巨大且频繁出现慢接口,可能会影响到项目中的其他服务 改造后方案 1. 方案实现 获取小程序码后不再上传七牛,直接通过图片流的方式返回给前端 2. 方案优点 取消了图片的上传操作,接口效率大幅提升,提高了小程序码的获取成功率,也减轻了服务的压力 3. 存在的问题 依旧存在小程序码获取失败的情况 4. 问题排查 经排查日志发现是accessToken失效导致,缓存的accessToken失效时间远比微信规定的失效时间短,那究竟又是什么情况会导致accessToken失效呢?经讨论和实验发现以下三点: 我们微信的appSecret授权给第三方网站使用(比如阿拉丁),他们也有获取小程序码的服务,运营可以通过阿拉丁获取小程序码,这就会导致阿拉丁使用我们的appSecret获取accessToken,以至于我们缓存中的accessToken失效。 后端缓存中的accessToken存入和获取的逻辑存在缺陷,每当从缓存读取accessToken时,若缓存不命中,则通过微信api获取新的accessToken然后再存入缓存,这个逻辑容易导致缓存穿透,即当多个请求都没有命中缓存时,只有一个线程能通过微信api拿到新的accessToken,其他线程都拿不到。 当一个accessToken存在时间比较长时,手动调用微信api获取小程序码,会看到微信的api也会存在概率获取不到码的情况,但是一个全新生成的accessToken则不会有这种情况,至少在10分钟之内非常稳定。 最终的方案 1. 方案实现 通知运营不要再使用阿拉丁的生成小程序码的功能,若有这方面需求可以找技术帮忙获取。 缓存中的accessToken有效时间缩短至5分钟,保证每次使用的accessToken都能稳定获取小程序码。 修改accessToken的获取机制,由定时器来获取accessToken并更新缓存,定时器每4分钟执行一次,以确保每个请求都能命中缓存,若定时器出现异常,则回退之前的逻辑(请求没有命中缓存,通过微信api重新获取accessToken)。 最终效果 这一个方案上线后,线上再也没有出现小程序码没有获取成功的情况,观察日志也没再出现获取失败的情况,目前已经两周保持100%成功率了。 [图片]
2019-06-04 - 【优化】解决swiper渲染很多图片时的卡顿
相信各位在开发的时候应该有遇到这样一个场景,比如商品的图片浏览,有时图片的浏览会很大,多的时候达几百张或上千张,这样就需要swiper里需要很多swiper-item,如此一来渲染的时候就会很消耗性能,渲染时会有一大段的空白时间,有时还会造成卡顿,体验非常差,下面给大家介绍一下我的解决方案。 首先是wxml结构: [图片] js: [图片] [图片] 主要是利用current属性,swiper里面只放3个swiper-item,要显示的图片放在第二,第一和第三放的是加载的动画背景,步骤如下: 1. 将请求到的数据存入一个数组picListAll内,这里不需要setData,只需要在data外面定义一个变量就行了,以减少渲染性能。 2. 把要显示的图片路径赋值给picUrl, 3. 切换的时候根据bindchange获取current属性,当current改变时判断当前图片在picListAll的index,根据index拿到图片再赋值给picUrl 主要实现步骤就是以上3 步,比较简单,要注意的是当切换到第一张和最后一张的时候要判断一下,把loding动画去掉,请求的时候还可以传入index参数以显示不同的图片,方便从前一页点击图片进入到此页面时能定位到该图片,例子里我是自己mock数据的,只是为了展示,如果你有服务器的话可以弄几百张看看效果,对比直接渲染和用以上方式渲染的差异。当然,这只是我的解决方案,如果各位有更好的方案欢迎一起讨论,一起进步. 系甘先,得闲饮茶 完整代码:https://github.com/HaveYuan/swiper
2019-01-25 - canvas绘制圆角头像
使用canvas的arc方法 circleImg(ctx, img, x, y, r) { ctx.save(); var d =2 * r; var cx = x + r; var cy = y + r; ctx.arc(cx, cy, r, 0, 2 * Math.PI); ctx.clip(); ctx.drawImage(img, x, y, d, d); ctx.restore(); }
2017-12-27