webpack 的出现真正意义上使前端的工程化构建趋于完美,本篇将详细的讲讲 webpack 常用的配置选项。
webpack,browserify 和 RequireJs
webpack算是近段时间来最火的一个模块打包工具了,要说及它的优点就不得不谈谈它的前辈browserify和RequireJs。
RequireJs
当时有一个开发人员一直头疼的问题:js 有方法或属性的依赖问题,所以引入 js 的顺序必须严格按照依赖的先后顺序来,这为我们多次加载 js 造成了困难,业务复杂的情况下使维护变得非常困难。为了解决这个问题,RequireJs出现了,它使模块的加载变得井井有条。
RequireJs是基于AMD规范的,也就是需要引入依赖的写法,如果写过 ng1 的童鞋应该对这种写法很熟悉,类似这样:
1 2 3 4 5 6 7 8 9 10
| define(function () { return function (a, b) { console.log(a + b); }; });
require(["module"], function (module) { module(1 + 2); });
|
写惯了CommonJS写法的我,再回头看看这种写法,确实觉得累赘,但当时确实是一种很具有开创新的思想。RequireJs的require方法是一个异步方法,它可以保证在对应依赖都加载完成的情况下才执行回调函数,这使维护起来异常简便,你也不用担心加载顺序的问题,你只要关心依赖之间的关系就可以了。但这样还有个问题就是每个模块还是要引入对应的 js,这样会发起多次 http 请求,对网页性能的影响很大,还好RequireJs提供了一个把各个模块整合到一个文件的工具,解决了多次加载文件的问题。
这个时候的RequireJs已经有webpack的雏形了。
browserify
随着node的发展,前端的工程化被不断的推上日程,其中有一个最主要的问题是工程化道路上所必须面对的:目前的浏览器还是只能支持ES5的语法,而node环境下模块都是用CommonJS规范构建的,怎么才能把CommonJS的语法编译成浏览器认识的ES5语法呢?在这探索的路上,browserify就应此出现。
browserify做了两件事:
- 对用CommonJS规范构建的node模块进行转换和包装。
- 对node的大多数包进行了适配,使它们能更好的在浏览器里运行。
保证了node模块能在浏览器顺畅运行,踏出了前端工程化的重要一步。
webpack
webpack正是吸取了前辈的特点,将他们整合到一起,形成了一套比较完善的打包构建系统,它既有完善的打包流程,又能让node模块完美的兼容各类浏览器。
随着webpack的不断发展,生态圈越来越大,渐渐成了主流的打包构建工具,前端自动化,工程化已经不再只是设想了,webpack已经帮我们实现了!
webpack 详解
webpack 常见配置讲解
既然webpack那么好用,我们肯定要好好看看它是怎么配置的,写出适合自己项目的配置,大大提高开发效率!
注意!本人用的是webpack@2.x版本,可能会与webpack@1.x版本的有些写法会不一样,下面会提到
webpack 官网 已经把配置选项讲解的很详细了(现在文档也有中文版的翻译了,很贴心),太细节的就不深入了,我会把一些常见的配置项拿出来,说说他们的用途和一些可能会遇到的坑,先贴一张我个人项目中部分的webpack的配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); const ExtractTextPlugin = require("extract-text-webpack-plugin");
const ROOT_PATH = path.resolve(__dirname);
const SRC_PATH = ROOT_PATH + "/src";
const DIST_PATH = ROOT_PATH + "/dist";
const NODE_MODULES_PATH = ROOT_PATH + "/node_modules"; const __DEV__ = process.env.NODE_ENV !== "production"; module.exports = { devtool: "source-map", context: ROOT_PATH, entry: { libs: [ "react", "react-dom", "redux", "react-redux", "react-router", "react-router-redux", ], app: [SRC_PATH + "/app.jsx"], }, output: { path: DIST_PATH, filename: __DEV__ ? "js/[name].js" : "js/[name].[chunkhash].js", chunkFilename: __DEV__ ? "js/[name].js" : "js/[name].[chunkhash].js", publicPath: "", }, module: { rules: [ { test: /\.(jsx|js)$/, include: SRC_PATH, loader: "babel-loader", }, ], }, resolve: { modules: [SRC_PATH, "node_modules"], alias: { "react-router": NODE_MODULES_PATH + "/react-router/lib/index.js", "react-redux": NODE_MODULES_PATH + "/react-redux/lib/index.js", }, extensions: [".js", ".json", ".jsx"], }, plugins: [ new HtmlWebpackPlugin({ minify: { collapseWhitespace: true, }, chunks: ["app", "libs"], template: SRC_PATH + "/app.ejs", }), new ExtractTextPlugin({ filename: "css/[name].[contenthash].css", disable: __DEV__, allChunks: true, }), new webpack.optimize.CommonsChunkPlugin({ name: ["libs", "manifest"], minChunks: Infinity, }), ], watch: true, devServer: { contentBase: path.join(__dirname, "dist"), compress: true, port: 9000, }, };
|
我们不难发现,日常开发比较常用的就那么几个属性:entry、output、module、resolve、plugins、devtool,context,watch,devServer只不过根据项目的复杂度,具体的配置会有不同的变化,下面介绍一下具体每个属性的作用和对应的参数的作用。
entry
根据词意,可以知道这是入口,简而言之就是你需要打包的入口 js 文件,路径和名称就在这里定义的,webpack打包的时候会找到这个入口文件,然后根据入口文件写入的依赖解析路径去引入对应的模块。写法也比较灵活,可以写成以下三种形式:
- 单页面应用入口,String 类型
- 多页面应用入口,Array 类型
1
| entry: ["./src/app1.js", "./src/app2.js"];
|
- 多页面应用入口,Object 类型
1 2 3 4
| entry: { chunk1: "./src/app1.js", chunk2: "./src/app2.js" }
|
如果想把不同的chunk区别开来,推荐用Object的形式,Object的key值对应了每个chunk的名称,使结构更清晰。
output
该属性对应了打包的输出配置项,类似这样:
1 2 3 4 5 6
| output: { path: './dist', filename:'js/[name].bundle.js', chunkFilename:'js/[name].js', publicPath: '/' }
|
path 代表了打包后输出的目录路径,为绝对路径。
filename 代表了打包输出文件的名称,对应的是从入口进入的chunk打包后的文件名,其中[name]属性会被chunk的名字替换,[id]会被模块 id(chunk id)所替换,[hash]会被每次构建产生的唯一的hash值替换,[chunkhash]会被根据chunk内容生成的hash值替换。
chunkFilename 是为了那些不是从标准入口进入的chunk命名用的,比较常见的就是通过CommonsChunkPlugin打包基础模块,比如react、redux这类的模块,而不是用户自己写的chunk模块,命名规则参考filename,是一样的。
publicPath这个配置项是为一些外部引入的资源如(图片,文件等)设置外部访问的公共URL,为什么要这么做呢?原因其实很简单,一句话概括就是开发环境和生产环境的不同,举个栗子:
比如你在开发环境写代码的时候你有一张图片是这么引入的
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| background-image:'../../img/login.png'
├── app.html ├── app.js ├── css │ └── index.css ├── img │ └── logo.png └── page └── login ├── index.css └── login.js
|
之后不管你是启动本地服务器或者发布到正式环境,都会进行一次打包,不管打包进内存或到某个输出目录,你的目录结构可能就变成这样:
1 2 3 4 5 6
| ├── css │ └── app.css ├── img │ └── logo.png └── index.html
|
很显然,目录的层级发生了变化,这时候你原先写的相对路径就变得不可靠了,会因找不到资源而报 404,publicPath就是为了解决这个而提出的,它可以是相对路径也可以是绝对路径,以下摘一段官网的配置说明:
1 2 3 4 5 6
| publicPath: "https://cdn.example.com/assets/", publicPath: "//cdn.example.com/assets/", publicPath: "/assets/", publicPath: "assets/", publicPath: "../assets/", publicPath: "",
|
它当做相对路径写的时候可以相对于自己本身或者服务器的根目录的,所以我们之前如果设置了publicPath,比如这样:
1 2
| publicPath: "/img/"; publicPath: "../img/";
|
那么最后我们在app.css里面看到的路径就会是这样:
1 2 3
| background-image:'/img/login.png' background-image:'../img/login.png'
|
怎么样,这样就很清晰了吧。这里还得提一点需要注意的, output.publicPath只是默认构建的时候的全局配置,有些loader也有自己的publicPath,这就看具体情境了,如果loader也配置了,那默认就是以loader配置的为主。
有些童鞋很惧怕这种属性,觉得和Path很像,就默认是差不多用处了,不会再去深究。这样对知识的积累很不好,记住一个原则,配置或属性只是为了给我们提供方便,没必要去惧怕它,都是为了解决某些问题而提出的,当我们明白它的用途,我们才能更好的解读配置的意义。
devtool可以让打包后的文件支持source-map,以对打包压缩后的代码进行调试,贴一张官网的配置参数图:
1 2 3 4 5 6 7 8 9
| devtool: "source-map", devtool: "inline-source-map", devtool: "eval-source-map", devtool: "hidden-source-map", devtool: "cheap-source-map", devtool: "cheap-module-source-map", devtool: "eval",
|
context
该配置项设置webpack的主目录entry和 module.rules.loader选项相对于此目录解析,也就是以设置的目录为基准解析路径。
module
这个选项为了处理项目中的不同类型的模块,配置也比较复杂,本文只拿常用的出来讲解,想看详细的配置说明请看官网,之前注释里说的webpack不同版本的问题,这里就有体现,对于webpack@1.x版本下,module的配置可能是下面这样:
1 2 3 4 5 6 7
| module: { loaders: [ {test: /\.js$/, loader: 'babel'}, {test: /\.css$/, loader: 'style!css'}, {test: /\.(jpg|png|gif|svg)$/, loader: 'url?limit=8192} ] }
|
而webpack@2.x版本下,则写法统一改成这样:
1 2 3 4 5 6 7 8 9
| module: { rules: [ { test: /\.(jsx|js)$/, include: SRC_PATH, loader: "babel-loader", }, ]; }
|
值得一提的是,不仅写法变了,webpack@2.x以后,-loader都不能被省略,不然会报语法错误。
rules是module的核心属性,它会提供一种规则数组,创建模块时,会去匹配并修改模块的创建方式。每个规则可以分为三部分条件(condition),结果(result)和嵌套规则(nested rule)。
条件(condition)很好理解,举个栗子:
1 2
| import "./css/index.css";
|
条件(condition)包括了被引入文件./css/index.css和导入这个文件的模块app.js两个文件的绝对路径。你也可以通过制定test的规则去匹配和筛选条件(condition)所匹配的文件流。
结果(result)里面包含了一些loader,当条件(condition)满足时,会去匹配对应的被引入的文件流,对这些文件进行处理,生成对应的 js 模块。简单来说,对引入文件的预处理就在这里面,比如把 ES6 语法编译成 ES5,把 JSX 编译成 ES5,把引入的 css,img 转换成 js 模块等等。
嵌套规则(nested rule)可以使用属性rules和oneOf指定嵌套规则。这些规则用于在规则条件(rule condition)匹配时进行取值。
resolve
这个选项能设置模块如何被解析,简单来说就是通过一定的规则去预定义webpack查找模块的方式,举个简单的栗子:
比如你在app.js这样引入一个login 模块
1 2
| import login from "./router/login";
|
如果你设置了resolve参数,比如这样:
1 2 3 4
| alias: { 'login':path.resolve(__dirname, 'src/router/login'), }
|
你可以把之前的相对路径直接替换成:
1 2
| import login from "login";
|
这样写是不是简洁不少?当webpack查找login 模块时,会直接根据你设置的绝对路径去查找,当层级很深的时候,再按相对路径去找明显太蠢了,这样写不仅省时,代码的可读性也更高了,下面简单介绍一下它的其他几个参数(比较常用)。
extensions是用来自动解析模块扩展名的,这个懒人必备,哈哈,写法如下:
1 2
| extensions: [".js", ".json", ".jsx"];
|
这样你再引入模块的时候就不用写扩展名了:
1 2 3
| import userList from "login/userList.jsx"; import userList from "login/userList";
|
设置之后,webpack再解析模块的时候会自动补全扩展名。
modules告诉webpack解析模块时应该搜索的目录。默认是搜索node_modules,搜索方式类似node通过相对路径一层层往上找。当我们想让webpack搜索指定目录,提高搜索效率的时候,也可以这么写
1 2
| modules: [path.resolve(__dirname, "src"), "node_modules"];
|
这样webpack会在搜索node_modules之前先搜索你指定的目录,此路径应是绝对路径。
plugins
plugins为webpack的插件列表,这个看具体插件,不同插件的写法不同,但都是实例化了一个对象,比如这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| plugins: [ new HtmlWebpackPlugin({ minify: { collapseWhitespace: true, }, chunks: ["app", "libs"], template: SRC_PATH + "/app.ejs", }), new ExtractTextPlugin({ filename: "css/[name].[contenthash].css", disable: __DEV__, allChunks: true, }), new webpack.optimize.CommonsChunkPlugin({ name: ["libs", "manifest"], minChunks: Infinity, }), ];
|
善用插件可以帮我们简化开发流程,之后的博客会介绍一下比较常用webpack插件及其配置。
watch
watch模式意味着在初始构建之后,webpack将继续监听任何已解析文件的更改,这个配置项我们基本不用去设置,因为开了下面这个配置项是默认开启watch模式的。
devServer
devServer是webpack的本地服务器,它使我们的开发变的自动而高效,由于它的配置项很多,我们这里也只讲常用的几个。
contentBase告诉服务器从哪里提供内容,webpack的本地服务器本质上是一个通过 node 启动的本地资源服务器,这里要了解个概念,通过本地服务器打包是不会生成实体文件的,而是会写进内存里面,既然没实体文件,那我们能提供的静态资源也只能是我们本地的资源文件,这就是contentBase的作用,默认是项目根目录:
当然你也可以自定义路径,比如我有一个目录结构是这样的:
1 2 3 4 5 6 7 8
| . ├── README.md ├── node_modules ├── package.json ├── src │ ├── app.js │ └── index.html └── webpack.config.js
|
当你没设置contentBase时
1 2 3 4
| devServer: { compress: true, port: 9000 }
|
你启动本地服务器后,因为默认是项目根目录,也就是这样
很显然,根目录下面没有静态资源可以加载的,我们的静态资源都在src里面,所以会出现下面的情况
当我们进入src里面的时候发现资源加载成功了
所以我们可以试着把contentBase加上
1 2 3 4 5
| devServer: { contentBase: __dirname+'/src/', compress: true, port: 9000 }
|
再启动本地服务器,会看到这么一句话
1
| content is served from /Users/fengji/Desktop/demo/src/
|
代表我们设置成功了,这时候你打开 9000 端口可以看到这样
这样就请求到了我们本地资源,要是你嫌设置麻烦,推荐使用html-webpack-plugin它会帮你把资源路径正确定义到你 html 页面的文件夹下。
compress代表是否启用gzip 压缩,推荐都设置为true,加载压缩完后的资源能加快构建速度。
port为你要开启服务器的端口。
inline设置为true,webpack会把一段实时刷新页面的脚本内联进你打包后的bundle文件里,你可以在控制台实时看到构建的信息。设置为flase,则用iframe内嵌 html 的形式构建,消息会实时显示在页面上,两种方式都可以用,个人比较偏向启用,控制台看起来直观一点。
hot模式为不刷新页面的情况下进行模块的热替换,这个才是自动化构建的精髓啊,强烈推荐开启!来体验实时构建的快感吧。
多提一句,要开启hot模式,还需要一个插件的支持webpack.HotModuleReplacementPlugin,直接用就行了,也很方便。
配置讲解差不多就这么多了,有哪里说的不对的,欢迎在底下评论,大家一起交流进步,后面可能会讲一些插件的用法,或者构建一个完整项目的流程,怎么区分生产和开发环境等等。。
备注:本篇博客皆为博主原创,转发请标明出处。