命令行工具 —— 手写类似 http-server 的静态服务器

前端开发者必备工具:http-server 使用详解 在前端开发中,我们经常需要一个简单而强大的静态文件服务器来测试我们的应用。http-server 就是这样一个轻量级的命令行 HTTP 服务器,它简单易用,无需配置,是前端开发者的得力助手。本文将深入探讨 http-server 的安装、基本使用、高级配置以及在实际开发中的应用场景。 阅读详情

在这里插入图片描述


阅读原文


前言

npm 里有个 http-server 的模块,是一个简单的、零配置的 HTTP 服务,它非常强大,同时非常简单,可以方便的帮助我们开启本地服务器,以及局域网共享,可以用来做测试,开发,学习时的环境配置,我们本节就模拟 http-server 实现一个自己的启动本地服务的命令行工具。


http-server 使用

http-server 服务器通过命令行启动,使用时需要安装,安装命令如下:

npm install http-server -g

启动本地服务器时在根目录下执行下面命令即可:

http-server [path] [option]

path 默认情况下是 ./public,否则是 ./,启动后可以通过 http://localhost:8080 来访问服务器,options 为其他参数, npm 官方文档 https://www.npmjs.com/package/http-server 有详细说明。

当通过浏览器访问 http://localhost:8080 以后,会将我们服务器根目录的目录结构显示在浏览器页面上,当点击文件夹时,可以继续显示内部的文件和文件夹,当点击文件时会直接通过服务器访问文件,并将文件内容显示在浏览器页面上。


实现命令行工具依赖的模块

1、chalk 模块

chalk 模块是用来控制命令行输出的文字颜色的第三方模块,使用前需要安装,安装命令如下:

npm install chalk

chalk 模块的用法如下,模块支持的颜色和更多的 API 可以在 npm 官方文档 https://www.npmjs.com/package/chalk 中查看。

// 文件位置:~static/tests/staticchalk-test.js
const chalk = require("chalk");

// 在命令行打印绿色和红色的 hello
console.log(chalk.green("hello"));
console.log(chalk.red("hello"));

在命令行窗口输入 node chalk-test.js 查看命令行打印 hello 的颜色。

2、debug 模块

debug 模块可以匹配当前环境变量 DEBUG 的值并输出相关信息,作用在于命令行工具可以根据不同情况输出的信息进行调试,是第三方模块,使用前需安装,命令如下。

npm install debug

debug 的简单使用如下,如果想了解更详细的 API 可以在 npm 官方文档 https://www.npmjs.com/package/debug 中查看。

// 文件位置:~static/tests/debug-test1.js —— 用法 1
const debug = require("debug")("hello");

debug("hi panda");

当我们在命令行中执行 node debug-test1.js 时发现命令窗口什么也没有打印,那是因为当前根目录的环境变量 DEBUG 的值必须和我们设置的 hello 相匹配才会打印相关信息。

设置环境变量,Window 可以通过 set DEBUG=hello 设置,Mac 可以通过 export DEBUG=hello 设置,设置环境变量后再次执行 node debug-test.js,我们会发现命令行打印出了下面内容。

hello hi panda +0ms

其中 hello 为我们设置 DEBUG 环境变量的值,hi panda 为调试方法 debug 方法打印的信息,+0ms 为距离上次执行的间隔时间。

// 文件位置:~static/tests/debug-test2.js —— 用法 2
const debugA = require("debug")("hello:a");
const debugB = require("debug")("hello:b");

debugA("hi panda");
debugB("hello panda");

上面的代码目的是可以让我们不同的 debug 方法可以匹配不同的环境变量,所以需要重新将环境变量的值设置为 hello:*,这样再次执行 node debug-test2.js 发现命令窗口打印了如下内容。

hello:a hi panda +0ms
hello:b hello panda +0ms

使用 debug 的好处就是可以在开发的时候打印一些调试用的信息,在开发完成后因为匹配不到环境变量,这些信息就会被隐藏。

3、commander 模块

commander 是著名的 Node 大神 TJ 的 “作品”,是一个开发命令行工具的解决方案,提供了用户命令行输入和参数解析的强大功能,commander 是第三方模块,使用时需要安装,命令如下。

npm install commander

基本用法如下:

// 文件位置:~static/tests/commander-test1.js
let commander = require("commander");

// 解析 Node 进程执行时的参数
commander.version("1.0.0").parse(process.argv);

上面文件中 version 方法代表当前执行文件模块的版本,parse 为解析参数为当前命令行进程参数的方法,process.argv 为参数集合(数组),第一个参数为执行的 node.exe 文件的绝对路径,第二个参数为当前执行文件的绝对路径,后面为通过命令行传入的参数,如 --host、--port 等。

在命令行执行 node commander-test.js --help 时默认会在命令行输出如下信息:

Usage: [options]
Options:
   -V, --version output the version number
   -h, --help output usage information

当然在我们的命令行工具中,参数不只 --version 和 --help 两个,我们更希望更多的参数更多的功能,并且可定制的描述信息,使用案例如下。

