小程序 webpack Typescript nodejs GIT 底层原理

287 阅读1小时+

三、小程序

简单谈谈微信小程序

在结构和样式方面,小程序提供了一些常用的标签与控件,比如:

view,小程序主要的布局元素,类似于html标签的div,你也完全可以像控制div那样去控制view。

scroll-view,你要滚动内容的话,没必要用view去做overflow,scroll-view提供了更为强大的功能,通过参数的调整,你可以控制滚动方向,触发的事件等等

配置文件app.json平级的还有一个app.js文件,是小程序的脚本代码。我们可以在这个文件中监听并处理小程序的生命周期函数、声明全局变量,在每个page目录里的js做当前页面的业务操作。但是小程序的页面的脚本逻辑是在JsCore中运行,JsCore是一个没有窗口对象的环境,所以不能在脚本中使用window,也无法在脚本中操作组件,所以我们常用的zepto/jquery 等类库也是无法使用的。

另一个app.wxss文件,这个是全局的样式,所有的页面都会调用到,每个项目目录下面的wxss是局部样式文件,不会和其他目录产生污染,可以放心使用样式名。

他提供的WXSS(WeiXin Style Sheets)是一套样式语言,具有 CSS 大部分特性,可以看作一套简化版的css。 同时为了更适合开发微信小程序,还对 CSS 进行了扩充以及修改,直接帮我们把适配的一部分工作都做了,比如他的rpx(responsive pixel),可以根据屏幕宽度进行自适应,规定屏幕宽为750rpx。如在 iPhone6 上,屏幕宽度为375px,共有750个物理像素,则750rpx = 375px = 750物理像素,1rpx = 0.5px = 1物理像素。

在调用微信生态系统功能时,微信小程序提供了相应的api,比如你要修改一个头像,可以使用wx.chooseImage等

小程序的原生组件有哪些

以微信小程序为例,可以分成容器组件、基础组件、表单组件、媒体组件、开放能力组件等

小程序的安卓版和ios版是怎么开发出来

小程序开发基于html、css、javascript,与web开发一样具有跨平台特性,一次开发即可在安卓和iOS等平台访问,但与普通web开发不同,小程序运行环境并不是浏览器,而是依附于各自的软件App,如微信小程序必须在微信中访问,支付宝小程序必须在支付宝中访问等,小程序的开发流程也有所不同,需要经过申请小程序帐号、安装小程序开发者工具、配置项目、开发、调试、上线发布等过程方可完成

uni-app弹窗被覆盖怎么解决

如果弹窗被别的内容覆盖,且设置很大的z-index也无法解决,这种情况多半是被一些如mapvideotextareacanvas等原生组件遮盖,因为原生组件层级高于前端组件,我们可以使用cover-view组件解决

小程序生命周期

onReady 生命周期函数--监听页面初次渲染完成

onShow 生命周期函数--监听页面显示

onHide 生命周期函数--监听页面隐藏

onUnload 生命周期函数--监听页面卸载

onPullDownRefresh 页面相关事件处理函数--监听用户下拉动作

onReachBottom 页面上拉触底事件的处理函数

onShareAppMessage 用户点击右上角转发

onPageScroll 页面滚动触发事件的处理函数

onTabItemTap 当前是 tab 页时,点击 tab 时触发

小程序路由跳转

  1. 通过组件navigator跳转,设置url属性指定跳转的路径,设置open-type属性指定跳转的类型(可选),open-type的属性有 redirect, switchTab, navigateBack

    // redirect 对应 API 中的 wx.redirect 方法
    <navigator url="/page/redirect/redirect?title=redirect" open-type="redirect">在当前页打开</navigator>
    
    // navigator 组件默认的 open-type 为 navigate 
    <navigator url="/page/navigate/navigate?title=navigate">跳转到新页面</navigator>
    
    // switchTab 对应 API 中的 wx.switchTab 方法
    <navigator url="/page/index/index" open-type="switchTab">切换 Tab</navigator>
    
    // reLanch 对应 API 中的 wx.reLanch 方法
    <navigator url="/page/redirect/redirect?title=redirect" open-type="redirect">//关闭所有页面,打开到应用内的某个页面
    
    // navigateBack 对应 API 中的 wx.navigateBack 方法
    <navigator url="/page/index/index" open-type="navigateBack">关闭当前页面,返回上一级页面或多级页面</navigator>
    
  2. 通过api跳转,wx.navigateTo() , wx.navigateBack(), wx.redirectTo() , wx.switchTab(), wx.reLanch()

    wx.navigateTo({
      url: 'page/home/home?user_id=1'  // 页面 A
    })
    wx.navigateTo({
      url: 'page/detail/detail?product_id=2'  // 页面 B
    })
    // 跳转到页面 A
    wx.navigateBack({
      delta: 2  //返回指定页面
    })
    
    
    // 关闭当前页面,跳转到应用内的某个页面。
    wx.redirectTo({
      url: 'page/home/home?user_id=111'
    })
    
    
    // 跳转到tabBar页面(在app.json中注册过的tabBar页面),同时关闭其他非tabBar页面。
    wx.switchTab({
      url: 'page/index/index'
    })
    
    
    // 关闭所有页面,打开到应用内的某个页面。
    wx.reLanch({
      url: 'page/home/home?user_id=111'
    })
    

小程序的兼容问题有哪些

遇到的如下:

  • 1,ios下的zIndex层级问题,主要发生在iphone7和iphoneX下 绝对定位必须有一个共同的父元素。
  • 2,左右边框不生效 当边框的宽度设置为奇数的时候,可能会不生效 解决方法:将宽度设置为偶数的时候,在ios下就可以解决
  • 3,还有尽量不要用margin-bottom ,当元素是在整个页面的最底部的时候,在ios下可能margin-bottom会失效,所以建议,都使用padding-bottom

new Date跨平台兼容性问题

在Andriod使用new Date(“2018-05-30 00:00:00”)木有问题,但是在ios下面识别不出来。

因为IOS下面不能识别这种格式,需要用2018/05/30 00:00:00格式。可以使用正则表达式对做字符串替换,将短横替换为斜杠。var iosDate= date.replace(/-/g, '/');。

wx.getUserInfo()接口更改问题

微信小程序最近被吐槽最多的一个更改,就是用户使用wx.getUserInfo(开发和体验版)时不会弹出授权,正式版不受影响。现在授权方式是需要引导用户点击一个授权按钮,然后再弹出授权。

小程序框架都掌握哪一些,uniapp都会哪一些,平时开发遇到的困难

  • Taro
  • uni-app
  • WeUI
  • mpvue
  • iView Weapp

开发uni-app遇到的坑

上传图片

小程序时必须要写header:{“Content-Type”: “multipart/form-data”}, h5是必须省略

uni-app h5 端的ios图片不能加载问题

uni-app h5端 ios只能加载https的图片

uni-app 使用deep 穿透微信小程序生效 h5无作用

需要在methods同级下加一个 : options: { styleIsolation: ‘shared’ },

uni-app post请求如何传递数组 参数

在开发中我们接口上传图片是post请求 无法传递一个数组 解决方法如下

我们可以把数据转换成字符串 然后拼接到请求地址后后面 拼接字符串格式:image[]=arr[0]&image[]=arr[1]

imgURlClick(imgArr){
	return '?images[]='+imgArr.join('&images[]=')
}

小程序怎么获取手机号

  1. 准备一个button组件, 将 button 组件 open-type 的值设置为 getPhoneNumber,当用户点击并同意之后,可以通过 bindgetphonenumber 事件回调获取到动态令牌code;

    <button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber"></button>
    
    Page({
      getPhoneNumber (e) {
        console.log(e.detail.code)
      }
    })
    
  2. 接着把code传到开发者后台,并在开发者后台调用微信后台提供的 phonenumber.getPhoneNumber 接口,消费code来换取用户手机号。每个code有效期为5分钟,且只能消费一次。

    getPhoneNumber: function (e) {
        var that = this;
        console.log(e.detail.errMsg == "getPhoneNumber:ok");
        if (e.detail.errMsg == "getPhoneNumber:ok") {
          wx.request({
            url: 'http://localhost/index/users/decodePhone',
            data: {
              encryptedData: e.detail.encryptedData,
              iv: e.detail.iv,
              sessionKey: that.data.session_key,
              uid: "",
            },
            method: "post",
            success: function (res) {
              console.log(res);
            }
          })
        }
     }
    

    注:getPhoneNumber 返回的 codewx.login 返回的 code 作用是不一样的,不能混用.

    注:从基础库 2.21.2 开始,对获取手机号的接口进行了安全升级, 需要用户主动触发才能发起获取手机号接口,所以该功能不由 API 来调用,需用 button 组件的点击来触发。另外,新版本接口不再需要提前调用wx.login进行登录.

小程序的登录流程

登陆流程:

