《晋级篇(续):复杂项目下的代码分割》

目标

咱们在lesson4的基础上接着讲,这一节要解决两个问题:
1、第三方库的引入;
2、复杂项目下的按需加载。

知识点

1、使用extract-text-webpack-plugin打包多个css文件;
2、CommonsChunkPlugin:抽取公共模块;
3、ProvidePlugin:全局调用某模块;
4、require.ensure():按需加载模块。

课程内容

咱们还是以lesson4的demo为准,把lesson4的src开发目录复制到lesson5下,这一次咱们把项目搞得相对复杂一些,虽说现在比较成熟的前端团队都会有自己的ui库,为了方便咱们还是从成熟的bootstrap和font-awesome来切入,需要注意的是要处理好jquery和bootstrap的依赖关系。
首先把index.html中的

  1. <script type="text/javascript" src="http://cdn.bootcss.com/jquery/3.2.0/jquery.min.js"></script>

删除,然后npm安装jquery、bootstrap和font-awesome

  1. npm install jquery bootstrap font-awesome --save

copy以下代码到webpack.config.js中

  1. const path = require('path'),
  2. HtmlWebpackPlugin = require('html-webpack-plugin'),
  3. webpack = require('webpack'),
  4. ExtractTextPlugin = require("extract-text-webpack-plugin"),
  5. OpenBrowserPlugin = require('open-browser-webpack-plugin'),
  6. extractVendor = new ExtractTextPlugin('vendor.css'),
  7. // 抽取bootstrap和font-awesome公共样式
  8. extractStyle = new ExtractTextPlugin('style.css'); // 抽取自定义样式
  9. module.exports = {
  10. entry: process.env.NODE_ENV === 'production' ? {
  11. vendor: ['jquery', 'bootJs'],
  12. app: './webpack.entry'
  13. }: [
  14. 'webpack-dev-server/client?http://localhost:8080',
  15. 'webpack/hot/only-dev-server',
  16. './webpack.entry.js'
  17. ],
  18. output: {
  19. filename: 'bundle.[hash].js',
  20. path: path.resolve(__dirname, './build'),
  21. publicPath: ''
  22. },
  23. context: __dirname,
  24. module: {
  25. rules: [{
  26. test: /\.css/,
  27. use: process.env.NODE_ENV === 'production' ? extractVendor.extract({
  28. fallback: "style-loader",
  29. use: "css-loader?minimize=true"
  30. }) : ['style-loader', 'css-loader?sourceMap']
  31. },
  32. {
  33. test: /\.scss$/,
  34. use: process.env.NODE_ENV === 'production' ? extractStyle.extract({
  35. fallback: "style-loader",
  36. use: ["css-loader", "sass-loader"]
  37. }) : ['style-loader', 'css-loader?sourceMap', 'sass-loader?sourceMap']
  38. },
  39. {
  40. test: /\.(jpg|png)$/,
  41. use: ['url-loader?limit=10000&name=img/[name].[ext]']
  42. },
  43. {
  44. test: /\.html$/,
  45. use: 'html-loader?interpolate=require'
  46. },
  47. {
  48. test: /\.js$/,
  49. exclude: /node_modules/,
  50. use: {
  51. loader: 'babel-loader',
  52. options: {
  53. presets: ['env']
  54. }
  55. }
  56. },
  57. {
  58. test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/,
  59. use: ['file-loader?name=fonts/[name].[ext]']
  60. }]
  61. },
  62. plugins: process.env.NODE_ENV === 'production' ? [
  63. new HtmlWebpackPlugin({
  64. template: './src/index.html',
  65. filename: 'index.html'
  66. }),
  67. extractVendor,
  68. extractStyle,
  69. new webpack.DefinePlugin({
  70. 'NODE_ENV': JSON.stringify(process.env.NODE_ENV)
  71. }),
  72. new webpack.optimize.UglifyJsPlugin({
  73. compress: {
  74. warnings: true
  75. }
  76. }),
  77. // CommonsChunkPlugin可以让我们在几个模块之间抽取出公共部分内容,并且把他们添加到公共的打包模块中
  78. new webpack.optimize.CommonsChunkPlugin({
  79. name: "vendor",
  80. // 模块名
  81. filename: "vendor.js",
  82. // 文件名
  83. minChunks: Infinity,
  84. // 该模块至少被其他模块调用多少次时,才会被打包到公共模块中,这个数字必须大于等于2,当传入Infinity时会马上生成
  85. }),
  86. // ProvidePlugin可以全局引入某个模块,在其他模块不需要再手动引入且可以直接调用,也能解决其他第三方库(像bootstrap)对jquery的依赖
  87. new webpack.ProvidePlugin({
  88. $: 'jquery',
  89. // $ 是jquery的模块输出对象,下面的jQuery也是一样的,在其他模块中可以直接被调用
  90. jQuery: 'jquery'
  91. })
  92. ] : [
  93. new HtmlWebpackPlugin({
  94. template: './src/index.html',
  95. filename: 'index.html'
  96. }),
  97. new webpack.HotModuleReplacementPlugin(),
  98. new webpack.NamedModulesPlugin(),
  99. new webpack.DefinePlugin({
  100. 'NODE_ENV': JSON.stringify(process.env.NODE_ENV)
  101. }),
  102. new OpenBrowserPlugin({
  103. url: 'http://localhost:8080/'
  104. }),
  105. new webpack.ProvidePlugin({
  106. $: 'jquery',
  107. jQuery: 'jquery'
  108. })
  109. ],
  110. devServer: {
  111. contentBase: path.resolve(__dirname, 'src'),
  112. hot: true,
  113. noInfo: false
  114. },
  115. devtool: 'source-map',
  116. resolve: {
  117. extensions: ['.js', '.scss', '.html'],
  118. alias: {
  119. 'jquery': 'jquery/dist/jquery.min.js',
  120. 'bootCss': 'bootstrap/dist/css/bootstrap.css',
  121. 'bootJs': 'bootstrap/dist/js/bootstrap.js',
  122. 'fontAwesome': 'font-awesome/css/font-awesome.css'
  123. }
  124. }
  125. };

