玩转nodeJs文件模块

1,272 阅读9分钟

前言

上篇50+行代码搞定一行命令更新Npm包 - 掘金 (juejin.cn)介绍了自动化push仓库&&自动化更新版本等功能的实现。这篇说说自动生成模板文件、rollup按需加载打包配置以及自动生成目录列表,简单得不像话!

按需加载打包配置

按需加载听起来高大上,但本质就是将不同的模块功能文件分开打包成多个文件,而不是全部打包成一个主文件。

首先创建一个rollup.config.js 文件,然后下载需要的插件,创建文件pluginsCommon.js 用于配置共同的插件,相关代码如下:

const { getBabelOutputPlugin } = require('@rollup/plugin-babel')
//用于将typescript编译成javascript
const typescript = require('rollup-plugin-typescript2')
const resolve = require('rollup-plugin-node-resolve')
const commonjs = require('@rollup/plugin-commonjs')
//用于压缩
const { terser } = require("rollup-plugin-terser")
module.exports = [
    typescript(),
    commonjs(),
    resolve(),
    getBabelOutputPlugin({
        presets: ['@babel/preset-env'],
        allowAllFormats: true,
    }),
    terser()
]

不了解的插件,请Google。

rollup打包运行环境是nodeJs,遵循commonJS的规范。

rollup提供了很多种打包格式rollup打包格式 ,我需要的是一个用于做CDN加速的medash.min.js文件格式为umd以及按需加载的若干cjs 格式文件。

不了解umd、cjs的读者,请查阅rollup打包格式

medash.min.js 文件容易得到,只需要设置打包入口文件,入口文件为main.ts ,向rollup.config.js 中添加如下内容即可:

//...
const pluginsCommon = require('./pluginsCommon')

export default [
//...
 {
    input: "./main.ts",
    output: [
        {
            file: 'dist/medash.min.js',
            name: 'medash',
            format: 'umd'
        }
    ],
    plugins: pluginsCommon
}];

按需加载打包配置稍微麻烦点,我需要获取所有需要按需打包的文件路径。一个文件一个文件的手动写入相当麻烦,为此我创建一个build.js文件用于自动获取打包文件的路径。

我定义了一个变量名buildInputs 的数组,用于存储所有的文件路径,所有需要打包的文件均存于src 目录下。

思路: 获取src目录下所有的文件或文件夹,逐一进行遍历。若文件类型为ts 时,获取该文件的相对路径pushbuildInputs数组中;若文件类型是一个文件夹时,获取该文件夹下的文件目录,重复上一步操作。

实现上述需要使用nodeJs fs模块以及path模块,先进行导入:

const fs = require('fs');
const Path = require('path');

fs.readdirSync(path)获取文件夹目录文件:

//...
function getFileNames(befter, after) {
    let filesPath = Path.join(befter, after)
    return { files: fs.readdirSync(filesPath), path: filesPath };
}
//...

获取文件相对路径并pushbuildInputs 数组:

//...
function setBuildInputs(tsFile) {
    let filePath = tsFile.split('src')[1]
    buildInputs.push('./src' + filePath.replace(/\\/g, "/"))
}
//...

Path.extname(childrenPath)获取文件后缀名,针对不同的后缀名做不同的逻辑处理:

//...
function startBuild({ files, path }) {
    if (files.length === 0) {
        return;
    }
    files.forEach((file) => {
        let childrenPath = Path.join(path, file);
        let isExtname = Path.extname(childrenPath);
        if (isExtname === '.ts') {
            setBuildInputs(childrenPath);
        } else if (isExtname === '.js') {
            return;
        } else {
            startBuild(getFileNames(path, file));
        }
    })
}
startBuild(getFileNames(__dirname, '../src'));
//...

src 目录下残留一些.js 文件,选择性跳过。

效果演示:

源码地址:github.com/CatsAndMice…

自动生成模板文件

能帮我解决什么问题

  1. 每次想增加一个方法文件时,我需要创建四个文件,分别是src文件下的xxx.ts、测试test文件夹下的xxx.test.ts、文档docs文件夹下的xxx.md 以及example文件下的xxx.ts,我想要自动进行创建;
  2. 增加的方法,需要在xxx.test.ts、案例xxx.ts 导入以及main.ts 文件中导入导出步骤,我想要自动完成这一步的操作。

开撸

50+行代码搞定一行命令更新Npm包 - 掘金 (juejin.cn)中,提及到了inquirer 。同样的,实现自动生成模板文件也从终端交互入手。

先在build文件夹下,创建一个create.ts 文件。

我想增加一个方法时,终端应该提供一个目录选择,以便将增加的文件创建在对应的目录下,当我选择对应的目录后终端提供一个类似输入框的功能,用于给文件命名。

目录选择代码:

/...
import getSrcLists from "./getSrcLists";
//...
async function typesCheck() {
    const lists = await getSrcLists();
    inquirer.prompt([
        {
            name: 'list',
            type: 'list',
            message: '请选择对应的文件夹',
            choices: lists,
            default: [lists[0]]
        }
    ]).then(({ list }) => {
        getName(list)
    })
}
//...

