cyra-pure
TypeScript icon, indicating that this package has built-in type declarations

1.3.8 • Public • Published

Cyra

欢迎使用 Cyra

Cyra 是一个 JS 移动端单页视图容器。

Cyra 的设计是为了简化移动端单页应用(SPA)的开发,提供一个基础的框架结构以及环境管理,使用户能够集中精力在应用开发上。

三个简单的概念 PageRouteAction 来帮助开发者处理复杂的视图切换逻辑、数据传递及视图生命周期管理。

Cyra 不仅仅做了这些,她更多的是向开发者传递一种架构思想,帮助用户构建出更加健壮的移动端单页应用。

几乎任何称得上 SPA 的项目都可以使用 Cyra,在大型复杂单页应用上 Cyra 的优势会更为突出。

文档导航

如何开始?

  • 安装: npm i cyra --save

  • 引用:

    • ES6 或者 Typescript: import { Cyra } from 'cyra'
    • ES5: var Cyra = require('cyra').Cyra
  • 编码:

简单的 helloWorld 示例,参见 examples-helloWorld 在线演示 online-helloWorld

main.js

import { Cyra, Page } from '../../';
 
import home from './view/home';
 
// routes
Cyra.defineRoute('home', home);
 
Cyra.initApp({
    root: 'body',    // the root container selector
    default: 'home'  // default path
});

home.js

import { Cyra, Page } from 'cyra';
 
class HomePage extends Page {
 
    id () {
        return 'home';
    }
    // 定义了该函数后可以在进入视图时候改变页面title
    title () {
        return 'HOME-TITLE';
    }
 
    /*
        以下函数如若未做任何事,可以省略不写
    */
    initialize (next) {
        next();
    }
 
    willAppear (next) {
 
        this.container.innerHTML = 'Hello World';
 
        next();
    }
 
    didAppear (next) {
        next();
    }
 
    willDisappear (next) {
        next();
    }
}
 
export default new HomePage();

基础思想

在移动端 SPA 架构中,有以下几个重要的模块:

  • 视图导航

    所有导航动作被封装成了 Action,执行 Action 会执行跳转,你可以在 Action 中传递参数

    你可以在应用启动的时候看到这个应用里所有可能的跳转,下面在 Action 概念介绍的时候会讲到

    使用 Cyra 的单页应用可以在任何情况下刷新,Cyra 的框架设计会帮助你处理任何复杂的产品逻辑

  • 视图管理以及内容布局

    Cyra 会管理所有视图的生命周期,你可以轻松获悉每一个视图的执行阶段,并针对阶段进行操作

    Cyra 为每一个视图提供一个容器(div),用户可以在这个容器内自由发挥

  • 视图之间数据交互

    数据传递在使用 Cyra 后会变得无比简单,我们提供 URL 传递及弱数据传递, 我们会在后面详细介绍

  • 服务器数据交互

    关于服务器数据交互我们什么都不会做,完全由用户自由发挥

  • 单元测试

    由于所有的Page, Action 都被 Cyra 有序的管理,你可以比以往更方便的书写单元测试

  • 兼容性

    在我们以往的项目实践过程中,发现对于单页应用来说,兼容性往往是成功路上的绊脚石,我们常常会因为解决一个体验问题而引入非常多的 Bug。

    在 Cyra 的设计中,我们考虑了最大的兼容性,未使用 iScroll, History APILocalStorage 等技术

    同时也希望用户在使用这些技术的时候保持谨慎

在 Cyra 的架构中,根据大量的历史开发经验,应用以下几个重要的思想:

  1. 显式管理视图生命周期以及所有视图操作

  2. 提供视图容器,使视图之间最大程度隔离

  3. 一切都是钩子

核心概念

  • Page 表示一个视图,书写一个视图所需要的所有执行阶段都在 Page 中有相应的钩子

  • Action 表示一个导航动作,你只需要在其中定义从哪里出发,到哪里去,应用在启动的时候便可以获悉所有的视图及其可能跳转去的地方

  • Route 路由,表示 URL 及视图对象的对应关系

衍生概念

  • 强数据 指从一个视图跳转到另外一个视图的时候不能丢失的数据,这些数据会放在URL上,即使视图刷新也不会丢失

    实现使用 performAction

  • 弱数据 指在一个视图的展现,依赖另外一个视图传递过来的数据的时候,可以传递弱数据,用户可以判断是否存在弱数据,并执行相应的逻辑

    实现使用 prepareForAction 中的 destinationPagePerform

技术参数

  • 使用 TypeScript 编写,可以编译成 ES3, ES5, ES6。并可以在 TypeScript 项目中直接使用。

  • 使用 CommonJS 模块化规范,但同时也可以编译成 AMD 模块化规范使用。

  • 本身代码非常精简。大小为:7.93 kB GZIP后:2.6 KB

视图生命周期

先看生命周期图:

生命周期图

周期图说明

  1. 橙色的 entering,leaving 代表视图两大过程, 进入和离开。
  2. 最左边的 disappeared,appeared 代表两大状态,显示和隐藏。
  3. 其中 initialize, willAppear, didAppear, willDisappear 是用户可以自定义的钩子函数
  4. appearing,disappearing是系统内部的函数过程,主要控制视图的隐藏和展现。如果想要在视图展现和隐藏时添加动画,可改写这两个方法的默认控制,请参见 HOW-TO部分 的转场动画

生命周期

  1. 当载入页面,所有引用的视图都开始创建。
  2. 当 url 的 hash 值(hash 值为空时为用户定义的默认路径,即 initApp 中传入的 default)匹配到相关的route,其对应的视图(page) 开始执行 entering 的过程。
  3. 视图第一次开始展现时,依次执行 initialize, willAppear, didAppear, 若某个函数未定义则不执行,若在这其中某一步未调用next()也会中断执行。
  4. 视图第一次执行 entering 过程时,依次执行 initialize, willAppear, (appearing), didAppear,若未定义则跳过,若在这其中某一步未调用 next() 也会中断执行。
  5. 当视图第二次执行 entering 过程,将不再执行 initialize, willAppear,如若更改该行为请参见 HOW-TO部分 的如何控制重新加载
  6. 当一个页面开始执行 entering 过程,则上一个页面开始执行 leaving 过程,即 willDisappear,(disappearing)

使用指导

  • 提倡组件层面抽象,减少视图抽象

  • 将视图渲染、数据获取等操作放到视图中最合适的阶段中

  • 强数据传递尽量传递查询参数以保持视图独立,并保证 URL 不会太长

  • 弱数据传递需要做好足够的判断,以保证逻辑的健壮

API 文档

Cyra

  • defineRoute (path: string, page: Page): Route

    • @param {string} path - 匹配路径
    • @param {Page} page - page对象
    • @return {Page}

    该函数根据匹配路径和 视图实例(page 对象)定义一个路由并返回。匹配路径是根据 URL 后面的 hash 值判断的, 比如 index.html#home, 该 URL 匹配的路径为 home。当 URL 匹配路由的路径,则进入相应视图。

  • defineBackRouteRedirect (fromRoute: Route, toRoute: Route, redirectToRoute: Route): BackRoute

    • @param {Route} fromRoute - 跳转前route
    • @param {Route} toRoute - 跳转后route
    • @param {Route} redirectToRoute - redirect route
    • @return {BackRoute}

    该函数定义一个重定向路由,具体使用请参见 HOW-TO部分 的如何定向后退

  • defineAction (fromPageID: string, toPageID: string, isBack: boolean = false): Action

    • @param {string} fromPageID - 动作开始页面ID
    • @param {string} toPageID - 动作目标页面ID
    • @param {boolean} isBack - 是否在回退,可选参数,默认为false
    • @return {Action}

    该函数根据离开的视图 ID 和 要进入的视图 ID 定义一个 Action 并返回,Action 的含义主要是视图切换。具体使用请参见 HOW-TO部分 的如何定义一个 action

  • initApp (obj): void

    • @param {Object}
    • @example
    {
            root: '#container'  // spa 应用所在容器的选择器,默认为 'body'
            default: 'home'     // 当 hash 值为空时的默认路径
    }

    在这个函数里传入初始化单页应用需要的一些值。

  • models

    存储所有的Page,Action,Route,BackRoute实例的引用。 初始数据结构:

    {
        pages      : {},
        actions    : [],
        routes     : [],
        backRoutes : []
    };
  • extend (to, from)

    • @param {Object} to - 要扩展的对象
    • @param {Object} from - 要扩展的内容
    • @return {Object} 扩展之后的对象to

    注解: 该方法只是帮助 ES5 用户在不使用继承机制的情况下扩展 page 的方法

    Cyra.extend(new Page('home'), {
     
            initialize: function (next) {
                next();
            }
     
        // ...
    });

    ES6 或者 Typescript 用户请直接使用继承语法

    class HomePage extends Page {
            id (): {
                return 'home'
            }
            initialize: function (next) {
                next();
            }
     
            // ...
    }

