普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月28日首页

前端组件库开发实践:从零到发布

作者 codingWhat
2026年2月27日 23:53

为什么要搞自己的组件库?

先说说一些开发的名场面,看看你有没有中招:

  • 新开一个项目,复制粘贴上个项目的 components 文件夹,改改名字接着用;
  • 三个项目里同一套「表格 + 表单」各写各的,改一处要改三处;
  • 用现成 UI 库吧,按需加载配到怀疑人生,不用吧,自己从零写又太慢;
  • 产品说「这里要跟 XX 项目长得一样」,你发现那边早改版了,根本对不齐。

把共用的表格、表单、图表、布局、指令、工具函数收拢成一个库,统一维护、按需引用,多项目复用同一套体验和逻辑——这就是自研或二次封装组件库要解决的事。


一、先搭骨架:工程长什么样?

组件库不是 SPA,而是一个「被别的项目 import 的包」。举个实际项目为例:

my-ui-kit/
├── src/
│   ├── main.ts              # 库的唯一起点:样式、组件、指令、Composables 都从这里出去
│   ├── components/          # 按业务或功能分子目录Form、Table、Menu…
│   ├── directives/          # 自定义指令:复制、防抖、长按、拖拽等
│   ├── modules/             # 公共模块,比如 i18n、主题
│   ├── utils/               # 纯工具函数
│   └── vite-plugin.ts       # 可选:给使用方用的「按需解析」插件
├── playground/              # 本地调试用的示例项目,改完库立刻能看效果
├── dist/                    # 构建产物,不提交,发布时只发这个
├── locales/                 # 若有多语言,可随库一起发布
├── vite.config.ts
├── package.json
└── tsconfig.json

入口文件 main.ts 干三件事

  1. 样式:引入 Reset、UnoCSS/Tailwind 等,保证打出来的 dist/style.css 一份就够。
  2. 统一导出:组件、指令、Composables(如 useFormuseMenu)全部 export,方便 使用方 按需 import。
  3. 插件形态:导出一个带 install 的对象,使用方可以 app.use(MyUiKit) 一把梭全局注册。

入口示例:

// 样式最先
import '@unocss/reset/tailwind-compat.css'
import 'virtual:uno.css'

// 公共模块:i18n、指令安装函数等
import * as I18n from './modules/i18n'
export { I18n as I18nModule }
import { setupDirectives } from './directives'
export { setupDirectives }

// 组件:表格、表单、图表、菜单等
import Table from './components/Table/Table.vue'
import Form from './components/Form/Form.vue'
// ... 其余组件

// 指令
import XxxDirectiveCopy from './directives/modules/copy'
import XxxDirectiveDebounce from './directives/modules/debounce'
// ...

// 全局注册插件
export const globalPlugin = {
  install(app) {
    app.component('XxxTable', Table)
    app.component('XxxForm', Form)
    // ...
  }
}

// 具名导出:按需引用 + 友好 tree-shaking
export default globalPlugin
export { Table as XxxTable, Form as XxxForm, useForm, useMenu /* ... */ }
export { XxxDirectiveCopy, XxxDirectiveDebounce, /* ... */ }

这样使用方既可以「全量 + 全局注册」,也可以「只 import 用到的组件和 hooks」。


二、配 Vite 库模式:打出「可被 import 的包」

骨架有了,下一步是让 Vite 把项目打成库,而不是打成一个能跑的网页。

2.1 基础 lib 配置

vite.config.ts 里加上 build.lib

import { resolve } from 'path'
import pkg from './package.json'

export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, 'src/main.ts'),
      name: pkg.name,   // UMD 时挂到 window 上的名字
      formats: ['es', 'umd'],
      fileName: (format) => `${pkg.name}.${format}.js`,
    },
  },
})
  • es:给 Vite/Webpack/Rollup 用,支持 tree-shaking;
  • umd:给 script 标签或老环境兜底。

一执行 vite builddist/ 里就会出现 my-ui-kit.es.jsmy-ui-kit.umd.js,别的项目就能 import 了。

2.2 依赖别打包进来:external 是亲兄弟

组件库会用到 Vue、Element Plus、ECharts、vue-router 等——这些必须由使用方项目提供,不能打进你的 bundle,否则会出现「两份 Vue」「包体积爆炸」等问题。

rollupOptions 里把它们 external 掉,并告诉 UMD:「这些模块对应的是哪个全局变量」:

rollupOptions: {
  external: [
    'vue',
    'element-plus',
    'vue-router',
    'echarts',
    'vue-echarts',
    '@vueuse/core',
    'vue-i18n',
    // 若库里会 import 自己的子路径(如 locales),也要写进来,避免被打进 bundle
    'my-ui-kit/locales/zh-cn.json',
    'my-ui-kit/locales/en.json',
  ],
  output: {
    globals: {
      vue: 'Vue',
      'element-plus': 'ElementPlus',
      'vue-router': 'VueRouter',
      echarts: 'echarts',
      'vue-echarts': 'VueEcharts',
      'vue-i18n': 'VueI18n',
    },
    exports: 'named',  // 具名导出,方便 tree-shaking
  },
}

这样打出来的库又小又干净,运行时和业务项目共用同一套依赖,快去试试吧!


三、配 package.json:告诉 npm「入口在哪、发布什么」

3.1 主入口与类型

{
  "name": "my-ui-kit",
  "version": "1.0.0",
  "private": false,
  "main": "./dist/my-ui-kit.es.js",
  "module": "./dist/my-ui-kit.es.js",
  "types": "./dist/index.d.ts",
  "type": "module"
}
  • main / module:分别给 require 和 import 用(若只发 ESM,可都指到 es 产物)。
  • types:TS 声明入口。

3.2 使用 exports 一把梭

