Coder Social home page Coder Social logo

geoffzhu.github.io's People

Contributors

geoffzhu avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Forkers

panamer

geoffzhu.github.io's Issues

混合开发App架构

目前博主所做负责的App是一个高度混合的App,混合了Web(cordova)、Weex和Native。产生这样的杂交方案有一定历史原因,最早它只是一个单纯的Cordova混合App,但随着项目的迭代,web页面的加载和用户体验不能满足产品的需要,所有引入了Weex。在开发的过程中,已有架构有很多问题,导致各端开发效率和体验都不太好,经过思考,有了此文,以便解决多端开发协作遇到的问题。

App架构概述

为了讲清楚为何要做技术优化,我先从几个不同的角度来讲讲App整体的状况和开发中的痛点。

App启动流程

  1. App启动,拉取配置中心配置
// 省略了配置中心的模块概念
{
  host: 'http://75-up.domain-test.cn',
  loginApi: '/api/m/user/login',
  homePage: '/api/m/user/getHomePage',
  ...
  sidebarWeex: {
    '账户中心': 'http://static.domain-test.cn/sy/project/up-mobile-weex/accountCenter.js?weex=1'
  },
  sidebar: {
    '使用反馈': 'http://73-up.domain-test.cn/vip/m/user/getHomePage#/feedback',
    '更多设置': 'http://73-up.domain-test.cn/vip/m/user/getHomePage#/setting'
  }
}
  1. 加载首页,也就是配置中心host + homePage字段拼接后的地址,并将侧边栏的item显示出来,设置上跳转地址。

痛点

在开发上线的过程中,配置中心给我们带来了无尽的痛

  • 不同版本App,配置无法按版本兼容
  • 测试环境配置中心只能配到一台测试服务器,无法做到多人协同测试不同版本
  • 测试环境配置易出错,导致各种问题,比如:首页是sunyun75,侧边栏是sunyun73,导致token错误,被踢出登录
  • 主host只能通过配置中心写入,前端无法连到自己的开发电脑开发,降低了开发效率
  • 配置中心只能配置一个host,无法做到多名前端同时真机调试
  • 客户端因配置中心导致不必要的复杂度上升

Web(cordova)页面

细心的你可能已经发现了,我们的所有web页面是一个单页应用(single page application),后端吐出的HTML始终是同一个,我们通过前端路由(vue-router)来做的多个页面,也就是利用URL中#后的hash。FE还通过异步路由,解决了首屏JS包过大的问题。

每次提测上线,我们通过脚本,将Web页面引用的静态资源,js、css等发布到公司统一的静态资源服务器。

公司提供了两套静态资源服务,一个带缓存,用于线上,一个不带缓存,用于测试。

// 线上
https://static.domain.com

// 测试
https://static.test-domain.cn

痛点

  • 多套测试环境共用测试静态资源服务器,无法同时测试不同版本的web页面。

Weex页面

weex页面打包后实际上也是一个js文件,目前这个js文件也是放在公司统一的静态资源服务器的,和web页面的静态资源一致。

痛点

  • 多套测试环境共用测试静态资源服务器,无法同时测试不同版本的weex页面
  • weex页面因无法知道当前host,所以无法判断请求哪个环境的后端API

可以说配置中心带来了一系列的问题,从开发体验,到测试流程。而且产品同学还在不断的往配置中心中增加配置,比如加个首页banner图的配置之类的,有配置爆炸的趋势,每次上线弄配置也是如履薄冰。

我理想中的Hybrid App架构

我理想中的Hybrid App,应该是只改写Host就能在一个新环境跑起来的。就像我们在web开发中,服务跑在a.com上和跑在b.com上除了域名以外并没有区别。如果混合开发的App也将配置收敛到只有一个域名(Host),那岂不完美。这样做以后,我们可以在App中留一个配置Host的口,以便测试同学和前端同学修改App当前环境,以便开发和测试。多个人测试不同版本,完全通过域名隔离,互不影响。

结合我们目前的项目状况和可以利用的资源,我提供的方案是这样的

开发阶段,前端将App的Host配置为自己开发电脑的局域网IP,这样所有的静态资源、web页面和API均走前端同学的开发电脑(webpack-dev-server)。

测试阶段,前端通过npm run build:37 && npm run deploy:37将源码打包连同index.html发布到测试静态资源服务器的名为/37目录下,如果并行开发几个版本,测试同学可提供新的测试服务器,例如38,前端只需要npm run build:38 && npm run deploy:38即可。

上线,前端通过npm run build:prod && npm run deploy:prod,打包后的JS、CSS发布到带缓存的静态资源服务器的,将index.html发布到不带缓存的静态资源服务器。

后端需要做的改动

根据当前服务所在的域名抓取对应的index.html并缓存在内存中。简易流程如下

var HtmlPath

if (host为测试环境) {
  var 静态资源服务地址 = 'http://static.domain-test.cn/'
  var 当前测试环境编号 = 取得当前测试服务器编号,例如37号
  HtmlPath = 静态资源服务地址 + 项目资源目录 + 当前测试环境编号 + 'index.html'
} else if (host为正式环境) {
  var 线上不带缓存的静态资源服务地址 = 'http://static.domain.com/'
  HtmlPath = 线上不带缓存的静态资源服务地址 + 项目资源目录 + 'index.html'
}

客户端需要的改动

移除配置中心,将侧边栏跳转地址等写在App内或由前端控制。Host配置本地化,测试包留口可切换Host。这样Web页面的部分就解决了多人同时开发和测试的问题。

再说Weex页面,Weex页面的地址也要遵循只依赖Host,大致逻辑如下

if (host为IP) {
  weexBundleUrl = '从当前IP的9082端口获取'
} else if (host为测试域名) {
  var 当前测试环境编号 = 取得当前测试服务器编号,例如37号
  weexBundleUrl = 静态资源服务地址 + weex项目资源目录 + 当前测试环境编号 + 'index.html'
} else if (host为正式域名) {
  weexBundleUrl = 静态资源服务地址 + weex项目资源目录 + 'index.html'
}

通过这样一个方法,就可以做到通过Host,算出对应Weex页面的地址。

写下这篇文章主要是阐述我对整体架构的理解和针对目前团队结构提出的改进方案

推进整套方案是个漫长的过程,,keep going!!!

Geoff的Object-C学习笔记

语法篇

基础数据类型

int、float、double、char

int i = 0;
float i = 0.1;
doublle i = 0.1;
char i = 'A';

OC同时支持C语言的字符串

'C语言字符串'
NSSting: @"OC字符串"

变量名是大小写敏感的

限定词

用于对变量限定内存分配大小,关键词如下

long、long long、short、unsigned、signed

// 原本int是32位,通过限定词,扩展为64位,能存放更大的数据,long long同理
long int i
// 如果你只想存个状态码,不需要太大的整数,可以使用short,会得到16位的int变量
short int i

unsigned和signed代表有没有符号,也就是该变量有没有正负号

条件语句和循环语句

条件语句和循环语句中与js完全一致,不在赘述

goto语句

int i;
a:{
   i++ 
}
if (i < 5) goto a;

函数的定义与调用

<!-- 返回值类型(无使用void) + 名字 + (参数类型 参数名)-->
double multiplication (double a, double b) {
    double c = a * b
    return c
}

mutiplication(2.0, 3.0)

NSLog方法,浮点型采用%f输出,整型采用%d输出,字符串采用%s输出,例如

int a = 0;
float f = 0.1; 或 double f = 0.1;
char s = '字符串';
NSString *p = @"NS字符串";

NSLog(@'整型输出:%d', a);
NSLog(@'浮点型输出:%f', f);
NSLog(@'字符输出:%s', s);
NSLog(@'指针地址输出:%p', p);

%@ 打印NSString

面向对象语法

定义一个类

// 引入Foundation头文件,其中包括常见的NSObject, NSString等类。
#import <Foundation/Foundation.h>

// 其中的 : 代表继承的意思,也就是Class1继承自NSObject
@interface Class1 : NSObject

@end

调用类的方式

[类名 方法名]
[对象名 方法名]

// 初始化People类的一个实例p1
People *p1 = [[People alloc] init];
类的成员变量和属性
@interface Class1 : NSObject
{
    // 成员变量,这个例子是写在了.h文件中,不过已经推荐把成员变量写在.m文件中了
    int i = 0;
    NSString *peopleName
}

@property (nonatomic, strong) NSString *peopleName;
@end

如果声明了一个名为peopleName的属性,xcode会在类内自动创建一个名为_peopleName的成员变量。

对象方法和类方法

OC中定义类中的方法分为减号方法和加号方法,减号方法相当对象方法,也就是JavaScript中的Vue.prototype.fun1, 而加号方法被称为类方法(静态方法),类似于javascript中的Vue.nextTick

// 在.h文件中,声明对象上的方法,例如