copy以下代码到webpack.entry.js

  1. import "bootCss";
  2. import "fontAwesome";
  3. import "bootJs";
  4. const cssAndJsContext = require.context('./src', true, /\.(js|scss)$/i);
  5. cssAndJsContext.keys().forEach((key) => {
  6. cssAndJsContext(key);
  7. });
  8. if (NODE_ENV === 'development') {
  9. const htmlContext = require.context('./src', true, /\.html$/i);
  10. htmlContext.keys().forEach((key) => {
  11. htmlContext(key);
  12. });
  13. }

运行npm build命令,打包后文件如下:
《晋级篇(续):复杂项目下的代码分割》 - 图1
jquery和bootstrap.js被打包在vendor.js,bootstrap.css和font-awesome.css被打包在vendor.css中,本地打开index.html文件,页面显示正常。于此,咱们解决了第一个问题“第三方库的引入”。
随着项目体积越来越大,有些文件我们不需要页面初始化的时候就加载进来,而应该是当用户发生某个操作之后,按需加载。这里我们就需要用到require.ensure()
在webpack.config.js文件中的output属性增加一项参数:chunkFilename,copy以下代码到webpack.config.js

  1. const path = require('path'),
  2. HtmlWebpackPlugin = require('html-webpack-plugin'),
  3. webpack = require('webpack'),
  4. ExtractTextPlugin = require("extract-text-webpack-plugin"),
  5. OpenBrowserPlugin = require('open-browser-webpack-plugin'),
  6. extractVendor = new ExtractTextPlugin('vendor.css'),
  7. // 抽取bootstrap和font-awesome公共样式
  8. extractStyle = new ExtractTextPlugin('style.css'); // 抽取自定义样式
  9. module.exports = {
  10. entry: process.env.NODE_ENV === 'production' ? {
  11. vendor: ['jquery', 'bootJs'],
  12. app: './webpack.entry'
  13. }: [
  14. 'webpack-dev-server/client?http://localhost:8080',
  15. 'webpack/hot/only-dev-server',
  16. './webpack.entry.js'
  17. ],
  18. output: {
  19. filename: 'bundle.[hash].js',
  20. path: path.resolve(__dirname, './build'),
  21. publicPath: '',
  22. chunkFilename: "chunk.[name].[chunkhash].js" // 对于按需加载的模块,都不会写在entry入口文件中,chunkFilename是给这些按需加载模块的命名规则
  23. },
  24. context: __dirname,
  25. module: {
  26. rules: [{
  27. test: /\.css/,
  28. use: process.env.NODE_ENV === 'production' ? extractVendor.extract({
  29. fallback: "style-loader",
  30. use: "css-loader?minimize=true"
  31. }) : ['style-loader', 'css-loader?sourceMap']
  32. },
  33. {
  34. test: /\.scss$/,
  35. use: process.env.NODE_ENV === 'production' ? extractStyle.extract({
  36. fallback: "style-loader",
  37. use: ["css-loader", "sass-loader"]
  38. }) : ['style-loader', 'css-loader?sourceMap', 'sass-loader?sourceMap']
  39. },
  40. {
  41. test: /\.(jpg|png)$/,
  42. use: ['url-loader?limit=10000&name=img/[name].[ext]']
  43. },
  44. {
  45. test: /\.html$/,
  46. use: 'html-loader?interpolate=require'
  47. },
  48. {
  49. test: /\.js$/,
  50. exclude: /node_modules/,
  51. use: {
  52. loader: 'babel-loader',
  53. options: {
  54. presets: ['env']
  55. }
  56. }
  57. },
  58. {
  59. test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/,
  60. use: ['file-loader?name=fonts/[name].[ext]']
  61. }]
  62. },
  63. plugins: process.env.NODE_ENV === 'production' ? [
  64. new HtmlWebpackPlugin({
  65. template: './src/index.html',
  66. filename: 'index.html'
  67. }),
  68. extractVendor,
  69. extractStyle,
  70. new webpack.DefinePlugin({
  71. 'NODE_ENV': JSON.stringify(process.env.NODE_ENV)
  72. }),
  73. new webpack.optimize.UglifyJsPlugin({
  74. compress: {
  75. warnings: true
  76. }
  77. }),
  78. // CommonsChunkPlugin可以让我们在几个模块之间抽取出公共部分内容,并且把他们添加到公共的打包模块中
  79. new webpack.optimize.CommonsChunkPlugin({
  80. name: "vendor",
  81. // 模块名
  82. filename: "vendor.js",
  83. // 文件名
  84. minChunks: Infinity,
  85. // 该模块至少被其他模块调用多少次时,才会被打包到公共模块中,这个数字必须大于等于2,当传入Infinity时会马上生成
  86. }),
  87. // ProvidePlugin可以全局引入某个模块,在其他模块不需要再手动引入且可以直接调用,也能解决其他第三方库(像bootstrap)对jquery的依赖
  88. new webpack.ProvidePlugin({
  89. $: 'jquery',
  90. // $ 是jquery的模块输出对象,下面的jQuery也是一样的,在其他模块中可以直接被调用
  91. jQuery: 'jquery'
  92. })
  93. ] : [
  94. new HtmlWebpackPlugin({
  95. template: './src/index.html',
  96. filename: 'index.html'
  97. }),
  98. new webpack.HotModuleReplacementPlugin(),
  99. new webpack.NamedModulesPlugin(),
  100. new webpack.DefinePlugin({
  101. 'NODE_ENV': JSON.stringify(process.env.NODE_ENV)
  102. }),
  103. new OpenBrowserPlugin({
  104. url: 'http://localhost:8080/'
  105. }),
  106. new webpack.ProvidePlugin({
  107. $: 'jquery',
  108. jQuery: 'jquery'
  109. })
  110. ],
  111. devServer: {
  112. contentBase: path.resolve(__dirname, 'src'),
  113. hot: true,
  114. noInfo: false
  115. },
  116. devtool: 'source-map',
  117. resolve: {
  118. extensions: ['.js', '.scss', '.html'],
  119. alias: {
  120. 'jquery': 'jquery/dist/jquery.min.js',
  121. 'bootCss': 'bootstrap/dist/css/bootstrap.css',
  122. 'bootJs': 'bootstrap/dist/js/bootstrap.js',
  123. 'fontAwesome': 'font-awesome/css/font-awesome.css'
  124. }
  125. }
  126. };

copy以下代码到webpack.entry.js

  1. import "bootCss";
  2. import "fontAwesome";
  3. import "bootJs";
  4. const cssAndJsContext = require.context('./src', true, /[^\/][^abc]\.(js|scss)$/i); // 修改了正则表达式,使a.js,b.js,c.js不被引入
  5. cssAndJsContext.keys().forEach((key) => {
  6. cssAndJsContext(key);
  7. });
  8. if (NODE_ENV === 'development') {
  9. const htmlContext = require.context('./src', true, /\.html$/i);
  10. htmlContext.keys().forEach((key) => {
  11. htmlContext(key);
  12. });
  13. }

在src目录下新建public文件夹,并创建a.js,b.js,c.js三个文件

  1. cd src && mkdir public && cd public
  2. touch a.js b.js c.js

a.js

  1. console.log("I am A");

b.js

  1. console.log("I am B");

c.js

  1. console.log("I am C");

再修改下body.html

  1. <h1 class="body-title">this is body</h1>
  2. <i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
  3. <ul class="body-list">
  4. <li class="body-list-item" id="body-input">你可以使用BannerPlugin给你的每个打包文件加上你的签名<br>webpack教程<br>by kingvid</li>
  5. </ul>
  6. <button id="body-btn">点我</button>

以及修改下body.js

  1. // 这里不再需要再import或require jquery,在webpack.config.js中新增了externals属性,让jquery可以在webpack整个运行环境中被调用
  2. var element = $("#body-input"),
  3. str = element.html(),
  4. progress = 0,
  5. timer = setInterval(() => {
  6. let current = str.substr(progress, 1);
  7. if (current == '<') {
  8. progress = str.indexOf('>', progress) + 1;
  9. } else {
  10. progress++;
  11. }
  12. element.html(str.substring(0, progress) + (progress && 1 ? '_': ''));
  13. if (progress >= str.length) {
  14. clearInterval(timer);
  15. element.html(str.substring(0, progress));
  16. }
  17. },150);
  18. require('../../public/a.js'); // 这里会立即执行,会被打包到bundle.js文件中
  19. $("#body-btn").click(() => {
  20. // require.ensure(dependencies: String[], callback: function(require), chunkName: String)
  21. // dependencies:在执行之前加载完模块依赖
  22. // callback:模块依赖加载完全之后执行该回调函数,require函数传入该回调函数中,供函数内部调用
  23. // chunkName:webpack打包该模块时的生成的文件命名,当有多个require.ensure()使用相同的chunkname时,webpack会把它们统一打包到一个文件中,如果chunkName为空,传回模块id
  24. require.ensure(['../../public/b.js'],
  25. function(require) {
  26. require('../../public/c.js');
  27. // 注意b.js在这里是不会被执行的,它只是被加载了,如果要调用的话,需要执行`require('../../public/b.js')`
  28. },'bc');
  29. });

运行npm start,效果如下:
《晋级篇(续):复杂项目下的代码分割》 - 图2
可以看到a.js一开始就被执行了,c.js知道按钮被点击之后才被加载和执行,b.js只有被加载了没被执行

总结

至此,课程一开始中提到的问题一和问题二已被解决了,在代码分割这块的应用中,咱们可以更多的根据实际项目情况实现按需加载,在前期构建工作做得尽善尽美,使用户浏览页面时候更流畅。