普通视图

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

Webpack Loader 执行机制

作者 ak啊
2025年4月1日 17:28

一、Loader 链式调用机制

Loader 的执行分为 Pitch 阶段Normal 阶段,两者共同构成链式调用逻辑。


1. Pitch 阶段
  • 执行顺序:从左到右(与 Normal 阶段相反)。
  • 核心作用:拦截机制。如果某个 Loader 的 pitch 方法返回非 undefined 值,直接跳过后续 Loader,进入 Normal 阶段的逆向执行。
  • 伪代码逻辑
    const result = loaderA.pitch(remainingRequest, previousRequest, data);
    if (result !== undefined) {
      // 跳过后续 Loader,进入 Normal 阶段逆向执行
    }
    
2. Normal 阶段
  • 执行顺序:从右到左。
  • 核心作用:实际处理文件内容,上一个 Loader 的输出是下一个 Loader 的输入。

二、源码转换流程(runLoaders 核心逻辑)

Webpack 使用 loader-runner 模块处理 Loader 链。以下是简化后的源码分析:

关键源码:runLoaders 函数(简化版)
function runLoaders(resource, loaders, context, callback) {
  const loaderContext = context || {};
  let loaderIndex = 0; // 当前执行的 Loader 索引
  let processOptions = {
    resourceBuffer: null,
    readResource: fs.readFile.bind(fs)
  };

  // 迭代执行 Pitch 阶段
  iteratePitchingLoaders(processOptions, loaderContext, (err, result) => {
    if (err) return callback(err);
    callback(null, ...result);
  });

  function iteratePitchingLoaders(options, loaderContext, callback) {
    if (loaderIndex >= loaders.length) {
      // 所有 Pitch 执行完毕,读取资源
      return processResource(options, loaderContext, callback);
    }

    const currentLoader = loaders[loaderIndex];
    const pitchFn = currentLoader.pitch;

    loaderIndex++; // 移动到下一个 Loader

    if (!pitchFn) {
      // 没有 pitch 方法,继续下一个 Loader
      return iteratePitchingLoaders(options, loaderContext, callback);
    }

    // 执行当前 Loader 的 pitch 方法
    pitchFn.call(
      loaderContext,
      loaderContext.remainingRequest,
      loaderContext.previousRequest,
      (currentLoader.data = {})
    ), (err, ...args) => {
      if (args.length > 0) {
        const hasResult = args.some(arg => arg !== undefined);
        if (hasResult) {
          // Pitch 返回结果,跳过后续 Loader,逆向执行 Normal
          loaderIndex--;
          iterateNormalLoaders(options, loaderContext, args, callback);
          return;
        }
      }
      // 继续下一个 Pitch
      iteratePitchingLoaders(options, loaderContext, callback);
    });
  }

  function processResource(options, loaderContext, callback) {
    // 读取原始资源内容
    options.readResource(loaderContext.resource, (err, buffer) => {
      const resourceBuffer = buffer;
      iterateNormalLoaders(options, loaderContext, [resourceBuffer], callback);
    });
  }

  function iterateNormalLoaders(options, loaderContext, args, callback) {
    if (loaderIndex < 0) {
      // 所有 Normal 阶段完成
      return callback(null, args);
    }

    const currentLoader = loaders[loaderIndex];
    const normalFn = currentLoader.normal || currentLoader;

    loaderIndex--; // 逆向执行

    // 执行当前 Loader 的 Normal 方法
    normalFn.call(loaderContext, args[0], (err, ...returnArgs) => {
      if (err) return callback(err);
      iterateNormalLoaders(options, loaderContext, returnArgs, callback);
    });
  }
}

三、执行流程详解

  1. Pitch 阶段从左到右执行

    • 依次调用每个 Loader 的 pitch 方法。
    • 若某个 pitch 返回结果,跳过后续 Loader,直接进入 Normal 阶段。
  2. 读取资源文件

    • 若所有 pitch 均未拦截,读取原始文件内容。
  3. Normal 阶段从右到左执行

    • 将资源内容传递给最后一个 Loader 处理,结果逆向传递。

四、典型使用案例

案例:自定义 Loader 链观察执行顺序

Loader 配置

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.txt$/,
        use: [
          './loaders/loaderA.js',
          './loaders/loaderB.js',
          './loaders/loaderC.js'
        ]
      }
    ]
  }
};

Loader 实现

// loaderA.js
module.exports = function(source) {
  console.log('[Normal A]');
  return source + '-A';
};
module.exports.pitch = function() {
  console.log('[Pitch A]');
};

// loaderB.js
module.exports = function(source) {
  console.log('[Normal B]');
  return source + '-B';
};
module.exports.pitch = function() {
  console.log('[Pitch B]');
  // 返回非 undefined 值,拦截后续 Loader
  return '拦截内容';
};

