chenwl

chenwl 查看完整档案

广州编辑  |  填写毕业院校  |  填写所在公司/组织 youyoucuocuo.top 编辑
编辑

平坦的路面上曲折前行

个人动态

chenwl 发布了文章 · 11月27日

express中异步函数异常捕获

express中时使用 Async/await 编写异步代码时,每个 async 函数都要包裹在try/catch中,代码量多了看着冗余不优雅,express又不像koa的异步机制可以订阅全局的error事件,为了解决这个问题,需要写个捕获异步函数异常的中间件。

uncaughtException

开始能想到的肯定是try/catch了,但是也想过能否使用nodejs提供的uncaughtException事件,在全局捕获异常,例如下面的代码:

process.on("uncaughtException", (err) => console.log("uncaught Exception"));

const asyncError=()=>{
    throw new Error("some Error");
}

asyncError();

asyncError方法里面抛出的异常会被 uncaughtException订阅,但是在异步函数中,并没走到 uncaughtException,还是会抛出异常:

process.on("uncaughtException", (err) => console.log("uncaught Exception"));

const asyncError=()=>{
    throw new Error("some Error");
}

(async ()=>{
    // 抛出异常
    asyncError();
})()

而且Promise.reject也没走到uncaughtException里面:

const asyncError=()=>{
    return Promise.reject("some error")
}

(async ()=>{
    // 抛出异常
    await asyncError();
})()

所以在express中使用nodejs提供的uncaughtException处理异步错误不太合适,一方面没法捕获和定位上下文错误,另一方面也没法将错误异常提供给中间件函数处理

解决思路

要处理express中的异步函数错误,最好的方法当然是编写处理异常的中间件了,try/catch开路,包裹中间件方法,catch到的异常直接交给next函数处理,代码如下:

 const asyncHandler = fn =>{
    return (req,res,next)=>{
        try{
            fn(req,res,next)
        }catch(next)
    }
}
module.exports = asyncHandler;

接下来,在异步函数中引入中间件处理:

app.use(asyncHandler(async(req, res, next) => {
    await authenticate(req);
    next();
}));

app.get('/async', asyncHandler(async(req, res) => {
    const result = await request('http://example.com');
    res.end(result);
}));

使用asyncHandler方法包裹的async/await函数,如果出现错误就会被Error-handling中间件捕获了

但是每次用到异步函数的时候都要包裹asyncHandler方法,真正用起来也不是很爽,这里推荐使用express-async-errors中间件,其原理是将express里面的中间全部包裹上一层asyncHandler方法,让错误异常无所遁形,全部跑到Error-handling中间件。

前提是引入express后,先引入express-async-errors方法:

const express = require('express');
require('express-async-errors');
const User = require('./models/user');
const app = express();

app.get('/users', async (req, res) => {
  const users = await User.findAll();
  res.send(users);
});

接下来的在异步函数中,就不用都包裹上try/catch了,有错误提前throw Error,写起代码来美滋滋:

app.use(async (req, res) => {
  const user = await User.findByToken(req.get('authorization'));

  if (!user) throw Error("access denied");
});

app.use((err, req, res, next) => {
  if (err.message === 'access denied') {
    res.status(403);
    res.json({ error: err.message });
  }

  next(err);
});~~~~

参考链接:
Using Async/await in Express
Handling errors in express async middleware

查看原文

赞 0 收藏 0 评论 0

chenwl 发布了文章 · 11月18日

H5拖拽上传文件+进度条显示

这篇文章利用H5的拖放API,实现拖拽文件到浏览器上传的功能。

业务需求:

1、拖拽上传文件到浏览器可上传区域
2、上传文件停留在可上传区域,提示信息可上传。
3、释放上传文件,释放位置在可上传区域,执行上传操作,显示上传进度。

有点类似图片压缩网站 tinypng

拖放事件

HTML 的 drag & drop 使用了 DOM event model 以及从 mouse events 继承而来的 drag events 。一个典型的拖拽操作是这样的:用户选中一个可拖拽的(draggable)元素,并将其拖拽(鼠标不放开)到一个可放置的(droppable)元素,然后释放鼠标。

根据业务需求,这里将使用到的拖拽事件包括:

  • dragover 文件被进入到可上传区域时触发
  • dragleave 拖拽文件离开可上传区域时触发
  • drop 拖拽文件在可上传区域释放时触发

接下来编写可上传区域的dom元素:

<div id="target">
    <p class="tip">拖拽文件到此处上传</p>
</div>

添加事件:

const dragEl=document.getElementById("target");

dragEl.addEventListener("dragover",handleOver)
dragEl.addEventListener("drop",handleDrop)
dragEl.addEventListener("dragleave",handleLeave)

// 释放在目标区域
function handleDrop(ev){
    ev.preventDefault();
    // 获取释放文件
    console.log(ev.dataTransfer.files[0])
}
function handleOver(ev){
    ev.preventDefault();
    console.log("进入目标区域")
}
function handleLeave(ev){
    ev.preventDefault();
    console.log("离开目标区域")
}
添加的拖拽事件都要先阻止浏览器默认事件

提示信息

良好的提示信息可以让用户有更好的使用体验,接下来给上传区域添加提示信息:

<div class="u init" id="target">
    <p class="tip-start">拖拽文件到此处上传</p>
    <p class="tip-over">松开上传</p>
    <p class="tip-uploading">上传中...</p>
    <p class="tip-done">上传成功</p>
    <p class="tip-error">上传失败</p>
</div>

通过样式控制信息的显示隐藏:

.u{
    width: 100%;
    height: 95vh;
    display: flex;
    align-items: center;
    justify-content: center;
    transition:background .3s;
}
.u [class^="tip"]{
    opacity: .5;
    font-size: 23px;
    text-align: center;
}
.u [class^="tip"]{
    display: none;
}
.u.init{
    background-color: #f0f0f0;
}
.u.init .tip-start{
    display: block;
}
.u.actived{
    background-color: #f9f9f9;
    border: 1px dashed #ddd;
}
.u.actived .tip-over{
    display: block;
}
.u.uploading{
    background-color: #ddd;
}
.u.uploading .tip-uploading{
    display: block;
}
.u.success{
    background-color: #f5f5f5;
}
.u.success .tip-done{
    display: block;
}
.u.error{
    background-color: #ffd0d0;
}
.u.error .tip-error{
    display: block;
}

这里通过给target元素添加不同的类名,控制不同信息的样式,如下:

  • className=".u.init" 初始状态
  • className=".u.actived" 目标进入可上传区域
  • className=".u.uploading" 文件正在上传
  • className=".u.success" 文件上传成功
  • className=".u.error" 文件上传失败

接下来给拖拽事件添加上对应的显示类名:

function handleDrop(ev){
    ev.preventDefault();
    dragEl.className="u uploading"
    ...
}
function handleOver(ev){
    ev.preventDefault();
    dragEl.className="u actived"
}
function handleLeave(ev){
    ev.preventDefault();
    dragEl.className="u init"
}

文件上传

当文件在上传区域释放之后,获取到上传文件信息,利用FormData对象生成表单数据,接着执行上传操作。

function handleDrop(ev){
    ev.preventDefault();
    dragEl.className="u uploading"
    // 生成表单信息
    const fd = new FormData();
    const file = ev.dataTransfer.files[0];
    fd.append("file",file);
    // 上传文件函数
    upload(fd)
}

生成XMLHttpRequest发送表单信息给后端:

function upload(data){
    const xhr = new XMLHttpRequest();
    // 上传方法和路径
    xhr.open("POST","/upload");
    xhr.responseType = "json";    
    xhr.onload = function(){
        if(xhr.response && xhr.response.success){
            // 上传成功后,显示成功样式
            dragEl.className="u success"
            // 恢复初始状态
            setTimeout(()=>dragEl.className="u init",3000);
        }else{
            // 上传失败
            dragEl.className="u error";
            console.error(xhr.response.error);
        }
    }
    // 发送
    xhr.send(data);
}

为了防止不必要的bug产生,这里执行上传操作之前还需要做一些判断:

  • 文件还在上传状态,此时文件上传区域不可用
  • 拖拽的文件是否符合上传要求(文件类型、文件大小)
function handleDrop(ev){
    ev.preventDefault();
    // 还有文件在上传
    if(dragEl.className.indexOf("uploading")>=0) return;
    ...
    const MAX_SIZE =  200 * 1024 * 1024;
    if(file.size>= MAX_SIZE) return alert("文件大于 200mb");
}

上传进度条

通过xhr.upload.onprogress可以获取到文件的上传进度信息:

xhr.upload.onprogress=function({loaded,total}){
    const precent = (loaded/total)*100;
    console.log(precent)
}

给dom元素添加进度条:

<div class="tip-uploading">
    <p>上传中...</p>
    <p class="progress"><span class="line" id="precent"></span></p>
</div>

添加进度条样式:

.progress{
    height: 5px;
    width: 300px;
    overflow: hidden;
    position: relative;
    border: 1px solid #999;
}
.line{
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    transition:.1s;
    position: absolute;
    background-color: #999;
    transform: translateX(-100%);
}

进度条onprogress事件:

function upload(data){
    ...
    //进度条元素
    const precentEl = document.getElementById("precent");
    xhr.upload.onprogress=function({loaded,total}){
        const precent = (loaded/total)*100;
        // 进度位置
        precentEl.style.transform = `translateX(-${100-precent}%)`
        // 完成上传后,显示上传成功信息
        if(precent>=100) setTimeout(() => dragEl.className="u success", 500);
    }
    ...
}

后端 Express + multer 处理上传文件

整个流程不算复杂,可以通过expressmulter模拟整个流程。

打开控制台,初始化npm文件:

npm init -y

安装expressmulter

npm install express multer --save-dev

文件目录:

/- package.json
/- server.js
/- index.html
/- uploads
  • server.js
const { resolve } = require("path");
const express = require("express");
const app = express();
const multer = require("multer");

const upload = multer({ dest: resolve(__dirname, "./uploads") });

app.use(express.static(__dirname));
app.post("/upload", upload.single("file"), (req, res) => {
  res.send({ success: true, message: req.file });
});

app.listen(3000, () => console.log(`Serving on localhost:3000`));
  • index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>拖拽上传+进度条显示</title>
    <style>
        .u{
            width: 100%;
            height: 95vh;
            display: flex;
            align-items: center;
            justify-content: center;
            transition:background .3s;
        }
        .u.init{
            background-color: #f0f0f0;
        }
        .u.init .tip-start{
            display: block;
        }
        .u.actived{
            background-color: #f9f9f9;
            border: 1px dashed #ddd;
        }
        .u.actived .tip-over{
            display: block;
        }
        .u.uploading{
            background-color: #ddd;
        }
        .u.uploading .tip-uploading{
            display: block;
        }
        .u.success{
            background-color: #f5f5f5;
        }
        .u.success .tip-done{
             display: block;
        }
        .u.error{
            background-color: #ffd0d0;
        }
        .u.error .tip-error{
             display: block;
        }
        .u [class^="tip"]{
            opacity: .5;
            font-size: 23px;
            text-align: center;
        }
        .u [class^="tip"]{
            display: none;
        }
        .progress{
            height: 5px;
            width: 300px;
            overflow: hidden;
            position: relative;
            border: 1px solid #999;
        }
        .line{
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            transition:.1s;
            position: absolute;
            background-color: #999;
            transform: translateX(-100%);
        }
    </style>
</head>
<body>
<div class="u init" id="target">
    <p class="tip-start">拖拽文件到此处上传</p>
    <p class="tip-over">松开上传</p>
    <div class="tip-uploading">
        <p>上传中...</p>
        <p class="progress"><span class="line" id="precent"></span></p>
    </div>
    <p class="tip-done">上传成功</p>
    <p class="tip-error">上传失败</p>
</div>
<script>
    const dragEl=document.getElementById("target");
    dragEl.addEventListener("dragover",handleOver)
    dragEl.addEventListener("drop",handleDrop)
    dragEl.addEventListener("dragleave",handleLeave)

    function handleDrop(ev){
        ev.preventDefault();
        // 还有文件正在上传
        if(dragEl.className.indexOf("uploading")>=0) return;
    
        dragEl.className="u uploading"
        const file = ev.dataTransfer.files[0];
        const fd = new FormData();
        fd.append("file",file);
        upload(fd)
    }
    
    function handleOver(ev){
        ev.preventDefault();
        dragEl.className="u actived"
    }
    function handleLeave(ev){
        ev.preventDefault();
        dragEl.className="u init"
    }

    function upload(data){
        const xhr = new XMLHttpRequest();
        xhr.open("POST","/upload");
        xhr.responseType = "json";
        
        const precentEl = document.getElementById("precent");
        xhr.upload.onprogress=function({loaded,total}){
            const precent = (loaded/total)*100;
            precentEl.style.transform = `translateX(-${100-precent}%)`
            if(precent>=100) setTimeout(() => dragEl.className="u success", 500);
        }
        xhr.onload = function(){
            if(xhr.response && xhr.response.success){
                setTimeout(()=>dragEl.className="u init",3000);
            }else{
                dragEl.className="u error";
                console.error(xhr.response.error);
            }
        }
        xhr.send(data);
    }
</script>
</body>
</html>

控制台执行node server.js,浏览器打开地址可以看到整个流程。

查看原文

赞 1 收藏 0 评论 0

chenwl 发布了文章 · 11月17日

nodejs篇-手写express

通过手写源码可以加深对express的理解,例如路由调用、中间件的运行机制、错误捕获,功能扩展等。express官方源码

express使用

下面是初始化express最简单的示例:

const Express = require("./express")
const app = new Express()

app.use(function(req,res,next){
    req.str = "use";
    next();
});

app.get("/",(req,res,next)=>{
    req.str = "-get1";
    next()
},(req,res,next)=>{
    req.str+="-get2"
    next();
});

// get 请求
app.get("/",(req,res)=>res.end(req.str))
// post 请求
app.post("/post",(req,res)=>res.end("post"))

app.listen(3000);

运行上面的示例,打开地址localhost:3000可以看到use-get1-get2的结果,上面的示例主要包含这几个步骤:

  • 实例化一个app应用
  • app实例包含中间件方法use、路由getpost方法
  • 路由方法包含路径回调函数两个参数
  • requestresponse对象封装到回调函数里面
  • next方法可以决定是否向下执行
  • app实例包含启动和监听服务的方法listen

通过上面步骤的分析,有几点是比较重要的:

1、可以利用数组stack保存用户设置的路由函数和中间件函数。
2、保存在stack里面的函数应该有对应的名称或者路径方便匹配,可以封装成layer对象(名称,路径,方法)。
3、逐一取出stack里面的函数和请求request匹配 (路径和方法) ,匹配成功则执行,并把下一次的执行函数next作为参数传递。

项目结构

根据express先创建出对应的项目结构:

/- lib
    /- index.js
    /- application.js
    /- router
        /- index.js
        /- route.js
        /- layer.js

初始化

/lib/index.js初始化方法:

const Application = require("./application");

function createApplication(){
    return new Application()
}

module.exports = createApplication;

/lib/application.js内容如下:

const http = require("http")

function Application() {}

Application.prototype.listen = function () {
    // 执行监听函数后,开始创建Http服务
    const requestHandler = (req,res)=>{};
    http.createServer(requestHandler).listen(...arguments);
}

module.exports = Application

路由

主要的方法集中在router文件夹里面,里面包含index.jsroute.jslayer.js

分析:

  • 封装请求方法例如getpostdelete等,通过methods包获取
  • router/index 处理app下的路由和中间件
  • router/route 处理路由method下的函数
  • router/layer 保存路径方法函数
  • 每个layer先保存在数组stack里面
  • handleRequest取出layer进行匹配,匹配成功则执行,否则下一个layer匹配(封装成next方法)

router/layer.js

// 保存路径和方法
function Layer(path, handler) {
  this.path = path
  this.method = null;
  this.handler = handler
}
// 匹配路径和请求方法
Layer.prototype.match = function (pathname, method) {
  if (!this.method) {
    let path = this.path === "/" ? "/" : this.path + "/"
    return pathname.startsWith(path) || this.path === pathname;
  } else if (pathname === this.path && method === this.method) {
    return true
  }
}

module.exports = Layer

router/route.js

const methods = require("methods");
const Layer = require("./layer");

function Route(){
    this.stack = [];
}
// 封装请求方法、get、post、delete、put等
methods.forEach(method=>{
    Route.prototype[method] = function(handlers){
        handlers.forEach(handler=>{
            const layer = new Layer("/",handler);
            this.stack.push(layer);
        })
    }
})
// 取出stack保存的方法执行
Route.prototype.handler = function (req, res, next) {
    const dispatch = (index)=>{
        const layer = this.stack[index++];
        if(!layer) return next();
        layer.handler(req,res,next);
    }
    dispatch(0);
}

module.exports = Route;

router/index.js

这里有一点不容易理解,Router里面stack数组保存的layer,包含中间件函数use和路由route实例,
route实例里面也有自己的stack数组,都需要取出来匹配看是否执行:

// route 里面保存的layer
let route1 = [layer,layer,layer]
let route2 = [layer,layer,layer]
let route3 = [layer,layer,layer]

// Router里面中间件就是一个layer
let use = layer;

// Router里面的layer包含中间件和route实例
let Router = [use,route1,route2,route3]

决定是否往下执行的方法dispatch

const dispatch = (index)=>{
    const layer = this.stack[index++];
    // 不存在layer说明已经完成
    if(!layer) return done();
    // next 函数执行下一次的dispatch
    const next = ()=>dispatch(index);
    layer.handler(req,res,next);
}
dispatch(0);

router/index内容如下:

const url = require("url");
const methods = require("methods");
const Layer = require("./layer");
const Route = require("./route");

function Router() {
    this.stack = [];
}

Router.prototype.use = function(path,handler){
    // 中间件函数,如果只有一个参数,重置path参数
    if (typeof path === "function") {
      handler = path
      path = "/"
    }
    const layer = new Layer(path,handler);
    this.stack.push(layer);
}

// 路由方法
Router.prototype.route = function (path, method) {
  const route = new Route()
  // layer里面的handler方法其实是route实例的handler
  const layer = new Layer(path, route.handler.bind(route))
  // 保存请求方法
  layer.method = method
  this.stack.push(layer)
  return route
}

methods.forEach(method=>{
    Router.prototype[method]=function(path,handlers){
        const route = this.route(path,method);
        // 执行route的请求方法,handlers会被保存到route实例的stack数组里面
        route[method](handlers);
    }
})

Router.prototype.handler=function(req,res){
    // 所有layer都没有被匹配到执行done
    const done = ()=>res.end(`Not Found ${req.method} ${req.url}`);
    // 逐一取出layer匹配,看是否执行
    const dispatch=(index)=>{
        const layer = this.stack[index++];
        if(!layer) return done();
        const method = req.method.toLowerCase();
        const {pathname} = url.parse(req.url,true);
        const next = ()=>dispatch(index);
        layer.match(pathname,method) ? layer.handler(req,res,next) : next();
    }
    dispatch(0);
}

module.exports = Router;

application.js

const http = require("http")
const methods = require("methods")
const Router = require("./router")

function Application() {}

// 路由懒加载
Application.prototype.lazy_router = function () {
  if (!this.router) {
    this.router = new Router()
  }
}
// 绑定中间件
Application.prototype.use = function (path, handler) {
  this.lazy_router()
  this.router.use(path, handler)
}
// 绑定路由
methods.forEach((method) => {
  Application.prototype[method] = function (path, ...handlers) {
    this.lazy_router()
    this.router[method](path, handlers)
  }
})
Application.prototype.listen = function () {
  this.lazy_router()
  // 重新绑定this
  const handleRequest = this.router.handler.bind(this.router)
  http.createServer(handleRequest).listen(...arguments)
}

module.exports = Application

错误处理

express可以给next方法传参,参数代表出现错误信息,在以四个参数的中间件中,第一个参数为错误参数:

app.use(function(req,res,next){
    try {
        // 这里没有test方法,会被catch捕获
        req.test();
    } catch (error) {
        next(error)
    }
})

app.use(function(error,req,res,next)=>{
  // 第一个参数为next传递的错误信息
  // 显示结果:TypeError: req.test is not a function
  res.end(error);
});

上面的示例中,req并没有test方法,直接执行会被tryCatch捕获,将错误信息传递给next,将在中间件中出现四个参数的方法里面,以第一个参数的方式被取到。

接下来就需要对中间件的处理函数dispatch进行修改了,让它能够支持传参,并能够传递到出现四个参数的中间件方法中。

修改router/route.js如下:

Route.prototype.handler = function (req, res, next) {
  // 将累加的指针提取出来
  let index = 0
  // 支持传参
  const dispatch = (error) => {
    const layer = this.stack[index++];
    // 如果中间件取完或者出现错误参数,直接跳出
    if (!layer || error) return next(error)
    layer.handler(req, res, (error) => dispatch(error));
  }
  dispatch()
}

修改router/index.js如下:

Router.prototype.handler = function (req, res) {
  let index = 0
  // 保存错误信息
  let errorMsg = ""

  const done = () => {
    if (errorMsg) {
      // 如果有错误信息,并且没有中间件处理,在页面显示出来
      res.statusCode = 500
      res.end(`handle Error ${errorMsg}`)
    } else {
      res.statusCode = 404
      res.end(`Not Found ${req.method} ${req.url}`)
    }
  }
  // 支持传参
  const dispatch = (error) => {
    errorMsg = error
    const layer = this.stack[index++]
    if (!layer) return done()
    const { pathname } = url.parse(req.url, true)
    const method = req.method.toLowerCase()
    const next = (error) => dispatch(error)
    if (error) {
    // 如果有错误参数,交给handleError处理
      layer.handleError(error, req, res, next)
    } else if (layer.match(pathname, method)) {
    // 正常匹配
      layer.handleRequest(req, res, next)
    }else{
        next(error);
    }
  }

  dispatch()
}

同时给layer添加上两种响应处理,修改router/layer.js如下:

Layer.prototype.handleError = function (error, req, res, next) {
  // 没有method方法,代表是中间件,有四个参数代表是错误处理中间件
  if (!this.method && this.handler.length === 4) {
    return this.handler(error, req, res, next)
  } else {
    return next(error)
  }
}

Layer.prototype.handleRequest = function (req, res, next) {
  // 没有四个参数的方法才处理
  return this.handler.length !== 4 ? this.handler(req, res, next) : next()
}

带参数路由

express带参数的路由可以通过request.params获取,如下:

app.get("/article/:sort/:id",(req,res)=>{
    // 访问路径 /article/nodejs/123
    // req.params 可以得到结果:{"sort":"nodejs","id":"123"}
    res.send(req.params)
})

先分析下如何获取路由的路径参数,最终得到我们想要的keyvalue

  • 利用正则替换匹配的路径参数,匹配结果保存到keys数组
  • 替换后的正则匹配访问路径,获取匹配结果values数组
  • 组合keys数组和valuse数组
let path = "/get/:name/:id";
let url = "/get/chenwl/123";
 
let keys = [];
let regExpUrl = path.replace(/:([^\/]+)/g,function(){
    keys.push(arguments[1])
    return "([^/]+)"
})
let [,...values]= url.match(regExpUrl);
// keys = ["name","id"]
// values = ["chenwl","123"]
let result = keys.reduce(
  (memo, current, index) => ((memo[current] = values[index]), memo),
  {}
)
console.log(result) // {name:"chenwl",id:"123"}

这里使用更方便的路径正则匹配包path-to-regexp,使用起来更加方便:

const pathToRegExp = require("path-to-regexp")

let path = "/get/:name/:id"
let url = "/get/chenwl/123"

let keys = [];
let regExpUrl = pathToRegExp(path, keys);

let [,...values]=url.match(regExpUrl);

console.log(keys); // [{ name: 'name' },{ name: 'id'}]
console.log(values); // [ 'chenwl', '123' ]

这里将路径的匹配和参数的绑定放到Layer类中,修改router/layer.js如下:

const pathToRegexp = require("path-to-regexp")

function Layer(path, handler) {
  this.path = path
  this.handler = handler
  // 生成正则路径,同时将参数赋值到keys
  this.regexpUrl = pathToRegexp(this.path,this.keys =[]);
}

Layer.prototype.match = function (pathname, method) {
  if (!this.method) {
    let path = this.path === "/" ? "/" : this.path + "/"
    return pathname.startsWith(path) || this.path === pathname;
  } else if(method === this.method){
    // 匹配正则路径
    let [, ...values] = pathname.match(this.regexpUrl) || [];
    // 如果有值,生成key-value的对象,绑定到this.params中
    if(values.length){
      let keys = this.keys.map(k=>k.name);
      let params = keys.reduce((memo, current, i) => ((memo[current] = values[i]), memo),{})
      this.params = params
      return true;
    }
    // 没有匹配成功,判断路径是否相同
    return pathname === this.path
  }  
}

Layer.prototype.handleError = function (error, req, res, next) {
  if (!this.method && this.handler.length === 4) {
    return this.handler(error, req, res, next)
  } else {
    return next(error)
  }
}

Layer.prototype.handleRequest = function (req, res, next) {
  // 如果有params,添加到request中
  if(this.params) req.params = this.params;
  return this.handler.length !== 4 ? this.handler(req, res, next) : next()
}

module.exports = Layer

app.param

利用app.param可以对路由参数重新赋值,比如下面的操作,判断用户等级,提前显示对应标识:

app.param("level", (req, res, next, value, key) => {
  req.params.level = parseInt(value)
  next()
})

app.param("name", (req, res, next, value, key) => {
  if (req.params.level <= 2) {
    req.params.name = "👑 " + value
  }
  next()
})

app.get("/admin/:name/:level", (req, res) => {
  const { name, level } = req.params;
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" })
  res.end(`${name} level:${level}`)
})

这里需要给application.js添加上param方法,再交由Router去处理:

//application.js
Application.prototype.param = function (key, handler) {
  this.lazy_router()
  this.router.param(key, handler)
}

router/index.js给原型添加paramsFns对象,保存app.params的定义的方法:

function Router() {
  this.stack = []
  // {key:[handler]}
  this.paramsFns = {};
}

router.handler里面中间件进行时,判断layer.params是否有数据,有则先处理Router.paramsFns里面保存的app.param方法:

if (error) {
  layer.handleError(error, req, res, next)
} else if (layer.match(pathname, method)) {
  // 判断是否二级路由
  if (layer.matchMiddleRouter(pathname)) {
    // 截取中间件前置路由
    middleRouter = layer.path
    req.url = req.url.slice(middleRouter.length)
  }
  // layer.match 已经将匹配到的路由params绑定
  if(layer.params){
       // 这里再重新赋值给request对象
      req.params = layer.params;
      // 用handleParams先处理router.paramsFns里面的方法
      this.handleParams(layer, req, res, next)
  }else{
      layer.handleRequest(req, res, next)
  }

} else {
  next(error)
}

原本Layer里面的params也可以去除了:

Layer.prototype.handleRequest = function (req, res, next) {
- if(this.params) req.params = this.params;
  return this.handler.length !== 4 ? this.handler(req, res, next) : next()
}

接下来就是编写router.handleParams方法:

Router.prototype.handleParams = function (layer, req, res, next) {
    let stack = [];
    let keys = layer.keys.map(k=>k.name);
    // 取出当前layer下的所有params.key
    keys.forEach(key=>{
        let fns = this.paramsFns[key];
        if (fns && fns.length) {
              // 先保存到stack里面
              fns.forEach((fn) => stack.push({ key, fn }))
        }
    });

    let index = 0;
    let done = ()=>layer.handleRequest(req, res, next);
    let dispatch = ()=>{
        let paramFn = stack[index++];
        // 当前保存的params栈里面的方法都执行完毕再跳出继续执行后面的中间件
        if(!paramFn) return done();
        let {fn,key} = paramFn;
        // app.param 回调函数参数:req,res,next,value,key
        fn.call(this, req, res, () => dispatch(), req.params[key],key);
    }
    dispatch(0);
}

二级路由

express通过Express.Router()创建二级路由:

const user = Express.Router();
app.use("/user", user)
user.use((req, res, next) => {
    req.router = "/user";
    next();
});
user.get("/name", (req, res)=>{
  res.end(req.router + req.url) // /user/name
});

这里可以看到,创建二级路由并没有通过new生成router实例,而是在Router类上直接调用原型方法。

分析:

  • Router类既能实例化获取相关属性和方法,同时也是一个中间件函数
  • 二级路由的匹配,在中间件函数中判断
  • req.url返回的路径是二级路由路径,需要截取req.url路径
  • 二级路由router.stack里面的中间件执行完成后,再拼接回正常的req.url路径

接下来改造router/index.js这个构造函数,绑定新的原型链:

function Router() {
  // 返回的是一个中间件函数
  const router = (req, res, next) => {
    // router作为中间件函数,也通过绑定原型链proto也有了Router所有的方法和属性
    // 当中间件路径匹配成功,取出二级路由里面保存的stack逐一匹配,也就是执行它的handler方法
    router.handler(req, res, next)
  }
  router.stack = []
  router.paramsFns = {}
  // 重新赋值新的原型链
  router.__proto__ = proto
  return router
}

const proto = {}

// 原型方法绑定到proto上
// proto.param
// proto.handleParams
// proto.use
// proto.route
// proto[method]
// proto.handler

修改lib/application.jsExpress构造函数添加静态方法Router

const Application = require("./application");
+ const Router = require("./router");

function createApplication(){
    return new Application()
}
+ createApplication.Router = Router;

module.exports = createApplication;

修改router.handler方法:

proto.handler = function (req, res, done) {
  let index = 0
  let errorMsg = ""

  // 二级路由的done方法其实是中间件next,如果不存在则都没有匹配到
  done = done || (() => {
      if (errorMsg) {
        res.statusCode = 500
        res.end(`handle Error ${errorMsg}`)
      } else {
        res.statusCode = 404
        res.end(`Not Found ${req.method} ${req.url}`)
      }
    })
  
  // 二级路由根路径
  let middleRouter = ""
  const dispatch = (error) => {
    errorMsg = error
    const layer = this.stack[index++]
    if (!layer) return done()
    const { pathname } = url.parse(req.url, true)
    const method = req.method.toLowerCase()
    const next = (error) => dispatch(error)

    // 执行完二级路由的中间件函数后,拼接回请求的req.url路径
    if (middleRouter) {
      req.url = middleRouter + req.url
      middleRouter = ""
    }

    if (error) {
      layer.handleError(error, req, res, next)
    } else if (layer.match(pathname, method)) {
      // 判断是否二级路由
      if (layer.matchMiddleRouter(pathname)) {
        // 截取中间件前置路由
        middleRouter = layer.path
        req.url = req.url.slice(middleRouter.length)
      }

      layer.handleRequest(req, res, next)
    } else {
      next(error)
    }
  }

  dispatch()
}

同时给Layer类添加matchMiddleRouter方法,判断是否二级路由:

// 判断是否二级路由
Layer.prototype.matchMiddleRouter =  function(pathname){
  // 中间件 && 当前路径不等于"/" && 请求路径跟当前路径不同
  return !this.method && this.path !== "/" && this.path !== pathname
}

扩充方法

expressrequestresponse绑定了一些常用的熟悉和方法,这里主要实现最常用的几种:

  • req.path
  • req.query
  • res.send
  • res.sendFile

修改application.js,添加中间价方法绑定:

Application.prototype.init = function(){
  this.use((req,res)=>{
    const {pathname,query}= url.parse(req.url,true);
    req.path = pathname;
    req.query = query;
    // 发送值
    res.send = function(value){
      if (typeof value === "string" || Buffer.isBuffer(value)) {
        res.end(value)
      } else if (typeof value === "object") {
        res.end(JSON.stringify(value))
      }
    }
    // 发送文件
    res.sendFile = (filename, { root }) => {
      res.setHeader("Content-Type", mime.lookup(filename) + ";charset=utf-8")
      fs.createReadStream(path.join(root, filename)).pipe(res)
    }
    next();
  })
}

静态文件处理

Express通过静态方法static可以生成静态文件服务,给createApplication添加静态方法:

createApplication.static = function (dirname) {
  return function (req, res, next) {
    let { pathname } = url.parse(req.url, true)
    pathname = path.join(dirname, pathname)
    fs.stat(pathname, (err, statObj) => {
      if (err) return next();
      if (statObj.isFile()) {
        fs.createReadStream(pathname).pipe(res)
        return
      } else {
        return next()
      }
    })
  }
}

模板引擎和配置

在使用express的时候,通常会先使用app.set方法配置环境变量,如下:

// 设置环境比变量
app.set("env",process.env.NODE_ENV || "development"); 
//错误中间件判断当前环境,决定是否显示错误信息给用户
app.use((error,req,res,next)=>{
  req.get("env")==="development" ? res.send(error) : next();
})

修改application.js,添加配置对象和方法:

function Application() {
+  this.setting = {}
}

+ Application.prototype.set = function(key,value){
+  // 如果只有一个参数,返回value值,避免跟get方法冲突
+  if(arguments.length === 1) return this.setting[key];
+  this.setting[key] = value;
+ }

Application.prototype.init = function(){
   this.use((req, res, next) => {
     ...
     // 中间件给req添加上get方法
+     req.get = this.get.bind(this)
   }
}

methods.forEach((method) => {
  Application.prototype[method] = function (path, ...handlers) {
    // 如果是get方法,并且只有一个参数,通过set方法可以获得对应的value值
+   if(method === "get" && arguments.length===1){
+      return this.set(path)
+   }
    this.lazy_router()
    this.router[method](path, handlers)
  }
})

添加模板引擎,这里以ejs为例:

function Application() {
    this.setting = {
      "views": "views", // 模板文件夹
      "view engine": "ejs", // 渲染模板后缀
    }
    this.engines = {
      ".ejs": require("ejs").__express, // 渲染方法
    }
}
// 设置渲染模板函数,例如:app.engine(".html", require("ejs").__express)
Application.prototype.engine = function (ext, rednerFn) {
  this.engines[ext] = rednerFn
}

给中间价添加render函数:

res.render = (filename, obj = {}) => {
  try{
    // 获取模板后缀
    let extension = this.get("view engine")
    // 模板文件夹
    let dir = this.get("views")
    // 后缀前面加上".",例如.html或者.ejs
    extension = extension.includes(".") ? extension : "." + extension
    // 拼接文件
    let filepath = path.resolve(dir, filename + extension)
    // 获取渲染函数
    let renderFn = this.engines[extension]
    renderFn(filepath, obj, (err, html) => {
    // 渲染成功后返回
      res.end(html)
    })
  }catch(error){
    next(error);
  }
}

使用方法:

app.set("view engine","html")
app.engine(".html", require("ejs").__express)

app.get("/index",(req,res)=>{
  res.render("index")
})
查看原文

赞 1 收藏 1 评论 0

chenwl 发布了文章 · 11月9日

nodejs篇-手写koa中间件

koa-static

koa-static可以处理静态资源,参数是静态资源文件夹路径,官方的实现包含了更多的参数配,具体可以查看 koajs/static

实现分析:

  • 获取请求url路径,查找静态文件夹下的路径是否能匹配
  • 如果是路径是文件夹,查找文件夹下index.html文件
  • 设置响应头文件类型(mime)
  • gzip压缩,设置压缩类型并返回可读流
  • 出错或文件不存在next继续下一个中间件
const fs = require("fs")
const path = require("path")
const mime = require("mime")
const zlib = require("zlib")

function static(dir) {
  return async (ctx, next) => {
    try {
      let reqUrl = ctx.path
      let abspath = path.join(dir, reqUrl)
      let statObj = fs.statSync(abspath)
      // 如果是文件夹,拼接上index.html
      if (statObj.isDirectory()) {
        abspath = path.join(abspath, "index.html")
      }
      // 判断路径是否准确
      fs.accessSync(abspath);
      // 设置文件类型
      ctx.set("Content-Type", mime.getType(abspath) + ";charset=utf8")
      // 客户端允许的编码格式,判断是否需要gzip压缩
      const encoding = ctx.get("accept-encoding")
      if (/\bgzip\b/.test(encoding)) {
        ctx.set("Content-Encoding", "gzip")
        ctx.body = fs.createReadStream(abspath).pipe(zlib.createGzip())
      } else if (/\bdeflate\b/.test(encoding)) {
        ctx.set("Content-Encoding", "bdeflate")
        ctx.body = fs.createReadStream(abspath).pipe(zlib.createDeflate())
      } else {
        ctx.body = fs.createReadStream(abspath)
      }
    } catch (error) {
      await next()
    }
  }
}

module.exports = static

koa-bodyparser

koa-bodyparser可以处理POST请求的数据,将form-data数据解析到ctx.request.body,官方地址:koa-bodyparser

实现分析:

  • 读取请求数据
  • 设置响应头类型application/json
  • 判断客户端请求头content-type类型
  • 解析数据并绑定到ctx.request.body
function bodyParser() {
  return async (ctx, next) => {
    await new Promise((resolve, reject) => {
      let data = []
      ctx.req.on("data", (chunk) => {
        data.push(chunk)
      })
      ctx.req.on("end", () => {
        let ct = ctx.get("content-type")
        let body = {}
        ctx.set("Content-Type", "application/json")
        if (ct === "application/x-www-form-urlencoded") {
          body = require("querystring").parse(Buffer.concat(data).toString())
        }
        if (ct === "application/json") {
          body = JSON.parse(Buffer.concat(data).toString())
        }
        ctx.request.body = body
        resolve()
      })
      ctx.req.on("error", (error) => {
        reject(error)
      })
    })
    await next()
  }
}

module.exports = bodyParser

koa-router

koa-router可以让koa像express一样控制路由,源码koa-router实现比较复杂,这里实现一个简单版本。

实现分析:

  • 先将请求保存到数组middlewares
  • routes返回中间件函数,获取ctxnext
  • 从数组middlewares过滤出路径/方法相同的数据
  • 中间件处理,传入ctx和封装后的next,处理结束才调用真正的next方法
class Router {
  constructor() {
    // 保存中间件方法的数组
    this.middlewares = []
  }
  get(path, handler) {
    // get、post、delete、put这些方法都要处理,这里只实现get
    this.middlewares.push({ path, handler })
  }
  compose(routes, ctx,next) {
    // 派发每一个存储的中间件方法
    const dispatch = (index) => {
      // 处理完成才执行上下文的next
      if (routes.length === index) return next();
      // 将中间件的next包装成下个执行的dispath,防止多次执行上下文的next方法
      routes[index].handler(ctx, () => dispatch(++index))
    }
    dispatch(0)
  }
  routes() {
    return async (ctx, next) => {
      // 过滤出相同的存储对象
      let routes = this.middlewares.filter((item) => item.path === ctx.url)
      this.compose(routes, ctx,next)
    }
  }
}

module.exports = Router;

koa-better-body

koa里面处理文件上传使用的是koa-better-body,这里需要保证表单中带有multipart/form-data

<form action="/submiturl" method="POST" enctype="multipart/form-data">

下面是通过表单enctype为multipart/form-data提交后台拿到的数据:

------WebKitFormBoundaryfCunWPksjjur83I5
Content-Disposition: form-data; name="username"

chenwl
------WebKitFormBoundaryfCunWPksjjur83I5
Content-Disposition: form-data; name="password"

1234567
------WebKitFormBoundaryfCunWPksjjur83I5
Content-Disposition: form-data; name="avatar"; filename="test.txt"
Content-Type: text/plain

这里是文件内容
------WebKitFormBoundaryfCunWPksjjur83I5--

实现分析:

  • 获取请求信息,请求头需要有multipart/form-data
  • 切割请求信息,提取有用的信息
  • 包含filename的为文件,写入到对应路径
  • 提取其它信息保存到ctx.request.fields
const fs = require("fs");
const path = require("path");

Buffer.prototype.split = function(sep){
    let arr = [];
    let offset = 0;
    let len = Buffer.from(sep).length;
    let current = this.indexOf(sep,offset);
    while (current !== -1) {
        let data=this.slice(offset,current)
        arr.push(data);
        offset = current+len;
        current = this.indexOf(sep, offset)
    }
    arr.push(this.slice(offset));
    return arr;
}

module.exports = function ({ uploadDir }) {
  return async (ctx, next) => {
    // 结果放到 req.request.fields
    await new Promise((resolve, reject) => {
      let data = []
      ctx.req.on("data", (chunk) => {
        data.push(chunk)
      })
      ctx.req.on("end", () => {
        // multipart/form-data; boundary=----WebKitFormBoundaryvFyQ9QW1McYTqHkp
        const contentType = ctx.get("content-type")
        if (contentType.includes("multipart/form-data")) {
          const boundary = "--"+contentType.split("=")[1];
          const r = Buffer.concat(data);
          const arr = r.split(boundary).slice(1,-1);
          const fields = {};
          arr.forEach(line=>{
              let [head,body] = line.split("\r\n\r\n");
              body = body.slice(0,-2); // 取出有效的内容
              head = head.toString();  
              if(head.includes("filename")){
                // 处理文件 
                // 请求头长度  = 总共的内容长度 - 头部长度 - 4个换行符长度 
                const filecontent = line.slice(head.length+4,-2);
                const filenanme = head.match(/filename="(.*?)"/)[1] || uid();
                const uploadPath = path.join(uploadDir, filenanme)
                fs.writeFileSync(uploadPath, filecontent)
              }else{
                fields[head.match(/name="(.*?)"/)[1]] = body.toString();
              }
          })
          ctx.request.fields = fields
        }

        resolve()
      })
      ctx.req.on("error", (error) => {
        reject(error)
      })
    })
    await next()
  }
}

function uid(){
    return Math.random().toString(32).slice(2);
}
查看原文

赞 0 收藏 0 评论 0

chenwl 发布了文章 · 11月6日

nodejs篇-实现一个koa

实现koa有这几个主要步骤:

  • 封装httpServer
  • 上下文context和requestresponse对象
  • 中间件函数 middleware
  • 错误处理

koa 核心源码里面包括这几个文件:

|-- koa
    |-- lib
        |-- application.js
        |-- context.js
        |-- request.js
        |-- response.js

入口文件 application

application.js是核心入口文件,包括导出Koa类函数和核心代码的实现:

const http = require("http")

class Koa {
  constructor() {
    // 上下文 context
    this.ctx = Object.create({});
    // 中间件函数
    this.middleware = null
  }
  use(fn) {
    // 将用户传入的函数绑定到中间件函数中
    this.middleware = fn
  }
  handleRequest(req, res) {
    let ctx = this.ctx;
    // 给上下文添加 request 和 response 对象
    ctx.req = req;
    ctx.res = res;
    // 执行中间件函数
    this.middleware(ctx);
    // 返回结果
    ctx.body ? res.end(ctx.body) : res.end("Not Found")
  }
  listen() {
    let server = http.createServer(this.handleRequest.bind(this))
    server.listen(...arguments)
  }
}

module.exports = Koa;

上面的代码已经完成了httpServer服务的封装,有最基础的context上下文对象,绑定了requestresponse属性,可以在根目录下创建index.js文件测试:

let Koa = require("./koa/lib/application");
let app = new Koa();

app.use((ctx) => (ctx.body = ctx.req.url))

app.listen(3000);

context、request和response

源码里面使用gettersetter属性,封装上下文contextreqres,也就是requestresponse对象。

request

创建request.js,加入下面代码:

let url = require('url');

module.exports = {
  get path() {
    return url.parse(this.req.url,true).pathname
  },
  get query() {
      return url.parse(this.req.url,true).query
  }
}

response

创建response.js,加入下面代码:

module.exports = {
  _body: "",
  get body() {
    return this._body
  },
  set body(value) {
    this.res.statusCode = 200
    this._body = value
  },
}

context

创建context.js,加入下面代码:

let ctx = {}

function defineGetter(property,key){
    // 相当于去 property 上取值
    ctx.__defineGetter__(key,function(){
        return this[property][key]
    });
}

function defineSetter(property,key){
    // 相当于给 property 赋值
    ctx.__defineSetter__(key, function (value) {
      this[property][key]=value
    })
}

defineGetter("request","path");
defineGetter("request","query");

defineGetter("response","body");
defineSetter("response", "body")

module.exports = ctx;

源码里面通过__defineSetter____defineGetter__requestresponse的属性挂载到了上下文context,接下啦修改application.js,引入contextresponserequest

const context = require("./context")
const request = require("./request")
const response = require("./response")
...
    constructor() {
        ...
        // Object.create防止用户直接修改对象,保证每次new Koa都是新的对象
        this.context = Object.create(context)
        this.request = Object.create(request)
        this.response = Object.create(response)
    }
    createContext(req, res) {
        let ctx = this.context
        /*
        * ctx.request.req\ctx.req\ctx.req
        * ctx.response.res\ctx.res\ctx.res
        */
        ctx.request = this.request
        ctx.response = this.response
        ctx.request.req = ctx.req = req
        ctx.response.res = ctx.res = res

        return ctx
    }
    handleRequest(req, res) {
        // 获取新的上下文
        let ctx = this.createContext(req, res)
        // 执行中间件函数
        this.middleware(ctx);
        ...
    }

中间件 middleware

上面的middleware函数只绑定了一个方法,我们知道koa里面是可以绑定多个中间件函数,并且中间件函数包含上下文context和是否继续执行的next函数,因为koa2中使用的是async/await的方式,所以中间件函数返回的都会是一个Promise

middleware改成数组middlewares

  constructor() {
    ...
    this.middlewares = []
    ...
  }
  use(fn){
    //先将函数保存到中间件数组中
    this.middlewares.push(fn)
  }
  ...

接下来创建compose方法处理中间件函数:

  compose(ctx, middlewares) {
    // 当前函数执行指针
    let exectIndex = -1
    let dispatch = async function (index) {
    // 防止同一个中间件函数出现两个dispatch函数抛出异常
      if (exectIndex >= index) return Promise.reject("mulit called next();")
      exectIndex = index
      // 全部执行完成返回Promise
      if (index === middlewares.length) return Promise.resolve()
      // 取出中间件函数处理
      let middleware = middlewares[index]
      // next函数继续取出下一个中间件函数执行
      let next = () => dispatch(++index);
      // 返回执行的中间件函数
      return middleware(ctx, next)
    }
    return dispatch(0)
  }

修改handleRequest函数如下:

  handleRequest(req, res) {
    let ctx = this.createContext(req, res)
    res.statusCode = 404
    let p = this.compose(ctx, this.middlewares)
    p.then(() => {
      ctx.body ? res.end(ctx.body) : res.end("Not Found")
    })
  }

错误处理

koa 里面可以通过订阅error事件捕获中间件函数运行过程中出现的异常:

app.on("error",(error,ctx)=>{
    ctx.res.end(error.toString())
})

这理可以通过继承events对象,获得发布订阅的能力:

const EventEmiter = require("events")
class Koa extends EventEmiter {
    constructor() {
        super()
        ...
    }
    ...
    handleRequest(req,res){
        ...
        p.then(() => {
        ctx.body ? res.end(ctx.body) : res.end("Not Found")
        }).catch(error=>{
            // 铺货错误后发送给error
            this.emit("error", error, ctx)
        })
    }
}

完整的application.js代码:

const http = require("http")
const Stream = require("stream")
const EventEmiter = require("events")
const context = require("./context")
const request = require("./request")
const response = require("./response")

class Koa extends EventEmiter {
  constructor() {
    super()
    this.middlewares = []

    // Object.create防止用户直接修改对象,保证每次new Koa都是新的对象
    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
  }
  use(fn) {
    this.middlewares.push(fn)
  }
  compose(ctx, middlewares) {
    let exectIndex = -1
    let dispatch = async function (index) {
      if (exectIndex >= index) return Promise.reject("mulit called next();")
      exectIndex = index
      if (index === middlewares.length) return Promise.resolve()
      let middleware = middlewares[index]
      return middleware(ctx, () => dispatch(++index))
    }
    return dispatch(0)
  }
  createContext(req, res) {
    let ctx = this.context
    /*
    * ctx.request.req\ctx.req\ctx.req
    * ctx.response.res\ctx.res\ctx.res
    */
    ctx.request = this.request
    ctx.response = this.response
    ctx.request.req = ctx.req = req
    ctx.response.res = ctx.res = res

    return ctx
  }
  handleRequest(req, res) {
    let ctx = this.createContext(req, res)
    res.statusCode = 404
    let p = this.compose(ctx, this.middlewares)
    p.then(() => {
      if (ctx.body instanceof Stream) {
        res.setHeader("Content-Type", "application/octet-stream")
        res.setHeader("Content-Disposition", `attachment;filename=download`)
        return ctx.body.pipe(res)
      }
      if (ctx.body) {
        res.end(ctx.body)
      } else {
        res.end("Not Found")
      }
    }).catch((error) => {
      this.emit("error", error, ctx)
    })
  }
  listen() {
    let server = http.createServer(this.handleRequest.bind(this))
    server.listen(...arguments)
  }
}

module.exports = Koa
查看原文

赞 0 收藏 0 评论 0

chenwl 发布了文章 · 11月2日

理解跨域cors

这篇文章主要讲解在开发过程中,前后端是如何解决跨域问题的,关于跨源资源共享的理论知识可以通过阅读MDN 跨源资源共享(CORS)的文章了解。

解决跨域的手段有很多,这里主要是通过后端 nodejs来做示例。

准备工作

我们知道同源策略约定协议域名端口三者相同才能访问到资源,这里通过nodejs来创造一个不同端口的跨域请求环境。

api接口服务

  • api.js
// api.js
const handleRequest = (req, res) => {
    res.end(JSON.stringify({result:"success"})
)}

require("http").createServer(handleRequest).listen(3000)

前端服务

前端服务index.htmlserver.js

  • server.js
  • index.html
// server.js
const fs = require("fs")
const path = require("path")

const handleRequest=(req,res)=>{
    const indexpath = path.resolve(__dirname, "./index.html");
    fs.createReadStream(indexpath).pipe(res)
};

require("http").createServer(handleRequest).listen(8080)
//index.html
<html><body>
<script>
    const request = new XMLHttpRequest();
    request.responseType="json";
    request.open("GET","http://127.0.0.1:3000",true);
    request.onload = ()=>{
        console.log(request.response);
    }
    request.send();
</script>
</body></html>

启动服务

nodejs环境下执行node apinode server,打开浏览器,输入http://127.0.0.1:8080,在控制台可以查看到报错信息:

Access to XMLHttpRequest at 'http://127.0.0.1:3000/' from origin 'http://127.0.0.1:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Access-Control-Allow-Origin

解决跨域第一步是响应头设置Access-Control-Allow-Origin

Access-Control-Allow-Origin: <origin> | *

origin 参数的值指定了允许访问该资源的外域 URI,通配符 * 表示允许所有,修改api.js如下:

const handleRequest = (req, res) => {
    res.setHeader("Access-Control-Allow-Origin", "*")
    res.end(JSON.stringify({result:"success"}))
}

重新启动node api,刷新8080的地址就可以看到跨域访问的资源了

Access-Control-Allow-Methods

项目中使用RESTful API的话,除了getpost请求,一定还会有deleteput等请求,我们修改index.html,换成delete请求:

 request.open("DELETE","http://127.0.0.1:3000",true);

打开浏览器后会发现,控制台又出现了跨域的报错信息:

Access to XMLHttpRequest at 'http://127.0.0.1:3000/' from origin 'http://127.0.0.1:8080' has been blocked by CORS policy: Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response.

修改api.js,添加上Access-Control-Allow-Methods属性可以解决这个问题:

res.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,OPTIONS")

Access-Control-Max-Age

在浏览器控制台Network中,可以看到每次发送的XHR请求都带上一个options请求,这是因为浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求,我们可以给响应头添加Access-Control-Max-Age,避免每次都发送预检请求,限制options发送频率:

res.setHeader("Access-Control-Max-Age", 86400)

Access-Control-Max-Age 表明该响应的有效时间为 86400 秒,也就是 24 小时。在有效时间内,浏览器无须为同一请求再次发起预检请求。

请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。

Access-Control-Allow-Headers

在前后端的通信中,我们经常会在请求头加上tokan信息发送给服务端,修改index.html如下:

    request.open("POST","http://127.0.0.1:3000/get",true);
    request.responseType="json";
+    request.setRequestHeader("Authorization","token");
    request.onload = ()=>{
        console.log(request.response);
    }
    request.send();

打开浏览器控制台,我们会看到新的跨域报错信息:

Access to XMLHttpRequest at 'http://127.0.0.1:3000/get' from origin 'http://127.0.0.1:8080' has been blocked by CORS policy: Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response.

修改api.js,添加上Access-Control-Allow-Headers,指明实际请求中允许携带的首部字段:

res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization")

Access-Control-Allow-Credentials

我们在使用一些表单或者上传组件库的时候,经常可以看到withCredentials这个属性的设置,因为在跨域的情况下设置的cookies是不发送到服务器的,只有当后端响应头Access-Control-Allow-Credentialstrue时,才能获取到cookies信息。

// index.html
request.withCredentials = true;
document.cookie="name=chenwl";
res.setHeader("Access-Control-Allow-Credentials",true)

不过即便设置了Access-Control-Allow-Credentials,这里后端还是无法拿到cookies信息,因为之前Access-Control-Allow-Origin这里设置成了"*",这里要求 必须指定明确的、与请求网页一致的域名,修改Access-Control-Allow-Origin如下:

res.setHeader("Access-Control-Allow-Origin", req.headers.origin)

通常情况下,我们还会为Access-Control-Allow-Origin设置白名单:

const corsWhitelist = [
  "https://domain1.example",
  "https://domain2.example",
  "https://domain3.example",
]
if (corsWhitelist.indexOf(req.headers.origin) !== -1) {
    res.setHeader("Access-Control-Allow-Origin", req.headers.origin)
}

完整代码

api.js
const handleRequest = (req, res) => {
    res.setHeader("Access-Control-Allow-Origin", req.headers.origin)
    res.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,OPTIONS")
    res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization")
    res.setHeader("Access-Control-Max-Age", 86400)
    res.setHeader("Access-Control-Allow-Credentials", true)

    res.end(JSON.stringify({ cookie: req.headers.cookie }))
}

require("http").createServer(handleRequest).listen(3000)
index.html
<html><body>
<script>
    let request = new XMLHttpRequest();
    request.open("POST","http://127.0.0.1:3000/get",true);
    request.responseType="json";
    request.setRequestHeader("Authorization","tokena");
    request.setRequestHeader("Content-Type","application/json;charsrt=utf-8");
    request.withCredentials = true;
    document.cookie="name=chenwl";
    request.onload = ()=>{console.log(request.response)}
    request.send();
</script>
</body></html>
查看原文

赞 1 收藏 0 评论 0

chenwl 发布了文章 · 11月1日

HTTP消息头常见应用

Accept-Language

Accept-Language通常用来实现多语言:

Accept-Language: <language>
Accept-Language: *

// Multiple types, weighted with the quality value syntax:
Accept-Language: fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5

;q= (q-factor weighting) 表示优先顺序,用相对质量价值表示,又称为权重。

简单实现:

const http = require("http")
const languages = {zh: "你好",en: "hello",jp: "こんにちは",}

const server = http.createServer((req, res) => {
  res.setHeader("Content-Type", "text/plain;charset=utf-8")
  let lang = req.headers["accept-language"] // zh-CN,zh;q=0.9,en;q=0.8
  if (!lang) return res.end("")
  lang = lang
    .split(",")
    .filter((t) => t.includes(";"))
    .map((t) => {
      let [name, q] = t.split(";")
      return {
        name,
        q: q.slice(2) * 1,
      }
    })
    .sort((a, b) => a.q > b.q)
  for (let i = 0; i < lang.length; i++) {
    let work = languages[lang[i].name]
    if (work) {
      res.end(work)
      break
    }
  }
  res.end(JSON.stringify(lang))
})

server.listen(3000, () =>console.log(`Serving on: \r\n  http://localhost:3000`))

Accept-Ranges

HTTP请求范围

HTTP 协议范围请求允许服务器只发送 HTTP 消息的一部分到客户端。范围请求在传送大的媒体文件,或者与文件下载的断点续传功能搭配使用时非常有用。

范围请求的响应状态:

  • 206 Partial Content 服务请求成功
  • 416 Requested Range Not Satisfiable 请求范围越界(范围值超过了资源的大小)
  • 200 OK 不支持范围请求返回整个资源,分段下载时,要先判断

Range 请求头:

Range: bytes=start-end

Range: bytes=10-:第10个字节及最后个字节的数据 Range: bytes=40-100:第40个字节到第100个字节之间的数据.

注意,这个表示[start,end],即是包含请求头的start及end字节的,所以,下一个请求,应该是上一个请求的[end+1, nextEnd]

Content-Range 响应头

// 服务器响应了前(0-10)个字节的数据,该资源一共有(3103)个字节大小。
Content-Range: bytes 0-10/3103;
// 服务器响应了11个字节的数据(0-10)
Content-Length: 11;

代码实现

服务端按range范围下载

const path = require("path")
const http = require("http")
const fs = require("fs")
const DOWNLOAD_FILE = path.resolve(__dirname, "./server_download.txt")
const TOTAL = fs.statSync(DOWNLOAD_FILE).size;

http.createServer((req, res) => {
  res.setHeader("Content-Type", "text/plain;charset=utf-8")
  // curl http://www.example.com -i -H "Range: bytes=0-50"
  const range = req.headers["range"]
  // 没有range直接返回文件
  if (!range) return fs.createReadStream(DOWNLOAD_FILE).pipe(res)
  // 截取范围值,还有种用,隔开的情况这里暂不考虑
  let [, start, end] = range.match(/(\d*)-(\d*)/)
  start = start ? start * 1 : 0
  end = end ? end * 1 : TOTAL

  // 范围请求成功状态码 206 Partial Content
  res.statusCode = 206
  // 设置响应头
  res.setHeader("Content-Range", `bytes ${start}-${end}/${TOTAL}`)
  // 返回范围数据
  fs.createReadStream(DOWNLOAD_FILE, { start, end }).pipe(res)
}).listen(3000, () => console.log(`Serving on 3000`))

客户端下载

const path = require("path")
const http = require("http")
const fs = require("fs")
const DOWNLOAD_FILE = path.resolve(__dirname, "./client_download.txt")
const ws = fs.createWriteStream(DOWNLOAD_FILE)
let start = 0
let mode = "start"  //下载模式 "start" | "pause"

download()
function download() {
  const downloadConfig = {
    hostname: "localhost",
    port: 3000,
    encoding: "utf-8",
    headers: {
      Range: `bytes=${start}-${start + 100}`,
    },
  }
  const request = (res) => {
    let total = res.headers["content-range"].split("/")[1] * 1
    res.on("data", (chunk) => {
      ws.write(chunk)
      if (start <= total) {
        start += 101
        // 打印下载进度
        console.clear();
        console.log(`下载进度:${Math.min(parseInt((start / total) * 100), 100)}%\r\n按p键后回车可暂停`)
        setTimeout(()=>{
            // mode 是start模式 则继续下载
            mode === "start" ? download() : console.log("暂停下载,按任意键回车后下载")
        },1000)
      } else {ws.end()}
    })
    res.on("end", () => {
      if (total > start)  return;
        console.log("下载完成")
        process.exit(1)
    })
  }
  http.get(downloadConfig, request)
}

process.stdin.on("data", (chunk) => {
  if (chunk.toString().includes("p")) {
    //键盘p暂停下载
    mode = "pause"
  } else {
    mode = "start"
    download()
  }
})

User-Agent

User-Agent 首部包含了一个特征字符串,用来让网络协议的对端来识别发起请求的用户代理软件的应用类型、操作系统、软件开发商以及版本号。

User-Agent判断是否移动端,重定向到新地址:

require("http")
  .createServer((req, res) => {
    const ua = req.headers["user-agent"];
    const isMobile = /(iPhone|iPad|iPod|iOS|Android)/i.test(ua);
    const redirectUrl = isMobile ? "https://m.58.com/gz" : "https://gz.58.com";
    
    res.statusCode = 302;
    res.setHeader("Location", redirectUrl)
    res.end()
  })
  .listen(3000, () => console.log(`Serving on: \r\n  http://localhost:3000`))

Referer

Referer 请求头包含了当前请求页面的来源页面的地址,即表示当前页面是通过此来源页面里的链接进入的。服务端一般使用 Referer 请求头识别访问来源,可能会以此进行统计分析、日志记录以及缓存优化等。

示例

Referer: https://developer.mozilla.org/en-US/docs/Web/JavaScript

以下两种情况下,Referer 不会被发送:

  • 来源页面采用的协议为表示本地文件的 "file" 或者 "data" URI
  • 当前请求页面采用的是非安全协议,而来源页面采用的是安全协议(HTTPS)

判断盗链示例:

const url = require("url");
const http = require("http");

http.createServer((req, res) => {
    let referer = req.headers["referer"];

    if (referer) {
        let refererHost = url.parse(referer).host
        let host = req.headers["host"];
        if(refererHost!==host){
            // 被盗链了
        }
    }
}).listen(3000, () => console.log(`Serving on: \r\n  http://localhost:3000`))

Content-Encoding

Content-Encoding 是一个实体消息首部,用于对特定媒体类型的数据进行压缩。当这个首部出现的时候,它的值表示消息主体进行了何种方式的内容编码转换。这个消息首部用来告知客户端应该怎样解码才能获取在 Content-Type 中标示的媒体类型内容。

一般建议对数据尽可能地进行压缩,因此才有了这个消息首部的出现。不过对于特定类型的文件来说,比如jpeg图片文件,已经是进行过压缩的了。有时候再次进行额外的压缩无助于负载体积的减小,反而有可能会使其增大。

使用 gzip 方式进行压缩

服务端配置Content-Encoding字段:

Content-Encoding:gzip

客户端使用 Accept-Encoding 字段说明接收方法:

Accept-Encoding: gzip, deflate

代码实现

const fs = require("fs");
const zlib = require("zlib")
const http = require("http");

http
  .createServer((req, res) => {
    let encoding = req.headers["accept-encoding"]
    if(!encoding) return fs.createWriteStream("./test.html").pipe(res);

    if(/\bgzip\b/.test(encoding)){
        res.setHeader("Content-Encoding","gzip");
        return fs.createWriteStream("./test.html").pipe(zlib.createGzip()).pipe(res)
    }
    if(/\bdeflate\b/.test(encoding)){
        res.setHeader("Content-Encoding", "bdeflate")
        return fs.createWriteStream("./test.html").pipe(zlib.createDeflate()).pipe(res)
    }
  })
  .listen(3000, () => console.log(`Serving on: \r\n  http://localhost:3000`))

注意事项

根据HTTP规范,HTTP的消息头部的字段名,是不区分大小写的.

3.2. Header Fields
Each header field consists of a case-insensitive field name followed by a colon (”:“), optional leading whitespace, the field value, and optional trailing whitespace.
查看原文

赞 0 收藏 0 评论 0

chenwl 发布了文章 · 10月29日

nodejs篇-http缓存

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术,通过复用以前获取的资源,可以显着提高网站性能,降低服务器处理压力,减少了等待时间和网络流量。通过使用 HTTP缓存,变得更加响应性。

这篇文章会介绍三种缓存机制在nodejs中的实现,分别是:

  • 强制缓存 Cache-Control/Expires
  • 对比缓存

    • Last-Modified/If-Modified-Since
    • Etag/If-None-Match

两类缓存规则的不同

  • 强制缓存如果生效,不需要再和服务器发生交互,对比缓存不管是否生效,都需要与服务端发生交互。
  • 强制缓存优先级高于对比缓存强制缓存生效时,不再执行对比缓存规则。

新建http服务

为了方便测试,新建一个简单的http服务进行调试:

const http = require("http")
const url = require("url")
const mime = require("mime")
const fs = require("fs")

const server = http.createServer((req, res) => {
  const { pathname } = url.parse(req.url, true)
  const abspath = process.cwd() + pathname
  fs.stat(abspath, handleRequest)

  // 判断是文件还是文件夹
  function handleRequest(err, statObj) {
    if (err || statObj.isDirectory()) return sendError(err)
    sendFile()
  }
  // 响应错误请求
  function sendError(error) {
    res.statusCode = 404
    res.end(`Not Found \r\n ${error.toString()}`)
  }
  // 响应文件请求
  function sendFile() {
    res.setHeader("Content-Type", mime.getType(abspath) + ";charset=utf-8")
    fs.createReadStream(abspath).pipe(res)
  }
})

server.listen(3000, () => console.log("serving http://127.0.0.1:3000"))

强制缓存

强制缓存指的是在缓存数据未失效的情况下,可以直接使用缓存数据,浏览器通过服务器响应的header获取缓存规则信息。对于强制缓存,响应头header使用Cache-Control/Expires来标明失效规则。

Expires

Expires是HTTP1.0的东西,现在默认浏览器均默认使用HTTP 1.1,所以它的作用基本忽略,我们在响应头header设置Expires,浏览器根据它的到期时间来决定是否使用缓存数据:

res.setHeader("Expries",new Date(Date.now()+5*1000).toUTCString());

Cache-Control

Cache-Control是最重要的规则。常见的取值有private、public、no-cache、max-age,no-store,默认为private。

  • private 客户端可以缓存
  • public 可以被任何中间人(比如中间代理、CDN等)缓存
  • max-age=xxx 缓存的内容将在 xxx 秒后失效(单位是秒)
  • no-cache 需要使用对比缓存来验证缓存数据
  • no-store 所有内容都不会缓存,强制缓存对比缓存都不会触发

可以在handleRequest方法中添加给响应头设置Cache-Control,在浏览器刷新查看效果:

function handleRequest(err, statObj) {
    ...
    res.setHeader("Cache-Control","max-age=10");
    sendFile()
}

如果经常调试前端项目的开发人员,经常会把控制台Disable cache给勾上,这里记得一定要关掉它:

image.png

不出意外的话,可以在Network的请求中看到信息:

Status Code: 200 OK (from disk cache)

对比缓存

对比缓存,服务器将文件的修改信息发送给客户端(浏览器),客户端在下次请求的时候顺便带上,然后服务端就可以拿到上一次的修改信息,跟本地的文件修改信息做比较,告诉客户端是否用缓存数据还是用最新数据了

Last-Modified/If-Modified-Since 对比时间

通过statObjctime属性可以获取文件的修改时间,将这个修改信息通过请求头Last-Modified属性发送给浏览器:

const serverTime = statObj.ctime.toUTCString()
res.setHeader("Last-Modified", serverTime)

下次客户端请求的时候,也会在请求头通过if-modified-since带上:

const clientTime = req.headers["if-modified-since"]

修改handleRequest方法如下:

function handleRequest(err, statObj) {
    if (err || statObj.isDirectory()) return sendError(err)
    const clientTime = req.headers["if-modified-since"]
    const serverTime = statObj.ctime.toUTCString()
    // 如果本地的文件修改时间和浏览器返回的修改时间相同,则使用缓存的数据,返回304
    if (clientTime === serverTime) {
        res.statusCode = 304
        return res.end()
    }
    res.setHeader("Last-Modified", serverTime)
    res.setHeader("Cache-Control", "no-cache") //  对比缓存验证缓存数据
    sendFile()
  }

不过这种方式有两个弊端:

  • 如果一个文件被误修改,然后修改被撤销,这样内容虽然没变化,但最新修改时间会变
  • 文件被周期性的修改,文件内容没有变化,但最新修改时间会变化

Etag/If-None-Match 对比内容

上面提到的两个弊端,可以通过Etag/If-None-Match方式解决,也就是内容对比,不过Etag生成有一定的开销,如果文件频繁变化对服务器有额外压力。

当然我们不可能将内容都存在header里面,这里可以通过crypto将文件内容加密成一串秘钥,写在headerEtag属性中:

const crypto = require("crypto");
...
const md5 = crypto.createHash("md5"); // md5加密
const rs = fs.createReadStream(abspath); // 创建可读流
const data = [];
rs.on("data",(buffer)=>{
    md5.update(buffer); // 读取文件内容过程中加密
    data.push(buffer);
});
rs.on("end",()=>{
    const serverMatch = md5.digest("base64"); // 加密后的文件
    const clientMatch = req.headers["if-none-match"]; // 客户端下次请求会带上serverMatch
    if(clientMatch === serverMatch){ // 对比文件内容是否相同
        res.statusCode = 304;
        return res.end(null);
    }
    // 设置 ETag
    res.setHeader("ETag", serverMatch)
    res.end(Buffer.concat(data));
})

整合

我们可以在业务中根据自己的需要,设置对应的缓存方式,这里通过写个通用方法,将这三种模式整合起来:

  function cache(statObj) {
    // 强制缓存
    res.setHeader("Cache-Control", "max-age=60")

    // 时间对比
    let lastModified = statObj.ctime.toUTCString()
    let modifiedSince = req.headers["if-modified-since"]
    res.setHeader("Last-Modified", lastModified)
    if (modifiedSince !== lastModified) return false

    // 内容对比
    let etag = statObj.size + ""
    let noneMatch = req.headers["if-none-match"]
    res.setHeader("ETag", etag)
    if (etag !== noneMatch) return false

    return true
  }
  ...
  if(cache(statObj)){
    res.statusCode = 304
    return res.end(null)
  }

参考文章:
彻底弄懂HTTP缓存机制及原理
HTTP 缓存
协商缓存

查看原文

赞 0 收藏 0 评论 0

chenwl 发布了文章 · 10月29日

从零实现一个http-server并发布到npm

这篇文章参考http-server从零实现一个自己的http-server命令行工具

准备工作

跟控制台的交互,这里用到的是 commanderchalk

对于http服务中文件类型的判断这里用到mime,内容模板使用ejs

  • 文件格式类型 mime
  • 内容模板 ejs
在本地开发npm模块的时候,可以使用npm link命令,将npm模块链接到对应的运行项目中,方便地对模块进行调试和测试。

新建文件夹http-server,在文件根目录下执行命令npm init -y,生成package.json文件,给package.json新增bin命令:

// package.json
{
  "name": "http-server",
  "version": "1.0.0",
  "license": "MIT",
  "bin": {
    "my-http-server": "./bin/www.js"
  }
}

在控制台执行npm link可以将全局的my-http-server命令指向这个目录了,当然名称可以根据自己的需要更换,路径./bin/www.js是执行这个命令的文件。
接着就可以到想启动的http服务的文件目录下,打开控制台执行my-http-server启动我们的命令了,不过在这之前还要先创建www.js文件,我们执行的命令,实际是执行node www.js

创建:

$ mkdir bin
$ touch mkdir/www.js

为了让文件能够以nodejs的环境执行,www.js文件开头需要输入声明:

#! /usr/bin/env node

配置项

开始之前,还需要新建配置项,在http-server文件夹下新建config.js输入配置内容:

let config = {
  port: 3000, // 端口号
  host: "127.0.0.1", // 启动路径
  dir: process.cwd() // 命令行执行目录
}
module.exports = config;

通过commander可以让用户方便的跟控制台交互,修改bin/www.js文件下的内容如下:

#! /usr/bin/env node

let config = require("../config");
const commander = require("commander");
const {version} = require("../package.json");

commander
  .version(version)
  .option("-p --port <n>", "set http-server port") // 端口号
  .option("-0 --host <n>", "set http-server host") // 主机路径
  .option("-d --dir <n>", "set http-server directory") // 服务启动路径
  .on("--help",()=>{
      console.log("Example");
      console.log(" $ my-http-server --port --host");
  })
  .parse(process.argv)

// 拿到用户跟控制台交互后的命令,合并到配置文件中
 config = {...config,...commander};

保存文件,在控制台下输入my-webpack-server -h就可以看到对应的交互信息了。

createServer

http-server文件夹下新建server.js,创建Server类,封装httpcreateServer方法:

  • server.js
const http = require("http")

module.exports = class Server {
    constructor(config){
        this.dir = config.dir
        this.port = config.port
        this.host = config.host
    }
    handleRequest(req,res){}
    start(){
        const server = http.createServer(this.handleRequest.bind(this))
        server.listen(this.port, this.host, () => {
            // 成功后回调函数
        })
    }
}

为了能在启动服务后,在控制台输出多色彩的字符串信息,我们可以利用chalk这个依赖包,我们添加下面代码到server.js

const chalk = require("chalk")
...
server.listen(this.port, this.host, () => {
    console.log(chalk.yellow(`Starring up http-server,\r\nserving ${this.dir} \r\n Available on \r\n`))
    console.log(chalk.green(`http://${this.host}:${this.port}`))
})
...

handleRequest 处理请求

接下来开始封装请求方法,handleRequest函数主要做下面的工作:

  • 获取requestsurl路径
  • 判断路径是文件还是文件夹fs.stat
  • 若路径不存在对应文件文件夹,则返回404
  • 文件则判断文件mime类型,响应文件信息
  • 文件夹判断文件夹里面是否有index.html文件,有则渲染index.html,否则利用ejs模板渲染文件列表
const url = require("url")
const ejs = require("ejs")
const mime = require("mime")
const fs = require("mz/fs")

// 模板文件
const template = fs.readFileSync(
    path.resolve(__dirname, "./template.ejs"),
    "utf-8"
)

...
constructor(){...}
async handleRequest(req, res) {
    this.req = req
    this.res = res
    // 通过url解析路径名
    const { pathname } = url.parse(req.url, true)
    // 拿到文件绝对路径,decodeURIComponent 防止中文符号无法识别
    const absPath = path.join(this.dir, decodeURIComponent(pathname))
    try {
        // 判断是文件还是文件夹
        const statObj = await fs.stat(absPath)
        if (statObj.isDirectory()) {
            let indexPath = absPath+"/index.html";
            // 如果文件夹里面有index.html 则进入
            fs.access(indexPath)
            .then(this.sendFile.bind(this, indexPath))
            .catch(async () => {
                // 模板文件过滤掉.DS_Store,并加上href链接
                let files = await fs.readdir(absPath)
                files = files
                .filter((f) => f !== ".DS_Store")
                .map((file) => ({ file, href: path.join(pathname, file) }))
                    let str = ejs.render(template, {
                    name: path.basename(pathname) || "文件列表",
                    files,
                });
                res.setHeader("Content-Type", "text/html;charset=utf-8")
                res.end(str)
            })
        } else {
            // 文件直接返回
            this.sendFile(absPath)
        }
    } catch (error) {
        this.sendError(error)
    }
}
sendError(error) {
    this.res.statusCode = 404;
    // 返回错误信息,生产环境需要屏蔽
    this.res.end(error.toString())
}
sendFile(filePath) {
    // 判断文件mime类型
    this.res.setHeader(
        "Content-Type",
        mime.getType(filePath) + ";charset=utf-8"
    )
    // 创建文件可读流并返回
    fs.createReadStream(filePath).pipe(this.res)
}
start(){}

模板文件

模板文件渲染文件夹的文件列表

// template.ejs
<ul>
    <% files.forEach(function(item){ %>
    <li><a href="<%=item.href %>"><%=item.file%></a></li>
    <% }) %>
</ul>

引入

最后www.js文件引入Server类,初始化并渲染:

const Server = require("../server")
let server = new Server(config);
server.start()

找个想启动http服务的文件夹打开控制台,输入my-http-server就可以启动服务了。

发布npm包

http-server文件夹下打开控制台,输入下面指令:

  • 如果你是第一次发包,使用npm adduser
  • 如果不是第一次发,使用npm login

输入账号密码和注册邮箱

username:yourname
password:
Email:(this is public)email@domain.com 

确保是在package.json同级目录,然后执行npm publish发布:

$ npm publish
查看原文

赞 0 收藏 0 评论 0

chenwl 发布了文章 · 10月27日

nodejs篇-Stream

什么是流?流是一种抽象的数据结构。想象水流,当在水管中流动时

nodejs 的流接口:
image.png

Stream 有四种流类型:

  • Readable - 可读操作。
  • Writable - 可写操作。
  • Duplex - 可读可写操作.
  • Transform - 操作被写入数据,然后读出结果。

所有的 Stream 对象都是 EventEmitter 的实例。常用的事件有:

  • data - 当有数据可读时触发。
  • end - 没有更多的数据可读时触发。
  • error - 在接收和写入过程中发生错误时触发。
  • finish - 所有数据已被写入到底层系统时触发。

可读流 createReadStream

可读流是生产数据用来供程序消费的流。常见的数据生产方式有读取磁盘文件、读取网络请求内容等。

const fs = require("fs");
const path=require("path");
let rs = fs.createReadStream(path.resolve(__dirname, "./read.txt"))

// 保存读取的数据
const data=[];
// 读取数据
rs.on("data",(chunk)=>data.push(chunk));
// 读完收工
rs.on("end", () => console.log(Buffer.concat(data).toString()))
// 出错
rs.on("error", (err) => console.log(`${err.path}文件路径错误或损坏`));

可写流 createWriteStream

可写流是对数据流向设备的抽象,通常将可读流读取到的数据通过可写流写入。

const fs = require("fs");
const path=require("path");

let ws = fs.createWriteStream(path.resolve(__dirname, "./write.txt"))

ws.write("使用Stream写入文本数a据...\n");
ws.write(Buffer.from("使用Stream写入二进制数据...\n"))
ws.write("END.");
ws.end();
ws.on("finish", () =>console.log("写入完成"));

管道流

管道提供了一个输出流到输入流的机制。通常我们用于从一个流中获取数据并将数据传递到另外一个流中。

var fs = require("fs");

// 创建一个可读流
var readerStream = fs.createReadStream('input.txt');

// 创建一个可写流
var writerStream = fs.createWriteStream('output.txt');

// 管道读写操作
// 读取 input.txt 文件内容,并将内容写入到 output.txt 文件中
readerStream.pipe(writerStream);

利用管道流优化大文件在网络中的传输

通过读取文件到内容,在完成后再响应给客户端,遇到大文件消耗内存且等待时间过长

const fs = require('fs');
const server = require('http').createServer();

server.on('request', (req, res) => {
  fs.readFile('./big.file', (err, data) => {
    if (err) throw err;
    res.end(data);
  });
});

server.listen(8000);

利用http请求中requestresponse可读流和可写流的特性,通过管道传输

const fs = require('fs');
const server = require('http').createServer();

server.on('request', (req, res) => {
  const src = fs.createReadStream('./big.file');
  src.pipe(res);
});

server.listen(8000);

读流写流和双工流

所有可以读取数据的流都继承自stream.Readable,所有可以写入的流都继承自stream.Writable。

image.png

Readable

const { Readable } = require("stream")

let arr=["a","b","c","d"];
class MyReadStream extends Readable {
  _read() {
    if (arr.length)  return this.push(arr.splice(0, 1)[0])
    // 返回null表示读完
    return this.push(null)
  }
}

let rs = new MyReadStream()

let data = []
rs.on("data", (chunk) => data.push(chunk))
rs.on("end", () => {
  let result = Buffer.concat(data).toString()
  console.log(result)
})

Writable

const fs = require("fs")
const { resolve } = require("path")
const { Writable } = require("stream")

class MyWriteStream extends Writable {
  _write(chunk, encoding, callback) {
    fs.appendFile(resolve(__dirname, "./write.txt"), chunk, () => {
      // 3 秒写入一次
      // callback 相当于 clearBuffer() 清除缓存的读取数据
      setTimeout(callback, 3000)
    })
  }
}

let ws = new MyWriteStream()
ws.write("新")
ws.write("增")
ws.write("加")
ws.write("的")
ws.write("内容")

Duplex 双工流

双工流既能读又能写

const { Duplex } = require("stream")

class MyDuplex extends Duplex {
  _read() {
    console.log("read")
  }
  _write(chunk, encoding, callback) {
    console.log(chunk)
    callback()
  }
}

let duplex = new MyDuplex()

duplex.on("data", (chunk) => console.log("on data"))
duplex.write("1")
duplex.write("2")

Transform

_transform能够截取到写入的流内容

const { Transform } = require("stream")

class MyTransform extends Transform{
    _transform(chunk,encoding,callback){
        this.push("test"); // 多push个内容
        this.push(chunk);
        // this.push(null);
        callback();
    }
}

let myTransform = new MyTransform();
myTransform.on("data",(chunk)=>{
    console.log(chunk.toString());
})
myTransform.write("这是写入的内容");

修改输入控制台的输出内容:

const { Transform } = require("stream")
class MyTransform extends Transform{
    _transform(chunk,encoding,callback){
        // 对输入内容进行大小写转换
        this.push(chunk.toString().toUpperCase());
        this.push("\n\r")
        callback()
    }
}

let myTransform = new MyTransform();
process.stdin.pipe(myTransform).pipe(process.stdout);

参考链接
Node.js Streams: Everything you need to know
菜鸟教程
廖雪峰JavaScript教程

查看原文

赞 1 收藏 1 评论 0

认证与成就

  • 获得 38 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-05-01
个人主页被 1k 人浏览