Skip to content
On this page

JINT 项目实践

01 - 契机

前几天参加了中国 net conf 2022,在Headless cms分享的讲座中,注意到讲师PPT中提到的Jint框架,得空Github关注了下。
Jint的Wiki中介绍了Handlebar语义化模板框架,想着不仅能用到html结构的转义,对应的json串也能完美契合,准备探究下Jint在对接项目中的实践。

02 - Jint介绍

https://github.com/sebastienros/jint

Jint是.Net下的JavaScript解释器,可以用在自动化、游戏、规则、模板引擎中。
例如:AngleSharp、RavenDB

03 - Jint简单使用

c#
void Main()
{
	var engine = new Engine();
	var result = engine.Execute("1+2").GetCompletionValue().ToObject();
	Console.WriteLine(result);
}

Output: 3
c#
void Main()
{
	var engine = new Engine();
	engine.SetValue("log",new Action<object>(Console.WriteLine));
	while(true)
	{
		var statement = Console.ReadLine();
		var result = engine.Execute(statement).GetCompletionValue().ToObject();
		engine.SetValue("result",result);
		engine.Execute("log(result)").GetCompletionValue();
	}
}
Input:
a = 1 + 2
b = 3 + 4
function Add(x,y) { return x+y; }
c = Add(a,b)
    
Output:
3
7
null
10

04 - 项目实践的一些思考

041 - 问题描述

​ 对接三方系统,常常需要定义字段映射表,例如:

C#
public class FieldMappingConfig
{
    public long TenantId { get; set; }
    public long TeamId { get; set; }
    public string LeadFieldName { get; set; }
    public string ContactFieldName { get; set; }
    public string DefaultFieldValue { get; set; }
    /// <summary>
    /// 处理中英文、KV映射
    /// </summary>
    public Dictionary<string, string> ConvertFieldValues { get; set; }
    [MaxLength(32)]
    [Comment("多选字段映射拼接字符")]
    public string ConvertMultiFieldValueSplicingSymbol { get; set; }
}

如果需要同步的字段类型是下拉多选字段,Custouch系统内对应字段格式为 custom_a712sa: "甲;乙;丙",有些外部系统支持处理为A;B;C 对应的处理逻辑为

C#
...
// 处理字段值映射,不用区分是否是自定义字段
if (config.ConvertFieldValues.Any())
{
    // 多选字段
    if (!string.IsNullOrWhiteSpace(config.ConvertMultiFieldValueSplicingSymbol))
    {
        // 系统Lead默认";"拼接
        var tmpVal = val.Split(';', StringSplitOptions.RemoveEmptyEntries);
        convertFieldValue = string.Join(config.ConvertMultiFieldValueSplicingSymbol,
                                        tmpVal.Select(_ => config.ConvertFieldValues.GetValueOrDefault(_))
                                              .Where(_ => _ != default));
    }
    else
    {
        convertFieldValue = config.ConvertFieldValues.GetValueOrDefault(val);
    }
}

另一些外部系统支持处理为multiField_01: A,multiField_02: B,multiField_03:C,对应的处理逻辑如下

C#
if (config.FieldType == FieldType.MultiSelect)
{
    // 系统Lead默认";"拼接
    var tmpVal = (val as string).Split('', StringSplitOptions.RemoveEmptyEntries);
    var multiValues = tmpVal.Select(_ => config.ConvertFieldValues.GetValueOrDefault(_))
                            .Where(_ => _ != default)
                            .ToList();
    for (var i = 0; i < multiValues.Count; i++)
    {
        leadPropertiesDic.TryAdd($"{config.FieldName}{config.ConvertMultiFieldValueSplicingSymbol}{i}", multiValues[i]);
    }
    continue;
}
else
{
    tmpValue = config.ConvertFieldValues.GetValueOrDefault(tmpValue);
}

(FieldMappingConfig表结构有扩展

042 - 尝试用Jint结合Handlebars处理相似的转换逻辑

js
var Handlebars = require('./Statics/handlebars-v4.7.7');

const NumEmployeesMap = { "1-5人": "1-5", "5-25人": "5-25", "25-50人": "25-50", "50-100人": "50-100", "100-500人": "100-500", "500-1000人": "500-1000", "1000人及以上": "1000+" }
const SendingInquiriesMap = { "": "true", "": "false" };
const ConsultantExpertMap = { "": "true", "": "false" };
const SampleNameMap = { "泰莱膳食纤维": "Fibre", "SPLENDA® 善品糖®": "SPLENDA® Sucralose", "泰莱甜菊糖苷系列": "Stevia", "泰莱罗汉果甜苷": "PUREFRUIT®", "泰莱稳定系统": "SFS", "泰莱清洁标签淀粉": "Clean Label Starch", "泰莱变性淀粉系列": "Modify Starch Series", "泰莱木薯淀粉系列": "CMS" };

var context = {
    firstname: "",
    lastname: "1-5人",
    fullname: "泰莱膳食纤维;SPLENDA® 善品糖®;泰莱甜菊糖苷系列"
}

// custom process
Handlebars.registerHelper('NumEmployeesMap', function (aString) {
    return NumEmployeesMap[aString] || aString;
})
Handlebars.registerHelper('SendingInquiriesMap', function (aString) {
    return SendingInquiriesMap[aString] || aString;
})
Handlebars.registerHelper('ConsultantExpertMap', function (aString) {
    return ConsultantExpertMap[aString] || aString;
})
//泰莱膳食纤维;SPLENDA® 善品糖®;泰莱甜菊糖苷系列
Handlebars.registerHelper('SampleNameMap', function (aString) {
    return aString.split(';')
                  .map(sampleName => SampleNameMap[sampleName] || sampleName)
                  .reduce
                  (
                      (accumulator, currentValue) => {
                          return accumulator + ";" + currentValue
                      }
                  );
})
// pattern string
var patternStr = "{{SendingInquiriesMap firstname}}\n{{NumEmployeesMap lastname}}\n{{SampleNameMap fullname}}";
// compile the template
var template = Handlebars.compile(patternStr);
// execute the compiled template and print the output to the console
console.log(template(context));

Output:
true
1-5
Fibre;SPLENDA® Sucralose;Stevia

043 - 持续迭代,Net中完整使用(仅展示核心代码)

c#
#region Config Models

public enum ApiConfigType
{ 
	Lead
}
public class ApiConfig
{
	public long TenantId { get; set; }
	public long TeamId { get; set; }
	public ApiConfigType Type { get; set; }
	public string Template { get; set; }
}
public enum MappingConfigType
{
	Variable,
	Function
}
public class MappingConfig
{
	public long TenantId { get; set; }
	public long TeamId { get; set; }
	[Comment("规则类型")]
	public MappingConfigType Type { get; set; }
	[Comment("映射键")]
	public string Key { get; set; }
	[Comment("映射值")]
	public string Value { get; set; }
}
#endregion

#region Core Code

// 初始化Jint
var engine = new Engine();
// 加载处理语义化模板的handlebars.js文件
var handlebars = File.ReadAllText(@".\Statics\handlebars-v4.7.7.js");
engine.Execute(handlebars);

// 定义Dump方法,方便调试
engine.SetValue("log", new Action<object>(Console.WriteLine));

// 脚本:自定义属性,用来做字段映射转换
var variables = _dbContext.MappingConfigs.Where(_ => _.TenantId == tenantId &&
		 											 _.TeamId == teamId &&
		 											 _.Type == MappingConfigType.Variable)
 										 .ToList();
foreach (var variable in variables)
{
	engine.SetValue(variable.Key, JsonConvert.DeserializeObject(variable.Value));
}

// 脚本:自定义转化方法
var functions = _dbContext.MappingConfigs.Where(_ => _.TenantId == tenantId &&
													 _.TeamId == teamId &&
													 _.Type == MappingConfigType.Function)
									  	 .ToList();
foreach (var function in functions)
{
	engine.Execute($@"Handlebars.registerHelper('{function.Key}', {function.Value})");
}

var outputTemplate = _dbContext.ApiConfigs.FirstOrDefault(_ => _.TenantId == tenantId &&
															   _.TeamId == teamId &&
															   _.Type == ApiConfigType.Lead);

engine.SetValue("outputTemplate", outputTemplate.Template);

// 编译语义化模板
engine.Execute("var template = Handlebars.compile(outputTemplate)");

// 模拟数据源
var inputFilePath = @".\Resources\input.json";
var leadInfos = FileHelper.LoadJson<List<LeadInfo>>(inputFilePath);
engine.SetValue("input", leadInfos.First().Properties);

// 转化处理
engine.Execute("var result = template(input)");
engine.Execute("log(result)");

// 输出持久化
var outputFilePath = @".\Resources\result.txt";
FileHelper.SaveJson(outputFilePath, engine.GetValue("result").ToString());
var resultObj = FileHelper.LoadJson<Dictionary<string, object>>(outputFilePath);
resultObj.Dump("Output");

#endregion
    
input.json
[
    {
        "leadId": "1043482962202763264",
        "mailbox": "dhhdjdjf5@qq.com",
        "wechatAppId": "wx64d1b099d6beceb5",
        "wechatOpenId": "oS-YexHHxoOhFncYoQpdnqBtKyYY",
        "wechatUnionId": "oI32ys_-iEjxln2htw5EuclLC--Q",
        "properties": {
            "WechatAppId": "wx64d1b099d6beceb5",
            "WechatOpenId": "oS-YexHHxoOhFncYoQpdnqBtKyYY",
            "WechatUnionId": "oI32ys_-iEjxln2htw5EuclLC--Q",
            "FullName": "重复6",
            "FirstName": "重复6",
            "Mailbox": "dhhdjdjf5@qq.com",
            "Phone": "15588569965",
            "Sex": "",
            "FullAddress": "辽宁省; 锦州市; 义县; gshs",
            "Province": "辽宁省",
            "City": "锦州市",
            "District": "义县",
            "Detail": "gshs",
            "Industry": "制造业",
            "Organization": "市场易",
            "Department": "测试",
            "Position": "测试",
            "Remark": "",
            "Activity": "Hot",
            "OriginalSource": "线上咨询",
            "Campaign": "线下",
            "Custom_9b88bb": "1000人及以上",
            "Custom_080a8a": "泰莱膳食纤维;SPLENDA® 善品糖®;泰莱甜菊糖苷系列",
            "Custom_eadbaa": "",
            "Custom_abbacb": "",
            "SourceType": "Qn",
            "SourceDescribe": "{\"Id\":1037211650199117824,\"Title\":\"泰莱-hubspot测试\",\"Remark\":\"测试-勿删\"}",
            "LeadSourceType": "Qn",
            "LeadSourceDescribe": "{\"Id\":1037211650199117824,\"Title\":\"泰莱-hubspot测试\",\"Remark\":\"测试-勿删\"}",
            "ActiveScores": "151"
        }
    }
]
template.txt
{
  "campaign": "{{Campaign}}",
  "city": "{{City}}",
  "country": "{{Country}}",
  "country_01": "{{Country}}",
  "country_02": "{{City}}",
  "country_03": "{{City}}",
  "country_04": "{{City}}",
  "country_05": "{{City}}",
  "what_sample_s_would_you_like_sweetener_u_": "{{SampleNameMap Custom_080a8a}}",
  "numemployees": "{{NumEmployeesMap Custom_9b88bb}}",
  "send_me_the_latest_news_and_information_from_tate_lyle": "{{SendingInquiriesMap Custom_abbacb}}",
  "would_you_like_to_speak_with_an_expert_": "{{ConsultantExpertMap Custom_eadbaa}}",
  "department": "{{Department}}",
  "district": "{{District}}",
  "address": "{{FullAddress}}",
  "lastname": "{{FullName}}",
  "industry": "{{Industry}}",
  "email": "{{Mailbox}}",
  "company": "{{Organization}}",
  "leadsource": "{{OriginalSource}}",
  "phone": "{{Phone}}",
  "jobtitle": "{{Position}}",
  "state": "{{Province}}",
  "remark": "{{Remark}}",
  "gender": "{{Sex}}",
  "wechatUserInfo":{
      "wechatappid": "{{WechatAppId}}",
      "wechatopenid": "{{WechatOpenId}}",
      "wechatunionid": "{{WechatUnionId}}"
  }
}
result.txt
{
  "campaign": "线下",
  "city": "锦州市",
  "country": "",
  "country_01": "",
  "country_02": "锦州市",
  "country_03": "锦州市",
  "country_04": "锦州市",
  "country_05": "锦州市",
  "what_sample_s_would_you_like_sweetener_u_": "Fibre;SPLENDA® Sucralose;Stevia",
  "numemployees": "1000+",
  "send_me_the_latest_news_and_information_from_tate_lyle": "true",
  "would_you_like_to_speak_with_an_expert_": "true",
  "department": "测试",
  "district": "义县",
  "address": "辽宁省; 锦州市; 义县; gshs",
  "lastname": "重复6",
  "industry": "制造业",
  "email": "dhhdjdjf5@qq.com",
  "company": "市场易",
  "leadsource": "线上咨询",
  "phone": "15588569965",
  "jobtitle": "测试",
  "state": "辽宁省",
  "remark": "",
  "gender": "",
  "wechatUserInfo":{
      "wechatappid": "wx64d1b099d6beceb5",
      "wechatopenid": "oS-YexHHxoOhFncYoQpdnqBtKyYY",
      "wechatunionid": "oI32ys_-iEjxln2htw5EuclLC--Q"
  }
}

05 - 思考总结

Jint和语义化模板,为外部系统对接字段影身,提供了另一种技术可能。但本身需要比较高级定制化的操作设置,需要从产品、用户角度进行考虑,提供基础模板,定制化需求提供自定义处理能力。

Date: 2023/01/05

Authors: 韩文凯

Tags: 编码实战、JINT