分类: 技术

各种开发心得,包括语言、软件工程、开发工具等

  • 我所了解的 ChatGPT:二次开发;有何限制;对未来的影响

    我所了解的 ChatGPT:二次开发;有何限制;对未来的影响

    前言

    ChatGPT 其实去年底就已经在开发界大放异彩,但是圈子之外对它了解不多。春节过后,公关公司开工上班,马上开始紧锣密鼓的宣传,然后就开始破圈,如今已经是整个公共领域,尤其是创投方面最热门的话题。

    我最近也接到需求,要开发一个基于 OpenAI API 的小应用,于是开始深入了解。如今应用初步完成,对 OpenAI 的产品有了更具体的了解。再结合之前向做 AI 的朋友请教,并总结自己观察思考,于是想写一篇文章分享给大家。希望未来的 AI 世界我们都不要缺席。

    OpenAI 的服务

    ChatGPT 是 OpenAI 的一项服务。它的内核是 GPT-3.5。OpenAI 还提供很多其它服务,比如图像生成、文本分析、比 ChatGPT 质量略差的 GPT-3 等。使用这些服务需要一些操作或技巧,比如翻墙——这次是 OpenAI 先动的手。以下是我摸索出的一些经验,希望可以帮后来者省去一些时间。

    注册

    目前注册 OpenAI 比较麻烦,因为他不向中国用户提供服务,所以必须有国外手机号,并且全程使用全局代理才可以完成。国外手机号可以借用接号平台来绕过,大家可以按需选用;如果有国外的亲朋好友帮忙,就会很容易。

    绑卡

    如果你只想在网页端使用 ChatGPT,可以暂时不绑卡。如果要使用 API 或者其它服务,就得绑卡。绑卡很麻烦,国内信用卡都不支持。虚拟信用卡比如 Payoneer,只对企业开放,需要资质审查,也不太好搞。

    至于我,最后还是拜托国外的亲戚帮忙搞定。目前有三个月的免费期,暂时够我把设想的应用场景跑一遍了。

    GPT-3 与 GPT-3.5

    大热的 ChatGPT 就是 GPT-3.5,现在只提供网页服务,如果要当成 API 调用,需要一些转换步骤,比较麻烦。从开发角度来说,直接使用 OpenAI API 会简单很多,但是只能使用 GPT-3 模型,对话 质量会差一些。不过看起来 ChatGPT API 已经在登记预约中,猜测很快也会开放,所以先把代码写好,等待开放应该也可以。

    目前来看,我认为官方不希望大家偷摸使用 Web 接口,近期可以继续尝试,长远来看最好做好切换到 API 的准备。

    我的进展

    我目前实现了本地通过 OpenAI SDK 调用服务 API。不过官方 SDK 有些问题,比如因为使用 Axios,无法部署在 Vercel Edge Function,必须放在自己的服务器上。所以接下来我计划做两件事情:

    1. 尝试不用 SDK,把逻辑直接放在 Vercel Edge Function 里
      • Vercel Edge Function 无论是否开启 stream: true 都会报告 504,猜测是 OpenAI 封禁了 Vercel。理论上当然可以继续尝试绕过,不过长远来看,与服务商做斗争并不明智,先放着吧。
    2. 搭建 ChatGPT Web API 环境,以便直接使用更好的服务
      • 需要使用第三方反向代理服务器,存在一些风险。而且他们并不愿意提供部署代理服务器的方案,理由是给非官方代理方案更大的生存空间。我觉得那就不如耐心等待 ChatGPT API 开放。

    ChatGPT 的限制

    (以下内容感谢 @Gary 指导。)

    4097 tokens

    GPT-3.5 的最大长度是 4097 token,根据我做 AI 的朋友讲解,汉字=2token,英文=0.5 token。也就是 GPT-3.5 的上下文最多保持 2k 汉字或 8k 英文字符 的内容。所有文本合并到一起发给 AI,AI 给出答案;我们再把新文本续上,发过去,AI 给出新的答案。直到最初的内容被挤出去,产生新的上下文。

    这是什么意思呢?比如我们日常交流,都是自带上下文的,跟父母、跟同事、跟恋人说话不一样,也是因为上下文不同。我会跟游戏里的同好聊魔兽世界,但是如果跟父母说同样的话题,他们就会不知所云。这就是上下文的差异。

    换言之,我们跟 ChatGPT 对话,用中文,教给它一件事情,累计 2k 字之后,他就会忘记这个要求。要避免这种情况,我们就得每隔一段时间重新教它一次;或者,以编程的方式重构 prompt,添加先决条件,以便维持特定功能。

    听起来有理有据,但其实错误百出

    我称其为“尬聊之神”。ChatGPT 并不是真的智能,或者说,目前的 AI 实现都在从不同方向模拟人类的智能,也许我们最终会成功,但是现在似乎还有些距离。具体到 ChatGPT 上就是,你说什么,他都会给出回应,但是回应有没有价值,不好说。

    尤其在一些绝对的事实方面,因为训练语料的问题,ChatGPT 的表现会比较差。因为对它来说,假的、错的语料,只要语法正确,也是好语料。这方面 Bing 里号称 GPT-4(我对此版本号表示怀疑)的模型表现就会好很多,因为它会结合网页权重,使用更权威的材料。

    在编程领域也是如此。因为开源软件的关系,ChatGPT 拥有非常丰富的程序开发知识,可以帮我们解决很多问题,写出很多代码。但是这些代码写得如何、能不能跑起来,还很难讲。所以,能不能把编程的工作丢给它?目前不能,它甚至不具备基于语言特性进行逻辑推导(语法检查)的能力。——但是不代表我们不能用它提升效率、学习技术。

    还没有真正的智能,也无法持续学习

    前面说过,ChatGPT 可以在保留一定上下文的基础上,与当前用户进行有状态的交流。所以我们也可以教 ChatGPT 做一些事情,比如发出指令:“以后提到日期,都用 YYYY-MM-DD 的格式”。接下来,我们就能把 ChatGPT 当成自动格式转换器来使用。或者,我们可以让它换用不同的语气、不同的语法,改变输出的内容,契合某种风格。比如出名的胡总编模拟器、鲁迅模拟器等。

    但这些并不是自我意识与学习,本质上只是 ChatGPT 根据完整上下文合成的文本,而已。有很大的限制:首先我们必须保留足够的上下文,其次我们也没有办法直接把这个状态转移到其它用户。

    哪些未来更可期?

    ChatGPT 的出现,让大家都很兴奋,我也一样。我们都确定未来可期,但是通常来说,总会有一些未来更可期,另一些未来不那么可期。结合上面提到的问题,我认为有一些领域可能不太好做:

    老年人陪护(x)

    我有个朋友上一份工作主攻老年人市场,所以他立刻就问,能不能用 ChatGPT 做一款老年陪护软件。

    我认为不行。这里涉及到两个问题:

    1. 上下文限制。AI 会损失大量的历史记录,需要用户花费大量的时间反复训练。对有经验的用户来说,可以通过各种手法优化,对老年人来说,可能会反复经历挫折。
    2. 不够准确。因为训练语料的问题,ChatGPT 无法保证内容的准确性,如果老年人寻医问诊,可能得到错误的答案。众所周知,AI 不能背锅,这一点也很难解决。

    但也有一些领域会有很大的机会:

    语言类,翻译、文书等

    这方面算是 ChatGPT 的主场了,无论翻译,还是文书书写,目前来看 ChatGPT 都能完成的非常好。4097 tokens 的限制,可能需要我们在产品层面给予一定的辅助设计,但是在可以想象的空间内,都能产生不错的产品。包括但不限于:

    1. 小说生成器
    2. 解说文字生成器
    3. 内容/关键词提取器
    4. 商务邮件辅助工具
    5. 学外语辅助工具
    6. ……

    太多太多,不一一列举了。总之,这块儿几乎一定会产生很多应用,甚至我们现在就能见到不少。

    用户界面

    我认为 ChatGPT 最大的价值就是全新的用户界面。以前我们的用户界面,无论命令行、图形化,都只针对具体的需求,需要用户自己有清晰的认知、有明确的方向、并认真学习。如果用户没有学习过,就很难使用现有的产品。想象一下,如果用户可以用自然语言发出命令,那几乎所有产品界面都可以重建得更好用。

    举个例子,我们家 Siri 最常用的功能就是定时,比如煮泡面:嘿,siri,定时 4 分钟。但是其它功能很难做到,因为其它的功能描述起来太复杂,而且表达方式也比较多,Siri 目前处理不了。

    ChatGPT 则可以从用户的文字描述中提炼出有价值的信息;经过简单的训练之后,还可以发出指定的命令。所以我设想,将来很多东西都可以用它重建,比如(GPT 没有好的翻译,所以我就用姆伊姆伊来替代):

    • “姆伊姆伊,帮我叫水”——我家桶装水喝完之后,要打电话给水站让他们送水——ChatGPT 自动拨号,跟客服简单沟通,叫水。
    • “姆伊姆伊,帮我订个外卖,吃粉吧“——这个需求会复杂很多,除了外卖之外,我们需要 AI 分辨出“吃米粉”这样的需求,并且从历史当中,判断我们常吃的粉是哪一家,然后帮我们完成订外卖的需要。

    AI 公司的未来

    未来 OpenAI 这样的大型公司,能提供通用模型的公司会越来越少,因为通用模型数据量和计算量太大,小公司根本烧不起。但是做 AI 的小公司可能会冒出来,类似用 WordPress 做建站,小公司可以帮客户在大模型的基础上做 finetuning,帮助客户将 AI 集成到产品里。

    我会怎么做?

    首先,我一定要尝试用 ChatGPT 做产品。我觉得它是很重要的产品,是未来的重要组成部分。去年它开始在业内刷屏的时候,我没想到它能获得这么大的公众关注度,这对我们来说既是好消息也是坏消息。好消息是将来可以借助它的品牌做宣传,坏消息是势必有更多的竞争者入局。

    接下来是产品方向。我想做简历相关,让 ChatGPT 帮我们把简历做得更匹配 JD、更有竞争力。考虑到它在文本分析和生成方面的强势,我觉得这个方向有很大的机会。

    我也计划加入一家以 AI 应用层为主要产品的公司,不要错失良机。希望能找到合适的老板或团队,即尊重技术,又擅长市场,大家能够合作共赢。

    前端怎么做?

    我认为现在是前端的好机会,因为目前 ChatGPT 基于浏览器提供服务,所以浏览器扩展就有很大的想象空间。建议所有前端小伙伴都好好学习一下 ChatGPT 的相关知识,能够实际开发一两个相关产品。比如,有人会让 ChatGPT 推荐一些配色,如下图:

    ChatGPT 只能给出色值,不方便直接看到。我们就可以写一个浏览器插件,将页面上的颜色转换成色块显示出来,并且支持一键保存到自己的调色盘。利用好这段真空期,有很大的发展空间。

    总结

    以上,就是我从去年得知 ChatGPT,到最近一周基于 ChatGPT 开发浏览器扩展,再结合我看到的、聊到的、想到的内容,集中分享。

    希望对看到文章的各位有启发、有帮助。如果你对 ChatGPT,对近期的 AI 热潮有想法、有问题,欢迎留言讨论。更欢迎针对我文章的评议、讨论。

    未来,AI 一定会有一席之地,我们也一起来争取属于我们的新领地吧。

  • 2023 告别 CSS 预处理工具,彻底拥抱 TailwindCSS

    2023 告别 CSS 预处理工具,彻底拥抱 TailwindCSS

    CSS 是声明式语言,很简单,很好学,但是写起来很累,所有东西都要写出来才能生效。复用方面更是无从下手,虽然大家都在不断总结,但始终没能找到足够好用的方案,可以有效改善 CSS 开发。

    于是我们只好把视线转出 CSS 之外,转投 CSS 预处理工具,Less、SASS(SCSS)、Stylus,引入种种 CSS 不具备的功能,帮助我们改进开发体验。比如嵌套、函数、循环、条件,等等。然而如果你细心观察,实际上,这几个工具最近 5、6 年都没怎么更新(我说的是功能性),因为该有的都有了,甚至很稳定;其它来自于 CSS 的改进,几乎跟它们没什么关系,也不用更新。

    最近几年,随着 CSS 发展,一些新特性逐步引入,我觉得这些工具越来越难用,它们能带来的好处已经无法掩盖它们所造成的问题。是时候告别 CSS 预处理工具了,就像我们当年告别 jQuery 一样。

    为什么说预处理工具落后?

    我把理由分成三大类:

    预处理工具的问题

    • 对 CSS 函数兼容性不好,尤其是 rgba()hsl() 这些常用的颜色函数
    • 数值类型转换,有不符合预期的行为,比如 Stylus 实现 content:5

    CSS 的改进

    • CSS 拥有越来越多的函数,可以直接进行计算,比如前面提到的颜色;还有 calc() 来完成基础数学计算
    • CSS 变量非常好用,可以大大改进编程体验,配合各种 JS 框架,我们可以更容易的把数学逻辑和显示效果绑定在一起
    • CSS Houdini 可以实现新功能,即使不深入使用(JS 部分),也有好用的自定义属性
    • CSS 也开始从预处理工具吸收营养,比如近期的嵌套功能已经开始被整合,未来我们可以直接使用

    预处理工具无法跟进的问题

    • 很多缩写、复合属性无法处理,比如 background-imagebox-shadow 等,都支持多属性共同生效,预处理工具擅长的循环、条件、函数无法提供帮助。
    • 预处理,顾名思义,发生在生产之前。实际上,网页在实际浏览时,会有很多因素影响到渲染结果,比如分辨率、dark mode 等。预处理工具对这些需求也没有改进。

    替代方案

    我目前的替代方案基于 TailwindCSS,所以自然包含 PostCSS、AutoPrefixer 等工具。然后用 postcss-import 实现自动导入和模块化;使用 tailwindcss/nesting 实现嵌套。

    为什么选用 TailwindCSS?首先,实际开发中,不管使用什么前端框架,我们都需要大量原子化的胶水样式,比如调整间距、改变字号、给容器添加一些边框、圆角、阴影等。这些样式如果都手写,工作量并不小;学习不同的样式名也是负担;以及最重要的,CSS 优先级问题。使用 TailwindCSS 就都能很好解决。

    TailwindCSS 不仅包含一大堆原子化样式,自身也是个完整且优秀的 CSS 编译器。它包含 reset,提供一组全局通用的 CSS 变量;它可以从各种文件里把我们用到的样式提取出来,构建后生成的 CSS 里只有我们要用到样式,不会有多余的;它会分析我们对样式的使用,合理的调整样式顺序,保证样式能正确生效。使用 TailwindCSS 可以节省很多时间。

    它还自带若干插件,比如解决嵌套的 tailwindcss/nesting,支持内容类元素的的 @tailwindcss/typography 等。使用这些插件也可以帮我们节省很多时间。

    最后,TailwindCSS 的生态不断成长,我们的选择范围越来越宽:HeadlessUI、DaisyUI、付费的 Tailwind UI 等。方便我们从产品生命周期的任意阶段开始集成。

    推荐项目配置

    启动项目的时候,安装依赖。包含 PostCSS + AutoPrefixer、TailwindCSS 和 DaisyUI。前者提供 CSS 处理框架,包含自动导入 css 和嵌套功能;后两者提供可见的 UI。

    pnpm i postcss postcss-import tailwindcss autoprefixer daisyui -D

    自动初始化配置,-p 会自动生成 PostCSS 配置:

    pnpm tailwindcss init -p

    调整 postcss.config.js,启用 postcss-importtailwindcss/nesting。目前我们常用的嵌套规则和 CSS 规范略有区别,不过无所谓,规范也没确定,所以这样就足够了。

    module.exports = {
      plugins: {
        'postcss-import': {},
        'tailwindcss/nesting': {},
        tailwindcss: {},
        autoprefixer: {},
      },
    }

    然后调整 tailwind.config.js

    const DaisyUI = require('daisyui');
    // 这个插件可以帮我们处理文档类内容,我建议常用
    const Typography = require('@tailwindcss/typography');
    
    module.exports = {
      // 从以下文件查找用到的样式
      content: [
        './index.html',
        './src/**/*.{js,ts,jsx,tsx,vue}',
      ],
      theme: {
        extend: {
          // 扩充 TailwindCSS 没有包含的样式
        },
      },
      plugins: [
        DaisyUI,
        Typography,
      ],
      daisyui: {
        themes: [{
          // 只构建一个主题: luxury,并覆盖其中的两个属性
          luxury: {
            ...require('daisyui/src/colors/themes')[ '[data-theme=luxury]' ],
            primary: '#FFA028',
            '--bc': '0 0% 87.5%',
          },
        }],
      },
    }
    

    然后,创建样式入口 main.css。其它样式可以如常写在这个文件里,不过如果要 @import 其它 CSS 文件,就要进行一些调整。具体可以看官方文档。

    @tailwind base;
    @tailwind components;
    @tailwind utilities;

    然后在入口文件引用 main.css 即可:

    import './main.css';

    至此,新项目配置完成,可以照常开发了。

    下期预告

    这次我先分享了整体思路:用新的工具链替代预处理工具,保证已有的功能不缺失。那么下期分享的内容就是使用新的 CSS 特性,更好的完成开发。


    如果你对新 CSS 感兴趣,对预处理工具和新工具链有兴趣和疑问,欢迎留言讨论。如果本文对你有启发,也请帮我点赞分享,谢谢。


    本文参与了 SegmentFault 思否写作挑战赛,欢迎正在阅读的你也加入。

  • 【视频】肉山的模拟面试系列:初中级前端,欢迎奥利奥同学

    【视频】肉山的模拟面试系列:初中级前端,欢迎奥利奥同学

    某天群里,奥利奥同学突然提问:想参加模拟面试,不知道能不能搞。我一想,可以呀。我工作 16+ 年,面试过很多新人,也带过不少新人;给自己的公司招过人,也帮朋友招过人;有面试过新人,也跟资深老鸟聊过。感觉有蛮多能分享的内容。从客观需求来说:近期有朋友委托我帮忙做前端面试;我很久以前分享过的前端面试题也该更新一波了。

    于是就答应下来,准备每周做一期。开门第一单,就邀请奥利奥同学来做吧。于是就有了下面的视频:

    也欢迎大家移步 B 站帮我三连分享:肉山的模拟面试系列:初中级前端,欢迎奥利奥同学

    这位奥利奥同学,他毕业后就加入一家偏传统的公司,待了一年半。现在感觉有点被天花板挤压,想试试看新的挑战。于是我结合我的经验,对他的简历进行评价,然后简单过了一轮面试。以下是视频内容大纲:

    1. 如何准备简历
    2. 奥利奥同学的简历分析
    3. 初中级别前端面试
    4. 结对编程测试
    5. 面试点评
    6. 问题解答+交流

    希望以上内容对近期求职面试的同学有帮助。如果你对求职面试入职过试用期等有问题,欢迎在留言、弹幕里提出,与我讨论交流。如果你也想参加模拟面试,可以跟我联系,预约时间。

    感谢奥利奥同学帮我想到这个方向。

  • 【视频】Node.js 开发 RAR 解压缩命令行工具

    【视频】Node.js 开发 RAR 解压缩命令行工具

    拖来拖去,终于把 使用 node.js 开发命令行工具 workshop 的视频剪出来了,前几天上传到 B 站,访问量很一般,所以在自己的博客再捞一下。

    这次视频主要面向新手,主要呈现从 0 到 1 实现命令行工具的做法,希望观众无论基础如何,都能在看完视频之后,掌握封装仓库、实现命令行工具的做法。内容大约是:

    1. 不同系统下安装 node.js
    2. 创建命令行工具项目
    3. package.json 结构介绍
    4. 介绍 unrar-promise
    5. 介绍 yargs 实现命令行接口
    6. 开发功能
    7. 发布到 npm

    完成的项目放在 GitHub:meathill/unrar: a simple script to unarchive rar files (github.com) 非常简单,大家可以参考。

    有任何问题、建议均欢迎留言讨论。新的一年,我会努力多做视频、多做好视频,希望大家支持我。

  • 【视频】如何正确使用 TailwindCSS

    【视频】如何正确使用 TailwindCSS

    TailwindCSS 是一个争议很大的样式库。

    他封装了大量原子化的样式,比如 w-4,表示 width: 1remtext-gray-500,表示字体颜色为灰色500。如果我们某个节点同时需要两个样式,就是 class="w-4 text-gray-500"。极端一点的例子是这样子的:

    很明显,大家的争议点在于:

    1. 这么细碎的样式,我为什么不自己写?灵活性还高一些。
    2. 这么写跟 inline style 有什么区别?
    3. 开发一时爽,维护火葬场。

    基本上,发出这些疑问的都是前端。包括我,最初也是这样的想法。但是有一天,我要维护一个老项目,大部分组件都是现成的——引用自某个组件库,或者团队已经写好,只需要调整布局,我发现 TailwindCSS 简直是神器,太方便了。

    于是,当我反复看到大家争论该不该用 TailwindCSS 后,决定做一期视频,表达一下自己的态度:

    1. 我们做技术,要避免对一项技术做非黑即白的判断,更不应该轻易否定一项技术。同时,使用技术 A 并不代表就要拒绝技术 B。
    2. 具体到 TailwindCSS 上,使用它不代表我们从头到尾就要只能用 TailwindCSS;使用其它前端框架也不要求我们绝对不能使用 TailwindCSS。
    3. 所以正确的做法是,我们应该使用 TailwindCSS。
    4. 项目启动时,使用比较完整全面的前端框架,比如 Element UI、AntDesign,或者基于 TailwindCSS 打造的 DaisyUI;然后辅以 TailwindCSS。这样就可以同时照顾开发效率与维护效率,收获最佳效果。

    除了并不会降低开发效率之外,TailwindCSS 还有以下优势:

    1. 它跟内联样式有很大的区别,它的优先级很低,意味着我们也可以很容易覆盖、调整。
    2. TailwindCSS 样子很好看,直接能画出漂亮的界面。
    3. 基于 TailwindCSS 的代码分享很容易,只要复制粘贴 html 即可,在前端工程化日趋复杂的今天,简直是一股清流。
    4. 因为文档组织得更好,后端和其它领域的开发者也很喜欢使用 TailwindCSS 替代手写样式。

    所以无论如何,我都推荐所有团队所有开发者使用 TailwindCSS。当然,用其它原子化样式框架,比如 UnoCSS 也可以。

    如果你有其它意见和建议,欢迎提出讨论。如果你有 B 站账号,恳请三连+关注+转发,感谢。

  • 聊聊前端入门(1):HTML+CSS

    聊聊前端入门(1):HTML+CSS

    最近有一些新老同学入门前端,找我问问题,我从他们身上发现了一些共性问题,今天拿出来总结一下,希望后来者能吸取经验。

    前端三大件:HTML、CSS、JavaScript。这三位是根本,万变不离其宗,不管用多先进的技术,最终浏览器里跑的还是他们(不考虑 WASM、WebGL)。所以前端入门应该以这三种语言为基础,慢慢扩展到其它领域。

    当然有些同学可能上来就用框架,拜现代化前端技术所赐,有些同学可能完全没认真学习过这三门语言,也怼出了功能、甚至怼出了产品。但我建议,无论从上限考虑还是从下限考虑,都应该好好学习这三门语言本身。

    声明式语言

    入门一般都从 HTML、CSS 开始,因为它们很简单,简单的原因是因为它俩是声明式语言。那什么是声明式语言呢?标准定义咱们略过不谈,简单来说有两大特点:

    1. 需要什么你就写什么,需要这里有一个文字,那就写一个文字;需要有一张图片,就写一张图片。
    2. 不写的就不会出现。没有逻辑推演、计算,不会因为环境变量而有所不同,没写的就没有。

    于是它们有好处也有坏处:

    • 好处:没有心智负担,不用理解全局,就看某一行、某一段,写了就是有,没写就是没有。
    • 坏处:需要 100 个字就要写 100 个字。有 100 个元素共用一个属性,可能也要写 100 遍。

    如何学习 HTML+CSS

    有些同学上学期间学过编程,比如 C 语言、Java 语言。它们都是命令式语言,有更强的逻辑性,它们就像一本推理小说,你必须从头看到尾,不漏过一个线索,层层推理,才能得到结论。

    他们会套用自己在命令式语言方面的经验去学习 HTML+CSS,然后发现,好难……命令式语言的语法元素很少,难在组合出合适的逻辑、设计合适的数据结构。声明式语言能实现的效果总数是固定的,没有预先设计的部分,就是实现不了,谁都实现不了。但是每种效果的实现可能都是不同的,需要大量经验和记忆。用上一段的类比,声明式语言就像诗歌,甚至上一行跟下一行都没联系,但是连到一起还真有点意思。

    所以学习 HTML+CSS 就要有合适的方法。我个人的经验如下,

    1. 首先要知道

    HTML 标签 100+,CSS 属性 200+,这些都是知识性的内容,不知道就是不知道;不知道但是要用,那就做不出来。所以第一步是知道这些内容,方法大概是:

    1. 浏览文档,比如 MDN
    2. 自己实验,加深记忆和理解
    3. 跟背单词一样,要经常性重复这个过程,才能真的记住

    这个过程不需要了解的很透,重点在于知道这些标签和属性的存在,当你面对需求的时候,才会想到解决方案。

    2. 接下来要多尝试

    经常浏览 codepen.io 这样的网站,上面有很多别人做的范例,可以长很多见识,让你恍然大悟,原来可以这样做那个东西。

    同时,也可以反过来用看到的范例指导自己的学习。比如,看到一个 css 规则,这个你刚好不熟悉,就可以找来文档仔细阅读理解一下,或者在论坛上询问其他前辈。

    一段时间之后,相信你会对大部分 html、css 属性都了如指掌,看到人家的网站,也大概知道该怎么做。但是给你一个实际项目,你可能还做不出来,或者做不好。没关系,那是下一步的目标。

    3. 紧跟社区步伐

    跟其它语言不一样,声明式语言从诞生开始,语法上不会有太大变化,语法元素也很固定。升级换代主要来自增砖添瓦,即新规则、新属性。

    比如 CSS,早年我们布局只能依靠 float。很难用,有各种诘屈聱牙的概念需要记忆。后来就有了 display:flex,也就是弹性盒模型,好用的多。后来又有了网络布局。然后因为各种内部弹性外部弹性,又有了 min-contentmax-contentfit-content 等新属性。

    这些东西,很新,有时候没有办法找到合适的学习资料。于是就要紧跟社区来,通过技术账号了解到新知识,通过技术文档学习新知识。

    4. 突破自己

    自学里面,最难的一件事其实就是突破自己。就好像我今天看到一个帖子,楼主说准备找到工作后就办张健身卡去健身,然后下面一群人歪楼,说“为什么健身要办健身卡?”

    因为自己监督自己很难,自己给自己打分更难。所以学校才要安排各种考试,给学生打分,让大家知道自己的位置。

    所以,做出来自己看觉得还行,没有用。这里有几个建议:

    1. 瞄准一个产品,比如微信,做到像素级复制。可以截图然后调到 50% 透明度,叠到一起慢慢看。
    2. 选择最合适的技术,坚持到底,不要因为某些环节难以突破就乱来。
    3. 邀请比较有经验的前辈帮自己做 code review

    附加:最好找个靠谱的前辈做引路人

    (我想了想还是把这条加上。)

    互联网是个好地方,我们能免费获取几乎所有有价值的资料。但是对于 Web 开发、前端开发这种领域,长期积累的海量知识储备也可能成为大家学习的障碍——东西太多,不知道从何处下手。

    所以,有个靠谱的引路人也很重要,会帮你节省很多时间。笔者不才,亦好为人师,有需要的可以找我:

    新人常犯的错误

    新人难免会犯错,这里列举一些常见的、可以规避的问题:

    1. 沉迷细节无法自拔

    有些同学沉迷细节,喜欢抠字眼,经常会提出一些奇奇怪怪的问题。很多问题其实没有准确答案,也不需要准确答案,尤其是对于新人来说,可能三两年内都不会真的需要解决。

    尤其是 HTML、CSS,因为是声明式语言,很多时候,它们的行为就是这么设计的,不需要也没办法深究;有些设计,可能来自二十年前甚至更早,现在早已没有那个应用场景,新人难以理解也是正常的。

    所以我建议大家以记用法积累经验为主,辅以实操验证,不需要像学命令式语言那样去强迫自己理解。

    2. 遇到问题生拼硬凑

    有些同学遇到问题就发慌,然后开始百度,看到一个方案就复制粘贴试一试,能行就继续,不能行就复制粘贴下一个。结果代码就补丁摞补丁,各种解决方案混合在一起,毛病越来越多。到最后可能完全超出掌控、无法解决。

    这里大家需要明白,语言元素很多,组合方式也很多。同样的问题,可能来自不同的根源;类似的组合,也会产生不同的问题。绝大部分时候,我们都要先选择方案,然后围绕方案组织代码,解决遇到的问题。在不同方案间左右横跳,最后结果多半不是好处均沾,而是问题集中爆发。

    如果能够找到最佳实践,那就坚持。如果几个方案都差不多,那就选好一个,坚持下去。遇到问题,面对多个搜索结果,要分析它们是怎么做的,解决了什么问题,不要直接拷过来试。坚持一段时间,会有很好的结果。

    总结

    我当年也是自学成才,走过不少弯路,希望这系列分享能帮大家节省一些时间。

    有任何问题和建议,欢迎留言评论。下次聊聊 JS 方面的学习。

  • 在 Code.fun 做 Code Review(四)

    在 Code.fun 做 Code Review(四)

    时光如梭,一晃 2022 年已经过去 2/3,我们一起迎来 9 月。秋风送爽,丹桂漂亮,下面,我们一起回顾 8 月份我在 code.fun 完成的 Code Review 吧。

    关于 Code Review 的介绍和经验,欢迎大家回顾前三篇,本文暂不重复:


    (更多…)
  • 在 Code.fun 做 Code Review(三):聊聊 Promise 的错误处理、如何真正学到技术

    在 Code.fun 做 Code Review(三):聊聊 Promise 的错误处理、如何真正学到技术

    嗯,不知不知觉这个系列写到第三篇,这一篇会改变一下写法,从一次 Code Review 出发,讲解几个技术点,然后分享一下技术学习的经验,以及 Code Review 的用途。希望对大家有帮助。

    顺便推广下前两篇:

    0. 起因

    某天,我给一位同事做 code review,看到下面这段代码:

    const err = new Error('错误信息');
    
    return Promise.reject(err);

    于是我就回复:error 最好直接 throw

    然后他回复我:如果改成 throw 的话,这个 catch 好像是捕获不到 throw 的。

    这就很诡异了,这不符合 Promise 的设计;而 Promise 不是新生事物,它有很大的测试集可以保证行为符合预期,我觉得我们想遇到问题都很困难。于是我就跟这位同事连线,帮他分析问题。

    1. 真正的问题

    连线之后,我发现他的真正问题并不是按照我的要求修改代码之后遇到的。由于基础不太牢靠,他先写了一段实验代码,想要验证我的要求,结果这段代码行为出现异常:

    他希望能够通过 .catch(err => console.log(err)) 捕获并记录错误,但是从控制台的输出来看,错误却是 Uncaught(未捕获)。那么问题出在哪儿呢?

    作为曾经讲解过 Promise 的我当然是一眼就看出来问题所在,但是这位同学却琢磨不透。于是我就把问题发到研发群里,果然又问倒好几位同学。

    现在我请问各位读者老爷,你们知道么?或者换个方向,如果想正常使用 .catch() 捕获到错误,应该怎么修改呢?

    2. 问题解答

    Promise

    这里我们必须回到 Promise 的规范,才能了解上面截图的问题所在(关于详细规范,请参阅 MDN Promise,这里只摘我们需要的部分):

    1. Promise 主要用来改进编写异步函数的体验。
    2. 通过 new Promise(function (resolve, reject) {}) 会创建一个 Promise 实例,其中的参数应该是一个函数(通常是异步函数),接受两个参数:resolvereject。当异步操作成功之后,应调用 resolve(result) 并传递结果;当异步操作失败,应调用 reject(reason) 并传递错误信息。
    3. Promise 有三个状态:pendingfulfilledrejected,起始状态是 pending,变更后,就固定下来,不会再次变更。
    4. 如果异步函数本身抛出错误,Promise 也会进入 rejected 状态。
    5. fulfilled 的 Promise 实例会转入 .then() 处理;rejected 会转入 .catch() 处理。

    我们再回头看上面的截图,这里的问题在于,错误是在 setTimeout(异步函数)的回调函数里抛出的,抛出时当前 Promise 所在的执行栈已经结束了,回调函数是全新开启的执行栈,所以 Promise 无法捕获到它里面的异常;而它也没有主动调用 reject(err) 传出错误,所以就变成了 Uncaught(未捕获)。

    函数执行栈

    既然说到执行栈,我们就顺便补充一下执行栈的知识吧。形如以下代码:

    function a() {
      b();
    }
    function b() {
      c();
    }
    function c() {
      d();
    }
    function d() {
      // do something
    }
    a();

    当函数 a 执行的时候,运行时会开启一个新的执行栈,并且把 a 入栈接下来发现 a 调用了 b,就又会把 b 入栈;然后发现 c……运行时会重复这个过程。当一个函数执行完毕,比如 d,运行时就会把它移出栈,然后继续执行 c 的剩下部分。

    对于 JavaScript 而言,错误是可以冒泡的。即在 d 抛出的异常,如果没有被捕获,它就会一直上浮,直到回到全局。所以我们可以在栈内的任何一个环节捕获它;但如果跨栈,那就无法捕获。

    Event Loop

    异步函数的回调函数会由 Event Loop 开启新栈执行,与所以异步函数自身处于不同的执行栈,所以错误不会被异步函数捕获,自然也不会改变 Promise 的状态。

    解决问题

    所以,要正确捕获错误的话,就需要想办法捕获异步函数的回调函数的错误,即:

    function yy() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          // 方案1
          reject(new Error('错误信息'));
          // 方案2,实际中很少这么写,这里只是用来演示。但要注意,捕获必须放在回调栈,才能捕获到发生的错误。
          try {
            throw new Error('错误信息');
          } catch (e) {
            reject(e);
          }
        }, 1000);
    
        // 实际场景中,更多是这样的,err 作为参数传给回调函数
        fs.writeFile(path, content, 'utf8', (err) => {
          if (err) {
            return reject(err);
          }
          resolve();
        });
      });
    }

    3. 如何学习技术

    对于上述问题,我们只要掌握 Promise 规范、函数执行栈、Event Loop,就很容易判断。但是如果这三个概念有一个不太清楚,就容易犯迷糊。所以,很多时候,要避免出问题、要保证软件正常工作,我们必须清楚了解每个技术的规范、定义。

    比如 Promise、原型链、闭包等等,它们都不是自然界的产物;而是在长期软件开发实践中,由开发者总结设计出来,用于解决特定问题的发明。他们都有严格的定义、功用、优缺点,等等。我们日常开发,应该把这些东西当成知识储备起来,针对需求做排列组合,给出解决方案。

    有些同学相反,他们会记下来一些技术的用法,然后反推这些技术的特性。如果对一个技术不熟悉,他们会尝试做类似实际场景的实验,然后再想办法搬到实际场景中。这样做,覆盖常见场景可能没问题,遇到陌生的领域就容易踩坑。

    所以,我要强调,对于新技术,大家不熟悉又要尽快用在实际开发中,临时做点实验记住些经验用法当然可以。但不应满足于此,要找时间把技术规范补起来,把完整的设计至少读个几遍。一方面纠正自己的错误实践,另一方面,也可以扩大你对这个技术的应用面。

    4. Code Review 的作用

    这里也不得不提 Code Review 的一个重要作用:

    传承知识。Code Reviewe 是非常好的查缺补漏机会,可以针对性补强开发者的知识盲区,纠正不良习惯。

    通过 Code Review,我发现了一位同事在 Promise 和函数执行栈方面存在知识盲区,然后我借机帮他补齐了这方面的知识。接下来,我把这个问题分享到技术研讨群,还有几位同学也不是很清楚,也趁机补齐了。于是,我厂再出现这个问题的概率,就降低了。换言之,我厂技术群体的下限,就拔高了。

    思否上有同学问:什么样的技术 leader 是称职的? 我的答案第一条就是:

    给团队的技术兜底。通过工具、规范、流程,保证无论开发水平如何,都能尽快提交符合要求、满足规范、质量过硬的代码。

    具体一点:要重视每一次 Code Review,找到问题,解决问题,补全大家的知识点,提升团队下限。

    5. 总结

    上次的 Code Review 分享获得了意料之外的欢迎,希望这次同样能帮助到大家——我已经把上面的问题加入我的面试题库了,哈哈。

    我现在在 code.fun 工作,我们的产品会把设计稿自动转换成代码,期望大大提升前端的开发效率,如果你对这个产品感兴趣,欢迎联系我,或直接试用。我们公司提供远程工作岗位,有兴趣的同学可以联系我;朋友的公司 API7 也在招聘前端,有兴趣可以找我内推。

    如果诸位读者老爷对软件质量管理、软件开发效率、Code Review 有什么想问的、想说的,敬请在评论区与我互动。

    6. 扩展阅读

  • 在 Code.fun 做 Code Review(二)

    在 Code.fun 做 Code Review(二)

    这周周会上,有同事说:

    以前他的 PR 被 approved 就直接合;现在他会等上一两天,期待我来 review。晚上耍手机时如果突然一阵爆响就说明我 reviewing,他就爬起来好好看我的评论然后作出修改,学到很多东西。

    被承认当然很爽、很骄傲。于是我想起这个系列,该再更一篇了。本文选材自七月的 Code Review 经验,趁热先分享一波。更早的 Code Review 过两天再往前翻翻看能否整理出来。

    关于 Code Review 的介绍和经验,欢迎大家回顾:在 Code.fun 做 Code Review,本文我就不重复了。

    0. 不应该使用独立 <script src="...">

    在这个需求中,我们要使用火山引擎统计用户行为,某同事就直接在模版里添加了 <script src="..."> 导入初始化 JS。这段 JS 非常之短,只是帮火山引擎初始化执行环境,但是为了保证执行状态,它既不能 async 也不能 defer

    这样做有几个问题:

    1. <script> 会阻塞后面的 HTML 渲染和 JS 执行;
    2. 这段 JS 虽然短,但是它的下载时间不会短:
      1. 浏览器要解析 DNS,
      2. 然后文件服务器要找到文件
      3. 文件服务器甚至没有 http2

    正确的做法是把它直接塞到现在 JS 里,只增加几十个字节而已。

    1. mounted 钩子添加的事件处理函数没有清理;直接使用 window.onblur

    这位同学犯了两个错误:

    1. 他在 mounted 钩子里添加了事件处理函数,但是并没有在 destroyed 钩子里清理。这可能导致内存泄漏,也可能导致奇怪的表现。
    2. 他直接将处理函数绑在 window 上,如果其它库、其它代码使用了同样的事件,就会导致冲突。

    2. 糟糕的命名

    这里这位同学犯了两个错误:

    1. event 是名词,不应该用做函数名。函数名应该是动词、动宾短语、动副短语,表达一个动作。
    2. event 是个非常非常通用的单词,用作全局函数太容易和其它全局对象起冲突或起混淆,应该加长。

    结合上下文,合适的写法是:triggerHuoshanEvent()

    3. 依赖 JSON.parse()

    我们需要记录一个小数据:sourceType,这个小数据被封装在一堆很大的 JSON 里面。这位同学直接拿 JSON.parse() 解码,然后打点。这样做当然可行,但是效率上非常之浪费。

    大家千万不要小看 JSON.parse(),它其实有很多细节:

    1. JSON 数据格式非常之严格,换言之,绝大对数情况下,必须把所有字符串都分析完才能结束
    2. 这个过程中,会构建大量对象
    3. 之后不再使用,这些对象又要被逐个回收

    所以正确的写法,应该使用字符串匹配或者正则。字符串操作都是 O(1),而且会在匹配到结果后立即返回,所以性能优秀非常多。

    (其实吧,这位同学的正则也有些一言难尽,不过今天先不展开。)

    4. TypeScript 的数据转换

    使用 TypeScript 之后,我们需要声明函数的返回值类型。对于一些比较通用的返回值,比如截图中的 mongoose lean(),就要进行类型声明转换,后面的代码才知道取到的值是什么类型。这里大家要记住,无论是浏览器还是 node,TS 都要经历编译之后,变成 JS 再被执行。

    所以 TS 阶段的类型转换不会出现在最终执行的代码里,也不会影响效率。但是截图中属于不同的情况,它会留下一个 .then() ,并增加一个微任务,不是我们想要的结果。

    5. TS 的枚举值可以适当调整

    TypeScript 提供 enum 关键字,用来声明枚举值,可以大大提升开发效率,降低出错概率。枚举的值可以手动指定,也可以省略,省略后 TS 会自动帮我们从 0 开始递增。上面就是典型的使用场景,列举若干设计软件,最后是 Other(其它),可以做成单选多选放在表单里。

    这段代码的问题在于,设计软件很多,我们列出来的肯定有疏漏;即使没有,将来也会发布更多设计软件。比如有天,我们自己做了设计软件,叫 design.fun,那么该放到哪里呢?放到 Other 前面,它的值就会跟 Other 冲突;放到后面,又会使得数据看起来很奇怪。

    此时只要给 Other 一个大点的值,拉开差距,就可以解决这个问题:

    export enum DesignSoftware {
      Photoshop,
      XD,
      JsDesign,
      Other = 100, // 再大点也行,不过 100 应该够我们用了
    }

    6. 组件名称和根节点样式应包含不少于两个单词

    这个其实是 Vue 官方的风格建议。原因很简单,一个单词,无论作为组件名称还是根节点样式名,都太容易和其它组件重复,也会影响 IDE 里快速跳转的效率,所以尽量多写几个单词,反正编译过程可以自动优化。


    总结

    时间过的真快,距离上次分享 Code Review 经验已经过去四个月了,我在 Code.fun 也是半年多工龄的老员工了。经过半年多的努力,我厂的产品完整了很多,但也很难称得上完善。希望这次分享的内容对大家有用,也希望大家多来试用我们的产品。

    如果诸位读者老爷对软件质量管理、软件开发效率、Code Review 有什么想问的、想说的,敬请在评论区与我互动。

    我现在在 code.fun 工作,我们的产品会把设计稿自动转换成代码,期望大大提升前端的开发效率,如果你对这个产品感兴趣,欢迎联系我,或直接试用。

  • 纯 CSS 实现优惠券效果

    纯 CSS 实现优惠券效果

    (本文不是广告,因为没给钱。)我厂 code.fun 上线了付费购买与优惠券功能,欢迎各位新老顾客莅临。

    上面是优惠券的视觉效果,本文分享如何使用纯 CSS 实现它,希望对大家有帮助。

    0. 分析

    首先,我们来分析一下这个优惠券的实现方案。

    左边是摘要,右边是详情,这个部分用 display:flex 很容易就能搞定。中间的虚线,使用任意一个容器边框 + 少许 padding 即可实现。其它部分,也就是些字体行高,都不是很复杂。难点在于投影,尤其是左右两个挖空的半圆。

    1. 挖空

    1. 首先,我们给整个优惠券矩形加上投影
    2. 然后,我们给两边加上两个包含内投影的圆形
    3. 这个时候,两边的内投影原型会多出来一块,我们需要把它们盖住。但是,不能让矩形 overflow:hidden,因为投影也会变,如下图。
    .coupon {
      width: 15rem;
      height: 6rem;
      background: white;
      box-shadow: 1px 1px 6px rgba(0,0,0,.15);
      position: relative;
      overflow: hidden;
      
      &::before,
      &::after {
        background-color: white; 
        border-radius: 1rem;
        box-shadow: inset 1px 1px 6px rgba(0, 0, 0, 0.15);
        content: '';
        width: 2rem;
        height: 2rem;
        position: absolute;
        top: 2rem;
        z-index: 1;
      }  
    
      &::before {
        left: -1rem;
      }
      &::after {
        right: -1rem;
      }
    }

    2. 增加父容器

    我的第一反应是增加父容器,让父容器 overflow:hidden 来隐藏多出来的部分。但是不行,会影响投影。

    但是转念一想,我们可以不让父容器限制显示内容,而是在父容器内部增加一些元素,遮蔽多出来的内容。比如用两个纯色圆形,把竖着的阴影遮起来。

    于是我把上面的样式更名为 .coupon-inner,然后增加一个父容器。父容器的 ::before ::after 伪元素都搞成略小一圈的纯色圆形,把竖着的投影挡住,最终效果如下图。

    3. 总结

    到这里,效果就基本让人满意了。

    最终完成的代码可以在 codepen 里看到:

    https://codepen.io/meathill/embed/oNqxOXE?default-tab=html%2Cresult&editable=true

    使用纯 CSS 的好处,在于体积小、加载快,调整起来非常灵活,能用 CSS 最好都用 CSS。

    有任何问题和建议,欢迎评论、讨论。