// loaderC.js
module.exports = function(source) {
  console.log('[Normal C]');
  return source + '-C';
};
module.exports.pitch = function() {
  console.log('[Pitch C]');
};

执行结果

[Pitch A]
[Pitch B]  // B 的 pitch 返回拦截内容,跳过后续 Pitch
[Normal B] // 进入 Normal 阶段,从 B 开始逆向执行
[Normal A]
最终结果: "拦截内容-B-A"

五、关键总结

  1. Pitch 拦截:通过 pitch 方法提前返回结果,优化构建流程。
  2. 执行方向
    • Pitch:从左到右。
    • Normal:从右到左(若未拦截)。
  3. 资源处理runLoaders 通过 iteratePitchingLoadersiterateNormalLoaders 实现链式调用。
昨天 — 2025年3月31日首页

基于vite官方开源脚手架预设,实现一个 npm create template-vue3-ts-preset(2):分析入口文件

作者 大专哥
2025年3月31日 17:58

上一篇文章中我们寻找到了create-vite项目中,脚手架要运行的index入口文件,本篇主要讲解在入口文件,脚手架主要做了什么

最终实现效果:通过 pnpm create template-vue3-ts-preset 安装我们自己的项目

源码:这里

声明:本项目的核心源码也是基于开源vite修改而来,本质是想让大家明白创建一个脚手架并发布到npm上走完流程。

前置知识:了解npm、node 以及 《如何创建一个本地的脚手架》《基于vite官方开源脚手架预设,实现一个 npm create template-vue3-ts-preset(1):寻找 create-vite入口》

  1. 首先我们先观察代码最后一段,发现这样一句

image.png 此时我们可以看到有一个init方法执行了,那么经常编码的同学都知道这大概是一个程序的初始化功能,那么我们去文中寻找init这个方法

1. 首先第一句话为:

image.png 我们可以通过这句话拿到几个关键要素,argv以及formatTargetDir
2. 在全局搜索 这两个方法

image.png 首先这句话是采用了mri的方法 那么mri是啥,通过ai搜索我们可知,这个是一个命令参数解析器,我们先不管他的ts类型,先从这个方法的第一个参数看起

  • process.argv.slice(2): process是一个全局的node对象, argv是node中的一个方法,然后用slice(2)来截取,通过询问或查询资料可以得到:process.argv 是获取命令行参数数组的第三个值。
  • 第二个参数
      {
  alias: { h: 'help', t: 'template' },
  boolean: ['help', 'overwrite'],
  string: ['template'],
} 

这是一个对象 具体作用未知,然后我们查阅node文档可知 mri 方法的作用:就是用于解析命令行参数,并返回一个对象,接受两个参数,第一个参数是node中输入的命令,第二个参数是一些配置选项 所以这个方法其实是拿到我们cmd中输入的一些命令,例如:

    import mri from 'mri'
const argv = mri<{
    template?: string
    help?: boolean
    overwrite?: boolean
  }>(process.argv.slice(2), {
    alias: { h: 'help', t: 'template' },
    boolean: ['help', 'overwrite'],
    string: ['template'],
  })


const init = async () => {
    console.log(argv)
     const s=(argv._[0])
     console.log(s)
}
init()
运行:node index.js  1234
输出:{ _: [ '1234' ] }   1234


image.png 这个方法就比较简单了,接受一个参数 类型为string 去掉参数前后的空格以及末尾多余的“/”

所以:image.png 的作用是:获取用户输入内容的第三个字符串 首先判断它是否为空 如果为空则为undefined,如果不为空则去掉前后空格以及多余的“/”

接着我们继续往下看:

image.png 前三局句代码我们比较容易理解,他就是argv方法的第二个参数的前两个对象当中的,实际上我们获取到的:

  1. argTemplate 就是获取我们输入内容-t后的别名, string类型
  2. argOverwrite 则是判断目录是否要求被覆盖 boolean类型
  3. help 则是判断用户是否输入了 --help或者--h。boolean类型

下面的判断则是首先判断你是否输入了 --help或者--h 如果输入了就返回 帮助命令,然后推出 下面是 helpMessage: image.png 这些我们之后再研究,其实就是输出了一些文本命令,来帮助你需要vite做什么

接着我们继续往下看

image.png

第一句调用了pkgFromUserAgent 并传入了process.env.npm_config_user_agent

我们一步一步来分析:

  1. 首先pkgFromUserAgent 方法:
    function pkgFromUserAgent(userAgent: string | undefined): PkgInfo | undefined {
  if (!userAgent) return undefined
  const pkgSpec = userAgent.split(' ')[0]
  const pkgSpecArr = pkgSpec.split('/')
  return {
    name: pkgSpecArr[0],
    version: pkgSpecArr[1],
  }
}

很显然,这个方法就是将参数userAgent用空格进行切割,然后取第0位 然后将第0位在用/进行分割 将分割出来的数据第一个返回name的value 第二个为version的value

  1. 紧接着我们分析 process.env.npm_config_user_agent,通过经验我们可以分析到,这个似乎是node的某个信息,我们尝试运行 node process.env.npm_config_user_agent 发现打印出来为undefined 经过查询可知 这句话实际上是需要通过npm、pnpm 等包管理器运行才能拿到,因为是需要获取包管理器的信息的,(尝试:将这句话写到某个node脚本中,然后通过package的script 运行)所以我们大致可以得到这样一个结论:这个方法的意思是解析出我们当前使用的具体是哪一个包管理器(npm/pnpm/yarn....)和版本信息包括获取node版本信息等等 例如:

image.png

image.png

image.png

第二句,则是调用了prompts.cancel 的方法传入了“peration cancelled”。

我们接着分析prompts.cancel

  1. 首先我们通过全局搜索prompts 发现它来源于一个第三方库:

image.png 这是一个CLI交互库,其主要作用是在终端中与用户进行交互式对话。比如,我们在创建项目时,需要一步一步输入项目名称,选择模版等等,大概分为一下使用场景:

image.png 由此我们可知:prompts.cancel('Operation cancelled')实际上就是推出或者取消操作 并输出'Operation cancelled'

接着我们继续往下看

image.png 我们会发现其实这里的注释大致已经讲清楚了他的作用,主要是用来获取项目名称和目标目录的。 这里我会逐步进行分析:

  1. 首先:let targetDir = argTargetDir 在前面提到 argTargetDir是用来获取我们输入的用空格隔开的第三个内容的,其实就是我们输入命令之后的内容。
  2. 紧接着这里判断我们的输入是否存在,如果不存在的话就执行下面的内容,如果存在的话,首先是异步执行promps.text 方法,这个方法在上面的使用场景可知,他是获取文本输入的,其中message主要是提示用户的文字,defaultValue是默认选项,如果用户直接回车,则返回这个,placeHolder则是灰色的提示文字。
  3. 紧接着会用 prompts.isCancel来判断用户的输入,因为这里具有默认值,如果rojectName 为空只有一种可能:那就是用户进行了推出或者取消操作,那么此时我们直接推出就好了
  4. 接着是将我们输入的文字进行格式化,去掉前后空格之类的,这个方法我们前面已经说过了

接着我们继续往下看:

image.png 通过注释我们可知,这段代码的主要是在目录不为空的情况下进行的逻辑处理

  1. 首先这个判断中fs.existsSync(targetDir) fs是node当中查询本地文件的一个模块,这里主要是判断是否存在我们输入的文件夹名称,后半部分是判断 这个文件夹是否为空然后取反,结合起来就是:判断是否存在当前文件夹并且不为空。
  2. 紧接着就是判断目标文件夹不为空是否覆盖,如果覆盖则overwrite为yes,否则通过prompts,给用户提供了三个选择:推出 / 删除目录下的所有文件然后继续(等于清空文件夹)/ 不管里面有没有东西,都在里面创建项目
  3. 接着判断overwrite 为取消,如果是的话,就推出
  4. 这里的switch判断主要是对两种选择做出了操作,一个是yes,(清空文件夹),一个是no(推出程序)

接着我们继续往下看:

image.png

  1. 接着packageName是获取当前文件夹的绝对路径,作用是在创建我们的项目时指定路径,fs在对文件操作时需要绝对路径
  2. 紧接着使用isValidPackageName判断路径是否规范,如果中间有空格的话可能就会不合法
  3. 在路径不合法情况下为用户弹出一个输入内容,toValidPackageName(packageName)方法会给你一个合法的名称,如果你仍然坚持不合法的名称就会触发 输出 Invalid package.json name 但是此时程序并没有推出,但是也无法进行下一步,直到你输入一个正确的名称或者推出
  4. 这里判断你有没有推出,如果为true就推出
  5. 接着将这个绝对路径的名称赋值给packageName

接着我们继续往下看:

// Choose a framework and variant 内容中会有这一句注释(选择框架或者变体)这里就是需要我们执行create vite 输入名称之后 需要后续执行的操作

image.png

  1. 首先将我们在 create vite --t xxx 之后输入的内容赋值给 template ,然后将hasInvalidArgTemplate先设fasle
  2. 紧接着判断argTemplate 是否存在并且是否不在我们的模版名称之内:TEMPLATES的定义如下:

image.png 可以看到的是将 FRAMEWORKS中提取所有的variant.name 并扁平化一个数组,后续调用的时候用了includes方法来判断是否匹配

