• 售前

  • 售后

热门帖子
入门百科

webpack高级设置与优化详解

[复制链接]
小雨敲窗y 显示全部楼层 发表于 2021-8-16 11:31:04 |阅读模式 打印 上一主题 下一主题
所谓打包多页面,就是同时打包出多个 html 页面,打包多页面也是利用 html-webpack-plugin,只不过,在引入插件的时间是创建多个插件对象,因为一个html-webpack-plugin 插件对象只能打包出一个 html 页面。如:
  1. module.exports = {
  2. entry: {
  3.   index: "./src/index.js", // 指定打包输出的chunk名为index
  4.   foo: "./src/foo.js" // 指定打包输出的chunk名为foo
  5. },
  6. plugins: [
  7.   new HtmlWebpackPlugin({
  8.    template: "./src/index.html", // 要打包输出哪个文件,可以使用相对路径
  9.    filename: "index.html", // 打包输出后该html文件的名称
  10.    chunks: ["index"] // 数组元素为chunk名称,即entry属性值为对象的时候指定的名称,index页面只引入index.js
  11.   }),
  12.   new HtmlWebpackPlugin({
  13.    template: "./src/index.html", // 要打包输出哪个文件,可以使用相对路径
  14.    filename: "foo.html", // 打包输出后该html文件的名称
  15.    chunks: ["foo"] // 数组元素为chunk名称,即entry属性值为对象的时候指定的名称,foo页面只引入foo.js
  16.   }),
  17. ]
  18. }
复制代码
打包多页面时,关键在于 chunks 属性的设置,因为在没有设置 chunks 属性的环境下,打包输出的 index.html 和 foo.html 都会同时引入 index.js 和 foo.js。
以是必须设置 chunks 属性,来指定打包输出后的 html 文件中要引入的输出模块,数组的元素为 entry 属性值为对象的时间指定的 chunk 名,如上设置,才华实现,index.html 只引入 index.js,foo.html 只引入 foo.js 文件
二、设置 source-map
source-map 就是源码映射,告急是为了方便代码调试,因为我们打包上线后的代码会被压缩等处置处罚,导致全部代码都被压缩成了一行,如果代码中出现错误,那么浏览器只会提示堕落位置在第一行,这样我们无法真正知道堕落地方在源码中的具体位置。webpack 提供了一个 devtool 属性来设置源码映射。
  1. let foo = 1;
  2. console.lg(`console对象的方法名log写成了lg`); // 源文件第二行出错
  3. index.js:1 Uncaught TypeError: console.lg is not a function
  4. at Object.<anonymous> (index.js:1)
  5. at o (index.js:1)
  6. at Object.<anonymous> (index.js:1)
  7. at o (index.js:1)
  8. at index.js:1
  9. at index.js:1
复制代码
源码中堕落的位置明明是第二行代码,而浏览器中提示的错误确着实第一行,以是如果代码很复杂的环境下,我们就无法找到堕落的具体位置
devtool 常见的有 6 种设置:
1、source-map: 这种模式会产生一个.map文件,堕落了会提示具体的行和列,文件内里保留了打包后的文件与原始文件之间的映射关系,打包输出文件中会指向生成的.map文件,告诉js引擎源码在那边,由于源码与.map文件分离,以是需要浏览器发送哀求去获取.map文件,常用于生产环境,如:
  1. //# sourceMappingURL=index.js.map
复制代码
2、eval: 这种模式打包速率最快,不会生成.map文件,会利用eval将模块包裹,在末了加入sourceURL,常用于开发环境,如:
  1. //# sourceURL=webpack:///./src/index.js
复制代码
3、eval-source-map: 每个 module 会通过 eval() 来执行,而且生成一个 DataUrl 情势的 SourceMap (即 base64 编码情势内嵌到 eval 语句末了), 但是不会生成 .map 文件,可以淘汰网络哀求*,但是打包文件会非常大*。
  1. //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9zcmMvaW5kZXguanM/YjYzNSJdLCJuYW1lcyI6WyJmb28iLCJjb25zb2xlIiwibGciXSwibWFwcGluZ3MiOiJBQUFBLElBQUlBLEdBQUcsR0FBRyxDQUFWO0FBQ0FDLE9BQU8sQ0FBQ0MsRUFBUix1RSxDQUFxQyIsImZpbGUiOiIuL3NyYy9pbmRleC5qcy5qcyIsInNvdXJjZXNDb250ZW50IjpbImxldCBmb28gPSAxO1xuY29uc29sZS5sZyhgY29uc29sZeWvueixoeeahOaWueazleWQjWxvZ+WGmeaIkOS6hmxnYCk7IC8vIOa6kOaWh+S7tuesrOS6jOihjOWHuumUmVxuIl0sInNvdXJjZVJvb3QiOiIifQ==
  2. //# sourceURL=webpack-internal:///./src/index.js
