码农视界周刊

码农视界周刊 查看完整档案

上海编辑安徽科技学院  |  计算机 编辑无业游民  |  全栈工程师 编辑 xuxuewen.gitee.io/docs/#/ 编辑
编辑

没有一瞬间的脱胎换骨,编程是一次次的试探,也是一次且行且至的寻觅

个人动态

码农视界周刊 关注了用户 · 4月19日

Peter谭金杰 @jerrytanjinjie

前端架构师

微信公众号:前端巅峰

欢迎技术探讨~

个人微信:CALASFxiaotan

关注 4243

码农视界周刊 收藏了文章 · 4月16日

记一次 Electron + DLL/SO(node-ffi) + sqlite3 项目实践

背景

这是一个全栈项目,后端使用 node 。项目需要提供B端与C端两个版本:

  • B端要求支持多实例环境
  • C端要求跨平台离线独立运行

项目中一些复杂的数据处理功能由 C语言 编译而成的 动态链接库(DLL) (在 Linux 下叫做 Shared Library , 简称 SO ,以下统称 DLL),及 python 封装的接口以 http 形式提供服务。在C端版本中,由于需要满足离线独立运行的需求,python 服务被打包成 可执行程序node 通过执行命令行方式调用。

根据以上要求,我们做了以下技术选型。

技术选型

  1. node 版本

    选用了当时的 LTS 版本 10.15.3,但由于后来为了和 Electron 中内置的 node 版本保持统一,修改为了 10.11.0

    虽然 10.15.3 与 10.11.0 感觉版本相差不多,但确实有一些特性不同。例如 fs.mkdir , 10.15.3 支持 recursive 选项,但 10.11.0 不支持,导致迁移到 Electron 时,运行结果不符预期。因此如果同时要开发 B 端与 C 端,为了更方便迁移,最好一开始便确定好 node 的具体版本。

  2. 数据库

    由于需要支持C端 离线独立运行 的需求,数据库选用了 sqlite

    sqlite 是单文件数据库,npm 包 sqlite3 ,是对 sqlite3 引擎的一层封装。一个数据库文件、一个包含数据库引擎的 npm 包,使得C端打包成离线独立运行程序成为可能。

  3. DLL 的载入与调用

    node 中,DLL 的调用,主要借助以下两个 npm 包:

    • ffi : 实现 node 加载并调用 DLL
    • ref : 提供强大的内存指针操作
  4. C 端应用构建与打包

    Electron 是由 Github 开发,用 HTML,CSS 和 JavaScript 来构建跨平台桌面应用程序的一个开源库。Electron 通过将 Chromium 和 Node.js 合并到同一个运行时环境中,并将其打包为 Mac,Windows 和 Linux 系统下的应用来实现这一目的。

    Electron 对前端开发者的友好,及对跨平台的支持,使得我们决定使用 Electron 来对 B 端版本进行一次包装,最大程度复用 B 端代码。主要用到了这几个依赖:

    • electron@4.2.0 : 4.x 版本的 electron 内置的 node 版本是 10.x ,可以与 B 端所用 node 版本匹配
    • electron-builder : 用于 electron 打包

开发环境搭建及问题排查

  1. 依据 DLL位数 确定开发环境

    DLL位数 对开发环境搭建有很大的影响。以本项目为例,某 DLL windows 平台上只提供了 32 位版本,导致我们在开发时,必须选用 32 位 node。万幸的是,不需要系统也为 32 位。

    此处推荐使用 nvm 对 node 版本进行管理,当切换了 node 版本后,类似于 ffirefsqlite3node-sass 这样的原生模块都需要重装,借助 node-gyp 或同类工具进行重新编译。

    参考:native module on node.js

  2. 安装 refffisqlite3 等原生模块依赖时,需要借助 node-gyp 来针对当前系统平台进行编译。因此 node-gyp 的安装前置条件必须满足

    参考:github: node-gyp#Installation

    以下为本项目中实践步骤

    • Linux

      • 安装 python v2.7
      • 安装 make
      • 安装 gccgcc-c++

        • gcc 版本需要与使用的 DLL 依赖的 gcc 版本保持一致,gcc-c++ 需要与 gcc 版本保持一致
    • Windows

      • npm install --global --production windows-build-tools

        • windows-build-tools 能够帮助我们方便地配置好编译 node 原生模块需要的环境
        • 本项目中还使用了参数 --vs2015 ,默认情况安装的是 VS2017 ,一开始遇到了一些问题,后来换成 VS2015 过程要顺畅一些,这个选项可看情况选择
      • npm config set msvs_version 2015 ,如果安装的是 VS2017 ,则值设置为 2017
      • npm config set python python2.7的安装路径
  3. Electron 开发环境

    Electron 开发环境与第 2 点要求一致,不同的是,我们使用的 node 是 Electron 内置的 node ,而非系统安装的 node 。因此需要为 Electron 环境重新编译 node 原生模块。

    这一步可以借助npm包 electron-rebuildelectron-builder 完成

本项目使用的是第二种方式:

  • 安装依赖 electron-builder
  • 添加 "postinstall": "electron-builder install-app-deps" 到 npm scripts 中,这样每次安装依赖时,将会自动为我们进行原生模块编译

