构建工具

Vite

构建生产版本

自定义 chunk 及静态资源路径

使用 vite 默认选项打包出来的产物位于项目根目录下的 dist 文件夹中,里面包含了

  • assets 文件夹

  • index.html

  • 原 public 文件夹中的内容

assets 文件夹中包含了所有的 js、css 以及图片文件。如果我们想要将它们分开,可以进行如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
import { defineConfig } from 'vite'

export default defineConfig({
build: {
rollupOptions: {
output: {
assetFileNames: "assets/[ext]/[name]-[hash][extname]",
chunkFileNames: "assets/js/[name]-[hash].js"
}
}
}
})

assetFileNames 用于自定义构建结果中的静态资源名称(不包含 chunk),其有以下几个占位符:

  • [name]:静态资源的名称;

  • [hash]:基于静态资源内容的哈希值;

  • [extname]:包含 . 的文件后缀名;

  • [ext]:不包含 . 的文件后缀名。

此外,可以通过 ”/“ 将其划分到子目录。

详细内容见 配置选项 | Rollup 中文文档

chunkFileNames 用于对代码分割中产生的 chunk 自定义命名,用法同上。

详细内容见配置选项 | Rollup 中文文档

基于以上配置打包后,assets 文件夹中包含了 js、css、png 等文件夹。

这时,你可能会发现一个问题:打包后有一个 js 文件会比其他文件大很多。其实这个文件包含了 node_modules 中的所有依赖。那怎么将 node_modules 中的依赖拆分开来呢?我们可以自定义 chunk,将 node_modules 中的每种依赖打包成一个 chunk。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default defineConfig({
build: {
rollupOptions: {
output: {
assetFileNames: "assets/[ext]/[name]-[hash][extname]",
chunkFileNames: "assets/js/[name]/[name]-[hash].js",
manualChunks: (id) => {
if (id.includes("node_modules")) {
return id.split("node_modules/")[1].split("/")[0];
}
}
}
}
}
})

manualChunks 的参数 id 是文件的绝对路径,比如:

1
2
E:/develop/projects/work/document-managment/node_modules/lodash-es/methodOf.js
E:/develop/projects/work/document-managment/node_modules/lodash-es/min.js

如果你想在打包后将 node_modules 中的依赖放入 node_modules 文件夹中,可以这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default defineConfig({
build: {
rollupOptions: {
output: {
assetFileNames: "assets/[ext]/[name]-[hash][extname]",
chunkFileNames: (chunkInfo) => {
if (chunkInfo.name.includes("node_modules")) {
const name = chunkInfo.name.split("_")[2]; // node_modules_vue
return `assets/js/node_modules/${name}/[name]-[hash].js`;
}
return "assets/js/[name]/[name]-[hash].js";
},
manualChunks: (id) => {
if (id.includes("node_modules")) {
return "node_modules_" + id.split("node_modules/")[1].split("/")[0];
}
}
}
}
}
})

Gzip 压缩

当用户访问我们的 web 站点时,前端资源通过 Gzip 压缩后传输给客户端,比纯文本体积减少大概 60% 左右,能够节约客户端网络资源,提升响应速度,解决网络导致的资源加载瓶颈,和减小服务器压力。

gzip 工作原理
  1. 浏览器请求 url,并在请求头中设置属性 accept-encoding:gzip。表明浏览器支持 gzip,这个参数是浏览器自动会携带的请求头信息。
  2. 服务器收到浏览器发送的请求之后,服务器会返回压缩后的文件,并在响应头中包含 content-encoding: gzip。如果没有压缩文件,服务器会返回未压缩的请求文件。
  3. 浏览器接收到服务器的响应,根据 content-encoding: gzip 响应头使用 gzip 策略去解压压缩后的资源,通过 content-type 内容类型决定怎么编码读取该文件内容。
gzip 压缩方案
nginx 端压缩

浏览器请求资源时,nginx 服务器对文件进行 gzip 压缩后返回给浏览器。前端不用做任何修改正常打包,但这样会消耗服务器性能。nginx 开启 gzip 压缩,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
http {
sendfile on;

gzip on;
# 大于 length 的资源才压缩,默认为20字节
gzip_min_length: 2048;
#开启 gzip 静态压缩功能
gzip_static on;
#gzip 缓存大小
gzip_buffers 4 16k;
#开启 gzip 的 http 版本
gzip_http_version 1.1;
#gzip 压缩级别 1-10
gzip_comp_level 5;
#gzip 压缩类型
gzip_types text/plain application/javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
# 是否在http header中添加Vary: Accept-Encoding,建议开启
gzip_vary on;
}

nginx 想要开启 gzip 压缩需要确保 nginx 安装了 ngx_http_gzip_module 模块,gzip_static on 配置项需用到 http_gzip_static_module 模块,可使用命令 nginx -V 查看。

此外,系统还必须安装 zlib 库,因为 nginx 使用 zlib 对 http 包的内容进行 gzip:

1
sudo apt-get install zlib zlib-devel

若报错:”E:无法定位软件包 zlib-devel“,是因为在 ubuntu 软件源里 zlib 和 zlib-devel 叫作 zlib1g 和 zlib1g.dev。

1
sudo apt-get install zlib1g zlib1g.dev
前端压缩

打包时通过 vite/webpack 配置生成的对应的 .gz 文件,浏览器请求文件后会自动解压为 js 或者 css 等资源文件。