其中getSrcLists 方法作用是获取src 下的文件夹名称,用于进行目录选择。import fs from "fs/promises"; 导出的fs/promises 模块与import fs from "fs"; 导出的fs模块,区别在于fs模块API采用的是回调,而fs/promises 模块的API支持Promise ,开发者使用更方便。

import fs from "fs/promises";
import { srcPath } from "./const";
export default async () => {
    const dirLists = await fs.readdir(srcPath);
    return dirLists;
}

srcPath 其实就是一个绝对路径,const.ts 文件下就一行代码搞定。

import path from 'path';
//...
export const srcPath = path.join(__dirname, '../src');

创建文件命名功能是getName(list) 方法,参数list 是用户选择的目录。

//...
async function getName(fileName: string) {
    //...
    let { input } = await inquirer.prompt([
        {
            name: 'input',
            type: 'input',
            message: '请为创建的文件命名:',
        }
    ])
    if (isEmpty(input)) {
        err('error:创建文件未命名');
        return
    }
    const { isSpecialChar } = specialChar(input);
    const { isCh } = ch(input);
    if (isSpecialChar) {
        err('error:文件名含有特殊字符!');
        return
    }

    if (isCh) {
        err('error:文件名含有中文!');
        return
    }
    //...
}

命名校验:

  • 不能含有特殊字符;
  • 不能使用中文。

效果演示:

文件命名后,接下来就是创建对应的文件:

import path from "path";
import { testPath, srcPath, docsPath, examplePath, err } from './const';
//...
async function getName(fileName: string) {
    const createPath = path.join(srcPath, fileName);
    const createDocsPath = path.join(docsPath, fileName);
    const createTestPathPath = path.join(testPath, fileName);
    const createExamplePath = path.join(examplePath, fileName)
    //...
    createTestFile(createTestPathPath, input)
    createFile(createPath, input);
    createDocs(createDocsPath, input);
    createExample(createExamplePath, input);
   //...
}
//...

createPath、createTestPathPath、createDocsPath 、createExamplePath 这四个变量分别是src文件下xxx.ts、测试test文件夹下xxx.test.ts、文档docs文件夹下xxx.md 以及example文件夹下xxx.ts的路径。

import path from 'path';
//...
export const srcPath = path.join(__dirname, '../src');
export const testPath = path.join(__dirname, '../test');
export const docsPath = path.join(__dirname, '../docs/v3');
export const examplePath = path.join(__dirname, '../example');
//...

方法createTestFile、createFile、createDocs、createExample 逻辑类似,区别是创建的文件后缀名以及写入的内容。

创建createAndWrite.ts 文件,用于抽离公共的创建文件夹与创建文件并写入的功能:

import fs from "fs";
import path from 'path';
import { err } from "./const";
export function create(createPath: string, suffixName: string) {
    !fs.existsSync(createPath) && fs.mkdirSync(createPath);
    return path.join(createPath, suffixName)
}
export function write(createPath: string, context: string, callBack = () => { }) {
    if (fs.existsSync(createPath)) {
        err(createPath + '文件已存在!')
        return;
    }
    fs.writeFile(createPath, context, (error) => {
        if (error) {
            err(createPath + '文件创建失败!')
            return;
        }
        callBack();
    })
}

create 方法用于创建文件夹,write 方法用于创建文件并写入内容。

createTestFile、createFile、createDocs、createExample 逻辑是类似的,所以这里只把 createFile文件粘贴出来:

import { srcContext } from './const';
import { write, create } from './createAndWrite';
export default (createPath: string, name: string) => {
    const suffixName = name + '.ts';
    createPath = create(createPath, suffixName);
    write(createPath, srcContext())
}

createTestFile、createFile、createDocs、createExample 文件中会改变的变量const suffixName = name + '.ts'; ,还有就是srcContext

'.ts' 表示文件的后缀名,需要创建的文件后缀名有.md、.test.ts、.ts ; srcContext 是写入文件的内容函数,不同的文件写入的内容不同,而createTestFile、createFile、createDocs、createExample 中获取写入内容的函数名不同。

写入文件的内容是提前定义在consts.ts 文件中,这里代码就不粘贴了,感兴趣的读者请点击下面的源码地址。

源码地址:github.com/CatsAndMice…

整体的代码目录:

源码地址:github.com/CatsAndMice…

最后,解决新增方法自动导入main.ts 还有导出问题 。

同样的,我在create.ts文件中往getName 方法内添加一个addMainContext方法:

import path from "path";
//...
import addMainContext from './addMainContext';
//...

async function getName(fileName: string) {
    const createPath = path.join(srcPath, fileName);
   //...
    createExample(createExamplePath, input);
    addMainContext(createPath, input);
}
//...

addMainContext 文件中,代码逻辑为先读取main.ts内容,然后进行内容分段,判断是否已重复来决定是否添加该新的方法,最后再重新写入main.ts

