上手webpack,有这个教程就可以了

最近一直在学习webpack的基础知识,从以下5个大的点进行总结。

1.什么是webpack?
2.学习webpack需要掌握的基本知识点和概念
3.从头到尾做一个webpack的小demo
4.webpack在项目中的使用技巧和对项目的优化

image.png

什么是Webpack

webpack是近期最火的一款模块加载器兼打包工具,它能把各种资源,例如JS(含JSX)、coffee、样式(含less/sass)、图片等都作为模块来使用和处理,它能有Grunt或Gulp所有基本功能。webpack的官网是 https://webpack.github.io/ ,文档地址是https://webpack.github.io/docs,官网对webpack的定义是MODULE BUNDLER,他的目的就是把有依赖关系的各种文件打包成一系列的静态资源。

一、webpack 的优势

其优势主要可以归类为如下几个:

(1) webpack 是以 commonJS 的形式来书写脚本滴,但对 AMD/CMD 的支持也很全面,方便旧项目进行代码迁移。
(2)支持很多模块加载器的调用,可以使模块加载器灵活定制,比如babel-loader加载器,该加载器能使我们使用ES6的语法来编写代码;less-loader加载器,可以将less编译成css文件;
(3)开发便捷,能替代部分 grunt/gulp 的工作,比如打包、压缩混淆、图片转base64等。
可以通过配置打包成多个文件,有效的利用浏览器的缓存功能提升性能。
(4) Loader,加载器可以将其他资源整合到JS文件中,通过这种方式,可以讲所有的源文件形成一个模块.
(5)优秀的语法分析能力,支持 CommonJs AMD 规范
(6)有丰富的开源插件库,可以根据自己的需求自定义webpack的配置

二、webpack的工作原理

Webpack的工作方式是:把你的项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包为一个(或多个)浏览器可识别的JavaScript文件。


1031000-160bc667d3b6093a.png

简单的说就是分析代码,找到“require”、“exports”、“define”等关键词,并替换成对应模块的引用。

在一个配置文件中,指明对某些文件进行编译、压缩、组合等任务。把你的项目当成一个整体,通过一个给定的主文件 (index.js),webpack将从这个文件开始找到你的项目的所有的依赖文件,使用loaders处理他们,最后打包为一个浏览器可
以识别的js文件。

三、使用场景

在没有使用webpack之前:

举个例子:index.html里面有一大堆的css和js文件,如a.js b.js c.js等等
(1)a.js要用到b.js里面的某一个函数,则a.js要放在b.js后面
(2)c.js要用到a.js里面的一个函数,则c.js要放在a.js后面
(3)b.js又要用到某个js文件里面的函数,则b.js就要放在其后面

如果有N多个js文件,需要手动处理他们的关系,即容易出错。

使用webpack:

webpack的理念就是一切皆模块化,把一堆的css文件和js文件放在一个总的入口文件,通过require引入,剩下的事情webpack会处理,包括所有模块的前后依赖关系,打包、压缩、合并成一个js文件,公共代码抽离成一个js文件、某些自己指定的js单独打包,模块可以是css/js/imsge/font等等。

(1)根据模板生成HTML,并自动处理上面的css/js引用路径
(2)自动处理<img>里面的图片路径,css里面背景图的路径,字体引用
(3)开启本地服务器,一边改写代码,一边自动更新页面内容
(4)编译jsx es6 sass less coffescript等,并添加md5、sourcemap等辅助
(5)异步加载内容,不需要时不加载到DOM
(6)配合vue.js react.js等框架开发

四、WebPack和Grunt以及Gulp相比有什么特性

其实Webpack和另外两个并没有太多的可比性,Gulp/Grunt是一种能够优化前端的开发流程的工具,而WebPack是一种模块化的解决方案,不过Webpack的优点使得Webpack在很多场景下可以替代Gulp/Grunt类的工具。

Grunt和Gulp的工作方式是:在一个配置文件中,指明对某些文件进行类似编译,组合,压缩等任务的具体步骤,工具之后可以自动替你完成这些任务。


1031000-d0693c06bb3a00e3.png

如果实在要把二者进行比较,Webpack的处理速度更快更直接,能打包更多不同类型的文件。

五、webpack为什么要将所有资源放在一个文件里面?

我们知道,对于浏览器来说,加载的资源越少,响应的速度也就越快,所以有时候我们为了优化浏览器的性能,会尽可能的将资源合并到一个主文件app.js里面。但是这导致的很大的缺点:

当你的项目十分庞大的时候,不同的页面不能做到按需加载,而是将所有的资源一并加载,耗费时间长,性能降低。
会导致依赖库之间关系的混乱,特别是大型项目时,会变得难以维护和跟踪。比如:哪些文件是需要A模块加载完后才能执行的?哪些页面会受到多个样式表同时影响的? 等许多问题。
而webpack可以很好的解决以上缺点,因为它是一个十分聪明的模块打包系统,当你正确配置后,它会比你想象中的更强大,更优秀。

webpack需要掌握的基本知识点和概念

一个完整的webpack基本配置如下:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const extractPlugin = require('extract-text-webpack-plugin');

module.exports = {
    devtool: 'eval-source-map',
    entry: { //入口文件配置
        main: './src/main.js'
    },
    output: {
        path: path.resolve(__dirname, 'dist'), //打包后的文件存放的地方
        filename: 'js/[name].bundle.js', //打包后输出文件的文件名
    },
    module: {
        rules: [
            { test: /\.txt$/, use: 'raw-loader' },
            { //js loader
                test: /\.js$/,
                exclude: /(node_modules|bower_components)/, //避免转义node_modules 目录或者其他不需要的源代码。
                use: {
                    loader: 'babel-loader'
                }
            },
            {
                test: /\.(png|jpg)$/,
                use: {
                    loader: 'url-loader',
                },
            },
            {
                test: /\.css$/,
                use: [{
                        loader: 'style-loader/url'
                    },
                    {
                        loader: 'file-loader',
                        options: {
                            name: '[path][name].[ext]',
                        }
                    }
                ]
            }
        ]
    },
    plugins: [ //相关插件配置
        new HtmlWebpackPlugin({
            title: 'test page', //html标题
            filename: 'index.html', //打包后的html文件名,默认index.html
            template: './index.html', // html的源文件
            inject: true, //默认true,意为script标签位于html文件的 body 底部
            cache: true, //默认true,表示内容变化的时候生成一个新的文件
            chunks: ['index'], //表示编译时用到的入口文件
            date: new Date(),
            // excludeChunks: ['index']//表示编译时排除的入口文件
        }),
        new extractPlugin("css/[name].css")
    ],
    devServer: {
        contentBase: path.resolve(__dirname, 'dist'), //最好设置成绝对路径
        historyApiFallback: true, //true默认打开index.html,false会出现一个目录,一会演示
        hot: true,
        inline: true,
        stats: 'errors-only',
        host: '127.0.0.1',
        port: '8080',
        overlay: true, //出现错误之后会在页面中出现遮罩层提示
        open: true //运行之后自动打开本地浏览器
    }
}

entry: 是指入口文件的配置项,它是一个数组的原因是webpack允许多个入口点。

entry的概念和用法具体可参考entry配置,写的很详细。

output:是指输出文件的配置项

output的概念和用法具体可参考output配置,写的很详细。

filename : 表示输出文件的文件名
path :配置输出文件存放在本地的目录
publicPath,配置CDN的路径
chunkFilename ,处理异步加载时的命名规则
hash、chunkhash和contenthash三者的区别
hash是项目级别的,每次构建得出的hash都是相同的,这可能不利于文件的缓存
chunkhash是文件级别的,值是变动修改的文件的chunkhash值
contenthash是文件级别的,在拆分css文件时记得使用处理css的缓存

Resolve: Webpack 在启动后会从配置的入口模块出发找出所有依赖的模块,Resolve 配置 Webpack 如何寻找模块所对应的文件。 Webpack 内置 JavaScript 模块化语法解析功能,默认会采用模块化标准里约定好的规则去寻找,但你也可以根据自己的需要修改默认的规则。

alias: resolve.alias 配置项通过别名来把原导入路径映射成一个新的导入路径。例如使用以下配置:

// Webpack alias 配置
resolve:{
  alias:{
    components: './src/components/'
  }
}

当你通过 import Button from 'components/button 导入时,实际上被 alias 等价替换成了 import Button from './src/components/button' 。

以上 alias 配置的含义是把导入语句里的 components 关键字替换成 ./src/components/ 。

这样做可能会命中太多的导入语句,alias 还支持 $ 符号来缩小范围到只命中以关键字结尾的导入语句:

resolve:{
  alias:{
    'react$': '/path/to/react.min.js'
  }
}

extensions: 在导入语句没带文件后缀时,Webpack 会自动带上后缀后去尝试访问文件是否存在。 resolve.extensions 用于配置在尝试过程中用到的后缀列表,默认是:

extensions: ['.js', '.json']

也就是说当遇到 require('./data') 这样的导入语句时,Webpack 会先去寻找 ./data.js 文件,如果该文件不存在就去寻找 ./data.json 文件, 如果还是找不到就报错。

假如你想让 Webpack 优先使用目录下的 TypeScript 文件,可以这样配置:

extensions: ['.ts', '.js', '.json']

plugins :顾名思义,使用插件可以给webpack添加更多的功能,使webpack更加的灵活和强大,webpack有两种类型的插件:

(1)webpack内置的插件
// 首先要先安装webpack模块

var webpack = require("webpack");
module.exports = {
    new webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
      },
    })
};

(2)webpack外置插件

//npm install component-webpack-plugin 先要在安装该模版
var ComponentPlugin = require("component-webpack-plugin");
module.exports = {
    plugins: [
        new ComponentPlugin()
    ]
}

plugins的概念和用法具体可参考plugins的使用,写的很详细。

module 配置处理文件的选项
loaders: 一个含有wepback中能处理不同文件的加载器的数组。告诉webpack要利用哪种加载器来处理test所匹配的文件

loaders的概念和用法具体可参考loaders的使用,写的很详细。

loaders 的安装方法

$ npm install xxx-loader --save-dev

以less为例的话,先要安装less、less-loader、css-loader、style-loader模块。loader如下:

{
 test: /\.less$/,
    use: [
        "style-loader",
        "css-loader",
        "less-loader"
    ]
}

test:用来匹配相对应文件的正则表达式
use: 一个数组,里面放一个个的loader,执行顺序从右到左。上面几个loader的作用依次为:less-loader将代码转成css、css-loader用于解析(可以用来做css-moudle、压缩等)、style-loader则将解析后的样式嵌入js代码。
exclude:排除不满足条件的文件夹(这样可以排除webpack查找不必要的文件)
include:需要被loader 处理的文件或文件夹。

dev-server :Webpack提供一个可选的本地开发服务器,这个本地服务器基于node.js构建,可以实现你想要的这些功能之后会使用到;

详细的教程可以参考深入浅出的webpack构建工具---DevServer配置项

一个完整的dev-server如下:

devServer: {
        contentBase: path.resolve(__dirname, 'dist'), //最好设置成绝对路径
        historyApiFallback: true, //true默认打开index.html,false会出现一个目录,一会演示
        hot: true,
        inline: true,
        stats: 'errors-only',
        host: '127.0.0.1',
        port: '8080',
        overlay: true, //出现错误之后会在页面中出现遮罩层提示
        open: true //运行之后自动打开本地浏览器
    }
  1. contentBase:该配置项指定了服务器资源的根目录,如果不配置contentBase的话,那么contentBase默认是当前执行的目录,一般是项目的根目录。

  2. port:该配置属性指定了开启服务器的端口号。

  3. host:该配置项用于配置 DevServer的服务器监听地址。比如想让局域网的其他设备访问自己的本地服务,则可以在启动DevServer时带上 --host 0.0.0.0.

  4. headers:该配置项可以在HTTP响应中注入一些HTTP响应头

  5. historyApiFallback:该配置项属性是用来应对返回404页面时定向跳转到特定页面的。一般是应用在 HTML5中History API 的单页应用,比如在访问路由时候,访问不到该路由的时候,会跳转到index.html页面。

  6. hot:该配置项是指模块替换换功能,DevServer 默认行为是在发现源代码被更新后通过自动刷新整个页面来做到实时预览的,
    但是开启模块热替换功能后,它是通过在不刷新整个页面的情况下通过使用新模块替换旧模块来做到实时预览的。

  7. inline: webpack-dev-server 有两种模式可以实现自动刷新和模块热替换机制。inline: false;页面是被嵌入到一个iframe页面,并且在模块变化的时候重载页面。inline:true;它在构建变化后的代码会通过代理客户端来控制网页刷新。

  8. open: 该属性用于DevServer启动且第一次构建完成时,自动使用我们的系统默认浏览器去打开网页。

  9. overlay:该属性是用来在编译出错的时候,在浏览器页面上显示错误。该属性值默认为false,需要的话,设置该参数为true。

  10. stats(字符串):该属性配置是用来在编译的时候再命令行中输出的内容

  11. compress:该属性是一个布尔型的值,默认为false,当他为true的时候,它会对所有服务器资源采用gzip进行压缩。

  12. proxy: 用来解决跨域。假如现在我们本地访问的域名是 http://localhost:8081, 但是我现在调用的是百度页面中的一个接口,该接口地址是:http://news.baidu.com/widget?ajax=json&id=ad。现在我们只需要在devServer中的proxy的配置就可以了:
    如下配置:

proxy: {
  '/api': {
    target: 'http://news.baidu.com', // 目标接口的域名
    // secure: true,  // https 的时候 使用该参数
    changeOrigin: true,  // 是否跨域
    pathRewrite: {
      '^/api' : ''  // 重写路径
    }
  }
}

然后我们在需要请求接口的js里面编写如下代码:

import axios from 'axios';

axios.get('/api/widget?ajax=json&id=ad').then(res => {
  console.log(res);
});

resolve:其它解决方案配置;

resolve.root,绝对路径, 查找module的话从这里开始查找(可选)
resolve.modulesDirectories,取相对路径,所以比起 root ,所以会多 parse 很多路径。查找module(可选)
resolve.extensions,自动扩展文件后缀名,意味着我们require模块可以省略不写后缀名
resolve.alias,模块别名定义,方便后续直接引用别名,无须多写长长的地址

从头到尾做一个webpack的小demo

初步了解了Webpack基本知识点后,我们一步步的开始学习使用Webpack。

1.初始化项目

Webpack可以使用npm安装,新建一个空的练习文件夹(此处命名为webpack-test),在终端中转到该文件夹后执行下述指令就可以完成安装。

npm init

在上述练习文件夹中创建一个package.json文件,这是一个标准的npm说明文件,里面蕴含了丰富的信息,包括当前项目的依赖模块,自定义的脚本任务等等。在终端中使用npm init命令可以自动创建这个package.json文件。
输入这个命令后,终端会问你一系列诸如项目名称,项目描述,作者等信息,不过不用担心,如果你不准备在npm中发布你的模块,这些问题的答案都不重要,回车默认即可。初始化完成。

