webpack前端工程环境搭建

298 阅读6分钟

这篇文章我们来好好说一下关于webpack前端工程化的一些环境搭建。

sourceMap

为什么要使用sourceMap?

  1. 为了开发时,快速的定位问题
  2. 线上代码,我们有时候也会开启 前端错误监控,快速的定位问题

源代码与打包后的代码的映射关系,通过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指南:webpack.docschina.org/guides/hot-…

启动HMR

//devServer配置
  devServer: {
        contentBase: "./dist", //告诉服务器从哪里提供内容。
        port: 8081,//监听端口
        open: true,//当open启用时,dev服务器将打开浏览器。
        hot: true,
        //即便HMR不⽣效,浏览器也不⾃动刷新,就开启hotOnly
        hotOnly: true
    },

webpack.config.js文件头部引入webpack包

引入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;
}

具体实现效果:

CSSHMR

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

效果:

JSHMR

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
  1. bel-loader是webpack 与 babel的通信桥梁,不会做把es6转成es5的⼯作,这部分⼯作需要⽤到 @babel/preset-env来做
  2. @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"]
            }
        }
},

现在我们再来看看我们的测试代码被解析成什么

babel解析

我们可以看到箭头函数被转行了 但是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 //安装

然后我们进行打包

实现效果

React解析

自定义Plugin

官⽅⽂档:webpack.js.org/contribute/…

如何自己实现一个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列表

这里面就是所有的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;

打包后:

打包后bundle文件

我们就实现了在打包后输出一个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]
})
  1. 目录结构调整
  • 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//展开数组
        ]
    }
    

打包后的文件:

MPA打包后的bundle

这样我们就实现了多页面通用打包方案,如果还有新的页面就按照home/list/detail等文件夹的结构新建一个文件夹即可