大数跨境
0
0

前端库开发经验小结

前端库开发经验小结 微鲤技术团队
2023-06-07
2
导读:本文介绍了前端库的开发过程和经验小结






01


开发背景


业务组内前端项目有大量日历黄历相关计算,且需要应用在H5、小程序等诸多平台,这部分算法原先放在js文件中作为模块导出,并没有封装成库。这样做的缺点有:
  1. API没有统一的文档,使用不方便。
  2. 需要重复通过调用函数实现业务。
  3. 不便于管理,算法中有错误没法及时修改到每一个项目。(例如传统万年历与现代农历新年交替日存在分歧等)
尽管网上已经有诸多相关轮子,但是找到一个完全适合自身业务的并不容易,于是我开始考虑是否能自己造个轮子,供自己和组内成员使用提高以开发效率。





02


组件库设计


做前端库开发同我们做项目一样,先定目标再谈方法,而我作为库开发者同时也是库的第一个使用者,我会首先问自己:
这个库能解决什么问题?我希望这个库使用起来是什么样的?
首先库能解决上面三个痛点:
1. 通过编写API文档方便用户使用或查阅,做好版本维护
2. 进一步封装部分实现,减少开发业务时的代码量
3. 通过更新库的版本,在项目中更新引用库版本来实现功能同步
第二点,我希望这个日历库能像 Axios,momentjs 等经典js库一样易用。
对于某一天的日期信息,我希望可以实例化一个类就能获得全部数据,例如:
let cal = new Calendar();// cal中应该包含了年月日、星期、农历年月日、当天节日等等信息
let cal = new Calendar(2000, 1, 1);// Calendar应该支持传入参数生成指定日期实例

库的引用方式应该是:

import Calendar from "weli-calendar"; // 农历import Almanac from "weli-calendar/almanac"; // 黄历
let alc = new Almanac();// Almanac 作为黄历类,它的实例应该包括宜忌、彭祖百忌等全部黄历数据

根据以上需求可以确定期望的打包结构为:

dist/calendar/index.jsdist/almanac/index.js

在package.json中配置项目入口

{"main": "/calendar/index.js",}

就可以实现我们期望的默认引入农历模块

项目结构


