多端多页面项目Webpack打包实践与优化

2019 年 6 月 26 日 IMWeb前端社区

本文由 IMWeb 团队成员 Ciccy 首发于 IMWeb 社区网站 imweb.io。点击阅读原文查看 IMWeb 社区更多精彩文章。

webpack的核心是一切皆模块,所以它其实本质上就是个静态模块打包器。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。官网显示的这幅图很形象地描述了这个过程:

webpack4相比于3做了很多优化,最大的改变就是支持了零配置打包,不再强制要求必须进行繁琐的webpack配置。 webpack4 新增了一个 mode 配置项。Mode 有两个值:development 或者是 production,默认值是 production。webpack4 针对不同的mode提供了不同的默认配置,这对于只希望配置打包出入口,不想深入了解其他配置的开发人员,提供了最基础的打包优化。当然entry,output ,mode这些配置项也都有默认值,mode默认为production。不同mode的区别与默认配置可以参考https://segmentfault.com/a/1190000013712229

那么接下来我们来我们从零开始一步步完成一个完整项目的配置,每部分配置除了会列出基础配置,还会给出一些额外需要注意的事项,也是我在项目中的踩坑总结。

先贴一下项目目录结构:

  
  
    
  1. - src

  2. - common 公用代码库

  3. - pages

  4. - [活动名称]\_[h5|pc]

  5. - index.js

  6. - index.html

一、多页面入口配置

首先我们看看项目的打包入口如何配置: webpack打包入口支持但入口和多入口,但入口文件只限于js文件(据说webpack5在考虑增加HTML文件和CSS文件作为入口)。

多入口时,给entry传入对象即可,如下所示, 其中对象的key值则是入口的name:

  
  
    
  1. const config = {

  2. entry: {

  3. pageOne: './src/pageOne/index.js',

  4. pageTwo: './src/pageTwo/index.js',

  5. pageThree: './src/pageThree/index.js'

  6. }

  7. };

显然,我们的项目页面数量是未知的,将所有页面都枚举在配置里显然是不合理的,所以可以定义 getEntry()方法来遍历指定文件夹获取入口。

  
  
    
  1. const webpack = require("webpack");

  2. const glob = require("glob");


  3. function getEntry() {

  4. const entry = {};

  5. //读取src目录所有page入口

  6. glob.sync('./src/pages/*/*/index.js')

  7. .forEach(function (filePath) {

  8. var name = filePath.match(/\/pages\/(.+)\/index.js/);

  9. name = name[1];

  10. entry[name] = filePath;

  11. });

  12. return entry;

  13. };


  14. module.exports = {

  15. mode: 'development',

  16. // 多入口

  17. entry: getEntry(),

  18. }

二、打包输出配置

无论是单入口还是多入口,都只能指定一个输出配置。我们看看项目的 output配置

  
  
    
  1. output: {

  2. publicPath: CDN.js,

  3. filename: '[name].[chunkhash].js',

  4. chunkFilename: '[name]_[chunkhash].min.js',

  5. path: distDir,

  6. },

  • filename: 输出文件的文件

  • path: 输出文件的绝对路径

  • chunkFilename:非入口打包出的文件名称

  • publicPath: 文件中静态资源的引用路径

通常,dev环境时,不用配置publicPath,此时静态资源的引用路径相对于HTML页面。而生产环境时,把publicPath的值设为CDN的目录路径就可以了。 这里配置有几点需要注意的:

1、动态publicPath

这里说了是多端多页面项目,多端只的就是PC和H5两端,那么这就意味着各端的CDN资源路径是不一样的,所以publicPath值也应该不一样。如何动态设置publicPath呢?

webpack 提供了 __webpack_public_path__来动态设置publicPath,我们在入口文件的最顶部进行定义即可,如下所示 index.js

  
  
    
  1. __webpack_public_path__ = myRuntimePublicPath; // 一定要写在最顶部

2、hash值的区别

hash:以项目为维度生成的hash值,项目全部文件都共用一个hash值 chunkhash: 以chunk为维度生成的hash值,不同入口生成不同的chunkhash值 contenthash: 根据资源内容生成的hash值 一般是用chunkhash,contenthash也有使用场合,比如在mini-css-extract-plugin插件配置使用,后面会详细讲到。

