前端面试小册链接:https://docs.chenqaq.com

五大主流浏览器以及四大内核

①IE(Internet explorer)内核:Trident

②火狐(Firefox)内核:Gecko

③谷歌(Chrome)内核:blink(旧版内核Webkit)

④苹果(Safari)内核:Webkit

⑤欧朋(Opera)内核:blink (前内核Presto)
常见其它浏览器及内核:

搜狗浏览器 Trident(兼容模式)+Webkit(高速模式);

QQ浏览器 Trident(兼容模式)+Webkit(高速模式);

UC浏览器 Trident(兼容模式)+Webkit(高速模式);

360浏览器 IE+Chrome双内核;

百度浏览器 IE内核

猎豹浏览器 IE+Chrome双内核;

世界之窗浏览器 IE内核

遨游浏览器 Trident(兼容模式)+Webkit(高速模式);

1、如何理解MVVM模式和MVC模式

MVC是经典的开发模式,model数据库,提供数据,view,视图,controller业务逻辑,路由分配,这种开发模式controller负责的太多,难以维护,m层和v层直接打交道,高耦合,所以为了解决这些问题,出现了MVVM模式,目前开发中用的模式,model后端提供数据,view视图,vm框架核心,model后端负责,view和VM前端负责,前后端分离,实现高内聚,低耦合,前后端同时开工,没有先后顺序,提高开发效率。

2、前端性能优化的方法有哪些?

①减少http请求,减少请求的体积,比如用雪碧图,gulp或者webpack压缩文件

②通过规范布局来减少DOM数量,减少DOM操作,比如事件委托。

③把对应的文件放在对应的位置,css放在head里,js放在body底部。

④图片懒加载,按需加载。

⑤对于ajax请求可以使用get请求,一来get请求存在缓存机制,二来get请求只发请求头速度快。

⑥利用cdn加速来减轻服务端的压力,把你的资源放在人家的服务器上,但是数据库还是在自己的服务器上。

⑦使用多线程和异步请求

3、H5新增特性有哪些?

①语义化标签

②canvas svg

③视频 音频

④本地存储

⑤地理定位

⑥离线存储:在离线状态上也可以访问之前的页面

⑦webwork 多线程执行JavaScript

⑧websocket 即时通信

4、简述对语义化标签的理解

标签语义化能让页面结构更加清晰,便于后期维护,便于浏览器和搜索引擎解析

5、简述ajax实现流程以及优缺点?

首先创建xmlHttpRequest实例对象,然后调用open方法指定请求的方式和请求路径,默认是异步请求,之后调用send方法发送请求,然后监听实例对象下的onreadystatechange方法,当状态满足的时候,拿到请求结果responseText,最后进行页面的渲染即可。

优缺点:

优点:ajax能实现网页局部更新,不用更新整个网页,减少带宽使用,提高加载速度

缺点:支持同源策略,存在跨域问题

6、Ajax中get和post请求方式的区别?

①post数据量大,get请求速度快

②两者的请求方式不同,get请求参数拼接在路径后面,post请求放在请求体中,在这之前设置请求头

③get请求存在缓存问题,可以拼接时间戳来解决数据得不到更新的问题

7、jsonp的实现流程

动态的创建script标签,后端返回回调函数的执行,并将请求的数据作为实参传入

//创建script标签

var script=document.createElement('script')

//设置回调函数

function getData(data){

//数据请求回来会被触发的函数

console.log(data);

}

//设置script的src属性,设置请求地址

script.src="http://localhost:3000?callback=getData";

//让script生效

document.body.appendChild(script);

8、ajax和jsonp的区别

①两者最大的区别就是ajax遵循同源策略,不能跨域,而jsonp可以

②ajax有get和post两种请求,jsonp只有一种get请求

③两者的实现方式也不一样,ajax是创建实例对象,jsonp是动态的创建script标签

9、json对象与字符串互转方法?

JSON.stringify()

JSON.parse()

10、Css清除浮动的方式

①给父元素设置overflow:hidden(不推荐,不能和position配合使用,因为超出的尺寸的会被隐藏,可能会影响页面元素布局)

