标签: frontend

  • 年轻人的第一个 nx 插件

    This entry is part 1 of 4 in the series Nx Monorepo Experience

    我们的项目中使用 lingui 作为 i18n 方案,在之前的实践中,每个项目都在 package.json 中维护了调用 lingui 提取 i18n 文本的命令,除此之外,在每个项目的 CI 中也包含了检查 i18n 文本是否存在遗漏的脚本。

    把项目迁移到 nx 管理的 monorepo 后,就可以着手把各个项目中重复维护的 lingui 相关脚本交给 nx 插件来实现,减少维护 lingui 相关脚本的负担。

    添加 lingui executor

    lingui 并没有提供官方的 nx 插件,但我们自己在 monorepo 中创建一个 nx plugin 来实现相关的功能。一般来说,nx 插件能够提供三种能力:

    1. executors,用来执行工具命令,例如,编译、运行测试
    2. generators,用来创建模板文件
    3. 自动推断项目类型,可以根据项目类型自动地为项目添加相关的开发脚本

    对于 lingui 插件来说,只需要用到其中的 executor,在项目的 project.json 中添加 lingui executor,就可以通过 nx lingui 来调用 lingui cli

    借助 nx 提供的 nx-plugin 插件,可以很快的把实现这些功能的基本模板搭建起来。

    实现 Lingui Executor

    Lingui Executor 的内容非常简单,只需要调用 lingui cli 就行了。这里我推荐使用 zx 来执行命令,zx 开箱即用,帮你处理好了调用命令行程序的一切工作,包括参数解析、命令行输出捕获、glob 匹配。唯一需要注意的是,zx 目前只提供 ESM 模块,而 nx 暂时无法调用 ESM 模块插件。为了兼容这两者之间的模块格式,我们的插件只能编译为 CJS 模块,通过 import('zx') 来引用 zx 中的函数。方便起见,我封装了 zx 中最常用的 $ 函数,使之可以自然地在 CJS 中使用:

    import { logger } from '@nrwl/devkit';
    export async function $$(template: TemplateStringsArray, ...args: any) {  
     const { $ } = await import('zx');  
     try {
       // 强制终端彩色输出
       $.env.FORCE_COLOR = '1';  
       // 关闭命令回显
       $.verbose = false;  
       if ($$.cwd) {  
         $.cwd = $$.cwd;  
       }  
       const process = $(template, ...args);  
       process.stdout.on('data', (data) => {  
         logger.info(data.toString());  
       });  
       process.stderr.on('data', (data) => {  
         logger.info(data.toString());  
       });  
       return await process;  
     } catch (e) {  
       logger.error(e.stdout);  
       logger.error(e.stderr);  
       throw e;  
     }  
    }  
      
    $$.cwd = undefined as string | undefined;

    需要注意的是,zx 执行命令的 stdio 必须通过 @nrwl/devkit 输出到终端,否则 nx 的缓存机制将无法取得 executor 执行过程中产生的命令行输出内容。

    export default async function runExecutor(options: ILinguiExecutorSchema, context: ExecutorContext) {    
     if (!options.localesDir) {  
       logger.error('options.localesDir is required');  
       return { success: false };  
     }  
     const localesDirPath = path.resolve(context.root, options.localesDir);  
     if (!context.projectName) {  
       return {  
         success: true,  
       };  
     }  
     const projectCwd = await getProjectCwd(context);  
     $$.cwd = projectCwd;
     try {
       await $$`yarn exec lingui extract --clean`;
     } catch (e) {
       return { success: false };
     } 
     return {  
       success: true,  
     };  
    }

    编译插件

    在运行 nx 命令时,nx 插件会被执行,所以需要在开发者调用任何 nx 命令之前就把本地项目中的插件编译成 nx 可以加载的 CJS 模块。我的做法是给本地插件加上一个 postinstall 脚本,这样在运行 yarn install 的时候,nx 插件就会被自动地编译,不需要手动干预。

    nx 默认生成的模板会使用 TypeScript 将代码编译为 CJS 模块,但 tsc 的编译速度比较慢,测试下来稍微有点影响实际的开发体验,我们可以用 swc 作为 tsc 的替代品,编译速度会快上很多。

    另一个用来编译插件的选项是 esbuild,但 esbuild 的目标并不是替代 tsc,在某些行为上跟 tsc 有不少差异,需要对项目做很多改动,才能适配 nx 执行 executor 的机制。对于 nx 插件来说,executor 跟 generator 都是插件模块的入口(entry),而 esbuild 是设计来将代码打包成一个 Bundle 的。

    最后

    给自己的项目开发 nx 插件是一件非常简单的事情,但是由于 nx 加载插件的限制,需要注意插件的编译方式以及 ESM 的引用方式。

  • 修改 WordPress 主题默认字体

    我很喜欢 WordPress 2022 主题的设计,但是这款主题默认使用的是衬线字体,我看着很不舒服,经过断断续续的折腾,终于把找到了最合适的方法来修改这款主题的默认字体。

    TL;DR

    在不使用第三方插件的前提下,为了让主题的修改在展示页面跟编辑页面同时生效,必须要借助主题文件编辑器。打开编辑器,选中 WordPress 2022 主题,在右边找到 theme.json

    Theme File Editor in Menu
    theme.json

    接着将文件中所有的 var(--wp--preset--font-family--source-serif-pro) 替换为 var(--wp-preset--font-family--system-font) 即可。system-font 默认使用的就是用户操作系统的非衬线字体。

    为什么不用自定义 CSS?自定义 CSS 需要同时覆盖展示页面跟管理页面的样式,而且还需要针对不同的 Block 单独编写样式,太麻烦了,直接编辑主题文件可以在所有看得到的地方起效。

  • 如何重新导出 antd

    因为种种原因,你可能想要将 antd 组件从另一个模块中重新导出,但是这么做并没有像看上去那么简单。

    导出类型

    重新导出 antd 的类型可能是整个需求中最简单的部分,因为我们只需要简单的使用 export type { xxx } from 'antd' 就行了。

    首先我们需要使用 require.resolve 获取 antd 的安装路径,然后用正则表达式解析一下 antd/index.d.ts 的内容,其中以 export type 开头的就是 antd 导出的类型。获取到这些类型名称后,就可以生成代码,在我们自己的模块中重新导出。

    导出组件

    与类型信息不同,想要重新导出组件还需要考虑支持「按需引用」功能。虽然目前的 antd 文档中写着 JS 组件默认支持基于 ESM 按需引用功能,但是组件对应的样式依赖 babel-plugin-import 实现按需引用。

    然而, babel-plugin-import 并不支持 export { xxx } from 'antd' 的语法,它甚至会报错

    考虑到 babel-plugin-import 目前并没有人在积极地维护,而且我个人也比较讨厌这种依赖 babel 魔改代码的手段,我决定迂回地解决这个问题——先导入,再导出。

    使用简单的 import 语句让 babel-plugin-import 正常地工作,然后再将导入的变量重新 export 出去。不过这里涉及到一个问题,如果我在同一个文件中将 antd 所有的组件全部导入的话,由于 babel-plugin-import 的原因,所有组件的样式将在加载这个文件的时候被加载。这样就破坏了按需加载的特性。

    为了避免上述问题,我们得把 babel-plugin-import 的副作用隔离到独立的模块中,以 Button 组件为例:

    import { Button as _Button } from 'antd';
    
    export const Button: typeof _Button = _Button;
    export * from './lib/button';

    如此一来, index.ts 就完全变成用来按需引用子模块的入口,在加载 index.ts 的时候就不会加载所有组件的样式了。

    除此之外,你可能还发现了我在导出组件的时候特意标注了变量的类型,这是因为 TypeScript 要求我们给导出的变量类型起一个稳定的名字,否则编译会报错。