前言
先解释一下来由,大概率也是大家工作中经常会面临的问题:
- 所有的项目都会有一套自己的webpack配置,版本差异差的离谱,不同项目完全不兼容;
- 所有的项目都是从0到1的,搭建者能力不同、或者技术设计的针对点不同、考虑的全面程度不一样的等,很难给出一套比较实用又接近完美的配置,缺少沉淀;
我理想的状态是在cover住一定的兼容版本的前提下,尽可能去沉淀去优化一套打包编译工具,集思广益。 即使出现下一个版本,跨版本的问题,也可以基于当前比较完全的配置直接去针对性升级。这也是我理解的工程化的样子。
工程化组件,一定要考虑的是可复用性以及可扩展性。理解是,我是小霸王的一张游戏卡,插到指定版本的小霸王游戏机上,都可以运行~当然,前端技术迭代飞快,我们面对的是工程,要的是稳定而不是实验,所以,所有的工程化方案都是基于相对稳定版本依赖实现。
SuperBuilders作为一项前端打包编译通用解决方案提出,支持Webpacker、Rolluper、Viter,全部适配实际项目多环境打包的案例。
接下来结合实际项目配置,讲解一下vite常用配置。
- Viter
主要功能:
- 热更新:完善的TypeScript + Vue + sass技术栈热更新支持。
- 环境区分:
process.env.D_ENV或者__APP_ENV__判断,值为dev,test,pre,production,sim。 - 构建性能优化。
- 完全可扩展,暴露方法可以传入对应自定义配置进行融合。
传参解析
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
}
}