绝大部分生产项目都是基于
cli
脚手架创建一个比较完善的项目,从早期的webpack
配置工程师到后面的无需配置,大大解放了前端工程建设。但是时常会遇到,不依赖成熟的脚手架,从零搭过项目吗,有遇到哪些问题吗?或者有了解loader
和plugin
吗?如果只是使用脚手架,作为一个深耕业务一线的工具人,什么?还要自己搭?还要写loader
,这就过分了。
正文开始...
前置
我们先了解下webpack
能干什么
webpack
是一个静态打包工具,根据入口文件构建一个依赖图,根据需要的模块组合成一个bundle.js
或者多个bundle.js
,用它来展示静态资源
关于webpack
的一些核心概念,主要有以下,参考官网
entry
1、entry
入口(依赖入口文件,webpack 首先根据这个文件去做内部模块的依赖关系)
// webpack.config.js
module.exports = {
entry: './src/app.js'
};
// or
/*
// 是以下这种方式的简写 定义一个别名main
module.exports = {
entry: {
main: ./src/app.js'
}
}
*/
也可以是一个数组
// webpack.config.js
module.exports = {
entry: ['./src/app.js', './src/b.js'],
vendor: './src/vendor.js'
};
在分离应用 app.js 与第三方包时,可以将第三方包单独打包成vender.js
,我们将第三方包打包成一个独立的chunk
,内容hash
值保持不变,这样浏览器利用缓存加载这些第三方js
,可以减少加载时间,提高网站的访问速度。
不过目前webpack4.0.0
已经不建议这么做,主要可以使用optimization.splitChunks
选项,将app
与vendor
会分成独立的文件,而不是在入口处创建独立的entry
output
2、output
输出(把依赖的文件输出一个指定的目录
下)
主要会根据entry
的入口文件名输出到指定的文件名目录中,默认会输出到dist
文件中
const path = require('path');
// webpack.config.js
module.exports = {
entry: {
app: './src/app.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js'
}
};
/*
module.exports = {
entry: './src/app.js',
output: {
filename: '[name].bundle.js'
}
}
*/
// 默认输出 /dist/app.bundle.js
module
3、module
配制loader
插件,loader
能让webpack
处理各种文件,并把文件转换为可依赖的模块,以及可以被添加到依赖图中。其中test
是匹配对应文件类型,use
是该文件类型用什么loader
转换,在打包前运行。
module.exports = {
module: {
rules: [
{
test: /\.less$/,
use: 'less-loader'
},
{
test: /\.ts$/,
use: 'ts-loader'
},
{
test: /\.css$/,
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader',
options: {
modules: true
}
},
{
loader: 'sass-loader'
}
]
}
]
}
};
plugins
4、plugins
主要是在整个运行时都会作用,打包优化,资源管理,注入环境
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })]
};
mode
5、mode
指定打包环境,development
与production
,默认是production
从零开始一个项目搭建
新建一个目录webpack-01
,执行npm init -y
npm init -y // 生成一个默认的package.json
在package.json
中配置scirpt
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
}
}
首先我们在在开发依赖安装webpack
与webpack-cli
,执行npm i webpack webpack-cli --save-dev
在webpack5
中我们默认新建一个webpack
的默认配置文件webpack.config.js
const path = require('path');
module.exports = {
entry: {
app: './src/app.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
libraryTarget: 'commonjs'
},
mode: 'production'
};
我们在src
目录下新建一个app.js
并写入一段js
代码
console.log('hello, webpack');
在终端执行npm run build
,这个命令我在package.json
的script
中配置
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"build:test_dev": "webpack --config webpack_test_dev_config.js",
"build:test_prd": "webpack --config webpack_test_prd_config.js",
"build:default": "webpack --config webpack.config.js",
"build:o": "webpack ./src/app.js -o dist/app.js"
},
此时就会生成一个在dist
文件,并且名字就是app.bundle.js
并且控制台上已经成功了
```js
> webpack
asset app.bundle.js 151 bytes [emitted] [minimized] (name: app)
./src/app.js 29 bytes [built] [code generated]
webpack 5.72.1 compiled successfully in 209 ms
我们打开一下生成的app.bundle.js
,我们发现是这样的,这是在model:production
下生成的一个匿名的自定义函数。
// app.bundle.js
(() => {
var e = {};
console.log(3), console.log('hello, webpack');
var o = exports;
for (var l in e) o[l] = e[l];
e.__esModule && Object.defineProperty(o, '__esModule', { value: !0 });
})();
这是生产环境输出的代码,就是在一个匿名函数中输出了结果,并且在{}
上绑定了一个__esModule
的对象属性,有这样一段代码var o = exports;
主要是因为我们在output
中新增了libraryTarget:commonjs
,这个会决定js
输出的结果。
我们再来看下如果mode:development
那么是怎么样
// 这是在mode: development下生成一个bundle.js
/*
* ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development").
* This devtool is neither made for production nor for readable output files.
* It uses "eval()" calls to create a separate source file in the browser devtools.
* If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/)
* or disable the default devtool with "devtool: false".
* If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/).
*/
/******/ (() => { // webpackBootstrap
/******/ var __webpack_modules__ = ({
/***/ "./src/app.js":
/*!********************!*\
!*** ./src/app.js ***!
\********************/
/***/ (() => {
eval("\nfunction twoSum(a, b) {\n return a+b\n}\nconst result = twoSum(1,2);\nconsole.log(result);\nconsole.log('hello, webpack');\n\n//# sourceURL=webpack://webpack-01/./src/app.js?");
/***/ })
/******/ });
/************************************************************************/
/******/
/******/ // startup
/******/ // Load entry module and return exports
/******/ // This entry module can't be inlined because the eval devtool is used.
/******/ var __webpack_exports__ = {};
/******/ __webpack_modules__["./src/app.js"]( ""./src/app.js"");
/******/
/******/ })()
;
这上面的代码就是运行mode:development
模式下生成的,简化一下就是
(() => {
var webpackModules = {
'./src/app.js': () => evel('app.js内部的代码')
};
weboackModules['./src/app.js']("'./src/app.js'");
})();
在开发环境就是会以文件路径为key
,然后通过evel
执行app.js
的内容,并且调用这个webpackModules
执行evel
函数
注意我们默认libraryTarget
如果不设置,那么就是var
,主要有以下几种amd
、commonjs2
,commonjs
,umd
通过以上,我们会发现我们可以用配置不同的命令执行打包不同的脚本,在默认情况下,npm run build
与执行npm run build:default
是等价的,我们会看到default
用--config webpack.config.js
指定了webpack
打包的环境的自定义配置文件。
如果配置默认文件名就是webpack.config.js
那么webpack
就会根据这个文件进行打包,webpack --config xxx.js
是指定自定义文件让webpack
根据xxx.js
输入与输出的文件进行一系列操作。
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"build:default": "webpack --config webpack.config.js",
},
除了以上,我们可以不使用配置webpack --config webpack.config.js
这个命令,而是直接在命令行-cli直接打包指定的文件输出到对应的文件下
"scripts": {
"build:o": "webpack ./src/app.js --output-path='./dist2' --output-filename='[name]_[hash].bundle.js'"
},
会创建dist2
目录并打包出来一个默认命名的main_ff7753e9dbb1e41a06a6.bundle.js
的文件
我们会发现我们配置了诸如webpack_test_dev_config.js
或者webpack_test_prd_config.js
z 这样的文件,通过build: test_dev
与build:test_prd
来区分,里面文件内容似乎大同小异,那么我可不可以复用一份文件,通过外面的环境参数来控制呢?这点在实际项目中会经常使用
环境参数
我们可以通过package.json
中指定的参数来确定,可以用--mode='xxx'
与--env a='xxx'
"scripts": {
"build2": "webpack --mode='production' --env libraryTarget='commonjs' --config webpack.config.js"
},
此时webpack.config.js
需要改成函数的方式 第二参数argv
能获取全部的配置的参数
// webpack.config.js
const path = require('path');
module.exports = function (env, argv) {
console.log(env, argv);
return {
entry: {
app: './src/app.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
library: 'MyTest',
libraryTarget: argv.libraryTarget
},
mode: argv.mode
};
};
因此我们就可以通过修改package.json
里面的变量,从而控制webpack.config.js
运行整个项目
我们已经创建了一个src/app.js
的入口文件,现在需要在浏览器上访问,因此需要构建一个index.html
,在根目录中新建public/index.html
,并且引入我刚打包的js
文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>hello-webpack</title>
</head>
<body>
<div id="app"></div>
<script src="../dist/app.bundle.js"></script>
</body>
</html>
终于大功告成,我打开浏览器,打开页面终于可以访问了,【我本地装了 live server】插件
但是,当我每次修改js
文件,我都要每次执行npm run build
这个命令,这就有些繁琐了,而且我本地是安装 vsode 插件的方式帮我打开页面的,这就有点坑了。
于是在webpack
中就有一个内置cli
watch 来监听文件的变化,我们只需要加上--watch
就可以了
"scripts": {
"build": "webpack --watch",
},
这种方式会一直监听文件的变化,当文件发生变化时,就会重新打包,页面会重新刷新。
当然还有一种方式,就是可以在webpack.config.js
中加入watch
// webpack.config.js
{
watch: true,
entry: {
app: './src/app.js'
},
}
然后我们就改回原来的,将--watch
去掉就行。
--watch
这种方式确实提升我本地开发效率,因为只要文件一发生变化,就会重新打包编译,结合vscode
的插件就会重新加载最新的文件,但是随着项目的庞大,那么这种效率就很低了,因此除了webpack
自身的 watch 方案,我们需要去了解另外一个方案webpack-dev-server
webpack-dev-server
我们需要借助一个非常强大的插件工具来实现本地静态服务
,这个插件就是webpack-dev-server,我们常常称呼为WDS
本地服务,他有热更新,并且浏览器会自动刷新页面,无需手动刷新页面
并且我们还需要引入另一个插件Html-webpack-plugins
这个插件,它可以自动帮我们引入打包后的文件。当我们启动本地服务,生地文件 js 文件会在内存中生成,并且被html
自动引入
我们在webpack.config.js
中引入html-webpack-plugin
const path = require('path');
// 引入html-webpack-plugin
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = function (env, argv) {
console.log(env);
console.log(argv);
return {
entry: {
app: './src/app.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
library: 'MyTest',
libraryTarget: argv.libraryTarget
},
mode: argv.mode,
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
})
]
};
};
并且在package.json
中增加server
命令,注意我们加了server
,webpack-dev-server
内部已经有对文件监听,当文件发生变化时,可以实时更新生成在内存的那个js
,这个server
命令就是我安装的webpack-dev-server
的命令
"scripts": {
"server": "webpack server"
},
控制台运行npm run server
默认打开 8080 端口,已经 ok 了
模块热更新(Hot Module Replacement)
现在当我每次修改文件时,整个文件都会重新 build,并且是在虚拟内存中引入,如果修改的只是部分文件,全部文件重新加载就有些浪费了,因此需要HMR
,热更新devServer hot,在运行时更新某个变化的文件模块,无需全部更新所有文件
// weboack.config.js
{
mode: argv.mode,
devServer: {
hot: true
},
}
当我添加完后,发现热更新还是和以前一样,没什么用,官方这里有解释hot-module-replacement,通俗讲就是要指定某些文件要热更新,不然默认只要文件发生更改就得全部重新编译,从而全站刷新。
写了一段测试代码
// utils/index
var str = '123';
function deepMerge(target) {
console.log(target, '=22==');
if (Array.isArray(target)) {
return target;
}
const result = {};
for (var key in target) {
if (Reflect.has(target, key)) {
if (Object.prototype.toString.call(target[key]) === '[object Object]') {
result[key] = deepMerge(target[key]);
} else {
result[key] = target[key];
}
}
}
return result;
}
console.log('深拷贝一个对象555', str);
export default deepMerge;
// module.exports = {
// deepMerge
// };
在app.js
中引入
import deepMerge from './utils/index';
// const { deepMerge } = require('./utils/index.js');
function twoSum(a, b) {
return a + b;
}
const userInfo = {
name: 'Maic',
age: 18,
test: {
book: 'webpack'
}
};
const result = twoSum(1, 2);
console.log(result, deepMerge(userInfo));
if (module.hot) {
// 这个文件
module.hot.accept('./utils/index.js', () => {});
}
const str = 'hello, webpack322266666';
console.log(str);
const app = document.getElementById('app');
app.innerHTML = str;
注意我们加了一段代码判断指定模块是否HMR
if (module.hot) {
// 这个文件
module.hot.accept('./utils/index.js', () => {});
}
这里注意一点
,指定的utils/index.js
必须是esModule
的方式输出,要不然不会生效 ,我们会发现,当我修改utils/index.js
时,会有一个请求 当你每改这个文件都会请求一个app.[hash].hot.update.js
这样的一个文件。
webpack-dev-server
内置了HMR
,我们用webpack server
这个命令就启动静态服务了,并且还内置了HMR
,如果我不想用命令呢,我们可以通过 API 的方式启动dev-server
,具体示例代码如下,新建一个config/server.js
const webpackDevServer = require('webpack-dev-server');
const webpack = require('webpack');
const config = require('../webpack.config.js');
const options = { hot: true, contentBase: '../dist', host: 'localhost' };
// 只能用V2版本https://github.com/webpack/webpack-dev-server/blob/v2
webpackDevServer.addDevServerEntrypoints(config, options);
const compiler = webpack(config);
const server = new webpackDevServer(compiler, options);
const PORT = '9000';
server.listen(PORT, 'localhost', () => {
console.log('server is start' + PORT);
});
webpack-dev-middleware 代替 webpack-dev-server
// config/server.js
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require('../webpack_test_dev_config');
const compiler = webpack(config);
// 设置静态资源目录
app.use(express.static('dist'));
app.use(webpackDevMiddleware(compiler, {}));
const PORT = 8000;
app.listen(PORT, () => {
console.log('server is start' + PORT);
});
然后命令行配置node config/server.js
,可以参考官网webpack-dev-middleware
加载 css[XHR 更新样式]
npm i style-loader css-loader --save-dev
配置加载 css 的loader
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
},
样式是内敛在html
里面的,如何提取成单个文件呢?
mini-css-extract-plugin 提取 css
// webpack.config.js
const miniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = function (env, argv) {
return {
module: {
rules: [
{
test: /\.css$/,
// use: ['style-loader', 'css-loader']
use: [miniCssExtractPlugin.loader, 'css-loader']
}
]
},
plugins: [
new miniCssExtractPlugin({
filename: 'css/[name].css'
})
]
};
};
我们把style-loader
去掉了,并且换成了miniCssExtractPlugin.loader
,并且在plugins
中加入插件,将 css 文件提取了指定文件中,此时就会发现index.html
内敛的样式就变成一个文件加载了。
图片资源加载
我们只知道css
用了css-loader
与style-loader
,那么图片以及特殊文件也是需要特殊loader
才能使用,具体参考图片
首先需要安装file-loader
执行 npm i file-loader --save-dev
// webpack.config.js
{
...
module: {
rules: [
{
test: /\.css$/,
use: [miniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.(png|svg|jpg|gif|jpeg)$/,
use: [
{
loader: 'file-loader',
options: {
outputPath: 'assets',
name: '[name].[ext]'
}
}
]
}
]
}
}
可以参考file-loader
,输出的图片文件可以加hash
值后缀,当打包上传后,如果文件没有更改,图片更容易从缓存中获取
在app.js
中加入引入图片
import deepMerge from './utils/index';
import '../assets/css/app.css';
import image1 from '../assets/images/1.png';
import image2 from '../assets/images/2.jpg';
// const { deepMerge } = require('./utils/index.js');
function twoSum(a, b) {
return a + b;
}
const userInfo = {
name: 'Maic',
age: 18,
test: {
book: '公众号:Web技术学苑'
}
};
const result = twoSum(1, 2);
console.log(result, deepMerge(userInfo));
if (module.hot) {
// 这个文件
module.hot.accept('./utils/index.js', () => {});
}
const str = `<div>
<h5>hello, webpack</h5>
<div>
<img src=${image1} />
</div>
<div>
<img src=${image2} />
</div>
</div>`;
console.log(str);
const app = document.getElementById('app');
app.innerHTML = str;
看下引入的图片页面
大功告成,css
与图片
资源都已经 OK 了
总结
1、了解webpack
是什么,它主要是前端构建工程化的一个工具,将一些譬如ts
,sass
,vue
,tsx
等等一些浏览器无法直接访问的资源,通过webpack
可以打包成最终浏览器可以访问的html
、css
、js
的文件。并且webpack
通过一系列的插件方式,提供loader
与plugins
这样的插件配置,达到可以编译各种文件。
2、了解webpack
编译入口的基本配置,entry
,output
、module
、plugins
以及利用devServer
开启热更新,并且使用module.hot.accept('path')
实现HMR
模块热替换功能
3、我们了解在命令行webpack --watch
可以做到实时监听文件的变化,每次文件变化,页面都会重新加载
4、我们学会如何使用加载css
以及图片资源
,学会配置css-loader
,style-loader
、file-loader
,以及利用min-css-extract-plugin
去提取css
,用html-webpack-plugin
插件实现本地WDS
静态文件与入口文件的映射,在html
中会自动引入实时打包的入口文件的app.bundle.js
5、熟悉从 0 到 1 搭建一个前端工程化项目
6、本文示例code-example
下一节会基于这个当下项目搭建vue
、react
项目,以及项目的tree-shaking
,懒加载
、缓存
,自定义loader,plugins
等