代码仓库初始化模板 (7) -- Webpack

目录

Webpack 静态模块打包工具。

安装

1
npm install --save-dev webpack html-webpack-plugin vue-loader mini-css-extract-plugin copy-webpack-plugin unplugin-auto-import unplugin-vue-components babel-loader @babel/core @babel/preset-env vue-style-loader css-loader postcss-loader less-loader ejs-loader url-loader webpack-merge eslint-webpack-plugin stylelint-webpack-plugin css-minimizer-webpack-plugin webpack-dev-server get-port@5.1.1 webpack-bundle-analyzer @types/webpack-bundle-analyzer dayjs cross-env @babel/preset-typescript autoprefixer postcss-preset-env

Webpack 配置

webpack.config.base.ts

build/webpack/webpack.config.base.tsview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import { Configuration, DefinePlugin } from 'webpack';
import path from 'node:path';
// eslint-disable-next-line @typescript-eslint/no-require-imports
import HtmlWebpackPlugin = require('html-webpack-plugin');
import { VueLoaderPlugin } from 'vue-loader';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import CopyPlugin from 'copy-webpack-plugin';
import AutoImport from 'unplugin-auto-import/webpack';
import Components from 'unplugin-vue-components/webpack';
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';

const config: Configuration = {
context: path.resolve(process.cwd(), './src'),
entry: {
app: path.resolve(process.cwd(), './src/app.ts'),
},
output: {
clean: true,
filename: 'script/[name].[contenthash].js',
path: path.resolve(process.cwd(), './dist-web'),
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
},
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader',
},
{
test: /\.(ts|tsx)$/,
use: 'babel-loader',
},
{
test: /\.(less|css)$/,
use: [
'vue-style-loader',
{
loader: MiniCssExtractPlugin.loader,
options: {
esModule: false,
},
},
'css-loader',
'postcss-loader',
{
loader: 'less-loader',
options: {
lessOptions: {
javascriptEnabled: true,
},
},
},
],
},
{
test: /\.ejs$/,
loader: 'ejs-loader',
options: {
esModule: false,
},
},
{
test: /\.(gif|png|jpe?g|svg)$/i,
use: [
{
loader: 'url-loader',
options: {
name: 'images/[name]_[contenthash].[ext]',
limit: 1024 * 5,
esModule: false,
},
},
],
type: 'javascript/auto',
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.vue', '.json'],
alias: {
'@src': path.resolve(process.cwd(), './src'),
},
},
optimization: {
runtimeChunk: 'single',
// splitChunks 用来拆分代码
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
plugins: [
new HtmlWebpackPlugin({
// template: path.resolve(selfModulePath, './src/index.ejs'),
template: path.resolve(process.cwd(), './src/index.html'),
templateParameters: {
title: 'loading',
timestamp: new Date().getTime(),
},
inject: 'body',
hash: true,
}),
new VueLoaderPlugin(),
new DefinePlugin({
__VUE_OPTIONS_API__: JSON.stringify(true),
__VUE_PROD_DEVTOOLS__: JSON.stringify(false),
'process.env.NODE_ENV': JSON.stringify(process.env['NODE_ENV']),
'process.env.STORAGE_ENCRYPT_KEY': JSON.stringify(process.env['STORAGE_ENCRYPT_KEY']),
}),
new CopyPlugin({
patterns: [
{
from: path.resolve(process.cwd(), './public'),
filter: async (resourcePath: string) => {
const pattern = /(.gitkeep)/;
return !pattern.test(resourcePath);
},
},
],
}),
AutoImport({
imports: [
{
'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar'],
},
],
}),
Components({
resolvers: [NaiveUiResolver()],
// 允许子目录作为组件的命名空间前缀。 防止组件命名冲突
directoryAsNamespace: true,
}),
],
performance: {
maxAssetSize: 1024 * 800, // 单位 bytes
maxEntrypointSize: 1024 * 1024,
},
};

export default config;

webpack.config.dev.ts

build/webpack/webpack.config.dev.tsview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import webpackBaseConfig from '@build/webpack/webpack.config.base';
import {
Configuration,
} from 'webpack';
import { merge } from 'webpack-merge';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';

const config: Configuration = merge(webpackBaseConfig, {
mode: 'development',
devtool: 'eval-source-map',
plugins: [
new MiniCssExtractPlugin({
filename: 'style/[name].css',
}),
],
stats: {
preset: 'minimal',
timings: false,
},
});

export default config;

webpack.config.prod.ts

build/webpack/webpack.config.prod.tsview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import webpackBaseConfig from '@build/webpack/webpack.config.base';
import { ProgressPlugin } from 'webpack';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import { merge } from 'webpack-merge';
import ESLintPlugin from 'eslint-webpack-plugin';
import StylelintPlugin from 'stylelint-webpack-plugin';
import CssMinimizerPlugin from 'css-minimizer-webpack-plugin';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import path from 'node:path';
import dayjs from 'dayjs';
import packageJSON from '../../package.json';

async function getConfig() {
const now = dayjs()
const analyzerDirPath = path.resolve(process.cwd(), `./.build-report/${now.format('YYYY-MM-DD HH_mm_ss')}`);
const analyzerFilePath = path.resolve(analyzerDirPath, './report.html');
return merge(webpackBaseConfig, {
mode: 'production',
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: analyzerFilePath,
reportTitle: `${packageJSON.name} ${now.format('YYYY-MM-DD HH:mm:ss')}`,
openAnalyzer: process.env['SHOW_ANALYZER'] === 'Y',
}),
new ProgressPlugin(),
new ESLintPlugin({
extensions: ['vue', 'js', 'jsx', 'cjs', 'mjs', 'ts', 'tsx', 'cts', 'mts'],
}),
new StylelintPlugin({
extensions: ['css', 'less', 'scss', 'sass'],
}),
new MiniCssExtractPlugin({
filename: 'style/[name].[contenthash].css',
}),
],
optimization: {
minimizer: [
// For webpack@5 you can use the `...` syntax to extend existing minimizers (i.e. `terser-webpack-plugin`), uncomment the next line
`...`,
new CssMinimizerPlugin(),
],
},
});
}

export default getConfig;

Build 代码

build.dev.ts

build/build.dev.tsview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
process.env['NODE_ENV'] = 'development'
import { env } from '@ljtang2009/my-admin-common'
env.initEnv()
import getPort from 'get-port';
import config from '@build/webpack/webpack.config.dev';
import { webpack } from 'webpack';
import WebpackDevServer from 'webpack-dev-server';

const runServer = async () => {

const apiServerUrl = process.env['API_MOCK_SERVER_URL'];

const devServerOptions: WebpackDevServer.Configuration = {
client: {
logging: 'info',
overlay: true, // 当出现编译错误或警告时,在浏览器中显示全屏覆盖。
},
hot: true, // 启用 webpack 的 热模块替换 特性
open: true,
port: await getPort({
port: parseInt(process.env['WEBSITE_DEV_SERVER_PORT']!, 10),
}),
proxy: {
'/api': apiServerUrl!,
},
}

const compiler = webpack(config);
const server = new WebpackDevServer(devServerOptions, compiler);
await server.start();
}

runServer();

build.prod.ts

build/build.prod.tsview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
process.env['NODE_ENV'] = 'production';
import { env } from '@ljtang2009/my-admin-common';
env.initEnv();
import getConfig from '@build/webpack/webpack.config.prod';
import webpack from 'webpack';

export async function build() {
const webpackConfig = await getConfig();
const stats = await runWebpack(webpackConfig);
if (stats) {
// eslint-disable-next-line no-console
console.info(
stats.toString({
colors: true,
modules: false,
entrypoints: false,
}),
);
}
return {
webpackConfig,
stats,
};
}

function runWebpack(webpackConfig: Record<string, any>) {
return new Promise<webpack.Stats | undefined>((resolve, reject) => {
webpack(webpackConfig, (err, stats) => {
if (err) {
reject(err);
} else {
resolve(stats);
}
});
});
}

build();

Package 脚本

1
2
3
4
5
6
7
{
"scripts": {
"start": "ts-node --project tsconfig.json build/build.dev.ts",
"build": "ts-node --project tsconfig.json build/build.prod.ts",
"build:analyze": "cross-env SHOW_ANALYZER=Y npm run build"
}
}

Browserslist

.browserslistrcview raw
1
2
3
4
5
6
7
8
9
10
[development]
> 1%
last 2 versions
not dead


[production]
> 1%
last 30 versions
not dead

Babel

babel.config.jsview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = (api) => {
api.cache(true);
return {
presets: [
['@babel/preset-env'],
[
'@babel/preset-typescript',
{
isTSX: true, // 必须设置,否者编译tsx时会报错
allowNamespaces: true,
allExtensions: true, // 必须设置,否者编译.vue 文件中ts 代码会报错
},
],
],
};
};