一站式前端開發框架Snowball介紹

Snowball

  • snowball 是一個一站式前端開發框架,你可使用snowball輕鬆構建出一套web app/hybrid appsnowball內置了view層,但同時也支持React。它比React全家桶輕量又支持更多功能,以下:
  • 路由系統:擁有多工程跨工程加載、頁面切換前進後退動畫效果、手勢返回、動態管理DOM等功能。
  • 狀態管理:immutable、響應式,和redux不一樣,snowball的狀態管理更符合OOP思想。
  • 視圖:fiber模式渲染,高性能,雙向綁定。 採用運行時模版編譯,在須要從服務端拉取模版渲染的場景優於ReactVueAngular等框架。
  • 路由系統和狀態管理都徹底適配React
  • 業務項目採用分層架構,主要分爲ControllerServiceView層,Controller層用來組織Service層,並經過injectable註解將數據注入到View層。
  • 項目地址:github.com/sorrymeika/…

路由

該路由方案專爲多團隊協做開發設計,將多個庫整合成一個單頁應用,讓全部業務都使用相同的跳轉動畫、手勢返回、頁面緩存。
發佈後到業務庫共用一份核心庫的js/css/image/iconfont,減小下載資源的大小。
一個核心框架庫+多個業務庫。業務庫之間不依賴,可單獨發佈。
複製代碼

多工程跨工程加載

  1. 核心框架 snowball 統一控制路由,須要在 snowball 中註冊須要加載的業務
  2. 業務庫打包後會生成asset-manifest.json文件,snowball 經過路由匹配到業務,並加載manifest中的js和css。
  3. 業務js加載時調用registerRoutes({...}) 方法註冊子路由
  4. snowball 在業務js/css加載完成後,根據業務註冊的子路由跳至對應頁面。

跳轉動畫和手勢返回

  1. 應用啓動後,可以使用 navigation.forwardnavigation.back 方法來控制頁面跳轉的動畫效果。使用 navigation.forward 跳轉頁面後,點擊瀏覽器返回上一頁會自帶返回動畫。若無需跳轉動畫可以使用 navigation.transitionTo 方法。
  2. 應用默認開啓手勢返回功能,navigation.forward 跳轉到新頁面以後,左滑頁面可返回上一頁。
  3. 頁面render時會監聽dom數量,若dom數量超過指定數量(默認20k),會自動umount老頁面的dom。

視圖和狀態管理

snowball的視圖層採用專有的模版語言、實時模版編譯和fiber模式渲染。視圖層接收string類型模版,組件實例化後,snowball會對模版進行實時編譯,生成虛擬dom。渲染階段會對實體dom的生成和變動進行分片渲染,避免界面卡頓。css

// 這是一個簡單的 `component` 示例
@component({
    tagName: 'Order',
    template: `<div @click={user.name='new name'}>{user.name}</div> <ul> <li sn-repeat="item,i in orderList" @click={this.handleOrder(item, i)}>{i}:{item.tradeCode}</li> </ul>`
})
class Order extends Model {
    handleOrder(item, i) {
        console.log(item, i);
    }
}

new Order({
    user: {
        name: 'UserName'
    },
    orderList: [{
        tradeCode: '1234'
    }]
}).appendTo(document.body)
複製代碼

優勢

  1. 在須要從服務端拉取模版渲染的場景優於ReactAngular等框架。
  2. 狀態管理優於React等框架。
  3. 使用髒數據檢查和fiber模式進行異步渲染,性能好。

狀態管理

  1. 內置多種數據類型,如ModelCollectionCollection類中包含多種經常使用數組操做方法
  2. immutable,數據變動後對比很是方便
  3. 使用觀察者模式而且提供多種操做函數,輕鬆監聽數據的變化

開發

Use Snowball

  1. run git clone git@github.com:sorrymeika/snowball.git
  2. run cd snowball && npm install
  3. run npm run project yourProjectName to create your own project
  4. import { env, Model } from "snowball"
  5. see https://github.com/sorrymeika/juicy to get the full example!

Getting Start

  • run cd yourProject && npm start to start development server, it'll open the project url in browser automatically!
  • run npm run test to run test cases!
  • run npm run build to build the production bundle.
  • run npm run sprity to build sprity images.
  • to see the built project, please visit http://localhost:3000/dist/#/

