普通视图

发现新文章,点击刷新页面。
今天 — 2025年9月8日首页

[python3] 5行随机算法(20ms)

作者 simpleson
2020年1月12日 13:10

随机大法好!

(大雾)
思路:当正确答案比错误答案还多时,不妨随便蒙一个。

class Solution:
    def getNoZeroIntegers(self, n: int) -> List[int]:
        while(True):
            L = random.randint(1,n)
            R = n-L
            if '0' not in str(L) and '0' not in str(R):
                return [L,R]

时间复杂度:O(n^0.046 * lg(n)),两个部分:

· While循环:O(n^0.046)
平均循环次数 == 命中无零整数的期望。生成数字每增加一位,就会有1/10的几率命中0,使得命中期望变为原来的10/9。
因此,平均循环次数为 (10/9) ^ lg(n),整理得n ^ lg(10/9),约为n的0.046次幂。
考虑到2147483647 ^ 0.046 = 2.673,在Int范围和O(1)几乎没啥区别。

· If校验:O(lg(n))
'0' not in dec(int)需要lg(n)的时间复杂度。

昨天以前首页

Elpis 开发框架搭建第二期 - Webpack5 实现工程化建设

作者 GalaxyMeteor
2025年9月6日 11:36

一、目标

使 业务文件 通过 解析引擎 转换成能够 供Koa进行页面渲染 的 产物文件

二、解析引擎的作用

  • 解析编译
    • 依赖分析
    • 编译
    • 输出
  • 模块分包
    • 模块分析
    • 模块拆分
    • 输出
  • 压缩优化与分流

三、分步实现

1. 完成 Webpack 5 基础打包配置

1.1 目录结构

在原有/app文件夹中新增webpack文件夹,并添加文件使结构如下

/app
  |----原有其他文件夹...
  |----/webpack
          |----build.js
          |----/confg
                  |----webpack.base.js

1.2 Webpack 相关配置

build.js配置内容

const webpack = require('webpack');
const webBaseConfig = require('./config/webpack.base.js');

console.log('\nbuilding... \n');

webpack(webBaseConfig, (err, stats) => {
    if (err) {
        throw err;
    }
    process.stdout.write(`${stats.toString({
        colors: true, // 在控制台输出色彩信息
        modules: false, // 不显示每个模块的打包信息
        children: false, // 不显示子模块的打包信息
        chunks: false, // 不显示每个代码块的信息
        chunkModules: true // 显示代码块中模块的信息
    })}\n`)
})

webpack.base.js配置内容

const path = require('path');
const webpack = require('webpack')
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');

/**
 * webpack 基础配置
 */
module.exports = {
    // 入口配置
    entry: {
        'entry.page1': './app/pages/page1/entry.page1.js',
        'entry.page2': './app/pages/page2/entry.page2.js'
    },
    // 模块解析配置(决定了要加载解析哪些模块以及用什么方式去解释)
    module: {
        rules: [
            {
                test: /\.vue$/,
                use: {
                    loader: 'vue-loader'
                }
            },
            {
                test: /\.js$/,
                include: [
                    // 只对业务代码进行 babel,加快 webpack 打包速度
                    path.resolve(process.cwd(), './app/pages')
                ],
                use: {
                    loader: 'babel-loader'
                }
            },
            {
                test: /\.(png|jpe?g|gif)(\?.+)?$/,
                use: {
                    loader: 'url-loader',
                    options: {
                        limit: 300,
                        esModule: false
                    }
                }
            },
            {
                test: /\.css$/,
                use: [ 'style-loader', 'css-loader' ]
            },
            {
                test: /\.less$/,
                use: [ 'style-loader', 'css-loader', 'less-loader' ]
            },
            {
                test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/,
                use: 'file-loader'
            }
        ]
    },
    // 产物输出路径
    output: {
        filename: 'js/[name]_[chunkhash:8].bundle.js',
        path: path.join(process.cwd(), './app/public/dist/prod'),
        publicPath: '/dist/prod',
        crossOriginLoading: 'anonymous'
    },
    // 配置模块解析的具体行为(定义 webpack 在打包时,如何找到并解析具体模块的路径)
    resolve: {
        extensions: ['.js', '.vue', '.less', '.css'],
        alias: {

        }
    },
    // 配置 webpack 插件
    plugins: [
        // 处理 .vue 文件,这个插件是必须的
        // 它的职能是将你定义过的其他规则复制并应用到 .vue 文件里
        // 例如,如果只有一条匹配规则 /\.js$/ 的规则,那么它会应用到 .vue 文件中的 <script> 板块中
        new VueLoaderPlugin(),
        // 把第三方库暴露到 window context 下
        new webpack.ProvidePlugin({
            Vue: 'vue'
        }),
        // 定义全局常量
        new webpack.DefinePlugin({
            __VUE_OPTIONS_API__: 'true', // 支持 vue 解析 optionsApi
            __VUE_PROD_DEVTOOLS: 'false', // 禁用 Vue 调试工具
            __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false' // 禁用生产环境显示 “水合” 信息
        }),
        // 构造最终渲染的页面模版
        new HtmlWebpackPlugin({
            // 产物 (最终模版) 输出路径
            filename: path.resolve(process.cwd(), './app/public/dist/', 'entry.page1.tpl'),
            // 指定要使用的模版文件
            template: path.resolve(process.cwd(), './app/view/entry.tpl'),
            // 要注入的代码块
            chunks: [ 'entry.page1']
        }),
        new HtmlWebpackPlugin({
            // 产物 (最终模版) 输出路径
            filename: path.resolve(process.cwd(), './app/public/dist/', 'entry.page2.tpl'),
            // 指定要使用的模版文件
            template: path.resolve(process.cwd(), './app/view/entry.tpl'),
            // 要注入的代码块
            chunks: [ 'entry.page2']
        })
    ],
    // 配置打包输出优化(代码分割、模块合并、缓存、TreeShaing、压缩等优化策略)
    optimization: {}
}

1.3 测试文件与模板文件配置

新增一些文件,用于后续验证打包效果

app/pages/page1/entry.page1.js

import { createApp } from 'vue';
import page1 from './page1.vue';
const app = createApp(page1);
app.mount('#root')

app/pages/page1/page1.vue

<template>
  <h1>page1</h1>
  <input v-model="content" />
</template>

<script setup>
import { ref } from 'vue';

const content = ref('');
console.log('page1 init')
</script>

<style lang="less" scoped>
h1{
  color: red;
}
</style>

app/pages/page2/entry.page2.js

import { createApp } from 'vue';
import page2 from './page2.vue';
const app = createApp(page2);
app.mount('#root')

app/pages/page2/page2.vue

<template>
    <h1>page2</h1>
    <input v-model="content" />
  </template>
  
  <script setup>
  import { ref } from 'vue';
  
  const content = ref('');
  console.log('page2 init')
  </script>
  
  <style lang="less" scoped>
  h1{
    color: blue;
  }
  </style>

app/view/entry.tpl

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>{{ name }}</title>
  <link href="/static/normalize.css" rel="stylesheet">
  <link href="/static/icon.png" rel="icon" type="image/x-icon">
</head>

<body style="color: red">
  <div id="root"></div>
  <input id="env" value="{{ env }}" style="display: none">
  <input id="options" value="{{ options }}" style="display: none">
</body>
<script type="text/javascript">
  try {
      window.env = document.getElementById('env').value;
      const options = document.getElementById('options').value;
      window.options = JSON.parse(options);
  } catch (e) {
      console.error(e)
  }
</script>

</html>

1.4 验证结果

node ./app/webpack/build.js

运行上述命令后,能够在 app/public/dist 文件看到打包后的产物

1.5 Controller 修改

由于我们将用于给 Koa 进行渲染的文件放在了 app/public/dist 文件夹中,所以我们需要修改之前的 app/controller/view.js ,对渲染路径进行修改

