• Stars
    star
    217
  • Rank 182,446 (Top 4 %)
  • Language
    HTML
  • Created almost 9 years ago
  • Updated about 6 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

The right way to load javascript files dynamically.

动态加载js文件的正确姿势

说明:

这个repository的结构:

  • img/:图片
  • LABjs-source/LABjs的源码,带注释,文中部分代码参考了该项目。
  • lazyload-sourcelazyload的源码,带注释,文中部分代码参考了该项目。
  • src/:本文档中涉及的代码,在Firefox 42中测试,使用Firebug观察和调试
  • README.md:本文档。

本文中给出了多种解决方式,方式1对应的代码是src/js/loader01.jssrc/index01.js,其他方式对应的代码位置类似。

Gif图片使用LICEcap生成。

目录:

硬编码在html源码中的script是如何加载的 || 从一个例子出发 || 方式1:一个错误的加载方式 || 方式2 || 方式3 || 方式4 || 方式5 || 方式6 Promise 串行 || 方式7 Promise 并行 || 方式8 Generator Promise || 现有哪些工具可以实现动态加载 || 其他 || 资料


最近在做一个为网页生成目录的工具awesome-toc,该工具提供了以jquery插件的形式使用的代码,也提供了一个基于Bookmarklet(小书签)的浏览器插件。

小书签需要向网页中注入多个js文件,也就相当于动态加载js文件。在编写这部分代码时候遇到坑了,于是深究了一段时间。

我在这里整理了动态加载js文件的若干思路,这对于理解异步编程很有用处,而且也适用于Nodejs

硬编码在html源码中的script是如何加载的

如果html中有:

<script type="text/javascript" src="1.js"></script>
<script type="text/javascript" src="2.js"></script>

那么,浏览器解析到

<script type="text/javascript" src="1.js"></script>

会停止渲染页面,去拉取1.js(IO操作),等到1.js的内容获取到后执行。 1.js执行完毕后,浏览器解析到

<script type="text/javascript" src="2.js"></script>

进行和1.js类似的操作。

不过现在部分浏览器支持async属性和defer属性,这个可以参考:

async vs defer attributes
script的defer和async

script -MDN指出:async对内联脚本(inline script)没有影响,defer的话因浏览器以及版本不同而影响不同。

从一个例子出发

举个实际的例子:

<html>
<head></head>
<body>

    <div id="container">
        <div id="header"></div>
        <div id="body">
            <button id="only-button"> hello world</button>
        </div>
        <div id="footer"></div>
    </div>

    <script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js" type="text/javascript"></script>
    <script src="./your.js" type="text/javascript"></script>
    <script src="./my.js" type="text/javascript"></script>
    
</body>
</html>

js/your.js:

console.log('your.js: time='+Date.parse(new Date()));

function myAlert(msg) {
    console.log('alert at ' + Date.parse(new Date()));
    alert(msg);
}

function myLog(msg) {
    console.log(msg);
}

js/my.js:

myLog('my.js: time='+Date.parse(new Date()));
$('#only-button').click(function() {
    myAlert("hello world");
});

可以看出jqueryjs/your.jsjs/my.js三者的关系如下:

  • js/my.js依赖于jqueryjs/your.js
  • jqueryjs/your.js之间没有依赖关系。

浏览器打开index00.html,等待js加载完毕,点击按钮hello world将会触发alert("hello world");

firbug控制台输出:

下面开始探索如何动态加载js文件。

方式1:一个错误的加载方式

文件js/loader01.js内容如下:

Loader = (function() {

  var loadScript = function(url) {
    var script = document.createElement( 'script' );
    script.setAttribute( 'src', url+'?'+'time='+Date.parse(new Date()));  // 不用缓存
    document.body.appendChild( script );
  };

  var loadMultiScript = function(url_array) {
    for (var idx=0; idx < url_array.length; idx++) {
      loadScript(url_array[idx]);
    }
  }

  return {
    load: loadMultiScript,
  };

})();  // end Loader

index01.html内容如下:

<html>
<head></head>
<body>

    <div id="container">
        <div id="header"></div>
        <div id="body">
            <button id="only-button"> hello world</button>
        </div>
        <div id="footer"></div>
    </div>

    <script src="./js/loader01.js" type="text/javascript"></script>
    <script type="text/javascript">
        Loader.load([
                    'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js', 
                    './js/your.js',
                    './js/my.js'
                     ]);
    </script>
    
