整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

多页面项目webpack打包实践

在最近的项目开发中,涉及到了多页面的 webpack 打包,以下是我项目过程中的一些踩坑总结。

前言

项目使用了 vue 作为框架来开发前端页面,其中需要开发多个前端页面,包括有登录、进游戏、充值等等。作为vue最佳的打包工具—— webpack,需要将各个页面分别打包到不同模板目录里。

但默认的 vue 项目框架是单页面应用的,并不能达到项目开发的目的。这就需要调整 webpack 的配置来实现多页面的发布处理。

以下是目录结构:

project
├───bin
│   └───vb.js
├───build
│   │   dev.js
│   │   release.js
│   │   webpack.config.base.js
│   │   webpack.config.build.js
│   └───webpack.config.dev.js
│   README.md
│   package.json
└───src
    ├───components
    │   │   count.vue
    │   │   dialog.vue
    │   │   errortips.vue
    │   └───...
    ├───game
    │   │   game.htm
    │   │   game.js
    │   └───game.vue
    ├───login
    │   │   login.htm
    │   │   login.js
    │   └───login.vue
    ├───pay
    │   │   pay_result.htm
    │   │   pay_result.js
    │   │   pay_result.vue
    │   │   pay.htm
    │   │   pay.js
    │   └───pay.vue
    └───...


修改配置前的一些知识

我们知道webpack的核心是一切皆模块,所以它本质上是一个静态模块打包器。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

官网显示的这幅图很形象地描述了这个过程。


从 webpack v4.0.0 开始,webpack 提供了一系列的配置默认项,让开发者可以零配置打包,不再强制要求必须进行繁琐的 webpack 配置,让开发者可以从繁琐的配置文件里抽出,专注应用的开发。但是若你需要有特殊的处理,webpack 仍然可以进行高度可配置来满足你的需求。

在开始前需要了解四个核心概念:

  • 入口(entry):指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。


  • 输出(output):指示 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件


  • loader:让 webpack 能够去处理那些非 JavaScript 文件


  • 插件(plugins):可以让 webpack 执行范围更广的任务。


本篇将会针对这4个核心配置的修改和优化来实现多页面打包。在 webpack4 的版本,还新增了一个 mode 配置项。mode 有两个值:development 或者是 production,用户可以启用相应模式下的 webpack 内置的优化。不同 mode 的区别与默认配置可以参考:https://segmentfault.com/a/1190000013712229


一、入口配置

在单页面应用里,一般在根目录下面会有一个 index.html 文件。它是页面的 html 模板文件。但是在多页面应用里,则会有多个应用模板文件,为了方便管理,可以将不同类的入口文件、逻辑处理和模板文件分别存放在相应的独立目录。若用到了组件,则单独将组件存放在一个目录。

project
└───src
    ├───components
    │   │   count.vue
    │   │   dialog.vue
    │   │   errortips.vue
    │   └───...
    ├───game
    │   │   game.htm
    │   │   game.js
    │   └───game.vue
    ├───login
    │   │   login.htm
    │   │   login.js
    │   └───login.vue
    ├───pay
    │   │   pay_result.htm
    │   │   pay_result.js
    │   │   pay_result.vue
    │   │   pay.htm
    │   │   pay.js
    │   └───pay.vue
    └───...


webpack 的入口配置中是支持多入口的,给 entry 传入对象即可,如下所示:

const config = {
  entry: {
    game: './src/game/game.js',
    login: './src/login/login.js',
    pay: './src/pay/pay.js',
    pay_result: './src/pay/pay_result.js'
  }
};

但这样的配置对于未知页面数量的项目并不友好,若每新增页面都要重新配置和重启程序,显然是不合理的。而我们可以创建一个getEntry()的方法来遍历文件夹来获取入口。

const fs = require('fs');
const glob = require("glob"); 
function getEntry() {
    const entry = {};
    //读取src目录所有page入口
    glob.sync('./src/*/*.js') //获取符合正则的文件数组
        .forEach(function (filePath) {
            var name = filePath.match(/\/src\/(.+)\/*.js/);
            name = name[1];
            //须有配套的模板文件才认为是入口
            if (!fs.existsSync('./src/' + name + '.htm')) {
                return;
            }
            entry[name] = filePath;
        });
    return entry;
};
module.exports = {
  // 多入口
  entry: getEntry(),
}


二、输出配置

输出配置仅需指定一个

const config = {
    output: {
        path: path.join(__projectDir, __setting.distJs),
        publicPath: __setting.domainJs, //自定义变量,用来定义公共静态资源路径
        filename: '[name][hash].js'
    },
};
  • path:目标输出目录的绝对路径
  • publicPath:文件中静态资源的引用路径

https://www.webpackjs.com/configuration/output/#output-publicpath

  • filename:用于输出文件的文件名

https://www.webpackjs.com/configuration/output/#output-filename

在配置中有以下几点需要注意:

  • publicPath

publicPath 是指定在浏览器中所引用的「此输出目录对应的公开 URL」。

简单的例子:

publicPath: "https://cdn.example.com/assets/"

输出到html则变成

<script src="https://cdn.example.com/assets/bundle.js"></script>

这个属性是整个项目共用的静态资源路径,若某个模块需要使用其他的静态资源路径。webpack 提供了__webpack_public_path__来动态设置 publicPath,只需在入口文件的最顶部定义即可。

__webpack_public_path__ = myRuntimePublicPath; // 一定要写在最顶部
  • hash

filename的[hash]是以项目为维度的 hash 值,若输出了多个文件,则文件名都会共用一个 hash 值。

filename的[chunkhash]是以chunk为维度生成的 hash 值,不同入口生成不同的 chunkhash 值。

filename的[contenthash]根据资源内容生成的 hash 值。

通常使用 hash 或 chunkhash,contenthash 通常用于某些特殊场景(官方文档在使用 ExtractTextWebpackPlugin 插件时有使用)。

https://www.webpackjs.com/plugins/extract-text-webpack-plugin/


三、loader配置

由于 webpack 只能理解 JavaScript 和 JSON 文件。而配置 loader 就是让 webpack 能够去处理其他类型的文件,并将它们转换为有效模块。

loader 可以使开发者在 import 或"加载"模块时预处理文件。例如,将内联图像转换为 data URL,或者允许开发者直接在 JavaScript 模块中 import CSS文件

1、js 模块

加载js模块,我们通常是为了引入babel,让其能将ES6的语法转成ES5,让项目能在低版本的浏览器上运行。

js文件需要使用babel的话,引入babel-loader

const config = {
    module: {
        rules: [{
            test: /\.js$/,
            include: [path.resolve(__projectDir, 'src')], //通过include精确指定只处理哪些目录下的文件
            exclude: /node_modules/, //设置哪些目录里的文件不进行处理
            loader: "babel-loader"
        }]
    }
}

但仅仅配置了babel-loader还不够,还需要配置 babel 的环境,需要引入 polyfill。

引入 polyfill 的方式有很多种,根据 vue 官方文档在浏览器兼容性的处理,默认使用的是@vue/babel-preset-app ,它通过@babel/preset-env和browserslist配置来决定项目需要的 polyfill。

https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/babel-preset-app


browserslist

项目根目录创建.browserslist文件

> 1%
last 2 versions

当然,你也可以在package.json文件里添加的browserslist字段来配置。

这个配置的目的是为了指定了项目的目标浏览器的范围,配置的值会被 @babel/preset-env 用来确定需要转译的 JavaScript 特性。

详细的说明可以查阅 https://github.com/browserslist/browserslist,了解如何指定浏览器范围。

Polyfill

项目根目录创建.babelrc文件

{
  "presets": [
    ["@babel/preset-env",
      {
        "modules": false, // 对ES6的模块文件不做转化,以便使用tree shaking、sideEffects等
        "useBuiltIns": "entry", // browserslist环境不支持的所有垫片都导入
        "corejs": {
          "version": 3, // 使用core-js@3
          "proposals": true
        }
      }
    ]
  ]
}

这里特别说下的是配置里的useBuiltIns,可设置的值分别是"usage" | "entry" | false,3个值分别代表:

  • usage 会根据配置的浏览器兼容,以及你代码中用到的 API 来进行 polyfill,实现了按需添加。
  • entry 根据配置的浏览器兼容,引入浏览器不兼容的 polyfill。这时会自动根据 browserslist 替换成浏览器不兼容的所有 polyfill。
  • false 此时不对 polyfill 做操作。如果引入@babel/polyfill,则无视配置的浏览器兼容,引入所有的 polyfill。

项目使用的是"useBuiltIns": "entry",所以需要指定corejs的版本,这里使用的版本是core-js@3,所以我们在 webpack 的入口配置里加上"core-js/stable"和 "regenerator-runtime/runtime"。

function getEntry() {
    const entry = {};
    //读取src目录所有page入口
    glob.sync('./src/*/*.js') //获取符合正则的文件数组
        .forEach(function (filePath) {
            var name = filePath.match(/\/src\/(.+)\/*.js/);
            name = name[1];
            //须有配套的模板文件才认为是入口
            if (!fs.existsSync('./src/' + name + '.htm')) {
                return;
            }
            entry[name] = ["core-js/stable", "regenerator-runtime/runtime", path.join(__projectDir, filePath)];
        });
    return entry;
};


2、css 模块

我们通常使用style-loader和css-loader。css-loader用来处理 js 文件中引入的 css 模块(处理@import和url()),style-loader是将css-loader打包好的css代码以<style>标签的形式插入到 html 文件中。而 webpack 对于 loader 的调用是从右往左的,所以通常是这样配置:

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

我们在项目中还经常会使用 sass 或者 scss。sass 是一种 CSS 的预编译语言。因此 webpack 要将其处理会使用更多 loader。

{
    test: /\.(sc|sa)ss$/,
    use: [{
        loader: 'vue-style-loader'
    }, {
        loader: 'css-loader',
        options: {
            sourceMap: true,
        }
    }, {
        loader: 'postcss-loader',
        options: {
            sourceMap: true
        }
    }, {
        loader: 'sass-loader',
        options: {
            sourceMap: true
        }
    }, {
        loader: 'sass-resources-loader', //组件里面使用全局scss
        options: {
            sourceMap: true,
            resources: [
                path.resolve('./src/public/css/common.scss')
            ]
        }
    }]
}

在使用sass-loader的时候若某个 scss 文件(比如a.scss)@import 了其他 scss 文件(比如b.scss),如果b.scss里的url()的路径是相对路径,在sass-loader处理过后给css-loader处理时就会报错,找不到url()里指定的资源。

这是因为sass-loader处理时,会将 scss 文件里 @import 路径的文件一并合并进来,结合上面的例子就是b.scss会被sass-loader合并进a.scss。

如何解决呢?可以有两个解决方法:

  • 将资源路径改为变量来统一管理
  • 通过 alias 设置路径别名,从而便捷使用绝对路径。注意在scss文件中使用 alias 里定义的路径别名时,需要带上~前缀,否则打包时仍会被识别为普通路径。

在项目中由于还用到了postcss-loader,我们还须要在根目录创建postcss-loader的配置文件postcss.config.js

//自动添加css浏览器前缀
module.exports = {
    plugins: [
        require('autoprefixer')
    ]
}


3、图片等静态资源

对于图片资源的打包,经常会使用file-loader来完成,配置也很简单:

{
  test: /\.(gif|png|jpe?g)$/,
  loader: 'file-loader',
}

打包后,会将图片移动到了 dist 目录下,并将该图片改名为[hash].[ext]格式的图片。开发者也可以根据需要,修改输出的文件名。

但在项目开发过程中,我们会创建很多张图片,这就使得页面在加载是时候会发送很多http请求,当页面图片过多,会影响的页面的性能。所以,这里推荐使用url-loader。

{
    test: /\.(png|jpg|jepg|svg|gif)$/,
    use: [{
        loader: 'url-loader',
        options: {
            limit: 10240, //这里的单位是b
            name: 'image/[name][hash].[ext]' //打包后输出路径
        }
    }]
}

使用url-loader我们可以通过设置limit的值,将文件大小小于某个值的图片打包成base64的形式存放在打包后的 js 中,若超过了这个设定值,默认会使用file-loader(所以虽然代码没有配置 file-loader,但还是需要使用安装file-loader),并且会将配置的选项传递给file-loader。


4、import AMD 模块

有时我们需要在项目里使用一些 AMD 模块或者完全不支持模块化的库。例如移动端经常使用的 zepto。如果我们直接使用 import zepto 的方式引入是会报错的:Uncaught TypeError: Cannot read property 'createElement' of undefined

要使用也很简单,使用script-loader和exports-loader即可:

{
    test: require.resolve('zepto'),
    use: ['exports-loader?window.Zepto','script-loader']
}
  • script-loader 用 eval 的方法将 zepto 在引入的时候执行了一遍,此时 zepto 库已存在于 window.Zepto
  • exports-loader 将传入的 window.Zepto 以 module.exports = window.Zepto 的形式向外暴露接口,使这个模块符合 CommonJS 规范,支持 import 这样我们就可以直接import $ from 'zepto'了,其他 AMD 模块或者其他不支持模块化的库也类似。



四、plugins

webpack 可以使用插件(plugins)来让开发者能够在打包的过程中实现更多功能,插件会在整个构建过程中生效,并执行相关的任务。这里会介绍几个比较实用的插件:

1、mini-css-extract-plugin

在使用style-loader处理后,css 文件会作为模块打包进 js 文件里。若我们想将 js 文件和 css 文件分离。就可以使用mini-css-extract-plugin:

module: {
    rules: [{
        test: /\.css$/,
        use: [{
            loader: MiniCssExtractPlugin.loader
        },
            'css-loader'
        ]
    }]
},
plugins: [
    new MiniCssExtractPlugin({
        filename: 'css/[hash].css'
    })
]


2、copy-webpack-plugin

有时候我们会有一些没经过打包的文件需要复制到我们的生产目录里,copy-webpack-plugin就可以实现这个功能。

plugins: [
    new CopyWebpackPlugin([
        {
            from: { glob: './src/public/*.htm', dot: true },
            to: path.join(__setting.distTpl, 'public','[name].htm')
        }
    ], { copyUnmodified: true })
]


3、html-webpack-plugin

我们前面介绍入口配置的时候会看到只配置了 js 文件,只是因为 webpack 现在入口只支持 js 文件,所以打包输出的也是 js 文件,那如果我们需要将 js 文件引入到 html 里,就需要使用到html-webpack-plugin插件。

html-webpack-plugin在使用的时候,是必须一个入口对应一个配置的,所以我们前面使用了多页面的配置,也需要进行相应的修改,修改后的getEntry方法:

const htmlPluginArray = [];
function getEntry() {
    const entry = {};
    //读取src目录所有page入口
    glob.sync('./src/' + __setting.moduleId + '/*.js')
        .forEach(function (filePath) {
            var name = filePath.match(/\/src\/(.+)\/*.js/);
            name = name[1];
            if (!fs.existsSync(path.join(__projectDir, './src/' + name + '.htm'))) {
                return;
            }
            entry[name] = ["core-js/stable", "regenerator-runtime/runtime", path.join(__projectDir, filePath)];
+           htmlPluginArray.push(new HtmlWebpackPlugin({
+               filename: `${__setting.distTpl}/${name}.htm`,
+               template: './src/' + name + '.htm',
+               inject: 'body',
+               minify: {
+                   removeComments: true,
+                   collapseWhitespace: true
+               },
+               chunks: [name],
+               inlineSource: '.(js|css)'
+           }))
        });
    return entry;
};


// 配置plugin,由于plugins通常使用数组类型来配置,
// 所以可以使用concat方法将配置好的html的数组添加进去。
plugins: [
    new MiniCssExtractPlugin({
        filename: 'css/[hash].css'
    })
].concat(htmlPluginArray),


里面的一些配置是要注意一下的:

  • filename

filename 是配置需要将 html 改成什么名字并输出到哪里的配置。这里配置的的路径是以 output 里配置的path为相对路径的,我们上面 output 配置的是

path: path.join(__projectDir, __setting.distJs)

那最终的html输出路径就是

path.join(__projectDir, __setting.distJs, 
`${__setting.distTpl}/${name}.htm`)
  • minify

是将html里的代码进行压缩。如果 minify 选项设置为 true 或者配置对象 ( true 是 webpack 模式为 production 时的默认值),生成的 HTML 将使用 HTML-minifier压缩代码,更多具体的配置可以看这里minification。

  • 其他

template 生成 filename 文件的模版。重点:与 filename 的路径不同, 当匹配模版路径的时候将会从项目的根路径开始。

inject 制定 webpack 打包的 js css 静态资源插入到 html 的位置。

chunks 指定模板允许添加哪个入口文件。若不配置这个会将所有的入口都添加进来。


4、html-webpack-inline-source-plugin

若我们想将打包好的 js 代码 inline 进 html 的话,就要使用到html-webpack-inline-source-plugin

可以看到上面html-webpack-plugin的配置里有inlineSource: '.(js|css)'

这就是告诉html-webpack-inline-source-plugin需要将打包好的代码 inline 进 html 里,插件需要添加到html-webpack-plugin的配置后

plugins: [
    new MiniCssExtractPlugin({
        filename: 'css/[hash].css'
    })
].concat(htmlPluginArray).concat([
    new HtmlWebpackInlineSourcePlugin()
])

但是html-webpack-inline-source-plugin也仅能将打包后输出的 js 文件引入 html,若你想将 html 码其他使用 script 标签加载的 js 文件或者 style 标签加载的 css 文件也 inline 进 html 里的话,html-webpack-inline-source-plugin并不能实现。从html-webpack-plugin里的 Issues 来看,html-webpack-plugin的作者也无意做这样的事情,但也给出了建议,可以借助html-webpack-plugin插件的 hooks html-webpack-plugin-before-html-processing达到我们需要的效果。


5、自定义插件

上面说到要将外部的静态文件也 inline 进 html,我们可以编写自定义插件,借助html-webpack-plugin插件的 hooks html-webpack-plugin-before-html-processing,再结合inline-source组件来实现我们的功能。

const {
    inlineSource
} = require('inline-source');//加载inline-source组件
//定义方法
function scriptInlineHtml(options) {
    // Configure your plugin with options...
    this.options = options || {};
}


scriptInlineHtml.prototype.apply = function (compiler) {
    let that = this;
    (compiler.hooks ? //判断webpack版本,4.0以上和4.0以下的处理不一样
        compiler.hooks.compilation.tap.bind(compiler.hooks.compilation, 'script-inline-html') :
        compiler.plugin.bind(compiler, 'compilation'))(function (compilation) {
            (compilation.hooks ?
                compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing.tapAsync.bind(compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing, 'script-inline-html') :
                compilation.plugin.bind(compilation, 'html-webpack-plugin-before-html-processing'))(async function (htmlPluginData, callback) {


                    //获取的html内容处理后重新赋值;
                    try {
                        htmlPluginData.html = await inlineSource(htmlPluginData.html, that.options);
                        // Do something with html
                    } catch (err) {
                        // Handle error
                    }
                    //继续执行下个插件
                    callback(null, htmlPluginData);
                });
        });
};
//webpack插件添加
plugins: [
    new MiniCssExtractPlugin({
        filename: 'css/[hash].css'
    })
].concat(htmlPluginArray).concat([
    new scriptInlineHtml(),
    new HtmlWebpackInlineSourcePlugin()
])

使用

<script src="/src/public/js/px2rem.js" inline></script>

这里结合 inline 静态资源,简单介绍了自定义插件的使用,在html-webpack-plugin构建 html 过程中,还提供其他一系列的事件。

Async:

  • html-webpack-plugin-before-html-generation
  • html-webpack-plugin-before-html-processing
  • html-webpack-plugin-alter-asset-tags
  • html-webpack-plugin-after-html-processing
  • html-webpack-plugin-after-emit

Sync:

  • html-webpack-plugin-alter-chunks

这些事件可以让我们在构建 html 的不同阶段里,通过一些处理来达到我们的目的。例如:可以结合smarty.js将使用了 smarty 的模板,引入一些模拟数据后解析成正常的html代码;读取 HTML 文件进行翻译文本的替换,实现页面的多语言化。打包不同皮肤的html文件等等。



五、其他配置

1、resolve

resolve 配置规定了 webpack 如何寻找各个依赖模块。

前面有讲到使用 alias 设置路径别名。在资源引用时,如果资源引用路径太深,又比较常用,我们可以定义路径别名,例如:

resolve: {
    alias: {
        '@': path.resolve(__projectDir, 'src')
    }
}

我们就可以直接在代码中这样引用了:

let backimg = require("@/public/image/common/ico-back.png").default;


2、webpack dev server

webpack-dev-server是开发时的必备利器,它可以在本地起一个简单的 web 服务器,当文件发生变化时,能够实时重新加载。webpack-dev-server的配置也很简单:

devServer: {
    contentBase: __projectDir, //页面的基础目录
    publicPath:'/',
    port: 8080,
    host: '127.0.0.1',
    open: true, //是否运行后自动打开浏览器
    hot: true
}

启动 webpack-dev-server 后,在目标文件夹中是看不到编译后的文件的,实时编译后的文件都保存到了内存当中。

1) HMR

hot设置为 true 是启用 webpack 的 模块热替换( HMR )功能,但这里注意必须要添加插件webpack.HotModuleReplacementPlugin 才能完全启用 HMR

2) publicPath

publicPath 路径下的打包文件可以在浏览器中访问,webpack-dev-server 打包的内容是放在内存中的,并没有实际创建文件,这些打包后的资源对外的的根目录就是 publicPath。

默认 devServer.publicPath 是 '/',所以你的包( bundle )可以通过 http://127.0.0.1:8080/bundle.js 访问。注意:当这里的 publicPath 和 output 的 publicPath 同时设置时,这里的优先级更高。


总结

webpack 的配置能介绍的点其实还有很多,例如开发环境和生产环境进行配置分离;利用浏览器的缓存将公共的模块抽离分开打包;还有很多常用 plugins 插件等等。

这篇文章是以我在开发某个多页面应用项目为例,总结了一些我在webpack配置上的理解。希望能对浏览这篇文章的小伙伴有帮助。



作者:HZH

来源-微信公众号:三七互娱技术团队

出处:https://mp.weixin.qq.com/s/JzZDqe-f_NRMmdxDLXC7tQ

篇我们主要介绍Webpack打包Javascript。当然,除了可以打包Javascript之外,webpack还可以打包html。但是这不是我们本篇的重点。我们可以参考 Webpack HTML 打包介绍——迹忆客

现在让我们扩展我们的项目——webpack-example,并为 entryoutput 属性指定自定义名称。 在 webpack.config.js 中,我们在 plugins 属性之前添加以下内容:

entry: {
  main: path.resolve(__dirname, './src/app.js'),
},
output: {
  filename: '[name].bundle.js',
  path: path.resolve(__dirname, 'deploy')
},

完整代码如下所示

webpack.config.js 文件

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

module.exports = {
    entry: {
        main: path.resolve(__dirname, './src/app.js'),
      },
      output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'deploy')
      },
    plugins: [
        new HtmlWebpackPlugin({
            hash: true,
            title: 'Webpack - 迹忆客(jiyik.com)',
        })
    ],
};

这里我们不使用 html 模板

在这里,我们将入口文件更改为 app.js,并将输出文件夹更改为 deploy 。 我们还稍微调整了生成的包文件的名称。 现在它将以条目的名称(“main”)开头,后跟单词“bundle”和 .js 文件扩展名。

现在我们创建 src/component.js 文件:

src/component.js

export default (text = "Hello, Webpack!!") => {
  const element = document.createElement("h1");

  element.innerHTML = text;

  return element;
};

接下来,我们将现在项目中的 index.js 重命名为 app.js 以此反映我们的更改,并将其内容替换为以下内容:

app.js

import component from './component';

document.body.appendChild(component());

现在让我们运行 webpack,看一下发生了什么

$ npm run dev

> webpack-example@1.0.0 dev /Users/jiyik/workspace/js/webpack-example
> webpack --mode development

asset main.bundle.js 4.33 KiB [emitted] (name: main)
asset index.html 552 bytes [emitted] [compared for emit]
runtime modules 670 bytes 3 modules
cacheable modules 235 bytes
  ./src/app.js 77 bytes [built] [code generated]
  ./src/component.js 158 bytes [built] [code generated]
webpack 5.54.0 compiled successfully in 142 ms

运行之后我们会在项目目录中看到生成了deploy文件夹,其中包含静态html文件和js文件

此时我们在浏览器中运行 deploy/index.html 文件,结果如下:

此外,如果我们检查 index.html 的源代码,我们会看到 script 标签中 src 属性的值更新为 main.bundle.js

此时,我们可以删除 webpack 最初生成的 dist 文件夹,因为我们不再需要它了。


将 ES6 转换成 ES5

接下来我们将了解如何将 ES6 转换为适用于所有浏览器的 ES5 的代码。 让我们从运行以下命令开始:

$ npm run dev -- --devtool inline-source-map

在这里,我运行 webpack 并将 devtool 选项设置为 inline-source-map 以使代码更具可读性。 这样可以更清楚地演示从 ES6 到 ES5 的代码转换。

下面我们打开 main.bundle.js

main.bundle.js 部分代码

/***/ "./src/component.js":
/*!**************************!*\
  !*** ./src/component.js ***!
  \**************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "default": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ });
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ((text = "Hello, Webpack!") => {
    const element = document.createElement("h1");
  
    element.innerHTML = text;
  
    return element;
  });

/***/ })

/******/     });

如您所见,来自 component.js 模块的现代 ES6 特性(箭头函数和 const 声明)默认不会转换为符合 ES5 的代码。 为了让我们的代码在旧浏览器中工作,我们必须添加 Babel 加载器:

$ npm install babel-loader @babel/core @babel/preset-env --save-dev

然后在 webpack.config.js 文件中,在 output 项之后添加 module 项,如下所示

module: {
  rules: [
    {
      test: /\.js$/,
      exclude: /node_modules/,
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    },
  ]
},

当我们为 webpack loader 定义规则时,通常需要定义三个主要属性:

  • test - 它描述了应该转换什么样的文件。
  • exclude - 它定义了不应该从加载器处理的文件。
  • use - 它告诉应该对匹配的模块使用哪个加载器。 在这里,我们还可以设置加载器选项,就像我们刚刚完成的 presets 选项一样。

webpack.config.js

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

module.exports = {
    entry: {
        main: path.resolve(__dirname, './src/app.js'),
    },
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'deploy')
    },
    module: {
        rules: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: {
              loader: 'babel-loader',
              options: {
                presets: ['@babel/preset-env']
              }
            }
          },
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'Webpack - 迹忆客(jiyik.com)',
        })
    ],
};

