Blog


  • Home

  • Archives

  • Search
close

Test Travis

Posted on 2019-06-03

Test

Vue2.x Table 组件

Posted on 2017-10-11

Vue2.x Table 组件从0到1。

API设计

Table组件是个功能比较复杂的组件,所以我们在尝试写一个Vue-table组件之前,参考了一些常用UI组件库中Table组件的设计。首先看的是同样基于Vue的Element组件库,其中的Table组件用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<el-table
:data="tableData"
style="width: 100%">
<el-table-column
prop="name"
label="姓名"
width="180">
</el-table-column>
<el-table-column
prop="address"
label="地址">
</el-table-column>
</el-table>
</template>

<script>
export default {
data() {
return {
tableData: [{
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, ...]
}
}
}
</script>

我们可以把表格的信息分为两部分,一部分是表格的数据,另一部分是表格的排列信息。那么在Element UI中,表格数据需要定义在data中,再通过props传到table组件里。表格排列信息则定义在el-table-column子组件中。

接下来,再看另一个UI组件库ant design,其Table组件用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const dataSource = [{
key: '1',
name: '胡彦斌',
age: 32,
address: '西湖区湖底公园1号'
}, ...];

const columns = [{
title: '姓名',
dataIndex: 'name',
key: 'name',
}, {
title: '年龄',
dataIndex: 'age',
key: 'age',
}, {
title: '住址',
dataIndex: 'address',
key: 'address',
}];

<Table dataSource={dataSource} columns={columns} />

与Element UI不同,ant design表格列的信息通过props传入组件。

将两个UI库中Table组件相比之后,我们觉得Element UI让用户把表格列信息写在模板中的设计更加直观清晰。所以,最后决定参考Element UI的设计。


初步设计

用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<m-table :data="tableData">
<m-table-col
prop="name"
label="姓名"
width="10%">
</m-table-col>
<m-table-col
prop="address"
label="地址"
width="40%">
</m-table-col>
</m-table>
</template>

<script>
export default {
data() {
return {
tableData: [{
name: '王小虎',
address: '上海市普陀区金沙江路 1518 弄'
}, ...]
}
}
}
</script>

这里的<m-table-col>组件只作为接入组件,并不具有展示功能。它的主要工作是在其初始化的时候将列的信息,如prop、label、width保存到一个对象中,将该对象$emit到父组件m-table上。m-table组件再将所有列组件的信息保存到columns数组中。

我们再来看一下渲染表头和表格主体的展示组件,其关系如下:

1
2
3
4
<m-table>
<m-table-head :columns="columns"></m-table-head>
<m-table-body :data="data" :columns="columns"></m-table-body>
</m-table>

<m-table-head>组件负责渲染表头,m-table-body组件负责渲染表格主体。这两个组件所作的处理就是循环生成<th>、<td>等表格元素,组合成一个表格。至此,一个可以展示数据的Table组件就算完成了。

但是,表格一般需要有一定的操作功能,比较常用的一个就是选中删除某一行。然而,在完成这一步的时候我们遇到了一些问题。


自定义column

假设现在要定义一列删除按钮,要如何让用户自如地定义按钮组件,最终在表格内部正常渲染,且点击具体的按钮可以获得对应row的信息?

一开始我们设想用slot获得子Vnode节点,它们记录了用户自定义组件声明时的内嵌内容,将它们保存在columns数组中,再在每一行重复渲染。初步设计如下:

1
2
3
4
5
6
<m-table-col
label="操作"
prop="active"
width="10%">
<m-button></m-button>
</m-table-col>

然而VNode是唯一的,而且即使将VNode深复制,也不可能动态改变VNode,比如把每一行的id传进该VNode。

最后的解决方法是$scopedSlots。scopedSlots被编译后,返回一个函数,该函数可以动态传入scope并生成VNode,问题便迎刃而解了。

Links:

Table组件中slot内容的跨级传递

Yet Another JavaScript Bridge

Posted on 2017-04-06

什么是JSBridge

在Hybrid App中,关键的部分是Native和Web的交互。JSBridge就是用来建立通信机制的。通过JSBridge,Native可以调用JS的本地方法,Web调用会被Native捕获的方法,最终实现通信。

下面介绍我们封装的一个JSBridge。来看一下它的API及其用法


API及其用法

一、YAJB初始化

初始化一个YAJB实例

用法

1
npm install yajb-js
1
2
var YAJB = require('yajb-js');
var yajb = new YAJB()

API

初始化实际上做了三件事情。首先是初始化一个事件队列,然后判断是在Android还是iOS平台,最后保存WebView初始化传入的data。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var YAJB = function(){
// init evnetQueue
this.eventQueue = []
this.counter = 1
......
// get global options
if (window.javaInterface) {
options = JSON.parse(window.javaInterface.toString())
this.isAndroid = true
}else if (window.YAJB_INJECT){
options = window.YAJB_INJECT
this.isiOS = true
}
......
// store data
this.platform = options.platform
this.data = options.data
// store instance
window.YAJB_INSTANCE = this
}

二、本地调用的方法

YAJB.send()

用法

yajb.send()中可以传入的参数有事件名和data,另外还可以定义一个函数。该函数将会在Native触发该事件之后被调用,可接收Native返回的值。

1
2
3
yajb.send("${eventName}", data).then(function(val){
console.log(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
YAJB.prototype.send = function(event, data) {
var that = this;
return new Promise(function(resolve, reject){
that.eventQueue.push({
event: event + "Resolved",
id : that.counter,
callback: function(value){
console.log("resolve")
resolve(value)
}
})
that._send({event:event,id:that.counter,data:JSON.stringify(data)});
that.counter++
})
}

YAJB._send()

上面提到了YAJB._send的作用是是将事件名,事件id和data发送给Native。那它具体是怎样实现的呢?

API

1
2
3
4
5
6
7
YAJB.prototype._send = function(option) {
if (this.isAndroid) {
window.location = "hybrid://" + option.event + ':' + option.id + '/'+ option.data
}else if (this.isiOS) {
// window.postMessage
}
}

可以看到,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
2
3
4
5
yajb.register("emit", function(data,fn){
console.log(data)
var result = data + "haha"
fn(result)
})

API

这里register做的事情很简单,只是把事件推到事件队列中。

1
2
3
4
5
6
YAJB.prototype.register = function(event,fn){
this.eventQueue.push({
event: event,
callback: 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
2
3
4
YAJB.prototype._emit = function(option){
var opt = JSON.parse(option)
this.checkQueue(opt)
}

接下来到事件队列中通过id找到${event}Resolve事件,执行回调函数

1
2
3
4
5
6
YAJB.prototype.checkQueue = function(option){
var event = this.eventQueue.find(function(item){
return item.id == option.id
})
event.callback(option.data)
}

YAJB._trigger()

用法

当Native调用_trigger的同时,传入事件名,事件id和data。根据事件名从事件队列中找出之前注册过的事件,传入Native返回的data并执行callback。如果在callback中还传入了第二个参数(如:yajb.register用法示例的fn),则会给Native发送${event}Resolved的响应事件。

API

1
2
3
4
5
6
7
8
9
10
11
12
YAJB.prototype._trigger = function(option){
var opt = JSON.parse(option)
var event = this.eventQueue.find(function(item){
return item.event === opt.event
})
var that = this
event.id = opt.id
event.callback(opt.data,function(result){
var op = {event: opt.event + 'Resolved',data:result,id:opt.id};
that._send(op);
})
}

初识Event Emitter

Posted on 2016-12-10

Event Emitter 是“观察者模式(Observer Pattern”在前端的一种呈现方式。

所谓观察者模式(Observer Pattern)就是定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。

比如,在知乎上,我们关注了某个用户,当这个用户有了动态,我们的TimeLine上面就会显示。如果取消关注之后,就不会再有其动态的推送了。这里,如果把该用户的动态看作是事件的话,我们就是”订阅者”,”监听”着该用户的动态;该用户就是”发布者”,他更新动态就“触发”了该事件。

回到Event emitter。之前在用Vue2.0写一个小东西,需要实现非父子组件通信的时候,发现已经没有了$dispatch和 $broadcast, 而要用$emit, Vue 里面的$emit 是什么样的呢?

Vue $emit

Vue文档介绍的用法:

1
2
3
4
5
6
7
8
//在简单的场景下,使用一个空的 Vue 实例作为中央事件总线:
var bus = new Vue()
// 触发组件 A 中的事件
bus.$emit('id-selected', 1)
// 在组件 B 创建的钩子中监听事件
bus.$on('id-selected', function (id) {
// ...
})

尝试了一下,跟着写了一个todo。部分组件结构:Todos -> todo -> delete,现在使delete组件与Todos组件这两个非父子组件用emitter通信。

首先,创建一个新的Vue实例bus

1
2
3
//bus.js
import Vue from 'vue'
export var bus = new Vue()

然后,在存在“发布者”和“订阅者”的组件里面import { bus } from 'route to../bus.js'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Todos.vue
import { bus } from '../bus.js'
export default{
//data
//components
......
created() {
......
bus.$on('remove',this.rmTodo)
},
methods:{
......
rmTodo(todo){
this.todos.splice(this.todos.indexOf(todo),1)
}
}
}
1
2
3
4
5
6
7
8
9
10
//delete.vue
import { bus } from '../bus.js'
export default{
//Array todos props from parent components
methods:{
remove(todo){
bus.$emit('remove',this.todo)
}
}
}

这看上去就像,当我们触发了delete组件里的remove 方法时,该组件就会emit 一个“remove”的事件名。而监听了“remove”事件的Todos组件就能接受传入的todo参数,并执行rmTodo。

但实际上Event Emitter是怎样的呢?戳开看Vue源码

主要看 $emit 和 $on 的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
for (let i = 0, l = cbs.length; i < l; i++) {
cbs[i].apply(vm, args)
}
}
return vm
}

Vue.prototype.$on = function (event: string, fn: Function): Component {
const vm: Component = this
;(vm._events[event] || (vm._events[event] = [])).push(fn)
return vm
}

可以看到,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function Emitter(){
//事件管理器
this.events = {};

this.emit = (eventName,data) =>{
const event = this.events[eventName];
if( event ) {
//遍历this.events[eventName]数组中的每一个函数,传入data并立即执行
event.forEach(fn => {
fn.call(null, data);
});
}
}

this.subscribe = (eventName, fn) =>{
//如果this.events中没有该eventName,则创建一个
if(!this.events[eventName]) {
this.events[eventName] = [];
}
//把fn推到数组中
this.events[eventName].push(fn);
}

this.off = (eventName, fn) =>{
return () => {
//移除该事件里面的fn
this.events[eventName] = this.events[eventName].filter(eventFn => fn != eventFn)
console.log(this.events)
}
}
}

以上就是emitter构造函数的所有代码。接下来我们用它来操作一些DOM,来看看具体是怎么样用的

1
2
3
4
5
6
7
//html
<body>
<input type="text">
<h1></h1>
<button id="btno">Change name</button>
<button id="btnt">change color</button>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
document.addEventListener("DOMContentLoaded", function() {
let input = document.querySelector('input[type="text"]');
let button = document.getElementById('btno');
let buttont = document.getElementById('btnt')
let h1 = document.querySelector('h1');

//构造emitter
let emitter = new Emitter();

//绑定emit,点击该按钮时会触发‘name-changed’事件
button.addEventListener('click', () => {
emitter.emit('name-changed', {name: input.value});
});

//绑定了一个emit 和 off,点击该按钮时会触发‘color’事件,并取消'change'事件中的,有回调函数changeName的订阅者的订阅
buttont.addEventListener('click', () => {
emitter.emit('color', {color: 'red'});
emitter.off('change',changeName)()
});

//定义两个function,作为订阅者的回调函数
function changeColor(data){
h1.style.color = data.color;
}

function changeName(data){
h1.innerHTML = `Your name is: ${data.name}`;
}

//订阅者
emitter.subscribe('change', changeName);
emitter.subscribe('color', changeColor);
});

首先构造一个emitter,emitter里面有事件管理器this.events={}和emit,subscribe两个方法。我们试着在两个button上绑定了click用来触发emit。

再写两个订阅者,分别订阅的是“change”和“color”事件。因为之前events管理器中还没有这两个事件,所以this.events[eventName] = []来创建以该事件名为key,空数组作为其value的对象,并把订阅者的函数push到数组中。

当点击按钮的时候,就会向事件名对应的函数数组中的每一个函数传入data参数,执行回调

Demo代码

JS Modularity

Posted on 2016-03-17

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
2
3
4
5
6
//export  default
export default function(num1, num2) {
return num1 + num2;
}
import sum from "example"
console.log(sum(1,2));//3
  • 可以rename
1
2
3
4
5
6
7
8
function sum(num1, num2) {
return num1 + num2;
}
export { sum as add };//在export里改
import { sum as add } from "example"; //在import里改will do

console.log(add(1,2));//3
console.log(typeof sum)//undefined

References

[1]Understanding ECMAScript6-Modules

[2]Programming JavaScript Applications-Chapter 4. Modules

[3]Eloquent JavaScript Chapter10 Modules

[4]js-bits

list-style-image sucks?

Posted on 2016-03-02

最近在学习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]

[1]MDN list-style

Amanda

Amanda

practice makes perfect

6 posts
GitHub Twitter
© 2019 Amanda
Powered by Hexo
Theme - NexT.Pisces