小程序逐字稿

487 阅读12分钟

mm## 1、说说小程序的开发流程?

  • 首先要在微信公众平台申请一个小程序账号,并填写相应信息。通过这个账号对小程序的开发进行管理。
  • 获取小程序唯一标识:AppID。他在小程序上线的时候用到
  • 下载微信开发者工具。微信开发者工具是官方提供的专门用于微信小程序开发调试的工具。
  • 通过开发者工具创建小程序项目
  • 配置项目中用到的域名
  • 项目开发完毕后预览项目并进行优化
  • 上传至微信服务器提交审核
  • 发布上线

2、说说小程序是如何上线的?

小程序上线前,我们先将项目中与代码逻辑无关的文件进行处理。我们可以将不用的图片和文档进行删除。还可以通过在project.config.json中配置来忽略这些文件。完后将小程序代码上传至小程序的官方服务器上,先将小程序设定为体验版本供公司测试人员进行内测,没问题的话提交审核,审核通过即可上线。

{
  "miniprogramRoot": "miniprogram/",
  "packOptions": {
    "ignore": [
      { "type": "folder", "value": "static/uploads" },
      { "type": "file", "value": "ui.zip" }
    ]
  }
}

`"type": "folder"` 忽略文件目录,`"value"` 用来指明具体忽略的文件目录
`"type": "file"` 忽略文件,`"value"` 用来指明具体忽略的文件

3、聊聊小程序的生命周期

小程序的生命周期是一组名称固定且会被自动调用执行的函数,它分为应用级别、页面级别和组件级别3种类型。

  • 应用级别:应用级别的生命周期函数定义在 app.js 当中

Snipaste_2023-06-16_22-24-17.png

  • 页面级别:页面级别的生命周期函数写在页面对应的页面 .js 当中

Snipaste_2023-06-16_22-25-29.png

  • 组件级别
    • 组件的生命周期,指的是组件自身的一些函数,这些函数在特殊的时间点或遇到一些特殊的框架事件时被自动触发。 其中,最重要的生命周期是 created attached detached ,包含一个组件实例生命流程的最主要时间点。

    • 组件实例刚刚被创建好时, created 生命周期被触发。此时,组件数据 this.data 就是在 Component 构造器中定义的数据 data 。 此时还不能调用 setData 。  通常情况下,这个生命周期只应该用于给组件 this 添加一些自定义属性字段。

    • 在组件完全初始化完毕、进入页面节点树后, attached 生命周期被触发。此时, this.data 已被初始化为组件的当前值。这个生命周期很有用,绝大多数初始化工作可以在这个时机进行。

    • 在组件离开页面节点树后, detached 生命周期被触发。退出一个页面时,如果组件还在页面节点树中,则 detached 会被触发。

4、小程序中能直接使用npm包吗?#两种构建方式

小程序不能直接使用在 npm 下载的模块包,必须经过小程序开发者工具进行构建后才可以使用。使用npm包的构建方式有两种:

  • 默认构建:我们可以打开小程序的终端安装需要的npm模块包,然后触发小程序里的“构建npm”命令即可将模块导入使用。
  • 自定义构建:为便于小程序的后期维护,我们通常新建一个单独文件夹存放小程序的相应文件,这时候,通过project.config.json 可以指定小程序的根目录,这样做的好处是能够优化目录结构,更好的管理项目的代码。
{
  "setting": {
    ...
    "packNpmManually": true,
    "packNpmRelationList": [
      {
        "packageJsonPath": "./package.json",
        "miniprogramNpmDistDir": "./miniprogram"
      }
    ],
    ...
  },
  "libVersion": "2.19.4",
  "miniprogramRoot": "miniprogram/",
  "appid": "wx3eb80995b7e84924",
  "projectname": "mpdemo",
}
  • packNpmManually 启用 npm 构建手动配置
  • packNpmRelationList 手动构建 npm 配置详情
  • miniprogramRoot 自定义小程序的根目录

5.小程序中如何提高首次加载的速度?

  • 分包加载:将小程序拆分成若干个部分叫做分包,在分包的基础上能够实现自动加载当前所需部分小程序代码,在一定程序能够提升小程序的加载速度,同时也能解决小程序代码包大小不能超过 2M 的限制。

  • 合理使用 setData:setData 是小程序开发中使用最频繁、也是最容易引发性能问题的接口。setData 应只用来进行渲染相关的数据更新。用 setData 的方式更新渲染无关的字段,会触发额外的渲染流程,或者增加传输的数据量,影响渲染耗时。

  • 渲染性能优化:

    • 适当监听页面或组件的 scroll 事件

    • 选择高性能的动画实现方式

      • 优先使用 CSS 渐变、CSS 动画、或小程序框架提供的其他动画实现方式完成动画;
    • 控制 WXML 节点数量和层级

      • 一个太大的 WXML 节点树会增加内存的使用,样式重排时间也会更长,影响体验。
    • 控制在 Page 构造时传入的自定义数据量

  • 资源加载优化

    • 控制图片资源的大小
    • 避免滥用 image 组件的 widthFix/heightFix 模式

6.分包预加载是如何实现的以及你的理解?

  • 我的理解: 分包是小程序项目开发过程中性能实现优化的一个重要方式。将小程序拆分成若干个部分叫做分包,在分包的基础上能够实现自动加载当前所需部分小程序代码,在一定程度上能够提升小程序的加载速度,同时也能解决小程序代码包大小不能超过 2M 的限制。
  • 如何实现:分包从形式上来看就是将某些功能相关的页面及其依赖的资源放到独立的文件夹中,然后在 app.json 文件通过 subPackages 配置要加载的分包:
{
  // 省略部分代码...
  "subPackages": [
    {
      "root": "分包的根路径",
      "name": "分包名称",
      "pages": [
        // 分页页面的路径
      ]
    }
  ]
}

  "subPackages": [
    {
      "root": "house_pkg",
      "pages": [
        "pages/list/index",
        "pages/detail/index",
        "pages/locate/index",
        "pages/building/index",
        "pages/room/index",
        "pages/form/index"
      ]
    }
  ],

分包预加载我们行为在进入某个页面时触发,通过在app.json增加preloadRule配置来控制。同一个分包中的页面享有共同的预下载大小限额2M。

7.聊聊你对小程序和vue的理解?

  • 相同点:
    • 1、都是数据驱动视图模式,都能实现数据的双向绑定
    • 2、都是单页面应用
  • 不同点:
    • 语法:

      • 小程序:遍历wx-for 获取数据this.data.item 赋值this.setData({item: 1}); 数据绑定: 转存失败,建议直接上传图片文件
      • vue:v-for this.item
    • 子组件写法

      • 小程序:a.创建子组件且有四个文件;b.父组件在引入自定义组件的时候需要通过usingComponents填写引入组件的组件名以及路径;c.在父组件中直接引入
      • vue:a.创建子组件且只有一个文件;b.在需要使用的父组件中通过import引入;c.在vue的components中注册;d.在模板中使用;
    • html元素的使用

      • 小程序有自己的标签
      • vue支持html标签
    • 生命周期不一样

      • 小程序:钩子函数因其跳转方式触发的钩子不一样
      • vue: vue项目中,只要界面跳转,钩子函数都会触发。
    • 数据请求

      • 小程序:在钩子函数onLoad或onShow里面请求数据
      • vue:在钩子函数created里面请求数据

8、项目中有封装过哪些工具方法?

  • 项目开发中,我们可以通过全局或者局部的方法进行工具的封装;全局封装和局部封装的区别在于,全局封装后将方法挂载到wx对象上,然后将这个对象引入到app.js中,其他界面可以直接通过wx对象调用。而,局部方法通过export default进行导出,其他界面使用的时候,需要先导入这个方法后才能用。这样会显得很麻烦。
  • 下面以封装http请求做个说明;我们先通过npm下载模块wechat-http;在utils文件夹里面新建http.js。通过import将下载好的模块导入。
    //全局封装 
    // 导入 http 模块
    import http from 'wechat-http'
    /**
     * 接口基础路径
     */
    http.baseURL = 'https://live-api.itheima.net'
    /**
     * 配置响应拦截器
     */
    http.intercept.response = (res) => {
      // 过滤接口返回的数据
      return res.data
    }
    /**
     * 挂载到wx全局对象
     */
    wx.http = http                
    
    //挂载到app.js中
    import http from '../utils/http.js' // 其他界面在使用这个方法的时候直接wx.http.get('xxxx')即可
        
    
    
    // 局部封装
    // 导入 http 模块
    import http from 'wechat-http'
    /**
     * 接口基础路径
     */
    http.baseURL = 'https://live-api.itheima.net'
    /**
     * 配置响应拦截器
     */
    http.intercept.response = (res) => {
      // 过滤接口返回的数据
      return res.data
    }
    export default http                                                                                   mm
    
    // 为确保其他界面能使用这个方法,我们需要先将方法引入app.js里面,然后其他界面就可以通过getApp()对这个方法进行调用
        // app.js
        import http from './utils/http'
        
        // 其他界面在调用的时候
        const http = getApp().http

9、说说项目中是怎么实现用户登录状态检测?

在我们项目中,有三个界面:首页(列表页)、登录页、首页详情页不需要token就能访问。其他界面都需要获取token才能访问。我们采取的办法是封装一些逻辑来检测用户是否登录。

  • 我们通过本地存储的token来判断用户的登录状态,在小程序启动时候读取本地存储并记录到应用实例当中,方便其他界面全局访问:
    // app.js
        App({
          onLaunch() {
            // 读取 token
            this.getToken()
          },
          getToken() {
            // 异步方式不会阻塞
            wx.getStorage({
              key: 'token',
              success: ({ data }) => {
                // this 指向应用实例
                this.token = data
              },
              fail() {},
            })
          }
        })

  • 小程序中不支持路由拦截,需要开发者自行封装路由拦截的功能,实践有许多的实现思路,本项目采用封装组件的方式实现。
    • 主要的步骤:

      • 封装名称为 authorization 的组件
      • 在生命周期函数中读取全局中记录的 token 数据
      • 获取当前页面的路径,在未登陆的情况下通过地址参数传给登录页面
    • 封装authorization组件并在app.js中注册。使用这个组件的逻辑是,这个组件里面是一个由变量isLogin控制的插槽。而这个变量的值(boolean)由token来决定:

    // 组件authorization将对应的内容包裹住
    <authorization>
      <block wx:if="{{true}}">             // wx:if 为小程序的控制属性
        ...
      </block>
      <view wx:else class="blank">
        您还没有认证房屋,请点击 <navigator hover-class="none" class="link" url=" ">添加</navigator>
      </view>
    </authorization>

    
    // 组件authorization里面的js文件获取本地存储的token并将其赋值给isLogin:有token则isLogin为真,那么被他包围的内容就会显示,相反,就会隐藏。
    
    //<!-- 用户未登录就不显示页面的内容 -->
    <slot wx:if="{{isLogin}}"></slot>
    
   // 在authorization中的index.js读取登录状态
    Component({
      data: {
        isLogin: false,
      },
      // 生命周期函数
      lifetimes: {
        attached() {
          // 登录状态
          const isLogin = !!getApp().token
          // 记录登录状态
          this.setData({ isLogin })
          // 未登录重定向到登录页
          if (!isLogin) {
            // 引导用户到登录页面
            wx.redirectTo({
              url: `/pages/login/index`,
            })
          }
        },
      },
    })
  • 一个优化:要实现这个功能需要咱们先获取用户正在访问的页面路径,然后把这个路径传给登录页面,这样在登录成功后便可以再跳转到回这个页面了。
    // components/authorization/index.js
    Component({
      data: {
        isLogin: false,
      },
      // 生命周期函数
      lifetimes: {
        attached() {
          // 登录状态
          const isLogin = !!getApp().token
          // 记录登录状态
          this.setData({ isLogin })
          // 未登录重定向到登录页
          if (!isLogin) {
            // 读取当前历史栈
            const pageStack = getCurrentPages()
            // 取出当前页面路径,登录成功能跳转到该页面
            const currentPage = pageStack[pageStack.length - 1]
            // 取出当前页面路径,登录成功能跳转到该页面
            const redirectURL = currentPage.route
            // 引导用户到登录页面
            wx.redirectTo({
              url: `/pages/login/index?redirectURL=/${redirectURL}`,
            })
          }
        },
      },
    })

10、项目中为什么要使用refreshToken?单token和双token的区别是什么

为什么要使用token:

  • token需要有一定的时效性,不然会使得用户的个人信息产生泄露,失效的token不能标识用户处于登陆状态。
  • 在用户使用产品的时候,若使用时token失效,用户此刻再去登陆,会产生不友好的体验。
  • refreshToken的出现就是为了解决以上两个缺陷。我们给refreshToken比如8天的有效期,而token的有效期为3小时,当token失效的时候,我们会在拦截器获取到后端返回的401状态码。然后通过refreshToken再次请求新的token。这样就降低了token被盗的风险。

token和refreshToken的区别是什么

  • token:访问令牌,一个用来访问受保护资源的凭证。

  • refreshToken:一个用来获取token的凭证。

  • 时效性

    • token一般有几个小时
    • refreshToken可能有几十天

11.说说refreshToken具体的实现思路?

  • pc端使用的是单token,当token失效的时候,会直接将界面切换到登录界面
  • 用户首次完成登陆的时候,会分别得到token和refreshToken。
App({
  // ...
  setToken(token, refresh_token) {
  // 拼凑合法token格式
  token = 'Bearer ' + token
  refresh_token = 'Bearer ' + refresh_token

  // 本地存储 token 和 refresh_token
  wx.setStorageSync('token', token)
  wx.setStorageSync('refresh_token', refresh_token)
  // 更新全局 token 和 refresh_token
  this.token = token
  this.refresh_token = refresh_token
  },
})
  • 当token失效后,我们调用接口会在拦截器中获取后端返回的401状态码。
  • 检测状态码若是401,我们会通过refreshToken向后端发送请求重新获取新的token和refreshToken以此延长token的时效。
   // 返回401状态码,使用refreshToken更新token
