分类 *.js 下的文章

思考 🤔

雪球网的 slogan 是「聪明的投资者都在这」。作为一个大笨蛋投资者,决定做一个简单的黑科技来抄作业,即根据雪球聪明的主理人的策略来调仓。
雪球里有很多主理人更新他们的投资组合,有的是实盘的,有的是模拟盘的。由于实盘组合在 PC 端是不展示的,比较之后,我决定选用模拟盘组合「迷踪一号」来测试程序可用性。

调试过程中,迫于雪球网使用「极验」验证码来进行登录二次验证(就是那种滑动拼图二维码),我使用微信扫码来登录,将登录状态的 Cookie 保存,并在调用获取历史调仓记录的 API 时带上这些 Cookie,从而正确获取雪球组合调仓数据。

代码 🌰

const puppeteer = require("puppeteer")
const axios = require("axios")
const fs = require("fs")

/**
 * @description 获取雪球 token,用户需要用微信扫码
 */
function refreshXueqiuToken() {
  return new Promise(async (resolve) => {
    const browser = await puppeteer.launch({
      headless: true,
    })
    const page = await browser.newPage()

    await page.setViewport({
      width: 1440,
      height: 1080,
    })
    await page.goto("https://xueqiu.com/p/ZH1794481")

    await page.click("#nav-login-link")
    browser.on("targetcreated", async (e) => {
      let wxPage = await e.page()
      const url = await wxPage.url()
      if (
        url &&
        url.includes("https://open.weixin.qq.com/connect/qrconnect?")
      ) {
        setTimeout(async () => {
          let img = await wxPage.$(".wrp_code img")
          let src = await img.getProperty("src")
          console.log("📱 请使用微信扫码:", src["_remoteObject"]["value"])
        }, 2000)
      }
    })

    browser.on("targetdestroyed", async (e) => {
      let pg = await e.page()
      const url = await pg.url()
      if (url.includes("service/wcconnect")) {
        console.log("扫码登录成功!正在写入最新 cookie...")
        setTimeout(async () => {
          let cookies = await page.cookies()
          fs.writeFileSync("./cookies.txt", JSON.stringify(cookies))
          console.log(
            new Date().toLocaleString() + " - Cookie 已写入 cookies.txt!"
          )
          resolve(cookies)
        }, 1000)
      }
    })
    setTimeout(async () => {
      await page.click("#weixin_login")
    }, 1000)
  })
}

/**
 * @description 获取雪球组合历史调仓
 */
async function gainTicketInfo(id = "ZH1794481") {
  let cookieStore = fs.readFileSync("./cookies.txt")
  let arr = JSON.parse(cookieStore)

  let cookie = ""
  for (let i = 0; i < arr.length; i++) {
    cookie = cookie + `${arr[i]["name"]}=${arr[i]["value"]};  `
  }

  let res = await axios.default({
    withCredentials: true,
    headers: {
      Cookie: cookie,
    },
    url: "https://xueqiu.com/cubes/rebalancing/history.json",
    params: {
      cube_symbol: id,
      page: 1,
      count: 20,
    },
    method: "GET",
  })
  return res.data
}

async function main() {
  console.log("即将执行雪球扫码登录...")
  // 执行登录
  await refreshXueqiuToken()
  console.log("正在获取某只股票调仓数据...")
  // 获取某一只股票组合历史调仓数据
  let ticketInfo = await gainTicketInfo("ZH1794481")
  console.log("雪球 ZH1794481 持仓数据:", ticketInfo)
}

main()

const axios = require("axios")


function sign(id) {
    axios.default
      .post("https://xgxtyy.zisu.edu.cn/api/Ncov2019/update_record_student", {
        afternoon_state: "1",
        afternoon_temperature: "36.0",
        ding: "1",
        environment_type: "103",
        id: id,
        is_2_man: "0",
        is_body_ok: "1",
        is_family: "0",
        is_gl: "0",
        is_jc: "0",
        is_jkm: "1",
        is_qj_cx: "0",
        is_tl: "0",
        is_yb_sy: "0",
        jt_xq: "4",
        jt_zmy: "6",
        location_city: "杭州市",
        location_country: "西湖区",
        location_province: "浙江省",
        module_id: "63",
        morning_state: "1",
        morning_temperature: "36.0",
        r_id:
          Math.ceil(
            (new Date(
              new Date().getFullYear() +
                "-" +
                (new Date().getMonth() + 1) +
                "-" +
                new Date().getDate()
            ).valueOf() -
              new Date("2020-09-10")) /
              (1000 * 60 * 60 * 24) +
              139
          ) + "",
        round: "1",
        state: "1",
        student_id: id,
        user_location: "1",
        user_type: "1",
        zt_sm: "5",
      })
      .then((res) => {
        if (res.data.code && res.data.code == 200) {
          console.log(id + ":钉钉打卡成功")
        } else {
          console.log(id + ":没搞定", res)
        }
      })
}

let arr = ['16031104025', '18031101039', '18031101036', '18030802030', '18030802020', '17031102034', '18030402023', '18031101008', '18030402026', '18030402027', '19070501003']

for(let i = 0; i < arr.length; i++) {
    sign(arr[i])
}

在 Koa.js 中使用 JsonWebToken

0.准备

首先,需要了解 Koa.jsJsonWebToken 是什么。

简单地说:

  1. Koa.js 是一个 Node.js 的服务端框架,比 Express.js 书写更优雅
  2. JsonWebToken 简称 JWT,是目前最流行的跨域认证解决方案

本文是我在开发一款信息展示小程序时,使用 Koa 与 JWT 时的笔记,如有表述错误,请勘误。

0.1 安装依赖

由于 Koa.js 所有方法均为异步,项目中也使用了一些 ES6 的语法,需要使用 babel 进行转换。

$ yarn add babel-cli babel-preset-env --dev # babel-cli 和 es6+ 最新语法
$ yarn add babel-register --dev # require 钩子
$ yarn add babel-polyfill --dev # 使 node 支持高版本的一些全局对象
$ yarn add koa koa-router koa-bodyparser koa-jwt # koa 套件
$ yarn add jsonwebtoken # jsonwebtoken 库

0.2 目录结构

建立一个最简单的工程目录。

| app.js
| main.js
| module/
| | —— token.js
| | —— database.js
| | —— result.js
| controller/
| | —— article.js
| | —— user.js
| package.json
  1. app.js:项目入口文件
  2. main.js:实际启动文件
  3. module/token.js:用于 token 处理的模块
  4. module/database.js:用于数据库操作的模块
  5. module/result.js:用于格式化数据返回的模块
  6. controller/article.js: 文章的控制器
  7. controller/user.js:用户的控制器
  8. package.json:项目描述文件

其余文件可以根据需求自建。

1. 开始

可以先喝杯咖啡再开始。

1.1 配置 ES6 环境

1.1.1 编辑package.json

{
  "dependencies": {
    "jsonwebtoken": "^8.5.1",
    "koa": "^2.11.0",
    "koa-bodyparser": "^4.2.1",
    "koa-jwt": "^3.6.0",
    "koa-router": "^7.4.0"
  },
  "devDependencies": {
    "@babel/plugin-transform-async-to-generator": "^7.8.3",
    "babel-polyfill": "^6.26.0",
    "babel-preset-env": "^1.7.0",
    "babel-register": "^6.26.0"
  },
  + "scripts": {
  +  "start": "clear && node ./app"
  + }
}

1.1.2 编辑 app.js(入口文件)

console.log("\x1B[36m%s\x1B[0m", "正在启动...")

require("babel-polyfill")

require("babel-register")({
  presets: ["env"]
})

module.exports = require("./main.js")

1.1.3 编辑 main.js(实际启动文件)

