在 vue-loader工作流程梳理 里我们提到,vue-loader 编译的一环中(样式部分会应用到 css-loader )<style>
和<template>
中引用的资源会被转换成模块请求,即require('xxx.png')
的形式。而 file-loader 则会将资源文件复制到指定的打包目录,同时把原本的模块引用(import/require()
)解析重写为输出文件的正确访问路径(url
)。
资源文件输出路径/访问路径
简单来说,file-loader
主要解决两件事:
1 指定输出文件的路径——即打包后文件的存储位置。
2 生成解析文件的路径——即打包后引用文件时的URL地址。
开发阶段在css或html标签中引用的资源路径,通常和项目打包后资源的访问路径不一样。因此在配置 file-loader 的过程中我们要把握和厘清输出目录outputPath
和引用路径前缀publicPath
这两项(可以按照 webpack 的output.path
,output.publicPath
的机制来理解),不然可能导致项目运行时图片报404错误。
vue-cli4 的默认配置下,图片文件都会被输出在/dist/static/img
目录,同时引用地址会被解析成绝对路径重写入url中。像这样:background-image: url(/static/img/denglun-bg.ba926c29.jpg)
。但是绝对路径不够灵活,比如用 nginx 配置 HTTPS 服务 时将项目部署在二级目录下,直接访问根目录肯定会出错。
开发环境用绝对路径不会有问题,而生产环境最终通过 mini-css-extract-plugin 把每个.vue
内的样式都提取到单独的.css
文件中。然后通过正确配置插件的publicPath
,或者 file-loader 的 publicPath
将 url 写成相对路径。
注⚠️:一般测试环境打包(vue-cli-service serve
)也会输出打包文件到我们配置的dist目录,只是都在内存中不可见罢了
file-loader 的配置项详解,即传递给 options 的参数。
-
outputPath 资源打包输出时存放的目录(相对于打包目录的路径)
默认值为 undefined,即直接输出在 dist (默认的打包目录) 下最终导出的文件路径为
webpackConfig.output.path
+file-loader.outputPath
+file-loader.name
若 file-loader 的配置为{ outputPath: 'static/img', file-loader.name: '[name].[contenthash].[ext]' }
时,(static是我的静态资源目录),最后打包图片存放的路径将会是dist/static/img/logo.da7ef7de.png
publicPath 定义目标文件的公共访问路径(前缀),即项目运行时能正确引用资源的路径前缀
默认值为webpackConfig.output.publicPath
+file-loader.outputPath
,
最终引用的文件路径则为webpackConfig.output.publicPath
+file-loader.outputPath
+file-loader.name
。项目运行时我们访问的index.html的根目录就是 dist,因此访问路径会像这样:/static/img/logo.da7ef7de.png
。-
name:打包后输出的文件名
我们可以把 outputPath 的内容直接写到name中,即在前面加上存放目录,一样可以生成需要的输出路径。这也是vue-cli4默认的做法。如:.loader('file-loader') .options({ name: '[name].[contenthash].[ext]', outputPath: 'static/img' })
简化成
.loader('file-loader') .options({ name: 'static/img/[name].[contenthash].[ext]' })
从生成的资源覆写 filename 或 chunkFilename 时,
vue.config.js
配置的assetsDir
会被忽略。
因此别忘了在前面加上静态资源目录,即assetsDir指定的目录,不然会直接在dist文件夹下,配置 outputPath 时同理。
用函数作为outputPath/publicPath选项的值
我们可以通过配置 publicPath 项使资源引用 URL 为相对路径,简易版:
.loader('file-loader')
.options: {
name: '[name].[contenthash].[ext]',
outputPath: 'static/img',
publicPath: '../static/img'
// publicPath: 'static/img' // 也可
}
另外,如要给不同资源分别定义存储目录/访问路径前缀,可以用函数来配置。outputPath/publicPath 的回调参数如下:
-
url
是 options.name 选项的值,如'[name].[contenthash].[ext]'
,必须配置在路径最后。需要注意当自定义配置了 outputPath 就不要在 options.name 里再加上目录了,name 只负责文件名就好,不然这样得到的 url 参数有了目录前缀,就不太方便再处理。 -
resourcePath
是资源打包前的原始绝对路径 -
context
是资源文件的根部上下文(rootContext),也就是项目根目录的绝对路径,或你自定义的 context 配置项
可以这样获取项目根目录到资源的相对路径:
const relativePath = path.relative(context, resourcePath)
;
path.relative(from, to) 方法:返回从
from
到to
的相对路径
举例:
path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb');
,会返回:'../../impl/bbb'
path.relative('/', '/src/assets/bg_images/main-bg.jpg');
,会返回'src/assets/bg_images/main-bg.jpg'
// outputPath 和 publicPath 配置演示,非vue-cli4的处理:
.options({
name: '[name].[contenthash].[ext]',
// outputPath: 'static/img', // 别忘了加上静态资源目录这个前缀,即assetsDir指定的目录,不然会直接在dist文件夹下
outputPath: function (url, resourcePath, context) {
// 返回从项目根目录到该图片的相对路径
const relativePath = path.relative(context, resourcePath)
const pathArr = relativePath.split('/')
// 如果你的静态资源目录结构较为简单(最多二个层级),图片只放在/src/assets/ 或/src/assets/xxx
// 希望根据assets下的目录结构原样输出,可以这样做
if (pathArr[3] !== undefined) {
return `static/img/${pathArr[2]}/${url}` // url 是上面配置的 name 的值,必须加在路径最后
}
return `static/img/${url}`
// 这些都可依照个人习惯来安排,个人建议没必要太复杂
// if (/denglun-bg\.jpg/.test(resourcePath)) {
// 如果图片以 denglun-bg.jpg 结尾
// return `static/denglun/${url}`
// }
// if (/bg_images\//.test(resourcePath)) {
// 如果图片路径包含 bg_images 目录
// return `static/bg_images/${url}`
// }
// return `static/img/${url}`
},
publicPath: (url, resourcePath, context) => {
// 如果要让资源引用地址输出为相对路径,把 `outputPath` 的内容拷贝一份到这里即可
}
},
这种情况如果需要指定资源引用URL为相对路径,也需用函数配置 publicPath,照着 outputPath
的内容依葫芦画瓢即可。
先看看vue-cli4生产环境打包的默认处理结果:
我们打包前的静态资源目录(assets
)的结构如下(只展示图片路径相关资源):
src
├─ assets
│ ├─ bg_images
│ │ └─ denglun-bg.jpg
│ ├─ denglun-bg.jpg
│ ├─ denglun-limit.jpg
│ └─ denglun.jpg
├─ styles
│ └─ index.scss
└─ views
└─ dashboard
└─ index.vue
打包目录的结构(同样只是做个演示):
dist
├─ index.html
└─ static
├─ css
│ ├─ app.02c07ea3.css
│ ├─ chunk-01c6456a.1d1f9d97.css
│ ├─ chunk-components.4732cb5c.css
│ └─ chunk-libs.a1d59a71.css
├─ img
│ ├─ denglun-bg.ba926c29.jpg
│ ├─ denglun-limit.433994b0.jpg
│ └─ denglun.901f400f.jpg
└─ js
├─ app.60f57078.js
└─ chunk-libs.8726a2b2.js
原先打包自认为是小图片却没转成内联base64?原来是其实我的图片全部都大于默认的limit值(也就是4096/4kb,不够仔细),改成10240后效果才出来:
测试环境打包后,css中的资源URL默认是/static/img/xxx.h86a0sh3.jpg
这样的绝对路径。现在我们来看看生产环境输出的*.css
文件中引用的资源相对路径是怎么处理的。
用 mini-css-extract-plugin 打包 css 时资源URL路径配置
这件事 vue-cli4 通过 mini-css-extract-plugin@0.9.0 已经帮我们完美处理好了~ ⚠️注意,这个插件最新版本把
publicPath
属性放到loader
下了,chainWebpack 链式配置时要放到 loaderOptions 里。😅 如果手动安装配置mini-css-extract-plugin
的话要留意区分。
打包后css文件的输出路径是dist/静态资源目录/css/name.[contenthash:8].css
,如:dist/static/css/app.e3db5d0a.css
,源码:
// ./node_modules/@vue/cli-service/lib/config/css.js
const filename = getAssetPath(
rootOptions,
`css/[name]${rootOptions.filenameHashing ? '.[contenthash:8]' : ''}.css`
)
由此css文件的访问绝对路径是/静态资源目录/css/文件名.css
,如:/static/css/app.e3db5d0a.css
生产环境模式,vue-cli 4 做了如下配置:
// ./node_modules/@vue/cli-service/lib/config/css.js
module.exports = (api, rootOptions) => {
api.chainWebpack(webpackConfig => {
// use relative publicPath in extracted CSS based on extract location
// 设置 publicPath 为输出的 css 文件基于项目打包根目录的相对路径
const cssPublicPath = process.env.VUE_CLI_BUILD_TARGET === 'lib'
// 在 lib 模式下, CSS 会被提取到根目录下
? './'
: '../'.repeat( // 将filename路径最前面的 './' '.\'先去掉,如果是'/'(绝对路径)就原样输出,再根据 (/ 或 \ 的 数量) -1,确定重复 '../' 的从次数
extractOptions.filename // 这个filename就是css文件的输出文件名
.replace(/^\.[\/\\]/, '')
.split(/[\/\\]/g)
.length - 1
)
function createCSSRule (lang, test, loader, options) {
// ... 省略了大段代码,主要截取配置publicPath部分,具体可以去源码
function applyLoaders (rule, isCssModule) {
if (shouldExtract) { // 若shouldExtract为true,表示生产环境且非shadowMode
rule
.use('extract-css-loader')
.loader(require('mini-css-extract-plugin').loader)
.options({
hmr: !isProd,
publicPath: cssPublicPath // 默认值是 webpack 配置的 output.publicPath
})
}
// ...省略
}
}
}
}
mini-css-extract-plugin
的作用是为每个包含css的js文件创建一个单独的.css
文件。
我们重点关注一下它 Loader 选项的 publicPath :<String|Function>
为 css 内引入的图片、文件等外部资源指定一个URL公共路径。
默认值为vue.config.js
的 publicPath (也就是 webpack 的 output.publicPath,一般是'/
')。css中引入的URL最终会处理成这个publicPath
+ 该资源文件的访问路径。
通过分析代码中的变量 cssPublicPath 得出,它的值即为打包后的 css 文件基于 dist 的相对路径。用这个前缀再加上资源的访问路径即可高枕无忧了(never goes wrong)。最终就是像url(../../static/img/denglun-bg.4baebe12.jpg)
看下 mini-css-extract-plugin 用函数配置 publicPath 的例子,也是一样的效果:
options: {
publicPath: (resourcePath, context) => {
// publicPath 是css文件相对于上下文(项目根目录)的相对路径
// path.dirname(resourcePath) ,返回resourcePath的目录
return path.relative(path.dirname(resourcePath), context) + '/';
},
},
其中:path.dirname(path) 的返回值是当前路径的上层目录(绝对路径)
例如:/css/main.css
所在目录为/css
,那么相对于项目根目录的路径就是 ../
;
而 /static/css/index.css
所在目录是/static/css
,publicPath 就会是 ../../
当我们用 image-webpack-loader 压缩图片后,size会小很多,基本都会被转成base64 URI内联在css和js中。后面这些就不用 mini-css-extract-plugin 处理css中资源的相对路径了。反正目标是静态资源小一点再小一点,需要 file-loader 输出的资源越少越好。
and file-loader 和 url-loader 在 webpack5 就弃用了😂,被资源模块(asset module)取代以后不用配置 loader 了,可以去了解下。
最后(也是写给自己):碰到不清楚的地方,一定要多看官方文档多分析源码(细读官网避免乱百度的弯路,碰到不懂的就去看源码理解透彻),靠自己厘清实现原理。不要框架帮忙整合好了就不管不顾了。不然永远只是照搬别人的配置,版本升级或者换个插件就不会了,毫无收获。
今后努力方向:精炼明了,杜绝长篇大论。