然后在运行 webpack 看会生成什么样的文件

$ npm run dev -- --devtool inline-source-map

> webpack-example@1.0.0 dev /Users/liuhanzeng/workspace/js/webpack-example
> webpack --mode development "--devtool" "inline-source-map"

asset main.bundle.js 7.02 KiB [emitted] (name: main)
asset index.html 257 bytes [compared for emit]
runtime modules 670 bytes 3 modules
cacheable modules 301 bytes
  ./src/app.js 76 bytes [built] [code generated]
  ./src/component.js 225 bytes [built] [code generated]
webpack 5.54.0 compiled successfully in 1340 ms

这次 main.bundle.js 中的代码:

/***/ "./src/component.js":
/*!**************************!*\
  !*** ./src/component.js ***!
  \**************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "default": () => (__WEBPACK_DEFAULT_EXPORT__)
/* harmony export */ });
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (function () {
  var text = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "Hello, Webpack!";
  var element = document.createElement("h1");
  element.innerHTML = text;
  return element;
});

/***/ })

/******/     });

非常完美。 现在我们可以使用现代 JS 功能(ES6),webpack 将转换我们的代码,以便它可以被旧浏览器执行。

天介绍 webpack 的一个最常用的插件:HTML Webpack Plugin。

说它是使用 webpack 开发前端项目必不可少的插件也不为过,因为它可以自动帮我们将 webpack 打包生成的文件(比如 js 文件、css 文件)嵌入到 html 文件中。

