基于webpack脚手架
关键模块:
-
zx: node脚本工具 ,提供实用函数来简化 shell 脚本编写,它使 CLI 开发非常方便快捷,集成了chalk、globby、fs-extra、os、node-fetch等工具包
-
commander:可以进行更加复杂的命令行参数解析。如 --help就借助了它
-
inquirer/prompts:可以处理复杂的用户输入,完成命令行输入交互。
- prompts是轻量级的,只有14.4kb,inquirer有238kb
-
download-git-repo: 在git中下载模板
-
ncp: 使用 Node 进行异步递归文件复制
-
metalsmith: 读取所有文件,实现模板渲染
-
consolidate: 模板引擎的结合体。统一模板引擎,包括了常用的jade和ejs
-
handlebar、ejs:模板引擎
-
chalk:使终端可以输出彩色信息文案。
-
ora/cli-spinners:可以让命令行出现好看的 loading。
-
boxen:可以在命令行中画出 Boxes 区块。
-
listr:可以在命令行中画出进度列表。
-
meow、arg:可以进行基础的命令行参数解析。
-
fs-extra: https://github.com/jprichardson/node-fs-extra
1、可替代fs模块,继承了fs所有方法和为fs方法添加了promise的支持
2、操作文件系统用的,比如复制,移动、创建文件这些
3、特点:1、支持promise 2、基于fs扩展了好多操作文件方法
-
npminstall : 使 npm install 快速方便
-
dotenv: 获取环境变量,可将
.env文件中的环境变量加载到process.env中 -
semver:版本比对。检查当前版本是否为最新版本
-
npmlog: npm的记录器工具,它支持自定义级别和彩色输出
-
yaml-front-matter:基于js-yaml, 实现json和yaml互转格式
-
globby:globby 是增强型的 glob,提供了
Promise的封装。它支持多模式匹配,相比于glob只能传入一个pattern字符串,globby支持传入一个pattern数组。- 可以用来匹配读取目录中所有的文件
1. 在命令行启动 cli
目标: 实现在命令行执行 my-cli 来启动我们的脚手架
-
1.新建项目目录 my-cli
$ mkdir my-cli$ cd my-cli$ npm init # 生成 package.json 文件 -
2.新建程序入口文件bin/index.js
mkdir bintouch bin/index.js # 新建 index.js文件- index.js
#! /usr/bin/env node// #! 符号的名称叫 Shebang,用于指定脚本的解释程序// Node CLI 应用入口文件必须要有这样的文件头// 如果是Linux 或者 macOS 系统下还需要修改此文件的读写权限为 755// 具体就是通过 chmod 755 cli.js 实现修改// 用于检查入口文件是否正常执行console.log('my-cli ~'); -
3.在 package.json 文件中指定入口文件为 cli.js
{"name": "my-cli","version": "1.0.0","description": "","main": "index.js","bin": {"my-cli": "bin/index.js"},"scripts": {"test": "echo \"Error: no test specified\" && exit 1"},"keywords": [],"author": "","license": "ISC","dependencies": {}}
注意点:
-
1.需要在package.json文件增加bin字段
- 用于配置内部命令对应的可执行文件位置。配置命令后,npm 会寻找到对应的可执行文件,然后在 node_modules/.bin 目录下建立对应的符号链接
-
2 .需要在bin文件的顶层增加:
#! /usr/bin/env node-
【用于指定脚本的解释程序】,解决了不同的用户node路径不同的问题,可以让系统动态的去查找node来执行你的脚本文件
#! 符号的名称叫 Shebang, 是Linux或者Unix系统的符号名称
-
根据当前用户环境,让系统去找到node去执行你的脚本文件
-
npm link 链接到全局
npm link # or yarn link
2、配置规范
eslint、perttier
eslint配置:
-
安装:
yarn add eslint -
执行eslint配置文件,可以生成eslint配置
- 执行这个命令,eslint会进行询问问题,如询问你想要的代码风格,用什么模块commonjs还是ESM,项目用什么框架,代码跑在浏览器还是node等等
- To check syntax, find problems, and enforce code style (可以检查句法,找到问题,代码格式)
npx eslint --init
3、解析用户参数
-
commander
yarn add commander -D
4、 询问用户信息
使用inquirer库
5、下载模板
1、可定义一个缓存的存储路径,第一次下载的时候,下载到缓存路径中, 如C:\Users\QWK\q-cli\template\node_modules 【下载模板】
2、如果缓存路径模板存在,则更新模板,即更新package.json信息以及对应模板文件的内容信息 【更新模板】
- ejs渲染模板
3、把缓存的存储路径的模板拷贝到当前目录下 【安装模板】
- copySync方法:复制文件或目录
创建项目
1、拉取所有的项目列出来,让用户去选择安装哪个项目
2、 选完之后,在显示所有的版本号
3、 可能需要用户配置一些数据,来结合模板渲染
4、创建模板文件
获取github用户下的仓库 【可以创建组织下的仓库,然后获取组织下仓库的所有项目】
- 去看github的api文档,里面是有介绍如何获取用户仓库或者组织仓库的所有项目信息
- 可以使用axios请求库,去请求仓库的项目列表
获取loading状态,如果下载失败,把loading显示失败状态: 使用ora库
选择模板项目:使用inquirer库
5、
- 在创建目录的时候,需要思考一个问题:目录是否已经存在?
-
如果存在
- 当
{ force: true }时,直接移除原来的目录,直接创建 - 当
{ force: false }时 询问用户是否需要覆盖
- 当
-
如果不存在,直接创建
- 这里用到了 fs 的扩展工具 fs-extra,先来安装一下
模板管理
- download-git-repo: 在git中下载模板
- 利用npm包,把npm包下载到本地,利用这个包的能力,来初始化项目
- 利用npx: 本质上调用子脚手架的思路
download-git-repo
利用npx
- 如:npx react-templace my-templace
上面代码运行时,npx 将 react-templace下载到一个临时目录,使用以后再删除。所以,以后再次执行上面的命令,会重新下载 react-templace。通过 @ 我们还可以指定模块版本:
- npx react-templace@x.x.x my-templace
模板渲染
metalsmith库
模板引擎:consolidate,ejs
例子
获取github仓库模板:
- 获取github用户下的仓库 【可以创建组织下的仓库,然后获取组织下仓库的所有项目】
- 去看github的api文档,里面是有介绍如何获取用户仓库或者组织仓库的所有项目信息
- 可以使用axios请求库,去请求仓库的项目列表
下载模板:
- download-git-repo库
把模板拷贝到当前脚手架目录中:
- 下载完模板之后,把模板拷贝到当前工作目录中,使用ncp库
模板渲染:
- metalsmith库、consolidate, ejs
const axios = require('axios');
// create的逻辑
/**
* 创建项目
* 拉取所有的项目列出来,让用户去选择安装哪个项目
* 选完之后,在显示所有的版本号
* 可能需要用户配置一些数据,来结合模板渲染
* 创建模板文件
*/
/**
* 获取github用户下的仓库 【可以创建组织下的仓库,然后获取组织下仓库的所有项目】
* 去看github的api文档,里面是有介绍如何获取用户仓库或者组织仓库的所有项目信息
* 可以使用axios请求库,去请求仓库的项目列表
*/
// 获取仓库的所有项目列表
const fetchRepoList = async () => {
// 请求仓库所有项目列表
const { data } = await axios.get('https://xxxxxxx');
return data;
};
// 获取项目的所有版本号
const fetchRepoTag = async () => {};
// 下载模板
// 封装loading
module.exports = async () => {
// 1.获取所有的项目模板
const repo = await fetchRepoList();
console.log(repo);
// 获取loading状态,如果下载失败,把loading显示失败状态: 使用ora库
// 选择模板项目:使用inquirer库
// 获取项目所有版本号
// 下载模板 (可做缓存)
/**
* 使用download-git-repo库
*下载模板后,把模板放在临时目录里,用于缓存,以备后期使用,不需要反复下载
*/
// 下载完模板之后,把模板拷贝到当前工作目录中,使用ncp库
// 模板编译渲染, 使用metalsmith
// 模板引擎,consolidate,ejs
// 让用户去填信息,去渲染模板
};
6、调试本地脚手架
- 在当前项目,npm link创建软链接
7、本地保存配置预设
- 像vue-cli一样,有.vuerc的json配置文件,保存用户之前的配置
- 被保存的preset将会存放在用户的home目录下
- 这样好处就是:
- 不用每次都重复配置脚手架
- 如果之前有保存预设配置,下次创建项目的时候,会直接读取预设的配置来生成项目
步骤:
- 1、是否保存这次配置的预设
8、输入命令有误,猜测用户意图
当我们输入命令有误的时候,理论是报错,但是为了用户体验,可以猜测用户的意图,给出命令正确提示,指引用户输入正确
-
主要是用到leven这个包,这是用于计算字符串编辑距离算法的 JS 实现
-
字符串编辑距离 实现测量两个字符串之间的差异
-
小例子:
const leven = require('leven');
// 如果不是当前已挂载的命令,会猜测用户意图
program.arguments('<command>').action(cmd => {
suggestCommands(cmd);
});
// 猜测用户意图
function suggestCommands(unknownCommand) {
const availableCommands = program.commands.map(cmd => cmd._name);
let suggestion;
availableCommands.forEach(cmd => {
const isBestMatch =
leven(cmd, unknownCommand) < leven(suggestion || '', unknownCommand);
if (leven(cmd, unknownCommand) < 3 && isBestMatch) {
suggestion = cmd;
}
});
if (suggestion) {
console.log(` ` + chalk.red(`Did you mean ${chalk.yellow(suggestion)}?`));
}
}
9、集成构建工具 【webpack、gulp】
参考:vue-cli源码
- vue-cli-dev\vue-cli-dev\packages@vue\cli-service
支持typescript
方式1:
- babel-loader + @babel/preset-typescript
方式2:
- ts-loader
常用到的库和插件
-
fs-extra:
1、可替代fs模块,继承了fs所有方法和为fs方法添加了promise的支持
2、操作文件系统用的,比如复制,移动、创建文件这些
3、特点:1、支持promise 2、基于fs扩展了好多操作文件方法
手写tool-cli脚手架,集成webpack打包工具
特点:
- webpack5现在打包速度也提升了不少
- 基于 webpack 构建,并带有合理的默认配置;
- 统一处理好webpack配置优化、打包优化等,用户无需重新配置
- 开发者可以做到零配置,就可以快速开发项目
- 暴露tool.config.js ,让用户可以自定义配置
- 通过配置脚手架tool.config.js配置webpack
思路:
-
1、在根目录下生成tool.config.js配置文件
-
2、tool.config.js的配置与webpack配置合并
-
3、可注册run命令,通过npx tool-cli run,命令运行,执行webpack配置
- run.js 【webpack沙箱环境启动】
import Webpack from "webpack";import devConfig from "../lib/webpack/webpack.dev";import WebpackDevServer from "webpack-dev-server";import buildConfig from "../lib/webpack/webpack.prod";import Generator from "../utils/Generator";import path from "path";import { get } from "lodash";type actionNameType = "dev" | "build";async function runDev() {const generator = new Generator();generator.generatorToolConfig();// 获取q.tool.config配置文件 devParam.devServer配置合并const toolPath = path.join(process.cwd(), generator.files.toolConfig);const toolConfigJSON = await import(toolPath);const { devParam } = toolConfigJSON;const compiler = Webpack(devConfig);const devServerConfig = {hot: true,open: true,host: "localhost",port: 8000,...get(devParam, "devServer", {}),};const devServer = new WebpackDevServer(devServerConfig, compiler);devServer.startCallback(() => {console.log("启动中...");});}function runBuild() {const compiler = Webpack(buildConfig);compiler.run((err, stats) => {stats.toJson("minimal");if (err) console.log(err);if (stats.hasErrors()) {console.log(new Error("Build failed with errors."));}if (stats.hasWarnings()) {console.warn(stats.hasWarnings());}});}export default (actionName: actionNameType, options) => {const actionMap = {dev: () => {runDev();},build: () => {runBuild();},};if (Object.keys(actionMap).includes(actionName)) {actionMap[actionName]();} else {console.log("error command");}};tool.config.js配置
export default = {//开发环境参数配置devParam:{devServer:{}},// 生产环境参数配置buildParam:{}// webpack配置webpack:(config)=>{console.log(config)}}
10、集成eslint、perttier
- 使用js-yam库来转化文件 ,yaml转json, json转yaml
- yaml-front-matter基于js-yaml
yaml-front-matter使用
- loadFront
- 将字符串解析为单个 YAML 文档。返回一个普通对象、一个字符串、一个数字、null 或 undefined
eslint
eslint配置文件:优先级 => eslintrc.js、eslintrc.yaml、eslintrc.yml、eslintrc.json
- extends
1、extends它将扩展和覆盖设置解析并合并到配置中
2、在node中,可以使用require.resolve来查询某个模块的完整路径 【require.resolve模块解析】
module.exports = {
extends:[require.resolve('./base')]
}
配置转化实现
const stringifyJS = require('./stringifyJS')
const merge = require('deepmerge')
const mergeArrayWithDedupe = (a, b) => Array.from(new Set([...a, ...b]))
const mergeOptions = {
arrayMerge: mergeArrayWithDedupe,
}
const transformJS = {
read: ({ filename, context }) => {
try {
return require(`./${filename}`, context, true)
} catch (e) {
return null
}
},
write: ({ value }) => `module.exports = ${stringifyJS(value, null, 4)}`,
}
const transformJSON = {
read: ({ source }) => JSON.parse(source),
write: ({ value, existing }) => JSON.stringify(merge(existing, value, mergeOptions), null, 4),
}
const transformYAML = {
read: ({ source }) => require('js-yaml').safeLoad(source),
write: ({ value, existing }) => require('js-yaml').safeDump(merge(existing, value, mergeOptions), {
skipInvalid: true,
}),
}
const transformLines = {
read: ({ source }) => source.split('\n'),
write: ({ value, existing }) => {
if (existing) {
value = existing.concat(value)
// Dedupe
value = value.filter((item, index) => value.indexOf(item) === index)
}
return value.join('\n')
},
}
module.exports = {
js: transformJS,
json: transformJSON,
yaml: transformYAML,
lines: transformLines,
}
11、下载远程模板 【react、vue】
react模板:【相关库、插件】
- react、react-dom
vue模板:【相关库、插件】
参考:https://juejin.cn/post/6978832288586924046
- vue
- vue-loader:
- 解析和转换.vue文件。提取出其中的逻辑代码 script,样式代码style,以及HTML 模板template,再分别把他们交给对应的loader去处理
- @vue/compiler-sfc
- 将解析完的vue单页面组件(sfc)编译为js
处理css
- css-loader:用于处理 .css 文件
- style-loader:将样式通过
<style>标签的形式 挂载到页面的 head 部分
关键代码:
import download from "download-git-repo";
import ora from "ora";
function downloadTemplate(dir, branch = "react") {
const downloadLoading = ora("正在下载模板~").start();
download(
`direct:https://github.com/qiuweikangdev/cli-template.git#${branch}`,
dir,
{ clone: true },
(err) => {
if (!err) {
downloadLoading.stop();
console.log("下载成功");
}
}
);
}
12、渲染模板文件
ejs渲染引擎使用
-
可通过使用ejs渲染引擎渲染模板文件
-
用来将
EJS模版结合着JSON数据转换为HTML
基本语法:
- 模板语法:
- <%= %> : 将模板输出到html页面
- <% %> : 一般用于控制语句和声明语句,比如:if…else,switch,for,var声明等
- <%% :输出<%
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= projectname %></title>
</head>
<body>
<div>
<%= projectname %>
</div>
</body>
</html>
- render
- 将字符串作为模板,是一种同步方法
const content = ejs.render(str,{
projectname:'xxx'
}, options)
- renderFile
filename参数是目标文件路径data是你要传进模板的数据对象option是配置选项, 可以省略, 具体内容查看文档function(err, str){}是回调函数.err是错误信息, 没有错误时为null.str是模板生成的 HTML 内容.
ejs.renderFile(filename, {
projectname:'xxx'
}, options, function(err, str){});
13、 发布脚手架
1、npm公开包的名称需要是唯一的。
2、指定官方源,确保能发包成功
- package.json的
publishConfig字段指定官方源- registry 源地址
- tag 发布对应的
dist-tag标签 - access 如果是
scoped包,一定需要设置为public(付费账号除外)
因为淘宝源只能下载包,不能上传包。但是不用淘宝源,安装其他依赖又慢的不行。 解决方法: 安装依赖,统一从淘宝源拉,保证依赖安装速度。发包时借助 npm包的package.json的
publishConfig字段指定官方源,确保能发包成功
"publishConfig": {
"registry": "http://registry.npmjs.org/", //发布到npm官网上,不是淘宝源
"access": "public"
},
流行脚手架源码分析 【vue-cli、create-react-app】
@vue/cli功能主要是三部分组成的:
@vue/cli负责命令行参数收集@vue/cli-service这个包是启动vue的引擎及核心,承载webpack配置。@vue/plugin-xxxvue-cli的插件,一个插件对应一个功能,与上边用户自定义feature选项一一对应。
脚手架命令注册和参数解析
实现参数解析:--version
命令注册
- process对象 是node提供的进程信息并对其进行控制
process.argv : 属性返回一个数组,其中包含当启动 Node.js 进程时传入的命令行参数
- 第一个元素是node进程
- 第二个元素将是正在执行的 JavaScript 脚本文件的路径
- 其余元素将是任何其他命令行参数
- 注册一个命令 init , 如:my-cli init
方式一:使用node提供的process对象
方式二:使用commander库,其作用是让node命令行程序的制作更加简单