
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
欢迎使用 Cyra
Cyra 是一个 JS 移动端单页视图容器。
Cyra 的设计是为了简化移动端单页应用(SPA)的开发,提供一个基础的框架结构以及环境管理,使用户能够集中精力在应用开发上。
三个简单的概念 Page, Route 和 Action 来帮助开发者处理复杂的视图切换逻辑、数据传递及视图生命周期管理。
Cyra 不仅仅做了这些,她更多的是向开发者传递一种架构思想,帮助用户构建出更加健壮的移动端单页应用。
几乎任何称得上 SPA 的项目都可以使用 Cyra,在大型复杂单页应用上 Cyra 的优势会更为突出。
文档导航
安装:
npm i cyra --save
引用:
import { Cyra } from 'cyra'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 API 及 LocalStorage 等技术
同时也希望用户在使用这些技术的时候保持谨慎
在 Cyra 的架构中,根据大量的历史开发经验,应用以下几个重要的思想:
显式管理视图生命周期以及所有视图操作
提供视图容器,使视图之间最大程度隔离
一切都是钩子
Page 表示一个视图,书写一个视图所需要的所有执行阶段都在 Page 中有相应的钩子
Action 表示一个导航动作,你只需要在其中定义从哪里出发,到哪里去,应用在启动的时候便可以获悉所有的视图及其可能跳转去的地方
Route 路由,表示 URL 及视图对象的对应关系
强数据 指从一个视图跳转到另外一个视图的时候不能丢失的数据,这些数据会放在URL上,即使视图刷新也不会丢失
实现使用 performAction
弱数据 指在一个视图的展现,依赖另外一个视图传递过来的数据的时候,可以传递弱数据,用户可以判断是否存在弱数据,并执行相应的逻辑
实现使用 prepareForAction 中的 destinationPagePerform
使用 TypeScript 编写,可以编译成 ES3, ES5, ES6。并可以在 TypeScript 项目中直接使用。
使用 CommonJS 模块化规范,但同时也可以编译成 AMD 模块化规范使用。
本身代码非常精简。大小为:7.93 kB GZIP后:2.6 KB
先看生命周期图:

提倡组件层面抽象,减少视图抽象
将视图渲染、数据获取等操作放到视图中最合适的阶段中
强数据传递尽量传递查询参数以保持视图独立,并保证 URL 不会太长
弱数据传递需要做好足够的判断,以保证逻辑的健壮
defineRoute (path: string, page: Page): Route
该函数根据匹配路径和 视图实例(page 对象)定义一个路由并返回。匹配路径是根据 URL 后面的 hash 值判断的, 比如 index.html#home, 该 URL 匹配的路径为 home。当 URL 匹配路由的路径,则进入相应视图。
defineBackRouteRedirect (fromRoute: Route, toRoute: Route, redirectToRoute: Route): BackRoute
该函数定义一个重定向路由,具体使用请参见 HOW-TO部分 的如何定向后退
defineAction (fromPageID: string, toPageID: string, isBack: boolean = false): Action
该函数根据离开的视图 ID 和 要进入的视图 ID 定义一个 Action 并返回,Action 的含义主要是视图切换。具体使用请参见 HOW-TO部分 的如何定义一个 action
initApp (obj): void
{
root: '#container' // spa 应用所在容器的选择器,默认为 'body'
default: 'home' // 当 hash 值为空时的默认路径
}
在这个函数里传入初始化单页应用需要的一些值。
models
存储所有的Page,Action,Route,BackRoute实例的引用。 初始数据结构:
{
pages : {},
actions : [],
routes : [],
backRoutes : []
};
extend (to, from)
注解: 该方法只是帮助 ES5 用户在不使用继承机制的情况下扩展 page 的方法
Cyra.extend(new Page('home'), {
initialize: function (next) {
next();
}
// ...
});
ES6 或者 Typescript 用户请直接使用继承语法
class HomePage extends Page {
id (): {
return 'home'
}
initialize: function (next) {
next();
}
// ...
}
performAction (action: Action, data?: any): void
执行一个定义好的 Action, 在当前视图中只能执行以当前视图为起点的 Action
prepareForAction (next: Function, action: Action, destinationPagePerform: Function): void
注解: 该方法不用主动调用,只需定义在page里,当执行 performAction 时会先自动执行该方法
destinationPagePerform (functionName: string, ...rest)
该函数在 prepareForAction 函数里使用,可以在当前视图执行目标视图的函数,后面传递的参数会全部传给目标函数。
应用场景示例:
具体使用请参见 HOW-TO部分 的如何传递弱数据及根据目标页结果控制是否切换视图
models
Cyra.models 的一个引用
actions
当前页面定义的 action 存储到这里,默认是个空对象。
例如,可以在page里这样定义:
this.actions['goToResult'] = Cyra.defineAction(this.id(), 'result');
具体参见 HOW-TO部分 的如何利用 action 跳转
context
context是个全局共享的对象,默认有如下属性:
如若有数据需要全局共享,可以放在这里面
container
当前 page 的容器,会根据用户定义的 id 函数自动生成一个唯一的 html 容器。
transferedData
其他页面传递过来的强数据在这里访问。具体参见 HOW-TO部分 的如何传递强数据
完整代码参见 examples-action 在线演示 online-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 上所以我们支持任意页面刷新,传递的强数据都不会丢失)
完整代码参见 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
注意:
完整代码参见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 函数会有三个参数:
上述示例中发生的 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();
}
}
}
});
注意:
完整代码参见 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 进行处理
完整代码参见 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();
}
})
注意:
FAQs
single page application view engine
The npm package cyra-pure receives a total of 9 weekly downloads. As such, cyra-pure popularity was classified as not popular.
We found that cyra-pure demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.