最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 今天学:Koa 起服务搬砖语雀 API,偷懒建博客

    正文概述 掘金(前端早早聊)   2021-06-27   621

    前端早早聊大会,与掘金联合举办。加 codingdreamer 进大会技术群,赢在新的起跑线。


    第二十九届|前端数据可视化专场,高强度一次性洞察可视化的前端玩法,7-17 全天直播,9 位讲师 9 个小时的知识轰炸(阿里云/蚂蚁/奇安信/小米等),报名上车? ):

    今天学:Koa 起服务搬砖语雀 API,偷懒建博客 所有往期都有全程录播,上手年票一次性解锁全部


    正文如下

    不想自己搭建数据库和后台编辑管理功能,如果把语雀当做是一个云数据库呢,有没有偷巧的办法?

    Node.js(以下简称为 Node) 对前端的最大魅力,无外乎可以启动一个 HTTP 服务后,来提供网站的服务能力,这可以帮助前端工程师完成很多好玩的作品,如何起服务呢:

    Node 起一个服务

    在 Node 的网络请求这里,两个最神奇的东西就是请求和返回,也就是 request 和 response,我们所谓提供的 web 服务,都是在 req 和 res 上面做各种加工,原生 Node 提供了这样的能力,但所有的脏活累活我们都得自己干,不论请求类型的判断,还是是 url 的解析,还是状态码的返回,市面上的 Node 框架也都是在 req 和 res 的基础上做各种封装:

    const http = require('http')
    
    const server = http.createServer((req, res) => {
      // 在这里基于 req 的请求类型和参数完成各种业务处理
      res.statusCode = 200
      res.setHeader('Content-Type', 'text/plain')
      res.end('Hello ZaoZaoLiao')
    })
    
    server.listen(3000, '127.0.0.1', () => {})
    

    Express 起一个服务

    作为上古经典框架,Express 框架(现在已经比几年前轻巧很多了)帮你做好了很多事情,比如请求类型和路由都不需要你处理了,可以拎过来马上根据用户的访问来返回不同的内容,大二全的设计能让你充分偷懒:

    const express = require('express')
    const app = express()
    
    app.get('/', (req, res) => {
      res.send('早')
    })
    app.get('/hi', (req, res) => {
      res.send('早聊')
    })
    app.listen(3001)
    

    Koa 起一个服务

    虽然 Express 很香,但它太重了,特别是早期基于 callback 的设计,让不少团队在 callback hell 的泥潭里填坑填了好多年,而 Koa 更小而美,支持异步(虽然 1 代 Koa 的 generator 有点丑陋),它只做最纯粹的部分,比如上下文的处理、流的处理、Cookie 的处理等等。

    当然 Koa 最吸引我们的是它的洋葱模型,请求可以一层层的进去,再一层层的出来,如果洋葱核心是我们要处理的业务,那么每一层皮都可以看作是外围的一些业务处理,请求在 Koa 中进出要穿越的这些皮,就是 Koa 的中间件,这样理论上我们可以为一个应用扩展出三四十个中间件,处理安全的,处理缓存的,处理日志的,处理页面渲染的...来让应用再次长得肥胖,不过中间件也要根据实际情况做增删,并不是越多越好,越多意味着不确定性越强(尤其是三方中间件),性能也会受影响(社区的的代码层次不齐,整体不一定可控,中间件多执行节点自然也多)。

    无论如何,Koa 让我们可以更精细的控制请求的进入和流出,这给开发者带来了诸多便利:

    const Koa = require('koa')
    const app = new Koa()
    const indent = (n) => new Array(n).join(' ')
    const mid1 = () => async (ctx, next) => {
      ctx.body = `<h3>请求 => 进入第一层中间件</h3>`
      await next()
      ctx.body += `<h3>响应 <= 从第一层中间件穿过</h3>`
    }
    const mid2 = () => async (ctx, next) => {
      ctx.body += `<h2>${indent(4)}请求 => 进入第二层中间件</h2>`
      await next()
      ctx.body += `<h2>${indent(4)}响应 <= 从第二层中间件穿出</h2>`
    }
    app.use(mid1())
    app.use(mid2())
    app.use(async (ctx, next) => {
      ctx.body += `<h1>${indent(12)}::处理核心业务 ::</h1>`
    })
    app.listen(2333)
    

    Egg 起一个服务

    Koa 虽然小而美,可以集成大量中间件,但一个复杂的企业级应用,需要更严谨的约束,无论是功能模型上的设计(体现在目录结构上),还是框架本身的能力集成(体现在模块的书写方式、彼此暴露的接口和调用形式上),都需要有一个既有约束力又方便扩展的架构,这时候 Egg 就登场了,Egg 奉行『约定优于配置』,按照一套统一的约定进行应用开发,除了 service/controller/loader/context...的进一步抽象和改造外,还提供了强大的插件能力,如官方文档所写,一个插件可以包含:

    • extend:扩展基础对象的上下文,提供各种工具类、属性。
    • middleware:增加一个或多个中间件,提供请求的前置、后置处理逻辑。
    • config:配置各个环境下插件自身的默认配置项。

    一个独立领域下的插件实现,可以在代码维护性非常高的情况下实现非常完善的功能,而插件也支持配置各个环境下的默认(最佳)配置,让我们使用插件的时候几乎可以不需要修改配置项。

    // app/controller/home.js
    const Controller = require('egg').Controller
    
    class HomeController extends Controller {
      async index() {
        const { ctx } = this
        ctx.body = 'hi, egg'
      }
    }
    
    module.exports = HomeController
    
    // app/controller/router.js
    module.exports = app => {
      const { router, controller } = app
      router.get('/', controller.home.index)
    }
    
    // config/config.default.js
    module.exports = appInfo => {
      const config = exports = {}
      config.keys = appInfo.name + '_1598512467216_9757'
      config.middleware = []
      const userConfig = {
        // myAppName: 'egg',
      }
    
      return {
        ...config,
        ...userConfig,
      }
    }
    
    // config/plugin.js
    module.exports = {
      // static: {
      //   enable: true,
      // }
    }
    
    

    大家可以前往 Egg 和 Koa 查看更多信息,官方写的非常好了。

    本地起一个简单的博客服务

    Egg 是基于 Koa 来封装的,还可以继续基于 Egg 封装更偏业务向的企业级框架,我们把焦点回归到 Koa,结合获取语雀 API 的能力,我们来用 Koa 搭建一个本地服务吧,本地不安装数据库,数据都从语雀上拿,模板引擎可以用 Pug,目录可以这样设计:

    .
    ├── README.md
    ├── app                                     # 整体应用服务
    │   ├── controllers                         # 控制器:处理业务逻辑
    │   │   ├── article.js                      # 文章详情业务处理
    │   │   └── home.js                         # 路由跳转至指定页面业务处理
    │   ├── router                              # 路由
    │   │   └── routes.js                       # 路由信息配置												
    │   ├── tasks                               # 对接第三方的一些服务任务
    │   │   └── yuque.js                        # yuque 业务逻辑处理:获取文档列表、文档详情、保存文档
    │   └── views                               # 页面
    │       ├── includes
    │       ├── layout.pug
    │       └── pages
    ├── config                                  # 服务配置文件
    │   └── config.js										
    ├── index.js                                # 入口文件
    ├── package-lock.json
    ├── package.json
    └──  public                                 # 静态资源
        ├── css
        │   ├── nav.css
        │   └── style.css
        └── images
            ├── logo.png
            ├── mobile-banner.png
            └── pc-banner.png
    
    

    模块可以安装这几个:

    • axios:可以用在浏览器和 Node.js 的基于 Promise 的 HTTP 客户端
    • Koa:基于 Node.js 平台的 Web 开发框架
    • koa-static:Koa 静态文件服务中间件
    • koa-router:Koa 路由中间件
    • koa-views:Koa 模版渲染中间件
    • moment:JavaScript 日期处理类库

    获取语雀的数据可以这样处理:

    const fs = require('fs')
    const { resolve } = require('path')
    const axios = require('axios')
    // 获取配置信息
    const config = require('../../config/config')
    const { repoId, api, token } = config.yuque
    
    // 把语雀拿来的文章存到本地
    const saveYuque = (article, html) => {
      // 先检查一下, pages 目录的路径是否存在
      // 路径不存在就自动生成一个 pages 目录(首次使用服务), 否则会报错, 会一致无法使用本地缓存
      // 路径存在, 直接在该路径保存语雀的博客文章
      const path = __dirname.substring(0, __dirname.length - 9) + 'public/pages'
      if (!fs.existsSync(path)) {
        fs.mkdirSync(path)
      }
    
      const file = resolve(__dirname, `../../public/pages/${article.id}.html`)
      if (!fs.existsSync(file)) {
        fs.writeFile(file, html, err => {
          if (err) console.log(err)
          console.log(`${article.title} 已写入本地`)
        })
      }
    }
    
    // 封装统一的请求
    const _request = async (pathname) => {
      const url = api + pathname
      return axios.get(url, {
        headers: { 'X-Auth-Token': token }
      }).then(res => {
        return res.data.data
      }).catch(err => {
        console.log(err)
      })
    }
    
    // 获取配置文件指定 repoId 下的所有文章
    const getDocList = async () => {
      try {
        const res = await _request(`/repos/${repoId}/docs`)
        return res
      } catch (err) {
        console.log('获取文章列表失败: ', err)
        return []
      }
    }
    
    // 获取配置文件指定 repoId 下的指定文章内容
    const getDocDetail = async (docId) => {
      try {
        const res = await _request(`/repos/${repoId}/docs/${docId}?raw=1`)
        return res
      } catch (err) {
        console.log('获取文章内容失败: ', err)
        return {}
      }
    }
    
    module.exports = {
      // getYuqueUser,
      getDocDetail,
      getDocList,
      saveYuque
    }
    

    路由可以添加几个博客页面:

    // 页面
    const Home = require('../controllers/home')
    const Article = require('../controllers/article')
    
    module.exports = router => {
      // 网站前台页面
      // router.get(url, controller)
      router.get('/', Home.homePage)
      router.get('/about', Home.about)
      router.get('/joinus', Home.joinus)
      router.get('/contact', Home.contact)
      router.get('/article/:_id', Article.detail)
    }
    

    几个页面交给主控制器处理:

    // 获取配置文件指定 repoId 下的所有文章的方法
    const { getDocList } = require('../tasks/yuque')
    const { teamName } = require('../../config/config')
    
    // 根据指定路径, 用一个 controller 把页面返回给客户端
    exports.homePage = async ctx => {
      const articles = await getDocList()
    
      // render(pug, pug 内需要的变量)
      ctx.body = await ctx.render('pages/index', {
        title: '首页',
        teamName,
        articles
      })
    }
    
    exports.about = async ctx => {
      ctx.body = await ctx.render('pages/about', {
        teamName
      })
    }
    
    exports.joinus = async ctx => {
      ctx.body = await ctx.render('pages/joinus', {
        teamName
      })
    }
    
    exports.contact = async ctx => {
      ctx.body = await ctx.render('pages/contact', {
        teamName
      })
    }
    

    控制器的代码可以这样处理:

    const fs = require('fs')
    const { resolve } = require('path')
    const { getDocDetail, saveYuque } = require('../tasks/yuque')
    const config = require('../../config/config')
    const { root } = config
    
    const streamEnd = fd => new Promise((resolve, reject) => {
      fd.on('end', () => resolve())
      fd.on('finish', () => resolve())
      fd.on('error', reject)
    })
    
    // 查看文章详情
    exports.detail = async ctx => {
      const _id = ctx.params._id
      const fileName = resolve(root, `${_id}.html`)
      const fileExists = fs.existsSync(fileName)
    
      // 首先去本地找是否缓存过资源,如果缓存过直接返回
      if (fileExists) {
        console.log('命中文章缓存,直接返回')
        // 拿到文件流,pipe 给 koa 的 res,让它接管流的返回
        ctx.type = 'text/html; charset=utf-8'
        ctx.status = 200
        const rs = fs.createReadStream(fileName).pipe(ctx.res)
        await streamEnd(rs)
      } else {
        console.log('未命中文章缓存,重新拉取')
        // 如果没缓存过,则从语雀 API 获取后直接返回
        const article = await getDocDetail(_id)
        const body = article.body_html.replace('<!doctype html>', '')
    
        // 服务器返回新拿到的文章数据
        const html = await ctx.render('pages/detail', {
          body,
          article,
          siteTitle: article.title
        })
        // 本地文件缓存也写一份
        saveYuque(article, html)
    
        ctx.body = html
      }
    }
    

    流程虽然简单,但如果大家去面试的时候,被面试官问起这里都缓存如何处理,以这种形式肯定是过不了关的,这里还需要考虑很多边界条件和风险点,比如资源有无、权限、有效性、类型及安全检查、流量判断...等等等等,其中缓存的部分,往往会成为一个考察重点,大家可以在上面多花一些心思,如下伪代码仅抛砖引玉:

    // 304 缓存有效期判断, 使用 If-Modified-Since,用 Etag 也可以
    const fStat = fs.statSync(filePath)
    const modified = req.headers['if-modified-since']
    const expectedModified = new Date(fStat.mtime).toGMTString()
    if (modified && modified == expectedModified) {
      res.statusCode = 304
      res.setHeader('Content-Type', mimeType[ext])
      res.setHeader('Cache-Control', 'max-age=3600')
      res.setHeader('Last-Modified', new Date(expectedModified).toGMTString())
      return
    }
    
    // 文件头信息设置
    res.statusCode = 200
    res.setHeader('Content-Type', mimeType[ext])
    res.setHeader('Cache-Control', 'max-age=3600')
    res.setHeader('Content-Encoding', 'gzip')
    res.setHeader('Last-Modified', new Date(expectedModified).toGMTString())
    
    // gzip 压缩,文件流 pipe 回去
    const stream = fs.createReadStream(filePath, {
      flags: 'r'
    })
    stream.on('error', () => {
      res.writeHead(404)
      res.end()
    })
    stream.pipe(zlib.createGzip()).pipe(res)
    
    

    前端早早聊会时不时发一些面向技术小白的学习文章,大家可以果断关注本账号,常年跟进新动态。


    别忘了第二十九届|前端数据可视化专场,高强度一次性洞察可视化的前端玩法,7-17 全天直播,9 位讲师(阿里云/蚂蚁/奇安信/小米等),报名上车? ):

    今天学:Koa 起服务搬砖语雀 API,偷懒建博客

    所有往期都有全程录播,可以购买年票一次性解锁全部

    ?更多活动


    点赞,评论,求 Mark。

    最后的效果

    今天学:Koa 起服务搬砖语雀 API,偷懒建博客 今天学:Koa 起服务搬砖语雀 API,偷懒建博客

    今天学:Koa 起服务搬砖语雀 API,偷懒建博客


    起源地下载网 » 今天学:Koa 起服务搬砖语雀 API,偷懒建博客

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元