②紧跟着设置浮动的元素的空标签设置clear:both;(不推荐,不利于页面优化)
③父级div定义 伪类:after 和 zoom(推荐使用)

//父级div定义伪类清除浮动代码
.clearfix:after{
    display:block;
    clear:both;
    content:"";
    visibility:hidden;
    height:0;
}
.clearfix{
    zoom:1;
}

④给父级元素单独定义高度height(不推荐,只适合高度固定的布局)
⑤父级div定义 overflow:auto(不推荐,会出现滚动条)
####11、Css动画和过渡的区别?

动画会自动开始执行,而且可以设置多个过渡状态,而过渡只有开始和结束两个状态

12、inline-block间距解决办法

①给父元素设置font-size:0;同时给子元素设置相应的大小

②设置浮动

13、简述事件委托的优点

①只需要绑定一次,减少DOM操作

②对于新添加的元素,同样可以触发事件

14、写出处理事件冒泡和阻止浏览器默认行为的兼容处理

//阻止事件冒泡

if(e.stopPropagation){

e.stopPropagation();

}else{

e.cancleBubble=true;

}

//阻止浏览器默认行为

if(e.preventDefault){

e.preventDefault();

}else{

e.returnValue=false;

}

15、获取页面滚动高度及设置页面滚动高度兼容写法

//获取页面滚动高度

varst=document.body.scrollTop||document.documentElement.scrollTop;

//设置页面滚动高度

window.scrollTo(x,y)    置顶的话0,0

16、伪类与伪元素的区别

伪类: 用来选择那些不能够被普通选择器选择的文档之外的元素,比如:hover

伪类用于当已有元素处于的某个状态时,为其添加对应的样式,这个状态是根据用户行为而动态变化的。

比如,当用户悬停在指定的元素时,我们可以通过:hover 来描述这个元素的状态。

虽然它和普通的 css 类相似,可以为已有的元素添加样式,但是它只有处于 dom树无法描述的状态下才能为元素添加样式,所以将其称为伪类

常见伪类::link,:visited,:hover,:active,:focus,:not(),:first-child,:last-child,:nth-child,:nth-last-child,:only-child,:target,:checked,:empty,:valid

伪元素:

伪元素用于创建一些不在文档树中的元素,并为其添加样式。比如说,我们可以通过:before 来在一个元素前增加一些文本,并为这些文本添加样式。虽然用户可以看到这些文本,但是这些文本实际上不在文档树中

伪元素前面是两个冒号,E::first-line 伪元素。会创造出不存在的新元素,由于 css 对单冒号的伪元素也支持,单双冒号都支持,但实际上现在css3 已经明确规定了伪类单冒号,伪元素双冒号的规则,用于区分它们

::before/:before在被选元素前插入内容::after/:after 在被元素后插入内容,其用法和特性与:before相似::placeholder 匹配占位符的文本,只有元素设置了placeholder 属性时,该伪元素才能生效

对于伪元素 :before和 :after 而言,属性 content 是必须设置的,它的值可以为字符串,也可以有其它形式,比如指向一张图片的 URL

总结

伪类和伪元素都是用来表示文档树以外的"元素"
伪类和伪元素分别用单冒号:和双冒号::来表示
伪类和伪元素的区别,最关键的点在于如果没有伪元素(或伪类),是否需要添加元素才能达到目的,如果是则是伪元素,反之则是伪类

17.js节流和防抖的实现及应用场景

在开发中,我们常常会去监听滚动事件或者用户输入框验证事件,如果事件处理没有频率限制,就会加重浏览器的负担,影响用户的体验感,
因此,我们可以采取防抖(debounce)和节流(throttle)来处理,减少调用事件的频率,达到较好的用户体验。

防抖

触发高频事件n秒内函数只会执行一次 ,如果n秒内函数再次被触发,那么就会重新计算时间
实现:每次触发事件时设置一个延迟调用方法,并且取消之前的延时调用方法
缺点:如果事件在规定的时间间隔内被不断的触发,则调用方***被不断的延迟

应用场景

1.典型的案例就是输入搜索:输入结束后n秒才进行搜索请求,n秒内又输入的内容,就重新计时。  
2.window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次。防止重复渲染

// 增加前缘触发功能
var debounce = (fn, wait-interval, immediate=false) => {
    let timer, startTimeStamp=0;
    let context, args;

    let run = (timerInterval)=>{
        timer= setTimeout(()=>{
            let now = (new Date()).getTime();
            let interval=now-startTimeStamp
            if(interval<timerInterval){ // the timer start time has been reset,so the interval is less than timerInterval
                console.log('debounce reset',timerInterval-interval);
                startTimeStamp=now;
                run(wait-interval);  // reset timer for left time 
            }else{
                if(!immediate){
                    fn.apply(context,args);
                }
                clearTimeout(timer);
                timer=null;
            }

        },timerInterval);
    }

    return function(){
        context=this;
        args=arguments;
        let now = (new Date()).getTime();
        startTimeStamp=now; // set timer start time

        if(!timer){
            console.log('debounce set',wait);
            if(immediate) {
                fn.apply(context,args);
            }
            run(wait);    // last timer alreay executed, set a new timer
        }

    }

}
节流

规定在n秒内,只能触发一次函数。如果在n秒内触发多次函数,只有一次生效
实现:当触发事件时,判断有没有正在执行的函数,直接return

应用场景
1.典型的案例就是鼠标不断点击触发,规定在n秒内多次点击只有一次生效。
2.监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断

/// 增加前缘
var throttling = (fn, wait, immediate) => {
    let timer, timeStamp=0;
    let context, args;

    let run = () => {
        timer=setTimeout(()=>{
            if(!immediate){
                fn.apply(context,args);
            }
            clearTimeout(timer);
            timer=null;
        },wait);
    }

    return function () {
        context=this;
        args=arguments;
        if(!timer){
            console.log("throttle, set");
            if(immediate){
                fn.apply(context,args);
            }
            run();
        }else{
            console.log("throttle, ignore");
        }
    }

}

在节流函数内部使用开始时间prev、当前时间now和剩余时间remain,当剩余时间小于等于0意味着执行处理函数,这样保证第一次就能立即执行函数并且每隔delay时间执行一次;

如果还没到时间,就会在remaining之后触发,保证最后一次触发事件也能执行函数,如果在remaining时间内又触发了滚动事件,那么会取消当前的计数器并计算出新的remaing时间。

通过时间戳和定时器的方法,我们实现了第一次立即执行,最后一次也执行,规定时间间隔执行的效果。

总结

防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。
如果事件触发是高频但是有停顿时,可以选择debounce; 在事件连续不断高频触发时,只能选择throttling,因为debounce可能会导致动作只被执行一次,界面出现跳跃。

18、数组扁平化(降维)

// 源数组:[1, [2, [3, [4, 5]]]]
// 降维后数组:[1, 2, 3, 4, 5]
方法1:数组遍历 + 递归
function flutten(arr) {
  let res = [];
  arr.map(item => {
      if(Array.isArray(item)) {
        res = res.concat(flutten(item))
      } else {
        res.push(item)
      }
    })
    return res;
}
方法2:join + split + map (适用于里面都是数字的)
function flutten(arr) {
  arr.join(',').split(',').map(item => {
      return Number(item)
    })
}
方法3:toSting + split + map (适用于里面都是数字的)
function flutten(arr) {
  arr.toString().split(',').map(item => {
      return Number(item)
    })
}
方法4:扩展运算符
function flutten(arr) {
  while(arr.some(item => Array.isArray(item))) {
    arr = [].concat(...arr)
  }
  return arr
}
方法5:reduce
function flutten(arr) {
  return arr.reduce((result,item) => {
      return result.concat(Array.isArray(item)?flutten(item):item)
    },[])
}
方法6:正则
let result = JSON.parse('[' + JSON.stringify(arr).replace(/\[|\]/g,'') + ']')
方法7:使用flat(es6新增)
let result = arr.flat(Infinity)