复制代码
4、cheap-source-map: 加上 cheap,就只会提示到第几行报错,少了列信息提示,同时不会对引入的库做映射,可以进步打包性能,但是会产生 .map 文件。
5、cheap-module-source-map: 和 cheap-source-map 相比,加上了 module,就会对引入的库做映射,而且也会产生 .map 文件,用于生产环境。
6、cheap-module-eval-source-map: 常用于开发环境,利用 cheap 模式可以大幅进步 souremap 生成的服从,加上 module 同时会对引入的库做映射,eval 进步打包构建速率,而且不会产生 .map 文件淘汰网络哀求。
凡是带 eval 的模式都不能用于生产环境,因为其不会产生 .map 文件,会导致打包后的文件变得非常大。通常我们并不关心列信息,以是都会利用 cheap 模式,但是我们也照旧需要对第三方库做映射,以便精准找到错误的位置。
三、watch 和 watchOptions 设置
webpack 可以监听文件变革,当它们修改后会重新编译,如果需要开启该功能,那么需要将 watch 设置为 true,具体监听设置通过 watchOptions 举行相应的设置。
  1. module.exports = {
  2. watch: true,
  3. watchOptions: {
  4.   poll: 1000, // 每隔一秒轮询一次文件是否发生变化
  5.   aggregateTimeout: 1000, // 当第一个文件更改,会在重新构建前增加延迟。这个选项允许 webpack 将这段时间内进行的任何其他更改都聚合到一次重新构建里
  6.   ignored: /node_modules/ // 排除一些文件的监听
  7. }
  8. }
复制代码
四、三个常见小插件的利用
1、clean-webpack-plugin: 其作用就是每次打包前先先将输出目录中的内容举行清空,然后再将打包输出的文件输出到输出目录中。
  1. const {CleanWebpackPlugin} = require("clean-webpack-plugin");
  2. module.exports = {
  3. plugins: [
  4.   new CleanWebpackPlugin() // 打包前清空输出目录
  5. ]
  6. }