-(id) initWithID:(int)newID andAge:(int)newAge andPrice:(float)newPrice;

-(返回值) 函数名: (参数类型1) 参数名1 标签1: (参数类型2) 参数名2 …{ 
    函数内容; 
}

+(void) nextTick;

在.m文件中实现类方法的时候还需要注意,类方法中不能调用成员变量。

再来说说带参数调用,例如调用上面的initWithID方法

People *p1 = [[People alloc] init];
[p1 initWithID:1 andAge:10 andPrice:100.0;
// 调用类方法
[People nextTick]

返回值中的id类型和instanceof类型,id类型可以返回任意类型的值,instanceof类型只能返回当前定义的类的实例类型,这在重写NSObject的init方法时经常使用。

访问修饰符

成员变量访问修饰符

@private
私有,只能在类内(.m文件)使用,无法被继承

@protected
受保护,只能在类内使用,可以被继承

@public
公有,能在类内也能在类外使用,使用语法为 ->,例如:
p1 -> varOfPublic

@package
框架权限,在框架内相当于受保护,框架外相当于私有

方法的访问修饰符

OC中方法没有访问修饰符,如果你不想让某个方法在类外被使用,可以直接在.m文件中定义这个方法,外部自然无法调用。

继承与多态

没有多继承,如果想使用多继承的话,要使用协议。继承的语法在上文中也已经出现了

// Class1 集成自NSObject,Foundation的头文件中有NSObject
#import <Foundation/Foundation.h>
@interface Class1 : NSObject
@end

OC中的多态,OC中可以重写方法,但被重写方法的参数和返回值必须要与父类中的一致。换做术语来说就是,OC只支持方法重写,但不支持方法重载。

self与super

这两个都是在类的实现文件中使用的
super为父类,可以调用父类的方法
self为当前类,可以调用当前实现类中的方法

现代浏览器性能优化-JS篇

众所周知,JS的加载和执行会阻塞浏览器渲染,所以目前业界普遍推荐把script放到</body>之前,以解决js执行时找不到dom等问题。但随着现代浏览器的普及,浏览器为我们提供了更多强大的武器,合理利用,方可大幅提高页面加载速度。

理解渲染过程(HTML Parser)

首先我们从浏览器的角度解释一下从输入URL到页面展示经历了些什么,以如下html文档举例

<html>
<head>
    <link rel="stylesheet" type="text/css" href="/style.css">
    <script type="text/javascript" src="/header.js"></script>
</head>
<body>
  <p>Text</p>
  <script type="text/javascript" src="/main.js"></script>
</body>
</html>

浏览器自上而下读取html文档(此过程叫html parser),当发现style.css文件时,浏览器parser停下来去搞css,等style.css下载并解析完毕,浏览器继续parser。紧接着发现header.js, 于是html parser又停了,浏览器下载并执行完header.js,继续parser。此时屏幕上还什么都没有。...parser,发现<p>,遂将p中文字展示了出来。紧接着又发现main.js,浏览器又停下parser,下载并执行完main.js才继续parser,直到页面渲染完毕。

我们假设header.js中只有一行代码console.log('header'), 但服务器响应很慢,要10秒才能把它返回给浏览器,浏览器执行这段代码需要1ms,那在这 10s+1ms 内,页面将一直空白。浏览器执行JS的时间取决于代码质量和硬件,并不是前端工程师随便可以优化的,所以优化的重点在JS的下载时间。

核心:减少JS下载时间

预先解析DNS

非常简单,效果立竿见影,加快页面加载时间,多用于预解析CDN的地址的DNS

<!--在head标签中,越早越好-->
<link rel="dns-prefetch" href="//example.com">

Preload

浏览器会在遇到如下link标签时,立刻开始下载main.js(不阻塞parser),并放在内存中,但不会执行其中的JS语句。
只有当遇到script标签加载的也是main.js的时候,浏览器才会直接将预先加载的JS执行掉。

<link rel="preload" href="/main.js" as="script">

Prefetch

浏览器会在空闲的时候,下载main.js, 并缓存到disk。当有页面使用的时候,直接从disk缓存中读取。其实就是把决定是否和什么时间加载这个资源的决定权交给浏览器。

如果prefetch还没下载完之前,浏览器发现script标签也引用了同样的资源,浏览器会再次发起请求,这样会严重影响性能的,加载了两次,,所以不要在当前页面马上就要用的资源上用prefetch,要用preload。

<link href="main.js" rel="prefetch">

JS在什么时候执行的(defer和async)

上面我们的例子中,script标签都是在没有多余属性的情况下执行的,只要下载过程结束,浏览器就会将JS执行掉。
defer和async是script标签的两个属性,用于在不阻塞页面文档解析的前提下,控制脚本的下载和执行。

defer,async与下载时机也有关,具体看这张图。

defer的执行时间是在所有元素解析完成之后,DOMContentLoaded 事件触发之前。

async的执行时间是在当前JS脚本下载完成后,所以多个async script是执行顺序是不固定的。所以async只能用于加载一些独立无依赖的代码,比如Google Analysis之类。

完美的结构

前面两节帮我们弄懂了JS的下载和执行时机,那什么样的页面才是完美符合现代浏览器的那?其实关键在于的preload和prefetch!提前告知浏览器,我们的网站马上要用的是什么,以后可能要用的是什么,浏览器才能更快的渲染页面。下面是一段实例代码

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Faster</title>
  <link rel="dns-prefetch" href="//cdn.com/">
  <link rel="preload" href="//js.cdn.com/currentPage-part1.js" as="script">
  <link rel="preload" href="//js.cdn.com/currentPage-part2.js" as="script">
  <link rel="preload" href="//js.cdn.com/currentPage-part3.js" as="script">

  <link rel="prefetch" href="//js.cdn.com/prefetch.js">
</head>
<body>

<script type="text/javascript" src="//js.cdn.com/currentPage-part1.js" defer></script>
<script type="text/javascript" src="//js.cdn.com/currentPage-part2.js" defer></script>
<script type="text/javascript" src="//js.cdn.com/currentPage-part3.js" defer></script>
</body>
</html>

首先,Parser在遇到head中preload时开始下载JS,读到script标签的时候,如果已经下载完了,直接按顺序执行之。如果没下载完,会等到下载完再执行,这样就刚进入页面就开始非阻塞的下载JS了。

其次,页面会在空闲时,加载prefetch的JS,如果之后页面发生跳转,跳转的目标页面引入了prefetch.js,浏览器会直接从disk缓存中读取执行。

将script标签依然放在</body>之前,并增加defer标签,确保老浏览器兼容,并在所有DOM元素解析完成之后执行其中的代码。

至此,完美的HTML结构出炉了。

CSS的下载和解析一样会阻塞浏览器,造成白屏,CSS中的字体文件更是影响首屏渲染关键因素之一,下一篇幅我会结合preload和prefetch,带你一起优化CSS,告诉你什么是最适合现代浏览器的CSS加载策略,期待的话,点个赞吧!

扩展阅读

前端工程师应该知道的Linux知识(一)

随着业务架构演进,服务端和客户端的渲染层均交由FEer处理,越来越多的前端开始接触Linux,做一些安装部署NodeJS项目之类的工作,本篇的主要内容就是介绍Linux下的基础知识和常用命令,帮助前端工程师在服务器中自由自在的傲游。

发行版

目前互联网公司使用的发行版主要是如下三个

  • CentOS
  • Debian
  • Ubuntu

如何知道一台机器的发行版?

lsb_release -a

想知道一台机器的基本信息(内核版本等)?

uname -a

基本概念

###用户与用户组
在Linux下,每个用户都属于一个用户组。文件和目录的读写权限等会根据用户用户组来区分。对于组外的用户,称之为``其他用户```。

目录

在Linux下目录是一等公民,万物开始于/。我们先说明一下系统根目录下常规的一些文件夹都是做什么的?详情见这张表

目录名 简介
/opt 第三方软件
/bin 一般用户可用一些执行文件
/sbin 一些系统执行文件
/tmp 临时文件
/home 一般用户的文件目录
/root root用户的文件目录
/boot 内核文件和开机引导程序等
/dev 设备文件
/etc 各种软件的配置文件和启动脚本
/lib 一些系统依赖的库
/usr 一些系统软件所在目录
/var 一些系统的日志文件和缓存文件

上面这张表中/boot目录以上的是我们一定要理解清楚的目录,至于/boot及以下的目录只需大概了解就好。

包管理工具

每个平台下有自己的包管理工具,作为一个常用Mac的前端工程师,你一定知道Homebrew,自己也常常brew install,安完就能在命令行中使用了,非常方便。Homebrew默认的源是在海外的,很慢很慢,所以大家一般会替换为国内的源,就跟npm切成淘宝源一个意思。

为什么扯这么多Mac上的东西那?原因在于,这套包管理机制和Linux各个发行版是一致的。各个发行版中推荐使用的包管理工具如下

  • CentOS -> yum
  • Debian -> apt-get
  • Ubuntu -> apt-get

通过包管理工具,可以轻松安装相应的包,直接就可以在命令行中使用。

其他还有一些相应格式安装包对应的安装工具,比如用于安装.deb文件的dpkg,还有用于安装.rpm文件的rpm

包的其实也不是什么高深的东西?其实就是一个或多个拥有可执行权限的文件,比如我们执行node命令,实际上就是执行了一个可执行的二进制文件。

我们可以使用which命令查看可执行文件在哪里?例如which node

基本命令

帮助命令

man

绝大部分的软件安装都为执行程序提供了帮助手册
编译安装的软件,可以配置/etc/man.config
指定man手册路径

man命令大多给出的文档非常详细,可能有的时候并不想详读,这时候推荐使用tldr查看帮助文档。tldr是一个在线的帮助文档工具,提供的文档比较社区化,简短易懂,悲伤的是Linux机器上默认是没有tldr的,需要自己安装下,详细文档在这里

目录&文件命令

命令 简介 示例
cp 复制文件/目录 cp path/to/file.ext path/to/copy.ext
cp path/to/file.ext path/to/copy
rm 删除文件/目录 rm path/to/file
rm -r path/to/folder
mv 移动文件/目录 mv source target
which 查找命了文件所在位置 which node
find 查找文件或目录 find root_path -name '*.ext'
find root_path -size +500k -size -10MB
ln 创建文件/目录的链接 ln node-v6.2 node
touch 新建空文件 touch filename
file 查看文件类型 file filename
pwd 查看当前工作目录 pwd
cd 切换工作目录 cd /path
ls 显示目录内容 ls -lh
mkdir 创建新的目录 mkdir –p /directory
du 统计目录及文件的空间占用情况 du -sh
du -h --max-depth=N path/to/folder

Tips: 几个常见的命令行参数
-h -human-readable 以人能看懂的方式展示
-r 递归文件夹
-f -force 强制

内容查看命令

命令 简介 示例
cat 查看显示文件内容 cat filename
more/less 分页查看文件内容 less filename
head/tail 查看文件开头/末尾的部分内容 tail -f web.log
wc 统计文件内容的单词数量等信息
grep 检索过滤文件内容 ```cat package.json

在文件查看命令中,我个人最推荐使用less命令,比较强大,基本可以满足我个人的所有需求
image


此为第一节全部内容,下一节我将详细介绍一些进程管理,CPU,内存使用量相关的命令,敬请期待。

(译)原生CSS网格布局学习笔记

原文地址

注:此文是我翻译的第一篇技术文章。适合有一定CSS原生网格布局使用经验的开发者(读前需要先去了解一下原生CSS网格的语法),原生CSS网格布局(Native CSS grid)截止目前还没有被任何正式版本的浏览器实现。

_以下是来自Oliver Williams的帖子. Oliver已经学习了相当长时间的原生CSS网格,可以说是在CSS网格方面有一定的发言权。在这篇文章中,他将以非同寻常的思路分析自己的CSS网格布局学习之路。我比较赞同他的想法,就是学习一门新技术的时候,把它们拆分成比较小的单元块并配上实例,一步一步的学习。这比直接学习网格布局的所有东西要好太多了。

浏览器原生CSS网格预计会在2017年年初得到支持. 在这之前你需要在浏览器中开启这个实验性的功能 (Firefox实验版默认是开启的). Chrome Canary是当前最好的实现. 同时,火狐有一个非常好的插件叫CSS Grid Inspector, 它能显示出网格的线,它是目前唯一可以在浏览器中运行的此类工具。

在 chrome的地址栏中输入chrome://flags, 找到 ‘实验性网络平台功能’ 并开启它. IE 和 Edge 实现的是一个比较老的网格标准,现在并不受支持。

网格布局不是将零散的块拼到一起

相信我,很快你就能掌握它的.

网格布局只能像左边那样,以矩形的单元块组合起来。并不能像右图那样,由一堆零散的多边形(跟俄罗斯方块那样的块)拼凑。

设计网格布局并不是为了取代弹性盒,相反,它是弹性盒的一种补充

虽然网格布局和弹性盒在某些方面起到相似的作用,而且你可以发现,很多人用弹性盒来实现网格布局,但这并不是设计弹性盒的初衷。Jake Archibald的这篇博文值得一读_Don't use flexbox for overall page layout

这篇博文大概的意思是:

  • Flexbox(弹性盒)是为一维布局设计的(行或列)。

  • CSS网格是为二维设计的.

Rachel Andrews也 说过类似的话:

Flexbox(弹性盒)用于一维布局 – 也就是行或者列. 网格用于二维布局 – 也就是多行多列.

它们可以很好的结合,你可以往弹性容器中放入网格,也可以在网格块中加入flex元素

来看个例子吧。 我们想在一个网格元素(grid item)里垂直居中一段文字, 但我们想要让背景(图片,颜色或渐变)覆盖整个的网格区域。 我们可以使用align-items属性,并把它的值设为center,但是如果这样背景并不会填满整个网格元素的区域。align-items 默认的值是 stretch-你不改变它,始终会填满整个空间的。我们把网格元素设为align-items:center并把网格元素(grid item)设置为一个弹性容器(flex container)。

.grid {
  align-items: stretch;
}

.griditem {
  display: flex;
  align-items: center;
}

给grid-column-end设置负值,意想不到的有用

在小屏幕下,写一个12列的网格,所有格子的跨度都12列。

你可以用网格这样做:

/* For small screens */
.span4, .span6, .spanAll {
  grid-column-end: span 12;
}

/* For large screens */
@media (min-width: 650px) {
  .span4 {
    grid-column-end: span 4;
  }
  .span6 {
    grid-column-end: span 6;
  }
}

这样的显示效果是没什么错误的,当使用CSS网格,重新定义列数非常简单。并且你可以通过设置grid-column-end: -1;来让你的页面始终是从左到右贯穿的。

/* For small screens */
.span4, .span6, .spanAll {
  grid-column-end: -1;
}

在大屏幕上,你想要尽可能的接近12列,但是在移动端,一行大概是1~4列。用media来改变grid-template-columns是非常容易的。

.grid {
  grid-template-columns: 1fr 1fr;
}

@media (min-width: 700px) {
  .grid {
    grid-template-columns: repeat(12, 1fr);
  }
}

有一些元素,我们想让它贯穿整个视口,比如像 header, footer,和一些大图啥的。

对于小屏幕,我们可以这样写:

.wide {
  grid-column: 1 / 3; /* start at 1, end at 3 */
}

不幸的是,当我们换到大屏的时候,一行12列,这些元素将仅仅占满前两列,并不会占满12列,我们需要定义新的grid-column-end,并且把他的值设为 13. 这种方式比较麻烦,还有一种简单的方式,grid-column: 1 / -1;,这样不论在什么屏幕尺寸下,它们都是占满整行的了。就像下面这样:

.wide, .hero, .header, .footer {
  grid-column: 1 / -1;
}

See the Pen Easier media queries with -1 by CSS GRID (@cssgrid) on CodePen.

网格区域可以命名,并使用一些隐含的名字

使用grid-template-areasgrid-line-numbers是两种控制行数的属性,你也可以两个同时用。你可以使用那些隐含的行名去设置你的网格。

.grid {
  grid-template-areas: "main main sidebar sidebar";
}

这段代码,我们能得到四个隐含名字,main-start, main-end, sidebar-start, 和 sidebar-end.

这可能很有用,如果你想重叠内容,无论是在几个网格区域或在一个特定分段的网格区域。

See the Pen implicit line names with grid areas by CSS GRID (@cssgrid) on CodePen.

定义网格区域的另一种方式

就像给网格的行命名,特殊的行名能用于设置网格区域,语法是这样的:

.grid {
  grid-template-areas:
    "header header header"
    "main main sidebar"
    "footer footer footer";
} 、

如果你的布局设计(太多列的布局!没列都要起名字,可能还需要空元素)中有很多空的区域,这种写法稍微有点麻烦。所以对于网格是有另一种写法的,在这种写法中,名字是什么无所谓,只要你合理利用到[name-start][name-end],也能达到自己的布局目的。下面是一个例子:

.grid {
  display: grid;
  grid-template-columns: 20px 100px [main-start] 1fr [main-end] 100px 20px;
  grid-template-rows: 100px [main-start] 100px [main-end] 100px;
}

.griditem1 {
  background-color: red;
  grid-area: main;
}

See the Pen Another way of defining grid-areas by CSS GRID (@cssgrid) on CodePen.

你可能并不想整个页面都用这种方式布局,但是如果你想要结合 grid-area来确定行数的话,它会非常适合。

相等尺寸网格(equal sized box layout)使用vmin单位

虽然你可以在CSS网格中使用任意尺寸的行或列,但是如果想要相等大小的格子并是响应式的,你就需要使用vmin单位了。

.grid {
  grid-template-columns: repeat(5, 20vw);
  grid-template-rows: repeat(5, 20vh);
}

这种布局在台式电脑和笔记本上基本都可以完美显示,但是在手机上,高度大于宽,内容将会溢出,产生出一个横向的滚动条。Dudley Storey写了篇blog说这件事the usefulness of a lesser-known css unit: vmin。这种方法,通过调整容器视口的百分比和内容位置,做到适配各种尺寸的屏幕。

.gridcontainer {
  display: grid;
  width: 100vw;
  height: 100vh;
  justify-content: center;
  align-content: center;
  grid-template-columns: repeat(5, 20vmin);
  grid-template-rows: repeat(5, 20vmin);
}

See the Pen Boxy Layout with CSS Grid and vmin by CSS GRID (@cssgrid) on CodePen.

绝对定位

当我们绝对定位一个网格元素的时候,这个元素会跑到它的容器中,我们可以用grid-column 和 grid-row来定位它。正常情况下,绝对定位使元素脱离文档流,它最适合的使用场景就是想要让元素重叠,并不打乱其他布局元素。除非你为每个元素声明grid-column-startgrid-row-start,要不然即使使用了绝对定位,元素也是不会重叠的。

尝试删除这个例子中div的position: absolute;,思考grid-column 和 grid-row的值,也可以试试修改它们,你就明白是什么意思了。

See the Pen preserving auto-placement with position: absolute by CSS GRID (@cssgrid) on CodePen.

改变网格元素(grid item)的顺序

如果你使用过弹性盒(flexbox)的order 属性,那你已经知道一些相关的知识了。所有的网格元素都有一个默认的order值0。所以如果给一个网格元素设置 order: 1;,这个元素将在所有元素的后面。
你可以给order属性设置负值,让它跑到所有item的前面。

See the Pen Order value by CSS GRID (@cssgrid) on CodePen.

grid中 minmax()的坑

想不想要整行随着内容的宽度而变宽,直到他们达到最大宽度,这种情况你可能想尝试使用 minmax()

.grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(1fr, 300px));
}