</body>
</html>

浏览器打开index01.html,点击按钮hello world,会发现什么都没发生。打开firebug,进入控制台,可以看到这样的错误:

很明显,my.js没等jquery就先执行了。又由于存在依赖关系,脚本的执行出现了错误。这不是我想要的。

在网上可以找到关于动态加载的一些说明,例如:

Opera/Firefox(老版本)下:脚本执行的顺序与节点被插入页面的顺序一致

IE/Safari/Chrome下:执行顺序无法得到保证

注意:

新版本的Firefox下,脚本执行的顺序与插入页面的顺序不一定一致,但可通过将script标签的async属性设置为false来保证顺序执行 老版本的Chrome下,脚本执行的顺序与插入页面的顺序不一定一致,但可通过将script标签的async属性设置为false来保证顺序执行

真够乱的!!(这段描述来自:LABJS源码浅析。)

为了解决我们遇到的问题,我们可以在loadScript函数中修改script对象async的值:

var loadScript = function(url) {
  var script = document.createElement('script');
  script.async = false;  // 这里
  script.setAttribute('src', url+'?'+'time='+Date.parse(new Date())); 
  document.body.appendChild(script);
};

浏览器打开,发现可以正常执行!可惜该方法只在某些浏览器的某些版本中有效,没有通用性。script browser compatibility给出了下面的兼容性列表:

下面探索的方法都可以正确的加载和执行多个脚本,不过有些同样有兼容性问题(例如Pormise方式)。

方式2

可以认为绝大部分浏览器动态加载脚本的方式如下:

  1. 动态加载多个脚本时,这些脚本的加载(IO操作)可能并行,可能串行。
  2. 一个脚本一旦加载完毕(IO结束),该脚本放入“待执行队列”,等待出队供js引擎去执行。

所以我们的示例中的三个js脚本的加载和执行顺序可以是下面的情况之一:

  1. jquery加载并执行,js/your.js加载并执行,js/my.js加载并执行。
  2. 和情况1类似,不过js/your.js在前,jquery在后。
  3. jqueryjs/your.js并行加载,按照加载完毕的顺序来执行;等jqueryjs/your.js都执行完毕后,加载并执行js/my.js

其中,“加载完毕”这是一个事件,浏览器的支持监测这个事件。这个事件在IE下是onreadystatechange ,其他浏览器下是onload

据此,Loading JavaScript without blocking给出了下面的代码:

function loadScript(url, callback){

    var script = document.createElement("script")
    script.type = "text/javascript";

    if (script.readyState){  //IE
        script.onreadystatechange = function(){
            if (script.readyState == "loaded" ||
                    script.readyState == "complete"){
                script.onreadystatechange = null;
                callback();
            }
        };
    } else {  //Others
        script.onload = function(){
            callback();
        };
    }

    script.src = url;
    document.body.appendChild(script);
}

callback函数可以是去加载另外一个js,不过如果要加载的js文件较多,就成了“回调地狱”(callback hell)。

回调地狱式可以通过一些模式来解决,例如下面给出的方式2:

Loader = (function() {

  var load_cursor = 0;
  var load_queue;

  var loadFinished = function() {
    load_cursor ++;
    if (load_cursor < load_queue.length) {
      loadScript();
    }
  }

  function loadError (oError) {
    console.error("The script " + oError.target.src + " is not accessible.");
  }


  var loadScript = function() {
    var url = load_queue[load_cursor];
    var script = document.createElement('script');
    script.type = "text/javascript";

    if (script.readyState){  //IE
        script.onreadystatechange = function(){
            if (script.readyState == "loaded" ||
                    script.readyState == "complete"){
                script.onreadystatechange = null;
                loadFinished();
            }
        };
    } else {  //Others
        script.onload = function(){
            loadFinished();
        };
    }

    script.onerror = loadError;

    script.src = url+'?'+'time='+Date.parse(new Date());
    document.body.appendChild(script);
  };

  var loadMultiScript = function(url_array) {
    load_cursor = 0;
    load_queue = url_array;
    loadScript();
  }

  return {
    load: loadMultiScript,
  };

})();  // end Loader

