为什么我们需要一个函数库:
假设我们在项目中,有一些比较通用的函数,比如:解析url, 复制对象,
我们用一个js文件保存这些通用的函数,用导出的方法来使用,他就是一个函数库了。
痛点是:如果多个项目使用这个函数库,我们就要考虑使用和维护优化的地方了,不然会导致添加或者修改函数库, 多个项目需要手动同步的问题
所以我们的函数库需要用到npm来做版本管理, 这样这个函数库就能更好的发挥他的能力
一个函数库应该具备
-
npm
版本管理 -
支持
按需引入 -
提供
多种模块导出方式 -
通过单元测试
-
文档
然后我们需要ts语法,加上了
-
支持
ts语法为了团队维护方便,加上了
-
自动化构建模板
选择打包工具构建
在开始的时候我们可以选一个打包工具:rollup或webpack
使用rollup构建
全局安装rollup
npm install rollup --global
-
构建项目目录
目录大概长这样:
构建的步骤:
-
新建src/
main.js, 这里只将index的模块全部暴露出去export * from './components/index' -
新建
package.json, 添加需要的依赖,包括rollup,typescript,还有rollup的两个插件/plugin-babel,plugin-typescript。{ "name": "ts-util-rollup", "version": "1.0.0", "scripts": { "build": "rollup -c" }, "main": "dist/index.js", // npm库入口 "dependencies": {}, "devDependencies": { "@rollup/plugin-babel": "^5.2.1", "@rollup/plugin-typescript": "^6.0.0", "rollup": "^2.32.0", "rollup-plugin-json": "^4.0.0", "tslib": "^2.0.3", "typescript": "^4.0.3" } } -
新增
rollup.config.js配置文件// rollup.config.js import json from 'rollup-plugin-json' import typescript from '@rollup/plugin-typescript' import pkg from './package.json' export default { input: 'src/main.ts', output: [ { name: pkg.name, file: pkg.main, format: 'umd' } ], plugins: [ json(), typescript({lib: ["es5", "es6", "dom"], target: "es5"}) ] }; -
编辑函数, 这里以一个很简单的检验手机号的函数为例,在src/components里添加一个模块函数
isMobile.ts,里面很简单就用正则判断了下手机号 ,我们这里直接用ts文件后缀export default function isMobile(v: any): boolean { return /^1[0-9][0-9]\d{8}$/.test(v) }然后引入到
index.ts里再统一暴露出去import isMobile from './isMobile' export { isMobile } -
运行打包命令
rollup -c或者我们定义的npm run build, 在dist里面会出现打包好的文件。dist/index.js:
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global['ts-util-rollup'] = {})); }(this, (function (exports) { 'use strict'; function isMobile(v) { return /^1[0-9][0-9]\d{8}$/.test(v); } exports.isMobile = isMobile; Object.defineProperty(exports, '__esModule', { value: true }); }))); -
验证
把打包好的
dist/index.js文件放到在一个项目里, 导入isMobile函数验证import {isMobile} from './index.js' ... console.log('手机号是否正确', isMobile('13680988189'))
使用webpack构建
- 安装webpack & webpack-cli
npm install --global webpack // 全局安装webpack
npm install webpack webpack-cli --save-dev // 此工具用于在命令行中运行 webpack
- 构建项目目录
-
src里的文件保持不变 -
新建
packjson.js{ "name": "ts-util-webpack", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "build": "webpack" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "ts-loader": "^8.0.11", "typescript": "^4.1.2", "webpack": "^5.8.0", "webpack-cli": "^4.2.0" } } -
新建
webpack.config.jsconst path = require('path'); module.exports = { entry: './src/main.ts', // 入口 output: { filename: 'index.umd.js', path: path.resolve(__dirname, 'dist'), library: 'tsUtil', libraryTarget: 'umd' // 导出模块为umd } }
-
添加支持
typescript安装
tsloadernpm install --save-dev typescript ts-loader新建
ts.config.js文件{ "compilerOptions": { "outDir": "./dist/", "noImplicitAny": true, "module": "es6", "target": "es5", "jsx": "react", "allowJs": true } }改造
webapck.config.js添加ts支持const path = require('path'); module.exports = { entry: './src/main.ts', output: { filename: 'index.umd.js', path: path.resolve(__dirname, 'dist'), library: 'tsUtil', libraryTarget: 'umd' }, // 模块, 使用tsloader解析ts文件 module: { rules: [ { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ } ] }, resolve: { extensions: [ '.tsx', '.ts', '.js' ] }, } -
打包验证
运行
npx webapck即可打包, 我们可以看到打包出了我们想要的文件:dist/index.umd.js, 同样的验证方法import {isMobile} from './index.umd.js' ... console.log('手机号是否正确', isMobile('13680988189'))
两种打包工具的取舍
webpack的诞生始于构建复杂的单页应用程序(SPA)的难题。
代码拆分解决了单页应用加载整个程序缓慢的问题, 将应用分解成可管理的块,按需加载,在我们需要快速响应的交互式网页中提升非常大。静态资源管理如图像和 CSS 可以导入到你的应用程序中,而且还能够被作为依赖图中的另一个节点
rollup则是用ES2015的模块设计,尽可能的高效构建能直接被其他js库应用的模块,这无疑是符合函数库的需求的,
- 他把所有代码放到一个地方,然后一次执行,
生成更简洁的代码,启动更快 提供功能单一,相对webpack更轻便
在社区里也有很多用webpack打包库的,也有很多应用用rollup来打包,目前在我们公司生产环境里用的是rollup来打包函数库,webpack则用于应用打包
发布到npm
-
运行
npm init初始化,会让你填内容,用默认的就好了 -
运行
npm login登录账号,还没有的话去npm官网注册一个 -
运行
npm publish发布 注意这里要用npm官方源,不能用淘宝源会输出一堆信息,我这里提示因为版本被拒绝了,因为我已经提交过1.0.0的版本,也就是说
不能publish相同版本,改一下packjson.js里的version,重新发布就行了。 -
在项目里安装验证:
提供更多模块规范
javascript内置是没有支持模块化,但是社区里给出了解决方案,就是模块规范,
对于你输出的dist文件,库的使用者可能和你用的不是同一个模块规范,如果要想库的使用者可以灵活的选择自己需要的模块方式,那就要对输出的格式做优化
配置几种主流的模块规范,使用者可以自己选择
output: [
{
name: pkg.name,
file: pkg.main,
format: 'umd'
},
{
file: pkg.common,
format: 'cjs'
},
{
file: pkg.module,
format: 'es'
}
],
我们这里配置三种模块, 而且加上后缀不一样,这样我们就能在dist目录里看到三个js文件,里面对应三种模块规范打包的代码
-
commonjs模块
- 使用
require和exports(module.exports)引用和导出的交互方式 - 同步加载
- 一个文件就是一个模块
配置后打包出来的代码
index.common.js:Object.defineProperty(exports, '__esModule', { value: true }); function isMobile(v) { return /^1[0-9][0-9]\d{8}$/.test(v); } exports.isMobile = isMobile;以及他的
调用规范:// ts-util 是库名称 const util = require('ts-util) // ... util.isMobile('123');commonJs属于node默认的模块规范,可以通过
webpack加载
- 使用
-
es模块
es6语言层面实现的模块机制, 未来模块标准import和export(export default)的模块交互- 浏览器支持有限
配置后打包出来的代码
index.module.js:
function isMobile(v) { return /^1[0-9][0-9]\d{8}$/.test(v); } export { isMobile };以及他的
调用规范:// ts-util 是库名称 import * as util from 'ts-util'; // ... util.isMobile('123');es模块
webpack和rollup都可加载
- umd模块
- 兼容的
通用模式 - 判断是否支持再使用
commonjs或amd或挂载到全局, 配置后打包出来的代码index.umd.js:
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : // 是否支持commonjs typeof define === 'function' && define.amd ? define(['exports'], factory) : // amd // 挂载全局 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global['ts-util-rollup'] = {})); }(this, (function (exports) { 'use strict'; function isMobile(v) { return /^1[0-9][0-9]\d{8}$/.test(v); } exports.isMobile = isMobile; Object.defineProperty(exports, '__esModule', { value: true }); }))); - 兼容的
添加单元测试
函数库必定要经过单元测试,这才是比较严谨的,而且一定要覆盖全
推荐用测试库jest,很多开源项目都在用
- 安装
jest和ts-jest
npm i jest --save-dev
npm i ts-jest --save-dev
- 新建一个
jest.config.js
module.exports = {
transform: {
'^.+\\.tsx?$': 'ts-jest' // ts文件使用ts-jest
}
}
- 编写测试文件:
isMobile.test.ts
import isMobile from './isMobile'
test('15919316514 正确返回true', () => {
expect(isMobile('15919316514')).toBe(true)
})
test('159193 少于11位返回false', () => {
expect(isMobile('159193')).toBe(false)
})
test('15919312312312312 超过11位返回false', () => {
expect(isMobile('15919312312312312')).toBe(false)
})
test('15919312312312312 返回false', () => {
expect(isMobile('15919312312312312')).toBe(false)
})
-
运行
jest可以看到我们通过了测试
自动化构建
如果我们现在要加一个新函数 我们会怎么做呢?
需要先去新建一个功能函数的ts文件,再新建一个jest的测试文件等等,如果文件层级比较深,随着函数库变丰富,层级深是必然的, 这个过程就会变得繁琐,还要再去文档里说明怎么新建,很麻烦的
我们可以用node文件操作自动化这一繁琐的过程 ,主要是利用node文件系统
-
我们先调整下文件结构使其更合理, 每个函数都有一个文件夹包裹, 测试文件放在里面
-
我们在
build文件夹里新建一个add.js的node运行文件add.js:
const fs = require('fs') const path = require('path') const readline = require('readline') // 终止 const exit = function (log) { console.error(log) process.exit(1) } // 模块路径 const MODULES_PATH = path.join(__dirname, '../src/components') // 模块名称 const MODULES = process.argv[2] if (!MODULES) { exit('请填入模块名称') } // 输出路径 const OUT_PATH = path.join(MODULES_PATH, MODULES) // 文件路径 const TEST_OUT_PATH = path.join(OUT_PATH, '__tests__') // 测试用例文件路径 const isExists = fs.existsSync(OUT_PATH) if (isExists) { exit(`${MODULES}目录已经存在`) } fs.mkdirSync(OUT_PATH) console.log('创建模块目录', OUT_PATH) fs.mkdirSync(TEST_OUT_PATH) console.log('创建测试目录', TEST_OUT_PATH) // 函数模版 const addFileTemp = ` export default function ${MODULES} (val:any) { return val } ` // 测试用例模版 const testTemp = ` import ${MODULES} from '../index' describe('${MODULES}', () => { it('结果通过啦', () => { expect(${MODULES}('a')).toBe('a') }) }) ` // 文件路径 const FILE_PATH = path.join(OUT_PATH, `/index.ts`) const TEST_PATH = path.join(TEST_OUT_PATH, `${MODULES}.test.ts`) // 写入文件 fs.writeFileSync(FILE_PATH, addFileTemp) fs.writeFileSync(TEST_PATH, testTemp) console.log('创建新模块成功', `路径: src/components/${MODULES}`)可以看出这个文件其实就是,通过我们输入的函数名称,在文件夹
/src/components/下帮我们创建了同函数名称的文件夹,里面包含了函数js文件,测试文件,并且都写入了基本的模板函数模板:
` export default function ${MODULES} (val:any) { return val } `测试文件模板:
` import ${MODULES} from '../index' describe('${MODULES}', () => { it('结果通过啦', () => { expect(${MODULES}('a')).toBe('a') }) }) ` -
然后我们在
package.json里添加script命令, 当我们使用npm run add的时候就是执行这个add.js文件"add": "node ./build/add.js" -
运行
npm run add cloneDeep新增一个深拷贝的函数控制台会输出
-
我们再去components文件夹下面看 cloneDeep已经生成好了
结构也符合我们的要求,我们就可以专注写函数的代码了,这对于多人协作来说非常方便
文档生成
如果是公司团队维护,那文档就很有必要,这里我懒得手写,就用了一个自动生成的apidoc
-
在代码中写上注释
/** * @api {function} is系列 isMobile * @apiName isMobile * @apiVersion 0.1.0 * @apiDescription 手机号码是否正确 * @apiGroup is系列 * @apiParam {any} v 手机号 * @apiSuccess (返回值) {boolean} val 结果 * */ export default function isMobile(v: any): boolean { return /^1[0-9][0-9]\d{8}$/.test(v) } -
新建命令
"doc": "apidoc -i src/modules/ -o doc/ -t apidoc/template/",-i src/modules/为需要apidoc去查找生成的源文件-o doc/为output即生成后的目录-t apidoc/template/为模板源文件,可以省略(方便部署的时候才可能会用到)
参考: