[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fXgem2ng6r9RUgn3GTbcukfkWfyTdif_UC9r_lJy_Q2I":3,"donations-sidebar":9,"posts-{\"page\":1,\"pageSize\":10,\"categoryId\":\"4ea3d8af-9cc3-49bb-a9cd-34dbcdc3bd85\"}":10,"sidebar-data":109},{"id":4,"name":5,"slug":5,"description":5,"sortOrder":6,"createdAt":7,"updatedAt":8},"4ea3d8af-9cc3-49bb-a9cd-34dbcdc3bd85","构建工具",8,"2022-06-02T07:28:13.000Z","2023-02-08T02:49:14.000Z",[],{"list":11,"total":106,"page":107,"pageSize":108},[12,31,47,63,77,90],{"id":13,"title":14,"slug":14,"content":15,"excerpt":14,"coverImage":16,"status":17,"isPinned":18,"pinnedAt":19,"viewCount":20,"publishedAt":21,"createdAt":21,"updatedAt":22,"category":23,"author":24,"tags":27},"93413af1-748f-4ebf-8c7b-8ce0815d2faf","npm更新依赖","## 全局安装依赖 npm-check\n```language\nnpm install npm-check -g\n```\n\n## 查看可更新包信息\n```language\nnpm-check\n```\n\n## 选择并更新相应的依赖包【空格选择、enter更新】\n```language\nnpm-check -u\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F1f84cb07025b657c2ddc599cfa50984f.jpeg","PUBLISHED",false,null,717,"2022-08-20T10:53:00.000Z","2026-05-25T05:35:06.000Z",{"id":4,"name":5,"slug":5},{"id":25,"name":26,"avatar":19},"f9d0f2de-c700-4f90-b535-afd3dbe78128","Admin",[28],{"id":29,"name":30,"slug":30},"0f1cc678-40e4-44b1-b2cf-a6fd8a1c867a","npm",{"id":32,"title":33,"slug":34,"content":35,"excerpt":33,"coverImage":36,"status":17,"isPinned":18,"pinnedAt":19,"viewCount":37,"publishedAt":38,"createdAt":38,"updatedAt":39,"category":40,"author":41,"tags":42},"942a23eb-f73e-4d47-9945-23d75fb2c791","Docker入门到精通教程","docker入门到精通教程","\n\n\n## 为什么要用 Docker\n\n> Docker 属于 Linux 容器的一种封装，提供简单易用的容器使用接口。它是目前最流行的 Linux 容器解决方案。\n\n## 安装\n\n#### 安装 `yum-utils` 软件包\n\n```shell\nsudo yum install -y yum-utils\n\nsudo yum-config-manager \\\n    --add-repo \\\n    https:\u002F\u002Fdownload.docker.com\u002Flinux\u002Fcentos\u002Fdocker-ce.repo\n```\n\n#### 安装 DOCKER 引擎\n\n```shell\nsudo yum install docker-ce docker-ce-cli containerd.io\n```\n\n#### 启动 Docker\n\n```shell\nsudo systemctl start docker\n```\n\n### 验证是否安装成功\n\n```shell\ndocker version\n```\n\n#### 通过运行`hello-world` 映像来验证是否正确安装了 Docker Engine 。\n\n```shell\nsudo docker run hello-world\n```\n\n## 常用命令\n\n### 查看当前有些什么 images\n\n```shell\ndocker images\n```\n\n### 查看所有运行或者不运行容器\n\n```shell\ndocker ps -a\n```\n\n### 停止、启动、杀死、重启一个容器\n\n```shell\ndocker stop Name或者ID\n\ndocker start Name或者ID\n\ndocker kill Name或者ID\n\ndocker restart name或者ID\n```\n\n### 删除 images（镜像），通过 image 的 id 来指定删除谁\n\n```shell\ndocker rmi \u003Cimage id>\n```\n\n## CentOS 7 清理删除卸载 Docker 环境\n\n### 杀死所有运行容器\n\n```shell\ndocker kill $(docker ps -a -q)\n```\n\n### 删除所有容器\n\n```shell\ndocker rm $(docker ps -a -q)\n```\n\n### 删除所有镜像\n\n```shell\ndocker rmi $(docker images -q)\n```\n\n### 停止 docker 服务\n\n```shell\nsystemctl stop docker\n```\n\n### 删除存储目录\n\n```shell\nrm -rf \u002Fetc\u002Fdocker\nrm -rf \u002Frun\u002Fdocker\nrm -rf \u002Fvar\u002Flib\u002Fdockershim\nrm -rf \u002Fvar\u002Flib\u002Fdocker\n```\n\n### 如果发现删除不掉，需要先 umount，如\n\n```shell\numount \u002Fvar\u002Flib\u002Fdocker\u002Fdevicemapper\n```\n\n### 卸载 docker 查看已安装的 docker 包\n\n```shell\nyum list installed | grep docker\n```\n\n### 卸载相关包\n\n```shell\nyum remove docker-engine docker-engine-selinux.noarch\n```\n\n## docker-compose\n\n> 对于 Compose 来说，大部分命令的对象既可以是项目本身，也可以指定为项目中的服务或者容器。如果没有特别的说明，命令对象将是项目，这意味着项目中所有的服务都会受到命令影响。执行 `docker-compose [COMMAND] --help` 或者 `docker-compose help [COMMAND]` 可以查看具体某个命令的使用格式。\n\n`docker-compose` 命令的基本的使用格式是\n\n```shell\ndocker-compose [-f=\u003Carg>...] [options] [COMMAND] [ARGS...]\n```\n\n##### 2. 命令选项\n\n- `-f, --file FILE` 指定使用的 Compose 模板文件，默认为 `docker-compose.yml`，可以多次指定。\n- `-p, --project-name NAME` 指定项目名称，默认将使用所在目录名称作为项目名。\n- `--x-networking` 使用 Docker 的可拔插网络后端特性\n- `--x-network-driver DRIVER` 指定网络后端的驱动，默认为 `bridge`\n- `--verbose` 输出更多调试信息。\n- `-v, --version` 打印版本并退出。\n\n##### 3.命令使用说明\n\n##### `up`\n\n格式为 `docker-compose up [options] [SERVICE...]`。\n\n- 该命令十分强大，它将尝试自动完成包括构建镜像，（重新）创建服务，启动服务，并关联服务相关容器的一系列操作。\n\n- 链接的服务都将会被自动启动，除非已经处于运行状态。\n\n- 可以说，大部分时候都可以直接通过该命令来启动一个项目。\n\n- 默认情况，`docker-compose up` 启动的容器都在前台，控制台将会同时打印所有容器的输出信息，可以很方便进行调试。\n\n- 当通过 `Ctrl-C` 停止命令时，所有容器将会停止。\n\n- 如果使用 `docker-compose up -d`，将会在后台启动并运行所有的容器。一般推荐生产环境下使用该选项。\n\n- 默认情况，如果服务容器已经存在，`docker-compose up` 将会尝试停止容器，然后重新创建（保持使用 `volumes-from` 挂载的卷），以保证新启动的服务匹配 `docker-compose.yml` 文件的最新内容\n\n---\n\n##### `down`\n\n- 此命令将会停止 `up` 命令所启动的容器，并移除网络\n\n---\n\n##### `exec`\n\n- 进入指定的容器。\n\n---\n\n##### `ps`\n\n格式为 `docker-compose ps [options] [SERVICE...]`。\n\n列出项目中目前的所有容器。\n\n选项：\n\n- `-q` 只打印容器的 ID 信息。\n\n---\n\n##### `restart`\n\n格式为 `docker-compose restart [options] [SERVICE...]`。\n\n重启项目中的服务。\n\n选项：\n\n- `-t, --timeout TIMEOUT` 指定重启前停止容器的超时（默认为 10 秒）。\n\n---\n\n##### `rm`\n\n格式为 `docker-compose rm [options] [SERVICE...]`。\n\n删除所有（停止状态的）服务容器。推荐先执行 `docker-compose stop` 命令来停止容器。\n\n选项：\n\n- `-f, --force` 强制直接删除，包括非停止状态的容器。一般尽量不要使用该选项。\n- `-v` 删除容器所挂载的数据卷。\n\n---\n\n##### `start`\n\n格式为 `docker-compose start [SERVICE...]`。\n\n启动已经存在的服务容器。\n\n---\n\n##### `stop`\n\n格式为 `docker-compose stop [options] [SERVICE...]`。\n\n停止已经处于运行状态的容器，但不删除它。通过 `docker-compose start` 可以再次启动这些容器。\n\n选项：\n\n- `-t, --timeout TIMEOUT` 停止容器时候的超时（默认为 10 秒）。\n\n---\n\n##### `top`\n\n查看各个服务容器内运行的进程。\n\n---\n\n##### `unpause`\n\n格式为 `docker-compose unpause [SERVICE...]`。\n\n恢复处于暂停状态中的服务。\n\n---\n\n```shell\ndocker-compose up\n```\n\n## docker-compose.yml参考文件\n\n```yml\nversion: '3.7'\n\n\nservices:\n    jenkins:\n        image: jenkins\u002Fjenkins:lts\n        container_name: jenkins\n        environment:\n            - TZ=Asia\u002FShanghai\n        volumes:\n            - \u002Fwww\u002Fwwwroot\u002F:\u002Fvar\u002Fjenkins_home\u002Fworkspace\u002F\n            - \u002Fvar\u002Frun\u002Fdocker.sock:\u002Fvar\u002Frun\u002Fdocker.sock\n            - \u002Fusr\u002Fbin\u002Fdocker:\u002Fusr\u002Fbin\u002Fdocker\n            - \u002Fusr\u002Flib\u002Fx86_64-linux-gnu\u002Flibltdl.so.7:\u002Fusr\u002Flib\u002Fx86_64-linux-gnu\u002Flibltdl.so.7\n        ports:\n            - \\\"8080:8080\\\"\n        expose:\n            - \\\"8080\\\"\n            - \\\"50000\\\"\n        privileged: true\n        user: root\n        restart: always\n\n    bt:\n       image: pch18\u002Fbaota\n       container_name: bt\n       volumes: \n            - \u002Fwww\u002Fwwwroot:\u002Fwww\u002Fwwwroot\n            - .\u002Fbaota\u002Flogs:\u002Fwww\u002Fwwwlogs\n       restart: unless-stopped\n       ports: \n            - \\\"80:80\\\"\n            - \\\"443:443\\\"\n            - \\\"8888:8888\\\"\n            - \\\"888:888\\\"\n            - \\\"3306:3306\\\"\n```\n\n## 部署 Portiner 容器\n\n### 拉取镜像\n\n```\ndocker search portainer\n```\n\n### 拉取镜像\n\n```\ndocker pull docker.io\u002Fportainer\u002Fportainer\n```\n\n```shell\ndocker volume create portainer_data\n\ndocker run -d -p 8000:8000 -p 9000:9000 --name=portainer --restart=always -v \u002Fvar\u002Frun\u002Fdocker.sock:\u002Fvar\u002Frun\u002Fdocker.sock -v portainer_data:\u002Fdata portainer\u002Fportainer\n```\n\n- 挂载本地：    -v \u002Fvar\u002Frun\u002Fdocker.sock:\u002Fvar\u002Frun\u002Fdocker.sock\n- 与容器的连接：  \u002Fvar\u002Frun\u002Fdocker.socker\n- 数据持久化：   -v portainer_data:\u002Fdata\n\n### 访问地址：\n\n```js\nip:9000\n```","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F1657982139209.png",929,"2022-07-16T14:36:02.000Z","2026-05-25T03:44:32.000Z",{"id":4,"name":5,"slug":5},{"id":25,"name":26,"avatar":19},[43],{"id":44,"name":45,"slug":46},"78a62bff-ff77-4878-8c25-3e6aae18c668","Docker","docker",{"id":48,"title":49,"slug":50,"content":51,"excerpt":49,"coverImage":52,"status":17,"isPinned":18,"pinnedAt":19,"viewCount":53,"publishedAt":54,"createdAt":54,"updatedAt":55,"category":56,"author":57,"tags":58},"df40d966-ea9f-4a04-b99d-b0b65fe02153","Webpack常用插件","webpack常用插件","\n## 压缩图片\n\n## 安装命令\n\n```shell\ncnpm i -D image-webpack-loader\n```\n\n## webpack.config.js\n\n```js\n    module: {\n        rules: [\n            \u002F\u002F图片文件\n            {\n                test: \u002F\\.(bmp|png|jpg|jpeg|ico|gif|webp|svg)$\u002F,\n                use: [{\n                        loader: 'file-loader',\n                        options: {\n                            limit: 1, \u002F\u002F 当图片小于1 k时 自动用base64转换\n                            name: '[name].[ext]',\n                            \u002F\u002F 图片输出的实际路径(相对于dist)\n                            outputPath: 'images',\n                            publicPath: '.\u002Fimages'\n                            \u002F\u002FpublicPath: 'https:\u002F\u002Fcdn2020.xiaolong0418.com\u002Fnavigation\u002Fimages'\n                        }\n                    },\n                    \u002F*对图片进行压缩*\u002F\n                    {\n                        loader: 'image-webpack-loader',\n                        options: {\n                            \u002F\u002F 压缩 jpeg 的配置\n                            mozjpeg: {\n                                progressive: true,\n                                quality: 65\n                            },\n                            \u002F\u002F 使用 imagemin-optipng 压缩 png，enabled: false 为关闭\n                            optipng: {\n                                enabled: false,\n                            },\n                            \u002F\u002F 使用 imagemin-pngquant 压缩 png\n                            pngquant: {\n                                quality: [0.65, 0.90],\n                                speed: 4\n                            },\n                            \u002F\u002F 压缩 gif 的配置\n                            gifsicle: {\n                                interlaced: false,\n                            },\n                            \u002F\u002F 开启 webp，会把 jpg 和 png 图片压缩为 webp 格式,建议不要开启，ios14以下系统不支持webp\n                            \u002F\u002F webp: {\n                            \u002F\u002F   quality: 75\n                            \u002F\u002F }\n                        }\n                    }\n                ]\n            }\n        ]\n    },\n```\n\n## vue.config.js\n\n```js\nmodule.exports = {\n  chainWebpack: (config) => {\n    if (IS_PROD) {\n      config.module\n        .rule('images')\n        .use('image-webpack-loader')\n        .loader('image-webpack-loader')\n        .options({\n          mozjpeg: { progressive: true, quality: 65 },\n          optipng: { enabled: false },\n          pngquant: { quality: [0.65, 0.9], speed: 4 },\n          gifsicle: { interlaced: false },\n          \u002F\u002F webp: { quality: 75 }\n        });\n    }\n  },\n};\n```\n\n## 常见问题\n\n#### 1、缺少依赖\n\n> 使用 cnpm 安装依赖\n\n#### 2、在某些版本的 OSX 上安装可能会因 libpng 依赖项而引发错误。可以通过安装最新版本的 libpng 来解决。\n\n```shell\nbrew install libpng\n```\n\n\n## 自动打包成zip压缩包\n## 安装命令\n\n```shell\nnpm i filemanager-webpack-plugin -D\n```\n\n## webpack.config.js\n\n```js\nconst FileManagerPlugin = require('filemanager-webpack-plugin');\nmodule: {\n        ...\n    \u002F\u002F 打包生成dist.zip\n    new FileManagerPlugin({\n      events: {\n        onEnd: {\n          delete: [\\\".\u002Fdist\u002Fdist.zip\\\"],\n          archive: [{ source: \\\".\u002Fdist\\\", destination: \\\".\u002Fdist\u002Fdist.zip\\\" }],\n        },\n      },\n    }),\n},\n```\n\n## 自动添加版权\n## webpack.config.js\n\n```js\nconst webpack = require('webpack');\n\nmodule.exports = {\n  \u002F\u002F添加版权\n  plugins: [\n    new webpack.BannerPlugin(\n      '最终版权归`浪里小白龙`所有,如有需求，请联系微信：xiaobailong0418'\n    ),\n  ],\n};\n```\n\n## vue.config.js\n\n```js\nconst webpack = require('webpack');\n\nmodule.exports = {\n  \u002F\u002F添加版权\n  configureWebpack: (config) => {\n    if (process.env.NODE_ENV !== 'production') return;\n    return {\n      plugins: [\n        new webpack.BannerPlugin(\n          '最终版权归`浪里小白龙`所有,如有需求，请联系微信：xiaobailong0418'\n        ),\n      ],\n    };\n  },\n}\n\n\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F1657981823581.png",650,"2022-07-16T14:31:48.000Z","2026-06-27T11:00:45.395Z",{"id":4,"name":5,"slug":5},{"id":25,"name":26,"avatar":19},[59],{"id":60,"name":61,"slug":62},"5782dff5-2ea2-4427-9696-d4363a7fd5bc","Webpack","webpack",{"id":64,"title":65,"slug":65,"content":66,"excerpt":65,"coverImage":67,"status":17,"isPinned":18,"pinnedAt":19,"viewCount":68,"publishedAt":69,"createdAt":69,"updatedAt":70,"category":71,"author":72,"tags":73},"4882bad9-06d0-4bf8-ac7f-6a14601ce875","项目规范化","\n\n## ESLint介绍\n\n- 最为流行的JavaScript lint 工具监测JS代码质量\n- ESLint很容易统一开发者的编码风格\n- ESLint可以帮助开发者提升编码能力\n\n### 初始化项目\n\n```shell\nnpm init --yes\n```\n\n### 安装ESLint依赖\n\n```shell\nnpm i eslint -D\n```\n\n### 查看版本\n\n```shell\nnpx eslint -v\n```\n\n初始化ESLint(选3、1)\n\n```\nnpx eslint --init\n```\n\n自动修复部分警告\n\n```\nnpx eslint .\\01.js --fix\n```\n\n### 事实证明，上面都是新手上路学习的，小白龙直接带你上高速\n\n## 目录结构\n\n```json\n|-- 项目名称\n    |-- .editorconfig             \u002F\u002F编辑器配置文件\n    |-- .eslintignore             \u002F\u002Feslint忽略文件\n    |-- .eslintrc.js              \u002F\u002Feslint配置文件\n    |-- .gitattributes            \u002F\u002Feslint配置文件\n    |-- .gitignore                \u002F\u002Fgit忽略文件\n    |-- .prettierignore           \u002F\u002Fprettier忽略文件\n    |-- .prettierrc.js            \u002F\u002Fprettier配置文件\n    |-- .stylelintignore          \u002F\u002Fstylelint忽略文件\n    |-- .stylelintrc.js           \u002F\u002Fstylelint配置文件\n    |-- .vscode\n    |   |-- settings.json         \u002F\u002Fvscode编辑器配置文件\n```\n\n\n\n### vscode配置\n\n|        插件         |    功能     |                             图片                             |\n| :-----------------: | :---------: | :----------------------------------------------------------: |\n|        Vetur        | 识别vue文件 | ![01](https:\u002F\u002Fcdn.xiaolong0418.com\u002Fblog\u002FSpecification\u002Fimg\u002F04.png) |\n|      Prettier       | 格式化代码  | ![01](https:\u002F\u002Fcdn.xiaolong0418.com\u002Fblog\u002FSpecification\u002Fimg\u002F02.png) |\n|       ESLint        | 规范js代码  | ![01](https:\u002F\u002Fcdn.xiaolong0418.com\u002Fblog\u002FSpecification\u002Fimg\u002F01.png) |\n| StyleLint   v0.87.6 | 规范css代码 | ![01](https:\u002F\u002Fcdn.xiaolong0418.com\u002Fblog\u002FSpecification\u002Fimg\u002F03.png) |\n\n\n\n>  在项目根目录，新建.vscode 配置settings.json\n\n```json\n{\n  \u002F\u002F根据文件后缀名定义vue文件类型\n  \\\"files.associations\\\": {\n    \\\"*.vue\\\": \\\"vue\\\"\n  },\n  \u002F\u002F配置 ESLint 检查的文件类型\n  \\\"eslint.validate\\\": [\\\"javascript\\\", \\\"javascriptreact\\\", \\\"html\\\", \\\"vue\\\"],\n  \u002F\u002F保存时\n  \\\"editor.codeActionsOnSave\\\": {\n    \u002F\u002Feslint自动修复js错误\n    \\\"source.fixAll.eslint\\\": true,\n    \u002F\u002FStyleLint自动修复css错误\n    \\\"source.fixAll.stylelint\\\": true\n  },\n  \\\"editor.formatOnSave\\\": true,\n\n  \\\"[vue]\\\": {\n    \\\"editor.defaultFormatter\\\": \\\"esbenp.prettier-vscode\\\"\n  },\n  \\\"[javascript]\\\": {\n    \\\"editor.defaultFormatter\\\": \\\"esbenp.prettier-vscode\\\"\n  },\n  \\\"[json]\\\": {\n    \\\"editor.defaultFormatter\\\": \\\"esbenp.prettier-vscode\\\"\n  },\n  \\\"[jsonc]\\\": {\n    \\\"editor.defaultFormatter\\\": \\\"esbenp.prettier-vscode\\\"\n  }\n}\n```\n\n### 在 package.json文件的 scripts加上命令, 规则检查自动修复js\n\n\n\n|          依赖          |           说明           |\n| :--------------------: | :----------------------: |\n|         eslint         |           检查           |\n| eslint-config-standard |           规范           |\n| eslint-config-prettier | 解决eslint和prettier冲突 |\n\n\n\n>  可以先用eslint初始化出.eslintrc.js，然后再安装eslint-config-standard和eslint-config-prettier(走高速的，可以忽略这段话)\n\n```shell\nnpm i -D   babel-eslint eslint-plugin-import eslint-plugin-node eslint-plugin-promise eslint-plugin-vue eslint-config-prettier\n```\n\n```js\nmodule.exports = {\n  env: {\n    browser: true,\n    es2021: true,\n    node: true,\n  },\n  extends: [\\\"plugin:vue\u002Frecommended\\\", \\\"standard\\\", \\\"prettier\\\"],\n  parserOptions: {\n    ecmaVersion: 12,\n    sourceType: \\\"module\\\",\n  },\n  plugins: [\\\"vue\\\"],\n  \u002F\u002F 设置全局变量,根据自己项目需求\n  globals: {\n    _lang: true,\n    APP: true,\n    JSInterface: true,\n    finishSign: true,\n    __area: true,\n    __lang: true,\n    _host: true,\n    _app: true,\n    _area: true,\n    hosts: true,\n    _hash: true,\n    _env: true,\n    _test: true,\n    _images: true,\n    assetsRetry: true,\n    AdjustDeepLink: true,\n  },\n  rules: {\n    \u002F\u002F 禁止校验v-html xss风险\n    \\\"vue\u002Fno-v-html\\\": 0,\n  },\n};\n```\n\n### 在 package.json文件的 scripts加上命令,lint为修复js错误，style为修复css错误\n\n```json\n\\\"lint\\\": \\\"eslint --ext .js,.vue src --fix   stylelint *.scss,css,.vue\\\",\n\n\\\"style\\\": \\\"stylelint 'src\u002F**\u002F*.(vue|scss|css)' --fix\\\"\n```\n\n### webpack配置\n\n```js\n{\n test: \u002F\\.(js|vue)$\u002F,\n use: {\n  loader: \\\"babel-eslint\\\",\n },\n enforce: \\\"pre\\\", \u002F\u002F 编译前检查\n include: [resolve(\\\"src\\\")],\n},\n```\n\n\n\n# stylelint 介绍\n\n- 强大的现代 linter，可帮助您避免错误并强制执行样式中的约定。\n- 了解最新的 CSS 语法，包括自定义属性和 4 级选择器\n- 从 HTML、markdown 和 CSS-in-JS 对象和模板文本中提取嵌入的样式\n- 解析类似 CSS 的语法，如 SCSS、Sass、Less 和 SugarSS\n- 有超过170 个内置规则来捕获错误、应用限制和强制执行风格约定\n- 支持插件，因此您可以创建自己的规则或使用社区编写的插件\n- 自动修复大多数风格违规\n\n\n\n### 安装\n\n|              依赖               |   功能   |\n| :-----------------------------: | :------: |\n|            stylelint            |   检查   |\n|    stylelint-config-standard    |   规范   |\n| stylelint-config-rational-order | 书写顺序 |\n|    stylelint-config-prettier    | 解决冲突 |\n\n\n\n```shell\nnpm i -D stylelint@13.13.1 stylelint-config-standard@22.0.0 stylelint-config-rational-order@0.1.2 stylelint-config-prettier@8.0.2 stylelint-scss@3.21.0\n```\n\n\n\n### 根目录创建 stylelint.config.js 文件\n\n```js\nmodule.exports = {\n  extends: [\n    \\\"stylelint-config-standard\\\",\n    \\\"stylelint-config-rational-order\\\",\n    \\\"stylelint-config-prettier\\\",\n  ],\n  plugins: [\\\"stylelint-scss\\\"],\n  \u002F\u002F, \\\"stylelint-prettier\\\"\n  rules: {\n    \u002F\u002F在字符串周围指定单引号或双引号。\n    \u002F\u002F\\\"string-quotes\\\": \\\"single\\\",\n    \\\"property-no-unknown\\\": [\n      true,\n      {\n        ignoreProperties: [\\\"composes\\\"],\n      },\n    ],\n    \\\"selector-pseudo-class-no-unknown\\\": [\n      true,\n      {\n        ignorePseudoClasses: [\\\"global\\\"],\n      },\n    ],\n    \\\"at-rule-no-unknown\\\": null,\n    \\\"scss\u002Fat-rule-no-unknown\\\": null,\n  },\n};\n```\n\n\n\n### 忽略stylelint对css的检验\n\n> 忽略整个文件，在首行加入 `\u002F* stylelint-disable *\u002F`\n\n```css\n\u002F* stylelint-disable *\u002F\nhtml {}\n```\n\n> 忽略多行\n\n```css\n\u002F* stylelint-disable *\u002F\nhtml {}\n.div {\n    color: red;\n}\n\u002F* stylelint-enable *\u002F\n```\n\n> 忽略一行， 在样式前加入 \u002F* stylelint-disable-next-line *\u002F 以忽略该行\n\n```css\n#id {\n  \u002F* stylelint-disable-next-line *\u002F\n  color: pink !important;\n}\n```\n\n> 在 .stylelintrc.json 內设定需要忽略的文件\n\n```sh\n{\n  ignoreFiles: [\\\"dist\u002F**\u002F*\\\", \\\"src\u002Fassets\u002Fscss\u002Fabc.scss\\\"]\n}\n```\n\n\n\n## husky + lint-staged 构建代码工作流\n\n- husky是一个 Git Hook 工具,它可以在代码提交前允许我们做一些事情，从而防止一些不好的代码被提交上去。\n\n- lint-staged是针对工作区修改的文件,这对我们只希望处理将要提交的文件将会非常有用。\n\n### 安装 husky lint-staged 依赖\n\n> 注意 windows 用户需要使用 npm 包管理器安装不然 git hooks会失效\n\n```shell\nyarn add -D husky lint-staged\n```\n\n\n\n### 配置 husky & lint-staged\n\n- 我们需要在代码提交前对代码做一下格式化并且如果代码不符合规范就不让提交,简单的做法就是在`husky`的`pre-commit`钩子去运行 `lint-staged`,`lintstaged `主要就干了三件事：\n\n- 第一件就是调用`eslint --ext .js,.vue src --fix` 修复不合符eslint规范的代码。\n\n- 第二件`stylelint 'src\u002F**\u002F*.(vue|scss|css)' --fix`修复不合符css规范的代码。\n\n- 最后如果都通过了就允许代码`commit`。\n\n```json\n  \\\"husky\\\": {\n    \\\"hooks\\\": {\n      \\\"pre-commit\\\": \\\"lint-staged\\\"\n    }\n  },\n  \\\"lint-staged\\\": {\n    \\\"*{.ts,.js}\\\":[\n      \\\"eslint --ext .js,.vue src --fix\\\",\n      \\\"stylelint 'src\u002F**\u002F*.(vue|scss|css)' --fix\\\",\n      \\\"git add\\\"\n    ]\n  }\n```\n\n\n### 2022\u002F8\u002F27加强版本\n\n### 安装husky\n```shell\npnpm add husky -D\n```\n\n### 快速创建husky目录\n```shell\nnpx --no-install husky install\n```\n> --no-install 参数表示强制 npx 使用项目中 node_modules 目录下的husky依赖包\n\n\n### 在.hucky文件夹中创建pre-commit文件\n```js\n#!\u002Fbin\u002Fsh\n. \\\"$(dirname \\\"$0\\\")\u002F_\u002Fhusky.sh\\\"\n\nnpm run lint\n```\n> pre-commit 在 commit 之前会执行 npm run lint 校验代码，可以定义你的执行脚本，校验不通过不允许 commit 提交\n\n### commitizen\n> commitizen 是一个撰写符合 Commit Message 标准的一款工具。通过它可以实现交互式撰写规范的 Commit Message。\n```shell\npnpm  add commitizen -D\n```\n> 安装完成后，一般我们都采用符合 Angular 的 Commit message 格式的提交规范，运行以下命令生成符合 Angular 提交规范格式的 Commit message\n\n```shell\nnpx --no-install commitizen init cz-conventional-changelog --save-dev --save-exact\n```\n\n### 在 package.json scrips 添加 \\\"commit\\\": \\\"git-cz\\\" 命令\n\n```language\nscripts: {\n  \\\"commit\\\": \\\"git add .\u002F && git-cz\\\"\n}\n```\n\n### 限制 commitlint\n\n> 由于 commitizen 并不是强制使用的，仍然可以通过 git commit 来提交，所以不管是 git-cz 还是 git commit 提交前，都要对 commit messag 进行一次校验，不符合规范的情况下是不允许进行 commit\n```shell\npnpm add @commitlint\u002Fcli @commitlint\u002Fconfig-conventional -D\n```\n\n### 在.hucky文件夹中创建commit-msg文件\n```js\n#!\u002Fbin\u002Fsh\n. \\\"$(dirname \\\"$0\\\")\u002F_\u002Fhusky.sh\\\"\n\nnpx --no-install commitlint --edit $1\n```\n\n### 根目录创建配置文件 commitlint.config.js\n```js\nmodule.exports = {\n  ignores: [(commit) => commit.includes('init')],\n  extends: ['@commitlint\u002Fconfig-conventional', 'cz'],\n  rules: {\n    'body-leading-blank': [2, 'always'],\n    'footer-leading-blank': [1, 'always'],\n    'header-max-length': [2, 'always', 108],\n    'subject-empty': [2, 'never'],\n    'type-empty': [2, 'never'],\n    'subject-case': [0],\n    'type-enum': [\n      2,\n      'always',\n      [\n        'feat',\n        'fix',\n        'perf',\n        'style',\n        'docs',\n        'test',\n        'refactor',\n        'build',\n        'ci',\n        'chore',\n        'revert',\n        'wip',\n        'workflow',\n        'types',\n        'release'\n      ]\n    ]\n  }\n}\n```\n\n### 上面的提示都是英文的，如果想自定义翻译成中文，需要安装 cz-customizable 来实现自定义 commit message 规则，以及安装对应的 commitlint-config-cz 来配套校验\n```shell\npnpm add cz-customizable  commitlint-config-cz -D\n```\n\n### 项目根目录，创建一个 .cz-config.js 文件\n```js\n\u002F\u002F .cz-config.js\nmodule.exports = {\n  types: [\n    { value: ':sparkles: feat', name: '✨ feat: 一项新功能' },\n    { value: ':bug: fix', name: '🐛 fix: 修复一个Bug' },\n    { value: ':memo: docs', name: '📝 docs: 文档变更' },\n    { value: ':lipstick: style', name: '💄 style: 代码风格，格式修复' },\n    { value: ':recycle: refactor', name: '♻️ refactor: 代码重构，注意和feat、fix区分开' },\n    { value: ':zap: perf', name: '⚡️ perf: 代码优化,改善性能' },\n    { value: ':white_check_mark: test', name: '✅ test: 测试' },\n    { value: ':rocket: chore', name: '🚀 chore: 变更构建流程或辅助工具' },\n    { value: ':rewind: revert', name: ':rewind: revert: 代码回退' },\n    { value: ':tada: init', name: '🎉 init: 项目初始化' },\n    { value: ':construction_worker: ci', name: '👷 对CI配置文件和脚本的更改' },\n    { value: ':package: build', name: '📦️ build: 变更项目构建或外部依赖' },\n    { value: ':construction: WIP', name: '🚧 WIP: 进行中的工作' }\n  ],\n  scopes: [\n    { name: 'component' },\n    { name: 'config' },\n    { name: 'docs' },\n    { name: 'src' },\n    { name: 'examples' },\n    { name: 'play' }\n  ],\n  \u002F\u002F allowTicketNumber: false,\n  \u002F\u002F isTicketNumberRequired: false,\n  \u002F\u002F ticketNumberPrefix: 'TICKET-',\n  \u002F\u002F ticketNumberRegExp: '\\\\d{1,5}',\n  \u002F\u002F it needs to match the value for field type. Eg.: 'fix'\n  \u002F\u002F scopeOverrides: {\n  \u002F\u002F   feat: [\n  \u002F\u002F     { name: 'element' }\n  \u002F\u002F   ],\n  \u002F\u002F   fix: [\n  \u002F\u002F     { name: 'element' },\n  \u002F\u002F     { name: 'style' },\n  \u002F\u002F   ]\n  \u002F\u002F },\n  \u002F\u002F override the messages, defaults are as follows\n  messages: {\n    type: '请选择提交类型(必填):',\n    scope: '请选择一个scope (可选):',\n    customScope: '请输入文件修改范围(可选):',\n    \u002F\u002F used if allowCustomScopes is true\n    subject: '请简要描述提交(必填):',\n    body: '请输入详细描述，使用\\\"|\\\"换行(可选):\\\n',\n    breaking: '列出任务非兼容性说明 (可选):\\\n',\n    footer: '请输入要关闭的issue，例如：#12, #34(可选):\\\n',\n    confirmCommit: '确定提交此说明吗？'\n  },\n  allowCustomScopes: true,\n  allowBreakingChanges: ['feat', 'fix'],\n  \u002F\u002F 限制 subject 长度\n  subjectLimit: 72\n  \u002F\u002F 跳过问题 skip any questions you want\n  \u002F\u002F skipQuestions: ['body', 'footer'],\n  \u002F\u002F breaklineChar: '|', \u002F\u002F It is supported for fields body and footer.\n  \u002F\u002F footerPrefix : 'ISSUES CLOSED:'\n  \u002F\u002F askForBreakingChangeFirst : true, \u002F\u002F default is false\n}\n```\n> 创建完 .cz-config.js 文件后，我们需要回到 package.json 文件中，将 config.commitizen.path 更改为 node_modules\u002Fcz-customizable\n```js\n{\n  \\\"config\\\": {\n    \\\"commitizen\\\": {\n      \\\"path\\\": \\\"node_modules\u002Fcz-customizable\\\"\n    }\n  }\n}\n```\n\n### 为了提交更好看，在 commit 头部添加了表情 gitmoji，需要安装这个插件\n\n```shell\npnpm add commitlint-config-gitmoji -D\n```\n\n### 修改 .commitlintrc.js 内容\n```js\nmodule.exports = {\n  \u002F\u002F extends: ['@commitlint\u002Fconfig-conventional', 'cz'],\n  extends: ['gitmoji']\n}\n```\n\n### 执行 pnpm commit 可以看到带表情的中文 commit message\n\n\n\n\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F1657981737234.png",962,"2022-07-16T14:29:11.000Z","2026-06-27T11:07:20.965Z",{"id":4,"name":5,"slug":5},{"id":25,"name":26,"avatar":19},[74],{"id":75,"name":76,"slug":76},"be7b10bc-49eb-4a03-bea7-ceb915d500fe","规范",{"id":78,"title":79,"slug":80,"content":81,"excerpt":79,"coverImage":82,"status":17,"isPinned":18,"pinnedAt":19,"viewCount":83,"publishedAt":84,"createdAt":84,"updatedAt":85,"category":86,"author":87,"tags":88},"8ffa54be-4291-4550-ab4a-9cee975d8c3e","Webpack 中的 loader 与 plugin","webpack-中的-loader-与-plugin","\n---\n\n## webpack 中的 loader 与 plugin\n\n> 提到 webpack，自然离不开 loader 与 blugin。Webpack 就像一条生产线，要经过一系列处理流程后才能将源文件转换成输出结果（loader）。这条生产线上的每个处理流程的职责都是单一的，多个流程之间有存在依赖关系，只有完成当前处理后才能交给下一个流程去处理。而插件（plugin）就像是一个插入到生产线中的一个功能，在特定的时机对生产线上的资源做处理。\n\n- Loader：用于对模块源码的转换，loader描述了webpack如何处理非javascript模块，并且在build中引入这些依赖。loader可以将文件从不同的语言（如TypeScript）转换为JavaScript。\n- Plugin：目的在于解决loader无法实现的其他事，从打包优化和压缩，到重新定义环境变量，功能强大到可以用来处理各种各样的任务。\n\n> 简而言之，loader可以理解成webpack的横向广度，有了loader，webpack才可以打包处理各种的扩展语言。而plugin可以理解为webpack的纵向深度，在生命周期内注入不同的插件来扩展更多的能力。\n\n## Loader 的原理与实现\n\n> Loader 就像是一个翻译官，每个 loader 可以把源资源转换成新的结果输出并传递给下一个 loader ，但是最后一个 Loader 必须返回 JavaScript （浏览器只能运行js代码，不支持其他扩展语言）。\n\n以处理less文件为例：  \n\n```js\nmodule:{ \n    rules: [  \n        {    \n            test: \u002F\\.less$\u002F,    \n            use: ['style-loader', 'css-loader', 'less-loader']  \n        } \n    ]\n}\n```\n\n- less-loader： 将 less 源代码转化为 css\n- css-loader：处理 less-loader 输出的 css，找出 css 中依赖的资源（@import 等），压缩资源\n- sytle-loader：处理 css-loader 输出的 css，把 css 转换成脚本加载的 js 代码插入到 DOM 中\n\n至于 loader 的解析顺序为什么是**从右向左**的，原因其实只是Webpack选择了compose函数式变成方式，而不是pipe的方式而已。\n\n\n## 如何实现一个loader呢?\n\n> 有一天我们想把项目 txt 文件中的蒋梨花全部替换为梨花酱，这个时候就可以通过loader来解决\n\n#### 1、 在config.js中配置项目中 .txt 结尾的文件使用我们的 demo-loader\n\n```js\n\u002F\u002F webpack.config.js\nmodule:{  \n    rules: [    \n        {        \n            test: \u002F\\.txt$\u002F,        \n            use: ['demo-loader'],        \n            options: {            \n                name: '梨花酱' \u002F\u002F 将要变更的通过配置项传入        \n            }      \n        }  \n    ]\n}\n```\n\n#### 2、 创建一个包含蒋梨花的txt文件，并引用（webpack不会处理未引用的文件）\n\n```\n\u002F\u002F test.txt\n你好，我是蒋梨花\n\u002F\u002F app.js (入口文件引用)\nconst text = require(.\u002Ftext.txt)\nconsole.log(test)\n```\n\n#### 3、  编写 loader\n\n```js\n\u002F\u002F demo-loader.js\nconst loaderUtils = require('loader-utils') \n\u002F\u002F 接收options配置\nmodule.exports = function(source) {    \n    const options = loaderUtils.getOptions(this)    \n    source = source.replace(\u002F蒋梨花\u002Fg, options.name)    \n    return `module.exports = ${JSON.stringify(sorce)}`    \n    \u002F\u002F 最终需要返回一段可执行的js脚本\n}\n```\n\n### 4) 执行一下，就会发现 txt 文件中的蒋梨花已经被替换为梨花酱了~\n\nTips: 在实现一个 loader 的时候，要牢记几个原则哦：**单一职责；链式组合；模块化；无状态**\n\n## Plugin 的原理与实现\n\n> plugin是运行在webpak打包过程中的某段逻辑，它主要的作用是根据webpack提供的一些hooks来进行一些额外的操作，使 webpack 更加灵活扩展。\n\n```js\nplugins: [    \n    new HtmlWebpackPlugin()  \n]\n```\n\n我们通过 new 来使用这个插件，可以看出插件的本质是一个构造函数。\n\n首先了解两个概念：Compiler 和Compilation\n\n> compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立，并配置好所有可操作的设置，包括 options，loader 和 plugin。当在 webpack 环境中应用一个插件时，插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。\n\n> compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时，每当检测到一个文件变化，就会创建一个新的 compilation，从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调，以供插件做自定义处理时选择使用。\n\n在 webpack 启动后，它会执行 new xxxPlugin(options) 来初始化插件实例。在初始化对象后，会去调用 xxxPlugin.apply(compiler) 并传入 compiler 对象。插件获得 compiler 对象后，可以通过\n\ncompiler.plugin('事件名', 回调函数) 的方式进行监听 webpack 广播出来的事件了。\n\n- 那么具体如何实现一个 plugin 呢？\n\n让我们来实现一个在文件中添加一段文字的功能插件：\n\n1） 在配置文件中，使用插件\n\n```js\n\u002F\u002F webpack.config.js   \nplugins: [    \n  new MyTestPlugin({       \n    msg: '你好我是小白龙' \u002F\u002F 传入的插件配置    \n  })\n]\n```\n\n2）编写 plugin 插件\n\n```js\n\u002F\u002F MyTestPlugin.js\nconst { ConcatSource } = require(\\\"webpack-sources\\\") \u002F\u002F 用来写入\nclass MyBannerPlugin {  \n    constructor(options) { \n        \u002F\u002F 获取传入的option信息    \n        this.msg = options.msg  \n    },  \u002F\u002F 我们需要一个apply方法(为了获取compiler)，接收compiler作为参数表示这次打包的上下文。  \n    apply (compiler) {    \n        const msg = this. msg    \u002F\u002F 指定挂载的 webpack 钩子函数    \n        \u002F\u002F 使用compiler钩子compilation，即编译（compilation）创建之后，执行插件。    \n        compiler.hooks.compilation.tap(\\\"MyTestPlugin\\\", compilation => {      \n        \u002F\u002F compilation的 optimizeChunkAssets 钩子，可以利用这个钩子实现为每个文件插入信息      \n            compilation.hooks.optimizeChunkAssets.tap(\\\"MyTestPlugin\\\", chunks => {        \n                for (const chunk of chunks) {          \n                    for (const file of chunk.files) {            \n                        compilation.updateAsset(file, old => {                       \n                            return new ConcatSource(msg,\\\"\\\n\\\", old);            \n                        });          \n                    }        \n                }      \n            })    \n        })  \n    }\n}\nmodule.exports = MyTestPlugin\n```\n\n可以看出，要实现一个plugin需要以下几步：\n\n- 首先需要声明一个 class 构造函数\n- 在class里面定义一个apply方法，接收compiler作为参数表示这次打包的上下文。\n- 指定挂载的webpack事件钩子\n- 处理webpack内部实例的特定数据\n- 功能完成后调用webpack提供的回调\n\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F1657981578309.png",808,"2022-07-16T14:27:56.000Z","2026-06-27T11:00:19.112Z",{"id":4,"name":5,"slug":5},{"id":25,"name":26,"avatar":19},[89],{"id":60,"name":61,"slug":62},{"id":91,"title":92,"slug":93,"content":94,"excerpt":92,"coverImage":95,"status":17,"isPinned":18,"pinnedAt":19,"viewCount":96,"publishedAt":97,"createdAt":97,"updatedAt":98,"category":99,"author":100,"tags":101},"017a769c-8df4-4ef7-b485-16558fdab198","Vite快速入门教程","vite快速入门教程","\n\n\n## 完整配置(vite.config.js)\n\n```js\nimport { ConfigEnv, loadEnv, UserConfig } from \\\"vite\\\";\nimport { resolve } from \\\"path\\\";\nimport vue from \\\"@vitejs\u002Fplugin-vue\\\";\n\u002F\u002F import vitePluginAliOss from 'vite-plugin-ali-oss';\n\n\u002F\u002F自动导入组件\nimport Components from \\\"unplugin-vue-components\u002Fvite\\\";\nimport { ElementPlusResolver } from \\\"unplugin-vue-components\u002Fresolvers\\\";\nimport AutoImport from \\\"unplugin-auto-import\u002Fvite\\\";\n\u002F\u002F按需引入ElementPlus组件\nimport { createStyleImportPlugin, ElementPlusResolve } from \\\"vite-plugin-style-import\\\";\n\n\u002F\u002F提供压缩和基于 ejs 模板功能的 vite 插件\nimport { createHtmlPlugin } from \\\"vite-plugin-html\\\";\n\nimport { visualizer } from \\\"rollup-plugin-visualizer\\\";\nimport { webUpdateNotice } from \\\"@plugin-web-update-notification\u002Fvite\\\";\nimport viteCompression from \\\"vite-plugin-compression\\\";\nimport importToCDN from \\\"vite-plugin-cdn-import\\\";\n\nfunction pathResolve(dir: string) {\n  return resolve(process.cwd(), \\\".\\\", dir);\n}\n\nexport default ({ mode }: ConfigEnv): UserConfig => {\n  \u002F\u002F 获取 .env 环境配置文件\n  const env = loadEnv(mode, process.cwd());\n\n  const plugins = [\n    vue(),\n\n    webUpdateNotice({\n      logVersion: true,\n      notificationProps: {\n        title: \\\"更新通知\\\",\n        description: \\\"网站内容有更新，请刷新页面，获取最新内容\\\",\n        buttonText: \\\"刷新\\\",\n        dismissButtonText: \\\"忽略\\\"\n      }\n    }),\n    visualizer(),\n    \u002F\u002F导入自定义组件\n    Components({\n      \u002F\u002F 指定组件位置，默认是src\u002Fcomponents\n      dirs: [\\\"src\u002Fcomponents\\\"],\n      extensions: [\\\"vue\\\"],\n      \u002F\u002F 配置文件生成位置\n      dts: \\\"src\u002Fcomponents.d.ts\\\",\n      \u002F\u002F ui库解析器，也可以自定义\n      resolvers: [ElementPlusResolver()]\n    }),\n\n    \u002F\u002F导入vue函数\n    AutoImport({\n      imports: [\\\"vue\\\", \\\"vue-router\\\", \\\"pinia\\\"],\n      \u002F\u002F 可以选择auto-import.d.ts生成的位置，使用ts建议设置为'src\u002Fauto-import.d.ts'\n      dts: \\\"src\u002Fauto-import.d.ts\\\",\n      \u002F\u002F eslint报错解决\n      eslintrc: {\n        enabled: true, \u002F\u002F Default `false`\n        filepath: \\\".\u002F.eslintrc-auto-import.json\\\", \u002F\u002F Default `.\u002F.eslintrc-auto-import.json`\n        globalsPropValue: true \u002F\u002F Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')\n      }\n    }),\n\n    \u002F\u002F按需引入vant组件样式\n    createStyleImportPlugin({\n      resolves: [ElementPlusResolve()]\n    }),\n\n    \u002F\u002Fejs变量注入\n    createHtmlPlugin({\n      minify: true,\n      \u002F\u002F entry: \\\"src\u002Fmain.ts\\\",\n      \u002F\u002F template: \\\"index.html\\\",\n      inject: {\n        data: {\n          title: env.VITE_APP_TITLE,\n          isProd: mode === \\\"production\\\"\n        }\n      }\n    }),\n\n    \u002F\u002Foss\n    \u002F\u002F vitePluginAliOss({\n    \u002F\u002F   region: env.VITE_OSS_REGION,\n    \u002F\u002F   accessKeyId: env.VITE_OSS_ACCESSKEYID,\n    \u002F\u002F   accessKeySecret: env.VITE_OSS_ACCESSKEYSECRET,\n    \u002F\u002F   bucket: env.VITE_OSS_BUCKET,\n    \u002F\u002F }),\n\n    \u002F\u002F 打包压缩，主要是本地gzip，如果服务器配置压缩也可以\n    viteCompression(),\n\n    importToCDN({\n      modules: [\n        {\n          name: \\\"videojs\\\",\n          var: \\\"videojs\\\",\n          path: \\\"https:\u002F\u002Fcdn.bootcdn.net\u002Fajax\u002Flibs\u002Fvideo.js\u002F8.2.0\u002Fvideo.min.js\\\",\n          css: \\\"https:\u002F\u002Fcdn.bootcdn.net\u002Fajax\u002Flibs\u002Fvideo.js\u002F8.2.0\u002Fvideo-js.min.css\\\"\n        }\n      ]\n    })\n  ]; \u002F\u002F 可将其他插件放入该数组\n\n  return {\n    \u002F\u002F开发或生产环境服务的公共基础路径\n    \u002F\u002F base: mode === 'production' ? env.VITE_OSS_URL : '.\u002F',\n    base: \\\".\u002F\\\",\n    resolve: {\n      alias: [\n        {\n          find: \u002F@\\\u002F\u002F,\n          replacement: pathResolve(\\\"src\\\") + \\\"\u002F\\\"\n        }\n      ]\n    },\n    css: {\n      preprocessorOptions: {\n        \u002F\u002F配置全局样式\n        \u002F\u002F scss: {\n        \u002F\u002F   \u002F\u002F设为 false 可以避免 Vite 清屏而错过在终端中打印某些关键信息\n        \u002F\u002F   charset: false,\n        \u002F\u002F   additionalData: ['@use \\\"@\u002Fassets\u002Fcss\u002Fglobalstyle.scss\\\" as *;'],\n        \u002F\u002F },\n      }\n    },\n    server: {\n      host: \\\"0.0.0.0\\\",\n      port: Number(env.VITE_APP_PORT),\n      open: false, \u002F\u002F 设置服务启动时是否自动打开浏览器\n      cors: true, \u002F\u002F 允许跨域\n      proxy: {\n        [env.VITE_APP_BASE_API]: {\n          target: env.VITE_APP_BASE_API_URL,\n          changeOrigin: true,\n          secure: false,\n          \u002F\u002F删除\u002Fapi\n          rewrite: path => path.replace(new RegExp(\\\"^\\\" + env.VITE_APP_BASE_API), \\\"\\\")\n        }\n      }\n    },\n    plugins: plugins,\n\n    build: {\n      minify: \\\"terser\\\",\n      \u002F\u002F限制的大小将500kb改成600kb\n      chunkSizeWarningLimit: 600,\n\n      terserOptions: {\n        compress: {\n          \u002F\u002F生产环境时移除console\n          drop_console: true,\n          drop_debugger: true\n        },\n        output: {\n          \u002F\u002F 去掉注释内容\n          comments: true\n        }\n      },\n\n      \u002F\u002F指定生成静态资源的存放路径\n      assetsDir: \\\"\u002F\\\",\n\n      rollupOptions: {\n        \u002F\u002F打包目录区分优化\n        output: {\n          \u002F\u002F manualChunks(id) {\n          \u002F\u002F   \u002F\u002F if (id.includes('node_modules')) {\n          \u002F\u002F   \u002F\u002F   return id.toString().split('node_modules\u002F')[1].split('\u002F')[0].toString();\n          \u002F\u002F   \u002F\u002F }\n          \u002F\u002F },\n\n          \u002F\u002F把echarts单独打包（按需引入分离出去）\n          manualChunks: {\n            echarts: [\\\"echarts\\\"]\n          },\n          chunkFileNames: \\\"static\u002F[name]-[hash].js\\\",\n          entryFileNames: \\\"static\u002F[name]-[hash].js\\\",\n          assetFileNames: \\\"static\u002F[name]-[hash].[ext]\\\"\n        }\n      }\n    }\n  };\n};\n\n```\n\n\n\n\n\n\n\n## 自动导入组件、hooks、样式\n\n|           插件            |           作用            |\n| :-----------------------: | :-----------------------: |\n|  unplugin-vue-components  |       组件自动引入        |\n| unplugin-auto-import\u002Fvite | vue3等插件 hooks 自动引入 |\n| vite-plugin-style-import  |       样式自动引入        |\n|      vue-global-api       |        eslint插件         |\n\n\n\n### 1.1 自动导入ui库,该插件内置了大多数流行库解析器\n\n```shell\nnpm install unplugin-vue-components -D\n```\n\n```js\n\u002F\u002F vite.config.js,插件会生成一个ui库组件以及指令路径components.d.ts文件\nimport { defineConfig } from 'vite'\nimport Components from 'unplugin-vue-components\u002Fvite'\nimport {\n  ElementPlusResolver,\n  AntDesignVueResolver,\n  VantResolver,\n  HeadlessUiResolver,\n  ElementUiResolver\n} from 'unplugin-vue-components\u002Fresolvers'\n\nexport default defineConfig({\n  plugins: [\n    Components({\n      \u002F\u002F ui库解析器，也可以自定义\n      resolvers: [\n        ElementPlusResolver(),\n        AntDesignVueResolver(),\n        VantResolver(),\n        HeadlessUiResolver(),\n        ElementUiResolver()\n      ]\n    })\n  ]\n})\n```\n\n### 1.2 自动导入自己的组件\n\n```js\n\u002F\u002F vite.config.js,插件会生成一个自己组件路径的components.d.ts文件\nimport { defineConfig } from 'vite'\nimport Components from 'unplugin-vue-components\u002Fvite'\n\nexport default defineConfig({\n  plugins: [\n    Components({\n      \u002F\u002F 指定组件位置，默认是src\u002Fcomponents\n      dirs: ['src\u002Fcomponents'],\n      \u002F\u002F ui库解析器\n      \u002F\u002F resolvers: [ElementPlusResolver()],\n      extensions: ['vue'],\n      \u002F\u002F 配置文件生成位置\n      dts: 'src\u002Fcomponents.d.ts'\n    })\n  ]\n})\n```\n\n\n\n## 2. unplugin-auto-import\u002Fvite\n\n**支持`vue`, `vue-router`, `vue-i18n`, `@vueuse\u002Fhead`, `@vueuse\u002Fcore`等自动引入**\n\n```js\n\u002F\u002F 引入前\nimport { ref, computed } from 'vue'\nconst count = ref(0)\nconst doubled = computed(() => count.value * 2)\n\n\u002F\u002F引入后\nconst count = ref(0)\nconst doubled = computed(() => count.value * 2)\n\n\n\u002F\u002F 引入前\nimport { useState } from 'react'\nexport function Counter() {\n  const [count, setCount] = useState(0)\n  return \u003Cdiv>{ count }\u003C\u002Fdiv>\n}\n\n\u002F\u002F引入后\nexport function Counter() {\n  const [count, setCount] = useState(0)\n  return \u003Cdiv>{ count }\u003C\u002Fdiv>\n}\n```\n\n#### 安装\n\n```shell\nnpm i -D unplugin-auto-import\n```\n\n```js\n\u002F\u002F vite.config.js\nimport { defineConfig } from 'vite'\nimport AutoImport from 'unplugin-auto-import\u002Fvite'\n\nexport default defineConfig({\n  plugins: [\n    AutoImport({\n      imports: ['vue', 'vue-router', 'vue-i18n', '@vueuse\u002Fhead', '@vueuse\u002Fcore'],\n      \u002F\u002F 可以选择auto-import.d.ts生成的位置，使用ts建议设置为'src\u002Fauto-import.d.ts'\n      \u002F\u002F dts: 'src\u002Fauto-import.d.ts'\n    })\n  ]\n})\n```\n\n`原理:` 安装的时候会自动生成auto-imports.d文件(默认是在根目录)\n\n## 3.vue-global-api解决eslint报错\n\n**在页面没有引入的情况下，使用unplugin-auto-import\u002Fvite来自动引入hooks，在项目中肯定会报错的，这时候需要在eslintrc.js中的extends引入vue-global-api，这个插件是vue3hooks的,其他自己找找，找不到的话可以手动配置一下globals**\n\n#### 安装 \n\n```shell\nnpm i vue-global-api -D\n```\n\n\n\n```js\n\u002F\u002F .eslintrc.js\nmodule.exports = {\n  extends: [\n    'vue-global-api'\n  ]\n};\n```\n\n它还为细粒度控制提供了相同的集合和单个 API 选项。\n\n```js\n\u002F\u002F .eslintrc.js\nmodule.exports = {\n  extends: [\n    \u002F\u002F collections\n    'vue-global-api\u002Freactivity',\n    'vue-global-api\u002Flifecycle',\n    'vue-global-api\u002Fcomponent',\n    \u002F\u002F single apis\n    'vue-global-api\u002Fref',\n    'vue-global-api\u002FtoRef',\n  ]\n};\n```\n\n## 4.vite-plugin-style-import\n\n当你使用unplugin-vue-components来引入ui库的时候\n\nmessage, notification 等引入样式不生效 安装vite-plugin-style-import即可\n\n**这里以一些流行库为例**\n\n```js\n\u002F\u002F vite.config.js\nimport { defineConfig } from 'vite'\nimport styleImport, {\n  AndDesignVueResolve,\n  VantResolve,\n  ElementPlusResolve,\n  NutuiResolve,\n  AntdResolve\n} from 'vite-plugin-style-import'\n\nexport default defineConfig({\n  plugins: [\n    styleImport({\n      resolves: [\n        AndDesignVueResolve(),\n        VantResolve(),\n        ElementPlusResolve(),\n        NutuiResolve(),\n        AntdResolve()\n      ],\n      \u002F\u002F 自定义规则\n      libs: [\n        {\n          libraryName: 'ant-design-vue',\n          esModule: true,\n          resolveStyle: (name) => {\n            return `ant-design-vue\u002Fes\u002F${name}\u002Fstyle\u002Findex`\n          }\n        }\n      ]\n    })\n  ],\n  \u002F\u002F 引用使用less的库要配置一下\n  css: {\n    preprocessorOptions: {\n      less: {\n        javascriptEnabled: true\n      }\n    }\n  }\n})\n```\n\n## 5. 注意点\n\n### 1. element-plus默认是英文\n\n**方案: 在app.vue加上ElConfigProvider**\n\n```vue\n\u003Ctemplate>\n  \u003Cdiv id=\\\"app\\\">\n    \u003Cel-config-provider :locale=\\\"locale\\\">\n      \u003Crouter-view>\u003C\u002Frouter-view>\n    \u003C\u002Fel-config-provider>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\u003Cscript setup>\nimport zhCn from 'element-plus\u002Flib\u002Flocale\u002Flang\u002Fzh-cn'\nconst locale = zhCn\n\u003C\u002Fscript>\n```\n\n**日期相关组件设置中文:**\n\n```vue\n\u003Cscript setup>\n\u002F\u002F 日历等与dayjs相关的组件，不想显示中文可以不加\n\u002F\u002F 第一种方法 使用中国时区weekStart默认为1\nimport 'dayjs\u002Flocale\u002Fzh-cn'\n\n\u002F\u002F 第二种方法 使用 weekStart可配置(只能是0或者1)\nimport dayjs from 'dayjs'\n\u002F\u002F 引入英文即为英文\nimport cn from 'dayjs\u002Flocale\u002Fzh-cn'\ndayjs.locale({\n  ...cn,\n  weekStart: 1\n})\n\n\nconst locale = zhCn\n\u003C\u002Fscript>\n```\n\n\n###  vite-plugin-ali-oss\n> 打包自动上传打包文件到oss\n```js\nimport vitePluginAliOss from 'vite-plugin-ali-oss';\n  \u002F\u002F 获取 .env 环境配置文件\nconst env = loadEnv(mode, process.cwd());\n\n    \u002F\u002Foss  plugins数组\nvitePluginAliOss({\n            region: env.VITE_OSS_REGION,\n            accessKeyId: env.VITE_OSS_ACCESSKEYID,\n            accessKeySecret: env.VITE_OSS_ACCESSKEYSECRET,\n            bucket: env.VITE_OSS_BUCKET\n    })\n```\n\n## vite使用cdn\n### vite-plugin-cdn-import\n\n```js\nimport importToCDN from 'vite-plugin-cdn-import';\nimportToCDN({\n    modules: [\n        {\n            name: 'vue',\n            var: 'Vue',\n            path: 'https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002Fvue@3.2.25\u002Fdist\u002Fvue.global.prod.js'\n        },\n        {\n            name: 'vue-i18n',\n            var: 'VueI18n',\n            path: 'https:\u002F\u002Fcdn.bootcdn.net\u002Fajax\u002Flibs\u002Fvue-i18n\u002F9.1.10\u002Fvue-i18n.global.prod.min.js'\n        },\n        {\n            name: 'vue-router',\n            var: 'VueRouter',\n            path: 'https:\u002F\u002Funpkg.com\u002Fvue-router@4.0.16\u002Fdist\u002Fvue-router.global.prod.js'\n        },\n\n        {\n            name: 'element-plus',\n            var: 'ElementPlus',\n            path: `https:\u002F\u002Funpkg.com\u002Felement-plus@2.2.6\u002Fdist\u002Findex.full.js`,\n            css: 'https:\u002F\u002Funpkg.com\u002Felement-plus\u002Fdist\u002Findex.css'\n        },\n        {\n            name: 'vue-demi',\n            var: 'VueDemi',\n            path: 'https:\u002F\u002Fcdn.bootcdn.net\u002Fajax\u002Flibs\u002Fvue-demi\u002F0.13.1\u002Findex.iife.js'\n        },\n        {\n            name: 'pinia',\n            var: 'Pinia',\n            path: 'https:\u002F\u002Fcdn.bootcdn.net\u002Fajax\u002Flibs\u002Fpinia\u002F2.0.14\u002Fpinia.iife.prod.min.js'\n        },\n        {\n            name: '@smallwei\u002Favue',\n            var: 'AVUE',\n            path: 'https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002F@smallwei\u002Favue@3.0.17'\n        }\n    ]\n}),\n这上面的都有引用关系所以都需要通过cdn的方式引入\nVueDemi这个是pinia用来判断是vue2还是vue3所需要的，要额外引入一下\n```\n\n> 第二种方案（通过rollup-plugin-external-globals）这个方式需要单独在index.html中引入cdn资源\n\n```js\nimport externalGlobals from 'rollup-plugin-external-globals';\nlet globals = externalGlobals({\n    vue: 'Vue',\n    'vue-i18n': 'VueI18n',\n    'vue-router': 'VueRouter',\n    'element-plus': 'ElementPlus',\n    pinia: 'Pinia',\n    '@smallwei\u002Favue': 'AVUE'\n});\n\n\u002F\u002Fvite.config.ts(注意plugins的位置是在build下)\n\nbuild:{\n    rollupOptions:{\n        \u002F\u002F 忽略打包\n        external: ['vue', 'vue-i18n', 'pinia', 'vue-router', 'element-plus','@smallwei\u002Favue'],\n    },\n    plugins: [globals],\n}\n\n\u002F\u002F index.html\n\n\u003Cscript src=\\\"https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002Fvue@3.2.25\u002Fdist\u002Fvue.global.prod.js\\\">\u003C\u002Fscript>\n\u003Cscript src=\\\"https:\u002F\u002Fcdn.bootcdn.net\u002Fajax\u002Flibs\u002Fvue-i18n\u002F9.1.10\u002Fvue-i18n.global.prod.min.js\\\">\u003C\u002Fscript>\n\u003Cscript src=\\\"https:\u002F\u002Funpkg.com\u002Fvue-router@4.0.16\u002Fdist\u002Fvue-router.global.prod.js\\\">\u003C\u002Fscript>\n\u003Cscript src=\\\"https:\u002F\u002Funpkg.com\u002Felement-plus@2.2.6\u002Fdist\u002Findex.full.js\\\">\u003C\u002Fscript>\n\u003Cscript src=\\\"https:\u002F\u002Fcdn.bootcdn.net\u002Fajax\u002Flibs\u002Fvue-demi\u002F0.13.1\u002Findex.iife.js\\\">\u003C\u002Fscript>\n\u003Cscript src=\\\"https:\u002F\u002Fcdn.bootcdn.net\u002Fajax\u002Flibs\u002Fpinia\u002F2.0.14\u002Fpinia.iife.prod.min.js\\\">\u003C\u002Fscript>\n\u003Cscript src=\\\"https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002F@smallwei\u002Favue@3.0.17\\\">\u003C\u002Fscript>\n```\n\n\n## 网页重新部署，通知用户-最佳实践\n## 安装插件\n```language\npnpm add @plugin-web-update-notification\u002Fvite -D\n```\n\n\n## 原理\n> 以 git commit hash (也支持 package.json version、build timestamp、custom) 为版本号，打包时将版本号写入 json 文件。客户端轮询服务器上的版本号（浏览器窗口的 visibilitychange、focus 事件辅助），和本地作比较，如果不相同则通知用户刷新页面。\n\n## vite.config.ts\n```language\nimport { webUpdateNotice } from '@plugin-web-update-notification\u002Fvite'\n\n\nexport default defineConfig({\n  plugins: [\n    vue(),\n    webUpdateNotice({\n      notificationProps: {\n        title: '更新通知',\n        description: '网站内容有更新，请刷新页面，获取最新内容',\n        buttonText: '刷新',\n        dismissButtonText: '忽略'\n      },\n    }),\n  ]\n})\n```\n\n\n\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F7243904146874d11fde3d902d5afa063.jpg",2438,"2022-06-02T07:30:36.000Z","2026-06-27T11:05:30.822Z",{"id":4,"name":5,"slug":5},{"id":25,"name":26,"avatar":19},[102],{"id":103,"name":104,"slug":105},"66f3aeb0-84ef-45f6-a43a-944eefc9895a","Vite","vite",6,1,10,{"categories":110,"tags":191,"postCount":362,"tagCount":363,"hotPosts":364},[111,118,123,130,136,142,149,153,160,161,165,170,176,181,185],{"id":112,"name":113,"slug":114,"description":115,"sortOrder":116,"createdAt":117,"updatedAt":117,"postCount":107},"e8d0bd45-d10c-46d3-8afb-0c072df7f8a7","技术","tech","技术文章",0,"2026-06-27T04:18:37.371Z",{"id":119,"name":120,"slug":121,"description":120,"sortOrder":107,"createdAt":122,"updatedAt":8,"postCount":106},"15ac46ad-edf7-4435-9a64-ff78117d58c5","Vue3 生态","vue3-生态","2022-05-21T08:05:39.000Z",{"id":124,"name":125,"slug":126,"description":125,"sortOrder":127,"createdAt":128,"updatedAt":8,"postCount":129},"11d4d397-685c-4180-a7b3-9b0e3a1e411e","Css","css",2,"2022-05-23T07:19:37.000Z",9,{"id":131,"name":132,"slug":132,"description":132,"sortOrder":133,"createdAt":134,"updatedAt":8,"postCount":135},"d10456a5-e649-4741-a38f-f07f266ce5f2","开发环境",3,"2022-05-24T01:52:41.000Z",13,{"id":137,"name":138,"slug":139,"description":138,"sortOrder":140,"createdAt":141,"updatedAt":8,"postCount":6},"5ed5cc62-43ea-49a2-b0b2-38bc7aae52a0","Vue3","vue3",4,"2022-05-24T01:55:05.000Z",{"id":143,"name":144,"slug":145,"description":144,"sortOrder":146,"createdAt":147,"updatedAt":8,"postCount":148},"da130ba9-d4f4-49f3-aa0f-149078097ef0","JavaScript","javascript",5,"2022-05-24T02:22:57.000Z",18,{"id":150,"name":151,"slug":151,"description":151,"sortOrder":106,"createdAt":152,"updatedAt":8,"postCount":107},"d8cbe380-54b3-4a61-a12d-5438c2918574","限时优惠","2022-05-25T07:18:03.000Z",{"id":154,"name":155,"slug":156,"description":155,"sortOrder":157,"createdAt":158,"updatedAt":8,"postCount":159},"e0f3b8d8-cfe7-41fb-802b-a79699d95968","JavaScript插件","javascript插件",7,"2022-06-01T14:08:31.000Z",16,{"id":4,"name":5,"slug":5,"description":5,"sortOrder":6,"createdAt":7,"updatedAt":8,"postCount":106},{"id":162,"name":163,"slug":163,"description":163,"sortOrder":129,"createdAt":164,"updatedAt":8,"postCount":6},"9ed9827c-9cbb-42da-80e4-d04c7fdba886","开发工具","2022-06-21T03:35:05.000Z",{"id":166,"name":167,"slug":168,"description":167,"sortOrder":108,"createdAt":169,"updatedAt":8,"postCount":146},"6b9179c3-17b2-43ff-a431-a03d6eb32d89","Vue2 生态","vue2-生态","2022-07-16T13:14:29.000Z",{"id":171,"name":172,"slug":173,"description":172,"sortOrder":174,"createdAt":175,"updatedAt":8,"postCount":146},"73a5f62c-3c47-45b9-9ae2-f29953ae8dc0","Node","node",11,"2022-07-16T13:15:39.000Z",{"id":177,"name":178,"slug":178,"description":178,"sortOrder":179,"createdAt":180,"updatedAt":8,"postCount":127},"2b696c16-48ef-403b-a88b-6e57cfc79596","开发问题",12,"2022-07-16T14:06:54.000Z",{"id":182,"name":183,"slug":183,"description":183,"sortOrder":135,"createdAt":184,"updatedAt":8,"postCount":107},"c0f0561e-a47a-4ecd-8caa-cc1df2315d57","算法","2022-07-16T14:22:34.000Z",{"id":186,"name":187,"slug":188,"description":187,"sortOrder":189,"createdAt":190,"updatedAt":8,"postCount":133},"a629c1f7-29f1-439e-be3c-29670b17ba20","Vue2","vue2",15,"2022-07-16T14:41:51.000Z",[192,198,203,206,208,213,217,221,226,231,236,241,242,247,251,255,260,265,270,272,276,280,284,286,290,295,297,300,303,306,309,312,316,319,321,323,326,330,333,337,341,344,347,349,352,354,356,359],{"id":193,"name":194,"slug":195,"createdAt":196,"updatedAt":197},"076bd8b9-293e-45cb-9dc3-e162007ca474","Axios","axios","2022-06-05T07:41:56.000Z","2025-12-30T07:26:21.000Z",{"id":199,"name":200,"slug":201,"createdAt":202,"updatedAt":8},"2aa7f6d0-1fac-4ed1-b9bb-f3afc813f42c","Axure","axure","2022-06-21T03:35:15.000Z",{"id":204,"name":125,"slug":126,"createdAt":205,"updatedAt":8},"b084ddd8-09be-4e57-98f0-cf4e376aecd7","2022-05-21T09:59:55.000Z",{"id":44,"name":45,"slug":46,"createdAt":207,"updatedAt":8},"2022-07-16T14:34:37.000Z",{"id":209,"name":210,"slug":211,"createdAt":212,"updatedAt":8},"2de16806-ef3f-4e54-a259-d1e1e182468c","Git","git","2022-07-16T14:25:15.000Z",{"id":214,"name":215,"slug":216,"createdAt":205,"updatedAt":8},"994cc226-578b-4a72-a57e-a47a63d2793e","JavaScript生态","javascript生态",{"id":218,"name":219,"slug":220,"createdAt":205,"updatedAt":8},"5086e93c-23b9-43d3-9643-cc87f0e9ee94","JenKins","jenkins",{"id":222,"name":223,"slug":224,"createdAt":225,"updatedAt":8},"b73007a8-bb5c-42a8-9fd9-163033a5b45d","Linux","linux","2022-07-16T14:40:17.000Z",{"id":227,"name":228,"slug":229,"createdAt":230,"updatedAt":8},"0b658b92-dd6b-4db3-a398-9f6d69950a02","Markdown","markdown","2022-07-16T14:39:25.000Z",{"id":232,"name":233,"slug":234,"createdAt":235,"updatedAt":8},"ab034d3a-6e5b-4db5-a2dc-faf4ccbb63f5","Nest","nest","2022-07-16T13:15:49.000Z",{"id":237,"name":238,"slug":239,"createdAt":240,"updatedAt":8},"52c41978-da06-4962-9636-45bbaeedda80","Nginx","nginx","2022-05-21T09:59:56.000Z",{"id":29,"name":30,"slug":30,"createdAt":240,"updatedAt":8},{"id":243,"name":244,"slug":245,"createdAt":246,"updatedAt":8},"a4370d78-70e1-4073-a8f6-3dc5d81fd8fd","Nuxt","nuxt","2022-06-01T13:07:07.000Z",{"id":248,"name":249,"slug":250,"createdAt":205,"updatedAt":8},"d232e01f-048e-4151-8a0a-fff9561f946f","Pinia","pinia",{"id":252,"name":253,"slug":254,"createdAt":240,"updatedAt":8},"14e9ab02-b0bb-4c85-8604-fe6f1f0f33cd","Pnpm","pnpm",{"id":256,"name":257,"slug":258,"createdAt":259,"updatedAt":259},"399d1d38-cc0d-43ce-8baf-c769447a2ebd","React生态","react生态","2023-02-21T02:03:09.000Z",{"id":261,"name":262,"slug":263,"createdAt":264,"updatedAt":8},"c95bbe84-bdd0-410a-86a9-e87958c55f4f","Redis","redis","2022-10-05T05:14:14.000Z",{"id":266,"name":267,"slug":268,"createdAt":269,"updatedAt":8},"6d05f9df-e116-450f-af57-85ed710c4870","Swiper","swiper","2022-06-01T14:08:46.000Z",{"id":103,"name":104,"slug":105,"createdAt":271,"updatedAt":8},"2022-06-02T07:28:24.000Z",{"id":273,"name":274,"slug":275,"createdAt":205,"updatedAt":8},"bf5b94d3-090b-4098-a03c-4bc69781fb2d","Vue","vue",{"id":277,"name":278,"slug":279,"createdAt":240,"updatedAt":8},"2f7fb1be-b9c5-4606-b54f-e9f66f2653b2","Vue-Router","vue-router",{"id":281,"name":282,"slug":283,"createdAt":205,"updatedAt":8},"2fef3b91-1c1c-4ae8-b2c1-0e04b4f9b3a2","Vue2生态","vue2生态",{"id":285,"name":138,"slug":139,"createdAt":205,"updatedAt":8},"62b94c93-724f-488d-8fc5-0449971d9204",{"id":287,"name":288,"slug":289,"createdAt":205,"updatedAt":8},"20bff9cd-7848-4c16-8775-42cf12b44b30","Vue3生态","vue3生态",{"id":291,"name":292,"slug":293,"createdAt":294,"updatedAt":8},"c807b2c6-cb12-4409-a1f1-6bea9f330a6b","Vuex","vuex","2022-07-16T13:14:59.000Z",{"id":60,"name":61,"slug":62,"createdAt":296,"updatedAt":8},"2022-07-16T14:33:41.000Z",{"id":298,"name":299,"slug":299,"createdAt":205,"updatedAt":8},"d0aa41f4-68f8-48d4-a4ed-3a503ea90451","下载",{"id":301,"name":302,"slug":302,"createdAt":205,"updatedAt":8},"a046060c-39ef-474a-8c85-2546aca0e2e5","代码片段",{"id":304,"name":305,"slug":305,"createdAt":205,"updatedAt":8},"fee73435-b2be-4b55-85b1-d133ea96aea4","伪元素",{"id":307,"name":308,"slug":308,"createdAt":205,"updatedAt":8},"436bd369-8c57-4869-8827-e88e50e5e0ab","伪类",{"id":310,"name":311,"slug":311,"createdAt":205,"updatedAt":8},"4c6be544-8a00-4445-92a3-e3dcbaf6142e","动画",{"id":313,"name":314,"slug":314,"createdAt":315,"updatedAt":8},"9321a12e-ea72-49a9-a32d-5566149f812f","图片压缩","2022-08-02T00:37:47.000Z",{"id":317,"name":318,"slug":318,"createdAt":240,"updatedAt":8},"512b16fb-576a-4397-a7c5-dd20e6a8f9ca","布局",{"id":320,"name":163,"slug":163,"createdAt":205,"updatedAt":8},"f32faa96-f2ec-45c6-9a17-2c76062edcb0",{"id":322,"name":132,"slug":132,"createdAt":205,"updatedAt":8},"3c46ed3f-6d6b-4f91-bcb3-af5112860bf5",{"id":324,"name":325,"slug":325,"createdAt":205,"updatedAt":8},"dbfc086a-73a6-4560-814d-593acb61cf98","性能优化",{"id":327,"name":328,"slug":328,"createdAt":329,"updatedAt":8},"1831cd06-0d6b-48f7-94fa-324782fe23cb","拖拽","2022-07-28T12:39:13.000Z",{"id":331,"name":332,"slug":332,"createdAt":240,"updatedAt":8},"9a74300d-06f7-46d0-80d9-8fe67ec0539b","数组",{"id":334,"name":335,"slug":335,"createdAt":336,"updatedAt":8},"19ac8998-7e0a-459b-9702-bb1adca70e8c","文本复制","2022-07-17T01:54:45.000Z",{"id":338,"name":339,"slug":339,"createdAt":340,"updatedAt":8},"5ff33473-71a4-4e02-8c82-f9ea369a768f","时间","2022-07-17T01:51:12.000Z",{"id":342,"name":343,"slug":343,"createdAt":205,"updatedAt":8},"aa47ca4d-d3f6-4cac-b495-2c67c9592c36","最新优惠",{"id":345,"name":346,"slug":346,"createdAt":240,"updatedAt":8},"f6766d54-54fc-405e-932d-b7d550559125","服务器",{"id":348,"name":5,"slug":5,"createdAt":205,"updatedAt":8},"d856559a-03ff-40b4-980d-3f272b998c3c",{"id":350,"name":351,"slug":351,"createdAt":205,"updatedAt":8},"692d5d68-b188-4e5c-aca8-65d0229399a1","渐变",{"id":353,"name":183,"slug":183,"createdAt":205,"updatedAt":8},"38e1fd6b-d7c6-4d62-bf70-7bacc175bea9",{"id":75,"name":76,"slug":76,"createdAt":355,"updatedAt":8},"2022-07-16T14:41:06.000Z",{"id":357,"name":358,"slug":358,"createdAt":240,"updatedAt":8},"b42e2916-ad62-4b8a-a863-cd8c19a829de","面试",{"id":360,"name":361,"slug":361,"createdAt":240,"updatedAt":8},"7069add9-b636-44f1-9cd4-ea3a6d2b85d3","面试题",104,48,[365,377,389,402,415],{"id":366,"title":367,"slug":367,"content":368,"excerpt":367,"coverImage":369,"status":17,"isPinned":18,"pinnedAt":19,"viewCount":370,"publishedAt":371,"createdAt":371,"updatedAt":372,"category":373,"author":374,"tags":375},"a1bd1f49-6f6d-4fea-9789-b5636e19a6b3","uni-app瀑布流","## 实现思路\n> 获取父组件的列表数组，watch监听数组长度变化，截取后面新的数据，创建两个左右数组，比较左右dom的长度，哪个短，就push一条数据进去，源数组删除一条数据。利用img的load（加载成功）和error方法（加载失败），触发数组的push，实现瀑布流\n\n## 代码实现\n\n```language\n\u003Ctemplate>\n    \u003Cview class=\\\"waterfall\\\">\n        \u003Cview class=\\\"waterfall_left\\\">\n            \u003Cview class=\\\"waterfall_list\\\" v-for=\\\"(item, index) in leftList\\\" :key=\\\"index\\\">\n                \u003CSearch\n                    :name=\\\"item.name\\\"\n                    :image=\\\"item.src\\\"\n                    :label=\\\"item.label\\\"\n                    :item=\\\"item\\\"\n                    @considerPush=\\\"considerPush\\\"\n                >\n                \u003C\u002FSearch>\n            \u003C\u002Fview>\n        \u003C\u002Fview>\n        \u003Cview class=\\\"waterfall_right\\\">\n            \u003Cview class=\\\"waterfall_list\\\" v-for=\\\"(item, index) in rightList\\\" :key=\\\"index\\\">\n                \u003CSearch\n                    :name=\\\"item.name\\\"\n                    :image=\\\"item.src\\\"\n                    :label=\\\"item.label\\\"\n                    :item=\\\"item\\\"\n                    @considerPush=\\\"considerPush\\\"\n                >\n                \u003C\u002FSearch>\n            \u003C\u002Fview>\n        \u003C\u002Fview>\n    \u003C\u002Fview>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nimport Search from '@\u002Fcomponents\u002Fsearch\u002Fsearch.vue';\nexport default {\n    components: { Search },\n    props: {\n        list: {\n            type: Array,\n            default: () => []\n        }\n    },\n    data() {\n        return {\n            \u002F\u002F 左侧列表\n            leftList: [],\n            \u002F\u002F 右侧列表\n            rightList: [],\n            \u002F\u002F 组件数据备份\n            newList: [],\n            \u002F\u002F默认请求数,主要为了正常排序\n            interceptNumber: 10\n        };\n    },\n    created() {\n        this.touchOff(); \u002F\u002F 触发排列\n    },\n    mounted() {},\n    watch: {\n        list(newValue, oldValue) {\n            this.interceptNumber = newValue.length - oldValue.length;\n            this.touchOff();\n        }\n    },\n    computed: {},\n    methods: {\n        \u002F\u002F 触发重新排列\n        touchOff() {\n            this.newList = [...this.list.slice(-this.interceptNumber)];\n            if (this.newList.length !== 0) {\n                this.leftList.push(this.newList.shift()); \u002F\u002F触发排列\n            }\n        },\n        \u002F\u002F 计算排列\n        considerPush() {\n            this.$nextTick(() => {\n                if (this.newList.length == 0) return; \u002F\u002F没有数据了\n                let leftH = 0;\n                let rightH = 0; \u002F\u002F左右高度\n                let query = uni.createSelectorQuery().in(this);\n                query.selectAll('.waterfall_left').boundingClientRect();\n                query.selectAll('.waterfall_right').boundingClientRect();\n                query.exec(res => {\n                    leftH = res[0].length != 0 ? res[0][0].height : 0; \u002F\u002F防止查询不到做个处理\n                    rightH = res[1].length != 0 ? res[1][0].height : 0;\n                    if (leftH == rightH || leftH \u003C rightH) {\n                        \u002F\u002F 相等 || 左边小\n                        this.leftList.push(this.newList.shift());\n                    } else {\n                        \u002F\u002F 右边小\n                        this.rightList.push(this.newList.shift());\n                    }\n\n                    \u002F\u002F console.log('左右高度：', leftH, rightH, leftH == rightH || leftH \u003C rightH);\n                });\n            });\n        }\n    }\n};\n\u003C\u002Fscript>\n\n\u003Cstyle scoped lang=\\\"scss\\\">\n.waterfall {\n    display: flex;\n    align-items: flex-start;\n    justify-content: flex-start;\n    .waterfall_left {\n        flex: 1;\n    }\n\n    .waterfall_right {\n        flex: 1;\n    }\n}\n\u003C\u002Fstyle>\n\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F7d4fa8d1d2775177b882a4656e3a5ed5.png",1819,"2022-11-22T07:54:41.000Z","2026-06-27T11:37:20.140Z",{"id":143,"name":144,"slug":145},{"id":25,"name":26,"avatar":19},[376],{"id":214,"name":215,"slug":216},{"id":378,"title":379,"slug":379,"content":380,"excerpt":379,"coverImage":381,"status":17,"isPinned":18,"pinnedAt":19,"viewCount":382,"publishedAt":383,"createdAt":383,"updatedAt":384,"category":385,"author":386,"tags":387},"b2c46bf6-d971-4cce-b21b-052dbea8e8a2","v-html使用img点击实现放大效果","## 代码实现\n```js\n\u002F**\n * JS获取html代码中所有的图片地址\n * @param htmlstr\n * @returns arr 数组\n *\u002F\n\nexport function getimgsrc(htmlstr) {\n    let reg = \u002F\u003Cimg.+?src=('|\\\")?([^'\\\"]+)('|\\\")?(?:\\s+|>)\u002Fg;\n    let arr = [];\n    let tem = 0;\n    \u002F\u002Feslint-disable-next-line\n    while ((tem = reg.exec(htmlstr))) {\n        arr.push(tem[2]); \u002F\u002F eslint-disable-line\n    }\n\n    return arr;\n}\n\n```\n\n\n\n```vue\n\u003Ctemplate>\n    \u003Cdiv class=\\\"image-expansion\\\" :class=\\\"classArr\\\">\n        \u003Cdiv @click.stop=\\\"hanldeImage($event)\\\" v-html=\\\"formatHtmlData\\\">\u003C\u002Fdiv>\n\n        \u003Cel-image-viewer\n            v-if=\\\"imgPreviewUrl\\\"\n            :initial-index=\\\"subscript\\\"\n            :src=\\\"imgPreviewUrl\\\"\n            :on-close=\\\"closeViewer\\\"\n            :url-list=\\\"imgList\\\"\n        >\u003C\u002Fel-image-viewer>\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript>\nimport { getimgsrc } from '..\u002F..\u002Futils\u002Fgetimgsrc';\nimport ElImageViewer from 'element-ui\u002Fpackages\u002Fimage\u002Fsrc\u002Fimage-viewer';\nexport default {\n    components: {\n        ElImageViewer\n    },\n    props: {\n        htmlData: {\n            type: String,\n            default: () => {\n                return '';\n            }\n        },\n        classArr: {\n            type: Array,\n            default: () => {\n                return ['min'];\n            }\n        },\n\n        isArticle: {\n            type: Boolean,\n            default: () => {\n                return false;\n            }\n        }\n    },\n    data() {\n        return {\n            imgList: [],\n            formatHtmlData: '',\n            imgPreviewUrl: '',\n            subscript: 0\n        };\n    },\n    computed: {},\n\n    watch: {\n        \u002F\u002F监听数据，防止数据不更新\n        htmlData: {\n            handler(newName, oldName) {\n                \u002F\u002F判断是否为文章\n                if (this.isArticle) {\n                    newName ? (this.formatHtmlData = newName.replace(\u002F(\u003C([^>]+)>)\u002Fgi, '').replace(\u002F[\\\r\\\n]\u002Fg, '')) : '';\n                } else {\n                    \u002F\u002F剔除strong和p标签\n                    newName ? (this.formatHtmlData = newName.replace(\u002F(\u003C\\\u002F?strong.*?>)|(\u003C\\\u002F?p.*?>)\u002Fg, '')) : '';\n\n                    \u002F\u002F获取html全部图片，push成图片数组\n                    this.imgList = Object.values(getimgsrc(this.formatHtmlData));\n                    \u002F\u002F获取图片下标\n                    let subscript = this.imgList.indexOf(this.imgPreviewUrl);\n                    this.subscript = subscript > -1 ? subscript : 0;\n                }\n            },\n\n            immediate: true\n        }\n    },\n\n    mounted() {},\n\n    methods: {\n        \u002F\u002F监听点击事件\n        hanldeImage(event) {\n            if (event.target.nodeName === 'IMG' || event.target.nodeName === 'img') {\n                \u002F\u002F获取点击的图片url,decodeURIComponent转码一下，防禁url转码\n                this.imgPreviewUrl = decodeURIComponent(event.target.currentSrc);\n\n                \u002F\u002F获取图片下标\n                let subscript = this.imgList.indexOf(this.imgPreviewUrl);\n                this.subscript = subscript > -1 ? subscript : 0;\n\n                \u002F\u002F禁止遮罩层后面的内容滚动\n                document.documentElement.style.overflowY = 'hidden';\n            } else {\n                this.$emit('goDetail');\n            }\n        },\n\n        \u002F\u002F关闭弹框\n        closeViewer() {\n            this.imgPreviewUrl = '';\n            \u002F\u002F恢复遮罩层后面的内容滚动\n            document.documentElement.style.overflowY = 'auto';\n        }\n    }\n};\n\u003C\u002Fscript>\n\n\u003Cstyle lang=\\\"scss\\\" scoped>\n.image-expansion {\n}\n\n.min {\n    \u002Fdeep\u002F img {\n        cursor: pointer;\n        height: 28px;\n        padding: 0 10px 3px;\n    }\n}\n\n.max {\n    \u002Fdeep\u002F img {\n        cursor: pointer;\n    }\n}\n\n.class1 {\n    \u002Fdeep\u002F div {\n        font-size: 15px;\n        font-family: Microsoft YaHei;\n        font-weight: 400;\n        color: #888888;\n        line-height: 30px;\n    }\n}\n\u003C\u002Fstyle>\n\n\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002Fc77c3fb113d1ab2f67e7afba1ca33b95.png",1572,"2023-01-10T07:22:29.000Z","2026-06-27T11:07:40.499Z",{"id":166,"name":167,"slug":168},{"id":25,"name":26,"avatar":19},[388],{"id":313,"name":314,"slug":314},{"id":390,"title":391,"slug":392,"content":393,"excerpt":391,"coverImage":394,"status":17,"isPinned":18,"pinnedAt":19,"viewCount":395,"publishedAt":396,"createdAt":396,"updatedAt":397,"category":398,"author":399,"tags":400},"9f33b49b-0785-4404-9a4e-d85937e7fc92","在 vue3 中优雅的使用 jsx\u002Ftsx","在-vue3-中优雅的使用-jsx-tsx","## 安装插件（@vitejs\u002Fplugin-vue-jsx）\n```shell\npnpm add @vitejs\u002Fplugin-vue-jsx -D\n```\n\n## 配置vite.config.ts\n```ts\nimport vueJsx from \\\"@vitejs\u002Fplugin-vue-jsx\\\";\n\nexport default defineConfig({\n  plugins: [\n    vueJsx(),\n  ]\n})\n```\n\n## 插值\n```language\n\u002F\u002F vue3模板语法\n\u003Cspan>{{ a + b }}\u003C\u002Fspan>\n\n\u002F\u002F jsx\u002Ftsx\n\u003Cspan>{ a + b }\u003C\u002Fspan>\n```\n\n\n## class与style 绑定\n\n```ts\n\u002F\u002F 模板字符串\n\u003Cdiv className={`header ${ isBg ? 'headerBg' : '' }`}>header\u003C\u002Fdiv>\n\u002F\u002F数组\n\u003Cdiv class={ [ 'header', isBg && 'headerBg' ] } >header\u003C\u002Fdiv>\n```\n\n```ts\nconst color = 'red'\nconst element = \u003Csapn style={{ color, fontSize: '16px' }}>style\u003C\u002Fsapn>\n```\n\n## 条件渲染\n\n```ts\n   setup() {\n       const isShow = false\n       const element = () => {\n           if (isShow) {\n               return \u003Cspan>我是if\u003C\u002Fspan>\n           } else {\n               return \u003Cspan>我是else\u003C\u002Fspan>\n           }\n       }\n       return () => (\n           \u003Cdiv>\n               \u003Cspan v-show={isShow}>我是v-show\u003C\u002Fspan>\n               {\n                   element()\n               }\n               {\n                   isShow ? \u003Cp>我是三目1\u003C\u002Fp> : \u003Cp>我是三目2\u003C\u002Fp>\n               }\n           \u003Cdiv>\n       )\n   }\n```\n\n## 列表渲染\n```language\nsetup() {\n   const listData = [\n       {name: 'Tom', age: 18},\n       {name: 'Jim', age: 20},\n       {name: 'Lucy', age: 16}\n   ]\n   return () => (\n       \u003Cdiv>\n           \u003Cdiv class={'box'}>\n               \u003Cspan>姓名\u003C\u002Fspan>\n               \u003Cspan>年龄\u003C\u002Fspan>\n           \u003C\u002Fdiv>\n           {\n               prop.listData.map(item => {\n                   return \u003Cdiv class={'box'}>\n                       \u003Cspan>{item.name}\u003C\u002Fspan>\n                       \u003Cspan>{item.age}\u003C\u002Fspan>\n                   \u003C\u002Fdiv>\n               })\n           }\n       \u003C\u002Fdiv>\n   )\n}\n\n```\n\n## 事件处理\n\n```language\nsetup() {\n    const clickBox = val => {\n        console.log(val)\n    }\n    return () => (\n        \u003Cdiv class={'box1'} onClick={() => clickBox('box1')}>\n            \u003Cspan>我是box1\u003C\u002Fspan>\n            \u003Cdiv class={'box2'} onClick={() => clickBox('box2')}>\n                \u003Cspan>我是box2\u003C\u002Fspan>\n                \u003Cdiv class={'box3'} onClick={withModifiers(() => clickBox('box3'), ['stop'])}>我是box3\u003C\u002Fdiv>\n            \u003C\u002Fdiv>\n        \u003C\u002Fdiv>\n    )\n}\n```\n\n## v-model\n\n```ts\n\u002F\u002F 正常写法\n\u003Cinput v-model=\\\"value\\\" \u002F> \u002F\u002F vue\n\u003Cinput v-model={value} \u002F> \u002F\u002F jsx\n\n\u002F\u002F 指定绑定值写法\n\u003Cinput v-model:modelValue=\\\"value\\\" \u002F> \u002F\u002F vue\n\u003Cinput v-model={[value,'modelValue']} \u002F> \u002F\u002F jsx\n\n\u002F\u002F 修饰符写法\n\u003Cinput v-model:modelValue.trim=\\\"value\\\" \u002F> \u002F\u002F vue\n\u003Cinput v-model={[value,'modelValue',['trim']]} \u002F> \u002F\u002F jsx\n```\n\n## slot插槽\n\n### 定义插槽\n```ts\nimport { renderSlot } from \\\"vue\\\"\nexport default defineComponent({\n    \u002F\u002F 从ctx中解构出来 slots\n    setup(props, { slots }) {\n        return () => (\n            \u003Cdiv>\n                { renderSlot(slots, 'default') }\n                { slots.title?.() }\n            \u003C\u002Fdiv>\n        )\n    }\n})\n\n```\n\n### 使用插槽\n\n```ts\nimport Vslot from '.\u002FslotTem'\nexport default defineComponent({\n    setup() {\n        return () => (\n            \u003Cdiv class={'box'}>\n                \u003CVslot v-slots={{\n                    title: () => {\n                        return \u003Cp>我是title插槽\u003C\u002Fp>\n                    },\n                    default: () => {\n                        return \u003Cp>我是default插槽\u003C\u002Fp>\n                    }\n                }} \u002F>\n            \u003C\u002Fdiv>\n        )\n    }\n})\n\n```\n\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F3e4cb0d3df611ee3f57b8ed503e1015e.png",1392,"2023-04-19T09:23:27.000Z","2026-06-27T11:08:34.303Z",{"id":137,"name":138,"slug":139},{"id":25,"name":26,"avatar":19},[401],{"id":285,"name":138,"slug":139},{"id":403,"title":404,"slug":405,"content":406,"excerpt":404,"coverImage":407,"status":17,"isPinned":18,"pinnedAt":19,"viewCount":408,"publishedAt":409,"createdAt":409,"updatedAt":410,"category":411,"author":412,"tags":413},"1dcdd755-0e46-45a3-8998-9a213fd3fcd5","Vue3 导入导出Excel","vue3-导入导出excel","## 安装\n```shell\npnpm add -S XLSX\n```\n\n\n## 方法封装\n```js\nimport * as XLSX from 'xlsx';\n\n\u002F\u002F参数说明\n\u002F\u002Fconfiguration: {\n\u002F\u002F  data: [], \u002F\u002F 导出的数据\n\u002F\u002F  head: {}, \u002F\u002F 导出的数据对应的表头\n\u002F\u002F  name: '', \u002F\u002F 导出的文件名\n\u002F\u002F  label: '', \u002F\u002F 导出的表单名\n\u002F\u002F  widthArr: [], \u002F\u002F 导出的表单列宽\n\u002F\u002F}\n\n\u002F\u002F 导出excel\nexport const ExportXlsx = (configuration) => {\n  const { data, head, name, label, widthArr } = configuration;\n\n  const list = data.map((item) => {\n    const obj = {};\n    for (const k in item) {\n      if (head[k]) {\n        obj[head[k]] = item[k];\n      }\n    }\n    return obj;\n  });\n\n  \u002F\u002F 创建工作表\n  const xLSXData = XLSX.utils.json_to_sheet(list);\n  \u002F\u002F 创建工作簿\n  const wb = XLSX.utils.book_new();\n  \u002F\u002F 将工作表放入工作簿中\n  XLSX.utils.book_append_sheet(wb, xLSXData, label);\n  xLSXData['!cols'] = [];\n  \u002F\u002F 设置列宽\n  widthArr.forEach((item) => {\n    xLSXData['!cols'].push({ wpx: item });\n  });\n\n  \u002F\u002F 生成文件并下载\n  XLSX.writeFile(wb, `${name}.xlsx`);\n};\n\n\u002F\u002F 导入excel\nexport const ImportXlsx = (e) => {\n  const file = e.target.files[0];\n  const reader = new FileReader();\n  reader.readAsArrayBuffer(file);\n  reader.onload = (e) => {\n    const data = e.target.result;\n    const workbook = XLSX.read(data, { type: 'binary', cellDates: true });\n    const wsname = workbook.SheetNames[0];\n    const outdata = XLSX.utils.sheet_to_json(workbook.Sheets[wsname]);\n    console.log(outdata);\n  };\n};\n\n```\n","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F80231a326d3846b895a7d46c99e738ce.png",1336,"2023-03-14T06:18:59.000Z","2026-06-27T11:37:23.273Z",{"id":143,"name":144,"slug":145},{"id":25,"name":26,"avatar":19},[414],{"id":214,"name":215,"slug":216},{"id":416,"title":417,"slug":418,"content":419,"excerpt":417,"coverImage":420,"status":17,"isPinned":18,"pinnedAt":19,"viewCount":421,"publishedAt":422,"createdAt":422,"updatedAt":423,"category":424,"author":428,"tags":429},"80920598-a452-4357-bf53-842b200560e8","React18入门到精通教程","react18入门到精通教程","\n## JSX实现列表渲染\n> 使用`map()`方法遍历数组，必须添加`key`属性提高性能\n```jsx\nconst songs = [\n  { id: 1, name: \\\"helo1\\\" },\n  { id: 2, name: \\\"helo2\\\" },\n  { id: 3, name: \\\"helo3\\\" },\n];\n\nfunction App() {\n  return (\n    \u003Cdiv>\n      \u003Cul>\n        {songs.map((song) => (\n          \u002F\u002F 关键：添加唯一key标识符（避免使用索引）\n          \u003Cli key={song.id}>  \n            {song.id}-{song.name}\n          \u003C\u002Fli>\n        ))}\n      \u003C\u002Ful>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n**最佳实践：**\n1. 使用`\u003Cul>`包裹列表项\n2. `key`应使用稳定唯一标识（如ID），避免数组索引\n3. 空列表处理：`{songs.length > 0 && ...}` 或 `{songs.map(...) || \u003CEmptyView\u002F>}`\n\n## JSX实现条件渲染\n### 简单逻辑\n```jsx\n\u002F\u002F 三元表达式\n{isLoggedIn ? \u003CDashboard \u002F> : \u003CLoginForm \u002F>}\n\n\u002F\u002F 逻辑短路\n{hasNotification && \u003CNotificationBadge count={5} \u002F>}\n\n\u002F\u002F 空值处理\n{userProfile?.avatar || \u003CDefaultAvatar \u002F>}\n```\n\n### 复杂逻辑\n```jsx\nconst renderContent = (type) => {\n  switch(type) {\n    case 'success': \n      return \u003CSuccessAlert \u002F>;\n    case 'error':\n      return \u003CErrorAlert \u002F>;\n    default:\n      return \u003CInfoAlert \u002F>;\n  }\n}\n\nfunction App() {\n  return (\n    \u003Cdiv className=\\\"container\\\">\n      {renderContent(status)}\n      \n      {\u002F* 另一种模式：立即执行函数 *\u002F}\n      {(() => {\n        if (isLoading) return \u003CSpinner \u002F>;\n        if (isEmpty) return \u003CEmptyState \u002F>;\n        return \u003CDataTable \u002F>;\n      })()}\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n## JSX样式处理\n### 行内样式\n```jsx\n\u002F\u002F 直接对象\n\u003Cdiv style={{ \n  color: 'white', \n  backgroundColor: 'teal',\n  padding: '1rem'\n}}>\n\n\u002F\u002F 样式对象复用\nconst alertStyle = {\n  padding: '15px',\n  borderRadius: '4px',\n  margin: '10px 0'\n};\n\nfunction Alert({ type }) {\n  return (\n    \u003Cdiv style={{\n      ...alertStyle,  \u002F\u002F 扩展运算符合并样式\n      background: type === 'error' ? '#f8d7da' : '#d4edda'\n    }}>\n      {message}\n    \u003C\u002Fdiv>\n  )\n}\n```\n\n### 类名控制（推荐）\n```css\n\u002F* styles.module.css *\u002F\n.card {\n  border: 1px solid #ddd;\n  border-radius: 8px;\n  padding: 20px;\n  box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n}\n\n.highlight {\n  background-color: #ffffe0;\n}\n```\n\n```jsx\nimport styles from '.\u002Fstyles.module.css';\n\nfunction ProductCard({ featured }) {\n  return (\n    \u003Cdiv className={`${styles.card} ${featured ? styles.highlight : ''}`}>\n      {\u002F* 内容 *\u002F}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## React 18新特性\n### 并发模式（Concurrent Mode）\n```jsx\nimport { startTransition } from 'react';\n\n\u002F\u002F 非紧急状态更新\nfunction handleSearch(query) {\n  startTransition(() => {\n    setSearchQuery(query); \u002F\u002F 可中断的渲染\n  });\n}\n```\n\n### 自动批处理（Automatic Batching）\n```jsx\n\u002F\u002F React 17及之前：两次渲染\n\u002F\u002F React 18：自动批处理，一次渲染\nfunction handleClick() {\n  setCount(c => c + 1);\n  setFlag(f => !f);\n}\n```\n\n## Redux状态管理（现代写法）\n### 安装依赖\n```bash\nnpm install @reduxjs\u002Ftoolkit react-redux\n```\n\n### 创建Store\n```js\n\u002F\u002F store.js\nimport { configureStore, createSlice } from '@reduxjs\u002Ftoolkit';\n\nconst counterSlice = createSlice({\n  name: 'counter',\n  initialState: { value: 0 },\n  reducers: {\n    increment: state => { state.value += 1 },\n    decrement: state => { state.value -= 1 },\n    incrementByAmount: (state, action) => {\n      state.value += action.payload\n    }\n  }\n});\n\n\u002F\u002F 异步操作示例\nexport const fetchUserData = () => async (dispatch) => {\n  const response = await fetch('\u002Fapi\u002Fuser');\n  dispatch(setUser(await response.json()));\n};\n\nexport const store = configureStore({\n  reducer: {\n    counter: counterSlice.reducer,\n    \u002F\u002F 其他reducer...\n  }\n});\n\nexport const { increment, decrement } = counterSlice.actions;\n```\n\n### 组件集成\n```jsx\n\u002F\u002F index.js\nimport { Provider } from 'react-redux';\nimport { store } from '.\u002Fstore';\n\nroot.render(\n  \u003CProvider store={store}>\n    \u003CApp \u002F>\n  \u003C\u002FProvider>\n);\n\n\u002F\u002F Counter.js\nimport { useSelector, useDispatch } from 'react-redux';\nimport { increment, decrement } from '.\u002Fstore';\n\nfunction Counter() {\n  const count = useSelector(state => state.counter.value);\n  const dispatch = useDispatch();\n\n  return (\n    \u003Cdiv>\n      \u003Cbutton onClick={() => dispatch(decrement())}>-\u003C\u002Fbutton>\n      \u003Cspan>{count}\u003C\u002Fspan>\n      \u003Cbutton onClick={() => dispatch(increment())}>+\u003C\u002Fbutton>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## 最佳实践总结\n1. **组件设计**：遵循单一职责原则，拆分智能组件（容器组件）和展示组件\n2. **状态管理**：\n   - 局部状态用`useState`\u002F`useReducer`\n   - 全局共享状态用Redux\n   - 避免过度使用状态提升\n3. **性能优化**：\n   - 使用`React.memo`记忆组件\n   - 使用`useCallback`\u002F`useMemo`避免不必要的重渲染\n   - 虚拟化长列表（react-window）\n4. **Hooks规范**：\n   - 避免在循环\u002F条件中使用Hook\n   - 自定义Hook以`use`前缀命名\n5. **TypeScript集成**：\n   ```tsx\n   interface UserCardProps {\n     name: string;\n     age: number;\n     onSelect: (id: string) => void;\n   }\n   \n   const UserCard: React.FC\u003CUserCardProps> = ({ name, age }) => (\n     \u003Cdiv>{name} ({age})\u003C\u002Fdiv>\n   )\n   ```\n\n下面为您完善教程，增加React路由和生命周期相关内容：\n\n## React路由管理（React Router v6）\n\n### 安装与基础配置\n```bash\nnpm install react-router-dom@6\n```\n\n### 路由基础结构\n```jsx\n\u002F\u002F index.js\nimport { BrowserRouter } from 'react-router-dom';\n\nconst root = ReactDOM.createRoot(document.getElementById('root'));\nroot.render(\n  \u003CBrowserRouter>\n    \u003CApp \u002F>\n  \u003C\u002FBrowserRouter>\n);\n\n\u002F\u002F App.js\nimport { Routes, Route, Link } from 'react-router-dom';\n\nfunction App() {\n  return (\n    \u003Cdiv>\n      \u003Cnav>\n        \u003CLink to=\\\"\u002F\\\">首页\u003C\u002FLink>\n        \u003CLink to=\\\"\u002Fabout\\\">关于\u003C\u002FLink>\n        \u003CLink to=\\\"\u002Fusers\\\">用户列表\u003C\u002FLink>\n      \u003C\u002Fnav>\n      \n      \u003CRoutes>\n        \u003CRoute path=\\\"\u002F\\\" element={\u003CHome \u002F>} \u002F>\n        \u003CRoute path=\\\"\u002Fabout\\\" element={\u003CAbout \u002F>} \u002F>\n        \u003CRoute path=\\\"\u002Fusers\\\" element={\u003CUserList \u002F>} \u002F>\n        \u003CRoute path=\\\"\u002Fusers\u002F:id\\\" element={\u003CUserDetail \u002F>} \u002F>\n        \u003CRoute path=\\\"*\\\" element={\u003CNotFound \u002F>} \u002F>\n      \u003C\u002FRoutes>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### 嵌套路由\n```jsx\n\u002F\u002F Dashboard.js\nimport { Outlet } from 'react-router-dom';\n\nfunction Dashboard() {\n  return (\n    \u003Cdiv>\n      \u003Ch2>仪表盘\u003C\u002Fh2>\n      \u003COutlet \u002F> {\u002F* 子路由渲染位置 *\u002F}\n    \u003C\u002Fdiv>\n  );\n}\n\n\u002F\u002F 路由配置\n\u003CRoutes>\n  \u003CRoute path=\\\"\u002Fdashboard\\\" element={\u003CDashboard \u002F>}>\n    \u003CRoute index element={\u003CDashboardHome \u002F>} \u002F>\n    \u003CRoute path=\\\"settings\\\" element={\u003CDashboardSettings \u002F>} \u002F>\n    \u003CRoute path=\\\"analytics\\\" element={\u003CDashboardAnalytics \u002F>} \u002F>\n  \u003C\u002FRoute>\n\u003C\u002FRoutes>\n```\n\n### 编程式导航\n```jsx\nimport { useNavigate, useParams, useLocation } from 'react-router-dom';\n\nfunction UserCard({ user }) {\n  const navigate = useNavigate();\n  \n  return (\n    \u003Cdiv onClick={() => navigate(`\u002Fusers\u002F${user.id}`)}>\n      {user.name}\n    \u003C\u002Fdiv>\n  );\n}\n\nfunction UserDetail() {\n  const { id } = useParams(); \u002F\u002F 获取URL参数\n  const location = useLocation(); \u002F\u002F 获取位置对象\n  \n  return (\n    \u003Cdiv>\n      \u003Ch2>用户ID: {id}\u003C\u002Fh2>\n      \u003Cp>当前路径: {location.pathname}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### 路由守卫（认证保护）\n```jsx\nimport { Navigate } from 'react-router-dom';\n\nfunction ProtectedRoute({ children }) {\n  const { isAuthenticated } = useAuth();\n  \n  if (!isAuthenticated) {\n    return \u003CNavigate to=\\\"\u002Flogin\\\" replace \u002F>;\n  }\n  \n  return children;\n}\n\n\u002F\u002F 使用\n\u003CRoute \n  path=\\\"\u002Fdashboard\\\" \n  element={\n    \u003CProtectedRoute>\n      \u003CDashboard \u002F>\n    \u003C\u002FProtectedRoute>\n  } \n\u002F>\n```\n\n## React生命周期\n\n### 类组件生命周期方法\n\n```jsx\nclass LifecycleDemo extends React.Component {\n  \u002F\u002F 1. 初始化阶段\n  constructor(props) {\n    super(props);\n    this.state = { count: 0 };\n    console.log('Constructor');\n  }\n\n  \u002F\u002F 2. 挂载阶段\n  componentDidMount() {\n    console.log('Component did mount');\n    \u002F\u002F 适合进行API调用、事件订阅\n    this.timer = setInterval(() => {\n      this.setState(prev => ({ count: prev.count + 1 }));\n    }, 1000);\n  }\n\n  \u002F\u002F 3. 更新阶段\n  shouldComponentUpdate(nextProps, nextState) {\n    console.log('Should component update?');\n    return nextState.count !== this.state.count;\n  }\n\n  componentDidUpdate(prevProps, prevState) {\n    console.log('Component did update');\n  }\n\n  \u002F\u002F 4. 卸载阶段\n  componentWillUnmount() {\n    console.log('Component will unmount');\n    \u002F\u002F 清理操作\n    clearInterval(this.timer);\n  }\n\n  render() {\n    console.log('Render');\n    return \u003Cdiv>Count: {this.state.count}\u003C\u002Fdiv>;\n  }\n}\n```\n\n### 函数组件生命周期（Hooks实现）\n\n```jsx\nimport { useState, useEffect } from 'react';\n\nfunction FunctionLifecycle() {\n  const [count, setCount] = useState(0);\n  const [data, setData] = useState(null);\n\n  \u002F\u002F 相当于componentDidMount + componentDidUpdate\n  useEffect(() => {\n    console.log('每次渲染后执行');\n  });\n\n  \u002F\u002F 相当于componentDidMount\n  useEffect(() => {\n    console.log('组件挂载后执行');\n    \n    \u002F\u002F 数据获取\n    fetch('\u002Fapi\u002Fdata')\n      .then(res => res.json())\n      .then(setData);\n    \n    \u002F\u002F 相当于componentWillUnmount\n    return () => {\n      console.log('组件卸载前执行');\n    };\n  }, []); \u002F\u002F 空依赖数组\n\n  \u002F\u002F 依赖变化时执行\n  useEffect(() => {\n    console.log('count变化时执行:', count);\n    \n    document.title = `Count: ${count}`;\n    \n    return () => {\n      console.log('清理count效果');\n    };\n  }, [count]); \u002F\u002F count依赖\n\n  return (\n    \u003Cdiv>\n      \u003Cp>Count: {count}\u003C\u002Fp>\n      \u003Cbutton onClick={() => setCount(c => c + 1)}>增加\u003C\u002Fbutton>\n      {data && \u003Cpre>{JSON.stringify(data, null, 2)}\u003C\u002Fpre>}\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n### 生命周期阶段对比\n\n| 阶段 | 类组件方法 | 函数组件Hook |\n|------|------------|--------------|\n| **挂载** | constructor | useState初始化 |\n|       | render      | 函数体执行    |\n|       | componentDidMount | useEffect(() => {}, []) |\n| **更新** | shouldComponentUpdate | React.memo, useMemo |\n|       | render      | 函数体执行    |\n|       | componentDidUpdate | useEffect(() => {}) |\n| **卸载** | componentWillUnmount | useEffect返回函数 |\n| **错误处理** | componentDidCatch | 暂无直接等效，需错误边界组件 |\n\n### 现代React开发建议\n\n1. **优先使用函数组件+Hooks**：\n   - 90%的场景可替代类组件\n   - 更简洁的代码结构\n   - 更好的逻辑复用\n\n2. **关键生命周期替代**：\n   - `componentDidMount` → `useEffect(() => {}, [])`\n   - `componentDidUpdate` → `useEffect(() => {})` 或带依赖的 `useEffect`\n   - `componentWillUnmount` → `useEffect(() => { return cleanup }, [])`\n   - `shouldComponentUpdate` → `React.memo` 或 `useMemo`\n\n3. **数据获取最佳实践**：\n```jsx\nuseEffect(() => {\n  let isMounted = true;\n  \n  const fetchData = async () => {\n    try {\n      const result = await api.getData();\n      if (isMounted) setData(result);\n    } catch (error) {\n      if (isMounted) setError(error);\n    }\n  };\n  \n  fetchData();\n  \n  return () => {\n    isMounted = false; \u002F\u002F 避免组件卸载后设置状态\n  };\n}, []);\n```\n\n## 路由与生命周期整合示例\n\n```jsx\nfunction UserProfile() {\n  const { id } = useParams();\n  const [user, setUser] = useState(null);\n  const [loading, setLoading] = useState(true);\n\n  useEffect(() => {\n    let isActive = true;\n    \n    const fetchUser = async () => {\n      try {\n        setLoading(true);\n        const data = await fetchUserById(id);\n        if (isActive) {\n          setUser(data);\n          setLoading(false);\n        }\n      } catch (error) {\n        if (isActive) {\n          setError(error.message);\n          setLoading(false);\n        }\n      }\n    };\n    \n    fetchUser();\n    \n    return () => {\n      isActive = false; \u002F\u002F 清理效果\n    };\n  }, [id]); \u002F\u002F id变化时重新获取\n\n  if (loading) return \u003CSpinner \u002F>;\n  \n  return (\n    \u003Cdiv>\n      \u003Ch2>{user.name}\u003C\u002Fh2>\n      \u003Cp>Email: {user.email}\u003C\u002Fp>\n    \u003C\u002Fdiv>\n  );\n}\n```\n\n## 完整项目结构建议\n\n```\nsrc\u002F\n├── components\u002F      # 通用UI组件\n├── pages\u002F           # 页面组件\n├── layouts\u002F         # 布局组件\n├── hooks\u002F           # 自定义Hooks\n├── store\u002F           # Redux状态\n│   ├── slices\u002F\n│   └── store.js\n├── services\u002F        # API服务\n├── routers\u002F         # 路由配置\n├── utils\u002F           # 工具函数\n├── assets\u002F          # 静态资源\n└── App.js           # 主应用组件\n```\n\n这些新增内容涵盖了React路由的现代用法（v6版本）以及React生命周期的详细解释，包括类组件和函数组件的实现方式对比。同时还提供了路由与生命周期整合的实际示例，帮助开发者理解如何在真实项目中应用这些概念。","https:\u002F\u002Fcdn.xiaolong0418.com\u002Fmyblog\u002Fimages\u002F46e205aa1bd33d1bb7201019fc2fdf43.png",1148,"2023-02-21T02:01:52.000Z","2026-06-27T11:00:41.557Z",{"id":425,"name":426,"slug":427},"f1701085-b8c1-413a-8750-58e7a0a33832","React","react",{"id":25,"name":26,"avatar":19},[430],{"id":256,"name":257,"slug":258}]