前情提要

自动 Import 工具,前端打字员的自我救赎

记第一次发布npm包经历,smart-import

GitHub:smart-import

develop是重构中的代码

master是1.0版本可以工作的代码

配置文件

from:待导入的模块

to:引用模块的文件

template:引用模块的方式

ignored:忽略的模块

{
    "from": "demo/pages/**/*.vue",
    "to": "demo/router/index.js",
    "template": "const moduleName = () => import(modulePath)",
    "ignored": [
        "demo/pages/pageA.vue"
    ]
}

实现监听文件的删除和添加

#!/usr/bin/env node
const path = require('path')
const chokidar = require('chokidar')
const config = JSON.parse(fs.readFileSync('smart-import.json'))

class SmartImport {
    constructor({ from }) {
        this.from = from
        this.extname = path.extname(from)
    }

    watch() {
        chokidar
            .watch(this.from, {
                ignoreInitial: true
            })
            .on('add', file => {
                console.log('add', file)
            })
            .on('unlink', file => {
                console.log('unlink', file)
            })
    }
}

let smartImport = new SmartImport(config)
smartImport.watch()

以上代码主要使用了chokidar来监听文件的变化。但存在一个问题,如果删除文件夹,而文件夹中包含匹配的模块,不会触发unlink事件。所以改成watch整个目录,然后在addunlink的回调中添加判断文件后缀的代码,因为我们可能只在意.vue,而不在意.js

...
watch() {
        chokidar
            .watch(path.dirname(this.from), {
                ignoreInitial: true
            })
            .on('add', file => {
                if (path.extname(file) === this.extname) {
                    console.log('add', file)
                }
            })
            .on('unlink', file => {
                if (path.extname(file) === this.extname) {
                    console.log('unlink', file)
                }
            })
    }
...

现在符合from的文件的变动(添加和删除)都被监视了,但是总觉得

if (path.extname(file) === this.extname) {
  
}

写了两遍,不开心

class SmartImport {
    constructor({ from }) {
        this.from = from
        this.extname = path.extname(from)
        this.checkExt = this.checkExt.bind(this)
    }

    watch() {
        const { from, checkExt } = this
        chokidar
            .watch(path.dirname(from), {
                ignoreInitial: true
            })
            .on(
                'add',
                checkExt(file => {
                    console.log('add', file)
                })
            )
            .on(
                'unlink',
                checkExt(file => {
                    console.log('unlink', file)
                })
            )
    }

    checkExt(cb) {
        return file => {
            if (path.extname(file) === this.extname) {
                cb(file)
            }
        }
    }
}

新添加了函数checkExt(),它的参数和返回值都是函数,只是添加了判断文件后缀名的逻辑。

高阶函数有木有!

函数式编程有木有!

另外就是注意通过this.checkExt = this.checkExt.bind(this),绑定this的指向。

文件的变动映射到数组中

定义一个数组保存匹配的文件,另外匹配文件的变动会触发doImport()事件

代码就变成了这样

class SmartImport {
    constructor({ from, ignored }) {
        this.from = from
        this.ignored = ignored
        this.extname = path.extname(from)
        this.modules = []
    }

    watch() {
        const { from, ignored, extname, modules } = this
        chokidar
            .watch(path.dirname(from), {
                ignoreInitial: true,
                ignored
            })
            .on(
                'add',
                this.checkExt(file => {
                    console.log('add', file)
                    modules.push(file)
                    this.doImport()
                })
            )
            .on(
                'unlink',
                this.checkExt(file => {
                    console.log('unlink', file)
                    _.remove(modules, p => p === file)
                    this.doImport()
                })
            )
    }

    checkExt(cb) {
        const { extname } = this
        return file => {
            if (path.extname(file) === extname) {
                cb(file)
            }
        }
    }

    doImport() {
        console.log('doImport...')
        console.log(this.modules)
    }
}

注意,我又把this.checkExt = this.checkExt.bind(this)给删了,还是直接通过this.checkExt()调用方便,虽然代码看起来凌乱了。

另外就是把this.doImport()又写了两遍。嗯,思考一下。其实modules变化,就应该触发doImport()

发布-订阅模式有木有

所以添加了个类ModuleEvent