Page

  • performAction (action: Action, data?: any): void

    • @param {action} actoin - Action 对象.
    • @param {object} data - 可选参数,要传递给目标视图的数据

    执行一个定义好的 Action, 在当前视图中只能执行以当前视图为起点的 Action

  • prepareForAction (next: Function, action: Action, destinationPagePerform: Function): void

    • @param {Function} next - 当这里的操作执行完了调用 next 转移控制权
    • @param {Action} action - 当前执行的 Action 实例
    • @param {Function} destinationPagePerform - 提供一种执行目标视图函数的方法

    注解: 该方法不用主动调用,只需定义在page里,当执行 performAction 时会先自动执行该方法

  • destinationPagePerform (functionName: string, ...rest)

    • @param {string} functionName - 要执行的目标页函数名

    该函数在 prepareForAction 函数里使用,可以在当前视图执行目标视图的函数,后面传递的参数会全部传给目标函数。

    应用场景示例:

    • 假设有视图 A 和视图 B, 视图 A 要跳转到视图 B, 但是要先判断下视图 B 所需要的数据是否请求成功再进行跳转,便可使用该方法。

    具体使用请参见 HOW-TO部分 的如何传递弱数据及根据目标页结果控制是否切换视图

  • models

    Cyra.models 的一个引用

  • actions

    当前页面定义的 action 存储到这里,默认是个空对象。

    例如,可以在page里这样定义:

    this.actions['goToResult'] = Cyra.defineAction(this.id(), 'result');

    具体参见 HOW-TO部分 的如何利用 action 跳转

  • context

    context是个全局共享的对象,默认有如下属性:
    
    • @property {string} root - 全局容器的css选择器
    • @property {Element} rootContainer - 全局容器
    • @property {Action} currentAction - 当前动作
    • @property {boolean} isBack - 是否正在执行回退页面的action

    如若有数据需要全局共享,可以放在这里面

  • container

    当前 page 的容器,会根据用户定义的 id 函数自动生成一个唯一的 html 容器。

  • transferedData

    其他页面传递过来的强数据在这里访问。具体参见 HOW-TO部分 的如何传递强数据

HOW TO

如何利用 Action 跳转?

完整代码参见 examples-action 在线演示 online-action

  • 首先,在 main.js 定义路由及初始化.
  • 然后在 view/home.js 里定义 action,并在需要定义跳转的时候执行 action。

home.js

class HomePage extends Page {
 
    id () {
        return 'home';
    }
 
    // 在此处定义跳转动作
    defineActions () {
        this.actions['goToResult'] = Cyra.defineAction(this.id(), 'result');
    }
 
    willAppear (next) {
 
        this.container.innerHTML = `
            <h2>Home</h2>
            <button id='toResult'> goToResult </button>
            `;
 
        let toResultBtn = this.container.querySelector('#toResult');
        toResultBtn.addEventListener('click', () => {
            this.performAction(this.actions['goToResult']);
        })
 
        next();
    }
 
    // ...
}

如何传递强数据?

完整代码参见 examples-solidData 在线演示 online-solidData

传递强数据只需在 performAction 的时候,把数据传入第二个参数。

home.js

class HomePage extends Page {
 
    id () {
        return 'home';
    }
 
    // 在此处定义跳转动作
    defineActions () {
        this.actions['goToResult'] = Cyra.defineAction(this.id(), 'result');
    }
 
    willAppear (next) {
 
        this.container.innerHTML = `
            <h2>抽奖</h2>
            <button id='draw'> 抽奖 </button>
            `;
 
        let drawBtn = this.container.querySelector('#draw');
        drawBtn.addEventListener('click', () => {
            this.performAction(this.actions['goToResult'], {gift: 'iphone', price: 1000});
        })
 
        next();
    }
 
    // ...
}

然后便可在跳转的目的页通过 transferedData 属性访问到这些数据

result.js

class ResultPage extends Page {
 
    id () {
        return 'result';
    }
 
    willAppear (next) {
 
        let data = this.transferedData;
 
        this.container.innerHTML = `
            <h2>结果</h2>
            <div class='info'>奖品: ${data.gift}</div>
            <div class='info'>价格: ${data.price}</div>
            `;
 
        next();
    }
 