项目主要分为calendar(农历相关算法) almanac(黄历相关算法)两部分
以calendar模块为例子,在calendar目录下,index为入口文件,它导出Calendar类,里面定义了一些农历相关属性:
export default class Calendar implements IGregorianCalenar, ILunarCalenar, IFestivalCalendar{  year: gregorianYear;  month: gregorianMonth; // 月 1-12  day: gregorianDay; // 日 1-31    ...
lunarYear: lunarYear; lunarMonth: lunarMonth; lunarDay: lunarDay; ...
festival?: IFestival[]; // 节日 solarTerm?: string; // 节气 ...}
可以看到这个类实现了三个接口IGregorianCalenar, ILunarCalenar, IFestivalCalendar,
而接口的定义全部放在calendar/interfaces.ts中,又以IGregorianCalenar为例:
export default interface IGregorianCalenar {    year: gregorianYear,    month: gregorianMonth, // 月 1-12    day: gregorianDay, // 日 1-31    formatMonth: string, // XX月    formatDay: string, // XX日    constellation: string, // 星座    yearWeek: number, // 今年第几周    week: number, // 星期 1-7    weekCn: string, // "周X"    weekCn2: string, // "星期X"    isCurYear: boolean, // 是今年    isCurMonth: boolean, // 是本月    isCurDay: boolean, // 是今天    diff: number // 距离今天日期差}
这个接口定义了公历相关的属性,同样的ILunarCalenar定义了农历相关属性,IFestivalCalendar是节日相关。
这样做的目的之一,是因为日历库中,公农历转换是一个非常核心的功能,例如在实现公历转农历的函数中,入参的类型为IGregorianCalenar,返回值的类型为ILunarCalenar,方便区分和理清业务。
calendar/libs中是这个模块的核心算法,它包括公农历转换、查询闰月、查询星座等等的计算函数,但是这些细节我们没有必要全部暴露给使用者,使用者应该只关注Calendar类。
对于某个日期的信息,我们通过实例化Calendar实现,而对于一些重要的工具方法,我们可以将它在libs中实现后挂载在Calendar上作为静态方法供用户使用,例如:
...    static getMonthlyCalendar(beginWeekEn?: 'Mon' | 'Sun'): Calendar[]    static getMonthlyCalendar(y?: gregorianYear | string, m?: gregorianMonth, beginWeekEn: 'Mon' | 'Sun' = 'Sun'): Calendar[] {       ...    }...
getMonthlyCalendar是一个传入年月、返回当月所有日期对象的函数,这个函数就可以作为静态方法挂在到Calendar上,在渲染月历盘等场景使用。
接下来是calendar/type:
export type gregorianYear = number; // 公历年export type gregorianMonth = number; // 公历月export type gregorianDay = number; // 公历日export type lunarYear = number; // 农历年export type lunarMonth = number; // 农历月export type lunarDay = number;  // 农历日
你可能会好奇他们都是number类型,为什么还要单独定义一次呢?回到上面这个函数:
getMonthlyCalendar(y?: gregorianYear | string, m?: gregorianMonth, beginWeekEn: 'Mon' | 'Sun' = 'Sun')
参数y的类型是gregorianYear,我们可以明白这个参数是公历年,如果这里仅仅写number,我们就还要额外通过注释去声明。
最后calendar/festivalList.ts是节日相关数据。
almanac黄历模块与calendar大致相同,不做赘述。





03


单元测试配置


我们在一般快速的迭代的业务开发中可能并不会做单元测试,因为业务本身快速迭代快速上线的特点,和单元测试的高开发成本有些许矛盾。
但是库的开发和业务有所不同,一个质量过关的库应该是”长期维护,稳定少bug”,因此单元测试在库的开发中是必不可少的。
单元测试使用了jest,配合mockjs用于模拟数据。在ts项目中使用jest还需要安装配置ts-jest和@types/jest。
在jest.config.js中配置preset:
{    preset: "ts-jest"}

jest为执行文件提供了describe等全局的方法,可以直接使用:

//test.tsdescribe("---lib 用例测试---", () => {    // 固定测试几个值    test("toXX", () => {        expect(lib.toXX(1)).toBe("01");        expect(lib.toXX(12)).toBe("12");        expect(lib.toXX('1')).toBe("01");        expect(lib.toXX('12')).toBe("12");    });})// lib.toXX是一个把数字类型月日转为XX字符类型的方法

package.json中配置test相关命令:

  "scripts": {    ...    "test": "jest",    "test-c": "jest --coverage",    "test-s": "jest --watchAll"  }

详细jest配置可参考官网 https://www.jestjs.cn/





04


单元测试技巧


多次循环提高测试准确性

对于一些不能保证一次正确就验证的函数,例如公农历转换等可能某些天正确某些天错误的情况,可以多次循环测试,提高准确性。
    // 随机取一天查询n天前    test("getPrevDay", () => {        for (let i = 0; i < 200; i++) {            // 测试一天前            let date = moment(Mock.mock('@date'));            let year = +date.year();            let month = +date.month() + 1;            let day = +date.date();            let nextD = date.add(-1, 'day')            expect(lib.getPrevDay(year, month, day)).toEqual({                year: nextD.year(),                month: nextD.month() + 1,                day: nextD.date(),            });
// 测试n天前 let date2 = moment(Mock.mock('@date')); let year2 = +date2.year(); let month2 = +date2.month() + 1; let day2 = +date2.date(); let n = Mock.mock({ "number|1-100": 100 }).number // 随机n天前 let futureD = date2.add(-n, 'day'); expect(lib.getPrevDay(year2, month2, day2, n)).toEqual({ year: futureD.year(), month: futureD.month() + 1, day: futureD.date(), }); } })
实际上,这个看起来有些“蠢”的方法,确实帮助找到了一些问题,例如:
北京时间
夏令时
1986年至1991年,中华人民共和国在全国范围实行了六年夏令时,每年从4月中旬的第一个星期日2时整(北京时间)到9月中旬第一个星期日的凌晨2时整(北京夏令时)。除1986年因是实行夏令时的第一年,从5月4日开始到9月14日结束外,其它年份均按规定的时段施行。夏令时实施期间,将时间向后调快一小时。1992年4月5日后不再实行。
在我通过时间戳计算日期差的时候,偶现部分日期总会出现差一天的情况,经过排查才了解到我国夏令时的政策,及时做兼容处理解决问题。

通过后端接口验证

从设计的角度,通过接口去做单元测试验证并不合理,实际上是一个“自己验证自己”的过程。
但是结合我们实际情况,这种方案是有可行性的,一是当前后端业务相关算法相对稳定,二是将后端代码改造为js成本较高,因此在库开发的初期我决定使用这种方法来做单测,可以快速有效的验证。
    // 公历转农历    test("solar2Lunar", () =&gt; {        for (let i = 0; i &lt; 100; i++) {            let date = moment(Mock.mock('@date'));            let year = +date.year();            let month = +date.month() + 1;            let day = +date.date();
let { lunarYear, lunarMonth, lunarDay, isLeap } = lib.solar2Lunar(year, month, day) expect(transferAPI(date.format('YYYYMMDD'), 0, 1)).resolves.toEqual({ date_id: `${lunarYear}${lib.toXX(lunarMonth)}${lib.toXX(lunarDay)}`, leap_month: isLeap ? 1 : 0 }) } })// transferAPI是一个异步方法,调用接口返回数据// expect验证异步方法要用到resolves,使用详情参考例子与官网





05


打包发布

打包

库的打包一般选择rollup,对于ts需要安装rollup-plugin-typescript2
// rollup.config.prod.jsexport default {    input: {        "calendar/index": "src/calendar/index.ts",        "almanac/index": "src/almanac/index.ts",    },    output: {        dir: '.',        format: 'cjs',        sourcemap: false,        chunkFileNames: "[name].js"        ...    },    plugins: [        typescript({            tsconfig: './tsconfig.json',            verbosity: 3,        }),        ...    ],};

配置打包命令

  "scripts": {    "build": "cross-env NODE_ENV=production rollup -c --config rollup.config.prod.js",    "build-dev": "cross-env NODE_ENV=development rollup -c --config rollup.config.prod.js",    "dev": "cross-env NODE_ENV=development rollup -c -w --config rollup.config.dev.js",    ...  },

发布

配置上传文件与命令

{  "files": [    "/almanac",    "/calendar",    "festivalList.js",    "/src",    "LICENSE",    "package.json",    "CHANGELOG.md",    "README.md"  ],  ...  "scripts": {    ...    "push": "npm run build &amp;&amp; yarn publish --registry=xxxxx",    ...  },}


通过publish命令可以把我们的库发布到npm,这样别人就可以通过npm insatll [包名]或其他方法安装使用我们的库。
如果希望把库发布到私服,需要提前注册登录私服,然后publish之后带上–registry=[私服地址]:
npm publish --registry=[私服地址]





06


总结


库的开发不是一蹴而就的,我们很难在一开始就做好所有的规划,因此我们可以在业务中一边使用一边完善我们的库,从v0.1.0或者v0.0.1开始,直到它进入到一个相对成熟易用的阶段,我们再去发布它的v1.0.0。
目前库开发相关的书籍并不是很多,这里推荐一本《现代JavaScript库开发原理、技术与实战》,其中很多知识技巧值得我们参考。



作者 | 赵智祺 Web前端开发工程师

本文来自微鲤技术团队,转载请注明出处。


【声明】内容源于网络
0
0
微鲤技术团队
践行数据驱动理念,相信技术改变世界。
内容 25
粉丝 0
微鲤技术团队 践行数据驱动理念,相信技术改变世界。
总阅读62
粉丝0
内容25