// 文件位置:~static/tests/commander-test2.js
let commander = require("commander");

// 解析 Node 进程执行时的参数
commander
    .version("1.0.0")
    .usage("[options]")
    .option('-p, --port <n>', 'server port')
    .option('-o, --host <n>', 'server host')
    .option('-d, --dir <n>', 'server dir')
    .parse(process.argv);

console.log(commander.port); // 3000
console.log(commander.host); // localhost
console.log(commander.dir); // public

在执行命令 node commander-test2.js --help 后会在命令窗口输出如下信息:

Usage: yourname-http-server [options]
Options:
   -V, --version output the version number
   -p, --port server port
   -o, --host server host
   -d, --dir server dir
   -h, --help output usage information

usage 方法可以让我们详细的定制参数的类型和描述,option 方法可以让我们添加执行 --help 指令时打印的命令以及对应的描述信息。

执行下面命令:

node commander-test2.js --port 3000 --host localhost --dir public

执行命令后我们发现其实给我们的参数挂在了 commander 对象上,方便我们取值。

在我们使用别人的命令行工具时会发现在上面输出信息的时候经常会在下面输出 How to use 的列表,更详细的描述了每条命令的作用及用法。

// 文件位置:&#126;static/tests/commander-test3.js
let commander = require("commander");

// 必须写到 parse 方法的前面
commander.on("--help", function () {
    console.log("\r\n  How to use:")
    console.log("    yourname-http-server --port <val>");
    console.log("    yourname-http-server --host <val>");
    console.log("    yourname-http-server --dir <val>");
});

// 解析 Node 进程执行时的参数
commander
    .version("1.0.0")
    .usage("[options]")
    .option('-p, --port <n>', 'server port')
    .option('-o, --host <n>', 'server host')
    .option('-d, --dir <n>', 'server dir')
    .parse(process.argv);

再次执行命令 node commander-test2.js --help 后会在命令窗口输出如下信息:

Usage: yourname-http-server [options]
Options:
   -V, --version output the version number
   -p, --port server port
   -o, --host server host
   -d, --dir server dir
   -h, --help output usage information
How to use:
   yourname-http-server --port
   yourname-http-server --host
   yourname-http-server --dir

以上是 commander 模块的基本用法,如想了解更详细的 API 和使用案例可以到 npm 官方文档查看,地址如下 https://www.npmjs.com/package/commander 。


实现静态服务的功能

1、文件目录

static
|- bin
| |- yourname-http-server.js
|- public
| |- css
| | |- style.css
| |- index.html
| |- 1.txt
|- tests
| |- chalk-test.js
| |- commander-test1.js
| |- commander-test2.js
| |- commander-test3.js
| |- debug-test1.js
| |- debug-test2.js
|- config.js
|- index.html
|- index.js
|- package-lock.json
|- package.json

2、配置文件

在启动静态服务的时候,我们希望可以通过命令行传参的形式来定义当前启动服务的主机名端口号,以及默认检索的文件根目录,所以需要配置文件来实现灵活传参。

// 文件位置:&#126;static/config.js
module.exports = {
    port: 3000,
    host: "localhost",
    dir: process.cwd()
}

在上面的配置中,默认端口号为 3000,默认主机名为 localhost,我们设置默认检索文件的根目录为通过命令行启动服务器的目录,而 process.cwd() 的值就是我们启动命令行执行命令的目录的绝对路径。

3、创建服务器 Server 类

因为我们的命令行工具启动本地服务可能是在系统的任意位置,或者指定启动服务访问的域,提高可配置性,并且要更方便给服务器扩展更多的方法处理不同的逻辑,所以需要创建一个 Server 类。

// 文件位置:&#126;static/index.js —— Server 类的创建
// 引入依赖
const http = require("http");
const url = require("url");
const path = require("path");
const fs = require("mz/fs");
const mime = require("mime");
const zlib = require("zlib");
const chalk = require("chalk");
const ejs = require("ejs");
const debug = require("debug")("http:a");

// 引入配置文件
const config = require("./config");

// 读取模板文件
const templateStr = fs.readFileSync(path.join(__dirname, "index.html"), "utf8");

class Server {
    constructor() {
        this.config = config; // 配置
        this.template = templateStr; // 模板
    }
}

我们在上面代码中引入了 config.js 配置文件,读取了用于启动服务后展示页面 index.html 的内容,并都挂在了 Server 类的实例上,目的是方便内部的方法使用以及达到不轻易操作全局变量的目的。

4、启动服务器的 start 方法

后面为了方便代码的拆分,我们将原型上的方法统一使用 Server.prototype.xxx 的方式来书写,实际的案例都是写在 Server 类里面的。

// 文件位置:&#126;static/index.js —— start 方法
Server.prototype.start = function () {
    // 创建服务
    const server = http.createServer(this.handleRequest.bind(this));

    // 从配置中解构端口号和主机名
    let { port, host } = this.config;

    // 启动服务
    server.listen(port, host, () => {
        debug(`server start http://${host}:${chalk.green(port)}`);
    });
}