这在生成的文件带有哈希串时尤为有用。

在 webpack 配置文件引入 HtmlWebpackPlugin 插件,然后在 plugins 数组中通过 new HtmlWebpackPlugin() 加入 HtmlWebpackPlugin 实例对象即可。

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

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'app.[contenthash:8].js',
  },
  mode: 'production',
  plugins: [
    new HtmlWebpackPlugin()
  ],
}

我们执行 npx webpack 命令后,webpack 额外给我们生成了一个 dist/index.html 文件。该 html 文件格式化后得到的内容为:

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Webpack App</title>
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <script defer="defer" src="app.c8b961ec13a790ae7d15.js"></script>
</head>
<body></body>
</html>

可以看到将打包好的 app.js 文件被自动嵌入到 head 元素下最后一个子元素位置。

这里打包文件名尾部被添加了内容哈希串,这意味着每次项目的内容发生变化,哈希串的值都不同。

试想下,如果你自己管理 html 文件,每次都要改这个 js 文件名,是要多累,还好有 HtmlWebpackPlugin 帮忙。

当然前面这种只是 HtmlWebpackPlugin 插件的默认用法,我们可以做更具体的定制化。

一些常用的属性

我们需要传入一个配置对象来进行模板渲染定制化。

HtmlWebpackPlugin 的配置非常丰富,不过常用的就几个。

