react 基础工程 Ⅱ(@reduxjs/toolkit & @craco/craco & vite)

书接上回《react 基础工程(react-redux & react-router)》,由于最近项目上的新实践新体验,所以记录一下。

依然,代码仓库:github

一、用 @reduxjs/toolkit 创建 reducer 和 api

其实工程里已经用到这个工具,但是当时还没完全发现它的优势,还以为不过是官方推荐的比较好用的三方库而已。才知道确实简化了处理以及还有别的封装功能。

1、简化写法

以前老式的 redux 写法:

import { cloneDeep } from 'lodash';
export const addStringAction = payload => ({
  type: 'ADD_STRING',
  payload: payload,
});
export const minusStringAction = payload => ({
  type: 'MINUS_STRING',
  payload: payload,
});
const defaultState = {
  b: 'abc',
};
export default function stringReducer(state = defaultState, action) {
  const { type, payload } = action;
  state = cloneDeep(state);
  switch (type) {
    case 'ADD_STRING':
      state.b += payload;
      return state;
    case 'MINUS_STRING':
      state.b = '';
      return state;
    default:
      return state;
  }
};

用 @reduxjs/toolkit 的写法:

import { createSlice } from '@reduxjs/toolkit';
export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0,
  },
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
  • a. 不需要再额外声明 action 了,不需要定义那些 type 了。reducers 字段下的每个方法就是个 action。用的时候直接 dispatch(increment(123)) 即可。

  • b. 而且好像可以直接改 state 上的字段,不需要返回一个完全新的 state 了?其实也是里面做了处理。看官方介绍,在注释里:

Redux Toolkit allows us to write “mutating” logic in reducers. It doesn’t actually mutate the state because it uses the Immer library, which detects changes to a “draft state” and produces a brand new immutable state based off those changes

Read More

NodeJS 的 commonjs、esm 模块使用以及一些字段释义

在之前 js 的模块化迟迟没有落地,所以 NodeJS 自己整了个 commonjs,也就是我们代码里常看到 require('moduleName')。后来浏览器支持了 esm 标准也就是代码里的 import export,NodeJS 也给加上了。

在不支持的年代,我们代码能直接写上模块化语句,都是 webpack 和 babel 的功劳。无论是 import 还是 require,最后都会处理成 _webpack_require_(应该是这个 api)。

这里记录下 NodeJS 使用 commonjs、esm 的一些注意点,以及 package.json 一些看了老是记不住的字段。

一、文件格式区分

NodeJS 要使用 esm 模块,文件必须声明为 .mjs 后缀。依然使用 .js 或者声明为 .cjs 都是使用 commonjs。

  1. 当然硬要在 .js/.cjs 文件使用 esm 语句(也就是 import)的话会报错

    SyntaxError: Cannot use import statement outside a module

  2. 反过来在 .mjs 文件使用 commonjs 语句(也就是 require)的话会报错

    ReferenceError: require is not defined in ES module scope, you can use import instead

二、混搭使用

正常使用当然是两厢安好,但是可能还是会出现一些旧模块是 commonjs 写的,新的又想用 esm 去写的情况。

  1. 在 .js/.cjs 文件引用了 esm 模块的话会报错

    require() of ES Module /xxx/yyy/zzz/utils.mjs not supported.Instead change the require of /xxx/yyy/zzz/utils.mjs to a dynamic import() which is available in all CommonJS modules.

按照指示,改成动态引入 import() 就可以了。

import('./utils.mjs').then(console.log);
// [Module: null prototype] {
//   default: [Function: runEsm],
//   runSub: [Function: runSub]
// }

可以看出,export default 导出的东西被挂载 default 字段上。上面说了在标准还没落地时,webpack 就是这么做的。不知道是不是 NodeJS 参考了这个实现。

  1. 在 .mjs 文件 文件引用了 commonjs 模块的话就没问题。= =!

只不过 commonjs 里并没有 default 这个概念,所以如 import runCommonDefault, { runExport } from './utils.cjs'; 里,runCommonDefault 就是整个 module.exports 导出的对象。而 runExport 就是纯粹对 module.exports 导出对象的解构,属于其中一个字段内容。

三、esm 模块不支持 __filename,__dirname

esm 模块里是没有 __filename__dirname 这两个全局变量的(其实不是全局变量,和 require 一样是外部传进来,姑且当其是个全局变量)。可以这样实现:

import path from 'path';
import url from 'url';
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

Read More

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