1. 项目搭建 -- 开发规范以及构建脚本的配置

635 阅读8分钟

这是一个学习github项目:vue-vben-admin的专栏,我会一步一步从零开始记录我的学习过程 vue-vben-admin源码:github.com/vbenjs/vue-… 本专栏对应的github:github.com/qq975036719…

1. 创建项目

pnpm create vite

1.1 安装@types/node提供node内置API的类型提示

pnpm i @types/node -D

1.2 安装@vitejs/plugin-vue-jsx支持jsx

pnpm i @vitejs/plugin-vue-jsx -D

1.2 修改tsconfig.json开启同时支持ESM和CJS方式导入

"allowSyntheticDefaultImports": true

1.3 添加类型声明文件的目录

项目中会用到很多的类型声明文件*.d.ts,我们需要有一个公共的入口存放它们,放在项目根目录下的types目录中,因此需要在tsconfig.json中配置一下

"typeRoots": ["./node_modules/@types", "./types"]

还要记得在**tsconfig.json****include**中包含**types**目录下的所有**ts****dts**文件

"types/**/*.d.ts",
"types/**/*.ts",

1.4 ts配置@路径别名

修改tsconfig.json

{
  "compilerOptions": {
    ...
    "baseUrl": "./",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

修改配置后需要重启vscode才会生效


1.5 vite配置@路径别名

修改vite.config.ts

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import * as path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
  },
  plugins: [vue()],
})

1.6 css相关

1.6.1 配置less全局变量

新建build目录,用来存放和项目构建有关的信息,包括vite.config.ts中的配置也分模块放到这里。

build目录下新建generate/generateModifyVars.tsless中用到的变量在这里配置

// build/generate/generateModifyVars.ts
/**
 * less 全局变量
 */
export function generateModifyVars() {
  return {
    'success-color': '#55D187',
    'error-color': '#ED6F6F',
    'warning-color': '#EFBD47',
    'font-size-base': '14px',
    'border-radius-base': '2px',
    'app-content-background': '#fafafa',
  }
}

还需要在tsconfig.node.json中包含build目录下的文件

{
  "include": ["vite.config.ts", "build/**/*"]
}

1.6.2 使用less/sass以及postcss

pnpm i less postcss postcss-html postcss-less autoprefixer -D

配置postcss,在src目录下创建postcss.config.js

module.exports = {
  plugins: {
    autoprefixer: {},
  },
}

vite.config.ts中配置css预处理器

css: {
  preprocessorOptions: {
    less: {
      modifyVars: generateModifyVars(),
      javascriptEnabled: true,
    },
  },
}

generateModifyVars()函数用于生成less的全局变量

/**
 * less 全局变量
 */
export default function generateModifyVars() {
  return {
    'success-color': '#55D187',
    'error-color': '#ED6F6F',
    'warning-color': '#EFBD47',
    'font-size-base': '14px',
    'border-radius-base': '2px',
    'app-content-background': '#fafafa',
  };
}

1.6.3 整合windicss

  1. 安装windicss以及vite-config-windicss
pnpm i windicss vite-plugin-windicss -D
  1. 加载到vite.config.ts
// vite.config.ts
plugins: [WindiCSS()],
  1. 导入windi.css
// main.ts
import 'virtual:windi.css'

1.6.4 配置全局样式入口

创建src/design/index.less,并写入一些全局样式

input:-webkit-autofill {
  box-shadow: 0 0 0 1000px white inset !important;
}

:-webkit-autofill {
  transition: background-color 5000s ease-in-out 0s !important;
}

html {
  overflow: hidden;
  text-size-adjust: 100%;
}

html,
body {
  width: 100%;
  height: 100%;
  overflow: visible !important;
  overflow-x: hidden !important;

  &.color-weak {
    filter: invert(80%);
  }

  &.gray-mode {
    filter: grayscale(100%);
    filter: progid:dximagetransform.microsoft.basicimage(grayscale=1);
  }
}

a:focus,
a:active,
button,
div,
svg,
span {
  outline: none !important;
}

2. 代码规范

