Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

玩转nodeJs文件模块 #34

Open
CatsAndMice opened this issue Mar 11, 2022 · 0 comments
Open

玩转nodeJs文件模块 #34

CatsAndMice opened this issue Mar 11, 2022 · 0 comments

Comments

@CatsAndMice
Copy link
Owner

前言

上篇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 文件,处理过程进行跳过。

效果演示:

源码地址:https://github.com/CatsAndMice/medash/blob/dev/rollup.config.js

自动生成模板文件

能帮我解决什么问题

  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 文件中,这里代码就不粘贴了,感兴趣的读者请点击下面的源码地址。

源码地址:https://github.com/CatsAndMice/medash/blob/dev/build/const.ts

整体的代码目录:

源码地址:https://github.com/CatsAndMice/medash/tree/dev/build

最后,解决新增方法自动导入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
    })
}

效果演示:

源码地址:https://github.com/CatsAndMice/medash/blob/dev/build/addMainContext.ts

自动生成目录文件

这里我使用一个文档网站生成器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());
    })
}

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

效果演示:

源码地址:https://github.com/CatsAndMice/medash/blob/dev/build/createDocs.ts

最后

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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant