webpack4学习系列(三):进阶

一、多页面开发

如果公司的产品需要做很多活动,一大堆促销活动,邀请活动,周年活动,每个活动之间没有关联,每个活动是独立的。 那么你们就需要做成多页面应用。
那么多页面还用 react,用 webpack 吗? 当然的,毕竟模块化,组件化,不管是开发还是维护上都是舒服太多了。(用 vue 也可以,看公司技术栈)

核心思路:
1.用 nodejs 遍历 src 目录下的文件,找出多页面的入口 Js 文件,以及对应的 HTML 模板(生成的文件,最终要注入到 html 模板才有意义, 因为不同的活动,模板可能不同,因此做成每个活动一个 html 模板)
2.匹配入口 JS 文件对应的 html 模板,然后用 HtmlWebpackPlugin插件 生成 html,并且注入对应的 js 文件
3.把生成的 html plugins 列表,放到 webpack 的插件配置中

实现目标:

npm run build //打包所有文件
npm run build  demo1 //打包单个项目demo1
npm run build demo1,demo2  //同时打包多个项目
npm run server //开启本地服务

step 1:在webpack.config.base.js文件中找出多页面的入口文件js

const fs = require('fs');
const optimist = require("optimist");
const cateName = optimist.argv.cate;
const entryPath = __dirname + '/src' + '/category/';
let entryObj = {};

if(cateName ===true){
    /*直接输入npm run build打包所有文件*/
    fs.readdirSync(entryPath).forEach((cateName)=>{
        if(cateName !== "index.html"&&cateName!==".DS_Store"){
            entryObj[cateName+'/'+cateName]=entryPath + cateName + "/" + cateName + '.js';
        }
    });
}else if(cateName.indexOf(",")){
    /*一次性打包多个入口文件以逗号分割 如:npm run build demo1,demo2*/
    var cateNameArray = cateName.split(',');
    for(var i=0;i<cateNameArray.length;i++){
        entryObj[cateNameArray[i] + '/' + cateNameArray[i]] = entryPath + cateNameArray[i] + '/' + cateNameArray[i] + '.js';
    }
}else {
    /*打包单个入口文件*/
    entryObj[cateName+"/"+cateName] = entryPath + cateName + '/' + cateName + '.js';
}

let config = {
    mode: 'none',
    entry: entryObj, //多入口文件以对象的形式设置每个入口js
    output: {
        libraryTarget: 'umd',
        path:__dirname +'/dist/',
        filename: "[name].js"  //出口js文件
    },
   devServer: {
        contentBase: "./src",//本地服务器所加载的页面所在的目录
        port:"8080",//设置监听端口
        historyApiFallback: true,//不跳转
        inline: true//源文件变更时,实时刷新
    },
   module:{}
   plugins: [
        new MiniCssExtractPlugin({
            filename: '[name].css'  //css文件单独打包
        })
    ]
}

module.exports = {
    config:config,
    entryObj:entryObj
};

step2:在webpack.config.js文件中匹配入口 JS 文件对应的 html 模板,并通过HtmlWebpackPlugin插件 生成 html且引入相应资源。最终插入到webpack配置中

var {config,entryObj} = require('./webpack.config.base');
var pages = Object.keys(entryObj);
var HTMLWebpackPlugin = require('html-webpack-plugin');

pages.forEach(function (pathname) {
    var template_local = entryObj[pathname].replace('.js',".html");
    var entryName = pathname.split("/")[0];
    var conf = {
        filename: entryName+'/' + entryName + '.html', //生成的html存放路径
        title:entryName,
        template: template_local, //html模板路径
        inject: true, //js插入的位置,true/'head'/'body'/false
        hash: true, //为静态资源生成hash值
        chunks: [pathname],//需要引入的chunk,不配置就会引入所有页面的资源
        minify: { //压缩HTML文件
            removeComments: true, //移除HTML中的注释
            collapseWhitespace: false //删除空白符与换行符
        }
    };
    config.plugins.push(new HTMLWebpackPlugin(conf));
});

module.exports= config;

