记录 hexo 使用 highlight.js 实现代码高亮

一直觉得文章代码不够醒目,虽然也确实是高亮了。想着应该是这主题原因,就打算换个高亮主题,就我最喜欢的 sublime 的 monokai 主题。即使现在用 vscode 也还是用这个主题。

网上查了一下,hexo 本身就是用了 highlightjs 实现代码高亮的,但是它配置的颜色实在是不太行,要换的话,得完全自己手动。

一、关掉 hexo 本身的 highlightjs 使用

项目根目录下的 _config.yml 文件,highlight 字段 enable 改为 false。关掉原本的配置,不然会和自己手动配置冲突。

可能会遇到没生效,也就是关闭后还有高亮的效果。这时候看看 hexo 版本是不是 7 以上。我就是用了 7 版本不行,后面换了 npm i hexo@^6.3.0 就可以了。我看了下载量最多的还是 6.3.0。换了版本后可以执行下 npx hexo clean 清一下缓存。

二、下载 js 和 css

官网Download 挑选功能,然后下载。也可以现在 官网Demo 看看主题效果预览。

下载后的压缩包,把 highlight.min.js 文件放到 项目/themes/正在生效的主题/source/js 文件夹下。
把要用的主题 css 拿出来,当然也可以所有 css 文件放进去,我就放了 monokai 主题的 css 而已,其他的用不上。把 monokai-sublime.min.css 文件放到 项目/themes/正在生效的主题/source/css/highlight/styles 下。

三、引用并使用 js 和 css

项目/themes/正在生效的主题/layout/_partial/head.ejs 文件里,引入刚刚放好的 css 文件,插入代码:

<link rel="stylesheet" href="/css/highlight/styles/monokai-sublime.min.css">

项目/themes/正在生效的主题/layout/_partial/after-footer.ejs 文件里,引入刚刚放好的 js 文件,然后初始化,插入代码:

<script src="/js/highlight.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlightjs-line-numbers.js/2.8.0/highlightjs-line-numbers.min.js"></script>
<script>
  $(function(){
    $('pre code').each((i, el) => {
    hljs.highlightElement(el);
  });
  $('code.hljs').each(function(i, el) {
     hljs.lineNumbersBlock(el);
  });
  }());
</script>

Read More

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 事件后触发');
});

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