【前端工程化】EasyBuilders-前端打包编译通用解决方案(02.EasyViter)

141 阅读8分钟

前言

先解释一下来由,大概率也是大家工作中经常会面临的问题:

  • 所有的项目都会有一套自己的webpack配置,版本差异差的离谱,不同项目完全不兼容;
  • 所有的项目都是从0到1的,搭建者能力不同、或者技术设计的针对点不同、考虑的全面程度不一样的等,很难给出一套比较实用又接近完美的配置,缺少沉淀;

我理想的状态是在cover住一定的兼容版本的前提下,尽可能去沉淀去优化一套打包编译工具,集思广益。 即使出现下一个版本,跨版本的问题,也可以基于当前比较完全的配置直接去针对性升级。这也是我理解的工程化的样子。

工程化组件,一定要考虑的是可复用性以及可扩展性。理解是,我是小霸王的一张游戏卡,插到指定版本的小霸王游戏机上,都可以运行~当然,前端技术迭代飞快,我们面对的是工程,要的是稳定而不是实验,所以,所有的工程化方案都是基于相对稳定版本依赖实现。

SuperBuilders作为一项前端打包编译通用解决方案提出,支持Webpacker、Rolluper、Viter,全部适配实际项目多环境打包的案例。

接下来结合实际项目配置,讲解一下vite常用配置。

  • Viter

主要功能:

  • 热更新:完善的TypeScript + Vue + sass技术栈热更新支持。
  • 环境区分:process.env.D_ENV或者__APP_ENV__判断,值为dev, test, pre, production,sim
  • 构建性能优化。
  • 完全可扩展,暴露方法可以传入对应自定义配置进行融合。

image.png

传参解析

export interface ConfigOptions {
  static: string; // 镜像节点 举例 https://aaaa.com/ccc/ddd
  devPort?: number; // 开发环境端口号
  report?: boolean; //是否调用webpack打印日志报告
}

主要分为以下几部分:

基础配置:

  • 通用基础配置
  • 生产基础配置
# 依赖

import _ from 'lodash';
import vue from '@vitejs/plugin-vue';                         // 提供 Vue 3 单文件组件支持
import vueJsx from '@vitejs/plugin-vue-jsx';                  // 提供 Vue 3 JSX 支持(通过 专用的 Babel 转换插件)。
import react from '@vitejs/plugin-react';                     // 提供完整的 React 支持
import legacy from '@vitejs/plugin-legacy';                   // 为打包后的文件提供传统浏览器兼容性支持, 前置依赖 terser
import commonjs from "rollup-plugin-commonjs";                // 加载 CommonJS 模块,Convert CommonJS modules to ES6
import importToCDN from 'vite-plugin-cdn-import';             // 生产环境CDN生产script支持,添加到html中
import externalGlobals from "rollup-plugin-external-globals"; // cdn
import viteCompression from 'vite-plugin-compression';        // GZip
import { createStyleImportPlugin, VantResolve } from 'vite-plugin-style-import'; // 第三方包样式按需引入
import AutoImport from 'unplugin-auto-import/vite';           // 自动导入Api
import Components from 'unplugin-vue-components/vite';        // 按需引入自定义组件
import { ElementPlusResolver, AntDesignVueResolver, NaiveUiResolver } from 'unplugin-vue-components/resolvers'


import { Root } from './utils';
import { ConfigOptions } from './types';

1.getBaseConfig 通用基础配置

无论是生产还是开发环境,共享的配置。