三、loader配置

配置好了输入输出后,我们就需要来配置对模块内容如何进行处理。webpack 只能理解 JavaScript 和 JSON 文件。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效模块。

1、js 模块

需要引入babel的话,我们就需要使用babel-loader

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

  
  
    
  1. {

  2. test: /\.js$/,

  3. loader: 'babel-loader',

  4. include: [path.resolve(rootDir, 'src')],

  5. },

使用babel时需要注意,Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码,如果要使用需要引入polyfill。

引入polyfill 的方式有很多种,这里推荐babel transformtime+ runtime, transform-time的作用是将遇到需要转化的语法时引入polyfill,而 run-time则是提供polyfill, 这样就可以做到按需引入,而不是所有的都打包进去。所以babel的配置如下:

  
  
    
  1. {

  2. "presets": [

  3. [

  4. "env",

  5. {

  6. "browsers": ["last 5 versions", "> 5%", "Android > 4.3"]

  7. }

  8. ],

  9. "stage-2"

  10. ],

  11. "plugins": [

  12. "transform-runtime"

  13. ]

  14. }

2、css 模块

对于css模块,常用的loader有style-loader和css-loader。 css loader用来处理js文件中引入的css模块(处理@import和url()), style-loader是将 css-loader打包好的css代码以 <style>标签的形式插入到html文件中。

这个项目用到了sass和post-css,所以这里还引入了sass-loader和postcss-loader。因为webpack对于loader的调用是从右往左的,所以配置如下:

  
  
    
  1. {

  2. // 增加对 SCSS 文件的支持

  3. test: /\.scss|\.css/,

  4. // SCSS 文件的处理顺序为先 sass-loader 再 css-loader 再 style-loader

  5. use: [

  6. 'style-loader',

  7. {

  8. loader: 'css-loader',

  9. // 给 css-loader 传入配置项

  10. options: {

  11. importLoaders: 2,

  12. },

  13. },

  14. 'postcss-loader',

  15. {

  16. loader: 'sass-loader',

  17. },

  18. ],

  19. },

如果你也使用了sass-loader,有个问题可能需要注意。当你的index.scss里@import了其他scss文件比如a.scss时,如果a.scss里使用了url(),且里面的路径是相对路径,那么在sass-loader 处理过后给css-loader处理时就会报错,找不到url()里指定的资源。这是为什么呢?

实际上,当sass-loader处理时,会将index.scss里@import的A.scss合并进来,最后只输出index.scss。但A.scss里的url()本来是以A.scss写的相对路径,这样合并又不对url()做处理的话,就导致了合并后无法定位到url()里的资源。对于这个问题,有两种解决办法:

  • 1)使用 resolve-url-loader,将 resolve-url-loader设置于 loader 链中的 sass-loader 之前,就可以重写 url。但是这个办法有个问题,那就是 resolve-url-loader不识别scss文件的行内注释语法,即 // 注释,这个问题使得接入一些已存在的公共样式库时会存在问题,目前还在研究是否有其他loader可以解决,大家有较好的解决办法也可以一起讨论。

  • 2)将资源路径改为变量来统一管理

  • 3)通过alias设置路径别名,从而便捷使用绝对路径。注意在scss文件中使用alias里定义的路径别名时,需要带上~前缀,否则打包时仍会被识别为普通路径

3、图片、字体等资源

对于图片等其他资源,我们一般使用file-loader进行处理,它实现的功能很简单:

  • 将要加载的文件复制到指定目录

  • 生成请求文件资源URL 具体配置如下:

  
  
    
  1. {

  2. test: /\.(gif|png|jpe?g|eot|woff|ttf|pdf)$/,

  3. loader: 'file-loader',

  4. },

4、import AMD 模块

尽管webpack既支持commonjs规范也支持AMD规范。但是我们如何通过import 的方式引入AMD 模块或者其他不支持模块化的库呢?

我们项目里使用到了zepto,这里就以zepto为例,在import zepto时会报错

  
  
    
  1. Uncaught TypeError: Cannot read property 'createElement' of undefined

这就是因为zepto只使用了AMD 规范导出模块。解决所有这类问题其实很简单,只需要使用 script-loaderexports-loader即可:

  
  
    
  1. {

  2. test: require.resolve('zepto'),

  3. use: ['exports-loader?window.Zepto','script-loader']

  4. }

  • script-loader 用 eval 的方法将 zepto 在引入的时候执行了一遍,此时 zepto 库已存在于 window.Zepto

  • exports-loader 将传入的 window.Zepto 以 module.exports = window.Zepto 的形式向外暴露接口,使这个模块符合 CommonJS 规范,支持 import 这样我们就可以直接 import$from'zepto'了,其他AMD 模块或者其他不支持模块化的库也类似。

四、plugin 配置

插件机制是webpack的核心之一,插件(Plugins)是用来拓展webpack功能的,它们会在整个构建过程中生效,执行相关的任务。我们一般使用插件来完善我们的构建流程,webpack有许多插件可用,这里只挑两个必备插件来详细说明

1、html-webpack-plugin

前面有说过,目前webpack的打包入口只支持JS文件,所以它打包输出的也是JS文件,那么如何把这个JS文件引入我们的html中去呢,手动引入无法监测到hash值的变化,肯定是不OK的。因此我们就用到了 html-webpack-plugin这个插件,它会将打包好的文件自动引入到指定的html中去,并将html文件输出在指定位置。

html-webpack-plugin使用时,一个实例操作只能一个html,所以对于多页面项目,我们需要创造多个实例,结合前面的getEntry方法,我们可以在遍历得到entry的时候进行实例化,得到htmlPluginArray

  
  
    
  1. const htmlPluginArray= [];


  2. function getEntry() {

  3. const entry = {};

  4. glob.sync('./src/pages/*/*/index.js')

  5. .forEach(function (filePath) {

  6. var name = filePath.match(/\/pages\/(.+)\/index.js/);

  7. name = name[1];

  8. entry[name] = filePath;


  9. // 实例化插件

  10. + htmlPluginArray.push(newHtmlWebpackPlugin({

  11. + filename: './' + name + '/index.html',

  12. + template: './src/pages/' + name + '/index.html',

  13. + }))


  14. });

  15. return entry;

  16. };


  17. // 配置plugin,此处省略其他配置代码

  18. plugins: [

  19. htmlPluginArray

  20. ],

2、mini-css-extract-plugin

前面使用css loader 和 style-loader对css文件进行处理后,css文件被作为模块也打包在了js文件中。实际生产环境,我们当然是希望js文件和css文件分离的,所以这里就可以使用 mini-css-extract-plugin。 具体配置如下:

  
  
    
  1. module: {

  2. rules: [

  3. {

  4. // 增加对 SCSS 文件的支持

  5. test: /\.scss|\.css/,

  6. // SCSS 文件的处理顺序为先 sass-loader 再 css-loader 再 style-loader

  7. use: [

  8. {

  9. + loader: MiniCssExtractPlugin.loader,

  10. + options: {

  11. + publicPath: CDN.css,

  12. },

  13. },

  14. {

  15. loader: 'css-loader',

  16. // 给 css-loader 传入配置项

  17. options: {

  18. importLoaders: 2,

  19. },

  20. },

  21. 'postcss-loader',

  22. {

  23. loader: 'sass-loader',

  24. },

  25. ],

  26. }

  27. ],

  28. },

  29. plugins: [

  30. new MiniCssExtractPlugin({

  31. filename: '[name].[contenthash].css',

  32. chunkFilename: '[name].[contenthash].css',

  33. }),

  34. ],

这里之所以设置为 contenthash,是用来解决抽离css文件后,js文件变化导致的css文件hash值变化的问题

五、其他配置

1、resolve

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

前面讲到的alias就是在这里配置。在资源引用时,如果资源引用路径太深,又比较常用,我们则可以定义路径别名,例如:

  
  
    
  1. alias: {

  2. h5: path.resolve(__dirname, 'src/common/h5/'),

  3. pc: path.resolve(__dirname, 'src/common/pc/'),

  4. }

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

  
  
    
  1. import Utility from 'h5/util';

2、webpack dev server

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

  
  
    
  1. devServer: {

  2. publicPath: '/act/',

  3. port: 8888,

  4. hot: true,

  5. },

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

1) HMR

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

2) publicPath

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