es6提供的新方法flat(depth) let a = [1,[2,3]]; a.flat(); // [1,2,3] a.flat(1); //[1,2,3] flat(depth) 方法中的参数depth,代表展开嵌套数组的深度,默认是1
所以我们可以添加参数1,或者直接调用flat()来对2维数组进行扁平化,如果我们可以提前知道数组的维度,对这个数组进行扁平化处理,参数depth的值就是数组的维度减一。
let a = [1,[2,3,[4,[5]]]]; a.flat(4-1); // [1,2,3,4,5] a是4维数组 其实还有一种更简单的办法,无需知道数组的维度,直接将目标数组变成1维数组。depth的值设置为Infinity。
let a = [1,[2,3,[4,[5]]]]; a.flat(Infinity); // [1,2,3,4,5] a是4维数组

19.http状态码

1开头:(被接受,需要继续处理。)

这一类型的状态码,代表请求已被接受,需要继续处理。这类响应是临时响应,只包含状态行和某些可选的响应头信息,并以空行结束。

100(客户端继续发送请求,这是临时响应):这个临时响应是用来通知客户端它的部分请求已经被服务器接收,且仍未被拒绝。客户端应当继续发送请求的剩余部分,或者如果请求已经完成,忽略这个响应。服务器必须在请求完成后向客户端发送一个最终响应。

101服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到HTTP的新版本协议。

102 (代表处理将被继续执行) 由WebDAV(RFC 2518)扩展的状态码,代表处理将被继续执行。
2开头 (请求成功)

这一类型的状态码,代表请求已成功被服务器接收、理解、并接受

200 (成功) 服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。

201 (已创建) 请求成功并且服务器创建了新的资源。

202 (已接受) 服务器已接受请求,但尚未处理。

203 (非授权信息) 服务器已成功处理了请求,但返回的信息可能来自另一来源。

204 (无内容) 服务器成功处理了请求,但没有返回任何内容。

205 (重置内容) 服务器成功处理了请求,但没有返回任何内容。

206 (部分内容) 服务器成功处理了部分 GET 请求。

207 (代表之后的消息体将是一个XML消息),并且可能依照之前子请求数量的不同,包含一系列独立的响应代码。
3开头 (请求被重定向)

这类状态码代表需要客户端采取进一步的操作才能完成请求。通常,这些状态码用来重定向,后续的请求地址(重定向目标)在本次响应的 Location 域中指明。

300 (多种选择) 针对请求,服务器可执行多种操作。 服务器可根据请求者 (user agent) 选择一项操作,或提供操作列表供请求者选择。

301 (永久移动) 请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。

