一站式前端开发框架Snowball介绍

1,750 阅读7分钟

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的生成和变更进行分片渲染,避免界面卡顿。

// 这是一个简单的 `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 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

or

  • 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