MySQL 使用笔记(二)

1.如何查看数据库中所有的存储过程?

A: mysql> show procedure status where db = 'db_for_mysql_crash_course'\G;;

Web API 的设计与开发--读书笔记

Web API 的设计与开发是开发者日常工作的重要内容,我们该如何来做好这项工作呢?我觉得一个务实的方法是先参考前辈们的做法,也就是站在巨人的肩上,理解消化后再尝试去突破,这样可能会事半功倍。经过一番搜索,找到了 <<Web API 的设计与开发>>, 我个人觉得这是一本对 Web API 进行全面、细致和深入剖析的书,对 Web API 的设计与开发很有帮助,值得一读。

书的内容是按照整分的逻辑组织,并依先易后难的顺序来讲解相关知识。下面我按自己的理解尝试对书中内容做个简单的总结。

Web API 是用于完成某种需求,由于需求会变化,所以一次就设计出完美 Web API 的想法是不现实的,所以一开始应该要给 Web API 的更改留有余地,这是很容易忽视的地方。推荐的做法是在 URI 中嵌入版本信息,典型的形式是 http://api.linkedin.com/v1/people

虽然一次就完美地设计 Web API 的想法不现实,但我们还是想尽量做好,减少 Web API 版本变更的次数,毕竟版本越多维护成本越高,那么我们该如何设计 Web API 呢?

Web API 通过 HTTP 协议来完成通信,在设计时我们应该最大程度地利用 HTTP 协议规范。基于标准协议设计的 API 至少要比使用私有协议设计的 API 更容易理解,还会减少使用时引入的 bug,使你的 API 得到更广泛的使用,提高利用已有的程序库或代码的可能。

有了整体设计原则后,我们来看下具体的请求和响应设计。API 的功能是为了完成项目的需求,最完备的请求会包含请求端点、请求方法、请求参数和请求数据体(Request Body),我们依次来审视请求的每个部分。

端点是指用于访问 API 的 URI,普适又重要的设计原则有:

  • 短小便于输入的 URI
  • 人可以读懂的 URI
  • 没有大小写混用的 URI
  • 不会暴露服务端架构的 URI
  • 规则统一的 URI

端点设计的注意事项:

  • 使用名词的复数形式
  • 注意所用的单词
  • 不使用空格及需要编码的字符
  • 使用连字符来连接多个单词

URI 和 HTTP 方法之间的关系可以认为是操作对象和操作方法的关系。如果把 URI 当作 API(HTTP) 的 “操作对象 = 资源”, HTTP 方法则表示 “进行怎样的操作”。通过用不同方法访问同一个 URI 端点,不但可以获取信息,还能修改信息、删除信息等,这样的思想正成为 Web API 设计的主流方式。

方法名 说明
GET 获取资源
POST 新增资源
PUT 覆盖已有资源
DELETE 删除资源
PATCH 更新部分资源
HEAD 获取资源的元信息

有时请求可能还需要传递参数,在设计 URI 时,必须决定是把特定参数放在查询参数里还是路径里,决策的依据有以下两点:

  • 是否是表示唯一资源所需的信息
  • 是否可以省略

请求数据体,个人认为可以采用面向对象编程的思想来设计,整个处理过程会轻松很多。

说完请求,让我们来看下响应。首先是正确使用状态码,国内由于历史原因遗留下来无论请求是否成功都一律返回 200 的问题,全站切换到 HTTPS 后,我们还是应该最大程度地利用 HTTP 规范,这样我们能受益于通用的 HTTP 程序库,减轻客户端的负担。

其次是数据格式,这里的数据格式是指该用怎样的形式来描述 API 返回的结构化数据,具体而言就是指 JSON、XML 等数据格式。关于这一点,事实上几乎没有可讨论的,因为我们通常就是使用 JSON 作为默认的数据格式,若有需求 API 也可以支持 XML 的格式,这是最贴近现实的做法。

再次是数据内部结构,我们重点看下数据应该以数组还是对象返回,作者更推荐使用对象来封装数据的方式,因为该方式有如下几个优点:

  • 更容易理解响应数据表示什么
  • 响应数据通过对象的封装实现了结构统一
  • 可以避免安全方面的风险

从次是各个数据的格式,各个数据项组成了最终的数据,只有掌握了如何处理单个数据项格式才能设计出合理的响应体数据格式。作者重点介绍了如何描述性别数据、日期格式和大整数,受益匪浅。

最后是出错信息的表示,同样,我们需要选择合适的状态码,出误信息建议以消息体的形式返回,出错信息应该包含详细的错误代码、人们能够读懂的相关信息,以及记载有详细说明的文档页面的 URI,如下所示:

1
2
3
4
5
6
7
{
  "error": {
      "code": 2013,
      "message": "Bad authentication token",
      "info": "http://docs.example.com/api/v1/authentication"
  }
}

如果想支持描述多个错误同时发生,可以返回出错信息数组,

1
2
3
4
5
6
7
8
9
{
  "errors": [
      {
          "code": 2013,
          "message": "Bad authentication token",
          "info": "http://docs.example.com/api/v1/authentication"
      }
  ]
}

以上是基础内容,作者最后还介绍进阶内容,开发牢固的 Web API,对我们把 Web API 设计和开发提高到新高度有非常大的帮助。

Ionic App 使用 Cordova Hot Code Push 实现热更新

本文主要记录 Ionic App 使用 cordova hot code push 实现热更新时遇到问题的解决方法,另外也简单记录下使用方法,方便日后查阅。

Cordova hot code push 插件的原作者已经不维护了,我们可以选择一个可能最好的 fork 来使用。 gitpop2 可以帮助我们选择,我从中选择了当前 star 最多的一个 fork。

Ionic App 使用 cordova hot code push 实现热更新的基本步骤如下:

  1. 在 ionic 工程中添加 cordova hot code push plugin

     $ ionic cordova plugin add https://github.com/snipking/cordova-hot-code-push.git
    
  2. 安装 Cordova Hot Code Push CLI client

     $ npm install -g cordova-hot-code-push-cli
    
  3. 为指定平台编译工程

     $ ionic cordova prepare android
    
  4. 执行插件初始化

     $ cd /path/to/project/root
     $ cordova-hcp init
    
  5. 生成插件配置文件

     $ cordova-hcp build
    
  6. 运行到设备上

  7. 开发和发布应用新版本的 web

     // 1. 开发
     // 2. 为指定平台编译工程生成 web 
     $ ionic build --engine=cordova --platform=android
     // 3. 生成新插件配置文件
     $ cordova-hcp build
     // 4. 部署到服务器
    

在使用的过程中遇到的第一个问题是更新之后白屏。使用 Chrome 的 remote devices 调试 android webview 找到了问题的原因,ionic 应用中 <base href="/" />, cordova hot code push 会将 web 代码拷贝到外部存储上,webview 使用形如 file:///data/user/0/com.tenneshop.liveupdatedemo/files/cordova-hot-code-push-plugin/2020.01.07-16.16.39/www/index.html 的路径来加载应用,此时 document.baseURI = /,加载其他相对路径的 js 文件时,是相对这个路径,例如 <script src="cordova.js"></script>,就是以 /cordova.js 去加载,于是就会提示找不到文件。从上面的分析我们也知道,解决问题的一个办法是修正 base href 的值,我们可以在 index.html 的 head 元素加入下面的代码:

1
2
3
<script>
    document.write('<base href="' + document.location.href + '" />');
</script>

这样我们就修正文件路径的问题,很不巧,虽然文件的路径是对了,但是 ionic 默认不响应 file schema 的请求,我们需要做些工作,先让 WebViewLocalServer.java 支持响应 file schema,将 createHostingDetails 改成如下实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
private void createHostingDetails() {
  final String assetPath = this.basePath;

  if (assetPath.indexOf('*') != -1) {
    throw new IllegalArgumentException("assetPath cannot contain the '*' character.");
  }

  PathHandler handler = new PathHandler() {
    @Override
    public InputStream handle(Uri url) {
      InputStream stream = null;
      String path = url.getPath();
      try {
        if (url.getScheme().equals("file")) {
          stream = protocolHandler.openFile(path);
        } else if (path.startsWith(contentStart)) {
          stream = protocolHandler.openContentUrl(url);
        } else if (path.startsWith(fileStart) || !isAsset) {
          if (!path.startsWith(fileStart)) {
            path = basePath + url.getPath();
          }
          stream = protocolHandler.openFile(path);
        } else {
          stream = protocolHandler.openAsset(assetPath + path);
        }
      } catch (IOException e) {
        Log.e(TAG, "Unable to open asset URL: " + url);
        Log.e(TAG, e.getLocalizedMessage());
        return null;
      }

      return stream;
    }
  };

  registerUriForScheme(httpScheme, handler, authority);
  registerUriForScheme(httpsScheme, handler, authority);
  if (!customScheme.equals(httpScheme) && !customScheme.equals(httpsScheme)) {
    registerUriForScheme(customScheme, handler, authority);
  }

  registerUriForScheme("file", handler, "");

}

然后是 isLocalFile 方法:

