在 Laravel 之外使用 Laravel-mix 的一点小坑,字体正确无法加载。

背景

项目不是 Laravel,需要写前端代码,然后 webpack 的配置不熟悉,所以选择了使用 laravel-mix 来编译前端代码。

前端选择的是 iview 来写页面。

使用 iview 需要 import 'iview/dist/styles/iview.css';

问题来源与解决过程

webpack.mix.js 的配置内容如下:

const fs = require('fs')
const mix = require('laravel-mix')
const path = require('path')
const webpack = require('webpack')

/**
 * webpack 配置
 */

mix.webpackConfig({
  output: {
    // 依据该路径进行编译以及异步加载
    publicPath: 'assets/main/',
    // 注意开发期间不加 hash,以免自动刷新失败
    chunkFilename: `js/chunk[name].${ mix.inProduction() ? '[chunkhash].' : '' }js`
  },

  plugins: [
    // 不打包 moment.js 的语言文件以减小体积
    new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
  ]
})

/**
 * SPA 资源编译
 */

mix
  .setPublicPath(path.normalize('Demo/public/assets/main'))
  .options({
    processCssUrls: false
  })
  .js('resources/assets/main/js/app.js', 'js')
  .extract([
    'axios',
    'lodash',
    'vue',
    'vue-router',
  ])
  .autoload({
    vue: ['Vue']
  })

/**
 * 发布时文件名打上 hash 以强制客户端更新
 */

if (mix.inProduction()) {
  mix.version()
} else {
  mix.disableNotifications()
}

// 加载 .env 中 browserSync proxy 的配置
let paths = fs.readFileSync('.env', 'utf8').match(/BROWSERSYNC_PROXY=\S*/)
let proxy = paths ? paths[0].split('=')[1] : 'server.test'

/**
 * 本地调试 dev-server 配置
 */

mix.browserSync({
  proxy,
  files: [
    'resources/assets/js/**/*.js',
    'Demo/Site/View/**/*.tpl.php',
  ]
})

  .disableNotifications()

最后编译后的访问结果是在控制台中获取到如下2个字体加载错误:

file

而文件的实际打包路径是:Demo/public/assets/main/fonts

file

各种百度,谷歌后没找到有用的信息。有如下相关信息:

iview icon打包问题 #1499

这个里面有个也许可行的方案,但是配置的时候找不到 utils ,utils 是 require进去的。我没找到这个 utils 的具体文件内容

segmentfault: iview 打包之后 找不到自带的icon图片,而且路径重复,点解

剩下还查过很多,都没有什么用处。

关键词 iview webpack fonts 自行组合搜索。

上述过程失败后,询问了前端的朋友,给出了一些他的建议,后来还是失败了。然后开始怀疑自己的配置 loader 并没有生效。

开始查看源码:
在文件 ./node_modules/laravel-mix/src/builder/webpack-rules.js 搜索 ttf 看到 96 - 114 行的如下内容。

file

立刻明白了。自己的配置因为 test 与 默认的不同。所以不会覆盖默认的配置。

所以进行了如下操作:
将该 rules 的 loader 段加入项目的 webpack.mix.js 中,并将 publicPath 修改为实际访问的地址 '/public/assets/main' + Config.resourceRoot

/**
 * webpack 配置
 */

mix.webpackConfig({
  output: {
    // 依据该路径进行编译以及异步加载
    publicPath: 'assets/main/',
    // 注意开发期间不加 hash,以免自动刷新失败
    chunkFilename: `js/chunk[name].${ mix.inProduction() ? '[chunkhash].' : '' }js`
  },

  plugins: [
    // 不打包 moment.js 的语言文件以减小体积
    new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
  ],

  module: {
    rules: [
      {
        test: /\.(woff2?|ttf|eot|svg|otf)$/,
        loader: 'file-loader',
        options: {
          publicPath: '/public/assets/main' + Config.resourceRoot
        }
      }
    ]
  }
})

重写 运行 yarn prod 后,控制台错误消失。问题解决。

附上第一次成功试验的完整文件内容

更完美的做法在文章尾部

const fs = require('fs')
const mix = require('laravel-mix')
const path = require('path')
const webpack = require('webpack')

/**
 * webpack 配置
 */

mix.webpackConfig({
  output: {
    // 依据该路径进行编译以及异步加载
    publicPath: 'assets/main/',
    // 注意开发期间不加 hash,以免自动刷新失败
    chunkFilename: `js/chunk[name].${ mix.inProduction() ? '[chunkhash].' : '' }js`
  },

  plugins: [
    // 不打包 moment.js 的语言文件以减小体积
    new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
  ],

  module: {
    rules: [
      {
        test: /\.(woff2?|ttf|eot|svg|otf)$/,
        loader: 'file-loader',
        options: {
          publicPath: '/public/assets/main' + Config.resourceRoot
        }
      }
    ]
  }
})

/**
 * SPA 资源编译
 */

mix
  .setPublicPath(path.normalize('Demo/public/assets/main'))
  .options({
    processCssUrls: false
  })
  .js('resources/assets/main/js/app.js', 'js')
  .extract([
    'axios',
    'lodash',
    'vue',
    'vue-router',
  ])
  .autoload({
    vue: ['Vue']
  })

/**
 * 发布时文件名打上 hash 以强制客户端更新
 */

if (mix.inProduction()) {
  mix.version()
} else {
  mix.disableNotifications()
}

// 加载 .env 中 browserSync proxy 的配置
let paths = fs.readFileSync('.env', 'utf8').match(/BROWSERSYNC_PROXY=\S*/)
let proxy = paths ? paths[0].split('=')[1] : 'server.test'

/**
 * 本地调试 dev-server 配置
 */