Electron 版本选择扩展资料:

    1. 安装依赖(其中一些问题也可能出现在 Electron 打包中)

      npm install

    DLL 的使用

    相比起环境配置, DLL 的使用要更简单。ffi 结合 ref ,基本使用可参考 ffi 官方示例ref 官方示例

    常见问题

    1. DLL 依赖于其他 DLL

      这个问题比较常见,我们尝试过两种方案

      1. 使用 ffi.DynamicLibrary 引入依赖

        let { RTLD_NOW, RTLD_GLOBAL } = ffi.DynamicLibrary.FLAGS;
        // 有多个则执行多次 ffi.DynamicLibrary 来引入多个
        ffi.DynamicLibrary(
            '/path/to/.dll/or/.so',
            RTLD_NOW | RTLD_GLOBAL
        );
      2. 使依赖可全局访问,详情见下文
    2. 遇到错误码为 126 的 win error

      这个表示无法找到 DLL ,可能是路径错误,也可能是其依赖未引入或无法被程序在全局路径访问。开发与部署环境有差异时,很容易遇到这种问题。

      那么我们可以怎样确定缺少的依赖是哪一个呢? Linux 提供了 ldd 支持我们查看程序运行所需的 DLL ,若无法找到某个依赖,其对应的地址将为 not found 。 Windows 自带的 CMD 不支持该命令,但 Git Bash 等工具为我们集成了该功能。另外 Windows 下也可以使用 Dependency Walker 等工具获取依赖。

    3. 想合并多个库到一个对象上

      ffi.Library 第三个参数支持传入一个对象,若传入这个对象, ffi 会将该库新增的方法添加到这个对象上,同名将被覆盖,最终返回这个对象,没有传这个参数时,会返回一个新的对象。可以这样做,但一般来说没有这种需求。

    4. 使用 ffi 读取复杂数据结构

      举一个简单的例子——读取字符串数组

      function readStringArray (buffer, total) {
          let arr = [];
          for (let i = 0; i < total; i++) {
              arr.push(ref.get(buffer, ref.sizeof.pointer * i, ref.types.CString));
          }
          return arr;
      }

      参考:Complex data structures with node-ffi

    使 DLL 或其依赖可全局访问

    Windows 环境

    • 共享目录方式
    Win 32Win 64
    32-bit DLLC:/Windows/System32C:/Windows/SysWOW64
    64-bit DLLC:/Windows/System32
    
    根据 DLL 位数及操作系统位数的不同,将 DLL 放到以上某个目录即可
    
    
    • 环境变量 PATH

      众所周知, Windows 下 PATH 环境变量非常神奇, DLL 也不例外。将 DLL 所在目录的绝对路径加到 PATH 环境变量中,可以实现 DLL 全局访问。放在 node 里面,可以通过如下方式动态设置:

      process.env.PATH += `${path.delimiter}${xxx}`;

    Linux 环境

    主要是通过命令 ldconfigldconfig 是一个 SO 管理命令,可以使 SO 为系统所共享。 ldconfig 按照一定规则搜寻 SO 并创建链接、缓存,供程序使用。

    以下为搜寻范围:

    • /lib 目录
    • /usr/lib 目录
    • /etc/ld.so.conf 文件中声明的目录
    • 通常情况下, /etc/ld.so.conf 文件中会有一行如下内容:

      include ld.so.conf.d/*.conf

      因此,目录 /etc/ld.so.conf.d 里以 .conf 结尾的文件中声明的目录也在搜寻范围

    • 全局变量 LD_LIBRARY_PATH 中设置的目录

    因此,将 SO 放置到如上所说的目录中,并执行 ldconfig 即可实现 SO 共享。我们可以修改 /etc/ld.so.conf 文件、新增 /etc/ld.so.conf.d/*.conf 文件或修改全局变量 LD_LIBRARY_PATH

    参考:

    一个 Linux 开机脚本配合 ldconfig 应用实践

    项目中文件解析部分依赖了许多 DLL ,其中有部分对图形界面产生了影响,导致用户再次登录时显示黑屏。

    文件解析的 DLL 依赖是通过 node 在程序启动时写了一个文件到 /etc/ld.so.conf.d 下,里面声明了依赖所在路径,linux 开机时会自动加载。而解决黑屏则需要在开机时,自动去除该依赖声明,避免用户无法进入系统桌面。

    因此此处需要用到开机脚本在开机时为我们做一些处理,示例如下:

    新建文件 delete-ldconfig.sh 内容如下:

    #!/bin/bash
    # chkconfig: 5 90 10
    # description: test
    
    rm -f /etc/ld.so.conf.d/test.conf
    ldconfig

    执行如下命令:

    cp ./delete-ldconfig.sh /etc/rc.d/init.d
    cd /etc/rc.d/init.d/
    chmod +x delete-ldconfig.sh
    chkconfig --add delete-ldconfig.sh
    chkconfig delete-ldconfig.sh on

    参考:

    Electron 主进程繁忙阻塞渲染进程问题

    C端进行文件解析测试时,使用了一个 node_modules 目录打包而成的压缩包,体积虽然不算很大,但小文件十分多。调用 DLL 解析时,大致耗时30分钟,后续程序处理又花了较久时间。在此期间,C端应用界面失去响应。

    经过查询,原来在 chromium 中,页面渲染时,UI 进程需要和 main process 不断的进行 sync IPC ,若此时 main process 忙,则 UI process 就会在 IPC 时阻塞。

    所以,不希望渲染进程被阻塞,就需要为主进程减负。如

    • 在大量同步代码中间断地插入异步处理,将执行权暂时交出
    • 使用多进程

    参考:Electron的主进程阻塞导致UI卡顿的问题

    中断同步代码

    在一开始的实现中,文件解析后的结果处理是使用一个 for 循环,无任何异步操作,是一个 CPU 密集型的任务。

    使用以下代码模拟该场景:

    (async () => {
        setInterval(() => {
            console.log('=====');
        }, 60);
        while (true) {
            // 一些处理
        }
    })();

    为了解决这块的阻塞问题,我们可以进行如下改造:

    (async () => {
        setInterval(() => {
            console.log('=====');
        }, 60);
        while (true) {
            await new Promise(resolve => {
                setImmediate(() => {
                    // 一些处理
                    resolve();
                });
            });
        }
    })();

    使用多进程

    在 Electron 中实现多进程有很多选择,如 Web Workers、Node 的 child_process 模块、 cluster 模块、 worker_threads 模块等。

    由于 Electron 项目安装的原生模块是经过重新编译的,且应用运行时,会出现环境变量上的差异,导致某些系统程序无法找到。因此,我们不能直接用 child_process.exec 等类似方式来启动我们子进程。

    此次实践中,我们经过多种尝试,最终决定采用如下方式:

    // 主进程
    const bkWorker = child_process.spawn(process.execPath /* 1 */, ['./app.js'], {
        stdio: [0, 1, 2, 'ipc'], /* 2 */
        cwd: __dirname, /* 3 */
        env: process.env /* 4 */
    });
    bkWorker.on('message', (message) => {
        // ...
    });
    
    // 子进程
    process.send(/* ... */); /* 5 */

    说明:

    1. 指定要执行的程序路径,主进程中, process.execPath 代表的是 electron 程序路径
    2. 启动一个父子进程间的IPC通道方便我们使用 process.send 进行通信
    3. 设置子进程的当前工作目录
    4. 同步父进程的环境变量到子进程

    参考:node文档-child_process.spawn

    spawn 的子进程意外退出重启处理探究

    主进程 index.js

    const { spawn } = require('child_process');
    
    let worker;
    
    function serve () {
        worker = spawn(process.execPath, ['./test1.js'], {
            stdio: [0, 1, 2, 'ipc'],
            cwd: __dirname,
            env: process.env
        });
        worker.on('message', (...args) => {
            console.log('message', ...args);
        });
        worker.on('error', (...args) => {
            console.log('error', ...args);
        });
        worker.on('exit', (code, signal) => {
            console.log('exit', code, signal);
        });
        worker.on('disconnect', () => {
            console.log('disconnect');
        });
        // 基本子进程退出,都会触发 worker.on('close')
        // 因此可以在这里做一些子进程重启之类的事
        worker.on('close', (code, signal) => {
            console.log('close', code, signal);
            // serve();
        });
    
        // 先后触发 worker 的 disconnect exit close 事件, exit 参数为 null SIGTERM
        // setTimeout(() => {
        //     worker.kill();
        // }, 2000);
    }
    
    // process.exit :主进程会马上退出,不会影响子进程,要退出子进程需要另做处理
    // process.abort :不会影响子进程,要退出子进程需要另做处理
    // throw Error :不会影响子进程,要退出子进程需要另做处理
    // setTimeout(() => {
    //     // process.exit();
    //     // process.abort();
    //     // throw new Error('1231');
    // }, 10000);
    
    setInterval(() => {
        console.log(process.pid, '===');
    }, 1000);
    
    serve();

    子进程 test1.js

    process.on('uncaughtException', (error) => {
        console.log('worker uncaughtException', error);
        process.send({
            type: 'error',
            msg: error
        });
    });
    
    process.send('connected');
    
    setInterval(() => {
        console.log(process.pid, '---');
    }, 1000);
    
    // 会触发 worker.on('disconnect') ,主 子 进程都不会退出,但连接中断,不能使用 process.send ,会报错
    // setTimeout(() => {
    //     process.disconnect();
    //     // process.send('hello?');
    // }, 3000);
    
    // process.abort :先后触发 worker 的 disconnect exit close 事件, exit 及 close 参数为 null SIGABRT
    // process.exit :先后触发 worker 的 disconnect exit close 事件, exit 及 close 参数为 0 null
    // setTimeout(() => {
    //     // process.abort();
    //     // process.exit();
    // }, 10000);
    
    // 用 process.on('uncaughtException') 处理
    // setTimeout(() =>{
    //     throw new Error('123');
    // }, 1500);

    打包后程序执行调试技巧

    使用命令行运行程序

    进入打包后程序所在目录,假设程序名为“Test”,则执行命令 ./Test 即可执行程序,并且程序中所有控制台输出都会打印在该终端中。我们可以借助输出信息进行调试。

    asar

    当 windows 打包开启 asar 时,打包后文件资源被归档到一个 asar 档案文件。如果刚好我们需要调试的话,可能需要更改代码后重新打包、安装、运行。但实际上有更方便的方式。

    asar 提供命令行工具,通过命令行,我们可以打包、列出归档文件中的文件列表、解压某个单文件、解压整个档案文件等功能。

    因此,我们的打包后调试过程可以被简化为:

    • 关闭应用,解压打包后 asar 文件
    • 修改代码,然后将修改后的文件整个重新打包,重启应用

    参考:npm-asar

    杂项

    Linux 虚拟机的安装及常用环境搭建

    虚拟机安装:http://note.youdao.com/notesh...

    环境搭建:http://note.youdao.com/notesh...

    VS Code 调试 Electron 配置文件

    {
        "version": "0.2.0",
        "configurations": [
            {
                "name": "Electron",
                "type": "node",
                "request": "launch",
                "cwd": "${workspaceRoot}",
                "program": "${workspaceFolder}/server/index.js",
                "runtimeExecutable": "${workspaceRoot}/server/node_modules/.bin/electron",
                "windows": {
                    "runtimeExecutable": "${workspaceRoot}/server/node_modules/.bin/electron.cmd"
                },
                "args": [
                    "."
                ],
                "outputCapture": "std",
                "env": {
                    "NODE_ENV": "development"
                }
            }
        ]
    }

    使用硬链接管理文件资源引用关系

    文件链接有两种:硬链接与符号链接。

    硬链接直接指向数据,会增加该文件的 inode 计数,数据只会存在一份,所有指向该数据的链接是同步的。在文件系统中, inode 为 0 的数据会被删除。而符号链接是记录的该文件位置,并不会增加文件的 inode 计数,平时我们使用到的快捷方式就是符号链接,目标文件删除,链接并不会消失,但这个链接指向的资源却无法再找到。

    在项目中有这样一个问题:整个系统是围绕着文件资源进行的业务处理,各个模块是串联进行,上游输出是下游输入。但各个流程的资源需要允许用户隔离管理,本模块对某个资源的依赖不影响其他模块的删除。

    如果要做到相互不依赖,可能的办法有:每个环节都存一份文件。但是这种方式,会浪费大量的硬盘空间,数据同步也不好处理。因此最终我们选择使用硬链接来实现该部分需求。

    硬链接操作起来非常简单,在 node 中主要有以下几个操作:

    const fs = require('fs');
    fs.link(existingPath, newPath, callback); // 创建硬链接
    fs.unlink(path, callback); // 删除硬链接
    fs.stat(path[, options], callback).nlink; // 查看 inode 数

    参考:

    postgre 数据导出

    rm -f dlp.sql&&pg_dump -U 用户名 -d 数据库名 -f 文件名.sql -h 服务器host -p 端口 -s

    参考:pg_dump

    错误处理

    事件相关

    • 事件内抛出的错误会被外层捕获

      const { EventEmitter } = require('events');
      const event = new EventEmitter();
      event.on('test', () => {
          // throw new Error('1');
          try {
              throw new Error('2');
          } catch (e) {
              event.emit('error', e);
          }
      });
      event.on('error', (e) => {
          console.log(e); // Error: 2
      });
      try {
          event.emit('test');
      } catch (e) {
          console.log(e); // Error: 1
      }

    同步异步相关

    • Promise 中的错误处理

      await 非常关键,没有 awaittry 无法捕获到 catchthrow 的错误

      (async () => {
          try {
              let res = await new Promise(() => {
                  throw new Error(2);
              }).catch(error => {
                  if (error.message === '1') {
                      return Promise.resolve('haha');
                  } else throw error;
              });
              console.log(res);
          } catch (e) {
              debugger;
          }
      })();

    跨盘符移动文件

    function moveFileCrossDevice (source, target) {
        return new Promise((resolve, reject) => {
            try {
                if (!fs.existsSync(source)) reject(new BEKnownError('源文件不存在'));
                let readStream = fs.createReadStream(source);
                let writeStream = fs.createWriteStream(target);
                readStream.on('end',function(){
                    fs.unlinkSync(source);
                    resolve();
                });
                readStream.on('error', (error) => {
                    reject(error);
                });
                writeStream.on('error', (error) => {
                    reject(error);
                });
                readStream.pipe(writeStream);
            } catch (e) {
                reject(e);
            }
        });
    }

    docker 常用命令

    http://note.youdao.com/notesh...

    查看原文

    码农视界周刊 赞了文章 · 4月16日

    记一次 Electron + DLL/SO(node-ffi) + sqlite3 项目实践

    背景

    这是一个全栈项目,后端使用 node 。项目需要提供B端与C端两个版本:

    • B端要求支持多实例环境
    • C端要求跨平台离线独立运行

    项目中一些复杂的数据处理功能由 C语言 编译而成的 动态链接库(DLL) (在 Linux 下叫做 Shared Library , 简称 SO ,以下统称 DLL),及 python 封装的接口以 http 形式提供服务。在C端版本中,由于需要满足离线独立运行的需求,python 服务被打包成 可执行程序node 通过执行命令行方式调用。

    根据以上要求,我们做了以下技术选型。

    技术选型

    1. node 版本

      选用了当时的 LTS 版本 10.15.3,但由于后来为了和 Electron 中内置的 node 版本保持统一,修改为了 10.11.0

      虽然 10.15.3 与 10.11.0 感觉版本相差不多,但确实有一些特性不同。例如 fs.mkdir , 10.15.3 支持 recursive 选项,但 10.11.0 不支持,导致迁移到 Electron 时,运行结果不符预期。因此如果同时要开发 B 端与 C 端,为了更方便迁移,最好一开始便确定好 node 的具体版本。

    2. 数据库

      由于需要支持C端 离线独立运行 的需求,数据库选用了 sqlite

      sqlite 是单文件数据库,npm 包 sqlite3 ,是对 sqlite3 引擎的一层封装。一个数据库文件、一个包含数据库引擎的 npm 包,使得C端打包成离线独立运行程序成为可能。

    3. DLL 的载入与调用

      node 中,DLL 的调用,主要借助以下两个 npm 包:

      • ffi : 实现 node 加载并调用 DLL
      • ref : 提供强大的内存指针操作
    4. C 端应用构建与打包

      Electron 是由 Github 开发,用 HTML,CSS 和 JavaScript 来构建跨平台桌面应用程序的一个开源库。Electron 通过将 Chromium 和 Node.js 合并到同一个运行时环境中,并将其打包为 Mac,Windows 和 Linux 系统下的应用来实现这一目的。

      Electron 对前端开发者的友好,及对跨平台的支持,使得我们决定使用 Electron 来对 B 端版本进行一次包装,最大程度复用 B 端代码。主要用到了这几个依赖:

      • electron@4.2.0 : 4.x 版本的 electron 内置的 node 版本是 10.x ,可以与 B 端所用 node 版本匹配
      • electron-builder : 用于 electron 打包

    开发环境搭建及问题排查

    1. 依据 DLL位数 确定开发环境

      DLL位数 对开发环境搭建有很大的影响。以本项目为例,某 DLL windows 平台上只提供了 32 位版本,导致我们在开发时,必须选用 32 位 node。万幸的是,不需要系统也为 32 位。

      此处推荐使用 nvm 对 node 版本进行管理,当切换了 node 版本后,类似于 ffirefsqlite3node-sass 这样的原生模块都需要重装,借助 node-gyp 或同类工具进行重新编译。

      参考:native module on node.js

    2. 安装 refffisqlite3 等原生模块依赖时,需要借助 node-gyp 来针对当前系统平台进行编译。因此 node-gyp 的安装前置条件必须满足

      参考:github: node-gyp#Installation

      以下为本项目中实践步骤

      • Linux

        • 安装 python v2.7
        • 安装 make
        • 安装 gccgcc-c++

          • gcc 版本需要与使用的 DLL 依赖的 gcc 版本保持一致,gcc-c++ 需要与 gcc 版本保持一致
      • Windows

        • npm install --global --production windows-build-tools

          • windows-build-tools 能够帮助我们方便地配置好编译 node 原生模块需要的环境
          • 本项目中还使用了参数 --vs2015 ,默认情况安装的是 VS2017 ,一开始遇到了一些问题,后来换成 VS2015 过程要顺畅一些,这个选项可看情况选择
        • npm config set msvs_version 2015 ,如果安装的是 VS2017 ,则值设置为 2017
        • npm config set python python2.7的安装路径
    3. Electron 开发环境

      Electron 开发环境与第 2 点要求一致,不同的是,我们使用的 node 是 Electron 内置的 node ,而非系统安装的 node 。因此需要为 Electron 环境重新编译 node 原生模块。

      这一步可以借助npm包 electron-rebuildelectron-builder 完成

    本项目使用的是第二种方式:

    • 安装依赖 electron-builder
    • 添加 "postinstall": "electron-builder install-app-deps" 到 npm scripts 中,这样每次安装依赖时,将会自动为我们进行原生模块编译

    Electron 版本选择扩展资料:

    1. 安装依赖(其中一些问题也可能出现在 Electron 打包中)

      npm install

    DLL 的使用

    相比起环境配置, DLL 的使用要更简单。ffi 结合 ref ,基本使用可参考 ffi 官方示例ref 官方示例

    常见问题

    1. DLL 依赖于其他 DLL

      这个问题比较常见,我们尝试过两种方案

      1. 使用 ffi.DynamicLibrary 引入依赖

        let { RTLD_NOW, RTLD_GLOBAL } = ffi.DynamicLibrary.FLAGS;
        // 有多个则执行多次 ffi.DynamicLibrary 来引入多个
        ffi.DynamicLibrary(
            '/path/to/.dll/or/.so',
            RTLD_NOW | RTLD_GLOBAL
        );
      2. 使依赖可全局访问,详情见下文
    2. 遇到错误码为 126 的 win error

      这个表示无法找到 DLL ,可能是路径错误,也可能是其依赖未引入或无法被程序在全局路径访问。开发与部署环境有差异时,很容易遇到这种问题。

      那么我们可以怎样确定缺少的依赖是哪一个呢? Linux 提供了 ldd 支持我们查看程序运行所需的 DLL ,若无法找到某个依赖,其对应的地址将为 not found 。 Windows 自带的 CMD 不支持该命令,但 Git Bash 等工具为我们集成了该功能。另外 Windows 下也可以使用 Dependency Walker 等工具获取依赖。

    3. 想合并多个库到一个对象上

      ffi.Library 第三个参数支持传入一个对象,若传入这个对象, ffi 会将该库新增的方法添加到这个对象上,同名将被覆盖,最终返回这个对象,没有传这个参数时,会返回一个新的对象。可以这样做,但一般来说没有这种需求。

    4. 使用 ffi 读取复杂数据结构

      举一个简单的例子——读取字符串数组

      function readStringArray (buffer, total) {
          let arr = [];
          for (let i = 0; i < total; i++) {
              arr.push(ref.get(buffer, ref.sizeof.pointer * i, ref.types.CString));
          }
          return arr;
      }

      参考:Complex data structures with node-ffi

    使 DLL 或其依赖可全局访问

    Windows 环境

    • 共享目录方式
    Win 32Win 64
    32-bit DLLC:/Windows/System32C:/Windows/SysWOW64
    64-bit DLLC:/Windows/System32
    
    根据 DLL 位数及操作系统位数的不同,将 DLL 放到以上某个目录即可
    
    
    • 环境变量 PATH

      众所周知, Windows 下 PATH 环境变量非常神奇, DLL 也不例外。将 DLL 所在目录的绝对路径加到 PATH 环境变量中,可以实现 DLL 全局访问。放在 node 里面,可以通过如下方式动态设置:

      process.env.PATH += `${path.delimiter}${xxx}`;

    Linux 环境

    主要是通过命令 ldconfigldconfig 是一个 SO 管理命令,可以使 SO 为系统所共享。 ldconfig 按照一定规则搜寻 SO 并创建链接、缓存,供程序使用。

    以下为搜寻范围:

    • /lib 目录
    • /usr/lib 目录
    • /etc/ld.so.conf 文件中声明的目录
    • 通常情况下, /etc/ld.so.conf 文件中会有一行如下内容:

      include ld.so.conf.d/*.conf

      因此,目录 /etc/ld.so.conf.d 里以 .conf 结尾的文件中声明的目录也在搜寻范围

    • 全局变量 LD_LIBRARY_PATH 中设置的目录

    因此,将 SO 放置到如上所说的目录中,并执行 ldconfig 即可实现 SO 共享。我们可以修改 /etc/ld.so.conf 文件、新增 /etc/ld.so.conf.d/*.conf 文件或修改全局变量 LD_LIBRARY_PATH

    参考:

    一个 Linux 开机脚本配合 ldconfig 应用实践

    项目中文件解析部分依赖了许多 DLL ,其中有部分对图形界面产生了影响,导致用户再次登录时显示黑屏。

    文件解析的 DLL 依赖是通过 node 在程序启动时写了一个文件到 /etc/ld.so.conf.d 下,里面声明了依赖所在路径,linux 开机时会自动加载。而解决黑屏则需要在开机时,自动去除该依赖声明,避免用户无法进入系统桌面。

    因此此处需要用到开机脚本在开机时为我们做一些处理,示例如下:

    新建文件 delete-ldconfig.sh 内容如下:

    #!/bin/bash
    # chkconfig: 5 90 10
    # description: test
    
    rm -f /etc/ld.so.conf.d/test.conf
    ldconfig

    执行如下命令:

    cp ./delete-ldconfig.sh /etc/rc.d/init.d
    cd /etc/rc.d/init.d/
    chmod +x delete-ldconfig.sh
    chkconfig --add delete-ldconfig.sh
    chkconfig delete-ldconfig.sh on

    参考:

    Electron 主进程繁忙阻塞渲染进程问题

    C端进行文件解析测试时,使用了一个 node_modules 目录打包而成的压缩包,体积虽然不算很大,但小文件十分多。调用 DLL 解析时,大致耗时30分钟,后续程序处理又花了较久时间。在此期间,C端应用界面失去响应。

    经过查询,原来在 chromium 中,页面渲染时,UI 进程需要和 main process 不断的进行 sync IPC ,若此时 main process 忙,则 UI process 就会在 IPC 时阻塞。

    所以,不希望渲染进程被阻塞,就需要为主进程减负。如

    • 在大量同步代码中间断地插入异步处理,将执行权暂时交出
    • 使用多进程

    参考:Electron的主进程阻塞导致UI卡顿的问题

    中断同步代码

    在一开始的实现中,文件解析后的结果处理是使用一个 for 循环,无任何异步操作,是一个 CPU 密集型的任务。

    使用以下代码模拟该场景:

    (async () => {
        setInterval(() => {
            console.log('=====');
        }, 60);
        while (true) {
            // 一些处理
        }
    })();

    为了解决这块的阻塞问题,我们可以进行如下改造:

    (async () => {
        setInterval(() => {
            console.log('=====');
        }, 60);
        while (true) {
            await new Promise(resolve => {
                setImmediate(() => {
                    // 一些处理
                    resolve();
                });
            });
        }
    })();

    使用多进程

    在 Electron 中实现多进程有很多选择,如 Web Workers、Node 的 child_process 模块、 cluster 模块、 worker_threads 模块等。

    由于 Electron 项目安装的原生模块是经过重新编译的,且应用运行时,会出现环境变量上的差异,导致某些系统程序无法找到。因此,我们不能直接用 child_process.exec 等类似方式来启动我们子进程。

    此次实践中,我们经过多种尝试,最终决定采用如下方式:

    // 主进程
    const bkWorker = child_process.spawn(process.execPath /* 1 */, ['./app.js'], {
        stdio: [0, 1, 2, 'ipc'], /* 2 */
        cwd: __dirname, /* 3 */
        env: process.env /* 4 */
    });
    bkWorker.on('message', (message) => {
        // ...
    });
    
    // 子进程
    process.send(/* ... */); /* 5 */

    说明:

    1. 指定要执行的程序路径,主进程中, process.execPath 代表的是 electron 程序路径
    2. 启动一个父子进程间的IPC通道方便我们使用 process.send 进行通信
    3. 设置子进程的当前工作目录
    4. 同步父进程的环境变量到子进程

    参考:node文档-child_process.spawn

    spawn 的子进程意外退出重启处理探究

    主进程 index.js

    const { spawn } = require('child_process');
    
    let worker;
    
    function serve () {
        worker = spawn(process.execPath, ['./test1.js'], {
            stdio: [0, 1, 2, 'ipc'],
            cwd: __dirname,
            env: process.env
        });
        worker.on('message', (...args) => {
            console.log('message', ...args);
        });
        worker.on('error', (...args) => {
            console.log('error', ...args);
        });
        worker.on('exit', (code, signal) => {
            console.log('exit', code, signal);
        });
        worker.on('disconnect', () => {
            console.log('disconnect');
        });
        // 基本子进程退出,都会触发 worker.on('close')
        // 因此可以在这里做一些子进程重启之类的事
        worker.on('close', (code, signal) => {
            console.log('close', code, signal);
            // serve();
        });
    
        // 先后触发 worker 的 disconnect exit close 事件, exit 参数为 null SIGTERM
        // setTimeout(() => {
        //     worker.kill();
        // }, 2000);
    }
    
    // process.exit :主进程会马上退出,不会影响子进程,要退出子进程需要另做处理
    // process.abort :不会影响子进程,要退出子进程需要另做处理
    // throw Error :不会影响子进程,要退出子进程需要另做处理
    // setTimeout(() => {
    //     // process.exit();
    //     // process.abort();
    //     // throw new Error('1231');
    // }, 10000);
    
    setInterval(() => {
        console.log(process.pid, '===');
    }, 1000);
    
    serve();

    子进程 test1.js

    process.on('uncaughtException', (error) => {
        console.log('worker uncaughtException', error);
        process.send({
            type: 'error',
            msg: error
        });
    });
    
    process.send('connected');
    
    setInterval(() => {
        console.log(process.pid, '---');
    }, 1000);
    
    // 会触发 worker.on('disconnect') ,主 子 进程都不会退出,但连接中断,不能使用 process.send ,会报错
    // setTimeout(() => {
    //     process.disconnect();
    //     // process.send('hello?');
    // }, 3000);
    
    // process.abort :先后触发 worker 的 disconnect exit close 事件, exit 及 close 参数为 null SIGABRT
    // process.exit :先后触发 worker 的 disconnect exit close 事件, exit 及 close 参数为 0 null
    // setTimeout(() => {
    //     // process.abort();
    //     // process.exit();
    // }, 10000);
    
    // 用 process.on('uncaughtException') 处理
    // setTimeout(() =>{
    //     throw new Error('123');
    // }, 1500);

    打包后程序执行调试技巧

    使用命令行运行程序

    进入打包后程序所在目录,假设程序名为“Test”,则执行命令 ./Test 即可执行程序,并且程序中所有控制台输出都会打印在该终端中。我们可以借助输出信息进行调试。

    asar

    当 windows 打包开启 asar 时,打包后文件资源被归档到一个 asar 档案文件。如果刚好我们需要调试的话,可能需要更改代码后重新打包、安装、运行。但实际上有更方便的方式。

    asar 提供命令行工具,通过命令行,我们可以打包、列出归档文件中的文件列表、解压某个单文件、解压整个档案文件等功能。

    因此,我们的打包后调试过程可以被简化为:

    • 关闭应用,解压打包后 asar 文件
    • 修改代码,然后将修改后的文件整个重新打包,重启应用

    参考:npm-asar

    杂项

    Linux 虚拟机的安装及常用环境搭建

    虚拟机安装:http://note.youdao.com/notesh...

    环境搭建:http://note.youdao.com/notesh...

    VS Code 调试 Electron 配置文件

    {
        "version": "0.2.0",
        "configurations": [
            {
                "name": "Electron",
                "type": "node",
                "request": "launch",
                "cwd": "${workspaceRoot}",
                "program": "${workspaceFolder}/server/index.js",
                "runtimeExecutable": "${workspaceRoot}/server/node_modules/.bin/electron",
                "windows": {
                    "runtimeExecutable": "${workspaceRoot}/server/node_modules/.bin/electron.cmd"
                },
                "args": [
                    "."
                ],
                "outputCapture": "std",
                "env": {
                    "NODE_ENV": "development"
                }
            }
        ]
    }

    使用硬链接管理文件资源引用关系

    文件链接有两种:硬链接与符号链接。

    硬链接直接指向数据,会增加该文件的 inode 计数,数据只会存在一份,所有指向该数据的链接是同步的。在文件系统中, inode 为 0 的数据会被删除。而符号链接是记录的该文件位置,并不会增加文件的 inode 计数,平时我们使用到的快捷方式就是符号链接,目标文件删除,链接并不会消失,但这个链接指向的资源却无法再找到。

    在项目中有这样一个问题:整个系统是围绕着文件资源进行的业务处理,各个模块是串联进行,上游输出是下游输入。但各个流程的资源需要允许用户隔离管理,本模块对某个资源的依赖不影响其他模块的删除。

    如果要做到相互不依赖,可能的办法有:每个环节都存一份文件。但是这种方式,会浪费大量的硬盘空间,数据同步也不好处理。因此最终我们选择使用硬链接来实现该部分需求。

    硬链接操作起来非常简单,在 node 中主要有以下几个操作:

    const fs = require('fs');
    fs.link(existingPath, newPath, callback); // 创建硬链接
    fs.unlink(path, callback); // 删除硬链接
    fs.stat(path[, options], callback).nlink; // 查看 inode 数

    参考:

    postgre 数据导出

    rm -f dlp.sql&&pg_dump -U 用户名 -d 数据库名 -f 文件名.sql -h 服务器host -p 端口 -s

    参考:pg_dump

    错误处理

    事件相关

    • 事件内抛出的错误会被外层捕获

      const { EventEmitter } = require('events');
      const event = new EventEmitter();
      event.on('test', () => {
          // throw new Error('1');
          try {
              throw new Error('2');
          } catch (e) {
              event.emit('error', e);
          }
      });
      event.on('error', (e) => {
          console.log(e); // Error: 2
      });
      try {
          event.emit('test');
      } catch (e) {
          console.log(e); // Error: 1
      }

    同步异步相关

    • Promise 中的错误处理

      await 非常关键,没有 awaittry 无法捕获到 catchthrow 的错误

      (async () => {
          try {
              let res = await new Promise(() => {
                  throw new Error(2);
              }).catch(error => {
                  if (error.message === '1') {
                      return Promise.resolve('haha');
                  } else throw error;
              });
              console.log(res);
          } catch (e) {
              debugger;
          }
      })();

    跨盘符移动文件

    function moveFileCrossDevice (source, target) {
        return new Promise((resolve, reject) => {
            try {
                if (!fs.existsSync(source)) reject(new BEKnownError('源文件不存在'));
                let readStream = fs.createReadStream(source);
                let writeStream = fs.createWriteStream(target);
                readStream.on('end',function(){
                    fs.unlinkSync(source);
                    resolve();
                });
                readStream.on('error', (error) => {
                    reject(error);
                });
                writeStream.on('error', (error) => {
                    reject(error);
                });
                readStream.pipe(writeStream);
            } catch (e) {
                reject(e);
            }
        });
    }

    docker 常用命令

    http://note.youdao.com/notesh...

    查看原文

    赞 3 收藏 3 评论 0

    码农视界周刊 分享了头条 · 2月25日

    全面分析了vue和react的差异,有效的帮助react开发者快速切换到vue,并且提高工作效率

    赞 0 收藏 0 评论 0

    码农视界周刊 回答了问题 · 2月23日

    解决点击按钮vue路由的页面跳不过来

    排查:
    1、第一次点击按钮之后,看看浏览器的url地址是否发生了变化,如果发生了变化,说明路由已经发生跳转,也就是 this.$router.push 完成了工作;从你的描述来看,你的这个跳转应该是发生了。
    2、浏览器地址发生了变化,但是页面没有出来,这说明你的 路由显示的地方写的有问题,可以参考项目中其他页面,或者自己使用了说明路由去看看官方文档

    浏览器错误:
    你的描述其实是有问题的,点击第一次不是没有反应,可能浏览器地址栏已经发生了变化,只是你自己不知道而已;

    当第一次点击之后,再次点击,vue-router就会 报错。因为他不允许相同路由 再 push 相同的路由

    关注 4 回答 4

    码农视界周刊 收藏了文章 · 2019-12-23

    uni-app微信小程序接入人脸核身SDK


    这几天使用uni-app开发某银行的一个微信小程序,需要集成接入腾讯云的人脸核身SDK,如上图所示,记录下整合接入过程及踩的一些坑,帮助后面需要的朋友们。关于uni-app接入人脸核身SDK有不懂的地方可以在下面提问,看到会及时回复。

    申请服务

    不是所有的企业都能够申请的,需要符合以下行业要求的客户才能申请
    政务:政府机构或事业单位
    金融:银行、保险
    医疗:公立医疗机构
    运营商:电信运营商
    教育:公立教育机构
    交通:航空、客运、网约车、交通卡、共享交通、轨道交通、租车
    旅游:酒店
    物流:快递、邮政、物流

    由于SDK会调用小程序原生的wx.startFacialRecognitionVerify方法,所以总共得申请2个服务
    SDK服务申请人脸核身服务
    小程序查看申请流程(需要发送邮件申请,使用该服务的小程序的appid,后面开发也是用的这个)
    重要的事情说3遍
    以上这2个服务都需要申请,缺一不可。
    以上这2个服务都需要申请,缺一不可。
    以上这2个服务都需要申请,缺一不可。

    下载SDK

    由于不是我申请的,所以怎么下载我也不知道,听群里的人说的是SDK腾讯云下发给客户的。

    SDK目录结构

    image.png

    SDK接入

    参考腾讯云文档的接入方法:https://cloud.tencent.com/document/product/1007/31071
    文档是针对原生小程序写的,所以页面引入的方法有所不同
    由于uni-app不支持直接引入小程序的原生页面,所以这里能想到的就是将它当作成一个微信小程序的组件,然后uni-app的页面引入这个组件

    解压引入SDK

    在uni-app项目中新建wxcomponents目录,将SDK解压后放到该目录
    image.png
    pages.jsonglobalStyle中全局引入小程序的组件,注意引用的路径

    "usingComponents": {
      "verify-mpsdk": "/wxcomponents/verify_mpsdk/index/index"
    }

    image.png

    新建人脸核身页面

    pages中新建人脸核身的页面face(名字可以随意,根据自己的需要起名),
    pages.json中配置页面
    image.png
    face页面中引入verify-mpsdk组件
    image.png
    最终的人脸核身的页面访问就是/pages/face/face

    初始化SDK

    在需要的页面初始化SDK,如有个页面需要点击按钮进行人脸核身,就在这个页面进行初始化。
    这个直接照着文档快速入门中的来就行了,这里就直接使用uni-app默认的index页面,
    适当修改下即可,大概代码如下:

    <template>
      <view class="content">
        <button type="primary"
          @tap="gotoVerify">
          进入人脸核身
        </button>
      </view>
    </template>
    
    <script>
        export default {
            data() {
                return {
                    BizToken: ''
                }
            },
            onLoad() {
                // 初始化慧眼实名核身组件
                const Verify = require('@/wxcomponents/verify_mpsdk/main.js')
                Verify.init()
            },
            methods: {
                // 单击进入人脸核身按钮时,触发该函数
                gotoVerify () {
                    this.BizToken = '' // 这里需要我们去客户后端调用DetectAuth接口获取BizToken
                    // 调用实名核身功能
                    wx.startVerify({
                            data: {
                                token: this.BizToken // BizToken
                            },
                            success: (res) => { // 验证成功后触发
                                    // res 包含验证成功的token, 这里需要加500ms延时,防止iOS下不执行后面的逻辑
                                    setTimeout(() => {
                                        // 验证成功后,拿到token后的逻辑处理,具体以客户自身逻辑为准
                                        console.log(res)
                                    }, 500)
                            },
                            fail: (err) => {  // 验证失败时触发
                                    // err 包含错误码,错误信息,弹窗提示错误
                                    setTimeout(() => {
                                            console.log(err)
                                            wx.showModal({
                                                title: "提示",
                                                content: err.ErrorMsg,
                                                showCancel: false
                                            })
                                    }, 500)
                            }
                    })
                }
            }
        }
    </script>

    注意下这里的BizToken,需要调用后端服务接口来获取,
    需要后端的同学调用腾讯云提供的DetectAuth来返回前端需要的BizToken
    调试开发阶段我们可以先通过腾讯云提供的工具
    API 3.0 Explorer
    直接来获取这个BizToken
    如果服务申请成功后控制台一般能找到SecretIdSecretKeyRuleId
    注意EndpointRegion选择的地区得保持和申请时选择的地区一致。
    填写完成后点击在线调用中的发送请求按钮,如果填的都对的话返回信息里面会有BizToken
    拿到BizToken后就可以直接使用了,修改下上面的代码:
    xxxxxxxxxxxxxxxxx就是拿到的BizToken

    this.BizToken = 'xxxxxxxxxxxxxxxxx' // 这里需要我们去客户后端调用DetectAuth接口获取BizToken

    image.png

    开发调试

    上面都做完后就可以进行调试了
    需要先在项目中manifest.json中配置上小程序的appid,这个appid就是上面申请服务中的appid,不然无法开启调试。
    image.png
    然后运行到微信开发工具(这里就不多说了),如果提示不是开发人员,就让该appid的管理员将你加到开发组里面就行了。
    运行成功后点击开发者工具的真机调试,扫描二维码开启真机调试模式。
    接下来就是踩坑了,会出现各种问题。

    踩坑及解决方法

    Component is not found in path

    这里开发者工具里面都是显示正常的,不会报这个错,
    手机扫码进入调试后控制台会出现这个报错,
    提示组件找不到,但是我们的路径都是对的,
    Component is not found in path "wxcomponents/verify_mpsdk/index/index"
    image.png
    问题出在这里将verify_mpsdk当成自定义组件了,
    小程序自定义组件引入的时候需要在文件JSON中指定"component": true
    找到wxcomponents\verify_mpsdk\index\index.json文件,加入"component": true即可
    重新开启调试扫码后上面的报错就没了。 
    image.png

    navigateTo:fail page

    点击按钮调用gotoVerify后会报一个页面找不到的错
    navigateTo:fail page "verify_mpsdk/index/index?isNotice=false" is not found
    image.png
    SDK默认的是跳转验证页面的地址是verify_mpsdk/index/index
    文档找了半天也没找到相应的配置地址,最后在SDK里面搜索找到了这个地址。
    所以只需要把这个地址改成我们所需要的地址就行了。
    找到wxcomponents\verify_mpsdk\main.js,里面搜索verify_mpsdk/index/index,
    找到后修改成上面人脸核身页面的地址pages/face/face
    保存后重试就能跳转到人脸核身的页面了。

    无操作、无报错大坑

    进入人脸核身的页面后会发现啥操作都没,控制台也没报错,
    image.png
    一度认为我自己弄的有问题,搞了好久也没弄好,也提了个工单(腾讯云工单反馈率还是很快的,几分钟后就有人回复了,这点赞一个),
    将代码和相关操作在工单里描述了下,对方也觉得的没问题,按照快速入门的代码应该是没问题的,对方也没找到啥问题,就让我加了一个腾讯云慧眼小助手的微信,
    本想着下午加人家看看啥问题的,中午吃完饭闲着的时候将SDK里面的文件都格式化后终于在index.js里面找到问题所在了。
    wxcomponents\verify_mpsdk\index\index.js文件中有个onLoad生命周期,
    image.png
    正常原生微信小程序进入到这个页面的时候会执行onLoad里面的代码,
    但是我们上面将这个SDK当作是一个自定义组件了,
    在uni-app中组件是不存在onLoad这个生命周期的,这个是页面所属的生命周期。
    找到问题所在就好解决了,我们可以在人脸核身的页面pages/face/face手动执行onLoad
    修改下pages/face/face的代码,如下:

    <template>
      <view class="face">
        <verify-mpsdk ref="verifyMpsdk"></verify-mpsdk>
      </view>
    </template>
    
    <script>
        export default {
            data() {
                return {
                    
                }
            },
            onLoad(i) {
                // 页面onLoad的时候手动调用
                this.$refs.verifyMpsdk.onLoad(i)
            }
        }
    </script>

    保存后重试,就能正常显示了
    image.png

    SDK图片异常

    点击快速验证进入下一步及后面的步骤的时候发现,页面的图片都挂掉了不显示,
    一开始我一直用的真机调试,页面上也不会出现破图,控制台也不会报图片异常的错误,
    导致我不知道怎么进行拍摄身份证,以为会自动识别身份证然后自动下一步,
    最后在开发者工具里面跑了一遍才知道是图片找不到了,然后拍照的图片按钮自然也就显示不了了。
    image.png
    image.png
    最后在SDK里面搜索/verify_mpsdk/images,在下面文件中找到关键词,
    wxcomponents\verify_mpsdk\templates\ocr\ocr.wxml
    image.png
    既然这种形式导致运行的时候图片找不到,我们可以把SDK所用的图片都复制到项目的static目录里
    static中新建verify_mpsdk目录,将SDK中的图片即wxcomponents\verify_mpsdk\images
    复制到static\verify_mpsdk中,最终形成以下目录形式
    image.png
    最后将wxcomponents\verify_mpsdk\templates\ocr\ocr.wxml中的/verify_mpsdk/images批量替换成
    /static/verify_mpsdk/images后重试即可,然后就都正常了。
    image.png
    image.png

    完整流程

    最后用真机调试完整跑一把
    image.png
    image.png
    image.png
    image.png

    备注:如果最上面的wx.startFacialRecognitionVerify服务没有申请到此时点击下一步的会弹出一个无权限的弹窗无法进行下一步

    image.png
    image.png

    这里就是活体人脸检测了,需要将脸对准框框,点击开始后需要读几个数字,

    image.png

    最后验证通过后会回到之前的页面(调用gotoVerify()方法的页面),
    验证成功后,会拿到一个BizToken
    可以在wx.startVerify回调函数success中打印自行查看。
    拿到BizToken后可以调用后端的接口,后端通过调用 GetDetectInfo 接口获取并返回本次核身的详细信息,包括身份证上的信息和身份证证图片等信息。
    前端拿到这些信息后根据自己的程序需要做处理。

    结语

    整合过程中遇到不少问题,百度加google也找不到相关的详细信息,
    人脸核身的相关文档都很简单,出现问题后无从下手,只能慢慢自己摸索解决了,
    最后写篇文章记录下整个过程,也能帮到后面需要集成这个SDK的朋友们。

    查看原文

    码农视界周刊 赞了文章 · 2019-12-23

    uni-app微信小程序接入人脸核身SDK


    这几天使用uni-app开发某银行的一个微信小程序,需要集成接入腾讯云的人脸核身SDK,如上图所示,记录下整合接入过程及踩的一些坑,帮助后面需要的朋友们。关于uni-app接入人脸核身SDK有不懂的地方可以在下面提问,看到会及时回复。

    申请服务

    不是所有的企业都能够申请的,需要符合以下行业要求的客户才能申请
    政务:政府机构或事业单位
    金融:银行、保险
    医疗:公立医疗机构
    运营商:电信运营商
    教育:公立教育机构
    交通:航空、客运、网约车、交通卡、共享交通、轨道交通、租车
    旅游:酒店
    物流:快递、邮政、物流

    由于SDK会调用小程序原生的wx.startFacialRecognitionVerify方法,所以总共得申请2个服务
    SDK服务申请人脸核身服务
    小程序查看申请流程(需要发送邮件申请,使用该服务的小程序的appid,后面开发也是用的这个)
    重要的事情说3遍
    以上这2个服务都需要申请,缺一不可。
    以上这2个服务都需要申请,缺一不可。
    以上这2个服务都需要申请,缺一不可。

    下载SDK

    由于不是我申请的,所以怎么下载我也不知道,听群里的人说的是SDK腾讯云下发给客户的。

    SDK目录结构

    image.png

    SDK接入

    参考腾讯云文档的接入方法:https://cloud.tencent.com/document/product/1007/31071
    文档是针对原生小程序写的,所以页面引入的方法有所不同
    由于uni-app不支持直接引入小程序的原生页面,所以这里能想到的就是将它当作成一个微信小程序的组件,然后uni-app的页面引入这个组件

    解压引入SDK

    在uni-app项目中新建wxcomponents目录,将SDK解压后放到该目录
    image.png
    pages.jsonglobalStyle中全局引入小程序的组件,注意引用的路径

    "usingComponents": {
      "verify-mpsdk": "/wxcomponents/verify_mpsdk/index/index"
    }

    image.png

    新建人脸核身页面

    pages中新建人脸核身的页面face(名字可以随意,根据自己的需要起名),
    pages.json中配置页面
    image.png
    face页面中引入verify-mpsdk组件
    image.png
    最终的人脸核身的页面访问就是/pages/face/face

    初始化SDK

    在需要的页面初始化SDK,如有个页面需要点击按钮进行人脸核身,就在这个页面进行初始化。
    这个直接照着文档快速入门中的来就行了,这里就直接使用uni-app默认的index页面,
    适当修改下即可,大概代码如下:

    <template>
      <view class="content">
        <button type="primary"
          @tap="gotoVerify">
          进入人脸核身
        </button>
      </view>
    </template>
    
    <script>
        export default {
            data() {
                return {
                    BizToken: ''
                }
            },
            onLoad() {
                // 初始化慧眼实名核身组件
                const Verify = require('@/wxcomponents/verify_mpsdk/main.js')
                Verify.init()
            },
            methods: {
                // 单击进入人脸核身按钮时,触发该函数
                gotoVerify () {
                    this.BizToken = '' // 这里需要我们去客户后端调用DetectAuth接口获取BizToken
                    // 调用实名核身功能
                    wx.startVerify({
                            data: {
                                token: this.BizToken // BizToken
                            },
                            success: (res) => { // 验证成功后触发
                                    // res 包含验证成功的token, 这里需要加500ms延时,防止iOS下不执行后面的逻辑
                                    setTimeout(() => {
                                        // 验证成功后,拿到token后的逻辑处理,具体以客户自身逻辑为准
                                        console.log(res)
                                    }, 500)
                            },
                            fail: (err) => {  // 验证失败时触发
                                    // err 包含错误码,错误信息,弹窗提示错误
                                    setTimeout(() => {
                                            console.log(err)
                                            wx.showModal({
                                                title: "提示",
                                                content: err.ErrorMsg,
                                                showCancel: false
                                            })
                                    }, 500)
                            }
                    })
                }
            }
        }
    </script>

    注意下这里的BizToken,需要调用后端服务接口来获取,
    需要后端的同学调用腾讯云提供的DetectAuth来返回前端需要的BizToken
    调试开发阶段我们可以先通过腾讯云提供的工具
    API 3.0 Explorer
    直接来获取这个BizToken
    如果服务申请成功后控制台一般能找到SecretIdSecretKeyRuleId
    注意EndpointRegion选择的地区得保持和申请时选择的地区一致。
    填写完成后点击在线调用中的发送请求按钮,如果填的都对的话返回信息里面会有BizToken
    拿到BizToken后就可以直接使用了,修改下上面的代码:
    xxxxxxxxxxxxxxxxx就是拿到的BizToken

    this.BizToken = 'xxxxxxxxxxxxxxxxx' // 这里需要我们去客户后端调用DetectAuth接口获取BizToken

    image.png

    开发调试

    上面都做完后就可以进行调试了
    需要先在项目中manifest.json中配置上小程序的appid,这个appid就是上面申请服务中的appid,不然无法开启调试。
    image.png
    然后运行到微信开发工具(这里就不多说了),如果提示不是开发人员,就让该appid的管理员将你加到开发组里面就行了。
    运行成功后点击开发者工具的真机调试,扫描二维码开启真机调试模式。
    接下来就是踩坑了,会出现各种问题。

    踩坑及解决方法

    Component is not found in path

    这里开发者工具里面都是显示正常的,不会报这个错,
    手机扫码进入调试后控制台会出现这个报错,
    提示组件找不到,但是我们的路径都是对的,
    Component is not found in path "wxcomponents/verify_mpsdk/index/index"
    image.png
    问题出在这里将verify_mpsdk当成自定义组件了,
    小程序自定义组件引入的时候需要在文件JSON中指定"component": true
    找到wxcomponents\verify_mpsdk\index\index.json文件,加入"component": true即可
    重新开启调试扫码后上面的报错就没了。 
    image.png

    navigateTo:fail page

    点击按钮调用gotoVerify后会报一个页面找不到的错
    navigateTo:fail page "verify_mpsdk/index/index?isNotice=false" is not found
    image.png
    SDK默认的是跳转验证页面的地址是verify_mpsdk/index/index
    文档找了半天也没找到相应的配置地址,最后在SDK里面搜索找到了这个地址。
    所以只需要把这个地址改成我们所需要的地址就行了。
    找到wxcomponents\verify_mpsdk\main.js,里面搜索verify_mpsdk/index/index,
    找到后修改成上面人脸核身页面的地址pages/face/face
    保存后重试就能跳转到人脸核身的页面了。

    无操作、无报错大坑

    进入人脸核身的页面后会发现啥操作都没,控制台也没报错,
    image.png
    一度认为我自己弄的有问题,搞了好久也没弄好,也提了个工单(腾讯云工单反馈率还是很快的,几分钟后就有人回复了,这点赞一个),
    将代码和相关操作在工单里描述了下,对方也觉得的没问题,按照快速入门的代码应该是没问题的,对方也没找到啥问题,就让我加了一个腾讯云慧眼小助手的微信,
    本想着下午加人家看看啥问题的,中午吃完饭闲着的时候将SDK里面的文件都格式化后终于在index.js里面找到问题所在了。
    wxcomponents\verify_mpsdk\index\index.js文件中有个onLoad生命周期,
    image.png
    正常原生微信小程序进入到这个页面的时候会执行onLoad里面的代码,
    但是我们上面将这个SDK当作是一个自定义组件了,
    在uni-app中组件是不存在onLoad这个生命周期的,这个是页面所属的生命周期。
    找到问题所在就好解决了,我们可以在人脸核身的页面pages/face/face手动执行onLoad
    修改下pages/face/face的代码,如下:

    <template>
      <view class="face">
        <verify-mpsdk ref="verifyMpsdk"></verify-mpsdk>
      </view>
    </template>
    
    <script>
        export default {
            data() {
                return {
                    
                }
            },
            onLoad(i) {
                // 页面onLoad的时候手动调用
                this.$refs.verifyMpsdk.onLoad(i)
            }
        }
    </script>

    保存后重试,就能正常显示了
    image.png

    SDK图片异常

    点击快速验证进入下一步及后面的步骤的时候发现,页面的图片都挂掉了不显示,
    一开始我一直用的真机调试,页面上也不会出现破图,控制台也不会报图片异常的错误,
    导致我不知道怎么进行拍摄身份证,以为会自动识别身份证然后自动下一步,
    最后在开发者工具里面跑了一遍才知道是图片找不到了,然后拍照的图片按钮自然也就显示不了了。
    image.png
    image.png
    最后在SDK里面搜索/verify_mpsdk/images,在下面文件中找到关键词,
    wxcomponents\verify_mpsdk\templates\ocr\ocr.wxml
    image.png
    既然这种形式导致运行的时候图片找不到,我们可以把SDK所用的图片都复制到项目的static目录里
    static中新建verify_mpsdk目录,将SDK中的图片即wxcomponents\verify_mpsdk\images
    复制到static\verify_mpsdk中,最终形成以下目录形式
    image.png
    最后将wxcomponents\verify_mpsdk\templates\ocr\ocr.wxml中的/verify_mpsdk/images批量替换成
    /static/verify_mpsdk/images后重试即可,然后就都正常了。
    image.png
    image.png

    完整流程

    最后用真机调试完整跑一把
    image.png
    image.png
    image.png
    image.png

    备注:如果最上面的wx.startFacialRecognitionVerify服务没有申请到此时点击下一步的会弹出一个无权限的弹窗无法进行下一步

    image.png
    image.png

    这里就是活体人脸检测了,需要将脸对准框框,点击开始后需要读几个数字,

    image.png

    最后验证通过后会回到之前的页面(调用gotoVerify()方法的页面),
    验证成功后,会拿到一个BizToken
    可以在wx.startVerify回调函数success中打印自行查看。
    拿到BizToken后可以调用后端的接口,后端通过调用 GetDetectInfo 接口获取并返回本次核身的详细信息,包括身份证上的信息和身份证证图片等信息。
    前端拿到这些信息后根据自己的程序需要做处理。

    结语

    整合过程中遇到不少问题,百度加google也找不到相关的详细信息,
    人脸核身的相关文档都很简单,出现问题后无从下手,只能慢慢自己摸索解决了,
    最后写篇文章记录下整个过程,也能帮到后面需要集成这个SDK的朋友们。

    查看原文

    赞 30 收藏 22 评论 8

    码农视界周刊 回答了问题 · 2019-12-20

    vuejs对“暂无数据”提示的闪现怎么处理?

    // 组件
    data(){
        return {
            data:null // 设置默认值为null
        }
    }
    
    // template
    <div v-show="data != null">
        <div v-if="data.length>0">
        </div>
        <div v-else> 暂无数据 </div>
    </div>

    另外,需要注意后端没有数据的时候的返回,如果后端在没有数据的时候返回 null,你前端需要简单处理下
    this.data = api.data || []

    注意上面是伪代码,api.data 标示服务的返回

    关注 5 回答 5

    码农视界周刊 回答了问题 · 2019-12-19

    react里动态添加class后css没加载?

    也不知你问题在哪

    1、不要看代码,直接在浏览器里面看看对应的 class 是不是有
    2、确保 dom 上的 class 是正确的

    测试方式:可以手动在浏览器里面修改 dom的class属性,看看是否能生效

    这个如果像你说的那样, class 已经被加到了 dom上,但是样式没出来; 那么就和 react 没关系

    关注 2 回答 2

    码农视界周刊 回答了问题 · 2019-12-19

    axios请求参数问题

    哈哈,
    java 里面还有 long float double,到底该是哪一个呢?

    关注 4 回答 4

    认证与成就

    • 获得 114 次点赞
    • 获得 13 枚徽章 获得 1 枚金徽章, 获得 2 枚银徽章, 获得 10 枚铜徽章

    擅长技能
    编辑

    开源项目 & 著作
    编辑

    (゚∀゚ )
    暂时没有

    注册于 2017-12-21
    个人主页被 1.2k 人浏览