http.intercept.response = async ({ statusCode, data }) => {
  // statusCode 为状态码
  if (statusCode === 401) {
    // 获取全局应用实例
    const app = getApp()
    // 使用 refreshToken 更新 token
    const res = await http({
      url: '/refreshToken',
      method: 'POST',
      header: {
        // 这时要注意使用的是 refresh_token
        Authorization: app.refresh_token,
      },
    })
    // 更新 token 和 refresh_token
    app.setToken(res.data.token, res.data.refreshToken)
  }

  // 过滤接口返回的数据
  return data
}
// ...

  • 调用延长token时效的接口会返回新的token和refreshToken。我们将其再次存放到本地存储中去。
http.intercept.response = async ({ statusCode, data, config }) => {
  // statusCode 为状态码
  if (statusCode === 401) {
    // config 是调用接口的参数
    // refreshToken 过期的情形
    if (config.url.includes('/refreshToken')) {
      // 读取当前历史栈
      const pageStack = getCurrentPages()
      // 取出当前页面路径,登录成功能跳转到该页面
      const lastPage = pageStack[pageStack.length - 1]
      // 取出当前页面路径,登录成功能跳转到该页面
      const redirectURL = lastPage.route

      // 引导用户到登录页面
      return wx.redirectTo({
        url: `/pages/login/index?redirectURL=/${redirectURL}`,
      })
    }

    // 获取全局应用实例
    const app = getApp()
    // 使用 refreshToken 更新 token
    const res = await http({
      url: '/refreshToken',
      method: 'POST',
      header: {
        // 这时要注意使用的是 refresh_token
        Authorization: app.refresh_token,
      },
    })

    // 更新 token 和 refresh_token
    app.setToken(res.data.token, res.data.refreshToken)
  }

  // 过滤接口返回的数据
  return data
}
  • 两个token都失效如何处理
  • 无感请求

12、谈谈你对uniapp和vue的理解

  • uniapp优点

    • 1、跨平台开发:可以通过打包实现一套代码多端运行,vue不能实现多端开发。
    • 2、丰富的组件库,包括自定义组件、原生组件和插件组件,可以帮助开发者快速构建应用技术。
    • 3、可扩展性和可定制性,uniapp支持插件开发和自定义主题
    • 4、门槛低,uniapp语法接近vue,对初学者容易上手
    • 5、uniapp有自动的框架预载,加载页面的速度更快
  • uniapp缺点

    • 1、功能受限,uniapp作为一个跨平台框架,其功能受到相应的限制,可能无法满足客户的特殊需求
    • 2、性能不高,为能兼顾各个平台的要求,uniapp存在性能问题,在配置低的手机中运行其程序会出现卡顿情况。
    • 3、灵活性差,因为需要适应不同的平台,所以某些方面会存在不灵活的情况出现。
  • vue优点

    • 1、轻量灵活:vue作为一个轻量级框架,很灵活,能满足不同开发需求
    • 2、上手简单:相较于reactJs偏原生的语法,vue语法简单易懂,易上手,掌握容易。
    • 3、生态系统完善:vue拥有包括router/vuex等完善的生态系统,让开发变得容易、高效。
    • 4、虚拟DOM:vue使用js的对象创建虚拟DOM,并且配合diff和patch算法对虚拟DOM进行高效的更新。让性能得以提升不少。
    • 5、MVVM设计模式,数据驱动视图
    • 6、组件化
  • vue缺点

    • 1、生态环境不如react
    • 2、不支持IE8以下版本浏览器
    • 3、单页面不利于SEO优化

13、请说说小程序的支付流程

    // 创建订单:请求创建订单接口,把收货地址、订单中包含的商品信息发送到服务器,服务器返回订单编号;
    // 微信支付
    async payOrder() {
      // 1. 创建订单
      // 1.1 发送请求
      const { meta, message } = await this.$http.post("/my/orders/create", {
        // 开发期间不填写真实的订单价格,写死订单总价为1分钱
        order_price: 0.01,
        // 收获地址
        consignee_addr: this.fullAddress,
        // 购物车中选中的商品
        goods: this.$store.state.cart.cartItems.filter(x => x.goods_state).map(x => ({
          goods_id: x.goods_id,
          goods_number: x.goods_count,
          goods_price: x.goods_price,
        })),
      });

      if (meta.status !== 200) {
        return this.$msg("创建订单失败!");
      }

      // 1.2 得到 “订单编号”
      const orderNumber = message.order_number;
    }
    
  // 获取支付参数:请求订单支付接口:把订单编号发送到服务器,服务器返回订单预支付的参数对象