上一篇文章我们讲了怎么用Node.js原生API来写一个web服务器,虽然代码比较丑,但是基本功能还是有的。但是一般我们不会直接用原生API来写,而是借助框架来做,比如本文要讲的Express。通过上一篇文章的铺垫,我们可以猜测,Express其实也没有什么黑魔法,也仅仅是原生API的封装,主要是用来提供更好的扩展性,使用起来更方便,代码更优雅。本文照例会从Express的基本使用入手,然后自己手写一个Express来替代他,也就是源码解析。

本文可运行代码已经上传GitHub,拿下来一边玩代码,一边看文章效果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/Express

简单示例

使用Express搭建一个最简单的Hello World也是几行代码就可以搞定,下面这个例子来源官方文档:

const express = require('express');
const app = express();
const port = 3000; app.get('/', (req, res) => {
res.send('Hello World!');
}); app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});

可以看到Express的路由可以直接用app.get这种方法来处理,比我们之前在http.createServer里面写一堆if优雅多了。我们用这种方式来改写下上一篇文章的代码:

const path = require("path");
const express = require("express");
const fs = require("fs");
const url = require("url"); const app = express();
const port = 3000; app.get("/", (req, res) => {
res.end("Hello World");
}); app.get("/api/users", (req, res) => {
const resData = [
{
id: 1,
name: "小明",
age: 18,
},
{
id: 2,
name: "小红",
age: 19,
},
];
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(resData));
}); app.post("/api/users", (req, res) => {
let postData = "";
req.on("data", (chunk) => {
postData = postData + chunk;
}); req.on("end", () => {
// 数据传完后往db.txt插入内容
fs.appendFile(path.join(__dirname, "db.txt"), postData, () => {
res.end(postData); // 数据写完后将数据再次返回
});
});
}); app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}/`);
});

Express还支持中间件,我们写个中间件来打印出每次请求的路径:

app.use((req, res, next) => {
const urlObject = url.parse(req.url);
const { pathname } = urlObject; console.log(`request path: ${pathname}`); next();
});

Express也支持静态资源托管,不过他的API是需要指定一个文件夹来单独存放静态资源的,比如我们新建一个public文件夹来存放静态资源,使用express.static中间件配置一下就行:

app.use(express.static(path.join(__dirname, 'public')));

然后就可以拿到静态资源了:

手写源码

手写源码才是本文的重点,前面的不过是铺垫,本文手写的目标就是自己写一个express来替换前面用到的express api,其实就是源码解析。在开始之前,我们先来看看用到了哪些API

  1. express(),第一个肯定是express函数,这个运行后会返回一个app的实例,后面用的很多方法都是这个app上的。
  2. app.listen,这个方法类似于原生的server.listen,用来启动服务器。
  3. app.get,这是处理路由的API,类似的还有app.post等。
  4. app.use,这是中间件的调用入口,所有中间件都要通过这个方法来调用。
  5. express.static,这个中间件帮助我们做静态资源托管,其实是另外一个库了,叫serve-static,因为跟Express架构关系不大,本文就先不讲他的源码了。

本文所有手写代码全部参照官方源码写成,方法名和变量名尽量与官方保持一致,大家可以对照着看,写到具体的方法时我也会贴出官方源码的地址。

express()

首先需要写的肯定是express(),这个方法是一切的开始,他会创建并返回一个app,这个app就是我们的web服务器

// express.js
var mixin = require('merge-descriptors');
var proto = require('./application'); // 创建web服务器的方法
function createApplication() {
// 这个app方法其实就是传给http.createServer的回调函数
var app = function (req, res) { }; mixin(app, proto, false); return app;
} exports = module.exports = createApplication;

上述代码就是我们在运行express()的时候执行的代码,其实就是个空壳,返回的app暂时是个空函数,真正的app并没在这里,而是在proto上,从上述代码可以看出proto其实就是application.js,然后通过下面这行代码将proto上的东西都赋值给了app

mixin(app, proto, false);

这行代码用到了一个第三方库merge-descriptors,这个库总共没有几行代码,做的事情也很简单,就是将proto上面的属性挨个赋值给app,对merge-descriptors源码感兴趣的可以看这里:https://github.com/component/merge-descriptors/blob/master/index.js

Express这里之所以使用mixin,而不是普通的面向对象来继承,是因为它除了要mixin proto外,还需要mixin其他库,也就是需要多继承,我这里省略了,但是官方源码是有的。

express.js对应的源码看这里:https://github.com/expressjs/express/blob/master/lib/express.js

app.listen

上面说了,express.js只是一个空壳,真正的appapplication.js里面,所以app.listen也是在这里。

// application.js

var app = exports = module.exports = {};

app.listen = function listen() {
var server = http.createServer(this);
return server.listen.apply(server, arguments);
};

上面代码就是调用原生http模块创建了一个服务器,但是传的参数是this,这里的this是什么呢?回想一下我们使用express的时候是这样用的:

const app = express();

app.listen(3000);

所以listen方法的实际调用者是express()的返回值,也就是上面express.js里面createApplication的返回值,也就是这个函数:

var app = function (req, res) {
};

所以这里的this也是这个函数,所以我在express.js里面就加了注释,这个函数是http.createServer的回调函数。现在这个函数是空的,实际上他应该是整个web服务器的处理入口,所以我们给他加上处理的逻辑,在里面再加一行代码:

var app = function(req, res) {
app.handle(req, res); // 这是真正的服务器处理入口
};

app.handle

app.handle也是挂载在app下面的,所以他实际也在application.js这个文件里面,下面我们来看看他干了什么:

app.handle = function handle(req, res) {
var router = this._router; // 最终的处理方法
var done = finalhandler(req, res); // 如果没有定义router
// 直接结束返回
if (!router) {
done();
return;
} // 有router,就用router来处理
router.handle(req, res, done);
}

上面代码可以看出,实际处理路由的是router,这是Router的一个实例,并且挂载在this上的,我们这里还没有给他赋值,如果没有赋值的话,会直接运行finalhandler并且结束处理。finalhandler也是一个第三方库,GitHub链接在这里:https://github.com/pillarjs/finalhandler。这个库的功能也不复杂,就是帮你处理一些收尾的工作,比如所有路由都没匹配上,你可能需要返回404并记录下error log,这个库就可以帮你做。

app.get

上面说了,在具体处理网络请求时,实际上是用app._router来处理的,那么app._router是在哪里赋值的呢?事实上app._router的赋值有多个地方,一个地方就是HTTP动词处理方法上,比如我们用到的app.get或者app.post。无论是app.get还是app.post都是调用的router方法来处理,所以可以统一用一个循环来写这一类的方法。

// HTTP动词的方法
var methods = ['get', 'post'];
methods.forEach(function (method) {
app[method] = function (path) {
this.lazyrouter(); var route = this._router.route(path);
route[method].apply(route, Array.prototype.slice.call(arguments, 1));
return this;
}
});

上面代码HTTP动词都放到了一个数组里面,官方源码中这个数组也是一个第三方库维护的,名字就叫methods,GitHub地址在这里:https://github.com/jshttp/methods。我这个例子因为只需要两个动词,就简化了,直接用数组了。这段代码其实给app创建了跟每个动词同名的函数,所有动词的处理函数都是一样的,都是去调router里面的对应方法来处理。这种将不同部分抽取出来,从而复用共同部分的代码,有点像我之前另一篇文章写过的设计模式----享元模式

我们注意到上面代码除了调用router来处理路由外,还有一行代码:

this.lazyrouter();

lazyrouter方法其实就是我们给this._router赋值的地方,代码也比较简单,就是检测下有没有_router,如果没有就给他赋个值,赋的值就是Router的一个实例:

app.lazyrouter = function lazyrouter() {
if (!this._router) {
this._router = new Router();
}
}

app.listenapp.handlemethods处理方法都在application.js里面,application.js源码在这里:https://github.com/expressjs/express/blob/master/lib/application.js

Router

写到这里我们发现我们已经使用了Router的多个API,比如:

  1. router.handle
  2. router.route
  3. route[method]

所以我们来看下Router这个类,下面的代码是从源码中简化出来的:

// router/index.js
var setPrototypeOf = require('setprototypeof'); var proto = module.exports = function () {
function router(req, res, next) {
router.handle(req, res, next);
} setPrototypeOf(router, proto); return router;
}

这段代码对我来说是比较奇怪的,我们在执行new Router()的时候其实执行的是new proto()new proto()并不是我奇怪的地方,奇怪的是他设置原型的方式。我之前在讲JS的面向对象的文章提到过如果你要给一个类加上类方法可以这样写:

function Class() {}

Class.prototype.method1 = function() {}

var instance = new Class();

这样instance.__proto__就会指向Class.prototype,你就可使用instance.method1了。

Express.js的上述代码其实也是实现了类似的效果,setprototypeof又是一个第三方库,作用类似Object.setPrototypeOf(obj, prototype),就是给一个对象设置原型,setprototypeof存在的意义就是兼容老标准的JS,也就是加了一些polyfill他的代码在这里。所以:

setPrototypeOf(router, proto);

这行代码的意思就是让router.__proto__指向protorouter是你在new proto()时的返回对象,执行了上面这行代码,这个router就可以拿到proto上的全部方法了。像router.handle这种方法就可以挂载到proto上了,成为proto.handle

绕了一大圈,其实就是JS面向对象的使用,给router添加类方法,但是为什么使用这么绕的方式,而不是像我上面那个Class那样用呢?这我就不是很清楚了,可能有什么历史原因吧。

路由架构

Router的基本结构知道了,要理解Router的具体代码,我们还需要对Express的路由架构有一个整体的认识。就以我们这两个示例API来说:

get /api/users

post /api/users

我们发现他们的path是一样的,都是/api/users,但是他们的请求方法,也就是method不一样。Express里面将path这一层提取出来作为了一个类,叫做Layer。但是对于一个Layer,我们只知道他的path,不知道method的话,是不能确定一个路由的,所以Layer上还添加了一个属性route,这个route上也存了一个数组,数组的每个项存了对应的method和回调函数handle。整个结构你可以理解成这个样子:

const router = {
stack: [
// 里面很多layer
{
path: '/api/users'
route: {
stack: [
// 里面存了多个method和回调函数
{
method: 'get',
handle: function1
},
{
method: 'post',
handle: function2
}
]
}
}
]
}

知道了这个结构我们可以猜到,整个流程可以分成两部分:注册路由匹配路由。当我们写app.getapp.post这些方法时,其实就是在router上添加layerroute。当一个网络请求过来时,其实就是遍历layerroute,找到对应的handle拿出来执行。

注意route数组里面的结构,每个项按理来说应该使用一种新的数据结构来存储,比如routeItem之类的。但是Express并没有这样做,而是将它和layer合在一起了,给layer添加了methodhandle属性。这在初次看源码的时候可能造成困惑,因为layer同时存在于routerstack上和routestack上,肩负了两种职责。

router.route

这个方法是我们前面注册路由的时候调用的一个方法,回顾下前面的注册路由的方法,比如app.get

app.get = function (path) {
this.lazyrouter(); var route = this._router.route(path);
route.get.apply(route, Array.prototype.slice.call(arguments, 1));
return this;
}

结合上面讲的路由架构,我们在注册路由的时候,应该给router添加对应的layerrouterouter.route的代码就不难写出了:

proto.route = function route(path) {
var route = new Route();
var layer = new Layer(path, route.dispatch.bind(route)); // 参数是path和回调函数 layer.route = route; this.stack.push(layer); return route;
}

Layer和Route构造函数

上面代码新建了RouteLayer实例,这两个类的构造函数其实也挺简单的。只是参数的申明和初始化:

// layer.js
module.exports = Layer; function Layer(path, fn) {
this.path = path; this.handle = fn;
this.method = '';
}
// route.js
module.exports = Route; function Route() {
this.stack = [];
this.methods = {}; // 一个加快查找的hash表
}

route.get

前面我们看到了app.get其实通过下面这行代码,最终调用的是route.get

route.get.apply(route, Array.prototype.slice.call(arguments, 1));

也知道了route.get这种动词处理函数,其实就是往route.stack上添加layer,那我们的route.get也可以写出来了:

var methods = ["get", "post"];
methods.forEach(function (method) {
Route.prototype[method] = function () {
// 支持传入多个回调函数
var handles = flatten(slice.call(arguments)); // 为每个回调新建一个layer,并加到stack上
for (var i = 0; i < handles.length; i++) {
var handle = handles[i]; // 每个handle都应该是个函数
if (typeof handle !== "function") {
var type = toString.call(handle);
var msg =
"Route." +
method +
"() requires a callback function but got a " +
type;
throw new Error(msg);
} // 注意这里的层级是layer.route.layer
// 前面第一个layer已经做个path的比较了,所以这里是第二个layer,path可以直接设置为/
var layer = new Layer("/", handle);
layer.method = method;
this.methods[method] = true; // 将methods对应的method设置为true,用于后面的快速查找
this.stack.push(layer);
}
};
});

这样,其实整个router的结构就构建出来了,后面就看看怎么用这个结构来处理请求了,也就是router.handle方法。

router.handle

前面说了app.handle实际上是调用的router.handle,也知道了router的结构是在stack上添加了layerrouter,所以router.handle需要做的就是从router.stack上找出对应的layerrouter并执行回调函数:

// 真正处理路由的函数
proto.handle = function handle(req, res, done) {
var self = this;
var idx = 0;
var stack = self.stack; // next方法来查找对应的layer和回调函数
next();
function next() {
// 使用第三方库parseUrl获取path,如果没有path,直接返回
var path = parseUrl(req).pathname;
if (path == null) {
return done();
} var layer;
var match;
var route; while (match !== true && idx < stack.length) {
layer = stack[idx++]; // 注意这里先执行 layer = stack[idx]; 再执行idx++;
match = layer.match(path); // 调用layer.match来检测当前路径是否匹配
route = layer.route; // 没匹配上,跳出当次循环
if (match !== true) {
continue;
} // layer匹配上了,但是没有route,也跳出当次循环
if (!route) {
continue;
} // 匹配上了,看看route上有没有对应的method
var method = req.method;
var has_method = route._handles_method(method);
// 如果没有对应的method,其实也是没匹配上,跳出当次循环
if (!has_method) {
match = false;
continue;
}
} // 循环完了还没有匹配的,就done了,其实就是404
if (match !== true) {
return done();
} // 如果匹配上了,就执行对应的回调函数
return layer.handle_request(req, res, next);
}
};

上面代码还用到了几个LayerRoute的实例方法:

layer.match(path): 检测当前layerpath是否匹配。

route._handles_method(method):检测当前routemethod是否匹配。

layer.handle_request(req, res, next):使用layer的回调函数来处理请求。

这几个方法看起来并不复杂,我们后面一个一个来实现。

到这里其实还有个疑问。从他整个的匹配流程来看,他寻找的其实是router.stack.layer这一层,但是最终应该执行的回调却是在router.stack.layer.route.stack.layer.handle。这是怎么通过router.stack.layer找到最终的router.stack.layer.route.stack.layer.handle来执行的呢?

这要回到我们前面的router.route方法:

proto.route = function route(path) {
var route = new Route();
var layer = new Layer(path, route.dispatch.bind(route)); layer.route = route; this.stack.push(layer); return route;
}

这里我们new Layer的时候给的回调其实是route.dispatch.bind(route),这个方法会再去route.stack上找到正确的layer来执行。所以router.handle真正的流程其实是:

  1. 找到path匹配的layer
  2. 拿出layer上的route,看看有没有匹配的method
  3. layermethod都有匹配的,再调用route.dispatch去找出真正的回调函数来执行。

所以又多了一个需要实现的函数,route.dispatch

layer.match

layer.match是用来检测当前path是否匹配的函数,用到了一个第三方库path-to-regexp,这个库可以将path转为正则表达式,方便后面的匹配,这个库在之前写过的react-router源码中也出现过。

var pathRegexp = require("path-to-regexp");

module.exports = Layer;

function Layer(path, fn) {
this.path = path; this.handle = fn;
this.method = ""; // 添加一个匹配正则
this.regexp = pathRegexp(path);
// 快速匹配/
this.regexp.fast_slash = path === "/";
}

然后就可以添加match实例方法了:

Layer.prototype.match = function match(path) {
var match; if (path != null) {
if (this.regexp.fast_slash) {
return true;
} match = this.regexp.exec(path);
} // 没匹配上,返回false
if (!match) {
return false;
} // 不然返回true
return true;
};

layer.handle_request

layer.handle_request是用来调用具体的回调函数的方法,其实就是拿出layer.handle来执行:

Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle; fn(req, res, next);
};

route._handles_method

route._handles_method就是检测当前route是否包含需要的method,因为之前添加了一个methods对象,可以用它来进行快速查找:

Route.prototype._handles_method = function _handles_method(method) {
var name = method.toLowerCase(); return Boolean(this.methods[name]);
};

route.dispatch

route.dispatch其实是router.stack.layer的回调函数,作用是找到对应的router.stack.layer.route.stack.layer.handle并执行。

Route.prototype.dispatch = function dispatch(req, res, done) {
var idx = 0;
var stack = this.stack; // 注意这个stack是route.stack // 如果stack为空,直接done
// 这里的done其实是router.stack.layer的next
// 也就是执行下一个router.stack.layer
if (stack.length === 0) {
return done();
} var method = req.method.toLowerCase(); // 这个next方法其实是在router.stack.layer.route.stack上寻找method匹配的layer
// 找到了就执行layer的回调函数
next();
function next() {
var layer = stack[idx++];
if (!layer) {
return done();
} if (layer.method && layer.method !== method) {
return next();
} layer.handle_request(req, res, next);
}
};

到这里其实Express整体的路由结构,注册和执行流程都完成了,贴下对应的官方源码:

Router类https://github.com/expressjs/express/blob/master/lib/router/index.js

Layer类https://github.com/expressjs/express/blob/master/lib/router/layer.js

Route类https://github.com/expressjs/express/blob/master/lib/router/route.js

中间件

其实我们前面已经隐含了中间件,从前面的结构可以看出,一个网络请求过来,会到router的第一个layer,然后调用next到到第二个layer,匹配上layerpath就执行回调,然后一直这样把所有的layer都走完。所以中间件是啥?中间件就是一个layer,他的path默认是/,也就是对所有请求都生效。按照这个思路,代码就简单了:

// application.js

// app.use就是调用router.use
app.use = function use(fn) {
var path = "/"; this.lazyrouter();
var router = this._router;
router.use(path, fn);
};

然后在router.use里面再加一层layer就行了:

proto.use = function use(path, fn) {
var layer = new Layer(path, fn); this.stack.push(layer);
};

总结

  1. Express也是用原生APIhttp.createServer来实现的。
  2. Express的主要工作是将http.createServer的回调函数拆出来了,构建了一个路由结构Router
  3. 这个路由结构由很多层layer组成。
  4. 一个中间件就是一个layer
  5. 路由也是一个layerlayer上有一个path属性来表示他可以处理的API路径。
  6. path可能有不同的method,每个method对应layer.route上的一个layer
  7. layer.route上的layer虽然名字和router上的layer一样,但是功能侧重点并不一样,这也是源码中让人困惑的一个点。
  8. layer.route上的layer的主要参数是methodhandle,如果method匹配了,就执行对应的handle
  9. 整个路由匹配过程其实就是遍历router.layer的一个过程。
  10. 每个请求来了都会遍历一遍所有的layer,匹配上就执行回调,一个请求可能会匹配上多个layer
  11. 总体来看,Express代码给人的感觉并不是很完美,特别是Layer类肩负两种职责,跟软件工程强调的单一职责原则不符,这也导致RouterLayerRoute三个类的调用关系有点混乱。而且对于继承和原型的使用都是很老的方式。可能也是这种不完美催生了Koa的诞生,下一篇文章我们就来看看Koa的源码吧。
  12. Express其实还对原生的reqres进行了扩展,让他们变得更好用,但是这个其实只相当于一个语法糖,对整体架构没有太大影响,所以本文就没涉及了。

本文可运行代码已经上传GitHub,拿下来一边玩代码,一边看文章效果更佳:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/Express

参考资料

Express官方文档:http://expressjs.com/

Express官方源码:https://github.com/expressjs/express/tree/master/lib

文章的最后,感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是作者持续创作的动力。

作者博文GitHub项目地址: https://github.com/dennis-jiang/Front-End-Knowledges

作者掘金文章汇总:https://juejin.im/post/5e3ffc85518825494e2772fd

我也搞了个公众号[进击的大前端],不打广告,不写水文,只发高质量原创,欢迎关注~

手写Express.js源码的更多相关文章

  1. 手写Koa.js源码

    用Node.js写一个web服务器,我前面已经写过两篇文章了: 第一篇是不使用任何框架也能搭建一个web服务器,主要是熟悉Node.js原生API的使用:使用Node.js原生API写一个web服务器 ...

  2. 手写@koa/router源码

    上一篇文章我们讲了Koa的基本架构,可以看到Koa的基本架构只有中间件内核,并没有其他功能,路由功能也没有.要实现路由功能我们必须引入第三方中间件,本文要讲的路由中间件是@koa/router,这个中 ...

  3. Underscore.js 源码学习笔记(上)

    版本 Underscore.js 1.9.1 一共 1693 行.注释我就删了,太长了… 整体是一个 (function() {...}());  这样的东西,我们应该知道这是一个 IIFE(立即执行 ...

  4. MVVM大比拼之avalon.js源码精析

    简介 avalon是国内 司徒正美 写的MVVM框架,相比同类框架它的特点是: 使用 observe 模式,性能高. 将原始对象用object.defineProperty重写,不需要用户像用knoc ...

  5. MVVM大比拼之knockout.js源码精析

    简介 本文主要对源码和内部机制做较深如的分析,基础部分请参阅官网文档. knockout.js (以下简称 ko )是最早将 MVVM 引入到前端的重要功臣之一.目前版本已更新到 3 .相比同类主要有 ...

  6. 深入理解unslider.js源码

    最近用到了一个挺好用的幻灯片插件,叫做unslider.js,就想看看怎么实现幻灯片功能,就看看源码,顺便自己也学习学习.看完之后收获很多,这里和大家分享一下. unslider.js 源码和使用教程 ...

  7. basket.js 源码分析

    basket.js 源码分析 一.前言 basket.js 可以用来加载js脚本并且保存到 LocalStorage 上,使我们可以更加精准地控制缓存,即使是在 http 缓存过期之后也可以使用.因此 ...

  8. underscore.js 源码

    underscore.js 源码 underscore]JavaScript 中如何判断两个元素是否 "相同" Why underscore 最近开始看 underscore.js ...

  9. 国籍控件(js源码)

    国籍控件(js源码) 一直苦于没有好的国籍控件可以用,于是抽空写了一个国籍控件,现分享给大家. 主要功能和界面介绍 国籍控件主要支持中文.英文过滤以及键盘上下事件. 源码介绍 国籍控件核心是两个文件, ...

随机推荐

  1. Kubernetes Pod水平自动伸缩(HPA)

    HPA简介 HAP,全称 Horizontal Pod Autoscaler, 可以基于 CPU 利用率自动扩缩 ReplicationController.Deployment 和 ReplicaS ...

  2. Centos-检查文件系统并尝试修复-fsck

    fsck 检查文件系统并尝试修改错误,修复对象为设备,本质上是调用 /sbin/fsck.filesystemName 命令, filesystemName是指定设备的文件系统类型,如图分区中有文件丢 ...

  3. Python练习题 015:一颗自由落地的球

    [Python练习题 015] 一球从100米高度自由落下,每次落地后反跳回原高度的一半,再落下.求它在第10次落地时,共经过多少米?第10次反弹多高? ----------------------- ...

  4. sqlserver和oracle修改表结构

    sqlserver和oracle修改表结构常用SQL Server:1.增加列  ALTER TABLE users ADD address varchar(30);2.删除列  ALTER TABL ...

  5. 092 01 Android 零基础入门 02 Java面向对象 02 Java封装 01 封装的实现 03 # 088 01 Android 零基础入门 02 Java面向对象 02 Java封装 02 static关键字 02 static关键字(中)

    092 01 Android 零基础入门 02 Java面向对象 02 Java封装 01 封装的实现 03 # 088 01 Android 零基础入门 02 Java面向对象 02 Java封装 ...

  6. matlab receive License Manager Error -103?

    参考:https://www.mathworks.com/matlabcentral/answers/91874-why-do-i-receive-license-manager-error-103 ...

  7. c++ 十进制、十六进制和BCD的相互转换,与打印printf,与函数调用

    转载: https://blog.csdn.net/sjhuangx/article/details/49947179   c++ 十进制.十六进制和BCD的相互转换 https://blog.csd ...

  8. vue使用vueCropper裁剪功能,代码复制直接使用

    //先安装包 npm install vue-cropper --save-dev <template> <div id="merchantInformation" ...

  9. Scala小记(一)

    Scala小记----初识Scala 一,什么是Scale? Scala是一门面向对象的,使用JVM运行的函数式编程语言,(函数式编程语言:指的就是那些将方法或者说是函数来作为参数 进行传递的编程语言 ...

  10. Dockerfile常用指令及使用

    Dockerfile常用指令及使用 1. dockerfile介绍 2. Dockerfile常用指令 指令 描述 FROM 构建新镜像是基于哪个镜像 MAINTAINER 进行维护者姓名或邮箱地址 ...