搜索FRAMEWORKS 我们大致可以看到这是一个数组,里面的内容其实就和我们外面的模版基本匹配,然后提取了一些要素, image.png

  1. 接着我们回到原文,当存在名称并不在模版中时候,将 template为空并将hasInvalidArgTemplate 修改为true

  2. 接着判断template是否存在,由上面可知,当输入--t后的名称存在并且不在预设模版中时 template就为undefined,此时就会执行这个if当中的代码,否则就跳过,(验证触发: pnpm create vite --t demo 输入这条命令 node就会执行 "${argTemplate}" isn't a valid template. Please choose from below: 当输入 pnpm create vite --t template-solid-ts 等时候,则会跳过这段代码

  3. 我们接着来看 判断中首先会触发一个互动,满足4的条件之后,option 展示FRAMEWORKS的所有选项,前文我们已经提到了,这里会展示第一层所有选项,最后将这个选项返回给framework

接着我们继续往下

image.png

  1. 这里首先会再次判断是否为空,为空直接推出
  2. 接着再次出发点一个互动,让选择以那种变体开发,也就是我们在创建项目时选择ts js...等一些操作
  3. 这里的逻辑操作基本和上面一样,所需要注意的只有getFullCustomCommand方法

image.png 这个方法实际上就是将 预设中的的包管理器(npm、pnpm等) 转换成我们使用的包管理器。 接下来我们一步一步分析这个方法

  1. 首先这个方法接受两个参数:customCommand:string以及pkgInfo?: PkgInfo 类型PkgInfo=interface PkgInfo { name: string version: string }
  2. 第一句的意思是判断当前是否使用了包管理器 有的话就用当前的,没有的话就用npm
  3. 第二句的意思是判断当前是不是yarn 如果是的话 版本必须为1.xxx 否则返回false这是一个boolean类型
  4. 接着返回customCommand 也就是我们输入的命令 首先用正则匹配npm 在满足条件的情况下,判断是否为 bun 、pnpm 满足的情况下返回对应的 包命令, 最后如果有其他情况则保留原有的格式
  5. 当不满足npm正则时候则匹配 isYarn 变量 如果满足就返回空,如果不满足的话就就去掉@latest
  6. 最后再用正则匹配下 pm exec 如果满足则将npm exec 替换成运行临时包的命令

好了,让我们在回到之前的代码当中, 然后判断下是否为空,为空推出。 最终将这个值赋值给template #我们接着来分析

image.png

  1. 首先我们将 向path.jion方法中传入全局变量中的cwd(也就是当前工作目录),在传入 我们targetDir(我们 在vite 后跟的第一个字符串),生成完整的绝对路径返回给root
  2. 紧接着通过 fs.mkdirSync 来创建新的文件夹
  3. 接着创建一个变量isReactSwc 默认为false
  4. 再然后我们拿到template 也就是包名,判断一下是否包含-swc这个字段,如果包含就把template设置为true,然后把名称中的swc删除掉,这一步其实是在针对react 因为这里reactswc模式要比babel-loader要快
  5. 再然后将获得的pkginfo做一下判断如果有就用输入的,如果没有默认用npm
  6. 再然后就用FRAMEWORKS循环对比判断其中的variants对象中的name是否与tamplate匹配 如果匹配就返回其中的customCommand解构给customCommand

我们接着往下

image.png

  1. 当结构完成之后,首先判断结构的值存不存在,如果存在的话,就调用getFullCustomCommand方法将名称以及包管理器名称作为参数传入 我们之前已经分析过getFullCustomCommand最后返回 成我们命令行中对应的包名+模版名称
  2. 然后通过结构拿到我们对应的包管理器名称,
  3. 再然后把命令参数中的TARGET_DIR 替换成我们所需的项目目录名称
  4. 接着同步执行这个命令,并传入管理器名称,目录名称 等,
  5. 当代码执行完毕以后,使用process.exit进行推出,
  6. 然后打印Scaffolding project in ${root}...

    我们接着往下

image.png

  1. 这里定义了一个templateDir 这个主要是用来获取模版的绝对路径的因为template之前源于variant variant本身并没有完整的模版名,所以这里需要拼接写 模版名,最终得到一个完整的外部的模版名称,
  2. 再然后就比较简单了,创建write利用fs模块 来创建文件了,首先先判断是文件还是文件夹,有如果是文件夹就创建文件夹没否则就拷贝文件(单个文件)
  3. 再然后就获取目前模板的绝对路径,赋值给files
  4. 然后就是设置package.json的一些关键信息
  5. 再然后我们需要写入自己的packge.json文件,因为package.json文件本身包含一些文件信息,所以不能直接拷贝
  6. 接下来的if (isReactSwc) 则是针对react-swc的操作,如果选择了这个那么就得是ts

然后接下来的就主要是输入到log中的内容了。

  1. 首先设置一个doneMessage为空字符串
  2. 然后获取我们创建的项目和当前目录之间的差异路径
  3. 将差异路径赋值给donemessage
  4. 然后判断创建目录的路径是否等于当前目录的路径,如果相等的话 ,就将其中的正则匹配通过三元表达式赋值给doneMessage 5.因为之前我们已经哪都pkgManager 他就包管理器名称,这里主要是针对yarn进行区分,然后拼接对应的字符串,最后通过 prompts.outro(doneMessage) 输出到页面

到此整个npm crete vite的功能基本已经实现了:接下来我们总结下这个主要干了些什么事情:

总结:

1. 首先是获取项目名称以及目标目录,
2. 对目标目录进行处理
3. 再然后就是获取我们想创建什么样的项目
4. 执行创建命令创建对应的项目。









 




昨天以前首页

对webpack工程化的理解

作者 晴空969
2025年3月29日 14:04

webpack是什么?

webpack是一个现代的前端打包工具,用于构建和优化Web应用程序的前端资源,包括js,css,图片,字体等。它的主要目标试讲所有依赖项打包到一个或多个静态文件中,以便在浏览器中加载,提高了代码的可维护性和性能。下面是我对webpack配置的一些理解。

入口配置entry

首先关键就是需要配置入口文件,例如我有两个入口文件,entry.page1和entry.page2,则需要进行如下配置:

// 入口配置
  entry: {
      'entry.page1': '/app/pages/page1/entry.page1.js' // 文件路径
      'entry.page2': '/app/pages/page1/entry.page1.js'
  },

同时需要配置htmlWebpackPlugin如下:

// html-webpack-plugin 辅助注入打包后的 bundle 文件到 tpl文件中
   new HtmlWebpackPlugin({
      // 产物(最终输出路径)
      filename: path.resolve(
        process.cwd(),
        "./app/public/dist",
        `entry.page1.tpl`
      ),
      // 指定要使用的模板文件
      template: path.resolve(process.cwd(), "./app/view/entry.tpl"),
      // 要注入的代码块
      chunks: ['entry.page1'],
    })
    new HtmlWebpackPlugin({
      // 产物(最终输出路径)
      filename: path.resolve(
        process.cwd(),
        "./app/public/dist",
        `entry.page2.tpl`
      ),
      // 指定要使用的模板文件
      template: path.resolve(process.cwd(), "./app/view/entry.tpl"),
      // 要注入的代码块
      chunks: ['entry.page2'],
    })

通过上面可以看出这样配置显然不太合理,如果有多个入口文件,难道要一个一个去写吗?所以这时针对多文件就需要来动态构造,具体实现如下:

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

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

通过这种方法不管有多少入口文件,就不需要每个都去配置,直接使用 pageEntrieshtmlWebpackPluginList 即可,大大的提高了我们的开发效率。

分包策略

好的分包策略目的是把改动和引用频率不一样的js区分出来,以达到更好利用浏览器缓存的效果。大体上可以分为三类,第一种是第三方lib库,在开发过程中基本不会进行改动,除非依赖版本升级;第二种是业务组件代码的公共部分,改动较少;第三种就是我们的业务代码需要经常改动。具体分包方式如下:

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

生产环境和开发环境的不同配置

生产环境

生产环境是最终需要打包发布上线的,所以通常需要一些额外的配置以达到更好的效果。

output(输出配置)

// 生产环境的output配置
  output: {
    filename: "js/[name]_[chunkhash:8].bundle.js",
    path: path.join(process.cwd(), "./app/public/dist/prod"),
    publicPath: "/dist/prod",
    crossOriginLoading: "anonymous",
  },

多线程打包配置

这里使用的happypack,还有另一种方式thread-loader也可完成。

const HappyPack = require("happypack");
const os = require("os");

// 多线程build设置
const happypackCommonConfig = {
  debug: false,
  threadPool: HappyPack.ThreadPool({ size: os.cpus().length }),
};

plugins: [
// 多线程打包js,加快打包速度
   new HappyPack({
      ...happypackCommonConfig,
      id: "js",
      loaders: [
        `babel-loader?${JSON.stringify({
          presets: ["@babel/preset-env"],
          plugins: ["@babel/plugin-transform-runtime"],
        })}`,
      ],
    }),
// 多线程打包css,加快打包速度
   new HappyPack({
      ...happypackCommonConfig,
      id: "css",
      loaders: [
        {
          path: "css-loader",
          options: {
            importLoaders: 1,
          },
        },
      ],
    }),
]

配置TerserWebpackPlugin

使用TerserWebpackPlugin的并发和缓存,提升压缩阶段的性能,同时清除console.log的打印信息。

const TerserWebpackPlugin = require("terser-webpack-plugin");
optimization: {
    // 使用TerserWebpackPlugin的并发和缓存,提升压缩阶段的性能
    // 清除console.log
    minimize: true,
    minimizer: [
      new TerserWebpackPlugin({
        cache: true, // 启用缓存来加速构建过程
        parallel: true, // 利用多核CPU的优势来加快压缩速度
        extractComments: false,
        terserOptions: {
          compress: {
            drop_console: true, // 删除console.log
          },
        },
      }),
    ],
  },

开发环境

开发环境为了提高开发效率需要配置热更新插件,以便在业务文件改动时可以实时的更新页面。主要流程就是通过一个devServer中间件,可以监控到业务文件的改动,并通知浏览器进行更新,总结来说就是需要拥有监控文件改动和通知页面更新的能力。具体实现方式如下:

// webpack.dev.js
const baseConfig = require("./webpack.base.js");
const merge = require("webpack-merge");
const webpack = require("webpack");
const path = require("path");

// devServer的配置
const DEV_SERVER_CONFIG = {
  HOST: "127.0.0.1",
  PORT: 9002,
  HMR_PATH: "__webpack_hmr", // 官方规定
  TIMEOUT: 20000,
};
const { HOST, PORT, HMR_PATH, TIMEOUT } = DEV_SERVER_CONFIG;

// 开发阶段的 entry 配置需要加入 hmr
Object.keys(baseConfig.entry).forEach((key) => {
  // 第三方包不作为hmr入口
  if (key !== "vendor") {
    baseConfig.entry[key] = [
      // 主入口文件
      baseConfig.entry[key],
      // hmr 更新入口,官方指定的 hmr 路径
      `webpack-hot-middleware/client?path=http://${HOST}:${PORT}/${HMR_PATH}&timeout=${TIMEOUT}&reload=true`,
    ];
  }
});

// 开发环境webpack配置
const webpackConfig = merge.smart(baseConfig, {
  mode: "development", // 指定开发环境
  // sourceMap 开发工具,呈现代码的映射关系,便于在开发过程中调试代码
  devtool: "eval-cheap-module-source-map",
  // 开发环境的output配置
  output: {
    filename: "js/[name]_[chunkhash:8].js",
    path: path.resolve(process.cwd(), "./app/public/dist/dev/"), // 输出文件存储路径
    publicPath: `http://${HOST}:${PORT}/public/dist/dev/`, // 外部资源公共路径
    globalObject: "this", // 全局变量
  },
  // 开发阶段插件
  plugins: [
    // 模块热替换允许在应用程序运行时替换,提高开发效率
    new webpack.HotModuleReplacementPlugin({
      multiStep: false,
    }), // 热更新插件
  ],
});

module.exports = {
  webpackConfig,
  // devServer 配置,给dev.js 使用
  DEV_SERVER_CONFIG,
};

dev.js

// 本地开发启动 devServer
const express = require("express");
const path = require("path");
const webpack = require("webpack");
const devMiddleware = require("webpack-dev-middleware");
const hotMiddleware = require("webpack-hot-middleware");
const consoler = require("consoler");

// 从webpack.dev.js获取配置
const { webpackConfig, DEV_SERVER_CONFIG } = require("./config/webpack.dev.js");

const app = express();
const compiler = webpack(webpackConfig);

// 指定静态文件目录
app.use(express.static(path.join(__dirname, "../public/dist")));

// 引用devMiddleware中间件(监控文件改动)
app.use(
  devMiddleware(compiler, {
    // 落地文件
    writeToDisk: (filePath) => filePath.endsWith(".tpl"),
    // 资源路径
    publicPath: webpackConfig.output.publicPath,
    // headers 配置
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
      "Access-Control-Allow-Headers":
        "X-Requested-With, content-type, Authorization",
    },
    stats: {
      colors: true,
    },
  })
);

