自动化构建

114 阅读8分钟

自动化构建就是把我们开发时候产生的源代码 自动化的 构建成 生产环境可以运行的代码或者程序,一般我们会把这个自动化构建的过程叫做自动化工作流,作用就是尽可能脱离运行环境的种种问题,在开发阶段去使用提高效率的语法、规范和标准

如果从零到一搭建一个项目,在项目架构初期设计,需要考虑一些问题:

  • 项目初始化
  • 确定需要的依赖并安装
webpack打包构建
eslintJs 规范检查,github.com/dustinspeck…
lint-stagedgit staged 文件 lint
stylelint样式检查
@babel/core、@babel/preset-reactBabel 相关
huskygit 钩子
cspell拼写检查
prettier代码格式化,prettier.io/
cz-git、commitizencz-git.qbb.sh/guide/
turbo构建缓存、构建优化,(monorepo 项目中极其有用)turbo.build/pack
zx写脚本
  • 运行脚本定义
  • ts 配置、eslint 配置、stylelint 配置
  • git 规范
  • 项目构建打包

npm scripts

npm scripts 是实现自动化构建工作流的最简单的方式。它写在 package.json 里的 scripts字段里。scripts字段是一个对象,它的每一个属性对应一段脚本。这些定义在package.json里面的脚本,就称为 npm 脚本。

项目相关的脚本可以集中在一个地方,不同项目的脚本命令只要功能相同 ,就可以有同样的对外接口。用户不需要知道怎么测试你的项目,只需要运行npm run test就好。查看当前项目的所有 npm 脚本命令,可以使用不带任何参数的npm run命令。

脚本定义是前端项目的自动化的一个重要环节。 因为我们需要先将项目常用的脚本定义进行罗列。项目常用的脚本定义有:

  • start
  • dev
  • build
  • preview 用作本地查看打包输出的产物
  • docs 构建项目中的文档。比如我们开发UI组件库,边开发边看效果边写文档(库开发文档工具)
  • clear 通常用rimraf 清除,清除构建的一些老的内容,以及包含老的依赖,整个清理我们的项目依赖和打包产物
  • check 检查,有lint检查、ts检查、style检查、spell检查
  • format
  • lint
  • spellcheck 用作样式代码的检查
  • typecheck 专门用作ts类型检查
  • commit
  • preinstall、prepare、postinstall 其实都是npm的钩子。

初始化包:

npm  init
//package.json
{
  "name": "project-starer",
  "version": "1.0.0",
  "description": "yk project start",
  "main": "index.js",
  "sideEffects": false,
  "scripts": {
    "start": "pnpm dev",
    "dev": "webpack serve --open --config ./config/webpack.dev.config.js",
    "build": "webpack --config ./config/webpack.prod.config.js",
    "turbo:build": "turbo build",
    "clear": "rimraf node_modules pnpm-lock.yaml .eslintcache docs dist",
    "format": "prettier --cache --write \"src/**/*.@(html|js|ts|tsx|css|scss|md)\"",
    "lint": "eslint --cache --rule 'new-cap: off' --ext=js,cjs,mjs,jsx,ts,tsx --fix ./src",
    "spellcheck": "cspell lint --dot --gitignore --color --cache --show-suggestions \"src/**/*.@(html|js|cjs|mjs|ts|tsx|css|scss|md)\"",
    "typecheck": "tsc --extendedDiagnostics --noEmit -p .",
    "postinstall": "husky install",
    "commit": "git-cz",
    "prepare": "pnpm build"
  },
  "author": "HiHi",
  "license": "MIT",
  "devDependencies": {
    "commitizen": "4.3.0",
    "cspell": "6.31.1",
    "eslint": "8.43.0",
    "git-cz": "4.9.0",
    "husky": "8.0.3",
    "prettier": "2.8.8",
    "rimraf": "5.0.1",
    "typescript": "5.1.3",
    "webpack": "5.87.0",
    "webpack-cli": "5.1.4",
    "webpack-dev-server": "4.15.1",
    "html-webpack-plugin": "5.5.3",
    "clean-webpack-plugin": "4.0.0",
    "raw-loader": "4.0.2",
    "turbo": "1.10.3"
  },
  "peerDependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.0.2"
  }
}

