Vue-cli: 自定义 pages 后,CSS 中的相对路径替换不正确

Created on 30 Jul 2019  ·  10Comments  ·  Source: vuejs/vue-cli

Version

3.8.4

Reproduction link

https://github.com/iUUCoder/issues-demo__vue-cli

Environment info

System:
    OS: macOS 10.14.5
    CPU: (8) x64 Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz
  Binaries:
    Node: 8.16.0 - ~/git/nvm/versions/node/v8.16.0/bin/node
    Yarn: Not Found
    npm: 6.4.1 - ~/git/nvm/versions/node/v8.16.0/bin/npm
  Browsers:
    Chrome: 75.0.3770.142
    Firefox: Not Found
    Safari: 12.1.1
  npmGlobalPackages:
    @vue/cli: 3.8.4

Steps to reproduce

执行命令,构建应用:

npm run build

What is expected?

构建时,对 CSS 中用到的 url 进行处理。

What is actually happening?

vue.config.js 中的配置:

module.exports = {
  pages: {
    "pages/home/index": {
      entry: "./src/pages/home/index.js",
      template: "src/pages/home/index.html"
    }
  }
};

项目源码 src/pages/home/Index.vue 中,设置了样式:

.page {
    color: red;
    background-image: url("./res/image.png");
}

构建输出结构:

dist
├── css
│   └── pages
│       └── home
│           └── index.0a0cd7d2.css
├── img
│   └── image.6704b667.png
├── js
│   ├── chunk-vendors.2ce01813.js
│   ├── chunk-vendors.2ce01813.js.map
│   └── pages
│       └── home
│           ├── index.cc5a5f68.js
│           └── index.cc5a5f68.js.map
└── pages
    └── home
        └── index.html

构建后的 dist/css/pages/home/index.0a0cd7d2.css:

.page{color:red;background-image:url(../img/image.6704b667.png)}

问题:

构建结果,对 CSS 中的相对路径处理不正确,没有考虑到配置了多级目录的情况。

Most helpful comment

@iUUCoder 我找到一个简单的workround,查看vue-cli源码发现,mini-css-extract-pluginpublicPath参数是相对于extract.filename计算出来的:

// use relative publicPath in extracted CSS based on extract location
const cssPublicPath = process.env.VUE_CLI_BUILD_TARGET === 'lib'
  // in lib mode, CSS is extracted to dist root.
  ? './'
  : '../'.repeat(
    extractOptions.filename
        .replace(/^\.[\/\\]/, '')
        .split(/[\/\\]/g)
        .length - 1
  )

vue-cli针对mini-css-extract-plugin插件生成的默认配置如下:

{
  filename: 'css/[name].[contenthash:8].css',
  chunkFilename: 'css/[name].[contenthash:8].css'
}

请注意:filenamechunkFilename使用的是相对路径,所以在计算publicPath时,路径层级会出现少一层的情况,导致background-image加载失败。

目前发现最简单的Workround是:显示的在vue.config.js文件中配置extract.filename,使其相对于根路径(比默认配置多一层),问题就解决了,示例代码如下:

module.exports = {

  publicPath: '/',

  css: {
    extract: process.env.NODE_ENV === 'development' ? false : {
      filename: '/css/[name].[contenthash:8].css',
      chunkFilename: '/css/[name].[contenthash:8].css'
    },
  },

  ...
}

All 10 comments

原因推测:

Vue Cli 配置了 mini-css-extract-pluginpublicPath ,对相对路径进行了特定处理,但却没有考虑多层级页面的情况。

源码:cli-service/lib/config/css.js:104行

解决:

我参考 cli-service/lib/config/css.js 的源码,对 Webpack 的配置进行了覆盖:

  • vue.config.js
// 配置项 css.extract 的值
const cssExtract = undefined;

module.exports = {
   css: {
        extract: cssExtract
   },
   chainWebpack: config => {
        /**
         * 覆盖 Webpack 配置
         */
        const isProd = process.env.NODE_ENV === 'production';
        const extract = cssExtract || isProd;
        const shadowMode = !!process.env.VUE_CLI_CSS_SHADOW_MODE;
        const shouldExtract = extract !== false && !shadowMode;
        if (shouldExtract) {
            [
                { lang: 'css', test: /\.css$/ },
                { lang: 'postcss', test: /\.p(ost)?css$/ },
                { lang: 'scss', test: /\.scss$/ },
                { lang: 'sass', test: /\.sass$/ },
                { lang: 'less', test: /\.less$/ },
                { lang: 'stylus', test: /\.styl(us)?$/ },
            ].forEach(({ lang, test }) => {
                const baseRule = config.module.rule(lang).test(test);
                [
                    // rules for <style lang="module">
                    baseRule.oneOf('vue-modules').resourceQuery(/module/),
                    // rules for <style>
                    baseRule.oneOf('vue').resourceQuery(/\?vue/),
                    // rules for *.module.* files
                    baseRule.oneOf('normal-modules').test(/\.module\.\w+$/),
                    // rules for normal CSS imports
                    baseRule.oneOf('normal'),
                ].forEach(rule => {
                    rule.use('extract-css-loader')
                        .loader(require('mini-css-extract-plugin').loader)
                        .options({
                            hmr: !isProd,
                        });
                });
            });
        }
    }
};

[email protected] 可以通过配置解决这个问题

fix: 可配置 __use_default_css_public_path__ 环境变量, 让 CSS 使用默认的 publicPath, 而不是使用相对路径

@iUUCoder 我找到一个简单的workround,查看vue-cli源码发现,mini-css-extract-pluginpublicPath参数是相对于extract.filename计算出来的:

// use relative publicPath in extracted CSS based on extract location
const cssPublicPath = process.env.VUE_CLI_BUILD_TARGET === 'lib'
  // in lib mode, CSS is extracted to dist root.
  ? './'
  : '../'.repeat(
    extractOptions.filename
        .replace(/^\.[\/\\]/, '')
        .split(/[\/\\]/g)
        .length - 1
  )

vue-cli针对mini-css-extract-plugin插件生成的默认配置如下:

{
  filename: 'css/[name].[contenthash:8].css',
  chunkFilename: 'css/[name].[contenthash:8].css'
}

请注意:filenamechunkFilename使用的是相对路径,所以在计算publicPath时,路径层级会出现少一层的情况,导致background-image加载失败。

目前发现最简单的Workround是:显示的在vue.config.js文件中配置extract.filename,使其相对于根路径(比默认配置多一层),问题就解决了,示例代码如下:

module.exports = {

  publicPath: '/',

  css: {
    extract: process.env.NODE_ENV === 'development' ? false : {
      filename: '/css/[name].[contenthash:8].css',
      chunkFilename: '/css/[name].[contenthash:8].css'
    },
  },

  ...
}

@iUUCoder 我找到一个简单的workround,查看vue-cli源码发现,mini-css-extract-pluginpublicPath参数是相对于extract.filename计算出来的:

// use relative publicPath in extracted CSS based on extract location
const cssPublicPath = process.env.VUE_CLI_BUILD_TARGET === 'lib'
  // in lib mode, CSS is extracted to dist root.
  ? './'
  : '../'.repeat(
    extractOptions.filename
        .replace(/^\.[\/\\]/, '')
        .split(/[\/\\]/g)
        .length - 1
  )

vue-cli针对mini-css-extract-plugin插件生成的默认配置如下:

{
  filename: 'css/[name].[contenthash:8].css',
  chunkFilename: 'css/[name].[contenthash:8].css'
}

_请注意:_filenamechunkFilename使用的是相对路径,所以在计算publicPath时,路径层级会出现少一层的情况,导致background-image加载失败。

目前发现最简单的Workround是:显示的在vue.config.js文件中配置extract.filename,使其相对于根路径(比默认配置多一层),问题就解决了,示例代码如下:

module.exports = {

  publicPath: '/',

  css: {
    extract: process.env.NODE_ENV === 'development' ? false : {
      filename: '/css/[name].[contenthash:8].css',
      chunkFilename: '/css/[name].[contenthash:8].css'
    },
  },

  ...
}

这个方法我也试过,但是要是路径复杂一点,或者路径变动,这块都得跟随着修改。

目前我觉得,还是覆盖配置比较有效,能够一劳永逸,就是比较冗长和麻烦。

期待官方给出更高效的方案。

have better methods for this problem?

have better methods for this problem?

我也这样设置了,但是还是不行,请问还有别的方法没,上面说的方法都试完了,我本地运行,样式表出不来了

我也遇见类似问题了,官方 没有什么 解决方案吗。。。

Hitting this exact same issue.. Spent quite a while trying to work around it. Might have to iterate through all the extract plugins and update the public path.

原因推测:

Vue Cli 配置了 mini-css-extract-pluginpublicPath ,对相对路径进行了特定处理,但却没有考虑多层级页面的情况。

源码:cli-service/lib/config/css.js:104行

解决:

我参考 cli-service/lib/config/css.js 的源码,对 Webpack 的配置进行了覆盖:

  • vue.config.js
// 配置项 css.extract 的值
const cssExtract = undefined;

module.exports = {
   css: {
        extract: cssExtract
   },
   chainWebpack: config => {
        /**
         * 覆盖 Webpack 配置
         */
        const isProd = process.env.NODE_ENV === 'production';
        const extract = cssExtract || isProd;
        const shadowMode = !!process.env.VUE_CLI_CSS_SHADOW_MODE;
        const shouldExtract = extract !== false && !shadowMode;
        if (shouldExtract) {
            [
                { lang: 'css', test: /\.css$/ },
                { lang: 'postcss', test: /\.p(ost)?css$/ },
                { lang: 'scss', test: /\.scss$/ },
                { lang: 'sass', test: /\.sass$/ },
                { lang: 'less', test: /\.less$/ },
                { lang: 'stylus', test: /\.styl(us)?$/ },
            ].forEach(({ lang, test }) => {
                const baseRule = config.module.rule(lang).test(test);
                [
                    // rules for <style lang="module">
                    baseRule.oneOf('vue-modules').resourceQuery(/module/),
                    // rules for <style>
                    baseRule.oneOf('vue').resourceQuery(/\?vue/),
                    // rules for *.module.* files
                    baseRule.oneOf('normal-modules').test(/\.module\.\w+$/),
                    // rules for normal CSS imports
                    baseRule.oneOf('normal'),
                ].forEach(rule => {
                    rule.use('extract-css-loader')
                        .loader(require('mini-css-extract-plugin').loader)
                        .options({
                            hmr: !isProd,
                        });
                });
            });
        }
    }
};

In my case, it works like this:

const styles = [
  'css',
  'postcss',
  'scss',
  'sass',
  'less',
  'stylus'
]

const modules = [
  'vue-modules',
  'vue',
  'normal-modules',
  'normal'
]

module.exports = {
  // ...
  chainWebpack: config => {
    // ...
    if (shouldExtract) {
      styles.forEach(s => {
        modules.forEach(m =>
          config
            .module
            .rule(s)
            .oneOf(m)
            .use('extract-css-loader')
            .tap(options => {
              options.publicPath = '' // Set whatever you want as publicPath
              return options
            })
        )
      })
    }
  }
  // ...
};

If you set the publicPath as 'https://example.com/', it will generate an image reference like url(https://example.com/your-image.png).

Check mini-css-extract-plugin for other loader options you can set.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

csakis picture csakis  ·  3Comments

Gonzalo2683 picture Gonzalo2683  ·  3Comments

eladcandroid picture eladcandroid  ·  3Comments

Akryum picture Akryum  ·  3Comments

OmgImAlexis picture OmgImAlexis  ·  3Comments