webpack5 下 background url 图片打包失败

一、省流

webpack5 下,css-loaderfile/url-loader 对于 css 文件(无论是 import 一份 css 文件还是 vue 里面的 style 块)里的 background-image: url() 资源文件处理有冲突。表现在:

打包出来的 url 使用的是 css-loader 自己产出的图片。但是这张图片生成有问题,导致图片无法显示。

解决方法看最后面。

二、溯源

根据《react 基础工程(react-redux & react-router)》里的配置,里面配置了 file/url-loader 去处理资源文件。

{
  test: /\.css$/,
  use: [
    isDev ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
    'css-loader',
  ],
},
{
  test: /\.(png|jpg|gif)$/i,
  use: [
    {
      loader: 'file-loader',
      options: {
        esModule: false,
        limit: 8192,
        name: 'media/[name].[hash].[ext]',
      },
    },
  ],
}

对于普通使用并没有问题。但是后面使用了 background-image: url() 发现图片显示不出来。去产物文件夹(我这里是 dist 文件夹)一看,发现在根路径多了几张图片,但是打不开,显示图片格式损坏。而配置 name: 'media/[name].[hash].[ext]' 里指定的 media 文件夹也有正常的图片。但是一看处理后的 css 使用的却是那一堆在根路径的损坏的图片。

也就是:background-image: url() 没有使用 file/url-loader 打包出来的图片,而是使用了别的 loader 产出的图片,而且这堆图片还生成失败了。

file/url-loader 去掉后,生成的图片正常了。也就是和这两个冲突了。接着看看问题在哪。插一句:我是在一个旧项目做了升级的,之前用的低版本也就是 webpack4 以及其他一系列低版本的配套是可以的,升级了后才出现问题。

一开始我怀疑是 mini-css-extract-plugin,怀疑是抽取 css 文件的时候,顺便生成了图片。后面我把这个去掉,只留下 css-loader 发现也会产出图片。这我着实没想到,以为只是单纯处理 css,遇到资源文件还是交给 file/url-loader。但是 webpack4 是好的,或者 webpack5 没有 file/url-loader 也是好的。

三、查找

那就缩小了排查范围,就着这几个关键字进行了搜索,终于发现问题:webpack5 对于资源文件处理进行了更改,具体可看官网《资源模块》介绍。也就是 webpack5 不需要再对资源文件指定相应的 loader 去处理了。而是加上 type 声明,比如 type: asset/resource。而如果我们还按照老的配置,可能会对资源重复处理,从而导致生成了一堆没法用的损坏图。

确实是 css-loaderfile/url-loader 有冲突,决定性证据也懒得去看源码了,这 webpack 4 升 5 真的改变好大。还记得刚出那会一大堆插件不兼容,不升级都没法用。

Read More

NodeJS 里的 stream(流)

流文件?文件流?其实很多语言都有,这里只是对于 NodeJS 里的一些流的用法做一个记录。

关于“流”的定义,百度百科的介绍也没有说出个所以然。我个人理解,就是顾名思义,类似于“水流”般的存在和运动,只不过实体不是水,而是二进制。作用是啥,缓冲效果,积少成多。对于大文件的读写,或者网络传输其实都用到流。

这里先分享下两篇大佬的文章:

文章里也介绍了 stream 的一些用法,这里再记录巩固一下。这里也有我个人对于流操作的实操:《文件切片上传(断点续传)》。

另外,官方文档镇楼:《Node.js v20.5.0 documentation》。

1、简单的文件流读取写入,记得是 读取流.pipe(写入流)

const fs = require('fs');
const readStream = fs.createReadStream(读取文件地址);
const writeStream = fs.createWriteStream(写入文件地址);
// 通过 pipe 执行拷贝,数据流转
readStream.pipe(writeStream);
// 数据读取完成监听,即拷贝完成
readStream.on('end', function () {
  console.log('拷贝完成');
});

2、网络响应也可以用 stream 方式返回。也就是 response 对象是一个写入流,写回到请求的浏览器前。

const fs = require('fs');
const http = require('http');
const server = http.createServer(function (req, res) {
  const readStream = fs.createReadStream(读取文件地址);
  readStream.pipe(res); // 将 res 作为 stream 的 dest
});
server.listen(8000);

3、根据上面文章对于 buffer 的介绍,测试确实不占内存

const fs = require('fs');
const json = {};
for(let i = 0; i < 100; i++) {
  json[i] = fs.createReadStream('package-lock.json');
  // json[i] = fs.readFileSync('package-lock.json');
}
for (const [key,value] of Object.entries(process.memoryUsage())){
  console.log(`Memory usage by ${key}, ${value/1000000}MB `);
}

所以 node 做 server 的时候对于文件操作可以用流形式,减少内存泄露的可能性。
依然大佬文章分享《Node.js内存溢出时如何处理?》。