2.1 eslint

  1. 安装
pnpm i eslint eslint-plugin-vue -D

# 让 eslint 识别 TypeScript 语法
pnpm i @typescript-eslint/parser -D

# 提供额外的适合 ts 的语法规则
pnpm i @typescript-eslint/eslint-plugin -D

# 用于编写 .eslintrc.js 时提供类型检查和语法提示
pnpm i eslint-define-config -D
  1. .eslintrc.js配置
// @ts-check
const { defineConfig } = require('eslint-define-config')
module.exports = defineConfig({
  root: true, // 指定为根配置,防止有上级的 eslint 继续向上查找配置文件
  env: {
    browser: true,
    node: true,
    es6: true, // 开启 es6 支持
  },
  parser: 'vue-eslint-parser',
  parserOptions: {
    parser: '@typescript-eslint/parser',
    ecmaVersion: 2020,
    sourceType: 'module',
    jsxPragma: 'React',
    ecmaFeatures: {
      jsx: true, // 开启 jsx 支持
    },
  },
  extends: [
    'plugin:vue/vue3-recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier',
    'plugin:prettier/recommended',
  ],
  rules: {
    // 遇到不需要的 lint 检查的时候在这里配置关闭
    '@typescript-eslint/no-var-requires': 'off',
  },
})
  1. .eslintignore忽略文件
*.sh
node_modules
*.md
*.woff
*.ttf
.vscode
.idea
dist
/public
/docs
.husky
.local
/bin
Dockerfile
  1. package.json中添加脚本
"lint:eslint": "eslint --cache --max-warnings 0  \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",

2.2 prettier

  1. 安装
pnpm i prettier eslint-config-prettier eslint-plugin-prettier -D
  1. 配置文件.prettierrc
{
  "printWidth": 100,
  "semi": true,
  "vueIndentScriptAndStyle": true,
  "singleQuote": true,
  "trailingComma": "all",
  "proseWrap": "never",
  "htmlWhitespaceSensitivity": "strict",
  "endOfLine": "auto"
}
  • semi:行尾分号
  • trailingComma:行尾加逗号
  • proseWrap:不折行
  1. 配置忽略文件.prettierignore
/dist/*
.local
.output.js
/node_modules/**

**/*.svg
**/*.sh

/public/*
  1. 添加package.json脚本
"lint:prettier": "prettier --write  \"src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",


2.3 stylelint

样式表文件linter

  1. 安装
pnpm i stylelint stylelint-config-html stylelint-config-prettier stylelint-config-standard stylelint-order -D
  1. 配置文件stylelint.config.js
module.exports = {
  root: true,
  plugins: ['stylelint-order'],
  customSyntax: 'postcss-less',
  extends: ['stylelint-config-standard', 'stylelint-config-prettier'],
  rules: {
    'selector-class-pattern': null,
    'selector-pseudo-class-no-unknown': [
      true,
      {
        ignorePseudoClasses: ['global'],
      },
    ],
    'selector-pseudo-element-no-unknown': [
      true,
      {
        ignorePseudoElements: ['v-deep'],
      },
    ],
    'at-rule-no-unknown': [
      true,
      {
        ignoreAtRules: [
          'tailwind',
          'apply',
          'variants',
          'responsive',
          'screen',
          'function',
          'if',
          'each',
          'include',
          'mixin',
        ],
      },
    ],
    'no-empty-source': null,
    'named-grid-areas-no-invalid': null,
    'unicode-bom': 'never',
    'no-descending-specificity': null,
    'font-family-no-missing-generic-family-keyword': null,
    'declaration-colon-space-after': 'always-single-line',
    'declaration-colon-space-before': 'never',
    // 'declaration-block-trailing-semicolon': 'always',
    'rule-empty-line-before': [
      'always',
      {
        ignore: ['after-comment', 'first-nested'],
      },
    ],
    'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }],
    'order/order': [
      [
        'dollar-variables',
        'custom-properties',
        'at-rules',
        'declarations',
        {
          type: 'at-rule',
          name: 'supports',
        },
        {
          type: 'at-rule',
          name: 'media',
        },
        'rules',
      ],
      { severity: 'warning' },
    ],
  },
  ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts'],
  overrides: [
    {
      files: ['*.vue', '**/*.vue'],
      extends: ['stylelint-config-recommended', 'stylelint-config-html'],
      rules: {
        'selector-pseudo-class-no-unknown': [
          true,
          {
            ignorePseudoClasses: ['deep', 'global'],
          },
        ],
        'selector-pseudo-element-no-unknown': [
          true,
          {
            ignorePseudoElements: ['v-deep', 'v-global', 'v-slotted'],
          },
        ],
      },
    },
  ],
};
  1. package.json中添加脚本
"lint:stylelint": "stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/"

3. Git规范

3.1 commitizen/commitlint

  1. 安装依赖
pnpm install -D commitizen cz-conventional-changelog @commitlint/config-conventional @commitlint/cli commitlint-config-cz cz-customizable
  • cz-customizable 用于自定义提示文案
  1. 配置package.json
{
  "scripts": {
    "commit": "git-cz"
  },
  "config": {
    "commitizen": {
      "path": "node_modules/cz-customizable"
    }
  }
}
  1. 配置commitlint.config.js
module.exports = {
  ignores: [(commit) => commit.includes('init')],
  extends: ['@commitlint/config-conventional'],
  rules: {
    'body-leading-blank': [2, 'always'],
    'footer-leading-blank': [1, 'always'],
    'header-max-length': [2, 'always', 108],
    'subject-empty': [2, 'never'],
    'type-empty': [2, 'never'],
    'subject-case': [0],
    'type-enum': [
      2,
      'always',
      [
        'feat', // 增加新功能
        'fix', // 修复问题/BUG
        'perf', // 优化/性能提升
        'style', // 代码风格相关无影响运行结果的
        'docs', // 文档/注释
        'test', // 测试相关
        'refactor', // 重构
        'build', // 构建相关
        'ci', // 持续集成
        'chore', // 依赖更新/脚手架配置修改等
        'revert', // 撤销修改
        'wip', // 开发中
        'workflow', // 工作流改进
        'types', // 类型定义文件更改
        'release', // 代码发布
      ],
    ],
  },
};
  1. 配置自定义提示文案:.cz-config.js
module.exports = {
  types: [
    { value: 'feat', name: 'feat: 增加新功能' },
    { value: 'fix', name: 'fix: 修复问题/BUG' },
    { value: 'perf', name: 'perf: 优化/性能提升' },
    { value: 'style', name: 'style: 代码风格相关无影响运行结果的' },
    { value: 'docs', name: 'docs: 文档/注释' },
    { value: 'test', name: 'test: 测试相关' },
    { value: 'refactor', name: 'refactor: 重构' },
    { value: 'build', name: 'build: 构建相关' },
    { value: 'ci', name: 'ci: 持续集成' },
    { value: 'release', name: 'release:  发布' },
    { value: 'deploy', name: 'deploy:   部署' },
    { value: 'test', name: 'test:     增加测试' },
    { value: 'chore', name: 'chore: 依赖更新/脚手架配置修改等' },
    { value: 'revert', name: 'revert: 撤销修改' },
    { value: 'wip', name: 'wip: 开发中' },
    { value: 'workflow', name: 'workflow: 工作流改进' },
    { value: 'types', name: 'types: 类型定义文件更改' },
    { value: 'release', name: 'release: 代码发布' },
  ],
  // override the messages, defaults are as follows
  messages: {
    type: '请选择提交类型:',
    customScope: '请输入修改的范围(可选):',
    subject: '请简要描述提交 message (必填):',
    body: '请输入详细描述(可选):',
    footer: '请输入要关闭的issue(可选):',
    confirmCommit: '确认使用以上信息提交?(y/n/e/h)',
  },
  allowCustomScopes: true,
  skipQuestions: ['body', 'footer'],
  subjectLimit: 72,
};

3.2 husky

  1. 安装
pnpm i husky lint-staged -D
  1. 添加package.json脚本
