Appearance
JINT 项目实践
01 - 契机
前几天参加了中国 net conf 2022,在Headless cms分享的讲座中,注意到讲师PPT中提到的Jint框架,得空Github关注了下。
Jint的Wiki中介绍了Handlebar语义化模板框架,想着不仅能用到html结构的转义,对应的json串也能完美契合,准备探究下Jint在对接项目中的实践。
02 - 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和语义化模板,为外部系统对接字段影身,提供了另一种技术可能。但本身需要比较高级定制化的操作设置,需要从产品、用户角度进行考虑,提供基础模板,定制化需求提供自定义处理能力。