snowball
是一個一站式前端開發框架,你可使用snowball
輕鬆構建出一套web app/hybrid app
。snowball
內置了view
層,但同時也支持React
。它比React
全家桶輕量又支持更多功能,以下:redux
不一樣,snowball
的狀態管理更符合OOP
思想。React
、Vue
和Angular
等框架。React
。Controller
、Service
、View
層,Controller
層用來組織Service
層,並經過injectable
註解將數據注入到View
層。該路由方案專爲多團隊協做開發設計,將多個庫整合成一個單頁應用,讓全部業務都使用相同的跳轉動畫、手勢返回、頁面緩存。
發佈後到業務庫共用一份核心庫的js/css/image/iconfont,減小下載資源的大小。
一個核心框架庫+多個業務庫。業務庫之間不依賴,可單獨發佈。
複製代碼
snowball
統一控制路由,須要在 snowball
中註冊須要加載的業務asset-manifest.json
文件,snowball
經過路由匹配到業務,並加載manifest中的js和css。registerRoutes({...})
方法註冊子路由snowball
在業務js/css加載完成後,根據業務註冊的子路由跳至對應頁面。navigation.forward
和 navigation.back
方法來控制頁面跳轉的動畫效果。使用 navigation.forward
跳轉頁面後,點擊瀏覽器返回上一頁
會自帶返回動畫。若無需跳轉動畫可以使用 navigation.transitionTo
方法。手勢返回
功能,navigation.forward
跳轉到新頁面以後,左滑頁面可返回上一頁。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)
複製代碼
React
和Angular
等框架。React
等框架。fiber
模式進行異步渲染,性能好。Model
和Collection
,Collection
類中包含多種經常使用數組操做方法immutable
,數據變動後對比很是方便git clone git@github.com:sorrymeika/snowball.git
cd snowball && npm install
npm run project yourProjectName
to create your own projectimport { env, Model } from "snowball"
https://github.com/sorrymeika/juicy
to get the full example!cd yourProject && npm start
to start development server, it'll open the project url in browser automatically!npm run test
to run test cases!npm run build
to build the production bundle.npm run sprity
to build sprity images.http://localhost:3000/dist/#/
if you get some error about canvas
html
brew install pkgconfig
if show "pkg-config: command not found"brew install cairo
if show "No package 'cairo' found"or前端
ornode
canvas
module from package.json
業務項目打包後會剔除掉`react`,`react-dom`,`polyfill`等框架和框架中的公共組件/公共樣式
複製代碼
snowball
會將React
等框架註冊到 window.Snowball
上snowball-loader
, 該loader會將 import React from "react"
替換成 const React = window.Snowball.React
snowball
會分大版本(1.x和2.x)和小版本(1.x.x和1.x.x),小版本升級(自動化測試)業務不感知。大版本升級業務需處理。snowball
會盡可能保證兼容性。讓大版本升級儘可能平滑。Controller
、Service
、View
層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);
}
}
複製代碼
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-[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-visible
或sn-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
的子項是 Observer
,Collection
的子項是 Model
,DictionaryList
的子項是 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
方法Model
、Collection
// 經過 `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.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)
方法model.collection('productList').add([{ id: 1 }]);
複製代碼
Model.prototype.model(key)
方法home.model('settings').attributes;
複製代碼
(Collection|Model).prototype._
方法/** * 搜索子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
方法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.prototype.map
方法Array.prototype.map
Collection.prototype.find
方法collection.find('id', 1);
複製代碼
Collection.prototype.filter
方法Array.prototype.filter
Collection.prototype.remove
方法collection.remove('id', 1);
collection.remove(model);
collection.remove(function(item) {
return true|false;
});
複製代碼
Collection.prototype.clear
方法Collection.prototype.each
方法Collection.prototype.toArray
| Collection.prototype.toJSON
方法(Observer|Model|Collection).prototype.destroy
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
複製代碼