相信大家对 Omi 应该都不陌生了,如果还有不了解的同学先看看这里。了解并使用 Omi 之后你会发现真的回不去了~~~
先简单说一下吧,Omi 就是一个可以快速开发项目的组件化框架,和 vue/react 一样为了节省生产力的。想了解 Omi 和 vue 还有 react 区别的,上面文档有讲解,或者加入群 256426170,可以面对面咨询 Omi 作者 dnt。我这篇文章将使用 Omi 从 0 到 1 来完成一个移动端的项目,让大家了解 Omi 开发的方便快捷。
开发准备:
这次我们挑选了一个日迹发现页来作为例子开发,如果有用手机QQ的同学,应该有知道“日迹”这个项目,这次我们就挑选了一个日迹的一个发现页,入口在手机QQ -> 动态 -> 日迹 -> 右上角发现
发现页如下
开发一个移动端页面和 PC 上开发是一样的,首先要分析页面划分模块,发现页很简单,可以看成一个列表,然后里面每一块是一个 item
如果不用组件化的话,ul+li 是不是就可以上手干了~但我们要告别原始社会的开发方式,采用 Omi 框架进行开发,下面就正式开始开发~
开发过程:
1/ 脚手架
开发一个项目 (一个页面也是一个项目),首先我们需要脚手架,脚手架可以从历史项目中复制过来,也可以自己重新搭建。使用 Omi 的话就方便很多啦,我们只需要下面两步
1 2 3 |
npm install omi-cli -g omi-cli init [project name] |
然后脚手架就 OK 了,下面简单的看一下脚手架,了解一下项目结构
下面那些.babelrc/.eslintrc/package.json 等就不说了
先看目录,config 是配置目录,里面有基础配置和项目配置,一般我们不需要修改
tools 里面是构建相关,webpack.base.js 是基础配置,然后测试环境和生产环境的区分就靠 script.js 了
src 是开发的目录,也是我们代码所在地,打开 src 再看一下
应该还是很好理解的,page 是页面,这里面每个目录就意味着有一个页面。页面的入口是目录下的 main
component 是组件,组件也是以文件夹为粒度来的,里面一定有一个 js 文件,然后组件相关的资源文件,样式文件也都放在 js 的同一目录下,比如这样
组件的图片/样式和 js 都有了,那外面的 css/img/js 呢?是一些全局资源和公共方法等,这样一来复用就极为方便了。
2/ 正式开发
首先我们引入一下 rem 统一的 js 代码,现在来说用 rem 还是比 px 方便很多的,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
;(function(win) { var doc = win.document; var docEl = doc.documentElement; var tid; function refreshRem() { var width = docEl.getBoundingClientRect().width; if (width > 540) { // 最大宽度 width = 540; } var rem = width / 10; // 将屏幕宽度分成10份, 1份为1rem docEl.style.fontSize = rem + 'px'; } win.addEventListener('resize', function() { clearTimeout(tid); tid = setTimeout(refreshRem, 300); }, false); win.addEventListener('pageshow', function(e) { if (e.persisted) { clearTimeout(tid); tid = setTimeout(refreshRem, 300); } }, false); refreshRem(); })(window); |
这样我们就将不同屏幕下的 rem 与 px 转换统一了,视觉稿上面的 px 单位除以 37.5 就可以了,这一步也可以在构建的时候做
接下来我们考虑到项目是一个长列表,说到长列表就肯定离不开滚动,说到滚动就想到了安卓下局部滚动会很卡。那么这里可以用全局滚动搞定么?可以的,因为页面本身不复杂。
那么复杂的情景下,必须是局部滚动的场景怎么办呢?AlloyTouch 欢迎你~解决各类滚动问题,而且有 Omi 插件的无缝支持版本。
准备工作都考虑完善之后我们就开始写第一个组件了!第一个组件可以看成是整个列表的一个包裹盒,盒子里面不仅有 list,还有按钮和一些其他的玩意
先上一下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
import List from '../list/index'; Omi.tag('List', List); class Main extends Omi.Component { constructor(data) { super(data); this.inTouch = false; this.touchXY = []; this.data.loadWord = '正在加载中...'; } style() { return ` .record { position: fixed; bottom: 0.533333rem; left: 50%; -webkit-transform: translateX(-50%); transform: translateX(-50%); background-image: url(${require('./img/record.png')}); width: 2.000000rem; height: 2.000000rem; background-size: 100% 100%; } .isend { position: relative; text-align: center; margin: 0 auto; margin-left: -12px; padding: 12px 0; font-size: 14px; color: rgba(119, 119, 119, 1); } `; } render() { return ` <div class="main"> <List omi-id="list"></List> <div class="record" ontouchmove="handleTouchMove(this, event)" ontouchstart="handleTouchStart(this, event)" ontouchend="handleTouchEnd(this, event)"></div> <div class="isend">${this.data.loadWord}</div> </div>`; } handleTouchMove(dom, e) { this.inTouch = false; } handleTouchStart(dom, e) { this.inTouch = true; this.touchXY[0] = e.touches[0].screenX; this.touchXY[1] = e.touches[0].screenY; } handleTouchEnd(dom, e) { console.log(e.changedTouches[0]); var diffX = Math.abs(e.changedTouches[0].screenX - this.touchXY[0]); var diffY = Math.abs(e.changedTouches[0].screenY - this.touchXY[1]); if(this.inTouch && diffX < 30 && diffY < 30) { // handle tap event.... this.inTouch = false; } e.preventDefault(); } } export default Main; |
超级简单明了,constructor 是组件的构造函数,也是生命周期的开始,因为我们包裹盒的组件一直存在,所以没有用上其他生命周期的方法。但 Omi 对组件生命周期的控制可是非常强大的,如下图
接着是 style 和 render,这里是用模版字符串写 css 和 html,很方便,但如果觉得麻烦也可以用文件的形式,后面会说
下面三个是啥呢?是自己模拟的 tap,因为移动端下 onclick 有 300ms 的延迟,所以我们用的点击都是模拟的。tap 用语言描述就是一次点击,我们要保证 touchend 时候手指的位置不能距离 touchstart 的位置太远,而且 end 和 start 期间不能触发 touchmove,这也就是自己实现 tap 的核心了。
如果有 zepto 的话本身可以用 ontap 事件,不必自己去写,但是我这里没有引入 zepto,而且 zepto 本身是 jquery 类似的写法,和框架开发还是比较背驰的。那么我们就只能自己写这么多代码去模拟么??
当然不是!因为我们有 alloyfinger-omi 版,我们只需要这样
安装:
1 2 |
npm install omi-finger |
使用:
1 2 3 |
import OmiFinger from 'omi-finger'; OmiFinger.init(); |
就可以了!alloytouch 里面的手势操作 omi-finger 都可以用,而且用起来也超级方便!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
...... render() { return ` <div class="main"> <List omi-id="list"></List> <div class="record" omi-finger tap="handleTap"></div> <div class="isend">${this.data.loadWord}</div> </div>`; } handleTap() { // handle tap event.... } ...... |
这样就可以了,这就是 Omi 插件体系的好处,顺带一提 alloytouch 也可以像 finger 这样使用~
这样最外层的包裹组件就已经 ok 了,我们来看核心的 list 组件。
再上代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
class List extends Omi.Component { constructor(data) { super(data); this.length = 0; this.data.leftList = []; this.data.rightList = []; } style() { return require('./_index.less'); } render() { return ` <div class="wrap clear" omi-finger tap="handleTap"> <div class="left"> ${ this.data.leftList.map((a, b) => `<Item data="data.leftList[${b}]"></Item>` ).join('') } </div> <div class="right"> ${ this.data.rightList.map((a, b) => `<Item data="data.rightList[${b}]"></Item>` ).join('') } </div> </div>`; } add(data) { for(let i = 0; i < data.length; i++) { // handle data if(i % 2 === 0) { this.data.leftList.push(info); } else { this.data.rightList.push(info); } } this.update(); } handleTap(e) { // handle tap; } reset() { this.data.leftList = []; this.data.rightList = []; } } |
首先可以看到和 main 不同的是,这里我们就把 css 给抽离成文件的形式了,纯看个人喜好。不过有一些需要注意的地方:
1. 全局 css 只需要在文件中 import 就可以了
2. 局部 css 或者 less 文件名必须以_开头,loader 会针对进行操作,就像上面的代码一样
3. html 抽离成文件的话需要用模版引擎的方式,上面代码用的是 ES6 模版字符串,这样的话是无法抽离成文件的。
omi.js 默认的模版引擎是 soda,如果还有喜欢 ejs、mustache 语法的同学,虽然 omi.js 本身没有内置该写法,但是用 omi.mustache.js 却将其默认为内置模版引擎
具体的情况如下:
- omi.js 使用 sodajs 为内置指令系统
- omi.art.js 使用 art-template 为内置模版引擎
- omi.lite.js 不包含任何模板引擎
- omi.mustache.js 使用 mustache.js 为内置模版引擎
接下来重点讲的就是其中的循环生成子组件部分。
循环渲染有多种方式,刚刚代码部分用的是 ES6 执行 map,然后获取到数组中每一个元素,渲染
我们也可以使用 omi 中内置的 soda 模版的指令方式,如下代码也可以实现同样的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 |
render() { return ` <div class="wrap clear" omi-finger tap="handleTap"> <div class="left"> <Item o-repeat="item in leftList" group-data="data.leftList"></Item> </div> <div class="right"> <Item o-repeat="item in rightList" group-data="data.rightList"></Item> </div> </div>`; } |
我们在 add 方法中进行数据的处理,这里组件的 data 下面有两个数组,分别是左右两边的。注意这里 add 方法最后有调用一个 update() 方法,omi 本身没有双向绑定,将更新的操作交给了开发者。当然如果希望双向绑定的话也可以引入 Mobx 之类的第三方库。
list 组件里面有一个 item 组件,这个 item 组件就是最后一个啦,它需要从 list 中接受到自己的数据,然后将数据给展示出来
数据传递的方式有很多种,简单的说一下
- on-* 代表传递父组件向子组件注入的回调函数,比 on-page-change="pageChangeHandler"
- data-* 代表直接传递字符串
- :data-* 代表传递 javascript 表达式,比如 data-num="1" 代表传递数字 1 而非字符串,data-num="1+1"可以传递 2。
- ::data-* 代表传递父组件的属性,比如上面的::data-items="data.items"就代表传递 this.data.items 给子组件
- data 代表传递父组件的属性,比如 data="user"代表传递 this.user 给子组件
- :data 代表传递 javascript 表达式,比如 data="{ name : 'dntzhang' , age : 18 }"代表传递 { name : 'dntzhang' , age : 18 } 给子组件
- group-data 代表传递父组件的数组一一映射到子组件
我们采用的是第 x 种,然后 item 中就是简单的展示啦
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
class Item extends Omi.Component { constructor(data) { super(data); console.log('data', data); } style() { return require('./_index.less'); } render() { return ` <div class="item"> <div class="card" vid="${this.data.vid}" shoot="${this.data.shoot}" uin="${this.data.uin}"> <div class="pic" style="background-image: url(${this.data.pic})"></div> <div class="txt"> <div class="head" style="background-image: url(${this.data.head})"></div> <div class="other"> <div class="nick" data-content='${this.data.nick}'>${this.data.nick}</div> <div class="info"> <span class="watch"><i></i>${this.data.watch}</span> <span class="like"><i></i>${this.data.like}</span> </div> </div> </div> </div> </div> `; } } export default Item; |
3/ 构建相关
开发过程中我们只需要 npm start,然后就可以专注的撸代码了
可以用默认的 localhost:9000 端口进行访问
也可以修改 config 目录下的 config.js 文件,用路由的方式访问,比如我这样
1 2 3 4 5 6 7 |
module.exports = { "webserver": "//xxx.qq.com/mobile/", "cdn": "", "port": "9000", "route": "/mobile/" }; |
当然我这里是有配置代理的,将 xxx.qq.com/mobile 指向了本地的 localhost:9000
当你开发完成后,只需要运行
1 2 |
**npm run dist** |
生产环境的代码就已经搞定了~接下来就是部署、提测...
结语
文章一些 cgi、util 相关的代码就省略掉了,主要目的是讲解 Omi 的开发。虽然是一个很小的页面,不过可以看出来用 omi+omi-cli 开发还是很简单的哈!Omi 的能力当然不止这一点点,我这篇文章只是抛砖引玉,大家想解放生产力的话, 快来使用 Omi 吧~~
在线体验地址,请使用手机 QQ 扫描下方二维码
github 地址:
有问题的话可以留言大家一起交流~