分类 默认分类 下的文章

外边距合并 Collapsing Margins

两个块级元素垂直外边距会合并的情况是W3C CSS2 规范定义之初就存在的,并不能说是 bug ,而是 future。

前端开发者往往不了解 CSS属性的由来和设计的本意,在实际使用中产生副作用。

下面几种常见的情况,会出现外边距合并:

相邻元素

相邻元素(包括空内容)之间外边距重叠是经常出现的情况,重叠时一般按照最大值为相邻距离。

迫不得已可以在它们之间加入 <br > 空元素来避免这种情况

嵌套元素

嵌套其中一个元素也会有这个问题,但仅仅发生在,嵌套元素没有固定的高度、没有 padding 和 border 的情况。

同样,创建 BFC 块格式化上下文 可以避免两个相邻的块级元素外边距合并的情况发生,但引入 BFC 会破坏原有 HTML 结构,并产生一些复杂的问题。

开发者应该避免这种情况发生,在整体布局上少用外边距。

参考

Everything You Need To Know About CSS Margins

What You Should Know About Collapsing Margins

W3C - 8.3.1 Collapsing margins

重翻译:css2.1规范 8.3.1节(margin边距折叠)

The Rules of Margin Collapse

块格式化上下文 - Web 开发者指南 | MDN

