CntChen Blog

FE Software Engineer


  • Home

  • Archives

SQL 温习

Posted on 2017-04-27 |

背景

公司搭建的接口文档平台 rap 使用了 MySQL 存储数据,后台同事对数据库表做了修改,作为 rap 测试的我在操作 rap 后需要去数据库看数据。
所以学习了一下 SQL 的基本操作。

本文内容

  • Ubuntu 终端下连接远程 MySQL server。
  • 复习 SQL 教程的笔记。

安装 client

因为只需要连接远程数据库并修改数据,不需要本地提供 mysql 服务,所以只安装 mysql 客户端。

1
$ sudo apt install mysql-client

连接到远程服务器

1
$ mysql -h 10.xx.xxx.xxx -u root -p -P 3306

其中:
mysql: 终端中的 MySQL 命令
-h:指定连接的 server host
-u:指定连接的用户名
-p:指定连接的密码,如果没有在命令行中指定,会在该命令执行后询问用户输入,这样可以避免黑客通过查看命令历史获取到 server 的连接密码。
直接输入密码的话:

  • -pxxxx,不用加空格,可能跟 MySQL 命令解析命令行参数的规则有关。
  • 或--password=xxxx,完整参数。

-P:指定 server 的 port,默认为 3306,所以可以省略

然后输入数据库密码,就可以连接到远程 server,此时终端会等待用户输入 SQL 语句。

1
2
3
4
Welcome to the MySQL monitor. Commands end with ; or \g.
...
mysql>

数据库查询

  • 注意:

    • 数据库语句后面需要加分号才会执行。
    • 数据库的关键字不区分大小写。
  • 显示数据库

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    mysql> show databases;
    +--------------------+
    | Database |
    +--------------------+
    | test_by_chenhanjie |
    | bbb |
    | ccc |
    | ddd |
    +--------------------+
    4 rows in set (0.00 sec)
  • 选择使用的数据库
    没有默认正在使用的数据库,所以需要先选择。

    1
    2
    3
    mysql> use test_by_chenhanjie;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
  • 表查询

    1
    2
    3
    4
    5
    6
    7
    8
    9
    mysql> show tables;
    +------------------------------+
    | Tables_in_test_by_chenhanjie |
    +------------------------------+
    | persons |
    | scores |
    | sss |
    | test |
    +------------------------------+

数据库操作

  • 新建数据库

    1
    2
    mysql> create database test_by_chenhanjie;
    Query OK, 1 row affected (0.03 sec)
  • 新建表

    1
    mysql> create table test ( id int, score int, info text);

此处省略一万字。。。

参考资料

参考资料不是放在最后的吗,怎么出了个大标题?
因为一个一个讲命令没有太大意义,不如系统看下文档。我看的是 w3schools 的教程,顺便强迫自己看英文。

https://www.w3schools.com/sql/default.asp

应该关注的点

  • 数据库组成
    一个数据库服务(RDBMS)包含多个数据库(Database);一个数据库由多个表(Table)组成;一个表由多个记录(Record)组成,一个记录为一行(Row);一个记录有多个字段(Field),一个表中的同一个字段为一列(Row)。

  • SQL 语法
    SQL 语法的关键在于理解关键字和关键字的用法,关键字是基础,对一个 SQL 语句的理解是思维的挑战和提示。

  • DML 和 DDL
    DML 对数据库中的数据进行增删改查操作;DDL 对数据库和表进行操作,用于定义数据库。

  • 数据类型
    数据类型是表中字段存储的数据类型,如INT TEXT等。

  • SQL 函数
    SQL 内置的函数可以用于数据的计算,如COUNT NOW等。

  • 数据筛选
    数据条件筛选是语句正确执行的关键。

  • 操作符
    包括算术操作符,位操作符,比较操作符,复合操作符(Compound Operators)和逻辑操作符。

  • 难点
    难点是学习中可能带来疑惑的地方,关键在于理解和实践。我总结的几个点:

    • 联合(Join)
    • 条件查询(Condition Query)
    • 别名(Aliases)
    • 主键(Primary Key)
    • 外键(Foreign Key)
    • 排序(Order)
    • 分组(Group)
    • 空(Null)
    • 操作符(Operator)
    • 索引(Index)
      索引可以提高查询速度,但是会减低修改(增删改)的效率,因为需要同时修改索引。

专业名词

SQL(Structured Query Language)
DML(Database Modify Language)
DDL (Database Define Language)
ANSI(American National Standards Institute)
RDBMS(Relational Database Management System)
ESC (escend)
DESC (descend)

后记

耗时两天,学到很多东西,欢迎批评指正。

EOF

Ubuntu16.10 迁移到 SSD

Posted on 2017-02-11 |

Ubuntu16.10 迁移到 SSD

背景

2016年双十一入手了一块500G的 SSD(Solid State Drive,固态硬盘),打算安装到自己的笔记本上。笔记本的 HDD(Hard Disk Drive,机械硬盘)已经跑了 Ubuntu16.10 + Win10 双系统。光驱位的硬盘支架也装好了,一直虚位以待。工作忙一直拖到了2017年。

公司的 PC 机器也是 Ubuntu16.10,并且安装的软件比较齐全,所以计划将 PC 的 Ubuntu16.10 迁移到 SSD 上,然后在笔记本上运行。

开工准备

  • Ubuntu16.10,PC 上安装,各项迁移步骤运行的环境并作为要迁移到 SSD 的系统。
  • Gparted Partition Editor,图形化的分区工具。
  • 外接硬盘盒,通过 USB 线将 SSD 连接到 PC 上。

基础知识

该章节是计算机启动和系统加载的一些概念,有助于加深对迁移原理的理解,注重实践的话可以直接跳过。

总结不一定准确,仅作为个人理解。干货可以看这篇文章:uefi-boot-how-does-that-actually-work-then

BIOS vs UEFI

BIOS(Basic Input/Output System)和 UEFI(Unified Extensible Firmware Interface )是不同的计算机启动固件(Fireware),需要硬件(通常为主板)支持,相互代替的,其中 UEFI 是比较新的方式。

  • BIOS
    经典的启动固件,会调用磁盘的 MBR,然后由 MBR 中的 loader 继续加载操作系统。
  • UEFI
    UEFI 用来代替 BIOS,并克服 BIOS 的缺点,大多数的 UEFI 固件会提供兼容 BIOS 的启动方式。

  • 区别
    可以看这篇文章:UEFI是什么?与BIOS的区别在哪里?

MBR vs GPT

MBR 与 GPT 用于存储硬盘的分区信息,是不同的硬盘分区表类型。

  • MBR
    MBR 表示 MBR 分区表,MBR 分区表在硬盘开头处存放了特殊的启动分区,称为 MBR(Master Boot Record,主启动记录),包含 Boot Loader 和硬盘逻辑分区。MBR 支持最大约2T的硬盘,最多能划分4个主分区,更多分区需要使用拓展分区实现。
    (MBR在行文中可以表示 MBR 分区表和主启动记录两个意思,注意甄别。)

  • GPT
    GPT 表示 GUID(Globally Unique Identifier) 分区表,是 UEFI 规范的一部分,用于替换 MBR 的分区方式。GPT 没有分区数和分区大小限制。

  • 区别
    可以看这篇文章:What’s the Difference Between GPT and MBR When Partitioning a Drive

File System

File System(文件系统)是存储媒介中文件存储的组织方式。
不同的文件系统类型有不同的速度,灵活性,安全性和占用空间。不同操作系统只支持特定的文件系统类型。
常见的文件系统类型有 FAT16,FAT32,NTFS,EXT3,EXT4,HFS 等。

磁盘发展史

Wikipedia 上有许多关于磁盘的资料,在磁盘分区上,我猜测的发展脉络是这样的:

  1. 磁盘跟内存一样直接物理寻址去访问数据;
  2. 为了方便,建立数据 Index,有了 File System;
  3. 需要多个分区,搞出了 Partition Tabel。

小结

  • BIOS/UEFI 跟 MBR/GPT 是不同层级的,BIOS/UEFI 是 Fireware,MBR/GPT 是分区表。
  • 推荐的使用方式: BIOS + MBR 或 UEFI + GPT:

    If you want to do a ‘BIOS compatibility’ type installation, you probably want to install to an MBR formatted disk.
    If you want to do a UEFI native installation, you probably want to install to a GPT formatted disk.

  • 理论上来说是可以组合使用的:

    Of course, to make life complicated, many firmwares can boot BIOS-style from a GPT formatted disk.
    UEFI firmwares are in fact technically required to be able to boot UEFI-style from an MBR formatted disk.

  • Windows 通常会要求 UEFI 的启动方式使用 GPT,不然不给继续安装。

SSD 分区

硬盘状态

使用外接硬盘盒,将 SSD 连接到 PC 机上,先查看硬盘状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ sudo fdisk -l
Disk /dev/sda: 465.8 GiB, 500107862016 bytes, 976773168 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes
Disklabel type: dos
Disk identifier: 0xb2708ce0
Device Boot Start End Sectors Size Id Type
/dev/sda1 * 2048 411647 409600 200M 7 HPFS/NTFS/exFAT
/dev/sda2 411648 210126847 209715200 100G 7 HPFS/NTFS/exFAT
/dev/sda3 210128894 913704959 703576066 335.5G f W95 Ext'd (LBA)
/dev/sda5 210128896 703989759 493860864 235.5G 83 Linux
/dev/sda6 703991808 704966655 974848 476M 83 Linux
/dev/sda7 704968704 764067839 59099136 28.2G 83 Linux
/dev/sda8 764069888 771973119 7903232 3.8G 82 Linux swap / Solaris
Partition 3 does not start on physical sector boundary.
Disk /dev/sdb: 489.1 GiB, 525112713216 bytes, 1025610768 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 33553920 bytes

其中/dev/sda为 PC 上的硬盘,装有 Ubuntu16.10 + Win7;/dev/sdb为 SSD,当前 SSD 为空盘。

