Vue2.x Table 组件
Vue2.x Table 组件从0到1。
API设计
Table组件是个功能比较复杂的组件,所以我们在尝试写一个Vue-table组件之前,参考了一些常用UI组件库中Table组件的设计。首先看的是同样基于Vue的Element组件库,其中的Table组件用法如下:
1 | <template> |
我们可以把表格的信息分为两部分,一部分是表格的数据,另一部分是表格的排列信息。那么在Element UI中,表格数据需要定义在data中,再通过props传到table组件里。表格排列信息则定义在el-table-column
子组件中。
接下来,再看另一个UI组件库ant design,其Table组件用法如下:
1 | const dataSource = [{ |
与Element UI不同,ant design表格列的信息通过props传入组件。
将两个UI库中Table组件相比之后,我们觉得Element UI让用户把表格列信息写在模板中的设计更加直观清晰。所以,最后决定参考Element UI的设计。
初步设计
用法:
1 | <template> |
这里的<m-table-col>
组件只作为接入组件,并不具有展示功能。它的主要工作是在其初始化的时候将列的信息,如prop
、label
、width
保存到一个对象中,将该对象$emit
到父组件m-table
上。m-table
组件再将所有列组件的信息保存到columns
数组中。
我们再来看一下渲染表头和表格主体的展示组件,其关系如下:
1 | <m-table> |
<m-table-head>
组件负责渲染表头,m-table-body
组件负责渲染表格主体。这两个组件所作的处理就是循环生成<th>
、<td>
等表格元素,组合成一个表格。至此,一个可以展示数据的Table组件就算完成了。
但是,表格一般需要有一定的操作功能,比较常用的一个就是选中删除某一行。然而,在完成这一步的时候我们遇到了一些问题。
自定义column
假设现在要定义一列删除按钮,要如何让用户自如地定义按钮组件,最终在表格内部正常渲染,且点击具体的按钮可以获得对应row的信息?
一开始我们设想用slot
获得子Vnode节点,它们记录了用户自定义组件声明时的内嵌内容,将它们保存在columns
数组中,再在每一行重复渲染。初步设计如下:
1 | <m-table-col |
然而VNode是唯一的,而且即使将VNode深复制,也不可能动态改变VNode,比如把每一行的id传进该VNode。
最后的解决方法是$scopedSlots
。scopedSlots被编译后,返回一个函数,该函数可以动态传入scope并生成VNode,问题便迎刃而解了。
Links:
Yet Another JavaScript Bridge
什么是JSBridge
在Hybrid App中,关键的部分是Native和Web的交互。JSBridge就是用来建立通信机制的。通过JSBridge,Native可以调用JS的本地方法,Web调用会被Native捕获的方法,最终实现通信。
下面介绍我们封装的一个JSBridge。来看一下它的API及其用法
API及其用法
一、YAJB初始化
初始化一个YAJB实例
用法
1 | npm install yajb-js |
1 | var YAJB = require('yajb-js'); |
API
初始化实际上做了三件事情。首先是初始化一个事件队列,然后判断是在Android还是iOS平台,最后保存WebView初始化传入的data。
1 | var YAJB = function(){ |
二、本地调用的方法
YAJB.send()
用法
yajb.send()中可以传入的参数有事件名和data,另外还可以定义一个函数。该函数将会在Native触发该事件之后被调用,可接收Native返回的值。
1 | yajb.send("${eventName}", data).then(function(val){ |
API
yajb.send()做的事情是,通过YAJB._send
将事件名、事件id和data发送给Native,并将event + “Resolved”
的事件加入事件队列
(YAJB默认当Native触发该事件后返回的事件的事件名为event + "Resolved"
)
这里利用了Promise,主要是为了用户可以通过Promise resolve function自定义执行成功的callback。当Native触发event + “Resolved”
的事件时,就会传入参数并执行callback,Promise resolve()
在此时也被调用。
1 | YAJB.prototype.send = function(event, data) { |
YAJB._send()
上面提到了YAJB._send
的作用是是将事件名,事件id和data发送给Native。那它具体是怎样实现的呢?
API
1 | YAJB.prototype._send = function(option) { |
可以看到,YAJB用的是URL scheme。我们约定的URL协议是hybrid://${eventName}:${eventid}/${data}
。当我们更改URL时,Native将会捕获该动作,并解析URL。如果该URL符合约定的协议,则不进行跳转。以此实现向Native发送消息。
YAJB.register()
用法
本地先注册一个事件,等待Native触发。
YAJB.register()需要传入事件名和Native触发该事件之后需要执行的回调函数。该回调函数第一个参数为Native返回的data,第二个参数为返回event Resolved事件给Native的函数
1 | yajb.register("emit", function(data,fn){ |
API
这里register做的事情很简单,只是把事件推到事件队列中。
1 | YAJB.prototype.register = function(event,fn){ |
接下来会具体介绍,Native触发已注册事件时所调用的_trigger
方法
三、提供给Native调用的方法
YAJB._emit()
用法
上文介绍YAJB.send()
的时候说到,Web通过send方法向Native发送该事件及其data,Native接收并解析了之后,返回事件名为${event}Resolved
的事件。
Native通过调用本地的_emit(),返回${event}Resolved
事件。
API
返回的option有{${event}Resolved,eventid,data}
1 | YAJB.prototype._emit = function(option){ |
接下来到事件队列中通过id找到${event}Resolve
事件,执行回调函数
1 | YAJB.prototype.checkQueue = function(option){ |
YAJB._trigger()
用法
当Native调用_trigger的同时,传入事件名,事件id和data。根据事件名从事件队列中找出之前注册过的事件,传入Native返回的data并执行callback。如果在callback中还传入了第二个参数(如:yajb.register用法示例的fn
),则会给Native发送${event}Resolved
的响应事件。
API
1 | YAJB.prototype._trigger = function(option){ |
初识Event Emitter
Event Emitter 是“观察者模式(Observer Pattern”在前端的一种呈现方式。
所谓观察者模式(Observer Pattern)就是定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。
比如,在知乎上,我们关注了某个用户,当这个用户有了动态,我们的TimeLine上面就会显示。如果取消关注之后,就不会再有其动态的推送了。这里,如果把该用户的动态看作是事件的话,我们就是”订阅者”,”监听”着该用户的动态;该用户就是”发布者”,他更新动态就“触发”了该事件。
回到Event emitter。之前在用Vue2.0写一个小东西,需要实现非父子组件通信的时候,发现已经没有了$dispatch
和 $broadcast
, 而要用$emit
, Vue 里面的$emit
是什么样的呢?
Vue $emit
Vue文档介绍的用法:
1 | //在简单的场景下,使用一个空的 Vue 实例作为中央事件总线: |
尝试了一下,跟着写了一个todo。部分组件结构:Todos -> todo -> delete,现在使delete组件与Todos组件这两个非父子组件用emitter通信。
首先,创建一个新的Vue实例bus
1 | //bus.js |
然后,在存在“发布者”和“订阅者”的组件里面import { bus } from 'route to../bus.js'
1 | //Todos.vue |
1 | //delete.vue |
这看上去就像,当我们触发了delete组件里的remove 方法时,该组件就会emit 一个“remove”的事件名。而监听了“remove”事件的Todos组件就能接受传入的todo参数,并执行rmTodo。
但实际上Event Emitter是怎样的呢?戳开看Vue源码
主要看 $emit
和 $on
的部分
1 | Vue.prototype.$emit = function (event: string): Component { |
可以看到,vm._events 就像是一个事件管理器,它存放事件名(event:string)作为key,其对应的value就是监听了该事件,并当该事件触发之后执行的所有函数
因为一个事件可能会被多个监听,所以函数存放在数组里,每有一个$on
,就会把其函数push到对应event的value中
(vm._events[event] || (vm._events[event] = [])).push(fn)
当事件被触发后,$emit
就会找到vm._events[event]
中的函数数组,传入参数并执行回调。
知道了原理之后,接下来我们尝试…
实现一个简单的Event emitter
1 | function Emitter(){ |
以上就是emitter构造函数的所有代码。接下来我们用它来操作一些DOM,来看看具体是怎么样用的
1 | //html |
1 | document.addEventListener("DOMContentLoaded", function() { |
首先构造一个emitter,emitter里面有事件管理器this.events={}
和emit,subscribe两个方法。我们试着在两个button上绑定了click用来触发emit。
再写两个订阅者,分别订阅的是“change”和“color”事件。因为之前events管理器中还没有这两个事件,所以this.events[eventName] = []
来创建以该事件名为key,空数组作为其value的对象,并把订阅者的函数push到数组中。
当点击按钮的时候,就会向事件名对应的函数数组中的每一个函数传入data参数,执行回调
JS Modularity
what is modularity?
JavaScript modules are encapsulated, meaning that they keep implementation details private, and expose a public API.
modules should be
specialized: 一个模块是专门解决一个问题的
independent: 每个模块是独立的。模块之间由API连接
decomposable: 可分解
recomposable: 可组合
substitutable: 可被代替
why modularity?
Code complexity grows. And modularity simplifies things!
solution to name collision
当一个项目大起来的时候,js的代码也越来越多,不进行闭包或者模块化就会产生大量的全局变量,而造成了命名冲突。
safely make change
在我们写一个app的时候,整个app可能由几个部分构成,且它们之间相互联系。在我们没有使每个部分模块化的情况下,对它们进行修改的时候还要考虑到它们对整体的影响。所以我们在进行修改之前,还要go through the whole program, 真是件吃力的事情。但如果把它们都模块化,就可以在这个模块里面直接修改,而不用担心它会对整个项目产生bad effect.
同样,也不用担心修改其他的部分会影响到它.
reuse the modules
模块可以被重复使用,and its API should be clean
make the code more readable
也使代码的结构性更强。
function
只能实现简单的封装。
var math = {
add: function add(a,b){
return a + b
},
sub: function sub(a,b){
return a - b
}
}
console.log(math.add(1,2));
console.log(add(1,2));//add is undefined
IIFE: Immediately-invoked Function Expression
能实现访问控制。
var module =(function(){
var private = "private";
var foo = function(){
console.log(private);
}
return{
foo:foo
}
})();
console.log(module.foo());
console.log(module.private);//undefined
function和IIFE都没有依赖声明。
我们该怎么更好地模块化呢?
CommonJS & AMD & ES6
CommonJS
多用于server上。不适合浏览器使用,不能发送异步请求。同步require
//content of foo.js
//先定义一个foo模块
var foo = function () {
return 'foo method result';
};
//再把foo模块暴露给其他模块
exports.method = foo
//content of bar.js
var Foo = require('../foo');//声明依赖foo模块
var barMethod = function () {
return 'barMethod result';
};
var fooMethod = function () {
return Foo.method();
};
//export
exports.barMethod = barMethod;
exports.fooMethod = fooMethod;
//依赖bar.js 这个模块
var bar = require('bar');
bar.barMethod();
bar.fooMethod();
AMD (Asynchronous Module Definition)
AMD里面模块是被异步加载的,加载完后就在缓存里。这能很好地适应浏览器的环境,因为这就不需要在每次加载应用时候,把所有的模块都重新加载一遍。而Commonjs,并不能做到这一点,所以说它不适合浏览器环境。
因为AMD要require一个define过的模块,要立刻获取define模块的内容,所以可以看到define{}里面的内容都要用return{}包起来
AMD还可以加载html,css文件
It should look like this
// content of foo.js define('module's name',function(){}) 定义模块foo
define('foo',function(){
return{
method: function(){
return "foo method"
}
}
});
//content of bar.js define('module's name',['dependency'],function(){}) 定义依赖模块foo的模块bar
define('bar',['foo'],function(){
return{
barMethod{
return "bar method"
}
fooMethod{
return foo.method();
}
}
});
//require(['dependency'],function(){}) 获取模块
require(['bar'],function(){
bar.barMethod();
bar.fooMethod();
})
ES6
HERE come the export and import keywords!
export
export var color = "red";
export let name = "Nicholas";
export const magicNumber = 7;
export function sum(num1, num2) {
return num1 + num2;
}
//export later
function multiply(num1, num2) {
return num1 * num2;
}
export multiply;
import{}
import {identifier1,identifier2,..} from "file"
//import everything
import * from "file"
some rules we need to know
一切都”use strict”
只有被export了,才能在其他模块被引用。
module的顶级作用域不能用this这个语句
需要给每个function和class起名字。但default可以有
1 | //export default |
- 可以rename
1 | function sum(num1, num2) { |
References
[1]Understanding ECMAScript6-Modules
[2]Programming JavaScript Applications-Chapter 4. Modules
[3]Eloquent JavaScript Chapter10 Modules
[4]js-bits
list-style-image sucks?
最近在学习jQuery,写一个文件夹开合的小组件。在阿里妈妈找了文件夹的svg图后,就准备把它们丢到list-style-image里。然而发现自己handle不了svg图的大小。于是,我寄希望于list-style-image有控制图大小的value。
就去查了查之前一直没有用过的list-style。所以,list-style-image里有什么?
先来看看MDN上关于list-style-iamge的介绍吧
Summary: The list-style-image property sets the image that will be used as the list item marker.
Values: url/none/inherited
Applies to: list-item
好像并没有能解决我问题的value。 但后来,我学会通过改svg图的源码来设置图的大小。嗯,解决了svg图的问题
可大家一般都怎么使用list-style-iamge呢? 再后来,我发现大家真的都不怎么用它:)
去看看阿里的官网
代码应该是这个样子的。可以看到它是在 i标签里插入图片 ,实现marker的效果
See the Pen LNEmMN by AMANDA (@amanda111) on CodePen.
再看看twitter的官网
用的是伪元素:before
See the Pen oxgyQE by AMANDA (@amanda111) on CodePen.
更多的是 用设置background-image的方法 ,设置background no-repeat,再给内容设一个合适的padding-left的值,就把background-image变到内容前面了。就有了小图标的赶脚。
See the Pen xVbWQQ by AMANDA (@amanda111) on CodePen.
现在用 list-style-image
See the Pen pyvKdz by AMANDA (@amanda111) on CodePen.
噢..发生了什么?用list-style-image插入小图标,发现image和内容之间有逼死强迫症的间距呀。而这个间距又是什么呢?
当不改变li标签样式的时,默认样式为list-style:disc。这时就可以看到实心点和内容的间距。这个间距应该是不能被改变的,是由浏览器决定的。所以,list-style-image 属性会有兼容性问题。
想到要调整图片位置,就想到有 list-style-position
so what is list-style-position?
Initial values: outside
Values: inside/outside
Applies to: list-item
See the Pen aNzgxR by AMANDA (@amanda111) on CodePen.
outside: The marker box is outside the principal block box
inside: The marker box is the first inline box in the principal block box, after which the element’s content flows.
就只有这两个value。所以..它并不能解决marker与内容的间距问题。但是,如果list-style-position有控制间距的value。
Actually,list-style-image is just fine.
[Reference Documentation]