机场推荐大全

unarto/share-ssr-v2ray-ss-akun - https://github.com/unarto/share-ssr-v2ray-ss-akun

hwanz/SSR-V2ray-Trojan-vpn: 免费白嫖公益机场合集SSR-V2ray/免费vpn-定时更新 - https://github.com/hwanz/SSR-V2ray-Trojan-vpn

最新SSR/V2ray专线机场推荐与评测(2022.03更新) - ssrv2ray - https://hwzhwz.github.io/posts/8c826425.html

jichangjiedian/README.md at c4bef7829bf424b012ee5debc3e04966aed43bd0 · sqlemma/jichangjiedian - https://github.com/sqlemma/jichangjiedian/blob/c4bef7829bf424b012ee5debc3e04966aed43bd0/README.md

free-ssr-v2ray/README.md at 2fd01d5967fd7d22131f55155cf6d8d3b86d16d9 · honven/free-ssr-v2ray - https://github.com/honven/free-ssr-v2ray/blob/2fd01d5967fd7d22131f55155cf6d8d3b86d16d9/README.md

jichangjiedian/README.md at main · sqlemma/jichangjiedian - https://github.com/sqlemma/jichangjiedian/blob/main/README.md

vscode 秒变 全功能 所见即所得 markdown 编辑器

最近写了一些 markdown, 在 vscode 上感觉一般的纯文本文档写起来还是比较舒服的,但是如果有表格编辑起来就比较麻烦了,排版很困难,于是想起了最近很多基于 electron/nwjs 的所见即所得的 markdown 编辑器,比如 typora, marktext 等等, 如果在 vscode 中实现类似功能编辑起来就很方便了。

于是便写了这个插件:

插件地址: https://marketplace.visualstudio.com/items?itemName=zaaack.markdown-editor

源码地址: https://github.com/zaaack/vscode-markdown-editor

基于强大 vditor 项目,vscode 端并不需要多少代码即可拥有众多 feature:

  • 可视化编辑 markdown
  • 图片上传 /粘贴
  • 黑暗模式, 多种代码高亮样式
  • 大纲、数学公式、脑图、图表、流程图、甘特图、时序图、五线谱、多媒体、语音阅读、标题锚点、代码高亮及复制、graphviz 渲染、plantumlUML 图

macOS

效率

项目 简介
utools 启动器,Alfred 替代品
PxCook sketch 预览工具,除此还有Sketch Measure, zeplin,蓝湖…
httppie http client 命令行工具
softmaker-office mac上启动最快的 office 套件
LICEcap mac 上压缩率最高的gif录屏软件
稻壳阅读器 高性能 pdf/epub 阅读器
magicanrest 定时锁屏休息
坚果云网盘 文件同步
sqltab 免费聚合数据库客户端
deskscreen 把手机/pad 等设备变成扩展屏幕(跨平台)
espanso 跨平台的系统全局snippets,文本快捷输入,支持 emoji
tinycal 小历日历

win10/11

项目 简介
lunacy win10打开编辑sketch文件
WSLHostPatcher wsl2 服务在局域网可访问
openhardware/coretemp cpu温度监控
mouseinc 鼠标手势+触发角
stretchly 20分钟锁屏休息20s

跨平台

项目 简介
local-cors-proxy/npm 本地反向代理服务添加 cors header
caddy go版 反向代理服务器
NeatDownloadManager 多线程下载工具

Android

项目 简介
Postern 代理切换

web

效率

项目 简介
excalidraw 手写样式的流程图
perfect-freehand 毛笔画图
draw.io 在线流程图
figma 在线原型图, 可以导出svg
alternativeto.com 替代品查找网站
asciiflow 基于文本的流程图
tool.lu 程序员工具箱
在线接码 临时手机号用于注册小号
photopea 在线ps, 支持导入 sketch 预览,不过只能看图
tailscale.com 免费vpn组网,支持远程访问内网机器
ss 机场推荐

资源

项目 简介
bookzz 电子书下载
bookrix 电子书下载
z-library zlibrary电子书

开发

项目 简介
gitpod.io 在线开发环境, db/IDE/shell 等 (开源免费)
poshover 推送服务
微信绑定qq邮箱发邮件 同上
Server 酱/喵提醒/Telegram 订阅/钉钉机器人 /PushPlus/gotify/ddpush 同上
vercel/netlify 云服务serverless
小米球 钉钉内网穿透 nat123 内网穿透
ghproxy/toolwa github下载加速
zellij tmux 替代品,界面更友好,对鼠标支持更好

娱乐

项目 简介
vip视频解析
网盘聚合搜索
v视频下载
downsub 下载YouTube字幕
https://www.youtubemy.com (网友自荐,还不错),可用 youtube 视频下载
https://pickvideo.net/(被河蟹,需要爬梯子) -》 同上
https://bitdownloader.com/en2 同上
http://www.clipconverter.cc/ ( 同上
http://fetchvideo.com/ (正常) 同上
mkvtoolnix 最快的字幕封装工具,将字幕导入到视频
ZY-Player 全网视频客户端

svg 文本自动换行 React 组件

SVG 的 text 标签是不支持自动换行的,但是如果想用 svg 作为一个画图模版还是需要文本换行。这个组件可以自动对 svg 节点进行截断,支持 tspan 单独设置样式,比如改变颜色啥的,但是暂时不支持 tspan 改变文本宽度,因为用的是别人写的测量宽度的包。本来这个作者也有个换行组件的,但是看源码发现不支持中文,所以写了这个组件。

用法:



<AutoText
    width={50 / 375 * window.innerWidth} // width should be 'px'
    x={47} // x, y can be svg coordinate
    y={337}
    fill="#494949"
    fontFamily="SourceHanSerif-Regular, Source Han Serif"
    fontSize={13}
    fontWeight="normal"
    lineSpacing={30} // or line-height
    spans={[
        'Helllo,',
        { tag: 'tspan', text: 'Amy', props: { fill: '#B09148' } },
        ', ',
        'what can I do for you?',
    ]}
/>

import 'core-js/fn/set'
import 'core-js/fn/map'

import React from 'react'
import svgTextSize from 'svg-text-size';


const rightSymbols = new Set(
  '“‘[「【﹝〔<‹«{『((<《'.split('')
)
const leftSymbols = new Set(
  ',,,!!??”\]」】》>’»﹞〕〗〉}))』'.split('')
)

const toggleSymbols = new Map([
  ['\'', 'right'],
  ['"', 'right'],
])

// Takes a string, and a width (and svg attrs, if they apply), and returns
// an array of lines, representing the break points in the string.
const textWrap = (text, width, attrs, doc = document, {
  _leftSymbols = leftSymbols,
  _rightSymbols = rightSymbols,
  _toggleSymbols = toggleSymbols,
}) => {
  let words = []
  let toggleMap = new Map()
  for (let i = 0; i < text.length; i++) {
    const char = text[i];
    if (_rightSymbols.has(char)) {
      words.push(char + text[i + 1])
      i += 1
    } else if (_leftSymbols.has(char)) {
      words[words.length - 1] += char
    } else if (_toggleSymbols.has(char)) {
      let isRightStart = _toggleSymbols.get(char) === 'right'
      if (!toggleMap.has(char)) {
        toggleMap.set(char, isRightStart ? 'right' : 'left')
      }
      let stickyDirection = toggleMap.get(char)
      if (stickyDirection === 'right') {
        words.push(char + text[i + 1])
        i += 1
      } else {
        words[words.length - 1] += char
      }
      toggleMap.set(char, stickyDirection === 'left' ? 'right' : 'left')
    } else if (/^\w+$/.test(char)) {
      let last = words[words.length - 1]
      if (/^\w+$/.test(last)) {
        words[words.length - 1] += char
      } else {
        words.push(char)
      }
    } else {
      words.push(char)
    }
  }
  let lines = [];
  let currentLine = [];
  words.forEach(word => {
    const newLine = [...currentLine, word];
    const size = svgTextSize(newLine.join(''), attrs, doc);
    if (size.width > width) {
      lines.push(currentLine.join(''));
      currentLine = [word];
    } else {
      currentLine.push(word);
    }
  });
  lines.push(currentLine.join(''));
  if (lines[0] === '') { lines.shift(); }
  return lines;
};

function getReactAttr(attrs) {
  let newAttrs = {}
  for (let key in attrs) {
    let val = attrs[key]
    key = key.replace(/([A-Z])/g, s => '-' + s.toLowerCase())
    switch (key) {
      case 'font-size':
      case 'line-spacing':
      case 'letter-spacing':
        if (typeof val === 'number') {
          val += 'px'
        }
        break;
      default:
        break;
    }
    newAttrs[key] = val
  }
  return newAttrs
}

/**
\```
props: {
 spans: [
   "some text",
   { tag: 'tspan', text: 'some text', props: { } }
 ],
 width: 100,
 x: 0,
 y: 0,
}
\```
 */
export default class SVGAutoText extends React.PureComponent {

  getAttrProps() {
    const {
      spans, width, x, y,
      leftSymbols, rightSymbols, toggleSymbols,
      ...props } = this.props
    return props
  }

  getWrappedSpans() {
    let spanSlices = []
    let startIdx = 0
    let fullText = ''
    let spans = this.props.spans.map(
      span => {
        if (!span.tag) {
          return {
            tag: 'tspan',
            text: span ? span + '' : '',
            props: null,
          }
        }
        span.text += ''
        return span
      }
    )
    for (const span of spans) {
      let end = startIdx + span.text.length
      spanSlices.push({
        ...span,
        start: startIdx,
        end,
      })
      startIdx = end
      fullText += span.text
    }
    let globalProps = this.getAttrProps()
    let lines = textWrap(fullText, this.props.width, getReactAttr(globalProps), this.props)
    let tspans = []
    let lineIdx = 0
    let lineLen = 0
    let sliceIdx = 0
    let sliceLen = 0

    let lastLine = -1
    let { x: offsetX = 0, y: offsetY = 0 } = this.props
    let lineHeight = parseInt(globalProps['lineSpacing'] || globalProps['lineHeight'], 10)
    let fontSize = parseInt(globalProps['fontSize'], 10) || 16
    function makeTspan(text, props, lineIdx) {
      let x = lastLine === lineIdx ? void 0 : offsetX
      lastLine = lineIdx
      return (
        <tspan
          key={tspans.length + text}
          {...props}
          x={x}
          y={lineIdx * (lineHeight ? lineHeight : fontSize * 1.4) + offsetY}
        >
          {text}
        </tspan>
      )
    }
    for (const line of lines) {
      lineLen += line.length
      while (sliceLen < lineLen && sliceIdx < spanSlices.length) {
        let slice = spanSlices[sliceIdx]
        tspans.push(makeTspan(slice.text, slice.props, lineIdx))
        sliceIdx++
        sliceLen += slice.text.length
      }
      if (sliceLen > lineLen) { // break slice
        let lastSlice = spanSlices[sliceIdx - 1]
        let text1 = lastSlice.text.slice(0, lastSlice.text.length - (sliceLen - lineLen))
        let text2 = lastSlice.text.slice(text1.length)

        tspans.pop()
        tspans.push(makeTspan(text1, lastSlice.props, lineIdx))
        tspans.push(makeTspan(text2, lastSlice.props, lineIdx + 1))
      }
      lineIdx++
    }
    return tspans
  }

  render() {
    return (
      <text {...this.getAttrProps()}>
        {this.getWrappedSpans()}
      </text>
    )
  }
}