    // ...
}

这些数据要求是 key, value 的对象,其中 value 不能为对象或者数组。因为会转成字符串放在 URL 上,所以不要存储大量数据。(数据放在 URL 上所以我们支持任意页面刷新,传递的强数据都不会丢失)

如何自定义强数据在 URL 上编码格式?

完整代码参见 examples-solidData 在线演示 online-solidData

对于如何传递强数据的示例中,数据最终会以这种形式 /#result/gift=iphone,price=1000 存到 URL 中,其中 result 为该视图对应的 route 路径

如若想要改变 URL 上数据编码形式,可以如示例中一样,在 initApp 自行定义

main.js

Cyra.initApp({
    root: 'body',    // the root container selector
    default: 'home',  // default path
    dataSplit: {
        start: '&',
        key: '=',
        item: '|'
    }
});

这样得到的编码格式为 #result&gift=iphone|price=1000

注意:

  • 由于 URL 长度限制,请不要通过此种方法传递大量数据
  • 数据会存储到 URL 上,数据类型都会转为字符串形式,如 true 会变为 'true'

如何传递弱数据?

完整代码参见examples-weakData 在线演示 online-weakData

弱数据是在 prepareForAction 里进行传递,prepareForAction 函数是在当前视图每次调用 performAction 前执行

home.js

class HomePage extends Page {
 
    id () {
        return 'home';
    }
 
    // 执行跳转动作前(performAction)会执行
    prepareForAction (next, action, destinationPagePerform) {
 
        // 传递弱数据, 目标页定义相关函数进行处理
        let drawTimes = destinationPagePerform('incDrawTimes', {from: 'home'});
        next();
 
   }
 
    // ...
}

perpareForAction 函数会有三个参数:

  1. next: 控制是否执行 perfromAction,不调 next 则相当于阻止了 performAction,不会切换视图
  2. action: performActon 中的 Action 实例,即当前执行的 action
  3. destinationPagePerform: 提供一种执行 action 的目标视图中的函数的能力,第一个参数为要执行的函数名,其余为函数参数,会全部传给对应函数。

上述示例中发生的 action 的目标视图是 result,执行的函数 incDrawTimes 也定义在这里:

result.js

class ResultPage extends Page {
 
    id () {
        return 'result';
    }
 
    incDrawTimes (obj) {
 
        if(obj.from === 'home') {
            if(this.drawTimes) {
                this.drawTimes++;
            } else {
                this.drawTimes = 1;
            }
            // udpate dom
            let timesDiv = this.container.querySelector('#times');
            if(timesDiv) {
                timesDiv.innerHTML = this.drawTimes;
            }
 
        }
 
        return this.drawTimes;
    }
 
    // ...
}

执行的函数的返回值也可以在 home.js 里访问到,因此可以提供这样一种特性,当前页可以现根据目标页某个函数的执行结果来判断是否要阻止当前的视图跳转。

如何根据目标视图初始化结果决定是否切换视图?

完整代码参见 examples-weakData 在线演示 online-weakData

在弱数据传递示例中,可以看到当要跳转到一个页面之前,可以在 prepareForAction 中定义目标页要执行的操作。 同时,目标页这些操作的结果可以返回给当前页。

对于弱数据传递实例稍作修改,即可根据目标页的执行结果控制是否切换视图。

home.js

class HomePage extends Page {
 
    id () {
        return 'home';
    }
 
    // 执行跳转动作前(performAction)会执行
    prepareForAction (next, action, destinationPagePerform) {
 
        // 传递弱数据, 目标页定义相关函数进行处理
        let drawTimes = destinationPagePerform('incDrawTimes', {from: 'home'});
 
        // 可根据函数返回值控制是否跳转过去, 未调 next 函数则不会切换视图
        if(drawTimes < 5) {
            next();
        } else {
            let tipDiv = this.container.querySelector('#tip');
            tipDiv.style.display = 'block';
        }
   }
 
    // ...
}

如何设计视图切换动画?

完整代码参见 examples-animation 在线演示 online-animation

只需在 initApp 时候传入相应的处理函数,便可自定义视图切换动画

Cyra.initApp({
    root: 'body',    // the root container selector
    default: 'home',  // default path
    pageTransition: {
        fromPageDisappear (next) {
            next();
        },
 
        toPageAppear (next) {
 
            // 可以检测到当前动作,可根据不同情况做出不同动画
            let currentAction = this.context.currentAction;
            if(currentAction && currentAction.fromPageID === 'result') {
                this.container.style.display = 'block';
                this.container.className = 'fadeIn';
                next(true);  // 传入true告知系统不用再执行默认处理
            } else {
                next();
            }
        }
    }
});