最终多页面打包效果如图

QQ20190613-163835@2x.png

二、UglifyJsPlugin插件

如果你有良好编程实践,你可能注重代码的可读性,因此你在代码中添加了大量的空白符(制表符、空格、换行符)和注释。在代码变得更漂亮的同时,也使得文件的体积大大增加了。另一方面,为了用户体验(指减少文件体积)而牺牲可读性也不可取,手工这么做的话很繁琐。因此,这儿有一种解决方案供你在项目中选择。

Webpack4中引入了一个新参数:mode。要求总是在配置中指定。如果不指定,会产生一个警告并退而其次的使用默认值,默认值就是production。如果你使用 mode:"production", Webpack将通过UglifyJSPlugin插件对代码进行压缩。而mode:“development”时的开发环境,为了减少打包耗时,往往不对代码进行压缩。
在package.json文件中可以对开发、生产环境的打包命令进行设置

"scripts": {
    "dev": "webpack --mode development --cate",
    "build": "webpack --mode production --cate"
  }

特殊的,在开发环境也可以手动配置UglifyJSPlugin插件,进行压缩

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
{
    mode: 'development',
    optimization: {
        minimize: true,
        minimizer: [
            new UglifyJsPlugin()
        ]
    },
    entry:entryObj,
    output:{},
    devServer:{},
    module:{},
    plugins:{}
}

三、tree shaking

1.是什么?
我们常常碰到这样的案例,需要从某文件中命名导出(某一个或几个变量、函数、对象等),然而这个文件还有许多其它(我们这次并不需要)的导出,webpack会不管三七二十一简单粗暴的将整个模块包含进来,使得我们最终打包的文件里有了许多不需要的垃圾。这就到了tree shaking出手的地方了,因为它能帮助我们干掉那些死代码,大大减少打包的尺寸。

2.使用要求?
1⃣️它依赖于 ES2015 模块系统中的静态结构特性,例如 importexport

因此,必须使用ES6模块,不能使用其它类型的模块如CommonJS之流。如果使用Babel的话,这里有一个小问题,因为Babel的预案(preset)默认会将任何模块类型都转译成CommonJS类型。修正这个问题也很简单,不是在.babelrc文件中就是在webpack.config.js文件中设置modules: false就好了

{
  "presets": [
    [
      "@babel/preset-env",
      {"modules": false}
    ],
    "@babel/preset-react"
  ]
}

2⃣️需要使用UglifyJsPlugin插件,如果在mode:"production"模式,这个插件已经默认添加了,如果在其它模式下,可以手工添加它。

optimization: {
        usedExports: true,
        sideEffects:true,
        minimize: true,
        minimizer:[new UglifyJsPlugin()]
    }

3⃣️在项目 package.json 文件中,添加一个 "sideEffects" 入口。
在一个纯粹的 ESM 模块世界中,识别出哪些文件有副作用很简单。然而,我们的项目无法达到这种纯度,所以,此时有必要向 webpack 的 compiler 提供提示哪些代码是“纯粹部分”。

这种方式是通过 package.json 的 "sideEffects" 属性来实现的。

{
  "name": "your-project",
  "sideEffects": false
}

如同上面提到的,如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false,来告知 webpack,它可以安全地删除未用到的 export 导出。
如果你的代码确实有一些副作用,那么可以改为提供一个数组:

{
  "name": "your-project",
  "sideEffects": [
    "./src/some-side-effectful-file.js",
    "*.css"
  ]
}

四、使用 source map

当 webpack 打包源代码时,可能会很难追踪到错误和警告在源代码中的原始位置。例如,如果将三个源文件(a.js, b.jsc.js)打包到一个 bundle(bundle.js)中,而其中一个源文件包含一个错误,那么堆栈跟踪就会简单地指向到 bundle.js。这并通常没有太多帮助,因为你可能需要准确地知道错误来自于哪个源文件。

为了更容易地追踪错误和警告,JavaScript 提供了 source map 功能,将编译后的代码映射回原始源代码。如果一个错误来自于 b.js,source map 就会明确的告诉你。

