声明:文章为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
前言
大家好,我是易师傅,一个专门搞前端的搬(touch)砖(fish)师傅 ~
在上两篇文章中《如何去搞基建》和《如何去搞前端团队规范》中主要给大家介绍了 前端基建
和前端规范
的理论篇;
下面就给大家带来代码实战篇 — 《如何去搞企业工具库》;
至于为什么搞工具库,可以看看 前端基建这里的介绍
长话短说,咱们直接开干 ~
快速体验:
一、初始化项目
因为我们的目的是想做成一个 monorepo 仓库,而为什么用 monorepo 仓库,一句话解释就是想把 多个项目
放在 一个仓库中
管理,不懂的可自行搜索或参考《现代前端工程为什么越来越离不开 Monorepo》;
而使用 monorepo 仓库使用较多的一般就是yarn workspace
和 lerna 包管理工具
之类的,但是我们要使用的是 pnpm workspace
,至于为啥?
一句话解释就是:速度快,节省磁盘,安全性高;
1. 初始化
pnpm init
2. 创建 pnpm-workspace.yaml
touch pnpm-workspace.yaml
3. 修改 pnpm-workspace.yaml
packages:
- packages/*
- playground
- docs
4. 新增 packages/core
目录
mkdir packages/core
cd packages/core
pnpm init
5. 新增 typescript 依赖
pnpm i typescript @types/node -Dw
# 初始化
npx tsc --init
到这里一个简单的 pnpm monorepo 仓库就搭建的差不多了;
你的目录应该就是如下图这样子的(其中 LICENSE 和 README.md 为手动创建):
好家伙,一顿操作下来是不是很简单呢 ~
二、pnpm workspace 指南
1. 安装根目录依赖
pnpm
提供了 -w, --workspace-root 参数,可以将依赖包安装到工程的根目录下,作为所有 packages 的公共依赖。
pnpm i typescript -w
2. 安装开发依赖,需加上 -D
参数
pnpm i typescript -Dw
3. 单独给 packages/core
安装指定依赖
pnpm
提供了 --filter 参数;
可以对指定的 packages/core
进行操作,其中 --filter
跟着的参数是 <package_selector>
也就是初始化的 packages/core
中 package.json
的 name
字段:
# 使用
pnpm add <package_name> --filter <package_selector>
# 例子
pnpm add typescript -D --filter @vmejs/core
4. 如何运行 packages/core
中的 scripts
脚本
因为我们想直接运行指定某个包 packages/*
下的某个脚本,那么你可以这么做:
# 运行 @vmejs/core 包中的 dev 命令
pnpm dev --filter @vmejs/core
# 运行 @vmejs/core 包中的 build 命令
pnpm build --filter @vmejs/core
5. 各个 packages/*
模块包间的相互依赖
在实际开发中,不可能只存在一个 packages/core
包,可能还有 packages/shared
等,那么我们如何在 packages/core
中依赖 packages/shared
呢?
直接运行:
pnpm install @vmejs/shared -r --filter @vmejs/core
但是安装后的包会带上具体版本,这里是不推荐的,所以需要我们手动更改 packages/core
目录 package.json
下的 "@vmejs/shared": "workspace:^1.0.0”
为 "@vmejs/shared": "workspace:*"
,如下图
"@vmejs/shared": "workspace:*"
当然,pnpm workspace
的使用远不止于此,但是对于开发咱们的工具库足矣;
如若您想了解更多,可前往官网查看更多
三、配置 eslint + prettier + husky + commitlint
eslint + prettier:代码质量
与 代码风格
是我们必须要做的项任务,正所谓没有规矩不成方圆;
husky + commitlint:git hooks
和 提交检测
我们也是必须要集成的;
如果你还有疑问,可以看看我的上一篇文章《前端规范都有哪些》;
1. 配置 eslint:
-
安装
pnpm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin -Dw
-
新建
.eslintrc
和.eslintignore
文件编辑
.eslintrc
文件:{ "root": true, "env": { "browser": true, "es2021": true, "es6": true, "node": true }, "parser": "@typescript-eslint/parser", "parserOptions": { "sourceType": "module", "ecmaVersion": 12, "ecmaFeatures": { "jsx": true, "tsx": true } }, "plugins": ["@typescript-eslint"], "rules": { "no-console": "error", "no-debugger": "error" } }
-
在
package.json
中script
添加脚本"lint": "eslint . --ext .vue,.js,.ts,.jsx,.tsx,.json --max-warnings 0 --cache", "lint:fix": "pnpm run lint --fix",
2. 配置 prettier:
-
安装
pnpm i prettier eslint-config-prettier eslint-plugin-prettier -Dw
-
新建 .prettierrc(非必须,可不填)
{ "bracketSpacing": true, "jsxBracketSameLine": true, "jsxSingleQuote": false, "printWidth": 140, "semi": true, "useTabs": false, "singleQuote": true, "tabWidth": 2, "endOfLine": "auto", "trailingComma": "all" }
-
在
package.json
中script
添加脚本"format": "prettier --write --cache .",
到这里你可以自己新建一个 test.ts
去测试是否生效;
3. 配置 husky:
-
安装
pnpm i husky lint-staged -Dw
-
在
package.json
中script
添加脚本script:{ "prepare": "husky install", }, "lint-staged": { "*.{vue,js,ts,jsx,tsx,md,json}": [ "pnpm run lint", "pnpm run format" ] }
-
初始化
husky
(按顺序运行以下命令)# 按顺序运行以下命令 npx husky install npx husky add .husky/pre-commit "npx --no-install lint-staged"
4. 配置 commitlint
-
安装
pnpm i @commitlint/config-conventional @commitlint/cli -Dw
-
创建 commitlint.config.ts
module.exports = { extends: ['@commitlint/config-conventional'], };
-
运行
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'
好的,到这里我们就基本安装完成了,下面我们一起 git push 验证一下吧!
5. 验证
-
新建
.gitignore
文件.DS_Store .history .vscode .idea .eslintcache .pnpm-debug.log *.local dist node_modules types coverage
-
git 提交
git add . && git commit -m "init(init): init"
-
执行效果如下图
好的呢,到这里就搭建好基本架子了,下面就开始动手吧 ~
四、添加共享函数集合(@vmejs/shared)
@vmejs/shared
是一个共享的函数包,包含了其他包所使用的的一些公共方法;
1. 初始化 packages/shared
包(已有可忽略)
# 1.新建
mkdir packages/shared
# 2.初始化
cd packages/shared && pnpm init
2. 添加常见的共享工具函数:
-
新建
packages/shared/is
目录 -
新建
packages/shared/is/index.ts
(不细诉详细过程):export const isClient = typeof window !== 'undefined' export const isDef = <T = any>(val?: T): val is T => typeof val !== 'undefined' export const assert = (condition: boolean, ...infos: any[]) => { if (!condition) console.warn(...infos) } const toString = Object.prototype.toString export const isBoolean = (val: any): val is boolean => typeof val === 'boolean' export const isFunction = <T extends Function> (val: any): val is T => typeof val === 'function' export const isNumber = (val: any): val is number => typeof val === 'number' export const isString = (val: unknown): val is string => typeof val === 'string' export const isObject = (val: any): val is object => toString.call(val) === '[object Object]' export const isWindow = (val: any): val is Window => typeof window !== 'undefined' && toString.call(val) === '[object Window]' export const now = () => Date.now() export const timestamp = () => +Date.now() export const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, n)) export const noop = () => {} export const rand = (min: number, max: number) => { min = Math.ceil(min) max = Math.floor(max) return Math.floor(Math.random() * (max - min + 1)) + min } export const isIOS = /* #__PURE__ */ isClient && window?.navigator?.userAgent && /iP(ad|hone|od)/.test(window.navigator.userAgent) export const hasOwn = <T extends object, K extends keyof T>(val: T, key: K): key is K => Object.prototype.hasOwnProperty.call(val, key)
3. 导出
- 新建
packages/shared/index.ts
文件export * from './is';
4. 在 packages/core
包中使用
import { isString } from '@vmejs/shared';
五、开始第一个函数(@vmejs/core)
1. 初始化 packages/core
包(已有可忽略)
# 1.新建
mkdir packages/core
# 2.初始化
cd packages/core && pnpm init
2. 新增获取当前浏览器设备信息函数
-
新增
packages/core/getDevice/index.ts
目录mkdir getDevice && touch index.ts
-
编写代码(不细述实现过程)
import { isString } from '@vmejs/shared' export const DEVICES = [ { regs: [ /\b(sch-i[89]0\d|shw-m380s|sm-[pt]\w{2,4}|gt-[pn]\d{2,4}|sgh-t8[56]9|nexus 10)/i, /\b((?:s[cgp]h|gt|sm)-\w+|galaxy nexus)/i, /samsung[- ]([-\w]+)/i, /sec-(sgh\w+)/i, ], vendor: 'Samsung', }, { regs: [ /\((ip(?:hone|od)[\w ]*);/i, /\((ipad);[-\w\),; ]+apple/i, /applecoremedia\/[\w\.]+ \((ipad)/i, /\b(ipad)\d\d?,\d\d?[;\]].+ios/i, ], vendor: 'Apple', }, { regs: [ /(pixel c)\b/i, /droid.+; (pixel[\daxl ]{0,6})(?: bui|\))/i, ], vendor: 'Google', }, { regs: [ /\b((?:ag[rs][23]?|bah2?|sht?|btv)-a?[lw]\d{2})\b(?!.+d\/s)/i, /(?:huawei|honor)([-\w ]+)[;\)]/i, /\b(nexus 6p|\w{2,4}e?-[atu]?[ln][\dx][012359c][adn]?)\b(?!.+d\/s)/i, ], vendor: 'Huawei', }, { regs: [ /\b(poco[\w ]+)(?: bui|\))/i, // Xiaomi POCO /\b; (\w+) build\/hm\1/i, // Xiaomi Hongmi 'numeric' models /\b(hm[-_ ]?note?[_ ]?(?:\d\w)?) bui/i, // Xiaomi Hongmi /\b(redmi[\-_ ]?(?:note|k)?[\w_ ]+)(?: bui|\))/i, // Xiaomi Redmi /\b(mi[-_ ]?(?:a\d|one|one[_ ]plus|note lte|max|cc)?[_ ]?(?:\d?\w?)[_ ]?(?:plus|se|lite)?)(?: bui|\))/i, // Xiaomi Mi /\b(mi[-_ ]?(?:pad)(?:[\w_ ]+))(?: bui|\))/i, // Mi Pad tablets ], vendor: 'Xiaomi', }, { regs: [ /; (\w+) bui.+ oppo/i, /\b(cph[12]\d{3}|p(?:af|c[al]|d\w|e[ar])[mt]\d0|x9007|a101op)\b/i, ], vendor: 'OPPO', }, { regs: [ /vivo (\w+)(?: bui|\))/i, /\b(v[12]\d{3}\w?[at])(?: bui|;)/i, ], vendor: 'Vivo', }, { regs: [ /(Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i, ], vendor: 'other', }, ] /** * 获取设备类型与供应商 * @param ua window.navigator.userAgent * @returns { model: '', vendor: '' } */ export const getDevice = (ua?: string) => { const device = { model: '', vendor: '', } if (!isString(ua)) { // node runtimes env if (global) return device ua = window.navigator.userAgent } device.model = 'pc' device.vendor = 'other' for (let i = 0; i <= DEVICES.length; i++) { if (!DEVICES[i]) break const { regs, vendor } = DEVICES[i] const findVal = regs.find(item => item.exec(ua as string)) if (findVal) { device.model = 'mobile' device.vendor = vendor break } } return device }
3. 导出:新建 packages/core/index.ts
文件
export * from './getDevice';
那写到这里其实咱们的第一个工具函数就完成了~
看了之后是不是觉得超简单呢?
是的,没错,的确简单!
但是我们在上篇文章《非大厂的我们,如何去卷一套标准的前端团队规范?》中有说明,前端测试的重要性;
所以咱们不可避免的要使用单元测试
来对我们的工具函数来进行 自动化测试
保证它的完整度;
六、单元测试
1. 测试工具的选择:
mocha
:Mocha 是一个功能丰富的 JavaScript 测试框架;jest
:facebook 出的一款 JavaScript 自动化测试框架,专注于简单性。vitest
:尤大团队打造,一个由 Vite 打造的单元测试框架,而且它很快!
无可厚非,我选择 vitest 来进行单元测试 ~
当然,你对其它测试框架比较熟悉,你亦可以选择,选择权在你 ~
2. 安装 vitest
pnpm i vitest -Dw
3. 新建 vitest.config.ts
import { resolve } from 'path'
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true
},
resolve: {
alias: {
'@vmejs/shared': resolve(__dirname, 'packages/shared/index.ts'),
'@vmejs/core': resolve(__dirname, 'packages/core/index.ts'),
},
},
})
4. 配置运行脚本:package.json
"script": {
...
"test": "vitest test", // 执行测试
"coverage": "vitest run --coverage" // 执行测试覆盖率,需要安装 @vitest/coverage-c8
...
}
5. 编写 packages/core/getDevice
函数的测试用例
-
新建
packages/core/getDevice/index.test.ts
文件import { expect, describe, it } from 'vitest' import { getDevice } from '.' describe('device test', () => { it('The device should return {}', () => { const browser = getDevice() expect(browser).toEqual({ model: '', vendor: '', }) }) it('The device should return mobile/Samsung', () => { const uaStr = 'Mozilla/5.0 (Linux; U; Android 2.3.5; de-de; SAMSUNG GT-S5830/S5830BUKS2 Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1' const device = getDevice(uaStr) expect(device).toEqual({ model: 'mobile', vendor: 'Samsung', }) }) it('The device should return mobile/Apple', () => { const uaStr = 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1' const device = getDevice(uaStr) expect(device).toEqual({ model: 'mobile', vendor: 'Apple', }) }) it('The device should return mobile/Apple', () => { const uaStr = 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1' const device = getDevice(uaStr) expect(device).toEqual({ model: 'mobile', vendor: 'Apple', }) }) it('The device should return mobile/Huawei', () => { const uaStr = 'Mozilla/5.0 (Linux; U; Android 7.0; zh-cn; KNT-AL20 Build/HUAWEIKNT-AL20) AppleWebKit/537.36 (KHTML, like Gecko) MQQBrowser/7.3 Chrome/37.0.0.0 Mobile Safari/537.36' const device = getDevice(uaStr) expect(device).toEqual({ model: 'mobile', vendor: 'Huawei', }) }) it('The device should return mobile/other', () => { const uaStr = 'Mozilla/5.0 (Linux; U; Android 2.3.5; en-gb; HTC Desire HD A9191 Build/GRJ90) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1' const device = getDevice(uaStr) expect(device).toEqual({ model: 'mobile', vendor: 'other', }) }) it('The device should return pc/other', () => { const uaStr = 'Mozilla/5.0 (Windows; U; Windows NT 5.1; de-CH) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.2' const device = getDevice(uaStr) expect(device).toEqual({ model: 'pc', vendor: 'other', }) }) })
6. 执行 pnpm test
如下图:
7. 执行 pnpm coverage
:测试覆盖率
-
安装
@vitest/coverage-c8
pnpm i @vitest/coverage-c8 -Dw
-
执行结果:
8. 其它测试工具的搭配(文章篇幅原因就不细述):
happy-dom
:在测试环境模拟 Web 浏览器,以便执行自动化测试脚本;@vitest/coverage-c8
:展示测试覆盖率;
到这里其实就已经可以大致的去使用了;
但是我们试想一下,要想做一个适合企业或者开源的一个工具库,那么配套的文档是必不可少的;
毕竟你写的代码不知道如何去使用那将完全失去了它的价值,所以项目文档是标配;
七、搭建文档
搭建文档的方法有很多,有开源的库
,也有一些现成的在线工具
,或者你有时间去自研
也是一个不错的选择;
为了专注于代码的实现层面,咱们可以使用现有的文档生成框架,那么 Vitepress
就是一个很不错的选择;
由于 Vitepress
是基于 Vite
的,所以它也很好的继承了 Bundless
特性,开发的代码能以秒级
速度在文档中看到运行效果,完全可以充当调试工具来使用。
1. 安装
pnpm i vitepress -Dw
2. 配置 vitepress.config.ts
:默认不需要配置
3. 配置运行脚本:package.json
"script": {
...
"docs:dev": "vitepress dev packages",
"docs:build": "vitepress build packages",
...
}
4. 运行 pnpm docs:dev
后如下图:
因为还未做任何配置,所以会是 404 页面
5. 基本配置:新建 packages/index.md
文件
---
layout: home
sidebar: false
title: vmejs
titleTemplate: 一个疯狂的开源前端工具库
hero:
name: vmejs
text: 一个疯狂的开源前端工具库
tagline: 🎉 一个疯狂的开源前端工具库
actions:
- theme: brand
text: 快速开始
link: /guide/
- theme: alt
text: 工具函数集合
link: /core/getDevice/
- theme: alt
text: Vue Hooks
link: /vue
- theme: alt
text: React Hooks
link: /react
- theme: alt
text: View on GitHub
link: https://github.com/jeddygong
features:
- title: 功能丰富
details: 众多工具函数任你选择
icon: 🎛
- title: React Hooks(建设中)
details: 集成你想要的 React hooks
icon: 🚀
- title: Vue Hooks(建设中)
details: 集成你想要的 Vue hooks
icon: ⚡
- title: 强类型支持
details: 使用TypeScript编写,良好类型支持
icon: 🦾
- title: SSR 支持(建设中)
details: 支持服务端渲染,无需额外配置
icon: 🛠
- title: 轻量级
details: 不依赖任何第三方库,体积小巧
icon: ☁️
---
6. 编写 packages/core/getDevice
函数的使用文档
-
新建
packages/core/getDevice/index.md
文件--- category: UA --- # getDevice 用于从 User-Agent(用户代理数据) 中解析出 `设备类型与供应商`,在浏览器(客户端)或 node.js(服务器端)中使用。 ## Usage ``` ts import { getDevice } from "@vmejs/core" const browser = getDevice() // do something ``` ## 文档 ### 参数 [ua]:可选] - 浏览器(客户端)中使用 [ua可选] ``` import { getDevice } from "@vmejs/core" // 未带参数 const browser = getDevice() // 使用参数 const browser = getDevice(window.navigator.userAgent) ``` - node.js(服务器端)中使用 [ua必传] ``` import http from 'http' import { getDevice } from "@vmejs/core" http.createServer(function (req, res) { // get user-agent header const browser = getDevice(req.headers['user-agent']); // write the result as response res.end(JSON.stringify(ua, null, ' ')); }) .listen(3000, '127.0.0.1'); ``` ### 返回值:`{model: '', vendor: ''}` - device.model:常见设备类型 ``` # Chrome/其它 pc # ios/android/平板 mobile ``` - device.vendor:常见供应商 ``` # mobile 常见供应商: Samsung, Apple, Coogle, Huawei, Xiaomi, OPPO, Vivo # 其它 other ```
7. 配置指导页:packages/guide/index.md
# 快速开始
## 安装
```bash
npm i @vmejs/core
```
## 简单使用
``` ts
import { getDevice } from "@vmejs/core"
const browser = getDevice()
// do something
```
<!-- 更多功能列表,请参阅 [functions list](/core/getDevice/)。 -->
8. 配置 vitepress
-
新建
packages/.vitepress
和packages/.vitepress/config.ts
文件:mkdir .vitepress && touch .vitepress/config.ts
-
配置
packages/.vitepress/config.ts
文件:const Guide = [ { text: 'Get Started', link: '/guide/' }, ] const functions = [ Guide, { text: 'getDevice', link: '/core/getDevice/' }, ] const vueHooks = [ functions, { text: '建设中', link: '' }, ] const reactHooks = [ functions, { text: '建设中', link: '' }, ] const DefaultSideBar = [ { text: '指导', items: Guide }, { text: "工具函数集合", items: functions }, { text: "Vue Hooks集合", items: vueHooks }, { text: "React Hooks集合", items: reactHooks }, ] export default { base: '/', title: 'velvet', lang: 'zh-CN', themeConfig: { logo: '/logo.png', lastUpdated: true, lastUpdatedText: '最后修改时间', socialLinks: [{ icon: 'github', link: 'https://github.com/vmejs/vmejs' }], nav: [ { text: '指南', link: '/guide/' }, { text: '函数集合', link: '/core/getDevice/' }, ], // 侧边栏 sidebar: { '/guide/': [ { text: '', items: DefaultSideBar } ], '/core/': [ { text: '', items: DefaultSideBar }, ], }, }, };
-
添加主题:
mkdir packages/.vitepress/theme && touch packages/.vitepress/theme/index.ts
-
配置
packages/.vitepress/theme/index.ts
文件:import DefaultTheme from 'vitepress/theme'; export default { ...DefaultTheme, };
9. 运行 pnpm docs:dev
后如下图:
到这里一个简单的文档库就搭建完成了,但是上面只能满足一些最基本的情况;
咱们试想一下后续函数库肯定会越来越多,那么显然上面手动添加每个路径和修改配置是不可取的;
秉持自动化的原则,针对工具库文档咱们后续会集成一个自动化生成的方法,贡献者只需要关注代码层面、单元测试和单个文档,降低代码的耦合度。
具体文章如果您感兴趣的话会在后续输出 ~
八、文档部署(Github/Gitlab Pages)
1. 文档部署的多种方式:
- 企业:一般都会部署在内网上,这个就需要运维童鞋做 CI/CD 了;
- 个人网站:最原始的办法就是
pnpm run docs:build
后把生成的包直接 copy 过去,当然你也可以做自动化; - GitHub/Gitlab:可用自动化来部署到对应的 GitHub Pages,下面就以 GitHub 的部署来讲解;
- 当然还有国外的一些免费的部署平台也很好用,这个可自行搜索;
2. 使用 GitHub 操作:
- 在配置文件
packages/.vitepress/config.js
,将base
属性设置为您的 GitHub 存储库的名称。如果你计划将站点部署到https://jeddygong.github.io/vmejs/
,那么你应该将 base 设置为'/vmejs/'
,以斜线开头和结尾。 - 创建
.github/workflows/docs-deploy.yml
文件:name: docs-deploy on: # 触发条件 # 每当 push 到 main 分支时触发部署 push: branches: [main] jobs: docs: runs-on: ubuntu-latest # 指定运行所需要的虚拟机环境(必填) steps: - uses: actions/checkout@v2 with: # “最近更新时间” 等 git 日志相关信息,需要拉取全部提交记录 fetch-depth: 0 - name: Install pnpm uses: pnpm/action-setup@v2 with: version: 7 - name: Setup Node.js uses: actions/setup-node@v2 with: # 选择要使用的 node 版本 node-version: '16' cache: 'pnpm' # 如果缓存没有命中,安装依赖 - name: Install dependencies run: pnpm install --no-frozen-lockfile --ignore-scripts # 运行构建脚本 - name: Build vitepress site run: pnpm docs:build env: DOC_ENV: preview NODE_OPTIONS: --max-old-space-size=4096 # 查看 workflow 的文档来获取更多信息 # @see https://github.com/crazy-max/ghaction-github-pages - name: Deploy to GitHub Pages uses: crazy-max/ghaction-github-pages@v2 # 环境变量 env: GITHUB_TOKEN: ${{ secrets.ACTION_SECRET }} with: # 部署到 gh-pages 分支 target_branch: gh-pages # 部署目录为 vitepress 的默认输出目录 build_dir: packages/.vitepress/dist
到这里如果你 git push 代码后会发现 GitHub 的 Actions 有运行,但是会一直构建失败;
这是为什么呢?
主要是由于权限的问题,所以下面我们生成对应的密钥才能操作;
3. 生成 GitHub 的 Secerts:
-
进入 GitHub 设置页面生成一个密钥:github.com/settings/to…
-
设置当前仓库的 Secerts
-
再重新去运行当前仓库的 Action
-
设置当前仓库的 pages 地址
-
访问:vmejs.github.io/vmejs/ 即可
到这里,咱们对工具库的搭配基本已经完成,下面就要开始着重输出环节了,毕竟开箱即用咱们还是要配置好的 ~
九、构建打包
要想别人开箱即用,那么构建打包少不了;
其实现在想要构建出 esm、cjs、iife
格式的构建工具有许多,像 Rollup、esbuild、webpack、vite、tsup、unbuild
都可以实现;
1. 什么是 esm、cjs、iife
格式
esm
格式:ECMAScript Module,现在使用的模块方案,使用import
export
来管理依赖;cjs
格式:CommonJS,只能在 NodeJS 上运行,使用require("module")
读取并加载模块;iife
格式:通过<script>
标签引入的自执行函数;
为了方便大家,让大家可以更快速的、更方便的、开箱即用的构建出 esm、cjs、iife
格式的包,咱们这里就以 tsup 为例;
2. 安装 tsup
pnpm add tsup -Dw
3. 在根目录下配置文件:tsup.config.ts
import { defineConfig } from 'tsup';
export default defineConfig([
{
entry: ['packages/core/index.ts'],
format: ['cjs', 'esm', 'iife'],
outDir: 'packages/core/dist',
dts: true, // 添加 .d.ts 文件
metafile: true, // 添加 meta 文件
minify: true, // 压缩
splitting: false,
sourcemap: true, // 添加 sourcemap 文件
clean: true, // 是否先清除打包的目录,例如 dist
},
{
entry: ['packages/shared/index.ts'],
format: ['cjs', 'esm', 'iife'],
outDir: 'packages/shared/dist',
dts: true, // 添加 .d.ts 文件
metafile: true, // 添加 meta 文件
minify: true, // 压缩
splitting: false,
sourcemap: true, // 添加 sourcemap 文件
clean: true, // 是否先清除打包的目录,例如 dist
},
]);
更多 defineConfig 参数详见
4. 在 package.json
下添加脚本
"scripts": {
"dev": "tsup --watch",
"build": "tsup"
},
5. 执行 pnpm dev
后如下图
6. 添加 packages/core、packaes/shared
子包的 package.json
入口:
{
...
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"unpkg": "./dist/index.global.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"require": "./dist/index.js",
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
},
"./*": "./*"
},
"engines": {
"node": ">=14",
"pnpm": ">=7.0.0"
},
"publishConfig": {
"access": "public"
},
...
}
7. 执行 pnpm build
打包构建即可
这里之所以选择 tsup
,主要是因为开箱即用、快速、易上手
等优点;虽然不及 rollup
的生态丰富,但是做一些工具库的代码构建足矣;
当然如果你对 rollup、webpack 或 vite
更熟悉,这里也可以用它们构建,可自行配置;
十、发布(npm/私有)
1. 登录 npm(按照提示输入用户名密码邮箱即可)
npm login
2. 注意:
- 如果发布的
npm
包名为:@xxx/yyy
格式,需要先在npm
注册名为:xxx 的organization
,否则会出现提交不成功; - 发布到
npm group
时默认为private
,所以我们需要手动在每个packages
子包中的package.json
中添加如下配置;"publishConfig": { "access": "public" },
3. 发布:npm publish
你是不是以为直接 npm publish
就发布成功了?
其实的确发布成功了,但是发布的是一个 monorepo 仓库
的代码,这也没办法用呢?
但是咱们的目的要分包发布,所以这时候搭配 pnpm workspace 的工具 changesets
就出现了
4. 安装 changesets
pnpm i @changesets/cli -Dw
5. 初始化 changesets
pnpm changeset init
6. 配置 package.json
的发布脚本
{
"script": {
"release": "changeset publish",
}
}
7. 运行 pnpm release
的最终发布
至此搞定 ~
总结
其实真要一篇文章详细的讲完所有的配置已经逻辑是不显示的,我想也没有多少人能愿意看完的下去;
但是上面的配置已经全部趋于一个完整的工具开源库;
只是其中有许多是需要我们优化的,这篇文章就不做详细的介绍了,因为每个优化都是一副长篇概论;
当然,既然做我们就要做好了,所以下面有几个自动化的优化是需要持续跟进的:
- 文档自动化打包,不需手动添加对应的路径;
- 文档抽离成一个单独的 docs 目录(可忽略);
- changeset 的更多功能使用,让开源更加轻松;
- Github 自动部署发布配置,自动 Tag/Release 等;
- 开源贡献指导文档;
- 更多 cli 脚本;
目前该开源库还是一个雏形,如果您想提升自己、对开源感兴趣、对 Vue Hooks 感兴趣、对 React Hooks 感兴趣,可以加入我们的 Team(vx: JeddyGong),一起 crazy 吧!
工具库模板地址:链接
Github 开源地址:链接
最后
该系列会是一个持续更新系列,关于 前端基建
,笔者主要会从如下图几个方面讲解,如果您想第一时间看到我的更新文章,可以关注我和我的《前端要搞基建》专栏
如果你对 Vite 感兴趣,可以看看我的专栏:《Vite 从入门到精通》
如果你对微前端感兴趣,可以看看我的专栏:《微前端从入门到精通》
如果想跟我一起讨论技术吹水聊球, 欢迎加入前端学习群聊(群人数太多,只能加vx,望谅解) vx: JeddyGong
感谢大家的支持,码字实在不易,其中如若有错误,望指出,如果您觉得文章不错,记得 点赞关注加收藏
哦 ~
关注我,带您一起搞基建 ~