react-native 项目 run-android 报无法安装应用

z at ZsMBP in ~/Projects/ReactXP/reactxp/samples/TodoList (master●)
$ react-native run-android
Scanning 740 folders for symlinks in /Users/z/Projects/ReactXP/reactxp/samples/TodoList/node_modules (7ms)
JS server already running.
Building and installing the app on the device (cd android && ./gradlew installDebug)...
Could not install the app on the device, read the error above for details.
Make sure you have an Android emulator running or a device connected and have
set up your Android development environment:
https://facebook.github.io/react-native/docs/android-setup.html

这个错误的解释废话太多,还是错的, 其实手动运行下命令cd android && ./gradlew installDebug就会发现是./android/.gradlew 这个文件没有执行权限导致的,运行一下

chmod +x ./android/.gradlew

就可以了

webpack2 -从0搭建一个完整的前端开发技术栈(6)

之前的框架是 full-stack 的,最近需要帮以前工作室的学妹弄一个简单的 h5 页面时才发现,自己居然手头没有一套靠谱的纯粹配置,如果从其他项目中剥离出来又实在提不起兴趣,于是下了官方的 create-react-app, 发现各种问题,比如

  1. 不支持 react 热更新,只支持热刷新
  2. 与 sass 整合很不方便,sass 直接编译到源码目录,干扰视线。。
  3. babel,webpack 也不支持扩展;

无法适应大型项目,和我理解中的只是用来写 demo, 或者简单页面的脚手架差不多。临时看了一下 styled-components, 发现这货比我想象的要高级,支持嵌套语法,auto-prefixer, 既然是 css-in-js,那么动态复用就完全不用担心了,更让我惊艳的是工具链的支持,在 atom 中,基本上必装的 language-babel 插件已经默认支持了,在 js 中也可以享受到强大的 css 提示。

于是在 create-react-app + styled-components 的组合下写起展示类页面来体验也挺不错的。

不过,没有一套自己常用的纯前端的脚手架总感觉心里不太安稳,而且转眼过去大半年了,webpack2, react-hot-loader v3 已经是正式版了,个人也积累了一些不错的库,于是便打算再折腾一个最新的脚手架。

作为一名合格的「前端配置工程师」,每半年~一年升级一次配置才能紧跟前端的潮流。。

先放代码:https://github.com/zaaack/react-starter.git

这里主要记录一下猜到的坑。。

react-hot-loader

毫无悬念,虽然之前有过成功配置 react-hot-loader 的经历,而且那时也是 v3,只不是是 v3-beta, 现在是 v3 正式版,但是还是在这里卡住了大部分时间…

其实官方的两个 demo 已经是非常友好的了,

webpack2 官方也有一个教程,不过需要注意的是 babel-preset-es2015 的参数配置有点老了,具体可以参考上面的 react-hot-loader-minimal-boilerplate

不过 react-hot-loader 还是太 hack了点,很多地方都需要加 patch,不仅仅是在 entry 中,babel presets 中也要加。开始时就是忘了加 babel presets 中的 react-hot-loader/babel, 导致虽然 log 正常,但是却完全没反应。没有报错提示无意是最难调试的了,只能去翻源码以及在源码中加 log。

发现之后加上去了,但是还是老样子,到底是哪里有问题呢?经过反复耐心测试才发现居然是少了 babel-polyfill, 而且还不能使用 babel-transform-runtime, 虽然之前的配置中 可以通过对 transform-runtime 加 {polyfill:false} 参数使之共存,但是这个 trick 貌似在最新版中不能用了,不过没关系,可以通过 NODE_ENV 区分啊,大不了 开发环境就不加嘛。

可是这样还是不行,不过还好在反复实践中发现
虽然 webpack 中配置了 babel, 但是还是会去读取 .bablerc 文件,在翻 babel-loader 的源码时在文档中成功找到了相关依据,改了一个配置就搞定了。。此时 react-hot-loader 所有的坑都已填满,可以放心食用了。。