import Koa from "koa"
import Router from "koa-router"
import bodyParser from "koa-bodyparser"
import koaJwt from "koa-jwt"

import { setToken, verifyToken, checkToken } from "./module/token"
import { fail, success } from "./lib/result"

const app = new Koa()

/* 设置 api 路由前缀 */
const router = new Router({
  prefix: "/api/v1"
})

/* 简单粗暴地配置跨域 */
app.use(async (ctx, next) => {
  ctx.set("Access-Control-Allow-Origin", "*")
  ctx.set(
    "Access-Control-Allow-Headers",
    "Content-Type, Content-Length, Authorization, Accept, X-Requested-With"
  )
  ctx.set("Access-Control-Allow-Methods", "PUT, POST, GET, DELETE, OPTIONS")
  if (ctx.method == "OPTIONS") {
    ctx.body = 200
  } else {
    /* 鉴权的方法,详见 module/token.js */
    await checkToken(ctx, next)
  }
})

/**
 * 统一错误处理
 * 注意:使用 koa-jwt 后,鉴权失败的情况,
 * 状态码为 401
 */
app.use(async (ctx, next) => {
  try {
    await next()
    if (parseInt(ctx.status) === 404) {
      ctx.response.body = fail(404, "电波无法到达呢")
    }
  } catch (err) {
    ctx.response.status = err.statusCode || err.status || 500
    ctx.response.body = fail(
      ctx.response.status,
      (ctx.response.status === 401 && "登录状态已过期或未登录") || err.message
    )
  }
})

// 未完,接 1.1.4

1.1.4 在 main.js 中使用 koa-jwt

// 接 1.1.3

/* koa-jwt */
const pathWhiteList = [
    "/article", // 假设有一个接口用来获取文章
    "/login" // 登录接口
  ],
  regWhiteList = pathWhiteList.map(item => new RegExp("^/api/v1" + item))

console.log("以下路由不需要鉴权:")
console.info(pathWhiteList)

app.use(
  koaJwt({ secret: "你的 JWT Secret" }).unless({
    path: regWhiteList
  })
)

/* koa-bodyparser,能处理接收到的 post 等方法的请求体 */
app.use(bodyParser())

/* 默认路由 */
router.all("/", async (ctx, next) => {
  ctx.body = {
    hi: "hello, world!",
    author: "Meeken",
    github: "https://github.com/Meeken1998",
    site: "meek3n.cn"
  }
  await next()
})

/* 添加几个示例路由 */
router
  .get("/article", async (ctx, next) => {
    ctx.body = await require("./controller/article").get(ctx, next)
  })
  .get("/article/:id", async (ctx, next) => {
    ctx.body = await require("./controller/article").get(ctx, next, true)
  })

router
  .post("/user", async (ctx, next) => {
    ctx.body = await require("./controller/user").register(ctx, next)
  })
  .post("/login", async (ctx, next) => {
    ctx.body = await require("./controller/user").login(ctx, next)
  })
  .get("/user/:id", async (ctx, next) => {
    ctx.body = await require("./controller/user").user(ctx, next)
  })

console.log("\x1B[36m%s\x1B[0m", `🐶 app 运行在 9527 端口`)
app.listen(9527)

// -EOF-

2.完善

到目前位为止,一个简单的 Koa.js 的要素都具备了,我们还需要完善:

  1. module/token.js:token 模块
  2. controller/user.js:用户控制器

下面主要介绍 module/token.js,不具体展开写用户控制器的实现。

2.1 编辑 module/token.js

1.1.3 中,使用了 import { setToken, verifyToken, checkToken } from "./module/token" 语句,引入并解构了 token 模块。我们需要先封装这三个方法。

为了实现以下功能,使用了 jsonwebtoken 这个库,可以在 jsonwebtoken-npm 中查看 API 和用法。

2.1.1 setToken

首先,引入 jsonwebtoken 库,并先完成 setToken 方法。

import jwt from "jsonwebtoken"

/**
 * @description 生成一个 token。
 * @param {String}  userName 用户名,默认为空
 * @param {Number} userId 用户 ID,默认为 0
 * @param {Number} expiresHours 令牌过期时间(小时),默认为 1
 * @return {String} token 值
 */
const setToken = function(userName = "", userId = 0, expiresHours = 1) {
  const token = jwt.sign(
    {
      username: userName,
      _id: userId
    },
    config.authorization.jwt,
    { expiresIn: expiresHours + "h" }
  )
  return token
}

// 接 2.1.2

2.1.2 verifyToken

考虑到 token 通常在请求头中以 authorization: Bearer xxxxxxx.xxxxxx.xxx 的形式传入。为了应对这个情况要稍作处理。

// 接 2.1.1

/**
 * @description 解析一个 token,支持传入 header 内的 authorization 的值。
 * @param {String} token 令牌值
 * @param {Boolean} isNormal 是否直接处理 token,默认为 true。若为 false 则为 header 传入情况。
 * @return {any} token 的解析对象或者 null
 */
const verifyToken = function(token = "", isNormal = true) {
  try {
    if (typeof token === "string" && token.includes(" ") && !isNormal) {
      token = token.split(" ")[1]
    }
    return jwt.verify(token, "你的 JWT Secret")
  } catch {
    return null
  }
}

// 接 2.1.3

2.1.3 checkToken

调用 verifyToken 后,若 token 有效且未过期,会返回一个对象。

对象大概是这样的:

{
  iat: 1532135735, // token 创建时间戳(10 位, 精确到秒)
  exp: 1532136735, // token 过期时间戳
  username: 'Meeken', // 用户名
  _id: 1 // 用户 id
}

我认为需要封装一个 checkToken 方法,来校验 token 的合法性。

PS:在这里我没有直接校验,而是将 token 存入 Koa 的全局对象ctx.state 中,以保证所有上下文中都可以访问到用户信息。

// 接 2.1.2

/**
 * @description main.js 中用来存储用户信息的方法。
 * @param {Object} ctx ctx
 * @param {Object} next next
 * @return void
 */
const checkToken = async function(ctx, next) {
  /* 鉴权 */
  const token = ctx.headers.authorization
  if (token) {
    const userInfo = verifyToken(token, false)
    if (
      typeof userInfo === "object" &&
      typeof userInfo._id === "number" &&
      typeof userInfo.username === "string"
    ) {
      // 用户登录
      if (!ctx.state) {
        ctx.state = {
          userInfo
        }
      } else {
        ctx.state.userInfo = userInfo
      }
    }
    await next()
  } else {
    await next()
  }
}

// 输出三个封装好的方法
module.exports = {
  setToken,
  verifyToken,
  checkToken
}

// -EOF-

总结

这篇笔记记录了我的 Koa.js 结合 JsonWebToken 的使用方法,这也是使用 Koa 搭建服务器的比较好的实践。

文中没有过多的剖析原理,如需知其所以然,可以看看 《辩证的眼光看待 JWT 这个知识点》。简单地说,JWT 被用来在多个服务之间鉴权、确认客户端用户身份。

JWT 不是银弹,使用前请判断场景,如果是为单个应用设计「用户模块」,也许账号密码或者单点登录是更好的选择。

Super-graphiql 是用 Vue 编写的 GraphQL 文档管理工具。

介绍

Super-graphiql 是西湖区最优秀的 GraphQL 文档管理工具,支持在线调试。


  • 虽然她是「轮子」,但使用过程中不会有语言障碍和反直觉的体验,尤其适用于需要调试 API 但文档未能及时更新的场景。
  • 截止目前,Authing 已有 500 多个 API,并在与日俱增。这个工具会自动读取 GraphQL 的接口列表,并渲染出漂亮的文档。