[CSS 外边距(margin)重叠及防止方法
边距重叠解决方案(BFC)](https://www.cnblogs.com/webdom/p/10643176.html)

往年目标

  • 100天慢跑 × 仅完成了 56天,有十几天没有带小米手环出门,工作上有加班的话整个月都懒下来没有出门跑步
  • 每月博文 × 只完成了 5篇,三篇总结了工作上的事件案例,React 编写小程序,和混合式应用 从理论到实践,尝试编写了表单应用。
  • 读完 5本书 × 只读完了3本,分别是《软件困局》《构建之法》《思维改变生活》
  • 增重到 110斤 × 没有做到!实在是有点难,应该分季度执行,每月增重1斤,再做后续计划
  • 参与一次计算机等级考试 √ 参加了软考中级,网络工程师,因为是裸考没有准备,考了个不及格
  • 规划一次国内旅行 √ 圣诞节跟钢琴妹妹到珠海长隆玩
  • 探索学习测试、后端技术 √。写了文章《小试牛刀 使用 Laravel 编写表单应用》

疫情

今年几乎有半年笼罩在疫情之下,春节假期(7天)从1月24日延长到了3月。 2月6日在疫情比较严重的时候回到广州,3月1日才陆续轮班上岗。

经历了一个月自己买菜煮饭的日子,每天想着吃啥也是挺痛苦,一开始方便面、麦当劳度日,后面吃腻了只能到菜市场买肉买菜,开始做饭。还不小心切到了手指,印象深刻。

工作

公司的多数同事奉行形式主义,任务导向,导致简单的事情变成非常复杂,真正伤害了实际用户,导致用户体验非常差。

唯一办法是多开展思想工作和分享会,从认知上改善这种行为。

读书

在年底的时候读到一本《思维改变生活》,真的让我大开眼界。

编程

更新 2017年编写的项目 filmy 相册,还有些文档没写完,仍未正式发布。

考试

  • 参加 《2020年下半年计算机技术与软件专业技术资格考试》 网络工程师(中级)

电影

由于疫情的关系,今年上映的电影很少

  • 拆弹专家2
  • 信条
  • 1917

新朋友

认识了四位新朋友,玉群、奶茶妹妹、欣怡、还有鸣鸣,大家聊了很多,很大程度上能认识到自己的优点和缺点,以及对方需要什么。

认识女孩子不要太急功近利,太着急只会让自己失望,一旦被冷落之后就会觉得自己一无是处,最好先收拾自信,认清优势。

还是要多认识些朋友,才能知道自己喜欢怎样的女孩子,希望在来年能让自己变得更好,不断努力向前。

存钱

到 12月,存到了计划 10万块的其中 9万,平时吃喝花的有点多,每周都有一两次奶茶,平均每月多花费 200块钱在奶茶上面,有时候还请同事喝。虽然说每月计划存 3500,总是会往存钱的卡扣钱来用,导致存得钱减少了。

7493块钱花在数码产品上

关于定制 2021年目标

2020年的目标完成度不算高。需要定制合适自己的目标, 免得打击积极性。

每季度应该检查目标完成情况,适度调整目标

  • 每月8次或者每周两次跑步
  • 每季度完成一篇较长的博文,或每月一篇工作技术总结
  • 每月增重1斤
  • 每季度一本书,建议两本技术,两本非技术
  • 参加计算机技术与软件专业技术资格考试
  • 规划国内旅行
  • 背前端面试题,参加一次面试
  • 存 3W 块钱

相关文章

道高一尺

疫情呆在家里,发现墙又高了不少, Clone Github 仓库时,速度极慢。

$ git clone [email protected]:docsifyjs/docsify.git
Cloning into 'docsify'...
remote: Enumerating objects: 8846, done.
Receiving objects:   1% (89/8846), 44.01 KiB | 1024 bytes/s
Receiving objects:   2% (177/8846), 68.01 KiB | 2.00 KiB/s

使用 SSH 方式安全性更高,不用输入帐户密码,只需配置 SSH keys 即可

魔高一丈

如果使用 HTTPS 的连接方式,可以很方便设置 http.proxy

git config --global http.proxy http://127.0.0.1:23333
git config --global --unset http.proxy

而 SSH 的连接需要配置 .ssh 目录下的 config 文件

Host github.com
  User git
  Port 22
  Hostname github.com
  IdentityFile "C:\Users\Mather\.ssh\id_rsa"
  ProxyCommand connect -S 127.0.0.1:23333 -a none %h %p
  TCPKeepAlive yes

Host ssh.github.com
  User git
  Port 443
  Hostname ssh.github.com
  IdentityFile "C:\Users\Mather\.ssh\id_rsa"
  ProxyCommand connect -S 127.0.0.1:23333 -a none %h %p
  TCPKeepAlive yes

看到速度已经起飞

$ git clone [email protected]:docsifyjs/docsify.git
Cloning into 'docsify'...
remote: Enumerating objects: 8846, done.
remote: Total 8846 (delta 0), reused 0 (delta 0), pack-reused 8846
Receiving objects: 100% (8846/8846), 16.68 MiB | 2.21 MiB/s, done.
Resolving deltas: 100% (5639/5639), done.

参考文档

SSH Proxy Command -- connect.c - wiki

Windows 下 Git SSH 连接方式配置 Socks 代理

git 设置和取消代理

macOS 给 Git(Github) 设置代理(HTTP/SSH

[toc]

混合式应用

如今手机应用的开发已经从客户端发展到跨平台,原生开发不再是一枝独秀。前端领域也尝试分一杯羹,更有后起的 Flutter 。

HybridApp 混合应用指的是,同时使用网页前端技术和原生技术开发的 App,通常由网页负责部分界面开发和业务逻辑,原生提供给前端调用的函数/接口,两者以 WebView 作为媒介建立通信,既拥有 Web 开发的速度又是,又能拥有强大的原生能力。

典型案例

是什么场景适合到混合式应用?

因为产品前期方案设计存在不足,没有合理地规范录入内容的结构,各种乱七八糟内容以 HTML 的形式保存到数据库中。久而久之,客户端难以解析这些结构,导致内容显示空白,甚至应用崩溃。

在内容管理系统中,文本和多媒体组成的内容,一般会以 HTML 代码的形式存储在数据库中。而客户端应用需要解析 HTML,转换成控件。

为了解决该问题,成本较低的方案是改用 Web 技术来展示这些文本和多媒体内容。

它能解决:

  • 客户端难以解析文本结构的问题。凭借 Web 前端天然的优势,浏览器可直接解析 HTML 代码
  • 发现问题或错误时,可随时修复更新,无需发布新版本客户端,减少因功能失效后的损失。

那么由 Web 实现主要的业务场景,也会存在技术难点:

  • 客户端与 Web 之间函数互相调用问题
  • Web 应用在客户端 WebView 组件的生命周期问题
  • 代码版本维护与发布更新问题
数据影响深远。数据的结构影响到了选取开发方案的结果。Editor.js 给提供了描述内容的方案,将内容描述成简洁的数据,易于清洗、扩展和集成的内容。

原生与 Web 相互调用

Webview 组件提供了功能上相似的 API。Android 平台拥有 JavascriptInterface API,而 iOS 则是 WKWebView API,但两端的调用方式不一致,必定导致一方的取舍。

引入 DSbridge 这个库来保证不同平台与 Web 相互调用功能的统一。以 Android 为例,原生调用前端 API的声明方式:

  • 在 Javascript 中定义 addValue API

    dsBridge.registerAsyn('addValue',function(l,r){
      return l+r;
    })
  • 在 Java 中调用 addValue API

    dwebView.callHandler("addValue",new Object[]{3,4},new OnReturnValue<Integer>(){
      @Override
      public void onValue(Integer retValue) {
          Log.d("jsbridge","call succeed,return value is "+retValue);
      }
    });

更多用例见 DSbridge 中文文档

握手与数据更新

从程序设计角度看,代码运行会有初始化、创建、执行、暂停、结束、销毁等生命周期。将对应的逻辑放在不同的生命周期中运行,才能表现出软件的有序性,易维护性,提高程序的运行效率。

在混合应用中,同样需要生命周期,保证了客户端与 Web 间的调用是有序的。双方传输数据前就必须建立连接,并要使每一方能够确知对方的存在

利用 DSbridge 定义名为 onWebViewCreated 的 API,确知双方都准备好进入下一生命周期。

定义 onWebViewCreated API

Android:

@JavascriptInterface
public void onWebViewCreated(Object msg, CompletionHandler<Boolean> handler) {
    handler.complete(true); //return true value to front end
}

Javascript:

const methodName = "onWebViewCreated";

dsBridge.call(methodName, (res) => {
    if(!!res) { 
        // get true from client
    }
});
定义 notifyWebViewUpdated API

在本文开头,混合式应用主要是解决展示富文本的业务需求。那么第一握手成功后,Web 应用即可主动调用相关 API,进入业务流程。

假设遇到用户信息更新,客户端应该通过事件通知的形式,告知 Web 应用主动获取最新的用户信息,更新内部数据和页面显示内容。

这种情况就像你收到一条包裹短信,其内容告知包裹所在的门店,你要赶在打样前到门店取回包裹

notifyWebViewUpdated 传入:

{
    "name": "user"
}

Android:

JSONObject updateObj = new JSONObject();
try { 
    updateObj.put("name", "user"); 
} catch (JSONException e) {
    e.printStackTrace();
}

dWebView.callHandler("notifyWebViewUpdated", new Object[]{ updateObj }, new OnReturnValue<Boolean>() {
    @Override
    public void onValue(Boolean retValue) { //callback from front end
        showToast(retValue); //retVlue is true
    }
});

Javascript:

dsBridge.register('notifyWebViewUpdated', (e) => {
    if(e.name === 'user') {
        //trigger get user API
    }
    return "true";
})

部署与维护

与多数 Web 应用一样,混合应用的 Web 项目同样需要部署在服务器或分发到 CDN。

版本管理

在混合项目中 Web 应用加载流程与浏览器是一致的,Webview 请求网页服务器,请求资源到本地,再完成渲染。

与传统项目不同的是,Web 应用和客户端版本之间是存在关系的,这种关系影响到部署。是因为混合应用方案未经过迭代,可能存在不明显的缺陷,避免在代码层兼容多种客户端,应采用版本一对一的方式进行部署。

某个Web 应用版本出现缺陷,针对具体版本进行修复并发布,避免了缺陷对其他客户端版本造成影响。这样做的好处是,保证版本正常运行同时,可迅速地对异常版本修复,将异常等级到最低

版本迭代

每个项目都有其独立的版本号,但在混合式项目中,客户端与 Web 应用项目版本号将产生关联,并记录在案,方便我们在项目迭代和方案复盘时,提供有力的数据。

客户端平台客户端版本Web 应用版本备注
Android7.0.01.0.0发布初始版本
iOS7.0.01.0.0-
Android7.0.01.0.1修复小问题,更新小版本号 1.0.1
iOS7.0.11.0.1-
Android7.0.11.1.0跟随客户端功能迭代,更新中版本号 1.1.0
iOS7.0.21.1.0-
iOS7.1.01.3.0跟随客户端功能迭代,更新中版本号 1.3.0

修复混合应用项目 ①

经过修复后的混合应用版本号从 1.0.0 更新至 1.0.1 ,产出的代码对应客户端 7.0.0 7.0.1 版本

更新小版本号时,根据数月内版本活跃量,筛选哪些版本适用本次升级迭代

发布新功能 ②

与客户端相关的功能,更新版本号, 如 1.1.0

并确定各平台客户端版本号 如: Android 7.0.1、 iOS 7.0.2 ,产出的代码对应这些版本即可

User-Agent

字符串 <应用名称>/<软件版本号> 会附在 User Agent 末尾,例如 7.0.0 版本 iPhone 客户端带有标识 iread/7.0.0

Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) iread/7.0

浏览器包含平台(iPhone/iPad/Android)标识,因此我们不需要再添加平台信息。

发布与部署

版本管理和迭代章节指出,不同项目版本将会分开部署,意味着每个对应版本的应用都是独立部署的,我们可以将部署内容分散在不同文件夹中,这里使用一个简单的配置展示独立部署效果。

D:\WWW\DEV.WEBSITE     <----- root
├─1.1.x                <----- $path_dir
│      index.html      <----- project entry
│      
├─1.2.x                <----- $path_dir
│      index.html
│      
├─1.3.x                <----- $path_dir
│      index.html
│      
└─default              <----- default version for all
       index.html

配合使用 Nginx map 和虚拟主机的特性,获取 HTTP Header 中的 User-Agent 后将虚拟主机的根目录映射至不同目录。

map $http_user_agent $path_dir {
    ~*iread/7.0.0     "1.1.x";
    ~*iread/7.0.1     "1.1.x";

    ~*iread/7.1.0     "1.2.x";
    ~*iread/7.2.0     "1.3.x";

    default           "default";
}

server {
    listen 80;
    server_name dev.website;
    root "D:/www/dev.website/$path_dir/";

    location / {
        index index.html index.htm index.php;
    }
}

也可以加入平台标识的判断

起因

前些日子听到运营反馈:从管理后台生成的内容因没有妥善保存,造成丢失,事后查找起来麻烦。所以这里需要制作表单功能,提交生成的内容以及相关信息。并在前台以列表形式展示出来,类似于网站留言板功能。

先前按照视频学习过 Laravel ,这次将通过实战,重新复习部分内容,并记录下来。相关开发环境和新项目手脚架,可以在 初尝 Laravel 5.8 文中找到,这里不再累述。

创建数据库迁移文件

应充分考虑数据表结构,简单来说要保存有用字段。从前端界面来说,有:

  • 链接名称;
  • 链接实体,包含原链接、短链接、重定向链接三种;
  • 链接的类型,依用途区分不同类型;
  • 链接打开后的页面,这里简单地用 id 表示;

用表格表示所有数据库字段:

字段名字段类型描述
idint自增 ID
titlestring链接名称
typestring链接类型
short_urlstring经过短链接处理后的链接
page_idstring打开后的页面 id
long_urltextURL 可能会很长,使用 text 字段类型
redirect_urltext同 long_url

定义该数据表名称为 links ,并创建对应的 Model 和 数据库迁移文件

php artisan make:model Link -m

// created file in
// app/Link.php
// database/migrations/2020_04_12_131409_create_links_table.php

接下来将上面填写好的表格,以配置的形式写入迁移文件中,Artisan 助手已为我们补全了 idtimestamps 基础字段:

// database/migrations/2020_04_12_131409_create_links_table.php

class CreateLinksTable extends Migration
{
    public function up()
    {
        Schema::create('links', function (Blueprint $table) {
            $table->bigIncrements('id');
            // add table field start
            $table->string('title');
            $table->string('type');
            $table->string('short_url');
            $table->string('page_id');
            $table->TEXT('long_url');
            $table->TEXT('redirect_url');
            // add table field end
            $table->timestamps();
        });
    }

   public function down()
    {
        Schema::dropIfExists('links');
    }
}

定义完数据表结构和字段类型后,无需在数据库中手动创建这张表,将工作交给 artisan 助手来实现。

使用 php artisan migrate 即可将迁移文件应用到数据库中,它会在数据库中创建 links 数据表。

Migrating: 2020_04_12_131409_create_links_table
Migrated:  2020_04_12_131409_create_links_table (0.02 seconds)

数据库交互模型

经典 MVC 模型中使用 Model 层处理数据库读写,Laravel 提供了名叫 Eloquent ORM 的功能,使得我们在代码中可以简洁高效地操作数据库。

在先前创建迁移文件时,Links Model 已经存在,中指定空的 guarded 属性,表示无黑名单,任何数据经过这里都会被保留下来,进入数据库操作:

// app/Link.php

class Link extends Model
{
    protected $guarded = [];
}

就算使用无黑名单的配置,我们还是要在接口请求时做表单验证,确保数据准确性。

定义 Web API 接口

Web API 接口用于对接前端,也可简单地将所有字段映射为接口内容,当参数和关联字段过多时,会显得十分臃肿。

Laraval 框架路由默认在 routes 文件夹中, routes/api.php 定义非模板渲染的页面请求,在此定义一个 POST 请求,将它指向 LinkController 控制器里的 store 方法;

同时定义请求路径上的必填参数 linkType,用于区分数据中的链接类型

Route::post('link/{linkType}', 'LinkController@store');

最后框架将此路由解析为路径 //my.domain.name/api/link/deeplink

表单验证

继续创建路由中定义的 LinkController ,它会处理请求中的内容:

php artisan make:controller LinkController

Requests 类可以非常方便地助我们验证请求中的表单字段,所以创建名为 StoreLink 的类:

php artisan make:request StoreLink

接下来可以真正地开始编写逻辑代码,在 LinkController 中定义 store 函数:

use App\Http\Requests\StoreLink;
use App\Link;
use Illuminate\Http\Request;

class LinkController extends Controller
{

    public function store(StoreLink $request)
    {
        $data = $this->validate(request(), $request -> rules());
        $link = Link::create($data);
    }

}
注意 store 函数的参数 StoreLink $request ,并不需要给 store 传递任何参数,只需要知名方法参数类型,框架会自动查找相关依赖自动注入。

别忘了我们还有 StoreLink ,只需要指定字段类型和期望值,它可以帮我们验证表单字段:

class StoreLink extends FormRequest
{
    public function rules()
    {
        return [
            'page_id'      => 'required',
            'title'        => 'required',
            'type'         => 'required',
            'long_url'     => 'required',
            'short_url'    => 'string|nullable',
            'redirect_url' => 'string|nullable',
        ];
    }
}

还记得 Web API 定义的 link/{linkType} 请求路径吗?这里的链接类型是附属在路径名称中的,这样做的好处是减少一些请求字段。

由于 type 字段是依附在请求路径中的,表单验证只会获取请求实体的内容,由于缺少type 字段,则会出现校检错误的提示:

{
    "message": "The given data was invalid.",
    "errors": {
        "type": [
            "The type field is required."
        ]
    }
}

必须先把请求路径中的 linkType 取出并赋值到 $request 实体中的 type 字段,才能通过 FormRequest 的验证:

class StoreLink extends FormRequest
{
    public function prepareForValidation()
    {
        $this -> merge(['type' => $this -> linkType]);
    }
}

测试接口

使用 Postman 发出模拟 POST 请求,什么都不发送,提示表单验证不通过

{
    "message": "The given data was invalid.",
    "errors": {
        "page_id": [
            "The page id field is required."
        ],
        "title": [
            "The title field is required."
        ],
        "long_url": [
            "The long url field is required."
        ]
    }
}

但是某些值虽然通过校检,但是因为数据库设计错误,导致报错

"short_url": "",
'short_url'    => 'string|nullable',
SQLSTATE[23000]: Integrity constraint violation: 

1048 Column 'short_url' cannot be null 

(SQL: insert into `links` (`page_id`, `title`, `type`, `long_url`, `short_url`, `redirect_url`, `updated_at`, `created_at`) values (1, 测试, openpage, 1, ?, 重定向链接, 2020-05-16 20:03:06, 2020-05-16 20:03:06))

改进数据库

因为初期设计数据表时,没有考虑到 short_url 可能为空值,导致插入数据失败。

那么现在来修改数据表,Laravel 不建议我们直接操作数据库,像创建数据表一样,创建迁移文件

artisan make:migration modify_links_table

在里头重新声明我们要修改的数据表字段类型和附加值

Schema::table('links', function (Blueprint $table) {

    $table->TEXT('long_url')->nullable()->change();
    $table->string('short_url')->nullable()->change();
    $table->TEXT('redirect_url')->nullable()->change();
    
});

使用 php artisan migrate 应用到数据库中,现在 short_url 字段为空时,也能保存到数据表。

+----+-------+----------+----------+-----------+--------------+---------+---------------------+---------------------+
| id | title | type     | long_url | short_url | redirect_url | page_id | created_at          | updated_at          |
+----+-------+----------+----------+-----------+--------------+---------+---------------------+---------------------+
|  1 | 测试  | openpage | 1        | NULL      | 重定向链接   | 1       | 2020-05-16 20:33:47 | 2020-05-16 20:33:47 |
+----+-------+----------+----------+-----------+--------------+---------+---------------------+---------------------+
1 row in set (0.02 sec)

获取所有数据

除了把数据记录起来,还得有个展示的功能。

提供路由

Route::get('link/{linkType}', 'LinkController@index');

在控制器中可查询数据表内容,并按照时间倒序排列

public function index($linkType = '')
{
    $lists = DB::table('links')
        ->where('type', '=', $linkType)
        ->orderBy('created_at', 'desc')
        ->paginate(100);

    return $lists;
}

以 JSON 的形式输出

{
    "current_page": 1,
    "data": [
        {
            "id": 1,
            "title": "测试",
            "type": "openpage",
            "long_url": "1",
            "short_url": null,
            "redirect_url": "重定向链接",
            "page_id": "1",
            "created_at": "2020-05-16 20:33:47",
            "updated_at": "2020-05-16 20:33:47"
        }
    ],
    "first_page_url": "http://api.misaka.im/api/link/openpage?page=1",
    "from": 1,
    "last_page": 1,
    "last_page_url": "http://api.misaka.im/api/link/openpage?page=1",
    "next_page_url": null,
    "path": "http://api.misaka.im/api/link/openpage",
    "per_page": 100,
    "prev_page_url": null,
    "to": 1,
    "total": 1
}

总结

在岗位上只做自身技术范围的事情,显得太被动。在 Web 应用开发的过程中,前后端技术紧密关联。

制定开发方案前,需要充分了解用户需求,在有限的时间内选择最优的解决方案。