一站式前端开发框架Snowball介绍
Snowball
snowball
是一个一站式前端开发框架,你可以使用snowball
轻松构建出一套web app/hybrid app
。snowball
内置了view
层,但同时也支持React
。它比React
全家桶轻量又支持更多功能,如下:- 路由系统:拥有多工程跨工程加载、页面切换前进后退动画效果、手势返回、动态管理DOM等功能。
- 状态管理:immutable、响应式,和
redux
不同,snowball
的状态管理更符合OOP
思想。 - 视图:fiber模式渲染,高性能,双向绑定。 采用运行时模版编译,在需要从服务端拉取模版渲染的场景优于
React
、Vue
和Angular
等框架。 - 路由系统和状态管理都完全适配
React
。 - 业务项目采用分层架构,主要分为
Controller
、Service
、View
层,Controller
层用来组织Service
层,并通过injectable
注解将数据注入到View
层。 - 项目地址:github.com/sorrymeika/…
路由
该路由方案专为多团队协作开发设计,将多个库整合成一个单页应用,让所有业务都使用相同的跳转动画、手势返回、页面缓存。
发布后到业务库共用一份核心库的js/css/image/iconfont,减少下载资源的大小。
一个核心框架库+多个业务库。业务库之间不依赖,可单独发布。
复制代码
多工程跨工程加载
- 核心框架
snowball
统一控制路由,需要在snowball
中注册需要加载的业务 - 业务库打包后会生成
asset-manifest.json
文件,snowball
通过路由匹配到业务,并加载manifest中的js和css。 - 业务js加载时调用
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
的生成和变更进行分片
渲染,避免界面卡顿。
// 这是一个简单的 `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
,数据变更后对比非常方便- 使用观察者模式并且提供多种操作函数,轻松监听数据的变化
开发
Use Snowball
- run
git clone git@github.com:sorrymeika/snowball.git
- run
cd snowball && npm install
- run
npm run project yourProjectName
to create your own project import { env, Model } from "snowball"
- 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 canvas
- 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
- see the Installation OSX to install without brew command
or
- just remove the
canvas
module frompackage.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);
}
}
复制代码
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-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变化
// 监听所有数据变动
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
相关文章