plugins: [
  new HtmlWebpackPlugin({
    title: '前端西瓜哥的博客',
    favicon: 'static/favicon.ico',
  }),
],
  • titile:设置网页标题;
  • filename:生成 html 文件名,默认值为 index/html
  • template:使用自己的模板,这里填这个模板的路径,使用了之后一些配置项就无效了,比如 title;
  • favicon:指定网站图标路径,除了会在 html 上填充 favicon 相关内容,还会将该文件拷贝到打包文件夹下,非常好用;
  • minify:是否压缩 html 文件。不设置时,如果 webpack 的 mode 为 production,就会压缩 html,移除多余的空格和注释之类的。

使用自定义 html 模板

在实际开发中,通常是创建一个 index.html 提供给 HtmlWebpackPlugin 插件作为模板。

这样的话,title 等配置和一些更细碎的内容就可以直接写到 html 上。相比配置,直接在 html 上编辑要更直观些。

我们在根目录创建一个 index.html 作为模板:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>前端西瓜哥</title>
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body></body>
</html>

这样就可以直接在 html 模板上添加 title,以及一些 cdn 形式的第三方库。

webpack.config.js 配置改为:

plugins: [
  new HtmlWebpackPlugin({
    template: 'index.html'
  }),
],

生成的 html 为:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>前端西瓜哥</title>
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  <script defer src="app.d02c9155f73c92f51bf5.js"></script>
</head>
<body></body>
</html>

