前端的构建打包工具很多,比如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就罢工了。

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也可以运行。那我们就运行试试,结果出现了一个警告:

这个警告就是告诉我们,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

显微镜下的webpack4入门-LMLPHP

webpack的一切操作都配置在webpack.config.js之中,可以说配好webpack.config.js,我们就可以坐等新鲜出炉的网站了。

从官方文档来看,webpack一共有5个主要地配置参数

  • Entry:切入点,也就是JS程序入口,按照这个入口开始创建模块依赖,默认./src/index.js
  • Output:输出口,打包程序的安放位置,默认./dist/main.js
  • Loaders:加载器,将除了JS和JSON以外的文件解析加载,比如txt,css等等。
  • Plugins:插件,可以做一些更加牛逼的效果,一般要new一个插件对象。
  • Mode(新增):productiondevelopment,这个是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的官网,我们可以发现:

也就是说style-loader就干一件事就是将我们处理好的CSS插入到DOM之中,否则我们的CSS只编译不生效。

如果我们不喜欢内联样式,并且觉得CSS文件没必要编译到JS文件之中,那么我们可以直接引入一个文件。我们可以这样配置。

module:  { }
,
             { }

利用style-loader/urlfile-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 =  { }

一般插件都是创建一个新的实例,然后加入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有三个参数productiondevelopmentnone,前两个是有预设的插件,而最后一个则是什么都没有,也就是说设置为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

我们接着看看其他的开发中使用的插件NamedModulesPluginNamedChunksPlugin,原本我们的webpack并不会给打包的模块加上姓名,一般都是按照序号来,从0开始,然后加载第几个模块。这个对机器来说无所谓,查找载入很快,但是对于人脑来说就是灾难了,所以这个时候给各个module和chunk加上姓名,便于开发的时候查找。

在没有mode的情况下,这些插件需要自己配置,而有了mode之后,我们的配置就可以省略了。

// webpack.development.config.js
module.exports =  { }

production

在线上版本中,我们第一个需要处理的就要混淆&压缩JS了吧。在线上mode中,自带JS混淆压缩,可以说这个功能很方便了。

// webpack.production.config.js
module.exports =  { }

本地服务器&&hot refresh

官方文档一共给出了3中实时编译的方法:--watch,webpack-dev-server``和webpack-dev-middleware`

--watch

--watch是个好方法,运行之后,会自动给我们编译文件。但是浏览器需要手动刷新才能出现最新的内容。

webpack-dev-server

webpack-dev-server虽然,可以直接在config中配置参数,但是还是需要安装一下,才可以使用。

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

但是使用webpack-dev-server,修改文件,并不会实时刷新浏览器,我们需要一些配置才可以。

首先需要在pligns中加入new webpack.HotModuleReplacementPlugin()

    plugins: [
        new webpack.HotModuleReplacementPlugin()
    ],
    devServer:  { }
,

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: { }
,

接着就是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 ()  { }

这样配置虽然麻烦,但是我们能看到实时编译的JS文件,对于网站的整体细节把控会更好。

总结

感觉写了一篇超长的入门文章,列出了webpack的配置用法,以及webpack插件的用法,可以说webpack插件是webpack之魂,扩展了许多其他的功能。还有如何实时编译我们的网站。

{ }

10-12 14:34