最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 技术提炼|盘点那些Vue项目中的优秀实践-小程序篇

    正文概述 掘金(Minorjone)   2020-12-01   381

    上一篇总结了有关控制台的优秀实践,

    技术提炼|盘点那些Vue项目中的优秀实践-PC控制台篇

    这一篇我们来聊聊小程序,这里的小程序开发使用的是uniapp。

    cross-env切换环境

    在pc端的vue项目中,我们通常会使用vue的环境变量来控制程序在不同环境的切换,例如:

    目录结构:

    ├── .env.development
    ├── .env.test
    ├── vue.config.js
    

    vue.config.js:

    module.exports = {
      devServer: {
        port: 8062,
        proxy: {
          '/api': {
            target: process.env.BASEURL,
            pathRewrite: {
              '^/api': '/api'
            }
          }
        }
      }
    }
    

    .env.development:

    BASEURL=http://developapi.com/
    

    .env.test:

    BASEURL=http://testapi.com/
    

    这样我们只需要通过package.json里不同环境的script运行,就可以切换到想要的环境。在小程序里,我们通常都是在微信开发工具里编译运行,这个时候我们就可以借助cross-envscript来进行环境的切换。

    首先,我们需要先安装cross-env

    npm install --save-dev cross-env
    

    然后我们改写package.json的script,让我们在运行script的时候额外用node执行一个js文件

    目录结构:

    ├── script                        
    │   └── build.js      # 用来修改baseurl的脚本文件
    ├── utils
    │   ├── http.js       # 对axios的再次封装,引入config中的url                      
    │   └── config.js     # 指定不同环境的baseurl
    ├── package.json
    ├── manifest.json
    

    package.json:

    "scripts": {
      "dev": "cross-env NODE_ENV=development node ./script/build.js",
      "test": "cross-env NODE_ENV=test node ./script/build.js",
      "pro": "cross-env NODE_ENV=production node ./script/build.js"
    }
    

    build.js:

    const fs = require('fs')
    const path = require('path')
    const manifest = require("../manifest.json")
    const config = require("../utils/config.json")
    
    switch (process.env.NODE_ENV) {
      case 'development':
        manifest["mp-weixin"].appid = 'somewxid1'
        config.DEV = 'https://devapi1.com/miniapp/'
        config.PRO = 'https://proapi1.com/miniapp/'
        break;
      case 'test':
        manifest["mp-weixin"].appid = 'somewxid2'
        config.DEV = 'https://devapi2.com/miniapp/'
        config.PRO = 'https://proapi2.com/miniapp/'
        break;
      case 'production':
        manifest["mp-weixin"].appid = 'somewxid3'
        config.DEV = 'https://devapi3.com/miniapp/'
        config.PRO = 'https://proapi3.com/miniapp/'
        break;
    }
    
    try {
      fs.writeFileSync(path.resolve(__dirname, '../manifest.json'), JSON.stringify(manifest, null, 4))
      fs.writeFileSync(path.resolve(__dirname, '../utils/config.json'), JSON.stringify(config, null, 4))
    } catch (error) {
      console.error(error)
    }
    
    console.log('修改成功')
    

    config.json:

    {
      "DEV": "https://devapi1.com/miniapp/",
      "PRO": "https://proapi1.com/miniapp/"
    }
    

    移动端开发中的css

    设计规范

    在移动端开发中,我们通常会有一份设计规范,这份规范通常会包含一下内容:

    • 项目中所使用的字体大小样式及其对应应用场景
    • 项目中使用到的颜色及其对应场景
    • 项目中一些通用组件的样式

    对于这个规范,我们的最佳实践方式是用一个css预编译器文件把这些样式写成常量,在页面中直接取用,例如:

    // mixins
    .ellipsis(@line: 2) {
      display: -webkit-box;
      -webkit-box-orient: vertical;
      -webkit-line-clamp: @line;
      overflow: hidden;
    }
    // Color Palette
    @primary: #E20000;
    @black: #000;
    @white: #fff;
    @gray-1: #f7f8fa;
    @gray-2: #f2f3f5;
    @gray-3: #ebedf0;
    @gray-4: #dcdee0;
    @gray-5: #c8c9cc;
    @gray-6: #969799;
    @gray-7: #646566;
    @gray-8: #323233;
    @red: #ee0a24;
    @blue: #1989fa;
    @orange: #ff976a;
    @orange-dark: #ed6a0c;
    @orange-light: #fffbe8;
    @green: #07c160;
    
    // Gradient Colors
    @gradient-red: linear-gradient(to right, #ff6034, #ee0a24);
    @gradient-orange: linear-gradient(to right, #ffd01e, #ff8917);
    
    // Component Colors
    @text-color: @gray-8;
    @active-color: @gray-2;
    @active-opacity: .7;
    @disabled-opacity: .5;
    @background-color: @gray-1;
    @background-color-light: #fafafa;
    
    // Padding
    @padding-base: 4px;
    @padding-xs: @padding-base * 2;
    @padding-sm: @padding-base * 3;
    @padding-md: @padding-base * 4;
    @padding-lg: @padding-base * 6;
    @padding-xl: @padding-base * 8;
    
    // Font
    @font-size-xs: 10px;
    @font-size-sm: 12px;
    @font-size-md: 14px;
    @font-size-lg: 16px;
    @font-weight-bold: 500;
    @price-integer-font-family: Avenir-Heavy, PingFang SC, Helvetica Neue, Arial, sans-serif;
    
    // Animation
    @animation-duration-base: .3s;
    @animation-duration-fast: .2s;
    
    // Border
    @border-color: @gray-3;
    @border-width-base: 1px;
    @border-radius-sm: 2px;
    @border-radius-md: 4px;
    @border-radius-lg: 8px;
    @border-radius-max: 999px;
    

    相对布局

    适配不同手机的css显示,无疑是一项绕不开的课题,这里主要想说说如何用好相对布局。

    px,em,rem

    px:

    • 解释:px像素(Pixel)。相对长度单位。像素px是相对于显示器屏幕分辨率而言的。

    em:

    • 解释:em是相对长度单位。相对于当前对象内文本的字体尺寸。如当前对行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸。
    • 缺点:em会继承父级元素的字体大小(参考物是父元素的font-size)所以会逐级嵌套计算。

    rem:

    • 解释:rem是css3中提出来基于em的优化,rem依旧是相对长度单位,但它相对的是根元素。
    • 优点:只修改根元素就成比例地调整所有字体大小,避免字体大小逐层复合的连锁反应。

    合理的使用px,em,rem可以帮助我们更好的控制大小。在各大小程序里也有诸如rpx这样小程序提供的相对长度单位可以使用。

    百分比和flex

    在实现相对布局上,早时候我们最常使用百分比结合行内元素的方法,但这类方法的缺点也十分明显,当我们行内的元素变多,我们需要手动重新计算百分比,动态的增减元素也需要重新计算,所以flex就变得更加受追捧。

    有关flex的教程,推荐大家看一看阮大神的这篇博客。

    分享生成海报

    大多数小程序都有分享进行推广的业务场景,虽然小程序自带分享卡片的功能,但因为它的不够直观和相对死板,实际开发中我们更多会使用生成海报分享。对于一些电商小程序,生成的海报还会附带一些额外的功能,例如用户分销上下级绑定,这里我们就来简单介绍一个分享海报的实现。(因为这里我们只专注实现逻辑,所以css的部分就不做展示了)

    目录结构:

    ├── components                   # 全局公共组件    
    │   ├── share-popup
    │   │   └── share-popup.vue      # 选择微信分享或海报分享上拉菜单
    │   └── poster-item
    │       └── poster-item.vue      # 生成海报组件
    ├── utils                 
    │   └── message.js               # 指定分享参数
    

    首先我们先来看看share-popup:

    <template>
      <uni-popup ref="popup">
        <div class="icon-content">
          <div class="item">
            <img class="icon" src="~@/static/poster.png" @click="clickPoster"/>
            <span>生成海报</span>
          </div>
          <div class="item" @click="close">
            <button
              class="share-btn"
              open-type="share"
              :data-
              :data-imgurl="shareInfo.png"
              :data-path="shareInfo.path">
              <img class="icon" src="~@/static/wechat_icon.png" />
            </button>
            <span>分享到微信</span>
          </div>
        </div>
      </uni-popup>
    </template>
    <script>
    export default {
      name: 'share-popup',
      props: {
        shareInfo: {
          type: Object,
          defalut () {
            return {
              title: '',
              path: '',
              image: ''
            }
          }
        }
      },
      data () {
        return {
        }
      },
      computed: {
    		info () {
    			return this.$store.state.user.userInfo
        }
      },
      methods: {
        clickPoster () {
          this.$emit('sharePoster')
          this.close()
        },
        open() {
          this.$refs.popup.open()
        },
        close() {
          this.$refs.popup.close()
        }
      }
    }
    </script>
    

    分享到微信其实就是用了微信button的开放接口,这里的关键在于我们调用组件时传入的参数。

    接下来我们看下poster-item,在这个组件里,我们将会在海报中展示这些信息:

    1. 用户昵称
    2. 用户头像
    3. 设计好的海报背景图
    4. 小程序分享二维码
    5. 海报中需要展示的分享详情(这里是商品价格、划线价、名称)
    <template>
      <uni-popup ref="popup" :maskClick="false" :animation="false">
        <div class="flex column">
          <div class="btn-close-wrapper">
            <div class="btn-close" @click="close"></div>
          </div>
          <canvas class="canvas-code" canvas-id="canvas" style="width: 300px;height: 452px;">
          </canvas>
          <div class="btn-save" @click.stop="save">保存到相册</div>
        </div>
      </uni-popup>
    </template>
    <script>
    const promisify = (fn) => {
      return function(args = {}) {
        return new Promise((resolve, reject) => {
          fn.call(null, {
            ...args,
            success (data) {
              console.log('data', data)
              resolve(data)
            },
            fail (err) {
              console.log('err', err)
              reject(err)
            }
          })
        })
      }
    }
    const downloadFile = promisify(uni.downloadFile)
    export default {
      name: 'poster-item',
      props: {
        item: {
          type: Object,
          defalut () {
            return {
              realPrice: '0.00',
              price: '0.00',
              name: '',
              image: ''
            }
          }
        },
        info: {
          page: '',
          scene: ''
        }
      },
      data () {
        return {
          imgSrc: '',
          ctx: {}
        }
      },
      computed: {
        username () {
          return this.$store.state.user.userInfo.nickname
        },
        avatar () {
          const avatar = this.$store.state.user.userInfo.avatar
          if (/^http/.test(avatar)) {
            return avatar
          }
          return false
        }
      },
      methods: {
        draw ({ realPrice, price, name, username, itemImage, avatar, qrcode }) {
          const ctx = this.ctx
          ctx.drawImage(itemImage,0, 0, 300, 300);
          ctx.setFillStyle('#F0F0F0')
          ctx.fillRect(0, 300, 300, 152)
          ctx.drawImage(require("@/static/bg.png"), 0, 322, 202, 131);
          
          ctx.font = 'bold 14px "HelveticaNeue-Bold,HelveticaNeue"'
          ctx.setFillStyle('#D0021B')
          ctx.fillText('¥', 24, 334)
          ctx.setFontSize(20)
          // realPrice
          ctx.fillText(realPrice, 38, 334)
          if (realPrice < price) {
            const w1 = ctx.measureText(realPrice).width
            ctx.font = 'normal 12px "HelveticaNeue"'
            ctx.setFillStyle('#4A4A4A')
            ctx.fillText(`¥${price}`, 40 + w1, 334)
            const w2 = ctx.measureText(`¥${price}`).width
            ctx.beginPath()
            ctx.moveTo(42 + w1, 330)
            ctx.lineTo(42 + w1 + w2, 330)
            ctx.stroke()
            ctx.closePath()
          }
          
          ctx.font = '12px PingFangSC-Regular,PingFang SC'
          ctx.setFillStyle('#4A4A4A')
          
          ctx.fillText(name.substring(0, 15), 24, 356, 152)
          ctx.fillText(name.substring(15, 30), 24, 372, 152)
          
          ctx.drawImage(qrcode, 205, 322, 67, 67);
          ctx.setFillStyle('#4A4A4A')
          ctx.fillText('长按识别', 216, 416, 67)
          
          ctx.save()
          
          const self = this
          ctx.draw(true, setTimeout((e) => {
            uni.canvasToTempFilePath({
              canvasId: 'canvas',
              success ({ tempFilePath }) {
                self.imgSrc = tempFilePath
              }
            }, self)
          }, 100))
        },
        open () {
          const { realPrice, price, name, image } = this.item
          this.$refs.popup.open()
          uni.showLoading()
          Promise.all([
            downloadFile({ url: image }), // 商品图
            downloadFile({ url: this.avatar || image }), // 用户头像
            // 从后台获取二维码
            downloadFile({ url: `someapi/getWxacode?page=${this.info.page}&scene=${encodeURIComponent(this.info.scene)}`, header: { 'Authorization': `Bearer ${uni.getStorageSync('token')}` } })
          ]).then(([
            { tempFilePath: itemImage },
            { tempFilePath: avatar },
            { tempFilePath: qrcode }
          ]) => {
            uni.hideLoading()
            this.draw({ realPrice, price, name, username: this.username, itemImage, avatar, qrcode })
          })
        },
        close () {
          this.$refs.popup.close()
          this.ctx.clearRect(0, 0, 300, 452)
        },
        save () {
          const self = this
          uni.showLoading()
          uni.saveImageToPhotosAlbum({
            filePath: this.imgSrc,
            success () {
              uni.showToast({ title: '保存成功' })
              self.close()
            },
            complete (res) {
              uni.hideLoading()
              console.log('complete', res)
            }
          })
        }
      },
      mounted () {
        this.ctx = uni.createCanvasContext('canvas', this)
      }
    }
    </script>
    

    这里我们主要是利用了canvas进行海报的绘制。二维码实现用户分销绑定的原理是二维码包含的跳转链接里有参数,在访问这些页面的时候,我们可以提前获取这些参数,完成绑定逻辑。

    在完成了这两个组件后,我们会在页面中这样使用:

    <template>
      <div>
        <!-- some other content -->
        <button @click="showShare">点击分享</button>
        <share-popup ref="share" @sharePoster="showPoster">
        </share-popup>
        <poster-item
          ref="poster"
          :item="{
            realPrice: product.realPrice,
            price: product.price,
            name: product.title,
            image: product.icons[0]
          }"
          :info="posterInfo">
        </poster-item>
      </div>
    </template>
    <script>
    export default {
      data () {
        return {
          product: {}
        }
      },
      computed: {
        query () {
          return {
            id: this.productId,
            memberId: this.$store.state.user.userInfo.id
          }
        },
        posterInfo () {
          return this.$message.makePoster(this.query)
        }
      },
      async onLoad(query){
        // query中包含我们二维码里的参数
        // 可以利用query里的值完成绑定
      },
      onShareAppMessage() {
        return {
          title: this.product.title,
          imageUrl: this.product.icons[0],
          path: this.$message.makeShare(this.query)
    	  }
      },
      methods: {
        showPoster () {
          this.$refs.poster.open()
        },
        showShare () {
          this.$refs.share.open()
        }
      }
    }
    </script>
    

    在页面使用的时候,我们其实做了两件事:

    • 页面加载的时候获取二维码里携带的信息
    • 将需要的信息传递给poster-item组件

    这里对信息的处理,我们用了$message方法做了个过滤,下面看看这个方法:

    const _dealPath = (path) => {
      if (path) {
        return path
      }
      const pages = getCurrentPages()
      console.log('_dealPath', pages[pages.length - 1].route)
      return pages[pages.length - 1].route
    }
    const message = {
      array: [],
      register ({ page, keys }) {
        this.array.push({ page, keys})
      },
      makeShare (params, path) {
        path = _dealPath(path)
        const index = this.array.findIndex(item => item.page === path)
        if (index > -1) {
          const { page, keys } = this.array[index]
          const query = keys.map(_key => {
            return `${_key}=${params[_key]}`
          }).join('&')
          console.log(`makeShare page: ${page}, query: ${query}`)
          return `/${page}?${query}`
        }
        return ''
      },
      makePoster (params, path) {
        path = _dealPath(path)
        const index = this.array.findIndex(item => item.page === path)
        if (index > -1) {
          const { page, keys } = this.array[index]
          const scene = keys.map(_key => {
            return params[_key]
          }).join('&')
          console.log(`makePoster page: ${page}, scene: ${scene}`)
          return { page, scene }
        }
        return null
      },
      resolveQuery (query) {
        const path = _dealPath()
        const index = this.array.findIndex(item => item.page === path)
        if (index > -1) {
          const { keys } = this.array[index]
          // 如果是海报
          if (query.scene) {
            const values = decodeURIComponent(query.scene).split('&')
            let res = {}
            keys.forEach((key, index) => {
              res[key] = values[index]
            })
            return res
          }
          // 如果是分享
          if (Object.keys(query).length === keys.length) {
            return query
          }
          return false
        }
        return false
      }
    }
    
    message.register({ page: 'pages/Search/detail', keys: ['id', 'memberId', 'timestamp'] })
    
    message.register({ page: 'pages/Home/index', keys: ['memberId'] })
    
    export default message
    

    这个方法的目的是指定不同的跳转页面生成二维码需要的参数,并进行拼接。使用这个方法的好处在于,以后我们可能会有很多页面需要有生成分享海报的功能,仅仅是每个页面上调用一个拼接参数的函数,会导致我们遗漏或多传递了参数,用这个函数进行过滤可以提前检测我们传递的参数是否正确。

    uniapp分包

    由于小程序有体积和资源加载限制,所以小程序平台提供了分包方式,优化小程序的下载和启动速度。

    所谓的主包,即放置默认启动页面/TabBar 页面,以及一些所有分包都需用到公共资源/JS 脚本;而分包则是根据pages.json的配置进行划分。

    在小程序启动时,默认会下载主包并启动主包内页面,当用户进入分包内某个页面时,会把对应分包自动下载下来,下载完成后再进行展示。此时终端界面会有等待提示。

    注意点:

    • subPackages 里的pages的路径是 root 下的相对路径,不是全路径。
    • 微信小程序每个分包的大小是2M,总体积一共不能超过16M。
    • 百度小程序每个分包的大小是2M,总体积一共不能超过8M。
    • 支付宝小程序每个分包的大小是2M,总体积一共不能超过4M。
    • QQ小程序每个分包的大小是2M,总体积一共不能超过24M。
    • 分包下支持独立的 static 目录,用来对静态资源进行分包。
    • 分包是按照分包的顺序进行打包的,所有的subpackages配置以外的文件路径,全部都被打包在主包(App)内。
    • subpackages无法嵌入另一个subpackages。
    • tabBar页面必须在App主包内。

    支持分包的目录结构:

    ┌─pages               
    │  ├─index
    │  │  └─index.vue    
    │  └─login
    │     └─login.vue    
    ├─pagesA   
    │  ├─static
    │  └─list
    │     └─list.vue 
    ├─pagesB    
    │  ├─static
    │  └─detail
    │     └─detail.vue  
    ├─static             
    ├─main.js       
    ├─App.vue          
    ├─manifest.json  
    └─pages.json 
    

    pages.json:

    {
      "pages": [{
        "path": "pages/index/index",
        "style": { ...}
      }, {
        "path": "pages/login/login",
        "style": { ...}
      }],
      "subPackages": [{
        "root": "pagesA",
        "pages": [{
          "path": "list/list",
          "style": { ...}
        }]
      }, {
        "root": "pagesB",
        "pages": [{
          "path": "detail/detail",
          "style": { ...}
        }]
      }],
      // 预加载
      "preloadRule": {
        "pagesA/list/list": {
          "network": "all",
          "packages": ["__APP__"]
        },
        "pagesB/detail/detail": {
          "network": "all",
          "packages": ["pagesA"]
        }
      }
    }
    

    起源地下载网 » 技术提炼|盘点那些Vue项目中的优秀实践-小程序篇

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元