在 start 方法中创建了服务,在启动服务时只需要创建 Server 的实例并调用 start 方法,由于服务的回调中会处理很多请求响应的逻辑,会导致 start 方法的臃肿,所以将服务的回调函数抽取成 Server 类的一个实例方法 handleRequest,需要注意的是 handleRequest 内部的 this 指向需要我们修正。

在启动服务时我们根据配置可以灵活的设置服务的地址,当设置 host 后,服务将只能通过 host 的值作为主机名的地址访问静态服务器,启动服务的提示我们通过匹配环境变量 DEBUG 的 debug 方法来打印,并将端口号设置成绿色。

5、服务回调 handleRequest 方法

在实现 handleRequest 之前我们应该了解要实现的功能,在 http-server 中,如果访问的服务地址路径后面指定具体要访问的文件,并且当前启动服务根目录按照访问路径可以查找到文件,将文件内容读取后响应给客户端,如果没指定文件,应该检索当前启动服务根目录或默认设置的目录结构,并将文件的结构通过模板渲染成超链接后将页面响应给客户端,再次点击页面的上的链接,如果是文件,直接读取并响应文件内容,如果是文件夹,则继续检索内部结构通过模板渲染成页面。

// 文件位置:&#126;static/index.js —— handleRequest 方法
Server.prototype.handleRequest = async function (req, res) {
    // 获取访问的路径,默认为 /
    this.pathname = url.parse(req.url, true).pathname;

    // 将访问的路径名转换成绝对路径,取到的 dir 就是绝对路径
    this.realPath = path.join(this.config.dir, this.pathname);

    debug(realPath); // 打印当前访问的绝对路径,用于调试

    try {
        // 获取 statObj 对象,如果 await 同步使用 try...catch 捕获非法路径
        let statObj = await fs.stat(this.realPath);

        if (statObj.isFile()) {
            // 如果是文件,直接返回文件内容
            this.sendFile(req, res, statObj);
        } else {
            // 如果是文件夹则检索文件夹通过模板渲染后返回页面
            this.sendDirDetails(req, res, statObj);
        }
    } catch (e) {
        // 如果路径非法,发送错误响应
        this.sendError(req, res, e);
    }
}

handleRequest 由于内部需要使用异步操作获取 statObj 对象,所以我们使用了 async 函数,为了函数内部可以使用 await 避免异步回调嵌套,由于 await 会等待到异步执行完毕后继续向下执行,我们可以使用 try...catch... 捕获非法的访问路径,并做出错误响应。

如果路径合法,我们需要检测访问路径对应的是文件还是文件夹,如果是文件则执行响应内容的逻辑,是文件夹执行检索文件夹渲染内部文件列表返回页面的逻辑。

所以我们将错误处理逻辑、响应文件内容逻辑和返回文件夹详情页面的逻辑分别抽离成 Server 类的三个实例方法 sendError、sendFile 和 sendDirDetails,使得 handleRequest 方法逻辑清晰且不那么臃肿。

6、错误响应 sendError 方法

在服务器处理不同的请求和响应时可能需要处理不同的错误,这些错误的不同就是捕获错误对象的不同,所以我们的 sendError 方法为了更方便的或取请求参数、处理响应以及更好的复用,将参数设置为请求对象、响应对象和错误对象。

// 文件位置:&#126;static/index.js —— sendError 方法
Server.prototype.sendError = function (req, res, err) {
    // 打印错误对象,方便调试
    console.log(chalk.red(err));

    // 设置错误状态码并响应 Not Found
    res.statusCode = 404;
    res.end("Not Found");
}

7、渲染目录 sendDirDetails 方法

在渲染文件夹详情之前我们首先要做的就是异步读取文件目录,所以我们同样使用 async 函数来实现,NodeJS 中有很多渲染页面的模板,我们本次使用 ejs,语法简单,比较常用,ejs 为第三方模块,使用前需安装,更详细的用法可参照 npm 官方文档 https://www.npmjs.com/package/ejs。

npm install ejs

sendDirDetails 的参数为请求对象、响应对象和 statObj。

// 文件位置:&#126;static/index.js —— sendDirDetails 方法
Server.prototype.sendDirDetails = async function (req, res, statObj) {
    // 读取当前文件夹
    let dirs = await fs.readdir(this.realPath);

    // 构造模板需要的数据
    dirs = dirs.map(dir => ({ name: dir, path: path.join(this.pathname, dir)}));

    // 渲染模板
    let pageStr = ejs.render(this.template, { dirs });

    // 响应客户端
    res.setHeader("Content-Type", "text/html;charset=utf8");
    res.end(pageStr);
}

还记得 Server 类的实例属性 template 存储的就是我们的模板(字符串),里面写的就是 ejs 的语法,我们使用 ejs 模块渲染的 render 方法可以将模板中的 JS 执行,并用传给该方法的参数的值替换掉模板中的变量,返回新的字符串,我们直接将字符串响应给客户端即可。

注意:在构建模板数据的时候 path 为超链接标签要跳转的路径,如果直接使用 dir 的值,多级访问还是会在根目录去查找,所以路径非法会返回 Not Found,我们需要在每次访问的时候都将上一次访问的路径与当前访问的文件夹或文件名进行拼接,保证路径的正确性。

8、ejs 模板 index.html

上面已经知道了该怎样使用 ejs 对模板进行渲染,也对模板构造了数据,接下来就是使用 ejs 的语法编写我们的模板内容。

<!-- 文件位置:&#126;static/index.html —— 模板 -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Server</title>
</head>
<body>
    <%dirs.forEach(function (item) {%>
        <li><a href="<%=item.path%>"><%=item.name%></a></li>
    <%})%>
</body>
</html>

模板中 JS 逻辑使用 <% %> 包裹,使用 <%= %> 输出变量。

9、返回文件内容 sendFile 方法

由于都是根据路径查找或操作文件目录并做出响应,sendFile 方法与 sendDirDetails 方法的参数相同,分别为 req、res 和 statObj。

// 文件位置:&#126;static/index.js —— sendFile 方法
Server.prototype.sendFile = function (req, res, statObj) {
    // 设置和处理缓存
    if (this.cache(req, res, statObj)) {
        res.statusCode = 304;
        return res.end();
    }

    // 创建可读流
    let rs = fs.createReadStream(this.realPath);

    // 响应文件类型
    res.setHeader("Content-Type", `${mime.getType(this.realPath)};charset=utf8`);

    // 压缩
    let zip = this.compress(req, res, statObj);
    if (zip) return rs.pipe(zip).pipe(res);

    // 处理范围请求
    if (this.range(req, res, statObj)) return;

    // 响应文件内容
    rs.pipe(res);
}

其实上面的方法通过在根目录执行 node index.js 启动服务后,通过我们默认配置的地址访问服务器,表面上就已经实现了 http-server 的功能,但是我们为了服务器的性能和功能更强大,又在这基础上实现了缓存策略、服务器压缩和处理范围请求的逻辑。

如果对缓存策略、服务器压缩和范围请求不了解可以看下面三篇文章:

我们将上面的三个功能分别抽离成了 Server 类的三个原型方法,cache、compress 和 range,并且这三个方法的参数都为 req、res 和 statObj。

10、缓存策略 cache 方法

我们本次的缓存兼容 HTTP 1.0 和 HTTP 1.1 版本,并且同时使用强制缓存和协商缓存共同存在的策略。

// 文件位置:&#126;static/index.js —— cache 方法
Server.prototype.cache = function (req, res, statObj) {
    // 创建协商缓存标识
    let etag = statObj.ctime.toGMTString() + statObj.size;
    let lastModified = statObj.ctime.toGMTString();

    // 设置强制缓存
    res.setHeader("Cache-Control", "max-age=30");
    res.setHeader("Expires", new Date(Date.now() + 30 * 1000).toUTCString());

    // 设置协商缓存
    res.setHeader("Etag", etag);
    res.setHeader("Last-Modified", lastModified);

    // 获取协商缓存请求头
    let { "if-none-match": ifNodeMatch, "if-modified-since": ifModifiedSince } = req.headers;

    if (etag !== ifNodeMatch && lastModified !== ifModifiedSince) {
        return false;
    } else {
        return true;
    }
}

我们使用的缓存策略为同时设置强制缓存和协商缓存,当强制缓存有效期内再次请求不会访问服务器,待强制缓存过期再次请求执行协商缓存策略,带标识访问服务器进行确认,确认的同时重新设置强制缓存和协商缓存的响应头信息,如果协商缓存任然生效,则直接返回 304 状态码,如果协商缓存失效则读取文件内容返回浏览器。

11、服务器压缩 compress 方法

为了减少文件数据在传输过程中消耗的流量和时间,我们在浏览器支持解压的情况下使用服务器压缩功能,浏览器会在请求时默认发送请求头 Accept-Encoding 通知我们的服务器当前支持的压缩格式,我们要做的就是按照压缩格式的优先级进行匹配,按照最高优先级的压缩格式进行压缩,将压缩后的数据返回,并通过响应头 Content-Encoding 通知浏览器当前的压缩格式(压缩流的本质为转化流)。

// 文件位置:&#126;static/index.js —— compress 方法
Server.prototype.compress = function (req, res, statObj) {
    // 获取浏览器支持的压缩格式
    let encoding = req.headers["accept-encoding"];

    // 支持 gzip 使用 gzip 压缩,支持 deflate 使用 deflate 压缩
    if (encoding && encoding.match(/\bgzip\b/)) {
        res.setHeader("Content-Encoding", "gzip");
        return zlib.createGzip();
    } else if (encoding && encoding.match(/\bdeflate\b/)) {
        res.setHeader("Content-Encoding", "deflate");
        return zlib.createDeflate();
    } else {
        return false; // 不支持压缩返回 false
    }
}

当浏览器支持压缩时,compress 方法返回的为优先级最高压缩格式的压缩流,不支持返回 false,存在压缩流,则将数据压缩并响应浏览器,与不压缩响应不同的是,需要使用压缩流将可读流转化为可写流写入响应 res 中,所以可读流执行了两次 pipe 方法。

12、处理范围请求 range 方法

range 方法处理的场景为客户端发送请求只想获取文件的某个范围的数据,此时通过 range 方法读取文件范围对应的内容响应给客户端,通过响应头 Accept-Ranges 通知浏览器当前响应范围请求,通过响应头 Content-Range 通知客户端响应的范围以及文件的总字节数。

// 文件位置:&#126;static/index.js —— range 方法
Server.prototype.range = function (req, res, statObj) {
    // 获取 range 请求头
    let range = req.headers["range"];

    if (range) {
        // 获取范围请求的开始和结束位置
        let [, start, end] = range.match(/(\d*)-(\d*)/);

        // 处理请求头中范围参数不传的问题
        start = start ? ParseInt(start) : 0;
        end = end ? ParseInt(end) : statObj.size - 1;

        // 设置范围请求响应
        res.statusCode = 206;
        res.setHeader("Accept-Ranges", "bytes");
        res.setHeader("Content-Range", `bytes ${start}-${end}/${statObj.size}`);
        fs.createReadStream(this.realPath, { start, end }).pipe(res);
        return true;
    } else {
        return false;
    }
}

range 方法默认返回值为布尔值,当不是范围请求时返回值为 false,则直接向下执行 sendFile 中的代码,正常读取文件全部内容并响应给浏览器,如果是范围请求则会处理范围请求后在直接结束后返回 true,会在 sendFile 中直接 return,不再向下执行。


将静态服务器关联到命令行

1、命令行启动服务器

http-server 实际上是通过命令行启动、并传参的,我们需要将我们的程序与命令行关联,关联命令行只需以下几个步骤。

首先,在根目录 package.json 文件中加入 bin 字段,值为对象,对象内属性为命令名称,值为对应执行文件的路径。

文件位置:~static/package.json

{
  "name": "yourname-http-server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "dependencies": {
    "chalk": "^2.4.1",
    "commander": "^2.17.1",
    "debug": "^3.1.0",
    "ejs": "^2.6.1",
    "mime": "^2.3.1",
    "mz": "^2.7.0"
  },
  "bin": {
    "yourname-http-server": "bin/yourname-http-server.js"
  },
  "devDependencies": {},
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

其次,在 yourname-http-server.js 文件中首行加入注释 #! /usr/bin/env node,在命令行执行命令时,默认会以 Node 执行 yourname-http-server.js 文件。

最后,想要使用我们的命令启动 yourname-http-server.js 文件,则需要将这条命令连接到全局(与 -g 安装效果相同),在当前根目录下执行以下命令。

npm link

当在命令行执行 yourname-http-server 时,Node 会默认执行 yourname-http-server.js 文件。

2、命令行的参数传递

我们现在知道在命令行执行命令后用 Node 启动的文件为 yourname-http-server.js,在启动文件时我们应该启动我们的服务器,并结合 commander 模块的参数解析,则需要用命令行传递的参数替换掉 config.js 中的默认参数。

// 文件位置:&#126;static/bin/yourname-http-server.js —— 命令行执行文件
const commander = require("commander");
const Server = require("../index");

// 增加 How to use
commander.on("--help", function () {
    console.log("\r\n  How to use: \r\n")
    console.log("    zf-server --port <val>");
    console.log("    zf-server --host <val>");
    console.log("    zf-server --dir <val>");
});

// 解析 Node 进程执行时的参数
commander
    .version("1.0.0")
    .usage("[options]")
    .option("-p, --port <n>", "server port")
    .option("-o, --host <n>", "server host")
    .option("-d, --dir <n>", "server dir")
    .parse(process.argv);

// 创建 Server 实例传入命令行解析的参数
const server = new Server(commander);

// 启动服务器
server.start();

我们之前把 config.js 的配置直接挂在了 Server 实例的 config 属性上,创建服务使用的参数也是直接从该属性上获取的,因此我们要用 commander 对象对应的参数覆盖实例上 config 的参数,所以在创建 Server 实例时传入了 commander 对象,下面稍微修改 Server 类的部分代码。

// 文件位置:&#126;static/index.js —— Server 类
class Server {
    constructor(options) {
        // 通过解构赋值将 options 的参数覆盖 config 的参数
        this.config = { ...config, ...options }; // 配置
        this.template = templateStr; // 模板
    }
}

执行下面命令,并通过浏览器访问 http://127.0.0.1:4000 来测试服务器功能。

yourname-http-server --port 4000 --host 127.0.0.1

3、在启动服务时自动打开浏览器

由于 JS 是单线程的,在命令行输入命令启动服务的同时不能去做其他的事,此时要靠多进程来帮助我们打开浏览器,在 JS 中开启一个子进程来打开浏览器。

// 文件位置:&#126;static/bin/yourname-http-server.js —— 命令行执行文件
const commander = require("commander");
const Server = require("../index");

// 增加 How to use
commander.on("--help", function () {
    console.log("\r\n  How to use: \r\n")
    console.log("    zf-server --port <val>");
    console.log("    zf-server --host <val>");
    console.log("    zf-server --dir <val>");
});

