Element使用的async-validator表单校验库源码超详细解析

大家好,我是若川。持续组织了8个月源码共读活动,感兴趣的可以 点此加我微信ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列。另外:目前建有江西|湖南|湖北籍前端群,可加我微信进群。


平常开发写 element 表单的时候,肯定少不了表单的校验,element 使用的是 async-validator 这个开源库。

44ab91e5ead0f352cafea54c5d19d373.png
image-20220517090312952

这篇文章详细分析一下 async-validator 的主流程。

使用方法

import Schema from 'async-validator';
const descriptor = {list: {required: true,type: 'number',},limit: [{required: true,message: '数量必填',},{validator(r, v, cb) {if (v < 100) {return cb(new Error('数量不能小于 100'));}cb();},},],
};
const validator = new Schema(descriptor);
validator.validate({ list: '12', limit: null },{ firstFields: true },(errors, fields) => {if (errors) {console.log('错误列表', errors);}},
);

我们需要定义 descriptor ,也就是我们在 element 中定义的 rules ,然后创建一个 Schema 对象。

最后调用 validate 函数,传递三个参数:

第一个参数是要校验的对象

第二个参数是 options 对象, firstFieldstrue ,表示同一个字段如果有多个校验规则,一旦出现校验不通过的规则后边的规则就不执行了。

还可以设置 firsttrue,这个是针对整个校验对象的,如果某个字段校验不通过,那么后边所有的字段就不再校验了。

第三个参数是校验结束后的回调函数,erros 保存了所有校验失败的字段以及 message 信息。

因此,上边代码的输出如下:

58af70b427694d197be4032df2966133.png
image-20220519081546339

list 对应结果的 message 是默认为我们添加的,limit 对应结果的 message 是我们自己设置的,会覆盖默认的 message

因为我们设置了 firstFieldstrue ,所以只校验了 limit 的第一个规则,第二个规则就没有走到。

我们给 limit 设置一个值,让它走到第二个校验规则。

validator.validate({ list: '12', limit: 3 },{ firstFields: true },(errors, fields) => {if (errors) {console.log('错误列表', errors);}},
);

输出如下:

6ead22d9ac6a7437d0f05238ed1731c1.png
image-20220519081840212

此时 limit 对应结果就是一个 Error 对象了,Error 对象除了本身的 message 属性,默认还为我们添加了 fieldfiledValue 属性。

预处理 descriptor

校验前  async-validator  会将传入的 descriptor 规范化。

我们传进入的是下边的样子:

const descriptor = {list: {required: true,type: 'number',},limit: [{required: true,message: '数量必填',},{validator(r, v, cb) {if (v < 100) {return cb(new Error('数量不能小于 100'));}cb();},},],
};

预处理后会变成下边的样子:

{list: [{rule: {required: true,type: 'number',field: 'list',fullField: 'list',validator: (rule, value, callback, source, options) => {const errors = [];const validate =rule.required ||(!rule.required && source.hasOwnProperty(rule.field));if (validate) {if (value === '') {value = undefined;}if (isEmptyValue(value) && !rule.required) {return callback();}rules.required(rule, value, source, errors, options);if (value !== undefined) {rules.type(rule, value, source, errors, options);rules.range(rule, value, source, errors, options);}}callback(errors);},},value: '12',source: {list: '12',limit: 3,},field: 'list',},],limit: [{rule: {required: true,message: '数量必填',field: 'limit',fullField: 'limit',type: 'string',validator: (rule, value, callback, source, options) => {const errors = [];const type = Array.isArray(value) ? 'array' : typeof value;rules.required(rule, value, source, errors, options, type);callback(errors);},},value: 3,source: {list: '12',limit: 3,},field: 'limit',},{rule: {field: 'limit',fullField: 'limit',type: 'string',validator(r, v, cb) {if (v < 100) {return cb(new Error('数量不能小于 100'));}cb();},},value: 3,source: {list: '12',limit: 3,},field: 'limit',},],
};

主要做了三件事情:

  1. 把每个字段的校验规则统一成了一个数组对象

  2. 把原本的校验对象放到了 rule 属性中,并且添加了 valuesourcefield 属性

  3. 根据 requiredtype 补充了默认的 validator 校验函数

预处理 descriptor 对应的源码

让我们过一下这部分源码。

