前言
本文是对某开源的项目webpack5 + react + typescript
项目地址逐行代码做分析,解剖一个成熟的环境所有配置的意义,理清一些常见的问题,比如
-
文件中的
import
转es5
被webpack
编译成了什么?webpack的分包配置splitChunks咋用? 你真的理解其选项值chunks
的值async
或者initial
或者all
是什么意思吗,这个可对分包优化至关重要啊!为什么package.json
没有依赖的包,在node_modules
下面会出现,npm install
包是以什么结构安装npm包呢? -
babel/core
有什么用,它跟babel-loader
的区别,babelrc
文件中配置项presets
和plugin
的区别是什么,babelrc
常用设置项知道多少,这个不清楚?那项目代码校验和格式化用到editorConfig、prettier、eslint,stylelint
他们的关系和区别是什么?如何配置防止它们冲突,比如eslint
也有css
校验,怎么让stylelint
跟它不起冲突,这些你要晋升为前端主管怎么能心里没数? -
如果你用的
vscode
,如何在工作区配置ctrl+s
自动保存,让你的js和css文件自动格式化,并配置为prettier
格式化,webpack5
和4
的配置中的变化、等等。。。
以上提到的知识点对我们深入了解项目环境搭建
非常重要, 你的项目你来时一般环境都是搭建好的,试过从0自己搭建不?是不是抄别人的配置,都一头雾水,完全不知道这些配置项时啥意思呢?
现在!本篇本章专解这个问题!废话少说,
我们先从package.json
说起,里面的每一行代码是什么意思。
package.json
package.json
里面有很多有趣的内容,我们先从依赖包说起,解释这个项目中,下面的依赖包分别有什么用。
"devDependencies": {
"@babel/core": "^7.13.13",
"@babel/plugin-transform-runtime": "^7.13.10",
"@babel/preset-env": "^7.13.12",
"@babel/preset-react": "^7.13.13",
"@babel/preset-typescript": "^7.13.0",
"@commitlint/cli": "^12.0.1",
"@commitlint/config-conventional": "^12.0.1",
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.3",
"@types/webpack-env": "^1.16.0",
"@typescript-eslint/eslint-plugin": "^4.19.0",
"@typescript-eslint/parser": "^4.19.0",
"babel-loader": "^8.2.2",
"chalk": "^4.1.0",
"clean-webpack-plugin": "^3.0.0",
"conventional-changelog-cli": "^2.1.1",
"copy-webpack-plugin": "^8.1.0",
"cross-env": "^7.0.3",
"css-loader": "^5.2.0",
"css-minimizer-webpack-plugin": "^1.3.0",
"detect-port-alt": "^1.1.6",
"error-overlay-webpack-plugin": "^0.4.2",
"eslint": "^7.22.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^8.1.0",
"eslint-import-resolver-typescript": "^2.4.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-promise": "^4.3.1",
"eslint-plugin-react": "^7.23.1",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-unicorn": "^29.0.0",
"fork-ts-checker-webpack-plugin": "^6.2.0",
"html-webpack-plugin": "^5.3.1",
"husky": "^4.3.8",
"ip": "^1.1.5",
"is-root": "^2.1.0",
"lint-staged": "^10.5.4",
"mini-css-extract-plugin": "^1.4.0",
"node-sass": "^5.0.0",
"postcss": "^8.2.8",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-loader": "^5.2.0",
"postcss-preset-env": "^6.7.0",
"prettier": "^2.2.1",
"sass-loader": "^11.0.1",
"style-loader": "^2.0.0",
"stylelint": "^13.12.0",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-rational-order": "^0.1.2",
"stylelint-config-standard": "^21.0.0",
"stylelint-declaration-block-no-ignored-properties": "^2.3.0",
"stylelint-order": "^4.1.0",
"stylelint-scss": "^3.19.0",
"terser-webpack-plugin": "^5.1.1",
"typescript": "^4.2.3",
"webpack": "^5.37.1",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^4.5.0",
"webpack-dev-server": "^3.11.2",
"webpack-merge": "^5.7.3",
"webpackbar": "^5.0.0-3"
},
"dependencies": {
"@babel/runtime-corejs3": "^7.13.10",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
@babel/core有啥用?
babel
的功能在于「代码转译」
,具体一点,即将目标代码转译为能够符合期望语法规范的代码。在转译的
过程中,babel
内部经历了「解析 - 转换 - 生成」
三个步骤。而 @babel/core
这个库则负责「解析」
,具体的「转换」
和「生成」
步骤则交给各种插件(plugin)和预设(preset)来完成。
你可以从@babel/core
自己的依赖里看到其中有三个包,叫@babel/generator
(将ast生成代码)、 @babel/parser
(将源代码转换为AST)、@babel/traverse
(转换AST),有这三个包,就能转换你的代码,案例如下:
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
import generate from '@babel/generator';
const code = 'const n = 1';
// 将源代码转换为AST
const ast = parse(code);
// 转换AST
traverse(ast, {
enter(path) {
// in this example change all the variable `n` to `x`
if (path.isIdentifier({ name: 'n' })) {
path.node.name = 'x';
}
},
});
// 生成代码 <- ast
const output = generate(ast, code);
console.log(output.code); // 'const x = 1;'
这应该非常清楚的了解babel/core
有什么用了吧,至于说怎么在traverse
阶段改变代码,就要用到其他的插件了,我们马上说一下babel-loader
,让你明白它跟babel/core
的区别
babel-loader
我们知道webpack
需要各种loader
,这些loader
的作用就是把文件做转化,比如babel-loader
是用来转化js
,jsx
,ts
,tsx
文件的。
比如我们写的js
代码是es6
,import xx模块 from ‘xx模块’
,为了浏览器兼容性,我们需要转化为es5
的写法,转译import
,那么这个时候就需要babel-loader
来帮忙了。
比如说一个简单的loader
怎么写呢,我们就知道babel-loader
大概是个什么东西了,
module.exports = source => {
// source 就是加载到的文件内容
console.log(source)
return "hello ~" // 返回一个字符串
}
上面我们把任何加载到的文件内容转化为一个字符串,也就是loader
无非是加工读到的文件,所以babel-loader
就是读取对应的jsx?|tsx?
文件,然后加工后返回而已
prest家族:@babel/preset-env、@babel/preset-react、@babel/preset-typescript、
- @babel/preset-typescript: 主要是用来编译
ts
文件的。
目前 TypeScript
的编译有两种方式。一种是使用 TypeScript
自家的编译器 typescript
编译(简称 TS 编译器),一种就是使用 Babel + @babel/preset-typescript
编译。
其中最好的选择就是使用Babel + @babel/preset-typescript
,主要原因是:
Babel
能够指定需要编译的浏览器环境。这一点 TS 编译器是不支持的。在babelrc
文件里可以设置编译的target
属性(在preset-env
插件上设置)为比如
"targets": {
"browsers": ["last 2 versions", "safari >= 7"], // 配置safari的版本大于7的语法才转译
"node": "6.10" // node版本支持到6.10
TS
编译器在编译过程中进行类型检查,类型检查是需要时间的,而babel
不做类型检查,编译速度就更快
@babel/preset-react: 主要是编译jsx
文件的,也就是解析jsx语
法的,比如说react
生成div,我们举一个例子,在jsx里面是这样的,转换成什么了呢?
<div></div>
转化后的react
的api
const reactElement = ReactElement.createElement(
... // 标签名称字符串/ReactClass,
... // [元素的属性值对对象],
... // [元素的子节点]
)
reactElement('div', null, '')
- @babel/preset-env:
@babel/preset-env
将基于你的实际浏览器及运行环境,自动的确定babel
插件及polyfill
,在不进行任何配置的情况下,@babel/preset-env
所包含的插件将支持所有最新的JS特性(ES2015
,ES2016
等,不包含 stage
阶段),将其转换成ES5
代码。例,那么只配置 @babel/preset-env
,转换时会抛出错误,需要另外安装相应的插件。
//.babelrc
{
"presets": ["@babel/preset-env"]
}
注意:@babel/preset-env
会根据你配置的目标环境,生成插件列表来编译。Babel
官方建议我们把 targets
的内容保存到 .browserslistrc
文件中 或者 package.json
里增加一个browserslit
节点,不然除了babel
外,其他的工具,例如browserslist
、post-css
等无法从 babel
配置文件里读取配置
如果你不是要兼容所有的浏览器和环境,推荐你指定目标环境,这样你的编译代码能够保持最小。
具体用法我们会在将babelrc
文件配置(babel
的配置文件)的时候详细说明。
@babel/plugin-transform-runtime、@babel/runtime-corejs
为什么我们需要它,我们来看看@babel/prest-env
编译完js文件后,会有哪些问题
-
比如我们使用字符串的inclues语法(es5中并不支持它,需要转译), 例如
Array.from
等静态方法,直接在global.Array
上添加;对于例如 includes 等实例方法,直接在global.Array.prototype
上添加。这样直接修改了全局变量的原型。 -
babel
转译 syntax 时,有时候会使用一些辅助的函数来帮忙转,比如:
class 语法中,babel 自定义了 _classCallCheck
这个函数来辅助;typeof
则是直接重写了一遍,自定义了 _typeof 这个函数来辅助
。这些函数叫做 helpers。每个项目文件都写无意是不合理的。
作用是将 helper
(辅助函数) 和 polyfill
(不修改全局变量原型的静态方法等) 都改为从一个统一的地方引入,并且引入的对象和全局变量是完全隔离的。
具体配置不详细说明了,到后面讲babelrc
文件的的时候说。
- @babel/runtime-corejs:
上面我们看到了@babel/prest-env
带来的问题,这两个问题@babel/plugin-transform-runtime可以解决,那@babel/runtime-corejs
又是个什么东西呢?
其中 @babel/plugin-transform-runtime
的作用是转译代码,转译后的代码中可能会引入 @babel/runtime-corejs
里面的模块,也就是说具体转译代码的函数是单独在另一个包里,就是@babel/runtime-corejs
里面
types家族:@types/react @types/react-dom @types/webpack-env
-
@types/react、@types/react-dom
这两个是react的typescript类型定义 -
@types/webpack-env
是webpack的typescript类型定义
eslint家族:eslint、eslint-config-airbnb、eslint-config-prettier...
-
eslint:是一个插件化并且可配置的
JavaScript
语法规则和代码风格的检查工具。这个就不多说了,大家都知道吧,不用eslint
的前端项目应该很少。 -
eslint-config-airbnb:
Airbnb
的eslint规则的标准,它依赖eslint, eslint-plugin-import, eslint-plugin-react, and eslint-plugin-jsx-a11y
等插件,并且对各个插件的版本有所要求。 -
eslint-config-prettier:
prettier
是一个代码格式化工具,比如说规范项目都使用单引号,还是双引号。而且,Prettier
还给予了一部分配置项,可以通过.prettierrc
文件修改。 -
所以相当于
Prettier
接管代码格式的问题,而使用Prettier + ESLint
就完完全全解决了代码格式和代码语法规则校验的问题。
但实际上使用起来配置有些小麻烦,但也不是什么大问题。因为 Prettier
和 ESLint
一起使用的时候会有冲突,我们需要使用 eslint-config-prettier
来关掉 (disable) 所有和 Prettier
冲突的 ESLint 的配置,
eslint-plugin-prettier
将 prettier 的 rules 以插件的形式加入到 ESLint 里面方法就是在 .eslintrc 里面将 prettier 设为最后一个 extends
// .eslintrc
{
"plugins": ["prettier"],
"rules": {
"prettier/prettier": "error"
}
}
将上面两个步骤和在一起就是下面的配置,也是官方的推荐配置
// .eslintrc
{
"extends": ["plugin:prettier/recommended"]
}
- eslint-plugin-import:用于校验
es6
的import
规则,如果增加import plugin
,在我们使用webpack
的时候,如果你配置了resolve.config.js
的alias
,那么我们希望import plugin的校验规则会从这里取模块的路径,此时需要配置,注意,此时同时要下载eslint-import-resolver-webpack
插件才能像下面一样设置
“rules”: {},
/** 这里传入webpack并不是import插件能识别webpack,
* 而且通过npm安装了「eslint-import-resolver-webpack」,
* 「import」插件通过「eslint-import-resolver-」+「webpack」找到该插件并使用,
* 就能解析webpack配置项。使用里面的参数。
**/
"settings": {
// 使用webpack中配置的resolve路径
"import/resolver": "webpack"
}
eslint-import-resolver-typescript:它也是「eslint-import-resolver-」
家族的一员,它的作用是
import/require
扩展名为 .ts/.tsx 的文件- 使用
tsconfig.json
中定义的paths路径 - 优先解析
@types/*
定义而不是普通的 .js
eslint-plugin-jsx-a11y: 该插件为你的 JSX
中的无障碍问题提供了 AST
的语法检测反馈。
eslint-plugin-react: 一些 react
的 eslint
的 rules
规范
eslint-plugin-react-hooks:检测react hooks的一些语法规范,并提供相应的rules
postcss家族:postcss、postcss-flexbugs-fixes、postcss-loaderpostcss-preset-env,autoprefixer
postcss: 是一个使用JavaScript插件来转换CSS
的工具。
PostCSS
本身很小,其只包含CSS
解析器,操作CSS
节点树的API
,source map
,以及一个节点树字符串化工具,其它功能都是通过插件来实现的,比如说插件有
1、添加浏览器内核前缀的
2、有检测css
代码的工具等等
postcss-flexbugs-fixes: 修复在一些浏览器上flex布局的bug,比如说
- 在ie10和标准的区别
----|标准| flex: 1 flex: 1 1 0% flex: 1 0 0px flex: auto flex: 1 1 auto flex: 1 0 auto
缩写声明 | 标准转义 | IE10转义 | (no flex declaration) | flex: 0 1 auto | flex: 0 0 auto | flex: 1 | flex: 1 1 0% | flex: 1 0 0px | flex: auto | flex: 1 1 auto | flex: 1 0 auto |
---|
postcss-loader:loader
的功能在上面已经说明,这个loader
是postcss
用来改变css
代码的loader
postcss-preset-env:这个插件主要是集成了(有了它不用下载autoprefixer插件)
autoprefixer:用于解析 CSS
并使用 Can I Use
中的值向 CSS
规则添加供应商前缀
style-resoures-loader:这个插件比较重要,即使这个项目没有用,我也建议大家项目用上。它的作用就是避免重复在每个样式文件中@import
导入,在各个css
文件中能够直接使用变量和公共的样式。
webpack家族:webpack、webpack-bundle-analyzer、webpack-cli、webpack-dev-server、webpack-merge、webpackbar
webpack:这个不用描述了吧。。。
webpack-cli:
- 是使用
webpack
的命令行工具,在4.x
版本之后不再作为webpack
的依赖了,我们使用时需要单独安装这个工具。
webpack-bundle-analyzer: webpack
打包体积分析工具,会让我们知道打包后的文件分别是由哪些文件组成,并且体积是多少,是一款优化分析打包文件的工具
webpack-dev-server:是一个小型的Node.js Express
服务器,它使用webpack-dev-middleware
来服务于webpack的包,除此自外,它还有一个通过Sock.js
来连接到服务器的微型运行时
webpack-merge:
- 一般情况,我们会把webpack文件分为,
webpack.common.js
(后面两个js文件共同的内容抽离出来),webpack.pro.js
(生产环境独有的内容),webpack.dev.js
(开发环境独有的内容)。 - 此时,我们需要一个方法来合并
webpack.common.js
和webpack.pro.js
变为生产环境的内容,同理common
和dev
也是如此。我们就需要webpack-merge
方法了。它的作用如下
const merge = require("webpack-merge");
merge(
{a : [1],b:5,c:20},
{a : [2],b:10, d: 421}
)
//合并后的结果
{a : [1,2] ,b :10 , c : 20, d : 421}
从上面的案例,我们可以看出来, 数组内容会合并,基础类型的值会被覆盖,这比较符合我们webpack.common.js
有一些plugins:[],webpack.pro.js
也有一些plugins是合并的需要,而不是覆盖。
stylelint 家族: stylelint、、stylelint-config-rational-order...
stylelint:stylelint
用于样式规范检查与修复,支持 .css .scss .less .sss
stylelint-config-prettier:关闭所有不必要的或可能与 Prettier
冲突的规则。
stylelint-config-rational-order:它对你的css
样式排序会有要求,具体为
Positioning -- 定位
Box Model -- 盒模型
Typography -- 版式
Visual -- 可见性(显示和隐藏)
Animation -- 动画
Misc -- 其它杂项
.declaration-order {
/* 1.Positioning 位置属性 */
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 10;
/* 2.Box Model 盒子属性 */
display: block;
float: right;
width: 100px;
height: 100px;
margin: 10px;
padding: 10px;
/* 3.Typography 文字属性 */
color: #888;
font: normal 16px Helvetica, sans-serif;
line-height: 1.3;
text-align: center;
/* 4.Visual 视觉属性 */
background-color: #eee;
border: 1px solid #888;
border-radius: 4px;
opacity: 1;
/* 5.Animation Misc 其他 */
transition: all 1s;
user-select: none;
}
你不按上面的顺序写css
的话,会警告或者报错。
stylelint-order:这个实现的功能也是排序,不过它跟上面的插件的区别是,它按照字母(英文是alpha sort
)排序,所以两个插件要配合使用。
stylelint-config-standard:该风格是 Stylelint
的维护者汲取了 GitHub、Google、Airbnb
多家之长生成的一套css风格规则。
stylelint-declaration-block-no-ignored-properties:这个插件的作用是警告那些不起作用的属性。比如说你设置了display:inline,width: 200px
,其实这里的width
是不起作用的,此时这个插件就会发出警告
chalk
打印有颜色文字的插件:用法比如说
// 控制台打印红色的hello
require('chalk').red('hello')
clean-webpack-plugin
webpack使用的插件,一般用在production环境,用来清除文件夹用的,就是类似rm -rf ./dist
conventional-changelog-cli、@commitlint/cli、@commitlint/config-conventional
commitlint
可以帮助我们进行 git commit
时的 message 格式是否符合规范,conventional-changelog
可以帮助我们快速生成 changelog
@commitlint/config-conventional
类似 eslint 配置文件中的 extends ,它是官方推荐的 angular
风格的 commitlint 配置
copy-webpack-plugin
在webpack中拷贝文件和文件夹
cross-env
它是运行跨平台设置和使用环境变量(Node中的环境变量)的脚本。因为在windows和linux|mac里设置环境变量的方法不一致,比如说
// 在windows系统上,我们使用:
"SET NODE_ENV=production && webpack --config build/webpack.config.js"
// 在Lunix系统和安装并使用了bash的windows的系统上,我们会使用:
"EXPORT NODE_ENV=production && webpack --config build/webpack.config.js"
mini-css-extract-plugin、css-minimizer-webpack-plugin
webpack 4.0
以后,官方推荐使用mini-css-extract-plugin
插件来打包css文件(从css文件中提取css代码到单独的文件中,对css
代码进行代码压缩等)
相对的,如果你不想提取css
,可以使用style-loader
,将css
内嵌到html
文件里。
使用方法和效果如下:(后面会在webpack配置文件分析里看到),
先举一个基础配置的例子。 webpack.config.js
:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css'
}),
],
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader, 'css-loader','postcss-loader' // postcss-loader 可选
],
},{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader, 'css-loader','postcss-loader','less-loader' // postcss-loader 可选
],
}
],
},
};
- 实战案例
基于以上配置
- 如果入口
app.js
中引用了Root,js
Root
引入了Topics.js
- 而
Root.js
中引用样式main.css
Topics.js
中引用了topics.css
。
// 入口文件 app.js
import Root from './components/Root'
// Root.js
import '../styles/main.less'
import Topics from './Topics'
// Topics.js
import "../styles/topics.less"
这种情况下,Topics
会和 Root
同属一个 chunk
,所以会一起都打包到 app.js
中, 结果就是 main.less 和 topics.less 会被提取到一个文件中:app.css
。而不是生成两个 css
文件。
Asset Size Chunks Chunk Names
app.css 332 bytes 1 [emitted] app
app.js 283 KiB 1 [emitted] [big] app
- 代码情景二
但是,如果 Root.js
中并没有直接引入 Topics
组件,而是配置了代码分割 ,比如模块的动态引入(也就是说你的topics模块,是impot()动态引入的),那么结果就不一样了:
Asset Size Chunks Chunk Names
app.css 260 bytes 1 [emitted] app
app.js 281 KiB 1 [emitted] [big] app
topics.bundle.js 2.55 KiB 4 [emitted] topics
topics.css 72 bytes 4 [emitted] topics
因为这个时候有两个 chunk,对应了两个 JS 文件,所以会提取这两个 JS 文件中的 CSS 生成对应的文件。这才是“为每个包含 CSS 的 JS 文件创建一个单独的 CSS 文件”
的真正含义。
- 情景三
但是,如果分割了 chunk
,还是只希望只生成一个 CSS
文件怎么办呢?也是可以做到的。但需要借助 Webpack 的配置 optimization.splitChunks.cacheGroups
。
先来看看配置怎么写的:
optimization: {
splitChunks: {
cacheGroups: {
// Extracting all CSS/less in a single file
styles: {
name: 'styles',
test: /\.(c|le)ss$/,
chunks: 'all',
enforce: true,
},
}
}
},
打包结果:
Asset Size Chunks Chunk Names
app.js 281 KiB 2 [emitted] [big] app
styles.bundle.js 402 bytes 0 [emitted] styles
styles.css 332 bytes 0 [emitted] styles
topics.bundle.js 2.38 KiB 5 [emitted] topics
继续加强上面的配置,压缩上面分理处的代码,
css-minimizer-webpack-plugin
是用来压缩分离出来的css的。使用方法如下:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /.s?css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
},
],
},
optimization: {
minimizer: [
// For webpack@5 you can use the `...` syntax to extend existing minimizers (i.e. `terser-webpack-plugin`), uncomment the next line
// `...`,
new CssMinimizerPlugin(),
],
},
};
detect-port-alt
这个包用来检测对应端口是否被占用,比如项目里发现启动3000端口被占用的话就+1,直到选择一个不被占用的端口(端口上限是65535)。
error-overlay-webpack-plugin
它提供了和 create-react-app 一样的错误遮罩:
用法如下:
const ErrorOverlayPlugin = require('error-overlay-webpack-plugin')
module.exports = {
plugins: [new ErrorOverlayPlugin()],
devtool: 'cheap-module-source-map', // 'eval' is not supported by error-overlay-webpack-plugin
}
@typescript-eslint/eslint-plugin、@typescript-eslint/parser
@typescript-eslint/parser:ESLint的解析器,用于解析typescript,从而检查和规范Typescript代码
@typescript-eslint/eslint-plugin:这是一个ESLint插件,包含了各类定义好的检测Typescript代码的规范
配置如下所示:
module.exports = {
parser: '@typescript-eslint/parser', // 定义ESLint的解析器
extends: ['plugin:@typescript-eslint/recommended'],// 定义文件继承的子规范
plugins: ['@typescript-eslint'],// 定义了该eslint文件所依赖的插件
env:{ // 指定代码的运行环境
browser: true,
node: true,
}
}
fork-ts-checker-webpack-plugin
它在一个单独的进程上运行类型检查器,该插件在编译之间重用抽象语法树,并与TSLint共享这些树。可以通过多进程模式进行扩展,以利用最大的CPU能力。
html-webpack-plugin
这个插件非常常用,几乎是必备的。
它的作用是:当使用 webpack
打包时,创建一个 html
文件,并把 webpack
打包后的静态文件自动插入到这个 html 文件当中。简单实用如下(讲webpack文件时会更详细介绍api):
{
entry: 'index.js',
output: {
path: __dirname + '/dist',
filename: 'bundle.js'
},
plugins: [
new HtmlWebpackPlugin({
title: 'My App',
filename: 'assets/admin.html' // 在 output.path 目录下生成 assets/admin.html 文件
})
]
}
husky、lint-staged
husky是一个npm包,安装后,可以很方便的在package.json配置git hook 脚本 。
比如,在 package.json 内配置如
"scripts": {
"lint": "eslint src"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
那么,在后续的每一次git commit
之前,都会执行一次对应的 hook 脚本npm run lint
。其他hook同理.
- lint-staged
如果我们 想对git 缓存区最新改动过的文件进行以上的格式化和 lint
规则校验,这就需要 lint-staged
了 。
如下:
{
"husky": {
"hooks": {
"pre-commit": "lint-staged",
}
},
"lint-staged": {
// 首先,我们会对暂存区后缀为 `.ts .tsx .js` 的文件进行 eslint 校验,
// --config 的作用是指定配置文件。
"*.{ts,tsx,js}": [
"eslint --config .eslintrc.js"
],
// 同理 是stylelint的校验
"*.{css,less,scss}": [
"stylelint --config .stylelintrc.js"
],
// prettier格式化
"*.{ts,tsx,js,json,html,yml,css,less,scss,md}": [
"prettier --write"
]
},
}
这里没有添加 --fix
来自动修复不符合规则的代码,因为自动修复的内容对我们不透明,这样不太好。
terser-webpack-plugin
是一个使用 terser
压缩js
的webpack
插件。
如果你使用的是 webpack v5
或以上版本,你不需要安装这个插件。webpack v5
自带最新的 terser-webpack-plugin
。如果使用 webpack v4
,则必须安装 terser-webpack-plugin v4
的版本。
简易用法如下,详细介绍留到后面webpack配置文件详解
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimizer: [new TerserPlugin(
parallel: true // 多线程
)],
},
};
package.json里的其它比较重要的字段
{
"main": "index.js",
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/server",
"build": "cross-env NODE_ENV=production webpack --config ./scripts/config/webpack.prod.js",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s",
"lint": "npm run lint-eslint && npm run lint-stylelint",
"lint-eslint": "eslint -c .eslintrc.js --ext .ts,.tsx,.js src",
"lint-stylelint": "stylelint --config .stylelintrc.js src/**/*.{less,css,scss}"
},
"browserslist": [">0.2%", "not dead", "ie >= 9", "not op_mini all"],
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"commit-msg": "commitlint --config .commitlintrc.js -E HUSKY_GIT_PARAMS"
}
},
"lint-staged": {
"*.{ts,tsx,js}": ["eslint --config .eslintrc.js"],
"*.{css,less,scss}": ["stylelint --config .stylelintrc.js"],
"*.{ts,tsx,js,json,html,yml,css,less,scss,md}": ["prettier --write"]
}
}
这里的main
需要跟一些其它字段来一起比较。比如browser,module,main
三个字段都可以出现在package.json
中,它们有什么区别呢?
我们直接说结论,具体详细分析,详情参考这篇文章开发插件package.json在webpack构建中的表现
结论
- webpack 选择 web 浏览器环境
- 插件的 package.json 是否配置了 browser 字段
- 存在:选择 browser 作为入口
- 不存在:
- 插件的 package.json 是否配置了 module 字段
- 存在:选择 module 作为入口
- 不存在:以 main 作为入口
- webapack 选择 node环境
- 插件的 package.json 是否配置了 module 字段
- 存在:选择 module 作为入口
- 不存在:以 main 作为入口
- 插件的 package.json 是否配置了 module 字段
根据上面的行为总结,我们在开发插件的时候,需要考虑插件是提供给web
环境还是node
环境,如果都涉及到且存在区别,就要明确指出 browser、module
字段。如果没有任何区别的话,使用 main
入口足以
.vscode中settings文件
这个文件对于使用vscode的用户比较重要,有一些设置非常棒,比如点击ctrl+s自动格式化你的文件,设置如下:
"editor.formatOnSave": true, // 自动格式化代码,我们使用的是prettier
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true, // 保存自动修复 eslint报错(有些报错必须手动修复)
"source.fixAll.stylelint": true // 保存自动修复 stylelint报错,也就是css报错
}
下图是具体的settings文件,逐一注释其中的作用
{
"css.validate": false, // 禁用vscode本身的css校验功能
"less.validate": false, // 禁用vscode本身的less校验功能
"scss.validate": false, // 禁用vscode本身的scss校验功能
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], // eslint 校验的文件格式
"search.exclude": { // 搜索文件时排除的文件夹
"**/node_modules": true,
"dist": true,
"build": true
},
"editor.formatOnSave": true, // 保存时,自动格式化
"editor.codeActionsOnSave": { // 保存时自动格式化eslint的规则和stylint的规则
"source.fixAll.eslint": true,
"source.fixAll.stylelint": true
},
"[javascript]": { // 底下类似是指校验js,jsx,ts,tsx的校验器是prettier,而不是vscode默认的
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
上面的保存时自动格式化eslint的规则和stylint的规则,需要注意的是,有些规则是必须手动修改的,不会自动保存格式化。
babelrc文件解析
下面的presets和plugins的区别是,
-
presets是一些预设,插件的对应名字是
babel-preset-xxx
。Babel
插件一般尽可能拆成小的力度,开发者可以按需引进。但是一个一个引进有时候很麻烦,能不能把一些常用的插件打成一个包给我们用呢,这就是presets
的作用和。 -
plugins
就是一个一个的插件集合,你要配特定的功能就可以加入到plugins中
以下的所有插件之前都介绍过,可以试着回忆一下哦
module.exports = {
presets: [
[
'@babel/preset-env', // 将基于你的实际浏览器及运行环境,自动的确定babel插件及polyfill
{
useBuiltIns: 'usage', // 按需使用
modules: false, // 意思是不转义import语法,主要是为了tree-shaking
},
],
'@babel/preset-react', // 转化js、jsx文件的插件集合
'@babel/preset-typescript', // 转化ts,tsx文件的插件集合
],
plugins: [
[
'@babel/plugin-transform-runtime',// 优化polyfill的插件
{
corejs: {
version: 3,
proposals: true,
},
},
],
],
};
这里详细解释一下@babel/preset-env
这个插件的详细的常见使用参数,因为它很重要,是babel
转义我们代码的关键插件:
- targets属性,最常见的是
- targets.node : 它可以指定编译当前node版本,或者 "node": true 或者 "node": "current", 它与 "node": process.versions.node 相同。
- targets.browsers:可以利用 browserslist 查询选择的浏览器 (例如: last 2 versions, > 5%)
但是这里不建议把browsers
信息写在eslinttc
里面,因为可能其他的插件也需要浏览器信息,最好写在package.json
中。
例如:
"browserslist": [">0.2%", "not dead", "ie >= 9", "not op_mini all"],
-
modules属性,如果是false,就是说导出方式是按es6 module,默认是commonjs规范
-
useBuiltIns:规定如何引入polyfill,比如说有些浏览器不支持promise,我们需要引入polyfill去兼容这些不支持promise的浏览器环境
- 值为usage 会根据配置的浏览器兼容,以及你代码中用到的 API 来进行 polyfill,实现了按需添加,并且使用了
useBuiltIns: 'usage'之后,就不必手动在入口文件中
import '@babel/polyfill'` - 值为
entry
配置项时, 根据target
中浏览器版本的支持,将polyfills
拆分引入,仅引入有浏览器不支持的polyfill
- corejs选项, 这个选项只会在与
useBuiltIns: usage
或者useBuiltIns: entry
一起使用时才会生效, 确保@babel/preset-env
为你的core-js
版本注入了正确的引入
- 值为usage 会根据配置的浏览器兼容,以及你代码中用到的 API 来进行 polyfill,实现了按需添加,并且使用了
接着,我们解释一下在'@babel/plugin-transform-runtime'插件的配置:
- corejs: 比如
['@babel/plugin-transform-runtime', { corejs: 2 }]
,指定一个数字将引入corejs
来重写需要polyfillAPI
的helpers
,这需要使用@babel/runtime-corejs2
作为依赖
技术细节:transform-runtime
转换器插件会做三件事:
- 当你使用
generators/async
函数时,自动引入@babel/runtime/regenerator
(可通过regenerator选项切换) - 若是需要,将使用
core-js
作为helpers
,而不是假定用户已经使用了polyfill
(可通过corejs选项切换) - 自动移除内联的 Babel helpers并取而代之使用
@babel/runtime/helpers
模块(可通过helpers选项切换)
最后,我们要提一个问题,就是import 通过webpack转义之后,变成了什么样子,我们用案例来说。
如下是一个非常简单的webpack编译的模块。
import { tmpPrint } from './tmp.js'
export function print () {
tmpPrint()
console.log('我是 num.js 的 print 方法')
}
会被webpack编译为以路径为key,以函数为value的对象。
{
"./src/num.js":
(function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, "print", function () { return print; });
// 加载 ./src/tmp.js 模块
var _tmp_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/tmp.js");
function print() {
Object(_tmp_js__WEBPACK_IMPORTED_MODULE_0__["tmpPrint"])()
console.log('我是 num.js 的 print 方法')
}
//# sourceURL=webpack:///./src/num.js?");
}),
}
我们接着看一下__webpack_require__.r,webpack_require.d,__webpack_require__分别是什么:
function __webpack_require__(moduleId) {
// 所有模块都会被缓存,如果在缓存里就直接从缓存里拿
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 这里是缓存的定义,i是id的意思,l是load的意思,exports是导出的内容
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 如果不是在缓存里,取出模块把module, module.exports, __webpack_require__作为参数放入到模块里
// 如下的modules[moduleId]中保存的内容就相当于这块内容:
// (function (module, __webpack_exports__, __webpack_require__) {
// "use strict";
// __webpack_require__.r(__webpack_exports__);
// __webpack_require__.d(__webpack_exports__, "print", function () { return // print; });
// 加载 ./src/tmp.js 模块
// var _tmp_js__WEBPACK_IMPORTED_MODULE_0__ = // __webpack_require__("./src/tmp.js");
// function print() {
// Object(_tmp_js__WEBPACK_IMPORTED_MODULE_0__["tmpPrint"])()
// console.log('我是 num.js 的 print 方法')
// }
//# sourceURL=webpack:///./src/num.js?");
// }),
// }
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
这下是不是懂了,很简单啊这个函数!就是加载和暴露模块用的
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
__webpack_require__.d = function (exports, name, getter) {
// __webpack_require__.o这个函数的意思是检查name是否是exports的属性
if (!__webpack_require__.o(exports, name)) {
// 如果exports原本没有name属性,就用defineProperty去定义name属性
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
// 这个函数就是用来标识是否是es模块的
__webpack_require__.r = function (exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
好了,我们接着回头接续分析那个src/sum.js的模块
{
"./src/num.js":
// 大家还记得上面讲require有一句
// modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 我们可以看到 module就等于下面函数的 module,代表这个模块对象
// module.exports对应__webpack_exports__,也就是__webpack_exports__代表module导出的对象
// __webpack_require__对应function的 __webpack_require__参数,也就是导入模块函数
(function (module, __webpack_exports__, __webpack_require__) {
"use strict";
// 这句话的意思把导出的exports对象标记为esmodule
__webpack_require__.r(__webpack_exports__);
// 这句话的意思是把模块下面的print函数,放入exports导出对象
__webpack_require__.d(__webpack_exports__, "print", function () { return print; });
// 加载 ./src/tmp.js
var _tmp_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/tmp.js");
function print() {
Object(_tmp_js__WEBPACK_IMPORTED_MODULE_0__["tmpPrint"])()
console.log('我是 num.js 的 print 方法')
}
//# sourceURL=webpack:///./src/num.js?");
}),
}
经过上面分析,发现最关键的一句就是__webpack_require__.r(webpack_exports),把导出对象标记为esModule,如果你没有用import,而是用commonjs的require,那么就不会有这一句
那问题又来了,如果是表示esmodule了,有啥用啊!这部分我就不写了,要不就是一篇专门讲import和require区别的文章了。
我直接说结论了
- 如果是import Header form './Header',在webpack里会转译为类似
require('./Header').default
- 如果是import * as Header form './Header',在webpack里会转译为类似
const Header = require('./Header')
Header.default 表示导出的Header组件
Header.A代表导出的A
// Header.js
export default Header;
export const A=1;
- 如果是import { A } form './Header',在webpack里会转译为类似
require('./Header').A
export default Header
会被挂在exports的default属性上export const A=1
,会被挂在exports的A属性上
意思是es6模块实际上被webpack
的一套规则还是变味了commonjs
规范而已。
上面没看懂?没关系的,更具体更清晰的推论,在tsconfig.js
文件的esModuleInterop参数讲解中会有更清晰的解释(这个是站在webpack编译的角度,下面esModuleInterop参数是在ts编译的角度,其实原理都是一样的)。
以下比较简单的文件,我就在文件注释中解释参数了
.commitlintrc文件解析
如下的rules的规范如下:rule
由name
和配置数组组成,如:'name:[0, 'always', 72]'
,数组中第一位为level
,可选0,1,2
,0
为disable
,1
为warning
,2
为error
,第二位为应用与否,可选always|never
,第三位该rule的值,下面的值代表你的commit开头必须是这些字段
module.exports = {
extends: ['@commitlint/config-conventional'], // 这个插件继承的是angular团队的提交规范
rules: {
'type-enum': [ // 解释上面已经提过数组每一位的意思
2,
'always',
['build', 'ci', 'chore', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test'],
],
},
};
.editorconfig文件分析
// 表明是最顶层的配置文件,发现设为 true 时,才会停止查找.editorconfig 文件
root = true
[*]
// tab 为 hard-tabs,space 为 soft-tabs 表示缩进符号,我们选的空格
indent_style = space
// 设置整数表示规定每级缩进的列数和 soft-tabs 的空格数。
// 如果设定为 tab,则会使用 tab_width 的值(如果已指定)
indent_size = 2
// 定义换行符,支持 lf、cr 和 crlf
end_of_line = lf
// 编码格式,支持 latin1、utf-8、utf-8-bom、utf-16be 和 utf-16le,不建议使用 uft-8-bom
charset = utf-8
// 设为 true 表示会除去换行行首的任意空白字符,false 反之
trim_trailing_whitespace = true
// 设为 true 表明使文件以一个空白行结尾,false 反之
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
.eslintrc文件分析
const OFF = 0;
const WARN = 1;
const ERROR = 2;
module.exports = {
// 要在配置文件中指定环境,请使用env键并通过将每个设置为来指定要启用的环境true。
// 例如,以下启用浏览器和Node.js环境:
// es6表示对于新的ES6全局变量,比如Set的支持,注意跟下面parserOptions的ecmaVersion对比一下
// ecmaVersion: 6 表示启用对于ES6语法的校验
//
env: {
browser: true,
es6: true,
node: true,
},
//
extends: [
'airbnb',
'airbnb/hooks',
'plugin:react/recommended',
'plugin:unicorn/recommended',
'plugin:promise/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
// 指定解析器
// 默认情况下,ESLint使用Espree作为其解析器。您可以选择指定在配置文件中使用其他解析器
parser: '@typescript-eslint/parser',
// parserOptions属性在文件中设置解析器选项
// ecmaVersion-设置为3、5(默认),6、7、8、9、10或11,以指定要使用的ECMAScript语法的版本。
// sourceType-设置为"script"(默认),或者"module"代码在ECMAScript模块中。
// ecmaFeatures -一个对象,指示您要使用哪些其他语言功能,参数如下
// globalReturn-允许return在全局声明
// impliedStrict-启用全局严格模式(如果ecmaVersion大于等于5)
// jsx-启用JSX
parserOptions: {
ecmaFeatures: {
impliedStrict: true,
jsx: true,
},
ecmaVersion: 12,
sourceType: 'module',
},
plugins: ['react', 'unicorn', 'promise', '@typescript-eslint', 'prettier'],
settings: {
// 这里的improt/resolver针对插件是eslint-import-resolver-xxx
// 比如下面的typescript里的规则,针对的就是插件eslint-import-resolver-typescript
// 再下面的node就是配置eslint-import-resolver-node
// 有人说我们没依赖eslint-import-resolver-node,哪里来的呢,是因为
// eslint-import-plugin插件依赖,所以也安装了它,这就要涉及到npm依赖包平铺的规则了,下面会讲
'import/resolver': {
typescript: {
directory: './tsconfig.json', // 这里主要解决的是别名的问题,tsconfig.json里有别名设置
},
node: {
extensions: ['.tsx', '.jsx', '.ts', '.js'],
},
},
},
rules: {
// 以下规则就不详细讲了,因为很多都是因为typescript插件bug跟eslint冲突不得不关闭一些规则
'import/extensions': [
ERROR,
'ignorePackages',
{
ts: 'never',
tsx: 'never',
js: 'never',
},
],
'import/no-extraneous-dependencies': [ERROR, { devDependencies: true }],
'import/prefer-default-export': OFF,
'import/no-unresolved': ERROR,
'import/no-dynamic-require': OFF,
'unicorn/better-regex': ERROR,
'unicorn/prevent-abbreviations': OFF,
'unicorn/filename-case': [
ERROR,
{
cases: {
// 中划线
kebabCase: true,
// 小驼峰
camelCase: true,
// 下划线
snakeCase: false,
// 大驼峰
pascalCase: true,
},
},
],
'unicorn/no-array-instanceof': WARN,
'unicorn/no-for-loop': WARN,
'unicorn/prefer-add-event-listener': [
ERROR,
{
excludedPackages: ['koa', 'sax'],
},
],
'unicorn/prefer-query-selector': ERROR,
'unicorn/no-null': OFF,
'unicorn/no-array-reduce': OFF,
'@typescript-eslint/no-useless-constructor': ERROR,
'@typescript-eslint/no-empty-function': WARN,
'@typescript-eslint/no-var-requires': OFF,
'@typescript-eslint/explicit-function-return-type': OFF,
'@typescript-eslint/explicit-module-boundary-types': OFF,
'@typescript-eslint/no-explicit-any': OFF,
'@typescript-eslint/no-use-before-define': ERROR,
'@typescript-eslint/no-unused-vars': WARN,
'no-unused-vars': OFF,
'react/jsx-filename-extension': [ERROR, { extensions: ['.tsx', 'ts', '.jsx', 'js'] }],
'react/jsx-indent-props': [ERROR, 2],
'react/jsx-indent': [ERROR, 2],
'react/jsx-one-expression-per-line': OFF,
'react/destructuring-assignment': OFF,
'react/state-in-constructor': OFF,
'react/jsx-props-no-spreading': OFF,
'react/prop-types': OFF,
'jsx-a11y/click-events-have-key-events': OFF,
'jsx-a11y/no-noninteractive-element-interactions': OFF,
'jsx-a11y/no-static-element-interactions': OFF,
'lines-between-class-members': [ERROR, 'always'],
// indent: [ERROR, 2, { SwitchCase: 1 }],
'linebreak-style': [ERROR, 'unix'],
quotes: [ERROR, 'single'],
semi: [ERROR, 'always'],
'no-unused-expressions': WARN,
'no-plusplus': OFF,
'no-console': OFF,
'class-methods-use-this': ERROR,
'jsx-quotes': [ERROR, 'prefer-single'],
'global-require': OFF,
'no-use-before-define': OFF,
'no-restricted-syntax': OFF,
'no-continue': OFF,
},
};
上面提到一个重要的点,就是eslint-import-resolver-node
,我们并没有在package.json
声明,咋node_modules里面就有它了呢?入下
- node_modules
- eslint-import-resolver-node // 为啥没有安装它,它确在第一层
这就涉及到npm安装依赖包的规则了,因为eslint-import-plugin
依赖eslint-import-resolver-node
,所以,node_modules
里面就会有,我们就简单讲一下npm
包安装(install)规则。
这问题曾经我面试的时候也遇到过,接下来我们简单了解一下:
嵌套结构
我们都知道,执行 npm install
后,依赖包被安装到了 node_modules
,在 npm
的早期版本, npm 处理依赖的方式简单粗暴,以递归的形式,严格按照 package.json
结构以及子依赖包的 package.json
结构将依赖安装到他们各自的 node_modules
中。直到有子依赖包不在依赖其他模块。也就是说,假如你的package.json如下
{
A模块:"1.0.0",
B模块:"1.0.0"
}
然后B模块有依赖C模块,B模块的package.json如下
{
C模块:"1.0.0"
}
那么整个项目依赖就是嵌套的,如下:
node_modules
- A模块
- B模块
- C模块
在 Windows 系统中,文件路径最大长度为260个字符,嵌套层级过深可能导致不可预知的问题。
扁平结构
为了解决以上问题,NPM 在 3.x 版本做了一次较大更新。其将早期的嵌套结构改为扁平结构:
安装模块时,不管其是直接依赖还是子依赖的依赖,优先将其安装在 node_modules
根目录。
还是上面的依赖结构,我们在执行 npm install
后将得到下面的目录结构:
node_modules
- A模块
- B模块
- C模块
当安装到相同模块时,判断已安装的模块版本是否符合新模块的版本范围,如果符合则跳过,不符合则在当前模块的 node_modules 下安装该模块。
就是说假如C模块依赖A模块的2.0.0版本,依赖图如下:
node_modules
- A模块 1.0.0
- B模块
- C模块
- A模块 2.0.0
其实铺平的结构也会有问题,我们这里就不详述了,上面提到的那篇文章真的不错,推荐详细看,里面设置npm相关知识的点比这里谈到的多得多。
.npmrc文件分析
.prettierrc文件分析
{
// tab缩进大小,默认为2
tabWidth: 2,
// 使用tab缩进,默认false
useTabs: true,
// 使用分号, 默认true
semi: false,
// 使用单引号, 默认false(在jsx中配置无效, 默认都是双引号)
singleQuote: true,
// 行尾逗号,默认none,可选 none|es5|all
// es5 包括es5中的数组、对象
// all 包括函数对象等所有可选
TrailingCooma: "none",
// 对象中的空格 默认true
// true: { foo: bar }
// false: {foo: bar}
bracketSpacing: true,
// JSX标签闭合位置 默认false
// false: <div
// className=""
// style={{}}
// >
// true: <div
// className=""
// style={{}} >
jsxBracketSameLine:false,
// 箭头函数参数括号 默认avoid 可选 avoid| always
// avoid 能省略括号的时候就省略 例如x => x
// always 总是有括号
arrowParens: 'always',
}
.stylelintrc文件分析
module.exports = {
// stylelint的配置可以在已有配置的基础上进行扩展,之后你自己书写的配置项将覆盖已有的配置。
// 配置的含义,我们前面已经讲过简单提一下stylelint-config-rational-order是配置书写顺序的
extends: ['stylelint-config-standard', 'stylelint-config-rational-order', 'stylelint-config-prettier'],
// plugins一般是由社区提供的,对stylelint已有规则进行扩展
// 也就说有些规则原本stylelint没有,就要插件自定义规则了
// 'stylelint-declaration-block-no-ignored-properties'这个插件的作用是警告那些不起作用的属性
plugins: ['stylelint-order', 'stylelint-declaration-block-no-ignored-properties', 'stylelint-scss'],
rules: {
// rules不详述了,可以访问这个网站搜寻,https://stylelint.docschina.org/user-guide/plugins/
'plugin/declaration-block-no-ignored-properties': true,
'comment-empty-line-before': null,
'declaration-empty-line-before': null,
'function-name-case': 'lower',
'no-descending-specificity': null,
'no-invalid-double-slash-comments': null,
'block-no-empty': null,
'value-keyword-case': null,
'rule-empty-line-before': ['always', { except: ['after-single-line-comment', 'first-nested'] }],
'at-rule-no-unknown': null,
'scss/at-rule-no-unknown': true,
},
// 忽略校验的文件,其中/**/*是glob语法,指的是所有文件和文件夹
ignoreFiles: ['node_modules/**/*', 'build/**/*', 'dist/**/*'],
};
tsconfig.json文件分析
为什么使用 tsconfig.json?
通常我们可以使用 tsc 命令来编译少量 TypeScript 文件, 但如果实际开发的项目,很少是只有单个文件,当我们需要编译整个项目时,就可以使用 tsconfig.json 文件,将需要使用到的配置都写进 tsconfig.json 文件
{
// 编译选项,跟编译ts相关
"compilerOptions": {
// 指定编译的ECMAScript目标版本。
// 枚举值:"ES3", "ES5", "ES6"/ "ES2015", "ES2016", "ES2017","ESNext"。
// 默认值: “ES3”,ESNext包含提案的内容
"target": "ES5",
// 指定生成哪个模块系统代码。枚举值:"None", "CommonJS", "AMD", "System", "UMD",
// "ES6", "ES2015","ESNext"。默认值根据--target选项不同而不同,当target设置为ES6时,
// 默认module为“ES6”,否则为“commonjs”
"module": "ESNext",
// 编译过程中需要引入的库文件的列表。比如没有esnext,Set、Reflect等api会被ts报错
"lib": ["dom", "dom.iterable", "esnext"],
// 是否允许编译javascript文件。如果设置为true,js后缀的文件也会被typescript进行编译
"allowJs": true,
// 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
"jsx": "react",
// 下面详解
"isolatedModules": true,
// 用于指定是否启用严格的类型检查,不过到底具体怎么严格我也不知道
"strict": true,
// 下面详解
"moduleResolution": "node",
// 下面详解
"esModuleInterop": true,
"resolveJsonModule": true,
// 下面详解
"baseUrl": "./",
// 路径别名,跟webpack alias一样,注意你是ts的话,必须webpack和ts都配
"paths": {
"Src/*": ["src/*"],
"Components/*": ["src/components/*"],
"Utils/*": ["src/utils/*"]
},
// 以下两个是跟装饰器功能有关,experimentalDecorators是 是否开启装饰器
// emitDecoratorMetadata是装饰器里的一个功能,如果你使用依赖注入,有可能需要开启它
// 依赖注入不懂的同学可以略过,后面会写一篇关于学习nestjs前置知识的文章
// 会讲怎么使用emitDecoratorMetadata实现依赖注入
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
// 禁止对同一个文件的不一致的引用。主要是文件大小写必须一致,比如引用a.js和A.js是不一样的
"forceConsistentCasingInFileNames": true,
// 忽略所有的声明文件( `*.d.ts`)的类型检查
"skipLibCheck": true,
// 下面详解
"allowSyntheticDefaultImports": true,
// 不生成输出文件
"noEmit": true
},
"exclude": ["node_modules"]
}
上面的jsx选项可以有三个值选择,我们详细解释一下:
jsx可选项包括:preserve, react 和 react-native。
这些模式仅影响编译阶段 - 类型检查不受影响。
preserve
模式将保持JSX
作为输出的一部分,又后面的编译器继续编译(例如Babel)。 此外,输出将具有.jsx文件扩展名。- react模式将编译
React.createElement
,在使用之前不需要经过JSX
转换,输出将具有.js文件扩展名。 - react-native模式相当于保留,因为它保留了所有
JSX
,但输出将具有.js文件扩展名
isolatedModules,这个选项有点复杂,查阅了不少资料。。。下面详细讲一下:
- 导出非值标识符
在 TypeScript 中,你可以引入一个类型,然后再将其导出:
import { someType, someFunction } from "someModule";
someFunction();
export { someType, someFunction };
Try
由于 someType
并没有值,所以生成的 export
将不会导出它(否则将导致 JavaScript 运行时的错误):
export { someFunction };
单文件转译器并不知道 someType
是否会产生一个值,所以导出一个只指向类型的名称会是一个错误。
- 非模块文件
如果设置了 isolatedModules
,则所有的实现文件必须是模块 (也就是它有某种形式的 import
/export
)。如果任意文件不是模块就会发生错误:
function fn() {}
'index.ts' cannot be compiled under '--isolatedModules' because it is considered a global script file. Add an import, export, or an empty 'export {}' statement to make it a module.'index.ts' cannot be compiled under '--isolatedModules' because it is considered a global script file. Add an import, export, or an empty 'export {}' statement to make it a module.Try
此限制不适用于 .d.ts
文件
- 指向
const enum
成员
在 TypeScript 中,当你引用一个 const enum
的成员时,该引用在生成的 JavaScript 中将会被其实际值所代替。这会将这样的 TypeScript 代码:
declare const enum Numbers {
Zero = 0,
One = 1,
}
console.log(Numbers.Zero + Numbers.One);
转换为这样的 JavaScript:
"use strict";
console.log(0 + 1);
在不知道这些成员值的情况下,其他转译器不能替换对 Numbers
的引用。如果无视的话则会导致运行时错误(运行时没有 Numbers
) 对象。 正因如此,当启用 isolatedModules
时,引用环境中的 const enum
成员将会是一个错误
- moduleResolution (参考《tsconfig详细配资》,详见文章底部)
可选值: classic | node
我们举一个例子,看看两种模式的工作机制,假设用户主目录下有一个ts-test的项目,里面有一个src目录,src目录下有一个a.ts文件,即/Users/**/ts-test/src/a.ts
- classic模块解析规则:
-
对于相对路径模块: 只会在当前相对路径下查找是否存在该文件(.ts文件),不会作进一步的解析,如"./src/a.ts"文件中,有一行
import { b } from "./b"
,那么其只会检测是否存在"./src/b.ts"
,没有就算找不到。 -
对于非相对路径模块: 编译器则会从包含导入文件的目录开始依次向上级目录遍历,尝试定位匹配的ts文件或者d.ts类型声明文件,如果
/Users/**/ts-test/src/a.ts
文件中有一行import { b } from "b"
,那么其查找过程如下:
-
/Users/**/ts-test/src/b.ts
/Users/**/ts-test/src/b.d.ts
/Users/**/ts-test/b.ts
/Users/**/ts-test/b.d.ts
/Users/**/b.ts
/Users/**/b.d.ts
/Users/b.ts
/Users/b.d.ts
/b.ts
/b.d.ts
- node模块解析规则:
- 对于相对路径模块:除了会在当前相对路径下查找是否存在该文件(.ts文件)外,还会作进一步的解析,如果在相对目录下没有找到对应的.ts文件,那么就会看一下是否存在同名的目录
- 如果有,那么再看一下里面是否有package.json文件,然后看里面有没有配置,main属性
- 如果配置了,则加载main所指向的文件(.ts或者.d.ts),如果没有配置main属性,那么就会看一下目录里有没有index.ts或者index.d.ts,有则加载。
- 对于非相对路径模块: 对于非相对路径模块,那么会直接到a.ts所在目录下的node_modules目录下去查找,也是遵循逐层遍历的规则,查找规则同上,同上node模块解析规则查找如下(一般情况下是找):
/Users/**/ts-test/src/node_modules/b.ts
/Users/**/ts-test/src/node_modules/b.d.ts
/Users/**/ts-test/src/node_modules/b/package.json(如果指定了main)
/Users/**/ts-test/src/node_modules/b/index.ts
/Users/**/ts-test/src/node_modules/b/index.d.ts
/Users/**/ts-test/node_modules/b.ts
/Users/**/ts-test/node_modules/b.d.ts
/Users/**/ts-test/node_modules/b/package.json(如果指定了main)
/Users/**/ts-test/node_modules/index.ts
/Users/**/ts-test/node_modules/index.d.ts
/Users/**/node_modules/b.ts
/Users/**/node_modules/b.d.ts
/Users/**/node_modules/b/package.json(如果指定了main)
/Users/**/node_modules/index.ts
/Users/**/node_modules/index.d.ts
/Users/node_modules/b.ts
/Users/node_modules/b.d.ts
/Users/node_modules/b/package.json(如果指定了main)
/Users/node_modules/index.ts
/Users/node_modules/index.d.ts
/node_modules/b.ts
/node_modules/b.d.ts
/node_modules/b/package.json(如果指定了main)
/node_modules/index.ts
/node_modules/index.d.ts
以上需要注意一点的是,还有一个typeRoots属性,默认是node_modules/@types,并且不管是classic解析还是node解析,都会到node_modules/@types目录下查找类型声明文件,即typeRoots和模块的解析规则无关
- baseUrl
这个是用于拓宽引入非相对模块时的查找路径的。其默认值就是"./" ,比如当moduleResolution
属性值为node的时候,如果我们引入了一个非相对模块,那么编译器只会到node_modules目录下去查找,但是如果配置了baseUrl
,那么编译器在node_modules
中没有找到的情况下,还会到baseUrl中指定的目录下查找;
同样moduleResolution
属性值为classic
的时候也是一样,除了到当前目录下找之外(逐层),如果没有找到还会到baseUrl
中指定的目录下查找;就是相当于拓宽了非相对模块的查找路径范围
- allowSyntheticDefaultImports
当设置为 true, 并且模块没有显式指定默认导出时,allowSyntheticDefaultImports
可以让你这样写导入:
import React from "react";
而不是:
import * as React from "react";
例如:allowSyntheticDefaultImports
不为 true 时:
// @filename: utilFunctions.js
Module '"/home/runner/work/TypeScript-Website/TypeScript-Website/packages/typescriptlang-org/utilFunctions"' has no default export.Module '"/home/runner/work/TypeScript-Website/TypeScript-Website/packages/typescriptlang-org/utilFunctions"' has no default export.
const getStringLength = (str) => str.length;
module.exports = {
getStringLength,
};
// @filename: index.ts
import utils from "./utilFunctions";
const count = utils.getStringLength("Check JS");
这段代码会引发一个错误,因为没有“default”对象可以导入,即使你认为应该有。 为了使用方便,Babel 这样的转译器会在没有默认导出时自动为其创建,使模块看起来更像:
// @filename: utilFunctions.js
const getStringLength = (str) => str.length;
const allFunctions = {
getStringLength,
};
module.exports = allFunctions;
module.exports.default = allFunctions;
本选项不会影响 TypeScript 生成的 JavaScript,它仅对类型检查起作用。
- esModuleInterop
这个参数涉及到es6模块和commonjs模块互相转换知识点了。具体参考这篇文章(这一参数就是一篇文章 esModuleInterop 到底做了什么?, 我这里简引用一下这篇文章的关键点。
首先我们看一下import
语法在ts中是如何被转译的!
- TS 默认编译规则
TS 对于 import 变量的转译规则为:
// before
import React from 'react';
console.log(React)
// after
var React = require('react');
console.log(React['default'])
// before
import {Component} from 'react';
console.log(Component);
// after
var React = require('react');
console.log(React.Component)
// before
import * as React from 'react';
console.log(React);
// after
var React = require('react');
console.log(React);
结论,可以看到:
- 对于
import
导入默认导出的模块,TS
在读这个模块的时候会去读取上面的default
属性 - 对于
import
导入非默认导出的变量,TS
会去读这个模块上面对应的属性 - 对于
import *,TS
会直接读该模块
TS、babel
对 export` 变量的转译规则为:(代码经过简化)
// before
export const name = "esm";
export default {
name: "esm default",
};
// after
exports.__esModule = true;
exports.name = "esm";
exports["default"] = {
name: "esm default"
}
可以看到:
- 对于
export default
的变量,TS
会将其放在module.exports
的 default 属性上 - 对于
export
的变量,TS
会将其放在module.exports
对应变量名的属性上 - 额外给
module.exports
增加一个__esModule: true 的属性,用来告诉编译器,这本来是一个 esm 模块
TS 开启 esModuleInterop 后的编译规则
回到标题上,esModuleInterop
这个属性默认为 false。改成 true 之后,TS 对于 import 的转译规则会发生一些变化(export 的规则不会变):
// before
import React from 'react';
console.log(React);
// after 代码经过简化
// __importDefault规则如下:
// 如果目标模块是 esm,就直接返回目标模块;否则将目标模块挂在一个对象的 defalut 上,返回该对象
var react = __importDefault(require('react'));
console.log(react['default']);
// before
import {Component} from 'react';
console.log(Component);
// after 代码经过简化
var react = require('react');
console.log(react.Component);
// before
import * as React from 'react';
console.log(React);
// after 代码经过简化
// _importStar 规则如下
// 如果目标模块是 esm,就直接返回目标模块。否则
// 将目标模块上所有的除了 default 以外的属性挪到 result 上
// 将目标模块自己挂到 result.default 上
var react = _importStar(require('react'));
console.log(react);
可以看到,对于默认导入和 namespace(*)导入,TS 使用了两个 helper 函数来帮忙
// 代码经过简化
var __importDefault = function (mod) {
return mod && mod.__esModule ? mod : { default: mod };
};
var __importStar = function (mod) {
if (mod && mod.__esModule) {
return mod;
}
var result = {};
for (var k in mod) {
if (k !== "default" && mod.hasOwnProperty(k)) {
result[k] = mod[k]
}
}
result["default"] = mod;
return result;
};
其实这个参数对于我们项目而言没有用,因为@babel/preset-typescript
会把类型清除掉,webpack
不会调用 tsc
,tsconfig.json
也会被忽略掉。
但是可以帮助我们拓宽视野,这样面试官让你聊es6
模块和commonjs
模块转换的话题(cjs 导入 esm (一般不会这样使用,除开这种情况),就会游刃有余
webpack相关配置
首先是工具文件:
env.js
// 判读是否是生产环境,这里这个项目的作者取了一个巧,判断非develop环境是这样的
// process.env.NODE_ENV !== 'production'
// 这样写不要好,有可能你们公司有很多环境,比如还有预发、灰度环境等等
const isDevelopment = process.env.NODE_ENV !== 'production';
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
isDevelopment,
isProduction,
};
path.js
// 以下是两个node模块
const path = require('path');
const fs = require('fs');
// 同步获取node执行的文件的工作目录, 我们的工作目录一般都是项目的根目录,这里就表示根目录
// 为啥这么说呢,因为package.json写着webpack --config ./scripts/config/webpack.prod.js
// webpack就是借助node的能力,它的 ./scripts就暴露是以项目目录为根目录
// 这里需要注意process.cwd和__dirname的区别
// process.cwd()返回当前工作目录。如:调用node命令执行脚本时的目录。
// __dirname返回源代码所在的目录
const appDirectory = fs.realpathSync(process.cwd());
// 获取绝对路径的方法函数
function resolveApp(relativePath) {
return path.resolve(appDirectory, relativePath);
}
// 默认extentions
const moduleFileExtensions = ['ts', 'tsx', 'js', 'jsx'];
/**
* Resolve module path
* @param {function} resolveFn resolve function
* @param {string} filePath file path
*/
function resolveModule(resolveFn, filePath) {
// Check if the file exists
const extension = moduleFileExtensions.find((ex) => fs.existsSync(resolveFn(`${filePath}.${ex}`)));
if (extension) {
return resolveFn(`${filePath}.${extension}`);
}
return resolveFn(`${filePath}.ts`); // default is .ts
}
module.exports = {
appBuild: resolveApp('build'),
appPublic: resolveApp('public'),
appIndex: resolveModule(resolveApp, 'src/index'), // Package entry path
appHtml: resolveApp('public/index.html'),
appNodeModules: resolveApp('node_modules'), // node_modules path
appSrc: resolveApp('src'),
appSrcComponents: resolveApp('src/components'),
appSrcUtils: resolveApp('src/utils'),
appProxySetup: resolveModule(resolveApp, 'src/setProxy'),
appPackageJson: resolveApp('package.json'),
appTsConfig: resolveApp('tsconfig.json'),
moduleFileExtensions,
};
webpack.common.js
这是webpack
生产环境和开发环境共同的配置文件
以下需要特别注意的参数是'css-loader'里有个importLoaders的参数,它的意思是需要举一个例子就明白了,
如下图:importLoader是1
-
我们在写
sass
或者less
的时候可以@import
去引入其他的sass
或less
文件,此时引用的文件如何被loader处理就跟这个参数有关了。 -
当
css-loader
处理index.scss
文件,读取到@import
语句的时候, 因为将importLoaders
设置为1
,那么a.scss
和b.scss
会被postcss-loader
给处理 -
如果将
importLoaders
设置为2
,那么a.scss
和b.scss
就会被postcss-loader
和sass-loader
给处理
下面的externals
属性是一个常见webpack
优化点,比如你会把react,react-dom
放入cdn
,这样就不用打包他们
这里还有一些webpack5
和webpack4
相同功能但配置有些区别的点:
- 之前使用
file-loader
,但是webpack5
现在已默认内置资源模块,根据官方配置,现在可以改为以下配置方式,不再需要安装额外插件:
module.exports = {
output: {
// ...
assetModuleFilename: 'images/[name].[contenthash:8].[ext]',
},
// other...
module: {
rules: [
// other...
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 4 * 1024,
},
},
},
{
test: /\.(eot|svg|ttf|woff|woff2?)$/,
type: 'asset/resource',
},
]
},
plugins: [//...],
}
缓存
这里提一个醒dll
在webpack
里已经过时了!过时了!以后谁给你推荐这个webpack优化就别理他就行了!因为配置hard-source-webpack-plugin都比配置dll容易的多,这还是webpack4的配置。都过时了
之前可以使用插件 hard-source-webpack-plugin
实现缓存,大大加快二次编译速度,现在webpack5
现在默认支持缓存,我们只需要以下配置即可:
module.exports = {
//...
cache: {
// 默认type是memory也就是缓存放到内存中
type: 'filesystem',
buildDependencies: {
config: [__filename],
},
},
//...
};
cache.buildDependencies
,它可以指定构建过程中的代码依赖。它的值类型有两种:文件和目录。
- 目录类型必须以斜杠(/)结尾。其他所有内容都解析为文件类型。
- 对于目录类型来说,会解析其最近的 package.json 中的 dependencies。
- 对于文件类型来说,我们将查看 node.js 模块缓存以寻找其依赖。
如下示例的意思是:
__filename
变量指向 node.js
中的当前文件。
cache.buildDependencies: {
// 它的作用是当配置文件内容或配置文件依赖的模块文件发生变化时,当前的构建缓存即失效
config: [__filename]
}
注意:当设置 cache.type: "filesystem"
时,webpack
会在内部以分层方式启用文件系统缓存和内存缓存。 从缓存读取时,会先查看内存缓存,如果内存缓存未找到,则降级到文件系统缓存。 写入缓存将同时写入内存缓存和文件系统缓存。也就是说它比memory
模式更好
// 插件把 webpack 打包后的静态文件自动插入到 html 文件当中
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 用来分离css为单独的文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// 添加打包进度条插件
const WebpackBar = require('webpackbar');
// 它在一个单独的进程上运行类型检查器,该插件在编译之间重用抽象语法树,并与TSLint共享这些树。
// 可以通过多进程模式进行扩展,以利用最大的CPU能力。
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
// 在webpack中拷贝文件和文件夹
const CopyPlugin = require('copy-webpack-plugin');
// 引入路径工具,上文已讲
const paths = require('../paths');
// 引入环境判断工具,上文已讲
const { isDevelopment, isProduction } = require('../env');
// 引入配置文件,上文已讲
const { imageInlineSizeLimit } = require('../conf');
// 这个函数是用来加载css相关loader的函数
// 如果是开发环境用style-loader,将css内嵌到html中,反之css单独打包
const getCssLoaders = (importLoaders) => [
isDevelopment ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: false,
sourceMap: isDevelopment,
importLoaders,
},
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
require('postcss-flexbugs-fixes'),
isProduction && [ // 开发环境不使用postcss-preset-env加浏览器前缀,加快打包时间
'postcss-preset-env',
{
autoprefixer: {
grid: true,
flexbox: 'no-2009',
},
stage: 3,
},
],
].filter(Boolean),
},
},
},
];
module.exports = {
// 入口信息
entry: {
app: paths.appIndex,
},
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename],
},
},
// 这里可以设置extensions和别名
// extensions就是webpack会识别的文件后缀的顺序,
// 如果你是tsx建议放到第一位,否则你写成['ts','tsx']会先检测是否是ts文件,不是才接着看是不是tsx
resolve: {
extensions: ['.tsx', '.ts', '.js', '.json'],
alias: {
Src: paths.appSrc,
Components: paths.appSrcComponents,
Utils: paths.appSrcUtils,
},
},
externals: {
react: 'React',
'react-dom': 'ReactDOM',
axios: 'axios',
},
module: {
rules: [
{
test: /\.(tsx?|js)$/,
loader: 'babel-loader',
options: { cacheDirectory: true }, // 这是一个webpack优化点,使用缓存
exclude: /node_modules/, // 这个也是webpack优化的点 exclude排除不需要编译的文件夹
},
{
test: /\.css$/,
use: getCssLoaders(1), // 这个讲得就是importLoaders属性运用,上面已经讲了
},
{
test: /\.scss$/,
use: [
...getCssLoaders(2),
{
loader: 'sass-loader',
options: {
sourceMap: isDevelopment,
},
},
],
},
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
type: 'asset', // webpack5自带的loader,webpack4依赖file-loader
parser: {
dataUrlCondition: {
maxSize: imageInlineSizeLimit,
},
},
},
{
test: /\.(eot|svg|ttf|woff|woff2?)$/,
type: 'asset/resource', // webpack5自带的loader,webpack4依赖file-loader
},
],
},
plugins: [
new HtmlWebpackPlugin({ // 这个模块是重点,下面详细讲
template: paths.appHtml,
cache: true,
}),
new CopyPlugin({ // 这个是复制文件或者目录的插件
patterns: [
{
context: paths.appPublic,
from: '*',
to: paths.appBuild,
toType: 'dir',
globOptions: {
dot: true,
gitignore: true,
ignore: ['**/index.html'],
},
},
],
}),
// 打包进度条插件
new WebpackBar({
name: isDevelopment ? 'RUNNING' : 'BUNDLING',
color: isDevelopment ? '#52c41a' : '#722ed1',
}),
// 插件功能上面已写
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: paths.appTsConfig,
},
}),
],
};
-
HtmlWebpackPlugin
- title
生成html文件的标题
- filename
就是html文件的文件名,默认是index.html
- template
指定你生成的文件所依赖哪一个html文件模板,模板类型可以是html、ejs
如果你设置的
title
和filename
于模板中发生了冲突,那么以你的title
和filename
的配置值为准。-
inject**
inject有四个值:
true
body
head
false
true
默认值,script标签位于html文件的 body 底部body
script标签位于html文件的 body 底部head
script标签位于html文件的 head中false
不插入生成的js文件,这个几乎不会用到的
-
favicon
给你生成的html文件生成一个
favicon
,值是一个路径plugins: [ new HtmlWebpackPlugin({ ... favicon: 'path/to/my_favicon.ico' })
然后再生成的html中就有了一个
link
标签- minify
使用minify会对生成的html文件进行压缩。注意,不能直接这样写:
minify: true
, 使用时候必须给定一个{ }
对象 )plugins: [ new HtmlWebpackPlugin({ ... minify: { removeAttributeQuotes: true // 移除属性的引号 } }) ]
- chunks
chunks主要用于多入口文件,当你有多个入口文件,那就回编译后生成多个打包后的文件,那么
chunks
就能选择你要使用那些js文件entry: { index: path.resolve(__dirname, './src/index.js'), devor: path.resolve(__dirname, './src/devor.js'), main: path.resolve(__dirname, './src/main.js') } plugins: [ new httpWebpackPlugin({ chunks: ['index','main'] }) ]
那么编译后:
<script type=text/javascript src="index.js"></script> <script type=text/javascript src="main.js"></script>
-
如果你没有设置chunks选项,那么默认是全部显示
-
chunksSortMode
script的顺序,默认四个选项: none
auto
dependency
{function}
'dependency' 不用说,按照不同文件的依赖关系来排序。
这里重点讲解一下function
的用法
如何配置出我们想要的顺序
new HtmlWebpackPlugin({
...
chunksSortMode: function (chunk1, chunk2) {
var order = ['common', 'public', 'index'];
var order1 = order.indexOf(chunk1.names[0]);
var order2 = order.indexOf(chunk2.names[0]);
return order1 - order2;
}
})
以上配置的顺序就是['common', 'public', 'index'],为什么呢,因为chunksSortMode这个函数就是数组的sort方法里的自定义函数,这里说白了就是数组[0, 1, 2]按升序排列。
接下来还有webpack.dev.js和webpack.prod.js两个文件(有点写不下去了,这篇文章查了n多资料,搞得现在脑袋有点昏啊)
我就快速写重点内容了,不贴代码了
-
webpack.dev.js里面的重点是devServer属性的配置
-
devServer配置详解:
devServer: {
// 提供静态文件目录地址
// 基于express.static实现, 所以这里你如果不请求静态文件,这个属性没啥用
contentBase: path.join(__dirname, 'dist'),
// 任意的 404 响应都被替代为 index.html
// 基于node connect-history-api-fallback包实现
// 我们知道vue和react有hash路由和history路由
// history路由需要设置这个参数为true,要不你刷新页面会空白屏
historyApiFallback: true,
// 是否一切服务都启用 gzip 压缩
// 基于node compression包实现
compress: true,
// 是否隐藏bundle信息
noInfo: true,
// 发生错误是否覆盖在页面上
overlay: true,
// 是否开启热加载
// 必须搭配webpack.HotModuleReplacementPlugin 才能完全启用 HMR。
// 如果 webpack 或 webpack-dev-server 是通过 --hot 选项启动的,那么这个插件会被自动添加
hot: true,
// 热加载模式
// true代表inline模式,false代表iframe模式
inline: true, // 默认是true
// 是否自动打开
open: true,
// 设置本地url和端口号
host: 'localhost',
port: 8080,
// 代理
// 基于node http-proxy-middleware包实现
proxy: {
// 匹配api前缀时,则代理到3001端口
// 即http://localhost:8080/api/123 = http://localhost:3001/api/123
// 注意:这里是把当前server8080代理到3001,而不是任意端口的api代理到3001
'/api': 'http://localhost:3001',
// 设置为true, 本地就会虚拟一个服务器接收你的请求并代你发送该请求
// 主要解决跨域问题
changeOrigin: true,
// 针对代理https
secure: false,
// 覆写路径:http://localhost:8080/api/123 = http://localhost:3001/123
pathRewrite: {'^/api' : ''}
}
}
- webpack.prod.js的重点是配置TerserPlugin,和optimization配置其中splitChunks是重点中的重点)
// 这个插件最开始讲了,一下的插件就略过都讲过了
const { merge } = require('webpack-merge');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const common = require('./webpack.common.js');
const paths = require('../paths');
const { shouldOpenAnalyzer, ANALYZER_HOST, ANALYZER_PORT } = require('../conf');
module.exports = merge(common, {
mode: 'production', 这个需要细讲,下面说
output: {
filename: 'js/[name].[contenthash:8].js',
path: paths.appBuild,
assetModuleFilename: 'images/[name].[contenthash:8].[ext]',
},
plugins: [
// 打包后会有dist(或者build,名字在output里设置)目录
// 再次打包时需要把之前的dist删掉后,再次生成dist
// 这个插件就是其删掉作用的
new CleanWebpackPlugin(),
// 提取css的插件
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].chunk.css',
}),
// 开启分析工具的插件,分析包的体积
shouldOpenAnalyzer &&
new BundleAnalyzerPlugin({
analyzerMode: 'server',
analyzerHost: ANALYZER_HOST,
analyzerPort: ANALYZER_PORT,
}),
].filter(Boolean),
// 这个重点下面讲
optimization: {
concatenateModules: false,
minimize: true,
minimizer: [
// new TerserPlugin({ // 这个常用配置后面下面讲
// extractComments: false,
// terserOptions: {
// compress: { pure_funcs: ['console.log'] },
// },
// }),
new CssMinimizerPlugin(), // css压缩插件
],
splitChunks: { // 这个是重点下面讲
chunks: 'all',
minSize: 0,
},
},
});
-
TerserPlugin
- test
默认值:
/.m?js(?.*)?$/i
, 用来匹配需要压缩的文件。- include
默认值:
undefined
, 匹配参与压缩的文件- exclude
默认值:
undefined
, 匹配参与压缩的文件parallel
类型:
Boolean|Number
默认值:true
这个参数很重要,启用多进程构建,可以大大提高打包速度,强烈建议开启
terserOptions: { format: { // 删除所有的注释 comments: true, } compress: { // 删除未引用的函数和变量 unused: true, // 删掉 debugger drop_debugger: true, // 移除 console drop_console: true, // 删除无法访问的代码 dead_code: true, unsafe_undefined: true, } }
-
mode
mode有三个参数production
,development
,none
,前两个参数会默认安装一堆插件,用来区分是开发环境还是生产环境。而none
的话,webpack就是最初的样子,无任何预设,需要从无到有开始配置。
所以我们了解是哪些插件,有啥用是理解webpack进化到现在的比较重要的知识点。
development模式下,webpack做了那些打包工作
在此mode下,就做了以下插件的事(还有其它配置,重点介绍下面的),其他都没做,所以这些插件可以省略,webpack默认就给你加上了,而且会将 DefinePlugin
中 process.env.NODE_ENV
的值设置为 development
// webpack.development.config.js
module.exports = {
+ mode: 'development'
- devtool: 'eval',
optimization: {
moduleIds: 'named',
chunkIds: 'named'
}
}
我们看看moduleIds
和chunkIds
这两个配置都做了啥,简而言之,就是帮助缓存生效的插件。
我们知道webpack最开始的版本并不会给模块加上名字,模块名都是数字,0,1,2,3
,但是对于我们人来说数字不好认,要是名字多好,便于开发的时候查找。
而且,你想想,如果我们在0
和1
模块之间,再加一个模块,那么顺序就是0
、新模块(现在是1
)、老的1模块(现在是2
),老的2
模块(现在是3
),这时候新模块就是1
,其它老模块数字依次+1
,这个时候缓存就失效了,虽然老的模块代码没变,但是这种缓存下标的方式,让缓存很容易失效,这就是为啥加上这个配置的原因
有了moduleIds
,模块都拥有了姓名,而且都是独一无二的key,不管新增减多少模块,模块的key都是固定的。
除了moduleIds
,还有一个chunkIds
,这个是给配置的每个chunks命名,原本的chunks也是数组,没有姓名。
production
在正式版本中,所省略的插件们,如下所示,我们会一个个分析。
// webpack.production.config.js
module.exports = {
+ mode: 'production'
- plugins: [
- new webpack.DefinePlugin({ "process.env.NODE_ENV": '"production"' }),
- new webpack.optimize.ModuleConcatenationPlugin(),
- new webpack.NoEmitOnErrorsPlugin(),
- new TerserPlugin(/* ... */),
- ]
}
terser-webpack-plugin
用于js代码压缩。在以前版本中,我们需要引入npm包terser-webpack-plugin
来进行压缩,现在我们可以在optimize
中进行配置达到同样的效果
配置之前已讲
ModuleConcatenationPlugin
这个是用来帮助作用域提升的,我们之前看了webpack打包出来的是类似
{
文件路径1:function()xx,
文件路径2:function()xx,
文件路径3:function()xx,
}
这样每个模块都在自己的function里面,都有自己的作用域,我们知道作用域链访问是有性能代价的,如果大家都提到一个作用域,对性能提升是有帮助的,这个插件就做这样的事。
NoEmitOnErrorsPlugin
这个就是用于防止程序报错,就算有错误也给我继续编译。
others
还有一些默认的插件配置,也就是可以不在plugins中引用的配置:
SideEffectsFlagPlugin
webpack.optimization.sideEffects
用于实现treeshaking
形式的死码删除。而为了实现treeshaking
,需要满足几个条件:
- 导入的模块已经标记了sideEffect,即package.json中的sideEffects这个属性为false。
- 当前模块引用了无副作用的模块,且没有被使用
这样,经过SideEffectsFlagPlugin
处理后,没有副作用且没有被使用的模块都会被打上sideEffectFree
标记。 在ModuleConcatenationPlugin
中,带着sideEffectFree
标记的模块将不会被打包。
// webpack.pord.config.js
module.exports = {
optimization: {
sideEffects: true
}
};
FlagIncludedChunksPlugin
即配置optimization.flagIncludedChunks
。该配置项会使webpack确认,若当前标记的chunk
a是另外一个chunk
A的子集并且已经A加载完成,则a将不会再次加载(包含关系)。
// webpack.pord.config.js
module.exports = {
optimization: {
flagIncludedChunks: true
}
};
FlagDependencyUsagePlugin
标记没有用到的依赖。
splitChunks
最后1个知识点来了哦!
这个配置对象中,其它都好说,最令人困惑的是chunks属性,我们来看看是个什么东西。
chunks
选项,决定要提取那些模块。-
默认是
async
:只提取异步加载的模块出来打包到一个文件中。- 异步加载的模块:通过
import('xxx')
或require(['xxx'],() =>{})
加载的模块。
- 异步加载的模块:通过
-
initial
:提取同步加载和异步加载模块,如果xxx在项目中异步加载了,也同步加载了,那么xxx这个模块会被提取两次,分别打包到不同的文件中。- 同步加载的模块:通过
import xxx
或require('xxx')
加载的模块。
- 同步加载的模块:通过
-
all
:不管异步加载还是同步加载的模块都提取出来,打包到一个文件中。
-
兄弟们,但是我遇到了问题,就是上面说的这些根本不管用,下面的案例摘自stockOverFolw
的高票回答,但是我用webpack5
同样的配置,根本得不到跟这个回答一致的答案,百思不得其解,后面我改进了一下,就可以了,后面再介绍,大家先看案例
app.js 如下,有一个静态模块导入叫my-static-module
,还有一个动态模块导入叫my-dynamic-module
//app.js
import "my-static-module";
if(some_condition_is_true){
import ("my-dynamic-module")
}
console.log("My app is running")
``
来看看chunks参数不一样,得到的结果会是多么不一样(配置如下)
module.exports = {
optimization: {
chunks: async | initial | all
}
};
- async (default)
会生成以下两个文件
bundle.js
(包括 app.js + my-static-module)chunk.js
(仅仅包括 my-dynamic-module)
- initial
会生成以下两个文件
app.js
(仅仅包括 app.js)bundle.js
(仅仅包括 my-static-module)chunk.js
(仅仅包括 my-dynamic-module)
- all
会生成以下两个文件
app.js
(仅仅包括 app.js only)bundle.js
(仅仅包括 my-static-module + my-dynamic-module)
可以看出,all
是比较极限的压缩
我无论怎么尝试,得出来的结果都是默认的async
导出的结果,可能是我配错了吧,希望有熟悉这项配置的大哥评论区留个言。
我后来是怎么改,就可以符合上面的答案了呢,我把chunks配置在cacheGroups
参数里,如下:
module.exports = {
splitChunks: {
cacheGroups: {
common: {
chunks: 'async' | 'all' | 'initial',
minSize: 0,
minChunks: 1,
},
},
},
};
这里顺便介绍一下minChunks是什么意思,意思是至少引用多少次才分离公共代码,我这里是1次,只要引用过模块都分离出去。
minSize
是规定被提取的模块在压缩前的大小最小值,单位为字节,默认为30000,只有超过了30000字节才会被提取,我们这里设置为0,是为了自己做实验,保证能被分离就分离出去。
接下来,介绍一下其他参数:
maxSize
选项:把提取出来的模块打包生成的文件大小不能超过maxSize值,如果超过了,要对其进行分割并打包生成新的文件。单位为字节,默认为0,表示不限制大小。maxAsyncRequests
选项:最大的按需(异步)加载次数,默认为 6。maxInitialRequests
选项:打包后的入口文件加载时,还能同时加载js文件的数量(包括入口文件),默认为4。- 先说一下优先级
maxInitialRequests
/maxAsyncRequests
<maxSize
<minSize
。 automaticNameDelimiter
选项:打包生成的js文件名的分割符,默认为~
。name
选项:打包生成js文件的名称。cacheGroups
选项,核心重点,配置提取模块的方案。里面每一项代表一个提取模块的方案。下面是cacheGroups
每项中特有的选项,其余选项和外面一致,若cacheGroups
每项中有,就按配置的,没有就使用外面配置的。-
test
选项:用来匹配要提取的模块的资源路径或名称。值是正则或函数。priority
选项:方案的优先级,值越大表示提取模块时优先采用此方案。默认值为0。reuseExistingChunk
选项:true
/false
。为true
时,如果当前要提取的模块,在已经在打包生成的js文件中存在,则将重用该模块,而不是把当前要提取的模块打包生成新的js文件。enforce
选项:true
/false
。为true
时,忽略minSize
,minChunks
,maxAsyncRequests
和maxInitialRequests
外面选项
能看到最后一定很不容易,欢迎点赞,后面会接着出文章,目前3篇正在写,也是自己最近学习完的知识
- form表单低代码平台之渲染器实现(渲染器就是schema => 表单)
- jest单元测试教程
- leetcode官方面试最常见150题之简单题
参考:
mini-css-extract-plugin插件快速入门
在Typescript项目中,如何优雅的使用ESLint和Prettier
实用husky介绍
我是这样搭建typescript+react
webpack官网
webpack import和export
tsconfig常用配置
前端工程化 - 剖析npm的包管理机制
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!