前言
学习nestjs之前,最重要的是学习它的依赖注入原理以及express框架源码大致原理(默认nestjs在express基础上添加功能)。
很多同学如果没有这些基础去学nestjs API的话,基本上就是死记硬背了,加上平时工作不会用到,很可能学了今天的,明天马上就忘了,所以学习了基础原理,不仅对于你面试有帮助,也对你日后造一些轮子,用一些轮子更加得心应手。
所以本篇就围绕这两个点写一个简易版的实现,帮助有兴趣的同学理解一下。
依赖注入能不能简单说明是啥意思,为啥要用这货?
我们从一个简单例子说起:
假设有一个类叫老王,专指那些住在美女隔壁的靓仔们。他们喜欢助人为乐,送大帽子。能力上擅长逃跑。
class Hobby {
constructor(gender){
return [gender, '助人为乐']
}
}
class Skill {
constructor(){
return ['送帽子', '跑路']
}
}
class Person {
hobby: Hobby
skill: Skill
constructor(){
this.hobby = new Hobby('女');
this.skill = new Skill();
}
}
console.log(new Person()); // { hobby: ['女', '助人为乐'], skill: ['送帽子', '跑路'] }
好了,这个Person类,我们看看有啥缺点:
- 每次创建Person类的实例,都要传入Hobby类和Skill类,也就是对这两个类都产生了依赖,假如有一天我们想创建不同的老王,比如有的老王喜欢小鲜肉,有的老王喜欢老腊肉,这个Person类写死了,没法定制
有同学马上就会说,这个简单啊,Hobby和Skill当做参数传入不就行了,是的,这个方法确实能解决问题,如下:
class Hobby {
constructor(gender){
return [gender, '助人为乐']
}
}
class Skill {
constructor(){
return ['送帽子', '跑路']
}
}
class Person {
hobby: Hobby
skill: Skill
constructor(hobby, skill){
this.hobby = hobby;
this.skill = skill;
}
}
// { hobby: [’男', '助人为乐'], skill: ['送帽子', '跑路'] }
console.log(new Person(new Hobby('男'), new Skill());
- 但有没有更好的办法,也就是这个类我们不用自己去new,像下面这样,系统帮我们自动导入呢?
class Person {
constructor(hobby: Hobby, skill: Skill){
}
hello(){
这里直接就可以用this.hobby了
}
}
也就是说,在你new Person的时候,hobby和skill参数,自动帮你实例化导入,而且你还需要new Person,能不能不new Person 都是自动调用并把参数导入?
-
这就引申出第一个概念就控制反转,以前我们都要自己主动去new,从而创建实例,现在是把创建实例的任务交给了一个容器(后面会实现,你就明白了,相当于一个掌控者,或者说造物主,专门来创建实例的,并管理依赖关系),所以控制权是不是就反转了,你主动的创建实例和控制依赖,反转为容器创建实例和控制依赖。
-
对于控制反转,最常用件的方式叫依赖注入,意思是容器动态的将依赖关系注入到组件中。
-
控制反转可以通过依赖注入实现,所以本质上是一回事。
到这里,其实大家也没觉得依赖注入有啥毛用吧!下面的讲解一言以蔽之就是,虽然我们js可以把函数或者类当参数传入另一个类里,这确实解决了之前讲的写死代码的问题,也就是说我们不用依赖注入也能解决代码耦合的问题,所以看起来我们并不是那么需要依赖注入。
但是nestjs的依赖注入不仅仅解决了类之间耦合的问题,还解决了创建类又要重新new 一下的问题,这样代码维护性就更友好了。为啥呢,假如A类依赖B类,用的时候是不是要new A(new B()),但是如果有一天B类要替换成C类,是不是全局都要找new A(new B())的代码,然后换成new A(new C()),好了,如果是nestjs的话,根本不用管谁new 的问题,在依赖关系的申请表上把B改成C就完事。
Reflect和Reflect Metadata
我们要依赖Reflect Metadata来实现依赖注入,所以要提这个API。
- Reflect是ES6新的API
- Reflect Metadata可以给装饰器添加自定义的一些信息,我们可以称之为元数据。
我们再来回顾一下那个喜欢喜欢助人为乐,送大帽子的老王兄弟。
import 'reflect-metadata';
const person = {};
Reflect.defineMetadata('name', 'laowang', person);
Reflect.defineMetadata('name', 'run so fast', person, 'skill');
console.log(Reflect.getOwnMetadata('name',target)); // laowang
console.log(Reflect.getOwnMetadata('name',target, 'skill')) // run so fast
啥意思呢,我们使用defineMetadata在person这个对象上添加了了一个元数据,name属性,值为老王
然后我们在person对象上又添加了一了skill属性,这个属性的name属性,值为run so fast
这个元数据实际上我们必须要用getOwnMetadata这个API获取,其实内部就是一个哈希表,它是类似这样的定义
person.name = 'laowang';
person.skill.name = 'run so fast';
这下理解了吧,我们看看如何用装饰器去实现。
import 'reflect-metadata';
function classMetadata(key, value) {
return function(target){
Reflect.defineMetadata(key, value, target)
}
}
function methodMetadata(key, value) {
return function(target, propertyName){
Reflect.defineMetadata(key, value, target, propertyName)
}
}
@classMetadata('name', 'laowang')
class Person {
@methodMetadata('name', 'run so fast')
skill():string {
return '来人就跑!来抓我啊!'
}
}
console.log(Reflect.getMetadata('name', Person)) // laowang
console.log(Reflect.getMetadata('name', Person.prototype, 'skill')) // run so fast
结合nestjs,Reflect Metadata练下手
import 'Reflect Metadata';
class A {
skill() {
console.log('助人为乐最快乐');
}
}
class B {
skill() {
console.log('逃跑翻墙真的爽');
}
}
function Module(metadata) {
const propsKeys = Object.keys(metadata);
return (target) => {
for (const property in metadata) {
if (metadata.hasOwnProperty(property)) {
Reflect.defineMetadata(property, metadata[property], target);
}
}
};
}
@Module({
controllers: [B],
providers: [A],
})
class C {}
const providers = Reflect.getMetadata('providers', C);
const controllers = Reflect.getMetadata('controllers', C);
console.log(providers, controllers); // [ [class A] ] [ [class B] ]
正式开始,实现依赖注入
我们刚才说一个容器负责创建实例和管理依赖,所以肯定是数据结构里有这些类的,要不你咋管理,所以就会有一个添加依赖的仓库。
实现服务注册
import 'reflect-metadata';
interface Type<T> {
new (...args: any[]): T;
}
interface ClassProvider<T> {
provide: Type<T>;
useClass: Type<T>;
}
class Container {
providers = new Map<Type<any>, ClassProvider<any>>();
/**
* 注册
*/
addProvider<T>(provider: ClassProvider<T>) {
this.providers.set(provider.provide, provider);
}
}
class Laowang {
age: 93;
}
const continer = new Container();
continer.addProvider<Laowang>({ provide: Laowang, useClass: Laowang });
console.log(continer.providers)
返回如下:
所以Continer就干一件事,注册依赖。
获取实例
刚才我们可以注册依赖类了,但是这些类如何获取呢?(你可能想,这获取了有咋地,跟依赖注入毛关系啊,别着急,后面会把这些知识点串在一起)
我们加入一个inject方法来获取。
class Container {
providers = new Map<Type<any>, ClassProvider<any>>();
/**
* 注册
*/
addProvider<T>(provider: ClassProvider<T>) {
this.providers.set(provider.provide, provider);
}
/**
* 获取
*/
inject(token: Type<any>){
return this.providers.get(token)?.useClass;
}
}
class Laowang {
age: 93;
}
const continer = new Container();
continer.addProvider<Laowang>({ provide: Laowang, useClass: Laowang });
console.log(continer.inject(Laowang)); // class Laowang {}
在讲下面的东西之前,我们需要了解下装饰器的语法:
/**
* 类装饰器
* @param constructor 类的构造函数
*/
function classDecorator(constructor: Function){}
/**
* 属性装饰器
* @param target 静态属性是类的构造函数,实例的属性是类的原型对象
* @param property 属性名称
*/
function propertyDecorator(target:any, property: string) {}
/**
* 方法装饰器
* @param target 静态方法是类的构造函数,实例方法是类的原型对象
* @param propery 方法名称
* @param descriptor 方法描述符
*/
function methodDecorator(target: any, propery: string, descriptor: PropertyDescriptor){}
/**
* 参数装饰器
* @param target 静态方法是类的构造函数,实例方法是类的原型对象
* @param methodName 方法名
* @param paramIndex 参数索引
*/
function paramDecorator(target: any, methodName: string, paramIndex){}
实现_decorate
接下来就是使用Reflect metadta了,我们先实现几个函数,先理解这些函数有啥用
class Hobby {}
const Test = (): ClassDecorator => (target) => {};
@Test()
class Person {
constructor(hobby: Hobby) {}
}
上面的代码会转化为下面,你就会理解为啥Person构造函数上有一个参数,new的时候不用传就有了。实际上,这些参数会通过'design: paramtypes'的key来获取到,如下:
// 注意下面的Hobby是一个类型,不是实例
const Person = _decorate([_metadata('design: paramtypes', [Hobby])], Person);
const _metadata = function (key, value) {
return function (target) {
Reflect.defineMetadata(key, value, target);
return target;
};
};
const _decorate = function (decorator, target, key, desc) {
const argsLength = arguments.length;
/**
* 如果参数小于3,说明是装饰的是类装饰器,所以装饰target
* 如果大于3,说明可能是是装饰方法的,所以返回desc
*/
let decoratorTarget =
argsLength < 3
? target
: desc === null
? (desc = Object.getOwnPropertyDescriptor(target, key))
: desc;
decoratorTarget =
argsLength < 3
? decorator(decoratorTarget)
: argsLength > 3
? decorator(target, key, decoratorTarget)
: decorator(target, key) || decoratorTarget;
};
好了,这里我们明白了我们只要把provider实现注册到continer上,那么根据我们上面的_decorate,就可以通过类型把provider上注册的实例拿出来给Person,所以在new的时候直接就能拿到,这里最关键的要记住,构造函数参数被放到了'design: paramtypes'标识符上。
我们举个例子这个'design: paramtypes'标识符有啥用。
import 'reflect-metadata';
type Constructor<T = any> = new (...args: any[]) => T;
const Test = (): ClassDecorator => (target) => {};
class OtherService {
a = 1;
}
@Test()
class TestService {
constructor(public readonly otherService: OtherService) {}
testMethod() {
console.log(this.otherService.a);
}
}
const Factory = <T>(target: Constructor<T>): T => {
// 获取所有注入的服务
const providers = Reflect.getMetadata('design:paramtypes', target); // [OtherService]
const args = providers.map((provider: Constructor) => new provider());
return new target(...args);
};
Factory(TestService).testMethod(); // 1
好了,接着我们再实现一下,类的方法,Reflect metaDta如何处理。
class C {
@log
foo(n: number) {
return n * 2;
}
}
在使用 @log 之前,需要提前在应用的某个地方定义这个方法。因此先来看看log 方法 Decorator 的实现。
function log(target: Function, key: string, value: any) {
return {
value: function (...args: any[]) {
// 做一些事情
// 你要明白target,key,value表示啥意思
}
};
}
我们看被转译后的代码
var C = (function () {
function C() {
}
C.prototype.foo = function (n) {
return n * 2;
};
Object.defineProperty(C.prototype, "foo",
__decorate([
log
], C.prototype, "foo", Object.getOwnPropertyDescriptor(C.prototype, "foo")));
return C;
})();
最关键的是这一部分
Object.defineProperty(
__decorate(
[log], // decorators
C.prototype, // target
"foo", // key
Object.getOwnPropertyDescriptor(C.prototype, "foo") // desc
);
);
好了,结合之前的__decorate代码,我们可以明白,
function log(target: Function, key: string, value: any)
- target === C.prototype
- key === "foo"
- value === Object.getOwnPropertyDescriptor(C.prototype, "foo")
所以就可以添加方法装饰器去操作了。
好了,我们把上面讲的这些穿起来说下面这个例子
import 'reflect-metadata';
class A {
skill() {
console.log('助人为乐最快乐');
}
}
class Hobby {}
const Test = (): ClassDecorator => (target) => {};
@Test()
class Person {
constructor(hobby: Hobby) {}
}
function Module(metadata) {
const propsKeys = Object.keys(metadata);
return (target) => {
for (const property in metadata) {
if (metadata.hasOwnProperty(property)) {
Reflect.defineMetadata(property, metadata[property], target);
}
}
};
}
@Module({
controllers: [Person],
providers: [Hobby],
})
class C {}
到这里为止,我们就有了一个Module,这个module的元数据里有controllers集合,也有providers集合。对吧。之前也讲到了,通过下面的API就可以获取:
const providers = Reflect.getMetadata('providers', C);
const controllers = Reflect.getMetadata('controllers', C);
然后我们看controllers中其中一个controller:Person,我们上面也讲了,它通过reflect metadata的处理,变成了这样
const Person = _decorate([_metadata('design: paramtypes', [Hobby])], Person);
好了,就是说Person这个controller天然就把需要的依赖让入到了'design: paramtypes'属性中,通过
Reflect.getMetadata('design:paramtypes', Person);
就可以获取到依赖是[Hobby],好,我们接着来看下面,这个是实例化controller的代码
const Factory = <T>(target: Constructor<T>): T => {
// 获取所有注入的服务
const providers = Reflect.getMetadata('providers', target);
// 下面的Continer就是我们之前建立的数据仓库,我们把依赖放进去
const continer = new Continer();
for(let i = 0; i < providers.length; i++){
continer.addProvider({ provide:providers[i], useClass: providers[i] })
}
const controllers = Reflect.getMetadata('controllers', target);
for(let i = 0; i < controllers.length; i++){
const currNeedProviders = Reflect.getMetadata('design:paramtypes', controllers[i]);
const args = currNeedProviders.map((provider) => continer.inject(provider));
new currNeedProv(...args)
}
};
Factory(C) // 这样就把所有controller都自动实例化了
以上就是简易实现nestjs依赖注入的思路了。接下来我们看看express的简易实现。
express简易版(代码非常简单)
第一步,我们实现如下效果:
- 访问/name路径,返回name字符串
- 访问/age路径,返回age字符串
//引入express
var express = require('./express');
//执行express函数
var app = express();
//监听端口
app.get('/name', function (req,res) {
res.end('name');
});
app.get('/age', function (req,res) {
res.end('age');
});
app.get('*', function (req,res) {
res.setHeader('content-type','text/plain;charset=utf8');
res.end('cannot match path');
});
app.listen(3000);
get方法实现:
//声明express函数
var express = function () {
var app = function (req,res) {
var urlObj = require('url').parse(req.url, true);
var pathname = urlObj.pathname;
var method = req.method.toLowerCase();
//找到匹配的路由
var route = app.routes.find(function (item) {
return item.path === pathname && item.method === method;
});
if(route){
route.fn(req,res);
}
res.end(`CANNOT ${method} ${pathname}`)
};
//增加监听方法
app.listen = function (port) {
require('http').createServer(app).listen(port);
};
app.routes = [];
//增加get方法
app.get = function (path,fn) {
app.routes.push({method:'get',path:path,fn:fn});
};
return app;
};
module.exports = express;
使用 * 匹配所有路径:
var route = app.routes.find(function (item) {
- return item.path === pathname && item.method === method;
+ return (item.path === pathname || item.path === '*') && item.method === method;
});
express的post方法
根据请求路径来处理客户端发出的POST请求
- 第一个参数path为请求的路径
- 第二个参数为处理请求的回调函数
app.post(path,function(req,res));
post方法的使用:
//引入express
var express = require('./express');
//执行express函数
var app = express();
//监听端口
app.post('/hello', function (req,res) {
res.end('hello');
});
app.post('*', function (req,res) {
res.end('post没找到');
});
app.listen(3000);
通过linux命令发送post请求
$ curl -X POST http://localhost:3000/hello
post的实现:
增加所有请求的方法
var methods = ['get','post','delete','put','options'];
methods.forEach(function (method) {
app[method] = function (path,fn) {
app.routes.push( { method:method, path:path, fn:fn } );
};
});
中间件
中间件就是处理HTTP请求的函数,用来完成各种特定的任务,比如检查用户是否登录、检测用户是否有权限访问等,它的特点是:
- 一个中间件处理完请求和响应可以把相应数据再传递给下一个中间件
- 回调函数的next参数,表示接受其他中间件的调用,函数体中的next(),表示将请求数据继续传递
- 可以根据路径来区分返回执行不同的中间件
中间件的使用方法:
增加中间件
var express = require('express');
var app = express();
app.use(function (req,res,next) {
console.log('过滤石头');
next();
});
app.use('/water', function (req,res,next) {
console.log('过滤沙子');
next();
});
app.get('/water', function (req,res) {
res.end('water');
});
app.listen(3000);
use方法的实现:在路由数组中增加中间件
app.use = function (path, fn) {
if(typeof fn !== 'function'){
fn = path;
path = '/';
}
app.routes.push({method:'middle', path:path, fn:fn});
}
app方法中增加Middleware判断:
- var route = app.routes.find(function (item) {
- return item.path==pathname&&item.method==method;
- });
- if(route){
- route.fn(req,res);
- }
var index = 0;
function next(){
if(index>=app.routes.length){
return res.end(`CANNOT ${method} ${pathname}`);
}
var route = app.routes[index++];
if(route.method == 'middle'){
if(route.path == '/'||pathname.startsWith(route.path+'/')|| pathname==route.path){
route.fn(req,res,next)
}else{
next();
}
}else{
if((route.path==pathname||route.path=='*')&&(route.method==method||route.method=='all')){
route.fn(req,res);
}else{
next();
}
}
}
next();
错误中间件:next中可以传递错误,默认执行错误中间件
var express = require('express');
var app = express();
app.use(function (req,res,next) {
console.log('过滤石头');
next('stone is too big');
});
app.use('/water', function (req,res,next) {
console.log('过滤沙子');
next();
});
app.get('/water', function (req,res) {
res.end('water');
});
app.use(function (err,req,res,next) {
console.log(err);
res.end(err);
});
app.listen(3000);
错误中间件的实现:对错误中间件进行处理
function next(err){
if(index>=app.routes.length){
return res.end(`CANNOT ${method} ${pathname}`);
}
var route = app.routes[index++];
+ if(err){
+ if(route.method == 'middle'&&route.fn.length==4){
+ route.fn(err,req,res,next);
+ }else{
+ next(err);
+ }
+ }else{
if(route.method == 'middle'){
if(route.path == '/'||pathname.startsWith(route.path+'/')|| pathname==route.path){
route.fn(req,res,next)
}else{
next();
}
}else{
if((route.path==pathname||route.path=='*')&&(route.method==method||route.method=='all')){
route.fn(req,res);
}else{
next();
}
}
+ }
}
获取参数和查询字符串
req.hostname
返回请求头里取的主机名req.path
返回请求的URL的路径名req.query
查询字符串
//http://localhost:3000/?a=1
app.get('/',function(req,res){
res.write(JSON.stringify(req.query))
res.end(req.hostname+" "+req.path);
});
具体实现:对请求增加方法
+ req.path = pathname;
+ req.hostname = req.headers['host'].split(':')[0];
+ req.query = urlObj.query;
获取params参数
req.params 匹配到的所有路径参数组成的对象
app.get('/water/:id/:name/home/:age', function (req,res) {
console.log(req.params);
res.end('water');
});
params实现:增加params属性
methods.forEach(function (method) {
app[method] = function (path,fn) {
var config = {method:method,path:path,fn:fn};
if(path.includes(":")){
//是路径参数 转换为正则
//并且增加params
var arr = [];
config.path = path.replace(/:([^/]+)/g, function () {
arr.push(arguments[1]);
return '([^/]+)';
});
config.params = arr;
}
app.routes.push(config);
};
});
+ if(route.params){
+ var matchers = pathname.match(new RegExp(route.path));
+ if(matchers){
+ var params = {};
+ for(var i = 0; i<route.params.length;i++){
+ params[route.params[i]] = matchers[i+1];
+ }
+ req.params = params;
+ route.fn(req,res);
+ }else{
+ next();
+ }
+}else{}
express中的send方法
参数为要响应的内容,可以智能处理不同类型的数据,在输出响应时会自动进行一些设置,比如HEAD信息、HTTP缓存支持等等
res.send([body]);
当参数是一个字符串时,这个方法会设置Content-type为text/html
app.get('/', function (req,res) {
res.send('<p>hello world</p>');
});
当参数是一个Array或者Object,这个方法返回json格式
app.get('/json', function (req,res) {
res.send({obj:1});
});
app.get('/arr', function (req,res) {
res.send([1,2,3]);
});
当参数是一个number类型,这个方法返回对应的状态码短语
app.get('/status', function (req,res) {
res.send(404); //not found
//res.status(404).send('没有找到');设置短语
});
send方法的实现:自定义send方法
res.send = function (msg) {
var type = typeof msg;
if (type === 'string' || Buffer.isBuffer(msg)) {
res.contentType('text/html').status(200).sendHeader().end(msg);
} else if (type === 'object') {
res.contentType('application/json').sendHeader().end(JSON.stringify(msg));
} else if (type === 'number') {
res.contentType('text/plain').status(msg).sendHeader().end(_http_server.STATUS_CODES[msg]);
}
};
模板的应用
3.1 安装ejs
npm安装ejs
$ npm install ejs
设置模板
使用ejs模版
var express = require('express');
var path = require('path');
var app = express();
app.set('view engine','ejs');
app.set('views',path.join(__dirname,'views'));
app.listen(3000);
3.3 渲染html
配置成html格式
app.set('view engine','html')
app.set('views',path.join(__dirname,'views'));
app.engine('html',require('ejs').__express);
3.4 渲染视图
- 第一个参数 要渲染的模板
- 第二个参数 渲染所需要的数据
app.get('/', function (req,res) {
res.render('hello',{title:'hello'},function(err,data){});
});
模板的实现
读取模版渲染
res.render = function (name, data) {
var viewEngine = engine.viewEngineList[engine.viewType];
if (viewEngine) {
viewEngine(path.join(engine.viewsPath, name + '.' + engine.viewType), data, function (err, data) {
if (err) {
res.status(500).sendHeader().send('view engine failure' + err);
} else {
res.status(200).contentType('text/html').sendHeader().send(data);
}
});
} else {
res.status(500).sendHeader().send('view engine failure');
}
}
静态文件服务器
如果要在网页中加载静态文件(css、js、img),就需要另外指定一个存放静态文件的目录,当浏览器发出非HTML文件请求时,服务器端就会到这个目录下去寻找相关文件
var express = require('express');
var app = express();
var path = require('path');
app.use(express.static(path.join(__dirname,'public')));
app.listen(3000);
静态文件服务器实现
配置静态服务器
express.static = function (p) {
return function (req, res, next) {
var staticPath = path.join(p, req.path);
var exists = fs.existsSync(staticPath);
if (exists) {
res.sendFile(staticPath);
} else {
next();
}
}
};
重定向
redirect方法允许网址的重定向,跳转到指定的url并且可以指定status,默认为302方式。
- 参数1 状态码(可选)
- 参数2 跳转的路径
res.redirect([status], url);
redirect使用
使用重定向
app.get('/', function (req,res) {
res.redirect('http://www.baidu.com')
});
redirect的实现
302重定向
res.redirect = function (url) {
res.status(302);
res.headers('Location', url || '/');
res.sendHeader();
res.end();
};
接收 post 响应体
安装body-parser
$ npm install body-parser
使用body-parser
接收请求体中的数据
app.get('/login', function (req,res) {
res.sendFile('./login.html',{root:__dirname})
});
app.post('/user', function (req,res) {
console.log(req.body);
res.send(req.body);
});
app.listen(3000);
req.body的实现
实现bodyParser
function bodyParser () {
return function (req,res,next) {
var result = '';
req.on('data', function (data) {
result+=data;
});
req.on('end', function () {
try{
req.body = JSON.parse(result);
}catch(e){
req.body = require('querystring').parse(result);
}
next();
})
}
};
if((route.path==pathname||route.path=='*')&&(route.method==method||route.method=='all')){
route.fn(req,res);
}else{
next();
}
}
参考: