绝大部分生产我的项目都是基于cli
脚手架创立一个比较完善的我的项目,从晚期的webpack
配置工程师到前面的无需配置,大大解放了前端工程建设。然而时常会遇到,不依赖成熟的脚手架,从零搭过我的项目吗,有遇到哪些问题吗?或者有理解loader
和plugin
吗?如果只是应用脚手架,作为一个深耕业务一线的工具人,什么?还要本人搭?还要写loader
,这就过分了。
注释开始...
前置
咱们先理解下webpack
能干什么
webpack
是一个动态打包工具,依据入口文件构建一个依赖图,依据须要的模块组合成一个bundle.js
或者多个bundle.js
,用它来展现动态资源
对于webpack
的一些外围概念,次要有以下,参考官网
entry
1、entry
入口(依赖入口文件,webpack首先依据这个文件去做外部模块的依赖关系)
// webpack.config.jsmodule.exports = { entry: './src/app.js'}// or/*// 是以下这种形式的简写 定义一个别名mainmodule.exports = { entry: { main: ./src/app.js' }}*/
也能够是一个数组
// webpack.config.jsmodule.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.jsmodule.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
并且管制台上曾经胜利了
webpackasset 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.jsconst 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-pluginconst 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/indexvar 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
[](https://www.webpackjs.com/gui... ""),具体示例代码如下,新建一个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/v2webpackDevServer.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.jsconst 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.jsconst 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
等