//loading ...
Loader.load([
            'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js', 
            './js/your.js',
            './js/my.js'
             ]);

load_queue是一个队列,保存需要依次加载的js的url。当一个js加载完毕后,load_cursor++用来模拟出队操作,然后加载下一个脚本。

onerror事件也添加了回调,用来处理无法加载的js文件。当遇到无法加载的js文件时停止加载,剩下的文件也不会加载了。

效果如下:

方式3

方式2是串行的去加载,我们稍加改进,让可以并行加载的js脚本尽可能地并行加载。

Loader = (function() {

  var group_queue;      // group list
  var group_cursor = 0; // current group cursor
  var current_group_finished = 0;  


  var loadFinished = function() {
    current_group_finished ++;
    if (current_group_finished == group_queue[group_cursor].length) {
      next_group();
      loadGroup();
    }
  };

  var next_group = function() {
    current_group_finished = 0;
    group_cursor ++;
  };

  var loadError = function(oError) {
    console.error("The script " + oError.target.src + " is not accessible.");
  };

  var loadScript = function(url) {
    console.log("load "+url);
    var script = document.createElement('script');
    script.type = "text/javascript";

    if (script.readyState){  //IE
        script.onreadystatechange = function() {
            if (script.readyState == "loaded" ||
                    script.readyState == "complete") {
                script.onreadystatechange = null;
                loadFinished();
            }
        };
    } else {  //Others
        script.onload = function(){
            loadFinished();
        };
    }

    script.onerror = loadError;

    script.src = url+'?'+'time='+Date.parse(new Date());
    document.body.appendChild(script);
  };

  var loadGroup = function() {
    if (group_cursor >= group_queue.length) 
      return;
    current_group_finished = 0;
    for (var idx=0; idx < group_queue[group_cursor].length; idx++) {
      loadScript(group_queue[group_cursor][idx]);
    }
  };

  var loadMultiGroup = function(url_groups) {
    group_cursor = 0;
    group_queue = url_groups;
    loadGroup();
  }

  return {
    load: loadMultiGroup,
  };

})();  // end Loader


//loading
var jquery = 'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
    your   = './js/your.js',
    my     = './js/my.js'
;
Loader.load([ [jquery, your], [my] ]);

Loader.load([ [jquery, your], [my] ]);代表着jqueryjs/your.js先尽可能快地加载和执行,等它们执行结束后,加载并执行./js/my.js

这里将每个子数组里的所有url看成一个group,group内部的脚本尽可能并行加载并执行,group之间则为串行。

这段代码里使用了一个计数器current_group_finished记录当前group中完成的url的数量,在这个数量和url的总数一致时,进入下一个group。

效果如下:

方式4

该方式是对方式3中代码的重构。

Loader = (function() {

  var group_queue = [];      // group list
  var current_group_finished = 0;  
  var finish_callback;
  var finish_context;

  var loadFinished = function() {
    current_group_finished ++;
    if (current_group_finished == group_queue[0].length) {
      next_group();
      loadGroup();
    }
  };

  var next_group = function() {
    group_queue.shift();
  };

  var loadError = function(oError) {
    console.error("The script " + oError.target.src + " is not accessible.");
  };

  var loadScript = function(url) {
    console.log("load "+url);
    var script = document.createElement('script');
    script.type = "text/javascript";

    if (script.readyState){  //IE
        script.onreadystatechange = function() {
            if (script.readyState == "loaded" ||
                    script.readyState == "complete") {
                script.onreadystatechange = null;
                loadFinished();
            }
        };
    } else {  //Others
        script.onload = function(){
            loadFinished();
        };
    }

    script.onerror = loadError;

    script.src = url+'?'+'time='+Date.parse(new Date());
    document.body.appendChild(script);
  };

  var loadGroup = function() {
    if (group_queue.length == 0) {
      finish_callback.call(finish_context);
      return;
    }
    current_group_finished = 0; 
    for (var idx=0; idx < group_queue[0].length; idx++) {
      loadScript(group_queue[0][idx]);
    }
  };

  var addGroup = function(url_array) {
    if (url_array.length > 0) {
      group_queue.push(url_array);
    }
  };

  var fire = function(callback, context) {
    finish_callback = callback || function() {};
    finish_context = context || {};
    loadGroup();
  };

  var instanceAPI = {
    load : function() {
      addGroup([].slice.call(arguments));
      return instanceAPI;
    },

    done : fire,
  };

  return instanceAPI;

})();  // end Loader