exports 可以精细控制「主入口、样式、多语言、Vite 插件」等子路径:

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/my-ui-kit.es.js",
      "require": "./dist/my-ui-kit.umd.js"
    },
    "./style": "./dist/style.css",
    "./dist/style.css": "./dist/style.css",
    "./locales/*": "./dist/locales/*",
    "./vite": {
      "types": "./dist/vite-plugin.d.ts",
      "import": "./dist/vite-plugin.js",
      "require": "./dist/vite-plugin.cjs"
    }
  }
}

这样使用方可以:

  • import X from 'my-ui-kit' → 主包;
  • import 'my-ui-kit/style' → 样式;
  • import zh from 'my-ui-kit/locales/zh-cn.json' → 多语言;
  • import { XxxComponentsResolver } from 'my-ui-kit/vite' → 按需解析插件(若你提供了)。

3.3 只发 dist,依赖交给使用方

{
  "files": ["dist"],
  "peerDependencies": {
    "vue": "^3.3.11",
    "element-plus": "^2.4.4"
  }
}
  • files:只把 dist 发上去,源码、playground、测试都不发。
  • peerDependencies:声明「我依赖这些,但请您自己装嘞」,避免重复安装、版本打架等问题。

四、TypeScript 类型:让用你库的人也有提示

源码是 .ts / .vue,构建出来是 .js,使用方在 TS 项目里要类型提示和类型检查,就得有一份 .d.tsvite-plugin-dts 会在 build 时根据源码自动生成声明文件:

import dts from 'vite-plugin-dts'

export default defineConfig({
  plugins: [
    dts({
      rollupTypes: true,  // 把零散的 .d.ts 滚成少量文件,发布更清爽
    }),
  ],
  build: { /* ... */ },
})

构建完 dist/ 里会有 index.d.ts 等,使用方装你的包就能自动获得类型补全。


五、样式与静态资源:别漏了 CSS 和 locales

  • 样式:在 main.ts 最上面引入 UnoCSS/Reset 等,构建后会生成 dist/style.css。在 package.jsonexports 里暴露 ./style,使用方 import 'my-ui-kit/style' 即可。
  • 多语言 / 静态资源:若库内用到了 locales/zh-cn.json 等,构建时要把它们拷到 dist,否则发布后引用会 404。用 rollup-plugin-copywriteBundle 阶段拷贝:
import copy from 'rollup-plugin-copy'

plugins: [
  copy({
    targets: [{ src: './locales', dest: 'dist/' }],
    hook: 'writeBundle',
  }),
]

记得在 exports 里加上 "./locales/*": "./dist/locales/*",使用方才能正确引用。


六、构建脚本与体积分析

构建和包信息都齐了,接下来把日常用的脚本配好,顺带加个体积分析,避免悄悄打进不该打的东西。

  • 先类型检查再构建:避免带着类型错误发布。例如 "build": "run-p type-check \"build-only\" --"build-only 里只跑 vite build
  • 体积分析:可以用 rollup-plugin-visualizer,构建时生成依赖占比图:
import { visualizer } from 'rollup-plugin-visualizer'

const isAnalysis = process.env.ANALYSIS === 'true'
plugins: [
  visualizer({ open: isAnalysis }),
]

脚本里加一条:"analysis": "cross-env ANALYSIS=true npm run build-only",需要时跑一下,可以心里有数。


七、本地联调:不发布也能在业务项目里用

在真正发 npm 之前,先用 link 在业务项目里跑一跑,确保装包、引用、样式、类型都没问题。

在组件库目录:

pnpm build
pnpm link --global

在业务项目目录:

pnpm link --global my-ui-kit

之后业务项目里的 import ... from 'my-ui-kit' 会直接指向你本地的 dist。改完库再执行一次 pnpm build,刷新页面就能看到效果——再也不用手动拷 dist 了,但是吧,这个地方还是会有缓存问题,没辙。


八、按需引入:既省心又省体积

全量 app.use(MyUiKit) 会把所有组件都打进 bundle;更推荐按需引用,让打包器帮你 tree-shake 掉没用的。

8.1 使用方自己按需 import

import { XxxTable, XxxForm, useForm } from 'my-ui-kit'
import 'my-ui-kit/style'

只要库是 ES Module + 具名导出,未用到的组件会被自然摇掉。

8.2 自动按需:unplugin-vue-components + 自定义 Resolver

如果希望使用方不用手写 import,直接在模板里写 <XxxTable /> 就自动从库里拉对应组件,可以给 unplugin-vue-components 提供一个自定义 Resolver。做法是:在库里导出一份「组件名 → 从哪个包、用什么名字引入」的规则。

例如在库的 src/vite-plugin.ts 里(包名、前缀已泛化):

import { ComponentResolver } from 'unplugin-vue-components/types'

// 可选:Composables 自动从库里引入,避免使用方到处写 import
export const XxxAutoImports = {
  'my-ui-kit': ['useForm', 'useMenu', 'useDrag']
}

export const XxxComponentsResolver = [
  {
    type: 'component',
    resolve: (componentName) => {
      if (componentName.startsWith('Xxx'))
        return { name: componentName, from: 'my-ui-kit' }
    },
  },
  {
    type: 'directive',
    resolve: (name) => {
      const map = {
        Copy: { importName: 'XxxDirectiveCopy' },
        Debounce: { importName: 'XxxDirectiveDebounce' },
        Draggable: { importName: 'XxxDirectiveDraggable' },
        WaterMarker: { importName: 'XxxDirectiveWaterMarker' },
        // ...
      }
      const item = map[name]
      if (!item) return
      return { name: item.importName, from: 'my-ui-kit' }
    },
  },
]

使用方在 vite.config.ts 里:

import Components from 'unplugin-vue-components/vite'
import { XxxComponentsResolver } from 'my-ui-kit/vite'

export default defineConfig({
  plugins: [
    Components({
      resolvers: [XxxComponentsResolver],
    }),
  ],
})

这样模板里用到的 Xxx* 组件和指令都会自动按需从 my-ui-kit 引入。注意:样式通常还是整份引入一次 my-ui-kit/style 即可。


