目标:使用webpack从零开始配置一个react项目javascript
借用官网的解释:本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序须要的每一个模块,而后将全部这些模块打包成一个或多个 bundle。css
要理解webpack是什么,要从两个词出发:“模块”和“打包”。html
以一个html页面为例,在页面中经过script标签引入了3个JavaScript文件a.js,b.js和c.js,每一个文件中分别定义了一个函数并导出给外部用。而且它们之间有必定的依赖关系,c.js依赖于b.js,b.js依赖于a.js。vue
由于有3个独立的js文件,因此在加载的时候浏览器须要发送三次http请求来获取这三个文件,而后依次执行其中的代码,若是其中有一个文件由于网络问题而延误了时间,那么整个页面的显示也会被延误。当咱们的项目逐渐变大,有几十个到上百个JavaScript文件的时候,那问题会更严重,不但有延迟问题,还会遇到很难维护的问题。因此须要尽量的合并文件,减小http请求,这个合并的过程就是打包,一般将几个分散的有依赖关系的文件打包为一个文件。java
一般状况下,为了提升开发效率,咱们会使用诸如ES6,less等,就须要在运行时作代码的转换,使得其能够在对应的终端上正常执行,这个过程若是每次都手动经过工具作转换的话很是的费时,理想的状况是,咱们可使用这些具备新特性的东西,又能够经过某种工具自动的完成转换,进而提高开发效率,这个自动转换的过程也是打包的过程。node
模块能够理解为一个单独的文件,或者一个方法,每个模块都是一个单独的做用域, 也就是说, 在该模块内部定义的变量, 没法被其余模块读取, 除非定义为global(浏览器中为window)对象的属性。模块能够被复用,模块之间能够被相互引用,好比一个处理浮点数加法的方法,就是一个模块,可能在多个地方使用,使用的地方直接导入这个方法便可。react
webpack的核心功能就是打包,下面咱们会针对不一样的文件类型作处理,充分发挥webpack的打包模块的做用。webpack
如下内容将从如下几个方面展开:web
mkdir react-fe
cd react-fe
yarn init // 初始化项目,生成package.json文件
yarn add webpack webpack-cli -D // 安装webpack
复制代码
若是对于生成的package.json中有不理解的,能够查看这篇文章package.json 知多少? package.json中添加脚本配置:express
"scripts": {
"build": "webpack --mode production"
},
复制代码
在项目目录下新建src文件,用于存放源文件,新建/src/index.js,内容任意
console.log('hello world');
复制代码
执行yarn build,会新增一个dist目录,里面是打包后的文件
!function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t){console.log("hello world")}]);
复制代码
webpack 运行时默认读取项目下的 webpack.config.js 文件做为配置;这个配置实际上是一个node js的脚本,脚本对外暴露一个配置对象,webpack经过这个对象来读取相关的一些配置,由于是node js的脚本,因此可使用任何node模块。一般一个项目会分为开发模式和生产模式,因此咱们建立一个目录build,此目录下的文件均和构建相关。以下:
|- build (打包配置目录)
|- webpack.config.js -- webpack打包基础配置文件
|- webpack.dev.config.js -- webpack 开发环境打包配置文件
|- webpack.prod.config.js -- webpack 生产环境 build打包配置文件
复制代码
webpack.config.js配置以下:
const path = require('path');
function resolve(dir) {
return path.join(__dirname, '..', dir)
}
function src(dir) {
return resolve(path.join('src', dir))
}
module.exports = {
entry: {
main: src('index.js'), // 入口文件
},
output: {
filename: '[name].js', // 输出文件
},
module: {
rules: []
},
plugins: []
}
复制代码
配置webpack的mode, 枚举值有production,development, none,在webpack4中配置了mode,至关于使用DefinePlugin设置了NODE_ENV,这个值用于区分生产模式仍是开发模式,后续会告诉webpack使用哪一种模式开启内置优化,mode值的不一样,build时默认的配置也会不一样,有了默认配置,就不须要手动启用插件了。
选项 | 描述 |
---|---|
development | 会将 process.env.NODE_ENV 的值设为 development。启用 NamedChunksPlugin 和 NamedModulesPlugin。 |
production | 会将 process.env.NODE_ENV 的值设为 production。启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 UglifyJsPlugin. |
配置webpack.dev.config.js
var base = require('./webpack.config');
base.mode = "development";
base.devtool = 'cheap-module-eval-source-map';
module.exports = base;
复制代码
配置webpack.prod.config.js
var base = require('./webpack.config');
base.mode = "production";
base.devtool = 'hidden-source-map';
module.exports = base;
复制代码
添加npm scripts
"scripts": {
"build": "webpack --config ./build/webpack.prod.config.js --progress --colors",
"dev": "webpack --config ./build/webpack.dev.config.js --progress --colors"
},
复制代码
cross-env是一个运行跨平台设置和使用环境变量的脚本,好比咱们想要分析打包后的各个文件大小以及依赖关系,能够添加"webpack-bundle-analyzer",这个包不是在每一次打包的时候都用到,因此,单独添加一个条件,只有在须要分析的时候才使用这个包
yarn add cross-env webpack-bundle-analyzer -D
// 添加npm scripts
"scripts": {
"analyze": "cross-env ANALYZE=1 npm run build",
},
// 修改webpack.prod.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
if (process.env.ANALYZE) {
base.plugins.push(new BundleAnalyzerPlugin());
}
复制代码
只设置 NODE_ENV,则不会自动设置 mode
webpack 中提供一种处理多种文件格式的机制,即是使用 loader。咱们能够把 loader 理解为是一个转换器,负责把某种文件格式的内容转换成 webpack 能够支持打包的模块。 loader自己是一个导出为function的node模块; 在没有任何loader处理的状况下,webpack默认只能处理js文件,最终输出js文件,而loader的做用就是把非js文件转换为webpack能够处理的js文件 将内联图像转换为data URL
负责解析 CSS 代码,主要是为了处理 CSS 中的依赖,例如 @import 和 url() 等引用外部文件的声明;将样式代码处理为js数组,样式代码处理为字符串
yarn add css-loader -D
复制代码
新建文件,目录以下:
|- src
|- index.html
|- index.js
|- index.css文件
// index.css
#app {
background-color: #f5f5f5;
color: blue;
}
#app p {
color: gray;
}
// index.js
const a = require('./index.css');
console.log(a);
// index.html
<div id="app">
<h4>hello webpack!</h4>
<p>hello loader!</p>
</div>
<script src="../dist/main.js"></script>
// webpack.config.js,添加rules
{
test: /\.css$/,
use: 'css-loader'
}
复制代码
执行 yarn build 能够看到打包后的main.js中,css-loader将样式代码处理成了js数组,而且咱们的样式代码被处理成了字符串。
function(n, t, o) {
(t = o(2)(!1)).push([
n.i,
"#app {\n background-color: #f5f5f5;\n color: blue;\n }\n #app p {\n color: gray;\n }",
""
]),
(n.exports = t);
},
复制代码
通过css-loader处理完的文件并无应用到页面上,若是想要样式生效,还须要style-loader的处理
yarn add style-loader -D
// webpack.config.js,修改rules
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
}
复制代码
style-loader将css-loader返回的样式数组一顿操做插入到html head,而后本身返回了一个空对象。
loader处理的时机:在import或”加载“时预处理文件,相似于其余构建工具中的task
loader处理顺序:从右向左执行,支持链式传递,链中的每一个 loader 会将转换应用在已处理过的资源上。一组链式的 loader 将按照相反的顺序执行。链中的第一个 loader 将其结果(也就是应用过转换后的资源)传递给下一个 loader,依此类推。最后,链中的最后一个 loader,返回 webpack 指望 JavaScript。
一般状况下,咱们会使用预处理器来编写css,好比使用less或者sass,这样能够大大提升开发效率,下面以less为例,将原来的css文件修改成less文件,而且内容修改以下:
// index.less
@theme-color: #ff8200;
@dark-gray: #707c93;
@light-gray: #b5c1d2;
#app {
background-color: @light-gray;
color: @theme-color;
p {
color: @dark-gray;
}
}
// 安装less less-loader,less处理并识别less文件,less-loader能够将less文件处理为css文件
yarn add less less-loader -D
// webpack.config.js
module: {
rules: [
{
test: /\.less?$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
}
]
},
// index.js
const a = require('./index.less');
复制代码
另外经常使用的处理样式的方式是,使用postcss,它是一种对css编译的工具,相似babel对js的处理,常见的功能如:
postcss 只是一个工具,自己不会对css一顿操做,它经过插件实现功能。
// 安装loader和plugin
yarn add postcss-loader postcss-pxtorem autoprefixer -D
// 项目根目录下建立postcss.config.js
module.exports = {
plugins: {
'autoprefixer': {},
'postcss-pxtorem': {
'rootValue': 108,
'propList': ['*'],
'minPixelValue': 2,
'selectorBlackList': [],
},
},
};
// 修改Webpack配置
module: {
rules: [
{
test: /\.less?$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader']
},
]
}
复制代码
开发中还有一个经常使用的功能是,好比对某个依赖的UI库作单位转换,也能够单独配置plugin,好比咱们项目中设计稿的尺寸是1080,可是依赖的库是750的,而且是px为单位的,为了兼容咱们的项目,就要对依赖库作单位转换。
module: {
rules: [
{
test: /\.css?$/,
use: [MiniCssExtractPlugin.loader, {
loader: 'css-loader',
options: {
minimize: true
}
}, {
loader: 'postcss-loader',
options: {
plugins: [
require('postcss-pxtorem')({
rootValue: 37.5,
propWhiteList: ['*'],
'minPixelValue': 2,
})
]
}
}],
include: /node_modules/ant-design/
},
]
}
复制代码
一般一个成熟的项目,咱们不会使用style-loader的方式将结果插入到head中,也不会本身手动去修改html中引入打包后文件的路径。下面将配合plugin使用
webpack的plugin比loader强大,经过钩子能够涉及整个构建流程,能够作一些在构建范围内的事情;理论上能够干涉 webpack 整个构建流程,能够在流程的每个步骤中定制本身的构建需求。
plugin:构建流程中处理构建任务,能够这么理解,模块代码的转换工做由loader处理,除此以外的任何其余工做均可以由plugin完成。
经常使用的两个插件:
yarn add html-webpack-plugin mini-css-extract-plugin -D
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css?$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html', // 配置输出文件名
template: src('index.html'),
}),
new MiniCssExtractPlugin({
filename: "static/css/[name].css"
}),
]
};
// index.html中去掉script部分,此处会自动在html中添加引用
复制代码
执行 yarn build的结果
|- dist
|- index.html
|- main.js
|- static
|- css
|- main.css
// index.html中会自动引入css和js
<link href="static/css/main.css" rel="stylesheet">
<body> <div id="app"> <h4>hello webpack!</h4> <p>hello loader!</p> </div> <script type="text/javascript" src="main.js"></script></body>
复制代码
做为普通的html开发,你可能会须要从网上下载一些js文件而不是使用cdn的方式引入,好比用于适配移动端的flexiable,这种文件不须要通过babel再次进行处理,因此打包后直接在html中引入便可
yarn add copy-webpack-plugin -D
// webpack.config.js
const CopyPlugin = require('copy-webpack-plugin');
plugins: [
new CopyPlugin([
{ from: src('flexible.js'), to: resolve('dist') },
]),
]
复制代码
如今的项目中咱们通常会使用ES6开发,因此须要用babel处理,关于babel的使用,能够参考babel学习
yarn add @babel/core @babel/preset-env @babel/plugin-transform-runtime babel-loader -D
yarn add @babel/runtime-corejs3
// 项目根目录下新建.babelrc文件
{
"presets": [
[
"@babel/preset-env",
{
"modules": "false",
"targets": {
"browsers": [
"> 1%",
"last 2 versions",
"ie >= 10",
"iOS >= 8",
"Android >= 4"
]
}
}
],
],
"plugins": [
[
"@babel/plugin-transform-runtime", {
"helpers": true,
"corejs": 3,
"regenerator": true
}
]
],
// "ignore": ["./src/three.min.js", "./src/panolens.js"]
}
// webpack.config.js,添加对js文件的处理
module: {
rules: [
{
test: /\.(js|jsx)$/,
loader: 'babel-loader',
exclude: /node_modules/,
include: resolve(''),
},
]
}
// index.js,修改此文件,测试babel是否生效
const a = require('./index.less');
const obj = {
name: 'apple',
sex:' female',
};
if (obj.hasOwnProperty('sex')) {
const div = document.createElement('div');
div.style.color = 'pink';
const { name } = obj;
div.innerText = name;
document.getElementById('app').appendChild(div);
}
console.log(a);
复制代码
处理图片,常使用的两个loader是url-loader和file-loader,其中 url-loader 是将图片转换成一个 DataURL,而后打包到 JavaScript 代码中,这对小的图片来讲是不错的处理方式,但是大图片这种处理方式就不适用了,无疑会增大js的体积,经过使用file-loader将文件处理后输出到目录中。
yarn add file-loader url-loader -D
// 新建存放图片目录
|- src
|- assets
|- img // 用于放全部的图片
// webpack.config.js
module: {
rules: [
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 8192, // 若是超过8192 字节的就使用file-loader处理,并按照下面规则生成文件
name: 'static/images/[name].[hash:8].[ext]'
},
},
],
},
]
}
// 修改index.less
div {
background: url(../src/assets/img/bg.jpg);
}
复制代码
到此为止,咱们已经实现了如何使用webpack来搭建一个项目了,即便不适用诸如react, vue这样的框架,也是能够正常启动打包项目的。
开发过程当中,咱们但愿边修改就能看到更新后的结果,因此本地开发启动服务热更新很重要,否则就得每次去build而后刷新看结果。 webpack为咱们提供了实现本地热更新的插件:webpack-dev-server,使用和配置很简单,可参照官方文档进行配置。可是本篇咱们不使用这个。经过使用express服务器,能够进行更多的扩展,结合使用其余的中间件来响应http请求及其余的功能,扩展性更好,较为灵活。 开启了 hot 功能的 webpack 会往咱们应用的主要代码中添加 WS 相关的代码,用于和服务器保持链接,等待更新动做。
// webpack.dev.config.js
Object.keys(base.entry).forEach(function (name) {
base.entry[name] = ['webpack-hot-middleware/client'].concat(base.entry[name]);
});
base.plugins.push(
new webpack.HotModuleReplacementPlugin(), // 模块热更新
);
// 修改入口文件
if (module.hot) {
module.hot.accept();
}
// devServer.js
var webpack = require('webpack');
var express = require('express');
var path = require('path');
var config = require('./build/webpack.dev.config');
var app = express();
// Webpack developer
var compiler = webpack(config);
var devMiddleWare = require('webpack-dev-middleware')(compiler, {
publicPath: config.output.publicPath,
stats: {
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}
});
app.use(devMiddleWare);
app.use(require('webpack-hot-middleware')(compiler));
var mfs = devMiddleWare.fileSystem;
var file = path.join(config.output.path, 'index.html');
app.get('/middle.html', (req, res) => {
res.end();
})
app.get('*', function (req, res) {
devMiddleWare.waitUntilValid(function () {
var html = mfs.readFileSync(file);
res.end(html);
})
})
var port = 3005;
app.listen(port, function (err, result) {
if (err) {
console.log(err);
}
console.log('Server running on http://localhost:' + port);
});
复制代码
按照上面的配置,能够实现,更新本地文件,浏览器自动刷新,可是有个问题,好比咱们在页面输入了两个值,自动刷新后,刚刚输入的值就没有了,对复杂的页面操做来讲,每次更新文件都须要从新进行一遍操做,是很影响效率的,形成这种现象的缘由是热更新不能存储state的状态,使用react-hot-loader能够解决这个问题。
下一篇文章介绍react的引入。