react 测试

其实之前一直都没写过 react 测试,老实说除了一些基础 node 库之外,我还没在业务代码中写过测试呢,更不用说 UI 测试了。。所以这次就干脆把测试也加上了。React 自带了 react-dom/test-utils,不过 API 太过冗长,Facebook 自己都不用, 参考了下阮老师的这篇, 发现很多 demo 都跑不起来了,倒不仅仅是因为接口的变动,更坑的是 bug…bug.

比如 renderIntoDocument 这个方法,最新的 react 已经去掉了返回值,就算看源码自己用 callback 实现也不行,因为我就尝试了,callback 也没有返回值了!连之前 callback 可以用的 this也变成 null 了。。所以要么直接用 document,要么用 airbnb 的 enzyme, 考虑到写测试是一件很痛苦的事情, 而且
enzyme 的接口其实和 jquery 很类似,目测挺方便的,本着不给自己添麻烦原则,就只能牺牲一下硬盘空间了。。我的 node_modules 目录已经快撑爆了吧。。

然而,随便给 demo 代码写了点 测试,想点击一个 react-router 的 Link 实现跳转,死活没反应。在 js-dom 的官方 readme 上看见了巨大的 “不支持跳转” 的说明,好吧,这很好解决,加个 NODE_ENV=test 换成 createMemoryHistory 不就可以了,issue 中也有类似的解决方案。可以还不行!!!一样的没有任何报错,任何反应(可见良好的错误提示是多么重要)。无聊之下又开始翻源码,打 log, 可以确定事件是触发了的,于是又去看 react-router 的 Link 组件,打 log, 终于发现是 因为 Link 中为了确保是鼠标左键点击做了 event.button === 0 的判断,而 jsdom 中的 MouseEvent.button === undefined, 并且是个老 issue 了,果断再来一发

至于 Facebook 官方的测试框架 jest 强大的 snapshot 功能,可喜的发现 AVA 已经 内置了,使用非常方便,就不细说了,直接看代码和官方文档吧。

恩,至此所有坑已填完,可以上传 repo 了。是的,这篇文章其实不是什么分享,只是单纯的吐槽,解决这些坑其实几乎毫无价值,半年之后,可能有些修了,可能又会有新的 bug。。人生苦短,且行且珍惜,所以半年更新一下配置,又可以保持一下自己的折腾能力了。。

最后,再来安利一发自己的 react 脚手架 react-starter

以后有时间再整个 koa 后台的 starter,graphql 的,ssr 的。。。

mac 下代理开发环境总结

在工作中开发,代理技术对于开发环境是不可避免的,小到入职时配置 VPN,再到开发环境中通过代理使用线上 API,通过代理拦截 App 请求,合理的代理应用对于开发来说经常会有事半功倍的作用。这里记录一下我在工作中代理的运用。

1. 代理服务器

我这里用的使用的是由阿里巴巴开源的基于 nodejs 的代理工具— anyproxy,优点是支持 https,使用 nodejs 作为配置文件,学习成本低,缺点是配置 API 看起来太老,连 promise 都不支持,大公司的开源项目不知道什么时候会挂刚上了官网,4.0 beta 已经开始支持 promise 了。

2. 手机代理软件

Android 5.0 以下可以使用 DroidProxy 直接修改 wifi 代理配置,但是 5.0 以上更新了安全策略,只能用开 VPN 的了。我这里用的是虽然界面很丑,但是好得能用的 Drony. (以上 APK 可以去 https://apkpure.com搜索下载)。虽然占用了 VPN,不过一般电脑上各种代理和 VPN 都是开启状态,因此手机上还是能走电脑上的代理和 VPN 的。

Drony 和 connectbot 的 ssh 端口转发冲突,导致无法代理 dns 访问电脑 vpn 的内网,localproxy chains 相关配置未测试..

rem 布局

/**
* /js/lib/flexrem.js
 *
 */

$inital-rem-size: 100px !default;
$design-width: 375px !default;


html {
  font-size: $inital-rem-size / $design-width * 320px;
}

@each $width in (320px, 360px, 400px, 414px, 480px, 500px, 650px) {
  @media only screen and (min-width: #{$width}) {
    html {
      font-size: $inital-rem-size / $design-width * $width;
    }
  }
}


@function px2rem($px) {
  @if unitless($px) { // 不管带不带单位都可以
    $px: $px * 1px;
  }
  @return $px / $inital-rem-size * 1rem;
}
/**
 *
 * 根据屏幕宽度自动设置html的fontSize, 使rem的大小自适应
 * usage:
 * dui.flexrem({
 *            //设计稿宽度
 *         		designWidth: <number>, initial: 375
 *         		//rem 初始像素值,initial: 100
 *         		//html的fontSize = (opts.initRem/opts.designWidth*winWidth)+'px'
 *         		initRem: <number>,
 *            //用来计算的最大的 winWidth, initial: 640
 *         		maxWinWidth: <number>
 *            //用来计算的最小的 winWidth, initial: 320
 *            minWinWidth: <number>,
 * })
 *
 * 由于android webview 偶尔会在 document.load 之后识别 viewport 导致 window.innerWidth 发生变化, 这段 js 需要在 window.onload 后运行,因此还需要配合 css 使加载前页面也能正确显示
 *
 *
 * $inital-rem-size: 100px; // 默认值
 * $design-width: 375px;// 默认值
 * @import '/css/card/widgets/_flexrem.scss';
 *
 * width: @px2rem(12)rem; => 12/100 = 0.12rem
 *
 */

!(function(win, doc) {

  var dui = window.dui || (window.dui = {}),
    timer = null,
    firstCalled = true,
    defaults = {
      designWidth: 375,
      initRem: 100,
      maxWinWidth: 640,
      minWinWidth: 320,
    },
    evts = 'onorientationchange' in win ? ['orientationchange', 'resize'] : ['resize'];

  if (dui.flexrem !== undefined) {
    return console.error('flexrem.js shouldn\'t be called twice!!')
  }
    /**
     * auto set html's fontSize to make rem flexable
     * @param  {object} opts
     *         {
     *            //设计稿宽度
     *         		designWidth: <number>, initial: 375
     *         		//rem 初始像素值,initial: 100
     *         		//html的fontSize = (opts.initRem/opts.designWidth*winWidth)+'px'
     *         		initRem: <number>,
     *            //用来计算的最大的 winWidth, initial: 960
     *         		maxWinWidth: <number>
     *         }
     * @return
     */
  dui.flexrem = function(opts) {
    if(!firstCalled) console.error('flexrem() can only be called once!');
    firstCalled =false
    var options = Object.assign({}, defaults, opts)
    for (var i = 0; i < evts.length; i++) {
      win.addEventListener(evts[i], function() {
        clearTimeout(timer);
        timer = setTimeout(setFontSize, 300);
      }, false);
    }

    win.addEventListener("pageshow", function(e) {
      if (e.persisted) {
        clearTimeout(timer);
        timer = setTimeout(setFontSize, 300);
      }
    }, false);
    // 初始化
    setFontSize();

    function setFontSize() {
      var winWidth = window.innerWidth || window.clientWidth;
      winWidth = Math.min(winWidth, options.maxWinWidth)
      winWidth = Math.max(winWidth, options.minWinWidth)
      var size = (winWidth / options.designWidth) * options.initRem;
      doc.documentElement.style.fontSize = size + 'px';
    }
  }


  function toType(obj) {
    return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
  }

  if (typeof Object.assign != 'function') {
  Object.assign = function (target, varArgs) { // .length of function is 2
    'use strict';
    if (target == null) { // TypeError if undefined or null
      throw new TypeError('Cannot convert undefined or null to object');
    }

    var to = Object(target);

    for (var index = 1; index < arguments.length; index++) {
      var nextSource = arguments[index];

      if (nextSource != null) { // Skip over if undefined or null
        for (var nextKey in nextSource) {
          // Avoid bugs when hasOwnProperty is shadowed
          if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
            to[nextKey] = nextSource[nextKey];
          }
        }
      }
    }
    return to;
  };
}



}(window, document));

linux 导入证书

xx.crt 文件拷贝到 /usr/local/share/ca-certificates/
然后运行

sudo update-ca-certificates

然后就可以了

graphql学习笔记-从0搭建一个完整的前端开发技术栈(5)

本来打算放弃这个系列了,但是前端发展太快,这个系列估计可以“永无止境”了。。

最近看了下 graphql, 了解了下当前的相关技术栈的整合情况,发现 relay 已经超出了我对查询一个 api 的耐心,而开源组织的 apollo client 以及一些简化的第三方 graphql 客户端都非常好用,大部分应用其实没有必要那么动辄 70多kb 的高性能客户端数据框架,平时 restful 的 api 没有字段按需返回, 缓存只用 http 协议自带的,性能也是完全满足需求的,而 relay 个人感觉为了优化而牺牲开发体验是得不偿失的,一个查询就需要新建一个文件,就算是服务端 mvc 一个文件也能写好几个查询啊,过去一行代码搞定的事情现在搞成一个文件,大大增加了前端项目的复杂度,简直不能忍。

还好 graphql 是一种协议,这意味着只要有足够多的人关注就会有层出不穷的社区项目,比如 apollo 就是其中优秀的一种,支持 redux, ssr(server-side-rendering), 可以搭配 react/angular/vanilla 使用。

不过既然是写博客记录,还是从头开始写起吧,先来看看 GraphQL。

GraphQL 简介

简单的说,GraphQL 是一种传输协议,最大的价值在于可以通过查询语言按需返回数据,避免了 restful 传输过多不必要的字段的问题,但是由于每次请求返回的数据不一样,因此复用接口时无法使用 http 自带的缓存协议,如etag,因此需要客户端框架来处理缓存,如 relay, apollo。 这篇文章主要内容其实就是官网的 learn 部分中第一篇的中文化,毕竟官网真心写的太好了。。主要的目的是为了后面服务端和客户端配置 GraphQL 提供一些基础。

使用 GraphQL 时,大致流程是先需要服务端定义好各种类型,然后通过 “root” 对象类型来进行查询,官网上称作entry point(入口点),然后客户端通过发送查询语句来得到相应的数据。这里先对查询语句有大致的了解,后续可以在实践中逐步学习服务端类型的定义。

查询和修改

直接看 Graphql 可能会感到难以接受,因为不知道背后的实现原理,对实际使用时并没有明确的概念。实际上GraphQL 只是一种协议而已,等到实现的时候其实并没有想像的那么神奇,服务度的每一个可以查询的接口都需要通过 schema 去定义出来。。

查询字段(Fields)

查询是和 json 非常类似的,只不过“只有key”部分而已,因为 value 部分等着由服务端提供啊。。
比如:

var query = `{
  hero {
    name
  }
}`

// ==>
var response = {
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}

可以看到服务端可以返回的数据结构和查询语句非常类似。

参数(Arguments)

查询语句中可以传递参数,比如:

var query = `{
  human(id: "1000") {
    name
    height
    unitHeight: height(unit: FOOT)
  }
}`

// =>
var response = {
  "data": {
    "human": {
      "name": "Luke Skywalker",
      "height": 1.72,
      "unitHeight": 5.6430448
    }
  }
}

