file-loader 配置详解以及资源相对路径处理

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.pathoutput.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) 方法:返回从fromto的相对路径
举例:
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
打包前的src/views/dashboard/index.vue
打包前的src/styles/index.scss
打包后图片的引用路径

原先打包自认为是小图片却没转成内联base64?原来是其实我的图片全部都大于默认的limit值(也就是4096/4kb,不够仔细),改成10240后效果才出来:

完美~
如果我们在 file-loader 配置outputPath使图片输出在不同的目录

测试环境打包后,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-loaderurl-loader 在 webpack5 就弃用了😂,被资源模块(asset module)取代以后不用配置 loader 了,可以去了解下。

最后(也是写给自己):碰到不清楚的地方,一定要多看官方文档多分析源码(细读官网避免乱百度的弯路,碰到不懂的就去看源码理解透彻),靠自己厘清实现原理。不要框架帮忙整合好了就不管不顾了。不然永远只是照搬别人的配置,版本升级或者换个插件就不会了,毫无收获。
今后努力方向:精炼明了,杜绝长篇大论。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容