小子 发布的文章

缓存那些事

浏览器缓存头

If-Modified-Since
If-None-Match
Last-Modified
Cache-Control
ETag
Expires

具体可以查看这篇文章中的附件 媒体中心设计分享

varnish / apache traffic server -> cdn

CSI: 指利用ajax等技术,将动态的数据使用异步的方式加载进页面 (比较适用于PC, H5)
SSI: 通常url后缀为shtml
ESI: 最具代表性的 varnish/ats (比较适用于App的接口)

具体可以查看这篇文章:页面静态化

上面的几种方案都需要走到后端的服务器,在并发和加载速度要求比较高的情况下,可以选择生成静态文件上传到cdn

local cache, redis, tair

多级缓存可以降低中心缓存服务器的压力,但是也会存在数据不一致的问题

当当网交易链路:简单的将local cache的过期时间设置为1分钟,降低缓存不一致的概率 (适用于一致性要求不高的情景)

缓存击穿的几种场景:

  1. 缓存过期失效

  2. 不存在的数据

  3. 缓存宕机

对于场景1,为避免瞬时流量将db和缓存击垮,可以使用一个锁,保证并发环境下,只有1个/少量线程写入同一条数据
对于场景2,可以使用empty object,在存取缓存的时候将其替换为null,如果为了池子中有效数据留存率,可以将empty object和正常数据分开存放
对于场景3,需事先脱离缓存,db裸压,保证在没有缓存的情况之后可以正常支持线上的流量 (可忍受的RT内)

pjax

对页面的局部更新,不过会将当前url塞到浏览器的历史记录中

具体可以查看这篇文章:slim框架中pjax的实现

bigpipe

利用服务器端的输出缓存,输出部分页面

缓存优化

在一台机器存不下1个业务所有缓存的时候,一般都会选择分片的策略(大多采用取模的办法),但有的时候缩减单个缓存对象的大小,也可以节省整个池子的资源

  1. 一般情况之下,key的重复度很高,可以选择缩减key的长度

  2. 在序列化的时候选择创建slim object,然后JSON.toJSONString()

  3. 使用gz/br压缩JSONString (考虑平滑兼容多种压缩方式,使用第一个字节作为标志位)

Java RPC增加spring定义支持

<bean id="demoService" class="lu.tool.provider.DemoServiceImpl" />
<rpc:service interface="lu.tool.provider.DemoService" ref="demoService" />
 ^                 ^                                   ^
 |- xsd:element    |- xsd:attribute                    |- xsd:attribute

XML Schema Definition

由motan.xsd简化而来rpc.xsd

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://tool.lu/schema/rpc"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            xmlns:tool="http://www.springframework.org/schema/tool"
            xmlns:beans="http://www.springframework.org/schema/beans"
            targetNamespace="http://tool.lu/schema/rpc">
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
    <xsd:import namespace="http://www.springframework.org/schema/tool"/>
    <xsd:import namespace="http://www.springframework.org/schema/beans"/>

    <xsd:complexType name="abstractConfig">
        <xsd:choice minOccurs="0" maxOccurs="unbounded">
            <xsd:element ref="beans:property" minOccurs="0" maxOccurs="unbounded"/>
        </xsd:choice>
        <xsd:anyAttribute namespace="##other" processContents="lax"/>
    </xsd:complexType>

    <xsd:element name="service">
        <xsd:complexType>
            <xsd:complexContent>
                <xsd:extension base="abstractConfig">
                    <xsd:attribute name="interface" type="xsd:token">
                        <xsd:annotation>
                            <xsd:documentation>
                                <![CDATA[ interface. ]]>
                            </xsd:documentation>
                            <xsd:appinfo>
                                <tool:annotation>
                                    <tool:expected-type type="java.lang.Class"/>
                                </tool:annotation>
                            </xsd:appinfo>
                        </xsd:annotation>
                    </xsd:attribute>
                    <xsd:attribute name="ref" type="xsd:string" use="optional">
                        <xsd:annotation>
                            <xsd:documentation>
                                <![CDATA[ bean id ]]>
                            </xsd:documentation>
                        </xsd:annotation>
                    </xsd:attribute>
                </xsd:extension>
            </xsd:complexContent>
        </xsd:complexType>
    </xsd:element>