// 解析 Node 进程执行时的参数
commander
    .version("1.0.0")
    .usage("[options]")
    .option("-p, --port <n>", "server port")
    .option("-o, --host <n>", "server host")
    .option("-d, --dir <n>", "server dir")
    .parse(process.argv);

// 创建 Server 实例传入命令行解析的参数
const server = new Server(commander);

// 启动服务器
server.start();

// ********** 以下为新增代码 **********
let { exec } = require("child_process");

// 判断系统执行不同的命令打开浏览器
let systemOrder = process.platform === "win32" ? "start" : "open";
exec(`${systemOrder} http://${commander.localhost}:${commander.port}`);
// ********** 以上为新增代码 **********

4、发布命令行工具到 npm

在发布我们自己实现的 npm 模块之前需要先做一件事,就是解除当前模块与全局环境的 link,我们可以通过两种方式,第一种方式是直接到系统存储命令文件的文件夹删除模块对应命令的 yourname-http-server.cmd 文件,第二种方式是在模块根目录启动命令行并输入如下命令。

npm unlink

输入下面命令进行登录:

npm login

登录成功后执行下面命令进行发布:

npm publish

发布成功后再次使用自己的模块需要通过 npm 下载并全局安装,命令如下:

npm install yourname-http-server -g

任意文件夹内打开命令行,并执行命令启动服务验证。

在发布模块之前如果使用 nrm 切换过其他的源,必须切换回 npm,再进行登录和发布操作。


总结

其实我们实现的静态服务器核心还在于处理请求和响应的逻辑上,只是不再手动输入 node 命令启动,而是借助一些第三方模块关联到了命令行并通过命令启动,开发其他类型的命令行工具也需要借助这些第三方模块,静态服务器只是其中之一,其实类似这种命令行工具在开发的角度来讲属于 “造轮子” 系列,可以独立开发命令行工具是一个成为前端架构的必备技能,希望通过本篇文章可以了解命令行工具的开发流程,在未来 “造轮子” 的道路上提供帮助。


金蝶云星空-协同开发云-安装开发环境 金蝶协同开发云就是不需要每一个开发都在自己的本机安装开发环境,自己安装可容易出错,包括源代码管理也一并解决了,大大减少了开发人员的工作量和降低了上手的难度。 阅读详情

相关推荐

生存网络与mlr3proba

通过结合生存网络和mlr3proba,可以使用生存网络模型来预测个体在给定时间点发生事件的概率,并使用mlr3proba提供的工具进行模型的训练、评估和选择最佳模型。iii)基本的机器学习(ML)方法,如重新排序和调整。在本文中,我们将只讨论前五种,因为它们在文献中得到了更好的建立,并且它们具有相同的接口,这简化了调优,我们将在下面看到。我们不会为模型指定自定义架构,而是使用默认架构,如果你熟悉PyTorch,那么你可以选择创建自己的架构,如果你愿意的话,通过将其传递给模型中的custom_net参数。

weixin_46587777的博客 1363

前端本地原生开发好用的 http server 服务(npm 包形式、支持热更新、简单好用)

有时候在本地原生网页开发的时候,需要起一个服务能够直接访问当现在正在开发index.html等内容,也就是希望通过npm包在进入项目文件夹后,直接可以起一个可访问的 http 链接,不希望在去安装nginx。serve:为本地静态页面创建一个可访问的服务,不支持热更新:为本地静态页面创建一个可访问的服务,不支持热更新,支持https:为本地静态页面创建一个可访问的服务,支持热更新(运行起来默认支持,可以修改延迟时间),支持https。

卡尔特斯 4950

【翼型】基于Matlab的NACA翼型气动性能仿真与优化【附源码】

本文详细介绍了如何使用Matlab进行NACA翼型的气动性能仿真与优化。内容涵盖从生成NACA翼型坐标、利用面板法快速估算升阻力,到进阶的粘性流模拟与优化设计入门。通过清晰的代码示例和分步指南,帮助读者掌握基于Matlab的翼型分析与优化流程,为飞行器或叶片设计提供实用工具和方法。

weixin_29032489的博客 411

npm包 -- http-Server

http-server 开启本地服务器需求下载使用 需求 在调试html文件时,如果在本地文件夹中直接打开,通常会使用file协议。 当代码中存在http或https的链接时,HTML页面就无法正常打开。 为了解决这种情况,需要在在本地开启一个本地的服务器。 下载 1、 下载node.js 官网地址: https://nodejs.org/ 下载完成后在命令行输入命令$ node -v以及$ np...

weixin_43827079的博客 2317

BEVFusion(mit)最强环境安装,部署复现

BEVFusion作为一项经典的工作,值得深入探索。然而由于配置环境比较麻烦,耽误了一些时间。经过几次成功配置之后,有了一些总结,在这里记录下来,供大家参考避坑,也方便自己下次查阅。

qq_45783225的博客 1万+

AI人工智能全模态大模型架构:推动智能交通发展的架构设计

