{"id":24419126,"url":"https://github.com/electroluxcode/data-card","last_synced_at":"2025-12-29T22:39:29.322Z","repository":{"id":199003798,"uuid":"701971208","full_name":"electroluxcode/data-card","owner":"electroluxcode","description":"ts构建的服务端监控卡片，可以用在你的readme上面","archived":false,"fork":false,"pushed_at":"2023-10-16T01:34:05.000Z","size":125,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-01-20T09:18:58.842Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://data-card-ten.vercel.app/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/electroluxcode.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2023-10-08T05:40:03.000Z","updated_at":"2023-12-13T10:47:04.000Z","dependencies_parsed_at":null,"dependency_job_id":"67406905-ba03-4e99-bb73-8eae335013aa","html_url":"https://github.com/electroluxcode/data-card","commit_stats":null,"previous_names":["yilaikesi/data-card","electroluxcode/data-card"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/electroluxcode%2Fdata-card","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/electroluxcode%2Fdata-card/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/electroluxcode%2Fdata-card/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/electroluxcode%2Fdata-card/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/electroluxcode","download_url":"https://codeload.github.com/electroluxcode/data-card/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243411194,"owners_count":20286559,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-01-20T09:19:03.007Z","updated_at":"2025-12-29T22:39:29.292Z","avatar_url":"https://github.com/electroluxcode.png","language":"TypeScript","readme":"# 从0教你搭建自己的服务端数据卡片\n\n\n\n## 1.1 从0搭建基本web结构\n\n```shell\n│  app.ts\n│  package.json\n│  README.md\n├─common\n│      cache.ts\n│      theme.ts\n│      utils.ts\n└─public\n```\n\n我们首先把这些基础的文件填充一下\n\n\n\n### 1.1.1 app.ts\n\n- 简单的api请求示例，没啥好说的\n\n  ```ts\n  const express = require('express');\n  const http = require('http');\n  const serveStatic = require('serve-static');\n  const app = express();\n  const { cacheTime } = require('./common/cache');\n  const path = require('path');;\n  app.use('/api/test', function(req, res) {\n      res.send({\n          code:200,\n          message:cacheTime\n      })\n  });\n  app.use(\n    serveStatic(path.join(__dirname, 'public'), {\n      maxAge: cacheTime * 1000,\n    })\n  );\n  const server = http.createServer(app);\n  server.listen(3000);\n  module.exports = app;\n  ```\n\n  \n\n### 1.1.2 common/cache.ts \n\n- 经典lru\n\n  ```ts\n  // https://github.com/isaacs/node-lru-cache\n  const LRU = require('lru-cache');\n  \n  const cacheTime = process.env.CACHE_TIME || 100 * 60; // 100 min\n  const maxCacheItems = process.env.MAX_CACHE_ITEMS || 1024;\n  \n  const options = {\n    max: maxCacheItems,\n    // how long to live in ms\n    ttl: (cacheTime as number) * 1000,\n    // return stale items before removing from cache?\n    allowStale: true,\n    updateAgeOnGet: false,\n    updateAgeOnHas: false,\n  };\n  \n  const cache = new LRU(options);\n  \n  export {\n    cache,\n    cacheTime,\n  };\n  \n  ```\n\n  \n\n### 1.1.3 common/theme.ts \n\n- 自定义主题的地方\n\n  ```ts\n  interface ThemeType{\n    IconColor:string;\n    TextColor:string;\n    BackgroundColor:string\n  }\n  let themes:Record\u003cstring,ThemeType\u003e = {\n    'black': {\n      IconColor: 'Lightblue',\n      TextColor: 'rgba(250,250,250,1)',\n      BackgroundColor:\"rgba(0,0,0,0.85)\"\n    },\n    'white': {\n      IconColor: '#2f80ed',\n      TextColor: '#434d58',\n      BackgroundColor:\"#fffefe\"\n    },\n    \"default\":{\n      IconColor: '#2f80ed',\n      TextColor: '#434d58',\n      BackgroundColor:\"#fffefe\"\n    }\n  };\n  function getTheme(theme = 'light') {\n    if (theme in themes) {\n      return themes[theme] as ThemeType;\n    } else {\n      return themes['light'] as ThemeType;\n    }\n  }\n  \n  export {\n    getTheme\n  }\n  ```\n\n### 1.1.3 common/utils.ts\n\n- 一些工具方法\n\n```ts\nconst mobileConfig = {\n  headers: {\n    'User-Agent':\n      'Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Mobile Safari/537.36',\n  },\n};\nconst desktopConfig = {\n  headers: {\n    'User-Agent':\n      'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36',\n  },\n};\n\nconst isEndsWithASCII = (str) =\u003e {\n  if (str.length === 0) return false;\n  return str.charCodeAt(str.length - 1) \u003c= 127;\n};\n\nconst encodeHTML = (str) =\u003e {\n  return str\n    .replace(/\u0026/g, '\u0026amp;')\n    .replace(/\u003c/g, '\u0026lt;')\n    .replace(/\u003e/g, '\u0026gt;')\n    .replace(/\"/g, '\u0026quot;')\n    .replace(/'/g, '\u0026apos;');\n};\n\nexport  {\n  mobileConfig,\n  desktopConfig,\n  isEndsWithASCII,\n  encodeHTML,\n};\n\n```\n\n\n\n### 1.1.4 tsconfig.json\n\n- 一些数据结构我才加typescript了。其他的懒得加。所以把strict 关掉了\n\n```ts\n{\n  \"compilerOptions\": {\n    \"target\": \"esnext\", // 使用最新的 ECMAScript 版本\n    \"module\": \"commonjs\",\n    \"lib\": [\n      \"ES2022\",\n      \"dom\",\n      \"es6\"\n    ],\n    \"rootDir\": \"./\", /* Specify the root folder within your source files. */\n    \"strict\": false /* Enable all strict type-checking options. */,\n    \"noImplicitThis\": false,\n    \"skipLibCheck\": true /* Skip type checking all .d.ts files. */,\n    \"esModuleInterop\": true, // important!\n  },\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n```\n\n\n\n### 1.1.5 package.json\n\n- 可以看到npm run dev 启动，这里启动用到了concurrently ，  nodemon 和 tsc 的 watch。主要是因为 nodemon 和 tsc 并不能够同时执行，所以需要 concurrently  作为媒介使得双方同时启动\n- 然后是 cheerio,axios都 是 用来执行爬取 html 的工具\n\n```json\n{\n    \"name\": \"data-card\",\n    \"version\": \"0.1.0\",\n    \"description\": \"\",\n    \"main\": \"app.js\",\n    \"scripts\": {\n        \"start\": \"node ./app.js\",\n        \"test\": \"node ./test.js\",\n        \"dev\": \"concurrently \\\"npm run dev:server\\\" \\\"npm run dev:compile\\\"\",\n        \"dev:compile\": \"tsc --project ./ --watch \",\n        \"dev:server\": \"nodemon ./app.js\"\n    },\n    \"repository\": {\n        \"type\": \"git\",\n        \"url\": \"git+https://github.com/songquanpeng/readme-stats.git\"\n    },\n    \"author\": \"\",\n    \"license\": \"MIT\",\n    \"bugs\": {\n        \"url\": \"https://github.com/songquanpeng/readme-stats/issues\"\n    },\n    \"homepage\": \"https://github.com/songquanpeng/readme-stats#readme\",\n    \"dependencies\": {\n        \"axios\": \"^0.21.1\",\n        \"cheerio\": \"^1.0.0-rc.5\",\n        \"concurrently\": \"^8.2.1\",\n        \"express\": \"^4.17.1\",\n        \"form-data\": \"^4.0.0\",\n        \"lru-cache\": \"^7.14.1\",\n        \"serve-static\": \"^1.15.0\",\n        \"typescript\": \"^5.2.2\"\n    },\n    \"devDependencies\": {\n        \"nodemon\": \"^2.0.7\",\n        \"prettier\": \"^2.2.1\"\n    },\n    \"prettier\": {\n        \"singleQuote\": true\n    },\n    \"engines\": {\n        \"node\": \"16.x\"\n    }\n}\n\n```\n\n\n\n\n\n\n\n### 1.1.6 启动\n\n这个时候我们可以 npm run dev 启动一下。然后你就可以试试访问你的\n\n\u003e http://localhost:3000/api/test\n\n如果这个时候界面中显示\n\n```ts\n{\n  \"code\": 200,\n  \"message\": \"success\"\n}\n```\n\n那么你的基本结构就已经完成了\n\n\n\n\n\n\n\n## 1.2 添加服务端爬取卡片(用掘金作为示例)\n\n\n\n爬取卡片我们分成几个层级来讲\n\n- model层：定义数据获取的地方\n- views层：怎么将数据渲染\n- common：主要是一些工具函数和一些静态资源：例如 icon，cache，theme\n\n\n\n\n\n我们首先需要定义我们的传参是啥 \n\n```ts\ninterface param{\n    id:string:number;\n\tlang:\"zh-CN\" | any;\n\ttheme:\"white\" | \"black\"\n}\n```\n\n定义以下的api传参示例:http://localhost:3000/api/juejin?id=3004311888208296\u0026lang=zh-CN\u0026theme=black。\n\n下面是流程图\n\n\n\n\n\n\n\n```mermaid\nflowchart TD\n    api[app.ts 传入 id lang theme]  --\u003e | 入口 | api_file(api文件夹/juejin.ts)\n    api_file(api文件夹/juejin.ts) --\u003e  iscache{缓存}\n    \n    iscache --\u003e | 有lru缓存 | iscacheyes[读取缓存]\n    iscacheyes --\u003e view\n    \n    iscache{缓存} --\u003e | 无lru缓存,执行爬虫爬取文件 | iscacheno[model/juejin.ts]\n    iscacheno --\u003e  crawldom(api接口后解析dom)\n    iscacheno --\u003e  crawlapi(api接口)\n    \n    crawldom --\u003e view[view/juejin.ts执行renderJuejinCard]\n    crawlapi --\u003e view[view/juejin.ts执行renderJuejinCard]\n     \n    view  --\u003e  renderitemlang[判断语言]\n    view  --\u003e  renderitemstruct[组装数据array的Item]\n    \n    renderitemlang --\u003e last[根据传入的数据common/render.ts 执行 Render]\n    renderitemstruct --\u003e last\n     \n    last --\u003e renderType[renderType判断type是title,shape,textarea连接dom]\n    last --\u003e renderTheme[渲染主题色]\n     \n```\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n### 1.2.0 入口 \n\n\n\n根路径的 app.ts 写入,如下方法\n\n```ts\nconst express = require('express');\nconst http = require('http');\nconst serveStatic = require('serve-static');\nconst app = express();\nconst { cacheTime } = require('./common/cache');\nconst path = require('path');;\n\napp.use('/api/test', function(req, res) {\n    res.send({\n        code:200,\n        message:\"succe3ss\"\n    })\n});\n\n\nconst juejin = require('./api/juejin');\napp.use('/api/juejin', juejin);\napp.use(\n  serveStatic(path.join(__dirname, 'public'), {\n    maxAge: cacheTime * 1000,\n  })\n);\nconst server = http.createServer(app);\n\nserver.listen(3000);\nmodule.exports = app;\n\n```\n\n\n\n\n\n### 1.2.1 controller层 \n\n\n\n我们在根路径新建一个api文件夹\n\n写入`juejin.ts`\n\n```ts\nconst {getJuejinInfo} = require('../model/Juejin');\nconst {renderJuejinCard} = require('../view/Juejin');\nconst { cacheTime, cache } = require('../common/cache');\n\n/**\n * @des 中间处理层 | 解析用户数据\n */\nexport = async (req, res) =\u003e {\n  const { id, theme, lang, raw } = req.query;\n  let key = 'j' + id;\n  let data = cache.get(key);\n  // if (!data) {\n  //   // 用来获取数据\n  //   data = await getJuejinInfo(id);\n  //   cache.set(key, data);\n  // }\n  data = {\n    user_name: 'username',\n    // 掘力值\n    power: 0,\n    // 关注人数\n    follower_count:0,\n    // 总浏览量\n    got_view_count:0,\n    // 总点赞量\n    got_digg_count:0,\n    description:\"简介\"\n  };\n  data.theme = theme;\n  if (raw) {\n    return res.json(data);\n  }\n  res.setHeader('Content-Type', 'image/svg+xml');\n  res.setHeader('Cache-Control', `public, max-age=${cacheTime}`);\n  return res.send(renderJuejinCard(data, lang));\n};\n\n```\n\n\n\n我们可以看到这里面入口主要是定义了两个 主要的逻辑\n\n- model层函数getJuejinInfo：主要用来做数据的获取\n- view层函数renderJuejinCard：主要用来做数据(svg)的渲染\n\n这两个东西在上文都有提到\n\n\n\n\n\n\n\n\n\n\n\n### 1.2.2  model层\n\n根目录下面新建model文件夹，新建juejin.ts\n\n我们在model 获取数据的方法主要有两种\n\n#### 1.2.2.1 axios 获取 json\n\n```ts\n这种方式是需要你提前通过抓包等方式获取 api 接口的地址。然后直接通过 axios 请求获取 请求体里面的json数据\n\nlet res = await axios.get(\n    `https://api.juejin.cn/user_api/v1/user/get?user_id=${id}`\n)\n\nresult = Object.assign({},result,res.data.data)\n```\n\n\n\n#### 1.2.2.2 axiosi获取dom,用cheerio进行解析\n\n第二种是cheerio，万一你不想通过抓包的方式获得api接口地址，你可以通过 axios 获取 整个 html 文件 然后用 cheerio 进行 html 的 解析，解析的规则跟css 的 规则一样\n\n```ts\n\n\n/**\n     * @des 第二种:解析html数据示例\n*/\nlet test =  await axios.get(\n    `https://juejin.cn/user/${id}`, {\n        \"Header\": {\n        }\n    }\n)\nlet $ = cheerio.load(test.data);\n    \n$('.username .user-name').each((i, e) =\u003e {\n\tresult.name = $(e).text();\n});\n\n```\n\n\n\n就是根据id 获取 html dom 中的 .username 和 .user-name 里面的 text 文本\n\n\n\n\n\n#### 1.2.2.3 解决ts数据显示不全 common/type.ts\n\n注意一下下方的ShowMe工具方法 如下\n\n```ts\n//  解决单层ts显示不全\nexport type ShowMe\u003cT\u003e = {\n    [K in keyof T]: T[K];\n} \u0026 {};\n```\n\n参考连接可以看：https://stackoverflow.com/questions/57683303/how-can-i-see-the-full-expanded-contract-of-a-typescript-type/57683652#57683652\n\n为了解决ts中数据类型 不显示全面的问题\n\n\n\n#### 1.2.2.4 示例代码\n\n最后model/juejin.ts  中写入\n\n```ts\nconst axios = require('axios');\nconst cheerio = require('cheerio');\nconst fs = require('fs');\nconst axiosConfig = require('../common/utils').mobileConfig;\nimport { ShowMe } from \"../common/type.js\"\n\n// type xx \u0026 {}\ntype JueJinReceiveType =  {\n  // [key : string ] : any;\n  user_name: string,\n  // 掘力值(yes)\n  power: number,\n  // 文章数(yes)\n  post_article_count: number,\n  // 总浏览量(yes)\n  got_view_count: number,\n  // 点赞(yes)\n  got_digg_count: number,\n  // 发布沸点\n  post_shortmsg_count: number,\n  // 默认添加theme\n  theme?:any\n} \n\n\n\nasync function getJuejinInfo(id) {\n  // 定义基本数据\n  let result:JueJinReceiveType = {\n    // 名字\n    user_name: 'username',\n    // 掘力值(yes)\n    power: 0,\n    // 文章数(yes)\n    post_article_count:0,\n    // 总浏览量(yes)\n    got_view_count:0,\n    // 点赞(yes)\n    got_digg_count:0,\n    // 发布沸点\n    post_shortmsg_count:1\n  };\n  \n  try {\n    // 两种数据 获取示例\n    /**\n     * @des 第一种:api示例\n     */\n    let res = await axios.get(\n      `https://api.juejin.cn/user_api/v1/user/get?user_id=${id}`\n    )\n\n    result = Object.assign({},result,res.data.data)\n\n    // 方便调试\n    fs.writeFile('./api_juejin.json', JSON.stringify(result), err =\u003e {\n      if (err) {\n        console.error(err);\n      }\n    });\n   \n  } catch (e) {\n    console.error(e);\n  }\n  return result as JueJinReceiveType;\n}\ntype JueJinReceiveTypeShow = ShowMe\u003cJueJinReceiveType\u003e\nexport {\n  getJuejinInfo,\n  JueJinReceiveTypeShow as JueJinReceiveType\n};\n\n\n```\n\n\n\n\n\n### 1.2.3 view层\n\n\n\n\n\nok 假如说你的数据已经准备好了，我们就可以开始渲染层的编写。这里我们会分成两个文件，第一个文件我们会对在model层的文件进行处理，第二个文件我们会对model层预处理的数据进行统一的渲染。\n\n我们要知道我们的数据卡片由哪几部分构成\n\n![服务端卡片解析](C:%5CUsers%5CAdmin%5CDesktop%5C%E6%9C%8D%E5%8A%A1%E7%AB%AF%E5%8D%A1%E7%89%87%E8%A7%A3%E6%9E%90.png)\n\n\n\n\n\n\n\n#### 1.2.3.1 数据格式\n\n\n\n我们可以看到这样一个简单的卡片我们可以分成3个领域进行渲染，渲染之前我们需要对数据进行处理，我们可以很简单列出 我们需要 进行组装代码的 格式\n\n\n\n```ts\ninterface RenderItemType {\n  type: \"title\" | \"textarea\" | \"shape\",\n  title: string,\n  text: any,\n  id?: string,\n  translate_y?: string | number,\n  icon?: IconKey\n}\n```\n\n\n\n在这之外我们还需要传入一个 theme来标识现在的 theme,这是black 下面的示例\n\n\n\n![服务端卡片解析 黑暗](C:%5CUsers%5CAdmin%5CDesktop%5C%E6%9C%8D%E5%8A%A1%E7%AB%AF%E5%8D%A1%E7%89%87%E8%A7%A3%E6%9E%90%20%E9%BB%91%E6%9A%97.png)\n\n\n\n我们总结一下目前需要传参的格式\n\n- theme\n- RenderItemType\n\n\n\n\n\n\n\n#### 1.2.2.2 数据处理\n\n\n\n\n\n##### 1.2.2.2.1 添加icon | common/icon.ts\n\n\n\nicon的格式我们采用的是svg，这里我采用的都是16*16的尺寸。更多的 svg可以去类似 https://www.iconfont.cn/ 的网站导出就好了。注意导出的fill 去掉，并且格式选择 16 * 16 .将 复制的 svg 代码 向下方一样进行排列 \n\n```ts\nenum icon  {\n    like=`\u003csvg \u003e\u003c/svg\u003e`,\n    another=`....你导入的svg`\n}\ntype IconKey = keyof typeof icon ; \nfunction getIcon(name:IconKey){\n\n    return icon[name]\n}\n\nexport {\n    getIcon,\n    IconKey\n}\n```\n\n\n\n上面的示例代码就是我们 在 common/icon.ts 中的 基本内存\n\n\n\n##### 1.2.2.2.2 添加多语言支持\n\n\n\n根路径下面新建 locale文件夹 ，写入juejin.ts\n\n```ts\n\ntype LocaleType = \"zh-CN\" | \"default\"\nfunction LocaleJuejin(key:LocaleType){\n    /**\n     * @common \n     * @articles : 文章数量\n     * @credits : 评论量\n     * @followers : 粉丝数量\n     * @likes : 获赞\n     * @views : 阅读量/访问量\n     * @description : 签名\n     * @favorites : 收藏\n     * @level : 等级\n     * @score : 分数\n     * @story : 动态\n     * \n     * @juejin特有的 \n     * @news : 发布沸点\n     * @score : 掘友分\n     * @story : 发布沸点\n     */\n    let locale = {\n        \"zh-CN\":{\n            favorites:\"收藏量\",\n            articles:\"文章数量\",\n            followers : \"粉丝数量\",\n            likes:\"获赞\",\n            views:\"阅读量\",\n            description:\"签名\",\n            credits:\"评论量\",\n            level:\"等级\",\n            score:\"掘友分\",\n            story:\"发布沸点\"\n        },\n        \"default\":{\n            favorites:\"favorites\",\n            articles:\"articles\",\n            followers : \"followers\",\n            likes:\"likes\",\n            views:\"views\",\n            description:\"description\",\n            credits:\"credits\",\n            level:\"level\",\n            score:\"score\",\n            story:\"story\"\n        }\n    }\n    return locale[key] ?? locale[\"default\"]\n}\n\n\nexport {\n    LocaleJuejin,\n    LocaleType\n}\n```\n\n\n\n这里就是简单的导入导出而已，没有什么好说的。\n\n\n\n##### 1.2.2.2.3 异常值处理\n\n这一块比较简单，就是数据过长的部分例如\n\n```ts\nxxxxxxxxxxxxxxxxxxxxxxxxx\n```\n\n变成\n\n```ts\nxx...\n```\n\n当然如果你还相对其他的数据进行处理也可以\n\n```ts\nlet lengthLimit = 14;\nif (description.length \u003e lengthLimit) {\n    description = description.substr(0, lengthLimit);\n    description += '...';\n  }\n```\n\n\n\n##### 1.2.2.2.4 组装\n\n这部分其实就没什么难的，只有一点比较麻烦，title(locale国际化的数据) 和  text (model层的数据)作为key和 value 对应起来有点麻烦其他的还好\n\n```ts\nimport { RenderCard  } from '../common/render';\nimport { LocaleJuejin,LocaleType  } from '../locale/juejin';\nimport { isEndsWithASCII, encodeHTML } from '../common/utils';\nimport {JueJinReceiveType} from \"../model/Juejin.js\"\nfunction renderJuejinCard(data:JueJinReceiveType, lang:LocaleType) {\n \n  let items = [];\n\n  if (isEndsWithASCII(data.user_name)) {\n    data.user_name += ' ';\n  }\n  let Locale = LocaleJuejin(lang)\n  items = [\n    {  title: `Electrolux ${Locale.MainTitle}`,  type: \"title\", color: \"blue\", icon: \"fire\" },\n    {  title: Locale?.articles, text: data.post_article_count, type: \"textarea\", color: \"\", icon: \"home\"},\n    { translate_y: 25, title: Locale.story, text: data.post_shortmsg_count, type: \"textarea\", color: \"\", icon: \"follower\"},\n    { translate_y: 50, title: Locale?.views, text: data.got_view_count, type: \"textarea\", color: \"\", icon: \"like\"},\n    { translate_y: 75, title: Locale?.likes, text: data.got_digg_count, type: \"textarea\", color: \"\", icon: \"fire\" },\n    { translate_y: 100, title: Locale?.score, text: data.power, type: \"textarea\", color: \"\", icon: \"fire\" },\n    {  text: 's', type: \"shape\", },\n  ];\n  return RenderCard(items, data.theme);\n}\n\nexport { renderJuejinCard };\n\n```\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n### 1.2.5 总结\n\n这一套组合拳下来，你的基础服务端卡片就可以按照这样的方式运行了，\n\n\n\n\n\n\n\n\n\n\n\n## 1.3 服务器部署\n\n\n\n那么我们最后怎么把我们的卡片放到服务器运行呢？这里我们采用 shell 脚本 + docker 的方式直接部署\n\n\n\n### 1.3.1 基本要求\n\n```shell\n# 首先当然是看看咱们的docker 有没有安装，没有安装的就安装一个，网上的教程很多，这里不赘述\ndocker -v\n```\n\n\n\n\n\n\n\n### 1.3.2 构建dockerfile 文件\n\n根目录中新建 Dockerfile文件，然后写入 \n\n```dockerfile\n# 使用基于 Node.js 的官方镜像作为基础\nFROM node:14\n\n# 设置工作目录\nWORKDIR /app/stats\n\n# 将项目文件复制到工作目录\nCOPY . /app/stats\n\n# 执行 npm install 安装项目依赖\nRUN npm install\n\n# 暴露容器的 3000 端口\nEXPOSE 3000\n\n# 运行 node app.js 命令启动应用\nCMD [\"node\", \"app.js\"]\n\n```\n\n注释比较清晰，这里不做过多描述\n\n\n\n\n\n### 1.3.3 写脚本前面的准备\n\n#### 1.3.3.1 在coding 的主机中\n\n我们首先思考一下咱们的docker应该怎么推送到服务器呢？在这里我们采用最原始的方法，通过docker hub 推送上去，然后连接到我们的 服务器把 镜像拉下来。因此这里我们需要首先注册docker hub 账号\n\n我们可以先构建一下 docker 产物 \n\n- 注意一下，这里的 docker_hub_username 是 你的 docker 用户名  。我的用户名 是 `electroluxcode`\n- docker_image_name 是你本地仓库的名字\n- docker_image_version 是你的 tag\n\n```shell\ndocker build  --platform linux/amd64 -t electroluxcode/docker_image_name:docker_image_version  . \n```\n\n\n\n来进行**docker的构建**。构建完成后你可以运行 \n\n```shell\ndocker images \n```\n\n就应该能看到 **docker的images** ，会有下面一样的输出\n\n```shell\nREPOSITORY                              TAG                    IMAGE ID       CREATED              SIZE\nelectroluxcode/docker_image_name   docker_image_version   1412b3607676   About a minute ago   1.08GB\n```\n\n\n\n我们试着把这个**docker image跑起来** ，运行docker run\n\n```shell\ndocker run -d --name firstcontainer -p 81:3000 docker_hub_username/docker_image_name:docker_image_version\n```\n\n\n\n如果发现 docker ps 没有输出 container,这个时候我们可以 运行下面指令**查看在 docker 内部的 log**\n\n```shell\ndocker logs -f -t --since=\"2019-08-09\" firstcontainer\n```\n\n\n\n如果没有报错那么我们就可以接下来的步骤。我们现在在我们coding的主机上面已经打包成镜像了，那么\n\n推上去的命令是\n\n```shell\ndocker push 你docker hub的用户名/docker_image_name:docker_image_version\n```\n\n\n\n```\ndocker push electroluxcode/docker_image_name:docker_image_version\n```\n\n\n\n\n\n#### 1.3.3.2 在服务器\n\n- 这里的 mycontainer 是 我们 想 运行的 容器 名字，可以支持 自定义\n- 81 是 真正向外面 暴露的端口 ，3000是容器内部的端口\n\n```ts\ndocker pull electroluxcode/docker_image_name:docker_image_version\ndocker run -d --name mycontainer -p 81:3000 electroluxcode/docker_image_name:docker_image_version\n```\n\n\n\n\n\n\n\n\n\n#### 1.3.3.3 总结\n\n\n\n好的，总结一下我们关于docker的基础命令\n\n```shell\n# 打包\ndocker build  --platform linux/amd64  -t docker_hub_username/docker_image_name:docker_image_version  .\n# 运行 \ndocker run -d --name firstcontainer -p 81:3000 docker_hub_username/docker_image_name:docker_image_version\n# 如果有报错\ndocker logs -f -t --since=\"2019-08-09\" firstcontainer\n# 没有报错就可以推送 \ndocker push 你docker hub的用户名/docker_image_name:docker_image_version\n# 服务器中\ndocker pull electroluxcode/docker_image_name:docker_image_version\ndocker run -d --name mycontainer -p 81:3000 electroluxcode/docker_image_name:docker_image_version\n```\n\n\n\n\n\n我们再加上一点linux脚本的基础知识，我们可以轻松组织一个脚本\n\n```shell\n#!/bin/bash\n\n# step1：初始化变量\nstart_time=$(date +%s) # 记录脚本开始时间\ndocker_image_version=${1:-1.1} # Docker镜像版本\ndocker_image_name=${2:-behind_image} # Docker 仓库 image 名字\ndocker_hub_username=${3:-electroluxcode} # Docker Hub 用户名\ncontainer_name=${4:-behind_container} # 容器名字\ncontainer_expose_port=${5:-3000} # 容器暴露的端口\n\nserver_username=${6:-ubuntu} # Linux服务器用户名\nserver_address=${7:-62.234.180.224} # Linux服务器地址\n\n\n\n# step2：没登陆首先登录一下\n# docker login -u 3451613934@qq.com -p \n# yarn build\n\n# step3： 注意这个 image 应该要提前构建\n# -t 指定构建镜像的名字和版本 \necho -e \"\\e[91m --本地build中--\"\ndocker build  --platform linux/amd64 -t $docker_hub_username/$docker_image_name:$docker_image_version  . \ndocker push $docker_hub_username/$docker_image_name:$docker_image_version\necho -e \"\\e[0m\"\n\n\necho -e \"\\e[92m --服务器部署中--\"\n# step4:这一步可以 ssh-copy-id 去除登录\nssh $server_username@$server_address \u003c\u003c EOF\nsudo -s\ndocker ps\ndocker stop $container_name\ndocker rm $container_name\ndocker rmi $(docker images -q)\n\ndocker images\ndocker pull $docker_hub_username/$docker_image_name:$docker_image_version\nif [ $? -ne 0 ]; then\n  echo \"Docker镜像拉取失败,脚本被终止.\"\n  exit 1\nfi\ndocker images\n\n# docker run -d --name bimddp_container -p 81:80 electroluxcode/bimddp_image:1.1\ndocker run -d --name $container_name -p $container_expose_port:3000 $docker_hub_username/$docker_image_name:$docker_image_version\ndocker ps\ndocker images\nexit\n\nEOF\n\necho -e \"\\e[0m\"\n# docker images |  docker rmi  id id | docker rmi $(docker images -q)\n# 计算整个脚本的用时\nend_time=$(date +%s)\nduration=$((end_time - start_time))\nformatted_duration=$(date -u -d @\"$duration\" +\"%H:%M:%S\")\n\necho \"✨🎉🎈恭喜您, 部署成功!\"\necho \"整个过程花费 $formatted_duration\"\n```\n\n\n\n\n\n\n\n## 1.4 vercel 部署示例\n\nhttps://vercel.com/new   注册一下，这个需要有的时候需要翻墙，各位看一下\n\n\n\n```shell\nnpm i vercel -g\nvercel login # 登录一下账号\n\n```\n\n\n\n根目录下面新建 vercel.json ，写入如下代码。\n\n```json\n{\n  \"version\": 2,\n  \"name\": \"data-card\",\n  \"builds\": [\n    {\n      \"src\": \"dist/app.js\",\n      \"use\": \"@vercel/node\"\n    }\n  ],\n  \"routes\": [\n    {\n      \"src\": \"/(.*)\",\n      \"dest\": \"dist/app.js\"\n    }\n  ]\n}\n```\n\n\n\n简单解释一下\n\n- dist/app.js 是入口文件\n- routes 下面是将匹配的路径进行重写\n\n\n\n\n\n然后 运行 npm install @vercel/node 安装一下依赖。最后执行\n\n```shell\n# 在你的项目下面执行,如果没有报错。并且地址能够访问直接推送就好了\nvercel dev  \n# 推送\nvercel \n```\n\n\n\n\n\n\n\n\n\n ","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felectroluxcode%2Fdata-card","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Felectroluxcode%2Fdata-card","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Felectroluxcode%2Fdata-card/lists"}