</xsd:schema>

NamespaceHandler

public class RpcNamespaceHandler extends NamespaceHandlerSupport {

    @Override
    public void init() {
        registerBeanDefinitionParser("service", new RpcBeanDefinitionParser(ServiceConfigBean.class));
    }
}

BeanDefinitionParser

未完待续...

public class RpcBeanDefinitionParser implements BeanDefinitionParser {

    private final Class<?> beanClass;

    public RpcBeanDefinitionParser(Class<?> beanClass) {
        this.beanClass = beanClass;
    }

    @Override
    public BeanDefinition parse(Element element, ParserContext parserContext) {
        // 具体逻辑
    }
}

Java RPC中的权重轮询

定义接口

public interface RoundRobin<T> {

    T nextData();

}

实现

算法来自nginx

public class WeightedRoundRobin<T> implements RoundRobin<T> {

    private List<Item<T>> items = new ArrayList<>();

    public WeightedRoundRobin(Map<T, Integer> datas) {
        List<Item<T>> initItems = datas.entrySet()
                .stream()
                .map(e -> new Item<>(e.getKey(), e.getValue()))
                .collect(Collectors.toList());
        items.addAll(initItems);
    }

    public T nextData() {
        Item<T> bestItem = null;
        int total = 0;
        for (Item<T> currentItem : items) {
            currentItem.currentWeight += currentItem.effectiveWeight;
            total += currentItem.effectiveWeight;
            if (currentItem.effectiveWeight < currentItem.weight) {
                currentItem.effectiveWeight++;
            }
            if (bestItem == null || currentItem.currentWeight > bestItem.currentWeight) {
                bestItem = currentItem;
            }
        }
        if (bestItem == null) {
            return null;
        }
        bestItem.currentWeight -= total;
        return bestItem.getData();
    }

    public List<Item<T>> getItems() {
        return items;
    }

    public void setItems(List<Item<T>> items) {
        this.items = items;
    }

    public static final class Item<T> {

        private T data;
        private int weight;
        private int effectiveWeight;
        private int currentWeight;

        public Item(T data, int weight) {
            this.data = data;
            this.weight = weight;
        }

        public T getData() {
            return data;
        }

        public void setData(T data) {
            this.data = data;
        }

        public int getWeight() {
            return weight;
        }

        public void setWeight(int weight) {
            this.weight = weight;
        }

        public int getEffectiveWeight() {
            return effectiveWeight;
        }

        public void setEffectiveWeight(int effectiveWeight) {
            this.effectiveWeight = effectiveWeight;
        }

        public int getCurrentWeight() {
            return currentWeight;
        }

        public void setCurrentWeight(int currentWeight) {
            this.currentWeight = currentWeight;
        }

        @Override
        public String toString() {
            return "Item{" +
                    "data=" + data +
                    ", weight=" + weight +
                    ", effectiveWeight=" + effectiveWeight +
                    ", currentWeight=" + currentWeight +
                    '}';
        }
    }

}

使用

        Map<Integer, Integer> testDatas = new HashMap<Integer, Integer>() {{
            put(1, 3); // 权重3
            put(2, 5); // 权重5
            put(3, 8); // 权重8
        }};
        WeightedRoundRobin<Integer> roundRobin = new WeightedRoundRobin<>(testDatas);

        for (int i = 0; i < 20; i++) {
            LOGGER.info("id: {}", roundRobin.nextData());
        }

ElasticSearch的玄学问题

Match查询

match,默认使用OR条件查询,即需要搜索的词条,被分词之后,分词之间的关系是或的关系;如果需要匹配所有,则需要指定逻辑为AND

match_phrase,保证词条被分词之后,结果的标题中词语的前后的顺序与搜索词条是一致的

Filter 和 Query 的区别

// ...

termsQuery

转拼音多音字的处理

背景