4、可以在运输过程中修改内容

如果有用过 gulp 打包过前端代码的话,可能会有印象它的用法就有流用法如 src.pipe(transform).pipe(dest) 这样子。
也就是数据可以在流管道中进行修改。靠的就是 stream.Transform 流。用法也很简单:

const stream = require('stream');
const myTransform = new stream.Transform({
  transform(chunk, encoding, callback) {
    const content = 
`/* prefix */
${chunk.toString()}
/* suffix */`
    callback(null, content);
  }
});

src.pipe(dest) 这样返回的是后者的写入流,也就是能写在中间的 src.pipe(transform/duplex).pipe(dest) 必须是个双工流,既可以是读取流也可以是写入流。

5、一些事件示例

readStream.on('data', (chunk) => { // chunk 传输的流二进制
  console.log('读取流 data 事件在传输时触发');
});
readStream.on('end', () => {
  console.log('读取流 end 事件在消费完毕时触发');
});
readStream.on('close', () => {
  console.log('读取流 close 事件在 end 事件后触发');
});
writeStream.on('drain', () => {
  console.log('写入流 drain 事件在可以接收更多的数据时触发');
});
writeStream.on('pipe', (src) => { // src => 读取流对象
  console.log('写入流 pipe 事件在读取流导入写入流的 pipe 操作时触发');
});
writeStream.on('error', (error) => { // src => 读取流对象
  console.log('写入流在流通道关闭后再写入就会报错',  error);
});
writeStream.on('finish', () => {
  console.log('写入流 finish 事件调用 end 方法触发'); 
});
writeStream.on('close', () => {
  console.log('写入流 close 事件在 finish 事件后触发');
});

最后再放两篇国外大佬写的介绍文章,不必要全看,看看代码示例也可以了。

用 webpack ModuleFederationPlugin 搭建微前端

webpack5 推出了一个 ModuleFederationPlugin,“模块联邦插件”。名字上就有点微前端的意思。网上的介绍原理什么的这里就不说了,就从实际的使用来看,它的作用有:

  1. 把本工程的一些内容单独打包成文件分享出去。
  2. 使用别的工程分享出来的文件。

用法其实也很简单,就一个插件而已,但是要实现到微前端落地还是有很多要改造的。基于此用 vue 工程做了个尝试案例,并对其中一些注意点做一些说明。

代码压缩包:micro-fontends.tar.gz

注意:代码里的 heal.com 网站,是我自己修改了 host,Mac 可以在 /etc/hosts,Windows 可以在 C:\Windows\System32\drivers\etc\hosts 文件里添加这一行或者改成自己喜欢的域名:0.0.0.0 heal.com

一、基础用法

new ModuleFederationPlugin({
  name: 'admin',
  filename: 'remoteEntry.js',
  remotes: {
    workforce: 'workforce@http://heal.com/workforce/remoteEntry.js',
    network: 'network@http://heal.com/network/remoteEntry.js',
  },
  exposes: {
    './init': './src/main.js',
    './layout': './src/components/Layout.vue',
  },
}),
  • name: 这个工程的模块名,比如这里是主应用,把它叫做 admin
  • filename: 这个工程分享的文件入口,也就是别的工程只需要应用这份入口文件就可以了,其他的文件会在这份文件里发出请求。
  • remotes: 使用其他工程的文件,比如使用了两个子应用 workforcenetwork 的东西,可以看到引用的就是 remoteEntry.js
  • exposes: 这个工程分享的具体内容,照着格式写就好了,注意 key 值是相对路径写法。

代码里的使用是使用 import() 异步动态引入的方式:import('admin/layout')。引用内容格式为 import('模块名/exposes的key值')。这也是个 promise,在 then 的回调里接收导出值即可。

二、用于微前端

使用方法看起来,是可以导出单个文件,也可以导出入口文件,让其牵出一模块代码。代码很简单,就这么一个用法。但是用于微前端的搭建还需要考虑:

  • 主子应用的引入。主应用也就是基座,必然由它来引用子应用的东西,然后来启动项目。那么子应用暴露什么出来好?
  • 开发问题。本地开发怎么启动,不能我开发子应用还要启动主应用吧?反过来开发主应用难道必须启动所有子应用?那还不如不拆。
  • 还是开发问题。上面那个的相反,假设现在已经达到每次只需要启动要修改的项目就好了。但是肯定会出现一个功能要同时修改主子应用的,主子项目如何联调开发。
  • 部署时的代理配置。主子应用已经是不同项目了,当然也是用不同容器承载。但是域名必须是同一个,怎么配置好代理转发可以正常访问到想要的资源?

那么一个个来看。

Read More

react 基础工程(react-redux & react-router)

vue 已经很熟悉。react 虽然也用过,但是对于整体的搭建还不是很熟。这次就使用 react-redux(^8.0.5)react-router(^6.11.2)
结合上次的《从零搭建前端工程》,搭建一个基础的 react 项目。加上一点注释,后面有用到也可以代码直接拷。

