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 了,可以去了解下。

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

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