分区策略

  • 笔记本是 2011 年的机器,主板启动引导好像不支持 UEFI,是用 BIOS。
  • 考虑 SSD 的拓展性,分区表选择 GPL,选用的引导方式为 BIOS + GPT。
  • 此时安装 GRUB 引导对分区划分有要求,具体参考接下文的《GRUB 引导》章节。
  • 先上分区结果:

    (注:前文出现的/dev/sdb1,/dev/sdf1和后面可能出现的/dev/sd#1都为同一个分区,因为多次插拔了 SSD ,所以标识一直按字母序递增)

    • 不建立/swap分区了,因为 Ubuntu17.04也要移除 swap 分区。
    • /dev/sdf1分区,建立 GRUB 引导所需分区,大小为 1M,分区文件类型为unformatted,分区 flag 为bios_grub。
    • /dev/sdf2分区,Linux /boot分区,大小 1G。
    • /dev/sdf3分区,Linux /分区,大小 50G。
    • /dev/sdf4分区,Linux /home分区,大小 300G。

分区操作

分区操作在 Gparted 软件中完成,命令行fdisk和parted也可以操作,但是我不熟悉。

  • 建立分区表
    SSD 是一个空磁盘,此时并没有分区表,所以要先建立分区表。分区表的格式选用 GPT:

    1. 打开 Gparted,点击 Device –> Created Partition Table。
    2. 选择partition tabel type为gpt,然后点击Apply。

  • 建立 GRUB 所需分区

    1. 分区大小为1M,分区类型为unformatted。

    2. 在新建的分区上点击右键,选择managerFlags,然后选中bios_grub选项。

  • 建立 Linux 系统分区
    根据上文<<分区策略>>章节,依次建立其他分区,分区的文件格式选择ext4。
    分区结果:

    1
    2
    3
    4
    5
    6
    $ sudo fdisk -l /dev/sdh
    Device Start End Sectors Size Type
    /dev/sdh1 2048 4095 2048 1M BIOS boot
    /dev/sdh2 4096 2101247 2097152 1G Linux filesystem
    /dev/sdh3 2101248 106958847 104857600 50G Linux filesystem
    /dev/sdh4 106958848 736104447 629145600 300G Linux filesystem

GRUB 引导

GRUB 是什么

GRUB(Grand Unified Boot loader)是硬盘中的软件,引导器(loader)的一种。目前主流版本是 GRUB2,可以看 GRUB2 中文介绍。

GRUB 用于从多操作系统的计算机中选择一个系统来启动,或从系统分区中选择特殊的内核配置。

provides a user the choice to boot one of multiple operating systems installed on a computer or select a specific kernel configuration available on a particular operating system’s partitions. – GRUB

示例:

如图:第一个选项和最后一个选项是选择不同的操作系统;第一个选项和第二个选项是选择不同的内核配置。

GRUB 位置

其启动代码(boot.img)直接安装在 MBR 中,然后执行 GRUB 内核镜像(core.img),最后从/boot/grub中读取配置和其他功能代码。
BIOS 引导方式中,MBR 分区表和 GPT 分区表的 GRUB 引导文件所放分区不同:

如图,GRUB 的执行顺序为 boot.img –> core.img –> /boot/grub/。

  • 在 MBR 分区表中,boot.img 和 core.img 都在 MBR 中。MBR 虽然只占用一个扇区(512Byte),但是其所在的磁道是空闲的,不会用于分区,可以放下 core.img。

    Some MBR code loads additional code for a boot manager from the first track of the disk, which it assumes to be “free” space that is not allocated to any disk partition, and executes it. – MBR

  • 在 GPT 分区表中,MBR 为 protected MBR(为兼容 MBR,在硬盘起始位置保留的空间),后面并没有空间放core.img,需要建一个专门的分区来放,称为BIOS boot partition,该分区的文件类型为unformatted,flag 为BOIS_grub,该 flag 用于标识core.img所要安装到的分区。若果使用 UEFI 引导,GRUB 读取的是 ESP 分区中的数据,不需要 flag 为 BIOS_grub的分区。

建立 GRUB 引导

使用 grup-install 的教程来安装 GRUB 到 SSD 盘。

  • 挂载 /boot
    挂载 SSD 的/boot为 PC Ubuntu 的/mnt,因为我们需要将 GRUB 配置文件放入 SSD 的/boot/grub中。

    1
    $ sudo mount /dev/sdb2 /mnt
  • 安装 GRUB
    执行以下命令:

    1
    $ sudo grub-install --target=i386-pc --root-directory=/mnt --recheck --debug /dev/sdb

如果看到以下输出,应该就是成功了:

1
2
...
Installation finished. No error reported.

此时/mnt目录下,应该有一个./boot/grub的文件夹:

1
2
3
/mnt/boot/grub ⌚ 20:54:33
$ ls
fonts grubenv i386-pc locale

  • 修复/grub位置
    查看下 PC Ubuntu 的/boot,/grub是直接放置在/boot下的:
    1
    2
    3
    /boot/grub ⌚ 13:30:33
    $ ls
    fonts gfxblacklist.txt grub.cfg grubenv i386-pc locale unicode.pf2

而grub-install /dev/sdb安装的 GRUB 是/mnt/boot/grub,其中/mnt是 SSD /dev/sdb2分区,从 SSD 启动 Ubuntu 的话,/dev/sdb2会挂载为/boot,此时 GRUB 的位置是/boot/boot/grub。而当grub-install /dev/dsa安装 GRUB 到 PC Ubuntu 启动磁盘时,生成的/grub是在/boot/grub。grub-install的处理逻辑应该是先判断/boot路径是否存在,没有就新建。
所以,要将/mnt/boot/grub移动到/mnt/grub:

1
$ sudo mv /mnt/boot/grub /mnt/grub

GRUB 引导修复类型

启动电脑后,当 GRUB 无法按照boot.img –> core.img –> /boot/grub/顺序执行时,会看到命令行界面,等待用户输入命令。此时可以通过输入 GRUB 内置的命令来修复 GRUB 引导。

boot.img是写在 MBR 中的,如果不能执行,直接跟 GRUB 引导方式说再见了,所以执行boot.img一般没问题。boot.img不能识别任何文件系统,core.img的位置是硬编码进boot.img的,所以执行boot.img一般没问题。因此,常见的引导问题集中在/boot/grub/,主要有两种,对应有两种引导修复模式:

  • GRUB Rescue 模式
    GRUB Rescue 模式是 GRUB 无法找到/boot分区,也就无法找到/boot/grub/。修复方法可以参考:grub rescue 模式下修复。

  • GRUB Normal 模式
    GRUB Normal 模式是 GRUB 无法找到 GRUB 菜单grub.cfg,无法选择合适的内核或系统来启动。修复方法可以参考:Boot GNU/Linux from GRUB。

数据复制

该步骤是把 PC 硬盘中几个 Linux 分区的数据拷贝到 SSD 上对应的分区。
(注意:PC Ubuntu 和 SSD Ubuntu 都有/、/boot、/home分区,阅读下文时注意辨别,我有时并没有写得很清晰。)

操作方式

操作的套路是先将 SSD 的分区使用mount命令挂载为 PC 的/mnt,使用cp命令复制数据,再用umount命令移出这个分区;对下一个分区做同样操作。

  • 挂载和移出操作

    1
    2
    3
    4
    // 挂载
    $ sudo mount /dev/sdb2 /mnt
    // 移出
    $ sudo umount /mnt
  • 复制操作
    使用cp指令要加-r,-f,-a参数,-r表示递归复制,-f表示强制覆盖,-a表示保留原文件的属性(mode,ownership,tiemstamps等)

    1
    $ sudo cp -rf -a source destination

复制/boot分区

SSD Ubuntu 的/boot从 PC Ubuntu 上看为/dev/sdb2,将/dev/sdb2挂载为 PC Ubuntu 的/mnt。安装 GRUB 之后,/mnt已经有/grub这个文件夹和默认的lost+found文件夹。
使用cp将 PC 的/boot中其他文件复制到/mnt。结果类似:

1
2
3
4
5
6
7
8
9
10
11
12
/mnt/ ⌚ 13:56:06
$ ls | sort
abi-4.8.0-36-generic
config-4.8.0-36-generic
grub
initrd.img-4.8.0-36-generic
lost+found
memtest86+.bin
memtest86+.elf
memtest86+_multiboot.bin
System.map-4.8.0-36-generic
vmlinuz-4.8.0-36-generic

复制/分区

SSD Ubuntu 的/分区(根目录)比较特殊:一些子目录挂载了其他分区,并存在“伪目录”,不同子目录有特定的用途。

所以复制/分区是有选择性的,不区分子目录进行复制,可能会提示“权限问题”、“无法访问”等错误。

  • 不需要复制的目录

    • /boot,/home,/mnt挂载了其他分区
    • /media /cdrom 挂载可移除的媒体(cdrom 等)
    • /swap交换分区(不需要交换分区了)
  • 需要复制的目录
    主要参考: Linux操作系统备份之二

    • /bin 系统可执行文件
    • /etc 系统核心配置文件
    • /opt 用户程序文件
    • /root root用户主目录
    • /sbin 系统可执行文件
    • /usr 程序安装目录
    • /var 系统运行目录
  • 需要手动创建的目录
    在/mnt中需要给 SSD 的/创建几个空目录。

    • /dev 主要存放与设备(包括外设)有关的文件
    • /proc 正在运行的内核信息映射
    • /sys 硬件设备的驱动程序信息

    这几个目录是 Linux 内核启动后由内核来挂载并存放信息的,不能从运行中的 PC Ubuntu 复制过去,但是需要建立空目录,不然内核启动后会报类似错误:

    1
    mount: mount point /dev does not exist

创建命令:

1
$ sudo mkdir dev proc sys

  • 操作策略
    • 每个目录单独执行复制命令,出错了好处理。

复制/home分区

挂载 SSD Ubuntu/home到 PC Ubuntu /mnt,然后全盘复制:

1
2
$ sudo mount /dev/sdb4 /mnt
$ sudo cp -rf -a /home/* /mnt

挂载/home和/boot分区

SSD Ubuntu 的/home和/boot需要挂载到/,挂载方法为:修改/ect/fstab。

  • 挂载/dev/sda3为 PC Ubuntu /mnt
  • 使用blkid查看 SSD 各分区的 UUID

    1
    2
    3
    4
    $ sudo blkid
    ...
    /dev/sdb3 UUID="a5eb2b0c-2104-4afe-aa78-93396d3e0986" TYPE="ext4" PARTUUID="b2708ce0-07"
    ...
  • 修改 SSD Ubuntu 的fstab文件

    1
    $ sudo vim /mnt/etc/fatab

fstab文件大概是这样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# /etc/fstab: static file system information.
#
# Use 'blkid' to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
#
# <file system> <mount point> <type> <options> <dump> <pass>
# / was on /dev/sda3 during installation
UUID=a5eb2b0c-2104-4afe-aa78-93396d3e0986 / ext4 errors=remount-ro 0 1
#
# /boot was on /dev/sda2 during installation
UUID=8cba10c6-dff2-4300-a630-ab0e7a4782af /boot ext4 defaults 0 2
#
# /home was on /dev/sda4 during installation
UUID=298ba5ad-d306-4b4a-aaa8-54312590dec6 /home ext4 defaults 0 2

GRUB 引导修复

将 SSD 通过 USB 插入到笔记本,开机,选择从 USB 启动。此时应该会是看到类似下图的画面。

说明已经进入到 GRUB 引导程序中,但是没有 GRUB 启动选项,无法继续引导了。距离成功仅剩一步:修复 GRUB 引导。

指定内核启动

  • 指定/boot分区和/grub位置(好像不需要这步,GRUB Rescue 才需要)

    1
    2
    3
    4
    // grub> root=hd0,gpt2
    // grub> prefix=(hd0,gpt2)/grub
    grub> set root=hd0,gpt2
    grub> set prefix=(hd0,gpt2)/grub
  • 设置启动的 Linux 内核

    1
    grub> linux /vmlinuz-4.8.0-36-generic ro root=/dev/sda2
  • 设置虚拟内存

    1
    grub> initrd /initrd.img-4.8.0-36-generic
  • 启动 SSD Ubuntu

    1
    grub> boot

到这一步应该可以启动 SSD 的 Ubuntu,但是下次重新开机,又需要手动指定内核才能启动,通过在 SSD Ubuntu 中重建 GRUB 引导可以解决该问题。

重建 GRUB 引导

从 SSD 开启 Ubuntu 成功后,执行以下命令:

1
2
$ sudo update-grub
$ sudo grub-install /dev/dsa

以上命令更新了 GRUB 可引导的系统/内核列表:/boot/grub/grub.cf,并重新安装了 GRUB。可以参考:Grub2/Installing。

笔记本下次开机,就能看到类似画面:

完成

将 SSD 放入笔记本内置硬盘位,将旧的 HDD 放到光驱位置,开机,完成!(撒花)!

结语

总共花了三天时间搞定这个事情,整理出文章花了N天,查看了很多资料,对计算机开机引导,硬盘分区和 GRUB 算是比较了解了。
现在笔记本有了 SSD + HDD,下一步可能会实践双硬盘的数据备份。
最后放上 HDD 凌乱的分区图,纪念这几年装机折腾的日子。折腾中总有收获。

Refenrences

  • UEFI是什么?与BIOS的区别在哪里

    http://www.ihacksoft.com/uefi.html

  • Linux:系统启动引导过程

    http://zhaodedong.leanote.com/post/Linux%EF%BC%9A%E7%B3%BB%E7%BB%9F%E5%90%AF%E5%8A%A8%E5%BC%95%E5%AF%BC%E8%BF%87%E7%A8%8B

  • What’s the Difference Between GPT and MBR When Partitioning a Drive

    http://www.howtogeek.com/193669/whats-the-difference-between-gpt-and-mbr-when-partitioning-a-drive/

  • 6 Stages of Linux Boot Process (Startup Sequence)

    http://www.thegeekstuff.com/2011/02/Linux-boot-process/

  • GRUB2 中文介绍

    https://my.oschina.net/guol/blog/37373

END

前后端分离开发模式的 mock 平台预研

Posted on 2016-11-15 |

原文地址

引入

mock(模拟): 是在项目测试中,对项目外部或不容易获取的对象/接口,用一个虚拟的对象/接口来模拟,以便测试。

背景

前后端分离

  • 前后端仅仅通过异步接口(AJAX/JSONP)来编程
  • 前后端都各自有自己的开发流程,构建工具,测试集合
  • 关注点分离,前后端变得相对独立并松耦合

前后端分离.png

开发流程

  • 后台编写和维护接口文档,在 API 变化时更新接口文档
  • 后台根据接口文档进行接口开发
  • 前端根据接口文档进行开发
  • 开发完成后联调和提交测试

开发流程.png

面临问题

  • 没有统一的文档编写规范,导致文档越来越乱,无法维护和阅读
  • 开发中的接口增删改等变动,需要较大的沟通成本
  • 对于一个新需求,前端开发的接口调用和自测依赖后台开发完毕
  • 将接口的风险后置,耽误项目时间

解决方法

  • 接口文档服务器 – 解决接口文档编辑和维护的问题
  • mock 数据 – 解决前端开发依赖真实后台接口的问题

接口文档服务器

功能

接口编辑功能

  • 类型1:根据接口描述语法书写接口,并保存为文本文件,然后使用生成工具生成在线接文档(HTML)
    – 也有一些类似 Markdown 的接口文档编辑器,参见:There Are Four API Design Editors To Choose From Now。
    接口书写转换为接口文档.png

  • 类型2:提供在线的接口编辑平台,进行可交互的接口编辑
    接口文档服务器.png

接口查看功能

  • 提供友好的接口文档查看功能

用法

  • 后台开发人员进行接口文档编写
    – 定义接口路径、接口上传字段、接口返回字段、字段含义、字段类型、字段取值
  • 前端开发人员查看接口文档

优点

  • 统一管理和维护接口文档
    – 提供了接口导入、接口模块化、接口版本化、可视化编辑等功能
  • 接口文档规范,可读性强,减少前后端接口沟通成本

前端 mock 方法回顾

前端开发过程中,使用 mock 数据来模拟接口的返回,对开发的代码进行业务逻辑测试。解决开发过程中对后台接口的依赖。

硬编码数据

将 mock 数据写在代码中。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// $.ajax({
// url: ‘https://cntchen.github.io/userInfo’,
// type: 'GET',
// success: function(dt) {
var dt = {
"isSuccess": true,
"errMsg": "This is error.",
"data": {
"userName": "Cntchen",
"about": "FE"
},
};
if (dt.isSuccess) {
render(dt.data);
} else {
console.log(dt.errMsg);
}
// },
// fail: function() {}
// });

优点

  • 可以快速修改测试数据

痛点

  • 无法模拟异步的网络请求,无法测试网络异常
  • 肮代码,联调前需要做较多改动,增加最终上真实环境的切换成本
    – 添加网络请求,修改接口、添加错误控制逻辑
  • 接口文档变化需要手动更新

请求拦截 & mock 数据

hijack(劫持)接口的网络请求,将请求的返回替换为代码中的 mock 数据。

实例

jquery-mockjax

The jQuery Mockjax Plugin provides a simple and extremely flexible interface for mocking or simulating ajax requests and responses

优点

  • 可以模拟异步的网络请求
  • 可以快速修改测试数据

痛点

  • 依赖特定的框架,如Jquery
  • 增加最终上真实环境的切换成本
  • 接口文档变换需要手动更新

本地 mock 服务器

将 mock 数据保存为本地文件。在前端调试的构建流中,用 node 开本地 mock 服务器,请求接口指向本地 mock 服务器,本地 mock 服务器 response mock 文件。

mock 文件

1
2
3
4
5
.mock
├── userInfo.json
├── userStars.json
├── blogs.json
└── following.json

接口调用

https://github.com/CntChen/userInfo –> localhost:port/userInfo

优点

  • 对代码改动较小,联调测试只需要改动接口 url
  • 可以快速修改测试数据

痛点

  • json 文件非常多
  • 接口文档变化需要手动更新

代理服务器

  • 使用 charles 或 fiddler 作为代理服务器
  • 使用代理服务器的 map(映射)& rewrite(重写) 功能

map local

  • 接口请求的返回映射为本地 mock 数据
    https://github.com/CntChen/userInfo –> localPath/userInfo

map local.png

  • 编辑映射规则
    map rule.png

map remote

  • 接口请求的返回映射为另一个远程接口的调用
    map remote.png

rewrite

  • 修改接口调用的 request 或 response,添加/删除/修改 HTTP request line/response line/headers/body
    rewrite data.png

  • 解决跨域问题
    使用 map 后,接口调用的 response 不带 CORS headers,跨域请求在浏览器端会报错。需要重写接口返回的 header,添加 CORS 的字段。
    rewrite cors.png

优点

  • 前端直接请求真实接口,无需修改代码
  • 可以修改接口返回数据

痛点

  • 需要处理跨域问题
  • 一个变更需要代理服务器进行多处改动,配置效率低下
  • 不支持 HTTP method 的区分
    – CORS 的 preflight 请求(OPTION)也会返回数据
  • 需要有远程接口或本地 mock 文件,与使用本地 mock 文件相同的痛点

mock 平台

接口文档服务器

使用接口文档服务器来定义接口数据结构

接口服务器.jpg

mock服务器

mock 服务器根据接口文档自动生成 mock 数据,实现了接口文档即API

mock服务器.jpg

优点

  • 接口文档自动生成和更新 mock 数据
  • 前端代码联调时改动小

缺点

  • 可能存在跨域问题

业界实践

公司实践

没有找到公司级别的框架,除了阿里的 RAP。可能原因:

  • 非关键性、开创性技术,没有太多研究价值
  • 许多大公司是小团队作战,没有统一的 mock 平台
  • 已经有一些稳定的接口,并不存在后台接口没有开发完成的问题
    – 而我们想探究的问题是:前后端同时开发时的 mock

github 开源库

  • faker.js
    随机生成固定字段的 mock 数据,如email,date,images等,支持国际化。

  • blueprint

    A powerful high-level API design language for web APIs.

    一种使用类markdown语法的接口编写语言,使用[json-schema][json-schema]和[mson][mson]作为接口字段描述。有完善的工具链进行接口文件 Edit,Test,Mock,Parse,Converter等。

  • swagger

    Swagger是一种 Rest API 的简单但强大的表示方式,标准的,语言无关,这种表示方式不但人可读,而且机器可读。可以作为 Rest API 的交互式文档,也可以作为 Rest API 的形式化的接口描述,生成客户端和服务端的代码。 –Swagger:Rest API的描述语言

    定义了一套接口文档编写语法,然后可以自动生成接口文档。相关项目: Swagger Editor ,用于编写 API 文档。Swagger UI restful 接口文档在线自动生成与功能测试软件。点击查看Swagger-UI在线示例。

  • wireMock

    WireMock is a simulator for HTTP-based APIs. Some might consider it a service virtualization tool or a mock server. It supports testing of edge cases and failure modes that the real API won’t reliably produce.

商业化方案

  • apiary
    商业化方案,blueprint开源项目的创造者。界面化,提供mock功能,生成各编程语言的调用代码(跟 postman 的 generate code snippets类似)。

其他实践

API Evangelist(API 布道者)

总结

对于前后端分离开发方式,已经有比较成熟的 mock 平台,主要解决了2个问题:

  • 接口文档的编辑和维护
  • 接口 mock 数据的自动生成和更新

后记

预研时间比较有限,有一些新的 mock 模式或优秀的 mock 平台没有覆盖到,欢迎补充。
笔者所在公司选用的平台是 RAP,后续会整理一篇 RAP 实践方面的文章。
问题来了:你开发中的 mock 方式是什么?

References

  • 图解基于node.js实现前后端分离

    http://yalishizhude.github.io/2016/04/19/front-back-separation/

  • TestDouble(介绍 mock 相关的概念)

    http://martinfowler.com/bliki/TestDouble.html

  • There Are Four API Design Editors To Choose From Now

    https://apievangelist.com/2014/11/21/there-are-four-api-design-editors-to-choose-from-now/

  • 联调之痛–契约测试

    http://www.ituring.com.cn/article/42460

  • Swagger:Rest API的描述语言

    https://zhuanlan.zhihu.com/p/21353795

  • Swagger - 前后端分离后的契约

    http://www.cnblogs.com/whitewolf/p/4686154.html

  • Swagger UI教程 API 文档神器 搭配Node使用

    http://www.jianshu.com/p/d6626e6bd72c#

END


最合适的Ajax内容编码类型

Posted on 2016-09-09 |

最合适的Ajax内容编码类型

背景

在公司开发的一个页面的Ajax请求使用了contentType:application/json,被后台的同事要求用x-www-form-urlencoded,撕逼撕不过他,赶紧回来学学知识。

引入

contentType是指http/https发送信息至服务器时的内容编码类型,contentType用于表明发送数据流的类型,服务器根据编码类型使用特定的解析方式,获取数据流中的数据。内容编码类型的作用,有点像本地文件的后缀名。

####问题来了
发送Ajax请求最合适的内容编码类型是什么?

常见的contentType

x-www-form-urlencoded

这是Jquery/Zepto Ajax默认的提交类型。最简例子为:

1
2
3
4
5
6
7
8
9
10
11
let userInfo = {
name: 'CntChen',
info: 'Front-End',
}
$.ajax({
url: 'https://github.com',
type: 'POST',
data: userInfo,
success: (data) => {},
});

此时默认的提交的contentType为application/x-www-form-urlencoded,此时提交的数据将会格式化成:

1
name=CntChen&info=Front-End

HTML的form表单默认的提交编码类型也是x-www-form-urlencoded,可能这就是Jquery/Zepto等类库(其实是Ajax:XMLHttpRequest)也默认使用contentType:x-www-form-urlencoded的原因,毕竟表单的历史比Ajax早多了。–我猜的,待验证

如果请求类型type是GET,格式化的字符串将直接拼接在url后发送到服务端;如果请求类型是POST,格式化的字符串将放在http body的Form Data中发送。

json

使用json内容编码发送数据,最简例子为:

1
2
3
4
5
6
7
8
9
10
11
12
let userInfo = {
name: 'CntChen',
Info: 'Front-End',
}
$.ajax({
url: 'https://github.com',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(userInfo),
success: (data) => {},
});

最主要的不同有3点:

  • 需要显式指定contentType为application/json,覆盖默认的contentType
  • 需要使用JSON.stringify序列化需要提交的数据对象,序列化的结果为:

    1
    {"name":"CntChen","info":"Front-End"}
  • 提交的类型不能为GET,使用GET的话,数据会在url中发送,此时就无法以json字符串的编码发送

multipart/form-data

When you are writing client-side code, all you need to know is use multipart/form-data when your form includes any < input type=”file” > elements.
– multipart/form-data

multipart/form-data主要用于传输文件数据的。

JS对象编码

对于扁平的参数对象,使用x-www-form-urlencoded或json并没有大的差别,后台都可以处理成对象,并且数据编码后的长度差别不大。
但是对于对象中嵌套对象,或对象字段包含数组,此时两种内容编码方式就有较大差别。

格式化demo

对象嵌套

1
2
3
4
5
6
7
{
userInfo :{
name: 'CntChen',
info: 'Front-End',
login: true,
},
}
  • to x-www-form-urlencoded (1)

    1
    userInfo[name]=CntChen&userInfo[info]=Front-End&userInfo[login]=true
  • to json (2)

    1
    {"userInfo":{"name":"CntChen","Info":"Front-End","login":true}}

对象字段为数组

1
2
3
4
5
6
7
8
9
10
11
12
{
authors:[
{
name: 'CntChen',
info: 'Front-End',
},
{
name: 'Eva',
info: 'Banker',
}
],
}
  • to x-www-form-urlencoded (3)

    1
    authors[0][name]=CntChen&authors[0][info]=Front-End&authors[1][name]=Eva&authors[1][info]=Banker
  • to json (4)

    1
    {"authors":[{"name":"CntChen","info":"Front-End"},{"name":"Eva","info":"Banker"}]}

可见:x-www-form-urlencoded是先将对象铺平,然后使用key=value的方式,用&作为间隔。对于嵌套对象的每个字段,都要传输其前缀,如(1)中的userInfo重复传输了3次;(3)中authors传输了4次。
如果对象是多重嵌套的,或者嵌套对象的字段较多,x-www-form-urlencoded会产生更多冗余信息。同时,x-www-form-urlencoded可读性不如json字符串。

回答问题:json最好

较小的传输量

从前文可以看出,使用json字符串的形式,可以减少冗余字段的传输,减少请求的数据量。

补充:可能你会觉得(4)中数组内的name和info也传输了多次,是不是也存在冗余?其实这不是冗余。因为对数组中的各对象,并不要求其具有相同的字段(数组中的对象并不是结构化的),所以不能忽略“相同”的字段名。使用x-www-form-urlencoded编码方式,数组内对象的字段也是重复传输。

请求与返回统一

目前许多前后端交互的返回数据是json字符串,这可能是考虑较小的传输量而作出的选择。同时,ES3.1添加了JSON对象,许多浏览器可以直接使用JSON对象,可以将json字符串解析为JS对象(JSON.parse),将JS对象编码为json字符串(JSON.stringify);
所以使用json编码请求数据,其编码解码非常方便,并且可以保持与后台返回数据的格式一致。
一致是一件很美好的事情。

框架的支持

目前Mvvm的前端框架如React,网络请求通常是提交一个JS对象(传输的时候编码为json字符串)。后台服务器如Koa,接收请求和响应的数据是json字符串。

可读性高

可读性高是json格式自带buff。

结论

赶紧使用contentType=applications/json。

References

  • Ajax

    http://css88.com/doc/zeptojs_api/#$.ajax

  • x-www-form-urlencoded VS json - Pros and Cons. And Vulns.

    http://homakov.blogspot.in/2012/06/x-www-form-urlencoded-vs-json-pros-and.html

  • What does enctype=’multipart/form-data’ mean?

    http://stackoverflow.com/questions/4526273/what-does-enctype-multipart-form-data-mean

  • Can I use JSON

    http://caniuse.com/#search=JSON

  • JSON

    http://www.json.org/

END

React再学习

Posted on 2016-06-14 |

背景

2015年暑假实习的时候实践了一小段时间的React,那时候还是用ES5写的,对React的整个架构和基础方法理解并不深入。2016年5月,希望可以在项目中学习,并使用ES6编写React。

引入

主要参考文档:React Docmument

要点

  • HTML tag小写,React Component 首字母大写

    React’s JSX uses the upper vs. lower case convention to distinguish between local component classes and HTML tags.

  • HTML的类名class和for

    Since class and for are reserved words in JavaScript, the JSX elements for built-inDOM nodes should use the attribute names className and htmlFor respectively, (eg. (div className=”foo” /) ). Custom elements should use class and for directly (eg./ (my-tag class=”foo” /) ).

  • props vs states
    props –> data from parent to child
    states –> for interactivity

  • attributes

    HTML tag 的开发者自定义attribute需要使用data-前缀,否则不渲染该attribute
    React Component则没问题

  • PropTypes
    为了保证复用的模块可以被正确地复用,可以规定接口(Props)的类型。

  • 传递属性

    Rest and Spread Properties
    Destructuring assignment

  • Component Lifecycle

使用ES6编写React

纯ES6的React写法

主要参考:es6-classes

  • class

    1
    2
    3
    4
    5
    class HelloMessage extends React.Component {
    render() {
    return <div>Hello {this.props.name}</div>;
    }
    }
  • propTypes defaultProps要写在类定义外面!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {count: props.initialCount};
this.tick = this.tick.bind(this);
}
tick() {
this.setState({count: this.state.count + 1});
}
render() {
return (
<div onClick={this.tick}>
Clicks: {this.state.count}
</div>
);
}
}
Counter.propTypes = { initialCount: React.PropTypes.number };
Counter.defaultProps = { initialCount: 0 };
  • 没有自动绑定

Methods follow the same semantics as regular ES6 classes, meaning that they don’t automatically bind this to the instance.

主要的ES6+ React语法

主要参考:React on ES6+

  • Component class定义
1
2
3
4
5
6
7
8
9
10
11
12
13
// The ES5 way
var Photo = React.createClass({
handleDoubleTap: function(e) { … },
render: function() { … },
});
// The ES6+ way
class Photo extends React.Component {
handleDoubleTap(e) { … }
render() { … }
}
  • 属性初始化(Property initializers)
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
34
35
36
37
38
39
40
41
42
43
44
// The ES5 way
var Video = React.createClass({
getDefaultProps: function() {
return {
autoPlay: false,
maxLoops: 10,
};
},
getInitialState: function() {
return {
loopsRemaining: this.props.maxLoops,
};
},
propTypes: {
autoPlay: React.PropTypes.bool.isRequired,
maxLoops: React.PropTypes.number.isRequired,
posterFrameSrc: React.PropTypes.string.isRequired,
videoSrc: React.PropTypes.string.isRequired,
},
});
// The ES6+ way
class Video extends React.Component {
static defaultProps = {
autoPlay: false,
maxLoops: 10,
}
static propTypes = {
autoPlay: React.PropTypes.bool.isRequired,
maxLoops: React.PropTypes.number.isRequired,
posterFrameSrc: React.PropTypes.string.isRequired,
videoSrc: React.PropTypes.string.isRequired,
}
state = {
loopsRemaining: this.props.maxLoops,
}
}
  • 箭头函数(Arrow function)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ES6 way
// Manually bind, wherever you need to
class PostInfo extends React.Component {
constructor(props) {
super(props);
// Manually bind this method to the component instance...
this.handleOptionsButtonClick = this.handleOptionsButtonClick.bind(this);
}
handleOptionsButtonClick(e) {
// ...to ensure that 'this' refers to the component instance here.
this.setState({showOptionsModal: true});
}
}
// 推荐,ES6+way,需要使用babel-preset-stage-0,实现`this`绑定
class PostInfo extends React.Component {
handleOptionsButtonClick = (e) => {
this.setState({showOptionsModal: true});
}
}
  • Dynamic property names & template strings

  • Destructuring & spread attributes

使用babel转译(Transpiling)

使用ES6编写react代码,还是需要使用babel转译后才能在浏览器中使用。

使用babel转译ES6 React需要三个babel预设模块:

1
2
3
babel-preset-es2015 此预设包含了所有的 es2015 插件
babel-preset-react 此预设包含了所有的 React 插件
babel-preset-stage-0 此预设包含了 stage 0 中的所有插件

然后在webpack中配置loader:

1
2
3
4
5
6
7
loaders: [{
test: /\.jsx?$/,
exclude: /node_modules/,
loader: 'babel',
query: {
presets: ['es2015', 'stage-0', 'react'],
}]

或者在package.json中添加字段:

1
2
3
4
5
6
"babel": {
"presets": [
"es2015",
"react",
"stage-0"
]

看坑

  • 需要babel-preset-stage-0模块

因为ES6+的React语法有一些是超出ES6规范的,比如ES7 property initializers和static关键字。

拓展

  • JavaScript规范的阶段

如果你对ES6/ES7和各种stage是如何界定的,可以查看这个知乎回答:如何评价 ECMAScript 2016(ES7)只新增2个特性

  • 检查代码所处阶段

推荐一个工具,可以查看你的代码是处于哪个阶段:babel repl

使用方法:贴入代码然后选择代码预设插件,如果没有报错,则该插件可以转译贴入的代码。

转译没有出错
转译没有出错

转译出错
转译出错

参考资料

React

https://github.com/facebook/react

规定接口(Props)的类型

https://facebook.github.io/react/docs/reusable-components.html

React Docmument

http://facebook.github.io/react/docs/getting-started.html

es6-classes

https://facebook.github.io/react/docs/reusable-components.html#es6-classes

React on ES6+

https://babeljs.io/blog/2015/06/07/react-on-es6-plus

如何评价 ECMAScript 2016(ES7)只新增2个特性

https://www.zhihu.com/question/39993685/answer/84166978

babel repl

https://babeljs.io/repl/

完

写博客使用MathJax

Posted on 2016-05-09 |

背景

在写博客时有时候需要插入公式。平时主要使用wiz写作,然后在hexo、简书中发布。

wiz

wiz(为知笔记)支持markdown语法,只要添加文章后缀为.md,此时是不支持编写公式的。
要同时支持markdown和MathJax公式,添加文章后缀为.mdp。

如果你在wiz中,你可以看到下面的公式:
$$test={hello}\times{world}$$

hexo网站

hexo网站默认不支持MathJax,可以通过hexo插件实现–hexo-math。
通过插件来polyfill的原理是hexo发布时可以插入script标签,所以就在页面渲染的时候引入了公式渲染器。
使用方法:

1
npm install hexo-math --save

然后直接就可以使用。hexo-math的其他配置请自行查看。

在hexo中如果安装了hexo-math,应该可以看到以下公式:
$$test={hello}\times{world}$$

简书

简书支持markdown,不支持MathJax,可以使用图片的方式来hack。
其原理是markdown的图片可以通过指定url从网络上引用,然后在url中填入公式作为请求参数,公式生成服务器根据请求参数渲染出图片,然后返回。
可以参考简书中编辑数学公式

可用的在线渲染器:

http://latex.codecogs.com/svg.latex?

在?后面直接填入公式内容,不需要$或$$。

在简书中,你应该可以看到以下公式:

该公式是一张图片。

其他平台的支持情况

  • github不支持

参考资料

hexo-math

https://github.com/akfish/hexo-math

简书中编辑数学公式

http://blog.szrf215.com/p/e8a14ec1c614

完

国内主要地图瓦片坐标系定义及计算原理

Posted on 2016-05-09 |

本文将介绍瓦片坐标相关知识,并提供高德地图、百度地图、谷歌地图的经纬度坐标与瓦片坐标的相互转换方法和类库。

背景

互联网地图服务商的在线地图都通过瓦片的方式提供,称为瓦片地图服务。最常见的地图瓦片是图片格式的,现在有的地图服务商也提供了矢量的瓦片数据,然后在用户端使用Canvas渲染成图片,如node-canvas实现百度地图个性化底图绘制。
在进行地图开发时,为获取特定经纬度所在区域的瓦片和获取瓦片上像素点对应的经纬度,经常需要进行经纬度坐标与瓦片坐标、像素坐标的相互转换。本文将介绍瓦片坐标相关知识,并提供高德地图、百度地图、谷歌地图的经纬度坐标与瓦片坐标的相互转换方法和转换类库–tile-lnglat-transform。

主要经纬度坐标系

国际标准的经纬度坐标是WGS84,Open Street Map、外国版的Google Map都是采用WGS84;高德地图使用的坐标系是GCJ-02;百度地图使用的坐标系是BD-09。高德地图和百度地图都提供了在线的单向坐标转换接口,将其他坐标系换化到自己的坐标系,但这种转换受限于http url请求字段长度和网络请求延迟,批量处理并不实用。离线相互转换可以通过开源JavaScript库coordtransform实现,误差在10米左右。
虽然各地图服务商经纬度坐标系不同,但某一互联网地图的经纬度坐标与瓦片坐标相互转换只与该地图商的墨卡托投影和瓦片编号的定义有关,跟地图商采用的大地坐标系标准无关。

墨卡托投影

使用经纬度表示位置的大地坐标系虽然可以描述地球上点的位置,但是对于地图地理数据在二维平面内展示的场景,需要通过投影的方式将三维空间中的点映射到二维空间中。地图投影需要建立地球表面点与投影平面点的一一对应关系,在互联网地图中常使用墨卡托投影。墨卡托投影是荷兰地理学家墨卡托于1569年提出的一种地球投影方法,该方法是圆柱投影的一种。投影的更多内容,可以查看地图投影的N种姿势。

墨卡托投影示意图

据我了解,各大地图服务商都采用了Web Mercator进行投影,瓦片坐标系的不同主要是投影截取的地球范围不同、瓦片坐标起点不同。

值得注意的是:

  • 墨卡托投影并不是一种坐标系,而是为了在二维平面上展示三维地球而进行的一种空间映射。所以在GIS地图和互联网地图中,虽然用户看到的地图经过了墨卡托投影,但依然使用经纬度坐标来表示地球上点的位置。
  • 在地图绘制和地图可视化时,就需要将地图数据使用投影的方式来呈现。

瓦片切割和瓦片坐标

对于经过墨卡托投影为平面的世界地图,在不同的地图分辨率(整个世界地图的像素大小)下,通过切割的方式将世界地图划分为像素为$256\times256$的地图单元,划分成的每一块地图单元称为地图瓦片。
地图瓦片具有以下特点:

  • 具有唯一的瓦片等级(Level)和瓦片坐标编号(tileX, tileY)。
  • 瓦片分辨率为256$\times$256。
  • 最小的地图等级是0,此时世界地图只由一张瓦片组成。
  • 瓦片等级越高,组成世界地图的瓦片数越多,可以展示的地图越详细。
  • 某一瓦片等级地图的瓦片是由低一级的各瓦片切割成的4个瓦片组成,形成了瓦片金字塔。

瓦片切割(瓦片金字塔)

高德地图瓦片坐标

坐标系定义

高德地图瓦片坐标与Google Map、Open Street Map相同。高德地图的墨卡托投影截取了纬度(约85.05ºS, 约85.05ºN)之间部分的地球,使得投影后的平面地图水平方向和垂直方向长度相等。将墨卡托投影地图的左上角作为瓦片坐标系起点,往左方向为X轴,X轴与北纬85.05º重合且方向向左;往下方向为Y轴,Y轴与东经180º(亦为西经180º)重合且方向向下。瓦片坐标最小等级为0级,此时平面地图是一个像素为256*256的瓦片。在某一瓦片层级Level下,瓦片坐标的X轴和Y轴各有$2^{Level}$个瓦片编号,瓦片地图上的瓦片总数为$2^{Level}\times2^{Level}$。

高德地图Level=2的瓦片坐标编号情况

如上图所示,此时X方向和Y方向各有4个瓦片编号,总瓦片数为16。中国大概位于高德瓦片坐标的(3,1)中。

坐标转换图解

高德地图坐标转换图解

从高德地图坐标转换图解中可以看出,高德地图的坐标转换具有以下特点:

  • 所有坐标转换都在某一瓦片等级下进行,不同瓦片等级下的转换结果不同。
  • 经纬度坐标可以直接转换为瓦片坐标和瓦片像素坐标。
  • 瓦片像素坐标需要结合其瓦片坐标才能得到该像素坐标的经纬度坐标。

坐标转换公式

方法参考:Slippy map tilenames

  • 经纬度坐标(lng, lat)转瓦片坐标(tileX, tileY):

$$tileX=\lfloor\frac{lng + 180}{360}\times{2^{Level}}\rfloor$$
$$tileY=\lfloor{(\frac{1}{2}-\frac{\ln(\tan(lat\times\pi/180)+\sec(lat\times\pi/180))}{2\timesπ}})\times{2^{Level}}\rfloor$$

  • 经纬度坐标(lng, lat)转像素坐标(pixelX, pixelY)

$$pixelX=\lfloor\frac{lng + 180}{360}\times{2^{Level}}\times256\%256\rfloor$$
$$pixelY=\lfloor{(1-\frac{\ln(\tan(lat\times\pi/180)+\sec(lat\times\pi/180))}{2\timesπ}})\times{2^{Level}}\times256\%256\rfloor$$

  • 瓦片(tileX, tileY)的像素坐标(pixelX, pixelY)转经纬度坐标(lng, lat)

$$lng=\frac{tileX+\frac{pixelX}{256}}{2^{Level}}\times360-180$$
$$lat=\arctan({\sinh({\pi-2\times\pi\times\frac{tileY+\frac{pixelY}{256}}{2^{Level}}})})\times\frac{180}{\pi}$$

百度地图瓦片坐标

坐标系定义

百度地图的瓦片坐标系定义与高德地图并不相同,其墨卡托投影的参数也不同。百度地图瓦片坐标以墨卡托投影地图中赤道与0º经线相交位置为原点,沿着赤道往左方向为X轴,沿着0º经线向上方向为Y轴。
百度瓦片坐标定义了另一种二维坐标系,称为百度平面坐标系。百度平面坐标系的坐标原点与百度瓦片坐标原点相同,以瓦片等级18级为基准,规定18级时百度平面坐标的一个单位等于屏幕上的一个像素。平面坐标与地图所展示的级别没有关系,也就是说在1级和18级下,同一个经纬度坐标的百度平面坐标都是一致的。

百度地图Level=2的瓦片坐标编号情况

此时X方向和Y方向各有4个瓦片编号,但是外围的某些瓦片只有部分区域有地图或完全没有地图。没有地图的区域也可以认为其瓦片是无效的,即百度地图中X方向或Y方向的有效瓦片不一定达到$2^{Level}$个。
中国大概位于百度瓦片坐标的(0,0)中。

坐标转换图解

百度地图坐标转换图解

从百度地图坐标转换图解中可以看出,百度地图的坐标转换具有以下特点:

  • 百度经纬度坐标与百度平面坐标可以直接相互转换,并且与瓦片地图等级无关。
  • 经纬度坐标需要先转换为平面坐标,然后才能在某一瓦片等级下转换为瓦片坐标和瓦片像素坐标。
  • 瓦片像素坐标需要结合其瓦片坐标才能得到该像素坐标的平面坐标,然后再转换为经纬度坐标。

坐标转换公式

方法参考:百度地图API详解之地图坐标系统
发现百度JavaScript API的一个bug:百度JavaScript API中经纬度坐标转瓦片坐标bug

  • 经纬度坐标(lng, lat)转平面坐标(pointX, pointY)
    百度经纬度坐标与百度平面坐标的相互转换,并没有公开的公式,需要通过百度地图的API实现。
    主要代码为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // Bmap为百度JavaScript API V2.0的地图对象
    lnglatToPoint(longitude, latitude) {
    let projection = new BMap.MercatorProjection();
    let lnglat = new BMap.Point(longitude, latitude);
    let point = projection.lngLatToPoint(lnglat);
    return {
    pointX: point.x,
    pointY: point.y
    };
    }
  • 平面坐标(pointX, pointY)转经纬度坐标(lng, lat)
    也需要通过百度地图的API实现。
    主要代码为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    pointToLnglat(pointX, pointY) {
    let projection = new BMap.MercatorProjection();
    let point = new BMap.Pixel(pointX, pointY);
    let lnglat = projection.pointToLngLat(point);
    return {
    lng: lnglat.lng,
    lat: lnglat.lat
    };
    }
  • 平面坐标(pointX, pointY)转瓦片坐标(tileX, tileY)

$$tileX=\lfloor\frac{pointX\times2^{Level-18}}{256}\rfloor$$
$$tileY=\lfloor\frac{pointY\times2^{Level-18}}{256}\rfloor$$

  • 平面坐标(pointX, pointY)转像素坐标(pixelX, pixelY)

$$pixelX=\lfloor{pointX\times2^{Level-18}-\lfloor\frac{pointX\times2^{Level-18}}{256}\rfloor\times256}\rfloor$$
$$pixelY=\lfloor{pointY\times2^{Level-18}-{\lfloor\frac{pointY\times2^{Level-18}}{256}\rfloor\times256}}\rfloor$$

  • 瓦片(tileX, tileY)的像素坐标(pixelX, pixelY)转平面坐标(pointX, pointY)

$$pointX=\frac{tileX\times256+pixelX}{2^{Level-18}}$$
$$pointY=\frac{tileY\times256+pixelY}{2^{Level-18}}$$

  • 经纬度坐标与瓦片坐标、像素坐标的相互转换,以平面坐标为中间量进行转换。

吐槽

百度地图JavaScript的代码非常奇葩,非常迷惑:
经纬度类是Point,平面坐标类是Pixel。
经纬度转平面坐标是lngLatToPoint,接收一个Point对象,返回一个Pixel对象。
平面坐标转经纬度坐标是在pointToLngLat,接收Pixel对象,返回一个Point对象。
WTF!

转换类库

本文笔者根据前文介绍的经纬度坐标与瓦片坐标、像素坐标相互转换规则,编写了一个JavaScript类库–tile-lnglat-transform,提供了高德地图、百度地图、谷歌地图的经纬度坐标与瓦片坐标的相互转换。该模块是使用了UMD模块打包方式,可以在node和broswer中使用。
类库地址:https://github.com/CntChen/tile-lnglat-transform
该类库的详细信息及使用方法请在项目主页中查看。

瓦片地图等级范围

  • 瓦片地图等级范围反映了地图可缩放的程度。
  • 虽然最小的瓦片等级是0,但是部分地图并不提供0级或其他较小瓦片等级的地图,因为此时的世界地图将会很小,不能铺满用户设备窗口。

经过实际测试,各地图服务商的瓦片等级和测试链接如下:

  • 百度图片瓦片的层级是[3~18] http://online1.map.bdimg.com/onlinelabel/?qt=tile&x=49310&y=10242&z=18
  • 百度主页的层级是[3~19] http://map.baidu.com/
  • 高德图片瓦片的层级是[1~19] http://wprd03.is.autonavi.com/appmaptile?style=7&x=427289&y=227618&z=19
  • 高德地图官网介绍的高德地图层级:

    获取当前地图缩放级别,在PC上,默认取值范围为[3,18];在移动设备上,默认取值范围为[3-19]

  • 谷歌地图瓦片层级是[0~21] http://mt2.google.cn/vt/lyrs=m@167000000&hl=zh-CN&gl=cn&x=1709157&y=910472&z=21&s=Galil

需注意的问题

  • 瓦片像素坐标的起始点
    • 高德地图、谷歌地图的瓦片坐标起点在左上角,像素坐标(pixelX, pixelY)在瓦片中的起点为左上角。
    • 百度地图中,像素坐标(pixelX, pixelY)的起点为左下角。

参考资料

瓦片地图服务

https://en.wikipedia.org/wiki/Tile_Map_Service

node-canvas实现百度地图个性化底图绘制

http://www.cnblogs.com/well1010/articles/baidu-map-node-canvas.html

tile-lnglat-transform

https://github.com/CntChen/tile-lnglat-transform

coordtransform

https://github.com/wandergis/coordtransform

地图投影的N种姿势

http://blog.sina.com.cn/s/blog_517eed9f0102w4rm.html

Web Mercator

https://en.wikipedia.org/wiki/Web_Mercator

Slippy map tilenames

http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames

百度地图API详解之地图坐标系统

http://www.cnblogs.com/jz1108/archive/2011/07/02/2095376.html

百度JavaScript API中经纬度坐标转瓦片坐标bug

http://cntchen.github.io/2016/05/09/百度JavaScirpt%20%20API中经纬度坐标转瓦片坐标bug/

高德地图层级

http://lbs.amap.com/api/javascript-api/reference/map/

完

百度JavaScript API中经纬度坐标转瓦片坐标bug

Posted on 2016-05-09 |

背景

目前我正在写一篇不同互联网地图经纬度坐标与瓦片坐标相互转换的文章,涉及百度地图、高德地图、谷歌地图,并提供了转换的类库。某一互联网地图的经纬度坐标与瓦片坐标相互转换只与该地图商的墨卡托投影和瓦片编号的定义有关,跟地图商采用的大地坐标系标准无关。
在百度地图的转换上出现一个错误,怀疑是bug。

Bug描述

百度平面坐标

百度地图采用了自己的坐标系统和瓦片编号方法,主要参考文章百度地图API详解之地图坐标系统。
百度地图定义了另一种坐标系,称为百度平面坐标系,百度的平面坐标可以与百度经纬度坐标直接转换,与当前的瓦片等级无关。
具体转换在百度地图JavaScript API中实现–JavaScript API Class MercatorProjection:

1
2
3
4
// 根据球面坐标获得平面坐标
lngLatToPoint(lngLat:Point)
// 根据平面坐标获得球面坐标
pointToLngLat(point:Pixel)

纬度为负数(南纬)时候的转换错误

使用百度地图API详解之地图坐标系统中提供的测试样例:

1
lnglat = {lng: 116.404, lat: 39.915}

得到结果是正确的平面坐标:

1
{ pointX: 12958175, pointY: 4825923.77 }

然后保持经度不变,纬度取负值:

1
lnglat = { lng: 116.404, lat: -39.915}

得到的平面坐标为:

1
{ pointX: 12958175, pointY: -4823785.38 }

怀疑是bug的情况:pointY 的绝对值并不相等。但是也有可能是百度地图的平面坐标原点纬度方向并不在赤道上,所以两个值的绝对值不等。

对{ pointX: 12958175, pointY: -4823785.38 }再转换为经纬度坐标,结果为:

1
{ lng: 116.404, lat: -39.900206 }

疑是bug的情况:转回经纬度后lat的值不等于初始值,而且误差是公里级别的。而{ pointX: 12958175, pointY: 4825923.77 }转换回经纬度的结果是正确的。多次测试发现lng无论正负与pointX的转换都是正确的;lat为正数时,转换为pointY,再由pointY转换为lat的结果是相同的;lat为负数时,相互转换结果不匹配。所以pointToLngLat(point:Pixel)方法应该没有问题,lngLatToPoint(lngLat:Point)方法有错误,并且是在lat为负数的情况时出错。

Bug分析

首先查看百度地图的JavaScript API V2.0:

http://api.map.baidu.com/getscript?v=2.0&ak=E4805d16520de693a3fe707cdc962045&t=20160503160001

相关代码为:
百度API bug代码

格式化一下,完整代码点这里,主要代码为:

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
Au: [75, 60, 45, 30, 15, 0],
iG: [
[-0.0015702102444, 111320.7020616939, 1704480524535203, -10338987376042340, 26112667856603880, -35149669176653700, 26595700718403920, -10725012454188240, 1800819912950474, 82.5],
[8.277824516172526E-4, 111320.7020463578, 6.477955746671607E8, -4.082003173641316E9, 1.077490566351142E10, -1.517187553151559E10, 1.205306533862167E10, -5.124939663577472E9, 9.133119359512032E8, 67.5],
[0.00337398766765, 111320.7020202162, 4481351.045890365, -2.339375119931662E7, 7.968221547186455E7, -1.159649932797253E8, 9.723671115602145E7, -4.366194633752821E7, 8477230.501135234, 52.5],
[0.00220636496208, 111320.7020209128, 51751.86112841131, 3796837.749470245, 992013.7397791013, -1221952.21711287, 1340652.697009075, -620943.6990984312, 144416.9293806241, 37.5],
[-3.441963504368392E-4, 111320.7020576856, 278.2353980772752, 2485758.690035394, 6070.750963243378, 54821.18345352118, 9540.606633304236, -2710.55326746645, 1405.483844121726, 22.5],
[-3.218135878613132E-4, 111320.7020701615, 0.00369383431289, 823725.6402795718, 0.46104986909093, 2351.343141331292, 1.58060784298199, 8.77738589078284, 0.37238884252424, 7.45]
],
{
// ...
for (var d = 0; d < this.Au.length; d++)
if (b.lat >= this.Au[d]) {
c = this.iG[d];
break
}
if (!c)
for (d = this.Au.length - 1; 0 <= d; d--)
if (b.lat <= -this.Au[d]) {
c = this.iG[d];
break
}
// ...
}

以上代码用于求pointY数值,方法是根据lat的数值,分段使用不同的参数:

  • 当lat大于等于0时,从数组 [75, 60, 45, 30, 15, 0]顺序判断lat的范围,然后使用对应的参数。
  • 当lat小于0时,从数组[75, 60, 45, 30, 15, 0]对数值取反并逆序判断lat的范围,然后使用对应的参数。

问题在于:当lat小于0时的第一个判断等价于b.lat <= -this.Au[this.Au.length - 1] –> b.lat<=0,该判断恒成立。一直使用this.Au[this.Au.length - 1]做为pointY计算参数。

修改代码为:

1
2
3
4
5
6
if (!c)
for (d = 0; d < this.Au.length; d++)
if (b.lat <= -this.Au[d]) {
c = this.iG[d];
break
}

转换后结果为:

1
{ pointX: 12958175, pointY: -4825923.77 }

再转换为经纬度:

1
{ lng: 116.404, lat: -39.915 }

问题解决。

结论

应该是百度地图代码写错了。不是故意混淆或加密的问题。稍微做修改就可以了。

BTY

计算参数this.Au二维数组中每行的最后一个数可以组成一个新数组:

1
[82.5, 67.5, 52.5, 37.5, 22.5, 7.45]

最中间两个数和为90,它们外边两个数和为90,最外边两个数和为89.95!不知到百度算法是怎样的,但是有点怀疑有问题。

参考文献

不同互联网地图经纬度坐标与瓦片坐标相互转换的文章

http://cntchen.github.io/2016/05/09/国内主要地图瓦片坐标系定义及计算原理/

百度地图API详解之地图坐标系统

http://www.cnblogs.com/jz1108/archive/2011/07/02/2095376.html

JavaScript API Class MercatorProjection

http://developer.baidu.com/map/reference/index.php?title=Class:地图类型类/MercatorProjection

JavaScript API V2.0

http://api.map.baidu.com/getscript?v=2.0&ak=E4805d16520de693a3fe707cdc962045&t=20160503160001

node-baidusdk.js

https://github.com/CntChen/tile-lnglat-transform/blob/master/src/node-baidusdk.js

完

Dashborad 页面开发记录

Posted on 2015-09-12 |

最大存储的monitor数目:50

1
var MaxMonitorNumber = 50;

使用 CookieInOne 存储已经打开的taskObj(大小有限,大概4K)
使用 LocalStorageInOne 存储已经打开的taskObj(IE8+,大小可达5M)
使用 LocalStorageInOne 存储当前页面的布局“快照”

需要浏览器打开cookie和localStoarge,并且浏览器设置为”退出不清理数据”

基于“快照”的页面布局存储

需求:

保留页面关闭前的打开monitor,保留其布局位置,不管任务是否删除,其占位都应该存在。

  • 快照内容:

    • Wall: freeWall添加的style和布局状态
    • Brick: freeWall添加的style和布局状态,其他内容是空壳,在页面载入后重新填充菜单、canvas等
    • monitorStatus: 各个monitor的图形大小和图形类型
      用于再次进入页面后更新默认monitor菜单
  • 快照收集函数 collectDashboardWallDiv 调用

    • 页面响应式布局的回调 onComplete
    • monitor删除的回调 registerDeleteTaskMonitorCallback
      应对所有monitor删除后没有brick,页面不会回调onComplete的情况
  • 快照释放

    • 进入页面时

    • 不要使用 outerHTML, outerHTML替换 #dashboardwall 的HTML文本,会返回一个新的DOM对象,使得依靠#dashboardwall初始化的 freeWall 只能在所有快照替换后才能初始化,正常情况下 freeWall 可以独立初始化

    1
    2
    3
    4
    5
    6
    7
    8
    function releaseDashboardwall() {
    var dashboardWallDiv = dashboardWallCookie.getItemArray()[0];
    if (dashboardWallDiv) {
    var dashboardWall = $('#dashboardwall');
    var sotred = $(dashboardWallDiv);
    dashboardWall[0].innerHTML = sotred[0].innerHTML; // 不要使用 outerHTML
    }
    }
    • defaultMonitorStatus
      从浏览器存储中获取各个monitor的默认菜单,在addTaskMonitor中填充菜单

基于 freeWall的页面响应式布局

  • freeWall

    • 特性

      • 可拖拽
      • 响应式布局
      • 多种布局风格等等
        -
    • 文档没提但是重要

      • freeWall会在brick中修改style和添加data-* 的标识,例如:data-position,data-handle等,用于标识brick的状态。

      • brick用户不能设置id,因为freeWall会给brick添加id属性,覆盖用户的设置。

      • 拖拽产生的布局优先级最高,brick拖拽后所在坐标位置,不会受fitWidht fitHeight fillHoles等布局变化的影响,需要通过清除brick的data-position来去除拖拽布局的优先级

      • draggable的handle设置方法:brick的data-handle="xxx",xxx为CSS选择器字段

  • 需求: monitor添加,删除,大小变化,拖拽

  • 实现方案:

    • Brick Default Size:300px - 300px
      300大小是最合适的,再小的话图形太小,再大的话窗口宽度X3可能会不够铺开,然后Brick被收缩为0,Chart绘制抛出异常
    1
    2
    3
    4
    5
    6
    7
    .dashboardbrick
    {
    ...
    width: 300px;
    height: 300px;
    ...
    }
    • Browser Width: 至少 1024px
      因为每个brick的大小为300px,保证Brick宽度可以X3

    • freeWall配置参数

      • draggabel:true,允许拖动。

      • cellW cellH 设置brick适配的单位长度,为300,则brick宽度为300,600,900等。最好设置与brick默认大小一致,设置太小会导致brick覆盖。

      • fixSize 必须指定,并为0。为0时brick无法改变宽高比例,但是可以改变大小适应窗口。

      • onComplete调用freeWall.fillHoles();,对删除brick后留下的空间进行填补。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    freeWall.reset({
    draggable: true,
    // selector: '.dashboardbrick',
    // animate: true,
    cache: true,
    cellW: defaultMonitorWidth,
    cellH: defalutMonitorHeight,
    delay: 0,
    fixSize: 0,
    gutterX: 15,
    gutterY: 15,
    onResize: function() {
    freeWall.refresh();
    },
    onComplete: function() {
    freeWall.fillHoles();
    collectDashboardWallDiv();
    }
    });
    • 拖拽设置

      • 不使用brick整个可响应拖拽,因为会freeWall会终止点击事件传递,使得图形设置菜单的popup管理无法处理(依赖document.click);
      • 设置data-handle=".monitortitletext",使用monitor标题作为拖动handle
  • Brick大小变化

    • 在registerChangeMonitorSizeCallback中处理

      1
      2
      3
      4
      5
      freeWall.fixSize({
      block: element,
      width: newWidth,
      height: newHeight
      });
    • 使用freeWall.fixPos()去除拖拽后的位置优先级,使得可以在图形变化后布局到合适位置
      例如:如果brick是一行中最后一个,宽度X2后,如果不freeWall.fixPos(),该brick会直接在原来位置宽度X2,页面宽度超过窗口宽度,页面宽会变成scroll

      1
      2
      3
      freeWall.fixPos({
      block: element
      });
    • Brick宽度X3操作需要窗口有较大的宽度

  • onResize事件

    • 假设用户不会做resize的操作

    • 再测试

看坑

  • freeWall的CSS padding对于brick无效,brick都是position:absolute,请使用margin

  • outerHTML或返回新的DOM对象,参考Element.outerHTML

    Replaces the element with the nodes generated by parsing the string content with the parent of element as the context node for the fragment parsing algorithm.

扩展

对freeWall源码做了修改:

  • 拓展freeWall.fixPos()方法,不带top或left参数时候,删除brick的位置属性data-position

    1
    2
    3
    freeWall.fixPos({
    block: element
    });
  • setDraggable,移除旧的handle绑定,然后设置新的handle绑定。

完

uihelper.js 开发记录

Posted on 2015-09-12 |

简单UI插件

功能

  • popupMgr,弹出菜单管理

  • drawerMgr,构建抽屉

  • Prompt,用户输入的表单控件

  • Action,事件菜单控件

  • Confirm,确认框控件

  • Message,提示信息控件

待开发

  • Status,状态菜单控件

  • Option,选项菜单控件

详细文档

未完成

前端介绍

Posted on 2015-09-12 |

前端简介

简介

  • 基于Browser大数据项目前台页面

  • 实现后台数据交互,前台数据展示和图形化

浏览器要求

  • IE9+

  • 必须开启cookie和localstorage,并且设置退出浏览器时不清理数据

功能 && 完成情况

  • 6个页面:搜索、源设置、任务管理、dashboard、帮助页、登陆页

  • Ajax交互、页面选择性更新、国际化、图形化、UI插件等

  • 基于node.js的模拟后台

Data Compass 前端结构

webroot 目录

1
2
3
4
5
6
<DIR> css
<DIR> font
<DIR> img
<DIR> js
<DIR> jslib
<DIR> page

HTML

  • 每个页面一个HTML

  • 引用 CSS JS

  • header 共用

JS基础库

  • Jquery
    重度依赖
    页面交互:内容替换、添加事件

  • Chart.js
    绘制图形,提供6种默认图形

  • eCharts.js
    绘制图形,强大的可交互处理

  • freeWall.js
    应用于dashboard,可拖拽自适应布局

  • pikaday.js
    时间选择器

  • json2.js
    对象序列化为JSON string 和 反序列化为对象

  • md5.js
    应用于login,md5生成

JS目录

  • 每个页面一个JS

  • util.js
    基础工具集,Cookie存储

  • uihelper.js
    简单UI控件

  • singleajax.js
    Ajax交互

  • languageservice.js / hwsearchlanguage.js
    国际化

  • hwchart.js 基于 Chart.js
    二次开发的绘图库

  • hwecharts.js 基于 eCharts.js
    二次开发的绘图库

  • interface.js
    前后台交互接口,非业务功能

  • commom.js
    各页面复用的js

CSS目录

  • commom.css
    各页面复用的css

  • 每个页面一个css

技术要点

  • ‘单例’的AJAX请求

  • 动态的DOM构建基于隐藏的页面模板
    _tmpl_idcls 为这类DOM的id后缀

  • 基于比较的页面更新,提升性能
    JSS

  • 代码控制图形legend和content大小,避免遮盖

前端下一步工作

  • 维护当前版本,完善功能

  • 基于框架和构建

附:前端技术简介

  • HTML

    HyperText Markup Language 超文本标记语言

    • 风格类似xml的

    • 富文本

    • 职责:结构

    1
    <a href="#"> hello world </a>
  • CSS

    Cascading Style Sheets 层叠样式表

    • 职责:样式
    1
    2
    3
    .helloworld{
    color: red;
    }
  • JS
    JavaScript

    • 类Java语法的脚本语言

    • 标准 ECMAScript

    • 职责:交互逻辑

    1
    console.log('hello world');
  • Browser
    解析和渲染页面,运行JS脚本。对HTML标准,CSS标准,JS标准支持不同

    • IE9+ 现代浏览器

    • 语法走向统一

完

前端进度

Posted on 2015-08-25 |

请不要看这个文档,已经停止更新

已经完成

  • 页面

    • set source page 页面

      1. 基于Cookie的搜索历史记录

      2. 基于window.history的浏览器历史回退

    • manager task page 任务管理页面

    • dashboard

      1. 基于FreeWall的响应式拖拽布局

      2. 多开图形编辑

    • 帮助页面

  • 功能

  • 提示 confirm插件

  • 弹出按钮 action

  • Chart.js 画图插件

    1. 支持其中默认图形

    2. 支持编写其他插件

  • 日期选择插件 pikaday

  • search page 的时间选择器

    1. 时间描述定义timepicker

    2. 时间描述字符处理

  • 简化静态文字的国际化显示

    使用 CSS Class: i18n 然后在 Jquery 中拾取

  • CSS 简单模块化 common.css

  • JS 模块化,代码分离完成,没有使用 AMD or CMD

  • 修改 UiHelper.drawerMgr 使抽屉可以在外部打开和关闭

  • source 提交的后台 mock

需要与后台确认功能 – set source page

  • 服务器提示信息的国际化(前端做还是后台完成)

  • 测试提交出错的提示和逻辑是否正确

  • 提交URL和前台数据更新

未完成功能

  • 单页应用 splunk 是单页应用 使用JS实现局部刷新

  • 页面刷新慢 参考 react 或 使用文本刷新法?

  • 查询时间交互使用插件方便用户输入

  • 添加代码注释和主要修改说明

基于Cookie的搜索历史提示

Posted on 2015-08-25 |

背景

在浏览器的地址栏中输入文字,可以看到有个下拉的选择栏,提供一些用户访问的网址。在公司实习参与的项目,前端页面也需要使用该功能,所以开发了一个基于Cookie的搜索历史提示。这里简单介绍一下实现方法。

实现方法

要点

1. Cookie操作

2. 键盘与鼠标交互

3.

看坑

1. 过期的 escape() unescape()

JavaScript 自带了字符串编码和解码的函数,这些函数属于JavaScript 全局对象。

W3school上的Cookie操作方法代码使用了过期的 escape() 和 unescape。(ECMAScript v3 开始过期)

应该使用

encodeURI() / decodeURI()

或者

encodeURIComponent() / decodeURIComponent() 

我使用的是:encodeURIComponent() decodeURIComponent()

关于两者的不同,可以查看 stackoverflow上的讨论: Best practice: escape, or encodeURI / encodeURIComponent

2. 在历史记录中,如果含有HTML转义字符,则需要做反转义处理

例如:

HTML 可见看到的一条历史记录

// 用户可见字段     
search a > 100

// 真实的HTML字段
<a id="history">search a &gt; 100</a>

使用 document.getElementById('history').innerHTML 得到的是 search a &gt; 100,而真正的搜索历史是 search a > 100。这时候需要反转义。

其实方法很简单,search a &gt; 100字段在 document.getElementById('history') DOM对象中就存在:

document.getElementById('history').innerHMTL // search a &gt; 100
document.getElementById('history').innerText  // search a > 100
document.getElementById('history').textContent // search a > 100

可以参考HTML反转义方法

惊喜

结语

码了个简历

Posted on 2015-08-15 |

背景

前端工程师需要一个网页简历

一直想做一个网页简历,刚好校招开始,所以做了一个简历练练手。

参考了知乎的讨论:一份优秀的前端开发工程师简历是怎么样的?

主要的山寨了前端大神张雯莉的简历:张雯莉的简历

请猛击我的简历查看效果:陈韩杰的简历

基本情况

Finish

  1. 页面CSS布局

  2. 设配IE9+及现代浏览器

  3. 页面切换:鼠标滚轮,键盘及触屏滑动

  4. 页面切换动画及页面内容动画

  5. 可点击的导航栏

TODO

  1. CSS设配移动端,主要是retina下的字体大小

  2. 内容润色

  3. 配套设施搞起来:Blog | Github

页面布局

整个页面禁止滚动,

页面切换

基于hash滚动页面

页面切换使用 Window Location 对象 的 location.hash

hash是什么

hash的格式为窗口文档的url中#号及其后全部字符。#代表网页中的一个位置,其右面的字符,就是该位置的标识符。

例如:

// location.href, 文档完整的url
http://cntchen.github.io/cv/#page1

// lacation.hash,标识符为'page1'
// #page1

hash的作用:

hash可以使页面滚动到文档中指定了标识符的区域,而并不刷新页面。可以通过指定HTML元素的id,为文档添加不同的”滚动标识”。

例如:

//HTML
<div id="page1">
    <!--其他-->    
</div>
<div id="page2">
    <!--其他-->
</div>

//JS 将id="page2"的div滚动到可视区域
window.location.hash = '#page2';

简历页面跳转就使用

可以参考阮一峰的文章:URL的井号

动画

适配

1.触摸事件

2.position垂直布局

  • 高度height确定的block

居中布局可以使用以下方法:

  1. 设置position为非static,使得top样式可以起作用

  2. 设置top: 50%,使得block从50%的高度开始布局

  3. 设置margin-top: half-height-of-block,让位置上移block height的一半

  • 高度不确定的block

例如:div的高度由div里面的内容撑开或div内容是JS动态添加,此时block的高度不确定,无法使用margin-top的方法。纯CSS布局我搜索不到可用的方法,考虑到简历每页的内容高度不高,使用CSS3的calc()简单处理了一下。

top: calc(50% - 200px);

在PC端效果不错,手机端的chrome 正常显示,但在UC和微信页面中不起作用,使用!important处理了一下

top: calc(50% - 200px) !important;
top: 30%;

!important提升CSS样式的优先级,手机端如果calc()不起作用,则会使用top: 30%

结语

码简历过程中遇到一些问题,解决问题的过程学到许多新的知识。但是更主要的是看到自己的不足和前端领域的广阔和大神倍出。更加明确乐自己的发展方向。

Offer快到碗里来吧。

console 输出 DOM 对象

Posted on 2015-08-01 |

背景

前端开发调试的时候,经常需要查看HTML DOM元素对象,DOM元素对象包含了HTML DOM的各种属性,例如:

  • 位置
    element.offsetHeight
    element.offsetWidth

  • 事件绑定
    element.onclick
    element.onkeydown

  • 子孙DOM节点和祖先DOM节点

  • 文本
  • 样式

总之炒鸡有用。

问题

一般使用 console 控制台打出DOM元素对象。

然而,事情是这样子的:在jsfiddle查看代码

HTML:

1
2
3
4
5
<div id="messagecontent">
<div class="message">
<p>hello</p>
</div>
</div>

JS:

1
2
3
4
5
var messageContent = document.getElementById('messagecontent');
var messages = messageContent.childNodes;
console.log(messageContent);
console.log(messages);
console.log(messages[0]);

在 chrome中

alt text

在IE中
alt text

方案

把DOM对象封装成Array的形式

1
2
3
4
5
6
7
var messageContent = document.getElementById('messagecontent');
var messages = messageContent.childNodes;
messages[0].style.color = 'red';
console.log([messageContent]);
console.log(messages);
console.log(messages[0]);
console.log([messages[0]]);

CntChen

15 posts
11 tags
© 2020 CntChen
Powered by Hexo
|
Theme — NexT.Muse v5.1.4