Webpack5入门到精通
安装
npm install webpack webpack-cli –g # 全局安装
npm install webpack webpack-cli –D # 局部安装
webpack命令指定某个文件为入口文件
npx webpack --entry ./src/main.js --output-path ./build
//或者改为:
“scripts”: {
"build":webpack --entry ./src/main.js --output-path ./build
}
或者在根目录新建webpack.config.js
const path = require('path')
module.exports = {
entry:'./src/main.js', //入口文件
output:{ //出口
filename:"bundle.js", //输入文件的名字
path:path.resolve(__dirname,'./build')
}
}
更改指定配置文件
例如将webpack.config.js更改为wk.config.js
“scripts”: {
"build":webpack --config ./wk.config.js
}
webpack是如何将项目打包?
- webpack在处理应用程序时,根据命令或者配置文件找到入口文件。
- 接着会生成一个依赖关系图(包含App中所需要的所有模块,JS、CSS、图片、字体)
- 然后遍历图结构,根据不同模块所依赖的loader,打包成一个个模块。
Loader
Loader是用于特定的模块类型进行转换。
css-loader的使用
css-loader安装:npm install css-loader -D
方式1:内联方式
import "style-loader!css-loader!../css/index.css"
方式2:CLI方式
//此方式webpack5已经弃用
"scripts":{
"build":"webpack --module-bind 'css=css-loader' --config wk.config.js"
}
方式3:loader配置方式
module.exports ={
module:{ //多个属性
rules:[ //存放Rule对象
{
test:/\.css$/i, //匹配资源
//loader:"css-loader" (方式一),
use:[
//{(方式二)
//loader:"css-loader",
//options:{}
// }
"css-loader" (方式三)
]
}
]
}
}
style-loader
css-loader负责将.css文件进行解析,style-loader将css插入到页面中。
安装: npm install style-loader -D
rules:[
{
test:/\.css$/,
use:[
"style-loader","css-loader" //loader处理是从下往上,从右往左,从后往前 进行处理
]
}
]
预处理器
例如:安装less. npm install less -D
方式1:npx less ./src/css/component.less > component.css
方式2:
module:{
rules:[
{
test:/\.less$/,
use:[
"style-loader",
"css-loader",
"less-loader"
]
}
]
}
SASS 同上
浏览器兼容性
查看node_modules是否安装过browserslist?
npm i browserslist
执行browserslist:
npx browserslist ">1%,last 2 version, not dead"
“ , ” 或者 “ or ” 或者 “换行” 并集关系
“ and ” 交集关系
“ not ” 取反
npx browserslist 命令会默认执行 .browserslistrc
方式1:
package.json:
"browserslist":{
">1%",
"last 2 version",
"not dead"
}
方式2:
创建文件 ".browserslistrc"
>1%
last 2 version
not dead
PostCSS工具
通过JS转换CSS样式,进行CSS适配,比如自动添加浏览器的前缀,css样式重置。
第一步:查找PostCSS在构建中的扩展,比如webpack中的postcss- loader;
第二步:选择可以添加PostCSS相关的插件
安装:
npm install postcss -D
如果需要在命令行运行postcss需要安装:
npm install postcss-cli -D
例如对单个css文件进行打包:
npx postcss -o result.css ./src/css/test.css
如果需要将css文件中的样式添加前缀,直接运行上面的命令会出现提示:You did not set any plugins ... 。因此需要安装autoprefixer
npm install autoprefixer -D
然后运行(对单个文件进行处理):
npx postcss --use autoprefixer -o result.css ./src/css/test.css
此时查看result.css:
:-webkit-full-screen {
font-size: 12px;
}
:-ms-fullscreen {
font-size: 12px;
}
:fullscreen {
font-size: 12px;
}
.content {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
transition: all 2s ease;
}
webpack 中使用postcss
安装postcss-loader:
npm install postcss-loader -D
配置webpack.config.js
rules:[
{
test:/\.css$/,
use:[
"style-loader",
"css-loader",
{
loader:"postcss-loader",
options:{
postcssOptions:{
plugins:[
require("autoprefixer")
]
}
}
}
]
}
]
postcss-preset-env
在配置post-loader时,配置插件并不需要使用autoprefixer
安装:
npm install postcss-preset-env -D
使用
rules:[
{
test:/\.css$/,
use:[
"style-loader",
"css-loader",
{
loader:"postcss-loader",
options:{
postcssOptions:{
plugins:[
//require("autoprefixer"), 可以删除掉
//require("postcss-preset-env")或者
"postcss-preset-env"
]
}
}
}
]
}
]
建议写法:
在根目录下创建 postcss.config.js
module.exports = {
plugins:[
require("postcss-preset-env")
]
}
当在Css文件中@import的新的Css文件。需要添加:
rules:[ { test:/\.css$/, use:[ "style-loader", { loader:"css-loader", options:{ importLoaders:1 //与后面loader个数有关 } }, "postcss-loader" ] } ]
file-loader
处理jpg、png等格式的图片,需要安装file-loader
安装:
npm install file-loader -D
wk.config.js的rules进行配置:
{
test:/\.(png|jpe?g|gif|svg)$/,
use:"file-loader"
}
需要注意的是在JS文件到如图片的时候(require)需要 “.default”
设置打包之后的图片名称:
{
test:/\.(png|jpe?g|gif|svg)$/,
use:{
loader:"file-loader",
options:{
name:"img/[name].[hash:6].[ext]", //hash:截取6位
//outputPath:"img" 指定输出目录
}
},
}
常用placeholder:
url-loader
将较小的文件,转换成base64文件,将文件嵌入到打包好的JS文件中。
{
test:/\.(png|jpg|jpeg|gif|svg)$/,
use:[
{
loader:'url-loader',
options:{
name:"img/[name].[hash:6].[ext]",
// outputPath:'img'
limit:100 * 1024 //小于此大小的文件转换为base64
}
}
]
}
asset module type
需要将上述的依赖中卸载 file-loader 和 url-loader
npm uninstall file-loader url-loader
然后在package.json中配置
{
test:/\.(png|jpg|jpeg|gif|svg)$/,
type:"asset/resource",
//type:"asset/inline" 将文件导入到jS文件中
generator:{ //自定义名称01
filename:"img/[name].[hash:6][ext]"
}
}
自定义名称02 (二选一):
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, './build'),
assetModuleFilename:"img/[name].[hash:6][ext]" //自定义文件名称
},
按照文件大小去设置文件转换(base64 / 路径. ):
需要配置:
{
test:/\.(png|jpg|jpeg|gif|svg)$/,
type:"asset",
generator:{
filename:"img/[name].[hash:6][ext]"
},
parser:{
dataUrlCondition:{
maxSize:100 * 1024
}
}
}
加载字体文件:
需要在package.json中配置:
{
test:/\.ttf|eot|woff2?$/i,
type:"asset/resource",
generator:{
filename:"font/[name].[hash:6][ext]"
}
}
Plugin
Plugin可以用于执行更加广泛的任务,比如打包优化、资源管理、环境变量注入等。
CleanWebpackPlugin
安装:
npm install clean-webpack-plugin -D
配置:
package.json:
const { CleanWebpackPlugin} = require("clean-webpack-plugin")
module.exports = {
plugins:[
new CleanWebpackPlugin()
]
}
HtmlWebpackPlugin
HTML文件是编写在根目录下的,而最终打包的dist文件夹中是没有index.html文件的,在进行项目部署的时,必然也是需要有对应的入口文件index.html。因此需要动态创建index.html
安装:
npm install html-webpack-plugin -D
配置:
package.json:
const HtmlWebpackPlugin = require("html-webpack-plugin")
plugins:[
new HtmlWebpackPlugin({
title:"thunder webpack", //作用于html模板中:htmlWebapckPlugin.options.title
template: './public/index.html',
})
]
DefinePlugin
在定义的模板中还有一个Base_Url变量:
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
DefinePlugin允许在编译时创建配置的全局常量,是一个webpack内置的插件(不需要单独安装)
引入
const { DefinePlugin } = require('webpack');
配置:
plugins:[
new DefinePlugin({
BASE_URL:'"./"'
})
]
CopyWebpackPlugin
将文件复制到指定的文件夹中.
安装:
npm install copy-webpack-plugin -D
在webpack.config.js中进行配置:
导入:
const CopyWebpackPlugin = require("copy-webpack-plugin")
配置
plugins:[
new CopyWebpackPlugin(
{
patterns:[
from:'public', //设置从哪一个源中开始复制
//to:'' //复制到的位置,可以省略,会默认复制到打包的目录下;
globOptions:{ //设置一些额外的选项,其中可以编写需要忽略的文件
ignore:[
"**/index.html", //也不需要复制,因为我们已经通过HtmlWebpackPlugin完成了index.html的生成;
"**/.DS_Store",//mac目录下回自动生成的一个文件;
"**/abc.txt"
]
}
]
})
]
mode
打包模式:
development / production(丑化过的JS代码)
在webapck.config.js配置:
module.exports = {
mode: 'development', //模式
...
...
};
devtool
修改打包之后的代码,默认是 env
module.exports = {
devtool:"source-map",//还有配置eval
...
...
};
模块化打包原理
创建文件
format.js
const dataFormate = (data) => {
return '2020-12-12'
}
const priceFormate = (proce) => {
return "100.00"
}
module.exports = {
dataFormate,
priceFormate
}
main.js
export function sum(a,b) {
return a+b
}
export function mul(a,b) {
return a+b
}
commonJS打包源码
commonJS 打包之后的代码,一行行分析通俗易懂
common_index.js
const { dataFormate,priceFormate} = require('./js/format')
bundle.js
//定义一个对象
// 形式为:key,value
var __webpack_modules__ = {
'./src/js/format.js': function (module) {
const dataFormate = (data) => {
return '2020-12-12';
};
const priceFormate = (proce) => {
return '100.00';
};
module.exports = {
dataFormate,
priceFormate,
};
},
};
// The module cache
//作为加载模块的缓存
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// 1.Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = (__webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {},
});
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
//具体开始执行代码的逻辑
!(function () {
const { dataFormate, priceFormate } =
__webpack_require__('./src/js/format.js');
})();
ES打包源码
ES打包之后的源码,一行行分析通俗易懂
es_index.js
import {sum,mul} from './js/main'
bundles.js
//1、定义了一个对象,对象里面做了模块的映射
var __webpack_modules__ = {
/***/ './src/js/main.js': /***/ function (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__,
) {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ sum: function () {
return /* binding */ sum;
},
/* harmony export */ mul: function () {
return /* binding */ mul;
},
/* harmony export */
});
function sum(a, b) {
return a + b;
}
function mul(a, b) {
return a + b;
}
/***/
},
};
// 2.The module cache
var __webpack_module_cache__ = {};
// 3.The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = (__webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {},
});
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
/* webpack/runtime/define property getters */
!(function () {
// define getter functions for harmony exports
//给__webpack_require__这个函数添加一个属性:d -> 值为function
__webpack_require__.d = function (exports, definition) {
for (var key in definition) {
if (
__webpack_require__.o(definition, key) &&
!__webpack_require__.o(exports, key)
) {
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
}
}
};
})();
/* webpack/runtime/hasOwnProperty shorthand */
!(function () {
__webpack_require__.o = function (obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
};
})();
/* webpack/runtime/make namespace object */
!(function () {
// define __esModule on exports
__webpack_require__.r = function (exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
/***********************************/
var __webpack_exports__ = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
!(function () {
/*!*************************!*\
!*** ./src/es_index.js ***!
\*************************/
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _js_main__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__(/*! ./js/main */ './src/js/main.js');
})();
认识source-map
当代码报错需要调试时(debug),调试转换后的代码是很困难的。这时ource-map是从已转换的代码,映射到原始的源文件中,史浏览器可以重构原始源并在调试器中显示重建的原始源。
source-map文件结构
version:当前使用的版本,也就是最新的第三版;
sources:从哪些文件转换过来的source-map和打包的代码(最初始的文件);
names:转换前的变量和属性名称(因为我目前使用的是development模式,所以不需要保留转换前的名 称); mappings:source-map用来和源文件映射的信息(比如位置信息等),一串base64 VLQ(veriablelength quantity可变长度值)编码;
file:打包后的文件(浏览器加载的文件);
sourceContent:转换前的具体代码信息(和sources是对应的关系); psourceRoot:所有的sources相对的根目录;
不生成source-map的几个值
配置一eval:
module.exports = {
mode: 'development', //模式,
devtool:"eval",//配置eval
...
...
};
生成对应的文件信息
eval(
'......sourceURL=webpack://thunder/./src/index.js?',
);
配置二默认不填写:
略
source-map
需要在package.json中配置:
module.exports = {
...
devtool:"source-map",
...
...
};
打包完成后在bundle.js中会有这么一行注释*//# sourceMappingURL=bundle.js.map*指向source-map文件。
eval-source-map
生成sourcemap,但是source-map是以DataUrl添加到eval函数的后面
inline-source-map
会生成sourcemap,但是source-map是以DataUrl添加到bundle文件的后面
cheap-source-map
会生成sourcemap,但是会更加高效一些(cheap低开销),因为它没有生成列映射(Column Mapping),在开发中,我们可以定位到错误。
cheap-module-source-map
类似于cheap-source-map,但是对源自loader的sourcemap处理会更好。如果loader对我们的源码进行了特殊的处理,此时我们需要用cmsm
当然我们需要安装一些loader比如时候ES6转换成ES5
npm install @babel/core babel-loader @babel/preset-env -D
进行配置:
module.exports = {
...
devtool: 'cheap-module-source-map',
...
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
],
},
...
};
cheap-source-map和cheap-module-source-map对比
cheap-source-map和cheap-module-source-map的区别:
建议使用
cheap-module-source-map
hidden-sorce-map
将boundle.js 中的注释
//# sourceMappingURL=bundle.js.map
删除掉(大可不用)
nosources-source-map
会生成sourcemap,但是生成的sourcemap只有错误信息的提示,不会生成源代码文件(大可不用);
提示信息:
点击错误信息,无法查看源码:
多个值的组合
组合的规则
-
inline-|hidden-|eval:三个值时三选一;
-
nosources:可选值;
-
cheap可选值,并且可以跟随module的值;
[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
开发阶段:推荐使用 source-map或者cheap-module-source-map。这分别是vue和react使用的值,可以获取调试信息,方便快速开发;
测试阶段:推荐使用 source-map或者cheap-module-source-map。测试阶段我们也希望在浏览器下看到正确的错误提示;
发布阶段:false、缺省值(不写)
认识babel
如果您需要直接运行babel的相关命令需要安装babel-cli
npm install @babel/cli -D
例如:
npx babel src --out-dir result
箭头函数转换
npm install @babel/plugin-transform-arrow-functions -D
例如:
npx babel src --out-dir result --plugins=@babel/plugin-transform-arrow-functions
const转换成var
npm install @babel/plugin-transform-block-scoping -D
例如:
npx babel src --out-dir result --plugins=@babel/plugin-transform-arrow-functions,@babel/plugin-transform-block-scoping
预设
npm install @babel/preset-env -D
例如:
npx babel src --out-dir result --presets=@babel/preset-env
当然您也可以在webpack.config.js中进行配置
module: { rules: [ { test: /\.js$/, use: { loader: 'babel-loader', options: { plugins: [ "@babel/plugin-transform-arrow-functions",//箭头函数 "@babel/plugin-transform-block-scoping" //const 转换成var ], ====>或直接使用“预设” presets:[ "@babel/preset-env" ] }, }, }, ], },需要注意的是
.browserslistrc文件非常重要~
babel编译器原理
一张图,两个链接(github / 相关资料(rb8u )).
options => presets 属性设置
use: {
loader: 'babel-loader',
options: {
presets:[
["@babel/preset-env",{
targets:["chrome 88"],
esmodules:true
}]
]
},
},
内部的适配权重高于. browserslistrc,建议不要采用。
Babel的配置文件
两种文件配置的方式:
babel.config.json (或者.js , .cjs , .mjs) 文件;
.babelrc.json (或者.babelrc , .js , .cjs , .mjs)文件;
在根目录创建 babel.config.js
module.exports = {
presets:[
"@babel/preset-env"
]
}
将webpack.config.js中的关于babel-loader的options配置注释,在进行测试
认识polyfill
安装
npm install core-js regenerator-runtime --save
配置babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
//配置polyfill
// useBuiltIns:false, //不适用任何polyfill
// useBuiltIns:"usage", //代码中需要那些polyfill,就引入polyfill
useBuiltIns: 'entry', //默认不会生效,需要在入口文件中引入 ==》 index.js
corejs: 3,
},
],
],
};
useBuiltIns: 设置 以什么样的方式来使用polyfill;
corejs: 设置corejs发版本,目前使用较多的是3.x的版本;
为了防止和node_modules中的模块冲突,建议在webpack.config.js中配置:
rules: [ { test: /\.js$/, exclude:/node_modules/, //预防在依赖中也存在babel-loader从而引发的冲突 use: { loader: 'babel-loader', options: { }, }, }, ],使用 “entry”时需要在入口文件顶部配置:
import "core-js/stable"; import "regenerator-runtime"这样会根据browserslist 目标导入所有的polyfill,但是包的体积也会变大
React中jsx支持
在写React代码时需要安装相关依赖:
npm install react react-dom --save
创建react_main.js 文件作为webpack的入口文件
import React, { Component } from 'react';
import ReactDom from 'react-dom';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
message: 'thunder',
};
}
render() {
return <div>{this.state.message}</div>;
}
}
ReactDom.render(<App></App>, document.getElementById('app'));
此时打包时是不能正常打包的会报错~,需要安装babel ==> @babel/preset-react -D
npm install @babel/preset-react -D
在babel.config.js中配置:
module.exports = {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'entry'
corejs: 3,
},
],
['@babel/preset-react'], //重点
],
};
webpack编译typeScript
全局安装typescript
npm install typescript -g
测试文件index.ts:
const message: string = 'hello TypeScript';
const foo = (info: string) => {
console.log(info);
};
foo(message);
export {}
编译指令:
tsc index.ts
ts-loader
安装:
npm intall ts-loader -D
然后在webpack.config.js中进行配置
module: {
rules: [
{
test: /\.js$/,
exclude:/node_modules/, //预防在依赖中也存在babel-loader从而引发的冲突
use: {
loader: 'babel-loader'
},
},
},
{
test:/\.ts$/,
use:"ts-loader"
}
],
},
此时在进行打包的时候会出现报错:
原因是缺少tsconfig.json文件
创建tsconfig.json文件命令:
tsc --init
打包:
npm run build
babel编译ts
直接修改webpack.config.js中的配置文件:
{
test:/\.ts$/,
exclude:/node_modules/,
use:"babel-loader"
}
当然我们还需要安装预设:
npm install @babel/preset-typescript -D
然后在babel.config.js在配置
presets: [
...
['@babel/preset-react'],
['@babel/preset-typescript'], //
],
npm run build
ts-loader和babel-loader选择
ts-loader:
- 直接编译typeScript,只能将ts转换成js;
- 如果希望在这个过程中添加对应的polyfill,ts-loader不支持;
- 我们需要借助babel来完成polyfill的填充功能。
babel-loader:
- 直接编译typeScript,只能将ts转换成js,并且可以实现polyfill的功能;
- 但是在编译的过程中,不会对类型错误进行检测。
推荐使用
在package.json中配置
"scripts": {
"build": "npm run type-check & webpack --config ./wk.config.js",
"type-check":"tsc --noEmit"
},
直接运行 npm run build
或者:
"scripts": {
"build": "webpack --config ./wk.config.js",
"type-check":"tsc --noEmit",
"type-check-watch":"tsc --noEmit --watch"
},
首先运行 npm run type-check-watch,此时终端会启动代码调试的功能,调试完毕后(Found 0 errors.Watch for file changes)
在运行 npm run build
ESLint
安装
npm install eslint -D
初始化eslint配置文件
npx eslint --init
检测
npx eslint ./src/index.js
相关配置略
webpack中配置eslint-loader
安装:
npm install eslint-loader -D
打包:
npm run build
vscode中使用esLint
略
webpack中加载vue
安装Vue:
npm install vue
安装 vue-loader
npm install vue-loader -D
安装 template
npm install vue-template-compiler -D
在webpack.config.js中配置:
const VueLoaderPlugin = require('vue-loader/lib/plugin');
...
module: {
rules: [
{
test: /\.less$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
'postcss-loader',
'less-loader',
],
},
{
test: /\.js$/,
exclude: /node_modules/,
use: ['babel-loader'],
},
{
test: /\.vue$/,
use: 'vue-loader',
},
],
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'thunder webpack',
template: './public/index.html',
}),
new DefinePlugin({
BASE_URL: '"./" ',
}),
new VueLoaderPlugin(),
],
打包:
npm run build
webpack的DevServer
完成自动编译,webpack提供了几种可选的方式:
- pwebpack watch mode
- webpack-dev-server
- webpack-dev-middleware
watch
避免每次在修改代码之后,我们需要重新执行npm run build因此需要实时监听代码的变化。
方式一:
在package.json中配置:
"scripts": {
"watch":"webpack --watch"
},
npm run watch
方式二:
直接在webpack.config.js中配置:
module.exports = {
watch: true,
...
};
npm run build
webpack-dev-server
上面的方式可以监听到文件的变化,但是事实上它本身是没有自动刷新浏览器的功能的.
它的功能就是
安装:
npm install webpack-dev-server -D
在package.json中配置:
"scripts": {
...
"serve":"webpack serve"
},
npm run serve
webpack-dev-server 在编译之后不会写入到任何输出文件。而是将 bundle 文件保留在内存中
webpack-dev-middleware
默认请情况下,当我们npm run serve的时候,会默认启动一个服务,我们可以根据自已的需求去自定义一个服务.
- webpack-dev-middleware 是一个封装器(wrapper),它可以把 webpack 处理过的文件发送到一个 server(见server.js代码)
- webpack-dev-server 在内部使用了它,然而它也可以作为一个单独的 package 来使用,以便根据需求进行 更多自定义设置
安装:
npm install webpack-dev-middleware express
在根目录创建server.js , 直接上代码。
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require('./webpack.config');
const compiler = webpack(config);
const middleware = webpackDevMiddleware(compiler);
app.use(middleware);
app.listen(3000, () => {
console.log('开启3000端口');
});
热更新(HMR)
模块热替换是指在 应用程序运行过程中,替换、添加、删除模块,而无需重新刷新整个页面。
优势:
- 不重新加载整个页面,这样可以保留某些应用程序的状态不丢失;
- 只更新需要变化的内容,节省开发的时间;
- 修改了css、js源代码,会立即在浏览器更新,相当于直接在浏览器的devtools中直接修改样式;
不开启HMR的情况下,当修改了源代码之后,整个页面会自动刷新,使用的是live reloading;不能再watch / webpack-dev-middleware 情况下使用
webpack.config.js中添加配置:
devServer: {
hot: true,
}
指定某个模块下面编写如下代码:
if (module.hot) {
module.hot.accept('./math.js', () => {
console.log('math模块发生了更新');
});
}
React-HMR
回顾首先安装react:
npm install react react-dom
安装相关babel
npm install @babel/core babel-loader @babel/preset-env @babel/preset-react -D
创建babel.config.js
module.exports = {
presets: [['@babel/preset-env'], ['@babel/preset-react']],
};
配置webpack.config.js
module:{
rules:[
{
test:/\.jsx?$/i,
use:"babel-loader"
}
]
},
创建jsx文件编写相关代码:
import React, { Component } from 'react'
export default class App extends Component {
constructor(props) {
super(props)
this.state = {
name:'thunder'
}
}
render() {
return (
<div>
{this.state.name}
</div>
)
}
}
在入口文件中引入
import React from 'react'
import ReactDOM from 'react-dom'
import ReactApp from './App.jsx'
...
...
ReactDOM.render(<ReactApp></ReactApp>,document.getElementById("app"))
在index.html中添加dom元素
<body>
<div id="app"></div>
</body>
安装依赖:
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh
在babel.config.js中添加配置:
module.exports = {
...
plugins:[
["react-refresh/babel"]
]
};
在webpack.config.js中添加
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin")
...
plugins: [
...
new ReactRefreshWebpackPlugin()
],
运行
npm run serve
Vue-HMR
创建.vue 文件
安装相关依赖
npm install vue
npm install vue-loader vue-template-compiler -D
npm install style-loader css-loader -D
配置webpack.config.js
...
const VueLoaderPlugin = require('vue-loader/lib/plugin');
module.exports = {
...
module: {
rules: [
...
{
test: /\.vue$/i,
use: 'vue-loader',
},
{
test:/\.css$/i,
use:[
"style-loader",
"css-loader"
]
}
],
},
plugins: [
...
new VueLoaderPlugin(),
],
};
HMR的原理
webpack-dev-server会创建两个服务:
提供静态资源的服务(express)和Socket服务(net.Socket);express server负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析);
HMR Socket Server,是一个socket的长连接:
长连接有一个最好的好处是建立连接后双方可以通信(服务器可以直接发送文件到客户端);当服务器监听到对应的模块发生变化时,会生成两个文件.json(manifest文件)和.js文件(update chunk);通过长连接,可以直接将这两个文件主动发送给客户端(浏览器);浏览器拿到两个新的文件后,通过HMR runtime机制,加载这两个文件,并且针对修改的模块进行更新;
原理图:
webpack对路径的处理
output中的path
path:作为webpack打包之后的输出目录,将静态资源的js,css等输入到指定文件夹中如:dist、build等
const path = require('path');
output: {
...
path: path.resolve(__dirname, './build'),
...
},
output中的publicPath
该属性是指定index.html文件打包引用的一个基本路径
- 它的默认值是一个空字符串,所以我们打包后引入js文件时,路径是 bundle.js;
- 如果将其设置为 / ,路径是 /bundle.js,那么浏览器会根据所在的域名+路径去请求对应的资源;
- 如果希望在本地直接打开html文件来运行,会将其设置为 ./,路径时 ./bundle.js,可以根据相对路径去查找资源;
devServer中的publicPath
- 它的默认值是 /,直接访问端口即可访问其中的资源 http://localhost:8080;
- 如果将其设置为了 /abc,那么我们需要通过 http://localhost:8080/abc才能访问到对应的打包后的资源;
- 并且这个时候,我们其中的bundle.js通过 http://localhost:8080/bundle.js也是无法访问的:
- 所以必须将output.publicPath也设置为 /abc;
- 官方其实有提到,建议 devServer.publicPath 与 output.publicPath相同;
devserver中的directory
作用是如果打包 后的资源,又依赖于其他的一些资源,那么就需要指定从哪里来查找这个内容:
- 在index.html中,需要依赖一个 abc.js 文件,这个文件我们存放在 public文件 中;
- 在index.html中,应该如何去引入这个文件呢
- 比如代码是这样的
<script src="./abc/tc/abc.js"></script> - 是这样打包后浏览器是无法通过相对路径去找到这个文件夹的;
所以代码是这样的:(存在着疑惑)<script src="/abc.js"></script>;- 设置directory即可;
- 比如代码是这样的
需要注意的是随着webpack5的更新devserver配置发生了很大的变化 正如第三点,在webpack5.65版本配置如下,可以正常运行,按照webpack5.65以前的版本属性
contentBase:path.resolve(__dirname, './abc'),inde.html中配置:<script src="./tc/abc.js"></script>可以正常运行。经过多次测试directory需要在路径前添加./abc/tc/abc.js。
目录结构:
index.html:
...
<script src="./abc/tc/abc.js"></script>
...
webpack.config.json
...
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, './build'),
publicPath: '/abc',
},
devServer: {
hot: true,
static:{
directory:path.resolve(__dirname, './abc'),
publicPath: '/abc',
},
},
...
devServer中的watchFiles
监听文件的变化,动态更新打包之后的代码,从而不会刷新整个页面.
配置如下:
devServer: {
hot: true,
watchFiles:'*',
...
},
详细配置见官网: watchFiles
devServer中的Gzip压缩
配置:
devServer: {
...
compress:true, //开启Gzip 压缩
...
},
devServer中的Proxy
设置代理来解决跨域访问的问题
配置:
devServer: {
...
proxy:{
'/api':{
target:'url',
pathRewrite:{ //重写路径,例如将'/api'替换为空. http://192.168.100.2/api/demo --> //http://192.168.100.2/demo
'/api':"",
secure:false, //跳过HTTPs证书验证
changeOrigin:true,
historyApiFallback: true//解决SPA页面在路由跳转之后,进行页面刷新时,返回404的错误。
}
}
}
...
},
devServer中的historyApiFallback
解决SPA页面在路由跳转之后,进行页面刷新 时,返回404的错误。
配置:
devServer: {
...
historyApiFallback: true
...
},
devServer.resolve中配置extensions
解析文件的后缀名,导入时将后缀删除
配置:
module.exports = {
...
resolve:{
extensions:['js','mjs',".json",'.jsx','.vue']
},
...
}
devServer.resolve中配置alias
配置别名
配置:
module.exports = {
...
resolve:{
alias:{
"@":path.resolve(__dirname,'./src')
}
},
...
}
webpack的环境分离
根目录创建 config 文件夹,分别创建 webpack.common.js 、webpack.dev.js 、 webpack.prod.js、path.js
webpack.common.js :
const path = require('path');
module.exports = function (env) {
const isProduction = env.production;
return {
// 配置绝对路径,默认使用 Node.js 进程的当前工作目录
// context:path.resolve(__dirname,"../"),
//entry:写上的相对路径,并不是相对于文件所在的路径,而是相对context配置的路径
entry: './src/index.js',
output: {
path: path.resolve(__dirname, '../build'),
},
};
};
最终实现截图:
path.js
const path = require('path');
const appDir = process.cwd();
const resolveApp = (relativePath) => path.resolve(appDir, relativePath);
module.exports = resolveApp;
webpack.common.js
const resolveApp = require("./paths");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const VueLoaderPlugin = require("vue-loader/lib/plugin");
const { merge } = require("webpack-merge");
const prodConfig = require("./webpack.prod");
const devConfig = require("./webpack.dev");
const commonConfig = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: resolveApp("./build"),
},
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(),
]
};
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 resolveApp = require('./paths');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const isProduction = false;
module.exports = {
mode: "development",
devServer: {
hot: true,
hotOnly: true,
compress: true,
contentBase: resolveApp("./why"),
watchContentBase: true,
proxy: {
"/tc": {
target: "http://localhost:8888",
pathRewrite: {
"^/tc": ""
},
secure: false,
changeOrigin: true
}
},
historyApiFallback: {
rewrites: [
{from: /abc/, to: "/index.html"} //当访问 params为/abc时会直接返回index.html
]
}
},
plugins: [
// 开发环境
new ReactRefreshWebpackPlugin(),
]
}
webpack.prod.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const isProduction = true;
module.exports = {
mode: "production",
plugins: [
// 生成环境
new CleanWebpackPlugin({}),
]
}
package.json
"scripts": {
"build": "webpack --config ./config/webpack.prod.js",
"serve": "webpack serve --config ./config/webpack.dev.js",
"build2": "webpack --config ./config/webpack.common.js --env production",
"serve2": "webpack serve --config ./config/webpack.common.js --env development"
},
webpack 手动分离代码
将代码分离到不同的bundle中,之后我们可以按需加载,或者并行加载这些文件,默认情况下,所有的JavaScript代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载, 就会影响首页的加载速度,代码分离可以分出出更小的bundle,以及控制资源加载优先级,提供代码的加载性能。
代码分离有三种:
-
入口起点:使用entry配置手动分离代码;
-
防止重复:使用Entry Dependencies或者SplitChunksPlugin去重和分离代码;
-
动态导入:通过模块的内联函数调用来分离代码;
入口起点及共享
配置一个index.js和main.js的入口,们分别有自己的代码逻辑,假如我们的index.js和main.js都依赖两个库:lodash、dayjs,
如果我们单纯的进行入口分离,那么打包后的两个bunlde都有会有一份lodash和dayjs,事实上我们可以对他们进行共享.
基本配置如下:
entry: {
main: {import:'./src/main.js',dependOn:shared},
index: {import:'./src/index.js',dependOn:"lodash"},
lodash: 'lodash',
dayjs:"dayjs",
shared:["lodash","dayjs"]
},
output: {
filename: '[name].bundle.js',
path: resolveApp('./build'),
},
额外配置:
optimization: {
minimize: true,
//对代码进行压缩的相关操作
minimizer: [
new TerserPlugin({
extractComments: false,//删除注释
}),
],
},
SplitChunks
另外一种分包的模式是splitChunk,它是使用SplitChunksPlugin来实现的
基础配置:
cacheGroups:用于对拆分的包就行分组,比如一个lodash在拆分之后,并不会立即打包,而是会等到有没有其他符合规则的包一起来打包
test属性:匹配符合规则的包;
name属性:拆分包的name属性;
filename属性:拆分包的名称,可以自己使用placeholder属性;
动态导入(dynamic import)
代码拆分的方式是动态导入时,webpack提供了两种实现动态导入的方式:
-
使用ECMAScript中的 import() 语法来完成,也是目前推荐的方式;
-
使用webpack遗留的 require.ensure,目前已经不推荐使用;
动态导入通常是一定会打包成独立的文件的,所以并不会再cacheGroups中进行配置;
通常会在output中,通过 chunkFilename 属性来命名;
默认情况下我们获取到的 [name] 是和id的名称保持一致的,
修改name的值,可以通过magic comments(魔法注释)的方式;
场景:
路由懒加载
chunkIds
optimization.chunkIds配置用于告知webpack模块的id采用什么算法生成。
有三个比较常见的值:
-
natural:按照数字的顺序使用id;
-
named:development下的默认值,一个可读的名称的id;
-
deterministic:确定性的,在不同的编译中不变的短数字id,有益于长期缓存。在生产模式中会默认开启
-
最佳实践:
- 开发过程中,我们推荐使用named;
- 打包过程中,我们推荐使用deterministic;
webpack懒加载
demo
element.js:
const element = document.createElement('div')
element.innerHTML = "Hello thunder"
export default element
index.js:
const button = document.createElement('button');
button.innerHTML = '点击我吧~';
button.addEventListener('click', () => {
//魔法注释
import(/*webpackChunkName: 'elemtn' */ './element.js').then(
({ defalut: element }) => {
document.body.appendChild(element);
},
);
});
document.body.appendChild(button);
如果页面进入的时候需要下载某文件,点击按钮加载已经下载的文件:
需要将魔法注释设置为:
/* webpackPrefetch:true */
或
/*webpackPreload:true*/
Prefetch VS Preload
- preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
- preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
- preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻
runtime
- 配置runtime相关的代码是否抽取到一个单独的chunk中:
-
runtime相关的代码指的是在运行环境中,对模块进行解析、加载、模块信息相关的代码;
-
比如我们的component、bar两个通过import函数相关的代码加载,就是通过runtime代码完成的;
- 抽离出来后,有利于浏览器缓存的策略:
- 比如我们修改了业务代码(main),那么runtime和component、bar的chunk是不需要重新加载的;
- 比如我们修改了component、bar的代码,那么main中的代码是不需要重新加载的;
- 设置的值:
- true/multiple:针对每个入口打包一个runtime文件;
- single:打包一个runtime文件;
- 对象:name属性决定runtimeChunk的名称;
配置如下:
module.exports = {
...
optimization:{
...
// true/multiple
// single
// object: name
runtimeChunk: {
name: function(entrypoint) {
return `tc-${entrypoint.name}`
}
}
}
...
}
第三方包CND配置
项目中使用到某些库,但是需要cnd加载,需要添加配置:
externals:{
lodash:"_", //暴露出的全局对象
dayjs:"dayjs"
},
在打包后的index.html中添加对应的CND链接
例如jQuery:
index.html
<script
src="https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous"
></script>
webpack.config.js
module.exports = {
//...
externals: {
jquery: 'jQuery',
},
};
这样就剥离了那些不需要改动的依赖模块,换句话,下面展示的代码还可以正常运行:
import $ from 'jquery';
$('.my-element').animate(/* ... */);
shimming
比如我们现在依赖一个第三方的库,这个第三方的库本身依赖axios,但是默认没有对axios进行导入(认 为全局存在axios),那么我们就可以通过ProvidePlugin来实现shimming的效果;
代码:
const webpack = require('webpack');
......
plugins: [
...
//当在代码走遇到某一个变量找不到时,可以通过ProvidePlugin,自动找到对应的库
new webpack.ProvidePlugin({
axios: 'axios', //变量名称 : 库
get: ['axios', 'get'],
}),
],
webpack并不推荐随意的使用shimming
CSS文件抽取
MiniCssExtractPlugin将css提取到一个独立的css文件中,该插件需要在webpack4+才可以使用
npm install mini-css-extract-plugin -D
plugins: [
...
new MinCssExtractPlugin({
filename:"css/[name].[hash:8].css",
chunkFilename:"css/[name].[contenthash:8].css"
})
]
module: {
rules: [
...
{
test: /\.css/i,
use: [MinCssExtractPlugin.loader, 'css-loader'],
},
],
},
Hash命名
webpack打包后的文件名配置:
- [ext] :目标文件/资源的文件扩展名。
- [name] :文件/资源的基本名称。
- [hash]:指定生成文件内容哈希值的哈希方法。
文件进行命名的时候,会使用placeholder,placeholder中有几个属性比较相似:
- hash、chunkhash、contenthash
- hash本身是通过MD4的散列函数处理后,生成一个128位的hash值(32个十六进制);
注意:
- hash值的生成和整个项目有关系:
- 比如我们现在有两个入口index.js和main.js;
- 它们分别会输出到不同的bundle文件中,并且在文件名称中我们有使用hash;
- 这个时候,如果修改了index.js文件中的内容,那么hash会发生变化;
- 那就意味着两个文件的名称都会发生变化
- chunkhash可以有效的解决上面的问题,它会根据不同的入口进行借来解析来生成hash值:
- 比如我们修改了index.js,那么main.js的chunkhash是不会发生改变的;
- contenthash表示生成的文件hash名称,只和内容有关系:
- 比如我们的index.js,引入了一个style.css,style.css有被抽取到一个独立的css文件中;
- 这个css文件在命名时,如果我们使用的是chunkhash;
- 那么当index.js文件的内容发生变化时,css文件的命名也会发生变化;
- 这个时候我们可以使用contenthash;
认识DLL库
DLL全称是动态链接库(Dynamic Link Library) ,是为软件在Windows中实现共享函数库的一种实现方式;
webpack中也有内置的DLL的功能,它指的是我们可以共享,并且不经常改变的代码,抽取成一个共享库;
DLL库的使用分为两步:
- 打包一个DLL库;
- 项目中引入DLL库;
注意:在升级到webpack4之后,React和Vue脚手架都移除了DLL库(下面的vue作者的回复)
搭建一个webpack环境:
npm init
安装相关依赖:
npm install webpack webpack-cli react react-dom
npm i clean-webpack-plugin -S-D
新建配置文件webpack.dell.js
const path = require('path');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const webpack = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
entry: {
react: ['react', 'react-dom'],
},
output: {
path: path.resolve(__dirname, './dll'),
filename: 'dll_[name].js',
library: 'dll_[name]',
},
optimization: {
minimizer: [
new TerserPlugin({
extractComments: false, //隐藏注释
}),
],
},
plugins: [
new CleanWebpackPlugin(),
new webpack.DllPlugin({
name: 'dll_[name]',
path: path.resolve(__dirname, './dll/[name].manifest.json'),
})
],
};
修改package.json文件:
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dll": "webpack --config ./webpack.dll.js"
}
运行:
npm run dll
结果:
会在根目录中生成dll文件夹
实践
保留上方的dll文件,webpack.dll.js 建议单独配置.
安装:add-asset-html-webpack-plugin
npm i add-asset-html-webpack-plugin -D
The plugin will add the given JS or CSS file to the files Webpack knows about, and put it into the list of assets
html-webpack-plugininjects into the generated html. Add the plugin to your config, providing it a filepath
webpack.common.js 配置插件:
plugins: [
new HtmlWebpackPlugin({
template: "./index.html",
}),
new VueLoaderPlugin(),
new webpack.DllReferencePlugin({ manifest:resolveApp("./dll/react.manifest.json"),
context: resolveApp("./"),
}),
new AddAssetHtmlPlugin({
filepath: resolveApp("./dll/dll_react.js"),
})
],
resolveApp获取绝对路径打印得:
resolveApp("./") : E:\mydata\webpack\webpack的DLL的使用 resolveApp("./dll/dll_react.js") : E:\mydata\webpack\webpack的DLL的使用\dll\react.manifest.json
打包
"scripts": {
"build": "webpack --config ./config/webpack.common.js --env production",
},
npm run build
打包的目录如下:
webpack中terser的使用
定义:
- Terser是一个JavaScript的解释(Parser)、Mangler(绞肉机)/Compressor(压缩机)的工具集;
- 早期使用 uglify-js来压缩、丑化我们的JavaScript代码,但是目前已经不再维护,并且不支持ES6+的 语法
- Terser是从 uglify-es fork 过来的,并且保留它原来的大部分API以及适配 uglify-es和uglify-js@3等
- Terser可以帮助我们压缩、丑化我们的代码,让我们的bundle变得更小
安装:
npm install terser
npx
命令行npx使用:
npx terser [input files] [options]
举例:
npx terser js/file1.js -o foo.min.js -c ...Compress option -m ...Mangle option
-
Compress option:
-
arrows:class或者object中的函数,转换成箭头函数;
-
arguments:将函数中使用 arguments[index]转成对应的形参名称;
-
dead_code:移除不可达的代码(tree shaking);
-
-
Mangle option
-
toplevel:默认值是false,顶层作用域中的变量名称,进行丑化(转换);
-
keep_classnames:默认值是false,是否保持依赖的类名称;
-
keep_fnames:默认值是false,是否保持原来的函数名称;
例如:
npx terser ./src/abc.js -o abc.min.js -c
arrows,arguments=true,dead_code -m
toplevel=true,keep_classnames=true,keep_fnames=true
TerserPlugin
修改webpack.prod.js
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
mode: 'production',
externals: {
lodash: '_',
dayjs: 'dayjs',
},
optimization: {
minimize: true,//改为false,关闭默认的minimize
minimizer: [
new TerserPlugin({
extractComments: false, // 提取注释
parallel: true, // 多核
terserOptions: {
compress: {
//压缩
arguments: true,
dead_code: true,
},
mangle: true,
toplevel: true,
keep_classnames: true,
keep_fnames: true,
},
}),
],
},
plugins: [
// 生成环境
new CleanWebpackPlugin({}),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:6].css',
}),
],
};
更多配置详见webpack官网
CSS压缩
安装插件:
npm install css-minimizer-webpack-plugin -D
修改webpack.prod.js
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
module.exports = {
mode: 'production',
...
plugins: [
// 生成环境
new CleanWebpackPlugin({}),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:6].css',
}),
new CssMinimizerPlugin()
],
};
Scope Hoisting
功能是对作用域进行提升,并且让webpack打包后的代码更小、运行更快;
- 默认情况下webpack打包会有很多的函数作用域,包括一些(比如最外层的)IIFE:
- 无论是从最开始的代码运行,还是加载一个模块,都需要执行一系列的函数;
- Scope Hoisting可以将函数合并到一个模块中来运行;
- 使用Scope Hoisting非常的简单,webpack已经内置了对应的模块:
- 在production模式下,默认这个模块就会启用;
- 在development模式下,我们需要自己来打开该模块
const webpack = require('webpack');
plugins: [
...
webpack.optimize.ModuleConcatenationPlugin(),
],
对比如下:
Tree Shaking
定义
- 最早的想法起源于LISP,用于消除未调用的代码(纯函数无副作用,可以放心的消除,这也是为什么要求我们在进 行函数式编程时,尽量使用纯函数的原因之一);
- 后来Tree Shaking也被应用于其他的语言,比如JavaScript、Dart;
- JavaScript的Tree Shaking:
- JavaScript进行Tree Shaking是源自打包工具rollup
- Tree Shaking依赖于ES Module的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系);
- webpack2正式内置支持了ES2015模块,和检测未使用模块的能力;
- 在webpack4正式扩展了这个能力,并且通过 package.json的 sideEffects属性作为标记,告知webpack在编译时, 哪里文件可以安全的删除掉;
- webpack5中,也提供了对部分CommonJS的tree shaking的支持;github.com/webpack/cha…
webpack中使用Tree Shaking
- webpack实现Tree Shaking 采用了两种不同的方案:
-
usedExports: 通过表计某些函数是否被使用,之后通过Terser来进行优化;
-
sideEffects:跳过整个模块/文件,直接查看该文件是否含有副作用.
-
usedExports
修改配置文件 --- webpack.prod.js:
module.exports = {
mode: 'development', //为了方便演示需要改为开发环境
devtool:"source-map",
externals: {
lodash: '_',
dayjs: 'dayjs',
},
optimization: {
usedExports: true,
minimize: false, //关闭minimize,防止进行terser优化
minimizer: [
new TerserPlugin({
extractComments: false, // 提取注释
parallel: true, // 多核
terserOptions: {
compress: {
//压缩
arguments: true,
dead_code: true,
},
mangle: true,
toplevel: true,
keep_classnames: true,
keep_fnames: true,
},
}),
],
},
plugins: [
// 生成环境
new CleanWebpackPlugin({}),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:6].css',
}),
new CssMinimizerPlugin(),
//new webpack.optimize.ModuleConcatenationPlugin(), 疑问点
],
};
测试代码:
math.js:
export function sum(num1, num2) {
return num1 + num2;
}
export function mul(num1, num2) {
return num1 - num2;
}
main.js
import { sum } from './math';
sum(1, 2);
打包(webpack --config ./config/webpack.common.js --env production )完成后:
可以看到注释中明确的标记了某个函数没有使用**(usedExports作用)**, 此时打开 minimize:true(开启 terser)
未出现关于 mul 任何代码。
上述配置文件中提到,开启作用域提升ModuleConcatenationPlugin的情况下,我多次测试不会出现 unused harmony export mul 相关信息.
开启ModuleConcatenationPlugin,打包信息如下首先开启usedExports:true , minimize:false
都为true 时:
对此有一些疑惑,不知道如何进行判断。希望评论区有前辈们的回复...
因此 usedExports必须结合Terser使用,将未使用的函数从代码中删除, 默认在production模式中是开启的
sideEffects
添加测试文件formate.js:
export const dataFormate = (data, type) => {
console.log(data);
}
在main.js中引入
import { sum } from './math';
import './formate.js';
sum(1, 2);
在package.json中添加配置:
{
"name": "thunder",
"version": "1.0.0",
"description": "",
"sideEffects": false, //添加sideEffects:false
"main": "index.js",
"scripts": {
"build": "webpack --config ./config/webpack.common.js --env production",
"watch": "webpack --watch",
"serve": "webpack serve --config ./config/webpack.dev.js --env development"
},
}
npm run build 后(关闭terser)
sideEffects:true时 两者对比
后者依然加载了formate函数
sideEffects 还可以改写为数组配置如下:
{
"sideEffects":[
"./src/format.js",
"**.css"
]
}
再次打包时会忽略数组中的文件,保留其副作用.
需要注意的是: 如果直接将值改为false,css文件将不会打包,为了解决这个问题需要在config添加配置:
{
test: /\.css/i,
use: [
isProduction ? MiniCssExtractPlugin.loader: "style-loader",
"css-loader"],
sideEffects: true, //保留副作用的模块 ,react脚手架
},
总结在生产环境中配置tree shaking.
在optimization中配置usedExports为true,来帮助Terser进行优化;
在package.json中配置sideEffects,直接对模块进行优化. 需要在module中添加sideEffects:true
CSS TreeShaking
安装: npm install purgecss-webpack-plugin glob -D
webpack.prod.js文件:
...
const PurgeCssPlugin = require('purgecss-webpack-plugin');
const glob = require('glob');
const resolveApp = require('./paths');
...
module.exports = {
...
plugins:[
...
new PurgeCssPlugin({
paths: glob.sync(`${resolveApp('./src')}/**/*`, { nodir: true }),//同步匹配文件夹中 的所有文件 nodir: true:不是文件夹,而是文件
safelist: function () {
return {
standard: ['body'],//那些标签不会被清除
};
},
}),
]
}
demo 测试:
main.js(入口文件):
import { sum } from './math';
import './formate.js';
import './style.css';
sum(1, 2);
style.css:
body {
width: 100%;
height: 100%;
}
需要注意的是首先需要将 standard: ['body'] 注释, 打包:
打包后的css文件为空, 此时我们需要加上standard属性, body对应的css才会保留.
demo测试2:
修改main.js:
import { sum } from './math';
import './formate.js';
import './style.css';
sum(1, 2);
const titleDiv = document.createElement('div');
titleDiv.className = 'title';
document.body.appendChild(titleDiv);
const h2El = document.createElement('h2');
document.body.appendChild(h2El);
修改style.css
body {
width: 100%;
height: 100%;
}
.title {
font-size: 20px;
color: #fff;
text-align: center;
line-height: 50px;
background-color: #f00;
}
h2 {
color:'#f00';
}
打包:
main.css:
/*!*****************************************************************!*\
!*** css ./node_modules/css-loader/dist/cjs.js!./src/style.css ***!
\*****************************************************************/body{height:100%;width:100%}
疑惑(待讨论): 查阅相关资料有些coder得到的结果,动态创建El,并且css 文件中含有此属性,safelist属性 并未包含此css, 打包后的文件仍然有相关的样式. 经过多次测试发现只有safelist属性含有此属性才会保留. 再次我并没有花费太长的时间,插件的作用就是对css TreeShaking,但是我们会将css提供很多base样式, 因此我更相信前者.
HTTP压缩
定义:HTTP压缩是一种内置在 服务器 和 客户端 之间的,以改进传输速度和带宽利用率的方式,
压缩流程:
- 第一步: HTTP数据在服务器发送前就已经被压缩了;(可以在webpack中完成)
- 第二步:兼容的浏览器在向服务器发送请求时,会告知服务器自己支持哪些压缩格式;
- 第三步:服务器在浏览器支持的压缩格式下,直接返回对应的压缩后的文件,并且在响应头中告知浏览器;
webpack 对文件压缩
安装插件: npm install compression-webpack-plugin -D
配置webpack.config.prod.js
const compressPlugin = require('compression-webpack-plugin');
module.exports = {
...
plugins:[
...
new compressPlugin({ // gzip压缩
threshold:0, //文件大小大于0B的时候才会被压缩,受minRation压缩比例的限制
test:/\.js$|\.css$|\.html$/, //压缩js,css,html
minRatio:0.8, //压缩比例 默认 0.8
algorithm: 'gzip', //压缩算法 默认 gzip
})
]
}
npm run build 打包:
HTML 文件中代码中的压缩
安装插件:npm install html-webpack-plugin --save -dev
文件配置 webpack.config.common.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
...
plugins: [
new HtmlWebpackPlugin({
template: "./index.html",
inject:true,//注入相关js css 文件 可选择属性: false(不注入) / head(头部) / body(body标签)
catche: true,//开启页面缓存,页面不发生改变时, 使用之前的缓存
// minify: true // 默认为false, 开启(true)后会对html进行压缩
minify:isProduction?{
//是否移除注释
removeComments:false,
removeRedundantAttributes:true,//是否移除多余的属性: 添加input标签添加属性type="text"(默认属性)打包后会将此属性删除
removeEmptyAttributes:true,//是否移除空属性
collapseWhitespace:true, //是否折叠空白
removeStyleLinkTypeAttributes:true,//是否移除link标签中的type属性
minifyCSS:true,//是否压缩style标签中的css
// minifyJS:true,//是否压缩script标签中的js
minifyJS: { //丑化标签中的js
mangle: {
toplevel: true,
}
}
}:false// 默认为false, 开启(true)后会对html进行压缩
}),
...
],
}
更多配置详细见官网 , 基本配置见代码注释, 此配置是对打包后的index.html 进行压缩,截图略
InlineChunkHtmlPlugin
辅助将一些chunk出来的模块,内联到html中,减少不必要的请求
安装:npm install react-dev-utils -D
在webpack.config.js中进行配置:
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
plugins: {
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime.*\.js/]),
}
}
需要在common.js中配置:
optimization:{ runtimeChunk: { name: function(entrypoint) { return `runtime` } } }
打包:
webpack自定义npm包(简述)
测试文件:
webpack.config.js:
const path = require('path');
module.exports = {
mode:'development',
entry: './index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
//AMD / CommonJS
libraryTarget: 'umd',
library:"myLibrary",
globalObject: 'this'
},
};
入口index.js
import * as math from './lib/math';
import * as format from './lib/formate';
export {
math,
format,
}
lib- formate、math
//formate
export function dataFormate() {
return 'dataFormate';
}
//math
export function sum(num1, num2) {
return num1 + num2;
}
export function mul(num1, num2) {
return num1 + num2;
}
目录结构:
npm init- npm login
- npm publish
webpack的打包分析
插件时间分析
npm install --save-dev speed-measure-webpack-plugin
webpack.common.js:
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = function(env) {
const isProduction = env.production;
process.env.NODE_ENV = isProduction ? "production" : "development";
const config = isProduction ? prodConfig : devConfig;
const mergeConfig = merge(commonConfig(isProduction), config);
return smp.wrap(mergeConfig);
};
特别注意的是,此插件已经很久不维护了,目前支持到webpack4.
打包过程中遇到的相关问题请到github,查找相关的解决办法.
打包文件分析
配置:script命令 package.json:
"scripts": {
...
"stats": "webpack --config ./config/webpack.common.js --env production --profile --json=stats.json"
},
npm run stats:
在根目录中会生成stats.json 文件
assets": [
{
"type": "asset",
"name": "js/runtime.c518c4.bundle.js",
"size": 5102,
"emitted": true,
"comparedForEmit": false,
"cached": false,
"info": {
"immutable": true,
"chunkhash": "c518c4",
"javascriptModule": false,
"related": {
"sourceMap": "js/runtime.c518c4.bundle.js.map",
"gzipped": "js/runtime.c518c4.bundle.js.gz"
},
"size": 5102
}
...
]
...
其次 还可以去专门的网站上进行分析
上传state.json文件得
插件分析
安装:npm install webpack-bundle-analyzer -D
配置(webpack.prod.js)
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
plugins: [
...
new BundleAnalyzerPlugin()
],
}
打包完毕后会启动Analyzer服务.
webpack源码解析(重中之重)
webpack-cli的启动流程
执行webpack相关命令时,首先来到node_modules下的webpack>bin>webpack.js
首先定义了cli对象,该对象installed属性执行了isInstalled属性,来判断node_modules是否包含webpack-cli. If函数判断是否安装,如果没有安装会有相对的提示信息. 相反会执行runCli函数,该函数require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));这一段代码实际上找到node_modules>webpack-cli>package.json中
"bin": {
"webpack-cli": "bin/cli.js"
},
并且导入cli.js,执行真正的runCLI(process.argv);
回头看runClick的代码const runCLI = require("../lib/bootstrap");.
创建了cli实例,执行了run方法, 继续深入 lib>webpack-cli.js, 找到代码929(webpack:5.65.0)行,run函数: 1. 初始化内置/外置命令 2.loadCommandByName函数中调用makeCommand()函数完成相关命令和启动的操作. 3. runWebpack 和 createCompiler 完整最终的Command合并
流程图如下:
webpack源码解析
主要会提及到两个问题: 1. plugins是什么时候注册的,什么时候运行的? 2. compiler和compilation 的区别
webpack源码 => lib => webpack.js:
webpack函数接受options和callback两个函数
create(){
...
if(Array.isArray(options)) {
compiler = createMultiCompiler(
options
);
...
}else {
...
compiler = createCompiler(webpackOptions); //核心
}
return { compiler, watch, watchOptions };
}
if(callback) {
...
compiler.run(() =>{})
return compiler;
}else {
return compiler;
}
create函数中核心是创建compiler , createCompiler函数(65行左右)主要做了以下几件事:
- 创建compiler对象
- 对nodeJS中的fs模块封装,同时挂载到compiler对象下
- 注册所有Plugins插件(注册不代表执行)
- 调用钩子函数 environment 和 afterEnvironment的call()
- 处理config文件中的plugins和rules
代码:
const createCompiler = rawOptions => {
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
const compiler = new Compiler(options.context, options);
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
//注册所有插件
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
applyWebpackOptionsDefaults(options);
//调用钩子函数 environment 和 afterEnvironment的call()
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
//process用于处理config文件中的plugins和rules
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
return compiler;
};
WebpackOptionsApply类 主要是将传入的属性转成webpack的login注入到webpack的生命周期中 将内置的plugin进行导入(所有plugin实事上贯穿webpack的整个构建流程)
回头看compiler 类,该类定义了webpack生命周期的所有hooks, 每一hook都可以注册(tap)任意事件,当进行编译的到某个生命周期, 会调用call方法调用已经注册的时间,因此Plugins可以在任何阶段进行调用,取决于在那一阶段注册了事件, 我们主要看run方法:
run(callback) {
const finalCallback = (err,stats) =>{}
const onComiled = (err, compilation) => {}
const run = () => {... this.compile(onCompiled);}
if (this.idle) {
this.cache.endIdle(err => {
if (err) return finalCallback(err);
this.idle = false;
run();
});
} else {
run();
}
}
内部的run函数执行 compile 方法开始编译
compile(callback) {
this.hooks.beforeCompile.callAsync(params, err => {
...
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
const logger = compilation.getLogger("webpack.Compiler");
..
//make的call
this.hooks.make.callAsync(compilation, err => {
...
this.hooks.finishMake.callAsync(compilation, err => {
...
});
});
});
}
compile 在webpack构建的时候所有阶段compiler都是一个全局存在的对象,(before - run - beforeCompiler - compile - make - finishMake - afterCompiler - done),只要是webpack的编译,都会先创建一个compiler
compilation**是到准备编译模块(比如main.js)才会创建compilation对象,主要存在于compile - make(之前)阶段
比如: watch -> 源代码发生改变就需要重新编译模块comPiler 可以继续使用,如果修改了webpack的配置, 那么需要重新执行npm run build*, Compilation 需要创建一个新的compilation对象*
那么什么时候编译的相关compilation呢?
结论: 模块编译发生在make阶段:hooks.make.callAsync 。Webpack.js : new WebpackOptionsApply().process(options, compiler)找到WebpackOptionsApply.js(335行左右)入口处理 new EntryOptionPlugin().apply(compiler); 来到EntryOptionPlugin.js文件:静态方法applyEntryOption:
static applyEntryOption(compiler, context, entry) {
console.log(context) //E:\mydata\webpack\webpack-5.72.0\itc
if (typeof entry === "function") {
const DynamicEntryPlugin = require("./DynamicEntryPlugin");
new DynamicEntryPlugin(context, entry).apply(compiler);
} else {
const EntryPlugin = require("./EntryPlugin");
console.log(37, entry); // { main: { import: [ './src/main.js' ] } }
for (const name of Object.keys(entry)) {
const desc = entry[name];
const options = EntryOptionPlugin.entryDescriptionToOptions(
compiler,
name,
desc
);
for (const entry of desc.import) {
new EntryPlugin(context, entry, options).apply(compiler);
}
}
}
}
entry参数就是指配置中的入口文件 , 我们重点关注EntryPlugin.js: new EntryPlugin()
class EntryPlugin {
constructor(context, entry, options) {
this.context = context;
this.entry = entry;
this.options = options || "";
}
apply(compiler) {
compiler.hooks.compilation.tap(
"EntryPlugin",
(compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(
EntryDependency,
normalModuleFactory
);
}
);
const { entry, options, context } = this;
const dep = EntryPlugin.createDependency(entry, options);
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => { //注册make hooks
compilation.addEntry(context, dep, options, err => {
callback(err);
});
});
...
}
我们终于找到make.tapAsync, 因此当开始编译执行到make生命周期时会将所有的模块进行编译
接下来我们分析Compilation.js:Compilation中的addEntry :
addEntry(context, entry, optionsOrName, callback) {
const options =
typeof optionsOrName === "object"
? optionsOrName
: { name: optionsOrName };
// context : E:\mydata\webpack\webpack-5.72.0\itc 根目录
// entry : EntryDependency {_parentModule: undefined,_parentDependenciesBlock: undefined,_parentDependenciesBlockIndex: -1,weak: false,optional: false, ..._loc: { name: 'main' },request: './src/main.js',userRequest: './src/main.js',range: undefined,assertions: undefined} 入口文件
//options : {name: 'main',filename: undefined,runtime: undefined,layer: undefined,dependOn: undefined,baseUri: undefined,publicPath: undefined,chunkLoading: undefined,asyncChunks: undefined,wasmLoading: undefined,library: undefined} 相关配置
this._addEntryItem(context, entry, "dependencies", options, callback);
}
进入当前文件下的 _addEntryItem 方法:
_addEntryItem(context, entry, target, options, callback) {
const { name } = options;
let entryData =
name !== undefined ? this.entries.get(name) : this.globalEntry;
...
this.hooks.addEntry.call(entry, options);
this.addModuleTree(
{
context,
dependency: entry,
contextInfo: entryData.options.layer
? { issuerLayer: entryData.options.layer }
: undefined
},
(err, module) => {
if (err) {
this.hooks.failedEntry.call(entry, options, err);
return callback(err);
}
this.hooks.succeedEntry.call(entry, options, module);
return callback(null, module);
}
);
}
最终的目的是将所有的模块组合成一个模块树.
addModuleTree({ context, dependency, contextInfo }, callback) {
...
this.handleModuleCreation(
{
factory: moduleFactory,
dependencies: [dependency],
originModule: null,
contextInfo,
context
},
(err, result) => {
if (err && this.bail) {
callback(err);
this.buildQueue.stop();
this.rebuildQueue.stop();
this.processDependenciesQueue.stop();
this.factorizeQueue.stop();
} else if (!err && result) {
callback(null, result);
} else {
callback();
}
}
);
}
所有模块最终的结构是转换成模块图, factorizeModule将所有的相关方法添加到队列中,执行addModule,将所有的模块全部添加到模块队列中, 当监听到hook的时候在处理对应的模块
handleModuleCreation(
{
factory,
dependencies,
originModule,
contextInfo,
context,
recursive = true,
connectOrigin = recursive
},
callback
) {
const moduleGraph = this.moduleGraph;
const currentProfile = this.profile ? new ModuleProfile() : undefined;
this.factorizeModule(
{
currentProfile,
factory,
dependencies,
factoryResult: true,
originModule,
contextInfo,
context
},
(err, factoryResult) => {
...
this.addModule(){
this._handleModuleBuildAndDependencies();
}
}
);
}
最终调用**_handleModuleBuildAndDependencies()**开始真正的构建(buildModule),将所有的构建模块添加到buildquene队列中:
_handleModuleBuildAndDependencies(originModule, module, recursive, callback) {
...
/**
* buildModule(module, callback) {this.buildQueue.add(module, callback);}
*/
this.buildModule(module, err => {
if (creatingModuleDuringBuildSet !== undefined) {
creatingModuleDuringBuildSet.delete(module);
}
if (err) {
if (!err.module) {
err.module = module;
}
this.errors.push(err);
return callback(err);
}
...
this.processModuleDependencies(module, err => {
if (err) {
return callback(err);
}
callback(null, module);
});
});
}
每次在buildQuene.add添加模块的时候通过_ensureProcessing函数监测处理进度,然后通过this.hooks.added.call(item);将需要打包的文件添加到队列中,最终会调用 _buildModule, 最终执行module.build开始构建,最终通过hooks.succeedModule生命周期构建完成
module.build 中的build方法继承自NormalModule.js下的build
以上就是compilation的编译过程
对于以上的解析有很大的不足. 对于webpack的源码,咨询很多的开发者,很多人回复是不难. 强烈建议下载一份源代码按照我上面的步骤去阅读. 后续我会继续更新,添加更加详细的注解
相关截图(来源 coderwhy)
自定义loader
loader是干啥的?它是用于对模块的源代码进行转换(处理),比如css-loader、styleloader、babel-loader等
搭建webpack基础环境, 创建loaders文件夹创建tc-loader01.js:
module.exports = function (content, sourceMap) {
console.log(content,'这是我的loader'); //content:值函数的上下文 console.log('hello main.js'); 这是我的loader
return content;
};
创建webpack.config.js:
const path = require('path');
module.exports = {
entry: './main.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, './build'),
},
module: {
rules: [
{
test: /\.js$/,
use: 'tc-loader01.js',
},
],
},
resolveLoader: {
//默认配置为 node_modules. 数组的含义: 如果node_modules中没有找到就去下一个路径去找
modules: ['node_modules', './loaders'],
},
};
pitch-loader
为了方便测试 , 创建 tc-loader02.js , tc-loader03.js
修改webpack.config.js
rules: [
{
test: /\.js$/,
use: ['tc-loader01.js','tc-loader02.js','tc-loader03.js']
},
],
npm run build 打印得:
pitch
pitch02
pitch03
console.log('hello main.js'); 这是我的loader03
console.log('hello main.js'); 这是我的loader02
console.log('hello main.js'); 这是我的loader
对于打印的顺序可以来到源码:
loader-runner库: 比较熟悉的runLoaders方法,实际上会执行iteratePitchingLoaders函数,该函数就是遍历Pitchloaders其中会对loaderContext.loaderIndex++,因此优先打印pitch. 最后调用loadLoader函数方法,其中执行loaderContext.loaderIndex--; iterateNormalLoaders(options, loaderContext, args, callback);倒序打印normalLoader
那么如何更改loader的执行顺序呢?
答案:设置enforce
enforce
enforce一共有四种方式:
- 默认所有的loader都是normal;
- 在行内设置的loader是inline(在前面将css加载时讲过,import 'loader1!loader2!./test.js');
- 也可以通过enforce设置 pre(前置) 和 post(后置);
在Pitching和Normal它们的执行顺序分别是:
- post, inline, normal, pre;
- pre, normal, inline, post;
同步loader和异步loader
//异步loader
module.exports = function(content) {
console.log(content,'这是我的async loader');
const callback = this.async();
setTimeout(() => {
callback(null,content)
},2000)
}
//同步loaders
module.exports = function (content, sourceMap) {
console.log(content,'这是我的loader');
//同步的loader, 两种方法返回数据
// 1. return content;
this.callback(null, content, sourceMap); // 2.
};
module.exports.pitch = function() {
console.log('pitch');
}
loader参数处理
修改webpack.config.js:
module: {
rules: [
{
test: /\.js$/i,
use:{
loader: 'tc-loader01.js',
options:{
name:'itc',
age:18
}
}
},
],
},
tc-loader01.js:
module.exports = function (content) {
console.log(content, '这是我的async loader');
//传入的参数
const options = this.getOptions();
console.log(options, 'options'); //{ name: 'itc', age: 18 } options
const callback = this.async();
setTimeout(() => {
callback(null, content);
}, 2000);
};
参数校验
安装依赖schema-utils: npm install schema-utils -D
相关参数同上
添加配置文件loader01:
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the user"
},
"age": {
"type": "number",
"description": "The age of the user"
}
},
//附加其他参数
"additionalProperties": true
}
修改tc-loader01.js:
const schema = require('../tc-schema/loader01.json')
//异步loader
module.exports = function (content) {
console.log(content, '这是我的async loader');
//传入的参数
const options = this.getOptions();
validate(schema,options,{
name:'loader01',
})
const callback = this.async();
setTimeout(() => {
callback(null, content);
}, 2000);
};
如果填入的类型与type不符, 报错信息会以description为准
自定义babel-loader
安装依赖: npm install @babel/core @babel/preset-env -D
添加tcbabel-loader.js文件
const babel = require('@babel/core');
module.exports = function (content) {
//设置为异步的loader
const callback = this.async();
//获取传入的参数
const options = this.getOptions();
//对源代码进行转换
babel.transform(content,options,(err,result)=>{
if (err) {
callback(err)
}else {
callback(null,result.code)
}
})
}
修改webpack.config.js 配置:
const path = require('path');
module.exports = {
entry: './main.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, './build'),
},
module: {
rules: [
{
test: /\.js$/i,
use: {
loader: 'tcbabel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
],
},
resolveLoader: {
//默认配置为 node_modules
modules: ['node_modules', './loaders'],
},
};
自定义md.loader
安装依赖:npm install marked
创建doc.md文件:
# 学习webpack
```js
console.log("Hello Loader");
const message = "Hello World";
console.log(message);
const foo = () => {
console.log("foo");
}
foo();
```
创建文件tcmd-loader.js:
const marked = require('marked')
module.exports = function(content) {
const htmlContent = marked.parse(content)
return htmlContent;
}
配置webpack.config.js:
...
{
test: /\.md$/i,
use: [
"tcmd-loader"
]
},
...
在main.js中引入md文件, npm run build之后会发现报错, 提示You may need an additional loader to handle the result of these loaders.
这时我们需要安装npm install html-loader -D ,修改配置文件:
use: [
"html-loader",
"tcmd-loader"
]
npm run build 打包成功
我们可以将html添加到body中, 修改main.js
import code from './doc.md'
let a = 10
console.log('hello main.js');
document.body.innerHTML = code
在次打包运行html:
那么如何将code 高亮呢?
安装依赖:npm install css-loader style-loader highlight.js
修改tcmd-loader.js:
const marked = require('marked')
const hljs = require('highlight.js')
module.exports = function(content) {
marked.setOptions({
highlight: function(code, lang, callback) {
return hljs.highlightAuto(code).value
}
});
const htmlContent = marked.parse(content)
//如果不是用html-loader, 手动转换需要如下配置:
const innerContent = "`"+ htmlContent + "`";
const moduleCode = `var code=${innerContent}; export default code;`;
return moduleCode;
}
添加code样式main.js:
...
import "highlight.js/styles/default.css";
...
修改webpack.config.js 添加loader:
{
test: /\.css$/i,
use: [
'style-loader',
'css-loader',
]
}
npm run build :
自定义Plugins
认识tapable
webpack中有两个重要的类Compiler和Compilation, 他们通过注入的方式来监听webpack的生命周期, 插件的注入离不开Hook, 其中webpack是创建了Tapable库中各种的Hook实例.
安装:npm install tapable -D
同步Hook(测试代码):
const { SyncHook, SyncBailHook, SyncLoopHook,SyncWaterfallHook } = require('tapable');
class TCLearnTabable {
constructor() {
this.counter = 0;
this.hooks = {
//syncHook: new SyncHook(['name', 'age']),
//bail: 在某一个事件监听函数中, 如果有返回值,那么后续监听的事件就不会执行了
// syncHook: new SyncBailHook(['name', 'age']),
//loop: 在某个事件监听函数中,如果返回值为true,那么这个回调函数会一直循环执行,返回undefined就停止执行
// syncHook: new SyncLoopHook(['name', 'age']),
//waterfall:第一次监听的函数返回值,会作为下一个监听函数的第一个参数
syncHook: new SyncWaterfallHook(['name', 'age']),
};
this.hooks.syncHook.tap('event1', (name, age) => {
if (this.counter++ < 3) {
console.log('event1', name, age);
return 'event1';
}
});
this.hooks.syncHook.tap('event2', (name, age) => {
console.log('event2', name, age);
});
}
emit() {
this.hooks.syncHook.call('why', 'age');
}
}
const lt = new TCLearnTabable();
lt.emit();
详细说明建议查看github文档
异步Hook(测试代码):
const {
AsyncSeriesHook,
AsyncParallelHook
} = require('tapable');
class TCLearnTabable {
constructor() {
this.counter = 0;
this.hooks = {
//series:在一个hook中,监听了两次事件(两个回调函数),这两个回调是串行执行
// asyncHook: new AsyncSeriesHook(['name', 'age']),
//parallel:子啊一个hook中,监听了多个事件, 是并行执行订单
asyncHook: new AsyncParallelHook(['name', 'age']),
};
this.hooks.asyncHook.tapAsync('event1',(name,age,callBack) => {
setTimeout(() => {
console.log('event1', name, age);
callBack();
},2000)
})
this.hooks.asyncHook.tapAsync('event2',(name,age,callBack) => {
setTimeout(() => {
console.log('event2', name, age);
callBack();
},2000)
})
this.hooks.asyncHook.tapPromise("eventPromise",(name,age) => {
return new Promise((resolve,reject) => {
setTimeout(() => {
console.log('eventPromise', name, age);
resolve();
},2000)
})
})
}
emit() {
this.hooks.asyncHook.promise("thunder",30).then((result) => {
console.log("第二次程序执行完成");
})
}
}
const lt = new TCLearnTabable();
lt.emit();
/*并行
event1 thunder 30
event2 thunder 30
eventPromise thunder 30
第二次程序执行完成
*/
自定义plugin
插件- 打包后自动上传
const { NodeSSH } = require('node-ssh');
class AutoUploadPlugin {
constructor(options) {
this.ssh = new NodeSSH();
this.options = options;
}
apply(compiler) {
compiler.hooks.afterEmit.tapAsync(
'AutoUpLoadPlugin',
async (compilation, callback) => {
console.log('自动上传文件');
//1.获取输出的文件夹
const outputPath = compilation.outputOptions.path;
console.log(outputPath);
//2.链接服务器ssh
await this.connectServer();
//3.删除文件中的内容
const serverDir = this.options.remotePath;
await this.ssh.execCommand(`rm -rf ${serverDir}/*`);
//4.上传文件到服务器(ssh链接)
await this.uploadFiles(outputPath, serverDir);
//5.关闭ssh链接
this.ssh.dispose();
callback();
},
);
}
async connectServer() {
try {
await this.ssh.connect({
host: this.options.host,
username: this.options.username,
password: this.options.password,
});
console.log('ssh链接成功');
} catch (err) {
console.log(err);
}
}
async uploadFiles(localPath, remotePath) {
const status = await this.ssh.putDirectory(localPath, remotePath, {
recursive: true, //递归上传
concurrency: 10, //并发上传
});
console.log('传送到服务器', status ? '成功' : '失败');
}
}
module.exports = AutoUploadPlugin;
React_App
创建react项目:npx create-react-app test_app
显示配置文件:npm run eject
待补充
Vue_App
待补充
Gulp(凉了)
什么是Gulp?
gulp 也是一种自动化构建工具, 他是基于文件Stream的构建流 ,将文件压缩压缩、整合、移动.
gulp可以定义一系列任务,等待任务被执行 . 而 webpack是一个模块化打包工具,可以使用各种各样的loader加载不同的模块,还可以使用各种plugins插件在webpack的打包生命周期中完成其他任务. gulp相对webpack 更加简单、易用,更适合编写自动化的任务.当然最重要的差别是gulp不支持模块化.
基本使用(单任务执行)
-
安装
npm install gulp -g -
创建gulpfile.js,创建一个任务:
const { task } = require('gulp'); //定义任务 const foo = (cb) => { console.log('foo'); cb(); }; //gulp 4版本以前的写法 task('bar', (cb) => { console.log('bar'); cb(); }); module.exports = { foo, }; //默认任务 npx gulp module.exports.default = (cb) => { console.log("default, task"); cb() }; -
运行:
npx gulp foo -
结果:
多任务执行
代码:
const {series,parallel} = require('gulp');
const task1 = (cb) => {
setTimeout(() => {
console.log(1);
cb();
}, 2000);
};
const task2 = (cb) => {
setTimeout(() => {
console.log(2);
cb();
}, 2000);
};
const task3 = (cb) => {
setTimeout(() => {
console.log(3);
cb();
}, 2000);
};
const seriesTasks = series(task1,task2,task3)//串行执行
const parallelTask = parallel(task1,task2,task3)//并行执行
const composeTask = series(parallelTask,seriesTasks)
module.exports = {
seriesTasks,parallelTask,composeTask
}
打印结果
console.log(seriesTasks)
console.log(parallelTask)
console.log(composeTask)
Gulp的文件匹配和监听
创建相关文件,编写代码:
/ => src => main.js
gulpfile.js
安装相关依赖:
npm install @babel/core @babel/preset-env gulp-babel gulp-terser -D
编写代码
gulpfile.js:
const { src, dest,watch } = require('gulp');
const babel = require('gulp-babel');
const terser = require('gulp-terser');
const jsTask = () => {
// src('./src/main.js')
return src('./src/*.js')
.pipe(babel({ presets: ['@babel/preset-env'] }))
.pipe(terser({ mangle: { toplevel: true } }))
.pipe(dest('./dist'));
};
watch("./src/**/*.js", jsTask);
module.exports = {
jsTask,
};
gulp-babel: ES6 => ES5
gulp-terser: 丑化及压缩
watch:监听文件变化,同步更新
需要注意的是,watch可以监听多个任务,用上一届提到的API替换即可
运行
npx gulp jsTask
案例练习
略
Rollup
Rollup是一个JavaScript的模块化打包工具,可以帮助我们编译小的代码到一个大的、复杂的代码中,比如一个库或者一个应用程序;
Rollup也是一个模块化的打包工具,但是Rollup主要是针对ES Module进行打包的;
vue、react、dayjs源码本身都是基于rollup的
打包命令: (-f : 文件格式, -o : 输出位置)
- commonJS :
npx rollup ./src/main.js -f cjs -o dist/bundle.js - iife(立即执行函数):
npx rollup ./src/main.js -f iife -o dist/bundle.browser.js - amd:
npx rollup ./src/main.js -f iife -o dist/bundle.amd.js - umd :
npx rollup ./src/main.js -f umd --name uitils -o dist/bundle.js
基本使用
安装:npm install rollup -D
main.js:
const message = 'Hello World';
console.log(message);
export const abc = () => {
console.log(66767);
};
运行打包命令即可
配置文件说明
修改package.json文件:
"scripts": {
...
"build": "rollup -c"
},
安装相关依赖
npm install @babel/core @babel/preset-env @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-node-resolve rollup rollup-plugin-terser rollup-plugin-postcss rollup-plugin-vue vue-template-compiler rollup-plugin-serve rollup-plugin-livereload rollup-plugin-replace -D
npm install lodash sass vue (测试)
基础配置:
新建src => main.js:
import { dateFormat } from './utils/formate.js';
import _ from 'lodash';
const message = 'Hello World';
console.log(message);
console.log(dateFormat());
console.log(_.join(["abc","cba"]));
export const abc = () => {
console.log(66767);
};
新建 utils => formate.js:
export const dateFormat = () => {
return 1111
}
基础配置文件rollup.config.js:
module.exports = {
input: 'src/main.js',
output:[{ //多文件输出
format:"umd",
name:"utils",
file:"dist/itc.util.js"
},
{
format:'cjs',
file:"dist/itc.util.cjs.js"
},
{
format:'es',
file:"dist/itc.util.es.js"
},
{
format:"amd",
file:"dist/itc.util.amd.js"
},
{
format:"iife",
name:"itc",
file:"dist/itc.util.iife.js"
}
]
}
新建index.html 运行, 引入itc.util.js文件,运行提示为 Cannot read properties of undefined (reading 'join'),这时我们需要引入lodashJS库
<body>
<script src="./node_modules/lodash/lodash.js"></script>
<script src="./dist/itc.util.js"></script>
<script>
console.log(utils.abc());
</script>
</body>
其他配置:
因为上述我们引入了babel所以新建babel.config.js:
module.exports = {
presets: ['@babel/preset-env'],
};
新建 styles => style.css
body {
background-color: red;
}
新建vue => app.vue
<template>
<div>
wo shi App
</div>
</template>
<script>
export default {
name:"app"
}
</script>
<style scoped>
</style>
更新main.js
import { dateFormat } from './utils/formate.js';
import _ from 'lodash';
import './css/style.css';
import Vue from 'vue';
import VueApp from './vue/app.vue';
new Vue({
render: (h) => h(VueApp),
}).$mount('app');
const message = 'Hello World';
console.log(message);
console.log(dateFormat());
console.log(_.join(['abc', 'cba']));
export const abc = () => {
console.log(66767);
};
external中不包含vue, 会出现
process is not defined为了解决这个问题需要安装:npm install rollup-plugin-replace -D更新配置文件replace({'process.env.NODE_ENV':JSON.stringify('production')}),传递环境变量
配置serve: 打包完成之后,自动打开浏览器,详情见配置文件
配置文件监听:两种方式:一种是在package.json中配置"rollup -c -w" 另一种是配置在config.js中配置watch 详情见配置文件
配置livereload:配置实时刷新(修改文件之后刷新页面)
更新rollup.config.js:
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import {terser} from 'rollup-plugin-terser';
import postcss from 'rollup-plugin-postcss';
import vue from 'rollup-plugin-vue';
import replace from 'rollup-plugin-replace';
import serve from 'rollup-plugin-serve';
import livereload from 'rollup-plugin-livereload';
export default {
input: 'src/main.js',
output: {
format: 'umd',
name: 'utils',
file: 'dist/itc.util.js',
globals: { //在html文件中必须引入对应的JS文件
lodash: '_',
vue: 'Vue',
},
},
watch:{
include: 'src/**',
exclude: 'node_modules/**'
},
external: ['lodash','vue'], //排除node_modules中的模块 这个非常重要,如果不排除,打包后的JS会包含相关依赖
plugins: [
commonjs(), //解决某个插件中使用了 CJS 导出的模块
resolve(), //引用node_modules值的模块
replace({
"process.env.NODE_ENV": JSON.stringify("production")
}),
babel({
babelHelpers: 'bundled',//辅助函数
exclude: 'node-modules/**',
}), //转译es6语法
postcss(), //转译css
vue(), //转译vue
terser(), //压缩
serve({
open: true,//自动打开浏览器
port:8080,//端口
contentBase: '.',//服务器根目录,哪一个文件夹
}),
livereload(),//实时刷新
],
};
环境分离
修改package.json:
"build": "rollup -c --environment NODE_ENV:production",
"serve": "rollup -c --environment NODE_ENV:development"
修改配置文件
...
const isProduction = process.env.NODE_ENV === 'production';
const plugins = [
commonjs(), //解决某个插件中使用了 CJS 导出的模块
resolve(), //引用node_modules值的模块
replace({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
}),
babel({
babelHelpers: 'bundled', //辅助函数
exclude: 'node-modules/**',
}), //转译es6语法
postcss(), //转译css
vue(), //转译vue
];
if (isProduction) {
plugins.push(terser()); //压缩
} else {
const devPlugins = [
serve({
open: true, //自动打开浏览器
port: 8080, //端口
contentBase: '.', //服务器根目录,哪一个文件夹
}),
livereload(), //实时刷新
];
plugins.push(...devPlugins);
}
export default {
...
plugins
};
Vite
略