作者: meathill

  • 推荐网页 IM:Drift

    推荐网页 IM:Drift

    可能有些同学注意到了,最近两个月,只要打开这个博客并且稍微浏览一会儿,右下角就会弹出一个聊天框。此时读者就可以跟我聊天,如果我当时在线(打开了管理端),就能看到消息并且实时回复;如果我不在线,可以上线后再回复,读者下次回到博客时,也可以看到消息。

    这个工具就是 Drift,它本质上是一个面向商家的网页 IM,支持匿名聊天,方便客户与商家交流。有点类似淘宝旺旺,只不过它可以嵌入任意网站。

    实际上我是帮我厂试水,装上之后还真的有人用它咨询问题,然后聊了几句,各方面体验都不错。就很快实装到我厂官网了。

    上线之后表现很好,工作日期间每隔一两天就会有人来咨询,而且接下来的进展也都比较理想,大部分可以发展到试用阶段。对比过去几年“联系我们”的邮箱,两者在接待客户方面真是天壤之别。

    不过 Drift 也有不少小毛病,比如:因为网站在国外,所以国内的启动速度比较慢;静态资源的缓存设置也有问题,导致每次加载都要很久(我都想去给他们调 CDN 配置了……)。国内的同学可以考虑用去哪儿的 StarTalk,不过从架设难度上比 drift 难用很多、功能弱很多、界面丑很多……

    总之,在网页上添加客服 IM 对增加客户有很大帮助,建议大家有机会都搞一下。

  • CSS 网页保持全屏并自动伸长

    CSS 网页保持全屏并自动伸长

    其实是个小需求,以前也搞过很多次,没想到前几天被坑了一下,记一笔。

    以前,如果想要页面在内容任意多的时候都能占满浏览器,可以简单设置:

    html,
    body {
      height: 100%;
    }

    但是这样设置,在 Safari 浏览器上,会将 <body> 固定为窗口高度,如果内容多,就会被底部挡住内容。解决方案是 body 高度用 min-height:100%

    如果是三行一列的结构,即上面是导航条,下面是脚部,中间随内容自适应,可以用:

    body {
      display: flex;
      flex-direction: column;
    }

    这个时候,不能用 flex-basis,在 Safari 上会失去弹性,也要用 min-height。所以,最终样式大概就是:

    html {
      height: 100%;
    }
    
    body {
      min-height: 100%;
      display: flex;
      flex-direction: column;
    }
    
    #nav {
      height: 4rem;
    }
    #bottom {
      height: 10rem;
    }
    #content {
      flex-grow: 1;
      min-height: 40rem;
    }

    另外,因为基本只有桌面浏览器需要这个功能,所以可以考虑加一个 @media (min-width: 576px) 做限制。

  • 解决跨域问题笔记

    解决跨域问题笔记

    跨域问题常遇常新,每次都觉得再也不会有问题了,结果过几天又会掉进新坑。

    因为种种原因,最近一个项目需要跨域请求 API。然后就随手设置了一下,结果 GET 没问题,POST 就不行,很明显是撞到跨域墙上了。

    最后发现原因:

    1. 我们启用了 basic auth 验证用户身份
    2. OPTIONS 也会被要求验证
    3. 预请求失败,后面的正式请求就不会发出

    趁着还没忘,总结一下跨域的处理过程:

    1. 首先,熟读《MDN HTTP访问控制(CORS)》
    2. 跨域时,复杂请求(除 HEADGETPOST) API 需要返回 CORS 头
    3. 发起复杂请求前,会发送一个 preflight 请求,也就是 OPTIONS,很多坑都在这个请求上
    4. OPTIONS 是浏览器自动发送的,不受我们控制,在开发者工具的 Network 面板里也看不到。我们经常需要模拟它,检查返回是否符合预期。请求头在后面。
    5. OPTIONS 无法处理 Basic auth,如果开了的话,要做特殊处理
    6. 需要返回 Access-Control-Allow-Origin 允许跨域的域名,简单点可以写 *,但如果要上传 cookie(withCredential: true),则必须写明域名,且只能是 一个 域名
    7. 所以如果有多个域名要跨域访问 API,需要在服务器端判断来源,并返回不同的域名
    8. 如果要上传 cookie,需要在请求时声明 withCredential: true
    9. 服务器还要返回许可的方法,即 Access-Control-Allow-Methods: GET, DELETE, PATCH 等,让浏览器判断
    10. 如果前面都通过了,浏览器才会发送正式请求。如果正式请求失败,则看不到任何返回。

    测试 OPTIONS 请求头

    OPTIONS /resource/foo 
    Access-Control-Request-Method: DELETE 
    Access-Control-Request-Headers: origin, x-requested-with
    Origin: https://foo.bar.org
  • Vue 2020 年路线图,Vue 3.0 计划于 Q2 发布

    Vue 2020 年路线图,Vue 3.0 计划于 Q2 发布

    昨天 Vue 团队更新了 2020 年的路线图,里面包含了很多 Vue 3.0 的信息。建议大家一定要看原文,地址在:https://github.com/vuejs/vue/projects/6。下面我结合自己的理解翻译一下:

    FAQ

    问:3.0 啥时候能好?

    答:请往后看。另外请注意,这些日期仅供参考,我们团队的首要目标是发布生产级别的高质量代码,不是赶 deadline。

    问:3.0 里都有啥变化啊?

    答:请自行翻阅最新的 RFC。另外,也要注意核心团队提交的 RFC 草案。

    如果某个 RFC 里包含破坏性变更,那么里面一定会有“升级策略”章节,讨论迁移问题。

    对于现在的 2.x 用户,我们会提供:

    • 迁移向导
    • 能够兼容 2.x 的兼容性版本(如果能兼容的话),并且对该升级的地方给出提示和升级建议
    • 命令行迁移工具
      • 自动升级能升级的代码
      • 不能自动升级的,扫描出来提示手动升级

    问:我是新人,我现在该学 Vue 2.0 还是等 3.0?

    答:如果你刚刚开始学习框架,那么应该开始使用 Vue 2。我们没有对 Vue 3 进行巨大的重新设计,所以大部分 Vue 2 知识仍将适用于 Vue 3。 如果你打算学习框架,没有必要等待。

    如果你要为某个生产级别的项目选择技术栈:

    • 如果项目需要立即动工:我们仍然建议使用 Vue 2,以便获得完整的、框架级别的支持。 但是,也请留心 3.0 中即将发生的更改,不要使用将被移除的功能,最好也不要使用与 Vue 2 深度耦合的第三方依赖。
    • 如果项目可以等到 Q2 末:那我们建议你等等,用 3.0。

    问:以后 2.x 会咋样呢?

    答:接下来会有一个小版本(2.7)更新:

    • 将兼容的 3.x 功能反向移植回 2.x
    • 对 3.x 弃用的功能发出警告

    这是 2.x 最后一个小版本,并提供长达 18 个月的 LTS(长期支持)。即使在 LTS 结束之后,我们也会继续提供重要的安全更新。

    问:Vuex 方面有什么计划么?

    答:一方面,我们正在开发Vuex(4.0)版本,其 API与 当前版本(3.0)完全相同,但与 Vue 3 兼容。我们力求向下兼容,让用户可以在 Vue 3 项目中继续使用现有 Vuex 代码。

    另一方面,我们也在尝试新的设计,更多的利用 Vue 3 的响应式 API,也让 Vuex API 不那么冗长。 这个新版本暂定为“ vuex-next”,也就是 5.0。 眼下,我们只是在进行早期探索,最早也要到 2020 年第三季度才会发布。

    2020 一季度计划

    • 3.0 SSR
    • 3.0 迁移
      • 升级向导(施工中)
      • 2.x 兼容版本
      • 迁移工具
    • 3.0 框架
      • router(施工中)
      • Vuex(施工中)
      • 测试工具(施工中)
      • JSX babel 插件(施工中)(我以为不会有这个东西了呢)
      • CLI
      • Devtools
      • 其它(虽然最后三个没标施工中,不过我觉得多半也是在施工中咯)
    • 3.0 beta
      • Q1 末发布!
      • 3.0 核心现在其实已经完成了,我们希望 API 到这个时候也能稳定下来。
      • 我们还需要更多的时间才能更新周边的库和工具。 如果您的使用场景对 router 和 vuex 没有硬性要求,这个时候就可以开始使用 3.0 了,但最好是非关键性应用程序。

    2020 二季度计划

    • 继续之前未完成的 3.0 框架工作
    • 季度中,发布 3.0 RC
      • 冻结 API,不再有重大变化。进入 RC 之前,所有涉及到重大变更的 RFC 都要定案。
      • 全家桶能够和 3.0 版本协同工作。
      • 3.0 版本就绪,此阶段基本可用。仍然会有一些小错误和框架集成问题,在 RC 阶段都会慢慢被解决掉。
    • 3.0 发布管理
      • 回归测试
      • 自动化每晚发布
      • 正式确定版本生命周期
    • 3.0 IE11 兼容性版本
    • 3.0 官方正式版

    2020 三季度计划

    发布 2.7 版本

    • 反相迁移 3.x 功能到 2.x
    • 对 3.x 中弃用的功能发出警告
    • 2.x 最后的小版本,LTS
  • 解决“[ERR_PACKAGE_PATH_NOT_EXPORTED]: No “exports” main resolved”

    解决“[ERR_PACKAGE_PATH_NOT_EXPORTED]: No “exports” main resolved”

    周末例行升级系统,今天打开项目,npm run dev,就报这个错误。检查代码,没变化,依赖也没变化。因为错误位置在 main.js,尝试给它加上 exports,无果。

    Google 之,发现一个非常新的 issue:https://github.com/babel/babel/issues/11216,3天前,来自 @babel/babel 仓库,多半是了。

    点进去一看,原来 node.js 从 13.10.1 之后,对 package.json 里的 exports 属性解读出现问题,继而导致 Babel 抛出错误。最简单的解决方法就是升级 Babel 到 7.8.4。

    升级后问题解决。

  • 使用 Proxy 创建有魔术属性/方法的类

    使用 Proxy 创建有魔术属性/方法的类

    前几天在 SF 回答了这个问题:如何使用proxy,如何在内部拦截get方法,然后翻了翻以前写的博客:使用 Proxy 添加魔术属性/方法,发现上次写完代理对象就停笔了,所以今天补全一下:创建有魔术属性/方法的类。这样就比较完整了。

    JavaScript 构造函数的特点

    ES6 增加了 class 关键字,梳理了面向对象的语法,现在我们可以这样定义一个类:

    class Person {
      constructor(name) {
        this.name = name; 
      }
    
      hello() {
        return `Hello, my name is ${this.name}.`;
      }
    }

    如果你有其它面向对象语言的经验,应该很容易理解这段代码。

    不过 JS 的构造函数特别,它支持 return 一个其它对象,作为 new SomeClass() 的结果。(如果不 return 或者 return 一个空对象,那么 new SomeClass() 得到的就是 SomeClass 的实例。)

    也就是说,原则上,我们可以这么做:

    class Person {
      constructor(name) {
        this.name = name;
        return {name: 'Meathill'};
      }
    }
    
    const person = new Person('张三');
    console.log(person.name); // 'Meathill'

    结合 Proxy

    理解了上一节的内容,我们就很容易得到这样一个类:

    class Person {
      constructor(name) {
        this.name = name;
    
        return new Proxy(this, {
          get(target, property) {
            if (property in target) {
              return target[property];
            }
            console.warn('Sorry, I can't do that.');
          }
        }
      }
    }

    在这个类的构造函数里,我返回了一个 Proxy 实例,代理了对真正 Person 实例的访问。当访问的属性/方法在实例上时,就返回需要的属性/方法,否则的话,输出警告。

    实际上,Proxy 的 getset 的功能远不止如此,上面的代码只是一些演示。

    用途

    魔法属性/方法主要有以下用途:

    1. 对象 a 要使用一部分对象 b 的功能,但是又不方便直接用原型链。比如上一篇文章的场景,我提供类 VElement 作为接口,实际完成工作的是另一个沙箱中的 Element。
    2. 不知道会怎么访问对象,希望所有访问都照顾到。
    3. 希望捕获到对对象的修改,也就是 Vue 3.0 的核心修改。

    Vue 3.0

    Vue 1.x & 2.x 期间,都在使用 ES5 的 Object.defineProperty 拦截对对象的修改,实现响应式。这样的做法看起来很神奇,给 Vue 带来了巨大的成功。但是这样做也有坏处:

    1. 声明实例时需要很多预处理工作,而且数据量越大处理的时间越久
    2. 不支持某些数组操作
    3. 不支持其它数据类型,比如 Set、Map
    4. 不支持后续的数据观察

    使用 Proxy 之后,以上问题全部都迎刃而解,甚至,因为 Proxy 是原生 API,性能表现更好,取得了内存减半、速度加倍的效果。

    想了解更多 Vue 3.0 的新特性,可以去看我在 SF 的分享:迎接 Vue 3.0。(注:这是免费广告,我不会从新购用户取得收益。)


    参考文章:

    Constructor, operator “new”(构造函数和操作符 “new”)

  • 从 uiprint.co 聊一聊“练习作品”

    从 uiprint.co 聊一聊“练习作品”

    我一直有做 Side project 的心,经常想实践一些 idea,不过多数止步于画原型。

    虽然我常年购买 Adobe Creative 和 Sketch,也经常使用 Photoshop、XD、Sketch 切页面,不过并不擅长真刀实枪的“创作”。一旦涉及到画原型,进度就会很慢,经常让我的 indie hacker 之路止步于此。于是我就想,要不还是画在纸上。

    然后我就找到这个网站:uiprint.co。上面有很多做好的 PDF,直接下载打印,就可以得到很好的设计底图,然后连连画画(上面有网格点),就能画出很棒的设计原型。这样做最大的好处是,设计思路不会中断,可以专注于产品逻辑。

    为了方便大家理解,我截了一张图

    接着聊聊“练习作品”。

    我经常逛技术论坛和问答区,常常看到有同学提问:“自学前端,能仿着教程写出 demo,接下来该怎么做?”,或者“应届生,该写什么项目经历?”

    面对这种问题,我都会建议他:

    1. 不要仿做 demo,要做有人用的东西
    2. 认真地进行推广、迭代
    3. 解决遇到的各种问题

    模仿写 demo,意味着作品没有得到真正的检验。没有职业经验的新人,他眼里的“也做得出来”、“挺好”,在商业产品里多半连及格都算不上。满足于做这样的作品,没有办法获得真正的提升,写到简历里,也没什么价值。

    接下来的问题是:做什么?

    首先,不要贪大求全。你当然可以做电商网站,或者自己从头写个博客、论坛,看起来很高端,但实际上既耗时耗力,也没有什么价值,因为没有人真的会用。

    其次,要把眼界放宽。互联网已经是一种基础设施,大部分行业都能被互联网赋能提升。与其照抄千篇一律的 todo-list,电商网站首页,好好想想自己周围的人需要什么,做一个他们会用的东西更有价值。

    uiprint.co 就是一个很好的例子:

    1. 它本身很简单,就是网页+可以下载的 PDF,纯静态,开发成本维护成本都很低。即使是前端新人,也不太会遭遇无法攻克的技术难题。
    2. 有价值,是 Product Hunt 日榜第一,所有需要做产品设计的人都可以获得帮助。不断有人用,作者就有不断更新的动力,也有不断升级的需求,比如视觉效果调整、访问量统计、添加内容的后台等等。
    3. 就更不用说这个网站本身可能带来的价值

    其实我们身边类似这样的机会并不少。比如 2010 年的时候,智能手机刚刚兴起,就有一个人告诉我,他想做一个应用,告诉你在北京上海这样的大城市怎么做地铁,哪站有厕所,哪站车门开在那边、等等。

    这样的项目坚持下来,虽然未必有经济收益,但一定能获得很多有价值的项目经验,对入行、对找工作,都会有很大帮助。


    总结一下:

    1. 想自学、想提升自己、想找工作,做项目一定要做有人用的东西,不要模仿做 demo
    2. 选择题目不需要特别大,也不需要纯互联网,有人用 是首要原则
    3. 接下来就是坚持,坚持开发、坚持维护、坚持推广

    希望对大家有帮助。

  • Flarum 添加导航并实现 i18n

    Flarum 添加导航并实现 i18n

    Flarum 是一个开源论坛软件,基于 PHP Laravel 打造,外观很好看,功能也不错,一直想试一试。近期我厂要建社区,在我的强烈建议下,就选用了它。

    Flarum 诞生很久了,现在的版本是 0.11.1-beta,还是测试版。官网也反复强调,不要把 flarum 用在生产环境,因为一切还不稳定。——这点从文档也能看出来,有些范例代码没法跑在最新版软件上。

    换言之,坑比想象的多。经过将近两周断断续续的折腾,踏空很多次,完成了一些些小东西。接下来分享一下,也当给自己做笔记了。

    (更多…)
  • 给 Hexo 增加替换大图并生成缩略图的功能

    给 Hexo 增加替换大图并生成缩略图的功能

    我厂的官方博客使用 Hexo 搭建,静态页比较符合我厂的技术风格。

    我厂博客可能会用到一些很大的火焰图 SVG,厂长担心打开速度会受拖累,所以让我想办法把大图替换成缩略图,单击再打开原图。

    经过一些研究,我大概得到以下线索:

    1. Hexo 会加载 `/scripts` 下面的脚本
    2. Hexo 是用钩子机制,跟 WordPress 非常像
    3. Hexo 的钩子函数可以使用 Promise

    那么,大概方案就出来了:

    1. 添加脚本,等待 `after_post_render` 钩子。这个钩子触发的时候,博文已经从 markdown 渲染成 html。
    2. 使用 cheerio,找到所有图片,检查图片大小,略过不太大的图片(我的标准是 500K)
    3. 对大图生成缩略图,替换 `src`。

    因为目标是 SVG => PNG/JPG,在 npm 里搜了一圈,发现大家不是 phantom.js 就是 puppeteer,那我就不需要用库了,自己直接写好了。

    经过反复的尝试,最终完成的代码如下:

    hexo.extend.filter.register('after_post_render', async function (data) {
      /** 摘录、详情、内容 */
      const dispose = ['excerpt', 'more', 'content'];
      for (const key of dispose) {
        const $ = cheerio.load(data[key], {
          ignoreWhitespace: false,
          xmlMode: false,
          lowerCaseTags: false,
          decodeEntities: false
        });
    
        // 获取所有需要调整大小的图片路径
        let images = $('img').map(async function () {
          let src = $(this).attr('src');
          if (!src) {
            if (config.log) {
              console.info("no src attr, skipped...");
              console.info($(this));
            }
            return;
          }
    
          // 顺便加上 `loading="lazy"`
          $(this).attr('loading', 'lazy');
    
          // take snapshot fo big SVGs
          const originalSrc = src;
          if (/\.svg$/.test(src)) {
            const img = resolve(__dirname, '../source', originalSrc);
            const {size} = await stat(img);
            if (size <= config.limit) {
              return;
            }
            $(this)
              .attr('data-src', src)
              .attr('src', `${src}.png`);
            return img;
          }
        }).get();
        images = await Promise.all(images);
        images = images.filter(image => !!image);
        // 只启动一次 pupppeteer,希望减少系统消耗
        if (images.length > 0) {
          const browser = await puppeteer.launch({
            defaultViewport: config,
          });
          const page = await browser.newPage();
          for (const image of images) {
            await page.goto(`file://${image}`);
            const buffer = await page.screenshot();
            const name = basename(image);
            console.log('screenshot for svg: ' + name);
            // 这一步很重要,因为 `after_post_render` 的时候图片还没有复制,所以要利用 hexo 自身的复制功能复制图片
            route.set(`images/${name}.png`, buffer);
          }
          await browser.close();
        }
        data[key] = $.html();
      }
      return data;
    });

    这段代码里,搞清楚应该用 route.set() 稍微花了些时间,Hexo 的文档写的真是差强人意。这个方法可以向指定位置写入内容,写入的可以是文本,也可以是二进制 buffer。如果写入的是绝对路径,那么就是简单的写入;如果是相对路径,就是向博文环境内写入,接下来的复制过程也会复制到最终代码里。