首先开篇依旧是解决之前的遗留问题,这是网友在下月亮有何贵干指出来的一个写框架会遇到的典型的 winPath 问题,也是我第一次维护 umi 所修改的问题,本来在 day12 编写的时候,应该写上去的,但是我给忘记了。
经典的 winPath 问题
在使用 path join 做路径拼接之后,会导致在 window 上的路径错误
随手用 winPath 处理一下。就是如图中这个朋友写的这样子。
packages/malita/src/entry.ts
import { winPath } from '@umijs/utils'; importStr += `import A${count} from '${winPath(route.element)}';\n`; 复制代码
接下里进入今天的主题,我们将多个项目的公共功能提取到框架层,形成开发脚手架。为了提效我们还可以将页面级别的内容整理成模版,用于快速创建相对应的资产,比如快速新建页面、快速新建服务、快速新建Mock等。
生成器的实现是我上手开发的第一个 node 服务,我觉得它可以作为一个新手入门的最佳上手项目。这么适合新手,为什么放到最后实现呢?这里留一个悬念,文末说明。
在没有生成器的时候,你如何在一个项目中新建一个新的路由页面,一般操作就是从另一个页面中复制,然后删除一些这个页面用不到的代码,然后再修改一些里面的内容。我们还是将这个过程分解出来,首先有一个地方存放了我们的模版文件,然后我们传入要修改的几个参数,替换模版文件中的数据,然后将它们写到我们指定的目录下。
其实昨天我们在编写依赖预编译的时候,用到了 copyFileSync
也可以理解为一个最基本的生成器,就是不改任何参数(文件名也是参数)。
fs.copyFileSync(path.join(pkgRoot, 'package.json'), path.join(target, 'package.json')) 复制代码
配置 generate 命令
program.command('generate').alias('g').description('微生成器').action(function(_, options) { const { generate } = require('../lib/generate'); generate(options.args); }); 复制代码
我们就可以使用 malita g 类型 参数
这样的命令,来调用我们的微生成器了。 实现就是从 options.args
取到“类型”和“参数”,然后用 fs
提供的 api 一步一步的实现我们的需求,这种原始的实现方式,我们之前的19天中,已经写了不少了,我这里就不用这种方式实现了。
这里介绍一种更加优雅的方式,是在 umi@4 中实现好的基础生成器,也是现在 umi@4 插件中推荐使用的微生成器最佳实践。
当时的设计灵感其实还是聚焦在“约定式”,因为 umi@4 中会提供很多的微生成器支撑,因此就想着直接约定了模版的地址,然后提供统一的方法进行构建和生产,一些需要用户提供的数据,使用问答式的交互来获取。
使用 @umijs/utils 的 generateFile 来快速完成生成器
import { lodash, generateFile } from '@umijs/utils'; import path from 'path'; import fs from 'fs'; export const generate = async (args: string[]) => { const [type, name] = args; const absSrcPath = path.resolve(process.cwd(), 'src'); const absPagesPath = path.resolve(absSrcPath, 'pages'); if (fs.existsSync(absPagesPath) && type && name) { generateFile({ path: path.join(__dirname, `../templates/${type}`), target: path.join(absPagesPath, name), data: { name: lodash.upperFirst(name), }, }); } } 复制代码
然后我们只要编写模版文件 packages/malita/templates/page/index.tsx.tpl
就好了。 只要关注需要动态替换的参数。
import React from 'react'; import type { FC } from 'react'; interface {{{ name }}}PageProps {} const {{{ name }}}Page: FC<{{{ name }}}PageProps> = () => { return <div>Hello {{{ name }}}</div>; }; export default {{{ name }}}Page; 复制代码
这样我们的生成器编写就只需要聚焦在模版文件的编写上。
如果我们需要一些用户输入或者选择的数据,那么我们可以给 generateFile
传递一个 questions
对象。
const questions = [ { name: 'hi', type: 'text', message: `What's your name?`, } ] as prompts.PromptObject[]; 复制代码
然后修改模版文件
return <div>Hello {{{ hi }}}</div>; 复制代码
执行验证
pnpm g page abc > @examples/app@1.0.0 g /Users/congxiaochen/Documents/malita/examples/app > malita g "page" "abc" ✔ What's your name? … malita Write: index.tsx 复制代码
最终生成 examples/app/src/pages/abc/index.tsx
import React from 'react'; import type { FC } from 'react'; interface AbcPageProps {} const AbcPage: FC<AbcPageProps> = () => { return <div>Hello malita</div>; }; export default AbcPage; 复制代码
你可以用相同的方式实现各种各样的微生成器,比如 umi 中的微生成器列表
感谢阅读,本来微生成器是要放在前面实现的,但是我看掘金上不少读者都是刚刚进入软件这个行业的,说来也巧,今天跟红尘炼心大佬聊天还说到这个。我觉得程序开发最重要的还是编程思维的培养,这一点不管是早期的快速入门还是后期的高P晋升都是非常重要的。
很多时候我们在拿到需求的时候,立马想到的是程序上的实现,要写怎样的代码,用怎样的语法之类的。但其实我们更应该去思考的是这个需求的场景,和未来可以复用的情况,涉及的边界等等。这也是评判新手和高手的一个标准。像我们这个系列的解题思路,就是完全新手向的思维,看到问题解决问题。我在里面预留了很多的问题和边界的缺陷,我期待的是你能够发现它们,我觉得这是一件很有趣的事情,希望这个系列带给你的更多的是思考,而不像我的其他文章一样,仅仅是“告诉你有个什么东西,它怎么用”。