前端早早聊大会,与掘金联合举办。加 codingdreamer 进大会技术群,赢在新的起跑线。javascript
第二十九届|前端数据可视化专场,高强度一次性洞察可视化的前端玩法,7-17 全天直播,9 位讲师 9 个小时的知识轰炸(阿里云/蚂蚁/奇安信/小米等),报名上车👉 ):css
全部往期都有全程录播,上手年票一次性解锁所有html
不想本身搭建数据库和后台编辑管理功能,若是把语雀当作是一个云数据库呢,有没有偷巧的办法?前端
Node.js(如下简称为 Node) 对前端的最大魅力,无外乎能够启动一个 HTTP 服务后,来提供网站的服务能力,这能够帮助前端工程师完成不少好玩的做品,如何起服务呢:java
在 Node 的网络请求这里,两个最神奇的东西就是请求和返回,也就是 request 和 response,咱们所谓提供的 web 服务,都是在 req 和 res 上面作各类加工,原生 Node 提供了这样的能力,但全部的脏活累活咱们都得本身干,不论请求类型的判断,仍是是 url 的解析,仍是状态码的返回,市面上的 Node 框架也都是在 req 和 res 的基础上作各类封装:ios
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 框架(如今已经比几年前轻巧不少了)帮你作好了不少事情,好比请求类型和路由都不须要你处理了,能够拎过来立刻根据用户的访问来返回不一样的内容,大二全的设计能让你充分偷懒:git
mkdir iexpress && cd iexpress && npm init --yes && npm i express -Sgithub
const express = require('express')
const app = express()
app.get('/', (req, res) => {
res.send('早')
})
app.get('/hi', (req, res) => {
res.send('早聊')
})
app.listen(3001)
复制代码
虽然 Express 很香,但它过重了,特别是早期基于 callback 的设计,让很多团队在 callback hell 的泥潭里填坑填了好多年,而 Koa 更小而美,支持异步(虽然 1 代 Koa 的 generator 有点丑陋),它只作最纯粹的部分,好比上下文的处理、流的处理、Cookie 的处理等等。web
固然 Koa 最吸引咱们的是它的洋葱模型,请求能够一层层的进去,再一层层的出来,若是洋葱核心是咱们要处理的业务,那么每一层皮均可以看做是外围的一些业务处理,请求在 Koa 中进出要穿越的这些皮,就是 Koa 的中间件,这样理论上咱们能够为一个应用扩展出三四十个中间件,处理安全的,处理缓存的,处理日志的,处理页面渲染的...来让应用再次长得肥胖,不过中间件也要根据实际状况作增删,并非越多越好,越多意味着不肯定性越强(尤为是三方中间件),性能也会受影响(社区的的代码层次不齐,总体不必定可控,中间件多执行节点天然也多)。面试
不管如何,Koa 让咱们能够更精细的控制请求的进入和流出,这给开发者带来了诸多便利:
mkdir ikoa && cd ikoa && npm init --yes && npm i koa -S
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)
复制代码
Koa 虽然小而美,能够集成大量中间件,但一个复杂的企业级应用,须要更严谨的约束,不管是功能模型上的设计(体如今目录结构上),仍是框架自己的能力集成(体如今模块的书写方式、彼此暴露的接口和调用形式上),都须要有一个既有约束力又方便扩展的架构,这时候 Egg 就登场了,Egg 奉行『约定优于配置』,按照一套统一的约定进行应用开发,除了 service/controller/loader/context...的进一步抽象和改造外,还提供了强大的插件能力,如官方文档所写,一个插件能够包含:
一个独立领域下的插件实现,能够在代码维护性很是高的状况下实现很是完善的功能,而插件也支持配置各个环境下的默认(最佳)配置,让咱们使用插件的时候几乎能够不须要修改配置项。
mkdir iegg && cd iegg && npm init egg --type=simple && npm i && npm run dev
// 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
复制代码
模块能够安装这几个:
获取语雀的数据能够这样处理:
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 位讲师(阿里云/蚂蚁/奇安信/小米等),报名上车👉 ):
全部往期都有全程录播,能够购买年票一次性解锁所有
点赞,评论,求 Mark。