-> peerDependencies、devDependencies和dependencies的区别

devDependencies是我们的开发环境依赖,不会打包到生产环境,对线上环境不会产生影响(webpack、vite、babel、ESlint...)
dependencies是我们要用到生产环境的依赖,没有这些会影响到项目的稳定运
peerDependencies是依赖的约束

Webpack 配置详解与优化

我觉得这篇文章写的很详细:[万字总结] 一文吃透 Webpack 核心原理 - 掘金 (juejin.cn)感兴趣可以看看

基础配置: ModeEntryOutputmoduleLoadersPlugins

  • Entry :指示从哪个文件开始打包
  • Output输出属性告诉 webpack 打包完的文件输出到哪里去 以及如何命名这些文件。
  • Loaders加载器,webpack 只能理解 JavaScript 和 JSON 文件,而处理其他类型的文件,并将它们转换为有效的模块,需要加载器。
  • Plugins:插件,扩展webpack的功能
  • Configuration:配置,它是一个标准的 Node.js CommonJS 模块
  • Modules:模式,主要有两种模式:开发模式 和 生产模式
  • Browser Compatibility:浏览器兼容性

Webpack 功能集非常庞大,包括:模块打包、代码拆分、按需加载、HMR、Tree-shaking、文件监听、sourcemap、Module Federation、devServer、DLL、多进程等等

webpack打包优化时,SplitChunksPlugin插件是最常用也最简便的方法之一。此功能允许将代码拆分为各种捆绑包,然后可以按需或并行加载这些捆绑包。可用于实现较小的捆绑包并控制资源负载优先级,如果使用得当,可能会对加载时间产生重大影响。

有三种常规的代码拆分方法可用:

  • 入口点: 使用入口配置手动拆分代码。
  • 防止重复: 使用条目依赖项或拆分块插件对进行重复数据删除和拆分。
  • 动态导入:通过模块内的内联函数调用拆分代码。

SplitChunksPlugin(拆分块插件),它允许我们将常见的依赖项提取到现有的入口块或全新的块中。让我们使用它来消除上一个示例中的依赖项:lodash
它的参数非常多:

optimization: {
    splitChunks: {
      chunks: 'async',
      ...
    }
}
  • chunks: 打包的模块是异步、同步、还是全部,对应的值为async initial all,也可以写成函数形式,自定义打包
  • minSize: 抽离公共包的最小size
  • maxSize: 最大size
  • minChunks: 最少使用次数
  • maxAsyncRequests: 最大异步请求数
  • maxInitialRequests: 最大同步请求数
  • automaticNameDelimiter: 默认情况下,webpack将使用块的名称和原始文件名称生成文件名(例如vendors~main.js)。此选项允许您指定用于生成的名称的分隔符。
  • automaticNameMaxLength: 允许设置由SplitChunksPlugin生成的块的名称字符的最大值
  • maxAsyncRequests: 按需加载时最大并行请求数
  • maxAsyncRequests: 入口处最大请求并行数
  • name: 生成块的名称,为true时,将根据块和缓存组密钥自动生成名称
  • cacheGroups: 缓存组可以继承或者覆盖上面的选项,但是priority test reuseExistingChunk 只能在这里设置。如果不想使用缓存组,可以直接置为false
    • priority: 表示缓存的优先级;
    • test: 缓存组的规则,表示符合条件的的放入当前缓存组,值可以是functionbooleanstringRegExp,默认为空;
    • reuseExistingChunk: 表示可以使用已经存在的块,即如果满足条件的块已经存在就使用已有的,不再创建一个新的块。

自定义插件(Plugin)

定义

Webpack 插件的本质是类,并且这个类必须定义 apply 方法,基于这些原则,我们首先定义一个最简单的 webpack 插件。实例代码如下:

export default class CusPlugin {
  constructor(options = {}) {
    this.options = options;
  }
  apply(compiler) {
    /*...*/
  }
}