mix.browserSync({
  proxy,
  files: [
    'resources/assets/js/**/*.js',
    'Demo/Site/View/**/*.tpl.php',
  ]
})

更完美的做法

也许你看到了如下配置项:setPublicPath 就猜想有没有 setResourcePath

答案是有的:在源码文件 node_modules/laravel-mix/src/Api.js 中搜索 setResourcePath 来到 400-409 行,看到如下内容:

file

说明我们可以直接用 setResourcePath 来设置我们的资源路径

mix.setResourceRoot('/public/assets/main/') 请不要漏掉最前面与最后一级文件的 /,原因自行试验

附上最终文件的完整文件内容

const fs = require('fs')
const mix = require('laravel-mix')
const path = require('path')
const webpack = require('webpack')

/**
 * webpack 配置
 */

mix.webpackConfig({
  output: {
    // 依据该路径进行编译以及异步加载
    publicPath: 'assets/main/',
    // 注意开发期间不加 hash,以免自动刷新失败
    chunkFilename: `js/chunk[name].${ mix.inProduction() ? '[chunkhash].' : '' }js`
  },

  plugins: [
    // 不打包 moment.js 的语言文件以减小体积
    new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
  ]
})

mix.setResourceRoot('/public/assets/main/')

/**
 * SPA 资源编译
 */

mix
  .setPublicPath(path.normalize('Demo/public/assets/main'))
  .options({
    processCssUrls: false
  })
  .js('resources/assets/main/js/app.js', 'js')
  .extract([
    'axios',
    'lodash',
    'vue',
    'vue-router',
  ])
  .autoload({
    vue: ['Vue']
  })

/**
 * 发布时文件名打上 hash 以强制客户端更新
 */

if (mix.inProduction()) {
  mix.version()
} else {
  mix.disableNotifications()
}

// 加载 .env 中 browserSync proxy 的配置
let paths = fs.readFileSync('.env', 'utf8').match(/BROWSERSYNC_PROXY=\S*/)
let proxy = paths ? paths[0].split('=')[1] : 'server.test'

/**
 * 本地调试 dev-server 配置
 */

mix.browserSync({
  proxy,
  files: [
    'resources/assets/js/**/*.js',
    'Demo/Site/View/**/*.tpl.php',
  ]
})

附上 laravel-mix 文档

https://github.com/JeffreyWay/laravel-mix/...

本作品采用《CC 协议》,转载必须注明作者和本文链接
Study hard and make progress every day. Study hard and make progress every day.
本帖由系统于 6年前 自动加精
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 13

老牟子,没想到在这能看你。

6年前 评论
mouyong

@努力做个技术男 不怎么活跃,^_^

6年前 评论

用 laravel + vue 的话,请问你后台的登陆是怎么设计的?

6年前 评论
mouyong

@maxrisk 我没用 laravel 来做分离。我觉得,登录接口获取需要的 token,然后其他的请求都使用这个 token 去请求。而除了登录外的接口都需要在中间件验证是否拥有 token。laravel 是在 header 中 发送自定义 header 头 Authorization: 路由保护

6年前 评论

@蜗牛 独立laravel的话,前台模版中的 mix函数你是要自己封装的吧。

6年前 评论
mouyong

@496604841 自己封装多麻烦丫!?虽然涉及到的代码不多。封装了一次,自己再次看,那个路径获取什么的自己都看不懂了。后来看到其他人在非 public/js 目录下用 mix 取文件。我就选者了模仿。直接用 laravel 的 mix 函数了。改了2~3行。重新看也能看着不那么费劲

6年前 评论
mouyong

@496604841 这是我的。加了个 rootDir


if (! function_exists('mix')) {
    /**
     * Get the path to a versioned Mix file.
     *
     * @param  string  $path
     * @param  string  $manifestDirectory
     * @param  string  $rootDir
     * @return \Illuminate\Support\HtmlString
     *
     * @throws \Exception
     */
    function mix($path, $manifestDirectory = '', $rootDir = '/public')
    {
        static $manifests = [];

        if (! Str::startsWith($path, '/')) {
            $path = "/{$path}";
        }

        if ($manifestDirectory && ! Str::startsWith($manifestDirectory, '/')) {
            $manifestDirectory = "/{$manifestDirectory}";
        }

        if (file_exists(public_path($manifestDirectory.'/hot'))) {
            return new HtmlString("//localhost:8080{$path}");
        }

        $manifestPath = public_path($manifestDirectory.'/mix-manifest.json');

        if (! isset($manifests[$manifestPath])) {
            if (! file_exists($manifestPath)) {
                throw new Exception('The Mix manifest does not exist.');
            }

            $manifests[$manifestPath] = json_decode(file_get_contents($manifestPath), true);
        }

        $manifest = $manifests[$manifestPath];

        if (! isset($manifest[$path])) {
            report(new Exception("Unable to locate Mix file: {$path}."));

            if (! app('config')->get('app.debug')) {
                return $path;
            }
        }

        return new HtmlString($rootDir . $manifestDirectory.$manifest[$path]);
    }
}
6年前 评论

@蜗牛 我之前的弄的时候,整个站点都是静态的,这么做的,就不行了,只能根据webpack来弄了

6年前 评论
mouyong

@496604841 恩。

6年前 评论
  • 我用的是下面的方法
mix.copyDirectory('node_modules/iview/dist/styles/', 'public/css');
6年前 评论
mouyong

@luffyzhao 感觉有点取巧了?

6年前 评论

@蜗牛

我只是觉得你那个有点麻烦,不过能达到的效果就好了。

6年前 评论
mouyong

@luffyzhao 我会这么干是因为在代码里面 import 字体后,会在我不需要的目录下产生 fonts 目录,而你这行代码的确可以做到,只是我当时并不想让它多出一个没必要的目录以及目录下的文件。

6年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!