webpack npm打包配置(react版本)

2,873 阅读12分钟

这篇文章是以打包react插件的形式,介绍webpack的一些配置信息。如果写简单插件的话还是推荐使用rollup,但是可以用写插件的形式去学习一下webpack的一些东西。(适用于初中级webpack学者)

1.安装node和npm,新建文件夹,在文件夹中执行npm init命令,一直回车生成一个package.json文件如下:

{
  "name": "cobrandcard",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

package.json文件的作用: 1)只要项目中使用了npm,项目根目录中都会有一个packgae.json文件,它可以手动创建,也可以通过执行npm init 命令来生成。 2)一般该文件中会记录项目的配置信息,版本、项目名称、许可证和作者等等,也会记录所需要的各种模块的依赖,包括开发依赖和执行依赖,还有scripts字段(稍后解释) 3)当执行npm install命令时,npm就会根据该文件中dependencies 和devDependencies 中的模块来下载相应的项目依赖。

问题:dependencies 和devDependencies 的区别? 一个是生产依赖,一个是开发依赖,生产依赖就是程序中用到的包,比如程序运行需要用到react,别人用你的插件的时候需要安装react才能运行程序,所以用插件的时候会下载生产环境dependencies的包。

2.在根目录下新建src文件夹,存放自己的代码片段。

index.html

<html>

<head>
  <meta charset='UTF-8'>
</head>

<body>
  <div id='app'></div>
  <script src='.src/index.js'></script>
</body>

</html>

src/index.js

window.document.getElementById('app').innerText = 'hello, world!'

打开index.html文件,就可以看到浏览器中显示hello world了。

3.利用webpack打包代码

1)安装webpack和webpack-cli 安装:npm install webpack webpack-cli --save-dev webpack-cli作用:可以在命令行使用webpack命令

在根目录使用npx webpack,默认打包src目录下的index.js文件,并在根目录生成一个dist文件夹,存放打包后的代码。

2)手动配置webpack打包项 默认的webpack配置文件为:webpack.config.js或者webpackfile.js 在根目录下新建webpack.config.js:

const path = require('path');

module.exports = {
  mode:'development',
  entry:'./src/index.js',
  output:{
    filename:'index.js',
    path:path.resolve(__dirname,'dist')
  }

命令行运行:npx webpack,发现根目录多了dist文件夹(打包后的文件)

利用package.json文件scripts脚本命令,快速执行打包命令

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build":"webpack --config webpack.config.js"
  },

npm run build执行相同的打包命令。

4.本地服务器webpack-dev-server

目的:在本地调试代码,不用手动打开index.html文件,不用手动刷新页面。 webpack-dev-server能帮我们做什么? 作用:比如在使用webpack-dev-server之前,我每修改一处代码,都需要刷新页面才能看到,如果想看打包后的代码是否显示正常,还需要重新打包,再刷新页面,才能看到,而且需要我们手动运行html文件。 在使用webpack-dev-server之后,它会帮我们在本地起一个简单的服务器,并且可以实现监测代码实时更新的功能,在配置热更新之后,还可以实现不刷新页面的情况下进行局部更新。 安装:npm install webpack-dev-server --save-dev 配置服务器信息:

devServer: {
    // 根目录下dist为基本目录
    contentBase: path.join(__dirname, "dist"),
    // 自动压缩代码
    compress: true,
    // 服务端口为1208
    port: 1208,
    // 自动打开浏览器
    open: true,
    host: "dev.jd.com",
    // publicPath: "/assets/",
    hot: true,
  },

配置script信息:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config webpack.config.js",
    "start":"webpack-dev-server"
  },

重新打包之后,运行npm start,发现并没有看到页面打印hello world,看到的是打包之后的文件夹,所以我们需要在dist文件夹下创建一个html文件。 需要安装html-webpack-plugin插件,来产生html文件。

const htmlWebpackPlugin = require('html-webpack-plugin')
plugins:[
    //数组 存放所有webpack插件
    new htmlWebpackPlugin({
      template:'./index.html',
      filename:'index.html'
    })
  ]

执行打包npm run build操作,发现在dist文件夹里面生成了html文件

<html>