第三方库建议使用自己本地项目的,会更稳定和安全些,比如上面就建议改为 <script src="static/jquery-3.6.0.min.js"></script>

这里会用到一个 copy-webpack-plugin 插件将一些文件或文件夹拷贝到打包目录下。关于这个插件我会另外专门写一篇文章讲解,这里不展开。

自定义 html 注入变量

webpack 支持通过使用 lodash.template() 的方式注入变量。

我们将模板 html 改为下面这样:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
  <%= htmlWebpackPlugin.options.saySomething %>
</body>
</html>

配置改为:

plugins: [
  new HtmlWebpackPlugin({
    template: 'index.html',
    title: '前端西瓜哥的博客',
    // 下面这个是自定义属性
    saySomething: 'Stay hungry, stay foolish'
  }),
],

将传入给 HtmlWebpackPlugin 的配置属性会成为 htmlWebpackPlugin.options 对象下的属性,嵌入到模板 html 下。

所以这里的生成结果是:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>前端西瓜哥的博客</title>
<script defer src="app.d02c9155f73c92f51bf5.js"></script></head>
<body>
  Stay hungry, stay foolish
</body>
</html>

因为使用了 lodash.template 模板渲染丰富,除了可以嵌入变量的值,还支持判断条件、循环等特性,基本上可以满足我们的绝大多数场景

结尾

HTML Webpack Plugin 是被广泛使用的 webpack 插件,用来将我们打包出来的文件自动嵌入到一个模板 HTML 中。

实际开发中,通常我们会使用自己编写的 html 模板。

我是前端西瓜哥,热衷于分享前端知识,欢迎关注我。