作者: Gryen7

  • Gryen-GTD 升级日志,composer 详解

    Gryen-GTD 升级日志,composer 详解

    许久没有更新过本博客系统了,前些天有朋友想使用本系统搭建自己的博客,发现看着我给的文档根本无从下手,无奈之下我发邮件向我求助。如果对 Linux、PHP、MySQL 等有基本的了解,以目前系统的状态,我也是无能为力的。怪自己懒,更新不及时。Gryen-GTD 更新频率视个人情况,有时更新较为频繁,有时几个月也不会有代码提交。开始此项目时,仅仅作为自己的一个实验,主要帮助自己熟悉 PHP 及周边知识,以免长时间不接触 PHP 的工作后遗忘。做的有点儿样子后,将其开源,希望能借助开源提高一下自己的编程水平,如果有幸能帮到一些朋友或有朋友对此感兴趣,可以共同开发,也算是额外的激励了。

    最近有时间维护下此项目,发现果然遗忘了很多,连 composer 都有点儿搞不懂了。

    Composer

    Composer 是 PHP 的一个依赖管理工具,类比与 Node 的 npm,通常,它会将 packages 安装到项目的 vendor 文件夹中,相当于 npm 的 node_modules。关于它的详细介绍可阅读Composer 中文文档。Composer 常用的命令如下:

    • composer install:如有 composer.lock 文件,会直接安装,否则,它会根据 composer.json 配置文件安装依赖;
    • composer require xxx [--dev]:它会安装xxxpackage,并将其记录到 composer.json 文件中,添加--dev参数可安装只在开发过程中使用的 package;
    • composer update:按照 composer.json 中给定的规则,升级依赖到最新版本,并且升级 composer.lock 文件
    • composer dump-autoload:只更新自动加载而不去更新依赖

    composer.json 文件详解

    以 laravel 的 composer.json 文件举例:

    {
        "name": "laravel/laravel",
        "type": "project",
        "description": "The Laravel Framework.",
        "keywords": [
            "framework",
            "laravel"
        ],
        "license": "MIT",
        "require": {
            "php": "^7.1.3",
            "fideloper/proxy": "^4.0",
            "laravel/framework": "5.7.*",
            "laravel/tinker": "^1.0"
        },
        "require-dev": {
            "beyondcode/laravel-dump-server": "^1.0",
            "filp/whoops": "^2.0",
            "fzaninotto/faker": "^1.4",
            "mockery/mockery": "^1.0",
            "nunomaduro/collision": "^2.0",
            "phpunit/phpunit": "^7.0"
        },
        "config": {
            "optimize-autoloader": true,
            "preferred-install": "dist",
            "sort-packages": true
        },
        "extra": {
            "laravel": {
                "dont-discover": []
            }
        },
        "autoload": {
            "psr-4": {
                "App\\": "app/"
            },
            "classmap": [
                "database/seeds",
                "database/factories"
            ]
        },
        "autoload-dev": {
            "psr-4": {
                "Tests\\": "tests/"
            }
        },
        "minimum-stability": "dev",
        "prefer-stable": true,
        "scripts": {
            "post-autoload-dump": [
                "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
                "@php artisan package:discover --ansi"
            ],
            "post-root-package-install": [
                "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
            ],
            "post-create-project-cmd": [
                "@php artisan key:generate --ansi"
            ]
        }
    }
    
    
    • 其中,license 及前面的节点是对项目的说明。
    • require 是项目发布需要的依赖
    • require-dev 是项目开发以及测试过程中需要的依赖
    • config 是一些配置,以上面为例:
      • optimize-autoloader:在 dump 的时候是否自动优化
      • preferred-install:设置 Composer 的默认安装方法,其他可能的值:source、dist、auto(默认值),source 是全部,dist 不包括版本信息,相当于去掉 package 的 .git 目录
      • sort-packages:用来保证当引入新的 package 的时候,composer.json 中的 packages 按照 package name 排序
    • extra 是供 scripts 使用的额外数据
    • autoload 和 autoload-dev:PHP autoloader 的自动加载映射
      • classmap:生成支持支持自定义加载的不遵循 PSR-0/4 规范的类库
      • files:这个配置上面没有,但是很可能会用到,这个可以明确指定在每次请求中都会载入的文件,比如定义的一些公共方法
    • minimum-stability:指定安装 package 时的最低稳定性要求,可能的值:dev、alpha、beta、RC、stable
    • prefer-stable:设置为 true,优先使用 package 的稳定版本
    • scripts 指定在安装的不同阶段挂载的脚本
      • post-autoload-dump:在自动加载器被转储后触发,无论是 install/update 还是 dump-autoload 命令都会触发。
      • post-root-package-install:在 create-project 命令期间,根包安装完成后触发。
      • post-create-project-cmd:在 create-project 命令执行后触发。

    其他配置的解释可参考:composer.json

    PHP autoload 的一点儿记录

    classmap 引用的所有组合,都会在 install/update 过程中生成,并存储到 vendor/composer/autoload_classmap.php 文件中。这个 map 是经过扫描指定目录(同样支持直接精确到文件)中所有的 .php 和 .inc 文件里内置的类而得到的。

  • “无重复字符的最长子串(Longest Substring Without Repeating Characters)”问题的 Python3 解答

    “无重复字符的最长子串(Longest Substring Without Repeating Characters)”问题的 Python3 解答

    题目描述

    给定一个字符串,找出不含有重复字符的最长子串的长度。

    示例

    示例 1:

    输入: "abcabcbb"
    输出: 3 
    解释: 无重复字符的最长子串是 "abc",其长度为 3。
    

    示例2:

    输入: "bbbbb"
    输出: 1
    解释: 无重复字符的最长子串是 "b",其长度为 1。
    

    示例3:

    输入: "pwwkew"
    输出: 3
    解释: 无重复字符的最长子串是 "wke",其长度为 3。
         请注意,答案必须是一个子串,"pwke" 是一个子序列 而不是子串。
    

    测试用例

    abcabcbb    3
    bbbbb   1
    pwwkew  3
    ""      0
    au      2
    dvdf    3
    cdd     2
    ddc     2
    

    解题思路

    逐字符遍历字符串,记录第一个无重复子串s[0]。查看下一个字符是否被rStr所包含,不包含就将此字符加到rStr的末尾,直到找到一个已经包含在rStr中的字符,此时,这个无重复子串的长度就有了。依次方法,继续查找子串,并与当前已经找到的子串做比较,记录最长的子串长度即可。字符串遍历完毕,问题得解。

    解答

    class Solution:
        def lengthOfLongestSubstring(self, s):
            """
            :type s: str
            :rtype: int
            """
            sLen = len(s)
            if sLen < 1:
                return 0
            rLen = 1
            startIndex = 1
            endIndex = 0
            rStr = s[0]
    
            while startIndex != sLen != endIndex:
                for i in range(startIndex, sLen):
                    if s[i] not in rStr:
                        endIndex = i + 1
                        rStr = s[startIndex - 1:endIndex]
                    else:
                        break
                rLen = max(rLen, len(rStr))
                if startIndex != sLen:
                    rStr = s[startIndex]
                startIndex = startIndex + 1
            return rLen
    
    

    算法分析

    上述算法用Python3实现的时候有一个边界问题,字符串为空的情况下长度为 0,返回此值即可,否则下面初始化第一个无重复子串就溢出了。

    上述算法时间复杂度是O(n^2),并不理想。

    中文官网的题目地址:无重复字符的最长子串

  • “两数之和(Two Sum)”问题的 Python3 解答

    “两数之和(Two Sum)”问题的 Python3 解答

    题目描述

    给定一个整数数组和一个目标值,找出数组中和为目标值的两个数。你可以假设每个输入只对应一种答案,且同样的元素不能被重复利用。

    示例

    给定 nums = [2, 7, 11, 15], target = 9
    因为 nums[0] + nums[1] = 2 + 7 = 9
    所以返回 [0, 1]

    解题思路

    要求的结果是两个索引组成的 List:[aIndex, bIndex],这两个索引对应的值记为 a, b。

    遍历列表,假设列表中第1项的值 a 是结果 List 中的一个索引的值,那么用 target 减去 a 后得到的结果 b 就是就是要找的另一个索引的值,列表除掉 a 后的子列表里面,如果 b 存在,问题已解——答案是 a 的索引和 b 的索引组成的列表 [aIndex, bIndex]。

    如果不存在,将列表的第二项赋值给 a,继续寻找 b,直到找到为止。

    解答

    class Solution:
        def twoSum(self, nums, target):
            """
            :type nums: List[int]
            :type target: int
            :rtype: List[int]
            """
    
            i = 0
    
            for num in nums:
                nextNum = target - num
                j = i + 1
                if nextNum in nums[j:]:
                    return [i, nums[j:].index(nextNum) + i + 1]
                i += 1
    
    

    中文官网的题目地址:两数之和

  • “两数相加(Add Two Numbers)”问题的 Python3 解答

    “两数相加(Add Two Numbers)”问题的 Python3 解答

    题目描述

    给定两个非空链表来表示两个非负整数。位数按照逆序方式存储,它们的每个节点只存储单个数字。将两数相加返回一个新的链表。

    你可以假设除了数字 0 之外,这两个数字都不会以零开头

    示例

    输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
    输出:7 -> 0 -> 8
    原因:342 + 465 = 807

    测试用例

    [2,4,3]
    [5,6,4]
    
    [1]
    [9,9]
    
    [5]
    [5]
    
    [0]
    [7,3]
    
    [9,9]
    [9]
    

    解题思路

    遍历链表,按节点两两相加,有进位将进位加到下一个节点。

    解题思路很简单,此题被标记为中等难度,是因为:

    1. 数据结构是链表;
    2. 两个链表长度不一定相等,尤其是有进位情况下的处理需考虑。

    解答

    # Definition for singly-linked list.
    # class ListNode:
    #     def __init__(self, x):
    #         self.val = x
    #         self.next = None
    
    class Solution:
        def addTwoNumbers(self, l1, l2):
            """
            :type l1: ListNode
            :type l2: ListNode
            :rtype: ListNode
            """
            l3 = l1
    
            while l1 != None or l2 != None:
                sum = l1.val + l2.val
                l1.val = sum % 10
    
                if sum >= 10:
                    if l1.next != None:
                        l1.next.val = l1.next.val + 1
    
                        if l1.next.val >= 10 and l2.next == None:
                            l2.next = ListNode(0)
                    else:
                        l1.next = ListNode(1)
    
                else:
                    if l1.next == None and l2.next != None:
                        l1.next = ListNode(0)
    
                if l2.next == None:
                    break
    
                l1 = l1.next
                l2 = l2.next
    
            return l3
    

    算法分析

    上述算法通过一次遍历完成,没有用单独的变量存储 carry 值。两个链表同节点相加的结果最大为 9 + 9 + 1 = 19,所以结果链表中对应节点的值永远可以用(l1[index] + l2[index] + carry) % 10来得到。

    上述算法将进位 carry 计入到了下一位中。具体解题过程为

    1. 用新变量 l3 保留对 l1 的引用
    2. l1 和 l2 对应节点做加法
    3. 有进位且 l1 有下一节点的情况下,将进位加入到 l1 的下一个节点;l1 没有下一个节点就给 l1 添加一个值为 1 的节点;没有进位执行步骤4
    4. l1 没有下一个节点了,但是 l2 还有下一个节点,就给 l1 添加一个值为 0 的节点
    5. 3 或 4 步骤执行完毕,如果 l2 没值了,就跳出循环,返回 l3,原题得解;否则将 l1 和 l2 移动一个节点,执行步骤2

    中文官网的题目地址:两数相加

  • 小九的故事

    小九的故事

    小九,是个江浙地区农村的姑娘。在她还上幼儿园的时候,妈妈出了车祸,失去了劳动能力。爸爸不久就去了外地打工,到她读初中了,都没回过家。后来听说在外面一直有个女人跟爸爸一起生活。村里的人知道了她爸爸的事情,小九走在路上,遇到街坊邻里,他们总是语带嘲讽地问她:“小九啊,你爸爸给你找小妈了吗?”,小九感到羞愧,好像是她做了不好的事情,出门都躲着人。

    小九的妈妈自从车祸之后性情变了许多,三番五次让她辍学打工养家,邻家婶婶告诉她妈妈不让小孩子上学是违法的,她妈妈才作罢。爸爸对小九甚至大声讲话都没有过,但是多年不回家,也不往家里寄钱的行为,让小九觉得他爸爸并不爱她。

    还在小学的时候,小九就猜自己不是亲生的,后来也得到了证实。她明白她现在的母亲这么做,是让她报养育之恩的。亲生父母的抛弃,领养家庭的事故和养母如此做法,都让她对恩情,对报恩有了不同的看法。小九认为养父母对她并没有恩情,养父母抚养她是为了在小九长大之后反过来照顾他们。对于亲生父母,她觉得不仅无恩而且他们对不起她,把她带到这个世界却抛弃了她。

    恩情是什么?一个人的给予,物质上或者感情上的,于你有益。给吃,给穿,困难时给予帮助都算是了。恩情置于父母与孩子之间,是父母给予孩子生命,给予抚育,父母对孩子就有了养育之恩。父母的给予与别人的给予是有不同的,是父母让孩子来到这个世界上的,如果在孩子小的时候不给予呵护与抚养,大概孩子都不能长大成人。现在许多强烈赞同“父母无恩论”的,对父母感情冷淡的人们,有很多是在成长的过程中遭遇了过多的痛苦,有些痛苦的来源甚至就是父母。既然无法好好抚养,为何要把自己带到这个世界上?

    如果父母对孩子真的有恩情,孩子也觉得父母对自己有恩情的话,应该更多地看父母在子女成长过程中是如何抚养的吧。更多的人与父母保持着良好的关系,对父母有着真挚的感情。这都源于父母在抚养子女长大的过程中就倾注了感情。子女能感受到恩情,也是在成长过程中父母与自己建立了良好的感情。生而不养的父母在感情上很难得到子女的认可吧。也有一些人觉得父母对自己有恩情,是在接受了良好的教育之后理性的认为。

    父母天生对子女有恩的看法,是人们人为给予亲子关系的一种处理办法,是一种于社会生活而言较好的教育。人之暮年,在社会养老制度不健全的情况下,如果没有能力照顾自己了,不靠子女靠谁呢?

    父母之于子女,或者子女之于父母都是相较于其他人的较强关系,这关系有多强还得看彼此付出的心血与感情。

    小九还有个哥哥,在外地上学。在小九上初中之后,哥哥结了婚,跟她和母亲住一起。这之后,父亲会每年回家一次。哥嫂与小九、母亲同住,日子依然紧巴。嫂子可以分担一些家务,于小九是一种解脱。后来嫂子生下了小侄子,日子开始不好过了。哥嫂隔三差五吵架,母亲迁怒于小九。有时候嫂子与母亲一起责备小九,小九承受不了,就离家出走。她以为家人们会马上去找她,结果每次都等到很晚了才去找,这让她感觉到没有被爱。

    小九大专是在父亲生活的城市读的,学费是父亲出的。小九去了父亲的“家”,见了那个女人。小九不喜欢那个女人,觉得她相貌平平,世故,虚伪。当年父亲犯了错,是这个女人保父亲出狱的。大概父亲与她生活除了母亲的事故之外,还有这个原因吧。小九在学校里面,同寝同学都是本地的,会问她父亲为什么不回家,小九只好说他父亲是出差或者说不想回家。她很怕同学们知道自己的家庭情况,很怕同学们像邻里街坊一样对她冷嘲热讽。

    大专毕业,小九听哥哥的话,投奔哥哥去了另外一个城市。实习的时候,一家不错的公司在她学校只招了她一个人,让她感觉自豪,但做销售工作的过程中,也接触到了社会黑暗的另一面。

    大概小时候生活不如意的孩子,非常渴望在长大后可以开始一段新的人生,一切都应该是美好的。一旦进入社会,接触到社会的阴暗面之后,又更加确定这个社会是不堪的。像小九一样从乡下去到城市里为生而活,需要与起点比自己高很多的人竞争,残酷而激烈。城市里,成年人世界里的人情世故也远比家乡复杂,远比小时候想象中的复杂。多次碰壁之后,甚至于产生了自我否定,自我怀疑,对美好未来的向往也没那么强烈了。

    实习结束后的求职并不顺利,她感到迷茫、绝望。哥哥照顾了她一段时间,她很焦虑,无所事事越久就越焦虑。她希望可以有个体面的工作,可以有个依靠。遭遇亲生父母抛弃,养父抛弃,养母对自己不单纯的付出,看到哥嫂婚后的争吵等,她对婚姻已经彻底丧失信心了。小九觉得婚姻是极不可靠的,她不断地去证实这一点,去勾引别人,去做第三者。亲手把别人的婚姻撕开裂痕,她感到愉快。她体会到了很多看似牢不可破的关系,其实存在撕裂的斥力,一旦被分开就像磁铁一样难以复合了。这就是真相吧。

  • 读李娟的《冬牧场》——读书读生活

    读李娟的《冬牧场》——读书读生活

    原本是打算写写《围城》的,遗忘的太多了。回头看的时候顺便看了下别人的书评,有些感受确定在读书的时候同样有过,但现在感觉没那么强烈了。留着重读之后再看看能不能写点什么。

    去年读的书里面,《冬牧场》是最能重拾小时候读书乐趣的作品。

    小时候读书,杂志、期刊类的“书”比较多,《读者》、《青年文摘》等的,这些都是从哥哥姐姐那里得来的。记得初中每次开学总会带几本《读者》等杂志去读,以应付刚开学那几天无所事事的时光。特别记得小学时候,每个学期会发一些课外读物,大概有两三种不同类型的读物,每人仅一本,同桌两个人是不同类型的,大家交换着看,另外还有一些作文类的报纸。我们班还建立了“图书角” ,也就是在教室黑板旁边角落放了一张书桌,班主任鼓励大家把自己的书放到“图书角”,登记在册,供全班同学借阅。除此之外,我很少能读到其他的书。在图书资源相对匮乏的年代,到手的书总是如饥似渴地读。记得从同学手里借到一本《简爱》,喜爱至极,没日没夜读了两天,读完了。对书中人物的遭遇感同身受,身临其境般,这种读书的奇妙感觉至今犹在,回味无穷。在图书资源相对匮乏的年代,对读物也是不挑食的,饿极了什么都吃,《故事会》、《小说界》等一些可能不适合小学生读的期刊、杂志,也读过一些。严格来说,《读者》、《青年文摘》我可能也是在不该读的年龄段里开始读的,最不该读的可能还有一些五行八卦类的地摊书……见到什么读什么而已。

    高中时期的读书应该比较符合自己的年龄了,那正是韩寒、郭敬明、安妮宝贝等流行于校园的时候。这段时间的读书得益于我有了一部手机,每天晚上熄灯后,查房的老师们走了后,我就着火柴盒大小的手机屏幕读了三年。有段时间痴迷于《福尔摩斯探案集》,每天晚上读一篇,读完了全集,我记得电子书资源有几十 M 之多,后来在书店翻看才知道那些文字印成书,得有几十公分厚了。此外,那个时候读的最多的应该是安妮宝贝的作品,安妮是那时我最喜欢的一个作家,我喜欢她优美、精炼、清澈凛冽的文字,不过也都是就着火柴盒大小的手机屏幕读的。大学放暑假回家,在火车站旁边的新华书店买过一本《春宴》,那是买的安妮宝贝的第一本书,装帧也喜欢,文字也喜欢,慢慢读,度过了一个美好的暑假。郭敬明的《悲伤逆流成河》是高一入学前的那个暑假读的,那年中考,考场就设在后来读高中的那所学校。一好友从校园附近的书店买了《悲伤逆流成河》和《乱世佳人》,他读完后,我借来读。《乱世佳人》下册他没读完,我只借到上册。我有个哥哥酷爱读书,我们两个都上学的时候,没到暑假,我们一起玩,他就捧一本书,坐在河边看。我记得我们讨论过郭敬明的《幻城》和韩寒,我都不甚了了。直到看到《悲伤逆流成河》才知道郭敬明文笔的厉害!高中读他的《最小说》,班里有几个他的书迷,其中有个漂亮的女同学每月都会买《最小说》,我们就轮流借着读。此外还读了他公司旗下许多作家的作品,记得有落落、安东尼等,那也是一段快乐的时光。

    读书得到的乐趣,除了对未知的渴求之外,可能还有想要一种“那样的生活”、“那样的爱情”……的满足。当然,这肯定不是读书乐趣的全部。读《冬牧场》感受到的快乐,应该就是这样的吧。

    读李娟的第一本书是《我的阿勒泰》,通过“多看” APP 读的电子版。爱上了那个在北疆生活的女子,爱上了她亲切坦诚而又清澈优美的文字。读的过程中一直在想象作者的模样——优雅、倔强,一个真正的活着,活得明媚的女子。从读《我的阿勒泰》到读《冬牧场》,过去了有三四年,不知道这两本书的写作顺序,在我这里,李娟的生活就在这两本书、三四年间延续着。读到《冬牧场》,第一感觉就是,对了!是那个女子!她现在的生活是什么样的呢?

    后来我知道李娟可能是一个遇到了也不会多看两眼的普通女子,身上并没有我印象中作家那种不食人间烟火的气质。她真的真实,放下笔杆子就是一活生生的人,拿起笔杆子就能把生活活生生地展现在你面前。一个牧民家庭,跟随季节迁徙,尤其在冬天,几个人,一群牲口,去到一望无际的戈壁滩放牧生活,除了无聊、寒冷,有什么可写的呢?拿起《冬牧场》,却感觉不到可能的乏味、无趣,反而让我开始思考,我正在经历的是生活吗?在城市里,上班下班重复着可能并无太多意义的工作,天空灰蒙蒙一片,好像李娟笔下的荒凉的戈壁滩倒挂起来了。这样自然的天色,却闻不到自然的味道。似一场梦,偶尔醒过来,只想逃离。

    我看到的人们呢?人们自私、虚伪、势力、奸诈,人们孤独,无助,无趣、可怜。人们努力建设着的社会秩序里,只有一小部分人在生活,剩下的都是为了生存而不情愿地建筑着那小部分人的生活。也许,这就是这个世界的画面。也许,喜欢李娟是因为她给了我没想到过的生活的另一幅画面。也许,童年的农村生活决定了我更需要一种接近于自然的生活方式。也许,长久以来的不断向上生活目前到尽头了,而这个尽头又是我不能坦然接受的。

    这或许也是一个二十多岁的青年的尴尬。生活看似给了人们无穷的选择权力,但具体到某一个人,未必可以做到“选择”。未必能选择一个最爱的人共度一生;未必能选择结婚不结婚,生不生孩子;甚至未必能选择做你真正喜欢的工作。大多时候的所谓“选择”,只不过是去争取一下,然后等待着“被选择”,这道理是从《向往的生活》黄磊老师那里学到的。到底很少有人能过上自己向往的生活。可还是有些人在二十多岁的时候对自己的生活有了选择,选择逃离目前所拥有的一切,去到一个自己想去的地方,开始一种生活,而且不显得矫揉造作。什么是苦日子?李娟和她笔下的人们风餐露宿的生活是苦日子吗?我不觉得是,没有自由与灵魂的生命才是苦的。

    我甚至在接近成年的时候还不太懂世界观、人生观、价值观有什么意义,虽然从小就被教育要树立正确的三观,被教育什么才是正确的三观。到了二十多岁,我开始怀疑这一切。这个世界和小时候眼中的世界不太一样,人生也不只有一种可能性,小时候被建立的“价值观”似乎与我现在所追求的也没多大关系。读到过一句话:当一个人为你服务,你却不能给他足够的报酬的时候,你就教育他,让他树立正确的三观。……嗯,重构自己的思想,这个过程是很痛苦的,甚至会毁了一切,但是一旦完成了,能坦然地从头再来一次,应该也是很痛快的吧。

  • 从浏览器页面呈现原理来讨论站点优化

    从浏览器页面呈现原理来讨论站点优化

    当前的浏览器现状

    当前主流浏览器使用的内核包括 Webkit(Safari、Chrome、Opera等),Trident(IE),Gecko(Firefox)等,其中最值得了解的当然是 Webkit。

    Webkit 是由苹果公司开发的,供自家浏览器 Safari 使用的内核。Safari 于2003年1月7日首次发行测试版。2010年4月,苹果公司宣布了其浏览器引擎的最新项目 Webkit2。Webkit 内核实际上包含 WebCore 渲染引擎和 JavaScriptCore javascript 引擎。

    Chrome 浏览器自2008年发行起,一直使用 Webkit 作为其内核原型。在13年发布的 Chrome 28.0.1469.0 版本中,又改为使用 Blink 内核。

    前面说过,Webkit 内核包含 WebCore 渲染引擎和 JavaScriptCore javascript 引擎。Chrome 中使用的 javascript 引擎实际上是自家开发的大名鼎鼎的 V8 引擎,V8 主要用在 Chromium 和 Chrome 浏览器中。因此,Chrome 中的 Webkit 和 Safari 中的 Webkit 并不等价,而从 Blink 内核开始,其差别越来越大。

    Chromium 和 Chrome 使用同样的内核,Chromium 是开源的,Chrome 是闭源的,许多功能会在 Chromium 浏览器上应用,稳定后才会在 Chrome 上推出。大多国产浏览器实际上是基于 Chromium 而不是 Webkit 开发的,因此会称 Chromium 为内核。

    移动设备上,Android 4.4 之前的浏览器默认使用 Webkit 内核,Android 4.4 以及之后使用 Chromium 内核。

    鉴于上述,本文技术方面的介绍,是在 Chromium 层面的。

    从资源加载过程来优化站点

    网页是如何展现在我们面前的

    从输入 URL 开始,到网页展现在我们面前,浏览器主要做了以下几件事件:

    1. 从网页的 URL 加载资源
    2. 构建 DOM 树
    3. 从 DOM 树构建 Webkit 的绘图上下文
    4. 从绘图上下文生成最终的图像

    当然,每一步都进行了复杂的处理。而且,浏览器一般会同时做这些事情。下面从网页开发者角度说明每一步中值得关注的点。

    从网页 URL 到加载资源的过程

    通过 URL 打开一个网站,无论这个网站是静态的还是动态的,浏览器通过这个 URL 识别到的都应该是一个 html 文档资源。然后浏览器通过解析这个 html 文档,通过标签识别文档中的其他资源,进一步加载其他资源。

    资源加载和缓存

    Chromium 使用多进程的资源加载和缓存机制。其中 Renderer 进程并没有请求资源的权限,当浏览器需要请求资源的时候,会交给 Browser 进程处理,Browser 进程处理完毕后再交给 Renderer 进程渲染页面。Browser 进程和 Renderer 进程通过进程间通信的方式传递数据。

    如果每次请求资源都从远程服务器请求,将会非常耗时和浪费资源,Chromium 采用了高效的缓存机制。需要请求资源的时候,会先去缓存查找资源是否已经存在,不存在时才会去请求网络资源。

    DNS 预取和 TCP 预连接

    在请求网络资源的时候,会经过域名解析和 TCP 连接过程,Chromium 针对这两个过程,分别做了优化。当我们在浏览网页的时候,Chromium 会提取网页中的超链接,利用系统的 DNS 机制预解析,并将解析结果保存下来,当我们点击链接的时候,就不需要进行 DNS 解析了。开发者可以显式声明哪些域名需要提前解析,做法如下:

    当我们在浏览器地址栏输入网址的时候,如果输入的网址和候选项匹配,那么 Chromium 也会预解析该域名。DNS 预取机制大概可以节省 60ms – 120ms 或者更长的时间。

    更进一步,Chromium 甚至会在 DNS 解析之后,用户点击链接之前提前建立 TCP 连接。这里,Chromium 会使用追踪技术预测我们可能点击的链接,在有很大把握的时候才会做这些事情。

    对于网页开发者的启示:

    • 减少网页的重定向,以更好地应用浏览器的 DNS 预取技术
    • 适当地合并资源,减少浏览器建立连接的次数
    • 压缩资源,节省数据量和数据传输时间

    在 Chromium 或者 Chrome 浏览器中,我们可以通过chrome://net-internals/#dns来查看 DNS 缓存情况,并可以通过 Clear cache 和 Flush sockets 工具来及时清除 DNS 缓存数据。在开发过程中,在切换域名 host 之后,用此工具可以使网页立即解析到新的地址,而不用重启浏览器。

    net-internals -w600

    net-internals 工具中的 DNS 面板截图

    从 html 文档构建 DOM 树的过程优化站点

    构建 DOM 树与资源加载

    上面介绍了浏览器加载资源的过程,在这个过程中,浏览器还会同时进行 DOM 树的构建工作,甚至下文要介绍的构建 Webkit 绘图上下文的工作也是同步进行的,要不然页面呈现在我们面前就太慢了。

    对于图片、CSS、视频等文件,浏览器从 HTML 代码解析到他们的 URL 的时候,会通过相关的资源管理器异步加载,这不会阻碍浏览器构建 DOM 树,但是 JavaScript 代码除外。当浏览器解析到 JavaScript 代码节点或者 JavaScript 资源的 URL,会停下 DOM 树的构建,去加载 JavaScript 文件,执行 JavaScript 代码,然后再继续构建 DOM 树。所以,我们要特别注意,当用 JavaScript 去操作 DOM 的时候,DOM 树很可能还没有构建完成,此时 JavaScript 是获取不到 DOM 节点的。总结 JavaScript 代码出现在文档中时会出现的具体问题:

    1\. 中止 DOM 树的构建;
    2\. 不会并发下载接下来文档中可能存在的 src 资源,比如图片;
    3\. JavaScript 代码如果涉及到对 DOM 的读写,可能会失败;
    4\. JavaScript 代码的执行耗费时间,延长文档的加载完成时间。
    
    

    针对上述问题,关注两个 JavaScript 事件:onload 和 DOMContentLoaded。顺便说明,支持 onload 事件的 JavaScript 对象有 image, layer, window,经常谈到的是window.onloadwindow.onload事件是在包括图像等资源在内的,整个页面全部加载完成之后才会触发,DOMContentLoaded事件在网页文档加载并解析完毕(DOM 树构建完成)之后就会触发。所以,对 DOM 的操作放在DOMContentLoaded事件触发之后最好。将 JavaScript 代码放在文档的最后(一般是置于 body 元素里面紧贴 body 闭合标签)也是一种方案。

    当然,确实有一些 JavaScript 代码是与 DOM 无关的。这种情况下,可以为 script 元素添加async属性,告诉浏览器这是可以异步执行的 JavaScript 代码。

    HTML 网页和它的 DOM 表示

    Webkit 实际上具有预扫描和预加载机制,在文档中解析到 JavaScript 代码的时候,会继续解析后面的文档查看有没有资源需要下载,有的话就下载,但在实际测试中,浏览器确实去下载资源了,但是由于 JavaScript 代码执行时的阻塞,资源并没有加载。而且其他的浏览器中不一定有此机制。因此,还是推荐使用前面的处理策略。

  • 冷漠

    冷漠

    周末网上看到两件事情。

    一件事发生在北京街头,一孩子骑自行车碰倒了一位阿姨,那孩子只扭头看了一眼,就走了。路过的一位摩托车骑手看到了之后,首先去把阿姨扶了起来,然后让那孩子过来向阿姨道歉。那孩子态度非常不好,摩托车手把他训斥了一顿。他跟那孩子说就是过来道个歉,什么事都没有。孩子的父亲自始至终一直在,但是什么也没说,什么也没做。骑手坚持,那孩子最终道了歉。骑手后来接受采访,说他这不是管闲事,这也不是闲事,他不希望这个世界这么冷漠。

    DSCF3035副本.jpg
    晚上的南艺艺术创客空间

    另外是一则广告,父母亲给自己的孩子买平板电脑,店员询问孩子多大了,父亲说孩子今年 5 岁,店员让父亲 7 年后再来给他买。父亲不解,店员说孩子还小,在他变得冷漠之前应该和家人在一起,享受家的温暖。

    DSCF1144副本.jpg
    白天的南艺艺术创客空间

    第三件事是我这周末又去了南艺。去年去的时候正值开学,校园里的创意街区,人来人往,沿街点缀设计精良的小店,极富感染力与艺术气息。这次去,南艺还在暑假中。夜色中,校园里只有晚饭后散步的三两人。走在创意街区,几只小猫在路灯下吃路人撒的猫粮,秋叶落下,只有凄凉。

    DSCF3039副本.jpg
    路灯下的猫

    真正有价值的是人,是正在活着的人。刨去了人的行为、感情,时间也会黯然失色。

    Image
    南艺下午茶
  • 傍晚雨中雨后的南京建邺

    傍晚雨中雨后的南京建邺

    二零一七年七月二日,南京,雨

    这天雨下得并不大,湿气倒很重

    从走廊向外望去,白茫茫一片

    是南方的那种烟雨楼台

    如果在白墙黑瓦的秦淮河畔

    大概是另一幅诗意的景象

    DSCF2845.jpg
    雨中

    雨过天晴后的建邺

    天际线还不错

    空气清澈而舒爽

    DSCF2895.jpg
    雨后
  • Laravel 使用 Laravel Mix 打包前端静态资源,引入 Vue

    Laravel 使用 Laravel Mix 打包前端静态资源,引入 Vue

    Laravel 的静态资源处理方式不断更新,5.4 版本从 基于 gulp 的 laravel-elixir 完全升级到构建于 webpack 之上的 laravel-mix。

    项目原来的打包策略

    1445238527elixir-1024x549.jpg

    本站基于 Laravel 开发,更新到 Laravel 5.4 之前的静态资源打包混合使用laravel-elixir(gulp)webpack,由于要实现静态资源(主要是 js)跟随版本的模块化加载,以便下一步实现静态资源分离,使用 CDN 加速(本站使用七牛),其时的gulpfile.js打包脚本如下:

    /* ...省略了一些变量定义代码... */
    
    elixir(function (mix) {
        /* bootstrap 字库 */
        mix.copy(AWESOME_FONT_PATH, `${DIST_PATH}/fonts`);
    
        /* 复制图片 */
        mix.copy('resources/assets/img', `${DIST_PATH}/img`);
    
        /* CSS 处理 */
        mix.sass('lib.scss', `${DIST_PATH_WITH_VERSION}/css/lib.css`);
        mix.sass('app.scss', `${DIST_PATH_WITH_VERSION}/css/app.css`);
        mix.sass('control.scss', `${DIST_PATH_WITH_VERSION}/css/control.css`);
    
        /* JS 处理 */
        mix.webpack([], `${DIST_PATH_WITH_VERSION}/js`);
    });
    

    webpack.config.js文件中的处理脚本如下:

    /* ...省略了一些变量定义代码... */
    
    // webpack 入口
    let entry = {
        vendor: ['jquery', 'bootstrap-sass']
    };
    
    /**
     * 生成文件入口
     */
    _.forEach(JS_MODULES, module => {
        if (module !== 'helpers') {
            entry[module] = path.resolve(JS_PATH, module, 'index.js');
        }
    });
    
    //noinspection JSUnresolvedVariable
    module.exports = {
        entry: entry,
        output: {
            filename: '[name].bundle.js'
        },
        resolve: {
            alias: {
                'jquery': path.join(__dirname, 'node_modules/jquery/src/jquery')
            }
        },
        plugins: [
            new webpack.optimize.CommonsChunkPlugin({
                names: ['vendor', 'manifest'],
            }),
        ],
    };
    

    更新到 Laravel 5.4 之后项目的打包策略

    1-LthzZzfbaat9WPg6hQRCNg.png

    更新到 Laravel 5.4 之后,全部使用 webpack 来打包静态资源,迁移步骤如下:

    1. 参考 Laravel 5.4 更新 package.json 文件;
    2. 添加webpack.mix.js文件;
    3. 复制一份webpack.config.js文件到项目的根目录(备份好原有的配置文件):cp node_modules/laravel-mix/setup/webpack.config.js webpack.config.js 删除package.json文件scripts字段中如下参数配置,以加载使用复制的配置文件--config=node_modules/laravel-mix/setup/webpack.config.js 使用复制的 webpack 配置文件的原因是要支持模块化跟随版本的打包方式,实际上仅仅修改了原有配置中的输出路径。
    4. 迁移原有的配置,webpack.mix.js文件更新如下:const { mix } = require('laravel-mix'); /* ...省略了一些变量定义代码... */ /* CSS 处理 */ mix.sass('resources/assets/sass/lib.scss', `${DIST_PATH_WITH_VERSION}/css/lib.css`) .sass('resources/assets/sass/app.scss', `${DIST_PATH_WITH_VERSION}/css/app.css`) .sass('resources/assets/sass/control.scss', `${DIST_PATH_WITH_VERSION}/css/control.css`); /* JS 处理 */ _.forEach(JS_MODULES, module => { if (module !== 'helpers') { mix.js(`${JS_PATH}/${module}/index.js`, `${DIST_PATH_WITH_VERSION}/js/${module}.bundle.js`); } }); /* Vue 处理 */ mix.js('resources/vue/index.js', `${DIST_PATH_WITH_VERSION}/js/vue.bundle.js`); /* 公共库 */ mix.extract(['jQuery', 'vue'], `${DIST_PATH_WITH_VERSION}/js/vendor.bundle.js`); 可以看到,添加了 Vue 处理路径配置,具体的处理脚本,原作者已经在webpack.config.js中写好了!

    无论是原有的打包方式,还是新的打包方式,,我们要做的也就是根据项目规划,配置一下打包文件入口和输出而已,其他的处理,Laravel 已经配置好了。

  • 树莓派(Raspberry Pi)入门实战记录之 SSH 局域网无线连接

    树莓派(Raspberry Pi)入门实战记录之 SSH 局域网无线连接

    树莓派作为一台完整的微型电脑,可玩性很高。树莓派使用 microSD Card 作为存储介质,安装系统就是把系统镜像写入 microSD Card 中。将写好系统的 microSD Card 放入树莓派的 microSD Card 插槽中,通电,就完成了树莓派启动。我手中的树莓派是 Raspberry Pi 3 Model B 型,集成了蓝牙和 WIFI 模块。

    在 Mac 下用 ApplePi-Baker 安装树莓派系统非常方便,ApplePi-Baker 的界面如下图所示:

    WechatIMG1.jpeg
    1. 左侧选择要写入树莓派系统的 microSD Card;
    2. 右侧上方小窗口 IMG file 选择下载好的树莓派系统镜像(我一般使用树莓派官方制作的 RASPBIAN 镜像,可以从树莓派官网下载到);
    3. 然后点击右侧上方小窗口中的 Restore Backup 按钮,等进度条跑满就完成了树莓派系统的制作。

    接下来的操作就是将写好系统的 microSD Card 装进树莓派,开机。

    如果有一台显示器,可以选择相应树莓派操作系统的图形界面版本,将显示器、鼠标、键盘接入树莓派来实现普通桌面电脑的操作体验。我拿到树莓派的时候手边并没有可用的显示器,也并不打算通过外接键盘、鼠标的方式实现对树莓派的控制,所以下面介绍通过 SSH 方式连接树莓派。如果通过 SSH 方式连接树莓派,在写好系统后要新建一个名为 SSH 的文件放入 microSD Card 的根目录,以打开树莓派的 SSH 功能。然后进行如下操作:

    1. 将树莓派通过网线连接路由器;
    2. 通过路由器的管理界面查看树莓派目前使用的 IP 地址,记下这个地址(我的是:192.168.0.15);
    3. 树莓派的默认用户是 pi,密码是 raspberry ,通过以下命令可以连接到树莓派:ssh pi@192.168.0.15 WX20170415-215417@2x.png

    经过上面三步已经通过 SSH 方式连接到了树莓派,接下来我们可以让树莓派通过无线连接方式连接到路由器(当然得是无线路由器)。我使用的是树莓派官方制作的 RASPBIAN 系统,选择其他系统可能操作不同。

    1. 查看可用的网路,必须使用 sudo (这步非必须,如果知道无线路由器的 ESSID 的话): sudo iwlist wlan0 scan 记下要连接的无线路由器的 ESSID:WX20170415-220916@2x.png
    2. 编辑 /etc/wpa_supplicant/wpa_supplicant.conf,添加无线网络配置: network={ ssid="Targaryen-Cloud" psk="your-password" }
    3. 重启系统,拔掉网线,通过路由器管理界面可以看到树莓派已经通过新的无线方式连接到路由器了。

    这一步结束后路由器可能会为树莓派分配新的 IP 地址,所以 SSH 连接树莓派时注意切换新的 IP 地址!

  • 在树莓派上安装 Docker 记录

    在树莓派上安装 Docker 记录

    树莓派默认源中的 Docker 版本太老了,一些 Docker 的新特性无法尝试,这里尝试在树莓派上安装新版本的 Docker。安装方法主要参考了开源中国的一篇文章 《Raspberry-jessie Docker 安装记》。如果去看原文并且也只是想安装 Docker 的话 ,建议从原文的第 5 步开始看起,前面是作者的试错过程。这里将可以成功在树莓派上安装新版本 Docker 的关键步骤整理如下:

    1. 添加源到系统,这里使用的是阿里的源,将 debhttp://mirrors.aliyun.com/docker-engine/apt/reporaspbian-jessie main 加入到系统 /etc/apt/sources.list 文件中。
    2. 将 key F76221572C52609D (这 key 是在添加源后执行 sudo apt-get update 的警告信息中获得的)添加到系统 apt-key 列表:gpg -a --export F76221572C52609D | sudo apt-key add -
    3. 安装 Dockersudo apt-get update sudo apt-get install docker-engine

    至此,完成了在树莓派上安装新版本 Docker 的全部过程!

    nature-2129493_1920.jpg