odoo16前端框架源码阅读——rpc_service.js

odoo16前端框架源码阅读——rpc_service.js

先介绍点背景知识,这样方便阅读代码。

一、 JSONRPC的规范

https://www.jsonrpc.org/specification

中文翻译版本:https://wiki.geekdream.com/Specification/json-rpc_2.0.html

JSON-RPC是一个无状态且轻量级的远程过程调用(RPC)协议。 本规范主要定义了一些数据结构及其相关的处理规则。它允许运行在基于socket,http等诸多不同消息传输环境的同一进程中。其使用JSON(RFC 4627)作为数据格式。

它为简单而生!

由于JSON-RPC使用JSON,它具有与其相同的类型系统(见http://www.json.org或RFC 4627)。JSON可以表示四个基本类型(String、Numbers、Booleans和Null)和两个结构化类型(Objects和Arrays)。 规范中,术语“Primitive”标记那4种原始类型,“Structured”标记两种结构化类型。任何时候文档涉及JSON数据类型,第一个字母都必须大写:Object,Array,String,Number,Boolean,Null。包括True和False也要大写。

1、请求对象

发送一个请求对象至服务端代表一个rpc调用, 一个请求对象包含下列成员:

jsonrpc

指定JSON-RPC协议版本的字符串,必须准确写为“2.0”

method

包含所要调用方法名称的字符串,以rpc开头的方法名,用英文句号(U+002E or ASCII 46)连接的为预留给rpc内部的方法名及扩展名,且不能在其他地方使用。

params

调用方法所需要的结构化参数值,该成员参数可以被省略。

id

已建立客户端的唯一标识id,值必须包含一个字符串、数值或NULL空值。如果不包含该成员则被认定为是一个通知。该值一般不为NULL[1],若为数值则不应该包含小数[2]。

服务端必须回答相同的值如果包含在响应对象。 这个成员用来两个对象之间的关联上下文。

没有包含“id”成员的请求对象为通知, 作为通知的请求对象表明客户端对相应的响应对象并不感兴趣

    const data = {
        id: rpcId,
        jsonrpc: "2.0",
        method: "call",
        params: params,
    };

2、响应对象

当发起一个rpc调用时,除通知之外,服务端都必须回复响应。响应表示为一个JSON对象,使用以下成员:

jsonrpc

指定JSON-RPC协议版本的字符串,必须准确写为“2.0”

result

该成员在成功时必须包含。

当调用方法引起错误时必须不包含该成员。

服务端中的被调用方法决定了该成员的值。

error

该成员在失败是必须包含。

当没有引起错误的时必须不包含该成员。

该成员参数值必须为5.1中定义的对象。

id

该成员必须包含。

该成员值必须于请求对象中的id成员值一致。

若在检查请求对象id时错误(例如参数错误或无效请求),则该值必须为空值。

响应对象必须包含result或error成员,但两个成员必须不能同时包含。

3、错误对象

当一个rpc调用遇到错误时,返回的响应对象必须包含错误成员参数,并且为带有下列成员参数的对象:

code

使用数值表示该异常的错误类型。 必须为整数。

message

对该错误的简单描述字符串。 该描述应尽量限定在简短的一句话。

data

包含关于错误附加信息的基本类型或结构化类型。该成员可忽略。 该成员值由服务端定义(例如详细的错误信息,嵌套的错误等)。

code	message	meaning
-32700	Parse error语法解析错误	服务端接收到无效的json。该错误发送于服务器尝试解析json文本
-32600	Invalid Request无效请求	发送的json不是一个有效的请求对象。
-32601	Method not found找不到方法	该方法不存在或无效
-32602	Invalid params无效的参数	无效的方法参数。
-32603	Internal error内部错误	JSON-RPC内部错误。
-32000 to -32099	Server error服务端错误	预留用于自定义的服务器错误。

二、rpc_service.js

路径:addons\web\static\src\core\network\rpc_service.js

1、引入相关模块, 新建了相关的Error类

导入了两个js模块, browser估计是跟浏览器相关

registry 是前端的注册表

然后是定义了四个Error继承自己标准类Error

  • RPCError
  • ConnectionLostError
  • ConnectionAbortedError
  • HTTPError
/** @odoo-module **/

import { browser } from "../browser/browser";
import { registry } from "../registry";

// -----------------------------------------------------------------------------
// Errors
// -----------------------------------------------------------------------------
export class RPCError extends Error {
    constructor() {
        super(...arguments);
        this.name = "RPC_ERROR";
        this.type = "server";
        this.code = null;
        this.data = null;
        this.exceptionName = null;
        this.subType = null;
    }
}

export class ConnectionLostError extends Error {}

export class ConnectionAbortedError extends Error {}

export class HTTPError extends Error {}

2、Error对象

根据响应值来返回一个RPCError,这句结构赋值挺有意思

const { code, data: errorData, message, type: subType } = reponse;

我猜是吧reponse中的4个属性 code,data,message,type 分别赋值给了code,errorData,message,subType,有两个变量名称做了替换。有啥必要吗?

// -----------------------------------------------------------------------------
// Main RPC method
// -----------------------------------------------------------------------------
export function makeErrorFromResponse(reponse) {
    // Odoo returns error like this, in a error field instead of properly
    // using http error codes...
    
    const error = new RPCError();
    error.exceptionName = errorData.name;
    error.subType = subType;
    error.data = errorData;
    error.message = message;
    error.code = code;
    return error;
}


3、定义相关变量

5个输入参数

  1. env 前端环境,似乎在前端js代码中,直接可以使用这个对象。
  2. rpcId id, 为了跟响应对应起来,所以需要这个id
  3. url 地址
  4. params 参数值
  5. setting 默认是一个空对象
export function jsonrpc(env, rpcId, url, params, settings = {}) {
    const bus = env.bus;
    const XHR = browser.XMLHttpRequest;
    const data = {
        id: rpcId,
        jsonrpc: "2.0",
        method: "call",
        params: params,
    };
    const request = settings.xhr || new XHR();
    let rejectFn;

参数看着有点多,其实在后面封装成service的时候, 前两个参数是不用传的,只需要传递后面三个就行, 其实params和setting都可以不用传,必须要传的只有url。

bus: 总线, 这里要通过总线发送一些信号,rpc毕竟是远程调用,难免有意外情况发生,所以需要bus进行通信。

XHR: 浏览器发起request请求

data: 标准的jsonrpc数据格式

rejectFn, 这里值得一提,在这里声明这个变量,但是并没有赋值,是为了后面使用。

4、复习promise

promise是ES6引入的异步编程的新解决方案,语法上Promise是一个构造函数,用来封装异步操作并可以获取成功或失败的结果。

// new Promise 生成一个异步任务,参数是具体执行任务的函数,接收两个参数
// 一般叫resolve和reject都是函数,异步任务执行成功调用前者,否则调用后者
// 这两个方法将改变p对象的状态,同时给下一步处理传递数据
// 然后调用p.then  ,接收两个函数型参数,分别对应异步任务成功的回调和失败的回调
const p = new Promise(function(resolve,reject){
    setTimeout(function () {
        // let data="数据库中的用户数据";
        // resolve(data);
        let err="数据读取失败";
        reject(err);
    },1000);
})

p.then(function(value){
    console.log(value);
},function(reason){
    console.error(reason);
})

// 其实promise解决的也是回调地狱,嵌套过多的问题,将异步任务封装成对象了

其实,promise分了两步来完成,对象本身只是执行了一个异步任务,并没有处理异步返回的结果。 异步任务返回的结果是在p.then函数中处理的。
异步任务的返回值会影响promise对象本身的状态,会决定p.then中执行哪个回调函数。

另外,就是promise可以链式调用,执行串行的异步任务。

5、promise 中的Bus

在promise里面往总线里发送了很多消息

        if (!settings.silent) {
            bus.trigger("RPC:REQUEST", data.id);
        }

这里既然发了,那么就一定有地方接收,在odoo中搜索一下RPC:REQUEST

addons\web\static\src\webclient\loading_indicator\loading_indicator.js

这个文件代码不长,直接贴过来好了。注释中大概的意思是:

加载指示器:

当用户执行一个动作,最好是给他一些反馈说明当前有些事情正在发生。 这个指示器的作用就是在屏幕的右下角显示一个小的矩形框,里面有Loading字样,并且还有rpc的id。 3秒之后,如果rpc依然没有完成,我们将阻塞整个UI。 回头测试一下。

/** @odoo-module **/

import { browser } from "@web/core/browser/browser";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { Transition } from "@web/core/transition";

import { Component, onWillDestroy, useState } from "@odoo/owl";

/**
 * Loading Indicator
 *
 * When the user performs an action, it is good to give him some feedback that
 * something is currently happening.  The purpose of the Loading Indicator is to
 * display a small rectangle on the bottom right of the screen with just the
 * text 'Loading' and the number of currently running rpcs.
 *
 * After a delay of 3s, if a rpc is still not completed, we also block the UI.
 */
export class LoadingIndicator extends Component {
    setup() {
        this.uiService = useService("ui");
        this.state = useState({
            count: 0,
            show: false,
        });
        this.rpcIds = new Set();
        this.shouldUnblock = false;
        this.startShowTimer = null;
        this.blockUITimer = null;
        this.env.bus.addEventListener("RPC:REQUEST", this.requestCall.bind(this));
        this.env.bus.addEventListener("RPC:RESPONSE", this.responseCall.bind(this));
        onWillDestroy(() => {
            this.env.bus.removeEventListener("RPC:REQUEST", this.requestCall.bind(this));
            this.env.bus.removeEventListener("RPC:RESPONSE", this.responseCall.bind(this));
        });
    }

    requestCall({ detail: rpcId }) {
        if (this.state.count === 0) {
            browser.clearTimeout(this.startShowTimer);
            this.startShowTimer = browser.setTimeout(() => {
                if (this.state.count) {
                    this.state.show = true;
                    this.blockUITimer = browser.setTimeout(() => {
                        this.shouldUnblock = true;
                        this.uiService.block();
                    }, 3000);
                }
            }, 250);
        }
        this.rpcIds.add(rpcId);
        this.state.count++;
    }

    responseCall({ detail: rpcId }) {
        this.rpcIds.delete(rpcId);
        this.state.count = this.rpcIds.size;
        if (this.state.count === 0) {
            browser.clearTimeout(this.startShowTimer);
            browser.clearTimeout(this.blockUITimer);
            this.state.show = false;
            if (this.shouldUnblock) {
                this.uiService.unblock();
                this.shouldUnblock = false;
            }
        }
    }
}

LoadingIndicator.template = "web.LoadingIndicator";
LoadingIndicator.components = { Transition };

registry.category("main_components").add("LoadingIndicator", {
    Component: LoadingIndicator,
});

6、request 绑定load事件

request绑定了一个load事件,也就是请求返回的时候触发的操作

        request.addEventListener("load", () => {
            if (request.status === 502) {
                // If Odoo is behind another server (eg.: nginx)
                if (!settings.silent) {
                    bus.trigger("RPC:RESPONSE", data.id);
                }
                reject(new ConnectionLostError());
                return;
            }
            let params;
            try {
                params = JSON.parse(request.response);
            } catch (_) {
                // the response isn't json parsable, which probably means that the rpc request could
                // not be handled by the server, e.g. PoolError('The Connection Pool Is Full')
                if (!settings.silent) {
                    bus.trigger("RPC:RESPONSE", data.id);
                }
                return reject(new ConnectionLostError());
           
            const { error: responseError, result: responseResult } = params;
            if (!settings.silent) {
                bus.trigger("RPC:RESPONSE", data.id);
            }
            if (!responseError) {
                return resolve(responseResult);
            }
            const error = makeErrorFromResponse(responseError);
            reject(error);
        });

6.1、status = 502 是个什么鬼?

502 Bad Gateway错误是指代理或网关从上一个服务器接收到的响应无效或不完整。通常,这种情况发生在文件太大或处理速度太慢的高流量网站上。例如,当您访问一个具有高流量的网站时,您的请求将被发送到它的代理服务器。如果代理服务器在尝试访问网站时无法从上游服务器获取完整的响应,则会生成502错误代码。

502错误代码通常是由代理服务器、网关或负载均衡器等设备导致的,而不是由您的计算机或网络连接引起的。这意味着您只能为自己的网络连接做些有限的调整,但无法修复网关响应错误。

注释中也写的明白,502可能是因为使用了nginx反向代理,而 502错误是nginx和odoo通讯不佳造成的,这种情况rpc执行失败,

执行这一句

 reject(new ConnectionLostError());

6.2 解析返回值

如果返回的不是json格式的数据,也会触发错误

 try {
        params = JSON.parse(request.response);
     } catch (_) {
                return reject(new ConnectionLostError());
                 }

catch 后面的这个下划线是什么鬼? 可能是并不关心发生了什么错误,只要解析错误,就调用reject

6.3 解构赋值

const { error: responseError, result: responseResult } = params;

根据jsonrpc规范, error和result 必须并且只能返回一个。

后面也做了判断

        if (!responseError) {
            return resolve(responseResult);
        }

如果没有错误,那就调用resolve,并返回。否则说明有错误发生,先生成一个error,然后调用reject

            const error = makeErrorFromResponse(responseError);
            reject(error);

7、request绑定 error事件

        request.addEventListener("error", () => {
            if (!settings.silent) {
                bus.trigger("RPC:RESPONSE", data.id);
            }
            reject(new ConnectionLostError());
        });

8、 该干正事了

        request.open("POST", url);
        request.setRequestHeader("Content-Type", "application/json");
        request.send(JSON.stringify(data));

三行代码:

1、用post方法请求的url,为什么不用get? 因为post更安全

2、指定了Content-Type为json, 这个很重要,如果不指定,服务器端不知道怎么解析数据

3、将data转成字符串并发送出去。(忙活半天,就是为了这句)

9、定义了promise.abort

Promise只有三种状态:pending、resolve、reject,一个异步的承诺一旦发出,经历等待(pending)后,最终只能为成功或者失败,中途无法取消(abort)。

这里定义promise.abort,注释也讲的明白,允许用户取消被忽略的rpc请求来接触对UI的阻塞并且不要显示错误。

        /**
     * @param {Boolean} rejectError Returns an error if true. Allows you to cancel
     *                  ignored rpc's in order to unblock the ui and not display an error.
     */
    promise.abort = function (rejectError = true) {
        if (request.abort) {
            request.abort();
        }
        if (!settings.silent) {
            bus.trigger("RPC:RESPONSE", data.id);
        }
        if (rejectError) {
            rejectFn(new ConnectionAbortedError("XmlHttpRequestError abort"));
        }
    };

10、jsonrpc

这个函数就干了一件事,定义了一个promise对象来发送rpc请求,并把它返回。

export function jsonrpc(env, rpcId, url, params, settings = {}) {
    const bus = env.bus;
    const XHR = browser.XMLHttpRequest;
    const data = {
        id: rpcId,
        jsonrpc: "2.0",
        method: "call",
        params: params,
    };
    const request = settings.xhr || new XHR();
    let rejectFn;
    const promise = new Promise((resolve, reject) => {
        rejectFn = reject;
        // handle success
        request.addEventListener("load", () => {

        });
        // handle failure
        request.addEventListener("error", () => {

        });
   
        request.open("POST", url);
        request.setRequestHeader("Content-Type", "application/json");
        request.send(JSON.stringify(data));
    });

    promise.abort = function (rejectError = true) {

    };
    return promise;
}

11、定义RPC服务

这里对jsonrpc做了进一步封装,并且注册为服务,看来每个服务都有个start函数,而且将env作为参数传进去。

// -----------------------------------------------------------------------------
// RPC service
// -----------------------------------------------------------------------------
export const rpcService = {
    async: true,
    start(env) {
        let rpcId = 0;
        return function rpc(route, params = {}, settings) {
            return jsonrpc(env, rpcId++, route, params, settings);
        };
    },
};

registry.category("services").add("rpc", rpcService);

附录: odoo16 rpc_service.js

/** @odoo-module **/

import { browser } from "../browser/browser";
import { registry } from "../registry";

// -----------------------------------------------------------------------------
// Errors
// -----------------------------------------------------------------------------
export class RPCError extends Error {
    constructor() {
        super(...arguments);
        this.name = "RPC_ERROR";
        this.type = "server";
        this.code = null;
        this.data = null;
        this.exceptionName = null;
        this.subType = null;
    }
}

export class ConnectionLostError extends Error {}

export class ConnectionAbortedError extends Error {}

export class HTTPError extends Error {}

// -----------------------------------------------------------------------------
// Main RPC method
// -----------------------------------------------------------------------------
export function makeErrorFromResponse(reponse) {
    // Odoo returns error like this, in a error field instead of properly
    // using http error codes...
    const { code, data: errorData, message, type: subType } = reponse;
    const error = new RPCError();
    error.exceptionName = errorData.name;
    error.subType = subType;
    error.data = errorData;
    error.message = message;
    error.code = code;
    return error;
}

export function jsonrpc(env, rpcId, url, params, settings = {}) {
    const bus = env.bus;
    const XHR = browser.XMLHttpRequest;
    const data = {
        id: rpcId,
        jsonrpc: "2.0",
        method: "call",
        params: params,
    };
    const request = settings.xhr || new XHR();
    let rejectFn;
    const promise = new Promise((resolve, reject) => {
        rejectFn = reject;
        if (!settings.silent) {
            bus.trigger("RPC:REQUEST", data.id);
        }
        // handle success
        request.addEventListener("load", () => {
            if (request.status === 502) {
                // If Odoo is behind another server (eg.: nginx)
                if (!settings.silent) {
                    bus.trigger("RPC:RESPONSE", data.id);
                }
                reject(new ConnectionLostError());
                return;
            }
            let params;
            try {
                params = JSON.parse(request.response);
            } catch (_) {
                // the response isn't json parsable, which probably means that the rpc request could
                // not be handled by the server, e.g. PoolError('The Connection Pool Is Full')
                if (!settings.silent) {
                    bus.trigger("RPC:RESPONSE", data.id);
                }
                return reject(new ConnectionLostError());
            }
            const { error: responseError, result: responseResult } = params;
            if (!settings.silent) {
                bus.trigger("RPC:RESPONSE", data.id);
            }
            if (!responseError) {
                return resolve(responseResult);
            }
            const error = makeErrorFromResponse(responseError);
            reject(error);
        });
        // handle failure
        request.addEventListener("error", () => {
            if (!settings.silent) {
                bus.trigger("RPC:RESPONSE", data.id);
            }
            reject(new ConnectionLostError());
        });
        // configure and send request
        request.open("POST", url);
        request.setRequestHeader("Content-Type", "application/json");
        request.send(JSON.stringify(data));
    });
    /**
     * @param {Boolean} rejectError Returns an error if true. Allows you to cancel
     *                  ignored rpc's in order to unblock the ui and not display an error.
     */
    promise.abort = function (rejectError = true) {
        if (request.abort) {
            request.abort();
        }
        if (!settings.silent) {
            bus.trigger("RPC:RESPONSE", data.id);
        }
        if (rejectError) {
            rejectFn(new ConnectionAbortedError("XmlHttpRequestError abort"));
        }
    };
    return promise;
}

// -----------------------------------------------------------------------------
// RPC service
// -----------------------------------------------------------------------------
export const rpcService = {
    async: true,
    start(env) {
        let rpcId = 0;
        return function rpc(route, params = {}, settings) {
            return jsonrpc(env, rpcId++, route, params, settings);
        };
    },
};

registry.category("services").add("rpc", rpcService);

你可能感兴趣的:(odoo16前端框架分析,rpc,javascript,odoo)