前端早早聊大會,與掘金聯合舉辦。加 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。