搭建一个 menorepo 风格的脚手架工程
使用 node.js 运行 js
~index.js
console.log('Welcome to Mortal World')
在当前文件打开命令行输入 node index.js,回车运行该命令
node index.js
安装 pnpm
在命令行窗口中输入以下代码,执行安装 pnpm 。
npm install pnpm -g
初始化工程
创建 mortal 文件夹,
在当前文件夹打开命令行输入 pnpm init 初始化工程,创建 package.json 文件。
pnpm init
~mortal\package.json
{
"name": "mortal",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
创建 pnpm-workspace.yaml
pnpm 是使用 workspace (工作空间) 来搭建一个 menorepo 风格的工程。在 mortal 文件夹中创建 pnpm-workspace.yaml 工作空间配置文件,并在该文件中添加如下配置代码
~mortal\pnpm-workspace.yaml
packages:
- 'packages/*'
- 'examples/*'
配置后,声明了 packages 和 examples 文件夹中子工程是同属一个工作空间的,工作空间中的子工程编译打包的产物都可以被其它子工程引用。
创建 packages 文件夹
创建 packages 文件夹
在 packages 文件夹中新建 mortal-cli 文件夹,输入 pnpm init 初始化工程,执行来初始化一个工程,执行成功后,会在该文件夹中生成一个 pakeage.json 文件。
pnpm init
在 pakeage.json 中添加 bin 字段,来声明 mortal 命令,添加后的代码如下所示:
~mortal\packages\mortal-cli\pakeage.json
{
"name": "mortal-cli",
"version": "1.0.0",
"description": "一个最简单的脚手架",
"main": "index.js",
"bin": {
"mortal": "./bin/index.js"
},
"scripts": {
"mortal": "node ./bin/index.js"
},
"author": "",
"license": "ISC"
}
在 mortal-cli 文件夹中新建 bin 文件夹,在 bin 文件夹中新建 index.js 文件,并在该文件中添加如下代码:
~mortal\packages\mortal-cli\bin\index.js
#!/usr/bin/env node
console.log('Welcome to Mortal World')
创建 examples 文件夹
创建 examples 文件夹
在 examples 文件夹中新建 app 文件夹,输入 pnpm init 初始化工程,执行来初始化一个工程,执行成功后,会在该文件夹中生成一个 pakeage.json 文件。
pnpm init
在 pakeage.json 中添加 dependencies 字段,来添加 mortal-cli 依赖。再给 scripts 增加一条自定义脚本命令。添加后的代码如下所示:
~mortal\examples\app\pakeage.json
{
"name": "app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"mortal": "mortal"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"mortal-cli": "workspace:*"
}
}
安装依赖
在 mortal 文件夹目录下运行 pnpm i 命令,安装依赖。
pnpm i
安装成功后,在 app 文件夹目录下运行 pnpm mortal,会发现命令行窗口打印出 Welcome to Mortal World,说明你的 menorepo 风格的脚手架工程的搭建成功了。
pnpm mortal
此时整个工程的目录结构如下所示
脚手架必备的模块
一个最简单的脚手架包含以下几个模块。
- 命令参数模块
- 用户交互模块
- 文件拷贝模块
- 动态文件生成模块
- 自动安装依赖模块
下面我们来一一将他们实现。
命令参数模块
获取命令参数
Node.js 中的 process 模块提供了当前 Node.js 进程相关的全局环境信息,比如命令参数、环境变量、命令运行路径等等。
脚手架提供的 mortal 命令后面还可以设置参数,标准的脚手架命令参数需要支持两种格式,比如:
mortal --name=orderPage
mortal --name orderPage
如果通过 process.argv 来获取,要额外处理两种不同的命令参数格式,不方便。
这里推荐 yargs 开源库来解析命令参数。
安装 yargs
pnpm add yargs --F mortal-cli
pnpm add 是 pnpm 中安装依赖包的命令, --F mortal-cli,是指定依赖安装到 mortal-cli 子工程中。
注意,mortal-cli 是取 mortal-cli 子工程中 package.json 中 name 字段的值,而不是 mortal-cli 子工程文件夹的名称。
使用 yargs
yargs 提供的 argv 属性是对两个格式的命令参数的处理结果。
在 bin/index.js 添加如下代码:
~mortal\packages\mortal-cli\bin\index.js
#!/usr/bin/env node
const yargs = require('yargs')
console.log('name', yargs.argv.name)
注意,以上代码是在 Node.js 环境中运行,Node.js 的模块是遵循 CommonJS 规范的,如果要依赖一个模块,要使用 Node.js 内置 require 系统函数引用模块使用。
在 app 文件夹目录下运行 pnpm mortal – --name=orderPage ,
pnpm mortal -- --name=orderPage
注意,在 pnpm mortal 后面需要加上两个连字符(--),这是为了告诉 pnpm 后面的参数是传递给命令 mortal 本身的,而不是传递给 pnpm 的。
结果如下图所示:
可以通过 yargs.argv.name 获取命令参数 name 的值。
设置子命令
假如脚手架要对外提供多个功能,不能将所有的功能都集中在 mortal 命令中实现。
可以通过 yargs 提供的 command 方法来设置一些子命令,让每个子命令对应各自功能,各司其职。
yargs.command 的用法
yargs.command 的用法是 yargs.command(cmd, desc, builder, handler)。
- cmd:字符串,子命令名称,也可以传递数组,如 [‘create’, ‘c’],表示子命令叫 create,其别名是 c;
- desc:字符串,子命令描述信息;
- builder:一个返回数组的函数,子命令参数信息配置,比如可以设置参数:
- alias:别名;
- demand:是否必填;default:默认值;
- describe:描述信息;
- type:参数类型,string | boolean | number。
- handler: 函数,可以在这个函数中专门处理该子命令参数。
设置一个用来生成一个模板的子命令
下面我们来设置一个用来生成一个模板的子命令,把这个子命令命名为 create。修改在 bin/index.js 文件中的代码,如下所示:
~mortal\packages\mortal-cli\bin\index.js
#!/usr/bin/env node
const yargs = require('yargs')
yargs.command(
['create', 'c'],
'新建一个模板',
function (yargs) {
return yargs.option('name', {
alias: 'n',
demand: true,
describe: '模板名称',
type: 'string'
})
},
function (argv) {
console.log('argv', argv)
}
).argv
在 app 文件夹目录下分别运行 pnpm mortal create – --name=orderPage 和 pnpm mortal c – --name=orderPage 命令,
pnpm mortal create -- --name=orderPage
pnpm mortal c -- --name=orderPage
执行结果如下图所示:
在上面我们配置了子命令 create 的参数 name 的一些参数信息。那这些要怎么展示给用户看呢?其实只要我们输入子命令的参数有错误,就会在命令行窗口中显示这些参数信息。
在 app 文件夹目录下运行 pnpm mortal c – --abc 命令,
pnpm mortal c -- --abc
执行结果如下图所示:
到此为止,我们最简单地实现了脚手架和用户之间的交互能力,但是如果自定义参数过多,那么命令行参数的交互方法对于用户来说是非常不友好的。所以我们还要实现一个用户交互模块。
用户交互模块
安装 inquirer
使用 inquirer 开源库来实现询问式的交互,运行以下命令安装 inquirer:
pnpm add inquirer@8.2.5 --F mortal-cli
为了使用 require 引入 inquirer ,要使用 8.2.5 版本的 inquirer。
这里我们主要使用了 inquirer 开源库的三个方面的能力:
- 询问用户问题
- 获取并解析用户的输入
- 检测用户的答案是否合法
inquirer.prompt()的使用方法
主要通过 inquirer.prompt() 来实现。prompt 函数接收一个数组,数组的每一项都是一个询问项,询问项有很多配置参数,下面是常用的配置项。
- type:提问的类型,常用的有
- 输入框:input;
- 确认:confirm;
- 单选组:list;
- 多选组:checkbox;
- name:存储当前问题答案的变量;
- message:问题的描述;
- default:默认值;
- choices:列表选项,在某些 type 下可用;
- validate:对用户的答案进行校验;
- filter:对用户的答案进行过滤处理,返回处理后的值。
创建一个模板文件
创建一个模板文件,大概会询问用户:模板文件名称、模板类型、使用什么框架开发、使用框架对应的哪个组件库开发等等。
在 bin 文件夹中新建 inquirer.js 文件夹,在里面添加如下代码:
~mortal\packages\mortal-cli\bin\inquirer.js
const inquirer = require('inquirer')
function inquirerPrompt(argv) {
const { name } = argv
return new Promise((resolve, reject) => {
inquirer
.prompt([
{
type: 'input',
name: 'name',
message: '模板名称',
default: name,
validate: function (val) {
if (!/^[a-zA-Z]+$/.test(val)) {
return '模板名称只能含有英文'
}
if (!/^[A-Z]/.test(val)) {
return '模板名称首字母必须大写'
}
return true
}
},
{
type: 'list',
name: 'type',
message: '模板类型',
choices: ['表单', '动态表单', '嵌套表单'],
filter: function (value) {
return {
表单: 'form',
动态表单: 'dynamicForm',
嵌套表单: 'nestedForm'
}[value]
}
},
{
type: 'list',
message: '使用什么框架开发',
choices: ['react', 'vue'],
name: 'frame'
}
])
.then(answers => {
const { frame } = answers
if (frame === 'react') {
inquirer
.prompt([
{
type: 'list',
message: '使用什么UI组件库开发',
choices: ['Ant Design'],
name: 'library'
}
])
.then(answers1 => {
resolve({
...answers,
...answers1
})
})
.catch(error => {
reject(error)
})
}
if (frame === 'vue') {
inquirer
.prompt([
{
type: 'list',
message: '使用什么UI组件库开发',
choices: ['Element'],
name: 'library'
}
])
.then(answers2 => {
resolve({
...answers,
...answers2
})
})
.catch(error => {
reject(error)
})
}
})
.catch(error => {
reject(error)
})
})
}
exports.inquirerPrompt = inquirerPrompt
其中 inquirer.prompt()返回的是一个 Promise,我们可以用 then 获取上个询问的答案,根据答案再发起对应的内容。
在 bin/index.js 中引入 inquirerPrompt。
~mortal\packages\mortal-cli\bin\index.js
#!/usr/bin/env node
const yargs = require('yargs')
const { inquirerPrompt } = require('./inquirer')
yargs.command(
['create', 'c'],
'新建一个模板',
function (yargs) {
return yargs.option('name', {
alias: 'n',
demand: true,
describe: '模板名称',
type: 'string'
})
},
function (argv) {
inquirerPrompt(argv).then(answers => {
console.log(answers)
})
}
).argv
运行命令
在 app 文件夹目录下运行 pnpm mortal c – --n Input 命令,
pnpm mortal c -- --n Input
执行结果如下图所示:
可以很清楚地看到“在使用什么框架开发”的询问中回答不同,下一个“使用什么 UI 组件库的开发”的询问可选项不一样。
回答完成后,在图中清楚地看到答案格式与内容。
文件夹拷贝模块
要生成一个模板文件,最简单的做法就是执行脚手架提供的命令后,把脚手架中的模板文件,拷贝到对应的地方。模板文件可以是单个文件,也可以是一个文件夹。
安装 copy-dir
使用开源库 copy-dir 来实现拷贝文件。
运行以下命令安装 copy-dir 。
pnpm add copy-dir --F mortal-cli
新建 copy.js 文件
在 bin 文件夹中新建 copy.js 文件,在里面添加如下代码:
~mortal\packages\mortal-cli\bin\copy.js
const copydir = require('copy-dir')
const fs = require('fs')
function copyDir(from, to, options) {
copydir.sync(from, to, options)
}
function checkMkdirExists(path) {
return fs.existsSync(path)
}
exports.checkMkdirExists = checkMkdirExists
exports.copyDir = copyDir
创建一个场景
在 bin 文件夹中新建 template 文件夹,用来存放模板文件,比如在 template 文件夹中创建一个 form 文件夹来存放表单模板,随意在 form 文件夹中创建一个 index.js,在里面随便写些内容。其目录结构如下所示:
使用 copyDir 方法
下面来实现把 packages/mortal/bin/template/form 这个文件夹拷贝到 examples/app/src/pages/OrderPage 中 。
在 bin/index.js 修改代码,修改后的代码如下所示:
~mortal\packages\mortal-cli\bin\index.js
#!/usr/bin/env node
const yargs = require('yargs')
const path = require('path')
const { inquirerPrompt } = require('./inquirer')
const { copyDir, checkMkdirExists } = require('./copy')
yargs.command(
['create', 'c'],
'新建一个模板',
function (yargs) {
return yargs.option('name', {
alias: 'n',
demand: true,
describe: '模板名称',
type: 'string'
})
},
function (argv) {
inquirerPrompt(argv).then(answers => {
const { name, type } = answers
const isMkdirExists = checkMkdirExists(path.resolve(process.cwd(), `./src/pages/${name}`))
if (isMkdirExists) {
console.log(`${name}文件夹已经存在`)
} else {
copyDir(path.resolve(__dirname, `./template/${type}`), path.resolve(process.cwd(), `./src/pages/${name}`))
}
})
}
).argv
使用拷贝文件方法 copyDir 的难点是参数 from 和 to 的赋值。其中 from 表示要拷贝文件的路径,to 表示要把文件拷贝到那里的路径。这里的路径最好使用绝对路径,因为在 Node.js 中使用相对路径会出现一系列奇奇怪怪的问题。
脚手架中的路径处理
path.resolve 方法
用 Node.js 中的 path 模块提供的 path.resolve( [from…], to ) 方法将路径转成绝对路径,就是将参数 to 拼接成一个绝对路径,[from … ] 为选填项,可以设置多个路径,如 path.resolve(‘./aaa’, ‘./bbb’, ‘./ccc’) ,使用时要注意 path.resolve 的路径拼接规则:
- 从后向前拼接路径;
- 若 to 以 / 开头,不会拼接到前面的路径;
- 若 to 以 …/ 开头,拼接前面的路径,且不含最后一节路径;
- 若 to 以 ./ 开头或者没有符号,则拼接前面路径。
从以上拼接规则来看,使用 path.resolve 时,要特别注意参数 to 的设置。
使用 copyDir 方法时,参数如何设置
将 copyDir 的参数 from 设置为 path.resolve(__dirname, ./template/${type}
),
其中 __dirname 是用来动态获取当前文件模块所属目录的绝对路径。
比如在 bin/index.js 文件中使用 __dirname ,__dirname 表示就是 bin/index.js 文件所属目录的绝对路径 D:\workfolder\mygit\interview\csdn\demo\menorepo\mortalpackages\mortal-cli\bin。
因为模板文件存放在 bin/template 文件夹中 ,copyDir 是在 bin/index.js 中使用,bin/template 文件夹相对 bin/index.js 文件的路径是 ./template,所以把 path.resolve 的参数 to 设置为 ./template/${type},
其中 type 是用户所选的模板类型。
假设 type 的模板类型是 form,那么 path.resolve(__dirname, `./template/form`) 得到的绝对路径是 D:\workfolder\mygit\interview\csdn\demo\menorepo\mortalpackages\mortal-cli\bin\template\form。
将 copyDir 的参数 to 设置为 path.resolve(process.cwd(), ${name}
),
其中 process.cwd() 当前 Node.js 进程执行时的文件所属目录的绝对路径。比如在 bin 文件夹目录下运行 node index.js 时,process.cwd() 得到的是 D:\workfolder\mygit\interview\csdn\demo\menorepo\mortalpackages\mortal-cli\bin。
运行 node index.js 相当运行 mortal 命令。而在现代前端工程中都是在 package.json 文件中 scripts 定义了脚本命令,如下所示:
~mortal\examples\app\pakeage.json
{
"name": "app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"mortal": "mortal"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"mortal-cli": "workspace:*"
}
}
运行 pnpm mortal 就相当运行 mortal 命令,那么执行 pnpm mortal 时,当前 Node.js 进程执行时的文件是 package.json 文件。那么 process.cwd() 得到的是 D:\workfolder\mygit\interview\csdn\demo\menorepo\mortalexamples\app。
因为要把 packages/mortal/bin/template/form 这个文件夹拷贝到 examples/app/src/pages/OrderPage 中,且 process.cwd() 的值是 D:\workfolder\mygit\interview\csdn\demo\menorepo\mortalexamples\app,src/pages 文件夹相对 examples/app 的路径是 ./src/pages ,所以把 path.resolve 的参数 to 设置为 ./src/pages/${name},其中 name 是用户所输入的模板名称。
目录守卫在
app 文件夹目录下运行 pnpm mortal create – --name=OrderPage,看能不能成功得把 packages/mortal/bin/template/form 这个文件夹拷贝到 examples/app/src/pages/OrderPage 中。
pnpm mortal create -- --name=OrderPage
报错了,提示 examples/app/src/pages 文件夹不存在。为了防止这种报错出现,我们要实现一个目录守护的方法 mkdirGuard ,比如 examples/app/src/pages 文件夹不存在,就创建一个 examples/app/src/pages 文件夹。
在 bin/copy.js 文件中,修改代码,如下所示:
~mortal\packages\mortal-cli\bin\copy.js
const copydir = require('copy-dir')
const fs = require('fs')
const path = require('path')
function mkdirGuard(target) {
try {
fs.mkdirSync(target, { recursive: true })
} catch (e) {
mkdirp(target)
function mkdirp(dir) {
if (fs.existsSync(dir)) {
return true
}
const dirname = path.dirname(dir)
mkdirp(dirname)
fs.mkdirSync(dir)
}
}
}
function copyDir(form, to, options) {
mkdirGuard(to)
copydir.sync(form, to, options)
}
function checkMkdirExists(path) {
return fs.existsSync(path)
}
exports.checkMkdirExists = checkMkdirExists
exports.mkdirGuard = mkdirGuard
exports.copyDir = copyDir
fs.mkdirSync 的语法
fs.mkdirSync 的语法格式:fs.mkdirSync(path[, options]),创建文件夹目录。
- path:文件夹目录路径;
- options:recursive 表示是否要创建父目录,true 要。
fs.existsSync 的语法格式
fs.existsSync 的语法格式:fs.existsSync(pach),检测目录是否存在,如果目录存在返回 true ,如果目录不存在返回 false。
- path:文件夹目录路径。
fs.dirname 的语法格式
path.dirname 的语法格式:path.dirname(path),用于获取给定路径的目录名。
- path:文件路径。
mkdirGuard 分析
在 mkdirGuard 方法内部,当要创建的目录 target 父级目录不存在时,调用 fs.mkdirSync(target),会报错走 catch 部分逻辑,在其中递归创建父级目录,使用 fs.existsSync(dir) 来判断父级目录是否存在,来终止递归。
这里要特别注意 fs.mkdirSync(dir) 创建父级目录要在 mkdirp(dirname) 之前调用,才能形成一个正确的创建顺序,否则创建父级目录过程会因父级目录的父级目录不存在报错。
运行命令
在 app 文件夹目录下运行 pnpm mortal create – --name=OrderPage,看这次能不能成功得把 packages/mortal/bin/template/form 这个文件夹拷贝到 examples/app/src/pages/OrderPage 中。
pnpm mortal create -- --name=OrderPage
成功添加,添加结果如下所示:
然后再运行 pnpm mortal create – --name=OrderPage 命令,会发现控制台打印出模板已经存在在提示。
这是为了防止用户修改后的模板文件,运行命令后被重新覆盖到初始状态。所以我们引入一个校验模板文件是否存在的 checkMkdirExists 方法,内部采用 fs.existsSync 来实现。
文件拷贝模块
文件拷贝分三步来实现,使用 fs.readFileSync 读取被拷贝的文件内容,然后创建一个文件,再使用 fs.writeFileSync 写入文件内容。
在 bin/copy.js 文件,在里面添加如下代码:
~mortal\packages\mortal-cli\bin\copy.js
function copyFile(from, to) {
const buffer = fs.readFileSync(from)
const parentPath = path.dirname(to)
mkdirGuard(parentPath)
fs.writeFileSync(to, buffer)
}
exports.copyFile = copyFile
接下来我们使用 copyFile 方法,在 bin/index.js 修改代码,修改后的代码如下所示:
~mortal\packages\mortal-cli\bin\index.js
#!/usr/bin/env node
const yargs = require('yargs')
const path = require('path')
const { inquirerPrompt } = require('./inquirer')
const { copyFile, checkMkdirExists } = require('./copy')
yargs.command(
['create', 'c'],
'新建一个模板',
function (yargs) {
return yargs.option('name', {
alias: 'n',
demand: true,
describe: '模板名称',
type: 'string'
})
},
function (argv) {
inquirerPrompt(argv).then(answers => {
const { name, type } = answers
const isMkdirExists = checkMkdirExists(path.resolve(process.cwd(), `./src/pages/${name}/index.js`))
if (isMkdirExists) {
console.log(`${name}/index.js文件已经存在`)
} else {
copyFile(path.resolve(__dirname, `./template/${type}/index.js`), path.resolve(process.cwd(), `./src/pages/${name}/index.js`), {
name
})
}
})
}
).argv
copyFile 和 copyDir 使用的区别在参数,copyFile 要求参数 from 和参数 to 都精确到文件路径。
在 app 文件夹目录下运行 pnpm mortal create – --name=OrderPage,
pnpm mortal create -- --name=OrderPage
执行结果如下图所示
动态文件生成模块
假设脚手架中提供的模板文件中某些信息需要根据用户输入的命令参数来动态生成对应的模板文件。
比如下面模板文件中 App 要动态替换成用户输入的命令参数 name 的值,该如何实现呢?
import React from 'react'
const App = () => {
return (
<div></div>
)
}
export default App
安装 mustache
这里推荐使用开源库 mustache 来实现,运行以下命令安装 mustache。
pnpm add mustache --F mortal-cli
mustache 简介
Mustache 插件是一种轻量级模板引擎,用于将变量插入到模板中生成最终的文本输出 1。
Mustache 插件的设计简单且易于使用,适用于多种编程语言,包括 JavaScript、Python、Ruby、Java 等。它的模板语法使用双大括号{{}}包裹变量或表达式,用于表示变量的占位符。
在使用 Mustache 插件的过程中,可以创建一个模板字符串,其中包含一个或多个变量的占位符。然后,将这个模板字符串传递给 Mustache 引擎,引擎会使用提供的数据替换占位符,并生成最终的输出结果 1。
mustache 的语法
下面来介绍一些常用语法
{{key}}
{{#key}} {{/key}}
{{^key}} {{/key}}
{{.}}
{{&key}}
简单绑定
使用 {{key}} 语法,key 要和 Mustache.render 方法中的第二个参数(一个对象)的属性名一致。例如:
Mustache.render('<span>{{name}}</span>',{name:'张三'})
输出:
<span>张三</span
绑定子属性
例如:
Mustache.render('<span>{{ifno.name}}</span>', { ifno: { name: '张三' } })
输出:
<span>张三</span>
循环渲染
如果 key 属性值是一个数组,则可以使用 {{#key}} {{/key}} 语法来循环展示。 其中 {{#}} 标记表示从该标记以后的内容全部都要循环展示,{{/}}标记表示循环结束。
例如:
Mustache.render(
'<span>{{#list}}{{name}}{{/list}}</span>',
{
list: [
{ name: '张三' },
{ name: '李四' },
{ name: '王五' },
]
}
)
输出:
<span>张三李四王五</span>如果 list 的值是 ['张三','李四','王五'],要把 {{name}} 替换成 {{.}} 才可以渲染。Mustache.render(
'<span>{{#list}}{{.}}{{/list}}</span>',
{
list: ['张三','李四','王五']
}
)
循环中二次处理数据
Mustache.render 方法中的第二个参数是个对象,其属性值可以是一个函数,渲染时候会执行函数输出返回值,函数中可以用 this 获取第二个参数的上下文。
例如:
Mustache.render( '<span>{{#list}}{{info}}{{/list}}</span>', { list: [ { name: '张三' }, { name: '李四' }, { name: '王五' }, ], info() { return this.name + ','; } } )
输出:
<span>张三,李四,王五,</span>
条件渲染
使用 {{#key}} {{/key}} 语法 和 {{^key}} {{/key}} 语法来实现条件渲染,当 key 为 false、0、[]、{}、null,既是 key == false 为真,{{#key}} {{/key}} 包裹的内容不渲染,{{^key}} {{/key}} 包裹的内容渲染
例如:
Mustache.render(
'<span>{{#show}}显示{{/show}}{{^show}}隐藏{{/show}}</span>',
{
show: false
}
)
输出:
<span>隐藏</span>
不转义 HTML 标签
使用 {{&key}} 语法来实现。
例如:
Mustache.render(
'<span>{{&key}}</span>',
{
key: '<span>标题</span>'
}
)
输出:
<span><span>标题</span></span>
使用 mustache
在 packages/mortal-cli/bin/template/form 文件夹中创建一个 index.tpl 文件,内容如下:
~mortal\packages\mortal-cli\bin\template\form\index.tpl
import React from 'react'
const {{name}} = () => {
return (
<div>测试</div>
)
}
export default {{name}}
先写一个 readTemplate 方法来读取这个 index.tpl 动态模板文件内容。在 bin/copy.js 文件,在里面添加如下代码:
~mortal\packages\mortal-cli\bin\copy.js
const Mustache = require('mustache')
function readTemplate(path, data = {}) {
const str = fs.readFileSync(path, { encoding: 'utf8' })
return Mustache.render(str, data)
}
exports.readTemplate = readTemplate
readTemplate 方法接收两个参数,path 动态模板文件的相对路径,data 动态模板文件的配置数据。
使用 Mustache.render(str, data) 生成模板文件内容返回,因为 Mustache.render 的第一个参数类型是个字符串,所以在调用 fs.readFileSync 时要指定 encoding 类型为 utf8,否则 fs.readFileSync 返回 Buffer 类型数据。
再写一个 copyTemplate 方法来拷贝模板文件到对应的地方,跟 copyFile 方法非常相似。在 bin/copy.js 文件,在里面添加如下代码:
~mortal\packages\mortal-cli\bin\copy.js
function copyTemplate(from, to, data = {}) {
if (path.extname(from) !== '.tpl') {
return copyFile(from, to)
}
const parentToPath = path.dirname(to)
mkdirGuard(parentToPath)
fs.writeFileSync(to, readTemplate(from, data))
}
exports.copyTemplate = copyTemplate
path.extname(from) 返回文件扩展名,比如 path.extname(index.tpl) 返回 .tpl。
在 bin/index.js 修改代码,修改后的代码如下所示:
~mortal\packages\mortal-cli\bin\index.js
#!/usr/bin/env node
const yargs = require('yargs')
const path = require('path')
const { inquirerPrompt } = require('./inquirer')
const { copyTemplate, checkMkdirExists } = require('./copy')
yargs.command(
['create', 'c'],
'新建一个模板',
function (yargs) {
return yargs.option('name', {
alias: 'n',
demand: true,
describe: '模板名称',
type: 'string'
})
},
function (argv) {
inquirerPrompt(argv).then(answers => {
const { name, type } = answers
const isMkdirExists = checkMkdirExists(path.resolve(process.cwd(), `./src/pages/${name}/index.js`))
if (isMkdirExists) {
console.log(`${name}/index.js文件已经存在`)
} else {
copyTemplate(path.resolve(__dirname, `./template/${type}/index.tpl`), path.resolve(process.cwd(), `./src/pages/${name}/index.js`), {
name
})
}
})
}
).argv
在 app 文件夹目录下运行 pnpm mortal create – --name=OrderPage,
pnpm mortal create -- --name=OrderPage
执行结果如下图所示:
自动安装依赖模块
假设模板是这样的:
~mortal\packages\mortal-cli\bin\template\form\index.js
import React from 'react'
import { Button, Form, Input } from 'antd'
const App = () => {
const onFinish = values => {
console.log('Success:', values)
}
return (
<Form onFinish={onFinish} autoComplete="off">
<Form.Item label="Username" name="username">
<Input />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
提交
</Button>
</Form.Item>
</Form>
)
}
export default App
可以看到模板中使用了 react 和 antd 这两个第三方依赖,假如使用模板的工程中没有安装这两个依赖,我们要实现在生成模板过程中就自动安装这两个依赖。
我们使用 Node 中 child_process 子进程这个模块来实现。
在 child_process 子进程中的最常用的语法是: child_process.exec(command, options, callback)
- command:命令,比如 pnpm install
- options:参数
- cwd:设置命令运行环境的路径
- env:环境变量
- timeout:运行执行现在
- callback:运行命令结束回调,(error, stdout, stderr) =>{ }
- 执行成功后 error 为 null,
- 执行失败后 error 为 Error 实例,
- stdout、stderr 为标准输出、标准错误,其格式默认是字符串。
在 bin 文件夹中新建 manager.js 文件,在里面添加如下代码:
~mortal\packages\mortal-cli\bin\manager.js
const path = require('path')
const { exec } = require('child_process')
const LibraryMap = {
'Ant Design': 'antd',
iView: 'view-ui-plus',
'Ant Design Vue': 'ant-design-vue',
Element: 'element-plus'
}
function install(cmdPath, options) {
const { frame, library } = options
const command = `pnpm add ${frame} && pnpm add ${LibraryMap[library]}`
return new Promise(function (resolve, reject) {
exec(
command,
{
cwd: path.resolve(cmdPath)
},
function (error, stdout, stderr) {
console.log('error', error)
console.log('stdout', stdout)
console.log('stderr', stderr)
return
}
)
})
}
exports.install = install
在 install 方法中 exec 的参数 command 是 pnpm 安装依赖命令,安装多个依赖时使用 && 拼接。参数 cwd 是所安装依赖工程的 package.json 文件路径,我们可以使用 process.cwd() 获取。已经在上文提到过,process.cwd() 是当前 Node.js 进程执行时的文件所属目录的绝对路径。接下来使用,
在 bin/index.js 修改代码,修改后的代码如下所示:
~mortal\packages\mortal-cli\bin\index.js
#!/usr/bin/env node
const yargs = require('yargs')
const path = require('path')
const { inquirerPrompt } = require('./inquirer')
const { copyTemplate, checkMkdirExists } = require('./copy')
const { install } = require('./manager')
yargs.command(
['create', 'c'],
'新建一个模板',
function (yargs) {
return yargs.option('name', {
alias: 'n',
demand: true,
describe: '模板名称',
type: 'string'
})
},
function (argv) {
inquirerPrompt(argv).then(answers => {
const { name, type } = answers
const isMkdirExists = checkMkdirExists(path.resolve(process.cwd(), `./src/pages/${name}/index.js`))
if (isMkdirExists) {
console.log(`${name}/index.js文件已经存在`)
} else {
copyTemplate(path.resolve(__dirname, `./template/${type}/index.tpl`), path.resolve(process.cwd(), `./src/pages/${name}/index.js`), {
name
})
install(process.cwd(), answers)
}
})
}
).argv
当执行完 copyTemplate 方法后,就开始执行 install(process.cwd(), answers) 自动安装模板中所需的依赖。
在 app 文件夹目录下运行 pnpm mortal create – --name=OrderPage,看能不能自动安装依赖。
等命令执行完成后,观察 examples\app\package.json 文件中的 dependencies 值是不是添加了 antd 和 react 依赖。
此外,我们在执行命令中会发现,光标一直在闪烁,好像卡住了,其中是依赖在安装。这里我们要引入一个加载动画,来解决这个不友好的现象。
使用开源库 ora 来实现加载动画
这里推荐使用开源库 ora 来实现加载动画。
运行以下命令安装 ora
pnpm add ora@5.4.1 --F mortal-cli
在 bin/manager.js 修改代码,修改后的代码如下所示:
~mortal\packages\mortal-cli\bin\manager.js
const path = require('path')
const { exec } = require('child_process')
const ora = require('ora')
const LibraryMap = {
'Ant Design': 'antd',
iView: 'view-ui-plus',
'Ant Design Vue': 'ant-design-vue',
Element: 'element-plus'
}
function install(cmdPath, options) {
const { frame, library } = options
const command = `pnpm add ${frame} && pnpm add ${LibraryMap[library]}`
return new Promise(function (resolve, reject) {
const spinner = ora()
spinner.start(`正在安装依赖,请稍等`)
exec(
command,
{
cwd: path.resolve(cmdPath)
},
function (error) {
if (error) {
reject()
spinner.fail(`依赖安装失败`)
return
}
spinner.succeed(`依赖安装成功`)
resolve()
return
}
)
})
}
exports.install = install
在 app 文件夹目录下运行 pnpm mortal create – --name=OrderPage,看一下执行效果。
发布和安装
在 packages/mortal 文件夹目录下运行,运行以下命令安装将脚手架发布到 npm 上。
pnpm publish --F mortal-cli
发布成功后。我们在一个任意工程中,执行
pnpm add mortal-cli -D
安装 mortal-cli 脚手架依赖成功后,在工程中执行 pnpm mortal create – --name=OrderPage 命令即可。
pnpm mortal create -- --name=OrderPage
参考资料
https://www.zhihu.com/question/586964594/answer/3140644620?utm_id=0