汉字转拼音五笔 最开始的时候选择了粗暴简单的方法,就是在遇到多音字的时候,直接取第一个读音;但是后来同事使用的时候发现多音字的转换效果太差了,于是进行了改造;刚开始的时候使用的php-jieba,但是php在每次request的时候都需要去加载jieba的词库,极其低效;所以选择了使用python来实现逻辑,php通过thrift来调用python的服务

处理流程

piyin.png

Java RPC中的反射 (Server)

每次根据方法名来反射获取Method的成本太大,所以在bean初始化的时候,就将该服务下interface的方法都放到HashMap里面

用来测试的interface

public interface TestApi {

    List<Integer> listIds();
    long convertId(long id);

}

扫描interface的方法

    private <T> Map<String, Method> initMethodMap(Class<T> clz) {
        Map<String, Method> methodMap = new HashMap<>();
        Method[] methods = clz.getMethods();

        for (Method method : methods) {
            String methodDesc = ReflectUtil.getMethodDesc(method);
            methodMap.put(methodDesc, method);
        }
        return methodMap;
    }

参数desc获取

class ReflectUtil {

    private static final String PARAM_SPLIT = ",";
    private static final String EMPTY_PARAM = "void";

    public static String getMethodParamDesc(Method method) {
        if (method.getParameterTypes() == null || method.getParameterTypes().length == 0) {
            return EMPTY_PARAM;
        }

        StringBuilder builder = new StringBuilder();

        Class<?>[] clzs = method.getParameterTypes();

        for (Class<?> clz : clzs) {
            String className = getName(clz);
            builder.append(className).append(PARAM_SPLIT);
        }

        return builder.substring(0, builder.length() - 1);
    }

    private static String getName(Class<?> clz) {
        if (!clz.isArray()) {
            return clz.getName();
        }

        StringBuilder sb = new StringBuilder();
        sb.append(clz.getName());
        while (clz.isArray()) {
            sb.append("[]");
            clz = clz.getComponentType();
        }

        return sb.toString();
    }

}

注:代码摘自 weibo 的 rpc 框架 motan; 并有部分修改

Java RPC中的代理 (Client)

调用图

rpc.png

Client实现

远程接口定义

public interface XxxApi {
    boolean remoteMethod();
}

Proxy工厂

public final class ProxyFactory {

    @SuppressWarnings("unchecked")
    public <T> T getProxy(Class<T> clz, InvocationHandler invocationHandler) {
        return (T) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[] {clz}, invocationHandler);
    }

}

函数返回值基本类型的默认值

public final class PrimitiveDefault {
    private static boolean defaultBoolean;
    private static char defaultChar;
    private static byte defaultByte;
    private static short defaultShort;
    private static int defaultInt;
    private static long defaultLong;
    private static float defaultFloat;
    private static double defaultDouble;
    private static Map<Class<?>, Object> primitiveValues = new HashMap<Class<?>, Object>();

    static {
        primitiveValues.put(boolean.class, defaultBoolean);
        primitiveValues.put(char.class, defaultChar);
        primitiveValues.put(byte.class, defaultByte);
        primitiveValues.put(short.class, defaultShort);
        primitiveValues.put(int.class, defaultInt);
        primitiveValues.put(long.class, defaultLong);
        primitiveValues.put(float.class, defaultFloat);
        primitiveValues.put(double.class, defaultDouble);
    }

    public static Object getDefaultReturnValue(Class<?> returnType) {
        return primitiveValues.get(returnType);
    }
}

Client调用

public final class Runner {

    private static final Logger logger = LoggerFactory.getLogger(Runner.class);

    public static void main(String[] ignore) {
        ProxyFactory proxyFactory = new ProxyFactory();

        XxxApi xxxApi = proxyFactory.getProxy(XxxApi.class, (proxy, method, args) -> {
            // 判断method是否定义过 todo
            logger.info("{} {}", method, args);
            // 产生1个默认值
            Class<?> returnType = method.getReturnType();
            if (returnType != null && returnType.isPrimitive()) {
                return PrimitiveDefault.getDefaultReturnValue(returnType);
            }
            return null;
        });

        xxxApi.remoteMethod();
    }

}