await ctx.render(`output${sep}entry.${ctx.params.page}`, {

修改为

await ctx.render(`dist${sep}entry.${ctx.params.page}`, {

2. Webpack 打包优化

2.1 实现动态构造

在基础配置中,pligun 的位置我们使用了多个 new HtmlWebpackPlugin 指定最终渲染的页面模版,但是这不利于后续更改与维护,所以我们将要采用动态构造的方式来提高可维护性。

app/webpack/config/webpack.base.js 引入所需新依赖

const glob = require("glob");

引入依赖后、webpack 基础配置前 实现动态构造

...原有引入依赖代码不变...

// 动态构造 pageEntries htmlWebpackPluginList
const pageEntries = {};
const htmlWebpackPluginList = [];

// 获取 app/pages 目录下所有入口文件 (entry.xxx.js)
const entryList = path.resolve(process.cwd(), "./app/pages/**/entry.*.js");
glob.sync(entryList).forEach((file) => {
  const entryName = path.basename(file, ".js");
  // 构造 entry
  pageEntries[entryName] = file;
  // 构造最终渲染的页面文件
  htmlWebpackPluginList.push(
    // html-webpack-plugin 辅助注入打包后的 bundle 文件到 tpl 文件中
    new HtmlWebpackPlugin({
      // 产物 (最终模版) 输出路径
      filename: path.resolve(
        process.cwd(),
        "./app/public/dist/",
        `${entryName}.tpl`
      ),
      // 指定要使用的模版文件
      template: path.resolve(process.cwd(), "./app/view/entry.tpl"),
      // 要注入的代码块
      chunks: [entryName],
    })
  );
});

module.exports = { ...已有内容不变... }

动态构造会将多个 HtmlWebpackPlugin 合并为一个 HtmlWebpackPluginList ,我们需要修改代码,将原有的多个 HtmlWebpackPlugin 替换为 HtmlWebpackPluginList

webpack.base.js文件内,将 plugins 中的

new HtmlWebpackPlugin({
    // 产物 (最终模版) 输出路径
    filename: path.resolve(process.cwd(), './app/public/dist/', 'entry.page1.tpl'),
    // 指定要使用的模版文件
    template: path.resolve(process.cwd(), './app/view/entry.tpl'),
    // 要注入的代码块
    chunks: [ 'entry.page1']
}),
new HtmlWebpackPlugin({
    // 产物 (最终模版) 输出路径
    filename: path.resolve(process.cwd(), './app/public/dist/', 'entry.page2.tpl'),
    // 指定要使用的模版文件
    template: path.resolve(process.cwd(), './app/view/entry.tpl'),
    // 要注入的代码块
    chunks: [ 'entry.page2']
})

替换为

...htmlWebpackPluginList,

至此,我们成功完成了动态构造的配置

2.2 打包输出优化配置

除了一些基础配置之外,我们还可以在 optimization 自定义一些打包优化规则

// 配置打包输出优化(代码分割、模块合并、缓存、TreeShaing、压缩等优化策略)
optimization: {
/**
 * 把 js 文件打包成3种类型
 * 1. vendor: 第三方 lib 库,基本不会改动,除非依赖版本升级
 * 2. common: 业务组件代码的公共部分抽取出来,改动较少
 * 3. entry.{page}: 不用页面 entry 里的业务组件代码的差异部分,会经常改动
 * 目的: 把改动和引用频率不一样的 js 区分出来,以达到更好利用浏览器缓存的效果
 */
splitChunks: {
    chunks: 'all', // 对同步和异步模块都进行分割
    maxAsyncRequests: 10, // 每次异步加载的最大并行请求数
    maxInitialRequests: 10, // 入口点的最大并行请求数
    cacheGroups: {
        vendor: { // 第三方依赖库
            test: /[\\/]node_modules[\\/]/,
            name: 'vendor', // 模块名称
            priority: 20, // 优先级,数字越大,优先级越高
            enforce: true, // 强制执行
            reuseExistingChunk: true, // 复用已有的公共 chunk
        },
        common: { // 公共模块
            name: 'common', // 模块名称
            minChunks: 2, // 被 2 处引用即被归为公共模块
            minSize: 1, // 最小分割文件大小 (1 byte)
            priority: 10, // 优先级
            reuseExistingChunk: true, // 复用已有的公共 chunk
        }
    }
}
},

2.3 其他配置

webpack.base.jsresolve.alias 中,我们可以定义一些变量以便后续在 require 时能够更便捷

alias: {
      $pages: path.resolve(process.cwd(), "./app/pages"),
      $common: path.resolve(process.cwd(), "./app/common"),
      $widgets: path.resolve(process.cwd(), "./app/widgets"),
      $store: path.resolve(process.cwd(), "./app/store"),
}

3. Webpack 环境分流配置

未完待续

elpis 源于 抖音“哲玄前端”《大前端全栈实践》

【URP】Unity 插入自定义RenderPass

作者 SmalBox
2025年9月6日 08:29

自定义渲染通道是一种改变通用渲染管道(URP)如何渲染场景或场景中的对象的方法。自定义呈现通道(RenderPass)包含自己的Render代码,可以在注入点将其添加到RenderPass中。

【从UnityURP开始探索游戏渲染】专栏-直达

添加自定义呈现通道(RenderPass):

  • 使用Scriptable render pass API创建自定义render pass的代码。
  • 将自定的render pass注入到URP管线中的指定注入点中,有两种方式:
    • RenderPipelineManager API注入自定义渲染通道
    • 或者通过创建一个可脚本化的RendererFeature添加到URP渲染器中。

使用Scriptable render pass API创建自定义render pass

  • Example custom render pass

    using UnityEngine;
    using UnityEngine.Rendering;
    using UnityEngine.Rendering.Universal;
    
    internal class ColorBlitPass : ScriptableRenderPass
    {
        ProfilingSampler m_ProfilingSampler = new ProfilingSampler("ColorBlit");
        Material m_Material;
        RTHandle m_CameraColorTarget;
        float m_Intensity;
    
        public ColorBlitPass(Material material)
        {
            m_Material = material;
            renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
        }
    
        public void SetTarget(RTHandle colorHandle, float intensity)
        {
            m_CameraColorTarget = colorHandle;
            m_Intensity = intensity;
        }
    
        public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
        {
            ConfigureTarget(m_CameraColorTarget);
        }
    
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            var cameraData = renderingData.cameraData;
            if (cameraData.camera.cameraType != CameraType.Game)
                return;
    
            if (m_Material == null)
                return;
    
            CommandBuffer cmd = CommandBufferPool.Get();
            using (new ProfilingScope(cmd, m_ProfilingSampler))
            {
                m_Material.SetFloat("_Intensity", m_Intensity);
                Blitter.BlitCameraTexture(cmd, m_CameraColorTarget, m_CameraColorTarget, m_Material, 0);
            }
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();
    
            CommandBufferPool.Release(cmd);
        }
    }
    
    

将自定的render pass注入到URP管线中的指定注入点中

RenderPipelineManager API注入自定义渲染通道

  • 通过RenderPipelineManager的注入点委托提供执行时机,加上Camera的EnqueuePass方法注入自定义RenderPass。

    public class EnqueuePass : MonoBehaviour
    {
        [SerializeField] private BlurSettings settings;    
        private BlurRenderPass blurRenderPass;
    
        private void OnEnable()
        {
            ...
            blurRenderPass = new BlurRenderPass(settings);
            // Subscribe the OnBeginCamera method to the beginCameraRendering event.
            RenderPipelineManager.beginCameraRendering += OnBeginCamera;
        }
    
        private void OnDisable()
        {
            RenderPipelineManager.beginCameraRendering -= OnBeginCamera;
            blurRenderPass.Dispose();
            ...
        }
    
        private void OnBeginCamera(ScriptableRenderContext context, Camera cam)
        {
            ...
            // Use the EnqueuePass method to inject a custom render pass
            cam.GetUniversalAdditionalCameraData()
                .scriptableRenderer.EnqueuePass(blurRenderPass);
        }
    }
    
    

创建一个可脚本化的RendererFeature

此示例执行将屏幕染成绿色的全屏blit。

  • 要创建自定义渲染通道,创建一个名为ColorBlitPass.cs的新c#脚本,然后从示例自定义渲染通道部分粘贴代码。
    • 注意:这个例子使用了Blitter API。不要使用CommandBuffer。URP中的Blit API。更多信息请参考Blit。
    • 使用上面定义好的定制Render Pass
  • 要创建Scriptable RendererFeature,将自定义渲染通道添加到渲染循环中,请创建一个名为ColorBlitRendererFeature.cs的新c#脚本,然后将示例Scriptable RendererFeature部分中的代码粘贴进来。
    • Example Scriptable Renderer Feature Scriptable Renderer Feature 添加 render pass 到渲染循环.

      using UnityEngine;
      using UnityEngine.Rendering;
      using UnityEngine.Rendering.Universal;
      
      internal class ColorBlitRendererFeature : ScriptableRendererFeature
      {
          public Shader m_Shader;
          public float m_Intensity;
      
          Material m_Material;
      
          ColorBlitPass m_RenderPass = null;
      
          public override void AddRenderPasses(ScriptableRenderer renderer,
                                          ref RenderingData renderingData)
          {
              if (renderingData.cameraData.cameraType == CameraType.Game)
                  renderer.EnqueuePass(m_RenderPass);
          }
      
          public override void SetupRenderPasses(ScriptableRenderer renderer,
                                              in RenderingData renderingData)
          {
              if (renderingData.cameraData.cameraType == CameraType.Game)
              {
                  // Calling ConfigureInput with the ScriptableRenderPassInput.Color argument
                  // ensures that the opaque texture is available to the Render Pass.
                  m_RenderPass.ConfigureInput(ScriptableRenderPassInput.Color);
                  m_RenderPass.SetTarget(renderer.cameraColorTargetHandle, m_Intensity);
              }
          }
      
          public override void Create()
          {
              m_Material = CoreUtils.CreateEngineMaterial(m_Shader);
              m_RenderPass = new ColorBlitPass(m_Material);
          }
      
          protected override void Dispose(bool disposing)
          {
              CoreUtils.Destroy(m_Material);
          }
      }
      
      
  • 要创建将像素染成绿色的着色器代码,请创建一个着色器文件,然后从示例着色器部分粘贴代码。
  • Example shader
    • 着色器执行渲染的GPU端。它从相机中采样颜色纹理,然后输出绿色值设置为所选强度的颜色。 注意:与Blitter API一起使用的着色器必须是手工编码的着色器。图形着色器与Blitter API不兼容。

      Shader "ColorBlit"
      {
          SubShader
          {
              Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalPipeline"}
              LOD 100
              ZWrite Off Cull Off
              Pass
              {
                  Name "ColorBlitPass"
      
                  HLSLPROGRAM
                  #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
                  
                  // The Blit.hlsl file provides the vertex shader (Vert),
                  // the input structure (Attributes) and the output structure (Varyings)
                  #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
      
                  #pragma vertex Vert
                  #pragma fragment frag
      
                  // Set the color texture from the camera as the input texture
                  TEXTURE2D_X(_CameraOpaqueTexture);
                  SAMPLER(sampler_CameraOpaqueTexture);
      
                  // Set up an intensity parameter
                  float _Intensity;
      
                  half4 frag (Varyings input) : SV_Target
                  {
                      UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
      
                      // Sample the color from the input texture
                      float4 color = SAMPLE_TEXTURE2D_X(_CameraOpaqueTexture, sampler_CameraOpaqueTexture, input.texcoord);
      
                      // Output the color from the texture, with the green value set to the chosen intensity
                      return color * float4(0, _Intensity, 0, 1);
                  }
                  ENDHLSL
              }
          }
      }
      
  • 将ColorBlitRendererFeature添加到当前URP Renderer资源中。有关更多信息,请参阅向URP渲染器添加渲染器功能。
  • 要更改亮度,请调整Color Blit Renderer Feature组件中的Intensity属性。

注意:如果项目使用XR,为了使示例可视化,在项目中安装MockHMD XR插件包,然后将渲染模式属性设置为单通道实例化。


docs.unity3d.com/Packages/co…


【从UnityURP开始探索游戏渲染】专栏-直达

(欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)

3495. 使数组元素都变为零的最少操作次数

作者 stormsunshine
2025年3月24日 06:17

解法

思路和算法

对正整数 $x$ 执行除以 $4$ 向下取整,将 $x$ 变成 $0$ 的执行次数与 $x$ 的值的关系如下:当 $1 \le x < 4$ 时,需要执行 $1$ 次;当 $4 \le x < 16$ 时,需要执行 $2$ 次;当 $16 \le x < 64$ 时,需要执行 $3$ 次;以此类推,当存在正整数 $p$ 满足 $4^{p - 1} \le x < 4^p$ 时,需要执行 $p$ 次。因此将 $x$ 变成 $0$ 的执行次数是 $\lfloor \log_4 x \rfloor + 1$。

对于二维数组 $\textit{queries}$ 中的每个查询 $[\textit{left}, \textit{right}]$,可以分别计算从 $\textit{left}$ 到 $\textit{right}$ 的每个正整数的执行次数,并得到区间 $[\textit{left}, \textit{right}]$ 中的所有正整数的执行次数之和。

由于 $\textit{left}$ 和 $\textit{right}$ 的取值范围是 $1 \le \textit{left} < \textit{right} \le 10^9$,因此如果直接遍历区间 $[\textit{left}, \textit{right}]$ 中的每个正整数计算执行次数,则时间复杂度过高,需要优化。

为了计算区间 $[\textit{left}, \textit{right}]$ 中的所有正整数的执行次数之和,可以分别计算区间 $[1, \textit{right}]$ 中的所有正整数的执行次数之和与区间 $[1, \textit{left} - 1]$ 中的所有正整数的执行次数之和,两项之差即为区间 $[\textit{left}, \textit{right}]$ 中的所有正整数的执行次数之和。

对于非负整数 $\textit{num}$,计算区间 $[1, \textit{num}]$ 中的所有正整数的执行次数之和的方法如下。

  1. 用 $\textit{currReductions}$ 表示当前执行次数,用 $\textit{start}$ 表示执行次数是 $\textit{currReductions}$ 的最小正整数,初始时 $\textit{currReductions} = 1$,$\textit{start} = 1$。

  2. 对于每个 $\textit{start}$ 计算 $\textit{end} = \min(\textit{start} \times 4 - 1, \textit{num})$,则区间 $[\textit{start}, \textit{end}]$ 为执行次数是 $\textit{currReductions}$ 的所有正整数的区间,该区间中的正整数个数是 $\textit{end} - \textit{start} + 1$,因此将区间 $[1, \textit{num}]$ 中的所有正整数的执行次数之和增加 $\textit{currReductions} \times (\textit{end} - \textit{start} + 1)$。然后将 $\textit{start}$ 的值乘以 $4$,将 $\textit{currReductions}$ 的值增加 $1$,重复上述操作。当 $\textit{start} > \textit{num}$ 时,结束操作,得到区间 $[1, \textit{num}]$ 中的所有正整数的执行次数之和。特别地,当 $\textit{num} = 0$ 时,上述做法也适用。

将区间 $[\textit{left}, \textit{right}]$ 中的所有正整数的执行次数之和记为 $\textit{reductions}$。当每次操作对两个正整数执行除以 $4$ 向下取整时,区间 $[\textit{left}, \textit{right}]$ 的最少操作次数等于 $\Big\lceil \dfrac{\textit{reductions}}{2} \Big\rceil$。理由如下。

  1. 对于正整数 $x$,用 $r(x)$ 表示对正整数 $x$ 执行除以 $4$ 向下取整,将 $x$ 变成 $0$ 的执行次数,则 $r(x) = \lfloor \log_4 x \rfloor + 1$。将区间 $[\textit{left}, \textit{right}]$ 中的每个正整数 $x$ 都替换成 $r(x)$,则得到从 $r(\textit{left})$ 到 $r(\textit{right})$ 的 $\textit{right} - \textit{left} + 1$ 个正整数组成的新数组,将新数组记为 $\textit{reductionsArr}$,则问题转换成:每次从新数组 $\textit{reductionsArr}$ 中选择两个元素分别减少 $1$,计算将新数组 $\textit{reductionsArr}$ 中的所有元素都变成零或负数的最少操作次数(由于 $0$ 除以 $4$ 仍等于 $0$,因此新数组中的元素变成负数也符合原数组中的元素变成 $0$)。

  2. 根据 $r(x)$ 的性质,新数组 $\textit{reductionsArr}$ 为单调递增数组且任意两个相邻元素之差等于 $0$ 或 $1$。每次从新数组 $\textit{reductionsArr}$ 中选择最大的两个元素分别减少 $1$,则可以经过若干次操作将所有的最大元素都减少 $1$ 且最多有一个次大元素减少 $1$。

  3. 经过若干次操作之后,一定可以将新数组 $\textit{reductionsArr}$ 变成所有元素值都相等(例如全部是 $r$)或其中一个元素值等于其余每个元素值加 $1$(例如只有一个元素是 $r + 1$,其余元素都是 $r$)。对于两种情况,都可以将元素值同步减少,直到所有元素变成 $0$ 或其中一个元素值是 $1$ 且其余每个元素值都是 $0$,当剩余一个元素值是 $1$ 时还需要额外操作一次才能将新数组 $\textit{reductionsArr}$ 中的所有元素都变成零或负数。因此在操作结束之后,新数组 $\textit{reductionsArr}$ 中最多有一个元素是 $-1$,其余元素都是 $0$,最少操作次数等于 $\Big\lceil \dfrac{\textit{reductions}}{2} \Big\rceil$。

分别计算二维数组 $\textit{queries}$ 中的每个查询的最少操作次数,计算所有查询结果的总和,即为答案。

代码

###Java

class Solution {
    public long minOperations(int[][] queries) {
        long operations = 0;
        for (int[] query : queries) {
            long reductions = countReductions(query[1]) - countReductions(query[0] - 1);
            operations += (reductions + 1) / 2;
        }
        return operations;
    }

    public long countReductions(int num) {
        long reductions = 0;
        int currReductions = 1;
        long start = 1;
        while (start <= num) {
            long end = Math.min(start * 4 - 1, num);
            reductions += currReductions * (end - start + 1);
            start *= 4;
            currReductions++;
        }
        return reductions;
    }
}

###C#

public class Solution {
    public long MinOperations(int[][] queries) {
        long operations = 0;
        foreach (int[] query in queries) {
            long reductions = CountReductions(query[1]) - CountReductions(query[0] - 1);
            operations += (reductions + 1) / 2;
        }
        return operations;
    }

    public long CountReductions(int num) {
        long reductions = 0;
        int currReductions = 1;
        long start = 1;
        while (start <= num) {
            long end = Math.Min(start * 4 - 1, num);
            reductions += currReductions * (end - start + 1);
            start *= 4;
            currReductions++;
        }
        return reductions;
    }
}

复杂度分析

  • 时间复杂度:$O(n \log m)$,其中 $n$ 是数组 $\textit{queries}$ 的长度,$m$ 是数组 $\textit{queries}$ 中的最大值。需要计算 $n$ 个查询的结果,每个查询的计算时间是 $O(\log m)$,因此时间复杂度是 $O(n \log m)$。

  • 空间复杂度:$O(1)$。

Uncaught ReferenceError: __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ is not defined

作者 FarmerLiusun
2025年9月5日 17:33

vue项目部署后访问页面空白,控制台报错:Uncaught ReferenceError: VUE_PROD_HYDRATION_MISMATCH_DETAILS is not defined

1.问题现象

vue项目编译后通过nginx部署,浏览器访问前端出现

  • 1.页面空白
  • 2.控制台报错:Uncaught ReferenceError: __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ is not defined image.png

2.问题解释

此错误意为: 未捕获的引用错误:__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ 变量未定义

  • 从 Vue 3.4 开始,引入了 __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ 这个编译时特性标志,用于控制生产环境中是否显示 hydration 不匹配的详细错误信息。
  • 当您的项目(或其某个依赖)尝试使用此新特性时,但如果项目运行的 Vue 版本低于 3.4(此例中为 3.2.47),运行时环境中不存在这个全局变量,从而导致此引用错误,并使得应用崩溃,呈现白屏。

3.问题解决

通过官方文档找到如下说明cn.vuejs.org/api/compile…

  • VUE_PROD_HYDRATION_MISMATCH_DETAILS****仅在3.4+中可用
  • 查看当前项目vue使用的是3.2.47版本 image.png

4.解决方案

方案一:修改编译配置

在构建工具中定义这个变量,让其存在并被设置为 false(即在生产环境禁用 hydration 不匹配详情)。

  1. 使用vue-cli image.png
  2. 使用vite image.png

方案二:升级 Vue

检查 package.json,将 Vue 及其相关依赖(@vue/compiler-sfcvue-router 等)升级到 3.4 或更高版本。这是最根本的解决方法,因为它确保了代码和运行时环境的一致性。

npm install vue@^3.4.0
# 或
yarn add vue@^3.4.0

5. 总结

  • 错误 __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ is not defined 是由于高版本特性在低版本环境中运行导致的。
  • 快速修复:在构建配置中手动定义该变量为 false
  • 升级修复:将 Vue 升级到 3.4+ 版本。

3516. 找到最近的人

作者 stormsunshine
2025年4月13日 18:53

解法

思路和算法

由于第 $1$ 个人和第 $2$ 个人的移动速度相同,因此与第 $3$ 个人的距离更近的人会先到达第 $3$ 个人的位置。

第 $1$ 个人到第 $3$ 个人的距离是 $\textit{distance}_1 = |x - z|$,第 $2$ 个人到第 $3$ 个人的距离是 $\textit{distance}_2 = |y - z|$。结果如下。

  • 如果 $\textit{distance}_1 < \textit{distance}_2$,则第 $1$ 个人先到达第 $3$ 个人的位置,返回 $1$。

  • 如果 $\textit{distance}_1 > \textit{distance}_2$,则第 $2$ 个人先到达第 $3$ 个人的位置,返回 $2$。

  • 如果 $\textit{distance}_1 = \textit{distance}_2$,则两个人同时到达第 $3$ 个人的位置,返回 $0$。

代码

###Java

class Solution {
    public int findClosest(int x, int y, int z) {
        int distance1 = Math.abs(x - z), distance2 = Math.abs(y - z);
        if (distance1 < distance2) {
            return 1;
        } else if (distance1 > distance2) {
            return 2;
        } else {
            return 0;
        }
    }
}

###C#

public class Solution {
    public int FindClosest(int x, int y, int z) {
        int distance1 = Math.Abs(x - z), distance2 = Math.Abs(y - z);
        if (distance1 < distance2) {
            return 1;
        } else if (distance1 > distance2) {
            return 2;
        } else {
            return 0;
        }
    }
}

复杂度分析

  • 时间复杂度:$O(1)$。

  • 空间复杂度:$O(1)$。

前端面试必问:深浅拷贝从基础到手写,一篇讲透

作者 TimelessHaze
2025年9月3日 18:20

深浅拷贝是前端面试的 "常驻嘉宾"—— 它不仅考察你对 JS 内存模型的理解,还能延伸到手写代码、性能优化等深层能力。很多面试官会从Object.assign()切入,一步步挖掘你的知识边界。这篇文章就按面试答题逻辑,把深浅拷贝的核心考点讲清楚,帮你在 "表演时间" 稳拿分。

一、浅拷贝:从 Object.assign () 说起

浅拷贝的核心是 "只拷贝对象表层属性,深层属性仍共享引用"—— 简单说,修改拷贝后对象的深层属性,会影响原对象。而Object.assign()是浅拷贝的典型代表,也是面试高频考点。

1.1 Object.assign () 的核心特性(面试必答)

先纠正一个常见误区:Object.assign() 会返回修改后的目标对象。如果目标对象是新创建的空对象(如{}),那本质上就是生成了一个 "新的浅拷贝对象"。

它的完整逻辑是:

  • 作用:将 1 个或多个 "源对象" 的可枚举属性(enumerable: true)复制到 "目标对象"。
  • 返回值:修改后的 "目标对象"(不是全新对象,除非目标对象本身是新创建的)。
  • 浅拷贝本质:若源对象的属性值是 "复杂数据类型"(对象 / 数组),拷贝的是其引用地址,而非实际值。

举个例子,一看就懂:

const obj = { 
  name: "hxt", 
  info: { age: 24 } // 深层复杂属性
};
// 目标对象是新创建的空对象,返回新的浅拷贝对象
const copyObj = Object.assign({}, obj);
copyObj.name = "newHxt"; // 修改表层属性:不影响原对象
copyObj.info.age = 25;   // 修改深层属性:影响原对象
console.log(obj.name);    // "hxt"(表层不变)
console.log(obj.info.age); // 25(深层被修改)

1.2 其他常见浅拷贝方法

除了Object.assign(),日常开发中还有这些浅拷贝方式,面试时提一句能体现知识面:

  • 数组专用:slice()、concat()(本质是返回新数组,但深层数组仍共享引用)
const arr = [1, [2, 3]];
const copyArr = arr.slice(); // 浅拷贝数组
copyArr[1][0] = 20;
console.log(arr[1][0]); // 20(深层被修改)
  • 扩展运算符(...) :对对象 / 数组通用,语法更简洁,同样是浅拷贝
const copyObj = { ...obj }; // 对象浅拷贝
const copyArr = [...arr];   // 数组浅拷贝

1.3 浅拷贝的使用场景

面试时被问 "什么时候用浅拷贝?",可以这样答:

当对象 / 数组的深层结构不需要修改,只需要复制表层属性时(比如合并配置、创建简单副本),用浅拷贝足够 —— 比深拷贝更高效,避免不必要的性能开销。

二、深拷贝:JSON 方法的便捷与局限

深拷贝的核心是 "完全切断引用,拷贝所有层级的属性"—— 修改拷贝对象的任何属性,都不会影响原对象。最常用的便捷方法是JSON.parse(JSON.stringify()),但它的局限是面试重点。

2.1 JSON 深拷贝的原理与优势

它的逻辑很简单:

  1. JSON.stringify(obj):将对象 "序列化" 为 JSON 字符串(把 JS 数据类型转成字符串格式);
  1. JSON.parse(str):将 JSON 字符串 "反序列化" 为新对象(重新创建内存空间,生成全新对象)。

优势是无需手写代码,一行实现深拷贝,适合简单场景:

const obj = { 
  field1: 1, 
  field4: { child: "child" } 
};
const deepCopyObj = JSON.parse(JSON.stringify(obj));
deepCopyObj.field4.child = "newChild";
console.log(obj.field4.child); // "child"(完全不影响原对象)

2.2 致命局限:这些情况它搞不定(面试必讲)

JSON.stringify()有严格的 "序列化规则",导致它无法处理以下场景,这也是面试官会追问的点:

  • 1. 无法拷贝 undefined、function、Symbol:这三类值不是合法的 JSON 数据类型,会被直接忽略(对象属性)或转为null(数组元素);
  • 2. 无法处理循环引用:若对象自身引用自身(如obj.obj = obj),序列化时会直接报错(TypeError: Converting circular structure to JSON);
  • 3. 特殊类型失真:Date 会被转成字符串、RegExp 会被转成空对象,无法保留原类型特性。

看个例子直观感受:

const obj = {
  func: () => {},       // function:被忽略
  undef: undefined,     // undefined:被忽略
  sym: Symbol("test"),  // Symbol:被忽略
  date: new Date()      // Date:被转成字符串
};
console.log(JSON.parse(JSON.stringify(obj)));
// 输出:{ date: "2024-05-XXTXX:XX:XX.XXXZ" }(其他属性全丢了)

三、手写深拷贝:从基础到解决循环引用

当JSON方法满足不了需求时,就需要手写深拷贝 —— 这是面试的 "加分项",能体现你的逻辑能力。我们分两步实现:先写基础版,再解决循环引用问题。

3.1 基础版深拷贝:递归遍历

核心思路:判断数据类型,简单类型直接返回,复杂类型(对象 / 数组)递归拷贝每一层属性

代码实现:

function clone(target) {
  // 1. 简单类型(Number/String/Boolean/null/undefined/Symbol/function)直接返回
  if (typeof target !== "object" || target === null) {
    return target;
  }
  // 2. 复杂类型:区分数组和对象
  let cloneTarget = Array.isArray(target) ? [] : {};
  // 3. 遍历属性,递归拷贝每一层
  for (const key in target) {
    // 只拷贝对象自身的属性(排除原型链上的属性)
    if (target.hasOwnProperty(key)) {
      cloneTarget[key] = clone(target[key]);
    }
  }
  return cloneTarget;
}

问题暴露:如果遇到循环引用(如obj.obj = obj),递归会无限执行,最终导致栈溢出(RangeError: Maximum call stack size exceeded)。

3.2 优化版:用 WeakMap 解决循环引用

要解决循环引用,关键是缓存已拷贝的对象—— 当再次遇到相同对象时,直接返回缓存的结果,避免重复递归。

这里为什么用WeakMap而不是Map?面试必答!

  • Map的键是 "强引用":即使键对应的对象在外部被销毁,Map仍会持有引用,导致内存泄漏;
  • WeakMap的键是 "弱引用":当键对应的对象在外部没有其他引用时,会被垃圾回收机制回收,不会造成内存泄漏。

优化后的代码:

function deepClone(target, cache = new WeakMap()) {
  // 1. 简单类型直接返回
  if (typeof target !== "object" || target === null) {
    return target;
  }
  // 2. 检查缓存:若已拷贝过该对象,直接返回缓存结果(解决循环引用)
  if (cache.has(target)) {
    return cache.get(target);
  }
  // 3. 区分数组和对象,创建拷贝容器
  let cloneTarget = Array.isArray(target) ? [] : {};
  // 4. 将当前对象存入缓存(键:原对象,值:拷贝对象)
  cache.set(target, cloneTarget);
  // 5. 递归拷贝属性(处理对象)
  for (const key in target) {
    if (target.hasOwnProperty(key)) {
      cloneTarget[key] = deepClone(target[key], cache);
    }
  }
  // 处理Date/RegExp等特殊类型(面试若没要求可省略)
  if (target instanceof Date) {
    return new Date(target);
  }
  if (target instanceof RegExp) {
    return new RegExp(target.source, target.flags);
  }
  return cloneTarget;
}

测试循环引用场景:

const obj = { name: "hxt" };
obj.self = obj; // 循环引用:对象引用自身
const copyObj = deepClone(obj);
console.log(copyObj.self === copyObj); // true(拷贝成功,无栈溢出)

四、面试答题思路总结

当面试官问 "谈谈你对深浅拷贝的理解" 时,别零散地回答,按这个框架说,逻辑清晰又全面:

  1. 先定义深浅拷贝

浅拷贝只拷贝表层属性,深层复杂类型共享引用;深拷贝完全拷贝所有层级,切断引用,修改拷贝对象不影响原对象。

  1. 从 Object.assign () 讲浅拷贝

说清它的特性(拷贝可枚举属性、返回目标对象)、浅拷贝本质(深层共享引用),再提扩展运算符、slice()等其他浅拷贝方法,以及使用场景(无需修改深层结构时用)。

  1. 讲深拷贝的两种方式
    • 便捷方案:JSON.parse(JSON.stringify()),优点是简单,缺点是处理不了 undefined/function/ 循环引用;
    • 手写方案:基础版用递归遍历,优化版用WeakMap解决循环引用,重点解释WeakMap的弱引用优势(避免内存泄漏)。
  1. 结合业务场景收尾

比如 "简单场景用 JSON 方法,需要处理循环引用或特殊类型时,用手写的深拷贝;不需要修改深层属性时,优先用浅拷贝提升性能"。

按这个思路答,既能覆盖所有核心考点,又能体现你 "从基础到应用" 的思考逻辑 —— 面试官想要的,就是这种有条理、有深度的回答。

用 MainActor.assumeIsolated 解决旧 API 与 Swift 6 适配问题

作者 Fatbobman
2025年9月3日 22:00

尽管 Swift 提供严格并发检查已有一段时间,但许多苹果官方 API 仍未对此进行充分适配,这种情况可能还会持续相当长的时间。随着 Swift 6 的逐步普及,这个问题变得愈发突出:开发者一方面希望享受 Swift 编译器带来的并发安全保障,另一方面又对如何让代码满足编译要求感到困惑。本文将通过一个 NSTextAttachmentViewProvider 的实现案例,介绍 MainActor.assumeIsolated 在特定场景下的妙用。

Bun 如何将 postMessage(string) 提速 500 倍,远超 NodeJs 🚀🚀🚀

作者 Moment
2025年9月2日 09:19

在 JavaScript 中,postMessage 是在多个工作线程之间传递数据的常用方法。在 Bun v1.2.21 中,postMessage(string) 的性能几乎与字符串大小无关。这对于多线程 JavaScript 服务器和命令行工具是一个巨大的改进。

20250902090321

通过避免对已知安全共享的字符串进行序列化,性能提高了多达 500 倍,并且在此基准测试中使用的峰值内存减少了约 22 倍。

字符串大小 Bun 1.2.21 Bun 1.2.20 Node 24.6.0
11 字符 543 ns 598 ns 806 ns
14 KB 460 ns 1,350 ns 1,220 ns
3 MB 593 ns 326,290 ns 242,110 ns

这种优化在你将字符串发送到工作线程时自动生效:

const response = await fetch("https://api.example.com/data");
const json = await response.text();

postMessage(json); // 对于大字符串,现在速度提升了 500 倍

这对于在工作线程之间传递大型 JSON 数据的应用程序特别有用,例如 API 服务器、数据处理管道和实时应用程序。

如下代码所示:

async function measureTime() {
  const url = "https://microsoftedge.github.io/Demos/json-dummy-data/5MB.json";

  // 开始计时
  console.time("Total Time");

  try {
    const response = await fetch(url);

    if (!response.ok) {
      throw new Error(`HTTP Error! Status: ${response.status}`);
    }

    // 等待JSON解析完成,但不将其赋值给变量,因为我们不需要使用它
    await response.json();

    console.timeEnd("Total Time");
  } catch (error) {
    // 如果发生错误,则结束计时并打印错误信息
    console.timeEnd("Total Time");
    console.error("An error occurred:", error);
  }
}

measureTime();

首先我们分别使用 NodeJs 和 Bun 的旧版本分别测试一下,最终输出结果如下图所示:

20250902091319

当我们把版本升级到最新之后,速度明显增高:

20250902091413

技术揭秘:JavaScriptCore 的优化

postMessage 通常使用结构化克隆算法(Structured Clone Algorithm)在发送到另一个线程之前序列化数据。这意味着将字符串的每个字节复制到一个新缓冲区,然后在另一端反序列化。

但问题是:在 JavaScriptCore(Bun 使用的引擎)中,字符串已经是线程安全的引用计数对象。字符串数据在创建后是不可变的,引用计数使用 std::atomic

class StringImplShape {
    std::atomic<unsigned> m_refCount;  // 线程安全!
    unsigned m_length;                  // 不可变
    union {
        const LChar* m_data8;           // 不可变
        const char16_t* m_data16;       // 不可变
    };
    mutable unsigned m_hashAndFlags;    // 唯一可变部分
};

因此,如果字符串已经是线程安全的,为什么在同一进程中的线程之间发送时还要进行序列化呢?([bun.com][1])

寻找快速路径

并非所有字符串都可以安全地共享。我们识别出三种需要序列化的类型:

  1. 原子字符串(Atom strings):线程本地的属性名和符号。

  2. 子字符串(Substrings):指向其他具有复杂生命周期的字符串。

  3. 绳状字符串(Rope strings):通过操作如 "foo" + "bar".slice() 创建的字符串。

对于其他所有字符串,我们可以完全跳过序列化。我们只需要确保在共享之前计算出惰性计算的哈希值(因为这是唯一可变的部分):

WTF::String toCrossThreadShareable(WTF::String& string)
{
    auto* impl = string.impl();

    // 不能共享原子、符号或子字符串
    if (impl->isAtom() || impl->isSymbol() ||
        impl->bufferOwnership() == StringImpl::BufferSubstring)
        return string.isolatedCopy();

    // 在共享之前强制计算哈希
    impl->hash();

    // 防止该线程进行原子化。
    impl->setNeverAtomicize();

    return string;  // 直接共享指针!
}

toCrossThreadShareable 函数通过识别哪些字符串可以安全地共享而无需序列化,实现了性能的显著提升。这种优化在多线程 JavaScript 服务器和命令行工具中尤为重要。理解其背后的原理有助于我们在开发中更高效地处理字符串数据。

快速路径条件

当满足以下条件时,优化将生效:

  • 你正在使用 postMessagestructuredClone

  • 你只发送一个字符串(而不是混合数据)。

  • 字符串不是子字符串、绳状字符串、原子或符号。

  • 你将字符串发送到同一进程中的另一个线程。

  • 字符串长度 ≥ 256 字符。

这涵盖了在工作线程之间发送字符串的极其常见的模式。

总结

通过识别哪些字符串可以安全地共享而无需序列化,Bun 在 postMessage(string) 上实现了显著的性能提升。这种优化对于多线程 JavaScript 服务器和命令行工具尤其有用。

写给这段旅程,也写给未来的自己 - 肘子的 Swift 周报 #100

作者 Fatbobman
2025年9月1日 22:00

一转眼,周报已经来到了第 100 期。回想 2023 年 10 月第一期时,我并没有把握自己能坚持这么久。但过去两年,通过持续创作,我收获了许多。

Zalo Mini App 初体验

作者 __M__
2025年9月1日 16:09

4aRp6pjqpxiVfn-NoTRmd5258cnG21MMw9WDW7KVwKA.png

这是一个风和日丽的上午,本牛马正品着咖啡哼着小曲儿打算开始一天忙碌(摸鱼),产品(很烦)突然找到我:“我们要开发一款小程序,可以搞么?”

小程序?可以搞么??!!笑话!这不洒洒水~~~

产品:“哦!是Zalo小程序”

o((⊙﹏⊙))o :“咋咯? 神马东西?能吃么?!” 一脸懵逼ing

于是,本牛马开始各种百度,Google,DeepSeek。然而查获的资源实在有限。好不容易找到一篇,(最低0.47元/天,解锁文章)。好吧,本牛马资金有限,超额要走OA。这个还是不开了。于是俺就开始用俺这 一瓶子不满,半瓶子晃荡的-4级英语加机翻硬撸官方文档。忍不住吐槽!Zalo官方文档是真的一言难尽!!!垃圾的一批。

故事讲完,下面开始正文

概述

Zalo 是越南当地的一款即时聊天软件,类似国内微信。但是小程序实现和国内出入很大,Zalo提供一些配置,可以将现有web直接转换为Zalo 小程序,灵活性很高。并且Zalo提供了一下api可以直接使用 Zalo 的一些高级功能。

网站托管服务

通过 Zalo Mini App,开发者的应用程序将被打包托管在 Zalo 的系统上,并通过 CDN 系统分发给用户,确保快速的访问速度并能够处理大量用户流量。

缓存和更新新版本

Zalo Mini App 的资源会保存在 Zalo 应用程序的 Cache 中,因此当用户第二次访问该应用程序时,几乎会立即加载,并且可以支持无需网络的离线加载机制。

安全和权限

Zalo Mini App 应用程序在分发给用户之前会经过 Zalo 系统的身份验证和测试,因此比常规的 Web 应用程序更加安全。

所有访问 Zalo Mini Apps 系统功能或访问用户信息的权限均须在实际使用前声明并获得批准。

创建小程序

  1. 前往  Zalo for Developer  并登录 Zalo 帐户。然后在 Zalo 平台上创建应用程序或使用现有应用程序。
  2. 在“设置” 中   选择   “启用应用程序”   ,以便外部用户使用。

zalo-developer-fd406f8685a9afb835517aee66b490f8.jpg 3. 接下来,你需要创建一个新的小程序。 应用管理页面,方便开发者管理自己的小程序,例如:管理权限、查看报表、发布小程序…… 创建小程序步骤:

  1. 进入应用管理页面
  2. 选择 add Mini App
  3. 填写所需信息。然后选择“新建”

create-mini-app-c0de3b97bd6514ab95f48fd301988116.jpg 创建小程序后,会收到一个小程序 ID,妥善保存。如果忘了也没关系,应用管理页面可以查。

开发

VScode 使用 Zalo Mini App 扩展

  1. 安装 Visual Studio CodeZalo Mini App 扩展
  2. 配置:在 Home 标签页,完成 配置应用ID安装依赖

image.png

  1. 登录:进入 Deploy 标签页 扫码登录开发者账号。
  2. 启动:进入 Run 标签页,选择合适的启动器,点击 Start

image.png

  1. 部署:进入 Deploy 标签页,选择部署环境,点击Deploy,注意:Deploy无法使用 env 文件。 该tab页可以查看小程序 Testing 环境发布的历史版本

image.png

使用 Zalo Mini App CLI

  1. 安装 Node JS
  2. 安装 Zalo Mini App CLI
  3. 安装依赖
pnpm install
  1. 登录 登录开发者账号:
pnpm run login
  1. 启动 开发服务器:
pnpm run start

部署 Dev环境: zmp-cli 选择 Development。

pnpm run deploy

部署 Test环境:zmp-cli 选择 Testing。

pnpm run deploy

调试

使用 pnpm run start 启动开发服务器后,命令行工具内出现二维码,用Zalo 扫描二维码查看小程序页面。

开启调试:浏览器打开 http://localhost:51019 调试页面。

点击 inspect 进入控制台,就可以愉快地敲BUG啦。

愉快地一天就这么结束了!!

Nuxt 开发指南

作者 lemooljiang
2025年9月1日 15:59

nuxt42.jpg

 

下载与资源

官网 |

中文文档 |

中文文档2 |

nuxt-modules |

naive ui |

Unocss |

windicss |

windicss github |

Nuxt 3.0 全栈开发视频 |

安装

"nuxt": ^4.0.3  nodejs: v22.17.1


1. 直接到github下载模板, https://github.com/nuxt/starter/tree/v4

  


 - 也可以用npm安装

 npm create nuxt <project-name>

 eg:npm create nuxt nuxtstart

  


 - 更改配置: package.json ->  name

  


2. npm install --global yarn   //yarn --version  1.22.22

  yarn config set registry https://registry.npmmirror.com  切换到淘宝源

  yarn install

  


3. yarn dev

http://localhost:3000/

  


//vs code插件

Vetur   Vue 3 Snippets

Naive Ui Snippets   //代码提示

UnoCSS              //代码提示

错误排除

  • 更改配置(nuxt.config.ts)一般要重启。

  • 编译时出错:Named export 'VResizeObserver' not found, nuxt.config.ts 文件中添加如下代码:

  build: { transpile: ['naive-ui', "vueuc"] },

  • MongoServerError:E11000 duplicate key error.... 这可能是数据表中的数据格式冲突导致的(有脏数据),删除整个表即可!

  • Naive UI中表格要有 block 属性,否则样式将变得怪异!<n-form block outline-none>

  • Error: [nuxt] A composable that requires access to the Nuxt instance was called outside of a plugin, Nuxt hook, Nuxt middleware, or Vue setup function.

  • useFetch的错误, let {data, error} = await useFetch(url)    ????

// data.value == null 都会报错,如下:

// Uncaught (in promise) TypeError: Cannot read properties of null (reading 'scope')

  • Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client   ???

  • devtools: { enabled: false }

  • server-api 在Nginx的https报务中无法开启,总有个错误 500 Server Error

全局错误处理

Nuxt是一个全栈框架,这意味着在不同的上下文中,有几种不可避免的用户运行时错误来源:

  1. Vue渲染生命周期中的错误(SSR + SPA)

  2. API或Nitro服务器生命周期中的错误

  3. 服务器和客户端启动错误(SSR + SPA)


//全局错误页面  error.vue

<template>

  <div class="pt-[50px]">

    <n-result

        status="500"

        title="错误提示"

        :description="error.message"

    >

        <template #icon>

            <img src="~/assets/img/sad.png" alt="喵星">

        </template>

        <template #footer>

            <n-button @click="$router.back()" mr-4>返回上一页</n-button>

            <n-button @click="handleError">回到首页</n-button>

        </template>

    </n-result>

  </div>

</template>

  


<script setup>

import { NButton, NResult } from "naive-ui"

  


defineProps({

  error: Object

})

const handleError = () => clearError({ redirect: '/' })

</script>

渲染模式

浏览器和服务器都可以解释JavaScript代码,将Vue.js组件渲染成HTML元素。这个步骤被称为渲染。Nuxt支持客户端和通用呈现。

客户端渲染 - CSR, 例如 Vue.js、React

通用渲染 - SSR, 例如 Nuxt、Next.js

静态站点生成 - SSG;

混合渲染 - 混合呈现允许每个路由使用路由规则不同的缓存规则,并决定服务器应该如何响应给定URL上的新请求。例如 Nuxt、Next.js

边缘渲染 - edge-side rendering。

naive UI

官网 |

安装指南 |

安装指南2 |

naive-ui-nuxt |


yarn add naive-ui   //^2.42.0  

  


1. nuxt.config.ts

build: { transpile: ['naive-ui', "vueuc"] },

  


2. app -> plugins -> naiveui.ts

import { setup } from '@css-render/vue3-ssr';

 

export default defineNuxtPlugin((nuxtApp) => {

    if (process.server && nuxtApp.ssrContext) {

        const { collect } = setup(nuxtApp.vueApp || {});

       

        // @ts-ignore

        const originalRender = nuxtApp.ssrContext.renderMeta?.bind(nuxtApp.ssrContext) || (() => ({}));

       

        nuxtApp.ssrContext.renderMeta = () => {

            // @ts-ignore

            const result = originalRender();

            // @ts-ignore

            const headTags = result?.headTags || "";

           

            return {

                headTags: headTags + collect()

            };

        };

    }

});

  


3. 按需引入

<template>

  <n-button>naive-ui</n-button>

</template>

  


<script setup>

  import { NButton } from 'naive-ui'

</script>

消息显示

脱离上下文的API

使用 createDiscreteApi 来创建一系列消息提示,它比较自由,推荐。


import { createDiscreteApi } from "naive-ui"

// import { useMessage } from "naive-ui" useMessage要有专门设置,暂时不用

  


let { message } = createDiscreteApi(["message"])

message.error("注册失败!\n"+error.value.data, { duration: 5e3 })

// message.error("注册失败!\n"+error.value.data)

message.success("注册成功", { duration: 5e3 })

// message.success("注册成功")

message.warning("How many roads must a man walk down")

message.loading("If I were you, I will realize that I love you more than any other guy")

message.info(

  "I don't know why nobody told you how to unfold your love",

  {

    keepAliveOnHover: true

  }

)

 

// 注意: 如果已经导入 message 而没有使用可能会导致错误!尤其是下拉菜单!

Unocss安装

这是一个按需原子化 CSS 引擎,比起直接使用 TailwindCSS 更轻更快!

它的预设样式可以直接使用 TailwindCSS 和 Windicss 的样式。


// yarn add @unocss/nuxt   //^0.57.4 "^0.58.8"

yarn add unocss @unocss/nuxt //^66.3.3

  


//配置模块,nuxt.config.ts:

  modules: [

    '@unocss/nuxt',

  ],

  


// 根目录创建 uno.config.ts

import {

  defineConfig, presetAttributify, presetIcons,

  presetTypography, presetUno, presetWebFonts,

  transformerDirectives, transformerVariantGroup

} from 'unocss'

  


export default defineConfig({

  rules: [

      //...

  ],

  shortcuts: [

    // ...

  ],

  theme: {

    colors: {

      // ...

    }

  },

  presets: [

    presetUno(),  //工具类预设

    presetAttributify(),  //属性化模式支持

    presetIcons(),    //icon支持

    presetTypography(),

    presetWebFonts({

      fonts: {

        // ...

      },

    }),

  ],

  transformers: [

    transformerDirectives(),

    transformerVariantGroup(),

  ],

})

Unocss基本语法

原子化css |

互动样式 |

样式参考 |

Tailwind CSS |

颜色样式 |

可以认为Unocss兼具了TailwindCSS 和 Windicss的优点,同时还具有自身的定制性和灵活性。按需引入,体积小,速度快。


1. 基本使用,可以直接在class中定义。在Unocss省略class直接写样式也是可以的,而且有代码提示。

<div class="bg-blue-200"> Index Page</div>

<div bg-pink-200 text-cyan>我的国家</div>

  


2. 在style中定义

<div class="text"> Index Page</div>

<style>

.text {

  @apply text-xs text-red-300 hover:bg-zinc-900;

}

.btn {

  @apply text-red-700  border border-red-500 !rounded-full;  // rounded-full !important

}

</style>

  


3. 混合的写法也是可以的

.test {

  color: white;

  font-size: 2rem;

  @apply bg-blue;

}

  


3. 属性模式(Attributify Mode)

<button

  bg="blue-400 hover:blue-500 dark:blue-500 dark:hover:blue-600"

  text="sm white"

  font="mono light"

  p="y-2 x-4"

  border="2 rounded blue-200"

>

  Button

</button>

  


4. `uno.config.ts`中定义 rules 或 shortcuts

rules: [

  ['custom-rule', { color: 'red' }],

  ['main2', { margin: '0.25rem', padding: '2rem', color: 'red',

  'background-color': 'blue'}],

],

shortcuts: {

  // shortcuts to multiple utilities

  'custom-shortcut': 'text-lg text-orange hover:text-teal',

  'btn': 'py-2 px-4 font-semibold rounded-lg shadow-md',

  'btn-green': 'text-white bg-green-500 hover:bg-green-700',

  // single utility alias

  'red': 'text-red-100'

}

  


/* //常用语句 */

bg-purple-100  背景颜色

text-purple-100 文本颜色

text-center   水平对齐 可用于字体和按键的中心对齐

align-middle  垂直对齐

!w-[80%]    //width: 80% !important;

shadow-sm  盒子阴影

cursor-pointer 指针样式

outline-none 无边框

font-italic  斜体

  


hover:bg-purple-500 鼠标悬停时

focus:bg-purple-900  鼠标点击时

flex justify-center items-center  元素居中

flex justify-between  两端对齐

min-h-sceen / min-h-100vh 高度满屏

min-h-80vh 最小高度(可以延伸)

h-80vh / calc(100vh - 10px)

w-full / w-100vw 宽度满屏

  


w-[95%] lg:w-[85%] ~= w-95vw  lg:w-85vw

  


文字和水平线

<div flex justify-center items-center><p m-l-4 text-4>区块链</p> <hr style="border: 1px solid gray; width: 75%;"></div>

  


<div flex ><div ml-auto>右对齐</div></div>

<div flex ><div ml-auto px-2 hover:bg-red>X</div></div> //删除按键

m-auto / mx-auto 居中

ml-auto 右对齐

mr-auto 左对齐

px-4 / py-4  左右/上下两边内边距(padding: 1rem;) 其它写法:px-[20px]

pt-4  上部内边距1rem(padding-top: 1rem;)

mx-4 / my-4  左右/上下两边外边距

  


animate-fade-in  渐入

<div border-t-dashed border-1></div>  虚线分割线

  


/* flex布局,两端对齐 */

<div flex mb-2 justify-between>  

  <p mr-2>自定义角色</p>

  <div w-65vw lg:w-42vw> xxx </div>

</div>

  


 /* 响应式 */

根据常用的设备分辨率方案,默认内置了 5 个断点,sm、md、lg、xl、2xl :

断点前缀  最小宽度  CSS

sm  640px @media (min-width: 640px) { ... }

md  768px @media (min-width: 768px) { ... }

lg  1024px  @media (min-width: 1024px) { ... }

xl  1280px  @media (min-width: 1280px) { ... }

2xl 1536px  @media (min-width: 1536px) { ... }

  


/* 定位手机屏幕 */

这种方式最令人们惊讶的地方是,要为移动设备设计样式,您需要使用无前缀的功能类,而不是带 sm: 前缀的版本。不要将 sm: 理解为”在小屏幕上”,而应将其视为”在小断点处”

请注意,我们不必为 sm 断点或 xl 断点指定背景色,您只需要指定一个功能类何时开始生效,而不是何时结束。

  


使用无前缀的功能类来定位移动设备,并在较大的断点处覆盖它们,下面是一些案例:

<div class="text-center sm:text-left"></div>  //在手机端居中,在屏幕变大时靠左

flex-1 flex justify-end lg:hidden  //手机端时显示,屏幕变大时隐藏

relative hidden lg:flex items-center ml-auto  //在电脑端的样式(在手机端时隐藏)

hidden lg:flex items-center px-4  //在电脑端的样式(在手机端时隐藏)

hidden lg:block ml-auto //在电脑端的样式(在手机端时隐藏)

ml-auto lg:hidden//在手机端时居中,电脑端隐藏

icones图标

官网 |

说明 |


yarn add @iconify-json/mdi   //注意按需安装!  //1.2.3

//yarn add @iconify-json/vscode-icons  

  


<div class="i-mdi-github text-3xl" />

<div class="i-mdi-twitter" />

<div class="i-mdi-arrow-collapse-right" />

<div class="i-mdi-alpha-i-circle" />  //提示

<div class="i-mdi-chevron-down" /> //向下的

  
  


<!-- 手机端 -->

<div class="ml-auto mr-2 bold lg:hidden" @click="activate('top')">

    <div class="i-mdi-dots-vertical" text-2xl/>

</div>

  


<div class="i-mdi-chevron-double-right text-2xl"

  mt-20 block lg:hidden  

  text-gray-600

  @click="activate('left')"

/>

全局变量配置


//app.config.ts

export default defineAppConfig({

    title: 'Hello Nuxt888',

    theme: {

      dark: true,

      colors: {

        primary: '#ff0000'

      }

    }

  })

 //app.vue

 {{ appConfig.title }}

 const appConfig = useAppConfig()

  


 //另外一个简洁的用法是写在composables中,export即可引用

export const title = "hello nuxt"

页面关键词配置

全局配置 |

配置 |


//nuxt.config.ts

app: {

  head: {

      titleTemplate: "%s - 固定标题",

      title: "这是首页",

      charset: "utf-8",

      htmlAttrs: {

          lang: "zh-cn"

      },

      meta: [

          { name: "description", content: "首页描述" },

          { name: "keywords", content: "首页关键词" },

      ]

  }

},

  


//index.vue 对单独页面的设置,会覆盖全局配置

useHead({

  title:"首页index",

  meta:[

      { name:"description",content:"首页描述2" },

      { name:"keywords",content:"首页关键词2" },

  ],

})

```  

  


## 全局CSS

```js

// 方法一

1. assets -> main.css

2. nuxt.config.ts中配置:

  css: [

    "~/assets/main.css",

  ],

  


// 方法二

//在 app.vue 中引入样式。注意不是在<style>中!

<script setup>

import "~/assets/main.css";

</script>  

  


// 方法三

//或者直接在 `uno.config.ts`中定义 shortcuts,它也是相当于全局的

shortcuts: {

  // shortcuts to multiple utilities

  'containerX': 'w-[95%] mx-auto text-4.4 lg:w-[85%]'

  'btn': 'py-2 px-4 font-semibold rounded-lg shadow-md',

  'btn-green': 'text-white bg-green-500 hover:bg-green-700',

  // single utility alias

  'red': 'text-red-100'

}

全局js

引用本地js

Nuxt入门

Nuxt中引用第三方js等资源文件,可将文件放在/assets或/public目录下

区别

    /assets目录下的文件会被webpack编译

    /public目录下的文件不会被编译

第三方文件放置在/public目录下

    /public/videojs/video-js.css

    /public/videojs/video.js

    /public/videojs/videojs-contrib-hls.js

  


  head: {

    ...

    link: [

      ...

      { rel: 'stylesheet', href: '/videojs/video-js.css' }

    ],

    script: [

      { src: '/videojs/video.js' },

      { src: '/videojs/videojs-contrib-hls.js' }

    ]

  },

外部js和css的引入


可组合函数: useHead, useHead只能与组件的setup和生命周期钩子一起使用

<script setup lang="ts">

  useHead({

  script: [

    { src: 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js' }

  ]

  })

</script>

  


//这里如果需要将js放置body区域末尾,直接添加参数

script: [

  {

    src: 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js',

    body:true

  }

]  

 

//引入外部css

<script setup lang="ts">

useHead({

  link: [

    {

      rel: 'preconnect',

      href: 'https://fonts.googleapis.com'

    },

    {

      rel: 'stylesheet',

      href: 'https://fonts.googleapis.com/css2?family=Roboto&display=swap',

      crossorigin: ''

    }

  ]

})

</script>

全局函数utils

Nuxt使用 utils/ 目录在整个应用程序中使用auto-imports自动导入辅助函数和其他实用程序!


export function test2() {

    console.log(12793, "test.js2")

}

  


//也可以自定义导出目录

//nuxt.config.ts

imports: {

  dirs: ["apis"]

}

composables

在composables/目录中编写自己的自动导入可重用函数。但它只导入顶层函数,如有二级目录,则必须在config中配置!


export const newFun = (i) => {

    return i+5

  }

  


//composables/gets/foo.js

imports: {

    dirs: ["composables/**"]

},  

```  

  


## 中间件

middleware 目录,Nuxt提供了一个可定制的路由中间件框架,可以在整个应用程序中使用,非常适合在导航到特定路由之前提取想要运行的代码。

```js

//middleware -> search.js

export default defineNuxtRouteMiddleware((to,from)=>{

    const { type,page } = to.params

    const { keyword } = to.query

  


    if(!keyword){

        return abortNavigation("搜索关键词不能为空")

    }

  


    if(!(["course","column"].includes(type)) || isNaN(+page)){

        return abortNavigation("页面不存在")

    }

})

//index.vue

definePageMeta({

    middleware:["search"]

})

  


//eg2

//中间件可以获取目标路由 to 和来源路由 from,还有两个很常用的工具方法:

abortNavigation(error):跳过,留在 fromnavigateTo(route):指定跳转目标。

export default defineNuxtRouteMiddleware((to, from) => {

  if (to.params.id === '1') {

    return abortNavigation()

  }

  return navigateTo('/')

})

  


//全局中间件

//命名时带上 global

middleware -> search.global.js

  


另外,中间件全名不能使用小驼峰的方法,要使用 `-`

插件


1. plugins/目录 -> myPlugin.ts

2. myPlugin.ts

export default defineNuxtPlugin(nuxtApp => {

  // Doing something with nuxtApp

})

export default defineNuxtPlugin(() => {

  return {

    provide: {

      hello: (msg: string) => `Hello ${msg}!`

    }

  }

})

<template>

  <div>

    {{ $hello('world') }}

  </div>

</template>

<script setup lang="ts">

// alternatively, you can also use it here

const { $hello } = useNuxtApp()

</script>

  


//Nuxt 上下文:NuxtApp

我们看到定义插件时,可以获取到 nuxtApp 对象,该对象是 NuxtApp 的实例,实际上是 Nuxt 提供的运行时上下文,可以同时用于客户端和服务端,并能帮我们访问 Vue实例、运行时钩子、运行时配置的变量、内部状态等。

  


我们需要了解 nuxtApp 一些重要的方法和属性以便使用:

    provide (name, value):定义全局变量和方法;

    hook(name, cb):定义 nuxt 钩子函数;

    vueApp:获取 vue 实例;

    ssrContext:服务端渲染时的上下文;

    payload:从服务端到客户端传递的数据和状态;

    isHydrating:用于检测是否正在客户端注水过程中。

layouts布局


layouts -> default.vue, <slot/>可以将其它页面插入

eg:

<template>

    <div class="body">

        <NavBar/>      

        <main class="containerX">

            <slot/>

        </main>

    </div>

</template>

  


//指定页面布局

layouts -> login.vue

//pages -> login.vue

definePageMeta({

    layout:"login",

    middleware:["only-visitor"]

})

另外,命名不能使用小驼峰的方法,要使用 `-`

Grid栅格系统

NaiveUI的Grid栅格系统,可以布局响应式的样式。

cols  number | ResponsiveDescription  24  一般是分成24列,每列用 span 表示占比


<n-grid x-gap="12" :cols="4">

  <n-gi>

    <div class="light-green" />

  </n-gi>

  <n-gi>

    <div class="green" />

  </n-gi>

  <n-gi>

    <div class="light-green" />

  </n-gi>

  <n-gi>

    <div class="green" />

  </n-gi>

</n-grid>

  


//响应式布局 分成4列,占比是手机端时 0:4, 电脑端时 1:3

<n-divider>Screen 响应式</n-divider>

<n-grid x-gap="20" cols="4" item-responsive responsive="screen">

  <n-grid-item span="0 l:1">

    <div class="light-green">

      m 以下:不显示<br>

      m 到 l:占据空间 1<br>

      l 以上:占据空间 2

    </div>

  </n-grid-item>

  <n-grid-item span="4 l:3">

    <div class="green">

      2

    </div>

  </n-grid-item>

</n-grid>

页面和路由

Nuxt会自动使用Vue Router在底层创建路由,页面的名字就是路由地址。

新建 pages 文件夹,在此文件夹内创建页面。


pages/about.vue ->  localhost:3000/about

  


默认情况下`pages/index.vue`是根路径 /

  


//二级路由: 页面文件在一个目录下

pages/user/info.vue ->  localhost:3000/user/info

  


//二级路由中保留父组件内容

1. 在同级目录下创建同名的 vue 文件,eg: pages/user + pages/user.vue

2. `user.vue`中加入`<NuxtPage/>`即可索引到下一级的页面(page)文件

  


//组件导入则用slot

<slot/>

动态路由

如果您在文件名中使用方括号 [ ],它将被转换为 动态路由 参数。您可以在文件名或目录中混合使用多个参数。

如果您希望参数是 可选的,必须使用双括号 [[]] 括起来,例如 ~/pages/[[slug]]/index.vue 或 ~/pages/[[slug]].vue 将匹配 / 和 /test。

目录结构

-| pages/

---| index.vue

---| users-[group]/

-----| [id].vue

在上述示例中,您可以通过 $route 对象在组件中访问 group/id:

pages/users-[group]/[id].vue


pages/users-[group]/[id].vue

<template>

  <p>{{ $route.params.group }} - {{ $route.params.id }}</p>

</template>

  


访问路由可以使用全局的 useRoute 函数,它与 Options API 中的 this.$route 功能相同。

const route = useRoute()

const group = route.params.group

const id = route.params.id

获取当前路径

useRoute 返回当前路由, 必须在setup函数、插件或路由中间件中调用。

在Vue组件的模板中,可以使用$route访问路由。


const route = useRoute()  // == $route

console.log(route.path) // '/about'

  


//获取路径id

$route.params.id

  


//除了动态参数和查询参数, useRoute() 还提供了以下与当前路由相关的计算引用:

fullPath: 与当前路由关联的编码URL,包含path、query和hash

hash: 以#开头的URL的解码hash部分

matched: 与当前路由位置相匹配的归一化路由数组

meta: 附加到记录的自定义数据

name: 路由记录的唯一名称

path: URL的编码路径名部分

redirectedFrom: 在到达当前路由位置之前试图访问的路由位置

useRouter

useRouter 返回路由器实例,必须在设置函数、插件或路由中间件中调用。

在Vue组件的模板中,你可以使用$router 来访问路由器。


const router = useRouter()

  


//几个重要参数:  

url: http://localhost:3000/user/userinfo

router.name: 'user-userinfo'

router.fullPath: '/user/userinfo'

router.path: '/user/userinfo'

  


//几个重要的方法

router.back()

//$router.back() 返回上一页

router.forward()

router.go()

router.push({ path: "/home" })

router.replace({ hash: "#bio" })

back: 如果可能的话,回溯历史,和router.go(-1)一样。

forward: 如果可能的话,和 router.go(1)一样,在历史上前进。

go: 在历史中向前或向后移动,而不受 router.back() 和 router.forward()中强制执行的等级限制。

  
  


//刷新页面

let router = useRouter()

router.go(0)

navigateTo


// 将 'to' 作为字符串传递

await navigateTo('/search')

  


// ... 或者作为路由对象

await navigateTo({ path: '/search' })

  


// 动态路由

await navigateTo({ path: '/post/'+permlink})

//post动态页面接受方法

const route = useRoute()

cosnt permlink = route.params.permlink

  


// ... 或者作为带有查询参数的路由对象

await navigateTo({

  path: '/search',

  query: {

    page: 1,

    sort: 'asc'

  }

})

//serarch页面接受方法

const route = useRoute()

const query = route.query //用此方法接受参数

生命周期函数

手册

由于 Nuxt 整合了 Vue、Nitro 前后端两个运行时,再加上它自身的创建过程,因此框架生命周期钩子分为三类:

 Nuxt 钩子;

 Vue App 钩子;

 Nitro App 钩子。


const nuxtApp = useNuxtApp()

eg:

nuxtApp.hook("page:start",(e)=>{

        bar.value?.start()

        // console.log("page:start");

    })

nuxtApp.hook("page:finish", () => {

  window.scrollTo(0, 0)

})

Vue的钩子函数

参考


//有时无法自动运行

onMounted(() => {

    console.log(699, "moundted")

    function x (){

      console.log(722, "moundted")

    }

    x()

})

  


//beforeMount mounted

翻页时默认回到顶部


//app -> router.options.js

export default {

    scrollBehavior (to, from, savedPosition) {

        // 在按下 后退/前进 按钮时。就会像浏览器的原生表现那样

        if(savedPosition){

            return savedPosition

        }

  


        return {

            top:0

        }

    }

}

组件


components -> NaveBar.vue,

<NaveBar /> 可直接导入无需引入

嵌套组件


components -> Ui -> Menu.vue

<UiMenu>

  test

</UiMenu>

// 或者这样写:

<ui-menu>

  test

</ui-menu>

子组件接受父组件参数

defineProps来接受父组件传来的值


//index.vue

<Menu :active = "active"></Menu>

<NuxtPage :collection="defaultCollection" />

  


//Menu.vue 接受参数

defineProps({

  active:{

      type:Boolean,

      default:false

  }

})

  


const props = defineProps({

    title: String,

    info: String,

    author: String,

  })

// 等价于以 字符串数组声明 props

//const props = defineProps(['title', 'info', 'author']);

// 如果在setup中使用则直接 props.title

子组件向父组件传递参数或方法

defineExpose用法

defineExpose来导出自身的值或函数


//Roles.vue

<script setup>

let sonmsg = '这是子数据'

const open = () => { console.log("hello")}

//把数据导出

defineExpose({

  sonmsg,

  open

})

</script>

  


//index.vue

<template>

<div>

  <Roles ref="RoleRef"></Roles>

</div>

</template>

<script setup>

let RoleRef = ref(null)  //数据挂载到RoleRef上

//在函数中使用

const onSubmit = () => {

  RoleRef.value.open()

  console.log(566, "imgmodel", RoleRef.value.sonmsg)

}

</script>

获取数据

参考

获取数据推荐useFetchfetch这两个函数就可以。


useFetch

//express

res.status(200).send({

  bot: "hello world"

})

//前端    

const url = "http://localhost:6200/test"

const fetchConfig = {

  method: 'POST',

  headers: {

    'Content-Type': 'application/json'

  },

  body: {

    prompt: "hello",

    temperature: 0.3

  }

}

const { data, pending, error, refresh } = await useFetch(url, fetchConfig)

// console.log(866, data.value.bot)

//参数:

baseURL?: string  //可以配置公共部分的url,再和url拼接

lazy?: boolean  //和pending 一起使用。为true时,先加载页面的pending,得到数据后再展示

transform?: (input: DataT) => DataT // 处理返回的数据

  


//如果有两个以上useFetch,得到的data会重名,可以另起名字

let { data:dataX,  error:errorX } = await getHttp('/pay/order', token.value)

  


// 错误处理 error

//express

return res.status(422).send("没有授权!")

  


let { data, error } = await useFetch(url, optionX)

if(error.value) {

  console.log(444, error.value)

  message.error("失败!\n"+error.value.data, { duration: 5e3 })

  return

}

更改查询结果的名称

transform 函数来对结果拦截,并做出一些处理逻辑。


const { data: mountains } = await useFetch('/api/mountains', {

  transform: (mountains) => {

    return mountains.map(mountain => ({ title: mountain.title, description: mountain.description }))

  }

})

  


// 只选择模板中使用的字段

const { data: mountain } = await useFetch('/api/mountains/everest', {

  pick: ['title', 'description']

})

<template>

  <h1>{{ mountain.title }}</h1>

  <p>{{ mountain.description }}</p>

</template>

  
  


const { data, pending, error, refresh } = await useFetch("/api/getonepost", config, {

  transform: (res) => {

    return res.result

  }

})

$fetch

$fetch是与useFetch类似的封装函数,不过$fetch使用却是大大不同。

useFetch会和表格关联,造成一些不必要的http访问,$fetch则不会。

$fetch不能直接捕获到错误,只能通过 try catch


const baseURL = "http://localhost:6200"  

const option = {

  method: "POST",

  headers: {

    'Content-Type': 'application/json'

  },

  baseURL

}

const loginApi2 = async (url, body) => {

  let obj = {

      ...option,

      body

  }

  let res

  try {

    res = await $fetch(url, obj)

    console.log(566, "login api2", res)

  } catch (error) {

    res = error

    console.log(444, "login api2 error", error)

  }

  return res

}

fetch

fetch是浏览器原生的方法,非常简洁实用。像streaming(推流)也只能用 fetch 来实现,其它方法实现不了!


//注意:一定要加headers, 否则无法传值!

const fetchConfig = {

  method: 'POST',

  headers: {

      'Content-Type': 'application/json',

  },

  body: JSON.stringify({

      prompt: prompt,

      temperature: 0.3

  })

  }

const s = await fetch(url, fetchConfig)

console.log(559, s)

// 它得到的结果是直接传送回来的对象

  


eg:

let option = {

  method: "POST",

  headers: {

    'Content-Type': 'application/json',

    'Authorization': 'Bearer ' + captchaToken.value

  },

  body: JSON.stringify(form)

}

let response = await fetch(baseURL+'/userapi/register', option)

if(response.ok){

  let resX = await response.json()

  message.success("注册成功", { duration: 5e3 })

} else {

  let err = await response.text()

  message.error("错误!\n"+err, { duration: 5e3 })

}

useLazyFetch

useLazyFetch 为useFetch提供了一个包装器,通过将lazy选项设置为true,在处理程序解析之前触发导航。

默认情况下,useFetch 会阻塞导航,直到它的async处理程序被解决。useLazyFetch则不会阻塞,实时渲染数据!它将有更好的用户体验。

useLazyFetch 与 useFetch 具有相同的签名。


<template>

  <div v-if="pending">

    Loading ...

  </div>

  <div v-else>

    <div v-for="post in posts">

      <!-- do something -->

    </div>

  </div>

</template>

  


<script setup>

/* Navigation will occur before fetching is complete.

  Handle pending and error states directly within your component's template

*/

const { pending, data: posts } = useLazyFetch('/api/posts')

watch(posts, (newPosts) => {

  // Because posts starts out null, you won't have access

  // to its contents immediately, but you can watch it.

})

</script>

只客户端渲染


//可以用以下测试是否在客户端:

console.log(12, document.cookie)

console.log(33, window)

注意: setup是在服务端,其中的函数却基本都在客户端渲染!

  


//组件中客户端渲染

<ClientOnly> component 只在客户端呈现它的slot。若只在客户端导入组件,请在客户端插件中注册该组件。

<ClientOnly fallback-tag="span" fallback="Loading comments...">

  客户端渲染

</ClientOnly>

  


//js中的使用

if (process.server){

  //服务器端

}

process.client  //客户端

  


//在路由中设为单页面渲染,也就是关闭ssr

//nuxt.config.ts 如下设置,则gpt页面则只客户端渲染

routeRules: {

    '/gpt': { ssr: false },    

},

状态管理

useState用于创建响应式的且服务端友好的跨组件状态,类似于Vue中的state的功能。

简单的需求只用useState就可以,复杂的话可以结合pinia


const counter = useState('counter', () => Math.round(Math.random() * 1000))

counter++

counter.value = 20 //重新赋值

  


const user = useState('user', () => {

  return {

    token: token.value,

    user: userObj.value

 }

})

  


//composables/useState.ts

export const useCounter = () => useState<number>('counter', () => 0)

export const useColor = () => useState<string>('color', () => 'pink')

  


<script setup>

const color = useColor() // Same as useState('color')

</script>

  


// 初始化状态

大多数情况下,你可能希望使用异步解析的数据来初始化状态。你可以使用 app.vue 组件和 callOnce 工具函数来实现这一点。

<script setup lang="ts">

const websiteConfig = useState('config')

  


await callOnce(async () => {

  websiteConfig.value = await $fetch('https://my-cms.com/api/website-config')

})

</script>

Pinia

文档

管理全局状态的模块,可以和useState一起使用。


yarn add pinia @pinia/nuxt

  


modules: [

    // ...

    '@pinia/nuxt',

  ],

useCookie

在你的页面中,组件和插件你可以使用useCookie,一个SSR友好的组合来读写cookies。


const cookie = useCookie(name, options)

eg:

const counter = useCookie('counter', { maxAge: 60 }) //60秒有效期

counter.value = {

  name: "hello",

  age: 37

}

const token = useCookie('token', {maxAge: 60 * 5})

token.value = "hello world"

  


let user = useCookie('users', {maxAge: 60 * 60 * 24 * 30})  //30天 本地测试会有些问题

user.value = "hello ddskljdsfklj"

console.log(257, "user", user, 886, user.value)

  


// 获取

const s = useCookie('counter')

s.value

  


const balance2 = useCookie('balance', {maxAge: 60 * 60, SameSite: "none"})

  


const hasConsent = useCookie('dialog', {

  sameSite: 'strict',

  default: () => 'no',

  maxAge: 2592000, // 30 days in seconds

});

  


//删除则设值为null即可

s.value = null

const token = useCookie("token")

token.value = null

  


const s = useCookie('counter')

const removeCookie = () => {

    s.value = null

}

  


// 将用户登录成功返回的token存储在cookie当中,用户登录成功的标识

const token = useCookie("token")

token.value = data.value.token

const user = useUser()

user.value = data.value

  


//判断token是否有值

const token = useCookie("token")

token.value == undefined  //无值

或者 :token.value == null

简写: !token.value  //无值则为true

  


//Server端的使用

// server/api/counter.ts

export default defineEventHandler(event => {

  // Read counter cookie

  let counter = getCookie(event, 'counter') || 0

  


  // Increase counter cookie by 1

  setCookie(event, 'counter', ++counter)

  


  // Send JSON response

  return { counter }

})

Layers

能够复用之前项目中的配置、已存在的组件、工具方法等等。Nuxt提供的 layers 特性能使我们非常容易地创建和维护这样的项目基础结构。

以下情况下比较适合使用 layers:

  • 共享可重用配置项;

  • 使用 components 目录共享组件库;

  • 使用 composables 和 utils 目录共享工具函数;

  • 创建 Nuxt 主题;

  • 创建模块预设;

  • 共享标准初始化步骤。

Server

文档

服务器 Server

Nuxt具备的类似于express的功能,具有接受和响应http的能力。

Nuxt自动扫描~/server/api, ~/server/routes, 和 ~/server/middleware目录中的文件,以注册具有HMR支持的API和服务器处理程序。

每个文件都应该导出一个用defineEventHandler()定义的默认函数。

处理程序可以直接返回JSON数据,一个Promise或使用event.node.res.end()发送响应。


//创建一个新文件server/api/hello.js:

//server/api/hello.js

export default defineEventHandler((event) => {

  return {

    api: 'works'

  }

})

//使用await $fetch('/api/hello')通用地调用这个API

  


eg2:

// server/routes/test.post.js  客户端必须使用POST方法

export default defineEventHandler(async (event) => {

  //获取客户端的body

  const body = await readBody(event)  

  console.log(695,"server body", body)

  return {

    hello: 'world'

  }

})

//客户端调用

const config = {

  method: 'POST',

  headers: {

      'Content-Type': 'application/json'

  },

  body: {

    content: "我是什么",

    temperature: 0.3

  }

}

const { data, pending, error, refresh } = await useFetch("/test", config)

  


// nuxt.config.ts设置

routeRules: {

  // 为 SEO 目的在构建时生成

  '/': { prerender: true },

  // 缓存 5分钟  好像在访问上出问题! 用 useState

  '/api/*': { cache: { maxAge: 60 * 5 } }

},

  


// 抛出错误

throw createError({

  statusCode: 400,

  statusMessage: "This user does not exist.",  //此处只能用英文,中文无法传输!

})

  


// String error where `statusCode` defaults to `500`

throw createError("An error occurred")

服务端工具函数

server/utils 目录中的工具函数,会自动导入

若用默认导出 export default ,则自动导入的函数名为文件名

若用具名导出 export function useSum ,则自动导入的函数名为函数定义的名称。


// server/utils/sum.ts

export function useSum(a: number, b: number) {

  return a + b;

}

  


//在接口文件中直接使用即可

// routes/index.ts

export default defineEventHandler(() => {

  const sum = useSum(1, 2) // auto-imported

  return { sum }

})

打包

  • SSR

默认情况下,直接执行(nuxt build) yarn build。代码会被打包到.output目录,打包产物分为 public 和 server 两部分。入口为 index.mjs,可以使用 node 或 pm2 等进程管理工具启动服务, node ./.output/server/index.mjs,也可以配合nuxt preview启动预览服务。


  //默认情况下nuxt是不会将环境变量打包进去的,需要在package.json中配置

  "build": "source .env && nuxt build",

  


  启动 node.js 服务

  `node .output/server/index.mjs`

  `node aijoe_v5/server/index.mjs`

  


  //ecosystem.config.js

  module.exports = {

    apps: [

      {

        name: 'czblog',

        port: '8080',

        exec_mode: 'cluster',

        instances: 'max',

        script: './.output/server/index.mjs'

      }

    ]

  }

  pm2 start ecosystem.config.js

  • SPA

ssr:false + nuxt generate。产物只有 .output/public 中的静态文件,发布 .output/public(还有 dist) 即可。但是 SPA 需要在运行时访问接口获取数据,因此仍然需要提供接口服务才能正常显示页面。

配置 ssr: false,然后执行 yarn generate


export default defineNuxtConfig({

  ssr: false,

})

  • SSG

nuxt generate。产物只有 .output/public 中的静态文件,发布 .output/public 即可。这种方式会在创建时生成页面内容,因此只需要提供静态服务即可预览。

  • 其他服务:presets,可用于其他非 node 运行时打包,例如 deno,serverless,edge worker 等。产物根据预设不同会有不同,部署需要按照对应的平台进行操作。

启动端口


//开发环境

//package.json

"dev": "nuxt dev --port 3200"

// 或是这样:

"config": {  // here are the changes

    "nuxt": {

        "host": "0.0.0.0",

        "port": "3333"

    }

}

//或者在 nuxt.config.ts 中设置 只在开发中有效

devServer: {

  port: 9685

}

//或者,在 .env 中设置 `PORT = 9685` 即可!  

这样访问:

 if(process.server){

  console.log(566, process.env.PORT)

 }

  


//设置服务器端口 port, 找到源码,直接修改即可!

源码: .output/server/index.mjs

//const port = destr(process.env.NITRO_PORT || process.env.PORT) || 3e3;

const port = 5896

数据推流

经测试,只有浏览器端的 fetch 才有 streaming 的能力,其它方法不行!

useFetch, $fetch都不行!


const url = "http://localhost:6200/gptstreaming"

const prompt = "天空为什么是蓝色的?"

let query = [{role: "user", content: prompt}]

let dataObj = {

  method: 'POST',

  headers: {

      'Content-Type': 'application/json',

  },

  body: JSON.stringify({

      query: query,

  })

}

const response = await fetch(url, dataObj)

if (response.ok) {  

  let i = 0

  let getStream = function (reader) {

      return reader.read().then(function (result) {

        // 如果数据已经读取完毕,直接返回

        if (result.done) {

          console.log(889, "result done")

          return

        }

        // 取出本段数据(二进制格式)

        let chunk = result.value

        console.log(226, chunk, typeof chunk)  //unit8array object

        let text = utf8ArrayToStr(chunk) //数据解析

        // 将本段数据追加到网页之中

        messageDiv.innerHTML += text

        // 递归处理下一段数据

        return getStream(reader)

      })

  }

  getStream(response.body.getReader())

}

加密


npm install bcrypt --save

// npm i node-gyp -g  如果node-pre-gyp ERR!

// npm install bcrypt --unsafe-perm --save

  


import bcrypt from 'bcrypt'

const saltRounds = 10

  


//加密

set(val) {

  let salt = bcrypt.genSaltSync(saltRounds)

  let hash = bcrypt.hashSync(val, salt)

  return hash

}

  


// 验证密码 为true则正确

const isPasswordValid = bcrypt.compareSync(

  req.body.password,

  user.password

)

jwt验证

Json Web Token (JWT) 是一种开放标准 (RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为 JSON 对象。此信息可以进行验证和信任,因为它是经过数字签名的。可以使用密钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 对公钥/私钥对对 JWT 进行签名。

可以认为JWT是用户认证的一种协议,可以理解为服务端颁发给客户端的一张身份证,拿到身份证后此后客户端进行接口访问时携带身份证,服务端通过验证以允许客户端访问服务器资源,每一项的含义:

  1. Json表示令牌的原始值是一个Json格式的数据

  2. web表示是在互联网传播

  3. token表示令牌

参考 |

参考2 |

参考3


npm install jsonwebtoken --save

// yarn add jsonwebtoken

  


import jwt from 'jsonwebtoken'

  


const SECRET = "fdfh85JYxxxxx"

// 生成token

const token = jwt.sign({

  id: String(user._id),

}, SECRET)

  


//时限

let token =  jwt.sign(data, SECRET, {

  expiresIn:  60 * 60 * 24 * 3 // 以s作为单位(目前设置的过期时间为3天)

});

  


//验证

const rawToken = String(req.headers.authorization).split(' ').pop()

const tokenData = jwt.verify(rawToken, SECRET)

// console.log(666, tokenData)  //{ id: 'dddfhxxx', iat: 1684119284, exp: 1684119304 }

  


// 获取用户id

const id = tokenData.id

  


// Authorization: Bearer

const token = useCookie('token')

headers: {

    'Authorization': 'Bearer ' + token.value,

    'Content-Type': 'application/json'

  }

  


//token token过期的错误会在 catch 捕获 `jwt expired`

try{

  // 获取客户端请求头的token

  const rawToken = String(req.headers.authorization).split(' ').pop()

  if(rawToken == "undefined"){

    return res.status(422).send("您尚未登录!")

  }

  const tokenData = jwt.verify(rawToken, SECRET)

  // 获取用户id

  const id = tokenData.id

  let user = await User.findById(id)

  if(user){

    req.user_id = id

    req.user = user

    next()

  } else {

    console.error(111)

    res.status(422).send("无效授权!")

  }

} catch (error) {

  console.error(112,"token auth", error)

  res.status(500).send('Something went wrong')

}

获取用户IP地址


async function smsGet(){

 let ip = req.headers['x-forwarded-for'] ||

        req.ip ||

        req.connection.remoteAddress ||

        req.socket.remoteAddress ||

        req.connection.socket.remoteAddress || '';

 if(ip.split(',').length>0){

    ip = ip.split(',')[0]

 }

 ip = ip.substr(ip.lastIndexOf(':')+1,ip.length);

  


console.log(123, ip)

}

// smsGet()

图形验证码

文档 |

node生成验证码图片


npm install svg-captcha --save

  


const svgCaptcha = require('svg-captcha')

  


const codeConfig = {

  size: 6,// 验证码长度

  ignoreChars: '0o1i', // 验证码字符中排除 0o1i

  noise: 2, // 干扰线条的数量

  fontSize:42,

  color:true,//开启文字颜色

  background:"#cc9966",//背景色

  width:150,

  height: 44

}

const captcha = svgCaptcha.create(codeConfig)

//{text:jjkjk, data: dddd}

  


//图形验证码与jwt结合

1. 生成验证码后,将`text`用 jwt生成token, 和 data一起发送给前端。

2. 服务器端接受到用户发送回的 text_user和 token。

3. 服务器解密token中的`text`和用户的text_user比较,一致则正解,反之则错误。

  


//图形验证码与数据库结合

和jwt类似,生成`text`存在数据库, data发给前端。用户填好验证码后提交到数据库后比对。

本地存值localStorage


// 用localStorage来实现一个预设值

1. 先从localStorage读取,有值则赋给它设初值

const defaultRole = ref(null)

if(process.client){

  let latest = localStorage.getItem("defaultRole")

  console.log(566, "latest", latest)

  if(latest != null){

    defaultRole.value = latest

    console.log(599, "latest")

  }

}  

2. //设置或更新 如果是空值 或是与前值不同,都重新赋值

let latest = localStorage.getItem("defaultRole")

if(latest == null || role != latest){

   localStorage.setItem('defaultRole', role)

   console.log(899, "set lastname")

}

删除div

js删除dom节点或div


<div id="img1">

  图片1

<input  type="button" onclick="del('img1')" value="删除元素"/>

</div>

  


<script>

function del(divId){

  document.getElementById(divId).remove()

}

</script>

模板字符串语法

字符串的扩展

模板字符串

ES6模板字符串

字符串模板拼接的点击事件

模板字面量是用反引号(`)分隔的字面量,允许多行字符串、带嵌入表达式的字符串插值和一种叫带标签的模板的特殊结构。


`string text`

  


`string text line 1

 string text line 2`

  


`string text ${expression} string text`

`string text ${fn()} string text`

  


//可以单行,多行,嵌入变量和函数都是可以的。还可以生成div结构

// 模板字符串生成dom元素

const loadingStripe = (uniqueId) => {

    return (

        `<span style='font-size: 20px;color: #191970;' id=${uniqueId}></span>`

    )

}

  


//如果要嵌入点击事件,就要相对麻烦一些

// 需要与onclick结合,再将这个函数挂载到window上

const itemStripe = (username, email, created, uniqueId) => {

  return (

    `

    <div border-dashed border-1 text-3.2 mb-2 id=${uniqueId}>

      <div flex ><div ml-auto px-2 hover:bg-red onclick="deleteUser('${userId}')">X</div></div>

      <div>名字: ${username}</div>  

      <div>email: ${email}</div>

      <div>时间: ${TimesToLocal(created)}</div>

    </div>

    `

  )

}

if(process.client){

  window.deleteUser = deleteUser

}

const deleteUser = async (userId) => {

  let body = {

    id: userId

  }

  let { data,  error} = await postHttp('/deluser', body, 'he')

  if(error.value) {

    message.error("删除失败!\n"+error.value.data, { duration: 5e3 })

    return

  }

  // document.getElementById(id).remove()

  message.success("删除成功!", { duration: 5e3 })

}

//TimesToLocal是全局的方法,时间转换

实时渲染div


<div v-for="(item,index) in history" :key="index"

  :id=item._id border-dashed border-1 text-3.2 mb-2

  >

    <div flex ><div ml-auto px-1 hover:bg-red @click="delX(item._id)">X</div></div>

    <div>名字: {{item.username}}</div>  

    <div>email: {{item.email}}</div>

</div>

<div text-center><n-button @click="more" v-if="moreFlag" strong secondary>更多</n-button></div>

//js

//把取得的值遍历,塞入temp, 再赋给history。history是响应式的,div就会实时渲染出来!

let history = ref(null)

let temp = []

for (let i in data.value.payhistory) {

    temp.push(data.value.payhistory[i])

  }

history.value = temp

  


//另一种方法

//直接使用document来操作DOM

<div id="item_container"></div>

//js

const itemStripe = (userId, username, email,created, uniqueId) => {

  return (

    `  

    <div border-dashed border-1 text-3.2 mb-2 id=${uniqueId}>

      <div flex ><div ml-auto px-1 hover:bg-red onclick="deleteUser('${userId}','${uniqueId}')">X</div></div>

      <div>名字: ${username}</div>  

      <div>email: ${email}</div>

      <div>时间: ${TimesToLocal(created)}</div>

    </div>

    `

  )

}

let itemContainer = document.querySelector('#item_container')

for (let i in data.value.payhistory) {

  console.log(56, data.value.payhistory[i])

  let userId = data.value.payhistory[i]._id

  let username = data.value.payhistory[i].username

  let email = data.value.payhistory[i].email

  let created = data.value.payhistory[i].created

  let uniqueId = generateUniqueId()

  itemContainer.innerHTML += itemStripe(userId, username, email, created, uniqueId)

}

上传图片至IPFS


//<!-- 上传图片 -->

<n-upload mt-2

  :default-upload="false"

  list-type="image-card"

  multiple

  @change="handleChangeX"

>

  上传参考图

</n-upload>

  


//js

const handleChangeX = (options) => {

  if(options.fileList.length == 0) {

    return

  }

  handleClick(options.fileList[0].file)

}

const handleClick = async (file) =>{

  let ref_img = await upImage(file)

  prompt.value += ref_img+' '

}

  


//composables

yarn add ipfs-http-client

  


import {create} from 'ipfs-http-client'

import { Buffer } from 'buffer'

  


export const readFile = (file) => {

  if (process.client){

    return new Promise(resolve => {

      let reader = new FileReader()

      reader.readAsArrayBuffer(file)

      reader.onload = (event) => {

        resolve(Buffer.from(event.target.result))

      }

    })

  }  

}

 

export const upImage = async(pos, file) => {

  if (process.client){

    const ipfs = create({ host: 'ipfs.example.com', port: '9059', protocol: 'https' })

    const ipfs_host = "https://example.com/ipfs/"

    let content = await readFile(file)

    let res = await ipfs.add(content)

    return ipfs_host+res.path

  }

}

显示图片


 <n-image

    width=98%

    mt-3

    src="https://example.com/ipfs/QmQKptGbRRziEjxxxxxx"

    rounded-2 alt="AIJoe Meme"

 />

 //width=98% 可以使得图片自适应大小

  


<div class="contentimg">

  <img

    width=98%

    mt-3

    :src="takeImage(item.body)"

    rounded-2

  />

</div>

<style>

.contentimg {

  width: 98%;

  height: 28rem;

  overflow: hidden;

  margin: auto;

}

.contentimg img {

  width: 100%;

  height: 100%;

  object-fit: cover;

}

</style>

图片自适应宽度或高度

参考


.container {

  width: 50%;

  height: 300px;

  overflow: hidden; /* 防止图片溢出容器 */

}

  


.container img {

  width: 100%;

  height: 100%;

  object-fit: cover;

}

  


上述代码中,我们定义了一个名为 container 的容器,并设置了宽度为 50% 和高度为 300px。接下来,我们通过 overflow 属性设置了容器的溢出属性为 hidden,以防止图片溢出容器。

  


在 img 标签中,我们使用了 width 和 height 属性将图片的大小设置为与容器相同,并且使用了 object-fit 属性将图片按比例缩放并居中显示。具体来说,object-fit 属性的值为 cover,意味着图片会拉伸或缩小以填充整个容器,并保持原始比例。

mavon-editor

文档

文档2

github

新版安装指南


1.yarn add mavon-editor@next  //"^3.0.2"

// npm install mavon-editor@next --save

  


2.添加并编辑插件 ~/plugins/mavon-editor.client.js

import mavonEditor from 'mavon-editor'

import 'mavon-editor/dist/css/index.css'

  


export default defineNuxtPlugin((nuxtApp) => {

  nuxtApp.vueApp.use(mavonEditor)

})

  


3. 前端使用

<template>

  <ClientOnly>

    <mavon-editor v-model="content" :toolbars="toolbarsConfig"/>

  </ClientOnly>

</template>

  


<script setup>

const model = ref()

const toolbarsConfig = {

  // ... toolbars config

}

</script>

  


4. 上传图片

//增加两个参数: md、@imgAdd

<mavon-editor v-model="content" ref="md" @imgAdd="upImage" /> <br>

<script setup>

  let md = ref()

  const readFile = (file) => {

    return new Promise(resolve => {

      let reader = new FileReader()

      reader.readAsArrayBuffer(file)

      reader.onload = (event) => {

        resolve(Buffer.from(event.target.result))

      }

    })

   }

  const upImage = async(pos, file) => {

    //换成自己的ipfs主机,或是使用自己的上传逻辑

    const ipfs = create({ host: 'ipfs.example.io', port: '2885', protocol: 'https' })

    const ipfs_host = "https://ipfs.example.io/ipfs/"

    let content = await readFile(file)

    let res = await ipfs.add(content)

    md.value.$img2Url(pos, ipfs_host+res.path)

  }

</script>

  


5. 保存草稿箱

@save="saveMavon"  快捷键 ctrl+s

saveMavon(value,render){

  console.log("this is render"+render)  //html渲染的格式

  console.log("this is value"+value)   //原始格式, String

}

  
  


6. 解析markdown文本

<ClientOnly>  

  <mavon-editor

    v-model="post.result.body"

    :subfield="false"

    defaultOpen="preview"

    :toolbarsFlag="false"

    :editable="false"

    :scrollStyle = true

    :ishljs="true"

  ></mavon-editor>

</ClientOnly>

nuxt-tiptap-editor

文档 |

参考 |

参考 |


yarn add nuxt-tiptap-editor

// npx nuxi@latest module add tiptap

  


//nuxt.config.ts设置:

//会自动添加

modules: ['nuxt-tiptap-editor']

//component -> TiptapEditor.vue

<template>

  <TiptapEditorContent :editor="editor" />

</template>

<script setup>

const editor = useEditor({

  content: "<p>I'm running Tiptap with Vue.js.</p>",

  extensions: [TiptapStarterKit],

})

const getContent = () => {

  console.log(111, unref(editor)) //是主要的使用方法集 unref(editor).commands

  console.log(9988, unref(editor).getHTML())

  return unref(editor).getHTML() //getText({ blockSeparator: "\n\n" }) getJSON()

}

onBeforeUnmount(() => {

  unref(editor).destroy()

})

//如果父组件调用的话就用 defineExpose的方法

defineExpose({

  getContent  

})

</script>

  


// 父组件 edit.vue

<TiptapEditor ref="contentRef"/>

  


const contentRef = ref()

const content = contentRef.value.getContent()  //即可得到编辑器中的数据

turndown

HTML转Markdown 能将复杂的HTML结构转化为简洁的Markdown,方便编辑和发布。

Turndown是轻量级的JavaScript库,适合前端或Node.js项目,易于集成到Web应用。


yarn add turndown  //7.2.0

// npm install marked turndown

  


import TurndownService from 'turndown'

  


const turndownService = new TurndownService()

  


const html = '<h1>Hello, World!</h1><p>This is a <em>sample</em> HTML document.</p>'

const markdown = turndownService.turndown(html)

  


console.log(markdown)

  
  


const { marked } = require('marked')

const Turndown = require('turndown')

const turndownService = new Turndown()

 

const html = '<div><h1>Dynamic</h1><p>Content</p></div>'

const markdown = turndownService.turndown(html)

console.log(markdown)

Marked

参考

解析Markdown文本


yarn add marked

// npm install marked

// const marked = require('marked');

import { marked } from 'marked'

  


const markdown = '# Hello, world!'

const options = {

  gfm: true,

  breaks: true,

  smartLists: true,

  highlight: function(code) {

    return require('highlight.js').highlightAuto(code).value

  }

}

  


const html = marked(markdown, options)

// const html = marked.parse('# Marked in Node.js\n\nRendered by **marked**.')

console.log(html)

  


<div v-html="mdshow(md)"></div>

const mdshow = (md) => marked(md, options)

实时监听和渲染


//ref() 和 computed() 被自动导入

const count = ref(1)

const double = computed(() => count.value * 2)

  


//使用computed监听路由变化

const route = useRoute()

const pageKey = computed(()=>route.fullPath)

const activeName = computed(()=>route.name)

  
  


//使用watch监听路由变化

watch(route, (to) => {

  console.log('路由',to)

}, {flush: 'pre', immediate: true, deep: true})

  
  


// 使用watch监听数据变化,做出相应逻辑

watch(tagOption, () => {

  console.log(6688, 'tagOption2', tagOption.value[[tagOption.value.length - 1]])

  tagsX.value += tagOption.value[[tagOption.value.length - 1]]

}, {flush: 'pre', immediate: true, deep: true})

  
  


//使用watch监听路由变化,即刻获取数据,渲染页面

// 每当进入页面都会发生一次 watch

// 每当 keywords 数据发生一次变化,就会发生一次 watch

watch(()=>route.query.keywords,(newVal)=>{

    keywords.value = newVal

    getData()

}, {flush: 'pre', immediate: true, deep: true})

实时多选数据进输入框

使用watch实现实时数据更新


//<!-- 标签选项 -->

<div m-y-3>选择标签或填写:

<n-checkbox-group v-model:value="tagOption">

  <n-space item-style="display: flex;">

    <n-checkbox value=" blockchain " label="区块链" />

    <n-checkbox value=" cryptocurrency " label="加密货币" />

    <n-checkbox value=" bitcoin " label="比特币" />

  </n-space>

</n-checkbox-group>

</div>

<div m-b-3>

<n-input

    v-model:value="tagsX"

    type="text"

    placeholder="输入英文标签,以逗号或空格分隔"

/>

</div>

<script setup>

const tagOption = ref(' ')

const tagsX = ref('cn  cn-reader ')

  


watch(tagOption, () => {

  tagsX.value += tagOption.value[[tagOption.value.length - 1]] //每次增加一条

}, {flush: 'pre', immediate: true, deep: true})

</script>

x+y必须是奇数,一行代码,0ms双百

2024年1月29日 13:59

Problem: 3021. Alice 和 Bob 玩鲜花游戏

[TOC]

思路

按题意,x + y 要是奇数。因此,推导下,结果就是:

$$n 内偶数的个数 * m 内奇数的个数 + m 内偶数的个数 * n 内奇数的个数$$

$$= (n >> 1) * ((m + 1) >> 1) + (m >> 1) * ((n + 1) >> 1)$$

Code

执行用时分布0ms击败100.00%;消耗内存分布5.55MB击败100.00%

###C

long long flowerGame(int n, int m) {
    return (long)(n >> 1) * ((m + 1) >> 1) + (long)(m >> 1) * ((n + 1) >> 1);
}

###Python3

class Solution:
    def flowerGame(self, n: int, m: int) -> int:
        return (n >> 1) * ((m + 1) >> 1) + (m >> 1) * ((n + 1) >> 1)

您若还有不同方法,欢迎贴在评论区,一起交流探讨! ^_^

↓ 点个赞,点收藏,留个言,再划走,感谢您支持作者! ^_^

零一开源|前沿技术周刊 #13

作者 kymjs张涛
2025年8月25日 13:52

前沿技术周刊 是一份专注于技术生态的周刊,每周更新。本周刊深入挖掘高质量技术内容,为开发者提供持续的知识更新与技术洞察。

订阅渠道:【零一开源】、 【掘金】、 【RSS


大厂在做什么

美团智能头盔作为专为外卖骑手打造的智能安全装备,具备蓝牙通话、戴盔识别、智能语音助手、碰撞摔倒监控等功能,核心软件功能围绕如何通过主动安全和被动安全相结合的方式有效保护骑手。 本期分享主要介绍智能头盔骑行通话质量、智能语音助手、碰撞摔倒监控三项软件能力。其中“骑行通话质量和智能语音助手”降低骑手操作手机导致的“分心”,帮助骑手“防患于未然”。“碰撞摔倒监控”最大限度的保护骑手、快速的感知事故和触发救治。
在数字内容井喷的时代,移动端已成为视频创作的重要阵地,而视频编辑页作为创作工具的核心场景,不仅为创作者提供了丰富的表达手段和创意平台,更是提升视频制作的效率。通过直观的操作界面和丰富的功能集成,用户可以轻松地将素材、音频、特效及文字等进行融合,创造出独具风格、彰显个性的作品。
如今,AI 编程工具正在重塑软件开发,其核心目标直指“开发民主化”。它们不再仅仅是补全代码片段的助手,而是能理解自然语言需求、生成可运行代码框架、甚至参与系统设计的“协作者”。这一背景下,越来越多的企业开始对外发布相关产品,美团便是其中之一。

新技术介绍

迄今为止最大的Compose更新带来了原生自动填充, 智能动画以及让构建Android用户界面如同魔法般轻松的功能
兄弟,你发的这篇Flutter 3.35更新的文章内容好像有点小状况啊——页面显示“环境异常”,得先验证才能看具体内容。我这刷了半天,也没瞅见更新了啥新特性、优化了哪些性能。要不你先去把验证搞定,把正经的更新内容放出来?等内容齐了,我再帮你扒拉扒拉这版3.35到底香不香~

深度技术

这篇文章我瞅着是讲Android底层的,主要扒了ART虚拟机加载Dex的整个流程,从Dex文件解析到内存映射、类加载这些关键步骤都拆得挺细。重点是结合脱壳场景,分析了加载过程里哪些节点能当通用脱壳点——比如某个钩子函数的调用时机、内存中Dex原始数据的暴露时刻。对咱们这种搞Android逆向或底层开发的来说,理清ART Dex加载逻辑,找脱壳点就有章法了,实操性挺强,值得细品。
在AI技术迅猛发展的今天,如何与大型语言模型高效“对话”已成为释放其潜力的关键。本文深入探讨了提示词工程(Prompt Engineering)这一新兴领域,系统解析了从基础概念到高级技巧的完整知识体系,并结合“淘宝XX业务数科Agent”和科研论文深度学习两大实战案例,揭示了高质量提示词如何将AI从“工具”升级为“智能协作者”。无论你是初学者还是实践者,都能从中掌握让AI真正为你所用的核心方法论。
Cursor 是近来大火的 coding agent 工具,凭借其深度集成的智能代码生成、上下文感知和对话式编程体验,极大地提升了开发效率,成为众多工程师日常开发的得力帮手。作为 Cursor 的付费用户,我已将其作为主力编码工具,每天在实际项目中频繁使用。只有真正深入使用,才能切身感受到它所带来的编程体验的神奇之处。在这个过程中,我也对其背后的技术实现产生了浓厚兴趣,本文试图通过一系列实验,深入分析 Cursor 在后台与大模型之间的通信机制,探寻 Cursor 智能能力背后的底层思想与设计原理。

码圈新闻

这两天在上海世博展览馆举行的 2025 世界人工智能大会(WAIC)热度相当高,上到央媒下到朋友圈不断看到,甚至总理李强、双奖(诺贝尔/图灵)得主辛顿都在开幕式出现,影响力爆表。 周末去逛了一天,AI 的落地场景之多令人咋舌,看完以后我给之前的好几个点子都划上了删除线。还是得多出来看看大厂/新秀公司都在做什么,避免做类似的事情。 这篇文章按照类别记录一下印象比较深刻的产品。
刚刷完2025 Google开发者大会的客户端内容,给咱3年+的老哥们捋捋重点。 Android 15是重头戏:后台任务管理收紧了,得注意`WorkManager`新的电量阈值限制,不然应用可能被系统强杀;UI渲染加了硬件加速新接口,复杂列表滑动能再提10-15帧,对电商、社交类应用挺香。 开发工具方面,Android Studio Hedgehog直接集成了AI代码诊断,写`Compose`时会自动提示重组优化点,试了下比之前手动查省事儿多了。Flutter 4.0也放了大招,原生代码互调延迟降了40%,混编项目终于不用再纠结性能损耗了。 哦对了,跨平台布局`Jetpack Multiwindow`支持更完善了,平板/折叠屏适配能少写一半适配代码。暂时就这些干货,后台优化和Flutter新特性建议优先上手,其他的可以先放收藏夹吃灰~

博客推荐

兄弟,你给的这篇文章内容好像有点问题啊。标题写着《适配 16KB 页面大小:提升应用性能并为用户提供更流畅的应用体验》,但正文全是微信环境异常的提示,什么“完成验证后继续访问”“小程序赞”“在看”之类的,根本瞅不见正经内容。这样我没法帮你总结摘要啊,估计是复制的时候出岔子了?要不你检查下内容是不是漏了,或者重新发下正文?等你弄好我再帮你扒拉扒拉~
兄弟们,刚瞅了眼你发的《深入浅出Android的Context机制》,内容咋全是微信验证、点赞那些玩意儿?正文好像没显示出来啊。不过Context这东西咱老安卓开发肯定熟,简单说就是个“万能管家”——访问资源、启动Activity/Fragment、调系统服务(比如LayoutInflater、NotificationManager)都得靠它。最容易踩坑的就是Context的生命周期:Application Context全局单例,跟着应用走;Activity Context跟页面生命周期绑定,用完就没。要是拿Activity Context搞个静态单例,页面关了还被占着,内存泄漏妥妥的。平时记着:长生命周期的对象(比如单例、Handler)别用Activity Context,能用Application Context就用,准没错。等你文章内容正常了再细扒,先记住这几点避坑~
一般来说ArkWeb作为鸿蒙的Web容器,性能是够用的。但是针对网页的前置处理条件较多,例如涉及到DNS,大量的资源下载,网页和动画渲染等。作为重度依赖资源链的容器,当某个资源还没ok,就会很容易出现白屏,卡端,长时间loading这些影响用户体验的问题。

GitHub 一周推荐


关于我们

零一开源】 是一个 文章开源项目 的分享站,有写博客开源项目的也欢迎来提供投递。 每周会搜集、整理当前的新技术、新文章,欢迎大家订阅。

[奸笑]

未来将至:人形机器人运动会 - 肘子的 Swift 周报 #99

作者 Fatbobman
2025年8月25日 22:00

不久前在北京举办的世界人形机器人运动会上,出现了许多令人忍俊不禁的场景:机器人对着空气挥拳、跑步时左摇右摆、踢球时相互碰撞后集体倒地。尽管这些画面看起来颇为滑稽,但回顾过去几年人形机器人的发展历程就会发现,即便当前的产品仍存在诸多不足,其进步却是惊人的。按照这样的发展速度,也许在十年甚至更短的时间内,人形机器人就将走进我们的日常生活,满足各种实际需求。

(多图预警!!!) 边统计边压缩,给你超详细的解答

作者 quantum-10
2020年7月5日 17:16

image.png

写在文首

看了很多题解,大家大致的思路基本一致,时间复杂度为O(n·n·m),但大多题解没有把过程表述的很完整,作为一个刷题的小白,我想在这写写小白也能看懂的思路,和做这道题的一些细节和收获

解题思路

题目要求既要考虑1x2, 2x1的情况, 也要考虑2x2的情况:
(1)先考虑1xn的情况,即在一行中(横向)可以找出多少个矩形**(这步我称之为"统计")**,代码如下:
统计行中的矩形数目 int now = 0; for (int k = 0; k < col; k++){ if (mat[j][k] == 0) now = 0; else now = k == 0 ? mat[j][0] : now + 1; ans += now; }

  1. 当有连续三个1的时候,用now分别递增为1,2,3(第一个1可以形成一个矩形,第二个1和第一个1可以形成1个矩形加上其自身一共是两个,第三个1分别可以和前面连续的1形成两个长度分别为3,2的矩形,加上自身一共三个),并加到ans中
  2. 这一步也可以记录连续的个数,使用等差数列求和公式计算连续1处可以形成的矩形数目,具体代码不展示
  3. 当遍历到mat[j][k] == 0时,即1不连续了,置now为0,以便后面遇到连续1时进行统计

(2)压缩

  1. 理论上,纵向的矩形也可以像横向那样进行统计,但是我们注意到我们不仅要求横向和纵向的矩形,还要求诸如2x2之类的
  2. 在(1)中我们得到所有大小为1的矩形和横向的各种大小的矩形的情况,所以后面我们只需要求纵向的大小大于1的矩形的情况即可
  3. 所以在这我们选择向下"压缩"题目所给二维数组,以便继续统计,这里"压缩"的思路理解好下次遇到类似的题能想出来就行,来看图吧

起始的二维数组
image.png

对其使用“按位与”进行压缩
image.png

第二次统计的结果
image.png

思考:这里我们可以发现第二次的统计是2xn的,既包含了2X1的情况也包含了2x2的情况,接下来的步骤继续如上循环"压缩"即可

代码

###java

class Solution {
    public int numSubmat(int[][] mat) {
        int row = mat.length, col = mat[0].length, ans = 0;

        for (int i = 0; i < row; i++){
            //统计
            for (int j = i; j < row; j++){
                int now = 0;
                for (int k = 0; k < col; k++){
                    if (mat[j][k] == 0) now = 0;
                    else now = k == 0 ? mat[j][0] : now + 1;
                    ans += now;
                }
            }
            //压缩
            for (int j = row - 1; j > i; j--){
                for (int k = 0; k < col; k++){
                    mat[j][k] = mat[j][k] & mat[j - 1][k];
                }
            }
        }
        return ans;
    }
}

希望这篇题解能让你看懂,喜欢的留下你宝贵的会心一赞吧,刷题之路,我们绝不言败!ヾ(◍°∇°◍)ノ゙

5454. 统计全 1 子矩形

作者 linmiaomiao
2020年7月5日 12:10

解题思路

矩阵里每个点(i.j)统计他这行左边到他这个位置最多有几个连续的1,存为left[i][j]。然后对于每个点(i.j),我们固定子矩形的右下角为(i.j),利用left从该行i向上寻找子矩阵左上角为第k行的矩阵个数。每次将子矩阵个数加到答案中即可。
时间复杂度O(nnm),空间复杂度O(nm)。

代码

###cpp

class Solution {
public:
    int numSubmat(vector<vector<int>>& mat) {
        int n = mat.size();
        int m = mat[0].size();
        vector<vector<int> > left(n,vector<int>(m));
        int now = 0;
        for(int i=0;i<n;i++){
            now = 0;
            for(int j=0;j<m;j++){
                if(mat[i][j] == 1) now ++;
                else now = 0;
                left[i][j] = now;
            }
        }
        int ans = 0,minx;
        for(int i=0;i<n;i++){
            for(int j=0;j<m;j++){
                minx = 0x3f3f3f3f;
                for(int k=i;k>=0;k--){
                    minx = min(left[k][j],minx);
                    ans += minx;
                }
            }
        }
        return ans;
    }
};
❌
❌