默认 devServer.publicPath 是 '/',所以你的包(bundle)可以通过 http://localhost:8888/bundle.js 访问。当我们要设置具体路径时记得要以 /开头,如上面配置所示,设置了 publicPath:'/act/'后bundle的访问路径则变成了: http://localhost:8888/act/bundle.js注意:当这里的publicPath和output的publicPath同时设置时,这里的优先级更高

3、配置分离

通常,我们本地开发环境和生产环境会采用不同的配置文件,发布上线时,我们会对资源进行压缩、合并等优化,但在本地开发时,为了提高构建速度,方便调试代码,我们则会省去这些优化配置,与此同时,我们更加关注模块热更新、localhost server等等。所以一般会为每个环境编写彼此独立的 webpack 配置,这里项目的webpack配置文件如下,其中webpack.common.js是用来放dev和dist里的公共配置:

这里会用到 webpack-merge工具进行配置的合并。 比如 webpack.common.js内容如下:

  
  
    
  1. module.exports = {

  2. module: {

  3. rules: []

  4. }

  5. };

webpack.dev.js的则可以使用webpack-merge合并配置:

  
  
    
  1. const merge = require('webpack-merge');

  2. const common = require('./webpack.common.js');

  3. module.exports = merge(common, {

  4. devtool: 'inline-source-map',

  5. devServer: {

  6. // dev 配置

  7. }

  8. });

所以我们可以在package.json添加我们的webpack启动命令如下:

  
  
    
  1. "scripts": {

  2. "dist": "cross-env NODE_ENV=production webpack --config webpack.dist.js",

  3. "dev": "webpack-dev-server --config webpack.dev.js",

  4. },

其中, cross-env NODE_ENV=production是用来设置node环境变量,设置环境变量的目的是因为许多库自身会判断当前环境,并在生产环境下做一些优化处理,而用cross-env来设置是为了兼容windows系统。

六、优化

到这里,我们项目已经能起来了,但是作为一名合格的程序猿,我们当然要探索更优实践。webpack有哪些常用的优化措施呢?

1、按需加载

webpack 提供了两种动态加载的语法。第一种,也是推荐选择的方式是,使用符合 ECMAScript 提案 的 import() 语法 来实现动态导入。第二种,则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure。

import() 会返回一个 promise,在代码中所有被import()的模块,都将打成一个单独的包,在浏览器运行到这一行代码时,就会自动请求这个资源,实现动态加载。* 使用import()时应该注意以下几点: *

  • 1)import()时可以通过注释语法import(/chunkName/'qqapi').then()来定义异步加载模块打包出来的chunkName,否则会默认以id作为chunkName

  • 2) 当bundle中已经以同步方式引入模块后,import()将不会再被webpack单独打包出js文件,可以认为是按需加载无效了

2、抽离公共模块

1)一般项目

为了合理利用浏览器缓存,一般会将不常变动的第三方库以及公共代码和业务代码分开打包

所以一般项目的打包策略为:

  • 第三方库打包出vendor(基本不变)

  • 引用两次以上的模块打包出common (变化较少)

  • 业务代码 (常变)

对于分包方式,webpack 4 移除 CommonsChunkPlugin,取而代之的是optimization.splitChunks 让我们看看这里怎么配置:

  
  
    
  1. splitChunks: {

  2. cacheGroups: {

  3. vendor: {

  4. test: /[\\/]node_modules[\\/]/

  5. name: 'vendor',

  6. chunks: 'initial',

  7. priority: 2,

  8. minChunks: 2

  9. },

  10. common: {

  11. test: /.js$/,

  12. name: 'common',

  13. chunks: 'initial',

  14. priority: 1,

  15. minChunks: 2

  16. }

  17. }}

注意抽离出来的代码要在HTML文件里引入

2)多端项目

由于项目包含两端代码,H5\PC部分依赖是独立的,单纯的从项目层面进行公共模块的抽离是不行的。

所以这里得详细设置公共库和代码的匹配规则。比如我们项目PC用的JQ,H5用的zepto,就可以配置

  
  
    
  1. optimization: {

  2. splitChunks: {

  3. cacheGroups: {

  4. h5common: {

  5. test: /zepto/,

  6. name: 'h5common',

  7. chunks: 'initial',

  8. priority: 1,

  9. minChunks: 1,

  10. },

  11. },

  12. },

  13. },

3、优化loader配置

配置loader时,我们可以通过exclude设置哪些目录下的文件不进行处理,通过include精确指定只处理哪些目录下的文件,以此来缩小处理范围,加快构建速度。

  
  
    
  1. module: {

  2. rules: [

  3. {

  4. test: /\.js$/,

  5. use: 'babel-loader',

  6. exclude: /node_modules/,

  7. include: path.resolve(__dirname, 'src')

  8. }

  9. ]

  10. }

4、限制路径解析范围

当我们引用模块时,如果出现import ‘zepto’这样的依赖引入方式,webpack会默认从当前目录往上逐层查找是否有 node_modules,然后在 node_modules下查找是否存在指定依赖。

为了减少搜索范围,我们可以通过设置 resolve.modules来告诉 webpack 解析这类依赖时应该搜索的目录

  
  
    
  1. resolve: {

  2. modules: [path.resolve(rootDir, 'node_modules')],

  3. },

总结

这篇文章以多端多页面项目为例,深入讲解了如何初始化项目webpack配置,这些实践不仅适用于这个项目,对于多页面项目和普通项目也同样适用。



关注我们

IMWeb 团队隶属腾讯公司,是国内最专业的前端团队之一。

我们专注前端领域多年,负责过 QQ 资料、QQ 注册、QQ 群等亿级业务。目前聚焦于在线教育领域,精心打磨 腾讯课堂、企鹅辅导 及 ABCMouse 三大产品。

社区官网

http://imweb.io/

加入我们

https://hr.tencent.com/position_detail.php?id=45616


扫码关注 IMWeb前端社区 公众号,获取最新前端好文

微博、掘金、Github、知乎可搜索 IMWebIMWeb团队 关注我们。


👇点击阅读原文获取更多参考资料

登录查看更多
0

相关内容

Bundle tool for the front-end
【Manning新书】现代Java实战,592页pdf
专知会员服务
99+阅读 · 2020年5月22日
【书籍推荐】简洁的Python编程(Clean Python),附274页pdf
专知会员服务
179+阅读 · 2020年1月1日
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
95+阅读 · 2019年12月4日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
22+阅读 · 2019年11月7日
硬核实践经验 - 企鹅辅导 RN 迁移及优化总结
IMWeb前端社区
5+阅读 · 2019年5月6日
Github项目推荐 | Pytorch TVM 扩展
AI研习社
11+阅读 · 2019年5月5日
Pupy – 全平台远程控制工具
黑白之道
43+阅读 · 2019年4月26日
百度开源项目OpenRASP快速上手指南
黑客技术与网络安全
5+阅读 · 2019年2月12日
React Native 分包哪家强?看这文就够了!
程序人生
13+阅读 · 2019年1月16日
2018年7月份GitHub开源项目排行榜
算法与数据结构
15+阅读 · 2018年8月3日
Python & 机器学习之项目实践 | 赠书
人工智能头条
14+阅读 · 2017年12月26日
开源巨献:阿里巴巴最热门29款开源项目
算法与数据结构
5+阅读 · 2017年7月14日
Arxiv
14+阅读 · 2019年11月26日
Arxiv
9+阅读 · 2019年11月6日
Arxiv
5+阅读 · 2018年4月30日
Arxiv
5+阅读 · 2017年7月23日
VIP会员
相关资讯
硬核实践经验 - 企鹅辅导 RN 迁移及优化总结
IMWeb前端社区
5+阅读 · 2019年5月6日
Github项目推荐 | Pytorch TVM 扩展
AI研习社
11+阅读 · 2019年5月5日
Pupy – 全平台远程控制工具
黑白之道
43+阅读 · 2019年4月26日
百度开源项目OpenRASP快速上手指南
黑客技术与网络安全
5+阅读 · 2019年2月12日
React Native 分包哪家强?看这文就够了!
程序人生
13+阅读 · 2019年1月16日
2018年7月份GitHub开源项目排行榜
算法与数据结构
15+阅读 · 2018年8月3日
Python & 机器学习之项目实践 | 赠书
人工智能头条
14+阅读 · 2017年12月26日
开源巨献:阿里巴巴最热门29款开源项目
算法与数据结构
5+阅读 · 2017年7月14日
Top
微信扫码咨询专知VIP会员