if you get some error about canvashtml

  • run brew install pkgconfig if show "pkg-config: command not found"
  • run brew install cairo if show "No package 'cairo' found"
  • if you don't have brew command in your computer, see the brew installation
  • install the XQuartz

or前端

ornode

  • just remove the canvas module from package.json

打包

業務項目打包後會剔除掉`react`,`react-dom`,`polyfill`等框架和框架中的公共組件/公共樣式
複製代碼
  1. snowball會將React等框架註冊到 window.Snowball
  2. 使用 snowball-loader, 該loader會將 import React from "react" 替換成 const React = window.Snowball.React

框架版本管理

  1. snowball 會分大版本(1.x和2.x)和小版本(1.x.x和1.x.x),小版本升級(自動化測試)業務不感知。大版本升級業務需處理。
  2. snowball 會盡可能保證兼容性。讓大版本升級儘可能平滑。

項目結構

  • 項目主要分爲ControllerServiceView
  • Controller層用來組織Service層,並經過injectable註解將數據注入到View

項目代碼示例

  • 看完上面的文檔再看例子
import { Model, Collection, Reaction, attributes } from 'snowball';
import { controller, injectable, service, observer } from 'snowball/app';

// Model 的接口必須定義
interface IUser {
    userId: number;
    userName: string;
}

// Model
class UserModel extends Model {
    static defaultAttributes = {
    }
    attributes: IUser
};

const user = new UserModel({
    userName: 'aaa'
});

console.log(user.get(''));

// 可用 Reactive Object 替換 Model
class User implements IUser {
    @attributes.number
    userId;

    @attributes.string
    userName;

    constructor(user: IUser) {
        User.init(this, user);
    }
}

// Reaction 需和 Reactive Object 配合使用
// observer 基於 Reaction 實現
const user = new User();
const reaction = new Reaction(() => {
    console.log('it works!');
});
reaction.track(() => {
    console.log(user.userId);
});

setTimeout(() => {
    user.userId = Date.now();
    reaction.destroy();
}, 1000);

// Service 的接口必須定義
interface IUserService {
    user: IUser;
    setUserName(): void;
    loadUser(): Promise<IUser>;
}

// Service
@service
class UserService implements IUserService {
    constructor() {
        this._user = new User();
    }

    get user() {
        return this._user
    }

    loadUser() {
    }

    setUserName(userName) {
        this.user.userName = userName;
    }
}

// observer 組件
@observer(['userService', 'buttonStatus'])
class App extends Component<{ userService: IUserService }, never> {
    @attributes.string
    ohNo = 'oh, no!!';

    ohYes = () => {
        this.ohNo = 'oh, yeah!!';
    }

    render() {
        const { userService } = this.props;
        return (
            <div onClick={userService.setUserName.bind(null)} > {userService.user.userName} <p onClick={this.ohYes}>{this.ohNo}</p> </div>
        )
    }
}

// Controller
@controller(App)
class AppController {
    @injectable userService: IUserService;
    @injectable buttonStatus;

    constructor({ location }) {
        this.userService = new UserService();
        this.buttonStatus = observable(1);
    }

    pgOnInit() {
        this.userService.loadUser();
    }

    @injectable
    buttonClick() {
        this.buttonStatus.set(0);
    }
}
複製代碼

api文檔

vm

  • vm是一個MVVM框架,內置模版引擎和多種數據類型

模版引擎

  • 這是一個簡單的 template
  • 使用 {expression}sn-屬性 來綁定數據
<header class="header {titleClass}">這是標題{title}{title?'aaa':encodeURIComponent(title)}</header>
<div class="main">
    <h1>{title}</h1>
    <ul>
        <li>時間:{util.formateDate(date,'yyyy-MM-dd')}</li>
        <li>user:{user.userName}</li>
        <li>friend:{friend.friendName}</li>
        <li sn-repeat="msg in messages">msg:{msg.content}</li>
        <li sn-repeat="item in collection">item:{item.name}</li>
    </ul>
    <sn-template id="item"><li>{name}</li></sn-template>
    <ul>
        <li sn-repeat="item in list">{item.name}</li>
        <sn-item props="{{ name: item.name }}" sn-repeat="item in list"></sn-item>
    </ul>
</div>
複製代碼

sn-屬性

  • sn-[events] dom事件
model.onButtonClick = function(userName) {
    alert(userName);
}

// 設置 `model` 的事件代理
model.delegate = {
    onButtonClick: function(user) {
        alert(user.userName);
    }
}
複製代碼
<div>
    <button sn-tap="this.onButtonClick(user.userName)">Click 0</button>
    <button sn-tap="delegate.onButtonClick(user)">Click 1</button>
</div>
複製代碼
  • sn-repeat 循環
var model = new ViewModel(this.$el, {
    title: '標題',
    list: [{
        name: 1,
        children: [{
            name: '子'
        }]
    }, {
        name: 2
    }]
});
複製代碼
<div class="item" sn-repeat="item,i in list|filter:like(item.name,'2')|orderBy:name asc,id desc,{orderByWhat} {ascOrDesc}">
    <p>這是標題{title},加上{item.name}</p>
    <ul>
        <li sn-repeat="child in item.children|orderBy:this.orderByFunction">{i}/{child.name+child.age}</li>
    </ul>
</div>
複製代碼
  • [sn-if] [sn-else-if] [sn-else] 條件控制
<div class="item" sn-if="{!title}">當title不爲空時插入該element</div>
<div class="item" sn-else-if="{title==3}">當title不爲空時插入該element</div>
<div class="item" sn-else>當title不爲空時插入該element</div>
複製代碼
  • sn-display 控件是否顯示(有淡入淡出效果,若不須要動畫效果可以使用sn-visiblesn-if
<div class="item" sn-display="{title}">當title不爲空時顯示</div>
複製代碼
  • sn-html 設置innerHTML
<div class="item" sn-html="{title}"></div>
複製代碼
  • sn-component 引入其餘組建
var model = new ViewModel({

    components: {
        tab: require('widget/tab')
    },

    el: template,
    
    delegate: this,

    attributes:  {
        title: '標題',
        list: [{
            name: 1,
            children: [{
                name: '子'
            }]
        }, {
            name: 2
        }]
    }
});

複製代碼
<div class="tab" sn-component="tab" sn-props="{{items:['生活服務','通訊服務']}}"></div><sn-tab class="tab" props="{{items:['生活服務','通訊服務']}}"></sn-tab>
複製代碼

vm.Observer

  • 可觀察對象,類的數據變化可被監聽
  • ViewModel, Model, Collection, List, Dictionary, DictionaryList, Emitter, State 都是 Observer 的子類,分別有不一樣的做用
import { Observer, ViewModel, Model, Collection, List, Emitter, State } from 'snowball';

var viewModel = new ViewModel({
    el: `<div> <sn-template id="item"><li>{name}</li></sn-template> <h1>{title}</h1> <ul> <li sn-repeat="item in list">{item.name}</li> <sn-item props="{{ name: item.name }}" sn-repeat="item in list"></sn-item> </ul> </div>`,
    attributes: {
        title: '標題',
        list: [{
            name: '列表'
        }]
    }
});

var model = new Model({
    id: 1,
    name: '名稱'
});

var collection = new Collection([{
    id: 2,
    name: '名稱2'
}]);

collection.add(model);
collection.add([{ id: 3, name: '名稱3' }]);

viewModel.set({
    data: model,
    list: collection
})
複製代碼

vm.Model|vm.Dictionary

  • Observer 的屬性變化不能被監聽,Model|Dictionary 的屬性變化可被監聽
  • Model 是深拷貝,且是 immutable 的,Dictionary 淺拷貝對象,Observer 不拷貝對象可接收值類型

vm.List|vm.Collection|vm.DictionaryList

  • List 的子項是 ObserverCollection 的子項是 ModelDictionaryList 的子項是 Dictionary
  • List 性能優於 Dictionary 優於 Collection
var collection = new Collection([{
    id: 2,
    name: '名稱2'
}]);

collection.add(model);
collection.add([{ id: 3, name: '名稱3' }]);

// 原數據中ID存在相同的則更新,不然添加
collection.update([{ id: 2, name: '新名稱2' },{ id: 3, name: '新名稱3' }], 'id');

// 根據ID更新
collection.updateBy('id', { id: 3, name: '新名稱' });

// 更換數組
collection.updateTo([{ id: 3, name: '新名稱' }], 'id');

複製代碼

(Observer|...).prototype.get 方法

Model.prototype.attributes|Collection.prototype.array 屬性(只讀)

var data = new Model({
    id: 1,
    name: 'immutable data'
})
// 同等於 data.get()
var oldAttributes = data.attributes;

// 數據無變化
data.set({
    id: 1
});
console.log(oldAttributes == data.attributes);
// true

data.set({
    name: '數據變化了'
});
console.log(oldAttributes == data.attributes);
// false

console.log(data.get('id'))
// 1
複製代碼

(Observer|...).prototype.set 方法

  • 設置 ModelCollection
// 經過 `set` 方法來改變數據
// 此時關聯了 `user` 的 `home` 的數據也會改變 
// 若原先的 `userName` 已經是'asdf',則不會觸發view更新
user.set({
    userName: 'asdf'
});

home.set({
    title: 1,
    user: {
        age: 10
    }
});

// 經過 `collection.set` 方法覆蓋數據
// 更新數據使用 `collection.update|updateBy` 等方法性能會更好
collection.set([{
    id: 1,
    name: 'A'
}]);

複製代碼

(Observer|...).prototype.observe 方法

  • 監聽 Model變化
// 監聽全部數據變更
model.observe(function(e) {

});

// Model|Dictionary 可監聽 `user` 屬性的數據變更
model.observe('user', function(e) {

});

// Model 監聽 `user.userName` 屬性變更
model.observe('user.userName', function(e) {
});
複製代碼

(Observer|...).prototype.unobserve 方法

  • 移除監聽

(Observer|...).prototype.compute 方法

  • 計算
// 計算
var computed = model.compute(({ user, id, homePageId }) => {
    return user + id + homePageId;
});
computed.observe((value) => {
});
computed.get();
複製代碼

Model.prototype.collection(key) 方法

  • 獲取屬性名爲key的collection,不存在即建立
model.collection('productList').add([{ id: 1 }]);
複製代碼

Model.prototype.model(key) 方法

  • 獲取屬性名爲key的model,不存在即建立
home.model('settings').attributes;
複製代碼

(Collection|Model).prototype._ 方法

  • Model/Collection 查詢
/** * 搜索子Model/Collection, * 支持多種搜索條件 * * 搜索子Model: * model._('user') 或 model._('user.address') * * 根據查詢條件查找子Collection下的Model: * model._('collection[id=222][0].options[text~="aa"&value="1"][0]') * model._('collection[id=222][0].options[text~="aa"&value="1",attr^='somevalue'|attr=1][0]') * * 且條件: * model._("collection[attr='somevalue'&att2=2][1].aaa[333]") * * 或條件: * model._("collection[attr^='somevalue'|attr=1]") * * 不存在時添加,不可用模糊搜索: * model._("collection[attr='somevalue',attr2=1][+]") * * @param {string} search 搜索條件 * @param {any} [def] collection[attr='val'][+]時的默認值 */
home._('collection[name~="aa"|id=1,type!=2]').toJSON();


/** * 查詢Collection的子Model/Collection * * 第n個: * collection._(1) * * 查詢全部符合的: * collection._("[attr='val']") * 數據類型也相同:[attr=='val'] * 以val開頭:[attr^='val'] * 以val結尾:[attr$='val'] * 包含val,區分大小寫:[attr*='val'] * 包含val,不區分大小寫:[attr~='val'] * 或:[attr='val'|attr=1,attr='val'|attr=1] * 且:[attr='val'&attr=1,attr='val'|attr=1] * * 查詢並返回第n個: * collection._("[attr='val'][n]") * * 一個都不存在則添加: * collection._("[attr='val'][+]") * * 結果小於n個時則添加: * collection._("[attr='val'][+n]") * * 刪除所有搜索到的,並返回被刪除的: * collection._("[attr='val'][-]") * * 刪除搜索結果中第n個,並返回被刪除的: * collection._("[attr='val'][-n]") * * @param {string} search 查詢條件 * @param {object} [def] 數據不存在時默認添加的數據 * * @return {array|Model|Collection} */
collection._('[name="aa"]').toJSON();
複製代碼

Collection.prototype.add 方法

// 經過 `collection.add` 方法添加數據
collection.add({ id: 2, name: 'B' })
collection.add([{ id: 3, name: 'C' }, { id: 4, name: 'D' }])
複製代碼

Collection.prototype.update 方法

// 經過 `collection.update` 方法更新數據
collection.update([{ id: 3, name: 'C1' }, { id: 4, name: 'D1' }], 'id');
collection.update([{ id: 3, name: 'C1' }, { id: 4, name: 'D1' }], function(a, b) {
    return a.id === b.id;
});
複製代碼

Collection.prototype.updateTo 方法

  • 更新成傳入的數組
var arr = [{ id: 3, name: 'C1' }, { id: 4, name: 'D1' }];

// 經過 `collection.updateTo` 方法更新數據
collection.updateTo(arr, 'id');
複製代碼

Collection.prototype.updateBy 方法

  • 根據 comparator 更新 collection
var data = [{ id: 3, name: 'C1' }, { id: 4, name: 'D1' }];

/** * 根據 comparator 更新Model * collection.updateBy('id', { id: 123 name: '更新掉name' }) * collection.updateBy('id', [{ id: 123 name: '更新掉name' }]) * * @param {String} comparator 屬性名/比較方法 * @param {Object} data * @param {boolean} renewItem 是否覆蓋匹配項 * * @return {Collection} self */
collection.updateBy(id, data, true|false);
複製代碼

Collection.prototype.unshift 方法

  • 首部插入數據
collection.unshift({ id: 1 });
複製代碼

Collection.prototype.splice 方法

  • 移除或插入數據
collection.splice(0,1,[{ id: 1 }]);
複製代碼

Collection.prototype.size 方法 | Collection.prototype.length 屬性

  • Collection 長度

Collection.prototype.map 方法

  • Array.prototype.map

Collection.prototype.find 方法

  • 查找某條子Model
collection.find('id', 1);
複製代碼

Collection.prototype.filter 方法

  • Array.prototype.filter

Collection.prototype.remove 方法

  • 從 collection 中移除
collection.remove('id', 1);

collection.remove(model);

collection.remove(function(item) {
    return true|false;
});
複製代碼

Collection.prototype.clear 方法

  • 清除 collection

Collection.prototype.each 方法

  • 遍歷 collection

Collection.prototype.toArray | Collection.prototype.toJSON 方法

  • 將 collection 轉爲數組

(Observer|Model|Collection).prototype.destroy

  • 銷燬 Model | Collection

observable

  • 可觀察對象

observable()

// 自動根據數據類型生成 observable object
// plainObject對應Model, array對應Collection, 其餘對應Observer
const observer = observable(0|{}|[]|'');

// 設置數據
observer.set(1);

// 數據無變化不會觸發事件
observer.observe((val) => {
    console.log(val);
});

// 移除監聽
observer.unobserve((val) => {
    console.log(val);
});

// 傳入function生成 observable object,它是隻讀的,不能set
const observer = observable((fn)=>{
  document.body.addEventListener('click', fn);
    return () => {
      document.body.removeEventListener('click', fn);
    }
});
複製代碼

vm.State

const state = new State();

// 異步設置觸發事件,而且會觸發3次
state.set(1);
state.set(2);
state.set(3);

console.log(state.get());
// undefined
複製代碼

vm.Emitter

const emitter = new Emitter();

// 同步觸發事件,而且會觸發3次
emitter.set(1);
emitter.set(2);
emitter.set(3);

console.log(emitter.get());
// 3
複製代碼

vm.attributes

class User {
    @attributes.number
    userId = 0;

    @attributes.string
    userName;

    @attributes.object
    auth;

    constructor(data) {
        User.init(this, data);
    }
}

const user = new User();
user.userId = 1;
user.userName = '張三';

// 監聽user
User.observe(user, ()=>{
});
// 監聽user.userId
User.observe(user, 'userId', ()=>{
});
// 計算user.userId
User.compute(user, 'userId', (userId)=>{
    return 'userId:' + userId;
});
// user to plainObject
User.get(user);

User.set(user, {
    userId: 1
});

User.set(user, (userModel) => {
    userModel.set({
        userId: 10
    })
});

for (var key in user) {
    console.log(key);
}
// userId
// userName
複製代碼
相關文章
相關標籤/搜索