<head>
  <meta charset='UTF-8'>
</head>

<body>
  <div id='app'></div>
<script src="main.js"></script></body>

</html>

npm start :本地服务器开启,浏览器看到hello world。

5.热更新(HMR)

之前的webpack-dev-server的配置只是实现了监听文件变化、自动打包,实时刷新页面的功能,但是我们如果想要实现热更新的话,还需要加上新的配置项,hot的配置项表示开启热更新,开启热更新的配置又需要用到hotModuleReplacementPlugin插件。 在这里插入图片描述 热更新配置之后,再修改css样式,保存发现页面没有进行刷新(是通过浏览器左上角的刷新按钮来观察的),直接局部更新视图,已经实现了热更新。 如果想实现修改js文件后热更新,要加一段业务代码: 在这里插入图片描述 保证在实现热更新之后,需要通知到业务代码重新render一遍,实现页面视图变化。

6.样式文件和图片文件的引入

我们在写UI插件的时候,还会用到css sass等样式文件、图片文件等,但是由于webpack是node写的,它是只能识别js类型的文件,其他类型的文件webpack无法识别.这个时候,我们就需要用loaders来把这些文件转换一下,使得webpack能够识别他们。

1)引入css文件 在src文件夹下新建index.css文件,给id为app的元素加点字体样式,npm start看下效果,页面报错,识别不了css文件。 安装:npm install style-loader css-loader --save-dev webpack.config.js 文件修改如下:

module:{
    rules:[
      {
        test:/\.css$/,
        use:['style-loader','css-loader'] //从右往左执行
      }
    ]
  }

重新打包并运行发现样式引入成功。 css-loader作用是解析import样式文件,style-loader作用是将样式添加到head标签当中。 原理:webpack用正则表达式的方式查找以css结尾的文件,并将他们都交给style-loader和css-loader。这样通过import引入的css文件在运行时就被转换为style标签并插入到html文件中。

2)引入图片文件 安装:npm install file-loader --save-dev

module:{
    rules:[
      {
        test:/\.css$/,
        use:['style-loader','css-loader']
      },
      {
        test:/\.(png|svg|gif|jpg)$/,
        use:'file-loader'
      }
    ]
  }

问题:url-loader和file-loader之间的区别?

{
  test: /\.(png|jpg|gif)$/i,
  loader: 'url-loader',
  options: {
    limit: 8192,
    mimetype: 'image/png' 
  }
}

当文件小于一定的大小时,我们会选择使用url-loader,它不会将图片文件单独打包,会将图片文件转换成base64的形式插入到css文件中,这样就会减少http请求的数量,减少损耗。url-loader会兼容file-loader,它会在文件大小小于8192的时候使用url-loader,大于的时候使用file-loader。 3)引入样式前缀 安装:npm i -D postcss-loader autoprefixer 作用:如果需要写一些css3的属性,比如transform等,我们希望webpack可以自动帮我们加上厂商前缀,便于兼容各个浏览器的版本。 根目录下新建postcss.config,js,配置如下:

module.exports = {
    plugins: [
        require('autoprefixer')({
            overrideBrowserslist: [
                "Android 4.1",
                "iOS 7.1",
                "Chrome > 31",
                "ff > 31",
                "ie >= 8"
            ]
        })
    ]
};

我们希望加入的样式前缀需要覆盖安卓4.1的系统、ios 7.1的系统等。 webpack.config.js的配置: 在这里插入图片描述 打包之后,我们发现css3属性都加上厂商属性了。

7.webpack 插件(plugins)

插件作用:在webpack运行到某一个时刻的时候,帮助你做一些事情。 1)HtmlWebpackPlugin 生成一个html文件,并将打包后的文件引入该html 2)CleanWebapckPlugin 打包前删除所有上一次打包好的文件 3)BundleAnalyzerPlugin 用来进行打包性能分析的插件 4)HotModuleReplacementPlugin 热更新所需要的插件

8.语法的转换(babel)

需求:此时我们又有一些其他的需求,写插件的时候,我们可能要用到es6的语法和api,此时我们就需要用到babel了,在webpack打包之前,需要用babel转义一下。

什么是babel?

babel是一个工具链,它能够将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,使得代码能够在当前和旧版本的浏览器中运行。

Es6的变化主要分为两部分:

1)语法部分:比如箭头函数和解构函数 --用@babel/preset-env去处理

2)API部分:比如map和promise --用@babel/polyfill 去处理

处理es6语法:

安装以上插件之后,在根目录下新建一个babel.config.json文件,加入以下规则:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "firefox": "60",
        },
        "useBuiltIns": "usage",
      }
    ]
  ]
}

默认情况下,它会转换浏览器不兼容的所有的es6+语法,但是有时候我们不需要兼容所有的浏览器版本,所以可以通过target来设定最低兼容的浏览器版本,这段代码的意思就是当firefox版本大于60的时候才进行转码,意思就是转换es6语法来兼容firefox60及以上的浏览器版本,还需要在webpack.config.js中加入babel-loader.

业务场景处理es6 API

在这里插入图片描述

用来处理es6 api的就是polyfill垫片,在文件的开头引入它就可以转换es6 api了,但是打包之后发现文件体积变大了好几倍,他把所有的包都引进来了,我们可以实现按需引入吗?

这个时候我们就用到了useBuiltIns属性,设置成usage之后,它表示按需引入,它会将我们程序中用到的ie8以上不支持的es6属性 通过全局变量的形式引入,兼容所有的api和原型方法,之所以还要加上corejs的版本为3,一是要声明corejs的版本,不然打包时会报错,二是corejs3改进了2的一些不足,可以兼容includes等原型方法。

插件场景处理es6 API

第二种配置方法的应用场景是插件或者框架,它是通过plugin-transform-runtime 去实现的。

在这里插入图片描述

为何要区分两种使用场景处理es6 API? 第一种方法是以全局变量的形式引入代码包,会造成全局变量的污染,业务场景中并不怕全局变量的污染。但是在插件的场景中,别人使用我们的插件时,我们不希望给别人的使用环境造成污染,所以选择使用第二种(会形成沙箱环境,与全局环境相隔离)。

9.加入react

需求:最后一部分,因为我们要写一个react的插件,但是现在webpack还没有办法识别jsx语法,我们就来配置一下。 主要加的配置就是在babel的配置文件中加入@babel/preset-react 这个插件集合,然后在webpack配置文件中使用babel-loader,用babel-loader去处理jsx语法,这样就可以使用react了

"@babel/preset-react"

这样就可以在项目中运行react代码。(当然也要安装react和react-dom喽)

10.生产环境打包

现在我们已经写完了一个插件,并在本地进行联调。接下来,我们要将它打包上传。 新建一个文件:webpack.config.product.js

const path = require('path');

module.exports = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    libraryTarget: 'umd',
    filename: 'index.js',
    path: path.resolve(__dirname, 'build'),
    chunkFilename: '[name].min.js'
  },
  externals: {
    'react': 'react',
    'react-dom': 'react-dom'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.(png|svg|gif|jpg)$/,
        use: 'file-loader'
      },
      {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/
      }
    ]
  }
}

libraryTarget 和 library 是开发类库必须要用的输出属性。 我们开发类库时希望别人用什么方式引入呢?引入的方式有以下几种: 传统的script方式:

<script src="demo.js"></script>

AMD方式:

define(['demo'], function(demo) {
demo();
});

commonjs方式:

const demo = require('demo');
demo();

ES6 模块引入

import demo from 'demo';

类库为什么支持不同方式的引入?这就是webpack.library和output.libraryTarget提供的功能。 output.libraryTarget 属性是控制webpack打包的内容如何被暴露的。 暴露的方式分为以下三种方式: 一.暴露一个变量 libraryTarget: "var"

output: {
    libraryTarget:'var',
    library:'abc',
    filename: "index.js",
    path: path.resolve(__dirname, "build"),
  },

webpack打包出来的值赋值给一个变量,该变量名就是output.library指定的值。 以下是webpack打包后的一部分内容: 在这里插入图片描述 将打包后的内容复制给一个全局变量,引用类库的时候直接使用该变量,nodejs环境不支持。

<head>
  <meta charset='UTF-8'>
  <script src='./build/index.js'></script>
  <script>
    console.log(abc.mytest())
  </script>
</head>

二.通过对象属性暴露 库的返回值分配给指定对象的指定属性。属性由output.library指定,对象由output.libraryTarget指定。 1)libraryTarget: "this"

this["myDemo"] = _entry_return_;

this.myDemo();
myDemo(); 

2)libraryTarget: "window"

window["myDemo"] = _entry_return_;

window.myDemo.doSomething();

3)libraryTarget: "global" global["myDemo"] = entry_return; 下面这种情况,可以支持nodejs环境。

mode: "production",
  entry: "./src/index.js",
  target:'node',
  output: {
    libraryTarget:'global',
    library:'abc',
    filename: "index.js",
    path: path.resolve(__dirname, "build"),
  },

以上三种方法是在公共对象上export出你的方法函数。 优点:减少变量冲突 缺点:nodejs环境不支持

三. 通过模块暴露

  1. libraryTarget: "commonjs"
exports["myDemo"] = _entry_return_;

require("myDemo").doSomething();

直接在exports对象上导出--定义在library上的变量,node支持,浏览器不支持

<head>
  <meta charset='UTF-8'>
  <script src='./build/index.js'></script>
  <script>
    console.log(require('abc').mytest())
  </script>
</head>

在这里插入图片描述 这个选项可以使用在commonjs环境中。 2) libraryTarget: "commonjs2"

module.exports = _entry_return_;

const myDemo = require("myDemo");
myDemo();

直接用module.exports导出,会忽略library变量,node支持,浏览器不支持,这个选项可以使用在commonjs环境中。 为什么commonjs不需要单独引入requirejs? commonjs是服务端模块化语言规范,在node中使用的时候会使用node中的requireJS。 3) libraryTarget: "amd"

define("myDemo", [], function() {
return _entry_return_;
});
require(['myDemo'], function(myDemo) {
// Do something with the library...
myDemo();
});
<head>
  <meta charset='UTF-8'>
  <script src='./build/index.js'></script>
  <script>
    require(['abc'], function (mytest) {
        // Do something with the library...
        console.log(mytest());
      });
    
  </script>
</head>

在这里插入图片描述 amd属于客户端模块语言的规范,需要用户自己引入requirejs才能使用。不支持nodejs环境,支持浏览器环境。 4) libraryTarget: "umd" 该方案支持commonjs、commonjs2、amd,可以在浏览器、node中通用。它会根据引用该插件的上下文来判断属于什么环境,使其和CommonJS、AMD兼容或者暴露为全局变量。

(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
  module.exports = factory();
else if(typeof define === 'function' && define.amd)
  define([], factory);
else if(typeof exports === 'object')
  exports["MyLibrary"] = factory();
else
  root["MyLibrary"] = factory();
})(typeof self !== 'undefined' ? self : this, function() {
return _entry_return_;
});
output: {
library: {
  root: "myDemo",
  amd: "my-demo",
  commonjs: "my-common-demo"
},
libraryTarget: "umd"
}

最后建议,如果目标明确,我只是兼容nodejs,那么选择commonjs/commonjs2,如果只兼容浏览器,那就选择暴露变量的方式,如果想通用,那就选择umd的方式,对于不同的情况做多种处理方式,是非常明智的选择。

11.在保存react状态的前提下进行热更新

在使用热更新之后,我们发现在一个文件中修改某一个内容,这个文件会重新render一次,那么该文件中的一些状态,比如react经典的计数器,就会被重置。 见下图,backtop文件修改之后,会render整个组件,状态重置。 在这里插入图片描述 这样才能做到在保存react状态的前提下进行热更新? 安装:npm install react-hot-loader @hot-loader/react-dom --save-dev babel.config.json:添加以下代码

"plugins": ["react-hot-loader/babel"]

backtop.js 用react-hot-loader包一层:

import { hot } from "react-hot-loader/root";
 
//.....
 
export default hot(App);

即可实现在保存react状态的前提下进行热更新。

以上:我们实现了基本的webpack打包实现react插件的配置,如果想要实现其他的功能,可以依次叠加。webpack路途遥远,祝愿大家成为一名坚强的‘高级webpack配置师’。