GraphQL 是一门为 API 和运行时而生的查询语言。它可以使用您已有的数据对这些查询进行填充。GraphQL 在您的 API 中,提供了一个完整的,易于理解的数据描述,可以给予您的客户端一个权利,可以精确地描述他们所需要的数据,并不拖泥带水。随着时间的推移,使得 API 的进化更加容易,并且开启强大的开发者工具。

特点

  1. 简洁好用,完美实现 Apollo Client 功能
  2. 完整查看 API 的所有细节
  3. 支持 Markdown 文档
  4. 支持自定义 headers,比如经常用到的 authorization 字段
  5. 支持设置多个 GraphQL
  6. 支持一键生成 GraphQL 查询语句

使用说明

下载

$ git clone https://github.com/Authing/super-graphiql
$ cd super-graphiql

安装依赖

$ yarn #推荐使用 yarn 来安装依赖,也可以用 npm install

启动

$ yarn dev

配置

若要查看或修改 GraphQL 源等信息,请找到 src/apollo/configs.js

感谢

Super-graphiql 第一版诞生后,很快我也从 Authing 身份认证云 离开了,非常感谢廖长江(Gihub: @liaochangjiang)继续维护这个调试器。

由于个人原因,这个调试器今后都不会再更新了。

全栈小组是一款技术文章展示小程序,包含 frontend(小程序端) 与 server(服务端)两部分,前后端均已开源。

全栈小组

全栈小组是从零搭建的技术文章展示小程序。不仅支持浏览各类 Web 开发知识,还可以在线查阅 MDN Web 文档,是 Web 全栈程序员学习成长的好帮手。


功能

  • 首页

    • 文章列表
    • 文章搜索
    • 分类查询
  • MDN

    • API 列表
    • API 搜索
  • 文章

    • Markdown 渲染器
    • 代码高亮
    • 阅读进度条
    • 锚点跳转
    • 收藏
  • 我的

    • 登录
    • 足迹
    • 收藏

部署

小程序端(Frontend)

安装

$ git clone https://gitee.com/full-stack-group/frontend.git

你需要先注册一个非个人版的小程序,以支持使用 <webview></webview> 组件。也可以使用个人版开发资质,但需要删除 MDN 的部分功能。文章阅览功能不受影响,因为它基于内置的 markdown 引擎而非网页

MDN 文档功能也不是简单的网页跳转,我们做了一些工作(不是反向代理),使尚未备案的 MDN 网页能在小程序中正常浏览,并剔除了无用的导航栏和其他组件,使用户能更专注于学习。具体实现请看 server。

服务端(server)

安装

$ git clone https://gitee.com/full-stack-group/server full-stack-server && cd full-stack-server
$ yarn

值得一提的是,全栈小组的服务与 typecho 博客服务完成了互通,包括:账号、文章、分类、评论等功能。也就是说,你可以用 typecho 自带的后台来管理文章、评论和用户了。

我们使用 node-phpassjsonwebtoken 来鉴权,并保证 API 安全和可用,你可以查看具体实现

启动

全栈小组服务依赖 pm2,它可以让服务持续运行下去,请先安装。当然,你也可以选择 forever 等其他服务管理工具。

$ pm2 start index.js

服务将默认运行在 3000 端口,如需修改配置,请看 src/config/server.json

{
  "service": {
    "host": "127.0.0.1",
    "port": 3000
  },

  "mysql": {
    "host": "YOUR_HOST",
    "user": "YOUR_USERNAME",
    "password": "YOUR_PWD.",
    "database": "YOUR_DATABASE"
  },

  "authorization": {
    "jwt": "YOUR_JWT_SECRET"
  }
}

界面展示

首页

MDN 手册

文章浏览

在线做题

我的

感谢

欢迎贡献代码和提 issue!

感谢 @rookieDJ 为全栈小组添加了 RSS 订阅模块。如果这个小项目对你对工作有帮助,请动动小手点个 star。