Webpack从入门到进阶(一)—附沿路学习案例代码
无论是作为专业的开发者还是接触互联网的普通人,其实都能深刻的感知到Web前端的发展是非常快速的
对于开发者来说我们会更加深有体会;
从后端渲染的JSP、PHP,到前端原生JavaScript,再到jQuery开发,再到目前的三大框架Vue、React、Angular;
开发方式也从原来的JavaScript的ES5语法,到ES6、7、8、9、10,到TypeScript,包括编写CSS的预处理器less、scss等;
前端开发的复杂化
前端开发目前我们面临哪些复杂的问题呢?
目前前端流行的三大框架:Vue、React、Angular
webpack是一个静态的模块化打包工具,为现代的JavaScript应用程序;
webpack is a static module bundler for modern JavaScript applications.
我们来对上面的解释进行拆解:
打包bundler:webpack可以将帮助我们进行打包,所以它是一个打包工具
静态的static:这样表述的原因是我们最终可以将代码打包成最终的静态资源(部署到静态服务器);
模块化module:webpack默认支持各种模块化开发,ES Module、CommonJS、AMD等;
现代的modern:我们前端说过,正是因为现代前端开发面临各种各样的问题,才催生了webpack的出现和发展;
Webpack 是一个前端资源加载和打包工具。所谓的模块就是在平时的前端开发中,用到一些静态资源,如JavaScript、CSS、图片等文件,webpack就将这些静态资源文件称之为模块。 webpack支持AMD和CommonJS,以及其他的一些模块系统,并且兼容多种JS书写规范,可以处理模块间的依赖关系,所以具有更强大的JS模块化的功能,它能对静态资源进行统一的管理以及打包发布。
作为一款 Grunt和Gulp的替代产品,Webpack受到大多数开发者的喜爱,因为它能够编译打包CSS,做CSS预处理,对JS的方言进行编译,打包图片,代码压缩等等。
与其他的构建工具相比,Webpack具有如下的一些优势:
在Webpack的世界里有两个最核心的概念:
一切皆模块
正如js文件可以是一个“模块(module)”一样,其他的(如css、image或html)文件也可视作模 块。因此,你可以require(‘myJSfile.js’)亦可以require(‘myCSSfile.css’)。这意味着我们可以将事物(业务)分割成更小的易于管理的片段,从而达到重复利用等的目的。
按需加载
传统的模块打包工具(module bundlers)最终将所有的模块编译生成一个庞大的bundle.js文件。但是在真实的app里边,“bundle.js”文件可能有10M到15M之大可能会导致应用一直处于加载中状态。因此Webpack使用许多特性来分割代码然后生成多个“bundle”文件,而且异步加载部分代码以实现按需加载。
Webpack官网图片
工作中的webpack
我们来提一个问题:webpack会被vite取代吗?
但是目前vite取代webpack还有很长的路要走
vite的核心思想并不是首创
webpack的更新迭代
关于vite的思考
我的个人观点:
学习任何的东西,重要的是学习核心思想:
任何工具的出现,都是更好的服务于我们开发:
无论是vite的出现,还是以后新的工具的出现,不要有任何排斥的思想;
我们要深刻的明白,工具都是为了更好的给我们提供服务;
不可能出现了某个工具,让我们的开发效率变得更低,而这个工具却可以变得非常流行,这是不存在的;
webpack的官方文档是https://webpack.js.org/
点击DOCUMENTATION来到文档页:
Webpack的运行是依赖Node环境的,所以我们电脑上必须有Node环境
Webpack的安装
webpack的安装目前分为两个:webpack、webpack-cli
那么它们是什么关系呢?
npm install webpack webpack-cli –g # 全局安装
npm install webpack webpack-cli –D # 局部安装
传统开发存在的问题
我们的代码存在什么问题呢?某些语法浏览器是不认识的(尤其在低版本浏览器上)
显然,上面存在的问题,让我们在发布静态资源时,是不能直接发布的,因为运行在用户浏览器必然会存在各种各样的兼容性问题。
我们可以通过webpack进行打包,之后运行打包之后的代码
webpack
生成一个dist文件夹,里面存放一个main.js的文件,就是我们打包之后的文件:
我们发现是可以正常进行打包的,但是有一个问题,webpack是如何确定我们的入口的呢?
在通常情况下,webpack需要打包的项目是非常复杂的,并且我们需要一系列的配置来满足要求,默认配置必然是不可以的
我们可以在根目录下创建一个webpack.config.js文件,来作为webpack的配置文件:
const path = require('path');
module.exports = {
entry: "./src/main.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "./dist")
}
}
继续执行webpack命令,依然可以正常打包
指定配置文件
但是如果我们的配置文件并不是webpack.config.js的名字,而是其他的名字呢?
比如我们将webpack.config.js修改成了 wk.config.js;
这个时候我们可以通过 --config 来指定对应的配置文件;
webpack --config wk.config.js
但是每次这样执行命令来对源码进行编译,会非常繁琐,所以我们可以在package.json中增加一个新的脚本:
{
"scripts": {
"build": webpack --config wk.config.js
}
}
之后我们执行 npm run build来打包即可。
webpack到底是如何对我们的项目进行打包的呢?
事实上webpack在处理应用程序时,它会根据命令或者配置文件找到入口文件;
从入口开始,会生成一个 依赖关系图,这个依赖关系图会包含应用程序中所需的所有模块(比如.js文件、css文件、图片、字体等);
然后遍历图结构,打包一个个模块(根据文件的不同使用不同的loader来解析);
为什么需要babel?
事实上,在开发中我们很少直接去接触babel,但是babel对于前端开发来说,目前是不可缺少的一部分:
开发中,我们想要使用ES6+的语法,想要使用TypeScript,开发React项目,它们都是离不开Babel的;
所以,学习Babel对于我们理解代码从编写到线上的转变过程直观重要;
了解真相,你才能获得真知的自由!
那么,Babel到底是什么呢?
Babel是一个工具链,主要用于旧浏览器或者缓解中将ECMAScript 2015+代码转换为向后兼容版本的JavaScript;
包括:语法转换、源代码转换、Polyfill实现目标缓解缺少的功能等;
[1, 2, 3].map((n) => n + 1)
[1, 2, 3].map(function(n) {
return n + 1
})
Babel命令行使用
babel本身可以作为一个独立的工具(和postcss一样),不和webpack等构建工具配置来单独使用
如果我们希望在命令行尝试使用babel,需要安装如下库:
@babel/core:babel的核心代码,必须安装;
@babel/cli:可以让我们在命令行使用babel;
npm install @babel/cli @babel/core
使用babel来处理我们的源代码:
src:是源文件的目录;
–out-dir:指定要输出的文件夹dist;
npx babel src --out-dir dist
插件的使用
比如我们需要转换箭头函数,那么我们就可以使用箭头函数转换相关的插件:
npm install @babel/plugin-transform-arrow-functions -D
npx babel src --out-dir dist --plugins=@babel/plugin-transform-arrow-functions
查看转换后的结果:我们会发现 const 并没有转成 var
这是因为 plugin-transform-arrow-functions,并没有提供这样的功能;
我们需要使用 plugin-transform-block-scoping 来完成这样的功能;
npm install @babel/plugin-transform-block-scoping -D
npx babel src --out-dir dist --plugins=@babel/plugin-transform-block-scoping,@babel/plugin-transform-arrow-functions
Babel的预设preset
但是如果要转换的内容过多,一个个设置是比较麻烦的,我们可以使用预设(preset):
安装@babel/preset-env预设:
npm install @babel/preset-env -D
执行如下命令:
npx babel src --out-dir dist --presets=@babel/preset-env
Babel的原理
babel是如何做到将我们的一段代码(ES6、TypeScript、React)转成另外一段代码(ES5)的呢?
从一种源代码(原生语言)转换成另一种源代码(目标语言),这是什么的工作呢?
就是编译器,事实上我们可以将babel看成就是一个编译器。
Babel编译器的作用就是将我们的源代码,转换成浏览器可以直接识别的另外一段源代码;
Babel也拥有编译器的工作流程:
解析阶段(Parsing)
转换阶段(Transformation)
生成阶段(Code Generation)
https://github.com/jamiebuilds/the-super-tiny-compiler
babel编译器执行原理
Babel的执行阶段
当然,这只是一个简化版的编译器工具流程,在每个阶段又会有自己具体的工作:
中间产生的代码
const name = "coderwhy";
const foo = (name) => console.log(name);
foo(name);
babel-loader
在实际开发中,我们通常会在构建工具中通过配置babel来对其进行使用的,比如在webpack中。
那么我们就需要去安装相关的依赖:
npm install babel-loader @babel/core
我们可以设置一个规则,在加载js文件时,使用我们的babel:
module: {
rules: [
{
test: /\.js$/,
use: {
loader: "babel-loader",
}
}
]
},
指定使用的插件
我们必须指定使用的插件才会生效
module: {
rules: [
{
test: /\.js?$/,
use: {
loader: "babel-loader",
options: {
plugins: [
"@babel/plugin-transform-arrow-functions",
"@babel/plugin-transform-block-scoping"
]
}
}
}
]
},
babel-preset
如果我们一个个去安装使用插件,那么需要手动来管理大量的babel插件,我们可以直接给webpack提供一个preset,webpack会根据我们的预设来加载对应的插件列表,并且将其传递给babel。
比如常见的预设有三个:
env
react
TypeScript
安装preset-env:
npm install @babel/preset-env
module: {
rules: [
{
test: /\.js$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"]
}
}
}
]
},
设置目标浏览器 browserslist
我们最终打包的JavaScript代码,是需要跑在目标浏览器上的,那么如何告知babel我们的目标浏览器呢?
browserslist工具
target属性
之前我们项目中已经使用了browserslist工具,我们可以对比一下不同的配置,打包的区别:
设置目标浏览器 targets
我们也可以通过targets来进行配置:
module: {
rules: [
{
test: /\.js$/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", {
targets: "last 2 version"
}]
}
}
}
]
},
那么,如果两个同时配置了,哪一个会生效呢?
配置的targets属性会覆盖browserslist;
但是在开发中,更推荐通过browserslist来配置,因为类似于postcss工具,也会使用browserslist,进行统一浏览器的适配;
Stage-X的preset
要了解Stage-X,我们需要先了解一下TC39的组织:
TC39是指技术委员会(Technical Committee)第 39 号;
它是 ECMA 的一部分,ECMA 是 “ECMAScript” 规范下的 JavaScript 语言标准化的机构;
ECMAScript 规范定义了 JavaScript 如何一步一步的进化、发展;
TC39 遵循的原则是:分阶段加入不同的语言特性,新流程涉及四个不同的 Stage
**Stage 0:**strawman(稻草人),任何尚未提交作为正式提案的讨论、想法变更或者补充都被认为是第 0 阶段的"稻草人";
**Stage 1:**proposal(提议),提案已经被正式化,并期望解决此问题,还需要观察与其他提案的相互影响;
**Stage 2:**draft(草稿),Stage 2 的提案应提供规范初稿、草稿。此时,语言的实现者开始观察 runtime 的具体实现是否合理;
**Stage 3:**candidate(候补),Stage 3 提案是建议的候选提案。在这个高级阶段,规范的编辑人员和评审人员必须在最终规范上签字。Stage 3 的提案不会有太大的改变,在对外发布之前只是修正一些问题;
**Stage 4:**finished(完成),进入 Stage 4 的提案将包含在 ECMAScript 的下一个修订版中;
Babel的Stage-X设置
在babel7之前(比如babel6中),我们会经常看到这种设置方式:
它表达的含义是使用对应的 babel-preset-stage-x 预设;
但是从babel7开始,已经不建议使用了,建议使用preset-env来设置;
module.exports = {
"presets": ["stage-0"]
}
Babel的配置文件
像之前一样,我们可以将babel的配置信息放到一个独立的文件中,babel给我们提供了两种配置文件的编写:
babel.config.json(或者.js,.cjs,.mjs)文件;
.babelrc.json(或者.babelrc,.js,.cjs,.mjs)文件;
它们两个有什么区别呢?目前很多的项目都采用了多包管理的方式(babel本身、element-plus、umi等);
.babelrc.json:早期使用较多的配置方式,但是对于配置Monorepos项目是比较麻烦的;
babel.config.json(babel7):可以直接作用于Monorepos项目的子包,更加推荐;
module.exports = {
"presets": [
["@babel/preset-env", {
targets: "last 2 version"
}]
]
}
认识polyfill
Polyfill是什么呢?
翻译:一种用于衣物、床具等的聚酯填充材料, 使这些物品更加温暖舒适;
理解:更像是应该填充物(垫片),一个补丁,可以帮助我们更好的使用JavaScript;
为什么时候会用到polyfill呢?
比如我们使用了一些语法特性(例如:Promise, Generator, Symbol等以及实例方法例如Array.prototype.includes等)
但是某些浏览器压根不认识这些特性,必然会报错;
我们可以使用polyfill来填充或者说打一个补丁,那么就会包含该特性了;
如何使用polyfill?
babel7.4.0之前,可以使用 @babel/polyfill的包,但是该包现在已经不推荐使用了:
babel7.4.0之后,可以通过单独引入core-js和regenerator-runtime来完成polyfill的使用:
npm install core-js regenerator-runtime --save
{
test: /\.m?js$/,
exclude: /node_modules/,
use: "babel-loader"
}
配置babel.config.js
我们需要在babel.config.js文件中进行配置,给preset-env配置一些属性:
useBuiltIns:设置以什么样的方式来使用polyfill;
corejs:设置corejs的版本,目前使用较多的是3.x的版本,比如我使用的是3.8.x的版本;
另外corejs可以设置是否对提议阶段的特性进行支持;
设置 proposals属性为true即可;
useBuiltIns属性设置
useBuiltIns属性有三个常见的值
第一个值:false
打包后的文件不使用polyfill来进行适配;
并且这个时候是不需要设置corejs属性的;
第二个值:usage
会根据源代码中出现的语言特性,自动检测所需要的polyfill;
这样可以确保最终包里的polyfill数量的最小化,打包的包相对会小一些;
可以设置corejs属性来确定使用的corejs的版本;
presets: [
["@babel/preset-env", {
useBuiltIns: "usage",
corejs: 3.8
}]
]
第三个值:entry
如果我们依赖的某一个库本身使用了某些polyfill的特性,但是因为我们使用的是usage,所以之后用户浏览器可能会报错;
所以,如果你担心出现这种情况,可以使用 entry;
并且需要在入口文件中添加 `import ‘core-js/stable’; import ‘regenerator-runtime/runtime’;
这样做会根据 browserslist 目标导入所有的polyfill,但是对应的包也会变大;
presets: [
["@babel/preset-env", {
useBuiltIns: "entry",
corejs: 3.8
}]
]
import "core-js/stable";
import "regenerator-runtime/runtime";
认识Plugin-transform-runtime(了解)
在前面我们使用的polyfill,默认情况是添加的所有特性都是全局的
如果我们正在编写一个工具库,这个工具库需要使用polyfill;
别人在使用我们工具时,工具库通过polyfill添加的特性,可能会污染它们的代码;
所以,当编写工具时,babel更推荐我们使用一个插件: @babel/plugin-transform-runtime来完成polyfill的功能;
使用Plugin-transform-runtime
安装 @babel/plugin-transform-runtime:
npm install @babel/plugin-transform-runtime -D
使用plugins来配置babel.config.js:
module.exports = {
presets: [
["@babel/preset-env", {
// false: 不用任何的polyfill相关的代码
// usage: 代码中需要哪些polyfill, 就引用相关的api
// entry: 手动在入口文件中导入 core-js/regenerator-runtime, 根据目标浏览器引入所有对应的polyfill
// useBuiltIns: "entry",
// corejs: 3.8
}],
["@babel/preset-react"]
],
plugins: [
["@babel/plugin-transform-runtime", {
corejs: 3
}]
]
}
注意:因为我们使用了corejs3,所以我们需要安装对应的库:
React的jsx支持
在我们编写react代码时,react使用的语法是jsx,jsx是可以直接使用babel来转换的。
对react jsx代码进行处理需要如下的插件:
@babel/plugin-syntax-jsx
@babel/plugin-transform-react-jsx
@babel/plugin-transform-react-display-name
但是开发中,我们并不需要一个个去安装这些插件,我们依然可以使用preset来配置:
npm install @babel/preset-react -D
presets: [
["@babel/preset-env", {
}],
["@babel/preset-react"],
]
TypeScript的编译
在项目开发中,我们会使用TypeScript来开发,那么TypeScript代码是需要转换成JavaScript代码。
可以通过TypeScript的compiler来转换成JavaScript:
npm install typescript -D
另外TypeScript的编译配置信息我们通常会编写一个tsconfig.json文件:
tsc --init
生成配置文件如下:
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"lib": ["ES2015", "DOM"], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
}
}
之后我们可以运行 npx tsc来编译自己的ts代码:
npx tsc
使用ts-loader
如果我们希望在webpack中使用TypeScript,那么我们可以使用ts-loader来处理ts文件:
npm install ts-loader -D
配置ts-loader:
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
"ts-loader"
]
}
之后,我们通过npm run build打包即可
使用babel-loader
除了可以使用TypeScript Compiler来编译TypeScript之外,我们也可以使用Babel:
Babel是有对TypeScript进行支持;
我们可以使用插件: @babel/tranform-typescript;
但是更推荐直接使用preset:@babel/preset-typescript;
我们来安装@babel/preset-typescript:
npm install @babel/preset-typescript -D
presets: [
["@babel/preset-env", {
// false: 不用任何的polyfill相关的代码
// usage: 代码中需要哪些polyfill, 就引用相关的api
// entry: 手动在入口文件中导入 core-js/regenerator-runtime, 根据目标浏览器引入所有对应的polyfill
useBuiltIns: "usage",
corejs: 3
}],
["@babel/preset-react"],
["@babel/preset-typescript"]
],
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
"ts-loader"
]
}
ts-loader和babel-loader选择
那么我们在开发中应该选择ts-loader还是babel-loader呢?
使用ts-loader(TypeScript Compiler)
来直接编译TypeScript,那么只能将ts转换成js;
如果我们还希望在这个过程中添加对应的polyfill,那么ts-loader是无能为力的;
我们需要借助于babel来完成polyfill的填充功能;
使用babel-loader(Babel)
来直接编译TypeScript,也可以将ts转换成js,并且可以实现polyfill的功能;
但是babel-loader在编译的过程中,不会对类型错误进行检测;
那么在开发中,我们如何可以同时保证两个情况都没有问题呢?
编译TypeScript最佳实践
事实上TypeScript官方文档有对其进行说明:
也就是说我们使用Babel来完成代码的转换,使用tsc来进行类型的检查。
但是,如何可以使用tsc来进行类型的检查呢?
在这里,我在scripts中添加了两个脚本,用于类型检查;
我们执行 npm run type-check可以对ts代码的类型进行检测;
我们执行 npm run type-check-watch可以实时的检测类型错误;
"scripts": {
"build": "webpack --config wk.config.js",
"type-check": "tsc --noEmit",
"type-check-watch": "tsc --noEmit --watch"
}
认识ESLint
什么是ESLint呢?
ESLint是一个静态代码分析工具(Static program analysis,在没有任何程序执行的情况下,对代码进行分析);
ESLint可以帮助我们在项目中建立统一的团队代码规范,保持正确、统一的代码风格,提高代码的可读性、可维护性;
并且ESLint的规则是可配置的,我们可以自定义属于自己的规则;
早期还有一些其他的工具,比如JSLint、JSHint、JSCS等,目前使用最多的是ESLint。
使用ESLint
首先我们需要安装ESLint:
npm install eslint -D
创建ESLint的配置文件:
npx eslint --init
选择想要使用的ESLint:
执行检测命令:
npx eslint ./src/main.js
ESLint的文件解析
默认创建的环境如下:
env:运行的环境,比如是浏览器,并且我们会使用es2021(对应的ecmaVersion是12)的语法;
extends:可以扩展当前的配置,让其继承自其他的配置信息,可以跟字符串或者数组(多个);
parserOptions:这里可以指定ESMAScript的版本、sourceType的类型
plugins:指定我们用到的插件;
rules:自定义的一些规则;
VSCode的ESLint插件
但是如果每次校验时,都需要执行一次npm run eslint就有点麻烦了,所以我们可以使用一个VSCode的插件:ESLint
一方面我们可以修改代码来修复错误,另外我们可以通过配置规则:
VSCode的Prettier插件
ESLint会帮助我们提示错误(或者警告),但是不会帮助我们自动修复:
在开发中我们希望文件在保存时,可以自动修复这些问题;
我们可以选择使用另外一个工具:prettier;
ESLint-Loader的使用
事实上,我们在编译代码的时候,也希望进行代码的eslint检测,这个时候我们就可以使用eslint-loader来完成了:
npm install eslint-loader -D
{
test: /\.m?js$/,
exclude: /node_modules/,
use: [
'babel-loader',
'eslint-loader'
]
}
加载Vue文件
import Vue from 'vue';
import App from "./App.vue";
new Vue({
render: h => h(App)
}).$mount("#app");
<template>
<div id="app">
<h2 class="title">{{message}}</h2>
</div>
</template>
<script>
export default {
data() {
return {
message: "Hello Vue"
}
}
}
</script>
<style scoped lang="less">
.title {
color: red
}
</style>
Webpack中配置vue加载
安装相关的依赖:
npm install vue-loader -D
npm install vue-template-compiler -D
配置webpack
{
test: /\.vue$/,
use: "vue-loader"
}
const VueLoaderPlugin = require('vue-loader/lib/plugin');
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: "coderwhy webpack",
template: "./index.html"
}),
new VueLoaderPlugin()
]
{
test: /\.less$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
importLoaders: 2
}
},
"postcss-loader",
"less-loader"
]
},
为什么要搭建本地服务器?
目前我们开发的代码,为了运行需要有两个操作:
操作一:npm run build,编译相关的代码;
操作二:通过live server或者直接通过浏览器,打开index.html代码,查看效果;
这个过程经常操作会影响我们的开发效率,我们希望可以做到,当文件发生变化时,可以自动的完成 编译 和 展示;
为了完成自动编译,webpack提供了几种可选的方式:
webpack watch mode;
webpack-dev-server;
webpack-dev-middleware
接下来,我们一个个来学习一下它们;
Webpack watch
webpack给我们提供了watch模式:
在该模式下,webpack依赖图中的所有文件,只要有一个发生了更新,那么代码将被重新编译;
我们不需要手动去运行 npm run build指令了;
如何开启watch呢?两种方式:
方式一:在导出的配置中,添加 watch: true;
方式二:在启动webpack的命令中,添加 --watch的标识;
这里我们选择方式二,在package.json的 scripts 中添加一个 watch 的脚本:
"scripts": {
"build": "webpack --config wk.config.js",
"watch": "webpack --watch",
"type-check": "tsc --noEmit",
"type-check-watch": "tsc --noEmit --watch"
}
webpack-dev-server
上面的方式可以监听到文件的变化,但是事实上它本身是没有自动刷新浏览器的功能的:
当然,目前我们可以在VSCode中使用live-server来完成这样的功能;
但是,我们希望在不适用live-server的情况下,可以具备live reloading(实时重新加载)的功能;
安装webpack-dev-server
npm install --save-dev webpack-dev-server
修改配置文件,告知 dev server,从什么位置查找文件:
devServer: {
contentBase: './build'
}
webpack-dev-server 在编译之后不会写入到任何输出文件。而是将 bundle 文件保留在内存中:
webpack-dev-middleware
webpack-dev-middleware 是一个封装器(wrapper),它可以把 webpack 处理过的文件发送到一个 server。
webpack-dev-server 在内部使用了它,然而它也可以作为一个单独的 package 来使用,以便根据需求进行 更多自定义设置;
我们可以搭配一个服务器来使用它,比如express;
npm install --save-dev express webpack-dev-middleware
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require("./webpack.config");
// 传入配置信息, webpack根据配置信息进行编译
const compiler = webpack(config);
const middleware = webpackDevMiddleware(compiler);
app.use(middleware);
app.listen(3000, () => {
console.log("服务已经开启在3000端口上~");
});
node server.js 运行代码
publicPath
Output中有两个很重要的属性:path和publicPath
path:用于指定文件的输出路径(比如打包的html、css、js等),是一个绝对路径;
publicPath:默认是一个空字符串,它为我们项目中的资源指定一个公共的路径(publicPath);
这个publicPath很不容易理解,其实就是给我们打包的资源,给它一个路径:
资源的路径 = output.publicPath + 打包资源的路径(比如"js/[name].bundle.js")
比较常设置的是两个值:
./:本地环境下可以使用这个相对路径;
/:服务器部署时使用,服务器地址 + /js/[name].bundle.js;
为什么要搭建本地服务器?
目前我们开发的代码,为了运行需要有两个操作:
操作一:npm run build,编译相关的代码;
操作二:通过live server或者直接通过浏览器,打开index.html代码,查看效果;
这个过程经常操作会影响我们的开发效率,我们希望可以做到,当文件发生变化时,可以自动的完成 编译 和 展示;
为了完成自动编译,webpack提供了几种可选的方式:
webpack watch mode;
webpack-dev-server;
webpack-dev-middleware
接下来,我们一个个来学习一下它们;
Webpack watch
webpack给我们提供了watch模式:
在该模式下,webpack依赖图中的所有文件,只要有一个发生了更新,那么代码将被重新编译;
我们不需要手动去运行 npm run build指令了;
如何开启watch呢?两种方式:
方式一:在导出的配置中,添加 watch: true;
方式二:在启动webpack的命令中,添加 --watch的标识;
这里我们选择方式二,在package.json的 scripts 中添加一个 watch 的脚本:
"scripts": {
"build": "webpack --config wk.config.js",
"watch": "webpack --watch",
"type-check": "tsc --noEmit",
"type-check-watch": "tsc --noEmit --watch"
}
webpack-dev-server
上面的方式可以监听到文件的变化,但是事实上它本身是没有自动刷新浏览器的功能的:
当然,目前我们可以在VSCode中使用live-server来完成这样的功能;
但是,我们希望在不适用live-server的情况下,可以具备live reloading(实时重新加载)的功能;
安装webpack-dev-server
npm install --save-dev webpack-dev-server
添加一个新的scripts脚本
"serve": "webpack serve --config wk.config.js"
webpack-dev-server 在编译之后不会写入到任何输出文件。而是将 bundle 文件保留在内存中:
webpack-dev-middleware
默认情况下,webpack-dev-server已经帮助我们做好了一切:
比如通过express启动一个服务,比如HMR(热模块替换);
如果我们想要有更好的自由度,可以使用webpack-dev-middleware;
什么是webpack-dev-middleware?
webpack-dev-middleware 是一个封装器(wrapper),它可以把 webpack 处理过的文件发送到一个 server;
webpack-dev-server 在内部使用了它,然而它也可以作为一个单独的 package 来使用,以便根据需求进行 更多自定义设置;
webpack-dev-middleware的使用
安装express和webpack-dev-middleware:
npm install --save-dev express webpack-dev-middleware
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require("./webpack.config");
// 传入配置信息, webpack根据配置信息进行编译
const compiler = webpack(config);
const middleware = webpackDevMiddleware(compiler);
app.use(middleware);
app.listen(3000, () => {
console.log("服务已经开启在3000端口上~");
});
node server.js 运行代码
也可以使用koa来搭建服务
认识模块热替换(HMR)
什么是HMR呢?
HMR的全称是Hot Module Replacement,翻译为模块热替换;
模块热替换是指在 应用程序运行过程中,替换、添加、删除模块,而无需重新刷新整个页面;
HMR通过如下几种方式,来提高开发的速度:
不重新加载整个页面,这样可以保留某些应用程序的状态不丢失;
只更新需要变化的内容,节省开发的时间;
修改了css、js源代码,会立即在浏览器更新,相当于直接在浏览器的devtools中直接修改样式;
如何使用HMR呢?
默认情况下,webpack-dev-server已经支持HMR,我们只需要开启即可;
在不开启HMR的情况下,当我们修改了源代码之后,整个页面会自动刷新,使用的是live reloading;
开启HMR
修改webpack的配置:
devServer: {
hot: true
}
浏览器可以看到如下效果:
但是你会发现,当我们修改了某一个模块的代码时,依然是刷新的整个页面:
这是因为我们需要去指定哪些模块发生更新时,进行HMR;
if (module.hot) {
module.hot.accept("./util.js", () => {
console.log("util更新了")
})
}
框架的HMR
有一个问题:在开发其他项目时,我们是否需要经常手动去写入 module.hot.accpet相关的API呢?
比如开发Vue、React项目,我们修改了组件,希望进行热更新,这个时候应该如何去操作呢?
事实上社区已经针对这些有很成熟的解决方案了:
比如vue开发中,我们使用vue-loader,此loader支持vue组件的HMR,提供开箱即用的体验;
比如react开发中,有React Hot Loader,实时调整react组件(目前React官方已经弃用了,改成使用react-refresh);
接下来我们分别对React、Vue实现一下HMR功能。
React的HMR
在之前,React是借助于React Hot Loader来实现的HMR,目前已经改成使用react-refresh来实现了。
安装实现HMR相关的依赖:
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh
修改webpack.config.js和babel.config.js文件:
const ReactRefreshWebpackPlugin = require('pmmmwh/react-refresh-webpack-plugin')
plugins: [
......
new ReactRefreshWebpackPlugin()
]
module.exports = {
....
plugins: [
['react-refresh/babel']
]
}
Vue的HMR
Vue的加载我们需要使用vue-loader,而vue-loader加载的组件默认会帮助我们进行HMR的处理。
安装加载vue所需要的依赖:
npm install vue-loader vue-template-compiler -D
配置webpack.config.js:
const VueLoaderPlugin = require('vue-loader/lib/plugin');
{
test: /\.vue/,
use: 'vue-loader'
}
plugins: [
....
new VueLoaderPlugin()
]
HMR的原理
那么HMR的原理是什么呢?如何可以做到只更新一个模块中的内容呢?
webpack-dev-server会创建两个服务:提供静态资源的服务(express)和Socket服务(net.Socket);
express server负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析);
HMR Socket Server,是一个socket的长连接:
长连接有一个最好的好处是建立连接后双方可以通信(服务器可以直接发送文件到客户端);
当服务器监听到对应的模块发生变化时,会生成两个文件.json(manifest文件)和.js文件(update chunk);
通过长连接,可以直接将这两个文件主动发送给客户端(浏览器);
浏览器拿到两个新的文件后,通过HMR runtime机制,加载这两个文件,并且针对修改的模块进行更新;
HMR的原理图
output的publicPath
output中的path的作用是告知webpack之后的输出目录:
output中还有一个publicPath属性,该属性是指定index.html文件打包引用的一个基本路径:
它的默认值是一个空字符串,所以我们打包后引入js文件时,路径是 bundle.js;
在开发中,我们也将其设置为 / ,路径是 /bundle.js,那么浏览器会根据所在的域名+路径去请求对应的资源;
如果我们希望在本地直接打开html文件来运行,会将其设置为 ./,路径时 ./bundle.js,可以根据相对路径去查找资源;
devServer的publicPath
devServer中也有一个publicPath的属性,该属性是指定本地服务所在的文件夹:
它的默认值是 /,也就是我们直接访问端口即可访问其中的资源 http://localhost:8080;
如果我们将其设置为了 /abc,那么我们需要通过 http://localhost:8080/abc才能访问到对应的打包后的资源;
并且这个时候,我们其中的bundle.js通过 http://localhost:8080/bundle.js也是无法访问的:
devServer的contentBase
devServer中contentBase对于我们直接访问打包后的资源其实并没有太大的作用,它的主要作用是如果我们打包后的资源,又依赖于其他的一些资源,那么就需要指定从哪里来查找这个内容:
比如在index.html中,我们需要依赖一个 abc.js 文件,这个文件我们存放在 public文件 中;
在index.html中,我们应该如何去引入这个文件呢?
;
;当然在devServer中还有一个可以监听contentBase发生变化后重新编译的一个属性:watchContentBase。
hotOnly、host配置
hotOnly是当代码编译失败时,是否刷新整个页面:
默认情况下当代码编译失败修复后,我们会重新刷新整个页面;
如果不希望重新刷新整个页面,可以设置hotOnly为true;
host设置主机地址:
默认值是localhost;
如果希望其他地方也可以访问,可以设置为 0.0.0.0;
localhost 和 0.0.0.0 的区别:
localhost:本质上是一个域名,通常情况下会被解析成127.0.0.1;
127.0.0.1:回环地址(Loop Back Address),表达的意思其实是我们主机自己发出去的包,直接被自己接收;
0.0.0.0:监听IPV4上所有的地址,再根据端口找到不同的应用程序;
port、open、compress
port设置监听的端口,默认情况下是8080
open是否打开浏览器:
默认值是false,设置为true会打开浏览器;
也可以设置为类似于 Google Chrome等值;
compress是否为静态文件开启gzip compression:
Proxy代理
proxy是我们开发中非常常用的一个配置选项,它的目的设置代理来解决跨域访问的问题:
比如我们的一个api请求是 http://localhost:8888,但是本地启动服务器的域名是 http://localhost:8000,这个时候发送网络请求就会出现跨域的问题;
那么我们可以将请求先发送到一个代理服务器,代理服务器和API服务器没有跨域的问题,就可以解决我们的跨域问题了;
我们可以进行如下的设置:
target:表示的是代理到的目标地址,比如 /api-hy/moment会被代理到 http://localhost:8888/api-hy/moment;
pathRewrite:默认情况下,我们的 /api-hy 也会被写入到URL中,如果希望删除,可以使用pathRewrite;
secure:默认情况下不接收转发到https的服务器上,如果希望支持,可以设置为false;
changeOrigin:它表示是否更新代理后请求的headers中host地址;
changeOrigin的解析
这个 changeOrigin官方说的非常模糊,通过查看源码我发现其实是要修改代理请求中的headers中的host属性:
因为我们真实的请求,其实是需要通过 http://localhost:8888来请求的;
但是因为使用了代码,默认情况下它的值时 http://localhost:8000;
如果我们需要修改,那么可以将changeOrigin设置为true即可;
historyApiFallback
historyApiFallback是开发中一个非常常见的属性,它主要的作用是解决SPA页面在路由跳转之后,进行页面刷新时,返回404的错误。
boolean值:默认是false
object类型的值,可以配置rewrites属性:
事实上devServer中实现historyApiFallback功能是通过connect-history-api-fallback库的:
resolve模块解析
resolve用于设置模块如何被解析:
在开发中我们会有各种各样的模块依赖,这些模块可能来自于自己编写的代码,也可能来自第三方库;
resolve可以帮助webpack从每个 require/import 语句中,找到需要引入到合适的模块代码;
webpack 使用 enhanced-resolve 来解析文件路径;
webpack能解析三种文件路径:
绝对路径
相对路径
在这种情况下,使用 import 或 require 的资源文件所处的目录,被认为是上下文目录;
在 import/require 中给定的相对路径,会拼接此上下文路径,来生成模块的绝对路径;
模块路径
在 resolve.modules中指定的所有目录检索模块;
默认值是 [‘node_modules’],所以默认会从node_modules中查找文件;
我们可以通过设置别名的方式来替换初识模块路径,具体后面讲解alias的配置;
确实文件还是文件夹
如果是一个文件:
如果文件具有扩展名,则直接打包文件;
否则,将使用 resolve.extensions选项作为文件扩展名解析;
如果是一个文件夹:
会在文件夹中根据 resolve.mainFiles配置选项中指定的文件顺序查找;
resolve.mainFiles的默认值是 [‘index’];
再根据 resolve.extensions来解析扩展名;
extensions和alias配置
extensions是解析到文件时自动添加扩展名:
默认值是 [’.wasm’, ‘.mjs’, ‘.js’, ‘.json’];
所以如果我们代码中想要添加加载 .vue 或者 jsx 或者 ts 等文件时,我们必须自己写上扩展名;
另一个非常好用的功能是配置别名alias:
特别是当我们项目的目录结构比较深的时候,或者一个文件的路径可能需要 …/…/…/这种路径片段;
我们可以给某些常见的路径起一个别名;
resolve: {
extensions: [".wasm", ".mjs", ".js", ".json", ".jsx", ".ts", ".vue"],
alias: {
"@": resolveApp('./src'),
pages: resolveApp("./src/pages")
}
}
如何区分开发环境
目前我们所有的webpack配置信息都是放到一个配置文件中的:webpack.config.js
当配置越来越多时,这个文件会变得越来越不容易维护;
并且某些配置是在开发环境需要使用的,某些配置是在生成环境需要使用的,当然某些配置是在开发和生成环境都会使用的;
所以,我们最好对配置进行划分,方便我们维护和管理;
那么,在启动时如何可以区分不同的配置呢?
方案一:编写两个不同的配置文件,开发和生成时,分别加载不同的配置文件即可;
方案二:使用相同的一个入口配置文件,通过设置参数来区分它们;
"scripts": {
"build": "webpack --config ./config/common.config --env production",
"serve": "webpack serve --config ./config/common.config",
}
入口文件解析
我们之前编写入口文件的规则是这样的:./src/index.js,但是如果我们的配置文件所在的位置变成了 config 目录,我们是否应该变成 …/src/index.js呢?
如果我们这样编写,会发现是报错的,依然要写成 ./src/index.js;
这是因为入口文件其实是和另一个属性时有关的 context;
context的作用是用于解析入口(entry point)和加载器(loader):
官方说法:默认是当前路径(但是经过我测试,默认应该是webpack的启动目录)
另外推荐在配置中传入一个值;
// context是配置文件所在目录
module.exports = {
context: path.resolve(__dirname, "./"),
entry: "../src/index.js"
}
// context是上一个目录
module.exports = {
context: path.resolve(__dirname, "../"),
entry: "../src/index.js"
}
配置文件的分离
这里我们创建三个文件:
webpack.common.js
webpack.dev.js
webpack.prod.js
//webpack.common.js
const resolveApp = require("./paths");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
const TerserPlugin = require("terser-webpack-plugin");
const { merge } = require("webpack-merge");
const prodConfig = require("./webpack.prod");
const devConfig = require("./webpack.dev");
const commonConfig = {
entry: {
main: "./src/main.js",
index: "./src/index.js"
// main: { import: "./src/main.js", dependOn: "shared" },
// index: { import: "./src/index.js", dependOn: "shared" },
// lodash: "lodash",
// dayjs: "dayjs"
// shared: ["lodash", "dayjs"]
},
output: {
filename: "[name].bundle.js",
path: resolveApp("./build"),
chunkFilename: "[name].[hash:6].chunk.js"
},
resolve: {
extensions: [".wasm", ".mjs", ".js", ".json", ".jsx", ".ts", ".vue"],
alias: {
"@": resolveApp("./src"),
pages: resolveApp("./src/pages"),
},
},
module: {
rules: [
{
test: /\.jsx?$/i,
use: "babel-loader",
},
{
test: /\.vue$/i,
use: "vue-loader",
},
{
test: /\.css/i,
use: ["style-loader", "css-loader"],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "./index.html",
}),
new VueLoaderPlugin(),
],
optimization: {
// 对代码进行压缩相关的操作
minimizer: [
new TerserPlugin({
extractComments: false,
}),
],
// natural: 使用自然数(不推荐),
// named: 使用包所在目录作为name(在开发环境推荐)
// deterministic: 生成id, 针对相同文件生成的id是不变
// chunkIds: "deterministic",
splitChunks: {
// async异步导入
// initial同步导入
// all 异步/同步导入
chunks: 'all',
// 最小尺寸: 如果拆分出来一个, 那么拆分出来的这个包的大小最小为minSize
minSize: 200,
// 将大于maxSize的包, 拆分成不小于minSize的包
maxSize: 200,
// minChunks表示引入的包, 至少被导入了几次
minChunks: 1,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
filename: "[id]_vendors.js",
// name: "vendor-chunks.js",
priority: -10
},
// bar: {
// test: /bar_/,
// filename: "[id]_bar.js"
// }
default: {
minChunks: 2,
filename: "common_[id].js",
priority: -20
}
}
},
// true/multiple
// single
// object: name
// runtimeChunk: "single"
runtimeChunk: {
name: function(entrypoint) {
return `wuyuxin-${entrypoint.name}`
}
}
}
};
module.exports = function(env) {
const isProduction = env.production;
process.env.NODE_ENV = isProduction ? "production": "development";
const config = isProduction ? prodConfig : devConfig;
const mergeConfig = merge(commonConfig, config);
return mergeConfig;
};
// webpack.dev.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const isProduction = false
const path = require('path')
module.exports = function(env) {
return {
mode: "development",
// 专门为webpack-dev-server
// devServer为开发过程中, 开启一个本地服务
devServer: {
// hot: true,
// host: "0.0.0.0",
port: 7777,
// open: true,
hotOnly: true,
compress: true,
// publicPath: "/",
watchContentBase: true,
contentBase: path.resolve(__dirname, "./why"),
proxy: {
// "/why": "http://localhost:8888"
"/why": {
target: "http://localhost:8888",
pathRewrite: {
"^/why": ""
},
secure: false,
changeOrigin: true
}
},
// historyApiFallback: true,
historyApiFallback: {
rewrites: [
{from: /abc/, to: "/index.html"}
]
}
},
plugins: [
new ReactRefreshWebpackPlugin(),
]
}
}
// webpack.prod.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const isProduction = true;
module.exports = {
mode: "production",
plugins: [
// 生成环境
new CleanWebpackPlugin({}),
]
}
认识代码分离
代码分离(Code Splitting)是webpack一个非常重要的特性:
它主要的目的是将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件;
比如默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页的加载速度;
代码分离可以分出出更小的bundle,以及控制资源加载优先级,提供代码的加载性能;
Webpack中常用的代码分离有三种:
入口起点:使用entry配置手动分离代码;
防止重复:使用Entry Dependencies或者SplitChunksPlugin去重和分离代码;
动态导入:通过模块的内联函数调用来分离代码;
多入口起点
入口起点的含义非常简单,就是配置多入口:
比如配置一个index.js和main.js的入口;
他们分别有自己的代码逻辑;
entry: {
index: "./src/index.js",
main: "./src/main.js"
},
output: {
filename: "[name].bundle.js",
path: resolveApp("./build")
}
Entry Dependencies(入口依赖)
假如我们的index.js和main.js都依赖两个库:lodash、dayjs
如果我们单纯的进行入口分离,那么打包后的两个bunlde都有会有一份lodash和dayjs;
事实上我们可以对他们进行共享;
entry: {
index: { import: "./src/index.js", dependOn: "shared" },
main: { import: "./src/main.js", dependOn: "shared" },
shared: ['loadsh', 'axios']
},
output: {
filename: "[name].bundle.js",
path: resolveApp("./build"),
publicPath: ""
}
SplitChunks
另外一种分包的模式是splitChunk,它是使用SplitChunksPlugin来实现的:
因为该插件webpack已经默认安装和集成,所以我们并不需要单独安装和直接使用该插件;
只需要提供SplitChunksPlugin相关的配置信息即可;
Webpack提供了SplitChunksPlugin默认的配置,我们也可以手动来修改它的配置:
optimization: {
splitChunks: {
chunks: 'all'
}
}
SplitChunks自定义配置
当然,我们可以自定义更多配置,我们来了解几个非常关键的属性:
splitChunks: {
// async异步导入
// initial同步导入
// all 异步/同步导入
chunks: "all",
// 拆分包的大小,至少为minSize
// 如果一个包拆分出来达不到minSize,那么这个包就不会拆分
minSize: 100,
// 将大于maxSize的包,拆分成不小于minSize的包
maxSize: 1000,
// 至少包被引入的次数
minChunks: 2,
// 最大的异步请求数量
maxAsyncRequests: 30,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
filename: "[id]_vendors.js",
// name: "vendor-chunks.js",
priority: -10
},
// bar: {
// test: /bar_/,
// filename: "[id]_bar.js"
// }
default: {
minChunks: 2,
filename: "common_[id].js",
priority: -20
}
}
}
SplitChunks自定义配置解析
Chunks:
默认值是async
另一个值是initial,表示对通过的代码进行处理
all表示对同步和异步代码都进行处理
minSize:
拆分包的大小, 至少为minSize;
如果一个包拆分出来达不到minSize,那么这个包就不会拆分;
maxSize:
minChunks:
至少被引入的次数,默认是1;
如果我们写一个2,但是引入了一次,那么不会被单独拆分;
name:设置拆包的名称
可以设置一个名称,也可以设置为false;
设置为false后,需要在cacheGroups中设置名称;
cacheGroups:
用于对拆分的包就行分组,比如一个lodash在拆分之后,并不会立即打包,而是会等到有没有其他符合规则的包一起来打包;
test属性:匹配符合规则的包;
name属性:拆分包的name属性;
filename属性:拆分包的名称,可以自己使用placeholder属性;
动态导入(dynamic import)
另外一个代码拆分的方式是动态导入时,webpack提供了两种实现动态导入的方式:
第一种,使用ECMAScript中的 import() 语法来完成,也是目前推荐的方式;
第二种,使用webpack遗留的 require.ensure,目前已经不推荐使用;
比如我们有一个模块 bar.js:
该模块我们希望在代码运行过程中来加载它(比如判断一个条件成立时加载);
因为我们并不确定这个模块中的代码一定会用到,所以最好拆分成一个独立的js文件;
这样可以保证不用到该内容时,浏览器不需要加载和处理该文件的js代码;
这个时候我们就可以使用动态导入;
注意:使用动态导入bar.js:
在webpack中,通过动态导入获取到一个对象;
真正导出的内容,在改对象的default属性中,所以我们需要做一个简单的解构;
动态导入的文件命名
动态导入的文件命名:
因为动态导入通常是一定会打包成独立的文件的,所以并不会再cacheGroups中进行配置;
那么它的命名我们通常会在output中,通过 chunkFilename 属性来命名;
output: {
filename: "[name].bundle.js",
path: resolveApp("./build"),
chunkFilename: "chunk_[id]_[name].js"
}
但是,你会发现默认情况下我们获取到的 [name] 是和id的名称保持一致的
import(/* webpackChunkName: "foo" */"./foo").then(res => {
console.log(res);
});
代码的懒加载
动态import使用最多的一个场景是懒加载(比如路由懒加载):
封装一个component.js,返回一个component对象;
我们可以在一个按钮点击时,加载这个对象;
const element = document.createElement('div');
element.innerHTML = "Hello Element";
export default element;
const button = document.createElement("button");
button.innerHTML = "加载元素";
button.addEventListener("click", () => {
// prefetch -> 魔法注释(magic comments)
/* webpackPrefetch: true */
/* webpackPreload: true */
import(
/* webpackChunkName: 'element' */
/* webpackPrefetch: true */
"./element"
).then(({default: element}) => {
document.body.appendChild(element);
})
});
document.body.appendChild(button);
optimization.chunkIds配置
optimization.chunkIds配置用于告知webpack模块的id采用什么算法生成。
有三个比较常见的值:
natural:按照数字的顺序使用id;
named:development下的默认值,一个可读的名称的id;
deterministic:确定性的,在不同的编译中不变的短数字id
在webpack4中是没有这个值的;
那个时候如果使用natural,那么在一些编译发生变化时,就会有问题;
最佳实践:
开发过程中,我们推荐使用named;
打包过程中,我们推荐使用deterministic;
optimization. runtimeChunk配置
配置runtime相关的代码是否抽取到一个单独的chunk中:
runtime相关的代码指的是在运行环境中,对模块进行解析、加载、模块信息相关的代码;
比如我们的component、bar两个通过import函数相关的代码加载,就是通过runtime代码完成的;
抽离出来后,有利于浏览器缓存的策略:
比如我们修改了业务代码(main),那么runtime和component、bar的chunk是不需要重新加载的;
比如我们修改了component、bar的代码,那么main中的代码是不需要重新加载的;
设置的值:
true/multiple:针对每个入口打包一个runtime文件;
single:打包一个runtime文件;
对象:name属性决定runtimeChunk的名称;
optimization: {
chunkIds: 'deterministic',
runtimeChunk: {
name: "runtime"
}
}
Prefetch和Preload
webpack v4.6.0+ 增加了对预获取和预加载的支持。
在声明 import 时,使用下面这些内置指令,来告知浏览器:
prefetch(预获取):将来某些导航下可能需要的资源
preload(预加载):当前导航下可能需要资源
与 prefetch 指令相比,preload 指令有许多不同之处:
preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
什么是CDN?
CDN称之为内容分发网络(Content Delivery Network或Content Distribution Network,缩写:CDN)
它是指通过相互连接的网络系统,利用最靠近每个用户的服务器;
更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户;
来提供高性能、可扩展性及低成本的网络内容传递给用户;
在开发中,我们使用CDN主要是两种方式:
方式一:打包的所有静态资源,放到CDN服务器,用户所有资源都是通过CDN服务器加载的;
方式二:一些第三方资源放到CDN服务器上;
购买CDN服务器
如果所有的静态资源都想要放到CDN服务器上,我们需要购买自己的CDN服务器;
目前阿里、腾讯、亚马逊、Google等都可以购买CDN服务器;
我们可以直接修改publicPath,在打包时添加上自己的CDN地址;
第三方库的CDN服务器
通常一些比较出名的开源框架都会将打包后的源码放到一些比较出名的、免费的CDN服务器上:
国际上使用比较多的是unpkg、JSDelivr、cdnjs;
国内也有一个比较好用的CDN是bootcdn;
在项目中,我们如何去引入这些CDN呢?
第一,在打包的时候我们不再需要对类似于lodash或者dayjs这些库进行打包;
第二,在html模块中,我们需要自己加入对应的CDN服务器地址;
第一步,我们可以通过webpack配置,来排除一些库的打包:
第二步,在html模块中,加入CDN服务器地址:
认识shimming
shimming是一个概念,是某一类功能的统称:
shimming翻译过来我们称之为 垫片,相当于给我们的代码填充一些垫片来处理一些问题;
比如我们现在依赖一个第三方的库,这个第三方的库本身依赖lodash,但是默认没有对lodash进行导入(认为全局存在lodash),那么我们就可以通过ProvidePlugin来实现shimming的效果;
注意:webpack并不推荐随意的使用shimming
Webpack背后的整个理念是使前端开发更加模块化;
也就是说,需要编写具有封闭性的、不存在隐含依赖(比如全局变量)的彼此隔离的模块;
Shimming预支全局变量
目前我们的lodash、dayjs都使用了CDN进行引入,所以相当于在全局是可以使用_和dayjs的
axios.get("http://123.207.32.32:8000/recommend").then(res => {
console.log(res)
})
get("http://123.207.32.32:8000/recommend").then(res => {
console.log(res)
})
我们可以通过使用ProvidePlugin来实现shimming的效果:
ProvidePlugin能够帮助我们在每个模块中,通过一个变量来获取一个package;
如果webpack看到这个模块,它将在最终的bundle中引入这个模块;
另外ProvidePlugin是webpack默认的一个插件,所以不需要专门导入;
new ProvidePlugin({
axios: "axios",
get: ["axios", "get"]
})
MiniCssExtractPlugin
MiniCssExtractPlugin可以帮助我们将css提取到一个独立的css文件中,该插件需要在webpack4+才可以使用。
首先,我们需要安装 mini-css-extract-plugin:
npm install mini-css-extract-plugin -D
配置rules和plugins:
plugins: [
new MiniCssExtractPlugin({
filename: "css/[name].[contenthash:6].css",
chunkFilename: "css/[name].[contenthash:6].css"
})
],
module: {
rules: [
{
{
test: /\.css/i,
// style-lodader -> development
use: [
isProduction ? MiniCssExtractPlugin.loader: "style-loader",
"css-loader"],
},
}
]
}
Hash、ContentHash、ChunkHash
在我们给打包的文件进行命名的时候,会使用placeholder,placeholder中有几个属性比较相似:
hash、chunkhash、contenthash
hash本身是通过MD4的散列函数处理后,生成一个128位的hash值(32个十六进制);
hash值的生成和整个项目有关系:
比如我们现在有两个入口index.js和main.js;
它们分别会输出到不同的bundle文件中,并且在文件名称中我们有使用hash;
这个时候,如果修改了index.js文件中的内容,那么hash会发生变化;
那就意味着两个文件的名称都会发生变化;
chunkhash可以有效的解决上面的问题,它会根据不同的入口进行借来解析来生成hash值:
contenthash表示生成的文件hash名称,只和内容有关系:
比如我们的index.js,引入了一个style.css,style.css有被抽取到一个独立的css文件中;
这个css文件在命名时,如果我们使用的是chunkhash;
那么当index.js文件的内容发生变化时,css文件的命名也会发生变化;
这个时候我们可以使用contenthash;
在进行前端开发设置谷歌浏览器跨域时遇到了问题
总结三种方法:
一、49版本以前的设置:
在桌面chrome快捷方式的属性中的目标输入框添加 --disable-web-security 添加部分与前面字符之间有空格(有文章说目标引号结尾的加 --args --disable-web-security,反正我试过,没有49版本之前的)
二、49版本以后的设置:
1.在电脑上新建一个目录,例如:C:\MyChromeDevUserData
2.在属性页面中的目标输入框里加上 --disable-web-security --user-data-dir=C:\MyChromeDevUserData,–user-data-dir的值就是刚才新建的目录(参考上面截图)
(我用此方法能成功设置)
三、如果以上两种方法失败,用以下方法(此方法为网上copy)
1.通过桌面快捷方式打开文件位置,找到谷歌浏览器安装位置,看到有个chrome.exe文件
2.在电脑上新建一个目录,例如:C:\MyChromeDevUserData
3.打开cmd命令行,进入快捷方式位置 例如
cd C:\Program Files (x86)\Google\Chrome\Application
4.通过命令行启动谷歌浏览器
C:\Program Files (x86)\Google\Chrome\Application>chrome.exe --disable-web-security --user-data-dir=C:\MyChromeDevUserData
各位根据自己情况做相应更改即可
<script type=‘module‘>
Document
通过默认浏览器运行时报错,加载js被阻止。出现了以下错误
解决方法如下:
配置webpack打包主入口和出口
npx webpack --entry .\src\main.js --output-path .\build // 报上面的错误
错误原因:
无法解析Windows的.\src\main.js
路径。需要换成./src/main.js
也就是,改成下面这样就不会报这个错了
npx webpack --entry ./src/main.js --output-path ./build
{
test: /\.(png|jpg|jpe?g|gif|svg)$/,
// type: "asset/resource", // file-loader效果
// type: "asset/inline", // url-loader
type: "asset",
generator: {
filename: "img/[name].[hash:6][ext]"
},
parser: {
dataUrlCondition: {
maxSize: 100 * 1024
}
}
},
const imgEl = new Image();
imgEl.src = require('../img/zznh.png').default
element.appendChild(imgEl)
通过以上方式打包之后的,加载不出zznh.png这张图片
如果换成下面这种引入方式,则可以加载出来
import zznhImage from "../img/zznh.png";
const imgEl = new Image();
// imgEl.src = require('../img/zznh.png').default
imgEl.src = zznhImage
element.appendChild(imgEl)
或者不用assetModuleType。重新使用file-loader那种方式
{
test: /\.(png|jpg|jpe?g|gif|svg)$/,
use: [
{
loader: 'file-loader',
options: {
name: "img/[name].[hash:6].[ext]",
// outputPath: 'img'
}
}
]
}
这样子打包也可以正常引入zznh这张图片
一开始,我以为是因为淘宝镜像引起的问题,因为一开始我通过npm安装依赖报错了,就直接用cnpm 安装依赖了
结果npm安装依赖之后的报错更加简便,只有一个
Unexpected identifier
百度也找不到答案,一开始我以为是postcss-cli的版本问题。我一开始的版本是9.1.0,这个时候执行上面那条命令,就只会爆出这个错误。后面我就重新安装postcss-cli
npm uninstall postcss-cli
npm install postcss-cli@8.3.1
之后再执行上述命令行语句之后,这时候的报错才显得正常了起来
ExperimentalWarning: The fs.promises API is experimental
原来是node版本过旧,我原先的node版本为10.16.0,这个版本太老了,重新安装node到14.17.6就一切都正常了
[webpack-cli] HookWebpackError: Not supported
at xxxxxxxxxxx\node_modules\copy-webpack-plugin\dist\index.js:485:13
.....
-- inner error --
版本问题导致
CopyWebpackPlugin报错版本:10.0.0
解决办法:降低版本
npm i -D copy-webpack-plugin@9.*
码云地址