评论

使用webpack打包多页应用

对于传统的多页应用,有着如公共模块的复用、样式兼容性、语法兼容性等等问题,而我们可以通过webpack进行构建,通过预编译的方式赋予多页应用更多的功能,本文将带大家使用webpack去配置多页应用。

引言

对于传统的多页应用,有着诸多比较棘手的问题,如公共模块的复用、样式兼容性、语法兼容性等等问题,而我们可以通过webpack进行构建,通过预编译的方式赋予多页应用更多的功能,如:模块化、代码检查、预处理器、最新语法、模板引擎等,本文将带大家使用webpack去配置多页应用。

多页入口的配置

多页入口的配置,其实就是配置entry属性为一个对象,先来看一个例子:

// webpack.config.js
module.exports = {
  entry: {
    pageOne: './src/pageOne/index.js',
    pageTwo: './src/pageTwo/index.js',
    pageThree: './src/pageThree/index.js'
  }
};

如上配置示例告诉 webpack 需要三个独立分离的依赖图,运行后,输出如下:

$ webpack
Hash: abc3dca0a96fc92ffdf9
Version: webpack 4.32.2
Time: 246ms
Built at: 2019-05-26 11:13:15

    Asset      Size        Chunks       Chunk Names
pageOne.js    952 bytes       0  [emitted]  pageOne
pageThree.js  956 bytes       1  [emitted]  pageThree
pageTwo.js    953 bytes       2  [emitted]  pageTwo

Entrypoint pageOne = pageOne.js
Entrypoint pageTwo = pageTwo.js
Entrypoint pageThree = pageThree.js

[0] ./src/pageOne/index.js 24 bytes {0} [built]
[1] ./src/pageTwo/index.js 24 bytes {2} [built]
[2] ./src/pageThree/index.js 25 bytes {1} [built]

之后可以在dist目录下看到,wepback生成了pageOne.jspageTwo.jspageThree.js这三个文件。

多页入口的配置方式

在多页应用中,一般配置多页入口,有两种配置方式:

  1. 使用配置文件进行配置
    优点:入口文件可以随意放置,只需要配置正确即可
    缺点:每次新增入口都需要先进行配置

  2. 约定一个目录存放入口文件
    优点:约定大于配置,消除了配置成本
    缺点:没有配置文件那么灵活,入口需要放置在约定目录

本文中我们将采用第2种方式进行配置。

首先,约定src/views目录为页面的入口,如下所示:

src
 ├──views
 │    ├──home
 │    │    ├──home.js
 │    │    ├──home.pug            使用 pug 模板引擎
 │    │    └──home.scss           使用 scss 预处理器处理样式
 │    ├──page1
 │    │    ├──page1.js
 │    │    ├──page1.pug           使用 pug 模板引擎
 │    │    └──page1.scss          使用 scss 预处理器处理样式
 │   ...
...

实现收集逻辑

我们会使用glob库去引入这些入口文件,关于glob的用法,请戳GitHub - isaacs/node-glob: glob functionality for node.js

新建build/entry.js文件,内容如下:

// build/entry.js
const path = require('path');
// 此处需要 "npm install glob"
const glob = require('glob');
// 相对于根目录取路径
const resolve = dir => path.join(__dirname, '../', dir);
// 收集的入口
const entry = {};
// 收集入口文件的目录
const filePaths = glob.sync(resolve('src/views/*'));

filePaths.forEach(filePath => {
  // 目录的名称,也就是打包的文件名
  const filename = path.basename(filePath);
  entry[filename] = `${filePath}/${filename}.js`;
});
exports.getEntry = function getEntry() {
    return entry;
};

修改 webpack.config.js文件,内容如下:

// webpack.config.js
const path = require('path');
const { getEntry } = require('./build/entry');
const resolve = dir => path.join(__dirname, dir);
module.exports = {
  entry: getEntry(),
  // 配置输出的路径
  output: {
    // 输出到 dist 目录下
    path: resolve('dist'),
    // 输出到 dist/js 目录下
    filename: 'js/[name].js'
  }
};

实现HTML的生成

要生成HTML文件,我们需要借助html-webpack-plugin插件。
因为我们这里使用了pug模板引擎,所以需要先经过pug-loader进行解析,关于pug更多信息,请戳入门指南 – Pug

修改 build/entry.js文件, 内容如下:

// build/entry.js
...

// 此处需要 "npm install html-webpack-plugin"
const HtmlWebpackPlugin = require('html-webpack-plugin');

...

exports.getEntryHTMLPlugins = function getEntryHTMLPlugins() {
  return Object.keys(entry).map(filename => {
    // 根据入口文件生成 HtmlWebpackPlugin 实例
    // 一个实例对应一个 html 文件
    return new HtmlWebpackPlugin({
      // 模板文件
      template: `${path.dirname(entry[filename])}/${filename}.pug`,
      // 输出的文件名
      filename: `${filename}.html`,
      chunks: [filename]
    });
  });
};