// 引入hotMiddleware中间件(实现热更新通讯)
app.use(
  hotMiddleware(compiler, {
    path: `/${DEV_SERVER_CONFIG.HMR_PATH}`,
    log: () => {},
  })
);

consoler("请等待webpack初次构建完成提示....");

// 启动 devServer
const port = DEV_SERVER_CONFIG.PORT;
app.listen(port, () => {
  console.log(`app listening on port ${port}`);
});

可以看到我们使用了 HotModuleReplacementPlugin 热替换插件允许在应用程序运行时替换,再借助 devMiddleware 监控文件改动,以及hotMiddleware 实现热更新通讯,同时也配置了sourceMap开发工具,便于在开发过程中调试代码。

总结

以上就是生产环境和开发环境的一些不同配置,当然webpack还有许多其他的配置这里没有一一列出,例如一些loader,处理vue文件的vue-loader,处理css文件的css-loader等,大家可以根据自己需要进行各种不一样的配置。通过这一章节对webpack的学习,让我更加清晰的了解了工程化相关知识,对项目中需要配置哪些东西也有了更深的理解,重要的是了解了一些主要思想,以后再使用其他工具如vite,rollup等也能够很快上手啦。fighting~

引用:抖音“哲玄前端”《大前端全栈实践》

(在线CAD集成)网页CAD二次开发中配置属性的详细教程

2025年3月28日 16:50

一、前言

Mxcad SDK 能够在线预览编辑CAD图纸,用户可根据项目需求选择不同的实现方式,如通过Vite、CDN、webpack分别使用mxcad。如果不清楚mxcad实现在线预览编辑CAD图纸的方法,可参考mxcad开发文档:www.mxdraw3d.com/mxcad_docs/…

成功创建mxcad对象后,在后续的开发过程中可能会遇到设置图纸操作习惯、监听图纸完全打开、设置图纸多选等一系列配置,mxcad内部为了方便用户的操作实现了上述一些类配置相关的方法或属性API,用户可自定义设置相关配置来满足自己项目的需求。本章就为大家介绍mxcad中与图纸操作相关的配置属性。

二、配置属性

mxcad在创建初始就可以直接设置配置属性,在创建mxcad对象的时候配置的属性将作为CAD项目加载的默认设置。下面以在vue3+ts构建的项目中创建mxcad对象并设置初始属性为例。

1. createMxCad()

在创建mxcad对象的时候,可以在createMxCad()方法内设置配置属性。

import { onMounted } from "vue";
import { createMxCad } from "mxcad";
onMounted(() => {
  const mode = "SharedArrayBuffer" in window ? "2d" : "2d-st";
  createMxCad({
    canvas: "#myCanvas",
    locateFile: (fileName) => {
      return new URL(
        `../../node_modules/mxcad/dist/wasm/${mode}/${fileName}`,
        import.meta.url
      ).href;
    },
    fileUrl: new URL("../../public/test2.mxweb", import.meta.url).href,
    fontspath: new URL("../assets/fonts", import.meta.url).href
  });
});

