基于 Vue-cli 项目打包为移动端app

前言

我写操作喜欢写全,主要是自己懂得不多,相关方面的知识不能融会贯通,知识壁垒很多,网上七拼八凑看的教程,现在整理成一份完整的攻略以备后用。

打包的代码比较简单,没有复杂功能,没有和后端交互,是我写的方格游戏移动版。主要目的是想体验一下 vue 转 app 的操作过程。

感兴趣的可以看下游戏的PC版,移动版也有,但我朋友说她手机上玩不了,哈哈,我的手机能玩啊。

方格游戏

实践参考《基于Vue的项目打包为移动端app》

https://baijiahao.baidu.com/s?id=1655878004078867586&wfr=spider&for=pc

项目准备

使用vue-cli开始一个Vue新项目

使用vue-cli脚手架,开始一个新项目。详情在我的另一篇博客里有,这里不再赘述。

项目目录

之前做项目练习的时候,没想过打包,静态资源都是随便放。现在需要打包了,静态资源需要放在static下面,包括图片,js 等。

indexPage.vue

<template>
  <div id="root">
    <div class="head" v-show="!showControl">
      <div v-show="!showTime" class="game_start_box">
        <div class="game_time_box">倒计时设置(10 ~ 60s):<input type="number" v-model="userSetGameTime" min="10" max="60" /></div>
        <div class="game_start_btn" @click="game_start">开始游戏</div>
      </div>

      <div v-show="showTime">倒计时<span>{ {gameTime}}</span>秒 </div>
    </div>

    <div class="container">
      <div class="chess" v-show="!showControl">
        <div class="chess-grid">
          <div ref="snowman" class="snowman" :style="moveStyle" title="按键盘方向键移动我哦" @touchend="dealTouchEvent($event)"></div>
        </div>
        <div class="chess-grid" v-for="(item,index) in chess" :key="index" :style="{background:item.background}">{ {item.usedScore == 0 ? item.usedScore : item.score}}</div>
        <div class="chess-grid">
          <div class="home"></div>
        </div>
      </div>

      <div class="score" v-show="showControl">
        <div class="title">战绩</div>
        <p v-for="(score,index) in gameScores" :key="index">第{ {index+1}}战得分<span>{ {score}}</span>分</p>
      </div>
    </div>

    <div class="control" v-show="showControl">
      <div class="btn auto" @click="game_review">本局复盘</div>
      <div class="btn refresh" @click="game_update">再来一局</div>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'indexPage',
    data() {
      return {
        EventUtil: "",
        timer1: "",
        touchingScreen: false, //是否正在触屏 限制 touch 触发频率
        showControl: false, //是否显示本局复盘、再来一局
        showTime: false, //是否显示倒计时 不显示倒计时的时候会显示开始游戏按钮
        is_get_home: false,
        gameTime: 30, //系统设置的游戏时间 30秒一局游戏
        userSetGameTime: 30, //用户设置的游戏时间
        gameScores: [], //单机多人游戏 游戏复盘,存储历史得分
        currentScore: 0, //当前一局游戏的得分
        moveDown: 0,
        moveRitht: 0,
        chess: [], //存储随机的分数和背景色数组
        moveStyle: {
          transform: "translate(0,0)"
        }, //偏移样式
        unit: 0, //记量单位 移动端 移动的距离不再是固定的
        isDictionaryUpdate: false, //字典中一部分数据只用更新一次,因此设置flag,isDictionaryUpdate 是否已经同步更新数据,默认否。
        dictionary: [ //棋盘各块偏移量对照字典 最后一格为终点格
          {
            "score": 0,
            " i": 0,
            "r": 1,
            "d": 0
          },
          {
            "score": 0,
            "i": 1,
            "r": 2,
            "d": 0
          },
          {
            "score": 0,
            "i": 2,
            "r": 3,
            "d": 0
          },
          {
            "score": 0,
            "i": 3,
            "r": 4,
            "d": 0
          },
          {
            "score": 0,
            "i": 4,
            "r": 5,
            "d": 0
          },
          {
            "score": 0,
            "i": 5,
            "r": 6,
            "d": 0
          },
          {
            "score": 0,
            "i": 6,
            "r": 0,
            "d": 1
          },
          {
            "score": 0,
            "i": 7,
            "r": 1,
            "d": 1
          },
          {
            "score": 0,
            "i": 8,
            "r": 2,
            "d": 1
          },
          {
            "score": 0,
            "i": 9,
            "r": 3,
            "d": 1
          },
          {
            "score": 0,
            "i": 10,
            "r": 4,
            "d": 1
          },
          {
            "score": 0,
            "i": 11,
            "r": 5,
            "d": 1
          },
          {
            "score": 0,
            "i": 12,
            "r": 6,
            "d": 1
          },
          {
            "score": 0,
            "i": 13,
            "r": 0,
            "d": 2
          },
          {
            "score": 0,
            "i": 14,
            "r": 1,
            "d": 2
          },
          {
            "score": 0,
            "i": 15,
            "r": 2,
            "d": 2
          },
          {
            "score": 0,
            "i": 16,
            "r": 3,
            "d": 2
          },
          {
            "score": 0,
            "i": 17,
            "r": 4,
            "d": 2
          },
          {
            "score": 0,
            "i": 18,
            "r": 5,
            "d": 2
          },
          {
            "score": 0,
            "i": 19,
            "r": 6,
            "d": 2
          },
          {
            "score": 0,
            "i": 20,
            "r": 0,
            "d": 3
          },
          {
            "score": 0,
            "i": 21,
            "r": 1,
            "d": 3
          },
          {
            "score": 0,
            "i": 22,
            "r": 2,
            "d": 3
          },
          {
            "score": 0,
            "i": 23,
            "r": 3,
            "d": 3
          },
          {
            "score": 0,
            "i": 24,
            "r": 4,
            "d": 3
          },
          {
            "score": 0,
            "i": 25,
            "r": 5,
            "d": 3
          },
          {
            "score": 0,
            "i": 26,
            "r": 6,
            "d": 3
          },
          {
            "score": 0,
            "i": 27,
            "r": 0,
            "d": 4
          },
          {
            "score": 0,
            "i": 28,
            "r": 1,
            "d": 4
          },
          {
            "score": 0,
            "i": 29,
            "r": 2,
            "d": 4
          },
          {
            "score": 0,
            "i": 30,
            "r": 3,
            "d": 4
          },
          {
            "score": 0,
            "i": 31,
            "r": 4,
            "d": 4
          },
          {
            "score": 0,
            "i": 32,
            "r": 5,
            "d": 4
          },
          {
            "score": 0,
            "i": 33,
            "r": 6,
            "d": 4
          },
          {
            "score": 0,
            "i": 34,
            "r": 0,
            "d": 5
          },
          {
            "score": 0,
            "i": 35,
            "r": 1,
            "d": 5
          },
          {
            "score": 0,
            "i": 36,
            "r": 2,
            "d": 5
          },
          {
            "score": 0,
            "i": 37,
            "r": 3,
            "d": 5
          },
          {
            "score": 0,
            "i": 38,
            "r": 4,
            "d": 5
          },
          {
            "score": 0,
            "i": 39,
            "r": 5,
            "d": 5
          },
          {
            "score": 0,
            "i": 40,
            "r": 6,
            "d": 5
          },
          {
            "score": 0,
            "i": 41,
            "r": 0,
            "d": 6
          },
          {
            "score": 0,
            "i": 42,
            "r": 1,
            "d": 6
          },
          {
            "score": 0,
            "i": 43,
            "r": 2,
            "d": 6
          },
          {
            "score": 0,
            "i": 44,
            "r": 3,
            "d": 6
          },
          {
            "score": 0,
            "i": 45,
            "r": 4,
            "d": 6
          },
          {
            "score": 0,
            "i": 46,
            "r": 5,
            "d": 6
          },
          {
            "score": 0,
            "i": 47,
            "r": 6,
            "d": 6
          }
        ]
      }
    },
    methods: {
      //生成随机分数棋格
      createChess: function() {
        //7*7方格,掐头去尾,需要生成47个随机数。
        var score;
        var bgColor;
        if (this.isDictionaryUpdate) {
          for (var i = 0; i < 47; i++) {
            // 按奇数偶数对应正负分值
            if (i % 2 == 0) {
              //正数 加分
              score = Math.round(Math.random() * 10) + 2;
              bgColor = "#16a05d";
            } else {
              //负数 减分
              score = -Math.round(Math.random() * 6) - 1;
              bgColor = "#e21918";
            }
            this.chess.push({
              "score": score,
              "usedScore": 100, //随便指定一个现今规则不可能有的一个分数即可
              "background": bgColor
            });
            //同步更新对照字典,存储分值。
            this.dictionary[i].score = score;
          }
        } else {
          for (var i = 0; i < 47; i++) {
            // 按奇数偶数对应正负分值
            if (i % 2 == 0) {
              //正数 加分
              score = Math.round(Math.random() * 10) + 2;
              bgColor = "#16a05d";
            } else {
              //负数 减分
              score = -Math.round(Math.random() * 6) - 1;
              bgColor = "#e21918";
            }
            this.chess.push({
              "score": score,
              "usedScore": 100, //随便指定一个现今规则不可能有的一个分数即可
              "background": bgColor
            });
            //同步更新对照字典,存储分值。
            this.dictionary[i].score = score;
            //同步更新对照字典,计算准确坐标偏移量
            this.dictionary[i].r = this.dictionary[i].r * this.unit;
            this.dictionary[i].d = this.dictionary[i].d * this.unit;
          }
          this.isDictionaryUpdate = true;
        }

      },
      //处理手机触屏事件
      dealTouchEvent: function(e) {
        e || event;
        this.touchingScreen = true;

        //使用的时候很简单,只需要像下面这样调用即可 up, right, down, left为四个回调函数,分别处理上下左右的滑动事件
        this.EventUtil.listenTouchDirection(e.target, true, this.upCallback, this.rightCallback, this.downCallback,
          this.leftCallback);
      },

      //touch的回调事件
      upCallback: function() {
        //当游戏倒计时显示时,即游戏还未结束,才能触发键盘事件,开始移动。
        if (this.showTime && this.touchingScreen) {
          this.touchingScreen = false; //触屏只触发一次
          //向上移动
          this.moveDown -= this.unit;
          this.moveDown < 0 ? this.moveDown = 0 : this.moveDown;
          this.moveStyle.transform = "translate(" + this.moveRitht + "px," + this.moveDown + "px)";
          //根据偏移的位置,统计得分。
          this.countScore(this.moveRitht, this.moveDown);
        }
      },
      rightCallback: function() {
        if (this.showTime && this.touchingScreen) {
          this.touchingScreen = false; //触屏只触发一次
          //向右移动
          this.moveRitht += this.unit;
          //判断界限值 不能超出棋盘活动
          this.moveRitht > 6 * this.unit ? this.moveRitht = 6 * this.unit : this.moveRitht;
          this.moveStyle.transform = "translate(" + this.moveRitht + "px," + this.moveDown + "px)";
          //根据偏移的位置,统计得分。
          this.countScore(this.moveRitht, this.moveDown);
        }
      },
      downCallback: function() {
        if (this.showTime && this.touchingScreen) {
          this.touchingScreen = false; //触屏只触发一次
          //向下移动
          this.moveDown += this.unit;
          this.moveDown > 6 * this.unit ? this.moveDown = 6 * this.unit : this.moveDown;
          this.moveStyle.transform = "translate(" + this.moveRitht + "px," + this.moveDown + "px)";
          //根据偏移的位置,统计得分。
          this.countScore(this.moveRitht, this.moveDown);
        }
      },
      leftCallback: function() {
        if (this.showTime && this.touchingScreen) {
          this.touchingScreen = false; //触屏只触发一次
          //向左移动
          this.moveRitht -= this.unit;
          //判断界限值 不能超出棋盘活动
          this.moveRitht < 0 ? this.moveRitht = 0 : this.moveRitht;
          this.moveStyle.transform = "translate(" + this.moveRitht + "px," + this.moveDown + "px)";
          //根据偏移的位置,统计得分。
          this.countScore(this.moveRitht, this.moveDown);
        }
      },

      //计算得分
      countScore: function(r, d) {
        //遍历偏移量字典,根据当前所在的位置,获取对应的分值。
        //偏移量字典(len=48)比棋格(len=47)多了一个终点的位置信息。
        if (!(Math.abs(r - this.unit * 6) < 7 && Math.abs(d - this.unit * 6) < 7)) {
          //不在家,赋值false 防止回家后再离开的情形
          this.is_get_home = false;
          for (var i = 0; i < 48; i++) {
            //因为浏览器将数值取整后返回,所以,与实际值差值小于1.累积偏移差值小于1*7. Math.abs 取绝对值.
            if (Math.abs(r - this.dictionary[i].r) < 7 && Math.abs(d - this.dictionary[i].d) < 7) {
              //游戏开始时,小女孩正常移动后再回到起点会报这个错误
              //undo: this.chess[i].usedScore  有时会报错 Cannot read property 'usedScore' of undefined
              if (this.chess[i].usedScore == 100) {
                this.currentScore += this.dictionary[i].score;
                //分数一次性有效 走过的分数变为0.
                //为了复盘,不直接改变分数,新分数存储到 usedScore
                this.chess[i].usedScore = 0;
              }
            }
          }
        } else {
          //雪人到家
          this.is_get_home = true;
        }

      },

      //计时器,每次时间-1,时间单位秒。
      timer: function() {
        this.gameTime -= 1
      },

      //用户点击游戏开始 创建定时器 显示倒计时
      game_start: function() {
        if (this.gameTime != this.userSetGameTime) {
          //系统设置的游戏时长和用户设置的游戏时长冲突,则使用用户设置的时长
          this.gameTime = this.userSetGameTime;
        }
        this.showTime = true;
        this.timer1 = setInterval(this.timer, 1000);
      },

      game_review: function() {
        this.parameter_reset();
        this.resetUsedScore();
      },

      //恢复棋盘 使用过的分数初始化
      resetUsedScore: function() {
        var len = 47;
        while (len--) {
          this.chess[len].usedScore = 100;
        }
      },

      //本局重玩,只需要重置参数。
      parameter_reset: function() {
        this.showControl = false;
        this.is_get_home = false;
        this.showTime = false; //先不显示倒计时,显示开始游戏按钮。
        this.moveRitht = 0;
        this.moveDown = 0;
        this.moveStyle.transform = "translate(0,0)";
        this.currentScore = 0;
      },

      //页面初始化 游戏重新开始
      game_update: function() {
        this.parameter_reset();
        this.gameScores = [];
        this.chess = [];
        this.createChess();
      }
    },
    watch: { //监测游戏时间
      gameTime() {
        if (this.gameTime == 0) {
          clearInterval(this.timer1);
          this.showControl = true;
          this.showTime = false; //倒计时结束,关闭倒计时结果显示
          if (this.is_get_home) {
            //游戏结束:倒计时结束,雪人进入小屋。当前得分计入。
            this.gameScores.push(this.currentScore);
          } else {
            //游戏失败: 倒计时结束,但雪人未进入小屋。本局得分为0。
            this.currentScore = 0;
            this.gameScores.push(0);
          }
        }
      }
    },
    mounted() {
      //计量单位等于小方格的宽或高
      //获取的高度值约等于实际值,存在差值。获取的值取了实际值的近似整数。
      this.unit = this.$refs.snowman.offsetHeight;
      this.game_update();

      this.EventUtil = {
        addHandler: function(element, type, handler) {
          if (element.addEventListener)
            element.addEventListener(type, handler, false);
          else if (element.attachEvent)
            element.attachEvent("on" + type, handler);
          else
            element["on" + type] = handler;
        },
        removeHandler: function(element, type, handler) {
          if (element.removeEventListener)
            element.removeEventListener(type, handler, false);
          else if (element.detachEvent)
            element.detachEvent("on" + type, handler);
          else
            element["on" + type] = handler;
        },
        /**
         * 监听触屏的方向
         * @param target            要绑定监听的目标元素
         * @param isPreventDefault  是否屏蔽掉触屏滑动的默认行为(例如页面的上下滚动,缩放等)
         * @param upCallback        向上滑动的监听回调(若不关心,可以不传,或传false)
         * @param rightCallback     向右滑动的监听回调(若不关心,可以不传,或传false)
         * @param downCallback      向下滑动的监听回调(若不关心,可以不传,或传false)
         * @param leftCallback      向左滑动的监听回调(若不关心,可以不传,或传false)
         */
        listenTouchDirection: function(target, isPreventDefault, upCallback, rightCallback, downCallback,
          leftCallback) {
          this.addHandler(target, "touchstart", handleTouchEvent);
          this.addHandler(target, "touchend", handleTouchEvent);
          this.addHandler(target, "touchmove", handleTouchEvent);
          var startX;
          var startY;

          function handleTouchEvent(event) {
            switch (event.type) {
              case "touchstart":
                startX = event.touches[0].pageX;
                startY = event.touches[0].pageY;
                break;
              case "touchend":
                var spanX = event.changedTouches[0].pageX - startX;
                var spanY = event.changedTouches[0].pageY - startY;

                if (Math.abs(spanX) > Math.abs(spanY)) { //认定为水平方向滑动
                  if (spanX > 30) { //向右
                    if (rightCallback)
                      rightCallback();
                  } else if (spanX < -30) { //向左
                    if (leftCallback)
                      leftCallback();
                  }
                } else { //认定为垂直方向滑动
                  if (spanY > 30) { //向下
                    if (downCallback)
                      downCallback();
                  } else if (spanY < -30) { //向上
                    if (upCallback)
                      upCallback();
                  }
                }

                break;
              case "touchmove":
                //阻止默认行为
                if (isPreventDefault)
                  event.preventDefault();
                break;
            }
          }
        }
      };
    }
  }
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
  body {
    width: 100%;
    height: 100%;
    margin: 0;
    background: #fff;
    overflow: hidden;
    box-sizing: border-box;
    color: #1a2a65;
  }

  div.container {
    width: 100%;
    margin-top: 80px;
    text-align: center;
  }

  div.container div.chess {
    width: 352px;
    margin: 0 auto;
    box-sizing: border-box;
    border: 1px solid #ccc;
    display: flex;
    justify-content: flex-start;
    flex-wrap: wrap;
  }

  div.chess-grid {
    /* 棋子使用百分比宽度和高度,浏览器返回近似整数值和字典中设置的偏移量不完全相等,会产生UI上的偏移,影响美观。 */
    width: 50px;
    height: 0;
    box-sizing: border-box;
    padding-bottom: 50px;
    /* 让div的高等于宽 */
    line-height: 50px;
    text-align: center;
    color: #fff;
    /*应用 CSS 属性 touch-action: none; 这样任何触屏事件都不会产生默认行为,但是 touch 事件照样触发,从而解决无法被动侦听事件preventDefault*/
    touch-action: none;
  }

  /*雪人*/
  div.snowman {
    width: 100%;
    padding-bottom: 100%;
    /* 让div的高等于宽 */
    box-sizing: border-box;
    background: url("../../static/img/head.jpg") no-repeat;
    background-size: 100% 100%;
    cursor: pointer;
  }

  /*雪人的家*/
  div.home {
    width: 100%;
    padding-bottom: 100%;
    /* 让div的高等于宽 */
    box-sizing: border-box;
    background: url("../../static/img/home.jpg") no-repeat;
    background-size: 100% 100%;
  }

  div.container div.score {
    width: 98%;
    margin: 0 auto;
    box-sizing: border-box;
    border: 1px solid red;
    background: #f7f7f7;
    text-align: center;
  }

  div.container div.score .title {
    font-size: 18px;
    background: red;
    color: #fff;
    padding: 10px;
  }

  /*游戏计时器*/
  div.head {
    width: 100%;
    height: 64px;
    box-sizing: border-box;
    text-align: center;
    position: fixed;
    top: 0;
    background: #f7f7f7;
    padding: 12px;
  }

  div.head span,
  div.score span {
    font-size: 24px;
    font-weight: bold;
    color: red;
    padding: 0 4px
  }

  div.game_start_box {
    display: flex;
    justify-content: center;
  }

  div.game_time_box {
    margin-right: 20px;
    line-height: 38px;
  }

  div.game_time_box input {
    height: 30px;
    width: 40px;
    box-sizing: border-box;
  }

  div.game_start_btn {
    width: 100px;
    height: 40px;
    line-height: 40px;
    box-sizing: border-box;
    border: 1px solid #ccc;
    color: #fff;
    cursor: pointer;
    background: green;
  }

  /*控制器*/
  div.control {
    width: 100%;
    height: 60px;
    line-height: 60px;
    text-align: center;
    display: flex;
    justify-content: center;
    position: fixed;
    bottom: 0;
    background: #f7f7f7;
  }

  div.btn {
    width: 200px;
    height: 40px;
    line-height: 40px;
    box-sizing: border-box;
    border: 1px solid #ccc;
    margin: 10px;
    color: #fff;
    cursor: pointer;
  }

  div.auto {
    background: green;
  }

  div.refresh {
    background: red;
  }
</style>
 

config下面的index.js

尤其要注意 assetsPublicPath: './'

项目打包

打包前

在项目的根目录新建一个文件 vue.confing.js

这里 webpack配置 我不是很明白,先记录一下。

文件内容

module.exports = {
  publicPath: './'
}

执行打包

上面提到的参考攻略里使用的是 yarn build ,我用的是 npm run build.

在项目的根目录里,输入 npm run build 

 

打包完成 

打包完成后,会在项目根目录自动生成打包好的文件。

如果文件有修改需要重新打包,直接把dist文件夹删除就行。然后在命令窗口重新执行 npm run build

我最开始 touch.js 放错位置了,导致打出的包里缺少该文件。

后来包里有文件,双击包里的index.html文件也能看到页面,但开始游戏的时候报自定义js中的一个方法找不到。

EventUtil is not defined

EventUtil 这个方法,我在touch.js 中有定义。

仔细看,发现是文件引入方法错了,在 indexPage.vue 文件里还在用 <script> 标签。

发现后改为 import {EventUtil } from xx 

touch.js 中也使用了 export {EventUtil } 抛出

始终无法生效,再三确认和查过资料,语法没有问题。

查资料有说需要在 main.js 中引入 ,我还试了在 App.vue 中,都没有成功。

解决不了先放弃,简单粗暴,直接把自定义 js 代码放入 indexPage.vue ,完美解决问题。

 