"prepare": "husky install"
  1. 添加hooks
npx husky add .husky/pre-commit "pnpm run lint:lint-staged"
  1. 添加commit-msg
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'
  1. 添加lint-staged配置文件./.husky/lintstagedrc.js
module.exports = {
  '*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
  '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': ['prettier --write--parser json'],
  'package.json': ['prettier --write'],
  '*.vue': ['eslint --fix', 'prettier --write', 'stylelint --fix'],
  '*.{scss,less,styl,html}': ['stylelint --fix', 'prettier --write'],
  '*.md': ['prettier --write'],
};
  1. package.json中添加lint脚本
"lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",

4. 集成pinia

4.1 安装pinia

pnpm i pinia

4.2 pinia目录结构

  1. 创建src/store/index.ts
import { createPinia } from 'pinia'

const store = createPinia()

export default store

5. 集成vue-router

5.1 安装vue-router

pnpm i vue-router@4

5.2 配置路由

创建src/router/router.config.ts

5.2.1 静态路由constantRoutes

// src/router/router.config.ts

// 静态路由
const constantRoutes: RouteRecordRaw[] = [
  {
    path: '/login',
    component: () => import('/@/views/login/index.vue'),
    name: 'login',
    meta: { title: '登录' },
  },
  {
    path: '/',
    name: 'App',
    redirect: '/app',
    meta: {
      title: 'App',
    },
  },
]

5.2.2 publicRoutes

配置403、404等路由信息

// src/router/router.config.ts

// 错误页面路由
export const publicRoutes: RouteRecordRaw[] = [
  {
    path: '/:pathMatch(.*)',
    redirect: '/404',
  },
  {
    path: '/404',
    component: () => import('@/views/404.vue'),
  },
]

5.2.3 accessRoutes

需要登录后才能访问的路由,先只配置一个首页,后面写业务的时候再完善。

// src/router/router.config.ts

// 需要登录以及相关权限才能访问的页面路由
export const accessRoutes: RouteRecordRaw[] = [
  {
    path: '/app',
    name: 'app',
    component: BasicLayout,
    redirect: '/app/home',
    meta: { title: '后台管理系统' },
    children: [
      {
        path: '/app/home',
        component: () => import('@/views/home/index.vue'),
        name: 'home',
        meta: {
          title: '首页',
          icon: 'liulanqi',
          auth: ['home'],
        },
      },
    ],
  },
]

5.3 导出router

src/router/index.ts中将router实例导出

import { createRouter, createWebHashHistory } from 'vue-router'
import routes from './router.config'

const router = createRouter({
  history: createWebHashHistory(),
  routes,
  strict: true,
  scrollBehavior: () => ({ left: 0, top: 0 }),
})

export default router

6. 集成ant-design-vue

6.1 安装ant-design-vue

pnpm i ant-design-vue

6.2 配置按需加载和自动导入

  1. 按需加载要用到unplugin-vue-components,自动导入要用到unplugin-auto-import,这样在使用vueref之类的函数时不需要导入可以直接使用(需要配置)。
pnpm i unplugin-vue-components unplugin-auto-import -D
  1. 修改vite.config.ts加载插件
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'

// https://vitejs.dev/config/
export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src'),
    },
  },
  plugins: [
    vue(),
    Components({
      dts: 'src/components.d.ts',
      resolvers: [AntDesignVueResolver()],
    }),
    AutoImport({
      imports: ['vue', 'vue-router', 'pinia'],
      dts: 'src/auto-imports.d.ts',
    }),
  ],
})

配置好后组件不用导入,也不用在components中注册了,直接在template中使用就行,不只是对于UI库的组件,即便是我们自己编写的组件也是可以的。


6.3 安装图标包

pnpm i @ant-design/icons-vue

7. Vite环境变量

  1. .env:所有情况下都会加载的配置文件
# port
VITE_PORT = 3100

# spa-title
VITE_GLOB_APP_TITLE = Plasticine Admin

# spa shortname
VITE_GLOB_APP_SHORT_NAME = vue_plasticine_admin
  1. .env.development:开发环境下才会加载的配置文件
# public path
VITE_PUBLIC_PATH = /
  1. .env.production:生产环境下才会加载的配置文件
# public path
VITE_PUBLIC_PATH = /

# Delete console
VITE_DROP_CONSOLE = true

# Whether to enable gzip or brotli compression
# Optional: gzip | brotli | none
# If you need multiple forms, you can use `,` to separate
VITE_BUILD_COMPRESS = 'none'

# Whether to delete origin files when using compress, default false
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false

# Whether to enable image compression
VITE_USE_IMAGEMIN= true

# Is it compatible with older browsers
VITE_LEGACY = false
  1. .env.test:测试环境下才会加载的配置文件
NODE_ENV=production
# Whether to open mock
VITE_USE_MOCK = true

# public path
VITE_PUBLIC_PATH = /

# Delete console
VITE_DROP_CONSOLE = true

# Whether to enable gzip or brotli compression
# Optional: gzip | brotli | none
# If you need multiple forms, you can use `,` to separate
VITE_BUILD_COMPRESS = 'none'

# Whether to delete origin files when using compress, default false
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false

# Whether to enable image compression
VITE_USE_IMAGEMIN= true

# Is it compatible with older browsers
VITE_LEGACY = false

vite默认是没有测试环境的,如果需要运行测试环境,应当加上--mode参数,比如

vite build --mode test

8. 配置构建脚本

该步骤主要是为了实现动态修改配置

构建项目之后,会自动生成 _app.config.js 文件并插入 index.html

_app.config.js 用于项目在打包后,需要动态修改配置的需求,如接口地址。不用重新进行打包,可在打包后修改 /dist/_app.config.js 内的变量,刷新即可更新代码内的局部变量

8.1 构建脚本

package.json

"build": "cross-env NODE_ENV=production vite build && esno ./build/script/postBuild.ts",
"build:test": "cross-env vite build --mode test && esno ./build/script/postBuild.ts",

8.2 esno

esno:用于直接运行ts代码,类似的库还有ts-node

pnpm i esno cross-env -D

8.3 cross-env

cross-env makes it so you can have a single command without worrying about setting or using the environment variable properly for the platform. Just set it like you would if it's running on a POSIX system, and cross-env will take care of setting it properly.

允许我们跨平台设置和使用环境变量,比如当我们使用 NODE_ENV = production 来设置环境变量的时候,大多数windows命令会提示将会阻塞或者异常,或者,windows不支持NODE_ENV=development的这样的设置方式,会报错。


8.4 postBuild.ts

build/script/postBuild.ts

// #!/usr/bin/env node

import { runBuildConfig } from './buildConf';
import chalk from 'chalk';

import pkg from '../../package.json';

export const runBuild = async () => {
  try {
    // 获取参数
    const argvList = process.argv.splice(2);

    // Generate configuration file
    if (!argvList.includes('disabled-config')) {
      runBuildConfig();
    }

    console.log(`✨ ${chalk.cyan(`[${pkg.name}]`)}` + ' - build successfully!');
  } catch (error) {
    console.log(chalk.red('vite build error:\n' + error));
    process.exit(1);
  }
};
runBuild();
  • argvList获取命令参数,当我们运行脚本时,比如npx esno ./build/script/postBuild.ts a 1 2 3,得到的argvList === ['s', 'a', '1', '2', '3']

获取到参数后判断是否要读取配置文件,如果有参数disabled-config则直接build,不读取配置。

  • import pkg from '../../package.json'需要在ts.config.json中开启resolveJsonModule选项,并且删除ts.config.node.json,全部的include都放在ts.config.json中管理即可,不需要分开管理。

接下来看看runBuildConfig的原理。


8.5 runBuildConfig

创建build/script/buildConf.ts,该文件中导出一个函数runBuildConfig,用于读取配置信息,为什么要这样设计呢?这是为了在修改一些配置的时候不用重新构建,直接修改配置文件即可生效

Generate additional configuration files when used for packaging. The file can be configured with some global variables, so that it can be changed directly externally without repackaging

