一、构建工具解决了什么问题
构建工具(如Webpack)的作用是帮助开发者自动化构建和打包应用程序的过程。它们解决了许多与前端开发相关的问题,包括以下几个方面:
-
模块化管理:构建工具允许开发者使用模块化的方式组织和管理代码。通过模块化,开发者可以将代码划分为多个小模块,提高代码的可维护性和重用性。
-
依赖管理:在前端开发中,通常会使用许多第三方库和框架。构建工具可以帮助自动处理这些依赖关系,确保它们正确地被引入和使用。
-
文件打包和压缩:构建工具可以将多个源代码文件打包为单个或多个输出文件。这样可以减少页面加载的请求数量,提高应用程序的性能。此外,构建工具还可以对这些输出文件进行压缩,减小文件大小,进一步提升加载速度。
-
代码转换和优化:构建工具可以对源代码进行转换和优化,以提高应用程序的性能和兼容性。例如,它可以将使用最新 JavaScript 特性编写的代码转换为支持更旧浏览器的版本。
-
开发环境支持:构建工具通常提供了开发环境的支持,例如自动刷新浏览器、热模块替换(Hot Module Replacement)等功能,使开发者可以更高效地进行开发和调试。
总而言之,构建工具简化了前端开发过程中的许多任务,提供了自动化的工作流程,使开发者能够更专注于业务逻辑的实现,同时提高了开发效率和应用程序的性能。
二、webpack 基础配置
const path = require('path')
console.log(path.default);
// 默认创建一个空的html文件,并引入打包后的文件,加入参数复制模版
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 取代style-loader,提取js中的css为单独文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 使用 cssnano 优化和压缩 CSS
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
// 设置nodejs的环境变量 NODE_ENV: production development none
// 也可以通过mode或在package.json里设置,优先级: mode > 此处 > package.json配置
process.env.NODE_ENV = 'development'
module.exports = {
// 模式 production development
// mode: 'production',
// 单页面应用使用单入口
entry: './src/index.ts',
// entry: ['./src/index.ts','./src/test.js'] // 数组形式的也是单入口,会合并打包成一个文件,
// 每个入口会单独打包成一个js文件,配合 HtmlWebpackPlugin 打包多页面应用
// entry: {
// main: './src/index.ts',
// test: './src/test.js'
// },
// 输出
output: {
filename: 'js/[name]-[contenthash].js', // 输出的文件名称
path: path.resolve(__dirname, 'dist'), // 输出的路径,必须是绝对路径
publicPath: 'auto', // 指定在浏览器中引用打包后的资源时的公共路径
clean: true // 在生成文件之前清空 output 目录
},
// loader,在构建过程中对源代码进行各种转换和处理
module: {
rules: [{
// 匹配的文件
test: /\.css$/,
// 使用的具体loader,从下至上执行
use: [
// 创建style标签,将js中的样式资源插入到head标签中生效
// 'style-loader',
MiniCssExtractPlugin.loader,
// 将css文件以字符串的形式变成commonjs模块加载到js中
'css-loader',
// css兼容性插件
'postcss-loader'
]
}, {
test: /\.less/,
use: [
// 'style-loader',
MiniCssExtractPlugin.loader,
'css-loader',
// 需要下载less-loader和less
'less-loader',
'postcss-loader'
]
},
// {
// test: /\.(jpg|png|gif)$/,
// // 需要下载url-loader和file-loader(webpack5不用这种方式了)
// loader: 'url-loader',
// options: {
// // 图片体积 小于 8kb,就会被base64编码
// // 优点:减少请求数量,减轻服务器压力
// // 缺点:本地解码可能会导致加载速度变慢
// limit: 8 * 1024,
// // webpack5需要加这行,解决:url-loader 的es6模块化,使用commonjs解析
// esModule: false
// },
// // v5需要加这行
// type: 'javascript/auto'
// },
{
// 问题:处理不了html中的img标签,需使用html-loader
test: /\.(jpg|png|gif)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024,
}
},
generator: {
filename: 'img/[name].[contenthash][ext]'
}
},
{
test: /\.html/,
loader: 'html-loader'
},
{
/**
js兼容性处理: 需安装babel-loader @babel/core @babel/preset-env
1、基本js兼容性处理 --> @babel/preset-env
问题:只能转换语法,不能转换api
2、全部js兼容性处理 --> @babel/polyfill
问题:引入了全部兼容性代码,体积太大
3、需要做兼容性处理的就做:按需加载 --> core-js
注意:代码也会变大不少,但比全部兼容小很多
*/
test: /\.(js|ts)$/,
exclude: /node_modules/,
use: [
/**
* 开启多进程打包
* 进程启动时间大概为600ms,进程通信也有开销
* 只有工作消耗时间比较长,才需要多进程打包
*/
// 'thread-loader',
{
loader: 'babel-loader',
options: {
// 预设: 指示babel做怎么样的兼容性处理
presets: [
['@babel/preset-env',
{
// 按需加载
useBuiltIns: 'usage',
// 指定core-js版本
corejs: {
version: 3
},
// 指定兼容性做到哪个版本浏览器
targets: {
chrome: '60',
firefox: '60',
ie: '9',
edge: '17'
}
}
]
],
// 开启babel缓存,第二次构建时,会读取之前的缓存
cacheDirectory: true
}
}
]
}
]
},
// 插件
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new MiniCssExtractPlugin({
filename: 'css/[name]-[contenthash].css'
}),
],
// 在生产环境开启压缩
optimization: {
// 在开发环境也启用压缩
// minimize: true,
minimizer: [
// 在 webpack@5 中,你可以使用 `...` 语法来扩展现有的 minimizer(即 `terser-webpack-plugin`),将下一行取消注释
`...`,
new CssMinimizerPlugin(),
],
/**
* 1、将node_modules中代码单独打包成一个名为vendors的js文件
* 2、自动分析多入口chunk中,有没有公共的文件。如果有只会打包一次。
*/
// splitChunks: {
// chunks: 'all'
// },
// runtimeChunk: 'single'
},
// 开发服务器,需安装 webpack-dev-server
devServer: {
static: {
directory: path.join(__dirname, 'public'),
publicPath: '/',
},
// 启用gzip压缩
compress: true,
port: 3000,
// 监控页面变化,自动刷新浏览器
watchFiles: ['./src/index.html'],
// 自动打开浏览器
open: true
},
// 控制是否生成,以及如何生成 source map。
// source-map:一种提供源代码到构建后代码映射技术(如果侯构建后代码出错,通过映射可以追踪源代码错误)
devtool: 'source-map',
// 防止将某些 import 的包(package)打包到 bundle 中
externals: {
// 将jquery模块视为外部依赖,并期望在运行时通过全局变量jQuery来引入它。
jquery: 'jQuery'
}
}
2.1、通过copy-webpack-plugin剪切文件
public文件夹通常用于放一些在项目里不使用的公共资源
npm install copy-webpack-plugin --save-dev
const CopyWebpackPlugin = require("copy-webpack-plugin");
plugins: [
new CopyWebpackPlugin({
patterns: [
{ from: path.reslove(__dirname, '../public'), to: '' }, // 将 public 目录中的文件复制到输出目录的根目录
],
}),
],
2.2、通过@svgr/webpack将svg转成组件
{
test: /\.svg$/,
use: [
{
loader: '@svgr/webpack',
options: {
svgoConfig: {
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false, // 不主动清除ViewBox
},
},
},
],
},
},
},
'url-loader', // 支持url的方式
],
},
// .d.ts配置
declare module '*.svg' {
import type * as React from 'react';
export const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
const src: string;
export default src;
}
三、vite 的使用
3.1、简介
vite 是下一代的打包构建工具,在开发环境通过 esbuild 预构建依赖,并以原生 esm 的方式提供源码, 可以达到快速的启动与更新;为了在生产环境中获得最佳的加载性能,仍需要打包,将代码进行 tree-shaking、懒加载和 chunk 分割等。
其中 esbuild 依赖预构建主要做了两件事:
1、将以 CommonJS 或 UMD 形式提供的依赖项转换为 ES 模块。(兼容性)
2、将具有许多内部模块的 ESM 依赖项转换为单个模块。(性能)
3.2、获取语法提示
// 方式一
import { defineConfig } from "vite";
export default defineConfig({
optimizeDeps,
});
// 方式二:jsdoc注释
/** @type import('vite').UserConfigExport */
const viteConfig = {}
3.3、开发环境与生产环境配置区分
import { defineConfig } from "vite";
const envResolver = {
build: () => {
const prodConfig = {
...require("./vite.base.config").default,
...require("./vite.prod.config").default,
};
console.log("生产环境配置--->", prodConfig);
return prodConfig;
},
serve: () => {
const serveConfig = {
...require("./vite.base.config").default,
...require("./vite.dev.config").default,
};
console.log("开发环境配置--->", serveConfig);
return serveConfig;
},
};
export default defineConfig(({ command }) => {
return envResolver[command]();
});
3.4、使用环境变量
创建 .env 文件,并写入环境变量
MY_CONST_A = 'A'
MY_CONST_B = 'B'
在 vite.config.js 中使用:
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd(), 'MY_')
console.log(env); // { MY_CONST_A: 'A', MY_CONST_B: 'B' }
return envResolver[command]();
});
在 src 目录下的源代码中使用:
// vite.base.config.js
import { defineConfig } from "vite";
export default defineConfig({
// 这里默认只支持 VITE_ 开头的环境变量,所以需要手动添加自己的前缀
envPrefix: ["VITE_", "MY_"],
// 如果你想暴露一个不含前缀的变量,可以使用 define 选项:
define: {
'import.meta.env.ENV_VARIABLE': JSON.stringify(process.env.ENV_VARIABLE)
},
});
// 在源代码中使用如下语句使用
console.log(import.meta.env);
3.5、配置css
import { defineConfig } from "vite";
export default defineConfig({
css: {
// 对css模块化的默认行为进行覆盖,配置会交给 postcss-modules 进行处理
modules: {
hashPrefix: "cool",
},
// 对预处理器进行配置
preprocessorOptions: {
less: {
globalVars: {
themeColor: "tomato",
},
},
},
// 开启css的sourceMap
devSourcemap: true,
// postcss 配置(也可以通过写 postcss.config.js 来配置),可以在 .browserslistrc 写入需要支持的浏览器
postcss: {
plugins: [require("postcss-preset-env")],
},
},
});
3.6、配置别名
3.6.1、手动配置
import { defineConfig } from "vite";
const path = require("path");
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
"@assets": path.resolve(__dirname, "./src/assets"),
},
},
});
若编辑器无法自动提示路径,在 jsconfig.json 中写入:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@assets/*": ["src/assets/*"]
}
}
}
若 vscode 无法实现快捷跳转,安装扩展别名路径跳转,并在 settings.json 中写入自己的配置:
"alias-skip.mappings": {
"@assets": "/src/assets",
},
有可能安了扩展也没得用,这点不如 WebStorm 啊!
3.6.2、使用 vite-aliases 插件配置
若出现报错:Failed to resolve entry for package "vite-aliases". The package may have incorrect main/module/exports specified in its package.json: No known conditions for "." specifier in "vite-aliases" package [plugin externalize-deps]。请将 vite-aliases 降级至 0.9.2 版本。
注:若想获得编辑器提示,还是需要配置 jsconfig.json。
3.6.3、手写简易版 vite-aliases 插件
读取文件的时候务必使用绝对路径,若使用相对路径,node会将用process.cwd()和相对路径进行拼接,若执行node命令的目录不对,将会读取失败
const path = require("path");
const fs = require("fs");
module.exports = () => {
return {
// Vite Specific Hooks
// config() 作用:在 Vite config 被解析之前调整配置文件
config() {
// 拿到src目录下的文件(夹)列表
const res = fs.readdirSync(path.resolve(process.cwd(), "./src"));
// 过滤出里面的文件夹
const dir = res.filter((item) => {
return fs
.statSync(path.resolve(process.cwd(), "./src", item))
.isDirectory();
});
// 获取 alias 配置对象
const getAliasObj = () => {
const aliasObj = {};
dir.forEach((item) => {
aliasObj[`@${item}`] = path.resolve(process.cwd(), "./src", item);
});
return {
"@": path.resolve(process.cwd(), "./src"), // 这里手动将 @ 映射为 src 目录
...aliasObj,
};
};
return {
resolve: {
alias: getAliasObj(),
},
};
},
};
};
3.7、配置mock
使用诸如 vite-plugin-mock-server 之类的插件,照着官网整就完事儿了。
下面我们手搓一个超简易版(不含hrm、这玩意有点复杂)的mock插件。
const fs = require("fs");
const path = require("path");
module.exports = () => {
return {
// 配置开发服务器的钩子
configureServer(server) {
// 获取 mock 文件夹下的 mockApis
const getMockApis = () => {
const files = fs.readdirSync(path.resolve(process.cwd(), "./mock"));
const mockApis = [];
files.forEach((item) => {
mockApis.push(
...require(path.resolve(process.cwd(), `./mock/${item}`))
);
});
return mockApis;
};
// 对 mockApis 进行去重
const unique = (mockApis) => {
const map = new Map();
mockApis.filter((item) => {
return (
!map.has(`${item.pattern} ${item.method}`) &&
map.set(`${item.pattern} ${item.method}`, item.handle)
);
});
return map;
};
// 拿到去重后的 mockApis
const mockApisMap = unique(getMockApis());
// 配置中间件,请求都会打到这里
server.middlewares.use((req, res, next) => {
if (mockApisMap.has(`${req.url} ${req.method}`)) {
mockApisMap.get(`${req.url} ${req.method}`)(req, res);
} else {
next();
}
});
},
};
};
mock文件如下:
module.exports = [
{
pattern: "/api/get",
method: "GET",
handle: (req, res) => {
const data = {
name: "Jay",
age: 18,
};
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify(data));
},
},
{
pattern: "/api/post",
method: "POST",
handle: (req, res) => {
res.end("hello");
},
},
];
3.8、开发服务器跨域配置
import { defineConfig } from "vite";
export default defineConfig({
server: {
proxy: {
"/api": {
target: "https://www.baidu.com", // 将请求到开发服务器的且以/api开头的请求,由开发服务器转发到https://www.baidu.com,而服务器之间不存在跨域
rewrite: (path) => path.replace("/api", ""), // 替换路径中的/api,真实请求地址为:https://www.baidu.com
changeOrigin: true, // 修改请求头中的host为target(注:并非修改origin字段)
},
},
},
});
js文件如下:
// 浏览器自动拼接,实际请求为:http://127.0.0.1:5173/api
fetch('/api').then(data => {
console.log('data', data)
})