snowball-all

1.0.0 • Public • Published

Snowball

  • snowball 是一个一站式前端开发框架,你可以使用snowball轻松构建出一套web app/hybrid appsnowball内置了view层,但同时也支持React。它比React全家桶轻量又支持更多功能,如下:
  • 依赖注入:通过注解进行依赖注入。
  • 路由系统:拥有多工程跨工程加载、页面切换前进后退动画效果、手势返回、动态管理DOM等功能。
  • 状态管理:immutable、响应式,和redux不同,snowball的状态管理更符合OOP思想。
  • 视图:fiber模式渲染,高性能,双向绑定。 支持字符串模版,采用运行时模版编译。
  • 路由系统和状态管理都完全适配React
  • 业务项目采用分层架构,主要分为ControllerViewModelServiceView

路由

该路由方案专为多团队协作开发设计,将多个库整合成一个单页应用,让所有业务都使用相同的跳转动画、手势返回、页面缓存。
发布后到业务库共用一份核心库的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。

状态管理

  1. 内置多种数据类型,如ModelCollectionCollection类中包含多种常用数组操作方法
  2. immutable,数据变更后对比非常方便
  3. 使用观察者模式并且提供多种操作函数,轻松监听数据的变化

内置视图

snowball的视图层采用专有的模版语言、实时模版编译和fiber模式渲染。视图层接收string类型模版,组件实例化后,snowball会对模版进行实时编译,生成虚拟dom。渲染阶段会对实体dom的生成和变更进行分片渲染,避免界面卡顿。

开发

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 or https://github.com/sorrymeika/nuclear 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/#/

打包

业务项目打包后会剔除掉`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 会尽量保证兼容性。让大版本升级尽量平滑。

项目结构

  • 项目结构主要分为ControllerViewModelServiceView
snowball-project
├── package.json
├── index.js
├── app
|   ├── router.js
│   └── home <!-- 业务文件夹 -->
│       ├── controllers <!-- 页面控制器 -->
│       ├── view-models <!-- 视图状态/业务逻辑 -->
│       ├── services <!-- api调用/业务逻辑 -->
│       ├── scss 
│       ├── containers <!-- 页面组件 -->
│       └── components <!-- 组件 -->
├── shared
│   ├── view-models
│   └── services <!-- 公共服务 -->

启动应用

  • index.js 创建并启动应用
import { createApplication, lazy } from 'snowball/app';

const app = createApplication({
    projects: {
        "^/(subroute1|subroute2)/": 'http://localhost/subproject/assets-manifest.json'
    },
    routes: {
        '/': require('./app/home/controllers/HomeController'),
        // 异步加载
        '/item/\\d+:id': import('./app/item/controllers/ItemController'),
        // 懒加载
        '/type/\\d+:type(?:/\\d+:subType)?': lazy(() => import('./app/type/controllers/TypeController')),
    },
    configuration: configuration({
        modules: {
            // 公共模块注册,注册的模块可使用`autowired`自动加载依赖
            userService: singleton(UserService)
        }
    }),
    options: {
        // 禁用跳转切换动画
        disableTransition: true
    },
    // 对app进行扩展
    extend() {
        return {
            env: {
                api: 'https://**'
            },
            get server() {
                if (!this[SymbolServer]) {
                    this[SymbolServer] = new Server({
                        baseUrl: this.env.api
                    })
                }
                return this[SymbolServer];
            }
        }
    }
}, document.getElementById('root'), callback);
  • domain/services/UserService.js
// Service 的接口必须定义
interface IUserService {
    getUser(): Promise<IUser>;
}

class UserService implements IUserService {
    getUser() {
        // Service 和 Controller 都可直接使用 app
        return this.app.server.post('/getUser');
    }
}
  • app/home/configuration.js
import { configuration } from "snowball/app";

export const HomeConfiguration = configuration({
    modules: {
        typeViewModel: TypeViewModel
    }
});
  • app/home/controllers/HomeController.js
import { controller, autowired, configuration } from "snowball/app";

// Controller
@controller({
    component: Home,
    configuration: HomeConfiguration
})
class HomeController {
    @autowired
    typeViewModel;

    onInit() {
    }
}
  • app/shared/view-models/TypeViewModel.js
import { observable } from 'snowball';
import { ViewModel, disposable } from 'snowball/app';

class TypeViewModel extends ViewModel {
    @observable types = [];
    @observable subTypes = [];

     @disposable
    _eventEmitter = new EventEmitter();

    constructor() {
        super();

        this._eventEmitter.on((e, typeId) => this.onTypeChange(typeId));
    }

    // 初始化事件,页面`onInit`完成且`autowired`第一次调用时触发
    // `ViewModel`的生命周期事件同`Controller`基本一样
    onInit() {
        this.types = await this.app.server.post('/getTypes');
    }

    dispatch = (e) => this._eventEmitter.emit(e);

    onTypeChange(typeId) {
        this.subTypes = await this.app.server.post('/getSubTypes', typeId);
    }
}
  • app/home/containers/Home.jsx
import { observable } from "snowball";
import { observer } from "snowball/app";

@observer
class Home extends Component {
    // 在 React.Component 中 `attributes` 可和 `observer` 配合使用
    @observable.string
    ohNo = 'oh, no!!';

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

    render() {
        return (
            <div>
                <p onClick={this.ohYes}>{this.ohNo}</p>
                <TypeSelect />
            </div>
        )
    }
}
  • app/home/containers/TypeSelect.jsx
import { inject, loadModule } from "snowball/app";

// 可通过 `inject` 方法将 `controller` 的属性注入到组件的 `props` 中
// `inject`方法内使用`autowired`可根据注册名称自动加载并实例化依赖
// `controller` 中以 `_`和'$'开头的属性会认为是私有属性,不可注入
const TypeSelect = inject(() => {
    const typeViewModel = loadModule('typeViewModel');
    return {
        types: typeViewModel.types,
        onTypeChange: typeViewModel.onTypeChange,
    };
})((props) => {
    const { type, subTypes, onTypeChange } = props;

    return (
        <>
            <select onChange={onTypeChange}>{types.map((type)=><option value={type.id}>{type.name}<option>)}</select>
            <select>{subTypes.map((subType)=><option>{subType.name}<option>)}</select>
        </>
    );
})

控制层 controllers

  • 控制层,可依赖shared,components,models,services,主要用来处理UI状态、整合service并暴露给UI等,本层一般只做整合,是页面/组件和应用层service的中介。

@controller 方法

  • 关联页面/组件跟Controller类的方法,一般使用修饰符模式

@autowired 方法

  • 自动装载依赖

import { controller } from "snowball";
import Home from "../containers/Home";

/**
 * Controller类生命周期
 *     onInit: 页面第一次打开,且动画开始前触发
 *     onShow: 页面显示,动画结束时触发
 *     onCreate: 页面第一次打开,且动画结束后触发
 *     onResume: 页面从后台进入前台,且动画结束时触发
 *     onPause: 页面从前台进入后台,且动画结束时触发
 *     onDestroy: 页面被销毁后触发
 * Controller方法、属性排序
 *     constructor
 *     页面生命周期
 *     属性 (get, set)
 *     方法
 */
@controller({
    component: Home,
    configuration: HomeConfiguration
})
export default class HomeController {
    // 将url上的参数或configuration.parameters的配置自动注入到this.type上
    // 同等于 @param('type', { 
    //    type: 'number' // 数据类型
    // });
    @param
    type;

    @autowired
    _userService;

    constructor(props, ctx) {
        // 框架会自动带入路由参数到 props 中
        // props.location.params 为路由 `/product/:type/:id ` 中的配置
        // props.location.query 为hash链接`?`后面的参数
        const id = ctx.location.params.id;
        const type = ctx.location.params.type;

        console.log('this.type === type:', this.type, type)

        // /home?source=wap&id=1
        // 此时 props.location.query 为 { source: 'wap',id: 1 }
        console.log(ctx.location.query);

        // 页面信息
        // 页面是否是激活状态
        // console.log(ctx.page.isActive());
        // 页面是否是销毁
        // console.log(ctx.page.isDestroyed());
    }

    // 页面初始化事件,数据请求不要放到 `constructor` 里,而是放在 `onInit` 里
    async onInit() {
        await this._userService.fetch();

        // 缓存页面数据到localStorage
        this.ctx.page.setCache({
            pageInfo: this.pageInfo
        });
    }

    onPause() {
    }

    onResume() {
    }

    onDestroy() {
    }

    get user() {
        return this._userService.getModel();
    }

    handleTitleClick() {
        this._userService.update();
    }
}



服务层 services

  • 服务层,主要作用为调用服务端接口,处理业务逻辑。应用services间可互相调用。必须写interface.
import UserModel from "../models/UserModel";

export default class UserService implements IUserService {

    constructor() {
        this._userModel = new UserModel();
    }

    userModel() {
        return this._userModel;
    }

    // 从服务端请求数据,命名规范为 request[Something](args)
    async request() {
        return await unicorn.getUserInfo();
    }

    // 从服务端请求数据并保存到本地model,命名规范为 pull[Something](args)
    async pull() {
        var res = await this.request();
        this.userModel.set(res.data);
        return res;
    }

    // 更新本地/远程数据,命名规范为 update[Something](args)
    update() {
        this.userModel.set({
            title: 'afsdfas'
        })
    }
}



页面层(containers)

  • 存放页面级组件,只负责展示和整合components,一般使用无状态组件,事件和业务逻辑操作统一交给controllersservices来处理。可依赖components
var User = function (props) {
    // 被映射的数据
    console.log(props.user);

    return <div onClick={controller.onClick}>{props.user.name}</div>;
}

inject 方法

  • controller的属性和方法,可通过inject跨组件注入到子组件的props中
import { inject } from 'snowball';

// 尽量使用修饰符和字符串参数,保证传 `props` 时能够覆盖 `context`
@inject('home', 'child')
class SomeComponent extends Component {
}

// `return`的属性会覆盖`props`的属性
inject(({ user, data }, props)=>{
    // 可自动加载configuration配置的类
    const userService = loadModule('userService');
    return {
        user,
        data
    }
})(Component)



应用和路由

createApplication 方法

  • 启动应用
import { createApplication, configuration, singleton } from 'snowball';
import HomeController from 'controller/HomeController';

// 子应用根路由注册
const projects = {
    "^/trade(?=/|)": "https://project.com/asset-manifest.json"
};
// 主应用路由注册,不可和子应用根路由重合
// 尽量把路由收敛到 `routes.js` 中
const routes = {
    '/': HomeController,
    '/product': import('controllers/ProductController')
};
// 启动应用
const app = createApplication({
    projects,
    routes,
    configuration: configuration({
        modules: {
            userService: singleton(UserService)
        }
    }),
    options: {
        // 禁用跳转切换动画
        disableTransition: true
    }
}, document.getElementById('root'), callback);

registerRoutes 方法

  • 注册路由
import { registerRoutes } from 'snowball';

/**
 * 路由列表格式为: 
 * {
 *   [key]: require('module')
 * }
 * 其中key为路由规则,可完全匹配、模糊匹配和正则匹配,示例:
 */
var routes = {
    // 完全匹配
    '/cart': require('bundle?lazy&name=cart!controllers/CartController')
    // 完全匹配
    "/medical": require('someComponent')
    // 模糊匹配
    "/market/:id": require('someComponent')
    // 正则匹配
    "/o2omarket/\\d+:id": require('someComponent'),
    // 正则匹配多路由
    "/proxy/home(?:/\\d+:logicId)?": require('someComponent')
    // 懒加载
    "/shop": require('bundle?lazy&name=ur-package-name!someComponent')
};
// 注册新路由
registerRoutes(routes);



app 和 ctx 应用上下文

  • 可在ControllerService中使用appctx属性
  • app是应用级的,ctx是页面级的。createApplication时通过extend扩展的属性都会挂到app上面。
import { controller } from 'snowball/app';
import User from './components/User';

@controller(User)
class UserController {
    @autowired
    _userService;
    
    constructor(props, ctx) {
        this.userId = ctx.location.params.id;
    }

    transitionToFav() {
        this.ctx.navigation.forward('/fav', {
            platform: this.app.env.PLATFORM
        })
    }
}

class UserService extends Service {
    transitionToOrder() {
        this.ctx.navigation.forward('/order')
    }
}

ctx.page 属性

  • 页面信息
// 当前页面是否活跃
ctx.page.isActive();

// 当前页面是否已被销毁
ctx.page.isDestroyed();

// 页面标题
ctx.page.title;

// 获取dom节点
ctx.page.findNode(selector);
ctx.page.findNodeAll(selector);

ctx.navigation 页面跳转,同app.navigation

ctx.navigation.forward 方法

  • 跳转页面,带前进动画
// 跳转到商品页
ctx.navigation.forward('/item/1')

// 跳转并传入 props
ctx.navigation.forward('/item/2', {
    action: 'dofast'
})

ctx.navigation.back 方法

  • 跳转页面,带返回动画
// 返回到首页
ctx.navigation.back('/')

// 返回到首页并传入 props
ctx.navigation.back('/', {
    action: 'dofast'
})

ctx.navigation.transitionTo 方法

  • 跳转页面
/**
 * @param {string} [url] 跳转连接
 * @param {boolean} [isForward] 是否带前进动画,前进动画:true,后退动画:false,不填无动画
 * @param {object} [props] 传给下个页面的props 
 */

// 不带动画跳转返回到首页并动画跳转到商品页,这样使用history records才不会错乱
ctx.navigation.transitionTo('/')
    .forward('/product/5');

// 不带动画跳转
ctx.navigation.transitionTo('/product/4');

ctx.navigation.replace 方法

  • 替换当前链接,覆盖最后一条历史
ctx.navigation.replace('/error/notfound?error=店铺状态异常');

ctx.navigation.home 方法

  • 返回到首页
ctx.navigation.home()

ctx.app

  • 同等于 app

ctx.service

  • 同等于 app.service

扩展页面页面和ctx

import { Page } from 'snowball/app';

// 生命周期
Page.extentions.lifecycle({
    initialize() {
        this.sharer = new Sharer();
    }

    onShow() {
        this.sharer.sync();
    }
});

// 扩展页面属性
Page.extentions.minxin({
    // console.log(this.ctx.page.newProp());
    newProp() {
        return 'newProp' + Date.now();
    }
});

// 扩展`ctx`
Page.extentions.ctx((page, ctx) => {
    const logger = {
        error: (message) => {
            console.error(ctx.location.url, ":", message);
        }
    }

    return {
        get logger() {
            return logger;
        }
    }
});

Model

  • models/UserModel.js
import { Model, Collection, Reaction, observable, initReactiveObject } from 'snowball';
import { controller, 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 {
    @observable.number
    userId;

    @observable.string
    userName;

    constructor(user: IUser) {
        initReactiveObject(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);

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-属性

  • on[event] dom事件
model.onButtonClick = function(userName) {
    alert(userName);
}

// 设置 `model` 的事件代理
model.delegate = {
    onButtonClick: function(user) {
        alert(user.userName);
    }
}
<div>
    <button onclick="this.onButtonClick(user.userName)">Click 0</button>
    <button onclick="delegate.onButtonClick(user)">Click 1</button>
</div>
  • sn-repeat 循环
<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-[componentName]大写字母开头的标签 引入其他组件
<sn-tab class="tab" props="{{items:['生活服务','通信服务']}}"></sn-tab>
<Tab class="tab" props="{{items:['生活服务','通信服务']}}"></Tab>

Observer

  • 可观察对象,类的数据变化可被监听
  • Model, Collection, List, Dictionary, Frame, State 都是 Observer 的子类,分别有不同的作用
import { Observer, Model, Collection, List, Frame, State } from 'snowball';

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
})

Model|Dictionary

  • Observer 的属性变化不能被监听,Model|Dictionary 的属性变化可被监听
  • Model 是深拷贝,且是 immutable 的,Dictionary 浅拷贝对象,Observer 不拷贝对象可接收值类型

List|Collection

  • List和Collection都是集合类,类实例及其子项的变化都可被监听
  • List 的子项如果是Plain Object,会使用Dictionary存储,非Plain Object使用Observer存储
  • Collection 的子项是 Model
  • List 性能优于 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 方法

  • 移除监听

(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);
    }
});

State

const state = new State();

// 异步设置触发事件,并且会触发3次
state.set(1);
state.set(2);
state.set(3);

console.log(state.get());
// undefined

Frame

const emitter = new Frame();

// 每次界面渲染完成之后触发事件,并且会触发3次
emitter.set(1);
emitter.set(2);
emitter.set(3);

console.log(emitter.get());
// 3

observable

class User {
    @observable
    anyType;

    @observable.number
    userId = 0;

    @observable.string
    userName;

    @observable.object
    auth;
}

const user = User.from({
    userId: 1,
    userName: '张三'
});

// 监听user.userId
asObservable(user).observe('userId', ()=>{
});

// 计算user.userId
compute(asObservable(user).observe('userId'), (userId) => {
    return 'userId:' + userId;
});

asObservable(user).set({
    name: 1
});

for (var key in user) {
    console.log(key);
}
// userId
// userName



公共组件

MainScrollView|ScrollView

  • 带滚动条和自动加载数据功能的组件
import { ScrollView, MainScrollView, scrollUtils } from 'snowball/components';

/**
 * @param {function} [onScrollViewInit] 初始化事件
 * @param {function} [pullToRefresh] 下拉刷新
 * @param {function} [onScrollToBottom] 滚动到底部事件
 * @param {string|PagiationStatus} [loadMoreStatus] 加载更多状态
*/

<MainScrollView
    loadMoreStatus={PagiationStatus}
    onScrollToBottom={autoLoadMore}
    onScroll={(e) => {
        console.log(e, e.target, e.x, e.y);
    }}
    pullToRefresh={()=>{  }}
    ref={mainScrollViewRef}
>
</MainScrollView>

<ScrollView onScroll={()=>{}} className="mk_shop__list">
<div>content</div>
</ScrollView>

Header

  • app头组件
import { Header } from 'snowball/components';

// 普通app头
<Header 
    title="Title"
/>

// 带搜索和按钮的app头
<Header
    className="hp_homepage__header header_primary"
    buttons={
        <button
            app-link="/order"
            className="iconfont icon-list cl_fff pl_s pr_s dp_b ta_c lh_1"
        >
            <i className="dp_b fs_xxs mt_2">我的订单</i>
        </button>
    }
>
    <form
        app-link="/medicalsearch"
        className="header_search iconfont icon-search flex"
        style={{ marginLeft: '-8%', width: '85%' }}
        onSubmit={() => false}
    >
        <span className="ps_r fs_m cl_999">{searchDocument ? searchDocument : '搜索'}</span>
    </form>
</Header>

小文字/小文字Tag标签(修复安卓小与12px的文字偏上)

  • 小文字/小文字Tag标签(修复安卓小与12px的文字偏上)
import { SmallTag, SmallText } from 'snowball/components';

<SmallTag className={'ur_tag'} text="aaa" fontSize="8" />
<SmallText text="aaa" fontSize="8" />

图片轮播

  • 图片轮播组件
import { Slider } from 'snowball/components';

<Slider 
    data={
        [{
            link: '/',
            src: 'image src'
        }]
    }
/>

图片浏览组件

  • 图片浏览组件
import { PhotoViewer } from 'snowball/components';

<PhotoViewer 
    images={
        [{
            src: 'image src'
        }]
    }
/>

通用弹框组件

import { toast, loader, popup } from 'snowball/widget';

// Toast
toast.showToast('2秒后消失');

// 显示/隐藏加载中
loader.showLoader();
loader.hideLoader();

// 弹框,具体参数见代码注释
popup.confirm({ title, content, onOk, onCancel });
popup.alert({ title, content, onOk });
popup.prompt();
// 通用弹框
popup.popup();



自定义事件

import { Event, EventEmitter } from 'snowball';

const emitter = new EventEmitter();

emitter.on('dataload', (e)=>{});
emitter.one('dataload', (e)=>{});
emitter.off('dataload');

emitter.trigger('dataload');
emitter.trigger(new Event('dataload', {
    target
}));



@component 注解

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


jsonp 接口请求

import { jsonp } from 'snowball';

await jsonp('src', params);



loadJs 加载js

import { loadJs } from 'snowball';

await loadJs('src');
await loadJs(['src', 'src1']);



util

  • 工具类
import { util } from 'snowball';

util.is(PlainObject|EmptyObject|Boolean|Number|String|Object|Array|Yes|No|Thenable)

util.qs

util.hashQs

import { util } from 'snowball';
const url = 'https://www.whatever.com/?app=snowball#/item/123?source=home';
const queryStringObject = util.qs(url);
// { app: 'snowball' }

const newUrl = util.qs(url, { biz: 'DJ' });
// 'https://www.whatever.com/?app=snowball&biz=DJ#/item/123?source=home'

const hashObject = util.hashQs(url);
// { source: 'home' }

const newUrlWithHash = util.hashQs(url, { biz: 'DJ' });
// 'https://www.whatever.com/?app=snowball#/item/123?source=home&biz=DJ'

util.clone

util.deepClone

util.extend

util.style

  • 插入样式表

util.encodeHTML 方法

  • html 转码

util.pick 方法

  • 同 _.pick

util.debounce

util.throttle

util.cookie 方法

  • 获取、设置document.cookie

util.store 方法

  • 获取、设置localStorage

util.equals 方法

  • 判断两个 Object、Array 结构和值是否相同(引用不同)

util.groupBy 方法

  • 数组分组

util.sum 方法

  • 数组求和

util.array 方法

  • 数组操作链
// 链式处理,并返回数组中某个item
util.array([{ id: 1 }, { id: 2 }])
    .filter('id', 1)
    .concat([{ id: 3 }])
    .map((item) => item)
    .exclude('id', 2)
    .find('id', 3);

// 链式处理,并返回 Array
util.array([{ id: 1 }, { id: 2 }])
    ._('[id=1,name=2]')
    .filter((item) => item.id == 1)
    .toArray();

util.query 方法

  • 数组搜索,类似 Collection.prototype._

util.formatDate 方法

  • 日期转字符串
util.formatDate(new Date(), 'yyyy-MM-dd HH:mm:ss_ffff');

// 2012-02-03 星期一
util.formatDate(new Date(), 'yyyy-MM-dd W');

// 刚刚、n分钟前、n小时前、昨天 HH:mm、yyyy-MM-dd HH:mm
util.formatDate(Date.now(), 'short');

// HH:mm、昨天 HH:mm、yyyy-MM-dd HH:mm
util.formatDate(Date.now(), 'minutes');

util.timeLeft 方法

// 1天 00:10:00
util.timeLeft(10000000);

// 00:10:00
util.timeLeft(10000);



Zepto

// 不推荐使用
import { $ } from 'snowball'
$('.class').on('click', ()=>alert(1));

Package Sidebar

Install

npm i snowball-all

Weekly Downloads

0

Version

1.0.0

License

none

Unpacked Size

1.3 MB

Total Files

244

Last publish

Collaborators

  • sorrymeika