自己完成一个脚手架项目
项目痛点
- 创建项目+通用代码(埋点、HTTP请求、工具方法、组件局)
- git操作(创建仓库、代码冲突、远程代码同步、创建版本、发布打tag)
- 构建+发布上线(依赖安装和构建、资源上传CDN、域名绑定、测试/正式服务器)
痛点分析
- 创建项目/组件时,存在大量重复代码拷贝:希望能够快速复用已有沉淀
- 协同开发时,由于git操作不规范,导致分支混乱,操作耗时:制定标准的git操作规范并集成到脚手架
- 发不上线耗时,而去容易出现各种错误:指定标准的上线流程和规范并集成到脚手架
需求分析
- 通用的研发脚手架
- 通用的项目/组件创建能力
- 模板支持定制,定制后能够发布生效
- 模板支持快速接入,级低的接入成本
- 通用的项目/组件发布能力
- 发布过程自动完成标准的git操作
- 发布成功后自动删除开发分支并创建tag
- 发布后自动完成云构建、OSS上传、CDN上传、域名绑定
- 发布过程支持测试/正式两种模式
技术方案设计
core模块技术方案
- 准备阶段
- 命令注册
- 命令执行
core
- prepare:检查版本号、检查node版本、检查root启动、检查用户主目录、检查入参、检查环境变量、检查是否为最新版本、提示更新
- registerCommand:注册init命令、注册publish命令、注册clean命令、支持debug
- execCommand:
核心库
- import-local
- commander
工具库
- npmlog
- fs-extra
- semver
- colors
- user-home
- dotenv
- root-check
脚手架拆包策略:
- 核心流程:core
- 命令:
- 初始化
- 发布
- 清除缓存
- 模型层:models
- Command命令
- Project项目
- Component组件
- Npm模块
- Git仓库
- 支撑模块:utils
- Git操作
- 云构建
- 工具方法
- API请求
- Git API
- 拆分原则(根据模块的功能拆分)
- 核心模块
- 命令模块
- 模型模块
- 工具模块
core模块的搭建
搭建core/cli脚手架
脚手架命令:rainbow-cli
lerna.json
{
"packages": ["core/*", "commands/*", "models/*", "utils/*"],
"version": "0.0.0"
}
core/cli 脚手架的包
npm i npmlog处理日志打印npm i import-local检查如果本地的node_modules有脚手架的包则直接使用本地的,没有才使用全局的
遇到报错:core/core/node_modules没有写入权限
解决:把node_modules删掉
定义项目目录结构
- core
- cli(脚手架项目)
- bin
- lib
- cli(脚手架项目)
- utils(工具类包)
- utils
- lib
- command
- models
调试到能正常运行:
- 使用
import-local库
core/cli/bin/index.js文件:测试import-local库
#!/usr/bin/env node
"use strict";
const importLocal = require("import-local");
if (importLocal(__filename)) {
require("npmlog").info("cli", "正在使用rainbow-cli本地的脚手架~");
} else {
require("../lib")(); //正常是执行了else里面的语句
}
测试rainbow-cli脚手架项目的import_local库是否生效:
- 在
rainbow-lerna-cli最外层npm i @rainbow-lerna-cli/core - 执行了
import-local里面的内容,说明生效了
新建utils/log模块: 用户定义日志输出类型
- 模块名称:
@rainbow-lerna-cli/log
新建lerna create log utils
info cli using local version of lerna
lerna notice cli v4.0.0
package name: (log) @rainbow-lerna-cli/log
version: (1.1.2) 0.0.1
description: @rainbow-lerna-cli log
keywords: log
homepage: (https://gitee.com/rainbowdiary/rainbow-lerna-cli/utils/log)
license: (ISC)
entry point: (lib/log.js) lib/index.js
git repository: (https://gitee.com/rainbowdiary/rainbow-lerna-cli)
About to write to /Users/rainbow/Documents/前端/脚手架开发/rainbow-lerna-cli/utils/log/package.json:
{
"name": "@rainbow-lerna-cli/log",
"version": "0.0.1",
"description": "@rainbow-lerna-cli log",
"keywords": [
"log"
],
"author": "rainbowdiary <zengcai666@gmail.com>",
"homepage": "https://gitee.com/rainbowdiary/rainbow-lerna-cli/utils/log",
"license": "ISC",
"main": "lib/index.js",
"directories": {
"lib": "lib",
"test": "__tests__"
},
"files": [
"lib"
],
"publishConfig": {
"registry": "https://registry.npmjs.org"
},
"repository": {
"type": "git",
"url": "https://gitee.com/rainbowdiary/rainbow-lerna-cli"
},
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1"
}
}
Is this OK? (yes) yes
lerna success create New package @rainbow-lerna-cli/log created at ./../../utils/log
lerna exec --scope @rainbow-lerna-cli/log npm i npmlog
在npmlog库的基础上自定义日志级别
添加core/cli/package.json本地依赖@rainbow-lerna-cli/log
lerna link
//完成本地`@rainbow-lerna-cli/log`的调试
"dependencies": {
"@rainbow-lerna-cli/log": "^0.0.1",
}
根据环境变量自定义log等级
npmlog库的功能:(查看npmlog源码或文档)
log.level- log.level定义log的等级。默认是info,info等级值之前的都不会生效
- 使用
process.env.LOG_LEVEL可以判断环境变量中的debug模式 -log.addLevel('success',2000,{fg: 'green', bold: true})自定义log
log.headingStylelog加上前缀
utils/log/lib/index.js
"use strict";
const log = require("npmlog");
module.exports = log;
log.level = process.env.LOG_LEVEL ? process.env.LOG_LEVEL : "info"; //根据环境变量定义log等级
log.addLevel("success", 2000, { fg: "green", bold: true }); //自定义log等级
log.heading = "rainbow"; //添加log前缀
log.headingStyle = { fg: "blue", bg: "black" }; //log前缀样式
本地调试依赖的写法
不然后面npm i会报错
"dependencies": {
"@rainbow-lerna-cli/log": "file:../../utils/log",
"colors": "^1.4.0",
"import-local": "^3.0.3",
"npmlog": "^5.0.1",
"root-check": "^2.0.0",
"semver": "^7.3.5"
}
检查版本号: checkPkgVersion
思路:拿到pakcage.json文件中的当前的version,和最低版本号lowestPkgVersion进行对比。如果当前版本号低于lowestPkgVersion则提示安装。
function checkPkgVersion() {
const currentPkgVersion = pkgVersion.version;
const lowestPkgVersion = constants.LOWEST_PKG_VERSION;
// 版本大于等于
if (!semver.gte(currentPkgVersion, lowestPkgVersion)) {
throw new Error(
`rainbow-cli suggest version higher than v${lowestPkgVersion}, Please update your version of rainbow-cli`
);
} else {
log.info("cli", currentPkgVersion);
}
}
定义core/cli/const.js文件,定义lowestPkgVersion:
module.exports = {
LOWEST_PKG_VERSION: "1.0.0",
};
- 小知识点: require支持什么文件
- .js/.json/.node
- .json文件,会JSON.parse输出
- .js文件则直接加载
- .其他文件,如果是js的代码,也是默认使用js解析器进行解析
- 如果不是js代码则会报错
检查node版本: checkNodeVersion
- 使用
semverlerna add semver --scope @rainbow-lerna-cli/core
- colors库
- 打印不同颜色
// 检查node版本号
function checkNodeVersion() {
const currentVersion = process.version;
const lowestVersion = constants.LOWEST_VERSION;
if (!semver.gte(currentVersion, lowestVersion)) {
throw new Error(
colors.red(
`rainbow-cli requires Node higher than v${lowestVersion}, Please update your version of Node`
)
);
}
}
}
查看终端效果:
看下lerna报错提示时库的使用:
检查root版本: checkRoot
- root-check@v1.0.0
function checkRoot() {
//如果一个文件是root账户创建的,那么普通用户是无法操作的,所以会报错各种权限问题
//修改uid,账户降级
const rootCheck = require("root-check");
rootCheck();
}
使用root-check之后使用sudo则会被降级处理
检查用户主目录: checkUserHome
user-home@v2.0.0path-exists直接调用会返回用户目录路径,需要判断这个路径是否存在
// 检查用户主目录
function checkUserHome() {
const userHome = require("user-home");
const pathExists = require("path-exists").sync;
if (!userHome || pathExists(userHome)) {
throw new Error(colors.red("当前登录用户主目录不存在!"));
}
}
检查入参: checkInputArgv
- minimist:拿到所有的argv;存入变量
argvs查看是否为debug模式rainbow-cli --debug
let argvs = {};
// 检查入参
function checkInputArgv() {
const minimist = require("minimist");
argvs = minimist(process.argv.slice(2));
}
检查是否有debug参数
// 如果为debug模式则设置log等级为verb
function checkArgv() {
if (argvs.debug) {
process.env.LOG_LEVEL = "verbose";
} else {
process.env.LOG_LEVEL = "info";
}
log.level = process.env.LOG_LEVEL;
// log.verbose("debug", "test debug"); //debug日志就可以打印出来了
}
检查变量: checkEnv
dotenv库- 将环境变量所在文件设置为用户主目录
userHome+.rainbow-cli-env
/**
* 定义常量
*/
module.exports = {
LOWEST_VERSION: "12.0.0",
LOWEST_PKG_VERSION: "1.0.0",
DEFAULT_CLI_HOME: ".rainbow-cli-env",
};
// 检查环境变量有哪些配置
function checkEnv() {
// 默认查看当前目录下的.env文件
const dotenv = require("dotenv");
const dotEnvPath = path.resolve(userHome, ".env");
let config = {};
if (pathExists(dotEnvPath)) {
dotenv.config({
path: dotEnvPath,
});
}
config = createDefaultCliPath();
log.verbose("环境变量", process.env.CLI_HOME_PATH);
}
function createDefaultCliPath() {
const cliConfig = {
home: userHome,
};
if (process.env.CLI_HOME) {
cliConfig["cliHome"] = path.join(userHome, rocess.env.CLI_HOME);
} else {
cliConfig["cliHome"] = path.join(userHome, DEFAULT_CLI_HOME);
}
process.env.CLI_HOME_PATH = cliConfig.cliHome;
}
检查版本更新: checkGlobalUpdate
utils/npm-info封装npm信息获取方法getNpmSemverVersions- 如果当前脚手架在npm平台发布了新版本,但是用户用的仍然是旧版本,需要提示更新新版本
async function checkGlobalUpdate() {
//使用npm接口拿到所有的版本号 https://registry.npmjs.org/@rainbow-lerna-cli/core
// 比对版本号,提示更新最新的版本号
const npmName = pkgVersion.name;
const currentPkgVersion = pkgVersion.version;
const lastVersion = await getNpmSemverVersions(currentPkgVersion, npmName);
if (lastVersion && semver.gt(lastVersion, currentPkgVersion)) {
log.warn(
colors.yellow(
`请手动更新 ${npmName},当前版本:${currentPkgVersion}, 最新版本:${lastVersion}
更新命令: npm install -g @rainbow-lerna-cli/core
`
)
);
}
}
脚手架命令注册
node支持ES Module
如何让node支持ES MODULE
模块化
- CMD/AMD/require.js
- CommonJS
- 加载: require()
- 输出module.exports / exports.x
- ES Module
- 加载:import
- 输出:export default / export function
模块化方案
commonJs nodejs实现了commonJs规范
- 特点
- 一个文件就是一个模块,拥有独立作用域
- 提供require引入模块,提供export导出模块属性方法,exports代表模块本身
- 暴露和加载模块
- module.exports和exports
- require(),用于加载模块
AMD Asynchronous Modeule Definition 异步模块加载机制
-
代表有RequireJs
-
特点
- 所有模块都进异步加载,模块加载不影响后面语句运行
- 所有依赖某些模块的语句均放在回调函数中
- 提供全局define函数定义模块,require引入模块,export导出模块
- define([module-name?],[array-of-dependencies?],[module-factory-or-object]); 其中: module-name: 模块标识,可以省略 array-of-dependencies: 所依赖的模块,可以省略 module-factory-or-object: 模块的实现,或者一个js对象
-
define是异步的
- 首先会异步的调用第二个参数中列出的依赖模块
- 所有模块被载入完成后,如果第三个参数是一个回调函数则执行
CMD规范 Common Module Definition通用模块定义
- seajs
- define(function(require,exports,module){...})
- factory是一个函数,提供equire, exports, module三个参数: 1、require 是一个方法,接受模块标识作为唯一参数,用来获取其他模块提供的接口:require(id) 2、exports 是一个对象,用来向外提供模块接口 3、module 是一个对象,上面存储了与当前模块相关联的一些属性和方法
ES6 import
在rainbow-test项目中新建lib/utils.js
//使用nodeJS的方式输出
module.exports = function (){
console.log('hello utils');
}
- 新建项目执行文件
index.js - 正常只能使用CommonJS的方式加载
#!/usr/bin/env node
const utils = require('./lib/utils');
utils();
但是希望支持ES MODULE如果不支持则会报错
#!/usr/bin/env node
import utils from './lib/utils';
utils();
所以需要将下面这段代码给webpack编译成node能识别的代码
import utils from './lib/utils';
utils();
方法1:使用webpack实现
将ES MODULE使用import方式加载的代码,转换为webpack打包后的代码,webpack编译后能让低版本的node也能识别编译后的代码
npm i webpack webpack-cliwebpack.config.js
const path = require("path")
module.exports = {
entry: "./bin/core.js",
output: {
path: path.join(__dirname, "/dist"),
filename: "core.js",
},
mode: "development",
};
core.js
import utils from '../lib/utils';
utils();
bin/index.js
#!/usr/bin/env node
require("../dist/core")
package.json
"bin": {
"rainbow-test": "bin/index.js"
},
"scripts": {
"build": "webpack",
"dev": "webpack -w"
}
问题
问题1:webpack无法编译nodeJs内置的方法。
解决:
修改
webpack.config.js中的target配置,默认是‘web’
问题2:复杂代码无法编译,比如async代码,webpack编译后,低版本node无法识别
查看
dist/core.jsasync相关代码都没有被编译
**解决:**使用babel-loader
npm i -D babel-loader @babel/core @babel/preset-env
webpack.config.js
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
},
],
},
重新npm run build查看dist/core.js 已经将async进行babel处理了。
但是运行还是报错:ReferenceError: regeneratorRuntime is not defined
原因:编译async await形成的代码,是以垫片的形式加入,垫片代码没有引入
npm i -D @babel/plugin-transform-runtime
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
plugins: ["@babel/plugin-transform-runtime"],
},
},
},
]
}
重新编译后:一直报错:
原因: 一直以为是webpack配置问题,最后查明是有行代码没有使用分号结尾。
import path from 'path'; //使用node内置方法
import {exists} from '../lib/utils';
// 加上这行就报错: TypeError: _lib_utils__WEBPACK_IMPORTED_MODULE_5___default(...).exists(...) is not a function
utils.exists(path.resolve('./'))
(async ()=>{
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log('ok');
})()
原因: JS解析问题。 通常,如果语句以(、[、 /、+、-、`开头时,就有可能被解释为上一行语句的一部分。 所以webpack编译的时候也将他们编译成为一体。
// 看下webpack解析结果
(0,_lib_utils__WEBPACK_IMPORTED_MODULE_6__.exists)(path__WEBPACK_IMPORTED_MODULE_5___default().resolve('./'))( /*#__PURE__*/(0,_babel_runtime_corejs3_helpers_esm_asyncToGenerator__WEBPACK_IMPORTED_MODULE_0__[\"default\"])
// 报错:
TypeError: (0 , _lib_utils__WEBPACK_IMPORTED_MODULE_6__.exists)(...) is not a function
举个🌰:以下代码会被解析为let hey = "hey"["liu", "liuxing"].forEach(console.log)
let hey = "hey"
["liu", "liuxing"].forEach(console.log);
//webpack解析报错:
TypeError: Cannot read property 'forEach' of undefined
// 看下webpack解析结果:
(_context2 = \"hey\"[(\"liu\", \"liuxing\")]).call(_context2, console.log);
方法2:使用NodeJS原生方法实现
- 将
.js文件改为.mjs文件 - 全部代码使用ES Module的方式加载和输出
- node14版本以下使用
node --experimental-modules bin/index.mjs运行- node14版本及以上已经支持,可以正常使用
node bin/index.mjs运行
新建core/exec: 动态加载
- node14版本及以上已经支持,可以正常使用
@rainbow-lerna-cli/exec
新建models/package: 动态加载
@rainbow-lerna-cli/package
提供给exec使用
提供一个实例化一个pakcage的类
class Package {
// 获取Pakcage最新版本
// 检查Package是否存在
// 安装Package
// Package更新
// 获取Pakcage入口文件
}
utils/format-path
识别Package的入口文件路径,兼容mac和windows系统
新建module/command
创建子命令init的父类Command