重構smart-import

nbb3210發表於2019-02-16

前情提要

自動 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()

未完待續

相關文章