广州品牌网站建设公司,潮州市网站建设,做亚马逊网站费用,wordpress 多标签插件脚手架简介
脚手架是创建前端项目的命令行工具#xff0c;集成了常用的功能和配置#xff0c;方便我们快速搭建项目#xff0c;目前网络上也有很多可供选择的脚手架。
一个简单脚手架的构成其实非常少#xff0c;即 代码模板 命令行工具。其中代码模板是脚手…脚手架简介
脚手架是创建前端项目的命令行工具集成了常用的功能和配置方便我们快速搭建项目目前网络上也有很多可供选择的脚手架。
一个简单脚手架的构成其实非常少即 代码模板 命令行工具。其中代码模板是脚手架需要生成给用户的工程代码命令行工具的作用是提供命令行界面根据用户输入的信息和代码模板生成工程。
代码模板根据脚手架功能的不同而不同但是不同脚手架命令行工具相似性较大。今天就来聊一下如何编写一个脚手架。
脚手架使用方式
先看一下我们编写的脚手架的使用方式。
启动脚手架
npm create xxx
# 或者
npx create-xxx其中npx方式可以接收更多参数
# 查看脚手架版本
npx create-xxx -v
# 指定生成的工程名称
npx create-xxx -n appname命令行执行命令后会自动下载我们的create-xxx包并执行。
命令行界面和生成代码
脚手架启动后会出现一个交互式命令行界面有若干个输入项或选择项等这个根据不同脚手架的功能而不同。例如create-xxx0.0.1的选项。 在用户输入和选择全部完成后脚手架会把代码生成到本地同时提示用户启动方式。
第一版工程结构
首先我们创建脚手架工程的第一版。
|-- create-xxx0.0.1|-- package.json|-- src|-- appName.js|-- create.js|-- index.js|-- prompt.js|-- setFileConfig.js|-- templates|-- auto-element|-- ...|-- lite-element|-- ...|-- trad-element|-- ...其中src是脚手架工具命令行工具部分templates是代码模板部分。
第一版代码和说明
启动方式
首先来看一下package.json。
{name: create-xxx,version: 0.0.1,description: ,main: src/index.js,bin: {create-xxx: src/index.js},scripts: {test: echo \Error: no test specified\ exit 1},author: jiazhen,dependencies: {chalk: ^4,commander: ^10.0.0,inquirer: ^8.0.0,ora: ^5}
}注意这里所有的依赖都是dependencies即生产环境依赖。
这里设置了bin字段值为工具的入口文件可以使用该命令启动。同时设置为create-xxx可以使用npm create xxx的方式启动。不过使用npx会在全局自动缓存该npm包因此启动时后面带个latest表示最新版本更好。否则你上传了新版本用户依然还会使用本地缓存的旧版本。
# 建议
npx create-xxxlatest我们在第一版开发时还不是一个完整的包因此本地调试可以直接使用node执行
node src/index.js后续代码中所有的模块引入都使用的require也就是CommonJS规范。这种规范可以让代码在Node.js中直接执行。如果使用ES Modules规范即import使用Node.js中直接执行会报错。
主要流程
入口文件
先看代码首先是入口文件src/index.js。
#! /usr/bin/env node
const { program } require(commander);
const { createDir, getCmdName } require(./appName);
const package require(../package.json);
const { getPromptValue } require(./prompt);
const create require(./create);
const ora require(ora);
const chalk require(chalk);// 创建成功后的提示
function succConsole(configs) {console.log();console.log(chalk.cyan( ${chalk.gray($)} cd ${configs.name}));console.log(chalk.cyan( ${chalk.gray($)} npm install));console.log(chalk.cyan( ${chalk.gray($)} npm run dev));
}// 主函数
async function main() {program.option(-n, --name value, app name);program.version(package.version, -v);program.parse();const options program.opts();// 从命令行中获取AppNamelet name getCmdName(options, program);// 检测名称并创建文件夹name await createDir(name);// 获取脚手架选项const promptValue await getPromptValue();const configs {...promptValue,name,};const spinner ora(工程正在创建中);// 创建工程await create(configs);console.log();spinner.succeed(工程创建完成);succConsole(configs);
}main();流程说明
使用commander库获取命令行参数主要有name和version。检测名称并创建工程文件夹。用户交互式的输入脚手架选项。复制代码并根据脚手架选项调整代码。生成结束输出成功提示。
注意示项
脚本说明 首先是代码的第一句#! /usr/bin/env node这表示将用node脚本执行该命令。如果没有这一句后面作为一个npm包被执行的时候会报错。获取版本 版本信息我们直接使用package.json中的版本号即可。把它作为一个模块引入直接取值。
获取工程名称
文件src/appName.js。
const inquirer require(inquirer);
const fs require(fs);
const namePrompt [{type: input,name: name,message: 请输入工程名称,default: xxx-app,},
];// 检测名称并创建文件夹
async function createDir(appName) {while (1) {if (!appName) {const res await inquirer.prompt(namePrompt);appName res.name;}try {// 创建文件夹const res fs.mkdirSync(appName);break;} catch (e) {console.log(error: 工程名称与现有文件夹重名请重新输入);appName null;}}return appName;
}// 从命令行中获取AppName
function getCmdName(options, program) {// 输入了name参数优先取nameif (options.name) return options.name;// 没有name参数则使用第一个输入项if (program.args program.args.length 0) return program.args[0];return null;
}module.exports {createDir,getCmdName,
};我们把工程名称作为后面放置工程代码所创建的文件夹名称因此这个名称特别重要。
首先我们尝试从命令行参数中获取工程名称。如果没有获取到则提示用户输入工程名称。这里还对工程名称做了校验校验内容是————是否与当前已有的文件重名如果重名则提示用户重新输入。文件名校验成功则创建文件夹。
获取脚手架选项
文件src/prompt.js。
const inquirer require(inquirer);
const promptList [{type: list,message: 请选择模板类型,name: tamplate,default: auto,choices: [{name: 自动版 (推荐首选,集成vue3生态新功能),value: auto,},{name: 精简版 (无多语言/多皮肤等功能),value: lite,},{name: 传统版 (不使用各类按需引入插件),value: trad,},],},{type: input,name: namespace,message: 请输入组件上下文即公共基础路径,default: /,},
];async function getPromptValue() {const res await inquirer.prompt(promptList);return res;
}module.exports {getPromptValue,
};这部分非常简单按照inquirer库的格式做好需要用户输入的内容取得用户输入的值即可。
获取模板代码
代码模板和脚手架一起存放
文件src/create.js。
const path require(path);
const fs require(fs);
const { setTargetConfig } require(./setFileConfig)// 获取模板代码
async function getTempLateCodes(configs) {// 包中的代码位置let srcPath path.join(__dirname,../templates,${configs.tamplate}-element);// 代码要放置的目标工程位置const targetPath path.join(process.cwd(), configs.name);// 复制代码到工程中// node.js 16.7 Aug 18,2021发布fs.cpSync(srcPath, targetPath, { recursive: true });// 根据配置修改模板文件await setTargetConfig(targetPath, configs)
}// 创建工程的主函数
async function create(configs) {getTempLateCodes(configs);
}module.exports create;有了这些配置之后我们就可以获取模板代码了。我这里需要根据不同的配置而使用不同的代码模板。通过查看上面的工程结构我们看到代码模板是和脚手架代码放置在同一个npm包中的因此直接使用fs复制文件即可。相比于分开存放放置在通过一个npm包中复制文件速度更快。而分开存放一半需要通过网络下载代码模板速度慢一些。
使用这种方式需要注意两个路径
npm包路径 脚手架代码所在的位置的路径这个路径可以用__dirname获取。 从这个路径内获取模板代码。工作目录 当前执行脚手架命令所在的目录。这个路径可以用process.cwd()获取。 这个路径工程名称就是工程的存放位置。模板代码要放置到这里。
代码模板和脚手架分开存放
代码模板是和脚手架代码放置在同一个npm包中脚手架版本会和模板版本强绑定。如果不希望绑定可以脚手架一个npm包代码模板使用另外一个npm包。这样假设代码模板有多个版本可以使用同一个脚手架安装不同版本的模板。甚至可以模板放置在git上脚手架启动时直接去git上下载代码。
根据脚手架选项调整代码
文件src/setFileConfig.js。
const path require(path);
const fs require(fs);
const { promisify } require(util);// 回调转promise版本
const fsWriteFile promisify(fs.writeFile);
const fsReadFile promisify(fs.readFile);// 根据配置修改模板文件
async function setTargetConfig(targetPath, configs) {await Promise.all([setPackageJson(targetPath, configs),setReadme(targetPath, configs),setEnv(targetPath, configs),]);
}// 修改package.json
async function setPackageJson(targetPath, configs) {const objPath path.join(targetPath, package.json);const package require(objPath);package.name configs.name;const jsonData JSON.stringify(package, null, 2);await fsWriteFile(objPath, jsonData);
}// 修改README.md
async function setReadme(targetPath, configs) {const objPath path.join(targetPath, README.md);let data await fsReadFile(objPath, utf8);data data.replace(XXX-APP-NAME, configs.name);await fsWriteFile(objPath, data);
}// 修改.env
async function setEnv(targetPath, configs) {let namespace configs.namespace;// 完善namespace数据if(!namespace.length) {namespace /}if(namespace[0] ! /) {namespace / namespace}if(namespace[namespace.length - 1] ! /) {namespace namespace /}// 写入文件const objPath path.join(targetPath, .env);let data await fsReadFile(objPath, utf8);// 正则中.不包含换行符这里正好截取一行data data.replace(/VITE_NAMESPACE.*/g,VITE_NAMESPACE ${namespace});await fsWriteFile(objPath, data);
}module.exports {setTargetConfig,
};这里也非常简单我们把模板代码当作普通文件去读根据配置修改文件内容再写入即可。我们可以把模板的可变部分使用特殊的字符串标记方便我们使用正则查找并替换。如果修改的内容较复杂甚至可以使用一些模板引擎。读文件是耗时操作可以使用Promise.all一起执行多个。如果操作更复杂耗时甚至可以考虑引入多线程技术。
打包上传
写好之后并测试完成后我们就可以把我们的脚手架作为一个npm包发布这样用户才能下载使用。
# 发布包
npm publish
# 删除包
npm unpublish create-xxx0.0.1在公共网络发布npm包需要先注册。
第二版工程结构
第一版的工程已经可以作为一个脚手架来使用了。但是还有几个小问题可以改进
所有依赖都是生产依赖这意味着用户在启动脚手架时还需要额外下载很多npm包需要等待一段时间。脚手架代码可以进行压缩。
针对这两个问题我对第一版脚手架工程进行了改进改进后的工程结构如下
|-- create-xxx0.0.2|-- package.json|-- bin|-- index.js|-- dist|-- index.cjs|-- src|-- appName.js|-- create.js|-- index.js|-- prompt.js|-- setFileConfig.js|-- templates|-- auto-element|-- ...|-- lite-element|-- ...|-- trad-element|-- ...|-- .eslintignore|-- .eslintrc.js|-- .gitignore|-- .npmrc|-- .prettierignore|-- .prettierrc.js|-- build.config.js第二版的改动
使用unbuild打包
第一版代码没有经过打包直接发布第二版如果希望去除生产环境的依赖那么就必须进行打包。我参考create-vite使用了unbuild作为打包工具。这是一个轻量级的基于rollup的工具。 看一下打包配置build.config.js:
import { defineBuildConfig } from unbuildexport default defineBuildConfig({// 入口文件entries: [src/index],clean: true,// 生成ts声明文件declaration: false,// 警告是否会引发报错failOnWarn: false,// rollup配置rollup: {// 生成cjsemitCJS: true,inlineDependencies: true,esbuild: {// 压缩代码minify: true,},resolve: {exportConditions: [node],},},
})打包和压缩代码都是unbuild完成的。打包之后会生成一个dist文件夹里面有index.cjs和index.mjs。我们使用的是cjs即CommonJS规范的文件。
更新依赖
来看一下package.json。
{name: create-xxx,version: 0.0.2,description: xxx脚手架,main: bin/index.js,bin: {create-xxx: bin/index.js},scripts: {build: unbuild,lint: eslint src --fix,pretty: prettier --write .},author: jiazhen,devDependencies: {commander: ^10.0.0,unbuild: ^1.1.2,prompts: ^2.4.2,eslint: ^8.30.0,prettier: ^2.8.1,eslint-config-prettier: ^8.5.0,eslint-define-config: ^1.12.0,eslint-plugin-prettier: ^4.2.1},files: [templates,dist/index.cjs,bin/index.js]
}可以看到没有了生产依赖全部换成开发依赖了。而且功能代码直接使用的依赖只剩下两个必须的commander和prompts。package.json还有其它改动后面再说。
为啥要换依赖因为我们要打包后要成为一个CommonJS规范无任何依赖的Node.js可执行文件但是很多包并不支持这种打包方式。原因可能有很多可能是使用了某些浏览器才有的属性可能引入了不能打包的依赖等等。就算同一个npm包也可能部分版本支持部分版本不支持。
比如我们使用的prompts就在文档里说明了依赖非常少 Simple: prompts has no big dependencies nor is it broken into a dozen tiny modules that only work well together. 关于依赖的选用可以参考对应npm包的文档说明、issues等也可以参考其它实现该方式的脚手架。
换了有些依赖部分功能需要稍微修改一下比如从inquirer换为prompts就需要改部分配置。
注意对于不支持的依赖我出现过打包成功但是dist中出现reuqire(string_decoder/)这种不存在的依赖的报错。
更换库引入方式
我试过rollup系列的打包工具包括viterollup和unbuild对于require即CommonJS规范引入的依赖不处理即依然作为一个外部依赖而不是直接打包进成果物。可能是这些工具不支持也可能是由于我没配置对的原因后续我再研究一下。目前先全部转为ES Modules规范。需要改动的地方很少大致只有引入和导出的方式。
// CommonJS规范 示例
const { program } require(commander);
const { createDir, getCmdName } require(./appName);
const package require(../package.json);
const { getPromptValue } require(./prompt);
const create require(./create);
const ora require(ora);
const chalk require(chalk);module.exports {createDir,getCmdName,
};// ES Modules规范 示例
import { program } from commander
import { createDir, getCmdName } from ./appName
import packageData from ../package.json
import { getPromptValue } from ./prompt
import create from ./createexport { createDir, getCmdName }Node.js本身自带的库可以保持原样依然使用CommonJS规范。因为只要安装了Node.js(不安装啥也用不了)就可以直接使用这些工具不需要被打包。而且这些工具部分是使用C写的也不能打包到前端成果中。
// 部分Node.js本身自带的库
const path require(path)
const fs require(fs)
const { promisify } require(util)固定入口文件
通过上面的改动我们已经可以生成一个包含依赖的文件dist/index.cjs了。但是我们注意到即使我们在入口文件src/index.js中写了#! /usr/bin/env node生成的文件中也不包含这一句。因为这一句被当作注释删掉了。
因此我们再新建一个文件bin/index.js来引入
#!/usr/bin/env node
require(../dist/index.cjs)注意这个文件不需要被打包而是固定的直接引入打包后的文件。这里执行的环境是Node.js因此需要使用CommonJS规范。
更新package.json这个文件直接作为bin中的启动入口文件。
其它改动
npm publish时仅仅上传必须的文件比如bin/index.jsdist/index.cjs和templates模板文件夹。在package.json中使用files指定。上传的内容越少执行时下载的速度越快。加入Eslint和Prettier方便进行语法检查和代码格式化。可以看到对应的依赖和命令。使用npm配置文件.npmrc上传时不用切换仓库。
参考
这个脚手架的实现有参考create-vite和其它脚手架等在这里表示感谢~
create-vite vite项目脚手架 https://github.com/vitejs/vite/tree/main/packages/create-vitePrompts https://github.com/terkelg/promptsunbuild https://github.com/unjs/unbuild