复制代码
需要留意的是,require("clean-webpack-plugin)的效果是一个对象而不是类,这个对象中的 CleanWebpackPlugin 属性才是一个类,我们就是用这个类去创建插件对象。
2、copy-webpack-plugin: 其作用就是打包的时间带上一些 readMe.md、history.md 等等一起输出到输出目录中。
  1. module.exports = {
  2. plugins: [
  3.   new CopyWebpackPlugin([
  4.    {
  5.     from: "./readMe.md", // 将项目根目录下的readMe.md文件一起拷贝到输出目录中
  6.     to: "" // 属性值为空字符串则表示是输出目录
  7.    }
  8.   ])
  9. ]
  10. }
复制代码
3、BannerPlugin: 其作用就是在打包输出的 js 文件的头部添加一些文字注释,比如版权说明等等,BannerPlugin 是 webpack 内置的插件,如:
  1. module.exports = {
  2. plugins: [
  3.   new webpack.BannerPlugin("Copyright © 2019") // 在js文件头部添加版权说明
  4. ]
  5. }
复制代码
五、webpack 跨域标题
为什么 webpack 会存在跨域标题?因为 webpack 打包的是前端代码,其终极会被摆设到前端服务器上,而前后端代码通常摆设在差别的服务器上,纵然是摆设在同一个服务器上,所利用的端口也是不一样的,当前端代码通过 ajax 等本领向后端服务器获取数据的时间,由于前后端代码不在同一个域中,故存在跨域标题。
比如,我们通过 webpack 的 devServer 来运行摆设我们的前端应用代码,devServer 启动在 8080 端口上,而前端应用代码中会通过 ajax 哀求后端数据,后端服务器启动在 3000 端口上。
  1. // index.js
  2. const xhr = new XMLHttpRequest();
  3. // xhr.open("get", "http://localhost:3000/api/test"); // 由于跨域问题无法直接访问到http://localhost:3000下的资源
  4. xhr.open("get", "/api/test"); // 本来是要访问http://localhost:3000/api/test
  5. xhr.onreadystatechange = () => {
  6. if (xhr.readyState === 4) {
  7.   console.log(xhr.responseText);
  8. }
  9. }
  10. xhr.send();
复制代码
由于前端代码是运行在浏览器中的,如果在前端代码中直接通过 ajax 向http://localhost:3000/api/test 发起哀求获取数据,那么由于浏览器同源计谋的影响,会存在跨域的标题,以是必须访问 /api/test。但是这样访问又会出现 404 标题,因为实在访问的是 http://localhost:8080/api/test,8080 服务器上是没有该资源的,解决办法就是通过 devServer 设置一个代理服务器
  1. module.exports = {
  2. devServer: {
  3.   proxy: {
  4.    "/api": "http://localhost:3000" // 路径以/api开头则代理到localhost:3000上
  5.   }
  6. }
  7. }
复制代码
访问 http://localhost:8080/api/test
就会被代理到http://localhost:3000/api/test 上,proxy 还支持路径的重写,如果 3000 端口服务器上并没有 /api/test 路径,只有 /test 路径,那么就可以对路径举行重写,将 /api 更换掉
  1. module.exports = {
  2. devServer: {
  3.   proxy: {
  4.    "/api": {
  5.     target: "http://localhost:3000",
  6.     pathRewrite: {"/api": ""} // 将/api替换掉
  7.    }
  8.   }
  9. }
  10. }
复制代码
访问 http://localhost:8080/api/test
就会被代理到 http://localhost:3000/test 上
如果前端只是想 mock 一些数据,并不需要真正的去访问后台服务器,那么我们可以通过 devServer 提供的 before 钩子函数获取到内置的服务器对象举行处置处罚哀求,这个内置的服务器对象就是 webpack 的 devServer 即 8080 端口的 server,因为是在同一个服务器中哀求数据以是也不会出现跨域标题。
  1. before(app) { // 此app即webpack的devServer
  2.    app.get("/api/test", (req, res, next) => {
  3.     res.json({name: "even"});
  4.    })
  5.   }
复制代码
我们还可以不通过 webpack 提供的 devServer 来启动 webpack,而是利用自己服务器来启动 webapck。
  1. const express = require("express");
  2. const app = express();
  3. const webpack = require("webpack"); // 引入webpack
  4. const config = require("./webpack.config.js"); // 引入配置文件
  5. const compiler = webpack(config); // 创建webpack的编译器
  6. const middleWare = require("webpack-dev-middleware"); //引入webpack的中间件
  7. app.use(middleWare(compiler)); // 将compiler编译器交给中间件处理
  8. app.get("/api/test", (req, res, next) => {
  9. res.json({name: "lhb"});
  10. });
  11. app.listen(3000);
复制代码
通过自界说服务器启动 webpack,这样 webpack 中的前端代码哀求数据就和服务器的资源在同一个域中了。
六、resolve 设置
resolve 用于设置模块的解析相关参数的,其属性值为一个对象。
1、modules: 告诉 webpack 解析模块时应该搜索的目录,即 require 或 import 模块的时间,只写模块名的时间,到那边去找,其属性值为数组,因为可设置多个模块搜索路径,其搜索路径必须为绝对路径,比如,src 目录下面有一个 foo.js 文件和 index.js 文件:
  1. // index.js
  2. const foo = require("./foo"); // 必须写全foo.js模块的路径
  3. // const foo = require("foo"); // resolve.modules中配置了模块解析路径为.src目录,则可用只写foo即可搜索到foo.js模块
  4. console.log(foo);
  5. module.exports = {
  6. resolve: {
  7.   modules: [path.resolve(__dirname, "./src/"), "node_modules"]
  8. },
  9. }
复制代码
由于 resolve.modules 中设置了 ./src 目录作为模块的搜索目录,以是 index.js 中可以只写模块名即可搜索到 foo.js 模块
2、alias: 用于给路径大概文件取别名,当 import 大概 require 的模块的路径非常长时,我们可以给该模块的路径大概整个路径名+文件名都设置成一个别名,然后直接引入别名即可找到该模块,比如,有一个模块位置非常深
  1. // const foo = require("./a/b/c/foo"); // foo.js在./src/a/b/c/foo.js
  2. // const foo = require("foo"); // foo被映射成了./src/a/b/c/foo.js文件
  3. const foo = require("bar/foo.js"); // bar被映射成了./src/a/b/c/路径
  4. console.log(foo);
  5. module.exports = {
  6. resolve: {
  7.   alias: {
  8.    "foo": path.resolve(__dirname, "./src/a/b/c/foo.js"),
  9.    "bar": path.resolve(__dirname, "./src/a/b/c/")
  10.   }
  11. },
  12. }
复制代码
需要留意的就是,alias 可以映射文件也可以映射路径
3、mainFields: 我们的 package.json 中可以有多个字段,用于决定优先利用哪个字段来导入模块,比如 bootstrap 模块中含有 js 也含有 css,其 package.json 文件中 main 字段对应的是"dist/js/bootstrap",style 字段中对应的是"dist/css/bootstrap.css",我们可以通过设置 mainFields 字段来改变默认引入,如:
  1. module.exports = {
  2. resolve: {
  3.   mainFields: ["style", "main"]
  4. },
  5. }
复制代码
4、extensions: 用于设置引入模块的时间,如果没有写模块后缀名,webpack 会自动添加后缀去查找,extensions 就是用于设置自动添加后缀的顺序,如:
  1. module.exports = {
  2. resolve: {
  3.   extensions: ["js", "vue"]
  4. },
  5. }
复制代码
如果项目中引入了 foo 模块,require("./foo"),其会优先找 ./foo.js,如果没有找到 ./foo.js 则会去找 ./foo.vue 文件
七、设置环境变量
设置环境变量需要用到 webpack 提供的一个内置插件 DefinePlugin 插件,其作用是将一个字符串值设置为全局变量,如:
  1. module.exports = {
  2. plugins: [
  3.   new webpack.DefinePlugin({
  4.    DEV_MODE: JSON.stringify('development') // 将'development'设置为全局变量DEV_MODE
  5.   }),
  6. ]
  7. }
复制代码
这样设置之后任何一个模块中都可以直接利用 DEV_MODE 变量了,而且其值为'development',与 ProvidePlugin 有点相似,ProvidePlugin 是将一个模块注入到全部模块中,实现模块不需要引入即可直接利用。
八、webpack 优化
1、noParse: 该设置是作为 module 的一个属性值,即不解析某些模块,所谓不解析,就是不去分析某个模块中的依靠关系,即不去管某个文件是否 import(依靠)了某个文件,对于一些独立的库,比如 jquery,其根本不存在依靠关系,jquery 不会去引入其他的库(要根据自己对某个模块的了解去判定是否要解析该模块),以是我们可以让 webpack 不去解析 jquery 的依靠关系,进步打包速率,如:
  1. module.exports = {
  2. module: {
  3.   noParse:/jquery/,//不去解析jquery中的依赖库
  4. }
  5. }
复制代码
noParse 是 module 设置中的一个属性,其属性值为一个正则表达式,填入不被解析的模块名称。
为了更清晰的展示 noParse 的作用,假设我们在入口文件 index.js 中引入 bar.js 模块,同时这个 bar.js 模块中也引入了 foo.js 模块,foo.js 不再依靠其他模块了,那么在不利用 noParse 的环境下,webpack 打包的时间,会先去分析 index.js 模块,发现其引入了 bar.js 模块,然后接着分析 bar.js 模块,发现其引入了 foo.js 模块,接着分析 foo.js 模块。
  1. Entrypoint index = index.js
  2. [./src/bar.js] 55 bytes {index} [built]
  3. [./src/foo.js] 21 bytes {index} [built]
  4. [./src/index.js] 81 bytes {index} [built]
复制代码
而此时如果利用了 noParse: /bar/,那么 webpack 打包的时间,会先去分析 index.js 模块,发现其引入了 bar.js 模块,但是由于 noParse 的作用,将不再继承解析 bar.js 模块了,即不会去分析 bar.js 中引入的 foo.js 模块了。
  1. Entrypoint index = index.js
  2. [./src/bar.js] 55 bytes {index} [built]
  3. [./src/index.js] 81 bytes {index} [built]
复制代码
2、exclude: 在 loader 中利用 exclude 扫除对某些目录中的文件处置处罚,即引入指定目录下的文件时间,不利用对应的 loader 举行处置处罚,exclude 是 loader 设置中的一个属性,属性值为正则表达式,如:
  1. module.exports = {
  2. module: {
  3.   rules: [
  4.    {
  5.     test: /.js$/,
  6.     use: [
  7.      {
  8.       loader: "babel-loader",
  9.       options: {
  10.        presets: ["@babel/preset-env"],
  11.        plugins: ["@babel/plugin-transform-runtime"]
  12.       }
  13.      }
  14.     ],
  15.     exclude: /node_modules/
  16.    }
  17.   ]
  18. }
  19. }
复制代码
3、利用 IgnorePlugin 来忽略某个模块中某些目录中的模块引用,比如在引入某个模块的时间,该模块会引入大量的语言包,而我们不会用到那么多语言包,如果都打包进项目中,那么就会影响打包速率和终极包的大小,然后再引入需要利用的语言包即可,如:
项目根目录下有一个 time 包,其中有一个 lang 包,lang 包中包罗了各种语言输出对应时间的 js 文件,time 包下的 index.js 会引入 lang 包下全部的 js 文件,那么当我们引入 time 模块的时间,就会将 lang 包下的全部 js 文件都打包进去,添加如下设置:
  1. const webpack = require("webpack");
  2. module.exports = {
  3. plugins: [
  4.   new webpack.IgnorePlugin(/lang/, /time/)
  5. ]
  6. }
复制代码
引入 time 模块的时间,如果 time 模块中引入了其中的 lang 模块中的内容,那么就忽略掉,即不引入 lang 模块中的内容,需要留意的是,这 /time/ 只是匹配文件夹和 time 模块的具体目录位置无关,即只要是引入了目录名为 time 中的内容就会生效。
4、利用 HappyPack:由于在打包过程中有大量的文件需要交个 loader 举行处置处罚,包罗解析和转换等操纵,而由于 js 是单线程的,以是这些文件只能一个一个地处置处罚,而 HappyPack 的工作原理就是充分发挥 CPU 的多核功能,将任务分解给多个子进程去并发执行,子进程处置处罚完后再将效果发送给主进程,happypack 告急起到一个任务劫持的作用,在创建 HappyPack 实例的时间要传入对应文件的 loader,即 use 部分,loader 设置中将利用颠末 HappyPack 包装后的 loader 举行处置处罚,如:
  1. const HappyPack = require("happypack"); // 安装并引入happypack模块
  2. module.exports = {
  3. plugins: [
  4.   new HappyPack({ // 这里对处理css文件的loader进行包装
  5.    id: "css",// 之前的loader根据具体的id进行引入
  6.    use: ["style-loader","css-loader"],
  7.    threads: 5 // 设置开启的进程数
  8.   })
  9. ],
  10. module: {
  11.   rules: [
  12.    {
  13.     test: /.css$/, // 匹配以.css结尾的文件
  14.     use: ["happypack/loader?id=css"] //根据happypack实例中配置的id引入包装后的laoder,这里的happyPack的h可以大写也可以小写
  15.    }
  16.   ]
  17. }
  18. }
复制代码
webpack 要打包的文件非常多的时间才需要利用 happypack 举行优化,因为开启多进程也是需要耗时间的,以是文件少的时间,利用 happypack 返回更耗时
5、抽离公共模块: 对于多入口环境,如果某个或某些模块,被两个以上文件所依靠,那么可以将这个模块单独抽离出来,不需要将这些公共的代码都打包进每个输出文件中,这样会造成代码的重复和流量的浪费,即如果有两个入口文件 index.js 和 other.js,它们都依靠了 foo.js,那么如果不抽离公共模块,那么 foo.js 中的代码都会打包进终极输出的 index.js 和 other.js 中去,即有两份 foo.js 了。抽离公共模块也很简单,直接在 optimization 中设置即可,如:
  1. module.exports = {
  2.   splitChunks: { // 分割代码块,即抽离公共模块
  3.    cacheGroups: { // 缓存组
  4.     common: { // 组名为common可自定义
  5.      chunks: "initial",
  6.      minSize: 0, // 文件大小为0字节以上才抽离
  7.      minChunks: 2, // 被引用过两次才抽离
  8.      name: "common/foo", // 定义抽离出的文件的名称
  9.     }
  10.    }
  11.   }
  12. }
复制代码
这样就会将公共的 foo.js 模块抽离到 common 目录下 foo.js 中了,但是如果我们也有多个文件依靠了第三方模块如 jquery,如果按以上设置,那么 jquery 也会被打包进 foo.js 中,会导致代码杂乱,以是我们盼望将 jquery 单独抽出来,即与 foo.js 分开,我们可以复制一份以上设置,并通过设置抽离代码权重的方式来实现,即优先抽离出 jquery,如:
  1. module.exports = {
  2.   splitChunks: { // 分割代码块,即抽离公共模块
  3.    cacheGroups: { // 缓存组
  4.     common: { // 组名为common可自定义
  5.      chunks: "initial",
  6.      minSize: 0, // 文件大小为0字节以上才抽离
  7.      minChunks: 2, // 被引用过两次才抽离
  8.      name: "common/foo", // 定义抽离出的文件的名称
  9.     },
  10.     verdor: {
  11.      test: /node_modules/,
  12.      priority: 1, // 设置打包权重,即优先抽离第三方模块
  13.      chunks: "initial",
  14.      minSize: 0, // 文件大小为0字节以上才抽离
  15.      minChunks: 2, // 被引用过两次才抽离
  16.      name: "common/jquery", // 定义抽离出的文件的名称
  17.     }
  18.    }
  19.   }
  20. }
复制代码
这样就会在 common 目录下同时抽离出 foo.js 和 jquery.js 了,需要留意的是,代码的抽离必须是该模块没有被扫除打包,即该模块会被打包进输出 bundle 中,如果第三方模块已经通过 externals 扫除打包,则以上 vendor 设置无效。
6、按需加载,即在需要利用的时间才打包输出,webpack 提供了 import() 方法,传入要动态加载的模块,来动态加载指定的模块,当 webpack 遇到 import()语句的时间,不会立刻去加载该模块,而是在用到该模块的时间,再去加载,也就是说打包的时间会一起打包出来,但是在浏览器中加载的时间并不会立刻加载,而是比及用到的时间再去加载,比如,点击按钮后才会加载某个模块,如:
  1. const button = document.createElement("button");
  2. button.innerText = "点我"
  3. button.addEventListener("click", () => { // 点击按钮后加载foo.js
  4. import("./foo").then((res) => { // import()返回的是一个Promise对象
  5.   console.log(res);
  6. });
  7. });
  8. document.body.appendChild(button);
复制代码
从中可以看到,import() 返回的是一个 Promise 对象,其告急就是利用 JSONP 实现动态加载,返回的 res 效果差别的 export 方式会有差别,如果利用的 module.exports 输出,那么返回的 res 就是 module.exports 输出的效果;如果利用的是 ES6 模块输出,即 export default 输出,那么返回的 res 效果就是 res.default,如:
  1. // ES6模块输出,res结果为
  2. {default: "foo", __esModule: true, Symbol(Symbol.toStringTag): "Module"}
复制代码
7、开启模块热更新: 模块热更新可以做到在不刷新网页的环境下,更新修改的模块,只编译变革的模块,而不消全部模块重新打包,大大进步开发服从,在未开启热更新的环境下,每次修改了模块,都会重新打包。
要开启模块热更新,那么只需要在 devServer 设置中添加 hot:true 即可。固然仅仅开启模块热更新是不敷的,我们需要做一些类似监听的操纵,当监听的模块发生变革的时间,重新加载该模块并执行,如:
  1. module.exports = {
  2. devServer: {
  3.   hot: true // 开启热更新
  4. }
  5. }
  6. ----------
  7. import foo from "./foo";
  8. console.log(foo);
  9. if (module.hot) {
  10. module.hot.accept("./foo", () => { // 监听到foo模块发生变化的时候
  11.   const foo = require("./foo"); // 重新引入该模块并执行
  12.   console.log(foo);
  13. });
  14. }
复制代码
更多关于webpack设置车优化的文章请点击下面的相关文章

帖子地址: 

回复

使用道具 举报

分享
推广
火星云矿 | 预约S19Pro,享500抵1000!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

草根技术分享(草根吧)是全球知名中文IT技术交流平台,创建于2021年,包含原创博客、精品问答、职业培训、技术社区、资源下载等产品服务,提供原创、优质、完整内容的专业IT技术开发社区。
  • 官方手机版

  • 微信公众号

  • 商务合作