在构造函数中,把 descriptor 所有字段的 rule 转为了数组,保存到 rules 对象中。

constructor(descriptor: Rules) {this.define(descriptor);
}define(rules: Rules) {if (!rules) {throw new Error('Cannot configure a schema with no rules');}if (typeof rules !== 'object' || Array.isArray(rules)) {throw new Error('Rules must be an object');}this.rules = {};Object.keys(rules).forEach(name => {const item: Rule = rules[name];this.rules[name] = Array.isArray(item) ? item : [item];});
}

剩下的处理都在 validate 函数中了,可以跟随下边的注释看一下:

validate(source_: Values, o: any = {}, oc: any = () => {}): Promise<Values> {let source: Values = source_;let options: ValidateOption = o;let callback: ValidateCallback = oc;if (typeof options === 'function') {callback = options;options = {};}...function complete(results: (ValidateError | ValidateError[])[]) {...}const series: Record<string, RuleValuePackage[]> = {};const keys = options.keys || Object.keys(this.rules); // 得到所有的要校验的 keykeys.forEach(z => { // 遍历所有字段const arr = this.rules[z];let value = source[z];arr.forEach(r => { // 遍历每个字段的所有 rulelet rule: InternalRuleItem = r;...// 如果是函数,放到 validator 属性中if (typeof rule === 'function') {rule = {validator: rule,};} else {rule = { ...rule };}// 填充 validator 属性rule.validator = this.getValidationMethod(rule);if (!rule.validator) {return;}// 填充其他属性rule.field = z;rule.fullField = rule.fullField || z;rule.type = this.getType(rule);series[z] = series[z] || [];// 保存到 series 中series[z].push({rule,value,source,field: z,});});});const errorFields = {};
}

看下上边的 getValidationMethod 方法:

getValidationMethod(rule: InternalRuleItem) {// 如果用户自定了,直接返回自定义的if (typeof rule.validator === 'function') {return rule.validator;}const keys = Object.keys(rule);const messageIndex = keys.indexOf('message');if (messageIndex !== -1) {keys.splice(messageIndex, 1);}// 如果只有一个 required 字段,返回 required 的校验函数if (keys.length === 1 && keys[0] === 'required') {return validators.required;}// 否则的根据 type 去返回校验函数return validators[this.getType(rule)] || undefined;
}

所有的校验函数都是提前定义好的:

5597dcb9285d7037c57dde1871ed922b.png
image-20220519094003441

在 前端的设计模式中-策略模式 中我们也提到过上边的逻辑。

循环校验

当我们有了预处理好的所有字段的校验规则。

const series = {list: [{rule: {required: true,type: 'number',field: 'list',fullField: 'list',validator: (rule, value, callback, source, options) => {const errors = [];const validate =rule.required ||(!rule.required && source.hasOwnProperty(rule.field));if (validate) {if (value === '') {value = undefined;}if (isEmptyValue(value) && !rule.required) {return callback();}rules.required(rule, value, source, errors, options);if (value !== undefined) {rules.type(rule, value, source, errors, options);rules.range(rule, value, source, errors, options);}}callback(errors);},},value: '12',source: {list: '12',limit: 3,},field: 'list',},],limit: [{rule: {required: true,message: '数量必填',field: 'limit',fullField: 'limit',type: 'string',validator: (rule, value, callback, source, options) => {const errors = [];const type = Array.isArray(value) ? 'array' : typeof value;rules.required(rule, value, source, errors, options, type);callback(errors);},},value: 3,source: {list: '12',limit: 3,},field: 'limit',},{rule: {field: 'limit',fullField: 'limit',type: 'string',validator(r, v, cb) {if (v < 100) {return cb(new Error('数量不能小于 100'));}cb();},},value: 3,source: {list: '12',limit: 3,},field: 'limit',},],
};

接下来只需要搞一个双重循环,执行所有的字段和每个字段的所有校验函数。

for(const field of Object.keys(series)) { // 遍历每一个字段for(const data of series[field]) { // 每一个规则const rule = data.rule;const res = rule.validator(rule, data.value, cb, data.source, options);}
}

ruledata.valuedata.source 就是当前规则相关的变量,options 是最开始调用校验的时候传进来的 { firstFields: true },,那么 cb 是什么?

cb 函数接受一个错误数据列表,如果返回的不是数组会包装为数组,然后对错误进行填充。

最后调用 doIt 函数,将校验结果传入,后边会介绍这个方法。

function cb(e = []) {let errorList = Array.isArray(e) ? e : [e];if (errorList.length && rule.message !== undefined) {errorList = [].concat(rule.message); // 错误列表优先使用 message 字段}// Fill error infolet filledErrors = errorList.map(complementError(rule, source));doIt(filledErrors); // 将当前字段的错误列表保存起来
}

complementError 会返回一个函数,将错误列表进行填充,主要就是补充了 fieldfieldValue 属性。

export function complementError(rule: InternalRuleItem, source: Values) {return (oe: ValidateError | (() => string) | string): ValidateError => {let fieldValue;if (rule.fullFields) {fieldValue = getValue(source, rule.fullFields);} else {fieldValue = source[(oe as any).field || rule.fullField];}if (isErrorObj(oe)) {oe.field = oe.field || rule.fullField;oe.fieldValue = fieldValue;return oe;}return {message: typeof oe === 'function' ? oe() : oe,fieldValue,field: ((oe as unknown) as ValidateError).field || rule.fullField,};};
}

收到的错误列表分为两种情况:

处理前如果 cb 收到的是 Error 列表,比如这样调用 cb(new Error('数量不能小于 100'));

那么处理前是下图:

c422ff004019d1fdf408f8ca30222e5f.png
image-20220521082112809

处理后,就会往 Error 对象中塞入 fieldfieldValue 属性。

b0c58b044064be1f390b8366d35f4004.png
image-20220521082219018

处理前如果cb 是字符串列表,比如这样调用 cb(['list is required', 'list is not a number'])

318f8a45820ae0482d08823f91008e24.png
image-20220521082345013

同样的,处理后也是塞入 fieldfieldValue 属性。

8e9db932fa9ff02fdb5658b9196cb2c0.png
image-20220521082457313

再回到我们的双重循环中。

for(const field of Object.keys(series)) { // 遍历每一个字段for(const data of series[field]) { // 每一个规则const rule = data.rule;const res = rule.validator(rule, data.value, cb, data.source, options);}
}

其中 validator 函数就是我们自己定义的:

validator(r, v, cb) {if (v < 100) {return cb(new Error('数量不能小于 100'));}cb();
},

由于 Element 官方示例是上边的样子,所以我们一般都按照上边的样子写,但其实我们也可以不调用 cb 函数,而是仅仅 return 字符串数组,或者 boolean 值,调用 cb 函数交给双重循环。

validator(r, v, cb) {if (v < 100) {return '数量不能小于 100'}return true;
},

双重循环中来处理 validator 的返回值去调用 cb 函数。

for(const field of Object.keys(series)) { // 遍历每一个字段for(const data of series[field]) { // 每一个规则const rule = data.rule;const res = rule.validator(rule, data.value, cb, data.source, options);// 根据返回的结果,去调用 cb 函数if (res === true) {cb();} else if (res === false) {cb(typeof rule.message === 'function'? rule.message(rule.fullField || rule.field): rule.message || `${rule.fullField || rule.field} fails`,);} else if (res instanceof Array) {cb(res);} else if (res instanceof Error) {cb(res.message);}}
}

asyncMap

向上边我们直接粗暴的写双重循环去依次校验也没有问题,但因为校验库还支持一些参数,比如前边介绍的:

2755a0d0e8ff1874b112dd6d662bce47.png
image-20220521083746778

如果是 for 循环中去处理 firstFieldsfirst 的逻辑,就过于耦合了,未来再扩充其他逻辑,双重循环中的逻辑就会越来越复杂。

async-validator  的处理方式在这里就比较优雅了,实现了 asyncMap 方法,作用就是遍历 series 数组,并且处理了 firstFieldsfirst 参数的逻辑。

下边来分析一下实现:

看一下 asyncMap 的入口参数。

export function asyncMap(objArr: Record<string, RuleValuePackage[]>,option: ValidateOption,func: ValidateFunc,callback: (errors: ValidateError[]) => void,source: Values,
){}

接受 5 个参数:

objArr:要遍历的 rule 规则,就是我们前边生成的 series 数组,即双重循环遍历的对象。

option :最开始传入的 option,可能包含 firstFieldsfirst 属性。

func:遍历过程的中会调用这个函数,会传入当前遍历的 rule 和一个 doIt 函数,doIt 函数需要接收处理好的校验结果。这里就需要我们之前 for 循环内部的处理逻辑。

callback : 全部检验结束后调用,会传入所有的校验结果。

source:要校验的对象。

这样我们就可以把 for 循环改为直接调用 asyncMap 函数了。

asyncMap(series,options,(data, doIt) => {...},results => {complete(results);},source,
);

第三个参数就是需要我们去处理 data 这个校验规则,也就是之前 for 循环中的逻辑移动过来。

其中 doIt 函数我们在之前讲的 cb 函数中调用即可。

(data, doIt) => {const rule = data.rule;rule.field = data.field;function cb(e: SyncErrorType | SyncErrorType[] = []) {let errorList = Array.isArray(e) ? e : [e];if (errorList.length && rule.message !== undefined) {errorList = [].concat(rule.message);}// Fill error infolet filledErrors = errorList.map(complementError(rule, source));doIt(filledErrors); // 将当前字段的错误列表保存起来}/******** for 循环中的逻辑 *****************/const res = rule.validator(rule, data.value, cb, data.source, options);if (res === true) {cb();} else if (res === false) {cb(typeof rule.message === 'function'? rule.message(rule.fullField || rule.field): rule.message || `${rule.fullField || rule.field} fails`,);} else if (res instanceof Array) {cb(res);} else if (res instanceof Error) {cb(res.message);}/***************************************/
},

最后就是全部遍历结束后的 complete 函数,我们只需要把 results 列表传到外边即可。

function complete(results) {let fields: ValidateFieldsError = {};if (!results.length) {callback(null, source);} else {fields = convertFieldsError(results);callback (results, fields);}
}

上边的 callback 函数就是我们调用校验函数时候外部传入的:

const validator = new Schema(descriptor);
validator.validate({ list: '12', limit: null },{ firstFields: true },//***** 上边的 callback ********************/(errors, fields) => {if (errors) {console.log('错误列表', errors);}},//*********************************************/
);

内层循环

双重循环的的外层是遍历所有字段,内层是遍历该字段的所有规则。

我们来先看一下内层循环的实现:

async-validator  库提供了 asyncParallelArray 方法。

function asyncParallelArray(arr: RuleValuePackage[],func: ValidateFunc,callback: (errors: ValidateError[]) => void,
) {const results: ValidateError[] = [];let total = 0;const arrLength = arr.length;function count(errors: ValidateError[]) {results.push(...(errors || []));total++;if (total === arrLength) {callback(results);}}arr.forEach(a => {func(a, count);});
}

接受三个参数:

arr 就是当前字段要遍历的规则列表。

func 是处理 rule 规则的函数,内部会调用这里的 count 方法,接受当前 a 的校验结果。

传入的 func 其实就是我们前边介绍过的 for 循环内部逻辑,a 是下边的 data 参数,count 就是下边的 doIt

(data, doIt) => {const rule = data.rule;rule.field = data.field;function cb(e: SyncErrorType | SyncErrorType[] = []) {let errorList = Array.isArray(e) ? e : [e];if (errorList.length && rule.message !== undefined) {errorList = [].concat(rule.message);}// Fill error infolet filledErrors = errorList.map(complementError(rule, source));doIt(filledErrors);}const res = rule.validator(rule, data.value, cb, data.source, options);if (res === true) {cb();} else if (res === false) {cb(typeof rule.message === 'function'? rule.message(rule.fullField || rule.field): rule.message || `${rule.fullField || rule.field} fails`,);} else if (res instanceof Array) {cb(res);} else if (res instanceof Error) {cb(res.message);}
},

第三个参数 callback 是当前 arr 全部校验结束后的回调,代表当前字段的所有校验规则都判断结束。

这里需要注意的是,我们是通过 count 进入的次数来判断是否去调用 callback 函数,而不是 arr 遍历结束后调用 callback

除了 asyncParallelArray 方法,因为有 firstFields 属性的存在,也就是遍历某个字段的所有规则时,如果出现校验不通过的规则就直接结束,后边的规则不再进行判断。

因此, async-validator  还提供了 asyncSerialArray 方法。

function asyncSerialArray(arr: RuleValuePackage[],func: ValidateFunc,callback: (errors: ValidateError[]) => void,
) {let index = 0;const arrLength = arr.length;function next(errors: ValidateError[]) {if (errors && errors.length) {callback(errors);return;}const original = index;index = index + 1;if (original < arrLength) {func(arr[original], next);} else {callback([]);}}next([]);
}

入口参数和 asyncParallelArray 是一致的,区别在于对于 arr 是顺序执行,如果过程中出现了校验不通过的规则,就直接调用 callback 结束。

外层循环

外层循环和上边很类似,其实就是遍历所有字段,然后把每个字段的校验列表传给内层循环即可。

export function asyncMap(objArr: Record<string, RuleValuePackage[]>,option: ValidateOption,func: ValidateFunc,callback: (errors: ValidateError[]) => void,source: Values,
) {const firstFields =option.firstFields === true? Object.keys(objArr): option.firstFields || [];const objArrKeys = Object.keys(objArr);const objArrLength = objArrKeys.length;let total = 0;const results: ValidateError[] = [];const next = (errors: ValidateError[]) => {results.push.apply(results, errors);total++;if (total === objArrLength) {callback(results);}};if (!objArrKeys.length) {callback(results);}objArrKeys.forEach(key => {const arr = objArr[key];if (firstFields.indexOf(key) !== -1) {asyncSerialArray(arr, func, next);} else {asyncParallelArray(arr, func, next);}});
}

入口参数前边已经介绍过了,可以看到我们做的就是遍历 objArrKeys 数组,然后根据 firstFields 的值去调用 asyncSerialArrayasyncParallelArray 。内存循环判断结束后会调用上边的 next 方法。

next 同样也是通过进入的次数,来判断是否调用 callback 函数,也就是前边介绍的 complete 方法。

和内层循环类似,因为有 first 属性的存在,也就是遍历某个字段时,存在校验不通过的字段就直接结束,后边的字段就不再进行判断。

我们只需要把所有规则打平,然后调用 asyncSerialArray 方法即可。

if (option.first) {const next = (errors: ValidateError[]) => {callback(errors);};const flattenArr = flattenObjArr(objArr);asyncSerialArray(flattenArr, func, next);
}function flattenObjArr(objArr: Record<string, RuleValuePackage[]>) {const ret: RuleValuePackage[] = [];Object.keys(objArr).forEach(k => {ret.push(...(objArr[k] || []));});return ret;
}

代码总

以上就是  async-validator 源码的主要流程了,说起来也简单,先预处理所有规则,然后通过 asyncMap 方法双层循环遍历所有校验规则即可,这个双层循环的抽离确实很优雅,避免了循环中耦合太多逻辑。

除了上边介绍的代码,因为 async-validator 还支持 Promise 的调用风格,校验函数支持 Promise 函数等其他功能,大家感兴趣也可以到 async-validator 看一下更详细的源码。

值得一提的点是,双层循环是通过计数来判断是否结束的,而进入计数其实就是调用 cb 函数。因此如果我们规则是下边的样子:

import Schema from '../src/index';
const descriptor = {limit: [{validator(r, v, cb) {if (v < 100) {cb('校验1');}cb();},},{validator(r, v, cb) {if (v < 50) {return cb('校验2');}cb();},},],
};
const validator = new Schema(descriptor);
validator.validate({ limit: 3 },(errors, fields) => {if (errors) {console.log('错误列表', errors);}},
);

因为我们没有传递 firstFields 属性,所以我们期望的是将 limit 所有的校验都进行了,limit 的值是 3 ,所以两个校验都没通过,应该输出下边的内容:

bb94535a53c31b93bd5316079cb81adf.png
image-20220521103342907

但其实只进行了第一个的校验:

548316e416b2bc2a985a316151e13cba.png
image-20220521103508233

原因就在于第一个 validator 进行了两次 cb ,然后内层循环的 callback 就提前调用了。

validator(r, v, cb) {if (v < 100) {cb('校验1');}cb();
},

因此我们最好保证一个 validator 只进行一次 cb ,走到 cb 后就直接 return。(因为 Element 会设置 firstFieldstrue,所以其实有多个 cb 也不影响最终结果)

validator(r, v, cb) {if (v < 100) {return cb('校验1');}cb();
},

并且一定要有一个 cb ,不然最终的回调函数永远也不会执行了,这就是为什么 Element 提示我们要进行 cb

a8a841b22904bf5b3e780702155d43a1.png
image-20220521103841307

但这里说的也不够严谨,我们也可以返回字符串,或者字符串数组、布尔值等, async-validator 内部会根据 validator 返回的结果去调用 cb 函数。

const res = rule.validator(rule, data.value, cb, data.source, options);if (res === true) {cb();} else if (res === false) {cb(typeof rule.message === 'function'? rule.message(rule.fullField || rule.field): rule.message || `${rule.fullField || rule.field} fails`,);} else if (res instanceof Array) {cb(res);} else if (res instanceof Error) {cb(res.message);}

async-validator  用计数的方式来判断是否去调用回调,就是为了实现异步的校验,当异步过程结束后才去调用 cb ,代表校验完成。

其他属性

平时写代码直接参照前人的校验规则去仿照着写了,大家也基本上是按照 Element 的样例来写校验规则,如果去  async-validator  看一下的话,会发现一些其他没听过的属性,这里也记录下。

validator 校验函数最多能接收到 5 个参数。

validator(rule, value, callback, source, options) {const errors = [];// test if email address already exists in a database// and add a validation error to the errors array if it doesreturn errors;
},

我们可以通过第四个参数 source 拿到整个表单的对象,如果想校验一些联动的逻辑,我们就可以通过 source 拿到其他字段的值。

对对象字段的校验,如果校验字段是个对象,我们可以通过 fields 来校验对象中的字段。

const descriptor = {address: {type: 'object',required: true,fields: {street: { type: 'string', required: true },city: { type: 'string', required: true },zip: { type: 'string', required: true, len: 8, message: 'invalid zip' },},},name: { type: 'string', required: true },
};
const validator = new Schema(descriptor);
validator.validate({ address: {} }, (errors, fields) => {// errors for address.street, address.city, address.zip
});

transform 函数,可以将值先进行一次转换,然后再进行校验。

const descriptor = {name: {type: 'string',required: true,pattern: /^[a-z]+$/,transform(value) {return value.trim();},},
};

asyncValidator,校验函数内部是用 Promise 或者直接返回一个 Promise

const fields = {asyncField: {asyncValidator(rule, value, callback) {ajax({url: 'xx',value: value,}).then(function(data) {callback();}, function(error) {callback(new Error(error));});},},promiseField: {asyncValidator(rule, value) {return ajax({url: 'xx',value: value,});},},
};

上边就是   async-validator  开源库的核心源码了,希望对你有帮助。

e685564796777c58093383ad967b37a0.gif

················· 若川简介 ·················

你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》20余篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经坚持写了8年,点击查看年度总结。
同时,最近组织了源码共读活动,帮助4000+前端人学会看源码。公众号愿景:帮助5年内前端人走向前列。

e4ae600c8d0b96ee2a90c13303753e25.png

扫码加我微信 ruochuan12、拉你进源码共读

今日话题

目前建有江西|湖南|湖北 籍 前端群,想进群的可以加我微信 ruochuan12 进群。分享、收藏、点赞、在看我的文章就是对我最大的支持~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/274637.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

从零手写 Vue 之响应式系统

大家好&#xff0c;我是若川。持续组织了8个月源码共读活动&#xff0c;感兴趣的可以 点此加我微信ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列。另外…

WPF 分页控件应用

效果图&#xff1a; 前台代码&#xff1a; <UserControl x:Class"Layout.UI.Comm.Pager"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml"xmlns:mc"http:/…

李宁品牌重塑_迈伊多品牌重塑的幕后

李宁品牌重塑This post was originally published on the Maido blog.这篇文章最初发表在 Maido博客上 。 You might notice that we’ve had a little facelift at Maido. Or you might not — and that’s totally fine. What we launched at the end of last year was not r…

搭建前端监控,如何采集异常数据?

大家好&#xff0c;我是若川。持续组织了近一年的源码共读活动&#xff0c;感兴趣的可以 点此加我微信ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列。…

产品经理如何提高创造力_如何提高产品设计师的创造力

产品经理如何提高创造力When David Kelley, Bill Moggridge, and Mike Nuttall founded IDEO, a consulting firm that would become one of the most innovative companies of the late 90s, they brought a new perspective in product development.当大卫凯利(David Kelley)…

Github上8个很棒的Vue项目

大家好&#xff0c;我是若川。持续组织了近一年的源码共读活动&#xff0c;感兴趣的可以 点此加我微信ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列。…

python 投资组合_成功投资组合的提示

python 投资组合Lately, I’ve had some free time during my job transition and have been reviewing a few of my friends’ design portfolios. Gradually, I found some common themes around the feedback I’ve given. And it occurred to me that others might find so…

Github上8个很棒的React项目

大家好&#xff0c;我是若川。持续组织了近一年的源码共读活动&#xff0c;感兴趣的可以 点此加我微信ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列。…

屏幕广播系统_如何设计系统,而不是屏幕

屏幕广播系统重点 (Top highlight)Over the past several decades, rapid advances in technology have dramatically enhanced the digital customer experience and their expectations. In the face of these heightened customer expectations, the role of the Interactio…

Umi 4 发布啦

大家好&#xff0c;我是若川。持续组织了近一年的源码共读活动&#xff0c;感兴趣的可以 点此加我微信ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列。…

Win32汇编--加载菜单资源

基本上的窗口都会有一个菜单,现在就来看看Win32汇编中是如何加载菜单的: 1>在工程中添加新的菜单资源 2>双击新添加的菜单资源进行编辑 3>菜单栏:Make->Compile RC来编译资源文件 4>导出资源中的ID号并写到数据段的.const中 5>下面是完整的源代码供参考:(工程…

Futura:从纳粹主义到月球-甚至更远

Reading the title of this article, the first thing that will come to mind for some is the funny expression of Buzz Lightyear — the Disney character — when he stretches his arms outwards and utters the famous phrase “To infinity and beyond!” before jump…

如何碎片化时间高效学习前端~

前端技术日新月异&#xff0c;发展迅速&#xff0c;作为一个与时俱进的前端工程师&#xff0c;需要不断的学习。这里强烈推荐几个前端开发工程师必备的优质公众号&#xff0c;希望对你有所帮助。大家可以像我一样&#xff0c;利用碎片时间阅读这些公众号的文章。前端从进阶到入…

爬取淘宝定价需要多久时间_如何对设计工作进行定价—停止收​​取时间并专注于价值

爬取淘宝定价需要多久时间Pricing creative work is a new concept for most freelancers who are starting their business. We are used to being paid for our time, either by an hourly wage or an annual salary. It makes it simple to quantify how much value we thin…

OEA 框架中集成的 RDLC 报表介绍

之前 OEA 一直用着一个 Delphi 开发的报表&#xff0c;所以两年来我一直就想在 OEA 中构建一个纯 .NET 的报表模块&#xff0c;但是一想到要开发复杂的报表引擎和设计器就觉得麻烦。所以这事一直拖着。最近开始研究一些成熟的报表引擎&#xff0c;经过对比&#xff0c;还是发现…

昆虫繁殖_“专为昆虫而生” –好奇!

昆虫繁殖重点 (Top highlight)The industry is changing towards a more agile approach and jacks of one trade can go extinct sooner than we think.该 行业正在发生变化 朝着更加灵活的方法和一个贸易的插Kong可以去灭绝快于我们的想法。 I’ve read a quote in a book r…

ECMAScript 2022 正式发布,有哪些新特性?

大家好&#xff0c;我是若川。持续组织了近一年的源码共读活动&#xff0c;感兴趣的可以 点此加我微信ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列。…

字母框如何影响UI内容的理解

What is your earliest memory of reading? Mine’s reading comics. I preferred films over books, I still do, but I seemed to have a fascination for comics. The experience of reading a comic, to me, was somewhere between watching a film and reading a novel, …

Vue2.7 本周发布?支持组合式 API、setup、css v-bind

大家好&#xff0c;我是若川。持续组织了近一年的源码共读活动&#xff0c;感兴趣的可以 点此加我微信ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列。…

马上7月,诚邀新老朋友参加近5000人的源码共读活动!

大家好&#xff0c;我是若川。最近有不少新朋友关注我。诚邀各位新老读者朋友参加源码共读活动。活动介绍可以点击文末的阅读原文。https://juejin.cn/post/7079706017579139102很多人关注我的公众号是因为我写了一系列源码文章&#xff0c;想参与源码共读活动。虽然现在有近50…