背景
目前公司用的是基于 vue-cli4 开发的脚手架,在vue-cli的基础增加了业务开发的 多语言、多皮肤,以及接口请求,权限等解决方案,能快速搭建项目的基本结构。
但随着技术的迭代,vue2.7/vue3的推出(composition-api),vite的发展,以及vue-cli的不维护,webpack5(缓存+模块联邦)的推出,目前这套脚手架确实有些老旧,需要升级。
使用过 vue3+ts+vite 的开发体验还是很丝滑,但目前的公司的情况上这套解决方案,还面临一些问题。下面讨论一些方案的选择:
- vite
vite带来的开发体验的提升,目前的生态发展也是不错的;
使用 vite 搭建工程,但集成公司所有的组件库会有一些问题,而且很多插件还需重新开发,目前不适合迁移到vite。
- webpack5
webpack5 带来的 cache 特性,是一个折中的方案,webpack 的兼容性不用多说,同时缓存也可以提高开发启动的速度,提升开发体验。同时可搭配 esbuild-loader 代替 babel-loader ,提升构建速度。
- vue3
vue3的升级带来的生态割裂,导致组件库的如果直接想要迁移很麻烦。公司的组件库短期应该不会有 vue3 版本。
- vue2.7
vue官方推出 vue2.7,虽然今年就不维护了,但可使用 composition-api,目前来说体验还是不错的,但有一些小问题不维护了,整体上还是值得升级的。
- ts
vue2.6 如果要使用 ts,要使用装饰器写法,这种风格也不再推荐;使用 composition-api 搭配 ts 体验还是不错的。ts 虽然一开始写起来有些痛苦,解决各种报错,但是长期来讲可大大提高项目的可维护性。
基于以上情况的分析和讨论,我基于webpack5+vue2.7+typescript 搭建了项目脚手架,同时使用了 esbuild-loader, 提升了构建速度,下面开始详细介绍如何从0到1 搭建一个脚手架。
安装依赖
只列出一些重要的依赖,一些loader和plugin后面会介绍:
- vue 2.7.14
- pinia:代替vuex
- vue-loader 15.10.1 :注意vue2只能v15
- webpack 、webpack-cli、webapck-dev-server最新版本
环境和配置文件
从 package.json 的命令开始
json
{
"scripts": {
"dev": "webpack serve --env development",
"build": "webpack --env production",
"build:check": "run-p typecheck build:only",
"typecheck": "vue-tsc --noEmit",
"lint": "eslint . --ext .js,.mjs,.ts,.vue --fix --ignore-path .gitignore",
}
}
区分两个环境:production
和development
根目录的webpack.config.js
javascript
const prodConfig = require('./config/webpack.prod.js');
const devConfig = require('./config/webpack.dev.js');
const { loadClientEnv } = require('./config/env.js');
module.exports = env => {
// 根据 env 加载不同的配置文件
return env.production ? prodConfig() : devConfig();
};
接下来就编写webpack config文件:配置文件分为 webpack.prod.js
和 webpack.dev.js
,以及公用配置 webpack.base.js
,通过webpack-merge
进行合并。
webpack.base.js
javascript
module.exports = webpackEnv => {
const isProd = webpackEnv === 'production';
return {
entry: paths.appEntry, // 入口文件
devtool: isProd
? 'nosources-source-map'
: 'eval-cheap-module-source-map',
// 输出配置
output: {
path: paths.appOutput,
publicPath: paths.getPublicPath(),
filename: genAssetPath('js', isProd),
chunkFilename: genAssetPath('js', isProd),
hashFunction: 'xxhash64',
clean: true /
},
resolve: {
extensions: paths.moduleFileExtensions,
alias: {
'@': paths.appSrc,
vue$: 'vue/dist/vue.runtime.esm.js', // fix:$attrs is readonly / $listeners is readonly
},
symlinks: false // 提升性能
},
plugins: [
// 下文讲解
],
module: {
noParse: /^(vue|vue-router|pinia|vue-i18n|axios)$/,
rules: getRules(isProd)
},
performance: false // 关闭性能提示
};
};
- 借鉴cra脚手架,path相关的配置都统一到了
paths
对象中:
javascript
const moduleFileExtensions = ['.js', '.mjs', '.ts', '.vue', '.json'];
module.exports = {
appTitle: 'vuetage',
appPath: resolveApp('.'),
appEntry: resolveApp('src/main.ts'),
appOutput: resolveApp('dist'), // 输出路径
appPublic: resolveApp('public'),
appSrc: resolveApp('src'),
appHtml: resolveApp('public/index.html'),
dotenv: resolveApp('.env'),
getPublicPath: () => ensureEndSlash(process.env.BASE_URL || '/'), // 从.env文件中读取后动态获取
getAssetsPath: () => removeSlash(process.env.VUE_APP_ASSETS || ''),
moduleFileExtensions
};
output.hashFunction
配置hash算法,默认值是md4
,这里指定xxhash64
作为优化。从 webpack v5.54.0+ 起,hashFunction
支持将xxhash64
作为更快的算法,当启用experiments.futureDefaults
时,此算法将被默认使用。output.clean
为true,清除 dist 目录, webpack5之前需要使用 clean-webpack-plugin插件
plugins
javascript
plugins: [
new VueLoaderPlugin(),
VueDefineOptions(),
new ESLintPlugin({
extensions: ['vue', 'js', 'mjs', 'ts'],
context: paths.appSrc,
cache: !isProd,
emitWarning: true,
failOnError: false,
lintDirtyModulesOnly: !isProd
}),
new DefinePlugin(getClientEnv()),
new HtmlWebpackPlugin({
title: paths.appTitle,
template: paths.appHtml,
filename: 'index.html',
templateParameters: getClientEnv(true),
}),
new FriendlyErrorsWebpackPlugin({
clearConsole: false
}),
new ProvidePlugin({
process: 'process/browser' // fix: Uncaught ReferenceError: process is not defined
}),
new IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/
}),
new CaseSensitivePathsPlugin()
],
- VueLoaderPlugin:vue-loader的插件
- VueDefineOptions:使用
unplugin-vue-define-options
vue中可以使用宏defineOptions
- ESLintPlugin:eslint-loader已经deprecated,要使用
eslint-webpack-plugin
- ProvidePlugin:webpack内置插件,webpack5取消了注入环境的polyfill
rules
介绍了就是比较重要的模块的处理规则配置,配置各种loader:
javascript
function getRules(isProd) {
return [
...getVueRules(),
...getAssetRules(),
...getCssRules(isProd),
...getScssRules(isProd),
...getJsRules(isProd),
...getTsRules(isProd)
];
}
function getStyleRules(isProd, preOptions) {
return [
...getAssetRules(),
...getCssRules(isProd),
...getScssRules(isProd, preOptions)
];
}
js和ts这里都是用了esbuild-loader 代替babel-loader
javascript
/* js */
const getJsRules = isProd => [
{
test: /\.m?js$/,
loader: require.resolve('esbuild-loader'),
include: [resolveApp('src')],
exclude: jsExclude,
options: {
loader: 'js',
target: isProd ? browserTarget : 'esnext'
}
}
];
/* ts */
const getTsRules = isProd => [
{
test: /\.ts$/,
loader: require.resolve('esbuild-loader'),
include: [resolveApp('src')],
exclude: /node_modules/,
options: {
loader: 'ts',
target: isProd ? browserTarget : 'esnext'
}
}
];
webpack内置了静态资源的处理能力,不需要再额外安装loader。具体看文档,这里仅示例一下:
javascript
{
type: 'asset', // url-loader - {maxSize} - file-loader
test: /\.(png|jpe?g|gif|webp|avif)(\?.*)?$/,
generator: {
filename: genAssetPath('img')
},
parser: {
dataUrlCondition: {
maxSize: 4 * 1024 // 4kb
}
}
}
- 样式文件的loader通过一个函数获取
javascript
function getStyleLoaders(type, extract, preOptions = {}, sourceMap = false) {
const cssFinalLoader = extract
? {
loader: MiniCssExtractPlugin.loader,
options: {}
}
: {
loader: require.resolve('vue-style-loader'),
options: { sourceMap }
};
const rules = [
cssFinalLoader,
{
loader: require.resolve('css-loader'),
options: {
sourceMap,
importLoaders: 2
}
},
{
loader: require.resolve('postcss-loader'), // 配置在 postcss.config.js
options: { sourceMap }
}
];
if (type === 'scss') {
rules.push({
loader: require.resolve('sass-loader'),
options: { sourceMap, ...preOptions }
});
}
return rules;
}
- 默认关闭了 source-map,打开后使用 F12 调试样式可能会引发问题,vue-cli也是默认关闭的。
webpack.dev.js
javascript
// webpack.dev.js
module.exports = () => {
return merge(baseConfig('development'), {
mode: 'development',
devServer: {
static: {
directory: paths.appPublic,
publicPath: paths.getPublicPath()
},
devMiddleware: {
// remove last slash so user can land on `/test` instead of `/test/`
publicPath: paths.getPublicPath().slice(0, -1)
},
// history路径在刷新出错时重定向开启
historyApiFallback: {
index: paths.getPublicPath()
},
open: false,
proxy,
},
cache: {
type: 'filesystem'
}
});
};
- 代理进行了单独文件的配置,这里就不贴代码了
webpack.prod.js
javascript
// webpack.prod.js
module.exports = () => {
return merge(baseConfig('production'), {
mode: 'production',
optimization: {
minimizer: [new EsbuildPlugin()],
splitChunks: {
cacheGroups: {
defaultVendors: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
common: {
name: 'chunk-common',
minChunks: 2,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true
}
}
}
},
plugins: [
new MiniCssExtractPlugin({
filename: genAssetPath('css'),
chunkFilename: genAssetPath('css')
}),
new CopyPlugin({
patterns: [
{
from: paths.appPublic,
to: paths.appOutput,
globOptions: {
ignore: ['**/index.html']
}
}
]
}),
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html)$/,
threshold: 10240
})
],
stats: {
all: false,
// 生成资源信息
assets: true,
cachedAssets: true,
assetsSpace: 100,
assetsSort: '!size',
version: true
}
});
};
- 这里压缩用了esbuild-loader 内置的插件
eslint配置
参考了 create-vue 脚手架的.eslintrc.js
配置:
javascript
require('@rushstack/eslint-patch/modern-module-resolution');
module.exports = {
root: true,
extends: [
'plugin:vue/essential',
'eslint:recommended',
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier'
],
env: {
node: true
},
parserOptions: {
ecmaVersion: 2022
},
rules: {
// 自定义
}
};
相关依赖:
javascript
"@vue/eslint-config-prettier": "^7.1.0",
"@vue/eslint-config-typescript": "^11.0.3",
"eslint": "^8.46.0",
"eslint-plugin-vue": "^9.16.1",
"eslint-webpack-plugin": "^4.0.1",
.prettierrc.js
配置:
javascript
module.exports = {
printWidth: 80, // 换行长度
tabWidth: 2,
singleQuote: true, // 使用单引号
semi: true, // 分号
trailingComma: 'none', // 无尾随逗号
arrowParens: 'avoid', // 箭头函数单个参数不加分号
endOfLine: 'auto' // 换行符
}
ts配置
项目安装的 ts 4.9.5 版本,配置如下:
javascript
{
"compilerOptions": {
"module": "ESNext",
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"baseUrl": ".",
"moduleResolution": "Node",
"resolveJsonModule": true,
// class-field
"useDefineForClassFields": true,
"ignoreDeprecations": "5.0",
// Required in Vue projects
"jsx": "preserve",
// `"noImplicitThis": true` is part of `strict`
// Added again here in case some users decide to disable `strict`.
// This enables stricter inference for data properties on `this`.
"noImplicitThis": true,
"strict": true,
"allowJs": true,
// esbuild
"isolatedModules": true,
// For `<script setup>`
// See <https://devblogs.microsoft.com/typescript/announcing-typescript-4-5-beta/#preserve-value-imports>
"preserveValueImports": true,
// Enforce using `import type` instead of `import` for types
"importsNotUsedAsValues": "error",
// Recommended
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
// See <https://github.com/vuejs/vue-cli/pull/5688>
"skipLibCheck": true,
"paths": {
"@/*": ["./src/*"]
},
"types": [
"node",
"webpack-env"
]
},
"vueCompilerOptions": {
"target": 2.7
},
"include": ["src/**/*", "src/**/*.vue", "tests/**/*"],
"exclude": ["node_modules"]
}
总结
本文通过使用 webpack5 从0到1搭建项目脚手架,参考业内的很多最近实践。使用了 vue2.7+ts 提高了项目的整体可维护性,使用 esbuild-loader 以及 webpack5 的缓存提高了项目的整体构建速度。
目前不足之处是,没有开发cli 交互,配置文件全部暴露,需要了解 webpack 的配置才能进行个性化配置,后续可进行抽象和封装,加入cli 交互。
参考
- vue-cli
- create-vue
- create-react-app