- 初试小程序接入three.js
看着小程序下的canvas日渐完善,特别是2.7.0库下新增了WebGL,终于可以摆脱原来用wx.createCanvasContext创建的2d上下文(不知为何在使用魔改后three.js中的CanvasRenderer渲染画面就是很慢,捕获JavaScript Profiler看着就是慢在draw方法上)。 不过理想很丰满,现实很骨感,想要在小程序上用three.js依然要来个大改造。让我们开始吧 官方文档里提供了一段如何获取WebGL Context的代码: [代码]Page({[代码][代码] [代码][代码]onReady() {[代码][代码] [代码][代码]const query = wx.createSelectorQuery()[代码][代码] [代码][代码]query.select([代码][代码]'#myCanvas'[代码][代码]).node().exec((res) => {[代码][代码] [代码][代码]const canvas = res[0].node[代码][代码] [代码][代码]const gl = canvas.getContext([代码][代码]'webgl'[代码][代码])[代码][代码] [代码][代码]console.log(gl)[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码]})[代码]我们就从这里入手 首先先写个wxml: [代码]<[代码][代码]canvas[代码] [代码]type[代码][代码]=[代码][代码]"webgl"[代码] [代码]id[代码][代码]=[代码][代码]"webgl"[代码] [代码]width[代码][代码]=[代码][代码]"{{canvasWidth||(320*2)}}"[代码] [代码]height[代码][代码]=[代码][代码]"{{canvasHeight||(504*2)}}"[代码] [代码]style[代码][代码]=[代码][代码]'width:{{canvasStyleWidth||"320px"}};height:{{canvasStyleHeight||"504px"}};'[代码] [代码]bindtouchstart[代码][代码]=[代码][代码]'onTouchStart'[代码] [代码]bindtouchmove[代码][代码]=[代码][代码]'onTouchMove'[代码] [代码]bindtouchend[代码][代码]=[代码][代码]'onTouchEnd'[代码][代码]></[代码][代码]canvas[代码][代码]>[代码]其中width和height是设置画布大小的,style中的width和height是设置画布的实际渲染大小的 然后js: [代码]onReady:[代码][代码]function[代码][代码](){[代码][代码] [代码][代码]var[代码] [代码]self = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]var[代码] [代码]query = wx.createSelectorQuery().select([代码][代码]'#webgl'[代码][代码]).node().exec((res) => {[代码][代码] [代码][代码]var[代码] [代码]canvas = res[0].node;[代码][代码] [代码][代码]requestAnimationFrame = canvas.requestAnimationFrame;[代码][代码] [代码][代码]canvas.width = canvas._width;[代码][代码] [代码][代码]canvas.height = canvas._height;[代码][代码] [代码][代码]canvas.style = {};[代码][代码] [代码][代码]canvas.style.width = canvas.width;[代码][代码] [代码][代码]canvas.style.height = canvas.height;[代码][代码] [代码][代码]self.init(canvas);[代码][代码] [代码][代码]self.animate();[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码]先模拟dom构造一个canvas对象,然后传入init方法中,我们在这里创建场景、相机、渲染器等 [代码]init: [代码][代码]function[代码] [代码](canvas) {[代码][代码]...[代码][代码] [代码][代码]camera = [代码][代码]new[代码] [代码]THREE.PerspectiveCamera(20, canvas.width / canvas.height, 1, 10000);[代码][代码] [代码][代码]scene = [代码][代码]new[代码] [代码]THREE.Scene();[代码][代码]...[代码][代码] [代码][代码]renderer = [代码][代码]new[代码] [代码]THREE.WebGLRenderer({ canvas: canvas, antialias: [代码][代码]true[代码] [代码]});[代码][代码] [代码][代码]}[代码]这样一个最基础的三维场景就搭好了,然后继续执行animate方法,开始渲染场景 [代码]animate:[代码][代码]function[代码][代码]() {[代码][代码] [代码][代码]requestAnimationFrame([代码][代码]this[代码][代码].animate);[代码][代码] [代码][代码]this[代码][代码].render();[代码][代码] [代码][代码]}[代码]接下来尝试跑一下three.js提供的例子 webgl_geometry_colors : [图片] 锯齿问题比较严重,暂时没找到解决办法,但总体来说还是可以的,至少场景渲染出来了 由于暂时没想到如何改造CanvasTexture,我把例子中的 [代码]var[代码] [代码]canvas = document.createElement( [代码][代码]'canvas'[代码] [代码]);[代码][代码]canvas.width = 128;[代码][代码]canvas.height = 128;[代码][代码]var[代码] [代码]context = canvas.getContext( [代码][代码]'2d'[代码] [代码]);[代码][代码]var[代码] [代码]gradient = context.createRadialGradient( canvas.width / 2, canvas.height / 2, 0, canvas.width / 2, canvas.height / 2, canvas.width / 2 );[代码][代码]gradient.addColorStop( 0.1, [代码][代码]'rgba(210,210,210,1)'[代码] [代码]);[代码][代码]gradient.addColorStop( 1, [代码][代码]'rgba(255,255,255,1)'[代码] [代码]);[代码][代码]context.fillStyle = gradient;[代码][代码]context.fillRect( 0, 0, canvas.width, canvas.height );[代码][代码]var[代码] [代码]shadowTexture = [代码][代码]new[代码] [代码]THREE.CanvasTexture( canvas );[代码]替换成 webgl_geometries 例子中的TextureLoader [代码]var[代码] [代码]shadowTexture = [代码][代码]new[代码] [代码]THREE.TextureLoader().load(canvas,[代码][代码]'../../textures/UV_Grid_Sm.jpg'[代码][代码]);[代码]可能有人会发现load方法中传入的参数多了一个canvas,因为小程序提供的api没法直接创建Image对象,仅有一个Canvas.createImage()方法可以创建Image对象。因此我们还需要改造一下TextureLoader中的load方法,先看一下原版中的load方法: [代码]Object.assign( TextureLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]var[代码] [代码]texture = [代码][代码]new[代码] [代码]Texture();[代码] [代码] [代码][代码]var[代码] [代码]loader = [代码][代码]new[代码] [代码]ImageLoader( [代码][代码]this[代码][代码].manager );[代码][代码] [代码][代码]loader.setCrossOrigin( [代码][代码]this[代码][代码].crossOrigin );[代码][代码] [代码][代码]loader.setPath( [代码][代码]this[代码][代码].path );[代码] [代码] [代码][代码]loader.load( url, [代码][代码]function[代码] [代码]( image ) {[代码]其中实际调用了ImageLoader来加载图片,在看看ImageLoader: [代码]Object.assign( ImageLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]if[代码] [代码]( url === undefined ) url = [代码][代码]''[代码][代码];[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].path !== undefined ) url = [代码][代码]this[代码][代码].path + url;[代码] [代码] [代码][代码]url = [代码][代码]this[代码][代码].manager.resolveURL( url );[代码] [代码] [代码][代码]var[代码] [代码]scope = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]var[代码] [代码]cached = Cache.get( url );[代码] [代码] [代码][代码]if[代码] [代码]( cached !== undefined ) {[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]setTimeout( [代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( cached );[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}, 0 );[代码] [代码] [代码][代码]return[代码] [代码]cached;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]var[代码] [代码]image = document.createElementNS( [代码][代码]'http://www.w3.org/1999/xhtml'[代码][代码], [代码][代码]'img'[代码] [代码]);[代码] [代码] [代码][代码]function[代码] [代码]onImageLoad() {[代码] [代码] [代码][代码]image.removeEventListener( [代码][代码]'load'[代码][代码], onImageLoad, [代码][代码]false[代码] [代码]);[代码][代码] [代码][代码]image.removeEventListener( [代码][代码]'error'[代码][代码], onImageError, [代码][代码]false[代码] [代码]);[代码] [代码] [代码][代码]Cache.add( url, [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]function[代码] [代码]onImageError( event ) {[代码] [代码] [代码][代码]image.removeEventListener( [代码][代码]'load'[代码][代码], onImageLoad, [代码][代码]false[代码] [代码]);[代码][代码] [代码][代码]image.removeEventListener( [代码][代码]'error'[代码][代码], onImageError, [代码][代码]false[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( onError ) onError( event );[代码] [代码] [代码][代码]scope.manager.itemError( url );[代码][代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]image.addEventListener( [代码][代码]'load'[代码][代码], onImageLoad, [代码][代码]false[代码] [代码]);[代码][代码] [代码][代码]image.addEventListener( [代码][代码]'error'[代码][代码], onImageError, [代码][代码]false[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( url.substr( 0, 5 ) !== [代码][代码]'data:'[代码] [代码]) {[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].crossOrigin !== undefined ) image.crossOrigin = [代码][代码]this[代码][代码].crossOrigin;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]image.src = url;[代码] [代码] [代码][代码]return[代码] [代码]image;[代码] [代码] [代码][代码]},[代码]document.createElementNS这种东西肯定是没法存在的,没办法,把canvas传进来用createImage方法创建Image对象,改造后: [代码]Object.assign( ImageLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( canvas,url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]if[代码] [代码]( url === undefined ) url = [代码][代码]''[代码][代码];[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].path !== undefined ) url = [代码][代码]this[代码][代码].path + url;[代码] [代码] [代码][代码]url = [代码][代码]this[代码][代码].manager.resolveURL( url );[代码] [代码] [代码][代码]var[代码] [代码]scope = [代码][代码]this[代码][代码];[代码] [代码] [代码][代码]var[代码] [代码]cached = Cache.get( url );[代码] [代码] [代码][代码]if[代码] [代码]( cached !== undefined ) {[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]setTimeout( [代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( cached );[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}, 0 );[代码] [代码] [代码][代码]return[代码] [代码]cached;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]//var image = document.createElementNS( 'http://www.w3.org/1999/xhtml', 'img' );[代码][代码] [代码][代码]console.log([代码][代码]this[代码][代码], canvas);[代码][代码] [代码][代码]var[代码] [代码]image = canvas.createImage();[代码] [代码] [代码][代码]function[代码] [代码]onImageLoad() {[代码] [代码] [代码][代码]//image.removeEventListener( 'load', onImageLoad, false );[代码][代码] [代码][代码]//image.removeEventListener( 'error', onImageError, false );[代码][代码] [代码][代码]image.onload = [代码][代码]function[代码] [代码]() { };[代码][代码] [代码][代码]image.onerror = [代码][代码]function[代码] [代码]() { };[代码] [代码] [代码][代码]Cache.add( url, [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]if[代码] [代码]( onLoad ) onLoad( [代码][代码]this[代码] [代码]);[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]function[代码] [代码]onImageError( event ) {[代码] [代码] [代码][代码]//image.removeEventListener( 'load', onImageLoad, false );[代码][代码] [代码][代码]//image.removeEventListener( 'error', onImageError, false );[代码][代码] [代码][代码]image.onload = [代码][代码]function[代码] [代码]() { };[代码][代码] [代码][代码]image.onerror = [代码][代码]function[代码] [代码]() { };[代码] [代码] [代码][代码]if[代码] [代码]( onError ) onError( event );[代码] [代码] [代码][代码]scope.manager.itemEnd( url );[代码][代码] [代码][代码]scope.manager.itemError( url );[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]//image.addEventListener( 'load', onImageLoad, false );[代码][代码] [代码][代码]//image.addEventListener( 'error', onImageError, false );[代码][代码] [代码][代码]image.onload = onImageLoad;[代码][代码] [代码][代码]image.onerror = onImageError;[代码] [代码] [代码][代码]if[代码] [代码]( url.substr( 0, 5 ) !== [代码][代码]'data:'[代码] [代码]) {[代码] [代码] [代码][代码]if[代码] [代码]( [代码][代码]this[代码][代码].crossOrigin !== undefined ) image.crossOrigin = [代码][代码]this[代码][代码].crossOrigin;[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]scope.manager.itemStart( url );[代码] [代码] [代码][代码]image.src = url;[代码] [代码] [代码][代码]return[代码] [代码]image;[代码] [代码] [代码][代码]},[代码]然后TextureLoader的load方法也改一下传参顺序: [代码]Object.assign( TextureLoader.prototype, {[代码] [代码] [代码][代码]crossOrigin: [代码][代码]'anonymous'[代码][代码],[代码] [代码] [代码][代码]load: [代码][代码]function[代码] [代码]( canvas,url, onLoad, onProgress, onError ) {[代码] [代码] [代码][代码]var[代码] [代码]texture = [代码][代码]new[代码] [代码]Texture();[代码] [代码] [代码][代码]var[代码] [代码]loader = [代码][代码]new[代码] [代码]ImageLoader( [代码][代码]this[代码][代码].manager );[代码][代码] [代码][代码]loader.setCrossOrigin( [代码][代码]this[代码][代码].crossOrigin );[代码][代码] [代码][代码]loader.setPath( [代码][代码]this[代码][代码].path );[代码] [代码] [代码][代码]loader.load( canvas,url, [代码][代码]function[代码] [代码]( image ) {[代码]OK! 这个例子代码我放在https://github.com/leo9960/xcx_threejs,大家可以接着研究一下。潜力还是比较大的,比如我拿它搞了个全景展示 [图片] ---------------------------------------------------------------------- 2019.5.26 新上传了全景展示的范例,基于panolens.js,欢迎围观
2019-05-26 - HQChart教程 - 如何快速的创建一个K线图
HQChart介绍 HQChart(https://github.com/jones2000/HQChart)是根据c++行情客户端软件移植到js平台上的一个项目。由金融图形库和麦语法(分析加语法)脚本执行器组成,所有指标计算都在前端完成,后台api只需要提供基础的行情数据就可以。 支持小程序,h5, 分2套代码完成,因为个个平台对canvas的支持和性能是不一样的,所有独立2套代码,最大的发挥个个平台canvas的性能。 支持手势操作(放大缩小,左右移动,十字光标) 。。。。。其他功能我就不介绍了,有兴趣的可以上github上看,上面有详细的使用教程和设计方案 创建K线图步骤 在WXML里面增加一个’canvas’元素, 并设置canvas-id, 定义好手势事件,bindtouchstart,bindtouchmove,bindtouchend [代码]<view class="container"> <canvas class="historychart" canvas-id="historychart" style="height:{{Height}}px; width:{{Width}}px;" bindtouchstart='historytouchstart' bindtouchmove='historytouchmove' bindtouchend='historytouchend'/> </view> [代码] 在对应的页面js文件里面 import HQChart组件 (wechathqchart/umychart.wechat.3.0.js) [代码]import { JSCommon } from "../wechathqchart/umychart.wechat.3.0.js"; [代码] 在page类里面创建HQChart组件,设置参数并绑定到’canvas’元素上 [代码]Page( { data: { Height: 0, Width: 0, }, HistoryChart: null, HistoryOption: { Type: '历史K线图', //如果要横屏类型使用:历史K线图横屏 Windows: //窗口指标 [ { Index: 'MA' }, { Index: 'VOL'}, { Index: 'RSI' } ], //ColorIndex: { Index: '五彩K线-双飞乌鸦' }, //五彩K线 //TradeIndex: { Index: '交易系统-BIAS' }, //交易系统 Symbol: "000001.sz", IsAutoUpate: true, //是自动更新数据 IsShowCorssCursorInfo: true, //是否显示十字光标的刻度信息 CorssCursorTouchEnd:true, //手离开屏幕 隐藏十字光标 KLine: { DragMode: 1, //拖拽模式 0 禁止拖拽 1 数据拖拽 2 区间选择 Right: 1, //复权 0 不复权 1 前复权 2 后复权 Period: 0, //周期 0 日线 1 周线 2 月线 3 年线 MaxReqeustDataCount: 1000, //数据个数 PageSize: 30, //一屏显示多少数据 Info: ["业绩预告", "公告", "互动易", "调研", "大宗交易", "龙虎榜"], //信息地雷 //Policy: ["30天地量", "20日均线,上穿62日均线"], //策略信息 InfoDrawType:0, DrawType: 0, }, //叠加股票 Overlay: [ //{Symbol:'000001.sz'} ], KLineTitle: //标题设置 { IsShowName: true, //显示股票名称 IsShowSettingInfo: true, //显示周期/复权 }, Border: //边框 { Left: 1, //左边间距 Right: 1, //右边间距 Top:30, Bottom:25, }, Frame: //子框架设置 [ { SplitCount: 3 }, { SplitCount: 2 }, { SplitCount: 2 }, ], ExtendChart: //扩展图形 [ { Name: 'KLineTooltip' } ], }, onLoad: function () { var self = this // 获取系统信息 wx.getSystemInfo({ success: function (res) { console.log(res); // 可使用窗口宽度、高度 //console.log('height=' + res.windowHeight); //console.log('width=' + res.windowWidth); self.setData({ Height: res.windowHeight, Width: res.windowWidth, Title: { Display:'block'}}); } }); }, onReady: function () { //创建历史K线类 var element = new JSCommon.JSCanvasElement(); element.ID = 'historychart'; element.Height = this.data.Height; //高度宽度需要手动绑定!! 微信没有元素类 element.Width = this.data.Width; this.HistoryChart = JSCommon.JSChart.Init(element); //把画布绑定到行情模块中 this.HistoryChart.SetOption(this.HistoryOption); }, //把画图事件绑定到hqchart控件上 historytouchstart: function (event) { if (this.HistoryChart) this.HistoryChart.OnTouchStart(event); }, historytouchmove: function (event) { if (this.HistoryChart) this.HistoryChart.OnTouchMove(event); }, historytouchend: function (event) { if (this.HistoryChart) this.HistoryChart.OnTouchEnd(event); }, }) [代码] 这样1个K线图+指标图就完成了 效果图 [图片] [图片] HQChart代码地址 地址: https://github.com/jones2000/HQChart
2019-06-15 - Lottie-前端实现AE动效
项目背景 在海外项目中,为了优化用户体验加入了几处微交互动画,实现方式是设计输出合成的雪碧图,前端通过序列帧实现动画效果: [图片] 序列帧: [图片] 动画效果: [图片] 序列帧: [图片] 帧动画的缺点和局限性比较明显,合成的雪碧图文件大,且在不同屏幕分辨率下可能会失真。经调研发现,Lottie是个简单、高效且性能高的动画方案。 Lottie是可应用于Android, iOS, Web和Windows的库,通过Bodymovin解析AE动画,并导出可在移动端和web端渲染动画的json文件。换言之,设计师用AE把动画效果做出来,再用Bodymovin导出相应地json文件给到前端,前端使用Lottie库就可以实现动画效果。 [图片] Bodymovin插件的安装与使用 关闭AE 下载并安装ZXP installer https://aescripts.com/learn/zxp-installer/ 下载最新版bodymovin插件 https://github.com/airbnb/lottie-web/blob/master/build/extension/bodymovin.zxp 把下载好的bodymovin.zxp拖到ZXP installer [图片] 打开AE,在菜单首选项->常规中勾选☑️允许脚本写入文件和访问网络(否则输出JSON文件时会失败) [图片] 在AE中制作动画,打开菜单窗口->拓展->Bodymovin,勾选要输出的动画,并设置输出文件目录,点击render [图片] 打开输出目录会看到生成的JSON文件,若动画里导入了外部图片,则会在images中存放JSON中引用的图片 前端使用lottie 静态URL https://cdnjs.com/libraries/lottie-web NPM [代码]npm install lottie-web [代码] 调用loadAnimation [代码]lottie.loadAnimation({ container: element, // 容器节点 renderer: 'svg', loop: true, autoplay: true, path: 'data.json' // JSON文件路径 }); [代码] vue-lottie 也可以在vue中使用lottie [代码] import lottie from '../lib/lottie'; import * as favAnmData from '../../raw/fav.json'; export default { props: { options: { type: Object, required: true }, height: Number, width: Number, }, data () { return { style: { width: this.width ? `${this.width}px` : '100%', height: this.height ? `${this.height}px` : '100%', overflow: 'hidden', margin: '0 auto' } } }, mounted () { this.anim = lottie.loadAnimation({ container: this.$refs.lavContainer, renderer: 'svg', loop: this.options.loop !== false, autoplay: this.options.autoplay !== false, animationData: favAnmData, assetsPath: this.options.assetsPath, rendererSettings: this.options.rendererSettings } ); this.$emit('animCreated', this.anim) } } [代码] loadAnimation参数 参数名 描述 container 用于渲染动画的HTML元素,需确保在调用loadAnimation时该元素已存在 renderer 渲染器,可选值为’svg’(默认值)/‘canvas’/‘html’。svg支持的功能最多,但html的性能更好且支持3d图层。各选项值支持的功能列表在此 loop 默认值为true。可传递需要循环的特定次数 autoplay 自动播放 path JSON文件路径 animationData JSON数据,与path互斥 name 传递该参数后,可在之后通过lottie命令引用该动画实例 rendererSettings 可传递给renderer实例的特定设置,具体可看 Lottie动画监听 Lottie提供了用于监听动画执行情况的事件: complete loopComplete enterFrame segmentStart config_ready(初始配置完成) data_ready(所有动画数据加载完成) DOMLoaded(元素已添加到DOM节点) destroy 可使用addEventListener监听事件 [代码]// 动画播放完成触发 anm.addEventListener('complete', anmLoaded); // 当前循环播放完成触发 anm.addEventListener('loopComplete', anmComplete); // 播放一帧动画的时候触发 anm.addEventListener('enterFrame', enterFrame); [代码] 控制动画播放速度和进度 可使用anm.pause和anm.play暂停和播放动画,调用anm.stop则会停止动画播放并回到动画第一帧的画面。 使用anm.setSpeed(speed)可调节动画速度,而anm.goToAndStop(value, isFrame)和anm.goToAndPlay可控制播放特定帧数,也可结合anm.totalFrames控制进度百分比,比如可传anm.totalFrames - 1跳到最后一帧。 [代码]anm.goToAndStop(anm.totalFrames - 1, 1); [代码] 这样的好处是可以把相关联的JSON文件合并,通过anm.goToAndPlay控制动画状态的切换,如下图例中一个JSON文件包含了2个动画状态的数据: [图片] 图片资源 JSON文件里assets设置了对图片的引用: [图片] 若想统一修改静态资源路径或者设置成绝对路径,可在调用loadAnimation时传入assetsPath参数: [代码]lottie.loadAnimation({ container: element, renderer: 'svg', path: 'data.json', assetsPath: 'URL' // 静态资源绝对路径 }); [代码] 功能支持列表 即使用bodymovin成功输出了JSON文件(没有报错),也会出现动效不如预期的情况,比如这是在AE中构建的形象图: [图片] 但在页面中渲染效果是这样的: [图片] 这是因为使用了不支持的Merge Paths功能 [图片] 因此对设计师而言,创建Lottie动画和往常制作AE动画有所不同,此文档记录了Bodymovin支持输出的AE功能列表,动画制作前需跟设计师沟通好,根据动画加载平台来确认可使用的AE功能。 除此之外,尽量遵循官方文档里对设计过程的指导和建议: 动画简单化。创建动画时需时刻记着保持JSON文件的精简,比如尽可能地绑定父子关系,在相似的图层上复制相同的关键帧会增加额外的代码,尽量不使用占用空间最多的路径关键帧动画。诸如自动跟踪描绘、颤动之类的技术会使得JSON文件变得非常大且耗性能。 建立形状图层。将AI、EPS、SVG和PDF等资源转换成形状图层否则无法在Lottie中正常使用,转换好后注意删除该资源以防被导出到JSON文件。 设置尺寸。在AE中可设置合成尺寸为任意大小,但需确保导出时合成尺寸和资源尺寸大小保持一致。 不使用表达式和特效。Lottie暂不支持。 注意遮罩尺寸。若使用alpha遮罩,遮照的大小会对性能产生很大的影响。尽可能地把遮罩尺寸维持到最小。 动画调试。若输出动画破损,通过每次导出特定图层来调试出哪些图层出了问题。然后在github中附上该图层文件提交问题,选择用其他方式重构该图层。 不使用混合模式和亮度蒙版。 不添加图层样式。 全屏动画。设置比想要支持的最宽屏幕更宽的导出尺寸。 设置空白对象。若使用空白对象,需确保勾选可见并设置透明度为0%否则不会被导出到JSON文件。 预览效果 由于以上所说的功能支持问题会导致输出动画效果不确定性,设计师和前端之间有个动画效果联调的过程,为了提高联调效率,设计师可先进行初步的效果预览,再把文件交付给前端。 方法1:输出预览HTML文件 渲染前设置所要渲染的文件 [图片] 勾选☑️Demo选项 [图片] 在输出的文件目录中就可找到可预览的demo.html文件 方法2:LottieFiles分享平台 把生成的JSON文件传到LottieFiles平台,可播放、暂停生成文件的动画效果,可设置图层颜色、动画速度,也可以下载lottie preview客户端在iOS或Android机子上预览。 [图片] LottieFiles平台还提供了很多线上公开的Lottie动画效果,可直接下载JSON文件使用 [图片] 交互hack Lottie的不足之处是没有对应的API操纵动画层,若想做更细化的动画处理,只能直接操作节点来实现。比如当播放完左图动画进入惊讶状态后,若想实现右图随鼠标移动而控制动画层的简单效果: [图片][图片] 开启调试面板可以看到,lottie-web通过使用<g>标签的transform属性来控制动画: [图片] 当元素已添加到DOM节点,找到想要控制的<g>标签,提取其transform属性的矩阵值,并使用rematrix解析矩阵值。 [代码]onIntroDone() { const Gs = this.refs.svg.querySelectorAll('svg > g > g > g'); Gs.forEach((node, i) => { // 过滤需要修改的节点 ... // 获取transform属性值 const styleArr = node.getAttribute('transform').split(','); styleArr[0] = styleArr[0].replace('matrix(', ''); styleArr[5] = styleArr[5].replace(')', ''); const style = `matrix(${styleArr[0]}, ${styleArr[1]}, ${styleArr[2]}, ${styleArr[3]}, ${styleArr[4]}, ${styleArr[5]})`; // 使用Rematrix解析 const transform = Rematrix.parse(style); this.matrices.push({ node, transform, prevTransform: transform }); } } [代码] 监听鼠标移动,设置新的transform属性值。 [代码]onMouseMove = (e) => { this.mouseCoords.x = e.clientX || e.pageX; this.mouseCoords.y = e.clientY || e.pageY; let x = this.mouseCoords.x - (this.props.browser.width / 2); let y = this.mouseCoords.y - (this.props.browser.height / 2); const diffX = (this.mouseCoords.prevX - x); const diffY = (this.mouseCoords.prevY - y); this.mouseCoords.prevX = x; this.mouseCoords.prevY = y; this.matrices.forEach((matrix, i) => { let translate = Rematrix.translate(diffX, diffY); const product = [matrix.prevTransform, translate].reduce(Rematrix.multiply); const css = `matrix(${product[0]}, ${product[1]}, ${product[4]}, ${product[5]}, ${product[12]}, ${product[13]})`; matrix.prevTransform = product; matrix.node.setAttribute('transform', css); }) } [代码] 进一步优化 看到一个方法,在AE中将图层命名为[代码]#id[代码]格式,生成的SVG相应的图层id会被设置为id,命名为[代码].class[代码]格式,相应的图层class会被设置为class [图片] 试了下的确可以,如下图,因此可通过这个方法快速找到需要操作的动画层,进一步简化代码: [图片] 小结 Lottie的缺点在于若在AE动画制作的过程不注意规范,会导致数据文件大、耗内存和性能的问题;Lottie-web的官方文档不够详尽,例如assetsPath参数是在看源码的时候发现的;开放的API不够齐全,无法很灵活地控制动画层。 而优点也很明显,Lottie能帮助提高开发效率,精简代码,易于调试和维护;资源文件小,输出动画效果保真;跨平台——Android, iOS, Web和Windows通用。 总的来说,Lottie的引用可以替代传统的GIF和帧动画,灵活利用好提供的属性和方法可以控制动画的播放,但需注意规范设计和开发的流程,才可以更高效地完成动画的制作与调试。
2019-03-25 - TypeScript入门完全指南(基础篇)
[TOC] 为什么JS需要类型检查 TypeScript的设计目标在这里可以查看到,简单概括为两点: 为JavaScript提供一个可选择的类型检查系统; 为JavaScript提供一个包含将来新特性的版本。 TypeScript的核心价值体现在第一点,第二点可以认为是TypeScript的向后兼容性保证,也是TypeScript必须要做到的。 那么为什么JS需要做静态类型检查呢?在几年前这个问题也许还会存在比较大的争议,在前端日趋复杂的今天,经过像Google、Microsoft、FaceBook这样的大公司实践表明,类型检查对于代码可维护性和可读性是有非常大的帮助的,尤其针对于需要长期维护的规模性系统。 TypeScript优势 在我看来,TypeScript能够带来最直观上的好处有三点: 帮助更好地重构代码; 类型声明本身是最好查阅的文档。 编辑器的智能提示更加友好。 一个好的代码习惯是时常对自己写过的代码进行小的重构,让代码往更可维护的方向去发展。然而对于已经上线的业务代码,往往测试覆盖率不会很高,当我们想要重构时,经常会担心自己的改动会产生各种不可预知的bug。哪怕是一个小的重命名,也有可能照顾不到所有的调用处造成问题。 如果是一个TypeScript项目,这种担心就会大大降低,我们可以依赖于TypeScript的静态检查特性帮助找出一个小的改动(如重命名)带来的其他模块的问题,甚至对于模块文件来说,我们可以直接借助编辑器的能力进行[代码]“一键重命名”[代码]操作。 另外一个问题,如果你接手过一个老项目,肯定会头痛于各种文档的缺失和几乎没有注释的代码,一个好的TypeScript项目,是可以做到代码即文档的,通过声明文件我们可以很好地看出各个字段的含义以及哪些是前端必须字段: [代码]// 砍价用户信息 export interface BargainJoinData { curr_price: number; // 当前价 curr_ts: number; // 当前时间 init_ts: number; // 创建时间 is_bottom_price: number; // 砍到底价 } [代码] TypeScript对开发者是友好的 TypeScript在设计之初,就确定了他们的目标并不是要做多么严格完备的类型强校验系统,而是能够更好地兼容JS,更贴合JS开发者的开发习惯。可以说这是MS的商业战略,也是TS能够成功的关键性因素之一。它对JS的兼容性主要表现为以下三个方面: 隐式的类型推断 [代码]var foo = 123; foo = "456"; // Error: cannot assign `string` to `number` [代码] 当我们对一个变量或函数等进行赋值时,TypeScript能够自动推断类型赋予变量,TypeScript背后有非常强大的自推断算法帮助识别类型,这个特性无疑可以帮助我们简化一些声明,不必像其他语言那样处处是声明,也可以让我们看代码时更加轻松。 结构化的类型 TypeScript旨在让JS开发者更简单地上手,因此将类型设计为“结构化”(Structural)的而非“名义式”(Nominal)的。 什么意思呢?意味着TypeScript的类型并不根据定义的名字绑定,只要是形似的类型,不管名称相不相同,都可以作为兼容类型(这很像所谓的duck typing),也就是说,下面的代码在TypeScript中是完全合法的: [代码]class Foo { method(input: string) { /* ... */ } } class Bar { method(input: string) { /* ... */ } } let test: Foo = new Bar(); // no Error! [代码] 这样实际上可以做到类型的最大化复用,只要形似,对于开发者也是最好理解的。(当然对于这个示例最好的做法是抽出一个公共的interface) 知名的JS库支持 TypeScript有强大的DefinitelyTyped社区支持,目前类型声明文件基本上已经覆盖了90%以上的常用JS库,在编写代码时我们的提示是非常友好的,也能做到安全的类型检查。(在使用第三方库时,可以现在这个项目中检索一下有没有该库的TS声明,直接引入即可) 回顾两个基础知识 在进入正式的TS类型介绍之前,让我们先回顾一下JS的两个基础: 相等性判断 我们都知道,在JS里,两个等号的判断会进行隐式的类型转换,如: [代码]console.log(5 == "5"); // true console.log(0 == ""); // true [代码] 在TS中,因为有了类型声明,因此这两个结果在TS的类型系统中恒为false,因此会有报错: [代码]This condition will always return 'false' since the types '5' and '"5"' have no overlap. [代码] 所以在代码层面,一方面我们要避免这样两个不同类型的比较,另一方面使用全等来代替两个等号,保证在编译期和运行期具有相同的语义。 对于TypeScript而言,只有[代码]null[代码]和[代码]undefined[代码]的隐式转换是合理的: [代码]console.log(undefined == undefined); // true console.log(null == undefined); // true console.log(0 == undefined); // false console.log('' == undefined); // false console.log(false == undefined); // false [代码] 类(Class) 对于ES6的Class,我们本身已经很熟悉了,值得一提的是,目前对于类的静态属性、成员属性等有一个提案——proposal-class-fields已经进入了Stage3,这个提案包含了很多东西,主要是类的静态属性、成员属性、公有属性和私有属性。其中,私有属性的提案在社区内引起了非常大的争议,由于它的丑陋和怪异遭受各路人马的抨击,现TC39委员会已决定重新思考该提案。 现在让我们来看看TypeScript对属性访问控制的情况: 可访问性 public protected private 类本身 是 是 是 子类 是 是 否 类的实例 是 否 否 可以看到,TS中的类成员访问和其他语言非常类似: [代码]class FooBase { public x: number; private y: number; protected z: number; } [代码] 对于类的成员构造函数初始化,TS提供了一个简单的声明方式: [代码]class Foo { constructor(public x:number) { } } [代码] 这段代码和下面是等同的: [代码]class Foo { x: number; constructor(x:number) { this.x = x; } } [代码] TS类型系统基础 基本性准则 在正式了解TypeScript之前,首先要明确两个基本概念: TypeScript的类型系统设计是可选的,意味着JavaScript就是TypeScript。 TypeScript的报错并不会阻止JS代码的生成,你可以渐进式地将JS逐步迁移为TS。 基本语法 [代码]:<TypeAnnotation> [代码] TypeScript的基本类型语法是在变量之后使用冒号进行类型标识,这种语法也揭示了TypeScript的类型声明实际上是可选的。 原始值类型 [代码]var num: number; var str: string; var bool: boolean; [代码] TypeScript支持三种原始值类型的声明,分别是[代码]number[代码]、[代码]string[代码]和[代码]boolean[代码]。 对于这三种原始值,TS同样支持以它们的字面量为类型: [代码]var num: 123; var str: '123'; var bool: true; [代码] 这类字面量类型配合上联合类型还是十分有用的,我们后面再讲。 数组类型 对于数组的声明也非常简单,只需要加上一个中括号声明类型即可: [代码]var boolArray: boolean[]; [代码] 以上就简单地定义了一个布尔类型的数组,大多数情况下,我们数组的元素类型是固定的,如果我们数组内存在不同类型的元素怎么办? 如果元素的个数是已知有限的,可以使用TS的元组类型: [代码]var nameNumber: [string, number]; [代码] 该声明也非常的形象直观,如果元素个数不固定且类型未知,这种情况较为罕见,可直接声明成any类型: [代码]var arr: any[] [代码] 接口类型 接口类型是TypeScript中最常见的组合类型,它能够将不同类型的字段组合在一起形成一个新的类型,这对于JS中的对象声明是十分友好的: [代码]interface Name { first: string; second: string; } var personName:Name = { first: '张三' } // Property 'second' is missing in type '{ first: string; }' but required in type 'Name' [代码] 上述例子可见,TypeScript对每一个字段都做了检查,若未定义接口声明的字段(非可选),则检查会抛出错误。 内联接口 对于对象来说,我们也可以使用内联接口来快速声明类型: [代码]var personName:{ first: string, second: string } = { first: '张三' } // Property 'second' is missing in type '{ first: string; }' but required in type 'Name' [代码] 内联接口可以帮助我们快速声明类型,但建议谨慎使用,对于可复用以及一般性的接口声明建议使用interface声明。 索引类型 对于对象而言,我们可以使用中括号的方式去存取值,对TS而言,同样支持相应的索引类型: [代码]interface Foo { [key:string]: number } [代码] 对于索引的key类型,TypeScript只支持[代码]number[代码]和[代码]string[代码]两种类型,且Number是string的一种特殊情况。 对于索引类型,我们在一般化的使用场景上更方便: [代码]interface NestedCSS { color?: string; nest?: { [selector: string]: NestedCSS; } } const example: NestedCSS = { color: 'red', nest: { '.subclass': { color: 'blue' } } } [代码] 类的接口 对于接口而言,另一个重要作用就是类可以实现接口: [代码]interface Point { x: number; y: number; z: number; // New member } class MyPoint implements Point { // ERROR : missing member `z` x: number; y: number; } [代码] 对类而言,实现接口,意味着需要实现接口的所有属性和方法,这和其他语言是类似的。 函数类型 函数是TypeScript中最常见的组成单元: [代码]interface Foo { foo: string; } // Return type annotated as `: Foo` function foo(sample: Foo): Foo { return sample; } [代码] 对于函数而言,本身有参数类型和返回值类型,都可进行声明。 可选参数 对于参数,我们可以声明可选参数,即在声明之后加一个问号: [代码]function foo(bar: number, bas?: string): void { // .. } [代码] void和never类型 另外,上述例子也表明,当函数没有返回值时,可以用[代码]void[代码]来表示。 当一个函数永远不会返回时,我们可以声明返回值类型为[代码]never[代码]: [代码]function bar(): never { throw new Error('never reach'); } [代码] callable和newable 我们还可以使用接口来定义函数,在这种函数实现接口的情形下,我们称这种定义为[代码]callable[代码]: [代码]interface Complex { (bar?: number, ...others: boolean[]): number; } var foo: Complex; [代码] 这种定义方式在可复用的函数声明中非常有用。 callable还有一种特殊的情况,该声明中指定了[代码]new[代码]的方法名,称之为[代码]newable[代码]: [代码]interface CallMeWithNewToGetString { new(): string } var foo: CallMeWithNewToGetString; new foo(); [代码] 这个在构造函数的声明时非常有用。 函数重载 最后,一个函数可以支持多种传参形式,这时候仅仅使用可选参数的约束可能是不够的,如: [代码]unction padding(a: number, b?: number, c?: number, d?: number) { if (b === undefined && c === undefined && d === undefined) { b = c = d = a; } else if (c === undefined && d === undefined) { c = a; d = b; } return { top: a, right: b, bottom: c, left: d }; } [代码] 这个函数可以支持四个参数、两个参数和一个参数,如果我们粗略的将后三个参数都设置为可选参数,那么当传入三个参数时,TS也会认为它是合法的,此时就失去了类型安全,更好的方式是声明函数重载: [代码]function padding(all: number); function padding(topAndBottom: number, leftAndRight: number); function padding(top: number, right: number, bottom: number, left: number); function padding(a: number, b?: number, c?: number, d?: number) { //... } [代码] 函数重载写法也非常简单,就是重复声明不同参数的函数类型,最后一个声明包含了兼容所有重载声明的实现。这样,TS类型系统就能准确的判断出该函数的多态性质了。 使用[代码]callable[代码]的方式也可以声明重载: [代码]interface Padding { (all: number): any (topAndBottom: number, leftAndRight: number): any (top: number, right: number, bottom: number, left: number): any } [代码] 特殊类型 any [代码]any[代码]在TypeScript中是一个比较特殊的类型,声明为[代码]any[代码]类型的变量就像动态语言一样不受约束,好像关闭了TS的类型检查一般。对于[代码]any[代码]类型的变量,可以将其赋予任何类型的值: [代码]var power: any; power = '123'; power = 123; [代码] [代码]any[代码]对于JS代码的迁移是十分友好的,在已经成型的TypeScript项目中,我们要慎用[代码]any[代码]类型,当你设置为[代码]any[代码]时,意味着告诉编辑器不要对它进行任何检查。 null和undefined [代码]null[代码]和[代码]undefined[代码]作为TypeScript的特殊类型,它同样有字面量的含义,之前我们已经了解到。 值得注意的是,[代码]null[代码]和[代码]undefined[代码]可以赋值给任意类型的变量: [代码]var num: number; var str: string; // 赋值给任意类型的变量都是合法的 num = null; str = undefined; [代码] void和never 在函数类型中,我们已经介绍了两种类型,专门修饰函数返回值。 readonly [代码]readonly[代码]是只读属性的修饰符,当我们的属性是只读时,可以用该修饰符加以约束,在类中,用[代码]readonly[代码]修饰的属性仅可以在构造函数中初始化: [代码]class Foo { readonly bar = 1; // OK readonly baz: string; constructor() { this.baz = "hello"; // OK } } [代码] 一个实用场景是在[代码]react[代码]中,[代码]props[代码]和[代码]state[代码]都是只读的: [代码]interface Props { readonly foo: number; } interface State { readonly bar: number; } export class Something extends React.Component<Props,State> { someMethod() { this.props.foo = 123; // ERROR: (props are immutable) this.state.baz = 456; // ERROR: (one should use this.setState) } } [代码] 当然,[代码]React[代码]本身在类的声明时会对传入的[代码]props[代码]和[代码]state[代码]做一层[代码]ReadOnly[代码]的包裹,因此无论我们是否在外面显式声明,赋值给[代码]props[代码]和[代码]state[代码]的行为都是会报错的。 注意,[代码]readonly[代码]听起来和[代码]const[代码]有点像,需要时刻保持一个概念: [代码]readonly[代码]是修饰属性的 [代码]const[代码]是声明变量的 泛型 在更加一般化的场景,我们的类型可能并不固定已知,它和[代码]any[代码]有点像,只不过我们希望在[代码]any[代码]的基础上能够有更近一步的约束,比如: [代码]function reverse<T>(items: T[]): T[] { var toreturn = []; for (let i = items.length - 1; i >= 0; i--) { toreturn.push(items[i]); } return toreturn; } [代码] [代码]reverse[代码]函数是一个很好的示例,对于一个通用的函数[代码]reverse[代码]来说,数组元素的类型是未知的,可以是任意类型,但[代码]reverse[代码]函数的返回值也是个数组,它和传入的数组类型是相同的,对于这个约束,我们可以使用泛型,其语法是尖括号,内置泛型变量,多个泛型变量用逗号隔开,泛型变量名称没有限制,一般而言我们以大写字母开头,多个泛型变量使用其语义命名,加上[代码]T[代码]为前缀。 在调用时,可以显示的指定泛型类型: [代码]var reversed = reverse<number>([1, 2, 3]); [代码] 也可以利用TypeScript的类型推断,进行隐式调用: [代码]var reversed = reverse([1, 2, 3]); [代码] 由于我们的参数类型是[代码]T[][代码],而传入的数组类型是一个[代码]number[][代码],此时[代码]T[代码]的类型被TypeScript自动推断为[代码]number[代码]。 对于泛型而言,我们同样可以作用于接口和类: [代码]interface Array<T> { reverse(): T[]; // ... } [代码] 联合类型 在JS中,一个变量的类型可能拥有多个,比如: [代码]function formatCommandline(command: string[]|string) { var line = ''; if (typeof command === 'string') { line = command.trim(); } else { line = command.join(' ').trim(); } } [代码] 此时我们可以使用一个[代码]|[代码]分割符来分割多种类型,对于这种复合类型,我们称之为[代码]联合类型[代码]。 交叉类型 如果说联合类型的语义等同于[代码]或者[代码],那么交叉类型的语义等同于集合中的[代码]并集[代码],下面的[代码]extend[代码]函数是最好的说明: [代码]function extend<T, U>(first: T, second: U): T & U { let result = <T & U> {}; for (let id in first) { result[id] = first[id]; } for (let id in second) { if (!result.hasOwnProperty(id)) { result[id] = second[id]; } } return result; } [代码] 该函数最终以[代码]T&U[代码]作为返回值值,该类型既包含了[代码]T[代码]的字段,也包含了[代码]U[代码]的字段,可以看做是两个类型的[代码]并集[代码]。 类型别名 TypeScript为类型的复用提供了更便捷的方式——类型别名。当你想复用类型时,可能在该场景下要为已经声明的类型换一个名字,此时可以使用type关键字来进行类型别名的定义: [代码]interface state { a: 1 } export type userState = state; [代码] 我们同样可以使用type来声明一个类型: [代码]type Text = string | { text: string }; type Coordinates = [number, number]; type Callback = (data: string) => void; [代码] 对于type和interface的取舍: 如果要用交叉类型或联合类型,使用type。 如果要用extend或implement,使用interface。 其余情况可看个人喜好,个人建议type更多应当用于需要起别名时,其他情况尽量使用interface。 枚举类型 对于组织一系列相关值的集合,最好的方式应当是枚举,比如一系列状态集合,一系列归类集合等等。 在TypeScript中,枚举的方式非常简单: [代码]enum Color { Red, Green, Blue } var col = Color.Red; [代码] 默认的枚举值是从0开始,如上述代码,[代码]Red=0[代码],[代码]Green=1[代码]依次类推。 当然我们还可以指定初始值: [代码]enum Color { Red = 3, Green, Blue } [代码] 此时[代码]Red=3[代码], [代码]Green=4[代码]依次类推。 大家知道在JavaScript中是不存在枚举类型的,那么TypeScript的枚举最终转换为JavaScript是什么样呢? [代码]var Color; (function (Color) { Color[Color["Red"] = 0] = "Red"; Color[Color["Green"] = 1] = "Green"; Color[Color["Blue"] = 2] = "Blue"; })(Color || (Color = {})); [代码] 从编译后的代码可以看到,转换为一个key-value的对象后,我们的访问也非常方便: [代码]var red = Color.Red; // 0 var redKey = Color[0]; // 'Red' var redKey = Color[Color.Red]; // 'Red' [代码] 既可以通过key来访问到值,也可以通过值来访问到key。 Flag标识位 对于枚举,有一种很实用的设计模式是使用位运算来标识(Flag)状态: [代码]enum EnvFlags { None = 0, QQ = 1 << 0, Weixin = 1 << 1 } function initShare(flags: EnvFlags) { if (flags & EnvFlags.QQ) { initQQShare(); } if (flags & EnvFlags.Weixin) { initWeixinShare(); } } [代码] 在我们使用标识位时,可以遵循以下规则: 使用 [代码]|=[代码] 增加标志位 使用 [代码]&=[代码] 和 [代码]~[代码]清除标志位 使用 [代码]|[代码] 联合标识位 如: [代码]var flag = EnvFlags.None; flag |= EnvFlags.QQ; // 加入QQ标识位 Flag &= ~EnvFlags.QQ; // 清除QQ标识位 Flag |= EnvFlags.QQ | EnvFlags.Weixin; // 加入QQ和微信标识位 [代码] 常量枚举 在枚举定义加上[代码]const[代码]声明,即可定义一个常量枚举: [代码]enum Color { Red = 3, Green, Blue } [代码] 对于常量枚举,TypeScript在编译后不会产生任何运行时代码,因此在一般情况下,应当优先使用常量枚举,减少不必要代码的产生。 字符串枚举 TypeScript还支持非数字类型的枚举——字符串枚举 [代码]export enum EvidenceTypeEnum { UNKNOWN = '', PASSPORT_VISA = 'passport_visa', PASSPORT = 'passport', SIGHTED_STUDENT_CARD = 'sighted_tertiary_edu_id', SIGHTED_KEYPASS_CARD = 'sighted_keypass_card', SIGHTED_PROOF_OF_AGE_CARD = 'sighted_proof_of_age_card', } [代码] 这类枚举和我们之前使用JavaScript定义常量集合的方式很像,好处在于调试或日志输出时,字符串比数字要包含更多的语义。 命名空间 在没有模块化的时代,我们为了防止全局的命名冲突,经常会以命名空间的形式组织代码: [代码](function(something) { something.foo = 123; })(something || (something = {})) [代码] TypeScript内置了[代码]namespace[代码]变量帮助定义命名空间: [代码]namespace Utility { export function log(msg) { console.log(msg); } export function error(msg) { console.error(msg); } } [代码] 对于我们自己的工程项目而言,一般建议使用ES6模块的方式去组织代码,而命名空间的模式可适用于对一些全局库的声明,如jQuery: [代码]namespace $ { export function ajax(//...) {} } [代码] 当然,命名空间还可以便捷地帮助我们声明静态方法,如和[代码]enum[代码]的结合使用: [代码]enum Weekday { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday } namespace Weekday { export function isBusinessDay(day: Weekday) { switch (day) { case Weekday.Saturday: case Weekday.Sunday: return false; default: return true; } } } const mon = Weekday.Monday; const sun = Weekday.Sunday; console.log(Weekday.isBusinessDay(mon)); // true console.log(Weekday.isBusinessDay(sun)); // false [代码] 关于命名规范 变量名、函数和文件名 推荐使用驼峰命名。 [代码]// Bad var FooVar; function BarFunc() { } // Good var fooVar; function barFunc() { } [代码] 类、命名空间 推荐使用帕斯卡命名。 成员变量和方法推荐使用驼峰命名。 [代码]// Bad class foo { } // Good class Foo { } // Bad class Foo { Bar: number; Baz() { } } // Good class Foo { bar: number; baz() { } } [代码] Interface、type 推荐使用帕斯卡命名。 成员字段推荐使用驼峰命名。 [代码]// Bad interface foo { } // Good interface Foo { } // Bad interface Foo { Bar: number; } // Good interface Foo { bar: number; } [代码] 关于模块规范 [代码]export default[代码]的争论 关于是否应该使用[代码]export default[代码]在这里有详尽的讨论,在AirBnb规范中也有[代码]prefer-default-export[代码]这条规则,但我认为在TypeScript中应当尽量不使用[代码]export default[代码]: 关于链接中提到的重命名问题, 甚至自动import,其实export default也是可以做到的,借助编辑器和TypeScript的静态能力。所以这一点还不是关键因素。 不过使用一般化的[代码]export[代码]更让我们容易获得智能提示: [代码]import /* here */ from 'something'; [代码] 在这种情况下,一般编辑器是不会给出智能提示的。 而这种: [代码]import { /* here */ } from 'something'; [代码] 我们可以通过智能提示做到快速引入。 除了这一点外,还有以下几点好处: 对CommonJS是友好的,如果使用export default,在commonJS下需要这样引入: [代码]const {default} = require('module/foo'); [代码] 多了个default无疑感觉非常奇怪。 对动态import是友好的,如果使用export default,还需要显示的通过default字段来访问: [代码]const HighChart = await import('https://code.highcharts.com/js/es-modules/masters/highcharts.src.js'); Highcharts.default.chart('container', { ... }); // 注意 `.default` [代码] 对于[代码]re-exporting[代码]是友好的,如果使用export default,那么进行[代码]re-export[代码]会比较麻烦: [代码]import Foo from "./foo"; export { Foo } [代码] 相比之下,如果没有[代码]export default[代码],我们可以直接使用: [代码]export * from "./foo" [代码] 实践中的一些坑 实践篇即将到来,敬请期待~
2019-03-18 - 深入解析JS的异步机制
1. JavaScript定义 JavaScript 是一种单线程编程语言,这意味着同一时间只能完成一件事情。也就是说,JavaScript 引擎只能在单一线程中处理一次语句。 优点:单线程语言简化了代码编写,因为你不必担心并发问题,但这也意味着你无法在不阻塞主线程的情况下执行网络请求等长时间操作。 缺点:当从 API 中请求一些数据。根据情况,服务器可能需要一些时间来处理请求,同时阻塞主线程,让网页无法响应。 2. 异步运行机制 CallBack,setTimeOut,ajax 等都是通过**事件循环(event loop)**实现的。 2.1 什么是Event Loop? 主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。 2.2 流程整体示意图 [图片] 2.3 总结异步运行到整体机制 主线程在运行的时候,将产生堆(heap)和栈(stack),栈中的代码会调用各种外部API,它们将在"任务队列"中根据类型不同,分类加入到相关任务队列中,如各种事件等。只要栈中的代码执行完毕,主线程就会去读取"任务队列",根据任务队列的优先级依次执行那些事件所对应的回调函数。这就是整体的事件循环。 2.4 任务队列的优先级 微任务队列中的所有任务都将在宏队列中的任务之前执行。也就是说,事件循环将首先在执行宏队列中的任何回调之前清空微任务队列。 ** 举例: ** [代码] console.log('Script start'); setTimeout(() => { console.log("setTimeout 1"); }, 0); setTimeout(() => { console.log("setTimeout 2"); }, 0); new Promise ((resolve, reject) => { resolve("Promise 1 resolved"); }) .then(res => console.log(res)) .catch(err => console.log(err)); new Promise ((resolve, reject) => { resolve("Promise 2 resolved"); }) .then(res => console.log(res)) .catch(err => console.log(err)); console.log('Script end'); [代码] 运行结果是: Script start Script end Promise 1 Promise 2 setTimeout 1 setTimeout 2 通过上述例子可以看到无论宏队列的位置在何方,只要微队列尚未清空,一定会先清空微队列后,在去执行宏队列。下面介绍微队列任务中比较典型的几个API,通过相关举例,让你更深入理解JS的异步机制。 3. 微任务队列 3.1 Promise(ES6) Promise,就是一个对象,用来传递异步操作的消息。 3.1.1 基础用法: [代码] var promise = new Promise(function(resolve, reject) { //异步处理逻辑 //处理结束后,调用resolve返回正常内容或调用reject返回异常内容 }) promise.then(function(result){ //正常返回执行部分,result是resolve返回内容 }, function(err){ //异常返回执行部分,err是reject返回内容 }) .catch(function(reason){ //catch效果和写在then的第二个参数里面一样。另外一个作用:在执行resolve的回调时,如果抛出异常了(代码出错了),那么并不过报错卡死JS,而是会进入到这个catch方法中,所以一般用catch替代then的第二个参数 }); [代码] 缺点: 无法取消 Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。再次,当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。 优点: Promise能够简化层层回调的写法,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。 3.1.2 用法注意点 - 顺序: [代码] new Promise((resolve, reject) => { resolve(1); console.log(2); }).then(r => { console.log(r); }); [代码] 运行结果是: 2 1 说明: 立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。也就是resolve(1)和console.log(2)是属于同步任务,需要全部执行完同步任务后,再去循环到resolve的then中。 3.1.3 用法注意点 - 状态: [代码] const p1 = new Promise(function (resolve, reject) { setTimeout(() => reject(new Error('fail')), 3000); }); const p2 = new Promise(function (resolve, reject) { setTimeout(() => resolve(p1), 1000); }); const p3 = new Promise(function (resolve, reject) { setTimeout(() => resolve(new Error('fail')), 1000); }); p2 .then(result => console.log("1:", result)) .catch(error => console.log("2:",error)); p3 .then(result => console.log("3:", result)) .catch(error => console.log("4:",error)); [代码] 运行结果是: 3: Error: fail at setTimeout (async.htm:182) 2: Error: fail at setTimeout (async.htm:174) 说明: p1是一个 Promise,3 秒之后变为rejected。p2和p3的状态是在 1 秒之后改变,p2 resolve方法返回的是 p1, p3 resolve方法返回的是 抛出异常。但由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以后面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数。 而p3返回的是自身的resolve,所以触发then中指定的回调函数。 3.1.4 用法注意点 - then链的处理: [代码] var p1 = p2 = new Promise(function (resolve){ resolve(100); }); p1.then((value) => { return value*2; }).then((value) => { return value*2; }).then((value) => { console.log("p1的执行结果:",value) }) p2.then((value) => { return value*2; }) p2.then((value) => { return value*2; }) p2.then((value) => { console.log("p2的执行结果:",value) }) [代码] 运行结果是: p2的执行结果: 100 p1的执行结果: 400 说明: p2写法中的 then 调用几乎是在同时开始执行的,而且传给每个 then 方法的 value 值都是 100。而p1中写法则采用了方法链的方式将多个 then 方法调用串连在了一起,各函数也会严格按照 resolve → then → then → then 的顺序执行,并且传给每个 then 方法的 value 的值都是前一个promise对象通过 return 返回的值。 ###3.1.4 用法注意点 - catch的处理: [代码] var p1 = new Promise(function (resolve, reject){ reject("test"); //throw new Error("test"); 效果同reject("test"); //reject(new Error("test")); 效果同reject("test"); resolve("ok"); }); p1 .then(value => console.log("p1 then:", value)) .catch(error => console.log("p1 error:", error)); p2 = new Promise(function (resolve, reject){ resolve("ok"); reject("test"); }); p2 .then(value => console.log("p2 then:", value)) .catch(error => console.log("p2 error:", error)); [代码] 运行结果是: p2 then: ok p1 error: test 说明: Promise 的状态一旦改变,就永久保持该状态,不会再变了。不会即抛异常又会正常resolve。 3.2 async/await(ES7) 3.2.1 async基础用法: async 用于申明一个 function 是异步的,返回的是一个 Promise 对象。 [代码] async function testAsync() { return "hello async"; } var result = testAsync(); console.log("1:", result); testAsync().then(result => console.log("2:", result)); async function mytest() { //"hello async"; } var result1 = mytest(); console.log("3:", result1); [代码] 运行结果是: 1: Promise {<resolved>: “hello async”} 3: Promise {<resolved>: undefined} 2: hello async 说明: async返回的是一个Promise对象,可以用 then 来接收,如果没有返回值的情况下,它会返回 Promise.resolve(undefined),所以在没有 await 的情况下执行 async 函数,它会立即执行,并不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。 3.2.2 await基础用法: await 只能出现在 async 函数中,用于等待一个异步方法执行完成(实际等的是一个返回值,强调 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果)。 [代码] function getMyInfo() { return Promise.resolve("hello 2019!"); } async function testAsync() { return "hello async"; } async function mytest() { return Promise.reject("hello async"); } async function test() { try { const v1 = await getMyInfo(); console.log("getV1"); const v2 = await testAsync(); console.log("getV2"); const v3 = await mytest(); console.log(v1, v2, v3); } catch (error) { console.log("error:", error); } } test(); [代码] 运行结果是: getV1 getV2 error: hello async 说明: await等到的如果是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。 放心,这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。 3.2.3 async/await的优势: 很多情况下,执行下一步操作是需要依赖上一步的返回结果的,如果当嵌套层次较多的时候,(举例3层的时候): [代码] const getRequest = () => { return promise1().then(result1 => { //do something return promise2(result1).then(result2 => { //do something return promise3(result1, result2) }) }) } [代码] 从上例可以看到嵌套内容太多。此时如果用async写法,可写成如下: [代码] const getRequest = async () => { const result1 = await promise1(); const result2 = await promise2(result1); return promise3(result1, result2); } [代码] 说明: async / await 使你的代码看起来像同步代码,它有效的消除then链,让你的代码更加简明,清晰。 总结:以上就是对JS的异步机制及相关应用的整体总结,如有需要欢迎交流~
2019-03-12 - PWA 实践之路
注:本文需要有一定的 PWA 基础 1. 什么是 PWA? 要知道一个东西是什么,我们通常可以从它的名字入手 因此我们看下 PWA 的全称是: Progressive Web App 回答 what 这种问题,重点在于名词,因此 PWA 是一个 APP,一个独立的、增强的、Web 实现的 APP 要达到这样的目的,PWA 提供了一系列的技术 & 标准,如下图所示: [图片] 具体每一项技术是什么就不再赘述了,感兴趣的同学自行网上搜索! 下面有一个简单的 demo 可以简单体会一下: [图片] 以后我们的 web 站点可以像 app 一样,这难道不是一个令人兴奋的事情吗? 所以 PWA 是值得我们前端开发者一直关注的技术! 按照目前的兼容性和环境来看,大家应用最多的还是 Service Worker,因此接下来我们也是把重点放在 SW 上面 那什么是 Service Worker ? 大家都知道就不卖关子了,其实就是一个 Cache 说到 Cache,就一定会想到性能优化了,请看我们的第二部分 2. 首屏优化 2.1. 静态资源优化 如何利用 Cache 来进行优化?这个基本套路应该无人不知了: [图片] 那么首次加载怎么办呢?首次加载是没有缓存资源的,所以会走到线上,所以等于没有任何优化 答案就是 Cache 的第二种常用技巧: precache(预加载) 预加载的意思就是在某个地方或特定时机预先把需要用到的资源加载并缓存 我们的做法如下图所示: [图片] 构建的时候,把整个项目用到的资源输出到一个 list 中,然后 inline 到 sw.js 里面 当 sw install 时,就会把这个 list 的资源全部请求并进行缓存 这样做的结果就是,无论用户第一次进入到我们站点的哪个页面,我们都会把整个站点所有的资源都加载回来并缓存 当用户跳转另外一个页面的时候,Cache 里面就有相应的资源了! 这是我们辅导课堂页面接入 sw 之后的首屏优化效果: [图片] 2.2. 动态数据优化 除了静态资源之外,我们还能缓存其他的内容吗? 答案肯定是可以的,我们还可以缓存 cgi 数据! [图片] 缓存 cgi 数据的流程和缓存静态资源的流程主要有2个差别,上图标红的地方: 需要添加一个开关功能,因为不是所有 cgi 都需要缓存的! 页面需要展示最新的数据,因此在返回缓存结果之后,还需要请求线上最新的数据,更新缓存,并且返回给页面展示,也就是说页面需要展示2次 页面展示2次需要考虑页面跳动的体验问题,选择权在于业务本身! 这是我们辅导上课页接入该功能后的首屏优化效果: [图片] 动态数据缓存是否有意义还需要额外的逻辑来判断,这块暂时是没有做的,后续会补上相关统计 2.3. 直出html优化 还能缓存什么?我们想到了直出的 html 这里就不展开讲了,因为我们的 jax 同学已经分享了一篇优秀的文章《企鹅辅导课程详情页毫秒开的秘密 - PWA 直出》,可以去看看哈~ 3. 替代离线包 PWA 与离线包本质上是一样的,都是离线缓存 正好,我们 PC 客户端的离线包系统年久失修,在这个契机下,我们启动了使用 PWA 替换离线包的方案! 核心流程不变,基本和缓存静态资源的流程是一致的 但是离线包系统是非常成熟的系统,要完全替换掉它还需要考虑许多方面的问题。 3.1. 更新机制 离线包有个自动更新的机制,每隔一段时间就会去请求离线包管理系统是否有更新,有的话就把最新的离线包拉下来自动更新替换,这样只需要1次跳转就能展示最新的页面。 SW 没有自动更新的逻辑,它需要在页面加载(一次跳转)之后才会去请求 sw.js,判断有变化才会进行更新,更新完了要等到下一次页面跳转(二次跳转)才能展示最新的页面。 这里有两个方案: 参考离线包的更新机制,也给 SW 实现一个自动更新的逻辑,借用 update 接口是可以做到主动去执行 SW 更新的。但是非常遗憾,我们的客户端 webkit 内核版本太低,并不支持这个接口 在第一次跳转之后更新 sw,然后检测 sw 状态,发现如果有更新,就用一定的策略来进行页面的刷新 我们使用第2个方案,部分代码如下: [图片] 在检测到 sw 更新之后,我们可以选择强刷,或者提示用户手动刷新页面,具体实现页面可以通过监听事件来处理 更多详细的实现方案可以参考这篇文章哈:How to Fix the Refresh Button When Using Service Workers 3.2. 首次打开问题 一般离线包是打进 app 的安装包一起发布的,在用户下载安装之后,离线包就已经存在于本地,因此第一次打开就能享受到离线包的缓存。 但是 sw 没有这个能力,同样我们也有两个方案: 在 app 安装的时候,添加一步,通过创建 webview 加载页面,页面执行 SW 的初始化工作,并展示相应的进度提示,在安装完成后需要把 webview 的 SW Cache 底层文件 copy 到相应的安装位置。这个方案太复杂,衡量下来没有采用。 在 app 启动时,创建一个隐藏的 webview,加载空页面去加载 SW。 我们使用第2个方案,因为我们的 app 启动会先让用户登录,如下图所示: [图片] 注:这里 app 不是移动端 app,是 pc 的客户端 app 3.3. 屏蔽机制 有时候我们不想使用离线缓存能力,比如在我们开发的时候 在离线包系统,通常会有一个开发者选项是【屏蔽离线包】 SW 也是需要这种能力的,这个方案就比较简单了,在 sw.js 的逻辑里有一个全局的开关,当开关关闭时,就不会走缓存逻辑 因此,我们可以在 dev 环境下把开关关闭即可达到屏蔽的作用 [图片] 3.4. 降级方案 当我们发布了一个错误代码的时候,我们需要快速降级容错的能力 在离线包系统里面有个过期的功能,可以把某个版本设置过期,也就是废弃掉: [图片] 我们利用之前提到的全局开关,通过一个管理接口来设置开关的起开和关闭,即可达到快速降级的目的: [图片] 整个流程大致是这样: 发布了错误代码,并且用户本地 sw 已经更新缓存了错误代码 在管理端关闭使用缓存开关,让用户走线上 快速修复代码并发布,到这里页面就已经恢复正常 在管理端开启使用缓存开关,恢复 SW 功能 请求管理接口是轮询的,这里后续有计划会改成 push,这样会更加及时,当然还要详细评估方案之后才能落实。 4. 如何方便接入? 我们把上述功能集成到了一个 webpack 插件当中,在构建的时候就自动输出 sw.js 并把相关内容注入到 html 文件中,该插件正准备开源哈~ 5. 未来 未来对于 PWA 还能做些什么?笔者以为有以下 2 个方面 5.1. 业务深耕 目前通用的能力已经基本挖掘完成,但是 SW 因为它独特的特性,它能做的事情太多了,但是具体要不要这么做也是因业务而异,而且这些内容可能会很复杂,所以我称为业务深耕。 SW 什么特性?请看下面 2 张图就可以理解了 [图片] [图片] 这种架构相信已经能够看出来了,没错,SW 有间件(层)的特性,那它能做的东西就太多了(虽然 SW 是用户端本地中间层) 简单举几个例子: server 负载控制:当发现 server 端高负载时,SW 可以丢弃请求,或者缓存请求,合并/延迟发送。 预请求:SW 能预缓存的资源是可以构建出来的资源,但是我们还有许多资源是不能在构建阶段知道的,比如图片,第三方资源等,SW 在返回资源请求(比如HTML、cgi 数据)之后,可以扫描资源里面的内容,如果发现包含了其他资源的 url,那说明页面很有可能待会就会请求这些资源,这时 SW 可以提前去请求这些资源;再比如翻页的数据 缓存自动更新:通过与 server 建立联系,当数据有变化时,server 直接把最新的数据 push 到 SW 进行缓存的更新 5.2. 关注 PWA 回到最开始,PWA 是一项令人兴奋的技术,但是浏览器兼容有限,因此期待并关注 PWA 技术的发展是很有必要的! 当然,能推动就更好了!比如推动我们的 X5 内核尽快支持新特性。 关注我们 IMWeb 团队隶属腾讯公司,是国内最专业的前端团队之一。 我们专注前端领域多年,负责过 QQ 资料、QQ 注册、QQ 群等亿级业务。目前聚焦于在线教育领域,精心打磨 腾讯课堂 及 企鹅辅导 两大产品。 社区官网: http://imweb.io/ 加入我们: https://hr.tencent.com/position_detail.php?id=45616 [图片] 扫码关注 IMWeb前端社区 公众号,获取最新前端好文 微博、掘金、Github、知乎可搜索 IMWeb 或 IMWeb团队 关注我们。
2019-03-12 - 你(可能)不知道的web api
简介 作为前端er,我们的工作与web是分不开的,随着HTML5的日益壮大,浏览器自带的webapi也随着增多。本篇文章主要选取了几个有趣且有用的webapi进行介绍,分别介绍其用法、用处以及浏览器支持度,同时我也分别为这几个api都做了一个简单的demo(真的很简单,样式等于没有~)这几个api分别是: page lifecycle onlineState 利用deviceOrientation制作一个随着手机旋转的正方体 battery status custom event 利用execCommand完成一个简单的富文本 page lifecycle(网页生命周期) 介绍 我们可以用document.visibitilityState来监听网页可见度,是否卸载,但是在手机和电脑上都会现这种情况,就是比如说页面打开过了很久没有打开,这时你看在浏览器的tab页中看着是可以看到内容的,但是点进去却需要加载。chrome68添加了 freeze和 resume事件,来完善的描述一个网页从加载到卸载,包括浏览器停止后台进程,释放资源各种生命阶段。从一个生命周期阶段到另外一个生命周期阶段会触发不同的事件,比如onfocus,onblur,onvisibilitychange,onfreeze等等,通过这些事件我们可以响应网页状态的转换。具体的教程推荐大家看看阮一峰大神的教程。 用法 [代码]window.addEventListener('blur',() => {}) window.addEventListener('visibilitychange',() => { // 通过这个方法来获取当前标签页在浏览器中的激活状态。 switch(document.visibilityState){ case'prerender': // 网页预渲染 但内容不可见 case'hidden': // 内容不可见 处于后台状态,最小化,或者锁屏状态 case'visible': // 内容可见 case'unloaded': // 文档被卸载 } }); [代码] 用处 大家可以看下这个demo [图片] 所以说,这个API的用处就是用来响应我们网页的状态,比如说我们的页面是在播放视频或者是一个网页的游戏,你可以通过这个API来去做出对应的响应,暂停视频,游戏暂停等等。 浏览器支持度 page visibilituState [图片] online state(网络状态) 这个API就很简单了,就是获取当前的网络状态,同时也有对应的事件去响应网络状态的变化。 用法 [代码]window.addEventListener('online',onlineHandler) window.addEventListener('offline',offlineHandler) [代码] 用处 比如说我们的网站是视频网站,正在播放的时候,网络中断了,我们可以通过这个API去响应,给用户相应的提示等等。 浏览器支持度 [图片] Vibration(震动) 让手机震动~~~ 嗯,就这么简单。 用法 [代码]// 可以传入一个大于0的数字,表示让手机震动相应的时间长度,单位为ms navigator.vibrate(100) // 也可以传入一个包含数字的数组,比如下面这样就是代表震动300ms,暂停200ms,震动100ms,暂停400ms,震动100ms navigator.vibrate([300,200,100,400,100]) // 也可以传入0或者一个全是0的数组,表示暂停震动 navigator.vibrate(0) [代码] 用处 用来给用户一个提示,比如说数据校验失败,当然震动不止这点作用,大家自己去扩展吧~~~ 浏览器支持度 [图片] device orientation(陀螺仪) 通过绑定事件来获取设备的物理朝向,可以获取到三个数值,分别是: alpha:设备沿着Z轴的旋转角度 [图片] beta:设备沿着X轴的旋转角度 [图片] gamma:设备沿着Y轴的旋转角度 [图片] 用法 [代码]window.addEventListener('deviceorientation',e => { console.log('Gamma:',e.gamma); console.log('Beta:',e.beta); console.log('Alpha:',e.Alpha); }) [代码] 用处 这种自然是web VR 中的使用场景会相对较多。这是我写的一个小demo [图片] 浏览器支持度 [图片] battery status 这个API就使用来获取当前的电池状态 用法 [代码]// 首先去判断当前浏览器是否支持此API if ('getBattery' in navigator) { // 通过这个方法来获取battery对象 navigator.getBattery().then(battery => { // battery 对象包括中含有四个属性 // charging 是否在充电 // level 剩余电量 // chargingTime 充满电所需事件 // dischargingTime 当前电量可使用时间 const { charging, level, chargingTime, dischargingTime } = battery; // 同时可以给当前battery对象添加事件 对应的分别时充电状态变化 和 电量变化 battery.onchargingchange = ev => { const { currentTarget } = ev; const { charging } = currentTarget; }; battery.onlevelchange = ev => { const { currentTarget } = ev; const { level } = ev; } }) } else { alert('当前浏览器不支持~~~') } [代码] 用处 用来温馨的提示用户当前电量~~~ 浏览器支持度 这个浏览器的支持度很低。。。。 [图片] execCommand 执行命令 当将HTML文档切换成设计模式时,就会暴露出 execcommand 方法,然后我们可以通过使用这个方法来执行一些命令,比如复制,剪切,修改选中文字粗体、斜体、背景色、颜色,缩进,插入图片等等等等。 用法 用法也很简单,这里简单介绍几个,详细的介绍大家可以去MDN上看看。 这个API接受三个参数,第一个是要执行的命令,第二个参数mdn上说是Boolean用来表示是否展现用户界面,但我也没测试出来有什么不同,第三个参数就是使用对应命令所需要传递的参数。 [代码]// 一般不会直接去操作我们本身的HTML文档,可以去插入一个iframe然后通过contentDocument来获取、操作iframe中的HTML文档。 let iframe = document.createElement('ifram'); let doc = iframe.contentDocument; // 首先要将HTML文档切换成设计模式 doc.designMode = 'on'; // 然后就可以使用execCommand 这个命令了; // 执行复制命令,复制选中区域 doc.execCommand('copy') // 剪切选中区域 doc.execCommand('cut') // 全选 doc.execCommand('selectAll') // 将选中文字变成粗体,同时接下来输入的文字也会成为粗体, doc.execCommand('bold') // 将选中文字变成斜体,同时接下来输入的文字也会成为斜体, doc.execCommand('italic') // 设置背景颜色,,比如设置背景色为红色,就传入 'red'即可 doc.execCommand('backColor',true,'red') [代码] 用处 我用这些命令简单的写了一个富文本的demo,大家可以看一下Demo,效果如下: [图片] 浏览器支持度 CustomEvent (自定义事件) 大家都知道各种事件是如何绑定的,但是有时候这些事件不够用呢,custom event就可以解决这样的问题。 用法 [代码]let dom = document.querySelector('#app'); // 绑定事件, 传递过来的值可以通过ev.detail 来获取 dom.addEventListener('log-in',(ev) => { const { detail } = ev; console.log(detail); // hello }) // 派发事件,需要传入两个参数,一个是事件类型,另外一个是一个对象,detail就是传递过去的值 dom.dispatchEvent(new CustomEvent('log-in',{ detail:'hello' })) [代码] 用处 绑定自定义事件,最近很火的框架Omi,其中的自定义事件就是基于customEvent实现的。 浏览器支持度 [图片] 最后 就先介绍到这些,web api越来越多,当然每个人不可能全都熟记于心,这篇文章也只是简单介绍一下,还有很多有意思而且很重要的API,比如:web components, service worker,genric sensor等等,不过这些都有很多人在钻研,同时文档相对较多。 相信你看完这些至少已经知道这些API的大概用法了,如果有兴趣了解用法的话,可以去看下我写的demo,也可以去看看MDN文档去深入研究一下。 参考 MDN文档 阮一峰大神的博客 web-api-you-dont-know 视频演讲 http://www.zhangyunling.com/725.html Omi WeElement源码
2019-03-01 - 微信小程序之登录态的探索
原文来自 https://segmentfault.com/a/1190000017042906 上一篇:开发微信小程序必须要知道的事 https://segmentfault.com/a/1190000017028505 登录,几乎什么项目都会用到,其重要性不言而喻,而小程序的登录却一直是为人头疼的一件事,这里我分享下我们在小程序登录上的探索通常的登录都是通过一个表单,这很正常,但如果在小程序里你也这么做那就有点不可思议了,微信的一键登录对用户体验有多好你难道不知道?不用是不是脑子有坑?最主要——你要利用微信的生态必须需要用微信的登录,以获取相关信息来和微信交互,OK,我们进入正题 用户在小程序、小游戏中需要点击组件后,才可以触发登录授权弹窗、授权自己的昵称头像等数据友情提示一下:[代码]wx.login[代码]并不需要点击组件,需要的是[代码]wx.getUserInfo[代码],但通常我们都会用到[代码]UnionID、encryptedData、iv等信息[代码]完成完整的登录流程,本文主要聚焦的也是这种场景所以之前直接通过调用API的方式就行不通了,那么问题来了——这个点击按钮要放到哪里? 放到首页,一进小程序就必须先登录。这样显然很粗暴,而且问题并没有解决,反而会把用户直接拒之门外,毕竟你不是用小程序做后台系统,什么场景都需要授权,先授权也是必须的 在需要授权的时候跳到登陆页面。这样就解决了上面遇到的不需要授权的时候也被强制授权,可是这样好吗? 体验上不好,操作被打断,尤其整个页面都不需要授权只有在一个地方需要授权的,例如:你正在读一篇文章,读罢深有感触,想评论一番,洋洋洒洒几十字写完正准备点击呢,他妈的跳转了!跳转了! 又一个漏斗,增加用户流失率。还TM要登录!很多用户心里一定这么想 那就直接放在需要登录的页面上(这不是漏斗吗?很多读者一定这么想。但想想看上面那个场景,点评论时只是需要点击下弹出的登录按钮,而且还假模假样的以微信的口吻提醒你需要登录,那你会不会登录?最起码你很愿意登录,而且来的很突然,我控几不住自己的手就点了!点了!) 可是这种方式有一个问题 怎么在需要的页面都能弹出登录按钮应该很多人都能想到:抽离出组件,那怎么保证在需要的页面都有这个组件呢?错杀一千也不能放过一个!把登录组件集成到共用的父组件,然后在每个页面都使用。我也建议这么做,因为这个共用的父组件其实又很多用处,例如iPhoneX适配等 等等,什么都准备好了,什么时候需要登录呢?XX,这个肯定是你自己控制的啦。嗯~好吧,我们来理一理 在哪里校验是否需要鉴权请求接口的时候,嗯~这是大家的共识 BOSS来了怎么鉴权 官方的这张图已经做了很详尽的说明,这里不做赘述 但是看到[代码]session_key[代码]了吗?看到官方同时说的了吗 所以问题又来了 怎么保证session_key的有效性诚如上图 要保证调用接口时后端[代码]session_key[代码]不失效,只能在每次调用前先使用[代码]wx.checkSession[代码]检查是否有效 实践中也发现[代码]wx.checkSeesion[代码]非常耗时,大约200ms,所以也不能每次接口调用前都使用[代码]wx.checkSession[代码]检查是否有效 同时要注意⚠️前端不能随便重新执行[代码]wx.login[代码],因为可能导致正在进行的其它后端任务session_key失效 天啦噜,怎么办?! 通过实践和偶然的发现——官方的示例代码 得知:在使用小程序期间session_key是不会失效的,so,你想到了什么? 在每个请求前去校验有效性 将校验有效性的结果存储起来 通过async/await和刚才存储起来的结果来保证不过多调用wx.checkSession 先问个问题:你准备用什么方式来存储校验的结果? 。。。 让思考先飞一会 。。。。。。 。。。。。。。。。 。。。。。。。。。。。。 storage吗?当然可以,不过不够完美,为什么?因为storage是永久的存储,而session_key的有效期却只是在使用小程序期间,所以你需要在小程序结束后手动重置该状态以重新校验其有效性,那是不是在app的onUnload里重置呢?不是!开发过小程序的应该都知道,那就是结束使用小程序的方式太多,不能保证每种方式都会触发onUnload,例如用户直接销毁了微信进程😳(其实你也可以在app的onShow里搞)那用什么呢?直接用内存啊,借助内存的自动管理来智能管理,所以最终代码应该是这样的 [代码]// doRequest.jslet wxSessionValid = null // 微信session_key的有效性// 带鉴权的请求封装async function doRequestWithCheckAuth() { ... if (typeof wxSessionValid !== 'boolean') { wxSessionValid = await checkWxSession() // 检查微信session是否有效 } if (!wxSessionValid) { await reLogin() // 重新登录 } wxSessionValid = true // 重新登陆后session_key一定有效 ... }[代码]这样是不是看起来比较完美了?嗯~ 不知道有没有同学着急问业务侧的session(自定义的登录态)怎么没讲?嗯,那现在讲吧 怎么校验完整的认证体系其实很简单,都不想把它作为一部分来讲,但既然讲了就必然有我想强调的 校验微信端的session_key略有麻烦,但不应该把它抛给服务端 服务端不能直接校验session_key的有效性而是通过调用接口发现错误了才知道失效了,这是被动的 服务端需要同时维护两个session 而放在前端我们只需要校验两个session的有效性即可,任何一个失效就重新登录,这是积极主动有效的操作,应该被提倡 贯通OK,基本上梳理的差不多了,就差弹登录按钮了,这个简单,调用刚才封装的组件的方法就行了嘛,bingo,可是,点完允许后呢?怎么继续用户的操作呢?怎么能让用户的体验不被打断呢?先回放下刚才reLogin的代码 [代码]async function reLogin() { // 确保有用户信息 await new Promise(resolve => { // ⚠️注意开头有await!!! wx.getSetting({ success: (res) => { // 如果用户没有授权或者没有必要的用户信息 if (!res.authSetting['scope.userInfo'] || !_.isRealTrue(wx.getStorageSync('userInfoRes').userInfo)) { navToLogin(resolve) // 去提示用户点击登录按钮,⚠️注意:并把当前的resolve带过去 } else { resolve() // 静默登录 } } }) }) return new Promise((resolve) => { wx.login({ success: res => { login(res.code).then((jwt) => { resolve(jwt) // resolve jwt }) // 通过code进行登录 }, fail(err) { wx.showToast({ title: err.errMsg, icon: 'none', duration: 2000 }) } }) }) }function navToLogin(resolve) { /* eslint-disable no-undef */ const pages = getCurrentPages() const page = pages[pages.length - 1] // 当前page page.openLoginModal(resolve) // 打开登录按钮弹框,并把当前的resolve带过去}[代码]上面的代码注释里有两个⚠️注意看到没?是的,通过回调的方式😂当用户同意授权了就继续余下的逻辑,如果被拒绝了,则安利他,再拒绝就终止操作,下次需要授权也会继续弹出授权 有不明白欢迎评论留言指出,我再做说明修改 完整源码以后会放出的,通过wepy搭建的一个框架 [代码]下一篇会讲api的封装[代码]
2018-11-16