class ModuleEvent {
    constructor() {
        this.modules = []
        this.events = []
    }

    on(event) {
        this.events.push(event)
    }

    emit(type, val) {
        if (type === 'push') {
            this.modules[type](val)
        } else {
            _.remove(this.modules, p => p === val)
        }
        for (let i = 0; i < this.events.length; i++) {
            this.events[i].apply(this, [type, this.modules])
        }
    }
}

同时修改类SmartImport

class SmartImport {
    constructor({ from, ignored }) {
        this.from = from
        this.ignored = ignored
        this.extname = path.extname(from)
        this.moduleEvent = new ModuleEvent()
    }

    init() {
        this.moduleEvent.on((type, modules) => {
            this.doImport(type, modules)
        })
        this.watch()
    }

    watch() {
        const { from, ignored, extname, modules } = this
        chokidar
            .watch(path.dirname(from), {
                ignoreInitial: true,
                ignored
            })
            .on(
                'add',
                this.checkExt(file => {
                    console.log('add', file)
                    this.moduleEvent.emit('push', file)
                })
            )
            .on(
                'unlink',
                this.checkExt(file => {
                    console.log('unlink', file)
                    this.moduleEvent.emit('remove', file)
                })
            )
    }

    checkExt(cb) {
        const { extname } = this
        return file => {
            if (path.extname(file) === extname) {
                cb(file)
            }
        }
    }

    doImport(type, modules) {
        console.log(`type: ${type}`)
        console.log(modules)
    }
}

let smartImport = new SmartImport(config)
smartImport.init()

终于理解了很多库中on方法的原理有木有!对象中有个events,专门存这些回调函数有木有

另外我们观察chokidar.on(eventType, cb),对比自己的moduleEvent.on(cb)。想想也是,也许我只想监听特定的事件呢

修改ModuleEvent

class ModuleEvent {
    constructor({ from, ignored }) {
        this.modules = glob.sync(from, {
            ignore: ignored
        })
        this.events = {}
    }

    on(type, cb) {
        if (!this.events[type]) {
            this.events[type] = []
        }
        this.events[type].push(cb)
    }

    emit(type, val) {
        if (type === 'push') {
            this.modules[type](val)
        } else {
            _.remove(this.modules, p => p === val)
        }
        for (let i = 0; i < this.events[type].length; i++) {
            this.events[type][i].apply(this, [this.modules])
        }
    }
}

后来觉得这个套路挺常见,将其抽象出来,最后形成代码如下

#!/usr/bin/env node
const fs = require('fs')
const path = require('path')
const glob = require('glob')
const chokidar = require('chokidar')
const _ = require('lodash')
const config = JSON.parse(fs.readFileSync('smart-import.json'))

const CustomEvent = (() => {
    let events = {}
    let on = (type, cb) => {
        if (!events[type]) {
            events[type] = []
        }
        events[type].push(cb)
    }
    let emit = (type, data) => {
        for (let i = 0; i < events[type].length; i++) {
           events[type][i].apply(this, [data])
        }
    }
    return {
        on,
        emit
    }
})()

class SmartImport {
    constructor({ from, ignored }) {
        this.from = from
        this.ignored = ignored
        this.extname = path.extname(from)
        this.modules = glob.sync(from, {
            ignore: ignored
        })
    }

    init() {
        CustomEvent.on('push', m => {
            console.log('Do pushing')
            this.modules.push(m)
        })
        CustomEvent.on('remove', m => {
            console.log('Do removing')
            _.remove(this.modules, p => p === m)
        })
        this.watch()
    }

    watch() {
        const { from, ignored, extname, modules } = this
        chokidar
            .watch(path.dirname(from), {
                ignoreInitial: true,
                ignored
            })
            .on(
                'add',
                this.checkExt(file => {
                    CustomEvent.emit('push', file)
                })
            )
            .on(
                'unlink',
                this.checkExt(file => {
                    CustomEvent.emit('remove', file)
                })
            )
    }

    checkExt(cb) {
        const { extname } = this
        return file => {
            if (path.extname(file) === extname) {
                cb(file)
            }
        }
    }
}

let smartImport = new SmartImport(config)
smartImport.init()

未完待续


nbb3210
436 声望31 粉丝

优雅地使用JavaScript解决问题