前端打包借助 vite-plugin-compression 插件,插件地址 GitHub - vbenjs/vite-plugin-compression: Use gzip or brotli to compress resources

安装

1
npm install vite-plugin-compression

使用

1
2
3
4
5
6
7
8
9
10
11
import { defineConfig } from 'vite'
import viteCompression from 'vite-plugin-compression';

export default defineConfig({
plugins: [
viteCompression({
threshold: 100*1024, // 体积大于 threshold 才会被压缩,单位 b
deleteOriginFile: false // 是否删除原文件
})
]
})

打包后,压缩后的文件与原文件会同时存在,可以通过配置 deleteOriginFile: true 删除未打包资源,但我们推荐保留未打包资源

此时,nginx 只需配置

1
gzip_static on;

即可。

注意: 如果 vue 项目的路由模式使用的是 history 且压缩后原文件被删除,那么当 nginx 配置了

1
try_files $uri /$uri /index.html

时,浏览器会报错:

1
Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of “text/html”. Strict MIME type checking is enforced for module scripts per HTML spec.

这是因为浏览器请求 .js 或 .css 文件时,服务器只有 .js.gz 或 .css.gz 文件,找不到时就会返回 index.html 文件。

解决方法就保留源文件,浏览器获取的依然是打包后的资源文件。


PostCSS

什么是 postcss

postcss 是使用 js 插件转换样式的工具。这些插件可以校验 css,添加浏览器前缀,编译未来 css 语法,内联图片等等。

postcss 本身只是一个 API,自身并不会改变 CSS,作为一个 API 它可以创建任何需要的插件和工具来改变 css。

那它是如何处理 css 的呢?分为以下三步:

  1. parser:将 css 字符串解析成抽象语法树(AST);
  2. processor:我们应用 postcss 插件,或是自定义插件,都是在这个过程中。根据 postcss 提供的API,对 parser 生成的 AST 做相应调整;
  3. stringfier:从 root 开始,层序遍历 AST,根据节点类型,拼接节点的数据为 css 字符串。

css 预处理器(sass/less)可以和 postcss 一起使用,postcss 处理的是 sass/less 处理之后的 css。

postcss 插件
  • autoprefixer:自动添加浏览器前缀;
  • cssnext:将最新的 css 语法转换成大多数浏览器都能理解的语法;
  • cssnano:压缩 css;
  • stylelint:css 代码检查工具,支持新 css 语法校验;
  • postcss-sprites:生成雪碧图;
  • postcss-modules:避免命名冲突;

更多插件见 https://www.postcss.parts/

使用

在使用这些 postcss 插件前,要先安装:

1
npm install autoprefixer, cssnext, cssnano

然后配置 vite.config.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { defineConfig } from 'vite'

const AutoPrefixer = require("autoprefixer");
const Cssnext = require("cssnext");
const Cssnano = require("cssnano");


export default defineConfig({
css: {
postcss: {
plugins: [
AutoPrefixer({
overrideBrowserslist: ["> 0.5%", "last 5 versions"]
}),
Cssnext,
Cssnano
]
}
}
})

使用 autoprefixer 插件需要配置 browserslist,可以在配置插件时配置(如上面代码),也可以在 package,json 中配置:

1
2
3
4
5
"browserslist": [
"> 0.3%",
"last 5 versions",
"Firefox > 100"
]

配置规则见 https://browsersl.ist/

Webpack

自定义 chunk 路径

webpack 自定义 chunk 路径要在 output 选项中:

1
2
3
4
5
6
7
8
9
10
module.exports = {
output: {
path: path.resolve(__dirname, "dist"),
// 控制入口文件对应的出口文件名
filename: "js/[name].[hash:8].js",
// 控制按需加载(动态导入)的文件对应的出口文件名,默认 [name] 是 id 值,可以在导入时使用魔法注释确定 chunk 名
chunkFilename: "js/[name].[chunkhash:8].js",
clean: true
}
}
  • filename:控制入口文件对应的出口文件名,比如入口文件名为 main.js,那么 [name] 就是 main;
  • chunkFilename:控制按需加载(动态导入)的文件对应的出口文件名,默认 [name][id] 相同,是数字,如果想用文件名,可以在导入时使用魔法注释确定 chunk 名。
1
2
3
const App = lazy(() => import(/* webpackChunkName: "app" */"../App"));
const Home = lazy(() => import(/* webpackChunkName: "home" */"../views/Home"));
const DataShow = lazy(() => import(/* webpackChunkName: "dataShow" */"../views/DataShow"));

自定义 css 文件路径

style-loader 会将 css 文件打包到 js 文件中,想要将 css 文件提取到单独的文件中可以用一下插件:

1
npm install mini-css-extract-plugin -D

配置 webpack:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module.exports = {
plugins: [
new MiniCssExtractPlugin({
// 在入口文件导入的 css 文件对应的出口文件名
filename: "css/main.css",
// 在 chunk 中导入的 css 文件对应的出口文件名
chunkFilename: "css/[name].css"
})
],
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader"]
}
]
}
}

这里需要搭配插件的 css loader。

自定义图片路径

图片打包后的路径可以在 fil-loader 中配置:

```js
module: {
rules: [
{
test: /.(png|jpe?g|gif)$/i,
loader: “file-loader”,
options: {
name: “[name].[ext]”,
outputPath: “./img”
}
}
]
}