San CLI 是一个命令行工具,其次它是一个内置 Webpack 的前端工程化构建工具。San CLI 在架构设计上采取了微核心和插件化的设计思想,我们可以通过插件机制添加命令行命令,还可以通过插件机制定制 Webpack 构建工具,从而满足不同 San 环境的前端工程化需求。
San CLI 在兼顾 San 生态的同时,尽量做到通用化配置,在设计之初,我们希望不局限于 San 的应用范畴,做可定制化的前端开发工具集。
下面分别从模块、命令行实现、脚手架和插件机制四大方面来介绍下 San CLI 的内部实现。
San CLI 的核心模块包含:
- san-cli:核心模块,负责整合整个工作流程和实现核心功能
- san-cli-utils:工具类
- san-cli-service:service 层
- san-cli-webpack:webpack build 和 dev-server 通用逻辑和 webpack 自研插件等
- san-cli-command-init:init 命令,脚手架
- san-loader:
.san
文件 webpack loader - san-hot-loader:给 san 组件添加 HMR 功能
- san-cli-plugin-*:对应 service 的 plugin
- san-cli-docit:一个方便编写组件文档和预览的小工具,可做建站工具,需要的模块包括:
- san-cli-markdown-loader:markdown-loader
- san-cli-docit-theme:docit 皮肤
结合模块的主流程可以如下图所示:
utils 中用的最多的是ttyLogger.js
中跟 tty 输出相关的函数,常见的有:
- ora
- chalk
- logger
- log
- debug
- info
- done/success
- warning/warn
- error
- fatal
- time/timeEnd:用于检测时间段耗时,需要配合
DEBUG=san-cli:perf
环境变量使用
San CLI 中的 logger 是通过自定义的 Consola Reporter 实现的,在插件中也可以直接调用这些方法使用。
如果要使用彩色突出显示 San CLI 的终端内容,强烈建议使用
randomColor.js
中的textColor
和bgColor
两个方法。
为了方便 Webpack 打包命令和 dev-server 相关的代码逻辑复用,我们将build
和serve
用到的两个方法进行了统一封装。这俩方法是promisify
的。除此之外该模块还包含了下面 Webpack 相关插件:
lib/formatStats.js
:在build
之后分析Stats
对象,在终端中输出分析结果;lib/HTMLPlugin.js
:html-webpack-plugin 的插件,给 html 页面增加打包后的 bundle 和在 head 中增加preload
和prefetch
的meta
;(主要增加对 smarty 的支持);lib/ModernModePlugin.js
:modern mode 打包插件;lib/SanFriendlyErrorsPlugin.js
:扩展 friendly-errors-webpack-plugin 的错误类型,统一终端 log。
另外utils.js
里面有一些工具函数可能在二次开发中会用得到。
为了方便理解下面的内容,在介绍 San CLI 的工作流程之前,先介绍下 San CLI 的核心概念:
- 流程:CLI 的流程分为两段,主流程和 Service 流程;
- 主流程:
index.js
的流程,是整个 CLI 的工作流程,如果有自定义的 command,则会执行对应的 handler;如果主流程没有相关命令,则会走到default
,default
会实例化 Service,进入 Service 流程; - Service 流程:CLI 的 Service 层设计,主要进行 Webpack 构建相关的处理逻辑;可以通过 Service 插件的
api.registerCommand
方法注册 Service 流程的命令; - P.S:
build
、serve
、inspect
都是走的 Service 流程。
- 主流程:
- Command:指的是通过yargs创建的命令行 bin 工具,它可以通过
.sanrc
的commands
字段对命令进行扩展; - Command 插件:指通过给 Command 添加自定义命令的方式,添加 Command 插件,这样的插件可以使用
san your_command_name [options]
方式在主流程触发; - Service:CLI 的 Service 层设计,主要进行 Webpack 构建相关的处理逻辑;
- Service 插件:Service 层的插件。
主流程通过 command handler 触发 Service 流程,如果存在对应的 command(通过.sanrc
扩展) 则会直接在主流程中执行对应的 handle。
San CLI 的命令行使用了yargs。在lib/commander.js
中,创建一个 yargs 实例,通过中间件机制添加了常用的方法和属性到argv
对象中,方便下游 handler 直接使用。
整个 CLI 的工作流程在index.js
中,大致流程如下:
- 检查 node 版本;
- 添加最新版本检查器;
- 调用
lib/command.js
创建 Command 实例 cli:- 添加全局 option
- 添加中间件:
- 设置全局
logLevel
- 设置
NODE_ENV
环境变量 - 给
argv
添加日志等属性方法
- 设置全局
- 加载内部命令:
init
、build
、serve
、inspect
和default
:default
中定义没被直接定义的命令会走 Service 层的 Command 实现default
中会实例化 Service,然后执行Service.run(commandName, argv)
- 加载
.sanrc
文件- 添加
.sanrc
中的 command,实现 CLI 的命令行插件 - 将
.sanrc
中跟 Service 相关配置通过 Command 中间件添加到argv
对象
- 添加
- 触发
process.argv
解析执行,开始 CLI 的正式执行。
项目脚手架初始化是在san-cli-command-init
中实现的,原理是通过 git 命令拉取对应 github/icode/gitlab 等脚手架模板的 repo 到本地,然后使用vinyl-fs将依次将文件进行处理后生成项目代码。
san-cli-command-init
的核心是一个TaskList
类,通过四步串行任务完成:
- 检查目录和离线包状态:检查模板的本地路径和离线包是否可用;
- 下载项目脚手架模板:从 github 等线上下载模板到 user-home 的模板缓存路径;
- 生成项目目录结构:使用
vinyl-fs
把模板从缓存目录遍历处理到对应的项目目录; - 安装项目依赖:询问是否安装
package.json
的依赖。
San CLI 支持 Command 插件和 Service 插件。
San CLI 的命令行插件值得是通过配置.sanrc
的commands
字段,给 CLI 添加自定义 Command,这里添加的 Command 可以通过san your_command_name [options]
方式使用。
Command 的插件需要遵循 yargs command module 规范,即按照下面的写法:
// Commander 定义
// name
exports.command = 'your_command_name [your_option]';
// description
exports.description = 'command description';
// options
exports.builder = {
option1: {
default: true,
type: 'boolean'
}
};
// handler 接收 commanderAPI 实例 cliAPI
exports.handler = cliAPI => {
console.log(`setting ${cliAPI.key} to ${cliAPI.value}`);
console.log(cliAPI.getPresets());
};
San CLI 在实现可扩展 Webpack 配置的设计上,借鉴了 Vue CLI 的 Service 机制。
Service 的使用方式如下:
const service = new Service(name, {
// cwd 目录
cwd,
// config 文件路径
configFile,
// 是否 watch
watch,
// mode production/development
mode,
// 是否使用内置 Plugin
useBuiltInPlugin,
// 项目配置,这里是从 sanrc 读取内容传入
// 优先级比 san.config.js 低
projectOptions,
// 传入的插件 list
plugins,
// 是否使用 progress
useProgress,
// 是否使用Profiler
useProfiler
});
// 开始执行,执行结果回调,callback 传入 PluginAPI 实例
service.run(callback);
现在以san serve
命令执行流程为例,讲解下整个工作流程:
- 首先 CLI 通过主流程的 Command 解析 bin 命令,进入
commands/serve
的 handler; - handler 主要是实例化 Service,实例化会将配置项和插件进行处理
- 然后执行
service.run(callback)
,进入 service 流程,这部分代码主要在service.run
中:loadEnv
:加载 env 文件;loadProjectOptions
:加载san.config.js
;init
:service 启动:- 初始化插件,并且依次执行;
- 依次执行 webpackChain 回调栈;
- 依次执行 webpackConfig 回调栈;
- 执行
callback
。
webpackChain 回调栈存储的是接收webpack-chain格式的 webpack 配置文件的处理函数; webpackConfig 回调栈存储的是接受普通 webpack 配置文件对象的处理函数。 P.S:handler 中可以通过 service 插件的 API 获取最终的 webpack config,然后结合
san-cli-webpack
的build
/serve
执行对应的打包操作。
插件的定义方法如下:
module.exports = {
id: 'plugin-id',
apply(api, projectOptions, pluginOptions) {
api.chainWebpack(webpackConfig => {
console.log(projectOptions);
webpackConfig.entry(/*...*/);
});
},
ui() {}
};
属性:
.id
:插件 id;.service
:service 实例;.log/logger
:日志对象,包含 debug/done/error/warn/log/fatal/trace/time/timeEnd/textColor/bgColor 等;.version
:San CLI 版本号。
常见方法包括:
.isProd()
:是不是生产环境打包,process.NODD_ENV==='production'
;.registerCommand(name, handler)
:注册 command 命令,实例化 Service 之后执行service.run(command, argv)
触发;.configWebpack(fn)
:将fn
压入 webpackConfig 回调栈,fn
会在出栈执行时接收 webpackConfig,用于修改 webpack config;.chainWebpack(fn)
:将fn
压入 webpackChain 回调栈,fn
会在出栈执行时接收 chainableConfig,用于 webpack-chain 语法修改 webpack config;.resolve(p)
:获取 CLI 执行目录的完整路径;.getWebpackChainConfig()
:获取 webpack-chain 格式的 config;.getWebpackConfig([chainableConfig])
:将传入的 webpack-chain 格式 config 处理成 webpackConfig 返回;.getCwd()
:获取 CLI 的执行目录;.getProjectOptions()
:获取项目的配置内容;.getVersion()
:获取 CLI 版本;.getPkg()
:获取当前项目package.json
内容;.addPlugin(plugin, options)
:添加插件;.middleware()
:添加 dev-server 中间件,这里注意:中间件需要使用 factory 函数返回
.middleware()
示例:
api.middleware(() =>
// return 一个 Expressjs 中间件
require('hulk-mock-server')({
contentBase: path.join(__dirname, './' + outputDir + '/'),
rootDir: path.join(__dirname, './mock'),
processors: [
`smarty?router=/template/*&baseDir=${path.join(__dirname, `./${outputDir}/template`)}&dataDir=${path.join(
__dirname,
'./mock/_data_'
)}`
] // eslint-disable-line
})
);
P.S:Service 是继承
EventEmitter
的,具有事件机制,不过目前还没有使用,sad~。