createMxCad方法中的初始必要属性:

A、canvas:canvas画布实例的id名

B、locateFile:mxcad的核心依赖mxcad库中/mxcad/dist/wasm目录下对应分类(2d|2d-st)中的 wasm 文件(该文件是c++编译生成的),其中 2d 目录下为多线程程序、2d-st 目录下为单线程程序,该参数用来指定 wasm 程序的网络路径。

C、fontspath:指定cad图纸中的字体文件加载路径。默认路径为dist/fonts,你可以在该目录中添加打开图纸需要的各种字体文件。

D、fileUrl:指定打开mxweb图纸的网络路径。

初始运行效果演示:

image-20250327170421832.png

其他属性:

1.1、openParameter:设置打开文件的参数,可以设置打开文件是否使用缓存,或者是否使用工作线程打开文件等。

   // 设置打开文件不使用缓存
   openParameter:{fetchAttributes:FetchAttributes.EMSCRIPTEN\_FETCH\_LOAD\_TO\_MEMORY}

1.2、onOpenFileComplete:监听打开文件成功的回调事件,在图纸打开完成后进行的操作可在该方法内执行。

   // 在图纸完全打开后控制台输出信息
   onOpenFileComplete:()=>{
       console.log('图纸完全打开!')
   }

1.3、viewBackgroundColor:设置视区背景颜色,值为rgb。

   // 初始打开图纸的背景颜色设置为白色
   viewBackgroundColor:{red:255,green:255,blue:255}

1.4、browse:是否设置为浏览模式,值为true或1时启用浏览模式,且CAD对象不能选中;值为2时启用浏览模式,CAD对象能选中,但不能夹点编辑;值为false时启用编辑模式。

   // 启动用浏览模式,图纸中的CAD对象均不能被选中和编辑
   browse:true
   /\*\*
    或
    browse:1
    \*/
   // 启动用浏览模式,CAD对象能被选中显示夹点但并不能进行夹点编辑
   browse:2
   // 编辑模式,图纸中的所有CAD对象均能被选中编辑
   browse:flase

1.5、middlePan:设置移动视区的操作方式。设置为0,点击鼠标左键移动视区;设置为1,点击鼠标中键移动视区;设置为2,点击鼠标中键和鼠标左键均可移动视区。

   // 点击鼠标中键移动视区
   middlePan:1
   // middlePan:2
   // middlePan:0