本文旨在阐述全模态大模型在智能交通领域的架构设计原理和应用方法。我们将探讨如何利用多模态AI技术解决交通流量预测、事故预警、路线优化等关键问题。文章将从基础概念入手,逐步深入技术细节,最后探讨实际应用和未来发展方向。全模态大模型:能够处理和理解多种数据模态(如图像、文本、语音等)的大型AI模型智能交通系统(ITS):利用先进技术优化交通流量和安全性的综合系统多模态融合:将不同数据源的信息整合处理的技术核心概念回顾全模态大模型是能够处理多种数据类型的强大AI系统。

AGI×大数据,开启智能时代的认知跃迁;解码AGI,赋能数据驱动的智能革命。 834

http-server介绍及使用

http-server介绍及使用 首先我们需要了解什么是http-server? http-server是一个超轻量级web服务器 当我们想要在服务器运行一些代码,但是又不会配置服务器的时候,就可以使用http-server就可以搞定了。 http-server可以将任何一个文件夹当作服务器的目录供自己使用 使用方法 1.因为http-server 需要用npm安装所以我们在使用前需要安装node.js 传送门 node.js 2. 打开终端使用npm安装http-server   因为我们想让任意一个

weixin_47284756的博客 3万+

玩转Matlab-Simscape(初级)-01-从一个简单模型开始学习之旅

本文从初学者的角度分享的关于Matlab simscape 的基础内容,简单分享了Simscape搭建模型,创建子系统,修改定义参数等内容,为将来深入学习和使用simscape做铺垫。

万金油笑匠的博客 6520

通过npm安装http-server实现本地服务器

步骤一: 先创建一个文件夹,然后打开cmd命令窗口,用cd命令进入这个文件夹。 步骤二: 用cd命令进入这个文件夹后,然后通过以下命令来使用npm安装http-server: npm install -g http-server 或者: cnpm install -g http-server 步骤三: 安装完成后,cmd输入http-server即可开启本地服务器。 http-server 然后把你的项目文件拉入这个文件夹即可运行。 ...

Y_one的博客 9523

英伟达 DGX Spark & Reachy Mini 入门教程(一)

英伟达在2026年CES上发布多款开放模型,包括Nemotron推理LLM、Isaac GR00T VLA和Cosmos世界模型,助力开发者构建个性化AI代理。文章详细介绍了如何通过DGXSpark和Reachy Mini机器人打造办公助手,涵盖材料准备(推理/视觉模型、文本转语音服务)、智能体构建(NeMo Agent Toolkit框架)、工具调用和路由策略实现。系统支持本地/云端部署,提供语音交互、视觉识别和动作执行功能,并保持完全开源可定制。最后给出运行演示的终端配置步骤和测试用例,鼓励开发者在此基

2506_90492529的博客 2713

强化学习原理及应用作业之动态规划算法【SYSU_2023SpringRL】

本次实践作业将在以下环境进行: 该环境由一个 6×6 网格组成,其中黄色圆圈为智能体出发点,黑色格子为无法通过的墙壁,若智能体向着墙壁方向移动,则会停留在原地,地图边界的移动同理。带有黄色边框的格子为终止状态,即若智能体行动至此状态则整个 episode 结束。红色和绿色表示当前该状态的奖励值,奖励越高则绿色越深,奖励越低则红色越深。状态空间:36(0-35 这 36 个整数,左下角为 0,向右 +1,向上 +6)动作空间:4(0-3 这 4 个整数分别代表左、右、上、下)奖励函数:普通格子 -0.1,到

zik的博客 1398

推荐一款简洁易用的HTTP服务器工具——http-server

如果你在开发过程中需要快速搭建一个本地Web服务器来测试你的网页应用,或者你需要向他人展示你的HTML/CSS/JS作品,那么你可以尝试一下这款名为http-server的Node.js模块。 ## 什么是http-server? http-server是一款基于Node.js的简单易用的命令行HTTP服务器。它支持静态文件服务,并且可以设置路由、跨域等选项。 ## http-server能...

gitblog_00020的博客 2061

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

实现一个静态web服务器、http server

前言 不管是哪一种语言的web框架,其核心都是一致的,那就是以http协议为核心,围绕着http请求和http响应这两方面做文章。至于衍生的数据持久化(cookie session 数据库 等)只是存储手段罢了。 如果要理解好http协议,没有什么比实现一个自定义的http client客户端和http server服务器更好的方法了。 这篇博客博主就打算使用多种语言来做同一件事情:实现一个Htt...

衡与墨的博客 1791

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

http-server 基于nodejs的http本地服务器

http-server 是一个简单的零配置命令行HTTP服务器, 基于 nodeJs. 如果你不想重复的写 nodeJs 的 web-server.js, 则可以使用这个. http-server所用场景: 作为前端的同学来说,想要运行一段代码,但又没有必要使用tomcat或是Apache http server放在服务器上运行,这个时候,一个简单的轻量的http-server就能搞...

muzidigbig的博客 1万+

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

http-server使用,启动本地服务器 & 使用serve包本地启动