九、发布到 npm

类型、构建、package.json、本地 link 都验证过了,就可以发版了:

  1. 版本号npm version patch(或改 package.json 里的 version)。
  2. 登录npm login(私有 registry 就按你们流程来)。
  3. 发布npm publish。若是 scoped 包且首次发,记得 npm publish --access public

发完,别人就能 pnpm add my-ui-kit ,然后愉快地使用啦。

总结:

把业务中积累的「components」 抽成公共包,从「复制粘贴」升级成「库模式构建 + 类型 + 规范发布 + 按需使用」,在多项目里复用同一套组件和指令就会稳很多。组件再也不是拖累,而是资源宝库!
后面还可以加上单元测试、文档站(如 VitePress)、Changelog 和语义化版本等等,一步步做成团队级的组件库产品。有坑一起踩,与掘友们共勉!

昨天 — 2026年2月27日首页

uniapp 多地区、多平台、多环境打包方案

作者 codingWhat
2026年2月27日 12:21

前言:本文基于真实的线上项目,介绍如何用一套代码支持多个地区多种平台(微信/支付宝小程序、H5 等)、多种部署环境(开发/测试/生产)的构建与打包,并给出具体代码与配置说明。

开始之前,大家先想象这样一个场景:同一套业务,要同时给 A 省、B 省、C 省上线微信小程序和支付宝小程序,每个省的标题、首页内容、支付方式、后端网关地址都不一样,还要区分 dev / qa / prod 多套环境。如果每一套都复制一份代码维护,成本会很快失控。那么我这篇文章讲的,就是怎样通过「一套代码 + 一组脚本」把这些组合全部收拢起来,统一维护。


一、需求与目标

维度 说明 示例
地区(DISTRICT) 不同省份/地区,不同应用标题、页面配置、manifest 新疆、安徽、江苏、吉林…
平台(UNI_PLATFORM) 不同运行端 微信小程序 mp-weixin、支付宝 mp-alipay、H5
环境(DEPLOY) 不同 API 与部署目标 dev、qa02、release_anhui、xinjiang_prod…

目标:通过一条 npm 脚本即可确定「地区 + 平台 + 环境」,打出对应产物,无需改代码。


二、整体思路

npm script (cross-env 注入环境变量)
    ↓
process.env.UNI_PLATFORM / DISTRICT / DEPLOY / NODE_ENV
    ↓
vue.config.js 加载时先执行 preBuild.js(类似于webpack自定义plugin的效果)
    ↓
preBuild.js 根据上述变量生成 CLIENT_TYPE、APP_TITLE、API_BASE_URL 等
    ↓
动态生成 pages.json、manifest.json(按地区合并)
    ↓
Webpack 通过 EnvironmentPlugin 将变量注入业务代码
    ↓
构建产物中 process.env.XXX 被替换为常量

核心有以下三点:

  1. 环境变量驱动:用 UNI_PLATFORMDISTRICTDEPLOY 等控制整条构建链。
  2. 构建前预处理:在 Webpack 之前跑 preBuild.js,统一把「平台/地区/环境」转成业务需要的 CLIENT_TYPEAPI_BASE_URLAPP_TITLE 等。
  3. 配置与代码注入:用 Webpack 官方 EnvironmentPluginprocess.env 中的键注入到前端代码,保证运行时代码能拿到当前构建的「地区/平台/环境」。

三、具体实现步骤与代码说明

3.1 用 cross-env 在 npm script 中传参

不同操作系统下设置环境变量方式不同,使用 cross-env 可统一写法。

安装:

npm i -D cross-env

package.json 中的脚本示例:

{
  "scripts": {
    "build:mp-weixin:anhui:release_anhui": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin DISTRICT=anhui DEPLOY=release_anhui vue-cli-service uni-build",
    "dev:mp-weixin:anhui:release_anhui": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin DISTRICT=anhui DEPLOY=dev_anhui vue-cli-service uni-build --watch"
  }
}

含义简述:

  • cross-env KEY=value 会在当前 Node 进程中设置 process.env.KEY = value
  • 后面执行的 vue-cli-service uni-build 与它加载的 vue.config.jspreBuild.js 都运行在同一进程,因此能直接读到这些环境变量。
  • 命名约定:dev/build + 平台 + 地区 + 环境,便于一眼看出打的是哪一套。

关于 --watch

  • 开发脚本(如 dev:mp-weixin:...)末尾加上 --watch,表示 开启监听模式:当代码或配置文件变更时,会自动重新编译对应平台/地区/环境的产物,无需每次手动重新执行命令,适合本地联调和反复修改。

常见变量:

变量 含义 示例
NODE_ENV 开发/生产 development / production
UNI_PLATFORM 平台 mp-weixin、mp-alipay、h5
DISTRICT 地区 anhui、xinjiang、jiangsu
DEPLOY 部署环境 dev、qa、release_anhui
CUSTOM_TAB 可选,自定义 tab 配置 如 pay、order_search

3.2 统一配置:preBuild.config.js

把「平台 → 客户端类型」「地区 → 应用标题」「部署环境 → API 地址」等做成映射表,便于维护和扩展。

config/preBuild.config.js:

// 平台与客户端类型(用于接口等)
const CLIENT_TYPE_MAP = {
  h5: 1,
  'mp-weixin':2,
  'mp-alipay':3,
  'mp-baidu': 4,
  'mp-toutiao': 5,
  'mp-qq': 6
}

// 地区与应用标题等展示配置
const DISTRICT_CONFIG_MAP = {
  jilin: { APP_TITLE: 'xxx' },
  xinjiang: { APP_TITLE: 'xxx' },
  // ... 其他地区
}

// 部署环境与 API 根地址(示例域名为占位)
const API_BASE_URL_MAP = {
  dev: 'https://api-dev.example.com/mobile-app/api',
  qa: 'https://api-qa02.example.com/mobile-app/api',
  release_regionA: 'https://api-release-regionA.example.com/mobile-app/api',
  // ... 其他环境
}

module.exports = {
  CLIENT_TYPE_MAP,
  DISTRICT_CONFIG_MAP,
  API_BASE_URL_MAP
}

后续新增地区或环境时,只需在此处增补配置,无需改构建脚本逻辑。


3.3 构建前预处理:preBuild.js

在 Webpack 读取 vue.config.js 之前,需要把「平台/地区/环境」转成业务和配置生成器使用的变量。因此把 preBuild.js 放在 vue.config.js 最前面执行。

build/preBuild.js:

/**
 * 设置自定义的 process.env.X 需同时在 vue.config.js 的
 * configureWebpack.plugins 里加入 EnvironmentPlugin 对应 key
 */
const { CLIENT_TYPE_MAP, DISTRICT_CONFIG_MAP, API_BASE_URL_MAP } = require('../config/preBuild.config')

// 版本号等固定值(示例)
process.env.VERSION = '1.0.0'

// 由「平台」得到客户端类型
process.env.CLIENT_TYPE = CLIENT_TYPE_MAP[process.env.UNI_PLATFORM]

// 由「地区」得到应用标题(缺省用 jilin)
process.env.APP_TITLE = DISTRICT_CONFIG_MAP[process.env.DISTRICT || 'xuzhou'].APP_TITLE

// 由「部署环境」得到 API 根地址(缺省用 dev)
process.env.API_BASE_URL = API_BASE_URL_MAP[process.env.DEPLOY || 'dev']

// 可选:自定义 tab 等,未传则空字符串
process.env.CUSTOM_TAB = process.env.CUSTOM_TAB || ''

// 便于排查:构建时打印当前维度
console.log('------------------------------')
console.log('NODE_ENV:', process.env.NODE_ENV)
console.log('DEPLOY:', process.env.DEPLOY)
console.log('UNI_PLATFORM:', process.env.UNI_PLATFORM)
console.log('DISTRICT:', process.env.DISTRICT)
console.log('CLIENT_TYPE:', process.env.CLIENT_TYPE)
console.log('API_BASE_URL:', process.env.API_BASE_URL)
console.log('------------------------------')

// 根据 DISTRICT 等动态生成 pages.json、manifest.json
require('./pages.json.js')
require('./manifest.json.js')

要点:

  • 只读不写:从 process.env 读取由 cross-env 注入的 UNI_PLATFORMDISTRICTDEPLOY
  • 派生变量:写入 CLIENT_TYPEAPP_TITLEAPI_BASE_URLCUSTOM_TAB 等,供后续 Webpack 和动态配置使用。
  • 执行顺序:本文件在 vue.config.js 的顶部被 require,因此先于 Webpack 配置执行;下面的 pages.json.jsmanifest.json.js 会用到当前的 process.env.DISTRICT 等。

3.4 在 vue.config.js 中接入 preBuild 与 EnvironmentPlugin

vue.config.js:

const webpack = require('webpack')
const path = require('path')

// 必须最先执行:注入 CLIENT_TYPE、API_BASE_URL 等,并生成 pages.json / manifest.json
require('./build/preBuild.js')

module.exports = {
  configureWebpack: {
    resolve: {
      alias: {
        // 按地区做目录别名,业务里 import 来自 @district 即当前地区配置
        '@district': path.join(__dirname, 'src/district', process.env.DISTRICT)
      }
    },
    plugins: [
      // 将 process.env 中列出的 key 在编译时注入到业务代码中
      // 业务代码中 process.env.UNI_PLATFORM 等会被替换为构建时的常量
      new webpack.EnvironmentPlugin([
        'UNI_PLATFORM',
        'CLIENT_TYPE',
        'VERSION',
        'DISTRICT',
        'API_BASE_URL',
        'CUSTOM_TAB'
      ])
    ]
  }
}

说明:

  • require('./build/preBuild.js'):保证在任何 webpack 配置使用 process.env.DISTRICTprocess.env.API_BASE_URL 等之前,这些变量就已经就绪。
  • @district 别名:指向 src/district/${DISTRICT},便于按地区维护配置(如 src/district/anhui/config.js),业务侧统一从 @district 引用。
  • EnvironmentPlugin:Webpack 内置插件,会把数组中列出的 process.env.XXX编译时替换为当前构建时的值,因此业务里写 process.env.UNI_PLATFORMprocess.env.DISTRICTprocess.env.API_BASE_URL 等即可,无需再传参。

3.5 按地区动态生成 pages.json(build/pages.json.js)

不同地区可能需要不同的页面列表、tabBar 等,因此把「基础配置」与「地区配置」合并后再写入 src/pages.json

build/pages.json.js:

const fs = require('fs')
const path = require('path')
const lodash = require('lodash')
const standardPages = require('../config/pages.js')

let districtPage = {}
let customPages = []
const district = process.env.DISTRICT

// 若存在该地区的 pages 配置则合并
try {
  districtPage = require(`../config/districts/${district}/pages.js`)
} catch (e) {
  console.log(`不存在【${district}】地区差异化 page.js,略过`)
}

// 若有 CUSTOM_TAB,可再合并 custom.xxx.pages.js
process.env.CUSTOM_TAB.split(',').forEach((type) => {
  // ... 按 type 加载 config/custom.${type}.pages.js 与 config/districts/${district}/ 下同名文件
})

const pageJSON = lodash.mergeWith({}, standardPages, districtPage, ...customPages, (objValue, srcValue, key) => {
  if (Array.isArray(objValue) && key === 'list') {
    return srcValue  // 如 tabBar.list 用地区配置覆盖,而不是数组合并
  }
})

fs.writeFileSync(path.join(__dirname, '../src/pages.json'), JSON.stringify(pageJSON, null, 2), { encoding: 'utf8' })
console.log('page.json 构建完成')

思路:对基础 pages + 地区 pages + 自定义 tab 配置 做 merge,对 list 类数组采用覆盖策略,避免 tab 等被意外合并。