//获取基础配置
export function getBaseConfig(options: ConfigOptions) {

  const baseConfig = {
    mode: 'development',             // 'development' 用于开发,'production' 用于构建
    // root: process.cwd,            // 项目根目录(index.html 文件所在的位置)
    base: '/',                       // 开发或生产环境服务的公共基础路径,publicPath
    define: {                        // 定义全局常量替换方式。其中每项在开发环境下会被定义在全局,而在构建时被静态替换。
      'process.env.D_ENV': '"dev"',
      '__APP_ENV__': '"dev"'
    },
    resolve: {
      extensions: ['.tsx', '.ts', '.jsx', '.js'],
      // 导入时想要省略的扩展名列表。注意,不 建议忽略自定义导入类型的扩展名(例如:.vue)
      alias: {                       // 别名,当使用文件系统路径的别名时,请始终使用绝对路径
        components: Root('src/components'),
        '@': Root('src'),
      },
    },
    // cacheDir: "node_modules/.vite"// 存储缓存文件的目录。
    css: {
      preprocessorOptions: {         // 指定传递给 CSS 预处理器的选项。文件扩展名用作选项的键
        scss: {
          additionalData: `@use "@/assets/styles/element/index.scss" as *;`,
          charset: false,            // 打包时出现 warning: "@charset" must be the first rule in the file 警告
        },
        less: {
          modifyVars: {},
          javascriptEnabled: true
        }
      },
      // devSourcemap: false,         // 在开发过程中是否启用 sourcemap。
      // postcss: {}                  // 内联的 PostCSS 配置(格式同 postcss.config.js),
      // 或者一个(默认基于项目根目录的)自定义的 PostCSS 配置路径。
    },
    plugins: [
      vue(),
      vueJsx(),
      react(),


      createStyleImportPlugin({       // 第三方组件样式按需加载配置,前置依赖 consola
        resolves: [VantResolve()]
      }),

      AutoImport({
        resolvers: [ElementPlusResolver()],
        imports: ['vue', 'vue-router', 'pinia'] // 自动导入api
      }),
      Components({
        resolvers: [ElementPlusResolver(
          {
            importStyle: "sass",
            directives: true,
            version: "1.2.0-beta.1",
          }
        ), NaiveUiResolver()],
      }),
    ],
    // esbuild: {}                   // 继承自 esbuild 转换选项。
    // https://esbuild.github.io/api/#transform-api
    // appType: 'spa'
  };
  return baseConfig;
}

2.getProdBaseConfig生产环境基础配置