node,http-server,serve

weixin_44867717的博客 3万+

零配置静态服务器终极指南:3分钟学会用http-server快速部署网站

你是否曾经为搭建本地开发服务器而烦恼?想要一个简单、快速、零配置的解决方案来预览你的静态网站吗?http-server正是你需要的答案!这个强大的命令行工具让你在3分钟内就能启动一个完整的HTTP服务器,无需任何复杂配置。无论你是前端开发者、设计师还是内容创作者,http-server都能让你的本地开发体验变得无比简单。 ## 🎯 为什么你需要一个零配置静态服务器? 在Web开发中,我们经常

gitblog_00920的博客 1075

http-server:零配置命令行HTTP服务器使用指南

http-server是一个简单、零配置的命令行静态HTTP服务器。它功能强大足以用于生产环境,同时又足够简单和可定制,可用于测试、本地开发和学习。 ## 快速开始 ### 使用npx直接运行 无需安装即可直接运行http-server: ``` npx http-server [path] [options] ``` ### 全局安装使用 通过npm全局安装http-server:

gitblog_00107的博客 1008

快速搭建静态HTTP服务器的终极解决方案:http-server完整指南

在当今快速发展的Web开发领域,静态文件服务是每个开发者都会遇到的基础需求。无论是本地开发调试、项目演示,还是小型项目部署,一个简单易用的HTTP服务器都至关重要。http-server作为一款零配置的命令行静态HTTP服务器,正是为解决这一需求而生。 ## 什么是http-server?🚀 http-server是一个基于Node.js开发的简单、零配置命令行静态HTTP服务器。它既强大到

gitblog_00026的博客 734

终极指南:如何快速搭建本地HTTP服务器 - 零配置命令行工具

你是否曾为搭建本地开发服务器而烦恼?😫 无论是前端开发、API测试还是静态网站预览,一个简单易用的HTTP服务器都是开发者的必备工具。今天我要介绍的**http-server**项目,就是一个功能强大却极其简单的零配置命令行HTTP服务器工具。这个工具特别适合新手和普通用户,无需复杂配置即可快速启动本地服务器。✨ ## 🚀 什么是http-server? **http-server**是一

gitblog_00943的博客 1105

http-server:一个简单的静态 HTTP 服务器

http-server是一个简单的、零配置的命令行静态 HTTP 服务器。它对于生产使用来说足够强大,但它足够简单且易于破解,可以用于测试、本地开发和学习。

枫的博客 1964

如何5分钟搭建本地静态服务器:http-server完整指南

还在为本地测试网页而烦恼吗?想要一个简单快速的静态HTTP服务器来展示你的项目吗?http-server就是你需要的终极解决方案!这款零配置命令行工具让搭建本地服务器变得前所未有的简单。 ## 🚀 为什么选择http-server? http-server是一个功能强大的静态HTTP服务器工具,它完美平衡了简单性与专业性。无论你是前端新手还是资深开发者,都能在几分钟内搭建起完整的本地服务环境

gitblog_00641的博客 985

HTTP服务器 -- http-server 的安装与运行

http-server 是一个简单的零配置命令行HTTP服务器, 基于 nodeJs. HttpServlet容器响应Web客户请求流程如下: 1)Web客户向Servlet容器发出Http请求; 2)Servlet容器解析Web客户的Http请求; 3)Servlet容器创建一个HttpRequest对象,在这个对象中封装Http请求信息; 4)Servlet容器创建一个HttpRe...

青红造了个大白之成长记 1万+

5分钟学会使用http-server搭建本地静态文件服务器教程

想要快速搭建一个本地HTTP服务器来预览网页项目吗?http-server是一个简单易用的零配置命令行静态HTTP服务器,无需复杂的设置就能立即投入使用。这篇教程将带你从零开始,5分钟内掌握这个强大的工具。 ## 立即体验:30秒启动你的第一个服务器 如果你已经安装了Node.js,那么可以直接使用npx命令启动http-server: ``` npx http-server ``` 就是

gitblog_01045的博客 1156

http-server终极指南:3分钟学会零配置静态HTTP服务器部署

http-server是一款简单高效的零配置命令行静态HTTP服务器,让开发者能够快速搭建本地服务器环境,无需复杂配置即可部署静态网站和文件。无论是前端开发测试还是静态内容展示,它都能提供便捷的解决方案。 ## 🚀 为什么选择http-server? 在开发过程中,你是否遇到过需要快速预览静态网页的需求?传统的打开HTML文件方式可能会遇到跨域问题,而配置复杂的服务器又过于繁琐。http-s

gitblog_00876的博客 731

本地开发 npm 好用的http server、好用的web server、静态服务器

有时需要快速启动一个web 服务器(http服务器)来伺服静态网页,安装nginx又太繁琐,那么可以考虑使用npm serve、http-server、webpack-dev-server

wangjun5159的专栏 4760
上一篇: HTTP 缓存的那些事儿
下一篇: AST 抽象语法树
Panda Shen
博客等级 码龄10年 18粉丝 50原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值