QQ日跡Omi實戰開發,從0到1

當耐特發表於2017-07-17

寫在前面

相信大家對Omi應該都不陌生了,如果還有不瞭解的同學先看看這裡。瞭解並使用Omi之後你會發現真的回不去了~~~

先簡單說一下吧,Omi就是一個可以快速開發專案的元件化框架,和vue/react一樣為了節省生產力的。想了解Omi和vue還有react區別的,上面文件有講解,或者加入群256426170,可以面對面諮詢Omi作者dnt。我這篇文章將使用Omi從0到1來完成一個移動端的專案,讓大家瞭解Omi開發的方便快捷。


開發準備:

這次我們挑選了一個日跡發現頁來作為例子開發,如果有用手機QQ的同學,應該有知道“日跡”這個專案,這次我們就挑選了一個日跡的一個發現頁,入口在手機QQ -> 動態 -> 日跡 -> 右上角發現
發現頁如下

開發一個移動端頁面和PC上開發是一樣的,首先要分析頁面劃分模組,發現頁很簡單,可以看成一個列表,然後裡面每一塊是一個item
如果不用元件化的話,ul+li是不是就可以上手幹了~但我們要告別原始社會的開發方式,採用Omi框架進行開發,下面就正式開始開發~

開發過程:


1/ 腳手架

開發一個專案(一個頁面也是一個專案),首先我們需要腳手架,腳手架可以從歷史專案中複製過來,也可以自己重新搭建。使用Omi的話就方便很多啦,我們只需要下面兩步

    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方便很多的,程式碼如下:

    ;(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外掛的無縫支援版本。

使用omi-touch

準備工作都考慮完善之後我們就開始寫第一個元件了!第一個元件可以看成是整個列表的一個包裹盒,盒子裡面不僅有list,還有按鈕和一些其他的玩意
先上一下程式碼

    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版,我們只需要這樣

安裝:

npm install omi-finger複製程式碼

使用:

    import OmiFinger from 'omi-finger';
    OmiFinger.init();複製程式碼

使用omi-finger

就可以了!alloytouch裡面的手勢操作omi-finger都可以用,而且用起來也超級方便!

    ......
    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元件。
再上程式碼

    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就可以了

  1. 區域性css或者less檔名必須以_開頭,loader會針對進行操作,就像上面的程式碼一樣
  2. 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模版的指令方式,如下程式碼也可以實現同樣的功能

    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中就是簡單的展示啦

    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檔案,用路由的方式訪問,比如我這樣

    module.exports = {
        "webserver": "//xxx.qq.com/mobile/",
        "cdn": "",
        "port": "9000",
        "route": "/mobile/"
    };複製程式碼

當然我這裡是有配置代理的,將xxx.qq.com/mobile指向了本地的localhost:9000
當你開發完成後,只需要執行

**npm run dist**複製程式碼

生產環境的程式碼就已經搞定了~接下來就是部署、提測...

結語

文章一些cgi、util相關的程式碼就省略掉了,主要目的是講解Omi的開發。雖然是一個很小的頁面,不過可以看出來用omi+omi-cli開發還是很簡單的哈!Omi的能力當然不止這一點點,我這篇文章只是拋磚引玉,大家想解放生產力的話,快來使用Omi吧~~

線上體驗地址,請使用手機QQ掃描下方二維碼

github地址:

有問題的話可以留言大家一起交流~

相關文章