修改 webpack.config.js文件,内容如下:

// webpack.config.js
...

const { getEntry, getEntryHTMLPlugins } = require('./build/entry');
module.exports = {

  ...,

  module: {
    rules: [
      {
        // 此处需要 "npm install pug pug-loader"
        // 配置 pug-loader 去解析转换 pug 文件
        test: /\.(pug|jade)$/,
        loader: 'pug-loader'
      }
    ]
  },
  plugins: [
    ...getEntryHTMLPlugins()
  ]
};

至此,多页入口的配置已经完成。

赋予多页应用更多的功能

入口配置完成之后,我们将完善 webpack 配置,为多页应用添加语法检查、样式预处理器、以及ES6语法转换,这些功能都是通过 loader 去处理的,我们可以简单的把 loader 看成是一个转换器。

webpack 在编译时,会对 import 的模块(文件)进行检查(也就是使用配置中的 rules 里配置的 test 正则去匹配模块),如果某个文件满足 rules 配置的规则,则会使用设置的 loader 去处理这个文件。

添加语法检查

通过 eslint 对编译前的脚本进行检查,修改 webpack.config.js文件,内容如下:

// webpack.config.js
...

module.exports = {
  ...,

  module: {
    rules: [
      ...,
      {
        // 对 js 后缀的文件进行检查
        test: /\.js$/,
        // 此处需要 "npm install eslint eslint-loader"
        loader: 'eslint-loader',
        // 加载器的执行循序,这里设置先执行
        // 也就是检查编译前的代码
        enfore: 'pre',
        exclude: /(node_modules)/
      }
    ]
  },
  ...
};

需要在根目录下创建.eslintrc文件,对 eslint 语法检查的规则进行配置,本文不对 eslint 配置做详细介绍,关于 eslint 的更多配置,请戳Configuring ESLint - ESLint - Pluggable JavaScript linter

ES6语法转换

对于 js 文件,通过 babel 进行转换,修改 webpack.config.js文件,内容如下:

// webpack.config.js
...

module.exports = {
  ...,
  module: {
    rules: [
      ...,
      {
        // 对 js 后缀的文件进行转换
        test: /\.js$/,
        // 此处需要 "npm install babel-loader @babel/core"
        loader: 'babel-loader',
        exclude: /(node_modules)/
      }
    ]
  },
  ...
};

需要在根目录下创建.babelrc文件,对 babel 的转换规则进行配置,本文不对 babel 配置做详细介绍,关于 babel 配置的更多信息,请戳Configure Babel · Babel

样式预处理器

这里样式的 loader 配置和上面两种不太一样,需要链式调用多个 loader,以确保样式能正常运作。修改 webpack.config.js文件,内容如下:

// webpack.config.js
...

// 此处需要 "npm install mini-css-extract-plugin"

// 此插件用于提取 css
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {

  ...,

  module: {
    rules: [
      ...,
      {

         // 对 sass 或 scss 文件进行处理
         test: /\.s(c|a)ss$/,
         use: [
           // 当配置了多个 loader 时,webpack 的处理方式是从后到前的顺序(也就是 sass-loader、postcss-loader、css-loader)
           // 这里先经过了 sass 解析、再通过 postcss 补全样式
           // 然后通过 css-loader 去处理一些 css module 或 @import 等问题
           // 最后通过插件的 loader 把样式提取出来
           MiniCssExtractPlugin.loader,
           // 此处需要 "npm install css-loader postcss-loader sass-loader"
           'css-loader',
           'postcss-loader',
           {
             loader: 'sass-loader',
             options: {
               // 此处需要 "npm install dart-sass"
               // 这里配置 sass 的解析器为 dart-sass,而不是 node-sass,主要是安装方便和处理速度的提升
               implementation: require('dart-sass')
             }
           }
         ]
      }
    ]
  },
  plugins: [
    ...,
    // 初始化插件实例,配置提出来的 css 路径和名称
    new MiniCssExtractPlugin({
      filename: 'css/[name].css'
    })
  ]
  ...
};

之后,我们需要在根目录下创建 postcss 的配置文件 postcss.config.js 去告知 postcss 去做什么样的处理,如下所示:

// postcss.config.js
module.exports = {
  plugins: {
    // 此处需要 "npm install autoprefixer"
    // 配置自动补全插件
    autoprefixer: {
      // 指定补全的浏览器版本,可以有效的减少补全的代码
      browsers: ['> 2%', 'last 5 versions', 'IE >= 10']
    }
  }
};

关于 browsers 匹配到的浏览器,可以戳这里进行查询:browserl.ist: A page to display compatible browsers from a browserslist string.