先看下main.ts 内容:

//...
import toArray from "./src/Array/toArray";
import ch from "./src/RegExp/ch";
export {
    ch,
    toArray,
    or,
    chain,
    composePromise
    //...
 }
 export default {
    ch,
    toArray,
    or,
    chain,
    composePromise
    //...
 }

它存在两种导出方式,导出方式均以export 关键字开头,所以读取main.ts内容后以export进行分割,把内容分成三部分存放至一个数组中。

import fs from "fs";
//...
export default (path: string, name: string) => {
    filePath = path
    fs.readFile('./main.ts', 'utf-8', (readError, data) => {
        if (readError) {
            console.error(readError);
            return;
        }
        let context = addContext(data.split('export'), name);
        context ? writeFile(context) : null
    })
}

第一部分是import 导入文件,第二、三部分都是导出。导入内容部分用于判断是否为重复导入,导出部分以{ 再次分割,分割完成后再把新增方法名重新拼接。

const getFilePath = function (filePath: string, name: string) {
    let afterPath = filePath.split('src')[1];
    afterPath = afterPath.replace('\\', '/');
    return './src' + afterPath + '/' + name;
}
const addContext = (args: string[], name: string) => {
    let importsHeader = `import ${name} from "${getFilePath(filePath, name)}";\r\n`;
    if (args[0].includes(importsHeader)) {
        err(name + '已导入!');
        return
    }
    let imports = args[0] + importsHeader;
    let contexts = args[1].split('{');
    let ctx = contexts[0] + `{\r\n${name},` + contexts[1];
    let dafaultContexts = args[2].split('{')
    let ctxs = dafaultContexts[0] + `{\r\n${name},` + dafaultContexts[1];
    return [imports, ctx, ctxs].join('export');
}

最后,再将内容写入main.ts

const writeFile = (context: string) => {
    fs.writeFile('./main.ts', context, (error) => {
        console.error(error);
    })
}
export default (path: string, name: string) => {
    filePath = path
    fs.readFile('./main.ts', 'utf-8', (readError, data) => {
        if (readError) {
            console.error(readError);
            return;
        }
        let context = addContext(data.split('export'), name);
        context ? writeFile(context) : null
    })
}

效果演示:

源码地址:github.com/CatsAndMice…

自动生成目录列表

这里我使用一个文档网站生成器docsify ,用于部署文档。

能帮我解决什么问题

  • 所有的文档都放于docs/v3文件夹下,docsify生成文档网站,前提是要把文档文件路径以[xxx](相对路径) 的格式收录至_sidebar.md 文件中,一个一个的手动写入太麻烦,我想要自动进行录入。

录入后,运行docsify可以得到一个在线文档网站链接了。

开撸

自动生成模板文件一节中,有提及到一个createDocs 方法,它用于创建.md文件并写入内容。自动生成目录实现从它这里入手。

先展示下它所处的目录:

从上图中注释,可以看出创建目录的入口为readDir函数,该函数作为参数被传递到write方法,这样做的目的是.md 创建后才执行readDir更新目录。

获取文件夹目录,逐一遍历创建异步函数去拼接[xxx](相对路径) ,并将异步函数添加至promises数组。

//创建目录
const readDir = async () => {
    let content = '* [快速开始](readme.md)\n';
    const dirs = await fsPromises.readdir(docsPath);
    const promises: any[] = [];
    dirs.forEach((dir) => {
        const promise = new Promise(async (resolve) => {
            const filePath = path.join(docsPath, dir);
            fsPromises.readdir(filePath).then(files => {
                content += getContent(dir, files);
                resolve(content);
            })
        })
        promises.push(promise);
    })
    //闭包,方便获取content的值
    allPromisesFinish(promises, () => content);
}

content += getContent(dir, files); getContent 用于排序并拼接内容格式,拼接好的格式再与变量content拼接。

const getContent = (dir, files) => {
    let text = `* ${dir}\n`;
    files.sort((a, b) => a - b);
    for (const file of files) {
        text += `  * [${file.split('.')[0]}](v3/${dir}/${file})\n`;
    }
    return text;
}

promises数组中所有的异步函数出结果后,将会执行() => { writeContent(content());} 写入内容至_sidebar.md

const writeContent = (content) => {
    const writeFilePath = path.join(__dirname, '../docs/_sidebar.md');
    fsPromises.writeFile(writeFilePath, content);
}

//全部的Promise状态完成后才进行文件写入
const allPromisesFinish = (promises, content) => {
    Promise.all(promises).then(() => {
        writeContent(content());
    })
}

一个简简单单的目录生成功能就完成了。

效果演示:

源码地址:github.com/CatsAndMice…

最后

文章介绍了作者使用nodeJs解决一些重复性的操作,也算是对nodeJs文件模块的一种刻意练习。

码字不易!如果我的文章对你有帮助,你的👍就是对我的最大支持^_^。