代码仓库:github

一、Router

当前最新版本为 v6,据说有些大改动,旧版本请参考《升级指南》。

路由文件声明:

import { createBrowserRouter, Navigate } from 'react-router-dom';
import Home from '@/views/Home';
import NotFound from '@/views/NotFound';

const router = createBrowserRouter([
  {
    path: '/home',
    element: <Home />,
  },
  {
    path: '/game',
    // element: <CardGame />,
    async lazy() { // 路由页面懒加载,也就是真的访问到再下载,打包也是独立的 js
      const comp = await import('@/views/CardGame');
      // 这里的 default 是 webpack 的处理,会把 export default 挂在 default 属性上
      return { Component: comp.default };
    },
  },
  {
    path: '/demo/:id',
    // element: <Demo />,
    async lazy() {
      // 注意:这里要 loader 结果返回,才会渲染组件。也就是会阻塞页面。
      const comp = await import('@/views/Demo');
      return { Component: comp.default, loader: comp.loader };
    },
  },
  {
    path: '/', // v6 的重定向改为 Navigate 组件
    element: <Navigate to={'/home'}/>,
  },
  {
    path: '*', // 404页面兜底,我觉得改成和上面的一样的重定向处理应该也可以
    element: <NotFound />,
  },
]);

export default router;

然后路由声明对象的使用:

import { RouterProvider } from 'react-router-dom';
import router from '@/router';

function App() {
  return (
    <RouterProvider router={router} />
  );
}
export default App;

Read More

一次前端性能优化的过程(多图)

性能优化听多了,又是各个注意点,又是监控什么的,如《JS性能优化探讨》《前端性能监控》,真正遇到了还是得具体场景具体分析。

一直觉得我的个人网站首页(现在不会了)很慢,进去后背景和熊猫头渲染得特别慢。当然这个慢是相对的,就我做过的 TO B 系统单单是前置接口的获取都要几秒了,只是有 loading 效果看起来不难受。但是我这个网页啥都没有,就那么几个图片和动画,着实不该。那就实际一步步看看吧。

一. 服务器连接速度

直接百度搜网站访问速度测试就有很多工具网站。一看都没啥问题,除了极个别的比较远的省份(我这是买的阿里云深圳的服务器)其他都在一秒以内。再 ping 一下,也可以看到并无延迟。也就是 DNS 解析和访问并没有问题。

二. 减小图片体积

那首先考虑资源图片过大,因为确实用了比较高清的图片,那就在不糊的情况下尽量缩小体积。兴冲冲地更新一看,没啥变化。还是有那个等待过程。那好吧,一不做而不休,我尝试把其他图片都去掉,只留下一个熊猫头,还是很慢,就证明不该是图片背锅。当然缩小体积是没有问题的。省流量嘛。

三. 使用 Devtool Performance

那就直接用工具看吧,打开开发者工具,切到 Performance,启动录制看看效果。找一下,在 Timings 那里看到 FP FCP 等事件,这里就是影响视觉观看的主要节点了。当然前面还有 Parse HTML 或者 Function call 等等事件。主要看 Parse HTML,因为要解析了 html,才会加载一系列资源,可以看出这一步并没有什么问题,也是很快就响应了。但是到 FCP 却隔了好几秒。也就是中间有东西耽搁了。
看了一下,是必要的 js 文件加载。有两个,一个是 app.***.js,基础的业务 js, 一个是 chunk-vendor.***.js,vue 框架的打包产物。毫无疑问,这两者都是不可或缺的。appjs 比较小还好,很快就获取完了。chunk-vendorjs有 300+kb,耗时几秒。可以看到它加载完后,很快就 FCP 了。所以它的加载就是问题关键了。

四. js 文件太大?

讲道理,300+kb 的 js 文件不算小,但也不能叫很大啊。怎么要好几秒才能加载完?当然我的服务器没有加速功能,如果放到 CDN 服务,我想应该效果会好点。但是没有这条件,那只能另外想想办法。我代码里用了 Element-UI,测试了一下确实是按需引入,去掉几个组件后,体积变小了。
但,这些就是要用到的,减不了。路由页面用的也是 import() 动态引入,也就是到了真正访问那个页面组件,才会去加载相应的 js。再说了业务代码也不会打包到 vendor 去。那体积没法减了。然后在自己本地测试了一下,以及线上的缓存状态下,页面出现都很快,更加印证了我的猜测,就是这个 “大 js 文件” 导致了页面渲染慢。(当然最好有个 CDN 什么的加速可以验证…)

事情本到这里就结束了(啥都没干,555…),结果又看到一个神奇的网站,谷歌的性能分析网站,地址:https://pagespeed.web.dev (可能需要科学上网)

Read More