自动化构建就是把我们开发时候产生的源代码 自动化的 构建成 生产环境可以运行的代码或者程序,一般我们会把这个自动化构建的过程叫做自动化工作流,作用就是尽可能脱离运行环境的种种问题,在开发阶段去使用提高效率的语法、规范和标准
如果从零到一搭建一个项目,在项目架构初期设计,需要考虑一些问题:
- 项目初始化
- 确定需要的依赖并安装
| webpack | 打包构建 |
| eslint | Js 规范检查,github.com/dustinspeck… |
| lint-staged | git staged 文件 lint |
| stylelint | 样式检查 |
| @babel/core、@babel/preset-react | Babel 相关 |
| husky | git 钩子 |
| cspell | 拼写检查 |
| prettier | 代码格式化,prettier.io/ |
| cz-git、commitizen | cz-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)感兴趣可以看看
基础配置: Mode、Entry、Output、module、Loaders 及 Plugins
- 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: 打包的模块是异步、同步、还是全部,对应的值为asyncinitialall,也可以写成函数形式,自定义打包minSize: 抽离公共包的最小sizemaxSize: 最大sizeminChunks: 最少使用次数maxAsyncRequests: 最大异步请求数maxInitialRequests: 最大同步请求数automaticNameDelimiter: 默认情况下,webpack将使用块的名称和原始文件名称生成文件名(例如vendors~main.js)。此选项允许您指定用于生成的名称的分隔符。automaticNameMaxLength: 允许设置由SplitChunksPlugin生成的块的名称字符的最大值maxAsyncRequests: 按需加载时最大并行请求数maxAsyncRequests: 入口处最大请求并行数name: 生成块的名称,为true时,将根据块和缓存组密钥自动生成名称cacheGroups: 缓存组可以继承或者覆盖上面的选项,但是prioritytestreuseExistingChunk只能在这里设置。如果不想使用缓存组,可以直接置为falsepriority: 表示缓存的优先级;test: 缓存组的规则,表示符合条件的的放入当前缓存组,值可以是function、boolean、string、RegExp,默认为空;reuseExistingChunk: 表示可以使用已经存在的块,即如果满足条件的块已经存在就使用已有的,不再创建一个新的块。
自定义插件(Plugin)
定义
Webpack 插件的本质是类,并且这个类必须定义 apply 方法,基于这些原则,我们首先定义一个最简单的 webpack 插件。实例代码如下:
export default class CusPlugin {
constructor(options = {}) {
this.options = options;
}
apply(compiler) {
/*...*/
}
}
我们可以发现,自定义插件的核心逻辑在 apply 方法中执行,我们可以为已经定义的 hook 添加监听事件,从而在对应事件调用时,完成我们定义的操作。有了这个概念,我们接下来通过一个很常见的例子,深入了解自定义插件的定义与使用。 现在有一个需求,需要在 webpack 打包完成后,将本次所有打包文件名称输出到 fileList.md 文件中。 也就是:
- 打包完成时机
- 打包生成资源
- 将处理后的信息输出到 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 做了很多屏蔽开发模式启动与打包的细节实现,通常我们只需要关心运行命令 vite 、vite build 来确定执行本地启动或打包构建。
{
"dev": "vite",
"build": "vite build"
}
产物预览
相较于 webpack,vite 打包生成内容的预览也很简单,只需要运行 vite preview ,简化了 webpack 打包分析相关的配置
{
"preview": "vite preview --open --port 4173"
}