消费生产模式扫表

背景

有一批数据需要导入到ElasticSearch中,但是写ElasticSearch的速度比较慢,需要采用多线程的方式,但是在每个线程中都扫表,会产生重复的数据段,所以采用生产消费的模型来解决该问题 (为什么不直接选择线程池?线程池提交是异步的,一般table中的数据量都比较大,很容易塞爆内存)

流程图

produce-consume.png

  1. 由生产者进行扫表,每次取出一批的数据(如:500条)

  2. 将500条数据放入java的Queue中

  3. 多个生产者来消费这个Queue

  4. 当生产者结束扫表,或者外部中断扫表的时候,中断消费者

中断消费者的方式,往Queue中扔入一个毒药对象,当消费者获取到毒药对象时,停止消费,并将毒药对象塞回Queue,用于停止其他消费者

功能点

  1. 开始扫表

  2. 暂停扫表

  3. 结束扫表

  4. 数据扫表状态

  5. 恢复扫表(支持指定offset)

实现

Producer

public class Producer implements Runnable {

    private final SynchronousQueue<List<Long>> queue;

    private volatile boolean running = true;

    public Producer(SynchronousQueue<List<Long>> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        long lastId = 0L;
        int batchSize = 500;
        while (running) {
            // select * from my_table where id > ${lastId} order by id asc limit ${batchSize};
            List<Long> ids = new ArrayList<>(); // 自行实现上面的查询
            if (CollectionUtils.isEmpty(ids)) {
                putQueueQuite(Context.poison);
                break;
            }
            putQueueQuite(ids);
            lastId = Collections.max(ids);
            if (ids.size() < batchSize) {
                putQueueQuite(Context.poison);
                break;
            }
        }
        // throw poison
    }

    private void putQueueQuite(List<Long> pill) {
        try {
            queue.put(pill);
        } catch (InterruptedException e) {
            // ignore
        }
    }

}

Consumer

public class Consumer implements Runnable {

    private final SynchronousQueue<List<Long>> queue;

    public Consumer(SynchronousQueue<List<Long>> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                List<Long> ids = queue.take();
                if (ids == Context.poison) {
                    queue.put(Context.poison);
                    break;
                }
                // do something
            } catch (InterruptedException e) {
                // ignore
            }
        }
    }
}

Context

public class Context {

    public static final List<Long> poison = new ArrayList<>();

}

Runner

public class Runner {
    public static void main(String[] args) {
        int maxThreads = 10;
        int consumerThreads = 3;
        ExecutorService executorService = Executors.newFixedThreadPool(maxThreads);
        SynchronousQueue<List<Long>> queue = new SynchronousQueue<>();
        executorService.submit(new Producer(queue));
        for (int i = 0; i < consumerThreads; i++) {
            executorService.submit(new Consumer(queue));
        }
    }
}

功能点的控制,自己实现就好了

订单号的生成规则

背景

防止订单Id号泄露每日流水,暴露商业机密;需要对订单Id号进行相应的处理,但是订单号的生成又需要满足以下条件

  1. 唯一性

  2. 语义性

  3. 考虑分库分表的情况能快速路由到相应的表

  4. 长度

大厂的生成策略

#平台rule来源
1大众点评时间戳+用户标识码+随机数大众点评订单系统分库分表实践
2美团团购单表自增Id * 100 + 买家Id后2位美团团购订单系统优化记
3淘宝发号器Id + 买家Id后4位淘宝在线交易数据演变

其他策略: 生产乱序码和真实的orderId关联

发号器

MTDDL——美团点评分布式数据访问层中间件

Leaf整体架构.png

id和code的转换(Base62)

id.png

版本加密解密的实现

package lib

import (
    "fmt"
    "strconv"
)

func Id2Code(id int, version byte) string {
    var code string = ""
    if version == '1' {
        number := id
        for {
            remain := number % 10000000
            str := Base62Encode(remain)
            code = str + code
            number = number / 10000000
            if number == 0 {
                break
            }
        }
        code = string(version) + code
    }
    return code
}