3.6 按地区动态生成 manifest.json(build/manifest.json.js)

小程序与 H5 的 appid、描述等可能按地区不同,同样采用「标准 manifest + 地区 manifest」合并。

build/manifest.json.js:

const fs = require('fs')
const path = require('path')
const lodash = require('lodash')

const standardManifest = {
  name: 'app',
  appid: 'xxx',
  'mp-weixin': { /* ... */ },
  'mp-alipay': { /* ... */ }
  // ...
}

let districtManifest = {}
try {
  districtManifest = require(`../config/districts/${process.env.DISTRICT}/manifest.json.js`)
} catch (e) {
  console.log('该地区不存在差异化 manifest.json.js')
}

const merged = lodash.merge({}, standardManifest, districtManifest)
fs.writeFileSync(path.join(__dirname, '../src/manifest.json'), JSON.stringify(merged, null, 4), { encoding: 'utf8' })
console.log('manifest.json 生成完成')

地区目录示例:config/districts/anhui/manifest.json.jsconfig/districts/xinjiang/pages.js 等,按需添加。


3.7 业务代码中如何使用

构建时环境变量已被注入,业务中直接读 process.env 即可。

按平台分支:

// 例如仅微信小程序展示某模块
if (process.env.UNI_PLATFORM === 'mp-weixin') {
  // 微信逻辑
}

// 计算属性中
moreModuleGroups: vm => process.env.UNI_PLATFORM === 'mp-weixin'
  ? xxxx
  : xxxx

按地区使用配置:

// 使用别名 @district,实际指向 src/district/${DISTRICT}
import banner from '@district/banner.jpg'

// 或直接使用注入的常量
const district = process.env.DISTRICT
const apiBase = process.env.API_BASE_URL

请求 API:

// 封装请求时用 process.env.API_BASE_URL 作为 baseURL
axios.create({ baseURL: process.env.API_BASE_URL })

四、目录与脚本约定小结

角色 路径/命令 作用
环境变量注入 cross-env UNI_PLATFORM=... DISTRICT=... DEPLOY=... 在 npm script 中传入维度
映射配置 config/preBuild.config.js 平台/地区/环境 → 客户端类型、标题、API
构建前逻辑 build/preBuild.js 设置 CLIENT_TYPE、API_BASE_URL 等并生成 pages/manifest
Webpack 入口 vue.config.js require preBuild、配置 @district、EnvironmentPlugin
动态页面配置 build/pages.json.js 合并基础 + 地区 pages,写回 src/pages.json
动态 manifest build/manifest.json.js 合并基础 + 地区 manifest,写回 src/manifest.json
地区前端配置 src/district/<DISTRICT>/config.js 业务通过 @district 引用
地区构建配置 config/districts/<DISTRICT>/pages.jsmanifest.json.js 仅该地区生效的页面与 manifest

五、新增地区/环境/平台时的操作清单

  1. 新增地区

    • config/preBuild.config.jsDISTRICT_CONFIG_MAP 中增加 APP_TITLE 等。
    • 如需差异化页面:在 config/districts/<新地区>/ 下增加 pages.js
    • 如需差异化 manifest:在 config/districts/<新地区>/ 下增加 manifest.json.js
    • src/district/ 下新增同名目录及 config.js(可参考现有地区)。
  2. 新增部署环境

    • config/preBuild.config.jsAPI_BASE_URL_MAP 中增加 DEPLOY → API 地址。
    • package.json 的 scripts 中增加对应 dev/build:平台:地区:环境 命令。
  3. 新增平台

    • CLIENT_TYPE_MAP 中增加平台与客户端类型。
    • 若 uniapp 支持该平台,只需在 script 中增加 UNI_PLATFORM=新平台 的 dev/build 脚本即可。

六、注意事项

  • EnvironmentPlugin 与 preBuild 一致:凡在 preBuild.js 里新加的 process.env.XXX,若要在业务代码中使用,需在 vue.config.jsEnvironmentPlugin 数组中增加 'XXX'
  • DISTRICT / DEPLOY 默认值:preBuild 中使用了 process.env.DISTRICT || 'xuzhou'process.env.DEPLOY || 'dev',未传时会有默认地区与环境,可按需修改。
  • 跨平台兼容:使用 cross-env 可避免在 Windows 与 Mac/Linux 下环境变量写法不一致的问题。

按上述方案,即可用「地区 + 平台 + 环境」三维度通过一条命令完成打包,并实现配置集中、扩展清晰,欢迎大家学习指正!

昨天以前首页

手把手系列之——前端工程化

作者 codingWhat
2026年2月25日 17:12

前言在公司,每天的工作都离不开一个内部平台,它的名字叫做devops(就是这么直白哈哈哈),这个系统包含了公司从商机、需求、开发、测试到部署的全流程。那什么是devops呢,本文将由此引入CI/CD前端自动化等一系列概念,并手把手带你进行实战!

什么是 DevOps?

image.png

DevOps 是 Development(开发)与 Operations(运维)的合成词(面试有被问到过,还好我会!),代表的是一套文化、实践与工具的集合,核心目标是打破开发与运维之间的壁垒,让软件从需求到上线的全流程更短、更稳、更可重复。

什么是 CI/CD?

CI/CD 是持续集成与持续交付/部署的缩写,是 DevOps 在「构建与发布」环节的具体实现方式

  • CI(Continuous Integration,持续集成)
    开发者频繁将代码合入主干(或主分支),每次合入后自动触发:拉取代码、安装依赖、执行 Lint、跑单测/集成测试、打包构建。目的是尽早发现集成错误,避免分支长期不合并带来的「大爆炸」式合并问题。

  • CD(Continuous Delivery / Deployment)

    • 持续交付:CI 通过后,产物处于「随时可发布」的状态,发布到生产支持人工审批或手动触发。
    • 持续部署:在持续交付的基础上再进一步,通过流水线自动将通过测试的版本部署到测试/预生产/生产环境。

