共计 27499 个字符,预计需要花费 69 分钟才能阅读完成。
浏览本文你可能取得:
1. 把握 Node 罕用模块的罕用 API
2. 把握开发基于业务的脚手架的流程
3. 理解 npm 包开发中你可能疏忽的细节
4. 浸透式 create-vant-cli-app 源码解读
5. 更多 …
起因
昨天 两周前一位共事要改某个旧的功能模块,我看内容很少又是用 Vue3
写的,迁徙到我搭建好了的 monorepo
我的项目里刚好适合。于是就让他来试水。
而后就是一顿操作。。各种小问题,幸好都得以解决。
尽管解决了,但集成进来的我的项目会越来越多,就算我不当“客服”,而是间接把初始化步骤写好了,共事也可能口碑载道,毕竟要手动改很多货色。如下图所示。
只是截取了一部分,我最初的论断是这个配置的事件必须搞成自动化。
因而,还是得把这几个月拖拖拉拉仍然没搞进去的“疾速初始化我的项目”的工作给实现。
然而怎么写呢?你让我手写一个 CLI 真写不进去。除非我平时就是不停地造这类轮子。
我始终比拟喜爱Vant
,这次会一边学习源码,一边开发一个基于咱们业务的脚手架。
筹备
咱们来依据 Vant 文档阐明 看看怎么运行脚手架的:通过 yarn create vant-cli-app
疾速创立我的项目,同时又反对手动装置,例如pnpm add @vant/cli -D
。
形式不惟一,而且很生疏,不行,开始晕了。
“简略点,搭建的形式简略点!”
定下第一个小指标:能通过 yarn create ektfe-cli-app
命令装置依赖并运行。
第一个小指标:本地运行 CLI
yarn create 是什么?原理是什么?
先从命令动手,yarn create
是什么?原理是什么?
依据 Yarn 中文文档可知命令格局:
yarn create <starter-kit-package> [<args>]
这个命令其实是简写! 次要帮忙你 同时 做两件事:
- 全局装置
create-<starter-kit-package>
(如果存在就将其更新到最新版) - 运行
package.json
的bin
字段下的可执行文件。并且还会将任何<args>
转发给它
也就是说,yarn create react-app my-app
等价于
$ yarn global add create-react-app
$ create-react-app my-app
留神 1:<starter-kit-package>
就是以 create-
结尾的 npm 包。
(所以 create-cli-app
的create-
是固定的,必须要加!)
留神 2:npm init
也是一样用法,例如:npm init react-app my-app
package.json 参数解读
npm 英文官网文档(但不全,有局部在 TS 文档、Node 文档里都有提及)
看开源我的项目可先看package.json
(明明是README.md
!)。
所以咱们来看 vant-cli
的package.json
。
后记:是的,过后先看vant-cli
。我认为我要开发的是这个。。
有些字段很要害,有些字段不要害。但还是整体理解一下吧。
黑体字是常常遗记但很重要的字段。
name:包名称,公布 npm 包的时候也是这个名称
version:版本号
type: package
下的 .js
被Node
以 cjs
或esm
加载。
main:加载这个 npm 包时的入口文件。
官网文档称:main
字段是一个模块 ID,是程序的次要入口点。即如果你的 npm 包名为 foo
,并且用户装置了它,而后在我的项目里导入例如require("foo")
,那么main
模块里 export
的对象将被返回。
门路是绝对于 npm 包根目录的模块。例如这里 lib/index.ts
就是执行打包后生成的 lib
目录中的index.ts
。当加载这个 npm 包的时候,会执行index.ts
。如果没有指定,默认是根目录的index.js
。
typings: 起初基本查不到这个字段,只看到 types 字段。于是眉头一皱; 计上心来把光标挪动到 typings 字段下面👇意思是“typings”字段与“types”同一个意思,应用哪个都行。
它们都是用于指定 TypeScript
我的项目中导入该 npm 包时应用的类型申明文件(.d.ts 文件)的地位。这个字段能够帮忙 TypeScript
编译器在导入 npm 包时正确地解决类型查看。
在晚期版本的
TypeScript
中,类型申明文件的扩展名是.d.ts
,而typings
字段用于指定该文件的地位。起初,TypeScript 2.0
引入了types
字段,作为typings
字段的替代品。因而,如果你在应用较新版本的TypeScript(2.0 及以上)
,应该应用types
字段。
bin: 命令名到本地文件名的一种映射。它容许在装置 npm 包后将脚本增加到零碎的 PATH 门路中。这些脚本能够是命令行工具或其余可执行文件。
当例如 vant-cli
被全局装置,该文件会被链接到全局 bin
目录,或者创立一个 cmd
去执行 bin
字段里的指定文件,因而它能够按名称运行。
另外,你必须确保 bin
字段援用的文件,以 #!/usr/bin/env node
结尾,否则脚本不会被视为可执行文件。
简略来说,会主动间接执行这个文件,例如 vant-cli 本地装置能够用 pnpm add @vant/cli -D
,指的是在以后工程目录的命令行能够执行bin.js
这个文件。
(在装置时,如果是全局装置,npm 将会应用符号链接把这些文件链接到 prefix/bin,如果是本地装置,会链接到./node_modules/.bin/)
留神:yarn create vant-cli-app
对应的不是 vant-cli
这个包的 name
字段 (@vant/cli) 或者 bin
字段 (vant-cli)。而是create-vant-cli-app
这个 npm 包里的 bin
字段,也就是说 yarn create
对应的是 create-vant-cli-app
我的项目。
辨别 yarn create 和 yarn add
首先, yarn create
和 yarn add
是两回事!
前者是全局装置,会蕴含两个步骤。后者是本地装置,即装置到以后工程目录里。
因为 yarn create
前面参数 <starter-kit-package>
是以“create-”结尾的 npm 包。所以 yarn create vant-cli-app
命令之所以能创立我的项目,实际上是全局装置一个“create-”结尾的 npm 包。即这里的 create-vant-cli-app
!所以你就能在windows
的 C 盘
的Yarn
装置门路里的 bin
目录找到了 create-vant-cli-app
和create-vant-cli-app.cmd
两个文件了!
而后就会主动执行这个 create-vant-cli-app
的入口文件,最初在你执行 yarn create vant-cli-app
命令的目录下帮你初始化一个我的项目。
为什么会主动执行?上文提到过, yarn create
蕴含两个步骤嘛,第一步全局装置,第二步就是执行 bin!
那 yarn add @vant/cli -D
又是怎么回事呢?
其实下面一大段都没提及 vant
的另一个 npm 包即 vant-cli
。@vant/cli
就是 vant-cli
的name
。咱们之前也提到这个就是 npm 包的名称。而 bin
是当你执行这个 npm 包时会执行对应的文件。
咦,刚刚如同说 main
是入口文件呀,那执行的不就是从 main
开始的吗?那当初又说 bin
也是执行的字段。
那到底 bin
和main
本质区别是什么?
辨别 main 和 bin 字段
再次回顾 main
和bin
字段:(当你埋怨“这两个字段刚刚不是说了吗”时,祝贺你,你把握得很牢固)
main
:当用户 install
某个 npm 包后并在代码里引入时,此时会进入 main
字段所指定的文件。
bin
:当全局装置某个 npm 包时,会链接到全局 bin
目录,像 xxx.cmd
就会创立一个 cmd
而后外面就执行这个 bin
字段对应的文件。
因而!! bin
所对应的文件,执行机会是在全局装置或本地装置的时候。而 main
字段所对应的文件,执行机会是在代码中导入的时候。这就是两者最大区别,两者看起来都在执行着什么,但执行的地位、机会都不同。
总算是理清了,集体感觉以上几个字段是最重要的。肯定要辨别分明。心愿不只有我是明天才分清的。
其它参数解读
engines:指定运行你这个包的 Node 版本(有可能有的性能是 Node 的某个版本之后才反对)(如果不指定,就是任何 Node 版本都能够)(也反对指定 npm 版本)
scripts:脚本命令
files:定义公布到 npm 仓库中的包时,应该蕴含哪些文件和目录。这个字段的作用是通知 npm 在公布包的时候只蕴含指定的文件和目录,防止将不必要的文件或目录公布到 npm 上。同时,也会作为依赖项装置到我的项目工程里
keywords:关键字汇合。一个字符串数组。益处:有助于他人通过 npm 搜寻到你的包。(description 同理)
publishConfig:公布 npm 包的拜访权限(public)、公布地址
repository:我的项目地址
bugs:bug 反馈地址
author:作者。只有一个人。另,contributors 是数组
devDependencies:开发环境会用到的依赖
dependencies:生产环境会用到的依赖
有的组件库还会有 module 字段,是打包生成一个 esm 语法的目录,module 字段就指向对应的入口文件。
至此,我(们)终于明确:公布 npm 包时如何指定文件 / 目录,装置 npm 包时执行 npm 包里的哪个文件,导入到我的项目工程后又会执行哪个文件。
如何本地开发 CLI?
执行一下 vant-cli
的dev
脚本命令,它实际上是执行tsc -w
。
已知 tsc
是用来编译 .ts
文件的。默认从当前目录开始编译,而这个也取决于tsconfig.json
。
为什么要用 tsconfig.json?
因为理论开发的我的项目,很少是只有单个文件,当咱们须要编译整个我的项目时,就能够应用 tsconfig.json
文件,将须要应用到的配置都写进 tsconfig.json
文件,这样就不必每次编译都手动输出配置,另外也不便团队合作开发。
而后能够看到 create-vant-cli-app
的tsconfig.json
是这样写的👇
{
"extends": "../../tsconfig",
"compilerOptions": {
"target": "ES2019",
"outDir": "./lib",
"module": "commonjs",
"declaration": true
},
"include": ["src/**/*"]
}
能够看出 outDir
目录是 ./lib
。所以当执行dev
命令时,tsc -w
就会监听 include
字段里所有 .ts
文件,一旦有文件变动,就编译输入到 ./lib
目录。
include 属性作用:指定编译须要编译的文件或目录。
ok,开始初始化我的项目。
初始化本人的 CLI
补充一下,最终生成的模板长这样:
配置 package.json
通过 pnpm init
等命令就能初始化了,再进行一些批改。
联合前文常识,咱们晓得:
main:我的项目里导入时,会进入的入口文件。
bin:执行 yarn create ektfe-cli-app
时,全局装置后,会执行的文件对应门路。
{
"name": "create-ektfe-cli-app",
"version": "0.0.1",
"description": "Create CLI App",
"main": "lib/index.js",
"bin": {"create-ektfe-cli-app": "./lib/index.js"},
"scripts": {"dev": "tsc --watch"},
"author": "gyt95",
"license": "MIT"
}
而 scripts
的dev
:运行我的项目,并 watch
变动,一旦变动就从新编译、输入。输入到哪里呢?这里就要配合 tsconfig.json
了。
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "ES2019",
"outDir": "./lib",
"module": "commonjs",
"declaration": true,
},
"include": ["src/**/*"]
}
而后创立 src
目录,创立index.ts
,输出👇
#!/usr/bin/env node
function init(){console.log(666)
}
init()
报错:无奈在 –isolatedModules 下编译 “index.ts”,因为它被视为全局脚本文件。请增加导入、导出或空的 “export {}” 语句来使它成为模块。ts(1208)
为什么会报错?
当咱们的 tsconfig.json
中的 isolatedModules
设置为 true
时,如果某个 .ts
文件中没有 import
或者 export
时,ts
则认为这个模块不是一个 ES Module
模块,它被认为是一个全局的脚本,这个时候在文件中增加任意一个 import
或者 export
都能够解决这个问题。
所以这里加上 export {}
就不会报错了。
#!/usr/bin/env node
function init(){console.log(666)
}
init()
export {}
为什么要设置 isolatedModules 为 true?
假如有如下两个 ts
文件,咱们在 a.ts
中导出了 Test
接口,在 b.ts
中引入了 a.ts
中的 Test
接口。
而后又在 b.ts
将Test
给导出。
export interface Test {}
import {Test} from './a';
export {Test};
这会造成一个什么问题呢?
如 Babel
对ts
本义时,它会先将 ts
的类型给擦除,也就是 a.ts
空了。然而当碰到 b.ts
文件时,Babel
并不能剖析出 export {Test}
它到底导出的是一个类型还是一个实实在在的 js
办法或者变量,这时候 Babel
抉择保留export
。
然而 a.ts
文件在转换时能够很容易的断定它就导出了一个类型,在转换为 js
时,a.ts
中的内容将被清空,而 b.ts
中导出的 Test
实际上是从 a.ts
中引入的,这时候就会产生报错。
如何解决上述问题?
ts
提供了 import type
或export type
,用来明确示意我引入 / 导出的是一个类型,而不是一个变量或者办法,应用 import type
引入的类型,将在转换为 js
时被擦除掉。
import {Test} from './a';
export type {Test};
其实就是基于“TS 的导入省略能力会检测如果导入类型就会在编译后被擦除”这一个性上,擦除后导致 JS
找不到这个类型了,于是报错。所以,如果设置为 true
就不会在非 tsc
编译器有这个状况,如果是 false
,就要你本人加import type
才行(3.8 新出的个性)
之前其实有理解过这个知识点。TypeScript 5.0 新个性 --verbatimModuleSyntax
要求你明确写类型导入,才会保留。否则,就会擦除。任何没有 type
修饰符的导入或导出,都会被保留。而任何应用 type
修饰符的内容,都会被删除。
即:“所有非仅类型导入 / 导出都会被保留,而仅类型导入 / 导出都会被移除。”
以前还有其它属性:
--importsNotUsedAsValues
:用于确认类型导入的应用(仅类型导入须要被显式标记,而未被应用的值导入依然将会保留)
--preserveValueImports
:用于显式防止局部导入语句的移除(所有值导入都将被残缺保留,防止 TypeScript 无奈检测其应用形式的状况)
因为以前当同时开启 preserveValueImports
和isolatedModules
配置时,isolatedModules
会让引入的类型必须是 type-only
。所以来自同一个文件的数据必须得分两条import
引入。
import {someFunc, BaseType} from "./some-module.js";
// ^^^^^^^^
// Error: 'BaseType' is a type and must be imported using a type-only import
// 除非
import type {BaseType} from "./some-module.js";
import {someFunc} from "./some-module.js"
而 TypeScript 4.5
容许一个 type
修饰词在 import
语句中👇
import {someFunc, type BaseType} from "./some-module.js";
isolatedModules
是为了防止非 tsc
的编译器编译时呈现类型被擦除但 import
的类型仍存在导致报错的问题。所以开启这个就为了平安编译,必须是模块隔离的。模块隔离,指的是导入和导出都是确定的,不是不置可否的。是类型就必须申明是类型。
但很多开源库都不开启这个,不太了解。。就正如我临时不了解为什么有 tsc
还须要用 Babel
编译 ts
代码?
小结:如果设置 isolatedModules
为true
,那么新建的 .ts
文件肯定要蕴含一个 import
或者export
。
执行 dev 命令运行 CLI
执行 dev
脚本后,tsc --watch
后生成 lib
目录
如何本地调试?
以前个别咱们都用 npm link
来调试组件库。当初用 yarn
应该也一样,看了下 Yarn 官网文档。是用的 yarn link
。留神如果yarn unlink
就失去了全局链接,此时再 yarn create
就会去线上 yarnpkg
查找。
pnpm 也有pnpm link
,具体可见 Pnpm 官网文档。
pnpm link <dir>
从执行此命令的门路或通过 <dir> 指定的文件夹,链接 package 到 node_modules 中。pnpm link --global
从执行此命令的门路或通过 <dir> 选项指定的文件夹,链接 package 到全局的 node_modules 中,所以使其能够被另一个应用 pnpm link --global <pkg> 的 package 援用。pnpm link --global <pkg>
将指定的包(<pkg>)从全局 node_modules 链接到 package 的 node_modules,从该 package 中执行或通过 --dir 选项指定。
因为我没有把 pnpm 退出到 PATH,所以应用 全局 link 就报错了👇
The configured global bin directory "C:\Users\xxxx\AppData\Local\pnpm" is not in PATH
有 yarn create
就行,目前为止,咱们终于实现了第一个小指标!
第二个小指标:减少询问
内置一个模板 & 围绕业务提供几个问题。
次要用到的库
已知从 index.ts
执行。看看 create-vant-cli-app
的代码
#!/usr/bin/env node
import consola from 'consola';
import {prompt} from 'inquirer';
import {ensureDir} from 'fs-extra';
import {VanGenerator} from './generator';
const PROMPTS = [
{
type: 'input',
name: 'name',
message: 'Your package name',
},
];
async function run() {const { name} = await prompt(PROMPTS);
try {await ensureDir(name);
const generator = new VanGenerator(name);
await generator.run();} catch (e) {consola.error(e);
}
}
run();
看到consola
,这看起来就是用于控制台打印输出的。
consola
https://github.com/unjs/consola
官网形容:用于 Node.js
和浏览器的优雅控制台记录器。
pnpm add consola
,用起来很简略。
const consola = require('consola')
// See types section for all available types
consola.success('Built!')
consola.info('Reporter: Some info')
consola.error(new Error('Foo'))
控制台展现:
下一个是inquirer
。
inquirer
https://github.com/SBoudrias/Inquirer.js
pnpm add inquirer
根本用法:传入一个“问题”数组给 inquirer
的prompt
函数。并异步获取后果。获取到的后果就是用户抉择的选项。
代码如下:
import {prompt} from 'inquirer';
const PROMPTS = [
{
type: 'input',
name: 'name',
message: 'Your package name',
},
];
async function run() {const { name} = await prompt(PROMPTS);
}
run();
当调用 prompt
函数后,如果失常调用,则会再调用 ensureDir
函数。这个函数来自另一个库fs-extra
。
fs-extra
https://github.com/jprichardson/node-fs-extra
官网形容:fs-extra
增加了原生 fs
模块中未蕴含的文件系统的办法,并为 fs
办法增加了 promise
反对。它还应用 graceful-fs
来避免 EMFILE
谬误。应该是 fs
的替代品。
那么 create-vant-cli-app
源码中的 await ensureDir(name)
意思是确保 name 这个目录存在。如果目录构造不存在,则创立它。而后通过new Template(name)
创立一个模板实例。并通过 run()
执行。
写了个简略逻辑:
#!/usr/bin/env node
import consola from 'consola'
import inquirer from 'inquirer'
const PROMPTS = [
{
type: 'input',
name: 'name',
message: '你须要创立的项目名称叫什么?'
},
{
type: 'list',
name: 'type',
choices: ['nsft', 'say', '其它'],
message: '以后我的项目属于哪个平台的?'
},
]
async function run (){const result1 = await inquirer.prompt(PROMPTS)
console.log(result1);
try {} catch (e) {consola.error(e)
}
}
run()
而后执行 dev
命令再 yarn link
命令。
而后到指标我的项目中执行yarn create ektfe-cli-app
。后果报错。
报错:Instead change the require of inquirer.js in C:\xxxx\xxx\xxx\index.js to a dynamic import() which is available in all CommonJS modules.
意思是 inquirer.js
的require
要更改为在所有 cjs
模块中都可用的动静import()
代码明明写的是 import
。那肯定是因为tsconfig.json
。于是把"module": "commonjs"
正文掉。
但又提醒:
不能在模块外应用 import 语句?
查资料找到相应解决办法:https://bobbyhadz.com/blog/javascript-syntaxerror-cannot-use-…
即要在 package.json
中增加 type: "module"
。回顾上文可知,字段意思是以后package
下的 .js
被Node.js
以 cjs
或esm
加载。
以后流程:
1. 咱们写的 .ts
文件会通过 tsc
编译输入到 lib
目录,例如 index.ts
会编译为 ./lib/index.js
,而后通过yarn link
进行全局链接。
2. 在另一个我的项目里咱们通过 yarn create ektfe-cli-app
全局装置和执行 CLI。此时所执行的是 bin
字段对应的文件./lib/index.js
。
3. 因为是在 Node
环境执行的,但此时咱们用的语法是 esm
的import
,所以咱们必须通过 type: "module"
通知 Node
以后 npm 包里的 .js
都会作为 es
模块加载。
你也能够把鼠标挪动到 package.json 的 type 字段上,会有以下提醒:
当设置为“module”时,type 字段容许包指定所有 .js 文件都是 ES 模块。如果“类型”字段被省略或设置为“commonjs”,则所有 .js 文件都被视为 CommonJS。
而最难堪的是一开始我就是想间接用默认的 cjs
模块标准。后果 inquirer
又要我用 esm
语法import
。
怎么会这样?!
关上 inquirer
的 npm 包网址(Github 也行),全局搜寻“commonjs”。
Inquirer v9 and higher are native esm modules, this mean you cannot use the commonjs syntax require(‘inquirer’) anymore. If you want to learn more about using native esm in Node, I’d recommend reading the following guide. Alternatively, you can rely on an older version until you’re ready to upgrade your environment:
npm install --save inquirer@^8.0.0
意思是 inquirer
版本如果是 9 或以上,那么就是原生 esm
模块。意味着你不能再用 cjs
语法require('inquirer')
。如果你想学习更多对于在 Node 中应用原生 esm 的常识,我将举荐浏览这个指南,或者你能够用旧版本,直到你筹备好降级你环境为止。
那怎么 create-vant-cli-app
又没问题?
去看它的 package.json
,inquirer
版本是 8+。
(因为我之前拉取 Vant
的源码到本地,也没同步后续代码,所以还是用的 inquirer
。起初我又到Github
再看,发现曾经改为 enquirer
了。基本上就是对 inquier
的从新实现)
也就是说,以后我用的 inquirer
版本是 9,只能是 package.json
指定 type
为module
。
构建询问流程,生成模板我的项目
制订一个根本流程👇(属于草稿版,开发过程中不断完善,所以有出入)
输出项目名称?> 数据图表
输出 html
名称?> data-charts
抉择:所属平台?> nsft
其它:输出具体名称?
(如果输出非 a -zA- Z 则提醒要输出英文单词)
(默认全转小写,而且不能和下面的同名,否则正告)
外部主动调配端口号,要改data.json
外部主动创立 module
目录,创立 data-charts.html
,外部title
增加“数据图表”
外部主动创立 src
目录,对于 App.vue
要进行批改
外部创立package.json
,批改对应的name
…
阶段 1:询问后创立目录
我心愿用户抉择了问题 3 的“其它”时,会提供一个输入框让用户输出。
实现形式比较简单粗犷。代码如下:
#!/usr/bin/env node
import consola from 'consola'
import inquirer from 'inquirer'
import fs from 'fs-extra'
async function run() {const { name, htmlName} = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: '你须要创立的项目名称叫什么?'
},
{
type: 'input',
name: 'htmlName',
message: '新我的项目对应的 html 名称叫什么?'
}])
if (!name || !htmlName) return;
const {type} = await inquirer.prompt([
{
type: 'list',
name: 'type',
choices: [{ name: 'nsft', value: 'nsft'},
{name: 'SAY', value: 'say'},
{name: '其它', value: 'others'},
],
message: '以后我的项目属于哪个平台的?'
}])
if (!type) return;
if (type === 'others') {const { newPlatformName} = await inquirer.prompt([
{
type: 'input',
name: 'newPlatformName',
message: '新的平台叫什么?'
}])
if (!newPlatformName) return;
console.log(`\n 正在为你创立新的平台 ${newPlatformName} 目录...`)
fs.ensureDir(newPlatformName)
}
}
run().catch(e => {consola.error(e)
})
来看看成果:
对应目录下胜利创立了平台。
这只是第一步。
优化 1:以后用户如果抉择的是已有平台,那么行将创立的目录会到对应平台。否则,会让用户本人输出一个,而后主动创立。
优化 2:判断以后根目录的 data.json
中的 PORT
列表,是否蕴含这个htmlName
,如果是,则提醒有重名,须要用户从新输出。否,则持续。
因而,针对优化 1,流程要做批改。应该是先抉择平台,检测后,再输出 htmlName
值。
而后就呈现一些细节问题。
如何检测重名?
通过 require('./data.json')
拿到 json
数据,再通过 PORT[type][htmlName]
检测对应零碎中是否蕴含同名的版块,是,则创立失败。否,则开始生成我的项目。
if(type !== 'others'){
try {let json = fs.readFileSync('data.json', 'utf-8') // 间接用 fs.readFileSync
const {PORT} = JSON.parse(json);
if(PORT[type][htmlName]){consola.error(` 创立失败!以后所抉择的 ${type} 平台存在同名的 html 名称 `)
}
} catch (error) {consola.error('当前目录下找不到 data.json')
}
}
// 开始生成我的项目
// ...
留神:fs.readFileSync
办法获取的值默认是 buffer
类型,所以传第二个参数 utf-8
能力返回咱们要的数据。
针对优化 2,又有细节问题。
因为不同平台属于不同工作区。那以后 PORT
是没有划分工作区,即所有平台的子项目的端口号都混同在一个对象里。前期保护治理都不不便。所以这里我把 PORT
再次做了细分。
除了细分,还增加了一段逻辑:遍历 PORT
所有 key
,找到合乎以后用户抉择的平台,如果不存在,间接创立,并且端口号依照规定自增 1000。如果已存在,则找到对应的key
的最初一个子的 key
,拿到对应的端口号,并再按规定自增。这里波及到fs
模块的 readFile
和writeFile
。
留神:最初 writeFile
时,要用 JSON.stringify
转换成字符串模式。
阶段 2:外部主动生成 html
难点:html 是主动生成、名字取决于用户输出的 htmlName
值,外部模板固定,但要嵌入用户输出的项目名称。
__dirname
间接应用发现报错:__dirname is not defined in ES module scope。
以前 __dirname
在Node
脚本中非常罕用,因为能够获取以后 JavaScript
文件所在文件夹的门路。
然而现在! 如果在 es
模块下就无奈间接应用 __dirname
了。(上文提到 package.json
里因为 inquirer.js
9.x 的起因而被迫写"type": "module"
,即采纳esm
语法。
解决办法 :须要从原生Node
模块 url
模块导入 Node
的url
和 fileURLToPath
函数,而后能够通过以下形式本人搞一个和 __dirname
作用一样的值。
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
参考:fix-dirname-not-defined-es-module-scope – flaviocopes.com
尽管晓得能够这么做,但我不晓得这两个函数有什么用啊!连忙去官网文档看一下。
fileURLToPath
作用:把 文件 url 转换为 本地文件门路。
像 import.meta.url
是以后 vant-cli-app
的bin/index.js
这个文件的绝对路径。
console.log(import.meta.url);
const __filename = fileURLToPath(import.meta.url)
console.log(__filename);
// file:///C:/disk_D/packages/create-ektfe-cli-app/lib/index.js
// C:\disk_D\packages\create-ektfe-cli-app\lib\index.js
奇怪,怎么就会有 file:///
呢?
file:/// 是什么?
file:///
是一个 URL 协定,用于示意本地文件系统上的文件门路。在 Node.js
中,当咱们读取本地文件时,文件门路通常是以 file:///
结尾的 URL。
file:// 和 file:/// 有什么区别?
file://
和 file:///
都是用于示意本地文件门路的 URL 协定,其中 file:///
是file://
协定的扩大,用于更严格地示意文件门路。
file://
是一种通用的文件 URL 协定,能够用于示意不同操作系统上的文件门路。例如:
- 在
Windows
中,文件门路可能是file:///C:/path/to/file
; - 在
macOS
中,文件门路可能是file:///Users/user/Documents/example.txt
。
在这种状况下,咱们能够应用file://
URL 协定来示意文件门路,它在不同操作系统上都能够应用。
然而,在某些状况下,file://
协定可能会有一些问题。例如,当文件门路中蕴含空格或非 ASCII 字符时,file://
协定可能无奈正确地解析文件门路。为了解决这个问题,file:///
协定引入了更严格的语法来示意文件门路,以确保它们能够被正确地解析。
具体来说,file:///
协定要求文件门路必须满足以下要求:
- 文件门路必须是绝对路径。
- 文件门路中的空格和非 ASCII 字符必须进行 URL 编码,例如空格应该被编码为
%20
。
因而,当咱们应用 file:///
协定来示意文件门路时,能够更加严格地保障文件门路的正确性。
但我依然无奈了解像 file:///Users/user/Documents/example.txt
这样的 URL,它的三道杠 ///
是怎么解析的?
解析 file:/// 结尾的文件门路
在 Node.js
中,能够应用内置的 url
模块来解析 URL。针对 file:///Users/user/Documents/example.txt
这个 URL,能够应用 url
模块的 parse
函数来解析该 URL,并获取其中的各个局部。具体代码如下:
const url = require('url');
const fileUrl = 'file:///Users/user/Documents/example.txt';
const parsedUrl = url.parse(fileUrl);
console.log(parsedUrl.protocol); // 'file:'
console.log(parsedUrl.pathname); // '/Users/user/Documents/example.txt'
在下面的代码中,咱们首先引入了 url
模块,并定义了一个 fileUrl
变量,它蕴含要解析的 URL。而后,咱们应用 url.parse
函数将 fileUrl
解析为一个 URL 对象。解析后,咱们能够通过拜访 protocol
和pathname
属性来获取 URL 的协定和门路信息。
留神:在解析 file:///
协定的 URL 时,url.parse
函数会将 file:///
协定解析为 file:
,并将/
视为门路的一部分。因而,在解析后的 URL 对象中,门路信息存储在 pathname
属性中,且门路前会主动增加一个 /
符号。
即 file:
后的 ///
会拆分为 //
和/
, /
会变成门路的一部分。所以在 pathname
里前缀增加 /
符号。
解析 file:// 结尾的文件门路
与 file:///
协定不同,file://
协定不要求文件门路必须是绝对路径,并且不须要对空格和非 ASCII 字符进行 URL 编码。因而,在解析 file://
协定的 URL 时,咱们须要针对具体的 URL 标准进行解析。
一种常见的解析办法是,应用正则表达式来匹配 URL 中的各个局部。
const fileUrl = 'file://Users/user/Documents/example.txt';
const parsedUrl = fileUrl.match(/^file://([^/]+)(/.*)?$/);
console.log(parsedUrl[1]); // 'Users'
console.log(parsedUrl[2]); // '/user/Documents/example.txt'
正则表达式包含两个分组,别离用于匹配主机名和文件门路。
^
示意字符串开始。
file://
示意 file:// 字符串。
([^/]+)
,^
指的是“非”,+
指的是至多 1 个。所以这里示意多个非 /
的字符。括号示意一个捕捉组。
(/.*)?
,/
指的是 /
,联合起来就是/.*
,例如下面的example.txt
。一个可选的捕捉组,匹配一个以 / 结尾,前面跟着任意字符的字符串,括号示意一个捕捉组,?
示意该组为可选,即字符串中可能不存在该局部
在正则表达式中,.
示意匹配任何单个字符,而 *
示意匹配前一个字符 0 次或屡次。所以,.*
组合在一起示意匹配任意数量的任何字符(包含 0 个字符),直到遇到下一个匹配规定或者字符串的结尾。
那么问题来了:fileURLToPath(import.meta.url)
中的 import.meta.url
是啥?
import.meta.url
首先,import.meta
是一个在 es
模块外部可间接应用的对象。它蕴含的是对于模块运行环境的信息。运行环境能够是浏览器,也能够是Node
。
其次,import.meta
对象是可扩大的,宿主(浏览器 /Node
)能够把任何有用的信息写进去。
因而,浏览器和 Node
都给 import.meta
写入url
。
所以 import.meta.url
就是你运行那个文件的绝对路径,然而只是个url
。
个别会通过 fileURLToPath
这个函数,进行转换。
file:///C:/xxx/xxx/bin/index.js
👉C:\xxx\xxx\bin\index.js
而后像 vite
源码的 create-vite
中,src/index.ts
里有一行 path.resolve(filename, '../..', template-${template})
,要拿到某个template
模板目录进行拼接。(src
和 template-xx
是同级的)
通过 fs.readdirSync(templateDir)
读取目录。失去的 files
,遍历,通过write
函数写入。
write
函数外部,会通过 path.join
拼接 root
和file
。root
是 path.join(cwd, targetDir)
,而cwd
就是process.cwd()
。
process.cwd()
官网文档形容:返回 Node.js 过程的当前工作目录。
就是你执行 yarn create vant-cli-app
的当前目录的绝对路径。
啊!这个才是我想要的!就是想要获取以后所在的目录。
奇怪!那上文的 __dirname 是?
它是以后正在执行的文件的目录的绝对路径。也就是说,以后我执行的 yarn create vant-cli-app
,对应执行的是bin
字段指向的文件,而这个文件的绝对路径,才是 __dirname
的值!
这里的 __dirname
是通过 path.dirname
接管 __filename
来失去的。
path.dirname
官网形容:获取传入的文件的父目录。
联合下图应该更明确了!
辨别 process.cwd 和 __dirname
process.cwd:以后你 执行命令所在的目录 的绝对路径
__dirname:以后 正在执行的文件所在目录 的绝对路径
ok!回到下面 create-vite
的path.join(cwd, targetDir)
。
那 targetDir
是啥?
在 create-vite/src/index.ts
的 init 函数里有个 getProjectName
函数,内容是判断 targetDir
是否为 ‘.’,是,path.basename(path.resolve())
,否,间接用
首先,我尝试在以后门路 C:\disk_D\gyt95
执行命令输入看看
console.log(path.resolve()); // C:\disk_D\gyt95
console.log(path.basename(path.resolve())); // gyt95
找官网文档看看形容。
path.basename()
官网形容:返回门路的最初一部分,相似于 Unix
的basename
命令。疏忽尾随目录分隔符。而且辨别大小写。
path.win32.basename('C:\foo.html', '.html');
// Returns: 'foo'
path.win32.basename('C:\foo.HTML', '.html');
// Returns: 'foo.HTML'
所以,下面的 path.basename(path.resolve())
就返回最初一部分,即gyt95
。
那么,path.resolve()
是什么?
path.resolve()
官网形容:将一系列门路或门路段解析为绝对路径。如果没有传递门路段,path.resolve()
将返回当前工作目录的绝对路径。
因而,path.resolve()
返回的就是当前工作目录的绝对路径,即C:\disk_D\gyt95
。
晓得了获取当前目录门路后,就要进入对应平台名称的目录里创立 htmlName
,但不须要真的进行“进入”的操作,因为write
自身就有“进入而后把数据写进去”的意思。(刚开始还始终找有什么函数能够进入某个目录)
但执行报错,因为不能用 cjs
格局的 require
,我写的是require('data.json')
解决办法:用 Node
的fs
模块读取以后是否存在这个文件。
依据教训,是有 fs.exist
的。查了下文档,发现原来 Node
的v16.19.1
曾经弃用,倡议改用 fs.stat
或者fs.access
。
以后,咱们要在 htmlName
的目录下创立一个 module
空目录,已知 mkdir
创立目录。如何实现?
其实超级简略!!你直接判断这个门路,fs.ensureDir
办法能直接判断你这个门路是否存在,如果不存在,能间接帮你创立这整个门路下的目录。。
创立结束后,我间接利用 fs.writeFile
创立 html 文件!
实现 template 动静生成!
const moduleTemplate = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${name}</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
`
// ensureDir 能够判断以后平台是否存在 htmlName 是否存在 module 目录
const modulePath = `${platformName}/${htmlName}/module`
try {await fs.ensureDir(modulePath)
await fs.writeFile(`${modulePath}/${htmlName}.html`, moduleTemplate, 'utf-8')
} catch (error) {console.log(error)
}
看看成果
进阶 1:动静生成平台选项
目前,提供的平台选项是写死的。
咱们心愿:平台选项是依据当前目录所有非 packages
等非凡目录而主动生成的。用一个数组存起来。
如果数组为空,就提醒找不到任何平台,请输出一个。
如果数组不为空,就展现给用户进行抉择。
此时用户感觉都不是以上的平台,那么就抉择最初一个选项“以上都不是”,就会提醒请自行输出一个。
首先,如何获取以后执行 node 的门路下所有目录的名称?
能够通过 fs.readdirSync()
办法读取以后门路下的所有目录和文件,而后应用 path
模块中的 path.join()
办法来将以后门路和目录名称拼接成一个残缺的门路,最初应用 fs.stat()
办法判断这个门路是否是一个目录。如果是目录,就将它的名称存储到一个数组中。
fs.readdirSync
官网形容:同步形式读取指定目录下所有文件和子目录名称。
但以上的思路有个问题:如果遇到 .
结尾的文件,就会报错。能够通过给 fs.readdirSync
设置 withFileType:true
获取所有目录和文件的信息,以下是代码和区别
const currentNoInfoDirFiles = fs.readdirSync(cwd)
console.log(currentNoInfoDirFiles);
const currentDirFiles = fs.readdirSync(cwd,{ withFileTypes: true})
currentDirFiles.forEach(v => console.log(v, v.isDirectory()));
能够看到只管能通过 fs.isDirectory()
判断是否为目录,但依然有一些 .
结尾的目录,须要过滤。并且,还有些非凡我的项目要过滤。综上,代码最初如下:
const BAN = ['packages', 'dist', 'node_modules'] // 这里看你须要自行添加,我的非凡目录就这些
currentDirFiles.forEach(file => {if(file.isDirectory() && !file.name.startsWith('.') && !BAN.includes(file.name)){
currentPlatforms.push({
name: file.name,
value: file.name
})
}
})
currentPlatforms.push({name: '其它', value: 'others'})
const {type} = await inquirer.prompt([
{
type: 'list',
name: 'type',
choices: currentPlatforms,
message: '以后我的项目属于哪个平台的?',
},
])
当输出新的名字时,咱们再做一些解决,防止用户成心写非凡目录的名字。
if (type === 'others') {const { newType} = await inquirer.prompt([
{
type: 'input',
name: 'newType',
message: '新平台的英文名称叫什么?',
},
])
if (!newType) return
fs.ensureDir(newType.toLowerCase())
if(BAN.includes(newType)){consola.error('不能填写以下名称:packages, dist, node_modules')
return;
}else if(currentPlatforms.includes(newType)){consola.error('这个名字曾经存在!重来!')
return;
}
consola.info(` 正在为你创立新的平台目录 ${newType}...`)
consola.info(` 为了对立命名标准,会主动转为小写...`)
platformName = newType
} else {platformName = type}
进阶 2:提取 template
上文看到咱们间接定义变量moduleTemplate
。然而更好的方法能够写template.html.tpl
。
本来我也想过这个方法,然而不明确怎么去自定义 template
的 html 名称,不晓得怎么改。
(不明确就对了,不明确就去理解它!)
直到起初我看了下 Vant
的实现形式,因为 create-vant-cli-app
里的模板蕴含package.json.tpl
。
因而,咱们要从 create-vite
回来,看回 create-vant-cli-app
的实现思路。过程中波及一系列 Node
的模块的API
,会逐个形容。
次要是 writing
函数和 copyTpl
函数,实现原理是这样的:
- 首先拿到 templatePath,具体实现:
// this.inputs.vueVersion 的值是用户抉择选项 Vue2/Vue3 传入的,对应的值为 vue2/vue3
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
GENERATOR_DIR = path.join(__dirname, '../generators');
const templatePath = path.join(GENERATOR_DIR, this.inputs.vueVersion).replace(
/\/g,
'/'
);
path.join
作用就是用特定于平台的分隔符将所有传入的门路段落拼接在一起。
(纯正拼接。如果参数 1 是 ..
,拼接 ../generators
就变成 ....\generators
,如果是 __dirname 即执行文件的目录的绝对路径例如 C:\disk_D
,拼接 ../generators
后就会变成C:\disk_D\generators
)
这里还有个关键点! 尽管咱们在代码里写的是 /
,但实际上 Windows
上打印进去的是 Windows
本人的分隔符`,也就是说,会主动本义。那上文能不能不应用
replace` 本义呢?
原本认为不行,但其实是能够的。因为 path.join
会用对应平台特定的分隔符来拼接。但像 create-vant-cli-app
这里增加了 replace
是因为之前有 issue 说 Windows10
零碎下执行 yarn create vant-cli-app
后只呈现一个node_modules
。所以还是倡议replace
一下。
依据上文提到的 __dirname
作用可知,实际上对应的是 lib/index.js
,那么 ../generators
,刚好就是和 lib
目录同级的 generators
。
这里用 replace
办法把 \
都改为 /
。(留神:这里\\
第一个 \
是JS
的转义字符,下文立即会说)
因而,用户抉择 Vue3
时,获取门路是to/path/generators/vue3
。而 to/path
是绝对路径。
能够用 fileURLToPath
解决。
门路分隔符 \ 和 / 的区别?
都是门路分隔符。
在 Windows
操作系统中,门路分隔符通常是反斜杠(\),而在 Unix
和 类 Unix
操作系统(如 Linux
和 macOS
)中,门路分隔符通常是正斜杠(/)。
在 JavaScript
中,反斜杠也能够用作转义字符。因而,如果你在 Windows
操作系统上编写JavaScript
代码并应用反斜杠作为门路分隔符,那么在某些状况下可能须要将门路中的反斜杠字符替换为斜杠字符,以确保正确解析门路。
- 接着拿到 templateFiles,具体实现:
const templateFiles = glob.sync(join(templatePath, '**', '*').replace(/\/g, '/'),
{dot: true,}
);
方才失去了 vue3
模板的绝对路径,当初通过 path.join
拼接出 to/path/generators/vue3/**/*
门路(是的,path.join
会主动加分隔符)
这里提到了 glob
。用的是 fast-glob
这个库,那么 glob 是什么?
glob
通过星号等 shell
所用的模式匹配文件。它个别被用来查找指定目录下的所有文件和子目录 glob.sync
只是同步形式。
晚期 Unix
(第 1-6 版,1969-1975)的命令行解释器依赖独立程序/etc/glob
开展参数中的通配符。这个程序会开展通配符并把开展后的文件列表传给命令。它的名字是 “global command” 的简称。起初这个性能由工具函数 glob() 提供,被shell
等程序应用。(译自 WikiPedia%23Origin))
(再次和 Unix
相干的知识点分割上)
其实就像这样的模式:用在命令行中的ls *.js
,用在 .gitignore
文件中的 build/*
。
所有用到 * 的门路能够用 glob 进行匹配。
这里有个参数 {dot: true}
,它是glob
库中的一个选项,用于匹配暗藏文件(以 . 结尾的文件)。如果将 dot 设置为 true,则 glob.sync()
将会匹配暗藏文件,否则将会疏忽这些文件。(而这里因为 create-vant-cli-app
的vue2/vue3
模板里都有 .gitignore
.eslintignore
这样的以点号 .
结尾的文件,所以要带上这个属性)
在下面的代码中,dot: true
示意匹配所有文件,包含暗藏文件。
- 遍历 templateFiles
templateFiles.forEach((filePath) => {
const outputPath = filePath
.replace('.tpl', '')
.replace(templatePath, this.outputDir);
this.copyTpl(filePath, outputPath, this.inputs);
});
每个门路都把 .tpl
字眼 replace
为空字符,再替换 templatePath
为输入目录。而后调用 copyTpl
办法,传入 3 个值:filePath
每个模板文件门路,outputPath
输入门路,this.inputs
用户输出的所有信息汇合。最初这个 this.inputs
是copyTpl
办法要依据用户输出的信息,对 .tpl
文件进行内容动静批改。
为什么要替换为输入门路?
目标是将模板文件的门路替换为在新我的项目中的相应门路。这是为了确保生成的文件被正确地搁置在新我的项目的相应地位上,并防止任何文件名抵触。
看下正文版
// 已知:outputDir 是 C:\aaa\username\projectName
// 已知:templatePath 是 C:\xxx\vant\create-vant-cli-app\generators\vue3
templateFiles.forEach((filePath) => {
// filePath 是 C:\xxx\vant\create-vant-cli-app\generators\vue3\package.json.tpl
const outputPath = filePath
.replace('.tpl', '')
// 变成:C:\xxx\vant\create-vant-cli-app\generators\vue3\package.json
.replace(templatePath, this.outputDir);
// 变成:C:\aaa\username\projectName\package.json'
this.copyTpl(filePath, outputPath, this.inputs);
});
- 执行 copyTpl 办法
已知,每个模板文件都会调用一次这个办法。那么以 create-vant-cli-app
为例,看看外部实现:
- 通过
fs.copySync
把源门路文件,复制到,指标门路。即输入的目录里 - 通过
fs.readFileSync
读取输入门路下文件内容 - 遍历用户输出的汇合
this.inputs
,通过正则表达式,查找是否有对应的模板语法 <%= ${key} %>,有,替换为 key 为 name 所对应的 value - 遍历结束就通过 fs.writeFileSync 把新的内容写入到输入文件里
function copyTpl(from: string, to: string, outputDir: string, args: Inputs) {
// 4-5-1 复制文件
fs.copySync(from, to)
// 4-5-2 读取文件
let content = fs.readFileSync(from, 'utf-8') // utf-8 是为了获取非 buffer 类型数据
// 4-5-2 遍历,替换掉模板语法
Object.keys(args).forEach(key => {
// 在正则表达式中,'g' 代表全局匹配模式,示意匹配字符串中所有符合条件的子串
const reg = new RegExp(`<%= ${key} %>`, 'g')
content = content.replace(reg, args[key as keyof Inputs])
})
// 4-5-3 写回输入目录的对应文件
fs.writeFileSync(to, content)
// 4-5-4 动静改名
if (path.basename(to) === 'template.html') {const newToPath = to.replace('template.html', `${args.htmlName}.html`)
fs.renameSync(to, newToPath)
}
// 4-5-5 提醒胜利
// 把指标门路的后面局部和平台分隔符去掉,剩下的就是文件名了
const name = to.replace(outputDir + path.sep, '')
consola.success(`${color.green('创立')} ${name}`)
}
留神:第 11 行有一个g
,不要误以为是字符了。。这在正则表达式里示意开启全局匹配模式。
fs.writeFileSync
当 file 是文件名时,同步将数据写入文件,如果文件已存在则替换该文件。数据能够是字符串或缓冲区。
当 file 是文件描述符时,其行为相似于间接调用 fs.write()
(举荐)
参数介绍:
- path:要写入的文件的门路(必须参数)。
- data:要写入到文件中的数据(必须参数)。
- options:一个可选的选项对象,用于指定文件的编码、文件模式、文件权限等(可选参数)。
- encoding:一个可选的编码字符串,用于指定写入文件时应用的编码格局(可选参数)。如果省略此参数,则默认应用 UTF- 8 编码。
这里蕴含动静批改 template.html.tpl 名称。(create-vant-cli-app
没有这一步)
因为模板的名称叫 template.html.tpl
,但具体我的项目的 html 应该是对应projectName
的。
这里能够用 fs
模块的一个办法renameSync
。
fs.renameSync
官网形容:将文件从 oldPath 重命名为 newPath。返回 undefined。
if(path.basename(to) === 'template.html'){const newToPath = to.replace('template.html', `${htmlName}.html`)
fs.renameSync(to, newToPath)
}
到了这里,才实现读取 - 遍历 - 写入,3 个步骤。
- 附加写入胜利提醒,这里用到了 path.sep
path.sep
官网形容:提供特定于平台的门路段分隔符。例如 Windows
下是这样的:
'foo\bar\baz'.split(path.sep);
// Returns: ['foo', 'bar', 'baz']
在 Windows
上,正斜杠 (/) 和反斜杠 () 都被承受为门路段分隔符;然而,门路办法只增加反斜杠 ()。
以下是具体代码👇
copyTpl(from: string, to: string, args: Record<string, any>) {fs.copySync(from, to);
let content = fs.readFileSync(to, 'utf-8');
Object.keys(args).forEach((key) => {const regexp = new RegExp(`<%= ${key} %>`, 'g');
content = content.replace(regexp, args[key]);
});
fs.writeFileSync(to, content);
const name = to.replace(this.outputDir + sep, '');
consola.success(`${color.green('create')} ${name}`);
}
小结:copyTpl
办法作用是复制模板里的文件到指定目录。其中蕴含相似于模板引擎的语法替换。把 .tpl
文件里的 <$= key $>
替换为对应的值。
有了下面的步骤,间接就生成剩下的文件了,包含package.json
。
阶段 3:外部主动生成目录及文件
其实就是下面的源码解析实现一次。
第三个小指标:制作欢送界面
这部分没有点创意是不行的。让 ChatGPT 帮我想方法,后果做进去的欢送界面至多我集体挺喜爱的。
import color from 'picocolors'
import boxen, {Options} from 'boxen'
import gradientString from 'gradient-string'
// 终极版
const welcomeMessage = gradientString('cyan', 'magenta').multiline(['Hello! 欢送应用 EKTFE 脚手架~', '😀🎉🚀'].join('')
)
const boxenOptions: Options = {
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'cyan',
backgroundColor: '#000',
}
console.log(boxen(welcomeMessage, boxenOptions))
具体的话,去 GitHub
搜下对应的库就好。
成果如图:
第四个小指标:公布并用于实际
是否要公布 npm 包?
咱们公司是没有公有服务器的。如果要公布,就只能公布到外网 npm 上。那如果不公布到 npm,其实也不是不行。就只有让对方在 create-cli-app
执行 yarn link
,而后在根目录执行yarn create xxx
即可。这种做法,对于网络不好的时候,这是个好方法。
最初我抉择公布到外网,反正也不是什么商业秘密。像之前提到的“平台选项”,不写死,没啥问题。能够说是通用的了。
而且公布到外网,应用起来比本地应用要不便,至多少了一步yarn link
。
次要的公布流程:(预计都被说烂了。但其实开发脚手架也是被说烂了,但我还是写了)
你只有注册 npm 账号,再通过 yarn login
和yarn publish
就能够公布了。(或者用 npm login
和npm publish
)
后续优化
1. 当呈现多个模版时,须要创立多个template
,并提供给用户进行抉择。
2. 某些我的项目可能从一开始就要装置一些库,例如echarts
。前期能够减少一个列表选项进行多选增加,这里预计又是常识盲区了。后期其实基于根目录的依赖项就行。
3. 目前 containers
目录下的子目录名称等还没有变成自动化,不过形式和前文提到的 template
相似。能够前期再写。
4. 代码层面的优化。
其它问题
.eslintrc.js 继承内部导致门路谬误?
module.exports = {extends: ['../../.eslintrc.js'],
}
本来打算通过 lint-staged .eslintignore
这类形式来跳过对 template
的eslint
检测。但发现不行。
最初想了个方法,间接把 .eslintrc.js
更名为 .eslintrc.js.tpl
就失去了它本来的作用了,复制文件时会去掉 .tpl
的后缀,就又复原了。
打包构建之前如何清理 lib 目录?
间接用 rimarf
不行。
rimraf 是什么?
一个用于递归删除文件和文件夹的 npm 包。在 Windows
上,因为文件夹门路的斜杠方向与 Unix
零碎不同,因而 rimraf
会呈现一些问题。
能够应用 cross-env
这个库,能够跨平台地设置环境变量。
"build": "cross-env rimraf lib && tsc"
也能够应用 del
这个库。
"build": "del lib && tsc"
如何用命令行创立 LICENSE?
发现还没有许可证,发现网上大多数是可视化创立,我想用命令行,license-generator 这个库能够解决。
小结
这篇文章是随着我浏览源码到开发脚手架整个过程始终写下来的,过程中看过 Vant
的源码、Node
文档、Yarn
文档,以及各种库的文档,起初因为开始用 ChatGPT,就决定试试在为我的日常工作学习提供一些帮忙。
每天花一些工夫开发脚手架、波及的各种细节还要做笔记(因为简直全是自己常识盲区)。不得不说波及的知识点还是挺多。用了一周工夫终于实现。再用近一周的工夫基于业务进行优化,并且要重新整理文章。整顿文章这个过程耗时很久,一是因为始终做笔记下来,前面的了解有时颠覆后面的,所以后面可能存在谬误的认知还有一些多余的揣测,所以须要重看本人之前写的内容,很多不须要的就去掉,还有很多排版等问题。另外,为了可读性补上行内代码的款式,体力活,我还是喜爱间接空格多一些,写起来难受,但不利于浏览。
原题目其实是“对于我编写一个脚手架顺便看完几个开源库源码的事”,但无奈就以上这点内容搞了很久,所以只能后续再钻研。
播种
- 对
yarn
的一些命令、package
的装置和执行、package.json
各种字段、脚手架的根本实现原理,都有较粗浅的意识; - 把
create-vant-cli-app
我的项目源码读完,尽管代码量超级少。但细节还是有的,次要是温习了不少Node
罕用的知识点(同理,能够把create-vite
等等也尝试拿下); - 输入了一个基于业务的定制化脚手架。通用脚手架多的是,基于业务才是重点;
- 乏味的是,因为是定制化,所以提醒语能够写直白点。如果是开源的通用脚手架,就要正式口气;
- 前期应用了 ChatGPT 解决了一小部分问题,它的益处是对于一些不太难但找起来比拟麻烦的知识点能够很快给出一个思路。如果遇到一些问题卡住了,去询问它比你查资料要快,而后比如说它抛出了一个 API,我就会去文档里找,的确快了不少。局部代码优化也能够借助它。不过它也常常会不苟言笑胡言乱语,所以如果没训练到位的话,留神不要被它忽悠了。
NEXT
先看下 create-vite
和create-vue
等源码。而后开始看 vant-cli
和vite
。