不幸的是,像上面这样看似简单,实际上是不行的。如果max小于min的话,css会被忽略。在minmax()fr不能使用。实际上实现这个需求很容易,在grid-template-columnsgrid-template-rows中使用auto,这样item就可以随着内容增大而变大了。
See the Pen The value of auto vs fr by CSS GRID (@cssgrid) on CodePen.

我们可以设置一个 max-width:

.grid {
  display: grid;
  grid-template-columns: repeat(3, auto);
}

.item {
  max-width: 300px;
}

See the Pen The limits of minmax by CSS GRID (@cssgrid) on CodePen.

minmax()的运行方式和使用我还没有完全想出来,虽然如此,我还是写了一篇文章(译者注:Medium entitled是什么我没有理解清楚,原文:I wrote an entire post on Medium entitled) The One Thing I Hate About Grid.

如果你给每一个网格线命名了的话,写布局将容易的多

有多种办法供你选择,如果你就想多写点,你可以给多行设置多个名字。

.grid {
  grid-template-columns: [col1-start] 1fr [col1-end col2-start] 1fr [col2-end];
}

最简单的命名约定使用网格自动编号。不是去写 [col2],而是写为col 2

.griditem1 {
  grid-column-start: col 2;
}

span关键字组合使用,我们就不用去写column-start和column-end中的各种网格线数字了,这样能直观许多。