2.安装webpack

全局安装(如果之前没安装的话,就全局安装一下,之前安装过了的话,就可以直接跳过这一步)

npm install -g webpack    //全局安装webpack

npm install --save-dev webpack   //局部安装

注意:如果使用的是4.0+的版本,还需要安装webpack-cli

cnpm i webpack-cli -s

npm安装注意两点:
(1)安装时如未指定版本号,则按最新的版本安装,这里webpack安装的是最新的4.17.1的版本

(2)npm install --save和npm install --save-dev的区别:

--save 会把依赖包名称添加到 package.json 文件 dependencies 键下,--save-dev 则添加到 package.json 文件 devDependencies 键下。dependencies是发布后还依赖的,devDependencies是开发时的依赖。

webpack和webpack-cli安装完成后在刚才创建的文件夹里面创建两个文件夹,分别起名为src文件夹和dist文件夹,src文件夹用来存放原始数据和我们将写的JavaScript模块,dist文件夹用来存放打包之后供浏览器读取的文件(包括使用webpack打包生成的js文件以及一个index.html文件)。接下来我们再创建几个文件价和文件,最后的层级如下图所示:


image.png

我们在index.html文件中写入最基础的html代码,它在这里目的在于引入打包后的js文件(这里我们先把之后打包后的js文件命名为./dist/js/index.bundle.js,之后我们还会详细讲述)。

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Webpack Sample Project</title>
  </head>
  <body>
    <div id='root'>
    </div>
    <script src="bundle.js"></script>
  </body>
</html>

我们在Greeter.js中定义一个返回包含问候信息的html元素的函数,并依据CommonJS规范导出这个函数为一个模块:

// Greeter.js
module.exports = function() {
  var greet = document.createElement('div');
  greet.textContent = "Hi there and greetings!";
  return greet;
};

main.js文件中我们写入下述代码,用以把Greeter模块返回的节点插入页面。

//main.js 
const greeter = require('./Greeter.js');
document.querySelector("#root").appendChild(greeter());

3. 配置webpack文件

Webpack拥有很多其它的比较高级的功能(比如说本文后面会介绍的loaders和plugins),这些功能其实都可以通过命令行模式实现,但是正如前面提到的,这样不太方便且容易出错的,更好的办法是定义一个配置文件,这个配置文件其实也是一个简单的JavaScript模块,我们可以把所有的与打包相关的信息放在里面。

继续上面的例子来说明如何写这个配置文件,在当前练习文件夹的根目录下新建一个名为webpack.config.js的文件,我们在其中写入如下所示的简单配置代码,目前的配置主要涉及到的内容是入口文件路径和打包后文件的存放路径。

module.exports = {
    entry: __dirname + "/src/main.js", //已多次提及的唯一入口文件
    output: {
        path: __dirname + '/dist',//打包后的文件存放的地方
        filename: 'js/[name].bundle.js',//打包后输出文件的文件名
    }
}

注:“__dirname”是node.js中的一个全局变量,它指向当前执行脚本所在的目录。

有了这个配置之后,再打包文件,只需在终端里运行webpack(非全局安装需使用node_modules/.bin/webpack)命令就可以了,这条命令会自动引用webpack.config.js文件中的配置选项,示例如下:


1031000-c02a675d8d8c9e56.png

更快捷的执行打包任务

在命令行中输入命令需要代码类似于node_modules/.bin/webpack这样的路径其实是比较烦人的,不过值得庆幸的是npm可以引导任务执行,对npm进行配置后可以在命令行中使用简单的npm start命令来替代上面略微繁琐的命令。在package.json中对scripts对象进行相关设置即可,设置方法如下。

{
  "name": "webpack-sample-project",
  "version": "1.0.0",
  "description": "Sample webpack project",
  "scripts": {
    "start": "webpack" // 修改的是这里,JSON文件不支持注释,引用时请清除
  },
  "author": "zhang",
  "license": "ISC",
  "devDependencies": {
    "webpack": "3.10.0"
  }
}

注:package.json中的script会安装一定顺序寻找命令对应位置,本地的node_modules/.bin路径就在这个寻找清单中,所以无论是全局还是局部安装的Webpack,你都不需要写前面那指明详细的路径了。
npm的start命令是一个特殊的脚本名称,其特殊性表现在,在命令行中使用npm start就可以执行其对于的命令,如果对应的此脚本名称不是start,想要在命令行中运行时,需要这样用npm run {script name}如npm run build,我们在命令行中输入npm start试试,输出结果如下:


1031000-a3e10146ffac4163.png

现在只需要使用npm start就可以打包文件了,有没有觉得webpack也不过如此嘛,不过不要太小瞧webpack,要充分发挥其强大的功能我们需要修改配置文件的其它选项,一项项来看。

Webpack的强大功能

(1)生成Source Maps(使调试更容易)

开发总是离不开调试,方便的调试能极大的提高开发效率,不过有时候通过打包后的文件,你是不容易找到出错了的地方,对应的你写的代码的位置的,Source Maps就是来帮我们解决这个问题的。

通过简单的配置,webpack就可以在打包时为我们生成的source maps,这为我们提供了一种对应编译文件和源文件的方法,使得编译后的代码可读性更高,也更容易调试。

在webpack的配置文件中配置source maps,需要配置devtool,它有以下四种不同的配置选项,各具优缺点,描述如下:


image.png

正如上表所述,上述选项由上到下打包速度越来越快,不过同时也具有越来越多的负面作用,较快的打包速度的后果就是对打包后的文件的的执行有一定影响。

对小到中型的项目中,eval-source-map是一个很好的选项,再次强调你只应该开发阶段使用它,我们继续对上文新建的webpack.config.js,进行如下配置:

module.exports = {
    devtool: 'eval-source-map',
    entry: __dirname + "/src/main.js", //已多次提及的唯一入口文件
    output: {
        path: __dirname + '/dist',//打包后的文件存放的地方
        filename: 'js/[name].bundle.js',//打包后输出文件的文件名
    }
}

cheap-module-eval-source-map方法构建速度更快,但是不利于调试,推荐在大型项目考虑时间成本时使用。

(2)使用webpack构建本地服务器

想不想让你的浏览器监听你的代码的修改,并自动刷新显示修改后的结果,其实Webpack提供一个可选的本地开发服务器,这个本地服务器基于node.js构建,可以实现你想要的这些功能,不过它是一个单独的组件,在webpack中进行配置之前需要单独安装它作为项目依赖

npm install --save-dev webpack-dev-server

devserver作为webpack配置选项中的一项,它的一些配置选项,更多配置可参考devServer详细配置

把这些命令加到webpack的配置文件中,现在的配置文件webpack.config.js如下所示

module.exports = {
  devServer: {
    // contentBase: path.join(__dirname, "dist"),
    headers: {
      'X-foo': '112233'
    },
    // hot: true,
    port: '8081',
    inline: true,
    open: true,
    overlay: true,
    stats: 'errors-only',
    proxy: {
      '/api': {
        target: 'http://news.baidu.com', // 目标接口的域名
        // secure: true,  // https 的时候 使用该参数
        changeOrigin: true,  // 是否跨域
        pathRewrite: {
          '^/api' : ''  // 重写路径
        }
      }
    }
  }
}

package.json中的scripts对象中添加如下命令,用以开启本地服务器:

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

在终端中输入npm run server即可在本地的8080端口查看结果

image.png
(3)Loaders

鼎鼎大名的Loaders登场了!

loader是webpack的核心概念之一,它的基本工作流是将一个文件以字符串的形式读入,对其进行语法分析及转换(或者直接在loader中引入现成的编译工具,例如sass-loader中就引入了node-sass将SCSS代码转换为CSS代码,再交由css-loader处理),然后交由下一环节进行处理,所有载入的模块最终都会经过moduleFactory处理,转成javascript可以识别和运行的代码,从而完成模块的集成。

loader支持链式调用,所以开发上需要严格遵循“单一职责”原则,即每个loader只负责自己需要负责的事情:将输入信息进行处理,并输出为下一个loader可识别的格式。

实际开发中,很少会出现需要自己写loader来实现复杂需求的场景,如果某个扩展名的文件无法快速集成到自动化构建工具里,估计很快就会被抛弃了,大家都那么忙是吧。但是了解loader的基本原理和编译器的基本原理却是非常有必要的。

常用的loaders有:

样式

css-loader : 解析css文件中代码
style-loader : 将css模块作为样式导出到DOM
less-loader : 加载和转义less文件
sass-loader : 加载和转义sass/scss文件

脚本转换编译

script-loader : 在全局上下文中执行一次javascript文件,不需要解析
babel-loader : 加载ES6 代码后使用Babel转义为ES5后浏览器才能解析

Files文件

file-loader:将要加载的文件复制到指定目录,生成请求文件资源URL

url-loader : 多数用于加载图片资源,超过文件大小显示则返回data URL
raw-loader : 加载文件原始内容(utf-8格式)

加载框架

vue-loader : 加载和转义vue组件
react-hot-loader : 动态刷新和转义react组件中修改的部分

loader的使用方法:

1.安装需要的loader

npm install --save-dev xxx-loader
//比如
npm install --save-dev less
npm install --save-dev less-loader
npm install --save-dev css-loader
npm install --save-dev style-loader

2.配置loader

Loaders需要单独安装并且需要在webpack.config.js中的modules关键字下进行配置,Loaders的配置包括以下几方面:

1.Rule.test

Rule.testRule.resource.test 的简写。如果你提供了一个 Rule.test 选项,就不能再提供 Rule.resource。详细请查看 Rule.resourceCondition.test

  1. Rule.use
    应用于模块的 UseEntries 列表。每个入口(entry)指定使用一个 loader。

传递字符串(如:use: [ "style-loader" ])是 loader 属性的简写方式(如:use: [ { loader: "style-loader "} ])。

例如:

use: [
  'style-loader',
  {
    loader: 'css-loader',
    options: {
      importLoaders: 1
    }
  },
  {
    loader: 'less-loader',
    options: {
      noIeCompat: true
    }
  }
]

3.exclude:排除不满足条件的文件夹(这样可以排除webpack查找不必要的文件)
4.include:需要被loader 处理的文件或文件夹
例如:

{
    module: {
        loaders: [{
            test: /\.js$/,
            loader: 'babel-loader',
            include: [
                path.resolve(__dirname, "app/src"),
                path.resolve(__dirname, "app/test")
            ],
            exclude: /node_modules/
        }]
    }
}
(4)Babel

Babel其实是一个编译JavaScript的平台,它可以编译代码帮你达到以下目的:

  • 让你能使用最新的JavaScript代码(ES6,ES7...),而不用管新标准是否被当前使用的浏览器完全支持;
  • 让你能使用基于JavaScript进行了拓展的语言,比如React的JSX;

Babel的安装

Babel其实是几个模块化的包,其核心功能位于称为babel-core的npm包中,webpack可以把其不同的包整合在一起使用,对于每一个你需要的功能或拓展,你都需要安装单独的包(用得最多的是解析Es6的babel-env-preset包和解析JSX的babel-preset-react包)。

我们先来一次性安装这些依赖包

// npm一次性安装多个依赖模块,模块之间用空格隔开
npm install --save-dev babel-core babel-loader babel-preset-env babel-preset-react

当我们使用babel-loader处理js文件的时候,实际上这个babel-loader只是webpack和babel做通信的一个桥梁,用了他之后,webpack和babel做了打通,但实际上,babel-loader并不会帮助我们把es6语法翻译成es5语法,还需要借助一些其它的模块才能够帮助我们把es6语法翻译成es5语法。babel/preset-env就是这样的一个模块,这里面包含了所有把es6转化成es5的规则。装好之后,还需要在webpack里面配置一下
webpack中配置Babel的方法如下:

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/public",//打包后的文件存放的地方
        filename: "bundle.js"//打包后输出文件的文件名
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true//实时刷新
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                exclude: /(node_modules|bower_components)/, //避免转义node_modules 目录或者其他不需要的源代码。
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: [
                            "env", "react"
                        ]
                    }
                },
                exclude: /node_modules/
            }
        ]
    }
};

现在你的webpack的配置已经允许你使用ES6以及JSX的语法了。继续用上面的例子进行测试,不过这次我们会使用React,记得先安装 React 和 React-DOM

npm install --save react react-dom

接下来我们使用ES6的语法,更新Greeter.js并返回一个React组件

//Greeter,js
import React, {Component} from 'react'
import config from './config.json';

class Greeter extends Component{
  render() {
    return (
      <div>
        {config.greetText}
      </div>
    );
  }
}

export default Greeter

修改main.js如下,使用ES6的模块定义和渲染Greeter模块

// main.js
import React from 'react';
import {render} from 'react-dom';
import Greeter from './Greeter';

render(<Greeter />, document.getElementById('root'));

重新使用npm start打包,如果之前打开的本地服务器没有关闭,你应该可以在localhost:8080下看到与之前一样的内容,这说明reactes6被正常打包了。

image.png

Babel的配置

Babel其实可以完全在 webpack.config.js 中进行配置,但是考虑到babel具有非常多的配置选项,在单一的webpack.config.js文件中进行配置往往使得这个文件显得太复杂,因此一些开发者支持把babel的配置选项放在一个单独的名为 ".babelrc" 的配置文件中。我们现在的babel的配置并不算复杂,不过之后我们会再加一些东西,因此现在我们就提取出相关部分,分两个配置文件进行配置(webpack会自动调用.babelrc里的babel配置选项),如下:

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/public",//打包后的文件存放的地方
        filename: "bundle.js"//打包后输出文件的文件名
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true//实时刷新
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            }
        ]
    }
};

//.babelrc
{
  "presets": ["react", "env"]
}

到目前为止,我们已经知道了,对于模块,Webpack能提供非常强大的处理功能,那哪些是模块呢。

一切皆模块

Webpack有一个不可不说的优点,它把所有的文件都都当做模块处理,JavaScript代码,CSS和fonts以及图片等等通过合适的loader都可以被处理。

(5)处理 CSS

webpack提供两个工具处理样式表,css-loaderstyle-loader,二者处理的任务不同,css-loader使你能够使用类似@importurl(...)的方法实现 require()的功能,style-loader将所有的计算后的样式加入页面中,二者组合在一起使你能够把样式表嵌入webpack打包后的JS文件中。

继续上面的例子

//安装
npm install --save-dev style-loader css-loader
//使用
module.exports = {

   ...
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader"
                    }
                ]
            }
        ]
    }
};

请注意这里对同一个文件引入多个loader的方法。因为weback对loader的执行是自下而上的,所以css-loader要放到style-loader的下面。

接下来,在app文件夹里创建一个名字为"main.css"的文件,对一些元素设置样式

/* main.css */
html {
  box-sizing: border-box;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
}

*, *:before, *:after {
  box-sizing: inherit;
}

body {
  margin: 0;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

h1, h2, h3, h4, h5, h6, p, ul {
  margin: 0;
  padding: 0;
}

我们这里例子中用到的webpack只有单一的入口,其它的模块需要通过 import, require, url等与入口文件建立其关联,为了让webpack能找到”main.css“文件,我们把它导入”main.js “中,如下

//main.js
import React from 'react';
import {render} from 'react-dom';
import Greeter from './Greeter';

import './main.css';//使用require导入css文件

render(<Greeter />, document.getElementById('root'));

通常情况下,css会和js打包到同一个文件中,并不会打包为一个单独的css文件,不过通过合适的配置webpack也可以把css打包为单独的文件的。

上面的代码说明webpack是怎么把css当做模块看待的,咱们继续看一个更加真实的css模块实践。

CSS module

在过去的一些年里,JavaScript通过一些新的语言特性,更好的工具以及更好的实践方法(比如说模块化)发展得非常迅速。模块使得开发者把复杂的代码转化为小的,干净的,依赖声明明确的单元,配合优化工具,依赖管理和加载管理可以自动完成。

不过前端的另外一部分,CSS发展就相对慢一些,大多的样式表却依旧巨大且充满了全局类名,维护和修改都非常困难。

被称为CSS modules的技术意在把JS的模块化思想带入CSS中来,通过CSS模块,所有的类名,动画名默认都只作用于当前模块。Webpack对CSS模块化提供了非常好的支持,只需要在CSS loader中进行简单配置即可,然后就可以直接把CSS的类名传递到组件的代码中,这样做有效避免了全局污染。具体的代码如下

module.exports = {

    ...

    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true, // 指定启用css modules
                            localIdentName: '[name]__[local]--[hash:base64:5]' // 指定css的类名格式
                        }
                    }
                ]
            }
        ]
    }
};

我们在app文件夹下创建一个Greeter.css文件来进行一下测试

/* Greeter.css */
.root {
  background-color: #eee;
  padding: 10px;
  border: 3px solid #ccc;
}

导入.root到Greeter.js中

import React, {Component} from 'react';
import config from './config.json';
import styles from './Greeter.css';//导入

class Greeter extends Component{
  render() {
    return (
      <div className={styles.root}> //使用cssModule添加类名的方法
        {config.greetText}
      </div>
    );
  }
}

export default Greeter

放心使用把,相同的类名也不会造成不同组件之间的污染。

4159771164-5a2dfb7918c27_articlex.png

CSS modules 也是一个很大的主题,有兴趣的话可以去其官方文档了解更多。

CSS预处理器

SassLess 之类的预处理器是对原生CSS的拓展,它们允许你使用类似于variables, nesting, mixins, inheritance等不存在于CSS中的特性来写CSS,CSS预处理器可以这些特殊类型的语句转化为浏览器可识别的CSS语句,

你现在可能都已经熟悉了,在webpack里使用相关loaders进行配置就可以使用了,以下是常用的CSS 处理loaders:

  • Less Loader
  • Sass Loader
  • Stylus Loader

不过其实也存在一个CSS的处理平台-PostCSS,它可以帮助你的CSS实现更多的功能,在其官方文档可了解更多相关知识。

举例来说如何使用PostCSS,我们使用PostCSS来为CSS代码自动添加适应不同浏览器的CSS前缀。

首先安装postcss-loaderautoprefixer(自动添加前缀的插件)

npm install --save-dev postcss-loader autoprefixer

接下来,在webpack配置文件中添加postcss-loader,在根目录新建postcss.config.js,并添加如下代码之后,重新使用npm start打包时,你写的css会自动根据Can i use里的数据添加不同前缀了。

//webpack.config.js
module.exports = {
    ...
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    }
}
// postcss.config.js
module.exports = {
    plugins: [
        require('autoprefixer')
    ]
}

至此,本文已经谈论了处理JS的Babel和处理CSS的PostCSS的基本用法,它们其实也是两个单独的平台,配合webpack可以很好的发挥它们的作用。接下来介绍Webpack中另一个非常重要的功能-Plugins

(6)插件(Plugins)

插件(Plugins)是用来拓展Webpack功能的,它们会在整个构建过程中生效,执行相关的任务。
Loaders和Plugins常常被弄混,但是他们其实是完全不同的东西,可以这么来说,loaders是在打包构建过程中用来处理源文件的(JSX,Scss,Less..),一次处理一个,插件并不直接操作单个文件,它直接对整个构建过程其作用。

Webpack有很多内置插件,同时也有很多第三方插件,可以让我们完成更加丰富的功能。

使用插件的方法

要使用某个插件,我们需要通过npm安装它,然后要做的就是在webpack配置中的plugins关键字部分添加该插件的一个实例(plugins是一个数组)继续上面的例子,我们添加1个插件,

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const extractPlugin = require('extract-text-webpack-plugin');

module.exports = {
    ....
    plugins: [ //相关插件配置
        new HtmlWebpackPlugin({
            title: 'test page', //html标题
            filename: 'index.html', //打包后的html文件名,默认index.html
            template: './index.html', // html的源文件
            inject: true, //默认true,意为script标签位于html文件的 body 底部
            cache: true, //默认true,表示内容变化的时候生成一个新的文件
            chunks: ['index'], //表示编译时用到的入口文件
            date: new Date(),
            // excludeChunks: ['index']//表示编译时排除的入口文件
        })
    ]
}

这就是webpack插件的基础用法了,下面给大家推荐几个常用的插件

HtmlWebpackPlugin

这个plugin曝光率很高,他主要有两个作用

1.为html文件中引入的外部资源如script、link。动态添加每次compile后的hash,防止引用缓存的外部文件问题
2.可以生成创建html的入口文件,比如单页面可以生成一个html文件入口,配置N个html-webpack-plugin可以生成N个页面入口

安装

npm install --save-dev html-webpack-plugin

配置可参考html-webpack-plugin的配置
再次执行npm start你会发现,build文件夹下面生成了index.html,并且页面body标签的最后面添加了生成的script.

image.png

image.png

Hot Module Replacement

Hot Module Replacement(HMR)也是webpack里很有用的一个插件,它允许你在修改组件代码后,自动刷新实时预览修改后的效果。

在webpack中实现HMR也很简单,只需要做两项配置

  1. 在webpack配置文件中添加HMR插件;
  2. 在Webpack Dev Server中添加“hot”参数;

不过配置完这些后,JS模块其实还是不能自动热加载的,还需要在你的JS模块中执行一个Webpack提供的API才能实现热加载,虽然这个API不难使用,但是如果是React模块,使用我们已经熟悉的Babel可以更方便的实现功能热加载。

整理下我们的思路,具体实现方法如下

  • Babelwebpack是独立的工具
  • 二者可以一起工作
  • 二者都可以通过插件拓展功能
  • HMR是一个webpack插件,它让你能浏览器中实时观察模块修改后的效果,但是如果你想让它工作,需要对模块进行额外的配额;
  • Babel有一个叫做react-transform-hrm的插件,可以在不对React模块进行额外的配置的前提下让HMR正常工作;

还是继续上例来实际看看如何配置

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true,
        hot: true
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html"//new 一个这个插件的实例,并传入相关的参数
        }),
        new webpack.HotModuleReplacementPlugin()//热加载插件
    ],
};

安装react-transform-hmr

npm install --save-dev babel-plugin-react-transform react-transform-hmr

配置Babel

// .babelrc
{
  "presets": ["react", "env"],
  "env": {
    "development": {
    "plugins": [["react-transform", {
       "transforms": [{
         "transform": "react-transform-hmr",

         "imports": ["react"],

         "locals": ["module"]
       }]
     }]]
    }
  }
}

现在当你使用React时,可以热加载模块了,每次保存就能在浏览器上看到更新内容。

(6)产品阶段的构建

目前为止,我们已经使用webpack构建了一个完整的开发环境。但是在产品阶段,可能还需要对打包的文件进行额外的处理,比如说优化,压缩,缓存以及分离CSS和JS。

对于复杂的项目来说,需要复杂的配置,这时候分解配置文件为多个小的文件可以使得事情井井有条,以上面的例子来说,我们创建一个webpack.production.config.js的文件,在里面加上基本的配置,它和原始的webpack.config.js很像,如下

// webpack.production.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js", //已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'null', //注意修改了这里,这能大大压缩我们的打包代码
    devServer: {
        contentBase: "./public", //本地服务器所加载的页面所在的目录
        historyApiFallback: true, //不跳转
        inline: true,
        hot: true
    },
    module: {
        rules: [{
            test: /(\.jsx|\.js)$/,
            use: {
                loader: "babel-loader"
            },
            exclude: /node_modules/
        }, {
            test: /\.css$/,
            use: ExtractTextPlugin.extract({
                fallback: "style-loader",
                use: [{
                    loader: "css-loader",
                    options: {
                        modules: true
                    }
                }, {
                    loader: "postcss-loader"
                }],
            })
        }]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html" //new 一个这个插件的实例,并传入相关的参数
        }),
        new webpack.HotModuleReplacementPlugin() //热加载插件
    ],
};
//package.json
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack",
    "server": "webpack-dev-server --open",
    "build": "NODE_ENV=production webpack --config ./webpack.production.config.js --progress"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
...
  },
  "dependencies": {
    "react": "^15.6.1",
    "react-dom": "^15.6.1"
  }
}

注意:如果是window电脑,build需要配置为"build": "set NODE_ENV=production && webpack --config ./webpack.production.config.js --progress".

好的,至此一个完整的webpack项目已经构建完毕。接下来总结一些项目中常用的优化和技巧

webpack在项目中的使用技巧和对项目的优化

参考文章
webpack教程
入门 Webpack,看这篇就够了
devserver的基本配置

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容