302 (临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。

303 (查看其他位置) 请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码。

304 (未修改)自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。

305 (使用代理) 请求者只能使用代理访问请求的网页。 如果服务器返回此响应,还表示请求者应使用代理。

307 (临时重定向) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
4开头:(请求错误)

这类的状态码代表了客户端看起来可能发生了错误,妨碍了服务器的处理。除非响应的是一个 HEAD 请求,否则服务器就应该返回一个解释当前错误状况的实体,以及这是临时的还是永久性的状况。这些状态码适用于任何请求方法。浏览器应当向用户显示任何包含在此类错误响应中的实体内容。

400 (错误请求) 服务器不理解请求的语法。

401 (未授权) 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。

403 (禁止) 服务器拒绝请求。

404 (未找到) 服务器找不到请求的网页。

405 (方法禁用) 禁用请求中指定的方法。

406 (不接受) 无法使用请求的内容特性响应请求的网页。

407 (需要代理授权) 此状态代码与 401(未授权)类似,但指定请求者应当授权使用代理。

408 (请求超时) 服务器等候请求时发生超时。

409 (冲突) 服务器在完成请求时发生冲突。 服务器必须在响应中包含有关冲突的信息。

410 (已删除) 如果请求的资源已永久删除,服务器就会返回此响应。

411 (需要有效长度) 服务器不接受不含有效内容长度标头字段的请求。

412 (未满足前提条件) 服务器未满足请求者在请求中设置的其中一个前提条件。

413 (请求实体过大) 服务器无法处理请求,因为请求实体过大,超出服务器的处理能力。

414 (请求的 URI 过长) 请求的 URI(通常为网址)过长,服务器无法处理。

415 (不支持的媒体类型) 请求的格式不受请求页面的支持。

416 (请求范围不符合要求) 如果页面无法提供请求的范围,则服务器会返回此状态代码。

417 (未满足期望值) 服务器未满足"期望"请求标头字段的要求。
5开头:(服务器错误)

这类状态码代表了服务器在处理请求的过程中有错误或者异常状态发生,也有可能是服务器意识到以当前的软硬件资源无法完成对请求的处理。除非这是一个HEAD 请求,否则服务器应当包含一个解释当前错误状态以及这个状况是临时的还是永久的解释信息实体。浏览器应当向用户展示任何在当前响应中被包含的实体。

500 (服务器内部错误) 服务器遇到错误,无法完成请求。

501 (尚未实施) 服务器不具备完成请求的功能。 例如,服务器无法识别请求方法时可能会返回此代码。

502 (错误网关) 服务器作为网关或代理,从上游服务器收到无效响应。(比如:nginx里设置了反向代理,自己代理给自己,形成了死循环,造成大量的访问日志,每秒上万)

503 (服务不可用) 服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。

504 (网关超时) 服务器作为网关或代理,但是没有及时从上游服务器收到请求。

505 (HTTP 版本不受支持) 服务器不支持请求中所用的 HTTP 协议版本

20.微信小程序保存图片加logo(canvas合成图片)

<view class="container">
  <view class="list" wx:for="{{imglist}}" wx:key="index" >
    <view class="title">
      <image src="../../images/logo.jpg" class="logo"/>
      <text class="title_name">{{item.title_name}}</text>
      <view class="btn" data-index="{{index}}" bindtap="download">
      一键发圈</view>
    </view>
    <view class="list_content">
      <text class="context" wx:for="{{item.content}}" wx:key="index">{{item}}</text>
      <text class="copy" bindtap='copy' data-index="{{index}}">复制</text>
      <view class="contimg">
        <view wx:for="{{item.imgarr}}" wx:key="index">

          <view class="cont_images"style="background:url({{item.img_url}}) no-repeat;background-size: cover;background-position:center 100;">

          </view>
          <canvas class='canvas' style="width:{{canvasWidth}}px;height:{{canvasHeight}}px;top:{{canvasHeight*2}}px;" canvas-id="firstCanvas"></canvas>
        </view>
      </view>

    </view>
  </view>
</view>
/**index.wxss**/
.container{
  width: 100%;
  margin: 0 auto;
  background: rgb(221,221,221);
}
.list{
  width:90%;
  padding: 0 5%;
  background: #fff;
}
.title{
  width: 100%;
  height:60rpx;
  padding-top: 5%;
}
.logo{
  display: block;
  float: left;
  width: 60rpx;
  height:60rpx;
}
.title_name{
  display: block;
  float: left;
  margin-left: 50rpx;
  color: #000;
  height: 60rpx;
  line-height: 60rpx;
  font-size: 30rpx;
  text-align: center;
  font-weight: bolder;
}
.btn{
  float: right;
  width:180rpx;
  height:60rpx;
  line-height: 60rpx;
  background: #111;
  border-radius:50rpx;
  color: #fff;
  text-align: center;
}
.list_content{
  width: 80%;
  padding: 3% 10%;
  padding-bottom: 3%;
  margin-bottom: 3%
}
.context{
  display: block;
  font-size: 26rpx;
  color: #444
}
.copy{
  display: block;
  color: rgb(113,164,206);
  font-size: 26rpx;
}
.contimg{
  width: 100%;
  margin-top: 3%;
  overflow: hidden;
}
.contimg>view{
  float: left;
  width: 32%;
  height:32%;
  margin-bottom: 2%;
  text-align: center;

}

.contimg>view:nth-child(3n+2){
  margin: 0 2%;
  margin-bottom: 2%;
}
.cont_images{
  position: relative;
  width: 100%;
  height: 0;
  padding-bottom: 100%;
  background-size: cover;
  background-position: center;

}
.canvas{
    position: fixed;
    top: 200%;
    left: 0;
    width: 100%;
    height: 100%; 
}
//获取应用实例
const app = getApp()

Page({
  data: {

    imglist: [
    ]
  },
  //事件处理函数 保存图片
  download(e) {
    var that = this;
    let index = e.currentTarget.dataset.index;
    that.getsave(0, that.data.imglist[index].imgarr.length, index);
    that.textPaste(index)
  },
  getsave(i, length, index) {
    var that = this;
    that.setData({
      canvasWidth: that.data.imglist[index].imgarr[i].width,
      canvasHeight: that.data.imglist[index].imgarr[i].height
    })
    wx.showLoading({
      title: '下载中(' + (i + 1) + '/' + length + ')',
    })
    const downloadTask = wx.downloadFile({
      url: this.data.imglist[index].imgarr[i].img_url,
      success: (res) => {
        let ctx = wx.createCanvasContext('firstCanvas');
        console.log(res)

        // 解决背景图尺寸不一致,导致绘制上去的logo变小问题(根本原因:绘制图尺寸与画布css尺寸不一致)
        let scal = that.data.imglist[index].imgarr[i].height / that.data.imglist[index].imgarr[i].width;
        if (scal>1){
          scal = 1;
        }else{
          scal = that.data.imglist[index].imgarr[i].height / that.data.imglist[index].imgarr[i].width;
        }
        console.log(scal)
        ctx.drawImage(res.tempFilePath, 0, 0, that.data.imglist[index].imgarr[i].width, that.data.imglist[index].imgarr[i].height);
        ctx.drawImage('../../images/logos.png', that.data.imglist[index].imgarr[i].width - 150 * scal, that.data.imglist[index].imgarr[i].height - 60 * scal, 122 * scal, 29 * scal);
        ctx.draw(false, function() {
          wx.canvasToTempFilePath({
            canvasId: 'firstCanvas',
            success: (ressss) => {
              wx.saveImageToPhotosAlbum({
                filePath: ressss.tempFilePath,
                success: (res) => {
                  if (i + 1 == length) {
                    wx.showToast({
                      title: '保存成功',
                    });
                  }
                  wx.hideLoading()
                  if (++i < length) {
                    that.getsave(i, length, index);
                  }
                },
                fail: (err) => {
                  wx.showToast({
                    title: '保存图片失败',
                    icon: 'none',
                  })
                },
              })
            },
            fail: (e) => {
              console.log(e)
            }
          })
        })

      },
    })
    // 下载进度
    downloadTask.onProgressUpdate((res) => {
      if (res.progress > 0) {
        this.setData({
          schedule: true,
          percent: res.progress
        })
      }
      if (res.progress == 100) {
        this.setData({
          schedule: false
        })
      }
    })
  },

  // 复制
  textPaste(index) {
    let idx = index;
    wx.showToast({
      title: '复制成功',
    })

    var text = this.data.imglist[idx].content;
    var str = '';
    for (var i = 0; i < text.length; i++) {
      str += text[i] + ' \n'
    }
    wx.setClipboardData({
      data: str,
      success: function(res) {
        wx.getClipboardData({ //这个api是把拿到的数据放到电脑系统中的
          success: function(res) {
            console.log(res.data) // data
          }
        })
      }
    })
  },
  // 复制
  copy(e) {
    let idx = e.currentTarget.dataset.index;
    wx.showToast({
      title: '复制成功',
    })
    var text = this.data.imglist[idx].content;
    var str = '';
    for (var i = 0; i < text.length; i++) {
      str += text[i] + ' \n'
    }

    wx.setClipboardData({
      data: str,
      success: function(res) {
        wx.getClipboardData({ //这个api是把拿到的数据放到电脑系统中的
          success: function(res) {
            console.log(res.data) // data
          }
        })
      }
    })
  },

  onLoad: function (options) {
    var scene = decodeURIComponent(options.scene)
    console.log(scene)
    var that = this;
    wx.request({
      url: 'https://weixin.tphoto.cn/micro/phoneverthrid/getplug',
      header: {
        'Content-Type': 'application/json'
      },
      success: function(res) {
        console.log(res)
        that.setData({
          imglist: res.data
        })
      }
    })
    if (app.globalData.userInfo) {
      this.setData({
        userInfo: app.globalData.userInfo,
        hasUserInfo: true
      })
    } else if (this.data.canIUse) {
      // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
      // 所以此处加入 callback 以防止这种情况
      app.userInfoReadyCallback = res => {
        this.setData({
          userInfo: res.userInfo,
          hasUserInfo: true
        })
      }
    } else {
      // 在没有 open-type=getUserInfo 版本的兼容处理
      wx.getUserInfo({
        success: res => {
          app.globalData.userInfo = res.userInfo
          this.setData({
            userInfo: res.userInfo,
            hasUserInfo: true
          })
        }
      })
    }
  },
  getUserInfo: function(e) {
    console.log(e)
    app.globalData.userInfo = e.detail.userInfo
    this.setData({
      userInfo: e.detail.userInfo,
      hasUserInfo: true
    })
  }

})
代码核心部分
//事件处理函数 保存图片
  download(e) {
    var that = this;
    let index = e.currentTarget.dataset.index;
    that.getsave(0, that.data.imglist[index].imgarr.length, index);
    that.textPaste(index)
  },
  getsave(i, length, index) {
    var that = this;//回调函数里不能直接用this
    this.setData({
      canvasWidth: this.data.imglist[index].imgarr[i].width,
      canvasHeight: this.data.imglist[index].imgarr[i].height
    })
    wx.showLoading({
      title: '下载中(' + (i + 1) + '/' + length + ')',
    })
    const downloadTask = wx.downloadFile({
      url: this.data.imglist[index].imgarr[i].img_url,
      success: (res) => {
        let ctx = wx.createCanvasContext('firstCanvas');
        ctx.save();
        console.log(res)

        // 解决背景图尺寸不一致,导致绘制上去的logo变小问题(根本原因:绘制图尺寸与画布css尺寸不一致)
        let scal = that.data.canvasHeight / that.data.canvasWidth;
        if (scal>1){
          scal = 1
        }else{
          scal = that.data.canvasHeight / that.data.canvasWidth;
        }
        //先绘制背景图
        ctx.drawImage(res.tempFilePath, 0, 0, that.data.imglist[index].imgarr[i].width, that.data.imglist[index].imgarr[i].height);
        //绘制logo图片时,带域名的图片会涉及到跨域问题,所以最好是使用本地图片或者将图片转为base64的格式
        ctx.drawImage('../../images/logos.png', that.data.imglist[index].imgarr[i].width - 150 * scal, that.data.imglist[index].imgarr[i].height - 60 * scal, 122 * scal, 29 * scal);
        ctx.draw(false, function() {
          wx.canvasToTempFilePath({
            canvasId: 'firstCanvas',
            success: (ressss) => {
              console.log(ressss)
              wx.saveImageToPhotosAlbum({
                filePath: ressss.tempFilePath,
                success: (res) => {

                  if (i + 1 == length) {
                    wx.showToast({
                      title: '保存成功',
                    });
                  }
                  wx.hideLoading()
                  if (++i < length) {
                    that.getsave(i, length, index);
                  }
                },
                fail: (err) => {
                  wx.showToast({
                    title: '保存图片失败',
                    icon: 'none',
                  })
                },
              })
            },
            fail: (e) => {
              console.log(e)
            }
          })
        })

      },
    })
    // 下载进度
    downloadTask.onProgressUpdate((res) => {
      if (res.progress > 0) {
        this.setData({
          schedule: true,
          percent: res.progress
        })
      }
      if (res.progress == 100) {
        this.setData({
          schedule: false
        })
      }
    })