简单来说就是:CI 负责「每次提交都能被自动验证」;CD 负责「通过验证的版本能快速、可控地发布出去」。二者通常由同一套流水线串联起来。

什么是前端自动化?

前端自动化 指的是在前端工程中,把原本依赖人工、重复性高的动作交给脚本或流水线自动完成,主要包括:

  • 代码质量:提交前/合入后自动执行 ESLint、Prettier、类型检查(TypeScript)等。
  • 测试:单元测试(Jest)、组件测试、E2E 测试在流水线中自动运行。
  • 构建与产物:根据分支或标签自动执行 npm/pnpm build,生成 dist 等产物,并归档或打镜像。
  • 部署:将构建产物自动同步到静态服务器、CDN,或构建成 Docker 镜像并推送到仓库、触发 K8s 等部署。

前端自动化的落地形式往往就是:在 CI/CD 流水线里为前端项目配置「安装依赖 → Lint → 测试 → 构建 → 部署」等步骤,从而减少人工操作、统一环境、提高发布频率与可靠性。

三者的联系

概念 定位 与另外两者的关系
DevOps 文化与流程层面的指导思想 强调「自动化、协作、快速反馈」;CI/CD 与前端自动化是其实践手段。
CI/CD 构建与发布环节的技术实践 在 DevOps 理念下,用流水线把「集成 → 测试 → 部署」自动化;前端项目参与其中即是在做「前端自动化」。
前端自动化 在前端域内的具体落地 通过接入 CI/CD(如 Jenkins、GitLab CI),把前端的检查、构建、部署纳入统一流水线,是 DevOps 在前端侧的体现。

总结
DevOps 解决「为什么要这样做」和「怎样组织」,是一个指导思想;
CI/CD 解决「用流水线把构建和发布自动化」;
前端自动化则是在前端项目里,用 CI/CD 和脚本把检查、构建、部署都跑起来

梳理完概念,可以回答这个问题了:为什么需要 DevOps 与 CI/CD呢?

传统研发的痛点

  • 发布慢、手工多:打包、上传、部署全靠人工,易出错且耗时长。像我们前端团队没有工程化之前需要手动维护十几个省市的社保渠道验证环境,要和服务器打交道不说,每次改完bug都需要手动上传到验证环境上,难受的要死

  • 环境不一致:本地、测试、生产环境差异大,开发动不动就说:在我本地好好的呀......

  • 反馈滞后:代码合并后很久才部署,问题发现晚、修复成本高

DevOps 与 CI/CD 能带来什么?

  • DevOps:开发与运维协作、流程自动化、基础设施即代码,缩短从需求到上线的周期。
  • CI(持续集成):代码合入后自动构建、测试,尽早发现集成问题。
  • CD(持续交付/部署):在 CI 通过后,自动或一键将产物部署到各类环境。

对前端而言,典型收益包括:提交即触发构建、自动跑 Lint/单测、自动打包并部署到测试/生产,减少重复劳动。


一、技术选型与整体架构

1.1 核心组件

角色 选型 说明
CI/CD 引擎 Jenkins 成熟、插件丰富,适合自定义流水线
代码仓库 GitLab 自带 CI/CD(GitLab CI),与 Jenkins 也可配合
容器化 Docker 统一构建与运行环境,便于「一次构建,到处运行」
私有仓库 Docker Registry / Harbor、Verdaccio(npm) 镜像与前端包私有化

说明:Gogs 仅适合做轻量 Git,不支持 CI/CD;若要做自动化构建与部署,建议用 GitLab(企业一般都本地部署) + 第三方 CI。

1.2 前端在流水线中的流程

代码提交 → 拉取代码 → 安装依赖 → Lint/测试 → 构建(dist) → 产物归档/镜像构建 → 部署

二、Jenkins 安装与基础配置

2.1 两种安装方式

  • 直装:在宿主机装 Java + Jenkins,与系统耦合,升级、迁移不便。
  • Docker 安装(推荐):隔离环境、易迁移、易复现。

单容器快速启动示例:

docker run -u root -d -p 10050:8080 -p 50000:50000 \
  -v /var/run/docker.sock:/var/run/docker.sock \
  jenkins/jenkins:2.479

验证容器是否运行:

docker ps | grep jenkins

若希望 在 Jenkins 里执行 Docker 命令(例如在流水线中 docker build / docker push),有两种常见做法:

  1. 挂载宿主机 Docker Socket:Jenkins 容器通过 /var/run/docker.sock 使用宿主机 Docker(上面命令已挂载)。
  2. Docker-in-Docker(DinD):在独立容器中跑 Docker 守护进程,Jenkins 通过 TCP 连接过去,与宿主机 Docker 隔离,避免版本冲突,更贴近「在 Jenkins 里跑 Docker」的直觉,有点那个俄罗斯套娃的感觉。

下面采用 DinD + 自定义 Jenkins 镜像 的方式,便于在流水线中稳定的使用 Docker。

2.2 自定义 Jenkins 镜像(内建 Docker CLI + 访问 Socket)

思路:基于官方 Jenkins 镜像,安装 Docker CLI,并让 Jenkins 用户能访问挂载的 docker.sock(或后续在 DinD 中通过 TCP 访问)。

示例 Dockerfile(使用腾讯/阿里镜像加速,便于国内环境):

ARG JENKINS_VERSION=2.479.1

FROM jenkins/jenkins:${JENKINS_VERSION}

USER root

# 使用国内镜像源
RUN apt-get install -y apt-transport-https \
    && if [ -f /etc/apt/sources.list ]; then sed -i "s@http://\\(deb\\|security\\).debian.org@https://mirrors.tencent.com@g" /etc/apt/sources.list; else echo "deb https://mirrors.tencent.com/debian $(. /etc/os-release && echo "$VERSION_CODENAME") main" > /etc/apt/sources.list && echo "deb https://mirrors.tencent.com/debian-security $(. /etc/os-release && echo "$VERSION_CODENAME")-security main" >> /etc/apt/sources.list; fi