source map 有很多不同的选项可用,请务必仔细阅读它们,以便可以根据需要进行配置。

对于本指南,我们使用 inline-source-map 选项,这有助于解释说明我们的目的(仅解释说明,不要用于生产环境):

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

  module.exports = {
    entry: {
      app: './src/index.js',
      print: './src/print.js'
    },
+   devtool: 'inline-source-map',
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Development'
      })
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist')
    }
  };

现在,让我们来做一些调试,在 Test.js 文件中生成一个错误:

import React, {Component} from 'react'

import './Test.less' ;

class Test extends Component{
    render() {
        return (
            <div className="contianer" onClick={()=>{cosnole.error('I get called from print.js!');}}>
                <div className="car car1">1</div>
                <div className="car car2">2</div>
                <div className="car car3">3</div>
                <div className="car car4">4</div>
            </div>
        )}
}

export default Test

在在浏览器打开最终生成的 index.html 文件,点击按钮,并且在控制台查看显示的错误。错误应该如下


error.png

这是非常有帮助的,因为现在我们知道了,所要解决的问题的确切位置。

五、选择一个开发工具

每次要编译代码时,手动运行 npm run build 就会变得很麻烦。

webpack 中有几个不同的选项,可以帮助你在代码发生变化后自动编译代码:

多数场景中,你可能需要使用 webpack-dev-server,但是不妨探讨一下以下的所有选项
1⃣️webpack's Watch Mode
使用观察模式,你可以指示 webpack "watch" 依赖图中的所有文件以进行更改。如果其中一个文件被更新,代码将被重新编译,所以你不必手动运行整个构建。

我们添加一个用于启动 webpack 的观察模式的 npm script 脚本:
package.json

{
    "name": "development",
    "version": "1.0.0",
    "description": "",
    "main": "webpack.config.js",
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
+     "watch": "webpack --watch",
      "build": "webpack"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "devDependencies": {
      "clean-webpack-plugin": "^0.1.16",
      "css-loader": "^0.28.4",
      "csv-loader": "^2.1.1",
      "file-loader": "^0.11.2",
      "html-webpack-plugin": "^2.29.0",
      "style-loader": "^0.18.2",
      "webpack": "^3.0.0",
      "xml-loader": "^1.2.1"
    }
  }

现在,你可以在命令行中运行 npm run watch,就会看到 webpack 编译代码,然而却不会退出命令行。这是因为 script 脚本还在观察文件。现在,修改并保存文件并检查终端窗口。应该可以看到 webpack 自动重新编译修改后的模块!
2⃣️webpack-dev-server
浏览器自动加载页面。如果现在修改和保存任意源文件,web 服务器就会自动重新加载编译后的代码
3⃣️webpack-dev-middleware
webpack-dev-middleware 是一个容器(wrapper),它可以把 webpack 处理后的文件传递给一个服务器(server)。 webpack-dev-server 在内部使用了它,同时,它也可以作为一个单独的包来使用,以便进行更多自定义设置来实现更多的需求。接下来是一个 webpack-dev-middleware 配合 express server 的示例。

npm install --save-dev express webpack-dev-middleware
const path = require('path');
  const HtmlWebpackPlugin = require('html-webpack-plugin');
  const CleanWebpackPlugin = require('clean-webpack-plugin');

  module.exports = {
    entry: {
      app: './src/index.js',
      print: './src/print.js'
    },
    devtool: 'inline-source-map',
    plugins: [
      new CleanWebpackPlugin(['dist']),
      new HtmlWebpackPlugin({
        title: 'Output Management'
      })
    ],
    output: {
      filename: '[name].bundle.js',
      path: path.resolve(__dirname, 'dist'),
+     publicPath: '/'
    }
  };

server.js

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);

// Tell express to use the webpack-dev-middleware and use the webpack.config.js
// configuration file as a base.
app.use(webpackDevMiddleware(compiler, {
  publicPath: config.output.publicPath
}));

// Serve the files on port 3000.
app.listen(3000, function () {
  console.log('Example app listening on port 3000!\n');
});