1.6、enableUndo:是否启用回退功能。设置为true则可以调用Mx_Undo命令回退操作;设置为false则禁用回退命令。默认设置为false。

   //设置启用回退功能
   enableUndo:true

1.7、enableIntelliSelect:是否启用对象选择功能。设置为true则启用;设置为false则不启用。

   // 启用对象选择功能
   enableIntelliSelect:true

1.8、multipleSelect:是否启用多选。设置为true则启用;设置为false则不启用。

   // 启用多选
   multipleSelectL:true

更多createMxCad方法内部的属性设置可参考:

www.mxdraw3d.com/mxcad_docs/…。  

2.MxFun.setIniset()

由于mxcad是依赖于mxdraw去显示的图纸,因此mxdraw中也提供了实现各种初始配置的方法:MxFun.setIniset(),我们可以在该方法中配置更多CAD项目的初始配置,其调用方法如下:

import { MxFun } from "mxdraw"
MxFun.setIniset({
    // 启动夹点编辑功能, 开启单选图形(mxcad默认开启)
    "EnableGripEidt": true,
    // 开启多选
    "multipleSelect": true
    /**
     * ......可配置更多iniConfig参数
     */
})
常用iniConfig参数:

2.1、 EnableIntelliSelect:是否启用智能选择。设置为true则启用;设置为false则不启用。

   // 启用智能选择
   EnableIntelliSelect:true 

2.2、EnableGripEdit:是否启动夹点编辑。设置为true或1表示启用,设置为0或fase表示禁用,设置为2表示选中对象后只显示夹点,但不响应响应夹点编辑。  

   // 启用夹点编辑
   EnableGripEdit:true

2.3、multipleSelect: 是否启动多选,启动多选设置后用户一次选择多个实体。设置为true则启用;设置为false则不启用,其默认值是false。

   // 启动多选-框选
   multipleSelect:true

2.4、IntelliSelectType:多选方式控制。设置为1:多选,但不支持连续多选,设置为2:多选,并支持连续多选,默认值为1。该设置生效的前提是要先启动多选。

   // 多选方式可以连续点选
   IntelliSelectType:2

2.5、autoResetRenderer:是否启用自动重置渲染器。设置为true则启用;设置为false则不启用

   // 启用自动重置渲染器
   autoResetRenderer:true

2.6、ForbiddenDynInput:是否禁用动态输入框。设置为true为禁用;设置为false则启用。

   //禁用动态输入框
   ForbiddenDynInput:true

2.7、inputRectWidth:设置夹点和拾取框的宽度,其单位是屏幕上的像素。

   //将夹点宽度设置为5px
   inputRectWidth:5

2.8、gripPointColor: 设置夹点颜色,其值为十六机制颜色值,如:0xFFFFFFFF,0xNRGB等。

   // 将夹点颜色设置为白色
   gripPointColor:0xFFFFFFFF

2.9、EnableDrawingViewAngle:是否使用图纸中的视区角度设置,默认为true 。

  // 不使用图纸中的视区角度
   EnableDrawingViewAngle:flase

更多MxFun.setIniset()方法内部的属性设置可参考: mxcad.github.io/mxdraw_api_…

3.McObject对象方法API

处理了上面介绍的两种方式外,mxcad中的McObject对象里也提供了设置CAD项目配置参数的方法。下面以常用的几种设置方法为例:

3.1、McObject.setBrower():是否设置为浏览模式。 

  import { MxCpp } from 'mxcad';
   // 设置为浏览模式
    MxCpp.getCurrentMxCAD().setBrowse(true);
   // 设置为编辑模式
    MxCpp.getCurrentMxCAD().setBrowse(false); 

3.2、McObject.setViewBackgroundColor():设置视区的背景色。

   import { MxCpp } from 'mxcad';
   // 将视区背景色设置为白色,值为rgb
    MxCpp.getCurrentMxCAD().setViewBackgroundColor(255,255,255);

3.3、McObject.setAttribute():设置mxcad对象的一些属性设置。

   import { MxCpp } from 'mxcad';
    MxCpp.getCurrentMxCAD().setAttribute({
        //启用回退功能
        EnableUndo:true,
        //显示精度设置,默认为0,可以取0 \~1000,1000为最高精度
        DisplayPrecision:1000
    });

更多McObject方法设置可参考: www.mxdraw3d.com/mxcad_docs/…

三、在线演示

用户可在我们提供的在线demo的编辑器中直接运行设置,查看实时效果,在线demo地址:demo2.mxdraw3d.com:3000/mxcad/

实时运行效果演示:

import { MxCpp } from 'mxcad';
// 将视区背景色设置为白色,值为rgb
 MxCpp.getCurrentMxCAD().setViewBackgroundColor(255,255,255);

如下图: image-20250328120924230.png

❌
❌