跳到主要内容

基于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

    bash
    $ mkdir my-cli
    $ cd my-cli
    $ npm init # 生成 package.json 文件
  • 2.新建程序入口文件bin/index.js

    bash
    mkdir bin
    touch bin/index.js # 新建 index.js文件
    • index.js
    js
    #! /usr/bin/env node

    // #! 符号的名称叫 Shebang,用于指定脚本的解释程序
    // Node CLI 应用入口文件必须要有这样的文件头
    // 如果是Linux 或者 macOS 系统下还需要修改此文件的读写权限为 755
    // 具体就是通过 chmod 755 cli.js 实现修改

    // 用于检查入口文件是否正常执行
    console.log('my-cli ~');
  • 3.在 package.json 文件中指定入口文件为 cli.js

    json
    {
    "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去执行你的脚本文件

bash
npm link # or yarn link

2、配置规范​

eslint、perttier

eslint配置:

  • 安装:

    bash
    yarn add eslint
  • 执行eslint配置文件,可以生成eslint配置

    • 执行这个命令,eslint会进行询问问题,如询问你想要的代码风格,用什么模块commonjs还是ESM,项目用什么框架,代码跑在浏览器还是node等等
    • To check syntax, find problems, and enforce code style (可以检查句法,找到问题,代码格式)
    bash
    npx eslint --init

3、解析用户参数​

  • commander

    bash
    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、

  • 在创建目录的时候,需要思考一个问题:目录是否已经存在?
  1. 如果存在

    • 当 { force: true } 时,直接移除原来的目录,直接创建
    • 当 { force: false } 时 询问用户是否需要覆盖
  2. 如果不存在,直接创建

  • 这里用到了 fs 的扩展工具 fs-extra,先来安装一下

模板管理​

  • download-git-repo: 在git中下载模板
  • 利用npm包,把npm包下载到本地,利用这个包的能力,来初始化项目
  • 利用npx: 本质上调用子脚手架的思路

download-git-repo

利用npx

  • 如:npx react-templace my-templace

上面代码运行时,npx 将 react-templace下载到一个临时目录,使用以后再删除。所以,以后再次执行上面的命令,会重新下载 react-templace。通过 @ 我们还可以指定模块版本:

模板渲染​

metalsmith库

模板引擎:consolidate,ejs

例子​

获取github仓库模板:

  • 获取github用户下的仓库 【可以创建组织下的仓库,然后获取组织下仓库的所有项目】
  • 去看github的api文档,里面是有介绍如何获取用户仓库或者组织仓库的所有项目信息
  • 可以使用axios请求库,去请求仓库的项目列表

下载模板:

  • download-git-repo库

把模板拷贝到当前脚手架目录中:

  • 下载完模板之后,把模板拷贝到当前工作目录中,使用ncp库

模板渲染:

  • metalsmith库、consolidate, ejs
js
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 实现

小例子:

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沙箱环境启动】
    js
    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配置

    text
    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模块解析】

json
module.exports = {
extends:[require.resolve('./base')]
}

配置转化实现​

js
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 部分

关键代码:

js
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

文档:https://ejs.bootcss.com/

基本语法:

  • 模板语法:
    • <%= %> : 将模板输出到html页面
    • <% %> : 一般用于控制语句和声明语句,比如:if…else,switch,for,var声明等
    • <%% :输出<%
html
<!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
    • 将字符串作为模板,是一种同步方法
js
const content = ejs.render(str,{
projectname:'xxx'
}, options)
  • renderFile
    • filename 参数是目标文件路径
    • data 是你要传进模板的数据对象
    • option 是配置选项, 可以省略, 具体内容查看文档
    • function(err, str){} 是回调函数.
    • err 是错误信息, 没有错误时为 null.
    • str 是模板生成的 HTML 内容.
js
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字段指定官方源,确保能发包成功

js
"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-xxx vue-cli的插件,一个插件对应一个功能,与上边用户自定义feature选项一一对应。

脚手架命令注册和参数解析​

实现参数解析:--version

命令注册​

  • process对象 是node提供的进程信息并对其进行控制

process.argv : 属性返回一个数组,其中包含当启动 Node.js 进程时传入的命令行参数

  • 第一个元素是node进程
  • 第二个元素将是正在执行的 JavaScript 脚本文件的路径
  • 其余元素将是任何其他命令行参数
  • 注册一个命令 init , 如:my-cli init

方式一:使用node提供的process对象

方式二:使用commander库,其作用是让node命令行程序的制作更加简单