前端的构建打包工具很多,比如grunt,gulp。相信这两者大家应该是耳熟能详的,上手相对简单,而且所需手敲的代码都是比较简单的。然后webpack的出现,让这两者打包工具都有点失宠了。webpack比起前两者打包工具,对于前端程序员JS编程能力的要求还是挺高的。不过需要兼容ie8及以下的小伙伴们,就不要考虑webpack了,他很傲娇地不兼容!
webpack 前期准备
webpack,这是一个组合词“web”+“pack”,web就是网站的意思,“pack”有打包的意思,webpack组合在一起就是网站打包的意思,这个名字相当暴力简单明了啊。webpack这款工具虽然很难学,但是自由度很大,玩转之后有种随心所欲的感觉。
在学习webpack之前,有几个基础的概念:
- JavaScript,如果这个编程能力不过关,比如不清楚ES6的语法,那么webpack学起来有些费力,还是要先去学习基础知识。
- nodejs,关于nodejs的日常用法,还是需要了解的,不然webpack改如何启动,都无从下手。
- CommonJS,这个规范是需要学习下的,webpack的配置文件就是按照这个规则。
如果以上几个技能都具备,那么恭喜我们可以开始webpack的学(求)习(虐)之旅了。
webpack 打包原理
在使用webpack之前,我们需要了解webpack的工作原理。webpack打包出来的JS不仅仅是压缩混淆我们的源文件,而且还对它做了其他的处理。
下面是webpack打包出来的JS文件和源文件:
"./src/index.js"
源文件
let str="index"
console.log(str)
复制代码
- webpack打包后
(function(modules) { // webpackBootstrap
/*此处省略N+1行*/
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({
"./src/index.js":(function(module, exports) {
eval("let str=\"index\"\r\nconsole.log(str)\n\n//# sourceURL=webpack:///./src/index.js?");
})
});
复制代码
是不是感觉本来小巧的JS,一下子变得臃肿了??似乎用webpack没有意义啊!不仅不能忙我压缩文件,还把源文件变胖了。
不要急,我们再看一个例子:
"./src/index.js"
源文件
require("./page1.js")
let str="index"
console.log(str)
复制代码
"./src/page1.js"
源文件
let str="page1"
console.log(str)
复制代码
- webpack打包后
(function(modules) { // webpackBootstrap
/*此处省略N+1行*/
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({
"./src/index.js": (function(module, exports, __webpack_require__) {
eval("__webpack_require__(/*! ./page1.js */ \"./src/page1.js\")\r\nlet str=\"index\"\r\nconsole.log(str)\n\n//# sourceURL=webpack:///./src/index.js?");
}),
"./src/page1.js":(function(module, exports) {
eval("let str=\"page1\"\r\nconsole.log(str)\n\n//# sourceURL=webpack:///./src/page1.js?");
})
});
复制代码
当有模块导入的时候,这个胖JS就展现了他真正的实力。通过__webpack_require__
来实现JS之间导入的功能。相当于我们不再需要用requirejs,seajs此类包管理器管理我们的前端模块了。webpack帮助我们完成了此类工作。是不是突然觉得这个胖JS不胖了。
webpack的打包原理,就是将各个模块变成字符串,存入健值或者数组之中,然后每个模块之间的关系,通过__webpack_require__
这个方法来实现。最后通过eval
这个函数将字符串变成可执行代码。
如果大家对__webpack_require__
的实现原理感兴趣,可以自己打包一个文件,不要压缩混淆,然后研究研究。
对webpack的期许
webpack这个工具,不可能只有打包压缩这个功能吧。既然是前端工具,那么必然要具备以下功能:
- 代码处理,如打包,编译等
- 自动生成HTML文件,比如模板生成页面
- 本地服务器,这个是必备功能,不然无法调试页面
- 自动编译代码,刷新浏览器,这个大家喜欢称之为hot replacement(热替换,热更新),也就是(修改过的)部分更新
那我们逐步来了解下webpack这些功能该如何实现。
webpack从0开始
如果你之前并未使用过webpack,那么就需要安装一下webpack,顺便学习下如何启动webpack。
STEP 1 INSTALL
webpack从4开始,webpack分成了两个包一个webpack一个webpack-cli,所以安装的时候要安装两个包,以及这个包我们是工具,非网站所依赖的包,所以记得放在开发依赖包之中。
npm install webpack webpack-cli -save-dev
复制代码
也许我们想可以直接安装webpack,不要webpack-cli。但是现实很残酷,如果没有安装CLI,系统就会告诉你,cli是必不可少的,不然webpack就罢工了。
One CLI for webpack must be installed.
STEP2 RUN
安装好了之后,我们应该怎么运行呢?这里有两个途径:
- npm v8.5以上有一个操作叫做
npx
,这个是干嘛的呢,是帮忙我们直接执行.bin,目录下的文件。node_modules\.bin\webpack.cmd
在这个路径下有webpack的执行命令,我们可以打开看看。当我们npx webpack
的时候,就是运行了这个文件。 - 通过配置
package.json
来运行文件,有个字段叫做scripts
,我们加一个start
,然后后面跟上命令。到时候我们呼唤npm start
就要可以运行webpack了。
"scripts": {
"start": "webpack --config webpack.config.js"
}
复制代码
webpack4开始支持零配置,也就是说我不用写webpack.config.js
也可以运行。那我们就运行试试,结果出现了一个警告:
WARNING in configuration The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment. You can also set it to 'none' to disable any default behavior. Learn more: webpack.js.org/concepts/mo…
这个警告就是告诉我们,webpack4中的mode
参数默认是production
,所以如果是development
的情况就一定要配置了。感觉是零配置似乎是非常牛逼的一个操作,但是实际上还是需要手动配置的,因为这个零配置只是帮我们做掉了一些简单的事,比如线上就压缩JS,开发版就不压缩JS,还有一些默认的路径之类的。实际上开发的时候,默认的路径肯定是不够用的。我们还是老老实实写配置吧。
我们配置一下,并且运行一下,在开发环境下打包,生成了一个/dist/main.js
文件。奇怪我的html文件怎么没有打包过来?对,HTML文件需要我们自己在dist之中创建的,也就是/dist/index.html
。并且路径要写好即将生成的JS链接。比如/dist/main.js
在html中引入,我就需要写成<script src="./main.js"></script>
module.exports = {
mode:"development",
};
复制代码
这个配置文件,大家都没有觉得写法很熟悉?对!就是CommonJs规范!下一节会详细解释webpack.config.js
该如何配置。
webpack的心脏——webpack.config.js
webpack的一切操作都配置在webpack.config.js
之中,可以说配好webpack.config.js
,我们就可以坐等新鲜出炉的网站了。
- Entry:切入点,也就是JS程序入口,按照这个入口开始创建模块依赖,默认
./src/index.js
。 - Output:输出口,打包程序的安放位置,默认
./dist/main.js
。 - Loaders:加载器,将除了JS和JSON以外的文件解析加载,比如txt,css等等。
- Plugins:插件,可以做一些更加牛逼的效果,一般要new一个插件对象。
- Mode(新增):
production
和development
,这个是webpack4新增的一个属性,用于区分开发版与线上版,也是很贴心的设置了。
Entry&Output,以及chunk的概念
在学些webpack的配置之前,我们最先接触的就是输入Entry和输出Output的配置。这里需要引入一个chunk的概念,我们在配置Entry的时候,有时候会设置好多个入口,这每一个入口都是一个chunk,也就是说chunk是根据Entry的配置而来的。大家一定要区分清楚chunk和module的概念啊。module就是编程的时候,我们所写的一块一块的功能块,然后模块之间会有依赖。然后chunk只是将当前模块和他的依赖模块,一起打包起来的代码块。
配置Entry,切入点JS入口也不是件容易的事。
Entry配置
- 单一入口,单个文件。整个程序只有一个JS,这个配置就很简单了,我么也可以不配置,因为默认
./src/index.js
。单个文件之间传入字符串即可。
entry: '需要打包的JS的相对或者绝对地址'
复制代码
- 单一入口,多个文件。有时候我们有好多独立的JS文件,但是我只想导出一个JS,这个时候就需要传入数组了。
entry: ["待打包JS-1","待打包JS-2"]
复制代码
- 多个入口,单个文件。这个时候我们就要配置健值了,都是默认值,怎么识别谁是谁。一般来说一个HTML只需要一个chunk,也就是一个入口点。所以这个一般用于多张页面的配置。
entry: {
JS1: "待打包JS-1",
JS2: "待打包JS-2"
}
复制代码
- 多个入口,多个文件。前面提到一个HTML只需要一个入口点,所以这里我们可以借鉴数组来完成此操作。
entry: {
JS1: ["待打包JS1-1","待打包JS1-2"],
JS2: ["待打包JS2-1","待打包JS2-2"]
}
复制代码
Output配置
输出口,安放打包好的JS,不配置就打包到默认文件,默认./dist/main.js
。
如果不需要分入口点,整个网站用一个JS。那么配置一个文件名就可以了。
output: {
filename: 'bundle.js',
}
复制代码
需要指定文件夹的操作,就再加一个path字段即可。
output: {
filename: 'bundle.js',
path: __dirname + '/dist'
}
复制代码
然而现实中,我们不可能只有一个JS,所以这个时候我们就需要配置多个输出口,不过这个不像entry可以配置健值。但是有一个很简便的办法filename: '[name].js'
,文件名我们用[name]
,这样打包出来的Js文件就会按照Entry配置的chunk名,来命名了。
当然我们经常回碰到CDN的问题,一个JS会被缓住,这时候我们可以用[hash]
这个参数,来帮我们filename: '[name].[hash].js'
这样每次生成的JS名就不一样了。
LOADER,模块的概念
在webpack中,任何文件都可以变成一个模块,然后被打包到JS之中。但是JS只认识JS,像CSS,或者typescript这类的非标准JS,该如何处理?这个时候Loader就出现了,他帮助webpack将CSS此类文件变成JS可识别的内容然后打包。所有的loader都需要额外下载安装,这里以最常用的CSS为例子,看我们如何将CSS打包到JS之中。
- 安装css-loader这个加载器
npm install --save-dev css-loader
复制代码
关于css-loader的用法,大家可以参考下官网。
- 在webpack中配置。大家不要把loader的配置名写成了loader,他的在webpack中的配置名是
module.rule
。
module: {
rules: [
{
test: /\.css$/,
use: [
{ loader: 'style-loader'},
{ loader: 'css-loader',options: {modules: true}}
]
}
]
}
复制代码
- 添加style-loader
也就是说loader所有的配置都在rules之下。这里我还配置了style-loader,那么我们既然又了css-loader为什么还要style-loader呢?感觉很累赘啊。那么接下来就要说说这两个loader的不同了。
打开styleloader的官网,我们可以发现:
Adds CSS to the DOM by injecting a
<style>
tag
也就是说style-loader就干一件事就是将我们处理好的CSS插入到DOM之中,否则我们的CSS只编译不生效。
如果我们不喜欢内联样式,并且觉得CSS文件没必要编译到JS文件之中,那么我们可以直接引入一个文件。我们可以这样配置。
module: {
rules: [
{
test: /\.css$/,
use: [
{ loader: 'style-loader/url'},
{ loader: "file-loader" }
]
}
]
}
复制代码
利用style-loader/url
和file-loader
来加载文件。这个时候会在我们的生产文件夹下新建一个css文件,然后js中会加载这个新建的css文件的路径。我们无需在页面上配置link,js会帮助我们自动生成一个link,引入我们的css文件。这样我们就不用将css和js打包到一起啦。
PLUGINS,更多优化操作
如果说loader只是对于JS的一个操作,比如将CSS转化到JS之中啦,那么plugins的功能就更加广泛,并不局限加载编译JS,比如HTML文件的操作。
这里有一个我刚开始的遇到的问题,就是:
webpack主要是负责JS的编译管理,那么我的HTML文件呢?难道要我一个个在dist之中创建好吗??
这个时候HTML Webpack Plugin出现啦,这个插件是专门用于创建管理HTML的。
首先是安装npm i --save-dev html-webpack-plugin
,然后是配置webpack:
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode:"development",
plugins: [
new HtmlWebpackPlugin()
]
}
复制代码
一般插件都是创建一个新的实例,然后加入plugins这个数组之中。
然后我们来看看这个HtmlWebpackPlugin插件,这个插件很强大,我们不仅可以控制模版,还可以配置页面内容,像下方这样。
test.html
<body>
<%= htmlWebpackPlugin.options.title %>
</body>
复制代码
webpack.config.js
plugins: [
new HtmlWebpackPlugin(), //生成自动的index.html
new HtmlWebpackPlugin({ // 生成一个test.html
title: 'Custom template using Handlebars',
filename: 'test.html',
template: path.join(__dirname,'src/test.html')
})
],
复制代码
由上述例子可以看出,为了保证插件的灵活性,比如我每个页面的配置不一样,我们就可以new好几个插件来处理我们的html文件。一个实例处理一个页面。
webpack4.0的新特色——mode
MODE有三个参数production
,development
,none
,前两个是有预设的插件,而最后一个则是什么都没有,也就是说设置为none
的话,webpack就是最初的样子,无任何预设,需要从无到有开始配置。
我们来研究下他们之间的配置的区别,首先是两者都有的一个new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development|production") })
,这个是用来让我们可以直接在js中引用"process.env.NODE_ENV"
的值,这样就可以在JS之中通过这个值来区别开发板与先上版本的不同脚本。
编译之前的index.js
console.log(process.env.NODE_ENV)
复制代码
编译之后的index.js
console.log("development")
复制代码
我们可以看到直接将我们的process.env.NODE_ENV
替换成了所以定义的内容。这个小功能可以帮助我们在写业务JS的时候,区分线上版本与开发版本。
development
我们接着看看其他的开发中使用的插件NamedModulesPlugin
和NamedChunksPlugin
,原本我们的webpack并不会给打包的模块加上姓名,一般都是按照序号来,从0开始,然后加载第几个模块。这个对机器来说无所谓,查找载入很快,但是对于人脑来说就是灾难了,所以这个时候给各个module和chunk加上姓名,便于开发的时候查找。
在没有mode的情况下,这些插件需要自己配置,而有了mode之后,我们的配置就可以省略了。
// webpack.development.config.js
module.exports = {
+ mode: 'development'
- devtool: 'eval',
- plugins: [
- new webpack.NamedModulesPlugin(),
- new webpack.NamedChunksPlugin(),
- new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }),
- ]
}
复制代码
production
在线上版本中,我们第一个需要处理的就要混淆&压缩JS了吧。在线上mode中,自带JS混淆压缩,可以说这个功能很方便了。
// webpack.production.config.js
module.exports = {
+ mode: 'production',
- plugins: [
- new UglifyJsPlugin(/* ... */),
- new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
- new webpack.optimize.ModuleConcatenationPlugin(),
- new webpack.NoEmitOnErrorsPlugin()
- ]
}
复制代码
本地服务器&&hot refresh
官方文档一共给出了3中实时编译的方法:--watch
,webpack-dev-server``和
webpack-dev-middleware`
--watch | webpack-dev-server | webpack-dev-middleware | |
---|---|---|---|
实时编译 | yes | yes | yes |
服务器 | no | yes | yes |
hot | no | yes | yes |
代码上手 | 简单 | 中等 | 稍困难 |
--watch
--watch是个好方法,运行之后,会自动给我们编译文件。但是浏览器需要手动刷新才能出现最新的内容。
webpack-dev-server
webpack-dev-server
虽然,可以直接在config中配置参数,但是还是需要安装一下,才可以使用。
npm install --save-dev webpack-dev-server
webpack不产出任何编译后的文件。他只提供内存中的代码,假装是真是的代码。如果你希望在其他目录中读取文件。可以更换publicPath 选项。每次修改都会实时编译。
但是使用webpack-dev-server
,修改文件,并不会实时刷新浏览器,我们需要一些配置才可以。
首先需要在pligns中加入new webpack.HotModuleReplacementPlugin()
。
plugins: [
new webpack.HotModuleReplacementPlugin()
],
devServer: {
contentBase: path.join(__dirname, 'dist'),
publicPath:"/",
},
复制代码
webpack-dev-server
虽然很方便,配置也简单,但是他编译出来的文件与npx webpack
编译出来的并不一样,因此调试起来未必很方便。
webpack-dev-middleware
看见middleware就应该知道这个是一个中间件,用于链接webpack的编译功能和其他nodejs服务器框架的桥梁,这边我们选择express这个框架。
首先是安装这两个包。
npm install --save-dev express webpack-dev-middleware
复制代码
这个比webpack-dev-server
要复杂一些,还需要安装一个express。但是这个的编译的内容是会写入dist文件的,实时更新,完全按照webpack
的编译来。她的原理就是先执行webpack,在更新到服务器上,这样我们访问的就是最新的内容了。
既然是中间件,那么就不是webpack亲生的,就需要在webpack-dev-server
配置的基础上加点料。
我们要在需要监控的入口点加入监控的js,像这样写:
entry:{
index:['webpack-hot-middleware/client?path=/__webpack_hmr&timeout=20000&reload=true',path.join(__dirname,"/src/index.js")],
},
复制代码
接着就是server.js的编写,想要写好这一部分,大家要先学会express,以及express中间件的用法。然后再是将webpack挂载到express之上。
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require('./webpack.config.js');
const compiler = webpack(config);
app.use(webpackDevMiddleware(compiler, {
publicPath: config.output.publicPath
}));
app.use(require("webpack-hot-middleware")(compiler));
app.listen(8080, function () {
console.log('Example app listening on port 8080!\n');
});
复制代码
这样配置虽然麻烦,但是我们能看到实时编译的JS文件,对于网站的整体细节把控会更好。
总结
感觉写了一篇超长的入门文章,列出了webpack的配置用法,以及webpack插件的用法,可以说webpack插件是webpack之魂,扩展了许多其他的功能。还有如何实时编译我们的网站。