export function getProdBaseConfig(options: ConfigOptions) {
  const base = getBaseConfig(options);
  const baseProdConfig = {
    mode: 'production',
    base: '/',
    define: {
      'process.env.D_ENV': '"prod"',
      '__APP_ENV__': '"prod"'
    },
    build: {
      target: 'modules',              // 设置最终构建的浏览器兼容目标。
      outDir: 'dist',                 // 指定输出路径,默认为项目根目录下的 dist 目录
      polyfillModulePreload: true,    // 用于决定是否自动注入 module preload 的 polyfill.默认true
      // 如果设置为 true,此 polyfill 会被自动注入到每个 index.html 入口的 proxy 模块中。
      terserOptions: {
        compress: {
          keep_infinity: true,        // 防止 Infinity 被压缩成 1/0,这可能会导致 Chrome 上的性能问题
          drop_console: true,         // 生产环境去除 console
          drop_debugger: true,        // 生产环境去除 debugger
          pure_funcs: ['console.log'],// 移除console
        },
      },
      rollupOptions: {                // 自定义底层的 Rollup 打包配置。
        output: {
          // manualChunks(id) {
          //   if (id.includes('node_modules')) {
          //     return id.toString().split('node_modules/')[1].split('/')[0].toString();
          //   }
          // },
          // manualChunks(id) {
          //   if (id.includes('node_modules')) {
          //     const arr = id.toString().split('node_modules/')[1].split('/')
          //     switch (arr[0]) {
          //       case '@vue':
          //       case 'element-plus':
          //       case 'naive-ui':
          //       case '@element-plus':
          //         return '_' + arr[0]
          //         break
          //       default:
          //         return '__vendor'
          //         break
          //     }
          //   }
          // },
          manualChunks(id) {
            if (id.includes('node_modules')) {
              return id.toString().split('node_modules/')[1].split('/')[0].toString();
            }
          },
          entryFileNames: `assets/[name].[hash].js`,
          chunkFileNames: `assets/[name].[hash].js`,
          assetFileNames: `assets/[name].[hash].[ext]`,
        },
        external: ['waterMark'],              // cdn
        plugins: [
          commonjs(),                         // 加载 CommonJS 模块, Convert CommonJS modules to ES6
          externalGlobals({
            waterMark: "waterMark",           // cdn
          })
        ],
        // brotliSize: false,                 // 不统计
        target: 'esnext',
        minify: 'esbuild'                     // 混淆器,terser构建后文件体积更小
      },
      sourcemap: true,                        // 构建后是否生成 source map 文件。
      // reportCompressedSize: false,         // 取消计算文件大小,加快打包速度
      chunkSizeWarningLimit: 1500,            // chunk 大小警告的限制(以 kbs 为单位)
      assetsInlineLimit: 4000,
      cssCodeSplit: true,                     // 如果禁用,整个项目中的所有 CSS 将被提取到一个 CSS 文件中。
      // 当启用时,在异步 chunk 中导入的 CSS 将内联到异步 chunk 本身,并在其被加载时插入。
      minify: 'esbuild'                       // 设置为 false 可以禁用最小化混淆,或是用来指定使用哪种混淆器。
      // 默认为 Esbuild,它比 terser 快 20-40 倍,压缩率只差 1%-2%。
    },
    plugins: [
      vue(),
      vueJsx(),
      react(),
      // Gzip
      viteCompression({                       // 生成压缩包gz
        verbose: true,
        disable: false,
        threshold: 10240,
        algorithm: 'gzip',
        ext: '.gz',
      }),
      importToCDN({
        modules: [
          // {
          //   name: 'element-plus',
          //   var: 'ElementPlus',
          //   path: `https://unpkg.com/element-plus`,
          //   css: 'https://unpkg.com/element-plus/dist/index.css',
          // },
        ],
      }),

      AutoImport({
        resolvers: [ElementPlusResolver()],
        imports: ['vue', 'vue-router', 'pinia'] // 自动导入api
        // Auto import APIs on-demand for Vite, Webpack, Rollup and esbuild. 
        // With TypeScript support. Powered by unplugin.
      }),
      createStyleImportPlugin({
        resolves: [VantResolve()]
      }),
      Components({
        resolvers: [ElementPlusResolver({
          importStyle: "sass",
          directives: true,
          version: "1.2.0-beta.1",
        }), NaiveUiResolver(), AntDesignVueResolver()],
      }),
      legacy({
        targets: ['defaults', 'not IE 11']
      })
    ]
  };
  return _.merge({}, base, baseProdConfig);
}

Tips: Vite没有提供类似webpack.merge这样深层merge的方法。
因为工程化设计,所以要将一份vite配置拆分为基础共享配置、环境通用配置、用户自定义配置三部分,必定要涉及到合并,这里选用了lodash的merge方法,也希望vite官方早些给push上去这些常用工具。

3.getDevConfig开发环境配置

开发环境vite配置,是基于BaseConfig的。这其中需要配置ViteServer。

import _ from 'lodash'
import { defineConfig } from 'vite'
import { ConfigOptions } from './types'
import { getBaseConfig } from './vite.base.config'

// https://vitejs.dev/config/
export const getDevConfig = (options: ConfigOptions, extra = {}) => {
  return defineConfig(
    _.merge({}, getBaseConfig(options), {
      server: {
        port: options.devPort || 9006,
        strictPort: true,                  // 设为 true 时若端口已被占用则会直接退出,而不是尝试下一个可用端口。
        open: true,                        // 在开发服务器启动时自动在浏览器中打开应用程序。
        cors: true,                        // 为开发服务器配置 CORS

        // proxy: {                        // 为开发服务器配置自定义代理规则,根据我们项目实际情况配置,使用httpProxy
        //     '/api': {
        //         target: 'http://127.0.0.1:8000',    // 后台服务地址
        //         changeOrigin: true, // 是否允许不同源
        //         secure: false,      // 支持https
        //         rewrite: path => path.replace(/^/api/, '')
        //     }
        // }

        // headers                          // 指定服务器响应的 header。
        hmr: { overlay: true },
      },
      hmr: { overlay: true },               // 禁用或配置 HMR 连接(用于 HMR websocket 必须使用不同的 http 服务器地址的情况)
      plugins: [

      ]
    },
      extra || {}
    )
  )
}

4.getProdConfig开发环境配置

剩下几个环境的配置几乎一样的,只是defined的环境变量可能有所不同。另外publicPath也是不同的,但这部分已经抽离到方法入参中了。

import _ from 'lodash'
import { defineConfig } from 'vite'
import { ConfigOptions } from './types'
import { getProdBaseConfig } from './vite.base.config'

// https://vitejs.dev/config/
export const getPreConfig = (options: ConfigOptions, extra = {}) => {
  return defineConfig(
    _.merge({}, getProdBaseConfig(options), {
      mode: 'production',
      base: options.static,
      define: {
        'process.env.D_ENV': '"prod"',
        '__APP_ENV__': '"prod"'
      },
      // plugins: [
      // ],
    },
      extra || {}
    )
  )
}

5. 工具库

import * as path from 'path';

export function Root(...paths: string[]) {
  return path.join(process.cwd(), ...paths);
  // process.cwd() 是当前执行node命令时候的文件夹地址 ——工作目录, 保证了文件在不同的目录下执行时,路径始终不变
  // __dirname 是被执行的js 文件的地址 ——文件所在目录, 实际上不是一个全局变量,而是每个模块内部的
};

最后,扔一下配置依赖版本

{
  "name": "viter",
  "version": "0.0.2",
  "description": "font-end builder / viter",
  "main": "./dist/index.js",
  "typings": "./dist/index.d.ts",
  "scripts": {
    "dev": "tsc -w",
    "ncu": "ncu -u && yarn",
    "build": "yarn run clean && tsc",
    "clean": "rm -rf ./dist",
    "pub": "yarn run build && npm publish"
  },
  "author": "DEYI",
  "license": "ISC",
  "dependencies": {

  },
  "devDependencies": {
    "@types/fork-ts-checker-webpack-plugin": "^0.4.5",
    "@types/node": "17.0.21",
    "@typescript-eslint/eslint-plugin": "^5.15.0",
    "@typescript-eslint/parser": "^5.15.0",
    "@vitejs/plugin-vue": "2.2.0",
    "@vitejs/plugin-vue-jsx": "^1.3.10",
    "@babel/preset-typescript": "^7.13.0",
    "babel-core": "^6.26.3",
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-preset-env": "^1.7.0",
    "babel-preset-stage-0": "^6.24.1",
    "node-sass": "^4.14.1",
    "npm-check-updates": "^7.1.1",
    "rollup-plugin-commonjs": "^10.1.0",
    "rollup-plugin-external-globals": "^0.6.1",
    "sass": "^1.49.9",
    "postcss": "^8.4.5",
    "postcss-html": "^1.3.0",
    "postcss-plugin-px2rem": "^0.8.1",
    "postcss-px-to-viewport": "^1.1.1",
    "typescript": "4.5.4",
    "unplugin-auto-import": "0.6.1",
    "unplugin-vue-components": "0.17.21",
    "vite": "2.8.0",
    "vite-plugin-compression": "^0.5.1",
    "vite-plugin-style-import": "^2.0.0",
    "vue-tsc": "0.29.8"
  }
}

需要注意一个点,大家在配置TSConfig时候要注意一下,避免编译报错还有一堆波浪线。 算了,贴一下我的ts配置吧。

{
  "compilerOptions": {
    "baseUrl": "./",
    "skipLibCheck": true,
    "target": "esnext",
    "module": "esnext",
    "moduleResolution": "node",
    "noImplicitAny": false,
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "outDir": "dist",
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "lib": ["esnext", "dom"],
    "types": ["vite/client"],
    "isolatedModules": true,
    "paths": {
      "@/*": ["src/*"],
      "components/*": ["src/components/*"],
      "assets/*": ["src/assets/*"],
      "*": ["src/*"]
    }
  },
  "exclude": [
    "node_modules",
    "test",
    "dist"
  ],
  "awesomeTypescriptLoaderOptions": {
    "useWebpackText": true,
    "useTranspileModule": true,
    "doTypeCheck": true,
    "forkChecker": true
  }
}