登陆流程

  • 首次登录

    • 调用小程序api接口 wx.login() 获取 临时登录凭证code ,这个code是有过期时间的.

    • 将这个code回传到开发者服务器(就是请求开发者服务器的登录接口,通过凭证进而换取用户登录态信息,包括用户的唯一标识(openid)及本次登录的会话密钥(session_key)等)

    • 拿到开发者服务器传回来的会话密钥(session_key)之后,前端需要保存起来.

      wx.setStorageSync('sessionKey', 'value')
      
  • 再次登录的时候,就要判断存储的session_key是否过期了

    • 获取缓存中的session_key,wx.getStorageSync('sessionKey')
    • 如果缓存中存在session_key,那么调用小程序api接口wx.checkSession()来判断登录态是否过期,回调成功说明当前 session_key 未过期,回调失败说明 session_key 已过期。登录态过期后前端需要再调用 wx.login()获取新的用户的code,然后再向开发者服务器发起登录请求.
    • 一般在项目开发,开发者服务器也会对用户的登录态做过期限制,所以这时在判断完微信服务器中登录态如果没有过期之后还要判断开发者服务器的登录态是否过期。(请求开发者服务器给定的接口进行请求判断就好)

    参考官网: 小程序登陆流程 登陆态过期检测

小程序如果版本更新了怎么通知用户

当小程序发布新的版本后,用户如果之前访问过该小程序,通过已打开的小程序进入(未手动删除),则会弹出提示,提醒用户更新新的版本。用户点击确定就可以自动重启更新,点击取消则关闭弹窗,不再更新.

  • 核心步骤:

    • 打开小程序, 检查小程序是否有新版本发布

      updateManager.onCheckForUpdate(function (res) {})
      
    • 小程序有新版本,则静默下载新版本,做好更新准备

      updateManager.onUpdateReady(function () {})
      
    • 新的版本已经下载好,调用 applyUpdate 应用新版本并重启小程序

      updateManager.applyUpdate()
      
  • 更新版本的模拟测试

    微信开发者工具上可以通过「编译模式」下的「下次编译模拟更新」开关来调试.

    点击编译模式设置下拉列表,然后点击“添加编译模式”,在自定义编译条件弹窗界面,点击下次编译时模拟更新,然后点击确定,重新编译就可以了.

    注: 需要注意的是,这种方式模拟更新一次之后就失效了,后边再测试仍需要对这种编译模式进行重新设置才可以.

  • 核心代码如下:

App({
  onLaunch: function(options) {
    this.autoUpdate()
  },
  autoUpdate:function(){
    var self=this
    // 获取小程序更新机制兼容
    if (wx.canIUse('getUpdateManager')) {
      const updateManager = wx.getUpdateManager()
      //1. 检查小程序是否有新版本发布
      updateManager.onCheckForUpdate(function (res) {
        // 请求完新版本信息的回调
        if (res.hasUpdate) {
          //2. 小程序有新版本,则静默下载新版本,做好更新准备
          updateManager.onUpdateReady(function () {
            wx.showModal({
              title: '更新提示',
              content: '新版本已经准备好,是否重启应用?',
              success: function (res) {
                if (res.confirm) {
                  //3. 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
                  updateManager.applyUpdate()
                } else if (res.cancel) {
                  //不应用
                }
              }
            })
          })
          updateManager.onUpdateFailed(function () {
            // 新的版本下载失败
            wx.showModal({
              title: '已经有新版本了哟~',
              content: '新版本已经上线啦~,请您删除当前小程序,重新搜索打开哟~',
            })
          })
        }
      })
    } else {
      // 如果希望用户在最新版本的客户端上体验您的小程序,可以这样子提示
      wx.showModal({
        title: '提示',
        content: '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。'
      })
    }
  }
})

小程序嵌入H5页面怎么做

  • 解决方式 :web-view

webview 指向网页的链接。可打开关联的公众号的文章,其它网页需登录小程序管理后台配置业务域名。

  • 具体实现步骤:

    • 登陆小程序管理后台, 配置服务器域名( h5页面所在的域名 )

    • 在小程序里面嵌入h5

      • 在小程序里面定义一个你想要的H5入口
      <navigator url="/page/navigate/navigate" hover-class="navigator-hover">跳转到新页面</navigator>
      
      • 新建一个页面,放置 webview , src指向h5网页的链接.
      <web-view src="{{url}}" bindmessage="getMessage"></web-view> </block>
      

    注: 实际开发中在h5页面中有可能需要向小程序发送消息, 实现h5页面和小程序页面的通信

    需要使用postMessage向小程序发送消息, 在h5中postMessage 注意,key必须叫做data,否则取不到.

小程序的生命周期函数有哪些?分别有什么作用?

小程序的生命周期函数大体分为三类:

  • 小程序应用的生命周期

    属性说明
    onLaunch监听小程序初始化, 全局只触发一次
    onShow监听小程序启动或切前台。
    onHide监听小程序切后台。

    参考官网: 应用的生命周期

  • 小程序页面的生命周期

    属性说明
    onLoad监听页面加载, 获取其他页面传过来的参数, 发起网络请求
    onShow监听页面显示
    onReady监听页面初次渲染完成
    onHide监听页面隐藏
    onUnload监听页面卸载

    参考官网: 页面的生命周期

  • 小程序组件的生命周期

    定义段描述
    created在组件实例刚刚被创建时执行,注意此时不能调用 setData )
    attached在组件实例进入页面节点树时执行)
    ready在组件布局完成后执行)
    moved在组件实例被移动到节点树另一个位置时执行)
    detached在组件实例被从页面节点树移除时执行)

四、webpack

webpack了解吗,讲一讲原理,怎么压缩代码

  1. 需要读到入口文件里面的内容。
  2. 分析入口文件,递归的去读取模块所依赖的文件内容,生成AST语法树。
  3. 根据AST语法树,生成浏览器能够运行的代码

webpack怎么配置

主要配置5个核心文件

1. mode:通过选择 `development`, `production``none` 之中的一个,来设置 `mode` 参数,你可以启用 webpack 内置在相应环境下的优化。
   2. entry:**入口起点(entry point)** 指示 webpack 应该使用哪个模块,来作为构建其内部 [依赖图(dependency graph)](https://webpack.docschina.org/concepts/dependency-graph/) 的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
   3. output:**output** 属性告诉 webpack 在哪里输出它所创建的 *bundle*,以及如何命名这些文件。主要输出文件的默认值是 `./dist/main.js`,其他生成文件默认放置在 `./dist` 文件夹中。
   4. loader:webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。**loader** 让 webpack 能够去处理其他类型的文件,并将它们转换为有效 [模块](https://webpack.docschina.org/concepts/modules),以供应用程序使用,以及被添加到依赖图中。
   5. plugin:loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。

webpack怎么打包

初始化参数:解析webpack配置参数,合并shell传入和webpack.config.js文件配置的参数,形成最后的配置结果;

开始编译:上一步得到的参数初始化compiler对象,注册所有配置的插件,插件 监听webpack构建生命周期的事件节点,做出相应的反应,执行对象的run方法开始执行编译;

确定入口:从配置的entry入口,开始解析文件构建AST语法树,找出依赖,递归下去;

编译模块:递归中根据文件类型和loader配置,调用所有配置的loader对文件进行转换,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;

完成模块编译并输出:递归完事后,得到每个文件结果,包含每个模块以及他们之间的依赖关系,根据entry或分包配置生成代码块chunk;

输出完成:输出所有的chunk到文件系统;

vue打包内存过大,怎么使用webpack来进行优化

开放式题目

  • 打包优化的目的

    1、优化项目启动速度,和性能

    2、必要的清理数据

    3、性能优化的主要方向

    cdn加载 -压缩js 减少项目在首次加载的时长(首屏加载优化) 4、目前的解决方向

    cdn加载不比多说,就是改为引入外部js路径

    首屏加载优化方面主要其实就两点

    第一: 尽可能的减少首次加载的文件体积,和进行分布加载

    第二: 首屏加载最好的解决方案就是ssr(服务端渲染),还利于seo 但是一般情况下没太多人选择ssr,因为只要不需要seo,ssr更多的是增加了项目开销和技术难度的。

    1、路由懒加载 在 Webpack 中,我们可以使用动态 import语法来定义代码分块点 (split point): import(’./Fee.vue’) // 返回 Promise如果您使用的是 Babel,你将需要添加 syntax-dynamic-import 插件,才能使 Babel 可以正确地解析语法。 结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件。

    const Fee = () => import('./Fee.vue') 在路由配置中什么都不需要改变,只需要像往常一样使用 Foo:

    const router = new VueRouter({
      routes: [
        { path: '/fee', component: Fee }
      ]
    })
    

    2、服务器和webpack打包同时配置Gzip Gzip是GNU zip的缩写,顾名思义是一种压缩技术。它将浏览器请求的文件先在服务器端进行压缩,然后传递给浏览器,浏览器解压之后再进行页面的解析工作。在服务端开启Gzip支持后,我们前端需要提供资源压缩包,通过Compression-Webpack-Plugin插件build提供压缩

    需要后端配置,这里提供nginx方式:

    http:{ 
          gzip on; #开启或关闭gzip on off
          gzip_disable "msie6"; #不使用gzip IE6
          gzip_min_length 100k; #gzip压缩最小文件大小,超出进行压缩(自行调节)
          gzip_buffers 4 16k; #buffer 不用修改
          gzip_comp_level 8; #压缩级别:1-10,数字越大压缩的越好,时间也越长
          gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; #  压缩文件类型 
    }
    

    // 安装插件

  $ cnpm i --save-dev compression-webpack-plugin

// 在vue-config.js 中加入

  const CompressionWebpackPlugin = require('compression-webpack-plugin');
  const productionGzipExtensions = [
    "js",
    "css",
    "svg",
    "woff",
    "ttf",
    "json",
    "html"
  ];
  const isProduction = process.env.NODE_ENV === 'production';
  .....
  module.exports = {
  ....
   // 配置webpack
   configureWebpack: config => {
    if (isProduction) {
     // 开启gzip压缩
     config.plugins.push(new CompressionWebpackPlugin({
      algorithm: 'gzip',
      test: /.js$|.html$|.json$|.css/,
      threshold: 10240,
      minRatio: 0.8
     }))
    }
   }
  }

3、优化打包chunk-vendor.js文件体积过大 当我们运行项目并且打包的时候,会发现chunk-vendors.js这个文件非常大,那是因为webpack将所有的依赖全都压缩到了这个文件里面,这时我们可以将其拆分,将所有的依赖都打包成单独的js。

  // 在vue-config.js 中加入
  
  .....
  module.exports = {
  ....
   // 配置webpack
   configureWebpack: config => {
    if (isProduction) {
      // 开启分离js
      config.optimization = {
        runtimeChunk: 'single',
        splitChunks: {
          chunks: 'all',
          maxInitialRequests: Infinity,
          minSize: 20000,
          cacheGroups: {
            vendor: {
              test: /[\/]node_modules[\/]/,
              name (module) {
                // get the name. E.g. node_modules/packageName/not/this/part.js
                // or node_modules/packageName
                const packageName = module.context.match(/[\/]node_modules[\/](.*?)([\/]|$)/)[1]
                // npm package names are URL-safe, but some servers don't like @ symbols
                return `npm.${packageName.replace('@', '')}`
              }
            }
          }
        }
      };
    }
   }
  }

// 至此,你会发现原先的vender文件没有了,同时多了好几个依赖的js文件

4、启用CDN加速 用Gzip已把文件的大小减少了三分之二了,但这个还是得不到满足。那我们就把那些不太可能改动的代码或者库分离出来,继续减小单个chunk-vendors,然后通过CDN加载进行加速加载资源。

  // 修改vue.config.js 分离不常用代码库
  // 如果不配置webpack也可直接在index.html引入
  
  module.exports = {
   configureWebpack: config => {
    if (isProduction) {
     config.externals = {
      'vue': 'Vue',
      'vue-router': 'VueRouter',
      'moment': 'moment'
     }
    }
   }
  }

// 在public文件夹的index.html 加载

  <script src="https://cdn.bootcss.com/vue/2.5.17-beta.0/vue.runtime.min.js"></script>
  <script src="https://cdn.bootcss.com/vue-router/3.0.1/vue-router.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js"></script>

5、完整vue.config.js代码

  const path = require('path')
  
  // 在vue-config.js 中加入
  // 开启gzip压缩
  const CompressionWebpackPlugin = require('compression-webpack-plugin');
  // 判断开发环境
  const isProduction = process.env.NODE_ENV === 'production';
  
  const resolve = dir => {
    return path.join(__dirname, dir)
  }
  
  // 项目部署基础
  // 默认情况下,我们假设你的应用将被部署在域的根目录下,
  // 例如:https://www.my-app.com/
  // 默认:'/'
  // 如果您的应用程序部署在子路径中,则需要在这指定子路径
  // 例如:https://www.foobar.com/my-app/
  // 需要将它改为'/my-app/'
  // iview-admin线上演示打包路径: https://file.iviewui.com/admin-dist/
  const BASE_URL = process.env.NODE_ENV === 'production'
    ? '/'
    : '/'
  
  module.exports = {
    //webpack配置
    configureWebpack:config => {
      // 开启gzip压缩
      if (isProduction) {
        config.plugins.push(new CompressionWebpackPlugin({
          algorithm: 'gzip',
          test: /.js$|.html$|.json$|.css/,
          threshold: 10240,
          minRatio: 0.8
        }));
        // 开启分离js
        config.optimization = {
          runtimeChunk: 'single',
          splitChunks: {
            chunks: 'all',
            maxInitialRequests: Infinity,
            minSize: 20000,
            cacheGroups: {
              vendor: {
                test: /[\/]node_modules[\/]/,
                name (module) {
                  // get the name. E.g. node_modules/packageName/not/this/part.js
                  // or node_modules/packageName
                  const packageName = module.context.match(/[\/]node_modules[\/](.*?)([\/]|$)/)[1]
                  // npm package names are URL-safe, but some servers don't like @ symbols
                  return `npm.${packageName.replace('@', '')}`
                }
              }
            }
          }
        };
        // 取消webpack警告的性能提示
        config.performance = {
          hints:'warning',
              //入口起点的最大体积
              maxEntrypointSize: 50000000,
              //生成文件的最大体积
              maxAssetSize: 30000000,
              //只给出 js 文件的性能提示
              assetFilter: function(assetFilename) {
            return assetFilename.endsWith('.js');
          }
        }
      }
    },
    // Project deployment base
    // By default we assume your app will be deployed at the root of a domain,
    // e.g. https://www.my-app.com/
    // If your app is deployed at a sub-path, you will need to specify that
    // sub-path here. For example, if your app is deployed at
    // https://www.foobar.com/my-app/
    // then change this to '/my-app/'
    publicPath: BASE_URL,
    // tweak internal webpack configuration.
    // see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
    devServer: {
      host: 'localhost',
      port: 8080, // 端口号
      hotOnly: false,
      https: false, // https:{type:Boolean}
      open: true, //配置自动启动浏览器
      proxy:null // 配置跨域处理,只有一个代理
  
    },
    // 如果你不需要使用eslint,把lintOnSave设为false即可
    lintOnSave: true,
    css:{
      loaderOptions:{
        less:{
          javascriptEnabled:true
        }
      },
      extract: true,// 是否使用css分离插件 ExtractTextPlugin
      sourceMap: false,// 开启 CSS source maps
      modules: false// 启用 CSS modules for all css / pre-processor files.
    },
    chainWebpack: config => {
      config.resolve.alias
        .set('@', resolve('src')) // key,value自行定义,比如.set('@@', resolve('src/components'))
        .set('@c', resolve('src/components'))
    },
    // 打包时不生成.map文件
    productionSourceMap: false
    // 这里写你调用接口的基础路径,来解决跨域,如果设置了代理,那你本地开发环境的axios的baseUrl要写为 '' ,即空字符串
    // devServer: {
    //   proxy: 'localhost:3000'
    // }
  }

webpack打包用过什么插件

  • 文件处理上

1、HtmlWebpackPlugin

包名:html-webpack-plugin

该插件将为你生成一个 HTML 文件, 在 body 中使用 script 标签引入你所有 webpack 生成的 bundle。 官方传送门

2、CleanWebpackPlugin

包名:clean-webpack-plugin

用于在打包前清理上一次项目生成的 bundle文件。默认情况下,此插件将删除webpack的Output.Path目录中的所有文件,以及在每次成功重建后所有未使用的webpack资源(assets)。如果使用的webpack版本是 4 以上的,默认 清理 <PROJECT_DIR>/dist/下的文件。 官方传送门

3、MiniCssExtractPlugin

包名:mini-css-extract-plugin

将 css 成生文件,而非内联 。该插件的主要是为了抽离 css 样式,防止将样式打包在 js 中引起页面样式加载错乱的现象。支持按需加载 css 和 sourceMap 官方传送门

4、HotModuleReplacementPlugin

包名:HotModuleReplacementPlugin

webpack 自带。在对CSS / JS文件进行修改时,可以立即更新浏览器(部分刷新)。依赖于 webpack-dev-server 官方传送门

5、ImageminPlugin

包名:imagemin-webpack-plugin

批量压缩图片。 官方传送门

6、PurifyCSSPlugin

包名:purifycss-webpack

从CSS中删除未使用的选择器(删除多余的 css 代码)。extract-text-webpack-plugin 一起使用 官方传送门

7、OptimizeCSSAssetsPlugin

包名:optimize-css-assets-webpack-plugin

压缩css文件。 官方传送门

8、CssMinimizerPlugin

包名: css-minimizer-webpack-plugin

压缩css文件。用于 webpack 5官方传送门

9、UglifyJsPlugin

包名:uglifyjs-webpack-plugin

压缩js文件。 官方传送门

10、ProvidePlugin

包名:ProvidePlugin

webpack 自带。自动加载模块,而不必在任何地方importrequire它们。例如:new webpack.ProvidePlugin({$: 'jquery',React: 'react'}) 官方传送门

11、SplitChunksPlugin

包名:-

webapck配置中的optimization字段中配置。cacheGroups 是关键,将文件提取打包成公共模块,像 抽取 node_modules里的文件。 官方传送门

12、CompressionPlugin

包名:compression-webpack-plugin

启用 gzip 压缩。 官方传送门

13、CopyWebpackPlugin

包名: copy-webpack-plugin

将已存在的单个文件或整个目录复制到构建目录中。多用于 将静态文件 因在打包时 webpack 并不会帮我们拷贝到 dist 目录 拷贝到 dist 目录 官方传送门

14、DefinePlugin

包名:DefinePlugin

webpack 自带。设置全局变量。如:new webpack.DefinePlugin({'process.env.NODE_ENV': JSON.stringify('production')}) 官方传送门

  • 打包速率上

1、DllPlugin

包名:DllPlugin

webpack 自带dllplugindllreferenceplugin提供了拆分捆绑包的方法,这些方式可以大大提高构建时间性能。 官方传送门

2、DLLReferencePlugin

包名:DLLReferencePlugin

webpack 自带。它引用了仅需要预先构建的依赖项的 DLL-only-Bundle官方传送门

3、ParallelUglifyPlugin

包名:webpack-parallel-uglify-plugin

开启多个子进程,把对多个文件压缩的工作分别给多个子进程去完成。减少构建时间。 官方传送门

4、HappyPack

包名:happypack

让 webpack 把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。提升 构建 速度 官方传送门

说说gulp和webpack的区别

开放式题目

Gulp强调的是前端开发的工作流程。我们可以通过配置一系列的task,定义task处理的事务(例如文件压缩合并、雪碧图、启动server、版本控制等),然后定义执行顺序,来让Gulp执行这些task,从而构建项目的整个前端开发流程。通俗一点来说,“Gulp就像是一个产品的流水线,整个产品从无到有,都要受流水线的控制,在流水线上我们可以对产品进行管理。”

Webpack是一个前端模块化方案,更侧重模块打包。我们可以把开发中的所有资源(图片、js文件、css文件等)都看成模块,通过loader(加载器)和plugins(插件)对资源进行处理,打包成符合生产环境部署的前端资源。 Webpack就是需要通过其配置文件(Webpack.config.js)中 entry 配置的一个入口文件(JS文件),然后在解析过程中,发现其他的模块,如scss等文件,再调用配置的loader或者插件对相关文件进行解析处理。

虽然Gulp 和 Webpack都是前端自动化构建工具,但看2者的定位就知道不是对等的。Gulp严格上讲,模块化不是他强调的东西,旨在规范前端开发流程。Webpack更明显的强调模块化开发,而那些文件压缩合并、预处理等功能,不过是他附带的功能。

五、Typescript

了解过TS吗?

ts是一种基于静态类型检查的强类型语言那当然js就是一种弱类型语言 由于我们在浏览器中不能编译ts语言所以我们需要安装编译器 安装下载 使用npm install -g typescript进行下载 使用tsc进行检测是否安装成功 在文件中间一个js文件,然后在文件中见一个ts文件,但是直接去使用的时候会报错,需要在终端中使用tsc ./js/hello.ts,这样之后可以在当前的就是文件中自动编译一个同名js文件。

let num:number=20
console.log(num)
console.log("str")

ts支持的数据类型

数组

let arr:number[]=[1,2,3,4,5]
//将let定义为一个数组,每一项都是number
let arr:number[]=[1,2,3,4,5,"str"]   //报错不能将类型string分配给类型number
let arr1:Array<number|string>=[1,2,3,4,5,"str"]//这样写就不会报错
//通过给范型添加多类型,让数组支持多种数据格式

元组Tuple 规定元素类型和规定元素数量和顺序的数组 特点:不要越界访问 定义的是什么类型写的就是什么类型,可以使用数组的下标取值,但是如果使用数组的push方法的话,虽然输出的数组中有,但是取值的话会报错可以打印出来但不建议这样写,这就说了元组的一个越界问题

let tu:[number,string]
tu=[1,"str"]

枚举 1.有限的可列举出来的命名和索引对应的类型 2枚举类型的优势:语义化可维护性 3原理:反向映射,互相指向

//定义了一个枚举
enum user{
    admin,
    guest,
    develoment,
    pm
}
console.log(user)
//使用user类型来定义枚举变量any

代表任意类型:

let t:any=10
t="str"
t=true

接口 跟另一个事物之间的一个媒介

interface userInfo{
    name:string;
    age:number;
     address?:string//问号代表该属性可添加可不添加
}
function getUserInfo(u:userInfo){
     console.log(u.name)   //张三
}
let user1 = {name:"张三",age:24,address:"北京"}
getUserInfo(user1)

使用ts写一个对象属性约束

使用{useName:string, password:number}约束传入的userData参数(并将password置为可选参数)

class User{
    useName:string;
    password?:number|undefined;
    //使用{useName:string, password?:number|undefined}约束传入的userData参数
    constructor(userData:{useName:string, password?:number|undefined}){
        this.useName=userData.useName;
        if(userData.password)
        this.password=userData.password;
    }
}

let u1=new User( {useName:"小猪猪", password:12233} );
let u2=new User( {useName:"大猪猪"} )

console.log(u1); 
console.log(u2);

说一下typescript中的泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

通俗理解:泛型就是解决类 接口 方法的复用性、以及对不特定数据类型的支持

function getData(value:string):string{
  return value //只能返回string类型的数据
}

//想要同时返回string和number类型
//1.通常: 代码冗余
function getData1(value:string):string{
  return value
}
function getData2(value:number):number{
  return value
}
//2.用any(可以解决,但是放弃了类型检查)
function getData(value:any):any{
  return value //什么类型都可以
}
//传入什么返回什么。比如传入number类型,必须返回number类型。传入string必须返回string类型。用any就可以不一致。

//泛型,可以支持不特定的数据类型
//要求:传入参数和返回的参数一致
//这里的T指泛型,也可以用任意字母取代,但是前后要一致
function getData<T>(value:T):T{
  return value
}
function getData<T>(value:T):T{
  return 'xxxx' //错误写法。不能将任何任性分配给T
}
//调用
getData<number>(123); //123 
getData<number>('xxx'); //错误写法

//可以调用的时候传一个泛型,返回的时候返回其他的类型(用的不多)
function getData<T>(value:T):any{
  return 'xxx'
}
//调用 
getData<number>(123); //xxx
getData<string>('xxx'); //xxx
//定义泛型是什么类型,就要传入什么类型的参数

如何在TS中对函数的返回值进行类型约束

ts中函数参数的类型定义

函数的参数可能是一个,也可能是多个,有可能是一个变量,一个对象,一个函数,一个数组等等。

1.函数的参数为单个或多个单一变量的类型定义

function fntA(one, two, three) {
	// 参数 "two" 隐式具有 "any" 类型,但可以从用法中推断出更好的类型。
    return one + two + three
}
const aResult = fntA(1, '3', true)

修改后:

function fntA(one: number, two: string, three: boolean) {
    return one + two + three
}
const aResult1 = fntA(1, '3', true)
// 如果函数的参数为单个或者多个变量的时候,只需要为这些参数进行静态类型下的基础类型定义就行

2.函数的参数为数组的类型定义

function fntB(arr) {
	//参数 "arr" 隐式具有 "any" 类型,但可以从用法中推断出更好的类型。
    return arr[0]
}
const bResult = fntB([1, 3, 5])

修改后:

function fntB(arr: number[]) {
    return arr[0]
}
const bResult1 = fntB([1, 3, 5])
// 如果参数是数组时,只需要为这些变量进行对象类型下的数组类型定义

3.函数的参数为对象的类型定义

function fntC({ one, two }) {
    return one + two
}
const cResult = fntC({ one: 6, two: 10 })

修改后:

function fntC({ one, two }: { one: number, two: number }) {
    return one + two
}
const cResult1 = fntC({ one: 6, two: 10 })
// 如果参数是对象,只需要为这些变量进行对象类型下的对象类型定义

4.函数的参数为函数的类型定义

function fntD(callback) {
	//参数 "callback" 隐式具有 "any" 类型,但可以从用法中推断出更好的类型
    callback(true)
}
function callback(bl: boolean): boolean {
    console.log(bl)
    return bl
}
const dResult = fntD(callback)

修改后:

function fntD(callback: (bl: boolean) => boolean) {
    callback(true)
}
function callback(bl: boolean): boolean {
    console.log(bl)
    return bl
}
const dResult = fntD(callback)
// 如果参数是函数,只需要为参数进行对象类型下的函数类型定义即可

ts中函数返回值的类型定义

当函数有返回值时,根据返回值的类型在相应的函数位置进行静态类型定义即可

返回数字:

function getTotal2(one: number, two: number): number {
    return one + two;
}
const total2 = getTotal(1, 2);
// 返回值为数字类型

返回布尔值

function getTotal2(one: number, two: number): boolean {
    return Boolean(one + two);
}
const total2 = getTotal(1, 2);
// 返回值为布尔类型

返回字符串

function getTotal2(one: string, two: string): string{
    return Bone + two;
}
const total2 = getTotal('1', '2');
// 返回值为字符串

返回对象

function getObj(name: string, age: number): { name: string, age: number } {
    return {name,age}
}
getObj('小红',16)
// 返回值为对象

返回数组

function getArr(arr: number[]) :number[]{
    let newArr = [...arr]
    return newArr
}
getArr([1,2,3,4])
// 返回值为数组

函数返回值为underfinde,仅仅时为了在内部实现某个功能,我们就可以给他一个类型注解void,代表没有任何返回值,

function sayName() {
    console.log('hello,world')
}

修改后:

function sayName1(): void {
    console.log('无返回值')
}

当函数没有返回值时

// 因为总是抛出异常,所以 error 将不会有返回值
// never 类型表示永远不会有值的一种类型
function error(message: string): never {
    throw new Error(message);
}

ts和js相比有什么区别

  • 在ts中完全可以使用js
  • ts是js的超集,并且ts比js多了一个类型检查功能

RX了解吗?

Rx 中强大的地方在于两处

  • 管道思想,通过管道,我们订阅了数据的来源,并在数据源更新时响应 。
  • 强大的操作符,通过操作符对流和流中的数据转换,拼接,以形成我们想要的数据模型 。

在 Rx 中,我们先预装好管道,通过管道流通数据 。这些管道的来源多种, create ,from, fromEvent, of .., 通过操作符将管道 拼接,合并,映射...形成最终的数据模型 。 对于管道来说,有两点非常重要

. 管道是懒执行的,只有订阅器 observer subscribe了 数据管道,这个管道才会有数据流通 。 . 整个节点组成一个完整的管道,订阅了后面的管道节点,也会同时订阅之前的管道节点 ,每个节点接受之前的值,并发出新值

Rx 如此高效和强大,得益于其强大的操作符 。 主要包含下面几类

创建操作符: create, range, of, from, fromEvent, fromPromise, empty .. 组合 contact ,merge, startWith, zip .. 时间 delay , throttle, dobounceTime, interval .. 过滤: filter, first, last, skip, distinct, take .. 转换: buffer,map, mapTo, mergeMap, switch, switchMap, reduce, scan .

六、nodejs

说说对nodejs的了解

  • Node.js 是一个开源和跨平台的 JavaScript 运行时环境。 它几乎是任何类型项目的流行工具!
  • Node.js 在浏览器之外运行 V8 JavaScript 引擎(Google Chrome 的内核)。 这使得 Node.js 的性能非常好。
  • Node.js 应用程序在单个进程中运行,无需为每个请求创建新的线程。 Node.js 在其标准库中提供了一组异步的 I/O 原语,以防止 JavaScript 代码阻塞,通常,Node.js 中的库是使用非阻塞范式编写的,使得阻塞行为成为异常而不是常态。
  • 当 Node.js 执行 I/O 操作时(比如从网络读取、访问数据库或文件系统),Node.js 将在响应返回时恢复操作(而不是阻塞线程和浪费 CPU 周期等待)。 这允许 Node.js 使用单个服务器处理数千个并发连接,而​​不会引入管理线程并发(这可能是错误的重要来源)的负担。
  • Node.js 具有独特的优势,因为数百万为浏览器编写 JavaScript 的前端开发者现在无需学习完全不同的语言,就可以编写除客户端代码之外的服务器端代码。
  • 在 Node.js 中,可以毫无问题地使用新的 ECMAScript 标准,因为你不必等待所有用户更新他们的浏览器,你负责通过更改 Node.js 版本来决定使用哪个 ECMAScript 版本,你还可以通过运行带有标志的 Node.js 来启用特定的实验性功能。

nodejs如何写接口,返回参数如何处理,有多少种方法

  • nodejs的原生模块http可用于编写接口,但一般推荐使用express或koa2来设计restful api。
  • 下面以 express 为例说明如何编写接口。
var express = require('express')
var app = express()
app.get('/', function (req, res) {
  res.send('hello world')
})
  • express响应客户端的方法有:end()/json()/render()/send()/jsonp()/sendFile()等。

websocket和http的区别

  • WebSocket 是 HTML5 中的协议,支持持久连续,http 协议不支持持久性连接。
  • Http1.0 和 HTTP1.1 都不支持持久性的链接,HTTP1.1 中的 keep-alive,将多个 http 请求合并为 1个。

常见的 HTTP Method 有哪些? GET/POST 区别?

1、常见的HTTP方法

  • GET:获取资源
  • POST:传输资源
  • PUT:更新资源
  • DELETE:删除资源
  • HEAD:获得报文首部

2、GET/POST的区别

  • GET在浏览器回退时是无害的,而POST会再次提交请求
  • GET请求会被浏览器主动缓存,而POST不会,除非手动设置
  • GET请求参数会被完整保留在浏览器的历史记录里,而POST中的参数不会被保留
  • GET请求在URL中传送的参数是有长度限制的,而POST没有限制
  • GET参数通过URL传递,POST放在Request body中
  • GET请求只能进行 url 编码,而POST支持多种编码方式
  • GET产生的URL地址可以被收藏,而POST不可以
  • 对参数的数据类型,GET只接受ASCII字符,而POST没有限制
  • GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息

说一说Tcp三次握手,四次挥手

1、三次握手

  • seq序号,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记
  • ack确认序号,只有ACK标志位为1时,确认序号字段才有效,ack=seq+1

2、四次挥手

关闭连接时,当服务器端收到FIN报文时,很可能并不会立即关闭链接,所以只能先回复一个ACK报文,告诉客户端:”你发的FIN报文我收到了”,只有等到服务器端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送,故需要四步握手。

阐述一下http1.0与http2.0的区别,及http和https区别

1、HTTP1.0和HTTP1.1的一些区别

  • 缓存处理,HTTP1.0中主要使用Last-Modified,Expires 来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略:ETag,Cache-Control… 带宽优化及网络连接的使用,HTTP1.1支持断点续传,即返回码是206(Partial Content)
  • 错误通知的管理,在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除…
  • Host头处理,在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)
  • 长连接,HTTP1.1中默认开启Connection: keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点

2、HTTP2.0和HTTP1.X相比的新特性

  • 新的二进制格式(Binary Format),HTTP1.x的解析是基于文本,基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合,基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮
  • header压缩,HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小
  • 服务端推送(server push),例如我的网页有一个sytle.css的请求,在客户端收到sytle.css数据的同时,服务端会将sytle.js的文件推送给客户端,当客户端再次尝试获取sytle.js时就可以直接从缓存中获取到,不用再发请求了

node.js如何导出页面数据形成报表

生成报表并下载是作为web应用中的一个传统功能,在实际项目中有广范的应用,实现原理也很简单,以NodeJS导出数据的方法为例,就是拿到页面数据对应id,然后根据id查询数据库,拿到所需数据后格式化为特点格式的数据,最后导出文件。

在nodejs中,也提供了很多的第三方库来实现这一功能,以node-xlsx导出excel文件为例,实现步骤如下:

  1. 下载node-xlsx

        npm install node-xlsx
    
  2. 编写接口

    以下代码使用express编写,调用接口实现下载功能

        const fs = require('fs');
        const path = require('path');
        const xlsx = require('node-xlsx');
        const express = require('express');
        const router = express.Router();
        const mongo = require('../db');
    
        router.post('/export', async (req, res) => {
            const { ids } = req.body;
            const query = {
                _id: { $in: ids }
            }
            // 查询数据库获取数据
            let result = await mongo.find(colName, query);
    
            
            // 设置表头
            const keys = Object.keys(Object.assign({}, ...result));
            rows[0] = keys;
    
            // 设置表格数据
            const rows = []
            result.map(item => {
                const values = []
                keys.forEach((key, idx) => {
                    if (item[key] === undefined) {
                        values[idx] = null;
                    } else {
                        values[idx] = item[key];
                    }
                })
                rows.push(values);
            });
    
            let data = xlsx.build([{ name: "商品列表", data: rows }]);
            const downloadPath = path.join(__dirname, '../../public/download');
            const filePath = `${downloadPath}/goodslist.xlsx`;
            fs.writeFileSync(filePath, data);
            res.download(filePath, `商品列表.xlsx`, (err) => {
                console.log('download err', err);
            });
        })
    

协商缓存和强缓存

浏览器缓存主要分为强强缓存(也称本地缓存)和协商缓存(也称弱缓存)。浏览器在第一次请求发生后,再次发送请求时:

  • 浏览器请求某一资源时,会先获取该资源缓存的header信息,然后根据header中的Cache-ControlExpires来判断是否过期。若没过期则直接从缓存中获取资源信息,包括缓存的header的信息,所以此次请求不会与服务器进行通信。这里判断是否过期,则是强缓存相关。后面会讲Cache-ControlExpires相关。
  • 如果显示已过期,浏览器会向服务器端发送请求,这个请求会携带第一次请求返回的有关缓存的header字段信息,比如客户端会通过If-None-Match头将先前服务器端发送过来的Etag发送给服务器,服务会对比这个客户端发过来的Etag是否与服务器的相同,若相同,就将If-None-Match的值设为false,返回状态304,客户端继续使用本地缓存,不解析服务器端发回来的数据,若不相同就将If-None-Match的值设为true,返回状态为200,客户端重新机械服务器端返回的数据;客户端还会通过If-Modified-Since头将先前服务器端发过来的最后修改时间戳发送给服务器,服务器端通过这个时间戳判断客户端的页面是否是最新的,如果不是最新的,则返回最新的内容,如果是最新的,则返回304,客户端继续使用本地缓存。

强缓存

强缓存是利用http头中的ExpiresCache-Control两个字段来控制的,用来表示资源的缓存时间。强缓存中,普通刷新会忽略它,但不会清除它,需要强制刷新。浏览器强制刷新,请求会带上Cache-Control:no-cachePragma:no-cache

Expires

Expires是http1.0的规范,它的值是一个绝对时间的GMT格式的时间字符串。如我现在这个网页的Expires值是:expires:Fri, 14 Apr 2017 10:47:02 GMT。这个时间代表这这个资源的失效时间,只要发送请求时间是在Expires之前,那么本地缓存始终有效,则在缓存中读取数据。所以这种方式有一个明显的缺点,由于失效的时间是一个绝对时间,所以当服务器与客户端时间偏差较大时,就会导致缓存混乱。如果同时出现Cache-Control:max-ageExpires,那么max-age优先级更高。如我主页的response headers部分如下:

cache-control:max-age=691200
expires:Fri, 14 Apr 2017 10:47:02 GMT

Cache-Control

Cache-Control是在http1.1中出现的,主要是利用该字段的max-age值来进行判断,它是一个相对时间,例如Cache-Control:max-age=3600,代表着资源的有效期是3600秒。cache-control除了该字段外,还有下面几个比较常用的设置值:

  • no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。
  • no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。
  • public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。
  • private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。 Cache-Control与Expires可以在服务端配置同时启用,同时启用的时候Cache-Control优先级高。

协商缓存

协商缓存就是由服务器来确定缓存资源是否可用,所以客户端与服务器端要通过某种标识来进行通信,从而让服务器判断请求资源是否可以缓存访问。

普通刷新会启用弱缓存,忽略强缓存。只有在地址栏或收藏夹输入网址、通过链接引用资源等情况下,浏览器才会启用强缓存,这也是为什么有时候我们更新一张图片、一个js文件,页面内容依然是旧的,但是直接浏览器访问那个图片或文件,看到的内容却是新的。

这个主要涉及到两组header字段:EtagIf-None-MatchLast-ModifiedIf-Modified-Since。上面以及说得很清楚这两组怎么使用啦~复习一下:

EtagIf-None-Match

Etag/If-None-Match返回的是一个校验码。ETag可以保证每一个资源是唯一的,资源变化都会导致ETag变化。服务器根据浏览器上送的If-None-Match值来判断是否命中缓存。

与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。

Last-Modify/If-Modify-Since

浏览器第一次请求一个资源的时候,服务器返回的header中会加上Last-Modify,Last-modify是一个时间标识该资源的最后修改时间,例如Last-Modify: Thu,31 Dec 2037 23:59:59 GMT。

当浏览器再次请求该资源时,request的请求头中会包含If-Modify-Since,该值为缓存之前返回的Last-Modify。服务器收到If-Modify-Since后,根据资源的最后修改时间判断是否命中缓存。

如果命中缓存,则返回304,并且不会返回资源内容,并且不会返回Last-Modify。

为什么要有Etag

你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:

  • 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
  • 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
  • 某些服务器不能精确的得到文件的最后修改时间。

Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。

http常用状态码有哪一些,说一说他们的作用

常用的状态码: 200表示成功,301表示重定向,404表示资源未找到,500表示服务器内部错误

状态码分类:

分类分类描述
1**信息,服务器收到请求,需要请求者继续执行操作
2**成功,操作被成功接收并处理
3**重定向,需要进一步的操作以完成请求
4**客户端错误,请求包含语法错误或无法完成请求
5**服务器错误,服务器在处理请求的过程中发生了错误

http状态码列表

状态码状态码英文名称中文描述
100Continue继续。客户端应继续其请求
101Switching Protocols切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到HTTP的新版本协议
200OK请求成功。一般用于GET与POST请求
201Created已创建。成功请求并创建了新的资源
202Accepted已接受。已经接受请求,但未处理完成
203Non-Authoritative Information非授权信息。请求成功。但返回的meta信息不在原始的服务器,而是一个副本
204No Content无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档
205Reset Content重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域
206Partial Content部分内容。服务器成功处理了部分GET请求
300Multiple Choices多种选择。请求的资源可包括多个位置,相应可返回一个资源特征与地址的列表用于用户终端(例如:浏览器)选择
301Moved Permanently永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替
302Found临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI
303See Other查看其它地址。与301类似。使用GET和POST请求查看
304Not Modified未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源
305Use Proxy使用代理。所请求的资源必须通过代理访问
306Unused已经被废弃的HTTP状态码
307Temporary Redirect临时重定向。与302类似。使用GET请求重定向
400Bad Request客户端请求的语法错误,服务器无法理解
401Unauthorized请求要求用户的身份认证
402Payment Required保留,将来使用
403Forbidden服务器理解请求客户端的请求,但是拒绝执行此请求
404Not Found服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置"您所请求的资源无法找到"的个性页面
405Method Not Allowed客户端请求中的方法被禁止
406Not Acceptable服务器无法根据客户端请求的内容特性完成请求
407Proxy Authentication Required请求要求代理的身份认证,与401类似,但请求者应当使用代理进行授权
408Request Time-out服务器等待客户端发送的请求时间过长,超时
409Conflict服务器完成客户端的 PUT 请求时可能返回此代码,服务器处理请求时发生了冲突
410Gone客户端请求的资源已经不存在。410不同于404,如果资源以前有现在被永久删除了可使用410代码,网站设计人员可通过301代码指定资源的新位置
411Length Required服务器无法处理客户端发送的不带Content-Length的请求信息
412Precondition Failed客户端请求信息的先决条件错误
413Request Entity Too Large由于请求的实体过大,服务器无法处理,因此拒绝请求。为防止客户端的连续请求,服务器可能会关闭连接。如果只是服务器暂时无法处理,则会包含一个Retry-After的响应信息
414Request-URI Too Large请求的URI过长(URI通常为网址),服务器无法处理
415Unsupported Media Type服务器无法处理请求附带的媒体格式
416Requested range not satisfiable客户端请求的范围无效
417Expectation Failed服务器无法满足Expect的请求头信息
500Internal Server Error服务器内部错误,无法完成请求
501Not Implemented服务器不支持请求的功能,无法完成请求
502Bad Gateway作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应
503Service Unavailable由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中
504Gateway Time-out充当网关或代理的服务器,未及时从远端服务器获取请求
505HTTP Version not supported服务器不支持请求的HTTP协议的版本,无法完成处理

网络攻击方案有哪些,自己有写过什么安全性方面的东西吗?

  1. iframe
  2. opener
  3. CSRF(跨站请求伪造)
  4. XSS(跨站脚本攻击)
  5. ClickJacking(点击劫持)
  6. HSTS(HTTP严格传输安全)
  7. CND劫持

很难通过技术手段完全避免 XSS,但我们可以总结以下原则减少漏洞的产生:

  • 利用模板引擎 开启模板引擎自带的 HTML 转义功能。例如: 在 ejs 中,尽量使用 <%= data %> 而不是 <%- data %>; 在 doT.js 中,尽量使用 {{! data } 而不是 {{= data }; 在 FreeMarker 中,确保引擎版本高于 2.3.24,并且选择正确的 freemarker.core.OutputFormat
  • 避免内联事件 尽量不要使用 onLoad="onload('{{data}}')"onClick="go('{{action}}')" 这种拼接内联事件的写法。在 JavaScript 中通过 .addEventlistener() 事件绑定会更安全。
  • 避免拼接 HTML 前端采用拼接 HTML 的方法比较危险,如果框架允许,使用 createElementsetAttribute 之类的方法实现。或者采用比较成熟的渲染框架,如 Vue/React 等。
  • 时刻保持警惕 在插入位置为 DOM 属性、链接等位置时,要打起精神,严加防范。
  • 增加攻击难度,降低攻击后果 通过 CSP、输入长度配置、接口安全措施等方法,增加攻击的难度,降低攻击的后果。
  • 主动检测和发现 可使用 XSS 攻击字符串和自动扫描工具寻找潜在的 XSS 漏洞。

静态资源部署到哪?

  • 存放静态资源文件到服务器

    但这种形式在性能上也有缺陷:

    • 受地理环境影响,离服务器越远资源加载越慢
    • 频繁请求资源对服务器压力较大
  • 存放静态资源文件到CDN

    为了进一步提升性能,可以把动态网页(index.html)和静态资源(js、css、image...)分开部署。静态资源被放置于 CDN 上.

    但是 CDN 也有缓存策略:新的静态资源发布后,需要一定的时间去覆盖各个边缘站点的旧资源。若某客户端获得了新的动态网页,但是附近的 CDN 节点尚未更新最近发布的静态资源,客户端即便放弃本地缓存,它加载的依旧是位于 CDN 上的“脏数据”。怎么办呢?干脆把文件名也给改了——让摘要信息成为文件名的一部分!具体实现可以仰仗 webpack,将 output.filename 设为 [name].[contenthash].js,输出文件和 html 模版都会帮你更改好.

    用摘要信息重命名后的资源文件,与旧资源就不同名了,不再需要以覆盖旧文件的形式主动更新各个地区的边缘站点。新版本发布后,浏览器首次请求资源,若 CDN 不存在该资源,便会向就近的边缘站点加载该文件,同时更新 CDN 缓存;这就彻底避免了 CDN 脏数据的问题.

说说你对nodejs的了解

我们从以下几方面来看nodejs.

  • 什么是nodejs?

    Node.js 是一个开源与跨平台的 JavaScript 运行时环境, 在浏览器外运行 V8 JavaScript 引擎(Google Chrome 的内核),利用事件驱动、非阻塞和异步输入输出模型等技术提高性能.

    可以理解为 Node.js 就是一个服务器端的、非阻塞式I/O的、事件驱动的JavaScript运行环境

    • 非阻塞异步

      Nodejs采用了非阻塞型I/O机制,在做I/O操作的时候不会造成任何的阻塞,当完成之后,以时间的形式通知执行操作

      例如在执行了访问数据库的代码之后,将立即转而执行其后面的代码,把数据库返回结果的处理代码放在回调函数中,从而提高了程序的执行效率

    • 事件驱动

      事件驱动就是当进来一个新的请求的时,请求将会被压入一个事件队列中,然后通过一个循环来检测队列中的事件状态变化,如果检测到有状态变化的事件,那么就执行该事件对应的处理代码,一般都是回调函数

  • 优缺点

    优点:

    • 处理高并发场景性能更佳
    • 适合I/O密集型应用,指的是应用在运行极限时,CPU占用率仍然比较低,大部分时间是在做 I/O硬盘内存读写操作

    因为Nodejs是单线程,带来的缺点有:

    • 不适合CPU密集型应用
    • 只支持单核CPU,不能充分利用CPU
    • 可靠性低,一旦代码某个环节崩溃,整个系统都崩溃
  • 应用场景

    借助Nodejs的特点和弊端,其应用场景分类如下:

    • 善于I/O,不善于计算。因为Nodejs是一个单线程,如果计算(同步)太多,则会阻塞这个线程
    • 大量并发的I/O,应用程序内部并不需要进行非常复杂的处理
    • 与 websocket 配合,开发长连接的实时交互应用程序

    具体场景可以表现为如下:

    • 第一大类:用户表单收集系统、后台管理系统、实时交互系统、考试系统、联网软件、高并发量的web应用程序
    • 第二大类:基于web、canvas等多人联网游戏
    • 第三大类:基于web的多人实时聊天客户端、聊天室、图文直播
    • 第四大类:单页面浏览器应用程序
    • 第五大类:操作数据库、为前端和移动端提供基于json的API

七、GIT

git经常用哪些指令

  • 产生代码库

    • 新建一个git代码库

      • git init
        
    • 下载远程项目和它的整个代码历史

      • git clone 远程仓库地址
        
  • 配置

    • 显示配置

      • git config --list [--global]
        
    • 编辑配置

      • git config -e [--global]
        
    • 设置用户信息

      • git config [--global] user.name "名"
        git config [--global] user.email "邮箱地址"
        
  • 暂存区文件操作

    • 增加文件到暂存区

      • # 1.添加当前目录的所有文件到暂存区
        git add .
        # 2.添加指定目录到暂存区,包括子目录
        git add [dir]
        # 3.添加指定文件到暂存区
        git add [file1] [file2] ...
        
    • 在暂存区中删除文件

      • # 删除工作区文件,并且将这次删除放入暂存区
        git rm [file1] [file2] ...
        # 停止追踪指定文件,但该文件会保留在工作区
        git rm --cached [file]
        
    • 重命名暂存区文件

      • # 改名文件,并且将这个改名放入暂存区
        git mv [file-original] [file-renamed]
        
  • 代码提交

    • # 提交暂存区到仓库区
      git commit -m [message]
      
  • 分支操作

    • # 列出所有本地分支
      git branch
      
      # 列出所有远程分支
      git branch -r
      
      # 列出所有本地分支和远程分支
      git branch -a
      
      # 新建一个分支,但依然停留在当前分支
      git branch [branch-name]
      
      # 新建一个分支,并切换到该分支
      git checkout -b [branch]
      
      # 新建一个分支,指向指定commit
      git branch [branch] [commit]
      
      # 新建一个分支,与指定的远程分支建立追踪关系
      git branch --track [branch] [remote-branch]
      
      # 切换到指定分支,并更新工作区
      git checkout [branch-name]
      
      # 切换到上一个分支
      git checkout -
      
      # 建立追踪关系,在现有分支与指定的远程分支之间
      git branch --set-upstream [branch] [remote-branch]
      
      # 合并指定分支到当前分支
      git merge [branch]
      
      # 选择一个commit,合并进当前分支
      git cherry-pick [commit]
      
      # 删除分支
      git branch -d [branch-name]
      
      # 删除远程分支
      git push origin --delete [branch-name]
      git branch -dr [remote/branch]
      
  • 信息查看

    • # 显示有变更的文件
      git status
      
      # 显示当前分支的版本历史
      git log
      
      # 显示commit历史,以及每次commit发生变更的文件
      git log --stat
      
      # 搜索提交历史,根据关键词
      git log -S [keyword]
      
      # 显示某个commit之后的所有变动,每个commit占据一行
      git log [tag] HEAD --pretty=format:%s
      
      # 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件
      git log [tag] HEAD --grep feature
      
      # 显示过去5次提交
      git log -5 --pretty --oneline
      
  • 同步操作

    • # 增加一个新的远程仓库,并命名
      git remote add [shortname] [url]
      
      # 取回远程仓库的变化,并与本地分支合并
      git pull [remote] [branch]
      
      # 上传本地指定分支到远程仓库
      git push [remote] [branch]
      
      # 强行推送当前分支到远程仓库,即使有冲突
      git push [remote] --force
      
      # 推送所有分支到远程仓库
      git push [remote] --all
      
  • 撤销操作

    • # 恢复暂存区的指定文件到工作区
      git checkout [file]
      
      # 恢复某个commit的指定文件到暂存区和工作区
      git checkout [commit] [file]
      
      # 恢复暂存区的所有文件到工作区
      git checkout .
      
      # 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
      git reset [file]
      
      # 重置暂存区与工作区,与上一次commit保持一致
      git reset --hard
      
      # 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
      git reset [commit]
      
      # 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
      git reset --hard [commit]
      
      # 重置当前HEAD为指定commit,但保持暂存区和工作区不变
      git reset --keep [commit]
      
      # 新建一个commit,用来撤销指定commit
      # 后者的所有变化都将被前者抵消,并且应用到当前分支
      git revert [commit]
      

git出现代码冲突怎么解决

冲突合并一般是因为自己的本地做的提交和服务器上的提交有差异,并且这些差异中的文件改动,Git不能自动合并,那么就需要用户手动进行合并

如我这边执行git pull origin master

如果Git能够自动合并,那么过程看起来是这样的:

拉取的时候,Git自动合并,并产生了一次提交。

如果Git不能够自动合并,那么会提示:

这个时候我们就可以知道README.MD有冲突,需要我们手动解决,修改README.MD解决冲突:

可以看出来,在1+1=几的这行代码上产生了冲突,解决冲突的目标是保留期望存在的代码,这里保留1+1=2,然后保存退出。

退出之后,确保所有的冲突都得以解决,然后就可以使用:

git add .
git commit -m "fixed conflicts"
git push origin master`

即可完成一次冲突的合并。

整个过程看起来是这样的:

你们团队是怎么管理git分支的

关于Git分支管理,每个团队在不同阶段都有自己的管理策略。我们团队以前采用的是版本分支管理策略,也就是每次上线新版本都会创建一个新的版本分支,而新的需求开发也从当前最新的版本分支迁出一个新的需求分支开发,线上bug则在版本分支上修改然后发布。

如何实现Git的免密操作

现在的远程仓库都提供了基于SSH协议的Git服务,在使用SSH协议访问仓库之前,需要先配置好账户/仓库的SSH公钥。

① 产生公私钥对

ssh-keygen -t rsa -C cherish@cherish.pw

产生完毕后,公私玥对位于c/Users/用户名/.ssh/

  • id_rsa:私钥(私有的钥匙,不能公开)
  • id_rsa.pub:公钥(可以公开的钥匙)

② 将公钥上传至远程仓库个人中心的设置里

以gitee为例,添加公钥请访问:gitee.com/profile/ssh…

注:如果想用现在的免密登录,请使用ssh协议去关联本地与线上仓库。

八、其它

loadsh 了解过吗?

Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库。

首先要明白的是lodash的所有函数都不会在原有的数据上进行操作,而是复制出一个新的数据而不改变原有数据。类似immutable.js的理念去处理。

Lodash 通过降低 array、number、objects、string 等等的使用难度从而让 JavaScript 变得更简单。 Lodash 的模块化方法 非常适用于:

  1. 遍历 array、object 和 string 对
  2. 值进行操作和检测
  3. 创建符合功能的函数

常用语法:cloneDeep深拷贝,uniq数组去重,compact 去除假值,filter和reject 过滤集合传入匿名函数, join 将 array 中的所有元素转换为由 separator 分隔的字符串等

是否用过混合APP开发

1.原生开发(NativeApp开发):像盖房子一样,先打地基然后浇地梁、房屋结构、一砖一瓦、钢筋水泥、电路走向等,原生APP同理:通过代码从每个页面、每个功能、每个效果、每个逻辑、每个步骤全部用代码写出来,一层层,一段段全用代码写出来

此种APP的数据都保存在本地,APP能及时调取,所以相应速度及流畅性有保障

2.混合开发(HTML5开发):这个就相当于一种框架开发,说白了就是网页;该模式通常由“HTML5云网站+APP应用客户端”两部份构成,APP应用客户端只需安装应用的框架部份,而应用的数据则是每次打开APP的时候,去云端取数据呈现给手机用户。

混合APP还有一种是套壳APP,套壳APP就是用H5的网页打包成APP,虽然是APP能安装到手机上,但是每个界面,全部是网页

混合APP开发优势:

时间短:基本都是模版拿来直接套上或打包成APP,会节省很大一部分时间。

价格便宜:代码不需要重新写,界面不用重新设计,都是固定的,可替换的地方很少,自己随便都能换上,所以价格相对便宜。

项目中的组件是如何使用的

可以使用第三方组件库以及自定义组件

vue项目中使用组件库为 vant / element-ui / iview / ant-design-vue

react常用组件库为 Ant Design / Ant Design Mobile

开发者也可以通过自定义实现组件的使用

hash和histoty的原理

  • hash模式:在浏览器中符号“#”,#以及#后面的字符称之为hash,用 window.location.hash 读取。特点:hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。
  • history模式:history采用HTML5的新特性;且提供了两个新方法: pushState(), replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更

window.location.href和history.push的区别

  • window.location.href 锚点跳转,不会加入路由栈。
  • history.push 向由路由栈中添加新的路由信息,点击浏览器返回按钮时返回到上一页。

商城项目中有写到调用微信,支付宝支付,简单讲述一下这个支付与后台对接的过程,微信支付的原理

  • 步骤1:用户在商户APP中选择商品,提交订单,选择微信支付。
  • 步骤2:商户后台收到用户支付单,调用微信支付统一下单接口。参见【统一下单API】。
  • 步骤3:统一下单接口返回正常的prepay_id,再按签名规范重新生成签名后,将数据传输给APP。参与签名的字段名为appid,partnerid,prepayid,noncestr,timestamp,package。注意:package的值格式为Sign=WXPay
  • 步骤4:商户APP调起微信支付。api参见本章节【app端开发步骤说明】
  • 步骤5:商户后台接收支付通知。api参见【支付结果通知API】
  • 步骤6:商户后台查询支付结果。,api参见【查询订单API】(查单实现可参考:支付回调和查单实现指引)

混合开发知道吗?你是怎么理解混合开发的,在项目中用到过混合开发吗?

  • 在APP应用中使用WebView嵌套H5页面,即为混合开发。
  • Android、IOS、uniapp、ReactNative、小程序,都支持混合开发了。

平时工作中有是否有接触linux系统?说说常用到linux命令?

1、常用的目录操作命令

  • 创建目录 mkdir <目录名称>
  • 删除目录 rm <目录名称>
  • 定位目录 cd <目录名称>
  • 查看目录文件 ls ll
  • 修改目录名 mv <目录名称> <新目录名称>
  • 拷贝目录 cp <目录名称> <新目录名称>

2、常用的文件操作命令

  • 创建文件 touch <文件名称> vi <文件名称>
  • 删除文件 rm <文件名称>
  • 修改文件名 mv <文件名称> <新文件名称>
  • 拷贝文件 cp <文件名称> <新文件名称>

3、常用的文件内容操作命令

  • 查看文件 cat <文件名称> head <文件名称> tail <文件名称>
  • 编辑文件内容 vi <文件名称>
  • 查找文件内容 grep '关键字' <文件名称>

echarts是什么,怎么用

echarts是一个基于 JavaScript 的开源可视化图表库,可以流畅的运行在 PC 和移动设备上,兼容绝大部分的浏览器(IE9/10/11,Chrome,Firefox,Safari等),底层依赖矢量图形库 ZRender,提供20 多种图表和十几种组件,支持Canvas、SVG 双引擎并能一键切换,让移动端渲染更加流畅

echarts源自百度,现由Apache 开源社区维护,官方提供了完善的文档( echarts.apache.org ),使用也非常简单,步骤如下:

  1. 下载echarts

  2. 引入echarts

        <!DOCTYPE html>
        <html>
            <head>
                <meta charset="utf-8" />
                <script src="echarts.js"></script>
            </head>
        </html>
    
  3. 添加一个div容器并设置宽高

    注意:不设置宽高无法显示效果

        <body>
            <div id="main" style="width: 600px;height:400px;"></div>
        </body>
    
  4. 初始化一个 echarts 实例并通过 setOption 方法生成一个图表(以柱状图为例)

        <script type="text/javascript">
            // 基于准备好的dom,初始化echarts实例
            var myChart = echarts.init(document.getElementById('main'));
    
            // 指定图表的配置项和数据
            var option = {
                title: {
                    text: 'ECharts 入门示例'
                },
                tooltip: {},
                legend: {
                    data: ['销量']
                },
                xAxis: {
                    data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
                },
                yAxis: {},
                series: [
                    {
                        name: '销量',
                        type: 'bar',
                        data: [5, 20, 36, 10, 10, 20]
                    }
                ]
            };
    
            // 使用刚指定的配置项和数据显示图表。
            myChart.setOption(option);
        </script>
    

Hash和history的区别

hash与history一般指Vue-router或React-router中的路由模式,Vue-router中通过mode配置选项设置,React-router中通过HashRouterBrowerRouter组件设置,区别如下:

  1. 最直观的区别就是在浏览器url中hash 带了一个很丑的 #,而history是没有
  2. hash即浏览器地址栏 URL 中的 # 符号,hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,因此改变 hash 不会重新加载页面
  3. history利用了 HTML5新增的 pushState()replaceState() 方法,在已有 back()forward()go() 的基础上,提供了对历史记录进行修改的功能。调用pushState()replaceState()时,虽然改变了当前的 URL,但浏览器不会向后端发送请求,但如何用户刷新页面,会导致浏览器向服务器发起请求,如后端没有做出适当的响应,则会显示404页面

谈谈宏任务与微任务的理解,举一个宏任务与微任务的api

要理解宏任务(macrotask)与微任务(microtask),就必须了解javascript中的事件循环机制(event loop)以及js代码的运行方式

要了解js代码的运行方式,得先搞懂以下几个概念

  • JS是单线程执行

    单线程指的是JS引擎线程

  • 宿主环境

    JS运行的环境,一般为浏览器或者Node

  • 执行栈

    是一个存储函数调用的栈结构,遵循先进后出的原则

JS引擎常驻于内存中,等待宿主将JS代码或函数传递给它执行,如何传递,这就是事件循环(event loop) 所做的事情:当js执行栈空闲时,事件循环机制会从任务队列中提取第一个任务进入到执行栈执行,优先提取微任务(microtask),待微任务队列清空后,再提取宏任务(macrotask),并不断重复该过程

在实际应用中,宏任务(macrotask)与微任务(microtask)的API分别如下:

  • 宏任务

    • setTimeout/setInterval
    • ajax
    • setImmediate (Node 独有)
    • requestAnimationFrame (浏览器独有)
    • I/O
    • UI rendering (浏览器独有)
  • 微任务

    • process.nextTick (Node 独有)
    • Promise
    • Object.observe
    • MutationObserver

对Event loop的了解?

Javascript是单线程的,那么各个任务进程是按照什么样的规范来执行的呢?这就涉及到Event Loop的概念了,EventLoop是在html5规范中明确定义的;

何为eventloop,javascript中的一种运行机制,用来解决浏览器单线程的问题

Event Loop是一个程序结构,用于等待和发送消息和事件。同步任务、异步任务、微任务、宏任务

javascript单线程任务从时间上分为同步任务和异步任务而异步任务又分为宏任务(macroTask)和微任务(microTask)

宏任务:主代码块、setTimeOut、setInterval、script、I/O操作、UI渲染

微任务:promise、async/await(返回的也是一个promise)、process.nextTick

在执行代码前,任务队列为空,所以会优先执行主代码块,再执行主代码块过程中,遇到同步任务则立即将任务放到调用栈执行任务,遇到宏任务则将任务放入宏任务队列中,遇到微任务则将任务放到微任务队列中。

主线程任务执行完之后,先检查微任务队列是否有任务,有的话则将按照先入先出的顺序将先放进来的微任务放入调用栈中执行,并将该任务从微任务队列中移除,执行完该任务后继续查看微任务队列中是否还有任务,有的话继续执行微任务,微任务队列null时,查看宏任务队列,有的话则按先进先出的顺序执行,将任务放入调用栈并将该任务从宏任务队列中移除,该任务执行完之后继续查看微任务队列,有的话则执行微任务队列,没有则继续执行宏任务队列中的任务。

需要注意的是:每次调用栈执行完一个宏任务之后,都会去查看微任务队列,如果microtask queue不为空,则会执行微任务队列中的任务,直到微任务队列为空再返回去查看执行宏任务队列中的任务

console.log('script start')
 
async function async1() {
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end') 
}
async1()
 
setTimeout(function() {
  console.log('setTimeout')
}, 0)
 
new Promise(resolve => {
  console.log('Promise')
  resolve()
})
  .then(function() {
    console.log('promise1')
  })
  .then(function() {
    console.log('promise2')
  })
 
console.log('script end')

最终输出 :

script start
VM70:8 async2 end
VM70:17 Promise
VM70:27 script end
VM70:5 async1 end
VM70:21 promise1
VM70:24 promise2
undefined
VM70:13 setTimeout

解析:

在说返回结果之前,先说一下async/await,async 返回的是一个promise,而await则等待一个promise对象执行,await之后的代码则相当于promise.then()

async function async1() {
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end') 
}
 
//等价于
new Promise(resolve => 
resolve(console.log('async2 end'))
).then(res => {
console.log('async1 end')
})

整体分析:先执行同步任务 script start -> async2 end -> await之后的console被放入微任务队列中 -> setTimeOut被放入宏任务队列中 ->promise -> promise.then被放入微任务队列中 -> script end -> 同步任务执行完毕,查看微任务队列 --> async1 end -> promise1 -> promise2 --> 微任务队列执行完毕,执行宏任务队列打印setTimeout