构建一个小项目——FlyBird,学习webpack和react。
(本文成文于2017/2/25)
从webpack开始
本篇从零开始,详细记录webpack的各个方面。
文章中将会放入很多链接以便扩展,我也会归纳总结,不读扩展不会影响到对本文的理解,但是有时间还是看看吧。
声明:
在阅读本文列出的链接文章时,若遇到与本文不同的,因为文章的时效性问题——
请以本文为标准
当前时间2017/2/26 在此之后出现的文章,读者请注意对比,自行判断
-
核心资料:
webpack2官网doc中文版——小书 后文中的简称—小书—专指此链接
此文档非常详细,最近一次修订时间为——2017 年 2 月十几号
目前阅读起来,尽管放心。本文为教程,小书为手册,二合一,棒棒哒。(您可以亲自去看看他是啥时候又更新了,如果依然比较新,或许很多问题,可以在里边找到答案) -
其他较完整的小书型资料:
webpack_github库doc
webpack.js.org(小书的英文原文)
webpack for React英文
webpack傻瓜教程(较老)
webpack中文教程——赵达(老书)后边的虽然是老书,但是,多条资料,就多一条路,不是么?
-
有助于学习理解的代码库
在文章结尾
开始
最近在学习react,难免看到网上各种webpack+react的文章,发现有些很全面的资料,内容却有些过时(比如有个gitbook的书,是在react还没有分离react react-dom的时候写的),而有的资料则虽然挺新,但是往往只谈一个方面。
种种原因,我决定,结合官网,记录下webpack的各个方面,系统学习一下。
从零开始构建小项目-FlyBird(源代码可在文章结尾处找到)
这是原始数据目录(原生写的)
可以看到,整个项目有一些js文件,一堆img文件,一个css文件。将来我们也要一步步亲自实现他们,这个目录展示了整个项目大概需要些什么。让我们使用webpack构建工作流来管理未来我们将要写的代码吧。
webpack安装与配置
1.npm
创建一个文件夹,并在文件夹下打开命令行
我们需要node-npm来安装和运行webpack,关于node和npm不懂得同学请自行百度。
拥有node-npm后
在当前文件夹初始化npm的package.json文件npm init
相关问题随便填。这会创建 package.json文件,不用担心问题填错,你可以之后修改它。
这句命令就表示,我们把当前文件夹,初始化为一个npm包,它处于npm的管理之下。
我们可以通过npm下载其他人的包,构成了自己包的依赖;当我们完成了我们的包,也可以发布它,让别人下载。最主要的是使用npm来管理依赖。
package.json文件用来配置当前包,配置文件含有很多属性,反映着包的不同信息,后文中,遇到一个介绍一个,而不做全面介绍。
(详情请移步 package.json属性详解)
当init后,我们有如下package.json
//package.json
{
"name": "mydemo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
- name,version是必须的,也是最重要的,他们表示包的名称和版本,构成了包的唯一标示。假如我们只是用npm来管理依赖,他们自然不重要,但是如果我们在制作一个供他人使用的工具包,便必须正确的书写他们,以便他人查找和使用。具体的规则请查看之前的网站。
- description main author license
这些属性,仅在需要制作工具包时关注。description author license 顾名思义,main则表示当别人引用(require)你的工具包时,入口文件在哪。 - scripts则与实际开发过程有关,是我们会经常用到的。
通过它,我们可以定义npm脚本,比如
//只看,不用写后边需要动手写,我会说
"scripts": {
"build": "webpack",
"dev": "webpack-dev-server --devtool eval --progress --colors --hot --content-base build"
}
- 当我们在命令行
npm run build
时,就等于webpack
,原理很简单,在执行npm run
命令时,会新创建一个Shell,并将当前目录的/node_modules/.bin/
加入到PATH
变量(环境变量),之后运行脚本命令,结束后,将PATH
恢复原样。
详情请移步:
阮一峰——npm scripts使用指南
npm-scripts官方文档(英文)
好吧,其实太高级的并木有卵用,目前,只知道可以懒省事就行啦(^o^)/~,比如npm run dev
,如果每次都打一长串,会疯的。
2.webpack
是时候安装webpack了!不过在安装之前,还要介绍一些概念。
概念——开发与发布
一个项目通常都会有,开发,发布这两种状态,也就是自己瞎捣鼓,和放网上让别人用。不管是哪种状态,我们的项目都可能会依赖别人的包。那么自然而然,因为状态的不同,依赖的包也不太一样。比如,在开发阶段,往往需要进行测试,看看能不能跑通,而测试工具显然在发布时是不必要的。
为了分别管理,npm在package.json提供了这样的字段
- devDependencies 声明—仅开发依赖
- dependencies 依赖包
在下载别人的包时,如果只期望下载一个发布的可运行版本,而不希望对此包进行任何开发,可以利用npm install --production
仅下载dependencies字段中的依赖。
好吧,对于我们的项目并没有什么卵用,因为我们将使用webpack管理整个工作流程,npm只是用来下载东西(囧),我只是说明一下。
开发与部署
开发到一定阶段需要发布一个版本,我们往往需要一个文件夹来保存整合后的项目。这个过程,就叫做部署"deploy"。这也是webpack的工作,会用到一个和开发阶段不同的webpack配置文件,它只是将输出目录换成了另一个而已。
在我们的小项目进入到这个阶段后,再细说。
安装webpack
npm install webpack --save-dev
这将在本地(当前文件夹下)安装webpack并在开发依赖字段(devDependencies )中保存信息。
webpack显然只需要在开发阶段用到
如果想要运行它,进入node_modules/.bin
,并运行它webpack
。
当然,我们也可以在上文提到的package.json中的scripts字段中配上
//动手写
"scripts": {
"build": "webpack" //由于scripts将node_modules/.bin加入到环境变量PATH中,所以脚本Shell可以搜索到webpack指令,`npm run a`等价的`webpack`也就可以运行了。
},
注意,不推荐全局安装 webpacknpm i webpack g
。这会锁定 webpack 到指定版本,并且在使用不同的 webpack 版本的项目中可能会导致构建失败。
webpack的使命
从上边目录图中也可以看到,我们需要用webpack管理很多东西,依赖包,自己写的jsx,css,各种各样的图片,也许还有字体。
为了性能,我们需要根据依赖关系,对各种jsx,css,img进行压缩整合为数量跟少的几个文件。
为了开发方便,我们需要浏览器自动刷新,sass/less自动转换的功能等等。
这些,就是webpack的使命,让我们的开发更高效。
构建目录
可以简单的划分为来源——去处,如图
- app文件夹,就所有我们手写的文件放的地方
-
build文件夹,则是经过webpack打包,自动生成的文件的去处。
在build文件夹下,新建index.html用来表示我们的索引页
它长这样,其中的div用来给做一些页面修改什么的用
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>FlyBird</title>
</head>
<body>
<div id="div1"></div>
<script src="bundle.js"></script>
</body>
</html>
对于部署文件夹来说,一般是这样
- dist文件夹,用来保存我们的发布版。
所以,最终,在app中写东西,打包到build中调试、看效果,不错的话,发布到dist文件夹里。
配置webpack
架子已经搭好了,现在,我们需要控制webpack的各种行为,添加各种功能。
可以有三种途径
- cli——即命令行形式,一般都会动过package.json中写入scripts字段的形式
- 配置文件
webpack.config.js
文件中写入字段,webpack在启动时会读取它,并依据其工作 - node api——其实配置文件也算node api,更广义的来讲,node api是一套配置文件的生成系统,根据不同的输入(从cli传参数
--a=b
),实现不同的配置。
对于第三种,本文不做过多介绍,在文章最后,我会贴出一位前辈的node api模板地址,有兴趣的同学可以移步文章结尾。
注意:当使用node api同时又使用了webpack.config.js时,webpack.config.js将不会生效
我们通过配置文件,此时你的目录应该是这样(新建文件)
因为我们会往两个地方打包,那么自然,得俩配置文件了。
-
webpack.config.js
目标build -
webpack.production.config.js
目标dist
接下来,通过webpack.config.js的配置来详细介绍配置属性(到部署时候再说后production配置)
进行如下配置
//动手写
// webpack.config.js
var path = require("path");
module.exports = {
entry: path.join(__dirname, '/app/main.js'),
output: {
path: path.join(__dirname, '/build'),//打包后的文件存放的地方
filename: "bundle.js"//打包后输出文件的文件名
}
};
__dirname 是当前运行的js所在的目录
模块的依赖书写方式:
方式取决于模块系统,上文中很明显是commonJS的方式。
webpack支持最新的模块系统——es6的模块系统(ES6 module import),
但是,注意:这并不是说它支持es6。
也就是说,我们可以直接使用如import/export语句来导入模块,但是,如果我们想要打包含有其他es6语法的模块时,依然需要babel转换器
关于模块系统这里有大略的介绍—— webpack中文指南——赵达(模块系统)
一个一个来说
基本配置属性介绍(一)——entry
1.基本概念
表示模块的来源,入口,起点。它的值可以是
- 字符串
entry: '某模块'
表示一个单一模块作为起点(当然,单一入口也可以用后边两种写),把这个模块需要的东西打包成一堆 - 数组
entry: ['模块1', '模块2']
模块1与模块2互相之间并没有依赖,但是我们还想把他们打包在一起,此时就用数组值的方式,webpack从左到右把各个模块及他们的依赖打包在一起([第一堆,第二堆]),然后从左到右首尾相连的拼接成一个文件。最终也是打包成一堆。 - 对象
//只看
entry: {
page1: "./page1",
page2: ["./entry1", "./entry2"]
}
这将会打包成两堆,每一堆都有一个[name]属性,值为对应entry中的属性名。
//只看
output: {
// Make sure to use [name] or [id] in output.filename
// when using multiple entry points
path: '/build',
filename: "[name].bundle.js",
chunkFilename: "[id].bundle.js"
}
- 在filename中使用[name]来生成对应打包堆特殊的名字。
- 混合使用,不在赘述
entry的官方文档
2.各种问题
-
entry值的写法问题
上网大眼一看,有三种
entry: path.resolve(__dirname, 'app');
entry: path.join(__dirname, 'app');
entry: __dirname + '/app';
在linux,mac环境下这三种是一样的,而在windows环境下,最后一种是错误的
-
这是因为,node的path模块的方法,在解析路径时候,会使用当前平台的路径分隔符,windows的是
\
。
如果我们直接使用+
号拼接,自然发生错误。
-
path.resolve与path.join
join方法仅仅进行路径拼接
resolve方法则会做一些解析工作,它会将参数从右到左拼起来,直到遇到一个绝对路径。path的node官网文档总结一下路径符号
'.'表示当前目录
`..`表示上一级目录
`/`表示路径起点——绝对路径的标志,通常为当前运行脚本所处的位置。
-
所以在使用resolve的时候要注意。
-
context配置——entry的根目录
//只看
{
context: path.join(__dirname, 'app'),
entry: "entry",
}
-
我们也可以通过context来定义entry的根目录,这也同时定义了后边
output.pathinfo
、loader项下的reslove(v2版本新改动,可在小书中查看)
的根目录。
如果我们不声明context字段,默认为process.cwd()
cwd()
是当前执行node命令时候的文件夹地址__dirname
是被执行的js 文件的地址 -
小结语
对于我们的FlyBird项目来说,显然仅仅是一个单页面应用,只需要一个入口,所以entry设置极其简单
//没啥变化
module.exports = {
entry: path.join(__dirname, '/app/main.js'),
output: {
path: path.join(__dirname, '/build'),
filename: "bundle.js"
}
};
基本配置属性介绍(二)output
1.基本概念
output规定了如何将打包后的一堆堆的东西,写在磁盘里。
它的内容真的非常多,我会挑出之后要用的,具体说说;想了解具体的,请看小书
2.各种问题
-
output.filename多个chunk输出?
也就是上边entry是对象情况,我们必须保证输出的名字唯一性。每个chunk都有一些属性来帮助我们达到目的-
[name]
最简单,就是在entry里边的属性名 -
[hash]
is replaced by the hash of the compilation -
[chunkhash]
is replaced by the hash of the chunk
后俩不明白?他们牵扯到了缓存,我会在下文做简述,并且之后会专门写一篇关于webpack缓存的总结
了解更多,请移步这里——hash-chunkhash的理解,区别。(这篇文章从概念上讲解了hash和chunkhash,在下文的缓存简述里,我会做进一步的说明)
2017/2/25 21:02更新——照现在这进度,想写缓存总结不知到猴年马月了,您还是看看下边的文章吧(下文简述已经写好了)
-
-
filename与chunkFIlename区别?
请移步——filename与chunkFIlename区别 -
output.path与output.publicPath?
output.path值为输出目录的绝对路径,也可用[hash]
output.publicPath项则被许多Webpack的插件用于在生产模式下更新内嵌到css、html文件里的url值,在热加载模块应当关注它,必须通过这个属性来告诉热加载模块去哪加载
关于他俩的区别的具体解释,请移步path与publicPath(往下翻,第4条,当然前边也可以看看)
3.小结语
好了,关于output常用就这几个。忍不住吐槽下,它属性真的太多了,还大部分不知道有啥用(想了解更多,去看小书哦)
到此我们已经完成了最简单的webpack.config.js的配置,通过这个配置webpack可以将main.js打包成bundle.js
下面让我们来试一试是否有效
~~
※配置webpack-dev-server这个服务器工具
webpack-dev-server可以让浏览器实时刷新,显示我们对文件的改动,不如趁着验证main.js和bundle.js的过程,也一并尝试一下。
webpack-dev-server是一个小型的node.js Express服务器,为webpack打包生成的资源文件提供Web服务。webpack-dev-server发送关于编译状态的消息到客户端,客户端根据消息作出响应。
——想看更多可以浏览这个webpack-dev-server解读
~
如果你不希望使用webpack-dev-server来启动服务,(要么是你有服务器了,要么是你想知道不用它怎么启动服务),详情请参考
小书——开发(如果你有服务器,请翻到此链接最后,查看webpack-dev-middleware,本文不会对这种情况做过多讨论)
1.安装
这个功能是一个独立的模块实现的,所以我们首先要安装这个模块
npm install --save-dev webpack-dev-server
默认情况下,它将在当前文件夹下启动一个websocket服务,端口号为8080
两种方法配置服务(选择其一)
1.配置文件 ——Node API
2.cmd指令(推荐)
- 配置文件(Node API)
// webpack.config.js加入
//只看
devServer: {
port: 8080 //设置监听端口(默认的就是8080)
contentBase: "./build",//本地服务器所加载的页面所在的目录
colors: true,//终端中输出结果为彩色
historyApiFallback: true,//不跳转,用于开发单页面应用,依赖于HTML5 history API 设置为true点击链接还是指向index.html
}
如果你需要以server.js的形式写出相关配置,也不是什么难事
const WebpackDevServer = require('webpack-dev-server');
const webpack = require('webpack');
const config = require('./webpack.config.js');
const path = require('path');
const compiler = webpack(config);
const server = new WebpackDevServer(compiler, {
contentBase: 'www',
hot: true,
filename: 'bundle.js',
publicPath: '/',
stats: {
colors: true,
},
});
server.listen(8080, 'localhost', function() {});
- 关于如何以server.js方式定义配置,将会在文章最后给出模板链接,已经有前辈做好了一切(伸手就有,感觉真好!上边代码里不认识的不要紧,)
-
cmd指令
webpack-dev-server --devtool eval-source-map --progress --colors --content-base ./build
// --代表一个指令,与上边各个属性对应
// 此处展示的是经常会在网上看到的写法,并非我的写法
注意:
有一些在前边出现的属性,并没有被对应的写在cmd指令中。事实上,大多数配置都有两种写法,比如:
historyApiFallback: true对应--history-api-fallback
port:8080对应--port 8080
这些并不是我介绍的重点,如果你想要了解更多细节,请移步
小书api章节
如果你希望看到一些具体例子,请移步
webpack-github的examples文件夹
如果我们使用cmd指令,每次都打那么长,绝对要疯,所以~~~
//动手写
// package.json
"scripts": {
"build": "webpack",
"dev": "webpack-dev-server --devtool eval-source-map --progress --colors --content-base ./build"
},
目前,最简单的配置已经完备。我们有了一个服务器,它的端口号默认是8080
让我们写一些具体的东西,测试一下它的效果
- 首先在app文件夹下新建main.js
它长这样
// 新建main.js
document.write('我');
里边的内容自然你随便写。
现在启动服务器
npm run dev
就可以在浏览器访问通过
localhost:8080
访问,此时,我们在本地做出更改,然后浏览器将自动刷新,即可看到效果。我们的页面整个都被刷新了,这显然是不高效的,为什么?
假如工程非常的庞大(实际开发中,往往都是‘非常庞大’),页面像我这篇文章一样,很长,,,修改一个很小的地方,如果页面整个刷新,第一,需要等待页面刷新完,很难过,第二,刷新出来,咱还得滚动半天到咱们修改的地方看效果。等等。都是不高效的。
所以我们需要另一个功能
- 热加载
然而实现这个功能的过程中会遇到很多问题,接下来我将通过——问与答逐个说明
问题一:自动刷新功能的两种模式?以及如何配置?
这个是个用来承上启下的问题,首先,我们要更深入的了解一下自动刷新功能。
1.两种模式
webpack-dev-server自带就有自动刷新功能,而且它是有两种启动模式的
-
iframe模式
在iframe模式下:页面是嵌套在一个iframe下的,在代码发生改动的时候,这个iframe会重新加载
关于iframe是什么请自行百度,这不是我的重点。
-
inline模式
在inline模式下:一个小型的webpack-dev-server客户端会作为入口文件打包,这个客户端会在后端代码改变的时候刷新页面
什么叫作为入口文件打包?它其实类似这样,请看
entry: [
'webpack-dev-server/client?http://0.0.0.0:9090',//资源服务器地址
'webpack/hot/only-dev-server',
path.resolve(__dirname, "app")
]
// 数组第一项就是那个小型服务器
-
这样的写法,网上webpack文章上经常见,联想一下上文介绍的entry的数组值的意思。没错,inline模式就是做了这样的事情。
关于数组第二项:它是一个api,然而它只有node api的写法,没有cli的写法。在浏览小书的api章节,你会发现一些类似的。
hotOnly: true
那么我们还可以像上边那样写,也就是手动的启动了这个服务。(揪住此功能的实体,把他强制打包进来)。
或许你在网上还见过另一种方式,通过index.html在bundle.js之前插入它。其实道理是一样的。
2.配置
webpack-dev-server默认开启inline模式,请看: