webpack基础探讨

103 阅读12分钟

 使用webpack
webpack命令使用
webpack --help (webpack -h) 查看所有的命令
webpack-v
打包命令 webpack [] 不适用webpack配置文件的时候
使用webpack配置文件, 直接webpack
--config 指定配置文件 (默认配置文件名称 webpack.config.js或者 webpackfile.js)
Basic Options
--entry 指定入口文件
--watch -w 检测文件系统的变化
--debug -d 打开调试状态
--devtool 生成sourceMap
--progress 进度条显示
-d 简写 --mode development
Module Options
Output Options
Advanced Options 高级选项
Resolved Options 解析选项
Optimization Options 优化选项
Stats Option 状态选项 (打包出来样式的选项)
使用webpack配置(配合node npm使用)
不同的配置文件, 开发环境, 生产环境, 测试环境, 打包给第三方使用的
第三方的脚手架vue-cli
交互式的初始化一个项目
项目迁移v1->v2
# wepbpack-cli的使用
webpack-cli init webpack-addons-demo
# 项目迁移
webpack-cli migrate <config> ## 只会升级配置文件, package.json里面的文件 需要手机升级
2. 直接使用webpack命名, 使用默认文件或者默认配置
// app.js
import sum from './sum'
conosle.log(sum(1,2))

// sum.js
export default function sum(a, b) {
return a + b;
}

// 打包命令: webpack app.js --output-path=dist --output-filename=bundle.js --mode development

// 指定配置文件 webpack --config webpack.config.dev.js
3. 编译ES6/7
babel-loader

## 安装最新版本loader
npm install babel-loader@8.0.0-beta @babel/core --save-dev
## 安装最新preset
npm install @babel/preset-env --save-dev
npm install babel-loader babel-core --save-dev

npm install babel-preset-env --save-dev 指定规范的版本, 只是针对语法

es2015
es2016
es2017
env 包括2015~2017, 以及latest 用的比较多

业内自定义的babel-preset-react
babel-preset-stage 0 ~3 表示规范组还没有正式发布阶段的
babel-presets - options - target 当需要编译的时候, 会根据指定的target来选择那些语法进行编译, 那些语法不进行编译
target.browsers 指定浏览器环境
target.browsers: 'last 2 versions' 主流浏览器的最后两个版本
target.browsers: '> 1%' 大于全球浏览器占有率1%的浏览器
数据来源是 browserlist中, can i use中

{
test: /\.js$/,
use: { // use: 'babel-loader' //可以直接是一个字符串
loader: 'babel-loader',
options: {
// 指定preset
presets: [['env', {
// 告诉babel, 当需要编译的时候, 会根据指定的target来选择那些语法进行编译, 那些语法不进行编译
targets: {
browsers: ['> 1%', 'last 2 versions'],
// chrome: '52' // 一些新语法浏览器直接支持 不会被转换
}
}]]
}
},
exclude: '/node-modules/'
}

// 当同时指定'> 1%', 'last 2 versions'的时候, 箭头函数会被转化, const, let等被转化, set不会被转化, num**2 转成了Math.pow
// 将targets换成 chrome: '52', 转化后代码基本和原生代码一样
target.node 指定node环境

babel-polyfill插件和babel-runtime-transform插件

针对一些方法比如数组的map, includes, Set并没有被babel处理, 但是在一些低版本的浏览器中这些方法并没有被实现, 所以需要借助这两个插件
babel-preset 只是针对语法, 而这两个插件针对函数和方法
generator
Set
Map
Array.from
Array.prototype.includes
上述方法都没有被babel处理, 所以就需要借助babel的插件进行处理
babel-polyfill 垫片, 浏览器之间标准实现的方式不一样,保持浏览器之间同样的API
全局垫片 (只要引入, 在全局范围内整个浏览器范围内, 可以对实现的API进行调用)
相当于对全局变量的一个污染, 为开发应用而准备的 (在业务中使用, 而不是框架比如vue)
使用: npm install babel-polyfill --save 真实项目中的依赖 所以是--save
在项目中使用 import 'babel-polyfill'
babel-runtime-transform
局部垫片
为开发框架而准备的, 不会污染全局变量, 会在局部的方法里面新增加变量和方法
优势: 当在代码中使用它的时候, 项目中的其他函数,如果使用es6/7方法, 会将每一个引用到的方法打包到单独的文件中去的; 如果使用了runtime-transform, 将其作为一个独立的整体单独打包进去, 相当于文件之间多余的代码就不会再有了
npm install babel-plugin-transform-runtime --save-dev
npm install babel-runtime --save
.babelrc 在里面配置和babel插件相关的内容

// app.js
import sum from './sum'

const func = () => {
console.log('hello babel')
}

func()

const arr = [1, 2, 3, 4, 5, 4, 3, 2, 1]
const arrb = arr.map(item => item * 2)

// 下面的语句 不会经过runtime编译
arr.includes(5);

// 会经过runtime编译 但是没有exports 使用的时候报错
console.log('SetB', new Set(arrb))

/*
function* gen() {
yield 1
}
*/
sum(1, 2)

// .babelrc
{
"presets": [["env", {
"targets": {
"browsers": ["> 1%", "last 2 versions"]
}
}]],
"plugins": [
"transform-runtime"
]
}

// 1. 当plugins为空的时候, 上面的代码会完整运行, 都不会被转义
// 2. 添加generator函数的时候, 会报错找不到 regenerator
// 3. 添插件的时候 includes不会编译, Set, generator会编译, 但是报错$export is not a function
// 4. 屏蔽插件plugins, 使用polyfill, 完美运行所有新属性, 但是打包文件很大, 达到了471Kb
实际开发中如何选择

如果是应用开发, 只需要配置preset, 如果要使用es6/7新语法, 使用polyfill
如果是开发UI库, 框架, 使用runtime
4. 编译TypeScript
JS的超集 tslang.cn 来自于微软
官方推荐: npm install typescript ts-loader --save-dev
第三方loader: npm install typescript awesome-typescript-loader --save-dev
配置: tsconfig.json
## 常用配置选项
compilerOptions:告诉编译器常用的配置选项, 比如 允许js 模块化方式指定:commonjs 指定输出路径等
compilerOptions.module: 模块化方式指定
compilerOptions.target: 编译之后的文件在什么环境下运行的 (类似将语言编译到什么程度)
compilerOptions.typeRoots: [
"./node_modules/@type", // 默认安装npm install @types/lodash时路径
"./typings/modules", // 使用typings安装的声明文件路径
] 指定types声明文件所在的地址
include: 给出一系列的文件路径, 表示需要编译的文件
exclude: 忽略的文件
allowJs: 是否允许js的语法
安装声明文件.这样在编译的时候就会给出警告错误, 告诉我们传递的参数类型有错误
npm install @types/lodash
npm install @types/vue
或者使用typings安装types声明文件, 使用compilerOptions.typeRoots
5. 提取公用代码
减少冗余代码(每一个页面都会存在公共代码, 造成带宽浪费)
提高用户的加载速度(只加载页面所需要的依赖, 其他页面在加载的时候, 公共代码已经加载好了)
CommonChunkPlugin (webpack.optimize.CommonChunkPlugin) // 针对webpack3 {

plugins: [
new webpack.optimize.CommonChunkPlugin({
name: String | Array, // 表示chunk的名称 ?
filename: String, // 公用代码打包的文件名称
minChunks: Number|function|Infinity // 数字表示为需要提取的公用代码出现的次数(最小是多少, 比如出现两次以上就提取到公用代码), Infinity 表示不讲任何的模块打包进去, 函数的话表示自定义逻辑
chunks: 表示指定提取代码的范围, 需要在哪几个代码快中提取公用代码
children: 是不是在entry的子模块中 还是在所有模块中查找依赖
deepChildren
async: 创建一个异步的公共代码流
})
]
}
// webpack4 optimization: {

splitChunks: {
chunks: 'all', // async(默认, 只会提取异步加载模块的公共代码), initial(提取初始入口模块的公共代码), all(同时提取前两者),
minSize: 0, // 30000, // 大于30K会被抽离到公共模块
minChunks: 2, // 模块出现一次就会被抽离到公共模块中, 如果是1的话, 表示将所有的模块都提走, 针对pageA中, 如果只有自己引用jQuery, 那么会生成jQuery-vendor.js 的打包文件
maxAsyncRequests: 5, // 异步模块, 一次最多只能加载5个,
maxInitialRequests: 3, // 入口模块最多只能加载3个
name: true
}
}
场景
单页应用
单页应用 + 第三方依赖
多页应用 + 第三方依赖 + webpack生成代码 (webpack内置函数)
针对单入口的commonChunksPlugin = 并没有将公共部分打包, 只有针对多入口才会
多入口文件的时候

entry: {
pageA: path.resolve(__dirname, 'src/cmp', 'pageA'),
pageB: path.resolve(__dirname, 'src/cmp', 'pageB')
// vendor: ['lodash']
},

// webpack3
plugins: [
new webpack.optimize.CommonPluginsChun({
name: 'vendor',
minChunks: Infinity
})
// 公共模块打包的名字为vendor, entry中也有vendor, 所以会将webpack生成代码以及lodash打包进vendor中
]

// webpack4
splitChunks: {
chunks: 'all', // async(默认, 只会提取异步加载模块的公共代码), initial(提取初始入口模块的公共代码), all(同时提取前两者),
minSize: 30000, // 大于30K会被抽离到公共模块
// minChunks: 2, // 模块出现两次次就会被抽离到公共模块中
minChunks: Infinity, // 不需要在任何的地方重复
maxAsyncRequests: 5, // 异步模块, 一次最多只能加载5个,
maxInitialRequests: 3, // 入口模块最多只能加载3个
// name: 'common' // 打包出来公共模块的名称
name: 'vendor' // 打包出来公共模块的名称
}

// 1. 会将pageA, pageB中 公共使用的模块打包成进common.chunk.js (name:'common'的时候), 公共模块中包括webpack生成的代码

// 2. lodash只在pageA中使用, 次数为1, 但是minChunks: 2, 所以lodash只会被打包进pageA中

// 3. 在entry中添加 vendor: ['lodash'] 将公共库lodash单独打包, 在webpack4中将其打包进了公共common.chunk中, vendor中只有对lodash的引用

// 4. 如果想将lodash和webpack运行生成时代码以及公共代码打包到一起, minChunks改成Infinity, name:vendor, 将所有生成的文件引用都放到vendor中了

// 5. 保持第三方代码的纯净, 即将第三方代码和webpack分离开, webapck3添加plugins, webpack4添加runtimeChunk配置
// webpack3
new webpack.optimize.CommonPluginsChun({
name: 'manifest',
minChunks: Infinity
})
// 发现vendor和manifest处大部分代码是一样的可以, 可以改成
new webpack.optimize.CommonPluginsChun({
names: ['vendor','manifest'],
minChunks: Infinity
})

// webpack4
runtimeChunk: {
name: 'manifest'
},

// 结果是: 将webpack生成的代码打包到manifest中, 将lodash打包进vendor中, 将引用次数超过两次的打包进vendor中
6. 代码分割和懒加载
通过代码分割和懒加载, 让用户在尽可能的下载时间内加载想要的页面, 只看一个页面的时候, 下载所有的代码, 带宽浪费;
在webpack中, 代码分割和懒加载是一个概念, webpack会自动分割代码, 然后再把需要的代码加载进来, 不是通过配置来实现的, 通过改变写代码的方式来实现的, 当依赖一个模块的时候, 告诉webpack我们是需要懒加载或者代码切分, 通过两种方式来实现

webpack.methods
require.ensure() 接收四个参数
第一个参数dependencies, 加载进来的代码并不会执行, 在callback中引入, 这个时候才会去执行, 第三个参数errorBack, 第四个参数chunkName
如果浏览器不支持promise, 需要添加垫片
require.include 只有一个参数, 只引入进来, 但不执行
当两个子模块都引入了第三个模块, 可以将第三个模块放入父模块中, 这样动态加载子模块的时候, 父模块已经有了第三方模块, 不会在多余加载; 比如subPageA, subPageB都引入了moduleA, 但是moduleA不会被打包进父依赖, 所以可以使用include
ES2015 loader spec (动态import) stage-3
早起system.import
后来import方式 返回一个Promise
import().then
webpack import function 通过注释的方式来解决动态的chunkName以及加载模式
import(
/*webpackChunkName: async-chunk-name*/
/*webpackMode: lazy*/
moduleName
)
代码分割的场景

分离业务代码和第三方依赖 (提取公共代码中有涉及)
分离业务代码 和 业务公共代码 和 第三方依赖; 相比于上一个,将业务代码拆成两部分
分离首次加载 和 访问后加载的代码 (访问速度优化相关的) - LazyLoad - 提高首屏加载速度
// 0. 单入口pageA, 不做任何的优化 直接引入 subPageA, subPageB, lodash 会发现pageA非常大

// 1. 异步引入, 将lodash打包到vendor中
require.ensure('lodash', require => {
const _ = require('lodash')
_.join([1, 2, 3], 4)
console.log(_)
}, 'vendor')

// 2. pageA.js中修改
if (page === 'subPageA') {
// require([]) 参数是空数组的话, 里面的require的包还是会被异步打包
require.ensure(['./subPageA'], require => {
// 如果不require的话, 那么就不会执行subPageA中的代码块
const subPageA = require('./subPageA')
console.log(subPageA)
}, 'subPageA')
} else if (page === 'subPageB') {
require.ensure(['./subPageB'], require => {
const subPageB = require('./subPageB')
console.log(subPageB)
}, 'subPageB')
}
// 结果: moduleA分别在打包好的文件 subPageA.chunk.js 和 subPageB.chunk.js中, 公共部分moduleA没有被提取出来

// 3. 单entry有上述公共代码的情况的话, 使用inlcude的情况处理, 将module在父模块pageA.js提前引入, 但是并不运行
require.include('./moduleA')
// 结果: moduleA被打包进入了pageA.bundle.js中, 这样就完成了代码分割


// --- import 方案 -------------
/* 坑: import 只有在stage-0 或者 syntax-dynamic-import
yarn add babel-preset-stage-0 babel-plugin-syntax-dynamic-import --dev
.babelrc { "presets": ["stage-0"], "plugins": ["syntax-dynamic-import"] }
上述两种情况只使用一种即可
*/
// 在import的时候 代码实际上已经执行了
if (page) {
import(
/* webpackChunkName: "subPageA" */
/* webpackMode: "lazy" */
'./subPageC'
).then(subPageC => {
console.log(subPageC)
})
} else {
import(
/* webpackChunkName: 'subPageD' */
/* webpackMode: "lazy" */
'./subPageD'
)
}
async 在代码分割中如何使用, 即结合commonChunkPlugin

// webpack.plugin.lazy.cmp.js
entry: {
pageA: path.resolve(__dirname, 'src/lazy_cmp', 'pageA'),
pageB: path.resolve(__dirname, 'src/lazy', 'pageB'),
vendor: ['lodash']
}

// webpack3
plugins: [
new wepback.optimize.CommonsChunkPlugin({
// async 指定为true表示异步模块, 或者指定为 异步模块提取后的名称
async: 'async-common',
children: true, // 表示不仅仅是两个入口页面之间, 而且还是两个页面之间的子依赖中去寻找
minChunks: 2
}),
new wepback.optimize.CommonsChunkPlugin({
// lodash打包进入vendor中, manifest是webpack运行时代码
names: ['vendor', 'manifest'],
minChunks: Infinity
})
]

// webpack4
optimization: {
// webpack runtime 代码
runtimeChunk: {
name: 'manifest'
},
// 公共模块提取
splitChunks: {
chunks: 'all', // async(默认, 只会提取异步加载模块的公共代码), initial(提取初始入口模块的公共代码), all(同时提取前两者),
minSize: 30000, // 大于30K会被抽离到公共模块
// minChunks: 2, // 模块出现两次次就会被抽离到公共模块中
minChunks: Infinity, // 不需要在任何的地方重复
maxAsyncRequests: 5, // 异步模块, 一次最多只能加载5个,
maxInitialRequests: 3, // 入口模块最多只能加载3个
name: 'vendor' // 打包出来公共模块的名称
}
}

// pageA.js
import _ from 'lodash'

/ 1. 这里不再使用include, 因为会和pageA打包到一起, 这里的目的是 将其异步单独提取出来
// require.include('./moduleA')

const page = 'subPageA' // 在pageB中, 这里page='subPageB', 其余一样
if (page) {
import(
/* webpackChunkName: "subPageA" */
/* webpackMode: "lazy" */
'./subPageA'
).then(subPageA => {
console.log(subPageA)
})
} else {
import(
/* webpackChunkName: 'subPageB' */
/* webpackMode: "lazy" */
'./subPageB'
)
}

// 2. webpack3 结果: 将异步打包结果中subPageA和subPageB中的公共模块moduleA, 单独的提取到了async-common-pageA.chunk.js中
这里比较坑的困惑: commonsChunkPlugin参数说的不是很明确, 比如async, children, deepChildren, minChunk, 他们之间是有依赖忽视关系的

// 3. webpack4 结果: chunks:all, 结果是将多次引用的公共模块moduleA, lodash提取到了vendor.chunk中, 其余的和webpack3一样, 生成打包文件pageA.chunk, pageB.chunk(入口文件), subPageA.chunk, subPageB.chunk(异步单独提取), manifest.chunk(webpack-runtime单独提取)
5. 处理CSS
每一个模块都有自己的css文件, 在使用的时候将css样式引入
如何在webpack中引入css

style-loader 在页面中创建style标签, 标签里面的内容就是css内容
style-loader/url
style-loader/useable
css-loader 如何让js可以import一个css文件, 包装一层, 让js可以引入css文件

// index.js
import './css/base.css'

// webpack.config.style.js
{
test: /\.css$/,
use: [
{
loader: 'style-loader'
},
{
loader: 'css-loader'
}
]
}

// 将打包后的文件引入到index.html中
// 1. 结果: 在html中生成了style标签, 将base.css标签中的样式放到了style标签中

// 2. 生成link标签的形式 (不过用的比较少) 注意publicPath配置
use: [
{
loader: 'style-loader/url'
// loader: 'style-loader/useable'
},
{
loader: 'file-loader'
}
]
// 结果: style-loader/url 单独生成一份css文件 , 但是引入多个文件的时候, 会生成多个link标签, 会造成越多的网路请求

//3. style-loader/useable
import base from 'base.css'
import common from 'common.css'
var flag = false;
setInterval(function() {
if(flag) {
base.use()
} else {
base.ununse()
}
flag = !flag;
}, 2000)
// base.use() 样式插入到style标签中
// common.unuse() // 控制样式不被引用
// 结果: 没过2000ms, 页面中样式循环引用和删除
6. StyleLoader 配置
insertAt (插入位置)
insertInto(插入到DOM)
singleton (是否只使用一个style标签) 当css模块比较多的时候 会有很多css标签
transform (转化, 浏览器环境下, 插入页面之前)
transform: './src/style/css.transform.js'

// css.transform.js 文件内容

// 该函数并不是在打包的时候执行的,在运行webpack的时候, 是不行执行的
// 在style-loader 将样式插入到DOM中的时候 执行的, 运行的环境是在浏览器环境下, 可以拿到浏览器参数, window,UA

// 可以根据当前浏览器来对当前的css进行形变
module.exports = function(css) {
console.log(css)
console.log(window.innerWidth)
// 输出形变以后的css
if (window.innerWidth >= 768) {
css = css.replace('yellow', 'dodgerblue')
} else {
css = css.replace('yellow', 'orange')
}
return css;
}
- 针对每一次在index.js中引入的css文件都会执行上面的代码
CssLoader 配置参数

alias 解析的别名 将引入css的路径映射到其他地方
importLoader 取决于css处理后面是不是还有其他的loader (sass会使用到 @import)
minimize 是否压缩
modules 是否启用css-modules
打包出来的样式class 都变成一段随机字符串
CSS modules
:local 给定一个本地的样式 局部的样式
:global 给定一个全局样式
compose 继承一个样式
compose ... from path 引入一个样式 (尽量将composes放在前面, 这样可以控制引入顺序, 样式不会被覆盖) // base.css .box { composes: big-box from './common.css'; height: 200px; width: 100px; border-radius: 4px; background: #696969; }
localIdentName: '[[path]][name]_[local]--[hash:base64:5]' 控制生成的class类名

path代表引用css路径 name表示文件名称 local本地样式名称
配置less/sass
npm install less-loader less --save-dev
npm install sass-loader node-sass --save-dev
.header {
composes: font from './header.less'
}
提取css代码 - 提取公共代码 做缓存 (不提取的话, 将css代码打包到了js文件中)
extract-loader
ExtractTextWebpackPlugin
npm install extract-text-webpack-plugin --save-dev
// webpack3
var ExtractTextWebpackPlugin = require('ExtractTextWebpackPlugin)

module: {
rules: [

{
test: /\.less$/,
use: ExtractTextWebpackPlugin.extract({
fallback: {
// 告诉webpack, 当不提取的时候, 使用何种方式将其加载到页面中
loader: 'style-loader,
options: {
singleton: true,
// transform: ''
}
},
use: [
{loader: 'css-loader'}
{loader: 'less-loader'}
], // 定义我们继续处理的loader
})
}
]
},
plugins: [
new ExtractTextWebpackPlugin({
filename: '[name].min.css', // 提取出来的css的名称
// 将css-loader的option中的minimize打开

// allChunks 给插件指定一个范围, 指定提取css的范围
// 1. 设置为true 表示所有的引用的css文件都提取
// 2. 设置为false, 默认, 只会提取初始化的css(异步加载不认为是初始化)
allChunks:false,
})
]
// webpack3 结果: index.bundle.js app.min.css 但是打开index.html 并没有插入进去

// webpack4
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
// loader: 'file-loader'
options: {
minimize: process.env.NODE_ENV === 'production',
modules: true,
localIdentName: '[path]_[name]_[local]--[hash:base64:5]'
}
},
{
loader: 'less-loader'
}
]
}

plugins: [
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: '[name].css',
chunkFilename: '[id].css'
})
]
- 异步引入a.js文件, 在a.js文件中引入a.less
1. 针对allChunks为false的情况
- webpack3: 生成a.bundle.js文件, css文件被当成js的一个模块被打包处理, 将css放在js文件里面, 一起被提取; css代码切分的一种方式, 将初始化加载和动态加载区分开; 借助动态加载的代码区分, 也是css-in-js的一个概念
- weboack4: 生成moduleA.chunk.js 和moduleA.chunk.css文件, 在index.bundle.js 包括了对于modulA.js和module.css文件的引用
2. webpack4使用splitChunks配置
optimization: {
splitChunks: {
cacheGroups: {
styles: {
name: 'styles',
test: /\.scss|css$/,
chunks: 'all', // merge all the css chunk to one file
enforce: true
}
}
}
}
- 结果: 生成index.bundle.js style.chunk.js style.chunk.css 将所有的样式文件都打包进了style.chunk.css文件中, 但是需要手动添加到项目htm中
- question: 为什么这里不会运行? npm run extract
PostCss (Autoprefixer CSS-nano CSS-next) A tool for transforming Css With Javascript用js去转化css的一个工具
联系到上一节中的css.transform.js, 但是时机是不一样的, PostCss是打包的时期, css.transform是浏览器插入到style标签中的时候
postcss的强大, 理解成为一个处理css的工具
安装 npm install postcss postcss-loader autoprefixer cssnano postcss-cssnext --save-dev
autoprefixer: 帮助加上浏览器前缀
css-nano 帮助我们优化压缩css, 在postcss可以当做插件使用, css-loader就是用的css-nano做的压缩
css-next 使用未来的css新语法
css variables
custom selectors 自定义选择器
calc() 动态计算 ...
{
loader: 'postcss-loader',
options: {
// require进来的插件给postcss使用的
ident: 'postcss', // 表明接下来的插件是给postcss使用的
plugins: [
// require('autoprefixer')(),
// 两个一起用cssnext 会给出警告, 提示已经包含autoprefixer
require('postcss-cssnext')()
]
}
},
一旦涉及到浏览器兼容性问题的时候, 一定会有针对的浏览器兼容问题, 使用browserlist, 让所有的插件都公用一份browserlist
可以放在package.json里面
.browserlistrc 存入对浏览器的要求
postcss-import 插件 将@import的文件内容直接放入到当前的css文件中, 但是存过来之后要考虑相对文件路径的变化, 需要配合postcss-url来使用