这篇文章我们来好好说一下关于webpack前端工程化的一些环境搭建。
sourceMap
为什么要使用sourceMap?
- 为了开发时,快速的定位问题
- 线上代码,我们有时候也会开启 前端错误监控,快速的定位问题
源代码与打包后的代码的映射关系,通过sourceMap定位到源代码。
在dev(生产)模式中,默认开启,关闭的话 可以在配置⽂件⾥
devtool:"none" //关闭
关于devtool的说明:webpack.docschina.org/configurati…
- eval: 速度最快,使⽤eval包裹模块代码,
- source-map: 产⽣ .map ⽂件 外部产⽣ 错误代码的准确信息和位置
- cheap:较快,不包含列信息 Module:第三⽅模块,包含loader的sourcemap(⽐如jsx to js ,babel的sourcemap)
- inline: 将 .map 作为DataURI嵌⼊,不单独⽣成 .map ⽂件
我们先将打印的代码故意输入错误
开启source-map后即可精准定位错误:
并且会生成一个.map文件
如果考虑到安全性或者不想用户看到这个map文件
我们可以使用
devtool: 'inline-source-map',
这样.map文件会被打包到bundle文件中,不会单独生成出来,但文件会变大
关于devtool配置推荐
这里引用一段官网的话
其中一些值适用于开发环境,一些适用于生产环境。对于开发环境,通常希望更快速的 source map,需要添加到 bundle 中以增加体积为代价,但是对于生产环境,则希望更精准的 source map,需要从 bundle 中分离并独立存在。
devtool:"cheap-module-eval-source-map",// 开发环境配置
//线上不推荐开启
devtool:"cheap-module-source-map", // 线上⽣成配置
WebpackDevServer
WebpackDevServer会帮助我们启动一个本地的服务器进行开发编译,并且将打包后的文件保存在硬件中
-
开发提升效率
每次改完代码都需要重新打包⼀次,打开浏览器,刷新⼀次,很麻烦,我们可以安装使⽤ webpackdevserver来改善这块的体验
-
安装
npm install webpack-dev-server -D -
基础配置
修改下package.js
"scripts": { "server": "webpack-dev-server" },webpack.config.js中配置devServer字段
devServer: { contentBase: "./dist", //告诉服务器从哪里提供内容。 port: 8081,//监听端口 compress: true,//传入一个 boolean 值,通知 SERVER 是否启用 gzip。 open: true,//当open启用时,dev服务器将打开浏览器。 }, -
启动
npm run server -
本地mock,解决接口跨域
前后端分离
前端和后端是可以并⾏开发的, 前端会依赖后端的接⼝
先给接⼝⽂档,和接⼝联调⽇期的
我们前端就可以本地mock数据,不打断⾃⼰的开发节奏联调期间,前后端分离,直接获取数据会跨域,上线后我们使⽤nginx转发,开发期间,webpack就可 以搞定这件事 启动⼀个服务器,mock⼀个接⼝:
// npm i express -D
// 创建⼀个server.js 修改scripts "server":"node server.js"
//server.js
const express = require("express");
const app = express();
app.get("/api/info", (req, res) => {
res.json({
name: "webpack",
info: "webpack-dev-server",
});
});
app.listen(9094, () => {
console.log("启动成功");
});
//node server.js
http://localhost:9094/api/info
项⽬中安装axios⼯具
//npm i axios -D
//index.js
import axios from 'axios'
axios.get('http://localhost:9094/api/info').then(res=>{
console.log(res)
})
// 会有跨域问题
修改webpack.config.js 设置服务器代理
devServer: {
//配置代理
proxy:{
"/api":{
target:"http://localhost:9094"
}
}
},
现在可以修改index.js
axios.get('/api/info').then((res) => {
console.log(res);
})
// 不必加上前缀了
现在我们可以在8081的端口访问到9094的接口了
Hot Module Replacement (HMR:热模块替换)
文档
启动HMR
//devServer配置
devServer: {
contentBase: "./dist", //告诉服务器从哪里提供内容。
port: 8081,//监听端口
open: true,//当open启用时,dev服务器将打开浏览器。
hot: true,
//即便HMR不⽣效,浏览器也不⾃动刷新,就开启hotOnly
hotOnly: true
},
webpack.config.js文件头部引入webpack包
在插件配置处添加:
plugins: [
new htmlWebpackPlugin({
template: "./src/index.html",
filename: "index.html",
chunks: ["index"],
}),
new CleanWebpackPlugin(),
new webpack.HotModuleReplacementPlugin()
],
案例:css热模块更新
import css from "./index.css";
var btn = document.createElement("button");
btn.innerHTML = "新增";
document.body.appendChild(btn);
btn.onclick = function () {
var div = document.createElement("div");
div.innerHTML = "item";
document.body.appendChild(div);
};
//index.css
div:nth-of-type(odd) {
background: yellow;
}
具体实现效果:
tip:注意启动HMR后,css抽离会不⽣效,还有不⽀持contenthash,chunkhash
处理js模块HMR
需要使⽤module.hot.accept来观察模块更新 从⽽更新
案例:
//counter.js
function counter() {
var div = document.createElement("div");
div.setAttribute("id", "counter");
div.innerHTML = 1;
div.onclick = function () {
div.innerHTML = parseInt(div.innerHTML, 10) + 1;
};
document.body.appendChild(div);
}
export default counter;
//number.js
function number() {
var div = document.createElement("div");
div.setAttribute("id", "number");
div.innerHTML = 100;
document.body.appendChild(div);
}
export default number;
//index.js
import counter from "./counter";
import number from "./number";
counter();
number();
if (module.hot) {
module.hot.accept("./number.js", function () {
document.body.removeChild(document.getElementById("number"));
number();
});
}
需要开启hot属性才能实现js的HMR
效果:
Babel处理ES6
官⽅⽹站:babeljs.io/
中⽂⽹站:www.babeljs.cn/
Babel是JavaScript编译器,能将ES6代码转换成ES5代码,让我们开发过程中放⼼使⽤JS新特性⽽不⽤担 ⼼兼容性问题。并且还可以通过插件机制根据需求灵活的扩展。
Babel在执⾏编译的过程中,会从项⽬根⽬录下的 .babelrc JSON⽂件中读取配置。没有该⽂件会从 loader的options地⽅读取配置。
测试代码:
//index.js
const arr = [new Promise(() => {}), new Promise(() => {})];
arr.map(item => {
console.log(item);
});
安装
npm i babel-loader @babel/core @babel/preset-env -D
- bel-loader是webpack 与 babel的通信桥梁,不会做把es6转成es5的⼯作,这部分⼯作需要⽤到 @babel/preset-env来做
- @babel/preset-env⾥包含了es,6,7,8转es5的转换规则
Presets
概念:作为 Babel 插件的组合,帮助我们转换语法,分为2种,一种为转换插件,比如ES678转换成ES5,还有一种是解析插件,比如react和ts 将它们的语法进行解析转换
使用:
//webpack.config.js
{
test:/\.js$/,
use:{
loader:"babel-loader",
options:{
presets:["@babel/preset-env"]
}
}
},
现在我们再来看看我们的测试代码被解析成什么
我们可以看到箭头函数被转行了 但是Promise还没有被转换,这是为什么呢?
默认的Babel只⽀持let等⼀些基础的特性转换,Promise等⼀些还有转换过来
这个时候我们需要引入一个垫片来处理
什么是垫片?
引入一段js,这段js里面包含了ES6+的所有特性实现
这时候需要借助@babel/polyfill,把es的新特性都装进来,来弥补低版本浏览器中缺失的特性
使用:
npm install --save @babel/polyfill //安装
引入:
//index.js 顶部
import "@babel/polyfill";
按需加载,减少冗余
我们会发现打包的体积⼤了很多,这是因为polyfill默认会把所有特性注⼊进来,假如我想我⽤到的es6+,才会注⼊,没⽤到的不注⼊,从⽽减少打包的体积
修改Webpack.config.js
options: {
presets: [
[
"@babel/preset-env",
{
//targets 需要兼容的目标浏览器
targets: {
edge: "17",
firefox: "60",
chrome: "67",
safari: "11.1"
},
corejs: 2,//新版本需要指定核⼼库版本
useBuiltIns: "entry"//按需注⼊
}
]
]
}
参数说明:
targets: 描述您为项目支持/目标的环境。
corejs: 新版本需要指定核⼼库版本
useBuiltIns 选项是 babel 7 的新功能,这个选项告诉 babel 如何配置 @babel/polyfill 。
它有三个参数可以使用:
① entry: 需要在 webpack 的⼊⼝⽂件⾥ import "@babel/polyfill" ⼀ 次。 babel 会根据你的使⽤情况导⼊垫⽚,没有使⽤的功能不会被导⼊相应的垫⽚。
② usage: 不需要 import ,全⾃动检测,但是要安装 @babel/polyfill 。(试验阶段)
③ false: 如果你 import "@babel/polyfill" ,它不会排除掉没有使⽤的垫⽚,程序体积会庞⼤。(不推荐)
请注意: usage 的⾏为类似 babel-transform-runtime,不会造成全局污染,因此也会不会对类似 Array.prototype.includes() 进⾏ polyfill。
扩展:babel配置文件 .babelrc
我们可以将babel的options配置专门提取到一个文件中
//.babelrc
//bable options
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
//browsersList
"chrome": "67"
},
"corejs": 2,
"useBuiltIns": "usage"
}
],
]
}
//webpack.config.js
{
test: /\.js$/,
loader: "babel-loader"
}
配置React打包环境
用webpack实现React打包环境
首先我们要安装react的语法解析插件
npm install react react-dom --save //安装
编写react代码:
//index.js
import React, { Component } from "react";
import ReactDom from "react-dom";
class App extends Component {
render() {
return <div>hello world</div>;
}
}
ReactDom.render(<App />, document.getElementById("app"));
安装babel与react转换的插件:
并且要在.babelrc中引用
npm install --save-dev @babel/preset-react //安装
然后我们进行打包
实现效果
自定义Plugin
如何自己实现一个plugin
Plugin是webpack的功能扩展,目的在于完成loader无法完成的事情
//首先我们可以看一下hooks的列表
const webpack = require("webpack");
const config = require("../webpack.config.js");
const webpackConfig = require("../webpack.config.js");
const compiler = webpack(webpackConfig);
// compiler.hooks
Object.keys(compiler.hooks).forEach((hookName) => {
compiler.hooks[hookName].tap("xixixi", () => {
console.log(`run -> ${hookName}`);
});
});
compiler.run();
打印结果:
这里面就是所有的hooks,而我们自定义编写我们的plugin就得在特定的hooks里面实现逻辑
参考:compiler-hooks webpack.js.org/api/compile…
案例:实现一个text-webpack-plugin插件
//实现text-webpack-plugin插件
// 输出一个txt文件到bundle文件
class textWebpackPlugin {
//我们的插件如何钩入webpack的complier hook呢?
//apply函数
apply(compiler) {
// compiler.hooks
// emit 输出 asset 到 output 目录之前执行。
compiler.hooks.emit.tapAsync("textWebpackPlugin", (compilation, cb) => {
// console.log(compilation.assets);
compilation.assets["kaikeba.txt"] = {
source: function () {
return "hello kaikeba.txt";
},
size: function () {
return 1024;
},
};
cb();
});
}
}
module.exports = textWebpackPlugin;
打包后:
我们就实现了在打包后输出一个txt文件的功能
扩展:多页面打包方案
我们要对entry以及htmlWebpackPlugins进行动态提取让其自动生成
entry:{
index:"./src/index",
list:"./src/list",
detail:"./src/detail"
}
new htmlWebpackPlugins({
title: "index.html",
template: path.join(__dirname, "./src/index/index.html"),
filename:"index.html",
chunks:[index]
})
- 目录结构调整
-
src
- home
- index.js
- index.html
- list
- index.js
- index.html
- detail
- index.js
- index.html
2.使⽤ glob.sync 第三⽅库来匹配路径
npm i glob -D //安装 const glob = require("glob") //调用MPA多页面打包通用方案
//初始化setMPA函数 const setMPA = () => { const entry = {}; const htmlWebpackPlugins = []; return { entry, htmlWebpackPlugins }; }; const { entry, htmlWebpackPlugins } = setMPA(); //目的是将entry和htmlWebpackPlugins替换成动态的具体实现:
const setMpa = () => { const entry = {};//入口文件对象 const htmlWebpackPlugins = []; //htmlWebpackPlugin插件数组 // 提取src里面的所有文件夹的index.js const entryFiles = glob.sync(path.join(__dirname, "./src/*/index.js")) entryFiles.map((item, index) => { const entryFile = entryFiles[index] // 提取index.js const match = entryFile.match(/src\/(.*)\/index\.js$/); // 提取文件名称 const pageName = match && match[1]; console.log(pageName);// 这里我们就可以得到home/list/detail的名称了 // 动态生成entry entry[pageName] = entryFile //动态生成htmlWebpackPlugin htmlWebpackPlugins.push( new htmlWebpackPlugin({ template: path.join(__dirname, `./src/${pageName}/index.html`), filename: `${pageName}.html`, chunks: [pageName], }) ); }) return { entry, htmlWebpackPlugins }; }; const { entry, htmlWebpackPlugins } = setMpa()最后在配置中调用动态生成的entry, htmlWebpackPlugins
module.exports = { entry, output: { path: path.resolve(__dirname, "./dist"), filename: "[name].js" }, plugins: [ // ... ...htmlWebpackPlugins//展开数组 ] } - home
打包后的文件:
这样我们就实现了多页面通用打包方案,如果还有新的页面就按照home/list/detail等文件夹的结构新建一个文件夹即可