package.json

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack --mode development --cate",
    "build": "webpack --mode production --cate",
    "server": "webpack-dev-server --open --cate",
    "watch": "webpack --watch --cate",
    "express":"node server/server.js --cate"
  },

现在,在你的终端执行 npm run express.打开浏览器,跳转到 http://localhost:3000,你应该看到你的webpack 应用程序已经运行!

六、生产环境构建

开发环境(development)和生产环境(production)的构建目标差异很大。在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。

虽然,以上我们将生产环境开发环境做了略微区分,但是,请注意,我们还是会遵循不重复原则(Don't repeat yourself - DRY),保留一个“通用”配置。为了将这些配置合并在一起,我们将使用一个名为 webpack-merge 的工具。通过“通用”配置,我们不必在环境特定(environment-specific)的配置中重复代码。

我们先从安装 webpack-merge 开始:

npm install --save-dev webpack-merge

webpack.common.js

+ const path = require('path');
+ const CleanWebpackPlugin = require('clean-webpack-plugin');
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
+
+ module.exports = {
+   entry: {
+     app: './src/index.js'
+   },
+   plugins: [
+     new CleanWebpackPlugin(['dist']),
+     new HtmlWebpackPlugin({
+       title: 'Production'
+     })
+   ],
+   output: {
+     filename: '[name].bundle.js',
+     path: path.resolve(__dirname, 'dist')
+   }
+ };

webpack.dev.js

+ const merge = require('webpack-merge');
+ const common = require('./webpack.common.js');
+
+ module.exports = merge(common, {
+   devtool: 'inline-source-map',
+   devServer: {
+     contentBase: './dist'
+   }
+ });

webpack.prod.js

+ const merge = require('webpack-merge');
+ const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
+ const common = require('./webpack.common.js');
+
+ module.exports = merge(common, {
+   plugins: [
+     new UglifyJSPlugin()
+   ]
+ });

现在,在 webpack.common.js 中,我们设置了 entryoutput 配置,并且在其中引入这两个环境公用的全部插件。在 webpack.dev.js 中,我们为此环境添加了推荐的 devtool(强大的 source map)和简单的 devServer 配置。最后,在 webpack.prod.js 中,我们引入了之前在 tree shaking 指南中介绍过的 UglifyJSPlugin

package.json

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack --config webpack.dev.js --cate",
    "build": "webpack --config webpack.prod.js --cate",
    "server": "webpack-dev-server --open --config webpack.dev.js --cate",
    "watch": "webpack --watch --cate",
    "express":"node server/server.js --cate"
  }

七、外部扩展(externals)

在日常的项目开发中,我们会用到各种第三方库来提高效率,但随之带来的问题就是打包后的js文件体积过大,导致加载时空白页时间过长,给用户的体验太差。

externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。

例如,从 CDN 引入 jQuery,而不是把它打包:

webpack.config.js

module.exports = {
  //...
  externals: {
    jquery: 'jQuery'
  }
};

index.html

<script
  src="https://code.jquery.com/jquery-3.1.0.js"
  integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
  crossorigin="anonymous">
</script>

八、解析(resolve)

module.exports = {
  //...
  resolve: {
        extensions: ['.js', '.jsx'],
        alias: {
            page: srcPath+'/page',
            components: srcPath+'/components',
            images: srcPath+'/images',
            mock: srcPath+'/mock',
            skin:srcPath+'/skin',
            util:srcPath+'/util',
        }
};

1⃣️ resolve.alias
创建 import 或 require 的别名,来确保模块引入变得更简单。
现在,替换「在导入时使用相对路径」这种方式,就像这样:

import {add} from '../../util/util'

你可以这样使用别名:

import {add} from 'util/util'

从而使得引用更简单
2⃣️ resolve.extensions
能够使用户在引入模块时不带扩展

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,233评论 6 495
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,357评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,831评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,313评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,417评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,470评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,482评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,265评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,708评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,997评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,176评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,503评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,150评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,391评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,034评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,063评论 2 352