先看看主要的函数runBuildConfig build/script/buildConf.ts

export function runBuildConfig() {
  // 读取指定前缀的环境变量 -- 默认前缀为 VITE_GLOB_
  // 如 .env 中有 VITE_GLOB_PLASTICINE = plasticine
  // 则 config.VITE_GLOB_PLASTICINE === 'plasticine'
  const config = getEnvConfig();
  // 读取配置文件变量名 -- 会被挂载到 window 对象上,并加上前后缀
  // 如读取到配置 VITE_GLOB_APP_SHORT_NAME = plasticine_admin
  // 则会有 window.__PRODUCTION__PLASTICINE_ADMIN__CONF__ 作为整个项目的配置
  const configFileName = getConfigFileName(config);

  // 根据配置文件的配置创建配置信息
  createConfig({ config, configName: configFileName, configFileName: GLOB_CONFIG_FILE_NAME });
}

一步一步来,先看看用到的第一个函数getEnvConfig

8.5.1 getEnvConfig

build/utils.ts

/**
 * 获取当前环境下生效的配置文件名
 */
function getConfFiles() {
  // 1. 获取执行的脚本 如运行 npx esno ./build/utils.ts --> esno .\\\\build\\\\utils.ts
  const script = process.env.npm_lifecycle_script;
  // 2. 用正则表达式匹配运行环境
  const reg = new RegExp('--mode ([a-z_\\d]+)');
  const result = reg.exec(script as string) as any;

  if (result) {
    // 传入了 mode 参数则使用对应的环境
    const mode = result[1] as string;
    return ['.env', `.env.${mode}`];
  }
  // 否则默认是生产环境
  return ['.env', '.env.production'];
}

/**
 * 获取指定前缀的环境变量
 * @param match 前缀
 * @param confFiles ext
 */
export function getEnvConfig(match = 'VITE_GLOB_', confFiles = getConfFiles()) {
  let envConfig = {};

  // 读取 confFiles 中的键值对配置项到 envConfig 对象中
  confFiles.forEach((item) => {
    try {
      // 读取当前脚本执行目录下的 confFiles 中的文件
      // 得到的 env 就是配置文件中的键值对
      const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item)));
      envConfig = { ...envConfig, ...env };
    } catch (e) {
      console.error(`Error in parsing ${item}`, e);
    }
  });

  // 只匹配 match 开头的配置项
  const reg = new RegExp(`^(${match})`);
  Object.keys(envConfig).forEach((key) => {
    if (!reg.test(key)) {
      // 不匹配的项利用 Reflect 反射将其从 envConfig 对象中删除
      Reflect.deleteProperty(envConfig, key);
    }
  });
  return envConfig;
}

8.5.2 getConfigFileName

build/utils.ts

/**
 * 获取配置文件的变量名
 * @param env
 */
export const getConfigFileName = (env: Record<string, any>) => {
  // 读取配置项 VITE_GLOB_APP_SHORT_NAME 进行拼接,没有配置该项则默认用 __APP
  return `__PRODUCTION__${env.VITE_GLOB_APP_SHORT_NAME || '__APP'}__CONF__`
    .toUpperCase()
    .replace(/\s/g, ''); // 去除空白字符
};

8.5.3 createConfig

这个才是重点,用于从读取到的配置项信息中动态注入一个配置项对象到windowbuild/script/buildConf.ts

/**
 * 根据配置参数去动态生成 window.项目配置属性 = { 配置项 }
 * 然后生成 _app.config.js 到打包输出目录下
 * @param params 配置参数
 */