我们可以发现,自定义插件的核心逻辑在 apply 方法中执行,我们可以为已经定义的 hook 添加监听事件,从而在对应事件调用时,完成我们定义的操作。有了这个概念,我们接下来通过一个很常见的例子,深入了解自定义插件的定义与使用。 现在有一个需求,需要在 webpack 打包完成后,将本次所有打包文件名称输出到 fileList.md 文件中。 也就是:

  1. 打包完成时机
  2. 打包生成资源
  3. 将处理后的信息输出到 fileList.md 文件

针对于第一部分,打包完成时机,我们可以通过compiler 对象上的 hooks 获取到 emit 钩子,然后为该钩子绑定一个新的事件函数。通过该钩子能够获取到 compilation 对象,通过该对象就能获取打包生成的资源。最终以 fileList.md 为名,为 compilation 指定新资源,从而实现 fileList 文件输出。

完善我们的 webpack plugin,代码示例如下:

export default class FileListPlugin {
  constructor(options = {}) {
    this.options = options;
    this.filename = this.options.filename || 'fileList.md';
  }
  apply(compiler) {
    // 打包完成时机
    compiler.hooks.emit.tap('FileListPlugin', compilation => {
      const { filename: fileName } = this;
      const { assets } = compilation;
      const fileCount = assets.length;
      let content = `# 本次打包共生成${fileCount}个文件\n\n`;
      // 遍历打包生成的资源
      for (let filename in asstes) {
        content += `- ${filename}\n`;
      }
      // 将信息输出到 fileList.md 文件并生成该文件
      compilation.assets[fileName] = {
        source: function() {
          return content;
        },
        size: function() {
          return content.length;
        },
      };
    });
  }
}

exports = module.exports = FileListPlugin;

使用

在 webpack 中使用该插件:

// webpack.config.js

const path = require('path');
const FileListPlugin = require('./path/to/plugins/file-list-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  plugins: [new FileListPlugin()],
};

自定义 Loader

定义

自定义 Loader 的本质是一个函数,该函数接收源码 source 参数,在这里首先需要明确一点,代码也不过是字符串,处理代码内容其实也就是字符串的处理,我们首先书写一个最简单的 loader,代码示例如下:

const loaderUtils = require('loader-utils');

exports = module.exports = function(source) {
  // 对 source 进行一些处理后...
  return source;
};

以上例子是一个最简单的 webpack loader,需求: 将给定代码中的模板内容替换为给定值
eg: 将 “{{author}}” 替换为 “hihi”。 假设有一个文件,代码如下:

console.log('{{author}}欢迎你!');

接下来我们改进一下我们的 loader,示例如下:

//  temp-loader.js

const loaderUtils = require('loader-utils');
const path = require('path');
const authorName = 'hihi';

exports = module.exports = function(source) {
  const matches = source.match(/{{author}}/g);
  for (const match of matches) {
    source = source.replace(match, authorName);
  }
  return source;
};

使用

自定义 loader 需要在 webpack.config.js 中进行配置:

// webpack.config.js

const path = require('path');

module.exports = {
  target: 'node',
  entry: './index',
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: '[name].js',
  },
  resolveLoader: {
    modules: ['./node_modules', './loaders'],
  },
  module: {
    rules: [
      {
        test: /.js$/,
        use: [
          {
            loader: 'temp-loader',
          },
        ],
      },
    ],
  },
};

这样,在项目下执行 yarn start, 或 npx webpack 就可以输出处理后的文件,文件内容为:

console.log('hihi欢迎你!');

Vite

相较于 webpack,vite 的概念就少多了,且 vite 更易上手

Vite 也有必须声明的配置文件:vite.config.js

启动与打包

Vite 做了很多屏蔽开发模式启动与打包的细节实现,通常我们只需要关心运行命令 vitevite build 来确定执行本地启动或打包构建。

{
    "dev": "vite",
    "build": "vite build"
}

产物预览

相较于 webpack,vite 打包生成内容的预览也很简单,只需要运行 vite preview ,简化了 webpack 打包分析相关的配置

{
    "preview": "vite preview --open --port 4173"
}