我們是袋鼠雲數棧 UED 團隊,致力於打造優秀的一站式資料中臺產品。我們始終保持工匠精神,探索前端道路,為社群積累並傳播經驗價值。
本文作者:琉易 https://liuxianyu.cn
本次分享基於『袋鼠雲數棧UED團隊』新發布的 UED Landing 頁 實踐得來,UED Landing 頁集合了團隊目前所有的基礎建設以及精選文章,是團隊展現風采的一個地方。
專案基於 next.js、ts、pnpm、koa2、MongoDB 等技術方式實現,程式碼倉庫:https://github.com/DTStack/UED,歡迎 star。
需求介紹
Landing 頁有一個專欄頁面,需要展示團隊以往發在掘金社群的文章、對應的標籤以及其他社群入口。
設計思路
基於上述的需求分析後,進行以下設計:
1、透過 node-schedule設定一個定時任務,每天去請求掘金的介面查詢最新的文章資料,包括每篇文章的標題、釋出人、閱讀量、釋出日期、標籤等;
2、將上述方法拿到的資料處理後存入 MongoDB 資料庫,只保留需要的欄位;
3、提供一個 RESTful 風格的介面,分頁返回文章列表和標籤列表,供專欄頁面查詢使用;
4、頁面請求介面,查詢標籤、文章資料 ,渲染頁面。
實現步驟
以下實現步驟比較詳細,類似的需求也可以按以下步驟去實現。
Docker 安裝 MongoDB
1、下載映象
docker pull mongo
2、建立掛載資料夾
mkdir -p /opt/dtstack/docker/mongo
cd /opt/dtstack/docker/mongo
3、啟動容器
docker run -v /opt/dtstack/docker/mongo:/data/db --name mongodb -p 27019:27017 -e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD='Admin123!@#' -d mongo --auth
命令解釋
- -v 掛載本地資料夾,儲存資料
- -name 給容器指定名稱
- -p 表示埠對映,
-p 宿主機port:容器port
,這裡不使用相同埠是為了防止攻擊 - -e 攜帶密碼等引數
- -d 後臺執行容器
- --auth MongoDB 進行許可權校驗
4、進入容器
docker exec -it mongodb mongo admin
注意:
rpc error: code = 2 desc = oci runtime error: exec failed: container_linux.go:235: starting container process caused “exec: “mongo”: executable file not found in $PATH”.
如果出現上述報錯,是下載的 mongodb 映象版本比較高,mongodb 5.0 以上的版本需要使用 mongosh命令來代替原來的 mongo 命令。
docker exec -it mongodb mongosh admin
5、驗證使用者名稱密碼登入
返回 1 代表登入成功。
db.auth('root', 'Admin123!@#')
6、使用資料庫
use landingDB
7、建立資料庫的管理員
db.createUser({ user: "landing-user", pwd: "landing-admin.1234", roles: [{ role: "readWrite", db: "landingDB" }] })
MongoDB 不允許同一視窗有多個使用者登入,退出再次進入終端:
db.auth('landing-user', 'landing-admin.1234')
8、建立表
db.createCollection('article')
db.createCollection('tag')
9、測試插入資料
db.article.insert({ id: 1, title: '測試文章標題' })
10、透過 MongoDB Compass 連線資料庫
mongodb://landing-user:landing-admin.1234@127.0.0.1:27019/landingDB
編寫 node 服務
1、藉助 koa2 啟動 node 服務
服務入口處新建定時任務,每天去掘金獲取文章資料
// 引入模組
const Koa = require('koa')
const schedule = require('node-schedule')
// 例項化
const app = new Koa()
const main = async () => {
await initDB()
// 儲存文章列表
const articleList = await getJueJinArticleList()
await insertArticles(articleList)
// 儲存標籤列表
const tagList = getTagList(articleList)
await insertTags(tagList)
}
app.listen(envJson.appPort, () => {
console.log(`app runs on port ${ envJson.appPort }`)
schedule.scheduleJob(cron, main)
})
2、將查詢的資料存入資料庫,並處理歷史資料
const { MongoClient } = require('mongodb')
const url = `mongodb://${username}:${password}@${host}:${port}/${dbName}`
const client = new MongoClient(url)
// 初始化資料庫連結
const initDB = async () => {
await client.connect()
console.log('Connected successfully to mongodb')
}
// 新增查詢到的文章列表
const insertArticles = async (articleList) => {
const db = client.db(dbName)
const collection = db.collection('article')
const updateResult = await collection.updateMany({ isDelete: 0 }, { $set: { isDelete: 1, updateTime: getDateStr() } })
console.log('updateArticles documents =>', updateResult)
const insertResult = await collection.insertMany(articleList)
console.log('insertArticles documents =>', insertResult)
}
3、提供介面,從資料庫讀取資料
介面文件
const Router = require('koa-router')
const router = new Router()
router.get('/api/getTagList', async (ctx) => {
try {
const db = client.db(dbName)
const collection = db.collection('tag')
const data = await collection.find({ isDelete: 0 }).toArray()
ctx.body = {
code: 200,
data,
message: '成功',
}
} catch (error) {
ctx.body = {
code: 1,
error
}
}
})
編寫頁面
1、頁面請求介面,拿到文章資料進行渲染,在標籤、分頁等引數變化時重新請求介面
useEffect(() => {
const params = {
page,
pageSize,
tag_id,
sort_type,
}
fetch(`/api/getArticleList?${new URLSearchParams(params).toString()}`)
.then(res => res.json())
.then(res => {
const { articleList, total } = res.data
setArticleList(articleList || [])
setTotal(total || [])
})
}, [tag_id, sort_type, page])
部署方式
一臺 CentOS 伺服器,安裝 node 14+,pnpm,pm2,Docker(可選),MongoDB,nginx。
mkdir -p /opt/dtstack
git clone https://github.com/DTStack/UED.git
cd UED
pnpm i
pnpm deploy
因為後端服務的介面一般不對外暴露,此處透過 nginx 進行轉發:
# ued landing 的 nginx 配置
# http
server {
listen 80;
server_name ued.dtstack.cn;
location / {
proxy_pass http://localhost:3004/;
}
location /api {
proxy_pass http://localhost:3002/api;
}
}