func Code2Id(code string) int {
    version := code[0]
    code = code[1:]
    if version == '1' {
        var buffer string = ""
        for i := len(code); i > 0; i -= 4 {
            start := i - 4
            if start < 0 {
                start = 0
            }
            seg := code[start:i]
            segId := Base62Decode(seg)
            // 大于7位非法
            if segId >= 100000000 {
                return 0
            }
            buffer += fmt.Sprintf("%07d", Base62Decode(seg))
        }
        result, _ := strconv.Atoi(buffer)
        return result
    }
    return 0
}

Base62加密解密的实现

package lib

import (
    "math"
    "bytes"
)

const dictLength = 62

var dict []byte = []byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}

func Base62Encode(id int) string {
    result := make([]byte, 0)
    number := id
    for number > 0 {
        round := number / dictLength
        remain := number % dictLength
        result = append([]byte{dict[remain]}, result...)
        number = round
    }
    return string(result)
}

func Base62Decode(code string) int {
    var result int = 0
    codeLength := len(code)
    for i, c := range []byte(code) {
        result += bytes.IndexByte(dict, c) * int(math.Pow(dictLength, float64(codeLength - 1 - i)))
    }
    return result
}

建一个socks5代理集群

背景

由于在线工具中部分工具有翻墙的需求,而又没有找到一个稳定的翻墙方案(支持集群),在此背景下产生了这个项目。

架构图

arch.png

功能点

  1. 多账户支持, socks5密码验证

  2. 可用性,集群保证

  3. 流量计算,阈值限制

  4. Custom DNS, 可进行域名白名单的控制

  5. 安全,防止暴力破解socks5密码

表设计

