前端工程化经历过不少优秀的工具,例如 Grunt、Gulp、webpack、rollup 等等,每种工具都有本身适用的场景,而现今应用最为普遍的当属 weback 打包了。所以 webpack 也天然而然成了面试官打探你是否懂前端工程化的重要指标。javascript
因为 webpack 技术栈比较复杂,所以决定分如下几篇文章全面深刻的讲解:css
webpack 是模块打包工具html
webpack 能够不进行任何配置(不进行任何配置时,webpack 会使用默认配置)打包以下代码:前端
// moduleA.js
function ModuleA(){
this.a = "a";
this.b = "b";
}
export default ModuleA
// index.js
import ModuleA from "./moduleA.js";
const module = new ModuleA();
复制代码
咱们知道浏览器是不认识的 import 语法的,直接在浏览器中运行这样的代码会报错。那么咱们就能够借助 webpack 来打包这样的代码,赋予 JavaScript 模块化的能力。java
最第一版本的 webpack 只能打包 JavaScript 代码,随着发展 css 文件,图片文件,字体文件均可以被 webpack 打包。node
本文将主要讲解 webpack 是如何打包这些资源的,属于比较基础的文章主要是为了后面讲解性能优化和原理作铺垫,若是已经对 webpack 比较熟悉的同窗能够跳过本文。react
mkdir webpackDemo // 建立文件夹
cd webpackDemo // 进入文件夹
npm init -y // 初始化package.json
npm install webpack webpack-cli -D // 开发环境安装 webpack 以及 webpack-cli
复制代码
经过这样安装以后,咱们能够在项目中使用 webpack 命令了。webpack
webpack.config.jsgit
const path = require('path');
module.exports = {
mode: 'development', // {1}
entry: { // {2}
main:'./src/index.js'
},
output: { // {3}
publicPath:"", // 全部dist文件添加统一的前缀地址,例如发布到cdn的域名就在这里统一添加
filename: 'bundle.js',
path: path.resolve(__dirname,'dist')
}
}
复制代码
代码分析:es6
development | production
[注意] 这个基础的配置文件哪怕你不写,咱们执行 webpack 命令也能够运行,那是由于 webpack 提供了一个默认配置文件。
建立文件进行简单打包:
src/moduleA.js
const moduleA = function () {
return "moduleA"
}
export default moduleA;
--------------------------------
src/index.js
import moduleA from "./moduleA";
console.log(moduleA());
复制代码
修改 package.json
srcipts
"scripts": {
"build": "webpack --config webpack.config.js"
}
复制代码
执行 npm run build 命令
源码通过简化,只把核心部分展现出来,方便理解
(function(modules) {
var installedModules = {};
function __webpack_require__(moduleId) {
// 缓存文件
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 初始化 moudle,而且也在缓存中存入一份
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 执行 "./src/index.js" 对应的函数体
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 标记"./src/index.js"该模块以及加载
module.l = true;
// 返回已经加载成功的模块
return module.exports;
}
// 匿名函数开始执行的位置,而且默认路径就是入口文件
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
// 传入匿名执行函数体的module对象,包含"./src/index.js","./src/moduleA.js"
// 以及它们对应要执行的函数体
({
"./src/index.js": (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _moduleA__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./moduleA */ \"./src/moduleA.js\");\n\n\nconsole.log(Object(_moduleA__WEBPACK_IMPORTED_MODULE_0__[\"default\"])());\n\n\n//# sourceURL=webpack:///./src/index.js?");
}),
"./src/moduleA.js": (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\nconst moduleA = function () {\n return \"moduleA\"\n}\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (moduleA);\n\n\n//# sourceURL=webpack:///./src/moduleA.js?");
})
});
复制代码
再来看看"./src/index.js"
对应的执行函数
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _moduleA__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./moduleA */ \"./src/moduleA.js\");\n\n\nconsole.log(Object(_moduleA__WEBPACK_IMPORTED_MODULE_0__[\"default\"])());\n\n\n//# sourceURL=webpack:///./src/index.js?");
})
复制代码
你会发现其实就是一个 eval 执行方法
咱们拆开 eval 来仔细看看里面是什么内容,简化后代码以下:
var moduleA = __webpack_require__("./src/moduleA.js");
console.log(Object(moduleA["default"])());
复制代码
上面源码中其实已经调用了 __webpack_require__(__webpack_require__.s = "./src/index.js");
,而后 "./src/index.js"
又递归调用了去获取 "./src/moduleA.js"
的输出对象。
咱们看看 "./src/moduleA.js"
代码会输出什么:
const moduleA = function () {
return "moduleA"
}
__webpack_exports__["default"] = (moduleA);
复制代码
再回头看看上面的代码就至关于:
console.log(Object(function () {
return "moduleA"
})());
复制代码
最后执行打印了 "moduleA"
经过这段源码的分析能够看出:
./src/index.js
而后递归的去把全部模块找到,因为递归的一个缺点,会进行重复计算,所以 __webpack_require__
函数中有一个缓存对象 installedModules
来处理这个问题。咱们知道 webpack 能够打包 JavaScript 模块,并且也早就据说 webpack 还能够打包图片、字体以及 css,这个时候就须要 loader 来帮助咱们识别这些文件了。
[注意] 碰到文件不能识别记得找 loader 便可。
修改配置文件:webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
entry: {
main:'./src/index.js'
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname,'dist')
},
module:{
rules:[
{
test:/\.(png|svg|jpg|gif)$/,
use:{
loader: 'url-loader',
options: {
name: '[name]_[hash].[ext]',
outputPath:"images", // 打包该资源到 images 文件夹下
limit: 2048 // 若是图片的大小,小于2048KB则时输出base64,不然输出图片
}
}
}
]
}
}
复制代码
修改:src/index.js
import moduleA from "./moduleA";
import header from "./header.jpg";
function insertImg(){
const imageElement = new Image();
imageElement.src = `dist/${header}`;
document.body.appendChild(imageElement);
}
insertImg();
复制代码
执行打包后,发现能够正常打包,而且 dist 目录下也多出了一个图片文件。
咱们简单分析下:
webpack 自己其实只认识 JavaScript 模块的,当碰到图片文件时便会去 module 的配置 rules 中找,发现 test:/\.(png|svg|jpg|gif)$/
,正则匹配到图片文件后缀时就使用 url-loader
进行处理,若是图片小于 2048KB
(这个能够设置成任意值,主要看项目)就输出 base64
。
{
test:/\.scss$/, // 正则匹配到.scss样式文件
use:[
'style-loader', // 把获得的CSS内容插入到HTML中
{
loader: 'css-loader',
options: {
importLoaders: 2, // scss中再次import scss文件,也一样执行 sass-loader 和 postcss-loader
modules: true // 启用 css module
}
},
'sass-loader', // 解析 scss 文件成 css 文件
'postcss-loader'// 自动增长厂商前缀 -webket -moz,使用它还须要建立postcss.config.js配置文件
]
}
复制代码
postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')
]
};
复制代码
打包解析:
xx.scss
样式文件;postcss-loader
自动增长厂商前缀 -webket -moz
;sass-loader
把 scss 文件转换成 css 文件;css-loader
处理 css 文件,其中 importLoaders:2
,是 scss 文件中引入了其它 scss 文件,须要重复调用 sass-loader
postcss-loader
的配置项;style-loader
把前面编译好的 css 文件内容以 <style>...</style>
形式插入到页面中。[注意] loader的执行顺序是数组后到前的执行顺序。
{
test: /\.(woff|woff2|eot|ttf|otf)$/, // 打包字体文件
use: ['file-loader'] // 把字体文件移动到dist目录下
}
复制代码
plugins 能够在 webpack 运行到某个时刻帮你作一些事情,至关于 webpack 在某一个生命周期插件作一些辅助的事情。
做用:
会在打包结束后,自动生产一个 HTML 文件(也可经过模板生成),并把打包生成的 JS 文件自动引入到 HTML 文件中。
使用:
const HtmlWebpackPlugin = require('html-webpack-plugin');
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html' // 使用模板文件
})
]
复制代码
做用:
每次输出打包结果时,先自动删除 output 配置的文件夹
使用:
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
plugins: [
...
new CleanWebpackPlugin() // 使用这个插件在每次生成dist目录前,先删除dist目录
]
复制代码
在开发过程当中有一个功能是很重要的,那就是错误调试,咱们在编写代码过程当中出现了错误,编译后的包若是提示不友好,将会严重影响咱们的开发效率。而经过配置 source map 就能够帮助咱们解决这个问题。
示例: 修改:src/index.js,增长一行错误的代码
console.log(a);
复制代码
因为mode: 'development'
开发模式是默认会打开source map功能的,咱们先关闭它。
devtool: 'none' // 关闭source map 配置
复制代码
执行打包来看下控制台的报错信息:
咱们去掉 devtool:'none'
这行配置,再执行打包:
总结下:source map 它是一个映射关系,它知道 dist 目录下 bundle.js 文件对应的实际是 index.js 文件中的多少行。
每次修改完代码以后都要手动去执行编译命令,这显然是不科学的,咱们但愿是每次写完代码,webpack 会进行自动编译,webpackDevServer 就能够帮助咱们。
增长配置:
devServer: {
contentBase: './dist', // 服务器启动根目录设置为dist
open: true, // 自动打开浏览器
port: 8081 // 配置服务启动端口,默认是8080
},
复制代码
它至关于帮助咱们开启了一个 web 服务,并监听了 src 下文件当文件有变更时,自动帮助咱们进行从新执行 webpack 编译。
咱们在 package.json
中增长一条命令:
"scripts": {
"start": "webpack-dev-server"
},
复制代码
如今咱们执行 npm start
命令后,能够看到控制台开始实行监听模式了,此时咱们任意更改业务代码,都会触发 webpack 从新编译。
项目根目录下增长:server.js
加载包: npm install express webpack-dev-middleware -D
const express = require('express');
const app = express();
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const config = require('./webpack.config.js'); // 引入webpack配置文件
const compiler = webpack(config); // webpack 编译运行时
// 告诉 express 使用 webpack-dev-middleware,
// 以及将 webpack.config.js 配置文件做为基础配置
app.use(webpackDevMiddleware(compiler, {}));
// 监听端口
app.listen(3000,()=>{
console.log('程序已启动在3000端口');
});
复制代码
webpack-dev-middleware
做用:
watch mode
监听资源的变动而后自动打包,本质上是调用 compiler
对象上的 watch 方法;compiler.outputFileSystem = new MemoryFileSystem()
;package.json 增长一条命令:
"scripts": {
"server": "node server.js"
},
复制代码
执行命令 npm run server
启动咱们自定义的服务,浏览器中输入 http://localhost:3000/
查看效果。
模块热更新功能会在应用程序运行过程当中,替换、添加或删除模块,而无需从新加载整个页面。
const webpack = require('webpack');
module.exports = {
devServer: {
contentBase: './dist',
open: true,
port: 8081,
hot: true // 热更新配置
},
plugins:[
new webpack.HotModuleReplacementPlugin() // 增长热更新插件
]
}
复制代码
在编写代码时常常会发现热更新失效,那是由于相应的 loader 没有去实现热更新,咱们看看如何简单实现一个热更新。
import moduleA from "./moduleA";
if (module.hot) {
module.hot.accept('./moduleA.js', function() {
console.log("moduleA 支持热更新拉");
console.log(moduleA());
})
}
复制代码
代码解释: 咱们引人本身编写的一个普通 ES6 语法模块,加入咱们想要实现热更新就必须手动监听相关文件,而后当接收到更新回调时,主动调用。
还记得上面讲 webpack 打包后的源码分析吗,webpack 给模块都创建了一个 module 对象,当你开启模块热更新时,在初始化 module 对象时增长了(源码通过删减):
function hotCreateModule(moduleId) {
var hot = {
active: true,
accept: function(dep, callback){
if (dep === undefined) hot._selfAccepted = true;
else if (typeof dep === "function") hot._selfAccepted = dep;
else if (typeof dep === "object")
for (var i = 0; i < dep.length; i++)
hot._acceptedDependencies[dep[i]] = callback || function() {};
else hot._acceptedDependencies[dep] = callback || function() {};
}
}
}
复制代码
module 对象中保存了监听文件路径和回调函数的依赖表,当监听的模块发生变动后,会去主动调用相关的回调函数,实现手动热更新。
[注意] 全部编写的业务模块,最终都会被 webpack 转换成 module 对象进行管理,若是开启热更新,那么 module 就会去增长 hot 相关属性。这些属性构成了 webpack 编译运行时对象。
显然你们都知道必需要使用 babel 来支持了,咱们具体看看如何配置
一、安装相关包
npm install babel-loader @babel/core @babel/preset-env @babel/polyfill -D
复制代码
二、修改配置 webpack.config.json
还记得文章上面说过,碰到不认识的文件类型的编译问题要求助 loader
module:{
rules:[
{
test: /\.js$/, // 正则匹配js文件
exclude: /node_modules/, // 排除 node_modules 文件夹
loader: "babel-loader", // 使用 babel-loader
options:{
presets:[
[
"@babel/preset-env", // {1}
{ useBuiltIns: "usage" } // {2}
]
]
}
}
]
}
复制代码
babel 配置解析:
Promise
Generator
是新语法Array.prototype.map
方法是新 API ,babel 是不会转换这个语法的,所以须要借助 polyfill
处理useBuiltIns
的配置是处理 @babel/polyfill
如何加载的,它有3个值 false
entry
usage
false
: 不对 polyfills
作任何操做;entry
: 根据 target
中浏览器版本的支持,将polyfills
拆分引入,仅引入有浏览器不支持的polyfill
usage
:检测代码中ES6/7/8
等的使用状况,仅仅加载代码中用到的polyfills
新建文件 src/moduleES6.js
const arr = [
new Promise(()=>{}),
new Promise(()=>{})
];
function handleArr(){
arr.map((item)=>{
console.log(item);
});
}
export default handleArr;
复制代码
修改文件 src/index.js
import moduleES6 from "./moduleES6";
moduleES6();
复制代码
执行打包后的源文件(简化后):
"./node_modules/core-js/modules/es6.array.map.js":
(function(module, exports, __webpack_require__) {
"use strict";
var $export = __webpack_require__("./node_modules/core-js/modules/_export.js");
var $map = __webpack_require__("./node_modules/core-js/modules/_array-methods.js")(1);
$export($export.P + $export.F * !__webpack_require__(/*! ./_strict-method */ "./node_modules/core-js/modules/_strict-method.js")([].map, true), 'Array', {
map: function map(callbackfn) {
return $map(this, callbackfn, arguments[1]);
}
});
复制代码
看代码就应该能明白了 polyfill 至关因而使用 ES5 的语法从新实现了 map 方法来兼容低版本浏览器。
而 polyfill 实现了 ES6-ES10 全部的语法十分庞大,咱们不可能所有引入,所以才会有这个配置 useBuiltIns: "usage"
只加载使用的语法。
安装相关依赖包
npm install @babel/preset-react -D
npm install react react-dom
复制代码
webpack.config.js
module:{
rules:[
{
test: /\.js$/, // 正则匹配js文件
exclude: /node_modules/, // 排除 node_modules 文件夹
loader: "babel-loader", // 使用 babel-loader
options:{
presets:[
[
"@babel/preset-env",
{ useBuiltIns: "usage" }
],
["@babel/preset-react"]
]
}
}
]
}
复制代码
直接在 presets 配置中增长一个 ["@babel/preset-react"]
配置便可, 那么这个 preset 就会帮助咱们把 React 中 JSX 语法转换成 React.createElement 这样的语法。
修改文件:src/index.js
import React,{Component} from 'react';
import ReactDom from 'react-dom';
class App extends Component{
render(){
const arr = [1,2,3,4];
return (
arr.map((item)=><p>num: {item}</p>)
)
}
}
ReactDom.render(<App />, document.getElementById('root')); 复制代码
执行打包命令 yarn build
能够正确打包而且显示正常界面。
随着项目的复杂度增长,babel 的配置也随之变的复杂,所以咱们须要把 babel 相关的配置提取成一个单独的文件进行配置方便管理,也就是咱们工程目录下的 .babelrc
文件。
{
"presets":[
["@babel/preset-env",{ "useBuiltIns": "usage" }],
["@babel/preset-react"]
]
}
复制代码
[注意] babel-laoder
执行 presets
配置顺序是数组的后到前,与同时使用多个 loader 的执行顺序是同样的。
也就是把 webpack.config.js
中的 babel-loader
中的 options
对象提取成一个单独文件。
bundle.js
文件足足有1M大,那是由于 react 以及 react-dom 都被打包进来了,webpack 优化的文章中会讲解如何
code-splitting
进行优化。
本文主要仍是以如何使用 webpack 为主线,让你们对 webpack 有一个初步的印象,而且学会使用 webpack 配置简单的项目,在面试中并不会问这些,所以这篇文章只是一篇铺垫性质的文章,真正面试中文的较多的仍是webpack 的高级用法以及性能优化,更甚至也会问及 webpack 的打包执行原理,在以后的几篇文章会详细讲解。