.grid {
  grid-template-columns: repeat(4, [col] 100px);
}

.griditem1 {
  grid-column: col 2 / span 2;
}

fr单位为什么那个的重要,让你摆脱麻烦的计算

想象一下一行上四等列这种布局,使用百分比是多么的容易grid-template-columns: 25% 25% 25% 25%

但是当想用grid-gap属性的时候那?如果设置grid-gap: 10px,那么这一行上将有三个空隙,每个10px,整体的宽度就是100% + 30px,大于100%滚动条就出来了。虽然可以通过计算来解决,但是如果使用fr,这太容易了grid-template-columns: 1fr 1fr 1fr 1fr

See the Pen fr unit vs percentage by CSS GRID (@cssgrid) on CodePen.

网格布局中第二个我较恶心的点

没有办法强制自动布局算法留下一些行和列是空的。

grid-gap可以让我们设置内容间的距离。grid-row-gapgrid-column-gap能设置行或列之间的间隙,可是如果我想让第一行和第二行相距10px,第二行和第三行相距50px,用现有的网格是没法实现的,除非建个空行占位。

你可能见到过像下面这样写grid-template-area的::

grid-template-rows:
  "header header header"
  "main    main   main"
  "  .       .       ."
  "secondary secondary secondary"
  "footer footer footer";

应该提供一个比较聪明的办法,让布局算法去做这件事。不幸的是,这样写也没用。此语法简单地表示,我们不想将第三行变成一个命名的网格区域。可是grid-template-rows将仍然在那结束。

Some design advice: You don't necessarily need 12 columns (and columns need not be uniform in size)

一些设计上的建议: 你不一点需要12列网格 (每一列不一定大小一致)

12列网格算是web design的默认配置了。Bootstrap引导大家用12列网格,导致很多框架都是12列网格。12既能被3整除也能被4整除,能让我们有更多种布局摆放方式。1行12列,1行6列,1行4列,1行3列,1行2列

虽然有些人喜欢每一个项目总是使用相同的网格,但是你应该去思考你真正需要的,有时候没有必要有更多的列,你应该建立一个网格,对针对你的内容去布局,而不是一个12列网格到处用。

看看这个例子 Gridset. Gridset是一个制作网格非常有用的工具, 但是原生CSS的网格不需要你使用任何工具,但是可以看看它展示的一些良好的网格设计。

看看我写的例子,CSS原生网格是多么的自由啊:

See the Pen text layout with CSS Grid module by CSS GRID (@cssgrid) on CodePen.

前端容灾

笔者也是在工作2年多以后才有接触到前端容灾的概念,毕竟项目不到一定规模,是不需要前端来做容灾的。

什么是容灾

容灾的概念始于后端,指当遇到某个服务器或某个机房发生自然灾害、断网断电等情况下的应急办法,可以保证服务依然可用。
新入行的小伙伴可能有疑问,都断网断电了怎么可能保证网站还可以正常访问那?其实这是对大型网站,理解不深导致的,你认为的网站是这样的

单机

像这种单机的服务自然没法做什么容灾了,这一台机器挂了服务也就挂了。但现在的大型网站,早就不是上图那个样子了,而是这样

同机房多机

甚至这样

多机房多机

最外层那个抗压的服务器一般是Nginx或Apache ,可以做到当某台机器挂了以后屏蔽掉那台机器,这就是后端容灾的基本手段之一,通过集群做到高可用。

说这些只是想让你理解什么是容灾,这不是今天的重点。今天的重点是前端容灾

什么是前端容灾

前端容灾指的是当后端接口挂了,依然可以保证页面展示信息完整。我们以百度首页中新闻模块举例,当你打开百度的时候,服务端渲染好的页面出来以后,需要请求新闻接口拿到数据渲染新闻模块。你的老板告诉你,不论任何情况,必须展示新闻和广告,即使新闻接口挂了。这个时候怎么办,也就是前端容灾的范畴了。

前端容灾可用手段

localstorage缓冲接口数据

新闻模块的接口,每次有返回的时候,都存入localstorage中,以接口路径为key,返回数据为value,当新闻接口请求失败的时候先从localstorage中读上次成功请求时候的数据,展示出来。

备份一份静态数据到CDN

让业主方同学提供一份兜底新闻数据,存放在CDN上,新闻接口返回失败,用户localstorage中也没有数据的时候,去CDN对应地址拉取数据渲染

利用Service worker的caches API做页面接口缓存

Service worker提供了fetch事件可供监听,当页面发出请求的时候,会先过fetch方法,你可以在这里定义任何你需要的缓存策略,比如请求成功后,将结果存入caches。Service worker中,你甚至可以缓存当前页面的HTML,让网站离线运行。

通过上面这些手段,可以保证只要给用户吐出HTML就能保证页面完整性。对于让页面始终可吐出HTML这件事,那就又属于后端,运维同学所做的了。可以借助上面我介绍到的集群的方式,也可以通过CDN的方式(动态DNS)来解决。

上面三种,是笔者在工作中使用过的前端容灾手段,如果你有更好的想法,可以在评论区告诉我,感谢🙏🏻

Geoff的weex视频教程

教程官网

