无工具,js直接在网页上实现模块化

gigipop 2019-07-01

大概原理就是异步加载一堆脚本文件,个人项目可以试试,企业项目使用请深思熟虑后再进行尝试。
我了解到的方法来自于《javascript设计模式》,作者是张容铭,因为觉得这个挺好玩的,给大家分享一下。


既然要直接在网页上实现模块化,肯定需要异步添加脚本文件,所以需要解决两个麻烦的问题

  1. 依赖的模块也有依赖怎么办
  2. 如何知道异步添加的js文件加载的状态

无工具,js直接在网页上实现模块化

无工具,js直接在网页上实现模块化

//main.js 不用关注细节,快速了解下调用方式即可
m.module(['dom/index'], function (dom) {
    var test = document.getElementById('test')
    console.log(dom(test).attr('id'))
    dom(test).css({
        'background': '#000',
        'color': '#fff'
    })
})
......
//dom/css.js
m.module('dom/css', ['shared/util'], function (util) {
    return function (ele, css, value) {
        if (util.isString(css)) {
            if (util.isNumber(value)) {
                return ele.style.css = value
            }
            return ele.style.css
        } else {
            for (var k in css) {
                ele.style[k] = css[k]
            }
        }
    }
})
......
//shared/util.js
m.module('shared/util', function () {
    return {
        isNumber: function (num) {
            return num === (0 || num) && num.constructor === Number
        },
        isString: function (str) {
            return str === ("" || str) && str.constructor === String
        }
    }
})

下面就开始实现这个暴露出来的module函数

遵守规则

将模块的实现隐藏起来,创建一个闭包

(function(m){
    var m = m()
})(function(){
    window.m = {}
})

工具函数

添加两个工具函数,loadScript和getUrl

//加载脚本
var loadScript = function (src) {
        var _script = document.createElement('script')
        _script.type = 'text/javascript'
        _script.async = true
        _script.src = src

        document.getElementsByTagName('head')[0].appendChild(_script)
    },
    //为地址添加一个.js
    getUrl = function (moduleName) {
        return String(moduleName).replace(/\.js$/g, '') + '.js'
    }

module函数的实现

通过上面的图片示例可以了解到,module函数包括了创建和调用模块的功能,它拥有三个参数

  • url 地址
  • deps 数据类型为数组的依赖模块
  • callback 该模块的主函数

获取参数

m.module=function(){
    var args = [].slice.call(arguments),
        //取最后一个参数,即callback
        callback = args.pop(),
        //获取依赖,且数据类型为数组
        deps = (args.length && args[args.length - 1] instanceof Array) ? args.pop() : [],
        //地址
        url = args.length ? args.pop() : null
...

这就是完整module函数的实现,初看很复杂,别急,module的关键就在于下面的两个函数(loadModule和setModule),凡是异步原理都是在和大脑作对,习惯就是新世界的大门,不要拘泥于阅读顺序

m.module = function () {
    var args = [].slice.call(arguments),
        callback = args.pop(),
        deps = (args.length && args[args.length - 1] instanceof Array) ? args.pop() : [],
        url = args.length ? args.pop() : null
        
        params = [],//依赖模块序列,主函数(回调函数)的使用的参数
        depsCount = 0,//该模块未加载完毕的依赖数量
        i = 0
        
    if (deps.length) {
        while (i < deps.length) {
            //闭包保存i
            (function (i) {
                //这样每个脚本执行的module都会有一个depsCount
                depsCount++
                //loadModule初始化是不会调用它的回调函数(缓冲器)的(可以先翻到下面看loadModule的实现)
                //但它会把回调函数添加到moduleCache中去,同时加载该依赖的脚本
                loadModule(deps[i], function (mod) {
                    //这里的mod是依赖模块的输出
                    params[i] = mod
                    //等于0的时候就会执行自己的回调
                    depsCount--  
                    if (depsCount === 0) {
                        //将依赖模块的输出添加到callback的参数中,这样主函数就可以直接使用参数进行调用
                        setModule(url, params, callback)
                    }
                })
            })(i)
            i++
        }
    } else {
        //一旦依赖走到底部,也就是一个脚本文件里的模块没有了依赖(可以先看看下面setModule)
        //loadModule初始化添加到moduleCache的回调就会执行,而depsCount就会-1
        setModule(url, [], callback)
    }
}

如果没有依赖的话,会直接执行setModule,该模块如果是被依赖的模块,就会调用loadModule缓存的缓冲器,也就是它的回调函数
可以先看看loadModule和setModule的实现

if(deps.length){
    ...
}else{
    setModule(url, [], callback)
}

添加一个moduleCache变量,用于缓存模块

//闭包内部
var moduleCache = {}
var setModule = function (moduleName, params, callback) {
    var _module, fn
    if (moduleCache[moduleName]) {
        _module = moduleCache[moduleName]
        _module.status = 'loaded'
        //export是模块的输出
        _module.exports = callback ? callback.apply(_module, params) : null
        while (fn = _module.onload.shift()) {
            //执行回调,并将自己的模块输出到缓冲器中
            fn(_module.exports)
        }
    } else {
        callback && callback.apply(null, params)
    }
}
//这里参数callback不是主函数,而是保存的缓冲器,详细翻回module的完整函数
var loadModule = function (moduleName, callback) {
    var _module
    //已初始化
    if (moduleCache[moduleName]) {
        _module = moduleCache[moduleName]
        if (_module.status === 'loaded') {
            //有就直接从moduleCache缓存中获取
            setTimeout(callback(_module.exports), 4)
        } else {
            _module.onload.push(callback)
        }
    } else {
        //初始化
        moduleCache[moduleName] = {
            //地址,也可以叫模块名称
            moduleName: moduleName,
            status: 'loading',
            //该模块return 的输出
            exports: null,
            onload: [callback]
        }
        //添加脚本
        loadScript(getUrl(moduleName))
    }
}

第一次写文章,如果觉得不好理解或者有行文不严谨的地方可以发下评论或者私信我修改
有帮助的话给我个赞哦

相关推荐