前言
我写操作喜欢写全,主要是自己懂得不多,相关方面的知识不能融会贯通,知识壁垒很多,网上七拼八凑看的教程,现在整理成一份完整的攻略以备后用。
打包的代码比较简单,没有复杂功能,没有和后端交互,是我写的方格游戏移动版。主要目的是想体验一下 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
这个编辑器真的挺好用的,尤其是对前端开发者来说,是为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
打开 manifest.json 进行配置。
图标配置就选择一张图,然后选自动生成吧。
开始打包
顶部工具栏,选择发行=》原生App云打包=>然后选择打 Android 的包。
打包成功之后,会返回apk下载文件的下载链接。
安卓平板下载apk文件后安装运行效果