![]() |
||
---|---|---|
.. | ||
README.md |
README.md
精通 Omix 框架
《精通Omix框架》书籍的免费电子版,有这一篇足够从入门到精通。
- 简介
- 安装Omix
- Hello Omix
- 介绍 JSX
- 组件与通讯
- 生命周期
- 插槽
- 表单
- 继承
- 获取DOM节点
- 插件体系
- closeScopedStyle
- 调试技巧
- Server Side Rendering
- Omix相关
简介
Omix(读音 / [ˈomɪkɜ:s] /, 汉字类似于 欧米可思) 是一款使用 JSX 创建Web用户界面的组件化框架,故得名:Omix。
其代码已托管至Github地址: https://github.com/AlloyTeam/omix
Omix框架 并不是用来替代 Omi框架 ,而是另外一种选择。概括起来包含下面优点和特性:
- 超迅捷的性能,经过测试, 居然是所有框架第一名, 不信可以看看dbmon with omix。虽然排第一,但是发现还有许多优化空间!!
- 良好的兼容性 - 兼容 IE8,兼容各种手机平版移动 Web 程序
- 超小的尺寸 - 7 kb (gzip)
- 内置支持 JSX 和 hyperscript , 喜欢哪种方式随意切换。需要注意 omix 里写 JSX 组件标签要小写
- 局部CSS - HTML+ Scoped CSS + JS组成可复用的组件。不用担心组件的CSS会污染组件外的
- 更自由的更新 - 每个组件都有 update 方法,自由选择你认为最佳的更新时机和最佳的更新方式
- 灵活的插件体系和丰富的插件生态
- omi-router : Omi专属的官方Router插件.
- omi-finger Omi的AlloyFinger插件,支持各种触摸事件和手势
- omi-transform Omi的transformjs插件,快速方便地设置DOM的CSS3 Transform属性
- omi-touch Omi的AlloyTouch插件,Omi项目的触摸运动解决方案(支持触摸滚动、旋转、翻页、选择等等)
安装 Omix
npm install omix
你也可以通过cdn获取omi,并在HTML里声明script变迁:
<script src="https://unpkg.com/omix@0.1.0/dist/omix.min.js"></script>
你也可以 Omi Cli 来初始化你的项目脚手架,它会帮你处理好一切构建相关的东西。
npm install omi-cli -g //安装CLI
omi init-x [project name] //初始化项目
cd your_project_name //转到目录
npm start //开发
npm run build //生成发布文件
Hello Omix
import Omi from 'omix';
class Hello extends Omi.Component {
render() {
return <div> Hello <h1 style="display:inline-block;">{this.data.name}</h1>!</div>
}
}
class App extends Omi.Component {
install() {
this.name = 'Omi'
}
handleClick(e) {
this.name = 'Omix'
this.update()
}
style() {
return `h3{
color:red;
cursor: pointer;
}`
}
render() {
return <div>
<Hello name={this.name}></Hello>
<h3 onclick={this.handleClick.bind(this)}>Scoped css and event test! click me!</h3>
</div>
}
}
Omi.render(new App(), '#container')
- 组件继承自
Omi.Component
, render
返回的是组件JSX 或者 hyperscriptstyle
返回的是组件的CSS,是局部的,不会污染组件以外的元素- 通过
name={this.name}
把传递给子组件hello,子组件直接在render里可以使用{this.data.name}来使用传递来的数据 - 通过
onclick={this.handleClick.bind(this)}"
给HTML元素绑定事件,在handleClick回调中可以拿到event对象
介绍 JSX
我们来观察一下声明的这个变量:
const element = <h1>Hello, world!</h1>;
这种看起来可能有些奇怪的标签语法既不是字符串也不是HTML.
它被称为 JSX, 一种 JavaScript 的语法扩展。JSX 乍看起来可能比较像是模版语言,但事实上它完全是在 JavaScript 内部实现的,和 hyperscript 可以互相转换。
在 JSX 中使用表达式
你可以任意地在 JSX 当中使用 JavaScript 表达式,在 JSX 当中的表达式要包含在大括号里。
例如 2 + 2
, user.firstName
, 以及 formatName(user)
都是可以使用的。
function formatName(user) {
return user.firstName + ' ' + user.lastName;
}
const user = {
firstName: 'Harper',
lastName: 'Perez'
};
const element = (
<h1>
Hello, {formatName(user)}!
</h1>
);
我们书写 JSX 的时候一般都会带上换行和缩进,这样可以增强代码的可读性。与此同时,我们同样推荐在 JSX 代码的外面扩上一个小括号,这样可以防止 分号自动插入 的bug.
JSX 本身其实也是一种表达式
在编译之后呢,JSX 其实会被转化为普通的 JavaScript 对象。
这也就意味着,你其实可以在 if
或者 for
语句里使用 JSX,将它赋值给变量,当作参数传入,作为返回值都可以:
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}
JSX 属性
你可以使用引号来定义以字符串为值的属性:
const element = <div tabIndex="0"></div>;
也可以使用大括号来定义以 JavaScript 表达式为值的属性:
const element = <img src={user.avatarUrl}></img>;
切记你使用了大括号包裹的 JavaScript 表达式时就不要再到外面套引号了。JSX 会将引号当中的内容识别为字符串而不是表达式。
JSX 嵌套
如果 JSX 标签是闭合式的,那么你需要在结尾处用 />
, 就好像 XML/HTML 一样:
const element = <img src={user.avatarUrl} />;
JSX 标签同样可以相互嵌套:
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
提醒:
Omix 里使用 JSX 更加宽松,你既可以使用
class
也可以使用className
。既可以使用onclick
也可以使用onClick
。
JSX 代表 Objects
转义器 virtual-dom-loader
会把 JSX 转换成一个名为 Omi.x()
的方法调用。
下面两种代码的作用是完全相同的:
const element = (
<h1 class="greeting">
Hello, world!
</h1>
);
const element = Omi.x(
'h1',
{class: 'greeting'},
['Hello, world!']
);
这样的对象被称为 “Omi 元素”。它代表所有你在屏幕上看到的东西。Omi 通过读取这些对象来构建 DOM 并保持数据内容一致。
Tip:
如果你是在使用本地编辑器编写 JSX 代码的话,推荐你去装一个支持 JSX 高亮的插件,这样更方便之后的开发学习。
JSX 例子
// JSX
<ul id="bestest-menu">
{this.data.items.map( item =>
<li class="item" {...attrs(item.id)}>{item.title}</li>
)}
</ul>
// JSX
<MyList>{this.data.items.map(item =>
<MyItem id={item.id} title={item.title} />
)}</MyList>
<MyComponent someProp={{x: 1, y: 2}}/>
组件与通讯
组件体系
Omix框架完全基于组件体系设计,我们希望开发者可以像搭积木一样制作Web程序,一切皆是组件,组件也可以嵌套子组件形成新的组件,新的组件又可以当作子组件嵌套至任意组件形成新的组件...
上面的Hello Omix的例子已经说明了组件大概,这里再使用Todo的例子来讲解 Omix 组件体系的使用。
先定义一个List组件用来展示列表:
class TodoList extends Omi.Component {
render() {
return (
<ul>
{this.data.items.map(item => (
<li key={item.id}>{item.text}</li>
))}
</ul>
)
}
}
- 使用
this.data.items.map
来遍历传入的items,在render方法中就能直接使用该标签。如下所示:
class TodoApp extends Omi.Component {
constructor(data) {
super(data)
this.data = {items: [{id: 1, text: 'Omi'}, {id: 2, text: 'AlloyTeam'}], text: ''}
}
handleSubmit(e) {
e.preventDefault()
this.data.items.push({id: this.data.items.length + 1, text: this.data.text})
this.data.text = ''
this.update()
}
handleChange(e) {
this.data.text = e.target.value
}
render() {
return <div>
<h3>TODO</h3>
<TodoList items={this.data.items}></TodoList>
<form onSubmit={this.handleSubmit.bind(this)}>
<input onChange={this.handleChange.bind(this)} value={this.data.text} />
<button>{'Add #' + (this.data.items.length + 1)}</button>
</form>
</div>
}
}
Omi.render(new TodoApp(), 'body')
父子通讯
import Pagination from './pagination.js'
import Content from './content.js'
...
handlePageChange(index) {
this.content.goto(index + 1)
// or get it by omi-id
// Omi.get('content').goto(index+1)
this.update()
}
render() {
return <div>
<h1>Pagination Example</h1>
<Content omi-name="content" omi-id="content"></Content>
<Pagination
name="pagination"
total={100}
pageSize={10}
numEdge={1}
numDisplay={4}
onPageChange={this.handlePageChange.bind(this)} ></Pagination>
</div>
}
...
- 通过
onPageChange={this.handlePageChange.bind(this)}
传递回调函数给子组件,子组件就可以执行注入的父组件中的方法 - 在pagination组件内部可以通过
this.data.onPageChange
来执行this.handlePageChange
方法 - 可以从上面的例子看到,通过标记
omi-name
或者omi-id
来获取组件的实例。标记omi-id
是全局注册,在程序任何地方可以通过Omi.get
拿到该对象的实例。
selfDataFirst
使用Omi的时候,你会发现,在子组件内部修改父组件传递过来的data,就是修改 this.data 然后进行 this.update 会没有效果。如果你把 data 的属性赋给 this 下,render 方法里只用 this 下的属性,再进行 update 就能成功 update。比如:
install (){
this.name = this.data.name
}
changeName (newName){
this.name = newName
this.update()
}
render (){
return <div>{this.name}<div>
}
如果坚持要用 data 渲染,这里会有个权重问题。Omi的组件 selfDataFirst 默认是 false,也就是代码 父组件传递的属性的权重更高,如果你想要子组件 this.data 的权重更高,可以通过修改 selfDataFirst。如:
<child-element selfDataFirst><child-element>
也可以在构造函数内修改如:
class ChildElement extends Omi.Component {
constructor(data) {
super(data)
this.data.selfDataFirst = true
}
...
当然 selfDataFirst 的原理只是 Object.assign 的顺序不一样,Object.assign 只是浅拷贝,所以如果是引用类型的话,不管修改父组件传递的属性还是子组件自身的 this.data
都是可以修改成功的。最后贴上 Omi 相关的源码方便大家理解:
if (cmi.data.selfDataFirst) {
cmi.data = Object.assign({}, root.properties, cmi.data)
} else {
cmi.data = Object.assign({}, cmi.data, root.properties)
}
生命周期
name | avatars | company |
---|---|---|
constructor | 构造函数 | new的时候 |
install | 初始化安装,这可以拿到用户传进的data进行处理 | 实例化 |
installed | 安装完成,HTML已经插入页面之后执行 | 实例化 |
uninstall | 卸载组件。执行remove方法会触发该事件 | 销毁时 |
beforeUpdate | 更新前 | 存在期 |
afterUpdate | 更新后 | 存在期 |
beforeRender | render函数执行之前 | 存在期和实例化 |
示意图
举个例子
class Timer extends Omi.Component {
constructor(data) {
super(data);
}
install () {
this.data = {secondsElapsed: 0};
}
tick() {
this.data.secondsElapsed++;
this.update();
}
installed(){
this.interval = setInterval(() => this.tick(), 1000);
}
uninstall() {
clearInterval(this.interval);
}
style () {
return `
.num { color:red; }
`;
}
render () {
return <div>Seconds Elapsed:<span class="num"> {this.data.secondsElapsed}</span></div>
}
}
插槽
class NotesList extends Omi.Component {
render() {
return <ol>
{ this.data.children.map(function(child) {
return <li>{child}</li>
})}
</ol>
}
}
class App extends Omi.Component {
render() {
return (
<NotesList a="a" class="sfds">
<span>hello</span>
<span>world</span>
</NotesList>
)
}
}
Omi.render(new App(), 'body')
通过 this.data.children
可以读取到父节点传递过来任意标签。
表单
Omi让一些表单操控起来更加方便,特别是select!
select标签
以前,我们需要像如下的方式选中一个选项:
<select>
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option selected value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
第三个option由于加上了selected,所有会被选中。这样带来的问题就是,开发者写的程序可能要操遍历每个option。而使用Omi,你只需要这样子:
<select value="coconut">
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
这样就能达到同样的效果。比如你想选择第一项:
<select value="grapefruit">
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
是不是非常方便?
举个例子
class FormTest extends Omi.Component {
constructor(data) {
super(data);
}
handleChange(target){
console.log(target.value)
this.data.value = target.value;
}
handleSubmit(evt) {
alert('Your favorite flavor is: ' + this.data.value);
evt.preventDefault();
}
render () {
return `
<form onsubmit="handleSubmit(event)">
<label>
Pick your favorite La Croix flavor:
<select value="{{value}}" onchange="handleChange(this)">
<option value="grapefruit">Grapefruit</option>
<option value="lime">Lime</option>
<option value="coconut">Coconut</option>
<option value="mango">Mango</option>
</select>
</label>
<input type="submit" value="Submit" />
</form>`;
}
}
Omi.render(new FormTest({ value: 'mango' }),'#container');
继承
通过继承机制,可以利用已有的数据类型来定义新的数据类型。所定义的新的数据类型不仅拥有新定义的成员,而且还同时拥有旧的成员。我们称已存在的用来派生新类的类为基类,又称为父类。由已存在的类派生出的新类称为派生类,又称为子类。
举个例子
class Hello extends Omi.Component {
constructor(data) {
super(data);
}
render() {
return <div>Hello {this.data.name}!</div>
}
}
class SubHello extends Hello {
constructor(data) {
super(data);
}
}
Omi.render(new SubHello({ name : 'Omi' }),'#container');
使用 Omix ,组合或者继承都可以。
获取DOM节点
虽然绝大部分情况下,开发者不需要去查找获取DOM,但是还是有需要获取DOM的场景,所以Omi提供了方便获取DOM节点的方式。
ref和refs
class Hello extends Omi.Component {
constructor(data) {
super(data);
}
style () {
return `
h1{
cursor:pointer;
}
`;
}
handleClick(){
alert(this.refs.abc.innerHTML);
}
render() {
return `
<div>
<h1 ref="abc" onclick="handleClick()">Hello ,{{name}}!</h1>
</div>
`;
}
}
Omi.render(new Hello({ name : "Omi" }),"#container");
可以看到通过在HTML中标记ref为abc,那么就通过this.refs.abc访问到该DOM节点。
插件体系
Omi是Web组件化框架,怎么又来了个插件的概念?
可以这么理解: Omi插件体系可以赋予dom元素一些能力,并且可以和组件的实例产生关联。
omi-drag
且看这个例子:
import OmiDrag from './omi-drag.js';
OmiDrag.init();
class App extends Omi.Component {
constructor(data) {
super(data);
}
render() {
return <div>
<div omi-drag class="test">Drag Me</div>
</div>
}
style(){
return `
.test{
width:100px;
height:100px;
color:white;
line-height:90px;
text-align:center;
background-color:#00BFF3;
}
`
}
}
Omi.render(new App(),"#container");
如上面的代码所示,通过在div上标记omi-drag,这个div就能够被用户使用鼠标拖拽。我们称omi-drag.js为omi插件。 是不是非常方便?那么这个omi-drag是怎么实现的?
Omi.extendPlugin
核心方法: Omi.extendPlugin( pluginName, handler )
下面的代码就是展示了如何通过 Omi.extendPlugin 赋予dom拖拽的能力:
;(function () {
var OmiDrag = {};
var Omi = typeof require === 'function'
? require('omi')
: window.Omi;
OmiDrag.init = function(){
Omi.extendPlugin('omi-drag',function(dom, instance){
dom.style.cursor='move';
var isMouseDown = false,
preX = null,
preY = null,
currentX = null,
currentY = null,
translateX = 0,
translateY = 0;
dom.addEventListener('mousedown',function(evt){
isMouseDown = true;
preX = evt.pageX;
preY = evt.pageY;
evt.stopPropagation();
},false);
window.addEventListener('mousemove',function(evt){
if(isMouseDown){
currentX = evt.pageX;
currentY = evt.pageY;
if(preX != null){
translateX += currentX - preX;
translateY += currentY - preY;
dom.style.transform = 'translateX('+translateX+'px) translateY('+translateY+'px)';
}
preX = currentX;
preY = currentY;
evt.preventDefault();
}
},false);
window.addEventListener('mouseup',function(){
isMouseDown = false;
preX = preY = currentX = currentY = null;
},false);
});
}
OmiDrag.destroy = function(){
delete Omi.plugins['omi-drag'];
};
if (typeof exports == "object") {
module.exports = OmiDrag;
} else if (typeof define == "function" && define.amd) {
define([], function(){ return OmiDrag });
} else {
window.OmiDrag = OmiDrag;
}
})();
方法: Omi.extendPlugin( pluginName, handler )
其中pluginName为插件的名称 其中handler为处理器。handler可以拿到标记了pluginName的dom以及dom所在的组件的实例,即 dom 和 instance。
通过 Omi.extendPlugin,可以赋予dom元素一些能力,也可以和组件的实例(instance)产生关联。 但是上面的例子没有和instance产生关联,我们接下来试试:
关联instance
我们想在组件里面能够监听到move并且执行回调。如下:
...
...
moveHandler(){
console.log('moving');
}
render() {
return <div>
<div omi-drag class="test">Drag Me</div>
</div>
}
...
主要被拖动过程中,moveHandler就不断地被执行。插件代码需要修改:
...
window.addEventListener('mousemove',function(evt){
if(isMouseDown){
currentX = evt.pageX;
currentY = evt.pageY;
if(preX != null){
translateX += currentX - preX;
translateY += currentY - preY;
dom.style.transform = 'translateX('+translateX+'px) translateY('+translateY+'px)';
}
preX = currentX;
preY = currentY;
evt.preventDefault();
instance.moveHandler(evt);
}
},false);
我们在里面增加了instance.moveHandler(evt);方法,用来执行组件实例上的moveHandler方法。 这样的话:就是组件的实例(instance)产生关联。但是还是有问题?如果标记了多个omi-drag 就会有问题!如:
...
render() {
return <div>
<div omi-drag class="test">Drag Me</div>
<div omi-drag class="test">Drag Me</div>
</div>
}
...
通常我们系统每个omi-drag都能对应一个回调函数,如:
...
...
moveHandlerA(){
console.log('moving');
}
moveHandlerB(){
console.log('moving');
}
render() {
return <div>
<div omi-drag class="test">Drag Me A</div>
<div omi-drag class="test">Drag Me B</div>
</div>
}
...
怎么办?怎么实现?有办法!通过dom传递数据给插件。
传递数据
先来看最后实现的效果:
...
...
moveHandlerA(){
console.log('moving');
}
moveHandlerB(){
console.log('moving');
}
render() {
return <div>
<div omi-drag class="test" dragMove="moveHandlerA" >Drag Me A</div>
<div omi-drag class="test" dragMove="moveHandlerB" >Drag Me B</div>
</div>
}
...
omi-drag修改的地方:
...
var handlerName = dom.getAttribute('dragMove');
window.addEventListener('mousemove',function(evt){
if(isMouseDown){
currentX = evt.pageX;
currentY = evt.pageY;
if(preX != null){
translateX += currentX - preX;
translateY += currentY - preY;
dom.style.transform = 'translateX('+translateX+'px) translateY('+translateY+'px)';
}
preX = currentX;
preY = currentY;
evt.preventDefault();
instance[handlerName](evt);
}
},false);
...
- 通过 var handlerName = dom.getAttribute('dragMove') 拿到dom上声明的dragMove
- 通过 instancehandlerName 去执行对应的方法
closeScopedStyle
- 通过
closeScopedStyle
可以关闭局部CSS: 如:
Omi.render(new App({ closeScopedStyle : true }, '#ctn'))
或者
<your-tag closeScopedStyle></your-tag>
调试技巧
console.log(Omi.instances)
所有的组件的实例都在Omi.instances
数组里,简单吧!
Server Side Rendering
let content = Omi.renderToString(new Root(data))
- content 为直出的 HTML + CSS + JS
- Root 为前后端共享的根节点
- data 为前后端共享的数据