手记

【前端骚操作】Mithril开发Android上架Google Play

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"]
}

组件

一共三大组件:

  1. detail 为主页面,意为展示图片详情页面。
  2. drawer 为左侧的抽屉。
  3. 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 就不需要传递了。

你可以传数据,当然也可以传函数回调,这里建议:

  1. 单向数据流,子组件尽量不要直接修改父组件数据。
  2. 如果要修改,使用回调函数修改,即父子组件使用同一个方法修改相关数据。

搞完。

共耗时约 12 小时吧,后面第一次上 Google Play 花了 3 小时。一共 15 小时,不算整理资源。

Google Play 的审核效率是远远超过 App Store 的。3 小时包含审核不通过,审核下架(就是这个 APP 废了),重新发布审核通过一系列过程。对了,还有人工交流……

最终效果:

不要疑惑,我这当然兼容 PC 版了……那个保存因为是 alert,位置不对没有截图进来。

网页体验地址,推荐手机模式


感谢上次评论让我发个实战教程的同学,呐,请收下……


欢迎关注,如有需要 Web,App,小程序,请留言联系。

4人推荐
随时随地看视频
慕课网APP