发包

开发工具准备

下载 HBuilder

https://www.dcloud.io/

这个编辑器真的挺好用的,尤其是对前端开发者来说,是为vue专门设计的。我觉得很好用,还赞助了10块钱支持呢,哈哈。

新建项目

在 HBuilder 中新建项目。

将原来打包好的项目,文件转移到新建的空项目中,所有文件,选择覆盖。

 

jre环境准备

jre 指 Java运行环境(Java Runtime Environment,简称JRE)

这有一篇特别好的文章,我就不多说了。

Windows环境下安装JDK、JRE和环境变量配置,详细的图文教程

就补充一下测试是否安装吧

在命令行中分别输入

java

javac

java -version

出现这种情况,先检查是否有安装 jre 或 jdk

echo %PATH%

echo %JAVA_HOME% 

有的话再检查环境变量是否忘记配了,检查了一下,果然没有配。具体的配置,引用的文章里写的很详细,我就是根据大神的指导完成的配置。

证书准备

操作不难,就自己生成一个证书,不要使用公共的。

Android平台签名证书(.keystore)生成指南

 

发行准备

在 DCloud 开发者中心使用 HBuilder 账号登录,创建一个新应用获取应用标识 AppID

https://dev.dcloud.net.cn/

打开 manifest.json 进行配置。

图标配置就选择一张图,然后选自动生成吧。 

 

开始打包

顶部工具栏,选择发行=》原生App云打包=>然后选择打 Android 的包。

打包成功之后,会返回apk下载文件的下载链接。

安卓平板下载apk文件后安装运行效果 

猜你喜欢

转载自blog.csdn.net/Irene1991/article/details/105391555