//loading
var jquery = 'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
    your   = './js/your.js',
    my     = './js/my.js'
;
// Loader.load(jquery, your).load(my).done();
Loader.load(jquery, your)
      .load(my)
      .done(function(){console.log(this.msg)}, {msg: 'finished'});

在调用多次load()函数后,必须调用done()函数。done()函数用来触发所有脚本的load。

方式5

这个方式是对方式4的重写。改进为调用load()时候尽可能去触发实际的load操作。

// 这里调试用的代码我没有删除

Loader = (function() {

    var group_queue  = [];      // group list

    //// url_item = {url:str, start: false, finished:false}

    // 用于调试
    var log = function(msg) {
        return;
        console.log(msg);
    }

    var isFunc = function(obj) { 
        return Object.prototype.toString.call(obj) == "[object Function]"; 
    }

    var isArray = function(obj) { 
        return Object.prototype.toString.call(obj) == "[object Array]"; 
    }

    var isAllStart = function(url_items) {
        for (var idx=0; idx<url_items.length; ++idx) {
            if (url_items[idx].start == false )
                return false;
        }
        return true;
    }

    var isAnyStart = function(url_items) {
        for (var idx=0; idx<url_items.length; ++idx) {
            if (url_items[idx].start == true )
                return true;
        }
        return false;
    }

    var isAllFinished = function(url_items) {
        for (var idx=0; idx<url_items.length; ++idx) {
            if (url_items[idx].finished == false )
                return false;
        }
        return true;
    }

    var isAnyFinished = function(url_items) {
        for (var idx=0; idx<url_items.length; ++idx) {
            if (url_items[idx].finished == true )
                return true;
        }
        return false;
    }

    var loadFinished = function() {
        nextGroup();
    };

    var showGroupInfo = function() {
        for (var idx=0; idx<group_queue.length; idx++) {
            group = group_queue[idx];
            if (isArray(group)) {
                log('**********************');
                for (var i=0; i<group.length; i++) {
                    log('url:     '+group[i].url);
                    log('start:   '+group[i].start);
                    log('finished:'+group[i].finished);
                    log('-------------------');
                }
                log('isAllStart: ' + isAllStart(group));
                log('isAnyStart: ' + isAnyStart(group));
                log('isAllFinished: ' + isAllFinished(group));
                log('isAnyFinished: ' + isAnyFinished(group));
                log('**********************');
            }
        }
    };

    var nextGroup = function() {
        while (group_queue.length > 0) {
            showGroupInfo();
            // is Func
            if (isFunc(group_queue[0])) {
                log('## nextGroup: exec func');
                group_queue[0]();  // exec
                group_queue.shift();
                continue;
            // is Array
            } else if (isAllFinished(group_queue[0])) {   
                log('## current group all finished');
                group_queue.shift();
                continue;
            } else if (!isAnyStart(group_queue[0])) {
                log('## current group no one start!');
                loadGroup();
                break;
            } else {
                break;
            }
        }
    };

    var loadError = function(oError) {
        console.error("The script " + oError.target.src + " is not accessible.");
    };

    var loadScript = function(url_item) {
        log("load "+url_item.url);
        url = url_item.url;
        url_item.start = true;
        var script = document.createElement('script');
        script.type = "text/javascript";

        if (script.readyState){  //IE
            script.onreadystatechange = function() {
                if (script.readyState == "loaded" ||
                        script.readyState == "complete") {
                    script.onreadystatechange = null;
                    url_item.finished = true;
                    loadFinished();
                }
            };
        } else {  //Others
            script.onload = function(){
                url_item.finished = true;
                loadFinished();
            };
        }

        script.onerror = loadError;

        script.src = url+'?'+'time='+Date.parse(new Date());
        document.body.appendChild(script);
    };

    var loadGroup = function() {
        for (var idx=0; idx < group_queue[0].length; idx++) {
            loadScript(group_queue[0][idx]);
        }
    };

    var addGroup = function(url_array) {
        log('add :' + url_array);
        if (url_array.length > 0) {
            group = [];
            for (var idx=0; idx<url_array.length; idx++) {
                url_item = {
                    url: url_array[idx],
                    start: false,
                    finished: false,
                };
                group.push(url_item);
            }
            group_queue.push(group);
        }
        nextGroup();
    };

    var addFunc = function(callback) {
        callback && isFunc(callback) &&  group_queue.push(callback);
        log(group_queue);
        nextGroup();
    };

    var instanceAPI = {
        load : function() {
            addGroup([].slice.call(arguments));
            return instanceAPI;
        },

        wait : function(callback) {
            addFunc(callback);
            return instanceAPI;
        }
    };

    return instanceAPI;

})();  // end Loader,这尼玛就是一个状态机


