标签: 前端工具链

  • 【视频】如何正确使用 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 站账号,恳请三连+关注+转发,感谢。

  • 初试 SWC(Speedy Web Compiler)

    初试 SWC(Speedy Web Compiler)

    SWC 是一个用 Rust 写的编译工具,功能跟 babel 很类似。它的优势在于速度,按照官网所说,在单核上它的速度是 babel 的 20倍,四核上是 babel 的 70 倍。

    我最近尝试升级工具链,第一步是使用 esbuild-loader 替代 babel-loader,比较顺利。但是 esbuild 只支持 ES2015,为了支持 IE11,我们还需要再转译一次。这次我想试试 SWC。

    安装与配置

    安装 swc 很简单。我打算 build 的时候用 @swc/cli 转译;debug IE11 的时候把 swc-loader 套在 esbuild-loader 前面输出。所以需要安装三个包:

    nom i @swc/core @swc/cli swc-loader -D

    接下来添加配置文件 .swcrc

    {
      "sourceMaps": false,
      "module": {
        "type": "umd"
      },
      "minify": true
    }

    默认目标平台 target: 'es5',不需要再写。其它语言选项也走默认即可。

    SWC sourceMap 支持三种配置:truefalseinline,意思一看就明白。但目前 bug 比较多,即使设置 sourceMap:false,也只是不生成 sourcemap 文件,对应的链接标记仍然会生成。

    module.type 有另一个 bug:即使目标平台是 ES5,它也会使用 ESM 引入依赖,导致语法解析出错。比如代码中使用了 async function,SWC 转译时就需要 import regeneratorRuntime from 'regenerator-runtime'。所以上面我配置 module.type"umd",一方面避免错误的 ESM import,另一方面可以通过全局方式引用这些库。

    minify: true 表示我们希望 SWC 转译代码时顺便把它们压缩一下。官方文档说这个功能还不是很完善,不过我暂时没发现明显的问题。

    swc 游乐场

    SWC 官方提供预览网站:SWC Playground – SWC。我们可以把源码贴上去,查看编译结果。也可以调整选项,看看不同选项对结果的影响。

    问题

    把 SWC 放进工具链的过程还算顺利,不过接下来使用的时候却遭遇了不少问题。

    首先就是上文说的 ESM 导入的问题。默认情况下 SWC 会 import 'regenerator-runtime',这个问题必须解决。考虑到这个代码特征比较明显,起初,我打算直接字符串过滤掉。后来翻了文档,又在 playground 里尝试了一下,发现 UMD 应该可以解决问题。

    然后是语法问题。下面这段代码 SWC 编译错误:

    // 输入
    let a = 10;
    for (let b = 0; b < a; b++) {
        let c = 0, b = 10, d = 100;
        console.log(b);
    }
    
    // 输出
    var a = 10;
    for (var b = 0; b < a; b++) {
        var c = 0, b = 10, d = 100;
        console.log(b);
    }

    这段代码其实有点意思,我看了很久才找到问题:let 声明变量时,for () 相对于下面的代码块,是上级 context;而 var 时,它们处于同级 context。所以下面这段循环只会运行一次,而不是像上面一样,执行 10 次。

    (这段代码很能考察 letvar 的不同,我准备把它加入我的面试题库。)

    还有其它一些语法问题,因为上面这个问题无法绕过,意味着使用 SWC 这条路不可行,所以我也没有深入研究,就不一一列举了。

    总结

    SWC 用户量太小,未知 Bug 很多;开发团队不大,又是用 Rust 写的,也给想贡献代码的前端社区带来很大阻碍。所以目前还难以应对大型项目、工业级别的需求。

    不过它的速度的确很快,对改进项目构建速度有很大帮助。希望那些 bug 能尽快修复,我们可以早点把它应用到产品当中。

  • babel@6 升级到 babel@7,兼容性代码膨胀的原因

    babel@6 升级到 babel@7,兼容性代码膨胀的原因

    最近尝试把厂里项目的依赖从 babel@6 升级到 babel@7,发现打包之后体积大了很多。于是打开 webpack-bundle-analyzer,果然大部分代码都是 corejs 引入的,项目本身的逻辑只占少部分。

    从报告来看,虽然目标浏览器的版本均高于 Promise 的启动版本(比如 Chrome 32),但 es.promise 仍然会被打包进来。于是以 es.promise 为突破口开始分析,找到答案:因为 JavaScript 引擎 V8 直至 v6.6 版本时,在 Promise 实现方面都存在严重 bug,所以 babel@7 保守地选择 Chrome 66/67 作为临界点。

    想来其它体积多半也是这么加上来的,就不再一个一个排查了。 ​​​

    这就很难处理。不升级吧,新特性兼容不了;升级吧,包体积变大,公司上层又未必同意。

    下一步试试 esbuild 吧,或者回头手动打包一套 polyfill。目前设想的方案是:

    1. 用 babel 之类的工具提取出所有特性
    2. 根据 caniuse 生成必须的特性列表
    3. 像上面说的 Promise,因为 bug 所以必须兼容,我们就不考虑了,可以反过来加一条 eslint 规则规避
    4. 最终生成新的 polyfill 打包进来
    (更多…)
  • 正确使用 @babel/preset-env 与 @babel/plugin-transform-runtime

    正确使用 @babel/preset-env 与 @babel/plugin-transform-runtime

    这几天又在折腾项目脚手架,看到之前的配置里用到 transform-runtime,于是就想研究下。找了半天,发现现有的文章、讨论都没有准确、合理的说明,所以写篇博客介绍一下。

    定位

    @babel/preset-env

    @babel/preset-env 是一大堆插件的集合,包含了当前浏览器环境下,所有语言特性的插件,可以根据 browserslist 的结果,选择合适的插件将新语言特性转译成旧浏览器可以支持的表达方式。

    官方推荐使用预制件(preset),可以大大节省配置时间,提高开发效率。

    @babel/plugin-transform-runtime

    babel 转译时,往往需要用到一些工具函数,比如 _extend。转译以文件为单位,意味着每个文件里都可能有重复的 babel 工具函数。现代化项目常有大量的文件,就可能产生重复和浪费。

    @babel/plugin-transform-runtime 可以帮我们把直接写入文件里工具函数变成对 core-js 的引用,可以减少转译后的文件大小。

    使用场景

    从上面的定位可以看出,@babel/plugin-transform-runtime@babel/preset-env 并不重合,两者的功能没有交集。

    对于任何需要考虑兼容性的项目,我们都应该使用 babel 进行转译,并使用 @babel/preset-env 降低配置的复杂度,大部分通用的配置如下:

    module.exports = {
      'presets': [
        [
          '@babel/preset-env',
          {
            'corejs': 3, // 使用 core-js@3 版本,core-js@2 从 ES2017 之后就没再更新了,不推荐使用
            'useBuiltIns': 'usage', // 只转译用到的新语言元素
            'bugfix': true, // v7.9 之后引入的新选项,尽量减小转译后的代码体积,v8 之后会变成默认选项
          },
        ],
      ],
    };

    小型项目可以到此为止,中大型项目、文件数量比较多的,可以引入 @babel/plugin-transform-transform。此时我们需要先安装两个依赖——注意,这俩依赖都要装:

    # 用来开发、debug 和编译
    npm i @babel/plugin-transform-runtime -D
    
    # 用来在生产环境中使用
    npm i @babel/runtime
    # 如果要使用特定的 core-js 版本,也可以安装特定的 runtime
    npm install --save @babel/runtime-corejs2
    npm install --save @babel/runtime-corejs3

    接下来,修改上面的 babel.config.js,加入插件配置:

    module.exports = {
      // 重复上面的配置
      "plugins": [
        // 其它插件
        [
          "@babel/plugin-transform-runtime",
          // 一般情况下我们使用默认配置即可
        ],
      ],
    };

    完成。

    库项目

    库类项目也推荐使用 @babel/plugin-transform-runtime,因为库项目通常会面临另一个问题。如果我们直接导入 core-js 作 polyfill 的话,像 PromiseSetMap 这样的全局对象就会被覆盖。对于一般的应用而言,问题不大;但如果是库,你无法预期其它开发者会在什么情况下使用你的库,很可能他的目标平台都支持这些新语法元素,不希望转译污染。

    此时,使用 @babel/plugin-transform-runtime 可以让 babel 在转译时使用沙箱包装 polyfill,避免污染全局。

    参考文档

  • 使用 Webpack 动态打包文件夹

    使用 Webpack 动态打包文件夹

    各式各样功能强大的小语言是我厂机器编程的特色,为了让更多的同学提前感受到 DSL 的威力,我们开发了 demo 应用:https://demo.openresty.com.cn/

    Demo 里面,需要添加一些范例代码,方便用户直接体验。这些代码,可以通过后端 API 获取,也可以直接编译到前端代码里。目前范例很少,直接打包到一起应该是最简单的做法。

    但是如何打包是个问题。经过一些研究,我觉得这样做好:

    1. 负责小语言的同事直接把范例写在项目仓库里,Edgelang 就用 .edge 扩展名,Navlang 就用 .nav 扩展名
    2. 前端在 webpack 里实现一个 requireAll 的功能,把所有代码当成纯文本用 raw-loader 加载进来
    3. 这样添加了代码后,重新 build 一下就好。范例文件可以用 lazy-load 的方式加载

    Webpack 提供了一个方法叫 require.context,可以深度遍历一个目录,返回一个 context module,用这个 API 我们就可以动态的加载这个目录下的文件——具体的讲解和参数说明大家看下文档吧,这个部分中文资料不太多,我不太确定译名。我们可以在范例代码的目录下放一个 index.js,负责加载所有代码范例:

    function requireAll(r) {
      r.keys().forEach(r);
    }
    
    const Languages = [
      'edgelang',
      'navlang',
    ];
    
    let context = require.context('./edgelang', true, /\.(edge|css)$/);
    requireAll(context);
    context = require.context('./navlang', true, /\.(nav|css)$/);
    requireAll(context);
    
    export default Languages;

    然后我们改一下 webpack.config.js 就可以了。

    module.exports = {
      module: {
        rules: [
          {
            test: /\.(nav|edge|fan)$/,
            use: 'raw-loader',
          },
        ],
      },
    };