Mithril&HbuilderX 上架 Google Play
后端使用的是 LeanCloud,为了快速开发而使用云后台。原计划是 Bmob,不过有 bug,而且工单反应慢就放弃,而换了 LeanCloud。
Google Play 相关成果信息展示
Google Play 的开发者后台地址:Google Play Console
Google Play 开发者后台
Google Play 结果
正文部分
由于 Mithril 本身就是一个极其轻量的纯 JS 框架,所以没有使用任何第三方的 UI 框架,纯手工,配上打包最终 Apk 大小是 2.3M,可以说是非常的轻量了。
一个展示的 APP,一个上传图片的网页。
模块
上面 unpackage 是 HBuilderX 打包忽略,懒得处理就把 src 内容放到 unpackage 里面了,所以 unpackage 相当于 src 开发路径。
css 与 js 文件夹是生成最终文件,非开发路径。
static 为引用的第三方库,av 是 LeanCloud 库(这名字我喜欢);hammer 是手势库,APP还是有点手势的好;mithril 就不用说了。
使用了 yarn 包管理,webpack 打包 ES6,LESS 编译使用的是 VSCode 拓展:Easy LESS,所以总配置就 webpack.config.js 与 .babelrc,如下:
webpack.config.js
module.exports = {
entry: './unpackage/js/main.js',
output: {
path: __dirname + '/js',
filename: 'app.js',
},
module: {
rules: [
{
test: /.js$/,
use: 'babel-loader',
},
],
},
resolve: {
alias: {
'@': __dirname + '/unpackage/js'
}
}
}
.babelrc
{
"presets": ["@babel/preset-env"]
}
组件
一共三大组件:
- detail 为主页面,意为展示图片详情页面。
- drawer 为左侧的抽屉。
- store 为数据控制中心,用来处理跨组件交互(项目还小,目前没怎么用到)。
toast 为兼容网页与 APP 的弹出对话组件,main 是项目入口文件。
可公开代码
main
main 入口只有一个 Detail,目前没有用到路由。
import Detail from './pages/detail'
m.mount(document.body, {
view() {
return [m(Detail)]
},
})
上面使用数组是为了后续拓展。
store
store 里面寄存一个单例,数据初始化在具体组件里面。
const STORE = Symbol('foo')
const store = {}
if (!global[STORE]) {
AV.init('xxx', 'yyy')
global[STORE] = store
}
export default global[STORE]
toast
调用原生的 toast,如果是网页就使用 alert。
//自定义弹框
export default msg => {
try {
plus.nativeUI.toast(msg)
} catch (error) {
alert(msg)
}
}
try 原则上是不提倡用的,但是用起来方便,如果使用 webpack 打包,效率损失是可忽视的,这是经过本人亲自测试的,如果是纯原生为经过 webpack 打包,就不要乱用了。
detail
import store from '@/store'
import Drawer from '@/components/drawer'
import Toast from '@/components/toast'
// 这里初始化 store 对应数据
let { list, classes } = (store.detail = {
list: [], // 返回图片结果列表
imgTitle: 'no img',
currentIndex: 0,
max: 10,
drawerShow: false,
classes: ['Asuna'], // 所有类型
selectedClass: '',
})
const state = store.detail
let currentUrl = '#'
// 获取抽屉列表数据
const getClasses = () => {
const query = new AV.Query('CLASSES')
query.find().then(res => {
classes = Array.from(res, item => item.get('className'))
try {
getList(classes[0])
} catch (error) {
console.log(error)
}
})
}
// 获取图片列表数据
const getList = className => {
state.selectedClass = className
if (state.selectedClass === '') return
list = [] // 先清空
state.currentIndex = 0 // 索引放到第一个
const query = new AV.Query(state.selectedClass)
query.addAscending('title')
query.find().then(res => {
list = res
state.max = res.length - 1
// 第三方,所以无法被动触发,需要手动
m.redraw()
})
}
// 子组件的回调
const changeSelectedClass = index => getList(classes[index])
// 索引加减
const cup = d => {
state.currentIndex += d
// 索引越界处理
if (state.currentIndex < 0) {
state.currentIndex = 0
Toast('本组第一张了。')
} else if (state.currentIndex > state.max) {
state.currentIndex = state.max
Toast('没有更多了。')
}
// 第三方,所以无法被动触发,需要手动
m.redraw()
}
// 子组件回调隐藏抽屉
const hiddenDrawer = () => (state.drawerShow = false)
// 图片组件,因为有 DOM 操作所以单独抽出来。
const Img = {
oncreate(v) {
// DOM 操作
const hammertime = new Hammer(v.dom, { domEvents: true })
hammertime.on('press', this.clickImg.bind(this))
hammertime.on('swipeleft', cup.bind(this, 1))
hammertime.on('swiperight', cup.bind(this, -1))
},
view(v) {
const { url } = v.attrs
currentUrl = url
return m("img.img[alt='img']", {
src: url,
})
},
clickImg() {
if (confirm('确定下载吗?')) {
console.log(currentUrl)
try {
plus.gallery.save(currentUrl, function() {
alert('保存图片到相册成功')
})
} catch (error) {
alert('PC没有保存相册功能')
}
}
},
}
// detail 组件
export default {
oncreate(v) {
getClasses()
},
view() {
const title =
list[state.currentIndex] && list[state.currentIndex].get('title')
const url = list[state.currentIndex] && list[state.currentIndex].get('url')
return m('.detail.page', [
m('header', [
m('button', { : () => (state.drawerShow = true) }, '更多'),
m('h1', title || state.imgTitle),
m('button', { : Img.clickImg }, '保存'),
]),
m('main', [m(Img, { url })]),
m('footer', [
m(
'button',
{
: () => state.currentIndex--,
disabled: state.currentIndex < 1,
},
'上一张'
),
m(
'button',
{
: () => state.currentIndex++,
disabled: state.currentIndex >= state.max,
},
'下一张'
),
]),
// 子组件通信
m(Drawer, {
drawerShow: state.drawerShow,
changeSelectedClass,
classes,
hiddenDrawer,
}),
])
},
}
关键注释已经添加,这里主要说一下 m.redraw() 的使用。
一般来说,不需要主动触发这个,但是当你的回调或者 state 数据变动被绑定在了第三方这种情况下,修改是不会触发 diff 树变动的,所以需要 m.redraw() 来手动处理。
drawer
import store from '../store'
const state = (store.drawer = {
selectedIndex: 0,
})
export default {
view(v) {
const { hiddenDrawer, drawerShow, changeSelectedClass, classes } = v.attrs
return m('.drawer.page', { class: drawerShow && 'drawer-show' }, [
m('.shadow-back', { : hiddenDrawer }, [
m('.content', [
// m('img.logo'),
m(
'ul',
classes.map((className, index) =>
m(
'li',
{
class: index === state.selectedIndex && 'selected',
: () => {
state.selectedIndex = index
changeSelectedClass(index)
},
},
className
)
)
),
]),
]),
])
},
}
drawer 就相对简单,主要是通过 v.attrs.x 来获取父组件传来的数据,当然,store 就不需要传递了。
你可以传数据,当然也可以传函数回调,这里建议:
- 单向数据流,子组件尽量不要直接修改父组件数据。
- 如果要修改,使用回调函数修改,即父子组件使用同一个方法修改相关数据。
搞完。
共耗时约 12 小时吧,后面第一次上 Google Play 花了 3 小时。一共 15 小时,不算整理资源。
Google Play 的审核效率是远远超过 App Store 的。3 小时包含审核不通过,审核下架(就是这个 APP 废了),重新发布审核通过一系列过程。对了,还有人工交流……
最终效果:
不要疑惑,我这当然兼容 PC 版了……那个保存因为是 alert,位置不对没有截图进来。
网页体验地址,推荐手机模式
感谢上次评论让我发个实战教程的同学,呐,请收下……
欢迎关注,如有需要 Web,App,小程序,请留言联系。