webpack 详细教程:带你深度解读Webpack系列 进阶篇
webpack 详细教程:带你深度解读Webpack系列 进阶篇//webpack.config.js const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: { index: './src/index.js' login: './src/login.js' } output: { path: path.resolve(__dirname 'dist') filename: '[name].[hash:6].js' } //... plugins: [
webpack 遇到 import(****) 这样的语法的时候,会这样处理:
- 因为入口新生成一个 Chunk
- 当代码执行到 import 所在的语句时,才会加载该 Chunk 所对应的文件(如这里的1.bundle.8bf4dc.js)
大家可以在浏览器中的控制台中,在 Network 的 Tab页 查看文件加载的情况,只有点击之后,才会加载对应的 JS。
5.热更新
- 首先配置 devServer 的 hot 为 true
- 并且在 plugins 中增加 new webpack.HotModuleReplacementPlugin()
//webpack.config.js
const webpack = require('webpack');
module.exports = {
//....
devServer: {
hot: true
}
plugins: [
new webpack.HotModuleReplacementPlugin() //热更新插件
]
}
我们配置了 HotModuleReplacementPlugin 之后,会发现,此时我们修改代码,仍然是整个页面都会刷新。不希望整个页面都刷新,还需要修改入口文件:
- 在入口文件中新增:
if(module && module.hot) {
module.hot.accept()
}
此时,再修改代码,不会造成整个页面的刷新。
6.多页应用打包
有时,我们的应用不一定是一个单页应用,而是一个多页应用,那么如何使用 webpack 进行打包呢。为了生成目录看起来清晰,不生成单独的 map 文件。
//webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js'
login: './src/login.js'
}
output: {
path: path.resolve(__dirname 'dist')
filename: '[name].[hash:6].js'
}
//...
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
filename: 'index.html' //打包后的文件名
})
new HtmlWebpackPlugin({
template: './public/login.html'
filename: 'login.html' //打包后的文件名
})
]
}
如果需要配置多个 HtmlWebpackPlugin,那么 filename 字段不可缺省,否则默认生成的都是 index.html,如果你希望 html 的文件名中也带有 hash,那么直接修改 fliename 字段即可,例如: filename: 'login.[hash:6].html'。
生成目录如下:
.
├── dist
│ ├── 2.463ccf.js
│ ├── assets
│ │ └── thor_e09b5c.jpeg
│ ├── css
│ │ ├── index.css
│ │ └── login.css
│ ├── index.463ccf.js
│ ├── index.html
│ ├── js
│ │ └── base.js
│ ├── login.463ccf.js
│ └── login.html
看起来,似乎是OK了,不过呢,查看 index.html 和 login.html 会发现,都同时引入了 index.f7d21a.js 和 login.f7d21a.js,通常这不是我们想要的,我们希望,index.html 中只引入 index.f7d21a.js,login.html 只引入 login.f7d21a.js。
HtmlWebpackPlugin 提供了一个 chunks 的参数,可以接受一个数组,配置此参数仅会将数组中指定的js引入到html文件中,此外,如果你需要引入多个JS文件,仅有少数不想引入,还可以指定 excludeChunks 参数,它接受一个数组。
//webpack.config.js
module.exports = {
//...
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
filename: 'index.html' //打包后的文件名
chunks: ['index']
})
new HtmlWebpackPlugin({
template: './public/login.html'
filename: 'login.html' //打包后的文件名
chunks: ['login']
})
]
}
执行 npm run build,可以看到 index.html 中仅引入了 index 的 JS 文件,而 login.html 中也仅引入了 login 的 JS 文件,符合我们的预期。
7.resolve 配置
resolve 配置 webpack 如何寻找模块所对应的文件。webpack 内置 JavaScript 模块化语法解析功能,默认会采用模块化标准里约定好的规则去寻找,但你可以根据自己的需要修改默认的规则。
- modules
resolve.modules 配置 webpack 去哪些目录下寻找第三方模块,默认情况下,只会去 node_modules 下寻找,如果你我们项目中某个文件夹下的模块经常被导入,不希望写很长的路径,那么就可以通过配置 resolve.modules 来简化。
//webpack.config.js
module.exports = {
//....
resolve: {
modules: ['./src/components' 'node_modules'] //从左到右依次查找
}
}
这样配置之后,我们 import Dialog from 'dialog',会去寻找 ./src/components/dialog,不再需要使用相对路径导入。如果在 ./src/components 下找不到的话,就会到 node_modules 下寻找。
- alias
resolve.alias 配置项通过别名把原导入路径映射成一个新的导入路径,例如:
//webpack.config.js
module.exports = {
//....
resolve: {
alias: {
'react-native': '@my/react-native-web' //这个包名是我随便写的哈
}
}
}
例如,我们有一个依赖 @my/react-native-web 可以实现 react-native 转 web。我们代码一般下面这样:
import { View ListView StyleSheet Animated } from 'react-native';
配置了别名之后,再转 web 时,会从 @my/react-native-web 寻找对应的依赖。
当然啦,如果某个依赖的名字太长了,你也可以给它配置一个短一点的别名,这样用起来比较爽,尤其是带有 scope 的包。
- extensions
适配多端的项目中,可能会出现 .web.js .wx.js,例如在转web的项目中,我们希望首先找 .web.js,如果没有,再找 .js。我们可以这样配置:
//webpack.config.js
module.exports = {
//....
resolve: {
extensions: ['web.js' '.js'] //当然,你还可以配置 .json .css
}
}
首先寻找 ../dialog.web.js ,如果不存在的话,再寻找 ../dialog.js。这在适配多端的代码中非常有用,否则,你就需要根据不同的平台去引入文件(以牺牲了速度为代价)。
import dialog from '../dialog';
当然,配置 extensions,我们就可以缺省文件后缀,在导入语句没带文件后缀时,会自动带上extensions 中配置的后缀后,去尝试访问文件是否存在,因此要将高频的后缀放在前面,并且数组不要太长,减少尝试次数。如果没有配置 extensions,默认只会找对对应的js文件。
- enforceExtension
如果配置了 resolve.enforceExtension 为 true,那么导入语句不能缺省文件后缀。
- mainFields
有一些第三方模块会提供多份代码,例如 bootstrap,可以查看 bootstrap 的 package.json 文件:
{
"style": "dist/css/bootstrap.css"
"sass": "scss/bootstrap.scss"
"main": "dist/js/bootstrap"
}
复制代码
resolve.mainFields 默认配置是 ['browser' 'main'],即首先找对应依赖 package.json 中的 brower 字段,如果没有,找 main 字段。
如:import 'bootstrap' 默认情况下,找的是对应的依赖的 package.json 的 main 字段指定的文件,即 dist/js/bootstrap。
假设我们希望,import 'bootsrap' 默认去找 css 文件的话,可以配置 resolve.mainFields 问:
//webpack.config.js
module.exports = {
//....
resolve: {
mainFields: ['style' 'main']
}
}
8.区分不同的环境
目前为止我们 webpack 的配置,都定义在了 webpack.config.js 中,对于需要区分是开发环境还是生产环境的情况,我们根据 process.env.NODE_ENV 去进行了区分配置,但是配置文件中如果有多处需要区分环境的配置,这种显然不是一个好办法。
更好的做法是创建多个配置文件,如: webpack.base.js、webpack.dev.js、webpack.prod.js。
- webpack.base.js 定义公共的配置
- webpack.dev.js 定义开发环境的配置
- webpack.prod.js 定义生产环境的配置
webpack-merge 专为 webpack 设计,提供了一个 merge 函数,用于连接数组,合并对象。
npm install webpack-merge -D
const merge = require('webpack-merge');
merge({
devtool: 'cheap-module-eval-source-map'
module: {
rules: [
{a: 1}
]
}
plugins: [1 2 3]
} {
devtool: 'none'
mode: "production"
module: {
rules: [
{a: 2}
{b: 1}
]
}
plugins: [4 5 6]
});
//合并后的结果为
{
devtool: 'none'
mode: "production"
module: {
rules: [
{a: 1}
{a: 2}
{b: 1}
]
}
plugins: [1 2 3 4 5 6]
}
webpack.config.base.js 中是通用的 webpack 配置,以 webpack.config.dev.js 为例,如下:
//webpack.config.dev.js
const merge = require('webpack-merge');
const baseWebpackConfig = require('./webpack.config.base');
module.exports = merge(baseWebpackConfig {
mode: 'development'
//...其它的一些配置
});
然后修改我们的 packag.json,指定对应的 config 文件:
//package.json
{
"scripts": {
"dev": "cross-env NODE_ENV=development webpack-dev-server --config=webpack.config.dev.js"
"build": "cross-env NODE_ENV=production webpack --config=webpack.config.prod.js"
}
}
你可以使用 merge 合并,也可以使用 merge.smart 合并,merge.smart 在合并loader时,会将同一匹配规则的进行合并,webpack-merge 的说明文档中给出了详细的示例。
9.定义环境变量
很多时候,我们在开发环境中会使用预发环境或者本地的域名,生产环境中使用线上域名,我们可以在 webpack 定义环境变量,然后在代码中使用。
使用 webpack 内置插件 DefinePlugin 来定义环境变量。
DefinePlugin 中的每个键,是一个标识符.
- 如果 value 是一个字符串,会被当做 code 片段
- 如果 value 不是一个字符串,会被 stri.jpgy
- 如果 value 是一个对象,正常对象定义即可
- 如果 key 中有 typeof,它只针对 typeof 调用定义
//webpack.config.dev.js
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.DefinePlugin({
DEV: JSON.stri.jpgy('dev') //字符串
FLAG: 'true' //FLAG 是个布尔类型
})
]
}
//index.js
if(DEV === 'dev') {
//开发环境
}else {
//生产环境
}
10.利用webpack解决跨域问题
假设前端在 3000 端口,服务端在 4000 端口,我们通过 webpack 配置的方式去实现跨域。
首先,我们本地创建一个 server.js:
let express = require('express');
let app = express();
app.get('/api/user' (req res) => {
res.json({name: '刘小夕'});
});
app.listen(4000);
执行代码(run code),现在我们可以在浏览器中访问到此接口: http://localhost:4000/api/user。
在 index.js 中请求 /api/user,修改 index.js 如下:
//需要将 localhost:3000 转发到 localhost:4000(服务端) 端口
fetch("/api/user")
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.log(err));
我们希望通过配置代理的方式,去访问 4000 的接口。
配置代理
修改 webpack 配置:
//webpack.config.js
module.exports = {
//...
devServer: {
proxy: {
"/api": "http://localhost:4000"
}
}
}
重新执行 npm run dev,可以看到控制台打印出来了 {name: "刘小夕"},实现了跨域。
大多情况,后端提供的接口并不包含 /api,即:/user,/info、/list 等,配置代理时,我们不可能罗列出每一个api。
修改我们的服务端代码,并重新执行。
//server.js
let express = require('express');
let app = express();
app.get('/user' (req res) => {
res.json({name: '刘小夕'});
});
app.listen(4000);
尽管后端的接口并不包含 /api,我们在请求后端接口时,仍然以 /api 开头,在配置代理时,去掉 /api,修改配置:
//webpack.config.js
module.exports = {
//...
devServer: {
proxy: {
'/api': {
target: 'http://localhost:4000'
pathRewrite: {
'/api': ''
}
}
}
}
}
11.前端模拟数据
简单数据模拟
module.exports = {
devServer: {
before(app) {
app.get('/user' (req res) => {
res.json({name: '刘小夕'})
})
}
}
}
在 src/index.js 中直接请求 /user 接口。
fetch("user")
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.log(err));
使用 mocker-api mock 数据接口
mocker-api 为 REST API 创建模拟 API。在没有实际 REST API 服务器的情况下测试应用程序时,它会很有用。
- 安装 mocker-api:
npm install mocker-api -D
- 在项目中新建mock文件夹,新建 mocker.js文件,文件如下:
module.exports = {
'GET /user': {name: '刘小夕'}
'POST /login/account': (req res) => {
const { password username } = req.body
if (password === '888888' && username === 'admin') {
return res.send({
status: 'ok'
code: 0
token: 'sdfsdfsdfdsf'
data: { id: 1 name: '刘小夕' }
})
} else {
return res.send({ status: 'error' code: 403 })
}
}
}
- 修改 webpack.config.base.js:
const apiMocker = require('mocker-api');
module.export = {
//...
devServer: {
before(app){
apiMocker(app path.resolve('./mock/mocker.js'))
}
}
}
这样,我们就可以直接在代码中像请求后端接口一样对mock数据进行请求。
- 重启 npm run dev,可以看到,控制台成功打印出来 {name:'刘小夕'}
- 我们在修改下 src/index.js,检查下POST接口是否成功
//src/index.js
fetch("/login/account" {
method: "POST"
headers: {
'Accept': 'application/json'
'Content-Type': 'application/json'
}
body: JSON.stri.jpgy({
username: "admin"
password: "888888"
})
})
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.log(err));
可以在控制台中看到接口返回的成功的数据。
进阶篇就到这里结束啦,最后一篇是优化篇,准备好小板凳和瓜子来约哦~~