function createConfig(params: CreateConfigParams) {
  const { configName, config, configFileName } = params;

  try {
    // 1. 生成注入代码
    const windowConf = `window.${configName}`; // 生成 项目配置属性名
    // 将配置项对象挂载到 windowConf 属性名上
    // 利用 Object.defineProperty 保证配置项对象不会被修改
    const configStr = `${windowConf}=${JSON.stringify(config)}
    Object.freeze(${windowConf});
    Object.defineProperty(window, ${windowConf}, {
      configurable: false,
      writable: false
    });`.replace(/\s/g, ''); // 去除空白字符

    // 2. 检查构建目录是否存在,不存在则会创建 -- 注意:用的是 fs-extra 而不是 fs
    fs.mkdirp(getRootPath(OUTPUT_DIR));

    // 3. 创建动态生成的配置文件到 OUTPUT_DIR 目录下,并且将注入的代码写入该文件中
    writeFileSync(getRootPath(OUTPUT_DIR, configFileName ?? '_app.config.js'), configStr);

    // 4. 输出配置文件生成成功信息
    console.log(chalk.cyan(`⭐ [${pkg.name}]`) + ` - configuration file is build successfully:`);
    console.log(chalk.gray(OUTPUT_DIR + '/' + chalk.green(configFileName)) + '\n');
  } catch (error) {
    console.log(chalk.red('❌ configuration file configuration file failed to package:\n' + error));
  }
}
  • 这里的fs不是node自带的fs,而是一个第三方库fs-extramkdirp会在目录不存在的时候创建目录
  • writeFileSync,从函数名就可以直到它是会以阻塞的方式创建文件的
  • chalk是一个用于控制台中输出有颜色文字的库

getRootPath能够获取项目根目录路径,并将传入的参数拼接到项目根目录路径后面,生成最终的绝对路径

/**
 * 获取 dir 相对于项目根目录的绝对路径
 * @param dir file path
 */
export function getRootPath(...dir: string[]) {
  return path.resolve(process.cwd(), ...dir);
}

本质上就是封装了一下path.resolve用于生成绝对路径 process.cwd()获取的是node命令执行时所在的路径


8.6 将生成的js加载到打包的html中

现在我们运行pnpm run build确实是可以生成_app.config.js这么一个配置文件了

window.__PRODUCTION__VUE_PLASTICINE_ADMIN__CONF__={"VITE_GLOB_APP_TITLE":"PlasticineAdmin","VITE_GLOB_APP_SHORT_NAME":"vue_plasticine_admin"};Object.freeze(window.__PRODUCTION__VUE_PLASTICINE_ADMIN__CONF__);Object.defineProperty(window,"__PRODUCTION__VUE_PLASTICINE_ADMIN__CONF__",{configurable:false,writable:false});

但是有一个问题,这个配置文件并没有被放到最终打包好的html文件中

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
    <script type="module" crossorigin src="/assets/index.e08c9cc4.js"></script>
    <link rel="stylesheet" href="/assets/index.b30a997d.css">
  </head>
  <body>
    <div id="app"></div>
    
  </body>
</html>

因此还差一步,就是让生成的js配置文件加载到html文件中,手动放上去太不靠谱了,这时候就要让viteplugin大显身手了,有一个插件vite-plugin-html,它可以帮我们实现注入代码到最终打包的html文件中的功能。

先安装vite-plugin-html

pnpm i vite-plugin-html -D

然后按该插件的文档进行如下配置

import { createHtmlPlugin } from 'vite-plugin-html';

export default defineConfig({
  plugins: [
    createHtmlPlugin({
      minify: true,
      inject: {
        data: {
          title: 'plasticine',
        },
        tags: [
          {
            tag: 'script',
            attrs: {
              src: '/_app.config.js',
            },
          },
        ],
      },
    }),
  ],
});

文档链接:vite-plugin-html文档

现在再进行打包,得到的html如下

<!doctype html><html lang="en"><head><script src="/_app.config.js"></script><meta charset="UTF-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>Vite App</title><script type="module" crossorigin src="/assets/index.e08c9cc4.js"></script><link rel="stylesheet" href="/assets/index.b30a997d.css"></head><body><div id="app"></div></body></html>

可以看到/_app.config.js被成功注入了

虽然现在是能够正常构建了,但是有一个问题,很多东西都是可配置的,但我们却把它们写死了,这十分不友好,接下来我们会进行一次代码重构,主要是搭建一个可配置的框架,将各个插件的配置分离到单个文件中去进行,再由一个统一的出口文件导出。这个我们放到下一篇文章讲解吧。