1
2
3
4
5
6
7
private boolean isLocalFile(Uri uri) {
  String path = uri.getPath();
  if (path.startsWith(contentStart) || path.startsWith(fileStart) || uri.getScheme().equals("file")) {
    return true;
  }
  return false;
}

做完这些工作后 ionic 就可以响应 file schema 请求了。

继续测试,我发现更新后第二次打开还是显示 App bundle asset 中的 web,这有点奇怪。仔细查看日志,确实有加载外部存储的 web , 但却被 http://localhost/ 的请求覆盖了,这是什么原因呢?经过对代码逻辑的一番梳理,我发现是 IonicWebViewEngine 中 onPageStarted 方法的原因:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void onPageStarted(WebView view, String url, Bitmap favicon) {
  super.onPageStarted(view, url, favicon);
  String launchUrl = parser.getLaunchUrl();
  if (!launchUrl.contains(WebViewLocalServer.httpsScheme) && !launchUrl.contains(WebViewLocalServer.httpScheme) && url.equals(launchUrl)) {
    view.stopLoading();
    // When using a custom scheme the app won't load if server start url doesn't end in /
    String startUrl = CDV_LOCAL_SERVER;
    if (!scheme.equalsIgnoreCase(WebViewLocalServer.httpsScheme) && !scheme.equalsIgnoreCase(WebViewLocalServer.httpScheme)) {
      startUrl += "/";
    }
    view.loadUrl(startUrl);
  }
}

MainActivity 触发 webview 加载 file:///android_asset/www/index.html,然后 cordova hot code push plugin 启动工作,它会让 webview 加载外部存储的 web,之后 IonicWebViewEngine 的 onPageStarted 收到 file:///android_asset/www/index.html 的请求的回调,它先停止了 webview 的加载工作,即 cordova hot code push plugin 启动加载外部存储的 web 的请求,再开始 http://localhost/ 的请求,也就是打印出来日志的记录。正是这个方法时序的问题导致成功更新之后再重启应用仍然加载 app bundle asset 的 web。一种解决办法是我们直接让 MainActivity 直接加载 http://localhost/,就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void onCreate(Bundle savedInstanceState)
{
   super.onCreate(savedInstanceState);

   // enable Cordova apps to be started in the background
   Bundle extras = getIntent().getExtras();
   if (extras != null && extras.getBoolean("cdvStartInBackground", false)) {
       moveTaskToBack(true);
   }
   launchUrl = "http://localhost/";
   // Set by <content src="index.html" /> in config.xml
   loadUrl(launchUrl);
}

这样热更新就可以正常工作了。

我继续做了点测试,又发现一个和 ionic icon 相关的问题,ionic 4 使用了 Fetch API 来请求 ionic icon 的 svg 资源,由于现在是使用 file schema 来指定资源路径,由于 Fetch API 不支持 file schema 所以就报错 Fetch API cannot load file:///xxx/www/svg/md-star.svg. URL scheme "file" is not supported. 我们得想办法来解决这个问题,一个办法替换 fetch 方法的实现,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<script>
   document.write('<base href="' + document.location.href + '" />');

   var originalFetch = window.fetch;

   window.fetch = function() {
       var args = [];
       for (var _i = 0; _i < arguments.length; _i++) {
           args[_i] = arguments[_i];
       }
       var url = args[0];
       if (typeof url === 'string' && url.match(/\.svg/)) {
           return new Promise(function(resolve, reject) {
               var req = new XMLHttpRequest();
               req.open('GET', url, true);
               req.addEventListener('load', function() {
                   resolve({
                       ok: true,
                       status: 200,
                       text: function() {
                           return Promise.resolve(req.responseText);
                       }
                   });
               });
               req.addEventListener('error', reject);
               req.send();
           });
       } else {
           return originalFetch.apply(void 0, args);
       }
   };
</script>

在这些测试过程中,我还发现 cordova hot code push 更新时只做了版本字符是否相等的判断,这在服务器端的版本低于本地版本时,插件仍然会做更新,这是有问题的,我们需要严格这里的判断,让它只有在服务端的版本高于本地版本时才做更新。相关代码位于 UpdateLoaderWorker 的 run 方法中。

最后一个要考虑的问题是如何将我们修改的代码和 ionic 的代码很好的整合起来?我现在的想法是创建一个私有的扩展 IonicWebViewEngine 和 WebViewLocalServer,然后借鉴 ionic 通过 config.xml 的 web 偏好设置的方法,像下面的代码:

1
<preference name="webView" value="com.ionicframework.cordova.webview.IonicWebViewEngine" />

回头测试下这个想法,好了有时间也许可以整理好代码提个 Pull Request。

Reference: