这篇文章是以打包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环境不支持
三. 通过模块暴露
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配置师’。