一、需求背景
有多个独立项目需要使用相同的组件(上传组件、省市区选择组件等),我们可以创建一个父项目,并在父项目中创建多个子项目,将公用组件封装在父项目中,并监听公用组件是否发生变化,同步到子项目中,实现只封装一次组件,即可在多个项目中使用的需求。
监听文件变化
要实现将文件同步至多个项目中,我们可以使用chokidar
监听父项目中组件文件夹中的文件与文件目录是否发生了改变,并通过使用express
和node.js
将发生改变的文件同步到子项目中。
二、chokidar
介绍
chokidar
是一个NPM
库,可以通过监听文件或者目录的变化,并在这些变化发生时触发回调函数。
使用方法
javascript
const chokidar = require("chokidar");
const watcher = chokidar.watch(path); // 监听的路径(本项目为父项目的组件文件夹)
watcher
.on("change", (mpath) => {
// 文件发生改变时触发
})
.on("add", (mpath) => {
// 新增文件时触发
})
.on("addDir", (mpath) => {
// 新增文件夹时触发
})
.on("unlink", (mpath) => {
// 删除文件时触发
})
.on("unlinkDir", (mpath) => {
// 删除文件夹时触发
})
三、使用commandar
配置命令行参数
commandar
是一个强大的命令行参数处理库,详细使用方法请参考文档: commander中文文档 |中文示例
vbnet
const { program } = require("commandar");
program
.version(version)
.option("-e, --employee", "Start employee project.")
.option("-c, --client", "Start client project")
.option("-l, --landing", "Start landing project")
.parse(process.argv);
version
方法
version
方法添加了用于显示命令版本的处理。
option
方法
option
方法用于定义选项,同时可以附加选项的简介,每个选项可以定义一个短选项名称(-后面接单个字符)和一个长选项名称(--后面接一个或者单个单词),使用逗号,空格或者" | "分隔。
parse
方法
parse
方法用于解析系统的命令行参数,一定不能省略,否则将会解析失败。
四、引入express
和fs
模块
express
基于node.js
平台的 web 应用开发框架,项目中用于启动服务,以便监听文件变化和同步文件。
fs
node.js
的文件系统模块,提供了丰富的函数和方法,可以进行文件的读取、写入、复制和删除等操作,同时也支持文件目录的创建、遍历和修改等操作。使用fs
模块,可以在node.js
环境中轻松的与文件系统进行交互。本项目中用到的函数与方法有:access
/stat
/writeFileSync
/mkdirSync
/readdir
/readFileSync
/existsSync
/statSync
/readdirSync
/unlinkSync
/rmdirSync
。
fs.access(path, callback)
:测试用户对path
指定文件或目录的权限,本项目中用于判断是否存在path
文件目录,不存在时创建目录。fs.stat(path, callback)
:获取文件或目录的信息,获取到的信息存在一个fs.stats类的对象中。有以下方法:
scss
stats.isFile()
stats.isDirectory()
stats.isBlockDevice()
stats.isCharacterDevice()
stats.isSymbolicLink()
stats.isFIFO()
stats.isScoket()
fs.writeFileSync(path, data[, options])
:同步写入内容。
makefile
path: 被写入文件的路径
data: 要写入的内容,字符串格式,项目中通过 fs.readFileSync(src)同步读取文件内容
options: 写入文件的参数配置,默认是utf8编码
fs.mkdirSync(path)
:同步地创建目录。fs.readdir(path)
:读取目录的内容。fs.readFileSync(path)
:读取文件内容fs.existsSync(path)
:判断是否存在路径,存在则返回true
,否则返回false
。fs.statSync(path)
:获取路径的fs.stats
类,与fs.stat()
作用类似。fs.readdirSync(path)
:读取目录(文件夹)的内容。fs.unlinkSync(path)
:同步删除目录中的文件或者符号链接,只适用于文件的删除。fs.rmdirSync(path)
:同步删除目录中的文件夹,只适用于文件夹的删除。- 更多方法的详细使用方法请参考:fs | Node .js API 文档
五、流程设计
引入所需的模块
在父项目根目录创建http.js
文件。
ini
const express = require("express");
const chokidar = require('chokidar');
const {
program
} = require('commander');
const path=require("path");
let fs = require('fs');
const app = express();
const port = 3002;
定义删除文件和复制文件的函数
scss
// 删除文件
function delDir(path, reservePath) {
if (fs.existsSync(path)) {
if (fs.statSync(path).isDirectory()) { // 判断是不是文件夹
let files = fs.readdirSync(path); // 拿到文件夹里面的内容
files.forEach((file, index) => {
let currentPath = path + "/" + file;
if (fs.statSync(currentPath).isDirectory()) {
// 如果还是文件夹,则继续执行delDir函数
delDir(currentPath, reservePath);
} else {
// 同步删除文件
fs.unlinkSync(currentPath);
}
});
if (path !== reservePath) {
fs.rmdirSync(path); // 删除原先的文件夹的目录
}
} else {
fs.unlinkSync(path);
}
}
}
// 复制文件
function copyDir(src, dist, onError) {
fs.access(dist, function(err){
if(err){
// 目录不存在时创建目录
fs.stat(src, function(err, stat) {
if(stat.isFile()) {
fs.writeFileSync(dist,'');
}else{
fs.mkdirSync(dist);
}
})
}
_copyCall(null, src, dist, onError);
});
}
function _copyCall(err, src, dist, onError) {
if(err){
onError(err);
return;
}
fs.stat(src, function(err, stat) {
if (stat.isFile()) {
fs.writeFileSync(dist, fs.readFileSync(src));
} else if (stat.isDirectory()){
fs.readdir(src, function(err, paths) {
if(err){
onError(err)
} else {
paths.forEach(function(_path) {
let _src = src + '/' + _path;
let _dist = dist + '/' + _path;
fs.stat(_src, function(err, stat) {
if(err) {
onError(err);
} else {
// 判断是文件还是目录
if (stat.isFile()) {
if (
path.extname(_dist).includes('~') ||
path.extname(_src).includes('~'))
{
return;
}
fs.writeFileSync(_dist, fs.readFileSync(_src));
} else if (stat.isDirectory()) {
// 是目录时,递归复制
copyDir(_src, _dist, onError)
}
}
})
})
}
})
}
})
}
设置命令行参数
arduino
program
.version("1.0.0")
.option('--employee', 'Start employee project')
.option('--client', 'Start client project')
.option('--landing', 'Start landing project')
.parse(process.argv);
启动服务
scss
app.use(express.static(path.join(__dirname, 'resources'))); // 加载静态资源
app.listen(port, () => {
const srcPath = path.resolve(__dirname, './components');
let distPaths = [];
// 判断是需要更新到哪一个项目
if(program.employee){
distPaths.push(path.resolve(__dirname,'./employee/_components'));
}
if(program.client){
distPaths.push(path.resolve(__dirname,'./client/_components'));
}
if(program.landing){
distPaths.push(path.resolve(__dirname,'./landing/_components'));
}
// 遍历文件目录,复制到对应的子项目的_components文件夹中
distPaths.forEach(distPath => {
copyDir(srcPath, distPath, (err) => {
console.error(err)
});
})
// 监听components文件夹是否发生了改变
const watcher = chokidar.watch(srcPath)
watcher
.on('change', (mpath) => {
_copy(mpath)
})
.on('add', (mpath) => {
_copy(mpath)
})
.on('addDir', (mpath) => {
_copy(mpath)
})
.on('unlink', (mpath) => {
_del(mpath);
})
.on('unlinkDir', (mpath) => {
_del(mpath);
})
function _copy (mpath) {
distPaths.forEach(distPath => {
copyDir(mpath,distPath + mpath.split('components')[1],(err) => {
console.error(err)
})
})
}
function _del (mpath) {
distPaths.forEach(distPath => {
delDir(distPath+mpath.split('components')[1]);
})
}
});
在package.json
中配置指令
json
"scripts": {
"start:employee": "node http.js --employee",
"start:client": "node http.js --client",
"start:landing": "node http.js --landing",
"start": "node http.js --client --employee --landing"
},