在查询语句里参数可以通过argName: value or argName: $variableName的方式传递值或者变量;
在服务端的 schema 中略有差异,通过argName: typeName = defaultValue的方式设置参数类型和默认值。

在这个例子中还使用了下面介绍的别名来得到同一字段的不同形式的值。

通过参数可以很容易的实现分页,客户端框架一般会提供包装的分页方法来进行优化。

别名(Alias)

别名用于在同一层级获取有着相同结构的不同数据,比如

var query = `{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}`
// =>
var response = {
  "data": {
    "empireHero": {
      "name": "Luke Skywalker"
    },
    "jediHero": {
      "name": "R2-D2"
    }
  }
}

片段(Fragments)

片段就是一个类型的一部分字段的集合,可以通过es6展开运算符来展开所有字段到当前的查询结构中,极大的方便了复用(感觉和类型放在一起更好理解)

var query = `{
  empireHero: hero(episode: EMPIRE) {
    name
  }
  jediHero: hero(episode: JEDI) {
    name
  }
}`
// =>
var response = {
  "data": {
    "empireHero": {
      "name": "Luke Skywalker"
    },
    "jediHero": {
      "name": "R2-D2"
    }
  }
}

变量(Variables)

虽然有了参数,但是查询语句只是一个字符串而已,还是需要有能传入动态参数的地方(虽然es6模版字符串也能做到),因此引入了变量的概念。
引入变量需要三个步骤

  1. 替换查询语句中的静态值为$variableName
  2. 声明$variableName为查询语句中可接受的变量
  3. variableName: value 通过另一个分开的变量键值传递给服务端,通常是 json 形式的 get 参数
var query = `query HeroNameAndFriends($episode: Episode) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}`
var variables = {
  "episode": "JEDI"
}
// =>
var response = {
  "data": {
    "hero": {
      "name": "R2-D2",
      "friends": [
        {
          "name": "Luke Skywalker"
        },
        {
          "name": "Han Solo"
        },
        {
          "name": "Leia Organa"
        }
      ]
    }
  }
}

操作名(Operation name)

上面例子中的查询语句很多是一种省略形式,我们来看一个完整的:

var query = `query HeroNameAndFriends($episode: Episode) {
  hero(episode: $episode) {
    name
    friends {
      name
    }
  }
}`

其中query是操作类型,是可以自定义的,比如定义成mutation, subscription,这里只是约定俗成的名字. HeroNameAndFriends是查询名字,服务端需要有对应的配置才能查询.

在 graphql 中,服务端的定义大致如下:

var schema = `
type Query {
  hero(episode: $episode) {
    name
    friends {
      name
    }
    # ...
  }
}

type Mutation {
  addHero(hero: $hero)
}

schema {
  query: Query
  mutation: Mutation
}
`

通过 schema 定义的字段在 GraphQL 中叫做 entry point (入口点), 其实和其他的对象类型差不多,唯一的区别在于可以通过query xxx { ... }mutation xxx { ... }的方式用来得到内部的查询定义。

指令(Directives)

通过指令可以通过变量修改查询的结构


var query = `
query Hero($episode: Episode, $withFriends: Boolean!) {
  hero(episode: $episode) {
    name
    friends @include(if: $withFriends) {
      name
    }
  }
}
`
var variables = {
  "episode": "JEDI",
  "withFriends": false
}

// =>
var response = {
  "data": {
    "hero": {
      "name": "R2-D2"
    }
  }
}

自带有两种指令,服务端也可以自定义更多:

@include(if: Boolean) 参数为true则包含该字段
@skip(if: Boolean) 参数为true则忽略该字段

行内片段

片段的简写形式


var query = `
query HeroForEpisode($ep: Episode!) {
  hero(episode: $ep) {
    name
    ... on Droid {
      primaryFunction
    }
    ... on Human {
      height
    }
  }
}
`

schema和type 的定义方式

参考:http://graphql.org/learn/schema

更多参考

http://graphql.org/learn