一款轻量级的mvvm前端框架,支持es6 class 的面向对象方式进行开发,让你从繁琐的dom操作中解放,并提高代码的可读性,支持IE9+以上浏览器。
对于mvvm框架,vue大家肯定不陌生,既然有了这么优秀的vue,那为啥还需要开发这款框架呢?
-
vue固然是强大的,但有时候我们做一些简单的页面,如纯展示的页面,只是少量的表单需要填写(如收集资料),这个时候你若引入vue来做数据绑定,就显得杀鸡用牛刀了(不,这是一把青龙偃月刀)。
-
基于我们现有的脚手架已经集成了jquery,如果再加上vue的话,体积会变得更大,那会影响页面加载的速度。
-
easyPage对于服务器端渲染的页面,可以使用反向赋值。什么是反向赋值,就是你在前端里面定义的变量,可以自动读取服务器端渲染的内容进行赋值。结合egg框架是非常有利的。
-
easyPage使用es6 class 的方式进行开发,如果你想尝试使用面向对象+组件式的开发体验,这款框架你一定不能错过。
假如你的页面并复杂,特别是活动页,频道页,页面交互并不复杂,特别是多个页面之间并没有太大的关联。这时你可以easyPage来加快你的开发效率。若你一开始用的jquery,后来想加入类似vue的数据驱动功能,但不想引入整个vue,此方案亦可。后期有需要可以改成不依赖jquery的,这样连jquery也不需要引入,对于移动端项目那是非常轻量了。
npm i @koicarp/easypage or yarn add @koicarp/easypage
下面举一个简单的输入框例子,演示数据驱动
<div id="app">
<input type="text" v-model="message" /> <button v-on="click=reset()">重置</button>
<div v-text="message"></div>
</div>
import { EasyPage, $init } from '@koicarp/easypage';
@$init({
// 需要编译的节点
$el: '#app'
})
export default class extends EasyPage {
message = '';
inited () {
console.log('------ 组件 初始化完成');
}
reset () {
this.message = '';
}
}
代码就是这么简单,就完成了双向绑定。在es6 的class中声明了一个变量message
,在页面中使用v-text="message"
把需要绑定的dom元素标记起来,一般message有变化,元素内的文本就会随之变化。v-on="click=reset()"
这个表示元素触发click事件会调用class中的reset方法,看到v-on这个属性就知道是事件绑定,跟vue的事件绑定写法略有不同,注意不要写错了。class中还定义了一个inited
方法,这个表示组件初始化完成的钩子函数,暂时也就只有这个钩子,后期有需要可以添加更多。
可以看到代码里面有这么一行export default class extends EasyPage
,直接导出一个class,脚手架可以自动读取并实例化,这个也是这个页面的入口class。
easyPage的用法跟vue非常相似,假如你有vue的基础,10分钟即可完成掌握。
假如现在页面已经在node端渲染好了,能否有办法把渲染好的内容赋值到class中的定义好变量。当然,我们用jquery来遍历dom元素,然后逐个赋值给变量这也没问题,不过我们有更加优雅的方式。下面演示一段egg渲染+反向赋值的例子:
controller:
async homeIndexPage () {
const ctx = this.ctx;
const userInfo = { id: 4, name: 'Baron' };
const productList = [{ id: 1, title: '小米手机' }, { id: 2, title: '华为手机' }];
await ctx.render('home/home.html', { userInfo, productList });
}
html:
<div id="app">
<h2 v-data="userInfo.id={{ userInfo.id }}" v-text="userInfo.name">{{ userInfo.name }}</h2>
<ul>
{% for item in productList %}
<li v-array="productList">
<span v-data="productList[{{ loop.index0 }}].id={{ item.id }}" >{{ loop.index0 }}</span>
.
<span v-text="productList[{{ loop.index0 }}].title">{{ item.title }}</span>
</li>
{% endfor %}
</ul>
</div>
egg渲染以后:
<div id="app">
<h2 v-data="userInfo.id=4" v-text="userInfo.name">Baron</h2>
<ul>
<li v-array="productList">
<span v-data="productList[0].id=1" >0</span>
.
<span v-text="productList[0].title">小米手机</span>
</li>
<li v-array="productList">
<span v-data="productList[1].id=2" >1</span>
.
<span v-text="productList[1].title">华为手机</span>
</li>
</ul>
</div>
反向赋值就是通过[v-text], [v-array], [v-data], [v-model]
这几个属性来标记起来,然后easyPage会读取这部分的内容赋值到class中的变量中。v-text会把元素内的文本赋值给属性内的变量,如userInfo.name
,然后执行userInfo.name = 'Baron'
,v-model就会读取input中的value属性然后赋值,这很容易理解。主要讲下v-array和v-data。
easyPage会读取v-array中的值,然后遍历v-array元素内的子元素,如上面的v-array所在的li子元素中有v-text="productList[0].title"
,则会执行productList[0].title='小米手机'
,v-array会先初始化数据的数量,然后才会执行赋值。
而v-data就是直接执行里面的表达式,如productList[1].id=2
,一般用在一些没有显示在页面上的内容,可是你又确实需要用到的变量。
规定:
1.反向赋值的变量,必须要在前端class中先声明好变量名,这样也有利于代码的可读性。
2.数组的反向赋值,需要用v-array进行标记,用于初始化数量。
在class中声明变量:
// 一定要先声明变量,不然赋值时找不到变量名会报错
userInfo = {
id: '',
name: ''
};
productList = [];
constructor () {
// 构造函数中,通过isAssign=true开启反向赋值
super({ isAssign: true });
}
初始化完后:
easyPage支持对一段HTML代码进行编译,然后插入到页面中,我们可以利用此功能做一些动态dom的插入,并支持数据的响应,如弹窗、ajax获取数据后动态渲染html等。
html模板:
<!-- template.html -->
<div class="content">
<h2 v-text="userInfo.name"></h2>
<ul>
<li v-for="(item, index) in productList">
<span v-text="index"></span>
.
<span v-text="item.title"></span>
</li>
</ul>
</div>
js部分:
import template from './template.html';
// 变量声明
userInfo = { id: 4, name: 'Baron' };
productList = [{ id: 1, title: '小米手机' }, { id: 2, title: '华为手机' }];
// ...
// $compile 方法用于编译html
insert () {
const html = this.$compile(template);
this.$el.find('.main').append(html);
}
// ...
主体页面html:
<div id="app">
<button v-on="click=insert()">插入html</button>
<div class="main"></div>
</div>
效果如下:
easyPage支持通过es6 class + html模板的方式来封装组件。跟动态模板编译类似,只是在声明class的时候通过es6的装饰器来将class和template进行绑定,同时也支持父组件传递数据到子组件。
修改了一下上面的html模板:
<div class="content">
<h2 v-text="userInfo.name"></h2>
<div><button v-on="click=addItem()">添加</button></div>
<ul>
<li v-for="(item, index) in productList">
<span v-text="index"></span>
.
<span v-text="item.title"></span>
</li>
</ul>
</div>
子组件:
import { EasyPage, $init, $on } from '@koicarp/easypage';
import template from './template.html';
@$init({
template: template
})
class Content extends EasyPage {
// 父组件传入到子组件的属性,需要显式声明
static propTypes = {
productList: Array
}
userInfo = { id: 4, name: 'Baron' };
constructor (prop) {
// EasyPage的构造函数中的prop参数为接收父组件传入的属性
super({ prop });
}
addItem () {
// 可以使用$emit来调用父组件的方法
this.$emit('addItem');
}
show (param) {
console.log(`打印: ${param}`);
}
}
父组件js:
insert () {
// $mount把子组件Content挂载到父组件中,其中this参数为父组件的this引用
// $mount方法第三个参数为选项,若传入name参数,这样父组件可以通过this.$children[name]来访问子组件中的方法
new Content({ productList: this.productList }).$mount(this, this.$el.find('.main'), { name: 'content' });
// 调用子组件的方法
this.$children.name.show(1111); // 打印:1111
}
// 通过$on装饰器,把addItem方法声明为自定义事件,其中'addItem'参数为事件名称
@$on('addItem')
addItem () {
this.productList.push({ id: 3, title: '诺基亚手机' });
}
效果如下:
从上面的案例可以看到父组件把this.productList
传入到子组件中,并且通过$emit
来跟父组件进行通信。除此以外,还可以通过在子组件中的this.$parent
来访问父组件的this引用,这样就可以调用this.$parent.addItem()
达到同样效果。
若在$mount
方法中传入name
参数,这样父组件可以通过this.$children[name]
来访问子组件中的方法。
规定:
子组件不能修改父组件中传入的prop,保证单向数据流。
父子组件间(甚至是兄弟组件)的通信可以通过$on和$emit来实现,具体api请看下面。
动态组件不支持反向赋值。
在easypage里,子组件的prop属性必须要显式声明才有效果,不然父组件传进来也不作数。prop显式声明能让组件的使用者清楚有哪些选项可用,传递的参数类型和说明等。 对于组件的开发者也更加有利于后期的维护,代码的可读性更高。
// 子组件
class Content extends EasyPage {
// 一定要用 static 静态类型来定义
static propTypes = {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
propA: String,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 不做类型检测,只有默认值
propE: {
default: []
}
};
constructor (prop) {
// EasyPage的构造函数中的prop参数为接收父组件传入的属性
super({ prop });
}
}
// 父组件
class Parent extends EasyPage {
dataA = 'dataA';
propB = ['hello'];
inited () {
// 当父组件和子组件传递的属性名不一致时,通过['父组件属性名:子组件属性名']来作为映射,若同名时,只需要传入子组件属性名即可
new Content({ 'dataA:propA': this.dataA, propB: propB });
}
}
代码进行一轮优化以后,已经支持数据和对象的直接赋值,使用Object.defineProperty+es6的Proxy对象进行处理。只是要注意一点,就是数组的concat方法:
// 一开始声明的属性
this.list = ['a', 'b', 'c'];
// 错误的做法,concat之后不能赋值给原本的变量,会报错
this.list = this.list.concat(['d']);
// 正确做法,等价于上面
this.list.push(...['d']);
代码示例在 https://github.com/baronhuang/egg-multiple-page-example 项目上,访问路径:/demo/easypage
$compile (template)
-
参数:
- {string|HTMLElement} template 需要编译的模板,可以是html元素
-
用法:
编译html模板,使其变成可供框架操控的元素。
const html = this.$compile(template);
this.$el.find('.main').append(html);
$mount (parentThis, slot, type = 'append')
- 参数:
- {Object} parentThis 父组件的this引用
- {HTMLElement} slot 父组件中的挂载点元素
- {Object} options 选项
- {string} options.type 挂载的方式,append(默认): 挂载到组件内部, replace: 替换挂载点
- {string} options.name 组件的名称,父组件可通过this.$children[name]来访问
- 用法: 把子组件挂载到父组件。
new Content({ productList: this.productList }).$mount(this, this.$el.find('.main'));
$on (event, callback)
- 参数:
- {string} event 事件名称
- {Function} callback 事件回调
- 用法: 自定义事件,可以用于子组件通信父组件,或者是兄弟组件通信。
this.$on('addItem', this.addItem);
$emit (event, data = null)
- 参数:
- {string} event 事件名称
- {*} data 需要传入事件回调的参数
- 用法: 触发自定义事件。
this.$emit('addItem');
v-text
把数据渲染成dom元素的文本。支持反向赋值。
<span v-text="message"></span>
v-for
常用用于数组中,遍历数组属性,根据数组中的字段渲染dom。其中item=数组中的元素,index=数组索引。记得里面的括号()
不能丢。
<li v-for="(item, index) in productList">
<span v-text="index"></span>
<span v-text="item.title"></span>
</li>
v-array
仅用于反向赋值。读取服务器端已经渲染好的HTML,根据子元素的v-text
,v-model
,v-data
等属性对组件class声明好的变量进行赋值。
{% for item in productList %}
<!-- v-array的值就是需要赋值的变量名 -->
<li v-array="productList">
<!-- 利用nunjunks for循环内置的变量来获取当前索引 -->
<span v-data="productList[{{ loop.index0 }}].id={{ item.id }}" >{{ loop.index0 }}</span>
<span v-text="productList[{{ loop.index0 }}].title">{{ item.title }}</span>
</li>
{% endfor %}
v-data
执行赋值表达式。v-data里面的值一定是赋值表达式,如:userInfo.id = 4
,可以利用此方法来做反向赋值。也可以用做临时变量,如v-class={orange: index==0}
,需要一个临时变量index,可以这样定义var index=0
,这样在v-class中就能获取到。多个变量赋值用,
隔开。
<div v-data="userInfo.id=4"></div>
v-class
根据v-class里面的对象进行匹配,如v-class="{orange: color=='orange', green: color=='green'}"
,若color的值等于'orange',则元素的class等于orange,非常好理解。注意的是非编译的情况是不会执行这个指令的。
<!-- color='orange'时,class="orange" -->
<div v-class="{orange: color=='orange'}" >橙色</div>
v-attr
根据组件的变量给dom元素添加属性,如href
、src
等一切属性。
<a v-attr="{href: 'product/' + product.id}">链接</a>
<img v-attr="{src: product.path}" />
v-model
只对于input和textarea标签有效。用法跟vue的v-model一致,数据的双向绑定。
v-on
对dom元素添加绑定事件。跟vue的v-on类似,写法上稍有不同。
<!-- 给按钮添加click事件,调用组件的addItem方法,可以传入event参数,让方法拿到事件源 -->
<div><button v-on="click=addItem()">添加</button></div>
v-show
v-show里面的表达式可用于操作dom元素的隐藏显示。注意要先定义好元素是隐藏的css,如display: none
,不然服务端渲染后会先看到dom,才会隐藏。
<!-- 元素默认是隐藏,当isShow==true时,才会显示 -->
<button v-on="click=addItem()" v-show="isShow">添加</button
inited()
当组件初始化完成(执行完所有的数据监听,模板的编译等)后执行。最好把你应用的初始化方法(如ajax请求)放在这里执行。
export default class extends EasyPage {
inited () {
console.log('------ 组件 初始化完成');
}
}
组件的参数配置都是通过构造函数里的super方法传入。
isAssign
- 类型:boolean,默认值false
- 作用:通过此参数来开启反向赋值。一般用在根组件,其他组件不建议开启(因为开了也没用)。
export default class extends EasyPage {
constructor () {
super({ isAssign: true });
}
}
prop
- 类型:object
- 作用:父组件可以通过prop属性传入数据到子组件
// 子组件定义
export Sub extends EasyPage {
constructor (prop) {
super({ prop });
}
}
// 父组件中
new Sub({ productList: this.productList });
内置装饰器可直接通过import { $init, $on } from '@koicarp/easypage';
这种方式来使用。
$init({ $el, exposeName, template })
-
参数:
- {HTMLElement} $el 需要监听的根元素
- {string} exposeName 组件名称,名称具有全局性,设置后可以使用window[exposeName]进行访问
- {string} template 可以通过模板的方式来初始化easyPage的内容,$el和template同时存在时,template优先级更高
-
用法:
组件class的初始化参数。
@$init({
$el: '#app',
exposeName: appPage
})
export default class extends EasyPage {
// ..
}
$expose()
-
用法:
定义当前方法是否暴露到window对象中。
export default class extends EasyPage {
// 可以通过window[addItem]来访问
$expose
addItem()
}
$event(selector, event, childSelector, data)
-
参数:
- {string} selector 事件的触发元素,jquery选择器方式
- {string} event 事件名称
- {string} childSelector 事件代理中的子元素
- {*} data 需要传输的数据,放在event.data上
-
用法:
给dom元素添加事件,并触发class中的方法,跟jquery的.on()方法参数一致
@$event(document, 'click', '.home-page .show-tips', { msg: 'ok' })
showTips (event) {
tips('提示成功');
}
$on(event)
-
参数:
- {string} event 事件名称
-
用法:
把方法添加到自定义事件中,等价于this.$on(event, callback)方法。
// 可通过this.$emit('addItem');
@$on('addItem')
addItem () {
this.productList.push({ id: 3, title: '诺基亚手机' });
}
$debounce(time)
-
参数:
- {number} time 多长时间以后执行原函数
-
用法:
防抖函数
$throttle(time)
-
参数:
- {number} time 多长时间内只触发一次函数
-
用法:
节流函数