// loading
var jquery = 'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
    your   = './js/your.js',
    my     = './js/my.js'
;
// Loader.load(jquery, your).load(my);
Loader.load(jquery, your)
      .wait(function(){console.log("yeah, jquery and your.js were loaded")})
      .load(my)
      .wait(function(){console.log("yeah, my.js was loaded")});

上面的调用中,每次load时候会尝试马上加载和执行这些脚本,而不是像方式4那样要等done()被调用。

另外出现了新的函数wait,当wait之前的load和wait执行结束后,该wait中的匿名函数会被调用。

效果如下:

方式6 Promise+串行

Promise是一种设计模式。关于Promise,下面的几篇文章值得一看:

当前浏览器对Promise的支持情况如下:

使用Promise解决脚本动态加载问题的方案如下:

function getJS(url) {
    return new Promise(function(resolve, reject) {
        var script = document.createElement('script');
        script.type = "text/javascript";

        if (script.readyState){  //IE
            script.onreadystatechange = function() {
                if (script.readyState == "loaded" ||
                        script.readyState == "complete") {
                    script.onreadystatechange = null;
                    resolve('success: '+url);
                }
            };
        } else {  //Others
            script.onload = function(){
                resolve('success: '+url);
            };
        }

        script.onerror = function() {
            reject(Error(url + 'load error!'));
        };

        script.src = url+'?'+'time='+Date.parse(new Date());
        document.body.appendChild(script);

    });
}

//loading
var jquery = 'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
    your   = './js/your.js',
    my     = './js/my.js'
;

getJS(jquery).then(function(msg){
    return getJS(your);
}).then(function(msg){
    return getJS(my);
}).then(function(msg){
    console.log(msg);
});

这个实现中js是串行加载的。

效果如下:

方式7 Promise+并行

可以使用Promise.all使jqueryjs/your.js并行加载。

Promise.all([getJS(jquery), getJS(your)]).then(function(results){
    return getJS(my);
}).then(function(msg){
    console.log(msg);
});

方式8 Generator+Promise

Promise配合生成器(Generator)可以让js程序按照串行的思维编写。

关于生成器,下面的几篇文章值得一看:

浏览器的支持情况如下:

来两个典型的生成器示例:

示例1:

function *addGenerator() {
  var i = 0;
  while (true) {
    i += yield i;
  }
}

var adder = addGenerator();
console.log( adder.next().value );  // yield i时候暂停 (循环1)
console.log( adder.next(5).value ); // 循环1中yield i的结果为5,i+=5,进入下一个循环(循环2),循环2中yield i 暂停,返回5
console.log( adder.next(5).value ); // 循环2中yield i的结果为5
console.log( adder.next(5).value ); // 循环3中yield i的结果为5
console.log( adder.next(50).value ); //循环4中yield i的结果为50,i+=50,进入循环6

输出:

0
5
10
15
65

示例2:

function* idMaker(){
  var index = 0;
  while(index < 3)
    yield index++;
}

var gen = idMaker();

while ( result = gen.next() ) {
    if (!result.done) {
        console.log(result.done + ':' + result.value);
    } else{
        console.log(result.done + ':' + result.value);
        break;
    }
}

输出:

false:0
false:1
false:2
true:undefined

下面的文章介绍了如何搭配Promise和Generator:

Generator+Promise实现js脚本动态加载的方式如下:

function getJS(url) {
    return new Promise(function(resolve, reject) {
        var script = document.createElement('script');
        script.type = "text/javascript";

        if (script.readyState){  //IE
            script.onreadystatechange = function() {
                if (script.readyState == "loaded" ||
                        script.readyState == "complete") {
                    script.onreadystatechange = null;
                    resolve('success: '+url);
                }
            };
        } else {  //Others
            script.onload = function() {
                resolve('success: '+url);
            };
        }

        script.onerror = function() {
            reject(Error(url + 'load error!'));
        };

        script.src = url+'?'+'time='+Date.parse(new Date());
        document.body.appendChild(script);

    });
}

function spawn(generatorFunc) {
  function continuer(verb, arg) {
    var result;
    try {
      result = generator[verb](arg);  // 这个result是生成器的返回值,有value和done两个属性
    } catch (err) {
      return Promise.reject(err);
    }
    if (result.done) {
      return result.value;
    } else {
      return Promise.resolve(result.value).then(onFulfilled, onRejected);  // result.value是promise对象
    }
  }
  var generator = generatorFunc();
  var onFulfilled = continuer.bind(continuer, "next");
  var onRejected = continuer.bind(continuer, "throw");
  return onFulfilled();
}

//// loading

var jquery = 'http://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
    your   = './js/your.js',
    my     = './js/my.js'
;

// “串行”代码在这里
spawn(function*() {
    try {
        yield getJS(jquery);
        console.log('jquery has loaded');
        yield getJS(your);
        console.log('your.js has loaded');
        yield getJS(my);
        console.log('my.js has loaded');
    } catch (err) { 
        console.log(err);
    }
});

效果如下:

现有哪些工具可以实现动态加载

For Your Script Loading Needs列出了许多工具,例如lazyloadLABjsRequireJS等。

有些工具也提供了新的思路,例如LABjs中可以使用ajax获取同域下的js文件。

其他

async

q

co

资料

Script Execution Control

LABJS源码浅析

Dynamic Script Execution Order

script节点的onload,onreadystatechange事件

readystatechange - MDN

readyState property - MSDN

Loading JavaScript without blocking

More Repositories

1

TextRank4ZH

🌳从中文文本中自动提取关键词和摘要
Python
3,067
star
2

Pinyin2Hanzi

拼音转汉字, 拼音输入法引擎, pin yin -> 拼音
Python
552
star
3

huno

A responsible theme for Hexo
CSS
495
star
4

another-tutorial-about-java-web

😿 another tutorial about java web
340
star
5

tencent-open-source

腾讯开源作品整理
Python
274
star
6

ChineseTone

[本项目不再维护] 将汉字转换为拼音, 支持多音字,拼音 -> pin yin
Hack
204
star
7

kmedoids

[Unmaintained] The Python implementation of k-medoids.
Python
119
star
8

flask-tutorial

Python Flask Web 框架入门教程
50
star
9

awesome-toc

generate awesome toc for web page
HTML
44
star
10

jianshu-site-search

简书站内搜索
Python
24
star
11

mybatis-tutorial

mybatis 入门教程
Java
16
star
12

Flask-dashboard-for-UPYUN

基于Python Flask框架的又拍云管理工具
JavaScript
16
star
13

paste-as-markdown

Paste HTML as Markdown
JavaScript
5
star
14

TPP

[Deprecated] A PHP framework
PHP
3
star
15

mini-pinyin

Get tone(pinyin) of Chinese character.
JavaScript
2
star
16

n-source

源码注释 https://github.com/tj/n
Shell
2
star
17

rq-source

源码注释 https://github.com/nvie/rq
Python
2
star
18

random-password-cli

Generate random password in cli.
JavaScript
2
star
19

center-text

Center the output in terminal.
Shell
1
star
20

terminal-text-width

Get the number of columns occupied by specified text in terminal.
JavaScript
1
star
21

pocha-demos

pocha is python's mocha.
Python
1
star
22

lyric-player

Play lyric in terminal.
JavaScript
1
star
23

p

PS1 management
Python
1
star
24

Timestamp-Workflow

Alfred Workflow 时间戳转换
Python
1
star
25

wxapp-find-pinyin

微信小程序:查拼音
JavaScript
1
star