2017年春节,工作以后最长的一次假期~~21天,总要找点事干,于是萌生出做视频教程的想法。毕竟当老师也是我的梦想之一吧!上学时候就挺好为人师的:joy_cat:,虽然自己也是半瓶子醋逛荡 :smile:

做这套视频的另一个原因是自己正在使用weex,相信weex做好了很有前途,也深知weex文档社区还不成熟。自己也可以贡献一份力,能影响几个人算几个人。
整套课程我花了15天录制,2天剪辑。录制的过程中,每天都是上午写稿子,下午录,倒也一点不累,唯独我这口痴比较难受,因为总是说错字,读错音,要一遍一遍的重录:ok_woman:,非常抓狂。

这是我视频教程的处女座,略显粗糙,但毕竟我已经把我对weex的理解都说出来了,无憾。因为我自己是前端出身,可能对Native方面理解错误,以免出现问题,我故意没有涉及Weex中Native模块等内容,这样也给我自己去学习Native留了个坑儿,学好了Native也可以再补。

想学习weex的小伙伴可以加群,这个群我在视频中也有提到,一起交流,keep fighting!
qq group

Workbox3 - ServiceWorker可以如此简单

如果你追求极致的WEB体验,你一定在站点中使用过PWA,也一定面临过在编写serviceWorker代码时的犹豫不决,因为serviceWorker太重要了,一旦注册在用户的浏览器,全站的请求都会被serviceWorker控制,一不留神,小问题也成了大问题了。不过到了现在有了Workbox 3,一切关于serviceWorker都不再是问题。

科普ServiceWorker

如果你已经熟悉ServiceWorker,可以跳过此段。

ServiceWorker是PWA中最重要的一部分,它是一个网站安插在用户浏览器中的大脑。ServiceWorker是这样被注册在页面上的

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
}

为什么说SW(下文将ServiceWorker简称为SW)是网站的大脑?举个例子,如果在www.example.com的根路径下注册了一个SW,那么这个SW将可以控制所有该浏览器向www.example.com站点发起的请求。只需要监听fetch事件,你就可以任意的操纵请求,可以返回从cacheStorage中读的数据,也可以通过fetch API发起新的请求,甚至可以new一个Response,返回给页面。

// 一段糟糕的sw代码,在这个SW注册好以后,整个SW控制站点的所有请求返回的都将是字符串"bad",包括页面的HTML
self.addEventListener('fetch', function(event) {
  event.respondWith(
    new Response('bad')
  );
});

就是因为SW权利太大了,写起来才会如履薄冰,一不小心有些页面资源就不能及时正确的更新了。

一个还算完整ServiceWorker

先来看一个直接手写的SW文件

var cacheStorageKey = 'cachesName'
var cacheList = [
  // 注册成功后要立即缓存的资源列表
]

// 当浏览器解析完sw文件时触发install事件
self.addEventListener('install', function(e) {
  // install事件中一般会将cacheList中要换存的内容通过addAll方法,拉一遍放入caches中
  e.waitUntil(
    caches.open(cacheStorageKey).then(function(cache) {
      return cache.addAll(cacheList)
    })
  )
})

// 激活时触发activate事件
self.addEventListener('activate', function(e) {
  // active事件中通常做一些过期资源释放的工作,匹配到就从caches中删除
  var cacheDeletePromises = caches.keys().then(cacheNames => {
    return Promise.all(cacheNames.map(name => {
      if (name !== cacheStorageKey) {
        return caches.delete(name);
      } else {
        return Promise.resolve();
      }
    }));
  });

  e.waitUntil(
    Promise.all([cacheDeletePromises])
  )
})

self.addEventListener('fetch', function(e) {
  // 在此编写缓存策略
  e.respondWith(
    // 可以通过匹配缓存中的资源返回
    caches.match(e.request)
    // 也可以从远端拉取
    fetch(e.request.url)
    // 也可以自己造
    new Response('自己造')
    // 也可以通过吧fetch拿到的响应通过caches.put方法放进chches
  )
})

其实所有站点SW的install和active都差不多,无非是做预缓存资源列表,更新后缓存清理的工作,逻辑不太复杂,而重点在于fetch事件。上面的代码,我把fetch事件的逻辑省略了,因为如果认真写的话,太多了,而且也不利于讲明白缓存策略这件事。想象一下,你需要根据不同文件的扩展名把不同的资源通过不同的策略缓存在caches中,各种css,js,html,图片,都需要单独搞一套缓存策略,你就知道fetch中需要写多少东西了吧。

Workbox 3

workbox的出现就是为了解决上面的问题的,它被定义为PWA相关的工具集合,其实围绕它的还有一些列工具,如workbox-cli、gulp-workbox、webpack-workbox-plagin等等,不过他们都不是今天的重点,今天想聊的就是workbox本身。

其实可以把workbox理解为Google官方PWA框架,它解决的就是用底层API写PWA太过复杂的问题。这里说的底层API,指的就是去监听SW的install, active, fetch事件做相应逻辑处理等。使用起来是这样的

// 首先引入workbox框架
importScripts('https://storage.googleapis.com/workbox-cdn/releases/3.3.0/workbox-sw.js');
workbox.precaching([
  // 注册成功后要立即缓存的资源列表
])

