目标:
- 彻底学会webpack
- 理解webpack作用及原理
- 上手项目的打包过程配置
- 拥有工程化的前端思维
一、认识webpack
1. webpack是什么
webpack其实就是个模块化打包工具(module bundler)。
官网解释: 本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图会映射项目所需的每个模块,并生成一个或多个 bundle。
符合ES Module的模块引入方式
除了ES Module还有nodejs使用的CommonJS,还有CMD、AMD,webpack可以识别任何模块引入的语法。
CommonJS
// 导出
module.exports = xxx
// 使用
let xxx = require('./**.js')
模块化参考文档: webpack.js.org/concepts/mo…
webpack.docschina.org/concepts/mo…
模块化方法:webpack.docschina.org/api/module-…
3. 搭建webpack环境
- webpack是基于nodejs开发的模块打包工具,本质上是由node实现的。
- 新版本的nodejs会提升webpack的打包速度。保持webpack版本和node版本尽量的新就会提升打包速度。高版本的webpack会利用node中的特性来提升打包速度。
// 查看node版本号
node -v
// 查看npm版本号
npm -v
// 创建webpack-demo文件夹
mkdir webpack-demo
// 进入webpack-demo
cd webpack-demo
// 执行npm init,帮助我们以node规范的形式创建一个项目(或包文件),生成package.json
npm init
// package.json配置
"private": true, // 代表为私有仓库,不会被发布到npm的线上仓库中
"main": "index.js", // 此项目不会向外暴露,因此删除此配置。
webpack 安装
// 不推荐全局安装,最好单独项目安装
npm install webpack webpack-cli -g
// 查看webpack版本号
webpack -v
// 卸载webpack(全局)
npm uninstall webpack webpack-cli -g
// 在项目中安装webpack (此时node_modules中会有webpack依赖的包)
npm install webpack webpack-cli --save-dev (-D)
webpack -v // command not found (nodejs会默认全局寻找webpack,因此无法找到,因为未安装到全局)
// node提供了npx命令,会帮助我们在当前这个项目的node_modules文件夹里找webpack
npx webpack -v
// 查看webpack相关版本信息
npm info webpack
// 安装指定版本的webpack
npm install webpack@4.***
4. 使用webpack的配置文件
文件名称:webpack.config.js
const path = require('path')
module.exports = {
// 解释:打包index.js文件,输出到bundle文件夹下,生成的名字为bundle.js
entry: 'index.js', // 打包文件
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'bundle',)'' // 打包后的文件指定的文件夹(默认为dist文件夹),需要是绝对路径
}
}
// 运行 npx webpack ,查看编译结果
npx webpack
// 将webpack.config.js该名为webpackconfig.js
npx webpack --config webpackconfig.js // 代表webpack以webpackconfig.js为配置文件进行打包
// 一般项目上线配置 package.json 如下
"build": "webpack --config ./build/webpack.prod.js"
package.json
"scripts": {
"bundle": "webpack",
}
// 运行 和上面的运行效果一样,这样就不用npx了
npm run bundle
以上共三种运行webpack的方式
global 全局
webpack index.js
local 当前项目
npx webpack index.js
npm scripts
npm run bundle -> webpack
webpack-cli的作用:使我们在命令行中能正确运行webpack这个命令,不安装则无法使用npx webpack 或 webpack命令
参考文档: webpack.docschina.org/guides/gett…
5. webpack打包输出内容
执行 `npm run build` 在控制台输出
Hash:5*****1**2**a* // 每次打包对应唯一一个hash值
Version:webpack 4.**.* // 打包对应webpack版本
Time:236ms Built at:2019-8-11 12:21:21 // 本次打包耗时,及打包的时间
Asset Size Chunks Chunk Names //打包后的文件名,大小,id(每个文件都有自己的id值),入口js文件名
bundle.js 2.66 KiB 0 [emitted] main
Entrypoint main=bundle.js
[0]./src/index.js 1068 bytes {0}[built]
mode: 'production' // 'development' 开发环境则代码不会被压缩,'production'则会被压缩
二、webpack核心概念
三问:
-
webpack是什么
-
模块是什么
-
配置文件的作用是什么
1. loader是什么
webpack 可以使用 loader 来预处理文件。这允许你打包除 JavaScript 之外的任何静态资源。你可以使用 Node.js 来很简单地编写自己的 loader。
webpack不能识别非js结尾的模块,此时就需要在module中配置相应的loader
配置项为module
module.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: {
loader: 'file-loader' // 使用file-loader来处理图片
}
},
{
test: /\.(j|t)sx?$/,
exclude: NODE_ENV === 'development' ? /(node_modules)/ : [],
use: [
{
loader: 'ts-loader',
options: {
transpileOnly: true,
}
}
]
},
]
}
}
2. 使用loader打包静态资源(图片)
webpack.docschina.org/loaders/fil…
webpack.docschina.org/loaders/url…
// file-loader
module.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: {
loader: 'file-loader', // 使用file-loader来处理图片
option: {
// placeholder 占位符
name: '[name]_[hash:8].[ext]', // [name] 原始文件名字,[ext]原始文件后缀,生成和原文件名一样的图片
// 当遇到 png|jpe?g|gif|svg 这几种类型的文件的时候,输出到images文件夹里(或图片资源服务器)
outputPath: 'images/'
}
}
},
]
}
}
// url-loader 有一部分功能和 file-loader 一致
module.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: {
loader: 'url-loader',
option: {
name: '[name]_[hash:8].[ext]',
outputPath: 'images/',
limit: 2048, // 图片超过2048字节(2KB)时,则打包到images/目录下。否则转化为base64,打包到js中
}
}
},
]
}
}
编译之后,发现图片无法找到,而是以base64的形式打包到了js中
url-loader将图片转化为base64字符串
url-loader最佳使用方式,图片比较小的时候,比较合适,节省http请求,图片大时则不合适,实现这种最佳实践的参数就是limit
也就是说url-loader比file-loader功能更强大
3. 使用loader打包静态资源(scss、css)
- css-loader: 分析出多个css之间的关系,并将这些css合并为一个css文件
- style-loader:在拿到css-loader生成的css文件后,将这段内容挂载到html的head中,插入
<style>标签 - sass-loader: 解析scss文件
- postcss-loader:添加浏览厂商前缀,参考文档:webpack.docschina.org/loaders/pos…
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
]
},
]
}
}
写scss、less、styles等文件时,参考文档:webpack.docschina.org/loaders/sas…
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'sass-loader',
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: (e) => {
const _plugins = [
PostcssFlexbugsFixes,
PostcssPresetEnv({
autoprefixer: {
flexbox: 'no-2009'
},
stage: 3
})
]
// mobile 文件夹下面的文件 px 自动转 rem
if (e.context.includes('/mobile')) {
_plugins.push(adaptive({ remUnit: 72, autoRem: true }))
}
return _plugins
}
}
}
]
},
]
}
}
在webpack的配置里,loader是有先后执行顺序的,从下到上,从右到左。
npm install sass-loader node-sass webpack --save-dev
npm i -D postcss-loader
npm install autoprefixer -D
4. 使用loader打包静态资源(scss、css)
- css-loader 常用配置项
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
option: {
importLoaders: 2, // 多层import引入的scss文件,也要走sass-loader和postcss-loader
}
},
'sass-loader',
'postcss-loader',
]
},
]
}
}
如果直接以import './index.scss;'这种方式引入css文件,会作用于所有相同的类名,相当于全局类名,很容易出现样式冲突的问题。
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
option: {
importLoaders: 2, // 多层import引入的scss文件,也要走sass-loader和postcss-loader
modules: true,
}
},
'sass-loader',
'postcss-loader',
]
},
]
}
}
新增 modules: true,引入方式改为import style from './index.scss;',使用方式为style.className,如:style.header。这样样式就避免了耦合。
- 处理字体文件(打包svg、ttf、eot、ttf等)
module.exports = {
module: {
rules: [
{
test: /\.(eot|ttf|svg)$/,
use: {
loader: 'file-loader',
}
},
]
}
}
参考文档:
-
webpack.docschina.org/guides/asse… 讲解了css文件、图片、字体文件、数据文件(csv等)的打包方案,打包的好处和使用技巧。
-
loaders: webpack.docschina.org/loaders/
css-loader、style-loader、sass-loader、postcss-loader
5. 使用plugins让打包更便捷
plugin可以在webpack运行到某个时刻的时候,帮你做一些事情。很像生命周期函数。
当需要使用plugin时,搜一下就可以了,太多了...
参考文档: webpack.docschina.org/plugins/htm…
配置文档: github.com/jantimon/ht…
HtmlWebpackPlugin作用:会在打包结束后,自动生成一个html文件,并把打包生成的js自动引入到这个html中。
npm install --save-dev html-webpack-plugin
var HtmlWebpackPlugin = require('html-webpack-plugin');
var path = require('path');
module.exports = {
entry: 'index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'index_bundle.js'
},
plugins: [new HtmlWebpackPlugin({
template: 'src/index.html', // 以src下的index.html作为模板生成html
})],
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'), // 输出到dist文件夹下
}
}
在输入html、css、js等文件的时候,会生成在同一级目录下,但如果你想将html文件单独放在某个问题夹下,如下图所示:
dist/
index.html
assets/
index.js
index.css
我们可以做如下配置:new HtmlWebpackPlugin({template: 'index.html', filename: "../index.html"}), 原文链接:github.com/jantimon/ht…
通过filename配置项的../将html文件提出到上一级目录中。
clean-webpack-plugin:每次重新打包的时候,自动删除上一次打包的dist文件中的内容。
npm install clean-webpack-plugin -D
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin');
var path = require('path');
module.exports = {
entry: 'index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'index_bundle.js'
},
plugins: [new HtmlWebpackPlugin({
template: 'src/index.html', // 以src下的index.html作为模板生成html
}),
new CleanWebpackPlugin(['dist']) // 删除dist文件夹下的内容
],
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'), // 输出到dist文件夹下
}
}
loader与plugin的区别:loader是用来预处理文件的,而plugin是在webpack运行到某个时刻的时候,会帮你做一些事情。
6. Entry与Output的基础配置
module.exports = {
entry: 'index.js', // 默认输出是main.js,在output中filename修改为bundle.js
output: {
path: path.resolve(__dirname, './dist'),
filename: 'index_bundle.js'
},
plugins: [new HtmlWebpackPlugin({
template: 'src/index.html', // 以src下的index.html作为模板生成html
}),
new CleanWebpackPlugin(['dist']) // 删除dist文件夹下的内容
],
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'), // 输出到dist文件夹下
}
}
多入口文件时,我们可以在output中用占位符来解决,如:[name] [hash]等
module.exports = {
entry: {
main: './src/index.js',
sub: './src/index.js',
},
output: {
path: path.resolve(__dirname, './dist'),
filename: 'index_bundle.js'
},
plugins: [new HtmlWebpackPlugin({
template: 'src/index.html', // 以src下的index.html作为模板生成html
}),
new CleanWebpackPlugin(['dist']) // 删除dist文件夹下的内容
],
output: {
publicPath: 'https://cdn.com', // 对生成文件添加前缀,js发布到cdn时可以使用
filename: '[name].js',
path: path.resolve(__dirname, 'dist'), // 输出到dist文件夹下
}
}
以上写法解决了多入口,都合并到index.html的问题,在html中生成两个script标签。还有多入口,多js,打到多html中的问题需要解决。
参考文档:
- webpack.docschina.org/concepts/ou…
- webpack.docschina.org/configurati…
- webpack.docschina.org/configurati…
- webpack.docschina.org/guides/outp… 这个文档需要认真阅读
7. SourceMap的配置
sourceMap是一个映射关系,他知道打包出错代码的位置对应的实际的开发中源代码错误的位置。做源代码和目标生成代码的映射。
参考文档: webpack.docschina.org/configurati…
devtool: 'source-map ' // 配置之后,打包后的代码中会自动生成 **.js.map 文件,表示文件映射关系。其实是一个VLQ的编码集合,对源代码和打包代码做了一个映射关系。
devtool: 'inline-source-map ' // inline 将**.js.map以dataURL的方式直接写在打包后的js中,不会再有**.js.map,精确到会告诉你哪一行的哪一列出错了,这样的映射会非常耗费性能。
devtool: 'cheap-inline-source-map ' // cheap 则会只告诉哪一行出错了,打包性能会得到提升,可以从devtool的对照表看出构建速度的变化。只要有cheap的都会有较大变化。
devtool: 'cheap-module-inline-source-map' // module还管第三方引入模块等的错误
devtool: 'eval' // 打包最快的方法,eval的js执行形式来生成sourceMap的对应关系,性能最好,但对复杂代码,提示并不全面。
// 最佳实践(开发)
devtool: 'cheap-module-eval-source-map'
// 最佳实践(线上)
devtool: 'cheap-module-source-map'
sourceMap的原理是什么?
重要参考文档:
segmentfault.com/a/119000000… www.html5rocks.com/en/tutorial… www.ruanyifeng.com/blog/2013/0… www.youtube.com/watch?v=NkV…
8. 使用WebpackDevServer提升开发效率
参考文档:webpack.docschina.org/configurati…
// package.json
"scripts": {
"dev": "webpack --watch" // --watch监听打包文件,只要发生变化,就会重新打包。只要有这个参数就生效。
}
但我们想要更丰富的功能:执行npm run dev就会自动打包,并自动打开浏览器,同时可以模拟一些服务器上的特性,此时就要借助WebpackDevServer来实现。
devServer:{
contentBase: './dist' // 服务器起在哪个文件夹下。WebpackDevServer会帮助我们在这个文件夹下起一个服务器
}
npm install webpack-dev-server -D
// package.json
"scripts": {
"dev": "webpack --watch"
"start": "webpack-dev-server" // 自动重新刷新浏览器
}
npm run start
细化配置
参考文档:
webpack.docschina.org/configurati… webpack.docschina.org/configurati…
devServer:{
port: 8080, // 默认8080
contentBase: './dist',
open: true, // 自动打开浏览器,并访问服务器地址。 file协议不行,不能发送ajax请求
proxy: {
'./api': 'http://localhost:3000' // 用户访问 /api 这个路径会被转发到 http://localhost:3000,支持跨域代理
}
}
扩充
// package.json
"scripts": {
"dev": "webpack --watch",
"start": "webpack-dev-server" ,
"middleware": "node server.js" // 自己写一个服务器
}
server.js
npm install express webpack-dev-middle-ware -D
const express = require('express')
const webpack = require('webpack')
const webpacDevMiddelware = require('webpack-dev-middle-ware')
const config = require('./webpack.config.js')
// 在node中使用webpack
const complier = webpack(config) // 做编译,返回一个编译器,可随时对代码进行编译
const app = express()
app.use(webpacDevMiddelware(complier, {
publicPath: config.output.publicPath // 打包生成路径
}))
app.listen(3000, () => {
console.log('server is running')
})
运行
npm run server
访问
localhost:3000
module.exports = {
output: {
publicPath: '/', // 对生成文件添加前缀
filename: '[name].js',
path: path.resolve(__dirname, 'dist'), // 输出到dist文件夹下
}
}
参考文档:
webpack.docschina.org/api/cli/ 文档中怎么写一些执行语法
webpack <entry> [<entry>] -o <output>
webpack index.js -o bundle.js // -o 命令:入口index.js,出口bundle.js,-o为output的简写
webpack.docschina.org/api/node/ 想在nodejs中运行webpack做一些事情,具体参数可参考此文档
本节参考文档:
webpack.docschina.org/guides/deve…
webpack.docschina.org/configurati…
9. Hot Module Replacement 热模块更新
热模块替换HMR,只更新你修改的内容,不刷新页面。从google的devTool来查看是否文件刷新。
// package.json
"scripts": {
"start": "webpack-dev-server" ,
}
webpack-dev-server打包后的dist中的内容放到了内存中,加快访问速度
const webpack = require('webpack')
module.exports = {
devServer:{
port: 8080, // 默认8080
contentBase: './dist',
open: true,
hot: true, // 让webpack-dev-server开启Hot Module Replacement功能
hotOnly: true, // 即使HMR功能没有生效,也不让浏览器自动刷新,
},
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
'postcss-loader',
]
},
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html',
}),
new CleanWebpackPlugin(['dist']), // 开发环境不需要此配置
new webpack.HotModuleReplacementPlugin() // 使用webpack插件,可用于开发环境
],
}
作用:
- 此时修改css之后,不会影响js的变更,只会重新替换css。
- 修改某个模块之后,只更新这个模块的代码,其他模块不受影响。
先判断当前项目是否开启HMR
// 只要***文件发生了变化,就会执行f()
if (module.hot) { // css-loader已经帮你实现了,因此自己不用写。VUE/REACT也已经帮助你实现了。
module.hot.accept('./***', () => {
f() // 要更新的方法
})
}
判断判断当前项目是否开启HMR,css-loader已经帮你实现了,因此自己不用写。VUE/REACT也已经帮助你实现了。因此不用写上面的这段代码。
参考文档:
- webpack.docschina.org/guides/hot-…
- webpack.docschina.org/api/hot-mod…
- webpack.docschina.org/concepts/ho… HMR底层webpack实现原理
10. 使用Babel处理ES6语法
babel官网: babeljs.io/
npm install --save-dev babel-loader @babel/core // @babel/core是babel的核心库,让babel识别js中的内容,把js转换为AST抽象语法树,再把抽象语法树编译转化为新的语法出来。
module: {
rules: [
{
test: /\.js$/, // 如果为js文件,则使用babel-loader分析js语法
exclude: /node_modules/, // node_modules 文件夹除外
loader: "babel-loader",
option: {
"presets": ["@babel/preset-env"] // 可以这样配置也可以在.babelrc下配置
}
}
]
}
npm install @babel/preset-env --save-dev // @babel/preset-env,其中babel-loader只是和webpack通信的一个桥梁,babel-loader并不会转化语法,还需要借助 @babel/preset-env来翻译相应语法。
.babelrc
{
"presets": ["@babel/preset-env"]
}
此时只做了语法翻译,但这样还缺少对象或函数,如promise,在低版本浏览器还是没有的,此时还需要把这些缺失的对象或函数补充到低版本浏览器中,此时需要借助babel-polyfill来实现。babel-polyfill实际上就是在window对象上挂载了方法,绑定了全局变量。
babeljs.io/docs/en/bab…
npm install --save @babel/polyfill
@babel/polyfill 会让你打包生成的包变的巨大无比。
import "@babel/polyfill"; // 这种写法会把所有的语法都打包的包里,而不是用了哪些,打包哪些。
module: {
rules: [
{
test: /\.js$/, // 如果为js文件,则使用babel-loader分析js语法
exclude: /node_modules/, // node_modules 文件夹除外
loader: 'babel-loader',
option: {
"presets": [['@babel/preset-env', { // 这种是解决业务代码的使用场景,但要写组件库的话就不适合了,这种写法会污染全局环境,因此就换成了下面的runtime的形式
"targets": { // 项目打包运行的浏览器,符合以下这些浏览器版本以上就可以,如:"chrome": "67",是chrome的67版本以上
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1",
},
useBuiltIns: 'usage' // 根据业务代码来觉得加哪些polyfill,打包就会小很多
}]]
}
}
]
}
npm install --save-dev @babel/plugin-transform-runtime
module: {
rules: [
{
test: /\.js$/, // 如果为js文件,则使用babel-loader分析js语法
exclude: /node_modules/, // node_modules 文件夹除外
loader: 'babel-loader',
option: {
"plugins": [
[
"@babel/plugin-transform-runtime", // 使用plugin的好处,可以避免使用presets时污染全局环境,会以闭包的形式注入或引入对应内容,不会污染环境,在写类库是会更好。
{
"absoluteRuntime": false,
"corejs": 2,
"helpers": true,
"regenerator": true,
"useESModules": false,
"version": "7.0.0-beta.0"
}
]
]
}
}
]
}
npm install --save @babel/runtime-corejs2
babel文档已有变更
import "@babel/polyfill";
在webpack4等较新的版本上,如果在webpack.config.js中配置了babel-loader相关内容,那么在.babelrc文件下,如果对@babel/present-env设置了useBuiltIns: 'usage',这样代码就不需要引入import "@babel/polyfill",会被自动引入,编译时的提示信息如下:
when setting `useBuiltIns: 'usage'`,polyfills are automatically imported when needed.
please remove the `import "@babel/polyfill"` call or use `useBuiltIns: 'usage'` instead.
11. webpack实现对React框架代码的打包
参考文档: babeljs.io/docs/en/bab…
npm install --save-dev @babel/preset-react // 解析jsx语法
.babelrc
执行顺序:从下往上,从右往左
{
"presets": [
['@babel/preset-env', {
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1",
},
useBuiltIns: 'usage'
}
],
"@babel/preset-react"
]
}