提取公共 js 代码

在多个页面中,我们可能会在 A 和 B 页面中都引入同一个 js 文件,为了复用这个 js 文件,我们可以通过配置,提取出该文件。

修改 webpack.config.js,内容如下:

// webpack.config.js
module.exports = {
  ...,
  optimization: {
    // 提取异步加载代码至 manifest 块中
    // 当我们代码中有异步加载的文件时,会在该文件内生成异步文件加载的逻辑
    // 当多个文件有异步加载时,每次在该文件生成的代码实际上大部分是相同的(基本上除了 chunkId 和 chunknamne)
    // 所以我们可以提取出相同的逻辑,放到一个文件中进行加载,以减少单个文件大小
    runtimeChunk: : {
      name: 'manifest'
    },
    splitChunks: {
      cacheGroups: {
        // 提取公共的 node_modules 代码至 vendors 块中
        // 通常文件中可能会通过 npm 安装一些资源包,如果有多个文件同时引入了相同的资源包,可以通过这种方式去提取该资源包,以减少单个文件的大小和复用相同的代码
        vendor: {
          name: 'vendors',
          test: /[\\/]node_modules[\\/]/
        }
      }
    }
  }
};

提取了公共的代码块之后,我们还需要去 HtmlWebpackPlugin的实例中引用这些公共的代码块,修改build/entry.js文件,内容如下:

// build/entry.js
...

exports.getEntryHTMLPlugins = function getEntryHTMLPlugins() {
  ...
  return new HtmlWebpackPlugin({
    ...,
    chunks: ['mainfest', 'vendor', filename]
  });
};

开发环境和生产环境的配置

开发环境和生产环境稍有不同,主要区别在于:

  • 生产环境:代码压缩
  • 开发环境:监听文件变动实时编译、soucemap

要针对不同环境进行编译处理,我们可以借助 node 的npm script来区分(通过加载不同的配置文件),在根目录下找到(如果已经有该文件)或新建package.json文件,内容如下:

{
  "scripts": {
    "dev": "webpack --config webpack.dev.conf.js --watch",
    "build": "webpack --config webpack.prod.conf.js"
  }
}

多于多个配置文件,有部分配置可能会重复,我们可以利用webpack-merge库对相同的配置进行合并,以减少不必要的重复配置。

开发环境的配置

根目录下新建webpack.dev.conf.js文件,内容如下:

// webpack.dev.conf.js
// 此处需要 "npm install webpack-merge"
const merge = require('webpack-merge');
const baseConf = require('./webpack.config.js');

// 在原有的基础上扩展 dev 时需要的配置
module.exports = merge(baseConf, {
  // webpack 4 新增的选项,会根据环境添加一些环境需要的配置
  // 如:
  // 添加了 NamedModulesPlugin(把 chunkid 换成 chunkname)
  // 设置 devtool 为 eval(为每个 module 会封装到 eval 里包裹起来执行,并且会在末尾追加注释//@ sourceURL.)
  // 公共代码的提取配置(optimization.splitChunks 选项)等
  
  mode: 'development'
});

关于mode属性的详细信息,请戳这里: https://webpack.js.org/configuration/mode/

生产环境的配置

根目录下新建webpack.prod.conf.js文件,内容如下:

// webpack.prod.conf.js
// 此处需要 "npm install webpack-merge"
const merge = require('webpack-merge');
const baseConf = require('./webpack.config.js');

// 在原有的基础上扩展 dev 时需要的配置
module.exports = merge(baseConf, {
  // 相比于 development,production 主要是添加了对代码的压缩
  mode: 'production'
});

至此,多页应用的配置就完成的差不多了。这就是全部了吗,当然不,如果有需要比如 vue 的单文件组件(SFC)也可以通过 vue-loader 进行配置,还有开发环境的热模块替换、静态资源文件的引入等。

总结

  1. 多页入口其实就是设置entry属性为一个对象,对于这个对象,我们可以通过配置文件或者是约定目录自动匹配的方式进行获取(又或者是直接在webpack配置上写)。

  2. webpack 中的 loader 类似一个转换器,对于满足给定规则的文件会使用相应的 loader 去处理,也就是说,不管什么类型的文件,只要有相应的 loader,就可以处理。

点赞 4
收藏
评论

2 个评论

  • 铭锋科技
    铭锋科技
    2019-08-28

    带上示例源码呀

    2019-08-28
    赞同 1
    回复 1
    • 凡科网
      凡科网
      2019-09-25
      2019-09-25
      回复
  • Shmily林
    Shmily林
    2019-08-29
    6666
    2019-08-29
    赞同
    回复 1
    • 凡科网
      凡科网
      2019-09-25
      多多交流
      2019-09-25
      回复
登录 后发表内容