// html的缓存策略
workbox.routing.registerRoute(
  new RegExp(''.*\.html'),
  workbox.strategies.networkFirst()
);

workbox.routing.registerRoute(
  new RegExp('.*\.(?:js|css)'),
  workbox.strategies.cacheFirst()
);

workbox.routing.registerRoute(
  new RegExp('https://your\.cdn\.com/'),
  workbox.strategies.staleWhileRevalidate()
);

workbox.routing.registerRoute(
  new RegExp('https://your\.img\.cdn\.com/'),
  workbox.strategies.cacheFirst({
    cacheName: 'example:img'
  })
);

上面的代码理解起来就容易的多了,通过workbox.precaching中的是install以后要塞进caches中的内容,workbox.routing.registerRoute中第一个参数是一个正则,匹配经过fetch事件的所有请求,如果匹配上了,就走相应的缓存策略workbox.strategies对象为我们提供了几种最常用的策略,如下

Stale-While-Revalidate

Cache First

Network First

Network Only

Cache Only

你可以通过plagin扩展这些策略,比如增加个缓存过期时间(官方有提供)什么的。甚至可以继续监听fetch事件,然后使用这些策略,官方文档在这里.

经验之谈

在经过一段时间的使用和思考以后,给出我认为最为合理,最为保守的缓存策略。

HTML,如果你想让页面离线可以访问,使用NetworkFirst,如果不需要离线访问,使用NetworkOnly,其他策略均不建议对HTML使用。

CSS和JS,情况比较复杂,因为一般站点的CSS,JS都在CDN上,SW并没有办法判断从CDN上请求下来的资源是否正确(HTTP 200),如果缓存了失败的结果,问题就大了。这种我建议使用Stale-While-Revalidate策略,既保证了页面速度,即便失败,用户刷新一下就更新了。

如果你的CSS,JS与站点在同一个域下,并且文件名中带了Hash版本号,那可以直接使用Cache First策略。

图片建议使用Cache First,并设置一定的失效事件,请求一次就不会再变动了。

上面这些只是普适性的策略,见仁见智。

还有,要牢记,对于不在同一域下的任何资源,绝对不能使用Cache only和Cache first。

参考文献

为ESlint写的一篇

在多人协作的前端团队中,ESlint是非常有必要。同一项目组,互相代码review,多人协同开发同一需求等等,没有ESlint,项目组内必然会充满吐槽和质疑,“WTF,怎么能这样写那?”。

先从心理上接受ESlint

进入项目组,接受项目组指定的规范,摒弃自己的习惯,这才是完美猿的作风。看过很多博客提到ESlint,作者总是有意无意透露出“限制太多,果断把它给关了”之类的意思,我理解,很多老FEer觉得用ESlint,就像是带了一身的枷锁,很难受,写的不舒服。如果项目是个人项目,那就关掉它吧,按照自己喜欢的方式来写,掺合进来的人也不会太多,符合自己代码风格,但多人项目,就要考虑,可能会有新人加入,毫无限制的写,最后的代码真的会让人哭的。

关于ESlint的配置

ESlint可以通过三种方式配置

  1. package.json中的eslintConfig字段
  2. 项目根目录的.eslintrc.js文件
  3. 命令行指定配置文件

目前我们项目多数为Vue项目,脚手架中已经集成了eslint,并且采用项目根目录的.eslintrc.js这种配置方式,可以单独梳理出当前的规则,结合自身FE团队需求,整出一套配置,发布到npm供团队使用,共同维护。以下以eslint-config-suyun为例

如何使用配置

首先团队商定一套eslint规范,写成.eslintrc.js文件并发布到npm,例如博主所在的速运团队为eslint-config-suyun。
使用时候也很方便,先安装预先发布到npm的eslint-config-suyun和babel-eslint(如果使用babel)

在项目根目录.elsintrc.js中配置extends: 'suyun',这样就完成了

修改eslint-config-suyun配置

任何人都可以修改此配置,前提是得到团队中大多数人的支持,并遵从以下三条:

  • 能够帮助发现代码错误的规则,全部开启
  • 配置不应该依赖于某个具体项目,而应尽可能的合理
  • 帮助保持团队的代码风格统一,而不是限制开发体验

总结

代码上的自由,是经验的长期积累换来的,没有积累的新人真的会搬起石头砸自己的脚,团队中培养新人是很有必要的,如果不开lint的话,很容易加快项目代码重构的周期,花更多的时间去填之前的坑。

所以,请使用ESlint吧!

在项目中正确使用fetch的姿势

fetch API 非常的简单

window.fetch(url).then((res) => {
    res.json();
}).then((result) => {
    console.log(result);
})

但是在实际项目开发中,很少会直接这么用,因为存在兼容性问题,易用性/通用性也不够。例如 GET/POST 请求、文件上传、图片获取等,实现方式都有较大的差异。

为什么要做更进一步的 fetch 封

前端开发者应该都很熟悉 jQuery 中的 $.ajax/$.post/$.get 这些方法,在各种框架或库中也存在着不尽相同的 ajax API 方法实现。类似这样的 xhr api 封装屏蔽了不同平台的差异性,简化了 xhr 的使用,在一些简单的应用中似乎已经可以满足需求了,但在中大型应用中还远远不够。

这些实现满足了易用性的需求,却难以完成基于业务特性和接口约定的通用逻辑处理。我在实际的项目中,看到了太多相似而重复的类似如下的代码:

window.fetch(url).then((res) => {
    res.json();
}).then((result) => {
    if (result.status) {
      console.log(result.data);
      ...
      ...
    } else {
      alert(result.msg || 'error')
    }
    
}).catch((err) => {
    console.log(err);
})

里面有大量错误处理的逻辑,还有res.json()这样的解析数据的中间过程。

在一个 fetch 请求的生命周期中,你可能需要做的事情有很多。如果你有考虑过如下列举的一些需求,那么,你该开始做业务层的封装了。
+更简洁的代码逻辑
+统一的数据获取方式、环境切换、数据 mock 处理
+接口约定与出错处理
+前端数据缓存(memory 与 session、local 级别)
+通用的回调处理 (成功/失败处理,xss 注入等数据预处理)
+按钮状态等处理
+公共参数获取与上报
+api 埋点性能收集(如超长耗时、网络超时、50x 出错等)

本仓库中有我封装的vue-fetch,依赖很多,而且是根据业务开发的,只为展示出一些思路,能让使用fetch如下面这样方便

  this.$fetch({
    url: url, *必填
    params: params *必填
    ...
    ...
    ...
  }).then((result) => {
    //不需要在这处理result.status
  })

封装中包含了错误处理和一些可以省略掉的步骤都封装在插件内部。
vue-fetch

巧用Proxy,实现指令解析

这次的故事要从Vue1.x的模版语法开始,Vue模板中通过指令传递props,语法如下

<a :href="foo.bar[0].url">...</a>

指令的值(既foo.bar[0].url)可以是任意对象取值字符串,只要符合JavaScript语法既可。这个字符串会在模板编译阶段转化为render函数,其中的foo.bar[0].url会被解析成['foo', 'bar', '0', 'url']。每次render函数执行时,通过解析后的数组,一个循环就可以取到值了data['foo']['bar']['0']['url']

上周我们团队Coding,就是实现这样一个方法,输入对象取值字符串,解析成数组。 最后的结果可以说是百花齐放,很有意思。我自己收益也颇多,尤其是对Proxy,所以才分享出来。

foo.bar[0].url解析成['foo', 'bar', '0', 'url'],看似简单的对象字符串解析,如果要实现的健壮,也是要下一番功夫的。毕竟javascript对象取值的写法也不少。在Vue1.x中,为了解决对象路径字符串解析的问题,直接祭出了有限状态机,这才算满足了需求。

可以停一分钟思考下,你会怎么实现上面这个方法呢?

...

好了,先来看看一号选手,一行代码

/**
 * 获取子字符串
 * @param  {String} source - 字符串
 * @return - 路径数组
 */
function getPath(source) {
  return source.replace(/]|\'|\"/g, '').replace(/\[/g, '.').split('.');
}

先把 ] ' "都替换掉再把[替换成.,最后split下。虽然满足最最基本满足需求,但稍微特殊点字符串就跪了,例如a['b[']

二号选手,正则一哥,一个我看不懂的正则基本就全搞出来了。

const MATCH_PATH_EXP = /[^\.\[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g;
/**
 * 获取子字符串
 * @param  {String} source - 字符串
 * @return - 路径数组
 */
function getPath(source) {
  const pathArray = [];
  source.replace(MATCH_PATH_EXP, (match, number, quote, subString) => {
    if (quote) {
      pathArray.push(subString.replace(/\\(\\)?/g, '$1')); // 转义符号修正
    } else {
      pathArray.push(number || match); // 有数组匹配数字,没数字匹配字母
    }
  });
  return pathArray;
}

最后一个让我受益颇多的,同样非常简单,而且比正则性能好得多,Proxy。

/**
 * 获取子字符串
 * @param  {String} source - 字符串
 * @return - 路径数组
 */
function getPath(s) {
  const accessLog = [];
  const objName = 'obj';
  const obj = new Proxy({}, {
    get: (o, key) => {
      accessLog.push(key);

      return obj;
    }
  });

  try {
    eval(`${objName}.${s}`);
  } catch (e) {
    const msg = `Invalid object, ${e.message}`;

    throw new Error(msg);
  }

  return accessLog;
}

用Proxy先处理一起,然后直接eval执行传入的字符串,每次返回同一个Proxy。这相当于直接用JS引擎解析了字符串,代码只负责接收引擎解析出来的key就好了,妙~。

5分钟实现一个Koa (Write Koa in 5 minutes)

首发地址

周五组内同学讨论搞一些好玩的东西,有人提到了类似『5分钟实现koa』,『100行实现react』的创意,仔细想了以后,5分钟实现koa并非不能实现,遂有了这篇博客。

准备

先打开koa官网,随意找出了一个代表koa核心功能的的demo就可以,如下

const Koa = require('koa');
const app = new Koa();

// x-response-time
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

// logger
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});

// response
app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

最终要实现的效果是实现的一个5min-koa模块,直接将代码中第一行替换为const Koa = require('./5min-koa');,程序可以正常执行就可以了。

Koa的核心

通过koa官网得知,app.listen方法实际上是如下代码的简写

const http = require('http');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);

所以我们可以先把app.listen实现出来

class Koa {
  constructor() {}
  callback() {
    return (req, res) => {
      // TODO
    }
  }
  listen(port) {
    http.createServer(this.callback()).listen(port);
  }
}

koa的核心分为四部分,分别是

  • context 上下文
  • middleware 中间件
  • request 请求
  • responce 响应

Context

我们先来实现一个最简化版的context,如下

class Context {
  constructor(app, req, res) {
    this.app = app
    this.req = req
    this.res = res
    // 为了尽可能缩短实现时间,我们直接使用原生的res和req,没有实现ctx上的ctx.request ctx.response
    // ctx.request ctx.response只是在原生res和req上包装处理了一层
  }
  // 实现一些demo中使用到的ctx上代理的方法
  get set() { return this.res.setHeader }
  get method() { return this.req.method }
  get url() { return this.req.url }
}

这样就完成了一个最基本的Context,别看小,已经够用了。
每一次有新的请求,都会创建一个新的ctx对象。

Middleware

koa的中间件是一个异步函数,接受两个参数,分别是ctx和next,其中ctx是当前的请求上下文,next是下一个中间件(也是异步函数),这样想来,我们需要一个维护中间件的数组,每次调用app.use就是往数组中push一个一步函数。所以use方法实现如下

use(middleware) {
  this.middlewares.push(middleware)
}

每次有新的请求,我们都需要把这次请求的上下文灌进数组中的每一个中间件里。单单灌进ctx还不够,还要使每个中间件都能通过next函数调用到下一个中间件。当我们调用next函数时,一般是不需要传参数的,而被调用的中间件中一定会接收到ctx和next两个参数。

调用方不需要传参,被调用方却能接到参数,这让我立刻想到bind方法,只要将每一个中间件所需要的ctx和next都提前绑定好,问题就解决了。下面的代码就是通过bind方法,将用户传入的middleware列表转换成next函数列表

let bindedMiddleware = []

for (let i = middlewares.length - 1; i >= 0; i--) {
  if (middlewares.length == i + 1) {
    // 最后一个中间件,next方法设置为Promise.resolve
    bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve))
  } else {
    bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0]))
  }
}