CREATE TABLE `pre_accounts` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `accountname` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `password` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
  `used` int(10) unsigned NOT NULL DEFAULT '0',
  `max` int(10) unsigned NOT NULL DEFAULT '0',
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_accountname` (`accountname`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Socks5的基础知识

这里就不重复写了,链接到 一个简单的Golang实现的Socks5 Proxy

功能点的实现

多账户支持, socks5密码验证

实现 github.com/armon/go-socks5/credentials.go CredentialStore 接口,进行校验;注意:每次proxy都会进行验证,如果请求量比较大,自行选择缓存方式

可用性,集群保证

每台socks5 server心跳上报到etcd,通过阈值,判断socks5的存活

流量计算,阈值限制

github.com/armon/go-socks5/request.go:358 修改这个函数的调用函数

安全,防止暴力破解socks5密码

使用 ratelimit 来保证,如果错误次数过多,则直接将IP加入黑名单;原理可以看之前写过的一篇文章 使用 redis 做限流

Guava的Strings.repeat

如果要自己实现一个repeat的话,最容易想到的可能会这样(示例不考虑int溢出的情况):

public static String repeat(String string, int count) {
    int length = string.length() * count;
    StringBuilder sb = new StringBuilder(length);
    for (int i = 0; i < count; i++) {
        sb.append(string);
    }
    return sb.toString();
}

用图画出来应该是这个样子的:

guava-strings-repeat (1).png

看到guava的实现,在位运算的时候还是小愣了一下,性能是比上面的实现好多了:

public static String repeat(String string, int count) {
  // 先去掉检测相关的判断,只看核心的实现
  final int len = string.length();
  final long longSize = (long) len * (long) count;
  final int size = (int) longSize;

  final char[] array = new char[size];
  string.getChars(0, len, array, 0);
  int n;
  for (n = len; n < size - n; n <<= 1) {
    System.arraycopy(array, 0, array, n, n);
  }
  System.arraycopy(array, 0, array, n, size - n);
  return new String(array);
}

用图画出来是这个样子的:

guava-strings-repeat.png

重点是这段代码:

  for (n = len; n < size - n; n <<= 1) {
    System.arraycopy(array, 0, array, n, n);
  }
  System.arraycopy(array, 0, array, n, size - n);

看下php的内核实现,其实原理一样,就是把现有的字串翻倍,当 [达到一半的长度, count为偶数] 或者 [刚超过一半的长度时, count为奇数] 做最后一次连接,只是写法不太一样:

// sed -n '5012,5058p' /usr/local/src/php-7.0.9/ext/standard/string.c
PHP_FUNCTION(str_repeat)
{
    zend_string        *input_str;        /* Input string */
    zend_long         mult;            /* Multiplier */
    zend_string    *result;        /* Resulting string */
    size_t        result_len;        /* Length of the resulting string */

    if (zend_parse_parameters(ZEND_NUM_ARGS(), "Sl", &input_str, &mult) == FAILURE) {
        return;
    }

    if (mult < 0) {
        php_error_docref(NULL, E_WARNING, "Second argument has to be greater than or equal to 0");
        return;
    }

    /* Don't waste our time if it's empty */
    /* ... or if the multiplier is zero */
    if (ZSTR_LEN(input_str) == 0 || mult == 0)
        RETURN_EMPTY_STRING();

    /* Initialize the result string */
    result = zend_string_safe_alloc(ZSTR_LEN(input_str), mult, 0, 0);
    result_len = ZSTR_LEN(input_str) * mult;

    /* Heavy optimization for situations where input string is 1 byte long */
    if (ZSTR_LEN(input_str) == 1) {
        memset(ZSTR_VAL(result), *ZSTR_VAL(input_str), mult);
    } else {
        char *s, *e, *ee;
        ptrdiff_t l=0;
        memcpy(ZSTR_VAL(result), ZSTR_VAL(input_str), ZSTR_LEN(input_str));
        s = ZSTR_VAL(result);
        e = ZSTR_VAL(result) + ZSTR_LEN(input_str);
        ee = ZSTR_VAL(result) + result_len;

        while (e<ee) {
            l = (e-s) < (ee-e) ? (e-s) : (ee-e);
            memmove(e, s, l);
            e += l;
        }
    }

    ZSTR_VAL(result)[result_len] = '\0';

    RETURN_NEW_STR(result);
}

TOC的生成

TOC是什么

TOC 即 Table of Content,就是将文档中的 h1-h6 抽取出来,并结构化的展示;可以通过链接直接跳转到相应的内容 (这里我使用锚点)

这个本身是没什么好写的,但是涉及到树形的结构化处理,要考虑到怪异的层级问题。

方案

由于原始文档的格式(markdown,html,asciidoc)比较多,但是用于最后展示的还是 html,所以为了方便统一的处理,先将文档都选染成html

toc.png

  1. 使用 ParseDown 渲染markdown

  2. 使用 Crawler 解析html

  3. 提取 h{n},结构化 h{n},并设置html中的 id 属性

  4. 展示树形 TOC

预览

9FBAC831-0717-4728-A06F-CA5CCC4D62AF.png

实现

安装依赖包

composer require erusev/parsedown-extra
composer require symfony/dom-crawler
composer require symfony/css-selector

代码实现

public function getTocAttribute() {
    if ($this->headerNodes) {
        return $this->headerNodes;
    }
    $crawler = new Crawler();
    $crawler->addHtmlContent($this->html, 'utf-8');
    $this->headerNodes = $crawler->filter('h1,h2,h3,h4,h5,h6')->each(function (Crawler $node, $i) {
        $attrId = 'header_' . $i;
        $node->getNode(0)->setAttribute('id', $attrId);
        return [
            'target' => '#' . $attrId,
            'level' => intval(substr($node->nodeName(), 1)),
            'text' => $node->text(),
        ];
    });
    $this->tocedHtml = $crawler->html();
    return $this->headerNodes;
}

树形结构化

class MenuItem {
    public $level;
    public $text;
    public $target;
    public $children = [];
    public function __construct($target, $level, $text) {
        $this->target = $target;
        $this->level = $level;
        $this->text = $text;
    }
}
$rootMenu = new MenuItem(-1, 0, '__root__');
foreach ($wiki->toc as $hn) {
    $menuItem = new MenuItem($hn['target'], $hn['level'], $hn['text']);
    // 每个菜单都从根目录开始寻找
    $lastMenu = $rootMenu;
    while (true) {
        $nestMenu = end($lastMenu->children);
        // 如果比上个层级还大,就挂载到他后面
        if (!$nestMenu && $hn['level'] > $lastMenu->level) {
            $lastMenu->children[] = $menuItem;
            break;
        }
        // 如果循环下来发现没有匹配的层级,则放进根目录
        if ($hn['level'] < $nestMenu->level) {
            $rootMenu->children[] = $menuItem;
            break;
        }
        // 如果和上个层级一样,就赛到上层的children
        if ($nestMenu->level >= $hn['level']) {
            $lastMenu->children[] = $menuItem;
            break;
        }
        $lastMenu = $nestMenu;
    }
}

渲染输出

function tree($menu) {
    $html = '<ul>';
    $html .= '<li><a href="' . $menu->target . '">' . $menu->text . '</a>';
    if ($menu->children) {
        foreach ($menu->children as $subMenu) {
            $html .= tree($subMenu);
        }
    }
    $html .= '</li></ul>';
    return $html;
}
echo '<div class="wiki-index"><div class="wiki-index-header">文章目录</div><div class="wiki-index-body">';
foreach ($rootMenu->children as $menu) {
    echo tree($menu);
}
echo '</div></div>';

阿里云地图流量动画效果的实现分析

预览

阿里云的:
preview1.gif

实验品:
preview2.gif

原理

阿里云的流量动画是先画出一条线,然后改变每个关键颜色的offset,实现类似抛投的效果;渐变的结构如下:

7451DFEF-A99C-4777-B035-FB1F501F5093.png

实现

这里只画出一条简单的直线作为例子,如果需要画出曲线,d3的geo相关的函数可以很方便的作出。

C1B9DB0B-3364-404F-BCAE-682BB2FD942D.png

代码如下:

(function () {

    var svg = d3.select("#container")
                .append("svg")
                .attr("width", 500)
                .attr("height", 200);
    var gradient = svg.append("defs")
                      .append("linearGradient")
                        .attr("id", "gradient")
                        .attr("x1", "0%")
                        .attr("x2", "100%");
    var stops = [];
    var stop;
    stop = gradient.append("stop").attr("stop-opacity", 1).attr("offset", "0%")
        .attr("stop-color", "rgba(255, 255, 255, 0)");
    stops.push(stop);

    // start stop
    stop = gradient.append("stop").attr("stop-opacity", 1).attr("offset", "0%")
        .attr("stop-color", "rgba(255, 255, 255, 0)");
    stops.push(stop);

    // start color
    stop = gradient.append("stop").attr("stop-opacity", 1).attr("offset", "0%")
        .attr("stop-color", "rgba(255, 255, 255, 0)");
    stops.push(stop);

    // end color
    stop = gradient.append("stop").attr("stop-opacity", 1).attr("offset", "100%")
        .attr("stop-color", "#009a61");
    stops.push(stop);

    // end stop
    stop = gradient.append("stop").attr("stop-opacity", 1).attr("offset", "100%")
        .attr("stop-color", "rgba(255, 255, 255, 0)");
    stops.push(stop);

    stop = gradient.append("stop").attr("stop-opacity", 1).attr("offset", "100%")
        .attr("stop-color", "rgba(255, 255, 255, 0)");
    stops.push(stop);


    var route = svg.append("line")
                    .attr("x1", 0)
                    .attr("y1", 100)
                    .attr("x2", 500)
                    .attr("y2", 200)
                    .style("stroke", "url(#gradient)")
                    .style("stroke-width", 4)
                    .style("stroke-linecap", "round")
                    .style("fill", "none");

    var myoffset = 0;
    setInterval(function () {
        myoffset += 1;
        stops.slice(1, 3).forEach(function (stop, i) {
            stop.attr("offset", (myoffset - 20) + "%");
        });
        stops.slice(3, 5).forEach(function (stop, i) {
            stop.attr("offset", myoffset + "%");
        });
        if (myoffset >= 100) {
            myoffset = 0;
        }
    }, 100);
})();