wepack搭建react项目(一)

目标:使用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

  1. 初始化webpack配置
  2. 配置css,使用预处理器
  3. 配置html,自动引入打包后的文件
  4. 使用 Babel 来支持 ES 新特性
  5. 处理图片
  6. 本地搭建服务器

初始化项目

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

配置css,使用预处理器

webpack 中提供一种处理多种文件格式的机制,即是使用 loader。咱们能够把 loader 理解为是一个转换器,负责把某种文件格式的内容转换成 webpack 能够支持打包的模块。 loader自己是一个导出为function的node模块; 在没有任何loader处理的状况下,webpack默认只能处理js文件,最终输出js文件,而loader的做用就是把非js文件转换为webpack能够处理的js文件 将内联图像转换为data URL

css-loader

负责解析 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。

less-loader

一般状况下,咱们会使用预处理器来编写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-loader

另外经常使用的处理样式的方式是,使用postcss,它是一种对css编译的工具,相似babel对js的处理,常见的功能如:

  1. 使用下一代css语法
  2. 自动补全浏览器前缀: autoprefixer
  3. 自动把px代为转换成rem: postcss-pxtorem
  4. css 代码压缩等等

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使用

添加plugin,分离html,css,js

webpack的plugin比loader强大,经过钩子能够涉及整个构建流程,能够作一些在构建范围内的事情;理论上能够干涉 webpack 整个构建流程,能够在流程的每个步骤中定制本身的构建需求。

plugin:构建流程中处理构建任务,能够这么理解,模块代码的转换工做由loader处理,除此以外的任何其余工做均可以由plugin完成。

经常使用的两个插件:

  1. html-webpack-plugin能够根据模板自动生成html代码,并自动引用css和js文件,这对生成的文件使用了hash的状况很是有用
  2. mini-css-extract-plugin 将js文件中引用的样式单独抽离成css文件,须要结合css-loader一块儿使用,另外,这个插件使用后就不须要style-loader
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') },
		]),
 ]
复制代码

使用 Babel 来支持 ES 新特性

如今的项目中咱们通常会使用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这样的框架,也是能够正常启动打包项目的。

webpack搭建本地服务器(express + webpack-dev-middleware)

开发过程当中,咱们但愿边修改就能看到更新后的结果,因此本地开发启动服务热更新很重要,否则就得每次去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的引入。

参考文章

  1. Webpack Loader简析(一):基本概念
  2. package.json 知多少?
  3. npm install 原理分析
  4. 强化:构建易用易扩展的工做流
相关文章
相关标签/搜索