npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

lovol

v0.5.9

Published

lovol

Readme

一、router介绍

这是一个简单的路由中间件,旨在帮助你快速搭建一个小型网站。将路由对象化Json的结构思维让你理解更加容易,支持常用的权限控制、模板、后端渲染、静态化等常用功能。 初期文档比较简陋,在后期会慢慢完善。

1.1开始

1.1.1引入并导出常用方法和变量

这些方法和变量都是常用的,可以先导出备用。

const lovol = require('lovol');

const {
    API, AUTH, STATIC,//方法描述
    GET, POST, PUT, DELETE,//请求方法
    initRouter, clearTemplate, clearStatic, clearCache//常用方法
} = lovol.router;

1.1.2初始化

第一个参数是路由的对象,第二个是配置参数。

const router = initRouter({
        [AUTH]() {//访问控制

            return true;
        },
        about() {//domain/about

            return {[STATIC]: true, qwe: 123}//可以静态化domain/about/1.html
        },
        admin: {
            async [AUTH]({request, response, ...data}) {

                // console.log(data);

                // return data.redirect('/about');//重定向

                return true;
            },
            [GET]({redirect, view}) {//默认的GET路由//domain/admin

                // redirect('/about');

                return view('/about');//使用别的页面
            },
            log: {
                async [GET]() {//domain/admin/log

                    return {qwe: 123};
                },
                api: {
                    [API]: true,//定义为接口,没有页面
                    [GET]() {

                    },
                    [POST]() {

                    }
                }
            },
        }
    }
    ,
    {
        max: 1024 * 1024 * 16,//请求的最大尺寸
        lang: 'zh-cn',//本地化
        charset: 'utf-8',
        defaultMethod: [POST],//表单请求会返回页面的方法
        // cacheTimeout: 10,//内存缓存的时间
        cacheTimeout: false,
        serverError: false,//是否显示服务器端错误
        bridge: {qwe: 123},//对象桥
        mime: {//mime-type支持格式
            json: 'application/json',
        },
    });

1.1.2使用

nodejs中有HTTP、HTTP/2、HTTPS,这里以HTTP为例。

http.createServer(async (request, response) => {
   
       let result = await router(request, response);//调用方法传入request,response
   
       // console.log(result);
   
       response.writeHead(
           result.head.statusCode,
           result.head.statusMessage,
           result.head.headers
       );
   
       if (typeof result.body.pipe === "function")
   
           result.body.pipe(response);//路由结果为流
   
       else
   
           response.end(result.body);//路由结果为值
   
   }).listen(80);

1.2目录结构

将会自动生成几个目录:shared(共享模板)、static(静态缓存)、views(页面模板)、wwwroot(资源目录)

┌ shared  ─ admin ┬ head.htm  //模板缓存文件
│                 ├ head.html //模板文件
│                 └ foot.html
├ static  ─ about ┬ 1.html    //静态缓存文件
│                 └ data.json //静态缓存文件
├ views   ┬ about.html
│         ├ admin.html
│         └ admin ─ log.html
└ wwwroot ┬ index.html
          ├ js
          ├ css
          └ img
  • 我不太会写文档这些,我就直接上例子吧。

1.3实例

1.3.1访问控制

[AUTH]() {
    ...
    return false;//false:浏览器会收到一个403错误,true:路由会继续向下进行。
    ...
}
        
//我比较懒直接贴代码
async [auth]({pathArray, getCookie, redirect, request}) {

    let cookie = getCookie();//校验部分cookie可以使用httponly来防止js直接篡改。
    
    let auth = cookie._token && cookie._token.length === 32 &&
        cookie._token === md5('my key 2019' + cookie._uid + cookie._menu);//验证cookie完整性
    
    if (pathArray[0] === 'login') {
    
        if (auth) {
    
            return redirect('/admin');//可以直接重定向
        }

    } else if (pathArray[0] === 'logout') {

        if (!auth) {

            return redirect('/admin');//可以直接重定向
        }

    } else if (!auth) {

        return redirect('/admin/login');

    } else if (pathArray.length === 0)

        return true;

    else if (cookie.menu)

        return cookie.menu.split(',').includes(pathArray[0]);//这是复杂一些的判断。

    else

        return false;//也可以返回403错误

    return true;
},
//这里面你还能做ip限制、流量控制、防恶意访问等你能想到的操作。

1.3.2目录默认文件

只要有defaultMethod里面的方法都会生成一个默认的文件根目录就是index.html,其他就是 [目录名].html。如果defaultMethod加入了POST。

initRouter({
    [GET]() {}
    about: null,
    admin: {
        [POST]() {},
        log() {}
    }
});
//生成如下文件
├ views   ┬ index.html       // [GET]() {}
          ├ about.html       // about: null
          ├ admin.html       // admin: {[POST]() {}}
          └ admin ─ log.html // admin: {log() {}}

1.3.3动态转静态

return的对象里面加入[STATIC]: true值这样浏览器以访问静态文件的时候就会在static目录里面保存相应的静态文件,第二次访问同一个文件的时候后台就不会重新渲染,而是直接访问这个静态文件。 所有这个的使用要严格判断控制,不然会被而已生成静态文件消耗服务器资源,好处就是能自动缓存文件,比如产品页。

product({pathArray, view}) {//domain/about
    
    let [key, id] = pathArray;
    
    id = id.split('.')[0];
    
    if (key === 'id' && isValid(id))
   
        return {[STATIC]: true, qwe: 123}//可以静态化domain/about/1.html
        
    else
    
        return view('/404', {id});
},
//生成文件
├ static ─ product - id ┬ 1.html // domain/product/id/1.html
                        └ 9.html // domain/product/id/9.html

1.3.4后端渲染

后端渲染是将路由到的方法对应views下的html文件动态执行返回渲染后的html文件,一个url路由结果如下, 比如domain/notify/index.html(含html后缀),它路由的顺序如下,逐条去搜索,都没找到就返回404:

1、/wwwroot/notify/index.html

2、/static/notify/index.html

3、根据路由路劲去找相应的方法,如果方法属于[API]则直接返回返回值

4、如果方法不是[API]再找/views/notify/index.html,有则将返回值传入scope里面去渲染模板,没有则返回404。

再比如domain/notify/index(不含html后缀)就不再去/wwwroot和/static目录去匹配了。如果在notify下找到了index的默认方法(GET、POST)就去/views 目录下找相应的模板文件,将方法返回值传入scope里面去渲染,将结果返给浏览器。如果没有则使用notify的默认方法和对应模板文件去渲染将多余的路由值(index)传入scope(pathArray: ['index'])中去渲染, 如果notify也没有就使用根路由的默认方法和模板文件去渲染,如果根目录也没有则返回404。

  • 注意,因为这个特性所以在用路由带参数的时候(domain/notify/[参数1]/[参数2];domain/notify/qwe/123),最好是notify就是节点的末端这样效率是最高的, 不然它会先遍历节点在子节点都没匹配的时候再选中notify方法来渲染。

1.4关键字

1.4.1关键字1:#{}

//代码
...
scope.test.key = 123;
...
<div>#{scope.test.key}</div>
...
//结果
<div>123</div>

//可以等价的看作
`<div>${scope.test.key}</div>`

//所以这样用也是一样的
<div>#{1+1}</div>           //<div>${1+1}</div>           //<div>2</div>
<div>#{parseInt(1.5)}</div> //<div>${parseInt(1.5)}</div> //<div>1</div>

//这个也能转义
<div>\#{1+1}</div>  //<div>#{1+1}</div>
<div>\#{</div>      //<div>#{</div>

1.4.2关键字2:{/}和{~}

//共享模板
/shared/comm/head.html
<h1>{scope.title}</h1>

//代码
...
{/comm/head,{title:'这是标题'}}//参数可缺省
<p>这是内容</p>
...
//结果
<h1>这是标题</h1>
<p>这是内容</p>

//以上面的代码我前提,则:
...
{~comm/head,{title:'标题标题标题'}}
<p>这是内容</p>
...

//结果
<h1>这是标题</h1>
<p>这是内容</p>
因为'{~}'会在.html生成.htm,下次使用则直接使用.htm文件,不在运算.html文件里的。

1.4.3关键字3: <script server></script>

//代码
<div>
<script server>
    if (1) {
        write(`<h1>${1+1}</h1>`);
    } else {
        write(`<h2>我不会被执行</h2>`);
    }
</script>
</div>

//结果
<div>
<h1>2</h1>
</div>

1.4.4关键字混合使用

/shared/foot.html
<p>#{scope.world}</p>

//代码
<div>
<script server>
    scope.share = 'foot';
    scope.world = 'hello';
</script>
{/#{scope.share}}
</div>

//结果
<div>
<p>hello</p>
</div>

1.4.5错误提示

//代码
<div>
{/qwe}
<script server>
    write(qwe)
</script>
</div>

//结果
<div>
    <!-- 未加载源摸板 /shared/qwe.html  -->
    <!-- server script 代码有误:ReferenceError: qwe is not defined-->
</div>

1.5路由方法的参数

initRouter({
    [GET](scope) {
        console.log(Object.keys(scope));
        //[ 'view',//使用其它模板,可使API返回页面:view('/more/help')
            'query',//url参数:domain/test?qwe=123&asd=456 //query: {qwe: 123, asd: 456}
            'bridge',//桥对象:bridge: {qwe: 123, asd: 456}//第二个参数中传入
            'urlObj',//url解析后的对象:Url {port: '', href: '', query: {}, ...}
            'nonView',//不返回页面,直接返回数据:nonView({qwe: 123})
            'request',//原生对象request
            'response',//原生对象response
            'writeBody',//等价response.write();
            'redirect',//重定向:redirect('http://domain2/')
            'getCookie',//获取cookie对象
            'setCookie',//设置cookie:setCookie([{key: '', value: '', path: '', domain: '', secure: '', httponly: '', expires: ''}]);
            'delCookie',//删除cookie:delCookie([{key: '', path: ''}, {key: '', path: ''}]);
            'data'//表单提交的数据:data: {qwe: 123, asd: 456},如果有文件则:data.file1.save('tmp', 'f1.jpg');则保存为:/wwwroot/tmp/f1.jpg
             ]
    }
}, {bridge: {qwe: 123, asd: 456}});