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
复制代码