RUN apt-get update

# 安装 Docker CLI(使用阿里云 Docker CE 源)
RUN apt-get install -y ca-certificates curl gnupg \
    && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
    && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://mirrors.aliyun.com/docker-ce/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
    | tee /etc/apt/sources.list.d/docker.list > /dev/null \
    && apt-get update \
    && apt-get install -y --no-install-recommends docker-ce-cli

# 使 jenkins 用户能访问 docker.sock(宿主机 docker 组 GID 多为 999,按需调整)
RUN groupadd -g 999 docker 2>/dev/null || true && usermod -aG docker jenkins

USER jenkins

构建:

docker build -t jenkins:2.479.1-docker .

测试(宿主机 socket 模式):

docker run --rm --name=test -v /var/run/docker.sock:/var/run/docker.sock \
  jenkins:2.479.1-docker docker -H unix:///var/run/docker.sock ps

通过 .sock 或 TCP 与 Docker 通信,可以避免在 Jenkins 容器内再装一套 Docker 引擎带来的版本与权限问题。

2.3 使用 Docker Compose 组网:Jenkins + DinD

下面用 Compose 把「Jenkins 容器」和「DinD 容器」组网,Jenkins 通过 TLS 的 TCP 连接 DinD,实现「在 Jenkins 里执行 Docker 命令」:
新建docker-compose.yml

services:

  jenkins-docker:
    image: arm64v8/docker:dind
    container_name: jenkins-docker
    privileged: true
    networks:
      jenkins:
        aliases:
          - docker
    environment:
      - DOCKER_TLS_CERTDIR=/certs
    volumes:
      - jenkins-docker-certs:/certs/client
      - jenkins-data:/var/jenkins_home

  jenkins-blueocean:
    image: jenkins:2.479.1-docker
    container_name: jenkins-blueocean
    restart: on-failure
    networks:
      - jenkins
    environment:
      - DOCKER_HOST=tcp://docker:2376
      - DOCKER_CERT_PATH=/certs/client
      - DOCKER_TLS_VERIFY=1
    volumes:
      - jenkins-data:/var/jenkins_home
      - jenkins-docker-certs:/certs/client:ro
      - /tmp:/tmp
    ports:
      - "10050:8080"
      - "50000:50000"

networks:
  jenkins:
    name: jenkins
    driver: bridge

volumes:
  jenkins-docker-certs:
  jenkins-data:
    driver: local
    driver_opts:
      type: none
      device: /home/jenkins/data

说明:

  • jenkins-docker:DinD 服务,暴露 2376(TLS)。
  • jenkins-blueocean:Jenkins 容器,通过 DOCKER_HOST=tcp://docker:2376 使用 DinD。
  • 证书与数据卷保证重启后 Jenkins 仍能连上 DinD,且数据持久化。

启动:

docker compose up -d

进入 Jenkins 容器排查时可用:

docker exec -it jenkins-blueocean /bin/sh

至此 Jenkins 安装与 Docker 环境就绪,可用于后续流水线中的镜像构建与推送。

2.4 基础配置(插件与镜像)

  • 访问:http://<宿主机IP>:10050(或你映射的端口)。
    image.png
  • 首次可先「选择插件来安装」,若部分插件安装失败可先跳过,稍后在「插件管理」中重试或更换更新中心。
  • 更新中心:若默认源慢,可在「Manage Jenkins → Plugin Manager → Advanced」中把 Update Site 换成国内镜像(如华为云,如果ping不通可以再换其他源),再安装 Node.js、Pipeline、Git、SSH 等插件。 image.pngimage.png
  • 权限:安装 Role-based Authorization Strategy,便于按项目/角色做细粒度控制。

三、GitLab 安装

若尚未安装 GitLab,可用 Docker 快速起一个(注意资源:建议 4 核 8GB 以上,避免 OOM):

  1. 新建目录,例如 gitlab

  2. 新建 .env

    GITLAB_HOME=/home/gitlab
    
  3. 新建 docker-compose.yml(示例为 ARM64,x86 可去掉 platform):

    services:
      web:
        image: 'gitlab/gitlab-ce:18.2.0-ce.0'
        platform: linux/arm64
        restart: always
        hostname: '10.211.55.4'
        environment:
          GITLAB_OMNIBUS_CONFIG: |
            external_url 'http://10.211.55.4:10082'
            gitlab_rails['gitlab_shell_ssh_port'] = 10083
        env_file:
          - .env
        ports:
          - '10082:10082'
          - '10083:22'
        volumes:
          - '$GITLAB_HOME/config:/etc/gitlab'
          - '$GITLAB_HOME/logs:/var/log/gitlab'
          - '$GITLAB_HOME/data:/var/opt/gitlab'
        shm_size: '512m'
    
  4. 执行 docker compose up -d 启动后,访问 http://<host>:10082/users/sign_in,即表示安装成功。 image.png


四、Jenkins 与 GitLab 打通(SSH + Webhook)

4.1 在 Jenkins 中配置 Git 凭据

image.png

  • 在 Jenkins「源码管理」中需要拉取 GitLab 仓库时,添加 Credentials

  • 类型选择 SSH Username with private key

  • 在宿主机或某台机器上生成密钥(若未使用默认路径,需在 Jenkins 中指定私钥路径):

    ssh-keygen -t ed25519 -C YourEmail
    cat <私钥路径>   # 将内容粘贴到 Jenkins 的 Key 中
    
  • Username 可填 git 或 GitLab 上对应用户名。

4.2 在 GitLab 中配置 Deploy Key

  • 进入项目:Settings → Repository → Deploy Keys
  • 新建部署密钥,把上面生成的 公钥 粘贴进去,勾选「公开访问」等按需勾选。
  • 这样 Jenkins 即可用该私钥拉取代码,无需个人账号密码。