最后我们就得到了一个next函数数组,也就是bindedMiddleware这个变量了。

Request

http.createServer中的回调函数,每次接收到请求的时候会被调用,所以我们在上面callback方法的TODO位置,编写处理请求的代码, 并将上面的middleware列表转next函数列表的代码放入其中。

function handleRequest(ctx, middlewares) {
  if (middlewares && middlewares.length > 0) {
    let bindedMiddleware = []
    for (let i = middlewares.length - 1; i >= 0; i--) {
      if (middlewares.length == i + 1) {
        bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve))
      } else {
        bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0]))
      }
    }
    return bindedMiddleware[0]()
  } else {
    return Promise.resolve()
  }
}

Responce

我们简单出来下相应就好了,直接将ctx.body发送给客户端。

function handleResponse (ctx) {
  return function() {
    ctx.res.writeHead(200, { 'Content-Type': 'text/plain' });
    ctx.res.end(ctx.body);
  }
}

完成Koa类的实现

koa的app实例上面带有on,emit等方法,这是node events模块实现好的东西。直接让Koa类继承自events模块就好了。
我们再将上面实现出来的handleRequest和handleResponse方法放入koa类的callback方法中,得到最终我们实现的Koa,一共58行代码,如下

const http = require('http');
const Emitter = require('events');

class Context {
  constructor(app, req, res) {
    this.app = app;
    this.req = req;
    this.res = res;
  }
  get set() { return this.res.setHeader }
  get method() { return this.req.method }
  get url() { return this.req.url }
}

class Koa extends Emitter{
  constructor(options) {
    super();
    this.options = options
    this.middlewares = [];
  }
  use(middleware) {
    this.middlewares.push(middleware);
  }
  callback() {
    return (req, res) => {
      let ctx = new Context(this, req, res);
      handleRequest(ctx, this.middlewares).then(handleResponse(ctx));
    }
  }
  listen(port) {
    http.createServer(this.callback()).listen(port);
  }
}

function handleRequest(ctx, middlewares) {
  if (middlewares && middlewares.length > 0) {
    let bindedMiddleware = [];
    for (let i = middlewares.length - 1; i >= 0; i--) {
      if (middlewares.length == i + 1) {
        bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve));
      } else {
        bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0]));
      }
    }
    return bindedMiddleware[0]();
  } else {
    return Promise.resolve();
  }
}

function handleResponse (ctx) {
  return function() {
    ctx.res.writeHead(200, { 'Content-Type': 'text/plain' });
    ctx.res.end(ctx.body);
  }
}

module.exports = Koa;

试试跑一下篇首的Demo,没什么问题。

结语

简版实现,码糙理不糙,展示出了koa核心的东西,但少了错误处理,也完全没有考虑性能啥的,需要完善的地方还很多很多。

笔者在写了这个5分钟koa以后去看了koa源码,发现实现思路基本就是这样,相信经过我的这个5分钟koa的洗礼,你去看koa源码一样小菜一碟。

Done!

我为什么使用styled-components

看了不少styled-components的文章,但没怎么找到与我产生共鸣的,所以就自己写一篇了

在写业务代码的过程中经常遇到前端命名规则和后端接口返回不一致的情况,我们需要在代码中做很多烦人的转换,例如

fetch().thne(resp => {
  this.userName = resp.user_nick_name;
})

我们需要这种把user_nick_name转换为userName的适配代码,以便把后端的数据放到前端的各种组件和Store中,这种操作是一种映射,通this.userName = resp.user_nick_name;这句绑定前后端数据的映射关系。

思维稍微发散一下,你会发现,CSS和DOM之间不也是通过这种映射关系绑定才让样式生效的嘛!

<div class="red-button"></div>

上面这段代码的写法,DOM和CSS是完全分离的,我们通过class="red-button"这种方式将CSS样式中的key与DOM绑定到一起,让样式生效。
这样写有好处,样式和DOM分离,单独来看都比较便于维护。

但问题就出在class="red-button"这句绑定上,当我找.red-button具体的样式时,如果项目目录结构比较规范,还算方便的就能找到CSS的文件,然后在文件中搜.red-button,定位红色按钮的样式代码,还算OK。但如果项目目录结构不规范,.red-button可能被定义在整个工程的anywhere。这时候就麻烦了,我一般都会直接全局搜索,并不是那么的舒服。

这时候一定会有人说css-modules,它可以让你像引入js那样引入css为json对象。这样的话,css文件就好找了。

import styles from './style.css';

但你有没有想过,那我们为什么需要写div, 为什么要有绑定映射用的class=

有没有仔细思考过红色按钮最直接的表达方式是什么?答案很简单,就是一个红色按钮而已啊。

<RedButton />

我感觉在JSX或者在DOM上面不段的写className="button", calss="button", class={styles.button},这些重复的class=,这样显得挺笨拙的,不是么?

再看下面这段代码:

.red-button {
  color: red;
}
<div class="red-button"></div>

我们该把关注点放在DOM元素到底是一个div标签还是button标签吗?显然不是,这都是多余的东西。

用styled-components写,烦人的绑定过程消失了,再也见不到class=这种绑定样式的语法了。

const RedButton = styled.div`
  color: red;
`;
<RedButton />

这是我使用styled-components的原因,你的呢?

响应式邮件设计工具推荐

最近工作上需要做一些HTML邮件模版,就是在平时邮箱里收到的那种推广邮件。深入研究之后,才知道这坑有多深。

回到蛮荒时代

最初我的理解是,既然是HTML,我大前端的看家本领!半小时搞定没啥问题。试了才知道,HTML邮件没有任何标准,而且不管是HTML和CSS全部都是被阉割了的(啥?你还想用JS?),并且不同邮件客户端之间差异也很大。对于div和float的处理也带有很多的不确定性,所以浮动布局,flexbox啥的一样也用不了,只能用90年代流行的表格布局,90年代啊!开发体验是奇差无比,更重要的是我并不想去学过时的table布局。

我这次的要做的是出模版,服务端套,还需要考虑到收件人可能用的是手机客户端查看的邮件,所以需要响应式。对于我来说,可以拖拽的邮件工具肯定是不合适的,它并不能很好的实现设计和需求,况且作为一名开发者应以它为耻。

找工具

1. foundation-emails

首先GitHub找到了foundation-emails,扫了一下文档和DEMO,感觉非常好用,还能用sass,狂笑!!等模版做完了,按照文档

npm run build

文档上说这是把所有样式插到行内(没看到会压缩啊),当我执行之后,我看到的是这样的画面。

还是要在build的之前往里面插入服务端模版引擎的循环语句?只能手动去格式化html代码再加循环?我首先试了试第一个在模版里面想替换的地方加几个模版引擎常用的

{{ }}

结果build之后,直接就编译没了,再去GitHub看看,找到了这条还没有修复的issue,看来大家都还没什么优雅的办法。

对于只需要替换个用户名或者只有少部分内容需要动态的邮件,foundation-emails是非常不错的选择,在其中使用响应式非常非常简单,格栅布局,就像这样

<columns large="6" small="12">

2. mjml

在社区里找到了这款工具,用它写的模版大概是这样的, 也很不错。

<mjml>
  <mj-body>
    <mj-container>
      <mj-section>
        <mj-column>
          <mj-image src="/assets/img/easy-and-quick.png"  width="112"></mj-image>
          <mj-text font-size="20px" color="#595959" align="center">Easy and Quick</mj-text>
        </mj-column>
        <mj-column>
          <mj-image src="/assets/img/responsive.png" width="135"></mj-image>
          <mj-text font-size="20px" color="#595959" align="center">Responsive</mj-text>
        </mj-column>
      </mj-section>
      <mj-section>
        <mj-column>
          <mj-button background-color="#F45E43" font-size="15px">Discover</mj-button>
        </mj-column>
      </mj-section>
    </mj-container>
  </mj-body>
</mjml>

而且它还有客户端,客户端中可以导出html,导出的代码还算清新。动态替换的内容,我通过官方demo中发现用[[ ]]包裹就没什么事了。

写在最后

这篇就是推荐俩工具。如果需要,请留言留言,我可以出个小教程啥的,完!

现代浏览器性能优化-CSS篇

我来填坑了,CSS篇终于写出来了,如果你没看过前面的JS篇,可以在这里观看

众所周知,CSS的加载会阻塞浏览器渲染或是引起浏览器重绘,目前业界普遍推荐把CSS放到<head>中,防止在CSS还没加载完,DOM就已经绘制出来了,造成CSS加载完成后的重绘。那在现代浏览器中我们有没有办法提高首屏渲染速度那?

你是不是经常在第一次打开某个网站的时候看到这种情况,本来的页面是这样的

实际上刚加载出来的是这样的

字体文件没加载出来,或者加载的太慢了

理解CSS解析过程

以下面这段HTML为例,解释一遍CSS加载解析的过程。

<html>
<head>
  <!-- headStyle.css中存在字体文件webfont.woff2 -->
  <link rel="stylesheet" type="text/css" href="/headStyle.css">
</head>
<body>
  <p>Text</p>
  <link rel="stylesheet" type="text/css" href="/bodyEndStyle.css">
</body>
</html>

浏览器自上而下读取HTML文档,当发现headStyle.css的时候,停止Parser HTML,开始下载headStyle.css,解析headStyle.css的过程中发现字体文件webfont.woff2,开始下载webfont.woff2,并继续解析css生成CSSStyleSheet。解析完毕后,继续Parser HTML,当发现p标签时,会将p标签结合当前的CSSStyleSheet展示出来,此时用户屏幕中已经有p标签的内容了。当浏览器发现bodyEndStyle.css时,就会下载headStyle.css,解析CSS,然后更新CSSStyleSheet,这时会引起一次重绘。当字体下载完毕的时候也会引起一次重绘。

这个过程中,有两个非常严重的问题。一、如果headStyle.css文件很大,浏览器需要解析很多行CSS后才能还有个字体文件需要下载,其实此时已经很晚了,字体下载时间稍长一点,就会出现我前面截图提到的问题。二、bodyEndStyle.css中如果存在p标签对应的样式,那p标签的样式会在bodyEndStyle.css解析完成后,改变一次样式,很影响体验。

如何解决这些问题那?其中也会用到一些JS篇中提到的点,如果没看过,建议先看看。

核心依旧是减少下载时间

JS篇中的预先解析DNS(dns-prefetch)依旧适用,提前解析CSS文件所在域名的DNS。

Preload

因为CSS已经在head中,我们不需要为css加preload属性了,但是css中用到的字体文件,一定要在所有css之前proload上。

<link rel="preload" href="/webfont.woff2" as="font">

首页CSS内联,非必要CSS异步加载

首页用到的CSS内联写在<head>中,其余CSS均采用异步加载,可以采用这种自己实现的加载CSS的方法,在合适的需要时加载需要的css

function LoadStyle(url) {
  try {
    document.createStyleSheet(url)
  } catch(e) {
    var cssLink = document.createElement('link');
    cssLink.rel = 'stylesheet';
    cssLink.type = 'text/css';
    cssLink.href = url;
    var head = document.getElementsByTagName('head')[0];
    head.appendChild(cssLink)
  }
}

如果你使用webpack,那就更轻松了,使用import函数,大致如下

// 在a.js模块中直接引入css
import 'style.css'
// 在需要a.js模块的地方
improt('path-of-a.js').then(module => {})

webpack打包后,其实是把style.css打包进了a.js,在异步加载a.js的时候,会将style.css中的代码插入haed标签中。

终极完美结构

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Faster</title>
  <link rel="dns-prefetch" href="//cdn.cn/">

  <link rel="preload" href="//cdn.cn/webfont.woff2" as="font">
  <link rel="preload" href="//cdn.cn/Page1-A.js" as="script">
  <link rel="preload" href="//cdn.cn/Page1-B.js" as="script">
  
  <link rel="prefetch" href="//cdn.cn/Page2.js">
  <link rel="prefetch" href="//cdn.cn/Page3.js">
  <link rel="prefetch" href="//cdn.cn/Page4.js">

  <style type="text/css">
    /* 首页用到的CSS内联 */
  </style>
</head>
<body>

<script type="text/javascript" src="//cdn.cn/Page1-A.js" defer></script>
<script type="text/javascript" src="//cdn.cn/Page1-B.js" defer></script>
</body>
</html>

在JS篇中,我已经解释过这套结构中JS的执行顺序了,本篇只是加入了CSS和字体。至此,我心中终极完美的页面HTML结构就是这样了。

如果你对异步加载CSS的方案感兴趣,欢迎留言与我讨论!

扩展阅读

记录我在使用PerformanceAPI遇到的问题

本文中Performance API指的是Navigation Timing API。这并不是一篇Navigation Timing API的介绍文章,而是我在使用中遇到的问题。

我在开发中遇到Navigation Timing API中的connectStart等时间节点并不是标准时间戳,而是0或者一个很小的数值,导致指标数据计算出错,尤其是IOS设备。原因如下:

IOS设备通过浏览器的前进后退按钮进入的页面,Navigation Timing API数据中connectStart,responseEnd等数据可能为0或者是一个比较小的数值,并不是对应时间点的时间戳。究其原因,IOS设备通过缓存读取页面时,Navigation Timing的计算与安卓实现不一致。

如果你还想了解下Navigation Timing API,可以继续往下看

Navigation Timing API

Navigation Timing API中包含全部的页面加载中关键节点的时间,例如navigationStart,connectEnd,responseEnd等时间。
具体的相关API可以去MDN查看,
浏览器支持程度也非常不错,移动端IOS9及以上,Android4及以上都支持,桌面端IE9也都支持。

一些常规的性能数据计算方法

DNS时间 = domainLookupEnd - domainLookupStart
TCP时间 = connectEnd - connectStart
后端时间 = responseEnd - connectEnd

白屏时间 = domInteractive - navigationStart
整屏时间 = loadEventEnd - navigationStart

解析dom树耗时 = domComplete - domInteractive
request请求耗时 = responseEnd - responseStart

我们团队就是按照如上指标来做的各个时间的统计,做了各种测试,线下数据都没什么问题。上线了以后拿到的首批数据中,后端时间计算出来竟然有负值,尤其在IOS设备下,苦苦寻找原因,终于发现问题所在。

IOS设备通过浏览器的前进后退按钮进入的页面,Navigation Timing API数据中connectStart,responseEnd等数据可能为0或者是一个比较小的数值,并不是对应时间点的时间戳。

关于首屏时间的定义

根据Navigation Timing API的时间,是没有办法计算首屏时间的,首屏时间也并没有严格的定义,我们团队采用的首屏时间如下

首屏时间 = (dom解析完毕 && 所有首屏图片加载完毕 )- navigationStart

标准

属性 含义
navigationStart 准备加载新页面的起始时间
redirectStart 如果发生了HTTP重定向,并且从导航开始,中间的每次重定向,都和当前文档同域的话,就返回开始重定向的timing.fetchStart的值。其他情况,则返回0
redirectEnd 如果发生了HTTP重定向,并且从导航开始,中间的每次重定向,都和当前文档同域的话,就返回最后一次重定向,接收到最后一个字节数据后的那个时间.其他情况则返回0
fetchStart 如果一个新的资源获取被发起,则 fetchStart必须返回用户代理开始检查其相关缓存的那个时间,其他情况则返回开始获取该资源的时间
domainLookupStart 返回用户代理对当前文档所属域进行DNS查询开始的时间。如果此请求没有DNS查询过程,如长连接,资源cache,甚至是本地资源等。 那么就返回 fetchStart的值
domainLookupEnd 返回用户代理对结束对当前文档所属域进行DNS查询的时间。如果此请求没有DNS查询过程,如长连接,资源cache,甚至是本地资源等。那么就返回 fetchStart的值
connectStart 返回用户代理向服务器服务器请求文档,开始建立连接的那个时间,如果此连接是一个长连接,又或者直接从缓存中获取资源(即没有与服务器建立连接)。则返回domainLookupEnd的值
(secureConnectionStart) 可选特性。用户代理如果没有对应的东东,就要把这个设置为undefined。如果有这个东东,并且是HTTPS协议,那么就要返回开始SSL握手的那个时间。 如果不是HTTPS, 那么就返回0
connectEnd 返回用户代理向服务器服务器请求文档,建立连接成功后的那个时间,如果此连接是一个长连接,又或者直接从缓存中获取资源(即没有与服务器建立连接)。则返回domainLookupEnd的值
requestStart 返回从服务器、缓存、本地资源等,开始请求文档的时间
responseStart 返回用户代理从服务器、缓存、本地资源中,接收到第一个字节数据的时间
responseEnd 返回用户代理接收到最后一个字符的时间,和当前连接被关闭的时间中,更早的那个。同样,文档可能来自服务器、缓存、或本地资源
domLoading 返回用户代理把其文档的 "current document readiness" 设置为 "loading"的时候
domInteractive 返回用户代理把其文档的 "current document readiness" 设置为 "interactive"的时候.
domContentLoadedEventStart 返回文档发生 DOMContentLoaded事件的时间
domContentLoadedEventEnd 文档的DOMContentLoaded 事件的结束时间
domComplete 返回用户代理把其文档的 "current document readiness" 设置为 "complete"的时候
loadEventStart 文档触发load事件的时间。如果load事件没有触发,那么该接口就返回0
loadEventEnd 文档触发load事件结束后的时间。如果load事件没有触发,那么该接口就返回0

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.