注意:

  • 执行 action 时系统默认会把当前视图隐藏,新的视图展现,在定义动画时候如果已经处理了视图容器的隐藏和展现,调用 next 函数时传入 true,便不会执行默认行为(更改 display 属性)
  • fromPageDisappear 是控制当前要离开的视图的动画效果,在这个函数里,this 指代的是要离开的视图,可以通过 this.container 访问到视图容器; toPageAppear 是控制当前要进入的视图的动画效果, 在这个函数里 this 指代的是要进入的视图。

如何控制浏览器后退流达到定向后退?

完整代码参见 examples-redirect 在线演示 online-redirect

假设我们有三个视图 A B C, 浏览顺序为 A => B => C, 视图 B 是一个中间处理视图页, 当从视图 C 用户点击浏览器回退,我们并不想让用户再经过一遍 B, 这个时候,我们的路由重定向功能可以解决这个问题。而且只需一行代码。

main.js

// routes
let routeHome   = Cyra.defineRoute('home', home);
let routeTmp    = Cyra.defineRoute('tmp', tmp);
let routeResult = Cyra.defineRoute('result', result);
 
// backRoutes
Cyra.defineBackRouteRedirect(routeResult, routeTmp, routeHome);

示例中,当我们在 result 视图中点击浏览器回退, 本来上一个视图应该是 tmp 视图,但是由于定义了这个重定向路由,会直接到 home 视图。

如何控制是否重新加载?

完整代码参见 examples-reload 在线演示 online-reload

视图生命周期 这一节中讲过,当第二次进入一个视图时, 生命周期函数 initialize, willAppear 是不会再次执行的。一般建议这两个周期要做的事为获取数据,渲染 DOM,事件绑定等只需第一次进入视图时需要执行的逻辑。

但是也有例外情况,如果页面每次进入都要求重新获取数据渲染 DOM 等, 那么是有必要每次进入页面都执行这两个函数的。所以,我们提供一种方法,可以完全控制这种特殊情况。只需在需要强制渲染的视图里定义 shouldReload 函数即可。如下:

reload.js

class ReloadPage extends Page {
 
    id () {
        return 'reload';
    }
 
    // 控制页面是否重载,即当第二次进入视图是否再次执行initialize, willAppear
    shouldReload (currentAction) {
 
        if(currentAction.fromPageID === 'home') {
            return true;
        } else {
            return false;
        }
    }
 
    // ...
}

甚至可以根据当前跳转动作, 不同的视图进入到当前视图执行不同的逻辑。这一切需要做的只是在需要重新渲染(即每次都执行 initialize, willAppear)的情况下,该函数返回 true 即可。

如果需要重载的页面比较多,可以直接在initApp中更改默认的配置(默认为每个页面均不重载),如下:

Cyra.initApp({
    root: 'body',    // the root container selector
    default: 'home',  // default path
    alwaysReload: true   可以通过这种方式改变页面重载的默认配置
});

同理,有特殊情况,也可在页面的 shouldReload 函数返回 false 进行处理

如何在 ES5 的项目中使用这个库?

完整代码参见 examples-helloEs5 在线演示 online-helloEs5

之前的代码示例是由 ES6 编写,那么如何在 ES5 的项目中使用这个库呢?

在 ES6 中我们是通过继承机制扩展视图,这种模式非常灵活,提供了很好的抽象, 用户甚至可以在框架的基础上编写自己的基础视图类, 使代码更加简洁。为了 ES5 用户更方便的使用,我们提供了一个函数,代码示例如下:

home.js

Cyra.extend(new Page('home'), {
 
    initialize: function (next) {
        next();
    },
    willAppear: function (next) {
        this.container.innerHTML = 'Hello World';
        next();
    },
    didAppear: function (next) {
        next();
    },
    willDisappear: function (next) {
        next();
    }
})

注意:

  • 视图 id 直接放在new Page() 中,不用编写 id 函数。
  • extend 函数接受两个参数, 示例中会把扩展后的 page 实例返回

Readme

Keywords

Package Sidebar

Install

npm i cyra-pure

Weekly Downloads

6

Version

1.3.8

License

ISC

Last publish

Collaborators

  • chenjiahan
  • zsky