webpack打包
webpack打包
打包工具
模块化解决了我们开发过程中的代码组织问题
但是模块化依然存在一些问题
1、EsModules存在环境兼容问题
2、模块文件比较多,网络请求频繁
3、所有的前端资源都需要模块化
所以模块化是必要的,但是也要解决对生产环境的影响
需要一个工具编译对应的代码,转换为兼容绝大多数环境的代码
将散落的文件打包到一起,解决模块文件频繁请求的问题
至于模块化的文件划分,只在开发阶段需要,可以更好的帮我们组织代码
运行环境没有必要,开发阶段通过模块编写,生产中打包到一个文件
至于其他的文件当作一个模块去使用,对于前端来说有一个统一的方案去使用
模块打包工具
webpack parcel rollup
web作为模块打包器解决js打包的问题,将零散的模块代码,打包到同一个js文件下,对于有文件兼容的代码,通过加载器(loader)对其进行编译转换,特殊类型资源的加载,例如加载样式、图片
通过 Plugin 实现各种自动化的构建任务,例如自动压缩
还具备代码拆分的能力(Code splitting),按照我们的需要进行打包,不用担心把所有的文件代码打包到一起,导致产生的文件比较大的问题。将首次加载所需要的代码打包到一起,然后将其他的模块进行单独存放,等到应用工作过程中,我们实际需要到哪个模块,再异步加载某个模块,实现增量加载或者渐进式加载。这样就不用担心文件太碎或者文件太大的问题。
资源模块:支持js中以模块化的方式去载入任意类型的资源文件,
打包工具解决的是整个前端的模块化,不单指js模块化,更好的享受模块化带来的优势
webpack使用
上手
解决前端模块化的方案
webpack是一个基于npm的工具模块,生成package.json,
yarn init --yes
安装webpack对应的核心模块以及对应的cli模块
sudo yarn add webpack webpack-cli --dev
查看webpack对应的版本
yarn webpack --version
打包
yarn webpack
package.json 修改打包命令
“scripts”: {
“build”:“webpack”
},
配置文件
新建文件webpack.config.js
运行在node环境中的js文件,按照CommonJs的方式编写代码
const path = require('path')
module.exports = {
entry:'./src/main.js',
output:{
filename:'bundle.js',
//指定文件输出路径,但是必须是个绝对路径
path:path.join(__dirname, 'output')
}
}
工作模式
webpack4新增了工作模式的配置,针对于不同环境的预设配置
打包时如果默认没有进行mode的属性,则是默认为production,在这环境下webpack会自动启动一些优化插件,比如说进行代码的压缩,但是打包的结果没办法阅读
或者使用webpack-cli进行取值配置启动,
yarn webpack --mode development//优化打包的速度,添加辅助打包东西
yarn webpack --mode none//打包不做任何操作
或者添加mode属性
const path = require('path')
module.exports = {
mode:'development',
entry:'./src/index.js',
output:{
filename:'bundle.js',
//指定文件输出路径,但是必须是个绝对路径
path:path.join(__dirname, 'output')
}
}
打包结果运行原理
将 Webpack 工作模式设置为 none,这样 Webpack 就会按照最原始的状态进行打包,所得到的代码更容易理解和阅读。(VSCode 中折叠代码的快捷键是 Ctrl + K,Ctrl + 0 )
const path = require('path')
module.exports = {
mode:'none',
entry:'./src/index.js',
output:{
filename:'bundle.js',
//指定文件输出路径,但是必须是个绝对路径
path:path.join(__dirname, 'output')
}
}
整体生成的代码其实就是一个立即执行函数,这个函数是 Webpack 工作入口(webpackBootstrap),它接收一个 modules 参数,调用时传入了一个数组。
展开这个数组,里面元素的函数对应的就是我们源代码中的模块,也就是说每个模块最终被包裹到了这样一个函数中,从而实现模块私有作用域,数组中每个元素都是一个参数列表相同的函数
/******/ (function(modules) { // webpackBootstrap
/******/ // 使用一个对象来缓存所有的模块,这样使的不会重复导入模块
/******/ var installedModules = {};
/******/
/******/ // 请求函数
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // 如果缓存里面导入过该模块,那么不需要在重新导入模块,返回改模块导出的结果
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // 创建一个新模块(并将其放入缓存
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // 执行模块功能
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // 将模块标记为已加载
/******/ module.l = true;
/******/
/******/ // 返回模块的导出
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // 暴露modules对象
/******/ __webpack_require__.m = modules;
/******/
/******/ // 公开模块缓存
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // 定义协调导出的getter函数
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // 在导出上定义
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // 创建假命名空间对象
/******/ // mode & 1: 值是模块id,需要它
/******/ // mode & 2: 将值的所有属性合并到ns中
/******/ // mode & 4: 已存在ns对象时返回值
/******/ // mode & 8|1: 表现得像要求的那样
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport函数与非协调模块兼容
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // 加载输入模块并返回导出
/******/ return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _heading_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1);
const heading = Object(_heading_js__WEBPACK_IMPORTED_MODULE_0__["default"])()
document.body.append(heading)
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony default export */ __webpack_exports__["default"] = (() => {
const element = document.createElement('h2')
element.textContent = 'Hello world'
element.addEventListener('click', () => {
alert('Hello webpack')
})
return element
});
/***/ })
/******/ ]);
最开始定义了一个 installedModules 对象用于存放或者缓存加载过的模块,然后定义了一个 require 函数(这个函数是用来加载模块的),再往后就是在 require 函数上挂载了一些其他的数据和工具函数。
这个函数执行到最后调用了 require 函数,传入的模块 id 为 0,开始加载模块。模块 id 实际上就是模块数组的元素下标,也就是说这里开始加载源代码中所谓的入口模块。
资源模块加载
webpack内部的loader只处理js文件,其他的文件解析需要引入对应的loader
安装css对应的loader
sudo yarn add css-loader style-loader --dev
const path = require('path')
module.exports = {
mode:'none',
entry: './src/main.css',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output')
},
module:{
rules:[
{
//用来匹配打包过程中对应的文件路径
test: /.css$/,
//使用结果从后向前执行
use:[
//将css生成的结果追加到页面上
'style-loader',
'css-loader',
]
}
]
}
}
导入资源模块
根据代码需要动态加载资源,需要资源的不是应用而是对应的代码,驱动整个前端应用
main.js
import './main.css'
const path = require('path')
module.exports = {
mode:'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output')
},
module:{
rules:[
{
//用来匹配打包过程中对应的文件路径
test: /.css$/,
//使用结果从后向前执行
use:[
//将css生成的结果追加到页面上
'style-loader',
'css-loader',
]
}
]
}
}
文件资源加载器
sudo yarn add file-loader --dev
import icon from ‘./icon.png’
const img = new Image()
img.src = icon
document.body.append(img)
const path = require('path')
module.exports = {
mode:'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
//告诉浏览器打包生成文件的路径
publicPath:'dist/'
},
module:{
rules:[
{
//用来匹配打包过程中对应的文件路径
test: /.css$/,
//使用结果从后向前执行
use:[
//将css生成的结果追加到页面上
'style-loader',
'css-loader',
]
},
{
//用来匹配打包过程中对应的文件路径
test: /.png$/,
//使用结果从后向前执行
use:[
'file-loader'
]
},
]
}
}
data url,url加载器
sudo yarn add url-loader --dev
将文件转换为url
小文件使用data-urls,减少请求次数
大文件单独提取存放,提高加载速度
const path = require('path')
module.exports = {
mode:'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
//告诉浏览器打包生成文件的路径
publicPath:'dist/'
},
module:{
rules:[
{
//用来匹配打包过程中对应的文件路径
test: /.css$/,
//使用结果从后向前执行
use:[
//将css生成的结果追加到页面上
'style-loader',
'css-loader',
]
},
{
//用来匹配打包过程中对应的文件路径
test: /.png$/,
//使用结果从后向前执行
use:{
loader: 'file-loader',
options: {
limit:10 * 1024
}
}
},
]
}
}
加载器分类
编译转换类 css-loader
文件操作类 file-loader
代码检查类 eslint-loader
转换ES6代码
webpack可以处理import和export
sudo yarn add babel-loader @babel/core @babel/preset-env --dev
const path = require('path')
module.exports = {
mode:'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
//告诉浏览器打包生成文件的路径
publicPath:'dist/'
},
module:{
rules:[
{
//用来匹配打包过程中对应的文件路径
test: /.js$/,
//使用结果从后向前执行
use:{
loader: 'babel-loader',
options: {
presets:['@babel/preset-env']
}
}
},
{
//用来匹配打包过程中对应的文件路径
test: /.css$/,
//使用结果从后向前执行
use:[
//将css生成的结果追加到页面上
'style-loader',
'css-loader',
]
},
{
//用来匹配打包过程中对应的文件路径
test: /.png$/,
//使用结果从后向前执行
use:{
loader: 'file-loader',
options: {
limit:10 * 1024
}
}
},
]
}
}
加载资源的方式
loader加载的非js也会触发资源加载
样式代码中的@import指令和url函数
html代码中图片标签的src属性
sudo yarn add html-loader -dev
html-loader 默认只会处理img标签下的src属性
const path = require('path')
module.exports = {
mode:'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
//告诉浏览器打包生成文件的路径
publicPath:'dist/'
},
module:{
rules:[
{
//用来匹配打包过程中对应的文件路径
test: /.js$/,
//使用结果从后向前执行
use:{
loader: 'babel-loader',
options: {
presets:['@babel/preset-env']
}
}
},
{
//用来匹配打包过程中对应的文件路径
test: /.css$/,
//使用结果从后向前执行
use:[
//将css生成的结果追加到页面上
'style-loader',
'css-loader',
]
},
{
//用来匹配打包过程中对应的文件路径
test: /.png$/,
//使用结果从后向前执行
use:{
loader: 'file-loader',
options: {
limit:10 * 1024
}
}
},
{
test: /.html$/,
use:{
loader: 'html-loader',
options:{
attrs:['img:src', 'a:href']
}
}
},
]
}
}
核心工作原理
loader机制是webpack打包的核心
插件机制
是为了增强,webpack在项目自动化方面的能力
plugin解决资源加载外的其他自动化工作
清除dist目录,拷贝静态资源到输出目录,压缩代码,
自动清除输出目录插件
sudo yarn add clean-webpack-plugin --dev
const path = require('path')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
module.exports = {
mode:'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
//告诉浏览器打包生成文件的路径
publicPath:'dist/'
},
module:{
rules:[
{
//用来匹配打包过程中对应的文件路径
test: /.js$/,
//使用结果从后向前执行
use:{
loader: 'babel-loader',
options: {
presets:['@babel/preset-env']
}
}
},
{
//用来匹配打包过程中对应的文件路径
test: /.css$/,
//使用结果从后向前执行
use:[
//将css生成的结果追加到页面上
'style-loader',
'css-loader',
]
},
{
//用来匹配打包过程中对应的文件路径
test: /.png$/,
//使用结果从后向前执行
use:{
loader: 'file-loader',
options: {
limit:10 * 1024
}
}
},
{
test: /.html$/,
use:{
loader: 'html-loader',
options:{
attrs:['img:src', 'a:href']
}
}
},
]
},
plugins:[
//创建实例放入数组中
new CleanWebpackPlugin()
]
}
自动生成html插件
自动生成使用bundle.js的HTML,通过webpack输出html文件,确保打包出来的文件路径的正确性 html-webpack-plugin,自动生成index.html
sudo yarn add html-webpack-plugin --dev
默认导出插件的类型,不需要解构,
const path = require('path')
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const htmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode:'none',
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'output'),
//告诉浏览器打包生成文件的路径
// publicPath:'dist/'
},
module:{
rules:[
{
//用来匹配打包过程中对应的文件路径
test: /.js$/,
//使用结果从后向前执行
use:{
loader: 'babel-loader',
options: {
presets:['@babel/preset-env']
}
}
},
{
//用来匹配打包过程中对应的文件路径
test: /.css$/,
//使用结果从后向前执行
use:[
//将css生成的结果追加到页面上
'style-loader',
'css-loader',
]
},
{
//用来匹配打包过程中对应的文件路径
test: /.png$/,
//使用结果从后向前执行
use:{
loader: 'file-loader',
options: {
limit:10 * 1024
}
}
},
{
test: /.html$/,
use:{
loader: 'html-loader',
options:{
attrs:['img:src', 'a:href']
}
}
},
]
},
plugins:[
//创建实例放入数组中
new CleanWebpackPlugin(),
//自动生成使用bundle.js的HTML
new htmlWebpackPlugin({
//生成dist文件夹下的index.html文件的属性配置
title:'webpackPlugin',
meta:{
viewreport:'width=device-width'
},
//生成的文件模板地址,可以自定义
template:''
}),
//创建额外的html文件
new htmlWebpackPlugin({
filename:'about.html'
})
]
}
插件实现的原理
通过在生命周期中挂载函数实现扩展
自动编译
watch工作模式,监听文件变化,自动重新打包,
以监视模式进行,
sudo yarn webpack --watch
webpack dev server
集成自动编译和自动刷新浏览器
sudo yarn add webpack-dev-serve --dev
yarn webpack-dev-serve --open//自动唤醒浏览器
支持配置代理
开发阶段
devServer:{
proxy:{
'/api':{
target:'https://api.github.com',
//将api替换为空
pathRewrite:{
'^api':''
},
//不能使用localhost:8080,作为github的主机名
changeOrigin:true
}
},
},
sourceMap
源代码地图,用于映射转换过后的代码与源代码之间的关系,主要是开发人员调试
devtool:'source-map',
HMR
hot module replacement
模块热替换
webpack-dev-serve --hot开启
也可以通过配置文件开启
devServer:{
hot:true
}
样式文件可以自动处理,更新过后可以及时替换,但是脚本文件没有规律,使用起来各不相同
脚本文件需要手动处理模块热替换逻辑
vue-cli 没有手动处理,但是脚本代码有一定的规律,都有通用的替换方法,内部集成了HMR方案
api:
module.hot.accept('./editor', ()=>{
console.log('edit模块更新了')
})
definePlugin
注入process.env.NODE_ENV
Tree Shaking
去除冗余代码
是一种功能搭配后的优化效果
optimization:{
//只导出外部使用的成员,比较代码
usedExports:true,
//尽可能将所有的模块合并到一起,输出到一个函数中,提升了运行效率,减少了输出代码的体积
concatenateModules:true,
//删除代码
minimize:true
}
最新版本的commonjs不会使Tree Shaking失效
sideEffects
标记代码是否有副作用,从而为Tree Shaking提供更大的压缩空间
副作用指的是除了导出成员之外的事情
一般用于开发npm包标记是否有副作用
代码分割
Code Splitting
不是每个模块在启动时都是必要的
分包,按需加载
目前主流的http1.1版本
同域并行请求限制
每次请求都有一定的延迟
大量的header浪费带宽流量
所以出现了新的解决方案
多入口打包,输出多个打包结果,
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'none',
entry: {
index: './src/index.js',
album: './src/album.js'
},
output: {
filename: '[name].bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/index.html',
filename: 'index.html',
chunks: ['index']
}),
new HtmlWebpackPlugin({
title: 'Multi Entry',
template: './src/album.html',
filename: 'album.html',
chunks: ['album']
})
]
}
公共模块提取
optimization: {
splitChunks: {
// 自动提取所有公共模块到单独 bundle
chunks: 'all'
}
},
Rollup
对比与webpack来说更加小巧,仅仅是一款ESM的打包器,没有任何其他额外的功能
不支持类似于HMR这种的高级特性。充分利用ESM的各种特性的高效打包器
sudo yarn add rollup --dev
//yarn能够自动找到node_modules下的cli信息
yarn rollup//运行
yarn rollup ./src/index.js//找到打包入口文件
yarn rollup ./src/index.js --format//指定输出格式 iife//自调用格式 --file dist/bundle.js//指定输出文件的路径
rollup打包结果只会保留用到的部分,默认开启tree shaking
配置文件
新建rollup.config.js文件
export default {
input:'src/index.js',
output:{
file:'dist/bundle.js',
format:'iife'
}
}
使用插件
插件是rollup唯一的扩展路径
rollup-plugin-json
//默认导出的是插件函数
import json from 'rollup-plugin-json'
export default {
input:'src/index.js',
output:{
file:'dist/bundle.js',
format:'iife'
},
//将函数的调用结果配置到数组中
plugins:[
json()
]
}
加载npm模块
不想webpack一样导入模块名称从而引用模块
需要辅助插件
sudo yarn add rollup-plugin-node-resolve --dev
加载CommonJs模块
sudo yarn add rollup-plugin-commonjs --dev
新建cjs_module.js
module.exports = {
foo:'bar'
}
//默认导出的是插件函数
import json from 'rollup-plugin-json'
import resolve from 'rollup-plugin-node-resolve'
import common from 'rollup-plugin-commonjs'
export default {
input:'src/index.js',
output:{
file:'dist/bundle.js',
format:'iife'
},
//将函数的调用结果配置到数组中
plugins:[
json(),
resolve(),
common()
]
}
// 导入模块成员
import { log } from './logger'
import messages from './messages'
import cjs from './cjs_module'
// 使用模块成员
const msg = messages.hi
log(msg)
log(cjs)
代码拆分是通过动态导入
优势:输出的结果更加扁平,执行效率也会更高,自动移除未引用的代码
缺点:加载非ESM的第三方模块比较复杂,
模块最终会被打包到一个函数中,无法实现HMR
浏览器的代码拆分需要以来AMD的库
parcel零配置打包器
sudo yarn add parcel-bundler --dev
以html作为打包文件入口
yarn parcel build src/index.html
一些插件可以自动引入,不要自己手动命令行输入,保存之后自动刷新页面