4.3 Webhook(代码推送触发构建)

  • 在 Jenkins 任务中启用「构建触发器」:Build when a change is pushed to GitLab(需安装 GitLab 插件)。
  • 在 GitLab 项目:Settings → Webhooks,URL 填 Jenkins 的 GitLab webhook 地址,并设置与 Jenkins 中一致的 Secret 令牌
  • 保存后可发送 Test 请求验证,确保 Jenkins 能收到推送事件并触发任务。

这样可实现:git push → GitLab → Webhook → Jenkins 自动构建,符合 CI 的「提交即构建」理念。


五、前端流水线

5.1 手动构建与部署

在 Jenkins 任务中增加「执行 shell」步骤,例如:

pnpm i
pnpm build

建议:在 Jenkins 中配置 Node.js 环境(Node 插件)并指定版本;设置 淘宝源 或项目级 .npmrc 加速依赖安装。构建完成后,前端产物一般在 dist,可在 Workspace 中查看。

部署方式二选一或组合使用:

  • Publish over SSH:在 Manage Jenkins → System 中配置 SSH 服务器,在任务的 Build Steps 中增加「Send files or execute commands over SSH」,指定源为 dist/**、远程目录(如 /var/www/my-app),并可增加「在远程执行命令」(如重载 Nginx),完成「构建 → 上传到目标机」的简单 CD。
  • Docker 镜像:在仓库中准备 Dockerfile(多阶段构建:先 pnpm build,再只保留 dist + Nginx),在任务中执行 docker build -t my-app:${BUILD_NUMBER} .docker push <私有仓库>/my-app:${BUILD_NUMBER}
  • 私有仓库可选阿里云、自建 Registry、Harbor(详见第六节「前端私有化」),从而在 Jenkins 内完成「构建 → 打镜像 → 推仓库 → 在 K8s 或 Docker 主机上拉取并部署」的完整 CD。

5.2 Jenkins Pipeline

把「拉代码 → 装依赖 → 构建 → 测试 → 归档/镜像 → 部署」写成 Pipeline 脚本(Declarative 或 Scripted),可以实现版本化管理、复用、审查。

  • 在任务类型中选择 Pipeline
  • SCM 读取 Jenkinsfile,或直接在任务里写 Pipeline 脚本。
  • 使用 node { ... }stagesharchiveArtifactsdocker.build 等步骤编排前端流水线。

流水线即代码,便于版本管理与审查,也可与 GitLab 分支策略配合(例如仅对 main 分支执行生产部署)。

5.3 GitLab CI/CD

image.png

若希望「流水线跟着仓库走」,也可采用 GitLab CI

  • 在仓库根目录添加 .gitlab-ci.yml
  • 定义 stages(如 buildtestdeploy)和 jobs
  • 每个 job 指定 image(如 node:20)、script(如 pnpm i && pnpm build)、artifacts(保存 dist)。
  • 通过 GitLab Runner 在 Docker 或宿主机上执行,无需单独维护 Jenkins。

Jenkins 与 GitLab CI 也可并存:例如用 GitLab CI 做 MR 内的构建与测试,用 Jenkins 做发布与对接内部部署系统。


六、前端私有化

前端链路中常涉及两类私有仓库:Docker 镜像(用于部署)与 npm 包(组件库、工具包)。私有化后可实现内网构建、权限管控与版本的统一。

6.1 Docker 镜像私有化

构建好的前端镜像需要推送到私有仓库,供内网或 K8s 拉取部署,常见方案如下:

  • 阿里云容器镜像服务:托管在云上,开箱即用,免费额度有限(如 300 个命名空间后要收费),适合小团队或试用。
  • 自建 Docker Registry:官方 registry 镜像即可搭建,无图形界面,适合仅需「能推能拉」的场景。
    docker run -d -p 5000:5000 --restart=always -v /opt/registry:/var/lib/registry registry:2
    
    使用前需在 Jenkins 或本机 docker login < registry 地址> 配置账号。
  • Harbor:企业级镜像仓库,提供 Web 管理、权限、镜像扫描、多仓库复制等功能,适合规范化和多环境同步,需单独部署与维护。

在流水线中于 docker build 之后执行 docker push <私有仓库地址>/<镜像名>:<标签>,即可将前端镜像推送到上述任一类仓库。

6.2 npm 包私有化(Verdaccio)

团队内部组件库、工具包希望私有发布时,可用 Verdaccio 搭建 npm 私有源:

npm install --global verdaccio
npm i -g nrm
verdaccio
nrm add private http://localhost:4873/
nrm use private
npm adduser
npm login

发布:

npm init -y
npm publish

image.png

若流水线需从私有源安装依赖,在 Jenkins 中配置 .npmrc 或相应环境变量即可。


七、小结

环节 建议
代码仓库 GitLab(或 GitHub),支持 Webhook、Deploy Key。
CI Jenkins 或 GitLab CI,提交/合并后自动安装依赖、Lint、测试、构建。
构建环境 Node 版本固定,依赖缓存(如 pnpm store)、国内镜像加速。
产物 归档 dist 或构建镜像,便于追溯与回滚。
部署 通过 SSH 传文件或推送 Docker 镜像至服务器/K8s 供拉取部署。
私有化 Docker 用 Registry/Harbor,npm 用 Verdaccio。
流水线形态 Pipeline as Code(Jenkinsfile / .gitlab-ci.yml),可版本化、可复用。

按上述步骤一步步走下来,即可实现「手工打包上传」升级为「提交即构建、构建即可部署」的前端 DevOps 闭环;再结合分支策略、环境隔离与权限控制,就可以在保证质量的前提下提升发布效率与可重复性。
说回我们公司的devops,开发部分支持新建流水线,实现了新建分支 => 分支合并 => 打包部署(docker) => 成果上传的开发闭环,大大节省了运维时间和重复劳动,也推荐大家搞台服务器或者本地搞个虚拟机试试看!

❌
❌